ecformat 0.2.0

command line tool to keep files correct in respect of your EditorConfig
Documentation
// SPDX-FileCopyrightText: Contributors to ecformat project <https://codeberg.org/BaumiCoder/ecformat>
//
// SPDX-License-Identifier: BlueOak-1.0.0

//! Tests for the `status` command of ecformat.

use std::path::{self, Path, PathBuf};

use ec4rs::{
    language_tags::LanguageTag,
    property::{Charset, FinalNewline, IndentSize, IndentStyle, SpellingLanguage, TrimTrailingWs},
};
use itertools::Itertools;

use ecformat::{
    status,
    status_types::{ConsideredFile, ConsideredFiles, EditorConfigFiles},
};

mod integration_test_utils;

use integration_test_utils::test_utils;

/// Test on the whole ecformat repository itself.
#[test]
fn test_status_ecformat() {
    let repo = test_utils::get_repo();
    let mut args = integration_test_utils::get_command_args(&repo);

    args.ignore_args.hidden = true;

    let status_info = status(&args).expect("Status command has no error");
    // EditorConfig files - Detailed assertion as these are quite stable
    assert_editorconfigs(
        &status_info.editorconfig_files,
        &["", "LICENSES", "tests/resources", ".vscode"],
        &repo,
    );
    // Considered files - Examples a rough overview as there are always changes
    let readme = get_considered_file(&status_info.considered_files, &repo.join("README.md"));
    let main = get_considered_file(
        &status_info.considered_files,
        &repo.join("src").join("main.rs"),
    );
    let this_file = get_considered_file(
        &status_info.considered_files,
        &repo.join("tests").join("status.rs"),
    );
    let examples = vec![readme, main, this_file];

    for file in examples {
        assert_charset(file);
        assert_end_of_line(file);
        assert_indent_style(file, IndentStyle::Spaces);
        assert_tab_width(file);
        assert_insert_final_newline(file, true);
        assert_spelling_language(file);
        assert_trim_trailing_whitespace(file, true);
    }
    // Markdown section [*.md]
    assert_indent_size(readme, 1);
    // common section [*]
    assert_indent_size(main, 4);
    assert_indent_size(this_file, 4);

    let license_dir = repo.join("LICENSES");
    let example_licenses = vec![
        get_considered_file(
            &status_info.considered_files,
            &license_dir.join("BlueOak-1.0.0.txt"),
        ),
        get_considered_file(
            &status_info.considered_files,
            &license_dir.join("CC0-1.0.txt"),
        ),
    ];
    for license in example_licenses {
        assert_all_not_set(license);
    }

    let tests_resources_dir = repo.join("tests").join("resources");
    let trim_trailing_whitespace_example = get_considered_file(
        &status_info.considered_files,
        &tests_resources_dir
            .join("trim_trailing_whitespace")
            .join("trimmed.md"),
    );
    let indent_style_example = get_considered_file(
        &status_info.considered_files,
        &tests_resources_dir
            .join("indent_style")
            .join("mix_allowed-tab-3-4-7-9.md"),
    );
    let indent_size_example = get_considered_file(
        &status_info.considered_files,
        &tests_resources_dir
            .join("indent_size")
            .join("correct_tabs-tab-4-4.md"),
    );
    let insert_final_newline_example = get_considered_file(
        &status_info.considered_files,
        &tests_resources_dir
            .join("insert_final_newline")
            .join("empty-needs_no_final_newline.md"),
    );

    assert_trim_trailing_whitespace(trim_trailing_whitespace_example, false);
    assert_indent_style(indent_style_example, IndentStyle::Tabs);
    assert_indent_style(indent_size_example, IndentStyle::Tabs);
    assert_insert_final_newline(insert_final_newline_example, false);
    assert_trim_trailing_whitespace(insert_final_newline_example, false);

    assert!(
        status_info.considered_files.files().len() > 128,
        "Considered files are ways too few"
    );
}

