1395 lines
48 KiB
Rust
1395 lines
48 KiB
Rust
#[path = "support/native_elf.rs"]
|
|
#[allow(
|
|
dead_code,
|
|
reason = "This test crate uses only the shared static ELF fixture"
|
|
)]
|
|
mod native_elf;
|
|
#[path = "../dever-tests/tests/support/process_runtime.rs"]
|
|
mod process_runtime;
|
|
#[path = "../dever-tests/tests/support/temp.rs"]
|
|
mod temp;
|
|
#[path = "support/wheel.rs"]
|
|
mod wheel_fixture;
|
|
|
|
use std::fs;
|
|
use std::io::{Read, Write};
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, Stdio};
|
|
use std::sync::mpsc;
|
|
use std::thread;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use dever_cli::libs::{LibSpec, LockFile, LockedArtifact, LockedLib, LockedWorker, RuntimePack};
|
|
use dever_cli::workers;
|
|
use dever_core::native::EmbeddedResource;
|
|
use dever_core::source::SourceMap;
|
|
use flate2::Compression;
|
|
use flate2::write::GzEncoder;
|
|
use serde_json::json;
|
|
use sha2::{Digest, Sha256};
|
|
use temp::TemporaryDirectory;
|
|
|
|
#[path = "external_workers/command.rs"]
|
|
mod command;
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[path = "external_workers/ffi.rs"]
|
|
mod ffi;
|
|
|
|
#[test]
|
|
fn native_ffi_workers_package_the_checked_contract_and_recursive_libraries() {
|
|
let root = TemporaryDirectory::new();
|
|
let mut sources = SourceMap::default();
|
|
sources.add(
|
|
"media/native/port.dever",
|
|
"triple(value: Int) (output: Int) fails app.NativeError",
|
|
);
|
|
sources.add(
|
|
"media/native/adapter.dever",
|
|
"external exec \"bin/worker\" {}",
|
|
);
|
|
sources.add(
|
|
"media/native/app.dever",
|
|
"type NativeError { error Unavailable }\ntriple(value: Int) (output: Int) { output = port.triple(value) }",
|
|
);
|
|
sources.add("media/native/api.dever", "cmd triple = app.triple");
|
|
let program = dever_core::check(&sources).unwrap_or_else(|errors| panic!("{errors:?}"));
|
|
let contract = &program.external_worker_contracts()[0];
|
|
let mut entry = resource(
|
|
contract.entry.clone(),
|
|
native_elf::compiler(&["libfixture.so"]),
|
|
);
|
|
entry.executable = true;
|
|
assert!(
|
|
workers::prepare(root.path(), &program, &[entry.clone()])
|
|
.unwrap_err()
|
|
.contains("libfixture.so")
|
|
);
|
|
let directory = root
|
|
.path()
|
|
.join(&contract.entry)
|
|
.parent()
|
|
.unwrap()
|
|
.join("lib");
|
|
fs::create_dir_all(&directory).unwrap();
|
|
fs::write(
|
|
directory.join("libfixture.so"),
|
|
native_elf::library("libfixture.so", &[]),
|
|
)
|
|
.unwrap();
|
|
let mut loader = resource(
|
|
format!("process-runtime/lib/{}", dever_process::loader_name()),
|
|
native_elf::static_executable(),
|
|
);
|
|
loader.executable = true;
|
|
let resources = workers::prepare(root.path(), &program, &[entry, loader]).unwrap();
|
|
let manifest = resources
|
|
.iter()
|
|
.find(|file| file.path.ends_with("/contract.json"))
|
|
.unwrap();
|
|
assert_eq!(
|
|
serde_json::from_slice::<serde_json::Value>(&manifest.bytes).unwrap(),
|
|
contract.sdk_manifest()
|
|
);
|
|
assert!(
|
|
resources
|
|
.iter()
|
|
.any(|file| file.path.ends_with("/lib/libfixture.so") && !file.executable)
|
|
);
|
|
let launch = resources
|
|
.iter()
|
|
.find(|file| file.path.ends_with(".dever-worker.json"))
|
|
.unwrap();
|
|
let launch: serde_json::Value = serde_json::from_slice(&launch.bytes).unwrap();
|
|
assert_eq!(launch["ecosystem"], "exec");
|
|
assert!(launch["executable"].as_str().unwrap().ends_with("/worker"));
|
|
assert_eq!(launch["arguments"], json!([]));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
struct OwnedProcessGroup(std::process::Child);
|
|
|
|
#[cfg(unix)]
|
|
impl OwnedProcessGroup {
|
|
fn stop(&mut self) {
|
|
if self.0.try_wait().ok().flatten().is_none()
|
|
&& let Some(pid) = rustix::process::Pid::from_raw(self.0.id() as i32)
|
|
{
|
|
let _ = rustix::process::kill_process_group(pid, rustix::process::Signal::KILL);
|
|
}
|
|
let _ = self.0.wait();
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
impl Drop for OwnedProcessGroup {
|
|
fn drop(&mut self) {
|
|
self.stop();
|
|
}
|
|
}
|
|
|
|
fn sha(bytes: &[u8]) -> String {
|
|
Sha256::digest(bytes)
|
|
.iter()
|
|
.map(|byte| format!("{byte:02x}"))
|
|
.collect()
|
|
}
|
|
|
|
fn archive(files: &[(&str, &[u8])]) -> Vec<u8> {
|
|
let encoder = GzEncoder::new(Vec::new(), Compression::none());
|
|
let mut tar = tar::Builder::new(encoder);
|
|
for (name, bytes) in files {
|
|
let mut header = tar::Header::new_gnu();
|
|
header.set_size(bytes.len() as u64);
|
|
header.set_mode(0o644);
|
|
header.set_cksum();
|
|
tar.append_data(&mut header, name, *bytes).unwrap();
|
|
}
|
|
tar.into_inner().unwrap().finish().unwrap()
|
|
}
|
|
|
|
fn wheel(metadata: &str) -> Vec<u8> {
|
|
wheel_fixture::pack(
|
|
"fixture",
|
|
"1.0.0",
|
|
&format!("{metadata}Tag: py3-none-any\n"),
|
|
&wheel_fixture::metadata("fixture", "1.0.0", &[], &["extra"]),
|
|
&[("fixture/__init__.py", b"value = 1\n".to_vec())],
|
|
)
|
|
}
|
|
|
|
fn resource(path: String, bytes: Vec<u8>) -> EmbeddedResource {
|
|
EmbeddedResource {
|
|
sha256: sha(&bytes),
|
|
path,
|
|
bytes,
|
|
executable: false,
|
|
}
|
|
}
|
|
|
|
fn prepare_managed_worker(
|
|
root: &Path,
|
|
program: &dever_core::hir::Program,
|
|
mut resources: Vec<EmbeddedResource>,
|
|
) -> Vec<EmbeddedResource> {
|
|
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
|
for asset in process_runtime::assets(&workspace) {
|
|
let mut file = resource(asset.path, asset.bytes);
|
|
file.executable = asset.executable;
|
|
resources.push(file);
|
|
}
|
|
let mut generated = workers::prepare(root, program, &resources).unwrap();
|
|
generated.extend(
|
|
resources
|
|
.into_iter()
|
|
.filter(|resource| resource.path.starts_with("process-runtime/")),
|
|
);
|
|
generated
|
|
}
|
|
|
|
fn frame(value: serde_json::Value) -> Vec<u8> {
|
|
let body = serde_json::to_vec(&value).unwrap();
|
|
let mut bytes = (body.len() as u32).to_be_bytes().to_vec();
|
|
bytes.extend(body);
|
|
bytes
|
|
}
|
|
|
|
fn response(stream: &mut impl Read) -> Option<serde_json::Value> {
|
|
let mut length = [0; 4];
|
|
stream.read_exact(&mut length).ok()?;
|
|
let mut body = vec![0; u32::from_be_bytes(length) as usize];
|
|
stream.read_exact(&mut body).ok()?;
|
|
serde_json::from_slice(&body).ok()
|
|
}
|
|
|
|
fn fixture(
|
|
root: &Path,
|
|
ecosystem: &str,
|
|
binary: &[u8],
|
|
package_metadata: Option<&str>,
|
|
) -> (dever_core::hir::Program, Vec<EmbeddedResource>) {
|
|
let entry = match ecosystem {
|
|
"npm" => "worker/main.mjs",
|
|
"pip" => "worker/main.py",
|
|
_ => panic!("unsupported fixture ecosystem"),
|
|
};
|
|
fixture_entry(root, ecosystem, binary, package_metadata, entry)
|
|
}
|
|
|
|
fn fixture_entry(
|
|
root: &Path,
|
|
ecosystem: &str,
|
|
binary: &[u8],
|
|
package_metadata: Option<&str>,
|
|
entry: &str,
|
|
) -> (dever_core::hir::Program, Vec<EmbeddedResource>) {
|
|
let (program, resources, _) =
|
|
fixture_entry_with_sources(root, ecosystem, binary, package_metadata, entry);
|
|
(program, resources)
|
|
}
|
|
|
|
fn fixture_entry_with_sources(
|
|
root: &Path,
|
|
ecosystem: &str,
|
|
binary: &[u8],
|
|
package_metadata: Option<&str>,
|
|
entry: &str,
|
|
) -> (dever_core::hir::Program, Vec<EmbeddedResource>, SourceMap) {
|
|
fixture_for_target(
|
|
root,
|
|
ecosystem,
|
|
binary,
|
|
package_metadata,
|
|
entry,
|
|
dever_cli::toolchain::BuildTarget::host().unwrap(),
|
|
)
|
|
}
|
|
|
|
fn fixture_for_target(
|
|
root: &Path,
|
|
ecosystem: &str,
|
|
binary: &[u8],
|
|
package_metadata: Option<&str>,
|
|
entry: &str,
|
|
target: dever_cli::toolchain::BuildTarget,
|
|
) -> (dever_core::hir::Program, Vec<EmbeddedResource>, SourceMap) {
|
|
let (executable, source) = match ecosystem {
|
|
"npm" if entry.ends_with(".cjs") => (
|
|
"bin/node",
|
|
"module.exports = { send: ({ value }) => ({ receipt: value + 1 }) };\n",
|
|
),
|
|
"npm" => (
|
|
"bin/node",
|
|
"export const send = ({ value }) => ({ receipt: value + 1 });\n",
|
|
),
|
|
"pip" => (
|
|
"bin/python3",
|
|
"async def send(payload, setting):\n return {'receipt': payload['value'] + 1}\n",
|
|
),
|
|
_ => panic!("unsupported fixture ecosystem"),
|
|
};
|
|
let library = if package_metadata.is_some() {
|
|
"lib \"fixture@1.0.0\""
|
|
} else {
|
|
""
|
|
};
|
|
let sources = worker_sources(ecosystem, entry, library);
|
|
let program = dever_core::check(&sources).unwrap_or_else(|errors| panic!("{errors:?}"));
|
|
let contract = program.external_worker_contracts().remove(0);
|
|
let entry_path = root.join(&contract.entry);
|
|
fs::create_dir_all(entry_path.parent().unwrap()).unwrap();
|
|
fs::write(entry_path, source).unwrap();
|
|
let target = target.platform();
|
|
let arguments = if ecosystem == "pip" {
|
|
vec!["-I", "-S", "-B"]
|
|
} else {
|
|
vec![]
|
|
};
|
|
let mut metadata = json!({"format":"dever-worker-runtime-v1","ecosystem":ecosystem,"target":target,
|
|
"executable":executable,"arguments":arguments});
|
|
if ecosystem == "pip" {
|
|
metadata["python_wheel_tags"] = json!(["py3-none-any"]);
|
|
let architecture = target.strip_prefix("linux-").unwrap();
|
|
metadata["python_extension_suffixes"] = json!([
|
|
format!(".cpython-312-{architecture}-linux-gnu.so"),
|
|
".abi3.so",
|
|
".so"
|
|
]);
|
|
let mut markers = serde_json::to_value(wheel_fixture::markers()).unwrap();
|
|
markers["platform_machine"] = json!(architecture);
|
|
metadata["python_markers"] = markers;
|
|
}
|
|
let pack = archive(&[
|
|
(
|
|
"dever-runtime.json",
|
|
&serde_json::to_vec(&metadata).unwrap(),
|
|
),
|
|
(executable, binary),
|
|
]);
|
|
let runtime = RuntimePack {
|
|
name: format!("fixture-{ecosystem}"),
|
|
version: "1".into(),
|
|
sha256: sha(&pack),
|
|
};
|
|
let mut archives = Vec::new();
|
|
let mut libraries = Vec::new();
|
|
if let Some(package_metadata) = package_metadata {
|
|
let spec: LibSpec = format!("{ecosystem}:fixture@1.0.0").parse().unwrap();
|
|
let (bytes, extension) = match ecosystem {
|
|
"npm" => (
|
|
archive(&[
|
|
("package/package.json", package_metadata.as_bytes()),
|
|
("package/index.js", b"export const value = 1;"),
|
|
]),
|
|
"tgz",
|
|
),
|
|
"pip" => (wheel(package_metadata), "whl"),
|
|
_ => panic!("unsupported fixture package"),
|
|
};
|
|
let path = format!(
|
|
"lib/{ecosystem}/{}/1.0.0/archive.{extension}",
|
|
sha(b"fixture")
|
|
);
|
|
libraries.push(LockedLib {
|
|
build: None,
|
|
spec: spec.clone(),
|
|
dependencies: vec![],
|
|
runtime: runtime.clone(),
|
|
artifacts: vec![LockedArtifact {
|
|
source: None,
|
|
target: target.into(),
|
|
path: path.clone(),
|
|
bytes: bytes.len() as u64,
|
|
sha256: sha(&bytes),
|
|
}],
|
|
schema: contract.schema.clone(),
|
|
});
|
|
archives.push(resource(path, bytes));
|
|
}
|
|
let mut lock = LockFile::new(libraries).unwrap();
|
|
if ecosystem == "npm" && package_metadata.is_some() {
|
|
lock.npm.push(dever_cli::libs::npm::Environment::single(
|
|
lock.libs[0].spec.clone(),
|
|
));
|
|
}
|
|
lock.workers.push(LockedWorker {
|
|
port: contract.port,
|
|
adapter: contract.adapter,
|
|
ecosystem: contract.ecosystem,
|
|
runtime: Some(runtime),
|
|
entry: contract.entry,
|
|
schema: contract.schema,
|
|
capabilities: contract.capabilities,
|
|
operations: contract.operations,
|
|
libs: contract
|
|
.libs
|
|
.iter()
|
|
.map(|value| value.parse().unwrap())
|
|
.collect(),
|
|
});
|
|
lock.write_atomic(root).unwrap();
|
|
archives.push(resource(
|
|
format!("lib/runtime/{ecosystem}/{target}/runtime.pack"),
|
|
pack,
|
|
));
|
|
(program, archives, sources)
|
|
}
|
|
|
|
fn worker_sources(ecosystem: &str, entry: &str, libraries: &str) -> SourceMap {
|
|
let mut sources = SourceMap::default();
|
|
sources.add(
|
|
"mail/send/port.dever",
|
|
"send(value: Int) (receipt: Int) fails app.SendResult",
|
|
);
|
|
sources.add("mail/send/app.dever", "type SendResult { error Unavailable(message: Text) }\nsend(value: Int) (receipt: Int) { receipt = port.send(value) }");
|
|
sources.add("mail/send/api.dever", "cmd send = app.send");
|
|
sources.add(
|
|
"mail/send/adapter.dever",
|
|
format!("external {ecosystem} \"{entry}\" {{ {libraries} }}"),
|
|
);
|
|
sources
|
|
}
|
|
|
|
#[test]
|
|
fn packages_checked_javascript_worker_without_host_resolution() {
|
|
let temp = TemporaryDirectory::new();
|
|
let (program, resources) = fixture(temp.path(), "npm", &native_elf::static_executable(), None);
|
|
let generated = workers::prepare(temp.path(), &program, &resources).unwrap();
|
|
let entry = &program.external_worker_contracts()[0].entry;
|
|
let launch = generated
|
|
.iter()
|
|
.find(|resource| resource.path == format!("{entry}.dever-worker.json"))
|
|
.unwrap();
|
|
let manifest: serde_json::Value = serde_json::from_slice(&launch.bytes).unwrap();
|
|
let executable = manifest["executable"].as_str().unwrap();
|
|
let runner = manifest["arguments"].as_array().unwrap().last().unwrap()["resource"]
|
|
.as_str()
|
|
.unwrap();
|
|
assert!(
|
|
generated
|
|
.iter()
|
|
.any(|resource| resource.path == executable && resource.executable)
|
|
);
|
|
assert!(generated.iter().any(|resource| resource.path == runner));
|
|
assert_eq!(manifest["entry"], entry.as_str());
|
|
assert_eq!(manifest["ecosystem"], "npm");
|
|
assert!(
|
|
generated
|
|
.iter()
|
|
.any(|resource| resource.path.ends_with("/contract.json"))
|
|
);
|
|
|
|
assert!(
|
|
workers::prepare(temp.path(), &program, &[])
|
|
.unwrap_err()
|
|
.contains("runtime pack is missing")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cross_target_interpreters_and_dependency_archives_remain_offline_and_target_bound() {
|
|
use dever_cli::toolchain::BuildTarget;
|
|
let target = BuildTarget::LinuxAarch64;
|
|
let mut arm = native_elf::static_executable();
|
|
arm[18..20].copy_from_slice(&183u16.to_le_bytes());
|
|
for (ecosystem, entry, metadata) in [
|
|
(
|
|
"pip",
|
|
"worker/main.py",
|
|
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\n",
|
|
),
|
|
(
|
|
"npm",
|
|
"worker/main.mjs",
|
|
r#"{"name":"fixture","version":"1.0.0"}"#,
|
|
),
|
|
] {
|
|
let temp = TemporaryDirectory::new();
|
|
let (program, resources, _) =
|
|
fixture_for_target(temp.path(), ecosystem, &arm, Some(metadata), entry, target);
|
|
let generated =
|
|
workers::prepare_for_target(temp.path(), &program, &resources, target, &[]).unwrap();
|
|
let binary = generated
|
|
.iter()
|
|
.find(|file| file.executable && file.path.contains("/runtime/bin/"))
|
|
.unwrap();
|
|
assert_eq!(
|
|
goblin::elf::Elf::parse(&binary.bytes)
|
|
.unwrap()
|
|
.header
|
|
.e_machine,
|
|
183
|
|
);
|
|
assert!(
|
|
generated
|
|
.iter()
|
|
.any(|file| file.path.contains(if ecosystem == "pip" {
|
|
"site-packages/fixture/__init__.py"
|
|
} else {
|
|
"node_modules/fixture/index.js"
|
|
}))
|
|
);
|
|
assert!(
|
|
workers::prepare_for_target(
|
|
temp.path(),
|
|
&program,
|
|
&resources,
|
|
BuildTarget::LinuxX86_64,
|
|
&[]
|
|
)
|
|
.unwrap_err()
|
|
.contains("runtime pack is missing")
|
|
);
|
|
|
|
let mut lock =
|
|
LockFile::decode(&fs::read(temp.path().join("dever.lock")).unwrap()).unwrap();
|
|
lock.libs[0].artifacts[0].target = "linux-x86_64".into();
|
|
lock.write_atomic(temp.path()).unwrap();
|
|
assert!(
|
|
workers::prepare_for_target(temp.path(), &program, &resources, target, &[])
|
|
.unwrap_err()
|
|
.contains("archive for linux-aarch64 is missing")
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn cross_target_runtime_rejects_host_binary_even_with_matching_pack_hash() {
|
|
use dever_cli::toolchain::BuildTarget;
|
|
let mut host = native_elf::static_executable();
|
|
host[18..20].copy_from_slice(&62u16.to_le_bytes());
|
|
for (ecosystem, entry) in [("pip", "worker/main.py"), ("npm", "worker/main.mjs")] {
|
|
let temp = TemporaryDirectory::new();
|
|
let (program, resources, _) = fixture_for_target(
|
|
temp.path(),
|
|
ecosystem,
|
|
&host,
|
|
None,
|
|
entry,
|
|
BuildTarget::LinuxAarch64,
|
|
);
|
|
assert!(
|
|
workers::prepare_for_target(
|
|
temp.path(),
|
|
&program,
|
|
&resources,
|
|
BuildTarget::LinuxAarch64,
|
|
&[]
|
|
)
|
|
.unwrap_err()
|
|
.contains("differs from target linux-aarch64")
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
#[ignore = "requires explicit Go runtime pack, official UUID proof/archive and process_runtime assets"]
|
|
fn compiles_go_worker_with_locked_tools_and_runs_without_source_or_go() {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
let temp = TemporaryDirectory::new();
|
|
let mut sources = SourceMap::default();
|
|
sources.add(
|
|
"mail/send/port.dever",
|
|
"send(value: Int) (receipt: Int) fails app.SendResult",
|
|
);
|
|
sources.add("mail/send/app.dever", "type SendResult { error Unavailable(message: Text) }\nsend(value: Int) (receipt: Int) { receipt = port.send(value) }");
|
|
sources.add("mail/send/api.dever", "cmd send = app.send");
|
|
sources.add(
|
|
"mail/send/adapter.dever",
|
|
"external go \"worker/main.go\" { lib \"github.com/google/uuid@1.6.0\" }",
|
|
);
|
|
let program = dever_core::check(&sources).unwrap();
|
|
let contract = program.external_worker_contracts().remove(0);
|
|
let entry = temp.path().join(&contract.entry);
|
|
fs::create_dir_all(entry.parent().unwrap()).unwrap();
|
|
fs::write(&entry, r#"package main
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
_ "embed"
|
|
"github.com/google/uuid"
|
|
)
|
|
//go:embed note.txt
|
|
var note string
|
|
func Send(_ context.Context, payload any, _ any) (any, error) {
|
|
value := payload.(map[string]any)["value"].(json.Number)
|
|
number, err := value.Int64()
|
|
if err != nil { return nil, err }
|
|
parsed, err := uuid.Parse("00000000-0000-0000-0000-000000000001")
|
|
if err != nil { return nil, err }
|
|
return map[string]any{"receipt": number + int64(parsed[15]) + TargetBonus() + int64(len(note)) - 1}, nil
|
|
}
|
|
"#).unwrap();
|
|
fs::write(entry.with_file_name("note.txt"), "x").unwrap();
|
|
fs::write(
|
|
entry.with_file_name("target_linux.go"),
|
|
"//go:build linux\npackage main\nfunc TargetBonus() int64 { return 0 }\n",
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
entry.with_file_name("target_windows.go"),
|
|
"//go:build windows\npackage main\nfunc TargetBonus() int64 { return 100 }\n",
|
|
)
|
|
.unwrap();
|
|
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
|
let prepared = workspace.join("target/go-managed");
|
|
let pack = fs::read(prepared.join("runtime.pack")).unwrap();
|
|
let target = dever_cli::toolchain::platform_identity();
|
|
let mut lock =
|
|
LockFile::decode(&fs::read(prepared.join("dependency/dever.lock")).unwrap()).unwrap();
|
|
assert_eq!(lock.libs.len(), 1);
|
|
let dependency = &mut lock.libs[0];
|
|
assert_eq!(dependency.spec.key(), "go:github.com/google/uuid@1.6.0");
|
|
assert_eq!(dependency.runtime.sha256, sha(&pack));
|
|
dependency.schema = contract.schema.clone();
|
|
let spec = dependency.spec.clone();
|
|
let runtime = dependency.runtime.clone();
|
|
let mut resources = vec![resource(
|
|
format!("lib/runtime/go/{target}/runtime.pack"),
|
|
pack,
|
|
)];
|
|
for artifact in &dependency.artifacts {
|
|
let bytes = fs::read(prepared.join("dependency/artifacts").join(&artifact.sha256)).unwrap();
|
|
assert_eq!(sha(&bytes), artifact.sha256);
|
|
resources.push(resource(artifact.path.clone(), bytes));
|
|
}
|
|
for asset in process_runtime::assets(&workspace) {
|
|
let mut file = resource(asset.path, asset.bytes);
|
|
file.executable = asset.executable;
|
|
resources.push(file);
|
|
}
|
|
lock.workers.push(LockedWorker {
|
|
port: contract.port.clone(),
|
|
adapter: contract.adapter.clone(),
|
|
ecosystem: "go".into(),
|
|
runtime: Some(runtime),
|
|
entry: contract.entry.clone(),
|
|
schema: contract.schema.clone(),
|
|
capabilities: contract.capabilities.clone(),
|
|
operations: contract.operations.clone(),
|
|
libs: vec![spec],
|
|
});
|
|
lock.write_atomic(temp.path()).unwrap();
|
|
let generated = workers::prepare(temp.path(), &program, &resources).unwrap();
|
|
assert!(
|
|
!generated
|
|
.iter()
|
|
.any(|resource| resource.path.contains("/source/")
|
|
|| resource.path.contains("/runtime/"))
|
|
);
|
|
let binary = generated
|
|
.iter()
|
|
.find(|resource| resource.path.ends_with("/worker") && resource.executable)
|
|
.unwrap();
|
|
let rebuilt = workers::prepare(temp.path(), &program, &resources).unwrap();
|
|
let identities = |files: &[EmbeddedResource]| {
|
|
files
|
|
.iter()
|
|
.map(|file| (file.path.clone(), file.sha256.clone(), file.executable))
|
|
.collect::<Vec<_>>()
|
|
};
|
|
assert_eq!(
|
|
identities(&generated),
|
|
identities(&rebuilt),
|
|
"fresh Go build directories must produce identical Worker resources"
|
|
);
|
|
let working_directory = temp
|
|
.path()
|
|
.join(binary.path.strip_suffix("/worker").unwrap());
|
|
for file in generated.iter().chain(
|
|
resources
|
|
.iter()
|
|
.filter(|file| file.path.starts_with("process-runtime/")),
|
|
) {
|
|
let path = temp.path().join(&file.path);
|
|
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
fs::write(&path, &file.bytes).unwrap();
|
|
if file.executable {
|
|
fs::set_permissions(path, fs::Permissions::from_mode(0o700)).unwrap();
|
|
}
|
|
}
|
|
fs::remove_dir_all(entry.parent().unwrap()).unwrap();
|
|
let child = dever_process::Launch {
|
|
assets: &temp.path().join("process-runtime"),
|
|
worker: &working_directory,
|
|
executable: &temp.path().join(&binary.path),
|
|
arguments: &[],
|
|
working_directory: &working_directory,
|
|
}
|
|
.command()
|
|
.unwrap()
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
assert_fixture_protocol(child, &contract);
|
|
}
|
|
|
|
#[test]
|
|
fn packages_zero_lib_python_worker_with_checked_manifest() {
|
|
let temp = TemporaryDirectory::new();
|
|
let (program, resources) = fixture(temp.path(), "pip", &native_elf::static_executable(), None);
|
|
let generated = workers::prepare(temp.path(), &program, &resources).unwrap();
|
|
let contract = program.external_worker_contracts().remove(0);
|
|
let launch = generated
|
|
.iter()
|
|
.find(|resource| resource.path == format!("{}.dever-worker.json", contract.entry))
|
|
.unwrap();
|
|
let manifest: serde_json::Value = serde_json::from_slice(&launch.bytes).unwrap();
|
|
assert_eq!(manifest["ecosystem"], "pip");
|
|
assert!(
|
|
manifest["executable"]
|
|
.as_str()
|
|
.unwrap()
|
|
.ends_with("/runtime/bin/python3")
|
|
);
|
|
assert!(
|
|
generated
|
|
.iter()
|
|
.any(|resource| resource.path.ends_with("/dever_component.py"))
|
|
);
|
|
assert!(
|
|
generated
|
|
.iter()
|
|
.any(|resource| resource.path.ends_with("/runner.py"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn installs_locked_npm_package_and_rejects_lifecycle_hooks() {
|
|
let pure = TemporaryDirectory::new();
|
|
let (program, resources) = fixture(
|
|
pure.path(),
|
|
"npm",
|
|
&native_elf::static_executable(),
|
|
Some(r#"{"name":"fixture","version":"1.0.0","type":"module"}"#),
|
|
);
|
|
let generated = workers::prepare(pure.path(), &program, &resources).unwrap();
|
|
assert!(generated.iter().any(|resource| {
|
|
resource
|
|
.path
|
|
.ends_with("/node_modules/fixture/package.json")
|
|
}));
|
|
assert!(
|
|
generated
|
|
.iter()
|
|
.any(|resource| resource.path.ends_with("/node_modules/fixture/index.js"))
|
|
);
|
|
|
|
let hooked = TemporaryDirectory::new();
|
|
let (program, resources) = fixture(
|
|
hooked.path(),
|
|
"npm",
|
|
&native_elf::static_executable(),
|
|
Some(r#"{"name":"fixture","version":"1.0.0","scripts":{"install":"node install.js"}}"#),
|
|
);
|
|
assert!(
|
|
workers::prepare(hooked.path(), &program, &resources)
|
|
.unwrap_err()
|
|
.contains("lifecycle scripts")
|
|
);
|
|
|
|
let collision = TemporaryDirectory::new();
|
|
let (program, mut resources) = fixture(
|
|
collision.path(),
|
|
"npm",
|
|
&native_elf::static_executable(),
|
|
Some(r#"{"name":"fixture","version":"1.0.0"}"#),
|
|
);
|
|
let archive_resource = resources
|
|
.iter_mut()
|
|
.find(|resource| resource.path.ends_with("archive.tgz"))
|
|
.unwrap();
|
|
*archive_resource = resource(
|
|
archive_resource.path.clone(),
|
|
archive(&[
|
|
(
|
|
"package/package.json",
|
|
br#"{"name":"fixture","version":"1.0.0"}"#,
|
|
),
|
|
("package/overlap", b"file"),
|
|
("package/overlap/child.js", b"file"),
|
|
]),
|
|
);
|
|
let mut lock =
|
|
LockFile::decode(&fs::read(collision.path().join("dever.lock")).unwrap()).unwrap();
|
|
lock.libs[0].artifacts[0].sha256 = archive_resource.sha256.clone();
|
|
lock.libs[0].artifacts[0].bytes = archive_resource.bytes.len() as u64;
|
|
lock.write_atomic(collision.path()).unwrap();
|
|
assert!(
|
|
workers::prepare(collision.path(), &program, &resources)
|
|
.unwrap_err()
|
|
.contains("file/directory conflict")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn installs_purelib_and_platlib_python_wheels() {
|
|
let pure = TemporaryDirectory::new();
|
|
let (program, resources) = fixture(
|
|
pure.path(),
|
|
"pip",
|
|
&native_elf::static_executable(),
|
|
Some("Wheel-Version: 1.0\nRoot-Is-Purelib: true\n"),
|
|
);
|
|
let generated = workers::prepare(pure.path(), &program, &resources).unwrap();
|
|
assert!(generated.iter().any(|resource| {
|
|
resource
|
|
.path
|
|
.ends_with("/site-packages/fixture/__init__.py")
|
|
}));
|
|
|
|
let platform = TemporaryDirectory::new();
|
|
let (program, resources) = fixture(
|
|
platform.path(),
|
|
"pip",
|
|
&native_elf::static_executable(),
|
|
Some("Wheel-Version: 1.0\nRoot-Is-Purelib: false\n"),
|
|
);
|
|
assert!(
|
|
workers::prepare(platform.path(), &program, &resources)
|
|
.unwrap()
|
|
.iter()
|
|
.any(|resource| resource
|
|
.path
|
|
.ends_with("/site-packages/fixture/__init__.py"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires the explicit official native-wheel and complete Python runtime fixture at target/python-managed"]
|
|
fn runs_official_native_python_wheel_as_managed_worker() {
|
|
run_native_python_fixture("python-managed");
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires explicitly prepared official PEP517 source build at target/source-build"]
|
|
fn runs_official_python_source_build_as_managed_worker() {
|
|
run_native_python_fixture("source-build");
|
|
}
|
|
|
|
fn run_native_python_fixture(fixture: &str) {
|
|
let temp = TemporaryDirectory::new();
|
|
let sources = worker_sources(
|
|
"pip",
|
|
"worker/main.py",
|
|
"lib \"simplejson@3.20.1\"\nlib \"prefixfixture@1.0.0\"",
|
|
);
|
|
let program = dever_core::check(&sources).unwrap();
|
|
let contract = program.external_worker_contracts().remove(0);
|
|
let entry = temp.path().join(&contract.entry);
|
|
fs::create_dir_all(entry.parent().unwrap()).unwrap();
|
|
fs::write(&entry, "import simplejson\nimport prefixfixture\nfrom simplejson import _speedups\nassert _speedups.__file__.endswith('.so')\nassert prefixfixture.VALUE == 'prefix-ok'\nasync def send(payload, setting):\n value = simplejson.loads(simplejson.dumps(payload))\n return {'receipt': value['value'] + 1}\n").unwrap();
|
|
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
|
let prepared = workspace.join("target").join(fixture);
|
|
let config: serde_json::Value =
|
|
serde_json::from_slice(&fs::read(prepared.join("config/setting.json")).unwrap()).unwrap();
|
|
let runtime_file = config
|
|
.get("runtime_file")
|
|
.and_then(serde_json::Value::as_str)
|
|
.unwrap_or("runtime.pack");
|
|
let pack = fs::read(prepared.join(runtime_file)).unwrap();
|
|
let mut lock =
|
|
LockFile::decode(&fs::read(prepared.join("dependency/dever.lock")).unwrap()).unwrap();
|
|
assert_eq!(lock.libs[0].spec.key(), "pip:simplejson@3.20.1");
|
|
assert_eq!(lock.libs[0].runtime.sha256, sha(&pack));
|
|
let runtime = lock.libs[0].runtime.clone();
|
|
let mut resources = vec![resource(
|
|
format!(
|
|
"lib/runtime/pip/{}/runtime.pack",
|
|
dever_cli::toolchain::platform_identity()
|
|
),
|
|
pack,
|
|
)];
|
|
for library in &mut lock.libs {
|
|
library.schema = contract.schema.clone();
|
|
for artifact in &library.artifacts {
|
|
let bytes =
|
|
fs::read(prepared.join("dependency/artifacts").join(&artifact.sha256)).unwrap();
|
|
assert_eq!(sha(&bytes), artifact.sha256);
|
|
resources.push(resource(artifact.path.clone(), bytes));
|
|
}
|
|
}
|
|
let probe = wheel_fixture::pack("prefixfixture", "1.0.0",
|
|
"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
|
|
&wheel_fixture::metadata("prefixfixture", "1.0.0", &[], &[]), &[
|
|
("prefixfixture/__init__.py", b"import sys, sysconfig\nfrom pathlib import Path\nassert sysconfig.get_path('data') == sys.prefix\nVALUE = (Path(sys.prefix) / 'share/prefixfixture/message.txt').read_text()\n".to_vec()),
|
|
("prefixfixture-1.0.0.data/data/share/prefixfixture/message.txt", b"prefix-ok".to_vec()),
|
|
("prefixfixture-1.0.0.data/scripts/prefix-probe", b"#!python\nimport prefixfixture\nprint(prefixfixture.VALUE)\n".to_vec()),
|
|
]);
|
|
let probe_path = format!("lib/pip/{}/1.0.0/archive.whl", sha(b"prefixfixture"));
|
|
lock.libs.push(LockedLib {
|
|
build: None,
|
|
spec: "pip:prefixfixture@1.0.0".parse().unwrap(),
|
|
dependencies: vec![],
|
|
runtime: runtime.clone(),
|
|
artifacts: vec![LockedArtifact {
|
|
source: None,
|
|
target: dever_cli::toolchain::platform_identity(),
|
|
path: probe_path.clone(),
|
|
bytes: probe.len() as u64,
|
|
sha256: sha(&probe),
|
|
}],
|
|
schema: contract.schema.clone(),
|
|
});
|
|
resources.push(resource(probe_path, probe));
|
|
lock.workers.push(LockedWorker {
|
|
port: contract.port.clone(),
|
|
adapter: contract.adapter.clone(),
|
|
ecosystem: "pip".into(),
|
|
runtime: Some(runtime),
|
|
entry: contract.entry.clone(),
|
|
schema: contract.schema.clone(),
|
|
capabilities: contract.capabilities.clone(),
|
|
operations: contract.operations.clone(),
|
|
libs: contract
|
|
.libs
|
|
.iter()
|
|
.map(|spec| spec.parse().unwrap())
|
|
.collect(),
|
|
});
|
|
lock.write_atomic(temp.path()).unwrap();
|
|
let generated = prepare_managed_worker(temp.path(), &program, resources);
|
|
assert!(
|
|
generated
|
|
.iter()
|
|
.any(|resource| resource.path.contains("_speedups.")
|
|
&& resource.bytes.starts_with(b"\x7fELF"))
|
|
);
|
|
let (deployment, launch) = run_packaged_protocol(&temp, &contract, "pip", &generated);
|
|
let output = dever_process::Launch {
|
|
assets: &deployment.join("process-runtime"),
|
|
worker: &launch.working_directory,
|
|
executable: &launch.working_directory.join("runtime/bin/prefix-probe"),
|
|
arguments: &[],
|
|
working_directory: &launch.working_directory,
|
|
}
|
|
.command()
|
|
.unwrap()
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
output.status.success(),
|
|
"standalone wheel script failed: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
assert_eq!(output.stdout, b"prefix-ok\n");
|
|
}
|
|
|
|
fn run_packaged_protocol(
|
|
temp: &TemporaryDirectory,
|
|
contract: &dever_core::hir::ExternalWorkerContract,
|
|
ecosystem: &str,
|
|
generated: &[EmbeddedResource],
|
|
) -> (PathBuf, dever_runtime::external::WorkerLaunch) {
|
|
let borrowed = generated
|
|
.iter()
|
|
.map(|resource| dever_runtime::external::Resource {
|
|
path: &resource.path,
|
|
bytes: &resource.bytes,
|
|
sha256: &resource.sha256,
|
|
executable: resource.executable,
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let deployment = dever_runtime::external::extract(
|
|
temp.path(),
|
|
&dever_runtime::external::bundle_digest(&borrowed),
|
|
&borrowed,
|
|
)
|
|
.unwrap();
|
|
let launch = dever_runtime::external::verified_worker_launch(
|
|
&deployment,
|
|
&borrowed,
|
|
&contract.entry,
|
|
ecosystem,
|
|
)
|
|
.unwrap();
|
|
let child = dever_process::Launch {
|
|
assets: &deployment.join("process-runtime"),
|
|
worker: &launch.working_directory,
|
|
executable: &launch.executable,
|
|
arguments: &launch.arguments,
|
|
working_directory: &launch.working_directory,
|
|
}
|
|
.command()
|
|
.unwrap()
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
assert_fixture_protocol(child, contract);
|
|
(deployment, launch)
|
|
}
|
|
|
|
#[path = "external_workers/npm.rs"]
|
|
mod npm_native;
|
|
|
|
#[test]
|
|
fn installs_one_wheel_for_base_and_extra_variants() {
|
|
let temp = TemporaryDirectory::new();
|
|
let (_, resources) = fixture_entry(
|
|
temp.path(),
|
|
"pip",
|
|
&native_elf::static_executable(),
|
|
Some("Wheel-Version: 1.0\nRoot-Is-Purelib: true\n"),
|
|
"worker/main.py",
|
|
);
|
|
let sources = worker_sources(
|
|
"pip",
|
|
"worker/main.py",
|
|
"lib \"fixture@1.0.0\"\nlib \"fixture[extra]@1.0.0\"",
|
|
);
|
|
let program = dever_core::check(&sources).unwrap_or_else(|errors| panic!("{errors:?}"));
|
|
let contract = program.external_worker_contracts().remove(0);
|
|
let mut lock = LockFile::decode(&fs::read(temp.path().join("dever.lock")).unwrap()).unwrap();
|
|
let mut extra = lock.libs[0].clone();
|
|
extra.spec = "pip:fixture[extra]@1.0.0".parse().unwrap();
|
|
lock.libs.push(extra);
|
|
lock.workers[0].schema = contract.schema.clone();
|
|
for library in &mut lock.libs {
|
|
library.schema = contract.schema.clone();
|
|
}
|
|
lock.workers[0].libs = contract
|
|
.libs
|
|
.iter()
|
|
.map(|spec| spec.parse().unwrap())
|
|
.collect();
|
|
lock.write_atomic(temp.path()).unwrap();
|
|
let generated = workers::prepare(temp.path(), &program, &resources).unwrap();
|
|
assert_eq!(
|
|
generated
|
|
.iter()
|
|
.filter(|resource| resource
|
|
.path
|
|
.ends_with("/site-packages/fixture/__init__.py"))
|
|
.count(),
|
|
1
|
|
);
|
|
|
|
lock.libs[1].artifacts[0].sha256 = "0".repeat(64);
|
|
lock.write_atomic(temp.path()).unwrap();
|
|
assert!(workers::prepare(temp.path(), &program, &resources).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_runtime_identity_and_adapter_source_links() {
|
|
let temp = TemporaryDirectory::new();
|
|
let (program, mut resources) =
|
|
fixture(temp.path(), "npm", &native_elf::static_executable(), None);
|
|
let target = dever_cli::toolchain::platform_identity();
|
|
let invalid = json!({"format":"dever-worker-runtime-v1","ecosystem":"pip","target":target,
|
|
"executable":"bin/node","arguments":[]});
|
|
let pack = archive(&[
|
|
("dever-runtime.json", &serde_json::to_vec(&invalid).unwrap()),
|
|
("bin/node", &native_elf::static_executable()),
|
|
]);
|
|
resources[0] = resource(resources[0].path.clone(), pack);
|
|
let mut lock = LockFile::decode(&fs::read(temp.path().join("dever.lock")).unwrap()).unwrap();
|
|
lock.workers[0].runtime.as_mut().unwrap().sha256 = resources[0].sha256.clone();
|
|
lock.write_atomic(temp.path()).unwrap();
|
|
assert!(
|
|
workers::prepare(temp.path(), &program, &resources)
|
|
.unwrap_err()
|
|
.contains("runtime pack identity")
|
|
);
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::symlink;
|
|
let linked = TemporaryDirectory::new();
|
|
let (linked_program, linked_resources) =
|
|
fixture(linked.path(), "npm", &native_elf::static_executable(), None);
|
|
let entry = &linked_program.external_worker_contracts()[0].entry;
|
|
symlink(
|
|
linked.path().join(entry),
|
|
linked.path().join(entry).with_file_name("helper.mjs"),
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
workers::prepare(linked.path(), &linked_program, &linked_resources)
|
|
.unwrap_err()
|
|
.contains("symbolic link")
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_python_runtime_without_isolation_flags() {
|
|
let temp = TemporaryDirectory::new();
|
|
let (program, mut resources) =
|
|
fixture(temp.path(), "pip", &native_elf::static_executable(), None);
|
|
let target = dever_cli::toolchain::platform_identity();
|
|
let metadata = json!({"format":"dever-worker-runtime-v1","ecosystem":"pip","target":target,
|
|
"executable":"bin/python3","arguments":[]});
|
|
let pack = archive(&[
|
|
(
|
|
"dever-runtime.json",
|
|
&serde_json::to_vec(&metadata).unwrap(),
|
|
),
|
|
("bin/python3", &native_elf::static_executable()),
|
|
]);
|
|
resources[0] = resource(resources[0].path.clone(), pack);
|
|
let mut lock = LockFile::decode(&fs::read(temp.path().join("dever.lock")).unwrap()).unwrap();
|
|
lock.workers[0].runtime.as_mut().unwrap().sha256 = resources[0].sha256.clone();
|
|
lock.write_atomic(temp.path()).unwrap();
|
|
assert!(
|
|
workers::prepare(temp.path(), &program, &resources)
|
|
.unwrap_err()
|
|
.contains("-I -S -B")
|
|
);
|
|
}
|
|
|
|
fn fixture_runtime(name: &str) -> String {
|
|
let path =
|
|
std::env::var(name).unwrap_or_else(|_| panic!("set {name} to an absolute runtime path"));
|
|
assert!(Path::new(&path).is_absolute());
|
|
path
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires the explicit host Node fixture path"]
|
|
fn runs_javascript_runner_from_explicit_fixture_runtime_path() {
|
|
run_fixture_runtime("npm", &fixture_runtime("DEVER_TEST_NODE"));
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires the explicit host Python fixture path"]
|
|
fn runs_python_runner_from_explicit_fixture_runtime_path() {
|
|
run_fixture_runtime("pip", &fixture_runtime("DEVER_TEST_PYTHON"));
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires the explicit host Node fixture path"]
|
|
fn binds_commonjs_exports_without_a_registration_table() {
|
|
let temp = TemporaryDirectory::new();
|
|
let (program, resources) = fixture_entry(
|
|
temp.path(),
|
|
"npm",
|
|
&native_elf::static_executable(),
|
|
None,
|
|
"worker/main.cjs",
|
|
);
|
|
execute_generated_fixture(
|
|
&temp,
|
|
&program,
|
|
&resources,
|
|
Some(&fixture_runtime("DEVER_TEST_NODE")),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires the explicit host Node fixture path"]
|
|
fn binds_esm_exports_without_a_registration_table() {
|
|
let temp = TemporaryDirectory::new();
|
|
let (program, resources) = fixture(temp.path(), "npm", &native_elf::static_executable(), None);
|
|
execute_generated_fixture(
|
|
&temp,
|
|
&program,
|
|
&resources,
|
|
Some(&fixture_runtime("DEVER_TEST_NODE")),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires the explicit host Python fixture path"]
|
|
fn imports_packaged_sdk_before_adapter_modules() {
|
|
let temp = TemporaryDirectory::new();
|
|
let (program, resources) = fixture(temp.path(), "pip", &native_elf::static_executable(), None);
|
|
let entry = &program.external_worker_contracts()[0].entry;
|
|
fs::write(
|
|
temp.path().join(entry).with_file_name("dever_component.py"),
|
|
"raise RuntimeError('shadowed SDK')\n",
|
|
)
|
|
.unwrap();
|
|
execute_generated_fixture(
|
|
&temp,
|
|
&program,
|
|
&resources,
|
|
Some(&fixture_runtime("DEVER_TEST_PYTHON")),
|
|
);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
#[ignore = "requires the explicit host Node fixture and native Rust compiler"]
|
|
fn compiled_managed_worker_runs_after_source_and_pack_are_removed() {
|
|
use std::os::unix::process::CommandExt;
|
|
|
|
let temp = TemporaryDirectory::new();
|
|
let binary = fs::read(&fixture_runtime("DEVER_TEST_NODE")).unwrap();
|
|
let (program, resources, sources) =
|
|
fixture_entry_with_sources(temp.path(), "npm", &binary, None, "worker/main.mjs");
|
|
let source_entry = program.external_worker_contracts()[0].entry.clone();
|
|
fs::write(
|
|
temp.path().join(&source_entry),
|
|
r#"
|
|
import { execFileSync } from 'node:child_process';
|
|
export const send = ({ value }) => {
|
|
const receipt = Number(execFileSync(process.execPath,
|
|
['-e', 'process.stdout.write(String(Number(process.argv[1]) + 1))', String(value)],
|
|
{ encoding: 'utf8', timeout: 3000, maxBuffer: 1024 }));
|
|
return { receipt };
|
|
};
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let raw_pack_path = temp.path().join("runtime.pack");
|
|
fs::write(&raw_pack_path, &resources[0].bytes).unwrap();
|
|
let raw = resource(resources[0].path.clone(), fs::read(&raw_pack_path).unwrap());
|
|
let packaged = prepare_managed_worker(temp.path(), &program, vec![raw]);
|
|
let native = dever_core::native::compile_project_with_resources(
|
|
&program,
|
|
&sources,
|
|
std::ffi::OsStr::new("rustc"),
|
|
dever_runtime::config::RuntimeProfile::default(),
|
|
&packaged,
|
|
)
|
|
.unwrap();
|
|
let executable = temp.path().join("program");
|
|
native.save(&executable).unwrap();
|
|
drop(native);
|
|
drop(sources);
|
|
drop(program);
|
|
drop(resources);
|
|
fs::remove_file(temp.path().join(source_entry)).unwrap();
|
|
fs::remove_file(temp.path().join("dever.lock")).unwrap();
|
|
fs::remove_file(raw_pack_path).unwrap();
|
|
fs::create_dir(temp.path().join("config")).unwrap();
|
|
fs::write(
|
|
temp.path().join("config/setting.json"),
|
|
r#"{"adapter":{"mail.send":{"use":"default"}}}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let child = Command::new(&executable)
|
|
.process_group(0)
|
|
.current_dir(temp.path())
|
|
.env_clear()
|
|
.args(["mail.send.send", "{\"value\":7}"])
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
let mut owned = OwnedProcessGroup(child);
|
|
let deadline = Instant::now() + Duration::from_secs(15);
|
|
let status = loop {
|
|
if let Some(status) = owned.0.try_wait().unwrap() {
|
|
break Some(status);
|
|
}
|
|
if Instant::now() >= deadline {
|
|
break None;
|
|
}
|
|
thread::sleep(Duration::from_millis(10));
|
|
};
|
|
if status.is_none() {
|
|
owned.stop();
|
|
}
|
|
let mut stdout = Vec::new();
|
|
let mut stderr = String::new();
|
|
owned
|
|
.0
|
|
.stdout
|
|
.take()
|
|
.unwrap()
|
|
.read_to_end(&mut stdout)
|
|
.unwrap();
|
|
owned
|
|
.0
|
|
.stderr
|
|
.take()
|
|
.unwrap()
|
|
.read_to_string(&mut stderr)
|
|
.unwrap();
|
|
assert!(
|
|
status.is_some_and(|status| status.success()),
|
|
"native managed Worker failed or timed out: {stderr}"
|
|
);
|
|
let response: serde_json::Value = serde_json::from_slice(&stdout).unwrap_or_else(|error| {
|
|
panic!(
|
|
"invalid CMD response: {error}; stdout={:?}; stderr={stderr}",
|
|
String::from_utf8_lossy(&stdout)
|
|
)
|
|
});
|
|
assert_eq!(response["code"], 0, "{response}; stderr={stderr}");
|
|
assert_eq!(response["data"], 8, "{response}; stderr={stderr}");
|
|
}
|
|
|
|
fn run_fixture_runtime(ecosystem: &str, binary_path: &str) {
|
|
let temp = TemporaryDirectory::new();
|
|
let binary = fs::read(binary_path).unwrap();
|
|
let (program, resources) = fixture(temp.path(), ecosystem, &binary, None);
|
|
execute_generated_fixture(&temp, &program, &resources, None);
|
|
}
|
|
|
|
fn execute_generated_fixture(
|
|
temp: &TemporaryDirectory,
|
|
program: &dever_core::hir::Program,
|
|
resources: &[EmbeddedResource],
|
|
host_runtime: Option<&str>,
|
|
) {
|
|
let generated = workers::prepare(temp.path(), program, resources).unwrap();
|
|
let entry = &program.external_worker_contracts()[0].entry;
|
|
let launch = generated
|
|
.iter()
|
|
.find(|resource| resource.path == format!("{entry}.dever-worker.json"))
|
|
.unwrap();
|
|
let manifest: serde_json::Value = serde_json::from_slice(&launch.bytes).unwrap();
|
|
let executable = manifest["executable"].as_str().unwrap();
|
|
let runner = manifest["arguments"].as_array().unwrap().last().unwrap()["resource"]
|
|
.as_str()
|
|
.unwrap();
|
|
|
|
for resource in &generated {
|
|
let path = temp.path().join(&resource.path);
|
|
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
fs::write(&path, &resource.bytes).unwrap();
|
|
#[cfg(unix)]
|
|
if resource.executable {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
|
|
}
|
|
}
|
|
let contract = program.external_worker_contracts().remove(0);
|
|
let runtime_path = host_runtime
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| temp.path().join(executable));
|
|
let mut command = Command::new(runtime_path);
|
|
for flag in manifest["arguments"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.take(manifest["arguments"].as_array().unwrap().len() - 1)
|
|
{
|
|
command.arg(flag["literal"].as_str().unwrap());
|
|
}
|
|
let child = command
|
|
.arg(temp.path().join(runner))
|
|
.arg("--dever-component")
|
|
.current_dir(
|
|
temp.path()
|
|
.join(manifest["working_directory"].as_str().unwrap()),
|
|
)
|
|
.env_clear()
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
assert_fixture_protocol(child, &contract);
|
|
}
|
|
|
|
fn assert_fixture_protocol(
|
|
mut child: std::process::Child,
|
|
contract: &dever_core::hir::ExternalWorkerContract,
|
|
) {
|
|
let hello = json!({"kind":"hello","version":"dever-component-1","port":contract.port,
|
|
"adapter":contract.adapter,"schema":contract.schema,"operations":contract.operations,
|
|
"capabilities":contract.capabilities,"setting":null});
|
|
let (sender, receiver) = mpsc::channel();
|
|
let mut stdout = child.stdout.take().unwrap();
|
|
let reader = thread::spawn(move || {
|
|
while let Some(message) = response(&mut stdout) {
|
|
if sender.send(message).is_err() {
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
let exchange = (|| -> Result<Vec<serde_json::Value>, String> {
|
|
let mut messages = Vec::new();
|
|
for request in [
|
|
hello,
|
|
json!({"kind":"call","id":1,"operation":"send","payload":{"value":7}}),
|
|
json!({"kind":"shutdown"}),
|
|
] {
|
|
child
|
|
.stdin
|
|
.as_mut()
|
|
.unwrap()
|
|
.write_all(&frame(request))
|
|
.map_err(|error| error.to_string())?;
|
|
let message = receiver
|
|
.recv_timeout(Duration::from_secs(10))
|
|
.map_err(|error| error.to_string())?;
|
|
messages.push(message);
|
|
}
|
|
Ok(messages)
|
|
})();
|
|
drop(child.stdin.take());
|
|
if exchange.is_err() {
|
|
let _ = child.kill();
|
|
}
|
|
let deadline = Instant::now() + Duration::from_secs(10);
|
|
let status = loop {
|
|
if let Some(status) = child.try_wait().unwrap() {
|
|
break status;
|
|
}
|
|
if Instant::now() >= deadline {
|
|
child.kill().unwrap();
|
|
child.wait().unwrap();
|
|
panic!("managed fixture Worker did not stop");
|
|
}
|
|
thread::sleep(Duration::from_millis(10));
|
|
};
|
|
let mut stderr = String::new();
|
|
reader.join().unwrap();
|
|
child
|
|
.stderr
|
|
.take()
|
|
.unwrap()
|
|
.read_to_string(&mut stderr)
|
|
.unwrap();
|
|
assert!(status.success(), "{stderr}");
|
|
let messages = exchange.unwrap_or_else(|error| panic!("{error}; stderr={stderr}"));
|
|
assert_eq!(messages.len(), 3, "{messages:?}; stderr={stderr}");
|
|
assert_eq!(messages[0]["kind"], "ready");
|
|
assert_eq!(
|
|
messages[1],
|
|
json!({"kind":"result","id":1,"payload":{"receipt":8}})
|
|
);
|
|
assert_eq!(messages[2], json!({"kind":"shutdown"}));
|
|
}
|