Files
core/test/llvm_external_source.rs
T

610 lines
22 KiB
Rust

//! Typed Worker calls and verified executable resources through the LLVM backend.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[path = "dever-tests/tests/support/llvm_module.rs"]
mod llvm_module;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[path = "dever-tests/tests/support/process.rs"]
mod process;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[path = "dever-tests/tests/support/sandbox.rs"]
mod sandbox;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[path = "dever-tests/tests/support/temp.rs"]
mod temp;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
mod native {
use super::{llvm_module, temp};
use dever_backend_bridge::{BinaryResource, Target, emit_object, emit_object_with_resources};
use dever_core::{hir::Program, native::EmbeddedResource, source::SourceMap};
use serde_json::json;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::Path;
use std::time::Duration;
const READ_APP: &str = r#"type Outcome {
Done(value: Int)
error Rejected(reason: Text)
}
read() (value: Int) { value = port.read() }
message(value: Outcome.Done(number)) (text: Text) recover("inspect Worker outcome") { text = "accepted" }
message(value: Outcome.Rejected(reason)) (text: Text) recover("inspect Worker outcome") { text = reason }
capture() (text: Text) { text = message(result(read())) }
"#;
const READ_PORT: &str = "read() (value: Int) fails app.Outcome";
const READ_API: &str = "cmd read = app.read\ncmd capture = app.capture";
const SETTING: &str = "setting {\n mode: Text\n identity: Text?\n marker: Text?\n}\n";
fn checked(ecosystem: &str, app: &str, port: &str, api: &str) -> (SourceMap, Program) {
let mut sources = SourceMap::default();
sources.add("notification/delivery/app.dever", app);
sources.add("notification/delivery/port.dever", port);
sources.add("notification/delivery/api.dever", api);
sources.add(
"notification/delivery/adapter.dever",
format!("{SETTING}external {ecosystem} \"worker\" {{ allow network\nallow file }}"),
);
let program = dever_core::check(&sources).unwrap_or_else(|errors| {
panic!(
"{}",
errors
.iter()
.map(|error| error.render(&sources))
.collect::<String>()
)
});
(sources, program)
}
fn read(ecosystem: &str) -> (SourceMap, Program) {
checked(ecosystem, READ_APP, READ_PORT, READ_API)
}
fn settings(program: &Program, mode: &str) -> String {
let contracts = program.external_worker_contracts();
let identity = contracts[0].errors.keys().next().unwrap();
json!({"adapter":{"notification.delivery":{"setting":{"mode":mode,"identity":identity,"marker":"/data/lifecycle/worker"},"files":{"lifecycle":{"path":"lifecycle","write":true}}}}})
.to_string()
}
fn driver(mut ir: String, fault: Option<(u32, Option<&str>)>) -> String {
let check = match fault {
Some((code, message)) => llvm_module::fault_check(code, message),
None => " %passed = icmp eq i32 %status, 0".into(),
};
ir.push_str(&format!("\ndefine i32 @dever_test_run() {{\nentry:\n %out = alloca i8\n %fault = alloca %dever.fault\n %status = call i32 @dever_application_entry(ptr %out, ptr %fault)\n{check}\n call void @dever_outputs_release(ptr %out)\n call void @dever_fault_release(ptr %fault)\n %exit = select i1 %passed, i32 0, i32 1\n ret i32 %exit\n}}\n"));
ir
}
fn fixture() -> Vec<u8> {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
fs::read(root.join("target/native-release-inputs/component-fixture"))
.expect("prepare the immutable component-fixture in target/native-release-inputs")
}
fn resource(path: impl Into<String>, bytes: Vec<u8>, executable: bool) -> EmbeddedResource {
EmbeddedResource {
path: path.into(),
sha256: format!("{:x}", Sha256::digest(&bytes)),
bytes,
executable,
}
}
fn resources(program: &Program) -> Vec<EmbeddedResource> {
let contract = &program.external_worker_contracts()[0];
let launch = json!({
"format":"dever-worker-launch-v1", "ecosystem":contract.ecosystem,
"entry":contract.entry, "executable":"workers/fixture/worker",
"arguments":[], "working_directory":"workers/fixture"
});
let mut resources =
super::sandbox::assets(&Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."))
.into_iter()
.map(|asset| resource(asset.path, asset.bytes, asset.executable))
.collect::<Vec<_>>();
resources.extend([
resource(
format!("{}.dever-worker.json", contract.entry),
serde_json::to_vec(&launch).unwrap(),
false,
),
resource("workers/fixture/worker", fixture(), true),
]);
resources
}
fn stage(directory: &temp::TemporaryDirectory, setting: &str) {
fs::create_dir_all(directory.path().join("config")).unwrap();
fs::create_dir_all(directory.path().join("lifecycle")).unwrap();
fs::write(directory.path().join("config/setting.json"), setting).unwrap();
}
fn run(
source: &(SourceMap, Program),
mode: &str,
command: &str,
body: &str,
fault: Option<(u32, Option<&str>)>,
) -> std::process::Output {
let (_, program) = source;
let directory = temp::TemporaryDirectory::new();
stage(&directory, &settings(program, mode));
let resources = resources(program);
let output = execute(
source,
&directory,
&resources,
&[command, body],
fault,
Duration::from_secs(60),
);
if fault.is_none() {
assert!(
directory.path().join("lifecycle/worker.shutdown").is_file(),
"Worker must complete orderly shutdown"
);
}
output
}
fn execute(
source: &(SourceMap, Program),
directory: &temp::TemporaryDirectory,
resources: &[EmbeddedResource],
arguments: &[&str],
fault: Option<(u32, Option<&str>)>,
timeout: Duration,
) -> std::process::Output {
let (sources, program) = source;
let module =
dever_core::llvm::emit_application_with_resources(program, sources, resources).unwrap();
let ir = driver(module.ir, fault);
let attachments = module
.resources
.iter()
.map(|resource| BinaryResource {
symbol: &resource.symbol,
bytes: resource.bytes,
})
.collect::<Vec<_>>();
let executable = llvm_module::link_with_resources(
directory,
&ir,
"external Worker",
64,
"managed-driver.c",
&attachments,
);
llvm_module::execute_program(&executable, "external Worker", timeout, arguments)
}
fn command_source() -> (SourceMap, Program) {
let mut sources = SourceMap::default();
sources.add(
"tools/command/adapter.dever",
"external command \"bin/tool\" {}",
);
sources.add("tools/command/port.dever", "execute(args: List<Text>, stdin: Bytes?) (output: dever.process.Output) fails dever.process.Error");
sources.add("tools/command/api.dever", "cmd read = app.read");
sources.add(
"tools/command/app.dever",
r#"type Summary {
code: Int
binary: Bool
stderr: Bool
eof: Bool
}
read() (summary: Summary) {
input = dever.bytes.from_ints([0, 255, 128, 10])
echoed = port.execute(["stdin"], input)
empty = port.execute(["stdin"], null)
failed = port.execute(["exit", "7"], null)
summary = Summary {
code = failed.code
binary = echoed.stdout == input
stderr = echoed.stderr == input
eof = dever.bytes.length(empty.stdout) == 0
}
}
"#,
);
let program = dever_core::check(&sources).unwrap_or_else(|errors| {
panic!(
"{}",
errors
.iter()
.map(|error| error.render(&sources))
.collect::<String>()
)
});
(sources, program)
}
#[test]
fn command_apps_emit_typed_bytes_objects_for_all_targets() {
let (sources, program) = command_source();
let ir = dever_core::llvm::emit_application(&program, &sources).unwrap();
for target in Target::ALL {
assert!(!emit_object(&ir, target).unwrap().is_empty());
}
}
#[test]
#[ignore = "requires fresh runtime-external archive, command-fixture and sandbox assets"]
fn command_binary_io_and_exit_status_use_real_typed_llvm_calls() {
let source = command_source();
let contract = &source.1.external_command_contracts()[0];
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let mut resources = super::sandbox::assets(&workspace)
.into_iter()
.map(|asset| resource(asset.path, asset.bytes, asset.executable))
.collect::<Vec<_>>();
resources.push(resource(
"commands/fixture/tool",
fs::read(workspace.join("target/native-release-inputs/command-fixture"))
.expect("prepare immutable command-fixture in target/native-release-inputs"),
true,
));
resources.push(resource(format!("{}.dever-command.json", contract.entry), serde_json::to_vec(&json!({
"format":"dever-command-launch-v1", "ecosystem":"command", "entry":contract.entry,
"executable":"commands/fixture/tool", "arguments":[], "working_directory":"commands/fixture"
})).unwrap(), false));
let directory = temp::TemporaryDirectory::new();
stage(&directory, "{}");
let output = execute(
&source,
&directory,
&resources,
&["tools.command.read", "{}"],
None,
// Each of 65 roots starts three isolated processes and verifies
// their resources, unlike the persistent single-call Worker probe.
Duration::from_secs(180),
);
result(
&output,
json!({"code":7, "binary":true, "stderr":true, "eof":true}),
);
}
fn result(output: &std::process::Output, expected: serde_json::Value) {
let rows: Vec<serde_json::Value> = String::from_utf8_lossy(&output.stdout)
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
assert_eq!(
rows,
vec![json!({"code":0,"message":"ok","data":expected}); 65]
);
}
#[test]
fn external_apps_emit_objects_for_all_targets_and_kernels_stay_closed() {
let (sources, program) = read("exec");
assert!(
dever_core::llvm::emit_kernel(&program, &sources, "notification.delivery.read")
.is_err()
);
let ir = dever_core::llvm::emit_application(&program, &sources).unwrap();
for target in Target::ALL {
assert!(!emit_object(&ir, target).unwrap().is_empty());
}
}
#[test]
fn large_embedded_resources_keep_ir_small_and_share_payloads() {
let (sources, program) = read("exec");
let bytes = vec![0xA5; 9 * 1024 * 1024];
let resources = [
resource("shared/large", bytes.clone(), false),
resource("shared/alias", bytes, true),
];
let module =
dever_core::llvm::emit_application_with_resources(&program, &sources, &resources)
.unwrap();
assert!(module.ir.len() < 1024 * 1024);
assert_eq!(module.resources.len(), 1);
assert_eq!(module.resources[0].bytes, resources[0].bytes);
assert_eq!(
module
.ir
.matches("= external constant [9437184 x i8]")
.count(),
1
);
assert_eq!(
module
.ir
.matches("ptr @dever_external_bytes_0, i64 9437184")
.count(),
2
);
}
#[test]
fn embedded_metadata_rejects_bad_hashes_before_linking() {
let (sources, program) = read("exec");
let mut entry = resource(
"module/notification/delivery/worker",
b"fixture".to_vec(),
true,
);
let resources = [
entry.clone(),
resource("shared/same-bytes", entry.bytes.clone(), false),
resource("shared/empty", Vec::new(), false),
];
let valid =
dever_core::llvm::emit_application_with_resources(&program, &sources, &resources)
.unwrap();
assert_eq!(
valid.resources.len(),
2,
"identical content has one initializer"
);
let attachments = valid
.resources
.iter()
.map(|resource| BinaryResource {
symbol: &resource.symbol,
bytes: resource.bytes,
})
.collect::<Vec<_>>();
for target in Target::ALL {
assert!(
!emit_object_with_resources(&valid.ir, target, &attachments)
.unwrap()
.is_empty()
);
}
entry.sha256 = "0".repeat(64);
assert!(
dever_core::llvm::emit_application_with_resources(&program, &sources, &[entry])
.is_err()
);
}
#[test]
#[ignore = "requires explicit runtime-external archive and existing fixture Worker"]
fn worker_success_and_declared_failure_survive_repeated_roots() {
let source = read("exec");
result(
&run(&source, "normal", "notification.delivery.read", "{}", None),
json!(7),
);
result(
&run(
&source,
"declared_error",
"notification.delivery.capture",
"{}",
None,
),
json!("fixture"),
);
}
#[test]
#[ignore = "requires explicit runtime-external archive and existing fixture Worker"]
fn typed_nested_values_and_multiple_outputs_round_trip() {
let source = checked(
"exec",
r#"type Outcome { error Rejected(reason: Text) }
type Payload { name: Text
tags: List<Text?>
count: Int
note: Text? }
echo(value: Payload) (answer: Payload) { answer = port.echo(value) }
"#,
"echo(value: app.Payload) (value: app.Payload) fails app.Outcome",
"cmd echo = app.echo",
);
let value = json!({"name":"你好 🌍","tags":["first",null,"last"],"count":9223372036854775807_i64,"note":null});
result(
&run(
&source,
"normal",
"notification.delivery.echo",
&json!({"value":value}).to_string(),
None,
),
value,
);
let source = checked(
"exec",
"type Outcome { error Rejected(reason: Text) }\ntype Pair { count: Int\n label: Text }\necho(input_count: Int, input_label: Text) (answer: Pair) { reply = port.echo(input_count, input_label)\n answer = Pair { count = reply.count\n label = reply.label } }",
"echo(count: Int, label: Text) (count: Int, label: Text) fails app.Outcome",
"cmd echo = app.echo",
);
result(
&run(
&source,
"normal",
"notification.delivery.echo",
r#"{"input_count":42,"input_label":"owned"}"#,
None,
),
json!({"count":42,"label":"owned"}),
);
let source = checked(
"exec",
"type Outcome { error Rejected(reason: Text) }\necho() (done: Bool) { port.echo()\n done = true }",
"echo() () fails app.Outcome",
"cmd echo = app.echo",
);
result(
&run(&source, "normal", "notification.delivery.echo", "{}", None),
json!(true),
);
}
#[test]
#[ignore = "requires explicit runtime-external archive and existing fixture Worker"]
fn malformed_output_and_business_error_payloads_are_runtime_faults() {
let source = read("exec");
for mode in [
"missing_output",
"extra_output",
"wrong_output",
"invalid_error",
"missing_error",
] {
let output = run(
&source,
mode,
"notification.delivery.capture",
"{}",
Some((3, None)),
);
assert!(
output.stdout.is_empty(),
"{mode} must not become a recoverable business error"
);
}
let source = checked(
"exec",
"type Outcome { error Rejected(reason: Text) }\nfail() (value: Int) { value = port.fail() }",
"fail() (value: Int) fails app.Outcome",
"cmd fail = app.fail",
);
let output = run(
&source,
"normal",
"notification.delivery.fail",
"{}",
Some((
3,
Some("external Adapter returned an undeclared business error"),
)),
);
assert!(output.stdout.is_empty());
let source = checked(
"exec",
"type Outcome { error Rejected(reason: Text) }\nread() (value: Int?) { value = port.read() }",
"read() (value: Int?) fails app.Outcome",
"cmd read = app.read",
);
assert!(
run(
&source,
"missing_output",
"notification.delivery.read",
"{}",
Some((3, None))
)
.stdout
.is_empty()
);
}
#[test]
#[ignore = "requires explicit runtime-external archive and existing fixture Worker"]
fn handshake_and_setting_failures_do_not_leak_a_worker() {
let source = read("exec");
for mode in ["duplicate_ready", "capability_mismatch"] {
assert!(
run(
&source,
mode,
"notification.delivery.read",
"{}",
Some((3, None))
)
.stdout
.is_empty()
);
}
let (sources, program) = source;
let ir = driver(
dever_core::llvm::emit_application(&program, &sources).unwrap(),
Some((3, Some("expected wire string"))),
);
// No worker exists: typed Setting rejection must happen before process launch.
assert!(
llvm_module::execute(
&ir,
"invalid external Setting",
64,
Duration::from_secs(15),
&["notification.delivery.read", "{}"],
Some(r#"{"adapter":{"notification.delivery":{"setting":{"mode":7}}}}"#)
)
.stdout
.is_empty()
);
}
#[test]
#[ignore = "requires explicit runtime-external archive and existing fixture Worker"]
fn cancelling_a_call_preserves_the_shared_worker() {
let source = checked(
"exec",
r#"type Outcome { error Rejected(reason: Text) }
pending() () { port.wait() }
read() (value: Int) {
task = run(pending())
dever.task.sleep(5)
stop(task)
value = port.read()
}
"#,
"wait() () fails app.Outcome\nread() (value: Int) fails app.Outcome",
"cmd read = app.read",
);
result(
&run(&source, "normal", "notification.delivery.read", "{}", None),
json!(7),
);
}
#[test]
#[ignore = "requires explicit runtime-external archive and existing fixture Worker"]
fn independent_executables_use_verified_embedded_exec_and_managed_launches() {
for ecosystem in ["exec", "pip", "npm", "go"] {
let (sources, program) = read(ecosystem);
let resources = resources(&program);
let module =
dever_core::llvm::emit_application_with_resources(&program, &sources, &resources)
.unwrap();
let ir = driver(module.ir, None);
let attachments = module
.resources
.iter()
.map(|resource| BinaryResource {
symbol: &resource.symbol,
bytes: resource.bytes,
})
.collect::<Vec<_>>();
let build = temp::TemporaryDirectory::new();
let executable = llvm_module::link_with_resources(
&build,
&ir,
ecosystem,
64,
"managed-driver.c",
&attachments,
);
let directory = temp::TemporaryDirectory::new();
let relocated = directory.path().join("program");
fs::copy(executable, &relocated).unwrap();
drop(build);
stage(&directory, &settings(&program, "embedded"));
result(
&llvm_module::execute_program(
&relocated,
ecosystem,
Duration::from_secs(120),
&["notification.delivery.read", "{}"],
),
json!(7),
);
assert!(
!directory.path().join("module").exists(),
"only embedded Worker resources may be used"
);
}
}
}