tomlsmith-cli 0.2.0

Command-line adapter for TomlSmith
Documentation
use std::{
    fs,
    io::Write,
    path::{Path, PathBuf},
    process::{Command, Stdio},
    sync::atomic::{AtomicU64, Ordering},
};

static NEXT_TEMP_DIRECTORY: AtomicU64 = AtomicU64::new(0);

struct TempDirectory(PathBuf);

impl TempDirectory {
    fn new() -> Self {
        let sequence = NEXT_TEMP_DIRECTORY.fetch_add(1, Ordering::Relaxed);
        let path = std::env::temp_dir().join(format!(
            "tomlsmith-cli-test-{}-{sequence}",
            std::process::id()
        ));
        fs::create_dir(&path).expect("temporary directory should be created");
        Self(path)
    }

    fn path(&self) -> &Path {
        &self.0
    }
}

impl Drop for TempDirectory {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.0);
    }
}

#[test]
fn fmt_check_reports_a_changed_file_without_rewriting_it() {
    let directory = TempDirectory::new();
    let path = directory.path().join("needs-formatting.toml");
    let original = "name=\"TomlSmith\"\n";
    fs::write(&path, original).expect("fixture should be written");

    let output = Command::new(env!("CARGO_BIN_EXE_tomlsmith"))
        .args(["fmt", "--check"])
        .arg(&path)
        .output()
        .expect("tomlsmith process should start");

    assert_eq!(output.status.code(), Some(1), "{output:?}");
    assert!(output.stdout.is_empty(), "{output:?}");
    assert_eq!(
        fs::read_to_string(&path).expect("fixture should remain readable"),
        original,
        "fmt --check must not modify the input file",
    );
}

#[test]
fn check_uses_process_exit_one_for_invalid_toml() {
    let directory = TempDirectory::new();
    let path = directory.path().join("invalid.toml");
    fs::write(&path, "broken\n").expect("fixture should be written");

    let output = Command::new(env!("CARGO_BIN_EXE_tomlsmith"))
        .arg("check")
        .arg(&path)
        .output()
        .expect("tomlsmith process should start");

    assert_eq!(output.status.code(), Some(1), "{output:?}");
    assert!(output.stdout.is_empty(), "{output:?}");
    let stderr = String::from_utf8(output.stderr).expect("diagnostics should be UTF-8");
    assert!(stderr.contains("parse.missing-equals"), "{stderr:?}");
}

#[test]
fn check_uses_process_exit_one_for_invalid_utf8() {
    let directory = TempDirectory::new();
    let path = directory.path().join("invalid-utf8.toml");
    fs::write(&path, [0xff]).expect("fixture should be written");

    let output = Command::new(env!("CARGO_BIN_EXE_tomlsmith"))
        .arg("check")
        .arg(&path)
        .output()
        .expect("tomlsmith process should start");

    assert_eq!(output.status.code(), Some(1), "{output:?}");
    assert!(output.stdout.is_empty(), "{output:?}");
    let stderr = String::from_utf8(output.stderr).expect("diagnostics should be UTF-8");
    assert!(stderr.contains("parse.invalid-utf8"), "{stderr:?}");
}

#[test]
fn fmt_rewrites_a_file_when_check_is_not_requested() {
    let directory = TempDirectory::new();
    let path = directory.path().join("format-me.toml");
    fs::write(&path, "name=\"TomlSmith\"\n").expect("fixture should be written");

    let output = Command::new(env!("CARGO_BIN_EXE_tomlsmith"))
        .arg("fmt")
        .arg(&path)
        .output()
        .expect("tomlsmith process should start");

    assert_eq!(output.status.code(), Some(0), "{output:?}");
    assert!(output.stdout.is_empty(), "{output:?}");
    assert!(output.stderr.is_empty(), "{output:?}");
    assert_eq!(
        fs::read_to_string(path).expect("formatted file should be readable"),
        "name = \"TomlSmith\"\n",
    );
}

