<?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 as L;
use seekquarry\yioop\library\UnitTest;

/**
 * UtilityTest checks the parts of Utility.php that stand on their own.
 * What is here so far is the counting of log messages that would
 * otherwise be written thousands of times a second. A reader of a broken
 * index meets the same fault on every posting, and writing an entry for
 * each of them made the log the slow part of the machine. What is
 * checked here is that many occurrences cost one entry, that the entry
 * says how many there were and over what range of values, and that what
 * a process still holds is written before it stops.
 *
 * @author Chris Pollett
 */
class UtilityTest extends UnitTest
{
    /**
     * $held is what the logging settings were before a case changed
     * them, so tearDown can put them back.
     * @var array
     */
    public $held = [];
    /**
     * $log is the file the cases read back what was written from.
     * @var string
     */
    public $log = "";
    /**
     * setUp keeps the logging settings a case is about to change, sends
     * what is written to a log of its own, and empties that log.
     */
    public function setUp()
    {
        $this->held = ["NO_LOGGING" => $_SERVER["NO_LOGGING"] ?? null,
            "LOG_TO_FILES" => $_SERVER["LOG_TO_FILES"] ?? null,
            "NO_ROTATE_LOGS" => $_SERVER["NO_ROTATE_LOGS"] ?? null];
        unset($_SERVER["NO_LOGGING"]);
        $_SERVER["LOG_TO_FILES"] = true;
        $_SERVER["NO_ROTATE_LOGS"] = true;
        $this->log = C\LOG_DIR . "/counter_test.log";
        file_put_contents($this->log, "");
    }
    /**
     * tearDown puts the logging settings back and takes the log away.
     */
    public function tearDown()
    {
        @unlink($this->log);
        foreach ($this->held as $name => $value) {
            if ($value === null) {
                unset($_SERVER[$name]);
            } else {
                $_SERVER[$name] = $value;
            }
        }
    }
    /**
     * said gives back everything written to this case's log so far.
     *
     * @return string what the log holds
     */
    public function said()
    {
        return file_exists($this->log) ? file_get_contents($this->log) : "";
    }
    /**
     * manyOccurrencesCostOneEntryTestCase checks the whole point of
     * counting a message: a thousand occurrences inside the waiting time
     * write nothing at all.
     */
    public function manyOccurrencesCostOneEntryTestCase()
    {
        for ($i = 0; $i < 1000; $i++) {
            L\crawlLogCounter("a fault on every posting", "counter_test");
        }
        $this->assertEqual("", $this->said(),
            "a thousand occurrences inside the waiting time write nothing");
    }
    /**
     * waitOfNothingWritesASummaryTestCase checks the other end of the
     * waiting time: a message told to wait no seconds writes a summary
     * saying how many times it happened since the one before.
     */
    public function waitOfNothingWritesASummaryTestCase()
    {
        L\crawlLogCounter("a fault worth an entry", "counter_test", 0);
        L\crawlLogCounter("a fault worth an entry", "counter_test", 0);
        $lines = array_filter(explode("\n", trim($this->said())));
        $this->assertEqual(2, count($lines),
            "each occurrence writes one line and no more");
        $this->assertTrue(strpos($lines[1], "[1 times in 0 s]") !== false,
            "and a summary counts only what came after the one before");
    }
    /**
     * summaryReadsAsTheMessageDidTestCase checks that the message a
     * caller wrote is still what the log says, with the count and the
     * stretch added at the end.
     */
    public function summaryReadsAsTheMessageDidTestCase()
    {
        L\crawlLogCounter("Posting decode error", "counter_test", 0);
        $this->assertTrue(strpos($this->said(), "Posting decode error") !==
            false, "the message a caller wrote is what the log says");
    }
    /**
     * eachMessageIsCountedApartTestCase checks that two messages counted
     * at once do not run into each other, since a reader of a broken
     * index meets several faults and each has its own count.
     */
    public function eachMessageIsCountedApartTestCase()
    {
        L\crawlLogCounter("the first fault", "counter_test", 0);
        L\crawlLogCounter("the first fault", "counter_test", 0);
        L\crawlLogCounter("the second fault", "counter_test", 0);
        $said = $this->said();
        $this->assertEqual(2, substr_count($said, "the first fault ["),
            "the first message counts its own occurrences");
        $this->assertTrue(strpos($said, "the second fault [1 times") !==
            false, "and so does the second");
    }
    /**
     * nothingIsWrittenWhenLoggingIsOffTestCase checks that a counted
     * message obeys the same setting every other log message does.
     */
    public function nothingIsWrittenWhenLoggingIsOffTestCase()
    {
        $_SERVER["NO_LOGGING"] = true;
        L\crawlLogCounter("a fault nobody asked to hear about",
            "counter_test", 0);
        unset($_SERVER["NO_LOGGING"]);
        $this->assertEqual("", $this->said(),
            "a run that asked for no logging is given none");
    }
    /**
     * fileIsHandedToTheWebUserTestCase checks that a file written
     * before the server drops from root to the user it runs as is
     * given to that user. Without it the file belongs to root, and
     * every later run as that user finds a file it cannot write: the
     * tests run from the screen printed a warning for each attempt.
     */
    public function fileIsHandedToTheWebUserTestCase()
    {
        $file_name = C\WORK_DIRECTORY . "/handed_over_test.txt";
        file_put_contents($file_name, "a file");
        $this->assertTrue(!L\giveFileToWebUser(""),
            "a file that is not there is left alone");
        $done = L\giveFileToWebUser($file_name);
        $named = !empty(C\WWW_USER);
        $rooted = function_exists("posix_getuid") &&
            posix_getuid() === 0;
        if (!$named || !$rooted) {
            $this->assertTrue(!$done, "nothing is handed over where " .
                "no user is named or this process may not give it");
        } else {
            clearstatcache();
            $owner = posix_getpwuid(fileowner($file_name));
            $this->assertEqual(C\WWW_USER, $owner["name"],
                "the file belongs to the user the server runs as");
        }
        unlink($file_name);
    }
    /**
     * logMadeByRootIsWrittenAgainTestCase checks that a log file made
     * while running as root can be appended to by the user the site
     * runs as. Without the handing over the append fails and, being
     * unchecked, says nothing, so a program started from the machines
     * screen wrote no log at all while the same program run with sudo
     * wrote one.
     */
    public function logMadeByRootIsWrittenAgainTestCase()
    {
        $file_name = C\WORK_DIRECTORY . "/handover_log_test.log";
        file_put_contents($file_name, "made first\n");
        L\giveFileToWebUser($file_name);
        clearstatcache();
        $named = !empty(C\WWW_USER) && function_exists("posix_getuid") &&
            posix_getuid() === 0;
        if ($named) {
            $owner = posix_getpwuid(fileowner($file_name));
            $this->assertEqual(C\WWW_USER, $owner["name"],
                "the log belongs to the user the site runs as");
        }
        $added = file_put_contents($file_name, "and again\n",
            FILE_APPEND);
        $this->assertTrue($added > 0,
            "a later write to the log lands rather than failing");
        unlink($file_name);
    }
}
