tryouts 3.0.0 → 3.1.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.
Files changed (33) 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 +101 -60
  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 +112 -15
  25. data/lib/tryouts/test_executor.rb +6 -4
  26. data/lib/tryouts/test_runner.rb +1 -1
  27. data/lib/tryouts/testbatch.rb +288 -98
  28. data/lib/tryouts/testcase.rb +141 -0
  29. data/lib/tryouts/translators/minitest_translator.rb +40 -11
  30. data/lib/tryouts/translators/rspec_translator.rb +47 -12
  31. data/lib/tryouts/version.rb +1 -1
  32. data/lib/tryouts.rb +42 -0
  33. metadata +16 -3
@@ -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
@@ -33,7 +33,25 @@ class Tryouts
33
33
  { type: :description, content: $1.strip, line: index }
34
34
  in /^#\s*TEST\s*\d*:\s*(.*)$/ # rubocop:disable Lint/DuplicateBranch
35
35
  { type: :description, content: $1.strip, line: index }
36
- in /^#\s*=>\s*(.*)$/ # Expectation
36
+ in /^#\s*=!>\s*(.*)$/ # Exception expectation (updated for consistency)
37
+ { type: :exception_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
38
+ in /^#\s*=<>\s*(.*)$/ # Intentional failure expectation
39
+ { type: :intentional_failure_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
40
+ in /^#\s*==>\s*(.*)$/ # Boolean true expectation
41
+ { type: :true_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
42
+ in %r{^#\s*=/=>\s*(.*)$} # Boolean false expectation
43
+ { type: :false_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
44
+ in /^#\s*=\|>\s*(.*)$/ # Boolean (true or false) expectation
45
+ { type: :boolean_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
46
+ in /^#\s*=:>\s*(.*)$/ # Result type expectation
47
+ { type: :result_type_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
48
+ in /^#\s*=~>\s*(.*)$/ # Regex match expectation
49
+ { type: :regex_match_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
50
+ in /^#\s*=%>\s*(.*)$/ # Performance time expectation
51
+ { type: :performance_time_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
52
+ in /^#\s*=(\d+)>\s*(.*)$/ # Output expectation (stdout/stderr with pipe number)
53
+ { type: :output_expectation, content: $2.strip, pipe: $1.to_i, line: index, ast: parse_expectation($2.strip) }
54
+ in /^#\s*=>\s*(.*)$/ # Regular expectation
37
55
  { type: :expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
38
56
  in /^##\s*=>\s*(.*)$/ # Commented out expectation (should be ignored)
39
57
  { type: :comment, content: '=>' + $1.strip, line: index }
@@ -56,19 +74,40 @@ class Tryouts
56
74
  def classify_potential_descriptions(tokens)
57
75
  tokens.map.with_index do |token, index|
58
76
  if token[:type] == :potential_description
59
- # Look ahead strictly for the pattern: [optional blanks] code expectation
60
- following_tokens = tokens[(index + 1)..]
77
+ # Check if this looks like a test description based on content and context
78
+ content = token[:content].strip
61
79
 
62
- # Skip blanks and find next non-blank tokens
63
- non_blank_following = following_tokens.reject { |t| t[:type] == :blank }
80
+ # Skip if it's clearly just a regular comment (short, lowercase, etc.)
81
+ # Test descriptions are typically longer and more descriptive
82
+ looks_like_regular_comment = content.length < 20 &&
83
+ content.downcase == content &&
84
+ !content.match?(/test|example|demonstrate|show/i)
64
85
 
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
86
+ # Check if there's code immediately before this (suggesting it's mid-test)
87
+ prev_token = index > 0 ? tokens[index - 1] : nil
88
+ has_code_before = prev_token && prev_token[:type] == :code
89
+
90
+ if looks_like_regular_comment || has_code_before
91
+ # Treat as regular comment
71
92
  token.merge(type: :comment)
93
+ else
94
+ # Look ahead for test pattern: code + at least one expectation within reasonable distance
95
+ following_tokens = tokens[(index + 1)..]
96
+
97
+ # Skip blanks and comments to find meaningful content
98
+ meaningful_following = following_tokens.reject { |t| [:blank, :comment].include?(t[:type]) }
99
+
100
+ # Look for test pattern: at least one code token followed by at least one expectation
101
+ # within the next 10 meaningful tokens (to avoid matching setup/teardown)
102
+ test_window = meaningful_following.first(10)
103
+ has_code = test_window.any? { |t| t[:type] == :code }
104
+ has_expectation = test_window.any? { |t| is_expectation_type?(t[:type]) }
105
+
106
+ if has_code && has_expectation
107
+ token.merge(type: :description)
108
+ else
109
+ token.merge(type: :comment)
110
+ end
72
111
  end
73
112
  else
74
113
  token
@@ -76,6 +115,16 @@ class Tryouts
76
115
  end
77
116
  end
78
117
 
118
+ # Check if token type represents any kind of expectation
119
+ def is_expectation_type?(type)
120
+ [
121
+ :expectation, :exception_expectation, :intentional_failure_expectation,
122
+ :true_expectation, :false_expectation, :boolean_expectation,
123
+ :result_type_expectation, :regex_match_expectation,
124
+ :performance_time_expectation, :output_expectation
125
+ ].include?(type)
126
+ end
127
+
79
128
  # Group tokens into logical test blocks using pattern matching
80
129
  def group_into_test_blocks(tokens)
81
130
  blocks = []
@@ -112,6 +161,33 @@ class Tryouts
112
161
  in [_, { type: :expectation }]
113
162
  current_block[:expectations] << token
114
163
 
164
+ in [_, { type: :exception_expectation }]
165
+ current_block[:expectations] << token
166
+
167
+ in [_, { type: :intentional_failure_expectation }]
168
+ current_block[:expectations] << token
169
+
170
+ in [_, { type: :true_expectation }]
171
+ current_block[:expectations] << token
172
+
173
+ in [_, { type: :false_expectation }]
174
+ current_block[:expectations] << token
175
+
176
+ in [_, { type: :boolean_expectation }]
177
+ current_block[:expectations] << token
178
+
179
+ in [_, { type: :result_type_expectation }]
180
+ current_block[:expectations] << token
181
+
182
+ in [_, { type: :regex_match_expectation }]
183
+ current_block[:expectations] << token
184
+
185
+ in [_, { type: :performance_time_expectation }]
186
+ current_block[:expectations] << token
187
+
188
+ in [_, { type: :output_expectation }]
189
+ current_block[:expectations] << token
190
+
115
191
  in [_, { type: :comment | :blank }]
116
192
  add_context_to_block(current_block, token)
117
193
  end
@@ -264,10 +340,11 @@ class Tryouts
264
340
  end
265
341
 
266
342
  def calculate_end_line(block)
267
- last_tokens = [*block[:code], *block[:expectations], *block[:comments]]
268
- return block[:start_line] if last_tokens.empty?
343
+ # Only consider actual content (code and expectations), not blank lines/comments
344
+ content_tokens = [*block[:code], *block[:expectations]]
345
+ return block[:start_line] if content_tokens.empty?
269
346
 
270
- last_tokens.map { |token| token[:line] }.max || block[:start_line]
347
+ content_tokens.map { |token| token[:line] }.max || block[:start_line]
271
348
  end
272
349
 
273
350
  def build_test_case(block)
@@ -286,7 +363,27 @@ class Tryouts
286
363
  TestCase.new(
287
364
  description: desc,
288
365
  code: extract_code_content(code_tokens),
289
- expectations: exp_tokens.map { |token| token[:content] },
366
+ expectations: exp_tokens.map { |token|
367
+ type = case token[:type]
368
+ when :exception_expectation then :exception
369
+ when :intentional_failure_expectation then :intentional_failure
370
+ when :true_expectation then :true
371
+ when :false_expectation then :false
372
+ when :boolean_expectation then :boolean
373
+ when :result_type_expectation then :result_type
374
+ when :regex_match_expectation then :regex_match
375
+ when :performance_time_expectation then :performance_time
376
+ when :output_expectation then :output
377
+ else :regular
378
+ end
379
+
380
+ # For output expectations, we need to preserve the pipe number
381
+ if token[:type] == :output_expectation
382
+ OutputExpectation.new(content: token[:content], type: type, pipe: token[:pipe])
383
+ else
384
+ Expectation.new(content: token[:content], type: type)
385
+ end
386
+ },
290
387
  line_range: start_line..end_line,
291
388
  path: @source_path,
292
389
  source_lines: source_lines,
@@ -38,6 +38,7 @@ class Tryouts
38
38
  global_tally: @global_tally,
39
39
  )
40
40
 
41
+ # TestBatch handles file output, so don't duplicate it here
41
42
  unless @options[:verbose]
42
43
  context_mode = @options[:shared_context] ? 'shared' : 'fresh'
43
44
  @output_manager.file_execution_start(@file, @testrun.total_tests, context_mode)
@@ -49,15 +50,16 @@ class Tryouts
49
50
  test_results << last_result if last_result
50
51
  end
51
52
 
52
- file_failed_count = test_results.count { |r| r[:status] == :failed }
53
- file_error_count = test_results.count { |r| r[:status] == :error }
54
- @global_tally[:total_tests] += batch.size
53
+ file_failed_count = test_results.count { |r| r.failed? }
54
+ file_error_count = test_results.count { |r| r.error? }
55
+ executed_test_count = test_results.size
56
+ @global_tally[:total_tests] += executed_test_count
55
57
  @global_tally[:total_failed] += file_failed_count
56
58
  @global_tally[:total_errors] += file_error_count
57
59
  @global_tally[:successful_files] += 1 if success
58
60
 
59
61
  duration = Time.now.to_f - @file_start.to_f
60
- @output_manager.file_success(@file, batch.size, file_failed_count, file_error_count, duration)
62
+ @output_manager.file_success(@file, executed_test_count, file_failed_count, file_error_count, duration)
61
63
 
62
64
  # Combine failures and errors to determine the exit code.
63
65
  success ? 0 : (file_failed_count + file_error_count)
@@ -79,7 +79,7 @@ class Tryouts
79
79
  def process_files
80
80
  failure_count = 0
81
81
 
82
- @files.each do |file|
82
+ @files.each_with_index do |file, idx|
83
83
  result = process_file(file)
84
84
  failure_count += result unless result.zero?
85
85
  status = result.zero? ? Console.color(:green, 'PASS') : Console.color(:red, 'FAIL')