ceedling 1.1.1 → 1.1.2

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/GIT_COMMIT_SHA +1 -1
  3. data/README.md +3 -3
  4. data/assets/broken_uncovered_file.c +13 -0
  5. data/assets/test_example_file_2.c +22 -0
  6. data/assets/tests_with_partials_coverage/src/blanks.c +10 -0
  7. data/assets/tests_with_partials_coverage/src/blanks.h +1 -0
  8. data/assets/tests_with_partials_coverage/src/counter.c +12 -0
  9. data/assets/tests_with_partials_coverage/src/counter.h +1 -0
  10. data/assets/tests_with_partials_coverage/src/decorators.c +11 -0
  11. data/assets/tests_with_partials_coverage/src/decorators.h +4 -0
  12. data/assets/tests_with_partials_coverage/test/test_blanks.c +8 -0
  13. data/assets/tests_with_partials_coverage/test/test_counter.c +9 -0
  14. data/assets/tests_with_partials_coverage/test/test_decorators.c +8 -0
  15. data/docs/Changelog.md +17 -3
  16. data/lib/ceedling/c_extractor/c_extractor_declarations.rb +3 -1
  17. data/lib/ceedling/c_extractor/c_extractor_preprocessing.rb +20 -7
  18. data/lib/ceedling/generators/generator_partials.rb +13 -3
  19. data/lib/ceedling/objects.yml +1 -0
  20. data/lib/ceedling/preprocess/preprocessinator_file_assembler.rb +10 -2
  21. data/lib/ceedling/preprocess/preprocessinator_reconstructor.rb +23 -15
  22. data/lib/version.rb +1 -1
  23. data/site-local/sitemap.xml.gz +0 -0
  24. data/spec/system/gcov_deployment_spec.rb +20 -6
  25. data/spec/system/support/gcov_common_test_cases.rb +31 -45
  26. data/spec/system/support/gcov_partials_test_cases.rb +116 -0
  27. data/spec/units/c_extractor/c_extractor_declarations_spec.rb +128 -0
  28. data/spec/units/c_extractor/c_extractor_definitions_spec.rb +7 -0
  29. data/spec/units/c_extractor/c_extractor_integration_spec.rb +17 -0
  30. data/spec/units/c_extractor/c_extractor_preprocessing_spec.rb +46 -0
  31. data/spec/units/generators/generator_partials_spec.rb +32 -6
  32. data/spec/units/preprocess/preprocessinator_file_assembler_spec.rb +96 -2
  33. data/spec/units/preprocess/preprocessinator_reconstructor_spec.rb +151 -1
  34. metadata +15 -2
@@ -18,8 +18,8 @@ module GcovCommonTestCases
18
18
  FileUtils.cp test_asset_path("example_file.c"), 'src/'
19
19
  FileUtils.cp test_asset_path("test_example_file_success.c"), 'test/'
20
20
 
21
- output = `bundle exec ruby -S ceedling gcov:all 2>&1`
22
- expect($?.exitstatus).to match(0) # Since test cases either pass or are ignored, Ceedling exits with success
21
+ output = @c.ceedling_build_exec("gcov:all")
22
+ expect(@c.last_exit_status).to eq(0) # Since test cases either pass or are ignored, Ceedling exits with success
23
23
  expect(output).to match(/TESTED:\s+\d/)
24
24
  expect(output).to match(/PASSED:\s+\d/)
25
25
  expect(output).to match(/FAILED:\s+\d/)
@@ -36,8 +36,8 @@ module GcovCommonTestCases
36
36
  FileUtils.cp test_asset_path("example_file.c"), 'src/'
37
37
  FileUtils.cp test_asset_path("test_example_file.c"), 'test/'
38
38
 
39
- output = `bundle exec ruby -S ceedling gcov:all 2>&1`
40
- expect($?.exitstatus).to match(1) # Unit test failures => Ceedling exit code 1
39
+ output = @c.ceedling_build_exec("gcov:all")
40
+ expect(@c.last_exit_status).to eq(1) # Unit test failures => Ceedling exit code 1
41
41
  expect(output).to match(/TESTED:\s+\d/)
42
42
  expect(output).to match(/PASSED:\s+\d/)
43
43
  expect(output).to match(/FAILED:\s+\d/)
@@ -122,8 +122,8 @@ module GcovCommonTestCases
122
122
  FileUtils.cp test_asset_path("example_file.c"), 'src/'
123
123
  FileUtils.cp test_asset_path("test_example_file_boom.c"), 'test/'
124
124
 
