1use itertools::Itertools;
2
3use miette::Diagnostic;
4use path_slash::PathExt;
5use serde::{Deserialize, Deserializer};
6use serde_enum_str::Serialize_enum_str;
7use std::{convert::Infallible, path::PathBuf};
8use thiserror::Error;
9
10use crate::lua_version::LuaVersion;
11
12use crate::{
13 config::{Config, ConfigBuilder, ConfigError},
14 lua_rockspec::per_platform_from_intermediate,
15 package::PackageReq,
16 project::{project_toml::LocalProjectTomlValidationError, Project},
17 rockspec::Rockspec,
18};
19
20use super::{PartialOverride, PerPlatform, PlatformOverridable};
21
22#[cfg(target_family = "unix")]
23const NLUA_EXE: &str = "nlua";
24#[cfg(target_family = "windows")]
25const NLUA_EXE: &str = "nlua.bat";
26
27#[derive(Error, Debug, Diagnostic)]
28#[non_exhaustive]
29pub enum TestSpecDecodeError {
30 #[error("the 'command' test type must specify either a 'command' or 'script' field")]
31 NoCommandOrScript,
32 #[error("the 'command' test type cannot have both 'command' and 'script' fields")]
33 CommandAndScript,
34}
35
36#[derive(Error, Debug, Diagnostic)]
37#[non_exhaustive]
38pub enum TestSpecError {
39 #[error("could not auto-detect the test spec. Please add one to your lux.toml")]
40 NoTestSpecDetected,
41 #[error("project validation failed")]
42 #[diagnostic(forward(0))]
43 LocalProjectTomlValidation(#[from] LocalProjectTomlValidationError),
44}
45
46#[derive(Clone, Debug, PartialEq, Default)]
47pub enum TestSpec {
48 #[default]
49 AutoDetect,
50 Busted(BustedTestSpec),
51 BustedNlua(BustedTestSpec),
52 Tiniest,
53 Command(CommandTestSpec),
54 Script(LuaScriptTestSpec),
55}
56
57#[derive(Clone, Debug, PartialEq)]
58pub(crate) enum ValidatedTestSpec {
59 Busted {
60 spec: BustedTestSpec,
61 dependencies: Vec<PackageReq>,
62 },
63 BustedNlua {
64 spec: BustedTestSpec,
65 dependencies: Vec<PackageReq>,
66 },
67 Tiniest,
68 Command(CommandTestSpec),
69 LuaScript(LuaScriptTestSpec),
70}
71
72impl TestSpec {
73 pub(crate) fn test_dependencies(&self, project: &Project) -> Vec<PackageReq> {
74 self.to_validated(project)
75 .ok()
76 .iter()
77 .flat_map(|spec| spec.test_dependencies())
78 .collect_vec()
79 }
80
81 pub(crate) fn to_validated(
82 &self,
83 project: &Project,
84 ) -> Result<ValidatedTestSpec, TestSpecError> {
85 let project_root = project.root();
86 let toml = project.toml().into_local()?;
87 let test_dependencies = toml.test_dependencies().current_platform();
88 let busted_dependency = test_dependencies
89 .iter()
90 .find(|dep| dep.name().to_string() == "busted");
91 let busted = busted_dependency
92 .map(|dep| dep.package_req.clone())
93 .unwrap_or_else(|| unsafe { PackageReq::new_unchecked("busted".into(), None) });
94 let nlua_dependency = test_dependencies
95 .iter()
96 .find(|dep| dep.name().to_string() == "nlua");
97 let nlua = nlua_dependency
98 .map(|dep| dep.package_req.clone())
99 .unwrap_or_else(|| unsafe { PackageReq::new_unchecked("nlua".into(), None) });
100 let is_busted = project_root.join(".busted").is_file() || busted_dependency.is_some();
101 match self {
102 Self::AutoDetect if is_busted => {
103 if nlua_dependency.is_some() {
104 Ok(ValidatedTestSpec::BustedNlua {
105 spec: BustedTestSpec::default(),
106 dependencies: vec![busted, nlua],
107 })
108 } else {
109 Ok(ValidatedTestSpec::Busted {
110 spec: BustedTestSpec::default(),
111 dependencies: vec![busted],
112 })
113 }
114 }
115 Self::Busted(spec) => Ok(ValidatedTestSpec::Busted {
116 spec: spec.clone(),
117 dependencies: vec![busted],
118 }),
119 Self::BustedNlua(spec) => Ok(ValidatedTestSpec::BustedNlua {
120 spec: spec.clone(),
121 dependencies: vec![busted, nlua],
122 }),
123 Self::Tiniest => Ok(ValidatedTestSpec::Tiniest),
124 Self::Command(spec) => Ok(ValidatedTestSpec::Command(spec.clone())),
125 Self::Script(spec) => Ok(ValidatedTestSpec::LuaScript(spec.clone())),
126 Self::AutoDetect => Err(TestSpecError::NoTestSpecDetected),
127 }
128 }
129}
130
131impl ValidatedTestSpec {
132 pub fn args(&self) -> Vec<String> {
133 match self {
134 Self::Busted {
135 spec,
136 dependencies: _,
137 } => spec.flags.clone(),
138 Self::BustedNlua {
139 spec,
140 dependencies: _,
141 } => {
142 let mut flags = spec.flags.clone();
143 flags.push("--ignore-lua".into());
147 flags
148 }
149 Self::Tiniest => Vec::new(),
150 Self::Command(spec) => spec.flags.clone(),
151 Self::LuaScript(spec) => std::iter::once(spec.script.to_slash_lossy().to_string())
152 .chain(spec.flags.clone())
153 .collect_vec(),
154 }
155 }
156
157 pub(crate) fn test_config(&self, config: &Config) -> Result<Config, ConfigError> {
158 match self {
159 Self::BustedNlua { .. } => {
160 let config_builder: ConfigBuilder = config.clone().into();
161
162 #[cfg(not(any(target_os = "macos", target_env = "msvc")))]
166 let lua_version = LuaVersion::Lua51;
167
168 #[cfg(any(target_os = "macos", target_env = "msvc"))]
169 let lua_version = LuaVersion::LuaJIT;
170
171 Ok(config_builder
172 .lua_version(Some(lua_version))
173 .variables(Some(
174 vec![("LUA".to_string(), NLUA_EXE.to_string())]
175 .into_iter()
176 .collect(),
177 ))
178 .build()?)
179 }
180 _ => Ok(config.clone()),
181 }
182 }
183
184 fn test_dependencies(&self) -> Vec<PackageReq> {
185 match self {
186 Self::Busted {
187 spec: _,
188 dependencies,
189 } => dependencies.clone(),
190 Self::BustedNlua {
191 spec: _,
192 dependencies,
193 } => dependencies.clone(),
194 Self::Tiniest => Vec::new(),
195 Self::Command(_) => Vec::new(),
196 Self::LuaScript(_) => Vec::new(),
197 }
198 }
199}
200
201impl TryFrom<TestSpecInternal> for TestSpec {
202 type Error = TestSpecDecodeError;
203
204 fn try_from(internal: TestSpecInternal) -> Result<Self, Self::Error> {
205 let test_spec = match internal.test_type {
206 Some(TestType::Busted) => Ok(Self::Busted(BustedTestSpec {
207 flags: internal.flags.unwrap_or_default(),
208 })),
209 Some(TestType::BustedNlua) => Ok(Self::BustedNlua(BustedTestSpec {
210 flags: internal.flags.unwrap_or_default(),
211 })),
212 Some(TestType::Tiniest) => Ok(Self::Tiniest),
213 Some(TestType::Command) => match (internal.command, internal.lua_script) {
214 (None, None) => Err(TestSpecDecodeError::NoCommandOrScript),
215 (None, Some(script)) => Ok(Self::Script(LuaScriptTestSpec {
216 script,
217 flags: internal.flags.unwrap_or_default(),
218 })),
219 (Some(command), None) => Ok(Self::Command(CommandTestSpec {
220 command,
221 flags: internal.flags.unwrap_or_default(),
222 })),
223 (Some(_), Some(_)) => Err(TestSpecDecodeError::CommandAndScript),
224 },
225 None => Ok(Self::default()),
226 }?;
227 Ok(test_spec)
228 }
229}
230
231impl<'de> Deserialize<'de> for PerPlatform<TestSpec> {
232 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
233 where
234 D: Deserializer<'de>,
235 {
236 per_platform_from_intermediate::<_, TestSpecInternal, _>(deserializer)
237 }
238}
239
240impl<'de> Deserialize<'de> for TestSpec {
241 fn deserialize<D>(deserializer: D) -> Result<TestSpec, D::Error>
242 where
243 D: Deserializer<'de>,
244 {
245 let internal = TestSpecInternal::deserialize(deserializer)?;
246 let test_spec = TestSpec::try_from(internal).map_err(serde::de::Error::custom)?;
247 Ok(test_spec)
248 }
249}
250
251#[derive(Clone, Debug, PartialEq, Default)]
253pub struct BustedTestSpec {
254 pub(crate) flags: Vec<String>,
256}
257
258impl BustedTestSpec {
259 pub fn flags(&self) -> &Vec<String> {
260 &self.flags
261 }
262}
263
264#[derive(Clone, Debug, PartialEq)]
266pub struct CommandTestSpec {
267 pub(crate) command: String,
269 pub(crate) flags: Vec<String>,
271}
272
273impl CommandTestSpec {
274 pub fn flags(&self) -> &Vec<String> {
275 &self.flags
276 }
277
278 pub fn command(&self) -> &str {
279 &self.command
280 }
281}
282
283#[derive(Clone, Debug, PartialEq)]
285pub struct LuaScriptTestSpec {
286 pub(crate) script: PathBuf,
288 pub(crate) flags: Vec<String>,
290}
291
292impl LuaScriptTestSpec {
293 pub fn flags(&self) -> &Vec<String> {
294 &self.flags
295 }
296
297 pub fn script(&self) -> &PathBuf {
298 &self.script
299 }
300}
301
302#[derive(Debug, Deserialize, Serialize_enum_str, PartialEq, Clone)]
303#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
304#[serde(rename_all = "kebab-case")]
305pub(crate) enum TestType {
306 Busted,
307 BustedNlua,
308 Tiniest,
309 Command,
310}
311
312#[derive(Debug, PartialEq, Deserialize, Default, Clone)]
313#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
314pub(crate) struct TestSpecInternal {
315 #[serde(default, rename = "type")]
319 pub(crate) test_type: Option<TestType>,
320 #[serde(default)]
322 pub(crate) flags: Option<Vec<String>>,
323 #[serde(default)]
325 pub(crate) command: Option<String>,
326 #[serde(default, rename = "script", alias = "lua_script")]
328 pub(crate) lua_script: Option<PathBuf>,
329}
330
331impl PartialOverride for TestSpecInternal {
332 type Err = Infallible;
333
334 fn apply_overrides(&self, override_spec: &Self) -> Result<Self, Self::Err> {
335 Ok(TestSpecInternal {
336 test_type: override_opt(&override_spec.test_type, &self.test_type),
337 flags: match (override_spec.flags.clone(), self.flags.clone()) {
338 (Some(override_vec), Some(base_vec)) => {
339 let merged: Vec<String> =
340 base_vec.into_iter().chain(override_vec).unique().collect();
341 Some(merged)
342 }
343 (None, base_vec @ Some(_)) => base_vec,
344 (override_vec @ Some(_), None) => override_vec,
345 _ => None,
346 },
347 command: match override_spec.lua_script.clone() {
348 Some(_) => None,
349 None => override_opt(&override_spec.command, &self.command),
350 },
351 lua_script: match override_spec.command.clone() {
352 Some(_) => None,
353 None => override_opt(&override_spec.lua_script, &self.lua_script),
354 },
355 })
356 }
357}
358
359impl PlatformOverridable for TestSpecInternal {
360 type Err = Infallible;
361
362 fn on_nil<T>() -> Result<PerPlatform<T>, <Self as PlatformOverridable>::Err>
363 where
364 T: PlatformOverridable,
365 T: Default,
366 {
367 Ok(PerPlatform::default())
368 }
369}
370
371fn override_opt<T: Clone>(override_opt: &Option<T>, base: &Option<T>) -> Option<T> {
372 match override_opt.clone() {
373 override_val @ Some(_) => override_val,
374 None => base.clone(),
375 }
376}
377
378#[cfg(test)]
379mod tests {
380
381 use ottavino::{Closure, Executor, Fuel, Lua};
382 use ottavino_util::serde::from_value;
383
384 use crate::lua_rockspec::PlatformIdentifier;
385
386 use super::*;
387
388 fn exec_lua<T: serde::de::DeserializeOwned>(
389 code: &str,
390 key: &'static str,
391 ) -> Result<T, ottavino::ExternError> {
392 Lua::core().try_enter(|ctx| {
393 let closure = Closure::load(ctx, None, code.as_bytes())?;
394 let executor = Executor::start(ctx, closure.into(), ());
395 executor.step(ctx, &mut Fuel::with(i32::MAX))?;
396 from_value(ctx.globals().get_value(ctx, key)).map_err(ottavino::Error::from)
397 })
398 }
399
400 #[tokio::test]
401 pub async fn test_spec_from_lua() {
402 let lua_content = "
403 test = {\n
404 }\n
405 ";
406 let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
407 assert!(matches!(test_spec.default, TestSpec::AutoDetect));
408 let lua_content = "
409 test = {\n
410 type = 'busted',\n
411 }\n
412 ";
413 let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
414 assert_eq!(
415 test_spec.default,
416 TestSpec::Busted(BustedTestSpec::default())
417 );
418 let lua_content = "
419 test = {\n
420 type = 'busted',\n
421 flags = { 'foo', 'bar' },\n
422 }\n
423 ";
424 let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
425 assert_eq!(
426 test_spec.default,
427 TestSpec::Busted(BustedTestSpec {
428 flags: vec!["foo".into(), "bar".into()],
429 })
430 );
431 let lua_content = "
432 test = {\n
433 type = 'command',\n
434 }\n
435 ";
436 let result: Result<PerPlatform<TestSpec>, _> = exec_lua(lua_content, "test");
437 let _err = result.unwrap_err();
438 let lua_content = "
439 test = {\n
440 type = 'command',\n
441 command = 'foo',\n
442 script = 'bar',\n
443 }\n
444 ";
445 let result: Result<PerPlatform<TestSpec>, _> = exec_lua(lua_content, "test");
446 let _err = result.unwrap_err();
447 let lua_content = "
448 test = {\n
449 type = 'command',\n
450 command = 'baz',\n
451 flags = { 'foo', 'bar' },\n
452 }\n
453 ";
454 let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
455 assert_eq!(
456 test_spec.default,
457 TestSpec::Command(CommandTestSpec {
458 command: "baz".into(),
459 flags: vec!["foo".into(), "bar".into()],
460 })
461 );
462 let lua_content = "
463 test = {\n
464 type = 'command',\n
465 script = 'test.lua',\n
466 flags = { 'foo', 'bar' },\n
467 }\n
468 ";
469 let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
470 assert_eq!(
471 test_spec.default,
472 TestSpec::Script(LuaScriptTestSpec {
473 script: PathBuf::from("test.lua"),
474 flags: vec!["foo".into(), "bar".into()],
475 })
476 );
477 let lua_content = "
478 test = {\n
479 type = 'command',\n
480 command = 'baz',\n
481 flags = { 'foo', 'bar' },\n
482 platforms = {\n
483 unix = { flags = { 'baz' }, },\n
484 macosx = {\n
485 script = 'bat.lua',\n
486 flags = { 'bat' },\n
487 },\n
488 linux = { type = 'busted' },\n
489 },\n
490 }\n
491 ";
492 let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
493 assert_eq!(
494 test_spec.default,
495 TestSpec::Command(CommandTestSpec {
496 command: "baz".into(),
497 flags: vec!["foo".into(), "bar".into()],
498 })
499 );
500 let unix = test_spec
501 .per_platform
502 .get(&PlatformIdentifier::Unix)
503 .unwrap();
504 assert_eq!(
505 *unix,
506 TestSpec::Command(CommandTestSpec {
507 command: "baz".into(),
508 flags: vec!["foo".into(), "bar".into(), "baz".into()],
509 })
510 );
511 let macosx = test_spec
512 .per_platform
513 .get(&PlatformIdentifier::MacOSX)
514 .unwrap();
515 assert_eq!(
516 *macosx,
517 TestSpec::Script(LuaScriptTestSpec {
518 script: "bat.lua".into(),
519 flags: vec!["foo".into(), "bar".into(), "bat".into(), "baz".into()],
520 })
521 );
522 let linux = test_spec
523 .per_platform
524 .get(&PlatformIdentifier::Linux)
525 .unwrap();
526 assert_eq!(
527 *linux,
528 TestSpec::Busted(BustedTestSpec {
529 flags: vec!["foo".into(), "bar".into(), "baz".into()],
530 })
531 );
532 let lua_content = "
533 test = {\n
534 type = 'busted-nlua',\n
535 }";
536 let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
537 assert_eq!(
538 test_spec.default,
539 TestSpec::BustedNlua(BustedTestSpec { flags: Vec::new() })
540 );
541 }
542}