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
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Taski
4
+ module Progress
5
+ module Layout
6
+ # Liquid filter module for colorizing text output.
7
+ # Uses ThemeDrop from context to get color codes, falls back to defaults.
8
+ #
9
+ # @example Usage in Liquid template
10
+ # {{ task.name | green }}
11
+ # {{ task.error_message | red }}
12
+ # {{ task.state | dim }}
13
+ module ColorFilter
14
+ DEFAULT_COLORS = {
15
+ red: "\e[31m",
16
+ green: "\e[32m",
17
+ yellow: "\e[33m",
18
+ dim: "\e[2m",
19
+ reset: "\e[0m"
20
+ }.freeze
21
+
22
+ def red(input) = colorize(input, :red)
23
+ def green(input) = colorize(input, :green)
24
+ def yellow(input) = colorize(input, :yellow)
25
+ def dim(input) = colorize(input, :dim)
26
+
27
+ # Format a count value using Theme's format_count method.
28
+ # Falls back to to_s if no template is provided.
29
+ #
30
+ # @example
31
+ # {{ execution.done_count | format_count }}
32
+ def format_count(input)
33
+ template = @context["template"]
34
+ template&.format_count(input) || input.to_s
35
+ end
36
+
37
+ # Format a duration value using Theme's format_duration method.
38
+ # Falls back to default formatting if no template is provided.
39
+ #
40
+ # @example
41
+ # {{ task.duration | format_duration }}
42
+ # {{ execution.total_duration | format_duration }}
43
+ def format_duration(input)
44
+ return "" if input.nil?
45
+
46
+ template = @context["template"]
47
+ template&.format_duration(input) || default_format_duration(input)
48
+ end
49
+
50
+ # Truncate a list to a maximum number of items, joining with separator.
51
+ # Uses Theme's truncate_list_separator and truncate_list_suffix if available.
52
+ #
53
+ # @example
54
+ # {{ execution.task_names | truncate_list: 3 }}
55
+ # # => "TaskA, TaskB, TaskC..."
56
+ def truncate_list(input, limit = 3)
57
+ return "" if input.nil?
58
+
59
+ items = input.is_a?(Array) ? input : [input]
60
+ return "" if items.empty?
61
+
62
+ template = @context["template"]
63
+ separator = template&.truncate_list_separator || ", "
64
+ suffix = template&.truncate_list_suffix || "..."
65
+
66
+ truncated = items.first(limit)
67
+ result = truncated.join(separator)
68
+ result += suffix if items.size > limit
69
+ result
70
+ end
71
+
72
+ # Extract short name from a fully qualified class name.
73
+ # Returns the last component after "::".
74
+ #
75
+ # @example
76
+ # {{ task.name | short_name }}
77
+ # # "MyModule::MyTask" => "MyTask"
78
+ def short_name(input)
79
+ return "" if input.nil?
80
+ input.to_s.split("::").last || input.to_s
81
+ end
82
+
83
+ # Truncate text to a maximum length, adding suffix if truncated.
84
+ # Uses Theme's truncate_text_suffix if available.
85
+ #
86
+ # @example
87
+ # {{ task.stdout | truncate_text: 40 }}
88
+ # # => "Uploading files to server..."
89
+ def truncate_text(input, max_length = 40)
90
+ return "" if input.nil?
91
+ return "" if max_length <= 0
92
+
93
+ text = input.to_s
94
+ return text if text.length <= max_length
95
+
96
+ template = @context["template"]
97
+ suffix = template&.truncate_text_suffix || "..."
98
+
99
+ truncated_length = [max_length - suffix.length, 0].max
100
+ if truncated_length == 0
101
+ suffix[0, max_length]
102
+ else
103
+ text[0, truncated_length] + suffix
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def colorize(input, color_name)
110
+ template = @context["template"]
111
+ color = template&.public_send(:"color_#{color_name}") || DEFAULT_COLORS[color_name]
112
+ reset = template&.color_reset || DEFAULT_COLORS[:reset]
113
+ "#{color}#{input}#{reset}"
114
+ end
115
+
116
+ def default_format_duration(ms)
117
+ if ms >= 1000
118
+ "#{(ms / 1000.0).round(1)}s"
119
+ else
120
+ "#{ms}ms"
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../theme/plain"
5
+
6
+ module Taski
7
+ module Progress
8
+ module Layout
9
+ # Log layout for non-TTY environments (CI, log files, piped output).
10
+ # Outputs plain text without terminal escape codes.
11
+ #
12
+ # Output format:
13
+ # [START] TaskName
14
+ # [DONE] TaskName (123.4ms)
15
+ # [FAIL] TaskName: Error message
16
+ #
17
+ # Uses Theme::Plain by default to ensure no ANSI escape codes in output.
18
+ class Log < Base
19
+ def initialize(output: $stderr, theme: nil)
20
+ theme ||= Theme::Plain.new
21
+ super
22
+ @output.sync = true if @output.respond_to?(:sync=)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../theme/compact"
5
+
6
+ module Taski
7
+ module Progress
8
+ module Layout
9
+ # Simple layout providing a minimalist single-line progress display.
10
+ # Shows task execution status in a compact format with spinner animation:
11
+ #
12
+ # ⠹ [3/5] DeployTask | Uploading files...
13
+ #
14
+ # Customization is done through Theme classes:
15
+ #
16
+ # class MyTheme < Taski::Progress::Theme::Base
17
+ # def spinner_frames
18
+ # %w[🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘]
19
+ # end
20
+ #
21
+ # def icon_success
22
+ # "🎉"
23
+ # end
24
+ #
25
+ # def format_count(count)
26
+ # "#{count}件"
27
+ # end
28
+ #
29
+ # def execution_complete
30
+ # '{% icon %} Done! {{ execution.completed_count | format_count }} tasks in {{ execution.total_duration | format_duration }}'
31
+ # end
32
+ # end
33
+ #
34
+ # layout = Taski::Progress::Layout::Simple.new(theme: MyTheme.new)
35
+ class Simple < Base
36
+ def initialize(output: $stdout, theme: nil)
37
+ theme ||= Theme::Compact.new
38
+ super
39
+ @renderer_thread = nil
40
+ @running = false
41
+ @running_mutex = Mutex.new
42
+ end
43
+
44
+ protected
45
+
46
+ # === Template method overrides ===
47
+
48
+ def handle_ready
49
+ graph = context&.dependency_graph
50
+ return unless graph
51
+
52
+ graph.all_tasks.each { |tc| register_task(tc) }
53
+ end
54
+
55
+ # Simple layout uses periodic status line updates instead of per-event output
56
+ def handle_task_update(_task_class, _current_state, _phase)
57
+ # No per-event output; status line is updated by render_live
58
+ end
59
+
60
+ def handle_group_started(_task_class, _group_name, _phase)
61
+ # No per-event output; status line is updated by render_live
62
+ end
63
+
64
+ def handle_group_completed(_task_class, _group_name, _phase, _duration)
65
+ # No per-event output; status line is updated by render_live
66
+ end
67
+
68
+ def should_activate?
69
+ tty?
70
+ end
71
+
72
+ def handle_start
73
+ @running_mutex.synchronize { @running = true }
74
+ start_spinner_timer
75
+ @output.print "\e[?25l" # Hide cursor
76
+ @renderer_thread = Thread.new do
77
+ loop do
78
+ break unless @running_mutex.synchronize { @running }
79
+ render_live
80
+ sleep @theme.render_interval
81
+ end
82
+ end
83
+ end
84
+
85
+ def handle_stop
86
+ @running_mutex.synchronize { @running = false }
87
+ @renderer_thread&.join
88
+ stop_spinner_timer
89
+ @output.print "\e[?25h" # Show cursor
90
+ render_final
91
+ end
92
+
93
+ private
94
+
95
+ def render_live
96
+ @monitor.synchronize do
97
+ line = build_status_line
98
+ # Truncate line to terminal width to prevent line wrap
99
+ max_width = terminal_width - 1 # Leave space for cursor
100
+ line = line[0, max_width] if line.length > max_width
101
+ # Clear line and write new content
102
+ @output.print "\r\e[K#{line}"
103
+ @output.flush
104
+ end
105
+ end
106
+
107
+ def terminal_width
108
+ @output.winsize[1]
109
+ rescue
110
+ 80 # Default fallback
111
+ end
112
+
113
+ def render_final
114
+ @monitor.synchronize do
115
+ line = if failed_count > 0
116
+ render_execution_failed(failed_count: failed_count, total_count: total_count, total_duration: total_duration)
117
+ else
118
+ render_execution_completed(completed_count: completed_count, total_count: total_count, total_duration: total_duration)
119
+ end
120
+
121
+ @output.print "\r\e[K#{line}\n"
122
+ @output.flush
123
+ end
124
+ end
125
+
126
+ def build_status_line
127
+ task_names = collect_current_task_names
128
+
129
+ primary_task = running_tasks.keys.first || cleaning_tasks.keys.first
130
+ task_stdout = build_task_stdout(primary_task)
131
+
132
+ render_execution_running(
133
+ done_count: done_count,
134
+ total_count: total_count,
135
+ task_names: task_names.empty? ? nil : task_names,
136
+ task_stdout: task_stdout
137
+ )
138
+ end
139
+
140
+ def collect_current_task_names
141
+ # Prioritize: cleaning > running > pending
142
+ current_tasks = if cleaning_tasks.any?
143
+ cleaning_tasks.keys
144
+ elsif running_tasks.any?
145
+ running_tasks.keys
146
+ elsif pending_tasks.any?
147
+ pending_tasks.keys
148
+ else
149
+ []
150
+ end
151
+
152
+ current_tasks.map { |t| task_class_name(t) }
153
+ end
154
+
155
+ def build_task_stdout(task_class)
156
+ return nil unless @output_capture && task_class
157
+
158
+ last_line = @output_capture.last_line_for(task_class)
159
+ return nil unless last_line && !last_line.strip.empty?
160
+
161
+ last_line.strip
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "liquid"
4
+ require_relative "filters"
5
+
6
+ module Taski
7
+ module Progress
8
+ module Layout
9
+ # Liquid tag for rendering animated spinner characters.
10
+ # Uses ThemeDrop from context to get spinner frames, falls back to defaults.
11
+ # Uses spinner_index from context to determine current frame.
12
+ #
13
+ # @example Usage in Liquid template
14
+ # {% spinner %} Loading...
15
+ # {% spinner %} [{{ done }}/{{ total }}]
16
+ class SpinnerTag < Liquid::Tag
17
+ DEFAULT_FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
18
+
19
+ def render(context)
20
+ template = context["template"]
21
+ frames = template&.spinner_frames || DEFAULT_FRAMES
22
+ index = context["spinner_index"] || 0
23
+ frames[index % frames.size]
24
+ end
25
+ end
26
+
27
+ # Liquid tag for rendering status icons based on current state.
28
+ # Uses ThemeDrop from context to get icons and colors.
29
+ # Uses state from context to determine which icon to show.
30
+ #
31
+ # @example Usage in Liquid template
32
+ # {% icon %} Task completed
33
+ # {% icon %} [{{ done }}/{{ total }}]
34
+ class IconTag < Liquid::Tag
35
+ DEFAULTS = {
36
+ icon_success: "✓",
37
+ icon_failure: "✗",
38
+ icon_pending: "○",
39
+ icon_skip: "⊘",
40
+ color_green: ColorFilter::DEFAULT_COLORS[:green],
41
+ color_red: ColorFilter::DEFAULT_COLORS[:red],
42
+ color_yellow: ColorFilter::DEFAULT_COLORS[:yellow],
43
+ color_dim: ColorFilter::DEFAULT_COLORS[:dim],
44
+ color_reset: ColorFilter::DEFAULT_COLORS[:reset]
45
+ }.freeze
46
+
47
+ STATE_CONFIG = {
48
+ "completed" => {icon: :icon_success, color: :color_green},
49
+ "failed" => {icon: :icon_failure, color: :color_red},
50
+ "running" => {icon: :icon_pending, color: :color_yellow},
51
+ "skipped" => {icon: :icon_skip, color: :color_dim}
52
+ }.freeze
53
+
54
+ def render(context)
55
+ @template = context["template"]
56
+ state = context["state"]&.to_s
57
+
58
+ config = STATE_CONFIG[state]
59
+ return get_value(:icon_pending) unless config
60
+
61
+ colorize(get_value(config[:icon]), config[:color])
62
+ end
63
+
64
+ private
65
+
66
+ def get_value(key)
67
+ @template&.public_send(key) || DEFAULTS[key]
68
+ end
69
+
70
+ def colorize(icon, color_key)
71
+ "#{get_value(color_key)}#{icon}#{get_value(:color_reset)}"
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "liquid"
4
+
5
+ module Taski
6
+ module Progress
7
+ module Layout
8
+ # Liquid Drop for Theme to enable method access from filters/tags.
9
+ # Wraps a Theme instance and delegates color/icon/spinner methods.
10
+ #
11
+ # @example Using in Liquid context
12
+ # drop = ThemeDrop.new(theme)
13
+ # Liquid::Template.parse("{{ theme.color_red }}")
14
+ # .render("theme" => drop)
15
+ class ThemeDrop < Liquid::Drop
16
+ def initialize(theme)
17
+ @theme = theme
18
+ end
19
+
20
+ # Color methods
21
+ def color_red = @theme.color_red
22
+ def color_green = @theme.color_green
23
+ def color_yellow = @theme.color_yellow
24
+ def color_dim = @theme.color_dim
25
+ def color_reset = @theme.color_reset
26
+
27
+ # Spinner settings
28
+ def spinner_frames = @theme.spinner_frames
29
+ def spinner_interval = @theme.spinner_interval
30
+ def render_interval = @theme.render_interval
31
+
32
+ # Status icons
33
+ def icon_success = @theme.icon_success
34
+ def icon_failure = @theme.icon_failure
35
+ def icon_pending = @theme.icon_pending
36
+ def icon_skip = @theme.icon_skip
37
+
38
+ # Formatting methods (used by filters)
39
+ def format_count(count) = @theme.format_count(count)
40
+ def format_duration(ms) = @theme.format_duration(ms)
41
+
42
+ # List truncation settings
43
+ def truncate_list_separator = @theme.truncate_list_separator
44
+ def truncate_list_suffix = @theme.truncate_list_suffix
45
+
46
+ # Text truncation settings
47
+ def truncate_text_suffix = @theme.truncate_text_suffix
48
+ end
49
+
50
+ # Base class for Liquid Drops with dynamic property access.
51
+ # Provides common functionality for TaskDrop and ExecutionDrop.
52
+ class DataDrop < Liquid::Drop
53
+ def initialize(**data)
54
+ @data = data
55
+ end
56
+
57
+ def liquid_method_missing(method)
58
+ @data[method.to_sym]
59
+ end
60
+ end
61
+
62
+ # Liquid Drop for task-specific variables.
63
+ # Provides access to individual task information in templates.
64
+ #
65
+ # Available properties: name, state, duration, error_message, group_name, stdout
66
+ #
67
+ # @example Using in Liquid template
68
+ # {{ task.name }} ({{ task.state }})
69
+ # {{ task.duration | format_duration }}
70
+ class TaskDrop < DataDrop; end
71
+
72
+ # Liquid Drop for execution-level variables.
73
+ # Provides access to overall execution state in templates.
74
+ #
75
+ # Available properties: state, pending_count, done_count, completed_count,
76
+ # failed_count, total_count, total_duration, root_task_name, task_names
77
+ #
78
+ # @example Using in Liquid template
79
+ # [{{ execution.completed_count }}/{{ execution.total_count }}]
80
+ # {{ execution.total_duration | format_duration }}
81
+ class ExecutionDrop < DataDrop; end
82
+ end
83
+ end
84
+ end