tryouts 3.4.0 → 3.5.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.
@@ -19,12 +19,7 @@ class Tryouts
19
19
  def phase_header(message, file_count: nil)
20
20
  return if message.include?('EXECUTING') # Skip execution phase headers
21
21
 
22
- header_line = message.center(@line_width)
23
- separator_line = '=' * @line_width
24
-
25
- puts(separator_line)
26
- puts(header_line)
27
- puts(separator_line)
22
+ puts("=== #{message} ===")
28
23
  end
29
24
 
30
25
  # File-level operations
@@ -40,12 +35,25 @@ class Tryouts
40
35
  extras << 'teardown' if teardown_present
41
36
  message += " (#{extras.join(', ')})" unless extras.empty?
42
37
 
43
- puts(indent_text(message, 2))
38
+ puts(indent_text(message, 1))
39
+ end
40
+
41
+ def parser_warnings(file_path, warnings:)
42
+ return if warnings.empty? || !@options.fetch(:warnings, true)
43
+
44
+ puts
45
+ puts Console.color(:yellow, "Parser Warnings:")
46
+ warnings.each do |warning|
47
+ puts " #{Console.color(:yellow, 'WARNING')}: #{warning.message} (line #{warning.line_number})"
48
+ puts " #{Console.color(:dim, warning.context)}" unless warning.context.empty?
49
+ puts " #{Console.color(:blue, warning.suggestion)}"
50
+ end
51
+ puts
44
52
  end
45
53
 
46
54
  def file_execution_start(_file_path, test_count:, context_mode:)
47
55
  message = "Running #{test_count} tests with #{context_mode} context"
48
- puts(indent_text(message, 1))
56
+ puts(indent_text(message, 0))
49
57
  end
50
58
 
51
59
  # Summary operations - show detailed failure summary
@@ -53,9 +61,7 @@ class Tryouts
53
61
  return unless failure_collector.any_failures?
54
62
 
55
63
  puts
56
- write '=' * 50
57
- puts
58
- puts Console.color(:red, 'Failed Tests:')
64
+ write '=== FAILURES ==='
59
65
 
60
66
  # Number failures sequentially across all files instead of per-file
61
67
  failure_number = 1
@@ -73,16 +79,14 @@ class Tryouts
73
79
  puts
74
80
  puts Console.color(:yellow, location)
75
81
  puts " #{failure_number}) #{failure.description}"
76
- puts " #{Console.color(:red, 'Failure:')} #{failure.failure_reason}"
82
+ puts " #{Console.color(:red, failure.failure_reason)}"
77
83
 
78
- # Show source context in verbose mode
84
+ # Show source context in compact format
79
85
  if failure.source_context.any?
80
- puts " #{Console.color(:cyan, 'Source:')}"
81
86
  failure.source_context.each do |line|
82
87
  puts " #{line.strip}"
83
88
  end
84
89
  end
85
- puts
86
90
  failure_number += 1
87
91
  end
88
92
  end
@@ -102,25 +106,27 @@ class Tryouts
102
106
 
103
107
  time_str = elapsed_time ? " (#{elapsed_time.round(2)}s)" : ''
104
108
  message = "✗ Out of #{total_tests} tests: #{details_str}#{time_str}"
105
- puts indent_text(Console.color(color, message), 2)
109
+ puts indent_text(Console.color(color, message), 1)
106
110
  else
107
111
  message = "#{total_tests} tests passed"
108
112
  color = :green
109
- puts indent_text(Console.color(color, "✓ #{message}"), 2)
113
+ puts indent_text(Console.color(color, "✓ #{message}"), 1)
110
114
  end
111
115
 
112
116
  return unless elapsed_time
113
117
 
114
118
  time_msg = "Completed in #{format_timing(elapsed_time).strip.tr('()', '')}"
115
- puts indent_text(Console.color(:dim, time_msg), 2)
119
+ puts indent_text(Console.color(:dim, time_msg), 1)
116
120
  end
117
121
 
118
122
  # Test-level operations
119
123
  def test_start(test_case:, index:, total:)
124
+ return unless @show_passed
125
+
120
126
  desc = test_case.description.to_s
121
127
  desc = 'Unnamed test' if desc.empty?
122
128
  message = "Test #{index}/#{total}: #{desc}"
123
- puts indent_text(Console.color(:dim, message), 2)
129
+ puts indent_text(Console.color(:dim, message), 1)
124
130
  end
