ceedling 1.1.3 → 1.1.4

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 (165) hide show
  1. checksums.yaml +4 -4
  2. data/GIT_COMMIT_SHA +1 -1
  3. data/README.md +1 -1
  4. data/assets/fake_crash_test_fixture.rb +14 -0
  5. data/assets/partial_all_module.h +11 -0
  6. data/assets/run_with_ubsan.rb +15 -0
  7. data/assets/same_dir_pairing_module.c +4 -0
  8. data/assets/same_dir_pairing_module.h +8 -0
  9. data/assets/same_dir_pairing_module_func.h +6 -0
  10. data/assets/same_dir_pairing_other.c +4 -0
  11. data/assets/same_dir_pairing_other.h +8 -0
  12. data/assets/test_example_file_ub_shift_overflow.c +22 -0
  13. data/assets/test_partial_all_module.c +16 -0
  14. data/assets/test_same_dir_pairing.c +23 -0
  15. data/docs/Changelog.md +21 -1
  16. data/docs/mkdocs/configuration/reference/index.md +8 -0
  17. data/docs/mkdocs/configuration/reference/partials.md +38 -0
  18. data/examples/wondrous_forest/src/LightSensor.c +21 -4
  19. data/examples/wondrous_forest/src/LightSensor.h +12 -4
  20. data/examples/wondrous_forest/test/TestLightSensor.c +34 -4
  21. data/lib/ceedling/c_extractor/c_extractor.rb +37 -11
  22. data/lib/ceedling/c_extractor/c_extractor_constants.rb +0 -3
  23. data/lib/ceedling/c_extractor/c_extractor_declarations.rb +26 -25
  24. data/lib/ceedling/c_extractor/c_extractor_functions.rb +5 -12
  25. data/lib/ceedling/config/configurator.rb +1 -0
  26. data/lib/ceedling/config/configurator_setup.rb +26 -0
  27. data/lib/ceedling/defaults.rb +8 -0
  28. data/lib/ceedling/file_path_utils.rb +7 -0
  29. data/lib/ceedling/file_wrapper.rb +8 -0
  30. data/lib/ceedling/generators/generator.rb +15 -0
  31. data/lib/ceedling/generators/generator_helper.rb +25 -6
  32. data/lib/ceedling/generators/generator_partials.rb +84 -3
  33. data/lib/ceedling/generators/generator_test_results_backtrace.rb +183 -83
  34. data/lib/ceedling/includes/includes.rb +66 -11
  35. data/lib/ceedling/objects.yml +5 -0
  36. data/lib/ceedling/partials/partializer.rb +18 -3
  37. data/lib/ceedling/partials/partializer_utils.rb +6 -1
  38. data/lib/ceedling/preprocess/preprocessinator_includes_handler.rb +43 -16
  39. data/lib/ceedling/test_invoker/test_build_executor.rb +19 -8
  40. data/lib/version.rb +1 -1
  41. data/mkdocs.yml +1 -0
  42. data/site-local/404.html +30 -0
  43. data/site-local/configuration/environment-vars.html +30 -0
  44. data/site-local/configuration/global-collections.html +30 -0
  45. data/site-local/configuration/index.html +30 -0
  46. data/site-local/configuration/loading.html +30 -0
  47. data/site-local/configuration/mixins.html +30 -0
  48. data/site-local/configuration/parallel-builds.html +30 -0
  49. data/site-local/configuration/project-file.html +30 -0
  50. data/site-local/configuration/reference/cexception.html +30 -0
  51. data/site-local/configuration/reference/cmock.html +30 -0
  52. data/site-local/configuration/reference/defines.html +30 -0
  53. data/site-local/configuration/reference/environment.html +30 -0
  54. data/site-local/configuration/reference/extension.html +30 -0
  55. data/site-local/configuration/reference/files.html +30 -0
  56. data/site-local/configuration/reference/flags.html +30 -0
  57. data/site-local/configuration/reference/index.html +36 -0
  58. data/site-local/configuration/reference/libraries.html +30 -0
  59. data/site-local/configuration/reference/mixins.html +30 -0
  60. data/site-local/configuration/reference/partials.html +4257 -0
  61. data/site-local/configuration/reference/paths.html +30 -0
  62. data/site-local/configuration/reference/plugins.html +30 -0
  63. data/site-local/configuration/reference/project.html +30 -0
  64. data/site-local/configuration/reference/release-build.html +31 -1
  65. data/site-local/configuration/reference/test-build.html +31 -1
  66. data/site-local/configuration/reference/test-runner.html +30 -0
  67. data/site-local/configuration/reference/tools.html +30 -0
  68. data/site-local/configuration/reference/unity.html +30 -0
  69. data/site-local/configuration/which-ceedling.html +30 -0
  70. data/site-local/development/index.html +30 -0
  71. data/site-local/development/plugins/configuration.html +30 -0
  72. data/site-local/development/plugins/index.html +30 -0
  73. data/site-local/development/plugins/plugin-subclass.html +30 -0
  74. data/site-local/development/plugins/rake-tasks.html +30 -0
  75. data/site-local/development/workflow.html +30 -0
  76. data/site-local/getting-started/command-line.html +30 -0
  77. data/site-local/getting-started/example-projects/cipher-quest.html +30 -0
  78. data/site-local/getting-started/example-projects/index.html +30 -0
  79. data/site-local/getting-started/example-projects/temp-sensor.html +30 -0
  80. data/site-local/getting-started/example-projects/wondrous-forest.html +30 -0
  81. data/site-local/getting-started/index.html +30 -0
  82. data/site-local/getting-started/installation.html +30 -0
  83. data/site-local/getting-started/quick-start.html +30 -0
  84. data/site-local/help.html +30 -0
  85. data/site-local/index.html +30 -0
  86. data/site-local/overview/build-system.html +30 -0
  87. data/site-local/overview/index.html +30 -0
  88. data/site-local/overview/test-environments.html +30 -0
  89. data/site-local/overview/tools-and-frameworks.html +30 -0
  90. data/site-local/plugins/beep.html +30 -0
  91. data/site-local/plugins/bullseye.html +30 -0
  92. data/site-local/plugins/command-hooks.html +30 -0
  93. data/site-local/plugins/compile-commands-json-db.html +30 -0
  94. data/site-local/plugins/cppcheck.html +30 -0
  95. data/site-local/plugins/dependencies.html +30 -0
  96. data/site-local/plugins/fff.html +30 -0
  97. data/site-local/plugins/gcov/examples.html +30 -0
  98. data/site-local/plugins/gcov/gcovr.html +30 -0
  99. data/site-local/plugins/gcov/index.html +30 -0
  100. data/site-local/plugins/gcov/overview.html +30 -0
  101. data/site-local/plugins/gcov/reportgenerator.html +30 -0
  102. data/site-local/plugins/gcov/reporting.html +30 -0
  103. data/site-local/plugins/gcov/setup.html +30 -0
  104. data/site-local/plugins/gcov/tool-versions.html +30 -0
  105. data/site-local/plugins/gcov/troubleshooting.html +30 -0
  106. data/site-local/plugins/index.html +30 -0
  107. data/site-local/plugins/module-generator.html +30 -0
  108. data/site-local/plugins/report-build-warnings-log.html +30 -0
  109. data/site-local/plugins/report-tests-gtestlike-stdout.html +30 -0
  110. data/site-local/plugins/report-tests-ide-stdout.html +30 -0
  111. data/site-local/plugins/report-tests-log-factory.html +30 -0
  112. data/site-local/plugins/report-tests-pretty-stdout.html +30 -0
  113. data/site-local/plugins/report-tests-raw-output-log.html +30 -0
  114. data/site-local/plugins/report-tests-teamcity-stdout.html +30 -0
  115. data/site-local/plugins/valgrind.html +30 -0
  116. data/site-local/project/release-history.html +30 -0
  117. data/site-local/project/upgrade.html +30 -0
  118. data/site-local/reference/build-directives.html +30 -0
  119. data/site-local/reference/command-line.html +30 -0
  120. data/site-local/reference/environment-vars.html +30 -0
  121. data/site-local/reference/gcov-plugin.html +30 -0
  122. data/site-local/reference/global-collections.html +30 -0
  123. data/site-local/reference/index.html +30 -0
  124. data/site-local/reference/partials-macros.html +30 -0
  125. data/site-local/reference/project-configuration.html +30 -0
  126. data/site-local/sitemap.xml.gz +0 -0
  127. data/site-local/snapshot/examples/cipher_quest/index.html +30 -0
  128. data/site-local/snapshot/examples/temp_sensor/index.html +30 -0
  129. data/site-local/snapshot/examples/wondrous_forest/index.html +30 -0
  130. data/site-local/testing-guide/build-directives.html +30 -0
  131. data/site-local/testing-guide/conventions.html +30 -0
  132. data/site-local/testing-guide/frameworks.html +30 -0
  133. data/site-local/testing-guide/index.html +30 -0
  134. data/site-local/testing-guide/partials/configuration.html +30 -0
  135. data/site-local/testing-guide/partials/conventions.html +30 -0
  136. data/site-local/testing-guide/partials/directives.html +30 -0
  137. data/site-local/testing-guide/partials/example.html +30 -0
  138. data/site-local/testing-guide/partials/index.html +30 -0
  139. data/site-local/testing-guide/partials/overview.html +30 -0
  140. data/site-local/testing-guide/partials/variables.html +30 -0
  141. data/site-local/testing-guide/test-cases.html +30 -0
  142. data/site-local/testing-guide/test-sample.html +30 -0
  143. data/site-local/testing-guide/test-suite-anatomy.html +30 -0
  144. data/spec/support/system/spec_system_helper.rb +25 -0
  145. data/spec/system/deployment_as_gem_spec.rb +12 -0
  146. data/spec/system/deployment_as_vendor_spec.rb +17 -0
  147. data/spec/system/encoding_stress_spec.rb +4 -4
  148. data/spec/system/example_wondrous_forest_spec.rb +2 -2
  149. data/spec/system/support/common_test_cases.rb +137 -0
  150. data/spec/units/c_extractor/c_extractor_declarations_spec.rb +27 -4
  151. data/spec/units/c_extractor/c_extractor_functions_spec.rb +1 -11
  152. data/spec/units/c_extractor/c_extractor_integration_spec.rb +18 -11
  153. data/spec/units/c_extractor/c_extractor_spec.rb +57 -1
  154. data/spec/units/config/configurator_setup_spec.rb +61 -0
  155. data/spec/units/file_path_utils_spec.rb +17 -0
  156. data/spec/units/generators/generator_helper_spec.rb +34 -7
  157. data/spec/units/generators/generator_partials_spec.rb +120 -12
  158. data/spec/units/generators/generator_test_results_backtrace_spec.rb +200 -37
  159. data/spec/units/includes/include_spec.rb +30 -18
  160. data/spec/units/includes/includes_spec.rb +100 -10
  161. data/spec/units/partials/partializer_spec.rb +113 -1
  162. data/spec/units/partials/partializer_utils_spec.rb +11 -0
  163. data/spec/units/preprocess/preprocessinator_includes_handler_spec.rb +96 -7
  164. data/spec/units/system_wrapper_spec.rb +72 -0
  165. metadata +19 -2
