173 lines
6.1 KiB
Python
173 lines
6.1 KiB
Python
"""Corpus generator for mplex_flow_test.cpp: runs the real tai.py validate_flow_spec and
|
|
PlexerFlow over generated specs and writes the results as JSON lines.
|
|
|
|
usage: python3 -I mplex_flow_gen.py <repo root> <out file>
|
|
"""
|
|
import copy
|
|
import importlib.util
|
|
import json
|
|
import random
|
|
import sys
|
|
|
|
root, out_path = sys.argv[1], sys.argv[2]
|
|
spec = importlib.util.spec_from_file_location("tai_mod", root + "/tai.py")
|
|
tai = importlib.util.module_from_spec(spec)
|
|
sys.modules["tai_mod"] = tai
|
|
spec.loader.exec_module(tai)
|
|
|
|
rng = random.Random(20260101)
|
|
|
|
BASES = [
|
|
{"v": 1, "title": "Deploy", "timeout": 300, "start": "main", "screens": [
|
|
{"id": "main", "title": "Where", "body": "Pick a target.",
|
|
"fields": [
|
|
{"id": "env", "type": "choice", "label": "Environment", "options": ["staging", "prod"], "default": 0},
|
|
{"id": "tag", "type": "text", "label": "Tag", "initial": "v1", "show_if": {"env": "prod"}},
|
|
{"id": "note", "type": "info", "text": "Prod deploys page the team."}],
|
|
"actions": [{"id": "go", "label": "Deploy"}, {"id": "back", "label": "Back"}],
|
|
"next": {"go": "$done", "back": "$cancel"}}]},
|
|
{"screens": [{"id": "a", "body": "hello"}, {"id": "b", "title": "B", "fields": [
|
|
{"id": "x", "type": "choice", "options": [{"label": "One", "value": 1}, {"label": "Two", "value": 2.5}, "three"]},
|
|
{"id": "y", "type": "text", "label": "Name é中", "show_if": {"x": [2.5, "three"]}}],
|
|
"next": "c"}, {"id": "c", "title": "C", "show_if": {"x": 1}, "body": "only one"},
|
|
{"id": "d", "actions": [{"id": "ok", "label": "OK"}, {"id": "again", "label": "Again"}],
|
|
"next": {"again": "a"}}]},
|
|
{"title": "T", "timeout": 5, "screens": [{"id": "only", "fields": [{"id": "i", "type": "info", "text": "just info"}]}]},
|
|
{"start": "second", "screens": [{"id": "first", "body": "1"}, {"id": "second", "body": "2", "next": "$cancel"}]},
|
|
]
|
|
|
|
ODD = [None, True, False, 0, 1, -3, 7, 1.0, 2.5, "", "x", " ", "a b", "$done", "$cancel", [], {}, [1], ["a"], {"a": 1}, "é" * 80, "x" * 65, "y" * 201, "z" * 2001]
|
|
|
|
|
|
def walk(node, path=()):
|
|
yield node, path
|
|
if isinstance(node, dict):
|
|
for key, value in list(node.items()):
|
|
yield from walk(value, path + (key,))
|
|
elif isinstance(node, list):
|
|
for index, value in enumerate(node):
|
|
yield from walk(value, path + (index,))
|
|
|
|
|
|
def at(node, path):
|
|
for step in path:
|
|
node = node[step]
|
|
return node
|
|
|
|
|
|
def mutate(base):
|
|
spec = copy.deepcopy(base)
|
|
for _ in range(rng.choice([1, 1, 1, 1, 2, 3])):
|
|
places = [path for _node, path in walk(spec) if path]
|
|
if not places:
|
|
break
|
|
path = rng.choice(places)
|
|
parent = at(spec, path[:-1])
|
|
key = path[-1]
|
|
action = rng.choice(["replace", "delete", "dupe", "replace"])
|
|
if action == "delete":
|
|
if isinstance(parent, dict):
|
|
del parent[key]
|
|
else:
|
|
parent.pop(key)
|
|
elif action == "dupe" and isinstance(parent, list) and parent:
|
|
parent.append(copy.deepcopy(parent[rng.randrange(len(parent))]))
|
|
else:
|
|
parent[key] = copy.deepcopy(rng.choice(ODD))
|
|
return spec
|
|
|
|
|
|
def run_validate(candidate):
|
|
try:
|
|
app, err = tai.validate_flow_spec(copy.deepcopy(candidate))
|
|
except Exception: # noqa: BLE001 python crashes are not part of the contract
|
|
return None
|
|
return app, err
|
|
|
|
|
|
class Script(tai.PlexerController):
|
|
kind = "script"
|
|
|
|
def __init__(self, replies):
|
|
self.replies = list(replies)
|
|
self.asks = []
|
|
|
|
def ask(self, screen, timeout):
|
|
self.asks.append(screen)
|
|
if not self.replies:
|
|
return {"status": "cancelled"}
|
|
return self.replies.pop(0)
|
|
|
|
|
|
def random_replies():
|
|
out = []
|
|
for _ in range(rng.randrange(0, 70)):
|
|
roll = rng.random()
|
|
if roll < 0.04:
|
|
out.append({"status": "cancelled"})
|
|
elif roll < 0.07:
|
|
out.append({"status": "timeout"})
|
|
elif roll < 0.55:
|
|
out.append({"status": "ok", "value": rng.randrange(0, 3)})
|
|
else:
|
|
out.append({"status": "ok", "value": rng.choice(["", "v2", "café", True])})
|
|
return out
|
|
|
|
|
|
def fix_replies(app, replies):
|
|
"""Index replies must be valid for the screen they answer; replay with a guard controller."""
|
|
guard = []
|
|
|
|
class Guard(tai.PlexerController):
|
|
kind = "guard"
|
|
|
|
def __init__(self):
|
|
self.todo = list(replies)
|
|
|
|
def ask(self, screen, timeout):
|
|
if not self.todo:
|
|
reply = {"status": "cancelled"}
|
|
else:
|
|
reply = self.todo.pop(0)
|
|
if reply.get("status") == "ok":
|
|
if screen["kind"] == "choice":
|
|
reply = {"status": "ok", "value": reply["value"] % len(screen["options"]) if isinstance(reply["value"], int) and not isinstance(reply["value"], bool) else 0}
|
|
elif screen["kind"] == "text":
|
|
reply = {"status": "ok", "value": reply["value"] if isinstance(reply["value"], str) else "t"}
|
|
else:
|
|
reply = {"status": "ok", "value": True}
|
|
guard.append(reply)
|
|
return reply
|
|
|
|
return Guard(), guard
|
|
|
|
|
|
lines = []
|
|
cases = 0
|
|
seen_specs = []
|
|
for base in BASES:
|
|
seen_specs.append(base)
|
|
for _ in range(4000):
|
|
seen_specs.append(mutate(rng.choice(BASES)))
|
|
for _ in range(60):
|
|
seen_specs.append(rng.choice(ODD))
|
|
for candidate in seen_specs:
|
|
result = run_validate(candidate)
|
|
if result is None:
|
|
continue
|
|
app, err = result
|
|
lines.append(json.dumps({"kind": "spec", "spec": candidate, "app": app, "err": err}))
|
|
cases += 1
|
|
if app is not None:
|
|
for _round in range(3):
|
|
controller, used = fix_replies(app, random_replies())
|
|
try:
|
|
outcome = tai.PlexerFlow(copy.deepcopy(app)).run(controller)
|
|
except Exception: # noqa: BLE001
|
|
continue
|
|
lines.append(json.dumps({"kind": "flow", "app": app, "replies": used, "outcome": outcome}))
|
|
cases += 1
|
|
with open(out_path, "w") as handle:
|
|
handle.write("\n".join(lines) + "\n")
|
|
print("cases", cases)
|