taski 0.8.3 → 0.9.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/README.md +108 -50
  4. data/docs/GUIDE.md +79 -55
  5. data/examples/README.md +10 -29
  6. data/examples/clean_demo.rb +25 -65
  7. data/examples/large_tree_demo.rb +356 -0
  8. data/examples/message_demo.rb +0 -1
  9. data/examples/progress_demo.rb +13 -24
  10. data/examples/reexecution_demo.rb +8 -44
  11. data/lib/taski/execution/execution_facade.rb +150 -0
  12. data/lib/taski/execution/executor.rb +167 -359
  13. data/lib/taski/execution/fiber_protocol.rb +27 -0
  14. data/lib/taski/execution/registry.rb +15 -19
  15. data/lib/taski/execution/scheduler.rb +161 -140
  16. data/lib/taski/execution/task_observer.rb +41 -0
  17. data/lib/taski/execution/task_output_router.rb +41 -58
  18. data/lib/taski/execution/task_wrapper.rb +123 -219
  19. data/lib/taski/execution/worker_pool.rb +279 -64
  20. data/lib/taski/logging.rb +105 -0
  21. data/lib/taski/progress/layout/base.rb +600 -0
  22. data/lib/taski/progress/layout/filters.rb +126 -0
  23. data/lib/taski/progress/layout/log.rb +27 -0
  24. data/lib/taski/progress/layout/simple.rb +166 -0
  25. data/lib/taski/progress/layout/tags.rb +76 -0
  26. data/lib/taski/progress/layout/theme_drop.rb +84 -0
  27. data/lib/taski/progress/layout/tree.rb +300 -0
  28. data/lib/taski/progress/theme/base.rb +224 -0
  29. data/lib/taski/progress/theme/compact.rb +58 -0
  30. data/lib/taski/progress/theme/default.rb +25 -0
  31. data/lib/taski/progress/theme/detail.rb +48 -0
  32. data/lib/taski/progress/theme/plain.rb +40 -0
  33. data/lib/taski/static_analysis/analyzer.rb +5 -17
  34. data/lib/taski/static_analysis/dependency_graph.rb +19 -1
  35. data/lib/taski/static_analysis/start_dep_analyzer.rb +400 -0
  36. data/lib/taski/static_analysis/visitor.rb +1 -39
  37. data/lib/taski/task.rb +49 -58
  38. data/lib/taski/task_proxy.rb +59 -0
  39. data/lib/taski/test_helper/errors.rb +1 -1
  40. data/lib/taski/test_helper.rb +22 -36
  41. data/lib/taski/version.rb +1 -1
  42. data/lib/taski.rb +62 -61
  43. data/sig/taski.rbs +194 -203
  44. metadata +34 -8
  45. data/examples/section_demo.rb +0 -195
  46. data/lib/taski/execution/base_progress_display.rb +0 -393
  47. data/lib/taski/execution/execution_context.rb +0 -390
  48. data/lib/taski/execution/plain_progress_display.rb +0 -76
  49. data/lib/taski/execution/simple_progress_display.rb +0 -247
  50. data/lib/taski/execution/tree_progress_display.rb +0 -643
  51. data/lib/taski/section.rb +0 -74
