#![cfg(windows)] mod common; const PRELUDE: &str = r#" const assert=(value,message)=>{if(!value)throw Error(message)}; async function rejects(action,name){try{await action()}catch(error){assert(error.name===name,'Expected '+name+', got '+error);return error}throw Error('Expected '+name)} "#; fn pass(source: &str) { common::run(&format!("{PRELUDE}\n{source}"), b""); } #[test] fn console_programs_run_with_exit_codes_env_and_input() { pass( r#" import {run,runSync} from 'avril:std'; for (const method of [run, runSync]) { const hello = await method('cmd', {args:['/d','/c','echo hello'], encoding:'utf-8'}); assert(hello.stdout === 'hello\r\n', 'PATH search appends .exe: ' + JSON.stringify(hello.stdout)); const failed = await method('cmd.exe', {args:['/d','/c','exit 3'], check:false}); assert(failed.code === 3 && failed.signal === null, 'exit code'); const env = await method('cmd.exe', {args:['/d','/c','echo %AVRIL_PROCESS_VALUE%'], env:{AVRIL_PROCESS_VALUE:'set'}, encoding:'utf-8'}); assert(env.stdout === 'set\r\n', 'explicit environment'); const found = await method('findstr', {args:['x'], input:'axb\r\nnope\r\n', encoding:'utf-8'}); assert(found.stdout === 'axb\r\n', 'piped stdin'); } "#, ); } #[test] fn batch_files_need_an_explicit_shell() { let dir = tempfile::tempdir().unwrap(); std::fs::write(dir.path().join("tool.cmd"), "@echo implicit\r\n").unwrap(); // Only a verbatim path keeps the trailing dot and space literal. let literal = dir.path().canonicalize().unwrap().join("tool.cmd. "); std::fs::write(literal, "@echo implicit\r\n").unwrap(); let source = format!( r#" import {{run}} from 'avril:std'; const cwd = {}; await rejects(()=>run('./tool.cmd', {{cwd}}), 'NotSupported'); await rejects(()=>run('./tool.cmd. ', {{cwd}}), 'NotSupported'); const explicit = await run('cmd.exe', {{args:['/d','/c','tool.cmd'], cwd, encoding:'utf-8'}}); assert(explicit.stdout === 'implicit\r\n', 'explicit cmd.exe'); "#, serde_json::to_string(dir.path()).unwrap() ); pass(&source); } #[test] fn group_termination_covers_the_job() { // The grandchild holds the output pipe; only terminating the whole job // lets the pipes close well before ping would finish. pass( r#" import {run} from 'avril:std'; const started = Date.now(); try { await run('cmd.exe', {args:['/d','/c','ping -n 30 127.0.0.1'], timeoutMs:300}); throw Error('expected timeout'); } catch (error) { assert(error.name === 'ProcessTimeoutError', 'timeout: ' + error); } assert(Date.now() - started < 4000, 'grandchild kept the pipe open'); "#, ); } #[test] fn only_terminating_signals_and_no_terminals() { pass( r#" import {spawn} from 'avril:std'; await rejects(()=>spawn('cmd.exe', {termination:{signal:'SIGUSR1'}}), 'NotSupported'); await rejects(()=>spawn('cmd.exe', {terminal:{}}), 'NotSupported'); const child = spawn('ping', {args:['-n','30','127.0.0.1'], stdout:'null'}); await rejects(()=>child.kill('SIGHUP'), 'NotSupported'); assert(child.kill('SIGTERM') === true, 'terminated'); const status = await child.status; assert(status.code === 1 && status.signal === null, 'terminated exit status'); "#, ); } #[test] fn respelled_entries_meet_the_same_grants() { let system = std::path::Path::new(&std::env::var_os("SystemRoot").unwrap()).join("System32"); let dir = tempfile::tempdir().unwrap(); let source = format!( r#" import {{run}} from 'avril:std'; await rejects(()=>run('CMD', {{args:['/d','/c','exit']}}), 'PermissionDenied'); await rejects(()=>run({}, {{args:['/d','/c','exit']}}), 'PermissionDenied'); "#, serde_json::to_string(&system.join("Cmd.Exe")).unwrap() ); std::fs::write(dir.path().join("main.js"), format!("{PRELUDE}\n{source}")).unwrap(); let output = common::wait( std::process::Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .arg(format!("--deny-run={}", system.join("CMD.EXE").display())) .arg("main.js") .current_dir(dir.path()) .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .unwrap(), ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); } #[test] fn signal_listeners_are_not_supported() { pass( r#" import {addSignalListener, removeSignalListener} from 'avril:std'; await rejects(() => addSignalListener('SIGUSR1', () => {}), 'NotSupported'); removeSignalListener('SIGUSR1', () => {}); "#, ); }