# 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
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", 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), ""))
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 >= 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: " & resp.substr(i, i+200)
report.add "\n But got: " & answer.substr(i, i+200)
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 = 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()
let port = parseInt(a)
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 = sexpToAnswer(sx)
doReport(filename, answer, resp, report)
finally:
socket.sendEpcStr "return arg"
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
failures += runTest(xx)
failures += runEpcTest(xx)
else:
for x in walkFiles(tpath / "t*.nim"):
echo "Test ", 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()