125
131
 
126
132
  def test_result(result_packet)
@@ -143,7 +149,7 @@ class Tryouts
143
149
  test_case = result_packet.test_case
144
150
  location = "#{Console.pretty_path(test_case.path)}:#{test_case.first_expectation_line + 1}"
145
151
  puts
146
- puts indent_text("#{status_line} @ #{location}", 2)
152
+ puts indent_text("#{status_line} @ #{location}", 1)
147
153
 
148
154
  # Show source code for verbose mode
149
155
  show_test_source_code(test_case)
@@ -160,21 +166,22 @@ class Tryouts
160
166
  def test_output(test_case:, output_text:, result_packet:)
161
167
  return if output_text.nil? || output_text.strip.empty?
162
168
 
163
- puts indent_text('Test Output:', 3)
164
- puts indent_text(Console.color(:dim, '--- BEGIN OUTPUT ---'), 3)
169
+ puts indent_text('Test Output:', 2)
170
+ puts indent_text(Console.color(:dim, '--- BEGIN OUTPUT ---'), 2)
165
171
 
166
172
  output_text.lines.each do |line|
167
- puts indent_text(line.chomp, 4)
173
+ puts indent_text(line.chomp, 3)
168
174
  end
169
175
 
170
- puts indent_text(Console.color(:dim, '--- END OUTPUT ---'), 3)
176
+ puts indent_text(Console.color(:dim, '--- END OUTPUT ---'), 2)
171
177
  puts
172
178
  end
173
179
 
174
180
  # Setup/teardown operations
175
181
  def setup_start(line_range:)
176
182
  message = "Executing global setup (lines #{line_range.first}..#{line_range.last})"
177
- puts indent_text(Console.color(:cyan, message), 2)
183
+ puts indent_text(Console.color(:cyan, message), 1)
184
+ puts
178
185
  end
179
186
 
180
187
  def setup_output(output_text)
@@ -187,8 +194,7 @@ class Tryouts
187
194
 
188
195
  def teardown_start(line_range:)
189
196
  message = "Executing teardown (lines #{line_range.first}..#{line_range.last})"
190
- puts indent_text(Console.color(:cyan, message), 2)
191
- puts
197
+ puts indent_text(Console.color(:cyan, message), 1)
192
198
  end
193
199
 
194
200
  def teardown_output(output_text)
@@ -201,8 +207,7 @@ class Tryouts
201
207
 
202
208
  def grand_total(total_tests:, failed_count:, error_count:, successful_files:, total_files:, elapsed_time:)
203
209
  puts
204
- puts '=' * @line_width
205
- puts 'Grand Total:'
210
+ puts '=== TOTAL ==='
206
211
 
207
212
  issues_count = failed_count + error_count
208
213
  time_str = if elapsed_time < 2.0
@@ -216,13 +221,12 @@ class Tryouts
216
221
  details = []
217
222
  details << "#{failed_count} failed" if failed_count > 0
218
223
  details << "#{error_count} errors" if error_count > 0
219
- puts "#{details.join(', ')}, #{passed} passed#{time_str}"
224
+ printf("%-10s %s\n", "Testcases:", "#{details.join(', ')}, #{passed} passed#{time_str}")
220
225
  else
221
- puts "#{total_tests} tests passed#{time_str}"
226
+ printf("%-10s %s\n", "Testcases:", "#{total_tests} tests passed#{time_str}")
222
227
  end
223
228
 
224
- puts "Files: #{successful_files} of #{total_files} successful"
225
- puts '=' * @line_width
229
+ printf("%-10s %s\n", "Files:", "#{successful_files} of #{total_files} passed")
226
230
  end
227
231
 
228
232
  # Debug and diagnostic output
@@ -243,16 +247,16 @@ class Tryouts
243
247
 
244
248
  def error_message(message, backtrace: nil)
245
249
  error_msg = Console.color(:red, "ERROR: #{message}")
246
- puts indent_text(error_msg, 1)
250
+ puts indent_text(error_msg, 0)
247
251
 
248
252
  return unless backtrace && @show_stack_traces
249
253
 
250
- puts indent_text('Details:', 2)
254
+ puts indent_text('Details:', 1)
251
255
  # Show first 10 lines of backtrace to avoid overwhelming output
252
256
  Console.pretty_backtrace(backtrace, limit: 10).each do |line|
