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"); }