Module: Roby::Test

Extended by:
Logger::Forward, Logger::Hierarchy
Includes:
Roby
Included in:
Distributed::Test, TestCase
Defined in:
lib/roby/test/tools.rb,
lib/roby/test/common.rb,
lib/roby/test/testcase.rb,
lib/roby/test/tasks/goto.rb,
lib/roby/test/tasks/empty_task.rb,
lib/roby/test/tasks/simple_task.rb

Defined Under Namespace

Modules: Assertions Classes: EmptyTask, FailedTimeout, Goto2D, SimpleTask, Stat, TestCase

Constant Summary collapse

Unit =
::Test::Unit
BASE_PORT =
1245
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}"
ASSERT_ANY_EVENTS_TLS =
:assert_any_events

Constants included from Roby

ROBY_LIB_DIR, ROBY_ROOT_DIR, RX_IN_FRAMEWORK, VERSION

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Roby

RelationSpace, app, check_failed_missions, condition_variable, control_thread, each_cycle, each_exception_handler, every, execute, filter_backtrace, format_exception, inside_control?, load_all_relations, log_exception, on_exception, once, outside_control?, poll_state_events, pretty_print_backtrace, return_condition_variable, wait_one_cycle, wait_until

Methods included from ExceptionHandlingObject

#handle_exception, #pass_exception

Class Attribute Details

.check_allocation_countObject

Returns the value of attribute check_allocation_count.



21
22
23
# File 'lib/roby/test/common.rb', line 21

def check_allocation_count
  @check_allocation_count
end

.event_assertionsObject (readonly)

A [thread, cv, positive, negative] list of event assertions



24
25
26
# File 'lib/roby/test/testcase.rb', line 24

def event_assertions
  @event_assertions
end

.waiting_threadsObject (readonly)

A set of threads waiting for something to happen. This is used during #teardown to make sure no threads are block indefinitely



68
69
70
# File 'lib/roby/test/testcase.rb', line 68

def waiting_threads
  @waiting_threads
end

Instance Attribute Details

#console_loggerObject

The console logger object. See #console_logger=



336
337
338
# File 'lib/roby/test/common.rb', line 336

def console_logger
  @console_logger
end

#original_collectionsObject (readonly)

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



35
36
37
# File 'lib/roby/test/common.rb', line 35

def original_collections
  @original_collections
end

#remote_processesObject (readonly)

The list of children started using #remote_process



194
195
196
# File 'lib/roby/test/common.rb', line 194

def remote_processes
  @remote_processes
end

#timingsObject (readonly)

Returns the value of attribute timings.



19
20
21
# File 'lib/roby/test/common.rb', line 19

def timings
  @timings
end

Class Method Details

.assert_any_event_result(positive, negative) ⇒ Object

Tests for events in positive and negative and returns the set of failing events if the assertion has finished. If the set is empty, it means that the assertion finished successfully



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/roby/test/testcase.rb', line 30

def assert_any_event_result(positive, negative)
		if positive_ev = positive.find { |ev| ev.happened? }
 return false, "#{positive_ev} happened"
		end
		failure = negative.find_all { |ev| ev.happened? }
		unless failure.empty?
 return true, "#{failure} happened"
		end

		if positive.all? { |ev| ev.unreachable? }
 return true, "all positive events are unreachable"
		end

		nil
end

.check_event_assertionsObject

This method is inserted in the control thread to implement Assertions#assert_events



48
49
50
51
52
53
54
55
56
57
# File 'lib/roby/test/testcase.rb', line 48

def check_event_assertions
		event_assertions.delete_if do |thread, cv, positive, negative|
 error, result = assert_any_event_result(positive, negative)
 if !error.nil?
			thread[ASSERT_ANY_EVENTS_TLS] = [error, result]
			cv.broadcast
			true
 end
		end
end

.finalize_event_assertionsObject



59
60
61
62
63
64
# File 'lib/roby/test/testcase.rb', line 59

def finalize_event_assertions
		check_event_assertions
		event_assertions.dup.each do |thread, *_|
 thread.raise ControlQuitError
		end
end

.interrupt_waiting_threadsObject

This proc is to be called by Control when it quits. It makes sure that threads which are waiting are interrupted



72
73
74
75
76
77
78
# File 'lib/roby/test/testcase.rb', line 72

def interrupt_waiting_threads
		waiting_threads.dup.each do |task|
 task.raise ControlQuitError
		end
ensure
		waiting_threads.clear
