summaryrefslogtreecommitdiffstats
path: root/.github/scripts/tests
diff options
context:
space:
mode:
authorNikita Kozlovskiy <[email protected]>2023-10-10 16:14:03 +0300
committernkozlovskiy <[email protected]>2023-10-10 16:45:28 +0300
commitd039bfa4e3a41b10fc8d1fd06d125842dcf94200 (patch)
treef2b24af746fbbfb9109b1d7994b4da23dda03400 /.github/scripts/tests
parent7722c2d79d4f144aa037357ef2564168286b759e (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.py46
-rw-r--r--.github/scripts/tests/junit_utils.py11
-rwxr-xr-x.github/scripts/tests/transform-ya-junit.py209
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()