#![allow(clippy::expect_used, clippy::panic)]
use std::path::{Path, PathBuf};
use prikk_object::{BlobPayload, BlockPayload, ObjectType, RecognitionClaimPayload, TagPayload};
use crate::container::decode_container_records;
use crate::fsutil::read_file_if_exists;
use crate::layout::{ContainerSlot, RepositoryLayout, persisted_object_types};
use crate::patch_replay::decode::decode_patch_operations;
struct DeclaredBreak {
version_pair: &'static str,
object_type: ObjectType,
reason: &'static str,
remedy: &'static str,
}
const DECLARED_BREAKS: &[DeclaredBreak] = &[DeclaredBreak {
version_pair: "0.22.1 -> 0.23.0",
object_type: ObjectType::Tag,
reason: "TagPayload gained two fields -- patch_set_digest and patch_count (RFC 117) -- added \
in place at schema_version 1, not as a new schema version. 0.23.0 reading a 0.22.1 tag \
fails with `Tag missing patch_set_digest`; 0.22.1 reading a 0.23.0 tag fails with \
`unknown Tag field tag: 6` (CHANGELOG.md, 0.23.0 \"Breaking change\").",
remedy: "A repository written by 0.22.1 that already holds a tag cannot be repaired under \
0.23.0 -- there is no `prikk tag delete`, and `prikk tag create` refuses when a tag \
ref of that name already exists. Keep using 0.22.1 for that repository, or start a \
fresh repository under 0.23.0. There is no in-place remediation today (CHANGELOG.md, \
0.23.0 \"Breaking change\").",
}];
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("prikk-store's manifest dir has a workspace root two levels up")
.to_path_buf()
}
fn last_release_fixture_root() -> PathBuf {
repo_root().join("crates/prikk-cli/tests/fixtures/rfc119_g1_0_24_0_repo")
}
fn check_type_decodes(
layout: &RepositoryLayout,
object_type: ObjectType,
) -> Result<Vec<u32>, String> {
let container_path = layout.container_slot_path(object_type, ContainerSlot::A);
let relative = layout
.repository_relative(&container_path)
.map_err(|err| err.to_string())?;
let Some(bytes) = read_file_if_exists(layout.repository_mutation_root(), &relative)
.map_err(|err| err.to_string())?
else {
return Ok(Vec::new());
};
let replay = decode_container_records(object_type, &bytes).map_err(|err| err.to_string())?;
let mut schema_versions = Vec::new();
for record in &replay.records {
let envelope = &record.envelope;
let decode_result: Result<(), prikk_error::PrikkError> = match object_type {
ObjectType::Block => {
BlockPayload::decode_canonical(&envelope.canonical_payload).map(|_| ())
}
ObjectType::Blob => {
BlobPayload::decode_canonical(&envelope.canonical_payload).map(|_| ())
}
ObjectType::Tag => {
TagPayload::decode_canonical(&envelope.canonical_payload).map(|_| ())
}
ObjectType::RefState => prikk_object::RefStatePayload::decode_canonical(
&envelope.canonical_payload,
envelope.schema_version,
)
.map(|_| ()),
ObjectType::RecognitionClaim => {
RecognitionClaimPayload::decode_canonical(&envelope.canonical_payload).map(|_| ())
}
ObjectType::Patch => {
decode_patch_operations(&envelope.canonical_payload, envelope.schema_version)
.map(|_| ())
}
other => {
panic!(
"G1 has no decode check wired up for persisted type {other} -- \
persisted_object_types() grew without this gate growing with it"
)
}
};
decode_result.map_err(|err| {
format!(
"object {} ({object_type}) failed to decode: {err}",
envelope.object_id()
)
})?;
schema_versions.push(envelope.schema_version);
}
Ok(schema_versions)
}
#[test]
fn last_release_fixture_exists() {
let root = last_release_fixture_root();
assert!(
root.join(".prikk").is_dir(),
"the last-release compatibility fixture is missing at {} -- see this module's doc for how \
it must be rebuilt (from the actual release tag's binary, never from current code)",
root.display()
);
}
#[test]
fn every_declared_break_names_a_persisted_object_type() {
for declared in DECLARED_BREAKS {
assert!(
persisted_object_types().contains(&declared.object_type),
"DECLARED_BREAKS entry for {} names {}, which is not a persisted object type",
declared.version_pair,
declared.object_type
);
assert!(
!declared.reason.trim().is_empty(),
"DECLARED_BREAKS entry for {} ({}) has no reason",
declared.version_pair,
declared.object_type
);
assert!(
!declared.remedy.trim().is_empty(),
"DECLARED_BREAKS entry for {} ({}) has no remedy",
declared.version_pair,
declared.object_type
);
}
}
#[test]
fn g1_last_release_fixture_is_compatible_or_the_break_is_declared() {
let root = last_release_fixture_root();
let layout = RepositoryLayout::open(&root).expect("last-release fixture repository opens");
for &object_type in &persisted_object_types() {
if let Err(message) = check_type_decodes(&layout, object_type) {
let declared = DECLARED_BREAKS
.iter()
.any(|break_| break_.object_type == object_type);
assert!(
declared,
"undeclared compatibility break: {message}, against the last-release fixture at \
{} -- if this break is authorized, add a DECLARED_BREAKS entry with a reason and a \
remedy; if not, this is a live defect",
root.display()
);
}
}
}
#[test]
fn last_release_fixture_coverage_matches_the_committed_counts() {
let root = last_release_fixture_root();
let layout = RepositoryLayout::open(&root).expect("last-release fixture repository opens");
let expected: &[(ObjectType, &[u32])] = &[
(ObjectType::Patch, &[2, 2]),
(ObjectType::Block, &[2]),
(ObjectType::Blob, &[1, 1]),
(ObjectType::RefState, &[1, 1]),
(ObjectType::Tag, &[1]),
(ObjectType::RecognitionClaim, &[1]),
(ObjectType::Attestation, &[]),
];
for &(object_type, expected_schemas) in expected {
let actual = check_type_decodes(&layout, object_type)
.unwrap_or_else(|err| panic!("{object_type} must decode cleanly here: {err}"));
assert_eq!(
actual, expected_schemas,
"{object_type}: expected schema versions {expected_schemas:?}, found {actual:?} -- \
update this deliberately if the fixture is ever legitimately rebuilt, never to paper \
over a coverage regression"
);
}
}