New edition of testing script

This commit is contained in:
Robert Bendun 2022-09-25 03:19:00 +02:00
parent 5a7c204ceb
commit 548b29d897
8 changed files with 329 additions and 155 deletions

View File

@ -188,8 +188,8 @@ static Result<Value> builtin_range(Interpreter&, std::vector<Value> args)
array.elements.push_back(start);
}
} else {
for (; start < stop; start += step) {
array.elements.push_back(stop - start - Number(1));
for (; stop > start; stop -= step) {
array.elements.push_back(stop - Number(1));
}
}
return array;

View File

@ -0,0 +1,14 @@
say (false and false);
say (false and true);
say (true and false);
say (true and true);
-- Test value preservation
say (0 and 5);
say (1 and 5);
say (false and 4);
say (true and 4);
-- Test lazy evaluation
(say 32; false) and (say 42);
(say 32; true) and (say 42);

View File

@ -0,0 +1,13 @@
say (false or false);
say (false or true);
say (true or false);
say (true or true);
-- Test value preservation
say (0 or 1);
say (0 or 0);
say (4 or 3);
-- Test lazy evaluation
(say 42; false) or say 10;
(say 42; true) or say 10;

View File

@ -0,0 +1,33 @@
-- 4! = 24 permutations
say (permute 0 1 2 3);
say (permute 0 1 3 2);
say (permute 0 2 1 3);
say (permute 0 2 3 1);
say (permute 0 3 1 2);
say (permute 0 3 2 1);
say (permute 1 0 2 3);
say (permute 1 0 3 2);
say (permute 1 2 0 3);
say (permute 1 2 3 0);
say (permute 1 3 0 2);
say (permute 1 3 2 0);
say (permute 2 0 1 3);
say (permute 2 0 3 1);
say (permute 2 1 0 3);
say (permute 2 1 3 0);
say (permute 2 3 0 1);
say (permute 2 3 1 0);
say (permute 3 0 1 2);
say (permute 3 0 2 1);
say (permute 3 1 0 2);
say (permute 3 1 2 0);
say (permute 3 2 0 1);
say (permute 3 2 1 0);
-- This array should be flattened to support case (permute array)
say (permute 3 2 [1; 0]);
-- Test if nested arrays are preserved
say (permute [[3; 2]; 4] [1; 0]);
say (permute [0; 1; 4; [3; 2]]);

View File

@ -0,0 +1,17 @@
say (range 0);
say (range (0 - 1));
say (range 10);
say (range 1 10);
say (range 1 10 2);
say (up 0);
say (up (0 - 1));
say (up 10);
say (up 1 10);
say (up 1 10 2);
say (down 0);
say (down (0 - 1));
say (down 10);
say (down 1 10);
say (down 1 10 2);

View File

@ -0,0 +1,107 @@
[
{
"name": "boolean",
"cases": [
{
"name": "logical_or.mq",
"exit_code": 0,
"stdin_lines": [],
"stdout_lines": [
"false",
"true",
"true",
"true",
"1",
"0",
"4",
"42",
"10",
"42"
],
"stderr_lines": []
},
{
"name": "logical_and.mq",
"exit_code": 0,
"stdin_lines": [],
"stdout_lines": [
"false",
"false",
"false",
"true",
"0",
"5",
"false",
"4",
"32",
"32",
"42"
],
"stderr_lines": []
}
]
},
{
"name": "builtin",
"cases": [
{
"name": "permute.mq",
"exit_code": 0,
"stdin_lines": [],
"stdout_lines": [
"[0; 1; 3; 2]",
"[0; 2; 1; 3]",
"[0; 2; 3; 1]",
"[0; 3; 1; 2]",
"[0; 3; 2; 1]",
"[1; 0; 2; 3]",
"[1; 0; 3; 2]",
"[1; 2; 0; 3]",
"[1; 2; 3; 0]",
"[1; 3; 0; 2]",
"[1; 3; 2; 0]",
"[2; 0; 1; 3]",
"[2; 0; 3; 1]",
"[2; 1; 0; 3]",
"[2; 1; 3; 0]",
"[2; 3; 0; 1]",
"[2; 3; 1; 0]",
"[3; 0; 1; 2]",
"[3; 0; 2; 1]",
"[3; 1; 0; 2]",
"[3; 1; 2; 0]",
"[3; 2; 0; 1]",
"[3; 2; 1; 0]",
"[0; 1; 2; 3]",
"[0; 1; 2; 3]",
"[0; 1; 4; [3; 2]]",
"[0; 4; [3; 2]; 1]"
],
"stderr_lines": []
},
{
"name": "range.mq",
"exit_code": 0,
"stdin_lines": [],
"stdout_lines": [
"[]",
"[]",
"[0; 1; 2; 3; 4; 5; 6; 7; 8; 9]",
"[1; 2; 3; 4; 5; 6; 7; 8; 9]",
"[1; 3; 5; 7; 9]",
"[]",
"[]",
"[0; 1; 2; 3; 4; 5; 6; 7; 8; 9]",
"[1; 2; 3; 4; 5; 6; 7; 8; 9]",
"[1; 3; 5; 7; 9]",
"[]",
"[]",
"[9; 8; 7; 6; 5; 4; 3; 2; 1; 0]",
"[9; 8; 7; 6; 5; 4; 3; 2; 1]",
"[9; 7; 5; 3; 1]"
],
"stderr_lines": []
}
]
}
]

View File

@ -1,3 +1,3 @@
test: bin/debug/musique
scripts/test.py test examples
python3 scripts/test.py

292
scripts/test.py Executable file → Normal file
View File

@ -1,170 +1,160 @@
#!/usr/bin/env python3
from dataclasses import dataclass
from glob import glob
from sys import argv
from sys import exit
import os.path
import shlex
import subprocess
import argparse
import dataclasses
import json
from unittest import case
import os
import subprocess
Interpreter = "bin/musique"
TEST_DB = "test_db.json"
INTERPRETER = "bin/debug/musique"
def directories_in_path(path: str):
dirs = []
while True:
dirname, _ = os.path.split(path)
if not dirname:
break
dirs.append(dirname)
path = dirname
if path == "/":
break
dirs.reverse()
return dirs
@dataclasses.dataclass
class Result:
exit_code: int = 0
stdin_lines: list[str] = dataclasses.field(default_factory=list)
stdout_lines: list[str] = dataclasses.field(default_factory=list)
stderr_lines: list[str] = dataclasses.field(default_factory=list)
def mkdir_recursive(path: str):
for directory in directories_in_path(path):
try:
os.mkdir(directory)
except FileExistsError:
@dataclasses.dataclass
class TestCase:
name: str
exit_code: int = 0
stdin_lines: list[str] = dataclasses.field(default_factory=list)
stdout_lines: list[str] = dataclasses.field(default_factory=list)
stderr_lines: list[str] = dataclasses.field(default_factory=list)
def run(self, interpreter: str, source: str, cwd: str):
result = subprocess.run(
args=[interpreter, source],
capture_output=True,
cwd=cwd,
text=True
)
return Result(
exit_code=result.returncode,
stdout_lines=result.stdout.splitlines(keepends=False),
stderr_lines=result.stderr.splitlines(keepends=False)
)
def record(self, interpreter: str, source: str, cwd: str):
print(f"Recording case {self.name}")
result = self.run(interpreter, source, cwd)
changes = []
if self.exit_code != result.exit_code: changes.append("exit code")
if self.stderr_lines != result.stderr_lines: changes.append("stderr")
if self.stdout_lines != result.stdout_lines: changes.append("stdout")
if changes:
print(f" changed: {', '.join(changes)}")
self.exit_code, self.stderr_lines, self.stdout_lines = result.exit_code, result.stderr_lines, result.stdout_lines
def test(self, interpreter: str, source: str, cwd: str):
print(f" Testing case {self.name} ", end="")
result = self.run(interpreter, source, cwd)
if self.exit_code == result.exit_code and self.stdout_lines == result.stdout_lines and self.stderr_lines == result.stderr_lines:
print("ok")
return True
print(f"FAILED")
print(f"File: {source}")
if self.exit_code != result.exit_code:
print(f"Different exit code - expected {self.exit_code}, got {result.exit_code}")
for name, expected, actual in [
("standard output", self.stdout_lines, result.stdout_lines),
("standard error", self.stderr_lines, result.stderr_lines)
]:
if expected == actual:
continue
@dataclass
class Test_Case:
returncode = 0
diff_line = None
for i, (exp_line, got_line) in enumerate(zip(expected, actual)):
if exp_line != got_line:
diff_line = i
break
@staticmethod
def from_file(fname : str):
with open(fname) as f:
content = json.load(f)
tc = Test_Case()
for name in content:
# assert hasattr(tc, name), "Test_Case does not have attribute %s" % (name,)
setattr(tc, name, content[name])
return tc
@staticmethod
def from_run(run, flags=[]):
tc = Test_Case()
for attr in ["returncode", "stdout", "stderr"]: ### TODO FLAGS
try:
run_attr = getattr(run, attr).decode()
except (UnicodeDecodeError, AttributeError):
run_attr = getattr(run, attr)
setattr(tc, attr, run_attr)
setattr(tc, "flags", flags)
return tc
def save(self, fname : str):
j = {}
for attr in ["returncode", "stdout", "stderr", "flags"]:
j[attr] = getattr(self, attr)
mkdir_recursive(fname)
with open(fname, 'w') as f:
json.dump(j, f, indent=4)
def cmd_run_echoed(cmd, **kwargs):
print("[CMD] %s" % " ".join(map(shlex.quote, cmd)))
return subprocess.run(cmd, **kwargs)
def find_path_for_test_case(path: str) -> str:
directory, filename = os.path.split(path)
return (directory if directory else ".") + "/.tests_cache/" + filename + ".json"
def run_tests(file_paths: list):
return_code = 0
for program_file in file_paths:
test_case_file = find_path_for_test_case(program_file)
if os.path.exists(test_case_file):
tc = Test_Case.from_file(test_case_file)
if diff_line is not None:
print(f"First difference at line {diff_line+1} in {name}:")
print(f" Expected: {expected[diff_line]}")
print(f" Got: {actual[diff_line]}")
elif len(expected) > len(actual):
print(f"Expected {name} is {len(expected) - len(actual)} lines longer then actual")
else:
continue
print(f"Actual {name} is {len(actual) - len(expected)} lines longer then expected")
flags_list = [Interpreter]
if hasattr(tc, "flags"):
flags_list.extend(tc.flags)
flags_list.append(program_file)
res = cmd_run_echoed(flags_list, capture_output=True)
for attr in [a for a in dir(tc) if a in ["returncode", "stdout", "stderr"]]:
tc_attr = getattr(tc, attr)
res_attr = getattr(res, attr)
try:
res_attr = res_attr.decode()
except (UnicodeDecodeError, AttributeError):
pass
if tc_attr != res_attr:
print(f"[ERROR] Failed test {program_file}")
print(f"Expected {attr} = ")
print(tc_attr)
print(f"Received {attr} = ")
print(res_attr)
return_code = 1
exit(return_code)
def record_tests(file_paths: list):
for program_file in file_paths:
test_case_file = find_path_for_test_case(program_file)
res = cmd_run_echoed([Interpreter, program_file], capture_output=True)
tc = Test_Case.from_run(res, [])
tc.save(test_case_file)
def add_tests(file_paths: list):
to_be_added = []
for program_file in file_paths:
test_case_file = find_path_for_test_case(program_file)
if not os.path.exists(test_case_file):
print(f"Add test {program_file}? (yes/no)")
if "yes".startswith(input().strip().lower()):
to_be_added.append(program_file)
record_tests(to_be_added)
return False
# list of files to test
def main():
file_paths, mode = [], run_tests
@dataclasses.dataclass
class TestSuite:
name: str
cases: list[TestCase] = dataclasses.field(default_factory=list)
if len(argv) < 2:
print("[ERROR] Expected mode argument (either 'record', 'add' or 'test')")
exit(1)
suites = list[TestSuite]()
if argv[1] == "test":
mode = run_tests
elif argv[1] == "record":
mode = record_tests
elif argv[1] == "add":
mode = add_tests
else:
print(f"[ERROR] Unrecognized mode '{argv[1]}'")
exit(1)
def traverse(discover: bool, update: bool):
to_record = list[tuple[TestSuite, TestCase]]()
if discover:
for suite_name in os.listdir(testing_dir):
if os.path.isdir(os.path.join(testing_dir, suite_name)) and suite_name not in (suite.name for suite in suites):
print(f"Discovered new test suite: {suite_name}")
suites.append(TestSuite(name=suite_name))
if len(argv) < 3:
print("[ERROR] Expected test case")
exit(1)
for suite in suites:
suite_path = os.path.join(testing_dir, suite.name)
for case_name in os.listdir(suite_path):
if os.path.isfile(os.path.join(suite_path, case_name)) and case_name not in (case.name for case in suite.cases):
print(f"In suite '{suite.name}' discovered new test case: {case_name}")
case = TestCase(name=case_name)
suite.cases.append(case)
to_record.append((suite, case))
interpreter = os.getenv("INTERPRETER")
if interpreter:
Interpreter = interpreter
if update:
to_record.extend(((suite, case) for suite in suites for case in suite.cases))
for path in argv[2:]:
if os.path.exists(path):
if os.path.isdir(path):
file_paths.extend(glob(f"{path}/*.mq"))
else:
file_paths.append(path)
elif mode == add_tests:
print("Adding: " + path)
for (suite, case) in to_record:
case.record(
interpreter=os.path.join(root, INTERPRETER),
source=os.path.join(testing_dir, suite.name, case.name),
cwd=root
)
mode(file_paths)
with open(test_db_path, "w") as f:
json_suites = [dataclasses.asdict(suite) for suite in suites]
json.dump(json_suites, f, indent=2)
def test():
successful, total = 0, 0
for suite in suites:
print(f"Testing suite {suite.name}")
for case in suite.cases:
successful += int(case.test(
interpreter=os.path.join(root, INTERPRETER),
source=os.path.join(testing_dir, suite.name, case.name),
cwd=root
))
total += 1
print(f"Passed {successful} out of {total} ({100 * successful // total}%)")
if __name__ == "__main__":
main()
parser = argparse.ArgumentParser(description="Regression test runner for Musique programming language")
parser.add_argument("-d", "--discover", action="store_true", help="Discover all tests that are not in testing database")
parser.add_argument("-u", "--update", action="store_true", help="Update all tests")
args = parser.parse_args()
root = os.path.dirname(os.path.dirname(__file__))
testing_dir = os.path.join(root, "regression-tests")
test_db_path = os.path.join(testing_dir, TEST_DB)
with open(test_db_path, "r") as f:
for src in json.load(f):
src["cases"] = [TestCase(**case) for case in src["cases"]]
suites.append(TestSuite(**src))
if args.discover or args.update:
traverse(discover=args.discover, update=args.update)
else:
test()