tryouts 3.3.0 → 3.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7ef307bf73a8bd6c70f52c47337d550ae7715e0b64d15c7f708c9fd407f3df2
4
- data.tar.gz: ccd166f03573bee1d31e3843ff95964cf5065ab4a1b2386e9769ec065a6958f5
3
+ metadata.gz: adb9f46213aee10ed4b9d2a1b9b8d5c9e385ea23f996445debc7458b43351512
4
+ data.tar.gz: 8d6abfed42e73bb340e13a62991b4855b8c0c18efa3092b4e3244a822a1612c1
5
5
  SHA512:
6
- metadata.gz: 22b0d458cfadd632495c4df95f41cca3c06c6e6b3e219b18afb0fd21bfc14d1d9a872b165fcd2dd1c26052a9f57b84dad77c4a5a14c3efa08f62d74f0ab34467
7
- data.tar.gz: 8cd6db263235736e26163b84568ad870de5835da3002e6f71226d961fc21e19d9eaef3cf2e02598a3907cb2701665e72a7e21ecea05b540f10ba37daa530a391
6
+ metadata.gz: 571410ed483879bd035b14c053a5c3496d51ad2248cff2534cf40fc06a72ece1aea13a7e61ae2c29b1e1f1768f19d3c5ede8c4e4a30a977c29aacb502c616988
7
+ data.tar.gz: 6b5186131242edf826ba44303daaee716ce1ec364683e521df5224818d796067209a9a0ae66cacc85d15796cfee92df2349372fc5df762ced52b8753e1196c0e
data/exe/try CHANGED
@@ -17,7 +17,7 @@ if ENV['COVERAGE'] || ENV['SIMPLECOV']
17
17
  add_group 'Core', 'lib/tryouts.rb'
18
18
  add_group 'CLI', 'lib/tryouts/cli'
19
19
  add_group 'Formatters', 'lib/tryouts/cli/formatters'
20
- add_group 'Parsers', 'lib/tryouts/prism_parser.rb'
20
+ add_group 'Parsers', 'lib/tryouts/parsers'
21
21
  add_group 'Data Structures', ['lib/tryouts/testcase.rb', 'lib/tryouts/testbatch.rb']
22
22
  add_group 'Translators', 'lib/tryouts/translators'
23
23
  add_group 'Execution', ['lib/tryouts/test_executor.rb', 'lib/tryouts/test_runner.rb', 'lib/tryouts/file_processor.rb']
@@ -57,6 +57,8 @@ class Tryouts
57
57
  puts Console.color(:red, 'Failed Tests:')
58
58
  puts
59
59
 
60
+ # Number failures sequentially across all files instead of per-file
61
+ failure_number = 1
60
62
  failure_collector.failures_by_file.each do |file_path, failures|
61
63
  failures.each do |failure|
62
64
  pretty_path = Console.pretty_path(file_path)
@@ -68,10 +70,11 @@ class Tryouts
68
70
  pretty_path
69
71
  end
70
72
 
71
- puts " #{location}"
73
+ puts " #{failure_number}) #{location}"
72
74
  puts " #{Console.color(:red, '✗')} #{failure.description}"
73
75
  puts " #{failure.failure_reason}"
74
76
  puts
77
+ failure_number += 1
75
78
  end
76
79
  end
77
80
  end
@@ -56,8 +56,10 @@ class Tryouts
56
56
  puts
57
57
  puts Console.color(:red, 'Failed Tests:')
58
58
 
59
+ # Number failures sequentially across all files instead of per-file
60
+ failure_number = 1
59
61
  failure_collector.failures_by_file.each do |file_path, failures|
60
- failures.each_with_index do |failure, index|
62
+ failures.each do |failure|
61
63
  pretty_path = Console.pretty_path(file_path)
62
64
 
63
65
  # Include line number with file path for easy copying/clicking
@@ -69,7 +71,7 @@ class Tryouts
69
71
 
70
72
  puts
71
73
  puts Console.color(:yellow, location)
72
- puts " #{index + 1}) #{failure.description}"
74
+ puts " #{failure_number}) #{failure.description}"
73
75
  puts " #{Console.color(:red, 'Failure:')} #{failure.failure_reason}"
74
76
 
75
77
  # Show source context in verbose mode
@@ -80,6 +82,7 @@ class Tryouts
80
82
  end
81
83
  end
82
84
  puts
85
+ failure_number += 1
83
86
  end
84
87
  end
85
88
  end
@@ -67,8 +67,8 @@ class Tryouts
67
67
  opts.on('-l', '--live', 'Live status display') { options[:live_status] = true }
68
68
 
69
69
  opts.separator "\nParser Options:"
