ceedling 1.1.6 → 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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/GIT_COMMIT_SHA +1 -1
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +2 -0
  5. data/README.md +4 -1
  6. data/docs/Changelog.md +41 -0
  7. data/docs/KnownIssues.md +3 -0
  8. data/docs/mkdocs/configuration/project-file.md +25 -7
  9. data/docs/mkdocs/configuration/reference/cexception.md +1 -1
  10. data/docs/mkdocs/configuration/reference/cmock.md +1 -1
  11. data/docs/mkdocs/configuration/reference/unity.md +1 -1
  12. data/docs/mkdocs/getting-started/quick-start.md +11 -8
  13. data/docs/mkdocs/plugins/gcov/gcovr.md +329 -189
  14. data/docs/mkdocs/plugins/gcov/reportgenerator.md +113 -31
  15. data/docs/mkdocs/reference/gcov-plugin.md +29 -6
  16. data/docs/mkdocs/snapshot/plugins/gcov/config/defaults.yml +0 -1
  17. data/lib/ceedling/c_extractor/c_extractor_declarations.rb +11 -10
  18. data/lib/ceedling/c_extractor/c_extractor_preprocessing.rb +39 -2
  19. data/lib/ceedling/generators/generator_test_results_backtrace.rb +21 -11
  20. data/lib/ceedling/generators/generator_test_runner.rb +5 -1
  21. data/lib/ceedling/plugins/plugin_reportinator.rb +9 -0
  22. data/lib/ceedling/plugins/report_tests_stdout_plugin.rb +1 -1
  23. data/lib/ceedling/preprocess/preprocessinator_line_marker_includes_extractor.rb +6 -1
  24. data/lib/ceedling/preprocess/preprocessinator_reconstructor.rb +18 -5
  25. data/lib/version.rb +1 -1
  26. data/plugins/gcov/config/defaults.yml +0 -1
  27. data/plugins/gcov/lib/console_reportinator.rb +7 -0
  28. data/plugins/gcov/lib/gcov.rb +64 -26
  29. data/plugins/gcov/lib/gcovr_reportinator.rb +91 -47
  30. data/plugins/gcov/lib/reportgenerator_reportinator.rb +10 -6
  31. data/plugins/valgrind/lib/valgrind.rb +1 -1
  32. data/site-local/configuration/project-file.html +78 -8
  33. data/site-local/configuration/reference/cexception.html +5 -5
  34. data/site-local/configuration/reference/cmock.html +5 -5
  35. data/site-local/configuration/reference/unity.html +5 -5
  36. data/site-local/getting-started/quick-start.html +13 -9
  37. data/site-local/plugins/gcov/gcovr.html +1220 -255
  38. data/site-local/plugins/gcov/reportgenerator.html +404 -46
  39. data/site-local/reference/gcov-plugin.html +48 -10
  40. data/site-local/sitemap.xml.gz +0 -0
  41. data/site-local/snapshot/plugins/gcov/config/defaults.yml +0 -1
  42. data/spec/support/system/system_context.rb +7 -3
  43. data/spec/system/deployment_as_gem_spec.rb +2 -0
  44. data/spec/system/deployment_as_vendor_spec.rb +2 -0
  45. data/spec/system/gcov_deployment_spec.rb +3 -0
  46. data/spec/system/support/common_test_cases.rb +42 -0
  47. data/spec/system/support/gcov_common_test_cases.rb +70 -0
  48. data/spec/units/c_extractor/c_extractor_declarations_spec.rb +16 -0
  49. data/spec/units/c_extractor/c_extractor_integration_spec.rb +53 -0
  50. data/spec/units/c_extractor/c_extractor_preprocessing_spec.rb +45 -0
  51. data/spec/units/generators/generator_partials_spec.rb +98 -0
  52. data/spec/units/generators/generator_test_results_backtrace_spec.rb +77 -0
  53. data/spec/units/generators/generator_test_runner_spec.rb +23 -0
  54. data/spec/units/plugin_reportinator_spec.rb +38 -0
  55. data/spec/units/preprocess/preprocessinator_line_marker_includes_extractor_spec.rb +64 -0
  56. data/spec/units/preprocess/preprocessinator_reconstructor_spec.rb +74 -0
  57. metadata +6 -2
