feat: release Dever 0.1.5 with direct workers and long transactions

This commit is contained in:
Codex committed 2026-10-11 11:17:33 +00:00
1 parent 89629c2197
commit a900d7e8db
153 files changed
+3676 -3708

No files matched your search

+151 -2
View File
@@ -40,6 +40,155 @@ fn reject(files: &[(&str, &str)], expected: &str) {
);
}
#[test]
fn ordinary_and_long_entries_have_explicit_transaction_policies() {
for entry in ["public post create = app.create", "cmd create = app.create"] {
let files = [
(
"content/item/model.dever",
"global type Item { name: Text }",
),
(
"content/item/app.dever",
"create(name: Text) (value: Text) { saved = model.create({ name = name })\nvalue = saved.name }",
),
("content/item/api.dever", entry),
];
check(&files);
let long_entry = if entry.starts_with("public ") {
entry.replace("public ", "public long ")
} else {
format!("long {entry}")
};
let mut long = files;
long[2].1 = &long_entry;
reject(
&long,
"long entry database writes must be enclosed in an explicit transaction",
);
let mut transactional = long;
let app = format!("transaction {}", files[1].1);
transactional[1].1 = &app;
check(&transactional);
}
}
#[test]
fn entry_orchestration_can_wait_between_explicit_transactions() {
let files = [
(
"content/item/model.dever",
"global type Item { name: Text }",
),
(
"content/item/app.dever",
"create() (count: Int) { first = store(\"first\")\ndever.task.sleep(1)\nsecond = store(\"second\")\ncount = first + second }\ntransaction store(name: Text) (count: Int) { saved = model.create({ name = name })\ncount = model.count() }",
),
(
"content/item/api.dever",
"public long post create = app.create\nlong cmd create = app.create",
),
];
let source_map = sources(&files);
let program = dever_core::check(&source_map).unwrap_or_else(|errors| panic!("{errors:?}"));
let native = dever_core::native::emit_project(
&program,
&source_map,
dever_runtime::config::RuntimeProfile {
sqlite: true,
postgres: false,
},
)
.unwrap();
assert!(!native.contains("let transaction = transaction_database.begin()"));
assert!(native.contains("let transaction_owner = if _database.is_none()"));
let (_root, settings) = auth_settings();
let mut configured = identity_sources().to_vec();
configured.extend([
files[0],
files[1],
("content/item/api/admin/create.dever", files[2].1),
]);
let configured_sources = sources(&configured);
let configured_program = dever_core::check_with_settings(&configured_sources, &settings)
.unwrap_or_else(|errors| panic!("{errors:?}"));
dever_core::llvm::emit_application(&configured_program, &configured_sources).unwrap();
let ordinary_entry = files[2].1.replace("long ", "");
let mut ordinary = files;
ordinary[2].1 = &ordinary_entry;
reject(&ordinary, "automatic entry transactions cannot contain");
let mut invalid = files;
let app = files[1].1.replace("create()", "transaction create()");
invalid[1].1 = &app;
reject(
&invalid,
"transaction functions cannot start concurrent work",
);
}
#[test]
fn job_entries_require_long_before_using_staged_transactions() {
let model = "global type Item { name: Text }";
let ordinary = "apply() () { saved = model.create({ name = \"job\" }) }";
let transactional = "transaction apply() () { saved = model.create({ name = \"job\" }) }";
for long in [false, true] {
let prefix = if long { "long " } else { "" };
let job = format!(
"database default\n{prefix}job apply() () retry(3) timeout(30000) {{ app.apply() }}"
);
let mut files = [
("content/item/model.dever", model),
("content/item/app.dever", ordinary),
("content/item/job.dever", job.as_str()),
];
if long {
reject(&files, "long entry database writes must be enclosed");
files[1].1 = transactional;
}
check(&files);
}
let files = [
("content/item/model.dever", model),
(
"content/item/app.dever",
"apply() () { save()\ndever.task.sleep(1) }\ntransaction save() () { saved = model.create({ name = \"job\" }) }",
),
(
"content/item/job.dever",
"database default\njob apply() () retry(3) timeout(30000) { app.apply() }",
),
];
reject(&files, "declare the entry 'long'");
let mut long = files;
long[2].1 = "database default\nlong job apply() () retry(3) timeout(30000) { app.apply() }";
check(&long);
}
#[test]
fn long_is_an_entry_modifier_not_an_app_function_modifier() {
for (path, text) in [
("content/item/api.dever", "long rest"),
(
"content/item/api.dever",
"long public post create = app.create",
),
("content/item/app.dever", "long create() () {}"),
("content/item/app.dever", "long transaction create() () {}"),
(
"content/item/api.dever",
"public long cmd create = app.create",
),
] {
let mut source = SourceMap::default();
let id = source.add(path, text);
assert!(dever_core::parse(source.get(id)).is_err());
}
check(&[(
"content/item/app.dever",
"long() (value: Int) { value = 1 }",
)]);
}
fn auth_settings() -> (temp::TemporaryDirectory, Settings) {
let root = temp::TemporaryDirectory::new();
fs::create_dir(root.path().join("config")).unwrap();
@@ -698,7 +847,7 @@ fn sql_projection_files(sqlite: &str, postgres: &str, method: &str) -> [(&'stati
(
"news/article/app.dever",
"type View { name: Text }\n\
read(name: Text, size: Int) (rows: List<View>) {\n\
transaction read(name: Text, size: Int) (rows: List<View>) {\n\
rows = model.lookup(name, size)\n}\n"
.into(),
),
@@ -767,7 +916,7 @@ fn unproven_sql_keeps_its_write_effect_in_each_dialect() {
#[test]
fn get_rejects_writes_reached_through_cross_domain_app_calls() {
let writer = "create(name: Text) (value: Text) { account = model.create({ name = text.trim(name) })\nvalue = account.name }";
let writer = "transaction create(name: Text) (value: Text) { account = model.create({ name = text.trim(name) })\nvalue = account.name }";
reject(
&[
(
+11 -45
View File
@@ -37,54 +37,20 @@ fn project(setting: &str) -> support::temp::TemporaryDirectory {
root
}
#[cfg(feature = "external")]
#[test]
fn worker_file_grants_are_explicit_and_cannot_write_executable_cache() {
let setting = |path: &str, write: bool| {
serde_json::json!({
"adapter": {"sample.worker": {"files": {"content": {"path": path, "write": write}}}}
})
.to_string()
};
let capabilities = vec!["file".into()];
let root = project(&setting("data/upload", true));
let settings = Settings::load_project(root.path()).unwrap();
let grants = settings
.external_grants("sample.worker", &capabilities)
.unwrap();
assert_eq!(grants.len(), 1);
assert_eq!(grants[0].source, root.path().join("data/upload"));
assert_eq!(grants[0].destination, std::path::Path::new("/data/content"));
assert!(grants[0].writable);
assert!(
settings
.external_grants("sample.worker", &[])
.unwrap_err()
.contains("file capability")
);
for path in [
"data",
"data/cache",
"data/cache/lib",
"data/cache/lib/worker",
] {
let root = project(&setting(path, true));
let settings = Settings::load_project(root.path()).unwrap();
assert!(
settings
.external_grants("sample.worker", &capabilities)
.unwrap_err()
.contains("executable resource cache")
fn obsolete_worker_mount_and_gpu_grants_are_rejected() {
for field in ["files", "gpu"] {
let root = project(
&serde_json::json!({
"adapter": {"sample.worker": {field: {}}}
})
.to_string(),
);
}
for path in ["../private", "/etc", ""] {
let root = project(&setting(path, false));
let settings = Settings::load_project(root.path()).unwrap();
assert!(
settings
.external_grants("sample.worker", &capabilities)
.unwrap_err()
.contains("relative to the application")
Settings::load_project(root.path())
.unwrap()
.select_adapter("sample.worker", &["default"])
.is_err()
);
}
}
+21 -6
View File
@@ -1060,8 +1060,13 @@ deliver(input: Work) () {
);
assert!(
std::process::Command::new("kill")
.args(["-TERM", &worker.0.id().to_string()])
std::process::Command::new("/bin/sh")
.args([
"-c",
"kill -TERM \"$1\"",
"dever-test-signal",
&worker.0.id().to_string()
])
.status()
.unwrap()
.success()
@@ -1202,8 +1207,13 @@ fn one_native_application_gates_api_worker_and_all_modes() {
panic!("{mode} did not reach its service state: {stderr}");
}
assert!(
std::process::Command::new("kill")
.args(["-TERM", &child.0.id().to_string()])
std::process::Command::new("/bin/sh")
.args([
"-c",
"kill -TERM \"$1\"",
"dever-test-signal",
&child.0.id().to_string()
])
.status()
.unwrap()
.success()
@@ -1269,8 +1279,13 @@ fn worker_signal_stops_claiming_and_drains_or_cancels_before_database_close() {
);
}
assert!(
std::process::Command::new("kill")
.args(["-TERM", &child.id().to_string()])
std::process::Command::new("/bin/sh")
.args([
"-c",
"kill -TERM \"$1\"",
"dever-test-signal",
&child.id().to_string()
])
.status()
.unwrap()
.success()
+34 -36
View File
@@ -9,8 +9,8 @@ use dever_runtime::wire::Encoder;
use serde_json::json;
use sha2::{Digest, Sha256};
#[path = "support/sandbox.rs"]
mod sandbox;
#[path = "support/process_runtime.rs"]
mod process_runtime;
#[path = "support/temp.rs"]
mod temp;
@@ -35,7 +35,6 @@ impl Fixture {
fs::create_dir(directory.path().join("markers")).unwrap();
fs::create_dir(directory.path().join("config")).unwrap();
let settings = json!({"adapter": {PORT: {
"files": {"markers": {"path":"markers", "write":true}},
"command": {"timeout_ms":timeout_ms, "output_limit":output_limit}
}}});
fs::write(
@@ -44,13 +43,13 @@ impl Fixture {
)
.unwrap();
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let mut assets = sandbox::assets(&workspace);
assets.push(sandbox::Asset {
let mut assets = process_runtime::assets(&workspace);
assets.push(process_runtime::Asset {
path: "commands/fixture/tool".into(),
bytes: program,
executable: true,
});
assets.push(sandbox::Asset {
assets.push(process_runtime::Asset {
path: "tool.dever-command.json".into(),
bytes: json!({"format":"dever-command-launch-v1", "ecosystem":"command", "entry":"tool",
"executable":"commands/fixture/tool", "arguments":[], "working_directory":"commands/fixture"})
@@ -104,6 +103,12 @@ struct Output {
stderr: Vec<u8>,
}
async fn tree(directory: std::path::PathBuf) -> Result<(), String> {
invoke(&["tree", directory.to_str().unwrap()], None)
.await
.map(|_| ())
}
async fn invoke(args: &[&str], stdin: Option<&[u8]>) -> Result<Output, String> {
let mut writer = Encoder::default();
writer.begin_object()?;
@@ -255,18 +260,12 @@ async fn heartbeat(fixture: &Fixture) -> Result<(), String> {
}
#[test]
fn guard_exec_failure_is_a_fault_not_a_program_exit_code() {
fn invalid_executable_is_rejected_before_a_program_exit_code_exists() {
let fixture = Fixture::with_program(5000, 1024 * 1024, b"not an ELF executable\n".to_vec());
dever_runtime::task::run_entry(async {
fixture.start().await?;
let error = invoke(&[], None)
.await
.err()
.ok_or("exec failure was returned as an exit code")?;
assert!(
error.contains("startup failed") && error.contains("cannot execute"),
"{error}"
);
let error = fixture.start().await.unwrap_err();
assert!(error.contains("invalid executable ELF"), "{error}");
assert!(!fixture.marker("starts").exists());
component::shutdown().await
})
.unwrap();
@@ -294,15 +293,26 @@ async fn assert_reaped(fixture: &Fixture) {
);
}
#[test]
fn successful_root_exit_stops_descendants_holding_output_pipes() {
let fixture = Fixture::new(5000, 1024 * 1024);
dever_runtime::task::run_entry(async {
fixture.start().await?;
let directory = fixture.marker("");
let output = invoke(&["tree-exit", directory.to_str().unwrap()], None).await?;
assert_eq!(output.code, 0);
assert_reaped(&fixture).await;
component::shutdown().await
})
.unwrap();
}
#[test]
fn timeout_reaps_descendants_and_keeps_the_adapter_available() {
let fixture = Fixture::new(800, 1024 * 1024);
dever_runtime::task::run_entry(async {
fixture.start().await?;
let call = dever_runtime::task::run(async {
invoke(&["tree", "/data/markers"], None).await.map(|_| ())
})
.await?;
let call = dever_runtime::task::run(tree(fixture.marker(""))).await?;
heartbeat(&fixture).await?;
let error = dever_runtime::task::wait(call)
.await
@@ -321,10 +331,7 @@ fn cancellation_and_root_close_reap_descendants_without_replay() {
let fixture = Fixture::new(5000, 1024 * 1024);
dever_runtime::task::run_entry(async {
fixture.start().await?;
let call = dever_runtime::task::run(async {
invoke(&["tree", "/data/markers"], None).await.map(|_| ())
})
.await?;
let call = dever_runtime::task::run(tree(fixture.marker(""))).await?;
heartbeat(&fixture).await?;
if root_close {
// Return while the call is active: the invocation closes its
@@ -361,10 +368,7 @@ fn queued_cancellation_does_not_wait_for_an_unrelated_running_command() {
let fixture = Fixture::new(5000, 1024 * 1024);
dever_runtime::task::run_entry(async {
fixture.start().await?;
let active = dever_runtime::task::run(async {
invoke(&["tree", "/data/markers"], None).await.map(|_| ())
})
.await?;
let active = dever_runtime::task::run(tree(fixture.marker(""))).await?;
heartbeat(&fixture).await?;
let (entered, admitted) = tokio::sync::oneshot::channel();
let queued = dever_runtime::task::run(async move {
@@ -395,10 +399,7 @@ fn task_deadline_waits_for_command_process_cleanup() {
let fixture = Fixture::new(5000, 1024 * 1024);
dever_runtime::task::run_entry(async {
fixture.start().await?;
let call = dever_runtime::task::run(async {
invoke(&["tree", "/data/markers"], None).await.map(|_| ())
})
.await?;
let call = dever_runtime::task::run(tree(fixture.marker(""))).await?;
heartbeat(&fixture).await?;
assert_eq!(dever_runtime::task::wait_timeout(call, 10).await?, None);
assert_reaped(&fixture).await;
@@ -412,10 +413,7 @@ fn explicit_shutdown_drains_active_and_queued_commands() {
let fixture = Fixture::new(5000, 1024 * 1024);
dever_runtime::task::run_entry(async {
fixture.start().await?;
let active = dever_runtime::task::run(async {
invoke(&["tree", "/data/markers"], None).await.map(|_| ())
})
.await?;
let active = dever_runtime::task::run(tree(fixture.marker(""))).await?;
heartbeat(&fixture).await?;
let queued =
dever_runtime::task::run(async { invoke(&["echo", "queued"], None).await.map(|_| ()) })
+13 -11
View File
@@ -5,8 +5,8 @@ use dever_runtime::component::{self, Definition, Reply};
use dever_runtime::wire::{Encoded, Encoder};
use sha2::{Digest, Sha256};
#[path = "support/sandbox.rs"]
mod sandbox;
#[path = "support/process_runtime.rs"]
mod process_runtime;
#[path = "support/temp.rs"]
mod temp;
@@ -28,8 +28,6 @@ fn component_protocol_and_lifecycle_are_bounded() {
let fixture = install_fixture();
let result = dever_runtime::task::run_entry(async {
fixture.prepare()?;
// One invocation keeps the same deployment grants even if settings are
// replaced between Worker startup, cancellation and restart.
fs::write(
fixture.directory.path().join("config/setting.json"),
"invalid",
@@ -252,6 +250,12 @@ fn definition(
mode: &'static str,
timeout_ms: u64,
) -> Definition {
let (_, setting) = component::adapter_settings()
.unwrap()
.select_adapter("notification.delivery", &["fixture"])
.unwrap();
let mut setting: serde_json::Value = serde_json::from_str(&setting.unwrap()).unwrap();
setting["mode"] = serde_json::json!(mode);
Definition {
ecosystem: "exec".into(),
key: key.into(),
@@ -261,9 +265,7 @@ fn definition(
schema: "a19ca3e615ce5e71".into(),
capabilities: vec!["network".into(), "file".into()],
operations: OPERATIONS.iter().map(|name| (*name).into()).collect(),
setting: Some(format!(
r#"{{"mode":"{mode}","marker":"/data/lifecycle/worker"}}"#
)),
setting: Some(setting.to_string()),
timeout_ms,
}
}
@@ -295,15 +297,15 @@ fn install_fixture() -> InstalledFixture {
let directory = temp::TemporaryDirectory::new();
fs::create_dir(directory.path().join("lifecycle")).unwrap();
fs::create_dir(directory.path().join("config")).unwrap();
fs::write(directory.path().join("config/setting.json"), r#"{"adapter":{"notification.delivery":{"files":{"lifecycle":{"path":"lifecycle","write":true}}}}}"#).unwrap();
fs::write(directory.path().join("config/setting.json"), serde_json::json!({"adapter":{"notification.delivery":{"setting":{"marker":directory.path().join("lifecycle/worker")}}}}).to_string()).unwrap();
let workspace = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let mut assets = sandbox::assets(&workspace);
assets.push(sandbox::Asset {
let mut assets = process_runtime::assets(&workspace);
assets.push(process_runtime::Asset {
path: "workers/fixture/worker".into(),
bytes: fs::read(env!("CARGO_BIN_EXE_component-fixture")).unwrap(),
executable: true,
});
assets.push(sandbox::Asset {
assets.push(process_runtime::Asset {
path: "worker.dever-worker.json".into(),
bytes: br#"{"format":"dever-worker-launch-v1","ecosystem":"exec","entry":"worker","executable":"workers/fixture/worker","arguments":[],"working_directory":"workers/fixture"}"#.to_vec(), executable: false,
});
+7 -5
View File
@@ -1,5 +1,5 @@
#[path = "support/sandbox.rs"]
mod sandbox;
#[path = "support/process_runtime.rs"]
mod process_runtime;
#[path = "support/temp.rs"]
mod temp;
@@ -385,7 +385,7 @@ fn existing_cache_permissions_are_checked_before_reuse_and_launch() {
}
#[test]
#[ignore = "requires explicitly prepared sandbox assets and the private Rust bootstrap compiler"]
#[ignore = "requires explicitly prepared process_runtime assets and the private Rust bootstrap compiler"]
fn packaged_exec_runs_without_source_worker_or_inherited_environment() {
let root = TemporaryDirectory::new();
let mut sources = dever_core::source::SourceMap::default();
@@ -424,7 +424,9 @@ fn packaged_exec_runs_without_source_worker_or_inherited_environment() {
bytes: manifest,
executable: false,
});
for asset in sandbox::assets(&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")) {
for asset in
process_runtime::assets(&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."))
{
resources.push(dever_core::native::EmbeddedResource {
path: asset.path,
sha256: digest(&asset.bytes),
@@ -446,7 +448,7 @@ fn packaged_exec_runs_without_source_worker_or_inherited_environment() {
fs::create_dir(root.path().join("lifecycle")).unwrap();
fs::write(
root.path().join("config/setting.json"),
r#"{"adapter":{"notification.mail":{"use":"default","setting":{"mode":"normal","marker":"/data/lifecycle/worker"},"files":{"lifecycle":{"path":"lifecycle","write":true}}}}}"#,
serde_json::json!({"adapter":{"notification.mail":{"use":"default","setting":{"mode":"normal","marker":root.path().join("lifecycle/worker")}}}}).to_string(),
)
.unwrap();
assert!(!root.path().join(&entry).exists());
+12
View File
@@ -56,6 +56,18 @@ fn assert_roundtrip_at(path: &FilePath, source: &str) -> String {
output
}
#[test]
fn long_entry_modifiers_roundtrip_without_changing_visibility_or_job_policy() {
let api = "public long post generate=app.generate\nlong cmd generate=app.generate\npost create=app.create\n";
let formatted = assert_roundtrip_at(FilePath::new("content/item/api.dever"), api);
assert!(formatted.contains("public long post generate = app.generate"));
assert!(formatted.contains("long cmd generate = app.generate"));
assert!(formatted.contains("post create = app.create"));
let job = "long job generate()()retry(3)timeout(30000){app.generate()}";
let formatted = assert_roundtrip_at(FilePath::new("content/item/job.dever"), job);
assert!(formatted.contains("long job generate() () retry(3) timeout(30000)"));
}
#[test]
fn canonical_layout_preserves_field_evaluation_order_and_groups() {
let source = "type Pair{a:Int\nb:Int}\npublic main( )(result:Pair){result=Pair{b=second()\na=first()}}\n";
+84 -3
View File
@@ -6,6 +6,9 @@ use dever_runtime::http::{Header, Request};
use std::fs;
use std::process::Command;
#[path = "support/process.rs"]
#[allow(dead_code)]
mod process;
#[path = "support/temp.rs"]
mod temp;
@@ -17,6 +20,81 @@ fn source(files: &[(&str, &str)]) -> SourceMap {
sources
}
#[cfg(feature = "sqlite")]
#[test]
fn command_releases_short_transactions_before_waiting_and_preserves_committed_stages() {
let sources = source(&[
(
"content/item/model.dever",
"global type Item { name: Text unique }",
),
(
"content/item/app.dever",
"transaction store(name: Text) (value: Text) { saved = model.create({ name = name })\nvalue = saved.name }\ncount() (total: Int) { total = model.count() }\nperform() (total: Int) { saved = store(\"first\")\ntask = run(count())\ntotal = wait(task) }\nfail_later() (value: Text) { committed = store(\"second\")\nvalue = store(committed) }",
),
(
"content/item/api.dever",
"long cmd perform = app.perform\nlong cmd fail_later = app.fail_later\ncmd fail_atomic = app.fail_later\ncmd count = app.count",
),
]);
let program = dever_core::check(&sources).unwrap_or_else(|errors| panic!("{errors:?}"));
let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into());
let native = dever_core::native::compile_project(
&program,
&sources,
&rustc,
RuntimeProfile {
sqlite: true,
postgres: false,
},
)
.unwrap();
let root = native.executable().parent().unwrap();
fs::create_dir_all(root.join("config")).unwrap();
fs::write(
root.join("config/setting.json"),
r#"{"database":{"default":{"type":"sqlite","path":"data/db/app.db","max_connections":1}}}"#,
)
.unwrap();
let invoke = |operation: &str| {
let mut child = Command::new(native.executable())
.current_dir(root)
.args([operation, "{}"])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
process::wait(&mut child, std::time::Duration::from_secs(10)).unwrap();
child.wait_with_output().unwrap()
};
let first = invoke("content.item.perform");
assert!(
first.status.success(),
"{}",
String::from_utf8_lossy(&first.stderr)
);
let first: serde_json::Value = serde_json::from_slice(&first.stdout).unwrap();
assert_eq!(first["data"], 1);
let atomic = invoke("content.item.fail_atomic");
assert!(!atomic.status.success());
assert!(String::from_utf8_lossy(&atomic.stderr).contains("UNIQUE"));
let retained = invoke("content.item.count");
assert!(retained.status.success());
let retained: serde_json::Value = serde_json::from_slice(&retained.stdout).unwrap();
assert_eq!(retained["data"], 1);
let failed = invoke("content.item.fail_later");
assert!(!failed.status.success());
assert!(String::from_utf8_lossy(&failed.stderr).contains("UNIQUE"));
let count = invoke("content.item.count");
assert!(
count.status.success(),
"{}",
String::from_utf8_lossy(&count.stderr)
);
let count: serde_json::Value = serde_json::from_slice(&count.stdout).unwrap();
assert_eq!(count["data"], 2);
}
#[test]
fn project_entry_collects_api_and_command_without_main() {
let sources = source(&[
@@ -175,13 +253,13 @@ fn generated_write_entry_rejects_detached_concurrency() {
.map(|error| error.render(&sources))
.collect::<String>();
assert!(
rendered.contains("transaction cannot start concurrent work"),
rendered.contains("automatic entry transactions cannot contain"),
"{rendered}"
);
}
#[test]
fn generated_write_entry_allows_structured_blocking_work() {
fn generated_long_entry_keeps_password_work_outside_its_write_transaction() {
let sources = source(&[
(
"user/account/model.dever",
@@ -201,7 +279,10 @@ transaction persist(hash: Text) (id: model.id) {
id = stored.id
}"#,
),
("user/account/api.dever", "post register = app.register"),
(
"user/account/api.dever",
"long post register = app.register",
),
]);
let program = dever_core::check(&sources).unwrap_or_else(|errors| {
panic!(
+70
View File
@@ -11,6 +11,76 @@ const ADAPTER: &str = "port.read() (value: Int) { value = 7 }";
const COMMAND_PORT: &str = "execute(args: List<Text>, stdin: Bytes?) (output: dever.process.Output) fails dever.process.Error";
const COMMAND_APP: &str = "read() (value: Int) { output = port.execute([\"argument with spaces\"], null)\n value = output.code }";
#[test]
fn external_worker_orchestration_requires_an_explicit_long_entry() {
for adapter in [
"external exec \"worker/mail\" {}",
"external exec \"worker/mail\" { allow gpu }",
] {
for long in [false, true] {
let entry = if long {
"long cmd read = app.read"
} else {
"cmd read = app.read"
};
let source = sources(
PORT,
adapter,
&[
("notification/mail/model.dever", "type Mail { value: Int }"),
(
"notification/mail/app.dever",
"type ReadResult { Read(value: Int)\n error Unavailable(message: Text) }\nread() (value: Int) { value = port.read()\nstore(value) }\ntransaction store(value: Int) () { stored = model.create({ value = value }) }",
),
("notification/mail/api.dever", entry),
],
);
if long {
let program =
dever_core::check(&source).unwrap_or_else(|errors| panic!("{errors:?}"));
let native = dever_core::native::emit_project(
&program,
&source,
dever_runtime::config::RuntimeProfile {
sqlite: true,
postgres: false,
},
)
.unwrap();
assert!(!native.contains("let transaction = transaction_database.begin()"));
dever_core::llvm::emit_application(&program, &source).unwrap();
} else {
rejected(&source, "declare the entry 'long'");
}
}
}
}
#[test]
fn transactions_reject_external_workers_even_without_declared_io_capabilities() {
for adapter in [
"external exec \"worker/mail\" {}",
"external exec \"worker/mail\" { allow gpu }",
] {
let source = sources(
PORT,
adapter,
&[
("notification/mail/model.dever", "type Mail { value: Int }"),
(
"notification/mail/app.dever",
"type ReadResult { Read(value: Int)\n error Unavailable(message: Text) }\ntransaction save() (value: Int) { value = port.read()\nstored = model.create({ value = value }) }",
),
("notification/mail/api.dever", "cmd save = app.save"),
],
);
rejected(
&source,
"transaction functions cannot start concurrent work or external I/O",
);
}
}
fn command_sources(port: &str, adapter: &str, app: &str) -> SourceMap {
sources(port, adapter, &[("notification/mail/app.dever", app)])
}
+13 -11
View File
@@ -22,7 +22,7 @@ fn diagnostics(sources: &SourceMap) -> String {
// Code-generation assertions may inspect unbound HIR; an executable API must
// use the same configured site/auth contract as the CLI.
fn compile_authenticated_rest(mut sources: SourceMap) {
fn compile_authenticated_rest(mut sources: SourceMap, user_id_type: &str) {
sources.add("access/user/model.dever", "global type User { name: Text }");
sources.add(
"access/tenant/model.dever",
@@ -30,16 +30,18 @@ fn compile_authenticated_rest(mut sources: SourceMap) {
);
sources.add(
"access/session/app.dever",
r#"type Identity {
format!(
r#"type Identity {{
id: Text
user_id: access.user.model.id?
user_id: {user_id_type}?
tenant_id: access.tenant.model.id?
}
verify(claims: dever.auth.Claims) (identity: Identity) {
identity = Identity { id = claims.subject
}}
verify(claims: dever.auth.Claims) (identity: Identity) {{
identity = Identity {{ id = claims.subject
user_id = null
tenant_id = null }
}"#,
tenant_id = null }}
}}"#
),
);
let directory = TemporaryDirectory::new();
std::fs::create_dir(directory.path().join("config")).unwrap();
@@ -109,7 +111,7 @@ fn rest_derives_the_public_model_without_an_app_wrapper() {
assert!(generated.contains("transaction.commit().await"));
assert!(generated.contains("rest_rollback(transaction).await"));
assert!(generated.contains("dever_runtime::api::text_bounds(\"name\""));
compile_authenticated_rest(sources);
compile_authenticated_rest(sources, "access.user.model.id");
}
#[test]
@@ -184,7 +186,7 @@ fn rest_owner_is_server_bound_and_scopes_every_database_statement() {
] {
assert!(generated.contains(sql), "missing {sql}: {generated}");
}
compile_authenticated_rest(sources);
compile_authenticated_rest(sources, "user.account.model.id");
}
#[test]
@@ -378,7 +380,7 @@ fn rest_field_bindings_are_checked_and_emit_static_search_sql() {
generated.contains("SELECT COUNT(*) FROM \\\"profile\\\" WHERE \\\"user_id\\\" = ?1"),
"{generated}"
);
compile_authenticated_rest(sources);
compile_authenticated_rest(sources, "user.account.model.id");
}
#[test]
+2 -2
View File
@@ -67,7 +67,7 @@ transaction store(credential_id: model.id) (id: model.id) {
),
(
"user/account/api/front/account.dever",
"public post login = app.login",
"public long post login = app.login",
),
]);
let program = checked_platform(&sources);
@@ -106,7 +106,7 @@ login(credential_id: model.id, password: Secret) (id: model.id) {
),
(
"user/account/api/front/account.dever",
"public post login = app.login",
"public long post login = app.login",
),
],
"explicit transaction function",
@@ -0,0 +1,29 @@
use std::fs;
use std::path::Path;
pub struct Asset {
pub path: String,
pub bytes: Vec<u8>,
pub executable: bool,
}
pub fn assets(_workspace: &Path) -> Vec<Asset> {
let prepared = std::path::PathBuf::from(
std::env::var_os("DEVER_PROCESS_RUNTIME")
.expect("set DEVER_PROCESS_RUNTIME to explicit native runtime assets"),
);
assert!(prepared.is_absolute());
let mut assets = Vec::new();
for entry in fs::read_dir(prepared.join("lib")).unwrap() {
let entry = entry.unwrap();
assert!(entry.file_type().unwrap().is_file());
let name = entry.file_name().into_string().unwrap();
assets.push(Asset {
path: format!("process-runtime/lib/{name}"),
bytes: fs::read(entry.path()).unwrap(),
executable: name.starts_with("ld-linux"),
});
}
assets.sort_by(|left, right| left.path.cmp(&right.path));
assets
}
-40
View File
@@ -1,40 +0,0 @@
//! Explicit private fixture assets; never consult PATH or download a tool.
use std::fs;
use std::path::Path;
pub struct Asset {
pub path: String,
pub bytes: Vec<u8>,
pub executable: bool,
}
pub fn assets(workspace: &Path) -> Vec<Asset> {
let prepared = workspace.join("target/sandbox-inputs/assets");
let mut assets = Vec::new();
for directory in ["bin", "lib"] {
for entry in fs::read_dir(prepared.join(directory))
.expect("prepare target/sandbox-inputs assets explicitly")
{
let entry = entry.unwrap();
assert!(entry.file_type().unwrap().is_file());
let name = entry.file_name().into_string().unwrap();
if directory == "bin" && name == "guard" {
continue;
}
assets.push(Asset {
path: format!("sandbox/{directory}/{name}"),
bytes: fs::read(entry.path()).unwrap(),
executable: directory == "bin" || name.starts_with("ld-linux"),
});
}
}
assets.push(Asset {
path: "sandbox/bin/guard".into(),
bytes: fs::read(workspace.join("target/debug/dever-sandbox-guard"))
.expect("build the current dever-sandbox-guard fixture explicitly"),
executable: true,
});
assets.sort_by(|left, right| left.path.cmp(&right.path));
assets
}
+1 -1
View File
@@ -267,7 +267,7 @@ fn upload_route_native_compiles_with_owned_resource() {
let native = dever_core::native::compile_project(
&program,
&sources,
std::ffi::OsStr::new("/root/.cargo/bin/rustc"),
std::ffi::OsStr::new("rustc"),
dever_runtime::config::RuntimeProfile::default(),
)
.unwrap();