end

.sampling(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
# File 'lib/roby/test/tools.rb', line 4

def sampling(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
			id = Roby::Control.every(period) do
  result = yield
  if result
				if compute_time
   result << Roby.control.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
cv.broadcast
   end
				end
  end
			end

			cv.wait(mt)
			Roby::Control.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



70
71
72
73
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
# File 'lib/roby/test/tools.rb', line 70

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

#assert_doesnt_timeout(seconds, message = "watchdog #{seconds} failed") ⇒ Object

Checks that the given block returns within seconds seconds



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

def assert_doesnt_timeout(seconds, message = "watchdog #{seconds} failed")
    watched_thread = Thread.current
    watchdog = Thread.new do
	sleep(seconds)
	watched_thread.raise FailedTimeout
    end

    assert_block(message) do
	begin
	    yield
	    true
	rescue FailedTimeout
	ensure
	    watchdog.kill
	    watchdog.join
	end
    end
end

#assert_marshallable(object) ⇒ Object



327
328
329
330
331
332
333
# File 'lib/roby/test/common.rb', line 327

def assert_marshallable(object)
    begin
	Marshal.dump(object)
	true
    rescue TypeError
    end
end

#assert_original_error(klass, localized_error_type = LocalizedError) ⇒ Object



292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/roby/test/common.rb', line 292

def assert_original_error(klass, localized_error_type = LocalizedError)
    old_level = Roby.logger.level
    Roby.logger.level = Logger::FATAL
    assert_nothing_raised do
	begin
	    yield
	rescue localized_error_type => e
	    assert_respond_to(e, :error)
	    assert_kind_of(klass, e.error)
	end
    end
ensure
    Roby.logger.level = old_level
end

#display_event_structure(object, relation, indent = " ") ⇒ Object



382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/roby/test/common.rb', line 382

def display_event_structure(object, relation, indent = "  ")
    result   = object.to_s
    object.history.each do |event|
	result << "#{indent}#{event.time.to_hms} #{event}"
    end
    children = object.child_objects(relation)
    unless children.empty?
	result << " ->\n" << indent
	children.each do |child|
	    result << display_event_structure(child, relation, indent + "  ")
	end
    end

    result
end

#display_timings!Object



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/roby/test/common.rb', line 340

def display_timings!
    timings = self.timings.sort_by { |_, t| t }
    ref = timings[0].last

    format, header, times = "", [], []
    format << "%#{method_name.size}s"
    header << method_name
    times  << ""
    timings.each do |name, time| 
	name = name.to_s
	time = "%.2f" % [time - ref]

	col_size = [name.size, time.size].max
	format << " % #{col_size}s"
	header << name
	times << time
    end

    puts
    puts format % header
    puts format % times
end

#new_planObject

Clear the plan and return it



28
29
30
31
# File 'lib/roby/test/common.rb', line 28

def new_plan
    Roby.plan.clear
    plan
end

#planObject

The plan used by the tests



25
# File 'lib/roby/test/common.rb', line 25

def plan; Roby.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


216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/roby/test/common.rb', line 216

def prepare_plan(options)
    options = validate_options options,
	:missions => 0, :discover => 0, :tasks => 0,
	:permanent => 0,
	:model => Roby::Task, :plan => plan

    missions, permanent, discovered, tasks = [], [], [], []
    (1..options[:missions]).each do |i|
	options[:plan].insert(t = options[:model].new(:id => "mission-#{i}"))
	missions << t
    end
    (1..options[:permanent]).each do |i|
	options[:plan].permanent(t = options[:model].new(:id => "perm-#{i}"))
	permanent << t
    end
    (1..options[:discover]).each do |i|
	options[:plan].discover(t = options[:model].new(:id => "discover-#{i}"))
	discovered << t
    end
    (1..options[:tasks]).each do |i|
	tasks << options[:model].new(:id => "task-#{i}")
    end

    result = []
    [missions, permanent, discovered, tasks].each do |set|
	unless set.empty?
	    set = *set
	    result << set
	end
    end
    if result.size == 1 then result.first
    else result
    end
end

#process_eventsObject

Process pending events



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

def process_events
    Roby::Control.synchronize do
	Roby.control.process_events
    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.



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/roby/test/common.rb', line 254

def remote_process
    start_r, start_w= IO.pipe
    quit_r, quit_w = IO.pipe
    remote_pid = fork do
	start_r.close
	yield
	start_w.write('OK')
	quit_r.read(2)
    end
    start_w.close
    start_r.read(2)

    remote_processes << [remote_pid, quit_w]
    remote_pid

ensure
    start_r.close
end

#restore_collectionsObject

Restors the collections saved by #save_collection to their previous state



45
46
47
48
49
50
51
52
53
54
# File 'lib/roby/test/common.rb', line 45

def restore_collections
    original_collections.each do |col, backup|
	col.clear
	if col.kind_of?(Hash)
	    col.merge! backup
	else
	    backup.each(&col.method(:<<))
	end
    end
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)



40
41
42
# File 'lib/roby/test/common.rb', line 40

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

#setupObject



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/roby/test/common.rb', line 56

def setup
    @console_logger ||= false
    if !defined? Roby::State
	Roby.app.reset
    end

    @original_roby_logger_level = Roby.logger.level
    @timings = { :start => Time.now }

    @original_collections = []
    Thread.abort_on_exception = true
    @remote_processes = []

    if Test.check_allocation_count
	GC.start
	GC.disable
    end

    unless DRb.primary_server
	DRb.start_service 'druby://localhost:0'
    end

    if defined? Roby::Planning::Planner
	Roby::Planning::Planner.last_id = 0 
    end

    # Save and restore Control's global arrays
    save_collection Roby::Control.event_processing
    save_collection Roby::Control.structure_checks
    save_collection Roby::Control.at_cycle_end_handlers
    save_collection Roby::EventGenerator.event_gathering
    Roby.control.abort_on_exception = true
    Roby.control.abort_on_application_exception = true
    Roby.control.abort_on_framework_exception = true

    save_collection Roby::Propagation.event_ordering
    save_collection Roby::Propagation.delayed_events

    save_collection Roby.exception_handlers
    timings[:setup] = Time.now
end

#stop_remote_processesObject

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



274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/roby/test/common.rb', line 274

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



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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/roby/test/common.rb', line 116

def teardown
    timings[:quit] = Time.now
    teardown_plan
    timings[:teardown_plan] = Time.now

    stop_remote_processes
    DRb.stop_service if DRb.thread

    restore_collections

    # Clear all relation graphs in TaskStructure and EventStructure
    spaces = []
    if defined? Roby::TaskStructure
	spaces << Roby::TaskStructure
    end
    if defined? Roby::EventStructure
	spaces << Roby::EventStructure
    end
    spaces.each do |space|
	space.relations.each do |rel| 
	    vertices = rel.enum_for(:each_vertex).to_a
	    unless vertices.empty?
		Roby.warn "  the following vertices are still present in #{rel}: #{vertices.to_a}"
		vertices.each { |v| v.clear_vertex }
	    end
	end
    end

    Roby::TaskStructure::Hierarchy.interesting_events.clear
    if defined? Roby::Control
	Roby.control.abort_on_exception = false
	Roby.control.abort_on_application_exception = false
	Roby.control.abort_on_framework_exception = false
    end

    if defined? Roby::Log
	Roby::Log.known_objects.clear
    end

    if Test.check_allocation_count
	require 'utilrb/objectstats'
	count = ObjectStats.count
	GC.start
	remains = ObjectStats.count
	Roby.warn "#{count} -> #{remains} (#{count - remains})"
    end
    timings[:end] = Time.now

    if display_timings?
	begin
	    display_timings!
	rescue
	    Roby.warn $!.full_message
	end
    end

rescue Exception => e
    STDERR.puts "failed teardown: #{e.full_message}"

ensure
    while Roby.control.running?
	Roby.control.quit
	Roby.control.join rescue nil
    end
    Roby.plan.clear

    Roby.logger.level = @original_roby_logger_level
    self.console_logger = false
end

#teardown_planObject



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/roby/test/common.rb', line 98

def teardown_plan
    old_gc_roby_logger_level = Roby.logger.level
    if debug_gc?
	Roby.logger.level = Logger::DEBUG
    end

    if !Roby.control.running?
	Roby.control.run :detach => true
    end

    Roby.control.quit
    Roby.control.join
    plan.clear

ensure
    Roby.logger.level = old_gc_roby_logger_level
end

#wait_thread_stopped(thread) ⇒ Object



375
376
377
378
379
380
# File 'lib/roby/test/common.rb', line 375

def wait_thread_stopped(thread)
    while !thread.stop?
	sleep(0.1)
	raise "#{thread} died" unless thread.alive?
    end
end