Class: Ace::TestRunner::Molecules::InProcessRunner

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/test_runner/molecules/in_process_runner.rb

Overview

Runs tests directly in the current Ruby process without spawning subprocesses This provides significantly faster execution for unit tests that don’t need isolation

Instance Method Summary collapse

Constructor Details

#initialize(timeout: nil) ⇒ InProcessRunner

Returns a new instance of InProcessRunner.



13
14
15
# File 'lib/ace/test_runner/molecules/in_process_runner.rb', line 13

def initialize(timeout: nil)
  @timeout = timeout
end

Instance Method Details

#execute_single_file(file, options = {}) ⇒ Object



174
175
176
# File 'lib/ace/test_runner/molecules/in_process_runner.rb', line 174

def execute_single_file(file, options = {})
  execute_tests([file], options)
end

#execute_tests(files, options = {}) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/ace/test_runner/molecules/in_process_runner.rb', line 17

def execute_tests(files, options = {})
  return empty_result if files.empty?

  start_time = Time.now

  # Capture stdout/stderr
  original_stdout = $stdout
  original_stderr = $stderr
  stdout_io = StringIO.new
  stderr_io = StringIO.new

  # Store original verbose setting
  original_verbose = $VERBOSE
  original_mt_no_autorun = ENV["MT_NO_AUTORUN"]

  begin
    $stdout = stdout_io
    $stderr = stderr_io
    $VERBOSE = nil if options[:suppress_warnings]

    # Prevent Minitest from auto-running
    ENV["MT_NO_AUTORUN"] = "1"

    # Add test directory to load path if not already there
    test_dir = File.expand_path("test")
    lib_dir = File.expand_path("lib")
    $LOAD_PATH.unshift(test_dir) unless $LOAD_PATH.include?(test_dir)
    $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)

    # Only require minitest/autorun if not already loaded
    # This prevents double runs when ace/test_support is loaded
    unless defined?(Minitest.autorun)
      require "minitest/autorun"
    end

    # First pass: check for line numbers and resolve test names
    # This must be done before loading files
    test_names_to_run = []
    files_to_load = []

    files.each do |file|
      # Check if file has line number (file:line format)
      if file =~ /^(.+):(\d+)$/
        actual_file = $1
        line_number = $2.to_i

        # Resolve line number to test name
        require_relative "../atoms/line_number_resolver"
        test_name = Ace::TestRunner::Atoms::LineNumberResolver.resolve_test_at_line(actual_file, line_number)
        test_names_to_run << test_name if test_name

        files_to_load << actual_file
      else
        files_to_load << file
      end
    end

    # Store test names in options to pass to run_minitest_with_args
    options = options.merge(test_names_filter: test_names_to_run) if test_names_to_run.any?

    # Clear previously loaded test classes to avoid accumulation between groups
    # This is crucial for in-process execution where tests from previous groups
    # would otherwise be re-run in subsequent groups
    Minitest::Runnable.runnables.clear

    # Setup Minitest::Reporters BEFORE loading test files
    # For in-process mode, we need to handle reporter state carefully
    # because Minitest::Reporters.use! only works properly on first call
    require "minitest/reporters"

    # Create a fresh reporter for this group
    reporter = Minitest::Reporters::DefaultReporter.new(io: $stdout)

    # If this isn't the first group, we need to replace the existing reporter
    if Minitest.reporter && Minitest.reporter.reporters
      $stdout.flush
      Minitest.reporter.reporters.clear
      Minitest.reporter.reporters << reporter
      # Reset reporter state for the new group
      reporter.start_time = nil
      # NOTE: Known limitation - progress dots don't show for subsequent groups
      # in in-process mode. This appears to be a Minitest::Reporters limitation
      # where some internal state prevents proper output after the first run.
      # Test counts and results are still accurate.
    else
      # First group - use the standard setup
      Minitest::Reporters.use! reporter
    end

    # Load the test files
    files_to_load.uniq.each do |file|
      file_path = File.expand_path(file)
      begin
        load file_path
      rescue LoadError => e
        stderr_io.puts "Failed to load #{file}: #{e.message}"
        # Re-raise to fail the entire test run
        raise
      end
    end

    # Run Minitest with captured output
    # Suppress Minitest's own output by using null reporter
    exit_code = if @timeout
      Timeout.timeout(@timeout) do
        run_minitest_silent(options)
      end
    else
      run_minitest_silent(options)
    end

    success = exit_code == true || exit_code == 0
  rescue Timeout::Error
    stderr_io.puts "Test execution timed out after #{@timeout} seconds"
    success = false
    exit_code = 124
  rescue LoadError
    # LoadError already logged in the loop above
    stderr_io.puts "Test run aborted due to load error"
    success = false
    exit_code = 1
  rescue => e
    stderr_io.puts "Error running tests: #{e.message}"
    stderr_io.puts e.backtrace.join("\n") if options[:verbose]
    success = false
    exit_code = 1
  ensure
    $stdout = original_stdout
    $stderr = original_stderr
    $VERBOSE = original_verbose

    # Restore original MT_NO_AUTORUN value
    if original_mt_no_autorun
      ENV["MT_NO_AUTORUN"] = original_mt_no_autorun
    else
      ENV.delete("MT_NO_AUTORUN")
    end
  end

  end_time = Time.now

  {
    stdout: stdout_io.string,
    stderr: stderr_io.string,
    status: OpenStruct.new(success?: success, exitstatus: if exit_code.is_a?(Integer)
                                                            exit_code
                                                          else
                                                            (success ? 0 : 1)
                                                          end),
    command: "in-process:#{files.join(",")}",
    start_time: start_time,
    end_time: end_time,
    duration: end_time - start_time,
    success: success
  }
end

#execute_with_progress(files, options = {}, &block) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/ace/test_runner/molecules/in_process_runner.rb', line 178

def execute_with_progress(files, options = {}, &block)
  # For in-process execution, we run all tests together for best performance
  result = execute_tests(files, options)

  # Send stdout event for per-test progress parsing
  if block_given? && result[:stdout]
    yield({type: :stdout, content: result[:stdout]})
  end

  # Simulate progress callbacks for compatibility
  if block_given?
    files.each { |file| yield({type: :start, file: file}) }
    files.each { |file| yield({type: :complete, file: file, success: result[:success], duration: result[:duration] / files.size}) }
  end

  result
end