Class: Roby::Test::ExecutionExpectations
- 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)
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
-
#achieve(description: nil, backtrace: caller(1)) {|all_propagation_info| ... } ⇒ Object
Expect that the given block returns true.
-
#become_unreachable(*generators, backtrace: caller(1)) ⇒ Object+
Expect that the generator(s) become unreachable.
-
#emit(*generators, backtrace: caller(1)) ⇒ Event, [Event]
Expect that an event is emitted after the expect_execution block.
-
#fail_to_start(task, reason: nil, backtrace: caller(1)) ⇒ nil
Expect that the given task fails to start.
-
#finalize(*plan_objects, backtrace: caller(1)) ⇒ nil
Expect that plan objects (task or event) are finalized.
-
#finish(task, backtrace: caller(1)) ⇒ Event
Expect that the given task finishes.
-
#finish_promise(promise, backtrace: caller(1)) ⇒ nil
Expect that the given promise finishes.
-
#have_error_matching(matcher, backtrace: caller(1)) ⇒ ExecutionException
Expect that an error is raised and not caught.
-
#have_framework_error_matching(error, backtrace: caller(1)) ⇒ Object
Expect that a framework error is added.
-
#have_handled_error_matching(matcher, backtrace: caller(1)) ⇒ ExecutionException
Expect that an error is raised and caught.
-
#have_internal_error(task, original_exception) ⇒ Event
Expect that the given task emits its internal_error event.
-
#have_running(task, backtrace: caller(1)) ⇒ nil
Expect that the given task either starts or is running, and does not stop.
-
#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.
-
#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.
-
#not_become_unreachable(*generators, backtrace: caller(1)) ⇒ Object
Expect that the generator(s) do not become unreachable.
-
#not_emit(*generators, within: 0, backtrace: caller(1)) ⇒ nil
Expect that an event is not emitted after the expect_execution block.
-
#not_finalize(*plan_objects, backtrace: caller(1)) ⇒ nil
Expect that plan objects (task or event) are not finalized.
-
#quarantine(task, backtrace: caller(1)) ⇒ nil
Expect that the given task is put in quarantine.
-
#start(task, backtrace: caller(1)) ⇒ Event
Expect that the given task starts.
Setup collapse
-
#display_exceptions(enable) ⇒ Object
Whether exceptions should be displayed by the execution engine.
-
#filter_out_related_errors(enable) ⇒ Object
When enabled, errors that are caused by something that has a matcher will not be reported as unexpected.
-
#garbage_collect(enable) ⇒ Object
Whether a garbage collection pass should be run.
-
#join_all_waiting_work(join) ⇒ Object
Whether the expectation test should wait for asynchronous work to finish between event propagations.
-
#poll(&block) ⇒ Object
Setups a block that should be called at each execution cycle.
-
#scheduler(enabled_or_scheduler) ⇒ Object
Controls the scheduler.
-
#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).
-
#validate_unexpected_errors(enable) ⇒ Object
Whether the expectations will pass if exceptions are propagated that are not explicitely expected.
-
#wait_until_timeout(wait) ⇒ Object
Whether the execution will run until the timeout if the expectations have not been met yet.
Class Method Summary collapse
- .format_propagation_info(propagation_info, indent: 0) ⇒ Object
-
.parse(test, plan, &block) ⇒ Expectation
Parse a expect { } block into an Expectation object.
Instance Method Summary collapse
-
#add_expectation(expectation) ⇒ Object
Add a new expectation to be run during #verify.
-
#compute_returned_objects(return_objects) ⇒ Object
Process the value returned by the ‘.to { }` block to convert it to the actual result of the expectations.
-
#execute(&block) ⇒ Object
Queue a block for execution.
- #find_all_unmet_expectations(all_propagation_info) ⇒ Object
- #find_tasks(*args) ⇒ Object
-
#has_pending_execute_blocks? ⇒ Boolean
Whether some blocks have been queued for execution with #execute.
-
#initialize(test, plan) ⇒ ExecutionExpectations
constructor
A new instance of ExecutionExpectations.
- #method_missing(m, *args, &block) ⇒ Object
- #parse(ret: true, &block) ⇒ Object
- #respond_to_missing?(m, include_private) ⇒ Boolean
-
#unexpected_error?(error) ⇒ Boolean
Checks whether the given error is unexpected, given the predicates.
- #validate_has_no_unexpected_error(propagation_info) ⇒ Object
-
#verify(&block) ⇒ Object
Verify that executing the given block in event propagation context will cause the expectations to be met.
- #with_execution_engine_setup ⇒ Object
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 = 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
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
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
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
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
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
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 |
#filter_out_related_errors(enable) ⇒ Object
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
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
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
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
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
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
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
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
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
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.
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.
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
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
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
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
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
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
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
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
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
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
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
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 all_errors = Roby.flatten_exception(error) = @expectations.any? do |expectation| all_errors.any? { |orig_e| expectation.relates_to_error?(orig_e) } end ! end |
#validate_has_no_unexpected_error(propagation_info) ⇒ Object
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
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
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
486 |
# File 'lib/roby/test/execution_expectations.rb', line 486 dsl_attribute :wait_until_timeout |
#with_execution_engine_setup ⇒ Object
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 |