char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
7f4058a
main
1use std:: collections:: BTreeMap ; 2use std:: path::{ Path , PathBuf }; 3use std:: process:: Command ; 4 5use avril_task::{ Action , Cache , Execution , Graph , Report , RunOptions , Step , Task }; 6 7fn task ( action : Action , dependencies : & [ & str ]) ->Task { 8Task { 9 action, 10dependencies : dependencies. iter (). map ( |d| d. to_string ()). collect (), 11 ..Task :: default () 12} 13} 14 15fn shell ( command : & str , dependencies : & [ & str ]) ->Task { 16task ( Action :: Shell ( command. into ()), dependencies) 17} 18 19fn graph ( tasks : & [( & str , Task )]) ->Graph { 20Graph { 21tasks : tasks 22. iter () 23. map ( |( name, task) |( name. to_string (), task. clone ())) 24. collect (), 25} 26} 27 28/// A minimal driver, standing in for the CLI and ext drivers. 29fn run ( root : & Path , graph : Graph , target : & str , cache : bool ) ->Report { 30let mut options =RunOptions :: new ( root. to_path_buf ()); 31if cache{ 32 options. cache =Some ( Cache :: new ( root. join ( "cache" ))); 33} 34let mut execution =Execution :: new ( graph, target, options). unwrap (); 35loop { 36match execution. step (). unwrap () { 37Step :: Done ( report) =>return report, 38Step :: Ready ( batch) =>{ 39for readyin batch{ 40let code =match & ready. action { 41Action :: Shell ( command) =>Command :: new ( "sh" ) 42. args ([ "-c" , command]) 43. current_dir ( root) 44. envs ( & ready. env ) 45. status () 46. unwrap () 47. code () 48. unwrap_or ( 1 ) as u8 , 49Action :: Argv ( argv) =>Command :: new ( & argv[ 0 ]) 50. args ( & argv[ 1 ..]) 51. current_dir ( root) 52. envs ( & ready. env ) 53. status () 54. unwrap () 55. code () 56. unwrap_or ( 1 ) as u8 , 57 other =>panic! ( "test driver cannot run {other:?}" ), 58}; 59 execution. finish ( & ready. name , code); 60} 61} 62} 63} 64} 65 66fn read ( root : & Path , name : & str ) ->String { 67 std:: fs:: read_to_string ( root. join ( name)). unwrap_or_default () 68} 69 70# [ test ] 71fn dependencies_run_before_dependents_and_diamonds_run_once () { 72let dir = tempfile:: tempdir (). unwrap (); 73let root = dir. path (); 74let g =graph ( & [ 75( "all" , task ( Action :: None , & [ "a" , "b" ])), 76( "a" , shell ( "printf a >> log; printf x >> count" , & [ "base" ])), 77( "b" , shell ( "printf b >> log; printf x >> count" , & [ "base" ])), 78( "base" , shell ( "printf . >> log; printf x >> count" , & [])), 79]); 80let report =run ( root, g, "all" , false ); 81assert_eq! ( report. code , 0 , "{report:?}" ); 82assert_eq! ( read ( root, "log" ), ".ab" ); 83// base ran once despite being a dependency of both a and b. 84assert_eq! ( read ( root, "count" ), "xxx" ); 85} 86 87# [ test ] 88fn sequential_failure_propagates_its_code_and_stops_the_chain () { 89let dir = tempfile:: tempdir (). unwrap (); 90let root = dir. path (); 91let g =graph ( & [ 92( "all" , task ( Action :: None , & [ "bad" , "after" ])), 93( "bad" , shell ( "exit 3" , & [])), 94( "after" , shell ( "printf nope >> log" , & [])), 95]); 96let report =run ( root, g, "all" , false ); 97assert_eq! ( report. code , 3 , "{report:?}" ); 98assert_eq! ( read ( root, "log" ), "" ); 99} 100 101# [ test ] 102fn parallel_failure_collapses_to_one_and_skips_the_dependent_action () { 103let dir = tempfile:: tempdir (). unwrap (); 104let root = dir. path (); 105let mut all =task ( Action :: Shell ( "printf nope >> log" . into ()), & [ "bad" , "ok" ]); 106 all. parallel =true ; 107let g =graph ( & [ 108( "all" , all), 109( "bad" , shell ( "exit 5" , & [])), 110( "ok" , shell ( "printf ok >> log" , & [])), 111]); 112let report =run ( root, g, "all" , false ); 113assert_eq! ( report. code , 1 , "{report:?}" ); 114assert_eq! ( read ( root, "log" ), "ok" ); 115assert_eq! ( report. failed . len (), 1 ); 116assert_eq! ( report. failed [ 0 ]. name , "bad" ); 117} 118 119# [ test ] 120fn cached_tasks_skip_until_an_output_disappears () { 121let dir = tempfile:: tempdir (). unwrap (); 122let root = dir. path (); 123 std:: fs:: write ( root. join ( "in.txt" ), "input" ). unwrap (); 124let make = ||{ 125let mut t =shell ( "printf run >> log; printf out > out.txt" , & []); 126 t. files =Some ( vec! [ "in.txt" . into ()]); 127 t. output =vec! [ "out.txt" . into ()]; 128graph ( & [( "build" , t)]) 129}; 130let first =run ( root, make (), "build" , true ); 131assert_eq! ( first. code , 0 ); 132let second =run ( root, make (), "build" , true ); 133assert_eq! ( second. cached , [ "build" ]); 134assert_eq! ( read ( root, "log" ), "run" ); 135 std:: fs:: remove_file ( root. join ( "out.txt" )). unwrap (); 136let third =run ( root, make (), "build" , true ); 137assert! ( third. cached . is_empty ()); 138assert_eq! ( read ( root, "log" ), "runrun" ); 139} 140 141# [ test ] 142fn cycles_and_unknown_tasks_are_errors () { 143let dir = tempfile:: tempdir (). unwrap (); 144let root = dir. path (); 145let cyclic =graph ( & [ 146( "a" , task ( Action :: None , & [ "b" ])), 147( "b" , task ( Action :: None , & [ "a" ])), 148]); 149let mut options =RunOptions :: new ( root. to_path_buf ()); 150 options. cache =None ; 151match Execution :: new ( cyclic, "a" , options) { 152Ok ( mut execution) =>{ 153let error = execution. step (). unwrap_err (). to_string (); 154assert! ( 155 error. contains ( "Task cycle detected: a -> b -> a" ), 156"{error}" 157); 158} 159Err ( error) =>{ 160assert! ( error. to_string (). contains ( "cycle" ), "{error}" ); 161} 162} 163 164let missing =graph ( & [( "a" , task ( Action :: None , & [ "nope" ]))]); 165let mut options =RunOptions :: new ( root. to_path_buf ()); 166 options. cache =None ; 167let mut execution =Execution :: new ( missing, "a" , options). unwrap (); 168let error = execution. step (). unwrap_err (). to_string (); 169assert! ( error. contains ( "Unknown task \"nope\"" ), "{error}" ); 170} 171 172# [ test ] 173fn argv_actions_pass_arguments_without_a_shell () { 174let dir = tempfile:: tempdir (). unwrap (); 175let root = dir. path (); 176let g =graph ( & [( 177"check" , 178task ( 179Action :: Argv ( vec! [ "test" . into (), "a b" . into (), "=" . into (), "a b" . into ()]), 180& [], 181), 182)]); 183let report =run ( root, g, "check" , false ); 184assert_eq! ( report. code , 0 , "{report:?}" ); 185} 186 187# [ test ] 188fn env_files_feed_the_task_environment () { 189let dir = tempfile:: tempdir (). unwrap (); 190let root = dir. path (); 191 std:: fs:: write ( root. join ( ".env" ), "GREETING=hello\n" ). unwrap (); 192let mut t =shell ( "printf %s \"$GREETING\" > out.txt" , & []); 193 t. env_file =Some ( PathBuf :: from ( ".env" )); 194 t. files =Some ( vec! []); 195// `files` must be non-empty to cache in avril.conf, but the engine only 196// requires the patterns to match; use the env file itself as input. 197 t. files =Some ( vec! [ ".env" . into ()]); 198 t. output =vec! [ "out.txt" . into ()]; 199let report =run ( root, graph ( & [( "build" , t)]), "build" , true ); 200assert_eq! ( report. code , 0 , "{report:?}" ); 201assert_eq! ( read ( root, "out.txt" ), "hello" ); 202} 203 204# [ test ] 205fn fingerprints_change_with_inputs () { 206let dir = tempfile:: tempdir (). unwrap (); 207let root = dir. path (); 208 std:: fs:: write ( root. join ( "in.txt" ), "one" ). unwrap (); 209let mut t =shell ( "true" , & []); 210 t. files =Some ( vec! [ "in.txt" . into ()]); 211let g =graph ( & [( "build" , t)]); 212let first = avril_task:: graph_fingerprint ( & g, root, "build" , & []). unwrap (); 213 std:: fs:: write ( root. join ( "in.txt" ), "two!!" ). unwrap (); 214let second = avril_task:: graph_fingerprint ( & g, root, "build" , & []). unwrap (); 215assert_ne! ( first, second); 216let mut by_name: BTreeMap < String , String > =BTreeMap :: new (); 217 by_name. insert ( "fingerprint" . into (), first); 218assert_eq! ( by_name. len (), 1 ); 219}