| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | */ |
| 7 | |
| 8 | import type {EjecaChildProcess} from 'shared/ejeca'; |
| 9 | |
| 10 | import {ejeca} from 'shared/ejeca'; |
| 11 | |
| 12 | describe('test running binaries', () => { |
| 13 | it('we can get both streams and awaitables', async () => { |
| 14 | const spawned = ejeca('node', ['-e', "console.log('uno') ; console.error('dos')"]); |
| 15 | let streamOut = ''; |
| 16 | let streamErr = ''; |
| 17 | spawned.stdout?.on('data', data => { |
| 18 | streamOut = data.toString(); |
| 19 | }); |
| 20 | spawned.stderr?.on('data', data => { |
| 21 | streamErr = data.toString(); |
| 22 | }); |
| 23 | const result = await spawned; |
| 24 | expect(result.stdout).toBe('uno'); |
| 25 | expect(streamOut).toBe('uno\n'); |
| 26 | expect(result.stderr).toBe('dos'); |
| 27 | expect(streamErr).toBe('dos\n'); |
| 28 | }); |
| 29 | |
| 30 | it('we can set pass stdin as a string', async () => { |
| 31 | const spawned = ejeca('node', [], {input: 'console.log("hemlo")'}); |
| 32 | expect((await spawned).stdout).toBe('hemlo'); |
| 33 | }); |
| 34 | |
| 35 | it('when erroring out the command name is present', async () => { |
| 36 | const spawned = ejeca('node', ['-', 'foo("bar")'], {input: 'babar'}); |
| 37 | await expect(spawned).rejects.toThrowErrorMatchingInlineSnapshot( |
| 38 | `"Command \`node "-" "foo(\\"bar\\")"\` exited with non-zero status with exit code 1"`, |
| 39 | ); |
| 40 | }); |
| 41 | |
| 42 | it('handles env var options correctly', async () => { |
| 43 | // We use yarn for our tests, so YARN_IGNORE_PATH should always be set |
| 44 | const input = |
| 45 | 'console.log("YARN_IGNORE_PATH" in process.env ? process.env.YARN_IGNORE_PATH : "not set")'; |
| 46 | let spawned = ejeca('node', ['-'], {input}); |
| 47 | expect((await spawned).stdout).not.toBe('babar'); |
| 48 | spawned = ejeca('node', ['-'], {input, env: {YARN_IGNORE_PATH: 'babar'}}); |
| 49 | expect((await spawned).stdout).toBe('babar'); |
| 50 | spawned = ejeca('node', ['-'], {input, env: {YARN_IGNORE_PATH: 'babar'}, extendEnv: true}); |
| 51 | expect((await spawned).stdout).toBe('babar'); |
| 52 | spawned = ejeca('node', ['-'], { |
| 53 | input, |
| 54 | env: {FOO: 'bar', PATH: process.env.PATH}, |
| 55 | extendEnv: false, |
| 56 | }); |
| 57 | expect((await spawned).stdout).toBe('not set'); |
| 58 | }); |
| 59 | |
| 60 | it('can specify whether to strip the final line', async () => { |
| 61 | const input = 'console.log("hello")'; |
| 62 | let spawned; |
| 63 | spawned = ejeca('node', ['-'], {input}); |
| 64 | expect((await spawned).stdout).toBe('hello'); |
| 65 | spawned = ejeca('node', ['-'], {input, stripFinalNewline: true}); |
| 66 | expect((await spawned).stdout).toBe('hello'); |
| 67 | spawned = ejeca('node', ['-'], {input, stripFinalNewline: false}); |
| 68 | expect((await spawned).stdout).toBe('hello\n'); |
| 69 | }); |
| 70 | |
| 71 | it('we can specify stdin', async () => { |
| 72 | let spawned; |
| 73 | spawned = ejeca('node', [], {input: 'console.log("hemlo")'}); |
| 74 | expect((await spawned).stdout).toBe('hemlo'); |
| 75 | spawned = ejeca('node', [], {input: 'console.log("hemlo")', stdin: 'pipe'}); |
| 76 | expect((await spawned).stdout).toBe('hemlo'); |
| 77 | spawned = ejeca('node', [], {input: 'console.log("hemlo")', stdin: 'ignore'}); |
| 78 | expect((await spawned).stdout).toBe(''); |
| 79 | }); |
| 80 | }); |
| 81 | |
| 82 | describe('test killing process', () => { |
| 83 | const sighandlerScript = ` |
| 84 | const argv = process.argv; |
| 85 | const sleep = (waitTimeInMs) => new Promise(resolve => setTimeout(resolve, waitTimeInMs)); |
| 86 | |
| 87 | (async function main() { |
| 88 | let exitOnSigTerm = false; |
| 89 | let delay = 0; |
| 90 | |
| 91 | if(argv.length > 2) { |
| 92 | delay = parseInt(argv[2]); |
| 93 | if(argv[argv.length - 1] !== "dontExitOnSigterm") { |
| 94 | exitOnSigTerm = true; |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | process.on('SIGTERM', () => { |
| 99 | console.log("I was asked to stop politely"); |
| 100 | if(exitOnSigTerm) { |
| 101 | process.exit(0) |
| 102 | } |
| 103 | }); |
| 104 | |
| 105 | console.log("Hello"); |
| 106 | |
| 107 | for(let i=0; i < delay; i++) { |
| 108 | await sleep(1000); |
| 109 | } |
| 110 | |
| 111 | console.log("Goodbye"); |
| 112 | })(); |
| 113 | `; |
| 114 | |
| 115 | const spawnAndKill = async ( |
| 116 | pythonArgs: string[] = [], |
| 117 | expectedOut: string = '', |
| 118 | killArgs: Parameters<EjecaChildProcess['kill']> = [], |
| 119 | expectedSignal?: string, |
| 120 | ) => { |
| 121 | const spawned = ejeca('node', ['-', ...pythonArgs], { |
| 122 | input: sighandlerScript, |
| 123 | }); |
| 124 | setTimeout(() => spawned.kill(...killArgs), 1000); |
| 125 | let outo = ''; |
| 126 | let signalo = undefined; |
| 127 | try { |
| 128 | outo = (await spawned).stdout; |
| 129 | } catch (err) { |
| 130 | if (err != null && typeof err === 'object' && 'stdout' in err && 'signal' in err) { |
| 131 | outo = err.stdout as string; |
| 132 | signalo = err.signal; |
| 133 | } |
| 134 | } |
| 135 | expect(outo).toBe(expectedOut); |
| 136 | expect(signalo).toBe(expectedSignal); |
| 137 | }; |
| 138 | |
| 139 | it('kill as sends sigterm by default', async () => { |
| 140 | await spawnAndKill(['3', 'dontExitOnSigterm'], 'Hello\nI was asked to stop politely\nGoodbye'); |
| 141 | }); |
| 142 | |
| 143 | it('sigkill can be set through force kill after a timeout', async () => { |
| 144 | await spawnAndKill( |
| 145 | ['4', 'dontExitOnSigterm'], |
| 146 | 'Hello\nI was asked to stop politely', |
| 147 | ['SIGTERM', {forceKillAfterTimeout: 2000}], |
| 148 | 'SIGKILL', |
| 149 | ); |
| 150 | }); |
| 151 | |
| 152 | it('sending sigkill just kills', async () => { |
| 153 | await spawnAndKill(['100000000000'], 'Hello', ['SIGKILL'], 'SIGKILL'); |
| 154 | }); |
| 155 | }); |
| 156 | |