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.
Files changed (240) hide show
  1. data/.gitignore +29 -0
  2. data/History.txt +4 -0
  3. data/License-fr.txt +519 -0
  4. data/License.txt +515 -0
  5. data/Manifest.txt +245 -0
  6. data/NOTES +4 -0
  7. data/README.txt +163 -0
  8. data/Rakefile +161 -0
  9. data/TODO.txt +146 -0
  10. data/app/README.txt +24 -0
  11. data/app/Rakefile +8 -0
  12. data/app/config/ROBOT.rb +5 -0
  13. data/app/config/app.yml +91 -0
  14. data/app/config/init.rb +7 -0
  15. data/app/config/roby.yml +3 -0
  16. data/app/controllers/.gitattributes +0 -0
  17. data/app/controllers/ROBOT.rb +2 -0
  18. data/app/data/.gitattributes +0 -0
  19. data/app/planners/ROBOT/main.rb +6 -0
  20. data/app/planners/main.rb +5 -0
  21. data/app/scripts/distributed +3 -0
  22. data/app/scripts/generate/bookmarks +3 -0
  23. data/app/scripts/replay +3 -0
  24. data/app/scripts/results +3 -0
  25. data/app/scripts/run +3 -0
  26. data/app/scripts/server +3 -0
  27. data/app/scripts/shell +3 -0
  28. data/app/scripts/test +3 -0
  29. data/app/tasks/.gitattributes +0 -0
  30. data/app/tasks/ROBOT/.gitattributes +0 -0
  31. data/bin/roby +210 -0
  32. data/bin/roby-log +168 -0
  33. data/bin/roby-shell +25 -0
  34. data/doc/images/event_generalization.png +0 -0
  35. data/doc/images/exception_propagation_1.png +0 -0
  36. data/doc/images/exception_propagation_2.png +0 -0
  37. data/doc/images/exception_propagation_3.png +0 -0
  38. data/doc/images/exception_propagation_4.png +0 -0
  39. data/doc/images/exception_propagation_5.png +0 -0
  40. data/doc/images/replay_handler_error.png +0 -0
  41. data/doc/images/replay_handler_error_0.png +0 -0
  42. data/doc/images/replay_handler_error_1.png +0 -0
  43. data/doc/images/roby_cycle_overview.png +0 -0
  44. data/doc/images/roby_replay_02.png +0 -0
  45. data/doc/images/roby_replay_03.png +0 -0
  46. data/doc/images/roby_replay_04.png +0 -0
  47. data/doc/images/roby_replay_event_representation.png +0 -0
  48. data/doc/images/roby_replay_first_state.png +0 -0
  49. data/doc/images/roby_replay_relations.png +0 -0
  50. data/doc/images/roby_replay_startup.png +0 -0
  51. data/doc/images/task_event_generalization.png +0 -0
  52. data/doc/papers.rdoc +11 -0
  53. data/doc/styles/allison.css +314 -0
  54. data/doc/styles/allison.js +316 -0
  55. data/doc/styles/allison.rb +276 -0
  56. data/doc/styles/jamis.rb +593 -0
  57. data/doc/tutorials/01-GettingStarted.rdoc +86 -0
  58. data/doc/tutorials/02-GoForward.rdoc +220 -0
  59. data/doc/tutorials/03-PlannedPath.rdoc +268 -0
  60. data/doc/tutorials/04-EventPropagation.rdoc +236 -0
  61. data/doc/tutorials/05-ErrorHandling.rdoc +319 -0
  62. data/doc/tutorials/06-Overview.rdoc +40 -0
  63. data/doc/videos.rdoc +69 -0
  64. data/ext/droby/dump.cc +175 -0
  65. data/ext/droby/extconf.rb +3 -0
  66. data/ext/graph/algorithm.cc +746 -0
  67. data/ext/graph/extconf.rb +7 -0
  68. data/ext/graph/graph.cc +529 -0
  69. data/ext/graph/graph.hh +183 -0
  70. data/ext/graph/iterator_sequence.hh +102 -0
  71. data/ext/graph/undirected_dfs.hh +226 -0
  72. data/ext/graph/undirected_graph.hh +421 -0
  73. data/lib/roby.rb +41 -0
  74. data/lib/roby/app.rb +870 -0
  75. data/lib/roby/app/rake.rb +56 -0
  76. data/lib/roby/app/run.rb +14 -0
  77. data/lib/roby/app/scripts/distributed.rb +13 -0
  78. data/lib/roby/app/scripts/generate/bookmarks.rb +162 -0
  79. data/lib/roby/app/scripts/replay.rb +31 -0
  80. data/lib/roby/app/scripts/results.rb +15 -0
  81. data/lib/roby/app/scripts/run.rb +26 -0
  82. data/lib/roby/app/scripts/server.rb +18 -0
  83. data/lib/roby/app/scripts/shell.rb +88 -0
  84. data/lib/roby/app/scripts/test.rb +40 -0
  85. data/lib/roby/basic_object.rb +151 -0
  86. data/lib/roby/config.rb +5 -0
  87. data/lib/roby/control.rb +747 -0
  88. data/lib/roby/decision_control.rb +17 -0
  89. data/lib/roby/distributed.rb +32 -0
  90. data/lib/roby/distributed/base.rb +440 -0
  91. data/lib/roby/distributed/communication.rb +871 -0
  92. data/lib/roby/distributed/connection_space.rb +592 -0
  93. data/lib/roby/distributed/distributed_object.rb +206 -0
  94. data/lib/roby/distributed/drb.rb +62 -0
  95. data/lib/roby/distributed/notifications.rb +539 -0
  96. data/lib/roby/distributed/peer.rb +550 -0
  97. data/lib/roby/distributed/protocol.rb +529 -0
  98. data/lib/roby/distributed/proxy.rb +343 -0
  99. data/lib/roby/distributed/subscription.rb +311 -0
  100. data/lib/roby/distributed/transaction.rb +498 -0
  101. data/lib/roby/event.rb +897 -0
  102. data/lib/roby/exceptions.rb +234 -0
  103. data/lib/roby/executives/simple.rb +30 -0
  104. data/lib/roby/graph.rb +166 -0
  105. data/lib/roby/interface.rb +390 -0
  106. data/lib/roby/log.rb +3 -0
  107. data/lib/roby/log/chronicle.rb +303 -0
  108. data/lib/roby/log/console.rb +72 -0
  109. data/lib/roby/log/data_stream.rb +197 -0
  110. data/lib/roby/log/dot.rb +279 -0
  111. data/lib/roby/log/event_stream.rb +151 -0
  112. data/lib/roby/log/file.rb +340 -0
  113. data/lib/roby/log/gui/basic_display.ui +83 -0
  114. data/lib/roby/log/gui/chronicle.rb +26 -0
  115. data/lib/roby/log/gui/chronicle_view.rb +40 -0
  116. data/lib/roby/log/gui/chronicle_view.ui +70 -0
  117. data/lib/roby/log/gui/data_displays.rb +172 -0
  118. data/lib/roby/log/gui/data_displays.ui +155 -0
  119. data/lib/roby/log/gui/notifications.rb +26 -0
  120. data/lib/roby/log/gui/relations.rb +248 -0
  121. data/lib/roby/log/gui/relations.ui +123 -0
  122. data/lib/roby/log/gui/relations_view.rb +185 -0
  123. data/lib/roby/log/gui/relations_view.ui +149 -0
  124. data/lib/roby/log/gui/replay.rb +327 -0
  125. data/lib/roby/log/gui/replay_controls.rb +200 -0
  126. data/lib/roby/log/gui/replay_controls.ui +259 -0
  127. data/lib/roby/log/gui/runtime.rb +130 -0
  128. data/lib/roby/log/hooks.rb +185 -0
  129. data/lib/roby/log/logger.rb +202 -0
  130. data/lib/roby/log/notifications.rb +244 -0
  131. data/lib/roby/log/plan_rebuilder.rb +470 -0
  132. data/lib/roby/log/relations.rb +1056 -0
  133. data/lib/roby/log/server.rb +550 -0
  134. data/lib/roby/log/sqlite.rb +47 -0
  135. data/lib/roby/log/timings.rb +164 -0
  136. data/lib/roby/plan-object.rb +247 -0
  137. data/lib/roby/plan.rb +762 -0
  138. data/lib/roby/planning.rb +13 -0
  139. data/lib/roby/planning/loops.rb +302 -0
  140. data/lib/roby/planning/model.rb +906 -0
  141. data/lib/roby/planning/task.rb +151 -0
  142. data/lib/roby/propagation.rb +562 -0
  143. data/lib/roby/query.rb +619 -0
  144. data/lib/roby/relations.rb +583 -0
  145. data/lib/roby/relations/conflicts.rb +70 -0
  146. data/lib/roby/relations/ensured.rb +20 -0
  147. data/lib/roby/relations/error_handling.rb +23 -0
  148. data/lib/roby/relations/events.rb +9 -0
  149. data/lib/roby/relations/executed_by.rb +193 -0
  150. data/lib/roby/relations/hierarchy.rb +239 -0
  151. data/lib/roby/relations/influence.rb +10 -0
  152. data/lib/roby/relations/planned_by.rb +63 -0
  153. data/lib/roby/robot.rb +7 -0
  154. data/lib/roby/standard_errors.rb +218 -0
  155. data/lib/roby/state.rb +5 -0
  156. data/lib/roby/state/events.rb +221 -0
  157. data/lib/roby/state/information.rb +55 -0
  158. data/lib/roby/state/pos.rb +110 -0
  159. data/lib/roby/state/shapes.rb +32 -0
  160. data/lib/roby/state/state.rb +353 -0
  161. data/lib/roby/support.rb +92 -0
  162. data/lib/roby/task-operations.rb +182 -0
  163. data/lib/roby/task.rb +1618 -0
  164. data/lib/roby/test/common.rb +399 -0
  165. data/lib/roby/test/distributed.rb +214 -0
  166. data/lib/roby/test/tasks/empty_task.rb +9 -0
  167. data/lib/roby/test/tasks/goto.rb +36 -0
  168. data/lib/roby/test/tasks/simple_task.rb +23 -0
  169. data/lib/roby/test/testcase.rb +519 -0
  170. data/lib/roby/test/tools.rb +160 -0
  171. data/lib/roby/thread_task.rb +87 -0
  172. data/lib/roby/transactions.rb +462 -0
  173. data/lib/roby/transactions/proxy.rb +292 -0
  174. data/lib/roby/transactions/updates.rb +139 -0
  175. data/plugins/fault_injection/History.txt +4 -0
  176. data/plugins/fault_injection/README.txt +37 -0
  177. data/plugins/fault_injection/Rakefile +18 -0
  178. data/plugins/fault_injection/TODO.txt +0 -0
  179. data/plugins/fault_injection/app.rb +52 -0
  180. data/plugins/fault_injection/fault_injection.rb +89 -0
  181. data/plugins/fault_injection/test/test_fault_injection.rb +84 -0
  182. data/plugins/subsystems/README.txt +40 -0
  183. data/plugins/subsystems/Rakefile +18 -0
  184. data/plugins/subsystems/app.rb +171 -0
  185. data/plugins/subsystems/test/app/README +24 -0
  186. data/plugins/subsystems/test/app/Rakefile +8 -0
  187. data/plugins/subsystems/test/app/config/app.yml +71 -0
  188. data/plugins/subsystems/test/app/config/init.rb +9 -0
  189. data/plugins/subsystems/test/app/config/roby.yml +3 -0
  190. data/plugins/subsystems/test/app/planners/main.rb +20 -0
  191. data/plugins/subsystems/test/app/scripts/distributed +3 -0
  192. data/plugins/subsystems/test/app/scripts/replay +3 -0
  193. data/plugins/subsystems/test/app/scripts/results +3 -0
  194. data/plugins/subsystems/test/app/scripts/run +3 -0
  195. data/plugins/subsystems/test/app/scripts/server +3 -0
  196. data/plugins/subsystems/test/app/scripts/shell +3 -0
  197. data/plugins/subsystems/test/app/scripts/test +3 -0
  198. data/plugins/subsystems/test/app/tasks/services.rb +15 -0
  199. data/plugins/subsystems/test/test_subsystems.rb +71 -0
  200. data/test/distributed/test_communication.rb +178 -0
  201. data/test/distributed/test_connection.rb +282 -0
  202. data/test/distributed/test_execution.rb +373 -0
  203. data/test/distributed/test_mixed_plan.rb +341 -0
  204. data/test/distributed/test_plan_notifications.rb +238 -0
  205. data/test/distributed/test_protocol.rb +516 -0
  206. data/test/distributed/test_query.rb +102 -0
  207. data/test/distributed/test_remote_plan.rb +491 -0
  208. data/test/distributed/test_transaction.rb +463 -0
  209. data/test/mockups/tasks.rb +27 -0
  210. data/test/planning/test_loops.rb +380 -0
  211. data/test/planning/test_model.rb +427 -0
  212. data/test/planning/test_task.rb +106 -0
  213. data/test/relations/test_conflicts.rb +42 -0
  214. data/test/relations/test_ensured.rb +38 -0
  215. data/test/relations/test_executed_by.rb +149 -0
  216. data/test/relations/test_hierarchy.rb +158 -0
  217. data/test/relations/test_planned_by.rb +54 -0
  218. data/test/suite_core.rb +24 -0
  219. data/test/suite_distributed.rb +9 -0
  220. data/test/suite_planning.rb +3 -0
  221. data/test/suite_relations.rb +8 -0
  222. data/test/test_bgl.rb +508 -0
  223. data/test/test_control.rb +399 -0
  224. data/test/test_event.rb +894 -0
  225. data/test/test_exceptions.rb +592 -0
  226. data/test/test_interface.rb +37 -0
  227. data/test/test_log.rb +114 -0
  228. data/test/test_log_server.rb +132 -0
  229. data/test/test_plan.rb +584 -0
  230. data/test/test_propagation.rb +210 -0
  231. data/test/test_query.rb +266 -0
  232. data/test/test_relations.rb +180 -0
  233. data/test/test_state.rb +414 -0
  234. data/test/test_support.rb +16 -0
  235. data/test/test_task.rb +938 -0
  236. data/test/test_testcase.rb +122 -0
  237. data/test/test_thread_task.rb +73 -0
  238. data/test/test_transactions.rb +569 -0
  239. data/test/test_transactions_proxy.rb +198 -0
  240. metadata +570 -0
@@ -0,0 +1,9 @@
1
+ module Roby
2
+ module Test
3
+ class EmptyTask < Roby::Task
4
+ terminates
5
+ forward :start => :success
6
+ end
7
+ end
8
+ end
9
+
@@ -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
+