@@ -1,247 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "base_progress_display"
4
-
5
- module Taski
6
- module Execution
7
- # SimpleProgressDisplay provides a minimalist single-line progress display
8
- # that shows task execution status in a compact format:
9
- #
10
- # ⠹ [3/5] DeployTask | Uploading files...
11
- #
12
- # This is an alternative to TreeProgressDisplay for users who prefer
13
- # less verbose output.
14
- class SimpleProgressDisplay < BaseProgressDisplay
15
- SPINNER_FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
16
- RENDER_INTERVAL = 0.1
17
-
18
- ICONS = {
19
- success: "✓",
20
- failure: "✗",
21
- pending: "○"
22
- }.freeze
23
-
24
- COLORS = {
25
- green: "\e[32m",
26
- red: "\e[31m",
27
- yellow: "\e[33m",
28
- dim: "\e[2m",
29
- reset: "\e[0m"
30
- }.freeze
31
-
32
- def initialize(output: $stdout)
33
- super
34
- @spinner_index = 0
35
- @renderer_thread = nil
36
- @running = false
37
- @section_candidates = {} # section_class => [candidate_classes]
38
- @section_candidate_subtrees = {} # section_class => { candidate_class => subtree_node }
39
- end
40
-
41
- protected
42
-
43
- # Template method: Called when root task is set
44
- def on_root_task_set
45
- build_tree_structure
46
- end
47
-
48
- # Template method: Called when a section impl is registered
49
- def on_section_impl_registered(section_class, impl_class)
50
- @tasks[impl_class] ||= TaskProgress.new
51
- @tasks[impl_class].is_impl_candidate = false
52
-
53
- # Mark the section itself as completed (it's represented by its impl)
54
- @tasks[section_class]&.run_state = :completed
55
-
56
- mark_unselected_candidates_completed(section_class, impl_class)
57
- end
58
-
59
- # Template method: Determine if display should activate
60
- def should_activate?
61
- tty?
62
- end
63
-
64
- # Template method: Called when display starts
65
- def on_start
66
- @running = true
67
- @output.print "\e[?25l" # Hide cursor
68
- @renderer_thread = Thread.new do
69
- loop do
70
- break unless @running
71
- render_live
72
- sleep RENDER_INTERVAL
73
- end
74
- end
75
- end
76
-
77
- # Template method: Called when display stops
78
- def on_stop
79
- @running = false
80
- @renderer_thread&.join
81
- @output.print "\e[?25h" # Show cursor
82
- render_final
83
- end
84
-
85
- private
86
-
87
- def build_tree_structure
88
- return unless @root_task_class
89
-
90
- # Use TreeProgressDisplay's static method for tree building
91
- tree = TreeProgressDisplay.build_tree_node(@root_task_class)
92
- register_tasks_from_tree(tree)
93
- collect_section_candidates(tree)
94
- end
95
-
96
- def collect_section_candidates(node)
97
- return unless node
98
-
99
- task_class = node[:task_class]
100
-
101
- # If this is a section, collect its implementation candidates and their subtrees
102
- if node[:is_section]
103
- candidate_nodes = node[:children].select { |c| c[:is_impl_candidate] }
104
- candidates = candidate_nodes.map { |c| c[:task_class] }
105
- @section_candidates[task_class] = candidates unless candidates.empty?
106
-
107
- # Store subtrees for each candidate to mark descendants as completed when not selected
108
- subtrees = {}
109
- candidate_nodes.each { |c| subtrees[c[:task_class]] = c }
110
- @section_candidate_subtrees[task_class] = subtrees unless subtrees.empty?
111
- end
112
-
113
- node[:children].each { |child| collect_section_candidates(child) }
114
- end
115
-
116
- # Mark unselected candidates and their exclusive subtrees as completed (skipped)
117
- def mark_unselected_candidates_completed(section_class, impl_class)
118
- selected_deps = collect_all_dependencies(impl_class)
119
- candidates = @section_candidates[section_class] || []
120
- subtrees = @section_candidate_subtrees[section_class] || {}
121
-
122
- candidates.each do |candidate|
123
- next if candidate == impl_class
124
- mark_subtree_completed(subtrees[candidate], exclude: selected_deps)
125
- end
126
- end
127
-
128
- # Recursively mark all pending tasks in a subtree as completed (skipped)
129
- # Only marks :pending tasks to avoid overwriting :running or :completed states
130
- # @param node [Hash] The subtree node
131
- # @param exclude [Set<Class>] Set of task classes to exclude (dependencies of selected impl)
132
- def mark_subtree_completed(node, exclude: Set.new)
133
- return unless node
134
-
135
- task_class = node[:task_class]
136
- mark_task_as_skipped(task_class) unless exclude.include?(task_class)
137
- node[:children].each { |child| mark_subtree_completed(child, exclude: exclude) }
138
- end
139
-
140
- def mark_task_as_skipped(task_class)
141
- progress = @tasks[task_class]
142
- progress.run_state = :completed if progress&.run_state == :pending
143
- end
144
-
145
- def render_live
146
- @monitor.synchronize do
147
- @spinner_index = (@spinner_index + 1) % SPINNER_FRAMES.size
148
- line = build_status_line
149
- # Clear line and write new content
150
- @output.print "\r\e[K#{line}"
151
- @output.flush
152
- end
153
- end
154
-
155
- def render_final
156
- @monitor.synchronize do
157
- total_duration = @start_time ? ((Time.now - @start_time) * 1000).to_i : 0
158
- completed = @tasks.values.count { |p| p.run_state == :completed }
159
- failed = @tasks.values.count { |p| p.run_state == :failed }
160
- total = @tasks.size
161
-
162
- line = if failed > 0
163
- failed_tasks = @tasks.select { |_, p| p.run_state == :failed }
164
- first_error = failed_tasks.values.first&.run_error
165
- error_msg = first_error ? ": #{first_error.message}" : ""
166
- "#{COLORS[:red]}#{ICONS[:failure]}#{COLORS[:reset]} [#{completed}/#{total}] " \
167
- "#{failed_tasks.keys.first} failed#{error_msg}"
168
- else
169
- "#{COLORS[:green]}#{ICONS[:success]}#{COLORS[:reset]} [#{completed}/#{total}] " \
170
- "All tasks completed (#{total_duration}ms)"
171
- end
172
-
173
- @output.print "\r\e[K#{line}\n"
174
- @output.flush
175
- end
176
- end
177
-
178
- def build_status_line
179
- running_tasks = @tasks.select { |_, p| p.run_state == :running }
180
- cleaning_tasks = @tasks.select { |_, p| p.clean_state == :cleaning }
181
- pending_tasks = @tasks.select { |_, p| p.run_state == :pending }
182
- failed_count = @tasks.values.count { |p| p.run_state == :failed }
183
- done_count = @tasks.values.count { |p| p.run_state == :completed || p.run_state == :failed }
184
-
185
- status_icon = determine_status_icon(failed_count, running_tasks, cleaning_tasks, pending_tasks)
186
- task_names = format_current_task_names(cleaning_tasks, running_tasks, pending_tasks)
187
-
188
- primary_task = running_tasks.keys.first || cleaning_tasks.keys.first
189
- output_suffix = build_output_suffix(primary_task)
190
-
191
- build_status_parts(status_icon, done_count, @tasks.size, task_names, output_suffix)
192
- end
193
-
194
- def determine_status_icon(failed_count, running_tasks, cleaning_tasks, pending_tasks)
195
- # Show success only when no failed, running, cleaning, or pending tasks
196
- # This prevents showing checkmark briefly when mark_subtree_completed marks tasks
197
- if failed_count > 0
198
- "#{COLORS[:red]}#{ICONS[:failure]}#{COLORS[:reset]}"
199
- elsif running_tasks.any? || cleaning_tasks.any? || pending_tasks.any?
200
- spinner = SPINNER_FRAMES[@spinner_index]
201
- "#{COLORS[:yellow]}#{spinner}#{COLORS[:reset]}"
202
- else
203
- "#{COLORS[:green]}#{ICONS[:success]}#{COLORS[:reset]}"
204
- end
205
- end
206
-
207
- def format_current_task_names(cleaning_tasks, running_tasks, pending_tasks)
208
- # Prioritize: cleaning > running > pending
209
- current_tasks = if cleaning_tasks.any?
210
- cleaning_tasks.keys
211
- elsif running_tasks.any?
212
- running_tasks.keys
213
- elsif pending_tasks.any?
214
- pending_tasks.keys.first(3)
215
- else
216
- []
217
- end
218
-
219
- names = current_tasks.first(3).map { |t| short_name(t) }.join(", ")
220
- names += "..." if current_tasks.size > 3
221
- names
222
- end
223
-
224
- def build_status_parts(status_icon, done_count, total, task_names, output_suffix)
225
- parts = ["#{status_icon} [#{done_count}/#{total}]"]
226
- parts << task_names unless task_names.empty?
227
- parts << "|" << output_suffix if output_suffix
228
- parts.join(" ")
229
- end
230
-
231
- def build_output_suffix(task_class)
232
- return nil unless @output_capture && task_class
233
-
234
- last_line = @output_capture.last_line_for(task_class)
235
- return nil unless last_line && !last_line.strip.empty?
236
-
237
- # Truncate if too long
238
- max_length = 40
239
- if last_line.length > max_length
240
- last_line[0, max_length - 3] + "..."
241
- else
242
- last_line
243
- end
244
- end
245
- end
246
- end
247
- end