Module: Roby::Test

Extended by:
Logger::Forward, Logger::Hierarchy
Includes:
Roby, TeardownPlans
Included in:
Self, TestCase
Defined in:
lib/roby/test/common.rb,
lib/roby/test/dsl.rb,
lib/roby/test/self.rb,
lib/roby/test/spec.rb,
lib/roby/test/error.rb,
lib/roby/test/tools.rb,
lib/roby/test/testcase.rb,
lib/roby/test/assertion.rb,
lib/roby/test/assertions.rb,
lib/roby/test/tasks/goto.rb,
lib/roby/test/run_planners.rb,
lib/roby/test/aruba_minitest.rb,
lib/roby/test/event_reporter.rb,
lib/roby/test/teardown_plans.rb,
lib/roby/test/expect_execution.rb,
lib/roby/test/minitest_helpers.rb,
lib/roby/test/roby_app_helpers.rb,
lib/roby/test/tasks/empty_task.rb,
lib/roby/test/execution_expectations.rb,
lib/roby/test/validate_state_machine.rb

Overview

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 Also:

Defined Under Namespace

Modules: ArubaMinitest, Assertions, DSL, ExpectExecution, MinitestHelpers, RobyAppHelpers, RunPlanners, Self, TeardownPlans Classes: Assertion, EmptyTask, Error, EventReporter, ExecutionExpectations, Goto2D, Spec, Stat, TestCase, ValidateStateMachine

Constant Summary collapse

BASE_PORT =
21000
DISCOVERY_SERVER =
"druby://localhost:#{BASE_PORT}"
REMOTE_PORT =
BASE_PORT + 1
LOCAL_PORT =
BASE_PORT + 2
REMOTE_SERVER =
"druby://localhost:#{BASE_PORT + 3}"
LOCAL_SERVER =
"druby://localhost:#{BASE_PORT + 4}"

Constants included from Roby

BIN_DIR, Conf, LOG_SYMBOLIC_TO_NUMERIC, NullTask, ROBY_LIB_DIR, ROBY_ROOT_DIR, RX_IN_FRAMEWORK, RX_IN_METARUBY, RX_IN_UTILRB, RX_REQUIRE, SelfTest, State, TaskService, VERSION, VirtualTask

Instance Attribute Summary collapse

Attributes included from TeardownPlans

#registered_plans

Class Method Summary collapse

Instance Method Summary collapse

Methods included from TeardownPlans

#clear_registered_plans, #register_plan, #teardown_registered_plans

Methods included from Roby

RelationSpace, app, color, control, disable_colors, display_exception, do_display_exception, do_display_exception_formatted, do_display_exception_raw, each_cycle, enable_colors, enable_colors_if_available, engine, error_deprecated, every, execute, execution_engine, filter_backtrace, flatten_exception, format_backtrace, format_exception, format_one_exception, format_time, from, from_conf, from_state, inside_control?, log_backtrace, log_error, log_exception, log_exception_with_backtrace, log_level_enabled?, log_pp, on_exception, once, outside_control?, plan, poll_state_events, pretty_print_backtrace, scheduler, scheduler=, wait_one_cycle, wait_until, warn_deprecated

Instance Attribute Details

#appObject (readonly)

Returns the value of attribute app.



122
123
124
# File 'lib/roby/test/common.rb', line 122

def app
  @app
end

#controlObject (readonly)

The decision control component used by the tests



66
67
68
# File 'lib/roby/test/common.rb', line 66

def control
  @control
end

#original_collectionsObject (readonly)

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



98
99
100
# File 'lib/roby/test/common.rb', line 98

def original_collections
  @original_collections
end

#planObject (readonly)

The plan used by the tests



64
65
66
# File 'lib/roby/test/common.rb', line 64

def plan
  @plan
end

#remote_processesObject (readonly)

The list of children started using #remote_process



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

def remote_processes
  @remote_processes
end

Class Method Details

.sampling(engine, duration, period, *fields) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 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) ⇒ Object

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



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# 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

Instance Method Details

#create_transactionObject



82
83
84
85
86
# File 'lib/roby/test/common.rb', line 82

def create_transaction
    t = Roby::Transaction.new(plan)
    @transactions << t
    t
end

#deprecated_featureObject



88
89
90
91
92
93
94
# 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) ⇒ Object



72
73
74
# File 'lib/roby/test/common.rb', line 72

def execute(&block)
    execution_engine.execute(&block)
end

#execution_engineObject



68
69
70
# 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) ⇒ Object

Use to call the original method on a partial mock



297
298
299
300
# 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) ⇒ Object

Use to call the original method on a partial mock



303
304
305
# 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) ⇒ Object

Deprecated.


156
157
158
159
# 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_planObject

Clear the plan and return it



77
78
79
80
# File 'lib/roby/test/common.rb', line 77

def new_plan
    plan.clear
    plan
end

#prepare_plan(options) ⇒ Object

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


330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# 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) ⇒ Object

Process pending events



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# 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) ⇒ Object

Repeatedly process events until a condition is met

Yield Returns:

  • (Boolean)

    true if the condition is met, false otherwise



282
283
284
285
286
287
288
289
290
291
292
293
294
# 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_processObject

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.



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# 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) ⇒ Object

Deprecated.


172
173
174
175
176
177
178
179
180
# 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_collectionsObject

Restors the collections saved by #save_collection to their previous state



108
109
110
111
112
113
114
115
116
117
118
119
120
# 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) ⇒ Object

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)



103
104
105
# File 'lib/roby/test/common.rb', line 103

def save_collection(obj)
    original_collections << [obj, obj.dup]
end

#set_log_level(log_object, level) ⇒ Object

Deprecated.


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

#setupObject



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# 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_processesObject

Stop all the remote processes that have been started using #remote_process



400
401
402
403
404
405
406
407
408
409
410
411
412
# 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

#teardownObject



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# 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) ⇒ Object

Deprecated.


183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# 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