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
@@ -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
@@ -128,7 +128,9 @@ class ReportGeneratorReportinator < GcovReportinator
128
128
 
129
129
  # Get the ReportGenerator options from the project options.
130
130
  def collect_reportgenerator_opts(opts)
131
- _opts = opts[REPORT_GENERATOR_SETTING_PREFIX.to_sym]
131
+ # dup prevents repeated calls from accumulating mutations on the shared opts hash --
132
+ # mirrors GcovrReportinator#collect_gcovr_opts' identical guard and rationale.
133
+ _opts = opts[REPORT_GENERATOR_SETTING_PREFIX.to_sym].dup
132
134
 
133
135
  # Auto-generate -filefilters: exclusion patterns for test paths and the build root.
134
136
  # These exclude test files and all generated/framework files from coverage reports.
@@ -275,17 +277,19 @@ class ReportGeneratorReportinator < GcovReportinator
275
277
  # Build filename-fragment patterns for the .gcno scan exclusion regex.
276
278
  # Excludes test files, generated mocks, and test runners from gcov processing.
277
279
  # The '.*' suffix handles source extensions embedded in .gcno filenames (e.g. test_foo.c.gcno).
280
+ # test_prefix/mock_prefix are config-driven, so they're Regexp-escaped -- unescaped, a
281
+ # metacharacter in either (e.g. a literal '.') would silently over- or under-match.
278
282
  def build_gcno_exclusions
279
283
  data = build_exclusion_data
280
284
  # Use [^\/\\]* (no path separators) rather than .* to match filenames only.
281
285
  # Ceedling names build output dirs after test files (e.g. build/gcov/out/test_foo/),
282
286
  # so .* would greedily match test_foo/bar.c and exclude ALL production sources.
283
287
  [
284
- "#{data[:test_prefix]}[^\\/\\\\]*", # test_foo.gcno
285
- "#{data[:mock_prefix]}[^\\/\\\\]*", # MockBar.gcno
286
- "[^\\/\\\\]*_runner[^\\/\\\\]*", # test_foo_runner.gcno
287
- File.basename(UNITY_C_FILE, '.*'), # unity.gcno
288
- File.basename(CMOCK_C_FILE, '.*') # cmock.gcno
288
+ "#{Regexp.escape(data[:test_prefix].to_s)}[^\\/\\\\]*", # test_foo.gcno
289
+ "#{Regexp.escape(data[:mock_prefix].to_s)}[^\\/\\\\]*", # MockBar.gcno
290
+ "[^\\/\\\\]*_runner[^\\/\\\\]*", # test_foo_runner.gcno
291
+ Regexp.escape(File.basename(UNITY_C_FILE, '.*')), # unity.gcno
292
+ Regexp.escape(File.basename(CMOCK_C_FILE, '.*')) # cmock.gcno
289
293
  ]
290
294
  end
291
295
 
@@ -5367,7 +5367,7 @@ it's never silently non-functional.</p>
5367
5367
  <p>Report the decision coverage. For HTML, JSON, and the summary report.
5368
5368
  (<code>gcovr --decisions</code>)</p>
5369
5369
  <p>Implied automatically whenever <code>:fail_under_decision</code> is set.</p>
5370
- <p><strong>Requires:</strong> <code>gcovr</code> 6.0 or higher</p>
5370
+ <p><strong>Requires:</strong> <code>gcovr</code> 5.1 or higher</p>
5371
5371
  <hr />
5372
5372
  <h3 id="fail_under_function"><code>:fail_under_function</code></h3>
5373
5373
  <p>Exit with a status of 16 if the total function coverage is less than this minimum
@@ -5387,15 +5387,20 @@ logs a warning without breaking the build.</p>
5387
5387
  <hr />
5388
5388
  <h3 id="branches"><code>:branches</code></h3>
5389
5389
  <p>Report the branch coverage instead of the line coverage. Applies to the text
5390
- report only. (<code>gcovr --branches</code>)</p>
5390
+ report only. Uses <code>gcovr --txt-metric branch</code> on <code>gcovr</code> 7.0+ (<code>--branches</code> is
5391
+ deprecated as of 7.0, though still functional); <code>gcovr --branches</code> below it.</p>
5391
5392
  <hr />
5392
5393
  <h3 id="sort_uncovered"><code>:sort_uncovered</code></h3>
5393
5394
  <p>Sort entries by increasing number of uncovered lines. Applies to text and HTML
