| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
import argparse |
| 16 |
import json |
| 17 |
import pathlib |
| 18 |
import sys |
| 19 |
import time |
| 20 |
import urllib.error |
| 21 |
import urllib.request |
| 22 |
|
| 23 |
import pt_convert |
| 24 |
|
| 25 |
RAW = "https://raw.githubusercontent.com/SingleStepTests/{repo}/main/{sub}/{name}.json" |
| 26 |
|
| 27 |
VARIANTS = { |
| 28 |
|
| 29 |
|
| 30 |
"wdc65c02": { |
| 31 |
"repo": "65x02", "sub": "wdc65c02/v1", "suffix": "", |
| 32 |
"cpu": pt_convert.CPU_65C02, |
| 33 |
}, |
| 34 |
|
| 35 |
|
| 36 |
"816-emu": { |
| 37 |
"repo": "65816", "sub": "v1", "suffix": ".e", |
| 38 |
"cpu": pt_convert.CPU_65816, |
| 39 |
}, |
| 40 |
"816-native": { |
| 41 |
"repo": "65816", "sub": "v1", "suffix": ".n", |
| 42 |
"cpu": pt_convert.CPU_65816, |
| 43 |
}, |
| 44 |
} |
| 45 |
|
| 46 |
ATTEMPTS = 4 |
| 47 |
|
| 48 |
|
| 49 |
def fetch_opcode(variant: str, op: int, dest: pathlib.Path) -> bool: |
| 50 |
spec = VARIANTS[variant] |
| 51 |
name = f"{op:02x}{spec['suffix']}" |
| 52 |
out = dest / f"{name}.json" |
| 53 |
if out.exists() and out.stat().st_size > 0: |
| 54 |
return True |
| 55 |
url = RAW.format(repo=spec["repo"], sub=spec["sub"], name=name) |
| 56 |
|
| 57 |
|
| 58 |
last = None |
| 59 |
for attempt in range(ATTEMPTS): |
| 60 |
try: |
| 61 |
with urllib.request.urlopen(url, timeout=60) as r: |
| 62 |
data = r.read() |
| 63 |
out.write_bytes(data) |
| 64 |
return True |
| 65 |
except urllib.error.HTTPError as e: |
| 66 |
if e.code == 404: |
| 67 |
return False |
| 68 |
last = e |
| 69 |
except Exception as e: |
| 70 |
last = e |
| 71 |
if attempt < ATTEMPTS - 1: |
| 72 |
time.sleep(2 ** attempt) |
| 73 |
raise SystemExit(f"${op:02X}: giving up after {ATTEMPTS} attempts: {last}") |
| 74 |
|
| 75 |
|
| 76 |
def load_cases(path: pathlib.Path): |
| 77 |
|
| 78 |
|
| 79 |
|
| 80 |
|
| 81 |
|
| 82 |
if path.stat().st_size == 0: |
| 83 |
return [] |
| 84 |
try: |
| 85 |
return json.loads(path.read_text(encoding="utf-8")) |
| 86 |
except json.JSONDecodeError: |
| 87 |
path.unlink() |
| 88 |
raise SystemExit(f"{path.name} is not valid JSON; deleted, run again") |
| 89 |
|
| 90 |
|
| 91 |
def main() -> int: |
| 92 |
ap = argparse.ArgumentParser() |
| 93 |
ap.add_argument("--variant", default="wdc65c02", choices=sorted(VARIANTS)) |
| 94 |
ap.add_argument( |
| 95 |
"--cases", |
| 96 |
type=int, |
| 97 |
default=100, |
| 98 |
help="cases per opcode, taken as a prefix (default 100)", |
| 99 |
) |
| 100 |
ap.add_argument( |
| 101 |
"--opcodes", |
| 102 |
default="", |
| 103 |
help="comma-separated hex opcodes; default is all 256", |
| 104 |
) |
| 105 |
ap.add_argument( |
| 106 |
"--force", |
| 107 |
action="store_true", |
| 108 |
help="re-fetch and re-convert even if the fixture already exists", |
| 109 |
) |
| 110 |
args = ap.parse_args() |
| 111 |
|
| 112 |
spec = VARIANTS[args.variant] |
| 113 |
here = pathlib.Path(__file__).resolve().parent |
| 114 |
out = here / "pt" / f"{args.variant}.bin" |
| 115 |
|
| 116 |
|
| 117 |
|
| 118 |
if out.exists() and out.stat().st_size > 0 and not args.force: |
| 119 |
print(f"{out} already present ({out.stat().st_size / 1e6:.1f} MB); " |
| 120 |
f"pass --force to rebuild") |
| 121 |
return 0 |
| 122 |
|
| 123 |
dest = here / "pt" / args.variant |
| 124 |
dest.mkdir(parents=True, exist_ok=True) |
| 125 |
|
| 126 |
if args.opcodes: |
| 127 |
wanted = [int(x, 16) for x in args.opcodes.split(",")] |
| 128 |
else: |
| 129 |
wanted = list(range(256)) |
| 130 |
|
| 131 |
have = [] |
| 132 |
for op in wanted: |
| 133 |
sys.stdout.write(f"\rfetching ${op:02X}...") |
| 134 |
sys.stdout.flush() |
| 135 |
if fetch_opcode(args.variant, op, dest): |
| 136 |
have.append(op) |
| 137 |
print(f"\r{len(have)} opcodes available ") |
| 138 |
|
| 139 |
cases = [] |
| 140 |
empty = [] |
| 141 |
short = [] |
| 142 |
for op in have: |
| 143 |
raw = load_cases(dest / f"{op:02x}{spec['suffix']}.json") |
| 144 |
if not raw: |
| 145 |
empty.append(op) |
| 146 |
continue |
| 147 |
if len(raw) < args.cases: |
| 148 |
short.append((op, len(raw))) |
| 149 |
cases.extend((op, spec["cpu"], c) for c in raw[: args.cases]) |
| 150 |
|
| 151 |
if empty: |
| 152 |
print("no cases: " + " ".join(f"${o:02X}" for o in empty)) |
| 153 |
for op, n in short: |
| 154 |
print(f" ${op:02X}: only {n} cases, wanted {args.cases}") |
| 155 |
|
| 156 |
pt_convert.write_fixture(out, cases) |
| 157 |
print(f"wrote {out} -- {len(cases)} cases, {out.stat().st_size / 1e6:.1f} MB") |
| 158 |
return 0 |
| 159 |
|
| 160 |
|
| 161 |
if __name__ == "__main__": |
| 162 |
raise SystemExit(main()) |