use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use crate::test_toml::{StepDef, TestToml};
#[derive(Debug, Clone)]
pub enum DiscoveredTest {
Simple {
name: String,
source: PathBuf,
setup: SetupConfig,
tests: Vec<TestEntry>,
browser: bool,
ram_override: Option<u32>,
},
Lifecycle {
name: String,
source: PathBuf,
steps: Vec<StepDef>,
browser: bool,
ram_override: Option<u32>,
},
}
#[derive(Debug, Clone, Default)]
pub struct SetupConfig {
pub services: Vec<String>,
pub quadlets: Vec<String>,
pub quadlet_dir: Option<PathBuf>,
}
impl DiscoveredTest {
pub fn name(&self) -> &str {
match self {
DiscoveredTest::Simple { name, .. } => name,
DiscoveredTest::Lifecycle { name, .. } => name,
}
}
pub fn source(&self) -> &Path {
match self {
DiscoveredTest::Simple { source, .. } => source,
DiscoveredTest::Lifecycle { source, .. } => source,
}
}
pub fn step_kinds(&self) -> Vec<&'static str> {
let mut kinds: Vec<&'static str> = Vec::new();
let push = |k: &'static str, v: &mut Vec<&'static str>| {
if !v.contains(&k) {
v.push(k);
}
};
match self {
DiscoveredTest::Lifecycle { steps, .. } => {
for step in steps {
let kind = match step {
StepDef::Add { .. } => "add",
StepDef::Remove { .. } => "remove",
StepDef::Reset => "reset",
StepDef::Wait { .. } => "wait",
StepDef::Shell { .. } => "shell",
StepDef::Http { .. } => "http",
StepDef::Playwright { .. } => "playwright",
StepDef::Mail { .. } => "mail",
};
push(kind, &mut kinds);
}
}
DiscoveredTest::Simple { setup, tests, .. } => {
if !setup.services.is_empty() || !setup.quadlets.is_empty() {
push("setup", &mut kinds);
}
if !tests.is_empty() {
push("shell", &mut kinds);
}
}
}
kinds
}
pub fn services(&self) -> Vec<&str> {
match self {
DiscoveredTest::Simple { setup, .. } => {
setup.services.iter().map(|s| s.as_str()).collect()
}
DiscoveredTest::Lifecycle { steps, .. } => {
let mut svcs = Vec::new();
for step in steps {
if let StepDef::Add { service, .. } = step
&& !svcs.contains(&service.as_str())
{
svcs.push(service.as_str());
}
}
svcs
}
}
}
pub fn tests(&self) -> &[TestEntry] {
match self {
DiscoveredTest::Simple { tests, .. } => tests,
DiscoveredTest::Lifecycle { .. } => &[],
}
}
pub fn test_count(&self) -> usize {
match self {
DiscoveredTest::Simple { tests, .. } => tests.len(),
DiscoveredTest::Lifecycle { steps, .. } => steps.len(),
}
}
#[allow(dead_code)]
pub fn summary(&self) -> String {
match self {
DiscoveredTest::Simple { name, setup, .. } => {
if setup.services.is_empty() {
name.clone()
} else {
format!("{} ({})", name, setup.services.join(" + "))
}
}
DiscoveredTest::Lifecycle { name, steps, .. } => {
format!("{} ({} steps)", name, steps.len())
}
}
}
pub fn is_lifecycle(&self) -> bool {
matches!(self, DiscoveredTest::Lifecycle { .. })
}
pub fn has_quadlets(&self) -> bool {
match self {
DiscoveredTest::Simple { setup, .. } => !setup.quadlets.is_empty(),
DiscoveredTest::Lifecycle { .. } => false,
}
}
pub fn needs_browser(&self) -> bool {
match self {
DiscoveredTest::Simple { browser, .. } => *browser,
DiscoveredTest::Lifecycle { browser, .. } => *browser,
}
}
pub fn ram_override(&self) -> Option<u32> {
match self {
DiscoveredTest::Simple { ram_override, .. } => *ram_override,
DiscoveredTest::Lifecycle { ram_override, .. } => *ram_override,
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TestEntry {
pub name: String,
pub run: String,
pub timeout_secs: u64,
pub env: std::collections::BTreeMap<String, String>,
}
pub fn discover_local_project(project_dir: &Path) -> Result<Option<DiscoveredTest>> {
let test_toml_path = project_dir.join("test.toml");
if !test_toml_path.exists() {
return Ok(None);
}
let parsed = TestToml::parse(&test_toml_path)?;
let quadlet_extensions = ["container", "volume", "network", "pod", "kube"];
let mut quadlet_files = Vec::new();
let entries = std::fs::read_dir(project_dir)
.with_context(|| format!("failed to read {}", project_dir.display()))?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if let Some(ext) = path.extension().and_then(|e| e.to_str())
&& quadlet_extensions.contains(&ext)
&& let Some(name) = path.file_name().and_then(|n| n.to_str())
{
quadlet_files.push(name.to_string());
}
}
if quadlet_files.is_empty() {
anyhow::bail!(
"test.toml found at {} but no quadlet files (.container, .volume, .network, .pod) in the same directory",
test_toml_path.display()
);
}
let project_dir = std::fs::canonicalize(project_dir)
.with_context(|| format!("failed to canonicalize {}", project_dir.display()))?;
let dir_name = project_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("project")
.to_string();
let mut tests =
discover_from_test_toml(&test_toml_path, &parsed, &dir_name, Some(&project_dir))?;
if tests.len() != 1 {
anyhow::bail!(
"local project test.toml must describe exactly one test (got {}); \
multi-test [[tests]] arrays are only supported inside the registry",
tests.len()
);
}
let mut test = tests.remove(0);
if let DiscoveredTest::Simple { ref mut setup, .. } = test
&& setup.quadlets.is_empty()
&& !quadlet_files.is_empty()
{
setup.quadlets = quadlet_files;
}
Ok(Some(test))
}
pub fn discover(registry_path: &Path) -> Result<Vec<DiscoveredTest>> {
let mut discovered = Vec::new();
let entries = std::fs::read_dir(registry_path)
.with_context(|| format!("failed to read registry at {}", registry_path.display()))?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let dir_name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
if dir_name == "tests" || dir_name.starts_with('.') {
continue;
}
let test_toml_path = path.join("test.toml");
if !test_toml_path.exists() {
continue;
}
match TestToml::parse(&test_toml_path) {
Ok(parsed) => {
match discover_from_test_toml(&test_toml_path, &parsed, &dir_name, None) {
Ok(tests) => discovered.extend(tests),
Err(e) => {
eprintln!(
"warning: failed to process {}: {e}",
test_toml_path.display()
);
}
}
}
Err(e) => {
eprintln!("warning: failed to parse {}: {e}", test_toml_path.display());
}
}
}
let tests_dir = registry_path.join("tests");
if tests_dir.is_dir() {
let entries = std::fs::read_dir(&tests_dir)
.with_context(|| format!("failed to read {}", tests_dir.display()))?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("toml") {
continue;
}
let file_stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
match TestToml::parse(&path) {
Ok(parsed) => match discover_from_test_toml(&path, &parsed, &file_stem, None) {
Ok(tests) => discovered.extend(tests),
Err(e) => {
eprintln!("warning: failed to process {}: {e}", path.display());
}
},
Err(e) => {
eprintln!("warning: failed to parse {}: {e}", path.display());
}
}
}
}
discovered.sort_by(|a, b| a.name().cmp(b.name()));
Ok(discovered)
}
fn discover_from_test_toml(
path: &Path,
parsed: &TestToml,
service_name_hint: &str,
quadlet_dir: Option<&Path>,
) -> Result<Vec<DiscoveredTest>> {
let new_format_tests: Vec<&crate::test_toml::TestDef> = parsed
.tests
.iter()
.filter(|t| !t.steps.is_empty())
.collect();
if !new_format_tests.is_empty() {
let is_service_owned = path
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
== Some(service_name_hint);
let mut out = Vec::with_capacity(new_format_tests.len());
for t in new_format_tests {
let qualified = if is_service_owned && t.name != service_name_hint {
format!("{service_name_hint}-{}", t.name)
} else {
t.name.clone()
};
out.push(DiscoveredTest::Lifecycle {
name: qualified,
source: path.to_path_buf(),
steps: t.steps.clone(),
browser: t.browser || parsed.needs_browser(),
ram_override: t.ram.or(parsed.ram_override()),
});
}
return Ok(out);
}
let name = parsed.name_or_default(path);
let test_name = if parsed.test.as_ref().and_then(|t| t.name.as_ref()).is_some() {
name
} else {
service_name_hint.to_string()
};
let browser = parsed.needs_browser();
let ram_override = parsed.ram_override();
if parsed.is_lifecycle() {
return Ok(vec![DiscoveredTest::Lifecycle {
name: test_name,
source: path.to_path_buf(),
steps: parsed.steps.clone(),
browser,
ram_override,
}]);
}
let setup = match &parsed.setup {
Some(s) => SetupConfig {
services: s.services.clone(),
quadlets: s.quadlets.clone(),
quadlet_dir: quadlet_dir.map(PathBuf::from),
},
None => SetupConfig {
services: vec![service_name_hint.to_string()],
quadlets: Vec::new(),
quadlet_dir: quadlet_dir.map(PathBuf::from),
},
};
let tests = parsed
.tests
.iter()
.map(|t| TestEntry {
name: t.name.clone(),
run: t.run.clone().unwrap_or_default(),
timeout_secs: t.timeout,
env: t.env.clone(),
})
.collect();
Ok(vec![DiscoveredTest::Simple {
name: test_name,
source: path.to_path_buf(),
setup,
tests,
browser,
ram_override,
}])
}
pub fn service_recommended_ram(registry_path: &Path, service_name: &str) -> Result<Option<u64>> {
let service_toml = registry_path.join(service_name).join("service.toml");
if !service_toml.exists() {
return Ok(None);
}
let content = std::fs::read_to_string(&service_toml)
.with_context(|| format!("failed to read {}", service_toml.display()))?;
let parsed: ServiceTomlRam = toml::from_str(&content)
.with_context(|| format!("failed to parse {}", service_toml.display()))?;
Ok(parsed.requirements.and_then(|r| r.ram.recommended))
}
pub fn vm_memory_for_test(registry_path: &Path, test: &DiscoveredTest) -> u32 {
if let Some(ram) = test.ram_override() {
return ram;
}
let services: Vec<&str> = match test {
DiscoveredTest::Lifecycle { steps, .. } => steps
.iter()
.filter_map(|s| match s {
StepDef::Add { service, .. } => Some(service.as_str()),
StepDef::Shell { run, .. } => run
.split_whitespace()
.collect::<Vec<_>>()
.windows(3)
.find(|w| w[0] == "ryra" && w[1] == "add")
.map(|w| w[2]),
_ => None,
})
.collect(),
_ => test.services(),
};
let service_ram: u64 = services
.iter()
.map(|svc| {
service_recommended_ram(registry_path, svc)
.ok()
.flatten()
.unwrap_or(128) })
.sum();
let browser_overhead = if test.needs_browser() { 512 } else { 0 };
let total = service_ram + 512 + browser_overhead; let rounded = total.div_ceil(512) * 512; rounded.max(1024) as u32
}
pub fn service_min_disk(registry_path: &Path, service_name: &str) -> Result<Option<u32>> {
let service_toml = registry_path.join(service_name).join("service.toml");
if !service_toml.exists() {
return Ok(None);
}
let content = std::fs::read_to_string(&service_toml)
.with_context(|| format!("failed to read {}", service_toml.display()))?;
let parsed: ServiceTomlDisk = toml::from_str(&content)
.with_context(|| format!("failed to parse {}", service_toml.display()))?;
Ok(parsed.requirements.and_then(|r| r.disk).map(|d| d.min))
}
pub fn vm_disk_for_test(registry_path: &Path, test: &DiscoveredTest) -> u32 {
let services: Vec<&str> = match test {
DiscoveredTest::Lifecycle { steps, .. } => steps
.iter()
.filter_map(|s| match s {
StepDef::Add { service, .. } => Some(service.as_str()),
StepDef::Shell { run, .. } => run
.split_whitespace()
.collect::<Vec<_>>()
.windows(3)
.find(|w| w[0] == "ryra" && w[1] == "add")
.map(|w| w[2]),
_ => None,
})
.collect(),
_ => test.services(),
};
let max_disk: u32 = services
.iter()
.filter_map(|svc| service_min_disk(registry_path, svc).ok().flatten())
.max()
.unwrap_or(0);
max_disk.max(20)
}
pub fn service_image(registry_path: &Path, service_name: &str) -> Result<Option<String>> {
let images = service_images(registry_path, service_name);
Ok(images.into_iter().next())
}
fn implied_services_from_args(args: &str) -> Vec<&str> {
let tokens: Vec<&str> = args.split_whitespace().collect();
let mut out: Vec<&str> = Vec::new();
let push = |svc: &'static str, out: &mut Vec<&str>| {
if !out.contains(&svc) {
out.push(svc);
}
};
let mut i = 0;
while i < tokens.len() {
let t = tokens[i];
if let Some(val) = t.strip_prefix("--smtp=") {
if !val.is_empty() && !out.contains(&val) {
out.push(val);
}
} else if t == "--smtp" {
if let Some(val) = tokens.get(i + 1)
&& !val.starts_with("--")
&& !out.contains(val)
{
out.push(val);
i += 1;
}
} else if t == "--auth" || t.starts_with("--auth=") {
push("authelia", &mut out);
} else if t == "--domain"
|| t.starts_with("--domain=")
|| t == "--url"
|| t.starts_with("--url=")
{
push("caddy", &mut out);
}
i += 1;
}
out
}
pub fn service_images(registry_path: &Path, service_name: &str) -> Vec<String> {
let quadlets_dir = registry_path.join(service_name).join("quadlets");
let mut images = Vec::new();
if let Ok(entries) = std::fs::read_dir(&quadlets_dir) {
for entry in entries.flatten() {
let path = entry.path();
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if !name.ends_with(".container") {
continue;
}
if let Ok(content) = std::fs::read_to_string(&path) {
for line in content.lines() {
let trimmed = line.trim();
if let Some(image) = trimmed.strip_prefix("Image=") {
let image = image.trim();
if !image.is_empty() && !images.contains(&image.to_string()) {
images.push(image.to_string());
}
}
}
}
}
}
images
}
pub fn images_for_test(registry_path: &Path, test: &DiscoveredTest) -> Vec<String> {
let mut images = Vec::new();
let add_image = |img: String, out: &mut Vec<String>| {
if !out.contains(&img) {
out.push(img);
}
};
let include_service = |svc: &str, out: &mut Vec<String>| {
for image in service_images(registry_path, svc) {
add_image(image, out);
}
};
match test {
DiscoveredTest::Lifecycle { steps, .. } => {
for step in steps {
match step {
StepDef::Add { service, args, .. } => {
include_service(service, &mut images);
if let Some(args_str) = args {
for implied in implied_services_from_args(args_str) {
include_service(implied, &mut images);
}
}
}
StepDef::Shell { run, .. } => {
let tokens: Vec<&str> = run.split_whitespace().collect();
if let Some(idx) = tokens
.windows(2)
.position(|w| w[0] == "ryra" && w[1] == "add")
{
if let Some(svc) = tokens.get(idx + 2) {
include_service(svc, &mut images);
}
let rest = &tokens[idx + 2..];
for implied in implied_services_from_args(&rest.join(" ")) {
include_service(implied, &mut images);
}
}
}
_ => {}
}
}
}
DiscoveredTest::Simple { setup, .. } => {
for service in &setup.services {
for image in service_images(registry_path, service) {
if !images.contains(&image) {
images.push(image);
}
}
}
if let Some(ref dir) = setup.quadlet_dir {
for quadlet in &setup.quadlets {
let full_path = dir.join(quadlet);
if quadlet.ends_with(".container")
&& let Ok(content) = std::fs::read_to_string(&full_path)
{
for line in content.lines() {
let trimmed = line.trim();
if let Some(image) = trimmed.strip_prefix("Image=") {
let image = image.trim();
if !image.is_empty() && !images.contains(&image.to_string()) {
images.push(image.to_string());
}
}
}
}
}
}
}
}
images
}
#[derive(serde::Deserialize)]
struct ServiceTomlRam {
#[serde(default)]
requirements: Option<RequirementsRam>,
}
#[derive(serde::Deserialize)]
struct ServiceTomlDisk {
#[serde(default)]
requirements: Option<RequirementsDisk>,
}
#[derive(serde::Deserialize)]
struct RequirementsDisk {
#[serde(default)]
disk: Option<DiskFields>,
}
#[derive(serde::Deserialize)]
struct DiskFields {
min: u32,
}
#[derive(serde::Deserialize)]
struct RequirementsRam {
ram: RamFields,
}
#[derive(serde::Deserialize)]
struct RamFields {
#[serde(default)]
recommended: Option<u64>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discover_simple_test_from_test_toml() {
let dir = tempfile::tempdir().unwrap();
let test_toml = dir.path().join("test.toml");
std::fs::write(
&test_toml,
r#"
[[tests]]
name = "responds"
run = "curl -sf http://127.0.0.1:$SERVICE_PORT_HTTP"
[[tests]]
name = "hostname"
run = "curl -s http://127.0.0.1:$SERVICE_PORT_HTTP | grep -q Hostname"
timeout = 10
"#,
)
.unwrap();
let parsed = TestToml::parse(&test_toml).unwrap();
let mut out = discover_from_test_toml(&test_toml, &parsed, "whoami", None).unwrap();
assert_eq!(
out.len(),
1,
"legacy shell form produces a single Simple test"
);
let test = out.remove(0);
assert_eq!(test.name(), "whoami");
assert!(!test.is_lifecycle());
assert_eq!(test.test_count(), 2);
assert_eq!(test.services(), vec!["whoami"]); assert_eq!(test.tests()[0].name, "responds");
assert_eq!(test.tests()[1].timeout_secs, 10);
}
#[test]
fn discover_simple_test_with_setup() {
let dir = tempfile::tempdir().unwrap();
let test_toml = dir.path().join("test.toml");
std::fs::write(
&test_toml,
r#"
[test]
name = "whoami-plus-postgres"
[setup]
services = ["whoami", "postgres"]
[[tests]]
name = "both-running"
run = "echo ok"
"#,
)
.unwrap();
let parsed = TestToml::parse(&test_toml).unwrap();
let mut out = discover_from_test_toml(&test_toml, &parsed, "combo", None).unwrap();
assert_eq!(out.len(), 1);
let test = out.remove(0);
assert_eq!(test.name(), "whoami-plus-postgres");
assert_eq!(test.services(), vec!["whoami", "postgres"]);
assert_eq!(test.test_count(), 1);
}
#[test]
fn discover_lifecycle_test() {
let dir = tempfile::tempdir().unwrap();
let test_toml = dir.path().join("test.toml");
std::fs::write(
&test_toml,
r#"
[test]
name = "remove-test"
[[steps]]
action = "add"
service = "whoami"
[[steps]]
action = "wait"
service = "whoami"
[[steps]]
action = "shell"
name = "responds"
run = "curl -sf http://localhost"
[[steps]]
action = "remove"
service = "whoami"
[[steps]]
action = "shell"
name = "gone"
run = "! id whoami"
"#,
)
.unwrap();
let parsed = TestToml::parse(&test_toml).unwrap();
let mut out = discover_from_test_toml(&test_toml, &parsed, "remove-test", None).unwrap();
assert_eq!(out.len(), 1);
let test = out.remove(0);
assert_eq!(test.name(), "remove-test");
assert!(test.is_lifecycle());
assert_eq!(test.test_count(), 5);
assert_eq!(test.services(), vec!["whoami"]); }
#[test]
fn discover_lifecycle_with_args() {
let dir = tempfile::tempdir().unwrap();
let test_toml = dir.path().join("test.toml");
std::fs::write(
&test_toml,
r#"
[test]
name = "auth-test"
[[steps]]
action = "add"
service = "caddy"
args = "--domain proxy.test.local"
[[steps]]
action = "shell"
name = "caddy up"
run = "curl -sf http://proxy.test.local"
"#,
)
.unwrap();
let parsed = TestToml::parse(&test_toml).unwrap();
let mut out = discover_from_test_toml(&test_toml, &parsed, "auth-test", None).unwrap();
assert_eq!(out.len(), 1);
let test = out.remove(0);
assert!(test.is_lifecycle());
if let DiscoveredTest::Lifecycle { steps, .. } = &test {
if let StepDef::Add { service, args, .. } = &steps[0] {
assert_eq!(service, "caddy");
assert_eq!(args.as_deref(), Some("--domain proxy.test.local"));
} else {
panic!("expected Add step");
}
} else {
panic!("expected Lifecycle variant");
}
}
#[test]
fn discover_multi_test_service_owned() {
let dir = tempfile::tempdir().unwrap();
let svc_dir = dir.path().join("whoami");
std::fs::create_dir(&svc_dir).unwrap();
let test_toml = svc_dir.join("test.toml");
std::fs::write(
&test_toml,
r#"
[[tests]]
name = "whoami"
[[tests.steps]]
action = "add"
service = "whoami"
[[tests.steps]]
action = "wait"
service = "whoami"
[[tests]]
name = "diff"
[[tests.steps]]
action = "add"
service = "whoami"
[[tests.steps]]
action = "shell"
name = "idempotent"
run = "true"
[[tests]]
name = "remove"
[[tests.steps]]
action = "add"
service = "whoami"
[[tests.steps]]
action = "remove"
service = "whoami"
"#,
)
.unwrap();
let parsed = TestToml::parse(&test_toml).unwrap();
let out = discover_from_test_toml(&test_toml, &parsed, "whoami", None).unwrap();
assert_eq!(out.len(), 3);
let names: Vec<&str> = out.iter().map(|t| t.name()).collect();
assert_eq!(names, vec!["whoami", "whoami-diff", "whoami-remove"]);
for t in &out {
assert!(t.is_lifecycle());
}
}
#[test]
fn discover_multi_test_cross_cutting() {
let dir = tempfile::tempdir().unwrap();
let tests_dir = dir.path().join("tests");
std::fs::create_dir(&tests_dir).unwrap();
let test_toml = tests_dir.join("cross-thing.toml");
std::fs::write(
&test_toml,
r#"
[[tests]]
name = "first"
[[tests.steps]]
action = "add"
service = "whoami"
[[tests]]
name = "second"
[[tests.steps]]
action = "add"
service = "whoami"
"#,
)
.unwrap();
let parsed = TestToml::parse(&test_toml).unwrap();
let out = discover_from_test_toml(&test_toml, &parsed, "cross-thing", None).unwrap();
assert_eq!(out.len(), 2);
let names: Vec<&str> = out.iter().map(|t| t.name()).collect();
assert_eq!(names, vec!["first", "second"]);
}
#[test]
fn reject_test_with_both_run_and_steps() {
let dir = tempfile::tempdir().unwrap();
let test_toml = dir.path().join("test.toml");
std::fs::write(
&test_toml,
r#"
[[tests]]
name = "bad"
run = "true"
[[tests.steps]]
action = "add"
service = "whoami"
"#,
)
.unwrap();
let err = TestToml::parse(&test_toml).expect_err("must reject run+steps");
let msg = format!("{err:#}");
assert!(
msg.contains("exactly one of `run` or `steps`"),
"got: {msg}"
);
}
#[test]
fn discover_registry() {
let registry = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../ryra-core/registry");
if !registry.exists() {
return; }
let discovered = discover(®istry).unwrap();
if discovered.is_empty() {
return; }
let names: Vec<&str> = discovered.iter().map(|d| d.name()).collect();
for test in &discovered {
assert!(!test.name().is_empty());
assert!(test.test_count() > 0);
}
if names.contains(&"whoami") {
let whoami = discovered.iter().find(|d| d.name() == "whoami").unwrap();
assert!(whoami.is_lifecycle());
assert!(whoami.test_count() >= 1);
}
}
#[test]
fn discover_local_project_from_dir() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path();
std::fs::write(
project_dir.join("test.toml"),
r#"
[test]
name = "test-app"
[[tests]]
name = "responds"
run = "curl -sf http://127.0.0.1:8080"
"#,
)
.unwrap();
std::fs::write(
project_dir.join("test-app.container"),
"[Container]\nImage=docker.io/traefik/whoami:v1.11.0\n\n[Service]\nRestart=always\n",
)
.unwrap();
std::fs::write(project_dir.join("test-app.volume"), "[Volume]\n").unwrap();
let result = discover_local_project(project_dir).unwrap();
assert!(result.is_some());
let test = result.unwrap();
assert_eq!(test.name(), "test-app");
assert!(test.has_quadlets());
assert_eq!(test.test_count(), 1);
if let DiscoveredTest::Simple { setup, .. } = &test {
assert!(setup.quadlet_dir.is_some());
} else {
panic!("expected Simple variant");
}
}
#[test]
fn discover_local_project_with_setup_services() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path();
std::fs::write(
project_dir.join("test.toml"),
r#"
[test]
name = "my-app"
[setup]
services = ["postgres", "redis"]
quadlets = ["my-app.container"]
[[tests]]
name = "health-check"
run = "curl -sf http://127.0.0.1:8080/health"
timeout = 10
"#,
)
.unwrap();
std::fs::write(
project_dir.join("my-app.container"),
"[Container]\nImage=docker.io/myapp:latest\n",
)
.unwrap();
let result = discover_local_project(project_dir).unwrap();
let test = result.unwrap();
assert_eq!(test.name(), "my-app");
assert_eq!(test.services(), vec!["postgres", "redis"]);
assert!(test.has_quadlets());
}
#[test]
fn discover_local_project_no_quadlets() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("test.toml"),
"[[tests]]\nname = \"check\"\nrun = \"true\"\n",
)
.unwrap();
let result = discover_local_project(dir.path());
assert!(result.is_err()); }
#[test]
fn browser_flag_on_simple_test() {
let dir = tempfile::tempdir().unwrap();
let test_toml = dir.path().join("test.toml");
std::fs::write(
&test_toml,
r#"
[test]
browser = true
[[tests]]
name = "browser check"
run = "true"
"#,
)
.unwrap();
let parsed = TestToml::parse(&test_toml).unwrap();
let mut out = discover_from_test_toml(&test_toml, &parsed, "my-test", None).unwrap();
assert_eq!(out.len(), 1);
let test = out.remove(0);
assert!(test.needs_browser());
}
}