Skip to main content

crap4rust/
test_module_registry.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use std::collections::HashSet;
6use std::path::{Path, PathBuf};
7
8use syn::{File, Item, ItemMod};
9
10use crate::impl_collector::is_test_attrs;
11use crate::normalize_path::normalize_path;
12
13pub struct TestModuleRegistry {
14    excluded_files: HashSet<String>,
15}
16
17impl TestModuleRegistry {
18    #[must_use]
19    pub fn build(files: &[(PathBuf, File)]) -> Self {
20        let mut excluded_files = HashSet::new();
21        for (file_path, syntax) in files {
22            let base_dir = Self::own_base_dir(file_path);
23            Self::collect_excluded_mods(&syntax.items, &base_dir, &mut excluded_files);
24        }
25        Self { excluded_files }
26    }
27
28    #[must_use]
29    pub fn is_excluded(&self, file_path: &Path) -> bool {
30        self.excluded_files.contains(&normalize_path(file_path))
31    }
32
33    fn own_base_dir(file_path: &Path) -> PathBuf {
34        let parent = file_path
35            .parent()
36            .map(Path::to_path_buf)
37            .unwrap_or_default();
38        let stem = file_path
39            .file_stem()
40            .and_then(|stem| stem.to_str())
41            .unwrap_or_default();
42        if matches!(stem, "lib" | "main" | "mod") {
43            parent
44        } else {
45            parent.join(stem)
46        }
47    }
48
49    fn collect_excluded_mods(
50        items: &[Item],
51        base_dir: &Path,
52        excluded_files: &mut HashSet<String>,
53    ) {
54        for item in items {
55            let Item::Mod(item_mod) = item else {
56                continue;
57            };
58            match &item_mod.content {
59                Some((_, nested_items)) => {
60                    let nested_dir = base_dir.join(item_mod.ident.to_string());
61                    Self::collect_excluded_mods(nested_items, &nested_dir, excluded_files);
62                }
63                None => Self::register_if_test_gated(item_mod, base_dir, excluded_files),
64            }
65        }
66    }
67
68    fn register_if_test_gated(
69        item_mod: &ItemMod,
70        base_dir: &Path,
71        excluded_files: &mut HashSet<String>,
72    ) {
73        if !is_test_attrs(&item_mod.attrs) {
74            return;
75        }
76        if let Some(target) = Self::resolve_file_module(base_dir, item_mod) {
77            excluded_files.insert(normalize_path(&target));
78        }
79    }
80
81    fn resolve_file_module(base_dir: &Path, item_mod: &ItemMod) -> Option<PathBuf> {
82        let name = item_mod.ident.to_string();
83
84        let sibling_file = base_dir.join(format!("{name}.rs"));
85        if sibling_file.exists() {
86            return Some(sibling_file);
87        }
88
89        let directory_file = base_dir.join(&name).join("mod.rs");
90        if directory_file.exists() {
91            return Some(directory_file);
92        }
93
94        None
95    }
96}