Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/bugmon/bugmon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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!")

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down
8 changes: 8 additions & 0 deletions src/bugmon/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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} "
Expand Down
98 changes: 98 additions & 0 deletions tests/test_bugmon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
25 changes: 25 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading