taski 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "monitor"
4
3
  require "stringio"
4
+ require_relative "base_progress_display"
5
5
 
6
6
  module Taski
7
7
  module Execution
8
8
  # Tree-based progress display that shows task execution in a tree structure
9
9
  # similar to Task.tree, with real-time status updates and stdout capture.
10
- class TreeProgressDisplay
10
+ class TreeProgressDisplay < BaseProgressDisplay
11
11
  SPINNER_FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
12
12
 
13
+ # Output display settings
14
+ OUTPUT_RESERVED_WIDTH = 30 # Characters reserved for tree structure
15
+ OUTPUT_MIN_LENGTH = 70 # Minimum visible output length
16
+ OUTPUT_SEPARATOR = " > " # Separator before task output
17
+ GROUP_SEPARATOR = " | " # Separator between group name and task name
18
+ TRUNCATION_ELLIPSIS = "..." # Ellipsis for truncated output
19
+
20
+ # Display settings
21
+ RENDER_INTERVAL = 0.1 # Seconds between display updates
22
+ DEFAULT_TERMINAL_WIDTH = 80 # Default terminal width when unknown
23
+ DEFAULT_TERMINAL_HEIGHT = 24 # Default terminal height when unknown
24
+
13
25
  # ANSI color codes (matching Task.tree)
