use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use anyhow::{Result, bail};
use crate::archive_write::{ArchiveWrite, FastWriter, SafeWriter};
use crate::gc::{CompactInPlace, Gc, NewGeneration};
pub fn env_reads() -> u64 {
ENV_READS.load(Ordering::Relaxed)
}
pub fn env_reads_here() -> u64 {
ENV_READS_HERE.with(|c| c.get())
}
static ENV_READS: AtomicU64 = AtomicU64::new(0);
thread_local! {
static ENV_READS_HERE: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
pub(crate) fn read_env(key: &str) -> Option<String> {
ENV_READS.fetch_add(1, Ordering::Relaxed);
ENV_READS_HERE.with(|c| c.set(c.get() + 1));
match std::env::var(key) {
Ok(v) if !v.trim().is_empty() => Some(v.trim().to_string()),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WriterArm {
Fast,
#[default]
Safe,
Uring,
}
impl WriterArm {
pub const ALL: [WriterArm; 3] = [WriterArm::Fast, WriterArm::Safe, WriterArm::Uring];
pub fn as_str(self) -> &'static str {
match self {
WriterArm::Fast => "fast",
WriterArm::Safe => "safe",
WriterArm::Uring => "uring",
}
}
pub fn durability(self) -> &'static str {
match self {
WriterArm::Fast => {
"none — page cache only; a machine crash after return loses the bytes, and there \
is no journal, so no crash recovery and no stable pack ordinals"
}
WriterArm::Safe | WriterArm::Uring => {
"full — blob fsynced, then a journal row fsynced; crash after return keeps both"
}
}
}
pub fn journal(self, blobs: &Path) -> Option<PathBuf> {
match self {
WriterArm::Fast => None,
WriterArm::Safe | WriterArm::Uring => Some(crate::archive_write::journal_path(blobs)),
}
}
pub fn create(self, blobs: &Path) -> Result<Box<dyn ArchiveWrite>> {
Ok(match self {
WriterArm::Fast => Box::new(FastWriter::create(blobs)?),
WriterArm::Safe => Box::new(SafeWriter::create(blobs)?),
#[cfg(target_os = "linux")]
WriterArm::Uring => Box::new(crate::uring_write::UringWriter::create(blobs)?),
#[cfg(not(target_os = "linux"))]
WriterArm::Uring => bail!(
"the `uring` writer arm is Linux-only — this target has no io_uring, and this \
crate does not substitute a pwrite path under an io_uring name"
),
})
}
pub fn parse(s: &str) -> Result<Self> {
Ok(match s.trim().to_ascii_lowercase().as_str() {
"fast" | "fastwriter" => WriterArm::Fast,
"safe" | "safewriter" => WriterArm::Safe,
"uring" | "uringwriter" | "io_uring" => WriterArm::Uring,
other => bail!(
"'{other}' is not a writer arm — expected one of fast, safe, uring \
({}={other})",
ENV_WRITER
),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IndexArm {
#[default]
OneTableFourColumns,
FourTables,
PackedPayload,
}
impl IndexArm {
pub const ALL: [IndexArm; 3] = [
IndexArm::OneTableFourColumns,
IndexArm::FourTables,
IndexArm::PackedPayload,
];
pub fn as_str(self) -> &'static str {
match self {
IndexArm::OneTableFourColumns => "one-table",
IndexArm::FourTables => "four-tables",
IndexArm::PackedPayload => "packed",
}
}
pub fn projection_name(self) -> &'static str {
match self {
IndexArm::OneTableFourColumns => "OneTableFourColumns",
IndexArm::FourTables => "FourTables",
IndexArm::PackedPayload => "PackedPayload",
}
}
pub fn parse(s: &str) -> Result<Self> {
Ok(match s.trim().to_ascii_lowercase().as_str() {
"one-table" | "one_table" | "onetablefourcolumns" | "one" => {
IndexArm::OneTableFourColumns
}
"four-tables" | "four_tables" | "fourtables" | "four" => IndexArm::FourTables,
"packed" | "packedpayload" => IndexArm::PackedPayload,
other => bail!(
"'{other}' is not an index arm — expected one of one-table, four-tables, packed \
({ENV_INDEX}={other})"
),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GcArm {
#[default]
NewGeneration,
CompactInPlace,
}
impl GcArm {
pub const ALL: [GcArm; 2] = [GcArm::NewGeneration, GcArm::CompactInPlace];
pub fn as_str(self) -> &'static str {
match self {
GcArm::NewGeneration => "new-generation",
GcArm::CompactInPlace => "in-place",
}
}
pub fn strategy(self) -> &'static str {
match self {
GcArm::NewGeneration => "NewGeneration",
GcArm::CompactInPlace => "CompactInPlace",
}
}
pub fn create(self) -> Box<dyn Gc + Send + Sync> {
match self {
GcArm::NewGeneration => Box::new(NewGeneration::new()),
GcArm::CompactInPlace => Box::new(CompactInPlace::new()),
}
}
pub fn parse(s: &str) -> Result<Self> {
Ok(match s.trim().to_ascii_lowercase().as_str() {
"new-generation" | "new_generation" | "newgeneration" | "new" | "generation" => {
GcArm::NewGeneration
}
"in-place" | "in_place" | "inplace" | "compactinplace" | "compact" => {
GcArm::CompactInPlace
}
other => bail!(
"'{other}' is not a gc arm — expected one of new-generation, in-place \
({ENV_GC}={other})"
),
})
}
}
pub const ENV_WRITER: &str = "ZNIPPY_GIT_WRITER";
pub const ENV_INDEX: &str = "ZNIPPY_GIT_INDEX";
pub const ENV_GC: &str = "ZNIPPY_GIT_GC";
pub const ENV_REDB_CACHE: &str = "ZNIPPY_GIT_REDB_CACHE_BYTES";
pub const ENV_BOUNDARY_DELTA: &str = "ZNIPPY_GIT_BOUNDARY_DELTA";
pub const ENV_EXPLODE: &str = "ZNIPPY_GIT_EXPLODE";
pub const ENV_REACH_COMMITS: &str = "ZNIPPY_GIT_REACH_COMMITS";
pub const ENV_EMIT_WORKERS: &str = "ZNIPPY_GIT_EMIT_WORKERS";
pub const ALL_ENV: &[&str] = &[
ENV_WRITER,
ENV_INDEX,
ENV_GC,
ENV_REDB_CACHE,
ENV_BOUNDARY_DELTA,
ENV_EXPLODE,
ENV_REACH_COMMITS,
ENV_EMIT_WORKERS,
];
pub fn redb_cache_bytes() -> Result<usize> {
let Some(raw) = read_env(ENV_REDB_CACHE) else {
return Ok(DEFAULT_REDB_CACHE_BYTES);
};
match raw.parse::<usize>() {
Ok(0) | Err(_) => bail!(
"{ENV_REDB_CACHE}={raw:?} is not a positive byte count; it bounds redb's page \
cache per database and the default is {DEFAULT_REDB_CACHE_BYTES}"
),
Ok(n) => Ok(n),
}
}
pub const DEFAULT_REDB_CACHE_BYTES: usize = 64 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StoreConfig {
pub writer: WriterArm,
pub index: IndexArm,
pub gc: GcArm,
pub redb_cache_bytes: usize,
}
impl Default for StoreConfig {
fn default() -> Self {
Self::DEFAULT
}
}
impl StoreConfig {
pub const DEFAULT: StoreConfig = StoreConfig {
writer: WriterArm::Safe,
index: IndexArm::OneTableFourColumns,
gc: GcArm::NewGeneration,
redb_cache_bytes: DEFAULT_REDB_CACHE_BYTES,
};
pub fn from_env() -> Result<Self> {
let mut cfg = StoreConfig::DEFAULT;
if let Some(v) = read_env(ENV_WRITER) {
cfg.writer = WriterArm::parse(&v)?;
}
if let Some(v) = read_env(ENV_INDEX) {
cfg.index = IndexArm::parse(&v)?;
}
if let Some(v) = read_env(ENV_GC) {
cfg.gc = GcArm::parse(&v)?;
}
cfg.redb_cache_bytes = redb_cache_bytes()?;
Ok(cfg)
}
pub fn with_writer(mut self, w: WriterArm) -> Self {
self.writer = w;
self
}
pub fn with_index(mut self, i: IndexArm) -> Self {
self.index = i;
self
}
pub fn with_gc(mut self, g: GcArm) -> Self {
self.gc = g;
self
}
pub fn with_redb_cache_bytes(mut self, bytes: usize) -> Self {
self.redb_cache_bytes = bytes;
self
}
}
impl std::fmt::Display for StoreConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}={} {}={} {}={} {}={}",
ENV_WRITER,
self.writer.as_str(),
ENV_INDEX,
self.index.as_str(),
ENV_GC,
self.gc.as_str(),
ENV_REDB_CACHE,
self.redb_cache_bytes
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::archive_write::read_journal;
use crate::git_ops::{GitOps, GitStore, open_from_env, open_selected};
use crate::index_layout::{OneTableFourColumns, PackedPayload};
use crate::object::GitHashKind;
use crate::store::tests::{real_pack, tmpdir};
use std::sync::Mutex;
fn loadavg() -> String {
std::fs::read_to_string("/proc/loadavg")
.unwrap_or_default()
.split_whitespace()
.take(3)
.collect::<Vec<_>>()
.join(" ")
}
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn with_env<R>(vars: &[(&str, &str)], f: impl FnOnce() -> R) -> R {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let saved: Vec<(String, Option<String>)> = [ENV_WRITER, ENV_INDEX, ENV_GC, ENV_REDB_CACHE]
.iter()
.map(|k| (k.to_string(), std::env::var(k).ok()))
.collect();
unsafe {
for (k, _) in &saved {
std::env::remove_var(k);
}
for (k, v) in vars {
std::env::set_var(k, v);
}
}
let out = f();
unsafe {
for (k, v) in &saved {
match v {
Some(v) => std::env::set_var(k, v),
None => std::env::remove_var(k),
}
}
}
out
}
#[test]
fn each_writer_arm_leaves_its_own_durability_on_disk() {
let (pack, rows) = real_pack();
let fast_dir = tmpdir("arm-writer-fast");
let fast = GitStore::<OneTableFourColumns>::open_with_arms(
&fast_dir,
"rickard",
GitHashKind::Sha1,
StoreConfig::DEFAULT.with_writer(WriterArm::Fast),
)
.unwrap();
let fast_tx = fast.put_pack(&pack).unwrap();
let (fo, fl) = fast_tx.extent.unwrap();
let fast_blobs = fast_dir.join("objects.pack");
let fast_journal = crate::archive_write::journal_path(&fast_blobs);
assert_eq!(
&std::fs::read(&fast_blobs).unwrap()[fo as usize..(fo + fl) as usize],
&pack[..],
"the fast arm did not store the pack verbatim — the arm may change the promise, \
never the payload"
);
assert!(
!fast_journal.exists(),
"the fast arm left a journal at {} — a writer that was not selected ran",
fast_journal.display()
);
let safe_dir = tmpdir("arm-writer-safe");
let safe = GitStore::<OneTableFourColumns>::open_with_arms(
&safe_dir,
"rickard",
GitHashKind::Sha1,
StoreConfig::DEFAULT.with_writer(WriterArm::Safe),
)
.unwrap();
let safe_tx = safe.put_pack(&pack).unwrap();
let (so, sl) = safe_tx.extent.unwrap();
let safe_blobs = safe_dir.join("objects.pack");
let safe_journal = crate::archive_write::journal_path(&safe_blobs);
assert_eq!(
&std::fs::read(&safe_blobs).unwrap()[so as usize..(so + sl) as usize],
&pack[..],
"the safe arm did not store the pack verbatim"
);
assert!(
safe_journal.exists(),
"the safe arm wrote no journal — the durable arm did not run"
);
assert_eq!(
read_journal(&safe_journal).unwrap(),
vec![(so, sl)],
"the journal does not name the extent that was acked"
);
assert_eq!((fo, fl), (so, sl), "the two arms disagree about the extent");
assert_eq!(fast.writer_name(), "FastWriter");
assert_eq!(safe.writer_name(), "SafeWriter");
fast.wait_indexed();
safe.wait_indexed();
assert_eq!(fast.object_count(), rows.len());
assert_eq!(safe.object_count(), rows.len());
eprintln!(
"load {}; {} objects: fast arm {} journal row(s), safe arm {} — {}",
loadavg(),
rows.len(),
if fast_journal.exists() { 1 } else { 0 },
read_journal(&safe_journal).unwrap().len(),
WriterArm::Fast.durability(),
);
}
#[cfg(target_os = "linux")]
#[test]
fn the_uring_arm_is_selectable_through_a_store_and_lands_the_same_journal() {
let dir = tmpdir("arm-writer-uring");
let store = GitStore::<OneTableFourColumns>::open_with_arms(
&dir,
"rickard",
GitHashKind::Sha1,
StoreConfig::DEFAULT.with_writer(WriterArm::Uring),
)
.expect(
"the io_uring arm could not be built on this kernel — not skipped, this is a real \
failure",
);
let (pack, oid) = crate::store::tests::one_blob_pack(b"a push down the io_uring chain");
let tx = store.put_pack(&pack).unwrap();
let (o, l) = tx.extent.unwrap();
let blobs = dir.join("objects.pack");
assert_eq!(
&std::fs::read(&blobs).unwrap()[o as usize..(o + l) as usize],
&pack[..],
"the uring arm did not store the pack verbatim"
);
assert_eq!(
read_journal(&crate::archive_write::journal_path(&blobs)).unwrap(),
vec![(o, l)],
"the uring arm's journal does not name the extent it acked — the two durable arms \
must write one journal format"
);
assert_eq!(store.writer_name(), "UringWriter", "the uring arm ran a different writer");
store.wait_indexed();
assert!(store.has(&oid).unwrap(), "the pushed object never reached the index");
}
#[test]
fn each_gc_arm_leaves_its_own_generation_on_disk() {
for arm in GcArm::ALL {
let dir = tmpdir(&format!("arm-gc-{}", arm.as_str()));
let store = GitStore::<OneTableFourColumns>::open_with_arms(
&dir,
"rickard",
GitHashKind::Sha1,
StoreConfig::DEFAULT.with_gc(arm),
)
.unwrap();
let (pack, _) = real_pack();
store.put(&pack, &[]).unwrap();
store.absorb_pending().unwrap();
let root = store
.graph_snapshot()
.into_iter()
.find(|c| c.generation == 1)
.expect("a root commit");
let root_raw = hex::decode(&root.oid).unwrap();
store
.update_ref("refs/heads/root", None, Some(&root_raw))
.unwrap();
let files = vec![
("pack-0.pack".to_string(), pack.clone()),
("pack-1.pack".to_string(), pack.clone()),
];
znippy_common::create_archive(store.archive_path(), &files, 3).unwrap();
let original = store.archive_path().to_path_buf();
let generation = crate::gc::next_generation(&original).unwrap();
let report = store.gc().unwrap();
match arm {
GcArm::NewGeneration => {
assert_eq!(report.archive, generation);
assert!(
generation.exists(),
"the new-generation arm produced no {}",
generation.display()
);
assert!(
!original.exists(),
"the new-generation arm kept the old generation at {}",
original.display()
);
assert_eq!(report.retired.as_deref(), Some(original.as_path()));
assert!(report.verified, "the new generation was not read back");
}
GcArm::CompactInPlace => {
assert!(
!generation.exists(),
"the in-place arm produced a new generation at {} — a gc that was not \
selected ran",
generation.display()
);
assert!(
original.exists(),
"the in-place arm removed the archive it compacts into"
);
assert_eq!(report.archive, original);
assert_eq!(report.retired, None);
}
}
assert_eq!(report.strategy, arm.strategy());
assert!(
report.bytes_after <= report.bytes_before,
"{} grew the archive: {} → {}",
arm.strategy(),
report.bytes_before,
report.bytes_after
);
eprintln!(
"load {}; gc arm {}: {} → {} bytes, archive now {}",
loadavg(),
arm.strategy(),
report.bytes_before,
report.bytes_after,
report.archive.display()
);
}
}
#[test]
fn each_index_arm_builds_its_own_projection_and_all_three_agree() {
let (pack, rows) = real_pack();
let mut built = Vec::new();
for arm in IndexArm::ALL {
let store = open_selected(
&tmpdir(&format!("arm-index-{}", arm.as_str())),
"rickard",
GitHashKind::Sha1,
StoreConfig::DEFAULT.with_index(arm),
)
.unwrap();
store.put_pack(&pack).unwrap();
store.wait_indexed();
store.rebuild_projection().unwrap();
assert_eq!(store.arms().index, arm);
assert_eq!(
store.index_name(),
arm.projection_name(),
"the {} arm built a projection calling itself {}",
arm.as_str(),
store.index_name()
);
built.push((arm, store));
}
for (i, (a, sa)) in built.iter().enumerate() {
for (b, sb) in &built[i + 1..] {
assert_ne!(
sa.index_ipc_bytes(),
sb.index_ipc_bytes(),
"{} and {} materialised the same {} IPC bytes — the index arm was not selected",
a.as_str(),
b.as_str(),
sa.index_ipc_bytes()
);
}
}
for r in rows.iter().take(512) {
let first = built[0].1.get(&r.oid).unwrap();
assert!(first.is_some(), "{} is missing from one-table", hex::encode(&r.oid));
for (arm, store) in &built[1..] {
assert_eq!(
store.get(&r.oid).unwrap(),
first,
"{} disagrees with one-table about {}",
arm.as_str(),
hex::encode(&r.oid)
);
}
}
eprintln!(
"load {}; {} objects: {}",
loadavg(),
rows.len(),
built
.iter()
.map(|(a, s)| format!("{} {} B", a.as_str(), s.index_ipc_bytes()))
.collect::<Vec<_>>()
.join(", ")
);
}
#[test]
fn the_default_is_unchanged_and_the_environment_cannot_move_it() {
with_env(
&[
(ENV_WRITER, "fast"),
(ENV_INDEX, "packed"),
(ENV_GC, "in-place"),
],
|| {
let dir = tmpdir("arm-default");
let store = GitStore::open(&dir, "rickard").unwrap();
let (pack, rows) = real_pack();
let tx = store.put(&pack, &[]).unwrap();
let journal = crate::archive_write::journal_path(&dir.join("objects.pack"));
assert!(
journal.exists(),
"GitStore::open wrote no journal — an environment variable moved the default \
durability contract"
);
assert_eq!(read_journal(&journal).unwrap(), vec![tx.extent.unwrap()]);
store.wait_indexed();
assert_eq!(store.object_count(), rows.len());
assert_eq!(
store.arms(),
StoreConfig::DEFAULT,
"GitStore::open did not build the shipping arms"
);
assert_eq!(store.writer_name(), "SafeWriter");
let root = store
.graph_snapshot()
.into_iter()
.find(|c| c.generation == 1)
.expect("a root commit");
let root_raw = hex::decode(&root.oid).unwrap();
store
.update_ref("refs/heads/root", None, Some(&root_raw))
.unwrap();
let files = vec![("pack-0.pack".to_string(), pack.clone())];
znippy_common::create_archive(store.archive_path(), &files, 3).unwrap();
let generation = crate::gc::next_generation(store.archive_path()).unwrap();
let report = store.gc().unwrap();
assert_eq!(report.strategy, "NewGeneration");
assert!(
generation.exists(),
"GitStore::open's gc arm is not NewGeneration — the environment moved it"
);
},
);
}
#[test]
fn the_selector_is_read_once_at_construction_and_never_per_operation() {
with_env(
&[
(ENV_WRITER, "safe"),
(ENV_INDEX, "four-tables"),
(ENV_GC, "in-place"),
],
|| {
let dir = tmpdir("arm-read-once");
let (pack, rows) = real_pack();
let before = env_reads_here();
let store = open_from_env(&dir, "rickard", GitHashKind::Sha1).unwrap();
let at_open = env_reads_here();
assert_eq!(
at_open - before,
5,
"opening a store read the environment {} times, not once per variable",
at_open - before
);
let mut ops = 0u64;
store.put(&pack, &[]).unwrap();
ops += 1;
let oids: Vec<&[u8]> = rows.iter().map(|r| r.oid.as_slice()).collect();
for oid in &oids {
assert!(store.has(oid).unwrap());
ops += 1;
}
let ext = store.extents(&oids).unwrap();
ops += 1;
assert_eq!(ext.len(), oids.len());
assert!(ext.iter().all(Option::is_some));
assert!(store.get(oids[0]).unwrap().is_some());
assert!(store.size(oids[0]).unwrap().is_some());
store.refs().unwrap();
ops += 3;
let after = env_reads_here();
assert_eq!(
after, at_open,
"the selector was read {} times while serving {ops} operations — it must be \
read once, at construction",
after - before
);
eprintln!(
"load {}; {ops} operations over {} objects: {} environment read(s), all of \
them at construction",
loadavg(),
rows.len(),
at_open - before,
);
},
);
}
#[test]
fn an_unknown_arm_refuses_to_open_rather_than_falling_back() {
with_env(&[(ENV_WRITER, "safest")], || {
let dir = tmpdir("arm-typo");
let e = match open_from_env(&dir, "rickard", GitHashKind::Sha1) {
Ok(_) => panic!("a typo opened a store on the default arm"),
Err(e) => e,
};
let msg = format!("{e:#}");
assert!(
msg.contains("'safest' is not a writer arm") && msg.contains(ENV_WRITER),
"the refusal must name the bad value and the variable it came from: {msg}"
);
});
with_env(&[], || {
assert_eq!(StoreConfig::from_env().unwrap(), StoreConfig::DEFAULT);
});
}
#[test]
fn the_selected_store_and_the_typed_store_answer_alike() {
let (pack, rows) = real_pack();
let arms = StoreConfig::DEFAULT.with_index(IndexArm::PackedPayload);
let boxed = open_selected(
&tmpdir("arm-selected"),
"rickard",
GitHashKind::Sha1,
arms,
)
.unwrap();
let typed = GitStore::<PackedPayload>::open_with_arms(
&tmpdir("arm-typed"),
"rickard",
GitHashKind::Sha1,
arms,
)
.unwrap();
boxed.put(&pack, &[]).unwrap();
typed.put(&pack, &[]).unwrap();
for r in rows.iter().take(512) {
let a = boxed.get(&r.oid).unwrap();
let b = typed.get(&r.oid).unwrap();
assert_eq!(a, b, "the boxed and typed stores disagree about {}", hex::encode(&r.oid));
assert!(a.is_some());
}
}
#[test]
fn every_arm_parses_from_the_name_an_operator_types() {
for a in WriterArm::ALL {
assert_eq!(WriterArm::parse(a.as_str()).unwrap(), a);
assert_eq!(WriterArm::parse(&a.as_str().to_uppercase()).unwrap(), a);
}
for a in IndexArm::ALL {
assert_eq!(IndexArm::parse(a.as_str()).unwrap(), a);
}
for a in GcArm::ALL {
assert_eq!(GcArm::parse(a.as_str()).unwrap(), a);
}
for bad in ["saef", "", "fastwriter2", "none"] {
assert!(
WriterArm::parse(bad).is_err(),
"'{bad}' parsed as a writer arm"
);
}
assert!(IndexArm::parse("one-tabel").is_err());
assert!(GcArm::parse("newgen").is_err());
}
#[test]
fn every_environment_read_in_this_crate_is_named_in_all_env() {
let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let this_file = std::fs::read_to_string(src.join("arms.rs")).unwrap();
let mut consts: std::collections::BTreeMap<String, String> = Default::default();
for line in this_file.lines() {
let t = line.trim();
let Some(rest) = t.strip_prefix("pub const ENV_") else { continue };
let Some((name, val)) = rest.split_once(": &str = \"") else { continue };
let val = val.split('"').next().unwrap();
consts.insert(format!("ENV_{name}"), val.to_string());
}
assert!(consts.len() >= 8, "fewer ENV_* constants than expected: {consts:?}");
let mut seen: Vec<(String, String)> = Vec::new();
let mut files = std::fs::read_dir(&src)
.unwrap()
.map(|e| e.unwrap().path())
.filter(|p| p.extension().is_some_and(|e| e == "rs"))
.collect::<Vec<_>>();
files.sort();
assert!(!files.is_empty());
for path in files {
let file = path.file_name().unwrap().to_string_lossy().into_owned();
let text = std::fs::read_to_string(&path).unwrap();
let non_test = match text.find("\n#[cfg(test)]") {
Some(at) => &text[..at],
None => &text[..],
};
for needle in ["env::var(", "env::var_os(", "read_env("] {
let mut from = 0;
while let Some(at) = non_test[from..].find(needle) {
let call_at = from + at;
from = call_at + needle.len();
let line_start = non_test[..call_at].rfind('\n').map_or(0, |i| i + 1);
let line = non_test[line_start..].lines().next().unwrap_or("").trim_start();
if line.starts_with("//") || line.starts_with("pub(crate) fn read_env") {
continue;
}
if file == "arms.rs" && needle == "env::var(" && line.contains("var(key)") {
continue;
}
let arg = non_test[from..].trim_start();
let key = if let Some(lit) = arg.strip_prefix('"') {
lit.split('"').next().unwrap().to_string()
} else {
let ident: String = arg
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == ':')
.collect();
let short = ident.rsplit("::").next().unwrap_or(&ident).to_string();
consts.get(&short).cloned().unwrap_or_else(|| {
panic!(
"{file} reads the environment through `{needle}{ident}…`, which \
is neither a string literal nor an ENV_* constant this test \
can resolve — name the key as a `pub const ENV_*` in arms.rs"
)
})
};
seen.push((file.clone(), key));
}
}
}
assert!(
seen.len() >= 8,
"the scan found only {} environment reads; it is supposed to find at least the \
eight in ALL_ENV ({seen:?})",
seen.len()
);
for (file, key) in &seen {
assert!(
ALL_ENV.contains(&key.as_str()),
"{file} reads {key} and ALL_ENV does not name it"
);
}
for key in ALL_ENV {
assert!(
seen.iter().any(|(_, k)| k == key),
"ALL_ENV names {key} but no non-test code in this crate reads it"
);
}
}
#[test]
fn the_default_config_is_the_shipping_combination() {
assert_eq!(StoreConfig::default(), StoreConfig::DEFAULT);
assert_eq!(StoreConfig::DEFAULT.writer, WriterArm::Safe);
assert_eq!(StoreConfig::DEFAULT.index, IndexArm::OneTableFourColumns);
assert_eq!(StoreConfig::DEFAULT.gc, GcArm::NewGeneration);
assert_eq!(
StoreConfig::DEFAULT.to_string(),
"ZNIPPY_GIT_WRITER=safe ZNIPPY_GIT_INDEX=one-table ZNIPPY_GIT_GC=new-generation \
ZNIPPY_GIT_REDB_CACHE_BYTES=67108864"
);
}
#[test]
fn only_the_arms_that_write_a_journal_name_one() {
let blobs = Path::new("/nonexistent/objects.pack");
assert_eq!(WriterArm::Fast.journal(blobs), None);
assert_eq!(
WriterArm::Safe.journal(blobs),
Some(PathBuf::from("/nonexistent/objects.pack.journal"))
);
assert_eq!(
WriterArm::Uring.journal(blobs),
WriterArm::Safe.journal(blobs),
"the two durable arms share one journal format (LAW 5) and must share its name"
);
}
#[test]
fn the_arms_durability_line_matches_the_writer_it_builds() {
let dir = crate::store::tests::tmpdir("arm-durability");
for arm in [WriterArm::Fast, WriterArm::Safe] {
let w = arm.create(&dir.join(format!("{}.pack", arm.as_str()))).unwrap();
match arm {
WriterArm::Fast => {
assert_eq!(w.name(), "FastWriter");
assert!(
w.durability().starts_with("none"),
"FastWriter must say plainly that it promises nothing: {}",
w.durability()
);
assert!(arm.durability().starts_with("none"));
}
WriterArm::Safe => {
assert_eq!(w.name(), "SafeWriter");
assert_eq!(w.durability(), arm.durability());
}
WriterArm::Uring => unreachable!(),
}
}
}
}