From 8677888101d340849fba258f8d8ec919a1fd367a Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Thu, 20 Aug 2026 17:38:31 -0400 Subject: [PATCH 1/2] Promote directives_only_fallback? into TestPipelineHelpers Needed by more than one pipeline-stage class once Partials preprocessing moves into its own class next. Co-Authored-By: Claude Sonnet 5 --- lib/ceedling/test_invoker/test_build_executor.rb | 8 -------- lib/ceedling/test_invoker/test_pipeline_helpers.rb | 8 ++++++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/ceedling/test_invoker/test_build_executor.rb b/lib/ceedling/test_invoker/test_build_executor.rb index 958f6be5..8c8f2195 100644 --- a/lib/ceedling/test_invoker/test_build_executor.rb +++ b/lib/ceedling/test_invoker/test_build_executor.rb @@ -833,14 +833,6 @@ def get_library_paths_to_arguments() private - # A preprocessing pass falls back to plain, non-directives-only handling either when - # directives-only support isn't available at all for this toolchain, or when this - # particular target's own directives-only output failed to generate (see - # generate_directives_only_output) despite support existing project-wide. - def directives_only_fallback?(directives_only, directives_only_filepath) - !directives_only or directives_only_filepath.nil? - end - # Shared body for stages 6 and 7 -- a partial's header and source files go through # identical preprocessing, differing only in which of `state.partials_headers`/ # `state.partials_sources` supplies the work and which Preprocessinator methods diff --git a/lib/ceedling/test_invoker/test_pipeline_helpers.rb b/lib/ceedling/test_invoker/test_pipeline_helpers.rb index 0bc42597..9f85526d 100644 --- a/lib/ceedling/test_invoker/test_pipeline_helpers.rb +++ b/lib/ceedling/test_invoker/test_pipeline_helpers.rb @@ -35,6 +35,14 @@ def dependency_meta(flags:, defines:, search_paths:) { flags: flags, defines: defines, search_paths: search_paths } end + # A preprocessing pass falls back to plain, non-directives-only handling either when + # directives-only support isn't available at all for this toolchain, or when this + # particular target's own directives-only output failed to generate (see + # generate_directives_only_output) despite support existing project-wide. + def directives_only_fallback?(directives_only, directives_only_filepath) + !directives_only or directives_only_filepath.nil? + end + # A Partial mock is Ceedling's own generated content, identifiable by its own naming # convention rather than by any real header it corresponds to (it has none). def mock_partial?(include) From c266f28ff41b7664e3fc17f60e24f0bc3ecbe218 Mon Sep 17 00:00:00 2001 From: Michael Karlesky Date: Thu, 20 Aug 2026 17:45:37 -0400 Subject: [PATCH 2/2] Extract Partials generation (stages 6-8) into a new PartialsManager class TestBuildExecutor owned every stage from preprocessing through execution; the three Partials stages (preprocessing partial headers/ sources, then extracting and generating implementation/interface content) are self-contained beyond the TestPipelineHelpers mixin already shared across this directory's pipeline classes, and are 446 lines on their own -- pulled into their own class, wired into TestPipelineManager alongside the other three pipeline-stage classes, mirroring the TestSourceFileDirectiveResolver precedent. No behavior change: the moved methods are unchanged, TestPipelineManager's stage sequence, conditions, and stop-point behavior are unchanged, and every existing unit example for the moved stages passed unmodified once retargeted at the new class's own (smaller) constructor. Co-Authored-By: Claude Sonnet 5 --- lib/ceedling/objects.yml | 14 +- lib/ceedling/test_invoker/README.md | 9 +- lib/ceedling/test_invoker/partials_manager.rb | 386 +++++++++++++++++ .../test_invoker/test_build_executor.rb | 358 ---------------- .../test_invoker/test_pipeline_manager.rb | 7 +- spec/units/partials_manager_spec.rb | 403 ++++++++++++++++++ spec/units/test_build_executor_spec.rb | 348 --------------- spec/units/test_pipeline_manager_spec.rb | 32 +- 8 files changed, 828 insertions(+), 729 deletions(-) create mode 100644 lib/ceedling/test_invoker/partials_manager.rb create mode 100644 spec/units/partials_manager_spec.rb diff --git a/lib/ceedling/objects.yml b/lib/ceedling/objects.yml index 61c0c3b9..82775cf6 100644 --- a/lib/ceedling/objects.yml +++ b/lib/ceedling/objects.yml @@ -398,6 +398,7 @@ test_pipeline_manager: compose: - test_build_setup - test_build_planner + - partials_manager - test_build_executor - configurator - batchinator @@ -450,7 +451,6 @@ test_build_executor: - reportinator - batchinator - preprocessinator - - partializer - generator - test_context_extractor - plugin_manager @@ -460,6 +460,18 @@ test_build_executor: - dependinator - test_source_file_directive_resolver +partials_manager: + compose: + - configurator + - loginator + - reportinator + - batchinator + - preprocessinator + - partializer + - generator + - dependinator + - file_path_utils + release_invoker: compose: - application diff --git a/lib/ceedling/test_invoker/README.md b/lib/ceedling/test_invoker/README.md index 0e14b895..36bb965a 100644 --- a/lib/ceedling/test_invoker/README.md +++ b/lib/ceedling/test_invoker/README.md @@ -6,15 +6,16 @@ Every test file in a run gets its own `Testable` record, and every `Testable` in ## The pipeline's shape -Four classes divide the work, and each owns a different concern: +Five classes divide the work, and each owns a different concern: - **`TestInvoker`** owns one test-build *run*, top to bottom. It opens the dependency cache before the run and flushes it afterward, fires the `pre_test_build`/`post_test_build` plugin hooks around it, and if anything anywhere in the pipeline raises, it's the one that catches the exception, logs it, and registers the run as a failed build rather than letting the exception escape. It also answers a couple of narrow questions once a run has finished -- which sources a given test pulled in, mainly for callers outside the pipeline that only care about dependency information and never asked for a real build. - **`TestPipelineManager`** owns the *stage sequence* itself: the fixed list of stages in the order below, and the handful of feature-toggle and stop-point conditions that let a stage recognize it has nothing to do on a given run. It has no idea how any stage actually accomplishes its work -- it only decides whether a given stage runs at all. - **`TestBuildSetup`** does the early, per-test *groundwork*: creating build directories, extracting a test file's `#include`s and build directives, and working out the flags, defines, and search paths every later stage will need. - **`TestBuildPlanner`** decides *what* needs to be generated and built -- which mocks a test needs, what its generated runner and Partials will look like, and, once everything upstream has run, the final list of sources, objects, and artifacts that have to be compiled and linked. -- **`TestBuildExecutor`** does the actual *work* the planner decided on: preprocessing, generating mocks and Partials, compiling objects, linking executables, and running the resulting test fixtures. +- **`PartialsManager`** preprocesses each Partial's header and source files and generates its actual implementation, interface, and (when needed) types-header content, calling into `Partializer` (`lib/ceedling/partials/`) for the underlying C-parsing/extraction work. +- **`TestBuildExecutor`** does the rest of the actual *work* the planner decided on: generating mocks, compiling objects, linking executables, and running the resulting test fixtures. -`TestBuildSetup`, `TestBuildPlanner`, and `TestBuildExecutor` never call each other directly -- `TestPipelineManager` is the only thing that sequences them, and it does so purely by calling each stage's method against the shared `PipelineState` in turn. +`TestBuildSetup`, `TestBuildPlanner`, `PartialsManager`, and `TestBuildExecutor` never call each other directly -- `TestPipelineManager` is the only thing that sequences them, and it does so purely by calling each stage's method against the shared `PipelineState` in turn. ## `PipelineState` and `Testable` @@ -35,7 +36,7 @@ Four classes divide the work, and each owns a different concern: **Planning (stage 5)** is where `TestBuildPlanner` decides what this test needs generated: which of its includes are mocks and where each one's real header actually resolves to, what its Partials configuration looks like, and where its generated test runner will live. Nothing is generated yet -- stage 5 only decides. -**Partials (stages 6-8)**, run only when a project uses Partials at all, and only when this particular run actually has any. A transform first flattens every test's Partials configuration into two flat lists -- one for partial header files, one for partial source files -- so stages 6 and 7 can preprocess every partial file across every test as a single parallel batch rather than nesting per-test loops. Stage 8 then extracts and generates each partial's actual implementation and interface content from whatever those preprocessing passes produced. +**Partials (stages 6-8)**, owned by `PartialsManager` and run only when a project uses Partials at all, and only when this particular run actually has any. A transform first flattens every test's Partials configuration into two flat lists -- one for partial header files, one for partial source files -- so stages 6 and 7 can preprocess every partial file across every test as a single parallel batch rather than nesting per-test loops. Stage 8 then extracts and generates each partial's actual implementation and interface content from whatever those preprocessing passes produced. **Mocks (stages 9-10)**, run only when a project uses mocks at all, and only when this particular run actually has any. A transform flattens every test's planned mocks into one flat list the same way Partials' own transform does. Stage 9, when mock preprocessing is enabled, preprocesses each header to be mocked. Stage 10 generates the actual mock source and header CMock will compile in. This is also the pipeline's first stop point: a run that only wants mocks regenerated (a `--mocking-only`-style build) stops right after this stage. diff --git a/lib/ceedling/test_invoker/partials_manager.rb b/lib/ceedling/test_invoker/partials_manager.rb new file mode 100644 index 00000000..c7a012ca --- /dev/null +++ b/lib/ceedling/test_invoker/partials_manager.rb @@ -0,0 +1,386 @@ +# ========================================================================= +# Ceedling - Test-Centered Build System for C +# ThrowTheSwitch.org +# Copyright (c) 2010-26 Mike Karlesky, Mark VanderVoord, & Greg Williams +# SPDX-License-Identifier: MIT +# ========================================================================= + +require 'ceedling/constants' +require 'ceedling/test_invoker/test_pipeline_helpers' + +class PartialsManager + + include TestPipelineHelpers + + constructor( + :configurator, + :loginator, + :reportinator, + :batchinator, + :preprocessinator, + :partializer, + :generator, + :dependinator, + :file_path_utils + ) + + # Stage 6: Preprocess partial header files for extract-and-generate pass. + def stage_preprocess_partial_headers(state) + preprocess_partials( + items: state.partials_headers, + kind: 'header', + noun: 'headers', + preserve_macros_method: :preprocess_partial_header_file_preserve_macros, + expand_macros_method: :preprocess_partial_header_expand_macros + ) + end + + # Stage 7: Preprocess partial source files for extract-and-generate pass. + def stage_preprocess_partial_sources(state) + preprocess_partials( + items: state.partials_sources, + kind: 'source', + noun: 'sources', + preserve_macros_method: :preprocess_partial_source_file_preserve_macros, + expand_macros_method: :preprocess_partial_source_expand_macros + ) + end + + # Stage 8: Extract and generate partial implementation and interface files. + # + # Extraction and validation below always run in full, for every partial, every + # invocation -- pure in-memory C parsing from whatever `config` already holds, + # cheap regardless of whether stages 6/7 did real preprocessing work or recalled + # it. `testable.partials.tests`/`.mocks` are rebuilt from that in-memory result + # every run too, since stage 14 reads those lists to decide compile sources + # regardless of whether anything on disk actually changed. + # + # The three disk writes below -- types header, implementation, interface -- are + # each independently optional (a module with no type/aggregate defs never gets + # a types header at all; implementation/interface are separately gated on + # extraction succeeding) and each gets its own DependencyTracker target rather + # than one combined target the way stage 6's three preprocessing passes share + # one: a target that's sometimes legitimately never written would never read + # back as fresh. + def stage_generate_partials(state) + directives_only = @configurator.test_build_preprocess_directives_only_available + + partials = [] + state.testables.each do |_, testable| + next if testable.partials.configs.empty? + testable.partials.configs.each do |_, config| + partials << { config: config, testable: testable } + end + end + + skipped_types = 0 + skipped_impl = 0 + skipped_interface = 0 + + @batchinator.exec(workload: :compile, things: partials) do |partial| + config = partial[:config] + testable = partial[:testable] + name = testable.name + + module_contents = @partializer.extract_module_contents( + name, + config, + fallback: !directives_only + ) + + @partializer.validate_config( c_module: module_contents, config: config, name: name ) + + @partializer.sanitize( module_contents ) + + # Antecedents mirror stages 6/7's own targets for this same module's header/source -- + # Partial generation's actual inputs (the preprocessed content those stages produce or + # recall) are already fully covered by the same file+flags/defines/search_paths. + # A declaration-only Partial (a prototype with no matching definition) has no source + # file to find, leaving config.source.filepath legitimately nil -- compact it out + # before it reaches path normalization, which expects real paths only. + antecedent_files = [config.header.filepath, config.source.filepath].compact + antecedent_meta = { + flags: testable.preprocess_flags, + defines: testable.preprocess_defines, + search_paths: testable.search_paths, + partials_max_extraction_length: @configurator.partials_max_extraction_length + } + + # Generated once and shared by the implementation and interface headers below (via + # their own includes lists), so a module tested and mocked in the same test file gets + # exactly one C definition of each of its typedefs and aggregate types. Nothing to + # write (and therefore nothing to track) when the module has no typedefs or aggregate + # definitions -- recompute the bare filename the same deterministic way + # GeneratorPartials#generate_types would have, purely so the includes-remapping below + # still knows what to #include. + types_header = nil + if !module_contents.type_definitions.empty? || !module_contents.aggregate_definitions.empty? + types_header = @file_path_utils.form_partial_types_header_filename( config.module ) + target = File.join( testable.paths[:partials], types_header ) + + @dependinator.register( target, files: antecedent_files, meta: antecedent_meta ) + + if @dependinator.stale?( target ) + @generator.generate_partial_types( + name: name, + partial: config.module, + c_module: module_contents, + output_path: testable.paths[:partials] + ) + @dependinator.mark_fresh( target ) + else + msg = @reportinator.generate_module_progress( + operation: 'Skipping Partial types generation for', + module_name: name, + filename: config.module + ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + state.lock.synchronize { skipped_types += 1 } + end + end + + implementation = @partializer.extract_implementation_functions( + test: name, + partial: config.module, + definitions: module_contents.function_definitions, + config: config + ) + + interface = @partializer.extract_interface_functions( + test: name, + partial: config.module, + definitions: module_contents.function_definitions, + declarations: module_contents.function_declarations, + config: config + ) + + @partializer.validate_extracted_functions( + name: name, + partial: config.module, + impl: implementation, + interface: interface + ) + + arg_hash = { + test: name, + partial: config.module, + function_definitions: implementation, + c_module: module_contents, + header_includes: @partializer.remap_implementation_header_includes( + name: config.module, + includes: (config.source.includes + config.header.includes), + partials: testable.partials.configs, + types_header: types_header, + test: name + ), + source_includes: @partializer.remap_implementation_source_includes( + name: config.module, + includes: (config.source.includes + config.header.includes), + partials: testable.partials.configs, + test: name + ), + input_filepath: config.source.filepath, + output_path: testable.paths[:partials] + } + + unless implementation.nil? + # The header this same call writes alongside the source isn't itself an + # antecedent -- tracking it too catches an externally modified/deleted header + # even when the source's own antecedents look unchanged. + target = File.join( testable.paths[:partials], @file_path_utils.form_partial_implementation_source_filename( config.module ) ) + header_target = File.join( testable.paths[:partials], @file_path_utils.form_partial_implementation_header_filename( config.module ) ) + + @dependinator.register( target, files: antecedent_files + [header_target], meta: antecedent_meta ) + + if @dependinator.stale?( target ) + @generator.generate_partial_implementation( **arg_hash ) + @dependinator.mark_fresh( target ) + else + msg = @reportinator.generate_module_progress( + operation: 'Skipping Partial implementation generation for', + module_name: name, + filename: config.module + ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + state.lock.synchronize { skipped_impl += 1 } + end + + state.lock.synchronize { testable.partials.tests << config.module } + end + + arg_hash = { + test: name, + partial: config.module, + function_declarations: interface, + includes: @partializer.remap_interface_header_includes( + name: config.module, + includes: (config.source.includes + config.header.includes), + partials: testable.partials.configs, + types_header: types_header, + test: name + ), + c_module: module_contents, + input_filepath: config.header.filepath, + output_path: testable.paths[:partials] + } + + unless interface.nil? + target = File.join( testable.paths[:partials], @file_path_utils.form_partial_interface_header_filename( config.module ) ) + + @dependinator.register( target, files: antecedent_files, meta: antecedent_meta ) + + if @dependinator.stale?( target ) + @generator.generate_partial_interface( **arg_hash ) + @dependinator.mark_fresh( target ) + else + msg = @reportinator.generate_module_progress( + operation: 'Skipping Partial interface generation for', + module_name: name, + filename: config.module + ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + state.lock.synchronize { skipped_interface += 1 } + end + + state.lock.synchronize { testable.partials.mocks << config.module } + end + end + + log_skip_summary( task: "Partial types generation", count: skipped_types, noun: "types headers" ) + log_skip_summary( task: "Partial implementation generation", count: skipped_impl, noun: "implementations" ) + log_skip_summary( task: "Partial interface generation", count: skipped_interface, noun: "interfaces" ) + end + + private + + # Shared body for stages 6 and 7 -- a partial's header and source files go through + # identical preprocessing, differing only in which of `state.partials_headers`/ + # `state.partials_sources` supplies the work and which Preprocessinator methods + # `kind` (a header or a source) is actually preprocessed by. + # + # A partial file's three preprocessing passes below (directives-only generation, + # preserve-macros preprocessing, full-expansion) all derive from the same + # antecedent file and the same preprocess flags/defines/search paths, so they're + # stale or fresh together as a single unit -- one DependencyTracker target per + # file per test covers all three. + # + # Settling every target's staleness in its own sequential pass first (cheap, no + # subprocess work) is what lets the three parallel batches below each just check + # `details.stale` instead of duplicating the register/stale? call three times + # over. On a stale target, all three passes run and populate `config` as they do + # today. On a fresh target, the two preprocessed output filepaths are recomputed + # the same deterministic way the preprocessor methods themselves compute them, + # and `config.includes` is recalled from the on-disk list a prior stale run wrote + # -- so `config` ends up populated identically either way, and stage 8 (which + # reads only `config`) needs no knowledge of which path produced it. + def preprocess_partials(items:, kind:, noun:, preserve_macros_method:, expand_macros_method:) + directives_only = @configurator.test_build_preprocess_directives_only_available + skipped = 0 + + items.each do |details| + config = details.config + testable = details.testable + name = testable.name + + target = @file_path_utils.form_preprocessed_file_filepath( config.filepath, name ) + + @dependinator.register( + target, + files: [config.filepath], + meta: dependency_meta( flags: testable.preprocess_flags, defines: testable.preprocess_defines, search_paths: testable.search_paths ) + ) + + details.preprocessed_target = target + details.stale = @dependinator.stale?( target ) + + if details.stale + msg = @reportinator.generate_module_progress( + operation: "Preprocessing partial #{kind} for", + module_name: name, + filename: File.basename( config.filepath ) + ) + @loginator.log( msg ) + else + msg = @reportinator.generate_module_progress( + operation: "Skipping partial #{kind} preprocessing for", + module_name: name, + filename: File.basename( config.filepath ) + ) + @loginator.log( msg, Verbosity::OBNOXIOUS ) + skipped += 1 + + config.directives_only_filepath = target + config.includes = @preprocessinator.load_includes_list( test: name, filepath: config.filepath ) + config.full_expansion_filepath = @file_path_utils.form_preprocessed_file_full_expansion_filepath( config.filepath, name ) + end + end + + log_skip_summary( task: "partial #{kind} preprocessing", count: skipped, noun: noun ) + + # Generate directive-only preprocessor output if available + @batchinator.exec(workload: :compile, things: items) do |details| + next unless details.stale + + config = details.config + testable = details.testable + name = testable.name + + arg_hash = { + filepath: config.filepath, + test: name, + flags: testable.preprocess_flags, + include_paths: testable.search_paths, + vendor_paths: vendor_search_paths(), + defines: testable.preprocess_defines + } + + details.directives_only_filepath = @preprocessinator.generate_directives_only_output( **arg_hash ) + end if directives_only + + # Preprocess and assemble files + @batchinator.exec(workload: :compile, things: items) do |details| + next unless details.stale + + config = details.config + testable = details.testable + name = testable.name + directives_only_filepath = details.directives_only_filepath + + arg_hash = { + test: name, + filepath: config.filepath, + directives_only_filepath: directives_only_filepath, + fallback: directives_only_fallback?( directives_only, directives_only_filepath ), + flags: testable.preprocess_flags, + include_paths: testable.search_paths, + vendor_paths: vendor_search_paths(), + defines: testable.preprocess_defines + } + + config.directives_only_filepath, config.includes = @preprocessinator.public_send( preserve_macros_method, **arg_hash ) + end + + # Full-preprocess files for expanded signature extraction. + @batchinator.exec(workload: :compile, things: items) do |details| + next unless details.stale + + config = details.config + testable = details.testable + name = testable.name + + arg_hash = { + filepath: config.filepath, + test: name, + flags: testable.preprocess_flags, + include_paths: testable.search_paths, + vendor_paths: vendor_search_paths(), + defines: testable.preprocess_defines + } + + config.full_expansion_filepath = @preprocessinator.public_send( expand_macros_method, **arg_hash ) + + @dependinator.mark_fresh( details.preprocessed_target ) + end + end + +end diff --git a/lib/ceedling/test_invoker/test_build_executor.rb b/lib/ceedling/test_invoker/test_build_executor.rb index 8c8f2195..f63ae5e3 100644 --- a/lib/ceedling/test_invoker/test_build_executor.rb +++ b/lib/ceedling/test_invoker/test_build_executor.rb @@ -21,7 +21,6 @@ class TestBuildExecutor :reportinator, :batchinator, :preprocessinator, - :partializer, :generator, :test_context_extractor, :plugin_manager, @@ -36,233 +35,6 @@ def setup() @context_extractor = @test_context_extractor end - # Stage 6: Preprocess partial header files for extract-and-generate pass. - def stage_preprocess_partial_headers(state) - preprocess_partials( - items: state.partials_headers, - kind: 'header', - noun: 'headers', - preserve_macros_method: :preprocess_partial_header_file_preserve_macros, - expand_macros_method: :preprocess_partial_header_expand_macros - ) - end - - # Stage 7: Preprocess partial source files for extract-and-generate pass. - def stage_preprocess_partial_sources(state) - preprocess_partials( - items: state.partials_sources, - kind: 'source', - noun: 'sources', - preserve_macros_method: :preprocess_partial_source_file_preserve_macros, - expand_macros_method: :preprocess_partial_source_expand_macros - ) - end - - # Stage 8: Extract and generate partial implementation and interface files. - # - # Extraction and validation below always run in full, for every partial, every - # invocation -- pure in-memory C parsing from whatever `config` already holds, - # cheap regardless of whether stages 6/7 did real preprocessing work or recalled - # it. `testable.partials.tests`/`.mocks` are rebuilt from that in-memory result - # every run too, since stage 14 reads those lists to decide compile sources - # regardless of whether anything on disk actually changed. - # - # The three disk writes below -- types header, implementation, interface -- are - # each independently optional (a module with no type/aggregate defs never gets - # a types header at all; implementation/interface are separately gated on - # extraction succeeding) and each gets its own DependencyTracker target rather - # than one combined target the way stage 6's three preprocessing passes share - # one: a target that's sometimes legitimately never written would never read - # back as fresh. - def stage_generate_partials(state) - directives_only = @configurator.test_build_preprocess_directives_only_available - - partials = [] - state.testables.each do |_, testable| - next if testable.partials.configs.empty? - testable.partials.configs.each do |_, config| - partials << { config: config, testable: testable } - end - end - - skipped_types = 0 - skipped_impl = 0 - skipped_interface = 0 - - @batchinator.exec(workload: :compile, things: partials) do |partial| - config = partial[:config] - testable = partial[:testable] - name = testable.name - - module_contents = @partializer.extract_module_contents( - name, - config, - fallback: !directives_only - ) - - @partializer.validate_config( c_module: module_contents, config: config, name: name ) - - @partializer.sanitize( module_contents ) - - # Antecedents mirror stages 6/7's own targets for this same module's header/source -- - # Partial generation's actual inputs (the preprocessed content those stages produce or - # recall) are already fully covered by the same file+flags/defines/search_paths. - # A declaration-only Partial (a prototype with no matching definition) has no source - # file to find, leaving config.source.filepath legitimately nil -- compact it out - # before it reaches path normalization, which expects real paths only. - antecedent_files = [config.header.filepath, config.source.filepath].compact - antecedent_meta = { - flags: testable.preprocess_flags, - defines: testable.preprocess_defines, - search_paths: testable.search_paths, - partials_max_extraction_length: @configurator.partials_max_extraction_length - } - - # Generated once and shared by the implementation and interface headers below (via - # their own includes lists), so a module tested and mocked in the same test file gets - # exactly one C definition of each of its typedefs and aggregate types. Nothing to - # write (and therefore nothing to track) when the module has no typedefs or aggregate - # definitions -- recompute the bare filename the same deterministic way - # GeneratorPartials#generate_types would have, purely so the includes-remapping below - # still knows what to #include. - types_header = nil - if !module_contents.type_definitions.empty? || !module_contents.aggregate_definitions.empty? - types_header = @file_path_utils.form_partial_types_header_filename( config.module ) - target = File.join( testable.paths[:partials], types_header ) - - @dependinator.register( target, files: antecedent_files, meta: antecedent_meta ) - - if @dependinator.stale?( target ) - @generator.generate_partial_types( - name: name, - partial: config.module, - c_module: module_contents, - output_path: testable.paths[:partials] - ) - @dependinator.mark_fresh( target ) - else - msg = @reportinator.generate_module_progress( - operation: 'Skipping Partial types generation for', - module_name: name, - filename: config.module - ) - @loginator.log( msg, Verbosity::OBNOXIOUS ) - state.lock.synchronize { skipped_types += 1 } - end - end - - implementation = @partializer.extract_implementation_functions( - test: name, - partial: config.module, - definitions: module_contents.function_definitions, - config: config - ) - - interface = @partializer.extract_interface_functions( - test: name, - partial: config.module, - definitions: module_contents.function_definitions, - declarations: module_contents.function_declarations, - config: config - ) - - @partializer.validate_extracted_functions( - name: name, - partial: config.module, - impl: implementation, - interface: interface - ) - - arg_hash = { - test: name, - partial: config.module, - function_definitions: implementation, - c_module: module_contents, - header_includes: @partializer.remap_implementation_header_includes( - name: config.module, - includes: (config.source.includes + config.header.includes), - partials: testable.partials.configs, - types_header: types_header, - test: name - ), - source_includes: @partializer.remap_implementation_source_includes( - name: config.module, - includes: (config.source.includes + config.header.includes), - partials: testable.partials.configs, - test: name - ), - input_filepath: config.source.filepath, - output_path: testable.paths[:partials] - } - - unless implementation.nil? - # The header this same call writes alongside the source isn't itself an - # antecedent -- tracking it too catches an externally modified/deleted header - # even when the source's own antecedents look unchanged. - target = File.join( testable.paths[:partials], @file_path_utils.form_partial_implementation_source_filename( config.module ) ) - header_target = File.join( testable.paths[:partials], @file_path_utils.form_partial_implementation_header_filename( config.module ) ) - - @dependinator.register( target, files: antecedent_files + [header_target], meta: antecedent_meta ) - - if @dependinator.stale?( target ) - @generator.generate_partial_implementation( **arg_hash ) - @dependinator.mark_fresh( target ) - else - msg = @reportinator.generate_module_progress( - operation: 'Skipping Partial implementation generation for', - module_name: name, - filename: config.module - ) - @loginator.log( msg, Verbosity::OBNOXIOUS ) - state.lock.synchronize { skipped_impl += 1 } - end - - state.lock.synchronize { testable.partials.tests << config.module } - end - - arg_hash = { - test: name, - partial: config.module, - function_declarations: interface, - includes: @partializer.remap_interface_header_includes( - name: config.module, - includes: (config.source.includes + config.header.includes), - partials: testable.partials.configs, - types_header: types_header, - test: name - ), - c_module: module_contents, - input_filepath: config.header.filepath, - output_path: testable.paths[:partials] - } - - unless interface.nil? - target = File.join( testable.paths[:partials], @file_path_utils.form_partial_interface_header_filename( config.module ) ) - - @dependinator.register( target, files: antecedent_files, meta: antecedent_meta ) - - if @dependinator.stale?( target ) - @generator.generate_partial_interface( **arg_hash ) - @dependinator.mark_fresh( target ) - else - msg = @reportinator.generate_module_progress( - operation: 'Skipping Partial interface generation for', - module_name: name, - filename: config.module - ) - @loginator.log( msg, Verbosity::OBNOXIOUS ) - state.lock.synchronize { skipped_interface += 1 } - end - - state.lock.synchronize { testable.partials.mocks << config.module } - end - end - - log_skip_summary( task: "Partial types generation", count: skipped_types, noun: "types headers" ) - log_skip_summary( task: "Partial implementation generation", count: skipped_impl, noun: "implementations" ) - log_skip_summary( task: "Partial interface generation", count: skipped_interface, noun: "interfaces" ) - end - # Stage 9: Preprocess header files to be mocked. def stage_preprocess_mocks(state) directives_only = @configurator.test_build_preprocess_directives_only_available @@ -833,136 +605,6 @@ def get_library_paths_to_arguments() private - # Shared body for stages 6 and 7 -- a partial's header and source files go through - # identical preprocessing, differing only in which of `state.partials_headers`/ - # `state.partials_sources` supplies the work and which Preprocessinator methods - # `kind` (a header or a source) is actually preprocessed by. - # - # A partial file's three preprocessing passes below (directives-only generation, - # preserve-macros preprocessing, full-expansion) all derive from the same - # antecedent file and the same preprocess flags/defines/search paths, so they're - # stale or fresh together as a single unit -- one DependencyTracker target per - # file per test covers all three. - # - # Settling every target's staleness in its own sequential pass first (cheap, no - # subprocess work) is what lets the three parallel batches below each just check - # `details.stale` instead of duplicating the register/stale? call three times - # over. On a stale target, all three passes run and populate `config` as they do - # today. On a fresh target, the two preprocessed output filepaths are recomputed - # the same deterministic way the preprocessor methods themselves compute them, - # and `config.includes` is recalled from the on-disk list a prior stale run wrote - # -- so `config` ends up populated identically either way, and stage 8 (which - # reads only `config`) needs no knowledge of which path produced it. - def preprocess_partials(items:, kind:, noun:, preserve_macros_method:, expand_macros_method:) - directives_only = @configurator.test_build_preprocess_directives_only_available - skipped = 0 - - items.each do |details| - config = details.config - testable = details.testable - name = testable.name - - target = @file_path_utils.form_preprocessed_file_filepath( config.filepath, name ) - - @dependinator.register( - target, - files: [config.filepath], - meta: dependency_meta( flags: testable.preprocess_flags, defines: testable.preprocess_defines, search_paths: testable.search_paths ) - ) - - details.preprocessed_target = target - details.stale = @dependinator.stale?( target ) - - if details.stale - msg = @reportinator.generate_module_progress( - operation: "Preprocessing partial #{kind} for", - module_name: name, - filename: File.basename( config.filepath ) - ) - @loginator.log( msg ) - else - msg = @reportinator.generate_module_progress( - operation: "Skipping partial #{kind} preprocessing for", - module_name: name, - filename: File.basename( config.filepath ) - ) - @loginator.log( msg, Verbosity::OBNOXIOUS ) - skipped += 1 - - config.directives_only_filepath = target - config.includes = @preprocessinator.load_includes_list( test: name, filepath: config.filepath ) - config.full_expansion_filepath = @file_path_utils.form_preprocessed_file_full_expansion_filepath( config.filepath, name ) - end - end - - log_skip_summary( task: "partial #{kind} preprocessing", count: skipped, noun: noun ) - - # Generate directive-only preprocessor output if available - @batchinator.exec(workload: :compile, things: items) do |details| - next unless details.stale - - config = details.config - testable = details.testable - name = testable.name - - arg_hash = { - filepath: config.filepath, - test: name, - flags: testable.preprocess_flags, - include_paths: testable.search_paths, - vendor_paths: vendor_search_paths(), - defines: testable.preprocess_defines - } - - details.directives_only_filepath = @preprocessinator.generate_directives_only_output( **arg_hash ) - end if directives_only - - # Preprocess and assemble files - @batchinator.exec(workload: :compile, things: items) do |details| - next unless details.stale - - config = details.config - testable = details.testable - name = testable.name - directives_only_filepath = details.directives_only_filepath - - arg_hash = { - test: name, - filepath: config.filepath, - directives_only_filepath: directives_only_filepath, - fallback: directives_only_fallback?( directives_only, directives_only_filepath ), - flags: testable.preprocess_flags, - include_paths: testable.search_paths, - vendor_paths: vendor_search_paths(), - defines: testable.preprocess_defines - } - - config.directives_only_filepath, config.includes = @preprocessinator.public_send( preserve_macros_method, **arg_hash ) - end - - # Full-preprocess files for expanded signature extraction. - @batchinator.exec(workload: :compile, things: items) do |details| - next unless details.stale - - config = details.config - testable = details.testable - name = testable.name - - arg_hash = { - filepath: config.filepath, - test: name, - flags: testable.preprocess_flags, - include_paths: testable.search_paths, - vendor_paths: vendor_search_paths(), - defines: testable.preprocess_defines - } - - config.full_expansion_filepath = @preprocessinator.public_send( expand_macros_method, **arg_hash ) - - @dependinator.mark_fresh( details.preprocessed_target ) - end - end - # Compile a single C or assembly source file into an object file. Returns # whether a real compile actually happened, so the caller can report how # many objects across the whole build needed nothing done. diff --git a/lib/ceedling/test_invoker/test_pipeline_manager.rb b/lib/ceedling/test_invoker/test_pipeline_manager.rb index 5b0fa643..2e3d4657 100644 --- a/lib/ceedling/test_invoker/test_pipeline_manager.rb +++ b/lib/ceedling/test_invoker/test_pipeline_manager.rb @@ -20,6 +20,7 @@ class TestPipelineManager constructor( :test_build_setup, :test_build_planner, + :partials_manager, :test_build_executor, :configurator, :batchinator, @@ -185,7 +186,7 @@ def build_stage_sequence condition: use_partials, empty_condition: ->(s) { s.partials_headers.empty? }, empty_notice: "no Partials to process", - body: ->(s) { @test_build_executor.stage_preprocess_partial_headers(s) } + body: ->(s) { @partials_manager.stage_preprocess_partial_headers(s) } ), # Stage 7 @@ -193,7 +194,7 @@ def build_stage_sequence condition: use_partials, empty_condition: ->(s) { s.partials_sources.empty? }, empty_notice: "no Partials to process", - body: ->(s) { @test_build_executor.stage_preprocess_partial_sources(s) } + body: ->(s) { @partials_manager.stage_preprocess_partial_sources(s) } ), # Stage 8 @@ -201,7 +202,7 @@ def build_stage_sequence condition: use_partials, empty_condition: ->(s) { s.partials_headers.empty? && s.partials_sources.empty? }, empty_notice: "no Partials to generate", - body: ->(s) { @test_build_executor.stage_generate_partials(s) } + body: ->(s) { @partials_manager.stage_generate_partials(s) } ), # Transform 2: Prepare mocks for parallel processing diff --git a/spec/units/partials_manager_spec.rb b/spec/units/partials_manager_spec.rb new file mode 100644 index 00000000..6424e013 --- /dev/null +++ b/spec/units/partials_manager_spec.rb @@ -0,0 +1,403 @@ +# ========================================================================= +# Ceedling - Test-Centered Build System for C +# ThrowTheSwitch.org +# Copyright (c) 2010-26 Mike Karlesky, Mark VanderVoord, & Greg Williams +# SPDX-License-Identifier: MIT +# ========================================================================= + +require 'spec_helper' +require 'ceedling/test_invoker/partials_manager' +require 'ceedling/test_invoker/test_invoker_types' +require 'ceedling/partials/partials' + +describe PartialsManager do + before(:each) do + @configurator = double( "Configurator" ) + @loginator = double( "Loginator" ) + @reportinator = double( "Reportinator" ) + @batchinator = double( "Batchinator" ) + @preprocessinator = double( "Preprocessinator" ) + @partializer = double( "Partializer" ) + @generator = double( "Generator" ) + @dependinator = double( "Dependinator" ) + @file_path_utils = double( "FilePathUtils" ) + + allow(@reportinator).to receive(:generate_module_progress).and_return( '' ) + allow(@reportinator).to receive(:generate_skip_summary).and_return( nil ) + allow(@loginator).to receive(:log) + + allow(@dependinator).to receive(:register) + allow(@dependinator).to receive(:stale?).and_return( true ) + allow(@dependinator).to receive(:mark_fresh) + + @manager = described_class.new( + { + :configurator => @configurator, + :loginator => @loginator, + :reportinator => @reportinator, + :batchinator => @batchinator, + :preprocessinator => @preprocessinator, + :partializer => @partializer, + :generator => @generator, + :dependinator => @dependinator, + :file_path_utils => @file_path_utils + } + ) + end + + # `@batchinator.exec` is a real collaborator only in production; here it's + # stubbed to synchronously yield every `things` entry to the given block, + # matching its real per-item iteration contract without pulling in Parallel. + def stub_batchinator_exec + allow(@batchinator).to receive(:exec) do |workload:, things:, &block| + things.each { |k, v| block.call(k, v) } + end + end + + context "#stage_preprocess_partial_headers" do + before(:each) do + stub_batchinator_exec() + + allow(@configurator).to receive(:project_build_vendor_ceedling_path).and_return( 'build/vendor/ceedling' ) + allow(@file_path_utils).to receive(:form_preprocessed_file_filepath).and_return( 'build/preprocess/Foo.h' ) + allow(@file_path_utils).to receive(:form_preprocessed_file_full_expansion_filepath).and_return( 'build/preprocess/full_expansion/Foo.h' ) + allow(@preprocessinator).to receive(:generate_directives_only_output).and_return( 'build/preprocess/raw/Foo.h' ) + allow(@preprocessinator).to receive(:preprocess_partial_header_file_preserve_macros).and_return( ['build/preprocess/raw/Foo.h', []] ) + allow(@preprocessinator).to receive(:preprocess_partial_header_expand_macros).and_return( 'build/preprocess/full_expansion/Foo.h' ) + allow(@preprocessinator).to receive(:load_includes_list).and_return( [] ) + + @testable = TestInvokerTypes::Testable.new( + :name => 'a_test', + :preprocess_flags => ['-Wall'], :preprocess_defines => ['TEST'], :search_paths => ['src'] + ) + @config = Partials::ConfigFileInfo.new( filepath: 'src/Foo.h' ) + @details = TestInvokerTypes::PartialWork.new( :config => @config, :testable => @testable, :directives_only_filepath => nil ) + @state = TestInvokerTypes::PipelineState.new( + :testables => { :a_test => @testable }, :partials_headers => [@details], :context => :test, :options => [] + ) + end + + it "registers the header's deterministic target with the header file as sole antecedent and preprocess flags/defines/search paths as meta" do + allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( false ) + + expect(@dependinator).to receive(:register).with( + 'build/preprocess/Foo.h', + files: ['src/Foo.h'], + meta: { flags: ['-Wall'], defines: ['TEST'], search_paths: ['src'] } + ) + + @manager.stage_preprocess_partial_headers( @state ) + end + + it "runs all three preprocessing passes and marks the target fresh once, at the end, when the dependency tracker reports it stale" do + allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) + allow(@dependinator).to receive(:stale?).and_return( true ) + + expect(@preprocessinator).to receive(:generate_directives_only_output).ordered + expect(@preprocessinator).to receive(:preprocess_partial_header_file_preserve_macros).ordered + expect(@preprocessinator).to receive(:preprocess_partial_header_expand_macros).ordered + expect(@dependinator).to receive(:mark_fresh).with('build/preprocess/Foo.h').ordered + + @manager.stage_preprocess_partial_headers( @state ) + end + + it "skips all three preprocessing passes and reconstructs config state from the deterministic paths and cached includes list when the dependency tracker reports it unchanged" do + allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) + allow(@dependinator).to receive(:stale?).and_return( false ) + cached_includes = [ double("Include") ] + allow(@preprocessinator).to receive(:load_includes_list).with( test: 'a_test', filepath: 'src/Foo.h' ).and_return( cached_includes ) + + expect(@preprocessinator).to_not receive(:generate_directives_only_output) + expect(@preprocessinator).to_not receive(:preprocess_partial_header_file_preserve_macros) + expect(@preprocessinator).to_not receive(:preprocess_partial_header_expand_macros) + expect(@dependinator).to_not receive(:mark_fresh) + + @manager.stage_preprocess_partial_headers( @state ) + + expect( @config.directives_only_filepath ).to eq( 'build/preprocess/Foo.h' ) + expect( @config.includes ).to eq( cached_includes ) + expect( @config.full_expansion_filepath ).to eq( 'build/preprocess/full_expansion/Foo.h' ) + end + + it "does nothing for the directives-only pass when directives-only preprocessing is unavailable for this toolchain, but still runs preserve-macros and full-expansion" do + allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( false ) + allow(@dependinator).to receive(:stale?).and_return( true ) + + expect(@preprocessinator).to_not receive(:generate_directives_only_output) + expect(@preprocessinator).to receive(:preprocess_partial_header_file_preserve_macros) + expect(@preprocessinator).to receive(:preprocess_partial_header_expand_macros) + + @manager.stage_preprocess_partial_headers( @state ) + end + + it "logs a NORMAL progress line for a header that needs preprocessing, and no OBNOXIOUS skip line" do + allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) + allow(@dependinator).to receive(:stale?).and_return( true ) + allow(@reportinator).to receive(:generate_module_progress) + .with( operation: 'Preprocessing partial header for', module_name: 'a_test', filename: 'Foo.h' ) + .and_return( 'Preprocessing partial header for a_test::Foo.h...' ) + + expect(@loginator).to receive(:log).with( 'Preprocessing partial header for a_test::Foo.h...' ) + expect(@loginator).to_not receive(:log).with( anything, Verbosity::OBNOXIOUS ) + + @manager.stage_preprocess_partial_headers( @state ) + end + + it "logs an OBNOXIOUS skip line for a header recalled from cache, and no NORMAL preprocessing line" do + allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) + allow(@dependinator).to receive(:stale?).and_return( false ) + allow(@reportinator).to receive(:generate_module_progress) + .with( operation: 'Skipping partial header preprocessing for', module_name: 'a_test', filename: 'Foo.h' ) + .and_return( 'Skipping partial header preprocessing for a_test::Foo.h...' ) + + expect(@loginator).to receive(:log).with( 'Skipping partial header preprocessing for a_test::Foo.h...', Verbosity::OBNOXIOUS ) + expect(@reportinator).to_not receive(:generate_module_progress) + .with( operation: 'Preprocessing partial header for', module_name: anything, filename: anything ) + + @manager.stage_preprocess_partial_headers( @state ) + end + end + + context "#stage_preprocess_partial_sources" do + before(:each) do + stub_batchinator_exec() + + allow(@configurator).to receive(:project_build_vendor_ceedling_path).and_return( 'build/vendor/ceedling' ) + allow(@file_path_utils).to receive(:form_preprocessed_file_filepath).and_return( 'build/preprocess/Foo.c' ) + allow(@file_path_utils).to receive(:form_preprocessed_file_full_expansion_filepath).and_return( 'build/preprocess/full_expansion/Foo.c' ) + allow(@preprocessinator).to receive(:generate_directives_only_output).and_return( 'build/preprocess/raw/Foo.c' ) + allow(@preprocessinator).to receive(:preprocess_partial_source_file_preserve_macros).and_return( ['build/preprocess/raw/Foo.c', []] ) + allow(@preprocessinator).to receive(:preprocess_partial_source_expand_macros).and_return( 'build/preprocess/full_expansion/Foo.c' ) + allow(@preprocessinator).to receive(:load_includes_list).and_return( [] ) + + @testable = TestInvokerTypes::Testable.new( + :name => 'a_test', + :preprocess_flags => ['-Wall'], :preprocess_defines => ['TEST'], :search_paths => ['src'] + ) + @config = Partials::ConfigFileInfo.new( filepath: 'src/Foo.c' ) + @details = TestInvokerTypes::PartialWork.new( :config => @config, :testable => @testable, :directives_only_filepath => nil ) + @state = TestInvokerTypes::PipelineState.new( + :testables => { :a_test => @testable }, :partials_sources => [@details], :context => :test, :options => [] + ) + end + + it "registers the source's deterministic target with the source file as sole antecedent and preprocess flags/defines/search paths as meta" do + allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( false ) + + expect(@dependinator).to receive(:register).with( + 'build/preprocess/Foo.c', + files: ['src/Foo.c'], + meta: { flags: ['-Wall'], defines: ['TEST'], search_paths: ['src'] } + ) + + @manager.stage_preprocess_partial_sources( @state ) + end + + it "runs all three preprocessing passes and marks the target fresh once, at the end, when the dependency tracker reports it stale" do + allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) + allow(@dependinator).to receive(:stale?).and_return( true ) + + expect(@preprocessinator).to receive(:generate_directives_only_output).ordered + expect(@preprocessinator).to receive(:preprocess_partial_source_file_preserve_macros).ordered + expect(@preprocessinator).to receive(:preprocess_partial_source_expand_macros).ordered + expect(@dependinator).to receive(:mark_fresh).with('build/preprocess/Foo.c').ordered + + @manager.stage_preprocess_partial_sources( @state ) + end + + it "skips all three preprocessing passes and reconstructs config state from the deterministic paths and cached includes list when the dependency tracker reports it unchanged" do + allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) + allow(@dependinator).to receive(:stale?).and_return( false ) + cached_includes = [ double("Include") ] + allow(@preprocessinator).to receive(:load_includes_list).with( test: 'a_test', filepath: 'src/Foo.c' ).and_return( cached_includes ) + + expect(@preprocessinator).to_not receive(:generate_directives_only_output) + expect(@preprocessinator).to_not receive(:preprocess_partial_source_file_preserve_macros) + expect(@preprocessinator).to_not receive(:preprocess_partial_source_expand_macros) + expect(@dependinator).to_not receive(:mark_fresh) + + @manager.stage_preprocess_partial_sources( @state ) + + expect( @config.directives_only_filepath ).to eq( 'build/preprocess/Foo.c' ) + expect( @config.includes ).to eq( cached_includes ) + expect( @config.full_expansion_filepath ).to eq( 'build/preprocess/full_expansion/Foo.c' ) + end + + it "does nothing for the directives-only pass when directives-only preprocessing is unavailable for this toolchain, but still runs preserve-macros and full-expansion" do + allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( false ) + allow(@dependinator).to receive(:stale?).and_return( true ) + + expect(@preprocessinator).to_not receive(:generate_directives_only_output) + expect(@preprocessinator).to receive(:preprocess_partial_source_file_preserve_macros) + expect(@preprocessinator).to receive(:preprocess_partial_source_expand_macros) + + @manager.stage_preprocess_partial_sources( @state ) + end + + it "logs a NORMAL progress line for a source that needs preprocessing, and no OBNOXIOUS skip line" do + allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) + allow(@dependinator).to receive(:stale?).and_return( true ) + allow(@reportinator).to receive(:generate_module_progress) + .with( operation: 'Preprocessing partial source for', module_name: 'a_test', filename: 'Foo.c' ) + .and_return( 'Preprocessing partial source for a_test::Foo.c...' ) + + expect(@loginator).to receive(:log).with( 'Preprocessing partial source for a_test::Foo.c...' ) + expect(@loginator).to_not receive(:log).with( anything, Verbosity::OBNOXIOUS ) + + @manager.stage_preprocess_partial_sources( @state ) + end + + it "logs an OBNOXIOUS skip line for a source recalled from cache, and no NORMAL preprocessing line" do + allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) + allow(@dependinator).to receive(:stale?).and_return( false ) + allow(@reportinator).to receive(:generate_module_progress) + .with( operation: 'Skipping partial source preprocessing for', module_name: 'a_test', filename: 'Foo.c' ) + .and_return( 'Skipping partial source preprocessing for a_test::Foo.c...' ) + + expect(@loginator).to receive(:log).with( 'Skipping partial source preprocessing for a_test::Foo.c...', Verbosity::OBNOXIOUS ) + expect(@reportinator).to_not receive(:generate_module_progress) + .with( operation: 'Preprocessing partial source for', module_name: anything, filename: anything ) + + @manager.stage_preprocess_partial_sources( @state ) + end + end + + context "#stage_generate_partials" do + before(:each) do + stub_batchinator_exec() + + allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( false ) + allow(@configurator).to receive(:partials_max_extraction_length).and_return( 5 ) + + @module_contents = double( "CModule", + function_definitions: [], + function_declarations: [], + type_definitions: [], + aggregate_definitions: [] + ) + allow(@partializer).to receive(:extract_module_contents).and_return( @module_contents ) + allow(@partializer).to receive(:validate_config) + allow(@partializer).to receive(:sanitize) + allow(@partializer).to receive(:validate_extracted_functions) + allow(@partializer).to receive(:remap_implementation_header_includes).and_return( [] ) + allow(@partializer).to receive(:remap_implementation_source_includes).and_return( [] ) + allow(@partializer).to receive(:remap_interface_header_includes).and_return( [] ) + allow(@generator).to receive(:generate_partial_types) + allow(@generator).to receive(:generate_partial_implementation) + allow(@generator).to receive(:generate_partial_interface) + + allow(@file_path_utils).to receive(:form_partial_types_header_filename).and_return( 'ceedling_partial_Foo_types.h' ) + allow(@file_path_utils).to receive(:form_partial_implementation_source_filename).and_return( 'ceedling_partial_Foo_impl.c' ) + allow(@file_path_utils).to receive(:form_partial_implementation_header_filename).and_return( 'ceedling_partial_Foo_impl.h' ) + allow(@file_path_utils).to receive(:form_partial_interface_header_filename).and_return( 'ceedling_partial_Foo_interface.h' ) + + allow(@dependinator).to receive(:register) + allow(@dependinator).to receive(:stale?).and_return( true ) + allow(@dependinator).to receive(:mark_fresh) + + @config = Partials::Config.new( + module: 'Foo', + header: Partials::ConfigFileInfo.new( filepath: 'src/Foo.h', includes: [] ), + source: Partials::ConfigFileInfo.new( filepath: 'src/Foo.c', includes: [] ) + ) + @testable = TestInvokerTypes::Testable.new( + :name => 'a_test', + :paths => { :partials => 'build/test/partials/a_test' } + ) + @testable.partials.configs = { 'Foo' => @config } + @state = TestInvokerTypes::PipelineState.new( + :testables => { :a_test => @testable }, :context => :test, :options => [], :lock => Mutex.new + ) + end + + # `config` here looks exactly as it would whether stage 6/7 just freshly + # preprocessed it or recalled it whole from a dependency-tracker cache + # hit -- this stage reads only `config` and has no way to tell the + # difference, so a single fixture covers both cases. + it "adds the module to tests and mocks when both implementation and interface are extracted" do + allow(@partializer).to receive(:extract_implementation_functions).and_return( [double("FunctionDefinition")] ) + allow(@partializer).to receive(:extract_interface_functions).and_return( [double("FunctionDeclaration")] ) + + @manager.stage_generate_partials( @state ) + + expect( @testable.partials.tests ).to eq( ['Foo'] ) + expect( @testable.partials.mocks ).to eq( ['Foo'] ) + end + + it "does not add to tests when no implementation is extracted" do + allow(@partializer).to receive(:extract_implementation_functions).and_return( nil ) + allow(@partializer).to receive(:extract_interface_functions).and_return( [double("FunctionDeclaration")] ) + + @manager.stage_generate_partials( @state ) + + expect( @testable.partials.tests ).to eq( [] ) + expect( @testable.partials.mocks ).to eq( ['Foo'] ) + end + + it "does not add to mocks when no interface is extracted" do + allow(@partializer).to receive(:extract_implementation_functions).and_return( [double("FunctionDefinition")] ) + allow(@partializer).to receive(:extract_interface_functions).and_return( nil ) + + @manager.stage_generate_partials( @state ) + + expect( @testable.partials.tests ).to eq( ['Foo'] ) + expect( @testable.partials.mocks ).to eq( [] ) + end + + it "skips writing types, implementation, and interface when the dependency tracker reports all three unchanged, but still updates tests/mocks bookkeeping" do + allow(@module_contents).to receive(:type_definitions).and_return( [double("TypeDef")] ) + allow(@partializer).to receive(:extract_implementation_functions).and_return( [double("FunctionDefinition")] ) + allow(@partializer).to receive(:extract_interface_functions).and_return( [double("FunctionDeclaration")] ) + allow(@dependinator).to receive(:stale?).and_return( false ) + + expect(@generator).to_not receive(:generate_partial_types) + expect(@generator).to_not receive(:generate_partial_implementation) + expect(@generator).to_not receive(:generate_partial_interface) + expect(@dependinator).to_not receive(:mark_fresh) + + @manager.stage_generate_partials( @state ) + + expect( @testable.partials.tests ).to eq( ['Foo'] ) + expect( @testable.partials.mocks ).to eq( ['Foo'] ) + end + + it "never registers or checks a types-header target when the module has no type or aggregate definitions" do + allow(@partializer).to receive(:extract_implementation_functions).and_return( [double("FunctionDefinition")] ) + allow(@partializer).to receive(:extract_interface_functions).and_return( [double("FunctionDeclaration")] ) + + expect(@dependinator).to_not receive(:register).with( /_types\.h$/, any_args ) + expect(@generator).to_not receive(:generate_partial_types) + + @manager.stage_generate_partials( @state ) + end + + it "never passes a nil filepath to the dependency tracker when a Partial has no paired source file" do + # A declaration-only Partial (a prototype with no matching .c definition) has + # no source file to find -- config.source.filepath legitimately stays nil. + @config.source = Partials::ConfigFileInfo.new( filepath: nil, includes: [] ) + allow(@partializer).to receive(:extract_implementation_functions).and_return( nil ) + allow(@partializer).to receive(:extract_interface_functions).and_return( [double("FunctionDeclaration")] ) + allow(@module_contents).to receive(:type_definitions).and_return( [double("TypeDef")] ) + + expect(@dependinator).to receive(:register).at_least(:once) do |_target, files:, meta:| + expect( files ).to_not include( nil ) + end + + @manager.stage_generate_partials( @state ) + end + + it "logs summary lines stating how many of each Partial artifact were recalled from cache" do + allow(@partializer).to receive(:extract_implementation_functions).and_return( [double("FunctionDefinition")] ) + allow(@partializer).to receive(:extract_interface_functions).and_return( [double("FunctionDeclaration")] ) + allow(@module_contents).to receive(:type_definitions).and_return( [double("TypeDef")] ) + allow(@dependinator).to receive(:stale?).and_return( false ) + + allow(@reportinator).to receive(:generate_skip_summary).and_return( "Skipping ... (nothing changed)..." ) + + expect(@loginator).to receive(:log).with( "Skipping ... (nothing changed)..." ).exactly(3).times + + @manager.stage_generate_partials( @state ) + end + end + +end diff --git a/spec/units/test_build_executor_spec.rb b/spec/units/test_build_executor_spec.rb index 56b25657..f714a1c5 100644 --- a/spec/units/test_build_executor_spec.rb +++ b/spec/units/test_build_executor_spec.rb @@ -23,7 +23,6 @@ @reportinator = double( "Reportinator" ) @batchinator = double( "Batchinator" ) @preprocessinator = double( "Preprocessinator" ) - @partializer = double( "Partializer" ) @generator = double( "Generator" ) @test_context_extractor = double( "TestContextExtractor" ) @plugin_manager = double( "PluginManager" ) @@ -80,7 +79,6 @@ :reportinator => @reportinator, :batchinator => @batchinator, :preprocessinator => @preprocessinator, - :partializer => @partializer, :generator => @generator, :test_context_extractor => @test_context_extractor, :plugin_manager => @plugin_manager, @@ -810,352 +808,6 @@ def build_args end end - context "#stage_preprocess_partial_headers" do - before(:each) do - stub_batchinator_exec() - - allow(@configurator).to receive(:project_build_vendor_ceedling_path).and_return( 'build/vendor/ceedling' ) - allow(@file_path_utils).to receive(:form_preprocessed_file_filepath).and_return( 'build/preprocess/Foo.h' ) - allow(@file_path_utils).to receive(:form_preprocessed_file_full_expansion_filepath).and_return( 'build/preprocess/full_expansion/Foo.h' ) - allow(@preprocessinator).to receive(:generate_directives_only_output).and_return( 'build/preprocess/raw/Foo.h' ) - allow(@preprocessinator).to receive(:preprocess_partial_header_file_preserve_macros).and_return( ['build/preprocess/raw/Foo.h', []] ) - allow(@preprocessinator).to receive(:preprocess_partial_header_expand_macros).and_return( 'build/preprocess/full_expansion/Foo.h' ) - allow(@preprocessinator).to receive(:load_includes_list).and_return( [] ) - - @testable = TestInvokerTypes::Testable.new( - :name => 'a_test', - :preprocess_flags => ['-Wall'], :preprocess_defines => ['TEST'], :search_paths => ['src'] - ) - @config = Partials::ConfigFileInfo.new( filepath: 'src/Foo.h' ) - @details = TestInvokerTypes::PartialWork.new( :config => @config, :testable => @testable, :directives_only_filepath => nil ) - @state = TestInvokerTypes::PipelineState.new( - :testables => { :a_test => @testable }, :partials_headers => [@details], :context => :test, :options => [] - ) - end - - it "registers the header's deterministic target with the header file as sole antecedent and preprocess flags/defines/search paths as meta" do - allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( false ) - - expect(@dependinator).to receive(:register).with( - 'build/preprocess/Foo.h', - files: ['src/Foo.h'], - meta: { flags: ['-Wall'], defines: ['TEST'], search_paths: ['src'] } - ) - - @executor.stage_preprocess_partial_headers( @state ) - end - - it "runs all three preprocessing passes and marks the target fresh once, at the end, when the dependency tracker reports it stale" do - allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) - allow(@dependinator).to receive(:stale?).and_return( true ) - - expect(@preprocessinator).to receive(:generate_directives_only_output).ordered - expect(@preprocessinator).to receive(:preprocess_partial_header_file_preserve_macros).ordered - expect(@preprocessinator).to receive(:preprocess_partial_header_expand_macros).ordered - expect(@dependinator).to receive(:mark_fresh).with('build/preprocess/Foo.h').ordered - - @executor.stage_preprocess_partial_headers( @state ) - end - - it "skips all three preprocessing passes and reconstructs config state from the deterministic paths and cached includes list when the dependency tracker reports it unchanged" do - allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) - allow(@dependinator).to receive(:stale?).and_return( false ) - cached_includes = [ double("Include") ] - allow(@preprocessinator).to receive(:load_includes_list).with( test: 'a_test', filepath: 'src/Foo.h' ).and_return( cached_includes ) - - expect(@preprocessinator).to_not receive(:generate_directives_only_output) - expect(@preprocessinator).to_not receive(:preprocess_partial_header_file_preserve_macros) - expect(@preprocessinator).to_not receive(:preprocess_partial_header_expand_macros) - expect(@dependinator).to_not receive(:mark_fresh) - - @executor.stage_preprocess_partial_headers( @state ) - - expect( @config.directives_only_filepath ).to eq( 'build/preprocess/Foo.h' ) - expect( @config.includes ).to eq( cached_includes ) - expect( @config.full_expansion_filepath ).to eq( 'build/preprocess/full_expansion/Foo.h' ) - end - - it "does nothing for the directives-only pass when directives-only preprocessing is unavailable for this toolchain, but still runs preserve-macros and full-expansion" do - allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( false ) - allow(@dependinator).to receive(:stale?).and_return( true ) - - expect(@preprocessinator).to_not receive(:generate_directives_only_output) - expect(@preprocessinator).to receive(:preprocess_partial_header_file_preserve_macros) - expect(@preprocessinator).to receive(:preprocess_partial_header_expand_macros) - - @executor.stage_preprocess_partial_headers( @state ) - end - - it "logs a NORMAL progress line for a header that needs preprocessing, and no OBNOXIOUS skip line" do - allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) - allow(@dependinator).to receive(:stale?).and_return( true ) - allow(@reportinator).to receive(:generate_module_progress) - .with( operation: 'Preprocessing partial header for', module_name: 'a_test', filename: 'Foo.h' ) - .and_return( 'Preprocessing partial header for a_test::Foo.h...' ) - - expect(@loginator).to receive(:log).with( 'Preprocessing partial header for a_test::Foo.h...' ) - expect(@loginator).to_not receive(:log).with( anything, Verbosity::OBNOXIOUS ) - - @executor.stage_preprocess_partial_headers( @state ) - end - - it "logs an OBNOXIOUS skip line for a header recalled from cache, and no NORMAL preprocessing line" do - allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) - allow(@dependinator).to receive(:stale?).and_return( false ) - allow(@reportinator).to receive(:generate_module_progress) - .with( operation: 'Skipping partial header preprocessing for', module_name: 'a_test', filename: 'Foo.h' ) - .and_return( 'Skipping partial header preprocessing for a_test::Foo.h...' ) - - expect(@loginator).to receive(:log).with( 'Skipping partial header preprocessing for a_test::Foo.h...', Verbosity::OBNOXIOUS ) - expect(@reportinator).to_not receive(:generate_module_progress) - .with( operation: 'Preprocessing partial header for', module_name: anything, filename: anything ) - - @executor.stage_preprocess_partial_headers( @state ) - end - end - - context "#stage_preprocess_partial_sources" do - before(:each) do - stub_batchinator_exec() - - allow(@configurator).to receive(:project_build_vendor_ceedling_path).and_return( 'build/vendor/ceedling' ) - allow(@file_path_utils).to receive(:form_preprocessed_file_filepath).and_return( 'build/preprocess/Foo.c' ) - allow(@file_path_utils).to receive(:form_preprocessed_file_full_expansion_filepath).and_return( 'build/preprocess/full_expansion/Foo.c' ) - allow(@preprocessinator).to receive(:generate_directives_only_output).and_return( 'build/preprocess/raw/Foo.c' ) - allow(@preprocessinator).to receive(:preprocess_partial_source_file_preserve_macros).and_return( ['build/preprocess/raw/Foo.c', []] ) - allow(@preprocessinator).to receive(:preprocess_partial_source_expand_macros).and_return( 'build/preprocess/full_expansion/Foo.c' ) - allow(@preprocessinator).to receive(:load_includes_list).and_return( [] ) - - @testable = TestInvokerTypes::Testable.new( - :name => 'a_test', - :preprocess_flags => ['-Wall'], :preprocess_defines => ['TEST'], :search_paths => ['src'] - ) - @config = Partials::ConfigFileInfo.new( filepath: 'src/Foo.c' ) - @details = TestInvokerTypes::PartialWork.new( :config => @config, :testable => @testable, :directives_only_filepath => nil ) - @state = TestInvokerTypes::PipelineState.new( - :testables => { :a_test => @testable }, :partials_sources => [@details], :context => :test, :options => [] - ) - end - - it "registers the source's deterministic target with the source file as sole antecedent and preprocess flags/defines/search paths as meta" do - allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( false ) - - expect(@dependinator).to receive(:register).with( - 'build/preprocess/Foo.c', - files: ['src/Foo.c'], - meta: { flags: ['-Wall'], defines: ['TEST'], search_paths: ['src'] } - ) - - @executor.stage_preprocess_partial_sources( @state ) - end - - it "runs all three preprocessing passes and marks the target fresh once, at the end, when the dependency tracker reports it stale" do - allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) - allow(@dependinator).to receive(:stale?).and_return( true ) - - expect(@preprocessinator).to receive(:generate_directives_only_output).ordered - expect(@preprocessinator).to receive(:preprocess_partial_source_file_preserve_macros).ordered - expect(@preprocessinator).to receive(:preprocess_partial_source_expand_macros).ordered - expect(@dependinator).to receive(:mark_fresh).with('build/preprocess/Foo.c').ordered - - @executor.stage_preprocess_partial_sources( @state ) - end - - it "skips all three preprocessing passes and reconstructs config state from the deterministic paths and cached includes list when the dependency tracker reports it unchanged" do - allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) - allow(@dependinator).to receive(:stale?).and_return( false ) - cached_includes = [ double("Include") ] - allow(@preprocessinator).to receive(:load_includes_list).with( test: 'a_test', filepath: 'src/Foo.c' ).and_return( cached_includes ) - - expect(@preprocessinator).to_not receive(:generate_directives_only_output) - expect(@preprocessinator).to_not receive(:preprocess_partial_source_file_preserve_macros) - expect(@preprocessinator).to_not receive(:preprocess_partial_source_expand_macros) - expect(@dependinator).to_not receive(:mark_fresh) - - @executor.stage_preprocess_partial_sources( @state ) - - expect( @config.directives_only_filepath ).to eq( 'build/preprocess/Foo.c' ) - expect( @config.includes ).to eq( cached_includes ) - expect( @config.full_expansion_filepath ).to eq( 'build/preprocess/full_expansion/Foo.c' ) - end - - it "does nothing for the directives-only pass when directives-only preprocessing is unavailable for this toolchain, but still runs preserve-macros and full-expansion" do - allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( false ) - allow(@dependinator).to receive(:stale?).and_return( true ) - - expect(@preprocessinator).to_not receive(:generate_directives_only_output) - expect(@preprocessinator).to receive(:preprocess_partial_source_file_preserve_macros) - expect(@preprocessinator).to receive(:preprocess_partial_source_expand_macros) - - @executor.stage_preprocess_partial_sources( @state ) - end - - it "logs a NORMAL progress line for a source that needs preprocessing, and no OBNOXIOUS skip line" do - allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) - allow(@dependinator).to receive(:stale?).and_return( true ) - allow(@reportinator).to receive(:generate_module_progress) - .with( operation: 'Preprocessing partial source for', module_name: 'a_test', filename: 'Foo.c' ) - .and_return( 'Preprocessing partial source for a_test::Foo.c...' ) - - expect(@loginator).to receive(:log).with( 'Preprocessing partial source for a_test::Foo.c...' ) - expect(@loginator).to_not receive(:log).with( anything, Verbosity::OBNOXIOUS ) - - @executor.stage_preprocess_partial_sources( @state ) - end - - it "logs an OBNOXIOUS skip line for a source recalled from cache, and no NORMAL preprocessing line" do - allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( true ) - allow(@dependinator).to receive(:stale?).and_return( false ) - allow(@reportinator).to receive(:generate_module_progress) - .with( operation: 'Skipping partial source preprocessing for', module_name: 'a_test', filename: 'Foo.c' ) - .and_return( 'Skipping partial source preprocessing for a_test::Foo.c...' ) - - expect(@loginator).to receive(:log).with( 'Skipping partial source preprocessing for a_test::Foo.c...', Verbosity::OBNOXIOUS ) - expect(@reportinator).to_not receive(:generate_module_progress) - .with( operation: 'Preprocessing partial source for', module_name: anything, filename: anything ) - - @executor.stage_preprocess_partial_sources( @state ) - end - end - - context "#stage_generate_partials" do - before(:each) do - stub_batchinator_exec() - - allow(@configurator).to receive(:test_build_preprocess_directives_only_available).and_return( false ) - allow(@configurator).to receive(:partials_max_extraction_length).and_return( 5 ) - - @module_contents = double( "CModule", - function_definitions: [], - function_declarations: [], - type_definitions: [], - aggregate_definitions: [] - ) - allow(@partializer).to receive(:extract_module_contents).and_return( @module_contents ) - allow(@partializer).to receive(:validate_config) - allow(@partializer).to receive(:sanitize) - allow(@partializer).to receive(:validate_extracted_functions) - allow(@partializer).to receive(:remap_implementation_header_includes).and_return( [] ) - allow(@partializer).to receive(:remap_implementation_source_includes).and_return( [] ) - allow(@partializer).to receive(:remap_interface_header_includes).and_return( [] ) - allow(@generator).to receive(:generate_partial_types) - allow(@generator).to receive(:generate_partial_implementation) - allow(@generator).to receive(:generate_partial_interface) - - allow(@file_path_utils).to receive(:form_partial_types_header_filename).and_return( 'ceedling_partial_Foo_types.h' ) - allow(@file_path_utils).to receive(:form_partial_implementation_source_filename).and_return( 'ceedling_partial_Foo_impl.c' ) - allow(@file_path_utils).to receive(:form_partial_implementation_header_filename).and_return( 'ceedling_partial_Foo_impl.h' ) - allow(@file_path_utils).to receive(:form_partial_interface_header_filename).and_return( 'ceedling_partial_Foo_interface.h' ) - - allow(@dependinator).to receive(:register) - allow(@dependinator).to receive(:stale?).and_return( true ) - allow(@dependinator).to receive(:mark_fresh) - - @config = Partials::Config.new( - module: 'Foo', - header: Partials::ConfigFileInfo.new( filepath: 'src/Foo.h', includes: [] ), - source: Partials::ConfigFileInfo.new( filepath: 'src/Foo.c', includes: [] ) - ) - @testable = TestInvokerTypes::Testable.new( - :name => 'a_test', - :paths => { :partials => 'build/test/partials/a_test' } - ) - @testable.partials.configs = { 'Foo' => @config } - @state = TestInvokerTypes::PipelineState.new( - :testables => { :a_test => @testable }, :context => :test, :options => [], :lock => Mutex.new - ) - end - - # `config` here looks exactly as it would whether stage 6/7 just freshly - # preprocessed it or recalled it whole from a dependency-tracker cache - # hit -- this stage reads only `config` and has no way to tell the - # difference, so a single fixture covers both cases. - it "adds the module to tests and mocks when both implementation and interface are extracted" do - allow(@partializer).to receive(:extract_implementation_functions).and_return( [double("FunctionDefinition")] ) - allow(@partializer).to receive(:extract_interface_functions).and_return( [double("FunctionDeclaration")] ) - - @executor.stage_generate_partials( @state ) - - expect( @testable.partials.tests ).to eq( ['Foo'] ) - expect( @testable.partials.mocks ).to eq( ['Foo'] ) - end - - it "does not add to tests when no implementation is extracted" do - allow(@partializer).to receive(:extract_implementation_functions).and_return( nil ) - allow(@partializer).to receive(:extract_interface_functions).and_return( [double("FunctionDeclaration")] ) - - @executor.stage_generate_partials( @state ) - - expect( @testable.partials.tests ).to eq( [] ) - expect( @testable.partials.mocks ).to eq( ['Foo'] ) - end - - it "does not add to mocks when no interface is extracted" do - allow(@partializer).to receive(:extract_implementation_functions).and_return( [double("FunctionDefinition")] ) - allow(@partializer).to receive(:extract_interface_functions).and_return( nil ) - - @executor.stage_generate_partials( @state ) - - expect( @testable.partials.tests ).to eq( ['Foo'] ) - expect( @testable.partials.mocks ).to eq( [] ) - end - - it "skips writing types, implementation, and interface when the dependency tracker reports all three unchanged, but still updates tests/mocks bookkeeping" do - allow(@module_contents).to receive(:type_definitions).and_return( [double("TypeDef")] ) - allow(@partializer).to receive(:extract_implementation_functions).and_return( [double("FunctionDefinition")] ) - allow(@partializer).to receive(:extract_interface_functions).and_return( [double("FunctionDeclaration")] ) - allow(@dependinator).to receive(:stale?).and_return( false ) - - expect(@generator).to_not receive(:generate_partial_types) - expect(@generator).to_not receive(:generate_partial_implementation) - expect(@generator).to_not receive(:generate_partial_interface) - expect(@dependinator).to_not receive(:mark_fresh) - - @executor.stage_generate_partials( @state ) - - expect( @testable.partials.tests ).to eq( ['Foo'] ) - expect( @testable.partials.mocks ).to eq( ['Foo'] ) - end - - it "never registers or checks a types-header target when the module has no type or aggregate definitions" do - allow(@partializer).to receive(:extract_implementation_functions).and_return( [double("FunctionDefinition")] ) - allow(@partializer).to receive(:extract_interface_functions).and_return( [double("FunctionDeclaration")] ) - - expect(@dependinator).to_not receive(:register).with( /_types\.h$/, any_args ) - expect(@generator).to_not receive(:generate_partial_types) - - @executor.stage_generate_partials( @state ) - end - - it "never passes a nil filepath to the dependency tracker when a Partial has no paired source file" do - # A declaration-only Partial (a prototype with no matching .c definition) has - # no source file to find -- config.source.filepath legitimately stays nil. - @config.source = Partials::ConfigFileInfo.new( filepath: nil, includes: [] ) - allow(@partializer).to receive(:extract_implementation_functions).and_return( nil ) - allow(@partializer).to receive(:extract_interface_functions).and_return( [double("FunctionDeclaration")] ) - allow(@module_contents).to receive(:type_definitions).and_return( [double("TypeDef")] ) - - expect(@dependinator).to receive(:register).at_least(:once) do |_target, files:, meta:| - expect( files ).to_not include( nil ) - end - - @executor.stage_generate_partials( @state ) - end - - it "logs summary lines stating how many of each Partial artifact were recalled from cache" do - allow(@partializer).to receive(:extract_implementation_functions).and_return( [double("FunctionDefinition")] ) - allow(@partializer).to receive(:extract_interface_functions).and_return( [double("FunctionDeclaration")] ) - allow(@module_contents).to receive(:type_definitions).and_return( [double("TypeDef")] ) - allow(@dependinator).to receive(:stale?).and_return( false ) - - allow(@reportinator).to receive(:generate_skip_summary).and_return( "Skipping ... (nothing changed)..." ) - - expect(@loginator).to receive(:log).with( "Skipping ... (nothing changed)..." ).exactly(3).times - - @executor.stage_generate_partials( @state ) - end - end - context "#tailor_search_paths" do PROJECT_BUILD_VENDOR_CMOCK_PATH = 'build/vendor/cmock' unless defined?(PROJECT_BUILD_VENDOR_CMOCK_PATH) PROJECT_BUILD_VENDOR_CEXCEPTION_PATH = 'build/vendor/cexception' unless defined?(PROJECT_BUILD_VENDOR_CEXCEPTION_PATH) diff --git a/spec/units/test_pipeline_manager_spec.rb b/spec/units/test_pipeline_manager_spec.rb index 351c1728..91ed455d 100644 --- a/spec/units/test_pipeline_manager_spec.rb +++ b/spec/units/test_pipeline_manager_spec.rb @@ -14,6 +14,7 @@ before(:each) do @test_build_setup = double( "TestBuildSetup" ) @test_build_planner = double( "TestBuildPlanner" ) + @partials_manager = double( "PartialsManager" ) @test_build_executor = double( "TestBuildExecutor" ) @configurator = double( "Configurator" ) @batchinator = double( "Batchinator" ) @@ -41,9 +42,9 @@ allow(@test_build_planner).to receive(:stage_determine_artifacts) allow(@test_build_planner).to receive(:stage_flatten_objects_list) - allow(@test_build_executor).to receive(:stage_preprocess_partial_headers) - allow(@test_build_executor).to receive(:stage_preprocess_partial_sources) - allow(@test_build_executor).to receive(:stage_generate_partials) + allow(@partials_manager).to receive(:stage_preprocess_partial_headers) + allow(@partials_manager).to receive(:stage_preprocess_partial_sources) + allow(@partials_manager).to receive(:stage_generate_partials) allow(@test_build_executor).to receive(:stage_preprocess_mocks) allow(@test_build_executor).to receive(:stage_generate_mocks) allow(@test_build_executor).to receive(:stage_preprocess_test_files) @@ -57,6 +58,7 @@ { :test_build_setup => @test_build_setup, :test_build_planner => @test_build_planner, + :partials_manager => @partials_manager, :test_build_executor => @test_build_executor, :configurator => @configurator, :batchinator => @batchinator, @@ -110,9 +112,9 @@ def state(options: [], partials_headers: [double("PartialWork")], partials_sourc expect(@test_build_setup).to receive(:stage_collect_preprocessor_context) expect(@test_build_planner).to receive(:stage_determine_files) expect(@test_build_planner).to receive(:stage_flatten_partials_lists) - expect(@test_build_executor).to receive(:stage_preprocess_partial_headers) - expect(@test_build_executor).to receive(:stage_preprocess_partial_sources) - expect(@test_build_executor).to receive(:stage_generate_partials) + expect(@partials_manager).to receive(:stage_preprocess_partial_headers) + expect(@partials_manager).to receive(:stage_preprocess_partial_sources) + expect(@partials_manager).to receive(:stage_generate_partials) expect(@test_build_planner).to receive(:stage_flatten_mocks_list) expect(@test_build_executor).to receive(:stage_preprocess_mocks) expect(@test_build_executor).to receive(:stage_generate_mocks) @@ -279,9 +281,9 @@ def state(options: [], partials_headers: [double("PartialWork")], partials_sourc expect(@test_build_setup).to receive(:stage_collect_preprocessor_context) expect(@test_build_planner).to receive(:stage_determine_files) expect(@test_build_planner).to receive(:stage_flatten_partials_lists) - expect(@test_build_executor).to receive(:stage_preprocess_partial_headers) - expect(@test_build_executor).to receive(:stage_preprocess_partial_sources) - expect(@test_build_executor).to receive(:stage_generate_partials) + expect(@partials_manager).to receive(:stage_preprocess_partial_headers) + expect(@partials_manager).to receive(:stage_preprocess_partial_sources) + expect(@partials_manager).to receive(:stage_generate_partials) expect(@test_build_planner).to receive(:stage_flatten_mocks_list) expect(@test_build_executor).to receive(:stage_preprocess_mocks) expect(@test_build_executor).to receive(:stage_generate_mocks) @@ -330,9 +332,9 @@ def state(options: [], partials_headers: [double("PartialWork")], partials_sourc allow(@configurator).to receive(:project_use_partials).and_return( false ) expect(@test_build_planner).to_not receive(:stage_flatten_partials_lists) - expect(@test_build_executor).to_not receive(:stage_preprocess_partial_headers) - expect(@test_build_executor).to_not receive(:stage_preprocess_partial_sources) - expect(@test_build_executor).to_not receive(:stage_generate_partials) + expect(@partials_manager).to_not receive(:stage_preprocess_partial_headers) + expect(@partials_manager).to_not receive(:stage_preprocess_partial_sources) + expect(@partials_manager).to_not receive(:stage_generate_partials) expect(@test_build_planner).to receive(:stage_determine_files) expect(@loginator).to_not receive(:log) @@ -359,9 +361,9 @@ def state(options: [], partials_headers: [double("PartialWork")], partials_sourc allow(@configurator).to receive(:project_use_partials).and_return( true ) expect(@test_build_planner).to receive(:stage_flatten_partials_lists) - expect(@test_build_executor).to_not receive(:stage_preprocess_partial_headers) - expect(@test_build_executor).to_not receive(:stage_preprocess_partial_sources) - expect(@test_build_executor).to_not receive(:stage_generate_partials) + expect(@partials_manager).to_not receive(:stage_preprocess_partial_headers) + expect(@partials_manager).to_not receive(:stage_preprocess_partial_sources) + expect(@partials_manager).to_not receive(:stage_generate_partials) expect(@loginator).to receive(:log) .with( "Preprocessing for Testing & Mocking Partials: no Partials to process", Verbosity::OBNOXIOUS )