Commit c9f13fc7 authored by Kirill Smelkov's avatar Kirill Smelkov

X Get rebuild tests to run in a sane time; Add proper random-based testing for rebuild

Now both normal and random tests for all Update and rebuild could be
exercised as part of regular test runs.

* t2:
  X rebuild: tests: Random testing
  X rebuild: tests: Don't exercise keys from keys2 that already became tracked after Track(keys1) + Update
  X rebuild: tests: Inline _assertTrack
  X rebuild: tests: Don't compute keyCover in trackSet
  X rebuild: tests: Don't recompute trackSet(keys1R2) several times
  X rebuild: tests: Move ΔBtail.Clone test out of hot inner loop into separate test
  X tests: Factor-out tree-test-env into tTreeEnv
  X xbtree: Less copy/garbage in RangedKeySet ops
  X rebuild: tests: Precompute kadj10·kadj21
  X rebuild: tests: Don't access ZODB in xtrackKeys
  X rebuild: tests: Don't access ZODB in XGetδKV
  X rebuild: tests: Don't reflect.DeepEqual in inner loop
  .
  .
  .
  .
  .
  .
parents 52c72dbb e9c4b619
...@@ -363,12 +363,12 @@ func (S PPTreeSubSet) verify() { ...@@ -363,12 +363,12 @@ func (S PPTreeSubSet) verify() {
}() }()
// recompute {} oid -> children and verify .nchild against it // recompute {} oid -> children and verify .nchild against it
children := map[zodb.Oid]SetOid{} children := make(map[zodb.Oid]SetOid, len(S))
for oid, t := range S { for oid, t := range S {
if t.parent != zodb.InvalidOid { if t.parent != zodb.InvalidOid {
cc, ok := children[t.parent] cc, ok := children[t.parent]
if !ok { if !ok {
cc = SetOid{} cc = make(SetOid, 1)
children[t.parent] = cc children[t.parent] = cc
} }
cc.Add(oid) cc.Add(oid)
......
...@@ -34,7 +34,7 @@ type KeyRange struct { ...@@ -34,7 +34,7 @@ type KeyRange struct {
hi_ Key // NOTE _not_ hi) to avoid overflow at ∞; hi = hi_ + 1 hi_ Key // NOTE _not_ hi) to avoid overflow at ∞; hi = hi_ + 1
} }
// RangedKeySet is set of Keys with adjacent keys coaleced into Ranges. // RangedKeySet is set of Keys with adjacent keys coalesced into Ranges.
// //
// Zero value represents empty set. // Zero value represents empty set.
type RangedKeySet struct { type RangedKeySet struct {
...@@ -109,20 +109,14 @@ func (S *RangedKeySet) AddRange(r KeyRange) { ...@@ -109,20 +109,14 @@ func (S *RangedKeySet) AddRange(r KeyRange) {
if (jhi - ilo) > 1 { if (jhi - ilo) > 1 {
lo := S.rangev[ilo].lo lo := S.rangev[ilo].lo
hi_ := S.rangev[jhi-1].hi_ hi_ := S.rangev[jhi-1].hi_
S.rangev = append( vReplaceSlice(&S.rangev, ilo,jhi, KeyRange{lo,hi_})
S.rangev[:ilo], append([]KeyRange{
KeyRange{lo, hi_}},
S.rangev[jhi:]...)...)
debugfRSet("\tmerge S[%d:%d]\t-> %s\n", ilo, jhi, S) debugfRSet("\tmerge S[%d:%d]\t-> %s\n", ilo, jhi, S)
} }
jhi = -1 // no longer valid jhi = -1 // no longer valid
// if [r.lo,r.hi) was outside of any entry - create new entry // if [r.lo,r.hi) was outside of any entry - create new entry
if r.hi_ < S.rangev[ilo].lo { if r.hi_ < S.rangev[ilo].lo {
S.rangev = append( vInsert(&S.rangev, ilo, r)
S.rangev[:ilo], append([]KeyRange{
r},
S.rangev[ilo:]...)...)
debugfRSet("\tinsert %s\t-> %s\n", r, S) debugfRSet("\tinsert %s\t-> %s\n", r, S)
} }
...@@ -140,20 +134,16 @@ func (S *RangedKeySet) AddRange(r KeyRange) { ...@@ -140,20 +134,16 @@ func (S *RangedKeySet) AddRange(r KeyRange) {
// and check if we should merge it with right/left neighbours // and check if we should merge it with right/left neighbours
if ilo+1 < len(S.rangev) { // right if ilo+1 < len(S.rangev) { // right
if S.rangev[ilo].hi_+1 == S.rangev[ilo+1].lo { if S.rangev[ilo].hi_+1 == S.rangev[ilo+1].lo {
S.rangev = append( vReplaceSlice(&S.rangev, ilo,ilo+2,
S.rangev[:ilo], append([]KeyRange{ KeyRange{S.rangev[ilo].lo, S.rangev[ilo+1].hi_})
KeyRange{S.rangev[ilo].lo, S.rangev[ilo+1].hi_}},
S.rangev[ilo+2:]...)...)
debugfRSet("\tmerge right\t-> %s\n", S) debugfRSet("\tmerge right\t-> %s\n", S)
} }
} }
if ilo > 0 { // left if ilo > 0 { // left
if S.rangev[ilo-1].hi_+1 == S.rangev[ilo].lo { if S.rangev[ilo-1].hi_+1 == S.rangev[ilo].lo {
S.rangev = append( vReplaceSlice(&S.rangev, ilo-1,ilo+1,
S.rangev[:ilo-1], append([]KeyRange{ KeyRange{S.rangev[ilo-1].lo, S.rangev[ilo].hi_})
KeyRange{S.rangev[ilo-1].lo, S.rangev[ilo].hi_}},
S.rangev[ilo+1:]...)...)
debugfRSet("\tmerge left\t-> %s\n", S) debugfRSet("\tmerge left\t-> %s\n", S)
} }
} }
...@@ -204,38 +194,27 @@ func (S *RangedKeySet) DelRange(r KeyRange) { ...@@ -204,38 +194,27 @@ func (S *RangedKeySet) DelRange(r KeyRange) {
} }
// [ilo+1:jhi-1] should be deleted // [ilo+1:jhi-1] should be deleted
// [ilo] and [jhi-1] overlap with [r.lo,r.hi) - they shuold be deleted, or shrinked, // [ilo] and [jhi-1] overlap with [r.lo,r.hi) - they should be deleted, or shrinked,
// or split+shrinked if ilo==jhi-1 and r is inside [ilo] // or split+shrinked if ilo==jhi-1 and r is inside [ilo]
if jhi-ilo == 1 && S.rangev[ilo].lo < r.lo && r.hi_ < S.rangev[ilo].hi_ { if jhi-ilo == 1 && S.rangev[ilo].lo < r.lo && r.hi_ < S.rangev[ilo].hi_ {
x := S.rangev[ilo] x := S.rangev[ilo]
S.rangev = append( vInsert(&S.rangev, ilo, x)
S.rangev[:ilo], append([]KeyRange{
x, x},
S.rangev[ilo+1:]...)...)
jhi++ jhi++
debugfRSet("\tpresplit copy %s\t-> %s\n", x, S) debugfRSet("\tpresplit copy %s\t-> %s\n", x, S)
} }
if S.rangev[ilo].lo < r.lo { // shrink left if S.rangev[ilo].lo < r.lo { // shrink left
S.rangev = append( S.rangev[ilo] = KeyRange{S.rangev[ilo].lo, r.lo-1}
S.rangev[:ilo], append([]KeyRange{
KeyRange{S.rangev[ilo].lo, r.lo-1}},
S.rangev[ilo+1:]...)...)
ilo++ ilo++
debugfRSet("\tshrink [%d] left\t-> %s\n", ilo, S) debugfRSet("\tshrink [%d] left\t-> %s\n", ilo, S)
} }
if r.hi_ < S.rangev[jhi-1].hi_ { // shrink right if r.hi_ < S.rangev[jhi-1].hi_ { // shrink right
S.rangev = append( S.rangev[jhi-1] = KeyRange{r.hi_+1, S.rangev[jhi-1].hi_}
S.rangev[:jhi-1], append([]KeyRange{
KeyRange{r.hi_+1, S.rangev[jhi-1].hi_}},
S.rangev[jhi:]...)...)
jhi-- jhi--
debugfRSet("\tshrink [%d] right\t-> %s\n", jhi-1, S) debugfRSet("\tshrink [%d] right\t-> %s\n", jhi-1, S)
} }
if (jhi - ilo) > 0 { if (jhi - ilo) > 0 {
S.rangev = append( vDeleteSlice(&S.rangev, ilo,jhi)
S.rangev[:ilo],
S.rangev[jhi:]...)
debugfRSet("\tdelete S[%d:%d]\t-> %s\n", ilo, jhi, S) debugfRSet("\tdelete S[%d:%d]\t-> %s\n", ilo, jhi, S)
} }
...@@ -414,3 +393,32 @@ func debugfRSet(format string, argv ...interface{}) { ...@@ -414,3 +393,32 @@ func debugfRSet(format string, argv ...interface{}) {
} }
fmt.Printf(format, argv...) fmt.Printf(format, argv...)
} }
// ---- slice ops ----
// vInsert inserts r into *pv[i].
func vInsert(pv *[]KeyRange, i int, r KeyRange) {
v := *pv
v = append(v, KeyRange{})
copy(v[i+1:], v[i:])
v[i] = r
*pv = v
}
// vDeleteSlice deletes *pv[lo:hi].
func vDeleteSlice(pv *[]KeyRange, lo,hi int) {
v := *pv
n := copy(v[lo:], v[hi:])
v = v[:lo+n]
*pv = v
}
// vReplaceSlice replaces *pv[lo:hi] with r.
func vReplaceSlice(pv *[]KeyRange, lo,hi int, r KeyRange) {
v := *pv
n := copy(v[lo+1:], v[hi:])
v[lo] = r
v = v[:lo+1+n]
*pv = v
}
...@@ -25,8 +25,8 @@ It is used as helper for ΔBtail tests. ...@@ -25,8 +25,8 @@ It is used as helper for ΔBtail tests.
The following subcommands are provided: The following subcommands are provided:
- `trees` transition ZODB tree through requested tree states, - `trees` transition ZODB tree through requested tree states,
- `allstructs` generates topologies for subset of all possible tree changes in - `allstructs` generate subset of all possible tree topologies for a tree
between two trees specified by two key->value dicts. specified by key->value dict.
Because python/pkg_resources startup is very slow(*) all subcommands can be Because python/pkg_resources startup is very slow(*) all subcommands can be
used either in CLI or in server mode, where requests are continuously read from used either in CLI or in server mode, where requests are continuously read from
...@@ -50,10 +50,10 @@ zconn.root()['treegen/tree']. ...@@ -50,10 +50,10 @@ zconn.root()['treegen/tree'].
Trees protocol specification: Trees protocol specification:
S: tree.srv start @<head> root=<tree-root-oid> S: tree.srv start @<head> root=<tree-root-oid>
C: <tree> C: <tree>
S: <tid> S: <tid>
C: <tree> C: <tree>
S: <tid> S: <tid>
... ...
session example: session example:
...@@ -69,43 +69,43 @@ session example: ...@@ -69,43 +69,43 @@ session example:
allstructs allstructs
---------- ----------
`treegen allstructs` generates topologies for subset of all possible tree `treegen allstructs` generates subset of all possible tree topologies for a tree
changes in between two trees specified by two key->value dicts. specified by key->value dict.
For every kv the following tree topologies are considered: 1) native (the one Given kv the following tree topologies are considered: 1) native (the one
that ZODB would usually create natively via regular usage), and 2) n random that ZODB would usually create natively via regular usage), and 2) n-1 random
ones. Then tree topologies are emitted corresponding to tree1->tree2 and ones. Then those tree topologies are emitted.
tree1<-tree2 transitions for all combinations of (tree1, tree2) pairs.
The output of `treegen allstructs` is valid input for `treegen trees`. The output of `treegen allstructs` is valid input for `treegen trees`.
Allstructs protocol specification: Allstructs protocol specification:
S: # allstructs.srv start S: # allstructs.srv start
C: <maxdepth> <maxsplit> <n>(/<seed>) <kv1> <kv2> C: <maxdepth> <maxsplit> <n>(/<seed>) <kv>
S: # allstructs <kv1> <kv2> S: # allstructs <kv>
S: # maxdepth=<maxdepth> maxsplit=<maxsplit> n=<n> seed=<seed> S: # maxdepth=<maxdepth> maxsplit=<maxsplit> n=<n> seed=<seed>
S: <tree1₀> S: <tree₀>
S: <tree2₀> S: <tree>
S: <tree1₁> S: <tree>
... ...
S: # ---- S: # ----
session example: session example:
# allstructs.srv start # allstructs.srv start
1 1 10 1:a 2:b 1 1 10 1:a,2:b,3:c
# allstructs 1:a 2:b # allstructs 1:a,2:b,3:c
# maxdepth=1 maxsplit=1 n=10 seed=1591369961 # maxdepth=1 maxsplit=1 n=10 seed=1624901326
T/B1:a T2/B1:a-B2:b,3:c
T/B2:b T3/B1:a,2:b-B3:c
T/T/B1:a T2/T-T3/B1:a-B2:b-B3:c
T/B2:b T/T3/B1:a,2:b-B3:c
T/B1:a T/T2/B1:a-B2:b,3:c
T/T/B2:b T/B1:a,2:b,3:c
T/T/B1:a T3/T2-T/B1:a-B2:b-B3:c
T/T/B2:b T2/T-T/B1:a-B2:b,3:c
T/B1:a T/T/B1:a,2:b,3:c
T3/T-T/B1:a,2:b-B3:c
# ---- # ----
-------- --------
...@@ -258,8 +258,8 @@ def TreesSrv(zstor, r): ...@@ -258,8 +258,8 @@ def TreesSrv(zstor, r):
def AllStructsSrv(r): def AllStructsSrv(r):
xprint('# allstructs.srv start') xprint('# allstructs.srv start')
for req in xreadlines(r): for req in xreadlines(r):
# maxdepth maxsplit n(/seed) kv1 kv2 # maxdepth maxsplit n(/seed) kv
maxdepth, maxsplit, n, kv1txt, kv2txt = req.split() maxdepth, maxsplit, n, kvtxt = req.split()
maxdepth = int(maxdepth) maxdepth = int(maxdepth)
maxsplit = int(maxsplit) maxsplit = int(maxsplit)
seed = None seed = None
...@@ -267,39 +267,32 @@ def AllStructsSrv(r): ...@@ -267,39 +267,32 @@ def AllStructsSrv(r):
n, seeds = n.split('/') n, seeds = n.split('/')
seed = int(seeds) seed = int(seeds)
n = int(n) n = int(n)
if kv1txt == 'ø': kv1txt = '' if kvtxt == 'ø': kvtxt = ''
if kv2txt == 'ø': kv2txt = ''
AllStructs(kv1txt, kv2txt, maxdepth, maxsplit, n, seed) AllStructs(kvtxt, maxdepth, maxsplit, n, seed)
xprint('# ----') xprint('# ----')
# AllStructs generates topologies for subset of all possible tree changes in # AllStructs generates subset of all possible topologies for a tree specified by kv dict.
# between kv1 and kv2. See top-level documentation for details. # See top-level documentation for details.
@func @func
def AllStructs(kv1txt, kv2txt, maxdepth, maxsplit, n, seed=None): def AllStructs(kvtxt, maxdepth, maxsplit, n, seed=None):
zstor = MappingStorage() # in RAM storage to create native ZODB topologies zstor = MappingStorage() # in RAM storage to create native ZODB topologies
zctx = ZCtx(zstor) zctx = ZCtx(zstor)
defer(zctx.close) defer(zctx.close)
kv1 = kvDecode(kv1txt, zctx.vdecode) kv = kvDecode(kvtxt, zctx.vdecode)
kv2 = kvDecode(kv2txt, zctx.vdecode)
print("# allstructs %s %s" % (kv1txt, kv2txt)) print("# allstructs %s" % kvtxt)
# create the tree # create the tree
ztree = zctx.root['ztree'] = LOBTree() ztree = zctx.root['ztree'] = LOBTree()
commit('init') commit('init')
# initial kv1 and kv2 states with topologies prepared as ZODB would do natively # initial kv state with topology prepared as ZODB would do natively
patch(ztree, diff({}, kv1), verify=kv1) patch(ztree, diff({}, kv), verify=kv)
if kv1 == {}: ztree._p_changed = True # to avoid empty commit - see TreesSrv if kv == {}: ztree._p_changed = True # to avoid empty commit - see TreesSrv
commit('kv1') commit('kv')
t1struct0 = xbtree.StructureOf(ztree) tstruct0 = xbtree.StructureOf(ztree)
patch(ztree, diff(kv1, kv2), verify=kv2)
if kv2 == kv1: ztree._p_changed = True
commit('kv2')
t2struct0 = xbtree.StructureOf(ztree)
# seed # seed
if seed is None: if seed is None:
...@@ -308,31 +301,15 @@ def AllStructs(kv1txt, kv2txt, maxdepth, maxsplit, n, seed=None): ...@@ -308,31 +301,15 @@ def AllStructs(kv1txt, kv2txt, maxdepth, maxsplit, n, seed=None):
random.seed(seed) random.seed(seed)
print("# maxdepth=%d maxsplit=%d n=%d seed=%d" % (maxdepth, maxsplit, n, seed)) print("# maxdepth=%d maxsplit=%d n=%d seed=%d" % (maxdepth, maxsplit, n, seed))
# make 2·n random samples from all tree topologies that can represent kv1 and kv2 # emit native + n-1 random samples from all tree topologies that can represent kv
t1structv = rsample(xbtree.AllStructs(kv1.keys(), maxdepth, maxsplit, kv=kv1), n) tstructv = rsample(xbtree.AllStructs(kv.keys(), maxdepth, maxsplit, kv=kv), n-1)
t2structv = rsample(xbtree.AllStructs(kv2.keys(), maxdepth, maxsplit, kv=kv2), n) if tstruct0 in tstructv: tstructv.remove(tstruct0) # avoid dups
tstructv.insert(0, tstruct0)
# all tree1 and tree2 topologies jumps in between we are going to emit:
# native + n random ones.
if t1struct0 in t1structv: t1structv.remove(t1struct0) # avoid dups
if t2struct0 in t2structv: t2structv.remove(t2struct0)
t1structv.insert(0, t1struct0)
t2structv.insert(0, t2struct0)
# XXX rework allstructs to accept only 1 kv and emit n structs for that one kv only
# -> iterate through the pairs/triplets in the caller (TestΔBTailAllStructs)
# emit topologies for tree1->tree2 and tree1<-tree2 transitions for all
# combinations of tree1 and tree2.
t12travel = list(bitravel2Way(t1structv, t2structv))
for i,tstruct in enumerate(t12travel):
if i%2 == 0:
assert tstruct in t1structv
else:
assert tstruct in t2structv
for tstruct in tstructv:
print(zctx.TopoEncode(tstruct)) print(zctx.TopoEncode(tstruct))
# rsample returns k random samples from seq. # rsample returns k random samples from seq.
# it differs from random.sample in that it does not keep whole list(seq) in memory. # it differs from random.sample in that it does not keep whole list(seq) in memory.
def rsample(seq, k): # -> [] of items; len <= k def rsample(seq, k): # -> [] of items; len <= k
...@@ -352,31 +329,6 @@ def rsample(seq, k): # -> [] of items; len <= k ...@@ -352,31 +329,6 @@ def rsample(seq, k): # -> [] of items; len <= k
sample[j] = item sample[j] = item
return sample return sample
# bitravel2Way generates travel path through all A<->B edges such
# that all edges a->b and a<-b are traveled and exactly once.
#
# The travel starts from A[0].
def bitravel2Way(A, B): # -> i[] of node
na = len(A); assert na > 0
nb = len(B); assert nb > 0
yield A[0] # A₀
for j in range(nb):
yield B[j] # A₀ -> Bj
for i in range(1,na):
yield A[i] # Ai <- Bj
yield B[j] # Ai -> Bj
yield A[0] # A₀ <- Bj
def test_bitravel2Way():
a,b,c = 'a','b','c'
A = [a,b,c]
B = [1, 2]
got = list(bitravel2Way(A, B))
want = [a,1,b,1,c,1,a,2,b,2,c,2,a]
assert got == want, (got, want)
test_bitravel2Way()
# kvEncode encodes key->value mapping into text. # kvEncode encodes key->value mapping into text.
# e.g. {1:'a', 2:'b'} -> '1:a,2:b' # e.g. {1:'a', 2:'b'} -> '1:a,2:b'
......
...@@ -91,6 +91,18 @@ type ΔValue struct { ...@@ -91,6 +91,18 @@ type ΔValue struct {
New Value New Value
} }
// String is like default %v, but uses ø for VDEL.
func (δv ΔValue) String() string {
old, new := "ø", "ø"
if δv.Old != VDEL {
old = δv.Old.String()
}
if δv.New != VDEL {
new = δv.New.String()
}
return fmt.Sprintf("{%s %s}", old, new)
}
// δZConnectTracked computes connected closure of δZ/T. // δZConnectTracked computes connected closure of δZ/T.
// //
......
...@@ -184,6 +184,7 @@ func (orig *ΔBtail) Clone() *ΔBtail { ...@@ -184,6 +184,7 @@ func (orig *ΔBtail) Clone() *ΔBtail {
} }
// vδBroots // vδBroots
klon.vδBroots = make([]ΔBroots, 0, len(orig.vδBroots))
for _, origδBroots := range orig.vδBroots { for _, origδBroots := range orig.vδBroots {
klonδBroots := ΔBroots{ klonδBroots := ΔBroots{
Rev: origδBroots.Rev, Rev: origδBroots.Rev,
...@@ -208,6 +209,7 @@ func (orig *ΔBtail) Clone() *ΔBtail { ...@@ -208,6 +209,7 @@ func (orig *ΔBtail) Clone() *ΔBtail {
// Clone returns copy of ΔTtail. // Clone returns copy of ΔTtail.
func (orig *ΔTtail) Clone() *ΔTtail { func (orig *ΔTtail) Clone() *ΔTtail {
klon := &ΔTtail{} klon := &ΔTtail{}
klon.vδT = make([]ΔTree, 0, len(orig.vδT))
for _, origδT := range orig.vδT { for _, origδT := range orig.vδT {
klonδT := ΔTree{ klonδT := ΔTree{
Rev: origδT.Rev, Rev: origδT.Rev,
......
...@@ -19,7 +19,29 @@ ...@@ -19,7 +19,29 @@
package xbtree package xbtree
// tests for δbtail.go // tests for δbtail.go
// XXX doc (2 ways of testing: explicit + allstructs), treegen py helper //
// This are the main tests for ΔBtail functionality. There are two primary testing concerns:
//
// 1) to verify treediff algorithm, and
// 2) to verify how ΔTtail rebuilds its history entries when set of tracked keys
// grow upon either new Track requests, or upon Update that turned out to
// trigger such growth of set of tracked keys.
//
// TestΔBTail*/Update and TestΔBTail*/rebuild exercise points "1" and "2" correspondingly.
//
// There are 2 testing approaches:
//
// a) transition a BTree in ZODB through particular tricky tree topologies
// and feed ΔBtail through created database transactions.
// b) transition a BTree in ZODB through random tree topologies and feed
// ΔBtail through created database transactions.
//
// TestΔBTail and TestΔBTailAllStructs implement approaches "a" and "b" correspondingly.
//
// testprog/treegen.py is used as helper to both:
//
// - commit a particular BTree topology into ZODB, and
// - to generate set of random tree topologies that all correspond to particular {k->v} dict.
import ( import (
"bufio" "bufio"
...@@ -27,7 +49,7 @@ import ( ...@@ -27,7 +49,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"io/ioutil" "math"
"math/rand" "math/rand"
"os" "os"
"os/exec" "os/exec"
...@@ -47,8 +69,6 @@ import ( ...@@ -47,8 +69,6 @@ import (
"lab.nexedi.com/nexedi/wendelin.core/wcfs/internal/xzodb" "lab.nexedi.com/nexedi/wendelin.core/wcfs/internal/xzodb"
) )
// XXX move infrastructure -> δbtail_treegen_test.go ?
// TreeGenSrv represents connection to running `treegen ...` server. // TreeGenSrv represents connection to running `treegen ...` server.
type TreeGenSrv struct { type TreeGenSrv struct {
argv []string argv []string
...@@ -216,9 +236,9 @@ func (tg *TreeSrv) Commit(tree string) (_ zodb.Tid, err error) { ...@@ -216,9 +236,9 @@ func (tg *TreeSrv) Commit(tree string) (_ zodb.Tid, err error) {
} }
// AllStructs returns response from `treegen allstructs` // AllStructs returns response from `treegen allstructs`
func (tg *AllStructsSrv) AllStructs(kv1, kv2 map[Key]string, maxdepth, maxsplit, n int, seed int64) (_ []string, err error) { func (tg *AllStructsSrv) AllStructs(kv map[Key]string, maxdepth, maxsplit, n int, seed int64) (_ []string, err error) {
req := fmt.Sprintf("%d %d %d/%d %s %s ", maxdepth, maxsplit, n, seed, kvtxt(kv1), kvtxt(kv2)) req := fmt.Sprintf("%d %d %d/%d %s", maxdepth, maxsplit, n, seed, kvtxt(kv))
defer xerr.Contextf(&err, "allstructs.srv: %s", req) defer xerr.Contextf(&err, "allstructs.srv: %s ", req)
_, err = io.WriteString(tg.pyin, req + "\n") _, err = io.WriteString(tg.pyin, req + "\n")
if err != nil { if err != nil {
...@@ -249,6 +269,7 @@ func (tg *AllStructsSrv) AllStructs(kv1, kv2 map[Key]string, maxdepth, maxsplit, ...@@ -249,6 +269,7 @@ func (tg *AllStructsSrv) AllStructs(kv1, kv2 map[Key]string, maxdepth, maxsplit,
// RTree represents Tree node covering [lo, hi_] key range in its parent tree. // RTree represents Tree node covering [lo, hi_] key range in its parent tree.
// XXX actually no coverage here -> kill? -> change to just `path []zodb.Oid` in RBucket?
type RTree struct { type RTree struct {
oid zodb.Oid oid zodb.Oid
parent *RTree parent *RTree
...@@ -264,6 +285,17 @@ type RBucket struct { ...@@ -264,6 +285,17 @@ type RBucket struct {
kv map[Key]string // bucket's k->v; values were ZBlk objects whose data is loaded instead. kv map[Key]string // bucket's k->v; values were ZBlk objects whose data is loaded instead.
} }
// Path returns path to this bucket from tree root.
func (rb *RBucket) Path() []zodb.Oid {
path := []zodb.Oid{rb.oid}
p := rb.parent
for p != nil {
path = append([]zodb.Oid{p.oid}, path...)
p = p.parent
}
return path
}
// RBucketSet represents set of buckets covering whole [-∞,∞) range. // RBucketSet represents set of buckets covering whole [-∞,∞) range.
type RBucketSet []*RBucket // k↑ type RBucketSet []*RBucket // k↑
...@@ -302,17 +334,26 @@ func (rbs RBucketSet) coverage() string { ...@@ -302,17 +334,26 @@ func (rbs RBucketSet) coverage() string {
// trackSet returns what should be ΔBtail.trackSet coverage for specified tracked key set. // trackSet returns what should be ΔBtail.trackSet coverage for specified tracked key set.
func (rbs RBucketSet) trackSet(tracked SetKey) PPTreeSubSet { func (rbs RBucketSet) trackSet(tracked SetKey) PPTreeSubSet {
trackSet, _ := rbs.trackSetWithCov(tracked) // nil = don't compute keyCover
// (trackSet is called from inside hot inner loop of rebuild test)
trackSet := rbs._trackSetWithCov(tracked, nil)
return trackSet return trackSet
} }
// trackSetWithCov returns what should be ΔBtail.trackSet and its key coverage for specified tracked key set. // trackSetWithCov returns what should be ΔBtail.trackSet and its key coverage for specified tracked key set.
func (rbs RBucketSet) trackSetWithCov(tracked SetKey) (trackSet PPTreeSubSet, keyCover *RangedKeySet) { func (rbs RBucketSet) trackSetWithCov(tracked SetKey) (trackSet PPTreeSubSet, keyCover *RangedKeySet) {
trackSet = PPTreeSubSet{}
keyCover = &RangedKeySet{} keyCover = &RangedKeySet{}
trackSet = rbs._trackSetWithCov(tracked, keyCover)
return trackSet, keyCover
}
func (rbs RBucketSet) _trackSetWithCov(tracked SetKey, outKeyCover *RangedKeySet) (trackSet PPTreeSubSet) {
trackSet = PPTreeSubSet{}
for k := range tracked { for k := range tracked {
kb := rbs.Get(k) kb := rbs.Get(k)
keyCover.AddRange(KeyRange{kb.lo, kb.hi_}) if outKeyCover != nil {
outKeyCover.AddRange(KeyRange{kb.lo, kb.hi_})
}
// trackSet explicitly records only regular buckets. // trackSet explicitly records only regular buckets.
// embedded buckets all have oid=zodb.InvalidOid and would lead to z // embedded buckets all have oid=zodb.InvalidOid and would lead to z
newNode := false newNode := false
...@@ -353,12 +394,13 @@ func (rbs RBucketSet) trackSetWithCov(tracked SetKey) (trackSet PPTreeSubSet, ke ...@@ -353,12 +394,13 @@ func (rbs RBucketSet) trackSetWithCov(tracked SetKey) (trackSet PPTreeSubSet, ke
p = p.parent p = p.parent
} }
} }
return trackSet, keyCover return trackSet
} }
// XGetTree loads Tree from zurl@at->obj<root>. // XGetTree loads Tree from zurl@at->obj<root>.
//
// Tree values must be ZBlk whose data is returned instead of references to ZBlk objects. // Tree values must be ZBlk whose data is returned instead of references to ZBlk objects.
// The tree is returned structured by buckets as // The tree is returned structured by buckets as
// //
...@@ -381,7 +423,7 @@ func XGetTree(db *zodb.DB, at zodb.Tid, root zodb.Oid) RBucketSet { ...@@ -381,7 +423,7 @@ func XGetTree(db *zodb.DB, at zodb.Tid, root zodb.Oid) RBucketSet {
xwalkDFS(ctx, KeyMin, KeyMax, ztree, func(rb *RBucket) { xwalkDFS(ctx, KeyMin, KeyMax, ztree, func(rb *RBucket) {
rbucketv = append(rbucketv, rb) rbucketv = append(rbucketv, rb)
}) })
if len(rbucketv) == 0 { // empty tree -> [-∞, ∞){} if len(rbucketv) == 0 { // empty tree -> [-∞,∞){}
etree := &RTree{ etree := &RTree{
oid: root, oid: root,
parent: nil, parent: nil,
...@@ -452,24 +494,15 @@ func _xwalkDFS(ctx context.Context, lo, hi_ Key, ztree *Tree, rparent *RTree, bv ...@@ -452,24 +494,15 @@ func _xwalkDFS(ctx context.Context, lo, hi_ Key, ztree *Tree, rparent *RTree, bv
} }
} }
// XGetδKV translates {k -> δ<oid>} to {k -> δ(ZBlk(oid).data)} according to db@at1..at2 snapshots. // XGetδKV translates {k -> δ<oid>} to {k -> δ(ZBlk(oid).data)} according to t1..t2 db snapshots.
func XGetδKV(db *zodb.DB, at1, at2 zodb.Tid, δkvOid map[Key]ΔValue) map[Key]Δstring { func XGetδKV(t1, t2 *tTreeCommit, δkvOid map[Key]ΔValue) map[Key]Δstring {
defer exc.Contextf("%s: get δkv %s..%s %v", db.Storage().URL(), at1, at2, δkvOid) δkv := make(map[Key]Δstring, len(δkvOid))
X := exc.Raiseif
txn, ctx := transaction.New(context.Background())
defer txn.Abort()
zconn1, err := db.Open(ctx, &zodb.ConnOptions{At: at1}); X(err)
zconn2, err := db.Open(ctx, &zodb.ConnOptions{At: at2}); X(err)
δkv := map[Key]Δstring{}
for k, δvOid := range δkvOid { for k, δvOid := range δkvOid {
δkv[k] = Δstring{ δkv[k] = Δstring{
Old: xzgetBlkData(ctx, zconn1, δvOid.Old), Old: t1.xgetBlkData(δvOid.Old),
New: xzgetBlkData(ctx, zconn2, δvOid.New), New: t2.xgetBlkData(δvOid.New),
} }
} }
return δkv return δkv
} }
...@@ -498,16 +531,16 @@ func XGetδKV(db *zodb.DB, at1, at2 zodb.Tid, δkvOid map[Key]ΔValue) map[Key] ...@@ -498,16 +531,16 @@ func XGetδKV(db *zodb.DB, at1, at2 zodb.Tid, δkvOid map[Key]ΔValue) map[Key]
// i.e. = δ(Av, Bv) for k: k ∈ U kadj(A,B)[·] // i.e. = δ(Av, Bv) for k: k ∈ U kadj(A,B)[·]
// ·∈T // ·∈T
// //
// XXX fix definition for "and changed" // XXX fix definition for "and changed, or coverage changed"
// //
// XXX adjacency matrix is symmetric: XXX KAdj verifies this at runtime // Note: adjacency matrix is symmetric (KAdj verifies this at runtime):
// //
// kadj(A,B) == kadj(B,A) // kadj(A,B) == kadj(B,A)
type KAdjMatrix map[Key]SetKey type KAdjMatrix map[Key]SetKey
// Map returns kadj·keys . // Map returns kadj·keys .
func (kadj KAdjMatrix) Map(keys SetKey) SetKey { func (kadj KAdjMatrix) Map(keys SetKey) SetKey {
res := SetKey{} res := make(SetKey, len(keys))
for k := range keys { for k := range keys {
to, ok := kadj[k] to, ok := kadj[k]
if !ok { if !ok {
...@@ -518,6 +551,23 @@ func (kadj KAdjMatrix) Map(keys SetKey) SetKey { ...@@ -518,6 +551,23 @@ func (kadj KAdjMatrix) Map(keys SetKey) SetKey {
return res return res
} }
// Mul returns kadjA·kadjB .
//
// (kadjA·kadjB).Map(keys) = kadjA.Map(kadjB.Map(keys))
func (kadjA KAdjMatrix) Mul(kadjB KAdjMatrix) KAdjMatrix {
// ~ assert kadjA.keys == kadjB.keys
// check only len here; the rest will be asserted by Map
if len(kadjA) != len(kadjB) {
panicf("kadj.Mul: different keys:\n\nkadjA: %v\nkadjB: %v", kadjA, kadjB)
}
kadj := make(KAdjMatrix, len(kadjB))
for k, tob := range kadjB {
kadj[k] = kadjA.Map(tob)
}
return kadj
}
// KAdj computes adjacency matrix for t1 -> t2 transition. // KAdj computes adjacency matrix for t1 -> t2 transition.
// //
// The set of keys for which kadj matrix is computed can be optionally provided. // The set of keys for which kadj matrix is computed can be optionally provided.
...@@ -666,20 +716,23 @@ func xverifyΔBTail_Update(t *testing.T, subj string, db *zodb.DB, treeRoot zodb ...@@ -666,20 +716,23 @@ func xverifyΔBTail_Update(t *testing.T, subj string, db *zodb.DB, treeRoot zodb
keys.Add(allKeyv[idx]) keys.Add(allKeyv[idx])
} }
// XXX allocates and keeps too much memory in -verylong // this t.Run allocates and keeps too much memory in -verylong
// XXX also not so useful as above "Update/t1->t2" ? // also it is not so useful as above "Update/t1->t2"
// t.Run(fmt.Sprintf(" track=%s", keys), func(t *testing.T) { //t.Run(fmt.Sprintf(" track=%s", keys), func(t *testing.T) {
xverifyΔBTail_Update1(t, subj, db, treeRoot, t1.at,t2.at, t1.xkv,t2.xkv, t2.δZ, t2.δxkv, keys, kadj12) xverifyΔBTail_Update1(t, subj, db, treeRoot, t1,t2, keys, kadj12)
// }) //})
} }
}) })
} }
// xverifyΔBTail_Update1 verifies how ΔBTail handles ZODB update at1->at2 from initial // xverifyΔBTail_Update1 verifies how ΔBTail handles ZODB update at1->at2 from initial
// tracked state defined by initialTrackedKeys. // tracked state defined by initialTrackedKeys.
func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zodb.Oid, at1,at2 zodb.Tid, xkv1,xkv2 RBucketSet, δZ *zodb.EventCommit, d12 map[Key]Δstring, initialTrackedKeys SetKey, kadj KAdjMatrix) { func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zodb.Oid, t1,t2 *tTreeCommit, initialTrackedKeys SetKey, kadj KAdjMatrix) {
X := exc.Raiseif X := exc.Raiseif
// t.Logf("\n>>> Track=%s\n", initialTrackedKeys) //t.Logf("\n>>> Track=%s\n", initialTrackedKeys)
δZ := t2.δZ
d12 := t2.δxkv
var TrackedδZ SetKey = nil var TrackedδZ SetKey = nil
var kadjTrackedδZ SetKey = nil var kadjTrackedδZ SetKey = nil
...@@ -711,18 +764,18 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod ...@@ -711,18 +764,18 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod
// δbtail @at1 with initial tracked set // δbtail @at1 with initial tracked set
δbtail := NewΔBtail(at1, db) δbtail := NewΔBtail(t1.at, db)
xtrackKeys(δbtail, treeRoot, initialTrackedKeys) xtrackKeys(δbtail, t1, initialTrackedKeys)
// TrackedδZ = Tracked ^ δZ (i.e. a tracked node has changed, or its coverage was changed) // TrackedδZ = Tracked ^ δZ (i.e. a tracked node has changed, or its coverage was changed)
TrackedδZ = SetKey{} TrackedδZ = SetKey{}
for k := range initialTrackedKeys { for k := range initialTrackedKeys {
leaf1 := xkv1.Get(k) leaf1 := t1.xkv.Get(k)
oid1 := leaf1.oid oid1 := leaf1.oid
if oid1 == zodb.InvalidOid { // embedded bucket if oid1 == zodb.InvalidOid { // embedded bucket
oid1 = leaf1.parent.oid oid1 = leaf1.parent.oid
} }
leaf2 := xkv2.Get(k) leaf2 := t2.xkv.Get(k)
oid2 := leaf2.oid oid2 := leaf2.oid
if oid2 == zodb.InvalidOid { // embedded bucket if oid2 == zodb.InvalidOid { // embedded bucket
oid2 = leaf2.parent.oid oid2 = leaf2.parent.oid
...@@ -738,15 +791,8 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod ...@@ -738,15 +791,8 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod
} }
// XXX why "except ∞" ? // assert TrackedδZ ∈ kadj[TrackedδZ]
// assert TrackedδZ ∈ kadj[TrackedδZ] except ∞ XXX -> initialTrackedKeys.Difference(kadjTrackedδZ) trackNotInKadj := TrackedδZ.Difference(kadjTrackedδZ)
trackNotInKadj := SetKey{}
for k := range TrackedδZ {
if !kadjTrackedδZ.Has(k) {
trackNotInKadj.Add(k)
}
}
trackNotInKadj.Del(KeyMax)
if len(trackNotInKadj) > 0 { if len(trackNotInKadj) > 0 {
badf("BUG: Tracked^δZ ∉ kadj[Tracked^δZ] ; extra=%v", trackNotInKadj) badf("BUG: Tracked^δZ ∉ kadj[Tracked^δZ] ; extra=%v", trackNotInKadj)
return return
...@@ -767,8 +813,8 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod ...@@ -767,8 +813,8 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod
// trackSet1 = xkv1[tracked1] // trackSet1 = xkv1[tracked1]
// trackSet2 = xkv2[tracked2] ( = xkv2[kadj[tracked1]] // trackSet2 = xkv2[tracked2] ( = xkv2[kadj[tracked1]]
trackSet1, tkeyCov1 := xkv1.trackSetWithCov(initialTrackedKeys) trackSet1, tkeyCov1 := t1.xkv.trackSetWithCov(initialTrackedKeys)
trackSet2, tkeyCov2 := xkv2.trackSetWithCov(initialTrackedKeys.Union(kadjTrackedδZ)) trackSet2, tkeyCov2 := t2.xkv.trackSetWithCov(initialTrackedKeys.Union(kadjTrackedδZ))
// verify δbtail.trackSet against @at1 // verify δbtail.trackSet against @at1
δbtail.assertTrack(t, "1", ø, trackSet1) δbtail.assertTrack(t, "1", ø, trackSet1)
...@@ -788,9 +834,6 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod ...@@ -788,9 +834,6 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod
// assert δtkeycov == δ(tkeyCov1, tkeyCov2) // assert δtkeycov == δ(tkeyCov1, tkeyCov2)
δtkeycovOK := tkeyCov2.Difference(tkeyCov1) δtkeycovOK := tkeyCov2.Difference(tkeyCov1)
//fmt.Printf("tkeyCov1: %s\n", tkeyCov1)
//fmt.Printf("tkeyCov2: %s\n", tkeyCov2)
//fmt.Printf("δtkeycov: %s\n", δtkeycovOK)
δtkeycov := &RangedKeySet{} δtkeycov := &RangedKeySet{}
if __, ok := δB1.ByRoot[treeRoot]; ok { if __, ok := δB1.ByRoot[treeRoot]; ok {
δtkeycov = __.δtkeycov1 δtkeycov = __.δtkeycov1
...@@ -832,8 +875,8 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod ...@@ -832,8 +875,8 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod
// δT <- δB // δT <- δB
δToid := δB.ΔByRoot[treeRoot] // {} k -> δoid δToid := δB.ΔByRoot[treeRoot] // {} k -> δoid
δT = XGetδKV(db, at1,at2, δToid) // {} k -> δ(ZBlk(oid).data) δT = XGetδKV(t1,t2, δToid) // {} k -> δ(ZBlk(oid).data)
// δT must be subset of d12. // δT must be subset of d12.
// changed keys, that are // changed keys, that are
...@@ -869,22 +912,14 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod ...@@ -869,22 +912,14 @@ func xverifyΔBTail_Update1(t *testing.T, subj string, db *zodb.DB, treeRoot zod
} }
} }
// assertTrack verifies that trackSet == trackSetOK.
// XXX place
// XXX inline into δbtail.assertTrack?
func _assertTrack(t *testing.T, subj string, trackSet, trackSetOK PPTreeSubSet) {
t.Helper()
if !trackSet.Equal(trackSetOK) {
t.Errorf("%s:\n\thave: %v\n\twant: %v", subj, trackSet, trackSetOK)
}
}
// assertTrack verifies state of .trackSet and ΔTtail.trackNew. // assertTrack verifies state of .trackSet and ΔTtail.trackNew.
// it assumes that only one tree root is being tracked. // it assumes that only one tree root is being tracked.
// XXX place // XXX place
func (δBtail *ΔBtail) assertTrack(t *testing.T, subj string, trackSetOK PPTreeSubSet, trackNewOK PPTreeSubSet) { func (δBtail *ΔBtail) assertTrack(t *testing.T, subj string, trackSetOK PPTreeSubSet, trackNewOK PPTreeSubSet) {
t.Helper() t.Helper()
_assertTrack(t, subj + ": trackSet", δBtail.trackSet, trackSetOK) if !δBtail.trackSet.Equal(trackSetOK) {
t.Errorf("%s: trackSet:\n\thave: %v\n\twant: %v", subj, δBtail.trackSet, trackSetOK)
}
roots := SetOid{} roots := SetOid{}
for root := range δBtail.vδTbyRoot { for root := range δBtail.vδTbyRoot {
...@@ -916,7 +951,9 @@ func (δBtail *ΔBtail) assertTrack(t *testing.T, subj string, trackSetOK PPTree ...@@ -916,7 +951,9 @@ func (δBtail *ΔBtail) assertTrack(t *testing.T, subj string, trackSetOK PPTree
t.Errorf("%s: trackNewRoots:\n\thave: %v\n\twant: %v", subj, δBtail.trackNewRoots, trackNewRootsOK) t.Errorf("%s: trackNewRoots:\n\thave: %v\n\twant: %v", subj, δBtail.trackNewRoots, trackNewRootsOK)
} }
_assertTrack(t, subj + ": vδT.trackNew", δTtail.trackNew, trackNewOK) if !δTtail.trackNew.Equal(trackNewOK) {
t.Errorf("%s: vδT.trackNew:\n\thave: %v\n\twant: %v", subj, δTtail.trackNew, trackNewOK)
}
} }
// xverifyΔBTail_rebuild verifies ΔBtail.rebuild during t0->t1->t2 transition. // xverifyΔBTail_rebuild verifies ΔBtail.rebuild during t0->t1->t2 transition.
...@@ -926,8 +963,6 @@ func (δBtail *ΔBtail) assertTrack(t *testing.T, subj string, trackSetOK PPTree ...@@ -926,8 +963,6 @@ func (δBtail *ΔBtail) assertTrack(t *testing.T, subj string, trackSetOK PPTree
// //
// It also exercises rebuild phase of ΔBtail.Update. // It also exercises rebuild phase of ΔBtail.Update.
func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1, t2 *tTreeCommit) { func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1, t2 *tTreeCommit) {
// t1 := t2.prev
// t0 := t1.prev
t.Run(fmt.Sprintf("rebuild/%s→%s", t0.tree, t1.tree), func(t *testing.T) { t.Run(fmt.Sprintf("rebuild/%s→%s", t0.tree, t1.tree), func(t *testing.T) {
tAllKeys := allTestKeys(t0, t1, t2) tAllKeys := allTestKeys(t0, t1, t2)
tAllKeyv := tAllKeys.SortedElements() tAllKeyv := tAllKeys.SortedElements()
...@@ -939,14 +974,17 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1 ...@@ -939,14 +974,17 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1
t2.at: "at2", t2.at: "at2",
} }
fmt.Printf("@%s: %v\n", xat[t0.at], t0.xkv.Flatten()) //fmt.Printf("@%s: %v\n", xat[t0.at], t0.xkv.Flatten())
fmt.Printf("@%s: %v\n", xat[t1.at], t1.xkv.Flatten()) //fmt.Printf("@%s: %v\n", xat[t1.at], t1.xkv.Flatten())
fmt.Printf("@%s: %v\n", xat[t2.at], t2.xkv.Flatten()) //fmt.Printf("@%s: %v\n", xat[t2.at], t2.xkv.Flatten())
kadj10 := KAdj(t1,t0, allTestKeys(t0,t1,t2)) kadj10 := KAdj(t1,t0, allTestKeys(t0,t1,t2))
kadj21 := KAdj(t2,t1, allTestKeys(t0,t1,t2)) kadj21 := KAdj(t2,t1, allTestKeys(t0,t1,t2))
kadj12 := KAdj(t1,t2, allTestKeys(t0,t1,t2)) kadj12 := KAdj(t1,t2, allTestKeys(t0,t1,t2))
// kadj210 = kadj10·kadj21
kadj210 := kadj10.Mul(kadj21)
ø := PPTreeSubSet{} ø := PPTreeSubSet{}
// verify t0 -> t1 Track(keys1) Rebuild -> t2 Track(keys2) Rebuild // verify t0 -> t1 Track(keys1) Rebuild -> t2 Track(keys2) Rebuild
...@@ -967,23 +1005,28 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1 ...@@ -967,23 +1005,28 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1
} }
} }
Tkeys1 := t1.xkv.trackSet(keys1)
Tkeys1_0 := t1.xkv.trackSet(keys1_0)
t.Run(fmt.Sprintf(" T%s;R", keys1), func(t *testing.T) { t.Run(fmt.Sprintf(" T%s;R", keys1), func(t *testing.T) {
δbtail := NewΔBtail(t0.at, db) δbtail := NewΔBtail(t0.at, db)
// assert trackSet=ø, trackNew=ø, vδB=[] // assert trackSet=ø, trackNew=ø, vδB=[]
δbtail.assertTrack(t, "@at0", ø, ø) // XXX + vδB δbtail.assertTrack(t, "@at0", ø, ø)
assertΔTtail(t, "@at0", δbtail, t0, treeRoot, xat,
/*vδT=ø*/)
xverifyΔBTail_rebuild_U(t, δbtail, db, treeRoot, t0, t1, xat, xverifyΔBTail_rebuild_U(t, δbtail, treeRoot, t0, t1, xat,
/*trackSet=*/ø, /*trackSet=*/ø,
/*vδT=ø*/) /*vδT=ø*/)
xverifyΔBTail_rebuild_TR(t, db, δbtail, t1, treeRoot, xat, xverifyΔBTail_rebuild_TR(t, δbtail, t1, treeRoot, xat,
// after Track(keys1) // after Track(keys1)
keys1, keys1,
/*trackSet=*/ ø, /*trackSet=*/ ø,
/*trackNew=*/ t1.xkv.trackSet(keys1), /*trackNew=*/ Tkeys1,
// after rebuild // after rebuild
/*trackSet=*/ t1.xkv.trackSet(keys1_0), /*trackSet=*/ Tkeys1_0,
/*vδT=*/ δkv1_1) /*vδT=*/ δkv1_1)
t.Run((" →" + t2.tree), func(t *testing.T) { t.Run((" →" + t2.tree), func(t *testing.T) {
...@@ -991,7 +1034,7 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1 ...@@ -991,7 +1034,7 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1
// Update() (which includes rebuild) // Update() (which includes rebuild)
keys1R2 := kadj12.Map(keys1) keys1R2 := kadj12.Map(keys1)
for { for {
keys1R2_ := kadj10.Map(kadj21.Map(keys1R2)) keys1R2_ := kadj210.Map(keys1R2)
if keys1R2.Equal(keys1R2_) { if keys1R2.Equal(keys1R2_) {
break break
} }
...@@ -1012,12 +1055,23 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1 ...@@ -1012,12 +1055,23 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1
} }
} }
xverifyΔBTail_rebuild_U(t, δbtail, db, treeRoot, t1, t2, xat, Tkeys1R2 := t2.xkv.trackSet(keys1R2)
/*trackSet=*/t2.xkv.trackSet(keys1R2),
xverifyΔBTail_rebuild_U(t, δbtail, treeRoot, t1, t2, xat,
/*trackSet=*/ Tkeys1R2,
/*vδT=*/ δkv1_k1R2, δkv2_k1R2) /*vδT=*/ δkv1_k1R2, δkv2_k1R2)
// tRestKeys2 = tAllKeys - keys1 // tRestKeys2 = tAllKeys - keys1
tRestKeys2 := tAllKeys.Difference(keys1) // reduce that to = tAllKeys - keys1R2 in short mode
// ( if key from keys2 already became tracked after Track(keys1) + Update,
// adding Track(that-key), is not adding much testing coverage to recompute paths )
var tRestKeys2 SetKey
if testing.Short() {
tRestKeys2 = tAllKeys.Difference(keys1R2)
} else {
tRestKeys2 = tAllKeys.Difference(keys1)
}
tRestKeyv2 := tRestKeys2.SortedElements() tRestKeyv2 := tRestKeys2.SortedElements()
for k2idx := range IntSets(len(tRestKeyv2)) { for k2idx := range IntSets(len(tRestKeyv2)) {
keys2 := SetKey{} keys2 := SetKey{}
...@@ -1029,13 +1083,15 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1 ...@@ -1029,13 +1083,15 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1
// Track(keys2) + rebuild // Track(keys2) + rebuild
keys12R2 := keys1R2.Union(keys2) keys12R2 := keys1R2.Union(keys2)
for { for {
keys12R2_ := kadj10.Map(kadj21.Map(keys12R2)) keys12R2_ := kadj210.Map(keys12R2)
if keys12R2.Equal(keys12R2_) { if keys12R2.Equal(keys12R2_) {
break break
} }
keys12R2 = keys12R2_ keys12R2 = keys12R2_
} }
Tkeys2 := t2.xkv.trackSet(keys2)
Tkeys12R2 := t2.xkv.trackSet(keys12R2)
/* /*
fmt.Printf("\n\n\nKKK\nkeys1=%s keys2=%s\n", keys1, keys2) fmt.Printf("\n\n\nKKK\nkeys1=%s keys2=%s\n", keys1, keys2)
fmt.Printf("keys1R2: %s\n", keys1R2) fmt.Printf("keys1R2: %s\n", keys1R2)
...@@ -1046,17 +1102,16 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1 ...@@ -1046,17 +1102,16 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1
fmt.Printf("t2.xkv: %v\n", t2.xkv) fmt.Printf("t2.xkv: %v\n", t2.xkv)
fmt.Printf("kadj21: %v\n", kadj21) fmt.Printf("kadj21: %v\n", kadj21)
fmt.Printf("kadj12: %v\n", kadj12) fmt.Printf("kadj12: %v\n", kadj12)
fmt.Printf("t2.xkv.trackSet(keys2) -> %s\n", t2.xkv.trackSet(keys2)) fmt.Printf("Tkeys2 -> %s\n", Tkeys2)
fmt.Printf("t2.xkv.trackSet(keys1R2) -> %s\n", t2.xkv.trackSet(keys1R2)) fmt.Printf("Tkeys1R2 -> %s\n", Tkeys1R2)
fmt.Printf("t2.xkv.trackSet(keys2) \\ t2.xkv.trackSet(keys1R2) -> %s\n", fmt.Printf("Tkeys2 \\ Tkeys1R2 -> %s\n", Tkeys2.Difference(Tkeys1R2))
t2.xkv.trackSet(keys2).Difference(t2.xkv.trackSet(keys1R2)))
fmt.Printf("\n\n\n") fmt.Printf("\n\n\n")
*/ */
// δkvX_k12R2 = tX.δxkv / keys12R2 // δkvX_k12R2 = tX.δxkv / keys12R2
δkv1_k12R2 := map[Key]Δstring{} δkv1_k12R2 := make(map[Key]Δstring, len(t1.δxkv))
δkv2_k12R2 := map[Key]Δstring{} δkv2_k12R2 := make(map[Key]Δstring, len(t2.δxkv))
for k := range keys12R2 { for k := range keys12R2 {
δv1, ok := t1.δxkv[k] δv1, ok := t1.δxkv[k]
if ok { if ok {
...@@ -1069,26 +1124,21 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1 ...@@ -1069,26 +1124,21 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1
} }
// t.Run is expensive at this level of nest // t.Run is expensive at this level of nest
// t.Run(" T"+keys2.String()+";R", func(t *testing.T) { //t.Run(" T"+keys2.String()+";R", func(t *testing.T) {
δbtail_ := δbtail.Clone() δbtail_ := δbtail.Clone()
xverifyΔBTail_rebuild_TR(t, db, δbtail_, t2, treeRoot, xat, xverifyΔBTail_rebuild_TR(t, δbtail_, t2, treeRoot, xat,
// after Track(keys2) // after Track(keys2)
keys2, keys2,
/*trackSet*/ t2.xkv.trackSet(keys1R2), /*trackSet*/ Tkeys1R2,
/*trackNew*/ t2.xkv.trackSet(keys2).Difference( /*trackNew*/ Tkeys2.Difference(
// trackNew should not cover ranges that are // trackNew should not cover ranges that are
// already in trackSet // already in trackSet
t2.xkv.trackSet(keys1R2)), Tkeys1R2),
// after rebuild // after rebuild
/* trackSet=*/ t2.xkv.trackSet(keys12R2), /* trackSet=*/ Tkeys12R2,
/*vδT=*/ δkv1_k12R2, δkv2_k12R2) /*vδT=*/ δkv1_k12R2, δkv2_k12R2)
//})
// XXX move vvv to separate test (out of this inner loop)
// ΔBtail.Clone had bug that aliased klon data to orig
assertΔTtail(t, "BUG: after clone check", δbtail, db, t2, treeRoot, xat,
/*vδT=*/ δkv1_k1R2, δkv2_k1R2)
// })
} }
}) })
}) })
...@@ -1097,7 +1147,7 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1 ...@@ -1097,7 +1147,7 @@ func xverifyΔBTail_rebuild(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, t0, t1
} }
// xverifyΔBTail_rebuild_U verifies ΔBtail state after Update(ti->tj). // xverifyΔBTail_rebuild_U verifies ΔBtail state after Update(ti->tj).
func xverifyΔBTail_rebuild_U(t *testing.T, δbtail *ΔBtail, db *zodb.DB, treeRoot zodb.Oid, ti, tj *tTreeCommit, xat map[zodb.Tid]string, trackSet PPTreeSubSet, vδTok ...map[Key]Δstring) { func xverifyΔBTail_rebuild_U(t *testing.T, δbtail *ΔBtail, treeRoot zodb.Oid, ti, tj *tTreeCommit, xat map[zodb.Tid]string, trackSet PPTreeSubSet, vδTok ...map[Key]Δstring) {
t.Helper() t.Helper()
X := exc.Raiseif X := exc.Raiseif
ø := PPTreeSubSet{} ø := PPTreeSubSet{}
...@@ -1106,44 +1156,72 @@ func xverifyΔBTail_rebuild_U(t *testing.T, δbtail *ΔBtail, db *zodb.DB, treeR ...@@ -1106,44 +1156,72 @@ func xverifyΔBTail_rebuild_U(t *testing.T, δbtail *ΔBtail, db *zodb.DB, treeR
// Update ati -> atj // Update ati -> atj
δB, err := δbtail.Update(tj.δZ); X(err) δB, err := δbtail.Update(tj.δZ); X(err)
// XXX assert δB.Rev = tj.at; δB = δ(ti,tj)/initially tracked
δbtail.assertTrack(t, subj, trackSet, ø) δbtail.assertTrack(t, subj, trackSet, ø)
_ = δB assertΔTtail(t, subj, δbtail, tj, treeRoot, xat, vδTok...)
assertΔTtail(t, subj, δbtail, db, tj, treeRoot, xat, vδTok...)
// assert δB = vδTok[-1]
var δT, δTok map[Key]Δstring
if l := len(vδTok); l > 0 {
δTok = vδTok[l-1]
}
if len(δTok) == 0 {
δTok = nil
}
δrootsOK := 1
if δTok == nil {
δrootsOK = 0
}
δroots := SetOid{}
for root := range δbtail.vδTbyRoot {
δroots.Add(root)
}
δToid, ok := δB.ΔByRoot[treeRoot]
if ok {
δT = XGetδKV(ti, tj, δToid)
}
if δB.Rev != tj.at {
t.Errorf("%s: δB.Rev: have %s ; want %s", subj, δB.Rev, tj.at)
}
if len(δB.ΔByRoot) != δrootsOK {
t.Errorf("%s: len(δB.ΔByRoot) != %d ; δroots=%v", subj, δrootsOK, δroots)
}
if !δTEqual(δT, δTok) {
t.Errorf("%s: δB.ΔBByRoot[%s]:\nhave: %v\nwant: %v", subj, treeRoot, δT, δTok)
}
} }
// xverifyΔBTail_rebuild_TR verifies ΔBtail state after Track(keys) + rebuild. // xverifyΔBTail_rebuild_TR verifies ΔBtail state after Track(keys) + rebuild.
func xverifyΔBTail_rebuild_TR(t *testing.T, db *zodb.DB, δbtail *ΔBtail, tj *tTreeCommit, treeRoot zodb.Oid, xat map[zodb.Tid]string, keys SetKey, trackSet PPTreeSubSet, trackNew, trackSetAfterRebuild PPTreeSubSet, vδTok ...map[Key]Δstring) { func xverifyΔBTail_rebuild_TR(t *testing.T, δbtail *ΔBtail, tj *tTreeCommit, treeRoot zodb.Oid, xat map[zodb.Tid]string, keys SetKey, trackSet PPTreeSubSet, trackNew, trackSetAfterRebuild PPTreeSubSet, vδTok ...map[Key]Δstring) {
t.Helper() t.Helper()
ø := PPTreeSubSet{} ø := PPTreeSubSet{}
// Track(keys) // Track(keys)
xtrackKeys(δbtail, treeRoot, keys) xtrackKeys(δbtail, tj, keys)
subj := fmt.Sprintf("@%s: after Track%v", xat[tj.at], keys) subj := fmt.Sprintf("@%s: after Track%v", xat[tj.at], keys)
δbtail.assertTrack(t, subj, trackSet, trackNew) δbtail.assertTrack(t, subj, trackSet, trackNew)
// XXX vδB=[ø]
δbtail.rebuildAll() δbtail.rebuildAll()
subj += " + rebuild" subj += " + rebuild"
δbtail.assertTrack(t, subj, trackSetAfterRebuild, ø) δbtail.assertTrack(t, subj, trackSetAfterRebuild, ø)
// XXX assert vδB=[δ1/T(keys)]
// XXX verify Get // XXX verify Get -> XXX assertΔTtail ?
// verify δbtail.vδTbyRoot[treeRoot] // verify δbtail.vδTbyRoot[treeRoot]
assertΔTtail(t, subj, δbtail, db, tj, treeRoot, xat, vδTok...) assertΔTtail(t, subj, δbtail, tj, treeRoot, xat, vδTok...)
} }
// assertΔTtail verifies state of ΔTtail that corresponds to treeRoot in δbtail. // assertΔTtail verifies state of ΔTtail that corresponds to treeRoot in δbtail.
func assertΔTtail(t *testing.T, subj string, δbtail *ΔBtail, db *zodb.DB, tj *tTreeCommit, treeRoot zodb.Oid, xat map[zodb.Tid]string, vδTok ...map[Key]Δstring) { func assertΔTtail(t *testing.T, subj string, δbtail *ΔBtail, tj *tTreeCommit, treeRoot zodb.Oid, xat map[zodb.Tid]string, vδTok ...map[Key]Δstring) {
t.Helper() t.Helper()
// XXX +KVAtTail, +lastRevOf // XXX +KVAtTail, +lastRevOf
l := len(vδTok) l := len(vδTok)
var vatOK []zodb.Tid var vatOK []zodb.Tid
var vδTok_ []map[Key]Δstring var vδTok_ []map[Key]Δstring
at2t := map[zodb.Tid]*tTreeCommit{tj.at: tj}
t0 := tj t0 := tj
for i := 0; i<l; i++ { for i := 0; i<l; i++ {
// empty vδTok entries means they should be absent in vδT // empty vδTok entries means they should be absent in vδT
...@@ -1152,6 +1230,7 @@ func assertΔTtail(t *testing.T, subj string, δbtail *ΔBtail, db *zodb.DB, tj ...@@ -1152,6 +1230,7 @@ func assertΔTtail(t *testing.T, subj string, δbtail *ΔBtail, db *zodb.DB, tj
vδTok_ = append([]map[Key]Δstring{δTok}, vδTok_...) vδTok_ = append([]map[Key]Δstring{δTok}, vδTok_...)
} }
t0 = t0.prev t0 = t0.prev
at2t[t0.at] = t0
} }
vδTok = vδTok_ vδTok = vδTok_
δTtail, ok := δbtail.vδTbyRoot[treeRoot] δTtail, ok := δbtail.vδTbyRoot[treeRoot]
...@@ -1166,12 +1245,12 @@ func assertΔTtail(t *testing.T, subj string, δbtail *ΔBtail, db *zodb.DB, tj ...@@ -1166,12 +1245,12 @@ func assertΔTtail(t *testing.T, subj string, δbtail *ΔBtail, db *zodb.DB, tj
atPrev := t0.at atPrev := t0.at
for _, δToid := range vδToid { for _, δToid := range vδToid {
vat = append(vat, δToid.Rev) vat = append(vat, δToid.Rev)
δT := XGetδKV(db, atPrev, δToid.Rev, δToid.ΔKV) // {} k -> δ(ZBlk(oid).data) δT := XGetδKV(at2t[atPrev], at2t[δToid.Rev], δToid.ΔKV) // {} k -> δ(ZBlk(oid).data)
vδT = append(vδT, δT) vδT = append(vδT, δT)
atPrev = δToid.Rev atPrev = δToid.Rev
} }
if !(reflect.DeepEqual(vat, vatOK) && reflect.DeepEqual(vδT, vδTok)) { if !(tidvEqual(vat, vatOK) && vδTEqual(vδT, vδTok)) {
have := "" have := ""
for i := 0; i<len(vδT); i++ { for i := 0; i<len(vδT); i++ {
have += fmt.Sprintf("\n\t@%s: %v", xat[vat[i]], vδT[i]) have += fmt.Sprintf("\n\t@%s: %v", xat[vat[i]], vδT[i])
...@@ -1186,36 +1265,26 @@ func assertΔTtail(t *testing.T, subj string, δbtail *ΔBtail, db *zodb.DB, tj ...@@ -1186,36 +1265,26 @@ func assertΔTtail(t *testing.T, subj string, δbtail *ΔBtail, db *zodb.DB, tj
// xtrackKeys issues δbtail.Track requests for tree[keys]. // xtrackKeys issues δbtail.Track requests for tree[keys].
// XXX place // XXX place
func xtrackKeys(δbtail *ΔBtail, treeRoot zodb.Oid, keys SetKey) { func xtrackKeys(δbtail *ΔBtail, t *tTreeCommit, keys SetKey) {
X := exc.Raiseif X := exc.Raiseif
head := δbtail.Head()
txn, ctx := transaction.New(context.Background()) if head != t.at {
defer txn.Abort() panicf("BUG: δbtail.head: %s ; t.at: %s", head, t.at)
zconn, err := δbtail.db.Open(ctx, &zodb.ConnOptions{At: δbtail.Head()}); X(err)
xtree, err := zgetNodeOrNil(ctx, zconn, treeRoot); X(err)
var ztree *Tree // = nil if treeRoot was deleted
if xtree != nil {
ztree = xtree.(*Tree)
} }
for k := range keys { for k := range keys {
if ztree != nil { // NOTE: if tree is deleted - the following adds it to tracked
_, _, path, err := ZTreeGetBlkData(ctx, ztree, k); X(err) // set with every key being a hole. This aligns with the
err = δbtail.Track(k, path); X(err) // following situation
} else { //
// if treeRoot is deleted - add it to tracked set with every key // T1 -> ø -> T2
// being a hole. This aligns with the following situation //
// // where after T1->ø, even though the tree becomes deleted, its root
// T1 -> ø -> T2 // continues to be tracked and all keys migrate to holes in the
// // tracking set. By aligning initial state to the same as after
// where after T1->ø, even though the tree becomes deleted, its root // T1->ø, we test what will happen on ø->T2.
// continues to be tracked and all keys migrate to holes in the b := t.xkv.Get(k)
// tracking set. By aligning initial state to the same as after err := δbtail.track(k, b.Path()); X(err)
// T1->ø, we test what will happen on ø->T2.
err = δbtail.track(k, []zodb.Oid{treeRoot}); X(err)
}
} }
} }
...@@ -1311,12 +1380,19 @@ func xverifyΔBTail_GetAt1(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, vt []*t ...@@ -1311,12 +1380,19 @@ func xverifyΔBTail_GetAt1(t *testing.T, db *zodb.DB, treeRoot zodb.Oid, vt []*t
*/ */
// ----------------------------------------
// ΔBTestEntry represents one entry in ΔBTail tests. // ΔBTestEntry represents one entry in ΔBTail tests.
type ΔBTestEntry struct { type ΔBTestEntry struct {
tree string // next tree topology tree string // next tree topology
kadjOK KAdjMatrix // adjacency matrix against previous case (optional) kadjOK KAdjMatrix // adjacency matrix against previous case (optional)
flags ΔBTestFlags
} }
type ΔBTestFlags int
const ΔBTest_SkipUpdate ΔBTestFlags = 1 // skip verifying Update for this test entry
const ΔBTest_SkipRebuild ΔBTestFlags = 2 // skip verifying rebuild for this test entry
// ΔBTest converts xtest into ΔBTestEntry. // ΔBTest converts xtest into ΔBTestEntry.
// xtest can be string|ΔBTestEntry. // xtest can be string|ΔBTestEntry.
func ΔBTest(xtest interface{}) ΔBTestEntry { func ΔBTest(xtest interface{}) ΔBTestEntry {
...@@ -1325,6 +1401,7 @@ func ΔBTest(xtest interface{}) ΔBTestEntry { ...@@ -1325,6 +1401,7 @@ func ΔBTest(xtest interface{}) ΔBTestEntry {
case string: case string:
test.tree = xtest test.tree = xtest
test.kadjOK = nil test.kadjOK = nil
test.flags = 0
case ΔBTestEntry: case ΔBTestEntry:
test = xtest test = xtest
default: default:
...@@ -1333,14 +1410,78 @@ func ΔBTest(xtest interface{}) ΔBTestEntry { ...@@ -1333,14 +1410,78 @@ func ΔBTest(xtest interface{}) ΔBTestEntry {
return test return test
} }
// tTreeEnv is tree-based testing environment.
//
// It combines TreeSrv and client side access to ZODB with committed trees.
// It should be created it via tNewTreeEnv().
type tTreeEnv struct {
*testing.T
work string // working directory
treeSrv *TreeSrv
zstor zodb.IStorage
db *zodb.DB
// all committed trees
commitv []*tTreeCommit
}
// tTreeCommit represent test commit changing a tree. // tTreeCommit represent test commit changing a tree.
type tTreeCommit struct { type tTreeCommit struct {
tree string // the tree in toplogy-encoding tree string // the tree in topology-encoding
prev *tTreeCommit // previous commit prev *tTreeCommit // previous commit
at zodb.Tid // commit revision at zodb.Tid // commit revision
δZ *zodb.EventCommit // raw ZODB changes; δZ.tid == at δZ *zodb.EventCommit // raw ZODB changes; δZ.tid == at
xkv RBucketSet // full tree state as of @at xkv RBucketSet // full tree state as of @at
δxkv map[Key]Δstring // full tree-diff against parent δxkv map[Key]Δstring // full tree-diff against parent
blkDataTab map[zodb.Oid]string // full snapshot of all ZBlk data @at
}
// tNewTreeEnv creates new tTreeEnv.
func tNewTreeEnv(t *testing.T) *tTreeEnv {
X := exc.Raiseif
t.Helper()
tt := &tTreeEnv{T: t}
var err error
work := t.TempDir()
tt.treeSrv, err = StartTreeSrv(work + "/1.fs"); X(err)
t.Cleanup(func() {
err := tt.treeSrv.Close(); X(err)
})
tt.zstor, err = zodb.Open(context.Background(), tt.treeSrv.zurl, &zodb.OpenOptions{
ReadOnly: true,
}); X(err)
t.Cleanup(func() {
err := tt.zstor.Close(); X(err)
})
tt.db = zodb.NewDB(tt.zstor, &zodb.DBOptions{
// We need objects to be cached, because otherwise it is too
// slow to run the test for many testcases, especially
// xverifyΔBTail_rebuild.
CacheControl: &tZODBCacheEverything{},
})
t.Cleanup(func() {
err := tt.db.Close(); X(err)
})
head := tt.treeSrv.head
t1 := &tTreeCommit{
tree: "T/B:", // treegen.py creates the tree as initially empty
prev: nil,
at: head,
xkv: XGetTree(tt.db, head, tt.Root()),
blkDataTab: xGetBlkDataTab(tt.db, head),
δZ: nil,
δxkv: nil,
}
tt.commitv = []*tTreeCommit{t1}
return tt
} }
// tZODBCacheEverything is workaround for ZODB/go not implementing real // tZODBCacheEverything is workaround for ZODB/go not implementing real
...@@ -1352,110 +1493,158 @@ func (_ *tZODBCacheEverything) PCacheClassify(_ zodb.IPersistent) zodb.PCachePol ...@@ -1352,110 +1493,158 @@ func (_ *tZODBCacheEverything) PCacheClassify(_ zodb.IPersistent) zodb.PCachePol
return zodb.PCachePinObject | zodb.PCacheKeepState return zodb.PCachePinObject | zodb.PCacheKeepState
} }
// testΔBTail verifies ΔBTail on sequence of tree topologies coming from testq. // Root returns OID of root tree node.
func testΔBTail(t *testing.T, testq chan ΔBTestEntry) { func (t *tTreeEnv) Root() zodb.Oid {
return t.treeSrv.treeRoot
}
// Head returns most-recently committed tree.
func (t *tTreeEnv) Head() *tTreeCommit {
return t.commitv[len(t.commitv)-1]
}
// CommitTree calls t.treeSrv.Commit and returns tTreeCommit corresponding to committed transaction.
func (t *tTreeEnv) CommitTree(tree string) *tTreeCommit {
// TODO X = FatalIf
X := exc.Raiseif X := exc.Raiseif
defer exc.Contextf("commit %s", tree)
watchq := make(chan zodb.Event)
at0 := t.zstor.AddWatch(watchq)
defer t.zstor.DelWatch(watchq)
tid, err := t.treeSrv.Commit(tree); X(err)
if !(tid > at0) {
exc.Raisef("treegen -> %s ; want > %s", tid, at0)
}
zevent := <-watchq
δZ := zevent.(*zodb.EventCommit)
if δZ.Tid != tid {
exc.Raisef("treegen -> %s ; watchq -> %s", tid, δZ)
}
// load tree structure from the db
// if the tree does not exist yet - report its structure as empty
var xkv RBucketSet
if tree != DEL {
xkv = XGetTree(t.db, δZ.Tid, t.Root())
} else {
// empty tree with real treeRoot as oid even though the tree is
// deleted. Having real oid in the root tests that after deletion,
// root of the tree stays in the tracking set. We need root to stay
// in trackSet because e.g. in
//
// T1 -> ø -> T2
//
// where the tree is first deleted, then recreated, without root
// staying in trackSet after ->ø, treediff will notice nothing when
// it comes to ->T2.
xkv = RBucketSet{
&RBucket{
oid: zodb.InvalidOid,
parent: &RTree{
oid: t.Root(), // NOTE oid is not InvalidOid
parent: nil,
},
lo: KeyMin,
hi_: KeyMax,
kv: map[Key]string{},
},
}
}
work, err := ioutil.TempDir("", "δBTail"); X(err) ttree := &tTreeCommit{
defer func() { tree: tree,
err := os.RemoveAll(work); X(err) at: δZ.Tid,
}() δZ: δZ,
tg, err := StartTreeSrv(work + "/1.fs"); X(err) xkv: xkv,
defer func() { blkDataTab: xGetBlkDataTab(t.db, δZ.Tid),
err := tg.Close(); X(err) }
}()
zstor, err := zodb.Open(context.Background(), tg.zurl, &zodb.OpenOptions{ tprev := t.Head()
ReadOnly: true, ttree.prev = tprev
}); X(err) ttree.δxkv = kvdiff(tprev.xkv.Flatten(), ttree.xkv.Flatten())
defer func() {
err := zstor.Close(); X(err)
}()
db := zodb.NewDB(zstor, &zodb.DBOptions{ t.commitv = append(t.commitv, ttree)
// We need objects to be cached, because otherwise it is too
// slow to run the test for many testcases, especially
// xverifyΔBTail_rebuild.
CacheControl: &tZODBCacheEverything{},
})
defer func() {
err := db.Close(); X(err)
}()
// XCommitTree calls tg.Commit and returns tTreeCommit corresponding to committed transaction. return ttree
XCommitTree := func(tree string) *tTreeCommit { }
defer exc.Contextf("commit %s", tree)
watchq := make(chan zodb.Event) // xGetBlkDataTab loads all ZBlk from db@at.
at0 := zstor.AddWatch(watchq) //
defer zstor.DelWatch(watchq) // it returns {} oid -> blkdata.
func xGetBlkDataTab(db *zodb.DB, at zodb.Tid) map[zodb.Oid]string {
defer exc.Contextf("%s: @%s: get blkdatatab", db.Storage().URL(), at)
X := exc.Raiseif
tid, err := tg.Commit(tree); X(err) blkDataTab := map[zodb.Oid]string{}
if !(tid > at0) { txn, ctx := transaction.New(context.Background())
exc.Raisef("treegen -> %s ; want > %s", tid, at0) defer txn.Abort()
} zconn, err := db.Open(ctx, &zodb.ConnOptions{At: at}); X(err)
zevent := <-watchq xzroot, err := zconn.Get(ctx, 0); X(err)
δZ := zevent.(*zodb.EventCommit) zroot, ok := xzroot.(*zodb.Map)
if δZ.Tid != tid { if !ok {
exc.Raisef("treegen -> %s ; watchq -> %s", tid, δZ) exc.Raisef("root: expected %s, got %s", xzodb.TypeOf(zroot), xzodb.TypeOf(xzroot))
} }
// load tree structure from the db err = zroot.PActivate(ctx); X(err)
// if the tree does not exist yet - report its structure as empty defer zroot.PDeactivate()
var xkv RBucketSet
if tree != DEL {
xkv = XGetTree(db, δZ.Tid, tg.treeRoot)
} else {
// empty tree with real tg.treeRoot as oid even though the tree is
// deleted. Having real oid in the root tests that after deletion,
// root of the tree stays in the tracking set. We need root to stay
// in trackSet because e.g. in
//
// T1 -> ø -> T2
//
// where the tree is first deleted, then recreated, without root
// staying in trackSet after ->ø, treediff will notice nothing when
// it comes to ->T2.
xkv = RBucketSet{
&RBucket{
oid: zodb.InvalidOid,
parent: &RTree{
oid: tg.treeRoot, // NOTE oid is not InvalidOid
parent: nil,
},
lo: KeyMin,
hi_: KeyMax,
kv: map[Key]string{},
},
}
}
return &tTreeCommit{ xzblkdir, ok := zroot.Data["treegen/values"]
tree: tree, if !ok {
at: δZ.Tid, exc.Raisef("root['treegen/values'] missing")
δZ: δZ, }
xkv: xkv, zblkdir, ok := xzblkdir.(*zodb.Map)
if !ok {
exc.Raisef("root['treegen/values']: expected %s, got %s", xzodb.TypeOf(zblkdir), xzodb.TypeOf(xzblkdir))
}
err = zblkdir.PActivate(ctx); X(err)
defer zblkdir.PDeactivate()
for k, xzblk := range zblkdir.Data {
zblk, ok := xzblk.(zodb.IPersistent)
if !ok {
exc.Raisef("root['treegen/values'][%q]: expected %s, got %s", k, xzodb.TypeOf(zblk), xzodb.TypeOf(xzblk))
} }
oid := zblk.POid()
data := xzgetBlkData(ctx, zconn, oid)
blkDataTab[oid] = data
} }
var t0 *tTreeCommit return blkDataTab
t1 := &tTreeCommit{ }
tree: "T/B:", // treegen.py creates the tree as initially empty
prev: nil, // XXX ok? // xgetBlkData loads blk data for ZBlk<oid> @t.at
at: tg.head, //
xkv: XGetTree(db, tg.head, tg.treeRoot), // For speed the load is done via preloaded t.blkDataTab instead of access to the DB.
δZ: nil, // XXX ok? func (t *tTreeCommit) xgetBlkData(oid zodb.Oid) string {
δxkv: nil, // XXX ok? if oid == VDEL {
return DEL
} }
data, ok := t.blkDataTab[oid]
if !ok {
exc.Raisef("getBlkData ZBlk<%s> @%s: no such ZBlk", oid, t.at)
}
return data
}
// testΔBTail verifies ΔBTail on sequence of tree topologies coming from testq.
func testΔBTail(t_ *testing.T, testq chan ΔBTestEntry) {
t := tNewTreeEnv(t_)
var t0 *tTreeCommit
for test := range testq { for test := range testq {
t2 := XCommitTree(test.tree) t1 := t.Head()
t2.δxkv = kvdiff(t1.xkv.Flatten(), t2.xkv.Flatten()) // XXX move to XCommitTree? t2 := t.CommitTree(test.tree)
t2.prev = t1 // XXX ----//----
subj := fmt.Sprintf("%s -> %s", t1.tree, t2.tree) subj := fmt.Sprintf("%s -> %s", t1.tree, t2.tree)
// t.Logf("\n\n\n**** %s ****\n\n", subj) //t.Logf("\n\n\n**** %s ****\n\n", subj)
// KAdj // KAdj
if kadjOK := test.kadjOK; kadjOK != nil { if kadjOK := test.kadjOK; kadjOK != nil {
...@@ -1468,11 +1657,13 @@ func testΔBTail(t *testing.T, testq chan ΔBTestEntry) { ...@@ -1468,11 +1657,13 @@ func testΔBTail(t *testing.T, testq chan ΔBTestEntry) {
} }
// ΔBTail.Update // ΔBTail.Update
xverifyΔBTail_Update(t, subj, db, tg.treeRoot, t1,t2) if test.flags & ΔBTest_SkipUpdate == 0 {
xverifyΔBTail_Update(t.T, subj, t.db, t.Root(), t1,t2)
}
// ΔBTail.rebuild // ΔBTail.rebuild
if t0 != nil { if t0 != nil && (test.flags & ΔBTest_SkipRebuild == 0) {
xverifyΔBTail_rebuild(t, db, tg.treeRoot, t0,t1,t2) xverifyΔBTail_rebuild(t.T, t.db, t.Root(), t0,t1,t2)
} }
t0, t1 = t1, t2 t0, t1 = t1, t2
...@@ -1618,9 +1809,9 @@ func TestΔBTail(t *testing.T) { ...@@ -1618,9 +1809,9 @@ func TestΔBTail(t *testing.T) {
oo: K(1,2,3,4,5,8,9,oo)}), oo: K(1,2,3,4,5,8,9,oo)}),
// 2 reflow to right B neighbour; 8 split into new B; δ=ø // 2 reflow to right B neighbour; 8 splits into new B; δ=ø
"T3/B1:a,2:b-B4:d,8:h", "T3/B1:a,2:b-B4:d,8:h",
"T2,5/B1:a-B2:b,4:d-B8:h", // XXX add A "T2,5/B1:a-B2:b,4:d-B8:h",
// case where kadj does not grow too much as leafs coverage remains stable // case where kadj does not grow too much as leafs coverage remains stable
"T4,8/B1:a,2:b-B5:d,6:e-B10:g,11:h", "T4,8/B1:a,2:b-B5:d,6:e-B10:g,11:h",
...@@ -1644,22 +1835,25 @@ func TestΔBTail(t *testing.T) { ...@@ -1644,22 +1835,25 @@ func TestΔBTail(t *testing.T) {
// * `Tree -> ø -> Tree` (tree is deleted and then recreated) // * `Tree -> ø -> Tree` (tree is deleted and then recreated)
DEL, DEL,
// tree rotation
"T3/B2:b-B3:c,4:d",
"T5/T3-T7/B2:a-B3:a,4:a-B6:a-B8:a",
// found by AllStructs ([1] is not changed, but because B1 is // found by AllStructs ([1] is not changed, but because B1 is
// unlinked and 1 migrates to other bucket, changes in that // unlinked and 1 migrates to other bucket, changes in that
// other bucket must be included into δT) // other bucket must be included into δT)
"T1,2/B0:e-B1:d-B2:g,3:a", "T1,2/B0:e-B1:d-B2:g,3:a",
"T1/B0:d-B1:d,2:d", "T1/B0:d-B1:d,2:d",
// ----//---- with depth=2
// XXX the same issue as ^^^ but with depth=2
"T1,2/T-T-T/B0:a-B1:b-B2:c,3:d", "T1,2/T-T-T/B0:a-B1:b-B2:c,3:d",
"T1/T-T/B0:e-B1:b,2:f", "T1/T-T/B0:e-B1:b,2:f",
// XXX depth=3 (to verify recursion and selecting which tree children to follow or not) // XXX depth=3 (to verify recursion and selecting which tree children to follow or not)
// XXX more
// degenerate topology from ZODB tests // degenerate topology from ZODB tests
// https://github.com/zopefoundation/ZODB/commit/6cd24e99f89b
// https://github.com/zopefoundation/BTrees/blob/4.7.2-1-g078ba60/BTrees/tests/testBTrees.py#L20-L57
"T4/T2-T/T-T-T6,10/B1:a-B3:b-T-T-T/T-B7:c-B11:d/B5:e", "T4/T2-T/T-T-T6,10/B1:a-B3:b-T-T-T/T-B7:c-B11:d/B5:e",
"T/B1:e,5:d,7:c,8:b,11:a", // -3 +8 "T/B1:e,5:d,7:c,8:b,11:a", // -3 +8
...@@ -1694,10 +1888,6 @@ func TestΔBTail(t *testing.T) { ...@@ -1694,10 +1888,6 @@ func TestΔBTail(t *testing.T) {
"T4/T-T/B3:f-T/B4:a", "T4/T-T/B3:f-T/B4:a",
// "T/T/T/B1:a,2:b",
// "T/T/B1:a",
// ---- found by AllStructs ---- // ---- found by AllStructs ----
// trackSet2 wrongly computed due to top not being tracked to tree root // trackSet2 wrongly computed due to top not being tracked to tree root
...@@ -1729,8 +1919,8 @@ func TestΔBTail(t *testing.T) { ...@@ -1729,8 +1919,8 @@ func TestΔBTail(t *testing.T) {
"T/T1,3/T-T2-T4/B0:b-T-T-B3:g-B4:c/B1:b-B2:e", "T/T1,3/T-T2-T4/B0:b-T-T-B3:g-B4:c/B1:b-B2:e",
"T1,4/T-T-T/T-T2-B4:f/T-T-T/B0:h-B1:b-B2:h,3:a", "T1,4/T-T-T/T-T2-B4:f/T-T-T/B0:h-B1:b-B2:h,3:a",
// XXX TODO classify "T2/B1:a-B7:g",
"T2/B1:a-B7:g", "T2,8/B1:a-B7:g-B9:i", "T2,8/B1:a-B7:g-B9:i",
"T2/B1:a-B2:b", "T/B1:a,2:b", "T2/B1:a-B2:b", "T/B1:a,2:b",
"T2,3/B1:a-B2:b-B3:c", "T/B1:a,2:b", "T2,3/B1:a-B2:b-B3:c", "T/B1:a,2:b",
...@@ -1744,11 +1934,6 @@ func TestΔBTail(t *testing.T) { ...@@ -1744,11 +1934,6 @@ func TestΔBTail(t *testing.T) {
3: K(3,99,oo), 3: K(3,99,oo),
99: K(3,99,oo), 99: K(3,99,oo),
oo: K(3,99,oo)}), oo: K(3,99,oo)}),
// XXX --------
// tree rotation
"T3/B2:b-B3:c,4:d",
"T5/T3-T7/B2:a-B3:a,4:a-B6:a-B8:a",
} }
// direct tree_i -> tree_{i+1} -> _{i+2} ... plus // direct tree_i -> tree_{i+1} -> _{i+2} ... plus
// reverse ... tree_i <- _{i+1} <- _{i+2} // reverse ... tree_i <- _{i+1} <- _{i+2}
...@@ -1770,12 +1955,11 @@ func TestΔBTail(t *testing.T) { ...@@ -1770,12 +1955,11 @@ func TestΔBTail(t *testing.T) {
} }
// TestΔBTailAllStructs verifies ΔBtail on tree topologies generated by AllStructs.
var ( var (
verylongFlag = flag.Bool("verylong", false, `switch tests to run in "very long" mode`) verylongFlag = flag.Bool("verylong", false, `switch tests to run in "very long" mode`)
randseedFlag = flag.Int64("randseed", -1, `seed for random number generator`) randseedFlag = flag.Int64("randseed", -1, `seed for random number generator`)
) )
// TestΔBTailAllStructs verifies ΔBtail on tree topologies generated by AllStructs.
func TestΔBTailAllStructs(t *testing.T) { func TestΔBTailAllStructs(t *testing.T) {
X := exc.Raiseif X := exc.Raiseif
...@@ -1808,7 +1992,7 @@ func TestΔBTailAllStructs(t *testing.T) { ...@@ -1808,7 +1992,7 @@ func TestΔBTailAllStructs(t *testing.T) {
n := N(10,10,100) n := N(10,10,100)
nkeys := N(3, 5, 10) nkeys := N(3, 5, 10)
// server to generate AllStructs(kv1, kv2, ...) // server to generate AllStructs(kv, ...)
sg, err := StartAllStructsSrv(); X(err) sg, err := StartAllStructsSrv(); X(err)
defer func() { defer func() {
err := sg.Close(); X(err) err := sg.Close(); X(err)
...@@ -1822,55 +2006,110 @@ func TestΔBTailAllStructs(t *testing.T) { ...@@ -1822,55 +2006,110 @@ func TestΔBTailAllStructs(t *testing.T) {
rng := rand.New(rand.NewSource(seed)) rng := rand.New(rand.NewSource(seed))
t.Logf("# maxdepth=%d maxsplit=%d nkeys=%d n=%d seed=%d", maxdepth, maxsplit, nkeys, n, seed) t.Logf("# maxdepth=%d maxsplit=%d nkeys=%d n=%d seed=%d", maxdepth, maxsplit, nkeys, n, seed)
// generate (kv1, kv2) pairs randomly // generate (kv1, kv2, kv3) randomly
// keysv1 and keysv2 are random shuffle of IntSets // keysv1, keysv2 and keysv3 are random shuffle of IntSets
var keysv1 [][]int var keysv1 [][]int
var keysv2 [][]int var keysv2 [][]int
var keysv3 [][]int
for keys := range IntSets(nkeys) { for keys := range IntSets(nkeys) {
keysv1 = append(keysv1, keys) keysv1 = append(keysv1, keys)
keysv2 = append(keysv2, keys) keysv2 = append(keysv2, keys)
keysv3 = append(keysv3, keys)
} }
v := keysv1 v := keysv1
rng.Shuffle(len(v), func(i,j int) { v[i], v[j] = v[j], v[i] }) rng.Shuffle(len(v), func(i,j int) { v[i], v[j] = v[j], v[i] })
v = keysv2 v = keysv2
rng.Shuffle(len(v), func(i,j int) { v[i], v[j] = v[j], v[i] }) rng.Shuffle(len(v), func(i,j int) { v[i], v[j] = v[j], v[i] })
v = keysv3
rng.Shuffle(len(v), func(i,j int) { v[i], v[j] = v[j], v[i] })
// generate cases: keysv1[i] -> keysv2[i] with values generated // given random (kv1, kv2, kv3) generate corresponding set of random tree
// randomly along the way. // topology sets (T1, T2, T3). Then iterate through T1->T2->T3->T1...
// elements such that all right-directed triplets are visited and only once.
// Test Update and rebuild on the generated tree sequences.
vv := "abcdefgh" vv := "abcdefgh"
randv := func() string { randv := func() string {
i := rng.Intn(len(vv)) i := rng.Intn(len(vv))
return vv[i:i+1] return vv[i:i+1]
} }
// XXX rework to be effective with testing rebuild: // the number of pairs is 3·n^2
// - AllStructs accept only 1 kv // the number of triplets is n^3
// - we call it 3 times for kv1 kv2 kv3 //
// - iterate via triTravelXXXway ... here instead of in treegen.py // limit n for emitted triplets, so that the amount of work for Update
// and rebuild tests is approximately of the same order.
nrebuild := int(math.Ceil(math.Pow(3*float64(n*n), 1./3)))
// in non-short mode rebuild tests are exercising more keys variants, plus every test case
// takes more time. Compensate for that as well.
if !testing.Short() {
nrebuild -= 3
}
testq := make(chan ΔBTestEntry) testq := make(chan ΔBTestEntry)
go func() { go func() {
defer close(testq) defer close(testq)
for i := range keysv1 { for i := range keysv1 {
keys1 := keysv1[i] keys1 := keysv1[i]
keys2 := keysv2[i] keys2 := keysv2[i]
keys3 := keysv3[i]
kv1 := map[Key]string{} kv1 := map[Key]string{}
kv2 := map[Key]string{} kv2 := map[Key]string{}
kv3 := map[Key]string{}
for _, k := range keys1 { kv1[Key(k)] = randv() } for _, k := range keys1 { kv1[Key(k)] = randv() }
for _, k := range keys2 { kv2[Key(k)] = randv() } for _, k := range keys2 { kv2[Key(k)] = randv() }
for _, k := range keys3 { kv3[Key(k)] = randv() }
// given (kv1, kv2) - test on automatically generated (tree1 -> tree2) treev1, err1 := sg.AllStructs(kv1, maxdepth, maxsplit, n, rng.Int63())
reqSeed := rng.Int63() treev2, err2 := sg.AllStructs(kv2, maxdepth, maxsplit, n, rng.Int63())
treev, err := sg.AllStructs(kv1, kv2, maxdepth, maxsplit, n, reqSeed) treev3, err3 := sg.AllStructs(kv3, maxdepth, maxsplit, n, rng.Int63())
err := xerr.Merge(err1, err2, err3)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for _, tree := range treev { emit := func(tree string, flags ΔBTestFlags) {
testq <- ΔBTestEntry{tree, nil} // skip emitting this entry if both Update and
// Rebuild are requested to be skipped.
if flags == (ΔBTest_SkipUpdate | ΔBTest_SkipRebuild) {
return
}
testq <- ΔBTestEntry{tree, nil, flags}
}
URSkipIf := func(ucond, rcond bool) ΔBTestFlags {
var flags ΔBTestFlags
if ucond {
flags |= ΔBTest_SkipUpdate
}
if rcond {
flags |= ΔBTest_SkipRebuild
}
return flags
} }
for j := range treev1 {
for k := range treev2 {
for l := range treev3 {
// limit rebuild to subset of tree topologies,
// because #(triplets) grow as n^3. See nrebuild
// definition above for details.
norebuild := (j >= nrebuild ||
k >= nrebuild ||
l >= nrebuild)
// C_{l-1} -> Aj (pair first seen on k=0)
emit(treev1[j], URSkipIf(k != 0, norebuild))
// Aj -> Bk (pair first seen on l=0)
emit(treev2[k], URSkipIf(l != 0, norebuild))
// Bk -> Cl (pair first seen on j=0)
emit(treev3[l], URSkipIf(j != 0, norebuild))
}
}
}
} }
}() }()
...@@ -1881,6 +2120,39 @@ func TestΔBTailAllStructs(t *testing.T) { ...@@ -1881,6 +2120,39 @@ func TestΔBTailAllStructs(t *testing.T) {
// ---- misc ---- // ---- misc ----
func TestΔBtailClone(t_ *testing.T) {
// ΔBtail.Clone had bug that aliased klon data to orig
t := tNewTreeEnv(t_)
X := exc.Raiseif
t0 := t.CommitTree("T2/B1:a-B2:b")
t1 := t.CommitTree("T2/B1:c-B2:d")
δbtail := NewΔBtail(t0.at, t.db)
_, err := δbtail.Update(t1.δZ); X(err)
_2 := SetKey{}; _2.Add(2)
xtrackKeys(δbtail, t1, _2)
err = δbtail.rebuildAll(); X(err)
xat := map[zodb.Tid]string{
t0.at: "at0",
t1.at: "at1",
}
δkv1_1 := map[Key]Δstring{2:{"b","d"}}
assertΔTtail(t.T, "orig @at1", δbtail, t1, t.Root(), xat, δkv1_1)
δbklon := δbtail.Clone()
assertΔTtail(t.T, "klon @at1", δbklon, t1, t.Root(), xat, δkv1_1)
t2 := t.CommitTree("T/B1:b,2:a")
_, err = δbtail.Update(t2.δZ); X(err)
xat[t2.at] = "at2"
δkv1_2 := map[Key]Δstring{1:{"a","c"}, 2:{"b","d"}}
δkv2_2 := map[Key]Δstring{1:{"c","b"}, 2:{"d","a"}}
assertΔTtail(t.T, "orig @at2", δbtail, t2, t.Root(), xat, δkv1_2, δkv2_2)
assertΔTtail(t.T, "klon @at1 after orig @at->@at2", δbklon, t1, t.Root(), xat, δkv1_1)
}
// IntSets generates all sets of integers in range [0,N) // IntSets generates all sets of integers in range [0,N)
func IntSets(N int) chan []int { func IntSets(N int) chan []int {
...@@ -2031,18 +2303,43 @@ func (b *RBucket) String() string { ...@@ -2031,18 +2303,43 @@ func (b *RBucket) String() string {
} }
// String is like default %v, but uses ø for VDEL. // XXX place
func (δv ΔValue) String() string { func tidvEqual(av, bv []zodb.Tid) bool {
old, new := DEL, DEL if len(av) != len(bv) {
if δv.Old != VDEL { return false
old = δv.Old.String() }
for i, a := range av {
if bv[i] != a {
return false
}
}
return true
}
func vδTEqual(vδa, vδb []map[Key]Δstring) bool {
if len(vδa) != len(vδb) {
return false
} }
if δv.New != VDEL { for i, δa := range vδa {
new = δv.New.String() if !δTEqual(δa, vδb[i]) {
return false
}
} }
return fmt.Sprintf("{%s %s}", old, new) return true
} }
func δTEqual(δa, δb map[Key]Δstring) bool {
if len(δa) != len(δb) {
return false
}
for k, δ := range δa {
δ_, ok := δb[k]
if !ok || δ != δ_ {
return false
}
}
return true
}
// ---------------------------------------- // ----------------------------------------
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment