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); array.elements.push_back(start);
} }
} else { } else {
for (; start < stop; start += step) { for (; stop > start; stop -= step) {
array.elements.push_back(stop - start - Number(1)); array.elements.push_back(stop - Number(1));
} }
} }
return array; 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 test: bin/debug/musique
scripts/test.py test examples python3 scripts/test.py

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

@ -1,170 +1,160 @@
#!/usr/bin/env python3 import argparse
from dataclasses import dataclass import dataclasses
from glob import glob
from sys import argv
from sys import exit
import os.path
import shlex
import subprocess
import json 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): @dataclasses.dataclass
dirs = [] class Result:
while True: exit_code: int = 0
dirname, _ = os.path.split(path) stdin_lines: list[str] = dataclasses.field(default_factory=list)
if not dirname: stdout_lines: list[str] = dataclasses.field(default_factory=list)
break stderr_lines: list[str] = dataclasses.field(default_factory=list)
dirs.append(dirname)
path = dirname
if path == "/":
break
dirs.reverse()
return dirs
def mkdir_recursive(path: str): @dataclasses.dataclass
for directory in directories_in_path(path): class TestCase:
try: name: str
os.mkdir(directory) exit_code: int = 0
except FileExistsError: stdin_lines: list[str] = dataclasses.field(default_factory=list)
continue stdout_lines: list[str] = dataclasses.field(default_factory=list)
stderr_lines: list[str] = dataclasses.field(default_factory=list)
@dataclass def run(self, interpreter: str, source: str, cwd: str):
class Test_Case: result = subprocess.run(
returncode = 0 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)
)
@staticmethod def record(self, interpreter: str, source: str, cwd: str):
def from_file(fname : str): print(f"Recording case {self.name}")
with open(fname) as f: result = self.run(interpreter, source, cwd)
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 changes = []
def from_run(run, flags=[]): if self.exit_code != result.exit_code: changes.append("exit code")
tc = Test_Case() if self.stderr_lines != result.stderr_lines: changes.append("stderr")
for attr in ["returncode", "stdout", "stderr"]: ### TODO FLAGS if self.stdout_lines != result.stdout_lines: changes.append("stdout")
try: if changes:
run_attr = getattr(run, attr).decode() print(f" changed: {', '.join(changes)}")
except (UnicodeDecodeError, AttributeError):
run_attr = getattr(run, attr)
setattr(tc, attr, run_attr) self.exit_code, self.stderr_lines, self.stdout_lines = result.exit_code, result.stderr_lines, result.stdout_lines
setattr(tc, "flags", flags)
return tc
def save(self, fname : str): def test(self, interpreter: str, source: str, cwd: str):
j = {} print(f" Testing case {self.name} ", end="")
for attr in ["returncode", "stdout", "stderr", "flags"]: result = self.run(interpreter, source, cwd)
j[attr] = getattr(self, attr) 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
mkdir_recursive(fname) print(f"FAILED")
with open(fname, 'w') as f: print(f"File: {source}")
json.dump(j, f, indent=4)
def cmd_run_echoed(cmd, **kwargs): if self.exit_code != result.exit_code:
print("[CMD] %s" % " ".join(map(shlex.quote, cmd))) print(f"Different exit code - expected {self.exit_code}, got {result.exit_code}")
return subprocess.run(cmd, **kwargs)
def find_path_for_test_case(path: str) -> str: for name, expected, actual in [
directory, filename = os.path.split(path) ("standard output", self.stdout_lines, result.stdout_lines),
return (directory if directory else ".") + "/.tests_cache/" + filename + ".json" ("standard error", self.stderr_lines, result.stderr_lines)
]:
if expected == actual:
continue
def run_tests(file_paths: list): diff_line = None
return_code = 0 for i, (exp_line, got_line) in enumerate(zip(expected, actual)):
for program_file in file_paths: if exp_line != got_line:
test_case_file = find_path_for_test_case(program_file) diff_line = i
if os.path.exists(test_case_file): break
tc = Test_Case.from_file(test_case_file)
else:
continue
flags_list = [Interpreter] if diff_line is not None:
if hasattr(tc, "flags"): print(f"First difference at line {diff_line+1} in {name}:")
flags_list.extend(tc.flags) print(f" Expected: {expected[diff_line]}")
flags_list.append(program_file) print(f" Got: {actual[diff_line]}")
elif len(expected) > len(actual):
res = cmd_run_echoed(flags_list, capture_output=True) print(f"Expected {name} is {len(expected) - len(actual)} lines longer then actual")
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)
# list of files to test
def main():
file_paths, mode = [], run_tests
if len(argv) < 2:
print("[ERROR] Expected mode argument (either 'record', 'add' or 'test')")
exit(1)
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)
if len(argv) < 3:
print("[ERROR] Expected test case")
exit(1)
interpreter = os.getenv("INTERPRETER")
if interpreter:
Interpreter = interpreter
for path in argv[2:]:
if os.path.exists(path):
if os.path.isdir(path):
file_paths.extend(glob(f"{path}/*.mq"))
else: else:
file_paths.append(path) print(f"Actual {name} is {len(actual) - len(expected)} lines longer then expected")
elif mode == add_tests:
print("Adding: " + path)
mode(file_paths) return False
@dataclasses.dataclass
class TestSuite:
name: str
cases: list[TestCase] = dataclasses.field(default_factory=list)
suites = list[TestSuite]()
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))
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))
if update:
to_record.extend(((suite, case) for suite in suites for case in suite.cases))
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
)
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__": 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()