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::>(), [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()); }