mirror of https://github.com/japl-lang/japl.git
commit
a5371ea25b
|
@ -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
|
||||
|
|
|
@ -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}' ===="
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
print("Hello, world.");
|
||||
//stdout:Hello, world.
|
||||
|
||||
//stdout:
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
|
@ -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]
|
||||
|
||||
|
|
@ -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]
|
|
@ -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]
|
|
@ -0,0 +1,7 @@
|
|||
[test: skipped]
|
||||
[skip]
|
||||
|
||||
[stdout]
|
||||
Hello this text won't be matched.
|
||||
[end]
|
||||
[end]
|
|
@ -1,5 +1,9 @@
|
|||
[Test: nan]
|
||||
[source: raw]
|
||||
print((5/0)*0);
|
||||
[end]
|
||||
[stdout]
|
||||
nan
|
||||
[end]
|
||||
[end]
|
||||
|
||||
//stdout:nan
|
||||
|
||||
//stdout:
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
106
tests/jats.nim
106
tests/jats.nim
|
@ -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")
|
||||
|
|
|
@ -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."
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
@ -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.*$" ]
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue