use std::{ffi::OsString, fs::OpenOptions};
use clap::Parser;
use midenup::{
channel::UserChannel, commands::Midenup, manifest::ComponentKind, miden_wrapper, version,
};
mod common;
use common::*;
#[test]
fn integration_install_stable() {
let _guard = common::harness::mutating_test_guard();
let test_env = environment_setup("integration_install_stable");
let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);
Midenup::try_parse_from(["midenup", "install", "stable"])
.unwrap()
.execute_with_state(&config, &mut state)
.expect("failed to install stable");
let state_file = test_env.midenup_home.join("state").with_extension("json");
assert!(state_file.exists(), "install must write local state");
let mainnet_dir = test_env.midenup_home.join("toolchains").join("mainnet");
assert!(mainnet_dir.exists());
assert!(mainnet_dir.is_symlink());
let mainnet_version = config
.upstream_manifest()
.unwrap()
.network_version("mainnet")
.expect("upstream must declare a mainnet network")
.clone();
assert_eq!(
std::fs::read_link(&mainnet_dir).unwrap().file_name().unwrap(),
std::ffi::OsStr::new(&mainnet_version.to_string()),
"the mainnet link must point at the channel upstream says mainnet names"
);
let reloaded = midenup::state::LocalState::load(&state_file).expect("state must reload");
assert!(reloaded.get(&mainnet_version).is_some());
}
#[test]
fn integration_install_places_every_artifact_kind() {
let _guard = common::harness::mutating_test_guard();
let test_env = environment_setup("integration_install_places_every_artifact_kind");
let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);
Midenup::try_parse_from(["midenup", "install", "stable", "--profile", "complete"])
.unwrap()
.execute_with_state(&config, &mut state)
.expect("failed to install stable");
let root = test_env.midenup_home.join("toolchains").join("mainnet");
assert!(
root.join("bin").join("miden-vm").exists(),
"executable -> bin/<installed-executable>"
);
assert!(root.join("lib").join("core.masp").exists(), "package -> lib/<artifact-id>");
assert!(
root.join("etc").join("assets").join("config.yml").exists(),
"asset -> etc/<component>/<artifact-id>"
);
assert_eq!(std::fs::read(root.join("lib").join("core.masp")).unwrap(), b"fixture-package");
}
#[test]
fn integration_install_creates_default_symlinks() {
let _guard = common::harness::mutating_test_guard();
let test_env = environment_setup("integration_install_creates_default_symlinks");
let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);
Midenup::try_parse_from(["midenup", "install", "stable"])
.unwrap()
.execute_with_state(&config, &mut state)
.expect("failed to install stable");
let opt = test_env.midenup_home.join("toolchains").join("mainnet").join("opt");
assert!(
opt.join("miden vm").symlink_metadata().is_ok(),
"missing default shim for 'vm' in {}",
opt.display()
);
}
#[test]
fn integration_install_unpacks_archived_artifacts() {
let _guard = common::harness::mutating_test_guard();
let test_env = environment_setup("integration_install_unpacks_archived_artifacts");
let fixture = common::harness::OfflineFixture::new(test_env.tmp_dir.path())
.with_channel("0.15.0")
.with_archived_component()
.build();
let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);
Midenup::try_parse_from(["midenup", "install", "0.15.0"])
.unwrap()
.execute_with_state(&config, &mut state)
.expect("failed to install");
let lib = test_env.midenup_home.join("toolchains").join("0.15.0").join("lib");
assert_eq!(
std::fs::read(lib.join("archived.masp")).unwrap(),
b"archived-package",
"the archived file must land under its artifact id"
);
let installed: Vec<String> = std::fs::read_dir(&lib)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
assert!(
!installed
.iter()
.any(|name| name.contains("packages-1.0") || name.ends_with(".tar.gz")),
"neither the container nor its directory may be installed: {installed:?}"
);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
std::fs::metadata(lib.join("archived.masp")).unwrap().permissions().mode() & 0o777,
0o644,
"the mode is the planned one for a package, not the archive entry's"
);
}
let midenup::state::PublicationRef::Managed { id, .. } = &state
.get(&semver::Version::new(0, 15, 0))
.expect("the install must be recorded")
.publication
else {
panic!("a fresh install must produce a managed publication");
};
let publication = midenup::paths::publication_dir(
&test_env.midenup_home,
&semver::Version::new(0, 15, 0),
id,
);
let receipt = midenup::publish::read_receipt(&publication).expect("a receipt must be written");
assert!(
receipt
.outputs
.iter()
.any(|o| o.path == std::path::Path::new("lib/archived.masp")
&& o.owner == "packages"
&& o.realized == midenup::state::RealizedMethod::Prebuilt),
"an archived artifact is recorded like any other prebuilt one: {:?}",
receipt.outputs
);
}
#[test]
fn integration_install_publishes_into_an_opaque_publication_with_a_receipt() {
let _guard = common::harness::mutating_test_guard();
let test_env = environment_setup("integration_install_publishes");
let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);
Midenup::try_parse_from(["midenup", "install", "0.15.0"])
.unwrap()
.execute_with_state(&config, &mut state)
.expect("failed to install");
let channel = semver::Version::new(0, 15, 0);
let installed = state.get(&channel).expect("the install must be recorded");
let midenup::state::PublicationRef::Managed { id, plan_key, .. } = &installed.publication
else {
panic!("a fresh install must produce a managed publication");
};
let publication = midenup::paths::publication_dir(&test_env.midenup_home, &channel, id);
assert!(publication.is_dir(), "{} must exist", publication.display());
assert!(
!publication.to_string_lossy().contains(&plan_key.to_string()[4..12]),
"the publication must not be named after the plan key"
);
let link = test_env.midenup_home.join("toolchains").join("0.15.0");
assert_eq!(
std::fs::canonicalize(&link).unwrap(),
std::fs::canonicalize(&publication).unwrap()
);
let receipt = midenup::publish::read_receipt(&publication).expect("a receipt must be written");
assert_eq!(receipt.publication_id, *id);
assert_eq!(&receipt.plan_key, plan_key);
assert!(
receipt.outputs.iter().any(|o| o.path == std::path::Path::new("bin/miden-vm")
&& o.owner == "vm"
&& o.realized == midenup::state::RealizedMethod::Prebuilt),
"the receipt must record every installed file and how it was obtained: {:?}",
receipt.outputs
);
}
#[test]
fn integration_install_republishes_rather_than_mutating() {
let _guard = common::harness::mutating_test_guard();
let test_env = environment_setup("integration_install_republishes");
let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);
let channel = semver::Version::new(0, 15, 0);
let publication_of =
|state: &LocalManifest| match &state.get(&channel).expect("installed").publication {
midenup::state::PublicationRef::Managed { id, .. } => {
midenup::paths::publication_dir(&test_env.midenup_home, &channel, id)
},
other => panic!("expected a managed publication, got {other:?}"),
};
Midenup::try_parse_from(["midenup", "install", "0.15.0"])
.unwrap()
.execute_with_state(&config, &mut state)
.expect("failed to install");
let first = publication_of(&state);
Midenup::try_parse_from(["midenup", "install", "0.15.0", "--profile", "complete"])
.unwrap()
.execute_with_state(&config, &mut state)
.expect("failed to reinstall");
let second = publication_of(&state);
assert_ne!(first, second, "a changed installed set must produce a new publication");
assert!(
first.is_dir(),
"the publication it replaced must be left intact for gc, not deleted underneath whatever \
may still be running from it"
);
assert!(
second.join("lib").join("core.masp").exists(),
"unchanged files must be seeded from the previous publication"
);
assert!(
second.join("etc").join("assets").join("config.yml").exists(),
"the added component must be installed"
);
}
#[test]
fn integration_var_survives_update_and_republication() {
let _guard = common::harness::mutating_test_guard();
let test_env = environment_setup("integration_var_survives");
let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);
let channel = semver::Version::new(0, 15, 0);
Midenup::try_parse_from(["midenup", "install", "0.15.0"])
.unwrap()
.execute_with_state(&config, &mut state)
.expect("failed to install");
let var =
midenup::paths::var_dir(&test_env.midenup_home, &UserChannel::Version(channel.clone()));
std::fs::create_dir_all(&var).unwrap();
std::fs::write(var.join("data"), b"user-database").unwrap();
Midenup::try_parse_from(["midenup", "install", "0.15.0", "--profile", "complete"])
.unwrap()
.execute_with_state(&config, &mut state)
.expect("failed to republish");
assert_eq!(
std::fs::read(var.join("data")).unwrap(),
b"user-database",
"user data must survive republication"
);
let midenup::state::PublicationRef::Managed { id, .. } =
&state.get(&channel).unwrap().publication
else {
panic!("expected a managed publication");
};
let publication = midenup::paths::publication_dir(&test_env.midenup_home, &channel, id);
assert!(!publication.join("var").exists(), "no publication may contain var/");
}
#[test]
fn integration_install_falls_back_to_cargo_when_an_artifact_is_unavailable() {
let _guard = common::harness::mutating_test_guard();
let test_env = environment_setup("integration_install_fallback");
let sources = common::harness::SourceFixture::build(test_env.tmp_dir.path());
let manifest = serde_json::json!({
"manifest_version": "3.0.0",
"date": 1735689600,
"channels": [{
"name": "0.15.0",
"components": [{
"name": "vm",
"version": {"kind": "path", "path": sources.path_crate.to_str().unwrap()},
"kind": "executable",
"installation_method": {
"kind": "prebuilt-with-cargo-fallback",
"crate_name": "fixture-vm"
},
"installed-executable": "miden-vm",
"profiles": ["minimal"],
"artifacts": {"miden-vm": {"uri": "file:///nonexistent/miden-vm"}}
}]
}]
});
let manifest_path = test_env.tmp_dir.path().join("fallback-manifest.json");
std::fs::write(&manifest_path, serde_json::to_string_pretty(&manifest).unwrap()).unwrap();
let (mut state, config) = test_setup(&test_env, &format!("file://{}", manifest_path.display()));
Midenup::try_parse_from(["midenup", "install", "0.15.0"])
.unwrap()
.execute_with_state(&config, &mut state)
.expect("a failed transfer with a declared fallback must not fail the install");
let channel = semver::Version::new(0, 15, 0);
let midenup::state::PublicationRef::Managed { id, .. } =
&state.get(&channel).expect("installed").publication
else {
panic!("expected a managed publication");
};
let publication = midenup::paths::publication_dir(&test_env.midenup_home, &channel, id);
assert!(
publication.join("bin").join("miden-vm").exists(),
"the fallback must install it"
);
let receipt = midenup::publish::read_receipt(&publication).unwrap();
let vm = receipt
.outputs
.iter()
.find(|o| o.owner == "vm")
.expect("the receipt must record the binary");
assert_eq!(
vm.realized,
midenup::state::RealizedMethod::Cargo,
"the receipt must record the method actually taken, not the one declared"
);
}
#[test]
fn integration_install_from_non_cargo() {
let _guard = common::harness::mutating_test_guard();
let test_env = environment_setup("integration_install_from_non_cargo");
let fixture = common::harness::SourceFixture::build(test_env.tmp_dir.path());
let manifest_dir = test_env.tmp_dir.path();
let recorded = |state: &LocalManifest| {
let named = std::fs::read_link(test_env.midenup_home.join("toolchains").join("mainnet"))
.expect("the mainnet symlink must exist");
let version = semver::Version::parse(named.file_name().unwrap().to_str().unwrap()).unwrap();
let channel = state
.get(&version)
.expect("state must record the channel mainnet names")
.as_channel();
let last_modification = match channel.get_component("vm").unwrap().version {
version::Authority::Path { last_modification, .. } => last_modification
.expect("a path authority must record the tree's modification time"),
ref authority => panic!("expected 'vm' to have a path authority, got {authority}"),
};
let revision = match &channel.get_component("client").unwrap().version {
version::Authority::Git {
target: version::GitTarget::Revision { hash },
..
} => hash.clone(),
authority => panic!("expected 'client' to have a git authority, got {authority}"),
};
(last_modification, revision)
};
let first = common::harness::write_source_manifest(
manifest_dir,
"manifest-1.json",
&fixture,
&fixture.revisions[0],
);
let (mut state, config) = test_setup(&test_env, &first);
Midenup::try_parse_from(["midenup", "install", "stable"])
.unwrap()
.execute_with_state(&config, &mut state)
.expect("failed to install stable");
let (time_when_installed, hash_when_installed) = recorded(&state);
assert_eq!(
hash_when_installed, fixture.revisions[0],
"the installed revision must be the one the manifest named"
);
Midenup::try_parse_from(["midenup", "update"])
.unwrap()
.execute_with_state(&config, &mut state)
.expect("failed to update");
let (unchanged_time, unchanged_revision) = recorded(&state);
assert_eq!(unchanged_time, time_when_installed, "an unchanged path must not be reinstalled");
assert_eq!(
unchanged_revision, hash_when_installed,
"an unchanged revision must not be reinstalled"
);
let second = common::harness::write_source_manifest(
manifest_dir,
"manifest-2.json",
&fixture,
&fixture.revisions[1],
);
let (_, config) = test_setup(&test_env, &second);
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(fixture.path_crate.join("trigger-update"))
.unwrap();
Midenup::try_parse_from(["midenup", "update", "--path-update=all"])
.unwrap()
.execute_with_state(&config, &mut state)
.expect("failed to update");
let (new_time, new_revision) = recorded(&state);
assert!(new_time > time_when_installed, "a touched path source must be reinstalled");
assert_eq!(
new_revision, fixture.revisions[1],
"a changed revision must be reinstalled at the new commit"
);
}
#[test]
fn integration_prerelease_components_are_runnable() {
let _guard = common::harness::mutating_test_guard();
let test_name = "integration_test_components";
let test_env = environment_setup(test_name);
const FILE: &str = full_path_manifest!("manifest/channel-manifest.json");
let (mut local_manifest, config) = test_setup(&test_env, FILE);
let command =
Midenup::try_parse_from(["midenup", "install", "stable", "--profile", "complete"]).unwrap();
command
.execute_with_state(&config, &mut local_manifest)
.expect("failed to install stable");
let named = std::fs::read_link(test_env.midenup_home.join("toolchains").join("mainnet"))
.expect("the mainnet symlink must exist");
let version = semver::Version::parse(named.file_name().unwrap().to_str().unwrap()).unwrap();
let stable_channel = local_manifest
.get(&version)
.expect("state must record the channel mainnet names")
.as_channel();
println!("Installed: {}", stable_channel);
for component in &stable_channel.components {
match component.kind() {
ComponentKind::Executable { installation_method, spec }
| ComponentKind::CargoExtension { installation_method, spec }
if !spec.is_hidden() =>
{
let argv: Vec<OsString> =
vec!["miden".into(), "help".into(), component.name.as_ref().into()];
miden_wrapper::miden_wrapper(&argv, &config, &mut local_manifest).unwrap_or_else(
|err| {
panic!(
"Component '{}' is not runnable through the 'miden' interface: {}",
component.name, err
)
},
);
},
ComponentKind::Executable { .. } | ComponentKind::CargoExtension { .. } => (),
ComponentKind::Asset
| ComponentKind::Command { .. }
| ComponentKind::Package
| ComponentKind::LegacyPackage { .. } => (),
ComponentKind::Unsupported { tag, .. } => {
panic!("component '{}' has unsupported kind '{tag}'", component.name)
},
}
}
}
#[test]
fn integration_a_path_source_that_moves_during_the_build_is_refused() {
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
let _guard = common::harness::mutating_test_guard();
let test_env = environment_setup("integration_path_moved");
let sources = common::harness::SourceFixture::build(test_env.tmp_dir.path());
let manifest = serde_json::json!({
"manifest_version": "3.0.0",
"date": 1735689600,
"channels": [{
"name": "0.15.0",
"components": [{
"name": "vm",
"version": {"kind": "path", "path": sources.path_crate.to_str().unwrap()},
"kind": "executable",
"installation_method": {"kind": "cargo", "crate_name": "fixture-vm"},
"installed-executable": "miden-vm",
"profiles": ["minimal"]
}]
}]
});
let manifest_path = test_env.tmp_dir.path().join("moving-source.json");
std::fs::write(&manifest_path, serde_json::to_string_pretty(&manifest).unwrap()).unwrap();
let (mut state, config) = test_setup(&test_env, &format!("file://{}", manifest_path.display()));
let editing = Arc::new(AtomicBool::new(true));
let editor = {
let editing = Arc::clone(&editing);
let file = sources.path_crate.join("edited-during-the-build");
std::thread::spawn(move || {
while editing.load(Ordering::Relaxed) {
let _ = std::fs::write(&file, format!("{:?}", std::time::SystemTime::now()));
std::thread::sleep(std::time::Duration::from_millis(25));
}
})
};
let result = Midenup::try_parse_from(["midenup", "install", "0.15.0"])
.unwrap()
.execute_with_state(&config, &mut state);
editing.store(false, Ordering::Relaxed);
editor.join().expect("the editing thread must not panic");
let err = result.expect_err("a source that moved during the build must not be published");
let message = format!("{err:#}");
assert!(
message.contains("changed") || message.contains("retry"),
"the diagnostic must say what happened: {message}"
);
assert!(
state.get(&semver::Version::new(0, 15, 0)).is_none(),
"and nothing may be recorded as installed"
);
}
#[test]
fn integration_install_after_a_patch_does_not_keep_the_patched_build() {
assert_unpatched_by("integration_install_after_patch", &["midenup", "install", "0.15.0"]);
}
#[test]
fn integration_update_after_a_patch_does_not_keep_the_patched_build() {
assert_unpatched_by(
"integration_update_after_patch",
&["midenup", "update", "--path-update=off", "0.15.0"],
);
}
fn assert_unpatched_by(test_name: &str, args: &[&str]) {
let _guard = common::harness::mutating_test_guard();
let test_env = environment_setup(test_name);
let fixture = common::harness::OfflineFixture::create(test_env.tmp_dir.path(), "0.15.0");
let sources = common::harness::SourceFixture::build(test_env.tmp_dir.path());
let (mut state, config) = test_setup(&test_env, &fixture.manifest_uri);
let channel = semver::Version::new(0, 15, 0);
let vm = midenup::paths::toolchain_link(&test_env.midenup_home, &channel)
.join("bin")
.join("miden-vm");
let upstream_vm = std::fs::read(fixture.dir.join("0.15.0").join("miden-vm")).unwrap();
let project = test_env.tmp_dir.path().join("patched-project");
std::fs::create_dir_all(&project).unwrap();
std::fs::write(
project.join("miden-toolchain.toml"),
format!(
"[toolchain]\nchannel = \"0.15.0\"\ncomponents = [\"vm\"]\n\n[patches.vm]\ncrate_name \
= \"fixture-vm\"\nversion = {{ kind = \"path\", path = \"{}\" }}\n",
sources.path_crate.display()
),
)
.unwrap();
let project_config = midenup::config::Config::init(
project,
test_env.midenup_home.clone(),
test_env.cargo_home.clone(),
&fixture.manifest_uri,
true,
)
.unwrap();
Midenup::try_parse_from(["miden", "help", "vm"])
.unwrap()
.execute_with_state(&project_config, &mut state)
.expect("failed to activate the patched toolchain");
assert!(std::fs::read(&vm).unwrap() != upstream_vm, "the patch must be installed");
Midenup::try_parse_from(args)
.unwrap()
.execute_with_state(&config, &mut state)
.unwrap_or_else(|err| panic!("{args:?} failed: {err:#}"));
let installation = state.get(&channel).unwrap();
assert!(installation.patches.is_empty());
let recorded_vm = installation.components.iter().find(|c| c.name == "vm").unwrap();
assert!(
matches!(recorded_vm.version, version::Authority::Registry { .. }),
"the upstream vm must be recorded, not the patch: {:?}",
recorded_vm.version
);
assert!(
std::fs::read(&vm).unwrap() == upstream_vm,
"the patched build must not be carried forward"
);
}