tryouts 3.3.1 → 3.4.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 +15 -4
- data/lib/tryouts/cli/formatters/agent.rb +450 -0
- data/lib/tryouts/cli/formatters/compact.rb +4 -3
- data/lib/tryouts/cli/formatters/factory.rb +5 -0
- data/lib/tryouts/cli/formatters/quiet.rb +4 -3
- data/lib/tryouts/cli/formatters/token_budget.rb +157 -0
- data/lib/tryouts/cli/formatters/verbose.rb +3 -2
- data/lib/tryouts/cli/formatters.rb +2 -0
- data/lib/tryouts/cli/opts.rb +86 -9
- data/lib/tryouts/console.rb +32 -4
- data/lib/tryouts/expectation_evaluators/exception.rb +8 -2
- data/lib/tryouts/expectation_evaluators/non_nil.rb +77 -0
- data/lib/tryouts/expectation_evaluators/registry.rb +2 -0
- data/lib/tryouts/file_processor.rb +6 -2
- data/lib/tryouts/parsers/enhanced_parser.rb +2 -0
- data/lib/tryouts/parsers/prism_parser.rb +2 -0
- data/lib/tryouts/parsers/shared_methods.rb +5 -1
- data/lib/tryouts/test_batch.rb +26 -10
- data/lib/tryouts/test_case.rb +3 -3
- data/lib/tryouts/test_executor.rb +6 -4
- data/lib/tryouts/test_result_aggregator.rb +138 -0
- data/lib/tryouts/test_runner.rb +81 -20
- data/lib/tryouts/version.rb +1 -1
- data/lib/tryouts.rb +5 -1
- metadata +19 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6b0f0a0ca7de83a58c4e822d127a963bb1fb08523d9146b24c8b33f8a2c12ff2
|
4
|
+
data.tar.gz: 2f4e61611e23176da42cc9bbf82a92a2cc1e9d6049943fd4d4827dcd2b34c84f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1505d26c47b4fb3fb75675425e6aaac3e1ba81b6f39ac8ae47622ae43acd0c19a13a16e82486663de0201f21bf7b997281744fc6d69cae6116e67065803b4a39
|
7
|
+
data.tar.gz: ca57f3c38348023c57f5e382fe8b45e29877393fae155a718c921550b4a2200b0fe37c1c31be4f41dcbc934527d8c6b53e74fbe88fcc574c53cbc3d765bb3534
|
data/README.md
CHANGED
@@ -1,17 +1,21 @@
|
|
1
|
-
# Tryouts v3
|
1
|
+
# Tryouts v3
|
2
2
|
|
3
3
|
**Ruby tests that read like documentation.**
|
4
4
|
|
5
5
|
A modern test framework for Ruby that uses comments to define expectations. Tryouts are meant to double as documentation, so the Ruby code should be plain and reminiscent of real code.
|
6
6
|
|
7
|
+
> [!NOTE]
|
8
|
+
> **Agent-Optimized Output**: Tryouts includes specialized output modes for LLM consumption with `--agent` flag, providing structured, token-efficient test results that are 60-80% smaller than traditional output while preserving debugging context.
|
9
|
+
|
7
10
|
> [!WARNING]
|
8
|
-
> Version 3.0+ uses Ruby's Prism parser and pattern matching, requiring Ruby 3.
|
11
|
+
> Version 3.0+ uses Ruby's Prism parser and pattern matching, requiring Ruby 3.2+
|
9
12
|
|
10
13
|
## Key Features
|
11
14
|
|
12
15
|
- **Documentation-style tests** using comment-based expectations (`#=>`)
|
13
16
|
- **Great expectation syntax** for more expressive assertions (`#==>` for true, `#=/=>` for false, `#=:>` for class/module)
|
14
17
|
- **Framework integration** write with tryouts syntax, run with RSpec or Minitest
|
18
|
+
- **Agent-optimized output** structured, token-efficient output for LLM consumption
|
15
19
|
- **Enhanced error reporting** with line numbers and context
|
16
20
|
|
17
21
|
## Installation
|
@@ -117,6 +121,13 @@ try -v # verbose (includes source code and return values)
|
|
117
121
|
try -q # quiet mode
|
118
122
|
try -f # show failures only
|
119
123
|
try -D # debug mode
|
124
|
+
|
125
|
+
# Agent-optimized output for LLMs
|
126
|
+
try --agent # structured, token-efficient output
|
127
|
+
try --agent --agent-focus summary # show only counts and problem files
|
128
|
+
try --agent --agent-focus first-failure # show first failure per file
|
129
|
+
try --agent --agent-focus critical # show only errors/exceptions
|
130
|
+
try --agent --agent-limit 1000 # limit output to 1000 tokens
|
120
131
|
```
|
121
132
|
|
122
133
|
### Exit Codes
|
@@ -127,14 +138,14 @@ try -D # debug mode
|
|
127
138
|
|
128
139
|
## Requirements
|
129
140
|
|
130
|
-
- **Ruby >= 3.2
|
141
|
+
- **Ruby >= 3.2** (for Prism parser and pattern matching)
|
131
142
|
- **RSpec** or **Minitest** (optional, for framework integration)
|
132
143
|
|
133
144
|
## Modern Architecture (v3+)
|
134
145
|
|
135
146
|
### Core Components
|
136
147
|
|
137
|
-
- **Prism Parser**:
|
148
|
+
- **Prism Parser**: Native Ruby parsing with pattern matching for line classification
|
138
149
|
- **Data Structures**: Immutable `Data.define` classes for test representation
|
139
150
|
- **Framework Translators**: Convert tryouts to RSpec/Minitest format
|
140
151
|
- **CLI**: Modern command-line interface with framework selection
|
@@ -0,0 +1,450 @@
|
|
1
|
+
# lib/tryouts/cli/formatters/agent.rb
|
2
|
+
|
3
|
+
require_relative 'token_budget'
|
4
|
+
|
5
|
+
class Tryouts
|
6
|
+
class CLI
|
7
|
+
# Agent-optimized formatter designed for LLM context management
|
8
|
+
# Features:
|
9
|
+
# - Token budget awareness
|
10
|
+
# - Structured YAML-like output
|
11
|
+
# - No redundant file paths
|
12
|
+
# - Smart truncation
|
13
|
+
# - Hierarchical organization
|
14
|
+
class AgentFormatter
|
15
|
+
include FormatterInterface
|
16
|
+
|
17
|
+
def initialize(options = {})
|
18
|
+
super
|
19
|
+
@budget = TokenBudget.new(options[:agent_limit] || TokenBudget::DEFAULT_LIMIT)
|
20
|
+
@focus_mode = options[:agent_focus] || :failures
|
21
|
+
@collected_files = []
|
22
|
+
@current_file_data = nil
|
23
|
+
@total_stats = { files: 0, tests: 0, failures: 0, errors: 0, elapsed: 0 }
|
24
|
+
@output_rendered = false
|
25
|
+
|
26
|
+
# No colors in agent mode for cleaner parsing
|
27
|
+
@use_colors = false
|
28
|
+
end
|
29
|
+
|
30
|
+
# Phase-level output - collect data, don't output immediately
|
31
|
+
def phase_header(message, file_count: nil)
|
32
|
+
# Store file count for later use, but only store actual file count
|
33
|
+
if file_count && message.include?("FILES")
|
34
|
+
@total_stats[:files] = file_count
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# File-level operations - start collecting file data
|
39
|
+
def file_start(file_path, context_info: {})
|
40
|
+
@current_file_data = {
|
41
|
+
path: relative_path(file_path),
|
42
|
+
tests: 0,
|
43
|
+
failures: [],
|
44
|
+
errors: [],
|
45
|
+
passed: 0
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def file_end(file_path, context_info: {})
|
50
|
+
# Finalize current file data
|
51
|
+
if @current_file_data
|
52
|
+
@collected_files << @current_file_data
|
53
|
+
@current_file_data = nil
|
54
|
+
end
|
55
|
+
# REMOVED: No longer attempts to render here to avoid premature output
|
56
|
+
end
|
57
|
+
|
58
|
+
def file_parsed(_file_path, test_count:, setup_present: false, teardown_present: false)
|
59
|
+
@current_file_data[:tests] = test_count if @current_file_data
|
60
|
+
@total_stats[:tests] += test_count
|
61
|
+
end
|
62
|
+
|
63
|
+
def file_result(_file_path, total_tests:, failed_count:, error_count:, elapsed_time: nil)
|
64
|
+
# Always update global totals
|
65
|
+
@total_stats[:failures] += failed_count
|
66
|
+
@total_stats[:errors] += error_count
|
67
|
+
@total_stats[:elapsed] += elapsed_time if elapsed_time
|
68
|
+
|
69
|
+
# Update per-file data when available
|
70
|
+
if @current_file_data
|
71
|
+
@current_file_data[:passed] = total_tests - failed_count - error_count
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
# Test-level operations - collect failure data
|
77
|
+
def test_result(result_packet)
|
78
|
+
return unless @current_file_data
|
79
|
+
|
80
|
+
# For summary mode, we still need to collect failures for counting, just don't build detailed data
|
81
|
+
if result_packet.failed? || result_packet.error?
|
82
|
+
if @focus_mode == :summary
|
83
|
+
# Just track counts for summary
|
84
|
+
if result_packet.error?
|
85
|
+
@current_file_data[:errors] << { basic: true }
|
86
|
+
else
|
87
|
+
@current_file_data[:failures] << { basic: true }
|
88
|
+
end
|
89
|
+
else
|
90
|
+
# Build detailed failure data for other modes
|
91
|
+
failure_data = build_failure_data(result_packet)
|
92
|
+
|
93
|
+
if result_packet.error?
|
94
|
+
@current_file_data[:errors] << failure_data
|
95
|
+
else
|
96
|
+
@current_file_data[:failures] << failure_data
|
97
|
+
end
|
98
|
+
|
99
|
+
# Mark truncation for first-failure mode (handle limiting in render phase)
|
100
|
+
if (@focus_mode == :first_failure || @focus_mode == :'first-failure') &&
|
101
|
+
(@current_file_data[:failures].size + @current_file_data[:errors].size) > 1
|
102
|
+
@current_file_data[:truncated] = true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Summary operations - reliable trigger for rendering
|
109
|
+
def batch_summary(failure_collector)
|
110
|
+
# This becomes the single, reliable trigger for rendering
|
111
|
+
grand_total(
|
112
|
+
total_tests: @total_stats[:tests],
|
113
|
+
failed_count: @collected_files.sum { |f| f[:failures].size },
|
114
|
+
error_count: @collected_files.sum { |f| f[:errors].size },
|
115
|
+
successful_files: @collected_files.size - @collected_files.count { |f| f[:failures].any? || f[:errors].any? },
|
116
|
+
total_files: @collected_files.size,
|
117
|
+
elapsed_time: @total_stats[:elapsed]
|
118
|
+
) unless @output_rendered
|
119
|
+
end
|
120
|
+
|
121
|
+
def grand_total(total_tests:, failed_count:, error_count:, successful_files:, total_files:, elapsed_time:)
|
122
|
+
return if @output_rendered # Prevent double rendering
|
123
|
+
|
124
|
+
@total_stats.merge!(
|
125
|
+
tests: total_tests,
|
126
|
+
failures: failed_count,
|
127
|
+
errors: error_count,
|
128
|
+
successful_files: successful_files,
|
129
|
+
total_files: total_files,
|
130
|
+
elapsed: elapsed_time
|
131
|
+
)
|
132
|
+
|
133
|
+
# Now render all collected data
|
134
|
+
render_agent_output
|
135
|
+
@output_rendered = true
|
136
|
+
end
|
137
|
+
|
138
|
+
# Override live status - not needed for agent mode
|
139
|
+
def live_status_capabilities
|
140
|
+
{
|
141
|
+
supports_coordination: false,
|
142
|
+
output_frequency: :none,
|
143
|
+
requires_tty: false
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def build_failure_data(result_packet)
|
150
|
+
test_case = result_packet.test_case
|
151
|
+
|
152
|
+
failure_data = {
|
153
|
+
line: (test_case.first_expectation_line || test_case.line_range&.first || 0) + 1,
|
154
|
+
test: test_case.description.to_s.empty? ? 'unnamed test' : test_case.description.to_s
|
155
|
+
}
|
156
|
+
|
157
|
+
case result_packet.status
|
158
|
+
when :error
|
159
|
+
error = result_packet.error
|
160
|
+
failure_data[:error] = error ? "#{error.class.name}: #{error.message}" : 'unknown error'
|
161
|
+
when :failed
|
162
|
+
if result_packet.expected_results.any? && result_packet.actual_results.any?
|
163
|
+
expected = @budget.smart_truncate(result_packet.first_expected, max_tokens: 25)
|
164
|
+
actual = @budget.smart_truncate(result_packet.first_actual, max_tokens: 25)
|
165
|
+
failure_data[:expected] = expected
|
166
|
+
failure_data[:got] = actual
|
167
|
+
|
168
|
+
# Add diff for strings if budget allows
|
169
|
+
if result_packet.first_expected.is_a?(String) &&
|
170
|
+
result_packet.first_actual.is_a?(String) &&
|
171
|
+
@budget.has_budget?
|
172
|
+
failure_data[:diff] = generate_simple_diff(result_packet.first_expected, result_packet.first_actual)
|
173
|
+
end
|
174
|
+
else
|
175
|
+
failure_data[:reason] = 'test failed'
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
failure_data
|
180
|
+
end
|
181
|
+
|
182
|
+
def generate_simple_diff(expected, actual)
|
183
|
+
return nil unless @budget.remaining > 100 # Only if we have decent budget left
|
184
|
+
|
185
|
+
# Simple line-by-line diff
|
186
|
+
exp_lines = expected.split("\n")
|
187
|
+
act_lines = actual.split("\n")
|
188
|
+
|
189
|
+
diff_lines = []
|
190
|
+
diff_lines << "- #{act_lines.first}" if act_lines.any?
|
191
|
+
diff_lines << "+ #{exp_lines.first}" if exp_lines.any?
|
192
|
+
|
193
|
+
diff_result = diff_lines.join("\n")
|
194
|
+
return @budget.fit_text(diff_result) if @budget.would_exceed?(diff_result)
|
195
|
+
diff_result
|
196
|
+
end
|
197
|
+
|
198
|
+
def render_agent_output
|
199
|
+
case @focus_mode
|
200
|
+
when :summary
|
201
|
+
render_summary_only
|
202
|
+
when :critical
|
203
|
+
render_critical_only
|
204
|
+
else
|
205
|
+
render_full_structured
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def render_summary_only
|
210
|
+
output = []
|
211
|
+
|
212
|
+
# Count failures manually from collected file data (same as other render methods)
|
213
|
+
failed_count = @collected_files.sum { |f| f[:failures].size }
|
214
|
+
error_count = @collected_files.sum { |f| f[:errors].size }
|
215
|
+
issues_count = failed_count + error_count
|
216
|
+
passed_count = [@total_stats[:tests] - issues_count, 0].max
|
217
|
+
|
218
|
+
if issues_count > 0
|
219
|
+
status = "FAIL: #{issues_count}/#{@total_stats[:tests]} tests"
|
220
|
+
details = []
|
221
|
+
details << "#{failed_count} failed" if failed_count > 0
|
222
|
+
details << "#{error_count} errors" if error_count > 0
|
223
|
+
status += " (#{details.join(', ')}, #{passed_count} passed)"
|
224
|
+
else
|
225
|
+
status = "PASS: #{@total_stats[:tests]} tests passed"
|
226
|
+
end
|
227
|
+
|
228
|
+
status += " (#{format_time(@total_stats[:elapsed])})" if @total_stats[:elapsed]
|
229
|
+
|
230
|
+
output << status
|
231
|
+
|
232
|
+
# Show which files had failures
|
233
|
+
files_with_issues = @collected_files.select { |f| f[:failures].any? || f[:errors].any? }
|
234
|
+
if files_with_issues.any?
|
235
|
+
output << ""
|
236
|
+
output << "Files with issues:"
|
237
|
+
files_with_issues.each do |file_data|
|
238
|
+
issue_count = file_data[:failures].size + file_data[:errors].size
|
239
|
+
output << " #{file_data[:path]}: #{issue_count} issue#{'s' if issue_count != 1}"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
puts output.join("\n")
|
244
|
+
end
|
245
|
+
|
246
|
+
def render_critical_only
|
247
|
+
# Only show errors (exceptions), skip assertion failures
|
248
|
+
critical_files = @collected_files.select { |f| f[:errors].any? }
|
249
|
+
|
250
|
+
if critical_files.empty?
|
251
|
+
puts "No critical errors found"
|
252
|
+
return
|
253
|
+
end
|
254
|
+
|
255
|
+
output = []
|
256
|
+
output << "CRITICAL: #{critical_files.size} file#{'s' if critical_files.size != 1} with errors"
|
257
|
+
output << ""
|
258
|
+
|
259
|
+
critical_files.each do |file_data|
|
260
|
+
unless @budget.has_budget?
|
261
|
+
output << "... (truncated due to token limit)"
|
262
|
+
break
|
263
|
+
end
|
264
|
+
|
265
|
+
output << "#{file_data[:path]}:"
|
266
|
+
|
267
|
+
file_data[:errors].each do |error|
|
268
|
+
error_line = " L#{error[:line]}: #{error[:error]}"
|
269
|
+
if @budget.would_exceed?(error_line)
|
270
|
+
output << @budget.fit_text(error_line)
|
271
|
+
else
|
272
|
+
output << error_line
|
273
|
+
@budget.consume(error_line)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
output << ""
|
278
|
+
end
|
279
|
+
|
280
|
+
puts output.join("\n")
|
281
|
+
end
|
282
|
+
|
283
|
+
def render_full_structured
|
284
|
+
output = []
|
285
|
+
|
286
|
+
# Header with overall stats
|
287
|
+
issues_count = @total_stats[:failures] + @total_stats[:errors]
|
288
|
+
passed_count = [@total_stats[:tests] - issues_count, 0].max
|
289
|
+
|
290
|
+
files_count = if @total_stats[:files].to_i > 0
|
291
|
+
@total_stats[:files]
|
292
|
+
else
|
293
|
+
@total_stats[:total_files] || @collected_files.size
|
294
|
+
end
|
295
|
+
|
296
|
+
if issues_count > 0
|
297
|
+
status_line = "FAIL: #{issues_count}/#{@total_stats[:tests]} tests (#{files_count} files, #{format_time(@total_stats[:elapsed])})"
|
298
|
+
else
|
299
|
+
status_line = "PASS: #{@total_stats[:tests]} tests (#{files_count} files, #{format_time(@total_stats[:elapsed])})"
|
300
|
+
end
|
301
|
+
|
302
|
+
# Always include status line
|
303
|
+
output << status_line
|
304
|
+
@budget.force_consume(status_line)
|
305
|
+
|
306
|
+
# Only show files with issues (unless focus is different)
|
307
|
+
files_to_show = case @focus_mode
|
308
|
+
when :failures, :first_failure
|
309
|
+
@collected_files.select { |f| f[:failures].any? || f[:errors].any? }
|
310
|
+
else
|
311
|
+
@collected_files.select { |f| f[:failures].any? || f[:errors].any? }
|
312
|
+
end
|
313
|
+
|
314
|
+
if files_to_show.any?
|
315
|
+
output << ""
|
316
|
+
@budget.consume("\n")
|
317
|
+
|
318
|
+
files_to_show.each do |file_data|
|
319
|
+
break unless @budget.has_budget?
|
320
|
+
|
321
|
+
file_section = render_file_section(file_data)
|
322
|
+
if @budget.would_exceed?(file_section)
|
323
|
+
# Try to fit what we can
|
324
|
+
truncated = @budget.fit_text(file_section, preserve_suffix: "\n ... (truncated)")
|
325
|
+
output << truncated if truncated.length > 20 # Only if meaningful content remains
|
326
|
+
break
|
327
|
+
else
|
328
|
+
output << file_section
|
329
|
+
@budget.consume(file_section)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
# Final summary line
|
335
|
+
summary = "Summary: #{passed_count} passed, #{@total_stats[:failures]} failed"
|
336
|
+
summary += ", #{@total_stats[:errors]} errors" if @total_stats[:errors] > 0
|
337
|
+
summary += " in #{@total_stats[:files]} files"
|
338
|
+
|
339
|
+
output << ""
|
340
|
+
output << summary
|
341
|
+
|
342
|
+
puts output.join("\n")
|
343
|
+
end
|
344
|
+
|
345
|
+
def render_file_section(file_data)
|
346
|
+
lines = []
|
347
|
+
|
348
|
+
# File header
|
349
|
+
lines << "#{file_data[:path]}:"
|
350
|
+
|
351
|
+
# For first-failure mode, only show first error or failure
|
352
|
+
if @focus_mode == :first_failure || @focus_mode == :'first-failure'
|
353
|
+
shown_count = 0
|
354
|
+
|
355
|
+
# Show first error
|
356
|
+
if file_data[:errors].any? && shown_count == 0
|
357
|
+
error = file_data[:errors].first
|
358
|
+
lines << " L#{error[:line]}: #{error[:error]}"
|
359
|
+
lines << " Test: #{error[:test]}" if error[:test] != 'unnamed test'
|
360
|
+
shown_count += 1
|
361
|
+
end
|
362
|
+
|
363
|
+
# Show first failure if no error was shown
|
364
|
+
if file_data[:failures].any? && shown_count == 0
|
365
|
+
failure = file_data[:failures].first
|
366
|
+
line_parts = [" L#{failure[:line]}:"]
|
367
|
+
|
368
|
+
if failure[:expected] && failure[:got]
|
369
|
+
line_parts << "expected #{failure[:expected]}, got #{failure[:got]}"
|
370
|
+
elsif failure[:reason]
|
371
|
+
line_parts << failure[:reason]
|
372
|
+
end
|
373
|
+
|
374
|
+
lines << line_parts.join(' ')
|
375
|
+
lines << " Test: #{failure[:test]}" if failure[:test] != 'unnamed test'
|
376
|
+
|
377
|
+
# Add diff if available and budget allows
|
378
|
+
if failure[:diff] && @budget.remaining > 50
|
379
|
+
lines << " Diff:"
|
380
|
+
failure[:diff].split("\n").each { |diff_line| lines << " #{diff_line}" }
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
# Show truncation notice
|
385
|
+
total_issues = file_data[:errors].size + file_data[:failures].size
|
386
|
+
if total_issues > 1
|
387
|
+
lines << " ... (#{total_issues - 1} more failures not shown)"
|
388
|
+
end
|
389
|
+
else
|
390
|
+
# Normal mode - show all errors and failures
|
391
|
+
# Errors first (more critical)
|
392
|
+
file_data[:errors].each do |error|
|
393
|
+
next if error[:basic] # Skip basic entries from summary mode
|
394
|
+
lines << " L#{error[:line]}: #{error[:error]}"
|
395
|
+
lines << " Test: #{error[:test]}" if error[:test] != 'unnamed test'
|
396
|
+
end
|
397
|
+
|
398
|
+
# Then failures
|
399
|
+
file_data[:failures].each do |failure|
|
400
|
+
next if failure[:basic] # Skip basic entries from summary mode
|
401
|
+
line_parts = [" L#{failure[:line]}:"]
|
402
|
+
|
403
|
+
if failure[:expected] && failure[:got]
|
404
|
+
line_parts << "expected #{failure[:expected]}, got #{failure[:got]}"
|
405
|
+
elsif failure[:reason]
|
406
|
+
line_parts << failure[:reason]
|
407
|
+
end
|
408
|
+
|
409
|
+
lines << line_parts.join(' ')
|
410
|
+
lines << " Test: #{failure[:test]}" if failure[:test] != 'unnamed test'
|
411
|
+
|
412
|
+
# Add diff if available and budget allows
|
413
|
+
if failure[:diff] && @budget.remaining > 50
|
414
|
+
lines << " Diff:"
|
415
|
+
failure[:diff].split("\n").each { |diff_line| lines << " #{diff_line}" }
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# Show truncation notice if applicable
|
420
|
+
if file_data[:truncated]
|
421
|
+
lines << " ... (more failures not shown)"
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
lines.join("\n")
|
426
|
+
end
|
427
|
+
|
428
|
+
def relative_path(file_path)
|
429
|
+
# Remove leading path components to save tokens
|
430
|
+
path = Pathname.new(file_path).relative_path_from(Pathname.pwd).to_s
|
431
|
+
# If relative path is longer, use just filename
|
432
|
+
path.include?('../') ? File.basename(file_path) : path
|
433
|
+
rescue
|
434
|
+
File.basename(file_path)
|
435
|
+
end
|
436
|
+
|
437
|
+
def format_time(seconds)
|
438
|
+
return '0ms' unless seconds
|
439
|
+
|
440
|
+
if seconds < 0.001
|
441
|
+
"#{(seconds * 1_000_000).round}μs"
|
442
|
+
elsif seconds < 1
|
443
|
+
"#{(seconds * 1000).round}ms"
|
444
|
+
else
|
445
|
+
"#{seconds.round(2)}s"
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
@@ -11,6 +11,7 @@ class Tryouts
|
|
11
11
|
@show_debug = options.fetch(:debug, false)
|
12
12
|
@show_trace = options.fetch(:trace, false)
|
13
13
|
@show_passed = options.fetch(:show_passed, true)
|
14
|
+
@show_stack_traces = options.fetch(:stack_traces, false) || options.fetch(:debug, false)
|
14
15
|
end
|
15
16
|
|
16
17
|
# Phase-level output - minimal for compact mode
|
@@ -238,10 +239,10 @@ class Tryouts
|
|
238
239
|
def error_message(message, backtrace: nil)
|
239
240
|
@stderr.puts Console.color(:red, "ERROR: #{message}")
|
240
241
|
|
241
|
-
return unless backtrace && @
|
242
|
+
return unless backtrace && @show_stack_traces
|
242
243
|
|
243
|
-
|
244
|
-
@stderr.puts indent_text(line
|
244
|
+
Console.pretty_backtrace(backtrace, limit: 3).each do |line|
|
245
|
+
@stderr.puts indent_text(line, 1)
|
245
246
|
end
|
246
247
|
end
|
247
248
|
|
@@ -12,6 +12,11 @@ class Tryouts
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def self.create_formatter(options = {})
|
15
|
+
# Check for agent mode first (takes precedence)
|
16
|
+
if options[:agent]
|
17
|
+
return AgentFormatter.new(options)
|
18
|
+
end
|
19
|
+
|
15
20
|
# Map boolean flags to format symbols if format not explicitly set
|
16
21
|
format = options[:format]&.to_sym || determine_format_from_flags(options)
|
17
22
|
|
@@ -10,6 +10,7 @@ class Tryouts
|
|
10
10
|
super
|
11
11
|
@show_errors = options.fetch(:show_errors, true)
|
12
12
|
@show_final_summary = options.fetch(:show_final_summary, true)
|
13
|
+
@show_stack_traces = options.fetch(:stack_traces, false) || options.fetch(:debug, false)
|
13
14
|
@current_file = nil
|
14
15
|
end
|
15
16
|
|
@@ -80,10 +81,10 @@ class Tryouts
|
|
80
81
|
@stderr.puts
|
81
82
|
@stderr.puts Console.color(:red, "ERROR: #{message}")
|
82
83
|
|
83
|
-
return unless backtrace && @
|
84
|
+
return unless backtrace && @show_stack_traces
|
84
85
|
|
85
|
-
|
86
|
-
@stderr.puts " #{line
|
86
|
+
Console.pretty_backtrace(backtrace, limit: 3).each do |line|
|
87
|
+
@stderr.puts " #{line}"
|
87
88
|
end
|
88
89
|
end
|
89
90
|
|