@@ -7,7 +7,9 @@
7
7
 
8
8
  require 'spec_helper'
9
9
  require 'ceedling/generators/generator_test_results_backtrace'
10
+ require 'ceedling/generators/generator_helper'
10
11
  require 'ceedling/constants'
12
+ require 'ceedling/exceptions'
11
13
 
12
14
  # ── Representative gdb output constants ──────────────────────────────────────
13
15
 
@@ -125,9 +127,9 @@ SIMPLE_ASSERT_CRASH_OUTPUT =
125
127
  "test_lib.out: src/lib.c:5: asserting: Assertion `0' failed.\n" \
126
128
  "Aborted (core dumped)\n"
127
129
 
128
- SIMPLE_PASS_OUTPUT = "test_lib.c:3:test_empty:PASS\n"
129
- SIMPLE_FAIL_OUTPUT = "test_lib.c:8:test_bad:FAIL: Expected 1 Was 2\n"
130
- SIMPLE_IGNORE_OUTPUT = "test_lib.c:12:test_skip:IGNORE\n"
130
+ SIMPLE_PASS_OUTPUT = "test_lib.c:3:test_empty:PASS\n---------\n1 Tests 0 Failures 0 Ignored\nOK\n"
131
+ SIMPLE_FAIL_OUTPUT = "test_lib.c:8:test_bad:FAIL: Expected 1 Was 2\n---------\n1 Tests 1 Failures 0 Ignored\nFAIL\n"
132
+ SIMPLE_IGNORE_OUTPUT = "test_lib.c:12:test_skip:IGNORE\n---------\n1 Tests 0 Failures 1 Ignored\nOK\n"
131
133
 
