godlint-cli 0.1.8

Command-line interface for Godlint.
#![allow(clippy::expect_used, clippy::unwrap_used)]

use std::{
    collections::BTreeSet,
    fs,
    path::{Path, PathBuf},
    process::{Command, Output},
};

use serde::Deserialize;

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ExpectedResult {
    #[serde(rename = "exit-code")]
    exit_code: i32,
    #[serde(default)]
    stdout: String,
    #[serde(default)]
    stderr: String,
}

macro_rules! fixture_tests {
    ($($name:ident => $directory:literal),+ $(,)?) => {
        const DECLARED: &[&str] = &[$($directory),+];

        $(
            #[test]
            fn $name() {
                assert_fixture($directory);
            }
        )+
    };
}

fixture_tests! {
    accountable_suppression => "accountable-suppression",
    clean_repository => "clean",
    decision_complexity => "decision-complexity",
    documented_empty_body => "documented-empty-body",
    else_if_chain => "else-if-chain",
    empty_function => "empty-function",
    enclosing_scope => "enclosing-scope",
    excluded_path => "excluded-path",
    file_size => "file-size",
    function_nesting => "function-nesting",
    function_size => "function-size",
    function_statements => "function-statements",
    invalid_syntax => "invalid-syntax",
    marker_word_boundary => "marker-word-boundary",
    multiway_branch => "multiway-branch",
    no_comments => "no-comments",
    no_comments_strict => "no-comments-strict",
    parameter_count => "parameter-count",
    receiver_parameters => "receiver-parameters",
    return_count => "return-count",
    restricted_call => "restricted-call",
    restricted_call_clean => "restricted-call-clean",
    no_dynamic_execution => "no-dynamic-execution",
    no_dynamic_execution_clean => "no-dynamic-execution-clean",
    direct_environment_read => "direct-environment-read",
    direct_environment_read_clean => "direct-environment-read-clean",
    explicit_timer_delay => "explicit-timer-delay",
    no_production_log => "no-production-log",
    dependency_boundary => "dependency-boundary",
    filename_case => "filename-case",
    filename_case_clean => "filename-case-clean",
    forbidden_dependency => "forbidden-dependency",
    forbidden_dependency_clean => "forbidden-dependency-clean",
    dependency_boundary_clean => "dependency-boundary-clean",
    restricted_import => "restricted-import",
    restricted_import_clean => "restricted-import-clean",
    no_production_log_clean => "no-production-log-clean",
    explicit_timer_delay_clean => "explicit-timer-delay-clean",
    rust_try_operator => "rust-try-operator",
    severity_below_threshold => "severity-below-threshold",
    suppression_applies => "suppression-applies",
    todo_requires_reference => "todo-requires-reference",
    unused_suppression => "unused-suppression",
    unused_suppression_clean => "unused-suppression-clean",
}

#[test]
fn every_fixture_directory_is_declared() {
    let present: BTreeSet<String> = fs::read_dir(fixtures_root())
        .unwrap_or_else(|error| panic!("reads fixtures: {error}"))
        .map(|entry| entry.unwrap_or_else(|error| panic!("reads entry: {error}")))
        .filter(|entry| entry.path().is_dir())
        .map(|entry| entry.file_name().to_string_lossy().into_owned())
        .collect();
    let declared: BTreeSet<String> = DECLARED.iter().map(|name| (*name).to_owned()).collect();

    assert_eq!(present, declared, "fixture directories and tests disagree");
}

fn fixtures_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/rules")
}

fn assert_fixture(directory: &str) {
    let fixture = fixtures_root().join(directory);
    let expected = expected_result(&fixture);
    let output = run(&fixture);

    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        expected.stdout,
        "{directory}: stdout"
    );
    assert_eq!(
        String::from_utf8_lossy(&output.stderr),
        expected.stderr,
        "{directory}: stderr"
    );
    assert_eq!(
        output.status.code(),
        Some(expected.exit_code),
        "{directory}: exit code"
    );
}

fn expected_result(fixture: &Path) -> ExpectedResult {
    let path = fixture.join("expected.yaml");
    let source = fs::read_to_string(&path)
        .unwrap_or_else(|error| panic!("reads {}: {error}", path.display()));

    yaml_serde::from_str(&source)
        .unwrap_or_else(|error| panic!("parses {}: {error}", path.display()))
}

fn run(fixture: &Path) -> Output {
    Command::new(env!("CARGO_BIN_EXE_godlint"))
        .current_dir(fixture)
        .args(["check", "."])
        .output()
        .unwrap_or_else(|error| panic!("runs godlint: {error}"))
}