125
- output = `bundle exec ruby -S ceedling gcov:all 2>&1`
126
- expect($?.exitstatus).to match(1) # Since a test explodes, Ceedling exits with error
125
+ output = @c.ceedling_build_exec("gcov:all")
126
+ expect(@c.last_exit_status).to eq(1) # Since a test explodes, Ceedling exits with error
127
127
  expect(output).to match(/(?:ERROR: Ceedling Failed)|(?:Ceedling could not complete operations because of errors)/)
128
128
  end
129
129
  end
@@ -133,8 +133,8 @@ module GcovCommonTestCases
133
133
  @c.with_context do
134
134
  Dir.chdir @proj_name do
135
135
  prep_project_yml_for_coverage
136
- output = `bundle exec ruby -S ceedling help 2>&1`
137
- expect($?.exitstatus).to match(0)
136
+ output = @c.ceedling_appcmd_exec("help")
137
+ expect(@c.last_exit_status).to eq(0)
138
138
  expect(output).to match(/ceedling gcov:\*/i)
139
139
  expect(output).to match(/ceedling gcov:all/i)
140
140
  end
@@ -149,7 +149,7 @@ module GcovCommonTestCases
149
149
  FileUtils.cp test_asset_path("example_file.c"), 'src/'
150
150
  FileUtils.cp test_asset_path("test_example_file_success.c"), 'test/'
151
151
 
152
- output = `bundle exec ruby -S ceedling gcov:all 2>&1`
152
+ output = @c.ceedling_build_exec("gcov:all")
153
153
  if @gcov_reports.include? :gcovr
154
154
  expect(output).to match(/Generating HTML coverage report in 'build\/artifacts\/gcov\/gcovr\/'\.\.\./)
155
155
  expect(File.exist?('build/artifacts/gcov/gcovr/GcovCoverageResults.html')).to eq true
@@ -170,8 +170,8 @@ module GcovCommonTestCases
170
170
  FileUtils.cp test_asset_path("example_file_with_stdio.c"), 'src/'
171
171
  FileUtils.cp test_asset_path("test_example_file_with_stdio.c"), 'test/'
172
172
 
173
- output = `bundle exec ruby -S ceedling gcov:all 2>&1`
174
- expect($?.exitstatus).to match(0)
173
+ output = @c.ceedling_build_exec("gcov:all")
174
+ expect(@c.last_exit_status).to eq(0)
175
175
  # Console summary must report coverage for the source file, not a warning.
176
176
  # When <stdio.h> is included, gcov may list a system header (e.g. _stdio.h)
177
177
  # as the first File entry — the fix ensures the correct source is found.
@@ -194,8 +194,8 @@ module GcovCommonTestCases
194
194
 
195
195
  @c.merge_project_yml_for_test({ :project => { :use_partials => true } })
196
196
 
197
- output = `bundle exec ruby -S ceedling gcov:all 2>&1`
198
- expect($?.exitstatus).to match(0)
197
+ output = @c.ceedling_build_exec("gcov:all")
198
+ expect(@c.last_exit_status).to eq(0)
199
199
  # Coverage must be reported for the Partial (gcov references the original
200
200
  # module via #line remapping); no spurious "Found no coverage results" warning.
201
201
  expect(output).to match(/Lines executed:/)
@@ -216,18 +216,7 @@ module GcovCommonTestCases
216
216
  # coverage entries: Ceedling builds one executable per test file, and both
217
217
  # link against example_file.c, producing two sets of .gcda coverage data
218
218
  # for the same functions.
219
- File.write('test/test_example_file_2.c', <<~C)
220
- #include "unity.h"
221
- #include "example_file.h"
222
-
223
- void setUp(void) {}
224
- void tearDown(void) {}
225
-
226
- void test_difference_between_two_numbers(void)
227
- {
228
- TEST_ASSERT_EQUAL_INT(0, difference_between_numbers(1, 1));
229
- }
230
- C
219
+ FileUtils.cp test_asset_path("test_example_file_2.c"), 'test/'
231
220
 
232
221
  # 'strict' causes gcovr to exit non-zero when the same function appears in
233
222
  # multiple test executables' coverage data. Ceedling's default is
@@ -246,11 +235,11 @@ module GcovCommonTestCases
246
235
  }
247
236
  })
248
237
 
249
- output = `bundle exec ruby -S ceedling gcov:all --verbosity=obnoxious 2>&1`
238
+ output = @c.ceedling_build_exec("gcov:all --verbosity=obnoxious")
250
239
  if @gcov_reports.include? :gcovr
