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
@@ -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
@@ -412,18 +412,36 @@ describe "Includes sorting" do
412
412
  MockInclude.new("mock_module.h"),
413
413
  UserInclude.new("config.h")
414
414
  ]
415
-
415
+
416
416
  Includes.sort!(includes)
417
-
417
+
418
418
  expect(includes.length).to eq(3)
419
419
  expect(includes[0]).to be_a(UserInclude)
420
420
  expect(includes[1]).to be_a(UserInclude)
421
421
  expect(includes[2]).to be_a(UserInclude)
422
422
  end
423
423
 
424
+ # Ruby's sort/sort_by family does not guarantee a stable sort -- every UserInclude
425
+ # here ties on the same comparator value, so a comparator-sort-based implementation
426
+ # is free to reorder them unpredictably across Ruby versions/platforms even with no
427
+ # actual duplicates present. This is the shape that exposed that gap in practice.
428
+ it "preserves each group's own original relative order exactly, not just which group it ends up in" do
429
+ header1 = UserInclude.new("header1.h")
430
+ header2 = UserInclude.new("header2.h")
431
+ header3 = UserInclude.new("header3.h")
432
+ stdio = SystemInclude.new("stdio.h")
433
+ stdlib = SystemInclude.new("stdlib.h")
434
+
435
+ includes = [header1, stdio, header2, stdlib, header3]
436
+
437
+ result = Includes.sort!(includes)
438
+
439
+ expect(result).to eq([stdio, stdlib, header1, header2, header3])
440
+ end
441
+
424
442
  it "handles empty array" do
425
443
  includes = []
426
-
444
+
427
445
  result = Includes.sort!(includes)
428
446
 
429
447
  expect(result).to eq([])
@@ -547,6 +547,46 @@ describe Partializer do
547
547
 
548
548
  expect(result).to match_array([UserInclude.new('header1.h')])
549
549
  end
550
+
551
+ # The shared types header's own top-of-file guard-spoofing macro must
552
+ # be defined before anything later in the list gets a chance to transitively
553
+ # re-include this module's real header a second, unguarded way -- splicing the
554
+ # types header in at the module's own original position (rather than appending it
555
+ # after everything else) is what guarantees that ordering.
556
+ it "splices the shared types header in at the module's own original list position, not at the end" do
557
+ includes = [
558
+ UserInclude.new('header1.h'),
559
+ UserInclude.new('module.h'),
560
+ UserInclude.new('header2.h')
561
+ ]
562
+ result = @partializer.remap_implementation_header_includes(
563
+ name: 'module',
564
+ includes: includes,
565
+ partials: {},
566
+ types_header: 'ceedling_partial_module_types.h'
567
+ )
568
+
569
+ expect(result).to eq([
570
+ UserInclude.new('header1.h'),
571
+ UserInclude.new('ceedling_partial_module_types.h'),
572
+ UserInclude.new('header2.h')
573
+ ])
574
+ end
575
+
576
+ it "appends the shared types header when the module's own header wasn't in the includes list to begin with" do
577
+ includes = [UserInclude.new('header1.h')]
578
+ result = @partializer.remap_implementation_header_includes(
579
+ name: 'module',
580
+ includes: includes,
581
+ partials: {},
582
+ types_header: 'ceedling_partial_module_types.h'
583
+ )
584
+
585
+ expect(result).to eq([
586
+ UserInclude.new('header1.h'),
587
+ UserInclude.new('ceedling_partial_module_types.h')
588
+ ])
589
+ end
550
590
  end
551
591
 
552
592
  ###
@@ -733,6 +773,28 @@ describe Partializer do
733
773
 
734
774
  expect(result).to match_array([UserInclude.new('header1.h')])
735
775
  end
776
+
777
+ # See the matching example in #remap_implementation_header_includes for the full
778
+ # rationale; the mockable interface header needs the identical ordering guarantee.
779
+ it "splices the shared types header in at the module's own original list position, not at the end" do
780
+ includes = [
781
+ UserInclude.new('header1.h'),
782
+ UserInclude.new('module.h'),
783
+ UserInclude.new('header2.h')
784
+ ]
785
+ result = @partializer.remap_interface_header_includes(
786
+ name: 'module',
787
+ includes: includes,
788
+ partials: {},
789
+ types_header: 'ceedling_partial_module_types.h'
790
+ )
791
+
792
+ expect(result).to eq([
793
+ UserInclude.new('header1.h'),
794
+ UserInclude.new('ceedling_partial_module_types.h'),
795
+ UserInclude.new('header2.h')
796
+ ])
797
+ end
736
798
  end
