2019-05-17 12:09:31 -07:00
//! This file will run at build time to autogenerate the WASI regression tests
//! It will compile the files indicated in TESTS, to:executable and .wasm
//! - Compile with the native rust target to get the expected output
//! - Compile with the latest WASI target to get the wasm
//! - Generate the test that will compare the output of running the .wasm file
//! with wasmer with the expected output
use glob ::glob ;
use std ::collections ::HashSet ;
use std ::fs ;
2020-03-13 15:41:50 -07:00
use std ::path ::{ Path , PathBuf } ;
2019-05-17 12:09:31 -07:00
use std ::process ::Command ;
use std ::fs ::File ;
use std ::io ::prelude ::* ;
2020-03-13 15:41:50 -07:00
use std ::io ::{ self , BufReader } ;
use crate ::util ;
use crate ::wasi_version ::* ;
2019-05-17 12:09:31 -07:00
static BANNER : & str = " // !!! THIS IS A GENERATED FILE !!!
// ANY MANUAL EDITS MAY BE OVERWRITTEN AT ANY TIME
// Files autogenerated with cargo build (build/wasitests.rs).\n";
2020-03-13 15:41:50 -07:00
/// Compile and execute the test file as native code, saving the results to be
/// compared against later.
///
/// This function attempts to clean up its output after it executes it.
fn generate_native_output ( temp_dir : & Path , file : & str , normalized_name : & str ) -> io ::Result < ( ) > {
let executable_path = temp_dir . join ( normalized_name ) ;
println! (
" Compiling program {} to native at {} " ,
file ,
executable_path . to_string_lossy ( )
) ;
2019-07-15 10:36:12 -07:00
let native_out = Command ::new ( " rustc " )
2019-05-17 12:09:31 -07:00
. arg ( file )
. arg ( " -o " )
2020-03-13 15:41:50 -07:00
. arg ( & executable_path )
2019-05-17 12:09:31 -07:00
. output ( )
. expect ( " Failed to compile program to native code " ) ;
2020-03-13 15:41:50 -07:00
util ::print_info_on_error ( & native_out , " COMPILATION FAILED " ) ;
2019-05-17 12:09:31 -07:00
#[ cfg(unix) ]
{
use std ::os ::unix ::fs ::PermissionsExt ;
2020-03-13 15:41:50 -07:00
let mut perm = executable_path
2019-05-17 12:09:31 -07:00
. metadata ( )
. expect ( " native executable " )
. permissions ( ) ;
perm . set_mode ( 0o766 ) ;
2020-03-13 15:41:50 -07:00
println! (
" Setting execute permissions on {} " ,
executable_path . to_string_lossy ( )
) ;
fs ::set_permissions ( & executable_path , perm ) ? ;
2019-05-17 12:09:31 -07:00
}
2020-03-13 15:41:50 -07:00
let result = Command ::new ( & executable_path )
2019-05-17 12:09:31 -07:00
. output ( )
. expect ( " Failed to execute native program " ) ;
2020-03-13 15:41:50 -07:00
util ::print_info_on_error ( & result , " NATIVE PROGRAM FAILED " ) ;
2019-05-30 11:58:52 -07:00
2020-03-13 15:41:50 -07:00
let mut output_path = executable_path . clone ( ) ;
output_path . set_extension ( " out " ) ;
2019-05-17 12:09:31 -07:00
2020-03-13 15:41:50 -07:00
println! ( " Writing output to {} " , output_path . to_string_lossy ( ) ) ;
fs ::write ( & output_path , result . stdout ) ? ;
Ok ( ( ) )
}
/// compile the Wasm file for the given version of WASI
///
/// returns the path of where the wasm file is
fn compile_wasm_for_version (
temp_dir : & Path ,
file : & str ,
base_dir : & Path ,
rs_mod_name : & str ,
version : WasiVersion ,
) -> io ::Result < PathBuf > {
let out_dir = base_dir . join ( version . get_directory_name ( ) ) ;
if ! out_dir . exists ( ) {
fs ::create_dir ( & out_dir ) ? ;
2019-08-01 16:38:34 +09:00
}
2020-03-13 15:41:50 -07:00
let wasm_out_name = {
let mut wasm_out_name = out_dir . join ( rs_mod_name ) ;
wasm_out_name . set_extension ( " wasm " ) ;
wasm_out_name
} ;
println! ( " Reading contents from file ` {} ` " , file ) ;
let file_contents : String = {
let mut fc = String ::new ( ) ;
let mut f = fs ::OpenOptions ::new ( ) . read ( true ) . open ( & file ) ? ;
f . read_to_string ( & mut fc ) ? ;
fc
} ;
let temp_wasi_rs_file_name = temp_dir . join ( format! ( " wasi_modified_version_ {} .rs " , rs_mod_name ) ) ;
2019-08-01 16:38:34 +09:00
{
2020-03-13 15:41:50 -07:00
let mut actual_file = fs ::OpenOptions ::new ( )
2019-08-01 16:38:34 +09:00
. write ( true )
. truncate ( true )
2020-03-13 15:41:50 -07:00
. create ( true )
. open ( & temp_wasi_rs_file_name )
2019-08-01 16:38:34 +09:00
. unwrap ( ) ;
2020-03-13 15:41:50 -07:00
actual_file . write_all ( b " #![feature(wasi_ext)] \n " ) . unwrap ( ) ;
actual_file . write_all ( file_contents . as_bytes ( ) ) . unwrap ( ) ;
2019-08-01 16:38:34 +09:00
}
2020-03-13 15:41:50 -07:00
println! (
" Compiling wasm module `{}` with toolchain `{}` " ,
& wasm_out_name . to_string_lossy ( ) ,
version . get_compiler_toolchain ( )
) ;
2019-07-15 10:36:12 -07:00
let wasm_compilation_out = Command ::new ( " rustc " )
2020-03-13 15:41:50 -07:00
. arg ( format! ( " + {} " , version . get_compiler_toolchain ( ) ) )
2019-05-17 12:09:31 -07:00
. arg ( " --target=wasm32-wasi " )
2019-07-15 10:36:12 -07:00
. arg ( " -C " )
2019-11-08 11:13:01 -08:00
. arg ( " opt-level=z " )
2020-03-13 15:41:50 -07:00
. arg ( & temp_wasi_rs_file_name )
2019-05-17 12:09:31 -07:00
. arg ( " -o " )
. arg ( & wasm_out_name )
. output ( )
2020-03-13 15:41:50 -07:00
. expect ( " Failed to compile program to wasm " ) ;
util ::print_info_on_error ( & wasm_compilation_out , " WASM COMPILATION " ) ;
println! (
" Removing file `{}` " ,
& temp_wasi_rs_file_name . to_string_lossy ( )
) ;
2019-07-15 10:36:12 -07:00
// to prevent commiting huge binary blobs forever
let wasm_strip_out = Command ::new ( " wasm-strip " )
. arg ( & wasm_out_name )
. output ( )
. expect ( " Failed to strip compiled wasm module " ) ;
2020-03-13 15:41:50 -07:00
util ::print_info_on_error ( & wasm_strip_out , " STRIPPING WASM " ) ;
let wasm_opt_out = Command ::new ( " wasm-opt " )
. arg ( " -Oz " )
. arg ( & wasm_out_name )
. arg ( " -o " )
. arg ( & wasm_out_name )
. output ( )
. expect ( " Failed to optimize compiled wasm module with wasm-opt! " ) ;
util ::print_info_on_error ( & wasm_opt_out , " OPTIMIZING WASM " ) ;
Ok ( wasm_out_name )
}
2019-05-17 12:09:31 -07:00
2020-03-13 15:41:50 -07:00
fn generate_test_file (
file : & str ,
rs_module_name : & str ,
wasm_out_name : & str ,
version : WasiVersion ,
ignores : & HashSet < String > ,
) -> io ::Result < String > {
let test_name = format! ( " {} _ {} " , version . get_directory_name ( ) , rs_module_name ) ;
let ignored = if ignores . contains ( & test_name ) | | ignores . contains ( rs_module_name ) {
2019-05-17 12:09:31 -07:00
" \n #[ignore] "
} else {
" "
} ;
2019-05-17 15:31:02 -07:00
2020-03-13 15:41:50 -07:00
let src_code = fs ::read_to_string ( file ) ? ;
2019-07-17 14:00:51 -07:00
let args : Args = extract_args_from_source_file ( & src_code ) . unwrap_or_default ( ) ;
2019-05-30 11:58:52 -07:00
let mapdir_args = {
let mut out_str = String ::new ( ) ;
out_str . push_str ( " vec![ " ) ;
for ( alias , real_dir ) in args . mapdir {
out_str . push_str ( & format! (
2019-07-13 16:00:18 -07:00
" ( \" {} \" .to_string(), ::std::path::PathBuf::from( \" {} \" )), " ,
2019-05-30 11:58:52 -07:00
alias , real_dir
) ) ;
2019-05-20 17:43:50 -07:00
}
2019-05-30 11:58:52 -07:00
out_str . push_str ( " ] " ) ;
out_str
} ;
let envvar_args = {
let mut out_str = String ::new ( ) ;
out_str . push_str ( " vec![ " ) ;
for entry in args . envvars {
out_str . push_str ( & format! ( " \" {} = {} \" .to_string(), " , entry . 0 , entry . 1 ) ) ;
}
out_str . push_str ( " ] " ) ;
out_str
2019-05-20 17:43:50 -07:00
} ;
2019-07-19 13:36:05 -07:00
let dir_args = {
2019-07-17 14:00:51 -07:00
let mut out_str = String ::new ( ) ;
out_str . push_str ( " vec![ " ) ;
for entry in args . po_dirs {
2019-10-02 11:00:27 -07:00
out_str . push_str ( & format! ( " std::path::PathBuf::from( \" {} \" ), " , entry ) ) ;
2019-07-17 14:00:51 -07:00
}
out_str . push_str ( " ] " ) ;
out_str
} ;
2019-05-17 12:09:31 -07:00
let contents = format! (
2019-09-11 17:41:25 -07:00
" {banner}
#[ test ] { ignore }
2020-03-13 15:41:50 -07:00
fn test_ { test_name } ( ) { {
2019-05-17 12:09:31 -07:00
assert_wasi_output! (
\ " ../../{module_path} \" ,
2020-03-13 15:41:50 -07:00
\ " {test_name} \" ,
2019-07-19 13:36:05 -07:00
{ dir_args } ,
2019-05-20 17:43:50 -07:00
{ mapdir_args } ,
2019-05-30 11:58:52 -07:00
{ envvar_args } ,
2019-05-17 12:09:31 -07:00
\ " ../../{test_output_path} \"
) ;
} }
" ,
2019-09-11 17:41:25 -07:00
banner = BANNER ,
2019-05-17 12:09:31 -07:00
ignore = ignored ,
module_path = wasm_out_name ,
2020-03-13 15:41:50 -07:00
test_name = & test_name ,
test_output_path = format! ( " wasitests/ {} .out " , rs_module_name ) ,
2019-07-19 13:36:05 -07:00
dir_args = dir_args ,
2019-05-20 17:43:50 -07:00
mapdir_args = mapdir_args ,
2019-05-30 11:58:52 -07:00
envvar_args = envvar_args
2019-05-17 12:09:31 -07:00
) ;
let rust_test_filepath = format! (
2020-03-13 15:41:50 -07:00
concat! ( env! ( " CARGO_MANIFEST_DIR " ) , " /tests/wasitests/{}.rs " ) ,
& test_name ,
2019-05-17 12:09:31 -07:00
) ;
2020-03-13 15:41:50 -07:00
fs ::write ( & rust_test_filepath , contents . as_bytes ( ) ) ? ;
Ok ( test_name )
}
/// Returns the a Vec of the test modules created
fn compile (
temp_dir : & Path ,
file : & str ,
ignores : & HashSet < String > ,
wasi_versions : & [ WasiVersion ] ,
) -> Vec < String > {
// TODO: hook up compile_wasm_for_version, etc with new args
assert! ( file . ends_with ( " .rs " ) ) ;
let rs_mod_name = {
Path ::new ( & file . to_lowercase ( ) )
. file_stem ( )
. unwrap ( )
. to_string_lossy ( )
. to_string ( )
} ;
let base_dir = Path ::new ( file ) . parent ( ) . unwrap ( ) ;
generate_native_output ( temp_dir , & file , & rs_mod_name ) . expect ( " Generate native output " ) ;
let mut out = vec! [ ] ;
for & version in wasi_versions {
let wasm_out_path = compile_wasm_for_version ( temp_dir , file , base_dir , & rs_mod_name , version )
. expect ( & format! ( " Could not compile Wasm to WASI version {:?} , perhaps you need to install the ` {} ` rust toolchain " , version , version . get_compiler_toolchain ( ) ) ) ;
let wasm_out_name = wasm_out_path . to_string_lossy ( ) ;
let test_mod = generate_test_file ( file , & rs_mod_name , & wasm_out_name , version , ignores )
. expect ( & format! ( " generate test file {} " , & rs_mod_name ) ) ;
out . push ( test_mod ) ;
}
out
}
fn run_prelude ( should_gen_all : bool ) -> & 'static [ WasiVersion ] {
if should_gen_all {
println! (
2020-03-16 12:37:23 -07:00
" Generating WASI tests for all versions of WASI. Run with WASI_TEST_GENERATE_ALL=0 to only generate the latest tests. "
2020-03-13 15:41:50 -07:00
) ;
} else {
println! (
" Generating WASI tests for the latest version of WASI. Run with WASI_TEST_GENERATE_ALL=1 to generate all versions of the tests. "
) ;
}
2019-05-17 12:09:31 -07:00
2020-03-13 15:41:50 -07:00
if should_gen_all {
ALL_WASI_VERSIONS
} else {
LATEST_WASI_VERSION
}
2019-05-17 12:09:31 -07:00
}
2020-03-13 15:41:50 -07:00
pub fn build ( should_gen_all : bool ) {
2019-05-17 12:09:31 -07:00
let rust_test_modpath = concat! ( env! ( " CARGO_MANIFEST_DIR " ) , " /tests/wasitests/mod.rs " ) ;
let mut modules : Vec < String > = Vec ::new ( ) ;
2020-03-13 15:41:50 -07:00
let wasi_versions = run_prelude ( should_gen_all ) ;
2019-05-17 12:09:31 -07:00
2020-03-13 15:41:50 -07:00
let temp_dir = tempfile ::TempDir ::new ( ) . unwrap ( ) ;
2019-05-17 12:09:31 -07:00
let ignores = read_ignore_list ( ) ;
for entry in glob ( " wasitests/*.rs " ) . unwrap ( ) {
match entry {
Ok ( path ) = > {
let test = path . to_str ( ) . unwrap ( ) ;
2020-03-13 15:41:50 -07:00
modules . extend ( compile ( temp_dir . path ( ) , test , & ignores , wasi_versions ) ) ;
2019-05-17 12:09:31 -07:00
}
Err ( e ) = > println! ( " {:?} " , e ) ,
}
}
2020-03-13 15:41:50 -07:00
println! ( " All modules generated. Generating test harness. " ) ;
2019-05-17 12:09:31 -07:00
modules . sort ( ) ;
let mut modules : Vec < String > = modules . iter ( ) . map ( | m | format! ( " mod {} ; " , m ) ) . collect ( ) ;
assert! ( modules . len ( ) > 0 , " Expected > 0 modules found " ) ;
modules . insert ( 0 , BANNER . to_string ( ) ) ;
modules . insert ( 1 , " // The _common module is not autogenerated. It provides common macros for the wasitests \n #[macro_use] \n mod _common; " . to_string ( ) ) ;
// We add an empty line
modules . push ( " " . to_string ( ) ) ;
let modfile : String = modules . join ( " \n " ) ;
2020-03-13 15:41:50 -07:00
let should_regen : bool = {
if let Ok ( mut f ) = fs ::File ::open ( & rust_test_modpath ) {
let mut s = String ::new ( ) ;
f . read_to_string ( & mut s ) . unwrap ( ) ;
s ! = modfile
} else {
false
}
} ;
if should_regen {
println! ( " Writing to ` {} ` " , & rust_test_modpath ) ;
let mut test_harness_file = fs ::OpenOptions ::new ( )
. write ( true )
. create ( true )
. truncate ( true )
. open ( & rust_test_modpath )
. unwrap ( ) ;
test_harness_file . write_all ( modfile . as_bytes ( ) ) . unwrap ( ) ;
2019-05-17 12:09:31 -07:00
}
}
fn read_ignore_list ( ) -> HashSet < String > {
let f = File ::open ( " wasitests/ignores.txt " ) . unwrap ( ) ;
let f = BufReader ::new ( f ) ;
f . lines ( )
. filter_map ( Result ::ok )
. map ( | v | v . to_lowercase ( ) )
. collect ( )
}
2019-05-20 17:43:50 -07:00
2019-05-30 11:58:52 -07:00
#[ derive(Debug, Default) ]
2019-05-20 17:43:50 -07:00
struct Args {
pub mapdir : Vec < ( String , String ) > ,
2019-05-30 11:58:52 -07:00
pub envvars : Vec < ( String , String ) > ,
2019-07-17 14:00:51 -07:00
/// pre-opened directories
pub po_dirs : Vec < String > ,
2019-05-20 17:43:50 -07:00
}
/// Pulls args to the program out of a comment at the top of the file starting with "// Args:"
fn extract_args_from_source_file ( source_code : & str ) -> Option < Args > {
if source_code . starts_with ( " // Args: " ) {
2019-05-30 11:58:52 -07:00
let mut args = Args ::default ( ) ;
2019-05-20 17:43:50 -07:00
for arg_line in source_code
. lines ( )
. skip ( 1 )
. take_while ( | line | line . starts_with ( " // " ) )
{
let tokenized = arg_line
. split_whitespace ( )
2019-07-15 10:36:12 -07:00
// skip trailing space
2019-05-20 17:43:50 -07:00
. skip ( 1 )
. map ( String ::from )
. collect ::< Vec < String > > ( ) ;
2019-07-15 10:36:12 -07:00
let command_name = {
let mut cn = tokenized [ 0 ] . clone ( ) ;
assert_eq! (
cn . pop ( ) ,
Some ( ':' ) ,
" Final character of argname must be a colon "
) ;
cn
} ;
match command_name . as_ref ( ) {
2019-05-20 17:43:50 -07:00
" mapdir " = > {
2019-07-15 10:36:12 -07:00
if let [ alias , real_dir ] = & tokenized [ 1 ] . split ( ':' ) . collect ::< Vec < & str > > ( ) [ .. ] {
2019-05-20 17:43:50 -07:00
args . mapdir . push ( ( alias . to_string ( ) , real_dir . to_string ( ) ) ) ;
} else {
eprintln! (
" Parse error in mapdir {} not parsed correctly " ,
2019-07-15 10:36:12 -07:00
& tokenized [ 1 ]
2019-05-20 17:43:50 -07:00
) ;
}
}
2019-05-30 11:58:52 -07:00
" env " = > {
2019-07-15 10:36:12 -07:00
if let [ name , val ] = & tokenized [ 1 ] . split ( '=' ) . collect ::< Vec < & str > > ( ) [ .. ] {
2019-05-30 11:58:52 -07:00
args . envvars . push ( ( name . to_string ( ) , val . to_string ( ) ) ) ;
} else {
2019-07-15 10:36:12 -07:00
eprintln! ( " Parse error in env {} not parsed correctly " , & tokenized [ 1 ] ) ;
2019-05-30 11:58:52 -07:00
}
}
2019-07-17 14:00:51 -07:00
" dir " = > {
args . po_dirs . push ( tokenized [ 1 ] . to_string ( ) ) ;
}
2019-05-20 17:43:50 -07:00
e = > {
eprintln! ( " WARN: comment arg: {} is not supported " , e ) ;
}
}
}
return Some ( args ) ;
}
None
}