ceedling 1.1.2 → 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 (173) 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_crash_sigsegv_with_param.c +32 -0
  13. data/assets/test_example_file_ub_shift_overflow.c +22 -0
  14. data/assets/test_partial_all_module.c +16 -0
  15. data/assets/test_same_dir_pairing.c +23 -0
  16. data/bin/actions_wrapper.rb +7 -6
  17. data/docs/Changelog.md +29 -1
  18. data/docs/mkdocs/configuration/reference/index.md +8 -0
  19. data/docs/mkdocs/configuration/reference/partials.md +38 -0
  20. data/examples/wondrous_forest/src/LightSensor.c +21 -4
  21. data/examples/wondrous_forest/src/LightSensor.h +12 -4
  22. data/examples/wondrous_forest/test/TestLightSensor.c +34 -4
  23. data/lib/ceedling/c_extractor/c_extractor.rb +37 -11
  24. data/lib/ceedling/c_extractor/c_extractor_constants.rb +0 -3
  25. data/lib/ceedling/c_extractor/c_extractor_declarations.rb +26 -25
  26. data/lib/ceedling/c_extractor/c_extractor_functions.rb +5 -12
  27. data/lib/ceedling/config/configurator.rb +8 -1
  28. data/lib/ceedling/config/configurator_setup.rb +26 -0
  29. data/lib/ceedling/defaults.rb +16 -2
  30. data/lib/ceedling/file_path_utils.rb +7 -0
  31. data/lib/ceedling/file_wrapper.rb +8 -0
  32. data/lib/ceedling/generators/generator.rb +15 -0
  33. data/lib/ceedling/generators/generator_helper.rb +25 -6
  34. data/lib/ceedling/generators/generator_partials.rb +84 -3
  35. data/lib/ceedling/generators/generator_test_results_backtrace.rb +268 -100
  36. data/lib/ceedling/generators/generator_test_runner.rb +20 -2
  37. data/lib/ceedling/includes/includes.rb +66 -11
  38. data/lib/ceedling/objects.yml +5 -0
  39. data/lib/ceedling/partials/partializer.rb +18 -3
  40. data/lib/ceedling/partials/partializer_utils.rb +6 -1
  41. data/lib/ceedling/preprocess/preprocessinator_includes_handler.rb +43 -16
  42. data/lib/ceedling/test_invoker/test_build_executor.rb +19 -8
  43. data/lib/version.rb +1 -1
  44. data/mkdocs.yml +1 -0
  45. data/plugins/gcov/lib/gcov.rb +2 -2
  46. data/site-local/404.html +30 -0
  47. data/site-local/configuration/environment-vars.html +30 -0
  48. data/site-local/configuration/global-collections.html +30 -0
  49. data/site-local/configuration/index.html +30 -0
  50. data/site-local/configuration/loading.html +30 -0
  51. data/site-local/configuration/mixins.html +30 -0
  52. data/site-local/configuration/parallel-builds.html +30 -0
  53. data/site-local/configuration/project-file.html +30 -0
  54. data/site-local/configuration/reference/cexception.html +30 -0
  55. data/site-local/configuration/reference/cmock.html +30 -0
  56. data/site-local/configuration/reference/defines.html +30 -0
  57. data/site-local/configuration/reference/environment.html +30 -0
  58. data/site-local/configuration/reference/extension.html +30 -0
  59. data/site-local/configuration/reference/files.html +30 -0
  60. data/site-local/configuration/reference/flags.html +30 -0
  61. data/site-local/configuration/reference/index.html +36 -0
  62. data/site-local/configuration/reference/libraries.html +30 -0
  63. data/site-local/configuration/reference/mixins.html +30 -0
  64. data/site-local/configuration/reference/partials.html +4257 -0
  65. data/site-local/configuration/reference/paths.html +30 -0
  66. data/site-local/configuration/reference/plugins.html +30 -0
  67. data/site-local/configuration/reference/project.html +30 -0
  68. data/site-local/configuration/reference/release-build.html +31 -1
  69. data/site-local/configuration/reference/test-build.html +31 -1
  70. data/site-local/configuration/reference/test-runner.html +30 -0
  71. data/site-local/configuration/reference/tools.html +30 -0
  72. data/site-local/configuration/reference/unity.html +30 -0
  73. data/site-local/configuration/which-ceedling.html +30 -0
  74. data/site-local/development/index.html +30 -0
  75. data/site-local/development/plugins/configuration.html +30 -0
  76. data/site-local/development/plugins/index.html +30 -0
  77. data/site-local/development/plugins/plugin-subclass.html +30 -0
  78. data/site-local/development/plugins/rake-tasks.html +30 -0
  79. data/site-local/development/workflow.html +30 -0
  80. data/site-local/getting-started/command-line.html +30 -0
  81. data/site-local/getting-started/example-projects/cipher-quest.html +30 -0
  82. data/site-local/getting-started/example-projects/index.html +30 -0
  83. data/site-local/getting-started/example-projects/temp-sensor.html +30 -0
  84. data/site-local/getting-started/example-projects/wondrous-forest.html +30 -0
  85. data/site-local/getting-started/index.html +30 -0
  86. data/site-local/getting-started/installation.html +30 -0
  87. data/site-local/getting-started/quick-start.html +30 -0
  88. data/site-local/help.html +30 -0
  89. data/site-local/index.html +30 -0
  90. data/site-local/overview/build-system.html +30 -0
  91. data/site-local/overview/index.html +30 -0
  92. data/site-local/overview/test-environments.html +30 -0
  93. data/site-local/overview/tools-and-frameworks.html +30 -0
  94. data/site-local/plugins/beep.html +30 -0
  95. data/site-local/plugins/bullseye.html +30 -0
  96. data/site-local/plugins/command-hooks.html +30 -0
  97. data/site-local/plugins/compile-commands-json-db.html +30 -0
  98. data/site-local/plugins/cppcheck.html +30 -0
  99. data/site-local/plugins/dependencies.html +30 -0
  100. data/site-local/plugins/fff.html +30 -0
  101. data/site-local/plugins/gcov/examples.html +30 -0
  102. data/site-local/plugins/gcov/gcovr.html +30 -0
  103. data/site-local/plugins/gcov/index.html +30 -0
  104. data/site-local/plugins/gcov/overview.html +30 -0
  105. data/site-local/plugins/gcov/reportgenerator.html +30 -0
  106. data/site-local/plugins/gcov/reporting.html +30 -0
  107. data/site-local/plugins/gcov/setup.html +30 -0
  108. data/site-local/plugins/gcov/tool-versions.html +30 -0
  109. data/site-local/plugins/gcov/troubleshooting.html +30 -0
  110. data/site-local/plugins/index.html +30 -0
  111. data/site-local/plugins/module-generator.html +30 -0
  112. data/site-local/plugins/report-build-warnings-log.html +30 -0
  113. data/site-local/plugins/report-tests-gtestlike-stdout.html +30 -0
  114. data/site-local/plugins/report-tests-ide-stdout.html +30 -0
  115. data/site-local/plugins/report-tests-log-factory.html +30 -0
  116. data/site-local/plugins/report-tests-pretty-stdout.html +30 -0
  117. data/site-local/plugins/report-tests-raw-output-log.html +30 -0
  118. data/site-local/plugins/report-tests-teamcity-stdout.html +30 -0
  119. data/site-local/plugins/valgrind.html +30 -0
  120. data/site-local/project/release-history.html +30 -0
  121. data/site-local/project/upgrade.html +30 -0
  122. data/site-local/reference/build-directives.html +30 -0
  123. data/site-local/reference/command-line.html +30 -0
  124. data/site-local/reference/environment-vars.html +30 -0
  125. data/site-local/reference/gcov-plugin.html +30 -0
  126. data/site-local/reference/global-collections.html +30 -0
  127. data/site-local/reference/index.html +30 -0
  128. data/site-local/reference/partials-macros.html +30 -0
  129. data/site-local/reference/project-configuration.html +30 -0
  130. data/site-local/sitemap.xml.gz +0 -0
  131. data/site-local/snapshot/examples/cipher_quest/index.html +30 -0
  132. data/site-local/snapshot/examples/temp_sensor/index.html +30 -0
  133. data/site-local/snapshot/examples/wondrous_forest/index.html +30 -0
  134. data/site-local/testing-guide/build-directives.html +30 -0
  135. data/site-local/testing-guide/conventions.html +30 -0
  136. data/site-local/testing-guide/frameworks.html +30 -0
  137. data/site-local/testing-guide/index.html +30 -0
  138. data/site-local/testing-guide/partials/configuration.html +30 -0
  139. data/site-local/testing-guide/partials/conventions.html +30 -0
  140. data/site-local/testing-guide/partials/directives.html +30 -0
  141. data/site-local/testing-guide/partials/example.html +30 -0
  142. data/site-local/testing-guide/partials/index.html +30 -0
  143. data/site-local/testing-guide/partials/overview.html +30 -0
  144. data/site-local/testing-guide/partials/variables.html +30 -0
  145. data/site-local/testing-guide/test-cases.html +30 -0
  146. data/site-local/testing-guide/test-sample.html +30 -0
  147. data/site-local/testing-guide/test-suite-anatomy.html +30 -0
  148. data/spec/support/system/spec_system_helper.rb +25 -0
  149. data/spec/support/system/system_context.rb +15 -0
  150. data/spec/system/deployment_as_gem_spec.rb +14 -0
  151. data/spec/system/deployment_as_vendor_spec.rb +19 -0
  152. data/spec/system/encoding_stress_spec.rb +4 -4
  153. data/spec/system/example_wondrous_forest_spec.rb +2 -2
  154. data/spec/system/support/common_test_cases.rb +227 -0
  155. data/spec/units/c_extractor/c_extractor_declarations_spec.rb +27 -4
  156. data/spec/units/c_extractor/c_extractor_functions_spec.rb +1 -11
  157. data/spec/units/c_extractor/c_extractor_integration_spec.rb +18 -11
  158. data/spec/units/c_extractor/c_extractor_spec.rb +57 -1
  159. data/spec/units/config/configurator_setup_spec.rb +61 -0
  160. data/spec/units/configurator_spec.rb +45 -0
  161. data/spec/units/file_path_utils_spec.rb +17 -0
  162. data/spec/units/generators/generator_helper_spec.rb +34 -7
  163. data/spec/units/generators/generator_partials_spec.rb +120 -12
  164. data/spec/units/generators/generator_test_results_backtrace_spec.rb +248 -41
  165. data/spec/units/generators/generator_test_runner_spec.rb +26 -2
  166. data/spec/units/includes/include_spec.rb +30 -18
  167. data/spec/units/includes/includes_spec.rb +100 -10
  168. data/spec/units/partials/partializer_spec.rb +113 -1
  169. data/spec/units/partials/partializer_utils_spec.rb +11 -0
  170. data/spec/units/preprocess/preprocessinator_includes_handler_spec.rb +96 -7
  171. data/spec/units/system_wrapper_spec.rb +72 -0
  172. data/spec/units/test_context_extractor_spec.rb +3 -3
  173. metadata +20 -2
