SeekquarrySeekquarry - Yioop Repo - Seekquarry

/ tests / WikiPagesFileTest.php⤓
<?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\WikiPagesFile;
use seekquarry\yioop\library as L;
use seekquarry\yioop\models\ImpressionModel;
use seekquarry\yioop\models\Model;
use seekquarry\yioop\models\WikiModel;
use seekquarry\yioop\models\datasources\Sqlite3Manager;

/**
 * WikiPagesFileTest checks that the WikiPagesFile class, which reads and
 * writes files of exported wiki pages, reads such a file the way PHP would
 * if the file were run. createdb, import-public-help and GroupWikiTool's
 * yioop import all read pages this way now, so a page the reader split
 * or cut short would reach every new site wrong.
 *
 * @author Chris Pollett
 */
class WikiPagesFileTest extends UnitTest
{
    /**
     * TEST_FILE is the file of pages written and read back by these cases
     */
    const TEST_FILE = __DIR__ . '/test_files/wiki_pages_file_test.php';
    /**
     * MANY_PAGES is the number of pages eachPageHoldsOnePageAtATimeTestCase
     * writes, enough that reading them all at once would show in memory.
     */
    const MANY_PAGES = 400;
    /**
     * PAGE_BYTES is about how many bytes long each of those pages is.
     */
    const PAGE_BYTES = 25000;
    /**
     * setUp does nothing, since nothing needs making before these cases.
     */
    public function setUp()
    {
    }
    /**
     * tearDown removes the file of pages a case wrote.
     */
    public function tearDown()
    {
        if (file_exists(self::TEST_FILE)) {
            unlink(self::TEST_FILE);
        }
    }
    /**
     * arraysFromRunning gives the arrays of pages that PHP makes when it
     * runs a file of pages, so a case can compare them with what the reader
     * finds.
     *
     * @param string $file_name the file to run
     * @return array the arrays the file sets, array name first, keeping
     *      only the arrays of pages
     */
    public function arraysFromRunning($file_name)
    {
        $public_pages = [];
        $help_pages = [];
        require $file_name;
        return ["public_pages" => $public_pages,
            "help_pages" => $help_pages];
    }
    /**
     * shippedFileReadsAsRunTestCase reads the file of public and help
     * pages that Yioop ships, src/configs/PublicHelpPages.php, both as text
     * and by running it, and checks the two give the same pages. Any page whose
     * text the reader ends early or runs past would differ here.
     */
    public function shippedFileReadsAsRunTestCase()
    {
        $file_name = C\BASE_DIR . "/configs/PublicHelpPages.php";
        $ran = $this->arraysFromRunning($file_name);
        $read = WikiPagesFile::read($file_name);
        $this->assertTrue(count($ran["help_pages"], COUNT_RECURSIVE) > 0,
            "the shipped file holds help pages");
        $this->assertEqual($read["public_pages"], $ran["public_pages"],
            "the public pages read as text match running the file");
        $this->assertEqual($read["help_pages"], $ran["help_pages"],
            "the help pages read as text match running the file");
    }
    /**
     * writtenPagesReadBackTestCase writes pages whose text looks like the
     * file's own lines and reads them back. A line that looks like the start of
     * a page stays in the page it is in, as does a line where EOD begins a
     * longer word. A page with a line PHP would take as its end, the end line
     * itself or EOD after spaces and before other text, as PHP 7.3 and later
     * end on, is written with another end mark and reads back whole.
     */
    public function writtenPagesReadBackTestCase()
    {
        $looks_like_start = "Before\n\$pages[\"en-US\"][\"Other\"] = " .
            "<<< 'EOD'\nAfter\nEODS; are fine";
        $pages = ["en-US" => ["Main" => "=Main=\n\nText.",
            "Lookalike" => $looks_like_start,
            "Holds_End" => "Before\nEOD;\nAfter",
            "Holds_Both" => "Before\nEOD;\n EOD_1\nAfter",
            "Indented_End" => "Before\n  EOD and more\nAfter"],
            "fr-FR" => ["Main" => "=Principale=\n"]];
        WikiPagesFile::write(self::TEST_FILE,
            ["pages" => ["Test pages", $pages]], "Test Pages",
            "WikiPagesFileTest");
        $read = WikiPagesFile::read(self::TEST_FILE);
        $this->assertEqual($read, ["pages" => $pages],
            "every page reads back as written, those holding an end line too");
        $this->assertTrue(@token_get_all(file_get_contents(self::TEST_FILE),
            TOKEN_PARSE) !== false, "the file is still valid PHP");
    }
    /**
     * eachPageHoldsOnePageAtATimeTestCase writes a file of 400 pages of
     * 25,000 bytes each, 10 MB in all, as a site's export of many pages
     * can be, and goes through it with eachPage. Every page comes out
     * whole and in order, and the memory in use never grows by more than
     * a few pages' worth, since import-public-help, which reads such a
     * file, ran out of memory when the whole file was held at once.
     */
    public function eachPageHoldsOnePageAtATimeTestCase()
    {
        $pages = [];
        for ($i = 0; $i < self::MANY_PAGES; $i++) {
            $pages["en-US"]["Page_$i"] = str_repeat("line $i\n",
                intdiv(self::PAGE_BYTES, strlen("line $i\n")));
        }
        WikiPagesFile::write(self::TEST_FILE,
            ["public_pages" => ["Test pages", $pages]], "Test Pages",
            "WikiPagesFileTest");
        $expected_names = array_keys($pages["en-US"]);
        $pages = null;
        $start = memory_get_usage();
        $largest = 0;
        $names = [];
        $whole = true;
        foreach (WikiPagesFile::eachPage(self::TEST_FILE) as [$array_name,
            $locale_tag, $page_name, $page]) {
            $names[] = $page_name;
            $whole = $whole && str_starts_with($page, "line ") &&
                strlen($page) > self::PAGE_BYTES - strlen("line 999\n");
            $largest = max($largest, memory_get_usage() - $start);
        }
        $this->assertEqual([$names, $whole], [$expected_names, true],
            "every page comes out whole and in order");
        $this->assertTrue($largest < 8 * self::PAGE_BYTES,
            "the memory in use grows by a few pages, not by the file");
    }
    /**
     * pageTimesReadBackTestCase checks that the time written for a page
     * comes back with that page from eachPage, that a page written with no
     * time comes back with -1, and that a time is not handed to the page
     * after the one it was written for. An import uses the time to date
     * each page as the exporting site had it dated.
     */
    public function pageTimesReadBackTestCase()
    {
        $pages = ["en-US" => ["Dated" => "text a", "Undated" => "text b",
            "repo$$1" => "an issue"]];
        $pubdates = ["en-US" => ["Dated" => 1100000000,
            "repo$$1" => 1200000000]];
        WikiPagesFile::write(self::TEST_FILE,
            ["public_pages" => ["Test pages", $pages, $pubdates]],
            "Test Pages", "WikiPagesFileTest");
        $read = [];
        foreach (WikiPagesFile::eachPage(self::TEST_FILE) as [, , $page_name,
            $page, $pubdate]) {
            $read[$page_name] = [$page, $pubdate];
        }
        $this->assertEqual($read, ["Dated" => ["text a", 1100000000],
            "Undated" => ["text b", -1], "repo$$1" => ["an issue",
            1200000000]], "each page comes back with its own time or -1");
    }
    /**
     * pageCommentsReadBackTestCase checks that the posts written for a
     * page's thread come back with that page from eachPage, together with
     * the page's time, that a page written with no posts comes back with
     * null, and that the posts are not handed out as a page of their own.
     */
    public function pageCommentsReadBackTestCase()
    {
        $pages = ["en-US" => ["Talked" => "text a", "Quiet" => "text b"]];
        $pubdates = ["en-US" => ["Talked" => 1100000000]];
        $posts = [["user_name" => "writer", "title" => "a title",
            "description" => "two\nlines and EOD;", "pubdate" => 5]];
        $comments = ["en-US" => ["Talked" => $posts]];
        WikiPagesFile::write(self::TEST_FILE, ["public_pages" =>
            ["Test pages", $pages, $pubdates, $comments]], "Test Pages",
            "WikiPagesFileTest");
        $read = [];
        foreach (WikiPagesFile::eachPage(self::TEST_FILE) as [$array_name,
            , $page_name, $page, $pubdate, $page_comments]) {
            $read["$array_name $page_name"] = [$page, $pubdate,
                $page_comments];
        }
        $this->assertEqual($read, [
            "public_pages Talked" => ["text a", 1100000000, $posts],
            "public_pages Quiet" => ["text b", -1, null]],
            "each page comes back with its own posts or null");
    }
    /**
     * historyAndPartsReadBackTestCase checks that the revisions written for
     * a page come back with that page from eachPage, that the kinds of data
     * a file is written as carrying come back from exportedParts, that a
     * file saying nothing of them gives just issue_pages, as files written
     * before they were said carried, and that an array whose pages are
     * handed to write by a generator, as a group's discussions are, is
     * written and read like any other.
     */
    public function historyAndPartsReadBackTestCase()
    {
        $revisions = [["pubdate" => 1, "user_name" => "writer",
            "edit_comment" => "first", "page" => "old\ntext"]];
        $threads = (function () {
            yield ["thread", "7", '[{"description":"a post"}]'];
        })();
        WikiPagesFile::write(self::TEST_FILE, [
            "pages" => ["Test pages", ["en-US" => ["Kept" => "new text"]],
                [], [], fn($locale_tag, $page_name) => $revisions],
            "discussions" => ["Test threads", $threads]], "Test Pages",
            "WikiPagesFileTest", ["history", "discussions"]);
        $read = [];
        foreach (WikiPagesFile::eachPage(self::TEST_FILE) as [$array_name,
            , $page_name, $page, , , $history]) {
            $read["$array_name $page_name"] = [$page, $history];
        }
        $this->assertEqual($read, ["pages Kept" => ["new text", $revisions],
            "discussions 7" => ['[{"description":"a post"}]', null]],
            "the page comes back with its revisions, the thread as written");
        $this->assertEqual(WikiPagesFile::exportedParts(self::TEST_FILE),
            ["history", "discussions"], "the file says what it carries");
        WikiPagesFile::write(self::TEST_FILE, ["pages" => ["Test pages",
            ["en-US" => ["Kept" => "text"]]]], "Test Pages",
            "WikiPagesFileTest");
        $this->assertEqual(WikiPagesFile::exportedParts(self::TEST_FILE),
            ["issue_pages"], "a file that says nothing carried issue pages");
    }
    /**
     * importedPageLinksReachTheirPagesTestCase checks that a page written
     * by the import, upgradePublicHelpWiki, is saved with the link address
     * WikiModel::WIKI_LINK_BASE in the Help group as well as in the Public
     * group. A page saved with no address, as the import did, drew a link
     * such as [[Demos]] as href="Demos", which reaches no page; with the
     * address, the link is drawn to the page Demos of the page's own group
     * whichever controller draws it. The case writes one page into each of
     * the two groups of a copy, in the tests folder, of the database Yioop
     * ships in src/data, and deletes them again.
     */
    public function importedPageLinksReachTheirPagesTestCase()
    {
        require_once C\BASE_DIR . "/library/UpgradeFunctions.php";
        $page_name = "Zz_Link_Test";
        WikiPagesFile::write(self::TEST_FILE, [
            "public_pages" => ["Test pages",
                ["en-US" => [$page_name => "[[Demos]]"]]],
            "help_pages" => ["Test pages",
                ["en-US" => [$page_name => "[[Demos]]"]]]],
            "Test Pages", "WikiPagesFileTest");
        $folder = C\TEST_DIR . "/test_files/wiki_pages_file_import";
        $model = new Model();
        $model->db->unlinkRecursive($folder);
        L\makePath($folder);
        $database = "$folder/public" . uniqid() . ".db";
        copy(C\BASE_DIR . "/data/public_default.db", $database);
        $wiki_model = new WikiModel();
        $wiki_model->db = new Sqlite3Manager();
        $wiki_model->db->connect("", "", "", $database);
        $wiki_model->resource_path = "$folder/resources";
        ImpressionModel::$log_folder = "$folder/impressions";
        L\upgradePublicHelpWiki($wiki_model->db, true, self::TEST_FILE);
        $linked = [];
        foreach ([C\PUBLIC_GROUP_ID, C\HELP_GROUP_ID] as $group_id) {
            $result = $wiki_model->db->execute("SELECT PAGE FROM " .
                "GROUP_PAGE WHERE GROUP_ID = ? AND TITLE = ? AND " .
                "LOCALE_TAG = 'en-US'", [$group_id, $page_name]);
            $row = ($result) ? $wiki_model->db->fetchArray($result) : false;
            $wiki_model->deleteGroupPage($group_id, $page_name, "en-US");
            $linked[] = str_contains($row['PAGE'] ?? "",
                WikiModel::WIKI_LINK_BASE . "Demos");
        }
        $wiki_model->db->disconnect();
        ImpressionModel::$log_folder = C\LOG_DIR . "/" .
            ImpressionModel::IMPRESSION_FOLDER;
        $model->db->unlinkRecursive($folder);
        $this->assertEqual($linked, [true, true],
            "the link is built from the link address in both groups");
    }
    /**
     * oneGroupImportLeavesTheOtherGroupTestCase checks that a file of just
     * the Public group's pages, as an export with -help writes, declares
     * only that group, and that importing it with wipe leaves the Help
     * group's pages as they were while the Public group keeps just the
     * file's page. Before, the wipe deleted every Help page. The shipped
     * file declares both groups. The case works on a copy, in the tests
     * folder, of the database Yioop ships in src/data.
     */
    public function oneGroupImportLeavesTheOtherGroupTestCase()
    {
        require_once C\BASE_DIR . "/library/UpgradeFunctions.php";
        $this->assertEqual(WikiPagesFile::exportedGroups(C\BASE_DIR .
            "/configs/PublicHelpPages.php"), ["public_pages", "help_pages"],
            "the shipped file declares both groups");
        $page_name = "Zz_One_Group_Test";
        WikiPagesFile::write(self::TEST_FILE, [
            "public_pages" => ["Test pages",
                ["en-US" => [$page_name => "Only public"]]]],
            "Test Pages", "WikiPagesFileTest", ["comments", "history"]);
        $this->assertEqual(WikiPagesFile::exportedGroups(self::TEST_FILE),
            ["public_pages"], "a one group file declares that group");
        $folder = C\TEST_DIR . "/test_files/wiki_pages_file_one_group";
        $model = new Model();
        $model->db->unlinkRecursive($folder);
        L\makePath($folder);
        $database = "$folder/public" . uniqid() . ".db";
        copy(C\BASE_DIR . "/data/public_default.db", $database);
        $wiki_model = new WikiModel();
        $wiki_model->db = new Sqlite3Manager();
        $wiki_model->db->connect("", "", "", $database);
        $wiki_model->resource_path = "$folder/resources";
        ImpressionModel::$log_folder = "$folder/impressions";
        $count = function ($group_id) use ($wiki_model) {
            $result = $wiki_model->db->execute("SELECT COUNT(*) AS NUM " .
                "FROM GROUP_PAGE WHERE GROUP_ID = ?", [$group_id]);
            $row = ($result) ? $wiki_model->db->fetchArray($result) : [];
            return intval($row['NUM'] ?? -1);
        };
        $help_before = $count(C\HELP_GROUP_ID);
        L\upgradePublicHelpWiki($wiki_model->db, true, self::TEST_FILE, true);
        $help_after = $count(C\HELP_GROUP_ID);
        $public_after = $count(C\PUBLIC_GROUP_ID);
        $wiki_model->db->disconnect();
        ImpressionModel::$log_folder = C\LOG_DIR . "/" .
            ImpressionModel::IMPRESSION_FOLDER;
        $model->db->unlinkRecursive($folder);
        $this->assertTrue($help_before > 0, "the copy holds Help pages");
        $this->assertEqual($help_after, $help_before,
            "the Help group is left as it was");
        $this->assertEqual($public_after, 1,
            "the Public group holds just the file's page");
    }
}