Skip to main content

Ledger

Struct Ledger 

Source
pub struct Ledger {
    pub version: u32,
    pub executions: Vec<Execution>,
}
Expand description

§Example

use majordomus_cli::evidence::Ledger;
let l = Ledger::empty();
assert_eq!(l.summary().executions, 0);
assert!(l.latest("suite:07_scope").is_none());

The latest execution of every test this repository has recorded.

One entry per test, never a history: the question it answers (“is this claim proven now?”) only ever reads the newest, and a run per commit for a hundred tests would grow without bound. It is a value first and a file second — Ledger::load and Ledger::save move it to and from LEDGER_PATH under a repository root, and a repository with no file has an empty one rather than an error.

use majordomus_cli::evidence::ledger::{Ledger, LEDGER_PATH, LEDGER_VERSION};

let mut ledger = Ledger::empty();
assert_eq!(ledger.version, LEDGER_VERSION);
assert_eq!(LEDGER_PATH, ".ai/repo/evidence/ledger.json");

ledger.merge([execution("07_scope", Outcome::Pass)]);
assert_eq!(ledger.executions.len(), 1);
assert_eq!(ledger.latest("suite:07_scope").unwrap().outcome, Outcome::Pass);

Fields§

§version: u32

The shape’s version.

§executions: Vec<Execution>

The executions, one per test, ordered by test id so the file diffs cleanly.

Implementations§

Source§

impl Ledger

Source

pub fn empty() -> Ledger

An empty ledger — what a repository that has recorded nothing has.

Indistinguishable from a missing file everywhere it matters: Ledger::load answers one for the other, Ledger::summary reports present: false for both, and both make every claim read not run, which is the honest answer in either case. Only Ledger::present, which asks the filesystem, tells them apart.

use majordomus_cli::evidence::ledger::{Ledger, LEDGER_VERSION};

let ledger = Ledger::empty();
assert!(ledger.executions.is_empty());
assert_eq!(ledger.version, LEDGER_VERSION, "it still declares the shape it is");
assert!(!ledger.summary().present, "nothing recorded is not a present ledger");
Source

pub fn parse(text: &str) -> Result<Ledger, Error>

Read a ledger from its text, wherever the text came from: the working copy under a repository root, or the file as a commit holds it.

The one reading of the shape, so that a ledger read from a commit and a ledger read from the working tree are refused for the same reasons in the same words. A version this executable does not know is refused rather than reinterpreted.

use majordomus_cli::evidence::ledger::{Ledger, LEDGER_VERSION};

let ledger = Ledger::parse(r#"{"version": 1, "executions": []}"#).unwrap();
assert_eq!(ledger.version, LEDGER_VERSION);
assert!(ledger.executions.is_empty());

let refused = Ledger::parse(r#"{"version": 99, "executions": []}"#).unwrap_err();
assert!(refused.to_string().contains("version 99"), "{refused}");
assert!(Ledger::parse("not json at all").is_err());
Source

pub fn load(root: &Path) -> Result<Ledger, Error>

Read the ledger of a repository. A repository with no ledger has an empty one; that is not an error, and the summary says which it was.

A ledger declaring a version this executable does not know is refused rather than reinterpreted: an absent ledger reports not run, and a misread one could report a pass that was never recorded.

use majordomus_cli::evidence::ledger::{Ledger, LEDGER_PATH};

let root = tempfile::tempdir().unwrap();
assert_eq!(Ledger::load(root.path()).unwrap(), Ledger::empty());

std::fs::create_dir_all(root.path().join(".ai/repo/evidence")).unwrap();
std::fs::write(
    root.path().join(LEDGER_PATH),
    r#"{"version": 99, "executions": []}"#,
)
.unwrap();
let refused = Ledger::load(root.path()).unwrap_err().to_string();
assert!(refused.contains("version 99"), "{refused}");
Source

pub fn present(root: &Path) -> bool

Whether the file is there at all, as opposed to there and empty.

use majordomus_cli::evidence::Ledger;

let root = tempfile::tempdir().unwrap();
assert!(!Ledger::present(root.path()));

Ledger::empty().save(root.path()).unwrap();
assert!(Ledger::present(root.path()), "written and empty is still written");
Source

pub fn latest(&self, test: &str) -> Option<&Execution>

The latest recorded execution of one test, by super::TestId::as_string.

None is the answer for a test no run has ever recorded, which the report renders as not run rather than as a failure.

use majordomus_cli::evidence::Ledger;

let mut ledger = Ledger::empty();
ledger.merge([execution("07_scope", Outcome::Fail)]);

assert_eq!(ledger.latest("suite:07_scope").unwrap().outcome, Outcome::Fail);
assert!(ledger.latest("suite:99_ghost").is_none(), "never run, never recorded");
Source

pub fn merge( &mut self, executions: impl IntoIterator<Item = Execution>, ) -> usize

Merge executions into the ledger: each one replaces the entry for its test and leaves every other alone.

A partial run therefore updates only what it ran, which is what makes bash test/run.sh 84_distribution_model worth recording. The alternative — a whole-ledger write per run — would silently delete the evidence for every test the run did not include, turning a one-case run into a repository that has proven one thing.

use majordomus_cli::evidence::Ledger;

let mut ledger = Ledger::empty();
ledger.merge([
    execution("07_scope", Outcome::Pass),
    execution("08_other", Outcome::Pass),
]);

// a second run of one case replaces that case and touches nothing else
assert_eq!(ledger.merge([execution("07_scope", Outcome::Fail)]), 1);
assert_eq!(ledger.executions.len(), 2, "it replaced rather than appended");
assert_eq!(ledger.latest("suite:07_scope").unwrap().outcome, Outcome::Fail);
assert_eq!(
    ledger.latest("suite:08_other").unwrap().outcome,
    Outcome::Pass,
    "recording one test must never erase another test's evidence",
);
Source

pub fn save(&self, root: &Path) -> Result<(), Error>

Write the ledger, creating its directory. Trailing newline, two-space indent: a tracked file a person reads in a diff.

use majordomus_cli::evidence::ledger::{Ledger, LEDGER_PATH};

let root = tempfile::tempdir().unwrap();
let mut ledger = Ledger::empty();
ledger.merge([execution("07_scope", Outcome::Pass)]);
ledger.save(root.path()).unwrap();

let text = std::fs::read_to_string(root.path().join(LEDGER_PATH)).unwrap();
assert!(text.ends_with('\n'), "a tracked file ends in a newline");
assert!(text.contains("\n  \"version\": 1"), "two-space indent: {text}");
assert_eq!(Ledger::load(root.path()).unwrap(), ledger);
Source

pub fn load_from(root: &Path, target: LedgerTarget) -> Result<Ledger, Error>

Read the ledger target names. An absent one is empty, as with Ledger::load, and a bad one is refused in its own name.

use majordomus_cli::evidence::ledger::{Ledger, LedgerTarget, LOCAL_LEDGER_PATH};

let root = tempfile::tempdir().unwrap();
assert_eq!(Ledger::load_from(root.path(), LedgerTarget::Local).unwrap(), Ledger::empty());
Ledger::empty().save_to(root.path(), LedgerTarget::Local).unwrap();
assert!(root.path().join(LOCAL_LEDGER_PATH).is_file());
assert!(!Ledger::present(root.path()), "the tracked ledger is not written");
Source

pub fn save_to(&self, root: &Path, target: LedgerTarget) -> Result<(), Error>

Write the ledger target names, as Ledger::save writes the tracked one.

use majordomus_cli::evidence::ledger::{Ledger, LedgerTarget, LEDGER_PATH};

let root = tempfile::tempdir().unwrap();
Ledger::empty().save_to(root.path(), LedgerTarget::Repo).unwrap();
assert!(root.path().join(LEDGER_PATH).is_file());
Source

pub fn summary(&self) -> LedgerSummary

What the ledger is, for a report that has to say where its evidence came from: where it lives, how much it holds, how new the newest of it is, and every distinct commit its executions were recorded against.

use majordomus_cli::evidence::ledger::{Ledger, LEDGER_PATH};

let mut ledger = Ledger::empty();
ledger.merge([
    execution("07_scope", Outcome::Pass),
    execution("08_other", Outcome::Fail),
]);

let summary = ledger.summary();
assert_eq!(summary.path, LEDGER_PATH);
assert_eq!(summary.executions, 2);
assert_eq!(summary.newest.as_deref(), Some("2026-09-11T00:00:00Z"));
assert_eq!(summary.commits, ["0".repeat(40)], "distinct commits, deduplicated");
assert!(!Ledger::empty().summary().present);
Source

pub fn by_outcome(&self) -> BTreeMap<String, usize>

How many executions there are of each outcome, for a one-line summary. An outcome nothing recorded is absent from the map rather than present as a zero.

use majordomus_cli::evidence::Ledger;

let mut ledger = Ledger::empty();
ledger.merge([
    execution("07_scope", Outcome::Pass),
    execution("08_other", Outcome::Fail),
    execution("09_third", Outcome::Pass),
]);

let counts = ledger.by_outcome();
assert_eq!(counts.get("pass"), Some(&2));
assert_eq!(counts.get("fail"), Some(&1));
assert_eq!(counts.get("skip"), None, "an outcome nothing recorded is absent");

Trait Implementations§

Source§

impl Clone for Ledger

Source§

fn clone(&self) -> Ledger

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Ledger

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Ledger

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl<'de> Deserialize<'de> for Ledger

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Eq for Ledger

Source§

impl PartialEq for Ledger

Source§

fn eq(&self, other: &Ledger) -> bool

Tests for self and other values to be equal, and is used by ==.
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
Source§

impl Serialize for Ledger

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for Ledger

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more