From 7188aeb09d1dd25c6ce8d58c2eebe736d64ff4bb Mon Sep 17 00:00:00 2001 From: pyoor Date: Fri, 16 Oct 2026 14:23:13 -0400 Subject: [PATCH] feat: add argument to disable pernosco recordings --- src/bugmon/bugmon.py | 19 ++++++++- src/bugmon/main.py | 8 ++++ tests/test_bugmon.py | 98 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_main.py | 25 +++++++++++ uv.lock | 2 +- 5 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 tests/test_main.py diff --git a/src/bugmon/bugmon.py b/src/bugmon/bugmon.py index 50612ff..b9ee6bd 100644 --- a/src/bugmon/bugmon.py +++ b/src/bugmon/bugmon.py @@ -72,6 +72,7 @@ def __init__( working_dir: Path, pernosco_creds: Optional[PernoscoCreds] = None, dry_run: Optional[bool] = False, + disable_pernosco: bool = False, ) -> None: """Initializes new BugMonitor instance @@ -80,6 +81,7 @@ def __init__( :param working_dir: Path to working directory :param pernosco_creds: Optional pernosco credentials. :param dry_run: Boolean indicating if changes should be made to the bug + :param disable_pernosco: Defer pernosco recording and submission :raises BugmonException: If pernosco_creds is supplied but pernosco is not configured """ self.bugsy = bugsy @@ -92,7 +94,12 @@ def __init__( self.dry_run = dry_run self.pernosco_creds = pernosco_creds - if pernosco_creds is not None and not is_pernosco_available(): + self.disable_pernosco = disable_pernosco + if ( + not disable_pernosco + and pernosco_creds is not None + and not is_pernosco_available() + ): if not dry_run: raise BugmonException("pernosco-submit is not properly configured!") @@ -542,7 +549,11 @@ def needs_confirm(self) -> bool: return False def needs_pernosco(self) -> bool: - """Helper function to determine eligibility for 'pernosco'""" + """Helper function to determine eligibility for pernosco""" + return not self.disable_pernosco and self._has_pernosco_request() + + def _has_pernosco_request(self) -> bool: + """Determine whether a pernosco request is pending, even while disabled.""" if "pernosco-failed" in self.bug.commands: return False @@ -651,6 +662,10 @@ def process(self, force_confirm: bool = False) -> None: # Pernosco sessions can be recorded at any time if self.needs_pernosco(): self._pernosco() + elif self.disable_pernosco and self._has_pernosco_request(): + # Keep the request queued without posting the close-out comment. + self._close_bug = False + log.info("Deferring Pernosco request while Pernosco is disabled") # Post updates and comments self.commit() diff --git a/src/bugmon/main.py b/src/bugmon/main.py index 760d5de..aad39fa 100644 --- a/src/bugmon/main.py +++ b/src/bugmon/main.py @@ -43,6 +43,13 @@ def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace: help="Force bug confirmation regardless of status", ) + parser.add_argument( + "--disable-pernosco", + action="store_true", + default=os.getenv("DISABLE_PERNOSCO") == "1", + help="Defer Pernosco recording and submission (also DISABLE_PERNOSCO=1)", + ) + # Bug selection bugs = parser.add_mutually_exclusive_group(required=True) bugs.add_argument("--bugs", nargs="+", help="Space separated list of bug numbers") @@ -126,6 +133,7 @@ def main(argv: Optional[List[str]] = None) -> int: working_dir, pernosco_creds, args.dry_run, + disable_pernosco=args.disable_pernosco, ) log.info( f"Analyzing bug {bug.id} " diff --git a/tests/test_bugmon.py b/tests/test_bugmon.py index 39de099..3de693e 100644 --- a/tests/test_bugmon.py +++ b/tests/test_bugmon.py @@ -297,3 +297,101 @@ def test_bugmon_already_verified_but_not_branches(bugmon): "release": 78, } assert bugmon.needs_verify() is True + + +@pytest.mark.parametrize("request_kind", ["command", "keyword"]) +@pytest.mark.parametrize("action", ["confirm", "verify", "bisect", None]) +def test_disabled_pernosco_preserves_pending_work( + bugmon, mocker, request_kind, action, caplog +): + """Deferred recording preserves requests and suppresses close-out comments.""" + bugmon.disable_pernosco = True + if request_kind == "command": + bugmon.add_command("pernosco") + else: + bugmon.bug.keywords.append("pernosco-wanted") + mocker.patch.object(bugmon, "is_supported", return_value=True) + for candidate in ("verify", "confirm", "bisect"): + mocker.patch.object( + bugmon, f"needs_{candidate}", return_value=candidate == action + ) + + def finish_analysis(): + bugmon._close_bug = True + bugmon.report("Analysis completed.") + + methods = { + "confirm": "_confirm_open", + "verify": "_verify_fixed", + "bisect": "_bisect", + } + analysis = None + if action is not None: + analysis = mocker.patch.object( + bugmon, methods[action], side_effect=finish_analysis + ) + record = mocker.patch.object(bugmon, "_pernosco") + submit = mocker.patch("bugmon.bugmon.submit_pernosco") + bugmon.process() + + record.assert_not_called() + submit.assert_not_called() + assert "bugmon" in bugmon.bug.keywords + assert "pernosco-failed" not in bugmon.bug.commands + if request_kind == "command": + assert "pernosco" in bugmon.bug.commands + else: + assert "pernosco-wanted" in bugmon.bug.keywords + diff = bugmon.bug.diff() + if action is not None: + analysis.assert_called_once_with() + assert diff["comment"]["body"] == "Analysis completed." + else: + assert "comment" not in diff + assert "Removing bugmon" not in caplog.text + assert not bugmon.needs_pernosco() + bugmon.disable_pernosco = False + assert bugmon.needs_pernosco() + bugmon.process() + record.assert_called_once_with() + + +@pytest.mark.parametrize("terminal", ["unsupported", "failed"]) +def test_disabled_pernosco_retains_terminal_cleanup(bugmon, mocker, terminal): + """Requests that cannot be fulfilled do not prevent normal cleanup.""" + bugmon.disable_pernosco = True + bugmon.add_command("pernosco") + if terminal == "failed": + bugmon.add_command("pernosco-failed") + bugmon._close_bug = True + mocker.patch.object(bugmon, "is_supported", return_value=terminal != "unsupported") + for action in ("verify", "confirm", "bisect"): + mocker.patch.object(bugmon, f"needs_{action}", return_value=False) + record = mocker.patch.object(bugmon, "_pernosco") + bugmon.process() + record.assert_not_called() + assert "bugmon" not in bugmon.bug.keywords + assert "Removing bugmon keyword" in bugmon.bug.diff()["comment"]["body"] + + +def test_disabled_pernosco_skips_submit_validation( + bug, bugsy, working_dir, pernosco_creds, mocker +): + """Disabled recording does not require an installed submit utility.""" + available = mocker.patch("bugmon.bugmon.is_pernosco_available", return_value=False) + monitor = BugMonitor(bugsy, bug, working_dir, pernosco_creds, disable_pernosco=True) + available.assert_not_called() + assert monitor.needs_pernosco() is False + + +@pytest.mark.parametrize( + "resolution", ["DUPLICATE", "INVALID", "WORKSFORME", "WONTFIX"] +) +def test_disabled_pernosco_cleans_up_terminal_resolution(bugmon, resolution): + """Terminal resolutions retain normal cleanup despite a pending request.""" + bugmon.disable_pernosco = True + bugmon.add_command("pernosco") + bugmon.bug.resolution = resolution + bugmon.process() + assert "bugmon" not in bugmon.bug.keywords + assert "Removing bugmon keyword" in bugmon.bug.diff()["comment"]["body"] diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..f9538c1 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,25 @@ +"""Standalone CLI controls for temporarily deferring Pernosco.""" + +import pytest + +from bugmon.main import main, parse_args + + +@pytest.mark.parametrize("value, disabled", [(None, False), ("0", False), ("1", True)]) +def test_disable_pernosco_environment(monkeypatch, value, disabled): + monkeypatch.delenv("DISABLE_PERNOSCO", raising=False) + if value is not None: + monkeypatch.setenv("DISABLE_PERNOSCO", value) + assert parse_args(["--bugs", "123"]).disable_pernosco is disabled + assert parse_args(["--disable-pernosco", "--bugs", "123"]).disable_pernosco + + +def test_main_passes_disable_pernosco(monkeypatch, mocker, bug_data): + monkeypatch.setenv("BZ_API_ROOT", "https://example.invalid/rest") + monkeypatch.setenv("BZ_API_KEY", "key") + client = mocker.patch("bugmon.main.Bugsy").return_value + client.request.return_value = {"bugs": [bug_data]} + monitor = mocker.patch("bugmon.main.BugMonitor") + assert main(["--disable-pernosco", "--bugs", str(bug_data["id"])]) == 0 + assert monitor.call_args.kwargs["disable_pernosco"] is True + monitor.return_value.process.assert_called_once_with(False) diff --git a/uv.lock b/uv.lock index a8aa677..35a6aac 100644 --- a/uv.lock +++ b/uv.lock @@ -139,7 +139,7 @@ wheels = [ [[package]] name = "bugmon" -version = "5.3.0" +version = "5.3.2" source = { editable = "." } dependencies = [ { name = "autobisect" },