taski 0.7.1 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ca85800083318a5bc4d40151f14e5c55d6474a81a89a095de6d2433a8609043
4
- data.tar.gz: d68b0a3f9bf5bab689882df7cdf22fe02a169008a3d4bd64a24eaea2699dce33
3
+ metadata.gz: 7c841d7a7a8e632045f42b090be2c6b10a9ef1510c9b9298bc50737f89486f28
4
+ data.tar.gz: 540f4595d667f191ac2a36464529e324fd56c5a5466dbcc2580b5227030501b5
5
5
  SHA512:
6
- metadata.gz: 3705a29cdf6e5fbe4a244b9431c35bc326e2c383f71e00fe7223a1e87f98728e565e1faebbd081111ab8127197e39294c89d05c6e35511b8405425f21b6a8af3
7
- data.tar.gz: 87d334c0e053bbd164c260e304f642a26305f14021de17bf10bb8d50180fd0160338efbca0821a3fc3af996b8212e3a10c52d5e533c54b6141b069e17f21af7a
6
+ metadata.gz: c4b2064e55edde2f504fd8c857efff572faf6de8466983c0081c2ca902c63784c75d121c482691cf24e1b8f80a863c97c2268e358271692967adc5612933da28
7
+ data.tar.gz: b56270d6324127635290a906bb6d7361a8c9609ad071ea307b26dcd3b16489c073be909e744680b52a202ab99019c611d9a0112cf2b18fc180f8113fb3fcef5a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.1] - 2026-01-26
11
+
12
+ ### Added
13
+ - `Taski.message` API for user-facing output during task execution ([#129](https://github.com/ahogappa/taski/pull/129))
14
+
15
+ ### Fixed
16
+ - Count unselected section candidates as completed in SimpleProgressDisplay ([#128](https://github.com/ahogappa/taski/pull/128))
17
+ - Prioritize environment variable over code settings for progress_mode ([#127](https://github.com/ahogappa/taski/pull/127))
18
+
19
+ ## [0.8.0] - 2026-01-23
20
+
21
+ ### Added
22
+ - `Taski::Env` class for system-managed execution environment information ([#125](https://github.com/ahogappa/taski/pull/125))
23
+ - Access via `Taski.env.working_directory`, `Taski.env.started_at`, `Taski.env.root_task`
24
+ - `args` and `workers` parameters to `Task.new` for direct task instantiation ([#125](https://github.com/ahogappa/taski/pull/125))
25
+ - `mock_env` helper in `TestHelper` for mocking environment in tests ([#125](https://github.com/ahogappa/taski/pull/125))
26
+
27
+ ### Changed
28
+ - Separate system attributes from `Taski.args` to `Taski.env` ([#125](https://github.com/ahogappa/taski/pull/125))
29
+ - `Taski.args` now holds only user-defined options passed via `run(args: {...})`
30
+ - `Taski.env` holds system-managed execution environment (`root_task`, `started_at`, `working_directory`)
31
+
32
+ ## [0.7.1] - 2026-01-22
33
+
34
+ ### Added
35
+ - `Taski::TestHelper` module for mocking task dependencies in unit tests ([#123](https://github.com/ahogappa/taski/pull/123))
36
+ - `mock_task(TaskClass, key: value)` to mock exported values without running tasks
37
+ - `assert_task_accessed` / `refute_task_accessed` for verifying dependency access
38
+ - Support for both Minitest and RSpec test frameworks
39
+ - Simple one-line progress display mode (`Taski.progress_mode = :simple`) as an alternative to tree display ([#112](https://github.com/ahogappa/taski/pull/112))
40
+ - Configure via `TASKI_PROGRESS_MODE` environment variable or `Taski.progress_mode` API
41
+ - Display captured task output (up to 30 lines) in AggregateError messages for better debugging ([#109](https://github.com/ahogappa/taski/pull/109))
42
+ - Background polling thread in TaskOutputRouter to ensure pipes are drained reliably ([#122](https://github.com/ahogappa/taski/pull/122))
43
+ - `Taski.with_args` helper method for safe argument lifecycle management ([#110](https://github.com/ahogappa/taski/pull/110))
44
+
45
+ ### Changed
46
+ - Progress display now uses alternate screen buffer and shows summary line after completion ([#107](https://github.com/ahogappa/taski/pull/107))
47
+ - Eliminate screen flickering in tree progress display with in-place overwrite rendering ([#121](https://github.com/ahogappa/taski/pull/121))
48
+ - Extract `BaseProgressDisplay` class for shared progress display functionality ([#117](https://github.com/ahogappa/taski/pull/117))
49
+
50
+ ### Fixed
51
+ - Wait for running dependencies in nested executor to prevent deadlock ([#106](https://github.com/ahogappa/taski/pull/106))
52
+ - Preserve namespace path when following method calls in static analysis ([#108](https://github.com/ahogappa/taski/pull/108))
53
+ - Prevent race condition in `Taski.args` lifecycle during concurrent execution ([#110](https://github.com/ahogappa/taski/pull/110))
54
+ - Ensure progress display cleanup on interrupt (Ctrl+C) ([#107](https://github.com/ahogappa/taski/pull/107))
55
+ - Always enable output in PlainProgressDisplay ([#117](https://github.com/ahogappa/taski/pull/117))
56
+
10
57
  ## [0.7.0] - 2025-12-23
11
58
 
12
59
  ### Added
data/README.md CHANGED
@@ -203,14 +203,14 @@ Pass custom options and access execution context from any task:
203
203
  ```ruby
204
204
  class DeployTask < Taski::Task
205
205
  def run
206
- # User-defined options
206
+ # User-defined options (Taski.args)
207
207
  env = Taski.args[:env]
208
208
  debug = Taski.args.fetch(:debug, false)
209
209
 
210
- # Runtime information
211
- puts "Working directory: #{Taski.args.working_directory}"
212
- puts "Started at: #{Taski.args.started_at}"
213
- puts "Root task: #{Taski.args.root_task}"
210
+ # Runtime environment information (Taski.env)
211
+ puts "Working directory: #{Taski.env.working_directory}"
212
+ puts "Started at: #{Taski.env.started_at}"
213
+ puts "Root task: #{Taski.env.root_task}"
214
214
  puts "Deploying to: #{env}"
215
215
  end
216
216
  end
@@ -219,13 +219,15 @@ end
219
219
  DeployTask.run(args: { env: "production", debug: true })
220
220
  ```
221
221
 
222
- Args API:
222
+ Args API (user-defined options):
223
223
  - `Taski.args[:key]` - Get option value (nil if not set)
224
224
  - `Taski.args.fetch(:key, default)` - Get with default value
225
225
  - `Taski.args.key?(:key)` - Check if option exists
226
- - `Taski.args.working_directory` - Execution directory
227
- - `Taski.args.started_at` - Execution start time
228
- - `Taski.args.root_task` - First task class called
226
+
227
+ Env API (execution environment):
228
+ - `Taski.env.working_directory` - Execution directory
229
+ - `Taski.env.started_at` - Execution start time
230
+ - `Taski.env.root_task` - First task class called
229
231
 
230
232
  ### Execution Model
231
233
 
data/examples/README.md CHANGED
@@ -46,9 +46,9 @@ ruby examples/args_demo.rb
46
46
  - User-defined options via `run(args: {...})`
47
47
  - `Taski.args[:key]` for option access
48
48
  - `Taski.args.fetch(:key, default)` for defaults
49
- - `Taski.args.working_directory`
50
- - `Taski.args.started_at`
51
- - `Taski.args.root_task`
49
+ - `Taski.env.working_directory`
50
+ - `Taski.env.started_at`
51
+ - `Taski.env.root_task`
52
52
 
53
53
  ---
54
54
 
@@ -1,35 +1,36 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Taski Args API Example
4
+ # Taski Args & Env API Example
5
5
  #
6
- # This example demonstrates the Args API for accessing runtime information:
7
- # - working_directory: Where execution started
8
- # - started_at: When execution began
9
- # - root_task: The first task class that was called
10
- # - User-defined options: Custom values passed via run(args: {...})
6
+ # This example demonstrates the Args and Env APIs:
7
+ # - Taski.env: Execution environment information
8
+ # - working_directory: Where execution started
9
+ # - started_at: When execution began
10
+ # - root_task: The first task class that was called
11
+ # - Taski.args: User-defined options passed via run(args: {...})
11
12
  #
12
13
  # Run: ruby examples/args_demo.rb
13
14
 
14
15
  require_relative "../lib/taski"
15
16
 
16
- puts "Taski Args API Example"
17
+ puts "Taski Args & Env API Example"
17
18
  puts "=" * 40
18
19
 
19
- # Task that uses args information for logging
20
+ # Task that uses env information for logging
20
21
  class SetupTask < Taski::Task
21
22
  exports :setup_info
22
23
 
23
24
  def run
24
25
  puts "Setup running..."
25
- puts " Working directory: #{Taski.args.working_directory}"
26
- puts " Started at: #{Taski.args.started_at}"
27
- puts " Root task: #{Taski.args.root_task}"
26
+ puts " Working directory: #{Taski.env.working_directory}"
27
+ puts " Started at: #{Taski.env.started_at}"
28
+ puts " Root task: #{Taski.env.root_task}"
28
29
  puts " Environment: #{Taski.args[:env]}"
29
30
 
30
31
  @setup_info = {
31
- directory: Taski.args.working_directory,
32
- timestamp: Taski.args.started_at,
32
+ directory: Taski.env.working_directory,
33
+ timestamp: Taski.env.started_at,
33
34
  env: Taski.args[:env]
34
35
  }
35
36
  end
@@ -40,8 +41,8 @@ class FileProcessor < Taski::Task
40
41
  exports :output_path
41
42
 
42
43
  def run
43
- # Use args to determine output location
44
- base_dir = Taski.args.working_directory
44
+ # Use env to determine output location
45
+ base_dir = Taski.env.working_directory
45
46
  env = Taski.args.fetch(:env, "development")
46
47
  @output_path = File.join(base_dir, "tmp", env, "output.txt")
47
48
 
@@ -55,7 +56,7 @@ class TimingTask < Taski::Task
55
56
  exports :duration_info
56
57
 
57
58
  def run
58
- start_time = Taski.args.started_at
59
+ start_time = Taski.env.started_at
59
60
  current_time = Time.now
60
61
  elapsed = current_time - start_time
61
62
 
@@ -76,7 +77,7 @@ class MainTask < Taski::Task
76
77
 
77
78
  def run
78
79
  puts "\nMainTask executing..."
79
- puts " Root task is: #{Taski.args.root_task}"
80
+ puts " Root task is: #{Taski.env.root_task}"
80
81
  puts " Environment: #{Taski.args[:env]}"
81
82
 
82
83
  # Access dependencies
@@ -88,7 +89,7 @@ class MainTask < Taski::Task
88
89
  setup: setup,
89
90
  output_path: output,
90
91
  timing: timing,
91
- root_task: Taski.args.root_task.to_s
92
+ root_task: Taski.env.root_task.to_s
92
93
  }
93
94
 
94
95
  puts "\nExecution Summary:"
@@ -114,5 +115,5 @@ puts "-" * 40
114
115
  puts MainTask.tree
115
116
 
116
117
  puts "\n" + "=" * 40
117
- puts "Args API demonstration complete!"
118
- puts "Note: Args provides runtime information and user options without affecting dependency analysis."
118
+ puts "Args & Env API demonstration complete!"
119
+ puts "Note: Args provides user options, Env provides execution environment info."
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Demonstrates Taski.message API
5
+ #
6
+ # Taski.message outputs text to the user without being captured by TaskOutputRouter.
7
+ # Messages are queued during progress display and shown after task completion.
8
+ #
9
+ # Usage:
10
+ # ruby examples/message_demo.rb
11
+ # TASKI_FORCE_PROGRESS=1 ruby examples/message_demo.rb # Force progress display
12
+
13
+ require_relative "../lib/taski"
14
+
15
+ class ProcessDataTask < Taski::Task
16
+ exports :processed_count
17
+
18
+ def run
19
+ puts "Starting data processing..." # Captured by TaskOutputRouter
20
+
21
+ # Simulate processing
22
+ 5.times do |i|
23
+ puts "Processing batch #{i + 1}/5..." # Captured
24
+ sleep 0.3
25
+ end
26
+
27
+ @processed_count = 42
28
+
29
+ # These messages bypass TaskOutputRouter and appear after execution
30
+ Taski.message("Created: /tmp/output.txt")
31
+ Taski.message("Summary: #{@processed_count} items processed successfully")
32
+ end
33
+ end
34
+
35
+ class GenerateReportTask < Taski::Task
36
+ exports :report_path
37
+
38
+ def run
39
+ # Dependency: ProcessDataTask will be executed first
40
+ count = ProcessDataTask.processed_count
41
+ puts "Generating report for #{count} items..." # Captured
42
+
43
+ sleep 0.5
44
+
45
+ @report_path = "/tmp/report.pdf"
46
+
47
+ Taski.message("Report available at: #{@report_path}")
48
+ end
49
+ end
50
+
51
+ puts "=== Taski.message Demo ==="
52
+ puts
53
+
54
+ GenerateReportTask.run
55
+
56
+ puts
57
+ puts "=== Done ==="
data/lib/taski/args.rb CHANGED
@@ -4,18 +4,12 @@ require "monitor"
4
4
 
5
5
  module Taski
6
6
  # Runtime arguments accessible from any task.
7
- # Holds user-defined options and execution metadata.
7
+ # Holds user-defined options passed by the user at execution time.
8
8
  # Args is immutable after creation - options cannot be modified during task execution.
9
9
  class Args
10
- attr_reader :started_at, :working_directory, :root_task
11
-
12
10
  # @param options [Hash] User-defined options (immutable after creation)
13
- # @param root_task [Class] The root task class that initiated execution
14
- def initialize(options:, root_task:)
11
+ def initialize(options:)
15
12
  @options = options.dup.freeze
16
- @root_task = root_task
17
- @started_at = Time.now
18
- @working_directory = Dir.pwd
19
13
  end
20
14
 
21
15
  # Get a user-defined option value
data/lib/taski/env.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Taski
4
+ # Runtime execution environment information.
5
+ # Holds system-managed metadata that is set automatically during task execution.
6
+ # Env is immutable after creation.
7
+ class Env
8
+ attr_reader :root_task, :started_at, :working_directory
9
+
10
+ # @param root_task [Class] The root task class that initiated execution
11
+ def initialize(root_task:)
12
+ @root_task = root_task
13
+ @started_at = Time.now
14
+ @working_directory = Dir.pwd
15
+ end
16
+ end
17
+ end
@@ -81,6 +81,7 @@ module Taski
81
81
  @output_capture = nil
82
82
  @original_stdout = nil
83
83
  @runtime_dependencies = {}
84
+ @message_queue = []
84
85
  end
85
86
 
86
87
  # Check if output capture is already active.
@@ -89,6 +90,31 @@ module Taski
89
90
  @monitor.synchronize { !@output_capture.nil? }
90
91
  end
91
92
 
93
+ # Queue a message to be displayed after execution completes.
94
+ # Thread-safe for access from worker threads.
95
+ #
96
+ # @param text [String] The message text to queue
97
+ def queue_message(text)
98
+ @monitor.synchronize { @message_queue << text }
99
+ end
100
+
101
+ # Flush all queued messages to the given output.
102
+ # Clears the queue after flushing.
103
+ #
104
+ # @param output [IO] The output stream to write messages to
105
+ def flush_messages(output)
106
+ messages = @monitor.synchronize { @message_queue.dup.tap { @message_queue.clear } }
107
+ messages.each { |msg| output.puts(msg) }
108
+ end
109
+
110
+ # Get the original stdout before output capture was set up.
111
+ # Thread-safe accessor.
112
+ #
113
+ # @return [IO, nil] The original stdout or nil if not captured
114
+ def original_stdout
115
+ @monitor.synchronize { @original_stdout }
116
+ end
117
+
92
118
  # Set up output capture for inline progress display.
93
119
  # Creates TaskOutputRouter and replaces $stdout.
94
120
  # Should only be called when progress display is active and not already set up.
@@ -418,9 +418,17 @@ module Taski
418
418
  ensure
419
419
  stop_progress_display
420
420
  @saved_output_capture = @execution_context.output_capture
421
+ flush_queued_messages if should_teardown_capture
421
422
  teardown_output_capture if should_teardown_capture
422
423
  end
423
424
 
425
+ # Flush queued messages from ExecutionContext to original stdout.
426
+ # Called after progress display stops to show user messages.
427
+ def flush_queued_messages
428
+ output = @execution_context.original_stdout || $stdout
429
+ @execution_context.flush_messages(output)
430
+ end
431
+
424
432
  def create_default_execution_context
425
433
  context = ExecutionContext.new
426
434
  progress = Taski.progress_display
@@ -34,6 +34,7 @@ module Taski
34
34
  @spinner_index = 0
35
35
  @renderer_thread = nil
36
36
  @running = false
37
+ @section_candidates = {} # section_class => [candidate_classes]
37
38
  end
38
39
 
39
40
  protected
@@ -44,9 +45,18 @@ module Taski
44
45
  end
45
46
 
46
47
  # Template method: Called when a section impl is registered
47
- def on_section_impl_registered(_section_class, impl_class)
48
+ def on_section_impl_registered(section_class, impl_class)
48
49
  @tasks[impl_class] ||= TaskProgress.new
49
50
  @tasks[impl_class].is_impl_candidate = false
51
+
52
+ # Mark unselected candidates as completed (skipped)
53
+ candidates = @section_candidates[section_class] || []
54
+ candidates.each do |candidate|
55
+ next if candidate == impl_class
56
+ progress = @tasks[candidate]
57
+ next unless progress
58
+ progress.run_state = :completed
59
+ end
50
60
  end
51
61
 
52
62
  # Template method: Determine if display should activate
@@ -83,6 +93,23 @@ module Taski
83
93
  # Use TreeProgressDisplay's static method for tree building
84
94
  tree = TreeProgressDisplay.build_tree_node(@root_task_class)
85
95
  register_tasks_from_tree(tree)
96
+ collect_section_candidates(tree)
97
+ end
98
+
99
+ def collect_section_candidates(node)
100
+ return unless node
101
+
102
+ task_class = node[:task_class]
103
+
104
+ # If this is a section, collect its implementation candidates
105
+ if node[:is_section]
106
+ candidates = node[:children]
107
+ .select { |c| c[:is_impl_candidate] }
108
+ .map { |c| c[:task_class] }
109
+ @section_candidates[task_class] = candidates unless candidates.empty?
110
+ end
111
+
112
+ node[:children].each { |child| collect_section_candidates(child) }
86
113
  end
87
114
 
88
115
  def render_live
@@ -38,10 +38,12 @@ module Taski
38
38
  # @param [Object] task - The task instance being wrapped.
39
39
  # @param [Object] registry - The registry used to query abort status and coordinate execution.
40
40
  # @param [Object, nil] execution_context - Optional execution context used to trigger and report execution and cleanup.
41
- def initialize(task, registry:, execution_context: nil)
41
+ # @param [Hash, nil] args - User-defined arguments for Task.new usage.
42
+ def initialize(task, registry:, execution_context: nil, args: nil)
42
43
  @task = task
43
44
  @registry = registry
44
45
  @execution_context = execution_context
46
+ @args = args
45
47
  @result = nil
46
48
  @clean_result = nil
47
49
  @error = nil
@@ -261,10 +263,18 @@ module Taski
261
263
  ##
262
264
  # Ensures args are set during block execution, then resets if they weren't set before.
263
265
  # This allows Task.new.run usage without requiring explicit args setup.
266
+ # If args are already set (e.g., from Task.run class method), just yields the block.
267
+ # Uses stored @args if set (from Task.new), otherwise uses empty hash.
264
268
  # @yield The block to execute with args lifecycle management
265
269
  # @return [Object] The result of the block
266
270
  def with_args_lifecycle(&block)
267
- Taski.with_args(options: {}, root_task: @task.class, &block)
271
+ # If args are already set, just execute the block
272
+ return yield if Taski.args
273
+
274
+ options = @args || {}
275
+ Taski.send(:with_env, root_task: @task.class) do
276
+ Taski.send(:with_args, options: options, &block)
277
+ end
268
278
  end
269
279
 
270
280
  ##
data/lib/taski/task.rb CHANGED
@@ -50,11 +50,24 @@ module Taski
50
50
  end
51
51
 
52
52
  ##
53
- # Creates a fresh TaskWrapper instance for re-execution support.
54
- # Use class methods (e.g., MyTask.result) for cached single execution.
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.
55
57
  # @return [Execution::TaskWrapper] A new wrapper for this task.
56
- def new
57
- fresh_wrapper
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
58
71
  end
59
72
 
60
73
  ##
@@ -138,9 +151,11 @@ module Taski
138
151
  # @return [Object] The result of the block
139
152
  def with_execution_setup(args:, workers:)
140
153
  validate_workers!(workers)
141
- Taski.with_args(options: args.merge(_workers: workers), root_task: self) do
142
- validate_no_circular_dependencies!
143
- yield fresh_wrapper
154
+ Taski.send(:with_env, root_task: self) do
155
+ Taski.send(:with_args, options: args.merge(_workers: workers)) do
156
+ validate_no_circular_dependencies!
157
+ yield fresh_wrapper
158
+ end
144
159
  end
145
160
  end
146
161
 
@@ -199,9 +214,11 @@ module Taski
199
214
  wrapper.get_exported_value(method)
200
215
  else
201
216
  # Outside execution - fresh execution (top-level call)
202
- Taski.with_args(options: {}, root_task: self) do
203
- validate_no_circular_dependencies!
204
- fresh_wrapper.get_exported_value(method)
217
+ Taski.send(:with_env, root_task: self) do
218
+ Taski.send(:with_args, options: {}) do
219
+ validate_no_circular_dependencies!
220
+ fresh_wrapper.get_exported_value(method)
221
+ end
205
222
  end
206
223
  end
207
224
  end
@@ -38,12 +38,14 @@ module Taski
38
38
  end
39
39
  end
40
40
 
41
- # Clears all registered mocks.
41
+ # Clears all registered mocks and resets args/env.
42
42
  # Should be called in test setup/teardown.
43
43
  def reset!
44
44
  @mutex.synchronize do
45
45
  @mocks = {}
46
46
  end
47
+ Taski.reset_args!
48
+ Taski.reset_env!
47
49
  end
48
50
  end
49
51
  end
@@ -43,9 +43,11 @@ module Taski
43
43
  end
44
44
  wrapper.get_exported_value(method)
45
45
  else
46
- Taski.with_args(options: {}, root_task: self) do
47
- validate_no_circular_dependencies!
48
- fresh_wrapper.get_exported_value(method)
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
49
51
  end
50
52
  end
51
53
  end
@@ -110,6 +112,36 @@ module Taski
110
112
  end
111
113
  end
112
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
+
113
145
  # Registers a mock for a task class with specified return values.
114
146
  # @param task_class [Class] A Taski::Task or Taski::Section subclass
115
147
  # @param values [Hash{Symbol => Object}] Method names mapped to return values
data/lib/taski/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Taski
4
- VERSION = "0.7.1"
4
+ VERSION = "0.8.1"
5
5
  end
data/lib/taski.rb CHANGED
@@ -14,6 +14,7 @@ require_relative "taski/execution/tree_progress_display"
14
14
  require_relative "taski/execution/simple_progress_display"
15
15
  require_relative "taski/execution/plain_progress_display"
16
16
  require_relative "taski/args"
17
+ require_relative "taski/env"
17
18
 
18
19
  module Taski
19
20
  class TaskAbortException < StandardError
@@ -138,6 +139,8 @@ module Taski
138
139
  end
139
140
 
140
141
  @args_monitor = Monitor.new
142
+ @env_monitor = Monitor.new
143
+ @message_monitor = Monitor.new
141
144
 
142
145
  # Get the current runtime arguments
143
146
  # @return [Args, nil] The current args or nil if no task is running
@@ -145,13 +148,67 @@ module Taski
145
148
  @args_monitor.synchronize { @args }
146
149
  end
147
150
 
151
+ # Get the current execution environment
152
+ # @return [Env, nil] The current env or nil if no task is running
153
+ def self.env
154
+ @env_monitor.synchronize { @env }
155
+ end
156
+
157
+ # Output a message to the user without being captured by TaskOutputRouter.
158
+ # During task execution with progress display, messages are queued and
159
+ # displayed after execution completes. Without progress display or outside
160
+ # task execution, messages are output immediately.
161
+ #
162
+ # @param text [String] The message text to display
163
+ def self.message(text)
164
+ @message_monitor.synchronize do
165
+ context = Execution::ExecutionContext.current
166
+ if context&.output_capture_active?
167
+ context.queue_message(text)
168
+ else
169
+ $stdout.puts(text)
170
+ end
171
+ end
172
+ end
173
+
174
+ # Start new execution environment (internal use only)
175
+ # @api private
176
+ # @return [Boolean] true if this call created the env, false if env already existed
177
+ def self.start_env(root_task:)
178
+ @env_monitor.synchronize do
179
+ return false if @env
180
+ @env = Env.new(root_task: root_task)
181
+ true
182
+ end
183
+ end
184
+
185
+ # Reset the execution environment (internal use only)
186
+ # @api private
187
+ def self.reset_env!
188
+ @env_monitor.synchronize { @env = nil }
189
+ end
190
+
191
+ # Execute a block with env lifecycle management.
192
+ # Creates env if it doesn't exist, and resets it only if this call created it.
193
+ # This prevents race conditions in concurrent execution.
194
+ #
195
+ # @param root_task [Class] The root task class
196
+ # @yield The block to execute with env available
197
+ # @return [Object] The result of the block
198
+ def self.with_env(root_task:)
199
+ created_env = start_env(root_task: root_task)
200
+ yield
201
+ ensure
202
+ reset_env! if created_env
203
+ end
204
+
148
205
  # Start new runtime arguments (internal use only)
149
206
  # @api private
150
207
  # @return [Boolean] true if this call created the args, false if args already existed
151
- def self.start_args(options:, root_task:)
208
+ def self.start_args(options:)
152
209
  @args_monitor.synchronize do
153
210
  return false if @args
154
- @args = Args.new(options: options, root_task: root_task)
211
+ @args = Args.new(options: options)
155
212
  true
156
213
  end
157
214
  end
@@ -167,11 +224,10 @@ module Taski
167
224
  # This prevents race conditions in concurrent execution.
168
225
  #
169
226
  # @param options [Hash] User-defined options
170
- # @param root_task [Class] The root task class
171
227
  # @yield The block to execute with args available
172
228
  # @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)
229
+ def self.with_args(options:)
230
+ created_args = start_args(options: options)
175
231
  yield
176
232
  ensure
177
233
  reset_args! if created_args
@@ -191,9 +247,14 @@ module Taski
191
247
  end
192
248
 
193
249
  # Get the current progress mode (:tree or :simple)
250
+ # Environment variable TASKI_PROGRESS_MODE takes precedence over code settings.
194
251
  # @return [Symbol] The current progress mode
195
252
  def self.progress_mode
196
- @progress_mode || progress_mode_from_env
253
+ if ENV["TASKI_PROGRESS_MODE"]
254
+ progress_mode_from_env
255
+ else
256
+ @progress_mode || :tree
257
+ end
197
258
  end
198
259
 
199
260
  # Set the progress mode (:tree or :simple)
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.7.1
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ahogappa
@@ -62,6 +62,7 @@ files:
62
62
  - examples/data_pipeline_demo.rb
63
63
  - examples/group_demo.rb
64
64
  - examples/large_tree_demo.rb
65
+ - examples/message_demo.rb
65
66
  - examples/nested_section_demo.rb
66
67
  - examples/parallel_progress_demo.rb
67
68
  - examples/quick_start.rb
@@ -72,6 +73,7 @@ files:
72
73
  - examples/tree_progress_demo.rb
73
74
  - lib/taski.rb
74
75
  - lib/taski/args.rb
76
+ - lib/taski/env.rb
75
77
  - lib/taski/execution/base_progress_display.rb
76
78
  - lib/taski/execution/execution_context.rb
77
79
  - lib/taski/execution/executor.rb
@@ -119,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
121
  - !ruby/object:Gem::Version
120
122
  version: '0'
121
123
  requirements: []
122
- rubygems_version: 4.0.3
124
+ rubygems_version: 4.0.4
123
125
  specification_version: 4
124
126
  summary: A simple yet powerful Ruby task runner with static dependency resolution
125
127
  (in development).