@@ -235,8 +235,11 @@ class SystemContext
235
235
  # encoding regardless of their actual content, so any project source content the
236
236
  # subprocess echoes (verbose logging, compiler output, etc.) can make later regex
237
237
  # matching against this string raise. Sanitize once here, the same way Ceedling itself
238
- # sanitizes file content it scans.
239
- @raw_output = (stdout + stderr).clean_encoding
238
+ # sanitizes file content it scans -- reused for both the raw output and the failure
239
+ # report below, since a real compiler's own diagnostic text (e.g. clang's curly-quoted
240
+ # identifiers) is exactly the kind of non-ASCII content this needs to survive too.
241
+ stdout, stderr = stdout.clean_encoding, stderr.clean_encoding
242
+ @raw_output = stdout + stderr
240
243
  @console_summary = compose_failure_report(stdout, stderr)
241
244
 
242
245
  SystemTestOutput.new(@raw_output)
@@ -250,7 +253,8 @@ class SystemContext
250
253
 
251
254
  @last_cmd = cmd
252
255
  @last_exit_status = status.exitstatus
253
- @raw_output = (stdout + stderr).clean_encoding
256
+ stdout, stderr = stdout.clean_encoding, stderr.clean_encoding
257
+ @raw_output = stdout + stderr
254
258
  @console_summary = compose_failure_report(stdout, stderr)
255
259
 
256
260
  SystemTestOutput.new(@raw_output)
@@ -87,6 +87,8 @@ ceedling_system_tests do
87
87
 
88
88
  describe "Verbosity and output" do
89
89
  test_case :test_project_with_named_verbosity
90
+ test_case :test_project_with_warnings_verbosity_prints_overall_summary
91
+ test_case :test_project_with_errors_verbosity_prints_overall_summary_on_passing_run
90
92
  test_case :test_project_with_numerical_verbosity
91
93
  test_case :report_tests_raw_output_log_plugin
92
94
  end
@@ -90,6 +90,8 @@ ceedling_system_tests do
90
90
 
91
91
  describe "Verbosity and output" do
92
92
  test_case :test_project_with_named_verbosity
93
+ test_case :test_project_with_warnings_verbosity_prints_overall_summary
94
+ test_case :test_project_with_errors_verbosity_prints_overall_summary_on_passing_run
93
95
  test_case :test_project_with_numerical_verbosity
94
96
  test_case :report_tests_raw_output_log_plugin
95
97
  end
@@ -33,8 +33,11 @@ ceedling_system_tests do
33
33
  end
34
34
  end
35
35
 
36
+ test_case :project_with_gcov_enabled_test_all_does_not_require_gcovr
36
37
  test_case :project_with_gcov_success
37
38
  test_case :project_with_gcov_fail
39
+ test_case :project_with_gcov_fail_under_decision
40
+ test_case :project_with_gcov_fail_under_line_rejects_zero
38
41
  test_case :gcov_console_report_with_system_header
39
42
  test_case :gcov_console_report_with_partial
40
43
  # TODO: Restore these tests when the :abort_on_uncovered option is restored in the Gcov plugin
@@ -201,6 +201,48 @@ module CommonSystemTestCases
201
201
  end
202
202
  end
203
203
 
