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 #[cfg_attr(
326 feature = "schema",
327 schemars(extend("x-lux-toml-example" = r#"
328[test]
329type = "command"
330command = "make"
331flags = ["test"]
332"#))
333 )]
334 pub(crate) command: Option<String>,
335 #[serde(default, rename = "script", alias = "lua_script")]
337 #[cfg_attr(
338 feature = "schema",
339 schemars(extend("x-lux-toml-example" = r#"
340[test]
341type = "script"
342script = "tests.lua"
343flags = []
344"#))
345 )]
346 pub(crate) lua_script: Option<PathBuf>,
347}
348
349impl PartialOverride for TestSpecInternal {
350 type Err = Infallible;
351
352 fn apply_overrides(&self, override_spec: &Self) -> Result<Self, Self::Err> {
353 Ok(TestSpecInternal {
354 test_type: override_opt(&override_spec.test_type, &self.test_type),
355 flags: match (override_spec.flags.clone(), self.flags.clone()) {
356 (Some(override_vec), Some(base_vec)) => {
357 let merged: Vec<String> =
358 base_vec.into_iter().chain(override_vec).unique().collect();
359 Some(merged)
360 }
361 (None, base_vec @ Some(_)) => base_vec,
362 (override_vec @ Some(_), None) => override_vec,
363 _ => None,
364 },
365 command: match override_spec.lua_script.clone() {
366 Some(_) => None,
367 None => override_opt(&override_spec.command, &self.command),
368 },
369 lua_script: match override_spec.command.clone() {
370 Some(_) => None,
371 None => override_opt(&override_spec.lua_script, &self.lua_script),
372 },
373 })
374 }
375}
376
377impl PlatformOverridable for TestSpecInternal {
378 type Err = Infallible;
379
380 fn on_nil<T>() -> Result<PerPlatform<T>, <Self as PlatformOverridable>::Err>
381 where
382 T: PlatformOverridable,
383 T: Default,
384 {
385 Ok(PerPlatform::default())
386 }
387}
388
389fn override_opt<T: Clone>(override_opt: &Option<T>, base: &Option<T>) -> Option<T> {
390 match override_opt.clone() {
391 override_val @ Some(_) => override_val,
392 None => base.clone(),
393 }
394}
395
396#[cfg(test)]
397mod tests {
398
399 use ottavino::{Closure, Executor, Fuel, Lua};
400 use ottavino_util::serde::from_value;
401
402 use crate::lua_rockspec::PlatformIdentifier;
403
404 use super::*;
405
406 fn exec_lua<T: serde::de::DeserializeOwned>(
407 code: &str,
408 key: &'static str,
409 ) -> Result<T, ottavino::ExternError> {
410 Lua::core().try_enter(|ctx| {
411 let closure = Closure::load(ctx, None, code.as_bytes())?;
412 let executor = Executor::start(ctx, closure.into(), ());
413 executor.step(ctx, &mut Fuel::with(i32::MAX))?;
414 from_value(ctx.globals().get_value(ctx, key)).map_err(ottavino::Error::from)
415 })
416 }
417
418 #[tokio::test]
419 pub async fn test_spec_from_lua() {
420 let lua_content = "
421 test = {\n
422 }\n
423 ";
424 let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
425 assert!(matches!(test_spec.default, TestSpec::AutoDetect));
426 let lua_content = "
427 test = {\n
428 type = 'busted',\n
429 }\n
430 ";
431 let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
432 assert_eq!(
433 test_spec.default,
434 TestSpec::Busted(BustedTestSpec::default())
435 );
436 let lua_content = "
437 test = {\n
438 type = 'busted',\n
439 flags = { 'foo', 'bar' },\n
440 }\n
441 ";
442 let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
443 assert_eq!(
444 test_spec.default,
445 TestSpec::Busted(BustedTestSpec {
446 flags: vec!["foo".into(), "bar".into()],
447 })
448 );
449 let lua_content = "
450 test = {\n
451 type = 'command',\n
452 }\n
453 ";
454 let result: Result<PerPlatform<TestSpec>, _> = exec_lua(lua_content, "test");
455 let _err = result.unwrap_err();
456 let lua_content = "
457 test = {\n
458 type = 'command',\n
459 command = 'foo',\n
460 script = 'bar',\n
461 }\n
462 ";
463 let result: Result<PerPlatform<TestSpec>, _> = exec_lua(lua_content, "test");
464 let _err = result.unwrap_err();
465 let lua_content = "
466 test = {\n
467 type = 'command',\n
468 command = 'baz',\n
469 flags = { 'foo', 'bar' },\n
470 }\n
471 ";
472 let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
473 assert_eq!(
474 test_spec.default,
475 TestSpec::Command(CommandTestSpec {
476 command: "baz".into(),
477 flags: vec!["foo".into(), "bar".into()],
478 })
479 );
480 let lua_content = "
481 test = {\n
482 type = 'command',\n
483 script = 'test.lua',\n
484 flags = { 'foo', 'bar' },\n
485 }\n
486 ";
487 let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
488 assert_eq!(
489 test_spec.default,
490 TestSpec::Script(LuaScriptTestSpec {
491 script: PathBuf::from("test.lua"),
492 flags: vec!["foo".into(), "bar".into()],
493 })
494 );
495 let lua_content = "
496 test = {\n
497 type = 'command',\n
498 command = 'baz',\n
499 flags = { 'foo', 'bar' },\n
500 platforms = {\n
501 unix = { flags = { 'baz' }, },\n
502 macosx = {\n
503 script = 'bat.lua',\n
504 flags = { 'bat' },\n
505 },\n
506 linux = { type = 'busted' },\n
507 },\n
508 }\n
509 ";
510 let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
511 assert_eq!(
512 test_spec.default,
513 TestSpec::Command(CommandTestSpec {
514 command: "baz".into(),
515 flags: vec!["foo".into(), "bar".into()],
516 })
517 );
518 let unix = test_spec
519 .per_platform
520 .get(&PlatformIdentifier::Unix)
521 .unwrap();
522 assert_eq!(
523 *unix,
524 TestSpec::Command(CommandTestSpec {
525 command: "baz".into(),
526 flags: vec!["foo".into(), "bar".into(), "baz".into()],
527 })
528 );
529 let macosx = test_spec
530 .per_platform
531 .get(&PlatformIdentifier::MacOSX)
532 .unwrap();
533 assert_eq!(
534 *macosx,
535 TestSpec::Script(LuaScriptTestSpec {
536 script: "bat.lua".into(),
537 flags: vec!["foo".into(), "bar".into(), "bat".into(), "baz".into()],
538 })
539 );
540 let linux = test_spec
541 .per_platform
542 .get(&PlatformIdentifier::Linux)
543 .unwrap();
544 assert_eq!(
545 *linux,
546 TestSpec::Busted(BustedTestSpec {
547 flags: vec!["foo".into(), "bar".into(), "baz".into()],
548 })
549 );
550 let lua_content = "
551 test = {\n
552 type = 'busted-nlua',\n
553 }";
554 let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
555 assert_eq!(
556 test_spec.default,
557 TestSpec::BustedNlua(BustedTestSpec { flags: Vec::new() })
558 );
559 }
560}