use stern4rust::rule::Rule;
use stern4rust::rules::testing::test_file_structure_rule::TestFileStructureRule;
use stern4rust::source_file::SourceFile;
const HEADER: &str = "// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>\n\
// Licensed under the MIT License\n\
// SPDX-License-Identifier: MIT\n";
const RULE: &str = "test-file-structure";
fn check(contents: &str) -> Vec<stern4rust::reporting::offence::Offence> {
TestFileStructureRule::new().check(&test_file(contents))
}
fn descriptions(contents: &str) -> Vec<String> {
check(contents)
.into_iter()
.map(|offence| offence.description)
.collect()
}
fn registry_file(path: &str) -> SourceFile {
SourceFile::new(
path,
&format!("{HEADER}\npub mod alpha_tests;\npub mod beta_tests;\n"),
)
}
fn source_file(contents: &str) -> SourceFile {
SourceFile::new("src/subject.rs", &format!("{HEADER}\n{contents}"))
}
fn test_file(contents: &str) -> SourceFile {
SourceFile::new("tests/subject_tests.rs", &format!("{HEADER}\n{contents}"))
}
#[test]
fn check_a_blank_line_between_imports_reports_it() {
let contents = "use alpha::One;\n\
\n\
use beta::Two;\n";
let offences = check(contents);
assert_eq!(offences.len(), 1);
assert!(offences[0].description.contains("blank"));
}
#[test]
fn check_a_comment_introducing_a_test_is_part_of_that_test() {
let contents = "#[test]\n\
fn alpha_does_something() {}\n\
\n\
// Why this one matters.\n\
#[test]\n\
fn beta_does_something() {}\n";
let offences = check(contents);
assert!(offences.is_empty(), "expected none, got {offences:?}");
}
#[test]
fn check_a_constant_after_a_helper_reports_the_section_order() {
let contents = "fn helper() {}\n\
\n\
const LATE: usize = 1;\n";
let offences = check(contents);
assert_eq!(offences.len(), 1);
assert!(offences[0].description.contains("constant"));
}
#[test]
fn check_a_file_holding_only_a_header_reports_nothing() {
let offences = check("");
assert!(offences.is_empty());
}
#[test]
fn check_a_file_in_the_expected_shape_reports_nothing() {
let contents = "use alpha::One;\n\
use beta::Two;\n\
\n\
const FIRST: usize = 1;\n\
\n\
const SECOND: usize = 2;\n\
\n\
fn helper_a() {}\n\
\n\
fn helper_b() {}\n\
\n\
#[test]\n\
fn alpha_does_something() {}\n\
\n\
#[test]\n\
fn beta_does_something() {}\n";
let offences = check(contents);
assert!(offences.is_empty(), "expected none, got {offences:?}");
}
#[test]
fn check_a_file_of_tests_alone_reports_nothing() {
let contents = "#[test]\n\
fn alpha_does_something() {}\n\
\n\
#[test]\n\
fn beta_does_something() {}\n";
let offences = check(contents);
assert!(offences.is_empty(), "expected none, got {offences:?}");
}
#[test]
fn check_a_file_outside_the_tests_tree_reports_nothing() {
let contents = "use beta::Two;\n\
use alpha::One;\n";
let offences = TestFileStructureRule::new().check(&source_file(contents));
assert!(offences.is_empty());
}
#[test]
fn check_a_file_that_does_not_parse_reports_nothing() {
let contents = "fn broken( {\n";
let offences = check(contents);
assert!(offences.is_empty());
}
#[test]
fn check_a_helper_after_a_test_reports_the_section_order() {
let contents = "#[test]\n\
fn alpha_does_something() {}\n\
\n\
fn helper() {}\n";
let offences = check(contents);
assert_eq!(offences.len(), 1);
assert!(offences[0].description.contains("helper"));
}
#[test]
fn check_a_nested_registry_file_reports_nothing() {
let registry = registry_file("tests/rules/mod.rs");
let offences = TestFileStructureRule::new().check(®istry);
assert!(offences.is_empty(), "expected none, got {offences:?}");
}
#[test]
fn check_a_registry_file_reports_nothing() {
let registry = registry_file("tests/all_tests.rs");
let offences = TestFileStructureRule::new().check(®istry);
assert!(offences.is_empty(), "expected none, got {offences:?}");
}
#[test]
fn check_a_section_with_a_single_entry_reports_nothing() {
let contents = "use only::One;\n\
\n\
const ONLY: usize = 1;\n\
\n\
fn only_helper() {}\n\
\n\
#[test]\n\
fn only_test() {}\n";
let offences = check(contents);
assert!(offences.is_empty(), "expected none, got {offences:?}");
}
#[test]
fn check_a_struct_helper_out_of_order_reports_it() {
let contents = "struct Recorder;\n\
\n\
fn build() {}\n";
let offences = check(contents);
assert_eq!(offences.len(), 1);
assert!(offences[0].description.contains("build"));
}
#[test]
fn check_a_struct_helper_sorts_among_the_helper_functions() {
let contents = "fn build() {}\n\
\n\
struct Recorder;\n\
\n\
fn verify() {}\n";
let offences = check(contents);
assert!(offences.is_empty(), "expected none, got {offences:?}");
}
#[test]
fn check_an_impl_block_sorts_under_the_type_it_implements() {
let contents = "fn build() {}\n\
\n\
struct Recorder;\n\
\n\
impl Recorder {\n\
fn record(&self) {}\n\
}\n";
let offences = check(contents);
assert!(offences.is_empty(), "expected none, got {offences:?}");
}
#[test]
fn check_an_import_after_a_test_reports_the_section_order() {
let contents = "#[test]\n\
fn alpha_does_something() {}\n\
\n\
use late::Import;\n";
let offences = check(contents);
assert_eq!(offences.len(), 1);
assert!(offences[0].description.contains("import"));
}
#[test]
fn check_constants_out_of_alphabetic_order_reports_the_later_one() {
let contents = "const SECOND: usize = 2;\n\
\n\
const FIRST: usize = 1;\n";
let offences = check(contents);
assert_eq!(offences.len(), 1);
assert!(offences[0].description.contains("FIRST"));
}
#[test]
fn check_constants_run_together_without_a_blank_line_reports_it() {
let contents = "const FIRST: usize = 1;\n\
const SECOND: usize = 2;\n";
let offences = check(contents);
assert_eq!(offences.len(), 1);
assert!(offences[0].description.contains("blank"));
}
#[test]
fn check_helpers_out_of_alphabetic_order_reports_the_later_one() {
let contents = "fn zulu() {}\n\
\n\
fn alpha() {}\n";
let offences = check(contents);
assert_eq!(offences.len(), 1);
assert!(offences[0].description.contains("alpha"));
}
#[test]
fn check_imports_out_of_alphabetic_order_around_a_crate_path_reports_nothing() {
let contents = "use crate::support::builders;\n\
use anyhow::Result;\n";
let offences = check(contents);
assert!(offences.is_empty(), "expected none, got {offences:?}");
}
#[test]
fn check_imports_out_of_alphabetic_order_beside_a_crate_path_still_reports_them() {
let contents = "use crate::support::builders;\n\
use zebra::Z;\n\
use anyhow::Result;\n";
let offences = check(contents);
assert_eq!(offences.len(), 1);
assert!(offences[0].description.contains("anyhow::Result"));
}
#[test]
fn check_imports_out_of_alphabetic_order_reports_the_later_one() {
let contents = "use beta::Two;\n\
use alpha::One;\n";
let offences = check(contents);
assert_eq!(offences.len(), 1);
assert_eq!(offences[0].rule, RULE);
assert!(offences[0].description.contains("alpha::One"));
}
#[test]
fn check_names_the_file_it_judged() {
let contents = "use beta::Two;\n\
use alpha::One;\n";
let files = descriptions(contents);
assert_eq!(files.len(), 1);
}
#[test]
fn check_reports_every_offence_in_a_file_rather_than_only_the_first() {
let contents = "use beta::Two;\n\
use alpha::One;\n\
\n\
#[test]\n\
fn zulu_does_something() {}\n\
\n\
#[test]\n\
fn alpha_does_something() {}\n";
let offences = check(contents);
assert_eq!(offences.len(), 2);
}
#[test]
fn check_reports_the_line_the_offending_item_starts_on() {
let contents = "use beta::Two;\n\
use alpha::One;\n";
let offences = check(contents);
assert_eq!(offences[0].line, 6);
}
#[test]
fn check_tests_out_of_alphabetic_order_reports_the_later_one() {
let contents = "#[test]\n\
fn zulu_does_something() {}\n\
\n\
#[test]\n\
fn alpha_does_something() {}\n";
let offences = check(contents);
assert_eq!(offences.len(), 1);
assert!(offences[0].description.contains("alpha_does_something"));
}
#[test]
fn check_two_blank_lines_between_tests_reports_it() {
let contents = "#[test]\n\
fn alpha_does_something() {}\n\
\n\
\n\
#[test]\n\
fn beta_does_something() {}\n";
let offences = check(contents);
assert_eq!(offences.len(), 1);
assert!(offences[0].description.contains("blank"));
}
#[test]
fn name_is_the_kebab_case_rule_name_used_in_the_report() {
let name = TestFileStructureRule::new().name();
assert_eq!(name, RULE);
}