132
134
 
133
135
  describe GeneratorTestResultsBacktrace do
@@ -140,16 +142,28 @@ describe GeneratorTestResultsBacktrace do
140
142
  @generator_test_results = instance_double('GeneratorTestResults')
141
143
  @file_path_utils = instance_double('FilePathUtils')
142
144
  @file_wrapper = instance_double('FileWrapper')
145
+ @loginator = double('loginator').as_null_object
146
+ # A real GeneratorHelper, not a double -- these tests exercise the actual crash
147
+ # detection logic a retry's status is checked against, not a stubbed stand-in for it.
148
+ @generator_helper = GeneratorHelper.new({ :loginator => @loginator })
143
149
 
144
150
  @backtrace = described_class.new({
145
151
  :configurator => @configurator,
146
152
  :tool_executor => @tool_executor,
147
153
  :generator_test_results => @generator_test_results,
154
+ :generator_helper => @generator_helper,
148
155
  :file_path_utils => @file_path_utils,
149
- :file_wrapper => @file_wrapper
156
+ :file_wrapper => @file_wrapper,
157
+ :loginator => @loginator
150
158
  })
151
159
  @backtrace.setup()
152
160
 
161
+ # A healthy retry sub-process status -- the common case for most stubs below. Crash
162
+ # scenarios that already resolve via a missing result-line match (the pre-existing
163
+ # "unresolved" path) don't depend on this value at all; it's included on every stub
164
+ # anyway so `crash_result[:status]` is never unexpectedly missing.
165
+ @ok_status = double('status', signaled?: false, success?: true, exitstatus: 0, termsig: nil)
166
+
153
167
  # Standard stubs used by most tests
154
168
  allow(@tool_executor).to receive(:build_command_line).and_return({ options: {} })
155
169
  allow(@file_path_utils).to receive(:form_test_gdb_log) do |test, context:, name:|
@@ -173,37 +187,113 @@ describe GeneratorTestResultsBacktrace do
173
187
  allow(@configurator).to receive(:tools_test_backtrace_gdb).and_return({})
174
188
  end
175
189
 
190
+ # Companion helper: a genuinely-crashing group paired alongside the group under
191
+ # test, matching realistic multi-test-case usage -- do_gdb only ever runs because
192
+ # the main run already crashed, so an isolated group with nothing but a clean match
193
+ # is indistinguishable from the #1198 bug pattern (every retry legitimately clean,
194
+ # contradicting the crash that triggered this method) and correctly falls back on
195
+ # its own, tested separately below.
196
+ let(:companion_case) { { test: 'test_crashes_elsewhere', symbol: 'test_crashes_elsewhere', line_number: 20 } }
197
+
176
198
  it 'handles a PASS test case and does not write a log' do
177
- allow(@tool_executor).to receive(:exec)
178
- .and_return({ output: "test_lib.c:9:test_asserting:PASS\n", time: 0.1, exit_code: 0 })
199
+ allow(@tool_executor).to receive(:exec).and_return(
200
+ { output: GDB_NO_SIGNAL_OUTPUT, time: 0.1, exit_code: 1, stderr: '', status: @ok_status },
201
+ { output: "test_lib.c:9:test_asserting:PASS\n---------\n1 Tests 0 Failures 0 Ignored\nOK\n",
202
+ time: 0.1, exit_code: 0, stderr: '', status: @ok_status }
203
+ )
179
204
 
180
- expect(@file_wrapper).not_to receive(:write)
205
+ expect(@file_wrapper).to receive(:write).once.with(%r{test_crashes_elsewhere}, anything, anything)
181
206
 
182
- result = @backtrace.do_gdb( filename, executable, shell_result, test_cases, context: :test )
207
+ expected_output_lines = []
208
+ allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
209
+ expected_output_lines = kwargs[:output]
210
+ 'regenerated'
211
+ end
183
212
 
184
- expect(result[:exit_code]).to eq(0)
213
+ @backtrace.do_gdb( filename, executable, shell_result, [companion_case] + test_cases, context: :test )
214
+
215
+ expect(expected_output_lines).to include('test_lib.c:9:test_asserting:PASS')
185
216
  end
186
217
 
