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.
@@ -15,19 +15,72 @@ class Tryouts
15
15
  try --direct --shared-context test_try.rb # Explicit shared context
16
16
  try --generate-rspec test_try.rb # Output RSpec code only
17
17
  try --inspect test_try.rb # Inspect file structure and validation
18
-
19
- File Format:
20
- ## Test description # Test case marker
21
- code_to_test # Ruby code
22
- #=> expected_result # Expectation (various types available)
18
+ try --agent test_try.rb # Agent-optimized structured output
19
+ try --agent --agent-limit 10000 tests/ # Agent mode with 10K token limit
20
+
21
+ Agent Output Modes:
22
+ --agent # Structured, token-efficient output
23
+ --agent-focus summary # Show counts and problem files only
24
+ --agent-focus first-failure # Show first failure per file
25
+ --agent-focus critical # Show errors/exceptions only
26
+ --agent-limit 1000 # Limit output to 1000 tokens
27
+
28
+ File Naming & Organization:
29
+ Files must end with '_try.rb' or '.try.rb' (e.g., auth_service_try.rb, user_model.try.rb)
30
+ Auto-discovery searches: ./try/, ./tryouts/, ./*_try.rb, ./*.try.rb patterns
31
+ Organize by feature/module: try/models/, try/services/, try/api/
32
+
33
+ Testcase Structure (3 required parts)
34
+ ## This is the description
35
+ echo 'This is ruby code under test'
36
+ true
37
+ #=> true # this is the expected result
38
+
39
+ File Structure (3 sections):
40
+ # Setup section (optional) - runs once before all tests
41
+ @shared_var = "available to all test cases"
42
+
43
+ ## TEST: Feature description
44
+ # Test case body with plain Ruby code
45
+ result = some_operation()
46
+ #=> expected_value
47
+
48
+ # Teardown section (optional) - runs once after all tests
49
+
50
+ Context Guidelines:
51
+ Shared Context (default): Instance variables persist across test cases
52
+ - Use for: Integration testing, stateful scenarios, realistic workflows
53
+ - Caution: Test order matters, state accumulates
54
+
55
+ Fresh Context (--rspec/--minitest): Each test gets isolated environment
56
+ - Use for: Unit testing, independent test cases
57
+ - Setup variables copied to each test, but changes don't persist
58
+
59
+ Writing Quality Tryouts:
60
+ - Use realistic, plain Ruby code (avoid mocks, test harnesses)
61
+ - Test descriptions start with ##, be specific about what's being tested
62
+ - One result per test case (last expression is the result)
63
+ - Use appropriate expectation types for clarity (#==> for boolean, #=:> for types)
64
+ - Keep tests focused and readable - they serve as documentation
23
65
 
24
66
  Great Expectations System:
25
- Multiple expectation types are supported for different testing needs.
26
-
27
67
  #=> Value equality #==> Must be true #=/=> Must be false
28
68
  #=|> True OR false #=!> Must raise error #=:> Type matching
29
69
  #=~> Regex matching #=%> Time constraints #=*> Non-nil result
30
70
  #=1> STDOUT content #=2> STDERR content #=<> Intentional failure
71
+
72
+ Exception Testing:
73
+ # Method 1: Rescue and test exception
74
+ begin
75
+ risky_operation
76
+ rescue StandardError => e
77
+ e.class
78
+ end
79
+ #=> StandardError
80
+
81
+ # Method 2: Let it raise and test with #=!>
82
+ risky_operation
83
+ #=!> error.is_a?(StandardError)
31
84
  HELP
32
85
 
33
86
  class << self
@@ -70,6 +123,26 @@ class Tryouts
70
123
  options[:parallel_threads] = threads.to_i if threads && threads.to_i > 0
71
124
  end
72
125
 
126
+ opts.separator "\nParser Options:"
127
+ opts.on('--strict', 'Require explicit test descriptions (fail on unnamed tests)') { options[:strict] = true }
128
+ opts.on('--no-strict', 'Allow unnamed tests (legacy behavior)') { options[:strict] = false }
129
+ opts.on('-w', '--warnings', 'Show parser warnings (default: true)') { options[:warnings] = true }
130
+ opts.on('--no-warnings', 'Suppress parser warnings') { options[:warnings] = false }
131
+
132
+ opts.separator "\nAgent-Optimized Output:"
133
+ opts.on('-a', '--agent', 'Agent-optimized structured output for LLM context management') do
134
+ options[:agent] = true
135
+ end
136
+ opts.on('--agent-limit TOKENS', Integer, 'Limit total output to token budget (default: 5000)') do |limit|
137
+ options[:agent] = true
138
+ options[:agent_limit] = limit
139
+ end
140
+ opts.on('--agent-focus TYPE', %w[failures first-failure summary critical],
141
+ 'Focus mode: failures, first-failure, summary, critical (default: failures)') do |focus|
142
+ options[:agent] = true
143
+ options[:agent_focus] = focus.to_sym
144
+ end
145
+
73
146
  opts.separator "\nParser Options:"
74
147
  opts.on('--enhanced-parser', 'Use enhanced parser with inhouse comment extraction (default)') { options[:parser] = :enhanced }
75
148
  opts.on('--legacy-parser', 'Use legacy prism parser') { options[:parser] = :prism }
data/lib/tryouts/cli.rb CHANGED
@@ -4,6 +4,7 @@ require 'optparse'
4
4
 
5
5
  require_relative 'cli/opts'
6
6
  require_relative 'cli/formatters'
7
+ require_relative 'cli/line_spec_parser'
7
8
  require_relative 'test_runner'
8
9
 
9
10
  class Tryouts
@@ -13,6 +14,8 @@ class Tryouts
13
14
  framework: :direct,
14
15
  verbose: false,
15
16
  inspect: false,
17
+ strict: true, # Default to strict mode for better UX
18
+ warnings: true, # Default to showing warnings
16
19
  }
17
20
  end
18
21
 
@@ -22,11 +25,14 @@ class Tryouts
22
25
  output_manager = FormatterFactory.create_output_manager(@options)
23
26
 
24
27
  handle_version_flag(@options, output_manager)
25
- validate_files_exist(files, output_manager)
28
+
29
+ # Parse line specs from file arguments
30
+ files_with_specs = parse_file_specs(files)
31
+ validate_files_exist(files_with_specs, output_manager)
26
32
 
27
33
  runner = TestRunner.new(
28
- files: files,
29
- options: @options,
34
+ files: files_with_specs.keys,
35
+ options: @options.merge(file_line_specs: files_with_specs),
30
36
  output_manager: output_manager,
31
37
  )
32
38
 
@@ -42,13 +48,24 @@ class Tryouts
42
48
  exit 0
43
49
  end
44
50
 
45
- def validate_files_exist(files, output_manager)
46
- missing_files = files.reject { |file| File.exist?(file) }
51
+ def validate_files_exist(files_with_specs, output_manager)
52
+ missing_files = files_with_specs.keys.reject { |file| File.exist?(file) }
47
53
 
48
54
  unless missing_files.empty?
49
55
  missing_files.each { |file| output_manager.error("File not found: #{file}") }
50
56
  exit 1
51
57
  end
52
58
  end
59
+
60
+ def parse_file_specs(files)
61
+ files_with_specs = {}
62
+
63
+ files.each do |file_arg|
64
+ filepath, line_spec = LineSpecParser.parse(file_arg)
65
+ files_with_specs[filepath] = line_spec
66
+ end
67
+
68
+ files_with_specs
69
+ end
53
70
  end
54
71
  end
@@ -20,8 +20,15 @@ class Tryouts
20
20
 
21
21
  def process
22
22
  testrun = create_parser(@file, @options).parse
23
+
24
+ # Apply line spec filtering before reporting test counts
25
+ if @options[:line_spec]
26
+ testrun = filter_testrun_by_line_spec(testrun)
27
+ end
28
+
23
29
  @global_tally[:aggregator].increment_total_files
24
30
  @output_manager.file_parsed(@file, testrun.total_tests)
31
+ @output_manager.parser_warnings(@file, warnings: testrun.warnings)
25
32
 
26
33
  if @options[:inspect]
27
34
  handle_inspect_mode(testrun)
@@ -38,6 +45,34 @@ class Tryouts
38
45
 
39
46
  private
40
47
 
48
+ def filter_testrun_by_line_spec(testrun)
49
+ require_relative 'cli/line_spec_parser'
50
+
51
+ line_spec = @options[:line_spec]
52
+
53
+ # Filter test cases to only those that match the line spec
54
+ filtered_cases = testrun.test_cases.select do |test_case|
55
+ Tryouts::CLI::LineSpecParser.matches?(test_case, line_spec)
56
+ end
57
+
58
+ # Check if any tests matched the line specification
59
+ if filtered_cases.empty?
60
+ @output_manager.file_failure(@file, "No test cases found matching line specification: #{line_spec}")
61
+ return testrun # Return original testrun to avoid breaking the pipeline
62
+ end
63
+
64
+ # Create a new testrun with filtered cases
65
+ # We need to preserve the setup and teardown but only include matching tests
66
+ testrun.class.new(
67
+ setup: testrun.setup,
68
+ test_cases: filtered_cases,
69
+ teardown: testrun.teardown,
70
+ source_file: testrun.source_file,
71
+ metadata: testrun.metadata,
72
+ warnings: testrun.warnings
73
+ )
74
+ end
75
+
41
76
  def create_parser(file, options)
42
77
  parser_type = options[:parser] || :enhanced # enhanced parser is now the default
43
78
 
@@ -47,9 +82,9 @@ class Tryouts
47
82
 
48
83
  case parser_type
49
84
  when :enhanced
50
- EnhancedParser.new(file)
85
+ EnhancedParser.new(file, options)
51
86
  when :prism
52
- PrismParser.new(file)
87
+ PrismParser.new(file, options)
53
88
  end
54
89
  end
55
90
 
@@ -0,0 +1,26 @@
1
+ # lib/tryouts/parser_warning.rb
2
+
3
+ class Tryouts
4
+ # Data structure for parser warnings
5
+ ParserWarning = Data.define(:type, :message, :line_number, :context, :suggestion) do
6
+ def self.unnamed_test(line_number:, context:)
7
+ new(
8
+ type: :unnamed_test,
9
+ message: "Test case without explicit description",
10
+ line_number: line_number,
11
+ context: context,
12
+ suggestion: "Add a test description using '## Description' prefix"
13
+ )
14
+ end
15
+
16
+ def self.ambiguous_test_boundary(line_number:, context:)
17
+ new(
18
+ type: :ambiguous_boundary,
19
+ message: "Ambiguous test case boundary detected",
20
+ line_number: line_number,
21
+ context: context,
22
+ suggestion: "Use explicit '## Description' to clarify test structure"
23
+ )
24
+ end
25
+ end
26
+ end
@@ -3,6 +3,7 @@
3
3
  require 'prism'
4
4
 
5
5
  require_relative 'shared_methods'
6
+ require_relative '../parser_warning'
6
7
 
7
8
  class Tryouts
8
9
  # Fixed PrismParser with pattern matching for robust token filtering
@@ -10,12 +11,14 @@ class Tryouts
10
11
  class BaseParser
11
12
  include Tryouts::Parsers::SharedMethods
12
13
 
13
- def initialize(source_path)
14
+ def initialize(source_path, options = {})
14
15
  @source_path = source_path
15
16
  @source = File.read(source_path)
16
17
  @lines = @source.lines.map(&:chomp)
17
18
  @prism_result = Prism.parse(@source)
18
19
  @parsed_at = Time.now
20
+ @options = options
21
+ @warnings = []
19
22
  end
20
23
 
21
24
  end
@@ -1,5 +1,7 @@
1
1
  # lib/tryouts/parsers/shared_methods.rb
2
2
 
3
+ require_relative '../parser_warning'
4
+
3
5
  class Tryouts
4
6
  module Parsers
5
7
  module SharedMethods
@@ -205,13 +207,19 @@ class Tryouts
205
207
  test_blocks = classified_blocks.filter { |block| block[:type] == :test }
206
208
  teardown_blocks = classified_blocks.filter { |block| block[:type] == :teardown }
207
209
 
208
- Testrun.new(
210
+ testrun = Testrun.new(
209
211
  setup: build_setup(setup_blocks),
210
212
  test_cases: test_blocks.map { |block| build_test_case(block) },
211
213
  teardown: build_teardown(teardown_blocks),
212
214
  source_file: @source_path,
213
215
  metadata: { parsed_at: @parsed_at, parser: parser_type },
216
+ warnings: warnings
214
217
  )
218
+
219
+ # Validate strict mode after collecting all warnings
220
+ validate_strict_mode(testrun)
221
+
222
+ testrun
215
223
  end
216
224
 
217
225
  def build_setup(setup_blocks)
@@ -354,6 +362,9 @@ class Tryouts
354
362
  start_line: Integer => start_line,
355
363
  end_line: Integer => end_line
356
364
  }
365
+ # Collect warning for unnamed test
366
+ collect_unnamed_test_warning(block)
367
+
357
368
  source_lines = @lines[start_line..end_line]
358
369
  first_expectation_line = exp_tokens.empty? ? start_line : exp_tokens.first[:line]
359
370
 
@@ -411,6 +422,44 @@ class Tryouts
411
422
  :shared
412
423
  end
413
424
 
425
+ # Warning collection methods
426
+ def add_warning(warning)
427
+ @warnings ||= []
428
+ @warnings << warning
429
+ end
430
+
431
+ def warnings
432
+ @warnings ||= []
433
+ end
434
+
435
+ def collect_unnamed_test_warning(block)
436
+ return unless block[:type] == :test && block[:description].empty?
437
+
438
+ line_number = block[:start_line] + 1
439
+ context = @lines[block[:start_line]] || ''
440
+
441
+ add_warning(ParserWarning.unnamed_test(
442
+ line_number: line_number,
443
+ context: context.strip
444
+ ))
445
+ end
446
+
447
+ def validate_strict_mode(testrun)
448
+ return unless @options[:strict]
449
+
450
+ unnamed_test_warnings = warnings.select { |w| w.type == :unnamed_test }
451
+ return if unnamed_test_warnings.empty?
452
+
453
+ # In strict mode, fail with first unnamed test error
454
+ first_warning = unnamed_test_warnings.first
455
+ raise TryoutSyntaxError.new(
456
+ "Strict mode: #{first_warning.message} at line #{first_warning.line_number}. #{first_warning.suggestion}",
457
+ line_number: first_warning.line_number,
458
+ context: first_warning.context,
459
+ source_file: @source_path
460
+ )
461
+ end
462
+
414
463
  end
415
464
  end
416
465
  end
@@ -67,7 +67,7 @@ class Tryouts
67
67
  end
68
68
  end
69
69
 
70
- Testrun = Data.define(:setup, :test_cases, :teardown, :source_file, :metadata) do
70
+ Testrun = Data.define(:setup, :test_cases, :teardown, :source_file, :metadata, :warnings) do
71
71
  def total_tests
72
72
  test_cases.size
73
73
  end
@@ -11,6 +11,7 @@ class Tryouts
11
11
  @output_manager = output_manager
12
12
  @translator = translator
13
13
  @global_tally = global_tally
14
+
14
15
  end
15
16
 
16
17
  def execute
@@ -28,6 +29,7 @@ class Tryouts
28
29
 
29
30
  private
30
31
 
32
+
31
33
  def execute_direct_mode
32
34
  batch = TestBatch.new(
33
35
  @testrun,
@@ -29,6 +29,7 @@ class Tryouts
29
29
  @output_manager = output_manager
30
30
  @translator = initialize_translator
31
31
  @global_tally = initialize_global_tally
32
+ @file_line_specs = options[:file_line_specs] || {}
32
33
  end
33
34
 
34
35
  def run
@@ -37,8 +38,17 @@ class Tryouts
37
38
 
38
39
  result = process_files
39
40
  show_failure_summary
40
- show_grand_total if @global_tally[:aggregator].get_file_counts[:total] > 1
41
- result
41
+ # Always show grand total for agent mode to ensure output, otherwise only for multiple files
42
+ if @options[:agent] || @global_tally[:aggregator].get_file_counts[:total] > 1
43
+ show_grand_total
44
+ end
45
+
46
+ # For agent critical mode, only count errors as failures
47
+ if @options[:agent] && (@options[:agent_focus] == :critical || @options[:agent_focus] == 'critical')
48
+ @global_tally[:aggregator].get_display_counts[:errors]
49
+ else
50
+ result
51
+ end
42
52
  end
43
53
 
44
54
  private
@@ -107,7 +117,7 @@ class Tryouts
107
117
  executor = Concurrent::ThreadPoolExecutor.new(
108
118
  min_threads: 1,
109
119
  max_threads: pool_size,
110
- max_queue: pool_size * 2, # Reasonable queue size
120
+ max_queue: @files.length, # Queue size must accommodate all files
111
121
  fallback_policy: :abort # Raise exception if pool and queue are exhausted
112
122
  )
113
123
 
@@ -142,10 +152,16 @@ class Tryouts
142
152
  failure_count
143
153
  end
144
154
 
145
- def process_file(file_path)
155
+ def process_file(file)
156
+ # Pass line spec for this file if available
157
+ file_options = @options.dup
158
+ if @file_line_specs && @file_line_specs[file]
159
+ file_options[:line_spec] = @file_line_specs[file]
160
+ end
161
+
146
162
  processor = FileProcessor.new(
147
- file: file_path,
148
- options: @options,
163
+ file: file,
164
+ options: file_options,
149
165
  output_manager: @output_manager,
150
166
  translator: @translator,
151
167
  global_tally: @global_tally,
@@ -154,7 +170,7 @@ class Tryouts
154
170
  rescue StandardError => ex
155
171
  handle_file_error(ex)
156
172
  @global_tally[:aggregator].add_infrastructure_failure(
157
- :file_processing, file_path, ex.message, ex
173
+ :file_processing, file, ex.message, ex
158
174
  )
159
175
  1
160
176
  end
@@ -1,5 +1,5 @@
1
1
  # lib/tryouts/version.rb
2
2
 
3
3
  class Tryouts
4
- VERSION = '3.3.2'
4
+ VERSION = '3.5.0'
5
5
  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.3.2
4
+ version: 3.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
@@ -137,6 +137,7 @@ files:
137
137
  - lib/tryouts.rb
138
138
  - lib/tryouts/cli.rb
139
139
  - lib/tryouts/cli/formatters.rb
140
+ - lib/tryouts/cli/formatters/agent.rb
140
141
  - lib/tryouts/cli/formatters/base.rb
141
142
  - lib/tryouts/cli/formatters/compact.rb
142
143
  - lib/tryouts/cli/formatters/factory.rb
@@ -144,8 +145,10 @@ files:
144
145
  - lib/tryouts/cli/formatters/output_manager.rb
145
146
  - lib/tryouts/cli/formatters/quiet.rb
146
147
  - lib/tryouts/cli/formatters/test_run_state.rb
148
+ - lib/tryouts/cli/formatters/token_budget.rb
147
149
  - lib/tryouts/cli/formatters/tty_status_display.rb
148
150
  - lib/tryouts/cli/formatters/verbose.rb
151
+ - lib/tryouts/cli/line_spec_parser.rb
149
152
  - lib/tryouts/cli/modes/generate.rb
150
153
  - lib/tryouts/cli/modes/inspect.rb
151
154
  - lib/tryouts/cli/opts.rb
@@ -167,6 +170,7 @@ files:
167
170
  - lib/tryouts/expectation_evaluators/true.rb
168
171
  - lib/tryouts/failure_collector.rb
169
172
  - lib/tryouts/file_processor.rb
173
+ - lib/tryouts/parser_warning.rb
170
174
  - lib/tryouts/parsers/base_parser.rb
171
175
  - lib/tryouts/parsers/enhanced_parser.rb
172
176
  - lib/tryouts/parsers/prism_parser.rb