#!/usr/bin/env python3 import argparse import glob import os import sys import subprocess import shutil class Configuration: def __init__(self, xic, testdir, spim, workdir, registers_description, plugin): self.xic = xic self.testdir = testdir self.spim = spim self.workdir = workdir self.registers_description = registers_description self.plugin = plugin def printself(self): print('Configuration:') print(' - xic: ', self.xic) print(' - testdir: ', self.testdir) print(' - spim: ', self.spim) print(' - workdir: ', self.workdir) if self.registers_description is not None: print(' - regdescr:', self.registers_description) if self.plugin: print(' - plugin:', self.plugin) class TestOutput: class Status: COMPILER_FAILURE = 0 COMPILER_SUCCES = 1 SPIM_FAILURE = 2 SPIM_SUCCESS = 3 def __init__(self): self.status = None self.compiler_stdout = None self.compiler_stderr = None self.compiler_ok = None self.spim_stdout = None self.spim_stderr = None self.spim_ok = None class TestInstrumentation: def __init__(self, test): self.test = test self.instrumented = False self.expected_output = [] self.should_parse = True self.stop_after = None self.should_typecheck = True self.typechecking_errors = [] self.selftest = None self.env = {} self.parse() self.validate() def content(self): # get content of all comments started with //@ lines = open(self.test).readlines() lines = [ line.strip() for line in lines ] lines = [ line for line in lines if line.startswith("//@") ] lines = [ line[3:] for line in lines ] return lines def parse(self): content = self.content() self.instrumented = "PRACOWNIA" in content if not self.instrumented: raise Exception('Test instrumentation is missing: %s' % self.test) for line in content: if line.startswith("out "): self.expected_output.append(line[4:]) elif line == "should_not_parse": self.should_parse = False elif line == "should_not_typecheck": self.should_typecheck = False elif line.startswith("tcError "): self.typechecking_errors.append(line[len("tcError "):]) elif line.startswith("stop_after"): self.stop_after = line[len("stop_after "):] elif line.startswith("env"): keyvalue = line[len("env "):] keyvalue = keyvalue.split('=') self.env[keyvalue[0]] = keyvalue[1] elif line.startswith('selftest'): arg = line[len('selftest'):].strip() if arg == 'pass': self.selftest = True elif arg == 'fail': self.selftest = False else: raise Exception("invalid @selftest directive") elif line == "PRACOWNIA": pass else: raise Exception("invalid test instrumentation: unknown directive: " + line) def validate(self): if not self.instrumented: return if not self.should_parse: if len(self.expected_output) > 0: raise Exception("test %s marked as @should_not_parse, but expected runtime output is specified (@out)" % self.test) if len(self.typechecking_errors) > 0: raise Exception("test %s marked as @should_not_parse, but expected typechecking errors are specified (@tcError)" % self.test) if not self.should_typecheck: raise Exception("test %s marked as @should_not_parse, but expected typechecking failure is marked (@should_not_typecheck)" % self.test) if not self.should_typecheck: if len(self.expected_output) > 0: raise Exception("test %s expects typechecking failure, but expected runtime output is specified (@out)" % self.test) if len(self.typechecking_errors): if len(self.expected_output) > 0: raise Exception("test %s expects typechecking errors, but expected runtime output is specified (@out)" % self.test) self.should_typecheck = False class Test: def __init__(self, test, instrumentation): self.test = test self.instrumentation = instrumentation def expecting_parsing_error(self): return not self.instrumentation.should_parse def expecting_typechecking_error(self): return not self.instrumentation.should_typecheck def expecting_compilation_failure(self): return self.expecting_parsing_error() or self.expecting_typechecking_error() def expecting_runtime_output(self): return not self.expecting_compilation_failure() and not self.instrumentation.stop_after class ExpectationMatcher: def __init__(self, test , output): self.test = test self.output = output def __match_output(self, stdout, expected): actual = list(reversed(stdout)) expected = list(reversed(expected)) for i in range(0, len(expected)): if len(actual) <= i: # nie sprawdzam tego przed petla bo chcialbym aby najpierw zmatchowal te linijki # co sie rzeczywiscie na stdout pojawily return False, "program output is too short, it contains %u lines, while expected out has %u" % (len(actual), len(expected)) expected_line = expected[i] actual_line = actual[i] if expected_line != actual_line: explanation = "mismatch on line (counting from bottom): %u\nexpected: %s\nactual: %s" % (i + 1, expected_line, actual_line) return False, explanation return True, "" def __real_match(self): # print('self.test.expecting_compilation_failure()', self.test.expecting_compilation_failure()) # print('self.test.instrumentation.should_typecheck', self.test.instrumentation.should_typecheck) # print('self.output.compiler_ok', self.output.compiler_ok) if self.test.expecting_compilation_failure(): xic_stderr = self.output.compiler_stderr.decode('utf8') xic_stderr = xic_stderr.splitlines() if len(xic_stderr) == 0: xic_last_line_stderr = None else: xic_last_line_stderr = xic_stderr[-1].strip() if self.output.compiler_ok: return False, "expected compiler failure" if self.test.expecting_parsing_error(): if xic_last_line_stderr == "Failed: parser": return True, "" return False, "expected parsing error" if self.test.expecting_typechecking_error(): if xic_last_line_stderr == "Failed: typechecker": return True, "" return False, "expected typchecking error" else: if not self.output.compiler_ok: return False, "program should be compiled, but compiler failed" if self.test.instrumentation.stop_after: return True, "" if len(self.output.spim_stderr) > 0: return False, "spim's stderr is not empty, execute program manually" if self.test.expecting_runtime_output(): if not self.output.spim_ok: return False, "spim was not executed properly" if len(self.test.instrumentation.expected_output) > 0: spim_stdout = self.output.spim_stdout.decode('utf8') spim_stdout = spim_stdout.splitlines() spim_stdout = [ line.strip() for line in spim_stdout ] return self.__match_output(spim_stdout,self.test.instrumentation.expected_output) return True, "" return None, "cannot match test expectations" def match(self): x_result, x_explanation = self.__real_match() if self.test.instrumentation.selftest == None: result, explanation = x_result, x_explanation else: result = x_result == self.test.instrumentation.selftest if result: explanation = "" else: explanation = "selftest failed: expected %s, got: %s, %s" % (self.test.instrumentation.selftest, x_result, x_explanation) return result, explanation class TestRawExecutor: def __init__(self, conf, test, env, run_spim, stop_point): self.conf = conf self.test = test self.env = env self.output_file = os.path.join(conf.workdir, 'main.s') self.test_output = TestOutput() self.run_spim = run_spim self.stop_point = stop_point def execute(self): self.prepare_env() ok, stdout, stderr = self.compile_program() self.test_output.compiler_stdout = stdout self.test_output.compiler_stderr = stderr self.test_output.compiler_ok = ok if not ok or not self.run_spim or self.stop_point: self.clean_env() return self.test_output ok, stdout, stderr = self.execute_program() self.test_output.spim_stdout = stdout self.test_output.spim_stderr = stderr self.test_output.spim_ok = ok self.clean_env() return self.test_output def compile_program(self): xs = [self.conf.xic, '-o', self.output_file] if self.stop_point: xs.append('--stop-after') xs.append(self.stop_point) if self.conf.registers_description is not None: xs.append('--registers-description') xs.append(self.conf.registers_description) if self.conf.plugin is not None: xs.append('--plugin') xs.append(self.conf.plugin) xs.append('--xi-log') xs.append(os.path.join(self.conf.workdir, 'xilog')) xs.append(self.test) env = dict(self.env) return self.__call(xs, env) def execute_program(self): return self.__call([self.conf.spim, '-file', self.output_file]) def prepare_env(self): shutil.rmtree(self.conf.workdir, ignore_errors=True) os.makedirs(self.conf.workdir) def clean_env(self): shutil.rmtree(self.conf.workdir, ignore_errors=True) def __call(self, xs, extenv={}): env = os.environ for k in extenv: env[k] = extenv[k] try: p = subprocess.Popen(xs, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) stdin, stdout = p.communicate(timeout=5) status = p.returncode == 0 return (status, stdin, stdout) except subprocess.TimeoutExpired: return (False, [], []) except Exception: # potem cos tu doklepie aby wykonywarka testow mogla sie kapnac, ze to nie test wykryl blad # a cos innego return (False, [], []) class TestExecutor: def __init__(self, test, conf): self.test = test self.conf = conf def execute(self): try: run_spim = self.test.expecting_runtime_output() stop_point = None if not run_spim: if self.test.expecting_parsing_error(): stop_point = "parser" elif self.test.expecting_typechecking_error(): stop_point = "typechecker" elif self.test.instrumentation.stop_after: stop_point = self.test.instrumentation.stop_after rawExecutor = TestRawExecutor(self.conf, self.test.test, self.test.instrumentation.env, run_spim, stop_point) test_output = rawExecutor.execute() matcher = ExpectationMatcher(self.test, test_output) return matcher.match() except Exception as e: raise e return None, "internal error: " + str(e) class TestRepository: def __init__(self, testdirs): self.tests = [] self.collect_tests(testdirs) def collect_tests(self, testdirs): testfiles = [] for testdir in testdirs: for path, _, files in os.walk(testdir): for file in files: if file.endswith(".xi"): testfiles.append(os.path.join(path, file)) testfiles = list(sorted(testfiles)) for testfile in testfiles: instrumentation = TestInstrumentation(testfile) test = Test(testfile, instrumentation) self.tests.append(test) def gen(self): for t in self.tests: yield t class Application: def __init__(self): args = self.create_argparse().parse_args() self.conf = Configuration(xic=args.xic, testdir=args.testdir, spim=args.spim, workdir=args.workdir, registers_description=args.registers_description, plugin=args.plugin) def create_argparse(self): parser = argparse.ArgumentParser(description='Xi tester') parser.add_argument('--xic', help='path to xi binary', default='./_build/install/default/bin/xi', type=str) parser.add_argument('--spim', help='path to spim binary', default='spim', type=str) parser.add_argument('--testdir', help='path to test directory', default='./tests', type=str) parser.add_argument('--workdir', help='working directory', default='workdir', type=str) parser.add_argument('--registers-description', help='xi --registers-description', default=None, type=str) parser.add_argument('--plugin', help='xi --plugin', type=str) return parser def run(self): print('Xi tester') self.conf.printself() self.test_repository = TestRepository([self.conf.testdir]) passed_tests = [] failed_tests = [] inconclusive_tests = [] for test in self.test_repository.gen(): print('==> running test', test.test) executor = TestExecutor(test, self.conf) result, explanation = executor.execute() if result == None: inconclusive_tests.append(test) status = "inconclusive: " + explanation elif result: passed_tests.append(test) status = "pass" elif not result: failed_tests.append(test) status = "fail: " + explanation print('--- result:', status) total = len(passed_tests) + len(failed_tests) + len(inconclusive_tests) print('===================') print('Total: ', total) print('Passed:', len(passed_tests)) print('Inconc:', len(inconclusive_tests)) print('Failed:', len(failed_tests)) for test in failed_tests: print(' -', test.test) Application().run()