@@ -16,12 +16,9 @@ class CExtractorDeclarations
16
16
 
17
17
  constructor :c_extractor_code_text
18
18
 
19
- attr_writer :max_line_length
20
-
21
19
  def setup()
22
20
  # Aliases
23
- @code_text = @c_extractor_code_text
24
- @max_line_length = DEFAULT_MAX_LINE_LENGTH
21
+ @code_text = @c_extractor_code_text
25
22
  end
26
23
 
27
24
  # Attempts to extract a complete variable declaration from the scanner
@@ -58,11 +55,14 @@ class CExtractorDeclarations
58
55
  # On failure: Resets scanner position to starting position
59
56
  #
60
57
  # Safety:
61
- # Enforces max_line_length limit to prevent infinite loops on malformed input
58
+ # A declaration with no terminating semicolon runs the scan to the end of
59
+ # whatever buffer the caller (CExtractor#extract_next_feature) has grown so
60
+ # far; that caller is the one place enforcing an overall length ceiling,
61
+ # since only it knows how much more of the file remains to try growing into.
62
62
  def try_extract_variable(scanner)
63
63
  start_pos = scanner.pos
64
64
 
65
- # Tracks paren, bracket, and brace depth simultaneously with inline string state machine and max_line_length guard — not suitable for collect_balanced()
65
+ # Tracks paren, bracket, and brace depth simultaneously with an inline string state machine — not suitable for collect_balanced()
66
66
  paren_depth = 0
