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.

Examples:

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

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)

Defined Under Namespace

Classes: Achieve, BecomeUnreachable, EmitGenerator, EmitGeneratorModel, ErrorExpectation, Expectation, FailsToStart, Finalize, HaveErrorMatching, HaveFrameworkError, HaveHandledErrorMatching, 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.



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# 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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

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



316
317
318
319
320
321
# 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

Class Method Details

.format_propagation_info(propagation_info, indent: 0) ⇒ Object



323
324
325
# 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

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

Parse a expect { } block into an Expectation object

Returns:



282
283
284
# File 'lib/roby/test/execution_expectations.rb', line 282

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



128
129
130
# 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) ⇒ Object

Add a new expectation to be run during #verify



499
500
501
502
# File 'lib/roby/test/execution_expectations.rb', line 499

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



85
86
87
88
89
90
91
92
93
94
# 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

#display_exceptions(enable) ⇒ Object

Whether exceptions should be displayed by the execution engine

The default is false

Parameters:

  • enable (Boolean)


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

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]

    Returns all the events whose generator match the query.

    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.

    expect_execution.to do
      emit find_tasks(MyTask).start_event
    end
    

    Parameters:

    Returns:

    • ([Event])

      all the events whose generator match the query

Parameters:

Returns:



63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 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) ⇒ Object

Queue a block for execution

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



508
509
510
511
# 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)) ⇒ nil

Expect that the given task fails to start

Parameters:

Returns:

  • (nil)


136
137
138
# 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)) ⇒ nil

Expect that plan objects (task or event) are finalized

Parameters:

Returns:

  • (nil)


184
185
186
187
188
189
# 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) ⇒ Object



665
666
667
668
669
# 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) ⇒ Object



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

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



174
175
176
177
178
# 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)) ⇒ nil

Expect that the given promise finishes

Parameters:

Returns:

  • (nil)


224
225
226
227
# File 'lib/roby/test/execution_expectations.rb', line 224

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

#garbage_collect(enable) ⇒ Object

Whether a garbage collection pass should be run

The default is false

Parameters:

  • enable (Boolean)


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

dsl_attribute :garbage_collect

#has_pending_execute_blocks?Boolean

Whether some blocks have been queued for execution with #execute

Returns:

  • (Boolean)


515
516
517
# 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)) ⇒ 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:



242
243
244
# 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)) ⇒ 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.



273
274
275
# 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)) ⇒ ExecutionException

Expect that an error is raised and caught

Examples:

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

Parameters:

Returns:



259
260
261
# 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) ⇒ Event

Expect that the given task emits its internal_error event

Parameters:

Returns:

  • (Event)

    the emitted internal error event



206
207
208
209
# 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)) ⇒ 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)


162
163
164
165
166
167
168
# 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

#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)


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

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



117
118
119
# 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

#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



100
101
102
103
104
# 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)) ⇒ 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)


31
32
33
34
35
36
37
38
39
40
# 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)) ⇒ nil

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

Parameters:

Returns:

  • (nil)


195
196
197
198
199
200
# 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) ⇒ Object



286
287
288
289
290
# 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)) ⇒ nil

Expect that the given task is put in quarantine

Parameters:

Returns:

  • (nil)


215
216
217
218
# 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) ⇒ Boolean

Returns:

  • (Boolean)


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

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:



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

dsl_attribute :scheduler

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

Expect that the given task starts

Parameters:

Returns:

  • (Event)

    the task’s start event



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

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)


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

dsl_attribute :timeout

#unexpected_error?(error) ⇒ Boolean

Returns:

  • (Boolean)


650
651
652
653
654
655
656
657
658
659
660
661
662
663
# 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) ⇒ Object



628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
# 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

#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)


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

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)



551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
# 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

#wait_until_timeout(wait) ⇒ Object

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

The default is 5s

Parameters:

  • wait (Boolean)


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

dsl_attribute :wait_until_timeout

#with_execution_engine_setupObject



519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
# 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