5394
- reports. (<code>gcovr --sort-uncovered</code>)</p>
5395
+ reports. Uses <code>gcovr --sort uncovered-number</code> on <code>gcovr</code> 7.0+
5396
+ (<code>--sort-uncovered</code> is deprecated as of 7.0, though still functional);
5397
+ <code>gcovr --sort-uncovered</code> below it.</p>
5395
5398
  <hr />
5396
5399
  <h3 id="sort_percentage"><code>:sort_percentage</code></h3>
5397
5400
  <p>Sort entries by increasing percentage of uncovered lines. Applies to text and
5398
- HTML reports. (<code>gcovr --sort-percentage</code>)</p>
5401
+ HTML reports. Uses <code>gcovr --sort uncovered-percent</code> on <code>gcovr</code> 7.0+
5402
+ (<code>--sort-percentage</code> is deprecated as of 7.0, though still functional);
5403
+ <code>gcovr --sort-percentage</code> below it.</p>
5399
5404
  <hr />
5400
5405
  <h3 id="print_summary"><code>:print_summary</code></h3>
5401
5406
  <p>Print a small report to stdout with line and branch percentage coverage. This
@@ -5668,7 +5668,7 @@ present — setting this option implies <code>:decisions</code> (below) automati
5668
5668
  <p>Report the decision coverage. For HTML, JSON, and the summary report.
5669
5669
  (<code>gcovr --decisions</code>)</p>
5670
5670
  <p>Implied automatically whenever <code>:fail_under_decision</code> is set.</p>
5671
- <p><strong>Requires:</strong> <code>gcovr</code> 6.0 or higher</p>
5671
+ <p><strong>Requires:</strong> <code>gcovr</code> 5.1 or higher</p>
5672
5672
  <hr />
5673
5673
  <h3 id="fail_under_function"><code>:fail_under_function</code></h3>
5674
5674
  <p>Exit with status 16 if total function coverage is below this percentage.
@@ -5686,15 +5686,20 @@ instead of only logging a warning.</p>
5686
5686
  <hr />
5687
5687
  <h3 id="branches"><code>:branches</code></h3>
5688
5688
  <p>Report branch coverage instead of line coverage. Applies to text reports only.
5689
- (<code>gcovr --branches</code>)</p>
5689
+ Uses <code>gcovr --txt-metric branch</code> on <code>gcovr</code> 7.0+ (<code>--branches</code> is deprecated as
5690
+ of 7.0, though still functional); <code>gcovr --branches</code> below it.</p>
5690
5691
  <hr />
5691
5692
  <h3 id="sort_uncovered"><code>:sort_uncovered</code></h3>
5692
5693
  <p>Sort report entries by increasing number of uncovered lines. Applies to text
5693
- and HTML reports. (<code>gcovr --sort-uncovered</code>)</p>
5694
+ and HTML reports. Uses <code>gcovr --sort uncovered-number</code> on <code>gcovr</code> 7.0+
5695
+ (<code>--sort-uncovered</code> is deprecated as of 7.0, though still functional);
5696
+ <code>gcovr --sort-uncovered</code> below it.</p>
5694
5697
  <hr />
5695
5698
  <h3 id="sort_percentage"><code>:sort_percentage</code></h3>
5696
5699
  <p>Sort report entries by increasing percentage of uncovered lines. Applies to
5697
- text and HTML reports. (<code>gcovr --sort-percentage</code>)</p>
5700
+ text and HTML reports. Uses <code>gcovr --sort uncovered-percent</code> on <code>gcovr</code> 7.0+
5701
+ (<code>--sort-percentage</code> is deprecated as of 7.0, though still functional);
5702
+ <code>gcovr --sort-percentage</code> below it.</p>
5698
5703
  <hr />
5699
5704
  <h3 id="print_summary"><code>:print_summary</code></h3>
5700
5705
  <p>Print a brief line- and branch-coverage summary to stdout in addition to any
Binary file
@@ -37,6 +37,7 @@ ceedling_system_tests do
37
37
  test_case :project_with_gcov_success
38
38
  test_case :project_with_gcov_fail
39
39
  test_case :project_with_gcov_fail_under_decision
40
+ test_case :project_with_gcov_fail_under_line_rejects_zero
40
41
  test_case :gcov_console_report_with_system_header
41
42
  test_case :gcov_console_report_with_partial
42
43
  # TODO: Restore these tests when the :abort_on_uncovered option is restored in the Gcov plugin
@@ -98,6 +98,24 @@ module GcovCommonTestCases
98
98
  end
99
99
  end
100
100
 
