Files
core/test/rust/native/testing.rs
T
2026-11-03 03:32:07 +00:00

385 lines
16 KiB
Rust

use super::Directory;
use dever::compiler::testing::{self, Suite};
use std::path::Path;
use std::time::Duration;
#[cfg(target_os = "linux")]
#[path = "testing/process.rs"]
mod process;
fn project() -> Directory {
let directory = Directory::new();
let root = directory.0.join("module/demo/value");
std::fs::create_dir_all(&root).unwrap();
for (name, text) in [
(
"app",
include_str!("../fixtures/testing/module/demo/value/app.dever"),
),
(
"port",
include_str!("../fixtures/testing/module/demo/value/port.dever"),
),
(
"adapter",
include_str!("../fixtures/testing/module/demo/value/adapter.dever"),
),
(
"api",
include_str!("../fixtures/testing/module/demo/value/api.dever"),
),
] {
std::fs::write(root.join(format!("{name}.dever")), text).unwrap();
}
std::fs::create_dir_all(directory.0.join("test/demo/value")).unwrap();
directory
}
fn case(root: &Path, name: &str, body: &str, fake: Option<&str>) {
let mut text = format!("{name}() () {{\n{body}\n}}\n");
if let Some(fake) = fake {
text.push_str(&format!(
"port.read(value: Int) (answer: Int) {{\n{fake}\n}}\n"
));
if fake.contains("echo") {
text.push_str("echo(value: Int) (answer: Int) { answer = value + 10 }\n");
}
}
std::fs::write(root.join(format!("test/demo/value/{name}.dever")), text).unwrap();
}
#[test]
fn native_test_suite_isolates_fake_effects_reports_failures_and_continues_after_timeout() {
let directory = project();
case(
&directory.0,
"first",
"assertEq(app.read(4), 5)",
Some("answer = value + 1"),
);
case(
&directory.0,
"second",
"assertEq(app.read(4), 14)",
Some("answer = wait(run(echo(value)))"),
);
case(
&directory.0,
"third",
"pending = run(dever.task.sleep(1000))\nassertEq(app.read(4), 99)\nwait(pending)",
Some("answer = value"),
);
case(&directory.0, "timeout", "dever.time.sleep(500)", None);
case(
&directory.0,
"valid",
"key = secret(\"case input\")\ndigest = dever.crypto.hmacSha256(key, dever.bytes.fromText(\"x\"))\nassertEq(dever.bytes.length(digest), 32)\nassertEq(dever.test.drainJobs(10), 0)",
None,
);
let suite = Suite::load(&directory.0).unwrap();
assert_eq!(suite.select(Some("demo.value.second")).unwrap(), [1]);
assert!(suite.select(Some("demo.valu")).is_err());
let report = testing::run(&suite, None, Duration::from_millis(150)).unwrap();
assert!(!report.success());
assert_eq!(
report
.cases
.iter()
.map(|case| case.success)
.collect::<Vec<_>>(),
[true, true, false, false, true]
);
assert!(report.cases[2].stderr.contains("actual: 4\nexpected: 99"));
assert!(report.cases[2].stderr.contains("third.dever:"));
assert!(report.cases[3].timed_out);
assert!(!directory.0.join("data").exists());
let filtered = testing::run(&suite, Some("demo.value.second"), Duration::from_secs(3)).unwrap();
assert!(filtered.success());
assert_eq!(filtered.not_run, 4);
assert_eq!(filtered.cases[0].index, 1);
}
#[test]
fn test_check_rejects_missing_fake_and_bad_unselected_case() {
let directory = project();
case(&directory.0, "first", "assertEq(app.read(4), 5)", None);
let error = Suite::load(&directory.0).err().unwrap();
assert!(
error.contains("fake") || error.contains("production Adapter"),
"{error}"
);
case(
&directory.0,
"first",
"assertEq(app.read(4), 5)",
Some("answer = value + 1"),
);
case(&directory.0, "second", "assert(42)", None);
assert!(Suite::load(&directory.0).is_err());
case(&directory.0, "second", "assertEq(1 +, 2)", None);
let output = std::process::Command::new(env!("CARGO_BIN_EXE_dever"))
.arg("test")
.arg(&directory.0)
.args(["--filter", "demo.value.first"])
.output()
.unwrap();
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("second.dever:"));
assert!(!String::from_utf8_lossy(&output.stdout).contains("PASS"));
}
#[test]
fn tooling_cli_formats_checks_and_executes_without_deployment_settings() {
let directory = project();
case(&directory.0, "simple", "assertEq(1 + 1, 2)", None);
std::fs::create_dir(directory.0.join("config")).unwrap();
std::fs::write(directory.0.join("config/setting.json"), r#"{"database":{"default":{"type":"postgres","host":"invalid.test","database":"production"}}}"#).unwrap();
let cli = |arguments: &[&str]| {
std::process::Command::new(env!("CARGO_BIN_EXE_dever"))
.arg(arguments[0])
.arg(&directory.0)
.args(&arguments[1..])
.output()
.unwrap()
};
assert!(!cli(&["fmt", "--check"]).status.success());
let formatted = cli(&["fmt"]);
assert!(
formatted.status.success(),
"{}",
String::from_utf8_lossy(&formatted.stderr)
);
assert!(cli(&["fmt", "--check"]).status.success());
let tested = cli(&[
"test",
"--filter",
"demo.value.simple",
"--timeout-ms",
"3000",
]);
assert!(
tested.status.success(),
"{}",
String::from_utf8_lossy(&tested.stderr)
);
assert!(String::from_utf8_lossy(&tested.stdout).contains("1 passed; 0 failed; 0 not run"));
assert!(!cli(&["test", "--filter", "demo.missing"]).status.success());
assert!(!cli(&["test", "--timeout-ms", "0"]).status.success());
assert!(!directory.0.join("data").exists());
std::fs::write(directory.0.join("dever.api"), "{}").unwrap();
assert!(
Suite::load(&directory.0).is_err(),
"tests must honor the API baseline"
);
}
#[test]
fn test_helpers_cannot_leak_into_production_or_call_ports_directly() {
let directory = project();
case(
&directory.0,
"direct",
"assertEq(port.read(4), 5)",
Some("answer = value + 1"),
);
assert!(Suite::load(&directory.0).is_err());
let source =
dever::syntax::source::Source::new(0, "case.dever", "main() () { assert(true) }".into());
let error = dever::compiler::check::check(
vec![dever::syntax::parse(&source).unwrap()],
&["main".into()],
)
.unwrap_err();
assert_eq!(error.code, "C006");
}
#[test]
fn test_private_choice_helpers_use_the_same_type_contract() {
let directory = project();
case(
&directory.0,
"local",
"assertEq(choose(State.Idle), 0)\nassertEq(choose(State.Ready), 1)",
None,
);
let path = directory.0.join("test/demo/value/local.dever");
let mut text = std::fs::read_to_string(&path).unwrap();
text.push_str("type State { Idle\nReady }\n\nchoose(value: State.Idle) (answer: Int) { answer = 0 }\nchoose(value: State.Ready) (answer: Int) { answer = 1 }\n");
std::fs::write(&path, text).unwrap();
let suite = Suite::load(&directory.0).unwrap();
assert_eq!(suite.cases.len(), 1);
}
#[test]
fn test_fake_can_implement_multiple_ports_but_must_complete_each_contract() {
let directory = project();
let module = directory.0.join("module/demo/value");
let port = std::fs::read_to_string(module.join("port.dever")).unwrap();
let adapter = std::fs::read_to_string(module.join("adapter.dever")).unwrap();
for role in ["port", "adapter"] {
std::fs::remove_file(module.join(format!("{role}.dever"))).unwrap();
std::fs::create_dir(module.join(role)).unwrap();
}
for topic in ["first", "second"] {
std::fs::write(
module.join(format!("port/{topic}.dever")),
port.replace("demo.value.port`", &format!("demo.value.port.{topic}`")),
)
.unwrap();
std::fs::write(
module.join(format!("adapter/{topic}.dever")),
adapter
.replace(
"demo.value.adapter`",
&format!("demo.value.adapter.{topic}`"),
)
.replace("port.read", &format!("port.{topic}.read")),
)
.unwrap();
}
let app = module.join("app.dever");
let text = std::fs::read_to_string(&app).unwrap();
std::fs::write(
&app,
text.replace(
"port.read(value)",
"port.first.read(value) + port.second.read(value)",
),
)
.unwrap();
case(
&directory.0,
"multiple",
"assertEq(app.read(4), 11)",
Some("answer = value + 1"),
);
let path = directory.0.join("test/demo/value/multiple.dever");
let first = std::fs::read_to_string(&path)
.unwrap()
.replace("port.read", "port.first.read");
std::fs::write(&path, &first).unwrap();
assert!(
Suite::load(&directory.0).is_err(),
"one missing fake must not use production"
);
let section = first[first.find("port.first.read(").unwrap()..]
.replace("port.first.read", "port.second.read")
.replace("value + 1", "value + 2");
std::fs::write(&path, format!("{first}\n{section}")).unwrap();
let suite = Suite::load(&directory.0).unwrap();
let report = testing::run(&suite, None, Duration::from_secs(5)).unwrap();
assert!(report.success(), "{}", report.cases[0].stderr);
}
#[test]
fn native_test_jobs_use_isolated_databases_virtual_clock_and_production_rollback() {
let directory = Directory::new();
std::fs::create_dir(directory.0.join("config")).unwrap();
std::fs::write(directory.0.join("config/setting.json"), r#"{"database":{"default":{"type":"postgres","host":"invalid.test","database":"production"}}}"#).unwrap();
let source_root =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../test/rust/fixtures/job/module/demo/queue");
let module = directory.0.join("module/demo/queue");
std::fs::create_dir_all(&module).unwrap();
for name in ["api", "app", "job", "model"] {
std::fs::copy(
source_root.join(format!("{name}.dever")),
module.join(format!("{name}.dever")),
)
.unwrap();
}
std::fs::create_dir_all(directory.0.join("test/demo/queue")).unwrap();
std::fs::write(
directory.0.join("test/demo/queue/idle.dever"),
"idle() () { assertEq(app.count(), 0) }\n",
)
.unwrap();
for topic in ["first", "second"] {
let body = "assertEq(app.count(), 0)\nfirst = app.submit(\"ok\", \"one\")\nsecond = app.submit(\"fail\", \"bad\")\nassert(first != second)\nassertEq(dever.test.drainJobs(10), 2)\nassertEq(app.count(), 1)\nassertEq(dever.test.drainJobs(10), 0)\nbefore = dever.time.now()\ndever.test.advanceClock(1000)\nassert(dever.time.now() != before)\nassertEq(dever.test.drainJobs(10), 1)\nassertEq(app.count(), 1)";
let text = format!("{topic}() () {{\n{body}\n}}\n");
std::fs::write(
directory.0.join(format!("test/demo/queue/{topic}.dever")),
text,
)
.unwrap();
}
let suite = Suite::load(&directory.0).unwrap();
let report = testing::run(&suite, None, Duration::from_secs(10)).unwrap();
for case in &report.cases {
assert!(case.success, "{}: {}", case.name, case.stderr);
}
assert_eq!(report.cases.len(), 3);
assert!(!directory.0.join("data").exists());
for topic in ["first", "idle", "second"] {
std::fs::remove_file(directory.0.join(format!("test/demo/queue/{topic}.dever"))).unwrap();
}
let job = module.join("job.dever");
let source = std::fs::read_to_string(&job).unwrap();
std::fs::write(job, format!("{source}\njob scheduled() () retry(1) timeout(1000) {{ app.apply(app.Payload {{ name = \"scheduled\" }}) }}\n\nschedule scheduled = \"* * * * *\"\n")).unwrap();
let app = module.join("app.dever");
let source = std::fs::read_to_string(&app).unwrap();
std::fs::write(app, format!("{source}\nsubmitEmpty(key: Text) (id: Id) {{ id = dever.job.enqueue(job.scheduled, key) }}\n")).unwrap();
std::fs::write(directory.0.join("test/demo/queue/cron.dever"), "cron() () {\n assertEq(dever.test.drainJobs(1), 1)\n assertEq(dever.test.drainJobs(1), 0)\n dever.test.advanceClock(60000)\n assertEq(dever.test.drainJobs(1), 1)\n assertEq(app.count(), 2)\n dever.test.advanceClock(180000)\n assertEq(dever.test.drainJobs(2), 2)\n assertEq(dever.test.drainJobs(2), 1)\n assertEq(dever.test.drainJobs(2), 0)\n assertEq(app.count(), 5)\n first = app.submitEmpty(\"manual\")\n second = app.submitEmpty(\"manual\")\n assertEq(first, second)\n assertEq(dever.test.drainJobs(2), 1)\n assertEq(app.count(), 6)\n}\n").unwrap();
let suite = Suite::load(&directory.0).unwrap();
let report = testing::run(&suite, None, Duration::from_secs(10)).unwrap();
assert!(report.success(), "{}", report.cases[0].stderr);
}
#[test]
fn native_test_jobs_from_protected_http_application_use_system_identity() {
let directory = Directory::new();
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../test/rust/fixtures/auth/module");
let mut pending = vec![fixture.clone()];
while let Some(source) = pending.pop() {
let destination = directory
.0
.join("module")
.join(source.strip_prefix(&fixture).unwrap());
std::fs::create_dir_all(&destination).unwrap();
for entry in std::fs::read_dir(source).unwrap() {
let entry = entry.unwrap();
if entry.file_type().unwrap().is_dir() {
pending.push(entry.path());
} else {
std::fs::copy(entry.path(), destination.join(entry.file_name())).unwrap();
}
}
}
let domain = directory.0.join("module/demo/account");
let jobs = domain.join("app/jobs.dever");
let source = std::fs::read_to_string(&jobs).unwrap();
std::fs::write(
&jobs,
source.replace("seed(input.subject,", "seed(port.identity(input.subject),"),
)
.unwrap();
std::fs::write(
domain.join("port.dever"),
"identity(value: Text) (answer: Text) fails dever.api.Error\n",
)
.unwrap();
let implementation = "port.identity(value: Text) (answer: Text) { answer = value }\n";
std::fs::write(domain.join("adapter.dever"), implementation).unwrap();
let production = dever::compiler::project::Project::load(&directory.0)
.unwrap()
.check()
.unwrap();
assert!(production.has_roles());
assert!(!production.jobs.is_empty());
std::fs::create_dir_all(directory.0.join("test/demo/account")).unwrap();
std::fs::write(directory.0.join("test/demo/account/jobs.dever"), "jobs() () {\n assertEq(app.jobCount(), 0)\n first = app.jobSubmit(\"from-test\", \"once\")\n second = app.jobSubmit(\"from-test\", \"once\")\n assertEq(first, second)\n assertEq(dever.test.drainJobs(3), 1)\n assertEq(app.jobCount(), 1)\n}\n").unwrap();
let test = directory.0.join("test/demo/account/jobs.dever");
let source = std::fs::read_to_string(&test).unwrap().replace("assertEq(app.jobCount(), 1)", "assertEq(app.jobCount(), 1)\n verified = app.verify(dever.auth.Claims { subject = \"fake-from-test\", session = \"from-job\", site = \"unused\", tenant = null })\n assertEq(verified.id, \"fake-from-test\")");
std::fs::write(
test,
format!(
"{source}\n{}",
implementation.replace("answer = value", "answer = \"fake-\" + value")
),
)
.unwrap();
let suite = Suite::load(&directory.0).unwrap();
let report = testing::run(&suite, None, Duration::from_secs(10)).unwrap();
assert!(report.success(), "{}", report.cases[0].stderr);
assert!(!directory.0.join("data").exists());
}