summary refs log tree commit diff stats
path: root/tests/testament
diff options
context:
space:
mode:
Diffstat (limited to 'tests/testament')
-rw-r--r--tests/testament/backend.nim20
-rw-r--r--tests/testament/caasdriver.nim8
-rw-r--r--tests/testament/categories.nim41
-rw-r--r--tests/testament/htmlgen.nim12
-rw-r--r--tests/testament/specs.nim30
-rw-r--r--tests/testament/tester.nim238
6 files changed, 253 insertions, 96 deletions
diff --git a/tests/testament/backend.nim b/tests/testament/backend.nim
index 11743c337..e2e2e2dd5 100644
--- a/tests/testament/backend.nim
+++ b/tests/testament/backend.nim
@@ -81,34 +81,34 @@ proc getCommit(db: TDbConn): CommitId =
   let hash = "git log -n 1"()[commLen..commLen+10]
   let branch = "git symbolic-ref --short HEAD"()
   if hash.len == 0 or branch.len == 0: quit "cannot determine git HEAD"
-  
+
   let id = db.getValue(sql"select id from [Commit] where hash = ? and branch = ?",
                        hash, branch)
   if id.len > 0:
     result = id.parseInt.CommitId
   else:
-    result = db.insertId(sql"insert into [Commit](hash, branch) values (?, ?)", 
+    result = db.insertId(sql"insert into [Commit](hash, branch) values (?, ?)",
                          hash, branch).CommitId
 
-proc writeTestResult*(name, category, target, 
+proc writeTestResult*(name, category, target,
                       action, result, expected, given: string) =
-  let id = db.getValue(sql"""select id from TestResult 
+  let id = db.getValue(sql"""select id from TestResult
                              where name = ? and category = ? and target = ? and
-                                machine = ? and [commit] = ?""", 
+                                machine = ? and [commit] = ?""",
                                 name, category, target,
                                 thisMachine, thisCommit)
   if id.len > 0:
     db.exec(sql"""update TestResult
-                  set action = ?, result = ?, expected = ?, given = ? 
+                  set action = ?, result = ?, expected = ?, given = ?
                   where id = ?""", action, result, expected, given, id)
   else:
-    db.exec(sql"""insert into TestResult(name, category, target, 
-                                         action, 
+    db.exec(sql"""insert into TestResult(name, category, target,
+                                         action,
                                          result, expected, given,
                                          [commit], machine)
-                  values (?,?,?,?,?,?,?,?,?) """, name, category, target, 
+                  values (?,?,?,?,?,?,?,?,?) """, name, category, target,
                                         action,
-                                        result, expected, given, 
+                                        result, expected, given,
                                         thisCommit, thisMachine)
 
 proc open*() =
diff --git a/tests/testament/caasdriver.nim b/tests/testament/caasdriver.nim
index c61a9f108..30383bddb 100644
--- a/tests/testament/caasdriver.nim
+++ b/tests/testament/caasdriver.nim
@@ -137,11 +137,11 @@ proc doScenario(script: string, output: Stream, mode: TRunMode, verbose: bool):
       if line.strip.len == 0: continue
 
       if line.startsWith("#"):
-        output.writeln line
+        output.writeLine line
         continue
       elif line.startsWith(">"):
         s.doCommand(line.substr(1).strip)
-        output.writeln line, "\n", if verbose: s.lastOutput else: ""
+        output.writeLine line, "\n", if verbose: s.lastOutput else: ""
       else:
         var expectMatch = true
         var pattern = s.replaceVars(line)
@@ -153,9 +153,9 @@ proc doScenario(script: string, output: Stream, mode: TRunMode, verbose: bool):
           s.lastOutput.find(re(pattern, flags = {reStudy})) != -1
 
         if expectMatch == actualMatch:
-          output.writeln "SUCCESS ", line
+          output.writeLine "SUCCESS ", line
         else:
-          output.writeln "FAILURE ", line
+          output.writeLine "FAILURE ", line
           result = false
 
 iterator caasTestsRunner*(filter = "", verbose = false): tuple[test,
diff --git a/tests/testament/categories.nim b/tests/testament/categories.nim
index 4476fccf2..3166942ec 100644
--- a/tests/testament/categories.nim
+++ b/tests/testament/categories.nim
@@ -89,8 +89,11 @@ proc runBasicDLLTest(c, r: var TResults, cat: Category, options: string) =
     var libpath = getEnv"LD_LIBRARY_PATH".string
     # Temporarily add the lib directory to LD_LIBRARY_PATH:
     putEnv("LD_LIBRARY_PATH", "lib:" & libpath)
+    defer: putEnv("LD_LIBRARY_PATH", libpath)
     var serverDll = DynlibFormat % "server"
     safeCopyFile("tests/dll" / serverDll, "lib" / serverDll)
+    var nimrtlDll = DynlibFormat % "nimrtl"
+    safeCopyFile("tests/dll" / nimrtlDll, "lib" / nimrtlDll)
 
   testSpec r, makeTest("tests/dll/client.nim", options & " -d:useNimRtl",
                        cat, actionRun)
@@ -114,13 +117,23 @@ proc gcTests(r: var TResults, cat: Category, options: string) =
     testSpec r, makeTest("tests/gc" / filename, options &
                   " -d:release -d:useRealtimeGC", cat, actionRun)
 
-  template test(filename: expr): stmt =
+  template testWithoutBoehm(filename: expr): stmt =
     testWithoutMs filename
     testSpec r, makeTest("tests/gc" / filename, options &
                   " --gc:markAndSweep", cat, actionRun)
     testSpec r, makeTest("tests/gc" / filename, options &
                   " -d:release --gc:markAndSweep", cat, actionRun)
-
+  template test(filename: expr): stmt =
+    testWithoutBoehm filename
+    when not defined(windows):
+      # AR: cannot find any boehm.dll on the net, right now, so disabled
+      # for windows:
+      testSpec r, makeTest("tests/gc" / filename, options &
+                    " --gc:boehm", cat, actionRun)
+      testSpec r, makeTest("tests/gc" / filename, options &
+                    " -d:release --gc:boehm", cat, actionRun)
+
+  test "gcemscripten"
   test "growobjcrash"
   test "gcbench"
   test "gcleak"
@@ -130,14 +143,26 @@ proc gcTests(r: var TResults, cat: Category, options: string) =
   test "gcleak4"
   # Disabled because it works and takes too long to run:
   #test "gcleak5"
-  test "weakrefs"
+  testWithoutBoehm "weakrefs"
   test "cycleleak"
-  test "closureleak"
+  testWithoutBoehm "closureleak"
   testWithoutMs "refarrayleak"
 
   test "stackrefleak"
   test "cyclecollector"
 
+proc longGCTests(r: var TResults, cat: Category, options: string) =
+  when defined(windows):
+    let cOptions = "gcc -ldl -DWIN"
+  else:
+    let cOptions = "gcc -ldl"
+
+  var c = initResults()
+  # According to ioTests, this should compile the file
+  testNoSpec c, makeTest("tests/realtimeGC/shared", options, cat, actionCompile)
+  testC r, makeTest("tests/realtimeGC/cmain", cOptions, cat, actionRun)
+  testSpec r, makeTest("tests/realtimeGC/nmain", options & "--threads: on", cat, actionRun)
+
 # ------------------------- threading tests -----------------------------------
 
 proc threadTests(r: var TResults, cat: Category, options: string) =
@@ -226,7 +251,7 @@ proc testStdlib(r: var TResults, pattern, options: string, cat: Category) =
   for test in os.walkFiles(pattern):
     let contents = readFile(test).string
     if contents.contains("when isMainModule"):
-      testSpec r, makeTest(test, options, cat, actionRun)
+      testSpec r, makeTest(test, options, cat, actionRunNoSpec)
     else:
       testNoSpec r, makeTest(test, options, cat, actionCompile)
 
@@ -327,7 +352,7 @@ proc `&?.`(a, b: string): string =
   # candidate for the stdlib?
   result = if a.endswith(b): a else: a & b
 
-proc processCategory(r: var TResults, cat: Category, options: string) =
+proc processCategory(r: var TResults, cat: Category, options: string, fileGlob: string = "t*.nim") =
   case cat.string.normalize
   of "rodfiles":
     discard # Disabled for now
@@ -340,6 +365,8 @@ proc processCategory(r: var TResults, cat: Category, options: string) =
     dllTests(r, cat, options)
   of "gc":
     gcTests(r, cat, options)
+  of "longgc":
+    longGCTests(r, cat, options)
   of "debugger":
     debuggerTests(r, cat, options)
   of "manyloc":
@@ -362,5 +389,5 @@ proc processCategory(r: var TResults, cat: Category, options: string) =
   of "nimble-all":
     testNimblePackages(r, cat, pfAll)
   else:
-    for name in os.walkFiles("tests" & DirSep &.? cat.string / "t*.nim"):
+    for name in os.walkFiles("tests" & DirSep &.? cat.string / fileGlob):
       testSpec r, makeTest(name, options, cat)
diff --git a/tests/testament/htmlgen.nim b/tests/testament/htmlgen.nim
index a9f739995..98ccf1170 100644
--- a/tests/testament/htmlgen.nim
+++ b/tests/testament/htmlgen.nim
@@ -138,7 +138,7 @@ proc generateHtml*(filename: string, commit: int; onlyFailing: bool) =
   # generate navigation:
   outfile.write("""<ul id="tabs">""")
   for m in db.rows(sql"select id, name, os, cpu from Machine order by id"):
-    outfile.writeln """<li><a href="#$#">$#: $#, $#</a></li>""" % m
+    outfile.writeLine """<li><a href="#$#">$#: $#, $#</a></li>""" % m
   outfile.write("</ul>")
 
   for currentMachine in db.rows(sql"select id from Machine order by id"):
@@ -195,7 +195,7 @@ proc generateJson*(filename: string, commit: int) =
   let machine = $backend.getMachine(db)
   let data = db.getRow(sql(selRow), lastCommit, machine)
 
-  outfile.writeln("""{"total": $#, "passed": $#, "skipped": $#""" % data)
+  outfile.writeLine("""{"total": $#, "passed": $#, "skipped": $#""" % data)
 
   let results = newJArray()
   for row in db.rows(sql(selResults), lastCommit):
@@ -208,7 +208,7 @@ proc generateJson*(filename: string, commit: int) =
     obj["expected"] = %row[5]
     obj["given"] = %row[6]
     results.add(obj)
-  outfile.writeln(""", "results": """)
+  outfile.writeLine(""", "results": """)
   outfile.write(results.pretty)
 
   if not previousCommit.isNil:
@@ -220,9 +220,9 @@ proc generateJson*(filename: string, commit: int) =
       obj["old"] = %row[1]
       obj["new"] = %row[2]
       diff.add obj
-    outfile.writeln(""", "diff": """)
-    outfile.writeln(diff.pretty)
+    outfile.writeLine(""", "diff": """)
+    outfile.writeLine(diff.pretty)
 
-  outfile.writeln "}"
+  outfile.writeLine "}"
   close(db)
   close(outfile)
diff --git a/tests/testament/specs.nim b/tests/testament/specs.nim
index 2a8a4ea24..bab17d2cd 100644
--- a/tests/testament/specs.nim
+++ b/tests/testament/specs.nim
@@ -10,13 +10,14 @@
 import parseutils, strutils, os, osproc, streams, parsecfg
 
 const
-  cmdTemplate* = r"nim $target --hints:on $options $file"
+  cmdTemplate* = r"compiler" / "nim $target --lib:lib --hints:on -d:testing $options $file"
 
 type
   TTestAction* = enum
     actionCompile = "compile"
     actionRun = "run"
     actionReject = "reject"
+    actionRunNoSpec = "runNoSpec"
   TResultEnum* = enum
     reNimcCrash,     # nim compiler seems to have crashed
     reMsgsDiffer,       # error messages differ
@@ -42,7 +43,10 @@ type
     action*: TTestAction
     file*, cmd*: string
     outp*: string
-    line*, exitCode*: int
+    line*, column*: int
+    tfile*: string
+    tline*, tcolumn*: int
+    exitCode*: int
     msg*: string
     ccodeCheck*: string
     err*: TResultEnum
@@ -77,7 +81,7 @@ proc extractSpec(filename: string): string =
 when not defined(nimhygiene):
   {.pragma: inject.}
 
-template parseSpecAux(fillResult: stmt) {.immediate.} =
+template parseSpecAux(fillResult: untyped) =
   var ss = newStringStream(extractSpec(filename))
   var p {.inject.}: CfgParser
   open(p, ss, filename, 1)
@@ -91,13 +95,21 @@ template parseSpecAux(fillResult: stmt) {.immediate.} =
       fillResult
   close(p)
 
-proc parseSpec*(filename: string): TSpec =
-  result.file = filename
+proc specDefaults*(result: var TSpec) =
   result.msg = ""
   result.outp = ""
   result.nimout = ""
   result.ccodeCheck = ""
   result.cmd = cmdTemplate
+  result.line = 0
+  result.column = 0
+  result.tfile = ""
+  result.tline = 0
+  result.tcolumn = 0
+
+proc parseSpec*(filename: string): TSpec =
+  specDefaults(result)
+  result.file = filename
   parseSpecAux:
     case normalize(e.key)
     of "action":
@@ -108,7 +120,11 @@ proc parseSpec*(filename: string): TSpec =
       else: echo ignoreMsg(p, e)
     of "file": result.file = e.value
     of "line": discard parseInt(e.value, result.line)
-    of "output": 
+    of "column": discard parseInt(e.value, result.column)
+    of "tfile": result.tfile = e.value
+    of "tline": discard parseInt(e.value, result.tline)
+    of "tcolumn": discard parseInt(e.value, result.tcolumn)
+    of "output":
       result.action = actionRun
       result.outp = e.value
     of "outputsub":
@@ -117,7 +133,7 @@ proc parseSpec*(filename: string): TSpec =
       result.substr = true
     of "sortoutput":
       result.sortoutput = parseCfgBool(e.value)
-    of "exitcode": 
+    of "exitcode":
       discard parseInt(e.value, result.exitCode)
     of "msg":
       result.msg = e.value
diff --git a/tests/testament/tester.nim b/tests/testament/tester.nim
index 7391b105e..636093a7f 100644
--- a/tests/testament/tester.nim
+++ b/tests/testament/tester.nim
@@ -12,7 +12,7 @@
 import
   parseutils, strutils, pegs, os, osproc, streams, parsecfg, json,
   marshal, backend, parseopt, specs, htmlgen, browsers, terminal,
-  algorithm, compiler/nodejs
+  algorithm, compiler/nodejs, re
 
 const
   resultsFile = "testresults.html"
@@ -23,6 +23,7 @@ const
 Command:
   all                         run all tests
   c|category <category>       run all the tests of a certain category
+  r|run <test>                run single test file
   html [commit]               generate $1 from the database; uses the latest
                               commit or a specific one (use -1 for the commit
                               before latest etc)
@@ -31,6 +32,7 @@ Arguments:
 Options:
   --print                   also print results to the console
   --failing                 only show failing/ignored tests
+  --pedantic                return non-zero status code if there are failures
 """ % resultsFile
 
 type
@@ -50,7 +52,9 @@ type
 
 let
   pegLineError =
-    peg"{[^(]*} '(' {\d+} ', ' \d+ ') ' ('Error') ':' \s* {.*}"
+    peg"{[^(]*} '(' {\d+} ', ' {\d+} ') ' ('Error') ':' \s* {.*}"
+  pegLineTemplate =
+    peg"{[^(]*} '(' {\d+} ', ' {\d+} ') ' 'template/generic instantiation from here'.*"
   pegOtherError = peg"'Error:' \s* {.*}"
   pegSuccess = peg"'Hint: operation successful'.*"
   pegOfInterest = pegLineError / pegOtherError
@@ -64,6 +68,7 @@ proc callCompiler(cmdTemplate, filename, options: string,
   let outp = p.outputStream
   var suc = ""
   var err = ""
+  var tmpl = ""
   var x = newStringOfCap(120)
   result.nimout = ""
   while outp.readLine(x.TaintedString) or running(p):
@@ -71,22 +76,53 @@ proc callCompiler(cmdTemplate, filename, options: string,
     if x =~ pegOfInterest:
       # `err` should contain the last error/warning message
       err = x
+    elif x =~ pegLineTemplate and err == "":
+      # `tmpl` contains the last template expansion before the error
+      tmpl = x
     elif x =~ pegSuccess:
       suc = x
   close(p)
   result.msg = ""
   result.file = ""
   result.outp = ""
-  result.line = -1
+  result.line = 0
+  result.column = 0
+  result.tfile = ""
+  result.tline = 0
+  result.tcolumn = 0
+  if tmpl =~ pegLineTemplate:
+    result.tfile = extractFilename(matches[0])
+    result.tline = parseInt(matches[1])
+    result.tcolumn = parseInt(matches[2])
   if err =~ pegLineError:
     result.file = extractFilename(matches[0])
     result.line = parseInt(matches[1])
-    result.msg = matches[2]
+    result.column = parseInt(matches[2])
+    result.msg = matches[3]
   elif err =~ pegOtherError:
     result.msg = matches[0]
   elif suc =~ pegSuccess:
     result.err = reSuccess
 
+proc callCCompiler(cmdTemplate, filename, options: string,
+                  target: TTarget): TSpec =
+  let c = parseCmdLine(cmdTemplate % ["target", targetToCmd[target],
+                       "options", options, "file", filename.quoteShell])
+  var p = startProcess(command="gcc", args=c[5.. ^1],
+                       options={poStdErrToStdOut, poUsePath})
+  let outp = p.outputStream
+  var x = newStringOfCap(120)
+  result.nimout = ""
+  result.msg = ""
+  result.file = ""
+  result.outp = ""
+  result.line = -1
+  while outp.readLine(x.TaintedString) or running(p):
+    result.nimout.add(x & "\n")
+  close(p)
+  if p.peekExitCode == 0:
+    result.err = reSuccess
+
 proc initResults: TResults =
   result.total = 0
   result.passed = 0
@@ -115,23 +151,38 @@ proc addResult(r: var TResults, test: TTest,
                           expected = expected,
                           given = given)
   r.data.addf("$#\t$#\t$#\t$#", name, expected, given, $success)
-  if success == reIgnored:
-    styledEcho styleBright, name, fgYellow, " [", $success, "]"
-  elif success != reSuccess:
-    styledEcho styleBright, name, fgRed, " [", $success, "]"
-    echo"Expected:"
-    styledEcho styleBright, expected
-    echo"Given:"
-    styledEcho styleBright, given
+  if success == reSuccess:
+    styledEcho fgGreen, "PASS: ", fgCyan, name
+  elif success == reIgnored:
+    styledEcho styleDim, fgYellow, "SKIP: ", styleBright, fgCyan, name
+  else:
+    styledEcho styleBright, fgRed, "FAIL: ", fgCyan, name
+    styledEcho styleBright, fgCyan, "Test \"", test.name, "\"", " in category \"", test.cat.string, "\""
+    styledEcho styleBright, fgRed, "Failure: ", $success
+    styledEcho fgYellow, "Expected:"
+    styledEcho styleBright, expected, "\n"
+    styledEcho fgYellow, "Gotten:"
+    styledEcho styleBright, given, "\n"
 
 proc cmpMsgs(r: var TResults, expected, given: TSpec, test: TTest) =
   if strip(expected.msg) notin strip(given.msg):
     r.addResult(test, expected.msg, given.msg, reMsgsDiffer)
-  elif extractFilename(expected.file) != extractFilename(given.file) and
+  elif expected.tfile == "" and 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)
+  elif expected.line   != given.line   and expected.line   != 0 or
+       expected.column != given.column and expected.column != 0:
+    r.addResult(test, $expected.line & ':' & $expected.column,
+                      $given.line    & ':' & $given.column,
+                      reLinesDiffer)
+  elif expected.tfile != "" and extractFilename(expected.tfile) != extractFilename(given.tfile) and
+      "internal error:" notin expected.msg:
+    r.addResult(test, expected.tfile, given.tfile, reFilesDiffer)
+  elif expected.tline   != given.tline   and expected.tline   != 0 or
+       expected.tcolumn != given.tcolumn and expected.tcolumn != 0:
+    r.addResult(test, $expected.tline & ':' & $expected.tcolumn,
+                      $given.tline    & ':' & $given.tcolumn,
+                      reLinesDiffer)
   else:
     r.addResult(test, expected.msg, given.msg, reSuccess)
     inc(r.passed)
@@ -183,72 +234,126 @@ proc compilerOutputTests(test: TTest, given: var TSpec, expected: TSpec;
       expectedmsg = expected.nimout
       givenmsg = given.nimout.strip
       nimoutCheck(test, expectedmsg, given)
+  else:
+    givenmsg = given.nimout.strip
   if given.err == reSuccess: inc(r.passed)
   r.addResult(test, expectedmsg, givenmsg, given.err)
 
+proc analyzeAndConsolidateOutput(s: string): string =
+  result = ""
+  let rows = s.splitLines
+  for i in 0 ..< rows.len:
+    if (let pos = find(rows[i], "Traceback (most recent call last)"); pos != -1):
+      result = substr(rows[i], pos) & "\n"
+      for i in i+1 ..< rows.len:
+        result.add rows[i] & "\n"
+        if not (rows[i] =~ re"^[^(]+\(\d+\)\s+"):
+          return
+    elif (let pos = find(rows[i], "SIGSEGV: Illegal storage access."); pos != -1):
+      result = substr(rows[i], pos)
+      return
+
 proc testSpec(r: var TResults, test: TTest) =
   # major entry point for a single test
   let tname = test.name.addFileExt(".nim")
   inc(r.total)
-  styledEcho "Processing ", fgCyan, extractFilename(tname)
-  var expected = parseSpec(tname)
+  var expected: TSpec
+  if test.action != actionRunNoSpec:
+    expected = parseSpec(tname)
+  else:
+    specDefaults expected
+    expected.action = actionRunNoSpec
+
   if expected.err == reIgnored:
     r.addResult(test, "", "", reIgnored)
     inc(r.skipped)
-  else:
-    case expected.action
-    of actionCompile:
-      var given = callCompiler(expected.cmd, test.name,
-        test.options & " --hint[Path]:off --hint[Processing]:off", test.target)
-      compilerOutputTests(test, given, expected, r)
-    of actionRun:
-      var given = callCompiler(expected.cmd, test.name, test.options,
-                               test.target)
-      if given.err != reSuccess:
-        r.addResult(test, "", given.msg, given.err)
-      else:
-        var exeFile: string
-        if test.target == targetJS:
-          let (dir, file, ext) = splitFile(tname)
-          exeFile = dir / "nimcache" / file & ".js"
-        else:
-          exeFile = changeFileExt(tname, ExeExt)
-        if existsFile(exeFile):
-          let nodejs = findNodeJs()
-          if test.target == targetJS and nodejs == "":
-            r.addResult(test, expected.outp, "nodejs binary not in PATH",
-                        reExeNotFound)
-            return
-          var (buf, exitCode) = execCmdEx(
-            (if test.target == targetJS: nodejs & " " else: "") & exeFile)
-          if exitCode != expected.exitCode:
-            r.addResult(test, "exitcode: " & $expected.exitCode,
-                              "exitcode: " & $exitCode, reExitCodesDiffer)
-          else:
-            var bufB = strip(buf.string)
-            if expected.sortoutput: bufB = makeDeterministic(bufB)
-            if bufB != strip(expected.outp):
-              if not (expected.substr and expected.outp in bufB):
-                given.err = reOutputsDiffer
-            compilerOutputTests(test, given, expected, r)
-        else:
-          r.addResult(test, expected.outp, "executable not found", reExeNotFound)
-    of actionReject:
-      var given = callCompiler(expected.cmd, test.name, test.options,
-                               test.target)
-      cmpMsgs(r, expected, given, test)
+    return
+
+  case expected.action
+  of actionCompile:
+    var given = callCompiler(expected.cmd, test.name,
+      test.options & " --hint[Path]:off --hint[Processing]:off", test.target)
+    compilerOutputTests(test, given, expected, r)
+  of actionRun, actionRunNoSpec:
+    # In this branch of code "early return" pattern is clearer than deep
+    # nested conditionals - the empty rows in between to clarify the "danger"
+    var given = callCompiler(expected.cmd, test.name, test.options,
+                             test.target)
+
+    if given.err != reSuccess:
+      r.addResult(test, "", given.msg, given.err)
+      return
+
+    let isJsTarget = test.target == targetJS
+    var exeFile: string
+    if isJsTarget:
+      let (dir, file, ext) = splitFile(tname)
+      exeFile = dir / "nimcache" / file & ".js" # *TODO* hardcoded "nimcache"
+    else:
+      exeFile = changeFileExt(tname, ExeExt)
+
+    if not existsFile(exeFile):
+      r.addResult(test, expected.outp, "executable not found", reExeNotFound)
+      return
+
+    let nodejs = if isJsTarget: findNodeJs() else: ""
+    if isJsTarget and nodejs == "":
+      r.addResult(test, expected.outp, "nodejs binary not in PATH",
+                  reExeNotFound)
+      return
+
+    let exeCmd = (if isJsTarget: nodejs & " " else: "") & exeFile
+    var (buf, exitCode) = execCmdEx(exeCmd, options = {poStdErrToStdOut})
+    let bufB = if expected.sortoutput: makeDeterministic(strip(buf.string))
+               else: strip(buf.string)
+    let expectedOut = strip(expected.outp)
+
+    if exitCode != expected.exitCode:
+      r.addResult(test, "exitcode: " & $expected.exitCode,
+                        "exitcode: " & $exitCode & "\n\nOutput:\n" &
+                        analyzeAndConsolidateOutput(bufB),
+                        reExitCodesDiffer)
+      return
+
+    if bufB != expectedOut:
+      if not (expected.substr and expectedOut in bufB):
+        given.err = reOutputsDiffer
+        r.addResult(test, expected.outp, bufB, reOutputsDiffer)
+        return
+
+    compilerOutputTests(test, given, expected, r)
+    return
+
+  of actionReject:
+    var given = callCompiler(expected.cmd, test.name, test.options,
+                             test.target)
+    cmpMsgs(r, expected, given, test)
+    return
 
 proc testNoSpec(r: var TResults, test: TTest) =
   # does not extract the spec because the file is not supposed to have any
   let tname = test.name.addFileExt(".nim")
   inc(r.total)
-  styledEcho "Processing ", fgCyan, extractFilename(tname)
   let given = callCompiler(cmdTemplate, test.name, test.options, test.target)
   r.addResult(test, "", given.msg, given.err)
   if given.err == reSuccess: inc(r.passed)
 
+proc testC(r: var TResults, test: TTest) =
+  # runs C code. Doesn't support any specs, just goes by exit code.
+  let tname = test.name.addFileExt(".c")
+  inc(r.total)
+  styledEcho "Processing ", fgCyan, extractFilename(tname)
+  var given = callCCompiler(cmdTemplate, test.name & ".c", test.options, test.target)
+  if given.err != reSuccess:
+    r.addResult(test, "", given.msg, given.err)
+  elif test.action == actionRun:
+    let exeFile = changeFileExt(test.name, ExeExt)
+    var (buf, exitCode) = execCmdEx(exeFile, options = {poStdErrToStdOut, poUseShell})
+    if exitCode != 0: given.err = reExitCodesDiffer
+  if given.err == reSuccess: inc(r.passed)
+
 proc makeTest(test, options: string, cat: Category, action = actionCompile,
-              target = targetC): TTest =
+              target = targetC, env: string = ""): TTest =
   # start with 'actionCompile', will be overwritten in the spec:
   result = TTest(cat: cat, name: test, options: options,
                  target: target, action: action)
@@ -267,12 +372,14 @@ proc main() =
   backend.open()
   var optPrintResults = false
   var optFailing = false
+  var optPedantic = false
   var p = initOptParser()
   p.next()
   while p.kind == cmdLongoption:
     case p.key.string.normalize
     of "print", "verbose": optPrintResults = true
     of "failing": optFailing = true
+    of "pedantic": optPedantic = true
     else: quit Usage
     p.next()
   if p.kind != cmdArgument: quit Usage
@@ -293,6 +400,11 @@ proc main() =
     var cat = Category(p.key)
     p.next
     processCategory(r, cat, p.cmdLineRest.string)
+  of "r", "run":
+    let (dir, file) = splitPath(p.key.string)
+    let (_, subdir) = splitPath(dir)
+    var cat = Category(subdir)
+    processCategory(r, cat, p.cmdLineRest.string, file)
   of "html":
     var commit = 0
     discard parseInt(p.cmdLineRest.string, commit)
@@ -305,8 +417,10 @@ proc main() =
     if action == "html": openDefaultBrowser(resultsFile)
     else: echo r, r.data
   backend.close()
+  if optPedantic:
+    var failed = r.total - r.passed - r.skipped
+    if failed > 0 : quit(QuitFailure)
 
 if paramCount() == 0:
   quit Usage
 main()
-