tryouts 3.5.1 → 3.6.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: ab0eb9c341b10dd5e505a3f62cc33a19a393cb294aecd756809b19c2484784f1
4
- data.tar.gz: cddfd1a827b0ef37a9e69224124f2f696bbd8c2f098edbfce79b563b3017fbcd
3
+ metadata.gz: 3abd83819eeaeaaa533ccf639c975c295737c0241aaded166bda76e95ae0f127
4
+ data.tar.gz: 0f75679e487a62ef5be88d75c66f6c508efa7ce0867a42354a9e99aac1ea6142
5
5
  SHA512:
6
- metadata.gz: 066b7697b04b3501f01a0994e9d10a66c5a94d593bdfa5cb1b618bb1264f228425bd7d8b4d98cf9a555a5e23bbe002f137c943d5195ad4ce972de30f03dfac9c
7
- data.tar.gz: 82390327e8da8e35d0c9eaa4557800b09bba76e065704f9d704acab45ab24dd62e97edc7aa54d8104ee50cab31a9dab321c75449386f70243674c8f2d838716a
6
+ metadata.gz: 7f389b6b637cb53cfeb7bd9d20ecd4e8d1392329ec1ec89b390d1f2eb2e0f43c2fda1d503422819b7694d13d867f81b0fa2a0bf644ab60f75004fc8ce92376ab
7
+ data.tar.gz: 0f72eef3666c7391e4cdfafa6f619878d04a685f7b77d6339de6f757940649b6c0e54f8581738592941f92beab11001b185da4869382544803c51d4acac5bdf1
data/README.md CHANGED
@@ -132,18 +132,15 @@ try --agent --agent-limit 1000 # limit output to 1000 tokens
132
132
 
133
133
  #### Why Not Pipe Test Output Directly to AI?
134
134
 
135
- Raw test output creates several problems when working with AI assistants:
135
+ I mean, you could. If that already works well, you could probably still benefit from an agent that is able to focus on the critical information for the task. And the extra context window space.
136
136
 
137
- - **Token bloat**: Verbose formatting wastes 60-80% of your context window on styling
138
- - **Signal vs noise**: Important failures get buried in passing test details and framework boilerplate
139
- - **Inconsistent parsing**: AI struggles with varying output formats across different test runs
140
- - **Context overflow**: Large test suites exceed AI token limits, truncating critical information
137
+ Raw test output creates problems when working with AI assistants: high token usage with inconsistent parsing across different runs, where the same logical failure might be interpreted differently, making it difficult to reliably produce and analyze results consistently.
141
138
 
142
- #### TOPA: A Better Approach
139
+ #### TOPAZ: A Better Approach
143
140
 
144
- Tryouts' `--agent` mode inspired the development of **TOPA (Test Output Protocol for AI)** - a standardized format optimized for AI analysis. The [tpane](https://github.com/delano/tpane) tool implements this protocol, transforming any test framework's output into structured, token-efficient formats.
141
+ Tryouts' `--agent` mode inspired the development of **TOPAZ (Test Output Protocol for AI Zealots)** - a standardized format optimized for AI analysis. The [tpane](https://github.com/delano/tpane) tool implements this protocol, transforming any test framework's output into structured, token-efficient formats.
145
142
 
146
- Instead of overwhelming AI with raw output, TOPA provides clean semantic data focusing on what actually needs attention - failures, errors, and actionable context.
143
+ Instead of overwhelming AI with raw output, TOPAZ provides clean semantic data focusing on what actually needs attention - failures, errors, and actionable context.
147
144
 
148
145
  ### Exit Codes
149
146
 
@@ -4,13 +4,13 @@ require_relative 'token_budget'
4
4
 
5
5
  class Tryouts
6
6
  class CLI
7
- # TOPA (Test Output Protocol for AI) Formatter
7
+ # TOPAZ (Test Output Protocol for AI Zealots) Formatter
8
8
  #
9
9
  # Language-agnostic test output format designed for LLM context management.
10
- # This formatter implements the TOPA v1.0 specification for structured,
10
+ # This formatter implements the TOPAZ v1.0 specification for structured,
11
11
  # token-efficient test result communication.