737
799
 
738
800
  ###
@@ -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
@@ -200,8 +200,10 @@ class UnityTestRunnerGenerator
200
200
  source_lines = source.split("\n")
201
201
  source_index = 0
202
202
  tests_and_line_numbers.size.times do |i|
203
+ # Compile once per test
204
+ name_regex = /(?:^|\s)#{tests_and_line_numbers[i][:test]}(?:\s|\()/
203
205
  source_lines[source_index..].each_with_index do |line, index|
204
- next unless line =~ /\s+#{tests_and_line_numbers[i][:test]}(?:\s|\()/
206
+ next unless line =~ name_regex
205
207
 
206
208
  source_index += index
207
209
  tests_and_line_numbers[i][:line_number] = source_index + 1
@@ -124,7 +124,7 @@ if $0 == __FILE__
124
124
  begin
125
125
  # look in the specified or current directory for result files
126
126
  args[0] ||= './'
127
- targets = "#{ARGV[0].tr('\\', '/')}**/*.test*"
127
+ targets = "#{args[0].tr('\\', '/')}**/*.test*"
128
128
  results = Dir[targets]
129
129
 
130
130
  raise "No *.testpass, *.testfail, or *.testresults files found in '#{targets}'" if results.empty?
@@ -133,7 +133,7 @@ if $0 == __FILE__
133
133
 
134
134
  # set the root path
135
135
  args[1] ||= "#{Dir.pwd}/"
136
- uts.root = ARGV[1]
136
+ uts.root = args[1]
137
137
 
138
138
  # run the summarizer
139
139
  puts uts.run
@@ -13,6 +13,13 @@ Prior to 2008, the project was an internal project and not released to the publi
13
13
 
14
14
  ## Log
15
15
 
16
+ ### Unity 2.7.2
17
+
18
+ Significant Bugfixes:
19
+
20
+ - Default `UNITY_INCLUDE_EXEC_TIME` macros compile as ISO C99, are statement-safe, and accept `-Wsign-conversion` (#838)
21
+ - `unity_test_summary.rb` honors its own default result directory and root path again. @youdie006
22
+
16
23
  ### Unity 2.7.0 (July 2026)
17
24
 
18
25
  New Features:
@@ -488,6 +488,10 @@ Define this to measure and report execution time for each test in the suite. Whe
488
488
  it's best to automatically find a way to determine the time in milliseconds. On most Windows, macos, or
489
489
  Linux environments, this is automatic. If not, you can give Unity more information.
490
490
 
491
+ On Unix and macOS, the default uses POSIX `clock_gettime(CLOCK_MONOTONIC)` when that clock is available.
492
+ If it is not (strict ISO C with no POSIX clocks in `<time.h>`), it falls back to the standard `clock()` function,
493
+ which is also the Windows default.
494
+
491
495
  #### `UNITY_CLOCK_MS`
492
496
 
493
497
  If you're working on a system (embedded or otherwise) which has an accessible millisecond timer. You can
@@ -395,33 +395,34 @@ typedef UNITY_FLOAT_TYPE UNITY_FLOAT;
395
395
  !defined(UNITY_EXEC_TIME_STOP) && \
396
396
  !defined(UNITY_PRINT_EXEC_TIME) && \
397
397
  !defined(UNITY_TIME_TYPE)
398
- /* If none any of these macros are defined then try to provide a default implementation */
398
+ /* If none of these macros are defined then try to provide a default implementation */
399
399
 
400
400
  #if defined(UNITY_CLOCK_MS)
401
401
  /* This is a simple way to get a default implementation on platforms that support getting a millisecond counter */
402
402
  #define UNITY_TIME_TYPE UNITY_UINT
403
403
  #define UNITY_EXEC_TIME_START() Unity.CurrentTestStartTime = UNITY_CLOCK_MS()
404
404
  #define UNITY_EXEC_TIME_STOP() Unity.CurrentTestStopTime = UNITY_CLOCK_MS()
405
- #define UNITY_PRINT_EXEC_TIME() { \
405
+ #define UNITY_PRINT_EXEC_TIME() do { \
406
406
  UNITY_UINT execTimeMs = (Unity.CurrentTestStopTime - Unity.CurrentTestStartTime); \
407
407
  UnityPrint(" ("); \
408
408
  UnityPrintNumberUnsigned(execTimeMs); \
409
409
  UnityPrint(" ms)"); \
410
- }
410
+ } while (0)
411
411
  #elif defined(_WIN32)
412
412
  #include <time.h>
413
413
  #define UNITY_TIME_TYPE clock_t
