mod support; use dever_core::source::SourceMap; use support::{checked, rejected, sources}; #[path = "../../../crates/dever-cli/src/api.rs"] mod cli_api; #[test] fn application_exports_are_checked_only_with_a_known_entry() { let program = checked(&sources( "public main() (answer: Int) { answer = helper() }\npublic helper() (answer: Int) { answer = 1 }\npublic unused() () {}", )); let warnings = program.application_warnings("main.main").unwrap(); assert_eq!(warnings.len(), 2); assert!(warnings.iter().all(|warning| warning.code == "W007")); assert!(program.application_warnings("app.missing").is_err()); let mut map = SourceMap::default(); map.add( "main.dever", "public main() (answer: Int) { answer = utility.read() }", ); map.add( "utility.dever", "public read() (answer: Int) { answer = 1 }", ); let cross_package_program = checked(&map); assert!( cross_package_program .application_warnings("main.main") .unwrap() .is_empty() ); assert!( !cross_package_program .api_snapshot() .contains("package app depends") ); } #[test] fn cli_baseline_is_explicit_never_overwrites_and_blocks_drift() { let directory = support::temp::TemporaryDirectory::new(); let path = directory.path().join("dever.api"); let original = checked(&sources("public main() (answer: Int) { answer = 1 }")); assert!( original .check_api( &original .api_snapshot() .replace("dever-api 5", "dever-api 4") ) .unwrap_err() .contains("expected dever-api 5") ); cli_api::check_baseline(&original, directory.path()).unwrap(); assert!(!path.exists()); cli_api::write(&original, Some(&path)).unwrap(); cli_api::check_baseline(&original, directory.path()).unwrap(); assert!(cli_api::write(&original, Some(&path)).is_err()); let expanded = checked(&sources( "public main() (answer: Int) { answer = 1 }\npublic extra() () {}", )); assert!( cli_api::check_baseline(&expanded, directory.path()) .unwrap_err() .contains("+ function main.extra") ); assert_eq!( std::fs::read_to_string(path).unwrap(), original.api_snapshot() ); } #[test] fn effects_follow_named_and_forwarded_handlers_without_tainting_pure_calls() { let shared = r#" apply(route: handler(value: Int) (answer: Int), value: Int) (answer: Int) { answer = route(value) } forward(route: handler(value: Int) (answer: Int), value: Int) (answer: Int) { answer = apply(route, value) } identity(value: Int) (answer: Int) pure { answer = value } noisy(value: Int) (answer: Int) { dever.io.println("called") answer = value } "#; checked(&sources(&format!( "{shared}\nmain() (answer: Int) pure {{ answer = forward(identity, 1) }}" ))); rejected( &format!("{shared}\nmain() (answer: Int) pure {{ answer = forward(noisy, 1) }}"), "has effects: stdout", ); rejected( "apply(route: handler(value: Int) (answer: Int), value: Int) (answer: Int) pure { answer = route(value) }", "has effects: handler", ); checked(&sources(&format!( "{shared}\nignore(route: handler(value: Int) (answer: Int), value: Int) (answer: Int) {{ answer = value }}\nmain() (answer: Int) pure {{ answer = ignore(noisy, 1) }}" ))); let collection = r#" map_values(route: handler(value: Int) (answer: Int), values: List) (answers: List) { answers = each(route, values) } identity(value: Int) (answer: Int) pure { answer = value } noisy(value: Int) (answer: Int) { dever.io.println("called") answer = value } "#; checked(&sources(&format!( "{collection}\nmain() (answers: List) pure {{ answers = map_values(identity, [1]) }}" ))); rejected( &format!( "{collection}\nmain() (answers: List) pure {{ answers = map_values(noisy, [1]) }}" ), "has effects: stdout", ); } #[test] fn pure_arithmetic_and_text_are_allowed_but_resource_and_clock_effects_are_rejected() { checked(&sources( "public main() (answer: Text) pure { answer = text.trim(\" x \") }", )); rejected( "clock() (result: Int) pure { result = dever.time.unix_millis() }", "has effects: time", ); rejected( "close_stream(stream: Stream) () pure { close(stream) }", "has effects: stream", ); } #[test] fn api_snapshot_detects_expansion_and_effect_changes_but_ignores_private_helpers() { let original = checked(&sources("public main() (answer: Int) { answer = 1 }")); let baseline = original.api_snapshot(); assert!(baseline.contains("function main.main")); assert!(!baseline.contains("function dever.")); original.check_api(&baseline).unwrap(); let helper = checked(&sources( "public main() (answer: Int) { answer = helper() }\nhelper() (answer: Int) { answer = 2 }", )); helper.check_api(&baseline).unwrap(); let expanded = checked(&sources( "public main() (answer: Int) { answer = 1 }\npublic helper() (answer: Int) { answer = 2 }", )); let difference = expanded.check_api(&baseline).unwrap_err(); assert!(difference.contains("+ function main.helper")); let noisy = checked(&sources( "public main() (answer: Int) { dever.io.println(\"x\")\nanswer = 1 }", )); assert!( noisy .check_api(&baseline) .unwrap_err() .contains("effects=[stdout]") ); } #[test] fn api_includes_constraints_and_private_construction_without_internal_representation() { let program = checked(&sources( "public type Port { value: Int >= 1 and <= 65535\nprivate secret: Text }", )); let snapshot = program.api_snapshot(); assert!(snapshot.contains("construct=private")); assert!(snapshot.contains("1..65535")); assert!(!snapshot.contains("secret")); } #[test] fn api_tracks_private_transitive_recovery_without_exposing_helper_names() { let original = checked(&sources("public main() (answer: Int) pure { answer = 1 }")); let changed = checked(&sources( "public main() (answer: Int) pure { answer = bridge() }\nbridge() (answer: Int) pure { answer = fallback() }\nfallback() (answer: Int) pure recover(\"missing configuration uses default\") { answer = 1 }", )); let snapshot = changed.api_snapshot(); assert!(snapshot.contains("missing configuration uses default")); assert!(!snapshot.contains("bridge")); assert!(!snapshot.contains("fallback")); assert!(changed.check_api(&original.api_snapshot()).is_err()); assert!( snapshot.contains("effects=[]"), "recovery itself is not an external effect" ); } #[test] fn recovery_through_handlers_only_propagates_when_the_handler_is_invoked() { let helpers = "fallback(value: Int) (answer: Int) recover(\"use default\") { answer = value }\napply(route: handler(value: Int) (answer: Int), value: Int) (answer: Int) { answer = route(value) }\nignore(route: handler(value: Int) (answer: Int), value: Int) (answer: Int) { answer = value }\n"; let invoked = checked(&sources(&format!( "{helpers}public main() (answer: Int) pure {{ answer = apply(fallback, 1) }}" ))); assert!(invoked.api_snapshot().contains("use default")); let ignored = checked(&sources(&format!( "{helpers}public main() (answer: Int) pure {{ answer = ignore(fallback, 1) }}" ))); assert!(!ignored.api_snapshot().contains("use default")); } #[test] fn effect_work_queue_preserves_long_chains_and_unused_handler_cycles() { let mut source = String::from("public main() () { step0() }\n"); for level in 0..64 { source.push_str(&format!("step{level}() () {{ step{}() }}\n", level + 1)); } source .push_str("step64() () recover(\"report optional event\") { dever.io.println(\"event\") }"); let snapshot = checked(&sources(&source)).api_snapshot(); assert!(snapshot.contains("effects=[stdout]"), "{snapshot}"); assert!(snapshot.contains("report optional event"), "{snapshot}"); rejected( &source.replace("main() () {", "main() () pure {"), "has effects: stdout", ); let cycle = "public main() () pure { bridge() }\nbridge() () { ignore(bridge) }\nignore(route: handler() ()) () recover(\"skip optional callback\") {}"; let snapshot = checked(&sources(cycle)).api_snapshot(); assert!(snapshot.contains("effects=[]"), "{snapshot}"); assert!(snapshot.contains("skip optional callback"), "{snapshot}"); let unused = cycle.replace( "bridge() () { ignore(bridge) }", "bridge() () { ignore(bridge)\nignore(noisy) }\nnoisy() () recover(\"unused recovery\") { dever.io.println(\"unused\") }", ); let snapshot = checked(&sources(&unused)).api_snapshot(); assert!(snapshot.contains("effects=[]"), "{snapshot}"); assert!(!snapshot.contains("unused recovery"), "{snapshot}"); }