12
12
  #
13
- # TOPA Features:
13
+ # TOPAZ Features:
14
14
  # - Language-agnostic field naming (snake_case, hierarchical)
15
15
  # - Standardized execution context (runtime, environment, VCS)
16
16
  # - Token budget awareness with smart truncation
@@ -27,7 +27,7 @@ class Tryouts
27
27
  # - environment: Normalized env vars (ci_system, app_env, etc.)
28
28
  # - test_framework: Framework name, isolation mode, parser
29
29
  # - execution_flags: Runtime flags in normalized form
30
- # - protocol: TOPA version and configuration
30
+ # - protocol: TOPAZ version and configuration
31
31
  # - project: Auto-detected project type
32
32
  # - test_discovery: File pattern matching rules
33
33
  #
@@ -49,7 +49,7 @@ class Tryouts
49
49
  @focus_mode = options[:agent_focus] || :failures
50
50
  @collected_files = []
51
51
  @current_file_data = nil
52
- @total_stats = { files: 0, tests: 0, failures: 0, errors: 0, elapsed: 0 }
52
+ @total_stats = { files: 0, tests: 0, failures: 0, errors: 0, elapsed_time: 0 }
53
53
  @output_rendered = false
54
54
  @options = options # Store all options for execution context display
55
55
  @all_warnings = [] # Store warnings globally for execution details
@@ -120,7 +120,7 @@ class Tryouts
120
120
  # Always update global totals
121
121
  @total_stats[:failures] += failed_count
122
122
  @total_stats[:errors] += error_count
123
- @total_stats[:elapsed] += elapsed_time if elapsed_time
123
+ @total_stats[:elapsed_time] += elapsed_time if elapsed_time
124
124
 
125
125
  # Update per-file data - file_result is called AFTER file_end, so data is in @collected_files
126
126
  relative_file_path = relative_path(file_path)
@@ -175,7 +175,7 @@ class Tryouts
175
175
  error_count: @collected_files.sum { |f| f[:errors].size },
176
176
  successful_files: @collected_files.size - @collected_files.count { |f| f[:failures].any? || f[:errors].any? },
177
177
  total_files: @collected_files.size,
178
- elapsed_time: @total_stats[:elapsed]
178
+ elapsed_time: @total_stats[:elapsed_time]
179
179
  ) unless @output_rendered
180
180
  end
181
181
 
@@ -188,7 +188,7 @@ class Tryouts
188
188
  errors: error_count,
189
189
  successful_files: successful_files,
190
190
  total_files: total_files,
191
- elapsed: elapsed_time
191
+ elapsed_time: elapsed_time,
192
192
  )
193
193
 
194
194
  # Now render all collected data
@@ -278,9 +278,11 @@ class Tryouts
278
278
  def render_summary_only
279
279
  output = []
280
280
 
281
- # Add execution context header for agent clarity
282
- output << render_execution_context
283
- output << ""
281
+ time_str = if @total_stats[:elapsed_time] < 2.0
282
+ " (#{(@total_stats[:elapsed_time] * 1000).round}ms)"
283
+ else
284
+ " (#{@total_stats[:elapsed_time].round(2)}s)"
285
+ end
284
286
 
285
287
  # Count failures manually from collected file data (same as other render methods)
286
288
  failed_count = @collected_files.sum { |f| f[:failures].size }
@@ -293,13 +295,15 @@ class Tryouts
293
295
  details = []
294
296
  details << "#{failed_count} failed" if failed_count > 0
295
297
  details << "#{error_count} errors" if error_count > 0
296
- status_parts << "FAIL: #{issues_count}/#{@total_stats[:tests]} tests (#{details.join(', ')}, #{passed_count} passed)"
298
+ summary = "#{passed_count} testcases passed, #{failed_count} failed"
299
+ summary += ", #{error_count} errors" if error_count > 0
300
+ status_parts << "SUMMARY: #{summary}#{time_str}"
297
301
  else
298
302
  # Agent doesn't need output in the positive case (i.e. for passing
299
303
  # tests). It just fills out the context window.
300
304
  end
301
305
 