67
67
  bracket_depth = 0
68
68
  brace_depth = 0
@@ -74,12 +74,6 @@ class CExtractorDeclarations
74
74
  until scanner.eos?
75
75
  char = scanner.peek(1)
76
76
 
77
- # Safety check -- prevent infinite loops on malformed input
78
- if (scanner.pos - start_pos) > @max_line_length
79
- scanner.pos = start_pos
80
- return [false, nil]
81
- end
82
-
83
77
  # Handle string literals
84
78
  if in_string
85
79
  if char == '\\'
@@ -302,8 +296,8 @@ class CExtractorDeclarations
302
296
  #
303
297
  # Returns: CVariableDeclaration
304
298
  def parse_declaration(individual_text, original_text)
305
- decorators = extract_decorators(individual_text)
306
- clean_text = strip_decorators(individual_text, decorators)
299
+ decorators, remaining = extract_decorators(individual_text)
300
+ clean_text = normalize_declaration_whitespace(remaining)
307
301
  # Strip compiler extensions for name/type/array_suffix extraction only;
308
302
  # clean_text (stored as .text) retains attributes for correct compilation.
309
303
  extraction_text = @code_text.strip_compiler_extensions(clean_text)
@@ -321,9 +315,17 @@ class CExtractorDeclarations
321
315
  )
