Skip to main content

zoi_cli/pkg/package/
test.rs

1use anyhow::{Result, anyhow};
2use colored::Colorize;
3use mlua::{Lua, LuaSerdeExt};
4
5use crate::{cmd, pkg};
6
7/// Runs tests for a Zoi package defined in a `.pkg.lua` file.
8///
9/// # Errors
10///
11/// Returns an error if the package file cannot be parsed, if the build
12/// environment cannot be set up, or if the package tests fail.
13pub fn run(args: &cmd::package::build::BuildCommand) -> Result<()> {
14    println!("Testing package from: {}", args.package_file.display());
15
16    let platform = if let Some(p) = args.platform.first() {
17        p.clone()
18    } else {
19        crate::pkg::utils::get_platform()?
20    };
21
22    let pkg_for_meta = pkg::lua::parser::parse_lua_package_for_platform(
23        args.package_file.to_str().ok_or_else(|| {
24            anyhow!(
25                "Path contains invalid UTF-8 characters: {}",
26                args.package_file.display()
27            )
28        })?,
29        &platform,
30        args.version_override.as_deref(),
31        None,
32        false
33    )?;
34
35    let version = if let Some(v) = &args.version_override {
36        v.clone()
37    } else {
38        pkg::resolve::get_default_version(&pkg_for_meta, None)?
39    };
40
41    let Some(resolved_build_type) =
42        crate::pkg::package::build::resolve_build_type(
43            args.r#type.as_deref(),
44            &pkg_for_meta.types,
45            &pkg_for_meta.name
46        )?
47    else {
48        println!(
49            "{} Skipping tests for package '{}': no build types supported.",
50            "::".bold().yellow(),
51            pkg_for_meta.name
52        );
53        return Ok(());
54    };
55
56    let build_dir = tempfile::Builder::new()
57        .prefix(&format!("zoi-test-{}-{}", pkg_for_meta.name, platform))
58        .tempdir()?;
59    println!("Using build directory: {}", build_dir.path().display());
60    let staging_dir = build_dir.path().join("staging");
61    std::fs::create_dir_all(&staging_dir)?;
62
63    let subs_to_test = if let Some(subs) = &args.sub {
64        subs.clone()
65    } else if let Some(subs) = &pkg_for_meta.sub_packages {
66        subs.clone()
67    } else {
68        vec![String::new()]
69    };
70
71    for sub_package in subs_to_test {
72        let sub_pkg_name = if sub_package.is_empty() {
73            None
74        } else {
75            Some(sub_package.as_str())
76        };
77
78        if !sub_package.is_empty() {
79            println!(
80                "{} Testing sub-package: {}",
81                "::".bold().blue(),
82                sub_package.cyan()
83            );
84        }
85
86        let lua = Lua::new();
87        pkg::lua::functions::setup_lua_environment(
88            &lua,
89            &platform,
90            Some(&version),
91            args.package_file.to_str(),
92            None,
93            Some(build_dir.path().to_str().unwrap_or("")),
94            Some(staging_dir.to_str().unwrap_or("")),
95            sub_pkg_name,
96            Some(pkg_for_meta.scope),
97            Some(resolved_build_type.as_str()),
98            false
99        )
100        .map_err(|e| anyhow!(e.to_string()))?;
101
102        let pkg_table = lua
103            .to_value(&pkg_for_meta)
104            .map_err(|e| anyhow!(e.to_string()))?;
105        lua.globals()
106            .set("PKG", pkg_table)
107            .map_err(|e| anyhow!(e.to_string()))?;
108
109        let lua_code = std::fs::read_to_string(&args.package_file)?;
110        lua.load(&lua_code)
111            .exec()
112            .map_err(|e| anyhow!(e.to_string()))?;
113
114        let lua_args =
115            lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
116        if !sub_package.is_empty() {
117            lua_args
118                .set("sub", sub_package.clone())
119                .map_err(|e| anyhow!(e.to_string()))?;
120        }
121
122        if let Ok(prepare_fn) = lua.globals().get::<mlua::Function>("prepare") {
123            println!("Running prepare()...");
124            prepare_fn
125                .call::<()>(lua_args.clone())
126                .map_err(|e| anyhow!(e.to_string()))?;
127        }
128
129        if let Ok(package_fn) = lua.globals().get::<mlua::Function>("package") {
130            println!("Running package()...");
131            package_fn
132                .call::<()>(lua_args.clone())
133                .map_err(|e| anyhow!(e.to_string()))?;
134        }
135
136        if let Ok(test_fn) = lua.globals().get::<mlua::Function>("test") {
137            println!("Running test()...");
138            let success: bool = match test_fn
139                .call::<mlua::Value>(lua_args.clone())
140            {
141                Ok(mlua::Value::Boolean(b)) => b,
142                Ok(mlua::Value::Nil) => {
143                    return Err(anyhow!(
144                        "The 'test' function in '{}' returned nil. It must \
145                         explicitly return a boolean (true or false).",
146                        args.package_file.display()
147                    ));
148                }
149                Ok(v) => {
150                    return Err(anyhow!(
151                        "The 'test' function in '{}' returned a non-boolean \
152                         value of type {:?}. It must return true or false.",
153                        args.package_file.display(),
154                        v.type_name()
155                    ));
156                }
157                Err(e) => return Err(anyhow!(e.to_string()))
158            };
159            if !success {
160                return Err(anyhow!(
161                    "Package tests failed for sub-package '{sub_package}'."
162                ));
163            }
164        } else if !sub_package.is_empty() {
165            println!(
166                "No test() function found for sub-package '{sub_package}', \
167                 skipping."
168            );
169        }
170    }
171
172    println!("{}", "All tests passed successfully.".green());
173    Ok(())
174}