244 lines
8.2 KiB
Rust
244 lines
8.2 KiB
Rust
use super::{Directory, fixture};
|
|
use dever::compiler::{build, emit, project::Project};
|
|
use std::path::Path;
|
|
|
|
fn document() -> String {
|
|
"verify() (answer: Bool) { answer = true }
|
|
"
|
|
.into()
|
|
}
|
|
|
|
fn write(root: &Path, name: &str, contents: impl AsRef<[u8]>) {
|
|
let path = root.join(name);
|
|
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
std::fs::write(path, contents).unwrap();
|
|
}
|
|
|
|
fn load_error(path: &Path, needle: &str) {
|
|
let error = Project::load(path).err().expect("invalid source accepted");
|
|
assert!(error.contains(needle), "{error}");
|
|
}
|
|
|
|
#[test]
|
|
fn entry_reachability_excludes_unused_task_code_but_keeps_checks() {
|
|
for (sample, entry, features) in [
|
|
("core.dever", "verify", vec![]),
|
|
("handler.dever", "verify", vec![]),
|
|
("task.dever", "verify", vec!["task"]),
|
|
("task.dever", "yes", vec![]),
|
|
] {
|
|
let project = Project::load(&fixture().with_file_name(sample)).unwrap();
|
|
let program = project.check().unwrap();
|
|
let root = program
|
|
.functions
|
|
.iter()
|
|
.position(|function| function.signature.source == 0 && function.signature.name == entry)
|
|
.unwrap();
|
|
let functions = program.reachable([root]);
|
|
assert!(functions.contains(&root));
|
|
assert!(functions.len() < program.functions.len());
|
|
assert_eq!(
|
|
build::runtime_features(&program, &functions),
|
|
features,
|
|
"{sample}/{entry}"
|
|
);
|
|
let source = emit::generate(&program, &project.sources, &functions);
|
|
if features.is_empty() {
|
|
assert!(
|
|
!source.contains("dever_runtime::task::"),
|
|
"{sample}/{entry}"
|
|
);
|
|
}
|
|
for index in 0..program.functions.len() {
|
|
if !functions.contains(&index) {
|
|
assert!(
|
|
!source.contains(&format!("fn function{index}(")),
|
|
"{sample}/{entry}: {index}"
|
|
);
|
|
assert!(
|
|
!source.contains(&format!("as function{index};")),
|
|
"{sample}/{entry}: {index}"
|
|
);
|
|
assert!(
|
|
!source.contains(&format!("enum Failure{index} {{")),
|
|
"{sample}/{entry}: {index}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
let mut project = Project::load(&fixture()).unwrap();
|
|
let source = &project.sources[0];
|
|
let text = format!(
|
|
"{}\nunused() (answer: Int) {{ answer = true }}\n",
|
|
source.text
|
|
);
|
|
project.sources[0] = dever::syntax::source::Source::new(source.id, source.path.clone(), text);
|
|
assert!(project.check().unwrap_err().contains("C005"));
|
|
}
|
|
|
|
#[test]
|
|
fn packages_execute_with_original_cross_file_failure_locations() {
|
|
let directory = Directory::new();
|
|
let path = fixture().with_file_name("package");
|
|
let project = Project::load(&path).unwrap();
|
|
let program = project.check().unwrap();
|
|
assert_eq!(
|
|
&program.packages[..3],
|
|
&["calc.batch", "calc.value", "main"]
|
|
);
|
|
let second = Project::load(&path).unwrap();
|
|
assert_eq!(
|
|
emit::generate(
|
|
&program,
|
|
&project.sources,
|
|
&(0..program.functions.len()).collect()
|
|
),
|
|
emit::generate(
|
|
&second.check().unwrap(),
|
|
&second.sources,
|
|
&(0..program.functions.len()).collect()
|
|
)
|
|
);
|
|
for (entry, expected) in [
|
|
("verify", None),
|
|
("calc.batch.verify", None),
|
|
(
|
|
"failureEntry",
|
|
Some(("Problem.Rejected", "Problem.Rejected(\"negative\")")),
|
|
),
|
|
(
|
|
"faultEntry",
|
|
Some(("integer arithmetic fault", "value + 1")),
|
|
),
|
|
] {
|
|
let binary =
|
|
build::build(&program, &project.sources, entry, &directory.0.join(entry)).unwrap();
|
|
let result = std::process::Command::new(binary).output().unwrap();
|
|
if let Some((message, needle)) = expected {
|
|
assert!(!result.status.success());
|
|
assert!(result.stdout.is_empty());
|
|
let stderr = String::from_utf8(result.stderr).unwrap();
|
|
let source = project
|
|
.sources
|
|
.iter()
|
|
.find(|source| source.path.ends_with("calc/value.dever"))
|
|
.unwrap();
|
|
let (line, column) = source.position(source.text.find(needle).unwrap());
|
|
assert!(stderr.contains(message), "{stderr}");
|
|
assert!(
|
|
stderr.contains(&format!("{}:{line}:{column}:", source.path.display())),
|
|
"{stderr}"
|
|
);
|
|
} else {
|
|
assert!(
|
|
result.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&result.stderr)
|
|
);
|
|
assert_eq!(result.stdout, b"true\n");
|
|
}
|
|
}
|
|
assert!(
|
|
build::build(
|
|
&program,
|
|
&project.sources,
|
|
"calc.missing",
|
|
&directory.0.join("missing")
|
|
)
|
|
.is_err()
|
|
);
|
|
assert!(!directory.0.join("missing").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn private_records_preserve_cross_package_value_semantics() {
|
|
let directory = Directory::new();
|
|
let project = Project::load(&fixture().with_file_name("private")).unwrap();
|
|
let program = project.check().unwrap();
|
|
let binary = build::build(
|
|
&program,
|
|
&project.sources,
|
|
"verify",
|
|
&directory.0.join("private"),
|
|
)
|
|
.unwrap();
|
|
let result = std::process::Command::new(binary).output().unwrap();
|
|
assert!(
|
|
result.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&result.stderr)
|
|
);
|
|
assert_eq!(result.stdout, b"true\n");
|
|
}
|
|
|
|
#[test]
|
|
fn package_paths_are_authoritative_and_discovery_is_stable() {
|
|
let directory = Directory::new();
|
|
write(&directory.0, "zeta/value.dever", document());
|
|
write(&directory.0, "alpha.dever", document());
|
|
write(&directory.0, "README.md", "not code");
|
|
write(&directory.0, ".hidden/invalid.dever", "not code");
|
|
let project = Project::load(&directory.0).unwrap();
|
|
let program = project.check().unwrap();
|
|
assert_eq!(&program.packages[..2], &["alpha", "zeta.value"]);
|
|
assert_eq!(
|
|
program.packages,
|
|
Project::load(&directory.0)
|
|
.unwrap()
|
|
.check()
|
|
.unwrap()
|
|
.packages
|
|
);
|
|
write(
|
|
&directory.0,
|
|
"zeta/value.dever",
|
|
document().replace("answer = true", "answer = 1"),
|
|
);
|
|
let error = Project::load(&directory.0).unwrap().check().unwrap_err();
|
|
assert!(
|
|
error.contains("zeta/value.dever:") && error.contains("C005"),
|
|
"{error}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn package_loader_rejects_collisions_names_and_invalid_sources() {
|
|
for (name, needle) in [
|
|
("Wrong.dever", "lower_snake_case"),
|
|
("user..bad.dever", "lower_snake_case"),
|
|
("dever/custom.dever", "reserved standard"),
|
|
("shop/account/app.dever", "application role"),
|
|
] {
|
|
let directory = Directory::new();
|
|
write(&directory.0, name, document());
|
|
load_error(&directory.0, needle);
|
|
}
|
|
let directory = Directory::new();
|
|
load_error(&directory.0, "no Dever sources");
|
|
write(&directory.0, "sample.dever", document());
|
|
write(&directory.0, "sample.dever.md", "verify() () {}");
|
|
load_error(&directory.0, "use .dever source");
|
|
let directory = Directory::new();
|
|
write(&directory.0, "bad.dever", [0xff]);
|
|
load_error(&directory.0, "bad.dever");
|
|
let directory = Directory::new();
|
|
write(&directory.0, "module/shop/account/app.dever", document());
|
|
let error = Project::load(&directory.0).unwrap().check().unwrap_err();
|
|
assert!(
|
|
error.contains("application has no supported API/CMD entry"),
|
|
"{error}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn package_loader_does_not_follow_source_links() {
|
|
let directory = Directory::new();
|
|
write(&directory.0, "sources/value.dever", document());
|
|
let source = directory.0.join("sources");
|
|
std::os::unix::fs::symlink(source.join("value.dever"), source.join("alias.dever")).unwrap();
|
|
load_error(&source, "source links");
|
|
load_error(&source.join("alias.dever"), "source links");
|
|
std::os::unix::fs::symlink(&source, directory.0.join("linked")).unwrap();
|
|
load_error(&directory.0.join("linked/value.dever"), "source links");
|
|
}
|