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
@@ -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"
@@ -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
@@ -397,6 +397,56 @@ describe GeneratorPartials do
397
397
 
398
398
  expect( buf.string ).to_not include("#define FOO 1")
399
399
  end
400
+
401
+ # #1262: two consecutive typedefs (no macro carry-forward involved at
402
+ # all) had no byte-exact adjacency coverage -- only the macro-before-
403
+ # typedef pending_macros path did.
404
+ it "emits two consecutive typedefs each on their own line" do
405
+ output_path = '/path/to/output'
406
+ name = 'my_module'
407
+ header_filename = 'my_module_types.h'
408
+
409
+ allow(@file_path_utils).to receive(:form_partial_types_header_filename).and_return(header_filename)
410
+
411
+ buf = StringIO.new()
412
+ allow(@file_wrapper).to receive(:open).and_yield(buf)
413
+
414
+ typedef_a = CExtractorTypes::CStatement.new(text: "typedef uint8_t Byte;", line_num: 1)
415
+ typedef_b = CExtractorTypes::CStatement.new(text: "typedef uint16_t Word;", line_num: 2)
416
+
417
+ c_module = CExtractorTypes::CModule.new(
418
+ type_definitions: [typedef_a, typedef_b],
419
+ element_sequence: [typedef_a, typedef_b]
420
+ )
421
+
422
+ @generator.generate_types(name: name, c_module: c_module, output_path: output_path)
423
+
424
+ expect( buf.string ).to include( "typedef uint8_t Byte;\ntypedef uint16_t Word;\n" )
425
+ end
426
+
427
+ # #1262: same gap for two consecutive aggregate definitions.
428
+ it "emits two consecutive aggregate definitions each on their own line" do
429
+ output_path = '/path/to/output'
430
+ name = 'my_module'
431
+ header_filename = 'my_module_types.h'
432
+
433
+ allow(@file_path_utils).to receive(:form_partial_types_header_filename).and_return(header_filename)
434
+
435
+ buf = StringIO.new()
436
+ allow(@file_wrapper).to receive(:open).and_yield(buf)
437
+
438
+ aggregate_a = CExtractorTypes::CStatement.new(text: "struct Point { int x; int y; };", line_num: 1)
439
+ aggregate_b = CExtractorTypes::CStatement.new(text: "struct Color { int r; int g; int b; };", line_num: 2)
440
+
441
+ c_module = CExtractorTypes::CModule.new(
442
+ aggregate_definitions: [aggregate_a, aggregate_b],
443
+ element_sequence: [aggregate_a, aggregate_b]
444
+ )
445
+
446
+ @generator.generate_types(name: name, c_module: c_module, output_path: output_path)
447
+
448
+ expect( buf.string ).to include( "struct Point { int x; int y; };\nstruct Color { int r; int g; int b; };\n" )
449
+ end
400
450
  end
401
451
 
402
452
  context "#generate_header (private method)" do
@@ -555,6 +605,54 @@ describe GeneratorPartials do
555
605
  expect( buf.string.strip() ).to eq file_contents.strip()
556
606
  end
557
607
 
608
+ # #1262: a header with several single-line macros in a row and nothing
609
+ # type-defining after them (so generate_types never runs at all, per its
610
+ # own empty-module guard) is the ordinary, ungoverned case -- every macro
611
+ # here goes through this inline CStatement branch, not generate_types'
612
+ # own carry-forward logic, which already had its own adjacency coverage.
613
+ it "emits three consecutive macros each on their own line, with nothing following them" do
614
+ file_contents = <<~CONTENTS
615
+ #ifndef __CEEDLING_GENERATED_REGS_H__
616
+ #define __CEEDLING_GENERATED_REGS_H__
617
+
618
+ #define START_ADDRESS 0x00
619
+ #define IDX_DATARATE (ADS124S08_REG_ADDR_DATARATE - START_ADDRESS)
620
+ #define IDX_REF (ADS124S08_REG_ADDR_REF - START_ADDRESS)
621
+
622
+ #endif // __CEEDLING_GENERATED_REGS_H__
623
+
624
+ CONTENTS
625
+
626
+ c_module = make_module(
627
+ make_stmt(text: "#define START_ADDRESS 0x00", line_num: 1),
628
+ make_stmt(text: "#define IDX_DATARATE (ADS124S08_REG_ADDR_DATARATE - START_ADDRESS)", line_num: 2),
629
+ make_stmt(text: "#define IDX_REF (ADS124S08_REG_ADDR_REF - START_ADDRESS)", line_num: 3)
630
+ )
631
+
632
+ @generator.send(:generate_header, buf, 'regs', [], [], c_module, false)
633
+ expect( buf.string.strip() ).to eq file_contents.strip()
634
+ end
635
+
636
+ # #1262 regression: the actual reported shape -- one macro's trailing // comment
637
+ # includes an apostrophe. Extraction is what previously merged the macros (see
638
+ # c_extractor specs); this confirms generate_header still emits each item's text,
639
+ # comment included, on its own line once extraction is correct.
640
+ it "emits each macro on its own line even when one has a trailing comment with an apostrophe (GH #1262)" do
641
+ c_module = make_module(
642
+ make_stmt(text: "#define START_ADDRESS 0x00 // don't change this", line_num: 1),
643
+ make_stmt(text: "#define IDX_DATARATE (ADS124S08_REG_ADDR_DATARATE - START_ADDRESS)", line_num: 2),
644
+ make_stmt(text: "#define IDX_REF (ADS124S08_REG_ADDR_REF - START_ADDRESS)", line_num: 3)
645
+ )
646
+
647
+ @generator.send(:generate_header, buf, 'regs', [], [], c_module, false)
648
+
649
+ expect( buf.string ).to include(
650
+ "#define START_ADDRESS 0x00 // don't change this\n" +
651
+ "#define IDX_DATARATE (ADS124S08_REG_ADDR_DATARATE - START_ADDRESS)\n" +
652
+ "#define IDX_REF (ADS124S08_REG_ADDR_REF - START_ADDRESS)\n"
653
+ )
654
+ end
655
+
558
656
  it "should emit macro and variable statements inline while routing typedefs and aggregates to the shared types header" do
