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 +4 -4
- data/README.md +5 -8
- data/lib/tryouts/cli/formatters/agent.rb +58 -34
- data/lib/tryouts/cli/opts.rb +15 -15
- data/lib/tryouts/file_processor.rb +2 -2
- data/lib/tryouts/parser_warning.rb +10 -0
- data/lib/tryouts/parsers/CLAUDE.md +178 -0
- data/lib/tryouts/parsers/base_parser.rb +101 -1
- data/lib/tryouts/parsers/enhanced_parser.rb +177 -25
- data/lib/tryouts/parsers/legacy_parser.rb +254 -0
- data/lib/tryouts/parsers/shared_methods.rb +5 -1
- data/lib/tryouts/test_batch.rb +1 -1
- data/lib/tryouts/test_executor.rb +12 -4
- data/lib/tryouts/test_result_aggregator.rb +15 -11
- data/lib/tryouts/test_runner.rb +18 -15
- data/lib/tryouts/version.rb +1 -1
- data/lib/tryouts.rb +1 -1
- metadata +3 -2
- data/lib/tryouts/parsers/prism_parser.rb +0 -122
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3abd83819eeaeaaa533ccf639c975c295737c0241aaded166bda76e95ae0f127
|
4
|
+
data.tar.gz: 0f75679e487a62ef5be88d75c66f6c508efa7ce0867a42354a9e99aac1ea6142
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
####
|
139
|
+
#### TOPAZ: A Better Approach
|
143
140
|
|
144
|
-
Tryouts' `--agent` mode inspired the development of **
|
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,
|
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
|
-
#
|
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
|
10
|
+
# This formatter implements the TOPAZ v1.0 specification for structured,
|
11
11
|
# token-efficient test result communication.
|
12
12
|
#
|
13
|
-
#
|
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:
|
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,
|
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[:
|
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[:
|
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
|
-
|
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
|
-
|
282
|
-
|
283
|
-
|
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
|
-
|
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[:
|
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 << "
|
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 << "
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
377
|
-
|
378
|
-
|
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
|
-
#
|
408
|
-
|
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 << "
|
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
|
-
#
|
596
|
-
context_lines << " protocol:
|
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?
|
data/lib/tryouts/cli/opts.rb
CHANGED
@@ -10,20 +10,20 @@ class Tryouts
|
|
10
10
|
Minitest: Fresh context (each test isolated)
|
11
11
|
|
12
12
|
Examples:
|
13
|
-
try test_try.rb
|
14
|
-
try --rspec test_try.rb
|
15
|
-
try --direct --shared-context test_try.rb
|
16
|
-
try --generate-rspec test_try.rb
|
17
|
-
try --inspect test_try.rb
|
18
|
-
try --agent test_try.rb
|
19
|
-
try --agent --agent-limit 10000 tests/
|
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
|
23
|
-
--agent-focus summary
|
24
|
-
--agent-focus first-failure
|
25
|
-
--agent-focus critical
|
26
|
-
--agent-limit 1000
|
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
|
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/
|
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
|
-
|
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
|