322
316
  end
323
317
 
324
- # Scan leading whole-word tokens against DECORATOR_KEYWORDS
318
+ # Storage-class specifiers and base-type qualifiers precede a C declaration's type as a
319
+ # contiguous run of keywords -- `static`, `extern`, and a `const`/`volatile` that qualifies
320
+ # the type itself all appear here (e.g. `static const int MAX;`). A qualifier bound to a
321
+ # pointer level instead, such as the second `const` in `char * const p;`, sits after the
322
+ # type and its `*` tokens rather than in this leading run, so it is grammatically part of
323
+ # the declarator, not a decorator. Peeling keywords off one at a time from the front, and
324
+ # stopping at the first non-keyword token, naturally separates the two: the same keyword
325
+ # (e.g. `const`) can appear once here and again later in the declarator without the two
326
+ # occurrences being confused with each other, since only the leading run is ever consumed.
325
327
  #
326
- # Returns: Array<String> ordered decorator keywords found at start of text
328
+ # Returns: [Array<String> ordered decorator keywords found at start of text, String remaining text after that run]
327
329
  def extract_decorators(text)
328
330
  decorators = []
329
331
  remaining = text.strip
@@ -339,18 +341,17 @@ class CExtractorDeclarations
339
341
  end
340
342
  break unless matched
341
343
  end
342
- decorators
344
+ [decorators, remaining]
343
345
  end
344
346
 
345
- # Remove decorator keywords from text and normalize whitespace
347
+ # A declaration with its leading decorator run already removed (see extract_decorators)
348
+ # still carries whatever whitespace and line breaks appeared in the source between its
349
+ # remaining tokens. This collapses that whitespace to single spaces and guarantees a
350
+ # trailing semicolon, so `.text` is stable, single-line C ready to compile.
346
351
  #
