tryouts 3.4.0 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +18 -3
- data/exe/try +14 -5
- data/lib/tryouts/cli/formatters/agent.rb +337 -57
- data/lib/tryouts/cli/formatters/base.rb +5 -1
- data/lib/tryouts/cli/formatters/compact.rb +14 -4
- data/lib/tryouts/cli/formatters/output_manager.rb +4 -0
- data/lib/tryouts/cli/formatters/verbose.rb +69 -56
- data/lib/tryouts/cli/line_spec_parser.rb +109 -0
- data/lib/tryouts/cli/opts.rb +6 -0
- data/lib/tryouts/cli.rb +22 -5
- data/lib/tryouts/expectation_evaluators/result_type.rb +15 -0
- 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 +15 -5
- data/lib/tryouts/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ab0eb9c341b10dd5e505a3f62cc33a19a393cb294aecd756809b19c2484784f1
|
4
|
+
data.tar.gz: cddfd1a827b0ef37a9e69224124f2f696bbd8c2f098edbfce79b563b3017fbcd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 066b7697b04b3501f01a0994e9d10a66c5a94d593bdfa5cb1b618bb1264f228425bd7d8b4d98cf9a555a5e23bbe002f137c943d5195ad4ce972de30f03dfac9c
|
7
|
+
data.tar.gz: 82390327e8da8e35d0c9eaa4557800b09bba76e065704f9d704acab45ab24dd62e97edc7aa54d8104ee50cab31a9dab321c75449386f70243674c8f2d838716a
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Tryouts
|
1
|
+
# Tryouts - A Ruby Testing Framework
|
2
2
|
|
3
3
|
**Ruby tests that read like documentation.**
|
4
4
|
|
@@ -130,6 +130,21 @@ try --agent --agent-focus critical # show only errors/exceptions
|
|
130
130
|
try --agent --agent-limit 1000 # limit output to 1000 tokens
|
131
131
|
```
|
132
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
|
+
|
133
148
|
### Exit Codes
|
134
149
|
|
135
150
|
- `0`: All tests pass
|
@@ -162,8 +177,8 @@ For real-world usage examples, see:
|
|
162
177
|
|
163
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:
|
164
179
|
|
165
|
-
- **Claude Sonnet 4** - Architecture design, code generation, and documentation
|
166
|
-
- **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
|
167
182
|
- **GitHub Copilot** - Code completion and refactoring assistance
|
168
183
|
- **Qodo Merge Pro** - Code review and quality improvements
|
169
184
|
|
data/exe/try
CHANGED
@@ -38,19 +38,28 @@ require_relative '../lib/tryouts'
|
|
38
38
|
lib_glob = File.join(Dir.pwd, '{lib,../lib,.}')
|
39
39
|
Tryouts.update_load_path(lib_glob) if Tryouts.respond_to?(:update_load_path)
|
40
40
|
|
41
|
+
# Capture original command for agent mode before ARGV gets modified
|
42
|
+
original_command = [$0] + ARGV
|
43
|
+
|
41
44
|
# Parse args and run CLI
|
42
45
|
begin
|
43
46
|
files, options = Tryouts::CLI.parse_args(ARGV)
|
44
47
|
|
45
|
-
#
|
48
|
+
# Add original command to options for agent formatter
|
49
|
+
options[:original_command] = original_command
|
50
|
+
|
51
|
+
# Expand files if directories are given, preserving line specs
|
46
52
|
expanded_files = []
|
47
53
|
files.each do |file_or_dir|
|
48
|
-
|
54
|
+
# Parse line spec from the argument
|
55
|
+
path_part, line_spec = Tryouts::CLI::LineSpecParser.parse(file_or_dir)
|
56
|
+
|
57
|
+
if File.directory?(path_part)
|
49
58
|
# 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(
|
59
|
+
dir_files = Dir.glob(['**/*_try.rb', '**/*.try.rb'], base: path_part)
|
60
|
+
expanded_files.concat(dir_files.map { |f| File.join(path_part, f) })
|
52
61
|
else
|
53
|
-
# If it's a file, add it as-is
|
62
|
+
# If it's a file, add it as-is (with line spec if present)
|
54
63
|
expanded_files << file_or_dir
|
55
64
|
end
|
56
65
|
end
|
@@ -4,13 +4,42 @@ require_relative 'token_budget'
|
|
4
4
|
|
5
5
|
class Tryouts
|
6
6
|
class CLI
|
7
|
-
#
|
8
|
-
#
|
9
|
-
# -
|
10
|
-
#
|
11
|
-
# -
|
12
|
-
#
|
13
|
-
#
|
7
|
+
# TOPA (Test Output Protocol for AI) Formatter
|
8
|
+
#
|
9
|
+
# Language-agnostic test output format designed for LLM context management.
|
10
|
+
# This formatter implements the TOPA v1.0 specification for structured,
|
11
|
+
# token-efficient test result communication.
|
12
|
+
#
|
13
|
+
# TOPA Features:
|
14
|
+
# - Language-agnostic field naming (snake_case, hierarchical)
|
15
|
+
# - Standardized execution context (runtime, environment, VCS)
|
16
|
+
# - Token budget awareness with smart truncation
|
17
|
+
# - Cross-platform compatibility (CI/CD, package managers)
|
18
|
+
# - Structured failure reporting with diffs
|
19
|
+
# - Protocol versioning for forward compatibility
|
20
|
+
#
|
21
|
+
# Field Specifications:
|
22
|
+
# - command: Exact command executed
|
23
|
+
# - process_id: System process identifier
|
24
|
+
# - runtime: Language, version, platform info
|
25
|
+
# - package_manager: Dependency management system
|
26
|
+
# - version_control: VCS branch/commit info
|
27
|
+
# - environment: Normalized env vars (ci_system, app_env, etc.)
|
28
|
+
# - test_framework: Framework name, isolation mode, parser
|
29
|
+
# - execution_flags: Runtime flags in normalized form
|
30
|
+
# - protocol: TOPA version and configuration
|
31
|
+
# - project: Auto-detected project type
|
32
|
+
# - test_discovery: File pattern matching rules
|
33
|
+
#
|
34
|
+
# Compatible with: Ruby/RSpec/Minitest, Python/pytest/unittest,
|
35
|
+
# JavaScript/Jest/Mocha, Java/JUnit, Go, C#/NUnit, and more.
|
36
|
+
#
|
37
|
+
# Language Adaptation Examples:
|
38
|
+
# Python: runtime.language=python, package_manager.name=pip/poetry/conda
|
39
|
+
# Node.js: runtime.language=javascript, package_manager.name=npm/yarn/pnpm
|
40
|
+
# Java: runtime.language=java, package_manager.name=maven/gradle
|
41
|
+
# Go: runtime.language=go, package_manager.name=go_modules
|
42
|
+
# C#: runtime.language=csharp, package_manager.name=nuget/dotnet
|
14
43
|
class AgentFormatter
|
15
44
|
include FormatterInterface
|
16
45
|
|
@@ -22,6 +51,9 @@ class Tryouts
|
|
22
51
|
@current_file_data = nil
|
23
52
|
@total_stats = { files: 0, tests: 0, failures: 0, errors: 0, elapsed: 0 }
|
24
53
|
@output_rendered = false
|
54
|
+
@options = options # Store all options for execution context display
|
55
|
+
@all_warnings = [] # Store warnings globally for execution details
|
56
|
+
@syntax_errors = [] # Store syntax errors for execution details
|
25
57
|
|
26
58
|
# No colors in agent mode for cleaner parsing
|
27
59
|
@use_colors = false
|
@@ -42,7 +74,8 @@ class Tryouts
|
|
42
74
|
tests: 0,
|
43
75
|
failures: [],
|
44
76
|
errors: [],
|
45
|
-
passed: 0
|
77
|
+
passed: 0,
|
78
|
+
context_info: context_info # Store context info for later display
|
46
79
|
}
|
47
80
|
end
|
48
81
|
|
@@ -56,19 +89,47 @@ class Tryouts
|
|
56
89
|
end
|
57
90
|
|
58
91
|
def file_parsed(_file_path, test_count:, setup_present: false, teardown_present: false)
|
59
|
-
|
92
|
+
if @current_file_data
|
93
|
+
@current_file_data[:tests] = test_count
|
94
|
+
end
|
60
95
|
@total_stats[:tests] += test_count
|
61
96
|
end
|
62
97
|
|
63
|
-
def
|
98
|
+
def parser_warnings(file_path, warnings:)
|
99
|
+
return if warnings.empty? || !@options.fetch(:warnings, true)
|
100
|
+
|
101
|
+
# Store warnings globally for execution details and per-file
|
102
|
+
warnings.each do |warning|
|
103
|
+
warning_data = {
|
104
|
+
type: warning.type.to_s,
|
105
|
+
message: warning.message,
|
106
|
+
line: warning.line_number,
|
107
|
+
suggestion: warning.suggestion,
|
108
|
+
file: relative_path(file_path)
|
109
|
+
}
|
110
|
+
@all_warnings << warning_data
|
111
|
+
end
|
112
|
+
|
113
|
+
# Also store in current file data for potential future use
|
114
|
+
if @current_file_data
|
115
|
+
@current_file_data[:warnings] = @all_warnings.select { |w| w[:file] == relative_path(file_path) }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def file_result(file_path, total_tests:, failed_count:, error_count:, elapsed_time: nil)
|
64
120
|
# Always update global totals
|
65
121
|
@total_stats[:failures] += failed_count
|
66
122
|
@total_stats[:errors] += error_count
|
67
123
|
@total_stats[:elapsed] += elapsed_time if elapsed_time
|
68
124
|
|
69
|
-
# Update per-file data
|
70
|
-
|
71
|
-
|
125
|
+
# Update per-file data - file_result is called AFTER file_end, so data is in @collected_files
|
126
|
+
relative_file_path = relative_path(file_path)
|
127
|
+
file_data = @collected_files.find { |f| f[:path] == relative_file_path }
|
128
|
+
|
129
|
+
if file_data
|
130
|
+
file_data[:passed] = total_tests - failed_count - error_count
|
131
|
+
# Also ensure tests count is correct if it wasn't set properly earlier
|
132
|
+
file_data[:tests] ||= total_tests
|
72
133
|
end
|
73
134
|
end
|
74
135
|
|
@@ -135,6 +196,14 @@ class Tryouts
|
|
135
196
|
@output_rendered = true
|
136
197
|
end
|
137
198
|
|
199
|
+
def error_message(message, backtrace: nil)
|
200
|
+
# Store syntax errors for display in execution details
|
201
|
+
@syntax_errors << {
|
202
|
+
message: message,
|
203
|
+
backtrace: backtrace
|
204
|
+
}
|
205
|
+
end
|
206
|
+
|
138
207
|
# Override live status - not needed for agent mode
|
139
208
|
def live_status_capabilities
|
140
209
|
{
|
@@ -209,50 +278,71 @@ class Tryouts
|
|
209
278
|
def render_summary_only
|
210
279
|
output = []
|
211
280
|
|
281
|
+
# Add execution context header for agent clarity
|
282
|
+
output << render_execution_context
|
283
|
+
output << ""
|
284
|
+
|
212
285
|
# Count failures manually from collected file data (same as other render methods)
|
213
286
|
failed_count = @collected_files.sum { |f| f[:failures].size }
|
214
287
|
error_count = @collected_files.sum { |f| f[:errors].size }
|
215
288
|
issues_count = failed_count + error_count
|
216
289
|
passed_count = [@total_stats[:tests] - issues_count, 0].max
|
217
290
|
|
291
|
+
status_parts = []
|
218
292
|
if issues_count > 0
|
219
|
-
status = "FAIL: #{issues_count}/#{@total_stats[:tests]} tests"
|
220
293
|
details = []
|
221
294
|
details << "#{failed_count} failed" if failed_count > 0
|
222
295
|
details << "#{error_count} errors" if error_count > 0
|
223
|
-
|
296
|
+
status_parts << "FAIL: #{issues_count}/#{@total_stats[:tests]} tests (#{details.join(', ')}, #{passed_count} passed)"
|
224
297
|
else
|
225
|
-
|
298
|
+
# Agent doesn't need output in the positive case (i.e. for passing
|
299
|
+
# tests). It just fills out the context window.
|
226
300
|
end
|
227
301
|
|
228
|
-
|
302
|
+
status_parts << "(#{format_time(@total_stats[:elapsed])})" if @total_stats[:elapsed]
|
229
303
|
|
230
|
-
output <<
|
304
|
+
output << status_parts.join(" ")
|
305
|
+
|
306
|
+
# Always show file information for agent context
|
307
|
+
output << ""
|
231
308
|
|
232
|
-
# Show which files had failures
|
233
309
|
files_with_issues = @collected_files.select { |f| f[:failures].any? || f[:errors].any? }
|
234
310
|
if files_with_issues.any?
|
235
|
-
output << ""
|
236
|
-
output << "Files with issues:"
|
311
|
+
output << "Files:"
|
237
312
|
files_with_issues.each do |file_data|
|
238
313
|
issue_count = file_data[:failures].size + file_data[:errors].size
|
239
314
|
output << " #{file_data[:path]}: #{issue_count} issue#{'s' if issue_count != 1}"
|
240
315
|
end
|
316
|
+
elsif @collected_files.any?
|
317
|
+
# Show files that were processed successfully
|
318
|
+
output << "Files:"
|
319
|
+
@collected_files.each do |file_data|
|
320
|
+
# Use the passed count from file_result if available, otherwise calculate
|
321
|
+
passed_tests = file_data[:passed] ||
|
322
|
+
((file_data[:tests] || 0) - file_data[:failures].size - file_data[:errors].size)
|
323
|
+
output << " #{file_data[:path]}: #{passed_tests} test#{'s' if passed_tests != 1} passed"
|
324
|
+
end
|
241
325
|
end
|
242
326
|
|
243
|
-
puts output.join("\n")
|
327
|
+
puts output.join("\n") if output.any?
|
244
328
|
end
|
245
329
|
|
246
330
|
def render_critical_only
|
247
331
|
# Only show errors (exceptions), skip assertion failures
|
248
332
|
critical_files = @collected_files.select { |f| f[:errors].any? }
|
249
333
|
|
334
|
+
output = []
|
335
|
+
|
336
|
+
# Add execution context header for agent clarity
|
337
|
+
output << render_execution_context
|
338
|
+
output << ""
|
339
|
+
|
250
340
|
if critical_files.empty?
|
251
|
-
|
341
|
+
output << "No critical errors found"
|
342
|
+
puts output.join("\n")
|
252
343
|
return
|
253
344
|
end
|
254
345
|
|
255
|
-
output = []
|
256
346
|
output << "CRITICAL: #{critical_files.size} file#{'s' if critical_files.size != 1} with errors"
|
257
347
|
output << ""
|
258
348
|
|
@@ -283,39 +373,21 @@ class Tryouts
|
|
283
373
|
def render_full_structured
|
284
374
|
output = []
|
285
375
|
|
286
|
-
#
|
287
|
-
|
288
|
-
|
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)
|
376
|
+
# Add execution context header for agent clarity
|
377
|
+
output << render_execution_context
|
378
|
+
output << ""
|
305
379
|
|
306
|
-
#
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
@collected_files.select { |f| f[:failures].any? || f[:errors].any? }
|
312
|
-
end
|
380
|
+
# Count actual failures from collected data
|
381
|
+
failed_count = @collected_files.sum { |f| f[:failures].size }
|
382
|
+
error_count = @collected_files.sum { |f| f[:errors].size }
|
383
|
+
issues_count = failed_count + error_count
|
384
|
+
passed_count = [@total_stats[:tests] - issues_count, 0].max
|
313
385
|
|
314
|
-
|
315
|
-
|
316
|
-
@budget.consume("\n")
|
386
|
+
# Show files with issues only
|
387
|
+
files_with_issues = @collected_files.select { |f| f[:failures].any? || f[:errors].any? }
|
317
388
|
|
318
|
-
|
389
|
+
if files_with_issues.any?
|
390
|
+
files_with_issues.each do |file_data|
|
319
391
|
break unless @budget.has_budget?
|
320
392
|
|
321
393
|
file_section = render_file_section(file_data)
|
@@ -329,14 +401,15 @@ class Tryouts
|
|
329
401
|
@budget.consume(file_section)
|
330
402
|
end
|
331
403
|
end
|
404
|
+
output << ""
|
332
405
|
end
|
333
406
|
|
334
407
|
# Final summary line
|
335
|
-
summary = "Summary:
|
336
|
-
summary += ", #{
|
408
|
+
summary = "Summary: \n"
|
409
|
+
summary += "#{passed_count} testcases passed, #{failed_count} failed"
|
410
|
+
summary += ", #{error_count} errors" if error_count > 0
|
337
411
|
summary += " in #{@total_stats[:files]} files"
|
338
412
|
|
339
|
-
output << ""
|
340
413
|
output << summary
|
341
414
|
|
342
415
|
puts output.join("\n")
|
@@ -348,6 +421,20 @@ class Tryouts
|
|
348
421
|
# File header
|
349
422
|
lines << "#{file_data[:path]}:"
|
350
423
|
|
424
|
+
# Check if file has any issues
|
425
|
+
has_issues = file_data[:failures].any? || file_data[:errors].any?
|
426
|
+
|
427
|
+
# If no issues, show success summary
|
428
|
+
if !has_issues
|
429
|
+
# Use the passed count from file_result if available, otherwise calculate
|
430
|
+
passed_tests = file_data[:passed] ||
|
431
|
+
((file_data[:tests] || 0) - file_data[:failures].size - file_data[:errors].size)
|
432
|
+
|
433
|
+
|
434
|
+
lines << " ✓ #{passed_tests} test#{'s' if passed_tests != 1} passed"
|
435
|
+
return lines.join("\n")
|
436
|
+
end
|
437
|
+
|
351
438
|
# For first-failure mode, only show first error or failure
|
352
439
|
if @focus_mode == :first_failure || @focus_mode == :'first-failure'
|
353
440
|
shown_count = 0
|
@@ -445,6 +532,199 @@ class Tryouts
|
|
445
532
|
"#{seconds.round(2)}s"
|
446
533
|
end
|
447
534
|
end
|
535
|
+
|
536
|
+
def render_execution_context
|
537
|
+
context_lines = []
|
538
|
+
context_lines << "EXECUTION_CONTEXT:"
|
539
|
+
|
540
|
+
# Command that was executed
|
541
|
+
if @options[:original_command]
|
542
|
+
command_str = @options[:original_command].join(' ')
|
543
|
+
context_lines << " command: #{command_str}"
|
544
|
+
end
|
545
|
+
|
546
|
+
# Compact system info on one line when possible
|
547
|
+
context_lines << " pid: #{Process.pid} | pwd: #{Dir.pwd}"
|
548
|
+
|
549
|
+
# Runtime - compact format
|
550
|
+
platform = RUBY_PLATFORM.gsub(/darwin\d+/, 'darwin') # Simplify darwin25 -> darwin
|
551
|
+
context_lines << " runtime: ruby #{RUBY_VERSION} (#{platform})"
|
552
|
+
|
553
|
+
# Package manager - only if present, compact format
|
554
|
+
if defined?(Bundler)
|
555
|
+
context_lines << " package_manager: bundler #{Bundler::VERSION}"
|
556
|
+
end
|
557
|
+
|
558
|
+
# Version control - compact single line with timeout protection
|
559
|
+
git_info = safe_git_info
|
560
|
+
if git_info[:branch] && git_info[:commit] && !git_info[:branch].empty? && !git_info[:commit].empty?
|
561
|
+
context_lines << " vcs: git #{git_info[:branch]}@#{git_info[:commit]}"
|
562
|
+
end
|
563
|
+
|
564
|
+
# Environment - only non-defaults
|
565
|
+
env_vars = build_environment_context
|
566
|
+
if env_vars.any?
|
567
|
+
# Compact key=value format
|
568
|
+
env_str = env_vars.map { |k, v| "#{k}=#{v}" }.join(', ')
|
569
|
+
context_lines << " environment: #{env_str}"
|
570
|
+
end
|
571
|
+
|
572
|
+
# Test framework - compact critical info only
|
573
|
+
framework = @options[:framework] || :direct
|
574
|
+
shared_context = if @options.key?(:shared_context)
|
575
|
+
@options[:shared_context]
|
576
|
+
else
|
577
|
+
# Apply framework defaults
|
578
|
+
case framework
|
579
|
+
when :rspec, :minitest
|
580
|
+
false
|
581
|
+
else
|
582
|
+
true # direct/tryouts defaults to shared
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
isolation = shared_context ? 'shared' : 'isolated'
|
587
|
+
context_lines << " test_framework: #{framework} (#{isolation})"
|
588
|
+
|
589
|
+
# Execution flags - only if non-standard
|
590
|
+
flags = build_execution_flags
|
591
|
+
if flags.any?
|
592
|
+
context_lines << " flags: #{flags.join(', ')}"
|
593
|
+
end
|
594
|
+
|
595
|
+
# TOPA protocol - compact
|
596
|
+
context_lines << " protocol: TOPA v1.0 | focus: #{@focus_mode} | limit: #{@budget.limit}"
|
597
|
+
|
598
|
+
# File count being tested
|
599
|
+
if @collected_files && @collected_files.any?
|
600
|
+
context_lines << " files_under_test: #{@collected_files.size}"
|
601
|
+
elsif @total_stats[:files] && @total_stats[:files] > 0
|
602
|
+
context_lines << " files_under_test: #{@total_stats[:files]}"
|
603
|
+
end
|
604
|
+
|
605
|
+
# Add syntax errors if any (these prevent test execution)
|
606
|
+
if @syntax_errors.any?
|
607
|
+
context_lines << ""
|
608
|
+
context_lines << "Syntax Errors:"
|
609
|
+
@syntax_errors.each do |error|
|
610
|
+
# Clean up the error message to remove redundant prefixes
|
611
|
+
clean_message = error[:message].gsub(/^ERROR:\s*/i, '').strip
|
612
|
+
context_lines << " #{clean_message}"
|
613
|
+
if error[:backtrace] && @options[:debug]
|
614
|
+
error[:backtrace].first(3).each do |trace|
|
615
|
+
context_lines << " #{trace}"
|
616
|
+
end
|
617
|
+
end
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
# Add warnings if any
|
622
|
+
if @all_warnings.any? && @options.fetch(:warnings, true)
|
623
|
+
context_lines << ""
|
624
|
+
context_lines << "Parser Warnings:"
|
625
|
+
@all_warnings.each do |warning|
|
626
|
+
context_lines << " #{warning[:file]}:#{warning[:line]}: #{warning[:message]}"
|
627
|
+
context_lines << " #{warning[:suggestion]}" if warning[:suggestion]
|
628
|
+
end
|
629
|
+
end
|
630
|
+
|
631
|
+
context_lines.join("\n")
|
632
|
+
end
|
633
|
+
|
634
|
+
# Build environment context with language-agnostic keys
|
635
|
+
def build_environment_context
|
636
|
+
env_vars = {}
|
637
|
+
|
638
|
+
# CI/CD detection - prioritize most specific
|
639
|
+
if ENV['GITHUB_ACTIONS']
|
640
|
+
env_vars['CI'] = 'github'
|
641
|
+
elsif ENV['GITLAB_CI']
|
642
|
+
env_vars['CI'] = 'gitlab'
|
643
|
+
elsif ENV['JENKINS_URL']
|
644
|
+
env_vars['CI'] = 'jenkins'
|
645
|
+
elsif ENV['CI']
|
646
|
+
env_vars['CI'] = 'true'
|
647
|
+
end
|
648
|
+
|
649
|
+
# Runtime environment - only if not default
|
650
|
+
if ENV['RAILS_ENV'] && ENV['RAILS_ENV'] != 'development'
|
651
|
+
env_vars['ENV'] = ENV['RAILS_ENV']
|
652
|
+
elsif ENV['RACK_ENV'] && ENV['RACK_ENV'] != 'development'
|
653
|
+
env_vars['ENV'] = ENV['RACK_ENV']
|
654
|
+
elsif ENV['NODE_ENV'] && ENV['NODE_ENV'] != 'development'
|
655
|
+
env_vars['ENV'] = ENV['NODE_ENV']
|
656
|
+
end
|
657
|
+
|
658
|
+
# Coverage - simplified
|
659
|
+
env_vars['COV'] = '1' if ENV['COVERAGE'] || ENV['SIMPLECOV']
|
660
|
+
|
661
|
+
# Test seed for reproducibility
|
662
|
+
env_vars['SEED'] = ENV['SEED'] if ENV['SEED']
|
663
|
+
|
664
|
+
env_vars
|
665
|
+
end
|
666
|
+
|
667
|
+
# Build execution flags in language-agnostic format
|
668
|
+
def build_execution_flags
|
669
|
+
flags = []
|
670
|
+
flags << "verbose" if @options[:verbose]
|
671
|
+
flags << "fails-only" if @options[:fails_only]
|
672
|
+
flags << "debug" if @options[:debug]
|
673
|
+
flags << "traces" if @options[:stack_traces] && !@options[:debug] # debug implies traces
|
674
|
+
flags << "parallel" if @options[:parallel]
|
675
|
+
flags << "line-spec" if @options[:line_spec]
|
676
|
+
flags << "strict" if @options[:strict]
|
677
|
+
flags << "quiet" if @options[:quiet]
|
678
|
+
flags
|
679
|
+
end
|
680
|
+
|
681
|
+
# Get test discovery patterns in language-agnostic format
|
682
|
+
def get_test_discovery_patterns
|
683
|
+
patterns = []
|
684
|
+
|
685
|
+
# Ruby/Tryouts patterns
|
686
|
+
patterns.concat([
|
687
|
+
"**/*_try.rb",
|
688
|
+
"**/*.try.rb",
|
689
|
+
"try/**/*.rb",
|
690
|
+
"tryouts/**/*.rb"
|
691
|
+
])
|
692
|
+
|
693
|
+
# TOPA-compatible patterns for other languages:
|
694
|
+
# Python: ["**/*_test.py", "**/test_*.py", "tests/**/*.py"]
|
695
|
+
# JavaScript: ["**/*.test.js", "**/*.spec.js", "__tests__/**/*.js"]
|
696
|
+
# Java: ["**/*Test.java", "**/Test*.java", "src/test/**/*.java"]
|
697
|
+
# Go: ["**/*_test.go"]
|
698
|
+
# C#: ["**/*Test.cs", "**/*Tests.cs"]
|
699
|
+
# PHP: ["**/*Test.php", "tests/**/*.php"]
|
700
|
+
# Rust: ["**/*_test.rs", "tests/**/*.rs"]
|
701
|
+
|
702
|
+
patterns
|
703
|
+
end
|
704
|
+
|
705
|
+
private
|
706
|
+
|
707
|
+
# Safely get git information with timeout protection
|
708
|
+
def safe_git_info
|
709
|
+
# Check if we're in a git repository
|
710
|
+
return {} unless File.directory?('.git') || system('git rev-parse --git-dir >/dev/null 2>&1')
|
711
|
+
|
712
|
+
require 'timeout'
|
713
|
+
|
714
|
+
Timeout.timeout(2) do
|
715
|
+
branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
716
|
+
commit = `git rev-parse --short HEAD 2>/dev/null`.strip
|
717
|
+
|
718
|
+
# Validate output to prevent injection
|
719
|
+
branch = nil unless branch =~ /\A[\w\-\/\.]+\z/
|
720
|
+
commit = nil unless commit =~ /\A[a-f0-9]+\z/i
|
721
|
+
|
722
|
+
{ branch: branch, commit: commit }
|
723
|
+
end
|
724
|
+
rescue Timeout::Error, StandardError
|
725
|
+
# Return empty hash on any error (timeout, permission, etc.)
|
726
|
+
{}
|
727
|
+
end
|
448
728
|
end
|
449
729
|
end
|
450
730
|
end
|
@@ -33,6 +33,10 @@ class Tryouts
|
|
33
33
|
# Default: no output
|
34
34
|
end
|
35
35
|
|
36
|
+
def parser_warnings(file_path, warnings:)
|
37
|
+
# Default: no output - override in specific formatters
|
38
|
+
end
|
39
|
+
|
36
40
|
def file_execution_start(file_path, test_count:, context_mode:)
|
37
41
|
# Default: no output
|
38
42
|
end
|
@@ -151,7 +155,7 @@ class Tryouts
|
|
151
155
|
end
|
152
156
|
|
153
157
|
def separator(style = :light)
|
154
|
-
width = @options.fetch(:line_width,
|
158
|
+
width = @options.fetch(:line_width, 60)
|
155
159
|
case style
|
156
160
|
when :heavy
|
157
161
|
'=' * width
|
@@ -44,9 +44,20 @@ class Tryouts
|
|
44
44
|
@stderr.puts indent_text("Parsed #{test_count} tests#{suffix}", 1)
|
45
45
|
end
|
46
46
|
|
47
|
+
def parser_warnings(file_path, warnings:)
|
48
|
+
return if warnings.empty? || !@options.fetch(:warnings, true)
|
49
|
+
|
50
|
+
@stderr.puts
|
51
|
+
@stderr.puts Console.color(:yellow, "Warnings:")
|
52
|
+
warnings.each do |warning|
|
53
|
+
@stderr.puts " #{Console.pretty_path(file_path)}:#{warning.line_number}: #{warning.message}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
47
57
|
def file_execution_start(file_path, test_count:, context_mode:)
|
48
58
|
pretty_path = Console.pretty_path(file_path)
|
49
|
-
|
59
|
+
puts
|
60
|
+
puts "#{pretty_path}: #{test_count} tests"
|
50
61
|
end
|
51
62
|
|
52
63
|
# Summary operations - show failure summary
|
@@ -204,7 +215,6 @@ class Tryouts
|
|
204
215
|
|
205
216
|
def grand_total(total_tests:, failed_count:, error_count:, successful_files:, total_files:, elapsed_time:)
|
206
217
|
@stderr.puts
|
207
|
-
@stderr.puts '=' * 50
|
208
218
|
|
209
219
|
issues_count = failed_count + error_count
|
210
220
|
if issues_count > 0
|
@@ -219,8 +229,8 @@ class Tryouts
|
|
219
229
|
|
220
230
|
time_str = format_timing(elapsed_time)
|
221
231
|
|
222
|
-
@stderr.puts "
|
223
|
-
@stderr.puts "
|
232
|
+
@stderr.puts "#{result}#{time_str}"
|
233
|
+
@stderr.puts "#{successful_files} of #{total_files} files passed"
|
224
234
|
end
|
225
235
|
|
226
236
|
# Debug and diagnostic output - minimal in compact mode
|
@@ -57,6 +57,10 @@ class Tryouts
|
|
57
57
|
)
|
58
58
|
end
|
59
59
|
|
60
|
+
def parser_warnings(file_path, warnings:)
|
61
|
+
@formatter.parser_warnings(file_path, warnings: warnings)
|
62
|
+
end
|
63
|
+
|
60
64
|
def file_execution_start(file_path, test_count, context_mode)
|
61
65
|
@formatter.file_execution_start(
|
62
66
|
file_path,
|