blockwatch 0.5.3

Language agnostic linter that keeps your code and documentation in sync and valid
Documentation
use assert_cmd::assert::OutputAssertExt;
use assert_cmd::cargo_bin_cmd;
use predicates::prelude::predicate;
use serde_json::json;

const LUA_MODE_ENV_VAR: &str = "BLOCKWATCH_LUA_MODE";

/// A one-block fixture whose Lua checker calls into `os`.
const MODE_FIXTURE: &str = "tests/testdata/check_lua/mode.py";

#[test]
fn with_valid_lua_script_succeeds() {
    let diff_content = r#"
diff --git a/tests/testdata/check_lua/success_and_failure.py b/tests/testdata/check_lua/success_and_failure.py
index 1111111..2222222 100644
--- a/tests/testdata/check_lua/success_and_failure.py
+++ b/tests/testdata/check_lua/success_and_failure.py
@@ -1,7 +1,7 @@
 colors = [
     # <block check-lua="tests/testdata/check_lua/success.lua">
     'red',
-    'green',
+    'yellow',
     'blue',
     # </block>
 ]
"#;

    let mut cmd = cargo_bin_cmd!();
    cmd.args(["--diff", "--only-changed"]);
    let output = cmd.write_stdin(diff_content).output().unwrap();

    output.assert().success();
}

#[test]
fn with_failing_lua_script_fails() {
    let diff_content = r#"
diff --git a/tests/testdata/check_lua/success_and_failure.py b/tests/testdata/check_lua/success_and_failure.py
index 1111111..2222222 100644
--- a/tests/testdata/check_lua/success_and_failure.py
+++ b/tests/testdata/check_lua/success_and_failure.py
@@ -9,7 +9,7 @@
 numbers = [
     # <block check-lua="tests/testdata/check_lua/fail.lua">
     '1',
-    '2',
+    '4',
     '3',
     # </block>
 ]
"#;

    let mut cmd = cargo_bin_cmd!();
    cmd.args(["--diff", "--only-changed"]);
    let output = cmd.write_stdin(diff_content).output().unwrap();

    output
        .assert()
        .failure()
        .code(1)
        .stderr(predicate::function(|output: &str| {
            let output_json: serde_json::Value =
                serde_json::from_str(output).expect("invalid json");
            let value: serde_json::Value = json!({
              "tests/testdata/check_lua/success_and_failure.py": [
                {
                  "range": {
                    "start": {
                        "line": 10,
                        "character": 7
                    },
                    "end": {
                        "line": 10,
                        "character": 59
                    }
                  },
                  "code": "check-lua",
                  "message": "Block tests/testdata/check_lua/success_and_failure.py:(unnamed) defined at line 10 failed Lua check: block content is invalid",
                  "severity": 1,
                  "data": {
                    "script": "tests/testdata/check_lua/fail.lua",
                    "lua_error": "block content is invalid"
                  }
                }
              ]
            });
            assert_eq!(output_json, value);
            true
        }));
}

#[test]
fn with_pattern_extracts_matching_content() {
    let mut cmd = cargo_bin_cmd!();
    cmd.arg("tests/testdata/check_lua/pattern.py");
    let output = cmd.output().unwrap();

    output.assert().success();
}

#[test]
fn with_pattern_passes_every_match_in_the_block() {
    let mut cmd = cargo_bin_cmd!();
    cmd.arg("tests/testdata/check_lua/pattern_many_matches.py");
    let output = cmd.output().unwrap();

    output.assert().success();
}

#[test]
fn with_pattern_no_match_passes_empty_content() {
    let mut cmd = cargo_bin_cmd!();
    cmd.arg("tests/testdata/check_lua/pattern_no_match.py");
    let output = cmd.output().unwrap();

    output.assert().success();
}

#[test]
fn lua_script_using_os_fails_in_sandboxed_mode() {
    let mut cmd = cargo_bin_cmd!();
    cmd.arg(MODE_FIXTURE);
    // Default (sandboxed) mode: os library is not available.
    let output = cmd.output().unwrap();

    output
        .assert()
        .failure()
        .code(1)
        .stderr(predicate::str::contains("os library is not available"));
}

#[test]
fn lua_script_using_os_succeeds_in_safe_mode() {
    let mut cmd = cargo_bin_cmd!();
    cmd.arg(MODE_FIXTURE);
    cmd.env(LUA_MODE_ENV_VAR, "safe");
    let output = cmd.output().unwrap();

    output.assert().success();
}