101
+ # :fail_under_* documents a 1-100 range; 0 was previously silently accepted as a
102
+ # threshold that could never actually fail (0% coverage is always >= 0).
103
+ def project_with_gcov_fail_under_line_rejects_zero
104
+ @c.with_context do
105
+ Dir.chdir @proj_name do
106
+ prep_project_yml_for_coverage
107
+ @c.merge_project_yml_for_test( { :gcov => { :gcovr => { :fail_under_line => 0 } } } )
108
+ FileUtils.cp test_asset_path("example_file.h"), 'src/'
109
+ FileUtils.cp test_asset_path("example_file.c"), 'src/'
110
+ FileUtils.cp test_asset_path("test_example_file_success.c"), 'test/'
111
+
112
+ output = @c.ceedling_build_exec("gcov:all")
113
+ expect(@c.last_exit_status).to_not eq(0)
114
+ expect(output).to match(/:fail_under_line.*must be an integer percentage/)
115
+ end
116
+ end
117
+ end
118
+
101
119
  # TODO: Restore this test when the :abort_on_uncovered option is restored in the Gcov plugin
102
120
  # def project_with_gcov_fail_because_of_uncovered_files
103
121
  # @c.with_context do
@@ -52,6 +52,22 @@ describe CExtractorDeclarations do
52
52
  expect(rest).to eq("")
53
53
  end
54
54
 
55
+ # #1266 follow-on: same unanchored-substring risk class as #1262/#1266. The type-side
56
+ # identifier "counter_t" itself contains "count" as a substring; a plain rindex(name)
57
+ # would still find the real trailing occurrence here (rindex searches right-to-left),
58
+ # but this pins down that the \b-anchored match extract_type now uses doesn't regress
59
+ # this ordinary case, where the type name happens to share a substring with the
60
+ # declared name.
61
+ it "extracts a variable whose type name contains the variable's own name as a substring" do
62
+ content = "counter_t count;"
63
+ success, variable, pos, rest = extract_variable.call(content)
64
+
65
+ expect(success).to be true
66
+ check_single(variable, name: 'count', type: 'counter_t', text: 'counter_t count;')
67
+ expect(pos).to eq(content.length)
68
+ expect(rest).to eq("")
69
+ end
70
+
55
71
  it "extracts simple char variable" do
56
72
  content = "char c;"
57
73
  success, variable, pos, rest = extract_variable.call(content)
@@ -565,6 +565,59 @@ describe CExtractor do
565
565
  expect( contents.variable_declarations.length ).to eq 0
566
566
  end
567
567
 
568
+ # #1262: Partials-generated headers occasionally concatenated two adjacent
569
+ # #define lines onto one physical line. Reconstructing the joined text
570
+ # from element_sequence (as generator_partials.rb does) rules the plain,
571
+ # comment-free case in or out formally, rather than by inspection of
572
+ # #_collect_directive alone. This shape -- three plain register-offset
573
+ # macros in a row, no comments -- is expected to reconstruct cleanly.
574
+ it "reconstructs three adjacent plain #define macros joined by newlines with no text lost between them" do
575
+ file_contents = <<~'CONTENTS'
576
+ #define START_ADDRESS 0x00
577
+ #define IDX_DATARATE (ADS124S08_REG_ADDR_DATARATE - START_ADDRESS)
578
+ #define IDX_REF (ADS124S08_REG_ADDR_REF - START_ADDRESS)
579
+ CONTENTS
580
+
581
+ contents = extract_from.call(file_contents)
582
+
583
+ expect( contents.macro_definitions.length ).to eq 3
584
+ joined = contents.element_sequence.map(&:text).join("\n")
585
+ expect( joined ).to eq(
586
+ "#define START_ADDRESS 0x00\n" +
587
+ "#define IDX_DATARATE (ADS124S08_REG_ADDR_DATARATE - START_ADDRESS)\n" +
588
+ "#define IDX_REF (ADS124S08_REG_ADDR_REF - START_ADDRESS)"
589
+ )
590
+ end
591
+
592
+ # #1262's actual reported trigger: a trailing // comment on one #define containing
593
+ # an ordinary English contraction. The stray apostrophe used to be scanned as the
594
+ # start of a char literal, sending the directive scanner hunting for a closing
595
+ # match straight through this macro's own newline and into the next two #defines
596
+ # -- merging all three into one macro_definitions entry with no separating newline,
597
+ # which is exactly what later caused Partials-generated headers to concatenate two
598
+ # #define lines onto one physical line ("stray '#' in program").
599
+ it "reconstructs three adjacent #define macros when one has a trailing comment with an apostrophe" do
600
+ file_contents = <<~'CONTENTS'
601
+ #define START_ADDRESS 0x00 // don't change this
602
+ #define IDX_DATARATE (ADS124S08_REG_ADDR_DATARATE - START_ADDRESS)
603
+ #define IDX_REF (ADS124S08_REG_ADDR_REF - START_ADDRESS)
604
+ CONTENTS
605
+
606
+ contents = extract_from.call(file_contents)
607
+
608
+ expect( contents.macro_definitions.length ).to eq 3
609
+ expect( contents.macro_definitions[0].text ).to eq "#define START_ADDRESS 0x00 // don't change this"
610
+ expect( contents.macro_definitions[1].text ).to eq "#define IDX_DATARATE (ADS124S08_REG_ADDR_DATARATE - START_ADDRESS)"
611
+ expect( contents.macro_definitions[2].text ).to eq "#define IDX_REF (ADS124S08_REG_ADDR_REF - START_ADDRESS)"
612
+
613
+ joined = contents.element_sequence.map(&:text).join("\n")
614
+ expect( joined ).to eq(
615
+ "#define START_ADDRESS 0x00 // don't change this\n" +
616
+ "#define IDX_DATARATE (ADS124S08_REG_ADDR_DATARATE - START_ADDRESS)\n" +
617
+ "#define IDX_REF (ADS124S08_REG_ADDR_REF - START_ADDRESS)"
618
+ )
619
+ end
620
+
568
621
  it "should extract a function definition following a #define with an escaped character in a string literal (GH #1184)" do
