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
@@ -32,15 +32,14 @@ class Tryouts
|
|
32
32
|
|
33
33
|
output = case level
|
34
34
|
when 0, 1
|
35
|
-
[
|
35
|
+
[separator_line, header_line, separator_line]
|
36
36
|
else
|
37
|
-
[
|
37
|
+
[header_line, separator_line]
|
38
38
|
end
|
39
39
|
|
40
40
|
with_indent(level) do
|
41
41
|
puts output.join("\n")
|
42
42
|
end
|
43
|
-
puts
|
44
43
|
end
|
45
44
|
|
46
45
|
# File-level operations
|
@@ -48,6 +47,10 @@ class Tryouts
|
|
48
47
|
puts file_header_visual(file_path)
|
49
48
|
end
|
50
49
|
|
50
|
+
def file_end(_file_path, _context_info = {})
|
51
|
+
# No output in verbose mode
|
52
|
+
end
|
53
|
+
|
51
54
|
def file_parsed(_file_path, _test_count, setup_present: false, teardown_present: false)
|
52
55
|
message = ''
|
53
56
|
|
@@ -64,28 +67,39 @@ class Tryouts
|
|
64
67
|
puts indent_text(message, 1)
|
65
68
|
end
|
66
69
|
|
70
|
+
# Summary operations
|
71
|
+
#
|
72
|
+
# Called right before file_result
|
73
|
+
def batch_summary(total_tests, failed_count, elapsed_time)
|
74
|
+
# No output in verbose mode
|
75
|
+
end
|
76
|
+
|
67
77
|
def file_result(_file_path, total_tests, failed_count, error_count, elapsed_time)
|
68
78
|
issues_count = failed_count + error_count
|
69
|
-
|
79
|
+
passed_count = total_tests - issues_count
|
80
|
+
details = [
|
81
|
+
"#{passed_count} passed",
|
82
|
+
]
|
70
83
|
|
71
|
-
|
84
|
+
if issues_count > 0
|
72
85
|
details << "#{failed_count} failed" if failed_count > 0
|
73
86
|
details << "#{error_count} errors" if error_count > 0
|
74
87
|
details_str = details.join(', ')
|
75
|
-
|
88
|
+
color = :red
|
89
|
+
|
90
|
+
time_str = elapsed_time ? " (#{elapsed_time.round(2)}s)" : ''
|
91
|
+
message = "✗ Out of #{total_tests} tests: #{details_str}#{time_str}"
|
92
|
+
puts indent_text(Console.color(color, message), 2)
|
76
93
|
else
|
77
|
-
|
94
|
+
message = "#{total_tests} tests passed"
|
95
|
+
color = :green
|
96
|
+
puts indent_text(Console.color(color, "✓ #{message}"), 2)
|
78
97
|
end
|
79
98
|
|
80
|
-
puts indent_text(status, 2)
|
81
99
|
return unless elapsed_time
|
82
100
|
|
83
|
-
time_msg =
|
84
|
-
|
85
|
-
"Completed in #{(elapsed_time * 1000).round}ms"
|
86
|
-
else
|
87
|
-
"Completed in #{elapsed_time.round(3)}s"
|
88
|
-
end
|
101
|
+
time_msg = "Completed in #{format_timing(elapsed_time).strip.tr('()', '')}"
|
102
|
+
|
89
103
|
puts indent_text(Console.color(:dim, time_msg), 2)
|
90
104
|
end
|
91
105
|
|
@@ -97,33 +111,42 @@ class Tryouts
|
|
97
111
|
puts indent_text(Console.color(:dim, message), 2)
|
98
112
|
end
|
99
113
|
|
100
|
-
def
|
101
|
-
|
114
|
+
def test_end(_test_case, _index, _total)
|
115
|
+
# No output in verbose mode
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_result(result_packet)
|
119
|
+
should_show = @show_passed || !result_packet.passed?
|
102
120
|
|
103
121
|
return unless should_show
|
104
122
|
|
105
|
-
status_line = case
|
106
|
-
|
123
|
+
status_line = case result_packet.status
|
124
|
+
when :passed
|
107
125
|
Console.color(:green, 'PASSED')
|
108
|
-
|
126
|
+
when :failed
|
109
127
|
Console.color(:red, 'FAILED')
|
110
|
-
|
128
|
+
when :error
|
111
129
|
Console.color(:red, 'ERROR')
|
112
|
-
|
130
|
+
when :skipped
|
113
131
|
Console.color(:yellow, 'SKIPPED')
|
114
132
|
else
|
115
133
|
'UNKNOWN'
|
116
|
-
|
134
|
+
end
|
117
135
|
|
118
|
-
|
119
|
-
|
136
|
+
test_case = result_packet.test_case
|
137
|
+
location = "#{Console.pretty_path(test_case.path)}:#{test_case.first_expectation_line + 1}"
|
138
|
+
puts
|
139
|
+
puts indent_text("#{status_line} @ #{location}", 2)
|
120
140
|
|
121
141
|
# Show source code for verbose mode
|
122
142
|
show_test_source_code(test_case)
|
123
143
|
|
124
144
|
# Show failure details for failed tests
|
125
|
-
if
|
126
|
-
show_failure_details(test_case, actual_results)
|
145
|
+
if result_packet.failed? || result_packet.error?
|
146
|
+
show_failure_details(test_case, result_packet.actual_results, result_packet.expected_results)
|
147
|
+
# Show exception details for passed exception expectations
|
148
|
+
elsif result_packet.passed? && has_exception_expectations?(test_case)
|
149
|
+
show_exception_details(test_case, result_packet.actual_results, result_packet.expected_results)
|
127
150
|
end
|
128
151
|
end
|
129
152
|
|
@@ -168,29 +191,13 @@ class Tryouts
|
|
168
191
|
end
|
169
192
|
end
|
170
193
|
|
171
|
-
# Summary operations
|
172
|
-
def batch_summary(total_tests, failed_count, elapsed_time)
|
173
|
-
if failed_count > 0
|
174
|
-
passed = total_tests - failed_count
|
175
|
-
message = "#{failed_count} failed, #{passed} passed"
|
176
|
-
color = :red
|
177
|
-
else
|
178
|
-
message = "#{total_tests} tests passed"
|
179
|
-
color = :green
|
180
|
-
end
|
181
|
-
|
182
|
-
time_str = elapsed_time ? " (#{elapsed_time.round(2)}s)" : ''
|
183
|
-
summary = Console.color(color, "#{message}#{time_str}")
|
184
|
-
puts summary
|
185
|
-
end
|
186
|
-
|
187
194
|
def grand_total(total_tests, failed_count, error_count, successful_files, total_files, elapsed_time)
|
188
195
|
puts
|
189
196
|
puts '=' * @line_width
|
190
197
|
puts 'Grand Total:'
|
191
198
|
|
192
199
|
issues_count = failed_count + error_count
|
193
|
-
time_str
|
200
|
+
time_str =
|
194
201
|
if elapsed_time < 2.0
|
195
202
|
" (#{(elapsed_time * 1000).round}ms)"
|
196
203
|
else
|
@@ -198,7 +205,7 @@ class Tryouts
|
|
198
205
|
end
|
199
206
|
|
200
207
|
if issues_count > 0
|
201
|
-
passed = total_tests - issues_count
|
208
|
+
passed = [total_tests - issues_count, 0].max # Ensure passed never goes negative
|
202
209
|
details = []
|
203
210
|
details << "#{failed_count} failed" if failed_count > 0
|
204
211
|
details << "#{error_count} errors" if error_count > 0
|
@@ -216,6 +223,7 @@ class Tryouts
|
|
216
223
|
return unless @show_debug
|
217
224
|
|
218
225
|
prefix = Console.color(:cyan, 'INFO ')
|
226
|
+
puts
|
219
227
|
puts indent_text("#{prefix} #{message}", level + 1)
|
220
228
|
end
|
221
229
|
|
@@ -260,9 +268,29 @@ class Tryouts
|
|
260
268
|
|
261
269
|
private
|
262
270
|
|
263
|
-
def
|
264
|
-
|
271
|
+
def has_exception_expectations?(test_case)
|
272
|
+
test_case.expectations.any? { |exp| exp.type == :exception }
|
273
|
+
end
|
274
|
+
|
275
|
+
def show_exception_details(test_case, actual_results, expected_results = [])
|
276
|
+
return if actual_results.empty?
|
277
|
+
|
278
|
+
puts indent_text('Exception Details:', 4)
|
279
|
+
|
280
|
+
actual_results.each_with_index do |actual, idx|
|
281
|
+
expected = expected_results[idx] if expected_results && idx < expected_results.length
|
282
|
+
expectation = test_case.expectations[idx] if test_case.expectations
|
265
283
|
|
284
|
+
if expectation&.type == :exception
|
285
|
+
puts indent_text("Caught: #{Console.color(:blue, actual.inspect)}", 5)
|
286
|
+
puts indent_text("Expectation: #{Console.color(:green, expectation.content)}", 5)
|
287
|
+
puts indent_text("Result: #{Console.color(:green, expected.inspect)}", 5) if expected
|
288
|
+
end
|
289
|
+
end
|
290
|
+
puts
|
291
|
+
end
|
292
|
+
|
293
|
+
def show_test_source_code(test_case)
|
266
294
|
# Use pre-captured source lines from parsing
|
267
295
|
start_line = test_case.line_range.first
|
268
296
|
|
@@ -270,9 +298,8 @@ class Tryouts
|
|
270
298
|
line_num = start_line + index
|
271
299
|
line_display = format('%3d: %s', line_num + 1, line_content)
|
272
300
|
|
273
|
-
# Highlight expectation lines by checking if this line
|
274
|
-
|
275
|
-
if line_content.match?(/^\s*#\s*=>\s*/)
|
301
|
+
# Highlight expectation lines by checking if this line contains any expectation syntax
|
302
|
+
if line_content.match?(/^\s*#\s*=(!|<|=|\/=|\||:|~|%|\d+)?>\s*/)
|
276
303
|
line_display = Console.color(:yellow, line_display)
|
277
304
|
end
|
278
305
|
|
@@ -281,24 +308,28 @@ class Tryouts
|
|
281
308
|
puts
|
282
309
|
end
|
283
310
|
|
284
|
-
def show_failure_details(test_case, actual_results)
|
311
|
+
def show_failure_details(test_case, actual_results, expected_results = [])
|
285
312
|
return if actual_results.empty?
|
286
313
|
|
287
|
-
puts indent_text('Expected vs Actual:', 3)
|
288
|
-
|
289
314
|
actual_results.each_with_index do |actual, idx|
|
315
|
+
expected = expected_results[idx] if expected_results && idx < expected_results.length
|
290
316
|
expected_line = test_case.expectations[idx] if test_case.expectations
|
291
317
|
|
292
|
-
if
|
293
|
-
|
318
|
+
if !expected.nil?
|
319
|
+
# Use the evaluated expected value from the evaluator
|
320
|
+
puts indent_text("Expected: #{Console.color(:green, expected.inspect)}", 4)
|
321
|
+
puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
|
322
|
+
elsif expected_line
|
323
|
+
# Fallback to raw expectation content
|
324
|
+
puts indent_text("Expected: #{Console.color(:green, expected_line.content)}", 4)
|
294
325
|
puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
|
295
326
|
else
|
296
327
|
puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
|
297
328
|
end
|
298
329
|
|
299
330
|
# Show difference if both are strings
|
300
|
-
if
|
301
|
-
show_string_diff(
|
331
|
+
if !expected.nil? && actual.is_a?(String) && expected.is_a?(String)
|
332
|
+
show_string_diff(expected, actual)
|
302
333
|
end
|
303
334
|
|
304
335
|
puts
|
@@ -320,11 +351,21 @@ class Tryouts
|
|
320
351
|
padding = '<' * padding_length
|
321
352
|
|
322
353
|
[
|
323
|
-
'-' * @line_width,
|
324
|
-
header_content + padding,
|
325
|
-
'-' * @line_width,
|
354
|
+
indent_text('-' * @line_width, 1),
|
355
|
+
indent_text(header_content + padding, 1),
|
356
|
+
indent_text('-' * @line_width, 1),
|
326
357
|
].join("\n")
|
327
358
|
end
|
359
|
+
|
360
|
+
def format_timing(elapsed_time)
|
361
|
+
if elapsed_time < 0.001
|
362
|
+
" (#{(elapsed_time * 1_000_000).round}μs)"
|
363
|
+
elsif elapsed_time < 1
|
364
|
+
" (#{(elapsed_time * 1000).round}ms)"
|
365
|
+
else
|
366
|
+
" (#{elapsed_time.round(2)}s)"
|
367
|
+
end
|
368
|
+
end
|
328
369
|
end
|
329
370
|
|
330
371
|
# Verbose formatter that only shows failures and errors
|
@@ -333,9 +374,9 @@ class Tryouts
|
|
333
374
|
super(options.merge(show_passed: false))
|
334
375
|
end
|
335
376
|
|
336
|
-
def test_result(
|
377
|
+
def test_result(result_packet)
|
337
378
|
# Only show failed/error tests, but with full source code
|
338
|
-
return if
|
379
|
+
return if result_packet.passed?
|
339
380
|
|
340
381
|
super
|
341
382
|
end
|
data/lib/tryouts/console.rb
CHANGED
@@ -76,49 +76,85 @@ class Tryouts
|
|
76
76
|
end
|
77
77
|
end
|
78
78
|
class << self
|
79
|
-
def bright(str)
|
80
|
-
str = [style(ATTRIBUTES[:bright]), str, default_style].join
|
79
|
+
def bright(str, io = $stdout)
|
80
|
+
str = [style(ATTRIBUTES[:bright], io: io), str, default_style(io)].join
|
81
81
|
str.extend Console::InstanceMethods
|
82
82
|
str
|
83
83
|
end
|
84
84
|
|
85
|
-
def underline(str)
|
86
|
-
str = [style(ATTRIBUTES[:underline]), str, default_style].join
|
85
|
+
def underline(str, io = $stdout)
|
86
|
+
str = [style(ATTRIBUTES[:underline], io: io), str, default_style(io)].join
|
87
87
|
str.extend Console::InstanceMethods
|
88
88
|
str
|
89
89
|
end
|
90
90
|
|
91
|
-
def reverse(str)
|
92
|
-
str = [style(ATTRIBUTES[:reverse]), str, default_style].join
|
91
|
+
def reverse(str, io = $stdout)
|
92
|
+
str = [style(ATTRIBUTES[:reverse], io: io), str, default_style(io)].join
|
93
93
|
str.extend Console::InstanceMethods
|
94
94
|
str
|
95
95
|
end
|
96
96
|
|
97
|
-
def color(col, str)
|
98
|
-
str = [style(COLOURS[col]), str, default_style].join
|
97
|
+
def color(col, str, io = $stdout)
|
98
|
+
str = [style(COLOURS[col], io: io), str, default_style(io)].join
|
99
99
|
str.extend Console::InstanceMethods
|
100
100
|
str
|
101
101
|
end
|
102
102
|
|
103
|
-
def att(name, str)
|
104
|
-
str = [style(ATTRIBUTES[name]), str, default_style].join
|
103
|
+
def att(name, str, io = $stdout)
|
104
|
+
str = [style(ATTRIBUTES[name], io: io), str, default_style(io)].join
|
105
105
|
str.extend Console::InstanceMethods
|
106
106
|
str
|
107
107
|
end
|
108
108
|
|
109
|
-
def bgcolor(col, str)
|
110
|
-
str = [style(ATTRIBUTES[col]), str, default_style].join
|
109
|
+
def bgcolor(col, str, io = $stdout)
|
110
|
+
str = [style(ATTRIBUTES[col], io: io), str, default_style(io)].join
|
111
111
|
str.extend Console::InstanceMethods
|
112
112
|
str
|
113
113
|
end
|
114
114
|
|
115
|
-
def style(*att)
|
116
|
-
#
|
117
|
-
|
115
|
+
def style(*att, io: nil)
|
116
|
+
# Only output ANSI codes if colors are supported
|
117
|
+
target_io = io || $stdout
|
118
|
+
|
119
|
+
# Explicit color control via environment variables
|
120
|
+
# FORCE_COLOR/CLICOLOR_FORCE override NO_COLOR
|
121
|
+
return "\e[%sm" % att.join(';') if ENV['FORCE_COLOR'] || ENV['CLICOLOR_FORCE']
|
122
|
+
return '' if ENV['NO_COLOR']
|
123
|
+
|
124
|
+
# Check if we're outputting to a real TTY
|
125
|
+
tty_output = (target_io.respond_to?(:tty?) && target_io.tty?) ||
|
126
|
+
($stdout.respond_to?(:tty?) && $stdout.tty?) ||
|
127
|
+
($stderr.respond_to?(:tty?) && $stderr.tty?)
|
128
|
+
|
129
|
+
# If we have a real TTY, always use colors
|
130
|
+
return "\e[%sm" % att.join(';') if tty_output
|
131
|
+
|
132
|
+
# For environments like Claude Code where TTY detection fails but we want colors
|
133
|
+
# Check if output appears to be redirected to a file/pipe
|
134
|
+
if ENV['TERM'] && ENV['TERM'] != 'dumb'
|
135
|
+
# Check if stdout/stderr look like they're redirected using file stats
|
136
|
+
begin
|
137
|
+
stdout_stat = $stdout.stat
|
138
|
+
stderr_stat = $stderr.stat
|
139
|
+
|
140
|
+
# If either stdout or stderr looks like a regular file or pipe, disable colors
|
141
|
+
stdout_redirected = stdout_stat.file? || stdout_stat.pipe?
|
142
|
+
stderr_redirected = stderr_stat.file? || stderr_stat.pipe?
|
143
|
+
|
144
|
+
# Enable colors if neither appears redirected
|
145
|
+
return "\e[%sm" % att.join(';') unless stdout_redirected || stderr_redirected
|
146
|
+
rescue
|
147
|
+
# If stat fails, fall back to enabling colors with TERM set
|
148
|
+
return "\e[%sm" % att.join(';')
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Default: no colors
|
153
|
+
''
|
118
154
|
end
|
119
155
|
|
120
|
-
def default_style
|
121
|
-
style(ATTRIBUTES[:default], COLOURS[:default], BGCOLOURS[:default])
|
156
|
+
def default_style(io = $stdout)
|
157
|
+
style(ATTRIBUTES[:default], COLOURS[:default], BGCOLOURS[:default], io: io)
|
122
158
|
end
|
123
159
|
|
124
160
|
# Converts an absolute file path to a path relative to the current working
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# lib/tryouts/expectation_evaluators/base.rb
|
2
|
+
|
3
|
+
class Tryouts
|
4
|
+
module ExpectationEvaluators
|
5
|
+
# Base class for all expectation evaluators
|
6
|
+
class Base
|
7
|
+
attr_reader :expectation, :test_case, :context
|
8
|
+
|
9
|
+
# @param expectation_type [Symbol] the type of expectation to check
|
10
|
+
# @return [Boolean] whether this evaluator can handle the given expectation type
|
11
|
+
def self.handles?(expectation_type)
|
12
|
+
raise NotImplementedError, "#{self} must implement handles? class method"
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param expectation [Object] the expectation object containing content and metadata
|
16
|
+
# @param test_case [Object] the test case being evaluated
|
17
|
+
# @param context [Object] the context in which to evaluate expectations
|
18
|
+
def initialize(expectation, test_case, context)
|
19
|
+
@expectation = expectation
|
20
|
+
@test_case = test_case
|
21
|
+
@context = context
|
22
|
+
end
|
23
|
+
|
24
|
+
# Evaluates the expectation against the actual result
|
25
|
+
# @param actual_result [Object] the result to evaluate against the expectation
|
26
|
+
# @return [Hash] evaluation result with passed status and details
|
27
|
+
def evaluate(actual_result = nil)
|
28
|
+
raise NotImplementedError, "#{self.class} must implement evaluate method"
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
# Evaluates expectation content in the test context with predefined variables
|
34
|
+
#
|
35
|
+
# This method is the core of the expectation evaluation system, providing context-aware
|
36
|
+
# variable access for different expectation types:
|
37
|
+
#
|
38
|
+
# VARIABLE AVAILABILITY:
|
39
|
+
# - `result`: contains actual_result (regular) or timing_ms (performance)
|
40
|
+
# - `_`: shorthand alias for the same data as result
|
41
|
+
#
|
42
|
+
# DESIGN DECISIONS:
|
43
|
+
# - Add new values to ExpectationResult to avoid method signature changes
|
44
|
+
# - Use define_singleton_method for clean variable injection
|
45
|
+
# - Using instance_eval for evaluation provides:
|
46
|
+
# - Full access to test context (instance variables, methods)
|
47
|
+
# - Clean variable injection (result, _)
|
48
|
+
# - Proper file/line reporting for debugging
|
49
|
+
# - Support for complex Ruby expressions in expectations
|
50
|
+
#
|
51
|
+
# Potential enhancements (without breaking changes):
|
52
|
+
# - Add more variables to ExpectationResult (memory usage, etc.)
|
53
|
+
# - Provide additional helper methods in evaluation context
|
54
|
+
# - Enhanced error reporting with better stack traces
|
55
|
+
#
|
56
|
+
# @param content [String] the expectation code to evaluate
|
57
|
+
# @param expectation_result [ExpectationResult] container with actual_result and timing data
|
58
|
+
# @return [Object] the result of evaluating the content
|
59
|
+
def eval_expectation_content(content, expectation_result = nil)
|
60
|
+
path = @test_case.path
|
61
|
+
range = @test_case.line_range
|
62
|
+
|
63
|
+
if expectation_result
|
64
|
+
# For performance expectations, timing data takes precedence for result/_
|
65
|
+
if expectation_result.execution_time_ns
|
66
|
+
timing_ms = expectation_result.execution_time_ms
|
67
|
+
@context.define_singleton_method(:result) { timing_ms }
|
68
|
+
@context.define_singleton_method(:_) { timing_ms }
|
69
|
+
elsif expectation_result.actual_result
|
70
|
+
# For regular expectations, use actual_result
|
71
|
+
@context.define_singleton_method(:result) { expectation_result.actual_result }
|
72
|
+
@context.define_singleton_method(:_) { expectation_result.actual_result }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
@context.instance_eval(content, path, range.first + 1)
|
77
|
+
end
|
78
|
+
|
79
|
+
def build_result(passed:, actual:, expected:, expectation: @expectation.content, error: nil)
|
80
|
+
result = {
|
81
|
+
passed: passed,
|
82
|
+
actual: actual,
|
83
|
+
expected: expected,
|
84
|
+
expectation: expectation,
|
85
|
+
}
|
86
|
+
result[:error] = error if error
|
87
|
+
result
|
88
|
+
end
|
89
|
+
|
90
|
+
def handle_evaluation_error(error, actual_result)
|
91
|
+
build_result(
|
92
|
+
passed: false,
|
93
|
+
actual: actual_result,
|
94
|
+
expected: "EXPECTED: #{error.message}",
|
95
|
+
expectation: @expectation.content,
|
96
|
+
error: error,
|
97
|
+
)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# lib/tryouts/expectation_evaluators/boolean.rb
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
class Tryouts
|
6
|
+
module ExpectationEvaluators
|
7
|
+
# Evaluator for flexible boolean expectations using syntax: #=|> expression
|
8
|
+
#
|
9
|
+
# PURPOSE:
|
10
|
+
# - Validates that an expression evaluates to either true or false (not truthy/falsy)
|
11
|
+
# - Provides lenient boolean validation accepting both true and false values
|
12
|
+
# - Distinguishes from strict true/false evaluators that require specific values
|
13
|
+
#
|
14
|
+
# SYNTAX: #=|> boolean_expression
|
15
|
+
# Examples:
|
16
|
+
# [1, 2, 3] #=|> result.empty? # Pass: expression is false
|
17
|
+
# [] #=|> result.empty? # Pass: expression is true
|
18
|
+
# [1, 2, 3] #=|> result.include?(2) # Pass: expression is true
|
19
|
+
# [1, 2, 3] #=|> result.include?(5) # Pass: expression is false
|
20
|
+
# [1, 2, 3] #=|> result.length # Fail: expression is 3 (truthy but not boolean)
|
21
|
+
# [] #=|> result.first # Fail: expression is nil (falsy but not boolean)
|
22
|
+
#
|
23
|
+
# BOOLEAN STRICTNESS:
|
24
|
+
# - Only passes when expression evaluates to exactly true OR exactly false
|
25
|
+
# - Fails for nil, 0, "", [], {}, or any non-boolean value
|
26
|
+
# - Uses Ruby's Array#include? for boolean type checking
|
27
|
+
# - More lenient than True/False evaluators but stricter than truthy/falsy
|
28
|
+
#
|
29
|
+
# IMPLEMENTATION DETAILS:
|
30
|
+
# - Expression has access to `result` and `_` variables (actual_result)
|
31
|
+
# - Expected display shows 'true or false' indicating flexible acceptance
|
32
|
+
# - Actual display shows the evaluated expression result
|
33
|
+
# - Distinguishes from regular expectations through boolean type validation
|
34
|
+
#
|
35
|
+
# DESIGN DECISIONS:
|
36
|
+
# - Flexible boolean matching allows either true or false values
|
37
|
+
# - Clear expected display explains the dual acceptance requirement
|
38
|
+
# - Expression evaluation provides boolean logic testing with flexibility
|
39
|
+
# - Part of unified #= prefix convention for all expectation types
|
40
|
+
# - Uses #=|> syntax to visually represent OR logic (true OR false)
|
41
|
+
class Boolean < Base
|
42
|
+
def self.handles?(expectation_type)
|
43
|
+
expectation_type == :boolean
|
44
|
+
end
|
45
|
+
|
46
|
+
def evaluate(actual_result = nil)
|
47
|
+
expectation_result = ExpectationResult.from_result(actual_result)
|
48
|
+
expression_result = eval_expectation_content(@expectation.content, expectation_result)
|
49
|
+
|
50
|
+
build_result(
|
51
|
+
passed: [true, false].include?(expression_result),
|
52
|
+
actual: expression_result,
|
53
|
+
expected: 'true or false',
|
54
|
+
)
|
55
|
+
rescue StandardError => ex
|
56
|
+
handle_evaluation_error(ex, actual_result)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# lib/tryouts/expectation_evaluators/exception.rb
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
class Tryouts
|
6
|
+
module ExpectationEvaluators
|
7
|
+
class Exception < Base
|
8
|
+
def self.handles?(expectation_type)
|
9
|
+
expectation_type == :exception
|
10
|
+
end
|
11
|
+
|
12
|
+
def evaluate(_actual_result = nil)
|
13
|
+
execute_test_code_and_evaluate_exception
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def execute_test_code_and_evaluate_exception
|
19
|
+
path = @test_case.path
|
20
|
+
range = @test_case.line_range
|
21
|
+
@context.instance_eval(@test_case.code, path, range.first + 1)
|
22
|
+
|
23
|
+
# Create result packet for evaluation to show what was expected
|
24
|
+
expectation_result = ExpectationResult.from_result(nil)
|
25
|
+
expected_value = eval_expectation_content(@expectation.content, expectation_result)
|
26
|
+
|
27
|
+
build_result(
|
28
|
+
passed: false,
|
29
|
+
actual: 'No exception was raised',
|
30
|
+
expected: expected_value,
|
31
|
+
)
|
32
|
+
rescue SystemStackError, NoMemoryError, SecurityError, ScriptError => ex
|
33
|
+
# Handle system-level exceptions that don't inherit from StandardError
|
34
|
+
# ScriptError includes: LoadError, SyntaxError, NotImplementedError
|
35
|
+
evaluate_exception_condition(ex)
|
36
|
+
rescue StandardError => ex
|
37
|
+
evaluate_exception_condition(ex)
|
38
|
+
end
|
39
|
+
|
40
|
+
def evaluate_exception_condition(caught_error)
|
41
|
+
@context.define_singleton_method(:error) { caught_error }
|
42
|
+
|
43
|
+
expectation_result = ExpectationResult.from_result(caught_error)
|
44
|
+
expected_value = eval_expectation_content(@expectation.content, expectation_result)
|
45
|
+
|
46
|
+
build_result(
|
47
|
+
passed: !!expected_value,
|
48
|
+
actual: caught_error.message,
|
49
|
+
expected: expected_value,
|
50
|
+
)
|
51
|
+
rescue StandardError => ex
|
52
|
+
build_result(
|
53
|
+
passed: false,
|
54
|
+
actual: caught_error.message,
|
55
|
+
expected: "EXPECTED: #{ex.message}",
|
56
|
+
error: ex,
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|