import unittest
from tests.fakes import BOT, HEAD, FakeGitea, FakeModel, config, diff_for, gitea_file, review_data
from tests.test_flow import run
from werkrevisor import report
from werkrevisor.verdict import Verdict
APP = "app.py"
def app_gitea(**kw):
return FakeGitea(files=[gitea_file(APP)], diff=diff_for(APP, added=["x = 1"]), **kw)
def finding_run(issues, header="[high] Off by one", gate="approval", gitea=None, start=40, end=41):
data = review_data(issues=issues, header=header)
for issue in data["review"]["key_issues_to_review"]:
issue.update(start_line=start, end_line=end)
gitea = gitea or app_gitea()
cfg = config(gate={"yellow_gate": gate, "human_reviewers": ["alice"]})
outcome, _ = run(gitea, FakeModel(data=data, prompt_files=[APP]), cfg=cfg)
return outcome, gitea
class LineCommentTest(unittest.TestCase):
def test_finding_with_a_line_becomes_a_line_comment(self):
outcome, gitea = finding_run([APP])
[comment] = gitea.review_comments[0]
self.assertEqual((comment["path"], comment["new_position"], comment["old_position"]), (APP, 40, 0))
self.assertTrue(comment["body"].startswith("**[high] Off by one** (werkrevisor)\n\nORANGE"))
self.assertIn("Breaks X.", comment["body"])
# The comment holds the full text until Gitea has accepted the line comment.
self.assertIn("Breaks X.", gitea.created_comments[0])
result = gitea.comments[-1]["body"]
self.assertIn(f"**[high] Off by one** at `{APP}:40-41`, ORANGE, as a line comment", result)
self.assertNotIn("Breaks X.", result)
def test_finding_without_a_usable_line_stays_in_the_result(self):
for name, issues, start, refuse in (("no line", [APP], 0, False), ("other file", ["other.py"], 40, False),
("refused by Gitea", [APP], 40, True),
# Gitea 1.27 answers a line beyond the end of the file with HTTP 500.
("line beyond the file", [APP], 40, 500)):
with self.subTest(name):
gitea = app_gitea()
gitea.refuse_line_comments = refuse
outcome, gitea = finding_run(issues, gitea=gitea, start=start, end=start)
self.assertEqual(gitea.review_comments, [])
# A refused line comment was announced first; the comment is then edited to carry the finding.
body = gitea.comments[-1]["body"]
self.assertIn("Breaks X.", body)
self.assertNotIn("as a line comment", body)
self.assertEqual("Gitea refused the line comments" in body, bool(refuse))
def test_a_green_review_refused_with_http_500_approves_without_line_comments(self):
gitea = app_gitea()
gitea.refuse_line_comments = 500
outcome, gitea = finding_run([APP], header="[low] Naming", gitea=gitea)
self.assertIs(outcome.result.verdict, Verdict.GREEN)
self.assertEqual([r[0] for r in gitea.created_reviews], ["APPROVED"])
self.assertEqual(gitea.review_comments, [[]])
self.assertIn("Breaks X.", gitea.comments[-1]["body"])
def test_without_the_approval_gate_line_comments_come_as_comment_review(self):
for gate, issues, header, review in (("block", [APP], "[low] Naming", ["COMMENT"]),
("block", [APP], "[high] Off by one", ["COMMENT"]),
("block", [], "[low] Naming", []),
("pass_with_review_request", [APP], "[low] Naming", ["COMMENT"]),
("pass_with_review_request", [], "[low] Naming", [])):
with self.subTest(gate=gate, issues=issues, header=header):
outcome, gitea = finding_run(issues, header=header, gate=gate)
self.assertEqual([r[0] for r in gitea.created_reviews], review)
def test_review_kind_follows_the_verdict(self):
for header, verdict, kind in (("[low] Naming", Verdict.GREEN, "APPROVED"),
("[high] Off by one", Verdict.ORANGE, "COMMENT")):
with self.subTest(verdict=verdict):
outcome, gitea = finding_run([APP], header=header)
self.assertIs(outcome.result.verdict, verdict)
self.assertEqual([r[0] for r in gitea.created_reviews], [kind])
self.assertEqual(len(gitea.review_comments[0]), 1)
outcome, gitea = finding_run([APP], gate="block")
self.assertEqual([r[0] for r in gitea.created_reviews], ["COMMENT"])
def test_red_gives_a_comment_review_and_a_failed_gate(self):
gitea = app_gitea()
gitea.raw_files["doc/PR/1.md"] = b"## Open Questions\n\n- [ ] who?\n"
gitea.files.append(gitea_file("doc/PR/1.md"))
outcome, gitea = finding_run([APP], gitea=gitea)
self.assertEqual((outcome.result.verdict, outcome.result.exit_code), (Verdict.RED, 20))
self.assertEqual([r[0] for r in gitea.created_reviews], ["COMMENT"])
class FailureTest(unittest.TestCase):
def test_failed_review_moves_the_findings_into_the_comment_and_a_rerun_submits_it(self):
gitea = app_gitea()
original = gitea.create_review
def failing(*args, **kwargs):
from werkrevisor.gitea import GiteaError
raise GiteaError("POST reviews failed with HTTP 500", 500)
gitea.create_review = failing
outcome, gitea = finding_run([APP], header="[low] Naming", gitea=gitea)
self.assertIs(outcome.result.verdict, Verdict.ERROR)
self.assertIn("Breaks X.", gitea.comments[0]["body"])
gitea.create_review = original
finding_run([APP], header="[low] Naming", gitea=gitea)
self.assertEqual([r[0] for r in gitea.created_reviews], ["APPROVED"])
def test_the_result_comment_keeps_the_findings_if_gitea_fails_twice(self):
gitea = app_gitea()
def failing(*args, **kwargs):
raise OSError("HTTP 502")
gitea.create_review = failing
gitea.edit_issue_comment = lambda comment_id, body: (_ for _ in ()).throw(OSError("HTTP 502"))
outcome, gitea = finding_run([APP], gitea=gitea)
self.assertIs(outcome.result.verdict, Verdict.ERROR)
self.assertIn("Breaks X.", gitea.comments[0]["body"])
def test_failed_edit_after_a_refusal_keeps_the_run_alive(self):
gitea = app_gitea()
gitea.refuse_line_comments = True
gitea.edit_issue_comment = lambda comment_id, body: (_ for _ in ()).throw(OSError("down"))
outcome, gitea = finding_run([APP], gitea=gitea)
self.assertIs(outcome.result.verdict, Verdict.ORANGE)
self.assertEqual(outcome.result.exit_code, 0)
class ReuseTest(unittest.TestCase):
def test_open_earlier_line_comment_is_reused(self):
gitea = app_gitea()
gitea.reviews.append({"id": 7, "state": "COMMENT", "commit_id": "c" * 40, "user": {"login": BOT}})
url = "https://git.example.org/team/app/pulls/7/files#issuecomment-77"
gitea.line_comments[7] = [{"user": {"login": BOT}, "path": APP, "position": 42, "resolver": None, "html_url": url}]
finding_run([APP], gitea=gitea)
self.assertEqual(gitea.review_comments, [])
self.assertIn(f"still open in [an earlier line comment]({url})", gitea.comments[-1]["body"])
def earlier(self, body="**[high] Old wording** (werkrevisor)\n\nORANGE\n\nOld text."):
gitea = app_gitea()
gitea.reviews.append({"id": 7, "state": "COMMENT", "commit_id": "c" * 40, "user": {"login": BOT}})
gitea.line_comments[7] = [{"id": 77, "user": {"login": BOT}, "path": APP, "position": 42, "resolver": None, "body": body,
"html_url": "https://x/77"}]
return gitea
def edits_of_77(self, gitea):
return [body for comment_id, body in gitea.edited_comments if comment_id == 77]
def test_a_reused_line_comment_gets_the_new_text(self):
outcome, gitea = finding_run([APP], gitea=self.earlier())
[body] = self.edits_of_77(gitea)
self.assertTrue(body.startswith("**[high] Off by one** (werkrevisor)\n\nORANGE"))
self.assertIn("Breaks X.", body)
self.assertNotIn("Old text.", body)
self.assertIn(f"Updated by the review of `{HEAD}`.", body)
self.assertEqual(gitea.review_comments, [])
result = gitea.comments[-1]["body"]
self.assertIn("still open in [an earlier line comment](https://x/77)", result)
self.assertNotIn("Breaks X.", result)
def test_a_reused_line_comment_with_the_same_text_stays_untouched(self):
_, first = finding_run([APP])
text = first.review_comments[0][0]["body"]
updated = f"{text}\n\nUpdated by the review of `{'c' * 40}`."
normalized = updated.replace("\n", "\r\n").replace("Breaks X.", "Breaks X. ") + "\r\n"
for name, body in (("as posted", text), ("as updated before", updated),
("with CRLF and trailing blanks from Gitea", normalized)):
with self.subTest(name):
_, gitea = finding_run([APP], gitea=self.earlier(body))
self.assertEqual(self.edits_of_77(gitea), [])
self.assertNotIn("Breaks X.", gitea.comments[-1]["body"])
def test_the_new_text_goes_into_the_result_comment_if_the_line_comment_cannot_take_it(self):
gitea = self.earlier()
edit = gitea.edit_issue_comment
def refusing(comment_id, body):
if comment_id == 77:
raise OSError("HTTP 500")
return edit(comment_id, body)
gitea.edit_issue_comment = refusing
outcome, gitea = finding_run([APP], gitea=gitea)
self.assertEqual(outcome.result.exit_code, 0)
result = gitea.comments[-1]["body"]
self.assertIn("still open in [an earlier line comment](https://x/77); its text is not updated", result)
self.assertIn("Breaks X.", result)
_, gitea = finding_run([APP, APP], gitea=self.earlier())
self.assertEqual(len(self.edits_of_77(gitea)), 1)
result = gitea.comments[-1]["body"]
self.assertEqual(result.count("still open in [an earlier line comment](https://x/77)"), 2)
self.assertEqual(result.count("Breaks X."), 1)
def test_a_reply_in_the_thread_of_a_bot_review_is_not_reused(self):
gitea = self.earlier()
gitea.line_comments[7][0]["resolver"] = {"login": "alice"}
# Gitea lists a reply among the comments of the review that started the thread.
gitea.line_comments[7].append({"id": 78, "user": {"login": "alice"}, "path": APP, "position": 42,
"resolver": None, "body": "Fixed.", "html_url": "https://x/78"})
finding_run([APP], gitea=gitea)
self.assertEqual(len(gitea.review_comments[-1]), 1)
self.assertEqual([c for c, _ in gitea.edited_comments if c in (77, 78)], [])
def test_resolved_far_or_other_file_comments_are_not_reused(self):
for comment in ({"path": APP, "position": 40, "resolver": {"login": "alice"}},
{"path": APP, "position": 50, "resolver": None},
{"path": "other.py", "position": 40, "resolver": None},
{"path": APP, "position": 0, "resolver": None}):
with self.subTest(comment=comment):
gitea = app_gitea()
gitea.reviews.append({"id": 7, "state": "COMMENT", "commit_id": "c" * 40, "user": {"login": BOT}})
gitea.line_comments[7] = [{**comment, "user": {"login": BOT}, "html_url": "https://x/old"}]
finding_run([APP], gitea=gitea)
self.assertEqual(len(gitea.review_comments[-1]), 1)
class FoldTest(unittest.TestCase):
def earlier(self, sha="c" * 40, verdict="ORANGE"):
body = f"\n\n## 🟠werkrevisor: {verdict}\n\nOld text."
return {"id": 3, "user": {"login": BOT}, "body": body, "html_url": "https://x/3"}
def test_new_result_folds_the_earlier_ones(self):
gitea = app_gitea(comments=[self.earlier(), {"id": 4, "user": {"login": "alice"}, "body": ""}])
finding_run([APP], gitea=gitea)
[body] = [body for comment_id, body in gitea.edited_comments if comment_id == 3]
# The new result 5 is shortened once its line comment exists, then the earlier one is folded.
self.assertEqual([c for c, _ in gitea.edited_comments], [5, 3])
self.assertIn("🟠werkrevisor ORANGE for ccccccc, superseded by "
f"the result for "
f"{HEAD[:7]}
", body)
self.assertIn("Old text.", body)
self.assertTrue(body.rstrip().endswith(" "))
def test_a_mention_of_the_head_does_not_protect_a_comment_from_folding(self):
earlier = self.earlier()
earlier["body"] += f"\nMentions {HEAD}."
gitea = app_gitea(comments=[earlier])
finding_run([APP], gitea=gitea)
# 4 is the new result, shortened once its line comment exists.
self.assertEqual([c for c, _ in gitea.edited_comments], [4, 3])
def test_a_codex_comment_of_an_earlier_head_is_not_folded(self):
for marker in (f"", f""):
with self.subTest(marker=marker):
codex = {"id": 3, "user": {"login": BOT}, "body": f"{marker}\n\nCodex text."}
gitea = app_gitea(comments=[codex])
finding_run([APP], gitea=gitea)
# 4 is the new result, shortened once its line comment exists.
self.assertEqual([c for c, _ in gitea.edited_comments], [4])
def test_a_failed_fold_is_retried_by_a_rerun(self):
gitea = app_gitea(comments=[self.earlier()])
gitea.edit_issue_comment = lambda comment_id, body: (_ for _ in ()).throw(OSError("down"))
finding_run([APP], gitea=gitea)
del gitea.edit_issue_comment
finding_run([APP], gitea=gitea)
# The rerun reuses the result 4, which keeps the full findings since its shortening failed.
self.assertEqual([c for c, _ in gitea.edited_comments], [3])
def test_folded_comment_keeps_its_markers(self):
folded = report.fold(self.earlier()["body"], "https://x/9", HEAD)
self.assertTrue(folded.startswith(f"\n\n"
f"{report.FOLDED_MARKER}\n"))
self.assertEqual(report.find_existing([{"user": {"login": BOT}, "body": folded}], "c" * 40, BOT)[1],
Verdict.ORANGE)
self.assertIsNone(report.fold(folded, "https://x/10", HEAD))
self.assertIsNone(report.fold("a human comment", "https://x/10", HEAD))
self.assertIsNone(report.fold(f"\nCodex text.", "https://x/10", HEAD))
if __name__ == "__main__":
unittest.main()