559
657
  # One item of each category that can appear in a generated Partial header:
560
658
  # macro_definitions → CStatement emitted as-is, inline
@@ -379,6 +379,32 @@ describe GeneratorTestResultsBacktrace do
379
379
  expect(crash_line).not_to include('failed to extract')
380
380
  end
381
381
 
382
+ # #1266 follow-on: the same unanchored-substring risk class as #1262/#1266, here in the
383
+ # "prefer whichever unresolved member's own symbol is named in the backtrace" search --
384
+ # no \b before the escaped symbol, so a shorter unresolved symbol that's a suffix of a
385
+ # longer one actually named in the crash frame can false-match and steal the attribution.
386
+ it 'attributes a crash to the member whose symbol actually appears in the backtrace, not a shorter symbol that is merely its suffix' do
387
+ test_cases_suffix = [
388
+ { test: 'test_x(1)', symbol: 'foo', line_number: 10 },
389
+ { test: 'test_x(2)', symbol: 'my_foo', line_number: 20 }
390
+ ]
391
+ filename_suffix = 'test_module.c'
392
+
393
+ crash_output = <<~GDB
394
+ Program received signal SIGSEGV, Segmentation fault.
395
+ 0x00005618066ea1fb in my_foo () at test/test_module.c:42
396
+ #0 0x00005618066ea1fb in my_foo () at test/test_module.c:42
397
+ GDB
398
+
399
+ allow(@tool_executor).to receive(:exec)
400
+ .and_return({ output: crash_output, time: 0.5, exit_code: 139, stderr: '', status: @ok_status })
401
+
402
+ expect(@file_wrapper).to receive(:write)
403
+ .with('/build/logs/test/test_module/test_x(2).gdb.log', /=== test_x\(2\) ===/, 'a')
404
+
405
+ @backtrace.do_gdb( filename_suffix, executable, shell_result, test_cases_suffix, context: :test )
406
+ end
407
+
382
408
  it 'handles a SIGABRT crash from assert() — shows assertion text, no source line' do
383
409
  test_cases_assert = [{ test: 'test_asserting', symbol: 'test_asserting', line_number: 8 }]
384
410
 
@@ -86,6 +86,29 @@ describe GeneratorTestRunner do
86
86
 
87
87
  expect( test_cases.first[:line_number] ).to eq( 5 )
88
88
  end
89
+
90
+ # #1266 follow-on: the same unanchored-substring risk class as #1262/#1266. A static
91
+ # helper function between two test cases whose name merely contains a later test's
92
+ # name as a substring (here "test_ab" inside "reset_test_ab_state") must not steal
93
+ # that test's line number, and an unescaped test name must not be treated as a regex.
94
+ it 'does not false-match a test name against an unrelated line that merely contains it as a substring' do
95
+ source = <<~SOURCE
96
+ void test_a(void) {}
97
+ void reset_test_ab_state(void) {}
98
+ void test_ab(void) {}
99
+ SOURCE
100
+
101
+ runner = build_runner( test_file_contents: source, preprocessed_file_contents: source )
102
+ test_cases = [
103
+ { test: 'test_a', line_number: 0 },
104
+ { test: 'test_ab', line_number: 0 }
105
+ ]
106
+
107
+ runner.send( :remap_line_numbers!, test_cases, source )
108
+
109
+ expect( test_cases[0][:line_number] ).to eq( 1 )
110
+ expect( test_cases[1][:line_number] ).to eq( 3 )
111
+ end
89
112
  end
