plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-core/tests/loader_test.rs
| 3d6f280 | 1 | #![allow(non_snake_case)] |
| 3d6f280 | 2 | use plum_core::loadAndMerge; |
| 8a7f6ad | 3 | use std::fs; |
| 8a7f6ad | 4 | use tempfile::TempDir; |
| 8a7f6ad | 5 | |
| 8a7f6ad | 6 | /// Writes `name` (without a directory prefix — always placed directly under |
| 8a7f6ad | 7 | /// the temp dir's `lib_path` subdirectory) with `contents`, creating parent |
| 8a7f6ad | 8 | /// directories as needed (so `"std/option"` works). |
| 3d6f280 | 9 | fn writeLibFile(lib_path: &std::path::Path, name: &str, contents: &str) -> std::path::PathBuf { |
| 8a7f6ad | 10 | let path = lib_path.join(format!("{}.plum", name)); |
| 8a7f6ad | 11 | fs::create_dir_all(path.parent().unwrap()).unwrap(); |
| 8a7f6ad | 12 | fs::write(&path, contents).unwrap(); |
| 8a7f6ad | 13 | path |
| 8a7f6ad | 14 | } |
| 8a7f6ad | 15 | |
| 8a7f6ad | 16 | #[test] |
| 3d6f280 | 17 | fn importSeesImportedDeclarations() { |
| 8a7f6ad | 18 | let dir = TempDir::new().unwrap(); |
| 8a7f6ad | 19 | let lib_path = dir.path().join("libs"); |
| 3d6f280 | 20 | writeLibFile(&lib_path, "helper", "module fixtures\n\nfun helperFn() -> Int =\n 42\n"); |
| 3d6f280 | 21 | let entry = writeLibFile(&lib_path, "main", "module fixtures\n\nimport helper\n\nfun main() -> Int =\n helperFn()\n"); |
| 8a7f6ad | 22 | |
| 3d6f280 | 23 | let merged = loadAndMerge(&entry, &lib_path).expect("loadAndMerge failed"); |
| 8a7f6ad | 24 | let names: Vec<&str> = merged.items.iter().filter_map(|i| match i { |
| 8a7f6ad | 25 | plum_core::ast::Item::Fn(f) => Some(f.name.as_str()), |
| 8a7f6ad | 26 | _ => None, |
| 8a7f6ad | 27 | }).collect(); |
| 8a7f6ad | 28 | assert!(names.contains(&"main"), "expected 'main' in merged items, got {:?}", names); |
| 8a7f6ad | 29 | assert!(names.contains(&"helperFn"), "expected 'helperFn' in merged items, got {:?}", names); |
| 8a7f6ad | 30 | } |
| 8a7f6ad | 31 | |
| 8a7f6ad | 32 | #[test] |
| 3d6f280 | 33 | fn transitiveImportSurfacesGrandchildDeclarations() { |
| 8a7f6ad | 34 | let dir = TempDir::new().unwrap(); |
| 8a7f6ad | 35 | let lib_path = dir.path().join("libs"); |
| 3d6f280 | 36 | writeLibFile(&lib_path, "c", "module fixtures\n\nfun cFn() -> Int =\n 3\n"); |
| 3d6f280 | 37 | writeLibFile(&lib_path, "b", "module fixtures\n\nimport c\n\nfun bFn() -> Int =\n cFn()\n"); |
| 3d6f280 | 38 | let entry = writeLibFile(&lib_path, "a", "module fixtures\n\nimport b\n\nfun main() -> Int =\n bFn()\n"); |
| 8a7f6ad | 39 | |
| 3d6f280 | 40 | let merged = loadAndMerge(&entry, &lib_path).expect("loadAndMerge failed"); |
| 8a7f6ad | 41 | let names: Vec<&str> = merged.items.iter().filter_map(|i| match i { |
| 8a7f6ad | 42 | plum_core::ast::Item::Fn(f) => Some(f.name.as_str()), |
| 8a7f6ad | 43 | _ => None, |
| 8a7f6ad | 44 | }).collect(); |
| 8a7f6ad | 45 | assert!(names.contains(&"cFn"), "expected transitively-imported 'cFn', got {:?}", names); |
| 8a7f6ad | 46 | } |
| 8a7f6ad | 47 | |
| 8a7f6ad | 48 | #[test] |
| 3d6f280 | 49 | fn diamondImportIncludesSharedDependencyOnce() { |
| 8a7f6ad | 50 | let dir = TempDir::new().unwrap(); |
| 8a7f6ad | 51 | let lib_path = dir.path().join("libs"); |
| 3d6f280 | 52 | writeLibFile(&lib_path, "d", "module fixtures\n\nfun dFn() -> Int =\n 4\n"); |
| 3d6f280 | 53 | writeLibFile(&lib_path, "b", "module fixtures\n\nimport d\n\nfun bFn() -> Int =\n dFn()\n"); |
| 3d6f280 | 54 | writeLibFile(&lib_path, "c", "module fixtures\n\nimport d\n\nfun cFn() -> Int =\n dFn()\n"); |
| 3d6f280 | 55 | let entry = writeLibFile(&lib_path, "a", "module fixtures\n\nimport b\nimport c\n\nfun main() -> Int =\n bFn() + cFn()\n"); |
| 8a7f6ad | 56 | |
| 3d6f280 | 57 | let merged = loadAndMerge(&entry, &lib_path).expect("loadAndMerge failed"); |
| 8a7f6ad | 58 | let d_count = merged.items.iter().filter(|i| matches!(i, plum_core::ast::Item::Fn(f) if f.name == "dFn")).count(); |
| 8a7f6ad | 59 | assert_eq!(d_count, 1, "expected 'dFn' exactly once in a diamond import, got {}", d_count); |
| 8a7f6ad | 60 | } |
| 8a7f6ad | 61 | |
| 8a7f6ad | 62 | #[test] |
| 3d6f280 | 63 | fn importCycleResolvesWithoutHangingOrErroring() { |
| 8a7f6ad | 64 | let dir = TempDir::new().unwrap(); |
| 8a7f6ad | 65 | let lib_path = dir.path().join("libs"); |
| 3d6f280 | 66 | writeLibFile(&lib_path, "b", "module fixtures\n\nimport a\n\nfun bFn() -> Int =\n 1\n"); |
| 3d6f280 | 67 | let entry = writeLibFile(&lib_path, "a", "module fixtures\n\nimport b\n\nfun main() -> Int =\n bFn()\n"); |
| 8a7f6ad | 68 | |
| 3d6f280 | 69 | let merged = loadAndMerge(&entry, &lib_path).expect("import cycle should resolve, not error"); |
| 8a7f6ad | 70 | let names: Vec<&str> = merged.items.iter().filter_map(|i| match i { |
| 8a7f6ad | 71 | plum_core::ast::Item::Fn(f) => Some(f.name.as_str()), |
| 8a7f6ad | 72 | _ => None, |
| 8a7f6ad | 73 | }).collect(); |
| 8a7f6ad | 74 | assert!(names.contains(&"main")); |
| 8a7f6ad | 75 | assert!(names.contains(&"bFn")); |
| 8a7f6ad | 76 | } |
| 8a7f6ad | 77 | |
| 8a7f6ad | 78 | #[test] |
| 3d6f280 | 79 | fn unresolvableImportIsAClearError() { |
| 8a7f6ad | 80 | let dir = TempDir::new().unwrap(); |
| 8a7f6ad | 81 | let lib_path = dir.path().join("libs"); |
| 3d6f280 | 82 | let entry = writeLibFile(&lib_path, "main", "module fixtures\n\nimport does_not_exist\n\nfun main() -> Int =\n 0\n"); |
| 8a7f6ad | 83 | |
| 3d6f280 | 84 | let result = loadAndMerge(&entry, &lib_path); |
| 8a7f6ad | 85 | assert!(result.is_err()); |
| 8a7f6ad | 86 | let msg = result.unwrap_err(); |
| 8a7f6ad | 87 | assert!(msg.contains("does_not_exist"), "expected error to name the missing import, got: {}", msg); |
| 8a7f6ad | 88 | } |
| 8a7f6ad | 89 | |
| 8a7f6ad | 90 | #[test] |
| 3d6f280 | 91 | fn duplicateTopLevelNameAcrossFilesIsAClearError() { |
| 8a7f6ad | 92 | let dir = TempDir::new().unwrap(); |
| 8a7f6ad | 93 | let lib_path = dir.path().join("libs"); |
| 3d6f280 | 94 | writeLibFile(&lib_path, "helper", "module fixtures\n\nfun sharedFn() -> Int =\n 1\n"); |
| 3d6f280 | 95 | let entry = writeLibFile(&lib_path, "main", "module fixtures\n\nimport helper\n\nfun sharedFn() -> Int =\n 2\n"); |
| 8a7f6ad | 96 | |
| 3d6f280 | 97 | let result = loadAndMerge(&entry, &lib_path); |
| 8a7f6ad | 98 | assert!(result.is_err()); |
| 8a7f6ad | 99 | let msg = result.unwrap_err(); |
| 8a7f6ad | 100 | assert!(msg.contains("sharedFn"), "expected error to name the duplicate 'sharedFn', got: {}", msg); |
| 8a7f6ad | 101 | } |
| 8a7f6ad | 102 | |
| 8a7f6ad | 103 | #[test] |
| 3d6f280 | 104 | fn sameMethodNameOnDifferentReceiversAcrossFilesIsNotACollision() { |
| 8a7f6ad | 105 | let dir = TempDir::new().unwrap(); |
| 8a7f6ad | 106 | let lib_path = dir.path().join("libs"); |
| 3d6f280 | 107 | writeLibFile(&lib_path, "cat", "\ |
| 8a7f6ad | 108 | module fixtures |
| 8a7f6ad | 109 | |
| 8a7f6ad | 110 | type Cat = |
| 8a7f6ad | 111 | age: Int |
| 8a7f6ad | 112 | |
| 2b8194c | 113 | fun length(self) -> Int = |
| 2b8194c | 114 | self.age |
| 8a7f6ad | 115 | "); |
| 3d6f280 | 116 | let entry = writeLibFile(&lib_path, "main", "\ |
| 8a7f6ad | 117 | module fixtures |
| 8a7f6ad | 118 | |
| 8a7f6ad | 119 | import cat |
| 8a7f6ad | 120 | |
| 8a7f6ad | 121 | type Box = |
| 8a7f6ad | 122 | items: Int |
| 8a7f6ad | 123 | |
| 2b8194c | 124 | fun length(self) -> Int = |
| 2b8194c | 125 | self.items |
| 8a7f6ad | 126 | |
| b48d3a3 | 127 | fun main() -> Int = |
| 8a7f6ad | 128 | 0 |
| 8a7f6ad | 129 | "); |
| 8a7f6ad | 130 | |
| 3d6f280 | 131 | let merged = loadAndMerge(&entry, &lib_path).expect("same method name on different receivers should not collide"); |
| 8a7f6ad | 132 | let method_count = merged.items.iter().filter(|i| matches!(i, plum_core::ast::Item::Fn(f) if f.name == "length")).count(); |
| 8a7f6ad | 133 | assert_eq!(method_count, 2, "expected both 'length' methods to survive the merge, got {}", method_count); |
| 8a7f6ad | 134 | } |