tryouts 3.0.0 → 3.1.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +51 -115
  3. data/exe/try +25 -4
  4. data/lib/tryouts/cli/formatters/base.rb +33 -21
  5. data/lib/tryouts/cli/formatters/compact.rb +122 -84
  6. data/lib/tryouts/cli/formatters/factory.rb +1 -1
  7. data/lib/tryouts/cli/formatters/output_manager.rb +13 -2
  8. data/lib/tryouts/cli/formatters/quiet.rb +22 -16
  9. data/lib/tryouts/cli/formatters/verbose.rb +102 -61
  10. data/lib/tryouts/console.rb +53 -17
  11. data/lib/tryouts/expectation_evaluators/base.rb +101 -0
  12. data/lib/tryouts/expectation_evaluators/boolean.rb +60 -0
  13. data/lib/tryouts/expectation_evaluators/exception.rb +61 -0
  14. data/lib/tryouts/expectation_evaluators/expectation_result.rb +67 -0
  15. data/lib/tryouts/expectation_evaluators/false.rb +60 -0
  16. data/lib/tryouts/expectation_evaluators/intentional_failure.rb +74 -0
  17. data/lib/tryouts/expectation_evaluators/output.rb +101 -0
  18. data/lib/tryouts/expectation_evaluators/performance_time.rb +81 -0
  19. data/lib/tryouts/expectation_evaluators/regex_match.rb +57 -0
  20. data/lib/tryouts/expectation_evaluators/registry.rb +66 -0
  21. data/lib/tryouts/expectation_evaluators/regular.rb +67 -0
  22. data/lib/tryouts/expectation_evaluators/result_type.rb +51 -0
  23. data/lib/tryouts/expectation_evaluators/true.rb +58 -0
  24. data/lib/tryouts/prism_parser.rb +221 -20
  25. data/lib/tryouts/test_batch.rb +556 -0
  26. data/lib/tryouts/test_case.rb +192 -0
  27. data/lib/tryouts/test_executor.rb +7 -5
  28. data/lib/tryouts/test_runner.rb +2 -2
  29. data/lib/tryouts/translators/minitest_translator.rb +78 -11
  30. data/lib/tryouts/translators/rspec_translator.rb +85 -12
  31. data/lib/tryouts/version.rb +1 -1
  32. data/lib/tryouts.rb +43 -1
  33. metadata +18 -5
  34. data/lib/tryouts/testbatch.rb +0 -314
  35. data/lib/tryouts/testcase.rb +0 -51
@@ -0,0 +1,58 @@
1
+ # lib/tryouts/expectation_evaluators/true.rb
2
+
3
+ require_relative 'base'
4
+
5
+ class Tryouts
6
+ module ExpectationEvaluators
7
+ # Evaluator for boolean true expectations using syntax: #==> expression
8
+ #
9
+ # PURPOSE:
10
+ # - Validates that an expression evaluates to exactly true (not truthy)
11
+ # - Provides explicit boolean validation for documentation-style tests
12
+ # - Distinguishes between true/false and truthy/falsy values
13
+ #
14
+ # SYNTAX: #==> boolean_expression
15
+ # Examples:
16
+ # [1, 2, 3] #==> result.length == 3 # Pass: expression is true
17
+ # [1, 2, 3] #==> result.include?(2) # Pass: expression is true
18
+ # [] #==> result.empty? # Pass: expression is true
19
+ # [1, 2, 3] #==> result.empty? # Fail: expression is false
20
+ # [1, 2, 3] #==> result.length # Fail: expression is 3 (truthy but not true)
21
+ #
22
+ # BOOLEAN STRICTNESS:
23
+ # - Only passes when expression evaluates to exactly true (not truthy)
24
+ # - Fails for false, nil, 0, "", [], {}, or any non-true value
25
+ # - Uses Ruby's === comparison for exact boolean matching
26
+ # - Encourages explicit boolean expressions in documentation
27
+ #
28
+ # IMPLEMENTATION DETAILS:
29
+ # - Expression has access to `result` and `_` variables (actual_result)
30
+ # - Expected display shows 'true (exactly)' for clarity
31
+ # - Actual display shows the evaluated expression result
32
+ # - Distinguishes from regular expectations through strict true matching
33
+ #
34
+ # DESIGN DECISIONS:
35
+ # - Strict true matching prevents accidental truthy value acceptance
36
+ # - Clear expected display explains the exact requirement
37
+ # - Expression evaluation provides flexible boolean logic testing
38
+ # - Part of unified #= prefix convention for all expectation types
39
+ class True < Base
40
+ def self.handles?(expectation_type)
41
+ expectation_type == :true # rubocop:disable Lint/BooleanSymbol
42
+ end
43
+
44
+ def evaluate(actual_result = nil)
45
+ expectation_result = ExpectationResult.from_result(actual_result)
46
+ expression_result = eval_expectation_content(@expectation.content, expectation_result)
47
+
48
+ build_result(
49
+ passed: expression_result == true,
50
+ actual: expression_result,
51
+ expected: true,
52
+ )
53
+ rescue StandardError => ex
54
+ handle_evaluation_error(ex, actual_result)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,7 +1,7 @@
1
1
  # Modern Ruby 3.4+ solution for the teardown bug