204
+ # The Overall Test Summary must still print at :warnings verbosity
205
+ # (`--verbosity=warnings`, Verbosity::COMPLAIN) on an all-passing run, not just
206
+ # when verbosity is :normal or higher, or when a test fails.
207
+ def test_project_with_warnings_verbosity_prints_overall_summary
208
+ @c.with_context do
209
+ Dir.chdir @proj_name do
210
+ FileUtils.cp test_asset_path("example_file.h"), 'src/'
211
+ FileUtils.cp test_asset_path("example_file.c"), 'src/'
212
+ FileUtils.cp test_asset_path("test_example_file_success.c"), 'test/'
213
+
214
+ output = @c.ceedling_build_exec("--verbosity=warnings")
215
+ expect(@c.last_exit_status).to eq(0)
216
+ expect(output).to match(/OVERALL TEST SUMMARY/)
217
+ expect(output).to match(/TESTED:\s+\d/)
218
+ expect(output).to match(/PASSED:\s+\d/)
219
+ expect(output).to match(/FAILED:\s+\d/)
220
+ expect(output).to match(/IGNORED:\s+\d/)
221
+ end
222
+ end
223
+ end
224
+
225
+ # ERRORS is the true floor for the Overall Test Summary -- a build's own
226
+ # test results are core information, visible even at the most restrictive
227
+ # verbosity short of :silent, whether the run passed or failed.
228
+ def test_project_with_errors_verbosity_prints_overall_summary_on_passing_run
229
+ @c.with_context do
230
+ Dir.chdir @proj_name do
231
+ FileUtils.cp test_asset_path("example_file.h"), 'src/'
232
+ FileUtils.cp test_asset_path("example_file.c"), 'src/'
233
+ FileUtils.cp test_asset_path("test_example_file_success.c"), 'test/'
234
+
235
+ output = @c.ceedling_build_exec("--verbosity=errors")
236
+ expect(@c.last_exit_status).to eq(0)
237
+ expect(output).to match(/OVERALL TEST SUMMARY/)
238
+ expect(output).to match(/TESTED:\s+\d/)
239
+ expect(output).to match(/PASSED:\s+\d/)
240
+ expect(output).to match(/FAILED:\s+\d/)
241
+ expect(output).to match(/IGNORED:\s+\d/)
242
+ end
243
+ end
244
+ end
245
+
204
246
  def test_project_with_numerical_verbosity
205
247
  @c.with_context do
206
248
  Dir.chdir @proj_name do
@@ -10,6 +10,38 @@ require_relative 'gcov_helpers'
10
10
  module GcovCommonTestCases
11
11
  include GcovHelpers
12
12
 
13
+ # #1252 -- Enabling the gcov plugin must not make gcovr a hard dependency of every
14
+ # build. Deliberately does NOT call prep_project_yml_for_coverage (which conditionally
15
+ # strips :utilities ➡️ gcovr when the host lacks a real gcovr) -- the point here is
16
+ # gcovr enabled in config but genuinely unreachable, reproducing #1252 regardless of
17
+ # what's actually installed on the machine running this spec.
18
+ def project_with_gcov_enabled_test_all_does_not_require_gcovr
19
+ @c.with_context do
20
+ Dir.chdir @proj_name do
21
+ @c.uncomment_project_yml_option_for_test("- gcov")
22
+ # Default :gcov ↳ :utilities already includes gcovr and :reports already
23
+ # includes HtmlBasic -- exactly the enabled-but-unused shape #1252 reported.
24
+ @c.merge_project_yml_for_test(
25
+ { :tools => { :gcov_gcovr_report => { :executable => 'nonexistent_gcovr_binary_for_1252_test' } } }
26
+ )
27
+ FileUtils.cp test_asset_path("example_file.h"), 'src/'
28
+ FileUtils.cp test_asset_path("example_file.c"), 'src/'
29
+ FileUtils.cp test_asset_path("test_example_file_success.c"), 'test/'
30
+
31
+ # An ordinary test:all run must succeed -- it never needs gcovr at all.
32
+ output = @c.ceedling_build_exec("test:all")
33
+ expect(@c.last_exit_status).to eq(0)
34
+ expect(output).to_not match(/nonexistent_gcovr_binary_for_1252_test/)
35
+
36
+ # A real gcov: build still needs gcovr and must still fail loudly when it's missing --
37
+ # the fix defers gcovr's tool validation, it doesn't remove it.
38
+ output = @c.ceedling_build_exec("gcov:all")
39
+ expect(@c.last_exit_status).to_not eq(0)
40
+ expect(output).to match(/nonexistent_gcovr_binary_for_1252_test/)
41
+ end
42
+ end
43
+ end
44
+
13
45
  def project_with_gcov_success
14
46
  @c.with_context do
15
47
  Dir.chdir @proj_name do
@@ -46,6 +78,44 @@ module GcovCommonTestCases
46
78
  end
47
79
  end
48
80
 
81
+ # gcovr's own --fail-under-decision is a no-op without --decisions also present --
82
+ # Ceedling supplies --decisions automatically whenever :fail_under_decision is
83
+ # configured, so a passing run reports success instead of gcovr's own opaque
84
+ # "need also option --decision" CLI error.
85
+ def project_with_gcov_fail_under_decision
86
+ @c.with_context do
87
+ Dir.chdir @proj_name do
88
+ prep_project_yml_for_coverage
89
+ @c.merge_project_yml_for_test( { :gcov => { :gcovr => { :fail_under_decision => 1 } } } )
90
+ FileUtils.cp test_asset_path("example_file.h"), 'src/'
91
+ FileUtils.cp test_asset_path("example_file.c"), 'src/'
92
+ FileUtils.cp test_asset_path("test_example_file_success.c"), 'test/'
93
+
94
+ output = @c.ceedling_build_exec("gcov:all")
95
+ expect(@c.last_exit_status).to eq(0)
96
+ expect(output).to_not match(/need also option --decision/)
97
+ end
98
+ end
99
+ end
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
+
49
119
  # TODO: Restore this test when the :abort_on_uncovered option is restored in the Gcov plugin
50
120
  # def project_with_gcov_fail_because_of_uncovered_files
51
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
@@ -130,6 +130,7 @@ SIMPLE_ASSERT_CRASH_OUTPUT =
130
130
  SIMPLE_PASS_OUTPUT = "test_lib.c:3:test_empty:PASS\n---------\n1 Tests 0 Failures 0 Ignored\nOK\n"
131
131
  SIMPLE_FAIL_OUTPUT = "test_lib.c:8:test_bad:FAIL: Expected 1 Was 2\n---------\n1 Tests 1 Failures 0 Ignored\nFAIL\n"
132
132
  SIMPLE_IGNORE_OUTPUT = "test_lib.c:12:test_skip:IGNORE\n---------\n1 Tests 0 Failures 1 Ignored\nOK\n"
133
+ SIMPLE_IGNORE_WITH_MESSAGE_OUTPUT = "test_lib.c:12:test_skip:IGNORE: Not yet implemented\n---------\n1 Tests 0 Failures 1 Ignored\nOK\n"
133
134
 
134
135
 
135
136
  describe GeneratorTestResultsBacktrace do
@@ -255,6 +256,31 @@ describe GeneratorTestResultsBacktrace do
255
256
  expect(expected_output_lines).to include('test_lib.c:9:test_asserting:FAIL: Expected 1 Was 2')
256
257
  end
257
258
 
259
+ # Ignored-crash false positive: a TEST_IGNORE_MESSAGE(...) test case carries a
260
+ # trailing message, same as FAIL. Before this fix, only FAIL tolerated one --
261
+ # a message-carrying IGNORE fell through to "unresolved" and was misreported
262
+ # as crash evidence (a second @file_wrapper.write, for a companion_case that
263
+ # never actually crashed).
264
+ it 'handles an IGNORE test case with a trailing message and does not write a log' do
265
+ allow(@tool_executor).to receive(:exec).and_return(
266
+ { output: GDB_NO_SIGNAL_OUTPUT, time: 0.1, exit_code: 1, stderr: '', status: @ok_status },
267
+ { output: "test_lib.c:9:test_asserting:IGNORE: Not yet implemented\n---------\n1 Tests 0 Failures 1 Ignored\nOK\n",
268
+ time: 0.1, exit_code: 0, stderr: '', status: @ok_status }
269
+ )
270
+
271
+ expect(@file_wrapper).to receive(:write).once.with(%r{test_crashes_elsewhere}, anything, anything)
272
+
273
+ expected_output_lines = []
274
+ allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
275
+ expected_output_lines = kwargs[:output]
276
+ 'regenerated'
277
+ end
278
+
279
+ @backtrace.do_gdb( filename, executable, shell_result, [companion_case] + test_cases, context: :test )
280
+
281
+ expect(expected_output_lines).to include('test_lib.c:9:test_asserting:IGNORE: Not yet implemented')
282
+ end
283
+
258
284
  it "does not trust a matched PASS line when the retry's own real status contradicts it" do