302
- status_parts << "(#{format_time(@total_stats[:elapsed])})" if @total_stats[:elapsed]
306
+ status_parts << "(#{format_time(@total_stats[:elapsed_time])})" if @total_stats[:elapsed_time]
303
307
 
304
308
  output << status_parts.join(" ")
305
309
 
@@ -308,14 +312,14 @@ class Tryouts
308
312
 
309
313
  files_with_issues = @collected_files.select { |f| f[:failures].any? || f[:errors].any? }
310
314
  if files_with_issues.any?
311
- output << "Files:"
315
+ output << "FILES:"
312
316
  files_with_issues.each do |file_data|
313
317
  issue_count = file_data[:failures].size + file_data[:errors].size
314
318
  output << " #{file_data[:path]}: #{issue_count} issue#{'s' if issue_count != 1}"
315
319
  end
316
320
  elsif @collected_files.any?
317
321
  # Show files that were processed successfully
318
- output << "Files:"
322
+ output << "FILES:"
319
323
  @collected_files.each do |file_data|
320
324
  # Use the passed count from file_result if available, otherwise calculate
321
325
  passed_tests = file_data[:passed] ||
@@ -324,6 +328,10 @@ class Tryouts
324
328
  end
325
329
  end
326
330
 
331
+ # Add execution context at the end
332
+ output << ""
333
+ output << render_execution_context
334
+
327
335
  puts output.join("\n") if output.any?
328
336
  end
329
337
 
@@ -331,21 +339,29 @@ class Tryouts
331
339
  # Only show errors (exceptions), skip assertion failures
332
340
  critical_files = @collected_files.select { |f| f[:errors].any? }
333
341
 
334
- output = []
342
+ time_str = if @total_stats[:elapsed_time] < 2.0
343
+ " (#{(@total_stats[:elapsed_time] * 1000).round}ms)"
344
+ else
345
+ " (#{@total_stats[:elapsed_time].round(2)}s)"
346
+ end
335
347
 
336
- # Add execution context header for agent clarity
337
- output << render_execution_context
338
- output << ""
348
+ output = []
339
349
 
340
350
  if critical_files.empty?
341
351
  output << "No critical errors found"
352
+ # Add execution context at the end
353
+ output << ""
354
+ output << render_execution_context
342
355
  puts output.join("\n")
343
356
  return
344
357
  end
345
358
 
346
- output << "CRITICAL: #{critical_files.size} file#{'s' if critical_files.size != 1} with errors"
359
+ # Summary first
360
+ output << "SUMMARY:"
361
+ output << "#{critical_files.size} file#{'s' if critical_files.size != 1} with critical errors#{time_str}"
347
362
  output << ""
348
363
 
364
+ # Error details
349
365
  critical_files.each do |file_data|
350
366
  unless @budget.has_budget?
351
367
  output << "... (truncated due to token limit)"
@@ -367,15 +383,20 @@ class Tryouts
367
383
  output << ""
368
384
  end
369
385
 
386
+ # Add execution context at the end
387
+ output << render_execution_context
388
+
370
389
  puts output.join("\n")
371
390
  end
372
391
 
373
392
  def render_full_structured
374
393
  output = []
375
394
 
376
- # Add execution context header for agent clarity
377
- output << render_execution_context
378
- output << ""
395
+ time_str = if @total_stats[:elapsed_time] < 2.0
396
+ " (#{(@total_stats[:elapsed_time] * 1000).round}ms)"
397
+ else
398
+ " (#{@total_stats[:elapsed_time].round(2)}s)"
399
+ end
379
400
 
380
401
  # Count actual failures from collected data
381
402
  failed_count = @collected_files.sum { |f| f[:failures].size }
@@ -383,6 +404,14 @@ class Tryouts
383
404
  issues_count = failed_count + error_count
384
405
  passed_count = [@total_stats[:tests] - issues_count, 0].max
385
406
 
407
+ # Summary first
408
+ output << "SUMMARY:"
409
+ summary = "#{passed_count} testcases passed, #{failed_count} failed"
410
+ summary += ", #{error_count} errors" if error_count > 0
411
+ summary += " in #{@total_stats[:files]} files#{time_str}"
412
+ output << summary
413
+ output << ""
414
+
386
415
  # Show files with issues only
