tryouts 3.0.0.pre2 → 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.
- 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 +101 -60
- 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 +112 -15
- data/lib/tryouts/test_executor.rb +6 -4
- data/lib/tryouts/test_runner.rb +1 -1
- data/lib/tryouts/testbatch.rb +288 -98
- data/lib/tryouts/testcase.rb +141 -0
- data/lib/tryouts/translators/minitest_translator.rb +40 -11
- data/lib/tryouts/translators/rspec_translator.rb +47 -12
- data/lib/tryouts/version.rb +1 -1
- data/lib/tryouts.rb +42 -0
- 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
|
data/lib/tryouts/prism_parser.rb
CHANGED
@@ -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
|
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
|
-
#
|
60
|
-
|
77
|
+
# Check if this looks like a test description based on content and context
|
78
|
+
content = token[:content].strip
|
61
79
|
|
62
|
-
# Skip
|
63
|
-
|
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
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
268
|
-
|
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
|
-
|
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|
|
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
|
53
|
-
file_error_count = test_results.count { |r| r
|
54
|
-
|
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,
|
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)
|
data/lib/tryouts/test_runner.rb
CHANGED
@@ -79,7 +79,7 @@ class Tryouts
|
|
79
79
|
def process_files
|
80
80
|
failure_count = 0
|
81
81
|
|
82
|
-
@files.
|
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')
|