#
#
# Nimrod Tester
# (c) Copyright 2013 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## This program verifies Nimrod against the testcases.
import
parseutils, strutils, pegs, os, osproc, streams, parsecfg, browsers, json,
marshal, cgi, parseopt, caasdriver
const
cmdTemplate = r"nimrod cc --hints:on $# $#"
resultsFile = "testresults.html"
jsonFile = "testresults.json"
Usage = "usage: tester [--print] " &
"reject|compile|run|" &
"merge|special|rodfiles| [nimrod options]\n" &
" or: tester test|comp|rej singleTest"
type
TTestAction = enum
actionCompile, actionRun, actionReject
TResultEnum = enum
reNimrodcCrash, # nimrod compiler seems to have crashed
reMsgsDiffer, # error messages differ
reFilesDiffer, # expected and given filenames differ
reLinesDiffer, # expected and given line numbers differ
reOutputsDiffer,
reExitcodesDiffer,
reInvalidPeg,
reCodegenFailure,
reCodeNotFound,
reExeNotFound,
reIgnored, # test is ignored
reSuccess # test was successful
TTarget = enum
targetC, targetCpp, targetObjC, targetJS
TSpec = object
action: TTestAction
file, cmd: string
outp: string
line, exitCode: int
msg: string
ccodeCheck: string
err: TResultEnum
substr: bool
TResults = object
total, passed, skipped: int
data: string
# ----------------------- Spec parser ----------------------------------------
when not defined(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(EInvalidValue, "cannot interpret as a bool: " & s)
proc extractSpec(filename: string): string =
const tripleQuote = "\"\"\""
var x = readFile(filename).string
var a = x.find(tripleQuote)
var b = x.find(tripleQuote, a+3)
# look for """ only in the first section
if a >= 0 and b > a and a < 40:
result = x.substr(a+3, b-1).replace("'''", tripleQuote)
else:
#echo "warning: file does not contain spec: " & filename
result = ""
when not defined(nimhygiene):
{.pragma: inject.}
template parseSpecAux(fillResult: stmt) {.immediate.} =
var ss = newStringStream(extractSpec(filename))
var p {.inject.}: TCfgParser
open(p, ss, filename, 1)
while true:
var e {.inject.} = next(p)
case e.kind
of cfgEof: break
of cfgSectionStart, cfgOption, cfgError:
echo ignoreMsg(p, e)
of cfgKeyValuePair:
fillResult
close(p)
proc parseSpec(filename: string): TSpec =
result.file = filename
result.msg = ""
result.outp = ""
result.ccodeCheck = ""
result.cmd = cmdTemplate
parseSpecAux:
case normalize(e.key)
of "action":
case e.value.normalize
of "compile": result.action = actionCompile
of "run": result.action = actionRun
of "reject": result.action = actionReject
else: echo ignoreMsg(p, e)
of "file": result.file = e.value
of "line": discard parseInt(e.value, result.line)
of "output": result.outp = e.value
of "outputsub":
result.outp = e.value
result.substr = true
of "exitcode":
discard parseInt(e.value, result.exitCode)
of "errormsg", "msg": result.msg = e.value
of "disabled":
if parseCfgBool(e.value): result.err = reIgnored
of "cmd": result.cmd = e.value
of "ccodecheck": result.ccodeCheck = e.value
else: echo ignoreMsg(p, e)
# ----------------------------------------------------------------------------
let
pegLineError =
peg"{[^(]*} '(' {\d+} ', ' \d+ ') ' ('Error'/'Warning') ':' \s* {.*}"
pegOtherError = peg"'Error:' \s* {.*}"
pegSuccess = peg"'Hint: operation successful'.*"
pegOfInterest = pegLineError / pegOtherError
proc callCompiler(cmdTemplate, filename, options: string): TSpec =
let c = parseCmdLine(cmdTemplate % [options, filename])
var p = startProcess(command=c[0], args=c[1.. -1],
options={poStdErrToStdOut, poUseShell})
let outp = p.outputStream
var suc = ""
var err = ""
var x = newStringOfCap(120)
while outp.readLine(x.TaintedString) or running(p):
if x =~ pegOfInterest:
# `err` should contain the last error/warning message
err = x
elif x =~ pegSuccess:
suc = x
close(p)
result.msg = ""
result.file = ""
result.outp = ""
result.line = -1
if err =~ pegLineError:
result.file = extractFilename(matches[0])
result.line = parseInt(matches[1])
result.msg = matches[2]
elif err =~ pegOtherError:
result.msg = matches[0]
elif suc =~ pegSuccess:
result.err = reSuccess
proc initResults: TResults =
result.total = 0
result.passed = 0
result.skipped = 0
result.data = ""
proc readResults(filename: string): TResults =
result = marshal.to[TResults](readFile(filename).string)
proc writeResults(filename: string, r: TResults) =
writeFile(filename, $$r)
proc `$`(x: TResults): string =
result = ("Tests passed: $1 / $3
\n" &
"Tests skipped: $2 / $3
\n") %
[$x.passed, $x.skipped, $x.total]
proc colorResult(r: TResultEnum): string =
case r
of reIgnored: result = "ignored"
of reSuccess: result = "yes"
else: result = "no"
const
TableHeader4 = "
Test | Expected | " &
"Given | Success |
\n"
TableHeader3 = "Test | " &
"Given | Success |
\n"
TableFooter = "
\n"
HtmlBegin = """
Test results
"""
HtmlEnd = ""
proc td(s: string): string =
result = s.substr(0, 200).XMLEncode
proc addResult(r: var TResults, test, expected, given: string,
success: TResultEnum) =
r.data.addf("$# | $# | $# | $# |
\n", [
XMLEncode(test), td(expected), td(given), success.colorResult])
proc addResult(r: var TResults, test, given: string,
success: TResultEnum) =
r.data.addf("$# | $# | $# |
\n", [
XMLEncode(test), td(given), success.colorResult])
proc listResults(reject, compile, run: TResults) =
var s = HtmlBegin
s.add("Tests to Reject
\n")
s.add($reject)
s.add(TableHeader4 & reject.data & TableFooter)
s.add("
Tests to Compile
\n")
s.add($compile)
s.add(TableHeader3 & compile.data & TableFooter)
s.add("
Tests to Run
\n")
s.add($run)
s.add(TableHeader4 & run.data & TableFooter)
s.add(HtmlEnd)
writeFile(resultsFile, s)
proc cmpMsgs(r: var TResults, expected, given: TSpec, test: string) =
if strip(expected.msg) notin strip(given.msg):
r.addResult(test, expected.msg, given.msg, reMsgsDiffer)
elif extractFilename(expected.file) != extractFilename(given.file) and
"internal error:" notin expected.msg:
r.addResult(test, expected.file, given.file, reFilesDiffer)
elif expected.line != given.line and expected.line != 0:
r.addResult(test, $expected.line, $given.line, reLinesDiffer)
else:
r.addResult(test, expected.msg, given.msg, reSuccess)
inc(r.passed)
proc rejectSingleTest(r: var TResults, test, options: string) =
let test = test.addFileExt(".nim")
var t = extractFilename(test)
inc(r.total)
echo t
var expected = parseSpec(test)
if expected.err == reIgnored:
r.addResult(t, "", "", reIgnored)
inc(r.skipped)
else:
var given = callCompiler(expected.cmd, test, options)
cmpMsgs(r, expected, given, t)
proc reject(r: var TResults, dir, options: string) =
## handle all the tests that the compiler should reject
for test in os.walkFiles(dir / "t*.nim"): rejectSingleTest(r, test, options)
proc codegenCheck(test, check, ext: string, given: var TSpec) =
if check.len > 0:
try:
let (path, name, ext2) = test.splitFile
echo path / "nimcache" / name.changeFileExt(ext)
let contents = readFile(path / "nimcache" / name.changeFileExt(ext)).string
if contents.find(check.peg) < 0:
given.err = reCodegenFailure
except EInvalidValue:
given.err = reInvalidPeg
except EIO:
given.err = reCodeNotFound
proc codegenChecks(test: string, expected: TSpec, given: var TSpec) =
codegenCheck(test, expected.ccodeCheck, ".c", given)
proc compile(r: var TResults, pattern, options: string) =
for test in os.walkFiles(pattern):
let t = extractFilename(test)
echo t
inc(r.total)
let expected = parseSpec(test)
if expected.err == reIgnored:
r.addResult(t, "", reIgnored)
inc(r.skipped)
else:
var given = callCompiler(expected.cmd, test, options)
if given.err == reSuccess:
codegenChecks(test, expected, given)
r.addResult(t, given.msg, given.err)
if given.err == reSuccess: inc(r.passed)
proc compileSingleTest(r: var TResults, test, options: string) =
# does not extract the spec because the file is not supposed to have any
let test = test.addFileExt(".nim")
let t = extractFilename(test)
inc(r.total)
echo t
let given = callCompiler(cmdTemplate, test, options)
r.addResult(t, given.msg, given.err)
if given.err == reSuccess: inc(r.passed)
proc runSingleTest(r: var TResults, test, options: string, target: TTarget) =
var test = test.addFileExt(".nim")
var t = extractFilename(test)
echo t
inc(r.total)
var expected = parseSpec(test)
if expected.err == reIgnored:
r.addResult(t, "", "", reIgnored)
inc(r.skipped)
else:
var given = callCompiler(expected.cmd, test, options)
if given.err != reSuccess:
r.addResult(t, "", given.msg, given.err)
else:
var exeFile: string
if target == targetC:
exeFile = changeFileExt(test, ExeExt)
else:
let (dir, file, ext) = splitFile(test)
exeFile = dir / "nimcache" / file & ".js"
if existsFile(exeFile):
var (buf, exitCode) = execCmdEx(
(if target==targetJS: "node " else: "") & exeFile)
if exitCode != expected.ExitCode:
r.addResult(t, "exitcode: " & $expected.ExitCode,
"exitcode: " & $exitCode, reExitCodesDiffer)
else:
if strip(buf.string) != strip(expected.outp):
if not (expected.substr and expected.outp in buf.string):
given.err = reOutputsDiffer
if given.err == reSuccess:
codeGenChecks(test, expected, given)
if given.err == reSuccess: inc(r.passed)
r.addResult(t, expected.outp, buf.string, given.err)
else:
r.addResult(t, expected.outp, "executable not found", reExeNotFound)
proc runSingleTest(r: var TResults, test, options: string) =
runSingleTest(r, test, options, targetC)
proc run(r: var TResults, dir, options: string) =
for test in os.walkFiles(dir / "t*.nim"): runSingleTest(r, test, options)
include specials
proc compileExample(r: var TResults, pattern, options: string) =
for test in os.walkFiles(pattern): compileSingleTest(r, test, options)
proc toJson(res: TResults): PJsonNode =
result = newJObject()
result["total"] = newJInt(res.total)
result["passed"] = newJInt(res.passed)
result["skipped"] = newJInt(res.skipped)
proc outputJSON(reject, compile, run: TResults) =
var doc = newJObject()
doc["reject"] = toJson(reject)
doc["compile"] = toJson(compile)
doc["run"] = toJson(run)
var s = pretty(doc)
writeFile(jsonFile, s)
proc runCaasTests(r: var TResults) =
for test, output, status, mode in caasTestsRunner():
r.addResult(test, "", output & "-> " & $mode,
if status: reSuccess else: reOutputsDiffer)
proc main() =
os.putenv "NIMTEST_NO_COLOR", "1"
os.putenv "NIMTEST_OUTPUT_LVL", "PRINT_FAILURES"
const
compileJson = "compile.json"
runJson = "run.json"
rejectJson = "reject.json"
var optPrintResults = false
var p = initOptParser()
p.next()
if p.kind == cmdLongoption:
case p.key.string
of "print": optPrintResults = true
else: quit usage
p.next()
if p.kind != cmdArgument: quit usage
var action = p.key.string.normalize
p.next()
var r = initResults()
case action
of "reject":
reject(r, "tests/reject", p.cmdLineRest.string)
rejectSpecialTests(r, p.cmdLineRest.string)
writeResults(rejectJson, r)
of "compile":
compile(r, "tests/compile/t*.nim", p.cmdLineRest.string)
compile(r, "tests/ccg/t*.nim", p.cmdLineRest.string)
compile(r, "tests/js.nim", p.cmdLineRest.string)
compileExample(r, "lib/pure/*.nim", p.cmdLineRest.string)
compileExample(r, "examples/*.nim", p.cmdLineRest.string)
compileExample(r, "examples/gtk/*.nim", p.cmdLineRest.string)
compileExample(r, "examples/talk/*.nim", p.cmdLineRest.string)
compileSpecialTests(r, p.cmdLineRest.string)
writeResults(compileJson, r)
of "run":
run(r, "tests/run", p.cmdLineRest.string)
runSpecialTests(r, p.cmdLineRest.string)
writeResults(runJson, r)
of "special":
runSpecialTests(r, p.cmdLineRest.string)
runCaasTests(r)
writeResults(runJson, r)
of "rodfiles":
runRodFiles(r, p.cmdLineRest.string)
writeResults(runJson, r)
of "js":
if existsFile(runJSon):
r = readResults(runJson)
runJsTests(r, p.cmdLineRest.string)
writeResults(runJson, r)
of "merge":
var rejectRes = readResults(rejectJson)
var compileRes = readResults(compileJson)
var runRes = readResults(runJson)
listResults(rejectRes, compileRes, runRes)
outputJSON(rejectRes, compileRes, runRes)
of "dll":
runDLLTests r, p.cmdLineRest.string
of "gc":
runGCTests(r, p.cmdLineRest.string)
of "test":
if p.kind != cmdArgument: quit usage
var testFile = p.key.string
p.next()
runSingleTest(r, testFile, p.cmdLineRest.string)
of "comp", "rej":
if p.kind != cmdArgument: quit usage
var testFile = p.key.string
p.next()
if peg"'/reject/'" in testFile or action == "rej":
rejectSingleTest(r, testFile, p.cmdLineRest.string)
elif peg"'/compile/'" in testFile or action == "comp":
compileSingleTest(r, testFile, p.cmdLineRest.string)
else:
runSingleTest(r, testFile, p.cmdLineRest.string)
else:
quit usage
if optPrintResults: echo r, r.data
if paramCount() == 0:
quit usage
main()