Merge pull request #39 from Productive2/master

Testing suite progress
This commit is contained in:
Mattia 2021-02-09 18:26:18 +01:00 committed by GitHub
commit a5371ea25b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 1260 additions and 270 deletions

View File

@ -28,14 +28,10 @@ import strformat
import parseutils
import strutils
proc natPrint*(args: seq[ptr Obj]): tuple[kind: retNative, result: ptr Obj] =
## Native function print
## Prints an object representation
## to stdout. If more than one argument
## is passed, they will be printed separated
## by a space
template join(args: seq[ptr Obj]): string =
## A template that returns the string
## representation of all args separated
## by a space.
var res = ""
for i in countup(0, args.high()):
let arg = args[i]
@ -43,12 +39,29 @@ proc natPrint*(args: seq[ptr Obj]): tuple[kind: retNative, result: ptr Obj] =
res = res & arg.stringify() & " "
else:
res = res & arg.stringify()
echo res
res
proc natPrint*(args: seq[ptr Obj]): tuple[kind: retNative, result: ptr Obj] =
## Native function print
## Prints an object representation
## to stdout. If more than one argument
## is passed, they will be printed separated
## by a space
# Note: we return nil and not asNil() because
# the VM will later use its own cached pointer
# to nil
echo join(args)
return (kind: retNative.Nil, result: nil)
proc natPrintErr*(args: seq[ptr Obj]): tuple[kind:
retNative, result: ptr Obj] =
## Native function printErr
## Prints an object representation
## to stderr. If more than one argument
## is passed, they will be printed separated
## by a space
writeLine stderr, join(args)
return (kind: retNative.Nil, result: nil)
proc natReadline*(args: seq[ptr Obj]): tuple[kind: retNative, result: ptr Obj] =
## Native function readline

View File

@ -67,7 +67,7 @@ proc disassembleInstruction*(chunk: Chunk, offset: int): int =
of jumpInstructions:
result = jumpInstruction($opcode, chunk, offset)
else:
echo &"Unknown opcode {opcode} at index {offset}"
echo &"DEBUG - Unknown opcode {opcode} at index {offset}"
result = offset + 1
@ -77,4 +77,4 @@ proc disassembleChunk*(chunk: Chunk, name: string) =
var index = 0
while index < chunk.code.len:
index = disassembleInstruction(chunk, index)
echo &"==== Debug session ended - Chunk '{name}' ===="
echo &"==== Debug session ended - Chunk '{name}' ===="

View File

@ -715,6 +715,7 @@ proc initStdlib*(vm: VM) =
when DEBUG_TRACE_VM:
echo "DEBUG - VM: Initializing stdlib"
vm.defineGlobal("print", newNative("print", natPrint, -1))
vm.defineGlobal("printErr", newNative("printErr", natPrintErr, -1))
vm.defineGlobal("clock", newNative("clock", natClock, 0))
vm.defineGlobal("round", newNative("round", natRound, -1))
vm.defineGlobal("toInt", newNative("toInt", natToInt, 1))

View File

@ -1,3 +1,6 @@
[Test: all]
[skip]
[source: mixed]
// Example file to test JAPL's syntax
// Mathematical expressions
@ -137,3 +140,5 @@ mark.greet();
"implicit start"[:5]; // From 0 to 5
"hello" + " world"; // Strings are immutable!
"hello" * 3; //hellohellohello
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: arithmetic]
[source: mixed]
//int arithmetic
print(7+5); //stdout:12
@ -32,3 +34,5 @@ print(64/-64);//stdout:-1.0
print(8/0);//stdout:inf
print(8/-0);//stdout:inf
print(-8/0);//stdout:-inf
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: bitwise]
[source: mixed]
print(~5 | 5);//stdout:-1
print(1 | 2);//stdout:3
print(1 & 2);//stdout:0
@ -7,3 +9,5 @@ print(32 | 64);//stdout:96
print(96 & 32);//stdout:32
print(~0);//stdout:-1
print(~356);//stdout:-357
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: booleans]
[source: mixed]
print(2 or 3);//stdout:2
print(2 and 3);//stdout:3
print(false or true);//stdout:true
@ -19,3 +21,5 @@ print(not 0);//stdout:true
print(not 1);//stdout:false
print(not 1 and not 2);//stdout:false
print(not (1 and 0));//stdout:true
[end]
[end]

View File

@ -1,4 +1,5 @@
[Test: callchain]
[source: mixed]
fun add2(x)
{
return x + 2;
@ -16,3 +17,5 @@ print(add2(sub2(mul2(sub2(5)))));
//5-2=3
//3*2=6
//stdout:6
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: comparisons]
[source: mixed]
var x = 4;
var y = 5;
var z = 6;
@ -40,3 +42,5 @@ if (8 <= z)
print("15");
else
print("16");//stdout:16
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: compile_time_intern]
[source: mixed]
//compile time interning
var a = "hello";
@ -9,5 +11,6 @@ print(a is b);//stdout:true
var x = "ex";
var y = "ey";
print(x is y);//stdout:false
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: constant_long]
[source: mixed]
// Test for constants
var v_1 = 1;
@ -128,3 +130,5 @@ var v_125 = 1;
var v_126 = 1;
var v_127 = 1;
var v_128 = 1;
[end]
[end]

View File

