use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
mod format_transition_support;
use format_transition_support::{
ActiveFixture, MAINTAINER_KEY_ID, MAINTAINER_SEED_HEX, StrictFailure,
build_current_format_strict_wal_fixture, build_legacy_fixture,
};
type TestResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
fn prikk(root: &Path) -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_prikk"));
command.current_dir(root);
command
}
fn run(root: &Path, args: &[&str]) -> TestResult<Output> {
Ok(prikk(root).args(args).output()?)
}
fn unique_root() -> TestResult<PathBuf> {
static SEQUENCE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_nanos();
let sequence = SEQUENCE.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let root = std::env::temp_dir().join(format!(
"prikk-format-transition-{}-{nonce}-{sequence}",
std::process::id()
));
std::fs::create_dir_all(&root)?;
Ok(root)
}
fn run_owned(root: &Path, args: &[String]) -> TestResult<Output> {
Ok(prikk(root)
.env("PRIKK_AUTHOR_KEY_ID", "legacy-author")
.env(
"PRIKK_AUTHOR_SEED",
"3636363636363636363636363636363636363636363636363636363636363636",
)
.env("PRIKK_MAINTAINER_KEY_ID", MAINTAINER_KEY_ID)
.env("PRIKK_MAINTAINER_SEED", MAINTAINER_SEED_HEX)
.args(args)
.output()?)
}
fn snapshot_tree(root: &Path) -> TestResult<BTreeMap<PathBuf, Vec<u8>>> {
fn walk(root: &Path, current: &Path, snapshot: &mut BTreeMap<PathBuf, Vec<u8>>) -> TestResult {
let mut entries = std::fs::read_dir(current)?.collect::<Result<Vec<_>, _>>()?;
entries.sort_by_key(std::fs::DirEntry::file_name);
for entry in entries {
let path = entry.path();
let relative = path.strip_prefix(root)?.to_path_buf();
let metadata = std::fs::symlink_metadata(&path)?;
if metadata.is_dir() {
snapshot.insert(relative.clone(), b"directory".to_vec());
walk(root, &path, snapshot)?;
} else if metadata.is_file() {
snapshot.insert(relative, std::fs::read(path)?);
} else {
snapshot.insert(
relative,
std::fs::read_link(path)?
.as_os_str()
.as_encoded_bytes()
.to_vec(),
);
}
}
Ok(())
}
let mut snapshot = BTreeMap::new();
walk(root, root, &mut snapshot)?;
Ok(snapshot)
}
fn assert_rejection_contract(
args: &[&str],
output: &Output,
detected_format: &str,
version_claim: Option<&str>,
no_migration_claim: &str,
) {
assert!(
!output.status.success(),
"{args:?} unexpectedly succeeded: stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
for expected in [detected_format, "requires format 6", no_migration_claim]
.into_iter()
.chain(version_claim)
{
assert!(
stderr.contains(expected),
"{args:?}: rejection message missing {expected:?}: {stderr}"
);
}
for unexpected in ["bundle export", "bundle import"] {
assert!(
!stderr.contains(unexpected),
"{args:?}: rejection message offers a migration step ({unexpected:?}) RFC 114 §5.3 \
says must not be offered: {stderr}"
);
}
}
#[test]
fn retired_format_repository_is_rejected_at_open_for_every_command() -> TestResult {
for (target_format, detected_format, version_claim, no_migration_claim) in [
(
b"1\n".as_slice(),
"this repository uses format 1",
Some("removed after 0.19.0"),
"migration from format 1 is not supported",
),
(
b"2\n".as_slice(),
"this repository uses format 2",
Some("removed after 0.19.0"),
"migration from format 2 is not supported",
),
(
b"3\n".as_slice(),
"this repository uses format 3",
None,
"there is no supported migration path",
),
(
b"4\n".as_slice(),
"this repository uses format 4",
None,
"there is no supported migration path",
),
(
b"5\n".as_slice(),
"this repository uses format 5",
None,
"there is no supported migration path",
),
] {
for active in [
ActiveFixture::RollbackDraft,
ActiveFixture::InterruptedPublication,
] {
let root = unique_root()?;
build_legacy_fixture(&root, active, target_format)?;
let before = snapshot_tree(&root)?;
for args in [
vec!["status"],
vec!["log"],
vec!["worktree-status"],
vec!["verify"],
vec!["doctor"],
vec!["checkout", "--plan-only"],
vec!["rollback-preview"],
vec!["commit", "-m", "must refuse"],
vec!["seal", "--allow-no-audit"],
vec![
"trust",
"maintainer",
"add",
"--key-id",
"legacy-refused",
"--public-key",
"0000000000000000000000000000000000000000000000000000000000000000",
],
] {
let owned_args = args.iter().map(ToString::to_string).collect::<Vec<_>>();
let output = run_owned(&root, &owned_args)?;
assert_rejection_contract(
&args,
&output,
detected_format,
version_claim,
no_migration_claim,
);
assert_eq!(
snapshot_tree(&root)?,
before,
"{args:?} must not mutate a rejected repository"
);
}
let _ = std::fs::remove_dir_all(root);
}
}
Ok(())
}
#[test]
fn reinit_over_a_format1_repository_refuses_and_preserves_it() -> TestResult {
let root = unique_root()?;
build_legacy_fixture(&root, ActiveFixture::InterruptedPublication, b"1\n")?;
let before = snapshot_tree(&root)?;
let reinit = run(&root, &["init"])?;
assert!(!reinit.status.success());
assert_eq!(std::fs::read(root.join(".prikk/FORMAT"))?, b"1\n");
assert_eq!(snapshot_tree(&root)?, before);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[path = "format_transition/matrix.rs"]
mod matrix;