japl/tests/testbuilder.nim

212 lines
8.2 KiB
Nim

# Copyright 2020 Mattia Giambirtone
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import testobject
import logutils
import testconfig
import os
import strutils
import strformat
## A rudimentary test builder. Converts test directories to test
## sequences.
proc parseModalLine(line: string): tuple[modal: bool, mode: string, detail: string, comment: bool] =
## parses one line. If it's a line that's a mode (in format [modename: detail] it returns modal: true, else modal: false.
## mode contains the content of the line, if it's modal the mode name
## detail contains the content of the detail of the modename. If empty or non modal ""
## if comment is true, the returned value has to be ignored
# when non modal, mode becomes the line
# when comment is true, it must not do anything to whenever it is exported
# initialize result
var start = 0
result.modal = false
result.mode = ""
result.detail = ""
result.comment = false
if line.len() > 0 and line[0] == '[':
if line.len() > 1:
if line[1] == '[':
# escaped early return
result.mode = line[1..line.high()]
return result
elif line[1] == ';':
# comment early return
result.comment = true
result.modal = true
return result
result.modal = true
start = 1
# not modal line early return
elif line.len() >= 3 and line[0..2] == "//[":
if line.len() > 3:
result.modal = true
start = 3
else:
fatal "Invalid line //[, no mode defined."
else:
result.mode = line
return result
# normal modal line:
var colon = false # if there has been a colon already
for i in countup(start, line.high()):
let ch = line[i]
if ch in Letters or ch in Digits or ch in {'_', '-'}:
# legal characters
if colon:
result.detail &= ($ch).toLower()
else:
result.mode &= ($ch).toLower()
elif ch == ':':
# colon
if not colon:
colon = true
else:
fatal &"Two colons in <{line}> not allowed."
elif ch in Whitespace:
discard
elif ch == ']':
# closing can only come at the end
if i != line.high():
fatal &"] is only allowed to close the line <{line}>."
elif ch == '[':
# can only start with it
if i > 0:
fatal &"[ is only allowed to open the modal line <{line}>."
else:
fatal &"Illegal character in <{line}>: {ch}."
# must be closed by it
if line[line.high()] != ']':
fatal &"Line <{line}> must be closed off by ']'."
proc buildTest(lines: seq[string], i: var int, name: string, path: string): Test =
## Builds a single test (starting with index i in lines, while i is modified)
result = newTest(name, path)
# since this is a very simple parser, some state can reduce code length
inc i # to discard the first "test" mode
var mode: string
var detail: string
var inside: bool = false
var body: string
var modeline: int = -1
while i < lines.len():
let parsed = parseModalLine(lines[i])
let line = parsed.mode
if parsed.modal and not parsed.comment:
if inside:
if parsed.mode == "end":
# end inside
if mode == "source" and (detail == "mixed"):
result.parseMixed(body)
elif mode == "source" and (detail == "raw" or detail == ""):
result.parseSource(body)
elif mode == "stdout" or mode == "stderr":
let err = (mode == "stderr")
if detail == "":
result.parseStdout(body, err = err)
elif detail == "re":
result.parseStdout(body, re = true, err = err)
elif detail == "nw":
result.parseStdout(body, nw = true, err = err)
elif detail == "nwre":
result.parseStdout(body, nw = true, re = true, err = err)
else:
fatal &"Invalid mode detail {detail} for mode {mode} in test {name} at line {modeline} in {path}. Valid are re, nw and nwre."
elif detail != "":
fatal &"Invalid mode detail {detail} for mode {mode} in test {name} at line {modeline} in {path}."
# non-modedetail modes below:
elif mode == "stdin":
result.parseStdin(body)
elif mode == "python":
result.parsePython(body)
else:
fatal &"Invalid mode {mode} for test {name} at line {modeline} in {path}."
inside = false
mode = ""
detail = ""
body = ""
modeline = -1
else:
fatal &"Invalid mode {parsed.mode} when inside a block (currently in mode {mode}) at line {i} in {path}."
else: # still if modal, but not inside
if parsed.mode == "skip":
result.m_skipped = true
if not force:
result.skip()
elif parsed.mode == "end":
# end of test
return result
else:
# start a new mode
inside = true
mode = parsed.mode
detail = parsed.detail
modeline = i
elif parsed.comment:
discard
elif inside: # when not modal
body &= line & "\n"
inc i
fatal &"Test mode unfinished (missing [end]?)."
proc buildTestFile(path: string): seq[Test] =
## Builds a test file consisting of multiple tests
log(LogLevel.Debug, &"Checking {path} for tests")
let lines = path.readFile().split('\n')
var i = 0
while i < lines.len():
let parsed = lines[i].parseModalLine()
if parsed.modal and not parsed.comment:
if parsed.mode == "test":
let testname = parsed.detail
log(LogLevel.Debug, &"Building test {testname} at {path}")
result.add buildTest(lines, i, testname, path)
else:
fatal &"Invalid mode at root-level {parsed.mode} at line {i} of file {path}."
# root can only contain "test" modes, anything else is just a comment (including modal and non modal comments)
inc i
proc buildTests*(testDir: string): seq[Test] =
## Builds all test within the directory testDir
## if testDir is a file, only build that one file
if not dirExists(testDir):
if fileExists(testDir):
result &= buildTestFile(testDir)
for test in result:
test.important = true
else:
fatal "test dir/file doesn't exist"
for kind, candidate in walkDir(testDir):
if dirExists(candidate):
log(LogLevel.Debug, &"Descending into dir {candidate}")
result &= buildTests(candidate)
else:
try:
result &= buildTestFile(candidate)
except FatalError:
discard
except:
write stderr, getCurrentExceptionMsg()
write stderr, getCurrentException().getStacktrace()
log(LogLevel.Error, &"Building test file {candidate} failed")