Class: GeneratorTestResultsBacktrace

Inherits:
Object
  • Object
show all
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

Instance Method Details

#do_gdb(filename, executable, shell_result, test_cases, context:) ⇒ Object

Re-runs each test case under gdb to identify which ones 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.



20
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
# File 'lib/ceedling/generators/generator_test_results_backtrace.rb', line 20

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
  test_cases.each do |test_case|
    # 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: test_case[:test] )
    @file_wrapper.mkdir( File.dirname( log_path ) )

    # Build the test fixture to run with our test case of interest
    command = @tool_executor.build_command_line(
      @configurator.tools_test_backtrace_gdb, [],
      gdb_script_filepath,
      executable,
      test_case[:test]
    )
    # 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 test case
    # Note: Running tests separately increases total execution time
    shell_result[:time] += crash_result[:time].to_f()

    test_output = ''

    # Process single test case stats
    case crash_result[:output]
    # Success test case
    when /(^#{filename}.+:PASS\s*$)/
      test_case_results[:passed]  += 1
      test_output = $1 # Grab regex match

    # Ignored test case
    when /(^#{filename}.+:IGNORE\s*$)/
      test_case_results[:ignored] += 1
      test_output = $1 # Grab regex match

    when /(^#{filename}.+:FAIL(:.+)?\s*$)/
      test_case_results[:failed]  += 1
      test_output = $1 # Grab regex match

    else # Crash failure case
      test_case_results[:failed]  += 1

      # Append full gdb output for this test case to the log
      @file_wrapper.write( log_path, "=== #{test_case[:test]} ===\n#{crash_result[:output]}\n", 'a' )

      # Collect file_name and line in which crash occurred
      matched = crash_result[:output].match( /#{test_case[:test]}\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[:test], 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_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_output =
            "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test case crashed" \
            " >> #{label}" \
            "#{NEWLINE_TOKEN}(#{log_path})"
        else
          test_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

    test_case_results[:output] << test_output
  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 individually to determine which ones 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.



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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
# File 'lib/ceedling/generators/generator_test_results_backtrace.rb', line 142

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
  test_cases.each do |test_case|
    # Build the test fixture to run with our test case of interest
    command = @tool_executor.build_command_line(
      @configurator.tools_test_fixture_simple_backtrace, [],
      executable,
      test_case[:test]
    )
    # 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 test case
    # Note: Running tests separately increases total execution time
    shell_result[:time] += crash_result[:time].to_f()

    # Process single test case stats
    case crash_result[:output]
    # Success test case
    when /(^#{filename}.+:PASS\s*$)/
      test_case_results[:passed]  += 1
      test_output = $1 # Grab regex match

    # Ignored test case
    when /(^#{filename}.+:IGNORE\s*$)/
      test_case_results[:ignored] += 1
      test_output = $1 # Grab regex match

    when /(^#{filename}.+:FAIL(:.+)?\s*$)/
      test_case_results[:failed]  += 1
      test_output = $1 # Grab regex match

    else # Crash failure case
      test_case_results[:failed]  += 1

      # 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?
    end

    # Collect up real and stand-in test results output
    test_case_results[:output] << test_output
  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

#setupObject



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