253
- puts indent_text(line, 3)
257
+ puts indent_text(line, 2)
254
258
  end
255
- puts indent_text("... (#{backtrace.length - 10} more lines)", 3) if backtrace.length > 10
259
+ puts indent_text("... (#{backtrace.length - 10} more lines)", 2) if backtrace.length > 10
256
260
  end
257
261
 
258
262
  def live_status_capabilities
@@ -272,16 +276,16 @@ class Tryouts
272
276
  def show_exception_details(test_case, actual_results, expected_results = [])
273
277
  return if actual_results.empty?
274
278
 
275
- puts indent_text('Exception Details:', 4)
279
+ puts indent_text('Exception Details:', 2)
276
280
 
277
281
  actual_results.each_with_index do |actual, idx|
278
282
  expected = expected_results[idx] if expected_results && idx < expected_results.length
279
283
  expectation = test_case.expectations[idx] if test_case.expectations
280
284
 
281
285
  if expectation&.type == :exception
282
- puts indent_text("Caught: #{Console.color(:blue, actual.inspect)}", 5)
283
- puts indent_text("Expectation: #{Console.color(:green, expectation.content)}", 5)
284
- puts indent_text("Result: #{Console.color(:green, expected.inspect)}", 5) if expected
286
+ puts indent_text("Caught: #{Console.color(:blue, actual.inspect)}", 3)
287
+ puts indent_text("Expectation: #{Console.color(:green, expectation.content)}", 3)
288
+ puts indent_text("Result: #{Console.color(:green, expected.inspect)}", 3) if expected
285
289
  end
286
290
  end
287
291
  puts
@@ -300,7 +304,7 @@ class Tryouts
300
304
  line_display = Console.color(:yellow, line_display)
301
305
  end
302
306
 
303
- puts indent_text(line_display, 4)
307
+ puts indent_text(line_display, 2)
304
308
  end
305
309
  puts
306
310
  end
@@ -314,15 +318,15 @@ class Tryouts
314
318
 
315
319
  if !expected.nil?
316
320
  # Use the evaluated expected value from the evaluator
317
- puts indent_text("Expected: #{Console.color(:green, expected.inspect)}", 4)
318
- puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
321
+ puts indent_text("Expected: #{Console.color(:green, expected.inspect)}", 2)
322
+ puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 2)
319
323
  elsif expected_line && !expected_results.empty?
320
324
  # Only show raw expectation content if we have expected_results (non-error case)
321
- puts indent_text("Expected: #{Console.color(:green, expected_line.content)}", 4)
322
- puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
325
+ puts indent_text("Expected: #{Console.color(:green, expected_line.content)}", 2)
326
+ puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 2)
323
327
  else
324
328
  # For error cases (empty expected_results), just show the error
325
- puts indent_text("Error: #{Console.color(:red, actual.inspect)}", 4)
329
+ puts indent_text("Error: #{Console.color(:red, actual.inspect)}", 2)
326
330
  end
327
331
 
328
332
  # Show difference if both are strings
@@ -337,21 +341,16 @@ class Tryouts
337
341
  def show_string_diff(expected, actual)
338
342
  return if expected == actual
339
343
 
340
- puts indent_text('Difference:', 4)
341
- puts indent_text("- #{Console.color(:red, actual)}", 5)
342
- puts indent_text("+ #{Console.color(:green, expected)}", 5)
344
+ puts indent_text('Difference:', 2)
345
+ puts indent_text("- #{Console.color(:red, actual)}", 3)
346
+ puts indent_text("+ #{Console.color(:green, expected)}", 3)
343
347
  end
344
348
 
345
349
  def file_header_visual(file_path)
346
350
  pretty_path = Console.pretty_path(file_path)
347
- header_content = ">>>>> #{pretty_path} "
348
- padding_length = [@line_width - header_content.length, 0].max
349
- padding = '<' * padding_length
350
351
 
351
352
  [
352
- indent_text('-' * @line_width, 1),
353
- indent_text(header_content + padding, 1),
354
- indent_text('-' * @line_width, 1),
353
+ indent_text("--- #{pretty_path} ---", 0)
355
354
  ].join("\n")
356
355
  end
357
356
  end
@@ -376,6 +375,20 @@ class Tryouts
376
375
  super
377
376
  end
378
377
 
