Compare commits
2 commits
06b2a2a993
...
1f00cff032
Author | SHA1 | Date | |
---|---|---|---|
1f00cff032 | |||
32203fadc0 |
8 changed files with 598 additions and 217 deletions
28
dub.json
28
dub.json
|
@ -16,5 +16,33 @@
|
|||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
6
dub.selections.json
Normal file
6
dub.selections.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"fileVersion": 1,
|
||||
"versions": {
|
||||
"xdiff": {"version":"e2396bc172eba813cdcd1a96c494e35d687f576a","repository":"git+https://git.zhirov.kz/dlang/xdiff.git"}
|
||||
}
|
||||
}
|
31
source/app.d
31
source/app.d
|
@ -1,25 +1,16 @@
|
|||
import std.stdio : writeln, stderr;
|
||||
import std.exception : collectException;
|
||||
import std.stdio;
|
||||
import xdiff_mmfile : MMFile;
|
||||
|
||||
import diff;
|
||||
|
||||
int main(string[] args)
|
||||
void main()
|
||||
{
|
||||
if (args.length != 3)
|
||||
{
|
||||
stderr.writeln("use: ", args[0], " OLD NEW");
|
||||
return 1;
|
||||
}
|
||||
auto a = MMFile.fromBytes(cast(ubyte[])"hello world\n");
|
||||
auto b = MMFile.fromBytes(cast(ubyte[])"hello world!\n");
|
||||
|
||||
auto eng = new DiffEngine(3);
|
||||
auto patch = a.computePatch(b);
|
||||
writeln("patch size: ", patch.size());
|
||||
|
||||
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;
|
||||
auto res = a.applyPatch(patch);
|
||||
writeln("apply success: ", res.success);
|
||||
write(res.patched.asSlice()); // печатаем результат
|
||||
writeln();
|
||||
}
|
||||
|
|
197
source/diff.d
197
source/diff.d
|
@ -1,197 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
33
source/xdiff_init.d
Normal file
33
source/xdiff_init.d
Normal file
|
@ -0,0 +1,33 @@
|
|||
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;
|
||||
}
|
108
source/xdiff_mmblocks.d
Normal file
108
source/xdiff_mmblocks.d
Normal file
|
@ -0,0 +1,108 @@
|
|||
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;
|
||||
}
|
||||
}
|
166
source/xdiff_mmfile.d
Normal file
166
source/xdiff_mmfile.d
Normal file
|
@ -0,0 +1,166 @@
|
|||
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;
|
||||
}
|
||||
}
|
246
source/xdiff_unittests.d
Normal file
246
source/xdiff_unittests.d
Normal file
|
@ -0,0 +1,246 @@
|
|||
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);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue