blob: e2abcd0b1907306be99a5ff9ef3f8e8a30e8d8af [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from magpie_fossil.cli import main
from magpie_fossil.client import FossilError
from magpie_fossil.forum import list_forum_threads, parse_forum_artifact, read_forum_thread
from magpie_fossil.ticket import get_ticket, list_tickets, submit_comment, submit_ticket
from magpie_fossil.wiki import list_wiki, read_wiki
# -- manifest parser tests -------------------------------------------------
def test_parse_forum_artifact() -> None:
manifest = (
"U alice\n"
"D 2026-07-02T12:34:56.789\n"
"W Thread Title\n"
"Z 1234567890abcdef\n"
"This is the forum body.\n"
"Line two.\n"
)
parsed = parse_forum_artifact(manifest)
assert parsed["author"] == "alice"
assert parsed["date"] == "2026-07-02 12:34:56.789"
assert parsed["title"] == "Thread Title"
assert parsed["parent"] is None
assert parsed["body"] == "This is the forum body.\nLine two."
def test_parse_forum_artifact_reply() -> None:
manifest = (
"U bob\nD 2026-07-02T13:45:00.000\nI root_uuid_123\nZ abcdef1234567890\nThis is a reply body.\n"
)
parsed = parse_forum_artifact(manifest)
assert parsed["author"] == "bob"
assert parsed["date"] == "2026-07-02 13:45:00.000"
assert parsed["title"] == ""
assert parsed["parent"] == "root_uuid_123"
assert parsed["body"] == "This is a reply body."
# -- ticket tests ----------------------------------------------------------
@patch("magpie_fossil.ticket.query_db")
def test_get_ticket_success(mock_query_db: MagicMock) -> None:
mock_query_db.side_effect = [
[
{
"tkt_id": 1,
"tkt_uuid": "abcdef123456",
"title": "Memory leak",
"status": "Open",
"comment": "Original body",
}
],
[{"comment": "Fix in progress", "login": "alice", "tkt_mtime": "2026-07-02 12:00:00"}],
]
tkt = get_ticket(Path("repo.fossil"), "abc")
assert tkt["tkt_id"] == 1
assert tkt["tkt_uuid"] == "abcdef123456"
assert len(tkt["comments"]) == 1
assert tkt["comments"][0]["body"] == "Fix in progress"
assert tkt["comments"][0]["author"] == "alice"
@patch("magpie_fossil.ticket.query_db")
def test_get_ticket_not_found(mock_query_db: MagicMock) -> None:
mock_query_db.return_value = []
with pytest.raises(FossilError, match="not found"):
get_ticket(Path("repo.fossil"), "abc")
@patch("magpie_fossil.ticket.query_db")
def test_get_ticket_ambiguous(mock_query_db: MagicMock) -> None:
mock_query_db.return_value = [{"tkt_uuid": "abc1"}, {"tkt_uuid": "abc2"}]
with pytest.raises(FossilError, match="Ambiguous UUID prefix"):
get_ticket(Path("repo.fossil"), "abc")
@patch("magpie_fossil.ticket.query_db")
def test_list_tickets(mock_query_db: MagicMock) -> None:
mock_query_db.return_value = [{"tkt_id": 1, "title": "T1"}]
res = list_tickets(Path("repo.fossil"))
assert len(res) == 1
assert res[0]["title"] == "T1"
@patch("magpie_fossil.ticket.run_fossil")
def test_submit_ticket(mock_run_fossil: MagicMock) -> None:
mock_run_fossil.return_value = "Created new ticket 1234567890abcdef12"
uuid = submit_ticket(Path("repo.fossil"), "T1", "Body", {"type": "Bug"})
assert uuid == "1234567890abcdef12"
mock_run_fossil.assert_called_once_with(
["ticket", "add", "-R", "repo.fossil", "--", "title", "T1", "comment", "Body", "type", "Bug"]
)
@patch("magpie_fossil.ticket.run_fossil")
@patch("magpie_fossil.ticket.query_db")
def test_submit_comment(mock_query_db: MagicMock, mock_run_fossil: MagicMock) -> None:
mock_query_db.return_value = [{"tkt_id": 1, "tkt_uuid": "abcdef123456"}]
mock_run_fossil.return_value = ""
uuid = submit_comment(Path("repo.fossil"), "abc", "My comment")
assert uuid == "abcdef123456"
mock_run_fossil.assert_called_once_with(
["ticket", "set", "abcdef123456", "-R", "repo.fossil", "--", "+comment", "My comment"]
)
# -- wiki tests ------------------------------------------------------------
@patch("magpie_fossil.wiki.run_fossil")
def test_list_wiki(mock_run_fossil: MagicMock) -> None:
mock_run_fossil.return_value = "Home\nDocs\n"
pages = list_wiki(Path("repo.fossil"))
assert pages == ["Home", "Docs"]
mock_run_fossil.assert_called_once_with(["wiki", "list", "-R", "repo.fossil"])
@patch("magpie_fossil.wiki.run_fossil")
def test_read_wiki(mock_run_fossil: MagicMock) -> None:
mock_run_fossil.return_value = "# Welcome"
content = read_wiki(Path("repo.fossil"), "Home")
assert content == "# Welcome"
mock_run_fossil.assert_called_once_with(["wiki", "export", "Home", "-R", "repo.fossil"])
# -- forum tests -----------------------------------------------------------
@patch("magpie_fossil.forum.run_fossil")
@patch("magpie_fossil.forum.query_db")
def test_list_forum_threads(mock_query_db: MagicMock, mock_run_fossil: MagicMock) -> None:
mock_query_db.return_value = [{"root_uuid": "uuid1"}]
mock_run_fossil.return_value = "U alice\nD 2026-07-02T12:00:00.000\nW Thread 1\nZ checksum\nBody content"
threads = list_forum_threads(Path("repo.fossil"))
assert len(threads) == 1
assert threads[0]["title"] == "Thread 1"
assert threads[0]["author"] == "alice"
@patch("magpie_fossil.forum.run_fossil")
@patch("magpie_fossil.forum.query_db")
def test_read_forum_thread(mock_query_db: MagicMock, mock_run_fossil: MagicMock) -> None:
mock_query_db.side_effect = [[{"rid": 10}], [{"uuid": "uuid1", "parent_uuid": None}]]
mock_run_fossil.return_value = "U bob\nD 2026-07-02T12:00:00.000\nW Thread 1\nZ checksum\nPost content"
posts = read_forum_thread(Path("repo.fossil"), "uuid1")
assert len(posts) == 1
assert posts[0]["author"] == "bob"
assert posts[0]["body"] == "Post content"
# -- CLI and client tests --------------------------------------------------
@patch("magpie_fossil.cli.find_repo_db")
@patch("magpie_fossil.cli.ticket.list_tickets")
def test_cli_ticket_list(
mock_list: MagicMock, mock_find: MagicMock, capsys: pytest.CaptureFixture[str]
) -> None:
mock_find.return_value = Path("repo.fossil")
mock_list.return_value = [{"tkt_uuid": "uuid1"}]
with patch("pathlib.Path.exists", return_value=True):
code = main(["ticket", "list"])
assert code == 0
captured = capsys.readouterr()
assert "uuid1" in captured.out
@patch("magpie_fossil.cli.find_repo_db")
def test_cli_no_repo(mock_find: MagicMock, capsys: pytest.CaptureFixture[str]) -> None:
mock_find.return_value = None
code = main(["ticket", "list"])
assert code == 1
captured = capsys.readouterr()
assert "Fossil repository database file not found" in captured.err
@patch("magpie_fossil.cli.find_repo_db")
@patch("magpie_fossil.cli.wiki.read_wiki")
def test_cli_wiki_read(
mock_read: MagicMock, mock_find: MagicMock, capsys: pytest.CaptureFixture[str]
) -> None:
mock_find.return_value = Path("repo.fossil")
mock_read.return_value = "# Markdown Content"
with patch("pathlib.Path.exists", return_value=True):
code = main(["wiki", "read", "Home"])
assert code == 0
captured = capsys.readouterr()
assert captured.out == "# Markdown Content"