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,10 +7,13 @@
7
7
 
8
8
  class GeneratorTestResultsBacktrace
9
9
 
10
- constructor :configurator, :tool_executor, :generator_test_results, :file_path_utils, :file_wrapper
10
+ constructor :configurator, :tool_executor, :generator_test_results, :generator_helper,
11
+ :file_path_utils, :file_wrapper, :loginator
11
12
 
12
13
  def setup()
13
14
  @RESULTS_COLLECTOR = Struct.new( :passed, :failed, :ignored, :output, keyword_init:true )
15
+ # Alias, matching Generator's own convention for the same dependency.
16
+ @helper = @generator_helper
14
17
  end
15
18
 
16
19
  # Re-runs each test case (or, for a parameterized test, each group of parameterized
@@ -29,6 +32,13 @@ class GeneratorTestResultsBacktrace
29
32
 
30
33
  test_name = File.basename( filename, '.*' )
31
34
 
35
+ # True once some retry group has actually shown crash evidence of its own -- an
36
+ # unresolved member, or (below) a group whose real status contradicts a fully clean
37
+ # set of matches. If this stays false across every group, the whole diagnostic never
38
+ # reproduced or attributed the crash the main run already detected, and none of it
39
+ # can be trusted -- see the fallback after the loop.
40
+ any_group_crashed = false
41
+
32
42
  # Iterate on test cases, one sub-process run per group (see `group_test_cases`)
33
43
  group_test_cases( test_cases ).each do |group|
34
44
  # Build the test fixture to run with our test case (or parameterized group) of interest
@@ -47,6 +57,10 @@ class GeneratorTestResultsBacktrace
47
57
  # Note: Running tests separately increases total execution time
48
58
  shell_result[:time] += crash_result[:time].to_f()
49
59
 
60
+ # Buffered separately from test_case_results and only merged in afterward, since
61
+ # the status check below can still discard every match here in favor of a crash
62
+ # attribution, once the whole group has been seen.
63
+ group_results = @RESULTS_COLLECTOR.new( passed:0, failed:0, ignored:0, output:[] )
50
64
  unresolved = []
51
65
 
52
66
  # Attribute each group member its own real result line, if Unity printed one
@@ -54,17 +68,17 @@ class GeneratorTestResultsBacktrace
54
68
  case crash_result[:output]
55
69
  # Success test case
56
70
  when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:PASS\s*$)/
57
- test_case_results[:passed] += 1
58
- test_case_results[:output] << $1
71
+ group_results[:passed] += 1
72
+ group_results[:output] << $1
59
73
 
60
74
  # Ignored test case
61
75
  when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:IGNORE\s*$)/
62
- test_case_results[:ignored] += 1
63
- test_case_results[:output] << $1
76
+ group_results[:ignored] += 1
77
+ group_results[:output] << $1
64
78
 
65
79
  when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:FAIL(:.+)?\s*$)/
66
- test_case_results[:failed] += 1
67
- test_case_results[:output] << $1
80
+ group_results[:failed] += 1
81
+ group_results[:output] << $1
68
82
 
69
83
  # No result line for this member -- either it crashed, or it never got to run
70
84
  # because an earlier member in this same group crashed. Resolved below.
@@ -73,81 +87,119 @@ class GeneratorTestResultsBacktrace
73
87
  end
74
88
  end
75
89
 
