taski 0.3.1 → 0.4.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/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/ast/2.4/ast.rbs +73 -0
- data/.gem_rbs_collection/minitest/5.25/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/abstract_reporter.rbs +52 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/assertion.rbs +17 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/assertions.rbs +590 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/backtrace_filter.rbs +23 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/bench_spec.rbs +102 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/benchmark.rbs +259 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/composite_reporter.rbs +25 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/compress.rbs +13 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/error_on_warning.rbs +3 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/expectation.rbs +2 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/expectations.rbs +21 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/guard.rbs +64 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/mock.rbs +64 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/parallel/executor.rbs +46 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/parallel/test/class_methods.rbs +5 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/parallel/test.rbs +3 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/parallel.rbs +2 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/pride_io.rbs +62 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/pride_lol.rbs +19 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/progress_reporter.rbs +11 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/reportable.rbs +53 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/reporter.rbs +5 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/result.rbs +28 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/runnable.rbs +163 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/skip.rbs +6 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/spec/dsl/instance_methods.rbs +48 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/spec/dsl.rbs +129 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/spec.rbs +11 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/statistics_reporter.rbs +81 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/summary_reporter.rbs +18 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/test/lifecycle_hooks.rbs +92 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/test.rbs +69 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/unexpected_error.rbs +12 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/unexpected_warning.rbs +6 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/unit/test_case.rbs +3 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/unit.rbs +4 -0
- data/.gem_rbs_collection/minitest/5.25/minitest.rbs +115 -0
- data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
- data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parser/3.2/manifest.yaml +7 -0
- data/.gem_rbs_collection/parser/3.2/parser.rbs +193 -0
- data/.gem_rbs_collection/parser/3.2/polyfill.rbs +4 -0
- data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rainbow/3.0/global.rbs +7 -0
- data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +209 -0
- data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +5 -0
- data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
- data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
- data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +17 -0
- data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +129 -0
- data/.gem_rbs_collection/rubocop-ast/1.30/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop-ast/1.30/rubocop-ast.rbs +771 -0
- data/.gem_rbs_collection/simplecov/0.22/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/simplecov/0.22/simplecov.rbs +54 -0
- data/README.md +138 -247
- data/Steepfile +19 -0
- data/docs/advanced-features.md +625 -0
- data/docs/api-guide.md +509 -0
- data/docs/error-handling.md +684 -0
- data/examples/README.md +95 -42
- data/examples/context_demo.rb +112 -0
- data/examples/data_pipeline_demo.rb +231 -0
- data/examples/parallel_progress_demo.rb +72 -0
- data/examples/quick_start.rb +4 -4
- data/examples/reexecution_demo.rb +127 -0
- data/examples/{section_configuration.rb → section_demo.rb} +49 -60
- data/lib/taski/context.rb +52 -0
- data/lib/taski/execution/coordinator.rb +63 -0
- data/lib/taski/execution/parallel_progress_display.rb +201 -0
- data/lib/taski/execution/registry.rb +72 -0
- data/lib/taski/execution/task_wrapper.rb +255 -0
- data/lib/taski/section.rb +26 -254
- data/lib/taski/static_analysis/analyzer.rb +34 -0
- data/lib/taski/static_analysis/dependency_graph.rb +90 -0
- data/lib/taski/static_analysis/visitor.rb +114 -0
- data/lib/taski/task.rb +173 -0
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +45 -39
- data/rbs_collection.lock.yaml +116 -0
- data/rbs_collection.yaml +19 -0
- data/sig/taski.rbs +269 -62
- metadata +97 -32
- data/examples/advanced_patterns.rb +0 -119
- data/examples/progress_demo.rb +0 -166
- data/examples/tree_demo.rb +0 -205
- data/lib/taski/dependency_analyzer.rb +0 -232
- data/lib/taski/exceptions.rb +0 -17
- data/lib/taski/logger.rb +0 -158
- data/lib/taski/logging/formatter_factory.rb +0 -34
- data/lib/taski/logging/formatter_interface.rb +0 -19
- data/lib/taski/logging/json_formatter.rb +0 -26
- data/lib/taski/logging/simple_formatter.rb +0 -16
- data/lib/taski/logging/structured_formatter.rb +0 -44
- data/lib/taski/progress/display_colors.rb +0 -17
- data/lib/taski/progress/display_manager.rb +0 -117
- data/lib/taski/progress/output_capture.rb +0 -105
- data/lib/taski/progress/spinner_animation.rb +0 -49
- data/lib/taski/progress/task_formatter.rb +0 -25
- data/lib/taski/progress/task_status.rb +0 -38
- data/lib/taski/progress/terminal_controller.rb +0 -35
- data/lib/taski/progress_display.rb +0 -57
- data/lib/taski/reference.rb +0 -40
- data/lib/taski/task/base.rb +0 -91
- data/lib/taski/task/define_api.rb +0 -156
- data/lib/taski/task/dependency_resolver.rb +0 -73
- data/lib/taski/task/exports_api.rb +0 -29
- data/lib/taski/task/instance_management.rb +0 -201
- data/lib/taski/tree_colors.rb +0 -91
- data/lib/taski/utils/dependency_resolver_helper.rb +0 -85
- data/lib/taski/utils/tree_display_helper.rb +0 -68
- data/lib/taski/utils.rb +0 -107
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module Taski
|
|
6
|
+
module Execution
|
|
7
|
+
class Registry
|
|
8
|
+
def initialize
|
|
9
|
+
@tasks = {}
|
|
10
|
+
@threads = []
|
|
11
|
+
@monitor = Monitor.new
|
|
12
|
+
@abort_requested = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param task_class [Class] The task class
|
|
16
|
+
# @yield Block to create the task instance if it doesn't exist
|
|
17
|
+
# @return [Object] The task instance
|
|
18
|
+
def get_or_create(task_class)
|
|
19
|
+
@tasks[task_class] ||= yield
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param task_class [Class] The task class
|
|
23
|
+
# @return [Object] The task instance
|
|
24
|
+
# @raise [RuntimeError] If the task is not registered
|
|
25
|
+
def get_task(task_class)
|
|
26
|
+
@tasks.fetch(task_class) do
|
|
27
|
+
raise "Task #{task_class} not registered"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param thread [Thread] The thread to register
|
|
32
|
+
def register_thread(thread)
|
|
33
|
+
@monitor.synchronize { @threads << thread }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def wait_all
|
|
37
|
+
threads = @monitor.synchronize { @threads.dup }
|
|
38
|
+
threads.each(&:join)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def reset!
|
|
42
|
+
@monitor.synchronize do
|
|
43
|
+
@tasks.clear
|
|
44
|
+
@threads.clear
|
|
45
|
+
@abort_requested = false
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def request_abort!
|
|
50
|
+
@monitor.synchronize { @abort_requested = true }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Boolean] true if abort has been requested
|
|
54
|
+
def abort_requested?
|
|
55
|
+
@monitor.synchronize { @abort_requested }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param task_class [Class] The task class to run
|
|
59
|
+
# @param exported_methods [Array<Symbol>] Methods to call to trigger execution
|
|
60
|
+
# @return [Object] The result of the task execution
|
|
61
|
+
def run(task_class, exported_methods)
|
|
62
|
+
exported_methods.each do |method|
|
|
63
|
+
task_class.public_send(method)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
wait_all
|
|
67
|
+
|
|
68
|
+
get_task(task_class).result
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module Taski
|
|
6
|
+
module Execution
|
|
7
|
+
class TaskTiming < Data.define(:start_time, :end_time)
|
|
8
|
+
# @return [Float, nil] Duration in milliseconds or nil if not available
|
|
9
|
+
def duration_ms
|
|
10
|
+
return nil unless start_time && end_time
|
|
11
|
+
((end_time - start_time) * 1000).round(1)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @return [TaskTiming] New timing with current time as start
|
|
15
|
+
def self.start_now
|
|
16
|
+
new(start_time: Time.now, end_time: nil)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [TaskTiming] New timing with current time as end
|
|
20
|
+
def with_end_now
|
|
21
|
+
with(end_time: Time.now)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class TaskWrapper
|
|
26
|
+
attr_reader :task, :result
|
|
27
|
+
|
|
28
|
+
STATE_PENDING = :pending
|
|
29
|
+
STATE_RUNNING = :running
|
|
30
|
+
STATE_COMPLETED = :completed
|
|
31
|
+
|
|
32
|
+
def initialize(task, registry:, coordinator:)
|
|
33
|
+
@task = task
|
|
34
|
+
@registry = registry
|
|
35
|
+
@coordinator = coordinator
|
|
36
|
+
@result = nil
|
|
37
|
+
@clean_result = nil
|
|
38
|
+
@error = nil
|
|
39
|
+
@monitor = Monitor.new
|
|
40
|
+
@condition = @monitor.new_cond
|
|
41
|
+
@clean_condition = @monitor.new_cond
|
|
42
|
+
@state = STATE_PENDING
|
|
43
|
+
@clean_state = STATE_PENDING
|
|
44
|
+
@timing = nil
|
|
45
|
+
|
|
46
|
+
register_with_progress_display
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Object] The result of task execution
|
|
50
|
+
def run
|
|
51
|
+
execute_task_if_needed
|
|
52
|
+
raise @error if @error # steep:ignore
|
|
53
|
+
@result
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [Object] The result of cleanup
|
|
57
|
+
def clean
|
|
58
|
+
execute_clean_if_needed
|
|
59
|
+
@clean_result
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param method_name [Symbol] The name of the exported method
|
|
63
|
+
# @return [Object] The exported value
|
|
64
|
+
def get_exported_value(method_name)
|
|
65
|
+
execute_task_if_needed
|
|
66
|
+
raise @error if @error # steep:ignore
|
|
67
|
+
@task.public_send(method_name)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def start_thread_with(&block)
|
|
73
|
+
thread = Thread.new(&block)
|
|
74
|
+
@registry.register_thread(thread)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Thread-safe state machine that ensures operations are executed exactly once.
|
|
78
|
+
# Uses pattern matching for exhaustive state handling.
|
|
79
|
+
def execute_with_state_pattern(state_getter:, starter:, waiter:, pre_start_check: nil)
|
|
80
|
+
@monitor.synchronize do
|
|
81
|
+
case state_getter.call
|
|
82
|
+
in STATE_PENDING
|
|
83
|
+
pre_start_check&.call
|
|
84
|
+
starter.call
|
|
85
|
+
waiter.call
|
|
86
|
+
in STATE_RUNNING
|
|
87
|
+
waiter.call
|
|
88
|
+
in STATE_COMPLETED
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def execute_task_if_needed
|
|
95
|
+
execute_with_state_pattern(
|
|
96
|
+
state_getter: -> { @state },
|
|
97
|
+
starter: -> { start_async_execution },
|
|
98
|
+
waiter: -> { wait_for_completion },
|
|
99
|
+
pre_start_check: -> {
|
|
100
|
+
if @registry.abort_requested?
|
|
101
|
+
raise Taski::TaskAbortException, "Execution aborted - no new tasks will start"
|
|
102
|
+
end
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def start_async_execution
|
|
108
|
+
@state = STATE_RUNNING
|
|
109
|
+
@timing = TaskTiming.start_now
|
|
110
|
+
update_progress(:running)
|
|
111
|
+
start_thread_with { execute_task }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def execute_task
|
|
115
|
+
if @registry.abort_requested?
|
|
116
|
+
@error = Taski::TaskAbortException.new("Execution aborted - no new tasks will start")
|
|
117
|
+
mark_completed
|
|
118
|
+
return
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
log_start
|
|
122
|
+
@coordinator.start_dependencies(@task.class)
|
|
123
|
+
wait_for_dependencies
|
|
124
|
+
@result = @task.run
|
|
125
|
+
mark_completed
|
|
126
|
+
log_completion
|
|
127
|
+
rescue Taski::TaskAbortException => e
|
|
128
|
+
@registry.request_abort!
|
|
129
|
+
@error = e
|
|
130
|
+
mark_completed
|
|
131
|
+
rescue => e
|
|
132
|
+
@error = e
|
|
133
|
+
mark_completed
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def wait_for_dependencies
|
|
137
|
+
dependencies = @task.class.cached_dependencies
|
|
138
|
+
return if dependencies.empty?
|
|
139
|
+
|
|
140
|
+
dependencies.each do |dep_class|
|
|
141
|
+
dep_class.exported_methods.each do |method|
|
|
142
|
+
dep_class.public_send(method)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def execute_clean_if_needed
|
|
148
|
+
execute_with_state_pattern(
|
|
149
|
+
state_getter: -> { @clean_state },
|
|
150
|
+
starter: -> { start_async_clean },
|
|
151
|
+
waiter: -> { wait_for_clean_completion }
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def start_async_clean
|
|
156
|
+
@clean_state = STATE_RUNNING
|
|
157
|
+
start_thread_with { execute_clean }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def execute_clean
|
|
161
|
+
log_clean_start
|
|
162
|
+
@clean_result = @task.clean
|
|
163
|
+
wait_for_clean_dependencies
|
|
164
|
+
mark_clean_completed
|
|
165
|
+
log_clean_completion
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def wait_for_clean_dependencies
|
|
169
|
+
dependencies = @task.class.cached_dependencies
|
|
170
|
+
return if dependencies.empty?
|
|
171
|
+
|
|
172
|
+
wait_threads = dependencies.map do |dep_class|
|
|
173
|
+
Thread.new do
|
|
174
|
+
dep_class.public_send(:clean)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
wait_threads.each(&:join)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def mark_completed
|
|
182
|
+
@timing = @timing&.with_end_now
|
|
183
|
+
@monitor.synchronize do
|
|
184
|
+
@state = STATE_COMPLETED
|
|
185
|
+
@condition.broadcast
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
if @error
|
|
189
|
+
update_progress(:failed, error: @error)
|
|
190
|
+
else
|
|
191
|
+
update_progress(:completed, duration: @timing&.duration_ms)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def mark_clean_completed
|
|
196
|
+
@monitor.synchronize do
|
|
197
|
+
@clean_state = STATE_COMPLETED
|
|
198
|
+
@clean_condition.broadcast
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def wait_for_completion
|
|
203
|
+
@condition.wait_until { @state == STATE_COMPLETED }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def wait_for_clean_completion
|
|
207
|
+
@clean_condition.wait_until { @clean_state == STATE_COMPLETED }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def debug_log(message)
|
|
211
|
+
return unless ENV["TASKI_DEBUG"]
|
|
212
|
+
puts message
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def log_start
|
|
216
|
+
debug_log("Invoking #{@task.class} wrapper in thread #{Thread.current.object_id}...")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def log_completion
|
|
220
|
+
debug_log("Wrapper #{@task.class} completed in thread #{Thread.current.object_id}.")
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def log_clean_start
|
|
224
|
+
debug_log("Cleaning #{@task.class} in thread #{Thread.current.object_id}...")
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def log_clean_completion
|
|
228
|
+
debug_log("Clean #{@task.class} completed in thread #{Thread.current.object_id}.")
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def register_with_progress_display
|
|
232
|
+
Taski.progress_display&.register_task(@task.class)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# @param state [Symbol] The new state
|
|
236
|
+
# @param duration [Float, nil] Duration in milliseconds
|
|
237
|
+
# @param error [Exception, nil] Error object
|
|
238
|
+
def update_progress(state, duration: nil, error: nil)
|
|
239
|
+
Taski.progress_display&.update_task(@task.class, state: state, duration: duration, error: error)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def method_missing(method_name, *args, &block)
|
|
243
|
+
if @task.class.method_defined?(method_name)
|
|
244
|
+
get_exported_value(method_name)
|
|
245
|
+
else
|
|
246
|
+
super
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
251
|
+
@task.class.method_defined?(method_name) || super
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
data/lib/taski/section.rb
CHANGED
|
@@ -1,272 +1,44 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
4
|
-
require_relative "utils"
|
|
5
|
-
require_relative "utils/tree_display_helper"
|
|
6
|
-
require_relative "utils/dependency_resolver_helper"
|
|
3
|
+
require_relative "task"
|
|
7
4
|
|
|
8
5
|
module Taski
|
|
9
|
-
|
|
10
|
-
# while maintaining static analysis capabilities
|
|
11
|
-
class Section
|
|
6
|
+
class Section < Task
|
|
12
7
|
class << self
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# @param queue [Array] Queue of tasks to process
|
|
17
|
-
# @param resolved [Array] Array of resolved tasks
|
|
18
|
-
# @return [self] Returns self for method chaining
|
|
19
|
-
def resolve(queue, resolved)
|
|
20
|
-
resolve_common(queue, resolved)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Resolve all dependencies in topological order
|
|
24
|
-
# @return [Array<Class>] Array of tasks in dependency order
|
|
25
|
-
def resolve_dependencies
|
|
26
|
-
resolve_dependencies_common
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Analyze dependencies when accessing interface methods
|
|
30
|
-
def analyze_dependencies_for_interfaces
|
|
31
|
-
interface_exports.each do |interface_method|
|
|
32
|
-
dependencies = gather_static_dependencies_for_interface(interface_method)
|
|
33
|
-
add_unique_dependencies(dependencies)
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
# Gather dependencies from interface method implementation
|
|
40
|
-
def gather_static_dependencies_for_interface(interface_method)
|
|
41
|
-
# For sections, we analyze the impl method
|
|
42
|
-
# Try instance method first, then class method
|
|
43
|
-
if impl_defined?
|
|
44
|
-
# For instance method, we can't analyze dependencies statically
|
|
45
|
-
# So we return empty array
|
|
46
|
-
[]
|
|
47
|
-
else
|
|
48
|
-
DependencyAnalyzer.analyze_method(self, :impl)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Add dependencies that don't already exist
|
|
53
|
-
def add_unique_dependencies(dep_classes)
|
|
54
|
-
dep_classes.each do |dep_class|
|
|
55
|
-
next if dep_class == self || dependency_exists?(dep_class)
|
|
56
|
-
add_dependency(dep_class)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Add a single dependency
|
|
61
|
-
def add_dependency(dep_class)
|
|
62
|
-
@dependencies ||= []
|
|
63
|
-
@dependencies << {klass: dep_class}
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Check if dependency already exists
|
|
67
|
-
def dependency_exists?(dep_class)
|
|
68
|
-
(@dependencies || []).any? { |d| d[:klass] == dep_class }
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Extract class from dependency specification
|
|
72
|
-
def extract_class(task)
|
|
73
|
-
case task
|
|
74
|
-
when Class
|
|
75
|
-
task
|
|
76
|
-
when Hash
|
|
77
|
-
task[:klass]
|
|
78
|
-
else
|
|
79
|
-
task
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
public
|
|
84
|
-
|
|
85
|
-
# === Instance Management (minimal for Section) ===
|
|
86
|
-
|
|
87
|
-
# Ensure section is available (no actual building needed)
|
|
88
|
-
# @return [self] Returns self for compatibility with Task interface
|
|
89
|
-
def ensure_instance_built
|
|
90
|
-
self
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Build method for compatibility (Section doesn't build instances)
|
|
94
|
-
# @param args [Hash] Optional arguments (ignored for sections)
|
|
95
|
-
# @return [self] Returns self
|
|
96
|
-
def build(**args)
|
|
97
|
-
self
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# Reset method for compatibility (Section doesn't have state to reset)
|
|
101
|
-
# @return [self] Returns self
|
|
102
|
-
def reset!
|
|
103
|
-
self
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Display dependency tree for this section
|
|
107
|
-
# @param prefix [String] Current indentation prefix
|
|
108
|
-
# @param visited [Set] Set of visited classes to prevent infinite loops
|
|
109
|
-
# @param color [Boolean] Whether to use color output
|
|
110
|
-
# @return [String] Formatted dependency tree
|
|
111
|
-
def tree(prefix = "", visited = Set.new, color: TreeColors.enabled?)
|
|
112
|
-
should_return_early, early_result, new_visited = handle_circular_dependency_check(visited, self, prefix)
|
|
113
|
-
return early_result if should_return_early
|
|
114
|
-
|
|
115
|
-
# Get section name with fallback for anonymous classes
|
|
116
|
-
section_name = name || to_s
|
|
117
|
-
colored_section_name = color ? TreeColors.section(section_name) : section_name
|
|
118
|
-
result = "#{prefix}#{colored_section_name}\n"
|
|
119
|
-
|
|
120
|
-
# Add possible implementations - detect from nested Task classes
|
|
121
|
-
possible_implementations = find_possible_implementations
|
|
122
|
-
if possible_implementations.any?
|
|
123
|
-
impl_names = possible_implementations.map { |impl| extract_implementation_name(impl) }
|
|
124
|
-
impl_text = "[One of: #{impl_names.join(", ")}]"
|
|
125
|
-
colored_impl_text = color ? TreeColors.implementations(impl_text) : impl_text
|
|
126
|
-
connector = color ? TreeColors.connector("└── ") : "└── "
|
|
127
|
-
result += "#{prefix}#{connector}#{colored_impl_text}\n"
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
dependencies = @dependencies || []
|
|
131
|
-
result += render_dependencies_tree(dependencies, prefix, new_visited, color)
|
|
132
|
-
|
|
133
|
-
result
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Define interface methods for this section
|
|
137
|
-
def interface(*names)
|
|
138
|
-
if names.empty?
|
|
139
|
-
raise ArgumentError, "interface requires at least one method name"
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
@interface_exports = names
|
|
143
|
-
|
|
144
|
-
# Create accessor methods for each interface name
|
|
145
|
-
names.each do |name|
|
|
146
|
-
define_singleton_method(name) do
|
|
147
|
-
# Get implementation class
|
|
148
|
-
implementation_class = get_implementation_class
|
|
149
|
-
|
|
150
|
-
# Check if implementation is nil
|
|
151
|
-
if implementation_class.nil?
|
|
152
|
-
raise SectionImplementationError,
|
|
153
|
-
"impl returned nil. " \
|
|
154
|
-
"Make sure impl returns a Task class."
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# Validate that it's a Task class
|
|
158
|
-
unless implementation_class.is_a?(Class) && implementation_class < Taski::Task
|
|
159
|
-
raise SectionImplementationError,
|
|
160
|
-
"impl must return a Task class, got #{implementation_class.class}. " \
|
|
161
|
-
"Make sure impl returns a class that inherits from Taski::Task."
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# Build the implementation and call the method
|
|
165
|
-
implementation = implementation_class.build
|
|
166
|
-
|
|
167
|
-
begin
|
|
168
|
-
implementation.send(name)
|
|
169
|
-
rescue NoMethodError
|
|
170
|
-
raise SectionImplementationError,
|
|
171
|
-
"Implementation does not provide required method '#{name}'. " \
|
|
172
|
-
"Make sure the implementation class has a '#{name}' method or " \
|
|
173
|
-
"exports :#{name} declaration."
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
# Automatically apply exports to existing nested Task classes
|
|
179
|
-
auto_apply_exports_to_existing_tasks
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Get the interface exports for this section
|
|
183
|
-
def interface_exports
|
|
184
|
-
@interface_exports || []
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
# Check if impl method is defined (as instance method)
|
|
188
|
-
def impl_defined?
|
|
189
|
-
instance_methods(false).include?(:impl)
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
# Get implementation class from instance method
|
|
193
|
-
def get_implementation_class
|
|
194
|
-
if impl_defined?
|
|
195
|
-
# Create a temporary instance to call impl method
|
|
196
|
-
allocate.impl
|
|
197
|
-
else
|
|
198
|
-
# Fall back to class method if exists
|
|
199
|
-
impl
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# Override const_set to auto-add exports to nested Task classes
|
|
204
|
-
def const_set(name, value)
|
|
205
|
-
result = super
|
|
206
|
-
|
|
207
|
-
# If the constant is a Task class and we have interface exports,
|
|
208
|
-
# automatically add exports to avoid duplication
|
|
209
|
-
if value.is_a?(Class) && value < Taski::Task && !interface_exports.empty?
|
|
210
|
-
# Add exports declaration to the nested task
|
|
211
|
-
exports_list = interface_exports
|
|
212
|
-
value.class_eval do
|
|
213
|
-
exports(*exports_list)
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
result
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
private
|
|
221
|
-
|
|
222
|
-
# Automatically apply exports to existing nested Task classes when interface is defined
|
|
223
|
-
def auto_apply_exports_to_existing_tasks
|
|
224
|
-
constants.each do |const_name|
|
|
225
|
-
const_value = const_get(const_name)
|
|
226
|
-
if const_value.is_a?(Class) && const_value < Taski::Task && !interface_exports.empty?
|
|
227
|
-
exports_list = interface_exports
|
|
228
|
-
const_value.class_eval do
|
|
229
|
-
exports(*exports_list) unless @exports_defined
|
|
230
|
-
@exports_defined = true
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
end
|
|
8
|
+
# @param interface_methods [Array<Symbol>] Names of interface methods
|
|
9
|
+
def interfaces(*interface_methods)
|
|
10
|
+
exports(*interface_methods)
|
|
234
11
|
end
|
|
12
|
+
end
|
|
235
13
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const_value = const_get(const_name)
|
|
241
|
-
if task_class?(const_value)
|
|
242
|
-
task_classes << const_value
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
task_classes
|
|
14
|
+
def run
|
|
15
|
+
implementation_class = impl
|
|
16
|
+
unless implementation_class
|
|
17
|
+
raise "Section #{self.class} does not have an implementation. Override 'impl' method."
|
|
246
18
|
end
|
|
247
19
|
|
|
248
|
-
|
|
249
|
-
def extract_implementation_name(impl_class)
|
|
250
|
-
class_name = impl_class.name
|
|
251
|
-
return impl_class.to_s unless class_name&.include?("::")
|
|
20
|
+
apply_interface_to_implementation(implementation_class)
|
|
252
21
|
|
|
253
|
-
|
|
22
|
+
self.class.exported_methods.each do |method|
|
|
23
|
+
value = implementation_class.public_send(method)
|
|
24
|
+
instance_variable_set("@#{method}", value)
|
|
254
25
|
end
|
|
26
|
+
end
|
|
255
27
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
28
|
+
# @return [Class] The implementation task class
|
|
29
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
30
|
+
def impl
|
|
31
|
+
raise NotImplementedError, "Subclasses must implement the impl method to return implementation class"
|
|
32
|
+
end
|
|
260
33
|
|
|
261
|
-
|
|
34
|
+
private
|
|
262
35
|
|
|
263
|
-
|
|
264
|
-
|
|
36
|
+
# @param implementation_class [Class] The implementation task class
|
|
37
|
+
def apply_interface_to_implementation(implementation_class)
|
|
38
|
+
interface_methods = self.class.exported_methods
|
|
39
|
+
return if interface_methods.empty?
|
|
265
40
|
|
|
266
|
-
|
|
267
|
-
def impl
|
|
268
|
-
raise NotImplementedError, "Subclass must implement impl"
|
|
269
|
-
end
|
|
41
|
+
implementation_class.exports(*interface_methods)
|
|
270
42
|
end
|
|
271
43
|
end
|
|
272
44
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "visitor"
|
|
5
|
+
|
|
6
|
+
module Taski
|
|
7
|
+
module StaticAnalysis
|
|
8
|
+
class Analyzer
|
|
9
|
+
# @param task_class [Class] The task class to analyze
|
|
10
|
+
# @return [Set<Class>] Set of task classes that are dependencies
|
|
11
|
+
def self.analyze(task_class)
|
|
12
|
+
source_location = extract_run_method_location(task_class)
|
|
13
|
+
return Set.new unless source_location
|
|
14
|
+
|
|
15
|
+
file_path, _line_number = source_location
|
|
16
|
+
parse_result = Prism.parse_file(file_path)
|
|
17
|
+
|
|
18
|
+
visitor = Visitor.new(task_class)
|
|
19
|
+
visitor.visit(parse_result.value)
|
|
20
|
+
visitor.dependencies
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param task_class [Class] The task class
|
|
24
|
+
# @return [Array<String, Integer>, nil] File path and line number, or nil
|
|
25
|
+
def self.extract_run_method_location(task_class)
|
|
26
|
+
task_class.instance_method(:run).source_location
|
|
27
|
+
rescue NameError
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private_class_method :extract_run_method_location
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|