refactor(bookspec): the specification lives in its own repository (P13 step 6)
bookspec/ (spec, examples, checker, runner, Lean, GnuCash adapter) moves to https://git.swaits.com/swaits/bookspec, history kept by git subtree split (public home; CC BY 4.0 for spec/ and examples/, MIT OR Apache-2.0 for the code). ledger-core and ledger-package depend on bookspec-checker and bookspec-runner as git dev-dependencies pinned to 74f2853d5f0db0066147c17fed8033cfe3d83a24, and read the examples through bookspec_checker::examples(). just check still runs every example via cargo test --workspace. The pin check against docs/spec/sources and the rules.md id check move into tests/bookspec.rs (they need this repository's files). The accounting is unchanged: no example or rule moved. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
1 parent
794807ab90
commit
b77a25fd7b
124 files changed
+145
-18081
No files matched your search
@@ -26,8 +26,10 @@ proptest = "1"
|
||||
# store (a dev-dependency only: the store depends on this crate), each in
|
||||
# a directory of its own.
|
||||
toml = "1"
|
||||
bookspec-checker = { path = "../../bookspec/checker" }
|
||||
bookspec-runner = { path = "../../bookspec/runner" }
|
||||
# The specification lives in its own repository (P13 step 6, D-34, D-65),
|
||||
# pinned to a commit.
|
||||
bookspec-checker = { git = "https://git.swaits.com/swaits/bookspec", rev = "74f2853d5f0db0066147c17fed8033cfe3d83a24" }
|
||||
bookspec-runner = { git = "https://git.swaits.com/swaits/bookspec", rev = "74f2853d5f0db0066147c17fed8033cfe3d83a24" }
|
||||
ledger-package = { path = "../ledger-package" }
|
||||
ledger-import = { path = "../ledger-import" }
|
||||
ledger-store = { path = "../ledger-store" }
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
use chrono::{Datelike, NaiveDate};
|
||||
use ledger_core::calendar::{PeriodDefaults, period_defaults};
|
||||
@@ -214,8 +214,14 @@ fn store_refusal(e: &StoreError) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn examples_dir() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../bookspec/examples")
|
||||
/// The text of the bookspec example file `file`, carried by the pinned
|
||||
/// `bookspec-checker` crate.
|
||||
fn example_text(file: &str) -> &'static str {
|
||||
bookspec_checker::examples()
|
||||
.into_iter()
|
||||
.find(|(name, _)| *name == file)
|
||||
.unwrap_or_else(|| panic!("no bookspec example {file}"))
|
||||
.1
|
||||
}
|
||||
|
||||
fn date(s: &str, what: &str) -> NaiveDate {
|
||||
@@ -3840,16 +3846,9 @@ fn run(ex: &Example) -> Vec<(String, Option<&'static str>)> {
|
||||
/// this proves the trait path, which is what another engine supplies.
|
||||
#[test]
|
||||
fn the_runner_reports_every_claimed_rule_green() {
|
||||
let mut files: Vec<PathBuf> = std::fs::read_dir(examples_dir())
|
||||
.expect("bookspec/examples")
|
||||
.map(|e| e.unwrap().path())
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "toml"))
|
||||
.collect();
|
||||
files.sort();
|
||||
let mut examples = Vec::new();
|
||||
for f in &files {
|
||||
let text = std::fs::read_to_string(f).unwrap();
|
||||
examples.push(Example::from_toml(&text).unwrap_or_else(|e| panic!("{}: {e}", f.display())));
|
||||
for (name, text) in bookspec_checker::examples() {
|
||||
examples.push(Example::from_toml(text).unwrap_or_else(|e| panic!("{name}: {e}")));
|
||||
}
|
||||
assert!(examples.len() >= 56, "the seed has at least 56 examples");
|
||||
let report = bookspec_runner::run(&examples, bookspec_checker::rules(), Books::blank);
|
||||
@@ -3877,19 +3876,13 @@ fn the_runner_reports_every_claimed_rule_green() {
|
||||
|
||||
#[test]
|
||||
fn every_bookspec_example_holds() {
|
||||
let mut files: Vec<PathBuf> = std::fs::read_dir(examples_dir())
|
||||
.expect("bookspec/examples")
|
||||
.map(|e| e.unwrap().path())
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "toml"))
|
||||
.collect();
|
||||
files.sort();
|
||||
let files = bookspec_checker::examples();
|
||||
let mut ids = BTreeSet::new();
|
||||
let mut checked: BTreeMap<String, usize> = BTreeMap::new();
|
||||
for f in &files {
|
||||
let text = std::fs::read_to_string(f).unwrap();
|
||||
let ex = Example::from_toml(&text).unwrap_or_else(|e| panic!("{}: {e}", f.display()));
|
||||
let stem = f.file_stem().unwrap().to_string_lossy();
|
||||
assert_eq!(ex.id, stem, "{}: the file is named for its id", f.display());
|
||||
for (name, text) in &files {
|
||||
let ex = Example::from_toml(text).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
let stem = name.strip_suffix(".toml").unwrap();
|
||||
assert_eq!(ex.id, stem, "{name}: the file is named for its id");
|
||||
assert!(ids.insert(ex.id.clone()), "{}: id used twice", ex.id);
|
||||
assert!(
|
||||
!ex.title.is_empty() && !ex.rule.is_empty() && !ex.tags.is_empty(),
|
||||
@@ -4038,19 +4031,16 @@ fn reverses_zero_is_refused() {
|
||||
/// ignored: an example can't silently run with other roles than it says.
|
||||
#[test]
|
||||
fn an_unknown_key_is_refused() {
|
||||
let text = std::fs::read_to_string(
|
||||
examples_dir().join("cash-flow.reversed-property-contribution.toml"),
|
||||
)
|
||||
.unwrap()
|
||||
.replace("cash_accounts = ", "cash_account = ");
|
||||
let text = example_text("cash-flow.reversed-property-contribution.toml")
|
||||
.replace("cash_accounts = ", "cash_account = ");
|
||||
assert!(toml::from_str::<Example>(&text).is_err());
|
||||
}
|
||||
|
||||
/// The example `file`'s text, changed by `edit`, loaded and its
|
||||
/// expectations checked as `every_bookspec_example_holds` checks them.
|
||||
fn run_edited(file: &str, edit: impl Fn(String) -> String) {
|
||||
let text = edit(std::fs::read_to_string(examples_dir().join(file)).unwrap());
|
||||
let before = std::fs::read_to_string(examples_dir().join(file)).unwrap();
|
||||
let before = example_text(file).to_string();
|
||||
let text = edit(before.clone());
|
||||
assert_ne!(text, before, "the edit changed nothing");
|
||||
let ex: Example = toml::from_str(&text).unwrap();
|
||||
run(&ex);
|
||||
@@ -4587,3 +4577,71 @@ fn an_invoice_lines_project_is_what_the_project_counts() {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Whitespace and typographic quotes made plain, so a pin copied from a
|
||||
/// wrapped or curly-quoted source still matches.
|
||||
fn squash(s: &str) -> String {
|
||||
let plain = s
|
||||
.replace(['\u{2019}', '\u{2018}'], "'")
|
||||
.replace(['\u{201c}', '\u{201d}'], "\"");
|
||||
plain.split_whitespace().collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
/// Every vendored citation in the specification names a file in
|
||||
/// `docs/spec/sources`, and each quoted pin is verbatim in it. (The
|
||||
/// specification crate checks the citations' form; the vendored texts live
|
||||
/// here.)
|
||||
#[test]
|
||||
fn every_vendored_pin_is_verbatim() {
|
||||
let sources = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/spec/sources");
|
||||
let mut vendored = 0;
|
||||
let mut bad = Vec::new();
|
||||
for (name, text) in bookspec_checker::spec_files() {
|
||||
for line in text.lines() {
|
||||
let Some(rest) = line.strip_prefix(" - vendored `") else {
|
||||
continue;
|
||||
};
|
||||
let (file, rest) = rest
|
||||
.split_once("`:")
|
||||
.unwrap_or_else(|| panic!("{name}: {line}"));
|
||||
let path = sources.join(file.strip_prefix("sources/").unwrap_or(file));
|
||||
let hay = squash(
|
||||
&std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|_| panic!("{name}: {file} is not vendored")),
|
||||
);
|
||||
for q in rest.split('"').skip(1).step_by(2) {
|
||||
if !hay.contains(&squash(q)) {
|
||||
bad.push(format!("{name}: {q:?} is not in {file}"));
|
||||
}
|
||||
}
|
||||
vendored += 1;
|
||||
}
|
||||
}
|
||||
assert!(bad.is_empty(), "pins not found:\n{}", bad.join("\n"));
|
||||
assert!(vendored >= 30, "vendored citations: {vendored}");
|
||||
}
|
||||
|
||||
/// `docs/spec/rules.md` carries every stated rule's id under its heading,
|
||||
/// and no longer calls a choice a decision (P13 step 3).
|
||||
#[test]
|
||||
fn rules_md_carries_every_id() {
|
||||
let text = std::fs::read_to_string(
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/spec/rules.md"),
|
||||
)
|
||||
.expect("docs/spec/rules.md");
|
||||
for rule in bookspec_checker::rules()
|
||||
.iter()
|
||||
.filter(|r| r.status.is_some())
|
||||
{
|
||||
assert!(
|
||||
text.contains(&format!("**bookspec:** `{}`", rule.id)),
|
||||
"B-{} carries {}",
|
||||
rule.b,
|
||||
rule.id
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!text.contains("DECISION"),
|
||||
"rules.md speaks bookspec's CHOICE"
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ zip = "9.0.1"
|
||||
workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
bookspec-checker = { git = "https://git.swaits.com/swaits/bookspec", rev = "74f2853d5f0db0066147c17fed8033cfe3d83a24" }
|
||||
proptest = "1"
|
||||
tempfile = "3"
|
||||
toml = "1"
|
||||
@@ -2142,18 +2142,19 @@ proptest::proptest! {
|
||||
}
|
||||
}
|
||||
|
||||
/// The bookspec example `bookspec/examples/iif-quickbooks-journal-file.toml`
|
||||
/// The bookspec example `iif-quickbooks-journal-file.toml` (bookspec's `examples/`)
|
||||
/// (P3's accept: each regression test also a bookspec example), its
|
||||
/// expected values derived by hand, against the engine: the example's
|
||||
/// books, built from its setup and events, export the very file and
|
||||
/// changes it says.
|
||||
#[test]
|
||||
fn the_bookspec_iif_example_matches_the_engine() {
|
||||
let path = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../bookspec/examples/iif-quickbooks-journal-file.toml"
|
||||
);
|
||||
let spec: toml::Value = toml::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
|
||||
let text = bookspec_checker::examples()
|
||||
.into_iter()
|
||||
.find(|(name, _)| *name == "iif-quickbooks-journal-file.toml")
|
||||
.expect("the bookspec IIF example")
|
||||
.1;
|
||||
let spec: toml::Value = toml::from_str(text).unwrap();
|
||||
let setup = &spec["setup"];
|
||||
let mut l = Ledger::new(Currency::USD);
|
||||
for a in setup["accounts"].as_array().unwrap() {
|
||||
|
||||
Reference in new issue
Block a user