diff options
| author | Nikita Kozlovskiy <[email protected]> | 2023-10-10 16:14:03 +0300 |
|---|---|---|
| committer | nkozlovskiy <[email protected]> | 2023-10-10 16:45:28 +0300 |
| commit | d039bfa4e3a41b10fc8d1fd06d125842dcf94200 (patch) | |
| tree | f2b24af746fbbfb9109b1d7994b4da23dda03400 /.github/scripts/tests | |
| parent | 7722c2d79d4f144aa037357ef2564168286b759e (diff) | |
add ya make and test support
add ya make and test support
Pull Request resolved: https://github.com/ydb-platform/ydb/pull/392
Diffstat (limited to '.github/scripts/tests')
| -rwxr-xr-x | .github/scripts/tests/generate-summary.py | 46 | ||||
| -rw-r--r-- | .github/scripts/tests/junit_utils.py | 11 | ||||
| -rwxr-xr-x | .github/scripts/tests/transform-ya-junit.py | 209 |
3 files changed, 251 insertions, 15 deletions
diff --git a/.github/scripts/tests/generate-summary.py b/.github/scripts/tests/generate-summary.py index fca597e9692..6cd3db8aaa0 100755 --- a/.github/scripts/tests/generate-summary.py +++ b/.github/scripts/tests/generate-summary.py @@ -136,6 +136,9 @@ class TestSummary: self.is_failed |= line.is_failed self.lines.append(line) + def render_line(self, items): + return f"| {' | '.join(items)} |" + def render(self, add_footnote=False): github_srv = os.environ.get("GITHUB_SERVER_URL", "https://github.com") repo = os.environ.get("GITHUB_REPOSITORY", "ydb-platform/ydb") @@ -144,25 +147,38 @@ class TestSummary: footnote = "[^1]" if add_footnote else f'<sup>[?]({footnote_url} "All mute rules are defined here")</sup>' + columns = [ + "TESTS", "PASSED", "ERRORS", "FAILED", "SKIPPED", f"MUTED{footnote}" + ] + + need_first_column = len(self.lines) > 1 + + if need_first_column: + columns.insert(0, "") + result = [ - f"| | TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED{footnote} |", - "| :--- | ---: | -----: | -----: | -----: | ------: | ----: |", + self.render_line(columns), ] + + if need_first_column: + result.append(self.render_line([':---'] + ['---:'] * (len(columns) - 1))) + else: + result.append(self.render_line(['---:'] * len(columns))) + for line in self.lines: report_url = line.report_url - result.append( - " | ".join( - [ - line.title, - render_pm(line.test_count, f"{report_url}", 0), - render_pm(line.passed, f"{report_url}#PASS", 0), - render_pm(line.errors, f"{report_url}#ERROR", 0), - render_pm(line.failed, f"{report_url}#FAIL", 0), - render_pm(line.skipped, f"{report_url}#SKIP", 0), - render_pm(line.muted, f"{report_url}#MUTE", 0), - ] - ) - ) + row = [] + if need_first_column: + row.append(line.title) + row.extend([ + render_pm(line.test_count, f"{report_url}", 0), + render_pm(line.passed, f"{report_url}#PASS", 0), + render_pm(line.errors, f"{report_url}#ERROR", 0), + render_pm(line.failed, f"{report_url}#FAIL", 0), + render_pm(line.skipped, f"{report_url}#SKIP", 0), + render_pm(line.muted, f"{report_url}#MUTE", 0), + ]) + result.append(self.render_line(row)) if add_footnote: result.append("") diff --git a/.github/scripts/tests/junit_utils.py b/.github/scripts/tests/junit_utils.py index b4ec72e406a..eca82b39563 100644 --- a/.github/scripts/tests/junit_utils.py +++ b/.github/scripts/tests/junit_utils.py @@ -18,6 +18,13 @@ def add_junit_link_property(testcase, name, url): def add_junit_property(testcase, name, value): props = get_or_create_properties(testcase) + + # remove existing property if exists + for item in props.findall("property"): + if item.get('name') == name: + props.remove(item) + break + props.append(ET.Element("property", dict(name=name, value=value))) @@ -92,3 +99,7 @@ def iter_xml_files(folder_or_file): for suite in suites: for case in suite.findall("testcase"): yield fn, suite, case + + +def is_faulty_testcase(testcase): + return testcase.find("failure") is not None or testcase.find("error") is not None diff --git a/.github/scripts/tests/transform-ya-junit.py b/.github/scripts/tests/transform-ya-junit.py new file mode 100755 index 00000000000..4ac7951f9b7 --- /dev/null +++ b/.github/scripts/tests/transform-ya-junit.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +import argparse +import re +import json +import os +import sys +from xml.etree import ElementTree as ET +from mute_utils import mute_target, pattern_to_re +from junit_utils import add_junit_link_property, is_faulty_testcase + + +def log_print(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + + +class YaMuteCheck: + def __init__(self): + self.regexps = set() + + def add_unittest(self, fn): + with open(fn, "r") as fp: + for line in fp: + line = line.strip() + path, rest = line.split("/") + path = path.replace("-", "/") + rest = rest.replace("::", ".") + self.populate(f"{path}/{rest}") + + def add_functest(self, fn): + with open(fn, "r") as fp: + for line in fp: + line = line.strip() + line = line.replace("::", ".") + self.populate(line) + + def populate(self, line): + pattern = pattern_to_re(line) + + try: + self.regexps.add(re.compile(pattern)) + except re.error: + log_print(f"Unable to compile regex {pattern!r}") + + def __call__(self, suitename, testname): + for r in self.regexps: + if r.match(f"{suitename}/{testname}"): + return True + return False + + +class YTestReportTrace: + def __init__(self, out_root): + self.out_root = out_root + self.traces = {} + + def load(self, subdir): + test_results_dir = f"{subdir}/test-results/" + for folder in os.listdir(os.path.join(self.out_root, test_results_dir)): + fn = os.path.join(self.out_root, test_results_dir, folder, "ytest.report.trace") + + if not os.path.isfile(fn): + continue + + with open(fn, "r") as fp: + for line in fp: + event = json.loads(line.strip()) + if event["name"] == "subtest-finished": + event = event["value"] + cls = event["class"] + subtest = event["subtest"] + cls = cls.replace("::", ".") + self.traces[(cls, subtest)] = event + + def has(self, cls, name): + return (cls, name) in self.traces + + def get_logs(self, cls, name): + trace = self.traces.get((cls, name)) + + if not trace: + return {} + + logs = trace["logs"] + + result = {} + for k, path in logs.items(): + if k == "logsdir": + continue + + result[k] = path.replace("$(BUILD_ROOT)", self.out_root) + + return result + + +def filter_empty_logs(logs): + result = {} + for k, v in logs.items(): + if os.stat(v).st_size == 0: + continue + result[k] = v + return result + + +def save_log(build_root, fn, out_dir, log_url_prefix, trunc_size): + fpath = os.path.relpath(fn, build_root) + + if out_dir is not None: + out_fn = os.path.join(out_dir, fpath) + fsize = os.stat(fn).st_size + + out_fn_dir = os.path.dirname(out_fn) + + if not os.path.isdir(out_fn_dir): + os.makedirs(out_fn_dir, 0o700) + + if trunc_size and fsize > trunc_size: + with open(fn, "rb") as in_fp: + in_fp.seek(fsize - trunc_size) + log_print(f"truncate {out_fn} to {trunc_size}") + with open(out_fn, "wb") as out_fp: + while 1: + buf = in_fp.read(8192) + if not buf: + break + out_fp.write(buf) + else: + os.symlink(fn, out_fn) + + return f"{log_url_prefix}{fpath}" + + +def transform(fp, mute_check: YaMuteCheck, ya_out_dir, save_inplace, log_url_prefix, log_out_dir, log_trunc_size): + tree = ET.parse(fp) + root = tree.getroot() + + for suite in root.findall("testsuite"): + suite_name = suite.get("name") + traces = YTestReportTrace(ya_out_dir) + traces.load(suite_name) + + for case in suite.findall("testcase"): + test_name = case.get("name") + case.set("classname", suite_name) + + is_fail = is_faulty_testcase(case) + + if mute_check(suite_name, test_name): + log_print("mute", suite_name, test_name) + mute_target(case) + + if is_fail and "." in test_name: + test_cls, test_method = test_name.rsplit(".", maxsplit=1) + logs = filter_empty_logs(traces.get_logs(test_cls, test_method)) + + if logs: + log_print(f"add {list(logs.keys())!r} properties for {test_cls}.{test_method}") + for name, fn in logs.items(): + url = save_log(ya_out_dir, fn, log_out_dir, log_url_prefix, log_trunc_size) + add_junit_link_property(case, name, url) + + if save_inplace: + tree.write(fp.name) + else: + ET.indent(root) + print(ET.tostring(root, encoding="unicode")) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-i", action="store_true", dest="save_inplace", default=False, help="modify input file in-place" + ) + parser.add_argument("--mu", help="unittest mute config") + parser.add_argument("--mf", help="functional test mute config") + parser.add_argument("--log-url-prefix", default="./", help="url prefix for logs") + parser.add_argument("--log-out-dir", help="symlink logs to specific directory") + parser.add_argument( + "--log-truncate-size", + dest="log_trunc_size", + type=int, + default=134217728, + help="truncate log after specific size, 0 disables truncation", + ) + parser.add_argument("--ya-out", help="ya make output dir (for searching logs and artifacts)") + parser.add_argument("in_file", type=argparse.FileType("r")) + + args = parser.parse_args() + + mute_check = YaMuteCheck() + + if args.mu: + mute_check.add_unittest(args.mu) + + if args.mf: + mute_check.add_functest(args.mf) + + transform( + args.in_file, + mute_check, + args.ya_out, + args.save_inplace, + args.log_url_prefix, + args.log_out_dir, + args.log_trunc_size, + ) + + +if __name__ == "__main__": + main() |
