use std::path::{Path, PathBuf};
use std::process;
use crate::json_envelope::{self, JsonEnvelope};
use super::{ConformanceJsonReport, CONFORMANCE_TEST_SCHEMA_VERSION};
pub(super) fn expectation_sibling(harn_file: &Path) -> Option<PathBuf> {
["expected", "error", "lint"]
.into_iter()
.map(|extension| harn_file.with_extension(extension))
.find(|candidate| candidate.exists())
}
pub(super) fn unrunnable_count(selected: &[(PathBuf, String)]) -> usize {
selected
.iter()
.filter(|(harn_file, _)| expectation_sibling(harn_file).is_none())
.count()
}
pub(super) fn empty_run_message(
suite_root: &Path,
selection: Option<&str>,
filter: Option<&str>,
selected: &[(PathBuf, String)],
) -> String {
let mut message = format!(
"no conformance tests ran under {} (selection: {}, filter: {})",
suite_root.display(),
selection.unwrap_or("<whole suite>"),
filter.unwrap_or("<none>"),
);
let unrunnable = unrunnable_count(selected);
if unrunnable > 0 {
message.push_str(&format!(
"; {unrunnable} of {} selected file(s) have no .expected, .error, or .lint sibling, so the runner could see them but not run them",
selected.len()
));
} else if selected.is_empty() {
message.push_str("; the selection matched no files at all - check the path and the filter");
}
message.push_str(
". A run that executed nothing is reported as a failure because an empty selection is otherwise indistinguishable from every test passing. Pass --allow-empty if an empty selection is expected here.",
);
message
}
pub(super) fn report_empty_run(json: bool, message: String) -> ! {
if json {
let envelope: JsonEnvelope<ConformanceJsonReport> = JsonEnvelope::err(
CONFORMANCE_TEST_SCHEMA_VERSION,
"conformance_empty_selection",
message,
);
println!("{}", json_envelope::to_string_pretty(&envelope));
} else {
eprintln!("{message}");
}
process::exit(1)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
struct TempTestDir {
dir: tempfile::TempDir,
}
impl TempTestDir {
fn new() -> Self {
let dir = tempfile::Builder::new()
.prefix("harn-cli-empty-run-")
.tempdir()
.unwrap();
Self { dir }
}
fn write(&self, relative: &str) {
let path = self.dir.path().join(relative);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, "// test").unwrap();
}
fn path(&self) -> &Path {
self.dir.path()
}
}
#[test]
fn expectation_sibling_finds_each_accepted_extension() {
for extension in ["expected", "error", "lint"] {
let temp = TempTestDir::new();
temp.write("tests/case.harn");
temp.write(&format!("tests/case.{extension}"));
let harn_file = temp.path().join("tests/case.harn");
assert_eq!(
expectation_sibling(&harn_file),
Some(harn_file.with_extension(extension)),
"{extension} sibling should make the file runnable"
);
}
}
#[test]
fn expectation_sibling_absent_for_a_library_module() {
let temp = TempTestDir::new();
temp.write("tests/_common.harn");
assert_eq!(
expectation_sibling(&temp.path().join("tests/_common.harn")),
None
);
}
#[test]
fn unrunnable_count_counts_only_the_files_without_a_sibling() {
let temp = TempTestDir::new();
temp.write("tests/runnable.harn");
temp.write("tests/runnable.expected");
temp.write("tests/_common.harn");
temp.write("tests/modules/helper.harn");
let selected: Vec<(PathBuf, String)> = [
"tests/runnable.harn",
"tests/_common.harn",
"tests/modules/helper.harn",
]
.into_iter()
.map(|relative| (temp.path().join(relative), relative.to_string()))
.collect();
assert_eq!(unrunnable_count(&selected), 2);
}
#[test]
fn empty_run_message_names_the_selection_the_filter_and_the_cause() {
let temp = TempTestDir::new();
temp.write("tests/_common.harn");
let selected = vec![(
temp.path().join("tests/_common.harn"),
"tests/_common.harn".to_string(),
)];
let message = empty_run_message(
temp.path(),
Some("tests/_common.harn"),
Some("common"),
&selected,
);
assert!(message.contains("tests/_common.harn"), "{message}");
assert!(message.contains("common"), "{message}");
assert!(
message.contains("1 of 1 selected file(s) have no .expected"),
"{message}"
);
assert!(message.contains("--allow-empty"), "{message}");
}
#[test]
fn empty_run_message_distinguishes_a_selection_that_matched_nothing() {
let temp = TempTestDir::new();
let message = empty_run_message(temp.path(), None, None, &[]);
assert!(message.contains("<whole suite>"), "{message}");
assert!(message.contains("<none>"), "{message}");
assert!(message.contains("matched no files at all"), "{message}");
assert!(
!message.contains("selected file(s) have no"),
"a selection that matched nothing must not blame missing siblings: {message}"
);
}
}