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.
- checksums.yaml +4 -4
- data/GIT_COMMIT_SHA +1 -1
- data/README.md +1 -1
- data/docs/Changelog.md +48 -0
- data/docs/KnownIssues.md +3 -0
- data/docs/mkdocs/plugins/gcov/gcovr.md +9 -4
- data/docs/mkdocs/reference/gcov-plugin.md +9 -4
- data/lib/ceedling/c_extractor/c_extractor_declarations.rb +11 -10
- data/lib/ceedling/c_extractor/c_extractor_preprocessing.rb +67 -2
- data/lib/ceedling/file_path_utils.rb +4 -0
- data/lib/ceedling/generators/generator_test_results_backtrace.rb +5 -3
- data/lib/ceedling/generators/generator_test_runner.rb +5 -1
- data/lib/ceedling/includes/includes.rb +8 -1
- data/lib/ceedling/partials/partializer.rb +69 -24
- data/lib/ceedling/preprocess/preprocessinator_line_marker_includes_extractor.rb +6 -1
- data/lib/ceedling/preprocess/preprocessinator_reconstructor.rb +18 -5
- data/lib/version.rb +1 -1
- data/plugins/gcov/lib/console_reportinator.rb +7 -0
- data/plugins/gcov/lib/gcov.rb +29 -10
- data/plugins/gcov/lib/gcovr_reportinator.rb +62 -48
- data/plugins/gcov/lib/reportgenerator_reportinator.rb +10 -6
- data/site-local/plugins/gcov/gcovr.html +9 -4
- data/site-local/reference/gcov-plugin.html +9 -4
- data/site-local/sitemap.xml.gz +0 -0
- data/spec/system/gcov_deployment_spec.rb +1 -0
- data/spec/system/support/gcov_common_test_cases.rb +18 -0
- data/spec/units/c_extractor/c_extractor_declarations_spec.rb +16 -0
- data/spec/units/c_extractor/c_extractor_integration_spec.rb +71 -0
- data/spec/units/c_extractor/c_extractor_preprocessing_spec.rb +122 -0
- data/spec/units/generators/generator_partials_spec.rb +98 -0
- data/spec/units/generators/generator_test_results_backtrace_spec.rb +26 -0
- data/spec/units/generators/generator_test_runner_spec.rb +23 -0
- data/spec/units/includes/includes_spec.rb +21 -3
- data/spec/units/partials/partializer_spec.rb +62 -0
- data/spec/units/preprocess/preprocessinator_line_marker_includes_extractor_spec.rb +64 -0
- data/spec/units/preprocess/preprocessinator_reconstructor_spec.rb +74 -0
- data/vendor/unity/auto/generate_test_runner.rb +3 -1
- data/vendor/unity/auto/unity_test_summary.rb +2 -2
- data/vendor/unity/docs/UnityChangeLog.md +7 -0
- data/vendor/unity/docs/UnityConfigurationGuide.md +4 -0
- data/vendor/unity/src/unity_internals.h +21 -7
- data/vendor/unity/test/Makefile +9 -2
- data/vendor/unity/test/rakefile_helper.rb +1 -0
- data/vendor/unity/test/tests/test_generate_test_runner.rb +49 -0
- data/vendor/unity/test/tests/test_unity_test_summary.rb +58 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 28f18f89f99b7e81f68293c994fa85ff25532ca62bd250c2d1c7e351ec3ec7a0
|
|
4
|
+
data.tar.gz: 1252cbea2a5752315c65826f8068313450cf08d0ea63259e1e8a5d422588140c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7aeb58a943b8df79fa31e12694a16019fba66cf798cc899eccb4d1e7b64d0b5fe09e5965f376791381cbc8c0e47a65c0563dbc5a00e1523714e813b8491a9896
|
|
7
|
+
data.tar.gz: e6cf0643c68cd8fa2c96b99de3e2e95ca276b27bf5ecb5cfa41ae9f1371ed55f3675ec5491d585b989629f2f89b0f16fc65f0586daa6ade065fbe903d9208087
|
data/GIT_COMMIT_SHA
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
2209dc2
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Ceedling 
|
|
2
2
|
========
|
|
3
3
|
|
|
4
|
-
**Ceedling 1.1.
|
|
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`
|
|
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.
|
|
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.
|
|
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.
|
|
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`
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
#
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?(
|
|
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
|
-
|
|
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.
|
|
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
|
-
#
|
|
90
|
-
|
|
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
|
|
93
|
-
#
|
|
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]
|
|
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
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
148
|
-
#
|
|
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:
|
|
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
|
-
#
|
|
171
|
-
|
|
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
|
|
174
|
-
#
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
@@ -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
|
data/plugins/gcov/lib/gcov.rb
CHANGED
|
@@ -70,16 +70,12 @@ class Gcov < Plugin
|
|
|
70
70
|
|
|
71
71
|
@mutex = Mutex.new()
|
|
72
72
|
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|