blob: 3bf28bd8d61b501c29fecec23cba5ac356f68c76 [file]
//! `scripts/conformance.py` against this build: the seven M0 steps over the surface the
//! binary serves, on both transports. The script is what another serving layer is held
//! to, so the laptop server passes it first; `scripts/conformance.test.sh` holds that the
//! script's assertions bite, against a fake with one defect at a time.
// An integration test is test code, but clippy's allow-unwrap-in-tests only sees
// #[cfg(test)] modules; here a panic is the failure being reported.
#![allow(clippy::unwrap_used)]
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
fn python() -> String {
for candidate in ["python3", "python"] {
if Command::new(candidate)
.arg("--version")
.output()
.is_ok_and(|o| o.status.success())
{
return candidate.to_string();
}
}
panic!("python3 is needed to run scripts/conformance.py");
}
fn script() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../scripts/conformance.py")
}
/// A path as the script's shell-style splitting reads it: quoted, so a Windows
/// backslash is a character and not an escape.
fn quoted(p: &Path) -> String {
format!("'{}'", p.display())
}
fn init(root: &Path) {
let out = Command::new(env!("CARGO_BIN_EXE_amem"))
.arg("--root")
.arg(root)
.args(["init", "--owner", "human:test:me", "--domain", "dom-conf"])
.output()
.unwrap();
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
}
/// Run the script with `args`; its whole output, which must end on every step passing.
fn conforms(args: &[String]) -> String {
let out = Command::new(python())
.arg(script())
.args(args)
.output()
.unwrap();
let text = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(out.status.success(), "conformance.py failed:\n{text}");
text
}
#[test]
fn the_seven_steps_pass_over_stdio_with_the_operator() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("memory");
init(&root);
let amem = quoted(Path::new(env!("CARGO_BIN_EXE_amem")));
let index = root.join("state").join("index.db");
let text = conforms(&[
"--stdio".into(),
format!("{amem} --root {} serve", quoted(&root)),
"--operator".into(),
format!("{amem} --root {}", quoted(&root)),
"--drop-index".into(),
format!(
"{} -c \"import os; os.remove(r'{}')\"",
python(),
index.display()
),
]);
assert!(text.contains("7 of 7 steps passed"), "{text}");
assert!(
!text.contains("the loss was not shown"),
"the rebuild was shown from a lost index: {text}"
);
}
/// The same seven steps with the ledger on PostgreSQL rather than in the SQLite file:
/// the grants name the server, the binary connects to it, and the tasks the steps file
/// land there. Skipped, saying so, where no server can be had (see
/// `dm_ledger::testing`).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_seven_steps_pass_over_stdio_with_a_postgres_ledger() {
let Some(server) = dm_ledger::testing::PgServer::start().await.unwrap() else {
eprintln!("skipped: the seven steps over a PostgreSQL ledger need a server");
return;
};
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("memory");
init(&root);
let grants = root.join("grants.toml");
let mut text = std::fs::read_to_string(&grants).unwrap();
text.push_str(&format!("\n[ledger]\nurl = \"{}\"\n", server.url()));
std::fs::write(&grants, text).unwrap();
// init opened the file ledger before the URL was there; from here on nothing may
std::fs::remove_file(root.join("state").join("ledger.db")).unwrap();
let amem = quoted(Path::new(env!("CARGO_BIN_EXE_amem")));
let index = root.join("state").join("index.db");
let args = vec![
"--stdio".into(),
format!("{amem} --root {} serve", quoted(&root)),
"--operator".into(),
format!("{amem} --root {}", quoted(&root)),
"--drop-index".into(),
format!(
"{} -c \"import os; os.remove(r'{}')\"",
python(),
index.display()
),
];
let text = tokio::task::spawn_blocking(move || conforms(&args))
.await
.unwrap();
assert!(text.contains("7 of 7 steps passed"), "{text}");
// the tasks are on the server, and nothing was written to the SQLite file
let (client, conn) = tokio_postgres::connect(server.url(), tokio_postgres::NoTls)
.await
.unwrap();
tokio::spawn(conn);
// each in its domain's schema, routed by the directory
let row = client
.query_one(
"SELECT COUNT(*), COUNT(DISTINCT d.schema) FROM public.ledger_tasks t
JOIN public.ledger_domains d ON d.domain = t.domain",
&[],
)
.await
.unwrap();
let tasks: i64 = row.get(0);
let schemas: i64 = row.get(1);
assert!(tasks > 0, "the steps filed tasks on the server");
assert_eq!(schemas, 1, "one domain, one schema");
assert!(
!root.join("state").join("ledger.db").exists(),
"no SQLite ledger was opened once the URL was set"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_seven_steps_pass_over_http() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("memory");
init(&root);
let memory = Arc::new(dm_server::Memory::open(&root).unwrap());
let egress = dm_egress::Egress::new(
memory,
std::collections::HashMap::new(),
&dir.path().join("telemetry.db"),
)
.unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let url = format!("http://{}/mcp", listener.local_addr().unwrap());
let app = egress.router();
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
let amem = quoted(Path::new(env!("CARGO_BIN_EXE_amem")));
let args = vec![
"--url".into(),
url,
"--operator".into(),
format!("{amem} --root {}", quoted(&root)),
];
let text = tokio::task::spawn_blocking(move || conforms(&args))
.await
.unwrap();
assert!(text.contains("7 of 7 steps passed"), "{text}");
}