taski 0.8.2 → 0.8.3
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 +8 -0
- data/lib/taski/execution/base_progress_display.rb +29 -0
- data/lib/taski/execution/simple_progress_display.rb +74 -33
- data/lib/taski/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cddae184fb1f55c69b42995e51c3bf5711d31f978ec383a00a73d2ce3531ca15
|
|
4
|
+
data.tar.gz: 3e79ca2bf3a6cd163272bbff98ff3b980f3a3b69293607dd45879501d50904c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9310e8b52be98d9087de88b4b7f5d105896edd85137c2f43e6c968c8daaf00dc6b4843ff9d1f228a089dcd7ceb379715c2b640d3c547ce173659dc7130cd1768
|
|
7
|
+
data.tar.gz: 65bade2024691c9c7527f6249294d01714563ff35c211472e6d984e82ba5acd0767251d33e05d8ade4208eabc8b1326eb47fc4f932b4699bf0b986e208fffcea
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.3] - 2026-01-26
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Improve progress display accuracy for section candidates ([#136](https://github.com/ahogappa/taski/pull/136))
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Extract helper methods for improved readability ([#136](https://github.com/ahogappa/taski/pull/136))
|
|
17
|
+
|
|
10
18
|
## [0.8.2] - 2026-01-26
|
|
11
19
|
|
|
12
20
|
### Fixed
|
|
@@ -290,8 +290,30 @@ module Taski
|
|
|
290
290
|
@output.tty?
|
|
291
291
|
end
|
|
292
292
|
|
|
293
|
+
# Collect all dependencies of a task class recursively
|
|
294
|
+
# Useful for determining which tasks are needed by a selected implementation
|
|
295
|
+
# @param task_class [Class] The task class to collect dependencies for
|
|
296
|
+
# @return [Set<Class>] Set of all dependency task classes (including the task itself)
|
|
297
|
+
def collect_all_dependencies(task_class)
|
|
298
|
+
deps = Set.new
|
|
299
|
+
collect_dependencies_recursive(task_class, deps)
|
|
300
|
+
deps
|
|
301
|
+
end
|
|
302
|
+
|
|
293
303
|
private
|
|
294
304
|
|
|
305
|
+
# Recursively collect dependencies into the given set
|
|
306
|
+
# @param task_class [Class] The task class
|
|
307
|
+
# @param collected [Set<Class>] Accumulated dependencies
|
|
308
|
+
def collect_dependencies_recursive(task_class, collected)
|
|
309
|
+
return if collected.include?(task_class)
|
|
310
|
+
collected.add(task_class)
|
|
311
|
+
|
|
312
|
+
task_class.cached_dependencies.each do |dep|
|
|
313
|
+
collect_dependencies_recursive(dep, collected)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
295
317
|
# Flush all queued messages to output.
|
|
296
318
|
# Called when progress display stops.
|
|
297
319
|
def flush_queued_messages
|
|
@@ -300,12 +322,15 @@ module Taski
|
|
|
300
322
|
end
|
|
301
323
|
|
|
302
324
|
# Apply state transition to TaskProgress
|
|
325
|
+
# Note: Once a task reaches :completed or :failed, it cannot go back to :running.
|
|
326
|
+
# This prevents progress count from decreasing when nested executors re-execute tasks.
|
|
303
327
|
def apply_state_transition(progress, state, duration, error)
|
|
304
328
|
case state
|
|
305
329
|
# Run lifecycle states
|
|
306
330
|
when :pending
|
|
307
331
|
progress.run_state = :pending
|
|
308
332
|
when :running
|
|
333
|
+
return if run_state_finalized?(progress)
|
|
309
334
|
progress.run_state = :running
|
|
310
335
|
progress.run_start_time = Time.now
|
|
311
336
|
when :completed
|
|
@@ -331,6 +356,10 @@ module Taski
|
|
|
331
356
|
end
|
|
332
357
|
end
|
|
333
358
|
|
|
359
|
+
def run_state_finalized?(progress)
|
|
360
|
+
progress.run_state == :completed || progress.run_state == :failed
|
|
361
|
+
end
|
|
362
|
+
|
|
334
363
|
# Apply state transition to GroupProgress
|
|
335
364
|
def apply_group_state_transition(progress, group_name, state, duration, error)
|
|
336
365
|
case state
|
|
@@ -35,6 +35,7 @@ module Taski
|
|
|
35
35
|
@renderer_thread = nil
|
|
36
36
|
@running = false
|
|
37
37
|
@section_candidates = {} # section_class => [candidate_classes]
|
|
38
|
+
@section_candidate_subtrees = {} # section_class => { candidate_class => subtree_node }
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
protected
|
|
@@ -50,18 +51,9 @@ module Taski
|
|
|
50
51
|
@tasks[impl_class].is_impl_candidate = false
|
|
51
52
|
|
|
52
53
|
# Mark the section itself as completed (it's represented by its impl)
|
|
53
|
-
|
|
54
|
-
@tasks[section_class].run_state = :completed
|
|
55
|
-
end
|
|
54
|
+
@tasks[section_class]&.run_state = :completed
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
candidates = @section_candidates[section_class] || []
|
|
59
|
-
candidates.each do |candidate|
|
|
60
|
-
next if candidate == impl_class
|
|
61
|
-
progress = @tasks[candidate]
|
|
62
|
-
next unless progress
|
|
63
|
-
progress.run_state = :completed
|
|
64
|
-
end
|
|
56
|
+
mark_unselected_candidates_completed(section_class, impl_class)
|
|
65
57
|
end
|
|
66
58
|
|
|
67
59
|
# Template method: Determine if display should activate
|
|
@@ -106,17 +98,50 @@ module Taski
|
|
|
106
98
|
|
|
107
99
|
task_class = node[:task_class]
|
|
108
100
|
|
|
109
|
-
# If this is a section, collect its implementation candidates
|
|
101
|
+
# If this is a section, collect its implementation candidates and their subtrees
|
|
110
102
|
if node[:is_section]
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.map { |c| c[:task_class] }
|
|
103
|
+
candidate_nodes = node[:children].select { |c| c[:is_impl_candidate] }
|
|
104
|
+
candidates = candidate_nodes.map { |c| c[:task_class] }
|
|
114
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?
|
|
115
111
|
end
|
|
116
112
|
|
|
117
113
|
node[:children].each { |child| collect_section_candidates(child) }
|
|
118
114
|
end
|
|
119
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
|
+
|
|
120
145
|
def render_live
|
|
121
146
|
@monitor.synchronize do
|
|
122
147
|
@spinner_index = (@spinner_index + 1) % SPINNER_FRAMES.size
|
|
@@ -153,37 +178,53 @@ module Taski
|
|
|
153
178
|
def build_status_line
|
|
154
179
|
running_tasks = @tasks.select { |_, p| p.run_state == :running }
|
|
155
180
|
cleaning_tasks = @tasks.select { |_, p| p.clean_state == :cleaning }
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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)
|
|
160
187
|
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
163
198
|
"#{COLORS[:red]}#{ICONS[:failure]}#{COLORS[:reset]}"
|
|
164
|
-
elsif running_tasks.any? || cleaning_tasks.any?
|
|
199
|
+
elsif running_tasks.any? || cleaning_tasks.any? || pending_tasks.any?
|
|
200
|
+
spinner = SPINNER_FRAMES[@spinner_index]
|
|
165
201
|
"#{COLORS[:yellow]}#{spinner}#{COLORS[:reset]}"
|
|
166
202
|
else
|
|
167
203
|
"#{COLORS[:green]}#{ICONS[:success]}#{COLORS[:reset]}"
|
|
168
204
|
end
|
|
205
|
+
end
|
|
169
206
|
|
|
170
|
-
|
|
207
|
+
def format_current_task_names(cleaning_tasks, running_tasks, pending_tasks)
|
|
208
|
+
# Prioritize: cleaning > running > pending
|
|
171
209
|
current_tasks = if cleaning_tasks.any?
|
|
172
|
-
cleaning_tasks.keys
|
|
210
|
+
cleaning_tasks.keys
|
|
211
|
+
elsif running_tasks.any?
|
|
212
|
+
running_tasks.keys
|
|
213
|
+
elsif pending_tasks.any?
|
|
214
|
+
pending_tasks.keys.first(3)
|
|
173
215
|
else
|
|
174
|
-
|
|
216
|
+
[]
|
|
175
217
|
end
|
|
176
218
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
output_suffix = build_output_suffix(running_tasks.keys.first || cleaning_tasks.keys.first)
|
|
219
|
+
names = current_tasks.first(3).map { |t| short_name(t) }.join(", ")
|
|
220
|
+
names += "..." if current_tasks.size > 3
|
|
221
|
+
names
|
|
222
|
+
end
|
|
182
223
|
|
|
183
|
-
|
|
184
|
-
parts
|
|
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?
|
|
185
227
|
parts << "|" << output_suffix if output_suffix
|
|
186
|
-
|
|
187
228
|
parts.join(" ")
|
|
188
229
|
end
|
|
189
230
|
|
data/lib/taski/version.rb
CHANGED