ceedling 1.1.8 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c1edc565bc9fcb24accc9fd0d5001076fe86564cb0acd02c7d1fc092476c2c1
4
- data.tar.gz: 0c5ec0119bc3249824177c6094afd3e3ef9ee7da698b92fed894924d45d83625
3
+ metadata.gz: 28f18f89f99b7e81f68293c994fa85ff25532ca62bd250c2d1c7e351ec3ec7a0
4
+ data.tar.gz: 1252cbea2a5752315c65826f8068313450cf08d0ea63259e1e8a5d422588140c
5
5
  SHA512:
6
- metadata.gz: 6efc0307567637d4019978131f45758d694b97ea93a4a7ada5ff6be8c682d70eff9d8d453de4882cc46344bb1b0aa098dbf7982582a7d7a168818caba2bd7e51
7
- data.tar.gz: 6366fddeaff5572f317efffb161ff64378cae4e6b2dea30aa11f3baf4aac1777611331483d071a080cb9a01aa1e4087409d5314559881ae0a12ce753a1f86311
6
+ metadata.gz: 7aeb58a943b8df79fa31e12694a16019fba66cf798cc899eccb4d1e7b64d0b5fe09e5965f376791381cbc8c0e47a65c0563dbc5a00e1523714e813b8491a9896
7
+ data.tar.gz: e6cf0643c68cd8fa2c96b99de3e2e95ca276b27bf5ecb5cfa41ae9f1371ed55f3675ec5491d585b989629f2f89b0f16fc65f0586daa6ade065fbe903d9208087
data/GIT_COMMIT_SHA CHANGED
@@ -1 +1 @@
1
- 885dc91
1
+ 2209dc2
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Ceedling ![CI](https://github.com/ThrowTheSwitch/Ceedling/workflows/CI/badge.svg)
2
2
  ========
3
3
 
4
- **Ceedling 1.1.8** is the latest and greatest.
4
+ **Ceedling 1.1.9** is the latest and greatest.
5
5
 
6
6
  See [_Release Notes_][release-notes], [_Changelog_](docs/Changelog.md),
7
7
  [_Breaking Changes_][breaking-changes], and [_Known Issues_][known-issues].
data/docs/Changelog.md CHANGED
@@ -10,6 +10,24 @@ This changelog is complemented by three other documents:
10
10
 
11
11
  ---
12
12
 
13
+ # [1.1.9] — 2026-09-20
14
+
15
+ ## 💪 Fixed
16
+
17
+ ### Partials
18
+
19
+ - [#1293](https://github.com/ThrowTheSwitch/Ceedling/issues/1293) Fixed a Partials-generated header occasionally redefining a type already declared by the real header it replaces, causing a `typedef redefinition with different types` compilation error. The triggering condition involved a source file whose real header was reachable both directly and through a macro-included intermediate file allowed by incomplete Partials include directive ordering.
20
+
21
+ ### Preprocessing
22
+
23
+ - Fixed whitespace occasionally inserted by the underlying compiler's preprocessor around a `#` or `##` operator (e.g. `x ##y` becoming `x ## y`) being carried through verbatim into Partials-generated and reconstructed macro definitions. Extracted macro text is now normalized so preprocessing stringize and token-paste operators always sit directly against their operands, regardless of what a given toolchain's preprocessor happens to emit.
24
+
25
+ ## ⚠️ Changed
26
+
27
+ - Updated Unity to latest version.
28
+
29
+ ---
30
+
13
31
  # [1.1.8] — 2026-09-10
14
32
 
15
33
  ## 💪 Fixed
data/docs/KnownIssues.md CHANGED
@@ -22,6 +22,7 @@ Known issues are complemented by three other documents:
22
22
  1. Any path for a C file specified with `TEST_SOURCE_FILE(...)` is in relation to **_project root_** — that is, from where you execute `ceedling` at the command line. If you move source files or change your directory structure, many of your `TEST_SOURCE_FILE(...)` calls may need to be updated. A more flexible and dynamic approach to path handling will come in a future update.
23
23
  1. In certain combinations of conditional preprocessing blocks with dependent symbols defined in another file, Ceedling can silently fail to extract computed includes (e.g. `#include SOME_MACRO()`).
24
24
  1. User includes (`#include "path/user.h"`) from source C files lose their relative path when Partial and mock header files are generated from source. Compilation failures can result from certain uncommon cases involving headers of the same names in different directories and a source file including both such headers.
25
+ 1. The automatic vendor copying of Unity, CMock, and CException into a project's build directory can intermittently fail with a file/directory type error, most often under antivirus/EDR file locking, cloud-sync filter drivers, or two concurrent Ceedling invocations racing the same destination. Deleting `build/` (or reinstalling the gem) and rebuilding works around it.
25
26
  1. The Bullseye code coverage plugin has been temporarily disabled as of 1.0.0. The makers of Bullseye have generously provided a license for development, and the plugin will be available in the next minor release.
26
27
 
27
28
  ---
@@ -246,7 +246,10 @@ class CExtractorPreprocessing
246
246
  # literal run -- it has to be seen and dispatched on its own below, before
247
247
  # any '"' or "'" inside the comment's own text gets mistaken for the start
248
248
  # of a string/char literal (see the // and /* branches for why that matters).
249
- text << (scanner.scan(/[^"'\\\n\/]*/) || '')
249
+ # '#' is excluded the same way, so a #/## operator is seen and dispatched on
250
+ # its own below rather than absorbed into this run along with whatever
251
+ # whitespace happens to surround it (see the ## and # branches for why).
252
+ text << (scanner.scan(/[^"'\\\n\/#]*/) || '')
250
253
 
251
254
  if scanner.scan(%r{//})
252
255
  # A trailing line comment (e.g. "// don't change this") can contain an
@@ -280,6 +283,31 @@ class CExtractorPreprocessing
280
283
  elsif scanner.scan(%r{/})
281
284
  text << '/'
282
285
 
286
+ elsif scanner.scan(/##/)
287
+ # The C standard specifies ##'s token-paste semantics but not the exact
288
+ # whitespace a preprocessor emits when reconstructing macro text -- real
289
+ # toolchains can and do disagree here, and Ceedling regenerates this text
290
+ # into partials rather than promising byte-for-byte reproduction of that
291
+ # spacing. Discard whatever immediately follows the operator in the source,
292
+ # so its right-hand operand always glues on directly. The left side only
293
+ # trims when a word character (the operand actually being pasted) is
294
+ # immediately adjacent -- a lookbehind, not a blind rstrip -- so unrelated
295
+ # punctuation spacing that happens to precede the operator (e.g. the space
296
+ # after a macro argument's comma, or after the parameter list's closing
297
+ # paren when the replacement list itself starts with the operator) is left
298
+ # alone; that space was never the preprocessor-reconstruction artifact this
299
+ # is defending against. ## checked before bare '#' below so a real paste
300
+ # operator is never split into two stringize matches.
301
+ text.sub!(/(?<=\w)[ \t]+\z/, '')
302
+ scanner.scan(/[ \t]*/)
303
+ text << '##'
304
+
305
+ elsif scanner.scan(/#/)
306
+ # Same rationale as ## above, for the stringize operator.
307
+ text.sub!(/(?<=\w)[ \t]+\z/, '')
308
+ scanner.scan(/[ \t]*/)
309
+ text << '#'
310
+
283
311
  elsif (ch = scanner.peek(1)) == '"' || ch == "'"
284
312
  before = scanner.pos
285
313
  @c_extractor_code_text.skip_c_string(scanner, ch)
@@ -124,6 +124,10 @@ class FilePathUtils
124
124
  pairs = paths.map { |p| [p, p.gsub('\\', '/').chomp('/')] }
125
125
 
126
126
  # Sort shallowest-first so ancestors are always encountered before their descendants.
127
+ # No tiebreaker for same-depth entries needed: Ruby's sort_by doesn't guarantee a
128
+ # stable order among ties, but two same-depth paths can never be each other's
129
+ # ancestor, so the ancestor-exclusion check below is correct regardless of which
130
+ # order same-depth ties come out in.
127
131
  pairs.sort_by! { |_, normalized| normalized.count('/') }
128
132
 
129
133
  kept = []
@@ -254,8 +254,15 @@ class Includes
254
254
  return _includes
255
255
  end
256
256
 
257
+ # Ruby's sort/sort_by family does not guarantee a stable sort -- the relative order
258
+ # of elements a comparator treats as equal (every UserInclude here, all mapping to
259
+ # the same "1") is free to come out differently across Ruby versions and platforms.
260
+ # partition sidesteps the question entirely: it's a simple filter, not a comparator
261
+ # sort, so each group's own original relative order is preserved by definition, on
262
+ # every Ruby version and platform, with nothing to be unstable about.
257
263
  def self.sort!(includes)
258
- includes.sort_by! { |include| include.is_a?(SystemInclude) ? 0 : 1 }
264
+ system, other = includes.partition { |include| include.is_a?(SystemInclude) }
265
+ includes.replace(system + other)
259
266
  return includes
260
267
  end
261
268
  end
@@ -86,20 +86,17 @@ class Partializer
86
86
  def remap_implementation_header_includes(name:, includes:, partials:, types_header: nil, test: nil)
87
87
  _includes = includes.clone()
88
88
 
89
- # Get list of all partialized module names
90
- partialized_modules = partials.keys
89
+ # This module's own header include is spliced out in favor of the shared types
90
+ # header at its exact original list position -- see #splice_in_replacement.
91
+ _includes = splice_in_replacement(includes: _includes, name: name, replacement: types_header)
91
92
 
92
- # Remove includes for all partialized modules
93
- # Remove our own orginal name as well
93
+ # Remove includes for every other partialized module (this module's own name was
94
+ # already handled above)
94
95
  _includes = remove_matching_includes(
95
96
  includes: _includes,
96
- modules: ([name] + partialized_modules)
97
+ modules: (partials.keys - [name])
97
98
  )
98
99
 
99
- # When this module has any typedefs or aggregate definitions, they live in a shared
100
- # header generated once and included here rather than duplicated inline.
101
- _includes << UserInclude.new(types_header) if types_header
102
-
103
100
  # Remove any duplicates
104
101
  Includes.sanitize!(_includes)
105
102
 
@@ -116,9 +113,19 @@ class Partializer
116
113
  def remap_implementation_source_includes(name:, includes:, partials:, test: nil)
117
114
  _includes = includes.clone()
118
115
 
119
- # Add implementation header
120
- _includes << UserInclude.new(
121
- @file_path_utils.form_partial_implementation_header_filename(name)
116
+ # Splice the implementation header in at this module's own header's original list
117
+ # position, same rationale as #splice_in_replacement -- the generated
118
+ # implementation header carries the shared types header in correctly-ordered
119
+ # position internally, but that alone doesn't help if THIS file's own separate
120
+ # include list still reaches an unrelated header that transitively re-includes the
121
+ # real module header before the implementation header (and everything it carries)
122
+ # is ever reached. Appending it at the very end (after this file's own copy of every
123
+ # other real include) would put it after exactly that kind of transitive
124
+ # re-inclusion instead of before it.
125
+ _includes = splice_in_replacement(
126
+ includes: _includes,
127
+ name: name,
128
+ replacement: @file_path_utils.form_partial_implementation_header_filename(name)
122
129
  )
123
130
 
124
131
  mockable_modules = []
@@ -144,11 +151,12 @@ class Partializer
144
151
  end
145
152
  end
146
153
 
147
- # Remove the original module header now that it's remapped to mockable interface
148
- # Remove our own orginal name as well
154
+ # Remove the original headers of any OTHER modules now remapped to mockable
155
+ # interfaces above -- this module's own original header was already handled by
156
+ # the splice above.
149
157
  _includes = remove_matching_includes(
150
158
  includes: _includes,
151
- modules: ([name] + mockable_modules)
159
+ modules: mockable_modules
152
160
  )
153
161
 
154
162
  # Remove any duplicates
@@ -167,20 +175,17 @@ class Partializer
167
175
  def remap_interface_header_includes(name:, includes:, partials:, types_header: nil, test: nil)
168
176
  _includes = includes.clone()
169
177
 
170
- # Get list of all partialized module names
171
- partialized_modules = partials.keys
178
+ # This module's own header include is spliced out in favor of the shared types
179
+ # header at its exact original list position -- see #splice_in_replacement.
180
+ _includes = splice_in_replacement(includes: _includes, name: name, replacement: types_header)
172
181
 
173
- # Remove includes for all partialized modules
174
- # Remove our own orginal name as well
182
+ # Remove includes for every other partialized module (this module's own name was
183
+ # already handled above)
175
184
  _includes = remove_matching_includes(
176
185
  includes: _includes,
177
- modules: ([name] + partialized_modules)
186
+ modules: (partials.keys - [name])
178
187
  )
179
188
 
180
- # When this module has any typedefs or aggregate definitions, they live in a shared
181
- # header generated once and included here rather than duplicated inline.
182
- _includes << UserInclude.new(types_header) if types_header
183
-
184
189
  # Remove any duplicates
185
190
  Includes.sanitize!(_includes)
186
191
 
@@ -452,6 +457,46 @@ class Partializer
452
457
  )
453
458
  end
454
459
 
460
+ # Swaps `name`'s own header include for `replacement` at that same list position,
461
+ # instead of stripping it out and appending the replacement at the very end.
462
+ #
463
+ # Both call sites' generated replacements (the shared types header; the generated
464
+ # implementation header, which itself carries the types header) spoof the real
465
+ # header's own include guard (a top-of-file `#define <ORIGINAL_GUARD>`) so that if the
466
+ # real header is *also* reached a second way -- e.g. a transitively-included,
467
+ # differently-named header that itself does a genuine `#include` of this module's
468
+ # real header -- the real header's guard is already
469
+ # tripped and its content (a second, conflicting copy of the same typedefs/structs)
470
+ # never gets processed. That only works if the spoofing macro is defined *before*
471
+ # such a transitive re-inclusion is reached, not after. Appending at the very end put
472
+ # it after any such re-inclusion instead of before it. Keeping the replacement at the
473
+ # original header's own list position preserves everything the generated content
474
+ # depends on that came before it in the source's own working include order (e.g. a
475
+ # shared Types.h), while still landing ahead of anything transitively re-reaching the
476
+ # real header afterward.
477
+ def splice_in_replacement(includes:, name:, replacement:)
478
+ return remove_matching_includes(includes: includes, modules: [name]) unless replacement
479
+
480
+ replaced = false
481
+ spliced = includes.map do |include|
482
+ if !replaced && include.filename.ext().downcase() == name.downcase()
483
+ replaced = true
484
+ UserInclude.new(replacement)
485
+ else
486
+ include
487
+ end
488
+ end
489
+ # This module's own header wasn't in the includes list at all -- no original
490
+ # position to preserve, so fall back to appending.
491
+ spliced << UserInclude.new(replacement) unless replaced
492
+
493
+ # A duplicate/case-variant entry beyond the first match (e.g. both 'module.h' and
494
+ # 'MODULE.H' present) has no meaningful position of its own to preserve -- drop it
495
+ # like remove_matching_includes always has.
496
+ spliced = remove_matching_includes(includes: spliced, modules: [name])
497
+ spliced
498
+ end
499
+
455
500
  # Remove includes that match the given module names (case-insensitive)
456
501
  # Returns a new array with matching includes removed
457
502
  def remove_matching_includes(includes:, modules:)
data/lib/version.rb CHANGED
@@ -15,7 +15,7 @@
15
15
  module Ceedling
16
16
  module Version
17
17
  # Convenience constants for gem building, etc.
18
- GEM = '1.1.8'
18
+ GEM = '1.1.9'
19
19
  TAG = GEM
20
20
 
21
21
  # If run as a script print Ceedling's version to $stdout
Binary file
@@ -1007,6 +1007,24 @@ describe CExtractor do
1007
1007
  expect(plain.decorators).to eq([])
1008
1008
  end
1009
1009
 
1010
+ it "normalizes a preprocessor-inserted space before ## in a backslash-continued macro definition" do
1011
+ file_contents = <<~CONTENTS
1012
+ #define MODULE_DEV_TYPE_CB(dev_type) \\
1013
+ void MODULE_TrigCbDev ##dev_type(void) \\
1014
+ { \\
1015
+ MODULE_CommonTrigCb(); \\
1016
+ }
1017
+
1018
+ static AlertEntry_t s_alert_table[ALERT_MANAGER_MAX_ALERTS];
1019
+ CONTENTS
1020
+
1021
+ contents = extract_from.call(file_contents)
1022
+
1023
+ expect(contents.macro_definitions.length).to eq 1
1024
+ expect(contents.macro_definitions[0].text).to include('MODULE_TrigCbDev##dev_type')
1025
+ expect(contents.macro_definitions[0].text).not_to include('MODULE_TrigCbDev ##dev_type')
1026
+ end
1027
+
1010
1028
  end
1011
1029
 
1012
1030
  end
@@ -388,6 +388,83 @@ describe CExtractorPreprocessing do
388
388
  expect(scanner.pos).to eq 0
389
389
  end
390
390
 
391
+ context "whitespace around # and ## operators" do
392
+ it "trims a space before ##" do
393
+ result, _pos = try_directive("#define M(x) void f ##x(void) { g(); }\n")
394
+ expect(result).to eq [true, "#define M(x) void f##x(void) { g(); }"]
395
+ end
396
+
397
+ it "trims a space after ##" do
398
+ result, _pos = try_directive("#define M(x) void f## x(void) { g(); }\n")
399
+ expect(result).to eq [true, "#define M(x) void f##x(void) { g(); }"]
400
+ end
401
+
402
+ it "trims space on both sides of ##" do
403
+ result, _pos = try_directive("#define M(x) void f ## x(void) { g(); }\n")
404
+ expect(result).to eq [true, "#define M(x) void f##x(void) { g(); }"]
405
+ end
406
+
407
+ it "trims a space after the stringize operator #" do
408
+ result, _pos = try_directive("#define S(x) # x\n")
409
+ expect(result).to eq [true, "#define S(x) #x"]
410
+ end
411
+
412
+ it "trims a space before a stringize operator directly gluing onto a preceding identifier" do
413
+ result, _pos = try_directive("#define M(a, b) a #b\n")
414
+ expect(result).to eq [true, "#define M(a, b) a#b"]
415
+ end
416
+
417
+ it "does not touch whitespace before # or ## when preceded by punctuation, not an identifier being pasted" do
418
+ # The space here separates the macro's own parameter list from a
419
+ # replacement list that happens to start with the operator -- that's
420
+ # ordinary formatting, not preprocessor-reconstruction spacing around an
421
+ # operand being pasted, so it's out of this fix's scope.
422
+ result, _pos = try_directive("#define S(x) #x\n")
423
+ expect(result).to eq [true, "#define S(x) #x"]
424
+ end
425
+
426
+ it "normalizes the GNU comma-deletion ,##__VA_ARGS__ form" do
427
+ result, _pos = try_directive("#define LOG(fmt, ...) printf(fmt, ## __VA_ARGS__)\n")
428
+ expect(result).to eq [true, "#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)"]
429
+ end
430
+
431
+ it "is idempotent for already-canonical ## spacing" do
432
+ input = "#define M(x) void f##x(void) { g(); }\n"
433
+ result, _pos = try_directive(input)
434
+ expect(result).to eq [true, input.rstrip]
435
+ end
436
+
437
+ it "is idempotent for already-canonical # spacing" do
438
+ input = "#define S(x) #x\n"
439
+ result, _pos = try_directive(input)
440
+ expect(result).to eq [true, input.rstrip]
441
+ end
442
+
443
+ it "does not touch ## inside a string literal" do
444
+ input = %q{#define M(x) "a ## b"} + "\n"
445
+ result, _pos = try_directive(input)
446
+ expect(result).to eq [true, input.rstrip]
447
+ end
448
+
449
+ it "does not touch ## inside a // comment" do
450
+ input = "#define M(x) x // a ## b\n"
451
+ result, _pos = try_directive(input)
452
+ expect(result).to eq [true, input.rstrip]
453
+ end
454
+
455
+ it "does not touch ## inside a /* */ comment" do
456
+ input = "#define M(x) x /* a ## b */\n"
457
+ result, _pos = try_directive(input)
458
+ expect(result).to eq [true, input.rstrip]
459
+ end
460
+
461
+ it "leaves ordinary whitespace unrelated to # or ## untouched" do
462
+ input = "#define M(x) void f ( ) ;\n"
463
+ result, _pos = try_directive(input)
464
+ expect(result).to eq [true, input.rstrip]
465
+ end
466
+ end
467
+
391
468
  end
392
469
 
393
470
  context "#filter_directive" 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
  ###
@@ -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]}"
@@ -0,0 +1,58 @@
1
+ # =========================================================================
2
+ # Unity - A Test Framework for C
3
+ # ThrowTheSwitch.org
4
+ # Copyright (c) 2007-26 Mike Karlesky, Mark VanderVoord, & Greg Williams
5
+ # SPDX-License-Identifier: MIT
6
+ # =========================================================================
7
+
8
+ require 'fileutils'
9
+ require_relative '../../auto/colour_reporter'
10
+
11
+ # the 'test:scripts' rake task tallies these, so only initialize them if we are loaded first
12
+ $generate_test_runner_tests ||= 0
13
+ $generate_test_runner_failures ||= 0
14
+ $unity_test_summary_failures = 0
15
+
16
+ SUMMARY_SCRIPT = File.expand_path('../../auto/unity_test_summary.rb', __dir__)
17
+ SUMMARY_SANDBOX = File.expand_path('../sandbox/test_summary', __dir__)
18
+ SUMMARY_RESULTS = <<~RESULTS
19
+ test/test_a.c:12:test_thing:PASS
20
+ test/test_b.c:34:test_broken:FAIL: Expected 1 Was 2
21
+ test/test_c.c:56:test_skipped:IGNORE: not ready
22
+
23
+ -----------------------
24
+ 3 Tests 1 Failures 1 Ignored
25
+ RESULTS
26
+
27
+ def summary_test(name, passed)
28
+ if passed
29
+ report "#{name}:PASS"
30
+ else
31
+ report "#{name}:FAIL"
32
+ $generate_test_runner_failures += 1
33
+ $unity_test_summary_failures += 1
34
+ end
35
+ $generate_test_runner_tests += 1
36
+ end
37
+
38
+ FileUtils.rm_rf(SUMMARY_SANDBOX)
39
+ FileUtils.mkdir_p(SUMMARY_SANDBOX)
40
+ File.write(File.join(SUMMARY_SANDBOX, 'test_sample.testfail'), SUMMARY_RESULTS)
41
+
42
+ begin
43
+ Dir.chdir(SUMMARY_SANDBOX) do
44
+ # with no arguments at all, the result files are looked for in the current directory
45
+ output = `ruby "#{SUMMARY_SCRIPT}" 2>&1`
46
+ summary_test('UnityTestSummary_DefaultsResultDirectoryToCurrentDirectory',
47
+ $?.success? && output.include?('3 TOTAL TESTS 1 TOTAL FAILURES 1 IGNORED'))
48
+
49
+ # with only a result directory given, the root path defaults to the current directory
50
+ expected = "#{Dir.pwd}/test/test_b.c:34".tr('/', '\\')
51
+ output = `ruby "#{SUMMARY_SCRIPT}" ./ 2>&1`
52
+ summary_test('UnityTestSummary_DefaultsRootPathToCurrentDirectory', output.include?(expected))
53
+ end
54
+ ensure
55
+ FileUtils.rm_rf(SUMMARY_SANDBOX)
56
+ end
57
+
58
+ raise "There were #{$unity_test_summary_failures} failures while testing unity_test_summary.rb" if $unity_test_summary_failures > 0
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.8
4
+ version: 1.1.9
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-10 00:00:00.000000000 Z
13
+ date: 2026-09-20 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rake
@@ -1739,6 +1739,7 @@ files:
1739
1739
  - vendor/unity/test/tests/test_unity_parameterized.c
1740
1740
  - vendor/unity/test/tests/test_unity_parameterizedDemo.c
1741
1741
  - vendor/unity/test/tests/test_unity_strings.c
1742
+ - vendor/unity/test/tests/test_unity_test_summary.rb
1742
1743
  - vendor/unity/test/tests/types_for_test.h
1743
1744
  - vendor/unity/unityConfig.cmake
1744
1745
  homepage: https://throwtheswitch.org/ceedling