<?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
 *
 * This file contains unit tests of the PackedTableTools class
 *
 * @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\models\Model;
use seekquarry\yioop\library\UnitTest;
use seekquarry\yioop\library\data_structures as LDS;

/**
 * Used to test the PackedTableTools class. PackedTableTools are used for
 * reading and storing rows with respect to some signature
 */
 class PackedTableToolsTest extends UnitTest
{
    /**
     * Prefix of folders for testing PackedTable's
     */
    const TEST_DIR = C\TEST_DIR . '/test_files/packed_test';
    /**
     * Creates test directory for the tests
     */
    public function setUp()
    {
        if (!file_exists(self::TEST_DIR)) {
            mkdir(self::TEST_DIR);
        }
    }
    /**
     * After the tests delete test directory
     */
    public function tearDown()
    {
        $model = new Model();
        $model->db->unlinkRecursive(self::TEST_DIR);
    }
    /**
     * Auxiliary function used during test that takes a PackedTableTools
     * object instantiated according to some row signature and an array
     * rows in that signature and checks that for all rows packing the row
     * followed by unpacking it returns the original row. IF a row
     * fails a testCase message is output
     *
     * @param PackedTableTools $table_factory used for pack and unpacking rows
     * @param array $rows rows according to $table_factory's signature
     */
    public function checkPackUnpackTableRows($table_factory, $rows)
    {
        foreach($rows as $row) {
            $packed_row = $table_factory->pack($row);
            $pack_unpack_row = $table_factory->unpack($packed_row);
            $id = $row['ID'];
            unset($row['ID']);
            $this->assertEqual($row, $pack_unpack_row[0],
                "Row $id pack unpack produces same.");
        }
    }
    /**
     * This tests packing and unpacking rows according to a PackedTableTools
     * signature in the case that there is only one record associated with the
     * primary key (no-clustering).
     */
    public function packUnpackSingleRowTestCase()
    {
        $table_factory = new LDS\PackedTableTools(
            ["PRIMARY KEY" => "ID", "A" => "INT", "B" => "TEXT",
             "C" => "INT"]);
        $rows = [
            ["ID" => 1, "A" => 5, "B" => "Hello World", "C"=> 256],
            ["ID" => 2, "A" => 65000, "B" => "yo", "C"=> 2000000]
        ];
        $this->checkPackUnpackTableRows($table_factory, $rows);
        $table_factory = new LDS\PackedTableTools(
            ["PRIMARY KEY" => "ID", "A" => "DOUBLE", "B" => "TEXT",
             "C" => "TEXT", "D" => "BOOL", "E" => "INT"]);
        $rows = [
            ["ID" => 1, "A" => 1.59, "B" => "Bong", "C" => "yockledoo",
                "D" => true, "E" => 1000],
            ["ID" => 2, "A" => 100.4, "B" => "Blah", "C" => "doodoo",
                "D" => false, "E" => 9990],
        ];
        $this->checkPackUnpackTableRows($table_factory, $rows);
    }
    /**
     * This tests packing and unpacking rows according to a PackedTableTools
     * signature in the case that where more than one record can be associated
     * with the primary key (clustering).
     */
    public function packUnpackMultiRowTestCase()
    {
        $table_factory = new LDS\PackedTableTools(
            ["PRIMARY KEY" => "ID", "A" => "INT", "B" => "TEXT",
             "C" => "INT"]);
        $rows = [
            ["A" => 5, "B" => "Hello World", "C"=> 256],
            ["A" => 20000, "B" => "laladida", "C"=> 5600]
        ];
        $packed_rows = $table_factory->pack($rows);
        $pack_unpack_rows = $table_factory->unpack($packed_rows);
        $this->assertEqual($rows, $pack_unpack_rows,
            "Two row pack unpack produces same.");
    }
    /**
     * This tests if a set of rows packed according to a PackedTableTools record
     * is saved that it can be reloaded and unpacked and still get the same
     * results
     */
    public function saveAddLoadTestCase()
    {
        $table_factory = new LDS\PackedTableTools(
            ["PRIMARY KEY" => "ID", "A" => "INT", "B" => "TEXT",
             "C" => "INT"]);
        $table_factory2 = new LDS\PackedTableTools(
            ["PRIMARY KEY" => "ID", "A" => "INT", "B" => "TEXT",
             "C" => "INT"], C\NS_COMPRESSORS . "GzipCompressor");
        $table = [];
        $hash_key1 = md5("1", true);
        $table_factory->add($table, $hash_key1,
            $table_factory->pack(["A" => 5, "B" => "Hello World", "C"=> 256]),
            mode: LDS\PackedTableTools::APPEND_MODE);
        $table_factory->add($table, $hash_key1,
            $table_factory->pack(["A" => 6, "B" => "Hello World2", "C"=> 257]),
            mode: LDS\PackedTableTools::APPEND_MODE);
        $hash_key2 = md5("2", true);
        $table_factory->add($table, $hash_key2,
            $table_factory->pack(["A" => 20000, "B" => "laladida",
            "C"=> 5600]));
        $table_factory->save(self::TEST_DIR . "/save.txt", $table);
        $loaded_table = $table_factory->load(self::TEST_DIR . "/save.txt");
        $this->assertEqual($table, $loaded_table,
            "Add two rows, save, load table gives same result.");
        $table_factory2->save(self::TEST_DIR . "/save2.txt", $table);
        $loaded_table2 = $table_factory2->load(self::TEST_DIR . "/save2.txt");
        $this->assertEqual($table, $loaded_table2,
            "Add two rows, save, load compressed table gives same result.");
        $entry1 = $loaded_table[$hash_key1];
        $start = 0;
        $num_entries = L\vByteDecode($entry1, $start);
        $this->assertEqual($num_entries, 2,
            "Row one has correct number of sub-entries.");
    }
    /**
     * Tests that the in-memory table cache stays within its byte budget,
     * drops the tables used longest ago first, spares a table that was just
     * used, and drops a table's entry markers in step with the table. This
     * guards the fix for the slow web-server memory growth that came from
     * the cache holding every partition it was ever asked for.
     */
    public function tableCacheBoundTestCase()
    {
        $signature = ["PRIMARY KEY" => "ID", "A" => "INT", "B" => "TEXT"];
        $tools = new LDS\PackedTableTools($signature);
        $paths = [];
        for ($i = 1; $i <= 4; $i++) {
            $one_table = [];
            $tools->add($one_table, md5((string)$i, true),
                $tools->pack(["A" => $i, "B" => "row for cache table $i"]));
            $paths[$i] = self::TEST_DIR . "/cache_$i.txt";
            $tools->save($paths[$i], $one_table);
        }
        $sample = $tools->load($paths[1],
            LDS\PackedTableTools::AS_STRING_MODE, true);
        $entry_size = strlen($sample);
        $tools = new LDS\PackedTableTools($signature);
        $tools->max_table_cache_bytes = 2 * $entry_size + 1;
        foreach ([1, 2, 3] as $number) {
            $tools->load($paths[$number],
                LDS\PackedTableTools::AS_STRING_MODE, true);
        }
        $this->assertTrue(
            $tools->table_cache_bytes <= $tools->max_table_cache_bytes,
            "Cache bytes stay within the budget after loading three tables.");
        $this->assertEqual(count($tools->table_cache), 2,
            "Only the two most recent tables stay cached.");
        $this->assertTrue(!isset($tools->table_cache[L\crawlHash($paths[1])]),
            "The least recently used table was evicted.");
        $tools->load($paths[2],
            LDS\PackedTableTools::AS_STRING_MODE, true);
        $tools->load($paths[4],
            LDS\PackedTableTools::AS_STRING_MODE, true);
        $this->assertTrue(!isset($tools->table_cache[L\crawlHash($paths[3])]),
            "Using a table spares it and the next oldest goes instead.");
        $this->assertTrue(isset($tools->table_cache[L\crawlHash($paths[2])]),
            "The recently used table is kept.");
        $tight = new LDS\PackedTableTools($signature);
        $tight->max_table_cache_bytes = $entry_size + 1;
        $tight->getEntryMarkers($paths[1]);
        $this->assertTrue(
            isset($tight->table_entry_markers[L\crawlHash($paths[1])]),
            "Markers are cached when a table is read.");
        $tight->getEntryMarkers($paths[2]);
        $this->assertTrue(
            !isset($tight->table_entry_markers[L\crawlHash($paths[1])]),
            "Evicting a table drops its markers too.");
    }
}