14
26
  COLORS = {
15
27
  reset: "\e[0m",
@@ -169,264 +181,54 @@ module Taski
169
181
  end
170
182
  end
171
183
 
172
- # Tracks the progress of a group within a task
173
- class GroupProgress
174
- attr_accessor :name, :state, :start_time, :end_time, :duration, :error, :last_message
175
-
176
- def initialize(name)
177
- @name = name
178
- @state = :pending
179
- @start_time = nil
180
- @end_time = nil
181
- @duration = nil
182
- @error = nil
183
- @last_message = nil
184
- end
185
- end
186
-
187
- class TaskProgress
188
- # Run lifecycle tracking
189
- attr_accessor :run_state, :run_start_time, :run_end_time, :run_error, :run_duration
190
- # Clean lifecycle tracking
191
- attr_accessor :clean_state, :clean_start_time, :clean_end_time, :clean_error, :clean_duration
192
- # Display properties
193
- attr_accessor :is_impl_candidate
194
- # Group tracking
195
- attr_accessor :groups, :current_group_index
196
-
197
- def initialize
198
- # Run lifecycle
199
- @run_state = :pending
200
- @run_start_time = nil
201
- @run_end_time = nil
202
- @run_error = nil
203
- @run_duration = nil
204
- # Clean lifecycle
205
- @clean_state = nil # nil means clean hasn't started
206
- @clean_start_time = nil
207
- @clean_end_time = nil
208
- @clean_error = nil
209
- @clean_duration = nil
210
- # Display
211
- @is_impl_candidate = false
212
- # Groups
213
- @groups = []
214
- @current_group_index = nil
215
- end
216
-
217
- # For backward compatibility - returns the most relevant state for display
218
- def state
219
- @clean_state || @run_state
220
- end
221
-
222
- # Legacy accessors for backward compatibility
223
- def start_time
224
- @clean_start_time || @run_start_time
225
- end
226
-
227
- def end_time
228
- @clean_end_time || @run_end_time
229
- end
230
-
231
- def error
232
- @clean_error || @run_error
233
- end
234
-
235
- def duration
236
- @clean_duration || @run_duration
237
- end
238
- end
239
-
240
184
  def initialize(output: $stdout)
241
- @output = output
242
- @tasks = {}
243
- @monitor = Monitor.new
185
+ super
244
186
  @spinner_index = 0
245
187
  @renderer_thread = nil
246
188
  @running = false
247
- @nest_level = 0 # Track nested executor calls
248
- @root_task_class = nil
249
189
  @tree_structure = nil
250
190
  @section_impl_map = {} # Section -> selected impl class
251
- @output_capture = nil # ThreadOutputCapture for getting task output
191
+ @last_line_count = 0 # Track number of lines drawn for cursor movement
252
192
  end
253
193
 
254
- # Set the output capture for getting task output
255
- # @param capture [ThreadOutputCapture] The output capture instance
256
- def set_output_capture(capture)
257
- @monitor.synchronize do
258
- @output_capture = capture
259
- end
260
- end
194
+ protected
261
195
 
262
- # Set the root task to build tree structure
263
- # Only sets root task if not already set (prevents nested executor overwrite)
264
- # @param root_task_class [Class] The root task class
265
- def set_root_task(root_task_class)
266
- @monitor.synchronize do
267
- return if @root_task_class # Don't overwrite existing root task
268
- @root_task_class = root_task_class
269
- build_tree_structure
270
- end
271
- end
272
-
273
- # Register which impl was selected for a section
274
- # @param section_class [Class] The section class
275
- # @param impl_class [Class] The selected implementation class
276
- def register_section_impl(section_class, impl_class)
277
- @monitor.synchronize do
278
- @section_impl_map[section_class] = impl_class
279
- end
280
- end
281
-
282
- # @param task_class [Class] The task class to register
283
- def register_task(task_class)
284
- @monitor.synchronize do
285
- return if @tasks.key?(task_class)
286
- @tasks[task_class] = TaskProgress.new
287
- end
196
+ # Template method: Called when root task is set
197
+ def on_root_task_set
198
+ build_tree_structure
288
199
  end
289
200
 
290
- # @param task_class [Class] The task class to check
291
- # @return [Boolean] true if the task is registered
292
- def task_registered?(task_class)
293
- @monitor.synchronize do
294
- @tasks.key?(task_class)
295
- end
201
+ # Template method: Called when a section impl is registered
202
+ def on_section_impl_registered(section_class, impl_class)
203
+ @section_impl_map[section_class] = impl_class
296
204
  end
297
205
 
298
- # @param task_class [Class] The task class to update
299
- # @param state [Symbol] The new state (:pending, :running, :completed, :failed, :cleaning, :clean_completed, :clean_failed)
300
- # @param duration [Float] Duration in milliseconds (for completed tasks)
301
- # @param error [Exception] Error object (for failed tasks)
302
- def update_task(task_class, state:, duration: nil, error: nil)
303
- @monitor.synchronize do
304
- progress = @tasks[task_class]
305
- return unless progress
306
-
307
- case state
308
- # Run lifecycle states
309
- when :pending
310
- progress.run_state = :pending
311
- when :running
312
- progress.run_state = :running
313
- progress.run_start_time = Time.now
314
- when :completed
315
- progress.run_state = :completed
316
- progress.run_end_time = Time.now
317
- progress.run_duration = duration if duration
318
- when :failed
319
- progress.run_state = :failed
320
- progress.run_end_time = Time.now
321
- progress.run_error = error if error
322
- # Clean lifecycle states
323
- when :cleaning
324
- progress.clean_state = :cleaning
325
- progress.clean_start_time = Time.now
326
- when :clean_completed
327
- progress.clean_state = :clean_completed
328
- progress.clean_end_time = Time.now
329
- progress.clean_duration = duration if duration
330
- when :clean_failed
331
- progress.clean_state = :clean_failed
332
- progress.clean_end_time = Time.now
333
- progress.clean_error = error if error
334
- end
335
- end
336
- end
337
-
338
- # @param task_class [Class] The task class
339
- # @return [Symbol] The task state
340
- def task_state(task_class)
341
- @monitor.synchronize do
342
- @tasks[task_class]&.state
343
- end
344
- end
345
-
346
- # Update group state for a task.
347
- # Called by ExecutionContext when group lifecycle events occur.
348
- #
349
- # @param task_class [Class] The task class containing the group
350
- # @param group_name [String] The name of the group
351
- # @param state [Symbol] The new state (:running, :completed, :failed)
352
- # @param duration [Float, nil] Duration in milliseconds (for completed groups)
353
- # @param error [Exception, nil] Error object (for failed groups)
354
- def update_group(task_class, group_name, state:, duration: nil, error: nil)
355
- @monitor.synchronize do
356
- progress = @tasks[task_class]
357
- return unless progress
358
-
359
- case state
360
- when :running
361
- # Create new group and set as current
362
- group = GroupProgress.new(group_name)
363
- group.state = :running
364
- group.start_time = Time.now
365
- progress.groups << group
366
- progress.current_group_index = progress.groups.size - 1
367
- when :completed
368
- # Find the group by name and mark completed
369
- group = progress.groups.find { |g| g.name == group_name && g.state == :running }
370
- if group
371
- group.state = :completed
372
- group.end_time = Time.now
373
- group.duration = duration
374
- end
375
- progress.current_group_index = nil
376
- when :failed
377
- # Find the group by name and mark failed
378
- group = progress.groups.find { |g| g.name == group_name && g.state == :running }
379
- if group
380
- group.state = :failed
381
- group.end_time = Time.now
382
- group.duration = duration
383
- group.error = error
384
- end
385
- progress.current_group_index = nil
386
- end
387
- end
206
+ # Template method: Determine if display should activate
207
+ def should_activate?
208
+ tty?
388
209
  end
389
210
 
390
- def start
391
- should_start = false
392
- @monitor.synchronize do
393
- @nest_level += 1
394
- return if @nest_level > 1 # Already running from outer executor
395
- return if @running
396
- return unless @output.tty?
397
-
398
- @running = true
399
- should_start = true
400
- end
401
-
402
- return unless should_start
403
-
404
- @output.print "\e[?25l" # Hide cursor
405
- @output.print "\e7" # Save cursor position (before any tree output)
211
+ # Template method: Called when display starts
212
+ def on_start
213
+ @running = true
214
+ @output.print "\e[?1049h" # Switch to alternate screen buffer
215
+ @output.print "\e[H" # Move cursor to home (top-left)
216
+ @output.print "\e[?25l" # Hide cursor
406
217
  @renderer_thread = Thread.new do
407
218
  loop do
408
219
  break unless @running
409
220
  render_live
410
- sleep 0.1
221
+ sleep RENDER_INTERVAL
411
222
  end
412
223
  end
413
224
  end
414
225
 
415
- def stop
416
- should_stop = false
417
- @monitor.synchronize do
418
- @nest_level -= 1 if @nest_level > 0
419
- return unless @nest_level == 0
420
- return unless @running
421
-
422
- @running = false
423
- should_stop = true
424
- end
425
-
426
- return unless should_stop
427
-
226
+ # Template method: Called when display stops
227
+ def on_stop
228
+ @running = false
428
229
  @renderer_thread&.join
429
- @output.print "\e[?25h" # Show cursor
230
+ @output.print "\e[?25h" # Show cursor
231
+ @output.print "\e[?1049l" # Switch back to main screen buffer
430
232
  render_final
431
233
  end
432
234
 
@@ -440,21 +242,6 @@ module Taski
440
242
  register_tasks_from_tree(@tree_structure)
441
243
  end
442
244
 
443
- # Register all tasks from tree structure
444
- def register_tasks_from_tree(node)
445
- return unless node
446
-
447
- task_class = node[:task_class]
448
- register_task(task_class)
449
-
450
- # Mark as impl candidate if applicable
451
- if node[:is_impl_candidate]
452
- @tasks[task_class].is_impl_candidate = true
453
- end
454
-
455
- node[:children].each { |child| register_tasks_from_tree(child) }
456
- end
457
-
458
245
  def render_live
459
246
  # Poll for new output from task pipes
460
247
  @output_capture&.poll
@@ -468,32 +255,82 @@ module Taski
468
255
 
469
256
  return if lines.nil? || lines.empty?
470
257
 
471
- # Restore cursor to saved position (from start) and clear
472
- @output.print "\e8" # Restore cursor position
473
- @output.print "\e[J" # Clear from cursor to end of screen
258
+ # Build complete frame in buffer for single write (flicker-free)
259
+ buffer = build_frame_buffer(lines)
260
+
261
+ # Write entire frame in single operation
262
+ @output.print buffer
263
+ @output.flush
264
+
265
+ @last_line_count = lines.size
266
+ end
474
267
 
475
- # Redraw all lines
268
+ ##
269
+ # Builds a complete frame buffer for flicker-free rendering.
270
+ # Uses cursor home positioning and line-by-line overwrite instead of clear.
271
+ # @param lines [Array<String>] The lines to render.
272
+ # @return [String] The complete frame buffer ready for single write.
273
+ def build_frame_buffer(lines)
274
+ buffer = +""
275
+
276
+ # Move cursor to home position (top-left) for overwrite
277
+ buffer << "\e[H"
278
+
279
+ # Build each line with clear-to-end-of-line for clean overwrite
476
280
  lines.each do |line|
477
- @output.print "#{line}\n"
281
+ buffer << line
282
+ buffer << "\e[K" # Clear from cursor to end of line (removes old content)
283
+ buffer << "\n"
478
284
  end
479
285
 
480
- @output.flush
286
+ # Clear any extra lines from previous render if current has fewer lines
287
+ if @last_line_count > lines.size
288
+ (@last_line_count - lines.size).times do
289
+ buffer << "\e[K\n" # Clear line and move to next
290
+ end
291
+ end
292
+
293
+ buffer
481
294
  end
482
295
 
483
296
  def render_final
484
297
  @monitor.synchronize do
485
- lines = build_tree_display
486
- return if lines.empty?
298
+ return unless @root_task_class
487
299
 
488
- # Restore cursor to saved position (from start) and clear
489
- @output.print "\e8" # Restore cursor position
490
- @output.print "\e[J" # Clear from cursor to end of screen
300
+ root_progress = @tasks[@root_task_class]
301
+ return unless root_progress
491
302
 
492
- # Print final state
493
- lines.each { |line| @output.puts line }
303
+ # Print single summary line instead of full tree
304
+ @output.puts build_summary_line(@root_task_class, root_progress)
494
305
  end
495
306
  end
496
307
 
308
+ def build_summary_line(task_class, progress)
309
+ # Determine overall status and icon
310
+ if progress.run_state == :failed || progress.clean_state == :clean_failed
311
+ icon = "#{COLORS[:error]}#{ICONS[:failed]}#{COLORS[:reset]}"
312
+ status = "#{COLORS[:error]}failed#{COLORS[:reset]}"
313
+ else
314
+ icon = "#{COLORS[:success]}#{ICONS[:completed]}#{COLORS[:reset]}"
315
+ status = "#{COLORS[:success]}completed#{COLORS[:reset]}"
316
+ end
317
+
318
+ name = "#{COLORS[:name]}#{task_class.name}#{COLORS[:reset]}"
319
+
320
+ # Calculate total duration
321
+ duration_str = ""
322
+ if progress.run_duration
323
+ duration_str = " (#{progress.run_duration}ms)"
324
+ end
325
+
326
+ # Count completed tasks
327
+ completed_count = @tasks.values.count { |p| p.run_state == :completed }
328
+ total_count = @tasks.values.count { |p| p.run_state != :pending || p == progress }
329
+ task_count_str = " [#{completed_count}/#{total_count} tasks]"
330
+
331
+ "#{icon} #{name} #{status}#{duration_str}#{task_count_str}"
332
+ end
333
+
497
334
  # Build display lines from tree structure
498
335
  def build_tree_display
499
336
  return [] unless @tree_structure
@@ -739,22 +576,22 @@ module Taski
739
576
  group_prefix = ""
740
577
  if progress&.current_group_index
741
578
  current_group = progress.groups[progress.current_group_index]
742
- group_prefix = "#{current_group.name}: " if current_group
579
+ group_prefix = "#{current_group.name}#{GROUP_SEPARATOR}" if current_group
743
580
  end
744
581
 
745
582
  # Truncate if too long (leave space for tree structure)
746
583
  terminal_cols = terminal_width
747
- max_output_length = terminal_cols - 50
748
- max_output_length = 20 if max_output_length < 20
584
+ max_output_length = terminal_cols - OUTPUT_RESERVED_WIDTH
585
+ max_output_length = OUTPUT_MIN_LENGTH if max_output_length < OUTPUT_MIN_LENGTH
749
586
 
750
587
  full_output = "#{group_prefix}#{last_line}"
751
588
  truncated = if full_output.length > max_output_length
752
- full_output[0, max_output_length - 3] + "..."
589
+ full_output[0, max_output_length - TRUNCATION_ELLIPSIS.length] + TRUNCATION_ELLIPSIS
753
590
  else
754
591
  full_output
755
592
  end
756
593
 
757
- " #{COLORS[:dim]}| #{truncated}#{COLORS[:reset]}"
594
+ "#{COLORS[:dim]}#{OUTPUT_SEPARATOR}#{truncated}#{COLORS[:reset]}"
758
595
  end
759
596
 
760
597
  ##
@@ -764,9 +601,22 @@ module Taski
764
601
  def terminal_width
765
602
  if @output.respond_to?(:winsize)
766
603
  _, cols = @output.winsize
767
- cols || 80
604
+ cols || DEFAULT_TERMINAL_WIDTH
605
+ else
606
+ DEFAULT_TERMINAL_WIDTH
607
+ end
608
+ end
609
+
610
+ ##
611
+ # Returns the terminal height in rows.
612
+ # Defaults to 24 if the output IO doesn't support winsize.
613
+ # @return [Integer] The terminal height in rows.
614
+ def terminal_height
615
+ if @output.respond_to?(:winsize)
616
+ rows, _ = @output.winsize
617
+ rows || DEFAULT_TERMINAL_HEIGHT
768
618
  else
769
- 80
619
+ DEFAULT_TERMINAL_HEIGHT
770
620
  end
771
621
  end
772
622
 
@@ -92,6 +92,9 @@ module Taski
92
92
  # Re-analyze the class methods
93
93
  # Preserve impl chain context: methods called from impl should continue
94
94
  # detecting constants as impl candidates
95
+ # Set namespace path from target class name for constant resolution
96
+ @current_namespace_path = @target_task_class.name.split("::")
97
+
95
98
  @class_method_defs.each do |method_name, method_node|
96
99
  next unless new_methods.include?(method_name)
97
100
 
data/lib/taski/task.rb CHANGED
@@ -82,12 +82,7 @@ module Taski
82
82
  # @raise [ArgumentError] If workers is not a positive integer or nil.
83
83
  # @return [Object] The result of task execution.
84
84
  def run(args: {}, workers: nil)
85
- validate_workers!(workers)
86
- Taski.start_args(options: args.merge(_workers: workers), root_task: self)
87
- validate_no_circular_dependencies!
88
- fresh_wrapper.run
89
- ensure
90
- Taski.reset_args!
85
+ with_execution_setup(args: args, workers: workers) { |wrapper| wrapper.run }
91
86
  end
92
87
 
93
88
  ##
@@ -99,12 +94,7 @@ module Taski
99
94
  # Must be a positive integer or nil.
100
95
  # @raise [ArgumentError] If workers is not a positive integer or nil.
101
96
  def clean(args: {}, workers: nil)
102
- validate_workers!(workers)
103
- Taski.start_args(options: args.merge(_workers: workers), root_task: self)
104
- validate_no_circular_dependencies!
105
- fresh_wrapper.clean
106
- ensure
107
- Taski.reset_args!
97
+ with_execution_setup(args: args, workers: workers) { |wrapper| wrapper.clean }
108
98
  end
109
99
 
110
100
  ##
@@ -118,12 +108,7 @@ module Taski
118
108
  # @raise [ArgumentError] If workers is not a positive integer or nil.
119
109
  # @return [Object] The result of task execution
120
110
  def run_and_clean(args: {}, workers: nil)
121
- validate_workers!(workers)
122
- Taski.start_args(options: args.merge(_workers: workers), root_task: self)
123
- validate_no_circular_dependencies!
124
- fresh_wrapper.run_and_clean
125
- ensure
126
- Taski.reset_args!
111
+ with_execution_setup(args: args, workers: workers) { |wrapper| wrapper.run_and_clean }
127
112
  end
128
113
 
129
114
  ##
@@ -144,6 +129,21 @@ module Taski
144
129
 
145
130
  private
146
131
 
132
+ ##
133
+ # Sets up execution environment and yields a fresh wrapper.
134
+ # Handles workers validation, args lifecycle, and dependency validation.
135
+ # @param args [Hash] User-defined arguments
136
+ # @param workers [Integer, nil] Number of worker threads
137
+ # @yield [wrapper] Block receiving the fresh wrapper to execute
138
+ # @return [Object] The result of the block
139
+ def with_execution_setup(args:, workers:)
140
+ 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
144
+ end
145
+ end
146
+
147
147
  ##
148
148
  # Creates a fresh TaskWrapper with its own registry.
149
149
  # Used for class method execution (Task.run) where each call is independent.
@@ -199,14 +199,9 @@ module Taski
199
199
  wrapper.get_exported_value(method)
200
200
  else
201
201
  # Outside execution - fresh execution (top-level call)
202
- args_was_nil = Taski.args.nil?
203
- begin
204
- Taski.start_args(options: {}, root_task: self) if args_was_nil
202
+ Taski.with_args(options: {}, root_task: self) do
205
203
  validate_no_circular_dependencies!
206
204
  fresh_wrapper.get_exported_value(method)
207
- ensure
208
- # Only reset if we set args
209
- Taski.reset_args! if args_was_nil
210
205
  end
211
206
  end
212
207
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Taski
4
+ module TestHelper
5
+ # Raised when attempting to mock a class that is not a Taski::Task or Taski::Section subclass.
6
+ class InvalidTaskError < ArgumentError
7
+ end
8
+
9
+ # Raised when attempting to mock a method that is not an exported method of the task.
10
+ class InvalidMethodError < ArgumentError
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ module Taski
6
+ module TestHelper
7
+ # Minitest integration module.
8
+ # Include this in your test class for automatic mock cleanup.
9
+ #
10
+ # @example
11
+ # class MyTaskTest < Minitest::Test
12
+ # include Taski::TestHelper::Minitest
13
+ #
14
+ # def test_something
15
+ # mock_task(FetchData, result: "mocked")
16
+ # # ... test code ...
17
+ # end
18
+ # # Mocks are automatically cleaned up after each test
19
+ # end
20
+ module Minitest
21
+ def self.included(base)
22
+ base.include(Taski::TestHelper)
23
+ end
24
+
25
+ # Reset mocks before each test to ensure clean state.
26
+ def setup
27
+ super
28
+ Taski::TestHelper.reset_mocks!
29
+ end
30
+
31
+ # Reset mocks after each test to prevent pollution.
32
+ def teardown
33
+ Taski::TestHelper.reset_mocks!
34
+ super
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,51 @@
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.
42
+ # Should be called in test setup/teardown.
43
+ def reset!
44
+ @mutex.synchronize do
45
+ @mocks = {}
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end