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
4use std::path::PathBuf;
5
6use regex::Regex;
7
8pub 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
14impl 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` .
31pub 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/// ````
110pub 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
118pub 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]
129fn test_extract_test_functions() {
130 let source: &str = r"
131/*
132```cpp
133auto xx = 0;
134auto yy = 0;
135```
136
137```rust
138let xx = 0;
139let 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]
155fn 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.
164pub 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]
172fn 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.
191pub 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]
200fn 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.
219fn 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]
229fn 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
242pub 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]
250fn 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