Files
2026-07-13 12:32:21 +08:00

344 lines
10 KiB
Python

import os
import re
import sys
import time
import atexit
import difflib
import inspect
import datetime
import subprocess
from inspect import getframeinfo
from globmatch import glob_match
import suites
def glob_match_ignore_slash(path, globs):
if path.startswith("/"):
path = path[1:]
globs = list(globs)
for i, g in enumerate(globs):
if g.startswith("/"):
globs[i] = g[1:]
return glob_match(path, globs)
def write_color(color):
term_colors = {
"red": 31,
"green": 32,
"yellow": 33,
"blue": 34,
"magenta": 35,
"cyan": 36,
"white": 37,
}
def decorator(fn):
def wrapper(self, *args):
if color not in term_colors:
raise ValueError("unknown color %s" % color)
sys.stdout.write('\033[%dm' % term_colors[color])
r = fn(self, *args)
sys.stdout.write('\033[0m')
return r
return wrapper
return decorator
def write_block_desc(desc_verb):
def decorator(fn):
def wrapper(self, suite: ExpectSuite, *args):
ExpectChain._log("[INFO] start to %s of suite %s" %
(desc_verb, suite.name))
start_time = time.time()
r = fn(self, suite, *args)
duration = time.time() - start_time
ExpectChain._log("[INFO] finish to %s of suite %s in %.2fms" % (
desc_verb, suite.name, duration*1000))
return r
return wrapper
return decorator
class ExpectSuite():
def __init__(self, name, manifest, use_rpath=False, tests={}):
self.name = name
self.manifest = manifest
self.use_rpath = use_rpath
self.tests = tests
class ExpectChain():
def __init__(self, infos):
self._infos = infos
self._all_failures = []
self._reset()
self.verbs = ("does_not", "equal", "match", "contain",
"contain_match", "less_than", "greater_than")
atexit.register(self._print_all_fails)
def _reset(self):
# clear states
self._logical_reverse = False
self._files = []
self._msg = ""
self._title_shown = False
self._checks_count = 0
self._failures_count = 0
self._last_attribute = None
def _ctx_info(self):
f = inspect.currentframe().f_back.f_back.f_back.f_back
fn_rel = os.path.relpath(getframeinfo(f).filename, os.getcwd())
return "%s:%d" % (fn_rel, f.f_lineno)
@classmethod
def _log(self, *args):
sys.stdout.write(" %s " % datetime.datetime.now().strftime('%b %d %X'))
print(*args)
@write_color("white")
def _print_title(self):
if self._title_shown:
return
self._log("[TEST] %s: %s" % (self._ctx_info(), self._msg))
self._title_shown = True
@write_color("red")
def _print_fail(self, msg):
self._log("[FAIL] %s" % msg)
self._all_failures.append("%s: %s" % (self._ctx_info(), msg))
self._failures_count += 1
@write_color("green")
def _print_ok(self, msg):
self._log("[OK ] %s" % msg)
@write_color("yellow")
def _print_error(self, msg):
self._log("[FAIL] %s" % msg)
def _print_result(self):
if self._checks_count == 0:
return
if self._failures_count == 0:
self._print_ok("%d check(s) passed for %d file(s)" %
(self._checks_count, len(self._files)))
else:
self._print_error("%d/%d check(s) failed for %d file(s)" % (
self._failures_count, self._checks_count, len(self._files)))
@write_color("red")
def _print_all_fails(self):
# flush pending result
self._print_result()
if self._all_failures:
self._print_error(
"Following failure(s) occurred:\n" + "\n".join(self._all_failures))
os._exit(1)
def _compare(self, attr, fn):
self._checks_count += 1
results = []
for f in self._files:
if not hasattr(f, attr):
continue # accept missing attribute for now
v = getattr(f, attr)
if self._key_name and isinstance(v, dict):
# TODO: explicit flag to accept missing key
if self._key_name not in v:
return True
v = v[self._key_name]
(r, err_template) = fn(v)
if r:
results.append(r)
if (not not r) == self._logical_reverse:
_not = "not"
if self._logical_reverse:
_not = "actually"
self._print_fail("file %s <%s>: %s" % (
f.relpath, attr, err_template.format(v, NOT=_not)
))
return False
return results
def _exist(self):
self._checks_count += 1
matched_files_count = len(self._files)
if (matched_files_count > 0) == self._logical_reverse:
self._print_fail("found %d files matching %s" % (
matched_files_count, self._path_glob))
return self
# following are verbs
def _equal(self, attr, expect):
return self._compare(attr, lambda a: (a == expect, "'{}' does {NOT} equal to '%s'" % expect))
def _match(self, attr, expect):
r = self._compare(attr, lambda a: (re.search(expect, a), "'{}' does {NOT} match '%s'" % expect))
self.last_macthes = r
return (not not r)
def _less_than(self, attr, expect):
def fn(a):
if isinstance(a, list):
ll = sorted(list(a))[-1]
else:
ll = a
return ll < expect, "'{}' is {NOT} less than %s" % expect
return self._compare(attr, fn)
def _greater_than(self, attr, expect):
def fn(a):
if isinstance(a, list):
ll = sorted(list(a))[0]
else:
ll = a
return ll > expect, "'{}' is {NOT} greater than %s" % expect
return self._compare(attr, fn)
def _contain(self, attr, expect):
def fn(a):
if isinstance(a, list):
ok = expect in a
msg = "'%s' is {NOT} found in the list" % expect
if not ok:
if len(a) == 0:
msg = "'%s' is empty" % attr
else:
closest = difflib.get_close_matches(expect, a, 1)
if len(closest) > 0:
msg += ", did you mean '%s'?" % closest[0]
return ok, msg
else:
return False, "%s is not a list" % attr
# should not reach here
return self._compare(attr, fn)
def _contain_match(self, attr, expect):
def fn(a):
if isinstance(a, list):
msg = "'%s' is {NOT} found in the list" % expect
for e in a:
r = re.search(expect, e)
if r:
return r, msg
return False, msg
else:
return False, "'%s' is not a list" % attr
return self._compare(attr, fn)
# following are public methods (test functions)
def to(self):
# does nothing, but helps to construct English
return self
def expect(self, path_glob, msg):
# lazy print last test result
self._print_result()
# reset states
self._reset()
self._msg = msg
self._print_title()
self._path_glob = path_glob
if isinstance(path_glob, str):
self._path_glob = [path_glob]
for f in self._infos:
if glob_match_ignore_slash(f.relpath, self._path_glob):
self._files.append(f)
return self
def do_not(self):
self._logical_reverse = True
return self
def does_not(self):
return self.do_not()
def is_not(self):
return self.do_not()
# access the value of the dict of key "key"
def key(self, key):
self._key_name = key
return self
def exist(self):
return self._exist()
def exists(self):
return self._exist()
def __getattr__(self, name):
dummy_call = lambda *x: self
verb = re.findall("^(.*?)(?:s|es)?$", name)[0]
if verb not in self.verbs:
# XXX: hack to support rpath/runpath
if self._current_suite.use_rpath and name == "runpath":
name = "rpath"
elif not self._current_suite.use_rpath and name == "rpath":
name = "runpath"
self._last_attribute = name
# reset
self._logical_reverse = False
self._key_name = None
return self
if not self._last_attribute:
self._print_error("attribute is not set before verb \"%s\"" % name)
return dummy_call
attr = self._last_attribute
for f in self._files:
if not hasattr(f, attr):
self._print_error(
"\"%s\" expect \"%s\" attribute to be present, but it's absent for %s (a %s)" % (
name, attr, f.relpath, type(f)))
return dummy_call
def cls(expect):
getattr(self, "_%s" % verb)(attr, expect)
return self
return cls
@write_block_desc("compare manifest")
def compare_manifest(self, suite: ExpectSuite, manifest: str):
self._current_suite = suite
if not suite.manifest:
return
diff_result = subprocess.run(
['diff', "-BbNaur", suite.manifest, '-'], input=manifest, stdout=subprocess.PIPE)
if diff_result.returncode != 0:
self._print_fail("manifest is not up-to-date:")
if diff_result.stdout:
print(diff_result.stdout.decode())
if diff_result.stderr:
print(diff_result.stderr.decode())
@write_block_desc("run test suite")
def run(self, suite: ExpectSuite):
self._current_suite = suite
for s in suite.tests:
s(self.expect, **suite.tests[s])
self._print_result() # cleanup the lazy buffer
def get_last_macthes(self):
return self.last_macthes