taski 0.8.2 → 0.9.0
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 +47 -0
- data/README.md +65 -50
- data/docs/GUIDE.md +41 -56
- 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 +156 -357
- 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 +238 -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/visitor.rb +1 -39
- data/lib/taski/task.rb +44 -58
- data/lib/taski/test_helper/errors.rb +1 -1
- data/lib/taski/test_helper.rb +21 -35
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +60 -61
- data/sig/taski.rbs +194 -203
- metadata +31 -8
- data/examples/section_demo.rb +0 -195
- data/lib/taski/execution/base_progress_display.rb +0 -364
- 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 -206
- data/lib/taski/execution/tree_progress_display.rb +0 -643
- data/lib/taski/section.rb +0 -74
|
@@ -7,41 +7,29 @@ module Taski
|
|
|
7
7
|
module StaticAnalysis
|
|
8
8
|
class Analyzer
|
|
9
9
|
# Analyzes a task class and returns its static dependencies.
|
|
10
|
-
#
|
|
11
|
-
# For Section: impl candidates detected from impl method and called methods (constants returned)
|
|
10
|
+
# Dependencies are detected from the run method and called methods (SomeTask.method calls).
|
|
12
11
|
#
|
|
13
12
|
# Static dependencies are used for:
|
|
14
13
|
# - Tree display visualization
|
|
15
14
|
# - Circular dependency detection
|
|
16
|
-
# - Task execution
|
|
15
|
+
# - Task execution ordering
|
|
17
16
|
#
|
|
18
17
|
# @param task_class [Class] The task class to analyze
|
|
19
18
|
# @return [Set<Class>] Set of task classes that are static dependencies
|
|
20
19
|
def self.analyze(task_class)
|
|
21
|
-
|
|
22
|
-
source_location = extract_method_location(task_class, target_method)
|
|
20
|
+
source_location = extract_method_location(task_class, :run)
|
|
23
21
|
return Set.new unless source_location
|
|
24
22
|
|
|
25
23
|
file_path, _line_number = source_location
|
|
26
24
|
parse_result = Prism.parse_file(file_path)
|
|
27
25
|
|
|
28
|
-
visitor = Visitor.new(task_class,
|
|
26
|
+
visitor = Visitor.new(task_class, :run)
|
|
29
27
|
visitor.visit(parse_result.value)
|
|
30
28
|
# Follow method calls to analyze dependencies in called methods
|
|
31
29
|
visitor.follow_method_calls
|
|
32
30
|
visitor.dependencies
|
|
33
31
|
end
|
|
34
32
|
|
|
35
|
-
# @param task_class [Class] The task class
|
|
36
|
-
# @return [Symbol] The method name to analyze (:run for Task, :impl for Section)
|
|
37
|
-
def self.target_method_for(task_class)
|
|
38
|
-
if defined?(Taski::Section) && task_class < Taski::Section
|
|
39
|
-
:impl
|
|
40
|
-
else
|
|
41
|
-
:run
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
33
|
# @param task_class [Class] The task class
|
|
46
34
|
# @param method_name [Symbol] The method name to extract location for
|
|
47
35
|
# @return [Array<String, Integer>, nil] File path and line number, or nil
|
|
@@ -51,7 +39,7 @@ module Taski
|
|
|
51
39
|
nil
|
|
52
40
|
end
|
|
53
41
|
|
|
54
|
-
private_class_method :
|
|
42
|
+
private_class_method :extract_method_location
|
|
55
43
|
end
|
|
56
44
|
end
|
|
57
45
|
end
|
|
@@ -13,7 +13,7 @@ module Taski
|
|
|
13
13
|
@graph = {}
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
# Build dependency graph starting from root task class
|
|
16
|
+
# Build dependency graph starting from root task class using static analysis
|
|
17
17
|
# @param root_task_class [Class] The root task class to analyze
|
|
18
18
|
# @return [DependencyGraph] self for method chaining
|
|
19
19
|
def build_from(root_task_class)
|
|
@@ -21,6 +21,14 @@ module Taski
|
|
|
21
21
|
self
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# Build dependency graph using cached_dependencies (runtime) instead of AST analysis
|
|
25
|
+
# @param root_task_class [Class] The root task class
|
|
26
|
+
# @return [DependencyGraph] self for method chaining
|
|
27
|
+
def build_from_cached(root_task_class)
|
|
28
|
+
collect_cached_dependencies(root_task_class)
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
24
32
|
# Get topologically sorted task classes (dependencies first)
|
|
25
33
|
# @return [Array<Class>] Sorted task classes
|
|
26
34
|
# @raise [TSort::Cyclic] If circular dependency is detected
|
|
@@ -85,6 +93,16 @@ module Taski
|
|
|
85
93
|
collect_dependencies(dep_class)
|
|
86
94
|
end
|
|
87
95
|
end
|
|
96
|
+
|
|
97
|
+
# Recursively collect dependencies using cached_dependencies
|
|
98
|
+
def collect_cached_dependencies(task_class)
|
|
99
|
+
return if @graph.key?(task_class)
|
|
100
|
+
|
|
101
|
+
deps = task_class.cached_dependencies
|
|
102
|
+
@graph[task_class] = deps.to_set
|
|
103
|
+
|
|
104
|
+
deps.each { |dep| collect_cached_dependencies(dep) }
|
|
105
|
+
end
|
|
88
106
|
end
|
|
89
107
|
end
|
|
90
108
|
end
|
|
@@ -25,8 +25,6 @@ module Taski
|
|
|
25
25
|
@method_calls_to_follow = Set.new
|
|
26
26
|
# Store method definitions found in the class for later analysis
|
|
27
27
|
@class_method_defs = {}
|
|
28
|
-
# Track if we're in an impl call chain (for Section constant detection)
|
|
29
|
-
@in_impl_chain = false
|
|
30
28
|
end
|
|
31
29
|
|
|
32
30
|
def visit_class_node(node)
|
|
@@ -48,8 +46,6 @@ module Taski
|
|
|
48
46
|
@analyzed_methods.add(node.name)
|
|
49
47
|
@in_target_method = true
|
|
50
48
|
@current_analyzing_method = node.name
|
|
51
|
-
# Start impl chain when entering impl method
|
|
52
|
-
@in_impl_chain = true if node.name == :impl && @target_method == :impl
|
|
53
49
|
super
|
|
54
50
|
@in_target_method = false
|
|
55
51
|
@current_analyzing_method = nil
|
|
@@ -66,21 +62,7 @@ module Taski
|
|
|
66
62
|
super
|
|
67
63
|
end
|
|
68
64
|
|
|
69
|
-
def visit_constant_read_node(node)
|
|
70
|
-
# For Section.impl, detect constants as impl candidates (static dependencies)
|
|
71
|
-
detect_impl_candidate(node) if in_impl_method?
|
|
72
|
-
super
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def visit_constant_path_node(node)
|
|
76
|
-
# For Section.impl, detect constants as impl candidates (static dependencies)
|
|
77
|
-
detect_impl_candidate(node) if in_impl_method?
|
|
78
|
-
super
|
|
79
|
-
end
|
|
80
|
-
|
|
81
65
|
# After visiting, follow any method calls that need analysis
|
|
82
|
-
# @in_impl_chain is preserved because methods called from impl should
|
|
83
|
-
# also detect constants as impl candidates
|
|
84
66
|
def follow_method_calls
|
|
85
67
|
new_methods = @method_calls_to_follow - @analyzed_methods
|
|
86
68
|
return if new_methods.empty?
|
|
@@ -90,8 +72,6 @@ module Taski
|
|
|
90
72
|
@method_calls_to_follow.clear
|
|
91
73
|
|
|
92
74
|
# Re-analyze the class methods
|
|
93
|
-
# Preserve impl chain context: methods called from impl should continue
|
|
94
|
-
# detecting constants as impl candidates
|
|
95
75
|
# Set namespace path from target class name for constant resolution
|
|
96
76
|
@current_namespace_path = @target_task_class.name.split("::")
|
|
97
77
|
|
|
@@ -140,10 +120,6 @@ module Taski
|
|
|
140
120
|
@methods_to_analyze.include?(method_name) && !@analyzed_methods.include?(method_name)
|
|
141
121
|
end
|
|
142
122
|
|
|
143
|
-
def in_impl_method?
|
|
144
|
-
@in_target_method && @in_impl_chain
|
|
145
|
-
end
|
|
146
|
-
|
|
147
123
|
# Detect method calls that should be followed (calls to methods in the same class)
|
|
148
124
|
def detect_method_call_to_follow(node)
|
|
149
125
|
# Only follow calls without explicit receiver (self.method or just method)
|
|
@@ -158,11 +134,6 @@ module Taski
|
|
|
158
134
|
receiver.is_a?(Prism::SelfNode)
|
|
159
135
|
end
|
|
160
136
|
|
|
161
|
-
def detect_impl_candidate(node)
|
|
162
|
-
constant_name = node.slice
|
|
163
|
-
resolve_and_add_dependency(constant_name)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
137
|
def detect_task_dependency(node)
|
|
167
138
|
return unless node.receiver
|
|
168
139
|
|
|
@@ -206,16 +177,7 @@ module Taski
|
|
|
206
177
|
end
|
|
207
178
|
|
|
208
179
|
def valid_dependency?(klass)
|
|
209
|
-
klass.is_a?(Class) &&
|
|
210
|
-
(is_parallel_task?(klass) || is_parallel_section?(klass))
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def is_parallel_task?(klass)
|
|
214
|
-
defined?(Taski::Task) && klass < Taski::Task
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def is_parallel_section?(klass)
|
|
218
|
-
defined?(Taski::Section) && klass < Taski::Section
|
|
180
|
+
klass.is_a?(Class) && defined?(Taski::Task) && klass < Taski::Task
|
|
219
181
|
end
|
|
220
182
|
end
|
|
221
183
|
end
|
data/lib/taski/task.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "stringio"
|
|
3
4
|
require_relative "static_analysis/analyzer"
|
|
4
5
|
require_relative "execution/registry"
|
|
5
6
|
require_relative "execution/task_wrapper"
|
|
7
|
+
require_relative "progress/layout/tree"
|
|
8
|
+
require_relative "progress/theme/plain"
|
|
6
9
|
|
|
7
10
|
module Taski
|
|
8
11
|
# Base class for all tasks in the Taski framework.
|
|
@@ -49,26 +52,7 @@ module Taski
|
|
|
49
52
|
@exported_methods ||= []
|
|
50
53
|
end
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
# Creates a task instance for manual execution control.
|
|
54
|
-
# Use class methods (e.g., MyTask.run) for simple execution.
|
|
55
|
-
# @param args [Hash] User-defined arguments accessible via Taski.args.
|
|
56
|
-
# @param workers [Integer, nil] Number of worker threads for parallel execution.
|
|
57
|
-
# @return [Execution::TaskWrapper] A new wrapper for this task.
|
|
58
|
-
def new(args: {}, workers: nil)
|
|
59
|
-
validate_workers!(workers)
|
|
60
|
-
fresh_registry = Execution::Registry.new
|
|
61
|
-
task_instance = allocate
|
|
62
|
-
task_instance.__send__(:initialize)
|
|
63
|
-
wrapper = Execution::TaskWrapper.new(
|
|
64
|
-
task_instance,
|
|
65
|
-
registry: fresh_registry,
|
|
66
|
-
execution_context: Execution::ExecutionContext.current,
|
|
67
|
-
args: args.merge(_workers: workers)
|
|
68
|
-
)
|
|
69
|
-
fresh_registry.register(self, wrapper)
|
|
70
|
-
wrapper
|
|
71
|
-
end
|
|
55
|
+
private :new
|
|
72
56
|
|
|
73
57
|
##
|
|
74
58
|
# Returns cached static dependencies for this task class.
|
|
@@ -98,30 +82,21 @@ module Taski
|
|
|
98
82
|
with_execution_setup(args: args, workers: workers) { |wrapper| wrapper.run }
|
|
99
83
|
end
|
|
100
84
|
|
|
101
|
-
##
|
|
102
|
-
# Executes the clean phase for the task and all its dependencies.
|
|
103
|
-
# Clean is executed in reverse dependency order.
|
|
104
|
-
# Creates a fresh registry each time for independent execution.
|
|
105
|
-
# @param args [Hash] User-defined arguments accessible via Taski.args.
|
|
106
|
-
# @param workers [Integer, nil] Number of worker threads for parallel execution.
|
|
107
|
-
# Must be a positive integer or nil.
|
|
108
|
-
# @raise [ArgumentError] If workers is not a positive integer or nil.
|
|
109
|
-
def clean(args: {}, workers: nil)
|
|
110
|
-
with_execution_setup(args: args, workers: workers) { |wrapper| wrapper.clean }
|
|
111
|
-
end
|
|
112
|
-
|
|
113
85
|
##
|
|
114
86
|
# Execute run followed by clean in a single operation.
|
|
115
|
-
#
|
|
116
|
-
#
|
|
87
|
+
# By default, clean is skipped when run fails.
|
|
88
|
+
# Use clean_on_failure: true to always execute clean for resource release.
|
|
89
|
+
# An optional block is executed between run and clean phases.
|
|
117
90
|
#
|
|
118
91
|
# @param args [Hash] User-defined arguments accessible via Taski.args.
|
|
119
92
|
# @param workers [Integer, nil] Number of worker threads for parallel execution.
|
|
120
93
|
# Must be a positive integer or nil.
|
|
94
|
+
# @param clean_on_failure [Boolean] When true, clean runs even if run raises (default: false).
|
|
121
95
|
# @raise [ArgumentError] If workers is not a positive integer or nil.
|
|
122
96
|
# @return [Object] The result of task execution
|
|
123
|
-
|
|
124
|
-
|
|
97
|
+
# @yield Optional block executed between run and clean phases
|
|
98
|
+
def run_and_clean(args: {}, workers: nil, clean_on_failure: false, &block)
|
|
99
|
+
with_execution_setup(args: args, workers: workers) { |wrapper| wrapper.run_and_clean(clean_on_failure: clean_on_failure, &block) }
|
|
125
100
|
end
|
|
126
101
|
|
|
127
102
|
##
|
|
@@ -137,7 +112,13 @@ module Taski
|
|
|
137
112
|
# Renders a static tree representation of the task dependencies.
|
|
138
113
|
# @return [String] The rendered tree string.
|
|
139
114
|
def tree
|
|
140
|
-
|
|
115
|
+
output = StringIO.new
|
|
116
|
+
theme = Progress::Theme::Plain.new
|
|
117
|
+
layout = Progress::Layout::Tree.new(output: output, theme: theme)
|
|
118
|
+
context = Execution::ExecutionFacade.new(root_task_class: self)
|
|
119
|
+
layout.context = context
|
|
120
|
+
layout.on_ready
|
|
121
|
+
layout.render_tree
|
|
141
122
|
end
|
|
142
123
|
|
|
143
124
|
private
|
|
@@ -170,7 +151,7 @@ module Taski
|
|
|
170
151
|
wrapper = Execution::TaskWrapper.new(
|
|
171
152
|
task_instance,
|
|
172
153
|
registry: fresh_registry,
|
|
173
|
-
|
|
154
|
+
execution_facade: Execution::ExecutionFacade.current
|
|
174
155
|
)
|
|
175
156
|
fresh_registry.register(self, wrapper)
|
|
176
157
|
wrapper
|
|
@@ -197,21 +178,28 @@ module Taski
|
|
|
197
178
|
singleton_class.undef_method(method) if singleton_class.method_defined?(method)
|
|
198
179
|
|
|
199
180
|
define_singleton_method(method) do
|
|
200
|
-
# Check if running inside an execution with a registry
|
|
201
181
|
registry = Taski.current_registry
|
|
202
182
|
if registry
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
)
|
|
183
|
+
if Thread.current[:taski_fiber_context]
|
|
184
|
+
# Fiber-based lazy resolution - yield to the worker loop
|
|
185
|
+
result = Fiber.yield([:need_dep, self, method])
|
|
186
|
+
if result.is_a?(Array) && result[0] == :_taski_error
|
|
187
|
+
raise result[1]
|
|
188
|
+
end
|
|
189
|
+
result
|
|
190
|
+
else
|
|
191
|
+
# Synchronous resolution (clean phase, outside Fiber)
|
|
192
|
+
wrapper = registry.get_or_create(self) do
|
|
193
|
+
task_instance = allocate
|
|
194
|
+
task_instance.__send__(:initialize)
|
|
195
|
+
Execution::TaskWrapper.new(
|
|
196
|
+
task_instance,
|
|
197
|
+
registry: registry,
|
|
198
|
+
execution_facade: Execution::ExecutionFacade.current
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
wrapper.get_exported_value(method)
|
|
213
202
|
end
|
|
214
|
-
wrapper.get_exported_value(method)
|
|
215
203
|
else
|
|
216
204
|
# Outside execution - fresh execution (top-level call)
|
|
217
205
|
Taski.send(:with_env, root_task: self) do
|
|
@@ -305,18 +293,16 @@ module Taski
|
|
|
305
293
|
# end
|
|
306
294
|
# end
|
|
307
295
|
def group(name)
|
|
308
|
-
context = Execution::
|
|
309
|
-
|
|
310
|
-
|
|
296
|
+
context = Execution::ExecutionFacade.current
|
|
297
|
+
phase = Thread.current[:taski_current_phase] || :run
|
|
298
|
+
context&.notify_group_started(self.class, name, phase: phase, timestamp: Time.now)
|
|
311
299
|
|
|
312
300
|
begin
|
|
313
301
|
result = yield
|
|
314
|
-
|
|
315
|
-
context&.notify_group_completed(self.class, name, duration: duration_ms)
|
|
302
|
+
context&.notify_group_completed(self.class, name, phase: phase, timestamp: Time.now)
|
|
316
303
|
result
|
|
317
|
-
rescue
|
|
318
|
-
|
|
319
|
-
context&.notify_group_completed(self.class, name, duration: duration_ms, error: e)
|
|
304
|
+
rescue
|
|
305
|
+
context&.notify_group_completed(self.class, name, phase: phase, timestamp: Time.now)
|
|
320
306
|
raise
|
|
321
307
|
end
|
|
322
308
|
end
|
data/lib/taski/test_helper.rb
CHANGED
|
@@ -19,37 +19,18 @@ module Taski
|
|
|
19
19
|
# end
|
|
20
20
|
module TestHelper
|
|
21
21
|
# Module prepended to Task's singleton class to intercept define_class_accessor.
|
|
22
|
+
# Wraps the original accessor with a mock check.
|
|
22
23
|
# @api private
|
|
23
24
|
module TaskExtension
|
|
24
25
|
def define_class_accessor(method)
|
|
25
|
-
|
|
26
|
+
super
|
|
27
|
+
original_method = self.method(method)
|
|
26
28
|
|
|
27
29
|
define_singleton_method(method) do
|
|
28
|
-
# Check for mock first
|
|
29
30
|
mock = MockRegistry.mock_for(self)
|
|
30
31
|
return mock.get_exported_value(method) if mock
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
registry = Taski.current_registry
|
|
34
|
-
if registry
|
|
35
|
-
wrapper = registry.get_or_create(self) do
|
|
36
|
-
task_instance = allocate
|
|
37
|
-
task_instance.__send__(:initialize)
|
|
38
|
-
Execution::TaskWrapper.new(
|
|
39
|
-
task_instance,
|
|
40
|
-
registry: registry,
|
|
41
|
-
execution_context: Execution::ExecutionContext.current
|
|
42
|
-
)
|
|
43
|
-
end
|
|
44
|
-
wrapper.get_exported_value(method)
|
|
45
|
-
else
|
|
46
|
-
Taski.send(:with_env, root_task: self) do
|
|
47
|
-
Taski.send(:with_args, options: {}) do
|
|
48
|
-
validate_no_circular_dependencies!
|
|
49
|
-
fresh_wrapper.get_exported_value(method)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
33
|
+
original_method.call
|
|
53
34
|
end
|
|
54
35
|
end
|
|
55
36
|
end
|
|
@@ -57,7 +38,7 @@ module Taski
|
|
|
57
38
|
# Module prepended to Scheduler to skip dependencies of mocked tasks.
|
|
58
39
|
# @api private
|
|
59
40
|
module SchedulerExtension
|
|
60
|
-
def
|
|
41
|
+
def load_graph(dependency_graph, root_task_class)
|
|
61
42
|
queue = [root_task_class]
|
|
62
43
|
|
|
63
44
|
while (task_class = queue.shift)
|
|
@@ -65,24 +46,29 @@ module Taski
|
|
|
65
46
|
|
|
66
47
|
# Mocked tasks have no dependencies (isolates indirect dependencies)
|
|
67
48
|
mock = MockRegistry.mock_for(task_class)
|
|
68
|
-
deps = mock ? Set.new : task_class
|
|
49
|
+
deps = mock ? Set.new : dependency_graph.dependencies_for(task_class)
|
|
69
50
|
@dependencies[task_class] = deps.dup
|
|
70
51
|
@task_states[task_class] = Taski::Execution::Scheduler::STATE_PENDING
|
|
52
|
+
@run_reverse_deps[task_class] ||= Set.new
|
|
71
53
|
|
|
72
|
-
deps.each
|
|
54
|
+
deps.each do |dep|
|
|
55
|
+
@run_reverse_deps[dep] ||= Set.new
|
|
56
|
+
@run_reverse_deps[dep].add(task_class)
|
|
57
|
+
log_dependency_resolved(task_class, dep)
|
|
58
|
+
queue << dep
|
|
59
|
+
end
|
|
73
60
|
end
|
|
74
61
|
end
|
|
75
62
|
end
|
|
76
63
|
|
|
77
|
-
# Module prepended to
|
|
64
|
+
# Module prepended to WorkerPool to skip execution of mocked tasks.
|
|
78
65
|
# @api private
|
|
79
|
-
module
|
|
80
|
-
def
|
|
66
|
+
module WorkerPoolExtension
|
|
67
|
+
def drive_fiber(task_class, wrapper, queue)
|
|
81
68
|
return if @registry.abort_requested?
|
|
82
69
|
|
|
83
|
-
# Skip execution if task is mocked
|
|
84
70
|
if MockRegistry.mock_for(task_class)
|
|
85
|
-
wrapper.mark_completed(nil)
|
|
71
|
+
wrapper.mark_completed(nil) unless wrapper.completed?
|
|
86
72
|
@completion_queue.push({task_class: task_class, wrapper: wrapper})
|
|
87
73
|
return
|
|
88
74
|
end
|
|
@@ -143,10 +129,10 @@ module Taski
|
|
|
143
129
|
end
|
|
144
130
|
|
|
145
131
|
# Registers a mock for a task class with specified return values.
|
|
146
|
-
# @param task_class [Class] A Taski::Task
|
|
132
|
+
# @param task_class [Class] A Taski::Task subclass
|
|
147
133
|
# @param values [Hash{Symbol => Object}] Method names mapped to return values
|
|
148
134
|
# @return [MockWrapper] The created mock wrapper
|
|
149
|
-
# @raise [InvalidTaskError] If task_class is not a Taski::Task
|
|
135
|
+
# @raise [InvalidTaskError] If task_class is not a Taski::Task subclass
|
|
150
136
|
# @raise [InvalidMethodError] If any method name is not an exported method
|
|
151
137
|
#
|
|
152
138
|
# @example
|
|
@@ -213,7 +199,7 @@ module Taski
|
|
|
213
199
|
return if valid
|
|
214
200
|
|
|
215
201
|
raise InvalidTaskError,
|
|
216
|
-
"Cannot mock #{task_class}: not a Taski::Task
|
|
202
|
+
"Cannot mock #{task_class}: not a Taski::Task subclass"
|
|
217
203
|
end
|
|
218
204
|
|
|
219
205
|
def validate_exported_methods!(task_class, method_names)
|
|
@@ -243,4 +229,4 @@ end
|
|
|
243
229
|
# Prepend extensions when test helper is loaded
|
|
244
230
|
Taski::Task.singleton_class.prepend(Taski::TestHelper::TaskExtension)
|
|
245
231
|
Taski::Execution::Scheduler.prepend(Taski::TestHelper::SchedulerExtension)
|
|
246
|
-
Taski::Execution::
|
|
232
|
+
Taski::Execution::WorkerPool.prepend(Taski::TestHelper::WorkerPoolExtension)
|
data/lib/taski/version.rb
CHANGED
data/lib/taski.rb
CHANGED
|
@@ -5,16 +5,18 @@ require_relative "taski/static_analysis/analyzer"
|
|
|
5
5
|
require_relative "taski/static_analysis/visitor"
|
|
6
6
|
require_relative "taski/static_analysis/dependency_graph"
|
|
7
7
|
require_relative "taski/execution/registry"
|
|
8
|
-
require_relative "taski/execution/
|
|
8
|
+
require_relative "taski/execution/task_observer"
|
|
9
|
+
require_relative "taski/execution/execution_facade"
|
|
9
10
|
require_relative "taski/execution/task_wrapper"
|
|
10
11
|
require_relative "taski/execution/scheduler"
|
|
11
12
|
require_relative "taski/execution/worker_pool"
|
|
12
13
|
require_relative "taski/execution/executor"
|
|
13
|
-
require_relative "taski/
|
|
14
|
-
require_relative "taski/
|
|
15
|
-
require_relative "taski/
|
|
14
|
+
require_relative "taski/progress/layout/log"
|
|
15
|
+
require_relative "taski/progress/layout/simple"
|
|
16
|
+
require_relative "taski/progress/layout/tree"
|
|
16
17
|
require_relative "taski/args"
|
|
17
18
|
require_relative "taski/env"
|
|
19
|
+
require_relative "taski/logging"
|
|
18
20
|
|
|
19
21
|
module Taski
|
|
20
22
|
class TaskAbortException < StandardError
|
|
@@ -47,8 +49,25 @@ module Taski
|
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
# Mixin for exception classes to enable transparent rescue matching with AggregateError.
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
+
#
|
|
53
|
+
# == How it works
|
|
54
|
+
#
|
|
55
|
+
# Ruby's +rescue+ clause uses the +===+ operator to match exceptions.
|
|
56
|
+
# This module overrides +===+ so that when the rescued class is compared
|
|
57
|
+
# against an AggregateError, it checks whether the AggregateError *contains*
|
|
58
|
+
# an error of that type. This means +rescue TaskError+ will match an
|
|
59
|
+
# AggregateError that wraps one or more TaskError instances, even though
|
|
60
|
+
# AggregateError does not inherit from TaskError.
|
|
61
|
+
#
|
|
62
|
+
# == Why this exists
|
|
63
|
+
#
|
|
64
|
+
# Taski's Executor always raises AggregateError (even for a single failure)
|
|
65
|
+
# to provide a uniform error interface. Without AggregateAware, callers would
|
|
66
|
+
# always need to rescue AggregateError and inspect its contents. With this
|
|
67
|
+
# module, callers can rescue the specific error class they care about:
|
|
68
|
+
#
|
|
69
|
+
# rescue MyTask::Error => e # matches AggregateError containing MyTask::Error
|
|
70
|
+
# rescue Taski::TaskError # matches AggregateError containing any TaskError
|
|
52
71
|
#
|
|
53
72
|
# @note TaskError and all TaskClass::Error classes already extend this module.
|
|
54
73
|
#
|
|
@@ -141,6 +160,19 @@ module Taski
|
|
|
141
160
|
@args_monitor = Monitor.new
|
|
142
161
|
@env_monitor = Monitor.new
|
|
143
162
|
@message_monitor = Monitor.new
|
|
163
|
+
@logger_monitor = Monitor.new
|
|
164
|
+
|
|
165
|
+
# Get the current logger for structured logging
|
|
166
|
+
# @return [Logger, nil] The configured logger or nil (disabled by default)
|
|
167
|
+
def self.logger
|
|
168
|
+
@logger_monitor.synchronize { @logger }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Set the logger for structured logging
|
|
172
|
+
# @param logger [Logger, nil] A Ruby Logger instance or nil to disable logging
|
|
173
|
+
def self.logger=(logger)
|
|
174
|
+
@logger_monitor.synchronize { @logger = logger }
|
|
175
|
+
end
|
|
144
176
|
|
|
145
177
|
# Get the current runtime arguments
|
|
146
178
|
# @return [Args, nil] The current args or nil if no task is running
|
|
@@ -233,66 +265,34 @@ module Taski
|
|
|
233
265
|
reset_args! if created_args
|
|
234
266
|
end
|
|
235
267
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
# - TASKI_PROGRESS_MODE=simple|tree: Set display mode (default: tree)
|
|
240
|
-
def self.progress_display
|
|
241
|
-
return nil if progress_disabled?
|
|
242
|
-
@progress_display ||= create_progress_display
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def self.progress_disabled?
|
|
246
|
-
ENV["TASKI_PROGRESS_DISABLE"] == "1"
|
|
247
|
-
end
|
|
268
|
+
NOT_CONFIGURED = Object.new.freeze
|
|
269
|
+
PROGRESS_MONITOR = Monitor.new
|
|
270
|
+
@progress_display = NOT_CONFIGURED
|
|
248
271
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
else
|
|
256
|
-
@progress_mode || :tree
|
|
272
|
+
def self.progress_display
|
|
273
|
+
PROGRESS_MONITOR.synchronize do
|
|
274
|
+
if @progress_display.equal?(NOT_CONFIGURED)
|
|
275
|
+
@progress_display = Progress::Layout::Simple.new
|
|
276
|
+
end
|
|
277
|
+
@progress_display
|
|
257
278
|
end
|
|
258
279
|
end
|
|
259
280
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
@progress_display = nil
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
def self.reset_progress_display!
|
|
270
|
-
@progress_display&.stop
|
|
271
|
-
@progress_display = nil
|
|
272
|
-
@progress_mode = nil
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
# @api private
|
|
276
|
-
def self.create_progress_display
|
|
277
|
-
case progress_mode
|
|
278
|
-
when :simple
|
|
279
|
-
Execution::SimpleProgressDisplay.new
|
|
280
|
-
when :plain
|
|
281
|
-
Execution::PlainProgressDisplay.new
|
|
282
|
-
else
|
|
283
|
-
Execution::TreeProgressDisplay.new
|
|
281
|
+
def self.progress_display=(display)
|
|
282
|
+
PROGRESS_MONITOR.synchronize do
|
|
283
|
+
unless @progress_display.equal?(NOT_CONFIGURED)
|
|
284
|
+
@progress_display.stop if @progress_display.respond_to?(:stop)
|
|
285
|
+
end
|
|
286
|
+
@progress_display = display
|
|
284
287
|
end
|
|
285
288
|
end
|
|
286
289
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
:plain
|
|
294
|
-
else
|
|
295
|
-
:tree
|
|
290
|
+
def self.reset_progress_display!
|
|
291
|
+
PROGRESS_MONITOR.synchronize do
|
|
292
|
+
unless @progress_display.equal?(NOT_CONFIGURED)
|
|
293
|
+
@progress_display.stop if @progress_display.respond_to?(:stop)
|
|
294
|
+
end
|
|
295
|
+
@progress_display = NOT_CONFIGURED
|
|
296
296
|
end
|
|
297
297
|
end
|
|
298
298
|
|
|
@@ -323,6 +323,5 @@ module Taski
|
|
|
323
323
|
end
|
|
324
324
|
end
|
|
325
325
|
|
|
326
|
-
# Load Task
|
|
326
|
+
# Load Task after Taski module is defined (it depends on TaskError)
|
|
327
327
|
require_relative "taski/task"
|
|
328
|
-
require_relative "taski/section"
|