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/minitest_plugin.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/droby_log_helpers.rb,
lib/roby/test/minitest_reporter.rb,
lib/roby/test/robot_test_helpers.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, DRobyLogHelpers, DSL, ExpectExecution, MinitestHelpers, MinitestPlugin, RobotTestHelpers, RobyAppHelpers, RunPlanners, Self, TeardownPlans Classes: Assertion, EmptyTask, Error, EventReporter, ExecutionExpectations, Goto2D, MinitestReporter, Spec, Stat, TestCase, ValidateStateMachine

Constant Summary collapse

BASE_PORT =
21_000
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 ExpectExecution

ExpectExecution::SETUP_METHODS

Constants included from Roby

BIN_DIR, Conf, FORMAT_EXCEPTION_RECURSION_GUARD_KEY, 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, Void, VoidClass

Instance Attribute Summary collapse

Attributes included from TeardownPlans

#default_teardown_poll, #registered_plans

Attributes included from ExpectExecution

#expect_execution_default_timeout

Class Method Summary collapse

Instance Method Summary collapse

Methods included from TeardownPlans

#clear_registered_plans, #initialize, #register_plan, #teardown_clear, #teardown_forced_killall, #teardown_killall, #teardown_registered_plans, #teardown_warn

Methods included from ExpectExecution

#add_expectations, #execute, #execute_one_cycle, #expect_execution, #reset_current_expect_execution, #setup_current_expect_execution

Methods included from Roby

RelationSpace, app, color, control, control=, disable_colors, display_exception, disposable, 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, find_in_path, flatten_exception, format_backtrace, format_exception, format_exception_recursion_guard, format_one_exception, format_time, from, from_conf, from_state, inside_control?, log_all_threads_backtraces, log_backtrace, log_callers, log_error, log_exception, log_exception_with_backtrace, log_level_enabled?, log_pp, make_backtrace_relative_to_app_dir, monotonic_time, null_disposable, on_exception, once, outside_control?, plan, poll_state_events, pretty_print_backtrace, sanitize_keywords, sanitize_keywords_to_array, sanitize_keywords_to_hash, scheduler, scheduler=, wait_one_cycle, wait_until, warn_deprecated, which

Instance Attribute Details

#appObject (readonly)

Returns the value of attribute app.



136
137
138
# File 'lib/roby/test/common.rb', line 136

def app
  @app
end

#controlObject (readonly)

The decision control component used by the tests



69
70
71
# File 'lib/roby/test/common.rb', line 69

def control
  @control
end

#original_collectionsObject (readonly)

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



112
113
114
# File 'lib/roby/test/common.rb', line 112

def original_collections
  @original_collections
end

#planObject (readonly)

The plan used by the tests



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

def plan
  @plan
end

#remote_processesObject (readonly)

The list of children started using #remote_process



322
323
324
# File 'lib/roby/test/common.rb', line 322

def remote_processes
  @remote_processes
end

Class Method Details

.register_spec_type(spec_type) ⇒ Object



245
246
247
248
249
250
# File 'lib/roby/test/spec.rb', line 245

def self.register_spec_type(spec_type)
    Minitest::Spec.register_spec_type spec_type do |desc, roby_spec: nil|
        (roby_spec == true || (roby_spec.nil? && !Roby::Test.self_test?)) &&
            yield(desc)
    end
end

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



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
56
57
# File 'lib/roby/test/tools.rb', line 6

