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
@@ -32,15 +32,14 @@ class Tryouts
32
32
 
33
33
  output = case level
34
34
  when 0, 1
35
- ['', separator_line, header_line, separator_line]
35
+ [separator_line, header_line, separator_line]
36
36
  else
37
- ['', header_line, separator_line]
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
- details = []
79
+ passed_count = total_tests - issues_count
80
+ details = [
81
+ "#{passed_count} passed",
82
+ ]
70
83
 
71
- status = if issues_count > 0
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
- Console.color(:red, "✗ #{issues_count}/#{total_tests} tests had issues (#{details_str})")
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
- Console.color(:green, "#{total_tests} tests passed")
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
- if elapsed_time < 2.0
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 test_result(test_case, result_status, actual_results = [], _elapsed_time = nil)
101
- should_show = @show_passed || result_status != :passed
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 result_status
106
- when :passed
123
+ status_line = case result_packet.status
124
+ when :passed
107
125
  Console.color(:green, 'PASSED')
108
- when :failed
126
+ when :failed
109
127
  Console.color(:red, 'FAILED')
110
- when :error
128
+ when :error
111
129
  Console.color(:red, 'ERROR')
112
- when :skipped
130
+ when :skipped
113
131
  Console.color(:yellow, 'SKIPPED')
114
132
  else
115
133
  'UNKNOWN'
116
- end
134
+ end
117
135
 
136
+ test_case = result_packet.test_case
118
137
  location = "#{Console.pretty_path(test_case.path)}:#{test_case.line_range.first + 1}"
119
- puts indent_text("#{status_line} #{test_case.description} @ #{location}", 2)
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 [:failed, :error].include?(result_status)
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 show_test_source_code(test_case)
264
- puts indent_text('Source code:', 3)
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
- # contains the expectation syntax
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 expected_line
293
- puts indent_text("Expected: #{Console.color(:green, expected_line)}", 4)
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 expected_line && actual.is_a?(String) && expected_line.is_a?(String)
301
- show_string_diff(expected_line, actual)
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(test_case, result_status, actual_results = [], elapsed_time = nil)
377
+ def test_result(result_packet)
337
378
  # Only show failed/error tests, but with full source code
338
- return if result_status == :passed
379
+ return if result_packet.passed?
339
380
 
340
381
  super
341
382
  end
@@ -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
- # => \e[8;34;42m
117
- "\e[%sm" % att.join(';')
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