251
240
  # Config file honored (fix applied): strict mode exits non-zero on duplicate functions.
252
241
  # Config file overridden by Ceedling CLI (bug): merge-use-line-max exits 0.
253
- expect($?.exitstatus).not_to eq(0)
242
+ expect(@c.last_exit_status).not_to eq(0)
254
243
 
255
244
  # No `--exclude` should appear on the gcovr command line when a config file is in use.
256
245
  expect(output).not_to match(/--exclude/)
@@ -269,8 +258,8 @@ module GcovCommonTestCases
269
258
 
270
259
  @c.merge_project_yml_for_test({:project => { :use_backtrace => :gdb }})
271
260
 
272
- output = `bundle exec ruby -S ceedling gcov:all 2>&1`
273
- expect($?.exitstatus).to match(1) # Ceedling should exit with error because of failed test due to crash
261
+ output = @c.ceedling_build_exec("gcov:all")
262
+ expect(@c.last_exit_status).to eq(1) # Ceedling should exit with error because of failed test due to crash
274
263
  expect(output).to match(/crashed/i)
275
264
  expect(output).to match(/Unit test failures/)
276
265
  expect(File.exist?('./build/gcov/results/test_example_file_crash_sigsegv.fail'))
@@ -303,8 +292,8 @@ module GcovCommonTestCases
303
292
 
304
293
  @c.merge_project_yml_for_test({:project => { :use_backtrace => :gdb }})
305
294
 
306
- output = `bundle exec ruby -S ceedling gcov:all --exclude_test_case=test_add_numbers_adds_numbers 2>&1`
307
- expect($?.exitstatus).to match(1) # Ceedling should exit with error because of failed test due to crash
295
+ output = @c.ceedling_build_exec("gcov:all --exclude_test_case=test_add_numbers_adds_numbers")
296
+ expect(@c.last_exit_status).to eq(1) # Ceedling should exit with error because of failed test due to crash
308
297
  expect(output).to match(/Test Case Crashed/i)
309
298
  expect(output).to match(/Unit test failures/)
310
299
  expect(File.exist?('./build/gcov/results/test_example_file_crash_sigsegv.fail'))
@@ -345,8 +334,8 @@ module GcovCommonTestCases
345
334
  updated_test_file.insert(updated_test_file.length(), add_test_case)
346
335
  File.write('test/test_example_file_crash_sigsegv.c', updated_test_file.join("\n"), mode: 'w')
347
336
 
348
- output = `bundle exec ruby -S ceedling gcov:all --exclude_test_case=test_add_numbers_will_fail 2>&1`
349
- expect($?.exitstatus).to match(0)
337
+ output = @c.ceedling_build_exec("gcov:all --exclude_test_case=test_add_numbers_will_fail")
338
+ expect(@c.last_exit_status).to eq(0)
350
339
  expect(File.exist?('./build/gcov/results/test_example_file_crash_sigsegv.pass'))
351
340
  expect(output).to match(/TESTED:\s+2/)
352
341
  expect(output).to match(/PASSED:\s+2/)
@@ -377,8 +366,8 @@ module GcovCommonTestCases
377
366
  # Untested — no test references this source file.
378
367
  FileUtils.cp test_asset_path("uncovered_example_file.c"), 'src/'
379
368
 
380
- output = `bundle exec ruby -S ceedling gcov:all 2>&1`
381
- expect($?.exitstatus).to match(0)
369
+ output = @c.ceedling_build_exec("gcov:all")
370
+ expect(@c.last_exit_status).to eq(0)
382
371
  expect(output).not_to match(/Untested Source Files/)
383
372
  expect(output).not_to match(/Processing Untested Sources/)
384
373
  expect(output).not_to match(/uncovered_example_file/)
@@ -397,8 +386,8 @@ module GcovCommonTestCases
397
386
  # Untested — no test references this source file.
398
387
  FileUtils.cp test_asset_path("uncovered_example_file.c"), 'src/'
399
388
 
400
- output = `bundle exec ruby -S ceedling gcov:all 2>&1`
401
- expect($?.exitstatus).to match(0)
389
+ output = @c.ceedling_build_exec("gcov:all")
390
+ expect(@c.last_exit_status).to eq(0)
402
391
  # Immediate warning-level filepath listing (no compilation attempted).
403
392
  expect(output).to match(/Untested.+not.+coverage report/)
404
393
  expect(output).to match(/uncovered_example_file\.c/)
@@ -421,13 +410,10 @@ module GcovCommonTestCases
421
410
 