#[cfg(unix)]
#[test]
fn fmt_preserves_a_symbolic_link_and_rewrites_its_target() {
    use std::os::unix::fs::symlink;

    let directory = TempDirectory::new();
    let target = directory.path().join("target.toml");
    let link = directory.path().join("linked.toml");
    fs::write(&target, "name=\"TomlSmith\"\n").expect("fixture should be written");
    symlink(&target, &link).expect("symbolic link should be created");

    let output = Command::new(env!("CARGO_BIN_EXE_tomlsmith"))
        .arg("fmt")
        .arg(&link)
        .output()
        .expect("tomlsmith process should start");

    assert_eq!(output.status.code(), Some(0), "{output:?}");
    assert!(output.stdout.is_empty(), "{output:?}");
    assert!(output.stderr.is_empty(), "{output:?}");
    assert!(
        fs::symlink_metadata(&link)
            .expect("symbolic link should remain")
            .file_type()
            .is_symlink(),
        "fmt must not replace the symbolic link itself",
    );
    assert_eq!(
        fs::read_to_string(target).expect("target should remain readable"),
        "name = \"TomlSmith\"\n",
    );
}

#[cfg(unix)]
#[test]
fn fmt_refuses_a_multiply_linked_file_without_breaking_link_identity() {
    use std::os::unix::fs::MetadataExt;

    let directory = TempDirectory::new();
    let first = directory.path().join("first.toml");
    let second = directory.path().join("second.toml");
    let original = "name=\"TomlSmith\"\n";
    fs::write(&first, original).expect("fixture should be written");
    fs::hard_link(&first, &second).expect("hard link should be created");
    let inode = fs::metadata(&first)
        .expect("metadata should be readable")
        .ino();

    let output = Command::new(env!("CARGO_BIN_EXE_tomlsmith"))
        .arg("fmt")
        .arg(&first)
        .output()
        .expect("tomlsmith process should start");

    assert_eq!(output.status.code(), Some(2), "{output:?}");
    assert!(output.stdout.is_empty(), "{output:?}");
    let stderr = String::from_utf8(output.stderr).expect("error should be UTF-8");
    assert!(stderr.contains("multiple hard links"), "{stderr:?}");
    assert_eq!(fs::read_to_string(&first).unwrap(), original);
    assert_eq!(fs::read_to_string(&second).unwrap(), original);
    assert_eq!(fs::metadata(&first).unwrap().ino(), inode);
    assert_eq!(fs::metadata(&second).unwrap().ino(), inode);
}

#[test]
fn parse_reads_a_file_and_emits_json() {
    let directory = TempDirectory::new();
    let path = directory.path().join("valid.toml");
    fs::write(&path, "name = \"TomlSmith\"\n").expect("fixture should be written");

    let output = Command::new(env!("CARGO_BIN_EXE_tomlsmith"))
        .args(["parse", "--toml-version", "1.0"])
        .arg(&path)
        .output()
        .expect("tomlsmith process should start");

    assert_eq!(output.status.code(), Some(0), "{output:?}");
    assert!(output.stderr.is_empty(), "{output:?}");
    let json: serde_json::Value =
        serde_json::from_slice(&output.stdout).expect("parse output should be JSON");
    assert_eq!(json["tomlVersion"], "1.0");
    assert_eq!(json["valid"], true);
}

#[test]
fn unsupported_toml_version_is_a_usage_error() {
    let output = Command::new(env!("CARGO_BIN_EXE_tomlsmith"))
        .args(["check", "--toml-version", "0.5"])
        .output()
        .expect("tomlsmith process should start");

    assert_eq!(output.status.code(), Some(2), "{output:?}");
    assert!(output.stdout.is_empty(), "{output:?}");
    let stderr = String::from_utf8(output.stderr).expect("usage errors should be UTF-8");
    assert!(stderr.contains("invalid value '0.5'"), "{stderr:?}");
}

#[test]
fn deeply_nested_dotted_keys_are_rejected_without_crashing() {
    let source = format!("{} = 1\n", vec!["level"; 5_000].join("."));
    let mut child = Command::new(env!("CARGO_BIN_EXE_tomlsmith"))
        .arg("check")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("tomlsmith process should start");
    child
        .stdin
        .take()
        .expect("stdin should be piped")
        .write_all(source.as_bytes())
        .expect("fixture should be written");
    let output = child
        .wait_with_output()
        .expect("tomlsmith process should exit");

    assert_eq!(output.status.code(), Some(1), "{output:?}");
    assert!(output.stdout.is_empty(), "{output:?}");
    let stderr = String::from_utf8(output.stderr).expect("diagnostics should be UTF-8");
    assert!(stderr.contains("parse.nesting-limit"), "{stderr:?}");
}