diff --git a/dub.json b/dub.json index 90807f2..d9c5233 100644 --- a/dub.json +++ b/dub.json @@ -16,33 +16,5 @@ "repository": "git+https://git.zhirov.kz/dlang/xdiff.git", "version": "e2396bc172eba813cdcd1a96c494e35d687f576a" } - }, - "configurations": [ - { - "name": "lib", - "targetType": "library", - "sourcePaths": [ - "source" - ], - "excludedSourceFiles": [ - "source/app.d" - ] - }, - { - "name": "app", - "targetType": "executable", - "mainSourceFile": "source/app.d", - "sourcePaths": [ - "source" - ] - } - ], - "buildTypes": { - "unittest": { - "dflags": [ - "-unittest", - "-g" - ] - } } } \ No newline at end of file diff --git a/dub.selections.json b/dub.selections.json deleted file mode 100644 index 0b7884b..0000000 --- a/dub.selections.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "fileVersion": 1, - "versions": { - "xdiff": {"version":"e2396bc172eba813cdcd1a96c494e35d687f576a","repository":"git+https://git.zhirov.kz/dlang/xdiff.git"} - } -} diff --git a/source/app.d b/source/app.d index aaec5e6..30e9f34 100644 --- a/source/app.d +++ b/source/app.d @@ -1,16 +1,25 @@ -import std.stdio; -import xdiff_mmfile : MMFile; +import std.stdio : writeln, stderr; +import std.exception : collectException; -void main() +import diff; + +int main(string[] args) { - auto a = MMFile.fromBytes(cast(ubyte[])"hello world\n"); - auto b = MMFile.fromBytes(cast(ubyte[])"hello world!\n"); + if (args.length != 3) + { + stderr.writeln("use: ", args[0], " OLD NEW"); + return 1; + } - auto patch = a.computePatch(b); - writeln("patch size: ", patch.size()); + auto eng = new DiffEngine(3); - auto res = a.applyPatch(patch); - writeln("apply success: ", res.success); - write(res.patched.asSlice()); // печатаем результат - writeln(); + string result; // сюда положим результат + auto ex = collectException(result = eng.diffFiles(args[1], args[2])); + if (ex !is null) + { + stderr.writeln(ex.msg); + return 2; + } + writeln(result); + return 0; } diff --git a/source/diff.d b/source/diff.d new file mode 100644 index 0000000..d3b1310 --- /dev/null +++ b/source/diff.d @@ -0,0 +1,197 @@ +module diff; + +import std.exception : enforce; +import std.file : read, exists; // бинарное чтение +import std.array : Appender, appender; +import std.string : representation, format; +import std.datetime : Clock, UTC; + +import core.stdc.string : memcpy; +import core.stdc.stdlib : malloc, free, realloc; + +import xdiff; + +//----------------------------- +// Аллокатор для libxdiff (обязателен на ряде сборок) +//----------------------------- +private extern (C) void* _wrap_malloc(void*, uint size) +{ + return malloc(size); +} + +private extern (C) void _wrap_free(void*, void* ptr) +{ + free(ptr); +} + +private extern (C) void* _wrap_realloc(void*, void* ptr, uint size) +{ + return realloc(ptr, size); +} + +private __gshared bool _allocatorReady = false; + +private void ensureAllocator() +{ + if (_allocatorReady) + return; + memallocator_t malt; + malt.priv = null; + malt.malloc = &_wrap_malloc; + malt.free = &_wrap_free; + malt.realloc = &_wrap_realloc; + enforce(xdl_set_allocator(&malt) == 0, "xdl_set_allocator failed"); + _allocatorReady = true; +} + +//----------------------------- +// RAII для mmfile_t +//----------------------------- +private struct MmFile +{ + mmfile_t mf; + + static MmFile fromBytes(const(ubyte)[] data, + long bsize = 8 * 1024, + ulong flags = XDL_MMF_ATOMIC) + { + ensureAllocator(); // критично: до init/writeallocate + MmFile m; + enforce(xdl_init_mmfile(&m.mf, bsize, flags) == 0, "xdl_init_mmfile failed"); + + if (data.length) + { + auto dst = cast(char*) xdl_mmfile_writeallocate(&m.mf, data.length); + enforce(dst !is null, "xdl_mmfile_writeallocate failed"); + memcpy(dst, data.ptr, data.length); + } + return m; + } + + ~this() + { + xdl_free_mmfile(&mf); + } +} + +//----------------------------- +// Синк-коллектор вывода libxdiff +//----------------------------- +private class BufferSink +{ + Appender!(ubyte[]) outbuf; + + this() + { + outbuf = appender!(ubyte[])(); + } + + extern (C) static int writeCB(void* priv, mmbuffer_t* mb, int nbuf) + { + auto self = cast(BufferSink) priv; + foreach (i; 0 .. nbuf) + { + size_t sz = cast(size_t) mb[i].size; + if (sz == 0) + continue; + self.outbuf.put((cast(ubyte*) mb[i].ptr)[0 .. sz]); + } + return 0; + } + + string toStringOwned() @trusted + { + return cast(string) outbuf.data.idup; + } +} + +//----------------------------- +// Публичный движок unified-diff +//----------------------------- +final class DiffEngine +{ + private long _ctxlen; + private bool _stripTrailingNewline; + + this(long ctxlen = 3, bool stripTrailingNewline = false) + { + _ctxlen = ctxlen; + _stripTrailingNewline = stripTrailingNewline; + } + + /// Diff двух текстов (UTF-8 строки) + string diffText(string oldText, string newText, + string oldLabel = "old", string newLabel = "new") + { + const(ubyte)[] a = _prep(oldText); + const(ubyte)[] b = _prep(newText); + return diffBytes(a, b, oldLabel, newLabel); + } + + /// Diff по путям (файлы читаются целиком, бинарно) + string diffFiles(string oldPath, string newPath, + string oldLabel = "", string newLabel = "") + { + enforce(exists(oldPath), "no such file: " ~ oldPath); + enforce(exists(newPath), "no such file: " ~ newPath); + + auto a = cast(const(ubyte)[]) read(oldPath); // бинарное чтение + auto b = cast(const(ubyte)[]) read(newPath); + + if (oldLabel.length == 0) + oldLabel = oldPath; + if (newLabel.length == 0) + newLabel = newPath; + + return diffBytes(a, b, oldLabel, newLabel); + } + + /// Базовый метод: diff двух буферов + string diffBytes(const(ubyte)[] oldBytes, const(ubyte)[] newBytes, + string oldLabel = "old", string newLabel = "new") + { + auto a = MmFile.fromBytes(oldBytes); + auto b = MmFile.fromBytes(newBytes); + + auto sink = new BufferSink; + xdemitcb_t ecb; + ecb.priv = cast(void*) sink; + ecb.outf = &BufferSink.writeCB; + + xpparam_t xpp; + xpp.flags = 0; + + xdemitconf_t xecfg; + xecfg.ctxlen = _ctxlen; + + auto pre = formatHeaders(oldLabel, newLabel); + + auto rc = xdl_diff(&a.mf, &b.mf, &xpp, &xecfg, &ecb); + enforce(rc >= 0, format("xdl_diff failed (rc=%s)", rc)); + + return pre ~ sink.toStringOwned(); + } + + static string formatHeaders(string oldLabel, string newLabel) + { + auto ts = Clock.currTime(UTC()).toISOExtString(); // YYYY-MM-DDTHH:MM:SS + return "--- " ~ oldLabel ~ "\t" ~ ts ~ "\n" + ~ "+++ " ~ newLabel ~ "\t" ~ ts ~ "\n"; + } + +private: + const(ubyte)[] _prep(string s) @trusted + { + auto bytes = cast(const(ubyte)[]) s.representation; + if (_stripTrailingNewline && (bytes.length && bytes[$ - 1] != '\n')) + { + ubyte[] tmp; + tmp.length = bytes.length + 1; + if (bytes.length) + memcpy(tmp.ptr, bytes.ptr, bytes.length); + tmp[$ - 1] = cast(ubyte) '\n'; + return cast(const(ubyte)[]) tmp; + } + return bytes; + } +} diff --git a/source/xdiff_init.d b/source/xdiff_init.d deleted file mode 100644 index d73cc09..0000000 --- a/source/xdiff_init.d +++ /dev/null @@ -1,33 +0,0 @@ -module xdiff_init; - -import core.stdc.stdlib : malloc, free, realloc; -import std.exception : enforce; -import xdiff; // твой низкоуровневый биндинг (xdiff.d) - -extern (C) private void* wrap_malloc(void*, uint size) { return malloc(size); } -extern (C) private void wrap_free (void*, void* p) { free(p); } -extern (C) private void* wrap_realloc(void*, void* p, uint size){ return realloc(p, size); } - -void ensureInit() -{ - // локальная статика инициализируется один раз на процесс - static bool done = false; - if (done) return; - - memallocator_t m; - m.priv = null; - m.malloc = &wrap_malloc; - m.free = &wrap_free; - m.realloc = &wrap_realloc; - - enforce(xdl_set_allocator(&m) == 0, "xdl_set_allocator failed"); - done = true; -} - -mmfile_t initMmfile(size_t len) -{ - ensureInit(); - mmfile_t mf; - enforce(xdl_init_mmfile(&mf, cast(long)len, XDL_MMF_ATOMIC) == 0, "xdl_init_mmfile failed"); - return mf; -} diff --git a/source/xdiff_mmblocks.d b/source/xdiff_mmblocks.d deleted file mode 100644 index 81eeec0..0000000 --- a/source/xdiff_mmblocks.d +++ /dev/null @@ -1,108 +0,0 @@ -module xdiff_mmblocks; - -import std.exception : enforce; -import xdiff_init : ensureInit, initMmfile; -import xdiff; - -final class MMBlocks -{ -private: - mmfile_t _inner; - bool _owned = true; // владеем ли блоками? - enum DEFAULT_BSIZE = 8 * 1024; - - // Глубокая копия: читаем все блоки и записываем в dst - static void deepCopyAll(mmfile_t* src, MMBlocks dst) - { - enforce(xdl_seek_mmfile(src, 0) == 0, "seek(0) failed"); - - long sz = 0; - auto p = xdl_mmfile_first(src, &sz); - while (p !is null && sz > 0) - { - auto wrote = xdl_write_mmfile(dst.mmfilePtr(), p, sz); - enforce(wrote == sz, "write failed during deep copy"); - p = xdl_mmfile_next(src, &sz); - } - } - -public: - this() - { - _inner = initMmfile(DEFAULT_BSIZE); - } - - static MMBlocks fromBytes(const(ubyte)[] data) - { - auto b = new MMBlocks; - if (data.length > 0) - { - auto wrote = xdl_write_mmfile(&b._inner, data.ptr, cast(long)data.length); - enforce(wrote == cast(long)data.length, "xdl_write_mmfile wrote less than requested"); - } - return b; - } - - ~this() - { - if (_owned && _inner.head !is null) - xdl_free_mmfile(&_inner); - - // обнулим на всякий случай - _inner.head = null; _inner.tail = null; - _inner.rcur = null; _inner.wcur = null; - _inner.fsize = 0; _inner.bsize = 0; - } - - // доступ к внутреннему mmfile_t* - mmfile_t* mmfilePtr() @trusted { return &_inner; } - - size_t size() { return cast(size_t)xdl_mmfile_size(&_inner); } - bool isCompact() const { return xdl_mmfile_iscompact(cast(mmfile_t*)&_inner) != 0; } - - int writeBuf(const(ubyte)[] buf) - { - auto wrote = xdl_write_mmfile(&_inner, buf.ptr, cast(long)buf.length); - return (wrote == cast(long)buf.length) ? 0 : -1; - } - - /// Глубокая копия (новое владение) - MMBlocks clone() const - { - auto dst = new MMBlocks; - // deepCopyAll принимает mmfile_t*, поэтому аккуратно снимаем const с заголовка: - deepCopyAll(cast(mmfile_t*)&_inner, dst); - return dst; - } - - /// Компактизация через пересборку содержимого - void toCompact() - { - if (isCompact()) return; - - auto dst = new MMBlocks; - deepCopyAll(&_inner, dst); - - // перехватываем владение содержимым dst - if (_owned && _inner.head !is null) - xdl_free_mmfile(&_inner); - - _inner = *dst.mmfilePtr(); - dst._owned = false; // dst больше не владеет - dst._inner.head = null; // чтобы финалайзер dst ничего не освободил - dst._inner.tail = null; - dst._inner.rcur = null; - dst._inner.wcur = null; - dst._inner.fsize = 0; - dst._inner.bsize = 0; - } - - /// Снять владение (используется при move в MMFile) - void disarm() - { - _owned = false; - _inner.head = null; _inner.tail = null; - _inner.rcur = null; _inner.wcur = null; - _inner.fsize = 0; _inner.bsize = 0; - } -} diff --git a/source/xdiff_mmfile.d b/source/xdiff_mmfile.d deleted file mode 100644 index bbd5251..0000000 --- a/source/xdiff_mmfile.d +++ /dev/null @@ -1,166 +0,0 @@ -module xdiff_mmfile; - -import std.exception : enforce; -import xdiff_init : ensureInit, initMmfile; -import xdiff_mmblocks : MMBlocks; -import xdiff; - -final class MMFile -{ -private: - mmfile_t _inner; - bool _owned = true; - -public: - this() - { - _inner = initMmfile(0); - } - - static MMFile fromBytes(const(ubyte)[] data) - { - auto f = new MMFile; - if (data.length > 0) - { - auto wrote = xdl_write_mmfile(&f._inner, data.ptr, cast(long)data.length); - enforce(wrote == cast(long)data.length, "xdl_write_mmfile wrote less than requested"); - } - return f; - } - - ~this() - { - if (_owned && _inner.head !is null) - xdl_free_mmfile(&_inner); - - _inner.head = null; _inner.tail = null; - _inner.rcur = null; _inner.wcur = null; - _inner.fsize = 0; _inner.bsize = 0; - } - - size_t size() { return cast(size_t)xdl_mmfile_size(&_inner); } - bool isCompact() const { return xdl_mmfile_iscompact(cast(mmfile_t*)&_inner) != 0; } - - const(ubyte)[] asSlice() const - { - enforce(isCompact(), "MMFile must be compact for asSlice"); - auto h = _inner.head; - if (h is null || h.size <= 0) return (cast(ubyte*)null)[0 .. 0]; - return (cast(const(ubyte)*)h.ptr)[0 .. cast(size_t)h.size]; - } - - ubyte[] asSliceMut() - { - enforce(isCompact(), "MMFile must be compact for asSliceMut"); - auto h = _inner.head; - if (h is null || h.size <= 0) return (cast(ubyte*)null)[0 .. 0]; - return (cast(ubyte*)h.ptr)[0 .. cast(size_t)h.size]; - } - - alias MMPatch = MMBlocks; - - /// Сформировать patch (diff self → other) - MMPatch computePatch(ref MMFile other) - { - auto patch = new MMBlocks(); - - static extern(C) int emitToPatch(void* priv, mmbuffer_t* bufs, int num) - { - auto target = cast(MMBlocks)priv; // класс - foreach (i; 0 .. num) - { - auto b = bufs + i; - auto wrote = xdl_writem_mmfile(target.mmfilePtr(), b, 1); - if (wrote != (*b).size) return -1; - } - return 0; - } - - xpparam_t xpp; xpp.flags = 0; - xdemitconf_t xec; xec.ctxlen = 3; - xdemitcb_t ecb; ecb.priv = cast(void*)patch; ecb.outf = &emitToPatch; - - auto rc = xdl_diff(&_inner, &other._inner, &xpp, &xec, &ecb); - enforce(rc == 0, "xdl_diff failed"); - return patch; - } - - struct PatchResult { bool success; MMFile patched; MMFile rejected; } - - /// Применить patch к self - PatchResult applyPatch(ref MMPatch patch) - { - patch.toCompact(); - - auto acc = new MMBlocks(); - auto rej = new MMBlocks(); - - static extern(C) int emitTo(void* priv, mmbuffer_t* bufs, int num) - { - auto target = cast(MMBlocks)priv; // класс - long expect = 0; foreach (i; 0 .. num) expect += (bufs + i).size; - auto wrote = xdl_writem_mmfile(target.mmfilePtr(), bufs, num); - return (wrote == expect) ? 0 : -1; - } - - xdemitcb_t accCb; accCb.priv = cast(void*)acc; accCb.outf = &emitTo; - xdemitcb_t rejCb; rejCb.priv = cast(void*)rej; rejCb.outf = &emitTo; - - auto rc = xdl_patch(&_inner, patch.mmfilePtr(), XDL_PATCH_NORMAL, &accCb, &rejCb); - enforce(rc == 0 || rc == 1, "xdl_patch failed"); - - auto accFile = MMFile.fromBlocksMoved(acc); - auto rejFile = MMFile.fromBlocksMoved(rej); - - bool ok = (rejFile.size() == 0); - return PatchResult(ok, accFile, rejFile); - } - - /// 3-way merge: коллбэки nothrow, чтобы не бросать через C - void merge3Raw( - ref MMFile f1, - ref MMFile f2, - scope int delegate(const(ubyte)[]) nothrow acceptCb, - scope int delegate(const(ubyte)[]) nothrow rejectCb) - { - static extern(C) int emitAccept(void* priv, mmbuffer_t* bufs, int num) - { - auto d = cast(int delegate(const(ubyte)[]) nothrow*)priv; - foreach (i; 0 .. num) - { - auto b = bufs + i; - auto slice = (cast(const(ubyte)*)((*b).ptr))[0 .. cast(size_t)((*b).size)]; - auto r = (*d)(slice); if (r != 0) return r; - } - return 0; - } - static extern(C) int emitReject(void* priv, mmbuffer_t* bufs, int num) - { - auto d = cast(int delegate(const(ubyte)[]) nothrow*)priv; - foreach (i; 0 .. num) - { - auto b = bufs + i; - auto slice = (cast(const(ubyte)*)((*b).ptr))[0 .. cast(size_t)((*b).size)]; - auto r = (*d)(slice); if (r != 0) return r; - } - return 0; - } - - auto a = acceptCb, r = rejectCb; - xdemitcb_t accCb; accCb.priv = cast(void*)&a; accCb.outf = &emitAccept; - xdemitcb_t rejCb; rejCb.priv = cast(void*)&r; rejCb.outf = &emitReject; - - auto rc = xdl_merge3(&_inner, &f1._inner, &f2._inner, &accCb, &rejCb); - enforce(rc == 0, "xdl_merge3 failed"); - } - - /// «Move» из MMBlocks: забрать владение внутренними блоками - static MMFile fromBlocksMoved(MMBlocks b) - { - b.toCompact(); - auto f = new MMFile; - f._inner = *b.mmfilePtr(); // копируем заголовок (указатели на блоки) - b.disarm(); // источник больше не владеет - return f; - } -} diff --git a/source/xdiff_unittests.d b/source/xdiff_unittests.d deleted file mode 100644 index 8646f42..0000000 --- a/source/xdiff_unittests.d +++ /dev/null @@ -1,246 +0,0 @@ -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); -}