gitlab-ce/spec/support/matchers/invoke_rop_steps.rb

255 lines
9.5 KiB
Ruby

# frozen_string_literal: true
require_relative '../../../lib/gitlab/fp/rop_helpers'
module InvokeRopSteps
private
include Gitlab::Fp::RopHelpers
def add_err_result_for_step(err_result_for_step, err_results_for_steps)
result_type = :err
step_class, returned_message = parse_result_for_step(err_result_for_step, result_type)
err_results_for_steps[step_class] = Result.err(returned_message)
end
def add_ok_result_for_step(ok_result_for_step, ok_results_for_steps)
result_type = :ok
step_class, returned_message = parse_result_for_step(ok_result_for_step, result_type)
ok_results_for_steps[step_class] = Result.ok(returned_message)
end
def parse_result_for_step(result_for_step, result_type)
unless result_for_step[:step_class].is_a?(Class)
raise "'with_#{result_type}_result_for_step' argument entry 'step_class' be of type 'Class'"
end
unless result_for_step[:returned_message].is_a?(RemoteDevelopment::Message)
raise "'with_#{result_type}_result_for_step' argument entry 'returned_message' be a subclass of " \
"'RemoteDevelopment::Messages'"
end
result_for_step => {
step_class: Class => step_class,
returned_message: RemoteDevelopment::Message => returned_message
}
[step_class, returned_message]
end
def validate_rop_steps(rop_steps)
raise "'invoke_rop_steps' argument must be an Array, but was a #{rop_steps.class}" unless rop_steps.is_a?(Array)
rop_steps.each do |expected_rop_step|
unless expected_rop_step.is_a?(Array)
raise "'invoke_rop_steps' argument array entry must be an Array, but was a #{expected_rop_step.class}"
end
unless expected_rop_step.size == 2
raise "'invoke_rop_steps' argument array entry must be an Array of size 2, " \
"but was an Array of size #{expected_rop_step.size}"
end
step_class = expected_rop_step[0]
unless step_class.is_a?(Class)
raise "'invoke_rop_steps' argument array entry first element '#{step_class}' must be a Class " \
"representing a step class, but was a #{step_class.class}"
end
step_action = expected_rop_step[1]
unless step_action.is_a?(Symbol)
raise "'invoke_rop_steps' argument array entry second element '#{step_action}' must be a Symbol, " \
"but was a #{step_action.class}"
end
unless [:map, :and_then].freeze.include?(step_action)
raise "'invoke_rop_steps' argument array entry second element ':#{step_action}' must be either " \
":map or :and_then, but was :#{step_action}"
end
end
end
def validate_main_class(main_class)
raise "'main_class' argument must be a Class, but was a #{main_class.class}" unless main_class.is_a?(Class)
end
def validate_main_class_was_specified_in_chain(main_class)
raise "'from_main_class' chain must be specified on all 'invoke_rop_steps' matchers" unless main_class
end
def validate_context_passed_along_steps(context)
raise "'context_passed_along_steps' argument must be a Hash, but was a #{context.class}" unless context.is_a?(Hash)
end
def validate_context_passed_along_steps_was_specified_in_chain(context)
raise "'context_passed_along_steps' chain must be specified on all 'invoke_rop_steps' matchers" unless context
end
def validate_expected_return_value(expected_return_value)
if expected_return_value.is_a?(Hash) || expected_return_value.is_a?(Result) || expected_return_value < RuntimeError
return
end
raise "'and_return_expected_value' argument must be a Hash,Result or a subclass of RuntimeError, " \
"but was a #{expected_return_value.class}"
end
def validate_expected_return_value_matcher_was_specified_in_chain(expected_return_value_matcher)
return if expected_return_value_matcher
raise "'and_return_expected_value' chain must be specified on all 'invoke_rop_steps' matchers"
end
def build_expected_rop_steps(
rop_steps:,
err_results_for_steps:,
ok_results_for_steps:,
context_passed_along_steps:
)
expected_rop_steps = []
rop_steps.each do |rop_step|
step_class = rop_step[0]
step_action = rop_step[1]
expected_rop_step = {
step_class: step_class,
step_class_method: retrieve_single_public_singleton_method(step_class),
step_action: step_action
}
if err_results_for_steps.key?(step_class)
expected_rop_step[:returned_object] = err_results_for_steps[step_class]
# Currently, only a single error step is supported, so we assign expected_rop_step as the last entry
# in expected_rop_steps, break out of the loop early, and do not add any more steps
expected_rop_steps << expected_rop_step
break
elsif ok_results_for_steps.key?(step_class)
expected_rop_step[:returned_object] = ok_results_for_steps[step_class]
elsif step_action == :and_then
expected_rop_step[:returned_object] = Result.ok(context_passed_along_steps)
elsif step_action == :map
expected_rop_step[:returned_object] = context_passed_along_steps
else
raise "Unexpected internal error when building expected ROP steps: step_action '#{step_action}' is invalid"
end
expected_rop_steps << expected_rop_step
end
expected_rop_steps
end
def setup_mock_expectations_for_steps(steps:, context_passed_along_steps:)
steps.each do |step|
step => {
step_class: Class => step_class,
step_class_method: Symbol => step_class_method,
returned_object: Result | Hash => returned_object
}
set_up_step_class_expectation(
step_class: step_class,
step_class_method: step_class_method,
context_passed_along_steps: context_passed_along_steps,
returned_object: returned_object
)
end
end
def set_up_step_class_expectation(
step_class:,
step_class_method:,
context_passed_along_steps:,
returned_object:
)
expect(step_class).to receive(step_class_method).with(context_passed_along_steps).ordered do
returned_object
end
end
end
RSpec::Matchers.define :invoke_rop_steps do |rop_steps|
include InvokeRopSteps
supports_block_expectations
main_class = nil
context_passed_along_steps = nil
err_results_for_steps = {}
ok_results_for_steps = {}
expected_return_value_matcher = nil
expected_return_value = nil
match do |block|
validate_main_class_was_specified_in_chain(main_class)
validate_context_passed_along_steps_was_specified_in_chain(context_passed_along_steps)
validate_expected_return_value_matcher_was_specified_in_chain(expected_return_value_matcher)
validate_rop_steps(rop_steps)
steps = build_expected_rop_steps(
rop_steps: rop_steps,
err_results_for_steps: err_results_for_steps,
ok_results_for_steps: ok_results_for_steps,
context_passed_along_steps: context_passed_along_steps
)
setup_mock_expectations_for_steps(
steps: steps,
context_passed_along_steps: context_passed_along_steps
)
# noinspection RubyNilAnalysis -- We ensure this is not nil
expected_return_value_matcher.call(block)
end
chain :from_main_class do |clazz|
main_class = clazz
validate_main_class(main_class)
main_class_method = retrieve_single_public_singleton_method(main_class)
expect(main_class).to receive(main_class_method).and_call_original
end
chain :with_context_passed_along_steps do |context|
validate_context_passed_along_steps(context)
# noinspection RubyUnusedLocalVariable -- TODO: open issue and add to https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues
context_passed_along_steps = context
end
chain :with_err_result_for_step do |err_result_for_step|
# For now, only one 'with_err_result_for_step' is allowed, since our current implementation of
# Result does not have any support for "*or*" methods which could continue after an
# error result (e.g. https://doc.rust-lang.org/std/result/enum.Result.html#method.or)
raise "Only one 'with_err_result_for_step' is allowed" unless err_results_for_steps.empty?
# noinspection RubyResolve -- TODO: open issue and add to https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues
add_err_result_for_step(err_result_for_step, err_results_for_steps)
end
chain :with_ok_result_for_step do |ok_result_for_step|
# Even though the OK step is normally only applicable to the last step in a chain, multiple steps
# are allowed to return OK results for other cases, e.g. if there is a `map` with a lambda in the middle
# the chain, which performs some processing on the context passed along the chain.
add_ok_result_for_step(ok_result_for_step, ok_results_for_steps)
end
chain :and_return_expected_value do |value|
validate_expected_return_value(value)
expected_return_value = value
# noinspection RubyUnusedLocalVariable -- TODO: open issue and add to https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues
expected_return_value_matcher = if value.is_a?(Hash) || value.is_a?(Result)
->(main) { expect(main.call).to eq(value) }
else
->(main) { expect { main.call }.to raise_error(value) }
end
end
failure_message do |block|
"expected returned value from #{block} to equal '#{expected_return_value}' " \
"but was '#{block.call}' instead."
end
end