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.
- checksums.yaml +4 -4
- data/README.md +51 -115
- data/exe/try +25 -4
- data/lib/tryouts/cli/formatters/base.rb +33 -21
- data/lib/tryouts/cli/formatters/compact.rb +122 -84
- data/lib/tryouts/cli/formatters/factory.rb +1 -1
- data/lib/tryouts/cli/formatters/output_manager.rb +13 -2
- data/lib/tryouts/cli/formatters/quiet.rb +22 -16
- data/lib/tryouts/cli/formatters/verbose.rb +102 -61
- data/lib/tryouts/console.rb +53 -17
- data/lib/tryouts/expectation_evaluators/base.rb +101 -0
- data/lib/tryouts/expectation_evaluators/boolean.rb +60 -0
- data/lib/tryouts/expectation_evaluators/exception.rb +61 -0
- data/lib/tryouts/expectation_evaluators/expectation_result.rb +67 -0
- data/lib/tryouts/expectation_evaluators/false.rb +60 -0
- data/lib/tryouts/expectation_evaluators/intentional_failure.rb +74 -0
- data/lib/tryouts/expectation_evaluators/output.rb +101 -0
- data/lib/tryouts/expectation_evaluators/performance_time.rb +81 -0
- data/lib/tryouts/expectation_evaluators/regex_match.rb +57 -0
- data/lib/tryouts/expectation_evaluators/registry.rb +66 -0
- data/lib/tryouts/expectation_evaluators/regular.rb +67 -0
- data/lib/tryouts/expectation_evaluators/result_type.rb +51 -0
- data/lib/tryouts/expectation_evaluators/true.rb +58 -0
- data/lib/tryouts/prism_parser.rb +221 -20
- data/lib/tryouts/test_batch.rb +556 -0
- data/lib/tryouts/test_case.rb +192 -0
- data/lib/tryouts/test_executor.rb +7 -5
- data/lib/tryouts/test_runner.rb +2 -2
- data/lib/tryouts/translators/minitest_translator.rb +78 -11
- data/lib/tryouts/translators/rspec_translator.rb +85 -12
- data/lib/tryouts/version.rb +1 -1
- data/lib/tryouts.rb +43 -1
- metadata +18 -5
- data/lib/tryouts/testbatch.rb +0 -314
- 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
|
data/lib/tryouts/prism_parser.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# Modern Ruby 3.4+ solution for the teardown bug
|
2
2
|
|
3
3
|
require 'prism'
|
4
|
-
require_relative '
|
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
|
20
|
-
|
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
|
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
|
-
#
|
52
|
-
|
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
|
-
#
|
60
|
-
|
177
|
+
# Check if this looks like a test description based on content and context
|
178
|
+
content = token[:content].strip
|
61
179
|
|
62
|
-
# Skip
|
63
|
-
|
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
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
268
|
-
|
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
|
-
|
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|
|
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}"
|