diff --git a/requirements.txt b/requirements.txt index 7d8ffe4..904e153 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ aiocoap[linkheader]>=0.4b3 +beautifulsoup4 iotlabcli pygithub pytest diff --git a/testutils/github.py b/testutils/github.py index e188e8e..8296fdf 100644 --- a/testutils/github.py +++ b/testutils/github.py @@ -3,13 +3,20 @@ import re import subprocess +from bs4 import BeautifulSoup from github import Github, GithubException +STICKY_COMMENT_COMMENT = "" GITHUB_DOMAIN = "github.com" API_URL = "https://api.%s" % GITHUB_DOMAIN REPO_NAME = "RIOT-OS/Release-Specs" GITHUBTOKEN_FILE = ".riotgithubtoken" +OUTCOME_EMOJIS = { + "passed": "✔", + "failed": "✖", + "skipped": "🟡", +} logger = logging.getLogger(__name__) @@ -124,42 +131,137 @@ def find_task_text(issue_body, tested_task): return None, None -def make_comment(pytest_report, issue, task): - comment = "### [{:02d}. {}]({})\n\n".format( - task["spec"]["spec"], - task["name"], - task["url"] - ) +def find_previous_comment(github, issue): + comment_comment = STICKY_COMMENT_COMMENT.format(user=github.user.name) + for comment in issue.get_comments(): + if comment_comment in comment.body: + return comment + return None + + +def create_comment(github, issue): + body = "

Test Report

\n\n" + body += STICKY_COMMENT_COMMENT.format(user=github.user.name) + body += """ + + + + + + +
TaskOutcome
+""" + try: + return issue.create_comment(body) + except GithubException as e: + logger.error("Unable to comment: {}".format(e)) + return None + + +def _generate_outcome_summary(pytest_report): run_url = None if "GITHUB_RUN_ID" in os.environ and \ "GITHUB_REPOSITORY" in os.environ and \ "GITHUB_SERVER_URL" in os.environ: run_url = "{GITHUB_SERVER_URL}/{GITHUB_REPOSITORY}/actions/runs/" \ "{GITHUB_RUN_ID}".format(**os.environ) - comment += "
{}{}{}\n\n" \ + outcome = "
{}{}{}\n\n" \ .format( ''.format(run_url) if run_url else '', pytest_report.outcome.upper(), '' if run_url else '' ) if pytest_report.longrepr: - comment += "###### Failures\n\n" - comment += "```\n" - comment += str(pytest_report.longrepr) - comment += "\n```\n\n" + outcome += "###### Failures\n\n" + outcome += "```\n" + outcome += str(pytest_report.longrepr) + outcome += "\n```\n\n" if pytest_report.sections: for title, body in pytest_report.sections: - comment += "###### {}\n\n".format(title) - comment += "```\n" - comment += str(body) - comment += "\n```\n\n" - comment += "
" + outcome += "###### {}\n\n".format(title) + outcome += "```\n" + outcome += str(body) + outcome += "\n```\n\n" + outcome += "
" + return outcome + + +def _get_tasks(comment, tbody, task): + tasks = [] + task_already_exists = False + for row in tbody.children: + if row.name != 'tr': + continue + cells = row.contents[0] + try: + emoji = cells[0].decode_contents() + task_cell = cells[1].contents[0] + outcome = cells[2] + except IndexError: + logger.error("Unexpected table format in %s:\n%s", + comment, row) + task_title = task.decode_contents().strip() + try: + task_url = task['href'] + except KeyError: + logger.error("Unexpected table format in %s:\n%s", + comment, task_cell) + if task_title == task["title"]: + task_already_exists = True + emoji = task["emoji"] + task_url = task["task_url"] + outcome = task["outcome"] + tasks.append((task_title, task_url, emoji, outcome)) + if not task_already_exists: + tasks.append((task["title"].strip(), task["url"], task["emoji"], + task["outcome"])) + tasks.sort() + + +def _update_soup(soup, tbody, tasks): + tbody.clear() + for task in tasks: + tr = soup.new_tag("tr") + tbody.append(tr) + emoji_td = soup.new_tag("td") + emoji_td.string = task[2] + tr.append(emoji_td) + task_td = soup.new_tag("td") + task_a = soup.new_tag("a", href=task[1]) + task_a.string = task[0] + task_td.append(task_a) + tr.append(task_td) + outcome_td = soup.new_tag("td") + outcome_td.append(tasks[3]) + tr.append(outcome_td) + + +def update_comment(pytest_report, comment, task): + soup = BeautifulSoup(comment.body, "html.parser") + task["title"] = "{:02d}. {}".format(task["spec"]["spec"], task["name"]) \ + .strip() + task["outcome"] = BeautifulSoup(_generate_outcome_summary(pytest_report), + "html.parser").details + task["emoji"] = OUTCOME_EMOJIS[pytest_report.outcome.lower()] + tbody = soup.find('tbody') + if tbody is None: + logger.error("Unable to find table body in %s:\n%s", + comment, comment.body) + return None + _update_soup(soup, tbody, _get_tasks(comment, tbody, task)) try: - return issue.create_comment(comment) + comment.edit(soup.decode_contents()) except GithubException as e: - logger.error("Unable to comment: {}".format(e)) + logger.error("Unable to update comment: {}".format(e)) return None +def make_comment(pytest_report, github, issue, task): + comment = find_previous_comment(github, issue) + if comment is None: + comment = create_comment(github, issue) + update_comment(pytest_report, comment, task) + + # pylint: disable=R0911,R0912 def update_issue(pytest_report): # noqa: C901 if pytest_report.when != 'call' or pytest_report.outcome == 'skipped': @@ -189,7 +291,7 @@ def update_issue(pytest_report): # noqa: C901 logger.warning("Unable to find task {spec}.{task} in the " "tracking issue".format(**tested_task)) elif not task["done"]: - comment = make_comment(pytest_report, issue, task) + comment = make_comment(pytest_report, github, issue, task) if comment: comment_url = comment.html_url else: