ceedling 1.1.7 → 1.1.9

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/GIT_COMMIT_SHA +1 -1
  3. data/README.md +1 -1
  4. data/docs/Changelog.md +48 -0
  5. data/docs/KnownIssues.md +3 -0
  6. data/docs/mkdocs/plugins/gcov/gcovr.md +9 -4
  7. data/docs/mkdocs/reference/gcov-plugin.md +9 -4
  8. data/lib/ceedling/c_extractor/c_extractor_declarations.rb +11 -10
  9. data/lib/ceedling/c_extractor/c_extractor_preprocessing.rb +67 -2
  10. data/lib/ceedling/file_path_utils.rb +4 -0
  11. data/lib/ceedling/generators/generator_test_results_backtrace.rb +5 -3
  12. data/lib/ceedling/generators/generator_test_runner.rb +5 -1
  13. data/lib/ceedling/includes/includes.rb +8 -1
  14. data/lib/ceedling/partials/partializer.rb +69 -24
  15. data/lib/ceedling/preprocess/preprocessinator_line_marker_includes_extractor.rb +6 -1
  16. data/lib/ceedling/preprocess/preprocessinator_reconstructor.rb +18 -5
  17. data/lib/version.rb +1 -1
  18. data/plugins/gcov/lib/console_reportinator.rb +7 -0
  19. data/plugins/gcov/lib/gcov.rb +29 -10
  20. data/plugins/gcov/lib/gcovr_reportinator.rb +62 -48
  21. data/plugins/gcov/lib/reportgenerator_reportinator.rb +10 -6
  22. data/site-local/plugins/gcov/gcovr.html +9 -4
  23. data/site-local/reference/gcov-plugin.html +9 -4
  24. data/site-local/sitemap.xml.gz +0 -0
  25. data/spec/system/gcov_deployment_spec.rb +1 -0
  26. data/spec/system/support/gcov_common_test_cases.rb +18 -0
  27. data/spec/units/c_extractor/c_extractor_declarations_spec.rb +16 -0
  28. data/spec/units/c_extractor/c_extractor_integration_spec.rb +71 -0
  29. data/spec/units/c_extractor/c_extractor_preprocessing_spec.rb +122 -0
  30. data/spec/units/generators/generator_partials_spec.rb +98 -0
  31. data/spec/units/generators/generator_test_results_backtrace_spec.rb +26 -0
  32. data/spec/units/generators/generator_test_runner_spec.rb +23 -0
  33. data/spec/units/includes/includes_spec.rb +21 -3
  34. data/spec/units/partials/partializer_spec.rb +62 -0
  35. data/spec/units/preprocess/preprocessinator_line_marker_includes_extractor_spec.rb +64 -0
  36. data/spec/units/preprocess/preprocessinator_reconstructor_spec.rb +74 -0
  37. data/vendor/unity/auto/generate_test_runner.rb +3 -1
  38. data/vendor/unity/auto/unity_test_summary.rb +2 -2
  39. data/vendor/unity/docs/UnityChangeLog.md +7 -0
  40. data/vendor/unity/docs/UnityConfigurationGuide.md +4 -0
  41. data/vendor/unity/src/unity_internals.h +21 -7
  42. data/vendor/unity/test/Makefile +9 -2
  43. data/vendor/unity/test/rakefile_helper.rb +1 -0
  44. data/vendor/unity/test/tests/test_generate_test_runner.rb +49 -0
  45. data/vendor/unity/test/tests/test_unity_test_summary.rb +58 -0
  46. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6379bc5002c0b3bbd2f670bffcbaf91afba5c0b5a8ea5438b626f758d8144f8d
4
- data.tar.gz: 9b1dec28ef0fb39d47b41fb868522a1398f0b055f74e877a4afc64009fdb7ac3
3
+ metadata.gz: 28f18f89f99b7e81f68293c994fa85ff25532ca62bd250c2d1c7e351ec3ec7a0
4
+ data.tar.gz: 1252cbea2a5752315c65826f8068313450cf08d0ea63259e1e8a5d422588140c
5
5
  SHA512:
6
- metadata.gz: d8db68c457e06b77007d1e954c4512a4c7e84abd13918546c31b5e05c4021e01e7ebafb5d3e85e0268d1742716e227d7860cdcc763f41f683c5d119f4841a998
7
- data.tar.gz: 32ae8949567ae23bf7afff0deea71e66e06ea5dd6c56fcdde1dd561c5404020550ca9a65b11840f19ffde3a702fed80e73c2de03af0b8a1341490605a9b6b266
6
+ metadata.gz: 7aeb58a943b8df79fa31e12694a16019fba66cf798cc899eccb4d1e7b64d0b5fe09e5965f376791381cbc8c0e47a65c0563dbc5a00e1523714e813b8491a9896
7
+ data.tar.gz: e6cf0643c68cd8fa2c96b99de3e2e95ca276b27bf5ecb5cfa41ae9f1371ed55f3675ec5491d585b989629f2f89b0f16fc65f0586daa6ade065fbe903d9208087
data/GIT_COMMIT_SHA CHANGED
@@ -1 +1 @@
1
- b1534f9
1
+ 2209dc2
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.7** is the latest and greatest.
4
+ **Ceedling 1.1.9** 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].
data/docs/Changelog.md CHANGED
@@ -10,6 +10,54 @@ This changelog is complemented by three other documents:
10
10
 
11
11
  ---
12
12
 
13
+ # [1.1.9] — 2026-09-20
14
+
15
+ ## 💪 Fixed
16
+
17
+ ### Partials
18
+
19
+ - [#1293](https://github.com/ThrowTheSwitch/Ceedling/issues/1293) Fixed a Partials-generated header occasionally redefining a type already declared by the real header it replaces, causing a `typedef redefinition with different types` compilation error. The triggering condition involved a source file whose real header was reachable both directly and through a macro-included intermediate file allowed by incomplete Partials include directive ordering.
20
+
21
+ ### Preprocessing
22
+
23
+ - Fixed whitespace occasionally inserted by the underlying compiler's preprocessor around a `#` or `##` operator (e.g. `x ##y` becoming `x ## y`) being carried through verbatim into Partials-generated and reconstructed macro definitions. Extracted macro text is now normalized so preprocessing stringize and token-paste operators always sit directly against their operands, regardless of what a given toolchain's preprocessor happens to emit.
24
+
25
+ ## ⚠️ Changed
26
+
27
+ - Updated Unity to latest version.
28
+
29
+ ---
30
+
31
+ # [1.1.8] — 2026-09-10
32
+
33
+ ## 💪 Fixed
34
+
35
+ - Fixed a crashed test case in a parameterized group occasionally being misattributed to a different, shorter-named test case in the same group whose name happened to be a substring of the one actually named in the crash backtrace.
36
+ - Fixed a test case's line number occasionally being misattributed to an unrelated helper function whose name merely contained the test's name as a substring, cascading incorrect line numbers to every test case after it in the same file.
37
+
38
+ ### Partials
39
+
40
+ - [#1262](https://github.com/ThrowTheSwitch/Ceedling/issues/1262) Fixed a Partials-generated header occasionally concatenating two adjacent `#define` lines in one line causing a stray '#' compilation error. The triggering condition involved a `//` or `/* */` comment containing an apostrophe or quote (e.g. `// Don't ...`) that disrupted string literal handling.
41
+
42
+ ### Preprocessing
43
+
44
+ - [#1266](https://github.com/ThrowTheSwitch/Ceedling/issues/1266) Fixed a reconstructed header silently dropping any macro whose name or value merely contained the header's own include guard as a substring (e.g. guard `RTC_H` matching inside `RTC_HOUR_SECONDS`), causing undeclared-identifier compile errors.
45
+ - [#1268](https://github.com/ThrowTheSwitch/Ceedling/issues/1268) Fixed a mockable header's `#include` losing the enums, structs, and function prototypes of a conditionally-included file when that `#include` was indented inside its `#ifdef` block, causing undeclared-identifier compile errors.
46
+
47
+ ### Gcov plugin
48
+
49
+ - Fixed `:gcov ↳ :gcovr ↳ :decisions` reporting an incorrect minimum-version requirement; the version check (and docs) now correctly require `gcovr` 5.1 or higher.
50
+ - Fixed `:gcov ↳ :gcovr ↳ :fail_under_line`/`:fail_under_branch`/`:fail_under_decision`/`:fail_under_function` silently accepting `0`, contradicting the documented 1–100 range; `0` is now rejected.
51
+ - Fixed `:gcov ↳ :gcovr ↳ :object_directory` and `:source_encoding` values containing a space breaking the constructed `gcovr` command line; both are now quoted consistently with every other string-valued option.
52
+ - Fixed a regex-metacharacter-injection risk (e.g. a literal `.`) in `:gcov ↳ :gcovr`'s and `:gcov ↳ :report_generator`'s auto-generated exclusion patterns in `:test_file_prefix`, `:mock_prefix`, or the build root path could silently over- or under-match files.
53
+ - Fixed `:gcov ↳ :mcdc` triggering a `gcc --version` check (and a potential build-breaking exception for an older compiler) on every build in which a project enables the Gcov plugin, not only `gcov:` builds.
54
+ - Fixed only the first of multiple simultaneously violated `:gcov ↳ :gcovr ↳ :fail_under_*` thresholds being reported when a build-breaking exception is raised; every violated threshold is now listed.
55
+ - Fixed a latent mutation risk in `:gcov ↳ :report_generator` option handling that could accumulate stale exclusion patterns across repeated report-generation calls within a single run.
56
+ - `:gcov ↳ :gcovr ↳ :branches`/`:sort_uncovered`/`:sort_percentage` now use `gcovr`'s renamed `--txt-metric branch`/`--sort uncovered-number`/`--sort uncovered-percent` flags on `gcovr` 7.0+, silencing `gcovr`'s deprecation warnings for those options. The pre-7.0 flag names are still used automatically with older `gcovr`.
57
+ - Fixed the Gcov console summary silently producing no report and no log line at all for a Partial-implementation source whose `gcov` output couldn't be matched. It now logs the same "Found no coverage results" message the equivalent non-Partial case already did.
58
+
59
+ ---
60
+
13
61
  # [1.1.7] — 2026-09-03
14
62
 
15
63
  ## 💪 Fixed
data/docs/KnownIssues.md CHANGED
@@ -20,6 +20,9 @@ Known issues are complemented by three other documents:
20
20
  1. While header file search paths are now customizable per executable, this currently only applies to the search paths the compiler uses. Distinguishing test files or header files of the same name in different directories for test runner and mock generation respectively continues to rely on educated guesses in Ceedling code.
21
21
  1. All header files needed for test compilation must be within the `:includes` path collection. Relative paths in include directives that extend outside the path collection will cause build problems.
22
22
  1. Any path for a C file specified with `TEST_SOURCE_FILE(...)` is in relation to **_project root_** — that is, from where you execute `ceedling` at the command line. If you move source files or change your directory structure, many of your `TEST_SOURCE_FILE(...)` calls may need to be updated. A more flexible and dynamic approach to path handling will come in a future update.
23
+ 1. In certain combinations of conditional preprocessing blocks with dependent symbols defined in another file, Ceedling can silently fail to extract computed includes (e.g. `#include SOME_MACRO()`).
24
+ 1. User includes (`#include "path/user.h"`) from source C files lose their relative path when Partial and mock header files are generated from source. Compilation failures can result from certain uncommon cases involving headers of the same names in different directories and a source file including both such headers.
25
+ 1. The automatic vendor copying of Unity, CMock, and CException into a project's build directory can intermittently fail with a file/directory type error, most often under antivirus/EDR file locking, cloud-sync filter drivers, or two concurrent Ceedling invocations racing the same destination. Deleting `build/` (or reinstalling the gem) and rebuilding works around it.
23
26
  1. The Bullseye code coverage plugin has been temporarily disabled as of 1.0.0. The makers of Bullseye have generously provided a license for development, and the plugin will be available in the next minor release.
24
27
 
25
28
  ---
@@ -128,7 +128,7 @@ Report the decision coverage. For HTML, JSON, and the summary report.
128
128
 
129
129
  Implied automatically whenever `:fail_under_decision` is set.
130
130
 
131
- **Requires:** `gcovr` 6.0 or higher
131
+ **Requires:** `gcovr` 5.1 or higher
132
132
 
133
133
  ---
134
134
 
@@ -163,21 +163,26 @@ Select the source file character encoding. (`gcovr --source-encoding`)
163
163
  ### `:branches`
164
164
 
165
165
  Report the branch coverage instead of the line coverage. Applies to the text
166
- report only. (`gcovr --branches`)
166
+ report only. Uses `gcovr --txt-metric branch` on `gcovr` 7.0+ (`--branches` is
167
+ deprecated as of 7.0, though still functional); `gcovr --branches` below it.
167
168
 
168
169
  ---
169
170
 
170
171
  ### `:sort_uncovered`
171
172
 
172
173
  Sort entries by increasing number of uncovered lines. Applies to text and HTML
173
- reports. (`gcovr --sort-uncovered`)
174
+ reports. Uses `gcovr --sort uncovered-number` on `gcovr` 7.0+
175
+ (`--sort-uncovered` is deprecated as of 7.0, though still functional);
176
+ `gcovr --sort-uncovered` below it.
174
177
 
175
178
  ---
176
179
 
177
180
  ### `:sort_percentage`
178
181
 
179
182
  Sort entries by increasing percentage of uncovered lines. Applies to text and
180
- HTML reports. (`gcovr --sort-percentage`)
183
+ HTML reports. Uses `gcovr --sort uncovered-percent` on `gcovr` 7.0+
184
+ (`--sort-percentage` is deprecated as of 7.0, though still functional);
185
+ `gcovr --sort-percentage` below it.
181
186
 
182
187
  ---
183
188
 
@@ -229,7 +229,7 @@ Report the decision coverage. For HTML, JSON, and the summary report.
229
229
 
230
230
  Implied automatically whenever `:fail_under_decision` is set.
231
231
 
232
- **Requires:** `gcovr` 6.0 or higher
232
+ **Requires:** `gcovr` 5.1 or higher
233
233
 
234
234
  ---
235
235
 
@@ -262,21 +262,26 @@ Source file character encoding. (`gcovr --source-encoding`)
262
262
  ### `:branches`
263
263
 
264
264
  Report branch coverage instead of line coverage. Applies to text reports only.
265
- (`gcovr --branches`)
265
+ Uses `gcovr --txt-metric branch` on `gcovr` 7.0+ (`--branches` is deprecated as
266
+ of 7.0, though still functional); `gcovr --branches` below it.
266
267
 
267
268
  ---
268
269
 
269
270
  ### `:sort_uncovered`
270
271
 
271
272
  Sort report entries by increasing number of uncovered lines. Applies to text
272
- and HTML reports. (`gcovr --sort-uncovered`)
273
+ and HTML reports. Uses `gcovr --sort uncovered-number` on `gcovr` 7.0+
274
+ (`--sort-uncovered` is deprecated as of 7.0, though still functional);
275
+ `gcovr --sort-uncovered` below it.
273
276
 
274
277
  ---
275
278
 
276
279
  ### `:sort_percentage`
277
280
 
278
281
  Sort report entries by increasing percentage of uncovered lines. Applies to
279
- text and HTML reports. (`gcovr --sort-percentage`)
282
+ text and HTML reports. Uses `gcovr --sort uncovered-percent` on `gcovr` 7.0+
283
+ (`--sort-percentage` is deprecated as of 7.0, though still functional);
284
+ `gcovr --sort-percentage` below it.
280
285
 
281
286
  ---
282
287
 
@@ -402,16 +402,17 @@ class CExtractorDeclarations
402
402
  return $1.strip
403
403
  end
404
404
 
405
- # Find the name in the text and take everything before it (including pointer specifiers)
406
- idx = text.rindex(name)
407
- return nil if idx.nil?
408
-
409
- type_part = text[0...idx]
410
- # Include any pointer specifiers attached to the name
411
- if text[idx..] =~ /^#{Regexp.escape(name)}/
412
- # Look for * immediately before name (with optional space)
413
- type_part = type_part.rstrip
414
- end
405
+ # Find the name in the text and take everything before it (including pointer specifiers).
406
+ # A plain rindex(name) would find the last substring occurrence of `name` anywhere in
407
+ # text, even inside an unrelated longer identifier that merely ends with it (e.g. `name`
408
+ # "count" inside a type-side identifier like "recount") -- \b anchors this to a whole
409
+ # identifier occurrence. Greedy `.*` in a single match (not scan) naturally finds the
410
+ # rightmost such occurrence, since it consumes as much as possible before backtracking
411
+ # just enough to let the trailing \bname\b still match.
412
+ match = text.match(/\A(.*)\b#{Regexp.escape(name)}\b/m)
413
+ return nil if match.nil?
414
+
415
+ type_part = match[1]
415
416
  type_part.strip
416
417
  end
417
418
 
@@ -241,9 +241,74 @@ class CExtractorPreprocessing
241
241
  text = scanner.scan(/#/)
242
242
 
243
243
  until scanner.eos?
244
- text << (scanner.scan(/[^"'\\\n]*/) || '')
244
+ # '/' is excluded here (alongside the quote/backslash/newline characters
245
+ # already excluded) so a comment marker is never swallowed into this plain
246
+ # literal run -- it has to be seen and dispatched on its own below, before
247
+ # any '"' or "'" inside the comment's own text gets mistaken for the start
248
+ # of a string/char literal (see the // and /* branches for why that matters).
249
+ # '#' is excluded the same way, so a #/## operator is seen and dispatched on
250
+ # its own below rather than absorbed into this run along with whatever
251
+ # whitespace happens to surround it (see the ## and # branches for why).
252
+ text << (scanner.scan(/[^"'\\\n\/#]*/) || '')
253
+
254
+ if scanner.scan(%r{//})
255
+ # A trailing line comment (e.g. "// don't change this") can contain an
256
+ # apostrophe or quote that isn't a literal delimiter at all. Consumed the
257
+ # ordinary way, that stray quote would send skip_c_string hunting for a
258
+ # closing match, straight through this directive's own newline and into
259
+ # whatever follows -- silently merging the next #define into this one.
260
+ # Comment content is captured verbatim and never scanned for literals.
261
+ #
262
+ # A '\' immediately before the physical newline still splices lines here,
263
+ # same as everywhere else in a directive (line splicing is a translation
264
+ # phase that happens before comments are stripped), so the comment -- and
265
+ # the directive -- keeps going onto the next physical line rather than
266
+ # ending at that newline.
267
+ before = scanner.pos - 2
268
+ loop do
269
+ scanner.scan(/[^\\\n]*/)
270
+ break if scanner.eos?
271
+ break unless scanner.scan(/\\\n/) || scanner.scan(/\\/)
272
+ end
273
+ text << scanner.string[before...scanner.pos]
274
+
275
+ elsif scanner.scan(%r{/\*})
276
+ # A block comment can legitimately span physical lines without ending the
277
+ # directive (only an un-commented newline does that) -- its content, quotes
278
+ # included, is likewise never scanned for literals, only for its own close.
279
+ before = scanner.pos - 2
280
+ scanner.skip_until(%r{\*/}) || scanner.terminate
281
+ text << scanner.string[before...scanner.pos]
282
+
283
+ elsif scanner.scan(%r{/})
284
+ text << '/'
285
+
286
+ elsif scanner.scan(/##/)
287
+ # The C standard specifies ##'s token-paste semantics but not the exact
288
+ # whitespace a preprocessor emits when reconstructing macro text -- real
289
+ # toolchains can and do disagree here, and Ceedling regenerates this text
290
+ # into partials rather than promising byte-for-byte reproduction of that
291
+ # spacing. Discard whatever immediately follows the operator in the source,
292
+ # so its right-hand operand always glues on directly. The left side only
293
+ # trims when a word character (the operand actually being pasted) is
294
+ # immediately adjacent -- a lookbehind, not a blind rstrip -- so unrelated
295
+ # punctuation spacing that happens to precede the operator (e.g. the space
296
+ # after a macro argument's comma, or after the parameter list's closing
297
+ # paren when the replacement list itself starts with the operator) is left
298
+ # alone; that space was never the preprocessor-reconstruction artifact this
299
+ # is defending against. ## checked before bare '#' below so a real paste
300
+ # operator is never split into two stringize matches.
301
+ text.sub!(/(?<=\w)[ \t]+\z/, '')
302
+ scanner.scan(/[ \t]*/)
303
+ text << '##'
304
+
305
+ elsif scanner.scan(/#/)
306
+ # Same rationale as ## above, for the stringize operator.
307
+ text.sub!(/(?<=\w)[ \t]+\z/, '')
308
+ scanner.scan(/[ \t]*/)
309
+ text << '#'
245
310
 
246
- if (ch = scanner.peek(1)) == '"' || ch == "'"
311
+ elsif (ch = scanner.peek(1)) == '"' || ch == "'"
247
312
  before = scanner.pos
248
313
  @c_extractor_code_text.skip_c_string(scanner, ch)
249
314
  text << scanner.string[before...scanner.pos]
@@ -124,6 +124,10 @@ class FilePathUtils
124
124
  pairs = paths.map { |p| [p, p.gsub('\\', '/').chomp('/')] }
125
125
 
126
126
  # Sort shallowest-first so ancestors are always encountered before their descendants.
127
+ # No tiebreaker for same-depth entries needed: Ruby's sort_by doesn't guarantee a
128
+ # stable order among ties, but two same-depth paths can never be each other's
129
+ # ancestor, so the ancestor-exclusion check below is correct regardless of which
130
+ # order same-depth ties come out in.
127
131
  pairs.sort_by! { |_, normalized| normalized.count('/') }
128
132
 
129
133
  kept = []
@@ -95,9 +95,11 @@ class GeneratorTestResultsBacktrace
95
95
  # Prefer whichever unresolved member's own C symbol is actually named in the gdb
96
96
  # backtrace (works regardless of position in the group); fall back to the first
97
97
  # unresolved member if no member's symbol can be found in the transcript (e.g. a
98
- # brief crash report with no frame information at all).
98
+ # brief crash report with no frame information at all). \b anchors the symbol so a
99
+ # shorter unresolved symbol that's merely a suffix of a longer one actually named in
100
+ # the frame (e.g. "foo" vs "my_foo") can't steal the attribution.
99
101
  crashed_case = unresolved.find do |tc|
100
- crash_result[:output].match?( /#{Regexp.escape(tc[:symbol])}\s*\(\)\sat/ )
102
+ crash_result[:output].match?( /\b#{Regexp.escape(tc[:symbol])}\s*\(\)\sat/ )
101
103
  end
102
104
  crashed_case ||= unresolved.first
103
105
 
@@ -458,7 +460,7 @@ class GeneratorTestResultsBacktrace
458
460
  output.lines.filter_map do |line|
459
461
  line = line.strip
460
462
  next if line.empty?
461
- next if line =~ /^#{filename}.+:(PASS|FAIL|IGNORE)/
463
+ next if line =~ /^#{Regexp.escape(filename)}.+:(PASS|FAIL|IGNORE)/
462
464
  line
463
465
  end
464
466
  end
@@ -129,7 +129,11 @@ class GeneratorTestRunner
129
129
  break if remaining.empty?
130
130
 
131
131
  next_case = remaining.first
132
- if (line =~ /#{next_case[:test]}/)
132
+ # Escaped (a raw test name can contain regex metacharacters) and \b-anchored on both
133
+ # sides so an unrelated line that merely contains this test's name as a substring --
134
+ # e.g. a helper function like `reset_test_ab_state()` sitting before `test_ab` itself
135
+ # -- can't steal its line number and cascade misalignment to every case after it.
136
+ if (line =~ /\b#{Regexp.escape(next_case[:test])}\b/)
133
137
  next_case[:line_number] = line_num
134
138
  remaining.shift
135
139
  end
@@ -254,8 +254,15 @@ class Includes
254
254
  return _includes
255
255
  end
256
256
 
257
+ # Ruby's sort/sort_by family does not guarantee a stable sort -- the relative order
258
+ # of elements a comparator treats as equal (every UserInclude here, all mapping to
259
+ # the same "1") is free to come out differently across Ruby versions and platforms.
260
+ # partition sidesteps the question entirely: it's a simple filter, not a comparator
261
+ # sort, so each group's own original relative order is preserved by definition, on
262
+ # every Ruby version and platform, with nothing to be unstable about.
257
263
  def self.sort!(includes)
258
- includes.sort_by! { |include| include.is_a?(SystemInclude) ? 0 : 1 }
264
+ system, other = includes.partition { |include| include.is_a?(SystemInclude) }
265
+ includes.replace(system + other)
259
266
  return includes
260
267
  end
261
268
  end
@@ -86,20 +86,17 @@ class Partializer
86
86
  def remap_implementation_header_includes(name:, includes:, partials:, types_header: nil, test: nil)
87
87
  _includes = includes.clone()
88
88
 
89
- # Get list of all partialized module names
90
- partialized_modules = partials.keys
89
+ # This module's own header include is spliced out in favor of the shared types
90
+ # header at its exact original list position -- see #splice_in_replacement.
91
+ _includes = splice_in_replacement(includes: _includes, name: name, replacement: types_header)
91
92
 
92
- # Remove includes for all partialized modules
93
- # Remove our own orginal name as well
93
+ # Remove includes for every other partialized module (this module's own name was
94
+ # already handled above)
94
95
  _includes = remove_matching_includes(
95
96
  includes: _includes,
96
- modules: ([name] + partialized_modules)
97
+ modules: (partials.keys - [name])
97
98
  )
98
99
 
99
- # When this module has any typedefs or aggregate definitions, they live in a shared
100
- # header generated once and included here rather than duplicated inline.
101
- _includes << UserInclude.new(types_header) if types_header
102
-
103
100
  # Remove any duplicates
104
101
  Includes.sanitize!(_includes)
105
102
 
@@ -116,9 +113,19 @@ class Partializer
116
113
  def remap_implementation_source_includes(name:, includes:, partials:, test: nil)
117
114
  _includes = includes.clone()
118
115
 
119
- # Add implementation header
120
- _includes << UserInclude.new(
121
- @file_path_utils.form_partial_implementation_header_filename(name)
116
+ # Splice the implementation header in at this module's own header's original list
117
+ # position, same rationale as #splice_in_replacement -- the generated
118
+ # implementation header carries the shared types header in correctly-ordered
119
+ # position internally, but that alone doesn't help if THIS file's own separate
120
+ # include list still reaches an unrelated header that transitively re-includes the
121
+ # real module header before the implementation header (and everything it carries)
122
+ # is ever reached. Appending it at the very end (after this file's own copy of every
123
+ # other real include) would put it after exactly that kind of transitive
124
+ # re-inclusion instead of before it.
125
+ _includes = splice_in_replacement(
126
+ includes: _includes,
127
+ name: name,
128
+ replacement: @file_path_utils.form_partial_implementation_header_filename(name)
122
129
  )
123
130
 
124
131
  mockable_modules = []
@@ -144,11 +151,12 @@ class Partializer
144
151
  end
145
152
  end
146
153
 
147
- # Remove the original module header now that it's remapped to mockable interface
148
- # Remove our own orginal name as well
154
+ # Remove the original headers of any OTHER modules now remapped to mockable
155
+ # interfaces above -- this module's own original header was already handled by
156
+ # the splice above.
149
157
  _includes = remove_matching_includes(
150
158
  includes: _includes,
151
- modules: ([name] + mockable_modules)
159
+ modules: mockable_modules
152
160
  )
153
161
 
154
162
  # Remove any duplicates
@@ -167,20 +175,17 @@ class Partializer
167
175
  def remap_interface_header_includes(name:, includes:, partials:, types_header: nil, test: nil)
168
176
  _includes = includes.clone()
169
177
 
170
- # Get list of all partialized module names
171
- partialized_modules = partials.keys
178
+ # This module's own header include is spliced out in favor of the shared types
179
+ # header at its exact original list position -- see #splice_in_replacement.
180
+ _includes = splice_in_replacement(includes: _includes, name: name, replacement: types_header)
172
181
 
173
- # Remove includes for all partialized modules
174
- # Remove our own orginal name as well
182
+ # Remove includes for every other partialized module (this module's own name was
183
+ # already handled above)
175
184
  _includes = remove_matching_includes(
176
185
  includes: _includes,
177
- modules: ([name] + partialized_modules)
186
+ modules: (partials.keys - [name])
178
187
  )
179
188
 
180
- # When this module has any typedefs or aggregate definitions, they live in a shared
181
- # header generated once and included here rather than duplicated inline.
182
- _includes << UserInclude.new(types_header) if types_header
183
-
184
189
  # Remove any duplicates
185
190
  Includes.sanitize!(_includes)
186
191
 
@@ -452,6 +457,46 @@ class Partializer
452
457
  )
453
458
  end
454
459
 
460
+ # Swaps `name`'s own header include for `replacement` at that same list position,
461
+ # instead of stripping it out and appending the replacement at the very end.
462
+ #
463
+ # Both call sites' generated replacements (the shared types header; the generated
464
+ # implementation header, which itself carries the types header) spoof the real
465
+ # header's own include guard (a top-of-file `#define <ORIGINAL_GUARD>`) so that if the
466
+ # real header is *also* reached a second way -- e.g. a transitively-included,
467
+ # differently-named header that itself does a genuine `#include` of this module's
468
+ # real header -- the real header's guard is already
469
+ # tripped and its content (a second, conflicting copy of the same typedefs/structs)
470
+ # never gets processed. That only works if the spoofing macro is defined *before*
471
+ # such a transitive re-inclusion is reached, not after. Appending at the very end put
472
+ # it after any such re-inclusion instead of before it. Keeping the replacement at the
473
+ # original header's own list position preserves everything the generated content
474
+ # depends on that came before it in the source's own working include order (e.g. a
475
+ # shared Types.h), while still landing ahead of anything transitively re-reaching the
476
+ # real header afterward.
477
+ def splice_in_replacement(includes:, name:, replacement:)
478
+ return remove_matching_includes(includes: includes, modules: [name]) unless replacement
479
+
480
+ replaced = false
481
+ spliced = includes.map do |include|
482
+ if !replaced && include.filename.ext().downcase() == name.downcase()
483
+ replaced = true
484
+ UserInclude.new(replacement)
485
+ else
486
+ include
487
+ end
488
+ end
489
+ # This module's own header wasn't in the includes list at all -- no original
490
+ # position to preserve, so fall back to appending.
491
+ spliced << UserInclude.new(replacement) unless replaced
492
+
493
+ # A duplicate/case-variant entry beyond the first match (e.g. both 'module.h' and
494
+ # 'MODULE.H' present) has no meaningful position of its own to preserve -- drop it
495
+ # like remove_matching_includes always has.
496
+ spliced = remove_matching_includes(includes: spliced, modules: [name])
497
+ spliced
498
+ end
499
+
455
500
  # Remove includes that match the given module names (case-insensitive)
456
501
  # Returns a new array with matching includes removed
457
502
  def remove_matching_includes(includes:, modules:)
@@ -82,7 +82,12 @@ require 'set'
82
82
 
83
83
  # Parse GCC preprocessor output (from -fdirectives-only) to extract system include directives
84
84
  class PreprocessinatorLineMarkerIncludesExtractor
85
- LINE_MARKER_REGEX = /^#\s+(\d+)\s+"([^"]+)"(?:\s+(\d+(?:\s+\d+)*))?$/ unless const_defined?(:LINE_MARKER_REGEX)
85
+ # Leading whitespace is tolerated (`^\s*`, not just `^`): GCC's -fdirectives-only output
86
+ # replaces a top-level #include with a line marker that inherits that #include's own
87
+ # original indentation (GH #1268) -- an indented `#include "x.h"` produces an indented
88
+ # ` # 1 "x.h" 1`, not the flush-left marker every OTHER marker GCC generates uses.
89
+ # Without this, such an include is silently missing from the extracted list entirely.
90
+ LINE_MARKER_REGEX = /^\s*#\s+(\d+)\s+"([^"]+)"(?:\s+(\d+(?:\s+\d+)*))?$/ unless const_defined?(:LINE_MARKER_REGEX)
86
91
 
87
92
  SYSTEM = :system unless const_defined?(:SYSTEM)
88
93
  USER = :user unless const_defined?(:USER)
@@ -236,8 +236,15 @@ class PreprocessinatorReconstructor
236
236
  def extract_macro_defs(file_contents, include_guard)
237
237
  macro_definitions = extract_multiline_directives( file_contents, 'define' )
238
238
 
239
- # Remove an include guard if provided
240
- macro_definitions.reject! {|macro| macro.include?( include_guard ) } if !include_guard.nil?
239
+ # Remove an include guard if provided. A plain substring test here would also reject
240
+ # any ordinary macro whose name or value happens to contain the guard string -- e.g.
241
+ # guard RTC_H matching inside RTC_HOUR_SECONDS' own name, or inside RTC_DAY_SECONDS'
242
+ # value, which references RTC_HOUR_SECONDS (GH #1266). Anchoring to "#define <guard>"
243
+ # followed by whitespace or end-of-line only matches the guard definition itself.
244
+ if !include_guard.nil?
245
+ include_guard_regex = /^\s*#\s*define\s+#{Regexp.escape(include_guard)}(?:\s|$)/
246
+ macro_definitions.reject! {|macro| macro =~ include_guard_regex }
247
+ end
241
248
 
242
249
  return macro_definitions
243
250
  end
@@ -271,10 +278,16 @@ class PreprocessinatorReconstructor
271
278
  def _scan_expansion_for_file(input, filepath, &block)
272
279
  # Expand filepath under inspection to ensure proper match
273
280
  extraction_filepath = File.expand_path( filepath )
274
- # Preprocessor directive blocks generally take the form of '# <digits> <text> [optional digits]'
275
- directive = /^# \d+ \"/
281
+ # Preprocessor directive blocks generally take the form of '# <digits> <text> [optional digits]'.
282
+ # Leading whitespace is tolerated (`^\s*`, not just `^`): GCC's -fdirectives-only output
283
+ # replaces a top-level #include with a line marker that inherits that #include's own
284
+ # original indentation (GH #1268) -- an indented `#include "x.h"` produces an indented
285
+ # ` # 1 "x.h" 1`, not the flush-left marker every OTHER marker GCC generates uses.
286
+ # Failing to recognize such a marker here leaves `extract` stuck at whatever it already
287
+ # was instead of correctly toggling off, leaking the ignored file's content into output.
288
+ directive = /^\s*# \d+ \"/
276
289
  # Line markers have the specific form of '# <digits> "path/filename.ext" [optional digits]' (see above)
277
- line_marker = /^#\s(\d+)\s\"(.+)\"/
290
+ line_marker = /^\s*#\s(\d+)\s\"(.+)\"/
278
291
  # Boolean to ping pong between line-by-line extract/ignore
279
292
  extract = false
280
293
 
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.7'
18
+ GEM = '1.1.9'
19
19
  TAG = GEM
20
20
 
21
21
  # If run as a script print Ceedling's version to $stdout
@@ -169,6 +169,13 @@ class ConsoleReportinator < GcovReportinator
169
169
  section = section.reject { |line| line.include?( File.basename( source,'.*') ) }
170
170
  report = section.map { |line| report_name + ' | ' + line }.join('')
171
171
  @loginator.log( report )
172
+ else
173
+ # A Partial whose remapped name still doesn't match any File header in gcov's
174
+ # output -- without this, a Partial's unparseable coverage silently produces no
175
+ # report and no log at all, unlike the equivalent non-Partial case just below.
176
+ @loginator.lazy( Verbosity::COMPLAIN ) do
177
+ "Found no coverage results for #{test}::#{File.basename(source)}"
178
+ end
172
179
  end
173
180
 
174
181
  # Otherwise, found no coverage results
@@ -70,16 +70,12 @@ class Gcov < Plugin
70
70
 
71
71
  @mutex = Mutex.new()
72
72
 
73
- # Validate MC/DC configuration against GCC version (only incurs gcc --version when :mcdc: TRUE)
74
- if @project_config[:gcov_mcdc]
75
- gcc_version = get_gcc_version()
76
- if gcc_version.major < 14
77
- raise CeedlingException.new(
78
- ":gcov ↳ :mcdc ➡️ Modified condition/decision coverage requires GCC 14 or higher " \
79
- "(found #{gcc_version.major}.#{gcc_version.minor})"
80
- )
81
- end
82
- end
73
+ # See validate_mcdc_gcc_version! below for why this isn't checked eagerly here --
74
+ # setup() runs for every build this plugin is merely *enabled* for, not only real
75
+ # gcov: builds. Shelling out to `gcc --version` here would make GCC-version
76
+ # validation (and a build-breaking exception on too-old a GCC) fire even for builds
77
+ # that never touch :mcdc at all, e.g. plain `test:all`.
78
+ @mcdc_gcc_checked = false
83
79
  end
84
80
 
85
81
  # Called within class and also externally by plugin Rakefile
@@ -179,6 +175,8 @@ class Gcov < Plugin
179
175
 
180
176
  # Compile all non-assembly files with coverage; gcovr --exclude filters non-production files from reports
181
177
  if File.extname(source) != EXTENSION_ASSEMBLY
178
+ validate_mcdc_gcc_version!
179
+
182
180
  arg_hash[:tool] = TOOLS_GCOV_COMPILER
183
181
  arg_hash[:msg] = @reportinator.generate_module_progress(
184
182
  operation: "Compiling with coverage",
@@ -192,6 +190,8 @@ class Gcov < Plugin
192
190
 
193
191
  def pre_link_execute(arg_hash)
194
192
  if arg_hash[:context] == GCOV_SYM
193
+ validate_mcdc_gcc_version!
194
+
195
195
  @cli_gcov_task = true
196
196
  arg_hash[:tool] = TOOLS_GCOV_LINKER
197
197
  arg_hash[:flags] += ['-fcondition-coverage'] if @project_config[:gcov_mcdc]
@@ -364,6 +364,25 @@ class Gcov < Plugin
364
364
  return reportinators
365
365
  end
366
366
 
367
+ # Lazily validates :mcdc against the installed GCC version -- called from every
368
+ # gcov-context build hook that could otherwise emit -fcondition-coverage (compile,
369
+ # link), memoized so `gcc --version` runs at most once regardless of how many source
370
+ # files or hooks fire before it. See setup()'s own comment for why this isn't checked
371
+ # eagerly there.
372
+ def validate_mcdc_gcc_version!
373
+ return if @mcdc_gcc_checked
374
+ @mcdc_gcc_checked = true
375
+ return unless @project_config[:gcov_mcdc]
376
+
377
+ gcc_version = get_gcc_version()
378
+ if gcc_version.major < 14
379
+ raise CeedlingException.new(
380
+ ":gcov ↳ :mcdc ➡️ Modified condition/decision coverage requires GCC 14 or higher " \
381
+ "(found #{gcc_version.major}.#{gcc_version.minor})"
382
+ )
383
+ end
384
+ end
385
+
367
386
  def get_gcc_version()
368
387
  command = @tool_executor.build_command_line( TOOLS_GCOV_GCC_VERSION, [])
369
388