@ -1,4 +1,8 @@
[Test: read_in_own_init_regex]
[source: raw]
var a = 1; { var a = a; }
//stdout:A fatal error occurred while compiling '''', line 1, at ';' -> cannot read local variable in its own initializer
//stdout:
[end]
[stderr: re]
[[^\-]*-> cannot read local variable in its own initializer
[end]
[end]

View File

@ -1,8 +1,10 @@
[Test: undefname_raw]
[source: raw]
var a = b;
//stdout:An unhandled exception occurred, traceback below:
//stdout: File '''', line 1, in <module>:
//stdout:ReferenceError: undefined name 'b'
//stdout:
[end]
[stderr]
An unhandled exception occurred, traceback below:
File '''', line 1, in <module>:
ReferenceError: undefined name 'b'
[end]
[end]

View File

@ -1,8 +1,10 @@
[Test: unsup_binary_instr]
[source: raw]
var a = 2 + "hey";
//stdout:An unhandled exception occurred, traceback below:
//stdout: File '''', line 1, in <module>:
//stdout:TypeError: unsupported binary operator '+' for objects of type 'integer' and 'string'
//stdout:
[end]
[stderr]
An unhandled exception occurred, traceback below:
File '''', line 1, in <module>:
TypeError: unsupported binary operator '+' for objects of type 'integer' and 'string'
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: problem1]
[source: mixed]
// Task: find the multiples of 3 and 5 below 1000, find their sum
var sum = 0;
@ -9,3 +11,5 @@ for (var x = 3; x < 1001; x = x + 1)
}
}
print(sum);//stdout:234168
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: problem2]
[source: mixed]
// Sum of even valued fibonacci numbers that don't exceed 4M
var a = 1;
@ -14,4 +16,5 @@ while (a < 4000000)
b = c;
}
print(sum);//stdout:4613732
[end]
[end]

View File

@ -1,3 +1,6 @@
[Test: problem4]
[skip]
[source: mixed]
// Find the largest palindrome that is a product of two 3 digit numbers
fun isPalindrome(n)
@ -63,3 +66,5 @@ for (var i = 100; i < 1000; i = i + 1)
}
}
print(largest);
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: fibonacci]
[source: raw]
fun fib(n) {
if (n < 2)
return n;
@ -11,4 +13,17 @@ print(fib(5));//stdout:5
print(fib(6));//stdout:8
print(fib(7));//stdout:13
print(fib(8));//stdout:21
print(fib(9));//stdout:34
print(fib(9));//stdout:3
[end]
[stdout]
1
1
2
3
5
8
13
21
34
[end]
[end]

View File

@ -1,6 +1,10 @@
[Test: for]
[source: mixed]
for (var x = 0; x < 2; x = x + 1)
{
print(x);
//stdout:0
//stdout:1
}
[end]
[end]

View File

@ -1,4 +1,6 @@
[Test: forwithfunction]
[skip]
[source: mixed]
var y = 0; //a global to keep track of state
//does not need closures for this to work yet
@ -82,3 +84,5 @@ for (var i = 0; i != -1; i = next(i))
// y = 9
//stdout:10
// y = 10
[end]
[end]

View File

@ -1,4 +0,0 @@
print("Hello, world.");
//stdout:Hello, world.
//stdout:

View File

@ -1,4 +1,15 @@
[Test: hellojapl]
[source: mixed]
print("Hello, JAPL.");
//stdout:Hello, JAPL.
[end]
[end]
//stdout:
[Test: hello_second_way]
[source: raw]
print("Hello, JAPL.");
[end]
[stdout]
Hello, JAPL.
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: if]
[source: mixed]
var x = 5;
if (x > 2)
{
@ -26,3 +28,5 @@ if (2 == x)
print("2");
else
print("not 2");//stdout:not 2
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: ifchain]
[source: mixed]
fun printInt(x) {
if (x == 1)
print("one");
@ -20,3 +22,5 @@ x = 7;
printInt(x);
x = 1;
printInt(x);//stdout:one
[end]
[end]

View File

@ -1,3 +1,13 @@
//stdin:Hello world!
[Test: inputtesttwo]
[source: raw]
print(readLine());
//stdout:Hello world!
[end]
[stdin]
Hello world!
[end]
[stdout]
Hello world!
[end]
[end]

View File

@ -1,3 +1,6 @@
[Test: is]
[skip]
[source:mixed]
var x = 4;
var y = x;
@ -19,4 +22,5 @@ print((l is z) is l);//stdout:true
var k;
print(k is nil);//stdout:true
[end]
[end]

View File

@ -1,4 +1,5 @@
[Test: lambdachain]
[source: mixed]
var add2 = lambda(x)
{
return x + 2;
@ -16,3 +17,5 @@ print(add2(sub2(mul2(sub2(5)))));
//5-2=3
//3*2=6
//stdout:6
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: glob_assgn_read]
[source: mixed]
var a0 = 451;
var a1 = 5098;
var a2 = 469;
@ -1499,3 +1501,5 @@ print(a151);//stdout:4839
print(a975);//stdout:7651
print(a7);//stdout:2979
print(a661);//stdout:8235
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: glob_with_sets]
[source: mixed]
var a0 = 829;
var a1 = 6820;
var a2 = 114;
@ -5236,3 +5238,5 @@ print(a87);//stdout:1282
print(a445);//stdout:1726
print(a790);//stdout:1140
print(a961);//stdout:1708
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: loc_assgn_read]
[source: mixed]
{
var a0 = 9103;
var a1 = 4565;
@ -1500,4 +1502,6 @@ print(a142);//stdout:4255
print(a722);//stdout:5380
print(a538);//stdout:8625
print(a809);//stdout:4506
}
}
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: loc_with_sets]
[source: mixed]
{
var a0 = 8313;
var a1 = 3509;
@ -5183,4 +5185,6 @@ print(a729);//stdout:2001
print(a380);//stdout:2145
print(a125);//stdout:4280
print(a55);//stdout:6992
}
}
[end]
[end]

19
tests/japl/meta/empty.jpl Normal file
View File

@ -0,0 +1,19 @@
[; This is a comment
This is a comment as well.
[[ This is a comment too
[test]
This is a comment as well.
An empty test should not crash, but it should pass.
[; more comments
[[ even more comments
Also note, a test without a name should not crash
the test builder, but it should be a test with
the name "".
[end]

19
tests/japl/meta/mixed.jpl Normal file
View File

@ -0,0 +1,19 @@
[test: mixed]
[source: mixed]
print("Hello", readLine());
//stdout:Hello world
//stdin:world
print("aaaaaa");
//stdoutre:a*
printErr("Hello", readLine());
//stderr:Hello error
//stdin:error
printErr("bbbbbb");
//stderrre:b*
[end]
[end]

24
tests/japl/meta/nw.jpl Normal file
View File

@ -0,0 +1,24 @@
[Test: nw]
[source]
print("hey");
print("second line");
printErr("hey there");
print("abcde");
printErr("12345");
printErr("0123456789.");
[end]
[stdout: nw]
hey
second line
[end]
[stdout: nwre]
[a-z]*
[end]
[stderr: nw]
hey there
[end]
[stderr: nwre]
[[0-9]*
[0-9]*.
[end]
[end]

25
tests/japl/meta/raw.jpl Normal file
View File

@ -0,0 +1,25 @@
[Test: raw]
[source: raw]
print("Hi", readLine());
print("aaaaaaa");
printErr("Bye", readLine());
printErr("bbbbbbb");
//stdout:This is not a part of the expected output
[end]
[stdin]
person
very important person
[end]
[stdout]
Hi person
[end]
[stdout: re]
a*
[end]
[stderr]
Bye very important person
[end]
[stderr: re]
b*
[end]
[end]

7
tests/japl/meta/skip.jpl Normal file
View File

@ -0,0 +1,7 @@
[test: skipped]
[skip]
[stdout]
Hello this text won't be matched.
[end]
[end]

View File

@ -1,5 +1,9 @@
[Test: nan]
[source: raw]
print((5/0)*0);
[end]
[stdout]
nan
[end]
[end]
//stdout:nan
//stdout:

View File

@ -1,3 +1,5 @@
[Test: reassignment]
[source: mixed]
{
var x = 5;
var y = x;
@ -23,3 +25,5 @@ fun resetter(x) {
var q = 5;
resetter(q);//stdout:7
print(q);//stdout:5
[end]
[end]

View File

@ -1,3 +1,6 @@
[Test: runtimeinterning]
[skip]
[source: mixed]
//runtime interning
var f = "leafy";
@ -11,5 +14,5 @@ print(h is j);//stdout:true
var x = "ex";
var y = "ey";
print(x is y);//stdout:false
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: shadowing]
[source: mixed]
//similar to vars.jpl, but more focused on shadowing
// simple shadowing
@ -74,3 +76,5 @@ eat();//stdout:nom nom nom
print(eat);//stdout:5
}
eat();//stdout:nom nom nom
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: strings]
[source: mixed]
var left = "left";
var right = "right";
var directions = left + " " + right;
@ -10,3 +12,5 @@ left = left + " side";
print(left);//stdout:left side
right = "side: " + right;
print(right);//stdout:side: right
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: vars]
[source: mixed]
var x = 1;
var y = 2;
print(x);//stdout:1
@ -28,3 +30,5 @@ longName = "hello";
print(longName); //stdout:hello
longName = longName + " world";
print(longName); //stdout:hello world
[end]
[end]

View File

@ -1,3 +1,5 @@
[Test: while]
[source: mixed]
var x = 5;
while (x > 0)
{
@ -18,3 +20,5 @@ while (x < 10)
string = string + "A";
}
print(string);//stdout:hAAAAAAAAAA
[end]
[end]

View File

@ -17,15 +17,23 @@
# a testrunner process
import ../src/vm
import os
var btvm = initVM()
try:
discard btvm.interpret(readFile(paramStr(1)), "")
var source: string
while true:
let ch = stdin.readChar()
if ch == char(4):
break
else:
source &= ch
discard btvm.interpret(source, "")
quit(0)
except:
let error = getCurrentException()
writeLine stderr, error.msg
writeLine stderr, error.getStacktrace()
quit(1)

View File

@ -12,38 +12,52 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# Just Another Test Suite for running JAPL tests
## Just Another Test Suite for running JAPL tests
import nim/nimtests
import testobject
import testutils
import logutils
import testconfig
import testbuilder
import testrun
import testeval
import localization
import os
import strformat
import parseopt
import strutils
import terminal
import re
const jatsVersion = "(dev)"
type
Action {.pure.} = enum
Run, Help, Version
## The action JATS takes.
DebugAction {.pure.} = enum
Interactive, Stdout
QuitValue {.pure.} = enum
Success, Failure, ArgParseErr, InternalErr
## The action JATS takes with the Debug Log output.
QuitValue {.pure.} = enum
Success, Failure, ArgParseErr, Unreachable, Interrupt, JatrNotFound,
UncaughtException
## The enum that specifies what each exit code means
when isMainModule:
# command line option parser
var optparser = initOptParser(commandLineParams())
# variables that define what JATS does
var action: Action = Action.Run
var debugActions: seq[DebugAction]
var targetFiles: seq[string]
var verbose = true
var quitVal = QuitValue.Success
proc evalKey(key: string) =
## Modifies the globals that define what JATS does based on the
## provided key/flag
let key = key.toLower()
if key == "h" or key == "help":
action = Action.Help
@ -62,9 +76,18 @@ when isMainModule:
proc evalKeyVal(key: string, val: string) =
## Modifies the globals that specify what JATS does based on
## the provided key/value pair
let key = key.toLower()
if key == "o" or key == "output":
targetFiles.add(val)
elif key == "j" or key == "jobs":
if val.match(re"^[0-9]*$"):
maxAliveTests = parseInt(val)
else:
echo "Can't parse non-integer option passed to -j/--jobs."
action = Action.Help
quitVal = QuitValue.ArgParseErr
else:
echo &"Unknown option: {key}"
action = Action.Help
@ -72,10 +95,12 @@ when isMainModule:
proc evalArg(key: string) =
## Modifies what JATS does based on a provided argument
echo &"Unexpected argument"
action = Action.Help
quitVal = QuitValue.ArgParseErr
# parse arguments
while true:
optparser.next()
case optparser.kind:
@ -90,6 +115,7 @@ when isMainModule:
proc printUsage =
## Prints JATS usage/help information to the terminal
echo """
JATS - Just Another Test Suite
@ -101,15 +127,17 @@ Flags:
-o:<filename> (or --output:<filename>) saves debug info to a file
-s (or --silent) will disable all output (except --stdout)
--stdout will put all debug info to stdout
-j:<parallel test count> (or --jobs:<parallel test count>) to specify number of tests to run parallel
-h (or --help) displays this help message
-v (or --version) displays the version number of JATS
"""
proc printVersion =
## Prints JATS version information to the terminal
echo &"JATS - Just Another Test Suite version {jatsVersion}"
# execute the action defined. Run is executed below, so not quitting
# runs it.
if action == Action.Help:
printUsage()
quit int(quitVal)
@ -120,42 +148,90 @@ Flags:
discard
else:
echo &"Unknown action {action}, please contact the devs to fix this."
quit int(QuitValue.InternalErr)
quit int(QuitValue.Unreachable)
# action Run
# define globals in logutils
setVerbosity(verbose)
setLogfiles(targetFiles)
# start of JATS
# run the test suite
try:
log(LogLevel.Debug, &"Welcome to JATS")
# the first half of the test suite defined in ~japl/tests/nim
runNimTests()
# the second half of the test suite defined in ~japl/tests/japl
# Find ~japl/tests/japl and the test runner JATR
var jatr = "jatr"
var testDir = "japl"
if not fileExists(jatr) and fileExists("tests" / jatr):
log(LogLevel.Debug, &"Must be in root: prepending \"tests\" to paths")
jatr = "tests" / jatr
testDir = "tests" / testDir
if not fileExists(jatr):
if fileExists("tests" / jatr):
log(LogLevel.Debug,
&"Must be in root: prepending \"tests\" to paths")
jatr = "tests" / jatr
testDir = "tests" / testDir
else:
# only those two dirs are realistically useful for now,
echo "The tests directory couldn't be found."
quit int(QuitValue.JatrNotFound)
# set the global var which specifies the path to the test runner
testRunner = jatr
log(LogLevel.Info, &"Running JAPL tests.")
log(LogLevel.Info, &"Building tests...")
# build tests (see testbuilder.nim)
let tests: seq[Test] = buildTests(testDir)
log(LogLevel.Debug, &"Tests built.")
# define interrupt (only here, because it's a closure over tests, so
# they can be killed)
proc ctrlc() {.noconv.} =
showCursor()
tests.killTests()
echo "Interrupted by ^C."
quit(int(QuitValue.Interrupt))
setControlCHook(ctrlc)
log(LogLevel.Info, &"Running tests...")
# run tests (see testrun.nim)
tests.runTests(jatr)
log(LogLevel.Debug, &"Tests ran.")
log(LogLevel.Debug, &"Evaluating tests...")
# evaluate tests (see testeval.nim)
tests.evalTests()
log(LogLevel.Debug, &"Tests evaluated.")
# print test results (see testeval.nim)
if not tests.printResults():
quitVal = QuitValue.Failure
log(LogLevel.Debug, &"Quitting JATS.")
# special options to view the entire debug log
except FatalError:
# a fatal raised by some code
writeLine stderr, getCurrentExceptionMsg()
quit(int(QuitValue.UncaughtException))
except:
# write the current exception message
writeLine stdout, getCurrentExceptionMessage()
writeLine stdout, getCurrentException().getStackTrace()
quit(int(QuitValue.UncaughtException))
finally:
# Always show logs, even if there's a crash
let logs = getTotalLog()
for action in debugActions:
case action:
of DebugAction.Interactive:
# try to find 'more' and 'less' as pagers
let lessExe = findExe("less", extensions = @[""])
let moreExe = findExe("more", extensions = @[""])
# prioritize 'less' if found, otherwise go for more
# or if both are "" = not found, then inform the lack
# of a recognized terminal pager
var viewer = if lessExe == "": moreExe else: lessExe
if viewer != "":
# more reliable than pipes
writeFile("testresults.txt", logs) # yes, testresults.txt is reserved
discard execShellCmd(viewer & " testresults.txt") # this way because of pipe buffer sizes
removeFile("testresults.txt")

32
tests/localization.nim Normal file
View File

@ -0,0 +1,32 @@
# 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 base64
const language = 1
when language == 1:
const aa* = "ICBfX18gICAgX19fICAgX19fICBfX18gIF9fXyAgX19fICAgICAgIF9fICAgICAgX18gIF9fXyAgICBfX18gICBfX18gIF9fXyAgX19fICBfX18gCiAvIF8gXCAgLyBfIFwgfCBfIFwvIF9ffHxfIF98fCBfX3wgICAgICBcIFwgICAgLyAvIC8gXyBcICAvIF8gXCB8IF8gXC8gX198fF8gX3x8IF9ffAp8IChfKSB8fCAoXykgfHwgIF8vXF9fIFwgfCB8IHwgX3wgICAgICAgIFwgXC9cLyAvIHwgKF8pIHx8IChfKSB8fCAgXy9cX18gXCB8IHwgfCBffCAKIFxfX18vICBcX19fLyB8X3wgIHxfX18vfF9fX3x8X19ffCAgICAgICAgXF8vXF8vICAgXF9fXy8gIFxfX18vIHxffCAgfF9fXy98X19ffHxfX198Cgo="
const bb* = "VXd1IFdlIG1hZGUgYSBmKmNreSB3dWNreSEhIEEgd2l0dGxlIGYqY2tvIGJvaW5nbyE="
const cc* = "VGhlIGNvZGUgbW9ua2V5cyBhdCBvdXIgaGVhZHF1YXJ0ZXJzIGFyZSB3b3JraW5nIFZFV1kgSEFXRCB0byBmaXggdGhpcyEK"
proc getCurrentExceptionMessage*: string =
when language == 1:
echo decode aa
echo decode bb
echo decode cc
return getCurrentExceptionMsg()
else:
echo "Unsupported language."

View File

@ -12,31 +12,67 @@
# See the License for the specific language governing permissions and
# limitations under the License.
## A quick library for writing debug logs, errors, fatals and progress bars
## for the test suite.
##
## Global variables:
##
## totalLog (can be written to with the proc log)
## verbose (can be set with the proc setVerbosity)
## logfiles (can be set with the proc setLogfiles)
##
## The rationale behind all three is that they have one value accross
## one jats process/instance, and they would bloat up every single proc
## signature, because they are needed for the proc log to work.
# logging stuff
import terminal
import strformat
import times
import strutils
import terminal, strformat, times, strutils
type LogLevel* {.pure.} = enum
## All the different possible log levels
Debug, # always written to file only (large outputs, such as the entire output of the failing test or stacktrace)
Info, # important information about the progress of the test suite
Error, # failing tests (printed with red)
Stdout, # always printed to stdout only (for cli experience)
Error, # failing tests (printed with yellow)
Fatal # always printed with red, halts the entire suite (test parsing errors, printed with red)
# log config: which log levels to show, show in silent mode and save to the
# detailed debug logs
const echoedLogs = {LogLevel.Info, LogLevel.Error, LogLevel.Fatal}
const echoedLogsSilent = {LogLevel.Fatal} # will be echoed even if test suite is silent
const savedLogs = {LogLevel.Debug, LogLevel.Info, LogLevel.Error, LogLevel.Fatal}
const echoedLogs = {LogLevel.Info, LogLevel.Error, LogLevel.Stdout}
const echoedLogsSilent = {LogLevel.Error}
const savedLogs = {LogLevel.Debug, LogLevel.Info, LogLevel.Error}
const logColors = [LogLevel.Debug: fgDefault, LogLevel.Info: fgGreen, LogLevel.Error: fgRed, LogLevel.Stdout: fgYellow]
# aesthetic config:
# progress bar length
const progbarLength = 25
# log level colors
const logColors = [LogLevel.Debug: fgDefault, LogLevel.Info: fgGreen,
LogLevel.Error: fgYellow, LogLevel.Fatal: fgRed]
# global vars for the proc log
var totalLog = ""
var verbose = true
var logfiles: seq[string]
# simple interfaces with the globals
proc setVerbosity*(verb: bool) =
## Sets the logging verbosity
verbose = verb
proc getTotalLog*: string =
## Returns all the detailed logs in ever logged in the jats instance
totalLog
proc setLogfiles*(files: seq[string]) =
## Sets files to write logs to
logfiles = files
# main logging command
proc log*(level: LogLevel, msg: string) =
## Adds a line to the total logs/stdout depending on config, together
## with the timestamp
let msg = &"[{$level} - {$getTime()}] {msg}"
if level in savedLogs:
totalLog &= msg & "\n"
@ -50,26 +86,41 @@ proc log*(level: LogLevel, msg: string) =
echo msg
setForegroundColor(fgDefault)
proc getTotalLog*: string =
totalLog
const progbarLength = 25
type FatalError* = ref object of CatchableError
proc fatal*(msg: string) =
## Creates a fatal error, logs it and raises it as an exception
log(LogLevel.Fatal, msg)
let e = new(FatalError)
e.msg = msg
raise e
# progress bar stuff
type Buffer* = ref object
## Represents an updateable line on the terminal
contents: string
previous: string
proc newBuffer*: Buffer =
## Creates a Buffer, hides the cursor
hideCursor()
new(result)
proc updateProgressBar*(buf: Buffer, text: string, total: int, current: int) =
## Fills a buffer with a progress bar with label (text) total cells (total)
## and filled cells (current)
if total <= 0:
return
var newline = ""
newline &= "["
let ratio = current / total
let filledCount = int(ratio * progbarLength)
if filledCount > 0:
newline &= "=".repeat(filledCount)
if filledCount < progbarLength:
if progbarLength - filledCount - 1 > 0:
newline &= " ".repeat(progbarLength - filledCount - 1)
newline &= &"] ({current}/{total}) {text}"
# to avoid process switching during half-written progress bars and whatnot all terminal editing happens at the end
@ -79,7 +130,10 @@ proc updateProgressBar*(buf: Buffer, text: string, total: int, current: int) =
buf.contents = newline
proc clearLineAndWrite(text: string, oldsize: int) =
write stdout, text & "\r"
## writes text to the beginning of the line
# oldsize is there for history, and so that the implementation
# of line clearing is flexible
write stdout, "\r" & text & "\r"
proc render*(buf: Buffer) =
if verbose: #and buf.previous != buf.contents:
@ -87,7 +141,7 @@ proc render*(buf: Buffer) =
buf.previous = buf.contents
proc endBuffer*(buf: Buffer) =
## Ends the existence of a buffer
## restores terminal status for good scrolling experience
showCursor()
proc setLogfiles*(files: seq[string]) =
logfiles = files

View File

@ -1,3 +1,18 @@
# 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.
# TO BE RUN FROM /tests
import random, os, strformat, tables

177
tests/testbuilder.nim Normal file
View File

@ -0,0 +1,177 @@
# 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 sequtils
import strformat
proc parseModalLine(line: string): tuple[modal: bool, mode: string, detail: string, comment: bool] =
# when non modal, mode becomes the line
# when comment is true, it must not do anything to whenever it is exported
let line = line
result.modal = false
result.mode = ""
result.detail = ""
result.comment = false
if line.len() > 0 and line[0] == '[':
if line.len() > 1:
if line[1] == '[':
result.mode = line[1..line.high()]
return result
elif line[1] == ';':
result.comment = true
result.modal = true
return result
result.modal = true
else:
result.mode = line
return result
var colon = false
for i in countup(0, line.high()):
let ch = line[i]
if ch in Letters or ch in Digits or ch in {'_', '-'}:
if colon:
result.detail &= ($ch).toLower()
else:
result.mode &= ($ch).toLower()
elif ch == ':':
if not colon:
colon = true
else:
fatal &"Two colons in <{line}> not allowed."
elif ch in Whitespace:
discard
elif ch == ']':
if i != line.high():
fatal &"] is only allowed to close the line <{line}>."
elif ch == '[':
if i > 0:
fatal &"[ is only allowed to open the modal line <{line}>."
else:
fatal &"Illegal character in <{line}>: {ch}."
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 =
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.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] =
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()
let line = parsed.mode
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] =
for candidateObj in walkDir(testDir):
let candidate = candidateObj.path
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")

23
tests/testconfig.nim Normal file
View File

@ -0,0 +1,23 @@
# 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.
const jatsVersion* = "(dev)"
var maxAliveTests* = 16 # number of tests that can run parallel
const testWait* = 100 # number of milliseconds per cycle
const timeout* = 50 # number of cycles after which a test is killed for timeout
var testRunner* = "jatr"
const outputIgnore* = [ "^DEBUG.*$" ]

60
tests/testeval.nim Normal file
View File

@ -0,0 +1,60 @@
# 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.
# Test object helpers
import testobject
import logutils
import os
import osproc
import streams
import strformat
import testconfig
proc evalTests*(tests: seq[Test]) =
for test in tests:
if test.result == TestResult.ToEval:
test.result = if test.eval(): TestResult.Success else: TestResult.Mismatch
proc printResults*(tests: seq[Test]): bool =
var
skipped = 0
success = 0
fail = 0
crash = 0
killed = 0
for test in tests:
log(LogLevel.Debug, &"Test {test.name}@{test.path} result: {test.result}")
case test.result:
of TestResult.Skip:
inc skipped
of TestResult.Mismatch:
inc fail
log(LogLevel.Debug, &"[{test.name}@{test.path}\nstdout:\n{test.output}\nstderr:\n{test.error}\nexpected stdout:\n{test.expectedOutput}\nexpected stderr:\n{test.expectedError}\n]")
log(LogLevel.Debug, &"\nMismatch pos for stdout: {test.mismatchPos}\nMismatch pos for stderr: {test.errorMismatchPos}")
of TestResult.Crash:
inc crash
log(LogLevel.Debug, &"{test.name}@{test.path} \ncrash:\n{test.error}")
of TestResult.Success:
inc success
of TestResult.Killed:
inc killed
else:
log(LogLevel.Error, &"Probably a testing suite bug: test {test.path} has result {test.result}. Refer to testeval.nim/printResults.")
let finalLevel = if fail == 0 and crash == 0 and killed == 0: LogLevel.Info else: LogLevel.Error
log(finalLevel, &"{tests.len()} tests: {success} succeeded, {skipped} skipped, {fail} failed, {killed} killed, {crash} crashed.")
result = fail == 0 and crash == 0

211
tests/testmarkup.md Normal file
View File

@ -0,0 +1,211 @@
# JATS test markup specification
This document specifies the format tests are written
in for JATS (Just Another Test Suite).
## Definitions
A **test file** represents a file inside the `~japl/tests/japl/` directory. All of these files are parsed
to find any defined tests.
A test file can contain multiple tests. A **test** is a
piece of JAPL source code, along with its input
(given in stdin) and expected output (in stdout and stderr), or the means to construct these fields. It
may also contain other metadata, such as the name
of the test, or whether it is to be skipped.
The test files are parsed line by line. The parser
switches **modes** during the parsing. The modes
dictate the state of the parser, and where each line
ends up (which field of which test).
There is a special mode, **root mode**, which describes
the state of the parser when it just enters the file.
It can stay in this state during parsing, can leave
it by entering another mode and can
return to it, by leaving any mode it has entered.
## Test files
Must be utf-8 (for now only ascii was tested).
Must not contain a BOM. Line endings must be a single
`\n`. Please configure your editor to support this.
## Syntax
### Mode syntax
The modes are constructed from modelines,
which are lines starting with the character '['.
Modelines also have to be closed by a ']' character
on the end of this line. These lines may not contain
whitespace before the opening '[' nor after then ending
']' characters. Inside the brackets, letters (case
insensitive), numbers, underscores and dashes form\
a name describing what the modeline does.
```
[ name ]
```
Optionally, an argument may be passed, which is
separated by a colon.
```
[ name : detail ]
```
Whitespace inside the brackets is ignored (even inside
names). More than one colon, or any character that
is not whitespace, a letter, a digit, a colon, an
underscore or a dash inside is a syntax error, which
results in a fatal error, causing the whole test
file to be invalid. The user is always warned when
such a fatal syntax error occurs.
It is possible for lines beginning with '[' to not
be modelines. When a line starts with '[[', it escapes
the opening left bracket, as if the line was a regular
one (the '[[' is reduced to '['). When a line starts
with '[;' it is a comment, which is not a modeline nor
a line that shows up in the current mode.
A different mode can be entered and left by the
following syntax.
```
[Modename]
[end]
```
A modeline that is not defined to be a legal one
for the current mode is a syntax error, which
invalidates the whole test file. It also raises
a visible warning to the user.
## Possible modes
### Root mode
Inside the root mode, all lines that are not modelines
are assumed to be comments.
There is one possible mode to enter from the root mode,
a test mode. The test modes are entered when the "test"
mode line is specified. The detail for the modeline
corresponds to the name of the test.
```
[test: testname]
[end]
```
### Test modes
Inside test modes, all lines that are not modelines are
assumed to be comments.
There are different modelines that do actions or
modes that can be entered from tests. They are all
defined below.
#### Skipping a test
The modeline `skip` skips a test. It does not enter
a different mode, so no `end` is neccessary.
```
[skip]
```
#### Adding JAPL source to a test
The modeline `source` enters the mode source, which
is useful for appending to the JAPL source of the
test.
```
[source]
print("Hello from JAPL!");
[end]
```
There are two kinds of source modes, raw and mixed.
Mixed source mode can be entered if the detail `mixed`
is specified. Raw source mode can be entered if the
detail `raw` is specified. When no detail is specified,
raw source mode is entered.
In raw source mode, all lines in the mode are
appended as they are to the JAPL source. In mixed
mode, comments inside this JAPL source can be
added to add lines to the expected stdout/stderr or
the stdin of the test using the legacy test format.
They are defined by the sequences `//stdout:`,
`//stderr:`, `//stdin:`, `//stdoutre:` and
`//stderrre:`. Every character after the colon and
before the end of the line is appended to the respective
field of the test. `stdout` adds a raw line to be
matched to the expected stdout of the test. `stdoutre`
adds a regex to match a line of the stdout of the test.
`stderr` and `stderrre` are the stderr equivalents.
`stdin` adds a line to the stdin that the JAPL source
can read from.
```
[source: mixed]
print("Hello from JAPL!");//stdout:Hello from JAPL!
[end]
```
#### Adding expected output to the test
The mode `stdout` can add standard output to expect
from the JAPL source when it is ran.
```
[test: hello]
[source: raw]
print("Banana.");
[end]
[stdout]
Banana.
[end]
[end]
```
The option `re` can be added if every line is to
be a regex matched against a line of stdout.
The option `nw` will strip leading and trailing
whitespace from every line in the mode before
adding it to the expected lines. The option `nwre`
adds regex based matching lines after stripping
whitespace.
The mode `stderr` does the same as `stdout`, but
for the standard error. It accepts the same options.
#### Adding input to the test
The mode `stdin` can add standard input that the
JAPL source of the test can read from.
```
[test: inputtest]
[source: raw]
print(readLine());
[end]
[stdin]
Hello there
[end]
[stdout]
Hello there
[end]
```
#### Adding python to the tests
Coming soon.

View File

@ -14,46 +14,212 @@
# Test object and helpers
import re, strutils, osproc
import testconfig
import re
import strutils
import osproc
import streams
import strutils
# types
type
TestResult* {.pure.} = enum
Unstarted, Running, ToEval, Success, Skip, Mismatch, Crash
Unstarted, Running, ToEval, Success, Skip, Mismatch, Crash, Killed
ExpectedLineKind* {.pure.} = enum
Raw, Regex
ExpectedLine* = object
kind*: ExpectedLineKind
content*: string
Test* = ref object
result*: TestResult
# test origins
source*: string
path*: string
expectedOutput*: string
expectedError*: string
name*: string
# generated after building
expectedOutput*: seq[ExpectedLine]
expectedError*: seq[ExpectedLine]
input*: string
# during running/output of running
output*: string
error*: string
process*: Process
cycles*: int
# after evaluation
result*: TestResult
mismatchPos*: int # only for result mismatch
errorMismatchPos*: int # same but for stderr
# parsing the test notation
# Helpers for building tests:
proc compileExpectedOutput*(source: string): string =
proc genEL(content: string, kind: ExpectedLineKind): ExpectedLine =
ExpectedLine(kind: kind, content: content)
proc compileExpectedOutput(source: string, rawkw: string, rekw: string): seq[ExpectedLine] =
for line in source.split('\n'):
if line =~ re"^.*//stdout:[ ]?(.*)$":
if line =~ re("^.*//" & rawkw & ":(.*)$"):
result &= genEL(matches[0], ExpectedLineKind.Raw)
elif line =~ re("^.*//" & rekw & ":(.*)$"):
result &= genEL(matches[0], ExpectedLineKind.Regex)
proc compileExpectedOutput(source: string): seq[ExpectedLine] =
compileExpectedOutput(source, "stdout", "stdoutre")
proc compileExpectedError(source: string): seq[ExpectedLine] =
compileExpectedOutput(source, "stderr", "stderrre")
proc compileInput(source: string): string =
for line in source.split('\n'):
if line =~ re"^.*//stdin:(.*)$":
result &= matches[0] & "\n"
proc parseMixed*(test: Test, source: string) =
test.source &= source
test.expectedOutput = compileExpectedOutput(source)
test.expectedError = compileExpectedError(source)
test.input = compileInput(source)
proc compileExpectedError*(source: string): string =
proc parseSource*(test: Test, source: string) =
test.source &= source
proc parseStdin*(test: Test, source: string) =
test.input &= source
proc parseStdout*(test: Test, source: string, re: bool = false, nw: bool = false, err: bool = false) =
var kind = ExpectedLineKind.Raw
if re:
kind = ExpectedLineKind.Regex
for line in source.split('\n'):
if line =~ re"^.*//stderr:[ ]?(.*)$":
result &= matches[0] & "\n"
var toAdd = line
if nw:
toAdd = toAdd.strip()
if err:
test.expectedError.add(genEL(toAdd, kind))
else:
test.expectedOutput.add(genEL(toAdd, kind))
proc compileInput*(source: string): string =
for line in source.split('\n'):
if line =~ re"^.*//stdin:[ ]?(.*)$":
result &= matches[0] & "\n"
if err:
while test.expectedError.len() > 0 and test.expectedError[test.expectedError.high()].content == "":
discard test.expectedError.pop()
else:
while test.expectedOutput.len() > 0 and test.expectedOutput[test.expectedOutput.high()].content == "":
discard test.expectedOutput.pop()
proc parseStderr*(test: Test, source: string, regex: bool = false) =
parseStdout(test, source, regex, true)
# stuff for cleaning test output
proc parsePython*(test: Test, source: string) =
discard # TODO
proc tuStrip*(input: string): string =
return input.replace(re"[\n\r]*$", "")
proc newTest*(name: string, path: string): Test =
new(result)
result.result = TestResult.Unstarted
result.path = path
result.name = name
result.mismatchPos = -1
result.errorMismatchPos = -1
proc skip*(test: Test) =
test.result = TestResult.Skip
# Expected line displayer
proc `$`*(el: ExpectedLine): string =
case el.kind:
of ExpectedLineKind.Raw:
result &= "raw \""
of ExpectedLineKind.Regex:
result &= "regex \""
result &= el.content & "\""
proc `$`*(els: seq[ExpectedLine]): string =
for el in els:
result &= $el & "\n"
# Helpers for running tests
proc start*(test: Test) =
test.process = startProcess(testRunner, options = {})
test.process.inputStream.write(test.source & $char(4) & test.input)
test.process.inputStream.close() # this is advised against in the stdlib, but this is what gets the job
# done. (Yes I tried flushing)
test.result = TestResult.Running
proc finish*(test: Test) =
# only call when the process has ended execution gracefully. Don't call after closing.
# Don't call while it's running.
test.output = test.process.outputStream.readAll()
test.error = test.process.errorStream.readAll()
if test.process.peekExitCode() == 0:
test.result = TestResult.ToEval # also means "finished running" with a zero exit code
else:
test.result = TestResult.Crash # also means "finished running" with a non-zero exit code
test.process.close()
proc kill*(test: Test) =
# alternative to finish
test.process.kill()
discard test.process.waitForExit()
test.result = TestResult.Killed
proc running*(test: Test): bool =
test.result == TestResult.Running and test.process.running()
# Helpers for evaluating tests
proc stdStrip(input: string): seq[string] =
var lines: seq[string]
for line in input.split('\n'):
var included = true
for pattern in outputIgnore:
if line.match(re(pattern)):
included = false
if included:
lines.add(line)
while lines.len() > 0 and lines[lines.high()] == "":
discard lines.pop()
lines
proc eval*(test: Test): bool =
let
outputLines = test.output.stdStrip()
errorLines = test.error.stdStrip()
# just for updated debug output
test.output = outputLines.join("\n")
test.error = errorLines.join("\n")
if test.expectedOutput.len() != outputLines.len():
test.mismatchPos = outputLines.len()
return false
if test.expectedError.len() != errorLines.len():
test.errorMismatchPos = errorLines.len()
return false
for i in countup(0, test.expectedOutput.high()):
let line = test.expectedOutput[i]
case line.kind:
of ExpectedLineKind.Raw:
if line.content != outputLines[i]:
test.mismatchPos = i
return false
of ExpectedLineKind.Regex:
if not outputLines[i].match(re(line.content)):
test.mismatchPos = i
return false
for i in countup(0, test.expectedError.high()):
let line = test.expectedError[i]
case line.kind:
of ExpectedLineKind.Raw:
if line.content != errorLines[i]:
test.errorMismatchPos = i
return false
of ExpectedLineKind.Regex:
if not errorLines[i].match(re(line.content)):
test.errorMismatchPos = i
return false
return true

80
tests/testrun.nim Normal file
View File

@ -0,0 +1,80 @@
# 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.
# Test runner supervisor/manager
import testobject
import logutils
import testconfig
import strformat
import os
proc runTest(test: Test) =
log(LogLevel.Debug, &"Starting test {test.path}.")
test.start()
proc tryFinishTest(test: Test): bool =
if test.running():
return false
test.finish()
log(LogLevel.Debug, &"Test {test.path} finished.")
return true
proc killTest(test: Test) =
if test.running():
test.kill()
log(LogLevel.Error, &"Test {test.path} was killed for taking too long.")
proc killTests*(tests: seq[Test]) =
for test in tests:
if test.running():
test.kill()
proc runTests*(tests: seq[Test], runner: string) =
var
aliveTests = 0
currentTest = 0
finishedTests = 0
buffer = newBuffer()
let totalTests = tests.len()
buffer.updateProgressBar(&"", totalTests, finishedTests)
buffer.render()
while aliveTests > 0 or currentTest < tests.len():
buffer.render()
sleep(testWait)
if aliveTests < maxAliveTests and currentTest < tests.len():
if tests[currentTest].result == TestResult.Unstarted:
tests[currentTest].runTest()
inc aliveTests
inc currentTest
else:
inc currentTest
inc finishedTests
for i in countup(0, min(currentTest, tests.high())):
if tests[i].result == TestResult.Running:
if tryFinishTest(tests[i]):
inc finishedTests
buffer.updateProgressBar(&"Finished {tests[i].path}.", totalTests, finishedTests)
dec aliveTests
elif tests[i].cycles >= timeout:
tests[i].killTest()
inc finishedTests
dec aliveTests
buffer.updateProgressBar(&"Killed {tests[i].path}.", totalTests, finishedTests)
else:
inc tests[i].cycles
buffer.render()
buffer.endBuffer()

View File

@ -1,169 +0,0 @@
# 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.
# Test object helpers
import testobject, logutils, os, osproc, streams, strformat
# Tests that represent not-yet implemented behaviour
const exceptions = ["all.jpl", "for_with_function.jpl", "runtime_interning.jpl"]
# TODO: for_with_function.jpl should already be implemented, check on it
proc buildTest(path: string): Test =
log(LogLevel.Debug, &"Building test {path}")
let source = readFile(path)
result = Test(
path: path,
result: if path.extractFilename in exceptions: TestResult.Skip
else: TestResult.Unstarted,
expectedOutput: compileExpectedOutput(source),
expectedError: compileExpectedError(source),
input: compileInput(source)
)
proc buildTests*(testDir: string): seq[Test] =
for candidateObj in walkDir(testDir):
let candidate = candidateObj.path
if dirExists(candidate):
log(LogLevel.Debug, &"Descending into dir {candidate}")
result &= buildTests(candidate)
else:
result.add buildTest(candidate)
proc runTest(test: Test, runner: string) =
log(LogLevel.Debug, &"Starting test {test.path}.")
let process = startProcess(runner, args = @[test.path])
test.process = process
if test.input.len() > 0:
var f: File
let suc = f.open(process.inputHandle, fmWrite)
if suc:
f.write(test.input)
else:
log(LogLevel.Error, &"Stdin File handle could not be opened for test {test.path}")
test.result = Crash
test.result = TestResult.Running
proc tryFinishTest(test: Test): bool =
if test.process.running():
return false
test.output = test.process.outputStream.readAll()
test.error = test.process.errorStream.readAll()
if test.process.peekExitCode() == 0:
test.result = TestResult.ToEval
else:
test.result = TestResult.Crash
test.process.close()
log(LogLevel.Debug, &"Test {test.path} finished.")
return true
proc killTest(test: Test) =
if test.process.running():
test.process.kill()
discard test.process.waitForExit()
log(LogLevel.Error, &"Test {test.path} was killed for taking too long.")
discard test.tryFinishTest()
const maxAliveTests = 16
const testWait = 100
const timeout = 100 # number of cycles after which a test is killed for timeout
proc runTests*(tests: seq[Test], runner: string) =
var
aliveTests = 0
currentTest = 0
finishedTests = 0
buffer = newBuffer()
let totalTests = tests.len()
buffer.updateProgressBar(&"", totalTests, finishedTests)
buffer.render()
while aliveTests > 0 or currentTest < tests.len():
buffer.render()
sleep(testWait)
if aliveTests < maxAliveTests and currentTest < tests.len():
if tests[currentTest].result == TestResult.Unstarted:
tests[currentTest].runTest(runner)
inc aliveTests
inc currentTest
else:
inc currentTest
inc finishedTests
for i in countup(0, min(currentTest, tests.high())):
if tests[i].result == TestResult.Running:
if tryFinishTest(tests[i]):
inc finishedTests
buffer.updateProgressBar(&"Finished {tests[i].path}.", totalTests, finishedTests)
dec aliveTests
elif tests[i].cycles >= timeout:
tests[i].killTest()
inc finishedTests
dec aliveTests
buffer.updateProgressBar(&"Killed {tests[i].path}.", totalTests, finishedTests)
else:
inc tests[i].cycles
buffer.render()
buffer.endBuffer()
proc evalTest(test: Test) =
test.output = test.output.tuStrip()
test.error = test.error.tuStrip()
test.expectedOutput = test.expectedOutput.tuStrip()
test.expectedError = test.expectedError.tuStrip()
if test.output != test.expectedOutput or test.error != test.expectedError:
test.result = TestResult.Mismatch
else:
test.result = TestResult.Success
proc evalTests*(tests: seq[Test]) =
for test in tests:
if test.result == TestResult.ToEval:
evalTest(test)
proc printResults*(tests: seq[Test]): bool =
var
skipped = 0
success = 0
fail = 0
crash = 0
for test in tests:
log(LogLevel.Debug, &"Test {test.path} result: {test.result}")
case test.result:
of TestResult.Skip:
inc skipped
of TestResult.Mismatch:
inc fail
log(LogLevel.Debug, &"[{test.path}\noutput:\n{test.output}\nerror:\n{test.error}\nexpected output:\n{test.expectedOutput}\nexpectedError:\n{test.expectedError}\n]")
of TestResult.Crash:
inc crash
log(LogLevel.Debug, &"{test.path} \ncrash:\n{test.error}")
of TestResult.Success:
inc success
else:
log(LogLevel.Error, &"Probably a testing suite bug: test {test.path} has result {test.result}")
let finalLevel = if fail == 0 and crash == 0: LogLevel.Info else: LogLevel.Error
log(finalLevel, &"{tests.len()} tests: {success} succeeded, {skipped} skipped, {fail} failed, {crash} crashed.")
result = fail == 0 and crash == 0