use std::fs;
use std::path::PathBuf;
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
struct Fixture {
root: PathBuf,
}
impl Fixture {
fn new(label: &str) -> Self {
let ordinal = NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed);
let root = std::env::temp_dir().join(format!(
"noxid-wo26-deployment-{label}-{}-{ordinal}",
std::process::id()
));
fs::create_dir_all(&root).expect("create WO-26 deployment fixture");
Self { root }
}
fn write(&self, relative: &str, contents: &str) {
let path = self.root.join(relative);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create fixture parent");
}
fs::write(path, contents).expect("write fixture file");
}
fn adapt(&self, adapter: &str, output: &str) -> Output {
Command::new(env!("CARGO_BIN_EXE_noxid"))
.args(["adapt", ".", "--adapter", adapter, "--out-dir", output])
.env_remove("DATABASE_URL")
.current_dir(&self.root)
.output()
.expect("run noxid adapt")
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
fn assert_success(output: &Output, context: &str) {
assert!(
output.status.success(),
"{context}:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
#[test]
fn vercel_max_duration_is_derived_from_project_endpoints_and_overridden_by_server_config() {
let fixture = Fixture::new("vercel-duration");
fixture.write(
"Noxid.toml",
"[app]\ntitle = \"Vercel duration\"\nbase = \"/console\"\n",
);
fixture.write(
"server/api/slow.get.nox",
"endpoint Slow { result: String timeout: 120001ms handler { return \"ok\" } }\n",
);
let derived = fixture.adapt("vercel", "dist-derived");
assert_success(&derived, "adapt project with derived Vercel duration");
let derived_config = fs::read_to_string(
fixture
.root
.join("dist-derived/.vercel/output/functions/noxid.func/.vc-config.json"),
)
.expect("read derived Vercel function config");
assert!(derived_config.contains("\"maxDuration\":121"));
fixture.write(
"Noxid.toml",
"[app]\ntitle = \"Vercel duration\"\nbase = \"/console\"\n\n[server]\nvercel_max_duration = 17\n",
);
let overridden = fixture.adapt("vercel", "dist-overridden");
assert_success(&overridden, "adapt project with overridden Vercel duration");
let overridden_config = fs::read_to_string(
fixture
.root
.join("dist-overridden/.vercel/output/functions/noxid.func/.vc-config.json"),
)
.expect("read overridden Vercel function config");
assert!(overridden_config.contains("\"maxDuration\":17"));
}
#[test]
fn every_adapter_completes_a_real_noxid_adapt_run() {
let fixture = Fixture::new("all-adapters");
fixture.write(
"Noxid.toml",
"[app]\ntitle = \"Adapter matrix\"\nbase = \"/console\"\n",
);
fixture.write(
"src/routes/+page.nox",
"component Home { route { title: \"Home\" } view { <main>Home</main> } }\n",
);
let expectations = [
("static", "deployment.plan.json"),
("node", "server.mjs"),
("deno", "server.ts"),
("cloudflare", "_redirects"),
("vercel", ".vercel/output/config.json"),
("netlify", "netlify.toml"),
];
for (adapter, artifact) in expectations {
let output = format!("dist-{adapter}");
let adapted = fixture.adapt(adapter, &output);
assert_success(&adapted, &format!("adapt project for {adapter}"));
assert!(
fixture.root.join(output).join(artifact).is_file(),
"{adapter} did not emit {artifact}"
);
}
}
#[test]
fn reachable_database_records_pending_migrations_in_the_deployment_plan() {
let fixture = Fixture::new("migration-warning");
fixture.write("Noxid.toml", "[app]\ntitle = \"Migration warning\"\n");
fixture.write(
"src/routes/+page.nox",
"component Home { route { title: \"Home\" } view { <main>Home</main> } }\n",
);
fixture.write(
"server/db/migrations/0001-create-probe.sql",
"CREATE TABLE deployment_probe (id INTEGER PRIMARY KEY);\n",
);
fs::create_dir_all(fixture.root.join("data")).expect("create SQLite data directory");
let adapted = Command::new(env!("CARGO_BIN_EXE_noxid"))
.args(["adapt", ".", "--adapter", "static", "--out-dir", "dist"])
.env("DATABASE_URL", "sqlite://data/deployment.db")
.current_dir(&fixture.root)
.output()
.expect("adapt with reachable SQLite database");
assert_success(&adapted, "adapt project with pending migration");
let plan = fs::read_to_string(fixture.root.join("dist/deployment.plan.json"))
.expect("read deployment plan");
assert!(plan.contains("pending database migrations"), "{plan}");
assert!(plan.contains("0001-create-probe.sql"), "{plan}");
assert!(plan.contains("noxid db migrate"), "{plan}");
}