76
- next if unresolved.empty?
77
-
78
- # Prefer whichever unresolved member's own C symbol is actually named in the gdb
79
- # backtrace (works regardless of position in the group); fall back to the first
80
- # unresolved member if no member's symbol can be found in the transcript (e.g. a
81
- # brief crash report with no frame information at all).
82
- crashed_case = unresolved.find do |tc|
83
- crash_result[:output].match?( /#{Regexp.escape(tc[:symbol])}\s*\(\)\sat/ )
84
- end
85
- crashed_case ||= unresolved.first
86
-
87
- # Per-test-case log file: <log_path>/<context>/<test_name>/<test_case>.gdb.log
88
- log_path = @file_path_utils.form_test_gdb_log( test_name, context: context, name: crashed_case[:test] )
89
- @file_wrapper.mkdir( File.dirname( log_path ) )
90
- @file_wrapper.write( log_path, "=== #{crashed_case[:test]} ===\n#{crash_result[:output]}\n", 'a' )
91
-
92
- unresolved.each do |test_case|
93
- test_case_results[:failed] += 1
94
-
95
- if !test_case.equal?( crashed_case )
96
- # An earlier case in this same parameterized group already crashed the
97
- # process -- this member never got a chance to run.
98
- test_case_results[:output] <<
99
- "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: " \
100
- "Test case not run -- an earlier case in this parameterized test group crashed"
101
- next
90
+ if !unresolved.empty?
91
+ # Prefer whichever unresolved member's own C symbol is actually named in the gdb
92
+ # backtrace (works regardless of position in the group); fall back to the first
93
+ # unresolved member if no member's symbol can be found in the transcript (e.g. a
94
+ # brief crash report with no frame information at all).
95
+ crashed_case = unresolved.find do |tc|
96
+ crash_result[:output].match?( /#{Regexp.escape(tc[:symbol])}\s*\(\)\sat/ )
102
97
  end
98
+ crashed_case ||= unresolved.first
103
99
 
104
- # Collect file_name and line in which crash occurred.
105
- # Match against the actual C symbol (`:symbol`), not the human-facing test name
106
- # (`:test`): a parameterized test case crashes inside a generated wrapper function
107
- # (`runner_args<N>_<test>`), not a function literally named `<test>(<args>)`.
108
- matched = crash_result[:output].match( /#{Regexp.escape(test_case[:symbol])}\s*\(\)\sat.+#{filename}:(\d+)\n/ )
109
-
110
- # If we found an error report line containing `test_case() at filename.c:###` in `gdb` output
111
- if matched
112
- # Line number
113
- line_number = matched[1]
114
-
115
- # Build terse signal label: "[SIGNAL] Description"
116
- signal_label = format_signal_label( crash_result[:output] )
117
-
118
- # Extract the offending source line (nil for assertion crashes or when unavailable)
119
- source_line = extract_source_line( crash_result[:output], test_case[:symbol], filename )
120
-
121
- # Unity's test executable output is line oriented.
122
- # Multi-line output is not possible (it looks like random `printf()` statements to the results parser).
123
- # "Encode" newlines in multiline string to be handled by the test results parser.
124
- crash_detail = source_line ? "#{NEWLINE_TOKEN}`#{source_line}`" : ''
125
-
126
- # Log path appears on its own encoded line so the results parser treats it separately
127
- test_case_results[:output] <<
128
- "#{filename}:#{line_number}:#{test_case[:test]}:FAIL: Test case crashed" \
129
- " >> #{signal_label}" \
130
- "#{crash_detail}" \
131
- "#{NEWLINE_TOKEN}(#{log_path})"
132
-
133
- # Try to extract a useful label even when no crash location frame was found.
134
- # A brief Windows assertion failure may report only the assertion text without frames.
135
- else
136
- label = format_signal_label( crash_result[:output] )
100
+ # Per-test-case log file: <log_path>/<context>/<test_name>/<test_case>.gdb.log
101
+ log_path = @file_path_utils.form_test_gdb_log( test_name, context: context, name: crashed_case[:test] )
102
+ @file_wrapper.mkdir( File.dirname( log_path ) )
103
+ @file_wrapper.write( log_path, "=== #{crashed_case[:test]} ===\n#{crash_result[:output]}\n", 'a' )
137
104
 
138
- if !label.empty?
139
- test_case_results[:output] <<
140
- "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test case crashed" \
141
- " >> #{label}" \
142
- "#{NEWLINE_TOKEN}(#{log_path})"
143
- else
144
- test_case_results[:output] <<
105
+ unresolved.each do |test_case|
106
+ group_results[:failed] += 1
107
+
108
+ if !test_case.equal?( crashed_case )
109
+ # An earlier case in this same parameterized group already crashed the
110
+ # process -- this member never got a chance to run.
111
+ group_results[:output] <<
145
112
  "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: " \
146
- "Test case crashed (failed to extract `gdb` report)" \
113
+ "Test case not run -- an earlier case in this parameterized test group crashed"
114
+ next
115
+ end
116
+
117
+ # Collect file_name and line in which crash occurred.
118
+ # Match against the actual C symbol (`:symbol`), not the human-facing test name
119
+ # (`:test`): a parameterized test case crashes inside a generated wrapper function
120
+ # (`runner_args<N>_<test>`), not a function literally named `<test>(<args>)`.
121
+ matched = crash_result[:output].match( /#{Regexp.escape(test_case[:symbol])}\s*\(\)\sat.+#{filename}:(\d+)\n/ )
122
+
123
+ # If we found an error report line containing `test_case() at filename.c:###` in `gdb` output
124
+ if matched
125
+ # Line number
126
+ line_number = matched[1]
127
+
128
+ # Build terse signal label: "[SIGNAL] Description"
129
+ signal_label = format_signal_label( crash_result[:output] )
130
+
131
+ # Extract the offending source line (nil for assertion crashes or when unavailable)
132
+ source_line = extract_source_line( crash_result[:output], test_case[:symbol], filename )
133
+
134
+ # Unity's test executable output is line oriented.
135
+ # Multi-line output is not possible (it looks like random `printf()` statements to the results parser).
136
+ # "Encode" newlines in multiline string to be handled by the test results parser.
137
+ crash_detail = source_line ? "#{NEWLINE_TOKEN}`#{source_line}`" : ''
138
+
139
+ # Log path appears on its own encoded line so the results parser treats it separately
140
+ group_results[:output] <<
141
+ "#{filename}:#{line_number}:#{test_case[:test]}:FAIL: Test case crashed" \
142
+ " >> #{signal_label}" \
143
+ "#{crash_detail}" \
147
144
  "#{NEWLINE_TOKEN}(#{log_path})"
145
+
146
+ # Try to extract a useful label even when no crash location frame was found.
147
+ # A brief Windows assertion failure may report only the assertion text without frames.
148
+ else
149
+ label = format_signal_label( crash_result[:output] )
150
+
151
+ if !label.empty?
152
+ group_results[:output] <<
153
+ "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test case crashed" \
154
+ " >> #{label}" \
155
+ "#{NEWLINE_TOKEN}(#{log_path})"
156
+ else
157
+ group_results[:output] <<
158
+ "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: " \
159
+ "Test case crashed (failed to extract `gdb` report)" \
160
+ "#{NEWLINE_TOKEN}(#{log_path})"
161
+ end
148
162
  end
149
163
  end
150
164
  end
165
+
166
+ # Every member in this group resolved via a matched result line -- but gdb
167
+ # normally exits successfully after handling a crash regardless of what happened
168
+ # to the debuggee, so a clean-looking group here is a weaker signal than it is for
169
+ # `do_simple`. Still checked for the same reason: a diagnostic retry's own real
170
+ # status contradicting a fully clean set of matches is never trustworthy, whatever
171
+ # tool produced it.
172
+ if unresolved.empty? && @helper.test_crash?( filename, executable, crash_result )
173
+ group_results = @RESULTS_COLLECTOR.new(
174
+ passed: 0, ignored: 0, failed: group.size,
175
+ output: group.map { |test_case|
176
+ "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test case crashed" \
177
+ " >> diagnostic retry's own exit status contradicts its reported clean result"
178
+ }
179
+ )
180
+ unresolved = group # non-empty now, marks this group as having shown crash evidence
181
+ end
182
+
183
+ any_group_crashed ||= !unresolved.empty?
184
+
185
+ test_case_results[:passed] += group_results[:passed]
186
+ test_case_results[:ignored] += group_results[:ignored]
187
+ test_case_results[:failed] += group_results[:failed]
188
+ test_case_results[:output].concat( group_results[:output] )
189
+ end
190
+
191
+ # No retry group, across the entire file, ever showed real crash evidence, yet this
192
+ # method only runs because the main run was already established as a crash. An
193
+ # all-clean diagnostic must never be allowed to overrule that -- fall back to the
194
+ # same whole-file crash-as-failure construction the :none backtrace setting uses.
195
+ unless any_group_crashed
196
+ @loginator.log(
197
+ "Diagnostic retry for `#{File.basename(executable)}` could not reproduce the crash " \
198
+ "already detected on the main run -- reporting the original crash rather than trusting " \
199
+ "an all-clear diagnostic that contradicts it.",
200
+ Verbosity::ERRORS, LogLabels::CRASH
201
+ )
202
+ return @generator_test_results.create_crash_failure( filename, shell_result, test_cases )
151
203
  end
152
204
 
153
205
  # Reset shell result exit code and output
@@ -175,6 +227,13 @@ class GeneratorTestResultsBacktrace
175
227
  # Reset time
176
228
  shell_result[:time] = 0
177
229
 
230
+ # True once some retry group has actually shown crash evidence of its own -- a
231
+ # member with no result line, or (below) a group whose real status contradicts a
232
+ # fully clean set of matches. If this stays false across every group, the whole
233
+ # diagnostic never reproduced or attributed the crash the main run already
234
+ # detected, and none of it can be trusted -- see the fallback after the loop.
235
+ any_group_crashed = false
236
+
178
237
  # Iterate on test cases, one sub-process run per group (see `group_test_cases`)
179
238
  group_test_cases( test_cases ).each do |group|
180
239
  # Build the test fixture to run with our test case (or parameterized group) of interest
@@ -192,6 +251,10 @@ class GeneratorTestResultsBacktrace
192
251
  # Note: Running tests separately increases total execution time
193
252
  shell_result[:time] += crash_result[:time].to_f()
194
253
 
254
+ # Buffered separately from test_case_results and only merged in afterward, since
255
+ # the status check below can still discard every match here in favor of a crash
256
+ # attribution, once the whole group has been seen.
257
+ group_results = @RESULTS_COLLECTOR.new( passed:0, failed:0, ignored:0, output:[] )
195
258
  crashed = false # Has the actual crash in this group already been attributed?
196
259
 
197
260
  # Attribute each group member its own real result line, if Unity printed one
@@ -199,25 +262,25 @@ class GeneratorTestResultsBacktrace
199
262
  case crash_result[:output]
200
263
  # Success test case
201
264
  when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:PASS\s*$)/
202
- test_case_results[:passed] += 1
203
- test_case_results[:output] << $1
265
+ group_results[:passed] += 1
266
+ group_results[:output] << $1
204
267
 
205
268
  # Ignored test case
206
269
  when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:IGNORE\s*$)/
207
- test_case_results[:ignored] += 1
208
- test_case_results[:output] << $1
270
+ group_results[:ignored] += 1
271
+ group_results[:output] << $1
209
272
 
210
273
  when /(^#{Regexp.escape(filename)}:\d+:#{Regexp.escape(test_case[:test])}:FAIL(:.+)?\s*$)/
211
- test_case_results[:failed] += 1
212
- test_case_results[:output] << $1
274
+ group_results[:failed] += 1
275
+ group_results[:output] << $1
213
276
 
214
277
  # No result line for this member -- either it crashed, or it never got to run
215
278
  # because an earlier member in this same group crashed.
216
279
  else
217
- test_case_results[:failed] += 1
280
+ group_results[:failed] += 1
218
281
 
219
282
  if crashed
220
- test_case_results[:output] <<
283
+ group_results[:output] <<
221
284
  "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: " \
222
285
  "Test case not run -- an earlier case in this parameterized test group crashed"
223
286
  else
@@ -226,10 +289,47 @@ class GeneratorTestResultsBacktrace
226
289
  extra = extract_simple_crash_output( crash_result[:output], filename )
227
290
  test_output = "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test case crashed"
228
291
  test_output += " >> #{extra.join(NEWLINE_TOKEN)}" unless extra.empty?
229
- test_case_results[:output] << test_output
292
+ group_results[:output] << test_output
230
293
  end
231
294
  end
232
295
  end
296
+
297
+ # Every member in this group resolved via a matched result line -- but a diagnostic
298
+ # retry running through its own, separately-configured tool (e.g. without the main
299
+ # :test_fixture's sanitizer options) can complete cleanly and print a legitimate-
300
+ # looking PASS even when it isn't a trustworthy stand-in for what actually happened.
301
+ # The same real-status check the main run itself relies on applies here too.
302
+ if !crashed && @helper.test_crash?( filename, executable, crash_result )
303
+ crashed = true
304
+ group_results = @RESULTS_COLLECTOR.new(
305
+ passed: 0, ignored: 0, failed: group.size,
306
+ output: group.map { |test_case|
307
+ "#{filename}:#{test_case[:line_number]}:#{test_case[:test]}:FAIL: Test case crashed" \
308
+ " >> diagnostic retry's own exit status contradicts its reported clean result"
309
+ }
310
+ )
311
+ end
312
+
313
+ any_group_crashed ||= crashed
314
+
315
+ test_case_results[:passed] += group_results[:passed]
316
+ test_case_results[:ignored] += group_results[:ignored]
317
+ test_case_results[:failed] += group_results[:failed]
318
+ test_case_results[:output].concat( group_results[:output] )
319
+ end
320
+
321
+ # No retry group, across the entire file, ever showed real crash evidence, yet this
322
+ # method only runs because the main run was already established as a crash. An
323
+ # all-clean diagnostic must never be allowed to overrule that -- fall back to the
324
+ # same whole-file crash-as-failure construction the :none backtrace setting uses.
325
+ unless any_group_crashed
326
+ @loginator.log(
327
+ "Diagnostic retry for `#{File.basename(executable)}` could not reproduce the crash " \
328
+ "already detected on the main run -- reporting the original crash rather than trusting " \
329
+ "an all-clear diagnostic that contradicts it.",
330
+ Verbosity::ERRORS, LogLabels::CRASH
331
+ )
332
+ return @generator_test_results.create_crash_failure( filename, shell_result, test_cases )
233
333
  end
234
334
 
235
335
  # Reset shell result exit code and output
@@ -32,10 +32,15 @@ class Includes
32
32
  else raise ArgumentError, "Unknown Include type: #{include.class}"
33
33
  end
34
34
 
35
- {
35
+ hash = {
36
36
  'type' => type,
37
37
  'filepath' => include.filepath,
38
38
  }
39
+ # `include_path` is only ever meaningful on a reconciled SystemInclude, so it's
40
+ # written only when actually set -- a cached build's YAML stays uncluttered for
41
+ # the common, plain case.
42
+ hash['include_path'] = include.include_path if include.include_path
43
+ hash
39
44
  end
40
45
  end
41
46
 
@@ -63,11 +68,11 @@ class Includes
63
68
 
64
69
  case hash['type']
65
70
  when 'user'
66
- UserInclude.new(hash['filepath'])
71
+ UserInclude.new(hash['filepath'], include_path: hash['include_path'])
67
72
  when 'mock'
68
73
  MockInclude.new(hash['filepath'])
69
74
  when 'system'
70
- SystemInclude.new(hash['filepath'])
75
+ SystemInclude.new(hash['filepath'], include_path: hash['include_path'])
71
76
  else
72
77
  raise ArgumentError, "Invalid include type: #{hash['type']}. Must be 'user' or 'system'"
73
78
  end
@@ -153,6 +158,16 @@ class Includes
153
158
  # Compares bare includes against user and system includes and applies the following rules:
154
159
  # 1. Intersection of bare includes and system includes.
155
160
  # 2. Intersection of bare includes and user includes.
161
+ #
162
+ # A matched system include's `filepath` is its real, resolved location (possibly
163
+ # absolute), which is what identity and deduplication need but is wrong to render
164
+ # verbatim into a generated file. Each matched system include is therefore rebuilt
165
+ # carrying `include_path:` recovered from `bare` -- the original, as-written directive
166
+ # text -- via `best_bare_match`, so `<sys/stat.h>` renders as written instead of
167
+ # collapsing to `<stat.h>`. User includes are left untouched; unlike a system header's
168
+ # `<...>` text, a quoted include's own bare-includes pass can be resolved by GCC's
169
+ # directory-relative search before reconciliation ever sees it, so `bare` isn't a
170
+ # reliable source of "the literal, as-written spelling" for a user include.
156
171
  def self.reconcile(bare:, user:, system:)
157
172
  # Validate input types
158
173
 
@@ -179,10 +194,15 @@ class Includes
179
194
  # Create set of bare include filenames for O(1) lookup
180
195
  bare_filenames = Set.new(bare.map(&:filename))
181
196
 
182
- # Intersect system includes with bare includes based on filename.
183
- # Keep system includes that have matching filenames in bare list.
197
+ # Intersect system includes with bare includes based on filename, then rebuild each
198
+ # matched system include carrying the original, as-written directive spelling
199
+ # recovered from bare via best_bare_match, since filepath alone (the real, resolved
200
+ # location) is wrong to render verbatim.
184
201
  system_includes = system.select do |include|
185
202
  bare_filenames.include?(include.filename)
203
+ end.map do |include|
204
+ original = best_bare_match(bare, include.filepath)
205
+ SystemInclude.new(include.filepath, include_path: original.filepath)
186
206
  end
187
207
 
188
208
  # Intersect user includes with bare includes based on filename.
@@ -196,6 +216,36 @@ class Includes
196
216
  return (system_includes + user_includes)
197
217
  end
198
218
 
219
+ # Two filepaths correspond if the shorter one's path segments equal the longer one's
220
+ # own trailing segments, exactly, in order -- checked without regard for which side is
221
+ # shorter, since either a bare entry or its candidate may be the one carrying less path.
222
+ def self.paths_correspond?(a, b)
223
+ segments_a = a.split(/[\\\/]/).reject(&:empty?)
224
+ segments_b = b.split(/[\\\/]/).reject(&:empty?)
225
+ shorter, longer = segments_a.length <= segments_b.length ? [segments_a, segments_b] : [segments_b, segments_a]
226
+ return longer.last(shorter.length) == shorter
227
+ end
228
+ private_class_method :paths_correspond?
229
+
230
+ # Finds the bare entry that best identifies a resolved system include's original,
231
+ # as-written spelling. A bare entry carries no information about whether its own
232
+ # #include was quoted or bracketed, so a project's own same-named quoted include
233
+ # (`#include "stat.h"`) can sit in `bare` right alongside a system one
234
+ # (`#include <sys/stat.h>`). Preferring the *longest* path correspondence, rather than
235
+ # merely the first one found, keeps a short, unrelated bare entry -- which trivially
236
+ # "corresponds" to any longer resolved path ending in the same filename -- from
237
+ # shadowing the correct, more specific entry. Falls back to the longest same-filename
238
+ # candidate, even without path correspondence, only when the compiler's real
239
+ # search-path structure doesn't literally match the directive's own spelling (a
240
+ # symlinked or vendored include root, for instance).
241
+ def self.best_bare_match(bare, filepath)
242
+ candidates = bare.select { |bare_include| bare_include.filename == File.basename(filepath) }
243
+ corresponding = candidates.select { |bare_include| paths_correspond?(bare_include.filepath, filepath) }
244
+ pool = corresponding.empty? ? candidates : corresponding
245
+ return pool.max_by { |bare_include| bare_include.filepath.split(/[\\\/]/).reject(&:empty?).length }
246
+ end
247
+ private_class_method :best_bare_match
248
+
199
249
  # Sort list so system includes are at the beginning
200
250
  # (Best practice)
201
251
  def self.sort(includes)
@@ -216,6 +266,7 @@ class Include
216
266
  attr_reader :filepath
217
267
  attr_reader :filename
218
268
  attr_reader :path
269
+ attr_reader :include_path
219
270
 
220
271
  # Initialize an Include object from a C include statement or simple filepath.
221
272
  #
@@ -224,18 +275,21 @@ class Include
224
275
  # - #include <stdio.h>
225
276
  # - A quoted/bracketed filepath (e.g., '"header.h"' or <stdio.h>')
226
277
  # - A plain filepath (e.g., 'path/to/header.h')
227
- # @param use_path [Boolean] (default: false)
228
- # - If true, use the full filepath in the include directive
229
- # - If false, use only the filename
278
+ # @param include_path [String, nil] (default: nil)
279
+ # - When set, this exact spelling is used in the include directive, taking
280
+ # precedence over `filepath`/`filename`. This exists for a reconciled
281
+ # SystemInclude, whose `filepath` is deliberately the header's real, resolved
282
+ # location (needed elsewhere for identity -- see `Includes.sanitize!`) rather than
283
+ # the original, as-written directive text a reconciled render needs.
230
284
  # @raise [ArgumentError] If the statement is empty or becomes empty after cleaning
231
- def initialize(statement, use_path: false)
285
+ def initialize(statement, include_path: nil)
232
286
  @filepath = clean(statement)
233
287
 
234
288
  raise ArgumentError, "Empty include statement" if @filepath.empty?
235
289
 
236
290
  @filename = File.basename(@filepath)
237
291
  @path = File.dirname(@filepath)
238
- @use_path = use_path
292
+ @include_path = clean(include_path) if include_path
239
293
  end
240
294
 
241
295
  # Method specialized by subclasses
@@ -294,7 +348,8 @@ class Include
294
348
 
295
349
  # Returns the configured entry to use in the include directive
296
350
  def include()
297
- @use_path ? @filepath : @filename
351
+ return @include_path if @include_path
352
+ @filename
298
353
  end
299
354
 
300
355
  private
@@ -249,8 +249,10 @@ generator_test_results_backtrace:
249
249
  - configurator
250
250
  - tool_executor
251
251
  - generator_test_results
252
+ - generator_helper
252
253
  - file_path_utils
253
254
  - file_wrapper
255
+ - loginator
254
256
 
255
257
  generator_test_results_sanity_checker:
256
258
  compose:
@@ -312,6 +314,7 @@ preprocessinator_includes_handler:
312
314
  - tool_executor
313
315
  - parsing_parcels
314
316
  - file_wrapper
317
+ - file_path_utils
315
318
  - yaml_wrapper
316
319
  - loginator
317
320
  - reportinator
@@ -354,6 +357,8 @@ c_extractor:
354
357
  - c_extractor_functions
355
358
  - c_extractor_preprocessing
356
359
  - c_extractor_definitions
360
+ - configurator
361
+ - loginator
357
362
 
358
363
  partializer_config:
359
364
  compose:
@@ -83,7 +83,7 @@ class Partializer
83
83
  end
84
84
 
85
85
  # When `test:` is provided, logs the resulting includes at OBNOXIOUS.
86
- def remap_implementation_header_includes(name:, includes:, partials:, test: nil)
86
+ def remap_implementation_header_includes(name:, includes:, partials:, types_header: nil, test: nil)
87
87
  _includes = includes.clone()
88
88
 
89
89
  # Get list of all partialized module names
@@ -96,6 +96,10 @@ class Partializer
96
96
  modules: ([name] + partialized_modules)
97
97
  )
98
98
 
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
+
99
103
  # Remove any duplicates
100
104
  Includes.sanitize!(_includes)
101
105
 
@@ -153,7 +157,7 @@ class Partializer
153
157
  end
154
158
 
155
159
  # When `test:` is provided, logs the resulting includes at OBNOXIOUS.
156
- def remap_interface_header_includes(name:, includes:, partials:, test: nil)
160
+ def remap_interface_header_includes(name:, includes:, partials:, types_header: nil, test: nil)
157
161
  _includes = includes.clone()
158
162
 
159
163
  # Get list of all partialized module names
@@ -166,6 +170,10 @@ class Partializer
166
170
  modules: ([name] + partialized_modules)
167
171
  )
168
172
 
173
+ # When this module has any typedefs or aggregate definitions, they live in a shared
174
+ # header generated once and included here rather than duplicated inline.
175
+ _includes << UserInclude.new(types_header) if types_header
176
+
169
177
  # Remove any duplicates
170
178
  Includes.sanitize!(_includes)
171
179
 
@@ -341,8 +349,15 @@ class Partializer
341
349
  Verbosity::DEBUG
342
350
  )
343
351
 
352
+ # A mock interface only ever needs a signature, never a body, so a declaration-only
353
+ # function (no body found anywhere in this module's merged content) is just as valid a
354
+ # candidate as one with a definition -- unlike extract_implementation_functions, which
355
+ # needs real code to inject and so stays definitions-only. A function named in both
356
+ # (declared in the header, defined in the paired source) is only counted once.
357
+ candidates = definitions + declarations.reject { |d| definitions.any? { |f| f.name == d.name } }
358
+
344
359
  # Build initial list by visibility; ACCUMULATE yields []
345
- funcs = @helper.filter_and_transform_funcs(definitions, pf.type, :interface)
360
+ funcs = @helper.filter_and_transform_funcs(candidates, pf.type, :interface)
346
361
 
347
362
  # Additions: search definitions first, then declarations
348
363
  pf.additions.each do |name|
@@ -47,9 +47,14 @@ class PartializerUtils
47
47
  code_block: new_code_block
48
48
  )
49
49
  when :interface
50
+ # A signature sourced from a CFunctionDefinition never carries a trailing semicolon
51
+ # (it's followed by a body, not one), but a signature sourced from a
52
+ # CFunctionDeclaration always does (it's a bare prototype). Strip it here so
53
+ # callers can render "#{signature};" uniformly regardless of which kind of
54
+ # function this interface entry came from.
50
55
  Partials.manufacture_function_declaration(
51
56
  name: func.name,
52
- signature: signature
57
+ signature: signature.sub(/;\s*\z/, '')
53
58
  )
54
59
  else
55
60
  PartializerRuntime.raise_on_option(output_type)