tryouts 3.3.2 → 3.5.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 +32 -6
- data/exe/try +8 -5
- data/lib/tryouts/cli/formatters/agent.rb +576 -0
- data/lib/tryouts/cli/formatters/base.rb +5 -1
- data/lib/tryouts/cli/formatters/compact.rb +14 -4
- data/lib/tryouts/cli/formatters/factory.rb +5 -0
- data/lib/tryouts/cli/formatters/output_manager.rb +4 -0
- data/lib/tryouts/cli/formatters/token_budget.rb +157 -0
- data/lib/tryouts/cli/formatters/verbose.rb +69 -56
- data/lib/tryouts/cli/formatters.rb +2 -0
- data/lib/tryouts/cli/line_spec_parser.rb +109 -0
- data/lib/tryouts/cli/opts.rb +80 -7
- data/lib/tryouts/cli.rb +22 -5
- data/lib/tryouts/file_processor.rb +37 -2
- data/lib/tryouts/parser_warning.rb +26 -0
- data/lib/tryouts/parsers/base_parser.rb +4 -1
- data/lib/tryouts/parsers/shared_methods.rb +50 -1
- data/lib/tryouts/test_case.rb +1 -1
- data/lib/tryouts/test_executor.rb +2 -0
- data/lib/tryouts/test_runner.rb +23 -7
- data/lib/tryouts/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b692cc3a63ac86e060b52e77c246305be0f209dbf06b31b1b08deefbc06434a
|
4
|
+
data.tar.gz: 156e86aa4bde377aa2158f3a059983a6fe5f58796f71545c9895a9b57668b126
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d289a9b5ccd6694bd63fe4ca2a0c8aaf610d702e74380bf4975ae78ffaf377d0f82a9ec481dbc51105de9f3b07681c32eaa8a1c3566a1eebd0a861796eaff1e3
|
7
|
+
data.tar.gz: a0db3ebf76b5a291e6ea9d81f1f6e268a95320d28040a10f2225171b221f04bb2ed525bae3f79785d50f4269eb7c9be1dec242012bbca0a6d4588ac9e7fdd362
|
data/README.md
CHANGED
@@ -1,17 +1,21 @@
|
|
1
|
-
# Tryouts
|
1
|
+
# Tryouts - A Ruby Testing Framework
|
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,8 +121,30 @@ 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
|
|
133
|
+
#### Why Not Pipe Test Output Directly to AI?
|
134
|
+
|
135
|
+
Raw test output creates several problems when working with AI assistants:
|
136
|
+
|
137
|
+
- **Token bloat**: Verbose formatting wastes 60-80% of your context window on styling
|
138
|
+
- **Signal vs noise**: Important failures get buried in passing test details and framework boilerplate
|
139
|
+
- **Inconsistent parsing**: AI struggles with varying output formats across different test runs
|
140
|
+
- **Context overflow**: Large test suites exceed AI token limits, truncating critical information
|
141
|
+
|
142
|
+
#### TOPA: A Better Approach
|
143
|
+
|
144
|
+
Tryouts' `--agent` mode inspired the development of **TOPA (Test Output Protocol for AI)** - a standardized format optimized for AI analysis. The [tpane](https://github.com/delano/tpane) tool implements this protocol, transforming any test framework's output into structured, token-efficient formats.
|
145
|
+
|
146
|
+
Instead of overwhelming AI with raw output, TOPA provides clean semantic data focusing on what actually needs attention - failures, errors, and actionable context.
|
147
|
+
|
122
148
|
### Exit Codes
|
123
149
|
|
124
150
|
- `0`: All tests pass
|
@@ -127,14 +153,14 @@ try -D # debug mode
|
|
127
153
|
|
128
154
|
## Requirements
|
129
155
|
|
130
|
-
- **Ruby >= 3.2
|
156
|
+
- **Ruby >= 3.2** (for Prism parser and pattern matching)
|
131
157
|
- **RSpec** or **Minitest** (optional, for framework integration)
|
132
158
|
|
133
159
|
## Modern Architecture (v3+)
|
134
160
|
|
135
161
|
### Core Components
|
136
162
|
|
137
|
-
- **Prism Parser**:
|
163
|
+
- **Prism Parser**: Native Ruby parsing with pattern matching for line classification
|
138
164
|
- **Data Structures**: Immutable `Data.define` classes for test representation
|
139
165
|
- **Framework Translators**: Convert tryouts to RSpec/Minitest format
|
140
166
|
- **CLI**: Modern command-line interface with framework selection
|
@@ -151,8 +177,8 @@ For real-world usage examples, see:
|
|
151
177
|
|
152
178
|
This version of Tryouts was developed with assistance from AI tools. The following tools provided significant help with architecture design, code generation, and documentation:
|
153
179
|
|
154
|
-
- **Claude Sonnet 4** - Architecture design, code generation, and documentation
|
155
|
-
- **Claude Desktop & Claude Code** - Interactive development sessions and debugging
|
180
|
+
- **Claude Sonnet 4, Opus 4.1** - Architecture design, code generation, and documentation
|
181
|
+
- **Claude Desktop & Claude Code (Max plan)** - Interactive development sessions and debugging
|
156
182
|
- **GitHub Copilot** - Code completion and refactoring assistance
|
157
183
|
- **Qodo Merge Pro** - Code review and quality improvements
|
158
184
|
|
data/exe/try
CHANGED
@@ -42,15 +42,18 @@ Tryouts.update_load_path(lib_glob) if Tryouts.respond_to?(:update_load_path)
|
|
42
42
|
begin
|
43
43
|
files, options = Tryouts::CLI.parse_args(ARGV)
|
44
44
|
|
45
|
-
# Expand files if directories are given
|
45
|
+
# Expand files if directories are given, preserving line specs
|
46
46
|
expanded_files = []
|
47
47
|
files.each do |file_or_dir|
|
48
|
-
|
48
|
+
# Parse line spec from the argument
|
49
|
+
path_part, line_spec = Tryouts::CLI::LineSpecParser.parse(file_or_dir)
|
50
|
+
|
51
|
+
if File.directory?(path_part)
|
49
52
|
# 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:
|
51
|
-
expanded_files.concat(dir_files.map { |f| File.join(
|
53
|
+
dir_files = Dir.glob(['**/*_try.rb', '**/*.try.rb'], base: path_part)
|
54
|
+
expanded_files.concat(dir_files.map { |f| File.join(path_part, f) })
|
52
55
|
else
|
53
|
-
# If it's a file, add it as-is
|
56
|
+
# If it's a file, add it as-is (with line spec if present)
|
54
57
|
expanded_files << file_or_dir
|
55
58
|
end
|
56
59
|
end
|
@@ -0,0 +1,576 @@
|
|
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
|
+
@options = options # Store all options for execution context display
|
26
|
+
@all_warnings = [] # Store warnings globally for execution details
|
27
|
+
@syntax_errors = [] # Store syntax errors for execution details
|
28
|
+
|
29
|
+
# No colors in agent mode for cleaner parsing
|
30
|
+
@use_colors = false
|
31
|
+
end
|
32
|
+
|
33
|
+
# Phase-level output - collect data, don't output immediately
|
34
|
+
def phase_header(message, file_count: nil)
|
35
|
+
# Store file count for later use, but only store actual file count
|
36
|
+
if file_count && message.include?("FILES")
|
37
|
+
@total_stats[:files] = file_count
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# File-level operations - start collecting file data
|
42
|
+
def file_start(file_path, context_info: {})
|
43
|
+
@current_file_data = {
|
44
|
+
path: relative_path(file_path),
|
45
|
+
tests: 0,
|
46
|
+
failures: [],
|
47
|
+
errors: [],
|
48
|
+
passed: 0,
|
49
|
+
context_info: context_info # Store context info for later display
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def file_end(file_path, context_info: {})
|
54
|
+
# Finalize current file data
|
55
|
+
if @current_file_data
|
56
|
+
@collected_files << @current_file_data
|
57
|
+
@current_file_data = nil
|
58
|
+
end
|
59
|
+
# REMOVED: No longer attempts to render here to avoid premature output
|
60
|
+
end
|
61
|
+
|
62
|
+
def file_parsed(_file_path, test_count:, setup_present: false, teardown_present: false)
|
63
|
+
if @current_file_data
|
64
|
+
@current_file_data[:tests] = test_count
|
65
|
+
end
|
66
|
+
@total_stats[:tests] += test_count
|
67
|
+
end
|
68
|
+
|
69
|
+
def parser_warnings(file_path, warnings:)
|
70
|
+
return if warnings.empty? || !@options.fetch(:warnings, true)
|
71
|
+
|
72
|
+
# Store warnings globally for execution details and per-file
|
73
|
+
warnings.each do |warning|
|
74
|
+
warning_data = {
|
75
|
+
type: warning.type.to_s,
|
76
|
+
message: warning.message,
|
77
|
+
line: warning.line_number,
|
78
|
+
suggestion: warning.suggestion,
|
79
|
+
file: relative_path(file_path)
|
80
|
+
}
|
81
|
+
@all_warnings << warning_data
|
82
|
+
end
|
83
|
+
|
84
|
+
# Also store in current file data for potential future use
|
85
|
+
if @current_file_data
|
86
|
+
@current_file_data[:warnings] = @all_warnings.select { |w| w[:file] == relative_path(file_path) }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def file_result(file_path, total_tests:, failed_count:, error_count:, elapsed_time: nil)
|
91
|
+
# Always update global totals
|
92
|
+
@total_stats[:failures] += failed_count
|
93
|
+
@total_stats[:errors] += error_count
|
94
|
+
@total_stats[:elapsed] += elapsed_time if elapsed_time
|
95
|
+
|
96
|
+
# Update per-file data - file_result is called AFTER file_end, so data is in @collected_files
|
97
|
+
relative_file_path = relative_path(file_path)
|
98
|
+
file_data = @collected_files.find { |f| f[:path] == relative_file_path }
|
99
|
+
|
100
|
+
if file_data
|
101
|
+
file_data[:passed] = total_tests - failed_count - error_count
|
102
|
+
# Also ensure tests count is correct if it wasn't set properly earlier
|
103
|
+
file_data[:tests] ||= total_tests
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
# Test-level operations - collect failure data
|
109
|
+
def test_result(result_packet)
|
110
|
+
return unless @current_file_data
|
111
|
+
|
112
|
+
# For summary mode, we still need to collect failures for counting, just don't build detailed data
|
113
|
+
if result_packet.failed? || result_packet.error?
|
114
|
+
if @focus_mode == :summary
|
115
|
+
# Just track counts for summary
|
116
|
+
if result_packet.error?
|
117
|
+
@current_file_data[:errors] << { basic: true }
|
118
|
+
else
|
119
|
+
@current_file_data[:failures] << { basic: true }
|
120
|
+
end
|
121
|
+
else
|
122
|
+
# Build detailed failure data for other modes
|
123
|
+
failure_data = build_failure_data(result_packet)
|
124
|
+
|
125
|
+
if result_packet.error?
|
126
|
+
@current_file_data[:errors] << failure_data
|
127
|
+
else
|
128
|
+
@current_file_data[:failures] << failure_data
|
129
|
+
end
|
130
|
+
|
131
|
+
# Mark truncation for first-failure mode (handle limiting in render phase)
|
132
|
+
if (@focus_mode == :first_failure || @focus_mode == :'first-failure') &&
|
133
|
+
(@current_file_data[:failures].size + @current_file_data[:errors].size) > 1
|
134
|
+
@current_file_data[:truncated] = true
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Summary operations - reliable trigger for rendering
|
141
|
+
def batch_summary(failure_collector)
|
142
|
+
# This becomes the single, reliable trigger for rendering
|
143
|
+
grand_total(
|
144
|
+
total_tests: @total_stats[:tests],
|
145
|
+
failed_count: @collected_files.sum { |f| f[:failures].size },
|
146
|
+
error_count: @collected_files.sum { |f| f[:errors].size },
|
147
|
+
successful_files: @collected_files.size - @collected_files.count { |f| f[:failures].any? || f[:errors].any? },
|
148
|
+
total_files: @collected_files.size,
|
149
|
+
elapsed_time: @total_stats[:elapsed]
|
150
|
+
) unless @output_rendered
|
151
|
+
end
|
152
|
+
|
153
|
+
def grand_total(total_tests:, failed_count:, error_count:, successful_files:, total_files:, elapsed_time:)
|
154
|
+
return if @output_rendered # Prevent double rendering
|
155
|
+
|
156
|
+
@total_stats.merge!(
|
157
|
+
tests: total_tests,
|
158
|
+
failures: failed_count,
|
159
|
+
errors: error_count,
|
160
|
+
successful_files: successful_files,
|
161
|
+
total_files: total_files,
|
162
|
+
elapsed: elapsed_time
|
163
|
+
)
|
164
|
+
|
165
|
+
# Now render all collected data
|
166
|
+
render_agent_output
|
167
|
+
@output_rendered = true
|
168
|
+
end
|
169
|
+
|
170
|
+
def error_message(message, backtrace: nil)
|
171
|
+
# Store syntax errors for display in execution details
|
172
|
+
@syntax_errors << {
|
173
|
+
message: message,
|
174
|
+
backtrace: backtrace
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
# Override live status - not needed for agent mode
|
179
|
+
def live_status_capabilities
|
180
|
+
{
|
181
|
+
supports_coordination: false,
|
182
|
+
output_frequency: :none,
|
183
|
+
requires_tty: false
|
184
|
+
}
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def build_failure_data(result_packet)
|
190
|
+
test_case = result_packet.test_case
|
191
|
+
|
192
|
+
failure_data = {
|
193
|
+
line: (test_case.first_expectation_line || test_case.line_range&.first || 0) + 1,
|
194
|
+
test: test_case.description.to_s.empty? ? 'unnamed test' : test_case.description.to_s
|
195
|
+
}
|
196
|
+
|
197
|
+
case result_packet.status
|
198
|
+
when :error
|
199
|
+
error = result_packet.error
|
200
|
+
failure_data[:error] = error ? "#{error.class.name}: #{error.message}" : 'unknown error'
|
201
|
+
when :failed
|
202
|
+
if result_packet.expected_results.any? && result_packet.actual_results.any?
|
203
|
+
expected = @budget.smart_truncate(result_packet.first_expected, max_tokens: 25)
|
204
|
+
actual = @budget.smart_truncate(result_packet.first_actual, max_tokens: 25)
|
205
|
+
failure_data[:expected] = expected
|
206
|
+
failure_data[:got] = actual
|
207
|
+
|
208
|
+
# Add diff for strings if budget allows
|
209
|
+
if result_packet.first_expected.is_a?(String) &&
|
210
|
+
result_packet.first_actual.is_a?(String) &&
|
211
|
+
@budget.has_budget?
|
212
|
+
failure_data[:diff] = generate_simple_diff(result_packet.first_expected, result_packet.first_actual)
|
213
|
+
end
|
214
|
+
else
|
215
|
+
failure_data[:reason] = 'test failed'
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
failure_data
|
220
|
+
end
|
221
|
+
|
222
|
+
def generate_simple_diff(expected, actual)
|
223
|
+
return nil unless @budget.remaining > 100 # Only if we have decent budget left
|
224
|
+
|
225
|
+
# Simple line-by-line diff
|
226
|
+
exp_lines = expected.split("\n")
|
227
|
+
act_lines = actual.split("\n")
|
228
|
+
|
229
|
+
diff_lines = []
|
230
|
+
diff_lines << "- #{act_lines.first}" if act_lines.any?
|
231
|
+
diff_lines << "+ #{exp_lines.first}" if exp_lines.any?
|
232
|
+
|
233
|
+
diff_result = diff_lines.join("\n")
|
234
|
+
return @budget.fit_text(diff_result) if @budget.would_exceed?(diff_result)
|
235
|
+
diff_result
|
236
|
+
end
|
237
|
+
|
238
|
+
def render_agent_output
|
239
|
+
case @focus_mode
|
240
|
+
when :summary
|
241
|
+
render_summary_only
|
242
|
+
when :critical
|
243
|
+
render_critical_only
|
244
|
+
else
|
245
|
+
render_full_structured
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def render_summary_only
|
250
|
+
output = []
|
251
|
+
|
252
|
+
# Add execution context header for agent clarity
|
253
|
+
output << render_execution_context
|
254
|
+
output << ""
|
255
|
+
|
256
|
+
# Count failures manually from collected file data (same as other render methods)
|
257
|
+
failed_count = @collected_files.sum { |f| f[:failures].size }
|
258
|
+
error_count = @collected_files.sum { |f| f[:errors].size }
|
259
|
+
issues_count = failed_count + error_count
|
260
|
+
passed_count = [@total_stats[:tests] - issues_count, 0].max
|
261
|
+
|
262
|
+
status_parts = []
|
263
|
+
if issues_count > 0
|
264
|
+
details = []
|
265
|
+
details << "#{failed_count} failed" if failed_count > 0
|
266
|
+
details << "#{error_count} errors" if error_count > 0
|
267
|
+
status_parts << "FAIL: #{issues_count}/#{@total_stats[:tests]} tests (#{details.join(', ')}, #{passed_count} passed)"
|
268
|
+
else
|
269
|
+
# Agent doesn't need output in the positive case (i.e. for passing
|
270
|
+
# tests). It just fills out the context window.
|
271
|
+
end
|
272
|
+
|
273
|
+
status_parts << "(#{format_time(@total_stats[:elapsed])})" if @total_stats[:elapsed]
|
274
|
+
|
275
|
+
output << status_parts.join(" ")
|
276
|
+
|
277
|
+
# Always show file information for agent context
|
278
|
+
output << ""
|
279
|
+
|
280
|
+
files_with_issues = @collected_files.select { |f| f[:failures].any? || f[:errors].any? }
|
281
|
+
if files_with_issues.any?
|
282
|
+
output << "Files:"
|
283
|
+
files_with_issues.each do |file_data|
|
284
|
+
issue_count = file_data[:failures].size + file_data[:errors].size
|
285
|
+
output << " #{file_data[:path]}: #{issue_count} issue#{'s' if issue_count != 1}"
|
286
|
+
end
|
287
|
+
elsif @collected_files.any?
|
288
|
+
# Show files that were processed successfully
|
289
|
+
output << "Files:"
|
290
|
+
@collected_files.each do |file_data|
|
291
|
+
# Use the passed count from file_result if available, otherwise calculate
|
292
|
+
passed_tests = file_data[:passed] ||
|
293
|
+
((file_data[:tests] || 0) - file_data[:failures].size - file_data[:errors].size)
|
294
|
+
output << " #{file_data[:path]}: #{passed_tests} test#{'s' if passed_tests != 1} passed"
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
puts output.join("\n") if output.any?
|
299
|
+
end
|
300
|
+
|
301
|
+
def render_critical_only
|
302
|
+
# Only show errors (exceptions), skip assertion failures
|
303
|
+
critical_files = @collected_files.select { |f| f[:errors].any? }
|
304
|
+
|
305
|
+
output = []
|
306
|
+
|
307
|
+
# Add execution context header for agent clarity
|
308
|
+
output << render_execution_context
|
309
|
+
output << ""
|
310
|
+
|
311
|
+
if critical_files.empty?
|
312
|
+
output << "No critical errors found"
|
313
|
+
puts output.join("\n")
|
314
|
+
return
|
315
|
+
end
|
316
|
+
|
317
|
+
output << "CRITICAL: #{critical_files.size} file#{'s' if critical_files.size != 1} with errors"
|
318
|
+
output << ""
|
319
|
+
|
320
|
+
critical_files.each do |file_data|
|
321
|
+
unless @budget.has_budget?
|
322
|
+
output << "... (truncated due to token limit)"
|
323
|
+
break
|
324
|
+
end
|
325
|
+
|
326
|
+
output << "#{file_data[:path]}:"
|
327
|
+
|
328
|
+
file_data[:errors].each do |error|
|
329
|
+
error_line = " L#{error[:line]}: #{error[:error]}"
|
330
|
+
if @budget.would_exceed?(error_line)
|
331
|
+
output << @budget.fit_text(error_line)
|
332
|
+
else
|
333
|
+
output << error_line
|
334
|
+
@budget.consume(error_line)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
output << ""
|
339
|
+
end
|
340
|
+
|
341
|
+
puts output.join("\n")
|
342
|
+
end
|
343
|
+
|
344
|
+
def render_full_structured
|
345
|
+
output = []
|
346
|
+
|
347
|
+
# Add execution context header for agent clarity
|
348
|
+
output << render_execution_context
|
349
|
+
output << ""
|
350
|
+
|
351
|
+
# Count actual failures from collected data
|
352
|
+
failed_count = @collected_files.sum { |f| f[:failures].size }
|
353
|
+
error_count = @collected_files.sum { |f| f[:errors].size }
|
354
|
+
issues_count = failed_count + error_count
|
355
|
+
passed_count = [@total_stats[:tests] - issues_count, 0].max
|
356
|
+
|
357
|
+
# Show files with issues only
|
358
|
+
files_with_issues = @collected_files.select { |f| f[:failures].any? || f[:errors].any? }
|
359
|
+
|
360
|
+
if files_with_issues.any?
|
361
|
+
files_with_issues.each do |file_data|
|
362
|
+
break unless @budget.has_budget?
|
363
|
+
|
364
|
+
file_section = render_file_section(file_data)
|
365
|
+
if @budget.would_exceed?(file_section)
|
366
|
+
# Try to fit what we can
|
367
|
+
truncated = @budget.fit_text(file_section, preserve_suffix: "\n ... (truncated)")
|
368
|
+
output << truncated if truncated.length > 20 # Only if meaningful content remains
|
369
|
+
break
|
370
|
+
else
|
371
|
+
output << file_section
|
372
|
+
@budget.consume(file_section)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
output << ""
|
376
|
+
end
|
377
|
+
|
378
|
+
# Final summary line
|
379
|
+
summary = "Summary: \n"
|
380
|
+
summary += "#{passed_count} testcases passed, #{failed_count} failed"
|
381
|
+
summary += ", #{error_count} errors" if error_count > 0
|
382
|
+
summary += " in #{@total_stats[:files]} files"
|
383
|
+
|
384
|
+
output << summary
|
385
|
+
|
386
|
+
puts output.join("\n")
|
387
|
+
end
|
388
|
+
|
389
|
+
def render_file_section(file_data)
|
390
|
+
lines = []
|
391
|
+
|
392
|
+
# File header
|
393
|
+
lines << "#{file_data[:path]}:"
|
394
|
+
|
395
|
+
# Check if file has any issues
|
396
|
+
has_issues = file_data[:failures].any? || file_data[:errors].any?
|
397
|
+
|
398
|
+
# If no issues, show success summary
|
399
|
+
if !has_issues
|
400
|
+
# Use the passed count from file_result if available, otherwise calculate
|
401
|
+
passed_tests = file_data[:passed] ||
|
402
|
+
((file_data[:tests] || 0) - file_data[:failures].size - file_data[:errors].size)
|
403
|
+
|
404
|
+
|
405
|
+
lines << " ✓ #{passed_tests} test#{'s' if passed_tests != 1} passed"
|
406
|
+
return lines.join("\n")
|
407
|
+
end
|
408
|
+
|
409
|
+
# For first-failure mode, only show first error or failure
|
410
|
+
if @focus_mode == :first_failure || @focus_mode == :'first-failure'
|
411
|
+
shown_count = 0
|
412
|
+
|
413
|
+
# Show first error
|
414
|
+
if file_data[:errors].any? && shown_count == 0
|
415
|
+
error = file_data[:errors].first
|
416
|
+
lines << " L#{error[:line]}: #{error[:error]}"
|
417
|
+
lines << " Test: #{error[:test]}" if error[:test] != 'unnamed test'
|
418
|
+
shown_count += 1
|
419
|
+
end
|
420
|
+
|
421
|
+
# Show first failure if no error was shown
|
422
|
+
if file_data[:failures].any? && shown_count == 0
|
423
|
+
failure = file_data[:failures].first
|
424
|
+
line_parts = [" L#{failure[:line]}:"]
|
425
|
+
|
426
|
+
if failure[:expected] && failure[:got]
|
427
|
+
line_parts << "expected #{failure[:expected]}, got #{failure[:got]}"
|
428
|
+
elsif failure[:reason]
|
429
|
+
line_parts << failure[:reason]
|
430
|
+
end
|
431
|
+
|
432
|
+
lines << line_parts.join(' ')
|
433
|
+
lines << " Test: #{failure[:test]}" if failure[:test] != 'unnamed test'
|
434
|
+
|
435
|
+
# Add diff if available and budget allows
|
436
|
+
if failure[:diff] && @budget.remaining > 50
|
437
|
+
lines << " Diff:"
|
438
|
+
failure[:diff].split("\n").each { |diff_line| lines << " #{diff_line}" }
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
# Show truncation notice
|
443
|
+
total_issues = file_data[:errors].size + file_data[:failures].size
|
444
|
+
if total_issues > 1
|
445
|
+
lines << " ... (#{total_issues - 1} more failures not shown)"
|
446
|
+
end
|
447
|
+
else
|
448
|
+
# Normal mode - show all errors and failures
|
449
|
+
# Errors first (more critical)
|
450
|
+
file_data[:errors].each do |error|
|
451
|
+
next if error[:basic] # Skip basic entries from summary mode
|
452
|
+
lines << " L#{error[:line]}: #{error[:error]}"
|
453
|
+
lines << " Test: #{error[:test]}" if error[:test] != 'unnamed test'
|
454
|
+
end
|
455
|
+
|
456
|
+
# Then failures
|
457
|
+
file_data[:failures].each do |failure|
|
458
|
+
next if failure[:basic] # Skip basic entries from summary mode
|
459
|
+
line_parts = [" L#{failure[:line]}:"]
|
460
|
+
|
461
|
+
if failure[:expected] && failure[:got]
|
462
|
+
line_parts << "expected #{failure[:expected]}, got #{failure[:got]}"
|
463
|
+
elsif failure[:reason]
|
464
|
+
line_parts << failure[:reason]
|
465
|
+
end
|
466
|
+
|
467
|
+
lines << line_parts.join(' ')
|
468
|
+
lines << " Test: #{failure[:test]}" if failure[:test] != 'unnamed test'
|
469
|
+
|
470
|
+
# Add diff if available and budget allows
|
471
|
+
if failure[:diff] && @budget.remaining > 50
|
472
|
+
lines << " Diff:"
|
473
|
+
failure[:diff].split("\n").each { |diff_line| lines << " #{diff_line}" }
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
# Show truncation notice if applicable
|
478
|
+
if file_data[:truncated]
|
479
|
+
lines << " ... (more failures not shown)"
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
lines.join("\n")
|
484
|
+
end
|
485
|
+
|
486
|
+
def relative_path(file_path)
|
487
|
+
# Remove leading path components to save tokens
|
488
|
+
path = Pathname.new(file_path).relative_path_from(Pathname.pwd).to_s
|
489
|
+
# If relative path is longer, use just filename
|
490
|
+
path.include?('../') ? File.basename(file_path) : path
|
491
|
+
rescue
|
492
|
+
File.basename(file_path)
|
493
|
+
end
|
494
|
+
|
495
|
+
def format_time(seconds)
|
496
|
+
return '0ms' unless seconds
|
497
|
+
|
498
|
+
if seconds < 0.001
|
499
|
+
"#{(seconds * 1_000_000).round}μs"
|
500
|
+
elsif seconds < 1
|
501
|
+
"#{(seconds * 1000).round}ms"
|
502
|
+
else
|
503
|
+
"#{seconds.round(2)}s"
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
def render_execution_context
|
508
|
+
context_lines = []
|
509
|
+
context_lines << "EXECUTION DETAILS:"
|
510
|
+
|
511
|
+
# Framework and context mode
|
512
|
+
framework = @options[:framework] || :direct
|
513
|
+
shared_context = if @options.key?(:shared_context)
|
514
|
+
@options[:shared_context]
|
515
|
+
else
|
516
|
+
# Apply framework defaults
|
517
|
+
case framework
|
518
|
+
when :rspec, :minitest
|
519
|
+
false
|
520
|
+
else
|
521
|
+
true # direct/tryouts defaults to shared
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
context_lines << " Framework: #{framework}"
|
526
|
+
context_lines << " Context mode: #{shared_context ? 'shared (variables persist across test cases)' : 'fresh (each test case isolated)'}"
|
527
|
+
|
528
|
+
# Parser type
|
529
|
+
parser = @options[:parser] || :enhanced
|
530
|
+
context_lines << " Parser: #{parser}"
|
531
|
+
|
532
|
+
# Other relevant flags
|
533
|
+
flags = []
|
534
|
+
flags << "verbose" if @options[:verbose]
|
535
|
+
flags << "fails-only" if @options[:fails_only]
|
536
|
+
flags << "debug" if @options[:debug]
|
537
|
+
flags << "stack-traces" if @options[:stack_traces]
|
538
|
+
flags << "parallel(#{@options[:parallel_threads] || 'auto'})" if @options[:parallel]
|
539
|
+
flags << "line-spec" if @options[:line_spec]
|
540
|
+
|
541
|
+
context_lines << " Flags: #{flags.any? ? flags.join(', ') : 'none'}" if flags.any?
|
542
|
+
|
543
|
+
# Agent-specific settings
|
544
|
+
context_lines << " Agent mode: focus=#{@focus_mode}, limit=#{@budget.limit} tokens"
|
545
|
+
|
546
|
+
# Add syntax errors if any (these prevent test execution)
|
547
|
+
if @syntax_errors.any?
|
548
|
+
context_lines << ""
|
549
|
+
context_lines << "Syntax Errors:"
|
550
|
+
@syntax_errors.each do |error|
|
551
|
+
# Clean up the error message to remove redundant prefixes
|
552
|
+
clean_message = error[:message].gsub(/^ERROR:\s*/i, '').strip
|
553
|
+
context_lines << " #{clean_message}"
|
554
|
+
if error[:backtrace] && @options[:debug]
|
555
|
+
error[:backtrace].first(3).each do |trace|
|
556
|
+
context_lines << " #{trace}"
|
557
|
+
end
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
# Add warnings if any
|
563
|
+
if @all_warnings.any? && @options.fetch(:warnings, true)
|
564
|
+
context_lines << ""
|
565
|
+
context_lines << "Parser Warnings:"
|
566
|
+
@all_warnings.each do |warning|
|
567
|
+
context_lines << " #{warning[:file]}:#{warning[:line]}: #{warning[:message]}"
|
568
|
+
context_lines << " #{warning[:suggestion]}" if warning[:suggestion]
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
context_lines.join("\n")
|
573
|
+
end
|
574
|
+
end
|
575
|
+
end
|
576
|
+
end
|