//! 从合法 Dever Port 到实际嵌入 ELF;无 fails 的声明必须在检查期拒绝。 use super::{Fixture, app, command, write}; use dever::compiler::{ build::{self, BuildTarget, Resource}, project::Project, }; use std::{fs, os::unix::fs::PermissionsExt, process::Command}; mod arguments; mod command; mod jobs; mod roots; #[test] fn external_builds_and_executes_after_source_removal() { let fixture = Fixture::new(&[]); let root = &fixture.directory.0; write( root, "module/demo/value/app.dever", app("port.native(input) + input"), ); write(root, "module/demo/value/api.dever", command()); write( root, "module/demo/value/port.dever", "native(input: Int) (value: Int)\n", ); write( root, "module/demo/value/adapter.dever", "external exec \"worker\" {}\n", ); let port = root.join("module/demo/value/port.dever"); assert!( Project::load(root) .unwrap() .check() .unwrap_err() .contains("fails") ); let source = fs::read_to_string(&port).unwrap().replace( "native(input: Int) (value: Int)", "native(input: Int) (value: Int) fails dever.api.Error", ); fs::write(&port, source).unwrap(); roots::prepare(root); jobs::prepare(root); let project = Project::load(root).unwrap(); let program = project.check().unwrap(); let contract = program.external_workers().next().unwrap(); let manifest: serde_json::Value = serde_json::from_str(&contract.contract).unwrap(); let identity = "dever-component-v1;setting=none;operation=native;input=input:wire-v1:Output:Int;;output=value:wire-v1:Output:Int;;dever.api.Error.Invalid;dever.api.Error.Unauthorized;dever.api.Error.Forbidden;dever.api.Error.NotFound;dever.api.Error.Conflict;dever.api.Error.TooManyRequests;"; let checksum = identity.bytes().fold(0xcbf29ce484222325u64, |hash, byte| { (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3) }); assert_eq!(manifest["schema"], format!("{checksum:016x}")); let worker = root.join("fixture"); let source = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("../test/rust/runtime/worker/fixture.rs"); let result = Command::new("rustup") .args(["run", "1.95.0", "rustc", "--edition=2024", "-O"]) .arg(source) .arg("-o") .arg(&worker) .output() .unwrap(); assert!( result.status.success(), "{}", String::from_utf8_lossy(&result.stderr) ); let launch = serde_json::to_vec(&serde_json::json!({"format":"dever-worker-launch-v1","ecosystem":"exec","entry":contract.entry,"executable":contract.entry,"arguments":[],"working_directory":"module/demo/value"})).unwrap(); let resources = [ (contract.entry.clone(), fs::read(&worker).unwrap(), true), ( format!("{}.dever-worker.json", contract.entry), launch, false, ), ] .into_iter() .map(|(path, bytes, executable)| Resource { path, bytes, executable, }) .collect::>(); let binary = build::build_with_resources( &program, &project.sources, "main", &root.join("app"), BuildTarget::host().unwrap(), &resources, ) .unwrap(); fs::remove_dir_all(root.join("module")).unwrap(); fs::remove_file(worker).unwrap(); fs::create_dir(root.join("data")).unwrap(); fs::set_permissions(root.join("data"), fs::Permissions::from_mode(0o755)).unwrap(); roots::commands(&binary); roots::http(&binary, root); jobs::run(&binary, root); } fn command_cases(binary: &std::path::Path, entry: &str) { for _ in 0..2 { let result = Command::new(binary) .env_clear() .args([entry, "{\"input\":8}"]) .output() .unwrap(); assert!( result.status.success(), "{}", String::from_utf8_lossy(&result.stderr) ); assert_eq!( result.stdout, b"{\"code\":0,\"message\":\"ok\",\"data\":50}\n" ); } for (input, expected) in [ (-1, "Worker output does not match checked Port"), (-2, "Worker returned an undeclared business error"), (-3, "Invalid"), ] { let output = Command::new(binary) .env_clear() .args([entry, &format!("{{\"input\":{input}}}")]) .output() .unwrap(); assert!(!output.status.success()); let error = String::from_utf8_lossy(&output.stderr); assert!( error.contains(expected), "input {input}, expected {expected}: {error}" ); if input != -3 { assert!(error.contains("adapter.dever:"), "{error}"); } } // Worker 已交付合法终态;根入口仍须等 shutdown,不能提前发布成功结果。 for input in [-4, -5] { let output = Command::new(binary) .env_clear() .args([entry, &format!("{{\"input\":{input}}}")]) .output() .unwrap(); assert!( !output.status.success(), "input {input}: shutdown failure was discarded" ); assert!( output.stdout.is_empty(), "input {input}: CMD published output before cleanup" ); let error = String::from_utf8_lossy(&output.stderr); assert!( error.contains("invalid Worker shutdown acknowledgement"), "input {input}: {error}" ); if input == -5 { assert!( error.contains("dever.api.Error.Invalid") && error.contains("cleanup:"), "business error and cleanup must both survive: {error}" ); } } }