347
- # Returns: String with decorators removed, whitespace normalized, semicolon retained
348
- def strip_decorators(text, decorators)
349
- result = text.dup
350
- decorators.each do |kw|
351
- result.gsub!(/\b#{Regexp.escape(kw)}\b\s*/, '')
352
- end
353
- # Normalize whitespace but preserve semicolon
352
+ # Returns: String with whitespace normalized, semicolon retained
353
+ def normalize_declaration_whitespace(remaining)
354
+ result = remaining.dup
354
355
  result.gsub!(/\r\n|\r|\n|\t/, ' ')
355
356
  result.gsub!(/\s+/, ' ')
356
357
  result.strip!
@@ -15,13 +15,9 @@ class CExtractorFunctions
15
15
 
16
16
  constructor :c_extractor_code_text
17
17
 
18
- attr_writer :max_line_length
19
-
20
18
  def setup()
21
19
  # Aliases
22
20
  @code_text = @c_extractor_code_text
23
-
24
- @max_line_length = DEFAULT_MAX_LINE_LENGTH
25
21
  end
26
22
 
27
23
  def try_extract_function_declaration(scanner)
@@ -124,7 +120,10 @@ class CExtractorFunctions
124
120
  # - Before '{' for definitions)
125
121
  #
126
122
  # Safety:
127
- # Enforces max_line_length limit to prevent infinite loops on malformed input
123
+ # A signature with no terminating ')'/'{' runs the scan to the end of
124
+ # whatever buffer the caller (CExtractor#extract_next_feature) has grown so
125
+ # far; that caller is the one place enforcing an overall length ceiling,
126
+ # since only it knows how much more of the file remains to try growing into.
128
127
  def extract_function_signature(scanner, type)
129
128
  start_pos = scanner.pos
130
129
  # Tracks paren depth while accumulating candidate end-positions — not suitable for collect_balanced()
@@ -133,16 +132,10 @@ class CExtractorFunctions
133
132
  string_char = nil
134
133
  signature_candidates = [] # Track positions where paren_depth returns to 0
135
134
  found_opening_paren = false # Track if we've seen an opening paren
136
-
135
+
137
136
  until scanner.eos?
138
137
  char = scanner.peek(1)
139
138
 
140
- # Safety check
141
- if (scanner.pos - start_pos) > @max_line_length
142
- scanner.pos = start_pos
143
- return nil
144
- end
145
-
146
139
  # Handle string literals
147
140
  if in_string
148
141
  if char == '\\'
@@ -88,10 +88,16 @@ class Configurator
88
88
  @loginator.log( "Reverted :cmock ↳ :treat_inlines to :exclude because this CMock feature is superseded by Partials.", Verbosity::COMPLAIN, LogLabels::NOTICE )
89
89
  end
90
90
 
91
- # If partials enabled, inject partials name prefix symbol to all test compilation.
91
+ # If partials enabled, inject partials name prefix symbol to all test compilation
92
+ # and, if a project defines its own preprocess-scoped defines matcher, to
93
+ # preprocessing as well -- `:preprocess:` defines are looked up independently
94
+ # of `:test:` defines and only fall back to the latter when `:preprocess:`
95
+ # is absent entirely, so a project-defined `:preprocess:` matcher needs this
96
+ # symbol appended directly or Partials' own macro-based #includes break.
92
97
  # Handle both the simple list and matcher hash config formats.
93
98
  _partials_prefix_symbol = "CEEDLING_PARTIALS_PREFIX=#{PARTIAL_FILENAME_PREFIX}"
94
99
  ConfigMatchinator.append_matcher_entries( config[:defines][:test], _partials_prefix_symbol )
100
+ ConfigMatchinator.append_matcher_entries( config[:defines][:preprocess], _partials_prefix_symbol )
95
101
  end
96
102
 
97
103
 
@@ -750,6 +756,7 @@ class Configurator
750
756
  blotter &= @configurator_setup.validate_test_preprocessor( config )
751
757
  blotter &= @configurator_setup.validate_backtrace( config )
752
758
  blotter &= @configurator_setup.validate_threads( config )
759
+ blotter &= @configurator_setup.validate_partials( config )
753
760
  blotter &= @configurator_setup.validate_plugins( config )
754
761
 
755
762
  # Informational notices
@@ -744,6 +744,32 @@ class ConfiguratorSetup
744
744
  return valid
745
745
  end
746
746
 
747
+ # `:max_extraction_length` is a multiplier of 1000 characters, not a raw character count
748
+ # (project-file ergonomics -- `5000` reads more clearly than `5000000`). The minimum here
749
+ # keeps that ceiling comfortably above any ordinary single C declaration or function
750
+ # signature, catching a value so small it would defeat the setting's own purpose (e.g.
751
+ # someone setting `1000` expecting "1000 characters" rather than "1000x characters").
752
+ def validate_partials(config)
753
+ valid = true
754
+
755
+ max_extraction_length = config[:partials][:max_extraction_length]
756
+
757
+ walk = @reportinator.generate_config_walk( [:partials, :max_extraction_length] )
758
+
759
+ case max_extraction_length
760
+ when Integer
761
+ if max_extraction_length < 10
762
+ @loginator.log( "#{walk} must be at least 10 (10,000 characters)", Verbosity::ERRORS )
763
+ valid = false
764
+ end
765
+ else
766
+ @loginator.log( "#{walk} is not an integer", Verbosity::ERRORS )
767
+ valid = false
768
+ end
769
+
770
+ return valid
771
+ end
772
+
747
773
  def validate_plugins(config)
748
774
  missing_plugins =
749
775
  Set.new( config[:plugins][:enabled] ) -
@@ -69,7 +69,10 @@ DEFAULT_TEST_FIXTURE_SIMPLE_BACKTRACE_TOOL = {
69
69
  :name => 'default_test_fixture_simple_backtrace'.freeze,
70
70
  :optional => false.freeze,
71
71
  :arguments => [
72
- '-n ${2}'.freeze # Exact test case name matching flag
72
+ # Test case matching flag and (quoted) value, fully composed by the caller as
73
+ # either `-n "<exact name>"` or `-f "<base name>"` -- see
74
+ # GeneratorTestResultsBacktrace#unity_filter_arg.
75
+ '${2}'.freeze
73
76
  ].freeze
74
77
  }
75
78
 
@@ -202,7 +205,10 @@ DEFAULT_TEST_BACKTRACE_GDB_TOOL = {
202
205
  "--command \"${1}\"".freeze, # Debug script file to run
203
206
  '--args'.freeze,
204
207
  '${2}'.freeze, # Test executable
205
- '-n ${3}'.freeze # Exact test case name matching flag
208
+ # Test case matching flag and (quoted) value, fully composed by the caller as
209
+ # either `-n "<exact name>"` or `-f "<base name>"` -- see
210
+ # GeneratorTestResultsBacktrace#unity_filter_arg.
211
+ '${3}'.freeze
206
212
  ].freeze
207
213
  }
