local sync9 = require "sync9" local unpack = table.unpack or unpack local DOCCOUNT = 4 local alphabet = "abcdefghijklmnopqrstuvwxyz 0123456789" local function randchar() local pos = math.random(#alphabet) return alphabet:sub(pos, pos) end local docsyncs = {} local patches = {} for idx = 1, DOCCOUNT do docsyncs[idx] = sync9.createresource(0, "") patches[idx] = {} end local function mergependingpatches(docidx, num) for idx = 1, #patches do if not docidx or idx == docidx then -- apply the changes to the "edited" document local patchcnt = math.min(num or #patches[idx], #patches[idx]) for i = 1, patchcnt do docsyncs[idx]:addversion(unpack(table.remove(patches[idx], 1))) end end end end local function getgraph(resource) local callbacks = {} resource:walkgraph(function(args) table.insert(callbacks, table.concat({args.version, args.value, args.level, args.offset, args.isdeleted and "t" or "f", args.isnode and "t" or "f"}, "-")) end) return table.concat(callbacks, ";"), #callbacks end local seed = math.random(10000) math.randomseed(seed) for i = 1, 5000 do if i % 1000 == 0 then local nodecnt = 0 docsyncs[1]:walkgraph(function() nodecnt = nodecnt + 1 end) print(("%d: value length = %s; graph node count = %s"):format(i, #docsyncs[1]:getvalue(), nodecnt)) end -- pick the document to work with local docidx = math.random(#docsyncs) local docsize = #(docsyncs[docidx]:getvalue()) local version = ("%06d-%s"):format(i, docidx) local parents = docsyncs[docidx]:getparents() local insert = docsize == 0 or math.random() < 0.5 -- insert can be done before and after a position, but delete only before local pos = math.random(docsize + (insert and 1 or 0)) - 1 local length = math.random(math.min(10, insert and 10 or math.max(1, docsize))) local content = randchar():rep(length) -- distribute the patch among the documents for idx = 1, #patches do local patch = insert and {pos, 0, content} or {pos, length} if idx == docidx then -- apply the changes to the "edited" document docsyncs[docidx]:addversion(version, {patch}, parents) else -- push pending patches to other documents table.insert(patches[idx], {version, {patch}, parents}) end end -- merge some of (up to 3) the pending changes for a random document if math.random() <= 0.3 then mergependingpatches(math.random(DOCCOUNT), math.random(3)) end -- cross-merge all of them with some probability if math.random() <= 0.1 then mergependingpatches() end -- compare the results if there are no outstanding patches local pendingcnt = 0 for i = 1, #patches do pendingcnt = pendingcnt + #patches[i] end if pendingcnt == 0 then local value = docsyncs[1]:getvalue() local graph, num = getgraph(docsyncs[1]) for i = 2, #patches do assert(value == docsyncs[i]:getvalue(), ("Failed value comparison %s (seed=%d)\n1=%s\n%d=%s\n"):format( i, seed, value, i, docsyncs[i]:getvalue())) assert(graph == getgraph(docsyncs[i]), ("Failed graph comparison %s (seed=%d)\n1=%s\n%d=%s\n"):format( i, seed, graph, i, getgraph(docsyncs[i]))) end -- prune all histories and compare again for i = 1, #patches do docsyncs[i]:prune() assert(value == docsyncs[i]:getvalue(), ("Failed value comparison after pruning %s (seed=%d)\nexpected=%s\nreceived=%s\n"):format( i, seed, value, docsyncs[i]:getvalue())) local parents = docsyncs[i]:getparents() -- if there is a single parent on top, everything has been pruned if not next(parents, next(parents)) then local pgraph, pnum = getgraph(docsyncs[i]) assert(pnum == 1, ("Failed history pruning %s (seed=%d)\nexpected=%s\nreceived=%s (%s)\n"):format( i, seed, 1, pnum, pgraph)) end end end end