use itertools::Itertools;
use path_slash::PathExt;
use serde_enum_str::Serialize_enum_str;
use std::{convert::Infallible, path::PathBuf};
use thiserror::Error;
use serde::{Deserialize, Deserializer};
use crate::lua_version::LuaVersion;
use crate::{
config::{Config, ConfigBuilder, ConfigError},
lua_rockspec::per_platform_from_intermediate,
package::PackageReq,
project::{project_toml::LocalProjectTomlValidationError, Project},
rockspec::Rockspec,
};
use super::{PartialOverride, PerPlatform, PlatformOverridable};
#[cfg(target_family = "unix")]
const NLUA_EXE: &str = "nlua";
#[cfg(target_family = "windows")]
const NLUA_EXE: &str = "nlua.bat";
#[derive(Error, Debug)]
pub enum TestSpecDecodeError {
#[error("the 'command' test type must specify either a 'command' or 'script' field")]
NoCommandOrScript,
#[error("the 'command' test type cannot have both 'command' and 'script' fields")]
CommandAndScript,
}
#[derive(Error, Debug)]
pub enum TestSpecError {
#[error("could not auto-detect the test spec. Please add one to your lux.toml")]
NoTestSpecDetected,
#[error("project validation failed:\n{0}")]
LocalProjectTomlValidation(#[from] LocalProjectTomlValidationError),
}
#[derive(Clone, Debug, PartialEq, Default)]
pub enum TestSpec {
#[default]
AutoDetect,
Busted(BustedTestSpec),
BustedNlua(BustedTestSpec),
Command(CommandTestSpec),
Script(LuaScriptTestSpec),
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum ValidatedTestSpec {
Busted(BustedTestSpec),
BustedNlua(BustedTestSpec),
Command(CommandTestSpec),
LuaScript(LuaScriptTestSpec),
}
impl TestSpec {
pub(crate) fn test_dependencies(&self, project: &Project) -> Vec<PackageReq> {
self.to_validated(project)
.ok()
.iter()
.flat_map(|spec| spec.test_dependencies())
.collect_vec()
}
pub(crate) fn to_validated(
&self,
project: &Project,
) -> Result<ValidatedTestSpec, TestSpecError> {
let project_root = project.root();
let toml = project.toml().into_local()?;
let test_dependencies = toml.test_dependencies().current_platform();
let is_busted = project_root.join(".busted").is_file()
|| test_dependencies
.iter()
.any(|dep| dep.name().to_string() == "busted");
match self {
Self::AutoDetect if is_busted => {
if test_dependencies
.iter()
.any(|dep| dep.name().to_string() == "nlua")
{
Ok(ValidatedTestSpec::BustedNlua(BustedTestSpec::default()))
} else {
Ok(ValidatedTestSpec::Busted(BustedTestSpec::default()))
}
}
Self::Busted(spec) => Ok(ValidatedTestSpec::Busted(spec.clone())),
Self::BustedNlua(spec) => Ok(ValidatedTestSpec::BustedNlua(spec.clone())),
Self::Command(spec) => Ok(ValidatedTestSpec::Command(spec.clone())),
Self::Script(spec) => Ok(ValidatedTestSpec::LuaScript(spec.clone())),
Self::AutoDetect => Err(TestSpecError::NoTestSpecDetected),
}
}
}
impl ValidatedTestSpec {
pub fn args(&self) -> Vec<String> {
match self {
Self::Busted(spec) => spec.flags.clone(),
Self::BustedNlua(spec) => {
let mut flags = spec.flags.clone();
flags.push("--ignore-lua".into());
flags
}
Self::Command(spec) => spec.flags.clone(),
Self::LuaScript(spec) => std::iter::once(spec.script.to_slash_lossy().to_string())
.chain(spec.flags.clone())
.collect_vec(),
}
}
pub(crate) fn test_config(&self, config: &Config) -> Result<Config, ConfigError> {
match self {
Self::BustedNlua(_) => {
let config_builder: ConfigBuilder = config.clone().into();
#[cfg(not(any(target_os = "macos", target_env = "msvc")))]
let lua_version = LuaVersion::Lua51;
#[cfg(any(target_os = "macos", target_env = "msvc"))]
let lua_version = LuaVersion::LuaJIT;
Ok(config_builder
.lua_version(Some(lua_version))
.variables(Some(
vec![("LUA".to_string(), NLUA_EXE.to_string())]
.into_iter()
.collect(),
))
.build()?)
}
_ => Ok(config.clone()),
}
}
fn test_dependencies(&self) -> Vec<PackageReq> {
match self {
Self::Busted(_) => unsafe { vec![PackageReq::new_unchecked("busted".into(), None)] },
Self::BustedNlua(_) => unsafe {
vec![
PackageReq::new_unchecked("busted".into(), None),
PackageReq::new_unchecked("nlua".into(), None),
]
},
Self::Command(_) => Vec::new(),
Self::LuaScript(_) => Vec::new(),
}
}
}
impl TryFrom<TestSpecInternal> for TestSpec {
type Error = TestSpecDecodeError;
fn try_from(internal: TestSpecInternal) -> Result<Self, Self::Error> {
let test_spec = match internal.test_type {
Some(TestType::Busted) => Ok(Self::Busted(BustedTestSpec {
flags: internal.flags.unwrap_or_default(),
})),
Some(TestType::BustedNlua) => Ok(Self::BustedNlua(BustedTestSpec {
flags: internal.flags.unwrap_or_default(),
})),
Some(TestType::Command) => match (internal.command, internal.lua_script) {
(None, None) => Err(TestSpecDecodeError::NoCommandOrScript),
(None, Some(script)) => Ok(Self::Script(LuaScriptTestSpec {
script,
flags: internal.flags.unwrap_or_default(),
})),
(Some(command), None) => Ok(Self::Command(CommandTestSpec {
command,
flags: internal.flags.unwrap_or_default(),
})),
(Some(_), Some(_)) => Err(TestSpecDecodeError::CommandAndScript),
},
None => Ok(Self::default()),
}?;
Ok(test_spec)
}
}
impl<'de> Deserialize<'de> for PerPlatform<TestSpec> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
per_platform_from_intermediate::<_, TestSpecInternal, _>(deserializer)
}
}
impl<'de> Deserialize<'de> for TestSpec {
fn deserialize<D>(deserializer: D) -> Result<TestSpec, D::Error>
where
D: Deserializer<'de>,
{
let internal = TestSpecInternal::deserialize(deserializer)?;
let test_spec = TestSpec::try_from(internal).map_err(serde::de::Error::custom)?;
Ok(test_spec)
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct BustedTestSpec {
pub(crate) flags: Vec<String>,
}
impl BustedTestSpec {
pub fn flags(&self) -> &Vec<String> {
&self.flags
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CommandTestSpec {
pub(crate) command: String,
pub(crate) flags: Vec<String>,
}
impl CommandTestSpec {
pub fn flags(&self) -> &Vec<String> {
&self.flags
}
pub fn command(&self) -> &str {
&self.command
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LuaScriptTestSpec {
pub(crate) script: PathBuf,
pub(crate) flags: Vec<String>,
}
impl LuaScriptTestSpec {
pub fn flags(&self) -> &Vec<String> {
&self.flags
}
pub fn script(&self) -> &PathBuf {
&self.script
}
}
#[derive(Debug, Deserialize, Serialize_enum_str, PartialEq, Clone)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum TestType {
Busted,
BustedNlua,
Command,
}
#[derive(Debug, PartialEq, Deserialize, Default, Clone)]
pub(crate) struct TestSpecInternal {
#[serde(default, rename = "type")]
pub(crate) test_type: Option<TestType>,
#[serde(default)]
pub(crate) flags: Option<Vec<String>>,
#[serde(default)]
pub(crate) command: Option<String>,
#[serde(default, rename = "script", alias = "lua_script")]
pub(crate) lua_script: Option<PathBuf>,
}
impl PartialOverride for TestSpecInternal {
type Err = Infallible;
fn apply_overrides(&self, override_spec: &Self) -> Result<Self, Self::Err> {
Ok(TestSpecInternal {
test_type: override_opt(&override_spec.test_type, &self.test_type),
flags: match (override_spec.flags.clone(), self.flags.clone()) {
(Some(override_vec), Some(base_vec)) => {
let merged: Vec<String> =
base_vec.into_iter().chain(override_vec).unique().collect();
Some(merged)
}
(None, base_vec @ Some(_)) => base_vec,
(override_vec @ Some(_), None) => override_vec,
_ => None,
},
command: match override_spec.lua_script.clone() {
Some(_) => None,
None => override_opt(&override_spec.command, &self.command),
},
lua_script: match override_spec.command.clone() {
Some(_) => None,
None => override_opt(&override_spec.lua_script, &self.lua_script),
},
})
}
}
impl PlatformOverridable for TestSpecInternal {
type Err = Infallible;
fn on_nil<T>() -> Result<PerPlatform<T>, <Self as PlatformOverridable>::Err>
where
T: PlatformOverridable,
T: Default,
{
Ok(PerPlatform::default())
}
}
fn override_opt<T: Clone>(override_opt: &Option<T>, base: &Option<T>) -> Option<T> {
match override_opt.clone() {
override_val @ Some(_) => override_val,
None => base.clone(),
}
}
#[cfg(test)]
mod tests {
use ottavino::{Closure, Executor, Fuel, Lua};
use ottavino_util::serde::from_value;
use crate::lua_rockspec::PlatformIdentifier;
use super::*;
fn exec_lua<T: serde::de::DeserializeOwned>(
code: &str,
key: &'static str,
) -> Result<T, ottavino::ExternError> {
Lua::core().try_enter(|ctx| {
let closure = Closure::load(ctx, None, code.as_bytes())?;
let executor = Executor::start(ctx, closure.into(), ());
executor.step(ctx, &mut Fuel::with(i32::MAX))?;
from_value(ctx.globals().get_value(ctx, key)).map_err(ottavino::Error::from)
})
}
#[tokio::test]
pub async fn test_spec_from_lua() {
let lua_content = "
test = {\n
}\n
";
let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
assert!(matches!(test_spec.default, TestSpec::AutoDetect));
let lua_content = "
test = {\n
type = 'busted',\n
}\n
";
let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
assert_eq!(
test_spec.default,
TestSpec::Busted(BustedTestSpec::default())
);
let lua_content = "
test = {\n
type = 'busted',\n
flags = { 'foo', 'bar' },\n
}\n
";
let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
assert_eq!(
test_spec.default,
TestSpec::Busted(BustedTestSpec {
flags: vec!["foo".into(), "bar".into()],
})
);
let lua_content = "
test = {\n
type = 'command',\n
}\n
";
let result: Result<PerPlatform<TestSpec>, _> = exec_lua(lua_content, "test");
let _err = result.unwrap_err();
let lua_content = "
test = {\n
type = 'command',\n
command = 'foo',\n
script = 'bar',\n
}\n
";
let result: Result<PerPlatform<TestSpec>, _> = exec_lua(lua_content, "test");
let _err = result.unwrap_err();
let lua_content = "
test = {\n
type = 'command',\n
command = 'baz',\n
flags = { 'foo', 'bar' },\n
}\n
";
let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
assert_eq!(
test_spec.default,
TestSpec::Command(CommandTestSpec {
command: "baz".into(),
flags: vec!["foo".into(), "bar".into()],
})
);
let lua_content = "
test = {\n
type = 'command',\n
script = 'test.lua',\n
flags = { 'foo', 'bar' },\n
}\n
";
let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
assert_eq!(
test_spec.default,
TestSpec::Script(LuaScriptTestSpec {
script: PathBuf::from("test.lua"),
flags: vec!["foo".into(), "bar".into()],
})
);
let lua_content = "
test = {\n
type = 'command',\n
command = 'baz',\n
flags = { 'foo', 'bar' },\n
platforms = {\n
unix = { flags = { 'baz' }, },\n
macosx = {\n
script = 'bat.lua',\n
flags = { 'bat' },\n
},\n
linux = { type = 'busted' },\n
},\n
}\n
";
let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
assert_eq!(
test_spec.default,
TestSpec::Command(CommandTestSpec {
command: "baz".into(),
flags: vec!["foo".into(), "bar".into()],
})
);
let unix = test_spec
.per_platform
.get(&PlatformIdentifier::Unix)
.unwrap();
assert_eq!(
*unix,
TestSpec::Command(CommandTestSpec {
command: "baz".into(),
flags: vec!["foo".into(), "bar".into(), "baz".into()],
})
);
let macosx = test_spec
.per_platform
.get(&PlatformIdentifier::MacOSX)
.unwrap();
assert_eq!(
*macosx,
TestSpec::Script(LuaScriptTestSpec {
script: "bat.lua".into(),
flags: vec!["foo".into(), "bar".into(), "bat".into(), "baz".into()],
})
);
let linux = test_spec
.per_platform
.get(&PlatformIdentifier::Linux)
.unwrap();
assert_eq!(
*linux,
TestSpec::Busted(BustedTestSpec {
flags: vec!["foo".into(), "bar".into(), "baz".into()],
})
);
let lua_content = "
test = {\n
type = 'busted-nlua',\n
}";
let test_spec: PerPlatform<TestSpec> = exec_lua(lua_content, "test").unwrap();
assert_eq!(
test_spec.default,
TestSpec::BustedNlua(BustedTestSpec { flags: Vec::new() })
);
}
}