Class: Roby::Test::ExecutionExpectations

Inherits:
Object
  • Object
show all
Defined in:
lib/roby/test/execution_expectations.rb

Overview

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.

expect_execution.to do
  achieve { plan.num_tasks }
end # => the number of tasks from the plan

expect_execution.to do
  event = emit task.start_event
  error = have_error_matching CodeError
  [error, event]
end # => the pair (raised error, emitted event)

Examples:

execute until a block returns true. The call returns the

block's return value

execute until an event was emitted and an error raised. The

call will in this case return the error object and the emitted event

Defined Under Namespace

Classes: Achieve, BecomeUnreachable, EmitGenerator, EmitGeneratorModel, ErrorExpectation, Expectation, FailsToStart, Finalize, HaveErrorMatching, HaveFrameworkError, HaveHandledErrorMatching, IgnoreErrorsFrom, Maintain, NotBecomeUnreachable, NotEmitGenerator, NotEmitGeneratorModel, NotFinalize, PromiseFinishes, Quarantine, UnexpectedErrors, Unmet

Expectations collapse

Setup collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(test, plan) ⇒ ExecutionExpectations

Returns a new instance of ExecutionExpectations.



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/roby/test/execution_expectations.rb', line 318

def initialize(test, plan)
    @test = test
    @plan = plan

    @expectations = []
    @execute_blocks = []
    @poll_blocks = []

    @scheduler = false
    @timeout = 5
    @join_all_waiting_work = true
    @wait_until_timeout = true
    @garbage_collect = false
    @validate_unexpected_errors = true
    @display_exceptions = false
    @filter_out_related_errors = true
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(m, *args, &block) ⇒ Object



344
345
346
347
348
349
350
# File 'lib/roby/test/execution_expectations.rb', line 344

def method_missing(m, *args, &block)
    if @test.respond_to?(m)
        @test.public_send(m, *args, &block)
    else
        super
    end
end

Class Method Details

.format_propagation_info(propagation_info, indent: 0) ⇒ Object



352
353
354
# File 'lib/roby/test/execution_expectations.rb', line 352

def self.format_propagation_info(propagation_info, indent: 0)
    PP.pp(propagation_info, "".dup).split("\n").join("\n#{' ' * indent}")
end

.parse(test, plan, &block) ⇒ Expectation

Parse a expect { } block into an Expectation object

Returns:



308
309
310
# File 'lib/roby/test/execution_expectations.rb', line 308

def self.parse(test, plan, &block)
    new(test, plan).parse(&block)
end

Instance Method Details

#achieve(description: nil, backtrace: caller(1)) {|all_propagation_info| ... } ⇒ Object

Expect that the given block returns true

Yield Parameters:

  • all_propagation_info (ExecutionEngine::PropagationInfo)

    all that happened during the propagations since the beginning of expect_execution block. It contains event emissions and raised/caught errors.

Yield Returns:

  • the value that should be returned by the expectation



143
144
145
# File 'lib/roby/test/execution_expectations.rb', line 143

def achieve(description: nil, backtrace: caller(1), &block)
    add_expectation(Achieve.new(block, description, backtrace))
end

#add_expectation(expectation) ⇒ Object

Add a new expectation to be run during #verify



560
561
562
563
# File 'lib/roby/test/execution_expectations.rb', line 560

def add_expectation(expectation)
    @expectations << expectation
    expectation
end

#become_unreachable(*generators, backtrace: caller(1)) ⇒ Object+

Expect that the generator(s) become unreachable

Parameters:

  • generators (Array<EventGenerator>)

    the generators that are expected to become unreachable

Returns:

  • (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



96
97
98
99
100
101
102
103
104
105
# File 'lib/roby/test/execution_expectations.rb', line 96

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

#compute_returned_objects(return_objects) ⇒ Object

Process the value returned by the ‘.to { }` block to convert it to the actual result of the expectations



698
699
700
701
702
703
704
# File 'lib/roby/test/execution_expectations.rb', line 698

def compute_returned_objects(return_objects)
    return_objects.map do |ret|
        obj = ret.respond_to?(:return_object) ? ret.return_object : ret
        obj = ret.filter_result(obj) if ret.respond_to?(:filter_result)
        obj
    end
end

#display_exceptions(enable) ⇒ Object

Whether exceptions should be displayed by the execution engine

The default is false

Parameters:

  • enable (Boolean)


549
# File 'lib/roby/test/execution_expectations.rb', line 549

dsl_attribute :display_exceptions

#emit(generator) ⇒ Event #emit(generator_query) ⇒ [Event]

Expect that an event is emitted after the expect_execution block

Overloads:

  • #emit(generator) ⇒ Event

    Returns the emitted event.

    Parameters:

    • generator (EventGenerator)

      the generator we’re waiting the emission of

    Returns:

    • (Event)

      the emitted event

  • #emit(generator_query) ⇒ [Event]

    expect_execution.to do

      emit find_tasks(MyTask).start_event
    end
    

    Examples:

    wait for the emission of the start event of any task of

    model MyTask. The call will return the emitted events that match
    this.
    

    Parameters:

    Returns:

    • ([Event])

      all the events whose generator match the query

Parameters:

Returns:



74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/roby/test/execution_expectations.rb', line 74

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) ⇒ Object

Queue a block for execution

This is meant to be used by expectation objects which require to perform some actions in execution context.



569
570
571
572
# File 'lib/roby/test/execution_expectations.rb', line 569

def execute(&block)
    @execute_blocks << block
    nil
end

#fail_to_start(task, reason: nil, backtrace: caller(1)) ⇒ nil

Expect that the given task fails to start

Parameters:

Returns:

  • (nil)


151
152
153
# File 'lib/roby/test/execution_expectations.rb', line 151

def fail_to_start(task, reason: nil, backtrace: caller(1))
    add_expectation(FailsToStart.new(task, reason, backtrace))
end

When enabled, errors that are caused by something that has a matcher will not be reported as unexpected. For instance, a DependencyFailedError caused by an event, when there is the corresponding ‘emit` predicate.

The default is true

Parameters:

  • enable (Boolean)


530
# File 'lib/roby/test/execution_expectations.rb', line 530

dsl_attribute :filter_out_related_errors

#finalize(*plan_objects, backtrace: caller(1)) ⇒ nil

Expect that plan objects (task or event) are finalized

Parameters:

Returns:

  • (nil)


201
202
203
204
205
# File 'lib/roby/test/execution_expectations.rb', line 201

def finalize(*plan_objects, backtrace: caller(1))
    plan_objects.map do |plan_object|
        add_expectation(Finalize.new(plan_object, backtrace))
    end
end

#find_all_unmet_expectations(all_propagation_info) ⇒ Object



751
752
753
754
755
# File 'lib/roby/test/execution_expectations.rb', line 751

def find_all_unmet_expectations(all_propagation_info)
    @expectations.find_all do |exp|
        !exp.update_match(all_propagation_info)
    end
end

#find_tasks(*args) ⇒ Object



336
337
338
# File 'lib/roby/test/execution_expectations.rb', line 336

def find_tasks(*args)
    @test.plan.find_tasks(*args)
end

#finish(task, backtrace: caller(1)) ⇒ Event

Expect that the given task finishes

Parameters:

Returns:

  • (Event)

    the task’s stop event



189
190
191
192
193
194
195
# File 'lib/roby/test/execution_expectations.rb', line 189

def finish(task, backtrace: caller(1))
    unless task.running?
        start_emitted = emit(task.start_event, backtrace: backtrace)
    end

    [start_emitted, emit(task.stop_event, backtrace: backtrace)].compact
end

#finish_promise(promise, backtrace: caller(1)) ⇒ nil

Expect that the given promise finishes

Parameters:

Returns:

  • (nil)


238
239
240
# File 'lib/roby/test/execution_expectations.rb', line 238

def finish_promise(promise, backtrace: caller(1))
    add_expectation(PromiseFinishes.new(promise, backtrace))
end

#garbage_collect(enable) ⇒ Object

Whether a garbage collection pass should be run

The default is false

Parameters:

  • enable (Boolean)


519
# File 'lib/roby/test/execution_expectations.rb', line 519

dsl_attribute :garbage_collect

#has_pending_execute_blocks?Boolean

Whether some blocks have been queued for execution with #execute

Returns:

  • (Boolean)


576
577
578
# File 'lib/roby/test/execution_expectations.rb', line 576

def has_pending_execute_blocks? # rubocop:disable Naming/PredicateName
    !@execute_blocks.empty?
end

#have_error_matching(matcher, backtrace: caller(1)) ⇒ ExecutionException

Expect that an error is raised and not caught

Examples:

expect that a ChildFailedError is raised from ‘task’

expect_execution.to do
  have_error_matching Roby::ChildFailedError.match.
    with_origin(task)
end

Parameters:

Returns:



255
256
257
# File 'lib/roby/test/execution_expectations.rb', line 255

def have_error_matching(matcher, backtrace: caller(1)) # rubocop:disable Naming/PredicateName
    add_expectation(HaveErrorMatching.new(matcher, backtrace))
end

#have_framework_error_matching(error, backtrace: caller(1)) ⇒ Object

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.



286
287
288
# File 'lib/roby/test/execution_expectations.rb', line 286

def have_framework_error_matching(error, backtrace: caller(1)) # rubocop:disable Naming/PredicateName
    add_expectation(HaveFrameworkError.new(error, backtrace))
end

#have_handled_error_matching(matcher, backtrace: caller(1)) ⇒ ExecutionException

Expect that an error is raised and caught

Examples:

expect that a ChildFailedError is raised from ‘task’ and handled

expect_execution.to do
  have_handled_error_matching Roby::ChildFailedError.match.
    with_origin(task)
end

Parameters:

Returns:



272
273
274
# File 'lib/roby/test/execution_expectations.rb', line 272

def have_handled_error_matching(matcher, backtrace: caller(1)) # rubocop:disable Naming/PredicateName
    add_expectation(HaveHandledErrorMatching.new(matcher, backtrace))
end

#have_internal_error(task, original_exception) ⇒ Event

Expect that the given task emits its internal_error event

Parameters:

Returns:

  • (Event)

    the emitted internal error event



221
222
223
224
# File 'lib/roby/test/execution_expectations.rb', line 221

def have_internal_error(task, original_exception) # rubocop:disable Naming/PredicateName
    [have_handled_error_matching(original_exception.match.with_origin(task)),
     emit(task.internal_error_event)]
end

#have_running(task, backtrace: caller(1)) ⇒ nil

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.

Examples:

task keeps running until action_task stops

expect_execution.to do
  keep_running task
  finish action_task
end

Parameters:

Returns:

  • (nil)


177
178
179
180
181
182
183
# File 'lib/roby/test/execution_expectations.rb', line 177

def have_running(task, backtrace: caller(1)) # rubocop:disable Naming/PredicateName
    unless task.running?
        start_emitted = emit(task.start_event, backtrace: backtrace)
    end

    [start_emitted, not_emit(task.stop_event)].compact
end

#ignore_errors_from(expectations, backtrace: caller(1)) ⇒ Object

Given another predicate, ignore all errors related to this predicate but do not expect the predicate itself to be fullfilled.

Examples:

ignore all errors originating from this event emission

ignore_errors_from emit(task.stop_event)


295
296
297
298
299
300
301
# File 'lib/roby/test/execution_expectations.rb', line 295

def ignore_errors_from(expectations, backtrace: caller(1))
    expectations = Array(expectations)
    expectations.each do |e|
        @expectations.delete(e)
    end
    add_expectation(IgnoreErrorsFrom.new(expectations, backtrace))
end

#join_all_waiting_work(join) ⇒ Object

Whether the expectation test should wait for asynchronous work to finish between event propagations

The default is true

Parameters:

  • join (Boolean)


496
# File 'lib/roby/test/execution_expectations.rb', line 496

dsl_attribute :join_all_waiting_work

#maintain(at_least_during: 0, description: nil, backtrace: caller(1)) {|all_propagation_info| ... } ⇒ Object

Expect that the given block is true during a certain amount of time

Parameters:

  • at_least_during (Float) (defaults to: 0)

    the minimum duration in seconds. If zero, the expectations will run at least one execution cycle. The exact duration depends on the other expectations.

Yield Parameters:

  • all_propagation_info (ExecutionEngine::PropagationInfo)

    all that happened during the propagations since the beginning of expect_execution block. It contains event emissions and raised/caught errors.

Yield Returns:

  • (Boolean)

    expected to be true over duration seconds



128
129
130
131
132
133
134
# File 'lib/roby/test/execution_expectations.rb', line 128

def maintain(
    at_least_during: 0, description: nil, backtrace: caller(1), &block
)
    add_expectation(
        Maintain.new(at_least_during, block, description, backtrace)
    )
end

#not_become_unreachable(*generators, backtrace: caller(1)) ⇒ Object

Expect that the generator(s) do not become unreachable

Parameters:

  • generators (Array<EventGenerator>)

    the generators that are expected to not become unreachable



111
112
113
114
115
# File 'lib/roby/test/execution_expectations.rb', line 111

def not_become_unreachable(*generators, backtrace: caller(1))
    generators.map do |generator|
        add_expectation(NotBecomeUnreachable.new(generator, backtrace))
    end
end

#not_emit(*generators, within: 0, backtrace: caller(1)) ⇒ nil

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

Returns:

  • (nil)


35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/roby/test/execution_expectations.rb', line 35

def not_emit(*generators, within: 0, backtrace: caller(1))
    generators.map do |generator|
        if generator.kind_of?(EventGenerator)
            add_expectation(
                NotEmitGenerator.new(generator, backtrace, within: within)
            )
        else
            add_expectation(
                NotEmitGeneratorModel.new(
                    generator, backtrace, within: within
                )
            )
        end
    end
end

#not_finalize(*plan_objects, backtrace: caller(1)) ⇒ nil

Expect that plan objects (task or event) are not finalized

Parameters:

Returns:

  • (nil)


211
212
213
214
215
# File 'lib/roby/test/execution_expectations.rb', line 211

def not_finalize(*plan_objects, backtrace: caller(1))
    plan_objects.map do |plan_object|
        add_expectation(NotFinalize.new(plan_object, backtrace))
    end
end

#parse(ret: true, &block) ⇒ Object



312
313
314
315
316
# File 'lib/roby/test/execution_expectations.rb', line 312

def parse(ret: true, &block)
    block_ret = instance_eval(&block)
    @return_objects = block_ret if ret
    self
end

#poll(&block) ⇒ Object

Setups a block that should be called at each execution cycle



552
553
554
555
# File 'lib/roby/test/execution_expectations.rb', line 552

def poll(&block)
    @poll_blocks << block
    self
end

#quarantine(task, backtrace: caller(1)) ⇒ nil

Expect that the given task is put in quarantine

Parameters:

Returns:

  • (nil)


230
231
232
# File 'lib/roby/test/execution_expectations.rb', line 230

def quarantine(task, backtrace: caller(1))
    add_expectation(Quarantine.new(task, backtrace))
end

#respond_to_missing?(m, include_private) ⇒ Boolean

Returns:

  • (Boolean)


340
341
342
# File 'lib/roby/test/execution_expectations.rb', line 340

def respond_to_missing?(m, include_private)
    @test.respond_to?(m) || super
end

#scheduler(enabled) ⇒ Object #scheduler(scheduler) ⇒ Object

Controls the scheduler

The default is false

Overloads:

  • #scheduler(enabled) ⇒ Object

    Parameters:

    • enabled (Boolean)

      controls whether the scheduler is enabled or not

  • #scheduler(scheduler) ⇒ Object

    Parameters:



510
# File 'lib/roby/test/execution_expectations.rb', line 510

dsl_attribute :scheduler

#start(task, backtrace: caller(1)) ⇒ Event

Expect that the given task starts

Parameters:

Returns:

  • (Event)

    the task’s start event



159
160
161
# File 'lib/roby/test/execution_expectations.rb', line 159

def start(task, backtrace: caller(1))
    emit task.start_event, backtrace: backtrace
end

#timeout(timeout) ⇒ Object

How long will the test wait either for asynchronous jobs (if #wait_until_timeout is false and #join_all_waiting_work is true) or until it succeeds (if #wait_until_timeout is true)

The default is 5s

Parameters:

  • timeout (Float)


476
# File 'lib/roby/test/execution_expectations.rb', line 476

dsl_attribute :timeout

#unexpected_error?(error) ⇒ Boolean

Checks whether the given error is unexpected, given the predicates

It filters out errors that are related to certain predicates, as tested with the Roby::Test::ExecutionExpectations::Expectation#relates_to_error? test

Parameters:

Returns:

  • (Boolean)


741
742
743
744
745
746
747
748
749
# File 'lib/roby/test/execution_expectations.rb', line 741

def unexpected_error?(error)
    return true unless @filter_out_related_errors

    all_errors = Roby.flatten_exception(error)
    has_related_predicate = @expectations.any? do |expectation|
        all_errors.any? { |orig_e| expectation.relates_to_error?(orig_e) }
    end
    !has_related_predicate
end

#validate_has_no_unexpected_error(propagation_info) ⇒ Object

Raises:



706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
# File 'lib/roby/test/execution_expectations.rb', line 706

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|
        is_internal_error =
            ev.generator.respond_to?(:symbol) &&
            ev.generator.symbol == :internal_error
        next unless is_internal_error

        ev.context.any? do |obj|
            unexpected_error?(obj) if obj.kind_of?(Exception)
        end
    end

    unexpected_errors += internal_errors.flat_map(&:context)
    return if unexpected_errors.empty?

    raise UnexpectedErrors.new(unexpected_errors)
end

#validate_unexpected_errors(enable) ⇒ Object

Whether the expectations will pass if exceptions are propagated that are not explicitely expected

The default is true

Parameters:

  • enable (Boolean)


540
# File 'lib/roby/test/execution_expectations.rb', line 540

dsl_attribute :validate_unexpected_errors

#verify(&block) ⇒ Object

Verify that executing the given block in event propagation context will cause the expectations to be met

Returns:

  • (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)

Raises:



612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
# File 'lib/roby/test/execution_expectations.rb', line 612

def verify(&block)
    all_propagation_info = ExecutionEngine::PropagationInfo.new
    timeout_deadline = Time.now_without_mock_time + @timeout

    @execute_blocks << block if block

    engine = @plan.execution_engine
    loop do
        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 |b|
                    b.call
                    true
                end
                @poll_blocks.each(&:call)

                if engine.has_waiting_work?
                    engine.process_waiting_work
                    Thread.pass
                end
            end
            all_propagation_info.merge(propagation_info)

            exceptions = engine.cycle_end({}, raise_framework_errors: false)
            all_propagation_info.framework_errors.concat(exceptions)
        end

        unmet = find_all_unmet_expectations(all_propagation_info)
        unachievable = unmet.find_all do |expectation|
            expectation.unachievable?(all_propagation_info)
        end
        unless unachievable.empty?
            unachievable = unachievable.map do |expectation|
                explanation = expectation
                              .explain_unachievable(all_propagation_info)
                [expectation, explanation]
            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_without_mock_time
        break if remaining_timeout < 0

        if @join_all_waiting_work && engine.has_waiting_work?
            next
        elsif has_pending_execute_blocks?
            next
        elsif unmet.empty?
            break
        elsif !@wait_until_timeout
            break
        end
    end

    if @join_all_waiting_work && engine.has_waiting_work?
        e = ExecutionEngine::JoinAllWaitingWorkTimeout.new(
            engine.waiting_work
        )
        raise UnexpectedErrors.new([e]),
              "some asynchronous work did not finish"
    end

    unmet = find_all_unmet_expectations(all_propagation_info)
    raise Unmet.new(unmet, all_propagation_info) unless unmet.empty?

    if @validate_unexpected_errors
        validate_has_no_unexpected_error(all_propagation_info)
    end

    if @return_objects.respond_to?(:to_ary)
        compute_returned_objects(@return_objects)
    else
        compute_returned_objects([@return_objects]).first
    end
end

#wait_until_timeout(wait) ⇒ Object

Whether the execution will run until the timeout if the expectations have not been met yet.

The default is true

Parameters:

  • wait (Boolean)


486
# File 'lib/roby/test/execution_expectations.rb', line 486

dsl_attribute :wait_until_timeout

#with_execution_engine_setupObject



580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
# File 'lib/roby/test/execution_expectations.rb', line 580

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?
    unless display_exceptions.nil?
        engine.display_exceptions = @display_exceptions
    end
    unless 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