208
214
 
@@ -289,6 +295,14 @@ DEFAULT_CEEDLING_PROJECT_CONFIG = {
289
295
  :preprocess_force_fallback => false,
290
296
  },
291
297
 
298
+ :partials => {
299
+ # A single C construct (declaration, definition, directive, etc.) is extracted into a
300
+ # growing buffer; this bounds how large that buffer may grow before extraction fails
301
+ # outright rather than silently dropping an oversized construct and everything after
302
+ # it in the file. Value is a multiplier of 1000 characters -- 5000 means 5,000,000.
303
+ :max_extraction_length => 5000,
304
+ },
305
+
292
306
  # Unlike other top-level entries, :environment is an array (of hashes) to preserve order
293
307
  :environment => [],
294
308
 
@@ -303,6 +303,13 @@ class FilePathUtils
303
303
  return PARTIAL_FILENAME_PREFIX + _module + '_interface' + EXTENSION_CORE_HEADER
304
304
  end
305
305
 
306
+ # A module's typedefs and aggregate (struct/enum/union) definitions are generated into this
307
+ # standalone header, shared by both the implementation and interface headers below, so a
308
+ # module tested and mocked in the same file never has its types defined more than once.
309
+ def form_partial_types_header_filename(_module)
310
+ return PARTIAL_FILENAME_PREFIX + _module + '_types' + EXTENSION_CORE_HEADER
311
+ end
312
+
306
313
  def form_mock_partial_interface_header_filename(_module)
307
314
  return @configurator.cmock_mock_prefix + PARTIAL_FILENAME_PREFIX + _module + '_interface' + EXTENSION_CORE_HEADER
308
315
  end
@@ -9,6 +9,7 @@ require 'rubygems'
9
9
  require 'rake' # for FileList
10
10
  require 'fileutils'
11
11
  require 'pathname'
12
+ require 'tmpdir'
12
13
  require 'ceedling/constants'
13
14
 
14
15
 
@@ -132,4 +133,11 @@ class FileWrapper
132
133
  return FileUtils.mkdir_p(folder)
133
134
  end
134
135
 
136
+ # Creates a uniquely-named, empty directory nested inside `parent` and returns its path.
137
+ # `parent` must already exist. Collision-free even across concurrent callers targeting
138
+ # the same `parent` -- Dir.mktmpdir retries internally on name clash.
139
+ def mkdir_tmp(prefix, parent)
140
+ return Dir.mktmpdir(prefix, parent)
141
+ end
142
+
135
143
  end
@@ -35,6 +35,21 @@ class Generator
35
35
  @backtrace = @generator_test_results_backtrace
36
36
  end
37
37
 
38
+ # A module's typedefs and aggregate definitions are identical regardless of whether a test
39
+ # file tests it, mocks it, or both, so they're generated once into their own header here
40
+ # rather than duplicated by generate_partial_implementation and generate_partial_interface.
41
+ # Returns the bare generated filename (for the caller to add to each header's own includes
42
+ # list) or nil when the module has no typedefs or aggregate definitions to share.
43
+ def generate_partial_types(name:, c_module:, output_path:)
44
+ arg_hash = {
45
+ :name => name,
46
+ :c_module => c_module,
47
+ :output_path => output_path
48
+ }
49
+
50
+ return @generator_partials.generate_types( **arg_hash )
51
+ end
52
+
38
53
  def generate_partial_interface(test:, partial:, function_declarations:, includes:, c_module:, input_filepath:, output_path:)
