roby 0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +29 -0
- data/History.txt +4 -0
- data/License-fr.txt +519 -0
- data/License.txt +515 -0
- data/Manifest.txt +245 -0
- data/NOTES +4 -0
- data/README.txt +163 -0
- data/Rakefile +161 -0
- data/TODO.txt +146 -0
- data/app/README.txt +24 -0
- data/app/Rakefile +8 -0
- data/app/config/ROBOT.rb +5 -0
- data/app/config/app.yml +91 -0
- data/app/config/init.rb +7 -0
- data/app/config/roby.yml +3 -0
- data/app/controllers/.gitattributes +0 -0
- data/app/controllers/ROBOT.rb +2 -0
- data/app/data/.gitattributes +0 -0
- data/app/planners/ROBOT/main.rb +6 -0
- data/app/planners/main.rb +5 -0
- data/app/scripts/distributed +3 -0
- data/app/scripts/generate/bookmarks +3 -0
- data/app/scripts/replay +3 -0
- data/app/scripts/results +3 -0
- data/app/scripts/run +3 -0
- data/app/scripts/server +3 -0
- data/app/scripts/shell +3 -0
- data/app/scripts/test +3 -0
- data/app/tasks/.gitattributes +0 -0
- data/app/tasks/ROBOT/.gitattributes +0 -0
- data/bin/roby +210 -0
- data/bin/roby-log +168 -0
- data/bin/roby-shell +25 -0
- data/doc/images/event_generalization.png +0 -0
- data/doc/images/exception_propagation_1.png +0 -0
- data/doc/images/exception_propagation_2.png +0 -0
- data/doc/images/exception_propagation_3.png +0 -0
- data/doc/images/exception_propagation_4.png +0 -0
- data/doc/images/exception_propagation_5.png +0 -0
- data/doc/images/replay_handler_error.png +0 -0
- data/doc/images/replay_handler_error_0.png +0 -0
- data/doc/images/replay_handler_error_1.png +0 -0
- data/doc/images/roby_cycle_overview.png +0 -0
- data/doc/images/roby_replay_02.png +0 -0
- data/doc/images/roby_replay_03.png +0 -0
- data/doc/images/roby_replay_04.png +0 -0
- data/doc/images/roby_replay_event_representation.png +0 -0
- data/doc/images/roby_replay_first_state.png +0 -0
- data/doc/images/roby_replay_relations.png +0 -0
- data/doc/images/roby_replay_startup.png +0 -0
- data/doc/images/task_event_generalization.png +0 -0
- data/doc/papers.rdoc +11 -0
- data/doc/styles/allison.css +314 -0
- data/doc/styles/allison.js +316 -0
- data/doc/styles/allison.rb +276 -0
- data/doc/styles/jamis.rb +593 -0
- data/doc/tutorials/01-GettingStarted.rdoc +86 -0
- data/doc/tutorials/02-GoForward.rdoc +220 -0
- data/doc/tutorials/03-PlannedPath.rdoc +268 -0
- data/doc/tutorials/04-EventPropagation.rdoc +236 -0
- data/doc/tutorials/05-ErrorHandling.rdoc +319 -0
- data/doc/tutorials/06-Overview.rdoc +40 -0
- data/doc/videos.rdoc +69 -0
- data/ext/droby/dump.cc +175 -0
- data/ext/droby/extconf.rb +3 -0
- data/ext/graph/algorithm.cc +746 -0
- data/ext/graph/extconf.rb +7 -0
- data/ext/graph/graph.cc +529 -0
- data/ext/graph/graph.hh +183 -0
- data/ext/graph/iterator_sequence.hh +102 -0
- data/ext/graph/undirected_dfs.hh +226 -0
- data/ext/graph/undirected_graph.hh +421 -0
- data/lib/roby.rb +41 -0
- data/lib/roby/app.rb +870 -0
- data/lib/roby/app/rake.rb +56 -0
- data/lib/roby/app/run.rb +14 -0
- data/lib/roby/app/scripts/distributed.rb +13 -0
- data/lib/roby/app/scripts/generate/bookmarks.rb +162 -0
- data/lib/roby/app/scripts/replay.rb +31 -0
- data/lib/roby/app/scripts/results.rb +15 -0
- data/lib/roby/app/scripts/run.rb +26 -0
- data/lib/roby/app/scripts/server.rb +18 -0
- data/lib/roby/app/scripts/shell.rb +88 -0
- data/lib/roby/app/scripts/test.rb +40 -0
- data/lib/roby/basic_object.rb +151 -0
- data/lib/roby/config.rb +5 -0
- data/lib/roby/control.rb +747 -0
- data/lib/roby/decision_control.rb +17 -0
- data/lib/roby/distributed.rb +32 -0
- data/lib/roby/distributed/base.rb +440 -0
- data/lib/roby/distributed/communication.rb +871 -0
- data/lib/roby/distributed/connection_space.rb +592 -0
- data/lib/roby/distributed/distributed_object.rb +206 -0
- data/lib/roby/distributed/drb.rb +62 -0
- data/lib/roby/distributed/notifications.rb +539 -0
- data/lib/roby/distributed/peer.rb +550 -0
- data/lib/roby/distributed/protocol.rb +529 -0
- data/lib/roby/distributed/proxy.rb +343 -0
- data/lib/roby/distributed/subscription.rb +311 -0
- data/lib/roby/distributed/transaction.rb +498 -0
- data/lib/roby/event.rb +897 -0
- data/lib/roby/exceptions.rb +234 -0
- data/lib/roby/executives/simple.rb +30 -0
- data/lib/roby/graph.rb +166 -0
- data/lib/roby/interface.rb +390 -0
- data/lib/roby/log.rb +3 -0
- data/lib/roby/log/chronicle.rb +303 -0
- data/lib/roby/log/console.rb +72 -0
- data/lib/roby/log/data_stream.rb +197 -0
- data/lib/roby/log/dot.rb +279 -0
- data/lib/roby/log/event_stream.rb +151 -0
- data/lib/roby/log/file.rb +340 -0
- data/lib/roby/log/gui/basic_display.ui +83 -0
- data/lib/roby/log/gui/chronicle.rb +26 -0
- data/lib/roby/log/gui/chronicle_view.rb +40 -0
- data/lib/roby/log/gui/chronicle_view.ui +70 -0
- data/lib/roby/log/gui/data_displays.rb +172 -0
- data/lib/roby/log/gui/data_displays.ui +155 -0
- data/lib/roby/log/gui/notifications.rb +26 -0
- data/lib/roby/log/gui/relations.rb +248 -0
- data/lib/roby/log/gui/relations.ui +123 -0
- data/lib/roby/log/gui/relations_view.rb +185 -0
- data/lib/roby/log/gui/relations_view.ui +149 -0
- data/lib/roby/log/gui/replay.rb +327 -0
- data/lib/roby/log/gui/replay_controls.rb +200 -0
- data/lib/roby/log/gui/replay_controls.ui +259 -0
- data/lib/roby/log/gui/runtime.rb +130 -0
- data/lib/roby/log/hooks.rb +185 -0
- data/lib/roby/log/logger.rb +202 -0
- data/lib/roby/log/notifications.rb +244 -0
- data/lib/roby/log/plan_rebuilder.rb +470 -0
- data/lib/roby/log/relations.rb +1056 -0
- data/lib/roby/log/server.rb +550 -0
- data/lib/roby/log/sqlite.rb +47 -0
- data/lib/roby/log/timings.rb +164 -0
- data/lib/roby/plan-object.rb +247 -0
- data/lib/roby/plan.rb +762 -0
- data/lib/roby/planning.rb +13 -0
- data/lib/roby/planning/loops.rb +302 -0
- data/lib/roby/planning/model.rb +906 -0
- data/lib/roby/planning/task.rb +151 -0
- data/lib/roby/propagation.rb +562 -0
- data/lib/roby/query.rb +619 -0
- data/lib/roby/relations.rb +583 -0
- data/lib/roby/relations/conflicts.rb +70 -0
- data/lib/roby/relations/ensured.rb +20 -0
- data/lib/roby/relations/error_handling.rb +23 -0
- data/lib/roby/relations/events.rb +9 -0
- data/lib/roby/relations/executed_by.rb +193 -0
- data/lib/roby/relations/hierarchy.rb +239 -0
- data/lib/roby/relations/influence.rb +10 -0
- data/lib/roby/relations/planned_by.rb +63 -0
- data/lib/roby/robot.rb +7 -0
- data/lib/roby/standard_errors.rb +218 -0
- data/lib/roby/state.rb +5 -0
- data/lib/roby/state/events.rb +221 -0
- data/lib/roby/state/information.rb +55 -0
- data/lib/roby/state/pos.rb +110 -0
- data/lib/roby/state/shapes.rb +32 -0
- data/lib/roby/state/state.rb +353 -0
- data/lib/roby/support.rb +92 -0
- data/lib/roby/task-operations.rb +182 -0
- data/lib/roby/task.rb +1618 -0
- data/lib/roby/test/common.rb +399 -0
- data/lib/roby/test/distributed.rb +214 -0
- data/lib/roby/test/tasks/empty_task.rb +9 -0
- data/lib/roby/test/tasks/goto.rb +36 -0
- data/lib/roby/test/tasks/simple_task.rb +23 -0
- data/lib/roby/test/testcase.rb +519 -0
- data/lib/roby/test/tools.rb +160 -0
- data/lib/roby/thread_task.rb +87 -0
- data/lib/roby/transactions.rb +462 -0
- data/lib/roby/transactions/proxy.rb +292 -0
- data/lib/roby/transactions/updates.rb +139 -0
- data/plugins/fault_injection/History.txt +4 -0
- data/plugins/fault_injection/README.txt +37 -0
- data/plugins/fault_injection/Rakefile +18 -0
- data/plugins/fault_injection/TODO.txt +0 -0
- data/plugins/fault_injection/app.rb +52 -0
- data/plugins/fault_injection/fault_injection.rb +89 -0
- data/plugins/fault_injection/test/test_fault_injection.rb +84 -0
- data/plugins/subsystems/README.txt +40 -0
- data/plugins/subsystems/Rakefile +18 -0
- data/plugins/subsystems/app.rb +171 -0
- data/plugins/subsystems/test/app/README +24 -0
- data/plugins/subsystems/test/app/Rakefile +8 -0
- data/plugins/subsystems/test/app/config/app.yml +71 -0
- data/plugins/subsystems/test/app/config/init.rb +9 -0
- data/plugins/subsystems/test/app/config/roby.yml +3 -0
- data/plugins/subsystems/test/app/planners/main.rb +20 -0
- data/plugins/subsystems/test/app/scripts/distributed +3 -0
- data/plugins/subsystems/test/app/scripts/replay +3 -0
- data/plugins/subsystems/test/app/scripts/results +3 -0
- data/plugins/subsystems/test/app/scripts/run +3 -0
- data/plugins/subsystems/test/app/scripts/server +3 -0
- data/plugins/subsystems/test/app/scripts/shell +3 -0
- data/plugins/subsystems/test/app/scripts/test +3 -0
- data/plugins/subsystems/test/app/tasks/services.rb +15 -0
- data/plugins/subsystems/test/test_subsystems.rb +71 -0
- data/test/distributed/test_communication.rb +178 -0
- data/test/distributed/test_connection.rb +282 -0
- data/test/distributed/test_execution.rb +373 -0
- data/test/distributed/test_mixed_plan.rb +341 -0
- data/test/distributed/test_plan_notifications.rb +238 -0
- data/test/distributed/test_protocol.rb +516 -0
- data/test/distributed/test_query.rb +102 -0
- data/test/distributed/test_remote_plan.rb +491 -0
- data/test/distributed/test_transaction.rb +463 -0
- data/test/mockups/tasks.rb +27 -0
- data/test/planning/test_loops.rb +380 -0
- data/test/planning/test_model.rb +427 -0
- data/test/planning/test_task.rb +106 -0
- data/test/relations/test_conflicts.rb +42 -0
- data/test/relations/test_ensured.rb +38 -0
- data/test/relations/test_executed_by.rb +149 -0
- data/test/relations/test_hierarchy.rb +158 -0
- data/test/relations/test_planned_by.rb +54 -0
- data/test/suite_core.rb +24 -0
- data/test/suite_distributed.rb +9 -0
- data/test/suite_planning.rb +3 -0
- data/test/suite_relations.rb +8 -0
- data/test/test_bgl.rb +508 -0
- data/test/test_control.rb +399 -0
- data/test/test_event.rb +894 -0
- data/test/test_exceptions.rb +592 -0
- data/test/test_interface.rb +37 -0
- data/test/test_log.rb +114 -0
- data/test/test_log_server.rb +132 -0
- data/test/test_plan.rb +584 -0
- data/test/test_propagation.rb +210 -0
- data/test/test_query.rb +266 -0
- data/test/test_relations.rb +180 -0
- data/test/test_state.rb +414 -0
- data/test/test_support.rb +16 -0
- data/test/test_task.rb +938 -0
- data/test/test_testcase.rb +122 -0
- data/test/test_thread_task.rb +73 -0
- data/test/test_transactions.rb +569 -0
- data/test/test_transactions_proxy.rb +198 -0
- metadata +570 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
|
|
2
|
+
module Roby
|
|
3
|
+
module Test
|
|
4
|
+
class Goto2D < Roby::Task
|
|
5
|
+
terminates
|
|
6
|
+
argument :x, :y
|
|
7
|
+
|
|
8
|
+
def speed; State.goto_speed end
|
|
9
|
+
def x; arguments[:x] end
|
|
10
|
+
def y; arguments[:y] end
|
|
11
|
+
|
|
12
|
+
poll do
|
|
13
|
+
dx = x - State.pos.x
|
|
14
|
+
dy = y - State.pos.y
|
|
15
|
+
d = Math.sqrt(dx * dx + dy * dy)
|
|
16
|
+
if d > speed
|
|
17
|
+
State.pos.x += speed * dx / d
|
|
18
|
+
State.pos.y += speed * dy / d
|
|
19
|
+
else
|
|
20
|
+
State.pos.x = x
|
|
21
|
+
State.pos.y = y
|
|
22
|
+
emit :success
|
|
23
|
+
end
|
|
24
|
+
STDERR.puts "#{x} #{y} #{speed} #{State.pos}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module Planning
|
|
28
|
+
planning_library
|
|
29
|
+
method(:go_to) do
|
|
30
|
+
Goto2D.new(arguments)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'roby/task'
|
|
2
|
+
|
|
3
|
+
module Roby
|
|
4
|
+
module Test
|
|
5
|
+
# This task model is a simple task where +start+, +success+ and
|
|
6
|
+
# +failed+ are pass-through controlable events. They have an +id+
|
|
7
|
+
# argument which is automatically set to the object's #object_id if not
|
|
8
|
+
# explicitely given at initialization.
|
|
9
|
+
class SimpleTask < Roby::Task
|
|
10
|
+
argument :id
|
|
11
|
+
|
|
12
|
+
def initialize(arguments = {}) # :nodoc:
|
|
13
|
+
arguments = { :id => object_id.to_s }.merge(arguments)
|
|
14
|
+
super(arguments)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
event :start, :command => true
|
|
18
|
+
event :success, :command => true, :terminal => true
|
|
19
|
+
terminates
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
require 'roby'
|
|
2
|
+
require 'active_support/core_ext/string/inflections'
|
|
3
|
+
class String # :nodoc: all
|
|
4
|
+
include ActiveSupport::CoreExtensions::String::Inflections
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
require 'test/unit'
|
|
8
|
+
require 'roby/test/common'
|
|
9
|
+
require 'roby/test/tools'
|
|
10
|
+
require 'fileutils'
|
|
11
|
+
|
|
12
|
+
module Roby
|
|
13
|
+
module Test
|
|
14
|
+
extend Logger::Hierarchy
|
|
15
|
+
extend Logger::Forward
|
|
16
|
+
|
|
17
|
+
@event_assertions = []
|
|
18
|
+
@waiting_threads = []
|
|
19
|
+
|
|
20
|
+
ASSERT_ANY_EVENTS_TLS = :assert_any_events
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# A [thread, cv, positive, negative] list of event assertions
|
|
24
|
+
attr_reader :event_assertions
|
|
25
|
+
|
|
26
|
+
# Tests for events in +positive+ and +negative+ and returns
|
|
27
|
+
# the set of failing events if the assertion has finished.
|
|
28
|
+
# If the set is empty, it means that the assertion finished
|
|
29
|
+
# successfully
|
|
30
|
+
def assert_any_event_result(positive, negative)
|
|
31
|
+
if positive_ev = positive.find { |ev| ev.happened? }
|
|
32
|
+
return false, "#{positive_ev} happened"
|
|
33
|
+
end
|
|
34
|
+
failure = negative.find_all { |ev| ev.happened? }
|
|
35
|
+
unless failure.empty?
|
|
36
|
+
return true, "#{failure} happened"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if positive.all? { |ev| ev.unreachable? }
|
|
40
|
+
return true, "all positive events are unreachable"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# This method is inserted in the control thread to implement
|
|
47
|
+
# Assertions#assert_events
|
|
48
|
+
def check_event_assertions
|
|
49
|
+
event_assertions.delete_if do |thread, cv, positive, negative|
|
|
50
|
+
error, result = assert_any_event_result(positive, negative)
|
|
51
|
+
if !error.nil?
|
|
52
|
+
thread[ASSERT_ANY_EVENTS_TLS] = [error, result]
|
|
53
|
+
cv.broadcast
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def finalize_event_assertions
|
|
60
|
+
check_event_assertions
|
|
61
|
+
event_assertions.dup.each do |thread, *_|
|
|
62
|
+
thread.raise ControlQuitError
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A set of threads waiting for something to happen. This is used
|
|
67
|
+
# during #teardown to make sure no threads are block indefinitely
|
|
68
|
+
attr_reader :waiting_threads
|
|
69
|
+
|
|
70
|
+
# This proc is to be called by Control when it quits. It makes sure
|
|
71
|
+
# that threads which are waiting are interrupted
|
|
72
|
+
def interrupt_waiting_threads
|
|
73
|
+
waiting_threads.dup.each do |task|
|
|
74
|
+
task.raise ControlQuitError
|
|
75
|
+
end
|
|
76
|
+
ensure
|
|
77
|
+
waiting_threads.clear
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
end
|
|
81
|
+
Roby::Control.at_cycle_end(&method(:check_event_assertions))
|
|
82
|
+
Roby::Control.finalizers << method(:finalize_event_assertions)
|
|
83
|
+
Roby::Control.finalizers << method(:interrupt_waiting_threads)
|
|
84
|
+
|
|
85
|
+
module Assertions
|
|
86
|
+
# Wait for any event in +positive+ to happen. If +negative+ is
|
|
87
|
+
# non-empty, any event happening in this set will make the
|
|
88
|
+
# assertion fail. If events in +positive+ are task events, the
|
|
89
|
+
# :stop events of the corresponding tasks are added to negative
|
|
90
|
+
# automatically.
|
|
91
|
+
#
|
|
92
|
+
# If a block is given, it is called from within the control thread
|
|
93
|
+
# after the checks are in place
|
|
94
|
+
#
|
|
95
|
+
# So, to check that a task fails, do
|
|
96
|
+
#
|
|
97
|
+
# assert_events(task.event(:fail)) do
|
|
98
|
+
# task.start!
|
|
99
|
+
# end
|
|
100
|
+
#
|
|
101
|
+
def assert_any_event(positive, negative = [], msg = nil, &block)
|
|
102
|
+
control_priority do
|
|
103
|
+
Roby.condition_variable(false) do |cv|
|
|
104
|
+
positive = Array[*positive].to_value_set
|
|
105
|
+
negative = Array[*negative].to_value_set
|
|
106
|
+
|
|
107
|
+
unreachability_reason = ValueSet.new
|
|
108
|
+
Roby::Control.synchronize do
|
|
109
|
+
positive.each do |ev|
|
|
110
|
+
ev.if_unreachable(true) do |reason|
|
|
111
|
+
unreachability_reason << reason if reason
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
error, result = Test.assert_any_event_result(positive, negative)
|
|
116
|
+
if error.nil?
|
|
117
|
+
this_thread = Thread.current
|
|
118
|
+
|
|
119
|
+
Test.event_assertions << [this_thread, cv, positive, negative]
|
|
120
|
+
Roby.once(&block) if block_given?
|
|
121
|
+
begin
|
|
122
|
+
cv.wait(Roby::Control.mutex)
|
|
123
|
+
ensure
|
|
124
|
+
Test.event_assertions.delete_if { |thread, _| thread == this_thread }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
error, result = this_thread[ASSERT_ANY_EVENTS_TLS]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if error
|
|
131
|
+
if !unreachability_reason.empty?
|
|
132
|
+
msg = unreachability_reason.map do |reason|
|
|
133
|
+
if reason.respond_to?(:context)
|
|
134
|
+
context = reason.context.map do |obj|
|
|
135
|
+
if obj.kind_of?(Exception)
|
|
136
|
+
obj.full_message
|
|
137
|
+
else
|
|
138
|
+
obj.to_s
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
reason.to_s + context.join("\n ")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
msg.join("\n ")
|
|
145
|
+
|
|
146
|
+
flunk("#{msg} all positive events are unreachable for the following reason:\n #{msg}")
|
|
147
|
+
elsif msg
|
|
148
|
+
flunk("#{msg} failed: #{result}")
|
|
149
|
+
else
|
|
150
|
+
flunk(result)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Starts +task+ and checks it succeeds
|
|
159
|
+
def assert_succeeds(task, *args)
|
|
160
|
+
control_priority do
|
|
161
|
+
if !task.kind_of?(Roby::Task)
|
|
162
|
+
Roby.execute do
|
|
163
|
+
plan.insert(task = planner.send(task, *args))
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
assert_any_event([task.event(:success)], [], nil) do
|
|
168
|
+
plan.permanent(task)
|
|
169
|
+
task.start! if task.pending?
|
|
170
|
+
yield if block_given?
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def control_priority
|
|
176
|
+
old_priority = Thread.current.priority
|
|
177
|
+
Thread.current.priority = Roby.control.thread.priority + 1
|
|
178
|
+
|
|
179
|
+
yield
|
|
180
|
+
ensure
|
|
181
|
+
Thread.current.priority = old_priority
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# This assertion fails if the relative error between +found+ and
|
|
185
|
+
# +expected+is more than +error+
|
|
186
|
+
def assert_relative_error(expected, found, error, msg = "")
|
|
187
|
+
if expected == 0
|
|
188
|
+
assert_in_delta(0, found, error, "comparing #{found} to #{expected} in #{msg}")
|
|
189
|
+
else
|
|
190
|
+
assert_in_delta(0, (found - expected) / expected, error, "comparing #{found} to #{expected} in #{msg}")
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# This assertion fails if +found+ and +expected+ are more than +dl+
|
|
195
|
+
# meters apart in the x, y and z coordinates, or +dt+ radians apart
|
|
196
|
+
# in angles
|
|
197
|
+
def assert_same_position(expected, found, dl = 0.01, dt = 0.01, msg = "")
|
|
198
|
+
assert_relative_error(expected.x, found.x, dl, msg)
|
|
199
|
+
assert_relative_error(expected.y, found.y, dl, msg)
|
|
200
|
+
assert_relative_error(expected.z, found.z, dl, msg)
|
|
201
|
+
assert_relative_error(expected.yaw, found.yaw, dt, msg)
|
|
202
|
+
assert_relative_error(expected.pitch, found.pitch, dt, msg)
|
|
203
|
+
assert_relative_error(expected.roll, found.roll, dt, msg)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# This is the base class for running tests which uses a Roby control
|
|
208
|
+
# loop (i.e. plan execution).
|
|
209
|
+
#
|
|
210
|
+
# Because configuration and planning can be robot-specific, parts of
|
|
211
|
+
# the tests can also be splitted into generic parts and specific parts.
|
|
212
|
+
# The TestCase.robot statement allows to specify that a given test case
|
|
213
|
+
# is specific to a given robot, in which case it is ran only if the
|
|
214
|
+
# call to <tt>scripts/test</tt> specified a robot which matches (i.e.
|
|
215
|
+
# same name and type).
|
|
216
|
+
#
|
|
217
|
+
# Finally, two other mode of operation control the way tests are ran
|
|
218
|
+
# [simulation]
|
|
219
|
+
# if the <tt>--sim</tt> flag is given to <tt>scripts/test</tt>, the
|
|
220
|
+
# tests are ran under simulation. Otherwise, they are run in live
|
|
221
|
+
# mode (see Roby::Application for a description of simulation and
|
|
222
|
+
# live modes). It is possible to constrain that a given test method
|
|
223
|
+
# is run only in simulation or live mode with the TestCase.sim and
|
|
224
|
+
# TestCase.nosim statements:
|
|
225
|
+
#
|
|
226
|
+
# sim :sim_only
|
|
227
|
+
# def test_sim_only
|
|
228
|
+
# end
|
|
229
|
+
#
|
|
230
|
+
# nosim :live_only
|
|
231
|
+
# def test_live_only
|
|
232
|
+
# end
|
|
233
|
+
# [interactive]
|
|
234
|
+
# Sometime, it is hard to actually assess the quality of processing
|
|
235
|
+
# results automatically. In these cases, it is possible to show the
|
|
236
|
+
# user the result of data processing, and then ask if the result is
|
|
237
|
+
# valid by using the #user_validation method. Nonetheless, the tests
|
|
238
|
+
# can be ran in automatic mode, in which the assertions which require
|
|
239
|
+
# user validation are simply skipped. The <tt>--interactive</tt> or
|
|
240
|
+
# <tt>-i</tt> flags of <tt>scripts/test</tt> specify that user
|
|
241
|
+
# interaction is possible.
|
|
242
|
+
class TestCase < Test::Unit::TestCase
|
|
243
|
+
include Roby::Test
|
|
244
|
+
include Assertions
|
|
245
|
+
class << self
|
|
246
|
+
attribute(:case_config) { Hash.new }
|
|
247
|
+
attribute(:methods_config) { Hash.new }
|
|
248
|
+
attr_reader :app_setup
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Sets the robot configuration for this test case. If a block is
|
|
252
|
+
# given, it is called between the time the robot configuration is
|
|
253
|
+
# loaded and the time the test methods are started. It can
|
|
254
|
+
# therefore be used to change the robot configuration for the need
|
|
255
|
+
# of this particular test case
|
|
256
|
+
def self.robot(name, kind = name, &block)
|
|
257
|
+
@app_setup = [name, kind, block]
|
|
258
|
+
apply_robot_setup
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
@@first_time = true
|
|
262
|
+
# Loads the configuration as specified by TestCase.robot
|
|
263
|
+
def self.apply_robot_setup
|
|
264
|
+
app = Roby.app
|
|
265
|
+
if @@first_time
|
|
266
|
+
# Make sure the log directory is empty
|
|
267
|
+
if File.exists?(app.log_dir)
|
|
268
|
+
if !Dir.new(app.log_dir).empty?
|
|
269
|
+
if !STDIN.ask("#{app.log_dir} still exists and must be cleaned before starting. Proceed ? [N,y]", false)
|
|
270
|
+
raise "user abort"
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
FileUtils.rm_rf app.log_dir
|
|
274
|
+
end
|
|
275
|
+
@@first_time = false
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
name, kind, block = app_setup
|
|
279
|
+
# Silently ignore the test suites which use a different robot
|
|
280
|
+
if app.robot_name &&
|
|
281
|
+
(app.robot_name != name || app.robot_type != kind)
|
|
282
|
+
return
|
|
283
|
+
end
|
|
284
|
+
app.robot name, kind
|
|
285
|
+
app.reset
|
|
286
|
+
app.single
|
|
287
|
+
app.setup
|
|
288
|
+
if block
|
|
289
|
+
block.call
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
app.control.delete('executive')
|
|
293
|
+
|
|
294
|
+
yield if block_given?
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Returns a fresh MainPlanner object for the current plan
|
|
298
|
+
def planner
|
|
299
|
+
MainPlanner.new(plan)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def setup # :nodoc:
|
|
303
|
+
super
|
|
304
|
+
Roby::Test.waiting_threads << Thread.current
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def teardown # :nodoc:
|
|
308
|
+
Roby::Test.waiting_threads.delete(Thread.current)
|
|
309
|
+
super
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def method_config # :nodoc:
|
|
313
|
+
self.class.case_config.merge(self.class.methods_config[method_name] || Hash.new)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Returns true if user interaction is to be disabled during this test
|
|
317
|
+
def automatic_testing?
|
|
318
|
+
Roby.app.automatic_testing?
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Progress report for the curren test. If +max+ is given, then
|
|
322
|
+
# +value+ is assumed to be between 0 and +max+. Otherwise, +value+
|
|
323
|
+
# is a float value between 0 and 1 and is displayed as a percentage.
|
|
324
|
+
def progress(value, max = nil)
|
|
325
|
+
if max
|
|
326
|
+
print "\r#{@method_name} progress: #{value}/#{max}"
|
|
327
|
+
else
|
|
328
|
+
print "\r#{@method_name} progress: #{"%.2f %%" % [value * 100]}"
|
|
329
|
+
end
|
|
330
|
+
STDOUT.flush
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def user_interaction
|
|
334
|
+
return unless automatic_testing?
|
|
335
|
+
|
|
336
|
+
test_result = catch(:validation_result) do
|
|
337
|
+
yield
|
|
338
|
+
return
|
|
339
|
+
end
|
|
340
|
+
if test_result
|
|
341
|
+
flunk(*test_result)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Ask for user validation. The method first yields, and then asks
|
|
346
|
+
# the user if the showed dataset is nominal. If the tests are ran
|
|
347
|
+
# in automated mode (#automatic_testing? returns true), it does
|
|
348
|
+
# nothing.
|
|
349
|
+
def user_validation(msg)
|
|
350
|
+
return if automatic_testing?
|
|
351
|
+
|
|
352
|
+
assert_block(msg) do
|
|
353
|
+
STDOUT.puts "Now validating #{msg}"
|
|
354
|
+
yield
|
|
355
|
+
|
|
356
|
+
STDIN.ask("\rIs the result OK ? [N,y]", false)
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Do not run +test_name+ inside a simulation environment
|
|
361
|
+
# +test_name+ is the name of the method without +test_+. For
|
|
362
|
+
# instance:
|
|
363
|
+
# nosim :init
|
|
364
|
+
# def test_init
|
|
365
|
+
# end
|
|
366
|
+
#
|
|
367
|
+
# See also TestCase.sim
|
|
368
|
+
def self.nosim(*names)
|
|
369
|
+
names.each do |test_name|
|
|
370
|
+
config = (methods_config[test_name.to_s] ||= Hash.new)
|
|
371
|
+
config[:mode] = :nosim
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Run +test_name+ only inside a simulation environment
|
|
376
|
+
# +test_name+ is the name of the method without +test_+. For
|
|
377
|
+
# instance:
|
|
378
|
+
# sim :init
|
|
379
|
+
# def test_init
|
|
380
|
+
# end
|
|
381
|
+
#
|
|
382
|
+
# See also TestCase.nosim
|
|
383
|
+
def self.sim(*names)
|
|
384
|
+
names.each do |test_name|
|
|
385
|
+
config = (methods_config[test_name.to_s] ||= Hash.new)
|
|
386
|
+
config[:mode] = :sim
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def self.suite # :nodoc:
|
|
391
|
+
method_names = public_instance_methods(true)
|
|
392
|
+
tests = method_names.delete_if {|method_name| method_name !~ /^(dataset|test)./}
|
|
393
|
+
suite = Test::Unit::TestSuite.new(name)
|
|
394
|
+
tests.sort.each do |test|
|
|
395
|
+
catch(:invalid_test) do
|
|
396
|
+
suite << new(test)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
if (suite.empty?)
|
|
400
|
+
catch(:invalid_test) do
|
|
401
|
+
suite << new("default_test")
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
return suite
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def run(result) # :nodoc:
|
|
408
|
+
Roby::Test.waiting_threads.clear
|
|
409
|
+
|
|
410
|
+
self.class.apply_robot_setup do
|
|
411
|
+
yield if block_given?
|
|
412
|
+
|
|
413
|
+
case method_config[:mode]
|
|
414
|
+
when :nosim
|
|
415
|
+
return if Roby.app.simulation?
|
|
416
|
+
when :sim
|
|
417
|
+
return unless Roby.app.simulation?
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
@failed_test = false
|
|
421
|
+
begin
|
|
422
|
+
Roby.app.run do
|
|
423
|
+
super
|
|
424
|
+
end
|
|
425
|
+
rescue Exception => e
|
|
426
|
+
if @_result
|
|
427
|
+
add_error(e)
|
|
428
|
+
else
|
|
429
|
+
raise
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
keep_logdir = @failed_test || Roby.app.testing_keep_logs?
|
|
434
|
+
save_logdir = (@failed_test && automatic_testing?) || Roby.app.testing_keep_logs?
|
|
435
|
+
if save_logdir
|
|
436
|
+
subdir = @failed_test ? 'failures' : 'results'
|
|
437
|
+
basedir = File.join(APP_DIR, 'test', subdir)
|
|
438
|
+
dirname = Roby::Application.unique_dirname(basedir, dataset_prefix)
|
|
439
|
+
|
|
440
|
+
if Roby.app.testing_overwrites_logs?
|
|
441
|
+
dirname.gsub! /\.\d+$/, ''
|
|
442
|
+
FileUtils.rm_rf dirname
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
FileUtils.mv Roby.app.log_dir, dirname
|
|
446
|
+
end
|
|
447
|
+
if !keep_logdir
|
|
448
|
+
FileUtils.rm_rf Roby.app.log_dir
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
rescue Exception
|
|
453
|
+
puts "testcase #{method_name} teardown failed with\n#{$!.full_message}"
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def add_error(*args, &block) # :nodoc:
|
|
457
|
+
@failed_test = true
|
|
458
|
+
super
|
|
459
|
+
end
|
|
460
|
+
def add_failure(*args, &block) # :nodoc:
|
|
461
|
+
@failed_test = true
|
|
462
|
+
super
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# The directory in which datasets are to be saved
|
|
466
|
+
def datasets_dir
|
|
467
|
+
"#{APP_DIR}/test/datasets"
|
|
468
|
+
end
|
|
469
|
+
# The directory into which the datasets generated by the current
|
|
470
|
+
# testcase are to be saved.
|
|
471
|
+
def dataset_prefix
|
|
472
|
+
"#{Roby.app.robot_name}-#{self.class.name.gsub('TC_', '').underscore}/, '')}"
|
|
473
|
+
end
|
|
474
|
+
# Returns the full path of the file name into which the log file +file+
|
|
475
|
+
# should be saved to be referred to as the +dataset_name+ dataset
|
|
476
|
+
def dataset_file_path(dataset_name, file)
|
|
477
|
+
path = File.join(datasets_dir, dataset_name, file)
|
|
478
|
+
if !File.file?(path)
|
|
479
|
+
raise "#{path} does not exist"
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
path
|
|
483
|
+
rescue
|
|
484
|
+
flunk("dataset #{dataset_name} has not been generated: #{$!.message}")
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Saves +file+, which is taken in the log directory, in the
|
|
488
|
+
# test/datasets directory. The data set is saved as
|
|
489
|
+
# 'robot-testname-testmethod-suffix'
|
|
490
|
+
def save_dataset(files = nil, suffix = '')
|
|
491
|
+
destname = dataset_prefix
|
|
492
|
+
destname << "-#{suffix}" unless suffix.empty?
|
|
493
|
+
|
|
494
|
+
dir = File.join(datasets_dir, destname)
|
|
495
|
+
if File.exists?(dir)
|
|
496
|
+
relative_dir = dir.gsub(/^#{Regexp.quote(APP_DIR)}/, '')
|
|
497
|
+
unless STDIN.ask("\r#{relative_dir} already exists. Delete ? [N,y]", false)
|
|
498
|
+
raise "user abort"
|
|
499
|
+
end
|
|
500
|
+
FileUtils.rm_rf dir
|
|
501
|
+
end
|
|
502
|
+
FileUtils.mkdir_p(dir)
|
|
503
|
+
|
|
504
|
+
files ||= Dir.entries(Roby.app.log_dir).find_all do |path|
|
|
505
|
+
File.file? File.join(Roby.app.log_dir, path)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
[*files].each do |path|
|
|
509
|
+
FileUtils.mv "#{Roby.app.log_dir}/#{path}", dir
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def sampling(*args, &block); Test.sampling(*args, &block) end
|
|
514
|
+
def stats(*args, &block); Test.stats(*args, &block) end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
|