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 }
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 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 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}