12 import subprocess as SUB
17 PROG = OS.path.basename(SYS.argv[0])
19 def base64_encode(buf):
20 ## No, you blundering morons, the result of Base64 encoding is text.
21 return B64.b64encode(buf).decode("us-ascii")
23 def binsearch(list, key, keyfn, lessfn):
28 if lessfn(keyfn(list[mid]), key): lo = mid + 1
30 if lo < n and not lessfn(key, keyfn(list[lo])): found = list[lo]
34 class WeightedChoice (object):
35 def __init__(me, choices):
38 for wt, opt in choices:
40 me._choices.append((acc - 1, opt))
43 i = rand.randrange(me._total)
44 ch, j = binsearch(me._choices, i,
45 lambda pair: pair[0], lambda x, y: x < y)
46 return me._choices[j][1]
48 class Collection (object):
49 def __init__(me, items):
53 if me._list is None: me._list = list(me._set); me._list.sort()
55 if me._set is None: me._set = set(me._list)
57 me._ensure_list(); return iter(me._list)
59 me._ensure_list(); return len(me._list)
60 def __contains__(me, x):
61 me._ensure_set(); return x in me._set
63 me._ensure_set(); me._set.add(x); me._list = None
65 me._ensure_set(); me._set.remove(x); me._list = None
67 me._ensure_list(); yy._ensure_list()
69 assert not me._list or not yy._list or me._list[-1] < yy._list[0]
72 assert not me._list or me._list[-1] < x;
73 assert not yy._list or x < yy._list[0]
75 return Collection(me._list + mid + yy._list)
78 x, i = binsearch(me._list, x, lambda x: x, lambda x, y: x < y)
81 return Collection(me._list[:i]), x, Collection(me._list[j:])
83 me._ensure_set(); yy._ensure_set()
84 return Collection(me._set | yy._set), Collection(me._set&yy._set)
86 me._ensure_set(); yy._ensure_set()
87 return Collection(me._set - yy._set), Collection(me._set&yy._set)
90 if not me._list: return None
91 else: return me._list[0]
94 if not me._list: return None
95 else: return me._list[-1]
97 class Options (object):
100 (usage = "%prog [-c STEPS] [-f FILE] [-l LIMIT] "
101 "[-n STEPS] [-y STEP] PROG [ARGS ...]")
102 op.disable_interspersed_args()
103 for short, long, kw in \
104 [("-c", "--ckpt-steps",
105 dict(type = "int", metavar = "STEPS",
106 dest = "ckpt_steps", default = 5000,
107 help = "number of steps between checkpoints")),
108 ("-f", "--ckpt-file",
109 dict(type = "string", metavar = "FILE",
110 dest = "ckpt_file", default = "soak.ckpt",
111 help = "file to hold checkpoint information")),
113 dict(type = "int", metavar = "LIMIT",
114 dest = "limit", default = None,
115 help = "exclusive limit value to store in test trees")),
117 dict(type = "int", metavar = "STEPS",
118 dest = "nsteps", default = None,
119 help = "number of steps to run before stopping")),
121 dict(type = "int", metavar = "STEP",
122 dest = "sync", default = None,
123 help = "check and print state from STEP"))]:
124 op.add_option(short, long, **kw)
125 opts, args = op.parse_args()
126 me.limit = opts.limit
127 me.ckpt_file = opts.ckpt_file
129 me.ckpt_steps = opts.ckpt_steps
130 me.nsteps = opts.nsteps
131 if len(args) < 1: op.print_usage(SYS.stderr); SYS.exit(2)
134 class Level (object):
135 def __init__(me, kind, base, limit, tree = "_"):
136 me.coll = Collection(map(int, RX.findall(r"\d+", tree)))
137 me.kind, me.base, me.limit, me.tree = \
138 kind, int(base), int(limit), tree.strip()
139 me.rlim = int(M.sqrt(me.limit - me.base))
141 file.write("%s %d %d %s\n" % (me.kind, me.base, me.limit, me.tree))
144 line = file.readline()
145 if line == "": return None
146 kind, base, limit, tree = line.split(maxsplit = 3)
147 return cls(kind, base, limit, tree)
149 class State (object):
150 def __init__(me, opts):
151 me._ckpt_file = opts.ckpt_file
153 with open(me._ckpt_file, "r") as f:
154 me.seed, = f.readline().split()
163 if opts.limit is not None and me.cur.limit != opts.limit:
164 raise ValueError("checkpointed limit %d /= command-line limit %d" %
165 (me.cur.limit, opts.limit))
166 except OSError as err:
167 if err.errno != E.ENOENT: raise
168 me.seed = base64_encode(OS.urandom(SEEDSZ))
169 if opts.limit is not None: me.limit = opts.limit
170 else: me.limit = 4096
172 me.cur = Level('base', 0, me.limit)
173 me.write_ckpt(reseed = False)
174 me.rand = RND.Random(me.seed)
175 n, b = 0, me.cur.limit
177 bb = int(M.sqrt(b)) + 4
182 me.stack.append(me.cur)
187 me.cur = me.stack.pop()
189 def write_ckpt(me, reseed = True):
191 me.seed = base64_encode(bytes(me.rand.randrange(256)
192 for _ in range(SEEDSZ)))
193 me.rand.seed(me.seed)
194 new = me._ckpt_file + ".new"
195 with open(new, "w") as f:
196 f.write("%s\n" % me.seed)
197 for lv in me.stack: lv.write(f)
199 OS.rename(new, me._ckpt_file)
201 try: OS.unlink(me._ckpt_file)
202 except OSError as err:
203 if err.errno == E.ENOENT: pass
207 ch = [(896, "addrm1"),
212 ch += [(ST.stklim - sp, "split"),
213 (ST.stklim - sp, "push")]
215 if ST.cur.kind == "join":
216 ch += [(sp, "join0"), (sp, "join1")]
217 elif ST.cur.kind == "setop":
218 ch += [(sp, "unisect"), (sp, "diffsect")]
219 elif ST.cur.kind == "base":
222 raise ValueError("unknown level kind `%s'" % ST.cur.kind)
224 return WeightedChoice(ch)
228 KID = SUB.Popen(OPTS.testprog, stdin = SUB.PIPE, stdout = SUB.PIPE)
232 SYS.stderr.write("%s: FAILED: %s\n" % (PROG, msg))
233 SYS.stderr.write("%s: step = %d\n" % (PROG, CSTEP))
237 SYS.stderr.write("%s: exit status = %d\n" % (PROG, rc))
240 def put(msg, echo = True):
241 try: KID.stdin.write(msg.encode()); KID.stdin.flush()
242 except OSError as err: fail("write failed: %s" % err)
243 if SYNC and echo: SYS.stdout.write("$ " + msg); SYS.stdout.flush()
244 def get(echo = True):
245 try: line = KID.stdout.readline().decode()
246 except OSError as err: fail("read failed: %s" % err)
247 if line == "": fail("unexpected end of file")
248 if SYNC and echo: SYS.stdout.write(line)
249 if line[-1] == "\n": return line[:-1]
254 put("D\n:;;END DUMP\n", echo = False)
256 line = get(echo = False)
257 if line == ";;END DUMP": break
258 SYS.stdout.write(line); SYS.stdout.write("\n")
261 put("i\n", echo = False)
262 line = get(echo = False)
263 if ST.cur.coll: ref = " ".join("%d" % i for i in ST.cur.coll)
264 else: ref = "(empty tree)"
265 if line != ref: fail("iteration mismatch: %s /= %s" % (line, ref))
266 put("!:;;END CHECK\n", echo = False)
267 line = get(echo = False)
268 if line != ";;END CHECK": fail("unexpected output: `%s'" % line)
271 put("L\n", echo = False)
272 ST.cur.tree = get(echo = False)
275 put("= %s\n" % lv.tree)
278 put("= %s\n" % ST.cur.tree)
283 while OPTS.nsteps is None or STEP < OPTS.nsteps:
285 if OPTS.sync is not None and OPTS.sync == STEP:
289 SYS.stdout.write("\n;; initial stack\n")
290 for i, lv in enumerate(ST.stack):
291 SYS.stdout.write(";; %3d = %s\n" % (i, lv.tree))
292 SYS.stdout.write(";; TOP = %s\n" % ST.cur.tree)
295 if SYNC: SYS.stdout.write("\n;; step %d\n" % CSTEP)
296 op = ch.choose(ST.rand)
299 k = ST.rand.randrange(ST.cur.base, ST.cur.limit)
300 if k in ST.cur.coll: ST.cur.coll.remove(k); put("%d-\n" % k)
301 else: ST.cur.coll.add(k); put("%d+\n" % k)
304 n = ST.rand.randrange(ST.cur.rlim)
305 i = ST.rand.randrange(ST.cur.base, ST.cur.limit - n)
307 foundp = m in ST.cur.coll
308 dir = ST.rand.choice([+1, -1])
310 if dir < 0: rr = reversed(rr)
315 if j not in ST.cur.coll: continue
316 if firstp: firstp = False
318 ST.cur.coll.remove(j); buf.write("%d-" % j)
321 if j in ST.cur.coll: continue
322 if firstp: firstp = False
324 ST.cur.coll.add(j); buf.write("%d+" % j)
325 if not firstp: buf.write("\n")
329 k = ST.rand.randrange(ST.cur.base, ST.cur.limit)
333 if k in ST.cur.coll: fail("key %d unexpectedly missing" % k)
335 m = RX.match(r"^#<node #0x[0-9a-f]{8} (\d+)>", line)
338 if kk != k: fail("search for key %d found %d instead" % (k, kk))
339 elif k not in ST.cur.coll: fail("key %d unexpectedly found" % k)
341 fail("unexpected response to lookup: `%s'" % line)
345 k = ST.rand.randrange(ST.cur.base, ST.cur.limit - ST.cur.rlim - 4)
347 left, mid, right = ST.cur.coll.split(k)
349 old_limit, ST.cur.limit = ST.cur.limit, k
350 ST.push(Level("split.mid", k, k + 1, str(mid)))
351 ST.push(Level("split.right", k + 1, old_limit,
352 " ".join("%d" % x for x in right)))
353 dump_tree(); check_tree(); put(")\n"); ST.pop()
354 dump_tree(); check_tree(); put(")\n"); ST.pop()
355 dump_tree(); check_tree(); snapshot()
357 if ST.cur.coll: new_base = ST.cur.coll.upper() + 2
358 else: new_base = ST.cur.base + 2
359 ST.push(Level("join", new_base, old_limit, "_"))
363 check_tree(); snapshot()
364 put("(\n"); ST.push(Level("setop", ST.cur.base, ST.cur.limit, "_"))
365 ST.stack[-1].limit = ST.cur.limit
369 lower = ST.stack[-1].coll
371 ST.stack[-1].coll = lower.join(None, ST.cur.coll)
372 ST.stack[-1].limit = ST.cur.limit
377 lower = ST.stack[-1].coll
378 if lower: base = lower.upper() + 1
379 else: base = ST.stack[-1].base + 1
380 if ST.cur.coll: limit = ST.cur.coll.lower()
381 else: limit = ST.cur.limit
382 k = ST.rand.randrange(base, limit)
384 ST.stack[-1].coll = lower.join(k, ST.cur.coll)
385 ST.stack[-1].limit = ST.cur.limit
389 elif op == "unisect":
391 ST.cur.coll, ST.stack[-1].coll = ST.cur.coll.unisect(ST.stack[-1].coll)
392 dump_tree(); check_tree(); put(")\n"); ST.pop()
395 elif op == "diffsect":
397 diff, ST.cur.coll = ST.cur.coll.diffsect(ST.stack[-1].coll)
398 ST.push(Level("diffsect.diff", ST.cur.base, ST.cur.limit,
399 " ".join("%d" % x for x in diff)))
400 dump_tree(); check_tree(); put(")\n"); ST.pop()
401 dump_tree(); check_tree(); put(")\n"); ST.pop()
405 raise ValueError("unexpected operation `%s'" % op)
407 STEP += 1; CSTEP += 1
409 if SYNC or CSTEP == OPTS.ckpt_steps: check_tree()
410 if CSTEP == OPTS.ckpt_steps:
413 CSTEP = 0; SYNC = False
417 if not ST.stack: break