422
411
  # Untested source with a syntax error — fails to compile regardless of
423
412
  # any :defines/:flags, deliberately triggering the guidance notice.
424
- File.write('src/broken_uncovered_file.c', <<~C)
425
- int broken_uncovered_function(int a, int b) {
426
- return a + b // Missing semicolon and closing brace: guaranteed compile error.
427
- C
413
+ FileUtils.cp test_asset_path("broken_uncovered_file.c"), 'src/'
428
414
 
429
- output = `bundle exec ruby -S ceedling gcov:all 2>&1`
430
- expect($?.exitstatus).not_to eq(0) # Fail-fast: compile failure fails the build
415
+ output = @c.ceedling_build_exec("gcov:all")
416
+ expect(@c.last_exit_status).not_to eq(0) # Fail-fast: compile failure fails the build
431
417
  expect(output).to match(/Compiling.+with coverage failed/)
432
418
  expect(output).to match(/:untested_sources.*:compile/)
433
419
  # Avoid unicode character matching in log matching, since some shells may not support them.
@@ -450,8 +436,8 @@ module GcovCommonTestCases
450
436
 
451
437
  # Run the standalone task directly — no prior `gcov:all` in this invocation,
452
438
  # so no test fixture is ever built or executed.
453
- output = `bundle exec ruby -S ceedling gcov:untested_sources 2>&1`
454
- expect($?.exitstatus).to match(0)
439
+ output = @c.ceedling_build_exec("gcov:untested_sources")
440
+ expect(@c.last_exit_status).to eq(0)
455
441
  expect(output).not_to match(/TESTED:\s+\d/) # No test build/run occurred
456
442
  expect(File.exist?('build/gcov/out/uncovered_example_file.o')).to eq true
457
443
  end
@@ -0,0 +1,116 @@
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 'rexml/document'
9
+ require_relative 'gcov_helpers'
10
+
11
+ module GcovPartialsTestCases
12
+ include GcovHelpers
13
+
14
+ # Runs `ceedling gcov:all` with Partials + Cobertura XML reporting enabled, then
15
+ # returns { line_number => hits } for `source_relpath` (e.g. 'src/blanks.c').
16
+ def partials_cobertura_hits_by_line(source_relpath)
17
+ @c.merge_project_yml_for_test({
18
+ :project => { :use_partials => true },
19
+ :gcov => { :reports => ['Cobertura'] }
20
+ })
21
+
22
+ output = @c.ceedling_build_exec("gcov:all")
23
+ expect(@c.last_exit_status).to eq(0)
24
+
25
+ cobertura_path = File.join('build', 'artifacts', 'gcov', 'gcovr', 'GcovCoverageCobertura.xml')
26
+ doc = REXML::Document.new(File.read(cobertura_path))
27
+
28
+ doc.elements.to_a("//class[@filename='#{source_relpath}']/lines/line").each_with_object({}) do |el, h|
29
+ h[el.attributes['number'].to_i] = el.attributes['hits'].to_i
30
+ end
31
+ end
32
+
33
+ def gcov_partials_coverage_blank_lines_in_function_body
34
+ @c.with_context do
35
+ Dir.chdir @proj_name do
36
+ prep_project_yml_for_coverage
37
+
38
+ # Multiple consecutive blank lines inside a function body -- the same
39
+ # shape a stripped multi-line comment leaves behind -- sit between the
40
+ # opening brace and the one executed statement. Each function in a
41
+ # generated Partial gets exactly one #line directive, at its own first
42
+ # line; every line after that is trusted to match the original source
43
+ # 1:1. If any lines partway through the body go missing during
44
+ # reconstruction, gcov's line attribution for everything after that
45
+ # point in the function shifts up by the deficit.
46
+ asset_base = test_asset_path("tests_with_partials_coverage")
47
+ FileUtils.cp "#{asset_base}/src/blanks.h", 'src/'
48
+ FileUtils.cp "#{asset_base}/src/blanks.c", 'src/'
49
+ FileUtils.cp "#{asset_base}/test/test_blanks.c", 'test/'
50
+
51
+ hits = partials_cobertura_hits_by_line('src/blanks.c')
52
+
53
+ # `return;` is on source line 5 -- it must show as hit.
54
+ expect(hits[5]).to be > 0
55
+ # The blank lines (3-4) must not appear as executable/hit lines at all.
56
+ expect(hits[3]).to be_nil
57
+ expect(hits[4]).to be_nil
58
+ end
59
+ end
60
+ end
61
+
62
+ def gcov_partials_coverage_decorators_on_own_lines
63
+ @c.with_context do
64
+ Dir.chdir @proj_name do
65
+ prep_project_yml_for_coverage
66
+
67
+ # `static` and `inline` each occupy their own line ahead of the return
68
+ # type. Partials strips both keywords from the front of the extracted
69
+ # function text (per the docs, functions are stripped of `static` and
70
+ # `inline`) and must bump the emitted #line directive forward by
71
+ # exactly the number of lines that stripping removed -- otherwise the
72
+ # whole body, including the blank line below, reports coverage against
73
+ # the wrong original source lines.
74
+ asset_base = test_asset_path("tests_with_partials_coverage")
75
+ FileUtils.cp "#{asset_base}/src/decorators.h", 'src/'
76
+ FileUtils.cp "#{asset_base}/src/decorators.c", 'src/'
77
+ FileUtils.cp "#{asset_base}/test/test_decorators.c", 'test/'
78
+
79
+ hits = partials_cobertura_hits_by_line('src/decorators.c')
80
+
81
+ # `return;` is on source line 7 -- it must show as hit.
82
+ expect(hits[7]).to be > 0
83
+ # The blank line (6) must not appear as an executable/hit line.
84
+ expect(hits[6]).to be_nil
85
+ end
86
+ end
87
+ end
88
+
89
+ def gcov_partials_coverage_function_scope_static_promotion
90
+ @c.with_context do
91
+ Dir.chdir @proj_name do
92
+ prep_project_yml_for_coverage
93
+
94
+ # A function-scope `static` variable is replaced with a no-op and
95
+ # promoted to module scope so the Partial's testable copy of the
96
+ # function keeps the original's persistence semantics (per the docs'
97
+ # Partials creation steps). Confirms that substitution doesn't shift
98
+ # the lines after it out of alignment with their original source line
99
+ # numbers.
100
+ asset_base = test_asset_path("tests_with_partials_coverage")
101
+ FileUtils.cp "#{asset_base}/src/counter.h", 'src/'
102
+ FileUtils.cp "#{asset_base}/src/counter.c", 'src/'
103
+ FileUtils.cp "#{asset_base}/test/test_counter.c", 'test/'
104
+
105
+ hits = partials_cobertura_hits_by_line('src/counter.c')
106
+
107
+ # `count++;` (line 5) and `return count;` (line 7) must both show as
108
+ # hit, correctly separated by the blank lines around them (4 and 6).
109
+ expect(hits[5]).to be > 0
110
+ expect(hits[7]).to be > 0
111
+ expect(hits[4]).to be_nil
112
+ expect(hits[6]).to be_nil
113
+ end
114
+ end
115
+ end
116
+ end
@@ -445,6 +445,58 @@ describe CExtractorDeclarations do
445
445
  end
446
446
  end
447
447
 
448
+ context "escape sequences in string literal initializers" do
449
+ it "extracts pointer variable with escaped quote in string literal initializer" do
450
+ content = 'char* s = "say \"hi\"";'
451
+ success, variable, pos, rest = extract_variable.call(content)
452
+
453
+ expect(success).to be true
454
+ check_single(variable, name: 's', type: 'char*', text: 'char* s = "say \"hi\"";')
455
+ expect(pos).to eq(content.length)
456
+ expect(rest).to eq("")
457
+ end
458
+
459
+ it "extracts pointer variable with escaped backslash in string literal initializer" do
460
+ content = 'char* s = "C:\\\\dir";'
461
+ success, variable, pos, rest = extract_variable.call(content)
462
+
463
+ expect(success).to be true
464
+ check_single(variable, name: 's', type: 'char*', text: 'char* s = "C:\\\\dir";')
465
+ expect(pos).to eq(content.length)
466
+ expect(rest).to eq("")
467
+ end
468
+
469
+ it "extracts pointer variable with escaped newline in string literal initializer" do
470
+ content = 'char* s = "line1\\nline2";'
471
+ success, variable, pos, rest = extract_variable.call(content)
472
+
473
+ expect(success).to be true
474
+ check_single(variable, name: 's', type: 'char*', text: 'char* s = "line1\\nline2";')
475
+ expect(pos).to eq(content.length)
476
+ expect(rest).to eq("")
477
+ end
478
+
479
+ it "extracts pointer variable with hex escape in string literal initializer" do
480
+ content = 'char* s = "\\x41\\x42";'
481
+ success, variable, pos, rest = extract_variable.call(content)
482
+
483
+ expect(success).to be true
484
+ check_single(variable, name: 's', type: 'char*', text: 'char* s = "\\x41\\x42";')
485
+ expect(pos).to eq(content.length)
486
+ expect(rest).to eq("")
487
+ end
488
+
489
+ it "extracts pointer variable with octal escape in string literal initializer" do
490
+ content = 'char* s = "\\101\\102";'
491
+ success, variable, pos, rest = extract_variable.call(content)
492
+
493
+ expect(success).to be true
494
+ check_single(variable, name: 's', type: 'char*', text: 'char* s = "\\101\\102";')
495
+ expect(pos).to eq(content.length)
496
+ expect(rest).to eq("")
497
+ end
498
+ end
499
+
448
500
  context "qualified type declarations" do
449
501
  it "extracts const variable" do
450
502
  content = "const int value;"
@@ -685,6 +737,82 @@ describe CExtractorDeclarations do
685
737
  end
686
738
  end
687
739
 
740
+ context "multiline declarations" do
741
+ it "extracts array declaration whose closing brace and semicolon fall on the final line after multiline data (GH #1182)" do
742
+ content = "static uint8_t arr[10] = {\n 0x00, 0x01, 0x02, 0x03, 0x04,\n 0x05, 0x06, 0x07, 0x08, 0x09\n};"
743
+ success, variable, pos, rest = extract_variable.call(content)
744
+
745
+ expect(success).to be true
746
+ check_single(variable, name: 'arr', type: 'uint8_t', decorators: ['static'],
747
+ text: 'uint8_t arr[10] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09 };',
748
+ array_suffix: '[10]')
749
+ expect(pos).to eq(content.length)
750
+ expect(rest).to eq("")
751
+ end
752
+
753
+ it "extracts nested multi-dimensional array initializer whose closing braces and semicolon fall on the final line" do
754
+ content = "static int matrix[2][3] = {\n {1, 2, 3},\n {4, 5, 6}\n};"
755
+ success, variable, pos, rest = extract_variable.call(content)
756
+
757
+ expect(success).to be true
758
+ check_single(variable, name: 'matrix', type: 'int', decorators: ['static'],
759
+ text: 'int matrix[2][3] = { {1, 2, 3}, {4, 5, 6} };',
760
+ array_suffix: '[2][3]')
761
+ expect(pos).to eq(content.length)
762
+ expect(rest).to eq("")
763
+ end
764
+
765
+ it "expands a compound declaration whose second declarator has a multiline brace initializer" do
766
+ content = "static int a, arr[3] = {\n 1,\n 2,\n 3\n};"
767
+ success, variable, pos, rest = extract_variable.call(content)
768
+
769
+ expect(success).to be true
770
+ expect(variable.length).to eq 2
771
+
772
+ expect(variable[0].name).to eq 'a'
773
+ expect(variable[0].decorators).to eq ['static']
774
+ expect(variable[0].text).to eq 'int a;'
775
+
776
+ expect(variable[1].name).to eq 'arr'
777
+ expect(variable[1].decorators).to eq ['static']
778
+ expect(variable[1].array_suffix).to eq '[3]'
779
+ expect(variable[1].text).to eq 'int arr[3] = { 1, 2, 3 };'
780
+
781
+ expect(pos).to eq(content.length)
782
+ expect(rest).to eq("")
783
+ end
784
+
785
+ it "extracts a declaration with the type and name split across lines and no braces involved" do
786
+ content = "int\n x;"
787
+ success, variable, pos, rest = extract_variable.call(content)
788
+
789
+ expect(success).to be true
790
+ check_single(variable, name: 'x', type: 'int', text: 'int x;')
791
+ expect(pos).to eq(content.length)
792
+ expect(rest).to eq("")
793
+ end
794
+
795
+ it "extracts a declaration whose initializer value falls on the line after the assignment" do
796
+ content = "int x =\n 5;"
797
+ success, variable, pos, rest = extract_variable.call(content)
798
+
799
+ expect(success).to be true
800
+ check_single(variable, name: 'x', type: 'int', text: 'int x = 5;')
801
+ expect(pos).to eq(content.length)
802
+ expect(rest).to eq("")
803
+ end
804
+
805
+ it "extracts a declaration whose terminating semicolon is alone on the line following a bracketed declarator" do
806
+ content = "int arr[10]\n;"
807
+ success, variable, pos, rest = extract_variable.call(content)
808
+
809
+ expect(success).to be true
810
+ check_single(variable, name: 'arr', type: 'int', text: 'int arr[10] ;', array_suffix: '[10]')
811
+ expect(pos).to eq(content.length)
812
+ expect(rest).to eq("")
813
+ end
814
+ end
815
+
688
816
  context "array_suffix field" do
689
817
  it "returns empty string for a scalar variable" do
690
818
  content = "static uint8 s_count;"
@@ -295,6 +295,13 @@ describe CExtractorDefinitions do
295
295
  expect(pos).to eq input.length
296
296
  end
297
297
 
298
+ it "extracts a named struct when the closing '}' and terminating ';' fall on separate lines" do
299
+ input = "struct Point {\n int x;\n}\n;\n"
300
+ result, pos = try_aggregate(input)
301
+ expect(result).to eq [true, input.chomp]
302
+ expect(pos).to eq input.length
303
+ end
304
+
298
305
  it "extracts an anonymous struct (no tag name)" do
299
306
  input = "struct { int x; int y; };\n"
300
307
  result, pos = try_aggregate(input)
@@ -561,6 +561,23 @@ describe CExtractor do
561
561
  expect( contents.variable_declarations.length ).to eq 0
562
562
  end
563
563
 
564
+ it "should extract a function definition following a #define with an escaped character in a string literal (GH #1184)" do
565
+ file_contents = <<~'CONTENTS'
566
+ #include "world.h"
567
+ #define SZ1 sizeof("\n")
568
+ void helloWorld() { return; }
569
+ CONTENTS
570
+
571
+ contents = extract_from.call(file_contents)
572
+
573
+ expect( contents.macro_definitions.length ).to eq 1
574
+ expect( contents.macro_definitions[0].text ).to eq '#define SZ1 sizeof("\n")'
575
+
576
+ expect( contents.function_definitions.length ).to eq 1
577
+ expect( contents.function_definitions[0].name ).to eq 'helloWorld'
578
+ expect( contents.function_definitions[0].body ).to eq '{ return; }'
579
+ end
580
+
564
581
  it "should consume #pragma and #include directives without storing them" do
565
582
  file_contents = <<~CONTENTS
566
583
  #pragma once
@@ -290,6 +290,45 @@ describe CExtractorPreprocessing do
290
290
  expect(pos).to eq input.length
291
291
  end
292
292
 
293
+ # --- Regression: GH #1184 — escape sequences inside a #define string literal ---
294
+ # A lone '\' preceding a string-literal escape (e.g. \n, \", \\) is not a line
295
+ # continuation and must not be mistaken for one, truncating the directive.
296
+
297
+ it "extracts a #define whose string literal contains an escaped newline" do
298
+ input = "#define SZ1 sizeof(\"\\n\")\n"
299
+ result, pos = try_directive(input)
300
+ expect(result).to eq [true, input.rstrip]
301
+ expect(pos).to eq input.length
302
+ end
303
+
304
+ it "extracts a #define whose string literal contains an escaped quote" do
305
+ input = "#define MSG \"say \\\"hi\\\"\"\n"
306
+ result, pos = try_directive(input)
307
+ expect(result).to eq [true, input.rstrip]
308
+ expect(pos).to eq input.length
309
+ end
310
+
311
+ it "extracts a #define whose string literal contains an escaped backslash" do
312
+ input = "#define PATH \"C:\\\\dir\"\n"
313
+ result, pos = try_directive(input)
314
+ expect(result).to eq [true, input.rstrip]
315
+ expect(pos).to eq input.length
316
+ end
317
+
318
+ it "extracts a #define with an escaped character and no trailing newline (EOS)" do
319
+ input = "#define SZ1 sizeof(\"\\n\")"
320
+ result, pos = try_directive(input)
321
+ expect(result).to eq [true, input]
322
+ expect(pos).to eq input.length
323
+ end
324
+
325
+ it "does not consume a following function definition after a #define with an escaped character" do
326
+ input = "#define SZ1 sizeof(\"\\n\")\nvoid helloWorld() { return; }\n"
327
+ result, pos = try_directive(input)
328
+ expect(result).to eq [true, "#define SZ1 sizeof(\"\\n\")"]
329
+ expect(pos).to eq "#define SZ1 sizeof(\"\\n\")\n".length
330
+ end
331
+
293
332
  it "stops at end of directive and does not consume following code" do
294
333
  input = "#define FOO 1\nint x = 0;"
295
334
  result, pos = try_directive(input)
@@ -483,6 +522,13 @@ describe CExtractorPreprocessing do
483
522
  expect(pos).to eq input.length
484
523
  end
485
524
 
525
+ it "handles a static assert whose terminating ';' falls on its own line after the closing ')'" do
526
+ input = "static_assert(sizeof(int) == 4, \"msg\")\n;\n"
527
+ result, pos = try_static_assert(input)
528
+ expect(result[0]).to be true
529
+ expect(pos).to eq input.length
530
+ end
531
+
486
532
  # --- Boundary behaviour ---
487
533
 
488
534
  it "stops at the ';' and does not consume following code" do
@@ -84,12 +84,14 @@ describe GeneratorPartials do
84
84
  header_file_handle = double('header_file_handle')
85
85
  source_file_handle = double('source_file_handle')
86
86
 
87
+ # Binary mode ('wb'): generated files carry embedded content whose line
88
+ # endings must reach disk unchanged, which text mode would not guarantee.
87
89
  allow(@file_wrapper).to receive(:open)
88
- .with(expected_header_filepath, 'w')
90
+ .with(expected_header_filepath, 'wb')
89
91
  .and_yield(header_file_handle)
90
92
 
91
93
  allow(@file_wrapper).to receive(:open)
92
- .with(expected_source_filepath, 'w')
94
+ .with(expected_source_filepath, 'wb')
93
95
  .and_yield(source_file_handle)
94
96
 
95
97
  # Spy on generate_header and generate_source -- allow them to be called but track the calls
@@ -145,8 +147,8 @@ describe GeneratorPartials do
145
147
  expect(@file_path_utils).to have_received(:form_partial_implementation_header_filename).with(name)
146
148
 
147
149
  # Verify file operations
148
- expect(@file_wrapper).to have_received(:open).with(expected_header_filepath, 'w')
149
- expect(@file_wrapper).to have_received(:open).with(expected_source_filepath, 'w')
150
+ expect(@file_wrapper).to have_received(:open).with(expected_header_filepath, 'wb')
151
+ expect(@file_wrapper).to have_received(:open).with(expected_source_filepath, 'wb')
150
152
 
151
153
  # Verify return value is the source filepath
152
154
  expect(result).to eq(expected_source_filepath)
@@ -167,9 +169,11 @@ describe GeneratorPartials do
167
169
  .and_return(header_filename)
168
170
 
169
171
  # Mock FileWrapper.open to yield a file handle
172
+ # Binary mode ('wb'): generated files carry embedded content whose line
173
+ # endings must reach disk unchanged, which text mode would not guarantee.
170
174
  file_handle = double('file_handle')
171
175
  allow(@file_wrapper).to receive(:open)
172
- .with(expected_filepath, 'w')
176
+ .with(expected_filepath, 'wb')
173
177
  .and_yield(file_handle)
174
178
 
175
179
  # Spy on generate_header -- allow it to be called but track the call
@@ -211,7 +215,7 @@ describe GeneratorPartials do
211
215
  expect(@file_path_utils).to have_received(:form_partial_interface_header_filename).with(name)
212
216
 
213
217
  # Verify file operations
214
- expect(@file_wrapper).to have_received(:open).with(expected_filepath, 'w')
218
+ expect(@file_wrapper).to have_received(:open).with(expected_filepath, 'wb')
215
219
 
216
220
  # Verify return value is the header filepath
217
221
  expect(result).to eq(expected_filepath)
@@ -649,6 +653,28 @@ describe GeneratorPartials do
649
653
  expect( buf.string.strip() ).to eq file_contents.strip()
650
654
  end
651
655
 
656
+ it "should pass a code_block's line endings through unchanged, including embedded CRLF" do
657
+ # code_block content is written to the IO handle exactly as given, byte for
658
+ # byte, since the file it ends up in is opened by its caller in binary mode
659
+ # specifically so nothing here alters a line ending already present in it.
660
+ code_block_with_crlf = "void helloBlanks(void) {\r\n\r\n return;\r\n}"
661
+
662
+ defn = Partials.manufacture_function_definition(
663
+ line_num: 2,
664
+ source_filepath: 'src/blanks.c',
665
+ name: 'helloBlanks',
666
+ signature: 'void helloBlanks(void)',
667
+ code_block: code_block_with_crlf
668
+ )
669
+ c_module = make_module(
670
+ CExtractorTypes::CFunctionDefinition.new(name: 'helloBlanks')
671
+ )
672
+
673
+ @generator.send(:generate_source, buf, [], [defn], c_module)
674
+
675
+ expect( buf.string ).to include( code_block_with_crlf )
676
+ end
677
+
652
678
  it "should emit only variable declarations and function definitions from all four element_sequence categories" do
653
679
  # Source generation emits two categories and silently skips two others:
654
680
  # CVariableDeclaration → emitted as-is