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