anytest/
context.rs

1use crate::{named_pattern::NamedPattern, LineNr, RelPath};
2use clap::ValueEnum;
3use std::{error::Error, ops, path::PathBuf};
4
5#[derive(ValueEnum, Clone, Debug)]
6pub enum Scope {
7    Suite,
8    File,
9    Line,
10}
11
12pub struct Nearest {
13    tests: Vec<String>,
14    namespaces: Vec<String>,
15    line_nr: Option<LineNr>,
16    // names: Vec<String>,
17}
18
19impl Nearest {
20    pub fn tests(&self) -> &[String] {
21        &self.tests
22    }
23
24    pub fn namespaces(&self) -> &[String] {
25        &self.namespaces
26    }
27
28    pub fn line_nr(&self) -> Option<LineNr> {
29        self.line_nr
30    }
31
32    pub fn has_tests(&self) -> bool {
33        !self.tests.is_empty()
34    }
35}
36
37#[derive(Debug)]
38pub struct Context {
39    rel_path: RelPath,
40    line_nr: Option<LineNr>,
41    scope: Scope,
42}
43
44impl Context {
45    pub fn new(
46        root: Option<&str>,
47        path: &str,
48        line_nr: Option<LineNr>,
49        scope: Option<Scope>,
50    ) -> Result<Self, Box<dyn Error>> {
51        let rel_path = RelPath::new(root, path)?;
52        let scope = if let Some(scope) = scope {
53            scope
54        } else if line_nr.is_some() {
55            Scope::Line
56        } else {
57            Scope::File
58        };
59
60        Ok(Self {
61            rel_path,
62            line_nr,
63            scope,
64        })
65    }
66
67    pub fn root(&self) -> &PathBuf {
68        self.rel_path.root()
69    }
70
71    pub fn path(&self) -> &PathBuf {
72        self.rel_path.path()
73    }
74
75    pub fn rel(&self) -> &PathBuf {
76        self.rel_path.rel()
77    }
78
79    pub fn rel_str(&self) -> &str {
80        self.rel_path.rel_str()
81    }
82
83    pub fn line_nr(&self) -> Option<LineNr> {
84        self.line_nr
85    }
86
87    pub fn line_nr_or_default(&self) -> LineNr {
88        self.line_nr.unwrap_or(1)
89    }
90
91    pub fn rel_full(&self) -> String {
92        format!("{}:{}", self.rel_str(), self.line_nr_or_default())
93    }
94
95    pub fn scope(&self) -> &Scope {
96        &self.scope
97    }
98
99    pub fn find_nearest(
100        &self,
101        test_patterns: &[NamedPattern],
102        namespace_patters: &[NamedPattern],
103        range: impl ops::RangeBounds<LineNr>,
104    ) -> Result<Nearest, Box<dyn Error>> {
105        if test_patterns.is_empty() {
106            return Err("Test patterns are empty".into());
107        }
108
109        let mut tests: Vec<String> = Vec::new();
110        let mut namespaces: Vec<String> = Vec::new();
111        // let names: Vec<String> = Vec::new();
112        let mut test_line_nr: Option<LineNr> = None;
113        let mut last_namespace_line_nr: Option<LineNr> = None;
114        let mut last_indent: Option<LineNr> = None;
115
116        for (line, number) in self.rel_path.lines(range)? {
117            let test_match = test_patterns.iter().find_map(|pattern| pattern.find(&line));
118            let namespace_match = namespace_patters
119                .iter()
120                .find_map(|pattern| pattern.find(&line));
121            let indent = line.chars().take_while(|c| c.is_whitespace()).count();
122
123            if let Some((test_match, _)) = test_match {
124                if last_indent.is_none()
125                    || (test_line_nr.is_none()
126                        && last_indent.unwrap() > indent
127                        && last_namespace_line_nr.is_some()
128                        && last_namespace_line_nr.unwrap() > number)
129                {
130                    if let Some(namespace_line_nr) = last_namespace_line_nr {
131                        if namespace_line_nr > number {
132                            namespaces.clear();
133                            last_namespace_line_nr = None;
134                        }
135                    }
136                    tests.push(test_match);
137                    // if let Some(test_name) = test_name {
138                    //     names.push(test_name);
139                    // }
140                    last_indent = Some(indent);
141                    test_line_nr = Some(number);
142                }
143            } else if let Some((namespace_match, _)) = namespace_match {
144                if last_indent.is_none() || indent < last_indent.unwrap() {
145                    namespaces.push(namespace_match);
146                    last_indent = Some(indent);
147                    last_namespace_line_nr = Some(number);
148                }
149            }
150        }
151
152        namespaces.reverse();
153        Ok(Nearest {
154            tests,
155            namespaces,
156            line_nr: test_line_nr,
157        })
158    }
159
160    pub fn find_file(&self, rel_path: &str) -> Option<RelPath> {
161        if let Ok(rel_path) = self.rel_path.file(rel_path) {
162            Some(rel_path)
163        } else {
164            None
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    fn get_scope(line: Option<LineNr>, scope: Option<Scope>) -> Scope {
174        let context = Context::new(Some("tests/fixtures/folder"), "file.txt", line, scope).unwrap();
175        context.scope().clone()
176    }
177
178    #[test]
179    fn test_context_new() {
180        assert!(matches!(get_scope(Some(123), None), Scope::Line));
181        assert!(matches!(
182            get_scope(Some(123), Some(Scope::Suite)),
183            Scope::Suite
184        ));
185        assert!(matches!(get_scope(None, None), Scope::File));
186    }
187
188    fn find_nearest(
189        test_patterns: &[NamedPattern],
190        namespace_patters: &[NamedPattern],
191        range: impl ops::RangeBounds<LineNr>,
192    ) -> Nearest {
193        Context::new(Some("tests/fixtures/folder"), "file.rb", None, None)
194            .unwrap()
195            .find_nearest(test_patterns, namespace_patters, range)
196            .unwrap()
197    }
198
199    #[test]
200    fn test_content_find_nearest() {
201        let nearest = find_nearest(
202            &[r"^\s*def\s+(test_\w+)".into()],
203            &[r"^\s*(?:class|module)\s+(\S+)".into()],
204            2..=1,
205        );
206
207        assert_eq!(nearest.tests(), vec!["test_method".to_string()]);
208        assert_eq!(nearest.namespaces(), vec!["TestClass".to_string()]);
209        assert_eq!(nearest.line_nr(), Some(2));
210    }
211}