387
416
  files_with_issues = @collected_files.select { |f| f[:failures].any? || f[:errors].any? }
388
417
 
@@ -404,13 +433,8 @@ class Tryouts
404
433
  output << ""
405
434
  end
406
435
 
407
- # Final summary line
408
- summary = "Summary: \n"
409
- summary += "#{passed_count} testcases passed, #{failed_count} failed"
410
- summary += ", #{error_count} errors" if error_count > 0
411
- summary += " in #{@total_stats[:files]} files"
412
-
413
- output << summary
436
+ # Add execution context at the end
437
+ output << render_execution_context
414
438
 
415
439
  puts output.join("\n")
416
440
  end
@@ -535,7 +559,7 @@ class Tryouts
535
559
 
536
560
  def render_execution_context
537
561
  context_lines = []
538
- context_lines << "EXECUTION_CONTEXT:"
562
+ context_lines << "CONTEXT:"
539
563
 
540
564
  # Command that was executed
541
565
  if @options[:original_command]
@@ -548,7 +572,7 @@ class Tryouts
548
572
 
549
573
  # Runtime - compact format
550
574
  platform = RUBY_PLATFORM.gsub(/darwin\d+/, 'darwin') # Simplify darwin25 -> darwin
551
- context_lines << " runtime: ruby #{RUBY_VERSION} (#{platform})"
575
+ context_lines << " runtime: ruby #{RUBY_VERSION} (#{platform}); tryouts #{Tryouts::VERSION}"
552
576
 
553
577
  # Package manager - only if present, compact format
554
578
  if defined?(Bundler)
@@ -592,8 +616,8 @@ class Tryouts
592
616
  context_lines << " flags: #{flags.join(', ')}"
593
617
  end
594
618
 
595
- # TOPA protocol - compact
596
- context_lines << " protocol: TOPA v1.0 | focus: #{@focus_mode} | limit: #{@budget.limit}"
619
+ # TOPAZ protocol - compact
620
+ context_lines << " protocol: TOPAZ v0.3 | focus: #{@focus_mode} | limit: #{@budget.limit}"
597
621
 
598
622
  # File count being tested
599
623
  if @collected_files && @collected_files.any?
@@ -10,20 +10,20 @@ class Tryouts
10
10
  Minitest: Fresh context (each test isolated)
11
11
 
12
12
  Examples:
13
- try test_try.rb # Tryouts test runner with shared context
14
- try --rspec test_try.rb # RSpec with fresh context
15
- try --direct --shared-context test_try.rb # Explicit shared context
16
- try --generate-rspec test_try.rb # Output RSpec code only
17
- try --inspect test_try.rb # Inspect file structure and validation
18
- try --agent test_try.rb # Agent-optimized structured output
19
- try --agent --agent-limit 10000 tests/ # Agent mode with 10K token limit
13
+ try test_try.rb # Tryouts test runner with shared context
14
+ try --rspec test_try.rb # RSpec with fresh context
15
+ try --direct --shared-context test_try.rb # Explicit shared context
16
+ try --generate-rspec test_try.rb # Output RSpec code only
17
+ try --inspect test_try.rb # Inspect file structure and validation
18
+ try --agent test_try.rb # Agent-optimized structured output
19
+ try --agent --agent-limit 10000 tests/ # Agent mode with 10K token limit
20
20
 
21
21
  Agent Output Modes:
22
- --agent # Structured, token-efficient output
23
- --agent-focus summary # Show counts and problem files only
24
- --agent-focus first-failure # Show first failure per file
25
- --agent-focus critical # Show errors/exceptions only
26
- --agent-limit 1000 # Limit output to 1000 tokens
22
+ --agent # Structured, token-efficient output
23
+ --agent-focus summary # Show counts and problem files only
24
+ --agent-focus first-failure # Show first failure per file
25
+ --agent-focus critical # Show errors/exceptions only
26
+ --agent-limit 1000 # Limit output to 1000 tokens
27
27
 
28
28
  File Naming & Organization:
29
29
  Files must end with '_try.rb' or '.try.rb' (e.g., auth_service_try.rb, user_model.try.rb)