187
218
  it 'handles an IGNORE test case and does not write a log' do
188
- allow(@tool_executor).to receive(:exec)
189
- .and_return({ output: "test_lib.c:9:test_asserting:IGNORE\n", time: 0.1, exit_code: 0 })
219
+ allow(@tool_executor).to receive(:exec).and_return(
220
+ { output: GDB_NO_SIGNAL_OUTPUT, time: 0.1, exit_code: 1, stderr: '', status: @ok_status },
221
+ { output: "test_lib.c:9:test_asserting:IGNORE\n---------\n1 Tests 0 Failures 1 Ignored\nOK\n",
222
+ time: 0.1, exit_code: 0, stderr: '', status: @ok_status }
223
+ )
190
224
 
191
- expect(@file_wrapper).not_to receive(:write)
225
+ expect(@file_wrapper).to receive(:write).once.with(%r{test_crashes_elsewhere}, anything, anything)
192
226
 
193
- result = @backtrace.do_gdb( filename, executable, shell_result, test_cases, context: :test )
227
+ expected_output_lines = []
228
+ allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
229
+ expected_output_lines = kwargs[:output]
230
+ 'regenerated'
231
+ end
194
232
 
195
- expect(result[:exit_code]).to eq(0)
233
+ @backtrace.do_gdb( filename, executable, shell_result, [companion_case] + test_cases, context: :test )
234
+
235
+ expect(expected_output_lines).to include('test_lib.c:9:test_asserting:IGNORE')
196
236
  end
197
237
 
198
238
  it 'handles a FAIL test case and does not write a log' do
199
- allow(@tool_executor).to receive(:exec)
200
- .and_return({ output: "test_lib.c:9:test_asserting:FAIL: Expected 1 Was 2\n", time: 0.1, exit_code: 1 })
239
+ allow(@tool_executor).to receive(:exec).and_return(
240
+ { output: GDB_NO_SIGNAL_OUTPUT, time: 0.1, exit_code: 1, stderr: '', status: @ok_status },
241
+ { output: "test_lib.c:9:test_asserting:FAIL: Expected 1 Was 2\n---------\n1 Tests 1 Failures 0 Ignored\nFAIL\n",
242
+ time: 0.1, exit_code: 1, stderr: '', status: @ok_status }
243
+ )
201
244
 
202
- expect(@file_wrapper).not_to receive(:write)
245
+ expect(@file_wrapper).to receive(:write).once.with(%r{test_crashes_elsewhere}, anything, anything)
246
+
247
+ expected_output_lines = []
248
+ allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
249
+ expected_output_lines = kwargs[:output]
250
+ 'regenerated'
251
+ end
252
+
253
+ @backtrace.do_gdb( filename, executable, shell_result, [companion_case] + test_cases, context: :test )
254
+
255
+ expect(expected_output_lines).to include('test_lib.c:9:test_asserting:FAIL: Expected 1 Was 2')
256
+ end
257
+
258
+ it "does not trust a matched PASS line when the retry's own real status contradicts it" do
259
+ # Regression test for issue #1198: a diagnostic retry can print a clean Unity
260
+ # result line while its own real process outcome says otherwise.
261
+ bad_status = double('status', signaled?: false, success?: false, exitstatus: 1, termsig: nil)
262
+
263
+ allow(@tool_executor).to receive(:exec).and_return({
264
+ output: "test_lib.c:9:test_asserting:PASS\n---------\n1 Tests 0 Failures 0 Ignored\nOK\n",
265
+ time: 0.1, exit_code: 0, stderr: '', status: bad_status
266
+ })
267
+
268
+ expected_output_lines = []
269
+ allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
270
+ expected_output_lines = kwargs[:output]
271
+ 'regenerated'
272
+ end
203
273
 
204
274
  result = @backtrace.do_gdb( filename, executable, shell_result, test_cases, context: :test )
205
275
 
206
276
  expect(result[:exit_code]).to eq(1)
277
+ expect(expected_output_lines.first).to include(':FAIL:')
278
+ end
279
+
280
+ it 'falls back to a whole-file crash failure when no retry ever reproduces the crash the main run detected' do
281
+ # Regression test for issue #1198: every retry can legitimately pass in its own,
282
+ # differently-configured environment even though the main run genuinely crashed.
283
+ # An all-clean diagnostic must not be allowed to overrule that.
284
+ allow(@tool_executor).to receive(:exec).and_return({
285
+ output: "test_lib.c:9:test_asserting:PASS\n---------\n1 Tests 0 Failures 0 Ignored\nOK\n",
286
+ time: 0.1, exit_code: 0, stderr: '', status: @ok_status
287
+ })
288
+
289
+ fallback_result = { output: 'crash-failure-output', exit_code: 1 }
290
+ expect(@generator_test_results).to receive(:create_crash_failure)
291
+ .with(filename, shell_result, test_cases)
292
+ .and_return(fallback_result)
293
+
294
+ result = @backtrace.do_gdb( filename, executable, shell_result, test_cases, context: :test )
295
+
296
+ expect(result).to eq(fallback_result)
207
297
  end
208
298
 
209
299
  it 'handles a SIGSEGV crash — writes log, includes signal label, backtick source line, and log path' do
@@ -211,7 +301,7 @@ describe GeneratorTestResultsBacktrace do
211
301
  filename_sigsegv = 'TestUsartModel.c'
212
302
 
213
303
  allow(@tool_executor).to receive(:exec)
214
- .and_return({ output: GDB_SIGSEGV_OUTPUT, time: 0.5, exit_code: 139 })
304
+ .and_return({ output: GDB_SIGSEGV_OUTPUT, time: 0.5, exit_code: 139, stderr: '', status: @ok_status })
215
305
 
216
306
  expected_output_lines = []
