tryouts 3.1.0 → 3.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b560848d9f29bcfe84ae8984317c4c9d87eaff34f9ebd91141ad7bfc0af1ca8
4
- data.tar.gz: 87c833052a1267da1f0e8a6786565baf0cd880280ca676951aaeddde7aa0601a
3
+ metadata.gz: d2dea4f111f12c35d204df54313a62045bd9e8841710b19323454e5a626ac4fd
4
+ data.tar.gz: 275b234c7ce6ca2debd28e9d861053bc041e825443683078fee710de2b45e0cf
5
5
  SHA512:
6
- metadata.gz: 72a738d41925f4e24ffc233d4e1e5f3dc96f7eaa9ee72eca9ab181da10ec3a283dd5b85b14b91c436acb777a005cb3529a1958c97fdc5f30c13f642de3466551
7
- data.tar.gz: 0102df1a296fa62e04e05fd97c0e21ea83e4e2e718885cde380047c784c8ef0304e64182fee7ae45aa2c8ccac585fe7df6f21f527a2cb11a7e88d33d912028eb
6
+ metadata.gz: 6c704d75d9bed36875b843b6014508df36292247e9c03bbc68892f1d7318db0cfd8ff9907c9c45d71d45441e9512dd5550e4c47b84a9075285ccf8980474c4b9
7
+ data.tar.gz: 0230e782d3a214bb92e647d2beb38e3b3ce1d3bbc129f6628ca14b0b7cef1430fee94ee7746bda55e78a8236da5df1fbf7b6270a8f0a186646f5a01662d0d667
data/exe/try CHANGED
@@ -46,8 +46,8 @@ begin
46
46
  expanded_files = []
47
47
  files.each do |file_or_dir|
48
48
  if File.directory?(file_or_dir)
49
- # If it's a directory, find all *_try.rb files within it
50
- dir_files = Dir.glob('**/*_try.rb', base: file_or_dir)
49
+ # If it's a directory, find all *_try.rb and *.try.rb files within it
50
+ dir_files = Dir.glob(['**/*_try.rb', '**/*.try.rb'], base: file_or_dir)
51
51
  expanded_files.concat(dir_files.map { |f| File.join(file_or_dir, f) })
52
52
  else
53
53
  # If it's a file, add it as-is
@@ -59,7 +59,7 @@ begin
59
59
  # Default file discovery if no files specified
60
60
  if files.empty?
61
61
  raw_files = Dir.glob(
62
- ['{app,apps,lib,try,tryouts}/**/*_try.rb', './*_try.rb'],
62
+ ['{app,apps,lib,try,tryouts}/**/*_try.rb', './*_try.rb', '{app,apps,lib,try,tryouts}/**/*.try.rb', './*.try.rb'],
63
63
  base: Dir.pwd,
64
64
  )
65
65
 
@@ -7,7 +7,6 @@ class Tryouts
7
7
  class CLI
8
8
  # Enhanced interface for all test output formatting
9
9
  module FormatterInterface
10
-
11
10
  attr_reader :current_indent
12
11
 
13
12
  # Phase-level output (major sections)
@@ -115,6 +114,5 @@ class Tryouts
115
114
  @current_indent = old_indent
116
115
  end
117
116
  end
118
-
119
117
  end
120
118
  end
@@ -22,7 +22,7 @@ class Tryouts
22
22
  io.puts text
23
23
  when 1
24
24
  # Skip execution phase headers - they create unwanted empty lines
25
- return
25
+ nil
26
26
  else
27
27
  # Other phase headers with minimal formatting
28
28
  io.puts indent_text(message, level - 1)
@@ -112,8 +112,8 @@ class Tryouts
112
112
  return if result_packet.passed? && !@show_passed
113
113
 
114
114
  test_case = result_packet.test_case
115
- desc = test_case.description.to_s
116
- desc = 'unnamed test' if desc.empty?
115
+ desc = test_case.description.to_s
116
+ desc = 'unnamed test' if desc.empty?
117
117
 
118
118
  case result_packet.status
119
119
  when :passed
@@ -133,6 +133,7 @@ class Tryouts
133
133
  if test_case.source_lines && test_case.source_lines.size <= 3
134
134
  test_case.source_lines.each do |line|
135
135
  next if line.strip.empty? || line.strip.start_with?('#')
136
+
136
137
  io.puts indent_text(" #{line.strip}", 1)
137
138
  break # Only show first relevant line
138
139
  end
@@ -209,7 +210,7 @@ class Tryouts
209
210
 
210
211
  time_str = format_timing(elapsed_time)
211
212
 
212
- io.puts "Total: #{result} (#{time_str})"
213
+ io.puts "Total: #{result} #{time_str}"
213
214
  io.puts "Files: #{successful_files} of #{total_files} successful"
214
215
  end
215
216
 
@@ -112,7 +112,7 @@ class Tryouts
112
112
 
113
113
  issues_count = failed_count + error_count
114
114
  if issues_count > 0
