char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: embed shared main and worker startup in the CLI688e96e

main
4.9 KiB129 linesraw
1#![cfg(windows)]
2mod common;
3
4const PRELUDE: &str = r#"
5const assert=(value,message)=>{if(!value)throw Error(message)};
6async function rejects(action,name){try{await action()}catch(error){assert(error.name===name,'Expected '+name+', got '+error);return error}throw Error('Expected '+name)}
7"#;
8
9fn pass(source: &str) {
10    common::run(&format!("{PRELUDE}\n{source}"), b"");
11}
12
13#[test]
14fn console_programs_run_with_exit_codes_env_and_input() {
15    pass(
16        r#"
17        import {run,runSync} from 'avril:std';
18        for (const method of [run, runSync]) {
19            const hello = await method('cmd', {args:['/d','/c','echo hello'], encoding:'utf-8'});
20            assert(hello.stdout === 'hello\r\n', 'PATH search appends .exe: ' + JSON.stringify(hello.stdout));
21            const failed = await method('cmd.exe', {args:['/d','/c','exit 3'], check:false});
22            assert(failed.code === 3 && failed.signal === null, 'exit code');
23            const env = await method('cmd.exe', {args:['/d','/c','echo %AVRIL_PROCESS_VALUE%'], env:{AVRIL_PROCESS_VALUE:'set'}, encoding:'utf-8'});
24            assert(env.stdout === 'set\r\n', 'explicit environment');
25            const found = await method('findstr', {args:['x'], input:'axb\r\nnope\r\n', encoding:'utf-8'});
26            assert(found.stdout === 'axb\r\n', 'piped stdin');
27        }
28    "#,
29    );
30}
31
32#[test]
33fn batch_files_need_an_explicit_shell() {
34    let dir = tempfile::tempdir().unwrap();
35    std::fs::write(dir.path().join("tool.cmd"), "@echo implicit\r\n").unwrap();
36    // Only a verbatim path keeps the trailing dot and space literal.
37    let literal = dir.path().canonicalize().unwrap().join("tool.cmd. ");
38    std::fs::write(literal, "@echo implicit\r\n").unwrap();
39    let source = format!(
40        r#"
41        import {{run}} from 'avril:std';
42        const cwd = {};
43        await rejects(()=>run('./tool.cmd', {{cwd}}), 'NotSupported');
44        await rejects(()=>run('./tool.cmd. ', {{cwd}}), 'NotSupported');
45        const explicit = await run('cmd.exe', {{args:['/d','/c','tool.cmd'], cwd, encoding:'utf-8'}});
46        assert(explicit.stdout === 'implicit\r\n', 'explicit cmd.exe');
47    "#,
48        serde_json::to_string(dir.path()).unwrap()
49    );
50    pass(&source);
51}
52
53#[test]
54fn group_termination_covers_the_job() {
55    // The grandchild holds the output pipe; only terminating the whole job
56    // lets the pipes close well before ping would finish.
57    pass(
58        r#"
59        import {run} from 'avril:std';
60        const started = Date.now();
61        try {
62            await run('cmd.exe', {args:['/d','/c','ping -n 30 127.0.0.1'], timeoutMs:300});
63            throw Error('expected timeout');
64        } catch (error) {
65            assert(error.name === 'ProcessTimeoutError', 'timeout: ' + error);
66        }
67        assert(Date.now() - started < 4000, 'grandchild kept the pipe open');
68    "#,
69    );
70}
71
72#[test]
73fn only_terminating_signals_and_no_terminals() {
74    pass(
75        r#"
76        import {spawn} from 'avril:std';
77        await rejects(()=>spawn('cmd.exe', {termination:{signal:'SIGUSR1'}}), 'NotSupported');
78        await rejects(()=>spawn('cmd.exe', {terminal:{}}), 'NotSupported');
79        const child = spawn('ping', {args:['-n','30','127.0.0.1'], stdout:'null'});
80        await rejects(()=>child.kill('SIGHUP'), 'NotSupported');
81        assert(child.kill('SIGTERM') === true, 'terminated');
82        const status = await child.status;
83        assert(status.code === 1 && status.signal === null, 'terminated exit status');
84    "#,
85    );
86}
87
88#[test]
89fn respelled_entries_meet_the_same_grants() {
90    let system = std::path::Path::new(&std::env::var_os("SystemRoot").unwrap()).join("System32");
91    let dir = tempfile::tempdir().unwrap();
92    let source = format!(
93        r#"
94        import {{run}} from 'avril:std';
95        await rejects(()=>run('CMD', {{args:['/d','/c','exit']}}), 'PermissionDenied');
96        await rejects(()=>run({}, {{args:['/d','/c','exit']}}), 'PermissionDenied');
97    "#,
98        serde_json::to_string(&system.join("Cmd.Exe")).unwrap()
99    );
100    std::fs::write(dir.path().join("main.js"), format!("{PRELUDE}\n{source}")).unwrap();
101    let output = common::wait(
102        std::process::Command::new(env!("CARGO_BIN_EXE_avril"))
103            .arg("run")
104            .arg(format!("--deny-run={}", system.join("CMD.EXE").display()))
105            .arg("main.js")
106            .current_dir(dir.path())
107            .stdin(std::process::Stdio::null())
108            .stdout(std::process::Stdio::piped())
109            .stderr(std::process::Stdio::piped())
110            .spawn()
111            .unwrap(),
112    );
113    assert!(
114        output.status.success(),
115        "{}",
116        String::from_utf8_lossy(&output.stderr)
117    );
118}
119
120#[test]
121fn signal_listeners_are_not_supported() {
122    pass(
123        r#"
124        import {addSignalListener, removeSignalListener} from 'avril:std';
125        await rejects(() => addSignalListener('SIGUSR1', () => {}), 'NotSupported');
126        removeSignalListener('SIGUSR1', () => {});
127    "#,
128    );
129}