use std::collections::BTreeMap;
use varar_core::drift::{
BaselineExample, BaselineStore, Drifted, LockFile, OathBaseline, derive_oath_baseline,
detect_drift, live_examples, message, parse_lock_file, prune_baselines, prune_lock_file,
reconcile_drift, stringify_lock_file,
};
use varar_core::handler::Handler;
use varar_core::hash::hash_source;
use varar_core::parse::parse;
use varar_core::plan::{ExecutionPlan, plan};
use varar_core::reference::empty_workspace;
use varar_core::registry::{Registry, add_step, create_registry};
use varar_core::span::Span;
use varar_core::step_kind::StepKind;
fn reg(with_step: bool) -> Registry {
let r = create_registry();
if with_step {
add_step(&r, "I withdraw {int}", "steps.ts", 1, Handler::noop(), Some(StepKind::Stimulus))
.unwrap()
} else {
r
}
}
fn roman_reg(with_step: bool) -> Registry {
let r = create_registry();
if with_step {
add_step(
&r,
"a decimal and a roman number",
"steps.ts",
1,
Handler::noop(),
Some(StepKind::Sensor),
)
.unwrap()
} else {
r
}
}
fn plan_of(source: &str, r: &Registry) -> ExecutionPlan {
plan(&parse("w.md", source), r, &empty_workspace())
}
fn bare(drifts: &[Drifted]) -> Vec<String> {
drifts
.iter()
.map(|d| format!("{}@{}", d.name, d.line))
.collect()
}
#[derive(Default)]
struct MemoryStore {
contents: Option<String>,
}
impl BaselineStore for MemoryStore {
fn read(&self) -> Option<String> {
self.contents.clone()
}
fn write(&mut self, c: &str) {
self.contents = Some(c.to_string());
}
}
fn library_lock() -> LockFile {
let mut oaths = BTreeMap::new();
oaths.insert(
"library.md".to_string(),
OathBaseline {
source_hash: "fnv1a:1a2b3c4d".to_string(),
examples: vec![BaselineExample {
name: "I check out".to_string(),
line: 7,
}],
},
);
LockFile { version: 2, oaths }
}
#[test]
fn hash_matches_the_typescript_vectors() {
assert_eq!("fnv1a:4f9f2cab", hash_source("hello"));
assert_eq!("fnv1a:1a47e90b", hash_source("abc"));
assert_eq!("fnv1a:4eace75e", hash_source("# Title\n"));
}
#[test]
fn live_examples_records_one_entry_per_example_producing_paragraph() {
let doc = parse("w.md", "I withdraw 40.");
assert_eq!(
vec![BaselineExample {
name: "I withdraw 40".to_string(),
line: 1
}],
live_examples(&doc, &plan_of("I withdraw 40.", ®(true)))
);
}
#[test]
fn derive_oath_baseline_carries_the_fingerprint() {
let source = "I withdraw 40.";
let doc = parse("w.md", source);
let baseline = derive_oath_baseline(source, &doc, &plan_of(source, ®(true)));
assert_eq!(hash_source(source), baseline.source_hash);
assert_eq!(
vec![BaselineExample {
name: "I withdraw 40".to_string(),
line: 1
}],
baseline.examples
);
}
#[test]
fn no_baseline_means_no_drift() {
let doc = parse("w.md", "I withdraw 40.");
assert!(detect_drift(None, &doc, &plan_of("I withdraw 40.", ®(true))).is_empty());
}
#[test]
fn a_renamed_step_drifts() {
let source = "I withdraw 40.";
let doc = parse("w.md", source);
let baseline = derive_oath_baseline(source, &doc, &plan_of(source, ®(true)));
assert_eq!(
vec!["I withdraw 40@1".to_string()],
bare(&detect_drift(Some(&baseline), &doc, &plan_of(source, ®(false))))
);
}
#[test]
fn an_in_place_typo_drifts() {
let before = "I withdraw 40.";
let baseline =
derive_oath_baseline(before, &parse("w.md", before), &plan_of(before, ®(true)));
let after = "I withdrraw 40.";
let after_doc = parse("w.md", after);
assert_eq!(
vec!["I withdraw 40@1".to_string()],
bare(&detect_drift(Some(&baseline), &after_doc, &plan_of(after, ®(true))))
);
}
#[test]
fn a_deleted_paragraph_is_not_drift() {
let before = "I withdraw 40.";
let baseline =
derive_oath_baseline(before, &parse("w.md", before), &plan_of(before, ®(true)));
let after_doc = parse("w.md", "");
assert!(detect_drift(Some(&baseline), &after_doc, &plan_of("", ®(true))).is_empty());
}
#[test]
fn moving_and_rewording_a_still_matching_example_does_not_drift() {
let before = "I withdraw 40.\n\nI withdraw 10.";
let baseline =
derive_oath_baseline(before, &parse("w.md", before), &plan_of(before, ®(true)));
let after = "I withdraw 11.\n\nI withdraw 40.";
assert!(
detect_drift(Some(&baseline), &parse("w.md", after), &plan_of(after, ®(true)))
.is_empty()
);
}
#[test]
fn move_reword_prose_on_old_line_does_not_false_positive() {
let before = "I withdraw 40.";
let baseline =
derive_oath_baseline(before, &parse("w.md", before), &plan_of(before, ®(true)));
let after = "Just some notes.\n\nI withdraw 41.";
assert!(
detect_drift(Some(&baseline), &parse("w.md", after), &plan_of(after, ®(true)))
.is_empty()
);
}
#[test]
fn a_paragraph_rewritten_past_recognition_is_not_drift() {
let before = "I withdraw 40.";
let baseline =
derive_oath_baseline(before, &parse("w.md", before), &plan_of(before, ®(true)));
let after = "The branch closed years ago.";
assert!(
detect_drift(Some(&baseline), &parse("w.md", after), &plan_of(after, ®(true)))
.is_empty()
);
}
const ROMAN: &str = "Each row gives a decimal and a roman number:\n\n| decimal | roman |\n| ------: | :---- |\n| 3 | III |\n| 9 | IX |\n";
#[test]
fn header_bound_table_records_its_binding_paragraph_once() {
let doc = parse("r.md", ROMAN);
assert_eq!(
vec![BaselineExample {
name: "Each row gives a decimal and a roman number:".to_string(),
line: 1
}],
live_examples(&doc, &plan(&doc, &roman_reg(true), &empty_workspace()))
);
}
#[test]
fn a_header_bound_binding_paragraph_that_stops_matching_drifts() {
let doc = parse("r.md", ROMAN);
let baseline =
derive_oath_baseline(ROMAN, &doc, &plan(&doc, &roman_reg(true), &empty_workspace()));
assert_eq!(
vec!["Each row gives a decimal and a roman number:@1".to_string()],
bare(&detect_drift(
Some(&baseline),
&doc,
&plan(&doc, &roman_reg(false), &empty_workspace())
))
);
}
#[test]
fn reconcile_records_then_reports_and_preserves_on_drift() {
let source = "I withdraw 40.";
let doc = parse("w.md", source);
let mut store = MemoryStore::default();
assert!(
reconcile_drift(&mut store, "w.md", source, &doc, &plan_of(source, ®(true)), false)
.is_empty()
);
let before_lock = store.contents.clone();
let drifts =
reconcile_drift(&mut store, "w.md", source, &doc, &plan_of(source, ®(false)), false);
assert_eq!(vec!["I withdraw 40@1".to_string()], bare(&drifts));
assert_eq!(before_lock, store.contents); }
#[test]
fn reconcile_update_mode_accepts_drift() {
let source = "I withdraw 40.";
let doc = parse("w.md", source);
let mut store = MemoryStore::default();
reconcile_drift(&mut store, "w.md", source, &doc, &plan_of(source, ®(true)), false);
assert!(
reconcile_drift(&mut store, "w.md", source, &doc, &plan_of(source, ®(false)), true)
.is_empty()
);
let lock = parse_lock_file(store.contents.as_ref().unwrap()).unwrap();
assert_eq!(Vec::<BaselineExample>::new(), lock.oaths.get("w.md").unwrap().examples);
}
const EXPECTED_LOCK: &str = "{\n \"version\": 2,\n \"oaths\": {\n \"library.md\": {\n \"sourceHash\": \"fnv1a:1a2b3c4d\",\n \"examples\": [\n {\n \"name\": \"I check out\",\n \"line\": 7\n }\n ]\n }\n }\n}\n";
#[test]
fn stringify_matches_the_typescript_serializer_byte_for_byte() {
assert_eq!(EXPECTED_LOCK, stringify_lock_file(&library_lock()));
}
#[test]
fn parse_round_trips_a_valid_lock() {
let parsed = parse_lock_file(&stringify_lock_file(&library_lock())).unwrap();
assert_eq!("fnv1a:1a2b3c4d", parsed.oaths.get("library.md").unwrap().source_hash);
assert_eq!(
vec![BaselineExample {
name: "I check out".to_string(),
line: 7
}],
parsed.oaths.get("library.md").unwrap().examples
);
}
#[test]
fn parse_rejects_malformed_input() {
assert!(parse_lock_file("not json").is_none());
assert!(parse_lock_file("{}").is_none());
assert!(parse_lock_file("{\"version\":1,\"oaths\":{}}").is_none());
assert!(parse_lock_file("{\"version\":2,\"oaths\":{\"a.md\":{\"examples\":[]}}}").is_none());
}
fn deposit_withdraw_reg(with_deposit: bool) -> Registry {
let mut r = create_registry();
if with_deposit {
r = add_step(
&r,
"I deposit {int}",
"steps.ts",
1,
Handler::noop(),
Some(StepKind::Stimulus),
)
.unwrap();
}
add_step(&r, "I withdraw {int}", "steps.ts", 2, Handler::noop(), Some(StepKind::Stimulus))
.unwrap()
}
#[test]
fn two_paragraphs_that_merge_into_one_example_are_each_a_live_baseline_entry() {
let source = "I deposit 100.\n\nI withdraw 40.";
let doc = parse("w.md", source);
let plan1 = plan(&doc, &deposit_withdraw_reg(true), &empty_workspace());
assert_eq!(1, plan1.examples.len());
assert_eq!(
vec![
BaselineExample {
name: "I deposit 100".to_string(),
line: 1
},
BaselineExample {
name: "I withdraw 40".to_string(),
line: 3
},
],
live_examples(&doc, &plan1)
);
}
#[test]
fn deleting_one_step_def_of_a_merged_example_drifts_only_the_now_prose_paragraph() {
let source = "I deposit 100.\n\nI withdraw 40.";
let doc = parse("w.md", source);
let baseline = derive_oath_baseline(
source,
&doc,
&plan(&doc, &deposit_withdraw_reg(true), &empty_workspace()),
);
let drift = detect_drift(
Some(&baseline),
&doc,
&plan(&doc, &deposit_withdraw_reg(false), &empty_workspace()),
);
assert_eq!(vec!["I deposit 100@1".to_string()], bare(&drift));
}
#[test]
fn drift_message_names_the_paragraph() {
let d = Drifted {
name: "I withdraw 40".to_string(),
line: 1,
span: Span::from_offsets("I withdraw 40.", 0, 13),
};
assert!(message(&d).contains("I withdraw 40"));
assert!(!message(&d).trim().is_empty());
}
fn lock_with_stale_path() -> String {
let source = "I withdraw 40.";
let doc = parse("w.md", source);
let baseline = derive_oath_baseline(source, &doc, &plan(&doc, ®(true), &empty_workspace()));
let mut oaths = BTreeMap::new();
oaths.insert("varar/w.md".to_string(), baseline.clone());
oaths.insert("w.md".to_string(), baseline);
stringify_lock_file(&LockFile { version: 2, oaths })
}
fn keep(paths: &[&str]) -> Vec<String> {
paths.iter().map(|p| (*p).to_string()).collect()
}
#[test]
fn prune_lock_file_keeps_only_the_paths_it_is_given() {
let lock = parse_lock_file(&lock_with_stale_path()).unwrap();
let pruned = prune_lock_file(&lock, &keep(&["varar/w.md"]));
assert_eq!(pruned.oaths.keys().collect::<Vec<_>>(), vec!["varar/w.md"]);
}
#[test]
fn prune_lock_file_is_a_no_op_when_every_path_is_still_live() {
let lock = parse_lock_file(&lock_with_stale_path()).unwrap();
assert_eq!(prune_lock_file(&lock, &keep(&["varar/w.md", "w.md"])), lock);
}
#[test]
fn prune_reports_stale_paths_without_update_and_does_not_write() {
let mut store = MemoryStore {
contents: Some(lock_with_stale_path()),
};
let before = store.contents.clone();
assert_eq!(prune_baselines(&mut store, &keep(&["varar/w.md"]), false), vec!["w.md"]);
assert_eq!(store.contents, before);
}
#[test]
fn prune_drops_stale_paths_under_update() {
let mut store = MemoryStore {
contents: Some(lock_with_stale_path()),
};
assert_eq!(prune_baselines(&mut store, &keep(&["varar/w.md"]), true), vec!["w.md"]);
let lock = parse_lock_file(&store.contents.unwrap()).unwrap();
assert_eq!(lock.oaths.keys().collect::<Vec<_>>(), vec!["varar/w.md"]);
}
#[test]
fn prune_leaves_a_lock_with_no_stale_paths_untouched() {
let mut store = MemoryStore {
contents: Some(lock_with_stale_path()),
};
let before = store.contents.clone();
assert!(prune_baselines(&mut store, &keep(&["varar/w.md", "w.md"]), true).is_empty());
assert_eq!(store.contents, before);
}
#[test]
fn prune_is_a_no_op_when_there_is_no_baseline_yet() {
let mut store = MemoryStore::default();
assert!(prune_baselines(&mut store, &keep(&["varar/w.md"]), true).is_empty());
assert_eq!(store.contents, None);
}