specs.nim 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. #
  2. #
  3. # Nim Tester
  4. # (c) Copyright 2015 Andreas Rumpf
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. import sequtils, parseutils, strutils, os, streams, parsecfg,
  10. tables, hashes, sets
  11. type TestamentData* = ref object
  12. # better to group globals under 1 object; could group the other ones here too
  13. batchArg*: string
  14. testamentNumBatch*: int
  15. testamentBatch*: int
  16. let testamentData0* = TestamentData()
  17. var compilerPrefix* = findExe("nim")
  18. let isTravis* = existsEnv("TRAVIS")
  19. let isAppVeyor* = existsEnv("APPVEYOR")
  20. let isAzure* = existsEnv("TF_BUILD")
  21. var skips*: seq[string]
  22. type
  23. TTestAction* = enum
  24. actionRun = "run"
  25. actionCompile = "compile"
  26. actionReject = "reject"
  27. TOutputCheck* = enum
  28. ocIgnore = "ignore"
  29. ocEqual = "equal"
  30. ocSubstr = "substr"
  31. TResultEnum* = enum
  32. reNimcCrash, # nim compiler seems to have crashed
  33. reMsgsDiffer, # error messages differ
  34. reFilesDiffer, # expected and given filenames differ
  35. reLinesDiffer, # expected and given line numbers differ
  36. reOutputsDiffer,
  37. reExitcodesDiffer, # exit codes of program or of valgrind differ
  38. reTimeout,
  39. reInvalidPeg,
  40. reCodegenFailure,
  41. reCodeNotFound,
  42. reExeNotFound,
  43. reInstallFailed # package installation failed
  44. reBuildFailed # package building failed
  45. reDisabled, # test is disabled
  46. reJoined, # test is disabled because it was joined into the megatest
  47. reSuccess # test was successful
  48. reInvalidSpec # test had problems to parse the spec
  49. TTarget* = enum
  50. targetC = "c"
  51. targetCpp = "cpp"
  52. targetObjC = "objc"
  53. targetJS = "js"
  54. InlineError* = object
  55. kind*: string
  56. msg*: string
  57. line*, col*: int
  58. ValgrindSpec* = enum
  59. disabled, enabled, leaking
  60. TSpec* = object
  61. # xxx make sure `isJoinableSpec` takes into account each field here.
  62. action*: TTestAction
  63. file*, cmd*: string
  64. input*: string
  65. outputCheck*: TOutputCheck
  66. sortoutput*: bool
  67. output*: string
  68. line*, column*: int
  69. exitCode*: int
  70. msg*: string
  71. ccodeCheck*: seq[string]
  72. maxCodeSize*: int
  73. err*: TResultEnum
  74. inCurrentBatch*: bool
  75. targets*: set[TTarget]
  76. matrix*: seq[string]
  77. nimout*: string
  78. nimoutFull*: bool # whether nimout is all compiler output or a subset
  79. parseErrors*: string # when the spec definition is invalid, this is not empty.
  80. unjoinable*: bool
  81. unbatchable*: bool
  82. # whether this test can be batchable via `NIM_TESTAMENT_BATCH`; only very
  83. # few tests are not batchable; the ones that are not could be turned batchable
  84. # by making the dependencies explicit
  85. useValgrind*: ValgrindSpec
  86. timeout*: float # in seconds, fractions possible,
  87. # but don't rely on much precision
  88. inlineErrors*: seq[InlineError] # line information to error message
  89. debugInfo*: string # debug info to give more context
  90. proc getCmd*(s: TSpec): string =
  91. if s.cmd.len == 0:
  92. result = compilerPrefix & " $target --hints:on -d:testing --nimblePath:build/deps/pkgs $options $file"
  93. else:
  94. result = s.cmd
  95. const
  96. targetToExt*: array[TTarget, string] = ["nim.c", "nim.cpp", "nim.m", "js"]
  97. targetToCmd*: array[TTarget, string] = ["c", "cpp", "objc", "js"]
  98. proc defaultOptions*(a: TTarget): string =
  99. case a
  100. of targetJS: "-d:nodejs"
  101. # once we start testing for `nim js -d:nimbrowser` (eg selenium or similar),
  102. # we can adapt this logic; or a given js test can override with `-u:nodejs`.
  103. else: ""
  104. when not declared(parseCfgBool):
  105. # candidate for the stdlib:
  106. proc parseCfgBool(s: string): bool =
  107. case normalize(s)
  108. of "y", "yes", "true", "1", "on": result = true
  109. of "n", "no", "false", "0", "off": result = false
  110. else: raise newException(ValueError, "cannot interpret as a bool: " & s)
  111. const
  112. inlineErrorMarker = "#[tt."
  113. proc extractErrorMsg(s: string; i: int; line: var int; col: var int; spec: var TSpec): int =
  114. result = i + len(inlineErrorMarker)
  115. inc col, len(inlineErrorMarker)
  116. var kind = ""
  117. while result < s.len and s[result] in IdentChars:
  118. kind.add s[result]
  119. inc result
  120. inc col
  121. var caret = (line, -1)
  122. template skipWhitespace =
  123. while result < s.len and s[result] in Whitespace:
  124. if s[result] == '\n':
  125. col = 1
  126. inc line
  127. else:
  128. inc col
  129. inc result
  130. skipWhitespace()
  131. if result < s.len and s[result] == '^':
  132. caret = (line-1, col)
  133. inc result
  134. inc col
  135. skipWhitespace()
  136. var msg = ""
  137. while result < s.len-1:
  138. if s[result] == '\n':
  139. inc result
  140. inc line
  141. col = 1
  142. elif s[result] == ']' and s[result+1] == '#':
  143. while msg.len > 0 and msg[^1] in Whitespace:
  144. setLen msg, msg.len - 1
  145. inc result
  146. inc col, 2
  147. if kind == "Error": spec.action = actionReject
  148. spec.unjoinable = true
  149. spec.inlineErrors.add InlineError(kind: kind, msg: msg, line: caret[0], col: caret[1])
  150. break
  151. else:
  152. msg.add s[result]
  153. inc result
  154. inc col
  155. proc extractSpec(filename: string; spec: var TSpec): string =
  156. const
  157. tripleQuote = "\"\"\""
  158. specStart = "discard " & tripleQuote
  159. var s = readFile(filename)
  160. var i = 0
  161. var a = -1
  162. var b = -1
  163. var line = 1
  164. var col = 1
  165. while i < s.len:
  166. if (i == 0 or s[i-1] != ' ') and s.continuesWith(specStart, i):
  167. # `s[i-1] == '\n'` would not work because of `tests/stdlib/tbase64.nim` which contains BOM (https://en.wikipedia.org/wiki/Byte_order_mark)
  168. const lineMax = 10
  169. if a != -1:
  170. raise newException(ValueError, "testament spec violation: duplicate `specStart` found: " & $(filename, a, b, line))
  171. elif line > lineMax:
  172. # not overly restrictive, but prevents mistaking some `specStart` as spec if deeep inside a test file
  173. raise newException(ValueError, "testament spec violation: `specStart` should be before line $1, or be indented; info: $2" % [$lineMax, $(filename, a, b, line)])
  174. i += specStart.len
  175. a = i
  176. elif a > -1 and b == -1 and s.continuesWith(tripleQuote, i):
  177. b = i
  178. i += tripleQuote.len
  179. elif s[i] == '\n':
  180. inc line
  181. inc i
  182. col = 1
  183. elif s.continuesWith(inlineErrorMarker, i):
  184. i = extractErrorMsg(s, i, line, col, spec)
  185. else:
  186. inc col
  187. inc i
  188. if a >= 0 and b > a:
  189. result = s.substr(a, b-1).multiReplace({"'''": tripleQuote, "\\31": "\31"})
  190. elif a >= 0:
  191. raise newException(ValueError, "testament spec violation: `specStart` found but not trailing `tripleQuote`: $1" % $(filename, a, b, line))
  192. else:
  193. result = ""
  194. proc parseTargets*(value: string): set[TTarget] =
  195. for v in value.normalize.splitWhitespace:
  196. case v
  197. of "c": result.incl(targetC)
  198. of "cpp", "c++": result.incl(targetCpp)
  199. of "objc": result.incl(targetObjC)
  200. of "js": result.incl(targetJS)
  201. else: raise newException(ValueError, "invalid target: '$#'" % v)
  202. proc addLine*(self: var string; a: string) =
  203. self.add a
  204. self.add "\n"
  205. proc addLine*(self: var string; a, b: string) =
  206. self.add a
  207. self.add b
  208. self.add "\n"
  209. proc initSpec*(filename: string): TSpec =
  210. result.file = filename
  211. proc isCurrentBatch*(testamentData: TestamentData; filename: string): bool =
  212. if testamentData.testamentNumBatch != 0:
  213. hash(filename) mod testamentData.testamentNumBatch == testamentData.testamentBatch
  214. else:
  215. true
  216. proc parseSpec*(filename: string): TSpec =
  217. result.file = filename
  218. let specStr = extractSpec(filename, result)
  219. var ss = newStringStream(specStr)
  220. var p: CfgParser
  221. open(p, ss, filename, 1)
  222. var flags: HashSet[string]
  223. var nimoutFound = false
  224. while true:
  225. var e = next(p)
  226. case e.kind
  227. of cfgKeyValuePair:
  228. let key = e.key.normalize
  229. const whiteListMulti = ["disabled", "ccodecheck"]
  230. ## list of flags that are correctly handled when passed multiple times
  231. ## (instead of being overwritten)
  232. if key notin whiteListMulti:
  233. doAssert key notin flags, $(key, filename)
  234. flags.incl key
  235. case key
  236. of "action":
  237. case e.value.normalize
  238. of "compile":
  239. result.action = actionCompile
  240. of "run":
  241. result.action = actionRun
  242. of "reject":
  243. result.action = actionReject
  244. else:
  245. result.parseErrors.addLine "cannot interpret as action: ", e.value
  246. of "file":
  247. if result.msg.len == 0 and result.nimout.len == 0:
  248. result.parseErrors.addLine "errormsg or msg needs to be specified before file"
  249. result.file = e.value
  250. of "line":
  251. if result.msg.len == 0 and result.nimout.len == 0:
  252. result.parseErrors.addLine "errormsg, msg or nimout needs to be specified before line"
  253. discard parseInt(e.value, result.line)
  254. of "column":
  255. if result.msg.len == 0 and result.nimout.len == 0:
  256. result.parseErrors.addLine "errormsg or msg needs to be specified before column"
  257. discard parseInt(e.value, result.column)
  258. of "output":
  259. if result.outputCheck != ocSubstr:
  260. result.outputCheck = ocEqual
  261. result.output = e.value
  262. of "input":
  263. result.input = e.value
  264. of "outputsub":
  265. result.outputCheck = ocSubstr
  266. result.output = strip(e.value)
  267. of "sortoutput":
  268. try:
  269. result.sortoutput = parseCfgBool(e.value)
  270. except:
  271. result.parseErrors.addLine getCurrentExceptionMsg()
  272. of "exitcode":
  273. discard parseInt(e.value, result.exitCode)
  274. result.action = actionRun
  275. of "errormsg":
  276. result.msg = e.value
  277. result.action = actionReject
  278. of "nimout":
  279. result.nimout = e.value
  280. nimoutFound = true
  281. of "nimoutfull":
  282. result.nimoutFull = parseCfgBool(e.value)
  283. of "batchable":
  284. result.unbatchable = not parseCfgBool(e.value)
  285. of "joinable":
  286. result.unjoinable = not parseCfgBool(e.value)
  287. of "valgrind":
  288. when defined(linux) and sizeof(int) == 8:
  289. result.useValgrind = if e.value.normalize == "leaks": leaking
  290. else: ValgrindSpec(parseCfgBool(e.value))
  291. result.unjoinable = true
  292. if result.useValgrind != disabled:
  293. result.outputCheck = ocSubstr
  294. else:
  295. # Windows lacks valgrind. Silly OS.
  296. # Valgrind only supports OSX <= 17.x
  297. result.useValgrind = disabled
  298. of "disabled":
  299. case e.value.normalize
  300. of "y", "yes", "true", "1", "on": result.err = reDisabled
  301. of "n", "no", "false", "0", "off": discard
  302. of "win", "windows":
  303. when defined(windows): result.err = reDisabled
  304. of "linux":
  305. when defined(linux): result.err = reDisabled
  306. of "bsd":
  307. when defined(bsd): result.err = reDisabled
  308. of "osx", "macosx": # xxx remove `macosx` alias?
  309. when defined(osx): result.err = reDisabled
  310. of "unix":
  311. when defined(unix): result.err = reDisabled
  312. of "posix":
  313. when defined(posix): result.err = reDisabled
  314. of "travis": # deprecated
  315. if isTravis: result.err = reDisabled
  316. of "appveyor": # deprecated
  317. if isAppVeyor: result.err = reDisabled
  318. of "azure":
  319. if isAzure: result.err = reDisabled
  320. of "32bit":
  321. if sizeof(int) == 4:
  322. result.err = reDisabled
  323. of "freebsd":
  324. when defined(freebsd): result.err = reDisabled
  325. of "arm64":
  326. when defined(arm64): result.err = reDisabled
  327. of "i386":
  328. when defined(i386): result.err = reDisabled
  329. of "openbsd":
  330. when defined(openbsd): result.err = reDisabled
  331. of "netbsd":
  332. when defined(netbsd): result.err = reDisabled
  333. else:
  334. result.parseErrors.addLine "cannot interpret as a bool: ", e.value
  335. of "cmd":
  336. if e.value.startsWith("nim "):
  337. result.cmd = compilerPrefix & e.value[3..^1]
  338. else:
  339. result.cmd = e.value
  340. of "ccodecheck":
  341. result.ccodeCheck.add e.value
  342. of "maxcodesize":
  343. discard parseInt(e.value, result.maxCodeSize)
  344. of "timeout":
  345. try:
  346. result.timeout = parseFloat(e.value)
  347. except ValueError:
  348. result.parseErrors.addLine "cannot interpret as a float: ", e.value
  349. of "targets", "target":
  350. try:
  351. result.targets.incl parseTargets(e.value)
  352. except ValueError as e:
  353. result.parseErrors.addLine e.msg
  354. of "matrix":
  355. for v in e.value.split(';'):
  356. result.matrix.add(v.strip)
  357. else:
  358. result.parseErrors.addLine "invalid key for test spec: ", e.key
  359. of cfgSectionStart:
  360. result.parseErrors.addLine "section ignored: ", e.section
  361. of cfgOption:
  362. result.parseErrors.addLine "command ignored: ", e.key & ": " & e.value
  363. of cfgError:
  364. result.parseErrors.addLine e.msg
  365. of cfgEof:
  366. break
  367. close(p)
  368. if skips.anyIt(it in result.file):
  369. result.err = reDisabled
  370. if nimoutFound and result.nimout.len == 0 and not result.nimoutFull:
  371. result.parseErrors.addLine "empty `nimout` is vacuously true, use `nimoutFull:true` if intentional"
  372. result.inCurrentBatch = isCurrentBatch(testamentData0, filename) or result.unbatchable
  373. if not result.inCurrentBatch:
  374. result.err = reDisabled