mirror of
https://github.com/nim-lang/Nim.git
synced 2025-12-28 17:04:41 +00:00
522 lines
18 KiB
Nim
522 lines
18 KiB
Nim
#
|
|
#
|
|
# Nim Tester
|
|
# (c) Copyright 2015 Andreas Rumpf
|
|
#
|
|
# See the file "copying.txt", included in this
|
|
# distribution, for details about the copyright.
|
|
#
|
|
|
|
import sequtils, parseutils, strutils, os, streams, parsecfg,
|
|
tables, hashes, sets
|
|
import compiler/platform
|
|
|
|
type TestamentData* = ref object
|
|
# better to group globals under 1 object; could group the other ones here too
|
|
batchArg*: string
|
|
testamentNumBatch*: int
|
|
testamentBatch*: int
|
|
|
|
let testamentData0* = TestamentData()
|
|
|
|
var compilerPrefix* = findExe("nim")
|
|
|
|
let isTravis* = existsEnv("TRAVIS")
|
|
let isAppVeyor* = existsEnv("APPVEYOR")
|
|
let isAzure* = existsEnv("TF_BUILD")
|
|
|
|
var skips*: seq[string]
|
|
|
|
type
|
|
TTestAction* = enum
|
|
actionRun = "run"
|
|
actionCompile = "compile"
|
|
actionReject = "reject"
|
|
|
|
TOutputCheck* = enum
|
|
ocIgnore = "ignore"
|
|
ocEqual = "equal"
|
|
ocSubstr = "substr"
|
|
|
|
TResultEnum* = enum
|
|
reNimcCrash, # nim compiler seems to have crashed
|
|
reMsgsDiffer, # error messages differ
|
|
reFilesDiffer, # expected and given filenames differ
|
|
reLinesDiffer, # expected and given line numbers differ
|
|
reOutputsDiffer,
|
|
reExitcodesDiffer, # exit codes of program or of valgrind differ
|
|
reTimeout,
|
|
reInvalidPeg,
|
|
reCodegenFailure,
|
|
reCodeNotFound,
|
|
reExeNotFound,
|
|
reInstallFailed # package installation failed
|
|
reBuildFailed # package building failed
|
|
reDisabled, # test is disabled
|
|
reJoined, # test is disabled because it was joined into the megatest
|
|
reSuccess # test was successful
|
|
reInvalidSpec # test had problems to parse the spec
|
|
|
|
TTarget* = enum
|
|
targetC = "c"
|
|
targetCpp = "cpp"
|
|
targetObjC = "objc"
|
|
targetJS = "js"
|
|
|
|
InlineError* = object
|
|
kind*: string
|
|
msg*: string
|
|
line*, col*: int
|
|
|
|
ValgrindSpec* = enum
|
|
disabled, enabled, leaking
|
|
|
|
TSpec* = object
|
|
# xxx make sure `isJoinableSpec` takes into account each field here.
|
|
action*: TTestAction
|
|
file*, cmd*: string
|
|
filename*: string ## Test filename (without path).
|
|
input*: string
|
|
outputCheck*: TOutputCheck
|
|
sortoutput*: bool
|
|
output*: string
|
|
line*, column*: int
|
|
exitCode*: int
|
|
msg*: string
|
|
ccodeCheck*: seq[string]
|
|
maxCodeSize*: int
|
|
err*: TResultEnum
|
|
inCurrentBatch*: bool
|
|
targets*: set[TTarget]
|
|
matrix*: seq[string]
|
|
nimout*: string
|
|
nimoutFull*: bool # whether nimout is all compiler output or a subset
|
|
parseErrors*: string # when the spec definition is invalid, this is not empty.
|
|
unjoinable*: bool
|
|
unbatchable*: bool
|
|
# whether this test can be batchable via `NIM_TESTAMENT_BATCH`; only very
|
|
# few tests are not batchable; the ones that are not could be turned batchable
|
|
# by making the dependencies explicit
|
|
useValgrind*: ValgrindSpec
|
|
timeout*: float # in seconds, fractions possible,
|
|
# but don't rely on much precision
|
|
inlineErrors*: seq[InlineError] # line information to error message
|
|
debugInfo*: string # debug info to give more context
|
|
|
|
proc getCmd*(s: TSpec): string =
|
|
if s.cmd.len == 0:
|
|
result = compilerPrefix & " $target --hints:on -d:testing --nimblePath:build/deps/pkgs2 $options $file"
|
|
else:
|
|
result = s.cmd
|
|
|
|
const
|
|
targetToExt*: array[TTarget, string] = ["nim.c", "nim.cpp", "nim.m", "js"]
|
|
targetToCmd*: array[TTarget, string] = ["c", "cpp", "objc", "js"]
|
|
|
|
proc defaultOptions*(a: TTarget): string =
|
|
case a
|
|
of targetJS: "-d:nodejs"
|
|
# once we start testing for `nim js -d:nimbrowser` (eg selenium or similar),
|
|
# we can adapt this logic; or a given js test can override with `-u:nodejs`.
|
|
else: ""
|
|
|
|
when not declared(parseCfgBool):
|
|
# candidate for the stdlib:
|
|
proc parseCfgBool(s: string): bool =
|
|
case normalize(s)
|
|
of "y", "yes", "true", "1", "on": result = true
|
|
of "n", "no", "false", "0", "off": result = false
|
|
else: raise newException(ValueError, "cannot interpret as a bool: " & s)
|
|
|
|
proc addLine*(self: var string; pieces: varargs[string]) =
|
|
for piece in pieces:
|
|
self.add piece
|
|
self.add "\n"
|
|
|
|
|
|
const
|
|
inlineErrorKindMarker = "tt."
|
|
inlineErrorMarker = "#[" & inlineErrorKindMarker
|
|
|
|
proc extractErrorMsg(s: string; i: int; line: var int; col: var int; spec: var TSpec): int =
|
|
## Extract inline error messages.
|
|
##
|
|
## Can parse a single message for a line:
|
|
##
|
|
## ```nim
|
|
## proc generic_proc*[T] {.no_destroy, userPragma.} = #[tt.Error
|
|
## ^ 'generic_proc' should be: 'genericProc' [Name] ]#
|
|
## ```
|
|
##
|
|
## Can parse multiple messages for a line when they are separated by ';':
|
|
##
|
|
## ```nim
|
|
## proc generic_proc*[T] {.no_destroy, userPragma.} = #[tt.Error
|
|
## ^ 'generic_proc' should be: 'genericProc' [Name]; tt.Error
|
|
## ^ 'no_destroy' should be: 'nodestroy' [Name]; tt.Error
|
|
## ^ 'userPragma' should be: 'user_pragma' [template declared in mstyleCheck.nim(10, 9)] [Name] ]#
|
|
## ```
|
|
##
|
|
## ```nim
|
|
## proc generic_proc*[T] {.no_destroy, userPragma.} = #[tt.Error
|
|
## ^ 'generic_proc' should be: 'genericProc' [Name];
|
|
## tt.Error ^ 'no_destroy' should be: 'nodestroy' [Name];
|
|
## tt.Error ^ 'userPragma' should be: 'user_pragma' [template declared in mstyleCheck.nim(10, 9)] [Name] ]#
|
|
## ```
|
|
result = i + len(inlineErrorMarker)
|
|
inc col, len(inlineErrorMarker)
|
|
let msgLine = line
|
|
var msgCol = -1
|
|
var msg = ""
|
|
var kind = ""
|
|
|
|
template parseKind =
|
|
while result < s.len and s[result] in IdentChars:
|
|
kind.add s[result]
|
|
inc result
|
|
inc col
|
|
if kind notin ["Hint", "Warning", "Error"]:
|
|
spec.parseErrors.addLine "expected inline message kind: Hint, Warning, Error"
|
|
|
|
template skipWhitespace =
|
|
while result < s.len and s[result] in Whitespace:
|
|
if s[result] == '\n':
|
|
col = 1
|
|
inc line
|
|
else:
|
|
inc col
|
|
inc result
|
|
|
|
template parseCaret =
|
|
if result < s.len and s[result] == '^':
|
|
msgCol = col
|
|
inc result
|
|
inc col
|
|
skipWhitespace()
|
|
else:
|
|
spec.parseErrors.addLine "expected column marker ('^') for inline message"
|
|
|
|
template isMsgDelimiter: bool =
|
|
s[result] == ';' and
|
|
(block:
|
|
let nextTokenIdx = result + 1 + parseutils.skipWhitespace(s, result + 1)
|
|
if s.len > nextTokenIdx + len(inlineErrorKindMarker) and
|
|
s[nextTokenIdx..(nextTokenIdx + len(inlineErrorKindMarker) - 1)] == inlineErrorKindMarker:
|
|
true
|
|
else:
|
|
false)
|
|
|
|
template trimTrailingMsgWhitespace =
|
|
while msg.len > 0 and msg[^1] in Whitespace:
|
|
setLen msg, msg.len - 1
|
|
|
|
template addInlineError =
|
|
doAssert msg[^1] notin Whitespace
|
|
if kind == "Error": spec.action = actionReject
|
|
spec.inlineErrors.add InlineError(kind: kind, msg: msg, line: msgLine, col: msgCol)
|
|
|
|
parseKind()
|
|
skipWhitespace()
|
|
parseCaret()
|
|
|
|
while result < s.len-1:
|
|
if s[result] == '\n':
|
|
if result > 0 and s[result - 1] == '\r':
|
|
msg[^1] = '\n'
|
|
else:
|
|
msg.add '\n'
|
|
inc result
|
|
inc line
|
|
col = 1
|
|
elif isMsgDelimiter():
|
|
trimTrailingMsgWhitespace()
|
|
inc result
|
|
skipWhitespace()
|
|
addInlineError()
|
|
inc result, len(inlineErrorKindMarker)
|
|
inc col, 1 + len(inlineErrorKindMarker)
|
|
kind.setLen 0
|
|
msg.setLen 0
|
|
parseKind()
|
|
skipWhitespace()
|
|
parseCaret()
|
|
elif s[result] == ']' and s[result+1] == '#':
|
|
trimTrailingMsgWhitespace()
|
|
inc result, 2
|
|
inc col, 2
|
|
addInlineError()
|
|
break
|
|
else:
|
|
msg.add s[result]
|
|
inc result
|
|
inc col
|
|
|
|
if spec.inlineErrors.len > 0:
|
|
spec.unjoinable = true
|
|
|
|
proc extractSpec(filename: string; spec: var TSpec): string =
|
|
const
|
|
tripleQuote = "\"\"\""
|
|
specStart = "discard " & tripleQuote
|
|
var s = readFile(filename)
|
|
|
|
var i = 0
|
|
var a = -1
|
|
var b = -1
|
|
var line = 1
|
|
var col = 1
|
|
while i < s.len:
|
|
if (i == 0 or s[i-1] != ' ') and s.continuesWith(specStart, i):
|
|
# `s[i-1] == '\n'` would not work because of `tests/stdlib/tbase64.nim` which contains BOM (https://en.wikipedia.org/wiki/Byte_order_mark)
|
|
const lineMax = 10
|
|
if a != -1:
|
|
raise newException(ValueError, "testament spec violation: duplicate `specStart` found: " & $(filename, a, b, line))
|
|
elif line > lineMax:
|
|
# not overly restrictive, but prevents mistaking some `specStart` as spec if deeep inside a test file
|
|
raise newException(ValueError, "testament spec violation: `specStart` should be before line $1, or be indented; info: $2" % [$lineMax, $(filename, a, b, line)])
|
|
i += specStart.len
|
|
a = i
|
|
elif a > -1 and b == -1 and s.continuesWith(tripleQuote, i):
|
|
b = i
|
|
i += tripleQuote.len
|
|
elif s[i] == '\n':
|
|
inc line
|
|
inc i
|
|
col = 1
|
|
elif s.continuesWith(inlineErrorMarker, i):
|
|
i = extractErrorMsg(s, i, line, col, spec)
|
|
else:
|
|
inc col
|
|
inc i
|
|
|
|
if a >= 0 and b > a:
|
|
result = s.substr(a, b-1).multiReplace({"'''": tripleQuote, "\\31": "\31"})
|
|
elif a >= 0:
|
|
raise newException(ValueError, "testament spec violation: `specStart` found but not trailing `tripleQuote`: $1" % $(filename, a, b, line))
|
|
else:
|
|
result = ""
|
|
|
|
proc parseTargets*(value: string): set[TTarget] =
|
|
for v in value.normalize.splitWhitespace:
|
|
case v
|
|
of "c": result.incl(targetC)
|
|
of "cpp", "c++": result.incl(targetCpp)
|
|
of "objc": result.incl(targetObjC)
|
|
of "js": result.incl(targetJS)
|
|
else: raise newException(ValueError, "invalid target: '$#'" % v)
|
|
|
|
proc initSpec*(filename: string): TSpec =
|
|
result.file = filename
|
|
|
|
proc isCurrentBatch*(testamentData: TestamentData; filename: string): bool =
|
|
if testamentData.testamentNumBatch != 0:
|
|
hash(filename) mod testamentData.testamentNumBatch == testamentData.testamentBatch
|
|
else:
|
|
true
|
|
|
|
proc parseSpec*(filename: string): TSpec =
|
|
result.file = filename
|
|
result.filename = extractFilename(filename)
|
|
let specStr = extractSpec(filename, result)
|
|
var ss = newStringStream(specStr)
|
|
var p: CfgParser
|
|
open(p, ss, filename, 1)
|
|
var flags: HashSet[string]
|
|
var nimoutFound = false
|
|
while true:
|
|
var e = next(p)
|
|
case e.kind
|
|
of cfgKeyValuePair:
|
|
let key = e.key.normalize
|
|
const whiteListMulti = ["disabled", "ccodecheck"]
|
|
## list of flags that are correctly handled when passed multiple times
|
|
## (instead of being overwritten)
|
|
if key notin whiteListMulti:
|
|
doAssert key notin flags, $(key, filename)
|
|
flags.incl key
|
|
case key
|
|
of "action":
|
|
case e.value.normalize
|
|
of "compile":
|
|
result.action = actionCompile
|
|
of "run":
|
|
result.action = actionRun
|
|
of "reject":
|
|
result.action = actionReject
|
|
else:
|
|
result.parseErrors.addLine "cannot interpret as action: ", e.value
|
|
of "file":
|
|
if result.msg.len == 0 and result.nimout.len == 0:
|
|
result.parseErrors.addLine "errormsg or msg needs to be specified before file"
|
|
result.file = e.value
|
|
of "line":
|
|
if result.msg.len == 0 and result.nimout.len == 0:
|
|
result.parseErrors.addLine "errormsg, msg or nimout needs to be specified before line"
|
|
discard parseInt(e.value, result.line)
|
|
of "column":
|
|
if result.msg.len == 0 and result.nimout.len == 0:
|
|
result.parseErrors.addLine "errormsg or msg needs to be specified before column"
|
|
discard parseInt(e.value, result.column)
|
|
of "output":
|
|
if result.outputCheck != ocSubstr:
|
|
result.outputCheck = ocEqual
|
|
result.output = e.value
|
|
of "input":
|
|
result.input = e.value
|
|
of "outputsub":
|
|
result.outputCheck = ocSubstr
|
|
result.output = strip(e.value)
|
|
of "sortoutput":
|
|
try:
|
|
result.sortoutput = parseCfgBool(e.value)
|
|
except:
|
|
result.parseErrors.addLine getCurrentExceptionMsg()
|
|
of "exitcode":
|
|
discard parseInt(e.value, result.exitCode)
|
|
result.action = actionRun
|
|
of "errormsg":
|
|
result.msg = e.value
|
|
result.action = actionReject
|
|
of "nimout":
|
|
result.nimout = e.value
|
|
nimoutFound = true
|
|
of "nimoutfull":
|
|
result.nimoutFull = parseCfgBool(e.value)
|
|
of "batchable":
|
|
result.unbatchable = not parseCfgBool(e.value)
|
|
of "joinable":
|
|
result.unjoinable = not parseCfgBool(e.value)
|
|
of "valgrind":
|
|
when defined(linux) and sizeof(int) == 8:
|
|
result.useValgrind = if e.value.normalize == "leaks": leaking
|
|
else: ValgrindSpec(parseCfgBool(e.value))
|
|
result.unjoinable = true
|
|
if result.useValgrind != disabled:
|
|
result.outputCheck = ocSubstr
|
|
else:
|
|
# Windows lacks valgrind. Silly OS.
|
|
# Valgrind only supports OSX <= 17.x
|
|
result.useValgrind = disabled
|
|
of "disabled":
|
|
let value = e.value.normalize
|
|
case value
|
|
of "y", "yes", "true", "1", "on": result.err = reDisabled
|
|
of "n", "no", "false", "0", "off": discard
|
|
# These values are defined in `compiler/options.isDefined`
|
|
of "win":
|
|
when defined(windows): result.err = reDisabled
|
|
of "linux":
|
|
when defined(linux): result.err = reDisabled
|
|
of "bsd":
|
|
when defined(bsd): result.err = reDisabled
|
|
of "osx":
|
|
when defined(osx): result.err = reDisabled
|
|
of "unix", "posix":
|
|
when defined(posix): result.err = reDisabled
|
|
of "freebsd":
|
|
when defined(freebsd): result.err = reDisabled
|
|
of "littleendian":
|
|
when defined(littleendian): result.err = reDisabled
|
|
of "bigendian":
|
|
when defined(bigendian): result.err = reDisabled
|
|
of "cpu8", "8bit":
|
|
when defined(cpu8): result.err = reDisabled
|
|
of "cpu16", "16bit":
|
|
when defined(cpu16): result.err = reDisabled
|
|
of "cpu32", "32bit":
|
|
when defined(cpu32): result.err = reDisabled
|
|
of "cpu64", "64bit":
|
|
when defined(cpu64): result.err = reDisabled
|
|
# These values are for CI environments
|
|
of "travis": # deprecated
|
|
if isTravis: result.err = reDisabled
|
|
of "appveyor": # deprecated
|
|
if isAppVeyor: result.err = reDisabled
|
|
of "azure":
|
|
if isAzure: result.err = reDisabled
|
|
else:
|
|
# Check whether the value exists as an OS or CPU that is
|
|
# defined in `compiler/platform`.
|
|
block checkHost:
|
|
for os in platform.OS:
|
|
# Check if the value exists as OS.
|
|
if value == os.name.normalize:
|
|
# The value exists; is it the same as the current host?
|
|
if value == hostOS.normalize:
|
|
# The value exists and is the same as the current host,
|
|
# so disable the test.
|
|
result.err = reDisabled
|
|
# The value was defined, so there is no need to check further
|
|
# values or raise an error.
|
|
break checkHost
|
|
for cpu in platform.CPU:
|
|
# Check if the value exists as CPU.
|
|
if value == cpu.name.normalize:
|
|
# The value exists; is it the same as the current host?
|
|
if value == hostCPU.normalize:
|
|
# The value exists and is the same as the current host,
|
|
# so disable the test.
|
|
result.err = reDisabled
|
|
# The value was defined, so there is no need to check further
|
|
# values or raise an error.
|
|
break checkHost
|
|
# The value doesn't exist as an OS, CPU, or any previous value
|
|
# defined in this case statement, so raise an error.
|
|
result.parseErrors.addLine "cannot interpret as a bool: ", e.value
|
|
of "cmd":
|
|
if e.value.startsWith("nim "):
|
|
result.cmd = compilerPrefix & e.value[3..^1]
|
|
else:
|
|
result.cmd = e.value
|
|
of "ccodecheck":
|
|
result.ccodeCheck.add e.value
|
|
of "maxcodesize":
|
|
discard parseInt(e.value, result.maxCodeSize)
|
|
of "timeout":
|
|
try:
|
|
result.timeout = parseFloat(e.value)
|
|
except ValueError:
|
|
result.parseErrors.addLine "cannot interpret as a float: ", e.value
|
|
of "targets", "target":
|
|
try:
|
|
result.targets.incl parseTargets(e.value)
|
|
except ValueError as e:
|
|
result.parseErrors.addLine e.msg
|
|
of "matrix":
|
|
for v in e.value.split(';'):
|
|
result.matrix.add(v.strip)
|
|
else:
|
|
result.parseErrors.addLine "invalid key for test spec: ", e.key
|
|
|
|
of cfgSectionStart:
|
|
result.parseErrors.addLine "section ignored: ", e.section
|
|
of cfgOption:
|
|
result.parseErrors.addLine "command ignored: ", e.key & ": " & e.value
|
|
of cfgError:
|
|
result.parseErrors.addLine e.msg
|
|
of cfgEof:
|
|
break
|
|
close(p)
|
|
|
|
if skips.anyIt(it in result.file):
|
|
result.err = reDisabled
|
|
|
|
if nimoutFound and result.nimout.len == 0 and not result.nimoutFull:
|
|
result.parseErrors.addLine "empty `nimout` is vacuously true, use `nimoutFull:true` if intentional"
|
|
|
|
result.inCurrentBatch = isCurrentBatch(testamentData0, filename) or result.unbatchable
|
|
if not result.inCurrentBatch:
|
|
result.err = reDisabled
|
|
|
|
# Interpolate variables in msgs:
|
|
template varSub(msg: string): string =
|
|
try:
|
|
msg % ["/", $DirSep, "file", result.filename]
|
|
except ValueError:
|
|
result.parseErrors.addLine "invalid variable interpolation (see 'https://nim-lang.github.io/Nim/testament.html#writing-unitests-output-message-variable-interpolation')"
|
|
msg
|
|
result.nimout = result.nimout.varSub
|
|
result.msg = result.msg.varSub
|
|
for inlineError in result.inlineErrors.mitems:
|
|
inlineError.msg = inlineError.msg.varSub
|