/src/test/shared/utilities/childProcess.test.ts

https://github.com/aws/aws-toolkit-vscode · TypeScript · 434 lines · 316 code · 86 blank · 32 comment · 20 complexity · 20b35caa4097185bba3663914646c404 MD5 · raw file

  1. /*!
  2. * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  3. * SPDX-License-Identifier: Apache-2.0
  4. */
  5. import * as assert from 'assert'
  6. import * as del from 'del'
  7. import * as fs from 'fs'
  8. import * as os from 'os'
  9. import * as path from 'path'
  10. import { makeTemporaryToolkitFolder } from '../../../shared/filesystemUtilities'
  11. import { ChildProcess, ChildProcessResult } from '../../../shared/utilities/childProcess'
  12. describe('ChildProcess', async () => {
  13. let tempFolder: string
  14. beforeEach(async () => {
  15. // Make a temp folder for all these tests
  16. // Stick some temp credentials files in there to load from
  17. tempFolder = await makeTemporaryToolkitFolder()
  18. })
  19. afterEach(() => {
  20. del.sync([tempFolder], { force: true })
  21. })
  22. describe('run', async () => {
  23. if (process.platform === 'win32') {
  24. it('runs and captures stdout - windows', async () => {
  25. const batchFile = path.join(tempFolder, 'test-script.bat')
  26. writeBatchFile(batchFile)
  27. const childProcess = new ChildProcess(batchFile)
  28. const result = await childProcess.run()
  29. validateChildProcessResult({
  30. childProcessResult: result,
  31. expectedExitCode: 0,
  32. expectedOutput: 'hi',
  33. })
  34. })
  35. it('runs cmd files containing a space in the filename and folder', async () => {
  36. const subfolder: string = path.join(tempFolder, 'sub folder')
  37. const command: string = path.join(subfolder, 'test script.cmd')
  38. fs.mkdirSync(subfolder)
  39. writeWindowsCommandFile(command)
  40. const childProcess = new ChildProcess(command)
  41. const result = await childProcess.run()
  42. validateChildProcessResult({
  43. childProcessResult: result,
  44. expectedExitCode: 0,
  45. expectedOutput: 'hi',
  46. })
  47. })
  48. it('errs when starting twice - windows', async () => {
  49. const batchFile = path.join(tempFolder, 'test-script.bat')
  50. writeBatchFile(batchFile)
  51. const childProcess = new ChildProcess(batchFile)
  52. // We want to verify that the error is thrown even if the first
  53. // invocation is still in progress, so we don't await the promise.
  54. // tslint:disable-next-line:no-floating-promises
  55. childProcess.run()
  56. try {
  57. await childProcess.run()
  58. } catch (err) {
  59. return
  60. }
  61. assert.fail('Expected exception, but none was thrown.')
  62. })
  63. } else {
  64. it('runs and captures stdout - unix', async () => {
  65. const scriptFile = path.join(tempFolder, 'test-script.sh')
  66. writeShellFile(scriptFile)
  67. const childProcess = new ChildProcess(scriptFile)
  68. const result = await childProcess.run()
  69. validateChildProcessResult({
  70. childProcessResult: result,
  71. expectedExitCode: 0,
  72. expectedOutput: 'hi',
  73. })
  74. })
  75. it('errs when starting twice - unix', async () => {
  76. const scriptFile = path.join(tempFolder, 'test-script.sh')
  77. writeShellFile(scriptFile)
  78. const childProcess = new ChildProcess(scriptFile)
  79. // We want to verify that the error is thrown even if the first
  80. // invocation is still in progress, so we don't await the promise.
  81. // tslint:disable-next-line:no-floating-promises
  82. childProcess.run()
  83. try {
  84. await childProcess.run()
  85. } catch (err) {
  86. return
  87. }
  88. assert.fail('Expected exception, but none was thrown.')
  89. })
  90. } // END Linux only tests
  91. it('runs scripts containing a space in the filename and folder', async () => {
  92. const subfolder: string = path.join(tempFolder, 'sub folder')
  93. let command: string
  94. fs.mkdirSync(subfolder)
  95. if (process.platform === 'win32') {
  96. command = path.join(subfolder, 'test script.bat')
  97. writeBatchFile(command)
  98. } else {
  99. command = path.join(subfolder, 'test script.sh')
  100. writeShellFile(command)
  101. }
  102. const childProcess = new ChildProcess(command)
  103. const result = await childProcess.run()
  104. validateChildProcessResult({
  105. childProcessResult: result,
  106. expectedExitCode: 0,
  107. expectedOutput: 'hi',
  108. })
  109. })
  110. it('reports error for missing executable', async () => {
  111. const batchFile = path.join(tempFolder, 'nonExistentScript')
  112. const childProcess = new ChildProcess(batchFile)
  113. const result = await childProcess.run()
  114. assert.notStrictEqual(result.exitCode, 0)
  115. assert.notStrictEqual(result.error, undefined)
  116. })
  117. function validateChildProcessResult({
  118. childProcessResult,
  119. expectedExitCode,
  120. expectedOutput,
  121. }: {
  122. childProcessResult: ChildProcessResult
  123. expectedExitCode: number
  124. expectedOutput: string
  125. }) {
  126. assert.strictEqual(
  127. childProcessResult.exitCode,
  128. expectedExitCode,
  129. `Expected exit code ${expectedExitCode}, got ${childProcessResult.exitCode}`
  130. )
  131. assert.strictEqual(
  132. childProcessResult.stdout,
  133. expectedOutput,
  134. `Expected stdout to be ${expectedOutput} , got: ${childProcessResult.stdout}`
  135. )
  136. }
  137. })
  138. describe('start', async () => {
  139. async function assertRegularRun(childProcess: ChildProcess): Promise<void> {
  140. await new Promise<void>(async (resolve, reject) => {
  141. await childProcess.start({
  142. onStdout: text => {
  143. assert.strictEqual(text, 'hi' + os.EOL, 'Unexpected stdout')
  144. },
  145. onClose: (code, signal) => {
  146. assert.strictEqual(code, 0, 'Unexpected close code')
  147. resolve()
  148. },
  149. })
  150. })
  151. }
  152. if (process.platform === 'win32') {
  153. it('starts and captures stdout - windows', async () => {
  154. const batchFile = path.join(tempFolder, 'test-script.bat')
  155. writeBatchFile(batchFile)
  156. const childProcess = new ChildProcess(batchFile)
  157. await assertRegularRun(childProcess)
  158. })
  159. it('runs cmd files containing a space in the filename and folder', async () => {
  160. const subfolder: string = path.join(tempFolder, 'sub folder')
  161. const command: string = path.join(subfolder, 'test script.cmd')
  162. fs.mkdirSync(subfolder)
  163. writeWindowsCommandFile(command)
  164. const childProcess = new ChildProcess(command)
  165. await assertRegularRun(childProcess)
  166. })
  167. it('errs when starting twice - windows', async () => {
  168. const batchFile = path.join(tempFolder, 'test-script.bat')
  169. writeBatchFile(batchFile)
  170. const childProcess = new ChildProcess(batchFile)
  171. // We want to verify that the error is thrown even if the first
  172. // invocation is still in progress, so we don't await the promise.
  173. // tslint:disable-next-line:no-floating-promises
  174. childProcess.start({})
  175. try {
  176. await childProcess.start({})
  177. } catch (err) {
  178. return
  179. }
  180. assert.fail('Expected exception, but none was thrown.')
  181. })
  182. } // END Windows only tests
  183. if (process.platform !== 'win32') {
  184. it('runs and captures stdout - unix', async () => {
  185. const scriptFile = path.join(tempFolder, 'test-script.sh')
  186. writeShellFile(scriptFile)
  187. const childProcess = new ChildProcess(scriptFile)
  188. await assertRegularRun(childProcess)
  189. })
  190. it('errs when starting twice - unix', async () => {
  191. const scriptFile = path.join(tempFolder, 'test-script.sh')
  192. writeShellFile(scriptFile)
  193. const childProcess = new ChildProcess(scriptFile)
  194. // We want to verify that the error is thrown even if the first
  195. // invocation is still in progress, so we don't await the promise.
  196. // tslint:disable-next-line:no-floating-promises
  197. childProcess.start({})
  198. try {
  199. await childProcess.start({})
  200. } catch (err) {
  201. return
  202. }
  203. assert.fail('Expected exception, but none was thrown.')
  204. })
  205. } // END Linux only tests
  206. it('runs scripts containing a space in the filename and folder', async () => {
  207. const subfolder: string = path.join(tempFolder, 'sub folder')
  208. let command: string
  209. fs.mkdirSync(subfolder)
  210. if (process.platform === 'win32') {
  211. command = path.join(subfolder, 'test script.bat')
  212. writeBatchFile(command)
  213. } else {
  214. command = path.join(subfolder, 'test script.sh')
  215. writeShellFile(command)
  216. }
  217. const childProcess = new ChildProcess(command)
  218. await assertRegularRun(childProcess)
  219. })
  220. it('reports error for missing executable', async () => {
  221. const batchFile = path.join(tempFolder, 'nonExistentScript')
  222. const childProcess = new ChildProcess(batchFile)
  223. await new Promise<void>(async (resolve, reject) => {
  224. await childProcess.start({
  225. onClose: (code, signal) => {
  226. assert.notStrictEqual(code, 0, 'Expected an error close code')
  227. resolve()
  228. },
  229. })
  230. })
  231. })
  232. })
  233. describe('kill & killed', async () => {
  234. if (process.platform === 'win32') {
  235. // tslint:disable-next-line:max-line-length
  236. it('detects running processes and successfully sends a kill signal to a running process - Windows', async () => {
  237. const batchFile = path.join(tempFolder, 'test-script.bat')
  238. writeBatchFileWithDelays(batchFile)
  239. const childProcess = new ChildProcess(batchFile)
  240. // awaiting this means that the script finishes before it can be killed
  241. // worst case scenario, this script will take 2 seconds to run its course
  242. // tslint:disable-next-line: no-floating-promises
  243. childProcess.run()
  244. assert.strictEqual(childProcess.killed, false)
  245. childProcess.kill()
  246. await new Promise<void>(resolve => {
  247. setTimeout(() => {
  248. assert.strictEqual(childProcess.killed, true)
  249. resolve()
  250. }, 100)
  251. })
  252. })
  253. it('can not kill previously killed processes - Windows', async () => {
  254. const batchFile = path.join(tempFolder, 'test-script.bat')
  255. writeBatchFileWithDelays(batchFile)
  256. const childProcess = new ChildProcess(batchFile)
  257. // awaiting this means that the script finishes before it can be killed
  258. // worst case scenario, this script will take 2 seconds to run its course
  259. // tslint:disable-next-line: no-floating-promises
  260. childProcess.run()
  261. childProcess.kill()
  262. await new Promise<void>(resolve => {
  263. setTimeout(() => {
  264. assert.strictEqual(childProcess.killed, true)
  265. resolve()
  266. }, 100)
  267. })
  268. await new Promise<void>(resolve => {
  269. setTimeout(() => {
  270. assert.throws(() => {
  271. childProcess.kill()
  272. })
  273. resolve()
  274. }, 100)
  275. })
  276. })
  277. } // END Windows-only tests
  278. if (process.platform !== 'win32') {
  279. // tslint:disable-next-line:max-line-length
  280. it('detects running processes and successfully sends a kill signal to a running process - Unix', async () => {
  281. const scriptFile = path.join(tempFolder, 'test-script.sh')
  282. writeShellFileWithDelays(scriptFile)
  283. const childProcess = new ChildProcess(scriptFile)
  284. // awaiting this means that the script finishes before it can be killed
  285. // worst case scenario, this script will take 2 seconds to run its course
  286. // tslint:disable-next-line: no-floating-promises
  287. childProcess.run()
  288. assert.strictEqual(childProcess.killed, false)
  289. childProcess.kill()
  290. await new Promise<void>(resolve => {
  291. setTimeout(() => {
  292. assert.strictEqual(childProcess.killed, true)
  293. resolve()
  294. }, 100)
  295. })
  296. })
  297. it('can not kill previously killed processes - Unix', async () => {
  298. const scriptFile = path.join(tempFolder, 'test-script.sh')
  299. writeShellFileWithDelays(scriptFile)
  300. const childProcess = new ChildProcess(scriptFile)
  301. // awaiting this means that the script finishes before it can be killed
  302. // worst case scenario, this script will take 2 seconds to run its course
  303. // tslint:disable-next-line: no-floating-promises
  304. childProcess.run()
  305. childProcess.kill()
  306. await new Promise<void>(resolve => {
  307. setTimeout(() => {
  308. assert.strictEqual(childProcess.killed, true)
  309. resolve()
  310. }, 100)
  311. })
  312. await new Promise<void>(resolve => {
  313. setTimeout(() => {
  314. assert.throws(() => {
  315. childProcess.kill()
  316. })
  317. resolve()
  318. }, 100)
  319. })
  320. })
  321. } // END Unix-only tests
  322. })
  323. function writeBatchFile(filename: string): void {
  324. fs.writeFileSync(filename, '@echo hi')
  325. }
  326. function writeBatchFileWithDelays(filename: string): void {
  327. const file = `
  328. @echo hi
  329. SLEEP 2
  330. @echo bye`
  331. fs.writeFileSync(filename, file)
  332. }
  333. function writeWindowsCommandFile(filename: string): void {
  334. fs.writeFileSync(filename, `@echo OFF${os.EOL}echo hi`)
  335. }
  336. function writeShellFile(filename: string): void {
  337. fs.writeFileSync(filename, 'echo hi')
  338. fs.chmodSync(filename, 0o744)
  339. }
  340. function writeShellFileWithDelays(filename: string): void {
  341. const file = `
  342. echo hi
  343. sleep 2
  344. echo bye`
  345. fs.writeFileSync(filename, file)
  346. fs.chmodSync(filename, 0o744)
  347. }
  348. })