90
113
 
91
114
  describe '#initialize / #test_cases' do
@@ -0,0 +1,64 @@
1
+ # =========================================================================
2
+ # Ceedling - Test-Centered Build System for C
3
+ # ThrowTheSwitch.org
4
+ # Copyright (c) 2010-26 Mike Karlesky, Mark VanderVoord, & Greg Williams
5
+ # SPDX-License-Identifier: MIT
6
+ # =========================================================================
7
+
8
+ require 'spec_helper'
9
+ require 'ceedling/preprocess/preprocessinator_line_marker_includes_extractor'
10
+ require 'ceedling/includes/includes'
11
+
12
+ # Regression coverage for GH #1268 only -- this class otherwise has no unit spec on this
13
+ # branch. Built on #extract_includes_from_string, a StringIO wrapper around the same
14
+ # private #extract_includes every production call eventually reaches.
15
+ describe PreprocessinatorLineMarkerIncludesExtractor do
16
+ before(:each) do
17
+ @include_factory = double('include_factory')
18
+
19
+ allow(@include_factory).to receive(:user_include_from_filepath) do |filepath|
20
+ UserInclude.new(filepath)
21
+ end
22
+ allow(@include_factory).to receive(:system_include_from_filepath) do |filepath|
23
+ SystemInclude.new(filepath)
24
+ end
25
+
26
+ @extractor = described_class.new(
27
+ :include_factory => @include_factory
28
+ )
29
+ end
30
+
31
+ def paths_of(includes)
32
+ includes.map(&:filepath)
33
+ end
34
+
35
+ describe '#extract_includes_from_string' do
36
+ # #1268: GCC's -fdirectives-only output preserves the ORIGINAL indentation of a
37
+ # top-level #include when it replaces that directive with a line marker entering
38
+ # the included file -- an indented ` #include "widget.h"` produces an indented
39
+ # ` # 1 "widget.h" 1`, not the flush-left marker every other marker GCC
40
+ # generates uses. LINE_MARKER_REGEX must recognize that marker too, or the include
41
+ # is silently missing from the extracted list entirely.
42
+ it 'recognizes a line marker that is itself indented (e.g. from an indented #include)' do
43
+ content = <<~OUTPUT
44
+ # 1 "test.c"
45
+ # 1 "widget.h" 1
46
+ OUTPUT
47
+
48
+ includes = @extractor.extract_includes_from_string( content, 'test.c', described_class::USER )
49
+
50
+ expect( paths_of(includes) ).to eq( ['widget.h'] )
51
+ end
52
+
53
+ it 'still recognizes an ordinary, flush-left line marker' do
54
+ content = <<~OUTPUT
55
+ # 1 "test.c"
56
+ # 1 "widget.h" 1
57
+ OUTPUT
58
+
59
+ includes = @extractor.extract_includes_from_string( content, 'test.c', described_class::USER )
60
+
61
+ expect( paths_of(includes) ).to eq( ['widget.h'] )
62
+ end
63
+ end
64
+ end
@@ -211,6 +211,41 @@ describe PreprocessinatorReconstructor do
211
211
  expect( @extractor.extract_file_as_array_from_expansion(input, filepath) ).to eq expected
212
212
  end
213
213
 
214
+ # #1268: GCC's -fdirectives-only output preserves the ORIGINAL indentation of a
215
+ # top-level #include when it replaces that directive with a line marker entering
216
+ # the included file -- e.g. a source line " #include \"Module3.h\"" produces
217
+ # " # 1 \"Module3.h\" 1", not the flush-left "# 1 ..." this method's marker
218
+ # regexes assumed every marker would be. An unrecognized entering-marker leaves
219
+ # `extract` stuck at whatever it already was instead of turning off, so the
220
+ # marker line itself (and everything up to the NEXT recognized marker, e.g. one
221
+ # from a nested system include triggered from inside the ignored file) leaks into
222
+ # the extracted output before the ping-pong finally self-corrects.
223
+ it "does not leak file content when the marker entering an included file is itself indented" do
224
+ filepath = "dir/our_file.c"
225
+
226
+ file_contents = [
227
+ '# 1 "dir/our_file.c"',
228
+ 'void wanted_before(void);',
229
+ ' # 1 "dir/included.h" 1', # indented entering-marker (leading whitespace from source)
230
+ 'int leaked_from_included_h;',
231
+ '# 1 "/usr/include/nested.h" 1 3 4', # a normal, flush-left marker from within included.h
232
+ 'int also_should_not_leak;',
233
+ '# 2 "dir/included.h" 2', # returning from nested.h back into included.h (still not our file)
234
+ 'int still_should_not_leak;',
235
+ '# 3 "dir/our_file.c" 2', # returning to our file
236
+ 'void wanted_after(void);',
237
+ ]
238
+
239
+ expected = [
240
+ 'void wanted_before(void);',
241
+ 'void wanted_after(void);',
242
+ ]
243
+
244
+ input = StringIO.new( file_contents.join( "\n" ) )
245
+
246
+ expect( @extractor.extract_file_as_array_from_expansion( input, filepath ) ).to eq expected
247
+ end
248
+
214
249
  it "should extract text of original file from preprocessed expansion ignoring embedded expansions having similar names" do
