Class: GeneratorTestResultsBacktrace
- Defined in:
- lib/ceedling/generators/generator_test_results_backtrace.rb
Overview
=========================================================================
Ceedling - Test-Centered Build System for C ThrowTheSwitch.org Copyright (c) 2010-26 Mike Karlesky, Mark VanderVoord, & Greg Williams SPDX-License-Identifier: MIT
Instance Method Summary collapse
-
#do_gdb(filename, executable, shell_result, test_cases, context:) ⇒ Object
Re-runs each test case (or, for a parameterized test, each group of parameterized cases -- see
group_test_cases) under gdb to identify which one(s) crashed and why. -
#do_simple(filename, executable, shell_result, test_cases, context:) ⇒ Object
Re-runs each test case (or, for a parameterized test, each group of parameterized cases -- see
group_test_cases) individually to determine which one(s) crashed. - #setup ⇒ Object
Instance Method Details
#do_gdb(filename, executable, shell_result, test_cases, context:) ⇒ Object
Re-runs each test case (or, for a parameterized test, each group of parameterized
cases -- see group_test_cases) under gdb to identify which one(s) crashed and why.
Writes the full gdb transcript to a per-test-case log file and assembles a
terse crash label (signal + description, optional source line in backticks)
for each failing test case. Returns a modified shell_result with regenerated output.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 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 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 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 158 159 160 161 162 163 164 |
# File 'lib/ceedling/generators/generator_test_results_backtrace.rb', line 21 def do_gdb(filename, executable, shell_result, test_cases, context:) gdb_script_filepath = File.join( @configurator.project_build_tests_root, BACKTRACE_GDB_SCRIPT_FILE ) # Clean stats tracker test_case_results = @RESULTS_COLLECTOR.new( passed:0, failed:0, ignored:0, output:[] ) # Reset time shell_result[:time] = 0 test_name = File.basename( filename, '.*' ) # Iterate on test cases, one sub-process run per group (see `group_test_cases`) group_test_cases( test_cases ).each do |group| # Build the test fixture to run with our test case (or parameterized group) of interest command = @tool_executor.build_command_line( @configurator.tools_test_backtrace_gdb, [], gdb_script_filepath, executable, unity_filter_arg( group ) ) # Things are gonna go boom, so ignore booms to get output command[:options][:boom] = false crash_result = @tool_executor.exec( command ) # Sum execution time for each sub-process run # Note: Running tests separately increases total execution time shell_result[:time] += crash_result[:time].to_f() unresolved = [] # Attribute each group member its own real result line, if Unity printed one group.each do |test_case| case crash_result[:output] # Success test case when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:PASS\s*$)/ test_case_results[:passed] += 1 test_case_results[:output] << $1 # Ignored test case when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:IGNORE\s*$)/ test_case_results[:ignored] += 1 test_case_results[:output] << $1 when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:FAIL(:.+)?\s*$)/ test_case_results[:failed] += 1 test_case_results[:output] << $1 # No result line for this member -- either it crashed, or it never got to run # because an earlier member in this same group crashed. Resolved below. else unresolved << test_case end end next if unresolved.empty? # Prefer whichever unresolved member's own C symbol is actually named in the gdb # backtrace (works regardless of position in the group); fall back to the first # unresolved member if no member's symbol can be found in the transcript (e.g. a # brief crash report with no frame information at all). crashed_case = unresolved.find do |tc| crash_result[:output].match?( /#{Regexp.escape(tc[:symbol])}\s*\(\)\sat/ ) end crashed_case ||= unresolved.first # Per-test-case log file: <log_path>/<context>/<test_name>/<test_case>.gdb.log log_path = @file_path_utils.form_test_gdb_log( test_name, context: context, name: crashed_case[:test] ) @file_wrapper.mkdir( File.dirname( log_path ) ) @file_wrapper.write( log_path, "=== #{crashed_case[:test]} ===\n#{crash_result[:output]}\n", 'a' ) unresolved.each do |test_case| test_case_results[:failed] += 1 if !test_case.equal?( crashed_case ) # An earlier case in this same parameterized group already crashed the # process -- this member never got a chance to run. test_case_results[:output] << "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: " \ "Test case not run -- an earlier case in this parameterized test group crashed" next end # Collect file_name and line in which crash occurred. # Match against the actual C symbol (`:symbol`), not the human-facing test name # (`:test`): a parameterized test case crashes inside a generated wrapper function # (`runner_args<N>_<test>`), not a function literally named `<test>(<args>)`. matched = crash_result[:output].match( /#{Regexp.escape(test_case[:symbol])}\s*\(\)\sat.+#{filename}:(\d+)\n/ ) # If we found an error report line containing `test_case() at filename.c:###` in `gdb` output if matched # Line number line_number = matched[1] # Build terse signal label: "[SIGNAL] Description" signal_label = format_signal_label( crash_result[:output] ) # Extract the offending source line (nil for assertion crashes or when unavailable) source_line = extract_source_line( crash_result[:output], test_case[:symbol], filename ) # Unity's test executable output is line oriented. # Multi-line output is not possible (it looks like random `printf()` statements to the results parser). # "Encode" newlines in multiline string to be handled by the test results parser. crash_detail = source_line ? "#{NEWLINE_TOKEN}`#{source_line}`" : '' # Log path appears on its own encoded line so the results parser treats it separately test_case_results[:output] << "#{filename}:#{line_number}:#{test_case[:test]}:FAIL: Test case crashed" \ " >> #{signal_label}" \ "#{crash_detail}" \ "#{NEWLINE_TOKEN}(#{log_path})" # Try to extract a useful label even when no crash location frame was found. # A brief Windows assertion failure may report only the assertion text without frames. else label = format_signal_label( crash_result[:output] ) if !label.empty? test_case_results[:output] << "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test case crashed" \ " >> #{label}" \ "#{NEWLINE_TOKEN}(#{log_path})" else test_case_results[:output] << "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: " \ "Test case crashed (failed to extract `gdb` report)" \ "#{NEWLINE_TOKEN}(#{log_path})" end end end end # Reset shell result exit code and output shell_result[:exit_code] = test_case_results[:failed] shell_result[:output] = @generator_test_results.regenerate_test_executable_stdout( total: test_cases.size(), ignored: test_case_results[:ignored], failed: test_case_results[:failed], output: test_case_results[:output] ) return shell_result end |
#do_simple(filename, executable, shell_result, test_cases, context:) ⇒ Object
Re-runs each test case (or, for a parameterized test, each group of parameterized
cases -- see group_test_cases) individually to determine which one(s) crashed.
For crash cases, captures any extra output from the test binary (e.g.
assertion messages on stderr) and includes it in the failure report.
Returns a modified shell_result with regenerated output.
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/ceedling/generators/generator_test_results_backtrace.rb', line 171 def do_simple(filename, executable, shell_result, test_cases, context:) # Clean stats tracker test_case_results = @RESULTS_COLLECTOR.new( passed:0, failed:0, ignored:0, output:[] ) # Reset time shell_result[:time] = 0 # Iterate on test cases, one sub-process run per group (see `group_test_cases`) group_test_cases( test_cases ).each do |group| # Build the test fixture to run with our test case (or parameterized group) of interest command = @tool_executor.build_command_line( @configurator.tools_test_fixture_simple_backtrace, [], executable, unity_filter_arg( group ) ) # Things are gonna go boom, so ignore booms to get output command[:options][:boom] = false crash_result = @tool_executor.exec( command ) # Sum execution time for each sub-process run # Note: Running tests separately increases total execution time shell_result[:time] += crash_result[:time].to_f() crashed = false # Has the actual crash in this group already been attributed? # Attribute each group member its own real result line, if Unity printed one group.each do |test_case| case crash_result[:output] # Success test case when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:PASS\s*$)/ test_case_results[:passed] += 1 test_case_results[:output] << $1 # Ignored test case when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:IGNORE\s*$)/ test_case_results[:ignored] += 1 test_case_results[:output] << $1 when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:FAIL(:.+)?\s*$)/ test_case_results[:failed] += 1 test_case_results[:output] << $1 # No result line for this member -- either it crashed, or it never got to run # because an earlier member in this same group crashed. else test_case_results[:failed] += 1 if crashed test_case_results[:output] << "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: " \ "Test case not run -- an earlier case in this parameterized test group crashed" else crashed = true # Collect any non-result, non-blank lines (e.g. assertion messages on stderr) extra = extract_simple_crash_output( crash_result[:output], filename ) test_output = "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test case crashed" test_output += " >> #{extra.join(NEWLINE_TOKEN)}" unless extra.empty? test_case_results[:output] << test_output end end end end # Reset shell result exit code and output shell_result[:exit_code] = test_case_results[:failed] shell_result[:output] = @generator_test_results.regenerate_test_executable_stdout( total: test_cases.size(), ignored: test_case_results[:ignored], failed: test_case_results[:failed], output: test_case_results[:output] ) return shell_result end |
#setup ⇒ Object
12 13 14 |
# File 'lib/ceedling/generators/generator_test_results_backtrace.rb', line 12 def setup() @RESULTS_COLLECTOR = Struct.new( :passed, :failed, :ignored, :output, keyword_init:true ) end |