/// Test on the LICENSES directory of the ecformat repository.
#[test]
fn test_status_ecformat_licenses() {
    let license_dir = test_utils::get_repo().join("LICENSES");
    let mut args = integration_test_utils::get_command_args(&license_dir);

    args.ignore_args.hidden = true;

    let status_info = status(&args).expect("Status command has no error");
    // EditorConfig files - Detailed assertion as these are quite stable
    assert_editorconfigs(&status_info.editorconfig_files, &[""], &license_dir);
    // Considered files - for all in LICENSES they should be no property set
    for file in status_info.considered_files.files() {
        assert_all_not_set(file);
    }
    assert!(
        status_info.considered_files.files().len() > 2,
        "Considered files are too few"
    );
}

/// Test on the `tests/resources` directory of the ecformat repository.
#[test]
fn test_status_ecformat_tests_resources() {
    // Use the relative path for the command call to test its behavior
    // on this kind of path with subdirectory sections in the EditorConfig.
    // Absolute path is the expected base of the returned paths.
    // Regression test for bug: https://codeberg.org/BaumiCoder/ecformat/issues/47
    let tests_resources_dir_relative = test_utils::get_repo_relative()
        .join("tests")
        .join("resources");
    let tests_resources_dir = path::absolute(&tests_resources_dir_relative)
        .expect("Absolute path to tests/resources needs to be available");
    let mut args = integration_test_utils::get_command_args(&tests_resources_dir_relative);

    args.ignore_args.hidden = true;

    let status_info = status(&args).expect("Status command has no error");
    // EditorConfig files - Detailed assertion as these are quite stable
    assert_editorconfigs(&status_info.editorconfig_files, &[""], &tests_resources_dir);
    // Considered files - assert property of the .md files in respective subdirectories
    let mut md_considered = false;
    let trim_trailing_whitespace_dir = tests_resources_dir.join("trim_trailing_whitespace");
    let mut trim_trailing_whitespace_considered = false;
    let indent_style_dir = tests_resources_dir.join("indent_style");
    let mut indent_style_considered = false;
    let indent_size_dir = tests_resources_dir.join("indent_size");
    let mut indent_size_considered = false;
    let insert_final_newline_dir = tests_resources_dir.join("insert_final_newline");
    let mut insert_final_newline_considered = false;

    for file in status_info.considered_files.files() {
        if file.path().extension().is_some_and(|e| e == "md") {
            md_considered = true;
            match file.path().parent().unwrap() {
                dir if dir == trim_trailing_whitespace_dir => {
                    trim_trailing_whitespace_considered = true;
                    assert_trim_trailing_whitespace(file, false);
                }
                dir if dir == indent_style_dir => {
                    indent_style_considered = true;
                    assert_indent_style(file, IndentStyle::Tabs);
                }
                dir if dir == indent_size_dir => {
                    indent_size_considered = true;
                    assert_indent_style(file, IndentStyle::Tabs);
                }
                dir if dir == insert_final_newline_dir => {
                    insert_final_newline_considered = true;
                    assert_insert_final_newline(file, false);
                    assert_trim_trailing_whitespace(file, false);
                }
                _ => (),
            }
            //
        }
    }
    assert!(md_considered, "No md file considered");
    assert!(
        trim_trailing_whitespace_considered,
        "No md file in trim_trailing_whitespace directory considered"
    );
    assert!(
        indent_style_considered,
        "No md file in indent_style directory considered"
    );
    assert!(
        indent_size_considered,
        "No md file in indent_size directory considered"
    );
    assert!(
        insert_final_newline_considered,
        "No md file in insert_final_newline directory considered"
    );
    assert!(
        status_info.considered_files.files().len() > 32,
        "Considered files are ways too few"
    );
}

/// Assert that the given `editorconfig_files` contains only
/// `.editorconfig` files in the `expected` directories (relative to the `target_dir`).
fn assert_editorconfigs(
    editorconfig_files: &EditorConfigFiles,
    expected: &[&str],
    target_dir: &Path,
) {
    let expected_editorconfis = expected
        .iter()
        .map(|dir| target_dir.join(dir).join(".editorconfig"))
        .sorted()
        .collect_vec();
    let found_editoroconfigs = editorconfig_files
        .paths()
        .iter()
        .cloned()
        .sorted()
        .collect_vec();
    assert_eq!(
        expected_editorconfis, found_editoroconfigs,
        "Found EditorConfig files are not as expected"
    );
}

