<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\tests;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library\UnitTest;
use seekquarry\yioop\library\wiki\WikiIssue;
use seekquarry\yioop\models\WikiModel;
/**
* Tests for WikiIssue, which shapes the record of a single tracked issue.
* Each test starts from a freshly opened issue and checks that opening,
* assigning, closing, reopening, and re-prioritizing set the right fields
* and add the right history entry, and that closing with an unknown reason
* falls back to fixed.
*
* @author Chris Pollett
*/
class WikiIssueTest extends UnitTest
{
/**
* Nothing to set up; every test builds its own issue record.
*/
public function setUp()
{
}
/**
* Nothing to tear down.
*/
public function tearDown()
{
}
/**
* A freshly opened issue is open, unassigned, starts at middle urgency
* because the reporter does not pick one, keeps its reporter, title,
* branch, and version, and has a single opened history entry.
*/
public function openTestCase()
{
$issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
$this->assertEqual(WikiIssue::STATUS_OPEN, $issue["status"],
"a new issue is open");
$this->assertEqual(7, $issue["reporter"],
"the reporter is remembered");
$this->assertEqual(WikiIssue::PRIORITY_MEDIUM, $issue["priority"],
"a new issue starts at middle urgency");
$this->assertEqual("Login bug", $issue["title"],
"the title is remembered");
$this->assertEqual("main", $issue["branch"],
"the branch is remembered");
$this->assertEqual("current", $issue["version"],
"the version is remembered");
$this->assertEqual(0, $issue["assignee"],
"a new issue is not assigned to anyone");
$this->assertEqual(1, count($issue["history"]),
"a new issue has one history entry");
$this->assertEqual(WikiIssue::ACTION_OPENED,
$issue["history"][0]["action"],
"the one history entry records the opening");
}
/**
* Assigning an issue records the assignee and adds an assigned history
* entry naming who made the change and who received the issue.
*/
public function assignTestCase()
{
$issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
$issue = WikiIssue::assign($issue, 9, 3, 1100);
$this->assertEqual(9, $issue["assignee"],
"the issue is now assigned to person nine");
$last = end($issue["history"]);
$this->assertEqual(WikiIssue::ACTION_ASSIGNED, $last["action"],
"the change is recorded as an assignment");
$this->assertEqual(3, $last["by"],
"the person who made the change is recorded");
$this->assertEqual(9, $last["to"],
"the person the issue went to is recorded");
}
/**
* Closing an issue as fixed marks it closed with that reason and adds a
* closed history entry.
*/
public function closeFixedTestCase()
{
$issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
$issue = WikiIssue::close($issue, WikiIssue::RESOLUTION_FIXED, 3,
1200);
$this->assertEqual(WikiIssue::STATUS_CLOSED, $issue["status"],
"the issue is now closed");
$this->assertEqual(WikiIssue::RESOLUTION_FIXED, $issue["resolution"],
"the issue is recorded as fixed");
$last = end($issue["history"]);
$this->assertEqual(WikiIssue::ACTION_CLOSED, $last["action"],
"the change is recorded as a closing");
}
/**
* Closing an issue as won't-fix keeps that reason.
*/
public function closeWontFixTestCase()
{
$issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
$issue = WikiIssue::close($issue, WikiIssue::RESOLUTION_WONT_FIX, 3,
1200);
$this->assertEqual(WikiIssue::RESOLUTION_WONT_FIX,
$issue["resolution"], "the won't-fix reason is kept");
}
/**
* Closing an issue with an unrecognized reason falls back to fixed
* rather than storing the bad reason.
*/
public function closeUnknownReasonTestCase()
{
$issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
$issue = WikiIssue::close($issue, "banana", 3, 1200);
$this->assertEqual(WikiIssue::RESOLUTION_FIXED, $issue["resolution"],
"an unknown closing reason becomes fixed");
}
/**
* Reopening a closed issue makes it open again and clears the reason it
* had been closed.
*/
public function reopenTestCase()
{
$issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
$issue = WikiIssue::close($issue, WikiIssue::RESOLUTION_FIXED, 3,
1200);
$issue = WikiIssue::reopen($issue, 3, 1300);
$this->assertEqual(WikiIssue::STATUS_OPEN, $issue["status"],
"the issue is open again");
$this->assertEqual("", $issue["resolution"],
"the old closing reason is cleared");
$last = end($issue["history"]);
$this->assertEqual(WikiIssue::ACTION_REOPENED, $last["action"],
"the reopening is recorded");
}
/**
* Changing an issue's priority updates it and records the change.
*/
public function setPriorityTestCase()
{
$issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
$issue = WikiIssue::setPriority($issue, 5, 3, 1400);
$this->assertEqual(5, $issue["priority"],
"the new priority is stored");
$last = end($issue["history"]);
$this->assertEqual(WikiIssue::ACTION_PRIORITY, $last["action"],
"the priority change is recorded");
}
/**
* Closing an issue as fixed with a commit keeps the commit on the record
* and in the history entry.
*/
public function closeWithCommitTestCase()
{
$issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
$issue = WikiIssue::close($issue, WikiIssue::RESOLUTION_FIXED, 3,
1200, "abc123");
$this->assertEqual("abc123", $issue["fix_commit"],
"the fixing commit is kept on the record");
$last = end($issue["history"]);
$this->assertEqual("abc123", $last["commit"],
"the fixing commit is kept in the history entry");
}
/**
* The display status reads reported, then assigned, then marked fixed,
* and reported again once reopened.
*/
public function displayStatusTestCase()
{
$issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
$this->assertEqual(WikiIssue::DISPLAY_REPORTED,
WikiIssue::displayStatus($issue),
"a new issue reads as reported");
$issue = WikiIssue::assign($issue, 9, 3, 1100);
$this->assertEqual(WikiIssue::DISPLAY_ASSIGNED,
WikiIssue::displayStatus($issue),
"an assigned issue reads as assigned");
$issue = WikiIssue::close($issue, WikiIssue::RESOLUTION_FIXED, 3,
1200);
$this->assertEqual(WikiIssue::DISPLAY_FIXED,
WikiIssue::displayStatus($issue),
"a fixed issue reads as marked fixed");
$issue = WikiIssue::reopen($issue, 3, 1300);
$this->assertEqual(WikiIssue::DISPLAY_ASSIGNED,
WikiIssue::displayStatus($issue),
"a reopened issue that still has an owner reads as assigned");
}
/**
* Putting an issue back to reported opens it, clears the assignee, and
* clears any closing reason.
*/
public function reportTestCase()
{
$issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
$issue = WikiIssue::assign($issue, 9, 3, 1100);
$issue = WikiIssue::close($issue, WikiIssue::RESOLUTION_FIXED, 3,
1200);
$issue = WikiIssue::report($issue, 3, 1300);
$this->assertEqual(WikiIssue::STATUS_OPEN, $issue["status"],
"the issue is open again");
$this->assertEqual(0, $issue["assignee"],
"the issue is held by no one");
$this->assertEqual(WikiIssue::DISPLAY_REPORTED,
WikiIssue::displayStatus($issue),
"the issue reads as reported");
}
/**
* Checks that the time an issue was reported is read off the opening
* line of its history, and that a record with no history reads as
* zero rather than failing.
*/
public function reportedTimeTestCase()
{
$record = WikiIssue::open(5, 1234, "a title", "main", "abc");
$this->assertEqual(WikiIssue::reportedTime($record), 1234,
"the reported time is the first history line's time");
$this->assertEqual(WikiIssue::reportedTime([]), 0,
"a record with no history reports zero");
}
/**
* Checks that the reporter shown for an issue is the username when an
* account reported it, and the handle typed in when nobody signed in
* did. A handle is preferred even if a username is somehow also given,
* since only an anonymous report carries one.
*/
public function reporterNameTestCase()
{
$record = WikiIssue::open(5, 1000, "a title", "main", "abc");
$this->assertEqual(WikiIssue::reporterName($record, "alice"),
"alice", "an account's report shows that account's name");
$this->assertEqual(WikiIssue::reporterName($record, ""), "",
"no account and no handle shows nothing");
$record["handle"] = "sam@example.com";
$this->assertEqual(WikiIssue::reporterName($record, ""),
"sam@example.com", "a report with no account shows its handle");
$this->assertEqual(WikiIssue::reporterName($record, "alice"),
"sam@example.com", "a handle is preferred to a username");
}
/**
* The user answerable for an issue's status follows the status: the
* reporter for one merely reported, the assignee for one assigned, and
* whoever last closed it for one fixed or won't be fixed.
*/
public function statusUserTestCase()
{
$issue = WikiIssue::open(7, 1000, "Login bug", "main", "current");
$this->assertEqual(7, WikiIssue::statusUser($issue),
"a reported issue points at its reporter");
$assigned = WikiIssue::assign($issue, 9, 7, 1100);
$this->assertEqual(9, WikiIssue::statusUser($assigned),
"an assigned issue points at its assignee");
$closed = WikiIssue::close($issue, WikiIssue::RESOLUTION_FIXED, 12,
1200);
$this->assertEqual(12, WikiIssue::statusUser($closed),
"a closed issue points at whoever closed it");
}
/**
* labelsTestCase checks that setLabels cleans the names, drops empty
* and repeated ones, notes the change in the history and keeps the
* labels as the record's last field, and that labelsLast moves them
* back there after a field is added.
*/
public function labelsTestCase()
{
$record = WikiIssue::open(5, 1000, "Crash", "master", "abc");
$this->assertEqual(($record["labels"] ?? []), [],
"a new issue has no labels");
$record = WikiIssue::setLabels($record, ["bug", " ui work ",
"bug", "%\"", "caf\u{e9}"], 7, 1100);
$this->assertEqual(($record["labels"] ?? []),
["bug", "ui work", "caf\u{e9}"],
"names are cleaned and empty and repeated ones dropped");
$this->assertEqual(array_key_last($record), "labels",
"the labels are the last field");
$this->assertEqual(end($record["history"])["action"],
WikiIssue::ACTION_LABELS, "the change is in the history");
$record["description"] = "added after";
$this->assertEqual(array_key_last(WikiIssue::labelsLast($record)),
"labels", "labelsLast moves the labels to the end");
}
/**
* labelPatternFindsOnlyIssuesWithTheLabelTestCase stores issue records
* the way WikiModel does, with quotation marks written as entities, in
* a database held in memory, and checks that the label filter's LIKE
* pattern finds just the issues carrying the label: not a longer label
* that starts the same, and not the word in a title or the history.
*/
public function labelPatternFindsOnlyIssuesWithTheLabelTestCase()
{
$db = new \SQLite3(":memory:");
$db->exec("CREATE TABLE GROUP_PAGE (ID INTEGER, PAGE TEXT)");
$records = [
1 => ["bug"],
2 => ["bug fix", "ui"],
3 => [],
4 => ["ui", "caf\u{e9}"],
];
foreach ($records as $id => $labels) {
$record = WikiIssue::open(5, 1000, "bug in caf\u{e9}", "master",
"abc");
$record = WikiIssue::setLabels($record, ["bug"], 7, 1100);
$record = WikiIssue::setLabels($record, $labels, 7, 1200);
$page = str_replace('"', """, json_encode(
WikiIssue::labelsLast($record)));
$statement = $db->prepare("INSERT INTO GROUP_PAGE VALUES (?, ?)");
$statement->bindValue(1, $id);
$statement->bindValue(2, $page);
$statement->execute();
}
$found = function ($label) use ($db) {
$statement = $db->prepare("SELECT ID FROM GROUP_PAGE WHERE " .
"PAGE LIKE ? ORDER BY ID");
$statement->bindValue(1, WikiModel::gitIssueLabelPattern($label));
$result = $statement->execute();
$ids = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$ids[] = $row["ID"];
}
return $ids;
};
$this->assertEqual($found("bug"), [1],
"only the issue labeled bug, not bug fix, a title or history");
$this->assertEqual($found("ui"), [2, 4],
"a label anywhere in the list");
$this->assertEqual($found("caf\u{e9}"), [4],
"a label with a letter outside ASCII");
$db->close();
}
/**
* issuePageMarksHideLabelListTestCase checks that the mark naming a
* repository's label list page, as in yioop$*, is among the marks
* WikiModel uses to leave issue pages out of a group's page list and to
* delete them with the repository page. The label list page showed in
* the page list before it was added.
*/
public function issuePageMarksHideLabelListTestCase()
{
$this->assertTrue(in_array(C\GIT_ISSUE_LABEL_SEPARATOR,
WikiModel::GIT_ISSUE_PAGE_MARKS),
"the label list page is left out of the page list");
$this->assertEqual(count(WikiModel::GIT_ISSUE_PAGE_MARKS), 4,
"issues, waiting issues, bans and the label list");
}
}