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

format_propagation_info(propagation_info, indent: 0) click to toggle source
# 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
new(test, plan) click to toggle source
# 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(test, plan, &block) click to toggle source

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

achieve(description: nil, backtrace: caller(1), &block) click to toggle source

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_expectation(expectation) click to toggle source

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
become_unreachable(*generators, backtrace: caller(1)) click to toggle source

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
emit(*generators, backtrace: caller(1)) click to toggle source

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
execute(&block) click to toggle source

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
fail_to_start(task, reason: nil, backtrace: caller(1)) click to toggle source

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
finalize(*plan_objects, backtrace: caller(1)) click to toggle source

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
find_all_unmet_expectations(all_propagation_info) click to toggle source
# 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
find_tasks(*args) click to toggle source
# File lib/roby/test/execution_expectations.rb, line 308
def find_tasks(*args)
    @test.plan.find_tasks(*args)
end
finish(task, backtrace: caller(1)) click to toggle source

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
finish_promise(promise, backtrace: caller(1)) click to toggle source

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
has_pending_execute_blocks?() click to toggle source

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
have_error_matching(matcher, backtrace: caller(1)) click to toggle source

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
have_framework_error_matching(error, backtrace: caller(1)) click to toggle source

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
have_handled_error_matching(matcher, backtrace: caller(1)) click to toggle source

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
have_internal_error(task, original_exception) click to toggle source

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
have_running(task, backtrace: caller(1)) click to toggle source

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
maintain(at_least_during: 0, description: nil, backtrace: caller(1), &block) click to toggle source

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
method_missing(m, *args, &block) click to toggle source
Calls superclass method
# 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
not_become_unreachable(*generators, backtrace: caller(1)) click to toggle source

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
not_emit(*generators, backtrace: caller(1)) click to toggle source

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
not_finalize(*plan_objects, backtrace: caller(1)) click to toggle source

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
parse(ret: true, &block) click to toggle source
# 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
quarantine(task, backtrace: caller(1)) click to toggle source

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
respond_to_missing?(m, include_private) click to toggle source
Calls superclass method
# File lib/roby/test/execution_expectations.rb, line 312
def respond_to_missing?(m, include_private)
    @test.respond_to?(m) || super
end
start(task, backtrace: caller(1)) click to toggle source

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
unexpected_error?(error) click to toggle source
# 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
validate_has_no_unexpected_error(propagation_info) click to toggle source
# 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(&block) click to toggle source

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
with_execution_engine_setup() { || ... } click to toggle source
# 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