Skip to main content

provenant/assembly/
assemblers.rs

1use crate::models::{DatasourceId, FileInfo, Package, TopLevelDependency};
2use strum::EnumIter;
3
4use super::{
5    AssemblerConfig, AssemblyMode, DirectoryMergeOutput, cargo_resource_assign,
6    cargo_workspace_merge, composer_resource_assign, conda_rootfs_merge, file_ref_resolve,
7    hackage_merge, npm_resource_assign, npm_workspace_merge, nuget_cpm_resolve,
8    ruby_resource_assign, swift_merge,
9};
10
11#[derive(Clone, Copy)]
12pub(super) enum SpecialDirectoryMergerKind {
13    Skip,
14    Hackage,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, EnumIter)]
18pub(super) enum PostAssemblyPassKind {
19    SwiftMerge,
20    CondaRootfsMerge,
21    NpmResourceAssign,
22    FileReferenceResolve,
23    RpmYumdbMerge,
24    NpmWorkspaceMerge,
25    CargoWorkspaceMerge,
26    NugetCpmResolve,
27    CargoResourceAssign,
28    ComposerResourceAssign,
29    RubyResourceAssign,
30}
31
32pub(super) fn special_directory_merger_for(
33    config_key: DatasourceId,
34) -> Option<SpecialDirectoryMergerKind> {
35    match config_key {
36        DatasourceId::HackageCabal => Some(SpecialDirectoryMergerKind::Hackage),
37        DatasourceId::SwiftPackageManifestJson => Some(SpecialDirectoryMergerKind::Skip),
38        _ => None,
39    }
40}
41
42pub(super) static POST_ASSEMBLY_PASSES: &[PostAssemblyPassKind] = &[
43    PostAssemblyPassKind::SwiftMerge,
44    PostAssemblyPassKind::CondaRootfsMerge,
45    PostAssemblyPassKind::NpmResourceAssign,
46    PostAssemblyPassKind::FileReferenceResolve,
47    PostAssemblyPassKind::RpmYumdbMerge,
48    PostAssemblyPassKind::NpmWorkspaceMerge,
49    PostAssemblyPassKind::CargoWorkspaceMerge,
50    PostAssemblyPassKind::NugetCpmResolve,
51    PostAssemblyPassKind::CargoResourceAssign,
52    PostAssemblyPassKind::ComposerResourceAssign,
53    PostAssemblyPassKind::RubyResourceAssign,
54];
55
56pub(super) fn run_post_assembly_passes(
57    files: &mut [FileInfo],
58    packages: &mut Vec<Package>,
59    dependencies: &mut Vec<TopLevelDependency>,
60) {
61    for pass in POST_ASSEMBLY_PASSES {
62        pass.run(files, packages, dependencies);
63    }
64}
65
66impl SpecialDirectoryMergerKind {
67    pub(super) fn run(
68        self,
69        files: &[FileInfo],
70        file_indices: &[usize],
71    ) -> Vec<DirectoryMergeOutput> {
72        match self {
73            Self::Skip => Vec::new(),
74            Self::Hackage => hackage_merge::assemble_hackage_packages(files, file_indices),
75        }
76    }
77}
78
79impl PostAssemblyPassKind {
80    fn run(
81        self,
82        files: &mut [FileInfo],
83        packages: &mut Vec<Package>,
84        dependencies: &mut Vec<TopLevelDependency>,
85    ) {
86        match self {
87            Self::SwiftMerge => swift_merge::assemble_swift_packages(files, packages, dependencies),
88            Self::CondaRootfsMerge => {
89                conda_rootfs_merge::merge_conda_rootfs_metadata(files, packages, dependencies)
90            }
91            Self::NpmResourceAssign => {
92                npm_resource_assign::assign_npm_package_resources(files, packages)
93            }
94            Self::FileReferenceResolve => {
95                file_ref_resolve::resolve_file_references(files, packages, dependencies)
96            }
97            Self::RpmYumdbMerge => file_ref_resolve::merge_rpm_yumdb_metadata(files, packages),
98            Self::NpmWorkspaceMerge => {
99                npm_workspace_merge::assemble_npm_workspaces(files, packages, dependencies)
100            }
101            Self::CargoWorkspaceMerge => {
102                cargo_workspace_merge::assemble_cargo_workspaces(files, packages, dependencies)
103            }
104            Self::NugetCpmResolve => {
105                nuget_cpm_resolve::resolve_nuget_cpm_versions(files, dependencies)
106            }
107            Self::CargoResourceAssign => {
108                cargo_resource_assign::assign_cargo_package_resources(files, packages)
109            }
110            Self::ComposerResourceAssign => {
111                composer_resource_assign::assign_composer_package_resources(files, packages)
112            }
113            Self::RubyResourceAssign => {
114                ruby_resource_assign::assign_ruby_package_resources(files, packages)
115            }
116        }
117    }
118}
119
120pub static ASSEMBLERS: &[AssemblerConfig] = &[
121    // ── Sibling-merge assemblers ──
122    //
123    // npm ecosystem: package.json + lockfiles in same directory.
124    // NOTE: npm-shrinkwrap.json emits "npm_package_lock_json" as its datasource_id,
125    // so "npm_shrinkwrap_json" is NOT a real datasource_id.
126    AssemblerConfig {
127        datasource_ids: &[
128            DatasourceId::BunLock,
129            DatasourceId::BunLockb,
130            DatasourceId::NpmPackageJson,
131            DatasourceId::NpmPackageLockJson,
132            DatasourceId::YarnLock,
133            DatasourceId::PnpmLockYaml,
134            DatasourceId::PnpmWorkspaceYaml,
135        ],
136        sibling_file_patterns: &[
137            "package.json",
138            "bun.lock",
139            "bun.lockb",
140            "package-lock.json",
141            "npm-shrinkwrap.json",
142            "yarn.lock",
143            "pnpm-lock.yaml",
144            "pnpm-workspace.yaml",
145        ],
146        mode: AssemblyMode::SiblingMerge,
147    },
148    // Rust/Cargo ecosystem
149    AssemblerConfig {
150        datasource_ids: &[DatasourceId::CargoToml, DatasourceId::CargoLock],
151        sibling_file_patterns: &["Cargo.toml", "Cargo.lock"],
152        mode: AssemblyMode::SiblingMerge,
153    },
154    // CocoaPods ecosystem
155    AssemblerConfig {
156        datasource_ids: &[
157            DatasourceId::CocoapodsPodspec,
158            DatasourceId::CocoapodsPodspecJson,
159            DatasourceId::CocoapodsPodfile,
160            DatasourceId::CocoapodsPodfileLock,
161        ],
162        sibling_file_patterns: &["*.podspec", "*.podspec.json", "Podfile", "Podfile.lock"],
163        mode: AssemblyMode::SiblingMerge,
164    },
165    // PHP Composer ecosystem
166    AssemblerConfig {
167        datasource_ids: &[DatasourceId::PhpComposerJson, DatasourceId::PhpComposerLock],
168        sibling_file_patterns: &[
169            "*composer.json",
170            "composer.*.json",
171            "*composer.lock",
172            "composer.*.lock",
173        ],
174        mode: AssemblyMode::SiblingMerge,
175    },
176    // Go ecosystem (includes legacy Godeps)
177    AssemblerConfig {
178        datasource_ids: &[
179            DatasourceId::GoMod,
180            DatasourceId::GoModGraph,
181            DatasourceId::GoSum,
182            DatasourceId::GoWork,
183            DatasourceId::Godeps,
184        ],
185        sibling_file_patterns: &[
186            "go.mod",
187            "go.work",
188            "go.mod.graph",
189            "go.modgraph",
190            "go.sum",
191            "Godeps.json",
192        ],
193        mode: AssemblyMode::SiblingMerge,
194    },
195    // Dart/Flutter ecosystem
196    AssemblerConfig {
197        datasource_ids: &[DatasourceId::PubspecYaml, DatasourceId::PubspecLock],
198        sibling_file_patterns: &["pubspec.yaml", "pubspec.lock"],
199        mode: AssemblyMode::SiblingMerge,
200    },
201    // Pixi ecosystem
202    AssemblerConfig {
203        datasource_ids: &[DatasourceId::PixiToml, DatasourceId::PixiLock],
204        sibling_file_patterns: &["pixi.toml", "pixi.lock"],
205        mode: AssemblyMode::SiblingMerge,
206    },
207    // Helm chart ecosystem
208    AssemblerConfig {
209        datasource_ids: &[DatasourceId::HelmChartYaml, DatasourceId::HelmChartLock],
210        sibling_file_patterns: &["Chart.yaml", "Chart.lock"],
211        mode: AssemblyMode::SiblingMerge,
212    },
213    AssemblerConfig {
214        datasource_ids: &[
215            DatasourceId::HackageCabal,
216            DatasourceId::HackageCabalProject,
217            DatasourceId::HackageStackYaml,
218        ],
219        sibling_file_patterns: &["*.cabal", "cabal.project", "stack.yaml"],
220        mode: AssemblyMode::SiblingMerge,
221    },
222    // Chef ecosystem
223    AssemblerConfig {
224        datasource_ids: &[
225            DatasourceId::ChefCookbookMetadataJson,
226            DatasourceId::ChefCookbookMetadataRb,
227        ],
228        sibling_file_patterns: &["metadata.json", "metadata.rb"],
229        mode: AssemblyMode::SiblingMerge,
230    },
231    // Conan (C/C++) ecosystem
232    AssemblerConfig {
233        datasource_ids: &[
234            DatasourceId::ConanConanFilePy,
235            DatasourceId::ConanConanFileTxt,
236            DatasourceId::ConanLock,
237            DatasourceId::ConanConanDataYml,
238        ],
239        sibling_file_patterns: &[
240            "conanfile.py",
241            "conanfile.txt",
242            "conan.lock",
243            "conandata.yml",
244        ],
245        mode: AssemblyMode::SiblingMerge,
246    },
247    // Maven/Java ecosystem (nested merge via META-INF)
248    AssemblerConfig {
249        datasource_ids: &[
250            DatasourceId::MavenPom,
251            DatasourceId::MavenPomProperties,
252            DatasourceId::JavaJarManifest,
253            DatasourceId::JavaOsgiManifest,
254        ],
255        sibling_file_patterns: &["pom.xml", "pom.properties", "**/META-INF/MANIFEST.MF"],
256        mode: AssemblyMode::SiblingMerge,
257    },
258    AssemblerConfig {
259        datasource_ids: &[DatasourceId::PypiWheel, DatasourceId::PypiPipOriginJson],
260        sibling_file_patterns: &["*.whl", "origin.json"],
261        mode: AssemblyMode::SiblingMerge,
262    },
263    // Python/PyPI ecosystem
264    AssemblerConfig {
265        datasource_ids: &[
266            DatasourceId::PypiPyprojectToml,
267            DatasourceId::PypiSetupPy,
268            DatasourceId::PypiSetupCfg,
269            DatasourceId::PypiWheel,
270            DatasourceId::PypiWheelMetadata,
271            DatasourceId::PypiEgg,
272            DatasourceId::PypiJson,
273            DatasourceId::PypiSdistPkginfo,
274            DatasourceId::PypiInspectDeplock,
275            DatasourceId::PipRequirements,
276            DatasourceId::PypiPoetryLock,
277            DatasourceId::PypiPylockToml,
278            DatasourceId::PypiUvLock,
279            DatasourceId::Pipfile,
280            DatasourceId::PipfileLock,
281        ],
282        sibling_file_patterns: &[
283            "pyproject.toml",
284            "setup.py",
285            "setup.cfg",
286            "PKG-INFO",
287            "METADATA",
288            "pypi.json",
289            "requirements*.txt",
290            "Pipfile",
291            "Pipfile.lock",
292            "poetry.lock",
293            "pylock.toml",
294            "pylock.*.toml",
295            "uv.lock",
296        ],
297        mode: AssemblyMode::SiblingMerge,
298    },
299    AssemblerConfig {
300        datasource_ids: &[DatasourceId::DenoJson, DatasourceId::DenoLock],
301        sibling_file_patterns: &["deno.json", "deno.jsonc", "deno.lock"],
302        mode: AssemblyMode::SiblingMerge,
303    },
304    // Ruby/RubyGems ecosystem
305    AssemblerConfig {
306        datasource_ids: &[
307            DatasourceId::GemArchiveExtracted,
308            DatasourceId::Gemspec,
309            DatasourceId::Gemfile,
310            DatasourceId::GemfileLock,
311            DatasourceId::GemArchive,
312        ],
313        sibling_file_patterns: &[
314            "metadata.gz-extract",
315            "**/data.gz-extract/*.gemspec",
316            "**/data.gz-extract/Gemfile",
317            "**/data.gz-extract/Gemfile.lock",
318            "*.gemspec",
319            "Gemfile",
320            "Gemfile.lock",
321        ],
322        mode: AssemblyMode::SiblingMerge,
323    },
324    // Conda ecosystem
325    AssemblerConfig {
326        datasource_ids: &[
327            DatasourceId::CondaMetaYaml,
328            DatasourceId::CondaYaml,
329            DatasourceId::CondaMetaJson,
330        ],
331        sibling_file_patterns: &[
332            "meta.yaml",
333            "meta.yml",
334            "environment.yml",
335            "environment.yaml",
336            "conda.yaml",
337            "env.yaml",
338            "*.json",
339        ],
340        mode: AssemblyMode::SiblingMerge,
341    },
342    // RPM specfile (source packages)
343    AssemblerConfig {
344        datasource_ids: &[DatasourceId::RpmSpecfile],
345        sibling_file_patterns: &["*.spec"],
346        mode: AssemblyMode::SiblingMerge,
347    },
348    // Debian source packages (nested merge via debian/ directory)
349    AssemblerConfig {
350        datasource_ids: &[
351            DatasourceId::DebianControlInSource,
352            DatasourceId::DebianCopyright,
353        ],
354        sibling_file_patterns: &["**/debian/control", "**/debian/copyright"],
355        mode: AssemblyMode::SiblingMerge,
356    },
357    // Gradle/Android ecosystem
358    AssemblerConfig {
359        datasource_ids: &[DatasourceId::BuildGradle, DatasourceId::GradleLockfile],
360        sibling_file_patterns: &["build.gradle", "build.gradle.kts", "gradle.lockfile"],
361        mode: AssemblyMode::SiblingMerge,
362    },
363    AssemblerConfig {
364        datasource_ids: &[DatasourceId::GradleModule],
365        sibling_file_patterns: &["*.module"],
366        mode: AssemblyMode::OnePerPackageData,
367    },
368    // CPAN/Perl ecosystem
369    AssemblerConfig {
370        datasource_ids: &[
371            DatasourceId::CpanMetaJson,
372            DatasourceId::CpanMetaYml,
373            DatasourceId::CpanManifest,
374            DatasourceId::CpanDistIni,
375            DatasourceId::CpanMakefile,
376        ],
377        sibling_file_patterns: &[
378            "META.json",
379            "META.yml",
380            "MANIFEST",
381            "dist.ini",
382            "Makefile.PL",
383        ],
384        mode: AssemblyMode::SiblingMerge,
385    },
386    // NuGet/.NET ecosystem
387    AssemblerConfig {
388        datasource_ids: &[
389            DatasourceId::NugetCsproj,
390            DatasourceId::NugetFsproj,
391            DatasourceId::NugetNuspec,
392            DatasourceId::NugetNupkg,
393            DatasourceId::NugetProjectJson,
394            DatasourceId::NugetProjectLockJson,
395            DatasourceId::NugetPackagesConfig,
396            DatasourceId::NugetPackagesLock,
397            DatasourceId::NugetVbproj,
398        ],
399        sibling_file_patterns: &[
400            "*.csproj",
401            "*.fsproj",
402            "*.nuspec",
403            "*.nupkg",
404            "project.json",
405            "project.lock.json",
406            "packages.config",
407            "packages.lock.json",
408            "*.vbproj",
409        ],
410        mode: AssemblyMode::SiblingMerge,
411    },
412    AssemblerConfig {
413        datasource_ids: &[DatasourceId::NugetDepsJson],
414        sibling_file_patterns: &["*.deps.json"],
415        mode: AssemblyMode::OnePerPackageData,
416    },
417    // Swift/SPM ecosystem
418    AssemblerConfig {
419        datasource_ids: &[
420            DatasourceId::SwiftPackageManifestJson,
421            DatasourceId::SwiftPackageResolved,
422            DatasourceId::SwiftPackageShowDependencies,
423        ],
424        sibling_file_patterns: &["Package.swift", "Package.resolved"],
425        mode: AssemblyMode::SiblingMerge,
426    },
427    // ── Standalone assemblers (single file → single package) ──
428    //
429    // These ecosystems have only one manifest file type with no sibling merging.
430    // They still need configs so their datasource_ids are recognized by the assembler.
431    //
432    // Bower (JavaScript)
433    AssemblerConfig {
434        datasource_ids: &[DatasourceId::BowerJson],
435        sibling_file_patterns: &["bower.json"],
436        mode: AssemblyMode::SiblingMerge,
437    },
438    // CRAN (R language)
439    AssemblerConfig {
440        datasource_ids: &[DatasourceId::CranDescription],
441        sibling_file_patterns: &["DESCRIPTION"],
442        mode: AssemblyMode::SiblingMerge,
443    },
444    // FreeBSD packages
445    AssemblerConfig {
446        datasource_ids: &[DatasourceId::FreebsdCompactManifest],
447        sibling_file_patterns: &["+COMPACT_MANIFEST"],
448        mode: AssemblyMode::SiblingMerge,
449    },
450    // Haxe ecosystem
451    AssemblerConfig {
452        datasource_ids: &[DatasourceId::HaxelibJson],
453        sibling_file_patterns: &["haxelib.json"],
454        mode: AssemblyMode::SiblingMerge,
455    },
456    // OCaml/opam ecosystem
457    AssemblerConfig {
458        datasource_ids: &[DatasourceId::OpamFile],
459        sibling_file_patterns: &["opam"],
460        mode: AssemblyMode::SiblingMerge,
461    },
462    // RPM Mariner manifest
463    AssemblerConfig {
464        datasource_ids: &[DatasourceId::RpmMarinerManifest],
465        sibling_file_patterns: &["*.rpm.manifest"],
466        mode: AssemblyMode::SiblingMerge,
467    },
468    AssemblerConfig {
469        datasource_ids: &[DatasourceId::RpmYumdb],
470        sibling_file_patterns: &["**/var/lib/yum/yumdb/*/*/from_repo"],
471        mode: AssemblyMode::OnePerPackageData,
472    },
473    // Microsoft Update Manifest
474    AssemblerConfig {
475        datasource_ids: &[DatasourceId::MicrosoftUpdateManifestMum],
476        sibling_file_patterns: &["*.mum"],
477        mode: AssemblyMode::SiblingMerge,
478    },
479    // Autotools (C/C++ build system)
480    AssemblerConfig {
481        datasource_ids: &[DatasourceId::AutotoolsConfigure],
482        sibling_file_patterns: &["configure", "configure.ac"],
483        mode: AssemblyMode::SiblingMerge,
484    },
485    // Bazel (build system)
486    AssemblerConfig {
487        datasource_ids: &[DatasourceId::BazelBuild],
488        sibling_file_patterns: &["BUILD"],
489        mode: AssemblyMode::SiblingMerge,
490    },
491    AssemblerConfig {
492        datasource_ids: &[DatasourceId::BazelModule],
493        sibling_file_patterns: &["MODULE.bazel"],
494        mode: AssemblyMode::OnePerPackageData,
495    },
496    // Buck (build system)
497    AssemblerConfig {
498        datasource_ids: &[DatasourceId::BuckFile, DatasourceId::BuckMetadata],
499        sibling_file_patterns: &["BUCK", ".buckconfig"],
500        mode: AssemblyMode::SiblingMerge,
501    },
502    // Ant/Ivy (Java dependency management)
503    AssemblerConfig {
504        datasource_ids: &[DatasourceId::AntIvyXml],
505        sibling_file_patterns: &["ivy.xml"],
506        mode: AssemblyMode::SiblingMerge,
507    },
508    // Meteor (JavaScript platform)
509    AssemblerConfig {
510        datasource_ids: &[DatasourceId::MeteorPackage],
511        sibling_file_patterns: &["package.js"],
512        mode: AssemblyMode::SiblingMerge,
513    },
514    // ── One-per-PackageData assemblers (database files with many packages) ──
515    //
516    // Alpine installed package database
517    AssemblerConfig {
518        datasource_ids: &[DatasourceId::AlpineInstalledDb],
519        sibling_file_patterns: &["installed"],
520        mode: AssemblyMode::OnePerPackageData,
521    },
522    AssemblerConfig {
523        datasource_ids: &[DatasourceId::AlpineApkbuild],
524        sibling_file_patterns: &["APKBUILD"],
525        mode: AssemblyMode::SiblingMerge,
526    },
527    // RPM installed package databases (BDB, NDB, SQLite)
528    AssemblerConfig {
529        datasource_ids: &[
530            DatasourceId::RpmInstalledDatabaseBdb,
531            DatasourceId::RpmInstalledDatabaseNdb,
532            DatasourceId::RpmInstalledDatabaseSqlite,
533        ],
534        sibling_file_patterns: &["Packages", "Packages.db", "rpmdb.sqlite"],
535        mode: AssemblyMode::OnePerPackageData,
536    },
537    // Debian installed package databases
538    AssemblerConfig {
539        datasource_ids: &[
540            DatasourceId::DebianInstalledStatusDb,
541            DatasourceId::DebianDistrolessInstalledDb,
542        ],
543        sibling_file_patterns: &["status"],
544        mode: AssemblyMode::OnePerPackageData,
545    },
546    AssemblerConfig {
547        datasource_ids: &[DatasourceId::AboutFile],
548        sibling_file_patterns: &["*.ABOUT"],
549        mode: AssemblyMode::OnePerPackageData,
550    },
551];
552
553/// Datasource IDs that are intentionally NOT assembled.
554///
555/// These are either:
556/// - Non-package metadata (readme, about, os_release)
557/// - Binary archives (require external extraction via ExtractCode before scanning)
558/// - Supplementary metadata files (not primary package definitions)
559///
560/// This list serves as documentation; it is not used at runtime.
561#[allow(dead_code)] // used only in tests (test_every_datasource_id_is_accounted_for)
562pub static UNASSEMBLED_DATASOURCE_IDS: &[DatasourceId] = &[
563    // Non-package metadata
564    DatasourceId::Readme,
565    DatasourceId::EtcOsRelease,
566    DatasourceId::Gitmodules,
567    // Binary archives (require external extraction via ExtractCode before scanning)
568    DatasourceId::AlpineApkArchive,
569    DatasourceId::AndroidAarLibrary,
570    DatasourceId::AndroidApk,
571    DatasourceId::AppleDmg,
572    DatasourceId::Axis2Mar,
573    DatasourceId::ChromeCrx,
574    DatasourceId::DebianDeb,
575    DatasourceId::DebianOriginalSourceTarball,
576    DatasourceId::DebianSourceMetadataTarball,
577    DatasourceId::InstallshieldInstaller,
578    DatasourceId::IosIpa,
579    DatasourceId::IsoDiskImage,
580    DatasourceId::JavaEarArchive,
581    DatasourceId::JavaJar,
582    DatasourceId::JavaWarArchive,
583    DatasourceId::JbossSar,
584    DatasourceId::MicrosoftCabinet,
585    DatasourceId::MozillaXpi,
586    DatasourceId::NsisInstaller,
587    DatasourceId::RpmArchive,
588    DatasourceId::SharShellArchive,
589    DatasourceId::SquashfsDiskImage,
590    // Supplementary metadata (not primary package definitions)
591    DatasourceId::ArchAurinfo,
592    DatasourceId::ArchPkginfo,
593    DatasourceId::ArchSrcinfo,
594    DatasourceId::Axis2ModuleXml,
595    DatasourceId::ClojureDepsEdn,
596    DatasourceId::ClojureProjectClj,
597    DatasourceId::DebianControlExtractedDeb,
598    DatasourceId::DebianInstalledFilesList,
599    DatasourceId::DebianInstalledMd5Sums,
600    DatasourceId::DebianMd5SumsInExtractedDeb,
601    DatasourceId::DebianSourceControlDsc,
602    DatasourceId::Dockerfile,
603    DatasourceId::HexMixLock,
604    DatasourceId::JavaEarApplicationXml,
605    DatasourceId::JavaWarWebXml,
606    DatasourceId::JbossServiceXml,
607    DatasourceId::MesonBuild,
608    DatasourceId::NugetDirectoryBuildProps,
609    DatasourceId::NugetDirectoryPackagesProps,
610    DatasourceId::RpmPackageLicenses,
611    DatasourceId::SbtBuildSbt,
612    DatasourceId::VcpkgJson,
613];
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618    use std::collections::HashSet;
619    use strum::IntoEnumIterator;
620
621    #[test]
622    fn test_every_datasource_id_is_accounted_for() {
623        let mut assembled: HashSet<DatasourceId> = HashSet::new();
624        for config in ASSEMBLERS {
625            for &dsid in config.datasource_ids {
626                assembled.insert(dsid);
627            }
628        }
629
630        let unassembled: HashSet<DatasourceId> =
631            UNASSEMBLED_DATASOURCE_IDS.iter().copied().collect();
632
633        let overlap: Vec<_> = assembled.intersection(&unassembled).collect();
634        assert!(
635            overlap.is_empty(),
636            "Datasource IDs in BOTH ASSEMBLERS and UNASSEMBLED: {overlap:?}"
637        );
638
639        let missing: Vec<_> = DatasourceId::iter()
640            .filter(|dsid| !assembled.contains(dsid) && !unassembled.contains(dsid))
641            .collect();
642
643        assert!(
644            missing.is_empty(),
645            "Datasource IDs in NEITHER ASSEMBLERS nor UNASSEMBLED: {missing:?}\n\
646             Add each to an AssemblerConfig in ASSEMBLERS, or to UNASSEMBLED_DATASOURCE_IDS."
647        );
648    }
649
650    #[test]
651    fn test_post_assembly_passes_are_unique() {
652        let unique: HashSet<PostAssemblyPassKind> = POST_ASSEMBLY_PASSES.iter().copied().collect();
653
654        assert_eq!(
655            unique.len(),
656            POST_ASSEMBLY_PASSES.len(),
657            "POST_ASSEMBLY_PASSES contains duplicate entries"
658        );
659    }
660
661    #[test]
662    fn test_every_post_assembly_pass_kind_is_registered_once() {
663        let registered: HashSet<PostAssemblyPassKind> =
664            POST_ASSEMBLY_PASSES.iter().copied().collect();
665
666        let missing: Vec<_> = PostAssemblyPassKind::iter()
667            .filter(|pass| !registered.contains(pass))
668            .collect();
669
670        assert!(
671            missing.is_empty(),
672            "Post-assembly pass variants not registered in POST_ASSEMBLY_PASSES: {missing:?}"
673        );
674
675        for pass in PostAssemblyPassKind::iter() {
676            let count = POST_ASSEMBLY_PASSES
677                .iter()
678                .filter(|registered| **registered == pass)
679                .count();
680            assert_eq!(
681                count, 1,
682                "Post-assembly pass {pass:?} should be registered exactly once"
683            );
684        }
685    }
686}