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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -0
- data/README.md +108 -50
- data/docs/GUIDE.md +79 -55
- data/examples/README.md +10 -29
- data/examples/clean_demo.rb +25 -65
- data/examples/large_tree_demo.rb +356 -0
- data/examples/message_demo.rb +0 -1
- data/examples/progress_demo.rb +13 -24
- data/examples/reexecution_demo.rb +8 -44
- data/lib/taski/execution/execution_facade.rb +150 -0
- data/lib/taski/execution/executor.rb +167 -359
- data/lib/taski/execution/fiber_protocol.rb +27 -0
- data/lib/taski/execution/registry.rb +15 -19
- data/lib/taski/execution/scheduler.rb +161 -140
- data/lib/taski/execution/task_observer.rb +41 -0
- data/lib/taski/execution/task_output_router.rb +41 -58
- data/lib/taski/execution/task_wrapper.rb +123 -219
- data/lib/taski/execution/worker_pool.rb +279 -64
- data/lib/taski/logging.rb +105 -0
- data/lib/taski/progress/layout/base.rb +600 -0
- data/lib/taski/progress/layout/filters.rb +126 -0
- data/lib/taski/progress/layout/log.rb +27 -0
- data/lib/taski/progress/layout/simple.rb +166 -0
- data/lib/taski/progress/layout/tags.rb +76 -0
- data/lib/taski/progress/layout/theme_drop.rb +84 -0
- data/lib/taski/progress/layout/tree.rb +300 -0
- data/lib/taski/progress/theme/base.rb +224 -0
- data/lib/taski/progress/theme/compact.rb +58 -0
- data/lib/taski/progress/theme/default.rb +25 -0
- data/lib/taski/progress/theme/detail.rb +48 -0
- data/lib/taski/progress/theme/plain.rb +40 -0
- data/lib/taski/static_analysis/analyzer.rb +5 -17
- data/lib/taski/static_analysis/dependency_graph.rb +19 -1
- data/lib/taski/static_analysis/start_dep_analyzer.rb +400 -0
- data/lib/taski/static_analysis/visitor.rb +1 -39
- data/lib/taski/task.rb +49 -58
- data/lib/taski/task_proxy.rb +59 -0
- data/lib/taski/test_helper/errors.rb +1 -1
- data/lib/taski/test_helper.rb +22 -36
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +62 -61
- data/sig/taski.rbs +194 -203
- metadata +34 -8
- data/examples/section_demo.rb +0 -195
- data/lib/taski/execution/base_progress_display.rb +0 -393
- data/lib/taski/execution/execution_context.rb +0 -390
- data/lib/taski/execution/plain_progress_display.rb +0 -76
- data/lib/taski/execution/simple_progress_display.rb +0 -247
- data/lib/taski/execution/tree_progress_display.rb +0 -643
- 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
|