use serde_json::Value;
use crate::common::{SOURCE_BOUNDARIES, Sandbox, stdout};
use crate::fixtures::{NATIVE, SCANNED, empty_folder, pair_at, qualified};
fn with_a_destination(sandbox: &Sandbox, boundary: crate::common::SourceBoundary) -> String {
let mut document: Value =
serde_json::from_str(&pair_at(sandbox, boundary)).expect("a fixture document is JSON");
document["sources"]["notes"] =
serde_json::json!({"plugin": "local-md", "config": empty_folder(sandbox, "notes")});
serde_json::to_string(&document).expect("a document renders")
}
fn bundle(sandbox: &Sandbox) -> Value {
let rendered = stdout(
sandbox
.command()
.arg("schema")
.assert()
.success()
.get_output(),
);
serde_json::from_str(&rendered).expect("the bundle is JSON")
}
fn validates(bundle: &Value, root: &str, document: &Value, what: &str) {
let schema = &bundle["roots"][root];
assert!(schema.is_object(), "the bundle has no root called {root}");
let validator = jsonschema::validator_for(schema)
.unwrap_or_else(|error| panic!("{root} is not a usable schema: {error}"));
let problems: Vec<String> = validator
.iter_errors(document)
.map(|problem| format!(" {} at {}", problem, problem.instance_path()))
.collect();
assert!(
problems.is_empty(),
"`{what}` does not validate against {root}:\n{}\n{}",
problems.join("\n"),
serde_json::to_string_pretty(document).expect("renders")
);
}
#[test]
fn every_verbs_machine_readable_output_validates_against_the_emitted_schema() {
for boundary in SOURCE_BOUNDARIES {
let sandbox = Sandbox::new();
sandbox.project_document(&with_a_destination(&sandbox, boundary));
let bundle = bundle(&sandbox);
for (arguments, root) in [
(
vec!["task", "list", "--json"],
"QueryResponseOfQualifiedTask",
),
(
vec!["task", "show", &qualified(NATIVE, "T-1"), "--json"],
"QueryResponseOfQualifiedTask",
),
(
vec!["project", "list", "--json"],
"QueryResponseOfQualifiedProject",
),
(
vec!["project", "show", &qualified(NATIVE, "P-1"), "--json"],
"QueryResponseOfQualifiedProject",
),
(
vec!["label", "list", "--json"],
"QueryResponseOfQualifiedLabel",
),
(
vec!["task", "deps", &qualified(NATIVE, "T-1"), "--json"],
"QueryResponseOfQualifiedEdge",
),
(
vec![
"task",
"deps",
&qualified(SCANNED, "T-2"),
"--direction",
"depended-on-by",
"--json",
],
"QueryResponseOfQualifiedEdge",
),
(
vec!["project", "deps", &qualified(NATIVE, "P-1"), "--json"],
"QueryResponseOfQualifiedEdge",
),
(
vec!["search", "alpha", "--json"],
"QueryResponseOfSearchHit",
),
] {
let rendered = stdout(
sandbox
.command()
.args(&arguments)
.assert()
.success()
.get_output(),
);
let document: Value = serde_json::from_str(&rendered).unwrap_or_else(|error| {
panic!("{arguments:?} did not emit JSON: {error}\n{rendered}")
});
validates(&bundle, root, &document, &arguments.join(" "));
assert!(
document["items"]
.as_array()
.is_some_and(|items| !items.is_empty()),
"{arguments:?} returned no rows to validate:\n{rendered}"
);
assert!(
document["plan"]["per_source"]
.as_array()
.is_some_and(|plans| !plans.is_empty()),
"every machine-readable response carries the plan:\n{rendered}"
);
}
let copied = stdout(
sandbox
.command()
.args([
"task",
"copy",
&qualified(NATIVE, "T-1"),
"--to",
"notes",
"--json",
])
.assert()
.success()
.get_output(),
);
let copied: Value = serde_json::from_str(&copied).expect("a copy emits JSON");
validates(&bundle, "CopyReport", &copied, "task copy --json");
assert_eq!(
copied["items"][0]["action"], "created",
"the copy has to have done something: {copied}"
);
validates(
&bundle,
"CopyOutcome",
&copied["items"][0],
"one entry of a copy report",
);
let copied_project = stdout(
sandbox
.command()
.args([
"project",
"copy",
&qualified(NATIVE, "P-1"),
"--to",
"notes",
"--no-tasks",
"--json",
])
.assert()
.success()
.get_output(),
);
let copied_project: Value =
serde_json::from_str(&copied_project).expect("a project copy emits JSON");
validates(
&bundle,
"CopyReport",
&copied_project,
"project copy --json",
);
assert_eq!(
copied_project["items"][0]["action"], "created",
"the project copy has to have done something: {copied_project}"
);
let effective = stdout(
sandbox
.command()
.args(["config", "show", "--json"])
.assert()
.success()
.get_output(),
);
let effective: Value = serde_json::from_str(&effective).expect("JSON");
validates(&bundle, "EffectiveConfig", &effective, "config show --json");
let listings = stdout(
sandbox
.command()
.args(["sources", "list", "--json"])
.assert()
.success()
.get_output(),
);
let listings: Value = serde_json::from_str(&listings).expect("JSON");
for listing in listings.as_array().expect("an array of listings") {
validates(&bundle, "SourceListing", listing, "sources list --json");
}
}
}
#[test]
fn the_plan_a_machine_reads_and_the_plan_a_person_reads_say_the_same_thing() {
for boundary in SOURCE_BOUNDARIES {
let sandbox = Sandbox::new();
sandbox.project_document(&pair_at(&sandbox, boundary));
let explained = stdout(
sandbox
.command()
.args(["task", "list", "--label", "bug", "--explain"])
.assert()
.success()
.get_output(),
);
let machine: Value = serde_json::from_str(&stdout(
sandbox
.command()
.args(["task", "list", "--label", "bug", "--json"])
.assert()
.success()
.get_output(),
))
.expect("JSON");
for entry in machine["plan"]["per_source"]
.as_array()
.expect("one entry per source")
{
let source = entry["source"].as_str().expect("a source name");
assert!(
explained.contains(source),
"the rendered plan omits {source}:\n{explained}"
);
for (field, label) in [
("pushed_down", "pushed down"),
("applied_locally", "applied locally"),
] {
for predicate in entry[field].as_array().expect("a list of predicates") {
let predicate = predicate.as_str().expect("a predicate name");
assert!(
explained.contains(&format!("{label}: {predicate}")),
"the rendered plan does not say `{label}: {predicate}`:\n{explained}"
);
}
}
}
let paged = stdout(
sandbox
.command()
.args(["task", "list", "--limit", "2"])
.assert()
.success()
.get_output(),
);
let token = paged
.lines()
.find_map(|line| line.strip_prefix("next page: --page "))
.expect("a walk longer than the limit reports where to resume");
let machine: Value = serde_json::from_str(&stdout(
sandbox
.command()
.args(["task", "list", "--limit", "2", "--json"])
.assert()
.success()
.get_output(),
))
.expect("JSON");
assert_eq!(machine["next"].as_str(), Some(token));
}
}