115
- passed = [total_tests - issues_count, 0].max # Ensure passed never goes negative
115
+ passed = [total_tests - issues_count, 0].max # Ensure passed never goes negative
116
116
  details = []
117
117
  details << "#{failed_count} failed" if failed_count > 0
118
118
  details << "#{error_count} errors" if error_count > 0
@@ -80,7 +80,7 @@ class Tryouts
80
80
  details = [
81
81
  "#{passed_count} passed",
82
82
  ]
83
-
83
+ puts
84
84
  if issues_count > 0
85
85
  details << "#{failed_count} failed" if failed_count > 0
86
86
  details << "#{error_count} errors" if error_count > 0
@@ -134,7 +134,7 @@ class Tryouts
134
134
  end
135
135
 
136
136
  test_case = result_packet.test_case
137
- location = "#{Console.pretty_path(test_case.path)}:#{test_case.line_range.first + 1}"
137
+ location = "#{Console.pretty_path(test_case.path)}:#{test_case.first_expectation_line + 1}"
138
138
  puts
139
139
  puts indent_text("#{status_line} @ #{location}", 2)
140
140
 
@@ -181,6 +181,7 @@ class Tryouts
181
181
  def teardown_start(line_range)
182
182
  message = "Executing teardown (lines #{line_range.first}..#{line_range.last})"
183
183
  puts indent_text(Console.color(:cyan, message), 2)
184
+ puts
184
185
  end
185
186
 
186
187
  def teardown_output(output_text)
@@ -278,7 +279,7 @@ class Tryouts
278
279
  puts indent_text('Exception Details:', 4)
279
280
 
280
281
  actual_results.each_with_index do |actual, idx|
281
- expected = expected_results[idx] if expected_results && idx < expected_results.length
282
+ expected = expected_results[idx] if expected_results && idx < expected_results.length
282
283
  expectation = test_case.expectations[idx] if test_case.expectations
283
284
 
284
285
  if expectation&.type == :exception
@@ -299,7 +300,7 @@ class Tryouts
299
300
  line_display = format('%3d: %s', line_num + 1, line_content)
300
301
 
301
302
  # Highlight expectation lines by checking if this line contains any expectation syntax
