<?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 <http://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\library\GitDelta;
use seekquarry\yioop\library\UnitTest;
/**
* Checks that the short description of how two versions of some content
* differ can be read back to give exactly the newer version, for text, for
* binary content, and for the awkward cases at the edges of the format.
*
* @author Chris Pollett
*/
class GitDeltaTest extends UnitTest
{
/**
* Nothing needs standing up: the maker and applier work on strings
* handed to them, touching neither disk nor database.
*/
public function setUp()
{
}
/**
* Nothing was stood up, so nothing needs taking down.
*/
public function tearDown()
{
}
/**
* A version rebuilt from a delta is exactly the version it was made
* from, whether the change is an edit in the middle, an addition at the
* end, or a truncation.
*/
public function textRoundTripTestCase()
{
$source = str_repeat("a line of ordinary source code\n", 120);
$edited = substr($source, 0, 1500) . "an inserted line\n" .
substr($source, 1500);
$appended = $source . "a line added at the end\n";
$shortened = substr($source, 0, 900);
$versions = ["edited" => $edited, "appended" => $appended,
"shortened" => $shortened];
foreach ($versions as $name => $target) {
$delta = GitDelta::create($source, $target);
$this->assertTrue($delta !== null,
"a delta is worth making for the $name version");
$this->assertEqual(GitDelta::apply($source, $delta), $target,
"the $name version is rebuilt exactly");
}
}
/**
* Content that is not text rebuilds exactly too, since the description
* is written in terms of bytes rather than lines.
*/
public function binaryRoundTripTestCase()
{
$source = "";
for ($i = 0; $i < 8000; $i++) {
$source .= chr($i % 256);
}
$target = substr($source, 0, 3000) . "\x00\x01\xff\xfe" .
substr($source, 3000);
$delta = GitDelta::create($source, $target);
$this->assertTrue($delta !== null,
"a delta is worth making for changed binary content");
$this->assertEqual(GitDelta::apply($source, $delta), $target,
"binary content is rebuilt exactly");
$this->assertTrue(strlen($delta) < strlen($target) / 10,
"the delta is much smaller than the content it describes");
}
/**
* Two versions with nothing in common are not worth describing as a
* difference, so no delta is made and the caller sends the content
* whole.
*/
public function unrelatedContentDeclinedTestCase()
{
$source = str_repeat("aaaabbbbccccdddd", 200);
$target = "";
for ($i = 0; $i < 3200; $i++) {
$target .= chr(($i * 7 + 13) % 256);
}
$delta = GitDelta::create($source, $target);
$this->assertTrue($delta === null,
"unrelated content is sent whole rather than as a delta");
}
/**
* An empty version on either side is sent whole, there being nothing to
* describe a difference against.
*/
public function emptyContentDeclinedTestCase()
{
$this->assertTrue(GitDelta::create("", "something") === null,
"nothing to build against means no delta");
$this->assertTrue(GitDelta::create("something", "") === null,
"nothing to build means no delta");
}
/**
* A version identical to the one before it is described by copy steps
* alone, so the delta is a few bytes whatever the content's length.
*/
public function identicalContentTestCase()
{
$source = str_repeat("unchanged content\n", 300);
$delta = GitDelta::create($source, $source);
$this->assertTrue($delta !== null,
"an unchanged version is worth describing as a delta");
$this->assertTrue(strlen($delta) < 50,
"an unchanged version needs only a few bytes to describe");
$this->assertEqual(GitDelta::apply($source, $delta), $source,
"an unchanged version is rebuilt exactly");
}
/**
* A damaged description is refused rather than acted on: one whose copy
* step reaches past the end of the version it is built against, and one
* that does not produce the length it declares.
*/
public function damagedDeltaRefusedTestCase()
{
$source = str_repeat("source content\n", 100);
$delta = GitDelta::create($source, $source . "tail\n");
$damaged = substr($delta, 0, strlen($delta) - 3);
$refused = false;
try {
GitDelta::apply($source, $damaged);
} catch (\Exception $exception) {
$refused = true;
}
$this->assertTrue($refused,
"a delta cut short is refused rather than acted on");
$refused = false;
try {
GitDelta::apply("short", $delta);
} catch (\Exception $exception) {
$refused = true;
}
$this->assertTrue($refused,
"a delta built against other content is refused");
}
}