def sampling(engine, duration, period, *fields)
    Test.info "starting sampling #{fields.join(', ')} every #{period}s for #{duration}s"

    samples = []
    fields.map!(&: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

            until timeout
                cv.wait(mt)
            end
            engine.remove_periodic_handler(id)
        end
    end

    samples
end

.self_test=(flag) ⇒ Object

Set #self_test?



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

def self.self_test=(flag)
    @self_test = flag
end

.self_test?Boolean

Whether we are running Roby’s own test suite or not

This is used for instance in test/spec to avoid using the Spec classes designed for ‘roby test` when running Roby’s own test suite

Returns:

  • (Boolean)


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

def self.self_test?
    @self_test
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



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
161
162
163
164
165
166
167
# File 'lib/roby/test/tools.rb', line 76

def stats(samples, spec)
    return if samples.empty?

    type = samples.first.class
    spec = spec.inject({}) 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(&: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



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

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

#deprecated_featureObject



102
103
104
105
106
107
108
# File 'lib/roby/test/common.rb', line 102

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

#execution_engineObject



86
87
88
# File 'lib/roby/test/common.rb', line 86

def execution_engine
    plan.execution_engine if plan&.executable?
end

#flexmock_call_original(object, method, *args, &block) ⇒ Object

Use to call the original method on a partial mock



302
303
304
305
# File 'lib/roby/test/common.rb', line 302

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



308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/roby/test/common.rb', line 308

def flexmock_invoke_original(object, method, *args, &block)
    if FlexMock::VERSION >= "3.0.0"
        object.instance_variable_get(:@flexmock_proxy)
              .proxy.flexmock_invoke_original(method, args, {}, block)
    elsif FlexMock::VERSION >= "2.4.0"
        object.instance_variable_get(:@flexmock_proxy)
              .proxy.flexmock_invoke_original(method, args, block)
    else
        object.instance_variable_get(:@flexmock_proxy)
              .proxy.flexmock_invoke_original(method, *args, &block)
    end
end

#inhibit_fatal_messages(&block) ⇒ Object

Deprecated.


167
168
169
170
# File 'lib/roby/test/common.rb', line 167

def inhibit_fatal_messages(&block)
    Roby.warn_deprecated "##{__method__} is deprecated, use #capture_log instead"
    with_log_level(Roby, Logger::FATAL, &block)
end

#make_random_plan(plan = Plan.new, tasks: 5, free_events: 5, task_relations: 5, event_relations: 5) ⇒ Object



388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/roby/test/common.rb', line 388

def make_random_plan(plan = Plan.new, tasks: 5, free_events: 5, task_relations: 5, event_relations: 5)
    tasks = (0...tasks).map do
        plan.add(t = Roby::Task.new)
        t
    end
    free_events = (0...free_events).map do
        plan.add(e = Roby::EventGenerator.new)
        e
    end
    events = (free_events + plan.task_events.to_a)

    task_relations.times do
        a = rand(tasks.size)
        b = rand(tasks.size)
        loop do
            begin
                tasks[a].depends_on tasks[b]
                break
            rescue Exception
                b = rand(tasks.size)
            end
        end
    end

    event_relations.times do
        a = rand(events.size)
        b = rand(events.size)
        loop do
            begin
                events[a].forward_to events[b]
                break
            rescue Exception
                b = rand(events.size)
            end
        end
    end
    plan
end

#new_planObject

Clear the plan and return it



91
92
93
94
# File 'lib/roby/test/common.rb', line 91

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


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
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/roby/test/common.rb', line 344

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

    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



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
276
277
278
279
280
# File 'lib/roby/test/common.rb', line 240

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 = []
    registered_plans.each do |p|
        engine = p.execution_engine

        loop do
            engine.start_new_cycle
            errors =
                begin
                    current_scheduler_state = engine.scheduler.enabled?
                    unless 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({})
            caller_block = nil

            break unless join_all_waiting_work && engine.has_waiting_work?
        end
    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



287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/roby/test/common.rb', line 287

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
    until 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.



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# File 'lib/roby/test/common.rb', line 430

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.


183
184
185
186
187
188
189
190
191
# File 'lib/roby/test/common.rb', line 183

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



122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/roby/test/common.rb', line 122

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)



117
118
119
# File 'lib/roby/test/common.rb', line 117

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

#set_log_level(log_object, level) ⇒ Object

Deprecated.


173
174
175
176
177
178
179
180
# File 'lib/roby/test/common.rb', line 173

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



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/roby/test/common.rb', line 138

def setup
    @app = Roby.app
    @app.development_mode = false
    Roby.app.reload_config
    @log_levels = {}
    @transactions = []

    @plan ||= Roby.app.plan
    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
end

#stop_remote_processesObject

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



454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/roby/test/common.rb', line 454

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



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
235
236
237
# File 'lib/roby/test/common.rb', line 209

def teardown
    Timecop.return

    @transactions.each do |trsc|
        unless trsc.finalized?
            trsc.discard_transaction
        end
    end
    teardown_registered_plans

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


194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/roby/test/common.rb', line 194

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