taski 0.5.0 → 0.7.1

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/README.md +168 -21
  4. data/docs/GUIDE.md +394 -0
  5. data/examples/README.md +65 -17
  6. data/examples/{context_demo.rb → args_demo.rb} +27 -27
  7. data/examples/clean_demo.rb +204 -0
  8. data/examples/data_pipeline_demo.rb +1 -1
  9. data/examples/group_demo.rb +113 -0
  10. data/examples/large_tree_demo.rb +519 -0
  11. data/examples/reexecution_demo.rb +93 -80
  12. data/examples/simple_progress_demo.rb +80 -0
  13. data/examples/system_call_demo.rb +56 -0
  14. data/lib/taski/{context.rb → args.rb} +3 -3
  15. data/lib/taski/execution/base_progress_display.rb +348 -0
  16. data/lib/taski/execution/execution_context.rb +383 -0
  17. data/lib/taski/execution/executor.rb +405 -134
  18. data/lib/taski/execution/plain_progress_display.rb +76 -0
  19. data/lib/taski/execution/registry.rb +17 -1
  20. data/lib/taski/execution/scheduler.rb +308 -0
  21. data/lib/taski/execution/simple_progress_display.rb +173 -0
  22. data/lib/taski/execution/task_output_pipe.rb +42 -0
  23. data/lib/taski/execution/task_output_router.rb +287 -0
  24. data/lib/taski/execution/task_wrapper.rb +215 -52
  25. data/lib/taski/execution/tree_progress_display.rb +349 -212
  26. data/lib/taski/execution/worker_pool.rb +104 -0
  27. data/lib/taski/section.rb +16 -3
  28. data/lib/taski/static_analysis/visitor.rb +3 -0
  29. data/lib/taski/task.rb +218 -37
  30. data/lib/taski/test_helper/errors.rb +13 -0
  31. data/lib/taski/test_helper/minitest.rb +38 -0
  32. data/lib/taski/test_helper/mock_registry.rb +51 -0
  33. data/lib/taski/test_helper/mock_wrapper.rb +46 -0
  34. data/lib/taski/test_helper/rspec.rb +38 -0
  35. data/lib/taski/test_helper.rb +214 -0
  36. data/lib/taski/version.rb +1 -1
  37. data/lib/taski.rb +211 -23
  38. data/sig/taski.rbs +207 -27
  39. metadata +25 -8
  40. data/docs/advanced-features.md +0 -625
  41. data/docs/api-guide.md +0 -509
  42. data/docs/error-handling.md +0 -684
  43. data/examples/section_progress_demo.rb +0 -78
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module Taski
6
+ module Execution
7
+ # Base class for progress display implementations.
8
+ # Provides common task tracking and lifecycle management.
9
+ # Subclasses override template methods for custom rendering.
10
+ class BaseProgressDisplay
11
+ # Shared task progress tracking
12
+ class TaskProgress
13
+ # Run lifecycle tracking
14
+ attr_accessor :run_state, :run_start_time, :run_end_time, :run_error, :run_duration
15
+ # Clean lifecycle tracking
16
+ attr_accessor :clean_state, :clean_start_time, :clean_end_time, :clean_error, :clean_duration
17
+ # Display properties
18
+ attr_accessor :is_impl_candidate
19
+ # Group tracking
20
+ attr_accessor :groups, :current_group_index
21
+
22
+ def initialize
23
+ # Run lifecycle
24
+ @run_state = :pending
25
+ @run_start_time = nil
26
+ @run_end_time = nil
27
+ @run_error = nil
28
+ @run_duration = nil
29
+ # Clean lifecycle
30
+ @clean_state = nil # nil means clean hasn't started
31
+ @clean_start_time = nil
32
+ @clean_end_time = nil
33
+ @clean_error = nil
34
+ @clean_duration = nil
35
+ # Display
36
+ @is_impl_candidate = false
37
+ # Groups
38
+ @groups = []
39
+ @current_group_index = nil
40
+ end
41
+
42
+ # Returns the most relevant state for display
43
+ def state
44
+ @clean_state || @run_state
45
+ end
46
+
47
+ # Legacy accessors for backward compatibility
48
+ def start_time
49
+ @clean_start_time || @run_start_time
50
+ end
51
+
52
+ def end_time
53
+ @clean_end_time || @run_end_time
54
+ end
55
+
56
+ def error
57
+ @clean_error || @run_error
58
+ end
59
+
60
+ def duration
61
+ @clean_duration || @run_duration
62
+ end
63
+ end
64
+
65
+ # Tracks the progress of a group within a task
66
+ class GroupProgress
67
+ attr_accessor :name, :state, :start_time, :end_time, :duration, :error, :last_message
68
+
69
+ def initialize(name)
70
+ @name = name
71
+ @state = :pending
72
+ @start_time = nil
73
+ @end_time = nil
74
+ @duration = nil
75
+ @error = nil
76
+ @last_message = nil
77
+ end
78
+ end
79
+
80
+ def initialize(output: $stdout)
81
+ @output = output
82
+ @tasks = {}
83
+ @monitor = Monitor.new
84
+ @nest_level = 0
85
+ @root_task_class = nil
86
+ @output_capture = nil
87
+ @start_time = nil
88
+ end
89
+
90
+ # Set the output capture for getting task output
91
+ # @param capture [ThreadOutputCapture] The output capture instance
92
+ def set_output_capture(capture)
93
+ @monitor.synchronize do
94
+ @output_capture = capture
95
+ end
96
+ end
97
+
98
+ # Set the root task to build tree structure
99
+ # Only sets root task if not already set (prevents nested executor overwrite)
100
+ # @param root_task_class [Class] The root task class
101
+ def set_root_task(root_task_class)
102
+ @monitor.synchronize do
103
+ return if @root_task_class # Don't overwrite existing root task
104
+ @root_task_class = root_task_class
105
+ on_root_task_set
106
+ end
107
+ end
108
+
109
+ # Register which impl was selected for a section
110
+ # @param section_class [Class] The section class
111
+ # @param impl_class [Class] The selected implementation class
112
+ def register_section_impl(section_class, impl_class)
113
+ @monitor.synchronize do
114
+ on_section_impl_registered(section_class, impl_class)
115
+ end
116
+ end
117
+
118
+ # @param task_class [Class] The task class to register
119
+ def register_task(task_class)
120
+ @monitor.synchronize do
121
+ return if @tasks.key?(task_class)
122
+ @tasks[task_class] = TaskProgress.new
123
+ on_task_registered(task_class)
124
+ end
125
+ end
126
+
127
+ # @param task_class [Class] The task class to check
128
+ # @return [Boolean] true if the task is registered
129
+ def task_registered?(task_class)
130
+ @monitor.synchronize do
131
+ @tasks.key?(task_class)
132
+ end
133
+ end
134
+
135
+ # @param task_class [Class] The task class to update
136
+ # @param state [Symbol] The new state
137
+ # @param duration [Float] Duration in milliseconds (for completed tasks)
138
+ # @param error [Exception] Error object (for failed tasks)
139
+ def update_task(task_class, state:, duration: nil, error: nil)
140
+ @monitor.synchronize do
141
+ progress = @tasks[task_class]
142
+ # Register task if not already registered (for late-registered tasks)
143
+ progress ||= @tasks[task_class] = TaskProgress.new
144
+
145
+ apply_state_transition(progress, state, duration, error)
146
+ on_task_updated(task_class, state, duration, error)
147
+ end
148
+ end
149
+
150
+ # @param task_class [Class] The task class
151
+ # @return [Symbol] The task state
152
+ def task_state(task_class)
153
+ @monitor.synchronize do
154
+ @tasks[task_class]&.state
155
+ end
156
+ end
157
+
158
+ # Update group state for a task.
159
+ # @param task_class [Class] The task class containing the group
160
+ # @param group_name [String] The name of the group
161
+ # @param state [Symbol] The new state (:running, :completed, :failed)
162
+ # @param duration [Float, nil] Duration in milliseconds (for completed groups)
163
+ # @param error [Exception, nil] Error object (for failed groups)
164
+ def update_group(task_class, group_name, state:, duration: nil, error: nil)
165
+ @monitor.synchronize do
166
+ progress = @tasks[task_class]
167
+ return unless progress
168
+
169
+ apply_group_state_transition(progress, group_name, state, duration, error)
170
+ on_group_updated(task_class, group_name, state, duration, error)
171
+ end
172
+ end
173
+
174
+ def start
175
+ should_start = false
176
+ @monitor.synchronize do
177
+ @nest_level += 1
178
+ return if @nest_level > 1 # Already running from outer executor
179
+ return unless should_activate?
180
+
181
+ @start_time = Time.now
182
+ should_start = true
183
+ end
184
+
185
+ return unless should_start
186
+
187
+ on_start
188
+ end
189
+
190
+ def stop
191
+ should_stop = false
192
+ @monitor.synchronize do
193
+ @nest_level -= 1 if @nest_level > 0
194
+ return unless @nest_level == 0
195
+
196
+ should_stop = true
197
+ end
198
+
199
+ return unless should_stop
200
+
201
+ on_stop
202
+ end
203
+
204
+ protected
205
+
206
+ # Template methods - override in subclasses
207
+
208
+ # Called when root task is set. Override to build tree structure.
209
+ def on_root_task_set
210
+ # Default: no-op
211
+ end
212
+
213
+ # Called when a section impl is registered.
214
+ def on_section_impl_registered(section_class, impl_class)
215
+ # Default: no-op
216
+ end
217
+
218
+ # Called when a task is registered.
219
+ def on_task_registered(task_class)
220
+ # Default: no-op
221
+ end
222
+
223
+ # Called when a task state is updated.
224
+ def on_task_updated(task_class, state, duration, error)
225
+ # Default: no-op
226
+ end
227
+
228
+ # Called when a group state is updated.
229
+ def on_group_updated(task_class, group_name, state, duration, error)
230
+ # Default: no-op
231
+ end
232
+
233
+ # Called to determine if display should activate.
234
+ # @return [Boolean] true if display should start
235
+ def should_activate?
236
+ true
237
+ end
238
+
239
+ # Called when display starts.
240
+ def on_start
241
+ # Default: no-op
242
+ end
243
+
244
+ # Called when display stops.
245
+ def on_stop
246
+ # Default: no-op
247
+ end
248
+
249
+ # Shared tree traversal for subclasses
250
+
251
+ # Register all tasks from a tree structure recursively
252
+ def register_tasks_from_tree(node)
253
+ return unless node
254
+
255
+ task_class = node[:task_class]
256
+ @tasks[task_class] ||= TaskProgress.new
257
+ @tasks[task_class].is_impl_candidate = true if node[:is_impl_candidate]
258
+
259
+ node[:children].each { |child| register_tasks_from_tree(child) }
260
+ end
261
+
262
+ # Utility methods for subclasses
263
+
264
+ # Get short name of a task class
265
+ def short_name(task_class)
266
+ return "Unknown" unless task_class
267
+ task_class.name&.split("::")&.last || task_class.to_s
268
+ end
269
+
270
+ # Format duration for display
271
+ def format_duration(ms)
272
+ if ms >= 1000
273
+ "#{(ms / 1000.0).round(1)}s"
274
+ else
275
+ "#{ms.round(1)}ms"
276
+ end
277
+ end
278
+
279
+ # Check if output is a TTY
280
+ def tty?
281
+ @output.tty?
282
+ end
283
+
284
+ private
285
+
286
+ # Apply state transition to TaskProgress
287
+ def apply_state_transition(progress, state, duration, error)
288
+ case state
289
+ # Run lifecycle states
290
+ when :pending
291
+ progress.run_state = :pending
292
+ when :running
293
+ progress.run_state = :running
294
+ progress.run_start_time = Time.now
295
+ when :completed
296
+ progress.run_state = :completed
297
+ progress.run_end_time = Time.now
298
+ progress.run_duration = duration if duration
299
+ when :failed
300
+ progress.run_state = :failed
301
+ progress.run_end_time = Time.now
302
+ progress.run_error = error if error
303
+ # Clean lifecycle states
304
+ when :cleaning
305
+ progress.clean_state = :cleaning
306
+ progress.clean_start_time = Time.now
307
+ when :clean_completed
308
+ progress.clean_state = :clean_completed
309
+ progress.clean_end_time = Time.now
310
+ progress.clean_duration = duration if duration
311
+ when :clean_failed
312
+ progress.clean_state = :clean_failed
313
+ progress.clean_end_time = Time.now
314
+ progress.clean_error = error if error
315
+ end
316
+ end
317
+
318
+ # Apply state transition to GroupProgress
319
+ def apply_group_state_transition(progress, group_name, state, duration, error)
320
+ case state
321
+ when :running
322
+ group = GroupProgress.new(group_name)
323
+ group.state = :running
324
+ group.start_time = Time.now
325
+ progress.groups << group
326
+ progress.current_group_index = progress.groups.size - 1
327
+ when :completed
328
+ group = progress.groups.find { |g| g.name == group_name && g.state == :running }
329
+ if group
330
+ group.state = :completed
331
+ group.end_time = Time.now
332
+ group.duration = duration
333
+ end
334
+ progress.current_group_index = nil
335
+ when :failed
336
+ group = progress.groups.find { |g| g.name == group_name && g.state == :running }
337
+ if group
338
+ group.state = :failed
339
+ group.end_time = Time.now
340
+ group.duration = duration
341
+ group.error = error
342
+ end
343
+ progress.current_group_index = nil
344
+ end
345
+ end
346
+ end
347
+ end
348
+ end