@@ -37,7 +37,7 @@ class Tryouts
37
37
  #=> true # this is the expected result
38
38
 
39
39
  File Structure (3 sections):
40
- # Setup section (optional) - runs once before all tests
40
+ # Setup section (optional) - code before first testcase runs once before all tests
41
41
  @shared_var = "available to all test cases"
42
42
 
43
43
  ## TEST: Feature description
@@ -45,9 +45,9 @@ class Tryouts
45
45
  result = some_operation()
46
46
  #=> expected_value
47
47
 
48
- # Teardown section (optional) - runs once after all tests
48
+ # Teardown section (optional) - code after last testcase runs once after all tests
49
49
 
50
- Context Guidelines:
50
+ Execution Context:
51
51
  Shared Context (default): Instance variables persist across test cases
52
52
  - Use for: Integration testing, stateful scenarios, realistic workflows
53
53
  - Caution: Test order matters, state accumulates
@@ -1,6 +1,6 @@
1
1
  # lib/tryouts/file_processor.rb
2
2
 
3
- require_relative 'parsers/prism_parser'
3
+ require_relative 'parsers/legacy_parser'
4
4
  require_relative 'parsers/enhanced_parser'
5
5
  require_relative 'test_executor'
6
6
  require_relative 'cli/modes/inspect'
@@ -84,7 +84,7 @@ class Tryouts
84
84
  when :enhanced
85
85
  EnhancedParser.new(file, options)
86
86
  when :prism
87
- PrismParser.new(file, options)
87
+ LegacyParser.new(file, options)
88
88
  end
89
89
  end
90
90
 
@@ -22,5 +22,15 @@ class Tryouts
22
22
  suggestion: "Use explicit '## Description' to clarify test structure"
23
23
  )
24
24
  end
25
+
26
+ def self.malformed_expectation(line_number:, syntax:, context:)
27
+ new(
28
+ type: :malformed_expectation,
29
+ message: "Malformed expectation syntax '#=#{syntax}>' at line #{line_number}",
30
+ line_number: line_number,
31
+ context: context,
32
+ suggestion: "Use valid expectation syntax like #=>, #==>, #=:>, #=!>, etc."
33
+ )
34
+ end
25
35
  end
26
36
  end
