3 ### Randomized testing of binary trees
5 ### (c) 2024 Straylight/Edgeware
8 ###----- Licensing notice ---------------------------------------------------
10 ### This file is part of Xyla, a library of binary trees.
12 ### Xyla is free software: you can redistribute it and/or modify it under
13 ### the terms of the GNU Lesser General Public License as published by the
14 ### Free Software Foundation; either version 3 of the License, or (at your
15 ### option) any later version.
17 ### Xyla is distributed in the hope that it will be useful, but WITHOUT
18 ### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19 ### FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
20 ### License for more details.
22 ### You should have received a copy of the GNU Lesser General Public
23 ### License along with Xyla. If not, see <https://www.gnu.org/licenses/>.
25 ###--------------------------------------------------------------------------
26 ### External dependencies.
37 import subprocess as SUB
40 ###--------------------------------------------------------------------------
46 PROG = OS.path.basename(SYS.argv[0])
48 def base64_encode(buf):
49 ## No, you blundering morons, the result of Base64 encoding is text.
50 return B64.b64encode(buf).decode("us-ascii")
52 def binsearch(list, key, keyfn, lessfn):
57 if lessfn(keyfn(list[mid]), key): lo = mid + 1
59 if lo < n and not lessfn(key, keyfn(list[lo])): found = list[lo]
63 class WeightedChoice (object):
64 def __init__(me, choices):
67 for wt, opt in choices:
69 me._choices.append((acc - 1, opt))
72 i = rand.randrange(me._total)
73 ch, j = binsearch(me._choices, i,
74 lambda pair: pair[0], lambda x, y: x < y)
75 return me._choices[j][1]
77 ###--------------------------------------------------------------------------
78 ### Super-stupid reference implementation.
80 class Collection (object):
81 def __init__(me, items):
85 if me._list is None: me._list = list(me._set); me._list.sort()
87 if me._set is None: me._set = set(me._list)
89 me._ensure_list(); return iter(me._list)
91 me._ensure_list(); return len(me._list)
92 def __contains__(me, x):
93 me._ensure_set(); return x in me._set
95 me._ensure_set(); me._set.add(x); me._list = None
97 me._ensure_set(); me._set.remove(x); me._list = None
99 me._ensure_list(); yy._ensure_list()
101 assert not me._list or not yy._list or me._list[-1] < yy._list[0]
104 assert not me._list or me._list[-1] < x;
105 assert not yy._list or x < yy._list[0]
107 return Collection(me._list + mid + yy._list)
110 x, i = binsearch(me._list, x, lambda x: x, lambda x, y: x < y)
113 return Collection(me._list[:i]), x, Collection(me._list[j:])
115 me._ensure_set(); yy._ensure_set()
116 return Collection(me._set | yy._set), Collection(me._set&yy._set)
117 def diffsect(me, yy):
118 me._ensure_set(); yy._ensure_set()
119 return Collection(me._set - yy._set), Collection(me._set&yy._set)
122 if not me._list: return None
123 else: return me._list[0]
126 if not me._list: return None
127 else: return me._list[-1]
129 ###--------------------------------------------------------------------------
130 ### Options processing.
132 class Options (object):
134 op = OP.OptionParser\
135 (usage = "%prog [-c STEPS] [-f FILE] [-l LIMIT] "
136 "[-n STEPS] [-s SEED] [-y STEP] PROG [ARGS ...]")
137 op.disable_interspersed_args()
138 for short, long, kw in \
139 [("-c", "--ckpt-steps",
140 dict(type = "int", metavar = "STEPS",
141 dest = "ckpt_steps", default = 5000,
142 help = "number of steps between checkpoints")),
143 ("-f", "--ckpt-file",
144 dict(type = "string", metavar = "FILE",
145 dest = "ckpt_file", default = "soak.ckpt",
146 help = "file to hold checkpoint information")),
148 dict(type = "int", metavar = "LIMIT",
149 dest = "limit", default = None,
150 help = "exclusive limit value to store in test trees")),
152 dict(type = "int", metavar = "STEPS",
153 dest = "nsteps", default = None,
154 help = "number of steps to run before stopping")),
156 dict(type = "string", metavar = "SEED",
157 dest = "seed", default = None,
158 help = "force initial seed")),
160 dict(type = "int", metavar = "STEP",
161 dest = "sync", default = None,
162 help = "check and print state from STEP"))]:
163 op.add_option(short, long, **kw)
164 opts, args = op.parse_args()
165 me.limit = opts.limit
166 me.ckpt_file = opts.ckpt_file
168 me.ckpt_steps = opts.ckpt_steps
169 me.nsteps = opts.nsteps
171 if len(args) < 1: op.print_usage(SYS.stderr); SYS.exit(2)
174 ###--------------------------------------------------------------------------
177 class Level (object):
178 def __init__(me, kind, base, limit, tree = "_"):
179 xtree, _ = RX.subn(r"0x[0-9a-f]{8}\$", "", tree)
180 me.coll = Collection(map(int, RX.findall(r"\d+", xtree)))
181 me.kind, me.base, me.limit, me.tree = \
182 kind, int(base), int(limit), tree.strip()
183 me.rlim = int(M.sqrt(me.limit - me.base))
185 file.write("%s %d %d %s\n" % (me.kind, me.base, me.limit, me.tree))
188 line = file.readline()
189 if line == "": return None
190 kind, base, limit, tree = line.split(None, 3)
191 return cls(kind, base, limit, tree)
193 class State (object):
194 def __init__(me, opts):
195 me._ckpt_file = opts.ckpt_file
197 with open(me._ckpt_file, "r") as f:
198 me.seed, = f.readline().split()
207 if opts.seed is not None and me.seed != opts.seed:
208 raise ValueError("checkpointed seed `%s' /= "
209 "command-line seed `%s'" % (me.seed, opts.seed))
210 if opts.limit is not None and me.cur.limit != opts.limit:
211 raise ValueError("checkpointed limit %d /= command-line limit %d" %
212 (me.cur.limit, opts.limit))
213 except IOError as err:
214 if err.errno != E.ENOENT: raise
215 if opts.seed is not None: me.seed = opts.seed
216 else: me.seed = base64_encode(OS.urandom(SEEDSZ))
217 if opts.limit is not None: me.limit = opts.limit
218 else: me.limit = 4096
220 me.cur = Level('base', 0, me.limit)
221 me.write_ckpt(reseed = False)
222 me.rand = RND.Random(me.seed)
223 n, b = 0, me.cur.limit
225 bb = int(M.sqrt(b)) + 4
230 me.stack.append(me.cur)
235 me.cur = me.stack.pop()
237 def write_ckpt(me, reseed = True):
239 me.seed = base64_encode(bytes(me.rand.randrange(256)
240 for _ in range(SEEDSZ)))
241 me.rand.seed(me.seed)
242 new = me._ckpt_file + ".new"
243 with open(new, "w") as f:
244 f.write("%s\n" % me.seed)
245 for lv in me.stack: lv.write(f)
247 try: OS.rename(new, me._ckpt_file)
248 except OSError as err:
249 if err.errno == E.EEXIST:
250 OS.unlink(me._ckpt_file); OS.rename(new, me._ckpt_file)
252 try: OS.unlink(me._ckpt_file)
253 except OSError as err:
254 if err.errno == E.ENOENT: pass
257 ###--------------------------------------------------------------------------
261 ch = [(896, "addrm1"),
266 ch += [(ST.stklim - sp, "split"),
267 (ST.stklim - sp, "push")]
269 if ST.cur.kind == "join":
270 ch += [(sp, "join0"), (sp, "join1")]
271 elif ST.cur.kind == "setop":
272 ch += [(sp, "unisect"), (sp, "diffsect")]
273 elif ST.cur.kind == "base":
276 raise ValueError("unknown level kind `%s'" % ST.cur.kind)
278 return WeightedChoice(ch)
282 KID = SUB.Popen(OPTS.testprog, universal_newlines = True,
283 stdin = SUB.PIPE, stdout = SUB.PIPE)
287 SYS.stderr.write("%s: FAILED: %s\n" % (PROG, msg))
288 SYS.stderr.write("%s: step = %d\n" % (PROG, CSTEP))
292 SYS.stderr.write("%s: exit status = %d\n" % (PROG, rc))
295 def put(msg, echo = True):
296 try: KID.stdin.write(msg); KID.stdin.flush()
297 except IOError as err: fail("write failed: %s" % err)
298 if SYNC and echo: SYS.stdout.write("$ " + msg); SYS.stdout.flush()
299 def get(echo = True):
300 try: line = KID.stdout.readline()
301 except IOError as err: fail("read failed: %s" % err)
302 if line == "": fail("unexpected end of file")
303 if SYNC and echo: SYS.stdout.write(line)
304 if line[-1] == "\n": return line[:-1]
309 put("D\n:;;END DUMP\n", echo = False)
311 line = get(echo = False)
312 if line == ";;END DUMP": break
313 SYS.stdout.write(line); SYS.stdout.write("\n")
316 put("K\n", echo = False)
317 line = get(echo = False)
318 if ST.cur.coll: ref = " ".join("%d" % i for i in ST.cur.coll)
319 else: ref = "(empty tree)"
320 if line != ref: fail("iteration mismatch: %s /= %s" % (line, ref))
323 put("L\n", echo = False)
324 ST.cur.tree = get(echo = False)
328 put("= %s\n" % lv.tree)
331 put("= %s\n" % ST.cur.tree)
333 put("S 0x%08x\n" % ST.rand.randrange(B32))
336 while OPTS.nsteps is None or STEP < OPTS.nsteps:
338 if OPTS.sync is not None and OPTS.sync == STEP:
342 SYS.stdout.write("\n;; initial stack\n")
343 for i, lv in enumerate(ST.stack):
344 SYS.stdout.write(";; %3d = %s\n" % (i, lv.tree))
345 SYS.stdout.write(";; TOP = %s\n" % ST.cur.tree)
348 if SYNC: SYS.stdout.write("\n;; step %d\n" % CSTEP)
349 op = ch.choose(ST.rand)
352 k = ST.rand.randrange(ST.cur.base, ST.cur.limit)
353 if k in ST.cur.coll: ST.cur.coll.remove(k); put("%d-\n" % k)
354 else: ST.cur.coll.add(k); put("%d+\n" % k)
357 n = ST.rand.randrange(ST.cur.rlim)
358 i = ST.rand.randrange(ST.cur.base, ST.cur.limit - n)
360 foundp = m in ST.cur.coll
361 dir = ST.rand.choice([+1, -1])
363 if dir < 0: rr = reversed(rr)
368 if j not in ST.cur.coll: continue
369 if firstp: firstp = False
370 else: buf.write(u" ")
371 ST.cur.coll.remove(j); buf.write(u"%d-" % j)
374 if j in ST.cur.coll: continue
375 if firstp: firstp = False
376 else: buf.write(u" ")
377 ST.cur.coll.add(j); buf.write(u"%d+" % j)
378 if not firstp: buf.write(u"\n")
382 k = ST.rand.randrange(ST.cur.base, ST.cur.limit)
386 if k in ST.cur.coll: fail("key %d unexpectedly missing" % k)
388 m = RX.match(r"^#<node #0x[0-9a-f]{8} (\d+)>", line)
391 if kk != k: fail("search for key %d found %d instead" % (k, kk))
392 elif k not in ST.cur.coll: fail("key %d unexpectedly found" % k)
394 fail("unexpected response to lookup: `%s'" % line)
398 k = ST.rand.randrange(ST.cur.base, ST.cur.limit - ST.cur.rlim - 4)
400 left, mid, right = ST.cur.coll.split(k)
402 old_limit, ST.cur.limit = ST.cur.limit, k
403 ST.push(Level("split.mid", k, k + 1, str(mid)))
404 ST.push(Level("split.right", k + 1, old_limit,
405 " ".join("%d" % x for x in right)))
406 dump_tree(); check_tree(); put(")\n"); ST.pop()
407 dump_tree(); check_tree(); put(")\n"); ST.pop()
408 dump_tree(); check_tree(); snapshot()
410 if ST.cur.coll: new_base = ST.cur.coll.upper() + 2
411 else: new_base = ST.cur.base + 2
412 ST.push(Level("join", new_base, old_limit, "_"))
416 check_tree(); snapshot()
417 put("(\n"); ST.push(Level("setop", ST.cur.base, ST.cur.limit, "_"))
418 ST.stack[-1].limit = ST.cur.limit
422 lower = ST.stack[-1].coll
424 ST.stack[-1].coll = lower.join(None, ST.cur.coll)
425 ST.stack[-1].limit = ST.cur.limit
430 lower = ST.stack[-1].coll
431 if lower: base = lower.upper() + 1
432 else: base = ST.stack[-1].base + 1
433 if ST.cur.coll: limit = ST.cur.coll.lower()
434 else: limit = ST.cur.limit
435 k = ST.rand.randrange(base, limit)
437 ST.stack[-1].coll = lower.join(k, ST.cur.coll)
438 ST.stack[-1].limit = ST.cur.limit
442 elif op == "unisect":
444 ST.cur.coll, ST.stack[-1].coll = ST.cur.coll.unisect(ST.stack[-1].coll)
445 dump_tree(); check_tree(); put(")\n"); ST.pop()
448 elif op == "diffsect":
450 diff, ST.cur.coll = ST.cur.coll.diffsect(ST.stack[-1].coll)
451 ST.push(Level("diffsect.diff", ST.cur.base, ST.cur.limit,
452 " ".join("%d" % x for x in diff)))
453 dump_tree(); check_tree(); put(")\n"); ST.pop()
454 dump_tree(); check_tree(); put(")\n"); ST.pop()
458 raise ValueError("unexpected operation `%s'" % op)
460 STEP += 1; CSTEP += 1
462 if SYNC or CSTEP == OPTS.ckpt_steps: check_tree()
463 if CSTEP == OPTS.ckpt_steps:
466 CSTEP = 0; SYNC = False
467 put("S 0x%08x\n" % ST.rand.randrange(B32))
471 if not ST.stack: break
478 ###----- That's all, folks --------------------------------------------------