1use crate::{CheckInfo, Htl, parent_dir, write_if_changed};
10use anyhow::{Context, Result};
11use mlua::{Function, Table, Value};
12use std::path::{Path, PathBuf};
13
14const TEST_LUA: &str = include_str!("../lua/test.lua");
15const TEST_DTL: &str = include_str!("../lua/test.d.tl");
16
17pub const DEFAULT_LIB: &str = "htl.test";
19
20pub fn lib_dir() -> Result<PathBuf> {
23 let dir = std::env::temp_dir().join(format!("htl-lib-{}", env!("CARGO_PKG_VERSION")));
24 write_if_changed(&dir.join("htl").join("test.d.tl"), TEST_DTL)
25 .with_context(|| format!("writing bundled declarations under {}", dir.display()))?;
26 Ok(dir)
27}
28
29impl Htl {
30 pub fn install_test_lib(&self) -> Result<()> {
32 self.preload(DEFAULT_LIB, TEST_LUA)?;
33 self.add_path(&lib_dir()?)?;
34 Ok(())
35 }
36}
37
38#[derive(Debug, Default)]
40pub struct FileReport {
41 pub path: PathBuf,
42 pub check: CheckInfo,
43 pub error: Option<String>,
45 pub passed: usize,
46 pub failed: usize,
47 pub failures: Vec<String>,
48 pub file_level: bool,
50}
51
52impl FileReport {
53 pub fn ok(&self) -> bool {
54 self.check.ok() && self.error.is_none() && self.failed == 0
55 }
56}
57
58pub fn discover_tests(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
61 let mut out = Vec::new();
62 for p in paths {
63 if p.is_file() {
64 out.push(p.clone());
65 continue;
66 }
67 let walker = walkdir::WalkDir::new(p).sort_by_file_name().into_iter().filter_entry(|e| {
68 let n = e.file_name().to_string_lossy();
69 !(e.file_type().is_dir() && (n == "target" || n == ".git" || n == "node_modules"))
70 });
71 for e in walker {
72 let e = e?;
73 let path = e.path();
74 if !crate::is_tl_source(path) {
75 continue;
76 }
77 let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
78 let in_tests_dir = path
79 .strip_prefix(p)
80 .ok()
81 .map(|rel| rel.components().any(|c| c.as_os_str() == "tests"))
82 .unwrap_or(false);
83 if name.ends_with("_test.tl") || in_tests_dir {
84 out.push(path.to_path_buf());
85 }
86 }
87 }
88 out.sort();
89 out.dedup();
90 Ok(out)
91}
92
93pub fn run_test_file(path: &Path, filter: Option<&str>, lib: &str, lint_spec: Option<&str>) -> Result<FileReport> {
96 let mut rep = FileReport { path: path.to_path_buf(), ..Default::default() };
97 let h = Htl::new()?;
98 if let Some(spec) = lint_spec {
99 h.configure_lints(spec)?;
100 }
101 h.install_test_lib()?;
102 let dir = parent_dir(path);
103 h.add_path(&dir)?;
104 #[cfg(feature = "pkg")]
105 if let Some(p) = crate::pkg::Project::find(path) {
106 h.apply_project(&p)?;
107 }
108 if dir.file_name().is_some_and(|n| n == "tests")
110 && let Some(root) = dir.parent()
111 {
112 h.add_path(root)?;
113 h.add_path(&root.join("src"))?;
114 }
115 h.install_searcher()?;
116 h.set_arg(&path.to_string_lossy(), &[])?;
117
118 let (code, check) = h.gen_lua(path)?;
119 rep.check = check;
120 let Some(code) = code else { return Ok(rep) };
121 if let Err(e) = h.exec(&code, &format!("@{}", path.display()), &[]) {
122 rep.error = Some(e.to_string());
123 return Ok(rep);
124 }
125
126 let package: Table = h.lua().globals().get("package")?;
128 let loaded: Table = package.get("loaded")?;
129 match loaded.get::<Value>(lib)? {
130 Value::Table(t) => {
131 let run: Function = t.get("run")?;
132 let report: Table = run.call(filter)?;
133 rep.passed = report.get::<Option<usize>>("passed")?.unwrap_or(0);
134 rep.failed = report.get::<Option<usize>>("failed")?.unwrap_or(0);
135 if let Ok(f) = report.get::<Table>("failures") {
136 rep.failures = f.sequence_values::<String>().collect::<mlua::Result<_>>()?;
137 }
138 }
139 _ => {
140 rep.file_level = true;
141 rep.passed = 1;
142 }
143 }
144 Ok(rep)
145}