use lightshuttle_export::{ComposeEmitter, Emitter, lower};
use lightshuttle_manifest::Manifest;
mod common;
const STACK: &str = r"
project:
name: shop
resources:
db:
postgres:
version: '16'
password: devsecret
volume: dbdata
cache:
redis:
version: '7'
api:
container:
image: alpine:3.20
ports:
- 8080:80
env:
LOG_LEVEL: info
APP_NAME: shop
depends_on: [db, cache]
builder:
dockerfile:
context: ./app
dockerfile: Dockerfile
target: runtime
worker:
container:
image: alpine:3.20
";
fn emit(yaml: &str) -> String {
let manifest = Manifest::parse(yaml).expect("manifest parses");
let model = lower(&manifest).expect("lowering succeeds");
let artifacts = ComposeEmitter.emit(&model).expect("emit succeeds");
assert_eq!(artifacts.files.len(), 1);
assert_eq!(artifacts.files[0].path.to_str(), Some("docker-compose.yml"));
artifacts.files[0].contents.clone()
}
#[test]
fn matches_golden_file() {
let golden = include_str!("golden/docker-compose.yml");
assert_eq!(
emit(STACK),
golden,
"generated compose drifted from the golden file; \
re-inspect the change and update tests/golden/docker-compose.yml if intended"
);
}
#[test]
#[ignore = "requires docker compose on the host"]
fn output_passes_docker_compose_config() {
use std::io::Write;
if !common::tool_available("docker") {
eprintln!("skipping: docker not found on PATH");
return;
}
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("docker-compose.yml");
let mut file = std::fs::File::create(&path).expect("write compose");
file.write_all(emit(STACK).as_bytes()).expect("write bytes");
let output = std::process::Command::new("docker")
.args(["compose", "-f"])
.arg(&path)
.arg("config")
.output()
.expect("docker compose runs");
assert!(
output.status.success(),
"docker compose config rejected the output:\n{}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn ports_default_to_loopback() {
let out = emit(STACK);
assert!(
out.contains("127.0.0.1:8080:80"),
"api port should bind loopback by default, got:\n{out}"
);
assert!(
out.contains("127.0.0.1:5432:5432"),
"postgres port should bind loopback by default, got:\n{out}"
);
}
#[test]
fn dependencies_use_service_healthy_condition() {
let out = emit(STACK);
assert!(out.contains("depends_on:"), "got:\n{out}");
assert!(
out.contains("condition: service_healthy"),
"depends_on should gate on health, got:\n{out}"
);
}
#[test]
fn named_volumes_are_declared_top_level() {
let out = emit(STACK);
assert!(out.contains("\nvolumes:\n"), "got:\n{out}");
assert!(out.contains("dbdata:"), "got:\n{out}");
}
#[test]
fn environment_is_sorted() {
let out = emit(STACK);
let app = out.find("APP_NAME").expect("APP_NAME present");
let log = out.find("LOG_LEVEL").expect("LOG_LEVEL present");
assert!(app < log, "environment keys should be sorted, got:\n{out}");
}