70
- opts.on('--enhanced-parser', 'Use enhanced parser with inhouse comment extraction') { options[:parser] = :enhanced }
71
- opts.on('--legacy-parser', 'Use legacy parser (current default)') { options[:parser] = :prism }
70
+ opts.on('--enhanced-parser', 'Use enhanced parser with inhouse comment extraction (default)') { options[:parser] = :enhanced }
71
+ opts.on('--legacy-parser', 'Use legacy prism parser') { options[:parser] = :prism }
72
72
 
73
73
  opts.separator "\nInspection Options:"
74
74
  opts.on('-i', '--inspect', 'Inspect file structure without running tests') { options[:inspect] = true }
@@ -40,9 +40,17 @@ class Tryouts
40
40
  expectation_result = ExpectationResult.from_result(actual_result)
41
41
  pattern = eval_expectation_content(@expectation.content, expectation_result)
42
42
 
43
- # Convert actual_result to string for regex matching
44
- string_result = actual_result.to_s
45
- match_result = string_result =~ pattern
43
+ # Auto-detect exceptions and use message for regex matching
44
+ # This allows #=~> /pattern/ to work naturally with exception messages
45
+ string_result = if actual_result.is_a?(Exception)
46
+ # Make error available in context for manual access if needed
47
+ @context.define_singleton_method(:error) { actual_result }
48
+ actual_result.message # Match against error message
49
+ else
50
+ actual_result.to_s # Normal case: convert to string
51
+ end
52
+
53
+ match_result = string_result =~ pattern
46
54
 
