use std::cell::RefCell; use std::fs; use alt_controller_image::build::BuildExecutor; use alt_controller_image::initramfs::{InitramfsBuilder, InitramfsRequest, InitramfsResult}; use alt_controller_image::manifest::ArtifactManifest; use alt_controller_image::package_installer::{PackageInstaller, PackageRequest}; use alt_controller_image::{BuildPlan, ImageSpec}; use tempfile::tempdir; #[derive(Default)] struct FixtureInstaller { requests: RefCell>, } impl PackageInstaller for FixtureInstaller { fn install(&self, request: &PackageRequest) -> anyhow::Result<()> { self.requests.borrow_mut().push(request.clone()); let boot = request.workdir().join("chroot/boot"); fs::create_dir_all(&boot)?; fs::write(boot.join("vmlinuz-6.12.0-rt1"), "kernel")?; Ok(()) } } struct FailingInstaller; impl PackageInstaller for FailingInstaller { fn install(&self, _request: &PackageRequest) -> anyhow::Result<()> { anyhow::bail!("simulated package installation failure") } } struct RootfslessInstaller; impl PackageInstaller for RootfslessInstaller { fn install(&self, _request: &PackageRequest) -> anyhow::Result<()> { Ok(()) } } struct RootfsFinalizationFailingInstaller; impl PackageInstaller for RootfsFinalizationFailingInstaller { fn install(&self, request: &PackageRequest) -> anyhow::Result<()> { let rootfs = request.workdir().join("chroot"); fs::create_dir_all(rootfs.join("boot"))?; fs::write(rootfs.join("boot/vmlinuz-6.12.0-rt1"), "kernel")?; fs::write(rootfs.join("etc"), "not a directory")?; Ok(()) } } struct FailingInitramfsBuilder; impl InitramfsBuilder for FailingInitramfsBuilder { fn build(&mut self, _request: &InitramfsRequest) -> anyhow::Result { anyhow::bail!("simulated initramfs build failure") } } struct ArchiveFailingInstaller; impl PackageInstaller for ArchiveFailingInstaller { fn install(&self, request: &PackageRequest) -> anyhow::Result<()> { let rootfs = request.workdir().join("chroot"); fs::create_dir_all(rootfs.join("boot"))?; fs::write(rootfs.join("boot/vmlinuz-6.12.0-rt1"), "kernel")?; std::os::unix::net::UnixListener::bind(rootfs.join("unsupported.socket")) .expect("create unsupported socket fixture") .set_nonblocking(true)?; Ok(()) } } struct MutatingInitrdBuilder; impl InitramfsBuilder for MutatingInitrdBuilder { fn build(&mut self, request: &InitramfsRequest) -> anyhow::Result { let initrd = request .rootfs() .join(format!("boot/initrd-{}.img", request.kernel().as_str())); fs::write(&initrd, "primary initrd")?; let result = InitramfsResult::from_rootfs(request.rootfs(), request.kernel())?; fs::write(initrd, "mutated initrd")?; Ok(result) } } #[derive(Default)] struct FixtureInitramfsBuilder; impl InitramfsBuilder for FixtureInitramfsBuilder { fn build(&mut self, request: &InitramfsRequest) -> anyhow::Result { fs::write( request .rootfs() .join(format!("boot/initrd-{}.img", request.kernel().as_str())), "initrd", )?; InitramfsResult::from_rootfs(request.rootfs(), request.kernel()) } } #[test] fn executes_the_native_build_stages_and_writes_a_companion_manifest() { let fixture = tempdir().expect("fixture directory"); let spec = ImageSpec::load(std::path::Path::new("profiles/alt-controller.toml")) .expect("load controller spec"); let plan = BuildPlan::compile(spec).expect("compile build plan"); let installer = FixtureInstaller::default(); let mut executor = BuildExecutor::new(&installer, FixtureInitramfsBuilder); let workspace = fixture.path().join("work"); let artifact = fixture.path().join("out/controller.tar"); let result = executor .execute(&plan, &workspace, "profiles/apt.conf", &artifact) .expect("execute native build"); assert_eq!(installer.requests.borrow().len(), 1); assert_eq!(result.artifact(), artifact); assert_eq!( fs::read_to_string(workspace.join("chroot/etc/initrd.mk.oem")).expect("read OEM recipe"), "FEATURES += add-modules cleanup compress qemu rdshell rootfs usb\nMODULES += ahci.ko ahci_platform.ko ext4 sd_mod.ko virtio_blk.ko virtio_net.ko virtio_pci.ko\n" ); assert!(artifact.is_file()); let manifest = ArtifactManifest::load(result.manifest()).expect("load companion manifest"); let initrd = result.initrd().expect("initrd result"); assert_eq!( manifest.initrd, Some(alt_controller_image::manifest::InitrdRecord::new( initrd.initrd_path().display().to_string(), initrd.sha256(), )) ); } #[test] fn refuses_to_reuse_an_existing_workspace() { let fixture = tempdir().expect("fixture directory"); let spec = ImageSpec::load(std::path::Path::new("profiles/alt-controller.toml")) .expect("load controller spec"); let plan = BuildPlan::compile(spec).expect("compile build plan"); let installer = FixtureInstaller::default(); let mut executor = BuildExecutor::new(&installer, FixtureInitramfsBuilder); let workspace = fixture.path().join("existing-work"); fs::create_dir(&workspace).expect("create existing workspace"); let error = executor .execute( &plan, &workspace, "profiles/apt.conf", fixture.path().join("image.tar"), ) .expect_err("existing workspace must be rejected"); assert!(error.to_string().contains("workspace already exists")); assert!(installer.requests.borrow().is_empty()); } #[test] fn rejects_an_existing_artifact_before_creating_the_workspace_or_installing() { let fixture = tempdir().expect("fixture directory"); let spec = ImageSpec::load(std::path::Path::new("profiles/alt-controller.toml")) .expect("load controller spec"); let plan = BuildPlan::compile(spec).expect("compile build plan"); let installer = FixtureInstaller::default(); let mut executor = BuildExecutor::new(&installer, FixtureInitramfsBuilder); let workspace = fixture.path().join("work"); let artifact = fixture.path().join("existing.tar"); fs::write(&artifact, "do not replace").expect("write pre-existing artifact"); let error = executor .execute(&plan, &workspace, "profiles/apt.conf", &artifact) .expect_err("pre-existing artifact must be rejected"); assert!(error.to_string().contains("output artifact already exists")); assert_eq!( fs::read_to_string(&artifact).expect("read artifact"), "do not replace" ); assert!(!workspace.exists()); assert!(installer.requests.borrow().is_empty()); } #[test] fn rejects_an_existing_companion_manifest_before_creating_the_workspace_or_installing() { let fixture = tempdir().expect("fixture directory"); let spec = ImageSpec::load(std::path::Path::new("profiles/alt-controller.toml")) .expect("load controller spec"); let plan = BuildPlan::compile(spec).expect("compile build plan"); let installer = FixtureInstaller::default(); let mut executor = BuildExecutor::new(&installer, FixtureInitramfsBuilder); let workspace = fixture.path().join("work"); let artifact = fixture.path().join("controller.tar"); let companion = ArtifactManifest::path_beside(&artifact).expect("companion manifest path"); fs::write(&companion, "do not replace").expect("write pre-existing companion manifest"); let error = executor .execute(&plan, &workspace, "profiles/apt.conf", &artifact) .expect_err("pre-existing companion manifest must be rejected"); assert!(error.to_string().contains("output manifest already exists")); assert_eq!( fs::read_to_string(&companion).expect("read companion manifest"), "do not replace" ); assert!(!workspace.exists()); assert!(installer.requests.borrow().is_empty()); } #[test] fn rejects_a_missing_apt_configuration_before_creating_the_workspace() { let fixture = tempdir().expect("fixture directory"); let spec = ImageSpec::load(std::path::Path::new("profiles/alt-controller.toml")) .expect("load controller spec"); let plan = BuildPlan::compile(spec).expect("compile build plan"); let installer = FixtureInstaller::default(); let mut executor = BuildExecutor::new(&installer, FixtureInitramfsBuilder); let workspace = fixture.path().join("work"); let missing_apt_config = fixture.path().join("missing-apt.conf"); let error = executor .execute( &plan, &workspace, &missing_apt_config, fixture.path().join("image.tar"), ) .expect_err("missing APT configuration must be rejected"); assert!( error .to_string() .contains("APT configuration does not exist") ); assert!(!workspace.exists()); assert!(installer.requests.borrow().is_empty()); } #[test] fn removes_the_new_workspace_when_package_installation_fails() { let fixture = tempdir().expect("fixture directory"); let spec = ImageSpec::load(std::path::Path::new("profiles/alt-controller.toml")) .expect("load controller spec"); let plan = BuildPlan::compile(spec).expect("compile build plan"); let mut executor = BuildExecutor::new(FailingInstaller, FixtureInitramfsBuilder); let workspace = fixture.path().join("work"); let artifact = fixture.path().join("image.tar"); let error = executor .execute(&plan, &workspace, "profiles/apt.conf", &artifact) .expect_err("failed installation must fail the build"); assert!( error .to_string() .contains("simulated package installation failure") ); assert!( !workspace.exists(), "failed build must not leave a workspace" ); assert!(!artifact.exists()); } #[test] fn removes_the_new_workspace_when_the_installer_does_not_create_a_rootfs() { let fixture = tempdir().expect("fixture directory"); let spec = ImageSpec::load(std::path::Path::new("profiles/alt-controller.toml")) .expect("load controller spec"); let plan = BuildPlan::compile(spec).expect("compile build plan"); let mut executor = BuildExecutor::new(RootfslessInstaller, FixtureInitramfsBuilder); let workspace = fixture.path().join("work"); let artifact = fixture.path().join("image.tar"); let error = executor .execute(&plan, &workspace, "profiles/apt.conf", &artifact) .expect_err("missing rootfs must fail the build"); assert!( error .to_string() .contains("package installer did not create rootfs") ); assert!( !workspace.exists(), "failed build must not leave a workspace" ); assert!(!artifact.exists()); } #[test] fn removes_the_new_workspace_when_rootfs_finalization_fails() { let fixture = tempdir().expect("fixture directory"); let spec = ImageSpec::load(std::path::Path::new("profiles/alt-controller.toml")) .expect("load controller spec"); let plan = BuildPlan::compile(spec).expect("compile build plan"); let mut executor = BuildExecutor::new(RootfsFinalizationFailingInstaller, FixtureInitramfsBuilder); let workspace = fixture.path().join("work"); let artifact = fixture.path().join("image.tar"); let _error = executor .execute(&plan, &workspace, "profiles/apt.conf", &artifact) .expect_err("failed rootfs finalization must fail the build"); assert!( !workspace.exists(), "failed build must not leave a workspace" ); assert!(!artifact.exists()); } #[test] fn removes_the_new_workspace_when_initramfs_build_fails() { let fixture = tempdir().expect("fixture directory"); let spec = ImageSpec::load(std::path::Path::new("profiles/alt-controller.toml")) .expect("load controller spec"); let plan = BuildPlan::compile(spec).expect("compile build plan"); let mut executor = BuildExecutor::new(FixtureInstaller::default(), FailingInitramfsBuilder); let workspace = fixture.path().join("work"); let artifact = fixture.path().join("image.tar"); let error = executor .execute(&plan, &workspace, "profiles/apt.conf", &artifact) .expect_err("failed initramfs build must fail the build"); assert!( error .to_string() .contains("simulated initramfs build failure") ); assert!( !workspace.exists(), "failed build must not leave a workspace" ); assert!(!artifact.exists()); } #[test] fn removes_the_new_workspace_and_partial_artifact_when_packaging_fails() { let fixture = tempdir().expect("fixture directory"); let spec = ImageSpec::load(std::path::Path::new("profiles/alt-controller.toml")) .expect("load controller spec"); let plan = BuildPlan::compile(spec).expect("compile build plan"); let mut executor = BuildExecutor::new(ArchiveFailingInstaller, FixtureInitramfsBuilder); let workspace = fixture.path().join("work"); let artifact = fixture.path().join("out/image.tar"); let error = executor .execute(&plan, &workspace, "profiles/apt.conf", &artifact) .expect_err("unsupported rootfs entry must fail packaging"); assert!(error.to_string().contains("unsupported rootfs entry type")); assert!( !workspace.exists(), "failed build must not leave a workspace" ); assert!( !artifact.exists(), "failed packaging must not leave an artifact" ); assert!( !ArtifactManifest::path_beside(&artifact) .expect("manifest path") .exists(), "failed packaging must not leave a companion manifest" ); } #[test] fn removes_the_new_workspace_and_artifact_when_manifest_collection_fails() { let fixture = tempdir().expect("fixture directory"); let spec = ImageSpec::load(std::path::Path::new("profiles/alt-controller.toml")) .expect("load controller spec"); let plan = BuildPlan::compile(spec).expect("compile build plan"); let mut executor = BuildExecutor::new(FixtureInstaller::default(), MutatingInitrdBuilder); let workspace = fixture.path().join("work"); let artifact = fixture.path().join("out/image.tar"); let error = executor .execute(&plan, &workspace, "profiles/apt.conf", &artifact) .expect_err("mutated initrd must fail before packaging"); assert!( error .to_string() .contains("generated initrd digest changed") ); assert!( !workspace.exists(), "failed build must not leave a workspace" ); assert!( !artifact.exists(), "failed manifest collection must not leave an artifact" ); assert!( !ArtifactManifest::path_beside(&artifact) .expect("manifest path") .exists(), "failed manifest collection must not leave a companion manifest" ); }