1012 lines
37 KiB
Rust
1012 lines
37 KiB
Rust
//! Actual default CLI boundary. Native cases consume an explicitly prepared
|
|
//! runtime archive; ordinary negative checks never start a compiler subprocess.
|
|
#![cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
|
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, Output, Stdio};
|
|
use std::time::Duration;
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
#[path = "support/llvm_pack.rs"]
|
|
mod pack;
|
|
#[path = "../dever-tests/tests/support/process.rs"]
|
|
mod process;
|
|
#[path = "../dever-tests/tests/support/temp.rs"]
|
|
mod temp;
|
|
use temp::TemporaryDirectory;
|
|
|
|
const APP: &str = r#"type Outcome { error Denied(message: Text, labels: List<Text?>) }
|
|
echo(name: Text) (value: Text) { dever.task.sleep(1)
|
|
value = name }
|
|
divide(divisor: Int) (value: Int) { value = 42 // divisor }
|
|
deny() (value: Text) { fail(Outcome.Denied("owned denial", ["nested label", null])) }
|
|
"#;
|
|
|
|
fn write(root: &Path, relative: &str, source: &str) {
|
|
let path = root.join(relative);
|
|
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
fs::write(path, source).unwrap();
|
|
}
|
|
|
|
fn project(root: &Path) {
|
|
write(root, "module/sample/echo/app.dever", APP);
|
|
write(
|
|
root,
|
|
"module/sample/echo/api.dever",
|
|
"cmd echo = app.echo\ncmd divide = app.divide\ncmd deny = app.deny",
|
|
);
|
|
write(root, "config/setting.json", "{}");
|
|
}
|
|
|
|
fn compiler(root: &Path, name: &str) -> PathBuf {
|
|
pack::compiler(Path::new(env!("CARGO_BIN_EXE_dever")), root, name)
|
|
}
|
|
|
|
fn output(command: &mut Command) -> Output {
|
|
let capture = TemporaryDirectory::new();
|
|
let stdout = capture.path().join("stdout");
|
|
let stderr = capture.path().join("stderr");
|
|
command
|
|
.stdout(Stdio::from(fs::File::create(&stdout).unwrap()))
|
|
.stderr(Stdio::from(fs::File::create(&stderr).unwrap()));
|
|
let status = process::status(command, Duration::from_secs(180)).unwrap();
|
|
Output {
|
|
status,
|
|
stdout: fs::read(stdout).unwrap(),
|
|
stderr: fs::read(stderr).unwrap(),
|
|
}
|
|
}
|
|
|
|
fn cli(compiler: &Path, project: &Path, action: &str, args: &[&str]) -> Output {
|
|
output(
|
|
Command::new(compiler)
|
|
.env_clear()
|
|
.arg(action)
|
|
.arg(project)
|
|
.args(args),
|
|
)
|
|
}
|
|
|
|
fn success(output: &Output) {
|
|
assert!(
|
|
output.status.success(),
|
|
"stdout: {}\nstderr: {}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
|
|
fn failure(output: &Output, expected: &str) {
|
|
assert!(!output.status.success(), "unexpected success");
|
|
let message = format!(
|
|
"{}{}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
assert!(
|
|
message.contains(expected),
|
|
"expected {expected:?}: {message}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_pack_fails_without_host_tools_and_check_and_empty_test_need_no_pack() {
|
|
let toolchain = TemporaryDirectory::new();
|
|
let compiler = compiler(toolchain.path(), "dever");
|
|
let help = output(Command::new(&compiler).env_clear().arg("--help"));
|
|
success(&help);
|
|
assert!(String::from_utf8_lossy(&help.stdout).contains("dever run <project-root>"));
|
|
let project_dir = TemporaryDirectory::new();
|
|
project(project_dir.path());
|
|
success(&cli(&compiler, project_dir.path(), "check", &[]));
|
|
let tests = cli(&compiler, project_dir.path(), "test", &[]);
|
|
success(&tests);
|
|
assert!(String::from_utf8_lossy(&tests.stdout).contains("0 passed; 0 failed"));
|
|
failure(
|
|
&cli(
|
|
&compiler,
|
|
project_dir.path(),
|
|
"run",
|
|
&["--", "sample.echo.echo", "{\"name\":\"Ada\"}"],
|
|
),
|
|
"runtime pack",
|
|
);
|
|
assert!(!toolchain.path().join("cache").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn application_check_rejects_redundancy_before_preparing_build_resources() {
|
|
let toolchain = TemporaryDirectory::new();
|
|
let compiler = compiler(toolchain.path(), "dever");
|
|
let project_dir = TemporaryDirectory::new();
|
|
let root = project_dir.path();
|
|
write(root, "config/setting.json", "{}");
|
|
write(
|
|
root,
|
|
"module/sample/profile/domain.dever",
|
|
"normalize(value: Text) (result: Text) pure { result = text.trim(value) }\nalias(value: Text) (result: Text) pure { result = normalize(value) }\norphan() (result: Text) pure { result = \"unused\" }",
|
|
);
|
|
write(
|
|
root,
|
|
"module/sample/profile/app.dever",
|
|
"greet(value: Text) (result: Text) { result = domain.alias(value) }\nunused() (result: Text) { result = \"unused\" }",
|
|
);
|
|
write(
|
|
root,
|
|
"module/sample/profile/api.dever",
|
|
"cmd greet = app.greet",
|
|
);
|
|
let checked = cli(&compiler, root, "check", &[]);
|
|
failure(&checked, "C016");
|
|
let diagnostic = String::from_utf8_lossy(&checked.stderr);
|
|
for expected in [
|
|
"sample/profile/app.dever:2:1: error[C016]",
|
|
"function 'sample.profile.unused' is unreachable",
|
|
"sample/profile/domain.dever:2:1: error[C016]",
|
|
"only forwards unchanged inputs",
|
|
"sample/profile/domain.dever:3:1: error[C016]",
|
|
"function 'sample.profile.domain.orphan' is unreachable",
|
|
] {
|
|
assert!(diagnostic.contains(expected), "{expected}: {diagnostic}");
|
|
}
|
|
assert_eq!(diagnostic.matches("error[C016]").count(), 3, "{diagnostic}");
|
|
assert!(!diagnostic.contains("warning[W005]"), "{diagnostic}");
|
|
assert!(
|
|
diagnostic.contains("sample/profile/domain.dever:1:1: note:"),
|
|
"{diagnostic}"
|
|
);
|
|
// The same source fails before native packs, output publication or execution.
|
|
let executable = root.join("must-not-build");
|
|
for (action, args) in [
|
|
("run", Vec::new()),
|
|
("build", vec!["--output", executable.to_str().unwrap()]),
|
|
] {
|
|
let rejected = cli(&compiler, root, action, &args);
|
|
assert!(!rejected.status.success());
|
|
assert_eq!(rejected.stderr, checked.stderr);
|
|
}
|
|
assert!(!executable.exists());
|
|
assert!(!root.join("data").exists());
|
|
assert!(!toolchain.path().join("cache").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn application_check_rejects_proven_duplicates_before_runtime_or_database_access() {
|
|
let toolchain = TemporaryDirectory::new();
|
|
let compiler = compiler(toolchain.path(), "dever");
|
|
for (domain, app, diagnostic, location, warning) in [
|
|
(
|
|
"first(value: Int) (answer: Int) pure { answer = value + 1 }\nsecond(renamed: Int) (answer: Int) pure { answer = renamed + 1 }",
|
|
"total(value: Int) (answer: Int) { first = domain.first(value)\nsecond = domain.second(value)\nanswer = first + second }",
|
|
"same implementation and contract",
|
|
"sample/count/domain.dever:2:1: error[C016]",
|
|
"warning[W003]",
|
|
),
|
|
(
|
|
"calculate(value: Int) (answer: Int) pure { answer = value + 1 }",
|
|
"total(value: Int) (answer: Int) { first = domain.calculate(value)\nsecond = domain.calculate(value)\nanswer = first + second }",
|
|
"earlier result is still available",
|
|
"sample/count/app.dever:2:",
|
|
"warning[W004]",
|
|
),
|
|
] {
|
|
let project_dir = TemporaryDirectory::new();
|
|
let root = project_dir.path();
|
|
write(root, "config/setting.json", "{}");
|
|
write(root, "module/sample/count/model.dever", "type Count { value: Int }");
|
|
write(root, "module/sample/count/domain.dever", domain);
|
|
write(root, "module/sample/count/app.dever", app);
|
|
write(root, "module/sample/count/api.dever", "cmd total = app.total");
|
|
let checked = cli(&compiler, root, "check", &[]);
|
|
for expected in ["C016", diagnostic, location, "previous implementation or computation"] {
|
|
failure(&checked, expected);
|
|
}
|
|
let stderr = String::from_utf8_lossy(&checked.stderr);
|
|
assert_eq!(stderr.matches("error[C016]").count(), 1, "{stderr}");
|
|
assert!(!stderr.contains(warning), "{stderr}");
|
|
assert_eq!(
|
|
stderr.matches("note: previous implementation or computation").count(),
|
|
1,
|
|
"{stderr}"
|
|
);
|
|
let executable = root.join("must-not-build");
|
|
for (action, arguments) in [
|
|
("run", vec!["--", "sample.count.total", "{\"value\":1}"]),
|
|
("build", vec!["--output", executable.to_str().unwrap()]),
|
|
] {
|
|
let rejected = cli(&compiler, root, action, &arguments);
|
|
assert!(!rejected.status.success());
|
|
assert_eq!(rejected.stderr, checked.stderr);
|
|
}
|
|
let snapshot = cli(&compiler, root, "api", &[]);
|
|
success(&snapshot);
|
|
let stderr = String::from_utf8_lossy(&snapshot.stderr);
|
|
assert!(stderr.contains(warning), "{stderr}");
|
|
assert!(!stderr.contains("C016"), "{stderr}");
|
|
write(
|
|
root,
|
|
"test/sample/count/value.dever",
|
|
"value() () { assert_eq(app.total(1), 4) }",
|
|
);
|
|
let tests = cli(&compiler, root, "test", &[]);
|
|
failure(&tests, "runtime pack");
|
|
let stderr = String::from_utf8_lossy(&tests.stderr);
|
|
assert!(stderr.contains(warning), "{stderr}");
|
|
assert!(!stderr.contains("C016"), "{stderr}");
|
|
write(root, "dever.api", "invalid baseline");
|
|
let baseline = cli(&compiler, root, "check", &[]);
|
|
failure(&baseline, "invalid API baseline");
|
|
let stderr = String::from_utf8_lossy(&baseline.stderr);
|
|
assert!(stderr.contains(warning), "{stderr}");
|
|
assert!(!stderr.contains("C016"), "{stderr}");
|
|
assert!(!executable.exists());
|
|
assert!(!root.join("data").exists());
|
|
}
|
|
assert!(!toolchain.path().join("cache").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn application_check_keeps_warnings_with_distinct_evidence_and_other_codes() {
|
|
let toolchain = TemporaryDirectory::new();
|
|
let compiler = compiler(toolchain.path(), "dever");
|
|
let project_dir = TemporaryDirectory::new();
|
|
let root = project_dir.path();
|
|
write(root, "config/setting.json", "{}");
|
|
write(
|
|
root,
|
|
"module/sample/count/domain.dever",
|
|
"orphan(value: Int) (answer: Int) pure { answer = value + 1 }\nfirst(value: Int) (answer: Int) pure { answer = value + 1 }\nsecond(value: Int) (answer: Int) pure { answer = value + 1 }",
|
|
);
|
|
write(
|
|
root,
|
|
"module/sample/count/app.dever",
|
|
"total(value: Int) (answer: Int) { unused = 0\nfirst = domain.first(value)\nsecond = domain.second(value)\nanswer = first + second }",
|
|
);
|
|
write(root, "module/sample/count/api.dever", "cmd total = app.total");
|
|
let checked = cli(&compiler, root, "check", &[]);
|
|
failure(&checked, "C016");
|
|
let stderr = String::from_utf8_lossy(&checked.stderr);
|
|
assert_eq!(stderr.matches("error[C016]").count(), 2, "{stderr}");
|
|
// Warning proof includes the orphan; production proof starts at the first reachable function.
|
|
assert_eq!(stderr.matches("warning[W003]").count(), 2, "{stderr}");
|
|
assert_eq!(stderr.matches("warning[W001]").count(), 1, "{stderr}");
|
|
assert_eq!(
|
|
stderr.matches("sample/count/domain.dever:1:1: note:").count(),
|
|
2,
|
|
"{stderr}"
|
|
);
|
|
assert_eq!(
|
|
stderr.matches("sample/count/domain.dever:2:1: note:").count(),
|
|
1,
|
|
"{stderr}"
|
|
);
|
|
assert!(!root.join("data").exists());
|
|
assert!(!toolchain.path().join("cache").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn application_check_rejects_plain_model_get_wrapper_without_opening_database() {
|
|
let toolchain = TemporaryDirectory::new();
|
|
let compiler = compiler(toolchain.path(), "dever");
|
|
let project_dir = TemporaryDirectory::new();
|
|
let root = project_dir.path();
|
|
write(root, "config/setting.json", "{}");
|
|
write(
|
|
root,
|
|
"module/sample/profile/model.dever",
|
|
"type Profile { name: Text }",
|
|
);
|
|
write(
|
|
root,
|
|
"module/sample/profile/app.dever",
|
|
"lookup(id: model.id) (profile: model.Profile) { profile = model.get(id) }",
|
|
);
|
|
write(
|
|
root,
|
|
"module/sample/profile/api.dever",
|
|
"cmd lookup = app.lookup",
|
|
);
|
|
let rejected = cli(&compiler, root, "check", &[]);
|
|
failure(&rejected, "C016");
|
|
let diagnostic = String::from_utf8_lossy(&rejected.stderr);
|
|
for expected in [
|
|
"sample/profile/app.dever:1:1: error[C016]",
|
|
"only forwards a Model CRUD operation; declare rest for plain CRUD",
|
|
] {
|
|
assert!(diagnostic.contains(expected), "{expected}: {diagnostic}");
|
|
}
|
|
assert!(!root.join("data").exists());
|
|
assert!(!toolchain.path().join("cache").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn application_check_accepts_both_templates_and_preserves_markdown_locations() {
|
|
let toolchain = TemporaryDirectory::new();
|
|
let compiler = compiler(toolchain.path(), "dever");
|
|
let workspace = TemporaryDirectory::new();
|
|
for markdown in [false, true] {
|
|
let root = workspace
|
|
.path()
|
|
.join(if markdown { "markdown" } else { "plain" });
|
|
dever_cli::project::create(&root, markdown).unwrap();
|
|
success(&cli(&compiler, &root, "check", &[]));
|
|
// Removing the production declaration must not make the test a production root.
|
|
let suffix = if markdown { "dever.md" } else { "dever" };
|
|
fs::remove_file(root.join(format!("module/hello/greeting/api.{suffix}"))).unwrap();
|
|
let rejected = cli(&compiler, &root, "check", &[]);
|
|
failure(&rejected, "C016");
|
|
failure(
|
|
&rejected,
|
|
"unreachable from API, CMD, or Job production entries",
|
|
);
|
|
let app = root.join(format!("module/hello/greeting/app.{suffix}"));
|
|
let source = fs::read_to_string(&app).unwrap();
|
|
let line = source
|
|
.lines()
|
|
.position(|line| line.starts_with("greet("))
|
|
.unwrap()
|
|
+ 1;
|
|
failure(&rejected, &format!("hello/greeting/app.{suffix}:{line}:1"));
|
|
assert!(!root.join("data").exists());
|
|
}
|
|
assert!(!toolchain.path().join("cache").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn application_check_needs_no_database_worker_or_runtime_pack() {
|
|
let toolchain = TemporaryDirectory::new();
|
|
let compiler = compiler(toolchain.path(), "dever");
|
|
let project_dir = TemporaryDirectory::new();
|
|
let root = project_dir.path();
|
|
write(root, "config/setting.json", "{}");
|
|
write(
|
|
root,
|
|
"module/sample/profile/model.dever",
|
|
"type Profile { name: Text }",
|
|
);
|
|
// A model-only project has no App/Domain code to reject, even without entries.
|
|
success(&cli(&compiler, root, "check", &[]));
|
|
write(
|
|
root,
|
|
"module/notification/mail/app.dever",
|
|
"read() (value: Int) { value = port.read() }",
|
|
);
|
|
write(
|
|
root,
|
|
"module/notification/mail/port.dever",
|
|
"read() (value: Int) fails dever.time.SleepResult",
|
|
);
|
|
write(
|
|
root,
|
|
"module/notification/mail/adapter.dever",
|
|
"external exec \"worker/must-not-start\" {}",
|
|
);
|
|
write(
|
|
root,
|
|
"module/notification/mail/api.dever",
|
|
"cmd read = app.read",
|
|
);
|
|
success(&cli(&compiler, root, "check", &[]));
|
|
assert!(!root.join("data").exists());
|
|
assert!(!root.join("dever.lock").exists());
|
|
assert!(!toolchain.path().join("cache").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn application_check_does_not_apply_production_reachability_to_tests() {
|
|
let toolchain = TemporaryDirectory::new();
|
|
let compiler = compiler(toolchain.path(), "dever");
|
|
let project_dir = TemporaryDirectory::new();
|
|
let root = project_dir.path();
|
|
write(
|
|
root,
|
|
"module/sample/profile/app.dever",
|
|
"greet() (value: Text) { value = \"hello\" }",
|
|
);
|
|
write(root, "config/setting.json", "{}");
|
|
failure(&cli(&compiler, root, "check", &[]), "C016");
|
|
let empty = cli(&compiler, root, "test", &[]);
|
|
success(&empty);
|
|
assert!(String::from_utf8_lossy(&empty.stdout).contains("0 passed; 0 failed"));
|
|
write(
|
|
root,
|
|
"test/sample/profile/greeting.dever",
|
|
"greeting() () { assert_eq(app.greet(), \"hello\") }",
|
|
);
|
|
// The real case reaches the test compiler's missing-pack boundary, not C016.
|
|
let tests = cli(&compiler, root, "test", &[]);
|
|
failure(&tests, "runtime pack");
|
|
assert!(!String::from_utf8_lossy(&tests.stderr).contains("C016"));
|
|
assert!(!String::from_utf8_lossy(&tests.stdout).contains("C016"));
|
|
assert!(!toolchain.path().join("cache").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_still_checks_unselected_sources_and_reports_no_match_before_compilation() {
|
|
let toolchain = TemporaryDirectory::new();
|
|
let compiler = compiler(toolchain.path(), "dever");
|
|
let project_dir = TemporaryDirectory::new();
|
|
let root = project_dir.path();
|
|
project(root);
|
|
write(root, "test/sample/echo/first.dever", "first() () { assert_eq(1, 1) }");
|
|
write(root, "test/sample/echo/second.dever", "second() () { assert_eq(1, 1) }");
|
|
for filter in ["sam", "sample.ec", "sample.echo.fir", "sample.echo.missing"] {
|
|
let result = cli(&compiler, root, "test", &["--filter", filter]);
|
|
failure(&result, "no tests match filter");
|
|
assert!(!String::from_utf8_lossy(&result.stdout).contains("runtime pack"));
|
|
}
|
|
let selected = cli(&compiler, root, "test", &["--filter", "sample.echo.second"]);
|
|
failure(&selected, "runtime pack");
|
|
let report = String::from_utf8_lossy(&selected.stdout);
|
|
assert!(report.contains("0 passed; 1 failed; 1 not run"), "{report}");
|
|
assert!(!report.contains("test sample/echo/first ..."), "{report}");
|
|
write(root, "test/sample/echo/first.dever", "first() () { missing() }");
|
|
failure(&cli(&compiler, root, "test", &["--filter", "sample.echo.second"]), "C004");
|
|
assert!(!toolchain.path().join("cache").exists());
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires an explicitly prepared runtime pack adjacent to CARGO_BIN_EXE_dever; owned short-lived cases only"]
|
|
fn llvm_test_controls_preserve_indices_prefix_boundaries_and_timeout_continuation() {
|
|
let compiler = Path::new(env!("CARGO_BIN_EXE_dever"));
|
|
let directory = TemporaryDirectory::new();
|
|
let root = directory.path();
|
|
project(root);
|
|
for (relative, source) in [
|
|
("test/sample/echo/alpha.dever", "alpha() () { assert_eq(1, 2) }"),
|
|
("test/sample/echo/beta.dever", "beta() () { assert_eq(7, 7) }"),
|
|
("test/sample/echo/slow.dever", "slow() () { dever.task.sleep(30000) }"),
|
|
("test/sample/echo/zeta.dever", "zeta() () { assert_eq(9, 9) }"),
|
|
("test/sample/echoes/first.dever", "first() () { assert_eq(1, 2) }"),
|
|
("test/samples/echo/first.dever", "first() () { assert_eq(1, 2) }"),
|
|
] {
|
|
write(root, relative, source);
|
|
}
|
|
let selected = cli(compiler, root, "test", &["--filter", "sample.echo.beta"]);
|
|
success(&selected);
|
|
let report = String::from_utf8_lossy(&selected.stdout);
|
|
assert!(report.contains("test sample/echo/beta ... ok"), "{report}");
|
|
assert!(report.contains("1 passed; 0 failed; 5 not run"), "{report}");
|
|
assert!(!report.contains("sample/echo/alpha"), "{report}");
|
|
|
|
for (filter, counts) in [
|
|
("sample.echo", "2 passed; 2 failed; 2 not run"),
|
|
("sample", "2 passed; 3 failed; 1 not run"),
|
|
] {
|
|
let selected = cli(compiler, root, "test", &["--filter", filter, "--timeout-ms", "250"]);
|
|
failure(&selected, counts);
|
|
let report = String::from_utf8_lossy(&selected.stdout);
|
|
assert!(report.contains("test process timed out after 250 ms"), "{report}");
|
|
assert!(report.contains("test sample/echo/zeta ... ok"), "{report}");
|
|
assert!(!report.contains("test samples/echo/first"), "{report}");
|
|
}
|
|
failure(&cli(compiler, root, "test", &["--filter", "sample.echo.bet"]), "no tests match filter");
|
|
}
|
|
|
|
#[test]
|
|
fn cross_build_requires_the_selected_pack_even_when_host_pack_exists() {
|
|
let root = TemporaryDirectory::new();
|
|
let compiler = compiler(root.path(), "dever");
|
|
pack::manifest(root.path(), false);
|
|
let project_dir = TemporaryDirectory::new();
|
|
project(project_dir.path());
|
|
let executable = root.path().join("arm-application");
|
|
failure(
|
|
&cli(
|
|
&compiler,
|
|
project_dir.path(),
|
|
"build",
|
|
&[
|
|
"--target",
|
|
"linux-aarch64",
|
|
"--output",
|
|
executable.to_str().unwrap(),
|
|
],
|
|
),
|
|
"runtime/linux-aarch64",
|
|
);
|
|
assert!(!executable.exists());
|
|
assert!(!root.path().join("cache").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn managed_cores_never_fall_back_to_private_packs_or_cache() {
|
|
let root = TemporaryDirectory::new();
|
|
let project_dir = TemporaryDirectory::new();
|
|
project(project_dir.path());
|
|
for (directory, diagnostic) in [
|
|
(
|
|
root.path().join("standalone"),
|
|
"managed compiler must be installed",
|
|
),
|
|
(
|
|
root.path().join("versions/0.1.0"),
|
|
"cannot inspect deverd state directory",
|
|
),
|
|
] {
|
|
let compiler = compiler(&directory, "dever-core");
|
|
failure(&cli(&compiler, project_dir.path(), "run", &[]), diagnostic);
|
|
assert!(!directory.join("cache").exists());
|
|
}
|
|
let installed = root.path().join("versions/0.1.0/dever-core");
|
|
let worker = output(
|
|
Command::new(installed)
|
|
.env_clear()
|
|
.current_dir(project_dir.path())
|
|
.arg("--dever-compile-worker"),
|
|
);
|
|
failure(&worker, "service-owned native cache staging directory");
|
|
assert!(!project_dir.path().join("program").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn pack_contract_rejects_drift_duplicate_paths_and_undeclared_link_inputs() {
|
|
let root = TemporaryDirectory::new();
|
|
let compiler = compiler(root.path(), "dever");
|
|
let original = pack::manifest(root.path(), false);
|
|
let project_dir = TemporaryDirectory::new();
|
|
project(project_dir.path());
|
|
let mutations: &[(&str, Value, &str)] = &[
|
|
("/format", json!("future"), "does not match"),
|
|
("/compiler_version", json!("99.0.0"), "does not match"),
|
|
("/abi", json!(99), "does not match"),
|
|
(
|
|
"/target",
|
|
json!("aarch64-unknown-linux-gnu"),
|
|
"does not match",
|
|
),
|
|
("/profiles/base", Value::Null, "missing the required 'base'"),
|
|
(
|
|
"/profiles/base/runtime",
|
|
json!("missing.a"),
|
|
"undeclared or duplicate",
|
|
),
|
|
(
|
|
"/profiles/base/libraries/0",
|
|
json!("runtime.a"),
|
|
"undeclared or duplicate",
|
|
),
|
|
(
|
|
"/files/0/path",
|
|
json!("../runtime.a"),
|
|
"normalized relative",
|
|
),
|
|
(
|
|
"/files/0/path",
|
|
json!("nested//runtime.a"),
|
|
"normalized relative",
|
|
),
|
|
(
|
|
"/files/0/path",
|
|
json!("C:\\runtime.a"),
|
|
"normalized relative",
|
|
),
|
|
(
|
|
"/files/1/path",
|
|
json!("runtime.a"),
|
|
"duplicate runtime file",
|
|
),
|
|
("/files/0/bytes", json!(9), "SHA-256 verification"),
|
|
(
|
|
"/files/0/sha256",
|
|
json!("0".repeat(64)),
|
|
"SHA-256 verification",
|
|
),
|
|
];
|
|
for (pointer, value, expected) in mutations {
|
|
let mut manifest = original.clone();
|
|
*manifest.pointer_mut(pointer).unwrap() = value.clone();
|
|
pack::write_manifest(root.path(), &manifest);
|
|
failure(&cli(&compiler, project_dir.path(), "run", &[]), expected);
|
|
assert!(!root.path().join("cache").exists());
|
|
}
|
|
let text = serde_json::to_string(&original).unwrap();
|
|
fs::write(
|
|
pack::pack_root(root.path()).join("manifest.json"),
|
|
text.replacen("\"abi\":1", "\"abi\":1,\"abi\":1", 1),
|
|
)
|
|
.unwrap();
|
|
failure(
|
|
&cli(&compiler, project_dir.path(), "run", &[]),
|
|
"duplicate field",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pack_rejects_symlinks_and_disguised_linker_scripts() {
|
|
use std::os::unix::fs::symlink;
|
|
let root = TemporaryDirectory::new();
|
|
let compiler = compiler(root.path(), "dever");
|
|
let mut manifest = pack::manifest(root.path(), false);
|
|
let project_dir = TemporaryDirectory::new();
|
|
project(project_dir.path());
|
|
let runtime = pack::pack_root(root.path()).join("runtime.a");
|
|
let outside = root.path().join("outside.a");
|
|
fs::rename(&runtime, &outside).unwrap();
|
|
symlink(&outside, &runtime).unwrap();
|
|
failure(
|
|
&cli(&compiler, project_dir.path(), "run", &[]),
|
|
"must be a real file",
|
|
);
|
|
fs::remove_file(&runtime).unwrap();
|
|
for content in [b"INPUT(/usr/lib/forbidden.a)".as_slice(), b"!<thin>\n"] {
|
|
fs::write(&runtime, content).unwrap();
|
|
manifest["files"][0]["bytes"] = json!(content.len());
|
|
manifest["files"][0]["sha256"] = json!(pack::digest(&runtime));
|
|
pack::write_manifest(root.path(), &manifest);
|
|
failure(
|
|
&cli(&compiler, project_dir.path(), "run", &[]),
|
|
"linker scripts and thin archives are forbidden",
|
|
);
|
|
}
|
|
assert!(!root.path().join("cache").exists());
|
|
}
|
|
|
|
fn cached_programs(root: &Path) -> Vec<PathBuf> {
|
|
let mut programs = fs::read_dir(root.join("cache/native"))
|
|
.unwrap()
|
|
.map(|entry| entry.unwrap().path().join("program"))
|
|
.filter(|path| path.is_file())
|
|
.collect::<Vec<_>>();
|
|
programs.sort();
|
|
programs
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires explicitly prepared full runtime archive and Linux CRT files; serial offline CLI execution"]
|
|
fn default_llvm_runs_builds_moves_and_reuses_cache_without_host_toolchain() {
|
|
let root = TemporaryDirectory::new();
|
|
let compiler = compiler(root.path(), "dever");
|
|
pack::manifest(root.path(), true);
|
|
let project_dir = TemporaryDirectory::new();
|
|
project(project_dir.path());
|
|
let args = ["--", "sample.echo.echo", "{\"name\":\"你好 LLVM\"}"];
|
|
let marker = root.path().join("forbidden-tool-was-started");
|
|
for name in ["rustc", "cargo", "cc"] {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let path = root.path().join(name);
|
|
fs::write(
|
|
&path,
|
|
format!(
|
|
"#!/bin/sh\nprintf forbidden > '{}'\nexit 99\n",
|
|
marker.display()
|
|
),
|
|
)
|
|
.unwrap();
|
|
fs::set_permissions(path, fs::Permissions::from_mode(0o700)).unwrap();
|
|
}
|
|
let run = output(
|
|
Command::new(&compiler)
|
|
.env_clear()
|
|
.env("PATH", root.path())
|
|
.env("RUSTC", root.path().join("rustc"))
|
|
.env("CARGO", root.path().join("cargo"))
|
|
.env("CC", root.path().join("cc"))
|
|
.arg("run")
|
|
.arg(project_dir.path())
|
|
.args(args),
|
|
);
|
|
success(&run);
|
|
assert!(
|
|
!marker.exists(),
|
|
"LLVM compilation must not start a host compiler"
|
|
);
|
|
let response: Value = serde_json::from_slice(&run.stdout).unwrap();
|
|
assert_eq!(response["data"], "你好 LLVM");
|
|
let cached = cached_programs(root.path());
|
|
assert_eq!(cached.len(), 1);
|
|
let modified = fs::metadata(&cached[0]).unwrap().modified().unwrap();
|
|
let binary = project_dir.path().join("standalone");
|
|
success(&cli(
|
|
&compiler,
|
|
project_dir.path(),
|
|
"build",
|
|
&["--output", binary.to_str().unwrap()],
|
|
));
|
|
assert_eq!(
|
|
fs::metadata(&cached[0]).unwrap().modified().unwrap(),
|
|
modified
|
|
);
|
|
let first_digest = pack::digest(&binary);
|
|
failure(
|
|
&cli(
|
|
&compiler,
|
|
project_dir.path(),
|
|
"build",
|
|
&["--output", binary.to_str().unwrap()],
|
|
),
|
|
"cannot save executable",
|
|
);
|
|
assert_eq!(pack::digest(&binary), first_digest);
|
|
let moved = TemporaryDirectory::new();
|
|
let relocated = moved.path().join("application");
|
|
fs::rename(&binary, &relocated).unwrap();
|
|
write(moved.path(), "config/setting.json", "{}");
|
|
success(&output(
|
|
Command::new(&relocated).env_clear().args(&args[1..]),
|
|
));
|
|
{
|
|
use std::os::fd::OwnedFd;
|
|
use std::os::unix::net::UnixStream;
|
|
let (reader, writer) = UnixStream::pair().unwrap();
|
|
drop(reader);
|
|
let stderr = moved.path().join("closed-stdout-error");
|
|
let status = process::status(
|
|
Command::new(&relocated)
|
|
.env_clear()
|
|
.args(&args[1..])
|
|
.stdout(Stdio::from(OwnedFd::from(writer)))
|
|
.stderr(Stdio::from(fs::File::create(&stderr).unwrap())),
|
|
Duration::from_secs(30),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
status.code(),
|
|
Some(1),
|
|
"closed stdout must return a checked failure, not SIGPIPE"
|
|
);
|
|
let error = fs::read_to_string(stderr).unwrap();
|
|
assert!(
|
|
error.contains("Broken pipe") && error.contains(".dever"),
|
|
"{error}"
|
|
);
|
|
}
|
|
let denied = cli(
|
|
&compiler,
|
|
project_dir.path(),
|
|
"run",
|
|
&["--", "sample.echo.deny", "{}"],
|
|
);
|
|
failure(&denied, "owned denial");
|
|
assert!(String::from_utf8_lossy(&denied.stderr).contains("nested label"));
|
|
assert!(String::from_utf8_lossy(&denied.stderr).contains("app.dever"));
|
|
failure(
|
|
&cli(
|
|
&compiler,
|
|
project_dir.path(),
|
|
"run",
|
|
&["--", "sample.echo.echo", "{}"],
|
|
),
|
|
"name",
|
|
);
|
|
let failure_output = cli(
|
|
&compiler,
|
|
project_dir.path(),
|
|
"run",
|
|
&["--", "sample.echo.divide", "{\"divisor\":0}"],
|
|
);
|
|
failure(&failure_output, "division");
|
|
assert!(String::from_utf8_lossy(&failure_output.stderr).contains("app.dever"));
|
|
// Corrupt only the fixture-owned cache copy, never a hard-linked runtime.
|
|
fs::write(&cached[0], b"corrupt cached executable").unwrap();
|
|
failure(
|
|
&cli(&compiler, project_dir.path(), "run", &args),
|
|
"incomplete or modified",
|
|
);
|
|
assert_eq!(fs::read(&cached[0]).unwrap(), b"corrupt cached executable");
|
|
write(
|
|
project_dir.path(),
|
|
"module/sample/echo/app.dever",
|
|
&APP.replace("value = name", "value = name + \"!\""),
|
|
);
|
|
let changed = cli(&compiler, project_dir.path(), "run", &args);
|
|
success(&changed);
|
|
assert_eq!(
|
|
serde_json::from_slice::<Value>(&changed.stdout).unwrap()["data"],
|
|
"你好 LLVM!"
|
|
);
|
|
assert_eq!(cached_programs(root.path()).len(), 2);
|
|
|
|
// Even a byte-level manifest change has its own exact pack identity.
|
|
let manifest = pack::pack_root(root.path()).join("manifest.json");
|
|
let mut bytes = fs::read(&manifest).unwrap();
|
|
bytes.push(b'\n');
|
|
fs::write(&manifest, bytes).unwrap();
|
|
success(&cli(&compiler, project_dir.path(), "run", &args));
|
|
assert_eq!(cached_programs(root.path()).len(), 3);
|
|
|
|
let markdown = TemporaryDirectory::new();
|
|
write(markdown.path(), "config/setting.json", "{}");
|
|
write(
|
|
markdown.path(),
|
|
"module/system/health/app.dever.md",
|
|
include_str!("../../examples/cms/md/module/system/health/app.dever.md"),
|
|
);
|
|
write(
|
|
markdown.path(),
|
|
"module/system/health/api.dever.md",
|
|
include_str!("../../examples/cms/md/module/system/health/api.dever.md"),
|
|
);
|
|
let response = cli(
|
|
&compiler,
|
|
markdown.path(),
|
|
"run",
|
|
&["--", "system.health.ping", "{}"],
|
|
);
|
|
success(&response);
|
|
assert_eq!(
|
|
serde_json::from_slice::<Value>(&response.stdout).unwrap()["data"]["status"],
|
|
"ready"
|
|
);
|
|
assert_eq!(cached_programs(root.path()).len(), 4);
|
|
let object = pack::pack_root(root.path()).join("crt1.o");
|
|
let mut corrupt = fs::read(&object).unwrap();
|
|
corrupt[16] ^= 1;
|
|
// Break this fixture's hard link before mutation; the host CRT is immutable.
|
|
fs::remove_file(&object).unwrap();
|
|
fs::write(&object, corrupt).unwrap();
|
|
failure(
|
|
&cli(&compiler, project_dir.path(), "run", &args),
|
|
"SHA-256 verification",
|
|
);
|
|
assert_eq!(cached_programs(root.path()).len(), 4);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires explicitly prepared runtime archive and component-fixture; no downloads or language SDK"]
|
|
fn unsigned_llvm_run_and_build_reject_exec_workers() {
|
|
let root = TemporaryDirectory::new();
|
|
let compiler = compiler(root.path(), "dever");
|
|
pack::manifest(root.path(), true);
|
|
let project_dir = TemporaryDirectory::new();
|
|
let directory = project_dir.path();
|
|
write(
|
|
directory,
|
|
"module/notification/delivery/app.dever",
|
|
"read() (value: Int) { value = port.read() }",
|
|
);
|
|
write(
|
|
directory,
|
|
"module/notification/delivery/port.dever",
|
|
"read() (value: Int) fails dever.time.SleepResult",
|
|
);
|
|
write(
|
|
directory,
|
|
"module/notification/delivery/adapter.dever",
|
|
"setting { mode: Text }\nexternal exec \"worker\" {}",
|
|
);
|
|
write(
|
|
directory,
|
|
"module/notification/delivery/api.dever",
|
|
"cmd read = app.read",
|
|
);
|
|
let settings = r#"{"adapter":{"notification.delivery":{"setting":{"mode":"embedded"}}}}"#;
|
|
write(directory, "config/setting.json", settings);
|
|
let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../../target/native-release-inputs/component-fixture");
|
|
assert!(
|
|
fixture.is_file(),
|
|
"prepare the immutable component-fixture in target/native-release-inputs"
|
|
);
|
|
fs::copy(
|
|
fixture,
|
|
directory.join("module/notification/delivery/worker"),
|
|
)
|
|
.unwrap();
|
|
let args = ["--", "notification.delivery.read", "{}"];
|
|
let run = cli(&compiler, directory, "run", &args);
|
|
let binary = directory.join("packaged");
|
|
let build = cli(
|
|
&compiler,
|
|
directory,
|
|
"build",
|
|
&["--output", binary.to_str().unwrap()],
|
|
);
|
|
for rejected in [run, build] {
|
|
assert!(!rejected.status.success());
|
|
assert!(rejected.stdout.is_empty());
|
|
assert_eq!(
|
|
String::from_utf8(rejected.stderr).unwrap().trim(),
|
|
"real Lib providers require a signed managed Dever release"
|
|
);
|
|
}
|
|
assert!(!binary.exists());
|
|
assert!(!root.path().join("cache").exists());
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires explicitly prepared full runtime archive; isolated SQLite and case-local fake processes"]
|
|
fn llvm_test_preserves_case_isolation_fakes_and_failure_continuation() {
|
|
let root = TemporaryDirectory::new();
|
|
let compiler = compiler(root.path(), "dever");
|
|
pack::manifest(root.path(), true);
|
|
let project_dir = TemporaryDirectory::new();
|
|
let directory = project_dir.path();
|
|
write(
|
|
directory,
|
|
"module/sample/account/model.dever",
|
|
"type Account { name: Text }",
|
|
);
|
|
write(
|
|
directory,
|
|
"module/sample/account/app.dever",
|
|
"create(name: Text) (count: Int) { row = model.create({ name = name })\n count = model.count() }",
|
|
);
|
|
write(
|
|
directory,
|
|
"module/notification/mail/app.dever",
|
|
"read() (value: Int) { value = port.read() }",
|
|
);
|
|
write(
|
|
directory,
|
|
"module/notification/mail/port.dever",
|
|
"read() (value: Int) fails dever.time.SleepResult",
|
|
);
|
|
write(
|
|
directory,
|
|
"module/notification/mail/adapter.dever",
|
|
"external exec \"worker/must-not-start\" {}",
|
|
);
|
|
write(
|
|
directory,
|
|
"config/setting.json",
|
|
r#"{"database":{"default":{"type":"sqlite","path":"deployment.db"}}}"#,
|
|
);
|
|
// The CLI reads project dependency metadata from valid JSON, but test
|
|
// execution must use its generated database, never this unusable directory.
|
|
fs::create_dir(directory.join("deployment.db")).unwrap();
|
|
for name in ["first", "second"] {
|
|
write(
|
|
directory,
|
|
&format!("test/sample/account/{name}.dever"),
|
|
&format!("{name}() () {{ assert_eq(app.create(\"Ada\"), 1) }}"),
|
|
);
|
|
}
|
|
write(
|
|
directory,
|
|
"test/notification/mail/blocking.dever",
|
|
"blocking() () { dever.time.sleep(1)\n assert_eq(app.read(), 11) }\nport.read() (value: Int) { value = 11 }",
|
|
);
|
|
write(
|
|
directory,
|
|
"test/notification/mail/suspending.dever",
|
|
"suspending() () { assert_eq(app.read(), 23) }\nport.read() (value: Int) { dever.task.sleep(1)\n value = 23 }",
|
|
);
|
|
write(
|
|
directory,
|
|
"test/sample/account/failure.dever",
|
|
"failure() () { assert_eq(1, 2) }",
|
|
);
|
|
write(
|
|
directory,
|
|
"test/sample/account/business.dever",
|
|
"type Outcome { error Denied(message: Text) }\nbusiness() () { fail(Outcome.Denied(\"test business denial\")) }",
|
|
);
|
|
let result = cli(&compiler, directory, "test", &[]);
|
|
failure(&result, "4 passed; 2 failed; 0 not run");
|
|
let report = String::from_utf8_lossy(&result.stdout);
|
|
assert!(
|
|
report.contains("test sample/account/second ... ok"),
|
|
"{report}"
|
|
);
|
|
assert!(report.contains("failure.dever"), "{report}");
|
|
assert!(
|
|
report.contains("assert_eq failed: actual = 1, expected = 2"),
|
|
"{report}"
|
|
);
|
|
assert!(report.contains("test business denial"), "{report}");
|
|
assert!(!directory.join("data").exists());
|
|
assert_eq!(cached_programs(root.path()).len(), 1);
|
|
let suite = &cached_programs(root.path())[0];
|
|
for args in [
|
|
vec![],
|
|
vec![""],
|
|
vec!["-1"],
|
|
vec!["+0"],
|
|
vec![" 0"],
|
|
vec!["0 "],
|
|
vec!["18446744073709551616"],
|
|
vec!["6"],
|
|
vec!["0", "extra"],
|
|
] {
|
|
let rejected = output(Command::new(suite).env_clear().args(args));
|
|
failure(&rejected, "test program requires a valid case index");
|
|
assert_eq!(
|
|
rejected.stderr,
|
|
b"test program requires a valid case index\n"
|
|
);
|
|
}
|
|
}
|