Skip to main content

htl_core/
testing.rs

1//! `htl test`: discovery, one isolated Lua state per test file, compile, report.
2//!
3//! The runner owns nothing about assertions. A test file `require`s a library; the
4//! bundled default is `htl.test` (`describe` / `it` / `expect`, typed via `test.d.tl`).
5//! Any library exposing `run(filter) -> { passed, failed, failures }` under the
6//! module name the runner is told about plugs in the same way. A file that uses no
7//! such library is judged at file level: it passes if it runs to completion.
8
9use 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
17/// Module name of the bundled assertion library.
18pub const DEFAULT_LIB: &str = "htl.test";
19
20/// What this library writes under [`crate::lib_dir`]: its declaration, and the path it
21/// takes there. A list of one, and a list rather than the constant because
22/// [`crate::lib_dir`] hashes it into the directory's name — the name and the contents come
23/// from the same place, so the first cannot describe files the binary does not write.
24pub(crate) fn declarations() -> Vec<(String, String)> {
25    vec![("htl/test.d.tl".to_string(), TEST_DTL.to_string())]
26}
27
28/// [`crate::lib_dir`] with this library's declaration in it: `htl/test.d.tl`, written on
29/// demand, only when its content changes.
30pub fn lib_dir() -> Result<PathBuf> {
31    let dir = crate::lib_dir();
32    for (path, source) in declarations() {
33        write_if_changed(&dir.join(path), &source)
34            .with_context(|| format!("writing bundled declarations under {}", dir.display()))?;
35    }
36    Ok(dir)
37}
38
39impl Htl {
40    /// Make `require("htl.test")` work at runtime and its types visible to the checker.
41    pub fn install_test_lib(&self) -> Result<()> {
42        // A label, not a path: the library ships inside the binary, so `htl/test.tl` is
43        // not a file anyone could open. Its frames read `htl.test:404` and stop there.
44        self.preload_at(DEFAULT_LIB, &format!("={DEFAULT_LIB}"), TEST_LUA)?;
45        self.add_path(&lib_dir()?)?;
46        Ok(())
47    }
48}
49
50/// Outcome of one test file.
51#[derive(Debug, Default, Clone)]
52pub struct FileReport {
53    /// The test file this is about, as it was discovered or named on the command line.
54    pub path: PathBuf,
55    /// What checking it found. A file that does not type-check still gets a report —
56    /// with this carrying the errors and no tests below it — rather than being dropped,
57    /// so a run says which files it could not get to.
58    pub check: CheckInfo,
59    /// Runtime error outside any test (e.g. the file itself raised).
60    pub error: Option<String>,
61    /// Tests the library reported as passing.
62    pub passed: usize,
63    /// Tests it reported as failing. `ok` is false while this is non-zero, and the run's
64    /// exit code is the sum of it over every file.
65    pub failed: usize,
66    /// One message per failure, already formatted by the library — the runner owns no
67    /// assertion and so has nothing of its own to say about why one failed.
68    pub failures: Vec<String>,
69    /// `true` when no test library was used and the verdict is file-level.
70    pub file_level: bool,
71    /// Per-test outcomes when the library reports them (`htl.test` does).
72    pub tests: Vec<TestResult>,
73    /// Wall time for the whole file: check, load, and every test.
74    pub duration_ms: f64,
75    /// Snapshot files created on this run (first `to_match_snapshot` of a name).
76    pub snapshots_written: Vec<String>,
77    /// Snapshot files rewritten because `update_snapshots` was set.
78    pub snapshots_updated: Vec<String>,
79    /// With `coverage`: `(chunk source as Lua names it, executed lines)`.
80    pub coverage: Vec<(String, Vec<usize>)>,
81}
82
83/// One test's outcome, as reported by the assertion library.
84#[derive(Debug, Clone, Default)]
85pub struct TestResult {
86    /// The name the library reported. `htl.test` joins its `describe` and `it` text with
87    /// ` > `, and that whole string is what `--filter` matches against.
88    pub name: String,
89    /// Whether it passed. A file's [`failures`](FileReport::failures) carry why the false
90    /// ones did; this is the per-test verdict a reporter lists.
91    pub ok: bool,
92    /// Wall time for this test alone, against
93    /// [`FileReport::duration_ms`](FileReport::duration_ms) for the file around it.
94    pub ms: f64,
95}
96
97/// Runner options passed through to the library's `run(filter, opts)`.
98#[derive(Debug, Clone, Default)]
99pub struct RunOptions {
100    /// Stop at the first failing test in a file.
101    pub fail_fast: bool,
102    /// Rewrite snapshots that differ instead of failing (`htl test --update`).
103    pub update_snapshots: bool,
104    /// Record executed lines per chunk while the file runs (`htl test --coverage`).
105    pub coverage: bool,
106    /// Seed for the run (`htl test --seed`). Each file draws from a stream derived from
107    /// this and its own path, so one file's values do not depend on which other files ran
108    /// or in what order: `--filter` reproduces what the full run did, and a failure can be
109    /// looked at again on its own. `None` leaves the state's own seeding alone.
110    pub seed: Option<u64>,
111}
112
113/// The seed one file gets, from the run's seed and its path.
114///
115/// Derived rather than taken from a shared stream, so the answer to "what did this file
116/// draw" does not depend on the company it kept. SplitMix64 over an FNV-1a of the path:
117/// the point is that neighbouring paths land far apart, not that it is unguessable.
118pub fn file_seed(run_seed: u64, path: &Path) -> u64 {
119    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
120    for b in path.to_string_lossy().as_bytes() {
121        h ^= *b as u64;
122        h = h.wrapping_mul(0x0000_0100_0000_01b3);
123    }
124    let mut z = run_seed.wrapping_add(h).wrapping_add(0x9e37_79b9_7f4a_7c15);
125    z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
126    z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
127    z ^ (z >> 31)
128}
129
130/// Where a test file's snapshots live: `<dir>/__snapshots__/<file stem>/`.
131pub fn snapshot_dir(test_file: &Path) -> PathBuf {
132    let stem = test_file
133        .file_stem()
134        .and_then(|s| s.to_str())
135        .unwrap_or("test");
136    parent_dir(test_file).join("__snapshots__").join(stem)
137}
138
139impl FileReport {
140    /// Whether this file is a pass: it checked, it did not raise outside a test, and no
141    /// test failed. The three are separate fields because a reporter says which of them
142    /// went wrong, and one answer is what an exit code needs.
143    pub fn ok(&self) -> bool {
144        self.check.ok() && self.error.is_none() && self.failed == 0
145    }
146}
147
148/// `*_test.tl` anywhere, plus every `.tl` under a directory named `tests`.
149/// Explicit file paths are always included. Does not enter [`crate::SKIP_DIRS`],
150/// dot-directories or the project's mlua-pkg dir (dependencies' tests are theirs).
151pub fn discover_tests(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
152    discover_tests_skipping(paths, &[])
153}
154
155/// [`discover_tests`], not entering `skip` either — directories named by path rather than
156/// by name ([`crate::patched_dirs`]: a patched dependency's `*_test.tl` are its own suite,
157/// not the project's).
158pub fn discover_tests_skipping(paths: &[PathBuf], skip: &[PathBuf]) -> Result<Vec<PathBuf>> {
159    let mut out = Vec::new();
160    for p in paths {
161        if p.is_file() {
162            out.push(p.clone());
163            continue;
164        }
165        let mut extra = crate::project_skip_dirs(p);
166        extra.extend(skip.iter().cloned());
167        let root = p.clone();
168        let walker = walkdir::WalkDir::new(p)
169            .sort_by_file_name()
170            .into_iter()
171            .filter_entry(move |e| e.path() == root || !crate::is_skipped_dir(e.path(), &extra));
172        for e in walker {
173            let e = e?;
174            let path = e.path();
175            if !crate::is_tl_source(path) {
176                continue;
177            }
178            let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
179            let in_tests_dir = path
180                .strip_prefix(p)
181                .ok()
182                .map(|rel| rel.components().any(|c| c.as_os_str() == "tests"))
183                .unwrap_or(false);
184            if name.ends_with("_test.tl") || in_tests_dir {
185                out.push(path.to_path_buf());
186            }
187        }
188    }
189    out.sort();
190    out.dedup();
191    Ok(out)
192}
193
194/// Run one test file in a fresh state. `lib` is the assertion library module to
195/// consult for `run(filter)` after the file executed (default: `htl.test`).
196pub fn run_test_file(
197    path: &Path,
198    filter: Option<&str>,
199    lib: &str,
200    lint_spec: Option<&str>,
201    opts: &RunOptions,
202) -> Result<FileReport> {
203    TestSession::new(lint_spec, lib, filter, opts.clone())?.run_file(path)
204}
205
206/// A whole project's tests, run from Rust.
207///
208/// [`run_test_file`] runs one file and [`crate::project::test`] runs a project; this is
209/// the second of those with the parts a command line supplies filled in with defaults, so
210/// that a host embedding Teal can put its scripts' tests in `cargo test` instead of
211/// shelling out to `htl test`:
212///
213/// ```rust,ignore
214/// #[test]
215/// fn teal_tests_pass() {
216///     let rep = htl::testing::run_tests(&["scripts".into()], &htl::testing::Suite::default())
217///         .expect("the run itself");
218///     assert!(rep.ok(), "{:#?}", rep.failures());
219/// }
220/// ```
221///
222/// Every field is what `htl test` takes a flag for, and [`Default`] is what the command
223/// does with no flags — except the store, which is off here: a run under `cargo test`
224/// starts wherever cargo put it, and a host that wants the cache asks for it.
225///
226/// A whole project is more than one file, so this is the project layer's
227/// ([`crate::project::test`]) and asks for its features; the umbrella `htl` crate a host
228/// depends on has both.
229#[cfg(all(feature = "pkg", feature = "dts"))]
230#[derive(Debug, Clone, Default)]
231pub struct Suite {
232    /// Run only the tests whose name contains this (`htl test --filter`).
233    pub filter: Option<String>,
234    /// Module name of the assertion library, when it is not [`DEFAULT_LIB`].
235    pub lib: Option<String>,
236    /// A lint selection, merged after `htl.toml`'s so that it wins (`htl test --lint`).
237    pub lint: Option<String>,
238    /// Fail-fast, snapshot updating, coverage and the seed. `seed: None` draws one for the
239    /// run and [`SuiteReport::seed`] says which, so a failure can be repeated.
240    pub run: RunOptions,
241    /// Keep a run cache under the project root, as `htl test` does. Off by default.
242    pub cache: bool,
243}
244
245/// What a [`run_tests`] run found: every file's report, and the counts over them.
246#[cfg(all(feature = "pkg", feature = "dts"))]
247#[derive(Debug)]
248pub struct SuiteReport {
249    /// One per file that ran, in the order they ran.
250    pub files: Vec<FileReport>,
251    /// Every diagnostic the checks produced, over the whole run, as values.
252    pub diagnostics: Vec<crate::Diagnostic>,
253    /// Tests that passed, summed over the files that ran — tests, not files, so a run of
254    /// one file with forty assertions is forty here and one in [`files`](Self::files).
255    pub passed: usize,
256    /// Tests that failed, summed the same way. A file that failed to check contributes
257    /// nothing to either count and shows up in
258    /// [`files_with_errors`](Self::files_with_errors) instead, which is why that is the
259    /// field [`ok`](Self::ok) reads.
260    pub failed: usize,
261    /// Files that failed to check, raised, or had a failing test.
262    pub files_with_errors: usize,
263    /// Files discovered but never run, because `fail_fast` stopped the run.
264    pub skipped: usize,
265    /// The seed every file's stream was derived from.
266    pub seed: u64,
267    /// Wall time for the run: discovery, every file's
268    /// [`duration_ms`](FileReport::duration_ms), and the coverage report when one was
269    /// asked for. Larger than the files' sum rather than equal to it.
270    pub duration_ms: f64,
271    /// With `run.coverage`: what the line hooks saw.
272    pub coverage: Option<crate::project::CoverageReport>,
273}
274
275#[cfg(all(feature = "pkg", feature = "dts"))]
276impl SuiteReport {
277    /// Whether every file that ran was ok. What an assertion in a `#[test]` reads.
278    pub fn ok(&self) -> bool {
279        self.files_with_errors == 0
280    }
281
282    /// What to put in the assertion message: each failing file with what went wrong —
283    /// a check that failed, a file that raised, or the tests that did not pass.
284    pub fn failures(&self) -> Vec<String> {
285        let mut out = Vec::new();
286        for f in self.files.iter().filter(|f| !f.ok()) {
287            let at = f.path.display();
288            if !f.check.ok() {
289                out.extend(f.check.errors.iter().map(|e| format!("{at}: {e}")));
290            }
291            if let Some(e) = &f.error {
292                out.push(format!("{at}: {e}"));
293            }
294            out.extend(f.failures.iter().map(|m| format!("{at}: {m}")));
295        }
296        out
297    }
298}
299
300/// Run the tests under `paths` — a project root, a directory, or the files themselves —
301/// and hand back what happened.
302///
303/// Discovery is [`discover_tests_skipping`]'s: `*_test.tl` anywhere and every `.tl` under a
304/// `tests` directory, minus a patched dependency's own suite. `htl.toml` is found from the
305/// first path, as the command does. Nothing is printed and nothing decides an exit code —
306/// [`SuiteReport`] is the whole answer, and asserting on it is the caller's.
307#[cfg(all(feature = "pkg", feature = "dts"))]
308pub fn run_tests(paths: &[PathBuf], suite: &Suite) -> Result<SuiteReport> {
309    use crate::project;
310    let paths: Vec<PathBuf> = if paths.is_empty() {
311        vec![PathBuf::from(".")]
312    } else {
313        paths.to_vec()
314    };
315    let files = discover_tests_skipping(&paths, &project::patched(&paths))?;
316    let cfg = project::config_of(&paths[0])?;
317    let opts = project::TestOptions {
318        config: &cfg,
319        lint: suite.lint.as_deref(),
320        lib: suite.lib.as_deref().unwrap_or(DEFAULT_LIB),
321        filter: suite.filter.as_deref(),
322        run: suite.run.clone(),
323        cache: project::cache_options(
324            suite.cache,
325            Some(crate::cache::Mode::PerModule),
326            &cfg,
327            false,
328        ),
329    };
330    let mut sink = project::Sink::new(project::Collect::default());
331    let mut reports: Vec<FileReport> = Vec::new();
332    let mut diagnostics: Vec<crate::Diagnostic> = Vec::new();
333    let rep = project::test(&mut sink, &files, &opts, &mut |r, sink| {
334        diagnostics.extend(sink.out().take());
335        reports.push(r.clone());
336    })?;
337    Ok(SuiteReport {
338        files: reports,
339        diagnostics,
340        passed: rep.passed,
341        failed: rep.failed,
342        files_with_errors: rep.files_with_errors,
343        skipped: rep.skipped(),
344        seed: rep.seed,
345        duration_ms: rep.duration_ms,
346        coverage: rep.coverage,
347    })
348}
349
350/// One checker for a whole run: every test file gets its own fresh program state
351/// (globals, `package.loaded`, module state), but modules are type-checked and
352/// generated once and served to every file from the checker's store.
353pub struct TestSession {
354    checker: Htl,
355    lib: String,
356    filter: Option<String>,
357    opts: RunOptions,
358}
359
360impl TestSession {
361    /// Build the one checker a run shares. `lint_spec` is the same `--lint` string the
362    /// CLI takes and is applied once here; `lib` is the module a test file requires for
363    /// its assertions ([`DEFAULT_LIB`] unless the caller has its own); `filter` and
364    /// `opts` are handed to that library's `run` for every file.
365    pub fn new(
366        lint_spec: Option<&str>,
367        lib: &str,
368        filter: Option<&str>,
369        opts: RunOptions,
370    ) -> Result<Self> {
371        let checker = Htl::new()?;
372        if let Some(spec) = lint_spec {
373            checker.configure_lints(spec)?;
374        }
375        Ok(Self {
376            checker,
377            lib: lib.to_string(),
378            filter: filter.map(String::from),
379            opts,
380        })
381    }
382
383    /// The session's checker (for [`Htl::executable_ranges`] on the sources a run touched).
384    pub fn checker(&self) -> &Htl {
385        &self.checker
386    }
387
388    /// Run one file in a fresh program state borrowing the session's checker. The
389    /// checker's search path is restored afterwards so files do not see each other's
390    /// directories.
391    pub fn run_file(&self, path: &Path) -> Result<FileReport> {
392        self.run_file_with(path, None, &[]).map(|(rep, _)| rep)
393    }
394
395    /// Run one file, reusing Lua the caller generated earlier.
396    ///
397    /// `generated` is `(the Lua, what checking it reported)`. Everything before the codegen
398    /// still happens — the searcher, the search path, the project and the config all have to
399    /// be in place before the code can execute — and everything after it happens as usual.
400    /// Only the check and the codegen are skipped.
401    ///
402    /// The run itself is never reused, and this signature cannot express reusing it: a test
403    /// has to run to say whether it passes.
404    ///
405    /// Returns the report and, when this call generated the Lua rather than being handed it,
406    /// that Lua — so a caller keeping a cache has something to keep.
407    /// `preload` is `(module name, its generated Lua, the file it came from)` for modules
408    /// this file will require. Each one goes in front of the searcher, so requiring it does
409    /// not check and generate it during the run. A module not in the list still loads the
410    /// usual way; the list is an optimisation, never a restriction on what can be required.
411    pub fn run_file_with(
412        &self,
413        path: &Path,
414        generated: Option<(&str, &CheckInfo)>,
415        preload: &[(String, String, PathBuf)],
416    ) -> Result<(FileReport, Option<String>)> {
417        let started = std::time::Instant::now();
418        let saved = self.checker.search_path()?;
419        let h = Htl::with_checker(&self.checker)?;
420        let mut code = None;
421        let out = run_in(
422            &h,
423            path,
424            RunIn {
425                filter: self.filter.as_deref(),
426                lib: &self.lib,
427                opts: &self.opts,
428                generated,
429                preload,
430            },
431            &mut code,
432        );
433        self.checker.set_search_path(&saved)?;
434        let mut rep = out?;
435        rep.duration_ms = started.elapsed().as_secs_f64() * 1000.0;
436        Ok((rep, code))
437    }
438}
439
440/// What one file's run needs from its session, and what the caller already has for it.
441struct RunIn<'a> {
442    filter: Option<&'a str>,
443    lib: &'a str,
444    opts: &'a RunOptions,
445    /// Lua and diagnostics from an earlier run, when the caller kept them.
446    generated: Option<(&'a str, &'a CheckInfo)>,
447    /// Modules to put in front of the searcher before the file executes.
448    preload: &'a [(String, String, PathBuf)],
449}
450
451fn run_in(h: &Htl, path: &Path, r: RunIn<'_>, out_code: &mut Option<String>) -> Result<FileReport> {
452    let RunIn {
453        filter,
454        lib,
455        opts,
456        generated,
457        preload,
458    } = r;
459    let mut rep = FileReport {
460        path: path.to_path_buf(),
461        ..Default::default()
462    };
463    let profile = std::env::var_os("HTL_PROFILE").is_some();
464    let mut t0 = std::time::Instant::now();
465    let phase = |label: &str, t0: &mut std::time::Instant| {
466        if profile {
467            eprintln!(
468                "profile: {label:<8} {:7.1} ms  {}",
469                t0.elapsed().as_secs_f64() * 1000.0,
470                path.display()
471            );
472        }
473        *t0 = std::time::Instant::now();
474    };
475    phase("state", &mut t0);
476    h.install_test_lib()?;
477    #[cfg(feature = "std")]
478    h.install_std()?;
479    let dir = parent_dir(path);
480    h.add_path(&dir)?;
481    #[cfg(feature = "pkg")]
482    if let Some(p) = crate::pkg::Project::find(path) {
483        h.apply_project(&p)?;
484    }
485    if let Some((cfg_path, cfg)) = crate::config::HtlConfig::find(path)? {
486        h.apply_config(&parent_dir(&cfg_path), &cfg)?;
487    }
488    // tests/foo_test.tl commonly requires modules from the project root or src/.
489    if dir.file_name().is_some_and(|n| n == "tests")
490        && let Some(root) = dir.parent()
491    {
492        h.add_path(root)?;
493        h.add_path(&root.join("src"))?;
494    }
495    h.install_searcher()?;
496    // Before the searcher gets a chance to be asked. Position 1 beats position 2.
497    for (name, code, from) in preload {
498        h.preload_generated(name, code, from)?;
499    }
500    h.set_arg(&path.to_string_lossy(), &[])?;
501    phase("setup", &mut t0);
502
503    let (code, check) = match generated {
504        Some((code, check)) => (Some(code.to_string()), check.clone()),
505        None => {
506            let (code, check) = h.gen_lua(path)?;
507            // Hand the caller what was generated, before any of the early returns below: a
508            // file whose tests fail still generated the Lua that failed, and the next run
509            // should not have to generate it again to find that out.
510            *out_code = code.clone();
511            (code, check)
512        }
513    };
514    phase("gen_lua", &mut t0);
515    rep.check = check;
516    let Some(code) = code else { return Ok(rep) };
517    // Before the file runs, so a module it requires draws from the same stream. Seeding
518    // the state's own generator rather than handing out a private one means a test that
519    // already calls `math.random` becomes reproducible without being rewritten.
520    if let Some(run_seed) = opts.seed {
521        let math: Table = h.lua().globals().get("math")?;
522        let randomseed: Function = math.get("randomseed")?;
523        randomseed.call::<()>(file_seed(run_seed, path) as i64)?;
524    }
525    if opts.coverage {
526        h.coverage_start()?;
527    }
528    if let Err(e) = h.exec(&code, &format!("@{}", path.display()), &[]) {
529        // With the frames: a file that raised while loading is a development failure, and
530        // the per-test failures beside it have carried a traceback all along.
531        rep.error = Some(crate::developer_message(&e));
532        if opts.coverage {
533            rep.coverage = h.coverage_stop()?;
534        }
535        return Ok(rep);
536    }
537    phase("exec", &mut t0);
538
539    // Did the file load the assertion library? Then ask it for the verdict.
540    let package: Table = h.lua().globals().get("package")?;
541    let loaded: Table = package.get("loaded")?;
542    match loaded.get::<Value>(lib)? {
543        Value::Table(t) => {
544            // Snapshots: tell the library where this file's live and whether to
545            // rewrite them. Lua cannot create a directory, so it borrows one.
546            if let Ok(configure) = t.get::<Function>("configure") {
547                let cfg = h.lua().create_table()?;
548                cfg.set(
549                    "snapshot_dir",
550                    snapshot_dir(path).to_string_lossy().as_ref(),
551                )?;
552                cfg.set("update", opts.update_snapshots)?;
553                cfg.set(
554                    "mkdir",
555                    h.lua().create_function(|_, dir: String| {
556                        std::fs::create_dir_all(&dir).map_err(mlua::Error::external)
557                    })?,
558                )?;
559                configure.call::<()>(cfg)?;
560            }
561            let run: Function = t.get("run")?;
562            let lua_opts = h.lua().create_table()?;
563            lua_opts.set("fail_fast", opts.fail_fast)?;
564            let report: Table = run.call((filter, lua_opts))?;
565            for (key, into) in [
566                ("snapshots_written", &mut rep.snapshots_written),
567                ("snapshots_updated", &mut rep.snapshots_updated),
568            ] {
569                if let Ok(list) = report.get::<Table>(key) {
570                    *into = list
571                        .sequence_values::<String>()
572                        .collect::<mlua::Result<_>>()?;
573                }
574            }
575            phase("run", &mut t0);
576            rep.passed = report.get::<Option<usize>>("passed")?.unwrap_or(0);
577            rep.failed = report.get::<Option<usize>>("failed")?.unwrap_or(0);
578            if let Ok(f) = report.get::<Table>("failures") {
579                rep.failures = f.sequence_values::<String>().collect::<mlua::Result<_>>()?;
580            }
581            if let Ok(tests) = report.get::<Table>("tests") {
582                for tr in tests.sequence_values::<Table>() {
583                    let tr = tr?;
584                    rep.tests.push(TestResult {
585                        name: tr.get::<Option<String>>("name")?.unwrap_or_default(),
586                        ok: tr.get::<Option<bool>>("ok")?.unwrap_or(false),
587                        ms: tr.get::<Option<f64>>("ms")?.unwrap_or(0.0),
588                    });
589                }
590            }
591        }
592        _ => {
593            rep.file_level = true;
594            rep.passed = 1;
595        }
596    }
597    if opts.coverage {
598        rep.coverage = h.coverage_stop()?;
599    }
600    Ok(rep)
601}