217
307
  allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
@@ -241,7 +331,7 @@ describe GeneratorTestResultsBacktrace do
241
331
  filename_param = 'test_module_d.c'
242
332
 
243
333
  allow(@tool_executor).to receive(:exec)
244
- .and_return({ output: GDB_SIGSEGV_PARAM_OUTPUT, time: 0.5, exit_code: 139 })
334
+ .and_return({ output: GDB_SIGSEGV_PARAM_OUTPUT, time: 0.5, exit_code: 139, stderr: '', status: @ok_status })
245
335
 
246
336
  expected_output_lines = []
247
337
  allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
@@ -267,7 +357,7 @@ describe GeneratorTestResultsBacktrace do
267
357
  test_cases_assert = [{ test: 'test_asserting', symbol: 'test_asserting', line_number: 8 }]
268
358
 
269
359
  allow(@tool_executor).to receive(:exec)
270
- .and_return({ output: GDB_SIGABRT_ASSERT_OUTPUT, time: 0.3, exit_code: 134 })
360
+ .and_return({ output: GDB_SIGABRT_ASSERT_OUTPUT, time: 0.3, exit_code: 134, stderr: '', status: @ok_status })
271
361
 
272
362
  expected_output_lines = []
273
363
  allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
@@ -288,7 +378,7 @@ describe GeneratorTestResultsBacktrace do
288
378
 
289
379
  it 'handles a crash with no identifiable signal — fallback message, log path without "log: " prefix' do
290
380
  allow(@tool_executor).to receive(:exec)
291
- .and_return({ output: GDB_NO_SIGNAL_OUTPUT, time: 0.1, exit_code: 1 })
381
+ .and_return({ output: GDB_NO_SIGNAL_OUTPUT, time: 0.1, exit_code: 1, stderr: '', status: @ok_status })
292
382
 
293
383
  expected_output_lines = []
294
384
  allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
@@ -317,42 +407,115 @@ describe GeneratorTestResultsBacktrace do
317
407
  allow(@configurator).to receive(:tools_test_fixture_simple_backtrace).and_return({})
318
408
  end
319
409
 
410
+ # Each of the three cases below is paired with a companion group that genuinely
411
+ # crashes -- a realistic multi-test-case file, where the group under test is only
412
+ # one of several. Without a companion, an isolated group with nothing but a clean
413
+ # match is indistinguishable from the #1198 bug pattern (every retry legitimately
414
+ # clean, contradicting the crash do_simple only ever runs because of) and correctly
415
+ # triggers the whole-file fallback tested below in its own right.
416
+
320
417
  it 'handles a PASS test case' do
321
- allow(@tool_executor).to receive(:exec)
322
- .and_return({ output: SIMPLE_PASS_OUTPUT, time: 0.05, exit_code: 0 })
418
+ test_cases_multi = [{ test: 'test_crashes_elsewhere', line_number: 20 }] + test_cases
323
419
 
324
- result = @backtrace.do_simple( filename, executable, shell_result, test_cases, context: :test )
420
+ allow(@tool_executor).to receive(:exec).and_return(
421
+ { output: SIMPLE_ASSERT_CRASH_OUTPUT, time: 0.1, exit_code: 134, stderr: '', status: @ok_status },
422
+ { output: SIMPLE_PASS_OUTPUT, time: 0.05, exit_code: 0, stderr: '', status: @ok_status }
423
+ )
325
424
 
326
- expect(result[:exit_code]).to eq(0)
425
+ expected_output_lines = []
426
+ allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
427
+ expected_output_lines = kwargs[:output]
428
+ 'regenerated'
429
+ end
430
+
431
+ @backtrace.do_simple( filename, executable, shell_result, test_cases_multi, context: :test )
432
+
433
+ expect(expected_output_lines).to include('test_lib.c:3:test_empty:PASS')
327
434
  end
328
435
 
329
436
  it 'handles a FAIL test case with a message' do
330
- test_cases_fail = [{ test: 'test_bad', line_number: 8 }]
437
+ test_cases_fail = [{ test: 'test_crashes_elsewhere', line_number: 20 }, { test: 'test_bad', line_number: 8 }]
331
438
 
332
- allow(@tool_executor).to receive(:exec)
333
- .and_return({ output: SIMPLE_FAIL_OUTPUT, time: 0.05, exit_code: 1 })
439
+ allow(@tool_executor).to receive(:exec).and_return(
440
+ { output: SIMPLE_ASSERT_CRASH_OUTPUT, time: 0.1, exit_code: 134, stderr: '', status: @ok_status },
441
+ { output: SIMPLE_FAIL_OUTPUT, time: 0.05, exit_code: 1, stderr: '', status: @ok_status }
442
+ )
443
+
444
+ expected_output_lines = []
445
+ allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
446
+ expected_output_lines = kwargs[:output]
447
+ 'regenerated'
448
+ end
334
449
 
335
- result = @backtrace.do_simple( filename, executable, shell_result, test_cases_fail, context: :test )
450
+ @backtrace.do_simple( filename, executable, shell_result, test_cases_fail, context: :test )
336
451
 
337
- expect(result[:exit_code]).to eq(1)
452
+ expect(expected_output_lines).to include('test_lib.c:8:test_bad:FAIL: Expected 1 Was 2')
338
453
  end
339
454
 
340
455
  it 'handles an IGNORE test case' do
