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