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

294
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:
continue
@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)
@dataclass
class Test_Case:
returncode = 0
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)
)
@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
def record(self, interpreter: str, source: str, cwd: str):
print(f"Recording case {self.name}")
result = self.run(interpreter, source, cwd)
@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)
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)}")
setattr(tc, attr, run_attr)
setattr(tc, "flags", flags)
return tc
self.exit_code, self.stderr_lines, self.stdout_lines = result.exit_code, result.stderr_lines, result.stdout_lines
def save(self, fname : str):
j = {}
for attr in ["returncode", "stdout", "stderr", "flags"]:
j[attr] = getattr(self, attr)
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
mkdir_recursive(fname)
with open(fname, 'w') as f:
json.dump(j, f, indent=4)
print(f"FAILED")
print(f"File: {source}")
def cmd_run_echoed(cmd, **kwargs):
print("[CMD] %s" % " ".join(map(shlex.quote, cmd)))
return subprocess.run(cmd, **kwargs)
if self.exit_code != result.exit_code:
print(f"Different exit code - expected {self.exit_code}, got {result.exit_code}")
def find_path_for_test_case(path: str) -> str:
directory, filename = os.path.split(path)
return (directory if directory else ".") + "/.tests_cache/" + filename + ".json"
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
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)
else:
continue
diff_line = None
for i, (exp_line, got_line) in enumerate(zip(expected, actual)):
if exp_line != got_line:
diff_line = i
break
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)
# 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"))
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:
file_paths.append(path)
elif mode == add_tests:
print("Adding: " + path)
print(f"Actual {name} is {len(actual) - len(expected)} lines longer then expected")
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__":
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()