项目文件夹

文件
2026-07-13 12:32:21 +08:00

344 行
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