tosproc.nim 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. discard """
  2. joinable: false
  3. """
  4. #[
  5. joinable: false
  6. because it'd need cleanup up stdout
  7. see also: tests/osproc/*.nim; consider merging those into a single test here
  8. (easier to factor and test more things as a single self contained test)
  9. ]#
  10. import std/[assertions, syncio]
  11. when defined(case_testfile): # compiled test file for child process
  12. from posix import exitnow
  13. proc c_exit2(code: cint): void {.importc: "_exit", header: "<unistd.h>".}
  14. import os
  15. var a = 0
  16. proc fun(b = 0) =
  17. a.inc
  18. if a mod 10000000 == 0: # prevents optimizing it away
  19. echo a
  20. fun(b+1)
  21. proc main() =
  22. let args = commandLineParams()
  23. echo (msg: "child binary", pid: getCurrentProcessId())
  24. let arg = args[0]
  25. echo (arg: arg)
  26. case arg
  27. of "exit_0":
  28. if true: quit(0)
  29. of "exit_1":
  30. if true: quit(1)
  31. of "exit_2":
  32. if true: quit(2)
  33. of "exit_42":
  34. if true: quit(42)
  35. of "exitnow_139":
  36. if true: exitnow(139)
  37. of "c_exit2_139":
  38. if true: c_exit2(139)
  39. of "quit_139":
  40. # `exitStatusLikeShell` doesn't distinguish between a process that
  41. # exit(139) and a process that gets killed with `SIGSEGV` because
  42. # 139 = 11 + 128 = SIGSEGV + 128.
  43. # However, as #10249 shows, this leads to bad debugging experience
  44. # when a child process dies with SIGSEGV, leaving no trace of why it
  45. # failed. The shell (and lldb debugger) solves that by inserting a
  46. # helpful msg: `segmentation fault` when it detects a signal killed
  47. # the child.
  48. # todo: expose an API that will show more diagnostic, returning
  49. # (exitCode, signal) instead of just `shellExitCode`.
  50. if true: quit(139)
  51. of "exit_recursion": # stack overflow by infinite recursion
  52. fun()
  53. echo a
  54. of "exit_array": # bad array access
  55. echo args[1]
  56. main()
  57. elif defined(case_testfile2):
  58. import strutils
  59. let x = stdin.readLine()
  60. echo x.parseInt + 5
  61. elif defined(case_testfile3):
  62. echo "start ta_out"
  63. stdout.writeLine("to stdout")
  64. stdout.flushFile()
  65. stdout.writeLine("to stdout")
  66. stdout.flushFile()
  67. stderr.writeLine("to stderr")
  68. stderr.flushFile()
  69. stderr.writeLine("to stderr")
  70. stderr.flushFile()
  71. stdout.writeLine("to stdout")
  72. stdout.flushFile()
  73. stdout.writeLine("to stdout")
  74. stdout.flushFile()
  75. echo "end ta_out"
  76. elif defined(case_testfile4):
  77. import system # we could remove that
  78. quit(QuitFailure)
  79. else: # main driver
  80. import stdtest/[specialpaths, unittest_light]
  81. import os, osproc, strutils
  82. const nim = getCurrentCompilerExe()
  83. const sourcePath = currentSourcePath()
  84. let dir = getCurrentDir() / "tests" / "osproc"
  85. template deferScoped(cleanup, body) =
  86. # pending https://github.com/nim-lang/RFCs/issues/236#issuecomment-646855314
  87. # xxx move to std/sugar or (preferably) some low level module
  88. try: body
  89. finally: cleanup
  90. # we're testing `execShellCmd` so don't rely on it to compile test file
  91. # note: this should be exported in posix.nim
  92. proc c_system(cmd: cstring): cint {.importc: "system", header: "<stdlib.h>".}
  93. proc compileNimProg(opt: string, name: string): string =
  94. result = buildDir / name.addFileExt(ExeExt)
  95. let cmd = "$# c -o:$# --hints:off $# $#" % [nim.quoteShell, result.quoteShell, opt, sourcePath.quoteShell]
  96. doAssert c_system(cmd) == 0, $cmd
  97. doAssert result.fileExists
  98. block execShellCmdTest:
  99. let output = compileNimProg("-d:release -d:case_testfile", "D20190111T024543")
  100. ## use it
  101. template runTest(arg: string, expected: int) =
  102. echo (arg2: arg, expected2: expected)
  103. assertEquals execShellCmd(output & " " & arg), expected
  104. runTest("exit_0", 0)
  105. runTest("exitnow_139", 139)
  106. runTest("c_exit2_139", 139)
  107. when defined(posix):
  108. runTest("quit_139", 127) # The quit value gets saturated to 127
  109. else:
  110. runTest("quit_139", 139)
  111. block execCmdTest:
  112. let output = compileNimProg("-d:release -d:case_testfile", "D20220705T221100")
  113. doAssert execCmd(output & " exit_0") == 0
  114. doAssert execCmd(output & " exit_1") == 1
  115. doAssert execCmd(output & " exit_2") == 2
  116. doAssert execCmd(output & " exit_42") == 42
  117. import std/streams
  118. block execProcessTest:
  119. let dir = sourcePath.parentDir
  120. let (_, err) = execCmdEx(nim & " c " & quoteShell(dir / "osproctest.nim"))
  121. doAssert err == 0
  122. let exePath = dir / addFileExt("osproctest", ExeExt)
  123. let outStr1 = execProcess(exePath, workingDir = dir, args = ["foo",
  124. "b A r"], options = {})
  125. doAssert outStr1 == dir & "\nfoo\nb A r\n"
  126. const testDir = "t e st"
  127. createDir(testDir)
  128. doAssert dirExists(testDir)
  129. let outStr2 = execProcess(exePath, workingDir = testDir, args = ["x yz"],
  130. options = {})
  131. doAssert outStr2 == absolutePath(testDir) & "\nx yz\n"
  132. removeDir(testDir)
  133. # test for PipeOutStream
  134. var
  135. p = startProcess(exePath, args = ["abcdefghi", "foo", "bar", "0123456"])
  136. outStrm = p.peekableOutputStream
  137. var tmp: string
  138. doAssert outStrm.readLine(tmp)
  139. doAssert outStrm.readChar == 'a'
  140. doAssert outStrm.peekChar == 'b'
  141. doAssert outStrm.readChar == 'b'
  142. doAssert outStrm.readChar == 'c'
  143. doAssert outStrm.peekChar == 'd'
  144. doAssert outStrm.peekChar == 'd'
  145. doAssert outStrm.readChar == 'd'
  146. doAssert outStrm.readStr(2) == "ef"
  147. doAssert outStrm.peekStr(2) == "gh"
  148. doAssert outStrm.peekStr(2) == "gh"
  149. doAssert outStrm.readStr(1) == "g"
  150. doAssert outStrm.readStr(3) == "hi\n"
  151. doAssert outStrm.readLine == "foo"
  152. doAssert outStrm.readChar == 'b'
  153. doAssert outStrm.peekChar == 'a'
  154. doAssert outStrm.readLine == "ar"
  155. tmp.setLen(4)
  156. tmp[0] = 'n'
  157. doAssert outStrm.readDataStr(tmp, 1..3) == 3
  158. doAssert tmp == "n012"
  159. doAssert outStrm.peekStr(3) == "345"
  160. doAssert outStrm.readDataStr(tmp, 1..2) == 2
  161. doAssert tmp == "n342"
  162. doAssert outStrm.peekStr(2) == "56"
  163. doAssert outStrm.readDataStr(tmp, 0..3) == 3
  164. doAssert tmp == "56\n2"
  165. p.close
  166. p = startProcess(exePath, args = ["123"])
  167. outStrm = p.peekableOutputStream
  168. let c = outStrm.peekChar
  169. doAssert outStrm.readLine(tmp)
  170. doAssert tmp[0] == c
  171. tmp.setLen(7)
  172. doAssert outStrm.peekData(addr tmp[0], 7) == 4
  173. doAssert tmp[0..3] == "123\n"
  174. doAssert outStrm.peekData(addr tmp[0], 7) == 4
  175. doAssert tmp[0..3] == "123\n"
  176. doAssert outStrm.readData(addr tmp[0], 7) == 4
  177. doAssert tmp[0..3] == "123\n"
  178. p.close
  179. try:
  180. removeFile(exePath)
  181. except OSError:
  182. discard
  183. block: # test for startProcess (more tests needed)
  184. # bugfix: windows stdin.close was a noop and led to blocking reads
  185. proc startProcessTest(command: string, options: set[ProcessOption] = {
  186. poStdErrToStdOut, poUsePath}, input = ""): tuple[
  187. output: string,
  188. exitCode: int] {.tags:
  189. [ExecIOEffect, ReadIOEffect, RootEffect], gcsafe.} =
  190. var p = startProcess(command, options = options + {poEvalCommand})
  191. var outp = outputStream(p)
  192. if input.len > 0: inputStream(p).write(input)
  193. close inputStream(p)
  194. result = ("", -1)
  195. var line = newStringOfCap(120)
  196. while true:
  197. if outp.readLine(line):
  198. result[0].add(line)
  199. result[0].add("\n")
  200. else:
  201. result[1] = peekExitCode(p)
  202. if result[1] != -1: break
  203. close(p)
  204. var result = startProcessTest("nim r --hints:off -", options = {}, input = "echo 3*4")
  205. doAssert result == ("12\n", 0)
  206. block: # startProcess stdin (replaces old test `tstdin` + `ta_in`)
  207. let output = compileNimProg("-d:case_testfile2", "D20200626T215919")
  208. var p = startProcess(output, dir) # dir not needed though
  209. p.inputStream.write("5\n")
  210. p.inputStream.flush()
  211. var line = ""
  212. var s: seq[string]
  213. while p.outputStream.readLine(line):
  214. s.add line
  215. doAssert s == @["10"]
  216. block:
  217. let output = compileNimProg("-d:case_testfile3", "D20200626T221233")
  218. var x = newStringOfCap(120)
  219. block: # startProcess stdout poStdErrToStdOut (replaces old test `tstdout` + `ta_out`)
  220. var p = startProcess(output, dir, options={poStdErrToStdOut})
  221. deferScoped: p.close()
  222. do:
  223. var sout: seq[string]
  224. while p.outputStream.readLine(x): sout.add x
  225. doAssert sout == @["start ta_out", "to stdout", "to stdout", "to stderr", "to stderr", "to stdout", "to stdout", "end ta_out"]
  226. block: # startProcess stderr (replaces old test `tstderr` + `ta_out`)
  227. var p = startProcess(output, dir, options={})
  228. deferScoped: p.close()
  229. do:
  230. var serr, sout: seq[string]
  231. while p.errorStream.readLine(x): serr.add x
  232. while p.outputStream.readLine(x): sout.add x
  233. doAssert serr == @["to stderr", "to stderr"]
  234. doAssert sout == @["start ta_out", "to stdout", "to stdout", "to stdout", "to stdout", "end ta_out"]
  235. block: # startProcess exit code (replaces old test `texitcode` + `tafalse`)
  236. let output = compileNimProg("-d:case_testfile4", "D20200626T224758")
  237. var p = startProcess(output, dir)
  238. doAssert waitForExit(p) == QuitFailure
  239. p = startProcess(output, dir)
  240. var running = true
  241. while running:
  242. # xxx: avoid busyloop?
  243. running = running(p)
  244. doAssert waitForExit(p) == QuitFailure
  245. # make sure that first call to running() after process exit returns false
  246. p = startProcess(output, dir)
  247. for j in 0..<30: # refs #13449
  248. os.sleep(50)
  249. if not running(p): break
  250. doAssert not running(p)
  251. doAssert waitForExit(p) == QuitFailure # avoid zombies
  252. import std/strtabs
  253. block execProcessTest:
  254. var result = execCmdEx("nim r --hints:off -", options = {}, input = "echo 3*4")
  255. stripLineEnd(result[0])
  256. doAssert result == ("12", 0)
  257. when not defined(windows):
  258. doAssert execCmdEx("ls --nonexistent").exitCode != 0
  259. when false:
  260. # bug: on windows, this raises; on posix, passes
  261. doAssert execCmdEx("nonexistent").exitCode != 0
  262. when defined(posix):
  263. doAssert execCmdEx("echo $FO", env = newStringTable({"FO": "B"})) == ("B\n", 0)
  264. doAssert execCmdEx("echo $PWD", workingDir = "/") == ("/\n", 0)
  265. block: # bug #17749
  266. let output = compileNimProg("-d:case_testfile4", "D20210417T011153")
  267. var p = startProcess(output, dir)
  268. let inp = p.inputStream
  269. var count = 0
  270. when defined(windows):
  271. # xxx we should make osproc.hsWriteData raise IOError on windows, consistent
  272. # with posix; we could also (in addition) make IOError a subclass of OSError.
  273. type SIGPIPEError = OSError
  274. else:
  275. type SIGPIPEError = IOError
  276. doAssertRaises(SIGPIPEError):
  277. for i in 0..<100000:
  278. count.inc
  279. inp.writeLine "ok" # was giving SIGPIPE and crashing
  280. doAssert count >= 100
  281. doAssert waitForExit(p) == QuitFailure
  282. close(p) # xxx isn't that missing in other places?