47
55
  build_result(
48
56
  passed: !match_result.nil?,
@@ -36,7 +36,15 @@ class Tryouts
36
36
 
37
37
  def evaluate(actual_result = nil)
38
38
  expectation_result = ExpectationResult.from_result(actual_result)
39
- expected_class = eval_expectation_content(@expectation.content, expectation_result)
39
+
40
+ # Try to evaluate in test context first, then fallback to global context for constants
41
+ begin
42
+ expected_class = eval_expectation_content(@expectation.content, expectation_result)
43
+ rescue NameError => e
44
+ # If we can't find the constant in test context, try global context
45
+ # This is common for exception classes like ArgumentError, StandardError, etc.
46
+ expected_class = Object.const_get(@expectation.content.strip)
47
+ end
40
48
 
41
49
  build_result(
42
50
  passed: actual_result.is_a?(expected_class),
@@ -1,7 +1,7 @@
1
1
  # lib/tryouts/file_processor.rb
2
2
 
3
- require_relative 'prism_parser'
4
- require_relative 'enhanced_parser'
3
+ require_relative 'parsers/prism_parser'
4
+ require_relative 'parsers/enhanced_parser'
5
5
  require_relative 'test_executor'
6
6
  require_relative 'cli/modes/inspect'
7
7
  require_relative 'cli/modes/generate'
@@ -39,7 +39,7 @@ class Tryouts
39
39
  private
40
40
 
41
41
  def create_parser(file, options)
42
- parser_type = options[:parser] || :prism # default to legacy for safe rollout
42
+ parser_type = options[:parser] || :enhanced # enhanced parser is now the default
43
43
 
44
44
  unless PARSER_TYPES.include?(parser_type)
45
45
  raise ArgumentError, "Unknown parser: #{parser_type}. Allowed types: #{PARSER_TYPES.join(', ')}"
@@ -0,0 +1,23 @@
1
+ # lib/tryouts/parsers/base_parser.rb
2
+
3
+ require 'prism'
4
+
5
+ require_relative 'shared_methods'
6
+
7
+ class Tryouts
8
+ # Fixed PrismParser with pattern matching for robust token filtering
9
+ module Parsers
10
+ class BaseParser
11
+ include Tryouts::Parsers::SharedMethods
12
+
13
+ def initialize(source_path)
14
+ @source_path = source_path
15
+ @source = File.read(source_path)
16
+ @lines = @source.lines.map(&:chomp)
17
+ @prism_result = Prism.parse(@source)
18
+ @parsed_at = Time.now
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,113 @@
1
+ # lib/tryouts/parsers/enhanced_parser.rb
2
+
3
+ # Enhanced parser using Prism's inhouse comment extraction capabilities
4
+ # Drop-in replacement for PrismParser that eliminates HEREDOC parsing issues
5
+
6
+ require_relative '../test_case'
7
+ require_relative 'base_parser'
8
+
9
+ class Tryouts
10
+ # Enhanced parser that replaces manual line-by-line parsing with inhouse Prism APIs
11
+ # while maintaining full compatibility with the original parser's logic structure
12
+ class EnhancedParser < Tryouts::Parsers::BaseParser
13
+
14
+ def parse
15
+ return handle_syntax_errors if @prism_result.failure?
16
+
17
+ # Use inhouse comment extraction instead of line-by-line regex parsing
18
+ # This automatically excludes HEREDOC content!
19
+ tokens = tokenize_content_with_inhouse_extraction
20
+ test_boundaries = find_test_case_boundaries(tokens)
21
+ tokens = classify_potential_descriptions_with_boundaries(tokens, test_boundaries)
22
+ test_blocks = group_into_test_blocks(tokens)
23
+ process_test_blocks(test_blocks)
24
+ end
25
+
26
+ private
27
+
28
+ # Inhouse comment extraction - replaces the manual regex parsing
29
+ def tokenize_content_with_inhouse_extraction
30
+ tokens = []
31
+
32
+ # Get all comments using inhouse Prism extraction
33
+ comments = Prism.parse_comments(@source)
34
+ comment_by_line = comments.group_by { |comment| comment.location.start_line }
35
+
36
+ # Process each line, handling multiple comments per line
37
+ @lines.each_with_index do |line, index|
38
+ line_number = index + 1
39
+
40
+ if (comments_for_line = comment_by_line[line_number]) && !comments_for_line.empty?
41
+ emitted_code = false
42
+ comments_for_line.sort_by! { |c| c.location.start_column }
43
+ comments_for_line.each do |comment|
44
+ comment_content = comment.slice.strip
45
+ if comment.location.start_column > 0
46
+ unless emitted_code
47
+ tokens << { type: :code, content: line, line: index, ast: parse_ruby_line(line) }
48
+ emitted_code = true
49
+ end
50
+ # Inline comment may carry expectations; classify it too
51
+ tokens << classify_comment_inhousely(comment_content, line_number)
52
+ else
53
+ tokens << classify_comment_inhousely(comment_content, line_number)
54
+ end
55
+ end
56
+ next
57
+ end
58
+
59
+ # Handle non-comment lines (blank lines and code)
60
+ token = case line
61
+ when /^\s*$/
62
+ { type: :blank, line: index }
63
+ else
64
+ { type: :code, content: line, line: index, ast: parse_ruby_line(line) }
65
+ end
66
+ tokens << token
67
+ end
68
+
69
+ tokens
70
+ end
71
+
72
+ # Inhouse comment classification - replaces complex regex patterns
73
+ def classify_comment_inhousely(content, line_number)
74
+ case content
75
+ when /^##\s*(.*)$/
76
+ { type: :description, content: $1.strip, line: line_number - 1 }
77
+ when /^#\s*TEST\s*\d*:\s*(.*)$/
78
+ { type: :description, content: $1.strip, line: line_number - 1 }
79
+ when /^#\s*=!>\s*(.*)$/
80
+ { type: :exception_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
81
+ when /^#\s*=<>\s*(.*)$/
82
+ { type: :intentional_failure_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
83
+ when /^#\s*==>\s*(.*)$/
84
+ { type: :true_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
85
+ when %r{^#\s*=/=>\s*(.*)$}
86
+ { type: :false_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
87
+ when /^#\s*=\|>\s*(.*)$/
88
+ { type: :boolean_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
89
+ when /^#\s*=:>\s*(.*)$/
90
+ { type: :result_type_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
91
+ when /^#\s*=~>\s*(.*)$/
92
+ { type: :regex_match_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
93
+ when /^#\s*=%>\s*(.*)$/
94
+ { type: :performance_time_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
95
+ when /^#\s*=(\d+)>\s*(.*)$/
96
+ { type: :output_expectation, content: $2.strip, pipe: $1.to_i, line: line_number - 1, ast: parse_expectation($2.strip) }
97
+ when /^#\s*=>\s*(.*)$/
98
+ { type: :expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
99
+ when /^##\s*=>\s*(.*)$/
100
+ { type: :comment, content: '=>' + $1.strip, line: line_number - 1 }
101
+ when /^#\s*(.*)$/
102
+ { type: :potential_description, content: $1.strip, line: line_number - 1 }
103
+ else
104
+ { type: :comment, content: content.sub(/^#\s*/, ''), line: line_number - 1 }
105
+ end
106
+ end
107
+
108
+ # Parser type identification for metadata
109
+ def parser_type
110
+ :enhanced
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,120 @@
1
+ # lib/tryouts/parsers/prism_parser.rb
2
+
3
+ require_relative '../test_case'
4
+ require_relative 'base_parser'
5
+
6
+ class Tryouts
7
+ # Fixed PrismParser with pattern matching for robust token filtering
8
+ class PrismParser < Tryouts::Parsers::BaseParser
9
+
10
+ def parse
11
+ return handle_syntax_errors if @prism_result.failure?
12
+
13
+ tokens = tokenize_content
14
+ test_boundaries = find_test_case_boundaries(tokens)
15
+ tokens = classify_potential_descriptions_with_boundaries(tokens, test_boundaries)
16
+ test_blocks = group_into_test_blocks(tokens)
17
+ process_test_blocks(test_blocks)
18
+ end
19
+
20
+ private
21
+
22
+ # Tokenize content using pattern matching for clean line classification
23
+ def tokenize_content
24
+ tokens = []
25
+
26
+ @lines.each_with_index do |line, index|
27
+ token = case line
28
+ in /^##\s*(.*)$/ # Test description format: ## description
29
+ { type: :description, content: $1.strip, line: index }
30
+ in /^#\s*TEST\s*\d*:\s*(.*)$/ # rubocop:disable Lint/DuplicateBranch
31
+ { type: :description, content: $1.strip, line: index }
32
+ in /^#\s*=!>\s*(.*)$/ # Exception expectation (updated for consistency)
33
+ { type: :exception_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
34
+ in /^#\s*=<>\s*(.*)$/ # Intentional failure expectation
35
+ { type: :intentional_failure_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
36
+ in /^#\s*==>\s*(.*)$/ # Boolean true expectation
37
+ { type: :true_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
38
+ in %r{^#\s*=/=>\s*(.*)$} # Boolean false expectation
39
+ { type: :false_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
40
+ in /^#\s*=\|>\s*(.*)$/ # Boolean (true or false) expectation
41
+ { type: :boolean_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
42
+ in /^#\s*=:>\s*(.*)$/ # Result type expectation
43
+ { type: :result_type_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
44
+ in /^#\s*=~>\s*(.*)$/ # Regex match expectation
45
+ { type: :regex_match_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
46
+ in /^#\s*=%>\s*(.*)$/ # Performance time expectation
47
+ { type: :performance_time_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
48
+ in /^#\s*=(\d+)>\s*(.*)$/ # Output expectation (stdout/stderr with pipe number)
49
+ { type: :output_expectation, content: $2.strip, pipe: $1.to_i, line: index, ast: parse_expectation($2.strip) }
50
+ in /^#\s*=>\s*(.*)$/ # Regular expectation
51
+ { type: :expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
52
+ in /^##\s*=>\s*(.*)$/ # Commented out expectation (should be ignored)
53
+ { type: :comment, content: '=>' + $1.strip, line: index }
54
+ in /^#\s*(.*)$/ # Single hash comment - potential description
55
+ { type: :potential_description, content: $1.strip, line: index }
56
+ in /^\s*$/ # Blank line
57
+ { type: :blank, line: index }
58
+ else # Ruby code
59
+ { type: :code, content: line, line: index, ast: parse_ruby_line(line) }
60
+ end
61
+
62
+ tokens << token
63
+ end
64
+
65
+ # Return tokens with potential_descriptions - they'll be classified later with test boundaries
66
+ tokens
67
+ end
68
+
69
+
70
+ # Convert potential_descriptions to descriptions or comments based on context
71
+ def classify_potential_descriptions(tokens)
72
+ tokens.map.with_index do |token, index|
73
+ if token[:type] == :potential_description
74
+ # Check if this looks like a test description based on content and context
75
+ content = token[:content].strip
76
+
77
+ # Skip if it's clearly just a regular comment (short, lowercase, etc.)
78
+ # Test descriptions are typically longer and more descriptive
79
+ looks_like_regular_comment = content.length < 20 &&
80
+ content.downcase == content &&
81
+ !content.match?(/test|example|demonstrate|show/i)
82
+
83
+ # Check if there's code immediately before this (suggesting it's mid-test)
84
+ prev_token = index > 0 ? tokens[index - 1] : nil
85
+ has_code_before = prev_token && prev_token[:type] == :code
86
+
87
+ if looks_like_regular_comment || has_code_before
88
+ # Treat as regular comment
89
+ token.merge(type: :comment)
90
+ else
91
+ # Look ahead for test pattern: code + at least one expectation within reasonable distance
92
+ following_tokens = tokens[(index + 1)..]
93
+
94
+ # Skip blanks and comments to find meaningful content
95
+ meaningful_following = following_tokens.reject { |t| [:blank, :comment].include?(t[:type]) }
96
+
97
+ # Look for test pattern: at least one code token followed by at least one expectation
98
+ # within the next 10 meaningful tokens (to avoid matching setup/teardown)
99
+ test_window = meaningful_following.first(10)
100
+ has_code = test_window.any? { |t| t[:type] == :code }
101
+ has_expectation = test_window.any? { |t| is_expectation_type?(t[:type]) }
102
+
103
+ if has_code && has_expectation
104
+ token.merge(type: :description)
105
+ else
106
+ token.merge(type: :comment)
107
+ end
108
+ end
109
+ else
110
+ token
111
+ end
112
+ end
113
+ end
114
+
115
+ # Parser type identification for metadata
116
+ def parser_type
117
+ :prism_v2_fixed
118
+ end
119
+ end
120
+ end