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

/**
 * Tests the reading of a blob out of a partition file, where the length
 * comes from the bundle's own index. A read asks the memory manager for
 * the whole length before it reads anything, so a length that is wrong
 * asks for that much memory whatever the file holds, and a long running
 * server near its memory limit dies of one such read.
 *
 * @author Chris Pollett
 */
class PartitionDocumentBundleTest extends UnitTest
{
    /**
     * $folder stores where a case writes the partition file it reads.
     * @var string
     */
    public $folder = "";
    /**
     * $bundle stores the bundle a case reads through.
     * @var object
     */
    public $bundle = null;
    /**
     * setUp makes a partition file holding a hundred bytes, which is the
     * file every case here reads from.
     */
    public function setUp()
    {
        $this->folder = C\WORK_DIRECTORY . "/temp/blobread" . getmypid();
        if (!file_exists($this->folder)) {
            mkdir($this->folder, 0777, true);
            @chmod($this->folder, 0777);
        }
        $this->bundle = new PartitionDocumentBundle($this->folder,
            ["PRIMARY KEY" => "KEY", "VALUE" => "BLOB"]);
        file_put_contents($this->folder . "/partition0.txt",
            str_repeat("small", 20));
        @chmod($this->folder . "/partition0.txt", 0777);
    }
    /**
     * tearDown takes away the partition file and the folder a case made.
     */
    public function tearDown()
    {
        if (!file_exists($this->folder)) {
            return;
        }
        foreach (glob($this->folder . "/*") as $one) {
            unlink($one);
        }
        rmdir($this->folder);
    }
    /**
     * anHonestLengthIsReadWholeTestCase checks that a length matching
     * what the file holds is read as it always was, so the guard on the
     * other lengths does not cost an ordinary read anything.
     */
    public function anHonestLengthIsReadWholeTestCase()
    {
        $file = $this->folder . "/partition0.txt";
        $said = PartitionDocumentBundle::lengthWorthReading($file, 0, 100);
        $this->assertEqual(100, $said,
            "a length the file can hold is left as it is");
        $said = PartitionDocumentBundle::lengthWorthReading($file, 50, 50);
        $this->assertEqual(50, $said,
            "and so is one that reaches exactly the end");
    }
    /**
     * lengthPastTheFileIsCutToWhatIsThereTestCase checks that a length
     * running past the end of the file is cut to what is left, so the
     * read asks for that much and no more.
     */
    public function lengthPastTheFileIsCutToWhatIsThereTestCase()
    {
        $file = $this->folder . "/partition0.txt";
        $said = PartitionDocumentBundle::lengthWorthReading($file, 0, 400);
        $this->assertEqual(100, $said,
            "a length past the end is cut to what the file holds");
        $said = PartitionDocumentBundle::lengthWorthReading($file, 200, 50);
        $this->assertEqual(0, $said,
            "and an offset past the end asks for nothing at all");
    }
    /**
     * lengthPastWhatABlobMayBeIsRefusedTestCase checks that a length
     * larger than any blob is refused outright. Such a length asks the
     * memory manager for that much before the file is touched, which is
     * how a server near its limit is ended by one corrupt entry.
     */
    public function lengthPastWhatABlobMayBeIsRefusedTestCase()
    {
        $file = $this->folder . "/partition0.txt";
        $said = PartitionDocumentBundle::lengthWorthReading($file, 0,
            C\MAX_BLOB_READ_LEN + 1);
        $this->assertEqual(0, $said,
            "a length past the most a blob may be asks for nothing");
        $said = PartitionDocumentBundle::lengthWorthReading($file, 0,
            291504352);
        $this->assertEqual(0, $said,
            "and so does the length that ended the live server");
    }
    /**
     * blobIsReadWithoutAskingForMoreThanTheFileHoldsTestCase reads
     * through the bundle itself and checks how much memory the read
     * asks for. A read of two hundred and seventy-eight megabytes from
     * a file of a hundred bytes used to ask for all of it.
     */
    public function blobIsReadWithoutAskingForMoreThanTheFileHoldsTestCase()
    {
        $file = $this->folder . "/partition0.txt";
        $before = memory_get_peak_usage(true);
        $said = $this->bundle->getArchive($file, 0, 291504352);
        $grew = memory_get_peak_usage(true) - $before;
        $this->assertTrue($grew < C\MAX_BLOB_READ_LEN,
            "the read asks for less than the most a blob may be, and " .
            "asked for " . $grew . " bytes");
        $this->assertTrue($said === false || strlen($said) <= 100,
            "and hands back nothing more than the file holds");
    }
}
