1use std::{io, ops::Deref, path::PathBuf, process::Command, sync::Arc};
2
3use crate::{
4 build::BuildBehaviour,
5 config::{Config, ConfigError},
6 lua_installation::{LuaBinary, LuaBinaryError},
7 lua_rockspec::{LuaVersionError, TestSpecError, ValidatedTestSpec},
8 package::{PackageName, PackageVersionReqError},
9 path::{Paths, PathsError},
10 progress::{MultiProgress, Progress},
11 project::{
12 project_toml::LocalProjectTomlValidationError, Project, ProjectError, ProjectTreeError,
13 },
14 rockspec::Rockspec,
15 tree::{self, TreeError},
16};
17use bon::Builder;
18use itertools::Itertools;
19use path_slash::PathBufExt;
20use thiserror::Error;
21
22use super::{
23 BuildProject, BuildProjectError, Install, InstallError, PackageInstallSpec, Sync, SyncError,
24};
25
26#[cfg(target_family = "unix")]
27const BUSTED_EXE: &str = "busted";
28#[cfg(target_family = "windows")]
29const BUSTED_EXE: &str = "busted.bat";
30
31#[derive(Builder)]
32#[builder(start_fn = new, finish_fn(name = _run, vis = ""))]
33pub struct Test<'a> {
34 #[builder(start_fn)]
35 project: Project,
36 #[builder(start_fn)]
37 config: &'a Config,
38
39 #[builder(field)]
40 args: Vec<String>,
41
42 no_lock: Option<bool>,
43
44 #[builder(default)]
45 env: TestEnv,
46 progress: Option<Arc<Progress<MultiProgress>>>,
47}
48
49impl<State: test_builder::State> TestBuilder<'_, State> {
50 pub fn arg(mut self, arg: impl Into<String>) -> Self {
51 self.args.push(arg.into());
52 self
53 }
54
55 pub fn args(mut self, args: impl IntoIterator<Item: Into<String>>) -> Self {
56 self.args.extend(args.into_iter().map_into());
57 self
58 }
59
60 pub async fn run(self) -> Result<(), RunTestsError>
61 where
62 State: test_builder::IsComplete,
63 {
64 run_tests(self._run()).await
65 }
66}
67
68#[derive(Default)]
69pub enum TestEnv {
70 #[default]
72 Pure,
73 Impure,
76}
77
78#[derive(Error, Debug)]
79pub enum RunTestsError {
80 #[error(transparent)]
81 Config(#[from] ConfigError),
82 #[error(transparent)]
83 InstallTestDependencies(#[from] InstallTestDependenciesError),
84 #[error("error building project:\n{0}")]
85 BuildProject(#[from] BuildProjectError),
86 #[error("tests failed!")]
87 TestFailure,
88 #[error("failed to execute `{0}`: {1}")]
89 RunCommandFailure(String, io::Error),
90 #[error(transparent)]
91 Io(#[from] io::Error),
92 #[error(transparent)]
93 Project(#[from] ProjectError),
94 #[error(transparent)]
95 Paths(#[from] PathsError),
96 #[error(transparent)]
97 Tree(#[from] ProjectTreeError),
98 #[error(transparent)]
99 ProjectTomlValidation(#[from] LocalProjectTomlValidationError),
100 #[error("failed to sync dependencies: {0}")]
101 Sync(#[from] SyncError),
102 #[error(transparent)]
103 TestSpec(#[from] TestSpecError),
104 #[error(transparent)]
105 LuaVersion(#[from] LuaVersionError),
106 #[error(transparent)]
107 LuaBinary(#[from] LuaBinaryError),
108}
109
110async fn run_tests(test: Test<'_>) -> Result<(), RunTestsError> {
111 let rocks = test.project.toml().into_local()?;
112
113 let test_spec = rocks
114 .test()
115 .current_platform()
116 .to_validated(&test.project)?;
117
118 let config = test_spec.test_config(test.config)?;
119
120 let no_lock = test.no_lock.unwrap_or(false);
121
122 let progress = test
123 .progress
124 .unwrap_or_else(|| MultiProgress::new_arc(&config));
125
126 if no_lock {
127 ensure_test_dependencies(&test.project, &rocks, &config, progress).await?;
128 } else {
129 Sync::new(&test.project, &config)
130 .progress(progress.clone())
131 .sync_test_dependencies()
132 .await?;
133 }
134
135 BuildProject::new(&test.project, &config)
136 .no_lock(no_lock)
137 .only_deps(false)
138 .build()
139 .await?;
140
141 let project_tree = test.project.tree(&config)?;
142 let test_tree = test.project.test_tree(&config)?;
143 let mut paths = Paths::new(&project_tree)?;
144 let test_tree_paths = Paths::new(&test_tree)?;
145 paths.prepend(&test_tree_paths);
146
147 let test_executable = match &test_spec {
148 ValidatedTestSpec::Busted(_) => BUSTED_EXE.to_string(),
149 ValidatedTestSpec::BustedNlua(_) => BUSTED_EXE.to_string(),
150 ValidatedTestSpec::Command(spec) => spec.command.to_string(),
151 ValidatedTestSpec::LuaScript(_) => {
152 let lua_version = test.project.lua_version(&config)?;
153 let lua_binary = LuaBinary::new(lua_version, &config);
154 let lua_bin_path: PathBuf = lua_binary.try_into()?;
155 lua_bin_path.to_slash_lossy().to_string()
156 }
157 };
158 let mut command = Command::new(&test_executable);
159 let mut command = command
160 .current_dir(test.project.root().deref())
161 .args(test_spec.args())
162 .args(test.args)
163 .env("PATH", paths.path_prepended().joined())
164 .env("LUA_PATH", paths.package_path().joined())
165 .env("LUA_CPATH", paths.package_cpath().joined());
166 if let TestEnv::Pure = test.env {
167 let home = test_tree.root().join("home");
170 let xdg = home.join("xdg");
171 let _ = tokio::fs::remove_dir_all(&home).await;
172 let xdg_config_home = xdg.join("config");
173 std::fs::create_dir_all(&xdg_config_home)?;
174 let xdg_state_home = xdg.join("local").join("state");
175 std::fs::create_dir_all(&xdg_state_home)?;
176 let xdg_data_home = xdg.join("local").join("share");
177 std::fs::create_dir_all(&xdg_data_home)?;
178 command = command
179 .env("HOME", home)
180 .env("XDG_CONFIG_HOME", xdg_config_home)
181 .env("XDG_STATE_HOME", xdg_state_home)
182 .env("XDG_DATA_HOME", xdg_data_home);
183 }
184 let status = match command.status() {
185 Ok(status) => Ok(status),
186 Err(err) => Err(RunTestsError::RunCommandFailure(test_executable, err)),
187 }?;
188 if status.success() {
189 Ok(())
190 } else {
191 Err(RunTestsError::TestFailure)
192 }
193}
194
195#[derive(Error, Debug)]
196#[error("error installing test dependencies: {0}")]
197pub enum InstallTestDependenciesError {
198 ProjectTree(#[from] ProjectTreeError),
199 Tree(#[from] TreeError),
200 Install(#[from] InstallError),
201 PackageVersionReq(#[from] PackageVersionReqError),
202}
203
204async fn ensure_test_dependencies(
207 project: &Project,
208 rockspec: &impl Rockspec,
209 config: &Config,
210 progress: Arc<Progress<MultiProgress>>,
211) -> Result<(), InstallTestDependenciesError> {
212 let test_tree = project.test_tree(config)?;
213 let rockspec_dependencies = rockspec.test_dependencies().current_platform();
214 let test_dependencies = rockspec
215 .test()
216 .current_platform()
217 .test_dependencies(project)
218 .iter()
219 .filter(|test_dep| {
220 !rockspec_dependencies
221 .iter()
222 .any(|dep| dep.name() == test_dep.name())
223 })
224 .filter_map(|dep| {
225 let build_behaviour = if test_tree
226 .match_rocks(dep)
227 .is_ok_and(|matches| matches.is_found())
228 {
229 Some(BuildBehaviour::NoForce)
230 } else {
231 Some(BuildBehaviour::Force)
232 };
233 build_behaviour.map(|build_behaviour| {
234 PackageInstallSpec::new(dep.clone(), tree::EntryType::Entrypoint)
235 .build_behaviour(build_behaviour)
236 .build()
237 })
238 })
239 .chain(
240 rockspec_dependencies
241 .iter()
242 .filter(|req| !req.name().eq(&PackageName::new("lua".into())))
243 .filter_map(|dep| {
244 let build_behaviour = if test_tree
245 .match_rocks(dep.package_req())
246 .is_ok_and(|matches| matches.is_found())
247 {
248 Some(BuildBehaviour::NoForce)
249 } else {
250 Some(BuildBehaviour::Force)
251 };
252 build_behaviour.map(|build_behaviour| {
253 PackageInstallSpec::new(
254 dep.package_req().clone(),
255 tree::EntryType::Entrypoint,
256 )
257 .build_behaviour(build_behaviour)
258 .pin(*dep.pin())
259 .opt(*dep.opt())
260 .maybe_source(dep.source.clone())
261 .build()
262 })
263 }),
264 )
265 .collect();
266
267 Install::new(config)
268 .packages(test_dependencies)
269 .tree(test_tree)
270 .progress(progress.clone())
271 .install()
272 .await?;
273
274 Ok(())
275}
276
277#[cfg(test)]
278mod tests {
279 use std::path::Path;
280
281 use crate::{
282 config::{ConfigBuilder, LuaVersion},
283 lua_installation::detect_installed_lua_version,
284 };
285
286 use super::*;
287 use assert_fs::{prelude::PathCopy, TempDir};
288
289 #[tokio::test]
290 async fn test_command_spec() {
291 let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
292 .join("resources/test/sample-projects/command-test/");
293 run_test(&project_root).await
294 }
295
296 #[tokio::test]
297 async fn test_lua_script_spec() {
298 let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
299 .join("resources/test/sample-projects/lua-script-test/");
300 run_test(&project_root).await
301 }
302
303 async fn run_test(project_root: &Path) {
304 let temp_dir = TempDir::new().unwrap();
305 temp_dir.copy_from(project_root, &["**"]).unwrap();
306 let project_root = temp_dir.path();
307 let project: Project = Project::from(project_root).unwrap().unwrap();
308 let tree_root = project.root().to_path_buf().join(".lux");
309 let _ = tokio::fs::remove_dir_all(&tree_root).await;
310
311 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
312
313 let config = ConfigBuilder::new()
314 .unwrap()
315 .user_tree(Some(tree_root))
316 .lua_version(lua_version)
317 .build()
318 .unwrap();
319
320 Test::new(project, &config).run().await.unwrap();
321 }
322}