@@ -0,0 +1,178 @@
1
+ In Ruby 3.4+, `case/when` and `case/in` represent fundamentally different approaches to conditional logic:
2
+
3
+ ## `case/when` - Traditional Equality Matching
4
+
5
+ Uses the `===` operator for comparison. Simple and straightforward:
6
+
7
+ ```ruby
8
+ def classify_response(status)
9
+ case status
10
+ when 200..299
11
+ "success"
12
+ when 400..499
13
+ "client_error"
14
+ when 500..599
15
+ "server_error"
16
+ when String
17
+ "string_status"
18
+ else
19
+ "unknown"
20
+ end
21
+ end
22
+ ```
23
+
24
+ ## `case/in` - Pattern Matching with Destructuring
25
+
26
+ Matches structure and binds variables. Much more powerful:
27
+
28
+ ```ruby
29
+ def process_api_response(response)
30
+ case response
31
+ in { status: 200, data: { user: { name: String => name, age: Integer => age } } }
32
+ "User #{name} is #{age} years old"
33
+
34
+ in { status: 200, data: Array => items } if items.length > 10
35
+ "Got #{items.length} items"
36
+
37
+ in { status: 400..499, error: { message: msg } }
38
+ "Client error: #{msg}"
39
+
40
+ in { status: 500.. }
41
+ "Server error occurred"
42
+
43
+ in nil | {}
44
+ "Empty response"
45
+ else
46
+ "Unexpected response format"
47
+ end
48
+ end
49
+ ```
50
+
51
+ ## Key Differences
52
+
53
+ ### 1. **Variable Binding**
54
+
55
+ ```ruby
56
+ # case/when - no binding
57
+ case user
58
+ when Hash
59
+ puts user[:name] # Must access manually
60
+ end
61
+
62
+ # case/in - automatic binding
63
+ case user
64
+ in { name: String => username, age: } # 'age' variable created automatically
65
+ puts username # Bound variable available
66
+ end
67
+ ```
68
+
69
+ ### 2. **Structural Matching**
70
+
71
+ ```ruby
72
+ # case/when - only surface comparison
73
+ case data
74
+ when Array
75
+ # Know it's an array, but not its contents
76
+ end
77
+
78
+ # case/in - deep structure matching
79
+ case data
80
+ in [first, *middle, last] if middle.length > 2
81
+ # Automatically destructured with guard condition
82
+ end
83
+ ```
84
+
85
+ ### 3. **Guard Conditions**
86
+
87
+ ```ruby
88
+ # case/when - separate if needed
89
+ case number
90
+ when Integer
91
+ if number > 100
92
+ "big integer"
93
+ else
94
+ "small integer"
95
+ end
96
+ end
97
+
98
+ # case/in - integrated guards
99
+ case number
100
+ in Integer => n if n > 100
101
+ "big integer"
102
+ in Integer
103
+ "small integer"
104
+ end
105
+ ```
106
+
107
+ ## Practical Example for Tryouts
108
+
109
+ For parsing tryout lines, here's the difference:
110
+
111
+ ### Traditional `case/when`
112
+ ```ruby
113
+ def parse_line(line)
114
+ case line
115
+ when /^##\s*(.+)/
116
+ [:description, $1.strip]
117
+ when /^#=>\s*(.+)/
118
+ [:expectation, $1.strip]
119
+ when /^#=\?>\s*(.+)/
120
+ [:debug_info, $1.strip]
121
+ else
122
+ [:code, line]
123
+ end
124
+ end
125
+ ```
126
+
127
+ ### Pattern Matching `case/in`
128
+ ```ruby
129
+ def parse_line(line)
130
+ case line
131
+ in /^##\s*(.+)/ => description
132
+ [:description, description.strip]
133
+ in /^#=>\s*(.+)/ => expectation
134
+ [:expectation, expectation.strip]
135
+ in /^#=\?>\s*(.+)/ => debug_expr
136
+ [:debug_info, debug_expr.strip]
137
+ in /^\s*$/
138
+ [:blank]
139
+ else
140
+ [:code, line]
141
+ end
142
+ end
143
+ ```
144
+
145
+ ## When to Use Which
146
+
147
+ ### Use `case/when` for:
148
+ - Simple value comparisons
149
+ - Class/type checking
150
+ - Range matching
151
+ - Traditional switch-like logic
152
+
153
+ ### Use `case/in` for:
154
+ - Complex data structure matching
155
+ - When you need variable binding
156
+ - Guard conditions
157
+ - Destructuring arrays/hashes
158
+ - Multiple conditions per branch
159
+
160
+ ## Ruby 3.4+ Enhancements
161
+
162
+ Ruby 3.4 added several pattern matching improvements:
163
+
164
+ ```ruby
165
+ # Variable binding in array patterns
166
+ case data
167
+ in [String => first, *String => middle, String => last]
168
+ # All string array with bound variables
169
+ end
170
+
171
+ # Hash patterns with rest
172
+ case config
173
+ in { required: true, **rest } if rest.keys.all? { |k| k.is_a?(Symbol) }
174
+ # Required config with symbol keys only
175
+ end
176
+ ```
177
+
178
+ For the Tryouts modernization, `case/in` provides cleaner syntax for parsing complex comment patterns while binding the captured content directly to variables, eliminating the need for global match variables like `$1`.
@@ -6,11 +6,89 @@ require_relative 'shared_methods'
6
6
  require_relative '../parser_warning'
7
7
 
8
8
  class Tryouts
9
- # Fixed PrismParser with pattern matching for robust token filtering
10
9
  module Parsers