414
414
  #define UNITY_GET_TIME(t) t = (clock_t)((clock() * 1000) / CLOCKS_PER_SEC)
415
415
  #define UNITY_EXEC_TIME_START() UNITY_GET_TIME(Unity.CurrentTestStartTime)
416
416
  #define UNITY_EXEC_TIME_STOP() UNITY_GET_TIME(Unity.CurrentTestStopTime)
417
- #define UNITY_PRINT_EXEC_TIME() { \
418
- UNITY_UINT execTimeMs = (Unity.CurrentTestStopTime - Unity.CurrentTestStartTime); \
417
+ #define UNITY_PRINT_EXEC_TIME() do { \
418
+ UNITY_UINT execTimeMs = (UNITY_UINT)(Unity.CurrentTestStopTime - Unity.CurrentTestStartTime); \
419
419
  UnityPrint(" ("); \
420
420
  UnityPrintNumberUnsigned(execTimeMs); \
421
421
  UnityPrint(" ms)"); \
422
- }
422
+ } while (0)
423
423
  #elif defined(__unix__) || defined(__APPLE__)
424
424
  #include <time.h>
425
+ #if defined(CLOCK_MONOTONIC)
425
426
  #define UNITY_TIME_TYPE struct timespec
426
427
  #define UNITY_GET_TIME(t) clock_gettime(CLOCK_MONOTONIC, &t)
427
428
  #define UNITY_EXEC_TIME_START() UNITY_GET_TIME(Unity.CurrentTestStartTime)
@@ -432,7 +433,20 @@ typedef UNITY_FLOAT_TYPE UNITY_FLOAT;
432
433
  UnityPrint(" ("); \
433
434
  UnityPrintNumberUnsigned(execTimeMs); \
434
435
  UnityPrint(" ms)"); \
435
- } while(0)
436
+ } while (0)
437
+ #else
438
+ /* CLOCK_MONOTONIC is POSIX, not ISO C. Fall back so -std=c99 still compiles. */
439
+ #define UNITY_TIME_TYPE clock_t
440
+ #define UNITY_GET_TIME(t) t = (clock_t)((clock() * 1000) / CLOCKS_PER_SEC)
441
+ #define UNITY_EXEC_TIME_START() UNITY_GET_TIME(Unity.CurrentTestStartTime)
442
+ #define UNITY_EXEC_TIME_STOP() UNITY_GET_TIME(Unity.CurrentTestStopTime)
443
+ #define UNITY_PRINT_EXEC_TIME() do { \
444
+ UNITY_UINT execTimeMs = (UNITY_UINT)(Unity.CurrentTestStopTime - Unity.CurrentTestStartTime); \
445
+ UnityPrint(" ("); \
446
+ UnityPrintNumberUnsigned(execTimeMs); \
447
+ UnityPrint(" ms)"); \
448
+ } while (0)
449
+ #endif
436
450
  #endif
437
451
  #endif
438
452
  #endif
@@ -43,7 +43,7 @@ TARGET = build/testunity-cov.exe
43
43
  # To generate coverage, call 'make -s', the default target runs.
44
44
  # For verbose output of all the tests, run 'make test'.
45
45
  default: test
46
- .PHONY: default coverage test clean
46
+ .PHONY: default coverage test clean intDetection execTimeDetection
47
47
  coverage: $(SRC1) $(SRC2) $(SRC3) $(SRC4) $(SRC5) $(SRC6) $(SRC7) $(SRC8)
48
48
  cd $(BUILD_DIR) && \
49
49
  $(CC) $(CFLAGS) $(DEFINES) $(foreach i,$(SRC1), ../$i) $(COV_FLAGS) -o ../$(TARGET)
@@ -122,10 +122,17 @@ test: $(SRC1) $(SRC2) $(SRC3) $(SRC4) $(SRC5) $(SRC6) $(SRC7) $(SRC8)
122
122
 
123
123
  # Compile only, for testing that preprocessor detection works
124
124
  UNITY_C_ONLY =-c ../src/unity.c -o $(BUILD_DIR)/unity.o
125
- intDetection:
125
+ intDetection: | $(BUILD_DIR)
126
126
  $(CC) $(CFLAGS) $(INC_DIR) $(UNITY_C_ONLY) -D UNITY_EXCLUDE_STDINT_H
127
127
  $(CC) $(CFLAGS) $(INC_DIR) $(UNITY_C_ONLY) -D UNITY_EXCLUDE_LIMITS_H
128
128
 
