taski 0.7.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ca85800083318a5bc4d40151f14e5c55d6474a81a89a095de6d2433a8609043
4
- data.tar.gz: d68b0a3f9bf5bab689882df7cdf22fe02a169008a3d4bd64a24eaea2699dce33
3
+ metadata.gz: f7677c0f3995a7c61b5850fb25c82c60626287584e5c7f918939ededc045e9ea
4
+ data.tar.gz: d470969c88a4df51503487aacdf2c6261b65e6ed75ee3c7cc2ff744dfd750365
5
5
  SHA512:
6
- metadata.gz: 3705a29cdf6e5fbe4a244b9431c35bc326e2c383f71e00fe7223a1e87f98728e565e1faebbd081111ab8127197e39294c89d05c6e35511b8405425f21b6a8af3
7
- data.tar.gz: 87d334c0e053bbd164c260e304f642a26305f14021de17bf10bb8d50180fd0160338efbca0821a3fc3af996b8212e3a10c52d5e533c54b6141b069e17f21af7a
6
+ metadata.gz: 5f93d9e7d10f7104851266f0252ec4365d445a90d3a25b885f289df5d8caafbe9f9250ff9236e4cca0791749100c83c46735a8e98e5c1b675662199ba7a0e547
7
+ data.tar.gz: eb4fb9f806cdd5097e0d3321d4cda9e3df183d4850c4aa8161ba09b71666444166f05b5e795b330227076d85f7410adbe2fc4f1e2b19c66d007ff1632d354f97
data/CHANGELOG.md CHANGED
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2026-01-23
11
+
12
+ ### Added
13
+ - `Taski::Env` class for system-managed execution environment information ([#125](https://github.com/ahogappa/taski/pull/125))
14
+ - Access via `Taski.env.working_directory`, `Taski.env.started_at`, `Taski.env.root_task`
15
+ - `args` and `workers` parameters to `Task.new` for direct task instantiation ([#125](https://github.com/ahogappa/taski/pull/125))
16
+ - `mock_env` helper in `TestHelper` for mocking environment in tests ([#125](https://github.com/ahogappa/taski/pull/125))
17
+
18
+ ### Changed
19
+ - Separate system attributes from `Taski.args` to `Taski.env` ([#125](https://github.com/ahogappa/taski/pull/125))
20
+ - `Taski.args` now holds only user-defined options passed via `run(args: {...})`
21
+ - `Taski.env` holds system-managed execution environment (`root_task`, `started_at`, `working_directory`)
22
+
23
+ ## [0.7.1] - 2026-01-22
24
+
25
+ ### Added
26
+ - `Taski::TestHelper` module for mocking task dependencies in unit tests ([#123](https://github.com/ahogappa/taski/pull/123))
27
+ - `mock_task(TaskClass, key: value)` to mock exported values without running tasks
28
+ - `assert_task_accessed` / `refute_task_accessed` for verifying dependency access
29
+ - Support for both Minitest and RSpec test frameworks
30
+ - Simple one-line progress display mode (`Taski.progress_mode = :simple`) as an alternative to tree display ([#112](https://github.com/ahogappa/taski/pull/112))
31
+ - Configure via `TASKI_PROGRESS_MODE` environment variable or `Taski.progress_mode` API
32
+ - Display captured task output (up to 30 lines) in AggregateError messages for better debugging ([#109](https://github.com/ahogappa/taski/pull/109))
33
+ - Background polling thread in TaskOutputRouter to ensure pipes are drained reliably ([#122](https://github.com/ahogappa/taski/pull/122))
34
+ - `Taski.with_args` helper method for safe argument lifecycle management ([#110](https://github.com/ahogappa/taski/pull/110))
35
+
36
+ ### Changed
37
+ - Progress display now uses alternate screen buffer and shows summary line after completion ([#107](https://github.com/ahogappa/taski/pull/107))
38
+ - Eliminate screen flickering in tree progress display with in-place overwrite rendering ([#121](https://github.com/ahogappa/taski/pull/121))
39
+ - Extract `BaseProgressDisplay` class for shared progress display functionality ([#117](https://github.com/ahogappa/taski/pull/117))
40
+
41
+ ### Fixed
42
+ - Wait for running dependencies in nested executor to prevent deadlock ([#106](https://github.com/ahogappa/taski/pull/106))
43
+ - Preserve namespace path when following method calls in static analysis ([#108](https://github.com/ahogappa/taski/pull/108))
44
+ - Prevent race condition in `Taski.args` lifecycle during concurrent execution ([#110](https://github.com/ahogappa/taski/pull/110))
45
+ - Ensure progress display cleanup on interrupt (Ctrl+C) ([#107](https://github.com/ahogappa/taski/pull/107))
46
+ - Always enable output in PlainProgressDisplay ([#117](https://github.com/ahogappa/taski/pull/117))
47
+
10
48
  ## [0.7.0] - 2025-12-23
11
49
 
12
50
  ### 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."
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
@@ -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.0"
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,7 @@ module Taski
138
139
  end
139
140
 
140
141
  @args_monitor = Monitor.new
142
+ @env_monitor = Monitor.new
141
143
 
142
144
  # Get the current runtime arguments
143
145
  # @return [Args, nil] The current args or nil if no task is running
@@ -145,13 +147,50 @@ module Taski
145
147
  @args_monitor.synchronize { @args }
146
148
  end
147
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
+
148
187
  # Start new runtime arguments (internal use only)
149
188
  # @api private
150
189
  # @return [Boolean] true if this call created the args, false if args already existed
151
- def self.start_args(options:, root_task:)
190
+ def self.start_args(options:)
152
191
  @args_monitor.synchronize do
153
192
  return false if @args
154
- @args = Args.new(options: options, root_task: root_task)
193
+ @args = Args.new(options: options)
155
194
  true
156
195
  end
157
196
  end
@@ -167,11 +206,10 @@ module Taski
167
206
  # This prevents race conditions in concurrent execution.
168
207
  #
169
208
  # @param options [Hash] User-defined options
170
- # @param root_task [Class] The root task class
171
209
  # @yield The block to execute with args available
172
210
  # @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)
211
+ def self.with_args(options:)
212
+ created_args = start_args(options: options)
175
213
  yield
176
214
  ensure
177
215
  reset_args! if created_args
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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ahogappa
@@ -72,6 +72,7 @@ files:
72
72
  - examples/tree_progress_demo.rb
73
73
  - lib/taski.rb
74
74
  - lib/taski/args.rb
75
+ - lib/taski/env.rb
75
76
  - lib/taski/execution/base_progress_display.rb
76
77
  - lib/taski/execution/execution_context.rb
77
78
  - lib/taski/execution/executor.rb