246 lines
7.1 KiB
D
246 lines
7.1 KiB
D
module xdiff_unittests;
|
||
|
||
import std.algorithm : equal;
|
||
import std.array : appender;
|
||
import std.exception : assertThrown;
|
||
import std.range : iota, cycle, take;
|
||
import xdiff_mmfile : MMFile;
|
||
import xdiff_mmblocks : MMBlocks;
|
||
|
||
// -------------------- Вспомогалки --------------------
|
||
|
||
/// Сгенерировать массив длиной n, цикляя значения 0..239
|
||
ubyte[] genCycled(size_t n)
|
||
{
|
||
auto acc = appender!(ubyte[])();
|
||
foreach (v; iota(0, 240).cycle.take(n))
|
||
acc.put(cast(ubyte)v);
|
||
return acc.data;
|
||
}
|
||
|
||
/// Вытянуть контент MMBlocks в байты (не разрушая оригинал)
|
||
ubyte[] blocksToBytes(const MMBlocks b)
|
||
{
|
||
auto tmp = b.clone(); // глубокая копия
|
||
auto mf = MMFile.fromBlocksMoved(tmp);
|
||
return (cast(const(ubyte)[]) mf.asSlice()).dup;
|
||
}
|
||
|
||
/// Сравнить содержимое двух MMFile по байтам
|
||
bool sameBytes(const MMFile a, const MMFile b)
|
||
{
|
||
return a.asSlice().equal(b.asSlice());
|
||
}
|
||
|
||
// -------------------- ТЕСТЫ --------------------
|
||
|
||
unittest // new_empty
|
||
{
|
||
auto f = new MMFile;
|
||
assert(f.size() == 0);
|
||
assert(f.isCompact());
|
||
}
|
||
|
||
unittest // new_from_bytes
|
||
{
|
||
auto data = cast(ubyte[])"hello world";
|
||
auto f = MMFile.fromBytes(data);
|
||
assert(f.size() == data.length);
|
||
assert(f.isCompact());
|
||
assert(f.asSlice().equal(data));
|
||
}
|
||
|
||
unittest // large_from_bytes
|
||
{
|
||
auto data = genCycled(15_000);
|
||
auto f = MMFile.fromBytes(data);
|
||
assert(f.size() == data.length);
|
||
assert(f.isCompact());
|
||
assert(f.asSlice().equal(data));
|
||
}
|
||
|
||
unittest // "clone" для MMFile: через байты
|
||
{
|
||
auto data = genCycled(15_000);
|
||
auto f = MMFile.fromBytes(data);
|
||
auto f2 = MMFile.fromBytes(f.asSlice().dup);
|
||
assert(sameBytes(f, f2));
|
||
}
|
||
|
||
unittest // clone_blocks
|
||
{
|
||
auto data = genCycled(15_000);
|
||
auto b = MMBlocks.fromBytes(data);
|
||
auto b2 = b.clone(); // глубокий
|
||
auto bytes = blocksToBytes(b);
|
||
auto bytes2 = blocksToBytes(b2);
|
||
assert(bytes.equal(bytes2));
|
||
}
|
||
|
||
unittest // eq для MMFile (по байтам)
|
||
{
|
||
auto data = genCycled(15_000);
|
||
auto f = MMFile.fromBytes(data);
|
||
auto f2 = MMFile.fromBytes(data.dup);
|
||
assert(sameBytes(f, f2));
|
||
}
|
||
|
||
unittest // as_slice
|
||
{
|
||
assert((new MMFile).asSlice().length == 0);
|
||
|
||
auto data = genCycled(15_000);
|
||
auto f = MMFile.fromBytes(data);
|
||
assert(f.asSlice().equal(data));
|
||
}
|
||
|
||
unittest // as_slice_mut
|
||
{
|
||
auto empty = new MMFile;
|
||
assert(empty.asSlice().length == 0);
|
||
|
||
auto data = genCycled(15_000);
|
||
auto f = MMFile.fromBytes(data);
|
||
assert(f.asSlice()[0] == data[0]);
|
||
|
||
auto mut = f.asSliceMut();
|
||
mut[0] = cast(ubyte) (mut[0] + 1);
|
||
assert(f.asSlice()[0] == data[0] + 1);
|
||
}
|
||
|
||
unittest // diff_simple → проверяем applyPatch
|
||
{
|
||
auto data = cast(ubyte[])"hello world\n";
|
||
auto data2 = cast(ubyte[])"hello world!\n";
|
||
auto f = MMFile.fromBytes(data);
|
||
auto f2 = MMFile.fromBytes(data2);
|
||
|
||
auto patch = f.computePatch(f2);
|
||
auto r = f.applyPatch(patch);
|
||
assert(r.success);
|
||
assert(r.patched.asSlice().equal(data2));
|
||
assert(r.rejected.asSlice().length == 0);
|
||
}
|
||
|
||
unittest // diff_with_mutation
|
||
{
|
||
auto f = MMFile.fromBytes(cast(ubyte[])"hello world\n");
|
||
auto f2 = MMFile.fromBytes(cast(ubyte[])"hello world!\n");
|
||
|
||
// 1-й прогон
|
||
auto p1 = f.computePatch(f2);
|
||
auto r1 = f.applyPatch(p1);
|
||
assert(r1.success);
|
||
assert(r1.patched.asSlice().equal(f2.asSlice()));
|
||
|
||
// Меняем первую букву во втором файле
|
||
auto m = f2.asSliceMut();
|
||
m[0] = cast(ubyte)'j';
|
||
|
||
// 2-й прогон
|
||
auto p2 = f.computePatch(f2);
|
||
auto r2 = f.applyPatch(p2);
|
||
assert(r2.success);
|
||
assert(r2.patched.asSlice().equal(f2.asSlice()));
|
||
}
|
||
|
||
unittest // merge3_simple (без конфликтов)
|
||
{
|
||
auto base = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world\n");
|
||
auto a = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world changed\n");
|
||
auto b = MMFile.fromBytes(cast(ubyte[])"header_changed\nline2\nline3\nline4\nhello world\n");
|
||
|
||
auto lines = appender!(string[])();
|
||
auto rej = appender!(string[])();
|
||
|
||
int acceptCb(const(ubyte)[] s) nothrow
|
||
{
|
||
lines.put(cast(string) s.idup);
|
||
return 0;
|
||
}
|
||
int rejectCb(const(ubyte)[] s) nothrow
|
||
{
|
||
rej.put(cast(string) s.idup);
|
||
return 0;
|
||
}
|
||
|
||
base.merge3Raw(a, b, &acceptCb, &rejectCb);
|
||
|
||
auto expected = [
|
||
"header_changed\n",
|
||
"line2\n",
|
||
"line3\n",
|
||
"line4\n",
|
||
"hello world changed\n",
|
||
];
|
||
assert(lines.data.equal(expected));
|
||
assert(rej.data.length == 0);
|
||
}
|
||
|
||
unittest // merge3_conflicts (есть rejected)
|
||
{
|
||
auto base = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world\n");
|
||
auto a = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world changed\n");
|
||
auto b = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world also changed\n");
|
||
|
||
auto lines = appender!(string[])();
|
||
auto rej = appender!(string[])();
|
||
|
||
int acceptCb(const(ubyte)[] s) nothrow { lines.put(cast(string)s.idup); return 0; }
|
||
int rejectCb(const(ubyte)[] s) nothrow { rej.put(cast(string)s.idup); return 0; }
|
||
|
||
base.merge3Raw(a, b, &acceptCb, &rejectCb);
|
||
|
||
assert(lines.data.length > 0);
|
||
assert(rej.data.length > 0);
|
||
}
|
||
|
||
unittest // merge3_panic_* в Rust → у нас error-ветка через nothrow-коды: вернём -1
|
||
{
|
||
auto base = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world\n");
|
||
auto a = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world changed\n");
|
||
auto b = MMFile.fromBytes(cast(ubyte[])"header_changed\nline2\nline3\nline4\nhello world\n");
|
||
|
||
auto lines = appender!(string[])();
|
||
|
||
int acceptCb(const(ubyte)[] s) nothrow
|
||
{
|
||
static size_t cnt = 0;
|
||
++cnt;
|
||
if (cnt > 3) return -1; // сигнализируем об ошибке
|
||
lines.put(cast(string)s.idup);
|
||
return 0;
|
||
}
|
||
int rejectCb(const(ubyte)[] s) nothrow { return 0; }
|
||
|
||
assertThrown!Exception( base.merge3Raw(a, b, &acceptCb, &rejectCb) );
|
||
}
|
||
|
||
unittest // patch_simple (computePatch→applyPatch)
|
||
{
|
||
auto base = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world\n");
|
||
auto want = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world changed\n");
|
||
|
||
auto patch = base.computePatch(want);
|
||
auto res = base.applyPatch(patch);
|
||
|
||
assert(res.success);
|
||
assert(sameBytes(res.patched, want));
|
||
assert(res.rejected.asSlice().length == 0);
|
||
}
|
||
|
||
unittest // patch_reject (искажаем базу перед apply)
|
||
{
|
||
auto base = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world\n");
|
||
auto want = MMFile.fromBytes(cast(ubyte[])"header changed\nline2\nline3\nline4\nhello world changed\n");
|
||
|
||
auto patch = base.computePatch(want);
|
||
|
||
auto m = base.asSliceMut();
|
||
m[0] = cast(ubyte)'b';
|
||
|
||
auto res = base.applyPatch(patch);
|
||
|
||
assert(!res.success);
|
||
assert(res.rejected.asSlice().length > 0);
|
||
}
|