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-wo27-pool-{label}-{}-{ordinal}",
std::process::id()
));
fs::create_dir_all(root.join("server/api")).expect("create endpoint directory");
fs::create_dir_all(root.join("server/queues")).expect("create queue directory");
fs::write(root.join("package.json"), "{\"type\":\"module\"}\n")
.expect("write package marker");
Self { root }
}
fn build(&self) -> Output {
Command::new(env!("CARGO_BIN_EXE_noxid"))
.args(["build", ".", "--out-dir", "dist"])
.current_dir(&self.root)
.output()
.expect("build pool fixture")
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
fn assert_success(output: &Output, phase: &str) {
assert!(
output.status.success(),
"{phase} failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn db_pool_flows_to_host_database_environment_and_queue_connection() {
let fixture = Fixture::new("threading");
fs::write(
fixture.root.join("Noxid.toml"),
"[app]\ntitle = \"Pool threading\"\n\n[server]\ndb_pool = 3\n",
)
.expect("write manifest");
fs::write(
fixture.root.join("server/api/pool.get.nox"),
"endpoint Pool { result: Int }\n",
)
.expect("write pool endpoint");
fs::write(
fixture.root.join("server/queues/Work.nox"),
"queue Work { payload { value: Int } retry: 1 backoff: 1s }\n",
)
.expect("write queue");
fs::write(
fixture.root.join("server/host.js"),
r#"export const endpoints = Object.freeze({
"endpoint:Pool@1": async (_args, context) => {
if (context.environment.self && context.environment !== context.environment.self) throw new Error("environment identity changed");
if (!context.environment.locked) context.environment.authorized = (context.environment.authorized ?? 0) + 1;
let overwriteRefused = false;
let deleteRefused = false;
try { context.environment.dbPool = 99; } catch { overwriteRefused = true; }
try { delete context.environment.dbPool; } catch { deleteRefused = true; }
if (!overwriteRefused || !deleteRefused || context.environment.dbPool !== 3) throw new Error("compiler-owned dbPool was mutable");
return context.environment.dbPool;
},
});
export const queues = Object.freeze({ "queue:Work": async ({ value }) => value });
"#,
)
.expect("write host");
let build = fixture.build();
assert_success(&build, "build configured pool fixture");
let handler = fs::read_to_string(fixture.root.join("dist/server/handler.js"))
.expect("read generated handler");
assert!(handler.contains("const databasePoolSize = 3;"));
assert!(handler.contains("postgres(url, { max: databasePoolSize })"));
assert!(!handler.contains("postgres(url, { max: 10 })"));
assert!(handler.contains("Object.defineProperty(environment, \"dbPool\""));
fs::write(
fixture.root.join("dist/exercise-pool.mjs"),
r#"import { fetch as handle } from "./server/handler.js";
const environment = { authorized: 0 };
environment.self = environment;
const response = await handle(new Request("http://noxid.test/api/pool"), environment);
const body = await response.json();
if (response.status !== 200 || body.value !== 3) throw new Error(`configured pool did not reach host: ${response.status} ${JSON.stringify(body)}`);
if (environment.authorized !== 1) throw new Error(`host writes did not reach the original environment: ${JSON.stringify(environment)}`);
const pool = Object.getOwnPropertyDescriptor(environment, "dbPool");
if (pool?.value !== 3 || pool.enumerable !== false || pool.writable !== false || pool.configurable !== false) throw new Error(`compiler-owned pool property changed: ${JSON.stringify(pool)}`);
delete process.env.dbPool;
delete process.env.authorized;
const processResponse = await handle(new Request("http://noxid.test/api/pool"), process.env);
const processBody = await processResponse.json();
if (processResponse.status !== 200 || processBody.value !== 3 || Object.hasOwn(process.env, "dbPool") || process.env.authorized !== "1") throw new Error(`process.env forwarding changed: ${processResponse.status} ${JSON.stringify(processBody)}`);
delete process.env.authorized;
const frozen = Object.freeze({ locked: true });
const frozenResponse = await handle(new Request("http://noxid.test/api/pool"), frozen);
const frozenBody = await frozenResponse.json();
if (frozenResponse.status !== 200 || frozenBody.value !== 3 || Object.hasOwn(frozen, "dbPool")) throw new Error(`frozen environment was mutated or lost dbPool: ${frozenResponse.status} ${JSON.stringify(frozenBody)}`);
"#,
)
.expect("write pool exercise");
let node = Command::new("node")
.arg("exercise-pool.mjs")
.current_dir(fixture.root.join("dist"))
.output()
.expect("exercise configured pool environment");
assert_success(&node, "exercise configured pool environment");
let adapter = fs::read_to_string(
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../plugins/drizzle-orm/adapter.js"),
)
.expect("read Drizzle adapter");
assert!(adapter.contains("const poolSize = environment?.dbPool ?? 10;"));
assert!(adapter.contains("postgres(url, { max: poolSize })"));
assert!(adapter.contains("connectionLimit: poolSize"));
assert!(!adapter.contains("postgres(url, { max: 10 })"));
let adapter_test = Command::new("node")
.args(["--test", "plugins/drizzle-orm/pool.test.mjs"])
.current_dir(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."))
.output()
.expect("run Drizzle pool contract");
assert_success(&adapter_test, "exercise Drizzle pool contract");
}