Add CuckooPluginPerModule for per-module mock generation#595
Add CuckooPluginPerModule for per-module mock generation#595SylvainRX wants to merge 2 commits intoBrightify:masterfrom
Conversation
- Create new CuckooPluginPerModule plugin that generates separate mock files per module dependency - Add content-equality guards in GenerateCommand to prevent unnecessary file rewrites - Skip writing output files when content unchanged This allows test targets to depend only on specific module mocks rather than all mocks in a single file, while maintaining the original plugin for existing users.
Enables test targets to override shared module mock generation by introducing CUCKOO_COMPOUND_MODULE_NAME (TARGET/MODULE format). - Plugin: - Pass CUCKOO_COMPOUND_MODULE_NAME env var for each dependency - Generate build command for test target itself (not just deps) - Generator: - Prioritize compound module key over plain module name - Support suppressor pattern (empty sources = empty output) This allows Cuckoofile.toml to specify different mock sources for the same module when used in different test targets, fixing issues where shared dependencies generate unwanted mocks.
| var modules: [Module] | ||
| if let compoundModuleName { | ||
| let compoundMatches = allModules.filter { $0.name == compoundModuleName } | ||
| if !compoundMatches.isEmpty { | ||
| // Compound key (TARGET/MODULE) found – use it exclusively. | ||
| // An entry with empty sources acts as a suppressor, producing an empty output file. | ||
| modules = compoundMatches | ||
| } else if let requestedModuleName { | ||
| // No compound override – fall back to the plain module name. | ||
| modules = allModules.filter { $0.name == requestedModuleName } | ||
| } else { | ||
| modules = [] | ||
| } | ||
| } else if let requestedModuleName { | ||
| modules = allModules.filter { $0.name == requestedModuleName } | ||
| } else { | ||
| modules = allModules | ||
| } |
There was a problem hiding this comment.
Since the allModules aren't used (and I can't think of why they would be), I'd like this filtering logic moved there instead, keeping this high-level implementation easy to read.
| targets: ["CuckooPluginSingleFile"] | ||
| ), | ||
| .plugin( | ||
| name: "CuckooPluginPerModule", |
There was a problem hiding this comment.
I'd use "CuckooPluginModular" instead, does that make sense for this implementation?
There was a problem hiding this comment.
I'm not sure comparing potentially large strings on every generation run is the way to go here. I'd rather cache the mocked file names and their latest modification date which is then compared to the generated file modification date. I'm not asking you to implement that, buut I'd rather leave the logic as it is until that is implemented.
MatyasKriz
left a comment
There was a problem hiding this comment.
Looks great! I appreciate you taking the time to improve the project like this. Especially the automatic source detection might be nice for small projects.
One more thing, though – please mention this new modular build tool in README, so that users don't miss it. 🙂
|
@MatyasKriz Thank you for the review and feedbacks! I'll address them as soon as I have some time. |
Summary
Adds a new
CuckooPluginPerModulebuild tool plugin that generates separate mock files for each module dependency, improving modularity and build performance. Also adds compound module name support for per-target mock configuration.Related issue: #555
Motivation
The existing
CuckooPluginSingleFilegenerates all mocks into a singleGeneratedMocks.swiftfile, which forces test targets to depend on mocks for all modules. This creates unnecessary coupling and can slow down incremental builds when using multiple test targets in a Swift Package.Additionally, when multiple test targets depend on the same module but need different mocks generated (e.g., different source files from that module), there was no way to configure this per-target.
Changes
New Plugin: CuckooPluginPerModule
GeneratedMocks_<ModuleName>.swiftCUCKOO_MODULE_NAMEenvironment variableGenerator/Plugin/PerModule/Performance Fix: Incremental Build Optimization
Added content-equality guards in
GenerateCommand.swiftto prevent unnecessary file writes:This prevents mtime churn on generated files, which was causing Swift to recompile all test sources on every build even when mocks were unchanged.
Compound Module Name Support
Enables test targets to override shared module mock generation:
CUCKOO_COMPOUND_MODULE_NAMEenvironment variable with formatTARGET/MODULE(e.g.,ManualCardEntryFeatureTests/PaymentMethodOnFileAPI)Cuckoofile.tomlentriessourcesarray generate empty output filesThis allows
Cuckoofile.tomlto specify different mock sources for the same module when used in different test targets.Example use case:
MyFirstModuleTestsneeds mocks fromMySharedModule'sClient.swiftbut not fromMySharedModule'sGenerated/Types.swift(which has unwanted dependencies).Backward compatible: Falls back to
CUCKOO_MODULE_NAMEif compound key not found.Usage
Basic Per-Module Plugin
Just add the plugin - it automatically generates mocks for all protocols/classes in your dependencies.
Package.swift:
Output:
No
Cuckoofile.tomlneeded - mocks are generated from all source files inMyFirstModule.Advanced: Per-Target Configuration
Use
Cuckoofile.tomlto customize which source files from each module get mocked. Useful when:Package.swift:
Cuckoofile.toml:
Result:
MyFirstModuleTestsgets mocks fromClient.swift(from test target config) ANDTypes.swift(from shared config)MySecondModuleTestsgets mocks fromTypes.swiftonlyMySharedModuledependency but generate different mocksNote: The plugin generates mocks for both the test target itself and its dependencies. If you need to prevent the shared config from also running (e.g., to avoid compilation overhead or mock conflicts), you can add a suppressor entry with empty sources using the compound module name format
["modules"."MyFirstModuleTests/MySharedModule"].Breaking Changes
None. All changes are backward compatible with existing plugins and configurations.