341
- test_cases_ignore = [{ test: 'test_skip', line_number: 12 }]
456
+ test_cases_ignore = [{ test: 'test_crashes_elsewhere', line_number: 20 }, { test: 'test_skip', line_number: 12 }]
457
+
458
+ allow(@tool_executor).to receive(:exec).and_return(
459
+ { output: SIMPLE_ASSERT_CRASH_OUTPUT, time: 0.1, exit_code: 134, stderr: '', status: @ok_status },
460
+ { output: SIMPLE_IGNORE_OUTPUT, time: 0.02, exit_code: 0, stderr: '', status: @ok_status }
461
+ )
462
+
463
+ expected_output_lines = []
464
+ allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
465
+ expected_output_lines = kwargs[:output]
466
+ 'regenerated'
467
+ end
468
+
469
+ @backtrace.do_simple( filename, executable, shell_result, test_cases_ignore, context: :test )
470
+
471
+ expect(expected_output_lines).to include('test_lib.c:12:test_skip:IGNORE')
472
+ end
473
+
474
+ it "does not trust a matched PASS line when the retry's own real status contradicts it" do
475
+ # Regression test for issue #1198: a diagnostic retry can print a clean Unity
476
+ # result line while its own real process outcome says otherwise.
477
+ bad_status = double('status', signaled?: false, success?: false, exitstatus: 1, termsig: nil)
342
478
 
343
479
  allow(@tool_executor).to receive(:exec)
344
- .and_return({ output: SIMPLE_IGNORE_OUTPUT, time: 0.02, exit_code: 0 })
480
+ .and_return({ output: SIMPLE_PASS_OUTPUT, time: 0.05, exit_code: 0, stderr: '', status: bad_status })
345
481
 
346
- result = @backtrace.do_simple( filename, executable, shell_result, test_cases_ignore, context: :test )
482
+ expected_output_lines = []
483
+ allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
484
+ expected_output_lines = kwargs[:output]
485
+ 'regenerated'
486
+ end
487
+
488
+ result = @backtrace.do_simple( filename, executable, shell_result, test_cases, context: :test )
489
+
490
+ expect(result[:exit_code]).to eq(1)
491
+ expect(expected_output_lines.first).to include(':FAIL:')
492
+ end
493
+
494
+ it 'falls back to a whole-file crash failure when no retry ever reproduces the crash the main run detected' do
495
+ # Regression test for issue #1198: the exact reported scenario -- a custom
496
+ # :test_fixture wrapper enables sanitizer halt-on-error and the main run crashes,
497
+ # but the unrelated, independently-configured test_fixture_simple_backtrace tool
498
+ # runs the same executable directly, without the wrapper, and it legitimately
499
+ # passes. Every retry can be entirely clean and still must not overrule a crash
500
+ # Ceedling already established.
501
+ allow(@tool_executor).to receive(:exec)
502
+ .and_return({ output: SIMPLE_PASS_OUTPUT, time: 0.05, exit_code: 0, stderr: '', status: @ok_status })
503
+
504
+ fallback_result = { output: 'crash-failure-output', exit_code: 1 }
505
+ expect(@generator_test_results).to receive(:create_crash_failure)
506
+ .with(filename, shell_result, test_cases)
507
+ .and_return(fallback_result)
508
+
509
+ result = @backtrace.do_simple( filename, executable, shell_result, test_cases, context: :test )
347
510
 
348
- expect(result[:exit_code]).to eq(0)
511
+ expect(result).to eq(fallback_result)
349
512
  end
350
513
 
351
514
  it 'handles a crash and includes extra executable output in the failure message' do
352
515
  test_cases_crash = [{ test: 'test_asserting', line_number: 8 }]
353
516
 
354
517
  allow(@tool_executor).to receive(:exec)
355
- .and_return({ output: SIMPLE_ASSERT_CRASH_OUTPUT, time: 0.1, exit_code: 134 })
518
+ .and_return({ output: SIMPLE_ASSERT_CRASH_OUTPUT, time: 0.1, exit_code: 134, stderr: '', status: @ok_status })
356
519
 
357
520
  expected_output_lines = []
358
521
  allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
@@ -372,7 +535,7 @@ describe GeneratorTestResultsBacktrace do
372
535
  test_cases_crash = [{ test: 'test_asserting', line_number: 8 }]
373
536
 
374
537
  allow(@tool_executor).to receive(:exec)
375
- .and_return({ output: "\n\n\n", time: 0.1, exit_code: 134 })
538
+ .and_return({ output: "\n\n\n", time: 0.1, exit_code: 134, stderr: '', status: @ok_status })
376
539
 
377
540
  expected_output_lines = []
378
541
  allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
@@ -515,7 +678,7 @@ describe GeneratorTestResultsBacktrace do
515
678
 
516
679
  it 'uses assertion label and metadata line number when no crash location frame is found' do
517
680
  allow(@tool_executor).to receive(:exec)
518
- .and_return({ output: GDB_WINDOWS_ASSERT_BRIEF_OUTPUT, time: 0.1, exit_code: 3 })
681
+ .and_return({ output: GDB_WINDOWS_ASSERT_BRIEF_OUTPUT, time: 0.1, exit_code: 3, stderr: '', status: @ok_status })
519
682
 
520
683
  expected_output_lines = []
521
684
  allow(@generator_test_results).to receive(:regenerate_test_executable_stdout) do |**kwargs|
@@ -30,9 +30,9 @@ describe UserInclude do
30
30
  expect(include_obj).to eq('header.h')
31
31
  end
32
32
 
33
- it "creates a UserInclude with a path and used path in string expansion" do
34
- include_obj = UserInclude.new("path/to/header.h", use_path: true)
35
-
33
+ it "creates a UserInclude with an include_path override used in string expansion" do
34
+ include_obj = UserInclude.new("path/to/header.h", include_path: "path/to/header.h")
35
+
36
36
  expect(include_obj.filename).to eq("header.h")
37
37
  expect(include_obj.filepath).to eq("path/to/header.h")
38
38
  expect("#{include_obj}").to eq('#include "path/to/header.h"')
