taski 0.5.0 → 0.7.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 +50 -0
- data/README.md +168 -21
- data/docs/GUIDE.md +394 -0
- data/examples/README.md +65 -17
- data/examples/{context_demo.rb → args_demo.rb} +27 -27
- data/examples/clean_demo.rb +204 -0
- data/examples/data_pipeline_demo.rb +1 -1
- data/examples/group_demo.rb +113 -0
- data/examples/large_tree_demo.rb +519 -0
- data/examples/reexecution_demo.rb +93 -80
- data/examples/simple_progress_demo.rb +80 -0
- data/examples/system_call_demo.rb +56 -0
- data/lib/taski/{context.rb → args.rb} +3 -3
- data/lib/taski/execution/base_progress_display.rb +348 -0
- data/lib/taski/execution/execution_context.rb +383 -0
- data/lib/taski/execution/executor.rb +405 -134
- data/lib/taski/execution/plain_progress_display.rb +76 -0
- data/lib/taski/execution/registry.rb +17 -1
- data/lib/taski/execution/scheduler.rb +308 -0
- data/lib/taski/execution/simple_progress_display.rb +173 -0
- data/lib/taski/execution/task_output_pipe.rb +42 -0
- data/lib/taski/execution/task_output_router.rb +287 -0
- data/lib/taski/execution/task_wrapper.rb +215 -52
- data/lib/taski/execution/tree_progress_display.rb +349 -212
- data/lib/taski/execution/worker_pool.rb +104 -0
- data/lib/taski/section.rb +16 -3
- data/lib/taski/static_analysis/visitor.rb +3 -0
- data/lib/taski/task.rb +218 -37
- data/lib/taski/test_helper/errors.rb +13 -0
- data/lib/taski/test_helper/minitest.rb +38 -0
- data/lib/taski/test_helper/mock_registry.rb +51 -0
- data/lib/taski/test_helper/mock_wrapper.rb +46 -0
- data/lib/taski/test_helper/rspec.rb +38 -0
- data/lib/taski/test_helper.rb +214 -0
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +211 -23
- data/sig/taski.rbs +207 -27
- metadata +25 -8
- data/docs/advanced-features.md +0 -625
- data/docs/api-guide.md +0 -509
- data/docs/error-handling.md +0 -684
- data/examples/section_progress_demo.rb +0 -78
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper/errors"
|
|
4
|
+
require_relative "test_helper/mock_wrapper"
|
|
5
|
+
require_relative "test_helper/mock_registry"
|
|
6
|
+
|
|
7
|
+
module Taski
|
|
8
|
+
# Test helper module for mocking Taski task dependencies in unit tests.
|
|
9
|
+
# Include this module in your test class to access mocking functionality.
|
|
10
|
+
#
|
|
11
|
+
# @example Minitest usage
|
|
12
|
+
# class BuildReportTest < Minitest::Test
|
|
13
|
+
# include Taski::TestHelper
|
|
14
|
+
#
|
|
15
|
+
# def test_builds_report
|
|
16
|
+
# mock_task(FetchData, users: [1, 2, 3])
|
|
17
|
+
# assert_equal 3, BuildReport.user_count
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
module TestHelper
|
|
21
|
+
# Module prepended to Task's singleton class to intercept define_class_accessor.
|
|
22
|
+
# @api private
|
|
23
|
+
module TaskExtension
|
|
24
|
+
def define_class_accessor(method)
|
|
25
|
+
singleton_class.undef_method(method) if singleton_class.method_defined?(method)
|
|
26
|
+
|
|
27
|
+
define_singleton_method(method) do
|
|
28
|
+
# Check for mock first
|
|
29
|
+
mock = MockRegistry.mock_for(self)
|
|
30
|
+
return mock.get_exported_value(method) if mock
|
|
31
|
+
|
|
32
|
+
# No mock - call original implementation via registry lookup
|
|
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.with_args(options: {}, root_task: self) do
|
|
47
|
+
validate_no_circular_dependencies!
|
|
48
|
+
fresh_wrapper.get_exported_value(method)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Module prepended to Scheduler to skip dependencies of mocked tasks.
|
|
56
|
+
# @api private
|
|
57
|
+
module SchedulerExtension
|
|
58
|
+
def build_dependency_graph(root_task_class)
|
|
59
|
+
queue = [root_task_class]
|
|
60
|
+
|
|
61
|
+
while (task_class = queue.shift)
|
|
62
|
+
next if @task_states.key?(task_class)
|
|
63
|
+
|
|
64
|
+
# Mocked tasks have no dependencies (isolates indirect dependencies)
|
|
65
|
+
mock = MockRegistry.mock_for(task_class)
|
|
66
|
+
deps = mock ? Set.new : task_class.cached_dependencies
|
|
67
|
+
@dependencies[task_class] = deps.dup
|
|
68
|
+
@task_states[task_class] = Taski::Execution::Scheduler::STATE_PENDING
|
|
69
|
+
|
|
70
|
+
deps.each { |dep| queue << dep }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Module prepended to Executor to skip execution of mocked tasks.
|
|
76
|
+
# @api private
|
|
77
|
+
module ExecutorExtension
|
|
78
|
+
def execute_task(task_class, wrapper)
|
|
79
|
+
return if @registry.abort_requested?
|
|
80
|
+
|
|
81
|
+
# Skip execution if task is mocked
|
|
82
|
+
if MockRegistry.mock_for(task_class)
|
|
83
|
+
wrapper.mark_completed(nil)
|
|
84
|
+
@completion_queue.push({task_class: task_class, wrapper: wrapper})
|
|
85
|
+
return
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
super
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class << self
|
|
93
|
+
# Checks if any mocks are currently registered.
|
|
94
|
+
# @return [Boolean] true if mocks exist
|
|
95
|
+
def mocks_active?
|
|
96
|
+
MockRegistry.mocks_active?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Retrieves the mock wrapper for a task class.
|
|
100
|
+
# @param task_class [Class] The task class to look up
|
|
101
|
+
# @return [MockWrapper, nil] The mock wrapper or nil if not mocked
|
|
102
|
+
def mock_for(task_class)
|
|
103
|
+
MockRegistry.mock_for(task_class)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Clears all registered mocks.
|
|
107
|
+
# Called automatically by test framework integrations.
|
|
108
|
+
def reset_mocks!
|
|
109
|
+
MockRegistry.reset!
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Registers a mock for a task class with specified return values.
|
|
114
|
+
# @param task_class [Class] A Taski::Task or Taski::Section subclass
|
|
115
|
+
# @param values [Hash{Symbol => Object}] Method names mapped to return values
|
|
116
|
+
# @return [MockWrapper] The created mock wrapper
|
|
117
|
+
# @raise [InvalidTaskError] If task_class is not a Taski::Task/Section subclass
|
|
118
|
+
# @raise [InvalidMethodError] If any method name is not an exported method
|
|
119
|
+
#
|
|
120
|
+
# @example
|
|
121
|
+
# mock_task(FetchData, result: { users: [1, 2, 3] })
|
|
122
|
+
# mock_task(Config, timeout: 30, retries: 3)
|
|
123
|
+
def mock_task(task_class, **values)
|
|
124
|
+
validate_task_class!(task_class)
|
|
125
|
+
validate_exported_methods!(task_class, values.keys)
|
|
126
|
+
|
|
127
|
+
mock_wrapper = MockWrapper.new(task_class, values)
|
|
128
|
+
MockRegistry.register(task_class, mock_wrapper)
|
|
129
|
+
mock_wrapper
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Asserts that a mocked task's method was accessed during the test.
|
|
133
|
+
# @param task_class [Class] The mocked task class
|
|
134
|
+
# @param method_name [Symbol] The exported method name
|
|
135
|
+
# @return [true] If assertion passes
|
|
136
|
+
# @raise [ArgumentError] If task_class was not mocked
|
|
137
|
+
# @raise [Minitest::Assertion, RSpec::Expectations::ExpectationNotMetError]
|
|
138
|
+
# If method was not accessed
|
|
139
|
+
def assert_task_accessed(task_class, method_name)
|
|
140
|
+
mock = fetch_mock!(task_class)
|
|
141
|
+
|
|
142
|
+
unless mock.accessed?(method_name)
|
|
143
|
+
raise assertion_error("Expected #{task_class}.#{method_name} to be accessed, but it was not")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
true
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Asserts that a mocked task's method was NOT accessed during the test.
|
|
150
|
+
# @param task_class [Class] The mocked task class
|
|
151
|
+
# @param method_name [Symbol] The exported method name
|
|
152
|
+
# @return [true] If assertion passes
|
|
153
|
+
# @raise [ArgumentError] If task_class was not mocked
|
|
154
|
+
# @raise [Minitest::Assertion, RSpec::Expectations::ExpectationNotMetError]
|
|
155
|
+
# If method was accessed
|
|
156
|
+
def refute_task_accessed(task_class, method_name)
|
|
157
|
+
mock = fetch_mock!(task_class)
|
|
158
|
+
|
|
159
|
+
if mock.accessed?(method_name)
|
|
160
|
+
count = mock.access_count(method_name)
|
|
161
|
+
raise assertion_error(
|
|
162
|
+
"Expected #{task_class}.#{method_name} not to be accessed, but it was accessed #{count} time(s)"
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
true
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
def fetch_mock!(task_class)
|
|
172
|
+
mock = MockRegistry.mock_for(task_class)
|
|
173
|
+
return mock if mock
|
|
174
|
+
|
|
175
|
+
raise ArgumentError, "Task #{task_class} was not mocked. Call mock_task first."
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def validate_task_class!(task_class)
|
|
179
|
+
valid = task_class.is_a?(Class) &&
|
|
180
|
+
(task_class < Taski::Task || task_class == Taski::Task)
|
|
181
|
+
return if valid
|
|
182
|
+
|
|
183
|
+
raise InvalidTaskError,
|
|
184
|
+
"Cannot mock #{task_class}: not a Taski::Task or Taski::Section subclass"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def validate_exported_methods!(task_class, method_names)
|
|
188
|
+
exported = task_class.exported_methods
|
|
189
|
+
method_names.each do |method_name|
|
|
190
|
+
unless exported.include?(method_name)
|
|
191
|
+
raise InvalidMethodError,
|
|
192
|
+
"Cannot mock :#{method_name} on #{task_class}: not an exported method. Exported: #{exported.inspect}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def assertion_error(message)
|
|
198
|
+
# Use the appropriate assertion error class based on the test framework
|
|
199
|
+
# Use fully qualified names to avoid namespace conflicts
|
|
200
|
+
if defined?(::Minitest::Assertion)
|
|
201
|
+
::Minitest::Assertion.new(message)
|
|
202
|
+
elsif defined?(::RSpec::Expectations::ExpectationNotMetError)
|
|
203
|
+
::RSpec::Expectations::ExpectationNotMetError.new(message)
|
|
204
|
+
else
|
|
205
|
+
RuntimeError.new(message)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Prepend extensions when test helper is loaded
|
|
212
|
+
Taski::Task.singleton_class.prepend(Taski::TestHelper::TaskExtension)
|
|
213
|
+
Taski::Execution::Scheduler.prepend(Taski::TestHelper::SchedulerExtension)
|
|
214
|
+
Taski::Execution::Executor.prepend(Taski::TestHelper::ExecutorExtension)
|
data/lib/taski/version.rb
CHANGED
data/lib/taski.rb
CHANGED
|
@@ -5,12 +5,15 @@ 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/execution_context"
|
|
8
9
|
require_relative "taski/execution/task_wrapper"
|
|
10
|
+
require_relative "taski/execution/scheduler"
|
|
11
|
+
require_relative "taski/execution/worker_pool"
|
|
9
12
|
require_relative "taski/execution/executor"
|
|
10
13
|
require_relative "taski/execution/tree_progress_display"
|
|
11
|
-
require_relative "taski/
|
|
12
|
-
require_relative "taski/
|
|
13
|
-
require_relative "taski/
|
|
14
|
+
require_relative "taski/execution/simple_progress_display"
|
|
15
|
+
require_relative "taski/execution/plain_progress_display"
|
|
16
|
+
require_relative "taski/args"
|
|
14
17
|
|
|
15
18
|
module Taski
|
|
16
19
|
class TaskAbortException < StandardError
|
|
@@ -28,52 +31,237 @@ module Taski
|
|
|
28
31
|
end
|
|
29
32
|
end
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
# Represents a single task failure with its context
|
|
35
|
+
class TaskFailure
|
|
36
|
+
attr_reader :task_class, :error, :output_lines
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
# @param task_class [Class] The task class that failed
|
|
39
|
+
# @param error [Exception] The exception that was raised
|
|
40
|
+
# @param output_lines [Array<String>] Recent output lines from the failed task
|
|
41
|
+
def initialize(task_class:, error:, output_lines: [])
|
|
42
|
+
@task_class = task_class
|
|
43
|
+
@error = error
|
|
44
|
+
@output_lines = output_lines
|
|
45
|
+
end
|
|
37
46
|
end
|
|
38
47
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
# Mixin for exception classes to enable transparent rescue matching with AggregateError.
|
|
49
|
+
# When extended by an exception class, `rescue ThatError` will also match
|
|
50
|
+
# an AggregateError that contains ThatError.
|
|
51
|
+
#
|
|
52
|
+
# @note TaskError and all TaskClass::Error classes already extend this module.
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# begin
|
|
56
|
+
# MyTask.value # raises AggregateError containing MyTask::Error
|
|
57
|
+
# rescue MyTask::Error => e
|
|
58
|
+
# puts "MyTask failed: #{e.message}"
|
|
59
|
+
# end
|
|
60
|
+
module AggregateAware
|
|
61
|
+
def ===(other)
|
|
62
|
+
return super unless other.is_a?(Taski::AggregateError)
|
|
63
|
+
|
|
64
|
+
other.includes?(self)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Base class for task-specific error wrappers.
|
|
69
|
+
# Each Task subclass automatically gets a ::Error class that inherits from this.
|
|
70
|
+
# This allows rescuing errors by task class: rescue MyTask::Error => e
|
|
71
|
+
#
|
|
72
|
+
# @example Rescuing task-specific errors
|
|
73
|
+
# begin
|
|
74
|
+
# MyTask.value
|
|
75
|
+
# rescue MyTask::Error => e
|
|
76
|
+
# puts "MyTask failed: #{e.message}"
|
|
77
|
+
# end
|
|
78
|
+
class TaskError < StandardError
|
|
79
|
+
extend AggregateAware
|
|
80
|
+
|
|
81
|
+
# @return [Exception] The original error that occurred in the task
|
|
82
|
+
attr_reader :cause
|
|
83
|
+
|
|
84
|
+
# @return [Class] The task class where the error occurred
|
|
85
|
+
attr_reader :task_class
|
|
86
|
+
|
|
87
|
+
# @param cause [Exception] The original error
|
|
88
|
+
# @param task_class [Class] The task class where the error occurred
|
|
89
|
+
def initialize(cause, task_class:)
|
|
90
|
+
@cause = cause
|
|
91
|
+
@task_class = task_class
|
|
92
|
+
super(cause.message)
|
|
93
|
+
set_backtrace(cause.backtrace)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Raised when multiple tasks fail during parallel execution
|
|
98
|
+
class AggregateError < StandardError
|
|
99
|
+
attr_reader :errors
|
|
100
|
+
|
|
101
|
+
# @param errors [Array<TaskFailure>] List of task failures
|
|
102
|
+
def initialize(errors)
|
|
103
|
+
@errors = errors
|
|
104
|
+
super(build_message)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Returns the first error for compatibility with exception chaining
|
|
108
|
+
# @return [Exception, nil] The first error or nil if no errors
|
|
109
|
+
def cause
|
|
110
|
+
errors.first&.error
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Check if this aggregate contains an error of the given type
|
|
114
|
+
# @param exception_class [Class] The exception class to check for
|
|
115
|
+
# @return [Boolean] true if any contained error is of the given type
|
|
116
|
+
def includes?(exception_class)
|
|
117
|
+
errors.any? { |f| f.error.is_a?(exception_class) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def build_message
|
|
123
|
+
task_word = (errors.size == 1) ? "task" : "tasks"
|
|
124
|
+
parts = ["#{errors.size} #{task_word} failed:"]
|
|
125
|
+
|
|
126
|
+
errors.each do |f|
|
|
127
|
+
parts << " - #{f.task_class.name}: #{f.error.message}"
|
|
128
|
+
|
|
129
|
+
# Include captured output if available
|
|
130
|
+
if f.output_lines && !f.output_lines.empty?
|
|
131
|
+
parts << " Output:"
|
|
132
|
+
f.output_lines.each { |line| parts << " #{line}" }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
parts.join("\n")
|
|
45
137
|
end
|
|
46
138
|
end
|
|
47
139
|
|
|
48
|
-
|
|
140
|
+
@args_monitor = Monitor.new
|
|
141
|
+
|
|
142
|
+
# Get the current runtime arguments
|
|
143
|
+
# @return [Args, nil] The current args or nil if no task is running
|
|
144
|
+
def self.args
|
|
145
|
+
@args_monitor.synchronize { @args }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Start new runtime arguments (internal use only)
|
|
49
149
|
# @api private
|
|
50
|
-
|
|
51
|
-
|
|
150
|
+
# @return [Boolean] true if this call created the args, false if args already existed
|
|
151
|
+
def self.start_args(options:, root_task:)
|
|
152
|
+
@args_monitor.synchronize do
|
|
153
|
+
return false if @args
|
|
154
|
+
@args = Args.new(options: options, root_task: root_task)
|
|
155
|
+
true
|
|
156
|
+
end
|
|
52
157
|
end
|
|
53
158
|
|
|
54
|
-
|
|
55
|
-
|
|
159
|
+
# Reset the runtime arguments (internal use only)
|
|
160
|
+
# @api private
|
|
161
|
+
def self.reset_args!
|
|
162
|
+
@args_monitor.synchronize { @args = nil }
|
|
56
163
|
end
|
|
57
164
|
|
|
58
|
-
|
|
59
|
-
|
|
165
|
+
# Execute a block with args lifecycle management.
|
|
166
|
+
# Creates args if they don't exist, and resets them only if this call created them.
|
|
167
|
+
# This prevents race conditions in concurrent execution.
|
|
168
|
+
#
|
|
169
|
+
# @param options [Hash] User-defined options
|
|
170
|
+
# @param root_task [Class] The root task class
|
|
171
|
+
# @yield The block to execute with args available
|
|
172
|
+
# @return [Object] The result of the block
|
|
173
|
+
def self.with_args(options:, root_task:)
|
|
174
|
+
created_args = start_args(options: options, root_task: root_task)
|
|
175
|
+
yield
|
|
176
|
+
ensure
|
|
177
|
+
reset_args! if created_args
|
|
60
178
|
end
|
|
61
179
|
|
|
62
180
|
# Progress display is enabled by default (tree-style).
|
|
63
181
|
# Environment variables:
|
|
64
182
|
# - TASKI_PROGRESS_DISABLE=1: Disable progress display entirely
|
|
65
|
-
# -
|
|
183
|
+
# - TASKI_PROGRESS_MODE=simple|tree: Set display mode (default: tree)
|
|
66
184
|
def self.progress_display
|
|
67
185
|
return nil if progress_disabled?
|
|
68
|
-
@progress_display ||=
|
|
186
|
+
@progress_display ||= create_progress_display
|
|
69
187
|
end
|
|
70
188
|
|
|
71
189
|
def self.progress_disabled?
|
|
72
190
|
ENV["TASKI_PROGRESS_DISABLE"] == "1"
|
|
73
191
|
end
|
|
74
192
|
|
|
193
|
+
# Get the current progress mode (:tree or :simple)
|
|
194
|
+
# @return [Symbol] The current progress mode
|
|
195
|
+
def self.progress_mode
|
|
196
|
+
@progress_mode || progress_mode_from_env
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Set the progress mode (:tree or :simple)
|
|
200
|
+
# @param mode [Symbol] The mode to use (:tree or :simple)
|
|
201
|
+
def self.progress_mode=(mode)
|
|
202
|
+
@progress_mode = mode.to_sym
|
|
203
|
+
# Reset display so it will be recreated with new mode
|
|
204
|
+
@progress_display&.stop
|
|
205
|
+
@progress_display = nil
|
|
206
|
+
end
|
|
207
|
+
|
|
75
208
|
def self.reset_progress_display!
|
|
76
209
|
@progress_display&.stop
|
|
77
210
|
@progress_display = nil
|
|
211
|
+
@progress_mode = nil
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# @api private
|
|
215
|
+
def self.create_progress_display
|
|
216
|
+
case progress_mode
|
|
217
|
+
when :simple
|
|
218
|
+
Execution::SimpleProgressDisplay.new
|
|
219
|
+
when :plain
|
|
220
|
+
Execution::PlainProgressDisplay.new
|
|
221
|
+
else
|
|
222
|
+
Execution::TreeProgressDisplay.new
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# @api private
|
|
227
|
+
def self.progress_mode_from_env
|
|
228
|
+
case ENV["TASKI_PROGRESS_MODE"]
|
|
229
|
+
when "simple"
|
|
230
|
+
:simple
|
|
231
|
+
when "plain"
|
|
232
|
+
:plain
|
|
233
|
+
else
|
|
234
|
+
:tree
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Get the worker count from the current args (set via Task.run(workers: n))
|
|
239
|
+
# @return [Integer, nil] The worker count or nil to use WorkerPool default
|
|
240
|
+
# @api private
|
|
241
|
+
def self.args_worker_count
|
|
242
|
+
args&.fetch(:_workers, nil)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Get the current registry for this thread (used during dependency resolution)
|
|
246
|
+
# @return [Execution::Registry, nil] The current registry or nil
|
|
247
|
+
# @api private
|
|
248
|
+
def self.current_registry
|
|
249
|
+
Thread.current[:taski_current_registry]
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Set the current registry for this thread (internal use only)
|
|
253
|
+
# @api private
|
|
254
|
+
def self.set_current_registry(registry)
|
|
255
|
+
Thread.current[:taski_current_registry] = registry
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Clear the current registry for this thread (internal use only)
|
|
259
|
+
# @api private
|
|
260
|
+
def self.clear_current_registry
|
|
261
|
+
Thread.current[:taski_current_registry] = nil
|
|
78
262
|
end
|
|
79
263
|
end
|
|
264
|
+
|
|
265
|
+
# Load Task and Section after Taski module is defined (they depend on TaskError)
|
|
266
|
+
require_relative "taski/task"
|
|
267
|
+
require_relative "taski/section"
|