Round-digest lows 2: length-framed source digest, lock cleanup, honest drift text, window tests #35
3 changed files with 120 additions and 17 deletions
|
|
@ -40,8 +40,10 @@ use crate::store::{CorpusStore, EntityStore, InventoryStore, PacketStore, open_c
|
|||
|
||||
/// The digest of the prompt builder's source: `context.rs`, then `split.rs`,
|
||||
/// whose scheme and digest function decide which lines the prompt holds as
|
||||
/// Train and name every prompt. Any edit to either, comments included, ends a
|
||||
/// run; code outside those files is not covered.
|
||||
/// Train and name every prompt. Each file enters the digest behind its length,
|
||||
/// so the pair cannot be re-cut into another pair with the same digest. Any
|
||||
/// edit to either, comments included, ends a run; code outside those files is
|
||||
/// not covered.
|
||||
pub const PROMPT_SOURCE_DIGEST: u64 =
|
||||
fnv1a64_of(&[include_bytes!("context.rs"), include_bytes!("split.rs")]);
|
||||
|
||||
|
|
@ -349,6 +351,9 @@ impl fmt::Display for RunError {
|
|||
RunError::Exists => f.write_str("the root already holds a run"),
|
||||
RunError::NoRun => f.write_str("the root holds no run"),
|
||||
RunError::Malformed(what) => write!(f, "malformed {what}"),
|
||||
RunError::Drift(DriftOf::Prompts) => f.write_str(
|
||||
"drift in Prompts (the prompt source digest covers context.rs and split.rs)",
|
||||
),
|
||||
RunError::Drift(of) => write!(f, "drift in {of:?}"),
|
||||
RunError::NoInventory => f.write_str("the inventory holds no revision"),
|
||||
RunError::Io(kind) => write!(f, "io error {kind:?}"),
|
||||
|
|
@ -517,7 +522,7 @@ impl RunStores {
|
|||
let head = root.join(INVENTORY_FILE);
|
||||
copy_new(Path::new(&record.inventory), &head)?;
|
||||
// The copy is ours from here: any later failure takes it away again, or
|
||||
// it would refuse every retry.
|
||||
// it would refuse every retry, and so with the lock the open left beside it.
|
||||
let pinned = (|| {
|
||||
let record = RunRecord {
|
||||
corpus_digest: file_digest(&record.corpus).ok_or(RunError::Store)?,
|
||||
|
|
@ -537,7 +542,7 @@ impl RunStores {
|
|||
Ok(run)
|
||||
})();
|
||||
if pinned.is_err() {
|
||||
let _ = std::fs::remove_file(&head);
|
||||
discard_inventory(root);
|
||||
}
|
||||
pinned
|
||||
}
|
||||
|
|
@ -1803,6 +1808,14 @@ mod tests {
|
|||
"write {fail_at} refused"
|
||||
);
|
||||
assert_eq!(out.is_ok(), fail_at == 2);
|
||||
if fail_at == 2 {
|
||||
// The fixture accepts: a drifted literal that made it reject
|
||||
// would write one round and one decision all the same.
|
||||
match &out {
|
||||
Ok(CycleOutcome::Decided(d)) => assert_eq!(d.verdict(), Verdict::Accepted),
|
||||
_ => panic!("the second write was not a decided cycle"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,26 +25,32 @@ pub enum SplitScheme {
|
|||
}
|
||||
|
||||
pub const fn fnv1a64(bytes: &[u8]) -> u64 {
|
||||
fnv1a64_of(&[bytes])
|
||||
fnv1a64_fold(0xcbf2_9ce4_8422_2325, bytes)
|
||||
}
|
||||
|
||||
/// FNV-1a 64 of the parts laid end to end.
|
||||
/// FNV-1a 64 of the parts, each preceded by its length as eight little-endian
|
||||
/// bytes, so no two different splits of the same bytes share a digest.
|
||||
pub const fn fnv1a64_of(parts: &[&[u8]]) -> u64 {
|
||||
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
let mut p = 0;
|
||||
while p < parts.len() {
|
||||
let bytes = parts[p];
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
h ^= bytes[i] as u64;
|
||||
h = h.wrapping_mul(0x0000_0100_0000_01b3);
|
||||
i += 1;
|
||||
}
|
||||
h = fnv1a64_fold(h, &(parts[p].len() as u64).to_le_bytes());
|
||||
h = fnv1a64_fold(h, parts[p]);
|
||||
p += 1;
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
const fn fnv1a64_fold(mut h: u64, bytes: &[u8]) -> u64 {
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
h ^= bytes[i] as u64;
|
||||
h = h.wrapping_mul(0x0000_0100_0000_01b3);
|
||||
i += 1;
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
/// 16 lowercase hex digits, zero-padded: how a digest is written everywhere.
|
||||
pub(crate) fn hex16(n: u64) -> String {
|
||||
format!("{n:016x}")
|
||||
|
|
|
|||
|
|
@ -370,8 +370,8 @@ fn a_store_changed_between_its_digest_and_its_open_is_drift() {
|
|||
Some(RunError::Drift(DriftOf::Corpus))
|
||||
);
|
||||
|
||||
// The same window at create: nothing is pinned and nothing is left behind,
|
||||
// so the retry creates.
|
||||
// The same window at create: nothing is pinned and the inventory copy and its
|
||||
// lock are taken away again, so the retry creates.
|
||||
let mut w = world();
|
||||
let dir = w.dir.path().to_path_buf();
|
||||
let root = dir.join("run");
|
||||
|
|
@ -389,21 +389,101 @@ fn a_store_changed_between_its_digest_and_its_open_is_drift() {
|
|||
Some(RunError::Drift(DriftOf::Corpus))
|
||||
);
|
||||
assert!(!root.join("inventory.cc").exists());
|
||||
// Not the copy and not the lock the store's open left beside it.
|
||||
assert!(!root.join("inventory.cc.lock").exists());
|
||||
assert_eq!(RunStores::open(&root).err(), Some(RunError::NoRun));
|
||||
RunStores::create(&root, "t", paths(), "fake-model", "bag").unwrap();
|
||||
}
|
||||
|
||||
/// Runs `change` in the window between the pins and the open, at `open` and at
|
||||
/// `create`, and returns what each refused with plus what `create` left behind.
|
||||
fn window(
|
||||
change: impl Fn(&mut common::World, &Path),
|
||||
) -> (Option<RunError>, Option<RunError>, bool, Option<RunError>) {
|
||||
let mut w = world();
|
||||
let dir = w.dir.path().to_path_buf();
|
||||
let root = dir.join("run");
|
||||
run_over(&dir);
|
||||
let opened = RunStores::open_between(&root, || change(&mut w, &root)).err();
|
||||
|
||||
let mut w = world();
|
||||
let dir = w.dir.path().to_path_buf();
|
||||
let root = dir.join("run");
|
||||
let (c, i, e) = (dir.join("c.cc"), dir.join("i.cc"), dir.join("entities.cc"));
|
||||
let paths = weksa::run::RunPaths {
|
||||
corpus: &c,
|
||||
inventory: &i,
|
||||
entities: &e,
|
||||
};
|
||||
let created = RunStores::create_between(&root, "t", paths, "fake-model", "bag", || {
|
||||
change(&mut w, &root)
|
||||
})
|
||||
.err();
|
||||
let left = root.join("inventory.cc").exists() || root.join("inventory.cc.lock").exists();
|
||||
(opened, created, left, RunStores::open(&root).err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_entity_store_changed_between_its_digest_and_its_open_is_drift() {
|
||||
let (opened, created, left, after) = window(|w, _| {
|
||||
w.entities.put(ent("kay", "en-US", &["Kay"])).unwrap();
|
||||
});
|
||||
assert_eq!(opened, Some(RunError::Drift(DriftOf::Entities)));
|
||||
assert_eq!(created, Some(RunError::Drift(DriftOf::Entities)));
|
||||
assert!(!left);
|
||||
assert_eq!(after, Some(RunError::NoRun));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_head_inventory_changed_between_its_digest_and_its_open_is_drift() {
|
||||
let (opened, created, left, after) = window(|_, root| {
|
||||
weksa::InventoryStore::open(&root.join("inventory.cc"))
|
||||
.unwrap()
|
||||
.admit_revision(
|
||||
weksa::InventoryRevision {
|
||||
n: 5,
|
||||
parent: Some(4),
|
||||
changes: vec![],
|
||||
frozen: false,
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(opened, Some(RunError::Drift(DriftOf::Inventory)));
|
||||
assert_eq!(created, Some(RunError::Drift(DriftOf::Inventory)));
|
||||
assert!(!left);
|
||||
assert_eq!(after, Some(RunError::NoRun));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_digests_cover_their_files() {
|
||||
let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let digest = |name: &str| fnv1a64(&std::fs::read(src.join(name)).unwrap());
|
||||
// The prompt's partition and names come from split.rs: it is covered too.
|
||||
let both = [
|
||||
let (context, split) = (
|
||||
std::fs::read(src.join("context.rs")).unwrap(),
|
||||
std::fs::read(src.join("split.rs")).unwrap(),
|
||||
);
|
||||
// Each file enters behind its length, so the pair has one framing.
|
||||
let framed = [
|
||||
(context.len() as u64).to_le_bytes().as_slice(),
|
||||
&context,
|
||||
(split.len() as u64).to_le_bytes().as_slice(),
|
||||
&split,
|
||||
]
|
||||
.concat();
|
||||
assert_eq!(weksa::run::PROMPT_SOURCE_DIGEST, fnv1a64(&both));
|
||||
assert_eq!(weksa::run::PROMPT_SOURCE_DIGEST, fnv1a64(&framed));
|
||||
assert_ne!(
|
||||
weksa::run::PROMPT_SOURCE_DIGEST,
|
||||
fnv1a64(&[context.clone(), split.clone()].concat()),
|
||||
"bare concatenation is not the digest"
|
||||
);
|
||||
// Moving the boundary between the parts moves the digest.
|
||||
assert_ne!(
|
||||
weksa::split::fnv1a64_of(&[b"ab", b"c"]),
|
||||
weksa::split::fnv1a64_of(&[b"a", b"bc"])
|
||||
);
|
||||
assert_ne!(weksa::run::PROMPT_SOURCE_DIGEST, digest("context.rs"));
|
||||
assert_eq!(weksa::run::LOWERER_SOURCE_DIGEST, digest("lower.rs"));
|
||||
assert_ne!(
|
||||
|
|
@ -1387,6 +1467,10 @@ fn errors_echo_no_input() {
|
|||
format!("{:?}", RunError::Drift(DriftOf::Model)),
|
||||
"drift in Model"
|
||||
);
|
||||
// The Prompts text names the two files its digest covers, and only them.
|
||||
let prompts = RunError::Drift(DriftOf::Prompts).to_string();
|
||||
assert!(prompts.contains("context.rs") && prompts.contains("split.rs"));
|
||||
assert_eq!(format!("{:?}", RunError::Drift(DriftOf::Prompts)), prompts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue