tosproc.nim 11 KB

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