569
622
  file_contents = <<~'CONTENTS'
570
623
  #include "world.h"
@@ -954,6 +1007,24 @@ describe CExtractor do
954
1007
  expect(plain.decorators).to eq([])
955
1008
  end
956
1009
 
1010
+ it "normalizes a preprocessor-inserted space before ## in a backslash-continued macro definition" do
1011
+ file_contents = <<~CONTENTS
1012
+ #define MODULE_DEV_TYPE_CB(dev_type) \\
1013
+ void MODULE_TrigCbDev ##dev_type(void) \\
1014
+ { \\
1015
+ MODULE_CommonTrigCb(); \\
1016
+ }
1017
+
1018
+ static AlertEntry_t s_alert_table[ALERT_MANAGER_MAX_ALERTS];
1019
+ CONTENTS
1020
+
1021
+ contents = extract_from.call(file_contents)
1022
+
1023
+ expect(contents.macro_definitions.length).to eq 1
1024
+ expect(contents.macro_definitions[0].text).to include('MODULE_TrigCbDev##dev_type')
1025
+ expect(contents.macro_definitions[0].text).not_to include('MODULE_TrigCbDev ##dev_type')
1026
+ end
1027
+
957
1028
  end
958
1029
 
959
1030
  end
@@ -336,6 +336,51 @@ describe CExtractorPreprocessing do
336
336
  expect(pos).to eq "#define FOO 1\n".length
337
337
  end
338
338
 
