use assert_cmd::assert::OutputAssertExt;
use assert_cmd::cargo_bin_cmd;
use predicates::prelude::predicate;
use serde_json::Value;
const CLEAN_GLOB: &str = "tests/testdata/verbosity/clean/**";
const FAILING_GLOB: &str = "tests/testdata/verbosity/failing/**";
const MIXED_GLOB: &str = "tests/testdata/verbosity/mixed/**";
const AFFECTS_GLOB: &str = "tests/testdata/verbosity/affects/**";
const DIFF_TOUCHING_THE_AFFECTS_PAIR: &str = r#"
diff --git a/tests/testdata/verbosity/affects/pair.py b/tests/testdata/verbosity/affects/pair.py
index 1111111..2222222 100644
--- a/tests/testdata/verbosity/affects/pair.py
+++ b/tests/testdata/verbosity/affects/pair.py
@@ -1,7 +1,7 @@
# <block name="source" affects=":target">
-SOURCE = "old"
+SOURCE = "value"
# </block>
# <block name="target">
-TARGET = "old"
+TARGET = "value"
# </block>"#;
const DIFF_TOUCHING_ONLY_THE_AFFECTS_TARGET: &str = r#"
diff --git a/tests/testdata/verbosity/affects/pair.py b/tests/testdata/verbosity/affects/pair.py
index 1111111..2222222 100644
--- a/tests/testdata/verbosity/affects/pair.py
+++ b/tests/testdata/verbosity/affects/pair.py
@@ -4,4 +4,4 @@
# <block name="target">
-TARGET = "old"
+TARGET = "value"
# </block>"#;
#[test]
fn without_the_flag_stdout_stays_empty() {
let mut cmd = cargo_bin_cmd!();
cmd.arg(CLEAN_GLOB);
let output = cmd.output().unwrap();
output.assert().success().stdout(predicate::str::is_empty());
}
#[test]
fn summary_level_prints_one_line_of_counts() {
let mut cmd = cargo_bin_cmd!();
cmd.arg(CLEAN_GLOB).arg("--verbosity").arg("summary");
let output = cmd.output().unwrap();
output
.assert()
.success()
.stdout(
"blockwatch: mode=all, 3/3 files, 3 blocks (1 unchecked, 0 needs --diff), 2 checks, 0 violations\n",
);
}
#[test]
fn summary_level_with_no_matching_files_reports_zeroes() {
let mut cmd = cargo_bin_cmd!();
cmd.arg("tests/testdata/no_such_directory/**")
.arg("--verbosity")
.arg("summary");
let output = cmd.output().unwrap();
output
.assert()
.success()
.stdout(
"blockwatch: mode=all, 0/0 files, 0 blocks (0 unchecked, 0 needs --diff), 0 checks, 0 violations\n",
);
}
#[test]
fn summary_level_without_a_diff_counts_the_rules_it_could_not_check() {
let mut cmd = cargo_bin_cmd!();
cmd.arg(AFFECTS_GLOB).arg("--verbosity").arg("summary");
let output = cmd.output().unwrap();
output.assert().success().stdout(
"blockwatch: mode=all, 1/1 files, 2 blocks (1 unchecked, 1 needs --diff), 1 checks, 0 violations\n",
);
}
#[test]
fn summary_level_with_the_diff_gated_validator_disabled_counts_no_rules_as_unchecked() {
let mut cmd = cargo_bin_cmd!();
cmd.arg(AFFECTS_GLOB)
.arg("--disable=affects")
.arg("--verbosity")
.arg("summary");
let output = cmd.output().unwrap();
output.assert().success().stdout(
"blockwatch: mode=all, 1/1 files, 2 blocks (2 unchecked, 0 needs --diff), 0 checks, 0 violations\n",
);
}
#[test]
fn summary_level_with_a_diff_reports_the_all_plus_diff_mode() {
let mut cmd = cargo_bin_cmd!();
cmd.arg("--diff")
.arg(AFFECTS_GLOB)
.arg("--verbosity")
.arg("summary")
.write_stdin(DIFF_TOUCHING_THE_AFFECTS_PAIR);
let output = cmd.output().unwrap();
output.assert().success().stdout(
"blockwatch: mode=all+diff, 1/1 files, 2 blocks (1 unchecked), 1 checks, 0 violations\n",
);
}
#[test]
fn summary_level_with_a_diff_that_misses_the_rule_omits_the_needs_diff_clause() {
let mut cmd = cargo_bin_cmd!();
cmd.arg("--diff")
.arg(AFFECTS_GLOB)
.arg("--verbosity")
.arg("summary")
.write_stdin(DIFF_TOUCHING_ONLY_THE_AFFECTS_TARGET);
let output = cmd.output().unwrap();
output.assert().success().stdout(
"blockwatch: mode=all+diff, 1/1 files, 2 blocks (1 unchecked), 1 checks, 0 violations\n",
);
}
#[test]
fn summary_level_with_only_changed_reports_the_only_changed_mode() {
let mut cmd = cargo_bin_cmd!();
cmd.args(["--diff", "--only-changed"])
.arg("--verbosity")
.arg("summary")
.write_stdin(DIFF_TOUCHING_THE_AFFECTS_PAIR);
let output = cmd.output().unwrap();
output.assert().success().stdout(
"blockwatch: mode=only-changed, 1/1 files, 2 blocks (1 unchecked), 1 checks, 0 violations\n",
);
}
#[test]
fn full_level_lists_every_block_and_the_validators_that_examined_it() {
let mut cmd = cargo_bin_cmd!();
cmd.arg(CLEAN_GLOB).arg("--verbosity").arg("full");
let output = cmd.output().unwrap();
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
output.assert().success();
assert_eq!(report["summary"]["blocks"], 3);
assert_eq!(report["summary"]["checks"], 2);
assert_eq!(report["summary"]["files_skipped"], 1);
assert_eq!(
report["files"]["tests/testdata/verbosity/clean/sorted.py"][0]["checks"],
serde_json::json!(["keep-sorted"])
);
}
#[test]
fn full_level_names_the_count_of_blocks_that_needed_a_diff() {
let mut cmd = cargo_bin_cmd!();
cmd.arg(AFFECTS_GLOB).arg("--verbosity").arg("full");
let output = cmd.output().unwrap();
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
output.assert().success();
assert_eq!(report["summary"]["blocks_needing_diff"], 1);
}
#[test]
fn full_level_under_a_diff_omits_the_needs_diff_key() {
let mut cmd = cargo_bin_cmd!();
cmd.arg("--diff")
.arg(AFFECTS_GLOB)
.arg("--verbosity")
.arg("full")
.write_stdin(DIFF_TOUCHING_ONLY_THE_AFFECTS_TARGET);
let output = cmd.output().unwrap();
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
output.assert().success();
assert_eq!(report["summary"].get("blocks_needing_diff"), None);
assert_eq!(report["summary"]["blocks_unchecked"], 1);
}
#[test]
fn full_level_shows_a_block_no_validator_examined() {
let mut cmd = cargo_bin_cmd!();
cmd.arg(CLEAN_GLOB).arg("--verbosity").arg("full");
let output = cmd.output().unwrap();
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
let block = &report["files"]["tests/testdata/verbosity/clean/noop.py"][0];
assert_eq!(block["name"], "noop");
assert_eq!(block["checks"], serde_json::json!([]));
assert_eq!(report["summary"]["blocks_unchecked"], 1);
}
#[test]
fn full_level_with_violations_writes_two_parseable_documents() {
let mut cmd = cargo_bin_cmd!();
cmd.arg(FAILING_GLOB).arg("--verbosity").arg("full");
let output = cmd.output().unwrap();
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
let violations: Value = serde_json::from_slice(&output.stderr).unwrap();
output.assert().failure().code(1);
assert_eq!(report["summary"]["violations"], 1);
assert_eq!(
report["files"]["tests/testdata/verbosity/failing/unsorted.py"][0]["checks"],
serde_json::json!(["keep-sorted"])
);
assert!(
violations["tests/testdata/verbosity/failing/unsorted.py"]
.as_array()
.is_some_and(|entries| !entries.is_empty())
);
}
const DIFF_TOUCHING_ONE_BLOCK: &str = r#"
diff --git a/tests/testdata/verbosity/diff/two_blocks.py b/tests/testdata/verbosity/diff/two_blocks.py
index 1111111..2222222 100644
--- a/tests/testdata/verbosity/diff/two_blocks.py
+++ b/tests/testdata/verbosity/diff/two_blocks.py
@@ -1,6 +1,6 @@
fruits = [
# <block name="fruits" keep-sorted="asc">
'apple',
- 'blueberry',
+ 'banana',
# </block>
]"#;
#[test]
fn full_level_under_a_diff_describes_only_the_blocks_in_scope() {
let mut cmd = cargo_bin_cmd!();
cmd.args(["--diff", "--only-changed"])
.arg("--verbosity")
.arg("full")
.write_stdin(DIFF_TOUCHING_ONE_BLOCK);
let output = cmd.output().unwrap();
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
output.assert().success();
let blocks = report["files"]["tests/testdata/verbosity/diff/two_blocks.py"]
.as_array()
.expect("the touched file is reported");
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0]["name"], "fruits");
assert_eq!(blocks[0]["is_content_modified"], true);
assert_eq!(blocks[0]["checks"], serde_json::json!(["keep-sorted"]));
assert_eq!(report["summary"]["mode"], "only-changed");
assert_eq!(report["summary"]["files_scanned"], 1);
assert_eq!(report["summary"]["blocks"], 1);
assert_eq!(report["summary"]["blocks_unchecked"], 0);
assert_eq!(report["summary"]["checks"], 1);
}
#[test]
fn summary_level_under_a_diff_counts_only_the_blocks_in_scope() {
let mut cmd = cargo_bin_cmd!();
cmd.args(["--diff", "--only-changed"])
.arg("--verbosity")
.arg("summary")
.write_stdin(DIFF_TOUCHING_ONE_BLOCK);
let output = cmd.output().unwrap();
output
.assert()
.success()
.stdout(
"blockwatch: mode=only-changed, 1/1 files, 1 blocks (0 unchecked), 1 checks, 0 violations\n",
);
}
#[test]
fn diff_mode_with_a_violation_writes_the_report_and_the_violations() {
let diff = r#"
diff --git a/tests/testdata/verbosity/failing/unsorted.py b/tests/testdata/verbosity/failing/unsorted.py
index 1111111..2222222 100644
--- a/tests/testdata/verbosity/failing/unsorted.py
+++ b/tests/testdata/verbosity/failing/unsorted.py
@@ -1,6 +1,6 @@
vegetables = [
# <block name="vegetables" keep-sorted="asc">
'tomato',
- 'squash',
+ 'potato',
# </block>
]"#;
let mut cmd = cargo_bin_cmd!();
cmd.args(["--diff", "--only-changed"])
.arg("--verbosity")
.arg("full")
.write_stdin(diff);
let output = cmd.output().unwrap();
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
let violations: Value = serde_json::from_slice(&output.stderr).unwrap();
output.assert().failure().code(1);
assert_eq!(report["summary"]["violations"], 1);
assert_eq!(
report["files"]["tests/testdata/verbosity/failing/unsorted.py"][0]["checks"],
serde_json::json!(["keep-sorted"])
);
assert!(
violations["tests/testdata/verbosity/failing/unsorted.py"]
.as_array()
.is_some_and(|entries| !entries.is_empty())
);
}
#[test]
fn full_level_under_a_diff_omits_a_reference_target_the_diff_did_not_touch() {
let diff = r#"
diff --git a/tests/testdata/same_as/source.rs b/tests/testdata/same_as/source.rs
index 1111111..2222222 100644
--- a/tests/testdata/same_as/source.rs
+++ b/tests/testdata/same_as/source.rs
@@ -1,3 +1,3 @@
// <block same-as="tests/testdata/same_as/target.md:port">
-const PORT: u16 = 8000;
+const PORT: u16 = 8080;
// </block>"#;
let mut cmd = cargo_bin_cmd!();
cmd.args(["--diff", "--only-changed"])
.arg("--verbosity")
.arg("full")
.write_stdin(diff);
let output = cmd.output().unwrap();
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
output.assert().success();
assert!(report["files"]["tests/testdata/same_as/target.md"].is_null());
assert_eq!(report["summary"]["files_scanned"], 1);
assert_eq!(report["summary"]["blocks"], 1);
assert_eq!(
report["files"]["tests/testdata/same_as/source.rs"][0]["checks"],
serde_json::json!(["same-as"])
);
}
#[test]
fn full_level_names_both_a_sync_and_an_async_validator_of_one_block() {
let mut cmd = cargo_bin_cmd!();
cmd.arg(MIXED_GLOB).arg("--verbosity").arg("full");
let output = cmd.output().unwrap();
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
output.assert().success();
assert_eq!(
report["files"]["tests/testdata/verbosity/mixed/both_kinds.py"][0]["checks"],
serde_json::json!(["check-lua", "keep-sorted"])
);
assert_eq!(report["summary"]["checks"], 2);
assert_eq!(
report["summary"]["validators"],
serde_json::json!({ "check-lua": 1, "keep-sorted": 1 })
);
}
#[test]
fn verbosity_with_the_list_subcommand_fails() {
let mut cmd = cargo_bin_cmd!();
cmd.arg("list").arg("--verbosity").arg("full");
let output = cmd.output().unwrap();
output
.assert()
.failure()
.stderr(predicate::str::contains("`list` subcommand"));
}