char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
d71572b
main
1//! V8 shares one read-only heap between the live isolates of a process and requires 2//! every isolate to bring a byte-identical image. These tests pin how snapshots, 3//! cold bootstraps, and failed creators respect that invariant. 4 5mod support; 6 7use std:: io; 8use std:: sync::{ Arc , Barrier }; 9 10use avril_core::{ Error , Extension , Op , Runtime , RuntimeOptions , Snapshot , SnapshotError }; 11 12fn layout ( name : &' static str , seed : u32 ) ->RuntimeOptions { 13let variant =match name{ 14"a" =>"globalThis.layout = 'a';" , 15"b" =>"globalThis.layout = 'b'; globalThis.extra = [1, 2, 3].map(value => () => value);" , 16"cold" =>"globalThis.layout = 'cold';" , 17 _ =>panic! ( "Unknown test layout: {name}" ), 18}; 19RuntimeOptions { 20extensions : vec! [ Extension { 21name : "ext:layout" , 22dependencies : & [], 23ops : vec! [ Op :: new ( "seed" , move |(): () |Ok ::< _ , io:: Error >( seed))], 24modules : Box :: leak ( Box :: new ([ 25( "ext:layout/variant.js" , variant), 26( 27"ext:layout/main.js" , 28r#" 29import 'ext:layout/variant.js'; 30import {onRuntimeStart} from 'ext:core/bootstrap'; 31import {seed} from 'ext:core/ops'; 32globalThis.started = 0; 33onRuntimeStart(() => { globalThis.seed = seed(); globalThis.started++; }); 34"# , 35), 36])), 37entry_point : Some ( "ext:layout/main.js" ), 38}], 39 ..Default :: default () 40} 41} 42 43fn state ( runtime : & mut Runtime ) ->( String , u32 , u32 ) { 44 runtime 45. execute_script ( "state.js" , "[layout, seed, started]" ) 46. unwrap () 47} 48 49fn write ( name : & str , snapshot : & Snapshot ) { 50 std:: fs:: write ( support:: directory (). join ( name), snapshot. as_bytes ()). unwrap (); 51} 52 53fn read ( name : & str ) ->Snapshot { 54let bytes = std:: fs:: read ( support:: directory (). join ( name)). unwrap (); 55// SAFETY: A sibling role produced this artifact with this executable. 56unsafe { Snapshot :: from_bytes ( bytes) }. unwrap () 57} 58 59fn rejected < T >( result : Result < T , Error >) ->bool { 60matches! ( 61 result, 62Err ( Error :: Snapshot ( SnapshotError :: ReadOnlyHeapInUse )) 63) 64} 65 66# [ test ] 67fn layouts_share_one_snapshot_heap () { 68match support:: role (). as_deref () { 69Some ( "create" ) =>{ 70 avril_core:: initialize_snapshot_generator (). unwrap (); 71let snapshot =Runtime :: create_snapshot ( vec! [ layout ( "a" , 0 ), layout ( "b" , 0 )]). unwrap (); 72write ( "layouts.snapshot" , & snapshot); 73} 74Some ( "restore" ) =>{ 75let snapshot =Arc :: new ( read ( "layouts.snapshot" )); 76assert! ( matches! ( 77Runtime :: from_snapshot ( layout ( "cold" , 9 ), & snapshot), 78Err ( Error :: Snapshot ( SnapshotError :: IncompatibleLayout )) 79)); 80let plan =[( "a" , 1 ), ( "b" , 2 ), ( "a" , 3 ), ( "b" , 4 )]; 81let barrier =Arc :: new ( Barrier :: new ( plan. len () +1 )); 82let threads: Vec < _ > = plan 83. into_iter () 84. map ( |( name, seed) |{ 85let ( snapshot, barrier) =( snapshot. clone (), barrier. clone ()); 86 std:: thread:: spawn ( move ||{ 87let mut runtime = 88Runtime :: from_snapshot ( layout ( name, seed), & snapshot). unwrap (); 89assert_eq! ( state ( & mut runtime), ( name. into (), seed, 1 )); 90 barrier. wait (); 91 barrier. wait (); 92assert_eq! ( state ( & mut runtime), ( name. into (), seed, 1 )); 93}) 94}) 95. collect (); 96 barrier. wait (); 97// Every live isolate shares the snapshot's read-only heap, so V8's 98// built-in heap, including snapshot creators, must wait. 99assert! ( rejected ( Runtime :: new ( RuntimeOptions :: default ()))); 100assert! ( rejected ( Runtime :: create_snapshot ( vec! [ layout ( "a" , 0 )]))); 101 barrier. wait (); 102for threadin threads{ 103 thread. join (). unwrap (); 104} 105let mut cold =Runtime :: new ( layout ( "cold" , 6 )). unwrap (); 106assert_eq! ( state ( & mut cold), ( "cold" . into (), 6 , 1 )); 107} 108 _ => support:: roles ( "layouts_share_one_snapshot_heap" , & [ "create" , "restore" ]), 109} 110} 111 112# [ test ] 113fn separately_generated_snapshots_never_overlap () { 114match support:: role (). as_deref () { 115Some ( "create" ) =>{ 116write ( 117"a.snapshot" , 118& Runtime :: create_snapshot ( vec! [ layout ( "a" , 0 )]). unwrap (), 119); 120write ( 121"b.snapshot" , 122& Runtime :: create_snapshot ( vec! [ layout ( "b" , 0 )]). unwrap (), 123); 124} 125Some ( "restore" ) =>{ 126let ( a, b) =( read ( "a.snapshot" ), read ( "b.snapshot" )); 127let mut first =Runtime :: from_snapshot ( layout ( "a" , 1 ), & a). unwrap (); 128assert! ( rejected ( Runtime :: from_snapshot ( layout ( "b" , 2 ), & b))); 129// Another import of the same artifact is the same read-only heap. 130// Isolates on one thread are entered in creation order, so use them in reverse. 131let mut again =Runtime :: from_snapshot ( layout ( "a" , 4 ), & read ( "a.snapshot" )). unwrap (); 132assert_eq! ( state ( & mut again), ( "a" . into (), 4 , 1 )); 133drop ( again); 134assert_eq! ( state ( & mut first), ( "a" . into (), 1 , 1 )); 135drop ( first); 136let mut second =Runtime :: from_snapshot ( layout ( "b" , 5 ), & b). unwrap (); 137assert_eq! ( state ( & mut second), ( "b" . into (), 5 , 1 )); 138} 139 _ => support:: roles ( 140"separately_generated_snapshots_never_overlap" , 141& [ "create" , "restore" ], 142), 143} 144} 145 146# [ test ] 147fn failed_creators_restore_the_thread_isolate_and_pin_the_built_in_heap () { 148if support:: role (). is_none () { 149return support:: roles ( 150"failed_creators_restore_the_thread_isolate_and_pin_the_built_in_heap" , 151& [ "run" ], 152); 153} 154let snapshot =Runtime :: create_snapshot ( vec! [ layout ( "a" , 0 )]). unwrap (); 155let mut earlier =Runtime :: new ( layout ( "cold" , 1 )). unwrap (); 156let failure =Runtime :: create_snapshot ( vec! [ RuntimeOptions { 157extensions : vec! [ Extension { 158name : "ext:fail" , 159dependencies : & [], 160ops : vec! [], 161modules : & [( "ext:fail/main.js" , "throw Error('failed static setup');" )], 162entry_point : Some ( "ext:fail/main.js" ), 163}], 164 ..Default :: default () 165}]); 166assert! ( 167matches! ( & failure, Err ( Error :: JavaScript ( message)) if message. contains ( "failed static setup" ) && message. contains ( "at ext:fail/main.js:1:7" )), 168"{:?}" , 169 failure. err () 170); 171// The leaked creator no longer shadows runtimes created before it on this thread. 172assert_eq! ( state ( & mut earlier), ( "cold" . into (), 1 , 1 )); 173drop ( earlier); 174// It still holds V8's built-in read-only heap for the rest of the process. 175assert! ( rejected ( Runtime :: from_snapshot ( layout ( "a" , 2 ), & snapshot))); 176let mut later =Runtime :: new ( layout ( "cold" , 3 )). unwrap (); 177assert_eq! ( state ( & mut later), ( "cold" . into (), 3 , 1 )); 178} 179 180# [ test ] 181fn snapshot_generators_reproduce_artifacts_and_refuse_runtimes () { 182match support:: role (). as_deref () { 183Some ( role @( "first" |"second" )) =>{ 184 avril_core:: initialize_snapshot_generator (). unwrap (); 185 avril_core:: initialize_snapshot_generator (). unwrap (); 186let snapshot =Runtime :: create_snapshot ( vec! [ layout ( "a" , 0 ), layout ( "b" , 0 )]). unwrap (); 187write ( & format! ( "{role}.snapshot" ), & snapshot); 188assert! ( matches! ( 189Runtime :: new ( RuntimeOptions :: default ()), 190Err ( Error :: Snapshot ( SnapshotError :: GeneratorProcess )) 191)); 192assert! ( matches! ( 193Runtime :: from_snapshot ( layout ( "a" , 1 ), & snapshot), 194Err ( Error :: Snapshot ( SnapshotError :: GeneratorProcess )) 195)); 196} 197Some ( "restore" ) =>{ 198let directory = support:: directory (); 199assert_eq! ( 200 std:: fs:: read ( directory. join ( "first.snapshot" )). unwrap (), 201 std:: fs:: read ( directory. join ( "second.snapshot" )). unwrap (), 202"snapshot generation is not reproducible" 203); 204let mut runtime = 205Runtime :: from_snapshot ( layout ( "b" , 7 ), & read ( "first.snapshot" )). unwrap (); 206assert_eq! ( state ( & mut runtime), ( "b" . into (), 7 , 1 )); 207assert! ( matches! ( 208 avril_core:: initialize_snapshot_generator (), 209Err ( SnapshotError :: RuntimeProcess ) 210)); 211} 212 _ => support:: roles ( 213"snapshot_generators_reproduce_artifacts_and_refuse_runtimes" , 214& [ "first" , "second" , "restore" ], 215), 216} 217} 218 219# [ test ] 220fn static_bootstrap_sees_stack_limits_and_restores_get_live_ones () { 221if support:: role (). is_none () { 222return support:: roles ( 223"static_bootstrap_sees_stack_limits_and_restores_get_live_ones" , 224& [ "run" ], 225); 226} 227let options = |source : &' static str |RuntimeOptions { 228extensions : vec! [ Extension { 229name : "ext:limit" , 230dependencies : & [], 231ops : vec! [], 232modules : Box :: leak ( Box :: new ([( "ext:limit/main.js" , source)])), 233entry_point : Some ( "ext:limit/main.js" ), 234}], 235 ..Default :: default () 236}; 237let observe =r#" 238globalThis.staticLimit = Error.stackTraceLimit; 239globalThis.staticStack = new Error('static').stack.includes('ext:limit/main.js:3:'); 240"# ; 241let probe ="[staticLimit, staticStack, JSON.stringify(Object.getOwnPropertyDescriptor(Error, 'stackTraceLimit'))]" ; 242let cold: ( u32 , bool , String ) =Runtime :: new ( options ( observe)) 243. unwrap () 244. execute_script ( "probe.js" , probe) 245. unwrap (); 246assert_eq! ( 247 cold, 248( 24910 , 250true , 251r#"{"value":10,"writable":true,"enumerable":true,"configurable":true}"# . into () 252) 253); 254let snapshot =Runtime :: create_snapshot ( vec! [ options ( observe)]). unwrap (); 255let state: ( u32 , bool , String ) =Runtime :: from_snapshot ( options ( observe), & snapshot) 256. unwrap () 257. execute_script ( "probe.js" , probe) 258. unwrap (); 259assert_eq! ( state, cold); 260// Restored contexts get V8's live property, so a changed static limit cannot persist. 261assert! ( matches! ( 262Runtime :: create_snapshot ( vec! [ options ( "Error.stackTraceLimit = 3;" )]), 263Err ( Error :: Snapshot ( SnapshotError :: ActiveState ( 264"Error.stackTraceLimit" 265))) 266)); 267} 268 269# [ test ] 270fn layout_fingerprints_frame_every_field () { 271if support:: role (). is_none () { 272return support:: roles ( "layout_fingerprints_frame_every_field" , & [ "run" ]); 273} 274let options = |ops : [ &' static str ; 2 ] |RuntimeOptions { 275extensions : vec! [ Extension { 276name : "ext:frame" , 277dependencies : & [], 278ops : ops 279. into_iter () 280. map ( |name|Op :: new ( name, |(): () |Ok ::< _ , io:: Error >(()))) 281. collect (), 282modules : & [( "ext:frame/main.js" , "" )], 283entry_point : Some ( "ext:frame/main.js" ), 284}], 285 ..Default :: default () 286}; 287let snapshot =Runtime :: create_snapshot ( vec! [ options ([ "ab" , "c" ])]). unwrap (); 288assert! ( matches! ( 289Runtime :: from_snapshot ( options ([ "a" , "bc" ]), & snapshot), 290Err ( Error :: Snapshot ( SnapshotError :: IncompatibleLayout )) 291)); 292Runtime :: from_snapshot ( options ([ "c" , "ab" ]), & snapshot). unwrap (); 293assert! ( matches! ( 294Runtime :: create_snapshot ( vec! [ options ([ "ab" , "c" ]), options ([ "c" , "ab" ])]), 295Err ( Error :: Snapshot ( SnapshotError :: DuplicateLayout )) 296)); 297} 298 299# [ test ] 300fn snapshot_containers_are_validated () { 301let cases: [ &' static [ u8 ]; 4 ] =[ 302b"" , 303b"not an avril snapshot" , 304b"AVRILSNP\x02\0\0\0\0\0\0\0" , 305b"AVRILSNP\x01\0\0\0\xff\0\0\0{}" , 306]; 307for bytesin cases{ 308// SAFETY: Only the avril container is read; invalid containers stop there. 309let result =unsafe { Snapshot :: from_bytes ( bytes) }; 310assert! ( matches! ( result, Err ( SnapshotError :: InvalidArtifact ( _)))); 311} 312} 313 314/// A module loader whose destructor panics, as a faulty native resource might. 315struct PanickingLoader ; 316 317impl Drop for PanickingLoader { 318fn drop ( & mut self ) { 319panic! ( "native state destructor panicked" ); 320} 321} 322 323impl avril_core:: ModuleLoader for PanickingLoader { 324fn resolve ( & self , specifier : & str , _: & str ) ->Result < String , Error > { 325Ok ( specifier. into ()) 326} 327 328fn load ( & self , name : & str ) ->Result < avril_core:: ModuleSource , Error > { 329Err ( Error :: Module ( format! ( "Missing {name}" ))) 330} 331} 332 333/// Core-owned native state (the module map) holds the only loader reference. 334fn panicking_state ( module : &' static str ) ->RuntimeOptions { 335RuntimeOptions { 336extensions : vec! [ Extension { 337name : "ext:panic" , 338dependencies : & [], 339ops : vec! [], 340modules : Box :: leak ( Box :: new ([( "ext:panic/main.js" , module)])), 341entry_point : Some ( "ext:panic/main.js" ), 342}], 343module_loader : Some ( std:: rc:: Rc :: new ( PanickingLoader )), 344} 345} 346 347# [ test ] 348fn isolates_leaked_by_panicking_disposal_keep_their_heap () { 349let Some ( role) = support:: role () else { 350return support:: roles ( 351"isolates_leaked_by_panicking_disposal_keep_their_heap" , 352& [ "out-of-order" , "runtime-state" , "creator-state" ], 353); 354}; 355let snapshot =Runtime :: create_snapshot ( vec! [ layout ( "a" , 0 )]). unwrap (); 356match role. as_str () { 357"out-of-order" =>{ 358let first =Runtime :: new ( layout ( "cold" , 1 )). unwrap (); 359let mut second =Runtime :: new ( layout ( "cold" , 2 )). unwrap (); 360// rusty_v8 refuses to dispose an isolate that is not the thread's current one. 361let dropped = std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( ||drop ( first))); 362assert! ( dropped. is_err ()); 363assert_eq! ( state ( & mut second), ( "cold" . into (), 2 , 1 )); 364} 365"runtime-state" =>{ 366let runtime =Runtime :: new ( panicking_state ( "" )). unwrap (); 367let dropped = std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( ||drop ( runtime))); 368assert! ( dropped. is_err ()); 369} 370 _ =>{ 371let mut earlier =Runtime :: new ( layout ( "cold" , 1 )). unwrap (); 372let failed = std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( ||{ 373Runtime :: create_snapshot ( vec! [ panicking_state ( "throw Error('static failure');" )]) 374})); 375assert! ( failed. is_err ()); 376// The abandoned creator still left the thread's isolate stack. 377assert_eq! ( state ( & mut earlier), ( "cold" . into (), 1 , 1 )); 378} 379} 380// The undisposed isolate still holds V8's built-in read-only heap. 381assert! ( rejected ( Runtime :: from_snapshot ( layout ( "a" , 3 ), & snapshot))); 382let mut later =Runtime :: new ( layout ( "cold" , 4 )). unwrap (); 383assert_eq! ( state ( & mut later), ( "cold" . into (), 4 , 1 )); 384}