@@ -110,14 +110,26 @@ describe SystemInclude do
110
110
  expect(include_obj).to eq('header.h')
111
111
  end
112
112
 
113
- it "creates a SystemInclude with a path and used path in string expansion" do
114
- include_obj = SystemInclude.new("path/to/header.h", use_path: true)
115
-
113
+ it "creates a SystemInclude with an include_path override used in string expansion" do
114
+ include_obj = SystemInclude.new("path/to/header.h", include_path: "path/to/header.h")
115
+
116
116
  expect(include_obj.filename).to eq("header.h")
117
117
  expect(include_obj.filepath).to eq("path/to/header.h")
118
118
  expect("#{include_obj}").to eq('#include <path/to/header.h>')
119
119
  expect(include_obj).to eq('path/to/header.h')
120
120
  end
121
+
122
+ it "renders include_path instead of the bare filename, while filepath/filename keep the resolved identity" do
123
+ include_obj = SystemInclude.new(
124
+ "/usr/include/x86_64-linux-gnu/sys/stat.h", include_path: "sys/stat.h"
125
+ )
126
+
127
+ expect(include_obj.filepath).to eq("/usr/include/x86_64-linux-gnu/sys/stat.h")
128
+ expect(include_obj.filename).to eq("stat.h")
129
+ expect(include_obj.include_path).to eq("sys/stat.h")
130
+ expect("#{include_obj}").to eq('#include <sys/stat.h>')
131
+ expect(include_obj).to eq('sys/stat.h')
132
+ end
121
133
  end
122
134
 
123
135
  describe "Graceful handling of input" do
@@ -189,9 +201,9 @@ describe "Include equality" do
189
201
  end
190
202
 
191
203
  it "compares not equal to another UserInclude with same filename but different paths" do
192
- include1 = UserInclude.new("path1/header.h", use_path: true)
193
- include2 = UserInclude.new("path2/header.h", use_path: true)
194
-
204
+ include1 = UserInclude.new("path1/header.h", include_path: "path1/header.h")
205
+ include2 = UserInclude.new("path2/header.h", include_path: "path2/header.h")
206
+
195
207
  expect(include1).not_to eq(include2)
196
208
  expect(include2).not_to eq(include1)
197
209
  end
@@ -266,9 +278,9 @@ describe "Include equality" do
266
278
  end
267
279
 
268
280
  it "compares not equal to another SystemInclude with same filename but different paths" do
269
- include1 = SystemInclude.new("sys/stdio.h", use_path: true)
270
- include2 = SystemInclude.new("other/stdio.h", use_path: true)
271
-
281
+ include1 = SystemInclude.new("sys/stdio.h", include_path: "sys/stdio.h")
282
+ include2 = SystemInclude.new("other/stdio.h", include_path: "other/stdio.h")
283
+
272
284
  expect(include1).not_to eq(include2)
273
285
  expect(include2).not_to eq(include1)
274
286
  end
@@ -342,9 +354,9 @@ describe "Include equality" do
342
354
  end
343
355
 
344
356
  it "compares not equal to another MockInclude with same filename but different paths" do
345
- include1 = MockInclude.new("mocks/mock_module.h", use_path: true)
346
- include2 = MockInclude.new("test/mocks/mock_module.h", use_path: true)
347
-
357
+ include1 = MockInclude.new("mocks/mock_module.h", include_path: "mocks/mock_module.h")
358
+ include2 = MockInclude.new("test/mocks/mock_module.h", include_path: "test/mocks/mock_module.h")
359
+
348
360
  expect(include1).not_to eq(include2)
349
361
  expect(include2).not_to eq(include1)
350
362
  end
@@ -547,9 +559,9 @@ describe "Include string coercion (to_str)" do
547
559
  expect(include_obj.to_str).to eq("header.h")
548
560
  end
549
561
 
550
- it "returns the full filepath when constructed with use_path: true" do
551
- include_obj = UserInclude.new("sub/dir/header.h", use_path: true)
552
- # to_str always returns @filename (the basename) regardless of use_path
562
+ it "returns the basename even when constructed with an include_path override" do
563
+ include_obj = UserInclude.new("sub/dir/header.h", include_path: "sub/dir/header.h")
564
+ # to_str always returns @filename (the basename) regardless of include_path
553
565
  expect(include_obj.to_str).to eq("header.h")
554
566
  end
555
567
  end
@@ -111,20 +111,38 @@ describe "Includes serialization" do
111
111
  expect(restored.map(&:filename)).to eq(["first.h", "second.h", "third.h", "fourth.h"])
112
112
  end
113
113
 
114
- it "handles includes with paths during serialization" do
114
+ it "handles includes with an include_path override during serialization" do
115
115
  original = [
116
- UserInclude.new("path/to/header.h", use_path: true),
117
- SystemInclude.new("sys/stdio.h", use_path: true),
118
- MockInclude.new("mocks/mock_module.h", use_path: true)
116
+ UserInclude.new("path/to/header.h", include_path: "path/to/header.h"),
117
+ SystemInclude.new("sys/stdio.h", include_path: "sys/stdio.h"),
118
+ MockInclude.new("mocks/mock_module.h")
119
119
  ]
120
-
120
+
121
121
  hashes = Includes.to_hashes(original)
122
122
  restored = Includes.from_hashes(hashes)
123
-
123
+
124
124
  expect(restored.length).to eq(3)
125
125
  expect(restored[0].filepath).to eq("path/to/header.h")
126
126
  expect(restored[1].filepath).to eq("sys/stdio.h")
127
127
  expect(restored[2].filepath).to eq("mocks/mock_module.h")
