taski 0.7.0 → 0.8.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 +38 -0
- data/README.md +139 -9
- data/docs/GUIDE.md +54 -0
- data/examples/README.md +3 -3
- data/examples/args_demo.rb +21 -20
- data/examples/data_pipeline_demo.rb +1 -1
- data/examples/large_tree_demo.rb +519 -0
- data/examples/simple_progress_demo.rb +80 -0
- data/lib/taski/args.rb +2 -8
- data/lib/taski/env.rb +17 -0
- data/lib/taski/execution/base_progress_display.rb +348 -0
- data/lib/taski/execution/execution_context.rb +4 -0
- data/lib/taski/execution/executor.rb +111 -131
- data/lib/taski/execution/plain_progress_display.rb +76 -0
- data/lib/taski/execution/simple_progress_display.rb +173 -0
- data/lib/taski/execution/task_output_router.rb +91 -20
- data/lib/taski/execution/task_wrapper.rb +34 -31
- data/lib/taski/execution/tree_progress_display.rb +121 -271
- data/lib/taski/static_analysis/visitor.rb +3 -0
- data/lib/taski/task.rb +42 -30
- 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 +53 -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 +246 -0
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +119 -8
- metadata +14 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Taski
|
|
4
|
+
module TestHelper
|
|
5
|
+
# Global registry that stores mock definitions for tests.
|
|
6
|
+
# Uses a Mutex for thread-safety when accessed from worker threads.
|
|
7
|
+
# Mocks should be reset in test setup/teardown to ensure test isolation.
|
|
8
|
+
module MockRegistry
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
@mocks = {}
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Registers a mock for a task class.
|
|
14
|
+
# If a mock already exists for this class, it is replaced.
|
|
15
|
+
# @param task_class [Class] The task class to mock
|
|
16
|
+
# @param mock_wrapper [MockWrapper] The mock wrapper instance
|
|
17
|
+
def register(task_class, mock_wrapper)
|
|
18
|
+
@mutex.synchronize do
|
|
19
|
+
@mocks[task_class] = mock_wrapper
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Retrieves the mock wrapper for a task class, if one exists.
|
|
24
|
+
# @param task_class [Class] The task class to look up
|
|
25
|
+
# @return [MockWrapper, nil] The mock wrapper or nil if not mocked
|
|
26
|
+
def mock_for(task_class)
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
@mocks[task_class]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Checks if any mocks are registered.
|
|
33
|
+
# Used for optimization to skip mock lookup in hot paths.
|
|
34
|
+
# @return [Boolean] true if mocks exist
|
|
35
|
+
def mocks_active?
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
!@mocks.empty?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Clears all registered mocks and resets args/env.
|
|
42
|
+
# Should be called in test setup/teardown.
|
|
43
|
+
def reset!
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
@mocks = {}
|
|
46
|
+
end
|
|
47
|
+
Taski.reset_args!
|
|
48
|
+
Taski.reset_env!
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Taski
|
|
4
|
+
module TestHelper
|
|
5
|
+
# Wraps a mocked task and returns pre-configured values without executing the task.
|
|
6
|
+
# Tracks which methods were accessed for verification in tests.
|
|
7
|
+
class MockWrapper
|
|
8
|
+
attr_reader :task_class, :mock_values
|
|
9
|
+
|
|
10
|
+
# @param task_class [Class] The task class being mocked
|
|
11
|
+
# @param mock_values [Hash{Symbol => Object}] Method names mapped to their return values
|
|
12
|
+
def initialize(task_class, mock_values)
|
|
13
|
+
@task_class = task_class
|
|
14
|
+
@mock_values = mock_values
|
|
15
|
+
@access_counts = Hash.new(0)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns the mocked value for a method and records the access.
|
|
19
|
+
# @param method_name [Symbol] The exported method name
|
|
20
|
+
# @return [Object] The pre-configured mock value
|
|
21
|
+
# @raise [KeyError] If method_name was not configured in the mock
|
|
22
|
+
def get_exported_value(method_name)
|
|
23
|
+
unless @mock_values.key?(method_name)
|
|
24
|
+
raise KeyError, "No mock value for method :#{method_name} on #{@task_class}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@access_counts[method_name] += 1
|
|
28
|
+
@mock_values[method_name]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Checks if a method was accessed at least once.
|
|
32
|
+
# @param method_name [Symbol] The method name to check
|
|
33
|
+
# @return [Boolean] true if accessed at least once
|
|
34
|
+
def accessed?(method_name)
|
|
35
|
+
@access_counts[method_name] > 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns the number of times a method was accessed.
|
|
39
|
+
# @param method_name [Symbol] The method name to check
|
|
40
|
+
# @return [Integer] Number of accesses (0 if never accessed)
|
|
41
|
+
def access_count(method_name)
|
|
42
|
+
@access_counts[method_name]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../test_helper"
|
|
4
|
+
|
|
5
|
+
module Taski
|
|
6
|
+
module TestHelper
|
|
7
|
+
# RSpec integration module.
|
|
8
|
+
# Include this in your RSpec examples for automatic mock cleanup.
|
|
9
|
+
#
|
|
10
|
+
# @example In spec_helper.rb
|
|
11
|
+
# require 'taski/test_helper/rspec'
|
|
12
|
+
#
|
|
13
|
+
# RSpec.configure do |config|
|
|
14
|
+
# config.include Taski::TestHelper::RSpec
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @example In individual specs
|
|
18
|
+
# RSpec.describe MyTask do
|
|
19
|
+
# include Taski::TestHelper::RSpec
|
|
20
|
+
#
|
|
21
|
+
# it "processes data" do
|
|
22
|
+
# mock_task(FetchData, result: "mocked")
|
|
23
|
+
# # ... test code ...
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
module RSpec
|
|
27
|
+
def self.included(base)
|
|
28
|
+
base.include(Taski::TestHelper)
|
|
29
|
+
|
|
30
|
+
# Add before/after hooks when included in RSpec
|
|
31
|
+
if base.respond_to?(:before) && base.respond_to?(:after)
|
|
32
|
+
base.before(:each) { Taski::TestHelper.reset_mocks! }
|
|
33
|
+
base.after(:each) { Taski::TestHelper.reset_mocks! }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
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.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
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Module prepended to Scheduler to skip dependencies of mocked tasks.
|
|
58
|
+
# @api private
|
|
59
|
+
module SchedulerExtension
|
|
60
|
+
def build_dependency_graph(root_task_class)
|
|
61
|
+
queue = [root_task_class]
|
|
62
|
+
|
|
63
|
+
while (task_class = queue.shift)
|
|
64
|
+
next if @task_states.key?(task_class)
|
|
65
|
+
|
|
66
|
+
# Mocked tasks have no dependencies (isolates indirect dependencies)
|
|
67
|
+
mock = MockRegistry.mock_for(task_class)
|
|
68
|
+
deps = mock ? Set.new : task_class.cached_dependencies
|
|
69
|
+
@dependencies[task_class] = deps.dup
|
|
70
|
+
@task_states[task_class] = Taski::Execution::Scheduler::STATE_PENDING
|
|
71
|
+
|
|
72
|
+
deps.each { |dep| queue << dep }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Module prepended to Executor to skip execution of mocked tasks.
|
|
78
|
+
# @api private
|
|
79
|
+
module ExecutorExtension
|
|
80
|
+
def execute_task(task_class, wrapper)
|
|
81
|
+
return if @registry.abort_requested?
|
|
82
|
+
|
|
83
|
+
# Skip execution if task is mocked
|
|
84
|
+
if MockRegistry.mock_for(task_class)
|
|
85
|
+
wrapper.mark_completed(nil)
|
|
86
|
+
@completion_queue.push({task_class: task_class, wrapper: wrapper})
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
super
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class << self
|
|
95
|
+
# Checks if any mocks are currently registered.
|
|
96
|
+
# @return [Boolean] true if mocks exist
|
|
97
|
+
def mocks_active?
|
|
98
|
+
MockRegistry.mocks_active?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Retrieves the mock wrapper for a task class.
|
|
102
|
+
# @param task_class [Class] The task class to look up
|
|
103
|
+
# @return [MockWrapper, nil] The mock wrapper or nil if not mocked
|
|
104
|
+
def mock_for(task_class)
|
|
105
|
+
MockRegistry.mock_for(task_class)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Clears all registered mocks.
|
|
109
|
+
# Called automatically by test framework integrations.
|
|
110
|
+
def reset_mocks!
|
|
111
|
+
MockRegistry.reset!
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Sets mock args for the duration of the test.
|
|
116
|
+
# This allows testing code that depends on Taski.args without running full task execution.
|
|
117
|
+
# Args are automatically cleared when MockRegistry.reset! is called (in test teardown).
|
|
118
|
+
# @param options [Hash] User-defined options to include in args
|
|
119
|
+
# @return [Taski::Args] The created args instance
|
|
120
|
+
#
|
|
121
|
+
# @example
|
|
122
|
+
# mock_args(env: "test", debug: true)
|
|
123
|
+
# assert_equal "test", Taski.args[:env]
|
|
124
|
+
def mock_args(**options)
|
|
125
|
+
Taski.reset_args!
|
|
126
|
+
Taski.send(:start_args, options: options)
|
|
127
|
+
Taski.args
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Sets mock env for the duration of the test.
|
|
131
|
+
# This allows testing code that depends on Taski.env without running full task execution.
|
|
132
|
+
# Env is automatically cleared when MockRegistry.reset! is called (in test teardown).
|
|
133
|
+
# @param root_task [Class] The root task class (defaults to nil for testing)
|
|
134
|
+
# @return [Taski::Env] The created env instance
|
|
135
|
+
#
|
|
136
|
+
# @example
|
|
137
|
+
# mock_env(root_task: MyTask)
|
|
138
|
+
# assert_equal MyTask, Taski.env.root_task
|
|
139
|
+
def mock_env(root_task: nil)
|
|
140
|
+
Taski.reset_env!
|
|
141
|
+
Taski.send(:start_env, root_task: root_task)
|
|
142
|
+
Taski.env
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Registers a mock for a task class with specified return values.
|
|
146
|
+
# @param task_class [Class] A Taski::Task or Taski::Section subclass
|
|
147
|
+
# @param values [Hash{Symbol => Object}] Method names mapped to return values
|
|
148
|
+
# @return [MockWrapper] The created mock wrapper
|
|
149
|
+
# @raise [InvalidTaskError] If task_class is not a Taski::Task/Section subclass
|
|
150
|
+
# @raise [InvalidMethodError] If any method name is not an exported method
|
|
151
|
+
#
|
|
152
|
+
# @example
|
|
153
|
+
# mock_task(FetchData, result: { users: [1, 2, 3] })
|
|
154
|
+
# mock_task(Config, timeout: 30, retries: 3)
|
|
155
|
+
def mock_task(task_class, **values)
|
|
156
|
+
validate_task_class!(task_class)
|
|
157
|
+
validate_exported_methods!(task_class, values.keys)
|
|
158
|
+
|
|
159
|
+
mock_wrapper = MockWrapper.new(task_class, values)
|
|
160
|
+
MockRegistry.register(task_class, mock_wrapper)
|
|
161
|
+
mock_wrapper
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Asserts that a mocked task's method was accessed during the test.
|
|
165
|
+
# @param task_class [Class] The mocked task class
|
|
166
|
+
# @param method_name [Symbol] The exported method name
|
|
167
|
+
# @return [true] If assertion passes
|
|
168
|
+
# @raise [ArgumentError] If task_class was not mocked
|
|
169
|
+
# @raise [Minitest::Assertion, RSpec::Expectations::ExpectationNotMetError]
|
|
170
|
+
# If method was not accessed
|
|
171
|
+
def assert_task_accessed(task_class, method_name)
|
|
172
|
+
mock = fetch_mock!(task_class)
|
|
173
|
+
|
|
174
|
+
unless mock.accessed?(method_name)
|
|
175
|
+
raise assertion_error("Expected #{task_class}.#{method_name} to be accessed, but it was not")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
true
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Asserts that a mocked task's method was NOT accessed during the test.
|
|
182
|
+
# @param task_class [Class] The mocked task class
|
|
183
|
+
# @param method_name [Symbol] The exported method name
|
|
184
|
+
# @return [true] If assertion passes
|
|
185
|
+
# @raise [ArgumentError] If task_class was not mocked
|
|
186
|
+
# @raise [Minitest::Assertion, RSpec::Expectations::ExpectationNotMetError]
|
|
187
|
+
# If method was accessed
|
|
188
|
+
def refute_task_accessed(task_class, method_name)
|
|
189
|
+
mock = fetch_mock!(task_class)
|
|
190
|
+
|
|
191
|
+
if mock.accessed?(method_name)
|
|
192
|
+
count = mock.access_count(method_name)
|
|
193
|
+
raise assertion_error(
|
|
194
|
+
"Expected #{task_class}.#{method_name} not to be accessed, but it was accessed #{count} time(s)"
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
true
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
def fetch_mock!(task_class)
|
|
204
|
+
mock = MockRegistry.mock_for(task_class)
|
|
205
|
+
return mock if mock
|
|
206
|
+
|
|
207
|
+
raise ArgumentError, "Task #{task_class} was not mocked. Call mock_task first."
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def validate_task_class!(task_class)
|
|
211
|
+
valid = task_class.is_a?(Class) &&
|
|
212
|
+
(task_class < Taski::Task || task_class == Taski::Task)
|
|
213
|
+
return if valid
|
|
214
|
+
|
|
215
|
+
raise InvalidTaskError,
|
|
216
|
+
"Cannot mock #{task_class}: not a Taski::Task or Taski::Section subclass"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def validate_exported_methods!(task_class, method_names)
|
|
220
|
+
exported = task_class.exported_methods
|
|
221
|
+
method_names.each do |method_name|
|
|
222
|
+
unless exported.include?(method_name)
|
|
223
|
+
raise InvalidMethodError,
|
|
224
|
+
"Cannot mock :#{method_name} on #{task_class}: not an exported method. Exported: #{exported.inspect}"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def assertion_error(message)
|
|
230
|
+
# Use the appropriate assertion error class based on the test framework
|
|
231
|
+
# Use fully qualified names to avoid namespace conflicts
|
|
232
|
+
if defined?(::Minitest::Assertion)
|
|
233
|
+
::Minitest::Assertion.new(message)
|
|
234
|
+
elsif defined?(::RSpec::Expectations::ExpectationNotMetError)
|
|
235
|
+
::RSpec::Expectations::ExpectationNotMetError.new(message)
|
|
236
|
+
else
|
|
237
|
+
RuntimeError.new(message)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Prepend extensions when test helper is loaded
|
|
244
|
+
Taski::Task.singleton_class.prepend(Taski::TestHelper::TaskExtension)
|
|
245
|
+
Taski::Execution::Scheduler.prepend(Taski::TestHelper::SchedulerExtension)
|
|
246
|
+
Taski::Execution::Executor.prepend(Taski::TestHelper::ExecutorExtension)
|
data/lib/taski/version.rb
CHANGED
data/lib/taski.rb
CHANGED
|
@@ -11,7 +11,10 @@ require_relative "taski/execution/scheduler"
|
|
|
11
11
|
require_relative "taski/execution/worker_pool"
|
|
12
12
|
require_relative "taski/execution/executor"
|
|
13
13
|
require_relative "taski/execution/tree_progress_display"
|
|
14
|
+
require_relative "taski/execution/simple_progress_display"
|
|
15
|
+
require_relative "taski/execution/plain_progress_display"
|
|
14
16
|
require_relative "taski/args"
|
|
17
|
+
require_relative "taski/env"
|
|
15
18
|
|
|
16
19
|
module Taski
|
|
17
20
|
class TaskAbortException < StandardError
|
|
@@ -31,13 +34,15 @@ module Taski
|
|
|
31
34
|
|
|
32
35
|
# Represents a single task failure with its context
|
|
33
36
|
class TaskFailure
|
|
34
|
-
attr_reader :task_class, :error
|
|
37
|
+
attr_reader :task_class, :error, :output_lines
|
|
35
38
|
|
|
36
39
|
# @param task_class [Class] The task class that failed
|
|
37
40
|
# @param error [Exception] The exception that was raised
|
|
38
|
-
|
|
41
|
+
# @param output_lines [Array<String>] Recent output lines from the failed task
|
|
42
|
+
def initialize(task_class:, error:, output_lines: [])
|
|
39
43
|
@task_class = task_class
|
|
40
44
|
@error = error
|
|
45
|
+
@output_lines = output_lines
|
|
41
46
|
end
|
|
42
47
|
end
|
|
43
48
|
|
|
@@ -117,12 +122,24 @@ module Taski
|
|
|
117
122
|
|
|
118
123
|
def build_message
|
|
119
124
|
task_word = (errors.size == 1) ? "task" : "tasks"
|
|
120
|
-
"#{errors.size} #{task_word} failed
|
|
121
|
-
|
|
125
|
+
parts = ["#{errors.size} #{task_word} failed:"]
|
|
126
|
+
|
|
127
|
+
errors.each do |f|
|
|
128
|
+
parts << " - #{f.task_class.name}: #{f.error.message}"
|
|
129
|
+
|
|
130
|
+
# Include captured output if available
|
|
131
|
+
if f.output_lines && !f.output_lines.empty?
|
|
132
|
+
parts << " Output:"
|
|
133
|
+
f.output_lines.each { |line| parts << " #{line}" }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
parts.join("\n")
|
|
122
138
|
end
|
|
123
139
|
end
|
|
124
140
|
|
|
125
141
|
@args_monitor = Monitor.new
|
|
142
|
+
@env_monitor = Monitor.new
|
|
126
143
|
|
|
127
144
|
# Get the current runtime arguments
|
|
128
145
|
# @return [Args, nil] The current args or nil if no task is running
|
|
@@ -130,12 +147,51 @@ module Taski
|
|
|
130
147
|
@args_monitor.synchronize { @args }
|
|
131
148
|
end
|
|
132
149
|
|
|
150
|
+
# Get the current execution environment
|
|
151
|
+
# @return [Env, nil] The current env or nil if no task is running
|
|
152
|
+
def self.env
|
|
153
|
+
@env_monitor.synchronize { @env }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Start new execution environment (internal use only)
|
|
157
|
+
# @api private
|
|
158
|
+
# @return [Boolean] true if this call created the env, false if env already existed
|
|
159
|
+
def self.start_env(root_task:)
|
|
160
|
+
@env_monitor.synchronize do
|
|
161
|
+
return false if @env
|
|
162
|
+
@env = Env.new(root_task: root_task)
|
|
163
|
+
true
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Reset the execution environment (internal use only)
|
|
168
|
+
# @api private
|
|
169
|
+
def self.reset_env!
|
|
170
|
+
@env_monitor.synchronize { @env = nil }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Execute a block with env lifecycle management.
|
|
174
|
+
# Creates env if it doesn't exist, and resets it only if this call created it.
|
|
175
|
+
# This prevents race conditions in concurrent execution.
|
|
176
|
+
#
|
|
177
|
+
# @param root_task [Class] The root task class
|
|
178
|
+
# @yield The block to execute with env available
|
|
179
|
+
# @return [Object] The result of the block
|
|
180
|
+
def self.with_env(root_task:)
|
|
181
|
+
created_env = start_env(root_task: root_task)
|
|
182
|
+
yield
|
|
183
|
+
ensure
|
|
184
|
+
reset_env! if created_env
|
|
185
|
+
end
|
|
186
|
+
|
|
133
187
|
# Start new runtime arguments (internal use only)
|
|
134
188
|
# @api private
|
|
135
|
-
|
|
189
|
+
# @return [Boolean] true if this call created the args, false if args already existed
|
|
190
|
+
def self.start_args(options:)
|
|
136
191
|
@args_monitor.synchronize do
|
|
137
|
-
return if @args
|
|
138
|
-
@args = Args.new(options: options
|
|
192
|
+
return false if @args
|
|
193
|
+
@args = Args.new(options: options)
|
|
194
|
+
true
|
|
139
195
|
end
|
|
140
196
|
end
|
|
141
197
|
|
|
@@ -145,21 +201,76 @@ module Taski
|
|
|
145
201
|
@args_monitor.synchronize { @args = nil }
|
|
146
202
|
end
|
|
147
203
|
|
|
204
|
+
# Execute a block with args lifecycle management.
|
|
205
|
+
# Creates args if they don't exist, and resets them only if this call created them.
|
|
206
|
+
# This prevents race conditions in concurrent execution.
|
|
207
|
+
#
|
|
208
|
+
# @param options [Hash] User-defined options
|
|
209
|
+
# @yield The block to execute with args available
|
|
210
|
+
# @return [Object] The result of the block
|
|
211
|
+
def self.with_args(options:)
|
|
212
|
+
created_args = start_args(options: options)
|
|
213
|
+
yield
|
|
214
|
+
ensure
|
|
215
|
+
reset_args! if created_args
|
|
216
|
+
end
|
|
217
|
+
|
|
148
218
|
# Progress display is enabled by default (tree-style).
|
|
149
219
|
# Environment variables:
|
|
150
220
|
# - TASKI_PROGRESS_DISABLE=1: Disable progress display entirely
|
|
221
|
+
# - TASKI_PROGRESS_MODE=simple|tree: Set display mode (default: tree)
|
|
151
222
|
def self.progress_display
|
|
152
223
|
return nil if progress_disabled?
|
|
153
|
-
@progress_display ||=
|
|
224
|
+
@progress_display ||= create_progress_display
|
|
154
225
|
end
|
|
155
226
|
|
|
156
227
|
def self.progress_disabled?
|
|
157
228
|
ENV["TASKI_PROGRESS_DISABLE"] == "1"
|
|
158
229
|
end
|
|
159
230
|
|
|
231
|
+
# Get the current progress mode (:tree or :simple)
|
|
232
|
+
# @return [Symbol] The current progress mode
|
|
233
|
+
def self.progress_mode
|
|
234
|
+
@progress_mode || progress_mode_from_env
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Set the progress mode (:tree or :simple)
|
|
238
|
+
# @param mode [Symbol] The mode to use (:tree or :simple)
|
|
239
|
+
def self.progress_mode=(mode)
|
|
240
|
+
@progress_mode = mode.to_sym
|
|
241
|
+
# Reset display so it will be recreated with new mode
|
|
242
|
+
@progress_display&.stop
|
|
243
|
+
@progress_display = nil
|
|
244
|
+
end
|
|
245
|
+
|
|
160
246
|
def self.reset_progress_display!
|
|
161
247
|
@progress_display&.stop
|
|
162
248
|
@progress_display = nil
|
|
249
|
+
@progress_mode = nil
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# @api private
|
|
253
|
+
def self.create_progress_display
|
|
254
|
+
case progress_mode
|
|
255
|
+
when :simple
|
|
256
|
+
Execution::SimpleProgressDisplay.new
|
|
257
|
+
when :plain
|
|
258
|
+
Execution::PlainProgressDisplay.new
|
|
259
|
+
else
|
|
260
|
+
Execution::TreeProgressDisplay.new
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# @api private
|
|
265
|
+
def self.progress_mode_from_env
|
|
266
|
+
case ENV["TASKI_PROGRESS_MODE"]
|
|
267
|
+
when "simple"
|
|
268
|
+
:simple
|
|
269
|
+
when "plain"
|
|
270
|
+
:plain
|
|
271
|
+
else
|
|
272
|
+
:tree
|
|
273
|
+
end
|
|
163
274
|
end
|
|
164
275
|
|
|
165
276
|
# Get the worker count from the current args (set via Task.run(workers: n))
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: taski
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ahogappa
|
|
@@ -61,19 +61,25 @@ files:
|
|
|
61
61
|
- examples/clean_demo.rb
|
|
62
62
|
- examples/data_pipeline_demo.rb
|
|
63
63
|
- examples/group_demo.rb
|
|
64
|
+
- examples/large_tree_demo.rb
|
|
64
65
|
- examples/nested_section_demo.rb
|
|
65
66
|
- examples/parallel_progress_demo.rb
|
|
66
67
|
- examples/quick_start.rb
|
|
67
68
|
- examples/reexecution_demo.rb
|
|
68
69
|
- examples/section_demo.rb
|
|
70
|
+
- examples/simple_progress_demo.rb
|
|
69
71
|
- examples/system_call_demo.rb
|
|
70
72
|
- examples/tree_progress_demo.rb
|
|
71
73
|
- lib/taski.rb
|
|
72
74
|
- lib/taski/args.rb
|
|
75
|
+
- lib/taski/env.rb
|
|
76
|
+
- lib/taski/execution/base_progress_display.rb
|
|
73
77
|
- lib/taski/execution/execution_context.rb
|
|
74
78
|
- lib/taski/execution/executor.rb
|
|
79
|
+
- lib/taski/execution/plain_progress_display.rb
|
|
75
80
|
- lib/taski/execution/registry.rb
|
|
76
81
|
- lib/taski/execution/scheduler.rb
|
|
82
|
+
- lib/taski/execution/simple_progress_display.rb
|
|
77
83
|
- lib/taski/execution/task_output_pipe.rb
|
|
78
84
|
- lib/taski/execution/task_output_router.rb
|
|
79
85
|
- lib/taski/execution/task_wrapper.rb
|
|
@@ -84,6 +90,12 @@ files:
|
|
|
84
90
|
- lib/taski/static_analysis/dependency_graph.rb
|
|
85
91
|
- lib/taski/static_analysis/visitor.rb
|
|
86
92
|
- lib/taski/task.rb
|
|
93
|
+
- lib/taski/test_helper.rb
|
|
94
|
+
- lib/taski/test_helper/errors.rb
|
|
95
|
+
- lib/taski/test_helper/minitest.rb
|
|
96
|
+
- lib/taski/test_helper/mock_registry.rb
|
|
97
|
+
- lib/taski/test_helper/mock_wrapper.rb
|
|
98
|
+
- lib/taski/test_helper/rspec.rb
|
|
87
99
|
- lib/taski/version.rb
|
|
88
100
|
- rbs_collection.lock.yaml
|
|
89
101
|
- rbs_collection.yaml
|
|
@@ -108,7 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
108
120
|
- !ruby/object:Gem::Version
|
|
109
121
|
version: '0'
|
|
110
122
|
requirements: []
|
|
111
|
-
rubygems_version:
|
|
123
|
+
rubygems_version: 4.0.3
|
|
112
124
|
specification_version: 4
|
|
113
125
|
summary: A simple yet powerful Ruby task runner with static dependency resolution
|
|
114
126
|
(in development).
|