class Roby::Test::ExecutionExpectations
Underlying implementation for Roby's when do end.expect … feature
The expectation's documented return value are NOT the values returned by the method itself, but the value that the user can expect out of the expectation run.
@example execute until a block returns true. The call returns the block's return value
expect_execution.to do achieve { plan.num_tasks } end # => the number of tasks from the plan
@example execute until an event was emitted and an error raised. The call will in this case return the error object and the emitted event
expect_execution.to do event = emit task.start_event error = have_error_matching CodeError [error, event] end # => the pair (raised error, emitted event)
Public Class Methods
# File lib/roby/test/execution_expectations.rb, line 323 def self.format_propagation_info(propagation_info, indent: 0) PP.pp(propagation_info).split("\n").join("\n" + " " * indent) end
# File lib/roby/test/execution_expectations.rb, line 292 def initialize(test, plan) @test = test @plan = plan @expectations = Array.new @execute_blocks = Array.new @scheduler = false @timeout = 5 @join_all_waiting_work = true @wait_until_timeout = true @garbage_collect = false @validate_unexpected_errors = true @display_exceptions = false end
Parse a expect { } block into an Expectation
object
@return [Expectation]
# File lib/roby/test/execution_expectations.rb, line 282 def self.parse(test, plan, &block) new(test, plan).parse(&block) end
Public Instance Methods
Expect that the given block returns true
@yieldparam [ExecutionEngine::PropagationInfo]
all_propagation_info all that happened during the propagations since the beginning of expect_execution block. It contains event emissions and raised/caught errors.
@yieldreturn the value that should be returned by the expectation
# File lib/roby/test/execution_expectations.rb, line 128 def achieve(description: nil, backtrace: caller(1), &block) add_expectation(Achieve.new(block, description, backtrace)) end
Add a new expectation to be run during {#verify}
# File lib/roby/test/execution_expectations.rb, line 499 def add_expectation(expectation) @expectations << expectation expectation end
Expect that the generator(s) become unreachable
@param [Array<EventGenerator>] generators the generators that are
expected to become unreachable
@return [Object,Array<Object>] if only one generator is provided,
its unreachability reason. Otherwise, the unreachability reasons of all the generators, in the same order than the argument
# File lib/roby/test/execution_expectations.rb, line 85 def become_unreachable(*generators, backtrace: caller(1)) return_values = generators.map do |generator| add_expectation(BecomeUnreachable.new(generator, backtrace)) end if return_values.size == 1 return_values.first else return_values end end
Expect that an event is emitted after the expect_execution block
@param [EventGenerator,Queries::EventGeneratorMatcher] generator @return [Event,]
@overload emit(generator)
@param [EventGenerator] generator the generator we're waiting the emission of @return [Event] the emitted event
@overload emit(generator_query)
@param [Queries::EventGeneratorMatcher] query a query that matches the event whose emission we're watching. @return [[Event]] all the events whose generator match the query @example wait for the emission of the start event of any task of model MyTask. The call will return the emitted events that match this. expect_execution.to do emit find_tasks(MyTask).start_event end
# File lib/roby/test/execution_expectations.rb, line 63 def emit(*generators, backtrace: caller(1)) return_values = generators.map do |generator| if generator.kind_of?(EventGenerator) add_expectation(EmitGenerator.new(generator, backtrace)) else add_expectation(EmitGeneratorModel.new(generator, backtrace)) end end if return_values.size == 1 return_values.first else return_values end end
Queue a block for execution
This is meant to be used by expectation objects which require to perform some actions in execution context.
# File lib/roby/test/execution_expectations.rb, line 508 def execute(&block) @execute_blocks << block nil end
Expect that the given task fails to start
@param [Task] task @return [nil]
# File lib/roby/test/execution_expectations.rb, line 136 def fail_to_start(task, reason: nil, backtrace: caller(1)) add_expectation(FailsToStart.new(task, reason, backtrace)) end
Expect that plan objects (task or event) are finalized
@param [Array<PlanObject>] plan_objects @return [nil]
# File lib/roby/test/execution_expectations.rb, line 184 def finalize(*plan_objects, backtrace: caller(1)) plan_objects.each do |plan_object| add_expectation(Finalize.new(plan_object, backtrace)) end nil end
# File lib/roby/test/execution_expectations.rb, line 665 def find_all_unmet_expectations(all_propagation_info) @expectations.find_all do |exp| !exp.update_match(all_propagation_info) end end
# File lib/roby/test/execution_expectations.rb, line 308 def find_tasks(*args) @test.plan.find_tasks(*args) end
Expect that the given task finishes
@param [Task] task @return [Event] the task's stop event
# File lib/roby/test/execution_expectations.rb, line 174 def finish(task, backtrace: caller(1)) emit task.start_event, backtrace: backtrace if !task.running? emit task.stop_event, backtrace: backtrace nil end
Expect that the given promise finishes
@param [Promise] promise @return [nil]
# File lib/roby/test/execution_expectations.rb, line 224 def finish_promise(promise, backtrace: caller(1)) add_expectation(PromiseFinishes.new(promise, backtrace)) nil end
Whether some blocks have been queued for execution with {#execute}
# File lib/roby/test/execution_expectations.rb, line 515 def has_pending_execute_blocks? !@execute_blocks.empty? end
Expect that an error is raised and not caught
@param [#===] matcher an error matching object. These are usually
obtained by calling {Exception.match} on an exception class and then refining the match by using the {Queries::LocalizedErrorMatcher} AP (see example above)I
@return [ExecutionException] the matched exception
@example expect that a {ChildFailedError} is raised from 'task'
expect_execution.to do have_error_matching Roby::ChildFailedError.match. with_origin(task) end
# File lib/roby/test/execution_expectations.rb, line 242 def have_error_matching(matcher, backtrace: caller(1)) add_expectation(HaveErrorMatching.new(matcher, backtrace)) end
Expect that a framework error is added
Framework errors are errors that are raised outside of user code. They are fatal inconsistencies, and cause the whole Roby
instance to quit forcefully
Unlike with {#have_error_matching} and {#have_handled_error_matching}, the error is rarely a LocalizedError
. For simple exceptions, one can simply use the exception class to match.
# File lib/roby/test/execution_expectations.rb, line 273 def have_framework_error_matching(error, backtrace: caller(1)) add_expectation(HaveFrameworkError.new(error, backtrace)) end
Expect that an error is raised and caught
@param [#===] matcher an error matching object. These are usually
obtained by calling {Exception.match} on an exception class and then refining the match by using the {Queries::LocalizedErrorMatcher} API (see example above)
@return [ExecutionException] the matched exception
@example expect that a {ChildFailedError} is raised from 'task' and caught somewhere
expect_execution.to do have_handled_error_matching Roby::ChildFailedError.match. with_origin(task) end
# File lib/roby/test/execution_expectations.rb, line 259 def have_handled_error_matching(matcher, backtrace: caller(1)) add_expectation(HaveHandledErrorMatching.new(matcher, backtrace)) end
Expect that the given task emits its internal_error event
@param [Task] task @return [Event] the emitted internal error event
# File lib/roby/test/execution_expectations.rb, line 206 def have_internal_error(task, original_exception) have_handled_error_matching original_exception.match.with_origin(task) emit task.internal_error_event end
Expect that the given task either starts or is running, and does not stop
The caveats of {#not_emit} apply to the “does not stop” part of the expectation. This should usually be used in conjunction with a synchronization point.
@example task keeps running until action_task stops
expect_execution.to do keep_running task finish action_task end
@param [Task] task @return [nil]
# File lib/roby/test/execution_expectations.rb, line 162 def have_running(task, backtrace: caller(1)) if !task.running? emit task.start_event, backtrace: backtrace end not_emit task.stop_event nil end
Expect that the given block is true during a certain amount of time
@param [Float] at_least_during the minimum duration in seconds. If
zero, the expectations will run at least one execution cycle. The exact duration depends on the other expectations.
@yieldparam [ExecutionEngine::PropagationInfo]
all_propagation_info all that happened during the propagations since the beginning of expect_execution block. It contains event emissions and raised/caught errors.
@yieldreturn [Boolean] expected to be true over duration seconds
# File lib/roby/test/execution_expectations.rb, line 117 def maintain(at_least_during: 0, description: nil, backtrace: caller(1), &block) add_expectation(Maintain.new(at_least_during, block, description, backtrace)) end
# File lib/roby/test/execution_expectations.rb, line 316 def method_missing(m, *args, &block) if @test.respond_to?(m) @test.public_send(m, *args, &block) else super end end
Expect that the generator(s) do not become unreachable
@param [Array<EventGenerator>] generators the generators that are
expected to not become unreachable
# File lib/roby/test/execution_expectations.rb, line 100 def not_become_unreachable(*generators, backtrace: caller(1)) generators.map do |generator| add_expectation(NotBecomeUnreachable.new(generator, backtrace)) end end
Expect that an event is not emitted after the expect_execution block
Note that only one event propagation pass is guaranteed to happen before the “no emission” expectation is validated. I.e. this cannot test for the non-existence of a delayed emission
@return [nil]
# File lib/roby/test/execution_expectations.rb, line 31 def not_emit(*generators, backtrace: caller(1)) generators.each do |generator| if generator.kind_of?(EventGenerator) add_expectation(NotEmitGenerator.new(generator, backtrace)) else add_expectation(NotEmitGeneratorModel.new(generator, backtrace)) end end nil end
Expect that plan objects (task or event) are not finalized
@param [Array<PlanObject>] plan_objects @return [nil]
# File lib/roby/test/execution_expectations.rb, line 195 def not_finalize(*plan_objects, backtrace: caller(1)) plan_objects.each do |plan_object| add_expectation(NotFinalize.new(plan_object, backtrace)) end nil end
# File lib/roby/test/execution_expectations.rb, line 286 def parse(ret: true, &block) block_ret = instance_eval(&block) @return_objects = block_ret if ret self end
Expect that the given task is put in quarantine
@param [Task] task @return [nil]
# File lib/roby/test/execution_expectations.rb, line 215 def quarantine(task, backtrace: caller(1)) add_expectation(Quarantine.new(task, backtrace)) nil end
# File lib/roby/test/execution_expectations.rb, line 312 def respond_to_missing?(m, include_private) @test.respond_to?(m) || super end
Expect that the given task starts
@param [Task] task @return [Event] the task's start event
# File lib/roby/test/execution_expectations.rb, line 144 def start(task, backtrace: caller(1)) emit task.start_event, backtrace: backtrace end
# File lib/roby/test/execution_expectations.rb, line 650 def unexpected_error?(error) @expectations.each do |expectation| if expectation.relates_to_error?(error) return false elsif error.respond_to?(:original_exceptions) error.original_exceptions.each do |orig_e| if expectation.relates_to_error?(orig_e) return false end end end end true end
# File lib/roby/test/execution_expectations.rb, line 628 def validate_has_no_unexpected_error(propagation_info) unexpected_errors = propagation_info.exceptions.find_all do |e| unexpected_error?(e) end unexpected_errors.concat propagation_info.each_framework_error. map(&:first).find_all { |e| unexpected_error?(e) } # Look for internal_error_event, which is how the tasks report # on their internal errors internal_errors = propagation_info.emitted_events.find_all do |ev| if ev.generator.respond_to?(:symbol) && ev.generator.symbol == :internal_error exceptions_context = ev.context.find_all { |obj| obj.kind_of?(Exception) } !exceptions_context.any? { |exception| @expectations.any? { |expectation| expectation.relates_to_error?(ExecutionException.new(exception)) } } end end unexpected_errors += internal_errors.flat_map { |ev| ev.context } if !unexpected_errors.empty? raise UnexpectedErrors.new(unexpected_errors) end end
Verify that executing the given block in event propagation context will cause the expectations to be met
@return [Object] a value or array of value as returned by the
parsed block. If the block returns expectations, they are converted to a user-visible object by calling their #return_object method. Each expectation documents this as their return value (for instance, {#achieve} returns the block's "trueish" value)
# File lib/roby/test/execution_expectations.rb, line 551 def verify(&block) all_propagation_info = ExecutionEngine::PropagationInfo.new timeout_deadline = Time.now + @timeout if block @execute_blocks << block end begin engine = @plan.execution_engine engine.start_new_cycle with_execution_engine_setup do propagation_info = engine.process_events(raise_framework_errors: false, garbage_collect_pass: @garbage_collect) do @execute_blocks.delete_if do |block| block.call true end end all_propagation_info.merge(propagation_info) exceptions = engine.cycle_end(Hash.new, raise_framework_errors: false) all_propagation_info.framework_errors.concat(exceptions) end unmet = find_all_unmet_expectations(all_propagation_info) unachievable = unmet.find_all { |expectation| expectation.unachievable?(all_propagation_info) } if !unachievable.empty? unachievable = unachievable.map do |expectation| [expectation, expectation.explain_unachievable(all_propagation_info)] end raise Unmet.new(unachievable, all_propagation_info) end if @validate_unexpected_errors validate_has_no_unexpected_error(all_propagation_info) end remaining_timeout = timeout_deadline - Time.now break if remaining_timeout < 0 if engine.has_waiting_work? && @join_all_waiting_work _, propagation_info = with_execution_engine_setup do engine.join_all_waiting_work(timeout: remaining_timeout) end all_propagation_info.merge(propagation_info) elsif !has_pending_execute_blocks? && unmet.empty? break end end while has_pending_execute_blocks? || @wait_until_timeout || (engine.has_waiting_work? && @join_all_waiting_work) unmet = find_all_unmet_expectations(all_propagation_info) if !unmet.empty? raise Unmet.new(unmet, all_propagation_info) end if @validate_unexpected_errors validate_has_no_unexpected_error(all_propagation_info) end if @return_objects.respond_to?(:to_ary) @return_objects.map do |obj| if obj.respond_to?(:return_object) obj.return_object else obj end end else obj = @return_objects if obj.respond_to?(:return_object) obj.return_object else obj end end end
# File lib/roby/test/execution_expectations.rb, line 519 def with_execution_engine_setup engine = @plan.execution_engine current_scheduler = engine.scheduler current_scheduler_state = engine.scheduler.enabled? current_display_exceptions = engine.display_exceptions? if !@display_exceptions.nil? engine.display_exceptions = @display_exceptions end if !@scheduler.nil? if @scheduler != true && @scheduler != false engine.scheduler = @scheduler else engine.scheduler.enabled = @scheduler end end yield ensure engine.scheduler = current_scheduler engine.scheduler.enabled = current_scheduler_state engine.display_exceptions = current_display_exceptions end