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()