fn get_considered_file<'a>(
    considered_files: &'a ConsideredFiles,
    file: &PathBuf,
) -> &'a ConsideredFile {
    considered_files
        .files()
        .iter()
        .find(|f| f.path() == file)
        .unwrap_or_else(|| panic!("{} needs to be a considered file", file.display()))
}

fn assert_charset(file: &ConsideredFile) {
    match file.charset {
        Some(charset) => assert_eq!(
            charset,
            Charset::Utf8, // most of the repo uses currently this charset
            "charset of {} is {charset} and not {} as expected",
            file.path().display(),
            Charset::Utf8
        ),
        None => panic!("no charset for {} found", file.path().display()),
    }
}

fn assert_end_of_line(file: &ConsideredFile) {
    if let Some(end_of_line) = file.end_of_line {
        // most of the repo uses git to handle the line breaks
        panic!(
            "end_of_line of {} is {end_of_line} and is not unset as expected",
            file.path().display(),
        )
    }
}

fn assert_indent_style(file: &ConsideredFile, expected: IndentStyle) {
    match file.indent_style {
        Some(indent_style) => assert_eq!(
            indent_style,
            expected,
            "indent_style of {} is {indent_style} and not {expected} as expected",
            file.path().display(),
        ),
        None => panic!("no indent_style for {} found", file.path().display()),
    }
}

fn assert_indent_size(file: &ConsideredFile, expected: usize) {
    match file.indent_size {
        Some(indent_size) => assert_eq!(
            indent_size,
            IndentSize::Value(expected),
            "indent_size of {} is {indent_size} and not {} as expected",
            file.path().display(),
            IndentSize::Value(expected),
        ),
        None => panic!("no indent_size for {} found", file.path().display()),
    }
}

fn assert_tab_width(file: &ConsideredFile) {
    if let Some(tab_width) = file.tab_width {
        // most of the repo does not use this property
        panic!(
            "tab_width of {} is {tab_width} and is not unset as expected",
            file.path().display(),
        )
    }
}

fn assert_insert_final_newline(file: &ConsideredFile, expected: bool) {
    match file.insert_final_newline {
        Some(FinalNewline::Value(insert_final_newline)) => assert_eq!(
            insert_final_newline,
            expected,
            "insert_final_newline of {} is set to {insert_final_newline}",
            file.path().display(),
        ),
        None => panic!(
            "no insert_final_newline for {} found",
            file.path().display()
        ),
    }
}

fn assert_spelling_language(file: &ConsideredFile) {
    match file.spelling_language.as_ref() {
        Some(SpellingLanguage::Value(language_tag)) => {
            // most of the repo uses currently this language
            let expected_tag = LanguageTag::parse("en-US").expect("en-US is a valid language tag");
            assert_eq!(
                language_tag,
                &expected_tag,
                "spelling_language of {} is {language_tag} and not {expected_tag} as expected",
                file.path().display(),
            )
        }
        None => panic!("no spelling_language for {} found", file.path().display()),
    }
}

fn assert_trim_trailing_whitespace(file: &ConsideredFile, expected: bool) {
    match file.trim_trailing_whitespace {
        Some(TrimTrailingWs::Value(trim_trailing_whitespace)) => assert_eq!(
            trim_trailing_whitespace,
            expected,
            "trim_trailing_whitespace of {} is set to {trim_trailing_whitespace}",
            file.path().display(),
        ),
        None => panic!(
            "no trim_trailing_whitespace for {} found",
            file.path().display()
        ),
    }
}

fn assert_all_not_set(file: &ConsideredFile) {
    assert!(
        file.charset.is_none(),
        "charset of {} is set",
        file.path().display()
    );
    assert!(
        file.end_of_line.is_none(),
        "end_of_line of {} is set",
        file.path().display()
    );
    assert!(
        file.indent_style.is_none(),
        "indent_style of {} is set",
        file.path().display()
    );
    assert!(
        file.tab_width.is_none(),
        "tab_width of {} is set",
        file.path().display()
    );
    assert!(
        file.insert_final_newline.is_none(),
        "insert_final_newline of {} is set",
        file.path().display()
    );
    assert!(
        file.spelling_language.is_none(),
        "spelling_language of {} is set",
        file.path().display()
    );
    assert!(
        file.trim_trailing_whitespace.is_none(),
        "trim_trailing_whitespace of {} is set",
        file.path().display()
    );
}