259
285
  # Regression test for issue #1198: a diagnostic retry can print a clean Unity
260
286
  # result line while its own real process outcome says otherwise.
@@ -353,6 +379,32 @@ describe GeneratorTestResultsBacktrace do
353
379
  expect(crash_line).not_to include('failed to extract')
354
380
  end
355
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
+
356
408
  it 'handles a SIGABRT crash from assert() — shows assertion text, no source line' do
357
409
  test_cases_assert = [{ test: 'test_asserting', symbol: 'test_asserting', line_number: 8 }]
358
410
 
@@ -471,6 +523,31 @@ describe GeneratorTestResultsBacktrace do
471
523
  expect(expected_output_lines).to include('test_lib.c:12:test_skip:IGNORE')
472
524
  end
473
525
 
526
+ # Ignored-crash false positive: a TEST_IGNORE_MESSAGE(...) test case carries a
527
+ # trailing message, same as FAIL (handled just above). Before this fix, only FAIL
528
+ # tolerated one -- a message-carrying IGNORE fell through to the "no result line"
529
+ # branch and was misreported as a second crashed test case (test_skip, alongside
530
+ # the real crash attributed to test_crashes_elsewhere).
531
+ it 'handles an IGNORE test case with a trailing message' do
532
+ test_cases_ignore = [{ test: 'test_crashes_elsewhere', line_number: 20 }, { test: 'test_skip', line_number: 12 }]
533
+
534
+ allow(@tool_executor).to receive(:exec).and_return(
535
+ { output: SIMPLE_ASSERT_CRASH_OUTPUT, time: 0.1, exit_code: 134, stderr: '', status: @ok_status },
536
+ { output: SIMPLE_IGNORE_WITH_MESSAGE_OUTPUT, time: 0.02, exit_code: 0, stderr: '', status: @ok_status }
537
+ )
538
+
539
+ expected_output_lines = []
540
+ allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
541
+ expected_output_lines = kwargs[:output]
542
+ 'regenerated'
543
+ end
544
+
545
+ @backtrace.do_simple( filename, executable, shell_result, test_cases_ignore, context: :test )
546
+
547
+ expect(expected_output_lines).to include('test_lib.c:12:test_skip:IGNORE: Not yet implemented')
548
+ expect(expected_output_lines).to_not include(a_string_matching(/test_skip:FAIL/))
549
+ end
550
+
474
551
  it "does not trust a matched PASS line when the retry's own real status contradicts it" do
475
552
  # Regression test for issue #1198: a diagnostic retry can print a clean Unity
476
553
  # result line while its own real process outcome says otherwise.
@@ -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,38 @@
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/constants'
10
+ require 'ceedling/plugins/plugin_reportinator'
11
+
12
+ describe PluginReportinator do
13
+ before(:each) do
14
+ @plugin_reportinator_helper = double('plugin_reportinator_helper')
15
+ @plugin_manager = double('plugin_manager')
16
+ @reportinator = double('reportinator')
17
+ @loginator = double('loginator')
18
+
19
+ @reportinator_plugin = described_class.new(
20
+ {
21
+ :plugin_reportinator_helper => @plugin_reportinator_helper,
22
+ :plugin_manager => @plugin_manager,
23
+ :reportinator => @reportinator,
24
+ :loginator => @loginator
25
+ }
26
+ )
27
+ end
28
+
29
+ # A build's own test results -- pass or fail -- are core information a user
30
+ # silencing routine build chatter still wants to see, so every plugin that
31
+ # prints post-build test results (report_tests_stdout_plugin, gcov, valgrind)
32
+ # shares this one answer instead of each deciding independently.
33
+ describe '#test_results_floor_verbosity' do
34
+ it 'is ERRORS' do
35
+ expect( @reportinator_plugin.test_results_floor_verbosity ).to eq( Verbosity::ERRORS )
36
+ end
37
+ end
38
+ end