ceedling 1.1.2 β†’ 1.1.3

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: 70deeef4f895489e967caec64bb020696e41fa327c83785eed4fa553a1c7b8bb
4
- data.tar.gz: a3f48f487a174c6710601b693a44273542ca3934ae072d622994a19dd9d64884
3
+ metadata.gz: 8aabde4baeef95fad89e87cf3bd6226ffa3f10df9e54708270c82f237f3efe49
4
+ data.tar.gz: '08693e70f80deab8cb70f11677291f368a159df04b35f7a8d9fec8025522063a'
5
5
  SHA512:
6
- metadata.gz: 8dcd78f2672ac8afcf375314ae458c0a734b000b0edae3aee34614492072aa2e53c5751ffbc15703b90e82527b1f740800a52231bf7922749df881c00aa3ed81
7
- data.tar.gz: 29bb2ceace5a9f6d20fbf0d3546a708c1a07677aa558681c95ba3cf7c4991a487edf29e58226a084d169ee700a290c09695aed9f8a8d04923eb39aac9ba90449
6
+ metadata.gz: ab5f32d7896199850b29f6328c9e3fbaea63c0f5bf3dff665434ee0bd488992f971b18afd1bcf194e5c6f2ca66636cd72e38ab26fed3e74904cb374094f9c2b7
7
+ data.tar.gz: fab7cf5fe911d2dbc24bdb16add93551bfe8d9b7a668b888735ccfe199d04431707d5e01ec2d5ce345ee518db9b8f34376e16b92788ba1780e2ed20b8a32832c
data/GIT_COMMIT_SHA CHANGED
@@ -1 +1 @@
1
- 6f17bf8
1
+ 049a9d7
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Ceedling ![CI](https://github.com/ThrowTheSwitch/Ceedling/workflows/CI/badge.svg)
2
2
  ========
3
3
 
4
- **Ceedling 1.1.2** is the latest and greatest.
4
+ **Ceedling 1.1.3** is the latest and greatest.
5
5
 
6
6
  See [_Release Notes_][release-notes], [_Changelog_](docs/Changelog.md),
7
7
  [_Breaking Changes_][breaking-changes], and [_Known Issues_][known-issues].
@@ -0,0 +1,32 @@
1
+ /* =========================================================================
2
+ Ceedling - Test-Centered Build System for C
3
+ ThrowTheSwitch.org
4
+ Copyright (c) 2010-26 Mike Karlesky, Mark VanderVoord, & Greg Williams
5
+ SPDX-License-Identifier: MIT
6
+ ========================================================================= */
7
+
8
+ #include <signal.h>
9
+ #include "unity.h"
10
+ #include "example_file.h"
11
+
12
+
13
+ void setUp(void) {}
14
+ void tearDown(void) {}
15
+
16
+ void test_add_numbers_will_fail(void) {
17
+ // Platform-independent way of forcing a crash
18
+ // NOTE: Avoid `nullptr` as it is a keyword in C23
19
+ uint32_t* a_null_pointer = (void*)0;
20
+ uint32_t i = *a_null_pointer;
21
+ TEST_ASSERT_EQUAL_INT(2, add_numbers(i,2));
22
+ }
23
+
24
+ // Regression coverage for issue #1185: a parameterized test defined after a
25
+ // crashing test in the same file must still report its own real PASS/FAIL
26
+ // results rather than being swept up as "crashed" alongside the actual crash.
27
+ TEST_CASE(5, 3, 2)
28
+ TEST_CASE(10, 4, 6)
29
+ TEST_CASE(0, 0, 0)
30
+ void test_difference_between_numbers_is_correct(int a, int b, int expected) {
31
+ TEST_ASSERT_EQUAL_INT(expected, difference_between_numbers(a, b));
32
+ }
@@ -13,16 +13,17 @@ class ActionsWrapper
13
13
  include Thor::Base
14
14
  include Thor::Actions
15
15
 
16
- JUNK_FILE_EXCLUDE_REGEX =
16
+ JUNK_FILE_EXCLUDE_REGEX = /(\.DS_Store)|(thumbs\.db)/
17
17
 
18
18
  # Most important mixin method is Thor::Actions class method `source_root()` we call externally
19
19
 
20
20
  def _directory(src, *args)
21
- # Insert exclusion of macOS and Windows preview junk files if an exclude pattern is not present
22
- # Thor's use of args is an array of call arguments, some of which can be single key/value hash options
23
- if !args.any? {|h| h.class != Hash ? false : !h[:exclude_pattern].nil?}
24
- args << {:exclude_pattern => /(\.DS_Store)|(thumbs\.db)/}
25
- end
21
+ # Build a single trailing options hash -- Thor's directory() only reads
22
+ # the last argument as options, so any caller-supplied hash (e.g.
23
+ # :force) and this default :exclude_pattern must live in the same hash.
24
+ options = args.last.is_a?(Hash) ? args.pop : {}
25
+ options[:exclude_pattern] ||= JUNK_FILE_EXCLUDE_REGEX
26
+ args << options
26
27
 
27
28
  directory( src, *args )
28
29
  end
data/docs/Changelog.md CHANGED
@@ -10,6 +10,14 @@ This changelog is complemented by three other documents:
10
10
 
11
11
  ---
12
12
 
13
+ # [1.1.3] β€” 2026-08-06
14
+
15
+ ## πŸ’ͺ Fixed
16
+ - [#1185](https://github.com/ThrowTheSwitch/Ceedling/issues/1185) Fixed / improved crash handling for parameterized Unity test cases.
17
+ - [#1186](https://github.com/ThrowTheSwitch/Ceedling/issues/1186) Fixed `gcc` version lookup for the Gcov plugin running on Windows.
18
+ - Ensure compilation symbols needed for Partials are injected when `:preprocess` defines are configured.
19
+ - Fixed use of Thor’s Actions#directory to ensure a single options hash.
20
+
13
21
  # [1.1.2] β€” 2026-07-27
14
22
 
15
23
  ## πŸ’ͺ Fixed
@@ -88,10 +88,16 @@ class Configurator
88
88
  @loginator.log( "Reverted :cmock ↳ :treat_inlines to :exclude because this CMock feature is superseded by Partials.", Verbosity::COMPLAIN, LogLabels::NOTICE )
89
89
  end
90
90
 
91
- # If partials enabled, inject partials name prefix symbol to all test compilation.
91
+ # If partials enabled, inject partials name prefix symbol to all test compilation
92
+ # and, if a project defines its own preprocess-scoped defines matcher, to
93
+ # preprocessing as well -- `:preprocess:` defines are looked up independently
94
+ # of `:test:` defines and only fall back to the latter when `:preprocess:`
95
+ # is absent entirely, so a project-defined `:preprocess:` matcher needs this
96
+ # symbol appended directly or Partials' own macro-based #includes break.
92
97
  # Handle both the simple list and matcher hash config formats.
93
98
  _partials_prefix_symbol = "CEEDLING_PARTIALS_PREFIX=#{PARTIAL_FILENAME_PREFIX}"
94
99
  ConfigMatchinator.append_matcher_entries( config[:defines][:test], _partials_prefix_symbol )
100
+ ConfigMatchinator.append_matcher_entries( config[:defines][:preprocess], _partials_prefix_symbol )
95
101
  end
96
102
 
97
103
 
@@ -69,7 +69,10 @@ DEFAULT_TEST_FIXTURE_SIMPLE_BACKTRACE_TOOL = {
69
69
  :name => 'default_test_fixture_simple_backtrace'.freeze,
70
70
  :optional => false.freeze,
71
71
  :arguments => [
72
- '-n ${2}'.freeze # Exact test case name matching flag
72
+ # Test case matching flag and (quoted) value, fully composed by the caller as
73
+ # either `-n "<exact name>"` or `-f "<base name>"` -- see
74
+ # GeneratorTestResultsBacktrace#unity_filter_arg.
75
+ '${2}'.freeze
73
76
  ].freeze
74
77
  }
75
78
 
@@ -202,7 +205,10 @@ DEFAULT_TEST_BACKTRACE_GDB_TOOL = {
202
205
  "--command \"${1}\"".freeze, # Debug script file to run
203
206
  '--args'.freeze,
204
207
  '${2}'.freeze, # Test executable
205
- '-n ${3}'.freeze # Exact test case name matching flag
208
+ # Test case matching flag and (quoted) value, fully composed by the caller as
209
+ # either `-n "<exact name>"` or `-f "<base name>"` -- see
210
+ # GeneratorTestResultsBacktrace#unity_filter_arg.
211
+ '${3}'.freeze
206
212
  ].freeze
207
213
  }
208
214
 
@@ -13,7 +13,8 @@ class GeneratorTestResultsBacktrace
13
13
  @RESULTS_COLLECTOR = Struct.new( :passed, :failed, :ignored, :output, keyword_init:true )
14
14
  end
15
15
 
16
- # Re-runs each test case under gdb to identify which ones crashed and why.
16
+ # Re-runs each test case (or, for a parameterized test, each group of parameterized
17
+ # cases -- see `group_test_cases`) under gdb to identify which one(s) crashed and why.
17
18
  # Writes the full gdb transcript to a per-test-case log file and assembles a
18
19
  # terse crash label (signal + description, optional source line in backticks)
19
20
  # for each failing test case. Returns a modified shell_result with regenerated output.
@@ -28,54 +29,83 @@ class GeneratorTestResultsBacktrace
28
29
 
29
30
  test_name = File.basename( filename, '.*' )
30
31
 
31
- # Iterate on test cases
32
- test_cases.each do |test_case|
33
- # Per-test-case log file: <log_path>/<context>/<test_name>/<test_case>.gdb.log
34
- log_path = @file_path_utils.form_test_gdb_log( test_name, context: context, name: test_case[:test] )
35
- @file_wrapper.mkdir( File.dirname( log_path ) )
36
-
37
- # Build the test fixture to run with our test case of interest
32
+ # Iterate on test cases, one sub-process run per group (see `group_test_cases`)
33
+ group_test_cases( test_cases ).each do |group|
34
+ # Build the test fixture to run with our test case (or parameterized group) of interest
38
35
  command = @tool_executor.build_command_line(
39
36
  @configurator.tools_test_backtrace_gdb, [],
40
37
  gdb_script_filepath,
41
38
  executable,
42
- test_case[:test]
39
+ unity_filter_arg( group )
43
40
  )
44
41
  # Things are gonna go boom, so ignore booms to get output
45
42
  command[:options][:boom] = false
46
43
 
47
44
  crash_result = @tool_executor.exec( command )
48
45
 
49
- # Sum execution time for each test case
46
+ # Sum execution time for each sub-process run
50
47
  # Note: Running tests separately increases total execution time
51
48
  shell_result[:time] += crash_result[:time].to_f()
52
49
 
53
- test_output = ''
50
+ unresolved = []
54
51
 
55
- # Process single test case stats
56
- case crash_result[:output]
57
- # Success test case
58
- when /(^#{filename}.+:PASS\s*$)/
59
- test_case_results[:passed] += 1
60
- test_output = $1 # Grab regex match
52
+ # Attribute each group member its own real result line, if Unity printed one
53
+ group.each do |test_case|
54
+ case crash_result[:output]
55
+ # Success test case
56
+ when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:PASS\s*$)/
57
+ test_case_results[:passed] += 1
58
+ test_case_results[:output] << $1
61
59
 
62
- # Ignored test case
63
- when /(^#{filename}.+:IGNORE\s*$)/
64
- test_case_results[:ignored] += 1
65
- test_output = $1 # Grab regex match
60
+ # Ignored test case
61
+ when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:IGNORE\s*$)/
62
+ test_case_results[:ignored] += 1
63
+ test_case_results[:output] << $1
66
64
 
67
- when /(^#{filename}.+:FAIL(:.+)?\s*$)/
68
- test_case_results[:failed] += 1
69
- test_output = $1 # Grab regex match
65
+ when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:FAIL(:.+)?\s*$)/
66
+ test_case_results[:failed] += 1
67
+ test_case_results[:output] << $1
70
68
 
71
- else # Crash failure case
72
- test_case_results[:failed] += 1
69
+ # No result line for this member -- either it crashed, or it never got to run
70
+ # because an earlier member in this same group crashed. Resolved below.
71
+ else
72
+ unresolved << test_case
73
+ end
74
+ end
73
75
 
74
- # Append full gdb output for this test case to the log
75
- @file_wrapper.write( log_path, "=== #{test_case[:test]} ===\n#{crash_result[:output]}\n", 'a' )
76
+ next if unresolved.empty?
76
77
 
77
- # Collect file_name and line in which crash occurred
78
- matched = crash_result[:output].match( /#{test_case[:test]}\s*\(\)\sat.+#{filename}:(\d+)\n/ )
78
+ # Prefer whichever unresolved member's own C symbol is actually named in the gdb
79
+ # backtrace (works regardless of position in the group); fall back to the first
80
+ # unresolved member if no member's symbol can be found in the transcript (e.g. a
81
+ # brief crash report with no frame information at all).
82
+ crashed_case = unresolved.find do |tc|
83
+ crash_result[:output].match?( /#{Regexp.escape(tc[:symbol])}\s*\(\)\sat/ )
84
+ end
85
+ crashed_case ||= unresolved.first
86
+
87
+ # Per-test-case log file: <log_path>/<context>/<test_name>/<test_case>.gdb.log
88
+ log_path = @file_path_utils.form_test_gdb_log( test_name, context: context, name: crashed_case[:test] )
89
+ @file_wrapper.mkdir( File.dirname( log_path ) )
90
+ @file_wrapper.write( log_path, "=== #{crashed_case[:test]} ===\n#{crash_result[:output]}\n", 'a' )
91
+
92
+ unresolved.each do |test_case|
93
+ test_case_results[:failed] += 1
94
+
95
+ if !test_case.equal?( crashed_case )
96
+ # An earlier case in this same parameterized group already crashed the
97
+ # process -- this member never got a chance to run.
98
+ test_case_results[:output] <<
99
+ "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: " \
100
+ "Test case not run -- an earlier case in this parameterized test group crashed"
101
+ next
102
+ end
103
+
104
+ # Collect file_name and line in which crash occurred.
105
+ # Match against the actual C symbol (`:symbol`), not the human-facing test name
106
+ # (`:test`): a parameterized test case crashes inside a generated wrapper function
107
+ # (`runner_args<N>_<test>`), not a function literally named `<test>(<args>)`.
108
+ matched = crash_result[:output].match( /#{Regexp.escape(test_case[:symbol])}\s*\(\)\sat.+#{filename}:(\d+)\n/ )
79
109
 
80
110
  # If we found an error report line containing `test_case() at filename.c:###` in `gdb` output
81
111
  if matched
@@ -86,7 +116,7 @@ class GeneratorTestResultsBacktrace
86
116
  signal_label = format_signal_label( crash_result[:output] )
87
117
 
88
118
  # Extract the offending source line (nil for assertion crashes or when unavailable)
89
- source_line = extract_source_line( crash_result[:output], test_case[:test], filename )
119
+ source_line = extract_source_line( crash_result[:output], test_case[:symbol], filename )
90
120
 
91
121
  # Unity's test executable output is line oriented.
92
122
  # Multi-line output is not possible (it looks like random `printf()` statements to the results parser).
@@ -94,7 +124,7 @@ class GeneratorTestResultsBacktrace
94
124
  crash_detail = source_line ? "#{NEWLINE_TOKEN}`#{source_line}`" : ''
95
125
 
96
126
  # Log path appears on its own encoded line so the results parser treats it separately
97
- test_output =
127
+ test_case_results[:output] <<
98
128
  "#{filename}:#{line_number}:#{test_case[:test]}:FAIL: Test case crashed" \
99
129
  " >> #{signal_label}" \
100
130
  "#{crash_detail}" \
@@ -106,20 +136,18 @@ class GeneratorTestResultsBacktrace
106
136
  label = format_signal_label( crash_result[:output] )
107
137
 
108
138
  if !label.empty?
109
- test_output =
139
+ test_case_results[:output] <<
110
140
  "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test case crashed" \
111
141
  " >> #{label}" \
112
142
  "#{NEWLINE_TOKEN}(#{log_path})"
113
143
  else
114
- test_output =
144
+ test_case_results[:output] <<
115
145
  "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: " \
116
146
  "Test case crashed (failed to extract `gdb` report)" \
117
147
  "#{NEWLINE_TOKEN}(#{log_path})"
118
148
  end
119
149
  end
120
150
  end
121
-
122
- test_case_results[:output] << test_output
123
151
  end
124
152
 
125
153
  # Reset shell result exit code and output
@@ -135,7 +163,8 @@ class GeneratorTestResultsBacktrace
135
163
  return shell_result
136
164
  end
137
165
 
138
- # Re-runs each test case individually to determine which ones crashed.
166
+ # Re-runs each test case (or, for a parameterized test, each group of parameterized
167
+ # cases -- see `group_test_cases`) individually to determine which one(s) crashed.
139
168
  # For crash cases, captures any extra output from the test binary (e.g.
140
169
  # assertion messages on stderr) and includes it in the failure report.
141
170
  # Returns a modified shell_result with regenerated output.
@@ -146,50 +175,61 @@ class GeneratorTestResultsBacktrace
146
175
  # Reset time
147
176
  shell_result[:time] = 0
148
177
 
149
- # Iterate on test cases
150
- test_cases.each do |test_case|
151
- # Build the test fixture to run with our test case of interest
178
+ # Iterate on test cases, one sub-process run per group (see `group_test_cases`)
179
+ group_test_cases( test_cases ).each do |group|
180
+ # Build the test fixture to run with our test case (or parameterized group) of interest
152
181
  command = @tool_executor.build_command_line(
153
182
  @configurator.tools_test_fixture_simple_backtrace, [],
154
183
  executable,
155
- test_case[:test]
184
+ unity_filter_arg( group )
156
185
  )
157
186
  # Things are gonna go boom, so ignore booms to get output
158
187
  command[:options][:boom] = false
159
188
 
160
189
  crash_result = @tool_executor.exec( command )
161
190
 
162
- # Sum execution time for each test case
191
+ # Sum execution time for each sub-process run
163
192
  # Note: Running tests separately increases total execution time
164
193
  shell_result[:time] += crash_result[:time].to_f()
165
194
 
166
- # Process single test case stats
167
- case crash_result[:output]
168
- # Success test case
169
- when /(^#{filename}.+:PASS\s*$)/
170
- test_case_results[:passed] += 1
171
- test_output = $1 # Grab regex match
172
-
173
- # Ignored test case
174
- when /(^#{filename}.+:IGNORE\s*$)/
175
- test_case_results[:ignored] += 1
176
- test_output = $1 # Grab regex match
177
-
178
- when /(^#{filename}.+:FAIL(:.+)?\s*$)/
179
- test_case_results[:failed] += 1
180
- test_output = $1 # Grab regex match
181
-
182
- else # Crash failure case
183
- test_case_results[:failed] += 1
184
-
185
- # Collect any non-result, non-blank lines (e.g. assertion messages on stderr)
186
- extra = extract_simple_crash_output( crash_result[:output], filename )
187
- test_output = "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test case crashed"
188
- test_output += " >> #{extra.join(NEWLINE_TOKEN)}" unless extra.empty?
189
- end
195
+ crashed = false # Has the actual crash in this group already been attributed?
196
+
197
+ # Attribute each group member its own real result line, if Unity printed one
198
+ group.each do |test_case|
199
+ case crash_result[:output]
200
+ # Success test case
201
+ when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:PASS\s*$)/
202
+ test_case_results[:passed] += 1
203
+ test_case_results[:output] << $1
190
204
 
191
- # Collect up real and stand-in test results output
192
- test_case_results[:output] << test_output
205
+ # Ignored test case
206
+ when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:IGNORE\s*$)/
207
+ test_case_results[:ignored] += 1
208
+ test_case_results[:output] << $1
209
+
210
+ when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:FAIL(:.+)?\s*$)/
211
+ test_case_results[:failed] += 1
212
+ test_case_results[:output] << $1
213
+
214
+ # No result line for this member -- either it crashed, or it never got to run
215
+ # because an earlier member in this same group crashed.
216
+ else
217
+ test_case_results[:failed] += 1
218
+
219
+ if crashed
220
+ test_case_results[:output] <<
221
+ "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: " \
222
+ "Test case not run -- an earlier case in this parameterized test group crashed"
223
+ else
224
+ crashed = true
225
+ # Collect any non-result, non-blank lines (e.g. assertion messages on stderr)
226
+ extra = extract_simple_crash_output( crash_result[:output], filename )
227
+ test_output = "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test case crashed"
228
+ test_output += " >> #{extra.join(NEWLINE_TOKEN)}" unless extra.empty?
229
+ test_case_results[:output] << test_output
230
+ end
231
+ end
232
+ end
193
233
  end
194
234
 
195
235
  # Reset shell result exit code and output
@@ -208,6 +248,34 @@ class GeneratorTestResultsBacktrace
208
248
  ### Private ###
209
249
  private
210
250
 
251
+ # Groups test cases so each parameterized test's cases are isolated together as one
252
+ # sub-process run instead of one run per case. Unity's `-n`/`-f` command-line filter
253
+ # parser treats a comma as a separator between multiple OR'd filter clauses, so an
254
+ # exact filter can never match a parameterized case's runtime name once it has more
255
+ # than one argument (e.g. `name(5, 3, 2)`). Grouping by base name and matching each
256
+ # member's own result line out of the group's single shared sub-run output sidesteps
257
+ # that limitation entirely. Non-parameterized test cases are unaffected: each forms
258
+ # its own single-member group, isolated exactly as before.
259
+ def group_test_cases(test_cases)
260
+ test_cases.group_by { |test_case| test_case[:test].sub(/\(.*\)\z/, '') }.values
261
+ end
262
+
263
+ # Builds the Unity command-line filter argument for isolating one group (see
264
+ # `group_test_cases`). A non-parameterized (single-member, no-args) group is isolated
265
+ # with an exact-match filter as before. A parameterized group is isolated with a
266
+ # non-strict prefix filter on its shared base name -- the only reliable way to select
267
+ # such a group, since its members' runtime names contain commas.
268
+ def unity_filter_arg(group)
269
+ test_name = group.first[:test]
270
+
271
+ if test_name.include?('(')
272
+ base_name = test_name.sub(/\(.*\)\z/, '')
273
+ %(-f "#{base_name}")
274
+ else
275
+ %(-n "#{test_name}")
276
+ end
277
+ end
278
+
211
279
  # Builds a terse crash label from gdb output.
212
280
  # Rules:
213
281
  # - Named signal explicitly in output β†’ "[SIGNAL] Description"
@@ -62,8 +62,26 @@ class GeneratorTestRunner
62
62
  # Unity's runner generator `find_tests()` produces an array of hashes with the following keys...
63
63
  # { test:, args:, call:, params:, line_number: }
64
64
 
65
- # For external use of test case names and line numbers, keep only those pieces of info
66
- @test_cases = @test_cases_internal.map {|hash| hash.slice( :test, :line_number )}
65
+ # For external use, reduce down to test name, runtime C symbol, and line number.
66
+ # A parameterized test (`:args` populated) is expanded into one entry per TEST_CASE /
67
+ # TEST_RANGE / TEST_MATRIX row, since Unity's generated runner registers each invocation
68
+ # under its own name (matching `generate_test_runner.rb`'s `"#{test}(#{args})"` naming) and
69
+ # runs it through its own wrapper function (`runner_args<N>_<test>`). Collapsing these to a
70
+ # single bare-name entry breaks exact-match test case isolation (crash handling) for
71
+ # parameterized tests.
72
+ @test_cases = @test_cases_internal.flat_map do |hash|
73
+ if hash[:args].nil? || hash[:args].empty?
74
+ [ { test: hash[:test], symbol: hash[:test], line_number: hash[:line_number] } ]
75
+ else
76
+ hash[:args].each_with_index.map do |args, idx|
77
+ {
78
+ test: "#{hash[:test]}(#{args})",
79
+ symbol: "runner_args#{idx + 1}_#{hash[:test]}",
80
+ line_number: hash[:line_number]
81
+ }
82
+ end
83
+ end
84
+ end
67
85
  end
68
86
 
69
87
  def extract_test_cases(source_contents)
data/lib/version.rb CHANGED
@@ -15,7 +15,7 @@
15
15
  module Ceedling
16
16
  module Version
17
17
  # Convenience constants for gem building, etc.
18
- GEM = '1.1.2'
18
+ GEM = '1.1.3'
19
19
  TAG = GEM
20
20
 
21
21
  # If run as a script print Ceedling's version to $stdout
@@ -354,8 +354,8 @@ class Gcov < Plugin
354
354
 
355
355
  shell_result = @tool_executor.exec( command )
356
356
 
357
- # First line of gcc --version: "gcc (...platform info...) major.minor.patch"
358
- version_match = shell_result[:output].match(/^gcc\s+.*\s+(\d+)\.(\d+)\.\d+/)
357
+ # First line of gcc --version: "gcc[.exe] (...platform info...) major.minor.patch"
358
+ version_match = shell_result[:output].match(/^gcc(?:#{Regexp.escape(EXTENSION_WIN_EXE)})?\s+.*\s+(\d+)\.(\d+)\.\d+/)
359
359
 
360
360
  if version_match.nil? || version_match[1].nil? || version_match[2].nil?
361
361
  raise CeedlingException.new("Could not collect `gcc` version from its command line")
Binary file
@@ -279,6 +279,21 @@ class SystemContext
279
279
  sections << stderr.strip
280
280
  end
281
281
 
282
+ # Loginator routes `Verbosity::DEBUG` messages (including the backtrace
283
+ # `log_debug_backtrace` emits for a caught exception) to stdout rather than
284
+ # stderr, and this method's stdout scan above only pulls lines containing
285
+ # the literal substrings 'ERROR'/'EXCEPTION' -- a bare backtrace line
286
+ # (file:line:in `method') contains neither, so without surfacing it here
287
+ # explicitly, the one piece of stdout most useful for diagnosing *why* a
288
+ # command failed would otherwise only ever reach the full raw-output log
289
+ # file, not this console-visible summary (what CI output actually shows).
290
+ label = 'Debug Backtrace ==>'
291
+ backtrace_block = extract_section(stdout, label)
292
+ unless backtrace_block.empty?
293
+ sections << ">> DEBUG BACKTRACE"
294
+ sections.concat(backtrace_block)
295
+ end
296
+
282
297
  label = 'FAILED TEST SUMMARY'
283
298
  failed_block = extract_section(stdout, label)
284
299
  unless failed_block.empty?
@@ -91,6 +91,7 @@ ceedling_system_tests do
91
91
  test_case :crash_none_writes_fail_results_file
92
92
  test_case :crash_simple_sigsegv_all_test_cases
93
93
  test_case :crash_simple_sigabrt_assert_failure
94
+ test_case :crash_simple_sigsegv_with_parameterized_test
94
95
  end
95
96
 
96
97
  describe "Backtrace with GDB" do
@@ -100,6 +101,7 @@ ceedling_system_tests do
100
101
  test_case :crash_gdb_sigsegv_targets_test_case_filter
101
102
  test_case :crash_gdb_sigsegv_excludes_test_case_filter
102
103
  test_case :crash_gdb_sigabrt_assert_failure
104
+ test_case :crash_gdb_sigsegv_with_parameterized_test
103
105
  end
104
106
 
105
107
  describe "Test filtering" do
@@ -94,6 +94,7 @@ ceedling_system_tests do
94
94
  test_case :crash_none_writes_fail_results_file
95
95
  test_case :crash_simple_sigsegv_all_test_cases
96
96
  test_case :crash_simple_sigabrt_assert_failure
97
+ test_case :crash_simple_sigsegv_with_parameterized_test
97
98
  end
98
99
 
99
100
  describe "Backtrace with GDB" do
@@ -103,6 +104,7 @@ ceedling_system_tests do
103
104
  test_case :crash_gdb_sigsegv_targets_test_case_filter
104
105
  test_case :crash_gdb_sigsegv_excludes_test_case_filter
105
106
  test_case :crash_gdb_sigabrt_assert_failure
107
+ test_case :crash_gdb_sigsegv_with_parameterized_test
106
108
  end
107
109
 
108
110
  describe "Test filtering" do
@@ -892,6 +892,96 @@ module CommonSystemTestCases
892
892
  end
893
893
  end
894
894
 
895
+ # Regression test for issue #1185: a parameterized test (TEST_CASE) defined after a
896
+ # crashing test in the same file must still report its own real PASS results instead
897
+ # of being swept up as "crashed" alongside the actual crash. Uses :use_backtrace => :simple
898
+ # (Ceedling's default), which is the mode in which the bug was reported.
899
+ def crash_simple_sigsegv_with_parameterized_test
900
+ @c.with_context do
901
+ Dir.chdir @proj_name do
902
+ FileUtils.cp test_asset_path("example_file.h"), 'src/'
903
+ FileUtils.cp test_asset_path("example_file.c"), 'src/'
904
+ FileUtils.cp test_asset_path("test_example_file_crash_sigsegv_with_param.c"), 'test/'
905
+
906
+ @c.merge_project_yml_for_test({
907
+ :project => { :use_backtrace => :simple },
908
+ :unity => { :use_param_tests => true }
909
+ })
910
+
911
+ output = @c.ceedling_build_exec("test:all")
912
+ expect(@c.last_exit_status).to eq(1) # Test should fail because of crash
913
+ expect(output).to match(/Unit test failures/)
914
+ # 1 crashing test + 3 parameterized cases
915
+ expect(output).to match(/TESTED:\s+4/)
916
+ expect(output).to match(/PASSED:\s+3/)
917
+ expect(output).to match(/FAILED:\s+1/)
918
+ expect(output).to match(/IGNORED:\s+0/)
919
+
920
+ result_file = './build/test/results/test_example_file_crash_sigsegv_with_param.fail'
921
+ expect(File.exist?(result_file)).to be(true)
922
+ results = YamlWrapper.new.load(result_file)
923
+
924
+ # Only the truly crashing test is reported as a failure/crash
925
+ expect(results[:failures].map { |f| f[:test] }).to eq(['test_add_numbers_will_fail'])
926
+ expect(results[:failures].first[:message]).to match(/Test case crashed/)
927
+
928
+ # Each parameterized case reports its own real (passing) result, by name, not a crash
929
+ expect(results[:successes].map { |s| s[:test] }).to contain_exactly(
930
+ 'test_difference_between_numbers_is_correct(5, 3, 2)',
931
+ 'test_difference_between_numbers_is_correct(10, 4, 6)',
932
+ 'test_difference_between_numbers_is_correct(0, 0, 0)'
933
+ )
934
+ end
935
+ end
936
+ end
937
+
938
+ # Same regression coverage as `crash_simple_sigsegv_with_parameterized_test`, but for
939
+ # :use_backtrace => :gdb: confirms gdb-based isolation also correctly distinguishes the
940
+ # crashing test from the unrelated parameterized test, and still attributes the crash to
941
+ # the correct source line (exercising the gdb crash-frame symbol matching fix).
942
+ def crash_gdb_sigsegv_with_parameterized_test
943
+ @c.with_context do
944
+ Dir.chdir @proj_name do
945
+ FileUtils.cp test_asset_path("example_file.h"), 'src/'
946
+ FileUtils.cp test_asset_path("example_file.c"), 'src/'
947
+ FileUtils.cp test_asset_path("test_example_file_crash_sigsegv_with_param.c"), 'test/'
948
+
949
+ @c.merge_project_yml_for_test({
950
+ :project => { :use_backtrace => :gdb },
951
+ :unity => { :use_param_tests => true }
952
+ })
953
+
954
+ output = @c.ceedling_build_exec("test:all")
955
+ expect(@c.last_exit_status).to eq(1) # Test should fail because of crash
956
+ expect(output).to match(/Unit test failures/)
957
+ expect(output).to match(/TESTED:\s+4/)
958
+ expect(output).to match(/PASSED:\s+3/)
959
+ expect(output).to match(/FAILED:\s+1/)
960
+ expect(output).to match(/IGNORED:\s+0/)
961
+
962
+ result_file = './build/test/results/test_example_file_crash_sigsegv_with_param.fail'
963
+ expect(File.exist?(result_file)).to be(true)
964
+ results = YamlWrapper.new.load(result_file)
965
+
966
+ # Only the truly crashing test is reported as a failure/crash
967
+ expect(results[:failures].map { |f| f[:test] }).to eq(['test_add_numbers_will_fail'])
968
+ expect(results[:failures].first[:message]).to match(/Test case crashed/)
969
+ expect(results[:failures].first[:message]).to match(/SIGSEGV/i)
970
+
971
+ # Each parameterized case reports its own real (passing) result, by name, not a crash
972
+ expect(results[:successes].map { |s| s[:test] }).to contain_exactly(
973
+ 'test_difference_between_numbers_is_correct(5, 3, 2)',
974
+ 'test_difference_between_numbers_is_correct(10, 4, 6)',
975
+ 'test_difference_between_numbers_is_correct(0, 0, 0)'
976
+ )
977
+
978
+ log_path = './build/logs/test/test_example_file_crash_sigsegv_with_param/test_add_numbers_will_fail.gdb.log'
979
+ expect(File.exist?(log_path)).to be(true)
980
+ expect(File.read(log_path)).to match(/SIGSEGV|Segmentation fault/i)
981
+ end
982
+ end
983
+ end
984
+
895
985
  def project_with_test_file_directly_including_source_file
896
986
  @c.with_context do
897
987
  Dir.chdir @proj_name do
@@ -9,6 +9,7 @@ require 'spec_helper'
9
9
  require 'ceedling/config/configurator'
10
10
  require 'ceedling/ruby_expandinator'
11
11
  require 'ceedling/exceptions'
12
+ require 'ceedling/constants'
12
13
 
13
14
  describe Configurator do
14
15
 
@@ -184,4 +185,48 @@ describe Configurator do
184
185
 
185
186
  end
186
187
 
188
+ describe "#set_partials_derived_config" do
189
+
190
+ def partials_config
191
+ { project: { use_partials: true }, defines: { test: ['TEST'] }, cmock: {} }
192
+ end
193
+
194
+ it "does nothing when :use_partials is disabled" do
195
+ config = partials_config
196
+ config[:project][:use_partials] = false
197
+
198
+ @configurator.set_partials_derived_config( config )
199
+
200
+ expect( config[:defines][:test] ).to eq( ['TEST'] )
201
+ expect( config[:defines] ).to_not have_key( :preprocess )
202
+ end
203
+
204
+ it "appends the partials prefix symbol to a simple :test: defines list" do
205
+ config = partials_config
206
+
207
+ @configurator.set_partials_derived_config( config )
208
+
209
+ expect( config[:defines][:test] ).to include( "CEEDLING_PARTIALS_PREFIX=#{PARTIAL_FILENAME_PREFIX}" )
210
+ end
211
+
212
+ it "leaves :preprocess: defines untouched when the project does not define that section" do
213
+ config = partials_config
214
+
215
+ @configurator.set_partials_derived_config( config )
216
+
217
+ expect( config[:defines] ).to_not have_key( :preprocess )
218
+ end
219
+
220
+ it "appends the partials prefix symbol under the :* matcher when the project defines its own :preprocess: matcher hash" do
221
+ config = partials_config
222
+ config[:defines][:preprocess] = { 'TestSoilMoisture.c' => ['CEEDLING_DELTA_PROBE'] }
223
+
224
+ @configurator.set_partials_derived_config( config )
225
+
226
+ expect( config[:defines][:preprocess][:*] ).to include( "CEEDLING_PARTIALS_PREFIX=#{PARTIAL_FILENAME_PREFIX}" )
227
+ expect( config[:defines][:preprocess]['TestSoilMoisture.c'] ).to eq( ['CEEDLING_DELTA_PROBE'] )
228
+ end
229
+
230
+ end
231
+
187
232
  end
@@ -65,6 +65,19 @@ GDB_NO_SIGNAL_OUTPUT = <<~GDB.freeze
65
65
  Inferior 1 (process 12345) exited with code 0139.
66
66
  GDB
67
67
 
68
+ # Parameterized test crash (issue #1185): the crashing frame names the generated
69
+ # wrapper function (`runner_args1_test_value_out_of_range_good`), not the human-facing
70
+ # test name (`test_value_out_of_range_good(101, 1)`) that Unity reports at runtime.
71
+ GDB_SIGSEGV_PARAM_OUTPUT = <<~GDB.freeze
72
+ [Thread debugging using libthread_db enabled]
73
+
74
+ Program received signal SIGSEGV, Segmentation fault.
75
+ 0x00005618066ea1fb in runner_args1_test_value_out_of_range_good () at test/test_module_d.c:157
76
+ 157 int invalid = 1 / 0;
77
+ #0 0x00005618066ea1fb in runner_args1_test_value_out_of_range_good () at test/test_module_d.c:157
78
+ #1 0x00005618066eb4de in run_test (func=0x5618066ea1e7 <runner_args1_test_value_out_of_range_good>, name=0x5618066eb2e0 "test_value_out_of_range_good(101, 1)", line_num=157) at build/test/runners/test_module_d_runner.c:76
79
+ GDB
80
+
68
81
  # Windows SIGSEGV β€” Thread N prefix, space-padded source line, DLL frames
69
82
  # Single-quoted heredoc: backslashes in Windows paths are literal, not escape sequences
70
83
  GDB_WINDOWS_SIGSEGV_OUTPUT = <<~'GDB'.freeze
@@ -153,7 +166,7 @@ describe GeneratorTestResultsBacktrace do
153
166
  let(:filename) { 'test_lib.c' }
154
167
  let(:executable) { 'build/test/out/test_lib/test_lib.out' }
155
168
  let(:shell_result){ { exit_code: 1, output: '', time: 0.0 } }
156
- let(:test_cases) { [{ test: 'test_asserting', line_number: 8 }] }
169
+ let(:test_cases) { [{ test: 'test_asserting', symbol: 'test_asserting', line_number: 8 }] }
157
170
 
158
171
  before(:each) do
159
172
  allow(@configurator).to receive(:project_build_tests_root).and_return('build/test')
@@ -194,7 +207,7 @@ describe GeneratorTestResultsBacktrace do
194
207
  end
195
208
 
196
209
  it 'handles a SIGSEGV crash β€” writes log, includes signal label, backtick source line, and log path' do
197
- test_cases_sigsegv = [{ test: 'testCrash', line_number: 37 }]
210
+ test_cases_sigsegv = [{ test: 'testCrash', symbol: 'testCrash', line_number: 37 }]
198
211
  filename_sigsegv = 'TestUsartModel.c'
199
212
 
200
213
  allow(@tool_executor).to receive(:exec)
@@ -219,8 +232,39 @@ describe GeneratorTestResultsBacktrace do
219
232
  expect(crash_line).to include('(/build/logs/test/TestUsartModel/testCrash.gdb.log)')
220
233
  end
221
234
 
222
- it 'handles a SIGABRT crash from assert() β€” shows assertion text, no source line (issue #1038)' do
223
- test_cases_assert = [{ test: 'test_asserting', line_number: 8 }]
235
+ it 'attributes a crash in a parameterized test to its wrapper symbol, not its display name' do
236
+ test_cases_param = [{
237
+ test: 'test_value_out_of_range_good(101, 1)',
238
+ symbol: 'runner_args1_test_value_out_of_range_good',
239
+ line_number: 157
240
+ }]
241
+ filename_param = 'test_module_d.c'
242
+
243
+ allow(@tool_executor).to receive(:exec)
244
+ .and_return({ output: GDB_SIGSEGV_PARAM_OUTPUT, time: 0.5, exit_code: 139 })
245
+
246
+ expected_output_lines = []
247
+ allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
248
+ expected_output_lines = kwargs[:output]
249
+ 'regenerated'
250
+ end
251
+
252
+ expect(@file_wrapper).to receive(:write)
253
+ .with('/build/logs/test/test_module_d/test_value_out_of_range_good(101, 1).gdb.log', /=== test_value_out_of_range_good\(101, 1\) ===/, 'a')
254
+
255
+ @backtrace.do_gdb( filename_param, executable, shell_result, test_cases_param, context: :test )
256
+
257
+ crash_line = expected_output_lines.first
258
+ # The FAIL line reports the human-facing display name (with args)...
259
+ expect(crash_line).to start_with('test_module_d.c:157:test_value_out_of_range_good(101, 1):FAIL: Test case crashed')
260
+ # ...but the crash frame was still correctly located via the wrapper symbol.
261
+ expect(crash_line).to include('>> [SIGSEGV] Segmentation fault')
262
+ expect(crash_line).to include("#{NEWLINE_TOKEN}`int invalid = 1 / 0;`")
263
+ expect(crash_line).not_to include('failed to extract')
264
+ end
265
+
266
+ it 'handles a SIGABRT crash from assert() β€” shows assertion text, no source line' do
267
+ test_cases_assert = [{ test: 'test_asserting', symbol: 'test_asserting', line_number: 8 }]
224
268
 
225
269
  allow(@tool_executor).to receive(:exec)
226
270
  .and_return({ output: GDB_SIGABRT_ASSERT_OUTPUT, time: 0.3, exit_code: 134 })
@@ -460,7 +504,7 @@ describe GeneratorTestResultsBacktrace do
460
504
 
461
505
  describe '#do_gdb with brief Windows assert (no crash frame)' do
462
506
  let(:filename_assert) { 'test_example_file_crash_assert.c' }
463
- let(:test_cases_assert) { [{ test: 'test_add_numbers_triggers_assert', line_number: 20 }] }
507
+ let(:test_cases_assert) { [{ test: 'test_add_numbers_triggers_assert', symbol: 'test_add_numbers_triggers_assert', line_number: 20 }] }
464
508
  let(:executable) { 'build/test/out/test_example_file_crash_assert/test_example_file_crash_assert.out' }
465
509
  let(:shell_result) { { exit_code: 1, output: '', time: 0.0 } }
466
510
 
@@ -96,7 +96,7 @@ describe GeneratorTestRunner do
96
96
 
97
97
  runner = build_runner( test_file_contents: source )
98
98
 
99
- expect( runner.test_cases ).to eq( [ { test: 'test_ShouldDoSomething', line_number: 2 } ] )
99
+ expect( runner.test_cases ).to eq( [ { test: 'test_ShouldDoSomething', symbol: 'test_ShouldDoSomething', line_number: 2 } ] )
100
100
  end
101
101
 
102
102
  it 'remaps line numbers back to the original file when preprocessed content is given' do
@@ -114,7 +114,31 @@ describe GeneratorTestRunner do
114
114
 
115
115
  runner = build_runner( test_file_contents: original, preprocessed_file_contents: preprocessed )
116
116
 
117
- expect( runner.test_cases ).to eq( [ { test: 'test_ShouldDoSomething', line_number: 3 } ] )
117
+ expect( runner.test_cases ).to eq( [ { test: 'test_ShouldDoSomething', symbol: 'test_ShouldDoSomething', line_number: 3 } ] )
118
+ end
119
+
120
+ it 'expands a parameterized test into one entry per TEST_CASE, carrying the runtime name and wrapper symbol' do
121
+ source = <<~SOURCE
122
+ void setUp(void) {}
123
+ void tearDown(void) {}
124
+
125
+ TEST_CASE(101, 1)
126
+ TEST_CASE(-1, 1)
127
+ void test_value_out_of_range_good(int a, int expected) {}
128
+ SOURCE
129
+
130
+ runner = described_class.new(
131
+ config: { use_param_tests: true },
132
+ test_file_contents: source,
133
+ parsing_parcels: @parsing_parcels
134
+ )
135
+
136
+ expect( runner.test_cases ).to eq(
137
+ [
138
+ { test: 'test_value_out_of_range_good(101, 1)', symbol: 'runner_args1_test_value_out_of_range_good', line_number: 6 },
139
+ { test: 'test_value_out_of_range_good(-1, 1)', symbol: 'runner_args2_test_value_out_of_range_good', line_number: 6 }
140
+ ]
141
+ )
118
142
  end
119
143
  end
120
144
  end
@@ -391,8 +391,8 @@ describe TestContextExtractor do
391
391
  @extractor.collect_context( filepath, input, TestContextExtractor::Context::TEST_RUNNER_DETAILS )
392
392
 
393
393
  expected = [
394
- {:line_number => 2, :test => 'test_this_function'},
395
- {:line_number => 12, :test => 'test_another_function'},
394
+ {:line_number => 2, :test => 'test_this_function', :symbol => 'test_this_function'},
395
+ {:line_number => 12, :test => 'test_another_function', :symbol => 'test_another_function'},
396
396
  ]
397
397
 
398
398
  expect( @extractor.lookup_test_cases( filepath ) ).to eq expected
@@ -411,7 +411,7 @@ describe TestContextExtractor do
411
411
  @extractor.collect_context( filepath, input, TestContextExtractor::Context::TEST_RUNNER_DETAILS )
412
412
  }.not_to raise_error
413
413
 
414
- expected = [{ :line_number => 2, :test => 'test_this_function' }]
414
+ expected = [{ :line_number => 2, :test => 'test_this_function', :symbol => 'test_this_function' }]
415
415
  expect( @extractor.lookup_test_cases( filepath ) ).to eq expected
416
416
  end
417
417
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ceedling
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark VanderVoord
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2026-07-27 00:00:00.000000000 Z
13
+ date: 2026-08-06 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rake
@@ -184,6 +184,7 @@ files:
184
184
  - assets/test_example_file_boom.c
185
185
  - assets/test_example_file_crash_assert.c
186
186
  - assets/test_example_file_crash_sigsegv.c
187
+ - assets/test_example_file_crash_sigsegv_with_param.c
187
188
  - assets/test_example_file_source_include.c
188
189
  - assets/test_example_file_success.c
189
190
  - assets/test_example_file_unity_printf.c