129
+ # UNITY_INCLUDE_EXEC_TIME is optional and was historically untested, so the
130
+ # default timer macros could rot. Compile them with Unity's own warning flags
131
+ # and with -Wsign-conversion (the failure reported in #838).
132
+ execTimeDetection: | $(BUILD_DIR)
133
+ $(CC) $(CFLAGS) $(INC_DIR) $(UNITY_C_ONLY) -D UNITY_INCLUDE_EXEC_TIME
134
+ $(CC) $(CFLAGS) -Wsign-conversion $(INC_DIR) $(UNITY_C_ONLY) -D UNITY_INCLUDE_EXEC_TIME
135
+
129
136
  $(BUILD_DIR)/test_unity_arraysRunner.c: tests/test_unity_arrays.c | $(BUILD_DIR)
130
137
  awk $(AWK_SCRIPT) tests/test_unity_arrays.c > $@
131
138
 
@@ -407,6 +407,7 @@ module RakefileHelpers
407
407
  combined_output = ''
408
408
  [ "make -s", # test with all defaults
409
409
  "make -s coverage", # test with coverage
410
+ "make -s execTimeDetection", # compile default UNITY_INCLUDE_EXEC_TIME macros
410
411
  "cd #{File.join("..","extras","fixture",'test')} && make -s default noStdlibMalloc",
411
412
  "cd #{File.join("..","extras","fixture",'test')} && make -s C89",
412
413
  "cd #{File.join("..","extras","memory",'test')} && make -s default noStdlibMalloc",
@@ -1334,6 +1334,55 @@ should 'GenerateSuiteTeardownWhenBeginAndEndAreOmitted' do
1334
1334
  $generate_test_runner_tests += 1
1335
1335
  end
1336
1336
 
1337
+ should 'FindTestsLineNumbersWhenNameStartsTheLine' do
1338
+ # The return type may sit on its own line, leaving the test name at column 0.
1339
+ # The line search must still find the definition (and must not scan to the end
1340
+ # of the file for every test, which is what made large files slow).
1341
+ source = "#include \"unity.h\"\n" \
1342
+ "\n" \
1343
+ "void\n" \
1344
+ "test_FirstAtColumnZero(void)\n" \
1345
+ "{\n" \
1346
+ "}\n" \
1347
+ "\n" \
1348
+ "void\n" \
1349
+ "test_SecondAtColumnZero(void)\n" \
1350
+ "{\n" \
1351
+ "}\n"
1352
+ found = UnityTestRunnerGenerator.new({}).find_tests(source).map { |t| [t[:test], t[:line_number]] }
1353
+ expected = [['test_FirstAtColumnZero', 4], ['test_SecondAtColumnZero', 9]]
1354
+ if found == expected
1355
+ report 'Runner_FindTestsLineNumbersWhenNameStartsTheLine:PASS'
1356
+ else
1357
+ report " FAIL: expected #{expected.inspect}, got #{found.inspect}"
1358
+ report 'Runner_FindTestsLineNumbersWhenNameStartsTheLine:FAIL'
1359
+ $generate_test_runner_failures += 1
1360
+ end
1361
+ $generate_test_runner_tests += 1
1362
+ end
1363
+
1364
+ should 'FindTestsLineNumbersWhenOneNameIsAPrefixOfAnother' do
1365
+ # Issue #288: test_my_function must not be located inside
1366
+ # test_my_function_invalid_behavior, which is defined first.
1367
+ source = "void test_my_function_invalid_behavior(void)\n" \
1368
+ "{\n" \
1369
+ "}\n" \
1370
+ "\n" \
1371
+ "void test_my_function(void)\n" \
1372
+ "{\n" \
1373
+ "}\n"
1374
+ found = UnityTestRunnerGenerator.new({}).find_tests(source).map { |t| [t[:test], t[:line_number]] }
1375
+ expected = [['test_my_function_invalid_behavior', 1], ['test_my_function', 5]]
1376
+ if found == expected
1377
+ report 'Runner_FindTestsLineNumbersWhenOneNameIsAPrefixOfAnother:PASS'
1378
+ else
1379
+ report " FAIL: expected #{expected.inspect}, got #{found.inspect}"
1380
+ report 'Runner_FindTestsLineNumbersWhenOneNameIsAPrefixOfAnother:FAIL'
1381
+ $generate_test_runner_failures += 1
1382
+ end
1383
+ $generate_test_runner_tests += 1
1384
+ end
1385
+
1337
1386
  RUNNER_TESTS.each do |testset|
1338
1387
  basename = File.basename(testset[:testfile], C_EXTENSION)
1339
1388
  testset_name = "Runner_#{basename}_#{testset[:name]}"