dxdiff/source/xdiff_unittests.d
2025-08-31 16:47:55 +03:00

246 lines
7.1 KiB
D
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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