# Chronos Test Suite # (c) Copyright 2022-Present # Status Research & Development GmbH # # Licensed under either of # Apache License, version 2.0, (LICENSE-APACHEv2) # MIT license (LICENSE-MIT) import std/os import stew/[base10, byteutils] import ".."/chronos/unittest2/asynctests import ".."/chronos/asyncproc when defined(posix): from ".."/chronos/osdefs import SIGKILL when defined(nimHasUsed): {.used.} suite "Asynchronous process management test suite": teardown: checkLeaks() const OutputTests = when defined(windows): [ ("ECHO TESTOUT", "TESTOUT\r\n", ""), ("ECHO TESTERR 1>&2", "", "TESTERR \r\n"), ("ECHO TESTBOTH && ECHO TESTBOTH 1>&2", "TESTBOTH \r\n", "TESTBOTH \r\n") ] else: [ ("echo TESTOUT", "TESTOUT\n", ""), ("echo TESTERR 1>&2", "", "TESTERR\n"), ("echo TESTBOTH && echo TESTBOTH 1>&2", "TESTBOTH\n", "TESTBOTH\n") ] const ExitCodes = [5, 13, 64, 100, 126, 127, 128, 130, 255] proc createBigMessage(size: int): seq[byte] = var message = "MESSAGE" result = newSeq[byte](size) for i in 0 ..< len(result): result[i] = byte(message[i mod len(message)]) when not(defined(windows)): proc getCurrentFD(): int = let local = initTAddress("127.0.0.1:34334") let sock = createAsyncSocket(local.getDomain(), SockType.SOCK_DGRAM, Protocol.IPPROTO_UDP) closeSocket(sock) return int(sock) var markFD = getCurrentFD() asyncTest "execCommand() exit codes test": for item in ExitCodes: let command = "exit " & Base10.toString(uint64(item)) let res = await execCommand(command) check res == item asyncTest "execCommandEx() exit codes and outputs test": for test in OutputTests: let response = await execCommandEx(test[0]) check: response.stdOutput == test[1] response.stdError == test[2] response.status == 0 asyncTest "waitForExit() & peekExitCode() exit codes test": let options = {AsyncProcessOption.EvalCommand} for item in ExitCodes: let command = "exit " & Base10.toString(uint64(item)) let process = await startProcess(command, options = options) try: let res = await process.waitForExit(InfiniteDuration) check: res == item process.peekExitCode().tryGet() == item process.running().tryGet() == false finally: await process.closeWait() asyncTest "addProcess() test": var handlerFut = newFuture[void]("process.handler.future") pidFd: ProcessHandle processCounter = 0 processExitCode = 0 process: AsyncProcessRef proc processHandler(udata: pointer) {.gcsafe.} = processCounter = cast[int](udata) processExitCode = process.peekExitCode().valueOr: handlerFut.fail(newException(ValueError, osErrorMsg(error))) return let res = removeProcess2(pidFd) if res.isErr(): handlerFut.fail(newException(ValueError, osErrorMsg(res.error()))) else: handlerFut.complete() let options = {AsyncProcessOption.EvalCommand} command = when defined(windows): "tests\\testproc.bat timeout1" else: "tests/testproc.sh timeout1" process = await startProcess(command, options = options) try: pidFd = block: let res = addProcess2(process.pid(), processHandler, cast[pointer](31337)) if res.isErr(): raiseAssert osErrorMsg(res.error()) res.get() await handlerFut.wait(5.seconds) check: processExitCode == 1 processCounter == 31337 finally: await process.closeWait() asyncTest "STDIN stream test": let command = when defined(windows): "tests\\testproc.bat stdin" else: "tests/testproc.sh stdin" options = {AsyncProcessOption.EvalCommand} shellHeader = "STDIN DATA: ".toBytes() smallTest = when defined(windows): "SMALL AMOUNT\r\n".toBytes() else: "SMALL AMOUNT\n".toBytes() let bigTest = when defined(windows): var res = createBigMessage(256) res.add(byte(0x0D)) res.add(byte(0x0A)) res else: var res = createBigMessage(256) res.add(byte(0x0A)) res for item in [smallTest, bigTest]: let process = await startProcess(command, options = options, stdinHandle = AsyncProcess.Pipe, stdoutHandle = AsyncProcess.Pipe) try: await process.stdinStream.write(item) let stdoutDataFut = process.stdoutStream.read() let res = await process.waitForExit(InfiniteDuration) await allFutures(stdoutDataFut) check: res == 0 stdoutDataFut.read() == shellHeader & item finally: await process.closeWait() asyncTest "STDOUT and STDERR streams test": let options = {AsyncProcessOption.EvalCommand} for test in OutputTests: let process = await startProcess(test[0], options = options, stdoutHandle = AsyncProcess.Pipe, stderrHandle = AsyncProcess.Pipe) try: let outBytesFut = process.stdoutStream.read() let errBytesFut = process.stderrStream.read() let res = await process.waitForExit(InfiniteDuration) await allFutures(outBytesFut, errBytesFut) check: string.fromBytes(outBytesFut.read()) == test[1] string.fromBytes(errBytesFut.read()) == test[2] res == 0 finally: await process.closeWait() asyncTest "STDERR to STDOUT streams test": let options = {AsyncProcessOption.EvalCommand, AsyncProcessOption.StdErrToStdOut} let command = when defined(windows): "ECHO TESTSTDOUT && ECHO TESTSTDERR 1>&2" else: "echo TESTSTDOUT && echo TESTSTDERR 1>&2" let expect = when defined(windows): "TESTSTDOUT \r\nTESTSTDERR \r\n" else: "TESTSTDOUT\nTESTSTDERR\n" let process = await startProcess(command, options = options, stdoutHandle = AsyncProcess.Pipe) try: let outBytesFut = process.stdoutStream.read() let res = await process.waitForExit(InfiniteDuration) await allFutures(outBytesFut) check: string.fromBytes(outBytesFut.read()) == expect res == 0 finally: await process.closeWait() asyncTest "Capture big amount of bytes from STDOUT stream test": when sizeof(int) == 4: skip() else: let options = {AsyncProcessOption.EvalCommand} let command = when defined(windows): "tests\\testproc.bat bigdata" else: "tests/testproc.sh bigdata" let expect = when defined(windows): 100_000 * (64 + 2) else: 100_000 * (64 + 1) let process = await startProcess(command, options = options, stdoutHandle = AsyncProcess.Pipe, stderrHandle = AsyncProcess.Pipe) try: let outBytesFut = process.stdoutStream.read() let errBytesFut = process.stderrStream.read() let res = await process.waitForExit(InfiniteDuration) await allFutures(outBytesFut, errBytesFut) check: res == 0 len(outBytesFut.read()) == expect len(errBytesFut.read()) == 0 finally: await process.closeWait() asyncTest "Long-waiting waitForExit() test": let command = when defined(windows): ("tests\\testproc.bat", "timeout2") else: ("tests/testproc.sh", "timeout2") let process = await startProcess(command[0], arguments = @[command[1]]) try: let res = await process.waitForExit(InfiniteDuration) check res == 2 finally: await process.closeWait() asyncTest "waitForExit(duration) test": let command = when defined(windows): ("tests\\testproc.bat", "timeout10") else: ("tests/testproc.sh", "timeout10") let expect = when defined(windows): 0 else: 128 + int(SIGKILL) let process = await startProcess(command[0], arguments = @[command[1]]) try: let res = await process.waitForExit(1.seconds) check res == expect finally: await process.closeWait() asyncTest "Child process environment test": let command = when defined(windows): ("tests\\testproc.bat", "envtest", 0, "CHILDPROCESSTEST\r\n") else: ("tests/testproc.sh", "envtest", 0, "CHILDPROCESSTEST\n") let env = getProcessEnvironment() env["CHRONOSASYNC"] = "CHILDPROCESSTEST" let process = await startProcess(command[0], arguments = @[command[1]], environment = env, stdoutHandle = AsyncProcess.Pipe) try: let outBytesFut = process.stdoutStream.read() let res = await process.waitForExit(InfiniteDuration) let outBytes = await outBytesFut check: res == command[2] string.fromBytes(outBytes) == command[3] finally: await process.closeWait() test "getProcessEnvironment() test": let env = getProcessEnvironment() when defined(windows): check len(env["SYSTEMROOT"]) > 0 else: check len(env["USER"]) > 0 asyncTest "Multiple processes waiting test": const ProcessesCount = 50 let command = when defined(windows): ("tests\\testproc.bat", "timeout2", 2) else: ("tests/testproc.sh", "timeout2", 2) var processes: seq[AsyncProcessRef] for n in 0 ..< ProcessesCount: let process = await startProcess(command[0], arguments = @[command[1]]) processes.add(process) try: var pending: seq[Future[int]] for process in processes: pending.add(process.waitForExit(10.seconds)) await allFutures(pending) for index in 0 ..< ProcessesCount: check pending[index].read() == command[2] finally: var pending: seq[Future[void]] for process in processes: pending.add(process.closeWait()) await allFutures(pending) asyncTest "Multiple processes exit codes test": const ProcessesCount = 50 let options = {AsyncProcessOption.EvalCommand} var processes: seq[AsyncProcessRef] for n in 0 ..< ProcessesCount: let command = "exit " & Base10.toString(uint64(n)) process = await startProcess(command, options = options) processes.add(process) try: var pending: seq[Future[int]] for process in processes: pending.add(process.waitForExit(10.seconds)) await allFutures(pending) for index in 0 ..< ProcessesCount: check pending[index].read() == index finally: var pending: seq[Future[void]] for process in processes: pending.add(process.closeWait()) await allFutures(pending) asyncTest "Multiple processes data capture test": const ProcessesCount = 50 let options = {AsyncProcessOption.EvalCommand} var processes: seq[AsyncProcessRef] for n in 0 ..< ProcessesCount: let command = when defined(windows): "ECHO TEST" & $n else: "echo TEST" & $n let process = await startProcess(command, options = options, stdoutHandle = AsyncProcess.Pipe) processes.add(process) try: var pendingReaders: seq[Future[seq[byte]]] var pendingWaiters: seq[Future[int]] for process in processes: pendingReaders.add(process.stdoutStream.read()) pendingWaiters.add(process.waitForExit(10.seconds)) await allFutures(pendingReaders) await allFutures(pendingWaiters) for index in 0 ..< ProcessesCount: let expect = when defined(windows): "TEST" & $index & "\r\n" else: "TEST" & $index & "\n" check string.fromBytes(pendingReaders[index].read()) == expect check pendingWaiters[index].read() == 0 finally: var pending: seq[Future[void]] for process in processes: pending.add(process.closeWait()) await allFutures(pending) asyncTest "terminate() test": let command = when defined(windows): ("tests\\testproc.bat", "timeout10", 0) else: ("tests/testproc.sh", "timeout10", 143) # 128 + SIGTERM let process = await startProcess(command[0], arguments = @[command[1]]) try: let resFut = process.waitForExit(InfiniteDuration) check process.terminate().isOk() let res = await resFut check res == command[2] finally: await process.closeWait() asyncTest "kill() test": let command = when defined(windows): ("tests\\testproc.bat", "timeout10", 0) else: ("tests/testproc.sh", "timeout10", 137) # 128 + SIGKILL let process = await startProcess(command[0], arguments = @[command[1]]) try: let resFut = process.waitForExit(InfiniteDuration) check process.kill().isOk() let res = await resFut check res == command[2] finally: await process.closeWait() asyncTest "killAndWaitForExit() test": let command = when defined(windows): ("tests\\testproc.bat", "timeout10", 0) else: ("tests/testproc.sh", "timeout10", 128 + int(SIGKILL)) let process = await startProcess(command[0], arguments = @[command[1]]) try: let exitCode = await process.killAndWaitForExit(10.seconds) check exitCode == command[2] finally: await process.closeWait() asyncTest "terminateAndWaitForExit() test": let command = when defined(windows): ("tests\\testproc.bat", "timeout10", 0) else: ("tests/testproc.sh", "timeout10", 128 + int(SIGTERM)) let process = await startProcess(command[0], arguments = @[command[1]]) try: let exitCode = await process.terminateAndWaitForExit(10.seconds) check exitCode == command[2] finally: await process.closeWait() asyncTest "terminateAndWaitForExit() timeout test": when defined(windows): skip() else: let command = ("tests/testproc.sh", "noterm", 128 + int(SIGKILL)) process = await startProcess(command[0], arguments = @[command[1]]) # We should wait here to allow `bash` execute `trap` command, otherwise # our test script will be killed with SIGTERM. Increase this timeout # if test become flaky. await sleepAsync(1.seconds) try: expect AsyncProcessTimeoutError: let exitCode {.used.} = await process.terminateAndWaitForExit(1.seconds) let exitCode = await process.killAndWaitForExit(10.seconds) check exitCode == command[2] finally: await process.closeWait() test "File descriptors leaks test": when defined(windows): skip() else: check getCurrentFD() == markFD