378
+ # Suppress setup/teardown output in fails-only mode
379
+ def setup_start(line_range:)
380
+ # No output in fails mode
381
+ end
382
+
383
+ def teardown_start(line_range:)
384
+ # No output in fails mode
385
+ end
386
+
387
+ # Suppress file result summaries in fails-only mode
388
+ def file_result(_file_path, total_tests:, failed_count:, error_count:, elapsed_time: nil)
389
+ # No output in fails mode - let the batch_summary handle failures
390
+ end
391
+
379
392
  def live_status_capabilities
380
393
  {
381
394
  supports_coordination: true, # Verbose can work with coordinated output
@@ -0,0 +1,109 @@
1
+ # lib/tryouts/cli/line_spec_parser.rb
2
+
3
+ class Tryouts
4
+ class CLI
5
+ class LineSpecParser
6
+ # Parse a file path with optional line specification
7
+ # Supports formats:
8
+ # - file.rb:19 (single line)
9
+ # - file.rb:19-45 (range)
10
+ # - file.rb:L19 (GitHub-style single line)
11
+ # - file.rb:L19-45 (GitHub-style range)
12
+ # - file.rb:L19-L45 (GitHub-style range with L on both)
13
+ #
14
+ # Returns [filepath, line_spec] where line_spec is nil or a Range/Integer
15
+ def self.parse(path_with_spec)
16
+ return [path_with_spec, nil] unless path_with_spec.include?(':')
17
+
18
+ # Split on the last colon to handle paths with colons
19
+ parts = path_with_spec.rpartition(':')
20
+ filepath = parts[0]
21
+ line_spec_str = parts[2]
22
+
23
+ # If the filepath is empty, it means there was no colon or the input started with colon
24
+ return [path_with_spec, nil] if filepath.empty?
25
+
26
+ # If the "line spec" part looks like a Windows drive letter, this isn't a line spec
27
+ return [path_with_spec, nil] if line_spec_str =~ /\A[a-zA-Z]\z/
28
+
29
+ # Parse the line specification
30
+ line_spec = parse_line_spec(line_spec_str)
31
+
32
+ # If we couldn't parse it, treat the whole thing as a filepath
33
+ return [path_with_spec, nil] if line_spec.nil?
34
+
35
+ [filepath, line_spec]
36
+ end
37
+
38
+ private
39
+
40
+ def self.parse_line_spec(spec)
41
+ return nil if spec.nil? || spec.empty?
42
+
43
+ # Remove 'L' prefix if present (GitHub style)
44
+ spec = spec.gsub(/L/i, '')
45
+
46
+ # Handle range (e.g., "19-80")
47
+ if spec.include?('-')
48
+ parts = spec.split('-', 2)
49
+
50
+ # Validate that both parts are numeric
51
+ return nil unless parts[0] =~ /\A\d+\z/ && parts[1] =~ /\A\d+\z/
52
+
53
+ start_line = parts[0].to_i
54
+ end_line = parts[1].to_i
55
+
56
+ # Validate the numbers
57
+ return nil if start_line <= 0 || end_line <= 0
58
+ return nil if start_line > end_line
59
+
60
+ start_line..end_line
61
+ else
62
+ # Single line number - validate it's numeric
63
+ return nil unless spec =~ /\A\d+\z/
64
+
65
+ line = spec.to_i
66
+ return nil if line <= 0
67
+
68
+ line
69
+ end
70
+ end
71
+
72
+ # Check if a test case at the given line range matches the line specification
73
+ # Test case line ranges are 0-based, user line specs are 1-based
74
+ def self.matches?(test_case, line_spec)
75
+ return true if line_spec.nil?
76
+
77
+ # Convert user's 1-based line spec to 0-based for comparison
78
+ zero_based_spec = case line_spec
79
+ when Integer
80
+ line_spec - 1
81
+ when Range
82
+ (line_spec.begin - 1)..(line_spec.end - 1)
83
+ else
84
+ line_spec
85
+ end
86
+
87
+ # Test case line_range is already 0-based
88
+ test_range = test_case.line_range
89
+
90
+ case zero_based_spec
91
+ when Integer
92
+ # Single line - check if it falls within the test's range
93
+ test_range.cover?(zero_based_spec)
94
+ when Range
95
+ # Range - check if there's any overlap
96
+ spec_start = zero_based_spec.begin
97
+ spec_end = zero_based_spec.end
98
+ test_start = test_range.begin
99
+ test_end = test_range.end
100
+
101
+ # Check for overlap: ranges overlap if start of one is before end of other
102
+ !(test_end < spec_start || spec_end < test_start)
103
+ else
104
+ true
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -123,6 +123,12 @@ class Tryouts
123
123
  options[:parallel_threads] = threads.to_i if threads && threads.to_i > 0
124
124
  end
125
125
 
126
+ opts.separator "\nParser Options:"
127
+ opts.on('--strict', 'Require explicit test descriptions (fail on unnamed tests)') { options[:strict] = true }
128
+ opts.on('--no-strict', 'Allow unnamed tests (legacy behavior)') { options[:strict] = false }
129
+ opts.on('-w', '--warnings', 'Show parser warnings (default: true)') { options[:warnings] = true }
130
+ opts.on('--no-warnings', 'Suppress parser warnings') { options[:warnings] = false }
131
+
126
132
  opts.separator "\nAgent-Optimized Output:"
127
133
  opts.on('-a', '--agent', 'Agent-optimized structured output for LLM context management') do
128
134
  options[:agent] = true
data/lib/tryouts/cli.rb CHANGED
@@ -4,6 +4,7 @@ require 'optparse'
4
4
 
5
5
  require_relative 'cli/opts'
6
6
  require_relative 'cli/formatters'
7
+ require_relative 'cli/line_spec_parser'
7
8
  require_relative 'test_runner'
8
9
 
9
10
  class Tryouts
@@ -13,6 +14,8 @@ class Tryouts
13
14
  framework: :direct,
14
15
  verbose: false,
15
16
  inspect: false,
17
+ strict: true, # Default to strict mode for better UX
18
+ warnings: true, # Default to showing warnings
16
19
  }
