Skip to main content

supercov_engine/
test_kind.rs

1//! A Python or Ruby test's kind from where it lives, read as the JavaScript
2//! runtime reads one (`runtime/javascript/provenance.mjs`): the nearest
3//! directory that names a kind, then the file's own name, then `unit`. A
4//! Rails app's Capybara tests live in `test/system` or `spec/features`, and
5//! its request specs in `spec/requests`; every one of them was reported as a
6//! unit test.
7
8const KINDS: &[(&str, &[&str])] = &[
9    ("unit", &["unit"]),
10    ("component", &["component", "components", "ct"]),
11    ("integration", &["integration", "int", "requests"]),
12    ("e2e", &["e2e", "end-to-end", "offline", "online"]),
13];
14
15/// Directories that say what a suite is only as a whole name.
16const DIRECTORIES: &[(&str, &str)] = &[("system", "e2e"), ("features", "e2e")];
17
18/// The kind one path segment names: the word furthest into it wins, as in
19/// `checkout-integration-e2e`.
20fn segment_kind(segment: &str) -> Option<&'static str> {
21    // Words split on `_`, `.`, `-` and camel case, keeping `end-to-end`.
22    let mut spaced = String::new();
23    let mut previous: Option<char> = None;
24    for character in segment.chars() {
25        if character.is_ascii_uppercase()
26            && previous.is_some_and(|before| before.is_ascii_lowercase() || before.is_ascii_digit())
27        {
28            spaced.push(' ');
29        }
30        spaced.push(if matches!(character, '_' | '.') {
31            ' '
32        } else {
33            character.to_ascii_lowercase()
34        });
35        previous = Some(character);
36    }
37    let spaced = spaced.replace("end-to-end", "end~to~end").replace('-', " ");
38    let mut best = None;
39    for (position, word) in spaced.split(' ').enumerate() {
40        let word = word.replace('~', "-");
41        if let Some((kind, _)) = KINDS
42            .iter()
43            .find(|(_, words)| words.contains(&word.as_str()))
44        {
45            best = Some((position, *kind));
46        }
47    }
48    best.map(|(_, kind)| kind)
49}
50
51/// The kind a tag names, as `@e2e` does: a test's own statement of what it
52/// is, which outranks where it lives.
53pub fn from_tags(tags: &[String]) -> Option<&'static str> {
54    tags.iter().find_map(
55        |tag| match tag.trim_start_matches('@').to_ascii_lowercase().as_str() {
56            "unit" => Some("unit"),
57            "component" => Some("component"),
58            "integration" => Some("integration"),
59            "e2e" => Some("e2e"),
60            _ => None,
61        },
62    )
63}
64
65/// A test's kind: the one a tag names, else the one its path names.
66pub fn with_tags(file: Option<&str>, tags: &[String]) -> &'static str {
67    from_tags(tags).unwrap_or_else(|| from_path(file))
68}
69
70pub fn from_path(file: Option<&str>) -> &'static str {
71    let Some(file) = file else {
72        return "unit";
73    };
74    let segments = file.split(['/', '\\']).collect::<Vec<_>>();
75    let (name, directories) = segments.split_last().unwrap_or((&"", &[]));
76    directories
77        .iter()
78        .rev()
79        .find_map(|directory| {
80            DIRECTORIES
81                .iter()
82                .find(|(name, _)| directory.eq_ignore_ascii_case(name))
83                .map(|(_, kind)| *kind)
84                .or_else(|| segment_kind(directory))
85        })
86        .or_else(|| segment_kind(name))
87        .unwrap_or("unit")
88}
89
90#[cfg(test)]
91mod tests {
92    use super::{from_path, from_tags, with_tags};
93
94    #[test]
95    fn a_tag_naming_a_level_is_the_kind_and_outranks_the_path() {
96        let tags = |values: &[&str]| {
97            values
98                .iter()
99                .map(|value| value.to_string())
100                .collect::<Vec<_>>()
101        };
102        assert_eq!(from_tags(&tags(&["api", "@E2E"])), Some("e2e"));
103        assert_eq!(from_tags(&tags(&["api", "smoke"])), None);
104        assert_eq!(
105            with_tags(Some("spec/models/a_spec.rb"), &tags(&["integration"])),
106            "integration"
107        );
108        assert_eq!(
109            with_tags(Some("test/e2e/a_test.py"), &tags(&["api"])),
110            "e2e"
111        );
112    }
113
114    #[test]
115    fn a_test_is_the_kind_its_directory_names() {
116        assert_eq!(from_path(Some("test/system/composer_test.rb")), "e2e");
117        assert_eq!(from_path(Some("spec/features/sign_in_spec.rb")), "e2e");
118        assert_eq!(
119            from_path(Some("spec/requests/rooms_spec.rb")),
120            "integration"
121        );
122        assert_eq!(
123            from_path(Some("tests/integration/test_api.py")),
124            "integration"
125        );
126        assert_eq!(from_path(Some("tests/e2e/test_flow.py")), "e2e");
127        assert_eq!(from_path(Some("test/models/user_test.rb")), "unit");
128        // The nearest directory wins, then the file's name.
129        assert_eq!(from_path(Some("tests/e2e/unit/test_x.py")), "unit");
130        assert_eq!(from_path(Some("tests/test_checkout_e2e.py")), "e2e");
131        assert_eq!(from_path(Some("tests/end-to-end/test_x.py")), "e2e");
132        assert_eq!(
133            from_path(Some("spec/apiIntegration_spec.rb")),
134            "integration"
135        );
136        // A word inside another word is not a kind.
137        assert_eq!(from_path(Some("tests/interior/test_x.py")), "unit");
138        assert_eq!(from_path(None), "unit");
139    }
140}