Class: Partializer

Inherits:
Object show all
Includes:
Partials
Defined in:
lib/ceedling/partials/partializer.rb

Constant Summary

Constants included from Partials

Partials::ACCUMULATE, Partials::DEDUCT, Partials::MOCK_PRIVATE, Partials::MOCK_PUBLIC, Partials::PRIVATE, Partials::PUBLIC, Partials::TEST_PRIVATE, Partials::TEST_PUBLIC

Instance Method Summary collapse

Methods included from Partials

manufacture_function_declaration, manufacture_function_definition

Instance Method Details

#extract_implementation_functions(test:, partial:, definitions:, config:) ⇒ Array<Partials::FunctionDefinition>

Returns ArrayPartials::FunctionDefinition for the testable partial implementation.

Processes the tests PartialFunctions config against extracted C function definitions:

PUBLIC     -- initial list is all non-private functions; additions inject named private functions
PRIVATE    -- initial list is all private functions; additions inject named public functions
ACCUMULATE -- initial list is empty; additions fill it entirely
nil        -- returns nil

Subtractions remove named functions from the assembled list. Any functions in mocks.additions are also removed from the final result. Parameters are expected to be pre-validated (no unknown names, no overlap, etc.).

Parameters:

  • test (String)

    Test file name (for log messages)

  • partial (String)

    Partial module name (for log messages)

  • definitions (Array<CFunctionDefinition>)

    Extracted function definitions

  • config (PartializerConfig::Config)

    Full partial config for the module

Returns:



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/ceedling/partials/partializer.rb', line 273

def extract_implementation_functions(test:, partial:, definitions:, config:)
  pf = config.tests
  return nil if pf.type.nil?

  @loginator.log(
    "Extracting testable Partial functions for #{test}::#{partial}: " \
    "type=#{pf.type} additions=#{pf.additions} subtractions=#{pf.subtractions}",
    Verbosity::DEBUG
  )

  # Build initial list by visibility; ACCUMULATE yields []
  funcs = @helper.filter_and_transform_funcs(definitions, pf.type, :impl)

  # Additions: only search definitions — code_block required for impl transform
  pf.additions.each do |name|
    next if funcs.any? { |f| f.name == name }
    func = @helper.find_and_transform_func(
      name:            name,
      primary_funcs:   definitions,
      secondary_funcs: [],
      output_type:     :impl
    )
    funcs << func if func
  end

  # Subtractions: remove named functions from list
  result = @helper.subtract_funcs(funcs: funcs, names: pf.subtractions)
  if !funcs.empty? && result.empty?
    @loginator.log(
      "Partial #{test}::#{partial} ⏩️ Subtractions left no testable functions",
      Verbosity::COMPLAIN,
      LogLabels::NOTICE
    )
  end

  # Remove any functions explicitly claimed by the mock side
  result = @helper.subtract_funcs(funcs: result, names: config.mocks.additions)

  _log_impl_functions(test, partial, result)

  return result
end

#extract_interface_functions(test:, partial:, definitions:, declarations:, config:) ⇒ Array<Partials::FunctionDeclaration>

Returns ArrayPartials::FunctionDeclaration for the mockable partial interface.

Processes the mocks PartialFunctions config against extracted C functions:

PUBLIC     -- initial list is all non-private functions; additions inject named private functions
PRIVATE    -- initial list is all private functions; additions inject named public functions
ACCUMULATE -- initial list is empty; additions fill it entirely
nil        -- returns nil

Subtractions remove named functions from the assembled list. Any functions in tests.additions are also removed from the final result. Parameters are expected to be pre-validated (no unknown names, no overlap, etc.). Additions search definitions first, then declarations; only the first match is used.

Parameters:

  • test (String)

    Test file name (for log messages)

  • partial (String)

    Partial module name (for log messages)

  • definitions (Array<CFunctionDefinition>)

    Extracted function definitions

  • declarations (Array<CFunctionDeclaration>)

    Extracted function declarations

  • config (PartializerConfig::Config)

    Full partial config for the module

Returns:



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/ceedling/partials/partializer.rb', line 334

