<?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);
}
}