339
+ # --- Regression: GH #1262 — a trailing // comment's apostrophe/quote merges
340
+ # the next directive into this one ---
341
+ # A '\'' or '"' inside an ordinary // comment (e.g. an English contraction
342
+ # like "don't") is not a string/char literal delimiter. Scanned as one
343
+ # anyway, it sends skip_c_string hunting for a closing match straight through
344
+ # this directive's own newline and into whatever follows, silently merging
345
+ # the next #define's text (and losing the newline between them) into this one.
346
+
347
+ it "extracts a #define with a trailing // comment containing an apostrophe" do
348
+ input = "#define START_ADDRESS 0x00 // don't change this\n"
349
+ result, pos = try_directive(input)
350
+ expect(result).to eq [true, input.rstrip]
351
+ expect(pos).to eq input.length
352
+ end
353
+
354
+ it "does not consume a following #define after a // comment containing an apostrophe" do
355
+ input = "#define START_ADDRESS 0x00 // don't change this\n#define IDX 1\n"
356
+ result, pos = try_directive(input)
357
+ expect(result).to eq [true, "#define START_ADDRESS 0x00 // don't change this"]
358
+ expect(pos).to eq "#define START_ADDRESS 0x00 // don't change this\n".length
359
+ end
360
+
361
+ it "does not consume a following #define after a // comment containing a double quote" do
362
+ input = %(#define LABEL "unbalanced) + %( quote in a comment: "\n#define IDX 1\n)
363
+ result, pos = try_directive(input)
364
+ expect(result[0]).to eq true
365
+ expect(result[1]).to start_with('#define LABEL')
366
+ expect(pos).to be < input.length
367
+ expect(input[pos..]).to eq "#define IDX 1\n"
368
+ end
369
+
370
+ it "extracts a #define with a trailing /* */ comment containing an apostrophe" do
371
+ input = "#define START_ADDRESS 0x00 /* don't change this */\n"
372
+ result, pos = try_directive(input)
373
+ expect(result).to eq [true, input.rstrip]
374
+ expect(pos).to eq input.length
375
+ end
376
+
377
+ it "carries a directive across physical lines when a // comment's own line ends in a backslash" do
378
+ input = "#define MACRO(x) \\\n // comment continues \\\n do_thing(x);\n"
379
+ result, pos = try_directive(input)
380
+ expect(result).to eq [true, input.rstrip]
381
+ expect(pos).to eq input.length
382
+ end
383
+
339
384
  it "leaves scanner position unchanged on failure" do
340
385
  scanner = StringScanner.new("int x;")
341
386
  scanner.pos = 0
@@ -343,6 +388,83 @@ describe CExtractorPreprocessing do
343
388
  expect(scanner.pos).to eq 0
344
389
  end
345
390
 
391
+ context "whitespace around # and ## operators" do
392
+ it "trims a space before ##" do
393
+ result, _pos = try_directive("#define M(x) void f ##x(void) { g(); }\n")
394
+ expect(result).to eq [true, "#define M(x) void f##x(void) { g(); }"]
395
+ end
396
+
397
+ it "trims a space after ##" do
398
+ result, _pos = try_directive("#define M(x) void f## x(void) { g(); }\n")
399
+ expect(result).to eq [true, "#define M(x) void f##x(void) { g(); }"]
400
+ end
401
+
402
+ it "trims space on both sides of ##" do
403
+ result, _pos = try_directive("#define M(x) void f ## x(void) { g(); }\n")
404
+ expect(result).to eq [true, "#define M(x) void f##x(void) { g(); }"]
405
+ end
406
+
407
+ it "trims a space after the stringize operator #" do
408
+ result, _pos = try_directive("#define S(x) # x\n")
409
+ expect(result).to eq [true, "#define S(x) #x"]
410
+ end
411
+
412
+ it "trims a space before a stringize operator directly gluing onto a preceding identifier" do
413
+ result, _pos = try_directive("#define M(a, b) a #b\n")
414
+ expect(result).to eq [true, "#define M(a, b) a#b"]
415
+ end
416
+
417
+ it "does not touch whitespace before # or ## when preceded by punctuation, not an identifier being pasted" do
418
+ # The space here separates the macro's own parameter list from a
419
+ # replacement list that happens to start with the operator -- that's
420
+ # ordinary formatting, not preprocessor-reconstruction spacing around an
421
+ # operand being pasted, so it's out of this fix's scope.
422
+ result, _pos = try_directive("#define S(x) #x\n")
423
+ expect(result).to eq [true, "#define S(x) #x"]
424
+ end
425
+
426
+ it "normalizes the GNU comma-deletion ,##__VA_ARGS__ form" do
427
+ result, _pos = try_directive("#define LOG(fmt, ...) printf(fmt, ## __VA_ARGS__)\n")
428
+ expect(result).to eq [true, "#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)"]
429
+ end
430
+
431
+ it "is idempotent for already-canonical ## spacing" do
432
+ input = "#define M(x) void f##x(void) { g(); }\n"
433
+ result, _pos = try_directive(input)
434
+ expect(result).to eq [true, input.rstrip]
435
+ end
436
+
437
+ it "is idempotent for already-canonical # spacing" do
438
+ input = "#define S(x) #x\n"
439
+ result, _pos = try_directive(input)
440
+ expect(result).to eq [true, input.rstrip]
441
+ end
442
+
443
+ it "does not touch ## inside a string literal" do
444
+ input = %q{#define M(x) "a ## b"} + "\n"
445
+ result, _pos = try_directive(input)
446
+ expect(result).to eq [true, input.rstrip]
447
+ end
448
+
449
+ it "does not touch ## inside a // comment" do
450
+ input = "#define M(x) x // a ## b\n"
451
+ result, _pos = try_directive(input)
452
+ expect(result).to eq [true, input.rstrip]
453
+ end
454
+
455
+ it "does not touch ## inside a /* */ comment" do
456
+ input = "#define M(x) x /* a ## b */\n"
457
+ result, _pos = try_directive(input)
458
+ expect(result).to eq [true, input.rstrip]
459
+ end
460
+
461
+ it "leaves ordinary whitespace unrelated to # or ## untouched" do
462
+ input = "#define M(x) void f ( ) ;\n"
463
+ result, _pos = try_directive(input)
464
+ expect(result).to eq [true, input.rstrip]
465
+ end
466
+ end
467
+
346
468
  end
347
469
 
348
470
  context "#filter_directive" do