2
2
 
3
3
  require 'prism'
4
- require_relative 'testcase'
4
+ require_relative 'test_case'
5
5
 
6
6
  class Tryouts
7
7
  # Fixed PrismParser with pattern matching for robust token filtering
@@ -16,8 +16,10 @@ class Tryouts
16
16
  def parse
17
17
  return handle_syntax_errors if @prism_result.failure?
18
18
 
19
- tokens = tokenize_content
20
- test_blocks = group_into_test_blocks(tokens)
19
+ tokens = tokenize_content
20
+ test_boundaries = find_test_case_boundaries(tokens)
21
+ tokens = classify_potential_descriptions_with_boundaries(tokens, test_boundaries)
22
+ test_blocks = group_into_test_blocks(tokens)
21
23
  process_test_blocks(test_blocks)
22
24
  end
23
25
 
@@ -33,7 +35,25 @@ class Tryouts
33
35
  { type: :description, content: $1.strip, line: index }
34
36
  in /^#\s*TEST\s*\d*:\s*(.*)$/ # rubocop:disable Lint/DuplicateBranch
35
37
  { type: :description, content: $1.strip, line: index }
36
- in /^#\s*=>\s*(.*)$/ # Expectation
38
+ in /^#\s*=!>\s*(.*)$/ # Exception expectation (updated for consistency)
39
+ { type: :exception_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
40
+ in /^#\s*=<>\s*(.*)$/ # Intentional failure expectation
41
+ { type: :intentional_failure_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
42
+ in /^#\s*==>\s*(.*)$/ # Boolean true expectation
43
+ { type: :true_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
44
+ in %r{^#\s*=/=>\s*(.*)$} # Boolean false expectation
45
+ { type: :false_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
46
+ in /^#\s*=\|>\s*(.*)$/ # Boolean (true or false) expectation
47
+ { type: :boolean_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
48
+ in /^#\s*=:>\s*(.*)$/ # Result type expectation
49
+ { type: :result_type_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
50
+ in /^#\s*=~>\s*(.*)$/ # Regex match expectation
51
+ { type: :regex_match_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
52
+ in /^#\s*=%>\s*(.*)$/ # Performance time expectation
53
+ { type: :performance_time_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
54
+ in /^#\s*=(\d+)>\s*(.*)$/ # Output expectation (stdout/stderr with pipe number)
55
+ { type: :output_expectation, content: $2.strip, pipe: $1.to_i, line: index, ast: parse_expectation($2.strip) }
56
+ in /^#\s*=>\s*(.*)$/ # Regular expectation
37
57
  { type: :expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
38
58
  in /^##\s*=>\s*(.*)$/ # Commented out expectation (should be ignored)
39
59
  { type: :comment, content: '=>' + $1.strip, line: index }
@@ -48,27 +68,146 @@ class Tryouts
48
68
  tokens << token
49
69
  end
50
70
 
51
- # Post-process to convert potential_descriptions to descriptions or comments
52
- classify_potential_descriptions(tokens)
71
+ # Return tokens with potential_descriptions - they'll be classified later with test boundaries
72
+ tokens
73
+ end
74
+
75
+ # Find actual test case boundaries by looking for ## descriptions or # TEST: patterns
76
+ # followed by code and expectations
77
+ def find_test_case_boundaries(tokens)
78
+ boundaries = []
79
+
80
+ tokens.each_with_index do |token, index|
81
+ # Look for explicit test descriptions (## or # TEST:)
82
+ if token[:type] == :description
83
+ # Find the end of this test case by looking for the last expectation
84
+ # before the next description or end of file
85
+ start_line = token[:line]
86
+ end_line = find_test_case_end(tokens, index)
87
+
88
+ boundaries << { start: start_line, end: end_line } if end_line
89
+ end
90
+ end
91
+
92
+ boundaries
93
+ end
94
+
95
+ # Find where a test case ends by looking for the last expectation
96
+ # before the next test description or end of tokens
97
+ def find_test_case_end(tokens, start_index)
98
+ last_expectation_line = nil
99
+
100
+ # Look forward from the description for expectations
101
+ (start_index + 1).upto(tokens.length - 1) do |i|
102
+ token = tokens[i]
103
+
104
+ # Stop if we hit another test description
105
+ break if token[:type] == :description
106
+
107
+ # Track the last expectation we see
108
+ if is_expectation_type?(token[:type])
109
+ last_expectation_line = token[:line]
110
+ end
111
+ end
112
+
113
+ last_expectation_line
114
+ end
115
+
116
+ # Convert potential_descriptions to descriptions or comments using test case boundaries
117
+ def classify_potential_descriptions_with_boundaries(tokens, test_boundaries)
118
+ tokens.map.with_index do |token, index|
119
+ if token[:type] == :potential_description
120
+ # Check if this comment falls within any test case boundary
121
+ line_num = token[:line]
122
+ within_test_case = test_boundaries.any? { |boundary|
123
+ line_num >= boundary[:start] && line_num <= boundary[:end]
124
+ }
125
+
126
+ if within_test_case
127
+ # This comment is within a test case, treat as regular comment
128
+ token.merge(type: :comment)
129
+ else
130
+ # For comments outside test boundaries, be more conservative
131
+ # Only treat as description if it immediately precedes a test pattern AND
132
+ # looks like a test description
133
+ content = token[:content].strip
134
+
135
+ # Check if this looks like a test description based on content
136
+ looks_like_test_description = content.match?(/test|example|demonstrate|show|should|when|given/i) &&
137
+ content.length > 10
138
+
139
+ # Check if there's code immediately before this (suggesting it's mid-test)
140
+ prev_token = index > 0 ? tokens[index - 1] : nil
141
+ has_code_before = prev_token && prev_token[:type] == :code
142
+
143
+ if has_code_before || !looks_like_test_description
144
+ # Treat as regular comment
145
+ token.merge(type: :comment)
146
+ else
147
+ # Look ahead for IMMEDIATE test pattern (stricter than before)
148
+ following_tokens = tokens[(index + 1)..]
149
+
150
+ # Skip blanks and comments to find meaningful content
151
+ meaningful_following = following_tokens.reject { |t| [:blank, :comment].include?(t[:type]) }
152
+
153
+ # Look for test pattern within next 5 tokens (more restrictive)
154
+ test_window = meaningful_following.first(5)
155
+ has_code = test_window.any? { |t| t[:type] == :code }
156
+ has_expectation = test_window.any? { |t| is_expectation_type?(t[:type]) }
157
+
158
+ # Only promote to description if BOTH code and expectation are found nearby
159
+ # AND it looks like a test description
160
+ if has_code && has_expectation && looks_like_test_description
161
+ token.merge(type: :description)
162
+ else
163
+ token.merge(type: :comment)
164
+ end
165
+ end
166
+ end
167
+ else
168
+ token
169
+ end
170
+ end
53
171
  end
54
172
 
55
173
  # Convert potential_descriptions to descriptions or comments based on context
56
174
  def classify_potential_descriptions(tokens)
57
175
  tokens.map.with_index do |token, index|
58
176
  if token[:type] == :potential_description
59
- # Look ahead strictly for the pattern: [optional blanks] code expectation
60
- following_tokens = tokens[(index + 1)..]
177
+ # Check if this looks like a test description based on content and context
178
+ content = token[:content].strip
61
179
 
62
- # Skip blanks and find next non-blank tokens
63
- non_blank_following = following_tokens.reject { |t| t[:type] == :blank }
180
+ # Skip if it's clearly just a regular comment (short, lowercase, etc.)
181
+ # Test descriptions are typically longer and more descriptive
182
+ looks_like_regular_comment = content.length < 20 &&
183
+ content.downcase == content &&
184
+ !content.match?(/test|example|demonstrate|show/i)
64
185
 
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
186
+ # Check if there's code immediately before this (suggesting it's mid-test)
187
+ prev_token = index > 0 ? tokens[index - 1] : nil
188
+ has_code_before = prev_token && prev_token[:type] == :code
189
+
190
+ if looks_like_regular_comment || has_code_before
191
+ # Treat as regular comment
71
192
  token.merge(type: :comment)
193
+ else
194
+ # Look ahead for test pattern: code + at least one expectation within reasonable distance
195
+ following_tokens = tokens[(index + 1)..]
196
+
197
+ # Skip blanks and comments to find meaningful content
198
+ meaningful_following = following_tokens.reject { |t| [:blank, :comment].include?(t[:type]) }
199
+
200
+ # Look for test pattern: at least one code token followed by at least one expectation
201
+ # within the next 10 meaningful tokens (to avoid matching setup/teardown)
202
+ test_window = meaningful_following.first(10)
203
+ has_code = test_window.any? { |t| t[:type] == :code }
204
+ has_expectation = test_window.any? { |t| is_expectation_type?(t[:type]) }
205
+
206
+ if has_code && has_expectation
207
+ token.merge(type: :description)
208
+ else
209
+ token.merge(type: :comment)
210
+ end
72
211
  end
73
212
  else
74
213
  token
@@ -76,6 +215,16 @@ class Tryouts
76
215
  end
77
216
  end
78
217
 
218
+ # Check if token type represents any kind of expectation
219
+ def is_expectation_type?(type)
220
+ [
221
+ :expectation, :exception_expectation, :intentional_failure_expectation,
222
+ :true_expectation, :false_expectation, :boolean_expectation,
223
+ :result_type_expectation, :regex_match_expectation,
224
+ :performance_time_expectation, :output_expectation
225
+ ].include?(type)
226
+ end
227
+
79
228
  # Group tokens into logical test blocks using pattern matching
80
229
  def group_into_test_blocks(tokens)
81
230
  blocks = []
@@ -112,6 +261,33 @@ class Tryouts
112
261
  in [_, { type: :expectation }]
113
262
  current_block[:expectations] << token
114
263
 
264
+ in [_, { type: :exception_expectation }]
265
+ current_block[:expectations] << token
266
+
267
+ in [_, { type: :intentional_failure_expectation }]
268
+ current_block[:expectations] << token
269
+
270
+ in [_, { type: :true_expectation }]
271
+ current_block[:expectations] << token
272
+
273
+ in [_, { type: :false_expectation }]
274
+ current_block[:expectations] << token
275
+
276
+ in [_, { type: :boolean_expectation }]
277
+ current_block[:expectations] << token
278
+
279
+ in [_, { type: :result_type_expectation }]
280
+ current_block[:expectations] << token
281
+
282
+ in [_, { type: :regex_match_expectation }]
283
+ current_block[:expectations] << token
284
+
285
+ in [_, { type: :performance_time_expectation }]
286
+ current_block[:expectations] << token
287
+
288
+ in [_, { type: :output_expectation }]
289
+ current_block[:expectations] << token
290
+
115
291
  in [_, { type: :comment | :blank }]
116
292
  add_context_to_block(current_block, token)
117
293
  end
@@ -264,10 +440,11 @@ class Tryouts
264
440
  end
265
441
 
266
442
  def calculate_end_line(block)
267
- last_tokens = [*block[:code], *block[:expectations], *block[:comments]]
268
- return block[:start_line] if last_tokens.empty?
443
+ # Only consider actual content (code and expectations), not blank lines/comments
444
+ content_tokens = [*block[:code], *block[:expectations]]
445
+ return block[:start_line] if content_tokens.empty?
269
446
 
270
- last_tokens.map { |token| token[:line] }.max || block[:start_line]
447
+ content_tokens.map { |token| token[:line] }.max || block[:start_line]
271
448
  end
272
449
 
273
450
  def build_test_case(block)
@@ -283,13 +460,37 @@ class Tryouts
283
460
  # Extract source lines from the original source during parsing
284
461
  source_lines = @lines[start_line..end_line]
285
462
 
463
+ # Find the first expectation line for better error reporting
464
+ first_expectation_line = exp_tokens.empty? ? start_line : exp_tokens.first[:line]
465
+
286
466
  TestCase.new(
287
467
  description: desc,
288
468
  code: extract_code_content(code_tokens),
289
- expectations: exp_tokens.map { |token| token[:content] },
469
+ expectations: exp_tokens.map { |token|
470
+ type = case token[:type]
471
+ when :exception_expectation then :exception
472
+ when :intentional_failure_expectation then :intentional_failure
473
+ when :true_expectation then :true
474
+ when :false_expectation then :false
475
+ when :boolean_expectation then :boolean
476
+ when :result_type_expectation then :result_type
477
+ when :regex_match_expectation then :regex_match
478
+ when :performance_time_expectation then :performance_time
479
+ when :output_expectation then :output
480
+ else :regular
481
+ end
482
+
483
+ # For output expectations, we need to preserve the pipe number
484
+ if token[:type] == :output_expectation
485
+ OutputExpectation.new(content: token[:content], type: type, pipe: token[:pipe])
486
+ else
487
+ Expectation.new(content: token[:content], type: type)
488
+ end
489
+ },
290
490
  line_range: start_line..end_line,
291
491
  path: @source_path,
292
492
  source_lines: source_lines,
493
+ first_expectation_line: first_expectation_line,
293
494
  )
294
495
  else
295
496
  raise "Invalid test block structure: #{block}"