use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use futures_util::future::join_all;
use slippy_linter::{
FileFilter, LintConfig, RawSlippyConfiguration, SlippyConfiguration, finalize,
};
fn config() -> SlippyConfiguration {
dotenvy::dotenv().ok();
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
let _ = tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_writer(std::io::stderr)
.with_target(false)
.without_time()
.try_init();
let raw: RawSlippyConfiguration = toml::from_str(include_str!("../slippy.toml")).unwrap();
finalize(raw, &Default::default()).unwrap()
}
#[tokio::test]
async fn lint_option_result_misuse() {
test_compare_stderr_for_lint("option_result_misuse").await;
}
#[tokio::test]
async fn lint_string_used_as_enum() {
test_compare_stderr_for_lint("string_used_as_enum").await;
}
#[tokio::test]
async fn lint_unnecessary_contains_in_loop() {
test_compare_stderr_for_lint("unnecessary_contains_in_loop").await;
}
#[tokio::test]
async fn lint_representable_illegal_state() {
test_errors_exist_for_lint("representable_illegal_state").await;
}
#[tokio::test]
async fn internal_examples_match_stderr() {
run_examples_in_subdir("internal").await;
}
async fn test_compare_stderr_for_lint(lint_name: &str) {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let lint_dir = root.join("examples").join("lints").join(lint_name);
let rel_dir = Path::new("examples").join("lints").join(lint_name);
let config = LintConfig::new(config(), true);
let mut entries: Vec<_> = std::fs::read_dir(&lint_dir)
.unwrap()
.map(|e| e.unwrap().path())
.collect();
entries.sort();
let mut tasks = Vec::new();
for entry in &entries {
if entry.extension() == Some(OsStr::new("rs")) {
let stderr_path = entry.with_extension("stderr");
if !stderr_path.exists() {
panic!("missing stderr file for {}", entry.display());
}
let name = entry.file_name().unwrap();
tasks.push((rel_dir.join(name), stderr_path.clone()));
}
}
if std::env::var("BATCH").is_ok() {
run_batch_tasks(&tasks, &config, 10, 9).await;
} else {
run_tasks_assert_all(&tasks, &config).await;
}
}
async fn test_errors_exist_for_lint(lint_name: &str) {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let lint_dir = root.join("examples").join("lints").join(lint_name);
let rel_dir = Path::new("examples").join("lints").join(lint_name);
let config = LintConfig::new(config(), true);
let tasks = collect_errors_tasks_in_dir(&lint_dir, &rel_dir);
if std::env::var("BATCH").is_ok() {
check_errors_exist_batch(&tasks, &config, 10, 9).await;
} else {
check_errors_exist(&tasks, &config).await;
}
}
fn collect_errors_tasks_in_dir(abs_dir: &Path, rel_dir: &Path) -> Vec<(PathBuf, PathBuf)> {
let mut entries: Vec<_> = std::fs::read_dir(abs_dir)
.unwrap()
.map(|e| e.unwrap().path())
.collect();
entries.sort();
let mut tasks = Vec::new();
for entry in &entries {
if entry.extension() == Some(OsStr::new("rs")) {
let errors_path = entry.with_extension("errors");
if !errors_path.exists() {
panic!("missing .errors file for {}", entry.display());
}
let name = entry.file_name().unwrap();
tasks.push((rel_dir.join(name), errors_path.clone()));
}
}
tasks
}
async fn check_errors_exist(tasks: &[(PathBuf, PathBuf)], config: &LintConfig<'_>) {
let futures = tasks
.iter()
.map(|(relative, errors_path)| check_errors(relative, errors_path, config));
let failures: Vec<String> = join_all(futures).await.into_iter().flatten().collect();
assert!(
failures.is_empty(),
"{} examples checked, {} failures:\n\n{}",
tasks.len(),
failures.len(),
failures.join("\n"),
);
}
async fn check_errors_exist_batch(
tasks: &[(PathBuf, PathBuf)],
config: &LintConfig<'_>,
runs: usize,
min_pass: usize,
) {
let mut pass_counts = vec![0usize; tasks.len()];
let mut all_errors: Vec<Vec<String>> = vec![Vec::new(); tasks.len()];
for _ in 0..runs {
let futures = tasks
.iter()
.map(|(relative, errors_path)| check_errors(relative, errors_path, config));
let results: Vec<Option<String>> = join_all(futures).await;
for (i, result) in results.into_iter().enumerate() {
match result {
None => pass_counts[i] += 1,
Some(err) => all_errors[i].push(err),
}
}
}
let failures: Vec<String> = tasks
.iter()
.zip(pass_counts.iter())
.zip(all_errors.iter())
.filter(|((_, count), _)| **count < min_pass)
.map(|(((relative, _), &count), errors)| {
format!(
"BATCH FAILURE: {} ({}/{} runs passed, need at least {}):\n{}",
relative.display(),
count,
runs,
min_pass,
errors.join("\n"),
)
})
.collect();
assert!(
failures.is_empty(),
"Batch failures:\n{}",
failures.join("\n")
);
}
async fn check_errors(
relative: &Path,
errors_path: &Path,
config: &LintConfig<'_>,
) -> Option<String> {
let errors_content = std::fs::read_to_string(errors_path).unwrap();
let expect_diagnostics = !errors_content.trim().is_empty();
let filter = FileFilter::allow_all();
let mut buf = Vec::new();
match slippy_linter::lint_file(relative, config, &filter, &mut buf).await {
Ok(_stats) => {
let output = String::from_utf8(buf).unwrap();
let has_output = !strip_ansi(&output).trim().is_empty();
if expect_diagnostics && !has_output {
Some(format!(
"EXPECTED DIAGNOSTICS but got none: {}",
relative.display(),
))
} else if !expect_diagnostics && has_output {
Some(format!(
"EXPECTED NO DIAGNOSTICS but got output: {}\n{}",
relative.display(),
strip_ansi(&output),
))
} else {
None
}
}
Err(e) => Some(format!("ERROR: {}: {e}\n", relative.display())),
}
}
async fn run_tasks_assert_all(tasks: &[(PathBuf, PathBuf)], config: &LintConfig<'_>) {
let futures = tasks
.iter()
.map(|(relative, stderr_path)| check_example(relative, stderr_path, config));
let failures: Vec<String> = join_all(futures).await.into_iter().flatten().collect();
assert!(
failures.is_empty(),
"{} examples checked, {} failures:\n\n{}",
tasks.len(),
failures.len(),
failures.join("\n"),
);
}
async fn run_batch_tasks(
tasks: &[(PathBuf, PathBuf)],
config: &LintConfig<'_>,
runs: usize,
min_pass: usize,
) {
let mut pass_counts = vec![0usize; tasks.len()];
let mut all_errors: Vec<Vec<String>> = vec![Vec::new(); tasks.len()];
for _ in 0..runs {
let futures = tasks
.iter()
.map(|(relative, stderr_path)| check_example(relative, stderr_path, config));
let results: Vec<Option<String>> = join_all(futures).await;
for (i, result) in results.into_iter().enumerate() {
match result {
None => pass_counts[i] += 1,
Some(err) => all_errors[i].push(err),
}
}
}
let failures: Vec<String> = tasks
.iter()
.zip(pass_counts.iter())
.zip(all_errors.iter())
.filter(|((_, count), _)| **count < min_pass)
.map(|(((relative, _), &count), errors)| {
format!(
"BATCH FAILURE: {} ({}/{} runs passed, need at least {}):\n{}",
relative.display(),
count,
runs,
min_pass,
errors.join("\n"),
)
})
.collect();
assert!(
failures.is_empty(),
"Batch failures:\n{}",
failures.join("\n")
);
}
async fn run_examples_in_subdir(subdir: &str) {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let examples_dir = root.join("examples").join(subdir);
let config = LintConfig::new(config(), true);
let mut entries: Vec<_> = std::fs::read_dir(&examples_dir)
.unwrap()
.map(|e| e.unwrap().path())
.collect();
entries.sort();
let mut tasks = Vec::new();
for entry in &entries {
if entry.extension() == Some(OsStr::new("rs")) {
let stderr_path = entry.with_extension("stderr");
if !stderr_path.exists() {
panic!("missing stderr file for {}", entry.display());
}
let name = entry.file_name().unwrap();
let relative = Path::new("examples").join(subdir).join(name);
tasks.push((relative, stderr_path));
} else if entry.is_dir() {
let dir_name = entry.file_name().unwrap();
let stderr_path = examples_dir.join(dir_name).with_extension("stderr");
if !stderr_path.exists() {
panic!("missing stderr file for {}", entry.display());
}
let main_rs = entry.join("main.rs");
let entry_file = if main_rs.exists() {
main_rs
} else {
panic!("MISSING ENTRY: {}: no main.rs or lib.rs", entry.display());
};
let relative = Path::new("examples")
.join(subdir)
.join(dir_name)
.join(entry_file.file_name().unwrap());
tasks.push((relative, stderr_path));
}
}
let futures = tasks
.iter()
.map(|(relative, stderr_path)| check_example(relative, stderr_path, &config));
let failures: Vec<String> = join_all(futures).await.into_iter().flatten().collect();
assert!(
failures.is_empty(),
"{} examples checked, {} failures:\n\n{}",
entries.len(),
failures.len(),
failures.join("\n"),
);
}
async fn check_example(
relative: &Path,
stderr_path: &Path,
config: &LintConfig<'_>,
) -> Option<String> {
let expected = std::fs::read_to_string(stderr_path).unwrap();
let filter = FileFilter::allow_all();
let mut buf = Vec::new();
match slippy_linter::lint_file(relative, config, &filter, &mut buf).await {
Ok(_stats) => {
let output = String::from_utf8(buf).unwrap();
if strip_ansi(&output).trim() != expected.trim() {
let actual = strip_ansi(&output);
Some(format!(
"MISMATCH: {}\n\n========================================= expected =========================================\n{expected}\n========================================= actual =========================================\n{actual}\n--- end ---",
relative.display(),
))
} else {
None
}
}
Err(e) => Some(format!("ERROR: {}: {e}\n", relative.display())),
}
}
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\x1b' {
if chars.next() == Some('[') {
for c in chars.by_ref() {
if c.is_ascii_alphabetic() {
break;
}
}
}
} else {
out.push(c);
}
}
out
}