module Roby::Test
This module is defining common support for tests that need the Roby
infrastructure
It assumes that the tests are started using roby's test command. Tests using this module can NOT be started with only e.g. testrb.
@see SelfTest
Constants
- BASE_PORT
- DISCOVERY_SERVER
- LOCAL_PORT
- LOCAL_SERVER
- REMOTE_PORT
- REMOTE_SERVER
- Stat
Attributes
The decision control component used by the tests
a [collection, collection_backup] array of the collections saved by original_collections
The plan used by the tests
The list of children started using remote_process
Public Class Methods
# File lib/roby/test/tools.rb, line 4 def sampling(engine, duration, period, *fields) Test.info "starting sampling #{fields.join(", ")} every #{period}s for #{duration}s" samples = Array.new fields.map! { |n| n.to_sym } if fields.include?(:dt) raise ArgumentError, "dt is reserved by #sampling" end if compute_time = !fields.include?(:t) fields << :t end fields << :dt sample_type = Struct.new(*fields) start = Time.now Roby.condition_variable(true) do |cv, mt| first_sample = nil mt.synchronize do timeout = false id = engine.every(period) do result = yield if result if compute_time result << engine.cycle_start end new_sample = sample_type.new(*result) unless samples.empty? new_sample.dt = new_sample.t- samples.last.t end samples << new_sample if samples.last.t - samples.first.t > duration mt.synchronize do timeout = true cv.broadcast end end end end while !timeout cv.wait(mt) end engine.remove_periodic_handler(id) end end samples end
Computes mean and standard deviation about the samples in samples
spec
describes what to compute:
-
if nothing is specified, we compute the statistics on
v(i - 1) - v(i)
-
if spec is 'rate', we compute the statistics on
(v(i - 1) - v(i)) / (t(i - 1) / t(i))
-
if spec is 'absolute', we compute the statistics on
v(i)
-
if spec is 'absolute_rate', we compute the statistics on
v(i) / (t(i - 1) / t(i))
The returned value is a struct with the same fields than the samples. Each element is a Stats object
# File lib/roby/test/tools.rb, line 74 def stats(samples, spec) return if samples.empty? type = samples.first.class spec = spec.inject(Hash.new) do |h, (k, v)| spec[k.to_sym] = v.to_sym spec end spec[:t] = :exclude spec[:dt] = :absolute # Initialize the result value fields = type.members. find_all { |n| spec[n.to_sym] != :exclude }. map { |n| n.to_sym } result = Struct.new(*fields).new fields.each do |name| result[name] = Stat.new(0, 0, 0, 0, nil, nil) end # Compute the deltas if the mode is not absolute last_sample = nil samples = samples.map do |original_sample| sample = original_sample.dup fields.each do |name| next unless value = sample[name] unless spec[name] == :absolute || spec[name] == :absolute_rate if last_sample && last_sample[name] sample[name] -= last_sample[name] else sample[name] = nil next end end end last_sample = original_sample sample end # Compute the rates if needed samples = samples.map do |sample| fields.each do |name| next unless value = sample[name] if spec[name] == :rate || spec[name] == :absolute_rate if sample.dt sample[name] = value / sample.dt else sample[name] = nil next end end end sample end samples.each do |sample| fields.each do |name| next unless value = sample[name] if !result[name].max || value > result[name].max result[name].max = value end if !result[name].min || value < result[name].min result[name].min = value end result[name].total += value result[name].count += 1 end last_sample = sample end result.each do |r| r.mean = Float(r.total) / r.count end samples.each do |sample| fields.each do |name| next unless value = sample[name] result[name].stddev += (value - result[name].mean) ** 2 end end result.each do |r| r.stddev = Math.sqrt(r.stddev / r.count) end result end
Public Instance Methods
# File lib/roby/test/common.rb, line 82 def create_transaction t = Roby::Transaction.new(plan) @transactions << t t end
# File lib/roby/test/common.rb, line 88 def deprecated_feature Roby.enable_deprecation_warnings = false flexmock(Roby).should_receive(:warn_deprecated).at_least.once yield ensure Roby.enable_deprecation_warnings = true end
# File lib/roby/test/common.rb, line 72 def execute(&block) execution_engine.execute(&block) end
# File lib/roby/test/common.rb, line 68 def execution_engine plan.execution_engine if plan && plan.executable? end
Use to call the original method on a partial mock
# File lib/roby/test/common.rb, line 297 def flexmock_call_original(object, method, *args, &block) Test.warn "#flexmock_call_original is deprecated, use #flexmock_invoke_original instead" flexmock_invoke_original(object, method, *args, &block) end
Use to call the original method on a partial mock
# File lib/roby/test/common.rb, line 303 def flexmock_invoke_original(object, method, *args, &block) object.instance_variable_get(:@flexmock_proxy).proxy.flexmock_invoke_original(method, args, &block) end
@deprecated use {Assertions#capture_log} instead
# File lib/roby/test/common.rb, line 156 def inhibit_fatal_messages(&block) Roby.warn_deprecated "##{__method__} is deprecated, use #capture_log instead" with_log_level(Roby, Logger::FATAL, &block) end
Clear the plan and return it
# File lib/roby/test/common.rb, line 77 def new_plan plan.clear plan end
Creates a set of tasks and returns them. Each task is given an unique 'id' which allows to recognize it in a failed assertion.
Known options are:
- missions
-
how many mission to create [0]
- discover
-
how many tasks should be discovered [0]
- tasks
-
how many tasks to create outside the plan [0]
- model
-
the task model [Roby::Task]
- plan
-
the plan to apply on [plan]
The return value is [missions, discovered, tasks]
(t1, t2), (t3, t4, t5), (t6, t7) = prepare_plan missions: 2, discover: 3, tasks: 2
An empty set is omitted
(t1, t2), (t6, t7) = prepare_plan missions: 2, tasks: 2
If a set is a singleton, the only object of this singleton is returned
t1, (t6, t7) = prepare_plan missions: 1, tasks: 2
# File lib/roby/test/common.rb, line 330 def prepare_plan(options) options = validate_options options, missions: 0, add: 0, discover: 0, tasks: 0, permanent: 0, model: Roby::Task, plan: plan missions, permanent, added, tasks = [], [], [], [] (1..options[:missions]).each do |i| options[:plan].add_mission_task(t = options[:model].new(id: "mission-#{i}")) missions << t end (1..options[:permanent]).each do |i| options[:plan].add_permanent_task(t = options[:model].new(id: "perm-#{i}")) permanent << t end (1..(options[:discover] + options[:add])).each do |i| options[:plan].add(t = options[:model].new(id: "discover-#{i}")) added << t end (1..options[:tasks]).each do |i| tasks << options[:model].new(id: "task-#{i}") end result = [] [missions, permanent, added, tasks].each do |set| unless set.empty? result << set end end result = result.map do |set| if set.size == 1 then set.first else set end end if result.size == 1 return result.first end return *result end
Process pending events
# File lib/roby/test/common.rb, line 237 def process_events(timeout: 2, enable_scheduler: nil, join_all_waiting_work: true, raise_errors: true, garbage_collect_pass: true, &caller_block) Roby.warn_deprecated "Test#process_events is deprecated, use #expect_execution instead" exceptions = Array.new registered_plans.each do |p| engine = p.execution_engine begin engine.start_new_cycle errors = begin current_scheduler_state = engine.scheduler.enabled? if !enable_scheduler.nil? engine.scheduler.enabled = enable_scheduler end engine.process_events(garbage_collect_pass: garbage_collect_pass, &caller_block) ensure engine.scheduler.enabled = current_scheduler_state end if join_all_waiting_work engine.join_all_waiting_work(timeout: timeout) end exceptions.concat(errors.exceptions) engine.cycle_end(Hash.new) caller_block = nil end while engine.has_waiting_work? && join_all_waiting_work end if raise_errors && !exceptions.empty? if exceptions.size == 1 e = exceptions.first raise e.exception else raise SynchronousEventProcessingMultipleErrors.new(exceptions.map(&:exception)) end end end
Repeatedly process events until a condition is met
@yieldreturn [Boolean] true if the condition is met, false otherwise
@param (see process_events
)
# File lib/roby/test/common.rb, line 282 def process_events_until(timeout: 5, join_all_waiting_work: false, **options) Roby.warn_deprecated "Test#process_events_until is deprecated, use #expect_execution.to { achieve { ... } } instead" start = Time.now while !yield now = Time.now remaining = timeout - (now - start) if remaining < 0 flunk("failed to reach condition #{proc} within #{timeout} seconds") end process_events(timeout: remaining, join_all_waiting_work: join_all_waiting_work, **options) sleep 0.01 end end
Start a new process and saves its PID in remote_processes
. If a block is given, it is called in the new child. remote_process
returns only after this block has returned.
# File lib/roby/test/common.rb, line 375 def remote_process start_r, start_w= IO.pipe quit_r, quit_w = IO.pipe remote_pid = fork do begin start_r.close yield rescue Exception => e puts e.full_message end start_w.write('OK') quit_r.read(2) end start_w.close result = start_r.read(2) remote_processes << [remote_pid, quit_w] remote_pid ensure # start_r.close end
@deprecated use {Assertions#capture_log} instead
# File lib/roby/test/common.rb, line 172 def reset_log_levels(warn_deprecated: true) if warn_deprecated Roby.warn_deprecated "##{__method__} is deprecated, use #capture_log instead" end @log_levels.each do |log_object, level| log_object.level = level end @log_levels.clear end
Restors the collections saved by save_collection
to their previous state
# File lib/roby/test/common.rb, line 108 def restore_collections original_collections.each do |col, backup| col.clear if col.kind_of?(Hash) col.merge! backup else backup.each do |obj| col << obj end end end original_collections.clear end
Saves the current state of obj
. This state will be restored by restore_collections
. obj
must respond to << to add new elements (hashes do not work whild arrays or sets do)
# File lib/roby/test/common.rb, line 103 def save_collection(obj) original_collections << [obj, obj.dup] end
@deprecated use {Assertions#capture_log} instead
# File lib/roby/test/common.rb, line 162 def set_log_level(log_object, level) Roby.warn_deprecated "#set_log_level is deprecated, use #capture_log instead" if log_object.respond_to?(:logger) log_object = log_object.logger end @log_levels[log_object] ||= log_object.level log_object.level = level end
# File lib/roby/test/common.rb, line 124 def setup @app = Roby.app @app.development_mode = false Roby.app.reload_config @log_levels = Hash.new @transactions = Array.new if !@plan @plan = Roby.app.plan end register_plan(@plan) super @console_logger ||= false @event_logger ||= false @original_roby_logger_level = Roby.logger.level @original_collections = [] Thread.abort_on_exception = false @remote_processes = [] Roby.app.log_server = false plan.execution_engine.gc_warning = false @watched_events = nil @handler_ids = Array.new end
Stop all the remote processes that have been started using remote_process
# File lib/roby/test/common.rb, line 400 def stop_remote_processes remote_processes.reverse.each do |pid, quit_w| begin quit_w.write('OK') rescue Errno::EPIPE end begin Process.waitpid(pid) rescue Errno::ECHILD end end remote_processes.clear end
# File lib/roby/test/common.rb, line 199 def teardown Timecop.return @transactions.each do |trsc| if !trsc.finalized? trsc.discard_transaction end end teardown_registered_plans if @handler_ids && execution_engine @handler_ids.each do |handler_id| execution_engine.remove_propagation_handler(handler_id) end end # Plan teardown would have disconnected the peers already stop_remote_processes restore_collections if defined? Roby::Application Roby.app.abort_on_exception = false Roby.app.abort_on_application_exception = true end super ensure reset_log_levels(warn_deprecated: false) clear_registered_plans if @original_roby_logger_level Roby.logger.level = @original_roby_logger_level end end
@deprecated use {Assertions#capture_log} instead
# File lib/roby/test/common.rb, line 183 def with_log_level(log_object, level) Roby.warn_deprecated "##{__method__} is deprecated, use #capture_log instead" if log_object.respond_to?(:logger) log_object = log_object.logger end current_level = log_object.level log_object.level = level yield ensure if current_level log_object.level = current_level end end