10
+ # Base class for all tryout parsers providing common functionality
11
+ #
12
+ # BaseParser establishes the foundation for parsing tryout files by handling
13
+ # file loading, Prism integration, and providing shared parsing infrastructure.
14
+ # All concrete parser implementations (EnhancedParser, LegacyParser) inherit
15
+ # from this class.
16
+ #
17
+ # @abstract Subclass and implement {#parse} to create a concrete parser
18
+ # @example Implementing a custom parser
19
+ # class MyCustomParser < Tryouts::Parsers::BaseParser
20
+ # def parse
21
+ # # Your parsing logic here
22
+ # # Must return a Tryouts::Testrun object
23
+ # end
24
+ #
25
+ # private
26
+ #
27
+ # def parser_type
28
+ # :custom
29
+ # end
30
+ # end
31
+ #
32
+ # @!attribute [r] source_path
33
+ # @return [String] Path to the source file being parsed
34
+ # @!attribute [r] source
35
+ # @return [String] Raw source code content
36
+ # @!attribute [r] lines
37
+ # @return [Array<String>] Source lines with line endings removed
38
+ # @!attribute [r] prism_result
39
+ # @return [Prism::ParseResult] Result of parsing source with Prism
40
+ # @!attribute [r] parsed_at
41
+ # @return [Time] Timestamp when parsing was initiated
42
+ # @!attribute [r] options
43
+ # @return [Hash] Parser configuration options
44
+ # @!attribute [r] warnings
45
+ # @return [Array<Tryouts::ParserWarning>] Collection of parsing warnings
46
+ #
47
+ # ## Shared Functionality
48
+ #
49
+ # ### 1. File and Source Management
50
+ # - Automatic file reading and line splitting
51
+ # - UTF-8 encoding handling
52
+ # - Path normalization and validation
53
+ #
54
+ # ### 2. Prism Integration
55
+ # - Automatic Prism parsing of source code
56
+ # - Syntax error detection and handling
57
+ # - AST access for advanced parsing needs
58
+ #
59
+ # ### 3. Warning System
60
+ # - Centralized warning collection and management
61
+ # - Type-safe warning objects with context
62
+ # - Integration with output formatters
63
+ #
64
+ # ### 4. Shared Methods
65
+ # - Token grouping and classification logic
66
+ # - Test case boundary detection
67
+ # - Common utility methods for all parsers
68
+ #
69
+ # ## Parser Requirements
70
+ #
71
+ # Concrete parser implementations must:
72
+ # 1. Implement the abstract `parse` method
73
+ # 2. Return a `Tryouts::Testrun` object
74
+ # 3. Handle syntax errors appropriately
75
+ # 4. Provide a unique `parser_type` identifier
76
+ #
77
+ # @see EnhancedParser For Prism-based comment extraction
78
+ # @see LegacyParser For line-by-line parsing approach
79
+ # @see SharedMethods For common parsing utilities
80
+ # @since 3.0.0
11
81
  class BaseParser
12
82
  include Tryouts::Parsers::SharedMethods
13
83
 
84
+ # Initialize a new parser instance
85
+ #
86
+ # @param source_path [String] Absolute path to the tryout source file
87
+ # @param options [Hash] Configuration options for parsing behavior
88
+ # @option options [Boolean] :strict Enable strict mode validation
89
+ # @option options [Boolean] :warnings Enable warning collection (default: true)
90
+ # @raise [Errno::ENOENT] If source file doesn't exist
91
+ # @raise [Errno::EACCES] If source file isn't readable
14
92
  def initialize(source_path, options = {})
15
93
  @source_path = source_path
16
94
  @source = File.read(source_path)
@@ -21,6 +99,28 @@ class Tryouts
21
99
  @warnings = []
22
100
  end
23
101
 
102
+ # Parse the source file into structured test data
103
+ #
104
+ # @abstract Subclasses must implement this method
105
+ # @return [Tryouts::Testrun] Parsed test structure with setup, tests, teardown, and warnings
106
+ # @raise [NotImplementedError] If called directly on BaseParser
107
+ def parse
108
+ raise NotImplementedError, "Subclasses must implement #parse"
109
+ end
110
+
111
+ protected
112
+
113
+ # Get the parser type identifier
114
+ #
115
+ # @abstract Subclasses should override to provide unique identifier
116
+ # @return [Symbol] Parser type identifier
117
+ def parser_type
118
+ :base
119
+ end
120
+
121
+ # Access to instance variables for subclasses
122
+ attr_reader :source_path, :source, :lines, :prism_result, :parsed_at, :options
123
+
24
124
  end
25
125
  end
26
126
  end