17
20
  end
18
21
 
@@ -22,11 +25,14 @@ class Tryouts
22
25
  output_manager = FormatterFactory.create_output_manager(@options)
23
26
 
24
27
  handle_version_flag(@options, output_manager)
25
- validate_files_exist(files, output_manager)
28
+
29
+ # Parse line specs from file arguments
30
+ files_with_specs = parse_file_specs(files)
31
+ validate_files_exist(files_with_specs, output_manager)
26
32
 
27
33
  runner = TestRunner.new(
28
- files: files,
29
- options: @options,
34
+ files: files_with_specs.keys,
35
+ options: @options.merge(file_line_specs: files_with_specs),
30
36
  output_manager: output_manager,
31
37
  )
32
38
 
@@ -42,13 +48,24 @@ class Tryouts
42
48
  exit 0
43
49
  end
44
50
 
45
- def validate_files_exist(files, output_manager)
46
- missing_files = files.reject { |file| File.exist?(file) }
51
+ def validate_files_exist(files_with_specs, output_manager)
52
+ missing_files = files_with_specs.keys.reject { |file| File.exist?(file) }
47
53
 
48
54
  unless missing_files.empty?
49
55
  missing_files.each { |file| output_manager.error("File not found: #{file}") }
50
56
  exit 1
51
57
  end
52
58
  end
59
+
60
+ def parse_file_specs(files)
61
+ files_with_specs = {}
62
+
63
+ files.each do |file_arg|
64
+ filepath, line_spec = LineSpecParser.parse(file_arg)
65
+ files_with_specs[filepath] = line_spec
66
+ end
67
+
68
+ files_with_specs
69
+ end
53
70
  end
54
71
  end
@@ -40,6 +40,21 @@ class Tryouts
40
40
  # Try to evaluate in test context first, then fallback to global context for constants
41
41
  begin
42
42
  expected_class = eval_expectation_content(@expectation.content, expectation_result)
43
+
44
+ # If the evaluated result is not a class/module (e.g., shadowed constant),
45
+ # fall back to global context resolution
46
+ unless expected_class.is_a?(Class) || expected_class.is_a?(Module)
47
+ # Attempt to get the constant from global context
48
+ # This handles cases where a local variable/method shadows a class constant
49
+ constant_name = @expectation.content.strip
50
+ # Ensure the constant name is valid before attempting const_get
51
+ if constant_name =~ /\A[A-Z][\w:]*\z/
52
+ expected_class = Object.const_get(constant_name)
53
+ else
54
+ # If it's not a valid constant name, the evaluation failed
55
+ raise TypeError, "Expected a Class or Module, got #{expected_class.class}"
56
+ end
57
+ end
43
58
  rescue NameError => e
