aboutsummaryrefslogtreecommitdiffstats
path: root/.github/scripts
diff options
context:
space:
mode:
authorNikita Kozlovskiy <nikitka@gmail.com>2023-08-31 17:21:44 +0300
committernkozlovskiy <nmk@ydb.tech>2023-08-31 17:50:38 +0300
commit0a6cd0452320909717ba06b29a39e6af06e004ff (patch)
treed737530d57e4d16ba5e72e1049d9099e696dd25c /.github/scripts
parentc4a7009cfefdddcc6860cb09469984246ad39750 (diff)
downloadydb-0a6cd0452320909717ba06b29a39e6af06e004ff.tar.gz
ci: PR comment posting with the testing results
ci: add comment posting to the pr with the test summary Pull Request resolved: #353
Diffstat (limited to '.github/scripts')
-rwxr-xr-x.github/scripts/tests/generate-summary.py214
1 files changed, 155 insertions, 59 deletions
diff --git a/.github/scripts/tests/generate-summary.py b/.github/scripts/tests/generate-summary.py
index 9ce5b32807..d3f88a28b5 100755
--- a/.github/scripts/tests/generate-summary.py
+++ b/.github/scripts/tests/generate-summary.py
@@ -1,9 +1,12 @@
#!/usr/bin/env python3
import argparse
import dataclasses
-import os, sys
+import os
+import json
+import sys
+from github import Github, Auth as GithubAuth
+from github.PullRequest import PullRequest
from enum import Enum
-from itertools import groupby
from operator import attrgetter
from typing import List, Optional
from jinja2 import Environment, FileSystemLoader
@@ -45,6 +48,108 @@ class TestResult:
def full_name(self):
return f"{self.classname}/{self.name}"
+ @classmethod
+ def from_junit(cls, testcase):
+ classname, name = testcase.get("classname"), testcase.get("name")
+ if testcase.find("failure") is not None:
+ status = TestStatus.FAIL
+ elif testcase.find("error") is not None:
+ status = TestStatus.ERROR
+ elif get_property_value(testcase, "mute") is not None:
+ status = TestStatus.MUTE
+ elif testcase.find("skipped") is not None:
+ status = TestStatus.SKIP
+ else:
+ status = TestStatus.PASS
+ log_url = get_property_value(testcase, "url:Log")
+
+ return cls(classname, name, status, log_url)
+
+
+class TestSummaryLine:
+ def __init__(self, title):
+ self.title = title
+ self.tests = []
+ self.is_failed = False
+ self.report_fn = self.report_url = None
+ self.counter = {s: 0 for s in TestStatus}
+
+ def add(self, test: TestResult):
+ self.is_failed |= test.status in (TestStatus.ERROR, TestStatus.FAIL)
+ self.counter[test.status] += 1
+ self.tests.append(test)
+
+ def add_report(self, fn, url):
+ self.report_fn = fn
+ self.report_url = url
+
+ @property
+ def test_count(self):
+ return len(self.tests)
+
+ @property
+ def passed(self):
+ return self.counter[TestStatus.PASS]
+
+ @property
+ def errors(self):
+ return self.counter[TestStatus.ERROR]
+
+ @property
+ def failed(self):
+ return self.counter[TestStatus.FAIL]
+
+ @property
+ def skipped(self):
+ return self.counter[TestStatus.SKIP]
+
+ @property
+ def muted(self):
+ return self.counter[TestStatus.MUTE]
+
+
+class TestSummary:
+ def __init__(self):
+ self.lines: List[TestSummaryLine] = []
+ self.is_failed = False
+
+ def add_line(self, line: TestSummaryLine):
+ self.is_failed |= line.is_failed
+ self.lines.append(line)
+
+ 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")
+
+ footnote_url = f"{github_srv}/{repo}/tree/main/.github/config"
+
+ footnote = "[^1]" if add_footnote else f'<sup>[?]({footnote_url} "All mute rules are defined here")</sup>'
+
+ result = [
+ f"| | TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED{footnote} |",
+ "| :--- | ---: | -----: | -----: | -----: | ------: | ----: |",
+ ]
+ 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),
+ ]
+ )
+ )
+
+ if add_footnote:
+ result.append("")
+ result.append(f"[^1]: All mute rules are defined [here]({footnote_url}).")
+ return result
+
def render_pm(value, url, diff=None):
if value:
@@ -94,14 +199,14 @@ def render_testlist_html(rows, fn):
fp.write(content)
-def write_summary(lines: List[str]):
+def write_summary(summary: TestSummary):
summary_fn = os.environ.get("GITHUB_STEP_SUMMARY")
if summary_fn:
fp = open(summary_fn, "at")
else:
fp = sys.stdout
- for line in lines:
+ for line in summary.render(add_footnote=True):
fp.write(f"{line}\n")
fp.write("\n")
@@ -109,69 +214,50 @@ def write_summary(lines: List[str]):
fp.close()
-def gen_summary(
- summary_url_prefix,
- summary_out_folder,
- paths,
-):
- summary = [
- "| | TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED[^1] |",
- "| :--- | ---: | -----: | -----: | -----: | ------: | ----: |",
- ]
- for title, html_fn, path in paths:
- tests = failed = errors = muted = skipped = passed = 0
+def gen_summary(summary_url_prefix, summary_out_folder, paths):
+ summary = TestSummary()
- test_results = []
+ for title, html_fn, path in paths:
+ summary_line = TestSummaryLine(title)
for fn, suite, case in iter_xml_files(path):
- tests += 1
- classname, name = case.get("classname"), case.get("name")
- if case.find("failure") is not None:
- failed += 1
- status = TestStatus.FAIL
- elif case.find("error") is not None:
- errors += 1
- status = TestStatus.ERROR
- elif get_property_value(case, "mute") is not None:
- muted += 1
- status = TestStatus.MUTE
- elif case.find("skipped") is not None:
- skipped += 1
- status = TestStatus.SKIP
- else:
- passed += 1
- status = TestStatus.PASS
-
- log_url = get_property_value(case, "url:Log")
-
- test_result = TestResult(classname=classname, name=name, status=status, log_url=log_url)
- test_results.append(test_result)
+ test_result = TestResult.from_junit(case)
+ summary_line.add(test_result)
report_url = f"{summary_url_prefix}{html_fn}"
- render_testlist_html(test_results, os.path.join(summary_out_folder, html_fn))
-
- summary.append(
- " | ".join(
- [
- title,
- render_pm(tests, f"{report_url}", 0),
- render_pm(passed, f"{report_url}#PASS", 0),
- render_pm(errors, f"{report_url}#ERROR", 0),
- render_pm(failed, f"{report_url}#FAIL", 0),
- render_pm(skipped, f"{report_url}#SKIP", 0),
- render_pm(muted, f"{report_url}#MUTE", 0),
- ]
- )
- )
+ render_testlist_html(summary_line.tests, os.path.join(summary_out_folder, html_fn))
+ summary_line.add_report(html_fn, report_url)
+ summary.add_line(summary_line)
+
+ return summary
- github_srv = os.environ.get("GITHUB_SERVER_URL", "https://github.com")
- repo = os.environ.get("GITHUB_REPOSITORY", "ydb-platform/ydb")
- summary.append("\n")
- summary.append(f"[^1]: All mute rules are defined [here]({github_srv}/{repo}/tree/main/.github/config).")
+def update_pr_comment(pr: PullRequest, summary: TestSummary):
+ header = f"<!-- status {pr.number} -->"
+
+ if summary.is_failed:
+ result = ":red_circle: Some tests failed"
+ else:
+ result = ":green_circle: All tests passed"
- write_summary(lines=summary)
+ body = [header, f"{result} for commit {pr.head.sha}."]
+
+ body.extend(summary.render())
+ body = "\n".join(body)
+
+ comment = None
+
+ for c in pr.get_issue_comments():
+ if c.body.startswith(header):
+ comment = c
+ break
+
+ if comment is None:
+ pr.create_issue_comment(body)
+ return
+
+ comment.edit(body)
def main():
@@ -188,7 +274,17 @@ def main():
paths = iter(args.args)
title_path = list(zip(paths, paths, paths))
- gen_summary(args.summary_url_prefix, args.summary_out_path, title_path)
+ summary = gen_summary(args.summary_url_prefix, args.summary_out_path, title_path)
+ write_summary(summary)
+
+ if os.environ.get("GITHUB_EVENT_NAME") in ("pull_request", "pull_request_target"):
+ gh = Github(auth=GithubAuth.Token(os.environ["GITHUB_TOKEN"]))
+
+ with open(os.environ["GITHUB_EVENT_PATH"]) as fp:
+ event = json.load(fp)
+
+ pr = gh.create_from_raw_data(PullRequest, event["pull_request"])
+ update_pr_comment(pr, summary)
if __name__ == "__main__":