302
- if line_content.match?(/^\s*#\s*=(!|<|=|\/=|\||:|~|%|\d+)?>\s*/)
303
+ if line_content.match?(%r{^\s*#\s*=(!|<|=|/=|\||:|~|%|\d+)?>\s*})
303
304
  line_display = Console.color(:yellow, line_display)
304
305
  end
305
306
 
@@ -319,12 +320,13 @@ class Tryouts
319
320
  # Use the evaluated expected value from the evaluator
320
321
  puts indent_text("Expected: #{Console.color(:green, expected.inspect)}", 4)
321
322
  puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
322
- elsif expected_line
323
- # Fallback to raw expectation content
323
+ elsif expected_line && !expected_results.empty?
324
+ # Only show raw expectation content if we have expected_results (non-error case)
324
325
  puts indent_text("Expected: #{Console.color(:green, expected_line.content)}", 4)
325
326
  puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
326
327
  else
327
- puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
328
+ # For error cases (empty expected_results), just show the error
329
+ puts indent_text("Error: #{Console.color(:red, actual.inspect)}", 4)
328
330
  end
329
331
 
330
332
  # Show difference if both are strings
@@ -143,7 +143,7 @@ class Tryouts
143
143
 
144
144
  # Enable colors if neither appears redirected
145
145
  return "\e[%sm" % att.join(';') unless stdout_redirected || stderr_redirected
146
- rescue
146
+ rescue StandardError
147
147
  # If stat fails, fall back to enabling colors with TERM set
148
148
  return "\e[%sm" % att.join(';')
149
149
  end
@@ -45,7 +45,7 @@ class Tryouts
45
45
 
46
46
  def evaluate(actual_result = nil)
47
47
  expectation_result = ExpectationResult.from_result(actual_result)
48
- expression_result = eval_expectation_content(@expectation.content, expectation_result)
48
+ expression_result = eval_expectation_content(@expectation.content, expectation_result)
49
49
 
50
50
  build_result(
51
51
  passed: [true, false].include?(expression_result),
@@ -22,7 +22,7 @@ class Tryouts
22
22
 
23
23
  # Create result packet for evaluation to show what was expected
24
24
  expectation_result = ExpectationResult.from_result(nil)
25
- expected_value = eval_expectation_content(@expectation.content, expectation_result)
25
+ expected_value = eval_expectation_content(@expectation.content, expectation_result)
26
26
 
27
27
  build_result(
28
28
  passed: false,
@@ -41,7 +41,7 @@ class Tryouts
41
41
  @context.define_singleton_method(:error) { caught_error }
42
42
 
43
43
  expectation_result = ExpectationResult.from_result(caught_error)
44
- expected_value = eval_expectation_content(@expectation.content, expectation_result)
44
+ expected_value = eval_expectation_content(@expectation.content, expectation_result)
45
45
 
46
46
  build_result(
47
47
  passed: !!expected_value,
@@ -32,7 +32,7 @@ class Tryouts
32
32
  start_time_ns: nil,
33
33
  end_time_ns: nil,
34
34
  stdout_content: nil,
35
- stderr_content: nil
35
+ stderr_content: nil,
36
36
  )
37
37
  end
38
38
 
@@ -46,7 +46,7 @@ class Tryouts
46
46
  start_time_ns: start_time_ns,
47
47
  end_time_ns: end_time_ns,
48
48
  stdout_content: nil,
49
- stderr_content: nil
49
+ stderr_content: nil,
50
50
  )
51
51
  end
52
52
 
@@ -59,7 +59,7 @@ class Tryouts
59
59
  start_time_ns: nil,
60
60
  end_time_ns: nil,
61
61
  stdout_content: stdout_content,
62
- stderr_content: stderr_content
62
+ stderr_content: stderr_content,
63
63
  )
64
64
  end
65
65
  end
@@ -45,7 +45,7 @@ class Tryouts
45
45
 
46
46
  def evaluate(actual_result = nil)
47
47
  expectation_result = ExpectationResult.from_result(actual_result)
48
- expression_result = eval_expectation_content(@expectation.content, expectation_result)
48
+ expression_result = eval_expectation_content(@expectation.content, expectation_result)
49
49
 
50
50
  build_result(
51
51
  passed: expression_result == false,
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative 'base'
4
4
  require_relative 'regular'
5
- require_relative '../testcase'
5
+ require_relative '../test_case'
6
6
 
7
7
  class Tryouts
8
8
  module ExpectationEvaluators
@@ -56,14 +56,14 @@ class Tryouts
56
56
 
57
57
  # Delegate to regular evaluator
58
58
  regular_evaluator = Regular.new(regular_expectation, @test_case, @context)
59
- regular_result = regular_evaluator.evaluate(actual_result)
59
+ regular_result = regular_evaluator.evaluate(actual_result)
60
60
 
61
61
  # Invert the result while preserving metadata
62
62
  build_result(
63
63
  passed: !regular_result[:passed],
64
64
  actual: regular_result[:actual],
65
65
  expected: "NOT #{regular_result[:expected]} (intentional failure)",
66
- expectation: @expectation.content
66
+ expectation: @expectation.content,
67
67
  )
68
68
  rescue StandardError => ex
69
69
  # If evaluation itself fails (not the expectation), that's a real error
@@ -56,9 +56,9 @@ class Tryouts
56
56
 
57
57
  # Get the appropriate captured content
58
58
  captured_content = case pipe_number
59
- when 1 then stdout_content || ""
60
- when 2 then stderr_content || ""
61
- else ""
59
+ when 1 then stdout_content || ''
60
+ when 2 then stderr_content || ''
61
+ else ''
62
62
  end
63
63
 
64
64
  # Create result packet for expression evaluation
@@ -82,8 +82,8 @@ class Tryouts
82
82
 
83
83
  # Build result with appropriate pipe description
84
84
  pipe_name = case pipe_number
85
- when 1 then "stdout"
86
- when 2 then "stderr"
85
+ when 1 then 'stdout'
86
+ when 2 then 'stderr'
87
87
  else "pipe#{pipe_number}"
88
88
  end
89
89
 
@@ -91,7 +91,7 @@ class Tryouts
91
91
  passed: matched,
92
92
  actual: "#{pipe_name}: #{captured_content.inspect}",
93
93
  expected: expected_pattern.inspect,
94
- expectation: @expectation.content
94
+ expectation: @expectation.content,
95
95
  )
96
96
  rescue StandardError => ex
97
97
  handle_evaluation_error(ex, actual_result)
@@ -54,18 +54,18 @@ class Tryouts
54
54
  passed: false,
55
55
  actual: 'No timing data available',
56
56
  expected: 'Performance measurement',
57
- error: 'Performance expectations require execution timing data'
57
+ error: 'Performance expectations require execution timing data',
58
58
  )
59
59
  end
60
60
 
61
61
  # Create result packet with timing data available to expectation
62
62
  expectation_result = ExpectationResult.from_timing(actual_result, execution_time_ns)
63
- expected_limit_ms = eval_expectation_content(@expectation.content, expectation_result)
63
+ expected_limit_ms = eval_expectation_content(@expectation.content, expectation_result)
64
64
 
65
65
  actual_time_ms = expectation_result.execution_time_ms
66
66
 
67
67
  # Performance tolerance: actual <= expected + 10% (not strict window)
68
- max_allowed_ms = expected_limit_ms * 1.1
68
+ max_allowed_ms = expected_limit_ms * 1.1
69
69
  within_tolerance = actual_time_ms <= max_allowed_ms
70
70
 
71
71
  build_result(
@@ -38,11 +38,11 @@ class Tryouts
38
38
 
39
39
  def evaluate(actual_result = nil)
40
40
  expectation_result = ExpectationResult.from_result(actual_result)
41
- pattern = eval_expectation_content(@expectation.content, expectation_result)
41
+ pattern = eval_expectation_content(@expectation.content, expectation_result)
42
42
 
43
43
  # Convert actual_result to string for regex matching
44
44
  string_result = actual_result.to_s
45
- match_result = string_result =~ pattern
45
+ match_result = string_result =~ pattern
46
46
 
47
47
  build_result(
48
48
  passed: !match_result.nil?,
@@ -52,7 +52,7 @@ class Tryouts
52
52
 
53
53
  def evaluate(actual_result = nil)
54
54
  expectation_result = ExpectationResult.from_result(actual_result)
55
- expected_value = eval_expectation_content(@expectation.content, expectation_result)
55
+ expected_value = eval_expectation_content(@expectation.content, expectation_result)
56
56
 
57
57
  build_result(
58
58
  passed: actual_result == expected_value,
@@ -36,7 +36,7 @@ class Tryouts
36
36
 
37
37
  def evaluate(actual_result = nil)
38
38
  expectation_result = ExpectationResult.from_result(actual_result)
39
- expected_class = eval_expectation_content(@expectation.content, expectation_result)
39
+ expected_class = eval_expectation_content(@expectation.content, expectation_result)
40
40
 
41
41
  build_result(
42
42
  passed: actual_result.is_a?(expected_class),
@@ -42,8 +42,8 @@ class Tryouts
42
42
  end
43
43
 
44
44
  def evaluate(actual_result = nil)
45
- expectation_result = ExpectationResult.from_result(actual_result)
46
- expression_result = eval_expectation_content(@expectation.content, expectation_result)
45
+ expectation_result = ExpectationResult.from_result(actual_result)
46
+ expression_result = eval_expectation_content(@expectation.content, expectation_result)
47
47
 
48
48
  build_result(
49
49
  passed: expression_result == true,
@@ -1,7 +1,7 @@
1
1
  # Modern Ruby 3.4+ solution for the teardown bug
2
2
 
3
3
  require 'prism'
4
- require_relative 'testcase'
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 = tokenize_content
20
- test_blocks = group_into_test_blocks(tokens)
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
 
@@ -66,8 +68,106 @@ class Tryouts
66
68
  tokens << token
67
69
  end
68
70
 
69
- # Post-process to convert potential_descriptions to descriptions or comments
70
- classify_potential_descriptions(tokens)
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? do |boundary|
123
+ line_num >= boundary[:start] && line_num <= boundary[:end]
124
+ end
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
71
171
  end
72
172
 
73
173
  # Convert potential_descriptions to descriptions or comments based on context
@@ -80,11 +180,11 @@ class Tryouts
80
180
  # Skip if it's clearly just a regular comment (short, lowercase, etc.)
81
181
  # Test descriptions are typically longer and more descriptive
82
182
  looks_like_regular_comment = content.length < 20 &&
83
- content.downcase == content &&
84
- !content.match?(/test|example|demonstrate|show/i)
183
+ content.downcase == content &&
184
+ !content.match?(/test|example|demonstrate|show/i)
85
185
 
86
186
  # Check if there's code immediately before this (suggesting it's mid-test)
87
- prev_token = index > 0 ? tokens[index - 1] : nil
187
+ prev_token = index > 0 ? tokens[index - 1] : nil
88
188
  has_code_before = prev_token && prev_token[:type] == :code
89
189
 
90
190
  if looks_like_regular_comment || has_code_before
@@ -99,8 +199,8 @@ class Tryouts
99
199
 
100
200
  # Look for test pattern: at least one code token followed by at least one expectation
101
201
  # 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 }
202
+ test_window = meaningful_following.first(10)
203
+ has_code = test_window.any? { |t| t[:type] == :code }
104
204
  has_expectation = test_window.any? { |t| is_expectation_type?(t[:type]) }
105
205
 
106
206
  if has_code && has_expectation
@@ -360,15 +460,18 @@ class Tryouts
360
460
  # Extract source lines from the original source during parsing
361
461
  source_lines = @lines[start_line..end_line]
362
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
+
363
466
  TestCase.new(
364
467
  description: desc,
365
468
  code: extract_code_content(code_tokens),
366
- expectations: exp_tokens.map { |token|
469
+ expectations: exp_tokens.map do |token|
367
470
  type = case token[:type]
368
471
  when :exception_expectation then :exception
369
472
  when :intentional_failure_expectation then :intentional_failure
370
- when :true_expectation then :true
371
- when :false_expectation then :false
473
+ when :true_expectation then :true # rubocop:disable Lint/BooleanSymbol
474
+ when :false_expectation then :false # rubocop:disable Lint/BooleanSymbol
372
475
  when :boolean_expectation then :boolean
373
476
  when :result_type_expectation then :result_type
374
477
  when :regex_match_expectation then :regex_match
@@ -383,10 +486,11 @@ class Tryouts
383
486
  else
384
487
  Expectation.new(content: token[:content], type: type)
385
488
  end
386
- },
489
+ end,
387
490
  line_range: start_line..end_line,
388
491
  path: @source_path,
389
492
  source_lines: source_lines,
493
+ first_expectation_line: first_expectation_line,
390
494
  )
391
495
  else
392
496
  raise "Invalid test block structure: #{block}"
@@ -38,10 +38,13 @@ class Tryouts
38
38
  @test_case_count = 0
39
39
  @setup_failed = false
40
40
 
41
+ # Setup container for fresh context mode - preserves @instance_variables from setup
42
+ @setup_container = nil
43
+
41
44
  # Circuit breaker for batch-level failure protection
42
- @consecutive_failures = 0
45
+ @consecutive_failures = 0
43
46
  @max_consecutive_failures = options[:max_consecutive_failures] || 10
44
- @circuit_breaker_active = false
47
+ @circuit_breaker_active = false
45
48
 
46
49
  # Expose context objects for testing - different strategies for each mode
47
50
  @shared_context = if options[:shared_context]
@@ -68,7 +71,19 @@ class Tryouts
68
71
 
69
72
  # Stop execution if setup failed
70
73
  if @setup_failed
71
- @output_manager&.error("Stopping batch execution due to setup failure")
74
+ @output_manager&.error('Stopping batch execution due to setup failure')
75
+ @status = :failed
76
+ finalize_results([])
77
+ return false
78
+ end
79
+ else
80
+ # Fresh context mode: execute setup once to establish shared @instance_variables
81
+ @output_manager&.info('Running setup for fresh context...', 2)
82
+ execute_fresh_context_setup
83
+
84
+ # Stop execution if setup failed
85
+ if @setup_failed
86
+ @output_manager&.error('Stopping batch execution due to setup failure')
72
87
  @status = :failed
73
88
  finalize_results([])
74
89
  return false
@@ -94,10 +109,10 @@ class Tryouts
94
109
  update_circuit_breaker(result)
95
110
 
96
111
  result
97
- rescue StandardError => e
98
- @output_manager&.test_end(test_case, idx, @test_case_count, status: :failed, error: e)
112
+ rescue StandardError => ex
113
+ @output_manager&.test_end(test_case, idx, @test_case_count, status: :failed, error: ex)
99
114
  # Create error result packet to maintain consistent data flow
100
- error_result = build_error_result(test_case, e)
115
+ error_result = build_error_result(test_case, ex)
101
116
  process_test_result(error_result)
102
117
 
103
118
  # Update circuit breaker for exception cases
@@ -174,7 +189,7 @@ class Tryouts
174
189
  error: result.error,
175
190
  captured_output: captured_output,
176
191
  elapsed_time: result.elapsed_time,
177
- metadata: result.metadata
192
+ metadata: result.metadata,
178
193
  )
179
194
  end
180
195
 
@@ -188,7 +203,7 @@ class Tryouts
188
203
  execute_test_case_with_container(test_case, @container)
189
204
  end
190
205
 
191
- # Fresh context execution - setup runs per test, isolated state
206
+ # Fresh context execution - tests run in isolated state but inherit setup @instance_variables
192
207
  def execute_with_fresh_context(test_case)
193
208
  fresh_container = if @shared_context.is_a?(FreshContextFactory)
194
209
  @shared_context.create_container
@@ -196,10 +211,12 @@ class Tryouts
196
211
  Object.new # Fallback for backwards compatibility
197
212
  end
198
213
 
199
- # Execute setup in fresh context if present
200
- setup = @testrun.setup
201
- if setup && !setup.code.empty?
202
- fresh_container.instance_eval(setup.code, setup.path, 1)
214
+ # Copy @instance_variables from setup container to fresh container
215
+ if @setup_container
216
+ @setup_container.instance_variables.each do |var|
217
+ value = @setup_container.instance_variable_get(var)
218
+ fresh_container.instance_variable_set(var, value)
219
+ end
203
220
  end
204
221
 
205
222
  execute_test_case_with_container(test_case, fresh_container)
@@ -225,7 +242,7 @@ class Tryouts
225
242
  # Check if we need output capture for any expectations
226
243
  needs_output_capture = test_case.expectations.any?(&:output?)
227
244
 
228
- result_value, execution_time_ns, stdout_content, stderr_content, expectations_result =
245
+ result_value, _, _, _, expectations_result =
229
246
  execute_with_timeout(test_timeout, test_case) do
230
247
  if needs_output_capture
231
248
  # Execute with output capture using Fiber-local isolation
@@ -239,9 +256,9 @@ class Tryouts
239
256
  else
240
257
  # Regular execution with timing capture only
241
258
  execution_start_ns = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
242
- result_value = container.instance_eval(code, path, range.first + 1)
243
- execution_end_ns = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
244
- execution_time_ns = execution_end_ns - execution_start_ns
259
+ result_value = container.instance_eval(code, path, range.first + 1)
260
+ execution_end_ns = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
261
+ execution_time_ns = execution_end_ns - execution_start_ns
245
262
 
246
263
  expectations_result = evaluate_expectations(test_case, result_value, container, execution_time_ns)
247
264
  [result_value, execution_time_ns, nil, nil, expectations_result]
@@ -276,9 +293,9 @@ class Tryouts
276
293
 
277
294
  # Execute with timing capture
278
295
  execution_start_ns = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
279
- result_value = container.instance_eval(code, path, range.first + 1)
280
- execution_end_ns = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
281
- execution_time_ns = execution_end_ns - execution_start_ns
296
+ result_value = container.instance_eval(code, path, range.first + 1)
297
+ execution_end_ns = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
298
+ execution_time_ns = execution_end_ns - execution_start_ns
282
299
 
283
300
  [result_value, execution_time_ns]
284
301
  end.resume.tap do |result_value, execution_time_ns|
@@ -317,7 +334,7 @@ class Tryouts
317
334
  {
318
335
  passed: evaluation_results.all? { |r| r[:passed] },
319
336
  actual_results: evaluation_results.map { |r| r[:actual] },
320
- expected_results: evaluation_results.map { |r| r[:expected] }
337
+ expected_results: evaluation_results.map { |r| r[:expected] },
321
338
  }
322
339
  end
323
340
 
@@ -328,14 +345,14 @@ class Tryouts
328
345
  test_case,
329
346
  result_value,
330
347
  expectations_result[:actual_results],
331
- expectations_result[:expected_results]
348
+ expectations_result[:expected_results],
332
349
  )
333
350
  else
334
351
  TestCaseResultPacket.from_failure(
335
352
  test_case,
336
353
  result_value,
337
354
  expectations_result[:actual_results],
338
- expectations_result[:expected_results]
355
+ expectations_result[:expected_results],
339
356
  )
340
357
  end
341
358
  end
@@ -375,7 +392,7 @@ class Tryouts
375
392
  @output_manager&.setup_output(captured_output) if captured_output && !captured_output.empty?
376
393
  end
377
394
  rescue StandardError => ex
378
- @setup_failed = true
395
+ @setup_failed = true
379
396
  @global_tally[:total_errors] += 1 if @global_tally
380
397
 
381
398
  # Classify error and handle appropriately
@@ -394,6 +411,43 @@ class Tryouts
394
411
  raise "Global setup failed (#{ex.class}): #{ex.message}"
395
412
  end
396
413
 
414
+ # Setup execution for fresh context mode - creates @setup_container with @instance_variables
415
+ def execute_fresh_context_setup
416
+ setup = @testrun.setup
417
+
418
+ if setup && !setup.code.empty? && !@options[:shared_context]
419
+ @output_manager&.setup_start(setup.line_range)
420
+
421
+ # Create setup container to hold @instance_variables
422
+ @setup_container = Object.new
423
+
424
+ # Capture setup output instead of letting it print directly
425
+ captured_output = capture_output do
426
+ @setup_container.instance_eval(setup.code, setup.path, setup.line_range.first + 1)
427
+ end
428
+
429
+ @output_manager&.setup_output(captured_output) if captured_output && !captured_output.empty?
430
+ end
431
+ rescue StandardError => ex
432
+ @setup_failed = true
433
+ @global_tally[:total_errors] += 1 if @global_tally
434
+
435
+ # Classify error and handle appropriately
436
+ error_type = Tryouts.classify_error(ex)
437
+
438
+ Tryouts.debug "Setup failed with #{error_type} error: (#{ex.class}): #{ex.message}"
439
+ Tryouts.trace ex.backtrace
440
+
441
+ # For non-catastrophic errors, we still stop batch execution
442
+ unless Tryouts.batch_stopping_error?(ex)
443
+ @output_manager&.error("Fresh context setup failed: #{ex.message}")
444
+ return
445
+ end
446
+
447
+ # For catastrophic errors, still raise to stop execution
448
+ raise "Fresh context setup failed (#{ex.class}): #{ex.message}"
449
+ end
450
+
397
451
  # Global teardown execution
398
452
  def execute_global_teardown
399
453
  teardown = @testrun.teardown
@@ -420,11 +474,11 @@ class Tryouts
420
474
  @output_manager&.error("Teardown failed: #{ex.message}")
421
475
 
422
476
  # Teardown failures are generally non-fatal - log and continue
423
- unless Tryouts.batch_stopping_error?(ex)
424
- @output_manager&.error("Continuing despite teardown failure")
425
- else
477
+ if Tryouts.batch_stopping_error?(ex)
426
478
  # Only catastrophic errors should potentially affect batch completion
427
- @output_manager&.error("Teardown failure may affect subsequent operations")
479
+ @output_manager&.error('Teardown failure may affect subsequent operations')
480
+ else
481
+ @output_manager&.error('Continuing despite teardown failure')
428
482
  end
429
483
  end
430
484
 
@@ -477,11 +531,9 @@ class Tryouts
477
531
  end
478
532
 
479
533
  # Timeout protection for individual test execution
480
- def execute_with_timeout(timeout_seconds, test_case)
481
- Timeout.timeout(timeout_seconds) do
482
- yield
483
- end
484
- rescue Timeout::Error => e
534
+ def execute_with_timeout(timeout_seconds, test_case, &)
535
+ Timeout.timeout(timeout_seconds, &)
536
+ rescue Timeout::Error
485
537
  Tryouts.debug "Test timeout after #{timeout_seconds}s: #{test_case.description}"
486
538
  raise StandardError.new("Test execution timeout (#{timeout_seconds}s)")
487
539
  end
@@ -496,7 +548,7 @@ class Tryouts
496
548
  end
497
549
  else
498
550
  # Reset on success
499
- @consecutive_failures = 0
551
+ @consecutive_failures = 0
500
552
  @circuit_breaker_active = false
501
553
  end
502
554
  end
@@ -3,7 +3,7 @@
3
3
  # Modern data structures using Ruby 3.2+ Data classes
4
4
  class Tryouts
5
5
  # Core data structures
6
- TestCase = Data.define(:description, :code, :expectations, :line_range, :path, :source_lines) do
6
+ TestCase = Data.define(:description, :code, :expectations, :line_range, :path, :source_lines, :first_expectation_line) do
7
7
  def empty?
8
8
  code.empty?
9
9
  end
@@ -89,7 +89,7 @@ class Tryouts
89
89
  :error, # Exception object (if any)
90
90
  :captured_output, # Captured stdout/stderr content
91
91
  :elapsed_time, # Execution timing (future use)
92
- :metadata # Hash for future extensibility
92
+ :metadata, # Hash for future extensibility
93
93
  ) do
94
94
  def passed?
95
95
  status == :passed
@@ -131,7 +131,7 @@ class Tryouts
131
131
  error: nil,
132
132
  captured_output: captured_output,
133
133
  elapsed_time: elapsed_time,
134
- metadata: metadata
134
+ metadata: metadata,
135
135
  )
136
136
  end
137
137
 
@@ -146,7 +146,7 @@ class Tryouts
146
146
  error: nil,
147
147
  captured_output: captured_output,
148
148
  elapsed_time: elapsed_time,
149
- metadata: metadata
149
+ metadata: metadata,
150
150
  )
151
151
  end
152
152
 
@@ -171,7 +171,7 @@ class Tryouts
171
171
  error: error,
172
172
  captured_output: captured_output,
173
173
  elapsed_time: elapsed_time,
174
- metadata: metadata
174
+ metadata: metadata,
175
175
  )
176
176
  end
177
177
  end
@@ -1,6 +1,6 @@
1
1
  # lib/tryouts/test_executor.rb
2
2
 
3
- require_relative 'testbatch'
3
+ require_relative 'test_batch'
4
4
 
5
5
  class Tryouts
6
6
  class TestExecutor
@@ -1,7 +1,7 @@
1
1
  # lib/tryouts/test_runner.rb
2
2
 
3
3
  require_relative 'prism_parser'
4
- require_relative 'testbatch'
4
+ require_relative 'test_batch'
5
5
  require_relative 'translators/rspec_translator'
6
6
  require_relative 'translators/minitest_translator'
7
7
  require_relative 'file_processor'
@@ -79,7 +79,7 @@ class Tryouts
79
79
  def process_files
80
80
  failure_count = 0
81
81
 
82
- @files.each_with_index do |file, idx|
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')
@@ -2,6 +2,44 @@
2
2
 
3
3
  class Tryouts
4
4
  module Translators
5
+ # Translates Tryouts test files to Minitest format
6
+ #
7
+ # IMPORTANT: Context Mode Differences
8
+ # ==================================
9
+ #
10
+ # Tryouts supports two context modes that behave differently than Minitest:
11
+ #
12
+ # 1. Tryouts Shared Context (default):
13
+ # - Setup runs once, all tests share the same context object
14
+ # - Tests can modify variables/state and affect subsequent tests
15
+ # - Behaves like a Ruby script executing top-to-bottom
16
+ # - Designed for documentation-style tests where examples build on each other
17
+ #
18
+ # 2. Tryouts Fresh Context (--no-shared-context):
19
+ # - Setup @instance_variables are copied to each test's fresh context
20
+ # - Tests are isolated but inherit setup state
21
+ # - Similar to Minitest's setup method but with setup state inheritance
22
+ #
23
+ # Minitest Translation Behavior:
24
+ # ==============================
25
+ # - Uses setup method which runs before each test (Minitest standard)
26
+ # - Each test method gets fresh context (Minitest standard)
27
+ # - Tests that rely on shared state between test cases WILL FAIL
28
+ # - This is intentional and reveals inappropriate test dependencies
29
+ #
30
+ # Example that works in Tryouts shared mode but fails in Minitest:
31
+ # ## TEST 1
32
+ # @counter = 1
33
+ # @counter
34
+ # #=> 1
35
+ #
36
+ # ## TEST 2
37
+ # @counter += 1 # Will be reset to 1 by setup, then fail
38
+ # @counter
39
+ # #=> 2
40
+ #
41
+ # Recommendation: Write tryouts tests that work in fresh context mode
42
+ # if you plan to use Minitest translation.
5
43
  class MinitestTranslator
6
44
  def initialize
7
45
  require 'minitest/test'
@@ -2,6 +2,44 @@
2
2
 
3
3
  class Tryouts
4
4
  module Translators
5
+ # Translates Tryouts test files to RSpec format
6
+ #
7
+ # IMPORTANT: Context Mode Differences
8
+ # ==================================
9
+ #
10
+ # Tryouts supports two context modes that behave differently than RSpec:
11
+ #
12
+ # 1. Tryouts Shared Context (default):
13
+ # - Setup runs once, all tests share the same context object
14
+ # - Tests can modify variables/state and affect subsequent tests
15
+ # - Behaves like a Ruby script executing top-to-bottom
16
+ # - Designed for documentation-style tests where examples build on each other
17
+ #
18
+ # 2. Tryouts Fresh Context (--no-shared-context):
19
+ # - Setup @instance_variables are copied to each test's fresh context
20
+ # - Tests are isolated but inherit setup state
21
+ # - Similar to RSpec's before(:each) but with setup state inheritance
22
+ #
23
+ # RSpec Translation Behavior:
24
+ # ===========================
25
+ # - Uses before(:all) for setup code (closest equivalent to shared context)
26
+ # - Each 'it' block gets fresh context (RSpec standard)
27
+ # - Tests that rely on shared state between test cases WILL FAIL
28
+ # - This is intentional and reveals inappropriate test dependencies
29
+ #
30
+ # Example that works in Tryouts shared mode but fails in RSpec:
31
+ # ## TEST 1
32
+ # @counter = 1
33
+ # @counter
34
+ # #=> 1
35
+ #
36
+ # ## TEST 2
37
+ # @counter += 1 # Will be nil in RSpec, causing failure
38
+ # @counter
39
+ # #=> 2
40
+ #
41
+ # Recommendation: Write tryouts tests that work in fresh context mode
42
+ # if you plan to use RSpec translation.
5
43
  class RSpecTranslator
6
44
  def initialize
7
45
  require 'rspec/core'
@@ -1,5 +1,5 @@
1
1
  # lib/tryouts/version.rb
2
2
 
3
3
  class Tryouts
4
- VERSION = '3.1.0'
4
+ VERSION = '3.1.2'
5
5
  end
data/lib/tryouts.rb CHANGED
@@ -1,14 +1,12 @@
1
1
  # lib/tryouts.rb
2
2
 
3
-
4
-
5
3
  require 'stringio'
6
4
  require 'timeout'
7
5
 
8
6
  TRYOUTS_LIB_HOME = __dir__ unless defined?(TRYOUTS_LIB_HOME)
9
7
 
10
8
  require_relative 'tryouts/console'
11
- require_relative 'tryouts/testbatch'
9
+ require_relative 'tryouts/test_batch'
12
10
  require_relative 'tryouts/version'
13
11
  require_relative 'tryouts/prism_parser'
14
12
  require_relative 'tryouts/cli'
@@ -20,7 +18,6 @@ class Tryouts
20
18
  @fails = false
21
19
  @container = Class.new
22
20
  @cases = [] # rubocop:disable ThreadSafety/MutableClassInstanceVariable
23
- @sysinfo = nil
24
21
  @testcase_io = StringIO.new
25
22
 
26
23
  module ClassMethods
@@ -28,12 +25,6 @@ class Tryouts
28
25
  attr_writer :debug
29
26
  attr_reader :cases, :testcase_io
30
27
 
31
- def sysinfo
32
- require 'sysinfo'
33
- @sysinfo ||= SysInfo.new
34
- @sysinfo
35
- end
36
-
37
28
  def debug?
38
29
  @debug == true
39
30
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tryouts
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
@@ -37,26 +37,6 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '3.0'
40
- - !ruby/object:Gem::Dependency
41
- name: sysinfo
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: '0.8'
47
- - - "<"
48
- - !ruby/object:Gem::Version
49
- version: '1.0'
50
- type: :runtime
51
- prerelease: false
52
- version_requirements: !ruby/object:Gem::Requirement
53
- requirements:
54
- - - ">="
55
- - !ruby/object:Gem::Version
56
- version: '0.8'
57
- - - "<"
58
- - !ruby/object:Gem::Version
59
- version: '1.0'
60
40
  description: A simple test framework for Ruby code that uses introspection to allow
61
41
  defining checks in comments.
62
42
  email: gems@solutious.com
@@ -98,10 +78,10 @@ files:
98
78
  - lib/tryouts/expectation_evaluators/true.rb
99
79
  - lib/tryouts/file_processor.rb
100
80
  - lib/tryouts/prism_parser.rb
81
+ - lib/tryouts/test_batch.rb
82
+ - lib/tryouts/test_case.rb
101
83
  - lib/tryouts/test_executor.rb
102
84
  - lib/tryouts/test_runner.rb
103
- - lib/tryouts/testbatch.rb
104
- - lib/tryouts/testcase.rb
105
85
  - lib/tryouts/translators/minitest_translator.rb
106
86
  - lib/tryouts/translators/rspec_translator.rb
107
87
  - lib/tryouts/version.rb