ceedling 1.1.3 → 1.1.5

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 +32 -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 +103 -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 +21 -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 +4258 -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 +223 -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
@@ -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 == '\\'
@@ -756,6 +756,7 @@ class Configurator
756
756
  blotter &= @configurator_setup.validate_test_preprocessor( config )
757
757
  blotter &= @configurator_setup.validate_backtrace( config )
758
758
  blotter &= @configurator_setup.validate_threads( config )
759
+ blotter &= @configurator_setup.validate_partials( config )
759
760
  blotter &= @configurator_setup.validate_plugins( config )
760
761
 
761
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] ) -
@@ -295,6 +295,14 @@ DEFAULT_CEEDLING_PROJECT_CONFIG = {
295
295
  :preprocess_force_fallback => false,
296
296
  },
297
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
+
298
306
  # Unlike other top-level entries, :environment is an array (of hashes) to preserve order
299
307
  :environment => [],
300
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,94 @@ 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
+ pending_macros = []
86
+
87
+ c_module.element_sequence.each do |item|
88
+ next unless item.is_a?(CExtractorTypes::CStatement)
89
+
90
+ if c_module.macro_definitions.include?(item)
91
+ pending_macros << item
92
+ next
93
+ end
94
+
95
+ next unless type_defining?(item, c_module)
96
+
97
+ # A typedef/struct/enum moved here may depend on a macro (e.g. an array-size
98
+ # constant) that preceded it in the original source -- this header is #included
99
+ # before any macro generate_header later emits inline into the non-shared
100
+ # header, so that macro would otherwise be undefined here. Carrying it forward
101
+ # is safe: an identical #define may legally repeat, so generate_header's own
102
+ # unmodified inline copy causes no redefinition conflict.
103
+ pending_macros.each { |macro| file << macro.text << "\n" }
104
+ pending_macros.clear
105
+
106
+ file << item.text << "\n"
107
+ anything_emitted = true
108
+ end
109
+
110
+ file << "\n" if anything_emitted
111
+ file << "#endif // #{guard}\n\n"
112
+ end
113
+
114
+ return header
115
+ end
116
+
58
117
  private
59
118
 
119
+ # A typedef or a non-typedef struct/enum/union tag definition establishes a type; C treats
120
+ # a second definition of the same type in one translation unit as a redefinition error even
121
+ # when the two definitions are textually identical. A macro statement carries no such
122
+ # restriction -- an identical #define may legally repeat -- so only membership in
123
+ # type_definitions or aggregate_definitions marks a CStatement as type-defining content
124
+ # that belongs in the shared types header instead of being emitted inline.
125
+ def type_defining?(item, c_module)
126
+ c_module.type_definitions.include?(item) || c_module.aggregate_definitions.include?(item)
127
+ end
128
+
129
+ # A `CVariableDeclaration`'s `decorators` mixes storage-class specifiers (`static`,
130
+ # `extern`) with base-type qualifiers (`const`, `volatile`, `restrict`) in their
131
+ # original source order, since both kinds of keyword lead a declaration the same
132
+ # way. An `extern` declaration and the variable's own definition both need the
133
+ # qualifiers reasserted -- omitting them produces a type that disagrees with any
134
+ # other declaration of the same variable elsewhere in the same translation unit --
135
+ # but neither may carry a storage-class specifier: `static` would contradict the
136
+ # external linkage the Partial is establishing, and `extern` is supplied literally
137
+ # by the caller. Filtering to TYPE_QUALIFIER_KEYWORDS keeps only what belongs on
138
+ # both forms.
139
+ #
140
+ # @param decl [CExtractorTypes::CVariableDeclaration]
141
+ # @return [String] e.g. "volatile " / "const volatile " / "" (trailing space when non-empty)
142
+ def qualifier_prefix(decl)
143
+ quals = decl.decorators.select { |kw| CExtractorConstants::TYPE_QUALIFIER_KEYWORDS.include?(kw) }
144
+ quals.empty? ? '' : "#{quals.join(' ')} "
145
+ end
146
+
60
147
  # Emit a partial header file.
61
148
  #
62
149
  # Iterates c_module.element_sequence to emit non-function items (macros, typedefs,
@@ -101,13 +188,21 @@ class GeneratorPartials
101
188
  c_module.element_sequence.each do |item|
102
189
  case item
103
190
  when CExtractorTypes::CStatement
191
+ # Typedefs and aggregate definitions live in the shared types header generated by
192
+ # generate_types instead of here, so that content defines a type exactly once no
193
+ # matter how many of a module's generated headers end up in the same test file.
194
+ next if type_defining?(item, c_module)
104
195
  io << item.text << "\n"
105
196
  last_was_func = false
106
197
  anything_emitted = true
107
198
  when CExtractorTypes::CVariableDeclaration
108
199
  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"
200
+ # If there is no array involved, array_suffix collapses to an empty string.
201
+ # A leading const/volatile/restrict on the original declaration (see
202
+ # qualifier_prefix) is part of the variable's type and belongs here too,
203
+ # so this extern agrees with the definition and any other declaration of
204
+ # the same variable elsewhere in the translation unit.
205
+ io << "extern #{qualifier_prefix(item)}#{item.type} #{item.name}#{item.array_suffix};\n"
111
206
  last_was_func = false
112
207
  anything_emitted = true
113
208
  when CExtractorTypes::CFunctionDefinition, CExtractorTypes::CFunctionDeclaration
@@ -170,7 +265,12 @@ class GeneratorPartials
170
265
  c_module.element_sequence.each do |item|
171
266
  case item
172
267
  when CExtractorTypes::CVariableDeclaration
173
- io << "#{item.text}\n"
268
+ # `item.text` is the declaration with its leading decorator run removed (see
269
+ # CExtractorDeclarations#extract_decorators), so a const/volatile/restrict
270
+ # qualifier on the base type is reapplied here via qualifier_prefix -- this
271
+ # definition's type must match the extern declaration emitted for the same
272
+ # variable in generate_header.
273
+ io << "#{qualifier_prefix(item)}#{item.text}\n"
174
274
  last_was_func = false
175
275
  anything_emitted = true
176
276
  when CExtractorTypes::CFunctionDefinition