specs.nim 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  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. import compiler/platform
  12. type TestamentData* = ref object
  13. # better to group globals under 1 object; could group the other ones here too
  14. batchArg*: string
  15. testamentNumBatch*: int
  16. testamentBatch*: int
  17. let testamentData0* = TestamentData()
  18. var compilerPrefix* = findExe("nim")
  19. let isTravis* = existsEnv("TRAVIS")
  20. let isAppVeyor* = existsEnv("APPVEYOR")
  21. let isAzure* = existsEnv("TF_BUILD")
  22. var skips*: seq[string]
  23. type
  24. TTestAction* = enum
  25. actionRun = "run"
  26. actionCompile = "compile"
  27. actionReject = "reject"
  28. TOutputCheck* = enum
  29. ocIgnore = "ignore"
  30. ocEqual = "equal"
  31. ocSubstr = "substr"
  32. TResultEnum* = enum
  33. reNimcCrash, # nim compiler seems to have crashed
  34. reMsgsDiffer, # error messages differ
  35. reFilesDiffer, # expected and given filenames differ
  36. reLinesDiffer, # expected and given line numbers differ
  37. reOutputsDiffer,
  38. reExitcodesDiffer, # exit codes of program or of valgrind differ
  39. reTimeout,
  40. reInvalidPeg,
  41. reCodegenFailure,
  42. reCodeNotFound,
  43. reExeNotFound,
  44. reInstallFailed # package installation failed
  45. reBuildFailed # package building failed
  46. reDisabled, # test is disabled
  47. reJoined, # test is disabled because it was joined into the megatest
  48. reSuccess # test was successful
  49. reInvalidSpec # test had problems to parse the spec
  50. reRetry # test is being retried
  51. TTarget* = enum
  52. targetC = "c"
  53. targetCpp = "cpp"
  54. targetObjC = "objc"
  55. targetJS = "js"
  56. InlineError* = object
  57. kind*: string
  58. msg*: string
  59. line*, col*: int
  60. ValgrindSpec* = enum
  61. disabled, enabled, leaking
  62. TSpec* = object
  63. # xxx make sure `isJoinableSpec` takes into account each field here.
  64. action*: TTestAction
  65. file*, cmd*: string
  66. filename*: string ## Test filename (without path).
  67. input*: string
  68. outputCheck*: TOutputCheck
  69. sortoutput*: bool
  70. output*: string
  71. line*, column*: int
  72. exitCode*: int
  73. msg*: string
  74. ccodeCheck*: seq[string]
  75. maxCodeSize*: int
  76. err*: TResultEnum
  77. inCurrentBatch*: bool
  78. targets*: set[TTarget]
  79. matrix*: seq[string]
  80. nimout*: string
  81. nimoutFull*: bool # whether nimout is all compiler output or a subset
  82. parseErrors*: string # when the spec definition is invalid, this is not empty.
  83. unjoinable*: bool
  84. unbatchable*: bool
  85. # whether this test can be batchable via `NIM_TESTAMENT_BATCH`; only very
  86. # few tests are not batchable; the ones that are not could be turned batchable
  87. # by making the dependencies explicit
  88. useValgrind*: ValgrindSpec
  89. timeout*: float # in seconds, fractions possible,
  90. # but don't rely on much precision
  91. inlineErrors*: seq[InlineError] # line information to error message
  92. debugInfo*: string # debug info to give more context
  93. retries*: int # number of retry attempts after the test fails
  94. proc getCmd*(s: TSpec): string =
  95. if s.cmd.len == 0:
  96. result = compilerPrefix & " $target --hints:on -d:testing --nimblePath:build/deps/pkgs2 $options $file"
  97. else:
  98. result = s.cmd
  99. const
  100. targetToExt*: array[TTarget, string] = ["nim.c", "nim.cpp", "nim.m", "js"]
  101. targetToCmd*: array[TTarget, string] = ["c", "cpp", "objc", "js"]
  102. proc defaultOptions*(a: TTarget): string =
  103. case a
  104. of targetJS: "-d:nodejs"
  105. # once we start testing for `nim js -d:nimbrowser` (eg selenium or similar),
  106. # we can adapt this logic; or a given js test can override with `-u:nodejs`.
  107. else: ""
  108. when not declared(parseCfgBool):
  109. # candidate for the stdlib:
  110. proc parseCfgBool(s: string): bool =
  111. case normalize(s)
  112. of "y", "yes", "true", "1", "on": result = true
  113. of "n", "no", "false", "0", "off": result = false
  114. else: raise newException(ValueError, "cannot interpret as a bool: " & s)
  115. proc addLine*(self: var string; pieces: varargs[string]) =
  116. for piece in pieces:
  117. self.add piece
  118. self.add "\n"
  119. const
  120. inlineErrorKindMarker = "tt."
  121. inlineErrorMarker = "#[" & inlineErrorKindMarker
  122. proc extractErrorMsg(s: string; i: int; line: var int; col: var int; spec: var TSpec): int =
  123. ## Extract inline error messages.
  124. ##
  125. ## Can parse a single message for a line:
  126. ##
  127. ## ```nim
  128. ## proc generic_proc*[T] {.no_destroy, userPragma.} = #[tt.Error
  129. ## ^ 'generic_proc' should be: 'genericProc' [Name] ]#
  130. ## ```
  131. ##
  132. ## Can parse multiple messages for a line when they are separated by ';':
  133. ##
  134. ## ```nim
  135. ## proc generic_proc*[T] {.no_destroy, userPragma.} = #[tt.Error
  136. ## ^ 'generic_proc' should be: 'genericProc' [Name]; tt.Error
  137. ## ^ 'no_destroy' should be: 'nodestroy' [Name]; tt.Error
  138. ## ^ 'userPragma' should be: 'user_pragma' [template declared in mstyleCheck.nim(10, 9)] [Name] ]#
  139. ## ```
  140. ##
  141. ## ```nim
  142. ## proc generic_proc*[T] {.no_destroy, userPragma.} = #[tt.Error
  143. ## ^ 'generic_proc' should be: 'genericProc' [Name];
  144. ## tt.Error ^ 'no_destroy' should be: 'nodestroy' [Name];
  145. ## tt.Error ^ 'userPragma' should be: 'user_pragma' [template declared in mstyleCheck.nim(10, 9)] [Name] ]#
  146. ## ```
  147. result = i + len(inlineErrorMarker)
  148. inc col, len(inlineErrorMarker)
  149. let msgLine = line
  150. var msgCol = -1
  151. var msg = ""
  152. var kind = ""
  153. template parseKind =
  154. while result < s.len and s[result] in IdentChars:
  155. kind.add s[result]
  156. inc result
  157. inc col
  158. if kind notin ["Hint", "Warning", "Error"]:
  159. spec.parseErrors.addLine "expected inline message kind: Hint, Warning, Error"
  160. template skipWhitespace =
  161. while result < s.len and s[result] in Whitespace:
  162. if s[result] == '\n':
  163. col = 1
  164. inc line
  165. else:
  166. inc col
  167. inc result
  168. template parseCaret =
  169. if result < s.len and s[result] == '^':
  170. msgCol = col
  171. inc result
  172. inc col
  173. skipWhitespace()
  174. else:
  175. spec.parseErrors.addLine "expected column marker ('^') for inline message"
  176. template isMsgDelimiter: bool =
  177. s[result] == ';' and
  178. (block:
  179. let nextTokenIdx = result + 1 + parseutils.skipWhitespace(s, result + 1)
  180. if s.len > nextTokenIdx + len(inlineErrorKindMarker) and
  181. s[nextTokenIdx..(nextTokenIdx + len(inlineErrorKindMarker) - 1)] == inlineErrorKindMarker:
  182. true
  183. else:
  184. false)
  185. template trimTrailingMsgWhitespace =
  186. while msg.len > 0 and msg[^1] in Whitespace:
  187. setLen msg, msg.len - 1
  188. template addInlineError =
  189. doAssert msg[^1] notin Whitespace
  190. if kind == "Error": spec.action = actionReject
  191. spec.inlineErrors.add InlineError(kind: kind, msg: msg, line: msgLine, col: msgCol)
  192. parseKind()
  193. skipWhitespace()
  194. parseCaret()
  195. while result < s.len-1:
  196. if s[result] == '\n':
  197. if result > 0 and s[result - 1] == '\r':
  198. msg[^1] = '\n'
  199. else:
  200. msg.add '\n'
  201. inc result
  202. inc line
  203. col = 1
  204. elif isMsgDelimiter():
  205. trimTrailingMsgWhitespace()
  206. inc result
  207. skipWhitespace()
  208. addInlineError()
  209. inc result, len(inlineErrorKindMarker)
  210. inc col, 1 + len(inlineErrorKindMarker)
  211. kind.setLen 0
  212. msg.setLen 0
  213. parseKind()
  214. skipWhitespace()
  215. parseCaret()
  216. elif s[result] == ']' and s[result+1] == '#':
  217. trimTrailingMsgWhitespace()
  218. inc result, 2
  219. inc col, 2
  220. addInlineError()
  221. break
  222. else:
  223. msg.add s[result]
  224. inc result
  225. inc col
  226. if spec.inlineErrors.len > 0:
  227. spec.unjoinable = true
  228. proc extractSpec(filename: string; spec: var TSpec): string =
  229. const
  230. tripleQuote = "\"\"\""
  231. specStart = "discard " & tripleQuote
  232. var s = readFile(filename)
  233. var i = 0
  234. var a = -1
  235. var b = -1
  236. var line = 1
  237. var col = 1
  238. while i < s.len:
  239. if (i == 0 or s[i-1] != ' ') and s.continuesWith(specStart, i):
  240. # `s[i-1] == '\n'` would not work because of `tests/stdlib/tbase64.nim` which contains BOM (https://en.wikipedia.org/wiki/Byte_order_mark)
  241. const lineMax = 10
  242. if a != -1:
  243. raise newException(ValueError, "testament spec violation: duplicate `specStart` found: " & $(filename, a, b, line))
  244. elif line > lineMax:
  245. # not overly restrictive, but prevents mistaking some `specStart` as spec if deeep inside a test file
  246. raise newException(ValueError, "testament spec violation: `specStart` should be before line $1, or be indented; info: $2" % [$lineMax, $(filename, a, b, line)])
  247. i += specStart.len
  248. a = i
  249. elif a > -1 and b == -1 and s.continuesWith(tripleQuote, i):
  250. b = i
  251. i += tripleQuote.len
  252. elif s[i] == '\n':
  253. inc line
  254. inc i
  255. col = 1
  256. elif s.continuesWith(inlineErrorMarker, i):
  257. i = extractErrorMsg(s, i, line, col, spec)
  258. else:
  259. inc col
  260. inc i
  261. if a >= 0 and b > a:
  262. result = s.substr(a, b-1).multiReplace({"'''": tripleQuote, "\\31": "\31"})
  263. elif a >= 0:
  264. raise newException(ValueError, "testament spec violation: `specStart` found but not trailing `tripleQuote`: $1" % $(filename, a, b, line))
  265. else:
  266. result = ""
  267. proc parseTargets*(value: string): set[TTarget] =
  268. for v in value.normalize.splitWhitespace:
  269. case v
  270. of "c": result.incl(targetC)
  271. of "cpp", "c++": result.incl(targetCpp)
  272. of "objc": result.incl(targetObjC)
  273. of "js": result.incl(targetJS)
  274. else: raise newException(ValueError, "invalid target: '$#'" % v)
  275. proc initSpec*(filename: string): TSpec =
  276. result.file = filename
  277. proc isCurrentBatch*(testamentData: TestamentData; filename: string): bool =
  278. if testamentData.testamentNumBatch != 0:
  279. hash(filename) mod testamentData.testamentNumBatch == testamentData.testamentBatch
  280. else:
  281. true
  282. proc parseSpec*(filename: string): TSpec =
  283. result.file = filename
  284. result.filename = extractFilename(filename)
  285. let specStr = extractSpec(filename, result)
  286. var ss = newStringStream(specStr)
  287. var p: CfgParser
  288. open(p, ss, filename, 1)
  289. var flags: HashSet[string]
  290. var nimoutFound = false
  291. while true:
  292. var e = next(p)
  293. case e.kind
  294. of cfgKeyValuePair:
  295. let key = e.key.normalize
  296. const whiteListMulti = ["disabled", "ccodecheck"]
  297. ## list of flags that are correctly handled when passed multiple times
  298. ## (instead of being overwritten)
  299. if key notin whiteListMulti:
  300. doAssert key notin flags, $(key, filename)
  301. flags.incl key
  302. case key
  303. of "action":
  304. case e.value.normalize
  305. of "compile":
  306. result.action = actionCompile
  307. of "run":
  308. result.action = actionRun
  309. of "reject":
  310. result.action = actionReject
  311. else:
  312. result.parseErrors.addLine "cannot interpret as action: ", e.value
  313. of "file":
  314. if result.msg.len == 0 and result.nimout.len == 0:
  315. result.parseErrors.addLine "errormsg or msg needs to be specified before file"
  316. result.file = e.value
  317. of "line":
  318. if result.msg.len == 0 and result.nimout.len == 0:
  319. result.parseErrors.addLine "errormsg, msg or nimout needs to be specified before line"
  320. discard parseInt(e.value, result.line)
  321. of "column":
  322. if result.msg.len == 0 and result.nimout.len == 0:
  323. result.parseErrors.addLine "errormsg or msg needs to be specified before column"
  324. discard parseInt(e.value, result.column)
  325. of "output":
  326. if result.outputCheck != ocSubstr:
  327. result.outputCheck = ocEqual
  328. result.output = e.value
  329. of "input":
  330. result.input = e.value
  331. of "outputsub":
  332. result.outputCheck = ocSubstr
  333. result.output = strip(e.value)
  334. of "sortoutput":
  335. try:
  336. result.sortoutput = parseCfgBool(e.value)
  337. except:
  338. result.parseErrors.addLine getCurrentExceptionMsg()
  339. of "exitcode":
  340. discard parseInt(e.value, result.exitCode)
  341. result.action = actionRun
  342. of "errormsg":
  343. result.msg = e.value
  344. result.action = actionReject
  345. of "nimout":
  346. result.nimout = e.value
  347. nimoutFound = true
  348. of "nimoutfull":
  349. result.nimoutFull = parseCfgBool(e.value)
  350. of "batchable":
  351. result.unbatchable = not parseCfgBool(e.value)
  352. of "joinable":
  353. result.unjoinable = not parseCfgBool(e.value)
  354. of "valgrind":
  355. when defined(linux) and sizeof(int) == 8:
  356. result.useValgrind = if e.value.normalize == "leaks": leaking
  357. else: ValgrindSpec(parseCfgBool(e.value))
  358. result.unjoinable = true
  359. if result.useValgrind != disabled:
  360. result.outputCheck = ocSubstr
  361. else:
  362. # Windows lacks valgrind. Silly OS.
  363. # Valgrind only supports OSX <= 17.x
  364. result.useValgrind = disabled
  365. of "disabled":
  366. let value = e.value.normalize
  367. case value
  368. of "y", "yes", "true", "1", "on": result.err = reDisabled
  369. of "n", "no", "false", "0", "off": discard
  370. # These values are defined in `compiler/options.isDefined`
  371. of "win":
  372. when defined(windows): result.err = reDisabled
  373. of "linux":
  374. when defined(linux): result.err = reDisabled
  375. of "bsd":
  376. when defined(bsd): result.err = reDisabled
  377. of "osx":
  378. when defined(osx): result.err = reDisabled
  379. of "unix", "posix":
  380. when defined(posix): result.err = reDisabled
  381. of "freebsd":
  382. when defined(freebsd): result.err = reDisabled
  383. of "littleendian":
  384. when defined(littleendian): result.err = reDisabled
  385. of "bigendian":
  386. when defined(bigendian): result.err = reDisabled
  387. of "cpu8", "8bit":
  388. when defined(cpu8): result.err = reDisabled
  389. of "cpu16", "16bit":
  390. when defined(cpu16): result.err = reDisabled
  391. of "cpu32", "32bit":
  392. when defined(cpu32): result.err = reDisabled
  393. of "cpu64", "64bit":
  394. when defined(cpu64): result.err = reDisabled
  395. # These values are for CI environments
  396. of "travis": # deprecated
  397. if isTravis: result.err = reDisabled
  398. of "appveyor": # deprecated
  399. if isAppVeyor: result.err = reDisabled
  400. of "azure":
  401. if isAzure: result.err = reDisabled
  402. else:
  403. # Check whether the value exists as an OS or CPU that is
  404. # defined in `compiler/platform`.
  405. block checkHost:
  406. for os in platform.OS:
  407. # Check if the value exists as OS.
  408. if value == os.name.normalize:
  409. # The value exists; is it the same as the current host?
  410. if value == hostOS.normalize:
  411. # The value exists and is the same as the current host,
  412. # so disable the test.
  413. result.err = reDisabled
  414. # The value was defined, so there is no need to check further
  415. # values or raise an error.
  416. break checkHost
  417. for cpu in platform.CPU:
  418. # Check if the value exists as CPU.
  419. if value == cpu.name.normalize:
  420. # The value exists; is it the same as the current host?
  421. if value == hostCPU.normalize:
  422. # The value exists and is the same as the current host,
  423. # so disable the test.
  424. result.err = reDisabled
  425. # The value was defined, so there is no need to check further
  426. # values or raise an error.
  427. break checkHost
  428. # The value doesn't exist as an OS, CPU, or any previous value
  429. # defined in this case statement, so raise an error.
  430. result.parseErrors.addLine "cannot interpret as a bool: ", e.value
  431. of "cmd":
  432. if e.value.startsWith("nim "):
  433. result.cmd = compilerPrefix & e.value[3..^1]
  434. else:
  435. result.cmd = e.value
  436. of "ccodecheck":
  437. result.ccodeCheck.add e.value
  438. of "maxcodesize":
  439. discard parseInt(e.value, result.maxCodeSize)
  440. of "timeout":
  441. try:
  442. result.timeout = parseFloat(e.value)
  443. except ValueError:
  444. result.parseErrors.addLine "cannot interpret as a float: ", e.value
  445. of "retries":
  446. discard parseInt(e.value, result.retries)
  447. of "targets", "target":
  448. try:
  449. result.targets.incl parseTargets(e.value)
  450. except ValueError as e:
  451. result.parseErrors.addLine e.msg
  452. of "matrix":
  453. for v in e.value.split(';'):
  454. result.matrix.add(v.strip)
  455. else:
  456. result.parseErrors.addLine "invalid key for test spec: ", e.key
  457. of cfgSectionStart:
  458. result.parseErrors.addLine "section ignored: ", e.section
  459. of cfgOption:
  460. result.parseErrors.addLine "command ignored: ", e.key & ": " & e.value
  461. of cfgError:
  462. result.parseErrors.addLine e.msg
  463. of cfgEof:
  464. break
  465. close(p)
  466. if skips.anyIt(it in result.file):
  467. result.err = reDisabled
  468. if nimoutFound and result.nimout.len == 0 and not result.nimoutFull:
  469. result.parseErrors.addLine "empty `nimout` is vacuously true, use `nimoutFull:true` if intentional"
  470. result.inCurrentBatch = isCurrentBatch(testamentData0, filename) or result.unbatchable
  471. if not result.inCurrentBatch:
  472. result.err = reDisabled
  473. # Interpolate variables in msgs:
  474. template varSub(msg: string): string =
  475. try:
  476. msg % ["/", $DirSep, "file", result.filename]
  477. except ValueError:
  478. result.parseErrors.addLine "invalid variable interpolation (see 'https://nim-lang.github.io/Nim/testament.html#writing-unit-tests-output-message-variable-interpolation')"
  479. msg
  480. result.nimout = result.nimout.varSub
  481. result.msg = result.msg.varSub
  482. for inlineError in result.inlineErrors.mitems:
  483. inlineError.msg = inlineError.msg.varSub