#[test]
fn ctx_affects_exposes_in_sync_affected_blocks_succeeds() {
    // The diff touches both the check-lua block and the affected docs block so the `affects`
    // validator is satisfied and the check-lua script runs against an in-sync pair.
    let diff_content = r#"
diff --git a/tests/testdata/check_lua/affects.py b/tests/testdata/check_lua/affects.py
index 1111111..2222222 100644
--- a/tests/testdata/check_lua/affects.py
+++ b/tests/testdata/check_lua/affects.py
@@ -3,3 +3,3 @@
     'blue',
-    'yellow',
+    'green',
     'red',
@@ -12,3 +12,3 @@
     'blue',
-    'yellow',
+    'green',
     'red',
"#;

    let mut cmd = cargo_bin_cmd!();
    cmd.args(["--diff", "--only-changed"]);
    let output = cmd.write_stdin(diff_content).output().unwrap();

    output.assert().success();
}

#[test]
fn ctx_affects_detects_out_of_sync_affected_block_fails() {
    // Both blocks are touched (so the `affects` validator passes), but their content has drifted
    // apart on disk, so the check-lua script reports the mismatch via ctx.affects.
    let diff_content = r#"
diff --git a/tests/testdata/check_lua/affects_drift.py b/tests/testdata/check_lua/affects_drift.py
index 1111111..2222222 100644
--- a/tests/testdata/check_lua/affects_drift.py
+++ b/tests/testdata/check_lua/affects_drift.py
@@ -3,3 +3,3 @@
     'blue',
-    'yellow',
+    'green',
     'red',
@@ -12,3 +12,3 @@
     'blue',
-    'orange',
+    'purple',
     'red',
"#;

    let mut cmd = cargo_bin_cmd!();
    cmd.args(["--diff", "--only-changed"]);
    let output = cmd.write_stdin(diff_content).output().unwrap();

    output
        .assert()
        .failure()
        .code(1)
        .stderr(predicate::str::contains(
            "block 'allowed-colors-docs' in tests/testdata/check_lua/affects_drift.py is out of sync",
        ));
}

#[test]
fn lua_script_using_os_succeeds_in_unsafe_mode() {
    let mut cmd = cargo_bin_cmd!();
    cmd.arg(MODE_FIXTURE);
    cmd.env(LUA_MODE_ENV_VAR, "unsafe");
    let output = cmd.output().unwrap();

    output.assert().success();
}

#[cfg(unix)]
#[test]
fn lua_script_blocked_in_io_call_times_out() {
    let mut cmd = cargo_bin_cmd!();
    cmd.arg("tests/testdata/check_lua/blocking_io.py");
    cmd.env(LUA_MODE_ENV_VAR, "unsafe");
    cmd.timeout(std::time::Duration::from_secs(20));
    let output = cmd.output().unwrap();

    output
        .assert()
        .failure()
        .code(1)
        .stderr(predicate::str::contains("timed out"));
}

/// A Lua checker sees the same repository-relative, forward-slash path however the run was scoped.
/// Without this, a checker that branches on paths changes verdict between a glob scan (which uses
/// native separators) and a diff scan (which uses the diff's forward slashes) on Windows.
#[test]
fn ctx_file_is_identical_in_glob_and_diff_modes() {
    let expected = "ctx.file=tests/testdata/check_lua/pathfmt/source.py";

    let mut glob_command = cargo_bin_cmd!();
    let glob_stderr = String::from_utf8(
        glob_command
            .arg("tests/testdata/check_lua/pathfmt/source.py")
            .output()
            .unwrap()
            .stderr,
    )
    .unwrap();

    let diff = "\
diff --git a/tests/testdata/check_lua/pathfmt/source.py b/tests/testdata/check_lua/pathfmt/source.py
--- a/tests/testdata/check_lua/pathfmt/source.py
+++ b/tests/testdata/check_lua/pathfmt/source.py
@@ -1,3 +1,3 @@
 # <block check-lua=\"tests/testdata/check_lua/pathfmt/report_path.lua\">
-value = 1
+value = 2
 # </block>
";
    let mut diff_command = cargo_bin_cmd!();
    diff_command.args(["--diff", "--only-changed"]);
    let diff_stderr =
        String::from_utf8(diff_command.write_stdin(diff).output().unwrap().stderr).unwrap();

    assert!(glob_stderr.contains(expected), "glob mode: {glob_stderr}");
    assert!(diff_stderr.contains(expected), "diff mode: {diff_stderr}");
}