39
54
  msg = @reportinator.generate_module_progress(
40
55
  operation: "Generating Partial mockable interface for",
@@ -18,21 +18,40 @@ class GeneratorHelper
18
18
 
19
19
  crash = false
20
20
 
21
- # Unix Signal 11 ==> SIGSEGV
22
- # Applies to Unix-like systems including MSYS on Windows
23
- if (shell_result[:status].termsig == 11)
24
- @loginator.lazy( Verbosity::DEBUG, LogLabels::CRASH ) do
25
- "#{runner} process terminated with SIGSEGV (Unix Signal 11)"
21
+ # Any signal death is unconditionally a crash, whatever text made it out beforehand.
22
+ # Broadened from a SIGSEGV(11)-only check: a sanitizer's halt_on_error (UBSan, ASan)
23
+ # raises SIGABRT, not SIGSEGV, and there's no reason to trust a process that a
24
+ # signal actually killed regardless of which one. Applies to Unix-like systems
25
+ # including MSYS on Windows -- native (non-MSYS) Windows Ruby never reports
26
+ # `signaled?` true at all, so this check is inert there rather than wrong; the
27
+ # stats-footer and stderr checks below already carry Windows crash detection.
28
+ if shell_result[:status] && shell_result[:status].signaled?
29
+ @loginator.lazy( Verbosity::DEBUG, LogLabels::CRASH ) do
30
+ "#{runner} process terminated by signal #{shell_result[:status].termsig}"
26
31
  end
27
32
  crash = true
28
33
  end
29
34
 
30
35
  # No test results found in test executable output
31
- if (shell_result[:output] =~ PATTERNS::TEST_STDOUT_STATISTICS).nil?
36
+ stats_match = shell_result[:output].match( PATTERNS::TEST_STDOUT_STATISTICS )
37
+ if stats_match.nil?
32
38
  # No debug logging here because we log this condition in the error log handling below
33
39
  crash = true
34
40
  end
35
41
 
42
+ # A clean summary (0 failures) is not trustworthy if the real process still exited
43
+ # nonzero afterward -- e.g. LeakSanitizer detecting a leak during teardown, after
44
+ # every test case already printed and Unity's own summary already looked clean.
45
+ # A nonzero exit consistent with Unity's own reported failures (its exit code
46
+ # convention is one per failed test case) is an ordinary failing run, not a crash.
47
+ if stats_match && shell_result[:status] && !shell_result[:status].success? &&
48
+ !shell_result[:status].signaled? && stats_match[2].to_i == 0
49
+ @loginator.lazy( Verbosity::DEBUG, LogLabels::CRASH ) do
50
+ "#{runner} reported a clean summary but exited with real status #{shell_result[:status].exitstatus}"
51
+ end
52
+ crash = true
53
+ end
54
+
36
55
  # Scan STDERR line by line for a segfault variant that is not attributed to the test file.
37
56
  # A line starting with test_filename is a Unity-reported test-case result, not an OS crash.
38
57
  # Checking each line individually avoids false negatives when attributed and bare segfault
@@ -8,6 +8,7 @@
8
8
  require 'ceedling/file_wrapper'
9
9
  require 'ceedling/partials/partials'
10
10
  require 'ceedling/c_extractor/c_extractor_types'
11
+ require 'ceedling/c_extractor/c_extractor_constants'
11
12
 
12
13
  class GeneratorPartials
13
14
 
@@ -55,8 +56,75 @@ class GeneratorPartials
55
56
  return filepath
56
57
  end
57
58
 
59
+ # A module's typedefs and aggregate (struct/enum/union) definitions are the same
60
+ # regardless of which functions a given test file chooses to test versus mock, so they're
61
+ # generated once here into their own header rather than by generate_header itself. Both
62
+ # the implementation and interface headers then simply #include this file, which means a
63
+ # module tested and mocked in the same test file -- each side producing its own generated
64
+ # header -- never ends up with two separate C definitions of the same type in one
65
+ # translation unit. Nothing is written and nil is returned when a module has no such
66
+ # content, since there's then nothing to share.
67
+ #
68
+ # @param name [String] Partial module name (used to form the filename and include guard)
69
+ # @param c_module [CExtractorTypes::CModule] Merged module with type_definitions/aggregate_definitions
70
+ # @param output_path [String] Directory shared with the implementation and interface headers
71
+ # @return [String, nil] The bare filename (for use as a sibling #include), or nil if nothing was generated
72
+ def generate_types(name:, c_module:, output_path:)
73
+ return nil if c_module.type_definitions.empty? && c_module.aggregate_definitions.empty?
74
+
75
+ header = @file_path_utils.form_partial_types_header_filename(name)
76
+ filepath = File.join(output_path, header)
77
+
78
+ # Binary mode: see generate_implementation above.
79
+ @file_wrapper.open(filepath, 'wb') do |file|
80
+ guard = FileWrapper.generate_include_guard(header)
81
+ file << "#ifndef #{guard}\n"
82
+ file << "#define #{guard}\n\n"
83
+
84
+ anything_emitted = false
85
+ c_module.element_sequence.each do |item|
86
+ next unless item.is_a?(CExtractorTypes::CStatement) && type_defining?(item, c_module)
87
+ file << item.text << "\n"
88
+ anything_emitted = true
89
+ end
90
+
91
+ file << "\n" if anything_emitted
92
+ file << "#endif // #{guard}\n\n"
93
+ end
94
+
95
+ return header
96
+ end
97
+
58
98
  private
59
99
 
100
+ # A typedef or a non-typedef struct/enum/union tag definition establishes a type; C treats
101
+ # a second definition of the same type in one translation unit as a redefinition error even
102
+ # when the two definitions are textually identical. A macro statement carries no such
103
+ # restriction -- an identical #define may legally repeat -- so only membership in
104
+ # type_definitions or aggregate_definitions marks a CStatement as type-defining content
105
+ # that belongs in the shared types header instead of being emitted inline.
106
+ def type_defining?(item, c_module)
107
+ c_module.type_definitions.include?(item) || c_module.aggregate_definitions.include?(item)
108
+ end
109
+
110
+ # A `CVariableDeclaration`'s `decorators` mixes storage-class specifiers (`static`,
111
+ # `extern`) with base-type qualifiers (`const`, `volatile`, `restrict`) in their
112
+ # original source order, since both kinds of keyword lead a declaration the same
113
+ # way. An `extern` declaration and the variable's own definition both need the
114
+ # qualifiers reasserted -- omitting them produces a type that disagrees with any
115
+ # other declaration of the same variable elsewhere in the same translation unit --
116
+ # but neither may carry a storage-class specifier: `static` would contradict the
117
+ # external linkage the Partial is establishing, and `extern` is supplied literally
118
+ # by the caller. Filtering to TYPE_QUALIFIER_KEYWORDS keeps only what belongs on
119
+ # both forms.
120
+ #
121
+ # @param decl [CExtractorTypes::CVariableDeclaration]
122
+ # @return [String] e.g. "volatile " / "const volatile " / "" (trailing space when non-empty)
123
+ def qualifier_prefix(decl)
124
+ quals = decl.decorators.select { |kw| CExtractorConstants::TYPE_QUALIFIER_KEYWORDS.include?(kw) }
125
+ quals.empty? ? '' : "#{quals.join(' ')} "
126
+ end
127
+
60
128
  # Emit a partial header file.
61
129
  #
62
130
  # Iterates c_module.element_sequence to emit non-function items (macros, typedefs,
@@ -101,13 +169,21 @@ class GeneratorPartials
101
169
  c_module.element_sequence.each do |item|
102
170
  case item
103
171
  when CExtractorTypes::CStatement
172
+ # Typedefs and aggregate definitions live in the shared types header generated by
173
+ # generate_types instead of here, so that content defines a type exactly once no
174
+ # matter how many of a module's generated headers end up in the same test file.
175
+ next if type_defining?(item, c_module)
104
176
  io << item.text << "\n"
105
177
  last_was_func = false
106
178
  anything_emitted = true
107
179
  when CExtractorTypes::CVariableDeclaration
108
180
  next unless include_variables
109
- # If there is no array involved, array_suffix collapses to an empty string
110
- io << "extern #{item.type} #{item.name}#{item.array_suffix};\n"
181
+ # If there is no array involved, array_suffix collapses to an empty string.
182
+ # A leading const/volatile/restrict on the original declaration (see
183
+ # qualifier_prefix) is part of the variable's type and belongs here too,
184
+ # so this extern agrees with the definition and any other declaration of
185
+ # the same variable elsewhere in the translation unit.
186
+ io << "extern #{qualifier_prefix(item)}#{item.type} #{item.name}#{item.array_suffix};\n"
111
187
  last_was_func = false
112
188
  anything_emitted = true
113
189
  when CExtractorTypes::CFunctionDefinition, CExtractorTypes::CFunctionDeclaration
@@ -170,7 +246,12 @@ class GeneratorPartials
170
246
  c_module.element_sequence.each do |item|
171
247
  case item
172
248
  when CExtractorTypes::CVariableDeclaration
173
- io << "#{item.text}\n"
249
+ # `item.text` is the declaration with its leading decorator run removed (see
250
+ # CExtractorDeclarations#extract_decorators), so a const/volatile/restrict
251
+ # qualifier on the base type is reapplied here via qualifier_prefix -- this
252
+ # definition's type must match the extern declaration emitted for the same
253
+ # variable in generate_header.
254
+ io << "#{qualifier_prefix(item)}#{item.text}\n"
174
255
  last_was_func = false
175
256
  anything_emitted = true
176
257
  when CExtractorTypes::CFunctionDefinition