128
+ # `include_path` must itself round-trip, not merely `filepath` -- otherwise a cached
129
+ # build silently reverts to filename-only rendering on its next, cache-hit run.
130
+ expect("#{restored[0]}").to eq('#include "path/to/header.h"')
131
+ expect("#{restored[1]}").to eq('#include <sys/stdio.h>')
132
+ end
133
+
134
+ it "round-trips include_path, and has it take rendering precedence over filepath/filename" do
135
+ original = [
136
+ SystemInclude.new("/usr/include/x86_64-linux-gnu/sys/stat.h", include_path: "sys/stat.h")
137
+ ]
138
+
139
+ hashes = Includes.to_hashes(original)
140
+ restored = Includes.from_hashes(hashes)
141
+
142
+ expect(restored.length).to eq(1)
143
+ expect(restored[0].filepath).to eq("/usr/include/x86_64-linux-gnu/sys/stat.h")
144
+ expect(restored[0].include_path).to eq("sys/stat.h")
145
+ expect("#{restored[0]}").to eq("#include <sys/stat.h>")
128
146
  end
129
147
 
130
148
  it "handles empty array" do
@@ -1054,16 +1072,88 @@ describe "Includes reconciliation" do
1054
1072
  Include.new("subdir/header.h"),
1055
1073
  Include.new("sys/types.h")
1056
1074
  ]
1057
-
1075
+
1058
1076
  user = [UserInclude.new("header.h")]
1059
1077
  system = [SystemInclude.new("types.h")]
1060
-
1078
+
1061
1079
  result = Includes.reconcile(bare: bare, user: user, system: system)
1062
-
1080
+
1063
1081
  # Should match by filename only
1064
1082
  expect(result.length).to eq(2)
1065
1083
  expect(result[0].filename).to eq("types.h")
1066
1084
  expect(result[1].filename).to eq("header.h")
1067
1085
  end
1086
+
1087
+ it "preserves a system include directive's original directory components instead of collapsing to its bare filename" do
1088
+ bare = [Include.new("sys/stat.h")]
1089
+ user = []
1090
+ system = [SystemInclude.new("/usr/include/x86_64-linux-gnu/sys/stat.h")]
1091
+
1092
+ result = Includes.reconcile(bare: bare, user: user, system: system)
1093
+
1094
+ expect(result.length).to eq(1)
1095
+ expect(result[0]).to be_a(SystemInclude)
1096
+ expect(result[0].filepath).to eq("/usr/include/x86_64-linux-gnu/sys/stat.h")
1097
+ expect("#{result[0]}").to eq("#include <sys/stat.h>")
1098
+ expect(Includes.from_hashes(Includes.to_hashes(result)).map(&:to_s)).to eq(["#include <sys/stat.h>"])
1099
+ end
1100
+
1101
+ it "prefers the more specific bare entry when a system header's basename collides with an unrelated bare entry" do
1102
+ # A project's own quoted `#include "stat.h"` sitting alongside a system
1103
+ # `#include <sys/stat.h>` in the same file: both reach `bare` as plain, untyped
1104
+ # Include objects, and the short entry trivially "corresponds" to any longer
1105
+ # resolved path ending in the same filename. The longer, genuinely-corresponding
1106
+ # entry must still win, regardless of which one happens to appear first in `bare`.
1107
+ bare = [
1108
+ Include.new("stat.h"),
1109
+ Include.new("sys/stat.h")
1110
+ ]
1111
+ user = []
1112
+ system = [SystemInclude.new("/usr/include/x86_64-linux-gnu/sys/stat.h")]
1113
+
1114
+ result = Includes.reconcile(bare: bare, user: user, system: system)
1115
+
1116
+ expect(result.length).to eq(1)
1117
+ expect("#{result[0]}").to eq("#include <sys/stat.h>")
1118
+ end
1119
+
1120
+ it "falls back to a same-filename bare entry when nothing corresponds by path" do
1121
+ # The compiler's real search-path structure can diverge from a directive's own
1122
+ # literal spelling (a symlinked or vendored include root, for instance), so a
1123
+ # resolved system path may not share any trailing segment with its bare entry
1124
+ # beyond the filename itself. Rendering the bare entry's own spelling is still
1125
+ # strictly better than collapsing to the bare filename alone.
1126
+ bare = [Include.new("compat/stat.h")]
1127
+ user = []
1128
+ system = [SystemInclude.new("/usr/include/x86_64-linux-gnu/bits/stat.h")]
1129
+
1130
+ result = Includes.reconcile(bare: bare, user: user, system: system)
1131
+
1132
+ expect(result.length).to eq(1)
1133
+ expect("#{result[0]}").to eq("#include <compat/stat.h>")
1134
+ end
1135
+
1136
+ it "does not hard-error over two same-named system headers, a normal toolchain wrapper-chain pattern rather than a project-file conflict" do
1137
+ # e.g. a compiler-provided stdint.h #include_next-ing its libc counterpart --
1138
+ # both real, both legitimately entered, both sharing a basename, neither a
1139
+ # project file whose identity is actually in question.
1140
+ bare = [Include.new("stdint.h")]
1141
+
1142
+ user = []
1143
+ system = [
1144
+ SystemInclude.new("/usr/lib/gcc/x86_64-linux-gnu/14/include/stdint.h"),
1145
+ SystemInclude.new("/usr/include/stdint.h")
1146
+ ]
1147
+
1148
+ result = nil
1149
+ expect {
1150
+ result = Includes.reconcile(bare: bare, user: user, system: system)
1151
+ }.not_to raise_error
1152
+
1153
+ expect(result.length).to eq(2)
1154
+ expect(result.map(&:filepath)).to include(
1155
+ "/usr/lib/gcc/x86_64-linux-gnu/14/include/stdint.h", "/usr/include/stdint.h"
1156
+ )
1157
+ end
1068
1158
  end
1069
- end
1159
+ end