1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use std::io::{BufWriter, Write};
5use std::path::Path;
6
7/// Returns a list of all the `.slint` files in the `tests/cases` subfolders.
8pub fn collect_test_cases() -> std::io::Result<Vec<test_driver_lib::TestCase>> {
9 let mut results = vec![];
10
11 let case_root_dir: std::path::PathBuf = [env!("CARGO_MANIFEST_DIR"), "cases"].iter().collect();
12
13 println!("cargo:rerun-if-env-changed=SLINT_TEST_FILTER");
14 let filter = std::env::var("SLINT_TEST_FILTER").ok();
15
16 for entry in walkdir::WalkDir::new(case_root_dir.clone()).follow_links(true) {
17 let entry = entry?;
18 let absolute_path = entry.into_path();
19 if absolute_path.is_dir() {
20 println!("cargo:rerun-if-changed={}", absolute_path.display());
21 continue;
22 }
23 let relative_path =
24 std::path::PathBuf::from(absolute_path.strip_prefix(&case_root_dir).unwrap());
25 if let Some(filter) = &filter {
26 if !relative_path.to_str().unwrap().contains(filter) {
27 continue;
28 }
29 }
30 if let Some(ext) = absolute_path.extension() {
31 if ext == "60" || ext == "slint" {
32 results.push(test_driver_lib::TestCase {
33 absolute_path,
34 relative_path,
35 requested_style: None,
36 });
37 }
38 }
39 }
40 Ok(results)
41}
42
43fn main() -> std::io::Result<()> {
44 let default_font_path: std::path::PathBuf =
45 [env!("CARGO_MANIFEST_DIR"), "..", "..", "demos", "printerdemo", "ui", "fonts"]
46 .iter()
47 .collect();
48
49 std::env::set_var("SLINT_DEFAULT_FONT", default_font_path.clone());
50 println!("cargo:rustc-env=SLINT_DEFAULT_FONT={}", default_font_path.display());
51
52 let mut generated_file = BufWriter::new(std::fs::File::create(
53 Path::new(&std::env::var_os("OUT_DIR").unwrap()).join("generated.rs"),
54 )?);
55
56 let references_root_dir: std::path::PathBuf =
57 [env!("CARGO_MANIFEST_DIR"), "references"].iter().collect();
58
59 let font_cache = i_slint_compiler::FontCache::default();
60
61 for (i, testcase) in
62 test_driver_lib::collect_test_cases("screenshots/cases")?.into_iter().enumerate()
63 {
64 let mut reference_path = references_root_dir
65 .join(testcase.relative_path.clone())
66 .with_extension("png")
67 .to_str()
68 .unwrap()
69 .escape_default()
70 .to_string();
71
72 reference_path = format!("\"{reference_path}\"");
73
74 println!("cargo:rerun-if-changed={}", testcase.absolute_path.display());
75 let mut module_name = testcase.identifier();
76 if module_name.starts_with(|c: char| !c.is_ascii_alphabetic()) {
77 module_name.insert(0, '_');
78 }
79 writeln!(generated_file, "#[path=\"{module_name}.rs\"] mod r#{module_name};")?;
80 let source = std::fs::read_to_string(&testcase.absolute_path)?;
81
82 let needle = "SLINT_SCALE_FACTOR=";
83 let scale_factor = source.find(needle).map(|p| {
84 let source = &source[p + needle.len()..];
85 let scale_factor: f32 = source
86 .find(char::is_whitespace)
87 .and_then(|end| source[..end].parse().ok())
88 .unwrap_or_else(|| {
89 panic!("Cannot parse {needle} for {}", testcase.relative_path.display())
90 });
91 scale_factor
92 });
93
94 let needle = "ROTATION_THRESHOLD=";
95 let rotation_threshold = source.find(needle).map_or(0., |p| {
96 source[p + needle.len()..]
97 .find(char::is_whitespace)
98 .and_then(|end| source[p + needle.len()..][..end].parse().ok())
99 .unwrap_or_else(|| {
100 panic!("Cannot parse {needle} for {}", testcase.relative_path.display())
101 })
102 });
103 let skip_clipping = source.contains("SKIP_CLIPPING");
104
105 let needle = "SIZE=";
106 let (size_w, size_h) = source.find(needle).map_or((64, 64), |p| {
107 source[p + needle.len()..]
108 .find(char::is_whitespace)
109 .and_then(|end| source[p + needle.len()..][..end].split_once('x'))
110 .and_then(|(w, h)| Some((w.parse().ok()?, h.parse().ok()?)))
111 .unwrap_or_else(|| {
112 panic!("Cannot parse {needle} for {}", testcase.relative_path.display())
113 })
114 });
115
116 let mut output = BufWriter::new(std::fs::File::create(
117 Path::new(&std::env::var_os("OUT_DIR").unwrap()).join(format!("{module_name}.rs")),
118 )?);
119
120 generate_source(
121 source.as_str(),
122 &mut output,
123 testcase,
124 scale_factor.unwrap_or(1.),
125 &font_cache,
126 )
127 .unwrap();
128
129 write!(
130 output,
131 r"
132 #[test] fn t_{i}() -> Result<(), Box<dyn std::error::Error>> {{
133 use crate::testing;
134
135 let window = testing::init_swr();
136 window.set_size(slint::PhysicalSize::new({size_w}, {size_h}));
137 let screenshot = {reference_path};
138 let options = testing::TestCaseOptions {{ rotation_threshold: {rotation_threshold}f32, skip_clipping: {skip_clipping} }};
139
140 let instance = TestCase::new().unwrap();
141 instance.show().unwrap();
142
143 testing::assert_with_render(screenshot, window.clone(), &options);
144
145 testing::assert_with_render_by_line(screenshot, window.clone(), &options);
146
147 Ok(())
148 }}",
149 )?;
150 }
151
152 //Make sure to use a consistent style
153 println!("cargo:rustc-env=SLINT_STYLE=fluent");
154 println!("cargo:rustc-env=SLINT_ENABLE_EXPERIMENTAL_FEATURES=1");
155
156 Ok(())
157}
158
159fn generate_source(
160 source: &str,
161 output: &mut impl Write,
162 testcase: test_driver_lib::TestCase,
163 scale_factor: f32,
164 font_cache: &i_slint_compiler::FontCache,
165) -> Result<(), std::io::Error> {
166 use i_slint_compiler::{diagnostics::BuildDiagnostics, *};
167
168 let include_paths = test_driver_lib::extract_include_paths(source)
169 .map(std::path::PathBuf::from)
170 .collect::<Vec<_>>();
171
172 let mut diag = BuildDiagnostics::default();
173 let syntax_node = parser::parse(source.to_owned(), Some(&testcase.absolute_path), &mut diag);
174 let mut compiler_config = CompilerConfiguration::new(generator::OutputFormat::Rust);
175 compiler_config.include_paths = include_paths;
176 compiler_config.embed_resources = EmbedResourcesKind::EmbedTextures;
177 compiler_config.enable_experimental = true;
178 compiler_config.style = Some("fluent".to_string());
179 compiler_config.const_scale_factor = scale_factor.into();
180 compiler_config.font_cache = font_cache.clone();
181 let (root_component, diag, loader) =
182 spin_on::spin_on(compile_syntax_node(syntax_node, diag, compiler_config));
183
184 if diag.has_errors() {
185 diag.print_warnings_and_exit_on_error();
186 return Err(std::io::Error::new(
187 std::io::ErrorKind::Other,
188 format!("build error in {:?}", testcase.absolute_path),
189 ));
190 } else {
191 diag.print();
192 }
193
194 generator::generate(
195 generator::OutputFormat::Rust,
196 output,
197 &root_component,
198 &loader.compiler_config,
199 )?;
200 Ok(())
201}
202