ceedling 1.1.7 → 1.1.8

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 (32) 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 +30 -0
  5. data/docs/KnownIssues.md +2 -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 +39 -2
  10. data/lib/ceedling/generators/generator_test_results_backtrace.rb +5 -3
  11. data/lib/ceedling/generators/generator_test_runner.rb +5 -1
  12. data/lib/ceedling/preprocess/preprocessinator_line_marker_includes_extractor.rb +6 -1
  13. data/lib/ceedling/preprocess/preprocessinator_reconstructor.rb +18 -5
  14. data/lib/version.rb +1 -1
  15. data/plugins/gcov/lib/console_reportinator.rb +7 -0
  16. data/plugins/gcov/lib/gcov.rb +29 -10
  17. data/plugins/gcov/lib/gcovr_reportinator.rb +62 -48
  18. data/plugins/gcov/lib/reportgenerator_reportinator.rb +10 -6
  19. data/site-local/plugins/gcov/gcovr.html +9 -4
  20. data/site-local/reference/gcov-plugin.html +9 -4
  21. data/site-local/sitemap.xml.gz +0 -0
  22. data/spec/system/gcov_deployment_spec.rb +1 -0
  23. data/spec/system/support/gcov_common_test_cases.rb +18 -0
  24. data/spec/units/c_extractor/c_extractor_declarations_spec.rb +16 -0
  25. data/spec/units/c_extractor/c_extractor_integration_spec.rb +53 -0
  26. data/spec/units/c_extractor/c_extractor_preprocessing_spec.rb +45 -0
  27. data/spec/units/generators/generator_partials_spec.rb +98 -0
  28. data/spec/units/generators/generator_test_results_backtrace_spec.rb +26 -0
  29. data/spec/units/generators/generator_test_runner_spec.rb +23 -0
  30. data/spec/units/preprocess/preprocessinator_line_marker_includes_extractor_spec.rb +64 -0
  31. data/spec/units/preprocess/preprocessinator_reconstructor_spec.rb +74 -0
  32. metadata +4 -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: 3c1edc565bc9fcb24accc9fd0d5001076fe86564cb0acd02c7d1fc092476c2c1
4
+ data.tar.gz: 0c5ec0119bc3249824177c6094afd3e3ef9ee7da698b92fed894924d45d83625
5
5
  SHA512:
6
- metadata.gz: d8db68c457e06b77007d1e954c4512a4c7e84abd13918546c31b5e05c4021e01e7ebafb5d3e85e0268d1742716e227d7860cdcc763f41f683c5d119f4841a998
7
- data.tar.gz: 32ae8949567ae23bf7afff0deea71e66e06ea5dd6c56fcdde1dd561c5404020550ca9a65b11840f19ffde3a702fed80e73c2de03af0b8a1341490605a9b6b266
6
+ metadata.gz: 6efc0307567637d4019978131f45758d694b97ea93a4a7ada5ff6be8c682d70eff9d8d453de4882cc46344bb1b0aa098dbf7982582a7d7a168818caba2bd7e51
7
+ data.tar.gz: 6366fddeaff5572f317efffb161ff64378cae4e6b2dea30aa11f3baf4aac1777611331483d071a080cb9a01aa1e4087409d5314559881ae0a12ce753a1f86311
data/GIT_COMMIT_SHA CHANGED
@@ -1 +1 @@
1
- b1534f9
1
+ 885dc91
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.8** 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,36 @@ This changelog is complemented by three other documents:
10
10
 
11
11
  ---
12
12
 
13
+ # [1.1.8] — 2026-09-10
14
+
15
+ ## 💪 Fixed
16
+
17
+ - 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.
18
+ - 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.
19
+
20
+ ### Partials
21
+
22
+ - [#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.
23
+
24
+ ### Preprocessing
25
+
26
+ - [#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.
27
+ - [#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.
28
+
29
+ ### Gcov plugin
30
+
31
+ - Fixed `:gcov ↳ :gcovr ↳ :decisions` reporting an incorrect minimum-version requirement; the version check (and docs) now correctly require `gcovr` 5.1 or higher.
32
+ - 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.
33
+ - 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.
34
+ - 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.
35
+ - 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.
36
+ - 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.
37
+ - 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.
38
+ - `: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`.
39
+ - 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.
40
+
41
+ ---
42
+
13
43
  # [1.1.7] — 2026-09-03
14
44
 
15
45
  ## 💪 Fixed
data/docs/KnownIssues.md CHANGED
@@ -20,6 +20,8 @@ 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.
23
25
  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
26
 
25
27
  ---
@@ -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,46 @@ 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
+ text << (scanner.scan(/[^"'\\\n\/]*/) || '')
250
+
251
+ if scanner.scan(%r{//})
252
+ # A trailing line comment (e.g. "// don't change this") can contain an
253
+ # apostrophe or quote that isn't a literal delimiter at all. Consumed the
254
+ # ordinary way, that stray quote would send skip_c_string hunting for a
255
+ # closing match, straight through this directive's own newline and into
256
+ # whatever follows -- silently merging the next #define into this one.
257
+ # Comment content is captured verbatim and never scanned for literals.
258
+ #
259
+ # A '\' immediately before the physical newline still splices lines here,
260
+ # same as everywhere else in a directive (line splicing is a translation
261
+ # phase that happens before comments are stripped), so the comment -- and
262
+ # the directive -- keeps going onto the next physical line rather than
263
+ # ending at that newline.
264
+ before = scanner.pos - 2
265
+ loop do
266
+ scanner.scan(/[^\\\n]*/)
267
+ break if scanner.eos?
268
+ break unless scanner.scan(/\\\n/) || scanner.scan(/\\/)
269
+ end
270
+ text << scanner.string[before...scanner.pos]
271
+
272
+ elsif scanner.scan(%r{/\*})
273
+ # A block comment can legitimately span physical lines without ending the
274
+ # directive (only an un-commented newline does that) -- its content, quotes
275
+ # included, is likewise never scanned for literals, only for its own close.
276
+ before = scanner.pos - 2
277
+ scanner.skip_until(%r{\*/}) || scanner.terminate
278
+ text << scanner.string[before...scanner.pos]
279
+
280
+ elsif scanner.scan(%r{/})
281
+ text << '/'
245
282
 
246
- if (ch = scanner.peek(1)) == '"' || ch == "'"
283
+ elsif (ch = scanner.peek(1)) == '"' || ch == "'"
247
284
  before = scanner.pos
248
285
  @c_extractor_code_text.skip_c_string(scanner, ch)
249
286
  text << scanner.string[before...scanner.pos]
@@ -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
@@ -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.8'
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
 
@@ -66,9 +66,12 @@ class GcovrReportinator < GcovReportinator
66
66
  )
67
67
  end
68
68
 
69
- if gcovr_opts[:decisions] && !min_version?( @gcovr_version, 6, 0 )
69
+ # --decisions was introduced in gcovr 5.1, not 6.0 -- confirmed directly against
70
+ # gcovr's own changelog and a real gcovr 5.1/5.0 install (5.1 accepts --decisions
71
+ # and proceeds; 5.0 rejects it outright as unrecognized).
72
+ if gcovr_opts[:decisions] && !min_version?( @gcovr_version, 5, 1 )
70
73
  raise CeedlingException.new(
71
- ":gcov ↳ :gcovr ↳ :decisions ➡️ requires gcovr 6 or higher " \
74
+ ":gcov ↳ :gcovr ↳ :decisions ➡️ requires gcovr 5.1 or higher " \
72
75
  "(found #{@gcovr_version.major}.#{@gcovr_version.minor})"
73
76
  )
74
77
  end
@@ -116,9 +119,18 @@ class GcovrReportinator < GcovReportinator
116
119
  args += "--gcov-filter \"#{gcovr_opts[:gcov_filter]}\" " unless gcovr_opts[:gcov_filter].nil?
117
120
  args += "--gcov-exclude \"#{gcovr_opts[:gcov_exclude]}\" " unless gcovr_opts[:gcov_exclude].nil?
118
121
  args += "--exclude-directories \"#{gcovr_opts[:exclude_directories]}\" " unless gcovr_opts[:exclude_directories].nil?
119
- args += "--branches " if gcovr_opts[:branches]
120
- args += "--sort-uncovered " if gcovr_opts[:sort_uncovered]
121
- args += "--sort-percentage " if gcovr_opts[:sort_percentage]
122
+ # gcovr 7.0 deprecated --branches/--sort-uncovered/--sort-percentage in favor of
123
+ # these renamed equivalents (still functional, but warn, below 7.0); use whichever
124
+ # form the detected gcovr_version actually expects.
125
+ if gcovr_opts[:branches]
126
+ args += (min_version?(gcovr_version, 7, 0) ? "--txt-metric branch " : "--branches ")
127
+ end
128
+ if gcovr_opts[:sort_uncovered]
129
+ args += (min_version?(gcovr_version, 7, 0) ? "--sort uncovered-number " : "--sort-uncovered ")
130
+ end
131
+ if gcovr_opts[:sort_percentage]
132
+ args += (min_version?(gcovr_version, 7, 0) ? "--sort uncovered-percent " : "--sort-percentage ")
133
+ end
122
134
  args += "--print-summary " if gcovr_opts[:print_summary]
123
135
  args += "--gcov-executable \"#{gcovr_opts[:gcov_executable]}\" " unless gcovr_opts[:gcov_executable].nil?
124
136
  args += "--exclude-unreachable-branches " if gcovr_opts[:exclude_unreachable_branches]
@@ -152,18 +164,24 @@ class GcovrReportinator < GcovReportinator
152
164
  next if gcovr_opts[opt].nil?
153
165
 
154
166
  value = gcovr_opts[opt]
155
-
167
+
156
168
  # Value sanity checks for :fail_under_* settings
157
169
  if opt.to_s =~ /fail_/
158
170
  if not value.is_a? Integer
159
171
  raise CeedlingException.new(":gcov ↳ :gcovr ↳ :#{opt} ➡️ '#{value}' must be an integer")
160
- elsif (value < 0) || (value > 100)
161
- raise CeedlingException.new(":gcov ↳ :gcovr ↳ :#{opt} ➡️ '#{value}' must be an integer percentage 0 – 100")
172
+ elsif (value < 1) || (value > 100)
173
+ raise CeedlingException.new(":gcov ↳ :gcovr ↳ :#{opt} ➡️ '#{value}' must be an integer percentage 1 – 100")
162
174
  end
163
175
  end
164
-
165
- # If the YAML key has a value, trasnform key into command line argument with value and concatenate
166
- args += "--#{opt.to_s.gsub('_','-')} #{value} " unless value.nil?
176
+
177
+ # :source_encoding/:object_directory are quoted (unlike the already-validated,
178
+ # always-integer :fail_under_* values) so a value containing a space doesn't
179
+ # break the constructed command line.
180
+ if [:source_encoding, :object_directory].include?(opt)
181
+ args += "--#{opt.to_s.gsub('_','-')} \"#{value}\" " unless value.nil?
182
+ else
183
+ args += "--#{opt.to_s.gsub('_','-')} #{value} " unless value.nil?
184
+ end
167
185
  end
168
186
 
169
187
  return args
@@ -409,7 +427,12 @@ class GcovrReportinator < GcovReportinator
409
427
  _opts[:report_exclude] = excludes unless excludes.empty?
410
428
  end
411
429
 
412
- _opts[:mcdc] = true if opts[:gcov_mcdc]
430
+ # No :mcdc-specific gcovr flag to set here -- GCC's own condition/MC-DC data flows
431
+ # into gcovr's reports unconditionally, in every format, regardless of --decisions
432
+ # (confirmed directly: gcovr's Condition-labeled HTML/JSON content is byte-identical
433
+ # with and without --decisions; that flag only adds its own separate, additive
434
+ # Decision data alongside it). :mcdc's own gcovr-version floor is still enforced in
435
+ # initialize -- there's just nothing more for gcovr's own CLI args to do here.
413
436
 
414
437
  return _opts
415
438
  end
@@ -417,22 +440,29 @@ class GcovrReportinator < GcovReportinator
417
440
 
418
441
  # Build a combined Python regex for gcovr's `--exclude` flag covering all
419
442
  # non-production file categories: test files, support files, and generated/framework files.
443
+ # Path/prefix values are config-driven, so they're Regexp-escaped -- interpolated raw, a
444
+ # metacharacter (e.g. a literal '.') in a project's own path would silently over- or
445
+ # under-match.
420
446
  def build_report_exclusions
421
447
  data = build_exclusion_data
422
448
  patterns = []
423
449
 
450
+ test_prefix = Regexp.escape(data[:test_prefix].to_s)
451
+ build_root = Regexp.escape(data[:build_root].to_s)
452
+ src_extension = Regexp.escape(data[:src_extension].to_s)
453
+
424
454
  data[:test_paths].each do |path|
425
455
  # Test files (e.g. test_foo.c)
426
- patterns << ".*#{path}.*/#{data[:test_prefix]}.+\\#{data[:src_extension]}$"
456
+ patterns << ".*#{Regexp.escape(path)}.*/#{test_prefix}.+\\#{src_extension}$"
427
457
  end
428
458
 
429
459
  # Support files (e.g. helpers, stubs, fixtures) — never production source
430
460
  data[:support_paths].each do |path|
431
- patterns << ".*#{path}/.+\\#{data[:src_extension]}$"
461
+ patterns << ".*#{Regexp.escape(path)}/.+\\#{src_extension}$"
432
462
  end
433
463
 
434
464
  # Any generated files for tests or vendored framework C source files below the root of the build directory
435
- patterns << ".*#{data[:build_root]}/.+\\#{EXTENSION_CORE_SOURCE}$"
465
+ patterns << ".*#{build_root}/.+\\#{EXTENSION_CORE_SOURCE}$"
436
466
 
437
467
  return patterns
438
468
  end
@@ -535,59 +565,43 @@ class GcovrReportinator < GcovReportinator
535
565
 
536
566
  # Output to console a human-friendly message on certain coverage failure exit codes.
537
567
  # Prefers the actual gcovr error text from stderr; falls back to descriptive strings.
538
- # Perform the logic on whether to raise an exception
568
+ # gcovr's composite exit code bit-ORs every violated :fail_under_* threshold together --
569
+ # every violated bit is collected and reported, not just whichever is checked first.
539
570
  def gcovr_exec_exception?(opts, exitcode, boom, shell_result=nil)
571
+ violations = []
540
572
 
541
573
  # Special handling of exit code 2 with --fail-under-line
542
574
  if ((exitcode & 2) == 2) and !opts[:fail_under_line].nil?
543
575
  fallback = "Line coverage is less than the configured minimum of #{opts[:fail_under_line]}%"
544
- msg = "Gcovr ⏩️ #{extract_gcovr_error_message( shell_result ) || fallback}"
545
- if boom
546
- raise CeedlingException.new(msg)
547
- else
548
- @loginator.log( msg, Verbosity::COMPLAIN )
549
- # Clear bit in exit code
550
- exitcode &= ~2
551
- end
576
+ violations << "Gcovr ⏩️ #{extract_gcovr_error_message( shell_result ) || fallback}"
577
+ exitcode &= ~2
552
578
  end
553
579
 
554
580
  # Special handling of exit code 4 with --fail-under-branch
555
581
  if ((exitcode & 4) == 4) and !opts[:fail_under_branch].nil?
556
582
  fallback = "Branch coverage is less than the configured minimum of #{opts[:fail_under_branch]}%"
557
- msg = "Gcovr ⏩️ #{extract_gcovr_error_message( shell_result ) || fallback}"
558
- if boom
559
- raise CeedlingException.new(msg)
560
- else
561
- @loginator.log( msg, Verbosity::COMPLAIN )
562
- # Clear bit in exit code
563
- exitcode &= ~4
564
- end
583
+ violations << "Gcovr ⏩️ #{extract_gcovr_error_message( shell_result ) || fallback}"
584
+ exitcode &= ~4
565
585
  end
566
586
 
567
587
  # Special handling of exit code 8 with --fail-under-decision
568
588
  if ((exitcode & 8) == 8) and !opts[:fail_under_decision].nil?
569
589
  fallback = "Decision coverage is less than the configured minimum of #{opts[:fail_under_decision]}%"
570
- msg = "Gcovr ⏩️ #{extract_gcovr_error_message( shell_result ) || fallback}"
571
- if boom
572
- raise CeedlingException.new(msg)
573
- else
574
- @loginator.log( msg, Verbosity::COMPLAIN )
575
- # Clear bit in exit code
576
- exitcode &= ~8
577
- end
590
+ violations << "Gcovr ⏩️ #{extract_gcovr_error_message( shell_result ) || fallback}"
591
+ exitcode &= ~8
578
592
  end
579
593
 
580
594
  # Special handling of exit code 16 with --fail-under-function
581
595
  if ((exitcode & 16) == 16) and !opts[:fail_under_function].nil?
582
596
  fallback = "Function coverage is less than the configured minimum of #{opts[:fail_under_function]}%"
583
- msg = "Gcovr ⏩️ #{extract_gcovr_error_message( shell_result ) || fallback}"
584
- if boom
585
- raise CeedlingException.new(msg)
586
- else
587
- @loginator.log( msg, Verbosity::COMPLAIN )
588
- # Clear bit in exit code
589
- exitcode &= ~16
590
- end
597
+ violations << "Gcovr ⏩️ #{extract_gcovr_error_message( shell_result ) || fallback}"
598
+ exitcode &= ~16
599
+ end
600
+
601
+ if boom
602
+ raise CeedlingException.new( violations.join("\n") ) unless violations.empty?
603
+ else
604
+ violations.each { |msg| @loginator.log( msg, Verbosity::COMPLAIN ) }
591
605
  end
592
606
 
593
607
  # A non-zero exit code is a problem