1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | use std::path::PathBuf; |
5 | |
6 | use regex::Regex; |
7 | |
8 | pub struct TestCase { |
9 | pub absolute_path: std::path::PathBuf, |
10 | pub relative_path: std::path::PathBuf, |
11 | pub requested_style: Option<&'static str>, |
12 | } |
13 | |
14 | impl TestCase { |
15 | /// Return a string which is a valid C++/Rust identifier |
16 | pub fn identifier(&self) -> String { |
17 | self.relative_path |
18 | .with_extension("" ) |
19 | .to_string_lossy() |
20 | .replace([std::path::MAIN_SEPARATOR, '-' ], to:"_" ) |
21 | } |
22 | |
23 | /// Returns true if the test case should be ignored for the specified driver. |
24 | pub fn is_ignored(&self, driver: &str) -> bool { |
25 | let source: String = std::fs::read_to_string(&self.absolute_path).unwrap(); |
26 | extract_ignores(&source).collect::<Vec<_>>().contains(&driver) |
27 | } |
28 | } |
29 | |
30 | /// Returns a list of all the `.slint` files in the subfolders e.g. `tests/cases` . |
31 | pub fn collect_test_cases(sub_folders: &str) -> std::io::Result<Vec<TestCase>> { |
32 | let mut results = vec![]; |
33 | |
34 | let mut all_styles = vec!["fluent" , "material" , "cupertino" , "cosmic" ]; |
35 | |
36 | // It is in the target/xxx/build directory |
37 | if std::env::var_os("OUT_DIR" ).map_or(false, |path| { |
38 | // Same logic as in i-slint-backend-selector's build script to get the path |
39 | let mut path: PathBuf = path.into(); |
40 | path.pop(); |
41 | path.pop(); |
42 | path.push("SLINT_DEFAULT_STYLE.txt" ); |
43 | std::fs::read_to_string(path).map_or(false, |style| style.trim().contains("qt" )) |
44 | }) { |
45 | all_styles.push("qt" ); |
46 | } |
47 | |
48 | let case_root_dir: std::path::PathBuf = |
49 | [env!("CARGO_MANIFEST_DIR" ), ".." , ".." , sub_folders].iter().collect(); |
50 | |
51 | println!("cargo:rerun-if-env-changed=SLINT_TEST_FILTER" ); |
52 | let filter = std::env::var("SLINT_TEST_FILTER" ).ok(); |
53 | |
54 | for entry in walkdir::WalkDir::new(case_root_dir.clone()).follow_links(true) { |
55 | let entry = entry?; |
56 | if entry.file_type().is_dir() { |
57 | println!("cargo:rerun-if-changed= {}" , entry.into_path().display()); |
58 | continue; |
59 | } |
60 | let absolute_path = entry.into_path(); |
61 | let relative_path = |
62 | std::path::PathBuf::from(absolute_path.strip_prefix(&case_root_dir).unwrap()); |
63 | if let Some(filter) = &filter { |
64 | if !relative_path.to_str().unwrap().contains(filter) { |
65 | continue; |
66 | } |
67 | } |
68 | if let Some(ext) = absolute_path.extension() { |
69 | if ext == "60" || ext == "slint" { |
70 | let styles_to_test: Vec<&'static str> = if relative_path.starts_with("widgets" ) { |
71 | let style_ignores = |
72 | extract_ignores(&std::fs::read_to_string(&absolute_path).unwrap()) |
73 | .filter_map(|ignore| { |
74 | ignore.strip_prefix("style-" ).map(ToString::to_string) |
75 | }) |
76 | .collect::<Vec<_>>(); |
77 | |
78 | all_styles |
79 | .iter() |
80 | .filter(|available_style| { |
81 | !style_ignores |
82 | .iter() |
83 | .any(|ignored_style| *available_style == ignored_style) |
84 | }) |
85 | .cloned() |
86 | .collect::<Vec<_>>() |
87 | } else { |
88 | vec!["" ] |
89 | }; |
90 | results.extend(styles_to_test.into_iter().map(|style| TestCase { |
91 | absolute_path: absolute_path.clone(), |
92 | relative_path: relative_path.clone(), |
93 | requested_style: if style.is_empty() { None } else { Some(style) }, |
94 | })); |
95 | } |
96 | } |
97 | } |
98 | Ok(results) |
99 | } |
100 | |
101 | /// A test functions looks something like |
102 | /// ````text |
103 | /// /* |
104 | /// ```cpp |
105 | /// TestCase instance; |
106 | /// assert(instance.x.get() == 0); |
107 | /// ``` |
108 | /// */ |
109 | /// ```` |
110 | pub struct TestFunction<'a> { |
111 | /// In the example above: `cpp` |
112 | pub language_id: &'a str, |
113 | /// The content of the test function |
114 | pub source: &'a str, |
115 | } |
116 | |
117 | /// Extract the test functions from |
118 | pub fn extract_test_functions(source: &str) -> impl Iterator<Item = TestFunction<'_>> { |
119 | lazy_static::lazy_static! { |
120 | static ref RX: Regex = Regex::new(r"(?sU)\r?\n```([a-z]+)\r?\n(.+)\r?\n```\r?\n" ).unwrap(); |
121 | } |
122 | RX.captures_iter(haystack:source).map(|mat: Captures<'_>| TestFunction { |
123 | language_id: mat.get(1).unwrap().as_str(), |
124 | source: mat.get(2).unwrap().as_str(), |
125 | }) |
126 | } |
127 | |
128 | #[test ] |
129 | fn test_extract_test_functions() { |
130 | let source: &str = r" |
131 | /* |
132 | ```cpp |
133 | auto xx = 0; |
134 | auto yy = 0; |
135 | ``` |
136 | |
137 | ```rust |
138 | let xx = 0; |
139 | let yy = 0; |
140 | ``` |
141 | */ |
142 | " ; |
143 | let mut r: impl Iterator- >
= extract_test_functions(source); |
144 | |
145 | let r1: TestFunction<'_> = r.next().unwrap(); |
146 | assert_eq!(r1.language_id, "cpp" ); |
147 | assert_eq!(r1.source, "auto xx = 0; \nauto yy = 0;" ); |
148 | |
149 | let r2: TestFunction<'_> = r.next().unwrap(); |
150 | assert_eq!(r2.language_id, "rust" ); |
151 | assert_eq!(r2.source, "let xx = 0; \nlet yy = 0;" ); |
152 | } |
153 | |
154 | #[test ] |
155 | fn test_extract_test_functions_win() { |
156 | let source: &str = "/* \r\n```cpp \r\nfoo \r\nbar \r\n``` \r\n*/ \r\n" ; |
157 | let mut r: impl Iterator- >
= extract_test_functions(source); |
158 | let r1: TestFunction<'_> = r.next().unwrap(); |
159 | assert_eq!(r1.language_id, "cpp" ); |
160 | assert_eq!(r1.source, "foo \r\nbar" ); |
161 | } |
162 | |
163 | /// Extract extra include paths from a comment in the source if present. |
164 | pub fn extract_include_paths(source: &str) -> impl Iterator<Item = &'_ str> { |
165 | lazy_static::lazy_static! { |
166 | static ref RX: Regex = Regex::new(r"//include_path:\s*(.+)\s*\n" ).unwrap(); |
167 | } |
168 | RX.captures_iter(haystack:source).map(|mat: Captures<'_>| mat.get(1).unwrap().as_str().trim()) |
169 | } |
170 | |
171 | #[test ] |
172 | fn test_extract_include_paths() { |
173 | assert!(extract_include_paths("something" ).next().is_none()); |
174 | |
175 | let source: &str = r" |
176 | //include_path: ../first |
177 | //include_path: ../second |
178 | Blah {} |
179 | " ; |
180 | |
181 | let r: Vec<&str> = extract_include_paths(source).collect::<Vec<_>>(); |
182 | assert_eq!(r, ["../first" , "../second" ]); |
183 | |
184 | // Windows \r\n |
185 | let source: &str = "//include_path: ../first \r\n//include_path: ../second \r\nBlah {} \r\n" ; |
186 | let r: Vec<&str> = extract_include_paths(source).collect::<Vec<_>>(); |
187 | assert_eq!(r, ["../first" , "../second" ]); |
188 | } |
189 | |
190 | /// Extract extra library paths from a comment in the source if present. |
191 | pub fn extract_library_paths(source: &str) -> impl Iterator<Item = (&'_ str, &'_ str)> { |
192 | lazy_static::lazy_static! { |
193 | static ref RX: Regex = Regex::new(r"//library_path\((.+)\):\s*(.+)\s*\n" ).unwrap(); |
194 | } |
195 | RXCaptureMatches<'_, '_>.captures_iter(haystack:source) |
196 | .map(|mat: Captures<'_>| (mat.get(1).unwrap().as_str().trim(), mat.get(2).unwrap().as_str().trim())) |
197 | } |
198 | |
199 | #[test ] |
200 | fn test_extract_library_paths() { |
201 | use std::collections::HashMap; |
202 | |
203 | assert!(extract_library_paths("something" ).next().is_none()); |
204 | |
205 | let source: &str = r" |
206 | //library_path(first): ../first/lib.slint |
207 | //library_path(second): ../second/lib.slint |
208 | Blah {} |
209 | " ; |
210 | |
211 | let r: HashMap<&str, &str> = extract_library_paths(source).collect::<HashMap<_, _>>(); |
212 | assert_eq!( |
213 | r, |
214 | HashMap::from([("first" , "../first/lib.slint" ), ("second" , "../second/lib.slint" )]) |
215 | ); |
216 | } |
217 | |
218 | /// Extract `//ignore` comments from the source. |
219 | fn extract_ignores(source: &str) -> impl Iterator<Item = &'_ str> { |
220 | lazy_static::lazy_static! { |
221 | static ref RX: Regex = Regex::new(r"//ignore:\s*(.+)\s*\n" ).unwrap(); |
222 | } |
223 | RX.captures_iter(haystack:source).flat_map(|mat: Captures<'_>| { |
224 | mat.get(1).unwrap().as_str().split(&[' ' , ',' ]).map(str::trim).filter(|s: &&str| !s.is_empty()) |
225 | }) |
226 | } |
227 | |
228 | #[test ] |
229 | fn test_extract_ignores() { |
230 | assert!(extract_ignores("something" ).next().is_none()); |
231 | |
232 | let source: &str = r" |
233 | //ignore: cpp |
234 | //ignore: rust, nodejs |
235 | Blah {} |
236 | " ; |
237 | |
238 | let r: Vec<&str> = extract_ignores(source).collect::<Vec<_>>(); |
239 | assert_eq!(r, ["cpp" , "rust" , "nodejs" ]); |
240 | } |
241 | |
242 | pub fn extract_cpp_namespace(source: &str) -> Option<String> { |
243 | lazy_static::lazy_static! { |
244 | static ref RX: Regex = Regex::new(r"//cpp-namespace:\s*(.+)\s*\n" ).unwrap(); |
245 | } |
246 | RX.captures(haystack:source).map(|mat: Captures<'_>| mat.get(1).unwrap().as_str().trim().to_string()) |
247 | } |
248 | |
249 | #[test ] |
250 | fn test_extract_cpp_namespace() { |
251 | assert!(extract_cpp_namespace("something" ).is_none()); |
252 | |
253 | let source: &str = r" |
254 | //cpp-namespace: ui |
255 | Blah {} |
256 | " ; |
257 | |
258 | let r: Option = extract_cpp_namespace(source); |
259 | assert_eq!(r, Some("ui" .to_string())); |
260 | } |
261 | |