215
250
  filepath = "dir1/dir2/our_file.c"
216
251
 
@@ -798,6 +833,45 @@ describe PreprocessinatorReconstructor do
798
833
  expect( @extractor.extract_macro_defs( file_text, '_INCLUDE_GUARD_' ) ).to eq expected
799
834
  end
800
835
 
836
+ # #1266: a plain substring test against the include guard rejected any macro whose
837
+ # name OR value merely contained the guard string, not only the guard macro itself.
838
+ # RTC_HOUR_SECONDS' own name contains the guard "RTC_H", and RTC_DAY_SECONDS'
839
+ # *value* references RTC_HOUR_SECONDS, so its captured text also contains "RTC_H" --
840
+ # both were silently dropped from the reconstructed header, breaking compilation.
841
+ it "does not reject an ordinary macro whose name or value merely contains the include guard as a substring (GH #1266)" do
842
+ file_text = <<~FILE_TEXT
843
+ #ifndef RTC_H
844
+ #define RTC_H
845
+
846
+ #define RTC_MINUTE_SECONDS 60u
847
+ #define RTC_HOUR_SECONDS (60u * RTC_MINUTE_SECONDS)
848
+ #define RTC_DAY_SECONDS (24u * RTC_HOUR_SECONDS)
849
+
850
+ #endif
851
+ FILE_TEXT
852
+
853
+ expected = [
854
+ "#define RTC_MINUTE_SECONDS 60u",
855
+ "#define RTC_HOUR_SECONDS (60u * RTC_MINUTE_SECONDS)",
856
+ "#define RTC_DAY_SECONDS (24u * RTC_HOUR_SECONDS)"
857
+ ]
858
+
859
+ expect( @extractor.extract_macro_defs( file_text, 'RTC_H' ) ).to eq expected
860
+ end
861
+
862
+ it "still rejects the include guard when it appears with trailing whitespace instead of a value" do
863
+ file_text = <<~FILE_TEXT
864
+ #ifndef RTC_H
865
+ #define RTC_H
866
+
867
+ #define RTC_MINUTE_SECONDS 60u
868
+ FILE_TEXT
869
+
870
+ expected = [ "#define RTC_MINUTE_SECONDS 60u" ]
871
+
872
+ expect( @extractor.extract_macro_defs( file_text, 'RTC_H' ) ).to eq expected
873
+ end
874
+
801
875
  end
802
876
 
803
877
  context "#compact_file_from_expansion" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ceedling
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.7
4
+ version: 1.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark VanderVoord
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2026-09-04 00:00:00.000000000 Z
13
+ date: 2026-09-10 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rake
@@ -1004,6 +1004,7 @@ files:
1004
1004
  - spec/units/preprocess/preprocessinator_comment_stripper_spec.rb
1005
1005
  - spec/units/preprocess/preprocessinator_file_assembler_spec.rb
1006
1006
  - spec/units/preprocess/preprocessinator_includes_handler_spec.rb
1007
+ - spec/units/preprocess/preprocessinator_line_marker_includes_extractor_spec.rb
1007
1008
  - spec/units/preprocess/preprocessinator_reconstructor_spec.rb
1008
1009
  - spec/units/preprocess/preprocessinator_spec.rb
1009
1010
  - spec/units/reportinator_spec.rb
@@ -1856,6 +1857,7 @@ test_files:
1856
1857
  - spec/units/preprocess/preprocessinator_comment_stripper_spec.rb
1857
1858
  - spec/units/preprocess/preprocessinator_file_assembler_spec.rb
1858
1859
  - spec/units/preprocess/preprocessinator_includes_handler_spec.rb
1860
+ - spec/units/preprocess/preprocessinator_line_marker_includes_extractor_spec.rb
1859
1861
  - spec/units/preprocess/preprocessinator_reconstructor_spec.rb
1860
1862
  - spec/units/preprocess/preprocessinator_spec.rb
1861
1863
  - spec/units/reportinator_spec.rb