diff --git a/tools/fossil/src/magpie_fossil/client.py b/tools/fossil/src/magpie_fossil/client.py index de198453c..489ae7c29 100644 --- a/tools/fossil/src/magpie_fossil/client.py +++ b/tools/fossil/src/magpie_fossil/client.py @@ -19,6 +19,7 @@ from __future__ import annotations +import contextlib import os import sqlite3 import subprocess @@ -67,13 +68,12 @@ def find_repo_db(start_dir: Path) -> Path | None: db_path = d / marker if db_path.exists(): try: - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - cursor.execute("SELECT value FROM vvar WHERE name = 'repository'") - row = cursor.fetchone() - conn.close() - if row: - return Path(row[0]) + with contextlib.closing(sqlite3.connect(db_path)) as conn: + cursor = conn.cursor() + cursor.execute("SELECT value FROM vvar WHERE name = 'repository'") + row = cursor.fetchone() + if row: + return Path(row[0]) except sqlite3.Error: pass return None @@ -82,12 +82,11 @@ def find_repo_db(start_dir: Path) -> Path | None: def query_db(repo_path: Path, query: str, params: Sequence[Any] = ()) -> list[dict[str, Any]]: """Execute a read-only SQL query against the Fossil SQLite database.""" try: - conn = sqlite3.connect(repo_path) - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - cursor.execute(query, params) - rows = cursor.fetchall() - conn.close() - return [dict(r) for r in rows] + with contextlib.closing(sqlite3.connect(repo_path)) as conn: + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute(query, params) + rows = cursor.fetchall() + return [dict(r) for r in rows] except sqlite3.Error as exc: raise FossilError(f"Failed to query Fossil SQLite database: {exc}") from exc diff --git a/tools/fossil/src/magpie_fossil/forum.py b/tools/fossil/src/magpie_fossil/forum.py index cd97769b7..15e7fff7f 100644 --- a/tools/fossil/src/magpie_fossil/forum.py +++ b/tools/fossil/src/magpie_fossil/forum.py @@ -85,8 +85,13 @@ def list_forum_threads(repo_path: Path) -> list[dict[str, Any]]: "date": parsed["date"], } ) - except Exception: - pass + except Exception as exc: + import sys + + print( + f"Warning: skipped corrupt or missing forum post artifact '{root_uuid}': {exc}", + file=sys.stderr, + ) return threads @@ -129,6 +134,11 @@ def read_forum_thread(repo_path: Path, thread_uuid: str) -> list[dict[str, Any]] "body": parsed["body"], } ) - except Exception: - pass + except Exception as exc: + import sys + + print( + f"Warning: skipped corrupt or missing forum post artifact '{post_uuid}': {exc}", + file=sys.stderr, + ) return posts diff --git a/tools/fossil/src/magpie_fossil/ticket.py b/tools/fossil/src/magpie_fossil/ticket.py index 4edbdcd64..02f00d9ee 100644 --- a/tools/fossil/src/magpie_fossil/ticket.py +++ b/tools/fossil/src/magpie_fossil/ticket.py @@ -49,9 +49,10 @@ def get_ticket(repo_path: Path, tkt_uuid: str) -> dict[str, Any]: mtime = chng.get("tkt_mtime") or chng.get("mtime") if c_text: comments.append({"id": idx + 1, "body": c_text, "author": user, "date": mtime}) - except Exception: - # If ticket_chng is missing/different, we still return the ticket without history comments - pass + except FossilError as exc: + import sys + + print(f"Warning: failed to query ticket comments (schema might be custom): {exc}", file=sys.stderr) # If the main ticket body has a comment, and no change comments were found, # we can treat that as the initial comment @@ -81,11 +82,10 @@ def list_tickets(repo_path: Path) -> list[dict[str, Any]]: def submit_ticket(repo_path: Path, title: str, body: str, extra_fields: dict[str, str] | None = None) -> str: """Create a new ticket using Fossil CLI.""" - args = ["ticket", "add", "title", title, "comment", body] + args = ["ticket", "add", "-R", str(repo_path), "--", "title", title, "comment", body] if extra_fields: for k, v in extra_fields.items(): args.extend([k, v]) - args.extend(["-R", str(repo_path)]) # Fossil ticket add outputs: "Created new ticket " out = run_fossil(args) @@ -102,10 +102,9 @@ def update_ticket_fields(repo_path: Path, tkt_uuid: str, fields: dict[str, str]) tkt = get_ticket(repo_path, tkt_uuid) full_uuid = tkt["tkt_uuid"] - args = ["ticket", "set", full_uuid] + args = ["ticket", "set", full_uuid, "-R", str(repo_path), "--"] for k, v in fields.items(): args.extend([k, v]) - args.extend(["-R", str(repo_path)]) run_fossil(args) return full_uuid diff --git a/tools/fossil/tests/test_fossil.py b/tools/fossil/tests/test_fossil.py index c889e3c3d..e2abcd0b1 100644 --- a/tools/fossil/tests/test_fossil.py +++ b/tools/fossil/tests/test_fossil.py @@ -111,7 +111,7 @@ def test_submit_ticket(mock_run_fossil: MagicMock) -> None: uuid = submit_ticket(Path("repo.fossil"), "T1", "Body", {"type": "Bug"}) assert uuid == "1234567890abcdef12" mock_run_fossil.assert_called_once_with( - ["ticket", "add", "title", "T1", "comment", "Body", "type", "Bug", "-R", "repo.fossil"] + ["ticket", "add", "-R", "repo.fossil", "--", "title", "T1", "comment", "Body", "type", "Bug"] ) @@ -123,7 +123,7 @@ def test_submit_comment(mock_query_db: MagicMock, mock_run_fossil: MagicMock) -> uuid = submit_comment(Path("repo.fossil"), "abc", "My comment") assert uuid == "abcdef123456" mock_run_fossil.assert_called_once_with( - ["ticket", "set", "abcdef123456", "+comment", "My comment", "-R", "repo.fossil"] + ["ticket", "set", "abcdef123456", "-R", "repo.fossil", "--", "+comment", "My comment"] ) diff --git a/tools/vcs/tests/test_vcs.py b/tools/vcs/tests/test_vcs.py index e280e27b6..96eac8c16 100644 --- a/tools/vcs/tests/test_vcs.py +++ b/tools/vcs/tests/test_vcs.py @@ -309,3 +309,58 @@ def test_cli_unimplemented_backend_errors(tmp_path: Path, capsys: pytest.Capture def test_cli_cat(git_repo: Path, capsys: pytest.CaptureFixture[str]) -> None: assert main(["-C", str(git_repo), "cat", "HEAD", "file.txt"]) == 0 assert capsys.readouterr().out == "hello\n" + + +def test_fossil_parser_status(monkeypatch: pytest.MonkeyPatch) -> None: + def mock_run(args: list[str], cwd: str | None = None, **kwargs: object) -> str: + if "changes" in args: + return "EDITED file1.txt\nADDED file2.txt\nDELETED file3.txt\n" + if "extras" in args: + return "untracked1.txt\nuntracked2.txt\n" + return "" + + monkeypatch.setattr("magpie_vcs._run", mock_run) + backend = FossilBackend(Path("/tmp/repo")) + status_out = backend.status() + assert "M file1.txt" in status_out + assert "A file2.txt" in status_out + assert "D file3.txt" in status_out + assert "? untracked1.txt" in status_out + assert "? untracked2.txt" in status_out + + +def test_fossil_parser_status_error(monkeypatch: pytest.MonkeyPatch) -> None: + def mock_run(args: list[str], cwd: str | None = None, **kwargs: object) -> str: + raise VCSError("command failed") + + monkeypatch.setattr("magpie_vcs._run", mock_run) + backend = FossilBackend(Path("/tmp/repo")) + assert backend.status() == "" + + +def test_fossil_parser_log(monkeypatch: pytest.MonkeyPatch) -> None: + sample_timeline = """ +=== 2026-07-08 === +17:00:00 [a1b2c3d4e5f6] Initial commit (user: arnav tags: trunk) +16:30:00 [f6e5d4c3b2a1] Fix a bug in forum post rendering (user: jsmith tags: trunk) +16:00:00 [1234567890ab] Add a new feature [options] (user: alice tags: trunk) +""" + + def mock_run(args: list[str], cwd: str | None = None, **kwargs: object) -> str: + if "timeline" in args: + return sample_timeline + return "" + + monkeypatch.setattr("magpie_vcs._run", mock_run) + backend = FossilBackend(Path("/tmp/repo")) + + # Test basic log + log_out = backend.log() + assert "a1b2c3d4e5f6 Initial commit" in log_out + assert "f6e5d4c3b2a1 Fix a bug in forum post rendering" in log_out + assert "1234567890ab Add a new feature [options]" in log_out + + # Test grep search + grep_out = backend.log(grep="bug") + assert "f6e5d4c3b2a1 Fix a bug in forum post rendering" in grep_out + assert "a1b2c3d4e5f6" not in grep_out