taski 0.9.0 → 0.9.2
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 +25 -0
- data/README.md +43 -0
- data/docs/GUIDE.md +40 -1
- data/lib/taski/execution/executor.rb +32 -23
- data/lib/taski/execution/fiber_protocol.rb +27 -0
- data/lib/taski/execution/task_wrapper.rb +2 -2
- data/lib/taski/execution/worker_pool.rb +95 -54
- data/lib/taski/progress/config.rb +90 -0
- data/lib/taski/progress/layout/base.rb +25 -3
- data/lib/taski/progress/layout/simple.rb +17 -31
- data/lib/taski/progress/layout/theme_drop.rb +1 -1
- data/lib/taski/progress/layout/tree/event.rb +49 -0
- data/lib/taski/progress/layout/tree/live.rb +85 -0
- data/lib/taski/progress/layout/tree/structure.rb +142 -0
- data/lib/taski/progress/layout/tree.rb +25 -283
- data/lib/taski/progress/theme/base.rb +1 -1
- data/lib/taski/progress/theme/compact.rb +1 -1
- data/lib/taski/static_analysis/start_dep_analyzer.rb +400 -0
- data/lib/taski/task.rb +11 -6
- data/lib/taski/task_proxy.rb +59 -0
- data/lib/taski/test_helper.rb +1 -1
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +25 -7
- metadata +22 -1
|
@@ -267,7 +267,7 @@ module Taski
|
|
|
267
267
|
if failed_count > 0
|
|
268
268
|
render_execution_failed(failed_count: failed_count, total_count: total_count, total_duration: total_duration, skipped_count: skipped_count)
|
|
269
269
|
else
|
|
270
|
-
render_execution_completed(
|
|
270
|
+
render_execution_completed(done_count: done_count, total_count: total_count, total_duration: total_duration, skipped_count: skipped_count)
|
|
271
271
|
end
|
|
272
272
|
end
|
|
273
273
|
|
|
@@ -372,8 +372,8 @@ module Taski
|
|
|
372
372
|
end
|
|
373
373
|
|
|
374
374
|
# Render execution complete event
|
|
375
|
-
def render_execution_completed(
|
|
376
|
-
execution = ExecutionDrop.new(state: :completed,
|
|
375
|
+
def render_execution_completed(done_count:, total_count:, total_duration:, skipped_count: 0)
|
|
376
|
+
execution = ExecutionDrop.new(state: :completed, done_count:, total_count:, total_duration:, skipped_count:)
|
|
377
377
|
render_execution_template(:execution_complete, execution:)
|
|
378
378
|
end
|
|
379
379
|
|
|
@@ -515,6 +515,28 @@ module Taski
|
|
|
515
515
|
task_class.name || task_class.to_s
|
|
516
516
|
end
|
|
517
517
|
|
|
518
|
+
# Start a periodic render loop in a background thread.
|
|
519
|
+
# Starts spinner timer and calls the given block at @theme.render_interval.
|
|
520
|
+
# @yield Block to execute each render cycle (called under @monitor.synchronize)
|
|
521
|
+
def render_loop(&block)
|
|
522
|
+
@render_thread_running = true
|
|
523
|
+
start_spinner_timer
|
|
524
|
+
@render_thread = Thread.new do
|
|
525
|
+
while @render_thread_running
|
|
526
|
+
@monitor.synchronize(&block)
|
|
527
|
+
sleep @theme.render_interval
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Stop the periodic render loop and spinner timer.
|
|
533
|
+
def stop_render_loop
|
|
534
|
+
@render_thread_running = false
|
|
535
|
+
@render_thread&.join
|
|
536
|
+
@render_thread = nil
|
|
537
|
+
stop_spinner_timer
|
|
538
|
+
end
|
|
539
|
+
|
|
518
540
|
# Check if output is a TTY
|
|
519
541
|
def tty?
|
|
520
542
|
@output.tty?
|
|
@@ -36,9 +36,6 @@ module Taski
|
|
|
36
36
|
def initialize(output: $stdout, theme: nil)
|
|
37
37
|
theme ||= Theme::Compact.new
|
|
38
38
|
super
|
|
39
|
-
@renderer_thread = nil
|
|
40
|
-
@running = false
|
|
41
|
-
@running_mutex = Mutex.new
|
|
42
39
|
end
|
|
43
40
|
|
|
44
41
|
protected
|
|
@@ -70,38 +67,26 @@ module Taski
|
|
|
70
67
|
end
|
|
71
68
|
|
|
72
69
|
def handle_start
|
|
73
|
-
@running_mutex.synchronize { @running = true }
|
|
74
|
-
start_spinner_timer
|
|
75
70
|
@output.print "\e[?25l" # Hide cursor
|
|
76
|
-
|
|
77
|
-
loop do
|
|
78
|
-
break unless @running_mutex.synchronize { @running }
|
|
79
|
-
render_live
|
|
80
|
-
sleep @theme.render_interval
|
|
81
|
-
end
|
|
82
|
-
end
|
|
71
|
+
render_loop { render_status_line }
|
|
83
72
|
end
|
|
84
73
|
|
|
85
74
|
def handle_stop
|
|
86
|
-
|
|
87
|
-
@renderer_thread&.join
|
|
88
|
-
stop_spinner_timer
|
|
75
|
+
stop_render_loop
|
|
89
76
|
@output.print "\e[?25h" # Show cursor
|
|
90
77
|
render_final
|
|
91
78
|
end
|
|
92
79
|
|
|
93
80
|
private
|
|
94
81
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
@output.flush
|
|
104
|
-
end
|
|
82
|
+
def render_status_line
|
|
83
|
+
line = build_status_line
|
|
84
|
+
# Truncate line to terminal width to prevent line wrap
|
|
85
|
+
max_width = terminal_width - 1 # Leave space for cursor
|
|
86
|
+
line = line[0, max_width] if line.length > max_width
|
|
87
|
+
# Clear line and write new content
|
|
88
|
+
@output.print "\r\e[K#{line}"
|
|
89
|
+
@output.flush
|
|
105
90
|
end
|
|
106
91
|
|
|
107
92
|
def terminal_width
|
|
@@ -113,9 +98,9 @@ module Taski
|
|
|
113
98
|
def render_final
|
|
114
99
|
@monitor.synchronize do
|
|
115
100
|
line = if failed_count > 0
|
|
116
|
-
render_execution_failed(failed_count: failed_count, total_count: total_count, total_duration: total_duration)
|
|
101
|
+
render_execution_failed(failed_count: failed_count, total_count: total_count, total_duration: total_duration, skipped_count: skipped_count)
|
|
117
102
|
else
|
|
118
|
-
render_execution_completed(
|
|
103
|
+
render_execution_completed(done_count: done_count, total_count: total_count, total_duration: total_duration, skipped_count: skipped_count)
|
|
119
104
|
end
|
|
120
105
|
|
|
121
106
|
@output.print "\r\e[K#{line}\n"
|
|
@@ -126,7 +111,7 @@ module Taski
|
|
|
126
111
|
def build_status_line
|
|
127
112
|
task_names = collect_current_task_names
|
|
128
113
|
|
|
129
|
-
primary_task = running_tasks.keys.
|
|
114
|
+
primary_task = running_tasks.keys.last || cleaning_tasks.keys.last
|
|
130
115
|
task_stdout = build_task_stdout(primary_task)
|
|
131
116
|
|
|
132
117
|
render_execution_running(
|
|
@@ -139,12 +124,13 @@ module Taski
|
|
|
139
124
|
|
|
140
125
|
def collect_current_task_names
|
|
141
126
|
# Prioritize: cleaning > running > pending
|
|
127
|
+
# Reverse so most recently started tasks appear first
|
|
142
128
|
current_tasks = if cleaning_tasks.any?
|
|
143
|
-
cleaning_tasks.keys
|
|
129
|
+
cleaning_tasks.keys.reverse
|
|
144
130
|
elsif running_tasks.any?
|
|
145
|
-
running_tasks.keys
|
|
131
|
+
running_tasks.keys.reverse
|
|
146
132
|
elsif pending_tasks.any?
|
|
147
|
-
pending_tasks.keys
|
|
133
|
+
pending_tasks.keys.reverse
|
|
148
134
|
else
|
|
149
135
|
[]
|
|
150
136
|
end
|
|
@@ -76,7 +76,7 @@ module Taski
|
|
|
76
76
|
# failed_count, total_count, total_duration, root_task_name, task_names
|
|
77
77
|
#
|
|
78
78
|
# @example Using in Liquid template
|
|
79
|
-
# [{{ execution.
|
|
79
|
+
# [{{ execution.done_count }}/{{ execution.total_count }}]
|
|
80
80
|
# {{ execution.total_duration | format_duration }}
|
|
81
81
|
class ExecutionDrop < DataDrop; end
|
|
82
82
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base"
|
|
4
|
+
require_relative "../../theme/detail"
|
|
5
|
+
require_relative "structure"
|
|
6
|
+
|
|
7
|
+
module Taski
|
|
8
|
+
module Progress
|
|
9
|
+
module Layout
|
|
10
|
+
module Tree
|
|
11
|
+
# Non-TTY event-driven tree layout.
|
|
12
|
+
# Outputs lines immediately with tree prefixes as events arrive.
|
|
13
|
+
# Used for logs, CI, piped output, and static tree display.
|
|
14
|
+
class Event < Base
|
|
15
|
+
include Structure
|
|
16
|
+
|
|
17
|
+
def initialize(output: $stderr, theme: nil)
|
|
18
|
+
theme ||= Theme::Detail.new
|
|
19
|
+
super
|
|
20
|
+
init_tree_structure
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
protected
|
|
24
|
+
|
|
25
|
+
def handle_ready
|
|
26
|
+
build_ready_tree
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def handle_task_update(task_class, current_state, phase)
|
|
30
|
+
progress = @tasks[task_class]
|
|
31
|
+
duration = compute_duration(progress, phase)
|
|
32
|
+
text = render_for_task_event(task_class, current_state, duration, nil, phase)
|
|
33
|
+
output_with_prefix(task_class, text) if text
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def handle_group_started(task_class, group_name, phase)
|
|
37
|
+
text = render_group_started(task_class, group_name: group_name)
|
|
38
|
+
output_with_prefix(task_class, text) if text
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def handle_group_completed(task_class, group_name, phase, duration)
|
|
42
|
+
text = render_group_succeeded(task_class, group_name: group_name, task_duration: duration)
|
|
43
|
+
output_with_prefix(task_class, text) if text
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base"
|
|
4
|
+
require_relative "../../theme/detail"
|
|
5
|
+
require_relative "structure"
|
|
6
|
+
|
|
7
|
+
module Taski
|
|
8
|
+
module Progress
|
|
9
|
+
module Layout
|
|
10
|
+
module Tree
|
|
11
|
+
# TTY periodic-update tree layout.
|
|
12
|
+
# Refreshes the entire tree display at regular intervals with spinner animation.
|
|
13
|
+
# Used for interactive terminal output.
|
|
14
|
+
class Live < Base
|
|
15
|
+
include Structure
|
|
16
|
+
|
|
17
|
+
def initialize(output: $stderr, theme: nil)
|
|
18
|
+
theme ||= Theme::Detail.new
|
|
19
|
+
super
|
|
20
|
+
init_tree_structure
|
|
21
|
+
@last_line_count = 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
protected
|
|
25
|
+
|
|
26
|
+
def handle_ready
|
|
27
|
+
build_ready_tree
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# TTY mode: skip per-event output, tree is updated by render_loop
|
|
31
|
+
def handle_task_update(_task_class, _current_state, _phase)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def handle_group_started(_task_class, _group_name, _phase)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def handle_group_completed(_task_class, _group_name, _phase, _duration)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def should_activate?
|
|
41
|
+
tty?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def handle_start
|
|
45
|
+
@output.print "\e[?25l" # Hide cursor
|
|
46
|
+
render_loop { render_tree_live }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def handle_stop
|
|
50
|
+
stop_render_loop
|
|
51
|
+
@output.print "\e[?25h" # Show cursor
|
|
52
|
+
render_final
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def render_tree_live
|
|
58
|
+
lines = build_tree_lines
|
|
59
|
+
clear_previous_output
|
|
60
|
+
lines.each { |line| @output.puts line }
|
|
61
|
+
@output.flush
|
|
62
|
+
@last_line_count = lines.size
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def render_final
|
|
66
|
+
@monitor.synchronize do
|
|
67
|
+
lines = build_tree_lines
|
|
68
|
+
clear_previous_output
|
|
69
|
+
|
|
70
|
+
lines.each { |line| @output.puts line }
|
|
71
|
+
@output.puts render_execution_summary
|
|
72
|
+
@output.flush
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def clear_previous_output
|
|
77
|
+
return if @last_line_count == 0
|
|
78
|
+
# Move cursor up and clear lines
|
|
79
|
+
@output.print "\e[#{@last_line_count}A\e[J"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Taski
|
|
4
|
+
module Progress
|
|
5
|
+
module Layout
|
|
6
|
+
module Tree
|
|
7
|
+
# Shared tree structure logic for Tree::Live and Tree::Event.
|
|
8
|
+
# Provides tree building, node registration, prefix generation,
|
|
9
|
+
# and tree rendering methods.
|
|
10
|
+
module Structure
|
|
11
|
+
# Tree connector characters
|
|
12
|
+
BRANCH = "├── "
|
|
13
|
+
LAST_BRANCH = "└── "
|
|
14
|
+
VERTICAL = "│ "
|
|
15
|
+
SPACE = " "
|
|
16
|
+
|
|
17
|
+
# Returns the tree structure as a string.
|
|
18
|
+
# Uses the current theme to render task content for each node.
|
|
19
|
+
def render_tree
|
|
20
|
+
build_tree_lines.join("\n") + "\n"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
protected
|
|
24
|
+
|
|
25
|
+
def init_tree_structure
|
|
26
|
+
@tree_nodes = {}
|
|
27
|
+
@node_depths = {}
|
|
28
|
+
@node_is_last = {}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_ready_tree
|
|
32
|
+
graph = context&.dependency_graph
|
|
33
|
+
root = context&.root_task_class
|
|
34
|
+
return unless graph && root
|
|
35
|
+
|
|
36
|
+
tree = build_tree_from_graph(root, graph)
|
|
37
|
+
register_tree_nodes(tree, depth: 0, is_last: true, ancestors_last: [])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Output text with tree prefix for the given task
|
|
41
|
+
def output_with_prefix(task_class, text)
|
|
42
|
+
prefix = build_tree_prefix(task_class)
|
|
43
|
+
output_line("#{prefix}#{text}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def build_tree_from_graph(task_class, graph, ancestors = Set.new)
|
|
49
|
+
is_circular = ancestors.include?(task_class)
|
|
50
|
+
node = {task_class: task_class, is_circular: is_circular, children: []}
|
|
51
|
+
return node if is_circular
|
|
52
|
+
|
|
53
|
+
new_ancestors = ancestors + [task_class]
|
|
54
|
+
deps = graph.dependencies_for(task_class)
|
|
55
|
+
deps.each do |dep|
|
|
56
|
+
child_node = build_tree_from_graph(dep, graph, new_ancestors)
|
|
57
|
+
node[:children] << child_node
|
|
58
|
+
end
|
|
59
|
+
node
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def register_tree_nodes(node, depth:, is_last:, ancestors_last:)
|
|
63
|
+
return unless node
|
|
64
|
+
|
|
65
|
+
task_class = node[:task_class]
|
|
66
|
+
@tasks[task_class] ||= new_task_progress
|
|
67
|
+
@tree_nodes[task_class] = node
|
|
68
|
+
@node_depths[task_class] = depth
|
|
69
|
+
@node_is_last[task_class] = {is_last: is_last, ancestors_last: ancestors_last.dup}
|
|
70
|
+
|
|
71
|
+
children = node[:children]
|
|
72
|
+
children.each_with_index do |child, index|
|
|
73
|
+
child_is_last = (index == children.size - 1)
|
|
74
|
+
new_ancestors_last = ancestors_last + [is_last]
|
|
75
|
+
register_tree_nodes(child, depth: depth + 1, is_last: child_is_last, ancestors_last: new_ancestors_last)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_tree_lines
|
|
80
|
+
return [] unless @root_task_class
|
|
81
|
+
|
|
82
|
+
lines = []
|
|
83
|
+
root_node = @tree_nodes[@root_task_class]
|
|
84
|
+
build_node_lines(root_node, lines)
|
|
85
|
+
lines
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def build_node_lines(node, lines)
|
|
89
|
+
return unless node
|
|
90
|
+
|
|
91
|
+
task_class = node[:task_class]
|
|
92
|
+
prefix = build_tree_prefix(task_class)
|
|
93
|
+
content = build_task_content(task_class)
|
|
94
|
+
lines << "#{prefix}#{content}"
|
|
95
|
+
|
|
96
|
+
node[:children].each do |child|
|
|
97
|
+
build_node_lines(child, lines)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def build_task_content(task_class)
|
|
102
|
+
progress = @tasks[task_class]
|
|
103
|
+
|
|
104
|
+
case progress&.dig(:run_state)
|
|
105
|
+
when :running
|
|
106
|
+
render_task_started(task_class)
|
|
107
|
+
when :completed
|
|
108
|
+
render_task_succeeded(task_class, task_duration: compute_duration(progress, :run))
|
|
109
|
+
when :failed
|
|
110
|
+
render_task_failed(task_class, error: nil)
|
|
111
|
+
when :skipped
|
|
112
|
+
render_task_skipped(task_class)
|
|
113
|
+
else
|
|
114
|
+
task = TaskDrop.new(name: task_class_name(task_class), state: :pending)
|
|
115
|
+
render_task_template(:task_pending, task:, execution: execution_drop)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def build_tree_prefix(task_class)
|
|
120
|
+
depth = @node_depths[task_class]
|
|
121
|
+
return "" if depth.nil? || depth == 0
|
|
122
|
+
|
|
123
|
+
last_info = @node_is_last[task_class]
|
|
124
|
+
return "" unless last_info
|
|
125
|
+
|
|
126
|
+
ancestors_last = last_info[:ancestors_last]
|
|
127
|
+
is_last = last_info[:is_last]
|
|
128
|
+
|
|
129
|
+
prefix = ""
|
|
130
|
+
# Skip the first ancestor (root) since root has no visual prefix
|
|
131
|
+
ancestors_last[1..].each do |ancestor_is_last|
|
|
132
|
+
prefix += ancestor_is_last ? SPACE : VERTICAL
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
prefix += is_last ? LAST_BRANCH : BRANCH
|
|
136
|
+
prefix
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|