specs.nim 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  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
  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,
  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 = "C++"
  52. targetObjC = "ObjC"
  53. targetJS = "JS"
  54. InlineError* = object
  55. kind*: string
  56. msg*: string
  57. line*, col*: int
  58. TSpec* = object
  59. action*: TTestAction
  60. file*, cmd*: string
  61. input*: string
  62. outputCheck*: TOutputCheck
  63. sortoutput*: bool
  64. output*: string
  65. line*, column*: int
  66. tfile*: string
  67. tline*, tcolumn*: int
  68. exitCode*: int
  69. msg*: string
  70. ccodeCheck*: string
  71. maxCodeSize*: int
  72. err*: TResultEnum
  73. inCurrentBatch*: bool
  74. targets*: set[TTarget]
  75. matrix*: seq[string]
  76. nimout*: string
  77. parseErrors*: string # when the spec definition is invalid, this is not empty.
  78. unjoinable*: bool
  79. unbatchable*: bool
  80. # whether this test can be batchable via `NIM_TESTAMENT_BATCH`; only very
  81. # few tests are not batchable; the ones that are not could be turned batchable
  82. # by making the dependencies explicit
  83. useValgrind*: bool
  84. timeout*: float # in seconds, fractions possible,
  85. # but don't rely on much precision
  86. inlineErrors*: seq[InlineError] # line information to error message
  87. proc getCmd*(s: TSpec): string =
  88. if s.cmd.len == 0:
  89. result = compilerPrefix & " $target --hints:on -d:testing --nimblePath:tests/deps $options $file"
  90. else:
  91. result = s.cmd
  92. const
  93. targetToExt*: array[TTarget, string] = ["nim.c", "nim.cpp", "nim.m", "js"]
  94. targetToCmd*: array[TTarget, string] = ["c", "cpp", "objc", "js"]
  95. proc defaultOptions*(a: TTarget): string =
  96. case a
  97. of targetJS: "-d:nodejs"
  98. # once we start testing for `nim js -d:nimbrowser` (eg selenium or similar),
  99. # we can adapt this logic; or a given js test can override with `-u:nodejs`.
  100. else: ""
  101. when not declared(parseCfgBool):
  102. # candidate for the stdlib:
  103. proc parseCfgBool(s: string): bool =
  104. case normalize(s)
  105. of "y", "yes", "true", "1", "on": result = true
  106. of "n", "no", "false", "0", "off": result = false
  107. else: raise newException(ValueError, "cannot interpret as a bool: " & s)
  108. const
  109. inlineErrorMarker = "#[tt."
  110. proc extractErrorMsg(s: string; i: int; line: var int; col: var int; spec: var TSpec): int =
  111. result = i + len(inlineErrorMarker)
  112. inc col, len(inlineErrorMarker)
  113. var kind = ""
  114. while result < s.len and s[result] in IdentChars:
  115. kind.add s[result]
  116. inc result
  117. inc col
  118. var caret = (line, -1)
  119. template skipWhitespace =
  120. while result < s.len and s[result] in Whitespace:
  121. if s[result] == '\n':
  122. col = 1
  123. inc line
  124. else:
  125. inc col
  126. inc result
  127. skipWhitespace()
  128. if result < s.len and s[result] == '^':
  129. caret = (line-1, col)
  130. inc result
  131. inc col
  132. skipWhitespace()
  133. var msg = ""
  134. while result < s.len-1:
  135. if s[result] == '\n':
  136. inc result
  137. inc line
  138. col = 1
  139. elif s[result] == ']' and s[result+1] == '#':
  140. while msg.len > 0 and msg[^1] in Whitespace:
  141. setLen msg, msg.len - 1
  142. inc result
  143. inc col, 2
  144. if kind == "Error": spec.action = actionReject
  145. spec.unjoinable = true
  146. spec.inlineErrors.add InlineError(kind: kind, msg: msg, line: caret[0], col: caret[1])
  147. break
  148. else:
  149. msg.add s[result]
  150. inc result
  151. inc col
  152. proc extractSpec(filename: string; spec: var TSpec): string =
  153. const
  154. tripleQuote = "\"\"\""
  155. var s = readFile(filename).string
  156. var i = 0
  157. var a = -1
  158. var b = -1
  159. var line = 1
  160. var col = 1
  161. while i < s.len:
  162. if s.continuesWith(tripleQuote, i):
  163. if a < 0: a = i
  164. elif b < 0: b = i
  165. inc i, 2
  166. inc col
  167. elif s[i] == '\n':
  168. inc line
  169. col = 1
  170. elif s.continuesWith(inlineErrorMarker, i):
  171. i = extractErrorMsg(s, i, line, col, spec)
  172. else:
  173. inc col
  174. inc i
  175. # look for """ only in the first section
  176. if a >= 0 and b > a and a < 40:
  177. result = s.substr(a+3, b-1).replace("'''", tripleQuote)
  178. else:
  179. #echo "warning: file does not contain spec: " & filename
  180. result = ""
  181. when not defined(nimhygiene):
  182. {.pragma: inject.}
  183. proc parseTargets*(value: string): set[TTarget] =
  184. for v in value.normalize.splitWhitespace:
  185. case v
  186. of "c": result.incl(targetC)
  187. of "cpp", "c++": result.incl(targetCpp)
  188. of "objc": result.incl(targetObjC)
  189. of "js": result.incl(targetJS)
  190. else: echo "target ignored: " & v
  191. proc addLine*(self: var string; a: string) =
  192. self.add a
  193. self.add "\n"
  194. proc addLine*(self: var string; a,b: string) =
  195. self.add a
  196. self.add b
  197. self.add "\n"
  198. proc initSpec*(filename: string): TSpec =
  199. result.file = filename
  200. proc isCurrentBatch(testamentData: TestamentData, filename: string): bool =
  201. if testamentData.testamentNumBatch != 0:
  202. hash(filename) mod testamentData.testamentNumBatch == testamentData.testamentBatch
  203. else:
  204. true
  205. proc parseSpec*(filename: string): TSpec =
  206. result.file = filename
  207. let specStr = extractSpec(filename, result)
  208. var ss = newStringStream(specStr)
  209. var p: CfgParser
  210. open(p, ss, filename, 1)
  211. while true:
  212. var e = next(p)
  213. case e.kind
  214. of cfgKeyValuePair:
  215. case normalize(e.key)
  216. of "action":
  217. case e.value.normalize
  218. of "compile":
  219. result.action = actionCompile
  220. of "run":
  221. result.action = actionRun
  222. of "reject":
  223. result.action = actionReject
  224. else:
  225. result.parseErrors.addLine "cannot interpret as action: ", e.value
  226. of "file":
  227. if result.msg.len == 0 and result.nimout.len == 0:
  228. result.parseErrors.addLine "errormsg or msg needs to be specified before file"
  229. result.file = e.value
  230. of "line":
  231. if result.msg.len == 0 and result.nimout.len == 0:
  232. result.parseErrors.addLine "errormsg, msg or nimout needs to be specified before line"
  233. discard parseInt(e.value, result.line)
  234. of "column":
  235. if result.msg.len == 0 and result.nimout.len == 0:
  236. result.parseErrors.addLine "errormsg or msg needs to be specified before column"
  237. discard parseInt(e.value, result.column)
  238. of "tfile":
  239. result.tfile = e.value
  240. of "tline":
  241. discard parseInt(e.value, result.tline)
  242. of "tcolumn":
  243. discard parseInt(e.value, result.tcolumn)
  244. of "output":
  245. if result.outputCheck != ocSubstr:
  246. result.outputCheck = ocEqual
  247. result.output = strip(e.value)
  248. of "input":
  249. result.input = e.value
  250. of "outputsub":
  251. result.outputCheck = ocSubstr
  252. result.output = strip(e.value)
  253. of "sortoutput":
  254. try:
  255. result.sortoutput = parseCfgBool(e.value)
  256. except:
  257. result.parseErrors.addLine getCurrentExceptionMsg()
  258. of "exitcode":
  259. discard parseInt(e.value, result.exitCode)
  260. result.action = actionRun
  261. of "msg":
  262. result.msg = e.value
  263. if result.action != actionRun:
  264. result.action = actionCompile
  265. of "errormsg", "errmsg":
  266. result.msg = e.value
  267. result.action = actionReject
  268. of "nimout":
  269. result.nimout = e.value
  270. of "batchable":
  271. result.unbatchable = not parseCfgBool(e.value)
  272. of "joinable":
  273. result.unjoinable = not parseCfgBool(e.value)
  274. of "valgrind":
  275. when defined(linux) and sizeof(int) == 8:
  276. result.useValgrind = parseCfgBool(e.value)
  277. result.unjoinable = true
  278. if result.useValgrind:
  279. result.outputCheck = ocSubstr
  280. else:
  281. # Windows lacks valgrind. Silly OS.
  282. # Valgrind only supports OSX <= 17.x
  283. result.useValgrind = false
  284. of "disabled":
  285. case e.value.normalize
  286. of "y", "yes", "true", "1", "on": result.err = reDisabled
  287. of "n", "no", "false", "0", "off": discard
  288. of "win", "windows":
  289. when defined(windows): result.err = reDisabled
  290. of "linux":
  291. when defined(linux): result.err = reDisabled
  292. of "bsd":
  293. when defined(bsd): result.err = reDisabled
  294. of "macosx":
  295. when defined(macosx): result.err = reDisabled
  296. of "unix":
  297. when defined(unix): result.err = reDisabled
  298. of "posix":
  299. when defined(posix): result.err = reDisabled
  300. of "travis":
  301. if isTravis: result.err = reDisabled
  302. of "appveyor":
  303. if isAppVeyor: result.err = reDisabled
  304. of "azure":
  305. if isAzure: result.err = reDisabled
  306. of "32bit":
  307. if sizeof(int) == 4:
  308. result.err = reDisabled
  309. of "freebsd":
  310. when defined(freebsd): result.err = reDisabled
  311. of "arm64":
  312. when defined(arm64): result.err = reDisabled
  313. of "i386":
  314. when defined(i386): result.err = reDisabled
  315. of "openbsd":
  316. when defined(openbsd): result.err = reDisabled
  317. of "netbsd":
  318. when defined(netbsd): result.err = reDisabled
  319. else:
  320. result.parseErrors.addLine "cannot interpret as a bool: ", e.value
  321. of "cmd":
  322. if e.value.startsWith("nim "):
  323. result.cmd = compilerPrefix & e.value[3..^1]
  324. else:
  325. result.cmd = e.value
  326. of "ccodecheck":
  327. result.ccodeCheck = e.value
  328. of "maxcodesize":
  329. discard parseInt(e.value, result.maxCodeSize)
  330. of "timeout":
  331. try:
  332. result.timeout = parseFloat(e.value)
  333. except ValueError:
  334. result.parseErrors.addLine "cannot interpret as a float: ", e.value
  335. of "target", "targets":
  336. for v in e.value.normalize.splitWhitespace:
  337. case v
  338. of "c":
  339. result.targets.incl(targetC)
  340. of "cpp", "c++":
  341. result.targets.incl(targetCpp)
  342. of "objc":
  343. result.targets.incl(targetObjC)
  344. of "js":
  345. result.targets.incl(targetJS)
  346. else:
  347. result.parseErrors.addLine "cannot interpret as a target: ", e.value
  348. of "matrix":
  349. for v in e.value.split(';'):
  350. result.matrix.add(v.strip)
  351. else:
  352. result.parseErrors.addLine "invalid key for test spec: ", e.key
  353. of cfgSectionStart:
  354. result.parseErrors.addLine "section ignored: ", e.section
  355. of cfgOption:
  356. result.parseErrors.addLine "command ignored: ", e.key & ": " & e.value
  357. of cfgError:
  358. result.parseErrors.addLine e.msg
  359. of cfgEof:
  360. break
  361. close(p)
  362. if skips.anyIt(it in result.file):
  363. result.err = reDisabled
  364. result.inCurrentBatch = isCurrentBatch(testamentData0, filename) or result.unbatchable
  365. if not result.inCurrentBatch:
  366. result.err = reDisabled