mod support; #[path = "../../../crates/dever-core/src/native/build/cache.rs"] #[allow(dead_code)] mod cache; #[path = "../../../crates/dever-cli/src/api.rs"] mod cli_api; use std::ffi::{OsStr, OsString}; use std::fs; use std::path::Path; use std::process::Command; use std::time::Instant; use dever_core::native::{NativeProgram, compile_with_cache}; use support::temp::TemporaryDirectory; fn rustc() -> OsString { std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()) } fn compile(source: &str, root: &Path, compiler: &OsStr) -> NativeProgram { let sources = support::sources(source); compile_with_cache( &support::checked(&sources), &sources, "main.main", compiler, root, ) .unwrap() } const HELLO: &str = "main() () { dever.io.println(\"cache hello\") }\n"; #[test] fn identified_artifacts_share_owned_cache_and_reject_modified_bytes() { use dever_core::native::compile_artifact; let directory = TemporaryDirectory::new(); let root = directory.path().join("cache"); let first = compile_artifact(&root, "llvm-source-and-pack-1", |_, output| { fs::write(output, b"complete artifact").map_err(|error| error.to_string()) }) .unwrap(); assert!(!first.cache_hit()); let second = compile_artifact(&root, "llvm-source-and-pack-1", |_, _| { panic!("an exact cache hit must not invoke the compiler") }) .unwrap(); assert!(second.cache_hit()); assert_ne!(first.executable(), second.executable()); let output = directory.path().join("published"); second.save(&output).unwrap(); assert_eq!( second.save(&output).unwrap_err().kind(), std::io::ErrorKind::AlreadyExists ); let temporary = first.executable().parent().unwrap().to_path_buf(); drop(first); assert!(!temporary.exists()); assert_eq!(fs::read(&output).unwrap(), b"complete artifact"); let stored = fs::read_dir(&root).unwrap().next().unwrap().unwrap().path(); fs::write(stored.join("program"), b"tampered").unwrap(); let result = compile_artifact(&root, "llvm-source-and-pack-1", |_, _| { panic!("corruption must fail before compilation") }); assert!(result.err().unwrap().contains("incomplete or modified")); let changed = compile_artifact(&root, "llvm-source-and-pack-2", |_, output| { fs::write(output, b"changed input").map_err(|error| error.to_string()) }) .unwrap(); assert!(!changed.cache_hit()); } #[test] fn identified_artifact_failure_releases_directory_without_publication() { use dever_core::native::compile_artifact; let directory = TemporaryDirectory::new(); let root = directory.path().join("cache"); let mut temporary = None; let result = compile_artifact(&root, "failed-link", |build, output| { temporary = Some(build.to_path_buf()); fs::write(output, b"incomplete").unwrap(); Err("link rejected".into()) }); assert_eq!(result.err().unwrap(), "link rejected"); assert!(!temporary.unwrap().exists()); assert!(!root.exists()); } #[test] fn workspace_profiles_keep_independent_tests_without_debug_or_incremental_artifacts() { let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); let cargo = fs::read_to_string(workspace.join("Cargo.toml")).unwrap(); for profile in ["dev", "test"] { let section = cargo .split(&format!("[profile.{profile}]")) .nth(1) .unwrap_or_else(|| panic!("missing profile.{profile}")) .split("\n[") .next() .unwrap(); assert!( section.contains("debug = 0"), "profile.{profile} enables debuginfo" ); assert!( section.contains("incremental = false"), "profile.{profile} enables incremental" ); } let release = cargo .split("[profile.release]") .nth(1) .unwrap() .split("\n[") .next() .unwrap(); assert!(release.contains("codegen-units = 1")); assert!(release.contains("lto = \"fat\"")); let tests = fs::read_to_string(workspace.join("test/dever-tests/Cargo.toml")).unwrap(); for target in [ "native_cache", "api_routes", "sqlite_orm", "postgres_orm", "external_component", ] { assert!( tests.contains(&format!("name = \"{target}\"")), "missing test target {target}" ); } assert!( tests.matches("[[test]]").count() >= 60, "test targets were unexpectedly merged" ); } #[test] fn repeated_build_reuses_native_code_and_keeps_owned_output_semantics() { let directory = TemporaryDirectory::new(); let root = directory.path().join("cache"); let started = Instant::now(); let first = compile(HELLO, &root, &rustc()); let first_time = started.elapsed(); assert!(!first.cache_hit()); let started = Instant::now(); let second = compile(HELLO, &root, &rustc()); eprintln!( "native cache: cold_ms={}, hit_ms={}", first_time.as_millis(), started.elapsed().as_millis() ); assert!(second.cache_hit()); assert_ne!(first.executable(), second.executable()); assert_eq!( Command::new(second.executable()).output().unwrap().stdout, b"cache hello\n" ); let output = directory.path().join("saved"); second.save(&output).unwrap(); let original = fs::read(&output).unwrap(); assert_eq!( second.save(&output).unwrap_err().kind(), std::io::ErrorKind::AlreadyExists ); assert_eq!(fs::read(&output).unwrap(), original); let temporary = second.executable().parent().unwrap().to_owned(); drop(second); assert!(!temporary.exists()); for entry in fs::read_dir(&root).unwrap() { let mut files: Vec<_> = fs::read_dir(entry.unwrap().path()) .unwrap() .map(|entry| entry.unwrap().file_name()) .collect(); files.sort(); assert_eq!(files, ["executable-fingerprint", "identity", "program"]); } let modified = HELLO.replace("cache hello", "changed source"); let changed = compile(&modified, &root, &rustc()); assert!(!changed.cache_hit()); assert_eq!( Command::new(changed.executable()).output().unwrap().stdout, b"changed source\n" ); } #[test] fn modified_cached_executables_are_never_run() { let directory = TemporaryDirectory::new(); let root = directory.path().join("cache"); let native = compile(HELLO, &root, &rustc()); let entry = fs::read_dir(&root).unwrap().next().unwrap().unwrap().path(); fs::write(entry.join("program"), "incomplete executable").unwrap(); let sources = support::sources(HELLO); let result = compile_with_cache( &support::checked(&sources), &sources, "main.main", &rustc(), &root, ); let Err(error) = result else { panic!("modified executable must not be reused") }; assert!(error.contains("incomplete or modified"), "{error}"); assert_eq!( Command::new(native.executable()).output().unwrap().stdout, b"cache hello\n" ); } #[test] fn concurrent_publish_never_exposes_partial_artifacts() { let directory = TemporaryDirectory::new(); let root = directory.path().join("cache"); let natives = std::thread::scope(|scope| { let first = scope.spawn(|| compile(HELLO, &root, &rustc())); let second = scope.spawn(|| compile(HELLO, &root, &rustc())); [first.join().unwrap(), second.join().unwrap()] }); for native in natives { assert_eq!( Command::new(native.executable()).output().unwrap().stdout, b"cache hello\n" ); } assert!(compile(HELLO, &root, &rustc()).cache_hit()); assert_eq!(fs::read_dir(root).unwrap().count(), 1); } #[cfg(unix)] #[test] fn changing_compiler_bytes_invalidates_even_when_version_is_unchanged() { use std::os::unix::fs::{PermissionsExt, symlink}; let directory = TemporaryDirectory::new(); let root = directory.path().join("cache"); let sysroot = Command::new(rustc()) .args(["--print", "sysroot"]) .output() .unwrap(); assert!(sysroot.status.success()); let bin = Path::new(std::str::from_utf8(&sysroot.stdout).unwrap().trim()).join("bin"); let wrapper = directory.path().join("rustc"); let quoted = bin .join("rustc") .display() .to_string() .replace('\'', "'\\''"); let script = format!("#!/bin/sh\nexec '{quoted}' \"$@\"\n"); fs::write(&wrapper, &script).unwrap(); fs::set_permissions(&wrapper, fs::Permissions::from_mode(0o700)).unwrap(); symlink(bin.join("cargo"), directory.path().join("cargo")).unwrap(); assert!(!compile(HELLO, &root, wrapper.as_os_str()).cache_hit()); assert!(compile(HELLO, &root, wrapper.as_os_str()).cache_hit()); fs::write(&wrapper, format!("{script}# changed wrapper\n")).unwrap(); assert!(!compile(HELLO, &root, wrapper.as_os_str()).cache_hit()); } #[test] fn runtime_dependencies_toolchain_version_and_options_are_cache_inputs() { let directory = TemporaryDirectory::new(); let runtime = directory.path().join("runtime"); fs::create_dir(&runtime).unwrap(); fs::create_dir(runtime.join("deps")).unwrap(); fs::write(runtime.join("libdever_runtime.rlib"), "runtime version one").unwrap(); fs::write( runtime.join("deps/libdependency.rlib"), "dependency version one", ) .unwrap(); let root = directory.path().join("cache"); fs::create_dir(&root).unwrap(); let program = directory.path().join("program"); fs::write(&program, "test executable bytes").unwrap(); let inputs = cache::RuntimeInputs::read(&runtime).unwrap(); let memoized_inputs = cache::RuntimeInputs::read(&runtime).unwrap(); let original = cache::Cache::new( &root, "generated", &rustc(), b"version one", &inputs, &["opt-level=3"], ) .unwrap(); let staging = directory.path().join("staging"); fs::create_dir(&staging).unwrap(); original.publish(&program, &staging).unwrap(); assert!( original .restore(&directory.path().join("restored")) .unwrap() ); let memoized = cache::Cache::new( &root, "generated", &rustc(), b"version one", &memoized_inputs, &["opt-level=3"], ) .unwrap(); assert!( memoized .restore(&directory.path().join("memoized")) .unwrap() ); for (version, options) in [ (b"version two".as_slice(), "opt-level=3"), (b"version one".as_slice(), "opt-level=2"), ] { let changed = cache::Cache::new(&root, "generated", &rustc(), version, &inputs, &[options]).unwrap(); assert!(!changed.restore(&directory.path().join("absent")).unwrap()); } for path in [ runtime.join("libdever_runtime.rlib"), runtime.join("deps/libdependency.rlib"), ] { fs::write(path, "changed bytes").unwrap(); let changed_inputs = cache::RuntimeInputs::read(&runtime).unwrap(); let changed = cache::Cache::new( &root, "generated", &rustc(), b"version one", &changed_inputs, &["opt-level=3"], ) .unwrap(); assert!(!changed.restore(&directory.path().join("absent")).unwrap()); } let snapshot = directory.path().join("snapshot"); inputs.write(&snapshot).unwrap(); assert_eq!( fs::read_to_string(snapshot.join("libdever_runtime.rlib")).unwrap(), "runtime version one" ); assert_eq!( fs::read_dir(&runtime) .unwrap() .filter_map(Result::ok) .filter(|entry| { entry .file_name() .to_string_lossy() .starts_with(".dever-runtime-identity-") }) .count(), 1 ); } #[test] fn runtime_input_views_are_exact_reused_and_concurrently_published() { let directory = TemporaryDirectory::new(); let source = directory.path().join("cargo/release/deps"); fs::create_dir_all(&source).unwrap(); let runtime = directory.path().join("cargo/release/libdever_runtime.rlib"); fs::write(&runtime, "runtime one").unwrap(); let first_dependency = source.join("libfirst-1.rlib"); let second_dependency = source.join("libsecond-2.rlib"); fs::write(&first_dependency, "first").unwrap(); fs::write(&second_dependency, "second").unwrap(); let inputs = directory.path().join("inputs"); let first = cache::RuntimeInputs::publish(&inputs, &runtime, std::slice::from_ref(&first_dependency)) .unwrap(); fs::remove_file(&runtime).unwrap(); fs::write(&runtime, "runtime two").unwrap(); let second = std::thread::scope(|scope| { let left = scope.spawn(|| { cache::RuntimeInputs::publish( &inputs, &runtime, std::slice::from_ref(&second_dependency), ) .unwrap() }); let right = scope.spawn(|| { cache::RuntimeInputs::publish( &inputs, &runtime, std::slice::from_ref(&second_dependency), ) .unwrap() }); [left.join().unwrap(), right.join().unwrap()] }); assert_eq!(fs::read_dir(&inputs).unwrap().count(), 2); let first_output = directory.path().join("first-output"); first.write(&first_output).unwrap(); assert!(first_output.join("deps/libfirst-1.rlib").is_file()); assert!(!first_output.join("deps/libsecond-2.rlib").exists()); for (index, input) in second.into_iter().enumerate() { let output = directory.path().join(format!("second-output-{index}")); input.write(&output).unwrap(); assert_eq!( fs::read_to_string(output.join("libdever_runtime.rlib")).unwrap(), "runtime two" ); assert!(output.join("deps/libsecond-2.rlib").is_file()); assert!(!output.join("deps/libfirst-1.rlib").exists()); } } #[cfg(unix)] #[test] fn runtime_input_views_reject_unknown_files_and_symbolic_links() { use std::os::unix::fs::symlink; let directory = TemporaryDirectory::new(); let runtime = directory.path().join("runtime"); fs::create_dir(&runtime).unwrap(); fs::create_dir(runtime.join("deps")).unwrap(); fs::write(runtime.join("libdever_runtime.rlib"), "runtime").unwrap(); fs::write(runtime.join("deps/libdependency.rlib"), "dependency").unwrap(); fs::write(runtime.join("deps/unknown.txt"), "unknown").unwrap(); assert!(cache::RuntimeInputs::read(&runtime).is_err()); fs::remove_file(runtime.join("deps/unknown.txt")).unwrap(); fs::remove_file(runtime.join("deps/libdependency.rlib")).unwrap(); let outside = directory.path().join("outside.rlib"); fs::write(&outside, "outside").unwrap(); symlink(&outside, runtime.join("deps/libdependency.rlib")).unwrap(); assert!(cache::RuntimeInputs::read(&runtime).is_err()); } #[test] fn checked_program_cache_preserves_source_identity_and_rechecks_api_baselines() { let original = support::sources(HELLO); let started = Instant::now(); let program = support::checked(&original); let cold = started.elapsed(); let started = Instant::now(); let cached = support::checked(&original); eprintln!( "frontend cache: cold_us={}, hit_us={}", cold.as_micros(), started.elapsed().as_micros() ); assert_eq!(program.api_snapshot(), cached.api_snapshot()); let directory = TemporaryDirectory::new(); let baseline = directory.path().join("dever.api"); cli_api::write(&program, Some(&baseline)).unwrap(); cli_api::check_baseline(&cached, directory.path()).unwrap(); fs::write(&baseline, "changed baseline").unwrap(); assert!(cli_api::check_baseline(&support::checked(&original), directory.path()).is_err()); let mut wrong_path = dever_core::source::SourceMap::default(); wrong_path.add("wrong.dever", HELLO); let wrong_program = support::checked(&wrong_path); assert!(dever_core::native::emit(&wrong_program, &wrong_path, "main.main").is_err()); let changed = support::sources(&HELLO.replace("dever.io.println", "unknown")); let errors = dever_core::check(&changed).unwrap_err(); let repeated_errors = dever_core::check(&changed).unwrap_err(); assert_eq!( errors[0].render(&changed), repeated_errors[0].render(&changed) ); assert!(errors.iter().any(|error| error.code == "C004")); let mut additional = dever_core::source::SourceMap::default(); additional.add("main.dever", HELLO); additional.add("user/account/app.dever", "extra() () {}\n"); fs::write(&baseline, program.api_snapshot()).unwrap(); assert!(cli_api::check_baseline(&support::checked(&additional), directory.path()).is_err()); } #[test] #[ignore = "requires a freshly built local dever-cli binary"] fn actual_cli_checks_baseline_on_check_run_and_cached_build() { let compiler = std::env::current_exe() .unwrap() .parent() .unwrap() .parent() .unwrap() .join(format!("dever{}", std::env::consts::EXE_SUFFIX)); assert!( compiler.is_file(), "build dever-cli before running this test" ); let directory = TemporaryDirectory::new(); let domain = directory.path().join("module/sample/cache"); fs::create_dir_all(&domain).unwrap(); fs::create_dir(directory.path().join("config")).unwrap(); fs::write( domain.join("app.dever"), "hello() (message: Text) { message = \"cache hello\" }\n", ) .unwrap(); fs::write(domain.join("api.dever"), "cmd hello = app.hello\n").unwrap(); fs::write(directory.path().join("config/setting.json"), "{}\n").unwrap(); let baseline = directory.path().join("dever.api"); let api = Command::new(&compiler) .arg("api") .arg(directory.path()) .arg("--output") .arg(&baseline) .output() .unwrap(); assert!( api.status.success(), "{}", String::from_utf8_lossy(&api.stderr) ); let check = Command::new(&compiler) .arg("check") .arg(directory.path()) .output() .unwrap(); assert!( check.status.success(), "{}", String::from_utf8_lossy(&check.stderr) ); let run = Command::new(&compiler) .arg("run") .arg(directory.path()) .args(["--", "sample.cache.hello", "{}"]) .output() .unwrap(); assert!( run.status.success(), "{}", String::from_utf8_lossy(&run.stderr) ); let expected = serde_json::json!({"code": 0, "message": "ok", "data": "cache hello"}); assert_eq!( serde_json::from_slice::(&run.stdout).unwrap(), expected ); let saved = directory.path().join("saved"); let build = Command::new(&compiler) .arg("build") .arg(directory.path()) .arg("--output") .arg(&saved) .output() .unwrap(); assert!( build.status.success(), "{}", String::from_utf8_lossy(&build.stderr) ); let standalone = Command::new(saved) .current_dir(directory.path()) .args(["sample.cache.hello", "{}"]) .output() .unwrap(); assert!( standalone.status.success(), "{}", String::from_utf8_lossy(&standalone.stderr) ); assert_eq!( serde_json::from_slice::(&standalone.stdout).unwrap(), expected ); fs::write(baseline, "changed baseline").unwrap(); let blocked_output = directory.path().join("blocked"); for action in ["check", "run", "build"] { let mut command = Command::new(&compiler); command.arg(action).arg(directory.path()); if action == "build" { command.arg("--output").arg(&blocked_output); } else if action == "run" { command.args(["--", "sample.cache.hello", "{}"]); } let output = command.output().unwrap(); assert!( !output.status.success(), "{action} skipped changed baseline" ); assert!( String::from_utf8_lossy(&output.stderr).contains("API baseline"), "{}", String::from_utf8_lossy(&output.stderr) ); assert!(output.stdout.is_empty()); } assert!(!blocked_output.exists()); }