def extract_interface_functions(test:, partial:, definitions:, declarations:, config:)
  pf = config.mocks
  return nil if pf.type.nil?

  @loginator.log(
    "Extracting mockable Partial functions for #{test}::#{partial}: " \
    "type=#{pf.type} additions=#{pf.additions} subtractions=#{pf.subtractions}",
    Verbosity::DEBUG
  )

  # Build initial list by visibility; ACCUMULATE yields []
  funcs = @helper.filter_and_transform_funcs(definitions, pf.type, :interface)

  # Additions: search definitions first, then declarations
  pf.additions.each do |name|
    next if funcs.any? { |f| f.name == name }
    func = @helper.find_and_transform_func(
      name:            name,
      primary_funcs:   definitions,
      secondary_funcs: declarations,
      output_type:     :interface
    )
    funcs << func if func
  end

  # Subtractions: remove named functions from list
  result = @helper.subtract_funcs(funcs: funcs, names: pf.subtractions)
  if !funcs.empty? && result.empty?
    @loginator.log(
      "Partial #{test}::#{partial} ⏩️ Subtractions left no mockable signatures",
      Verbosity::COMPLAIN,
      LogLabels::NOTICE
    )
  end

  # Remove any functions explicitly claimed by the test side
  result = @helper.subtract_funcs(funcs: result, names: config.tests.additions)

  _log_interface_functions(test, partial, result)

  return result
end

#extract_module_contents(name, config, fallback) ⇒ CExtractorTypes::CModule

Note:

The method always starts with an empty CModule and merges in contents from any provided files using the CModule's + operator for combining structures.

Extracts and combines C code contents from header and source files

This method uses CExtractor to parse C files and extract their contents including function definitions, function declarations, and variable declarations. If both header and source files are provided, their contents are merged into a single CModule structure.

Parameters:

  • header_filepath (String, nil)

    Path to the header file to extract from. If nil, no header content is extracted.

  • source_filepath (String, nil)

    Path to the source file to extract from. If nil, no source content is extracted.

Returns:

  • (CExtractorTypes::CModule)

    A merged CModule containing all extracted contents from both files. The structure includes:

    • function_definitions: Array of function definitions with full implementations
    • function_declarations: Array of function declarations (prototypes)
    • variable_declarations: Array of variable declarations


201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/ceedling/partials/partializer.rb', line 201

def extract_module_contents(name, config, fallback)
  # Array for CModule structs
  contents = [CExtractorTypes::CModule.new()]

  # Process the C module source and/or header associated with the Partial config
  [config.header, config.source].zip(['header', 'source']).each do |c_file, file_type|
    # Do nothing if there's no directives-only preprocessed filepath (e.g. no source only header for a Partial mock)
    next unless c_file.directives_only_filepath

    c_module = @c_extractor.from_file( c_file.directives_only_filepath )

    _log_module_contents(name, config.module, file_type, c_module)

    # Update function signatures from fully preprocessed output when available.
    # Replaces signature/decorators/signature_stripped (but NOT code_block) so that
    # macros wrapping `static` and `inline` are resolved before visibility filtering.
    if c_file.full_expansion_filepath
      @helper.update_signatures_from_full_expansion(
        funcs:                   c_module.function_definitions,
        full_expansion_filepath: c_file.full_expansion_filepath,
        name:                    name,
        module_name:             config.module,
        file_type:               file_type
      )
    end

    # Align extracted function definitions with line markers in preprocessor output.
    # This perfectly remaps functions found in expanded preprocessor output with 
    # original source location.
    # This routine depends on original, unaltered function definitions.
    @helper.associate_function_line_numbers(
      name: name,
      funcs: c_module.function_definitions,
      filepath: c_file.filepath,
      fallback: fallback
    )

    # 1. Find any function-scope static variable declarations.
    # 2. Replace them in function definitions with no-ops (for proper coverage reporting).
    # 3. Promote the function-scoped variables to be module-level variables.
    decls = @helper.extract_function_scope_static_vars(
      c_module.function_definitions,
      name: name, module_name: config.module, file_type: file_type
    )
    c_module.variable_declarations.concat(decls)
    c_module.element_sequence.concat(decls) unless decls.empty?

    contents << c_module
  end

  # Use `+` operator for CModule to merge everything
  contents = contents.reduce(&:+)

  return contents
end

#populate_filepaths(configs) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/ceedling/partials/partializer.rb', line 71

def populate_filepaths(configs)
  configs.each do |_module, config|
    # Every partial involves processing header files
    config.header.filepath = @file_finder.find_header_file(_module, :ignore)

    # Source file not needed only when mocking public functions exclusively
    unless !config.tests.present? && config.mocks.type == PUBLIC
      config.source.filepath = @file_finder.find_source_file(_module, :ignore)
    end
  end

  return configs
end

#remap_implementation_header_includes(name:, includes:, partials:, test: nil) ⇒ Object

When test: is provided, logs the resulting includes at OBNOXIOUS.



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/ceedling/partials/partializer.rb', line 86

def remap_implementation_header_includes(name:, includes:, partials:, test: nil)
  _includes = includes.clone()

  # Get list of all partialized module names
  partialized_modules = partials.keys

  # Remove includes for all partialized modules
  # Remove our own orginal name as well
  _includes = remove_matching_includes(
    includes: _includes,
    modules: ([name] + partialized_modules)
  )

  # Remove any duplicates
  Includes.sanitize!(_includes)

  @loginator.log_list(
    _includes,
    "Header includes to inject for testable Partial #{test}::#{name}:",
    Verbosity::OBNOXIOUS
  ) if test

  return _includes
