use common::binary;
use core::sync::atomic::{AtomicU64, Ordering};
use std::fs;
use std::process::Command;
mod csharp;
mod rust;
#[path = "../common/mod.rs"]
mod common;
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
#[test]
fn all_clean_file() {
let path = rust_fixture_dir().join("clean.rs");
let output = run_command(&["--dry-run"], &path);
assert!(
output.status.success(),
"all on clean file should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn all_md_dry_run_fixes_tables() {
let before = fix_fixture_dir().join("table_md_before.md");
let output = run_command(&["--dry-run"], &before);
assert!(
output.status.success(),
"all --dry-run on markdown should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
output.stdout.is_empty(),
"dry-run must not print reconstructed source to stdout"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("success[FIX]"),
"all --dry-run must report the table fix on stderr: {stderr}"
);
}
#[test]
fn all_md_in_place_fixes_tables() {
let expected = fs::read_to_string(fix_fixture_dir().join("table_md_after.md")).unwrap();
let tmp = temp_file("md");
fs::write(
&tmp,
fs::read_to_string(fix_fixture_dir().join("table_md_before.md")).unwrap(),
)
.unwrap();
let output = run_command(&[], &tmp);
assert!(
output.status.success(),
"all on markdown file should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let actual = fs::read_to_string(&tmp).unwrap();
let _ = fs::remove_file(&tmp);
assert_eq!(actual, expected, "in-place markdown fix must match after");
}
#[test]
fn all_reports_remaining_doc_gaps() {
let dir = temp_dir();
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("gap.rs");
std::fs::write(&file, "pub fn undocumented() {}\n").unwrap();
let output = run_command(&[], &file);
assert!(
!output.status.success(),
"all should fail on remaining doc gaps"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("DOC001"),
"all should report doc gaps, got:\n{stderr}"
);
let _ = std::fs::remove_dir_all(&dir);
}
fn assert_has_diagnostic(stderr: &str, code: &str, item_name: Option<&str>) {
assert!(
stderr.contains(code),
"stderr should contain {code}, got:\n{stderr}"
);
if let Some(name) = item_name {
assert!(
stderr.contains(name),
"stderr should mention `{name}`, got:\n{stderr}"
);
}
}
#[test]
fn check_nonexistent_path_fails() {
let nonexistent = std::env::temp_dir().join(format!(
"rust-llm-tidy-lint-missing-{}-{}.rs",
std::process::id(),
TEST_COUNTER.fetch_add(1, Ordering::Relaxed)
));
let output = run_command(&["--include", "lints"], &nonexistent);
assert!(
!output.status.success(),
"non-existent path should exit non-zero"
);
}
#[test]
fn check_recursive_directory() {
let dir = temp_dir();
let sub = dir.join("sub");
std::fs::create_dir_all(&sub).unwrap();
std::fs::copy(rust_fixture_dir().join("clean.rs"), dir.join("clean.rs")).unwrap();
std::fs::write(sub.join("dirty.rs"), "pub fn dirty() {}\n").unwrap();
let output = run_command(&["--include", "lints"], &dir);
assert!(
!output.status.success(),
"directory with missing docs should fail"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("DOC001") && stderr.contains("dirty.rs"),
"should flag the nested undocumented file, got:\n{stderr}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn default_run_lints_comment_prose_in_every_comment_family() {
let names = [
"default_budgets.go",
"default_budgets.rb",
"default_budgets.sql",
"default_budgets.el",
"default_budgets.erl",
];
let dir = temp_dir();
std::fs::create_dir_all(&dir).unwrap();
for name in names {
fs::copy(defaults_fixture_dir().join(name), dir.join(name)).unwrap();
}
let output = run_command(&["--dry-run"], &dir);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"the TEXT001 errors must fail the default run:\n{stderr}"
);
for name in names {
assert!(
stderr.contains(&format!("{name}:1: error[TEXT001]")),
"{name}: the comment paragraph must fire in the default run:\n{stderr}"
);
}
assert_eq!(
stderr.matches("TEXT001").count(),
names.len(),
"exactly one comment paragraph per family, never the string content:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT002").count(),
0,
"no doc line in the fixtures crosses the line budget:\n{stderr}"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn el_lexicon_measures_comments_not_strings() {
let (stderr, exit) = run_lexicon_fixture("doc_text_lexicon_budgets.el");
assert_ne!(exit, 0, "the TEXT001 errors must fail the run:\n{stderr}");
assert!(
stderr.contains(":1: error[TEXT001]"),
"TEXT001 must report at the comment paragraph's first line:\n{stderr}"
);
assert!(
stderr.contains(":9: error[TEXT001]"),
"TEXT001 must report at the block comment's first prose line:\n{stderr}"
);
assert!(
stderr.contains(":15: warning[TEXT002]"),
"TEXT002 must report at the over-long comment line:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT001").count(),
2,
"exactly the line and block comment paragraphs, never the string:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT002").count(),
1,
"exactly the over-long comment line, never the string:\n{stderr}"
);
}
#[test]
fn erl_lexicon_measures_comments_not_strings() {
let (stderr, exit) = run_lexicon_fixture("doc_text_lexicon_budgets.erl");
assert_ne!(exit, 0, "the TEXT001 error must fail the run:\n{stderr}");
assert!(
stderr.contains(":1: error[TEXT001]"),
"TEXT001 must report at the comment paragraph's first line:\n{stderr}"
);
assert!(
stderr.contains(":9: warning[TEXT002]"),
"TEXT002 must report at the over-long comment line:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT001").count(),
1,
"exactly the comment paragraph, never the binary literal:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT002").count(),
1,
"exactly the over-long comment line, never the binary literal:\n{stderr}"
);
}
#[test]
fn js_lexicon_string_probes_stay_quiet() {
let (stderr, exit) = run_lexicon_fixture("doc_text_lexicon_probes.js");
assert_eq!(exit, 0, "the probe fixture must be clean:\n{stderr}");
assert!(
stderr.is_empty(),
"template literal and string content must stay unmeasured:\n{stderr}"
);
}
#[test]
fn js_lexicon_text_budgets_fire_with_original_lines() {
let (stderr, exit) = run_lexicon_fixture("doc_text_lexicon_budgets.js");
assert_ne!(exit, 0, "the TEXT001 errors must fail the run:\n{stderr}");
assert!(
stderr.contains(":1: error[TEXT001]"),
"TEXT001 must report at the comment paragraph's first line:\n{stderr}"
);
assert!(
stderr.contains(":11: error[TEXT001]"),
"TEXT001 must report at the JSDoc paragraph's first line:\n{stderr}"
);
assert!(
stderr.contains(":20: warning[TEXT002]"),
"TEXT002 must report at the over-long comment line:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT001").count(),
2,
"expected exactly 2 TEXT001 findings:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT002").count(),
1,
"expected exactly 1 TEXT002 finding:\n{stderr}"
);
}
#[test]
fn json_alias_is_equivalent_to_output_mode() {
let path = rust_fixture_dir().join("clean.rs");
let alias = run_command(&["--include", "lints", "--json"], &path);
let mode = run_command(&["--include", "lints", "--output-mode", "json"], &path);
assert_eq!(alias.status.code(), mode.status.code());
assert_eq!(alias.stdout, mode.stdout);
}
#[test]
fn json_dry_run_records_changes_for_both_flags() {
let path = reorder_fixture_dir()
.join("rust")
.join("fn_interstitial_comment_travels_with_next_before.rs");
let alias = run_command(&["--include", "reorder", "--json", "--dry-run"], &path);
let mode = run_command(
&["--include", "reorder", "--output-mode", "json", "--dry-run"],
&path,
);
assert!(
alias.status.success(),
"--json dry-run should succeed: {}",
String::from_utf8_lossy(&alias.stderr)
);
assert!(
mode.status.success(),
"--output-mode json dry-run should succeed: {}",
String::from_utf8_lossy(&mode.stderr)
);
assert_eq!(
alias.stdout, mode.stdout,
"--json and --output-mode json must be equivalent in dry-run"
);
for output in [&alias, &mode] {
let stdout = String::from_utf8_lossy(&output.stdout);
let records: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("stdout must parse as JSON: {e}\n{stdout}"));
let array = records.as_array().expect("JSON output must be an array");
assert_eq!(array.len(), 1, "expected 1 reorder record, got:\n{stdout}");
assert_eq!(array[0]["severity"], "success");
assert_eq!(array[0]["code"], "REORDER");
assert!(
array[0]["title"].is_null(),
"change records carry no title, got:\n{stdout}"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.trim().starts_with('['),
"stderr must not carry JSON, got:\n{stderr}"
);
}
}
#[test]
fn json_in_place_run_records_fix_changes() {
let expected = fs::read_to_string(fix_fixture_dir().join("table_md_after.md")).unwrap();
let tmp = temp_file("md");
fs::write(
&tmp,
fs::read_to_string(fix_fixture_dir().join("table_md_before.md")).unwrap(),
)
.unwrap();
let output = run_command(&["--include", "tables", "--output-mode", "json"], &tmp);
assert!(
output.status.success(),
"fix in-place in JSON mode should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let actual = fs::read_to_string(&tmp).unwrap();
let _ = fs::remove_file(&tmp);
assert_eq!(
actual, expected,
"in-place fix must write the table_md_after fixture"
);
let stdout = String::from_utf8_lossy(&output.stdout);
let records: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("stdout must parse as JSON: {e}\n{stdout}"));
let array = records.as_array().expect("JSON output must be an array");
assert_eq!(array.len(), 1, "expected 1 fix record, got:\n{stdout}");
let rec = &array[0];
assert_eq!(rec["severity"], "success");
assert_eq!(rec["code"], "FIX");
assert_eq!(rec["item_kind"], "table");
assert!(rec["line"].is_null(), "table records carry no line");
assert!(rec["title"].is_null(), "change records carry no title");
assert_eq!(rec["message"], "tables were aligned");
}
#[test]
fn json_in_place_run_records_reorder_changes() {
let expected = fs::read_to_string(
reorder_fixture_dir()
.join("rust")
.join("fn_interstitial_comment_travels_with_next_after.rs"),
)
.unwrap();
let tmp = temp_file("rs");
fs::write(
&tmp,
fs::read_to_string(
reorder_fixture_dir()
.join("rust")
.join("fn_interstitial_comment_travels_with_next_before.rs"),
)
.unwrap(),
)
.unwrap();
let output = run_command(&["--include", "reorder", "--output-mode", "json"], &tmp);
assert!(
output.status.success(),
"reorder in-place in JSON mode should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let actual = fs::read_to_string(&tmp).unwrap();
let _ = fs::remove_file(&tmp);
assert_eq!(
actual, expected,
"in-place reorder must write the after fixture"
);
let stdout = String::from_utf8_lossy(&output.stdout);
let records: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("stdout must parse as JSON: {e}\n{stdout}"));
let array = records.as_array().expect("JSON output must be an array");
assert_eq!(array.len(), 1, "expected 1 reorder record, got:\n{stdout}");
let rec = &array[0];
assert_eq!(rec["severity"], "success");
assert_eq!(rec["code"], "REORDER");
}
#[test]
fn json_output_clean_file_prints_empty_array() {
let path = rust_fixture_dir().join("clean.rs");
let output = run_command(&["--include", "lints", "--output-mode", "json"], &path);
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout), "[]\n");
}
#[test]
fn json_output_combines_with_dry_run() {
let path = rust_fixture_dir().join("clean.rs");
let output = run_command(&["--output-mode", "json", "--dry-run"], &path);
assert!(
output.status.success(),
"JSON output must combine with --dry-run: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
serde_json::from_str::<serde_json::Value>(&stdout)
.unwrap_or_else(|e| panic!("stdout must parse as JSON: {e}\n{stdout}"));
}
#[test]
fn json_output_merges_lints_and_changes_in_one_document() {
let path = rust_fixture_dir().join("doc001_missing_docs.rs");
let output = run_command(
&[
"--include",
"lints",
"--include",
"reorder",
"--output-mode",
"json",
"--dry-run",
],
&path,
);
assert!(
!output.status.success(),
"DOC001 error findings must still fail a dry-run JSON run"
);
let stdout = String::from_utf8_lossy(&output.stdout);
let records: serde_json::Value = serde_json::from_str(&stdout).unwrap_or_else(|e| {
panic!("one JSON doc must parse despite the error bail: {e}\n{stdout}")
});
let array = records.as_array().expect("JSON output must be an array");
assert!(
array.iter().any(|r| r["severity"] == "error"
&& r["code"] == "DOC001"
&& r["title"] == "missing documentation"),
"array must contain titled lint findings, got:\n{stdout}"
);
assert!(
array
.iter()
.any(|r| r["severity"] == "success" && r["code"] == "REORDER" && r["title"].is_null()),
"array must contain untitled reorder change records, got:\n{stdout}"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.trim().starts_with('['),
"stderr must not carry JSON, got:\n{stderr}"
);
}
#[test]
fn json_output_prints_document_before_error_bail() {
let path = rust_fixture_dir().join("doc001_missing_docs.rs");
let output = run_command(&["--include", "lints", "--output-mode", "json"], &path);
assert!(
!output.status.success(),
"error-severity findings must fail the run in JSON mode"
);
let stdout = String::from_utf8_lossy(&output.stdout);
let findings: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("stdout must parse as JSON despite error bail: {e}\n{stdout}"));
let array = findings.as_array().expect("JSON output must be an array");
assert!(
!array.is_empty(),
"error fixture must produce findings on stdout"
);
assert!(array.iter().any(|f| f["severity"] == "error"));
assert!(
array
.iter()
.filter(|f| f["code"] == "DOC001")
.all(|f| f["title"] == "missing documentation"),
"DOC001 findings must carry the friendly title, got:\n{stdout}"
);
}
#[test]
fn json_output_records_reorder_changes() {
let path = reorder_fixture_dir()
.join("rust")
.join("fn_interstitial_comment_travels_with_next_before.rs");
let output = run_command(
&["--include", "reorder", "--output-mode", "json", "--dry-run"],
&path,
);
assert!(
output.status.success(),
"reorder dry-run in JSON mode should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let records: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("stdout must parse as JSON: {e}\n{stdout}"));
let array = records.as_array().expect("JSON output must be an array");
assert_eq!(array.len(), 1, "expected 1 reorder record, got:\n{stdout}");
let rec = &array[0];
assert_eq!(rec["severity"], "success");
assert_eq!(rec["code"], "REORDER");
assert!(
rec.as_object()
.expect("change record is an object")
.contains_key("title"),
"change records must emit an explicit title: null key, got:\n{stdout}"
);
assert!(
rec["title"].is_null(),
"change records carry no title, got:\n{stdout}"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.trim().starts_with('['),
"stderr must not carry JSON, got:\n{stderr}"
);
}
#[test]
fn json_output_reports_all_findings() {
let path = rust_fixture_dir().join("test001_test_naming.rs");
let output = run_command(&["--include", "lints", "--output-mode", "json"], &path);
assert!(
output.status.success(),
"warnings-only JSON run should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let findings: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("stdout must parse as JSON: {e}\n{stdout}"));
let array = findings.as_array().expect("JSON output must be an array");
assert_eq!(
array.len(),
3,
"expected 3 TEST001 findings, got:\n{stdout}"
);
for finding in array {
let keys: std::collections::BTreeSet<&str> = finding
.as_object()
.expect("each record is an object")
.keys()
.map(String::as_str)
.collect();
assert_eq!(
keys,
[
"path",
"line",
"severity",
"code",
"message",
"item_kind",
"item_name",
"title"
]
.into_iter()
.collect(),
"lint record must not carry change-only extras, got: {finding}"
);
assert_eq!(finding["severity"], "warning");
assert_eq!(finding["code"], "TEST001");
assert_eq!(finding["title"], "non-behavioral test name");
assert_eq!(finding["item_kind"], "fn");
assert_eq!(
finding["path"],
path.to_str().unwrap(),
"finding must carry the processed file path"
);
assert!(finding["line"].as_u64().is_some_and(|l| l >= 1));
assert!(
finding["message"].as_str().is_some_and(|m| !m.is_empty()),
"message field must be present and non-empty, got: {finding}"
);
assert!(
finding["item_name"].is_string(),
"item_name must be a string for a named item, got: {finding}"
);
}
assert!(
array.iter().all(|f| f["item_name"].is_string()),
"item_name must be non-null for named test functions, got:\n{stdout}"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("TEST001"),
"JSON mode must not duplicate diagnostics on stderr, got:\n{stderr}"
);
}
#[test]
fn md_clean_file_no_diagnostics() {
let path = temp_md("# Title\n\nShort prose paragraph.\n");
let output = run_command(&["--include", "lints"], &path);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"clean markdown should pass lint dispatch: {stderr}"
);
assert!(
stderr.is_empty(),
"clean markdown should produce no diagnostics, got:\n{stderr}"
);
}
#[test]
fn md_lints_skipped_when_whitelist_omits_lints() {
let path = temp_md(&oversized_paragraph_md());
let output = run_command(&["--include", "tables", "--dry-run"], &path);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"tables-only whitelist must not lint markdown: {stderr}"
);
assert!(
!stderr.contains("TEXT001") && !stderr.contains("TEXT002"),
"lint findings must be suppressed without `lints` in the whitelist:\n{stderr}"
);
}
#[test]
fn md_long_line_warns_text002_without_failing() {
let path = temp_md(&format!("{}\n", "x".repeat(81)));
let output = run_command(&["--include", "lints"], &path);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"TEXT002 warnings must not fail the run: {stderr}"
);
assert!(
stderr.contains(":1: warning[TEXT002]"),
"expected a TEXT002 warning at line 1, got:\n{stderr}"
);
}
#[test]
fn md_paragraph_over_limit_fails_with_text001() {
let path = temp_md(&oversized_paragraph_md());
let output = run_command(&["--include", "lints"], &path);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"TEXT001 errors on markdown must fail the run: {stderr}"
);
assert!(
stderr.contains(":3: error[TEXT001]"),
"expected a TEXT001 error at the paragraph's first line, got:\n{stderr}"
);
}
#[test]
fn md_text001_suppressed_by_exclude() {
let path = temp_md(&oversized_paragraph_md());
let output = run_command(&["--include", "lints", "--exclude", "TEXT001"], &path);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"excluding TEXT001 must clear the markdown error: {stderr}"
);
assert!(
!stderr.contains("TEXT001"),
"excluded code must not be reported, got:\n{stderr}"
);
}
#[test]
fn py_docstring_budgets_fire_with_original_lines() {
let (stderr, exit) = run_python_fixture("docstring_text_budgets.py");
assert_ne!(exit, 0, "the TEXT001 error must fail the run:\n{stderr}");
assert!(
stderr.contains(":2: error[TEXT001]"),
"TEXT001 must report at the docstring's first prose line:\n{stderr}"
);
assert!(
stderr.contains(":27: warning[TEXT002]"),
"TEXT002 must report at the over-long docstring line:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT001").count(),
1,
"the docstring paragraph only, never the payload:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT002").count(),
1,
"the wide docstring line only, never the doctest:\n{stderr}"
);
}
#[test]
fn py_text_checks_measure_comments_not_strings() {
let (stderr, exit) = run_python_fixture("doc_text_lexicon_budgets.py");
assert_ne!(exit, 0, "the TEXT001 error must fail the run:\n{stderr}");
assert!(
stderr.contains(":1: error[TEXT001]"),
"TEXT001 must report at the comment paragraph's first line:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT001").count(),
1,
"exactly the comment paragraph, never the string content:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT002").count(),
0,
"no doc line in the fixture crosses the line budget:\n{stderr}"
);
}
#[test]
fn rb_lexicon_measures_comment_prose_only() {
let (stderr, exit) = run_lexicon_fixture("doc_text_lexicon_budgets.rb");
assert_ne!(exit, 0, "the TEXT001 error must fail the run:\n{stderr}");
assert!(
stderr.contains(":1: error[TEXT001]"),
"the comment paragraph must measure from its first line:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT001").count(),
1,
"exactly the comment paragraph, never payload or code:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT002").count(),
0,
"no doc line in the fixture crosses the line budget:\n{stderr}"
);
}
fn run_rust_fixture(name: &str) -> (String, i32) {
let path = rust_fixture_dir().join(name);
let output = run_command(&["--include", "lints"], &path);
(
String::from_utf8_lossy(&output.stderr).to_string(),
output.status.code().unwrap_or(-1),
)
}
#[test]
fn sh_lexicon_ignores_heredoc_payload() {
let (stderr, exit) = run_lexicon_fixture("doc_text_lexicon_budgets.sh");
assert_ne!(exit, 0, "the TEXT001 error must fail the run:\n{stderr}");
assert_eq!(
stderr.matches("TEXT001").count(),
1,
"exactly the comment paragraph, never the heredoc payload:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT002").count(),
0,
"no doc line in the fixture crosses the line budget:\n{stderr}"
);
}
#[test]
fn sql_lexicon_measures_comments_not_strings() {
let (stderr, exit) = run_lexicon_fixture("doc_text_lexicon_budgets.sql");
assert_ne!(exit, 0, "the TEXT001 errors must fail the run:\n{stderr}");
assert!(
stderr.contains(":1: error[TEXT001]"),
"TEXT001 must report at the comment paragraph's first line:\n{stderr}"
);
assert!(
stderr.contains(":8: error[TEXT001]"),
"TEXT001 must report at the block comment's first prose line:\n{stderr}"
);
assert!(
stderr.contains(":13: warning[TEXT002]"),
"TEXT002 must report at the over-long comment line:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT001").count(),
2,
"exactly the line and block comment paragraphs, never the string:\n{stderr}"
);
assert_eq!(
stderr.matches("TEXT002").count(),
1,
"exactly the over-long comment line, never the string:\n{stderr}"
);
}
fn defaults_fixture_dir() -> std::path::PathBuf {
fixture_dir().join("defaults")
}
fn fix_fixture_dir() -> std::path::PathBuf {
manifest_dir().join("tests").join("fixtures").join("fix")
}
fn oversized_paragraph_md() -> String {
let lines: String = (0..10)
.map(|i| format!("sentence number {i} carries some filler text\n"))
.collect();
format!("# Title\n\n{lines}\nTrailer.\n")
}
fn reorder_fixture_dir() -> std::path::PathBuf {
manifest_dir()
.join("tests")
.join("fixtures")
.join("reorder")
}
fn run_lexicon_fixture(name: &str) -> (String, i32) {
let path = fixture_dir().join(name);
let output = run_command(&["--include", "lints"], &path);
(
String::from_utf8_lossy(&output.stderr).to_string(),
output.status.code().unwrap_or(-1),
)
}
fn run_python_fixture(name: &str) -> (String, i32) {
let path = python_fixture_dir().join(name);
let output = run_command(&["--include", "lints"], &path);
(
String::from_utf8_lossy(&output.stderr).to_string(),
output.status.code().unwrap_or(-1),
)
}
fn rust_fixture_dir() -> std::path::PathBuf {
fixture_dir().join("rust")
}
fn temp_dir() -> std::path::PathBuf {
let seq = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
std::env::temp_dir().join(format!("rust-llm-tidy-lint-dir-{}-{}", pid, seq))
}
fn temp_md(content: &str) -> std::path::PathBuf {
let path = temp_file("md");
fs::write(&path, content).unwrap();
path
}
fn python_fixture_dir() -> std::path::PathBuf {
fixture_dir().join("python")
}
fn run_command(args: &[&str], path: &std::path::Path) -> std::process::Output {
let mut cmd = Command::new(binary());
cmd.args(["--no-config"]).args(args).arg(path);
cmd.output()
.unwrap_or_else(|e| panic!("failed to spawn rust-llm-tidy on {}: {e}", path.display()))
}
fn temp_file(ext: &str) -> std::path::PathBuf {
let seq = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
std::env::temp_dir().join(format!("rust-llm-tidy-all-{}-{}.{}", pid, seq, ext))
}
fn fixture_dir() -> std::path::PathBuf {
manifest_dir().join("tests").join("fixtures").join("doc")
}
fn manifest_dir() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}