tryouts 2.4.1 → 3.0.0.pre2
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/LICENSE.txt +1 -1
- data/README.md +187 -73
- data/exe/try +53 -45
- data/exe/tryouts +2 -2
- data/lib/tryouts/cli/formatters/base.rb +108 -0
- data/lib/tryouts/cli/formatters/compact.rb +246 -0
- data/lib/tryouts/cli/formatters/factory.rb +52 -0
- data/lib/tryouts/cli/formatters/output_manager.rb +140 -0
- data/lib/tryouts/cli/formatters/quiet.rb +159 -0
- data/lib/tryouts/cli/formatters/verbose.rb +344 -0
- data/lib/tryouts/cli/formatters.rb +6 -0
- data/lib/tryouts/cli/modes/generate.rb +22 -0
- data/lib/tryouts/cli/modes/inspect.rb +42 -0
- data/lib/tryouts/cli/opts.rb +88 -0
- data/lib/tryouts/cli.rb +54 -0
- data/lib/tryouts/console.rb +55 -40
- data/lib/tryouts/file_processor.rb +66 -0
- data/lib/tryouts/prism_parser.rb +314 -0
- data/lib/tryouts/test_executor.rb +82 -0
- data/lib/tryouts/test_runner.rb +128 -0
- data/lib/tryouts/testbatch.rb +293 -53
- data/lib/tryouts/testcase.rb +32 -91
- data/lib/tryouts/translators/minitest_translator.rb +106 -0
- data/lib/tryouts/translators/rspec_translator.rb +88 -0
- data/lib/tryouts/version.rb +2 -12
- data/lib/tryouts.rb +16 -263
- metadata +60 -13
- data/VERSION.yml +0 -4
- data/lib/tryouts/section.rb +0 -27
@@ -0,0 +1,66 @@
|
|
1
|
+
# lib/tryouts/file_processor.rb
|
2
|
+
|
3
|
+
require_relative 'prism_parser'
|
4
|
+
require_relative 'test_executor'
|
5
|
+
require_relative 'cli/modes/inspect'
|
6
|
+
require_relative 'cli/modes/generate'
|
7
|
+
|
8
|
+
class Tryouts
|
9
|
+
class FileProcessor
|
10
|
+
def initialize(file:, options:, output_manager:, translator:, global_tally:)
|
11
|
+
@file = file
|
12
|
+
@options = options
|
13
|
+
@output_manager = output_manager
|
14
|
+
@translator = translator
|
15
|
+
@global_tally = global_tally
|
16
|
+
end
|
17
|
+
|
18
|
+
def process
|
19
|
+
testrun = PrismParser.new(@file).parse
|
20
|
+
@global_tally[:file_count] += 1
|
21
|
+
@output_manager.file_parsed(@file, testrun.total_tests)
|
22
|
+
|
23
|
+
if @options[:inspect]
|
24
|
+
handle_inspect_mode(testrun)
|
25
|
+
elsif @options[:generate_only]
|
26
|
+
handle_generate_only_mode(testrun)
|
27
|
+
else
|
28
|
+
execute_tests(testrun)
|
29
|
+
end
|
30
|
+
rescue TryoutSyntaxError => ex
|
31
|
+
handle_syntax_error(ex)
|
32
|
+
rescue StandardError, SystemStackError, LoadError, SecurityError, NoMemoryError => ex
|
33
|
+
handle_general_error(ex)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def handle_inspect_mode(testrun)
|
39
|
+
mode = Tryouts::CLI::InspectMode.new(@file, testrun, @options, @output_manager, @translator)
|
40
|
+
mode.handle
|
41
|
+
0
|
42
|
+
end
|
43
|
+
|
44
|
+
def handle_generate_only_mode(testrun)
|
45
|
+
mode = Tryouts::CLI::GenerateMode.new(@file, testrun, @options, @output_manager, @translator)
|
46
|
+
mode.handle
|
47
|
+
0
|
48
|
+
end
|
49
|
+
|
50
|
+
def execute_tests(testrun)
|
51
|
+
tex = TestExecutor.new(@file, testrun, @options, @output_manager, @translator, @global_tally)
|
52
|
+
tex.execute
|
53
|
+
end
|
54
|
+
|
55
|
+
def handle_syntax_error(ex)
|
56
|
+
@output_manager.file_failure(@file, "Syntax error: #{ex.message}")
|
57
|
+
1
|
58
|
+
end
|
59
|
+
|
60
|
+
def handle_general_error(ex)
|
61
|
+
@global_tally[:total_errors] += 1 if @global_tally
|
62
|
+
@output_manager.file_failure(@file, ex.message, ex.backtrace)
|
63
|
+
1
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,314 @@
|
|
1
|
+
# Modern Ruby 3.4+ solution for the teardown bug
|
2
|
+
|
3
|
+
require 'prism'
|
4
|
+
require_relative 'testcase'
|
5
|
+
|
6
|
+
class Tryouts
|
7
|
+
# Fixed PrismParser with pattern matching for robust token filtering
|
8
|
+
class PrismParser
|
9
|
+
def initialize(source_path)
|
10
|
+
@source_path = source_path
|
11
|
+
@source = File.read(source_path)
|
12
|
+
@lines = @source.lines.map(&:chomp)
|
13
|
+
@prism_result = Prism.parse(@source)
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse
|
17
|
+
return handle_syntax_errors if @prism_result.failure?
|
18
|
+
|
19
|
+
tokens = tokenize_content
|
20
|
+
test_blocks = group_into_test_blocks(tokens)
|
21
|
+
process_test_blocks(test_blocks)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Tokenize content using pattern matching for clean line classification
|
27
|
+
def tokenize_content
|
28
|
+
tokens = []
|
29
|
+
|
30
|
+
@lines.each_with_index do |line, index|
|
31
|
+
token = case line
|
32
|
+
in /^##\s*(.*)$/ # Test description format: ## description
|
33
|
+
{ type: :description, content: $1.strip, line: index }
|
34
|
+
in /^#\s*TEST\s*\d*:\s*(.*)$/ # rubocop:disable Lint/DuplicateBranch
|
35
|
+
{ type: :description, content: $1.strip, line: index }
|
36
|
+
in /^#\s*=>\s*(.*)$/ # Expectation
|
37
|
+
{ type: :expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
|
38
|
+
in /^##\s*=>\s*(.*)$/ # Commented out expectation (should be ignored)
|
39
|
+
{ type: :comment, content: '=>' + $1.strip, line: index }
|
40
|
+
in /^#\s*(.*)$/ # Single hash comment - potential description
|
41
|
+
{ type: :potential_description, content: $1.strip, line: index }
|
42
|
+
in /^\s*$/ # Blank line
|
43
|
+
{ type: :blank, line: index }
|
44
|
+
else # Ruby code
|
45
|
+
{ type: :code, content: line, line: index, ast: parse_ruby_line(line) }
|
46
|
+
end
|
47
|
+
|
48
|
+
tokens << token
|
49
|
+
end
|
50
|
+
|
51
|
+
# Post-process to convert potential_descriptions to descriptions or comments
|
52
|
+
classify_potential_descriptions(tokens)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Convert potential_descriptions to descriptions or comments based on context
|
56
|
+
def classify_potential_descriptions(tokens)
|
57
|
+
tokens.map.with_index do |token, index|
|
58
|
+
if token[:type] == :potential_description
|
59
|
+
# Look ahead strictly for the pattern: [optional blanks] code expectation
|
60
|
+
following_tokens = tokens[(index + 1)..]
|
61
|
+
|
62
|
+
# Skip blanks and find next non-blank tokens
|
63
|
+
non_blank_following = following_tokens.reject { |t| t[:type] == :blank }
|
64
|
+
|
65
|
+
# Must have: code immediately followed by expectation (with possible blanks between)
|
66
|
+
if non_blank_following.size >= 2 &&
|
67
|
+
non_blank_following[0][:type] == :code &&
|
68
|
+
non_blank_following[1][:type] == :expectation
|
69
|
+
token.merge(type: :description)
|
70
|
+
else
|
71
|
+
token.merge(type: :comment)
|
72
|
+
end
|
73
|
+
else
|
74
|
+
token
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Group tokens into logical test blocks using pattern matching
|
80
|
+
def group_into_test_blocks(tokens)
|
81
|
+
blocks = []
|
82
|
+
current_block = new_test_block
|
83
|
+
|
84
|
+
tokens.each do |token|
|
85
|
+
case [current_block, token]
|
86
|
+
in [_, { type: :description, content: String => desc, line: Integer => line_num }]
|
87
|
+
# Only combine descriptions if current block has a description but no code/expectations yet
|
88
|
+
# Allow blank lines between multi-line descriptions
|
89
|
+
if !current_block[:description].empty? && current_block[:code].empty? && current_block[:expectations].empty?
|
90
|
+
# Multi-line description continuation
|
91
|
+
current_block[:description] = [current_block[:description], desc].join(' ').strip
|
92
|
+
else
|
93
|
+
# Start new test block on description
|
94
|
+
blocks << current_block if block_has_content?(current_block)
|
95
|
+
current_block = new_test_block.merge(description: desc, start_line: line_num)
|
96
|
+
end
|
97
|
+
|
98
|
+
in [{ expectations: [], start_line: nil }, { type: :code, content: String => code, line: Integer => line_num }]
|
99
|
+
# First code in a new block - set start_line
|
100
|
+
current_block[:code] << token
|
101
|
+
current_block[:start_line] = line_num
|
102
|
+
|
103
|
+
in [{ expectations: [] }, { type: :code, content: String => code }]
|
104
|
+
# Code before expectations - add to current block
|
105
|
+
current_block[:code] << token
|
106
|
+
|
107
|
+
in [{ expectations: Array => exps }, { type: :code }] if !exps.empty?
|
108
|
+
# Code after expectations - finalize current block and start new one
|
109
|
+
blocks << current_block
|
110
|
+
current_block = new_test_block.merge(code: [token], start_line: token[:line])
|
111
|
+
|
112
|
+
in [_, { type: :expectation }]
|
113
|
+
current_block[:expectations] << token
|
114
|
+
|
115
|
+
in [_, { type: :comment | :blank }]
|
116
|
+
add_context_to_block(current_block, token)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
blocks << current_block if block_has_content?(current_block)
|
121
|
+
classify_blocks(blocks)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Process classified test blocks into domain objects
|
125
|
+
def process_test_blocks(classified_blocks)
|
126
|
+
setup_blocks = classified_blocks.filter { |block| block[:type] == :setup }
|
127
|
+
test_blocks = classified_blocks.filter { |block| block[:type] == :test }
|
128
|
+
teardown_blocks = classified_blocks.filter { |block| block[:type] == :teardown }
|
129
|
+
|
130
|
+
Testrun.new(
|
131
|
+
setup: build_setup(setup_blocks),
|
132
|
+
test_cases: test_blocks.map { |block| build_test_case(block) },
|
133
|
+
teardown: build_teardown(teardown_blocks),
|
134
|
+
source_file: @source_path,
|
135
|
+
metadata: { parsed_at: Time.now, parser: :prism_v2_fixed },
|
136
|
+
)
|
137
|
+
end
|
138
|
+
|
139
|
+
def build_setup(setup_blocks)
|
140
|
+
return Setup.new(code: '', line_range: 0..0, path: @source_path) if setup_blocks.empty?
|
141
|
+
|
142
|
+
Setup.new(
|
143
|
+
code: extract_pure_code_from_blocks(setup_blocks),
|
144
|
+
line_range: calculate_block_range(setup_blocks),
|
145
|
+
path: @source_path,
|
146
|
+
)
|
147
|
+
end
|
148
|
+
|
149
|
+
def build_teardown(teardown_blocks)
|
150
|
+
return Teardown.new(code: '', line_range: 0..0, path: @source_path) if teardown_blocks.empty?
|
151
|
+
|
152
|
+
Teardown.new(
|
153
|
+
code: extract_pure_code_from_blocks(teardown_blocks),
|
154
|
+
line_range: calculate_block_range(teardown_blocks),
|
155
|
+
path: @source_path,
|
156
|
+
)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Modern Ruby 3.4+ pattern matching for robust code extraction
|
160
|
+
# This filters out comments added by add_context_to_block explicitly
|
161
|
+
def extract_pure_code_from_blocks(blocks)
|
162
|
+
blocks
|
163
|
+
.flat_map { |block| block[:code] }
|
164
|
+
.filter_map do |token|
|
165
|
+
case token
|
166
|
+
in { type: :code, content: String => content }
|
167
|
+
content
|
168
|
+
else
|
169
|
+
nil
|
170
|
+
end
|
171
|
+
end
|
172
|
+
.join("\n")
|
173
|
+
end
|
174
|
+
|
175
|
+
def calculate_block_range(blocks)
|
176
|
+
return 0..0 if blocks.empty?
|
177
|
+
|
178
|
+
# Filter out blocks with nil line numbers and build valid ranges
|
179
|
+
valid_blocks = blocks.filter { |block| block[:start_line] && block[:end_line] }
|
180
|
+
return 0..0 if valid_blocks.empty?
|
181
|
+
|
182
|
+
line_ranges = valid_blocks.map { |block| block[:start_line]..block[:end_line] }
|
183
|
+
line_ranges.first.first..line_ranges.last.last
|
184
|
+
end
|
185
|
+
|
186
|
+
def extract_code_content(code_tokens)
|
187
|
+
code_tokens
|
188
|
+
.filter_map do |token|
|
189
|
+
case token
|
190
|
+
in { type: :code, content: String => content }
|
191
|
+
content
|
192
|
+
else
|
193
|
+
nil
|
194
|
+
end
|
195
|
+
end
|
196
|
+
.join("\n")
|
197
|
+
end
|
198
|
+
|
199
|
+
def parse_ruby_line(line)
|
200
|
+
return nil if line.strip.empty?
|
201
|
+
|
202
|
+
result = Prism.parse(line.strip)
|
203
|
+
case result
|
204
|
+
in { errors: [] => errors, value: { body: { body: [ast] } } }
|
205
|
+
ast
|
206
|
+
in { errors: Array => errors } if errors.any?
|
207
|
+
{ type: :parse_error, errors: errors, raw: line }
|
208
|
+
else
|
209
|
+
nil
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def parse_expectation(expr)
|
214
|
+
parse_ruby_line(expr)
|
215
|
+
end
|
216
|
+
|
217
|
+
def new_test_block
|
218
|
+
{
|
219
|
+
description: '',
|
220
|
+
code: [],
|
221
|
+
expectations: [],
|
222
|
+
comments: [],
|
223
|
+
start_line: nil,
|
224
|
+
end_line: nil,
|
225
|
+
}
|
226
|
+
end
|
227
|
+
|
228
|
+
def block_has_content?(block)
|
229
|
+
case block
|
230
|
+
in { description: String => desc, code: Array => code, expectations: Array => exps }
|
231
|
+
!desc.empty? || !code.empty? || !exps.empty?
|
232
|
+
else
|
233
|
+
false
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def add_context_to_block(block, token)
|
238
|
+
case [block[:expectations].empty?, token]
|
239
|
+
in [true, { type: :comment | :blank }]
|
240
|
+
# Comments before expectations go with code
|
241
|
+
block[:code] << token
|
242
|
+
in [false, { type: :comment | :blank }]
|
243
|
+
# Comments after expectations are test context
|
244
|
+
block[:comments] << token
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Classify blocks as setup, test, or teardown based on content
|
249
|
+
def classify_blocks(blocks)
|
250
|
+
blocks.map.with_index do |block, index|
|
251
|
+
block_type = case block
|
252
|
+
in { expectations: [] } if index == 0
|
253
|
+
:setup
|
254
|
+
in { expectations: [] } if index == blocks.size - 1
|
255
|
+
:teardown
|
256
|
+
in { expectations: Array => exps } if !exps.empty?
|
257
|
+
:test
|
258
|
+
else
|
259
|
+
:preamble # Default fallback
|
260
|
+
end
|
261
|
+
|
262
|
+
block.merge(type: block_type, end_line: calculate_end_line(block))
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def calculate_end_line(block)
|
267
|
+
last_tokens = [*block[:code], *block[:expectations], *block[:comments]]
|
268
|
+
return block[:start_line] if last_tokens.empty?
|
269
|
+
|
270
|
+
last_tokens.map { |token| token[:line] }.max || block[:start_line]
|
271
|
+
end
|
272
|
+
|
273
|
+
def build_test_case(block)
|
274
|
+
case block
|
275
|
+
in {
|
276
|
+
type: :test,
|
277
|
+
description: String => desc,
|
278
|
+
code: Array => code_tokens,
|
279
|
+
expectations: Array => exp_tokens,
|
280
|
+
start_line: Integer => start_line,
|
281
|
+
end_line: Integer => end_line
|
282
|
+
}
|
283
|
+
# Extract source lines from the original source during parsing
|
284
|
+
source_lines = @lines[start_line..end_line]
|
285
|
+
|
286
|
+
TestCase.new(
|
287
|
+
description: desc,
|
288
|
+
code: extract_code_content(code_tokens),
|
289
|
+
expectations: exp_tokens.map { |token| token[:content] },
|
290
|
+
line_range: start_line..end_line,
|
291
|
+
path: @source_path,
|
292
|
+
source_lines: source_lines,
|
293
|
+
)
|
294
|
+
else
|
295
|
+
raise "Invalid test block structure: #{block}"
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def handle_syntax_errors
|
300
|
+
errors = @prism_result.errors.map do |error|
|
301
|
+
line_context = @lines[error.location.start_line - 1] || ''
|
302
|
+
|
303
|
+
TryoutSyntaxError.new(
|
304
|
+
error.message,
|
305
|
+
line_number: error.location.start_line,
|
306
|
+
context: line_context,
|
307
|
+
source_file: @source_path,
|
308
|
+
)
|
309
|
+
end
|
310
|
+
|
311
|
+
raise errors.first if errors.any?
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# lib/tryouts/test_executor.rb
|
2
|
+
|
3
|
+
require_relative 'testbatch'
|
4
|
+
|
5
|
+
class Tryouts
|
6
|
+
class TestExecutor
|
7
|
+
def initialize(file, testrun, options, output_manager, translator, global_tally)
|
8
|
+
@file = file
|
9
|
+
@testrun = testrun
|
10
|
+
@options = options
|
11
|
+
@output_manager = output_manager
|
12
|
+
@translator = translator
|
13
|
+
@global_tally = global_tally
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute
|
17
|
+
@file_start = Time.now
|
18
|
+
|
19
|
+
case @options[:framework]
|
20
|
+
when :direct
|
21
|
+
execute_direct_mode
|
22
|
+
when :rspec
|
23
|
+
execute_rspec_mode
|
24
|
+
when :minitest
|
25
|
+
execute_minitest_mode
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def execute_direct_mode
|
32
|
+
batch = TestBatch.new(
|
33
|
+
@testrun,
|
34
|
+
shared_context: @options[:shared_context],
|
35
|
+
verbose: @options[:verbose],
|
36
|
+
fails_only: @options[:fails_only],
|
37
|
+
output_manager: @output_manager,
|
38
|
+
global_tally: @global_tally,
|
39
|
+
)
|
40
|
+
|
41
|
+
unless @options[:verbose]
|
42
|
+
context_mode = @options[:shared_context] ? 'shared' : 'fresh'
|
43
|
+
@output_manager.file_execution_start(@file, @testrun.total_tests, context_mode)
|
44
|
+
end
|
45
|
+
|
46
|
+
test_results = []
|
47
|
+
success = batch.run do
|
48
|
+
last_result = batch.results.last
|
49
|
+
test_results << last_result if last_result
|
50
|
+
end
|
51
|
+
|
52
|
+
file_failed_count = test_results.count { |r| r[:status] == :failed }
|
53
|
+
file_error_count = test_results.count { |r| r[:status] == :error }
|
54
|
+
@global_tally[:total_tests] += batch.size
|
55
|
+
@global_tally[:total_failed] += file_failed_count
|
56
|
+
@global_tally[:total_errors] += file_error_count
|
57
|
+
@global_tally[:successful_files] += 1 if success
|
58
|
+
|
59
|
+
duration = Time.now.to_f - @file_start.to_f
|
60
|
+
@output_manager.file_success(@file, batch.size, file_failed_count, file_error_count, duration)
|
61
|
+
|
62
|
+
# Combine failures and errors to determine the exit code.
|
63
|
+
success ? 0 : (file_failed_count + file_error_count)
|
64
|
+
end
|
65
|
+
|
66
|
+
def execute_rspec_mode
|
67
|
+
@output_manager.info 'Executing with RSpec framework', 2
|
68
|
+
@translator.translate(@testrun)
|
69
|
+
require 'rspec/core'
|
70
|
+
RSpec::Core::Runner.run([])
|
71
|
+
0
|
72
|
+
end
|
73
|
+
|
74
|
+
def execute_minitest_mode
|
75
|
+
@output_manager.info 'Executing with Minitest framework', 2
|
76
|
+
@translator.translate(@testrun)
|
77
|
+
ARGV.clear
|
78
|
+
require 'minitest/autorun'
|
79
|
+
0
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# lib/tryouts/test_runner.rb
|
2
|
+
|
3
|
+
require_relative 'prism_parser'
|
4
|
+
require_relative 'testbatch'
|
5
|
+
require_relative 'translators/rspec_translator'
|
6
|
+
require_relative 'translators/minitest_translator'
|
7
|
+
require_relative 'file_processor'
|
8
|
+
|
9
|
+
class Tryouts
|
10
|
+
class TestRunner
|
11
|
+
FRAMEWORKS = {
|
12
|
+
rspec: Translators::RSpecTranslator,
|
13
|
+
minitest: Translators::MinitestTranslator,
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
FRAMEWORK_DEFAULTS = {
|
17
|
+
direct: { shared_context: true, generate_only: false },
|
18
|
+
rspec: { shared_context: false, generate_only: false },
|
19
|
+
minitest: { shared_context: false, generate_only: false },
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
def initialize(files:, options:, output_manager:)
|
23
|
+
@files = files
|
24
|
+
@options = apply_framework_defaults(options)
|
25
|
+
@output_manager = output_manager
|
26
|
+
@translator = initialize_translator
|
27
|
+
@global_tally = initialize_global_tally
|
28
|
+
end
|
29
|
+
|
30
|
+
def run
|
31
|
+
log_run_info
|
32
|
+
validate_framework
|
33
|
+
|
34
|
+
result = process_files
|
35
|
+
show_grand_total if @global_tally[:file_count] > 1
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def log_run_info
|
42
|
+
@output_manager.processing_phase(@files.size)
|
43
|
+
@output_manager.info "Framework: #{@options[:framework]}", 1
|
44
|
+
@output_manager.info "Context: #{@options[:shared_context] ? 'shared' : 'fresh'}", 1
|
45
|
+
|
46
|
+
@files.each_with_index do |file, idx|
|
47
|
+
@output_manager.info "#{idx + 1}/#{@files.size}: #{Console.pretty_path(file)}", 1
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def apply_framework_defaults(options)
|
52
|
+
framework_defaults = FRAMEWORK_DEFAULTS[options[:framework]] || {}
|
53
|
+
framework_defaults.merge(options)
|
54
|
+
end
|
55
|
+
|
56
|
+
def validate_framework
|
57
|
+
unless @options[:framework] == :direct || FRAMEWORKS.key?(@options[:framework])
|
58
|
+
raise ArgumentError, "Unknown framework: #{@options[:framework]}. Available: #{FRAMEWORKS.keys.join(', ')}, direct"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def initialize_translator
|
63
|
+
return nil if @options[:framework] == :direct
|
64
|
+
|
65
|
+
FRAMEWORKS[@options[:framework]].new
|
66
|
+
end
|
67
|
+
|
68
|
+
def initialize_global_tally
|
69
|
+
{
|
70
|
+
total_tests: 0,
|
71
|
+
total_failed: 0,
|
72
|
+
total_errors: 0,
|
73
|
+
file_count: 0,
|
74
|
+
start_time: Time.now,
|
75
|
+
successful_files: 0,
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def process_files
|
80
|
+
failure_count = 0
|
81
|
+
|
82
|
+
@files.each do |file|
|
83
|
+
result = process_file(file)
|
84
|
+
failure_count += result unless result.zero?
|
85
|
+
status = result.zero? ? Console.color(:green, 'PASS') : Console.color(:red, 'FAIL')
|
86
|
+
@output_manager.info "#{status} #{Console.pretty_path(file)} (#{result} failures)", 1
|
87
|
+
end
|
88
|
+
|
89
|
+
failure_count
|
90
|
+
end
|
91
|
+
|
92
|
+
def process_file(file)
|
93
|
+
file = FileProcessor.new(
|
94
|
+
file: file,
|
95
|
+
options: @options,
|
96
|
+
output_manager: @output_manager,
|
97
|
+
translator: @translator,
|
98
|
+
global_tally: @global_tally,
|
99
|
+
)
|
100
|
+
file.process
|
101
|
+
rescue StandardError => ex
|
102
|
+
handle_file_error(ex)
|
103
|
+
@global_tally[:total_errors] += 1
|
104
|
+
1
|
105
|
+
end
|
106
|
+
|
107
|
+
def show_grand_total
|
108
|
+
elapsed_time = Time.now - @global_tally[:start_time]
|
109
|
+
@output_manager.grand_total(
|
110
|
+
@global_tally[:total_tests],
|
111
|
+
@global_tally[:total_failed],
|
112
|
+
@global_tally[:total_errors],
|
113
|
+
@global_tally[:successful_files],
|
114
|
+
@global_tally[:file_count],
|
115
|
+
elapsed_time,
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
def handle_file_error(exception)
|
120
|
+
@status = :error
|
121
|
+
Tryouts.debug "TestRunner#process_file: An error occurred processing #{file}: #{ex.message}"
|
122
|
+
error_message = "Batch execution failed: #{exception.message}"
|
123
|
+
backtrace = exception.respond_to?(:backtrace) ? exception.backtrace : nil
|
124
|
+
|
125
|
+
@output_manager&.error(error_message, backtrace)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|