44
59
  # If we can't find the constant in test context, try global context
45
60
  # This is common for exception classes like ArgumentError, StandardError, etc.
@@ -20,8 +20,15 @@ class Tryouts
20
20
 
21
21
  def process
22
22
  testrun = create_parser(@file, @options).parse
23
+
24
+ # Apply line spec filtering before reporting test counts
25
+ if @options[:line_spec]
26
+ testrun = filter_testrun_by_line_spec(testrun)
27
+ end
28
+
23
29
  @global_tally[:aggregator].increment_total_files
24
30
  @output_manager.file_parsed(@file, testrun.total_tests)
31
+ @output_manager.parser_warnings(@file, warnings: testrun.warnings)
25
32
 
26
33
  if @options[:inspect]
27
34
  handle_inspect_mode(testrun)
@@ -38,6 +45,34 @@ class Tryouts
38
45
 
39
46
  private
40
47
 
48
+ def filter_testrun_by_line_spec(testrun)
49
+ require_relative 'cli/line_spec_parser'
50
+
51
+ line_spec = @options[:line_spec]
52
+
53
+ # Filter test cases to only those that match the line spec
54
+ filtered_cases = testrun.test_cases.select do |test_case|
55
+ Tryouts::CLI::LineSpecParser.matches?(test_case, line_spec)
56
+ end
57
+
58
+ # Check if any tests matched the line specification
59
+ if filtered_cases.empty?
60
+ @output_manager.file_failure(@file, "No test cases found matching line specification: #{line_spec}")
61
+ return testrun # Return original testrun to avoid breaking the pipeline
62
+ end
63
+
64
+ # Create a new testrun with filtered cases
65
+ # We need to preserve the setup and teardown but only include matching tests
66
+ testrun.class.new(
67
+ setup: testrun.setup,
68
+ test_cases: filtered_cases,
69
+ teardown: testrun.teardown,
70
+ source_file: testrun.source_file,
71
+ metadata: testrun.metadata,
72
+ warnings: testrun.warnings
73
+ )
74
+ end
75
+
41
76
  def create_parser(file, options)
42
77
  parser_type = options[:parser] || :enhanced # enhanced parser is now the default
43
78
 
@@ -47,9 +82,9 @@ class Tryouts
47
82
 
48
83
  case parser_type
49
84
  when :enhanced
50
- EnhancedParser.new(file)
85
+ EnhancedParser.new(file, options)
51
86
  when :prism
52
- PrismParser.new(file)
87
+ PrismParser.new(file, options)
53
88
  end
54
89
  end
55
90
 
@@ -0,0 +1,26 @@
1
+ # lib/tryouts/parser_warning.rb
2
+
3
+ class Tryouts
4
+ # Data structure for parser warnings
5
+ ParserWarning = Data.define(:type, :message, :line_number, :context, :suggestion) do
6
+ def self.unnamed_test(line_number:, context:)
7
+ new(
8
+ type: :unnamed_test,
9
+ message: "Test case without explicit description",
10
+ line_number: line_number,
11
+ context: context,
12
+ suggestion: "Add a test description using '## Description' prefix"
13
+ )
14
+ end
15
+
16
+ def self.ambiguous_test_boundary(line_number:, context:)
17
+ new(
18
+ type: :ambiguous_boundary,
19
+ message: "Ambiguous test case boundary detected",
20
+ line_number: line_number,
21
+ context: context,
22
+ suggestion: "Use explicit '## Description' to clarify test structure"
23
+ )
24
+ end
25
+ end
26
+ end
@@ -3,6 +3,7 @@
3
3
  require 'prism'
4
4
 
5
5
  require_relative 'shared_methods'
6
+ require_relative '../parser_warning'
6
7
 
7
8
  class Tryouts
8
9
  # Fixed PrismParser with pattern matching for robust token filtering
@@ -10,12 +11,14 @@ class Tryouts
10
11
  class BaseParser
11
12
  include Tryouts::Parsers::SharedMethods
12
13
 
13
- def initialize(source_path)
14
+ def initialize(source_path, options = {})
14
15
  @source_path = source_path
15
16
  @source = File.read(source_path)
16
17
  @lines = @source.lines.map(&:chomp)
17
18
  @prism_result = Prism.parse(@source)
18
19
  @parsed_at = Time.now
20
+ @options = options
21
+ @warnings = []
19
22
  end
20
23
 
21
24
  end