Class: Ace::TestRunner::Atoms::ResultParser
- Inherits:
-
Object
- Object
- Ace::TestRunner::Atoms::ResultParser
- Defined in:
- lib/ace/test_runner/atoms/result_parser.rb
Overview
Parses test output into structured data
Constant Summary collapse
- PATTERNS =
Patterns for parsing minitest output
{ summary: /(\d+) (?:tests?|runs?), (\d+) assertions?, (\d+) failures?, (\d+) errors?, (\d+) skips?/, failure: /^\s+\d+\) (Failure|Error):\n(.+?)(?=^\s+\d+\) |^Finished in|\z)/m, # Pattern for inline verbose failures - match test name, FAIL marker, and content until next test or EOF # Matches: " test_name FAIL (0.02s)\n error details\n /path/file.rb:123..." inline_failure: /^\s+(test_[\w_]+).*?FAIL.*?\([\d.]+s\)\n(.*?)(?=^\s+test_[\w_]+.*?(?:PASS|FAIL|ERROR|SKIP)|^Finished in|\z)/m, # Pattern for inline errors - same structure as failures but with ERROR marker inline_error: /^\s+(test_[\w_]+).*?ERROR.*?\([\d.]+s\)\n(.*?)(?=^\s+test_[\w_]+.*?(?:PASS|FAIL|ERROR|SKIP)|^Finished in|\z)/m, location: /\[(.*?):(\d+)\]/, duration: /Finished in ([\d.]+)s/, deprecation: /DEPRECATION WARNING: (.+)/, # Pattern to capture individual test times from verbose output # Matches Minitest::Reporters DefaultReporter format: # " test_name PASS (0.00s)" test_time: /^\s+(test_[\w_]+).*?\s+(PASS|FAIL|ERROR|SKIP)\s+\(([\d.]+)s\)/, # Pattern for standard Minitest verbose format: # 1. "ClassName#test_name = 0.00 s = ." # 2. "ClassName#test_name 0.00 = ." test_time_standard: /^(\S+)#(test_[\w_]+)\s+(?:=\s+)?([\d.]+)\s*s?\s*=\s*([.FEWS])/ }.freeze
Instance Method Summary collapse
- #parse_deprecations(clean_output) ⇒ Object
- #parse_duration(clean_output) ⇒ Object
- #parse_failures(clean_output) ⇒ Object
- #parse_output(output) ⇒ Object
- #parse_summary(clean_output) ⇒ Object
- #parse_test_times(clean_output) ⇒ Object
Instance Method Details
#parse_deprecations(clean_output) ⇒ Object
104 105 106 107 108 109 110 111 112 |
# File 'lib/ace/test_runner/atoms/result_parser.rb', line 104 def parse_deprecations(clean_output) deprecations = [] clean_output.scan(PATTERNS[:deprecation]) do || deprecations << .first end deprecations.uniq end |
#parse_duration(clean_output) ⇒ Object
98 99 100 101 102 |
# File 'lib/ace/test_runner/atoms/result_parser.rb', line 98 def parse_duration(clean_output) # clean_output already has ANSI codes removed match = clean_output.match(PATTERNS[:duration]) match ? match[1].to_f : 0.0 end |
#parse_failures(clean_output) ⇒ Object
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/ace/test_runner/atoms/result_parser.rb', line 66 def parse_failures(clean_output) # clean_output already has ANSI codes removed failures = [] # First try to parse standard format failures clean_output.scan(PATTERNS[:failure]) do |type, content| failure = parse_single_failure(type, content) failures << failure if failure end # If no failures found, try inline verbose format # But ONLY if the output contains FAIL or ERROR to avoid expensive processing on success if failures.empty? && (clean_output.include?(" FAIL ") || clean_output.include?(" ERROR ")) # Split by test headers and process each block test_blocks = clean_output.split(/(?=^\s+test_[\w_]+)/).select { |s| s.match?(/^\s+test_/) } test_blocks.each do |block| # Check if this block contains a FAIL or ERROR if block =~ /^\s+(test_[\w_]+).*?(ERROR|FAIL).*?\(([\d.]+)s\)\n(.*)/m test_name = $1 type = ($2 == "ERROR") ? :error : :failure content = $4 failure = parse_inline_failure(test_name, content, type) failures << failure if failure end end end failures end |
#parse_output(output) ⇒ Object
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/ace/test_runner/atoms/result_parser.rb', line 30 def parse_output(output) # Clean ANSI color codes once for all parsing methods clean_output = output.gsub(/\e\[[0-9;]*m/, "") summary = parse_summary(clean_output) failures = parse_failures(clean_output) duration = parse_duration(clean_output) deprecations = parse_deprecations(clean_output) test_times = parse_test_times(clean_output) { raw_output: output, summary: summary, failures: failures, duration: duration, deprecations: deprecations, test_times: test_times } end |
#parse_summary(clean_output) ⇒ Object
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/ace/test_runner/atoms/result_parser.rb', line 50 def parse_summary(clean_output) # clean_output already has ANSI codes removed match = clean_output.match(PATTERNS[:summary]) return default_summary unless match { runs: match[1].to_i, assertions: match[2].to_i, failures: match[3].to_i, errors: match[4].to_i, skips: match[5].to_i, passed: match[1].to_i - match[3].to_i - match[4].to_i - match[5].to_i } end |
#parse_test_times(clean_output) ⇒ Object
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/ace/test_runner/atoms/result_parser.rb', line 114 def parse_test_times(clean_output) # clean_output already has ANSI codes removed test_times = [] # Build location index first to avoid O(n²) complexity location_index = {} clean_output.scan(/(test_[\w_]+).*?\[(.*?):(\d+)\]/) do |name, file, line| location_index[name] ||= "#{file}:#{line}" end # Try Minitest::Reporters DefaultReporter format first (most common) # Format: " test_name PASS (0.00s)" clean_output.scan(PATTERNS[:test_time]) do |test_name, status, time| test_times << { name: test_name, status: status, duration: time.to_f, location: location_index[test_name] } end # If no matches, try standard Minitest verbose format # Format: "ClassName#test_name = 0.00 s = ." or "ClassName#test_name 0.00 = ." if test_times.empty? clean_output.scan(PATTERNS[:test_time_standard]) do |class_name, test_name, time, status_char| status = case status_char when "." then "PASS" when "F" then "FAIL" when "E" then "ERROR" when "S" then "SKIP" else "UNKNOWN" end test_times << { name: test_name, class_name: class_name, status: status, duration: time.to_f, location: location_index[test_name] } end end test_times.sort_by { |t| -t[:duration] } end |