char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
1069809
main
1use std:: path:: Path ; 2use std:: process::{ Child , Command , Output , Stdio }; 3use std:: time::{ Duration , Instant }; 4 5# [ test ] 6fn task_module_exports_only_run_tasks_with_stable_function_metadata () { 7let dir =project ( & [ 8( "avril.conf" , "name: \"fixture\"\n" ), 9( 10"module.ts" , 11r#" 12import * as task from "avril:task"; 13const exports = Object.keys(task); 14if (exports.length !== 1 || exports[0] !== "runTasks") { 15throw new Error(`unexpected task exports: ${exports.join(",")}`); 16} 17if (task.runTasks.name !== "runTasks" || task.runTasks.length !== 1) { 18throw new Error(`unexpected runTasks metadata: ${task.runTasks.name}/${task.runTasks.length}`); 19} 20"# , 21), 22]); 23let output =avril ( dir. path (), & [ "run" , "module.ts" ]); 24assert! ( output. status . success (), "{}" , stderr ( & output)); 25} 26 27fn project ( files : & [( & str , & str )]) -> tempfile:: TempDir { 28let dir = tempfile:: tempdir (). unwrap (); 29for ( name, contents) in files{ 30let path = dir. path (). join ( name); 31 std:: fs:: create_dir_all ( path. parent (). unwrap ()). unwrap (); 32 std:: fs:: write ( path, contents). unwrap (); 33} 34 dir 35} 36 37fn avril ( dir : & Path , args : & [ & str ]) ->Output { 38Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 39. current_dir ( dir) 40. args ( args) 41. output () 42. unwrap () 43} 44 45fn stdout ( output : & Output ) ->String { 46String :: from_utf8 ( output. stdout . clone ()). unwrap () 47} 48 49fn stderr ( output : & Output ) ->String { 50String :: from_utf8 ( output. stderr . clone ()). unwrap () 51} 52 53const BUILD : & str =r#" 54import { runTasks } from "avril:task"; 55import { readTextFile, writeTextFile, mkdirs } from "avril:fs"; 56 57const report = await runTasks({ 58page: { 59files: ["input.txt"], 60output: ["dist/page.html"], 61run: async () => { 62const count = await readTextFile("count.txt").catch(() => ""); 63await writeTextFile("count.txt", count + "x"); 64await mkdirs("dist"); 65await writeTextFile("dist/page.html", "page\n"); 66}, 67}, 68shout: { 69dependencies: ["page"], 70command: ["cat", "dist/page.html"], 71}, 72fail: { command: ["false"] }, 73maybe: { 74dependencies: ["page"], 75run: (ctx) => (ctx.args.includes("fail") ? 7 : 0), 76}, 77all: { dependencies: ["shout"] }, 78}); 79console.log(`code=${report.code} completed=${report.completed.join(",")} cached=${report.cached.join(",")}`); 80if (report.code !== 0) throw new Error("build failed"); 81"# ; 82 83# [ test ] 84fn function_and_argv_tasks_run_and_cache () { 85let dir =project ( & [ 86( "build.ts" , BUILD ), 87( "input.txt" , "seed\n" ), 88( "avril.conf" , "name: \"fixture\"\n" ), 89]); 90let first =avril ( dir. path (), & [ "run" , "build.ts" ]); 91assert! ( first. status . success (), "{}" , stderr ( & first)); 92assert! ( stdout ( & first). contains ( "code=0" ), "{}" , stdout ( & first)); 93assert_eq! ( 94 std:: fs:: read_to_string ( dir. path (). join ( "dist/page.html" )). unwrap (), 95"page\n" 96); 97 98let second =avril ( dir. path (), & [ "run" , "build.ts" ]); 99assert! ( second. status . success (), "{}" , stderr ( & second)); 100assert! ( 101stdout ( & second). contains ( "cached=page" ), 102"{}" , 103stdout ( & second) 104); 105// The function task's body ran exactly once across both builds. 106assert_eq! ( 107 std:: fs:: read_to_string ( dir. path (). join ( "count.txt" )). unwrap (), 108"x" 109); 110} 111 112# [ test ] 113fn failed_tasks_are_reported_and_throw_if_failed_exits_nonzero () { 114let dir =project ( & [ 115( "avril.conf" , "name: \"fixture\"\n" ), 116( 117"fail.ts" , 118r#" 119import { runTasks } from "avril:task"; 120const report = await runTasks({ 121bad: { command: ["false"] }, 122ok: { command: ["true"] }, 123all: { dependencies: ["bad", "ok"], parallel: true }, 124}); 125console.log(`code=${report.code} failed=${report.failed.map((f) => f.name).join(",")}`); 126report.throwIfFailed(); 127"# , 128), 129]); 130let output =avril ( dir. path (), & [ "run" , "fail.ts" ]); 131assert! ( !output. status . success (), "{}" , stdout ( & output)); 132assert! ( 133stdout ( & output). contains ( "code=1 failed=bad" ), 134"{}" , 135stdout ( & output) 136); 137} 138 139struct Guard ( Child ); 140 141impl Drop for Guard { 142fn drop ( & mut self ) { 143let _ =self . 0 . kill (); 144let _ =self . 0 . wait (); 145} 146} 147 148fn wait_for ( path : & Path , deadline : Duration ) ->bool { 149let start =Instant :: now (); 150while start. elapsed () < deadline{ 151if path. exists () { 152return true ; 153} 154 std:: thread:: sleep ( Duration :: from_millis ( 100 )); 155} 156false 157} 158 159# [ test ] 160fn watch_reevaluates_the_graph_and_renders_new_files () { 161let dir =project ( & [ 162( "avril.conf" , "name: \"fixture\"\n" ), 163( "posts/one.md" , "# One\n" ), 164( 165"watch.ts" , 166r#" 167import { runTasks } from "avril:task"; 168import { readDir } from "avril:std"; 169import { writeTextFile, mkdirs } from "avril:fs"; 170 171await runTasks(async () => { 172const names = []; 173for await (const entry of readDir("posts")) { 174if (entry.isFile && entry.name.endsWith(".md")) names.push(entry.name); 175} 176names.sort(); 177await mkdirs("dist"); 178return { 179...Object.fromEntries(names.map((name) => [`post:${name}`, { 180files: [`posts/${name}`], 181output: [`dist/${name}.html`], 182run: async () => { await writeTextFile(`dist/${name}.html`, `rendered ${name}\n`); }, 183}])), 184all: { dependencies: names.map((name) => `post:${name}`) }, 185}; 186}, { target: "all", watch: true }); 187"# , 188), 189]); 190let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 191. current_dir ( dir. path ()) 192. args ([ "run" , "watch.ts" ]) 193. stdout ( Stdio :: piped ()) 194. stderr ( Stdio :: piped ()) 195. spawn () 196. unwrap (); 197let _guard =Guard ( child); 198 199assert! ( 200wait_for ( 201& dir. path (). join ( "dist/one.md.html" ), 202Duration :: from_secs ( 20 ) 203), 204"initial render did not happen" 205); 206 std:: fs:: write ( dir. path (). join ( "posts/two.md" ), "# Two\n" ). unwrap (); 207assert! ( 208wait_for ( 209& dir. path (). join ( "dist/two.md.html" ), 210Duration :: from_secs ( 20 ) 211), 212"watch did not render the new post" 213); 214let mut guard = _guard; 215let _ = guard. 0 . kill (); 216let _ = guard. 0 . wait (); 217} 218 219# [ test ] 220fn graph_errors_throw () { 221let dir =project ( & [ 222( "avril.conf" , "name: \"fixture\"\n" ), 223( 224"cycle.ts" , 225r#" 226import { runTasks } from "avril:task"; 227await runTasks({ 228a: { dependencies: ["b"], command: "true" }, 229b: { dependencies: ["a"], command: "true" }, 230all: { dependencies: ["a"] }, 231}); 232"# , 233), 234]); 235let output =avril ( dir. path (), & [ "run" , "cycle.ts" ]); 236assert! ( !output. status . success ()); 237let text =stderr ( & output); 238assert! ( text. contains ( "Task cycle detected" ), "{text}" ); 239}