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

app[R]
control[R]

The decision control component used by the tests

original_collections[R]

a [collection, collection_backup] array of the collections saved by original_collections

plan[R]

The plan used by the tests

remote_processes[R]

The list of children started using remote_process

Public Class Methods

sampling(engine, duration, period, *fields) { || ... } click to toggle source
# 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
stats(samples, spec) click to toggle source

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

create_transaction() click to toggle source
# File lib/roby/test/common.rb, line 82
def create_transaction
    t = Roby::Transaction.new(plan)
    @transactions << t
    t
end
deprecated_feature() { || ... } click to toggle source
# 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
execute(&block) click to toggle source
# File lib/roby/test/common.rb, line 72
def execute(&block)
    execution_engine.execute(&block)
end
execution_engine() click to toggle source
# File lib/roby/test/common.rb, line 68
def execution_engine
    plan.execution_engine if plan && plan.executable?
end
flexmock_call_original(object, method, *args, &block) click to toggle source

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
flexmock_invoke_original(object, method, *args, &block) click to toggle source

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

@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
new_plan() click to toggle source

Clear the plan and return it

# File lib/roby/test/common.rb, line 77
def new_plan
    plan.clear
    plan
end
prepare_plan(options) click to toggle source

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_events(timeout: 2, enable_scheduler: nil, join_all_waiting_work: true, raise_errors: true, garbage_collect_pass: true, &caller_block) click to toggle source

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
process_events_until(timeout: 5, join_all_waiting_work: false, **options) click to toggle source

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

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
reset_log_levels(warn_deprecated: true) click to toggle source

@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
restore_collections() click to toggle source

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
save_collection(obj) click to toggle source

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
set_log_level(log_object, level) click to toggle source

@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
setup() click to toggle source
Calls superclass method
# 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_remote_processes() click to toggle source

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
teardown() click to toggle source
Calls superclass method
# 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
with_log_level(log_object, level) { || ... } click to toggle source

@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