end

#remap_implementation_source_includes(name:, includes:, partials:, test: nil) ⇒ Object

When test: is provided, logs the resulting includes at OBNOXIOUS.



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/ceedling/partials/partializer.rb', line 112

def remap_implementation_source_includes(name:, includes:, partials:, test: nil)
  _includes = includes.clone()

  # Add implementation header
  _includes << UserInclude.new(
    @file_path_utils.form_partial_implementation_header_filename(name)
  )

  mockable_modules = []

  partials.each do |_module, config|
    # Remap mockable interface headers that will be injected into generated partial implementation
    if includes.any? { |include| include.filename.ext().downcase() == _module.downcase() }
      if [PUBLIC, PRIVATE].include?( config.mocks.type )
        # Insert mockable interface header from remapping of module name
        _includes << UserInclude.new(
          @file_path_utils.form_partial_interface_header_filename(_module)
        )
        # Remember the module for later removal of original header
        mockable_modules << _module
      end
    end
  end

  # Remove the original module header now that it's remapped to mockable interface
  # Remove our own orginal name as well
  _includes = remove_matching_includes(
    includes: _includes,
    modules: ([name] + mockable_modules)
  )

  # Remove any duplicates
  Includes.sanitize!(_includes)

  @loginator.log_list(
    _includes,
    "Source includes to inject for testable Partial #{test}::#{name}:",
    Verbosity::OBNOXIOUS
  ) if test

  return _includes
end

#remap_interface_header_includes(name:, includes:, partials:, test: nil) ⇒ Object

When test: is provided, logs the resulting includes at OBNOXIOUS.



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/ceedling/partials/partializer.rb', line 156

def remap_interface_header_includes(name:, includes:, partials:, test: nil)
  _includes = includes.clone()

  # Get list of all partialized module names
  partialized_modules = partials.keys

  # Remove includes for all partialized modules
  # Remove our own orginal name as well
  _includes = remove_matching_includes(
    includes: _includes,
    modules: ([name] + partialized_modules)
  )

  # Remove any duplicates
  Includes.sanitize!(_includes)

  @loginator.log_list(
    _includes,
    "Header includes to inject for mockable Partial #{test}::#{name}:",
    Verbosity::OBNOXIOUS
  ) if test

  return _includes
end

#sanitize(c_module) ⇒ Object



37
38
39
40
41
42
43
44
# File 'lib/ceedling/partials/partializer.rb', line 37

def sanitize(c_module)
  # Remove macro definitions that contain the CEEDLING_GENERATED sentinel string.
  # These are include-guard and boilerplate macros injected into Ceedling-generated header files.
  removed = c_module.macro_definitions.select { |m| m.text.include?(CEEDLING_GENERATED) }
  # Remove from both the macro_definitions list and the element_sequence that references it
  c_module.macro_definitions.reject! { |m| m.text.include?(CEEDLING_GENERATED) }
  c_module.element_sequence.reject!  { |e| removed.include?(e) }
end

#setupObject



24
25
26
27
# File 'lib/ceedling/partials/partializer.rb', line 24

def setup()
  # Alias
  @helper = @partializer_helper
end

#validate_config(c_module:, config:, name:) ⇒ Object



29
30
31
32
33
34
35
# File 'lib/ceedling/partials/partializer.rb', line 29

def validate_config(c_module:, config:, name:)
  msg = @reportinator.generate_progress("Validating Partial config for '#{name}'")
  @loginator.log(msg, Verbosity::DEBUG)
  @helper.validate_function_names_exist(c_module, config, name)
  @helper.validate_no_additions_subtractions_overlap(config, name)
  @helper.validate_additions_subtractions_visibility(c_module, config, name)
end

#validate_extracted_functions(name:, partial:, impl:, interface:) ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/ceedling/partials/partializer.rb', line 46

def validate_extracted_functions(name:, partial:, impl:, interface:)
  # Validation is only meaningful and possible if both references are non-nil.
  return if impl.nil? || interface.nil?

  # Validation is only meaningful if both lists have content.
  return if impl.empty? || interface.empty?

  impl_names      = Set.new(impl.map(&:name))
  interface_names = Set.new(interface.map(&:name))

  msg = @reportinator.generate_module_progress(
    module_name: name,
    filename: partial,
    operation: 'Validating Partial functions for'
  )
  @loginator.log(msg, Verbosity::DEBUG)

  overlap = impl_names & interface_names
  overlap.each do |func_name|
    raise CeedlingException.new(
      "#{name}: Partial '#{partial}' ⏩️ Function '#{func_name}' cannot be both testable and mockable"
    )
  end
end