summary refs log tree commit diff stats
path: root/nimsuggest/tester.nim
diff options
context:
space:
mode:
Diffstat (limited to 'nimsuggest/tester.nim')
-rw-r--r--nimsuggest/tester.nim374
1 files changed, 374 insertions, 0 deletions
diff --git a/nimsuggest/tester.nim b/nimsuggest/tester.nim
new file mode 100644
index 000000000..9b9488348
--- /dev/null
+++ b/nimsuggest/tester.nim
@@ -0,0 +1,374 @@
+# Tester for nimsuggest.
+# Every test file can have a #[!]# comment that is deleted from the input
+# before 'nimsuggest' is invoked to ensure this token doesn't make a
+# crucial difference for Nim's parser.
+# When debugging, to run a single test, use for e.g.:
+# `nim r nimsuggest/tester.nim nimsuggest/tests/tsug_accquote.nim`
+
+import os, osproc, strutils, streams, re, sexp, net
+from sequtils import toSeq
+
+type
+  Test = object
+    filename, cmd, dest: string
+    startup: seq[string]
+    script: seq[(string, string)]
+    disabled: bool
+
+const
+  DummyEof = "!EOF!"
+  tpath = "nimsuggest/tests"
+  # we could also use `stdtest/specialpaths`
+
+import std/compilesettings
+
+proc parseTest(filename: string; epcMode=false): Test =
+  const cursorMarker = "#[!]#"
+  let nimsug = "bin" / addFileExt("nimsuggest_testing", ExeExt)
+  doAssert nimsug.fileExists, nimsug
+  const libpath = querySetting(libPath)
+  result.filename = filename
+  result.dest = getTempDir() / extractFilename(filename)
+  result.cmd = nimsug & " --tester " & result.dest
+  result.script = @[]
+  result.startup = @[]
+  var tmp = open(result.dest, fmWrite)
+  var specSection = 0
+  var markers = newSeq[string]()
+  var i = 1
+  for x in lines(filename):
+    let marker = x.find(cursorMarker)
+    if marker >= 0:
+      if epcMode:
+        markers.add "(\"" & filename & "\" " & $i & " " & $marker & " \"" & result.dest & "\")"
+      else:
+        markers.add "\"" & filename & "\";\"" & result.dest & "\":" & $i & ":" & $marker
+      tmp.writeLine x.replace(cursorMarker, "")
+    else:
+      tmp.writeLine x
+    if x.contains("""""""""):
+      inc specSection
+    elif specSection == 1:
+      if x.startsWith("disabled:"):
+        if x.startsWith("disabled:true"):
+          result.disabled = true
+        else:
+          # be strict about format
+          doAssert x.startsWith("disabled:false")
+          result.disabled = false
+      elif x.startsWith("$nimsuggest"):
+        result.cmd = x % ["nimsuggest", nimsug, "file", filename, "lib", libpath]
+      elif x.startsWith("!"):
+        if result.cmd.len == 0:
+          result.startup.add x
+        else:
+          result.script.add((x, ""))
+      elif x.startsWith(">"):
+        # since 'markers' here are not complete yet, we do the $substitutions
+        # afterwards
+        result.script.add((x.substr(1).replaceWord("$path", tpath).replaceWord("$file", filename), ""))
+      elif x.len > 0:
+        # expected output line:
+        let x = x % ["file", filename, "lib", libpath]
+        result.script[^1][1].add x.replace(";;", "\t") & '\L'
+        # else: ignore empty lines for better readability of the specs
+    inc i
+  tmp.close()
+  # now that we know the markers, substitute them:
+  for a in mitems(result.script):
+    a[0] = a[0] % markers
+
+proc parseCmd(c: string): seq[string] =
+  # we don't support double quotes for now so that
+  # we can later support them properly with escapes and stuff.
+  result = @[]
+  var i = 0
+  var a = ""
+  while i < c.len:
+    setLen(a, 0)
+    # eat all delimiting whitespace
+    while i < c.len and c[i] in {' ', '\t', '\l', '\r'}: inc(i)
+    if i >= c.len: break
+    case c[i]
+    of '"': raise newException(ValueError, "double quotes not yet supported: " & c)
+    of '\'':
+      var delim = c[i]
+      inc(i) # skip ' or "
+      while i < c.len and c[i] != delim:
+        add a, c[i]
+        inc(i)
+      if i < c.len: inc(i)
+    else:
+      while i < c.len and c[i] > ' ':
+        add(a, c[i])
+        inc(i)
+    add(result, a)
+
+proc edit(tmpfile: string; x: seq[string]) =
+  if x.len != 3 and x.len != 4:
+    quit "!edit takes two or three arguments"
+  let f = if x.len >= 4: tpath / x[3] else: tmpfile
+  try:
+    let content = readFile(f)
+    let newcontent = content.replace(x[1], x[2])
+    if content == newcontent:
+      quit "wrong test case: edit had no effect"
+    writeFile(f, newcontent)
+  except IOError:
+    quit "cannot edit file " & tmpfile
+
+proc exec(x: seq[string]) =
+  if x.len != 2: quit "!exec takes one argument"
+  if execShellCmd(x[1]) != 0:
+    quit "External program failed " & x[1]
+
+proc copy(x: seq[string]) =
+  if x.len != 3: quit "!copy takes two arguments"
+  let rel = tpath
+  copyFile(rel / x[1], rel / x[2])
+
+proc del(x: seq[string]) =
+  if x.len != 2: quit "!del takes one argument"
+  removeFile(tpath / x[1])
+
+proc runCmd(cmd, dest: string): bool =
+  result = cmd[0] == '!'
+  if not result: return
+  let x = cmd.parseCmd()
+  case x[0]
+  of "!edit":
+    edit(dest, x)
+  of "!exec":
+    exec(x)
+  of "!copy":
+    copy(x)
+  of "!del":
+    del(x)
+  else:
+    quit "unknown command: " & cmd
+
+proc smartCompare(pattern, x: string): bool =
+  if pattern.contains('*'):
+    result = match(x, re(escapeRe(pattern).replace("\\x2A","(.*)"), {}))
+
+proc sendEpcStr(socket: Socket; cmd: string) =
+  let s = cmd.find(' ')
+  doAssert s > 0
+  var args = cmd.substr(s+1)
+  if not args.startsWith("("): args = escapeJson(args)
+  let c = "(call 567 " & cmd.substr(0, s) & args & ")"
+  socket.send toHex(c.len, 6)
+  socket.send c
+
+proc recvEpc(socket: Socket): string =
+  var L = newStringOfCap(6)
+  if socket.recv(L, 6) != 6:
+    raise newException(ValueError, "recv A failed #" & L & "#")
+  let x = parseHexInt(L)
+  result = newString(x)
+  if socket.recv(result, x) != x:
+    raise newException(ValueError, "recv B failed")
+
+proc sexpToAnswer(s: SexpNode): string =
+  result = ""
+  doAssert s.kind == SList
+  doAssert s.len >= 3
+  let m = s[2]
+  if m.kind != SList:
+    echo s
+  doAssert m.kind == SList
+  for a in m:
+    doAssert a.kind == SList
+    #s.section,
+    #s.symkind,
+    #s.qualifiedPath.map(newSString),
+    #s.filePath,
+    #s.forth,
+    #s.line,
+    #s.column,
+    #s.doc
+    if a.len >= 9:
+      let section = a[0].getStr
+      let symk = a[1].getStr
+      let qp = a[2]
+      let file = a[3].getStr
+      let typ = a[4].getStr
+      let line = a[5].getNum
+      let col = a[6].getNum
+      let doc = a[7].getStr.escape
+      result.add section
+      result.add '\t'
+      result.add symk
+      result.add '\t'
+      var i = 0
+      if qp.kind == SList:
+        for aa in qp:
+          if i > 0: result.add '.'
+          result.add aa.getStr
+          inc i
+      result.add '\t'
+      result.add typ
+      result.add '\t'
+      result.add file
+      result.add '\t'
+      result.addInt line
+      result.add '\t'
+      result.addInt col
+      result.add '\t'
+      result.add doc
+      result.add '\t'
+      result.addInt a[8].getNum
+      if a.len >= 11:
+        result.add '\t'
+        result.addInt a[9].getNum
+        result.add '\t'
+        result.addInt a[10].getNum
+      elif a.len >= 10:
+        result.add '\t'
+        result.add a[9].getStr
+    result.add '\L'
+
+proc doReport(filename, answer, resp: string; report: var string) =
+  if resp != answer and not smartCompare(resp, answer):
+    report.add "\nTest failed: " & filename
+    var hasDiff = false
+    for i in 0..min(resp.len-1, answer.len-1):
+      if resp[i] != answer[i]:
+        report.add "\n  Expected:\n" & resp
+        report.add "\n  But got:\n" & answer
+        hasDiff = true
+        break
+    if not hasDiff:
+      report.add "\n  Expected:  " & resp
+      report.add "\n  But got:   " & answer
+
+proc skipDisabledTest(test: Test): bool =
+  if test.disabled:
+    echo "disabled: " & test.filename
+  result = test.disabled
+
+proc runEpcTest(filename: string): int =
+  let s = parseTest(filename, true)
+  if s.skipDisabledTest: return 0
+  for req, _ in items(s.script):
+    if req.startsWith("highlight"):
+      echo "disabled epc: " & s.filename
+      return 0
+  for cmd in s.startup:
+    if not runCmd(cmd, s.dest):
+      quit "invalid command: " & cmd
+  let epccmd = if s.cmd.contains("--v3"):
+    s.cmd.replace("--tester", "--epc --log")
+  else:
+    s.cmd.replace("--tester", "--epc --v2 --log")
+  let cl = parseCmdLine(epccmd)
+  var p = startProcess(command=cl[0], args=cl[1 .. ^1],
+                       options={poStdErrToStdOut, poUsePath,
+                       poInteractive, poDaemon})
+  let outp = p.outputStream
+  var report = ""
+  var socket = newSocket()
+  try:
+    # read the port number:
+    when defined(posix):
+      var a = newStringOfCap(120)
+      discard outp.readLine(a)
+    else:
+      var i = 0
+      while not osproc.hasData(p) and i < 100:
+        os.sleep(50)
+        inc i
+      let a = outp.readAll().strip()
+    var port: int
+    try:
+      port = parseInt(a)
+    except ValueError:
+      echo "Error parsing port number: " & a
+      echo outp.readAll()
+      quit 1
+    socket.connect("localhost", Port(port))
+
+    for req, resp in items(s.script):
+      if not runCmd(req, s.dest):
+        socket.sendEpcStr(req)
+        let sx = parseSexp(socket.recvEpc())
+        if not req.startsWith("mod "):
+          let answer = if sx[2].kind == SNil: "" else: sexpToAnswer(sx)
+          doReport(filename, answer, resp, report)
+
+    socket.sendEpcStr "return arg"
+      # bugfix: this was in `finally` block, causing the original error to be
+      # potentially masked by another one in case `socket.sendEpcStr` raises
+      # (e.g. if socket couldn't connect in the 1st place)
+  finally:
+    close(p)
+  if report.len > 0:
+    echo "==== EPC ========================================"
+    echo report
+  result = report.len
+
+proc runTest(filename: string): int =
+  let s = parseTest filename
+  if s.skipDisabledTest: return 0
+  for cmd in s.startup:
+    if not runCmd(cmd, s.dest):
+      quit "invalid command: " & cmd
+  let cl = parseCmdLine(s.cmd)
+  var p = startProcess(command=cl[0], args=cl[1 .. ^1],
+                       options={poStdErrToStdOut, poUsePath,
+                       poInteractive, poDaemon})
+  let outp = p.outputStream
+  let inp = p.inputStream
+  var report = ""
+  var a = newStringOfCap(120)
+  try:
+    # read and ignore anything nimsuggest says at startup:
+    while outp.readLine(a):
+      if a == DummyEof: break
+    for req, resp in items(s.script):
+      if not runCmd(req, s.dest):
+        inp.writeLine(req)
+        inp.flush()
+        var answer = ""
+        while outp.readLine(a):
+          if a == DummyEof: break
+          answer.add a
+          answer.add '\L'
+        doReport(filename, answer, resp, report)
+  finally:
+    try:
+      inp.writeLine("quit")
+      inp.flush()
+    except IOError, OSError:
+      # assume it's SIGPIPE, ie, the child already died
+      discard
+    close(p)
+  if report.len > 0:
+    echo "==== STDIN ======================================"
+    echo report
+  result = report.len
+
+proc main() =
+  var failures = 0
+  if os.paramCount() > 0:
+    let x = os.paramStr(1)
+    let xx = expandFilename x
+    # run only stdio when running single test
+    failures += runTest(xx)
+  else:
+    let files = toSeq(walkFiles(tpath / "t*.nim"))
+    for i, x in files:
+      echo "$#/$# test: $#" % [$i, $files.len, x]
+      when defined(i386):
+        if x == "nimsuggest/tests/tmacro_highlight.nim":
+          echo "skipping" # workaround bug #17945
+          continue
+      let xx = expandFilename x
+      when not defined(windows):
+        # XXX Windows IO redirection seems bonkers:
+        failures += runTest(xx)
+      failures += runEpcTest(xx)
+  if failures > 0:
+    quit 1
+
+main()