mirror of
https://github.com/a2-4am/wozardry.git
synced 2025-01-22 06:30:48 +00:00
added some tests
This commit is contained in:
parent
98d1763c34
commit
ce82be2db1
12
README.md
12
README.md
@ -1,3 +1,15 @@
|
|||||||
|
# Installation
|
||||||
|
|
||||||
|
`wozardry` is written in [Python 3](https://www.python.org).
|
||||||
|
|
||||||
|
It requires [bitarray](https://pypi.org/project/bitarray/), which can be installed thusly:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ pip3 install -U bitarray
|
||||||
|
```
|
||||||
|
|
||||||
|
(Developers who wish to run the test suite should also install the `pytest` module with `pip3 install -U pytest`)
|
||||||
|
|
||||||
# Command line usage
|
# Command line usage
|
||||||
|
|
||||||
wozardry is primarily designed to be used on the command line to directly manipulate `.woz` disk images. It supports multiple commands, which are listed in the `wozardry -h` output.
|
wozardry is primarily designed to be used on the command line to directly manipulate `.woz` disk images. It supports multiple commands, which are listed in the `wozardry -h` output.
|
||||||
|
437
test_wozardry.py
Executable file
437
test_wozardry.py
Executable file
@ -0,0 +1,437 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Sources of all truth:
|
||||||
|
#
|
||||||
|
# - WOZ1 specification <https://applesaucefdc.com/woz/reference1/>
|
||||||
|
# - WOZ2 specification <https://applesaucefdc.com/woz/reference2/>
|
||||||
|
#
|
||||||
|
# There is no spec but the spec itself.
|
||||||
|
|
||||||
|
import wozardry
|
||||||
|
import pytest # https://pypi.org/project/pytest/
|
||||||
|
import argparse
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import io
|
||||||
|
|
||||||
|
# two valid .woz files in the repository
|
||||||
|
kValid1 = "test/valid1.woz"
|
||||||
|
kValid2 = "test/valid2.woz"
|
||||||
|
|
||||||
|
# valid WOZ1 header as string of hex
|
||||||
|
kHeader1 = "57 4F 5A 31 FF 0A 0D 0A 00 00 00 00 "
|
||||||
|
|
||||||
|
# valid WOZ2 header as string of hex
|
||||||
|
kHeader2 = "57 4F 5A 32 FF 0A 0D 0A 00 00 00 00 "
|
||||||
|
|
||||||
|
def bfh(s):
|
||||||
|
"""utility function to convert string of hex into a BytesIO stream"""
|
||||||
|
return io.BytesIO(bytes.fromhex(s))
|
||||||
|
|
||||||
|
#----- test file parser -----
|
||||||
|
|
||||||
|
def test_parse_header():
|
||||||
|
# incomplete header
|
||||||
|
with pytest.raises(wozardry.WozEOFError):
|
||||||
|
wozardry.WozReader(stream=bfh("57 4F 5A 32"))
|
||||||
|
|
||||||
|
with pytest.raises(wozardry.WozEOFError):
|
||||||
|
wozardry.WozReader(stream=bfh("57 4F 5A 32 FF 0A 0D"))
|
||||||
|
|
||||||
|
with pytest.raises(wozardry.WozEOFError):
|
||||||
|
wozardry.WozReader(stream=bfh("57 4F 5A 32 FF 0A 0D 0A 00 00 00"))
|
||||||
|
|
||||||
|
# invalid signature at offset 0
|
||||||
|
with pytest.raises(wozardry.WozHeaderError_NoWOZMarker):
|
||||||
|
wozardry.WozReader(stream=bfh("57 4F 5A 30 00 00 00 00"))
|
||||||
|
|
||||||
|
# invalid signature at offset 0
|
||||||
|
with pytest.raises(wozardry.WozHeaderError_NoWOZMarker):
|
||||||
|
wozardry.WozReader(stream=bfh("57 4F 5A 33 00 00 00 00"))
|
||||||
|
|
||||||
|
# missing FF byte at offset 4
|
||||||
|
with pytest.raises(wozardry.WozHeaderError_NoFF):
|
||||||
|
wozardry.WozReader(stream=bfh("57 4F 5A 32 00 0A 0D 0A"))
|
||||||
|
|
||||||
|
# missing 0A byte at offset 5
|
||||||
|
with pytest.raises(wozardry.WozHeaderError_NoLF):
|
||||||
|
wozardry.WozReader(stream=bfh("57 4F 5A 32 FF 0D 0D 0D"))
|
||||||
|
|
||||||
|
# missing 0D byte at offset 6
|
||||||
|
with pytest.raises(wozardry.WozHeaderError_NoLF):
|
||||||
|
wozardry.WozReader(stream=bfh("57 4F 5A 32 FF 0A 0A 0A"))
|
||||||
|
|
||||||
|
# missing 0A byte at offset 7
|
||||||
|
with pytest.raises(wozardry.WozHeaderError_NoLF):
|
||||||
|
wozardry.WozReader(stream=bfh("57 4F 5A 32 FF 0A 0D 0D"))
|
||||||
|
|
||||||
|
def test_parse_info():
|
||||||
|
# TMAP chunk before INFO chunk
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_MissingINFOChunk):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "54 4D 41 50 A0 00 00 00 " + "FF "*160))
|
||||||
|
|
||||||
|
# wrong INFO chunk size (too small)
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3B 00 00 00 " + "00 "*59))
|
||||||
|
|
||||||
|
# wrong INFO chunk size (too big)
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3D 00 00 00 " + "00 "*61))
|
||||||
|
|
||||||
|
# invalid version (0) in a WOZ1 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadVersion):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader1 + "49 4E 46 4F 3C 00 00 00 00" + "00 "*59))
|
||||||
|
|
||||||
|
# invalid version (0) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadVersion):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 00" + "00 "*59))
|
||||||
|
|
||||||
|
# invalid version (1) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadVersion):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 01" + "00 "*59))
|
||||||
|
|
||||||
|
# invalid version (2) in a WOZ1 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadVersion):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader1 + "49 4E 46 4F 3C 00 00 00 02" + "00 "*59))
|
||||||
|
|
||||||
|
# invalid disk type (0) in a WOZ1 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadDiskType):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader1 + "49 4E 46 4F 3C 00 00 00 01 00 " + "00 "*58))
|
||||||
|
|
||||||
|
# invalid disk type (0) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadDiskType):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 00 " + "00 "*58))
|
||||||
|
|
||||||
|
# invalid disk type (3) in a WOZ1 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadDiskType):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader1 + "49 4E 46 4F 3C 00 00 00 01 03 " + "00 "*58))
|
||||||
|
|
||||||
|
# invalid disk type (3) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadDiskType):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 03 " + "00 "*58))
|
||||||
|
|
||||||
|
# invalid write protected flag (2) in a WOZ1 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadWriteProtected):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader1 + "49 4E 46 4F 3C 00 00 00 01 01 02 " + "00 "*57))
|
||||||
|
|
||||||
|
# invalid write protected flag (2) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadWriteProtected):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 02 " + "00 "*57))
|
||||||
|
|
||||||
|
# invalid synchronized flag (2) in a WOZ1 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadSynchronized):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader1 + "49 4E 46 4F 3C 00 00 00 01 01 00 02 " + "00 "*56))
|
||||||
|
|
||||||
|
# invalid synchronized flag (2) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadSynchronized):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 02 " + "00 "*56))
|
||||||
|
|
||||||
|
# invalid cleaned flag (2) in a WOZ1 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadCleaned):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader1 + "49 4E 46 4F 3C 00 00 00 01 01 00 00 02 " + "00 "*55))
|
||||||
|
|
||||||
|
# invalid cleaned flag (2) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadCleaned):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 02 " + "00 "*55))
|
||||||
|
|
||||||
|
# invalid creator (bad UTF-8 bytes) in a WOZ1 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadCreator):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader1 + "49 4E 46 4F 3C 00 00 00 01 01 00 00 00 E0 80 80 " + "00 "*52))
|
||||||
|
|
||||||
|
# invalid creator (bad UTF-8 bytes) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadCreator):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 E0 80 80 " + "00 "*52))
|
||||||
|
|
||||||
|
# invalid disk sides (0) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadDiskSides):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "00 " + "00 "*22))
|
||||||
|
|
||||||
|
# invalid disk sides (3) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadDiskSides):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "03 " + "00 "*22))
|
||||||
|
|
||||||
|
# invalid disk sides (2, when disk type = 1) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadDiskSides):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "02 " + "00 "*22))
|
||||||
|
|
||||||
|
# invalid boot sector format (4) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadBootSectorFormat):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "01 04 " + "00 "*21))
|
||||||
|
|
||||||
|
# invalid boot sector format (1, when disk type = 2) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadBootSectorFormat):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 02 00 00 00 " + "20 "*32 + "01 01 " + "00 "*21))
|
||||||
|
|
||||||
|
# invalid boot sector format (2, when disk type = 2) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadBootSectorFormat):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 02 00 00 00 " + "20 "*32 + "01 02 " + "00 "*21))
|
||||||
|
|
||||||
|
# invalid boot sector format (3, when disk type = 2) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadBootSectorFormat):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 02 00 00 00 " + "20 "*32 + "01 02 " + "00 "*21))
|
||||||
|
|
||||||
|
# invalid optimal bit timing (23, when disk type = 1) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadOptimalBitTiming):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "01 00 17 " + "00 "*20))
|
||||||
|
|
||||||
|
# invalid optimal bit timing (41, when disk type = 1) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadOptimalBitTiming):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "01 00 29 " + "00 "*20))
|
||||||
|
|
||||||
|
# invalid optimal bit timing (7, when disk type = 2) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadOptimalBitTiming):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 02 00 00 00 " + "20 "*32 + "01 00 07 " + "00 "*20))
|
||||||
|
|
||||||
|
# invalid optimal bit timing (25, when disk type = 2) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadOptimalBitTiming):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 02 00 00 00 " + "20 "*32 + "01 00 19 " + "00 "*20))
|
||||||
|
|
||||||
|
# invalid optimal bit timing (0, when disk type = 1) in a WOZ2 file
|
||||||
|
# unlike other fields, this does not allow a 0 value to mean "unknown"
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadOptimalBitTiming):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "01 00 00 " + "00 "*20))
|
||||||
|
|
||||||
|
# invalid optimal bit timing (0, when disk type = 2) in a WOZ2 file
|
||||||
|
# unlike other fields, this does not allow a 0 value to mean "unknown"
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadOptimalBitTiming):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 02 00 00 00 " + "20 "*32 + "01 00 00 " + "00 "*20))
|
||||||
|
|
||||||
|
# invalid compatible hardware (00000010 00000000) in a WOZ2 file
|
||||||
|
# this field only uses the lower 9 bits (for 9 hardware models), so the 7 high bits must all be 0
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadCompatibleHardware):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "01 00 20 00 02 " + "00 "*18))
|
||||||
|
|
||||||
|
# invalid compatible hardware (00000100 00000000) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadCompatibleHardware):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "01 00 20 00 04 " + "00 "*18))
|
||||||
|
|
||||||
|
# invalid compatible hardware (00001000 00000000) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadCompatibleHardware):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "01 00 20 00 08 " + "00 "*18))
|
||||||
|
|
||||||
|
# invalid compatible hardware (00010000 00000000) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadCompatibleHardware):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "01 00 20 00 10 " + "00 "*18))
|
||||||
|
|
||||||
|
# invalid compatible hardware (00100000 00000000) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadCompatibleHardware):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "01 00 20 00 20 " + "00 "*18))
|
||||||
|
|
||||||
|
# invalid compatible hardware (01000000 00000000) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadCompatibleHardware):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "01 00 20 00 40 " + "00 "*18))
|
||||||
|
|
||||||
|
# invalid compatible hardware (10000000 00000000) in a WOZ2 file
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadCompatibleHardware):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "01 00 20 00 80 " + "00 "*18))
|
||||||
|
|
||||||
|
def test_parse_tmap():
|
||||||
|
# missing TMAP chunk
|
||||||
|
with pytest.raises(wozardry.WozTMAPFormatError_MissingTMAPChunk):
|
||||||
|
wozardry.WozReader(stream=bfh(kHeader2 + "49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "01 00 20 00 00 " + "00 "*18))
|
||||||
|
|
||||||
|
# TRKS chunk before TMAP chunk
|
||||||
|
with pytest.raises(wozardry.WozTMAPFormatError_MissingTMAPChunk):
|
||||||
|
wozardry.WozReader(
|
||||||
|
stream=bfh(
|
||||||
|
kHeader2 + \
|
||||||
|
"49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "01 00 20 00 00 " + "00 "*18 + \
|
||||||
|
"54 52 4B 53 00 00 00 00 "))
|
||||||
|
|
||||||
|
# TMAP points to non-existent TRK in TRKS chunk
|
||||||
|
with pytest.raises(wozardry.WozTMAPFormatError_BadTRKS):
|
||||||
|
wozardry.WozReader(
|
||||||
|
stream=bfh(
|
||||||
|
kHeader2 + \
|
||||||
|
"49 4E 46 4F 3C 00 00 00 02 01 00 00 00 " + "20 "*32 + "01 00 20 00 00 " + "00 "*18 + \
|
||||||
|
"54 4D 41 50 A0 00 00 00 00 " + "FF "*159 + \
|
||||||
|
"54 52 4B 53 00 00 00 00 "))
|
||||||
|
|
||||||
|
def test_parse_meta():
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#----- test command-line interface -----
|
||||||
|
|
||||||
|
def test_command_verify():
|
||||||
|
"""verify a valid WOZ1/WOZ2 file and exit cleanly"""
|
||||||
|
wozardry.parse_args(["verify", kValid1])
|
||||||
|
wozardry.parse_args(["verify", kValid2])
|
||||||
|
|
||||||
|
def test_command_dump_woz1(capsys):
|
||||||
|
"""dump a WOZ1 file and ensure it prints expected output"""
|
||||||
|
wozardry.parse_args(["dump", kValid1])
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "INFO: File format version: 1" in captured.out
|
||||||
|
assert "INFO: Disk type: 5.25-inch (140K)" in captured.out
|
||||||
|
assert "INFO: Write protected: no" in captured.out
|
||||||
|
assert "INFO: Tracks synchronized: no" in captured.out
|
||||||
|
assert "INFO: Weakbits cleaned: no" in captured.out
|
||||||
|
assert "INFO: Creator: wozardry" in captured.out
|
||||||
|
|
||||||
|
def test_command_dump_woz2(capsys):
|
||||||
|
"""dump a WOZ2 file and ensure it prints expected output"""
|
||||||
|
wozardry.parse_args(["dump", kValid2])
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "INFO: File format version: 2" in captured.out
|
||||||
|
assert "INFO: Disk type: 5.25-inch (140K)" in captured.out
|
||||||
|
assert "INFO: Write protected: no" in captured.out
|
||||||
|
assert "INFO: Tracks synchronized: no" in captured.out
|
||||||
|
assert "INFO: Weakbits cleaned: no" in captured.out
|
||||||
|
assert "INFO: Creator: wozardry" in captured.out
|
||||||
|
assert "INFO: Boot sector format: 0 (unknown)" in captured.out
|
||||||
|
assert "INFO: Optimal bit timing: 32 (standard)" in captured.out
|
||||||
|
assert "INFO: Compatible hardware: unknown" in captured.out
|
||||||
|
assert "INFO: Required RAM: unknown" in captured.out
|
||||||
|
assert "INFO: Largest track: 0 blocks" in captured.out
|
||||||
|
|
||||||
|
def test_command_edit_info_version_1_to_2():
|
||||||
|
"""convert a WOZ1 file to WOZ2 and ensure new info fields are set to default values"""
|
||||||
|
with tempfile.NamedTemporaryFile() as tmp:
|
||||||
|
shutil.copy(kValid1, tmp.name)
|
||||||
|
|
||||||
|
wozardry.parse_args(["edit", "-i", "version:2", tmp.name])
|
||||||
|
woz = wozardry.WozReader(tmp.name)
|
||||||
|
assert woz.woz_version == 2
|
||||||
|
assert woz.info["version"] == 2
|
||||||
|
assert woz.info["boot_sector_format"] == 0
|
||||||
|
assert woz.info["optimal_bit_timing"] == 32
|
||||||
|
assert woz.info["compatible_hardware"] == []
|
||||||
|
assert woz.info["required_ram"] == 0
|
||||||
|
|
||||||
|
def test_command_edit_info_disk_type():
|
||||||
|
"""edit a WOZ1/WOZ2 file to change the disk type"""
|
||||||
|
# this is pathological, don't do this in real life
|
||||||
|
def f(inputfile):
|
||||||
|
with tempfile.NamedTemporaryFile() as tmp:
|
||||||
|
shutil.copy(inputfile, tmp.name)
|
||||||
|
|
||||||
|
# disk_type = 1 is ok
|
||||||
|
wozardry.parse_args(["edit", "-i", "disk_type:1", tmp.name])
|
||||||
|
woz = wozardry.WozReader(tmp.name)
|
||||||
|
assert woz.info["disk_type"] == 1
|
||||||
|
|
||||||
|
# disk_type = 2 is ok
|
||||||
|
wozardry.parse_args(["edit", "-i", "disk_type:2", tmp.name])
|
||||||
|
woz = wozardry.WozReader(tmp.name)
|
||||||
|
assert woz.info["disk_type"] == 2
|
||||||
|
|
||||||
|
# disk_type = 0 is not ok
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadDiskType):
|
||||||
|
wozardry.parse_args(["edit", "-i", "disk_type:0", tmp.name])
|
||||||
|
|
||||||
|
# disk_type = 3 is not ok
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadDiskType):
|
||||||
|
wozardry.parse_args(["edit", "-i", "disk_type:3", tmp.name])
|
||||||
|
f(kValid1)
|
||||||
|
f(kValid2)
|
||||||
|
|
||||||
|
def test_command_edit_info_changing_disk_type_resets_optimal_bit_timing():
|
||||||
|
"""edit a WOZ2 file to change the disk type and ensure optimal bit timing is reset to default value"""
|
||||||
|
# this is pathological, don't do this in real life
|
||||||
|
with tempfile.NamedTemporaryFile() as tmp:
|
||||||
|
shutil.copy(kValid2, tmp.name)
|
||||||
|
wozardry.parse_args(["edit", "-i", "disk_type:2", tmp.name])
|
||||||
|
woz = wozardry.WozReader(tmp.name)
|
||||||
|
assert woz.info["optimal_bit_timing"] == wozardry.kDefaultBitTiming[2]
|
||||||
|
|
||||||
|
def test_command_edit_info_boolean_flags():
|
||||||
|
"""edit a WOZ1/WOZ2 file to change Boolean flags in a variety of ways and ensure they change"""
|
||||||
|
def f(inputfile):
|
||||||
|
with tempfile.NamedTemporaryFile() as tmp:
|
||||||
|
shutil.copy(inputfile, tmp.name)
|
||||||
|
|
||||||
|
for flag in ("write_protected", "synchronized", "cleaned"):
|
||||||
|
for true_value, false_value in (("1", "0"),
|
||||||
|
("yes", "no"),
|
||||||
|
("YES", "No"),
|
||||||
|
("true", "false"),
|
||||||
|
("tRuE", "FaLsE")):
|
||||||
|
wozardry.parse_args(["edit", "-i", "%s:%s" % (flag, true_value), tmp.name])
|
||||||
|
woz = wozardry.WozReader(tmp.name)
|
||||||
|
assert woz.info[flag] == True
|
||||||
|
wozardry.parse_args(["edit", "-i", "%s:%s" % (flag, false_value), tmp.name])
|
||||||
|
woz = wozardry.WozReader(tmp.name)
|
||||||
|
assert woz.info[flag] == False
|
||||||
|
f(kValid1)
|
||||||
|
f(kValid2)
|
||||||
|
|
||||||
|
def test_command_edit_disk_sides():
|
||||||
|
"""edit a WOZ2 file to change disk sides"""
|
||||||
|
with tempfile.NamedTemporaryFile() as tmp:
|
||||||
|
shutil.copy(kValid2, tmp.name)
|
||||||
|
|
||||||
|
# this file is a 5.25-inch disk image
|
||||||
|
woz = wozardry.WozReader(tmp.name)
|
||||||
|
assert woz.info["disk_type"] == 1
|
||||||
|
assert woz.info["disk_sides"] == 1
|
||||||
|
|
||||||
|
# 5.25-inch disk images can only be "1-sided"
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadDiskSides):
|
||||||
|
wozardry.parse_args(["edit", "-i", "disk_sides:2", tmp.name])
|
||||||
|
|
||||||
|
# now change it to a 3.5-inch disk image
|
||||||
|
wozardry.parse_args(["edit", "-i", "disk_type:2", tmp.name])
|
||||||
|
woz = wozardry.WozReader(tmp.name)
|
||||||
|
assert woz.info["disk_type"] == 2
|
||||||
|
|
||||||
|
# 3.5-inch disk images can be 1- or 2-sided
|
||||||
|
wozardry.parse_args(["edit", "-i", "disk_sides:2", tmp.name])
|
||||||
|
woz = wozardry.WozReader(tmp.name)
|
||||||
|
assert woz.info["disk_sides"] == 2
|
||||||
|
wozardry.parse_args(["edit", "-i", "disk_sides:1", tmp.name])
|
||||||
|
woz = wozardry.WozReader(tmp.name)
|
||||||
|
assert woz.info["disk_sides"] == 1
|
||||||
|
|
||||||
|
# ...but not 3-sided, that's silly
|
||||||
|
with pytest.raises(wozardry.WozINFOFormatError_BadDiskSides):
|
||||||
|
wozardry.parse_args(["edit", "-i", "disk_sides:3", tmp.name])
|
||||||
|
|
||||||
|
def test_command_edit_language():
|
||||||
|
"""edit a WOZ1/WOZ2 file to change the metadata language field"""
|
||||||
|
def f(inputfile):
|
||||||
|
with tempfile.NamedTemporaryFile() as tmp:
|
||||||
|
shutil.copy(inputfile, tmp.name)
|
||||||
|
|
||||||
|
for lang in wozardry.kLanguages:
|
||||||
|
wozardry.parse_args(["edit", "-m", "language:%s" % lang, tmp.name])
|
||||||
|
woz = wozardry.WozReader(tmp.name)
|
||||||
|
assert woz.meta["language"] == lang
|
||||||
|
f(kValid1)
|
||||||
|
f(kValid2)
|
||||||
|
|
||||||
|
def test_command_edit_requires_ram():
|
||||||
|
"""edit a WOZ1/WOZ2 file to change the metadata requires_ram field"""
|
||||||
|
def f(inputfile):
|
||||||
|
with tempfile.NamedTemporaryFile() as tmp:
|
||||||
|
shutil.copy(inputfile, tmp.name)
|
||||||
|
|
||||||
|
for ram in wozardry.kRequiresRAM:
|
||||||
|
wozardry.parse_args(["edit", "-m", "requires_ram:%s" % ram, tmp.name])
|
||||||
|
woz = wozardry.WozReader(tmp.name)
|
||||||
|
assert woz.meta["requires_ram"] == ram
|
||||||
|
|
||||||
|
# invalid required RAM (must be one of enumerated values)
|
||||||
|
with pytest.raises(wozardry.WozMETAFormatError_BadRAM):
|
||||||
|
wozardry.parse_args(["edit", "-m", "requires_ram:65K", tmp.name])
|
||||||
|
|
||||||
|
f(kValid1)
|
||||||
|
f(kValid2)
|
||||||
|
|
||||||
|
def test_command_edit_requires_machine():
|
||||||
|
"""edit a WOZ1/WOZ2 file to change the metadata requires_machine field"""
|
||||||
|
def f(inputfile):
|
||||||
|
with tempfile.NamedTemporaryFile() as tmp:
|
||||||
|
shutil.copy(inputfile, tmp.name)
|
||||||
|
|
||||||
|
for model in wozardry.kRequiresMachine:
|
||||||
|
wozardry.parse_args(["edit", "-m", "requires_machine:%s" % model, tmp.name])
|
||||||
|
woz = wozardry.WozReader(tmp.name)
|
||||||
|
assert woz.meta["requires_machine"] == model
|
||||||
|
|
||||||
|
# invalid machine (Apple IV)
|
||||||
|
with pytest.raises(wozardry.WozMETAFormatError_BadMachine):
|
||||||
|
wozardry.parse_args(["edit", "-m", "requires_machine:4", tmp.name])
|
||||||
|
|
||||||
|
f(kValid1)
|
||||||
|
f(kValid2)
|
56
wozardry.py
56
wozardry.py
@ -10,9 +10,10 @@ import collections
|
|||||||
import json
|
import json
|
||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
__version__ = "2.0-alpha" # https://semver.org
|
__version__ = "2.0-beta" # https://semver.org
|
||||||
__date__ = "2019-02-17"
|
__date__ = "2019-02-23"
|
||||||
__progname__ = "wozardry"
|
__progname__ = "wozardry"
|
||||||
__displayname__ = __progname__ + " " + __version__ + " by 4am (" + __date__ + ")"
|
__displayname__ = __progname__ + " " + __version__ + " by 4am (" + __date__ + ")"
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ kBitstreamLengthInBytes = 6646
|
|||||||
kLanguages = ("English","Spanish","French","German","Chinese","Japanese","Italian","Dutch","Portuguese","Danish","Finnish","Norwegian","Swedish","Russian","Polish","Turkish","Arabic","Thai","Czech","Hungarian","Catalan","Croatian","Greek","Hebrew","Romanian","Slovak","Ukrainian","Indonesian","Malay","Vietnamese","Other")
|
kLanguages = ("English","Spanish","French","German","Chinese","Japanese","Italian","Dutch","Portuguese","Danish","Finnish","Norwegian","Swedish","Russian","Polish","Turkish","Arabic","Thai","Czech","Hungarian","Catalan","Croatian","Greek","Hebrew","Romanian","Slovak","Ukrainian","Indonesian","Malay","Vietnamese","Other")
|
||||||
kRequiresRAM = ("16K","24K","32K","48K","64K","128K","256K","512K","768K","1M","1.25M","1.5M+","Unknown")
|
kRequiresRAM = ("16K","24K","32K","48K","64K","128K","256K","512K","768K","1M","1.25M","1.5M+","Unknown")
|
||||||
kRequiresMachine = ("2","2+","2e","2c","2e+","2gs","2c+","3","3+")
|
kRequiresMachine = ("2","2+","2e","2c","2e+","2gs","2c+","3","3+")
|
||||||
|
kDefaultBitTiming = (0, 32, 16)
|
||||||
|
|
||||||
# strings and things, for print routines and error messages
|
# strings and things, for print routines and error messages
|
||||||
sEOF = "Unexpected EOF"
|
sEOF = "Unexpected EOF"
|
||||||
@ -50,6 +52,7 @@ class WozHeaderError_NoWOZMarker(WozHeaderError): pass
|
|||||||
class WozHeaderError_NoFF(WozHeaderError): pass
|
class WozHeaderError_NoFF(WozHeaderError): pass
|
||||||
class WozHeaderError_NoLF(WozHeaderError): pass
|
class WozHeaderError_NoLF(WozHeaderError): pass
|
||||||
class WozINFOFormatError(WozFormatError): pass
|
class WozINFOFormatError(WozFormatError): pass
|
||||||
|
class WozINFOFormatError_MissingINFOChunk(WozINFOFormatError): pass
|
||||||
class WozINFOFormatError_BadVersion(WozINFOFormatError): pass
|
class WozINFOFormatError_BadVersion(WozINFOFormatError): pass
|
||||||
class WozINFOFormatError_BadDiskType(WozINFOFormatError): pass
|
class WozINFOFormatError_BadDiskType(WozINFOFormatError): pass
|
||||||
class WozINFOFormatError_BadWriteProtected(WozINFOFormatError): pass
|
class WozINFOFormatError_BadWriteProtected(WozINFOFormatError): pass
|
||||||
@ -59,7 +62,9 @@ class WozINFOFormatError_BadCreator(WozINFOFormatError): pass
|
|||||||
class WozINFOFormatError_BadDiskSides(WozINFOFormatError): pass
|
class WozINFOFormatError_BadDiskSides(WozINFOFormatError): pass
|
||||||
class WozINFOFormatError_BadBootSectorFormat(WozINFOFormatError): pass
|
class WozINFOFormatError_BadBootSectorFormat(WozINFOFormatError): pass
|
||||||
class WozINFOFormatError_BadOptimalBitTiming(WozINFOFormatError): pass
|
class WozINFOFormatError_BadOptimalBitTiming(WozINFOFormatError): pass
|
||||||
|
class WozINFOFormatError_BadCompatibleHardware(WozINFOFormatError): pass
|
||||||
class WozTMAPFormatError(WozFormatError): pass
|
class WozTMAPFormatError(WozFormatError): pass
|
||||||
|
class WozTMAPFormatError_MissingTMAPChunk(WozTMAPFormatError): pass
|
||||||
class WozTMAPFormatError_BadTRKS(WozTMAPFormatError): pass
|
class WozTMAPFormatError_BadTRKS(WozTMAPFormatError): pass
|
||||||
class WozTRKSFormatError(WozFormatError): pass
|
class WozTRKSFormatError(WozFormatError): pass
|
||||||
class WozMETAFormatError(WozFormatError): pass
|
class WozMETAFormatError(WozFormatError): pass
|
||||||
@ -317,6 +322,13 @@ class WozValidator:
|
|||||||
raise_if(optimal_bit_timing not in range(8, 25), WozINFOFormatError_BadOptimalBitTiming, "Bad optimal bit timing (expected 8-24 for a 3.5-inch disk, found %s)" % optimal_bit_timing)
|
raise_if(optimal_bit_timing not in range(8, 25), WozINFOFormatError_BadOptimalBitTiming, "Bad optimal bit timing (expected 8-24 for a 3.5-inch disk, found %s)" % optimal_bit_timing)
|
||||||
return optimal_bit_timing
|
return optimal_bit_timing
|
||||||
|
|
||||||
|
def validate_info_compatible_hardware(self, compatible_hardware):
|
||||||
|
"""|compatible_hardware| is bytes, returns same value as int"""
|
||||||
|
# assumes WOZ version 2 or later
|
||||||
|
compatible_hardware = from_uint16(compatible_hardware)
|
||||||
|
raise_if(compatible_hardware >= 0x01FF, WozINFOFormatError_BadCompatibleHardware, "Bad compatible hardware (7 high bits must be 0 but some were 1)")
|
||||||
|
return compatible_hardware
|
||||||
|
|
||||||
def validate_info_required_ram(self, required_ram):
|
def validate_info_required_ram(self, required_ram):
|
||||||
"""|required_ram| can be str, bytes, or int. returns same value as int"""
|
"""|required_ram| can be str, bytes, or int. returns same value as int"""
|
||||||
# assumes WOZ version 2 or later
|
# assumes WOZ version 2 or later
|
||||||
@ -351,6 +363,8 @@ class WozReader(WozDiskImage, WozValidator):
|
|||||||
def __init__(self, filename=None, stream=None):
|
def __init__(self, filename=None, stream=None):
|
||||||
WozDiskImage.__init__(self)
|
WozDiskImage.__init__(self)
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
|
seen_info = False
|
||||||
|
seen_tmap = False
|
||||||
with stream or open(filename, "rb") as f:
|
with stream or open(filename, "rb") as f:
|
||||||
header_raw = f.read(8)
|
header_raw = f.read(8)
|
||||||
raise_if(len(header_raw) != 8, WozEOFError, sEOF)
|
raise_if(len(header_raw) != 8, WozEOFError, sEOF)
|
||||||
@ -374,15 +388,23 @@ class WozReader(WozDiskImage, WozValidator):
|
|||||||
if chunk_id == kINFO:
|
if chunk_id == kINFO:
|
||||||
raise_if(chunk_size != 60, WozINFOFormatError, sBadChunkSize)
|
raise_if(chunk_size != 60, WozINFOFormatError, sBadChunkSize)
|
||||||
self.__process_info(data)
|
self.__process_info(data)
|
||||||
elif chunk_id == kTMAP:
|
seen_info = True
|
||||||
|
continue
|
||||||
|
raise_if(not seen_info, WozINFOFormatError_MissingINFOChunk, "Expected INFO chunk at offset 20")
|
||||||
|
if chunk_id == kTMAP:
|
||||||
raise_if(chunk_size != 160, WozTMAPFormatError, sBadChunkSize)
|
raise_if(chunk_size != 160, WozTMAPFormatError, sBadChunkSize)
|
||||||
self.__process_tmap(data)
|
self.__process_tmap(data)
|
||||||
elif chunk_id == kTRKS:
|
seen_tmap = True
|
||||||
|
continue
|
||||||
|
raise_if(not seen_tmap, WozTMAPFormatError_MissingTMAPChunk, "Expected TMAP chunk at offset 88")
|
||||||
|
if chunk_id == kTRKS:
|
||||||
self.__process_trks(data)
|
self.__process_trks(data)
|
||||||
elif chunk_id == kWRIT:
|
elif chunk_id == kWRIT:
|
||||||
self.__process_writ(data)
|
self.__process_writ(data)
|
||||||
elif chunk_id == kMETA:
|
elif chunk_id == kMETA:
|
||||||
self.__process_meta(data)
|
self.__process_meta(data)
|
||||||
|
raise_if(not seen_info, WozINFOFormatError_MissingINFOChunk, "Expected INFO chunk at offset 20")
|
||||||
|
raise_if(not seen_tmap, WozTMAPFormatError_MissingTMAPChunk, "Expected TMAP chunk at offset 88")
|
||||||
if crc:
|
if crc:
|
||||||
raise_if(crc != binascii.crc32(b"".join(all_data)) & 0xffffffff, WozCRCError, "Bad CRC")
|
raise_if(crc != binascii.crc32(b"".join(all_data)) & 0xffffffff, WozCRCError, "Bad CRC")
|
||||||
|
|
||||||
@ -403,7 +425,7 @@ class WozReader(WozDiskImage, WozValidator):
|
|||||||
self.info["disk_sides"] = self.validate_info_disk_sides(data[37]) # int
|
self.info["disk_sides"] = self.validate_info_disk_sides(data[37]) # int
|
||||||
self.info["boot_sector_format"] = self.validate_info_boot_sector_format(data[38]) # int
|
self.info["boot_sector_format"] = self.validate_info_boot_sector_format(data[38]) # int
|
||||||
self.info["optimal_bit_timing"] = self.validate_info_optimal_bit_timing(data[39]) # int
|
self.info["optimal_bit_timing"] = self.validate_info_optimal_bit_timing(data[39]) # int
|
||||||
compatible_hardware_bitfield = from_uint16(data[40:42])
|
compatible_hardware_bitfield = self.validate_info_compatible_hardware(data[40:42]) # int
|
||||||
compatible_hardware_list = []
|
compatible_hardware_list = []
|
||||||
for offset in range(9):
|
for offset in range(9):
|
||||||
if compatible_hardware_bitfield & (1 << offset):
|
if compatible_hardware_bitfield & (1 << offset):
|
||||||
@ -729,10 +751,9 @@ class CommandDump(BaseCommand):
|
|||||||
if disk_type == 1: # 5.25-inch disk
|
if disk_type == 1: # 5.25-inch disk
|
||||||
boot_sector_format = info["boot_sector_format"]
|
boot_sector_format = info["boot_sector_format"]
|
||||||
print("INFO: Boot sector format:".ljust(self.kWidth), "%s (%s)" % (boot_sector_format, tBootSectorFormat[boot_sector_format]))
|
print("INFO: Boot sector format:".ljust(self.kWidth), "%s (%s)" % (boot_sector_format, tBootSectorFormat[boot_sector_format]))
|
||||||
default_bit_timing = 32
|
|
||||||
else: # 3.5-inch disk
|
else: # 3.5-inch disk
|
||||||
print("INFO: Disk sides:".ljust(self.kWidth), disk_sides)
|
print("INFO: Disk sides:".ljust(self.kWidth), disk_sides)
|
||||||
default_bit_timing = 16
|
default_bit_timing = kDefaultBitTiming[disk_type]
|
||||||
optimal_bit_timing = info["optimal_bit_timing"]
|
optimal_bit_timing = info["optimal_bit_timing"]
|
||||||
print("INFO: Optimal bit timing:".ljust(self.kWidth), optimal_bit_timing,
|
print("INFO: Optimal bit timing:".ljust(self.kWidth), optimal_bit_timing,
|
||||||
optimal_bit_timing == default_bit_timing and "(standard)" or
|
optimal_bit_timing == default_bit_timing and "(standard)" or
|
||||||
@ -811,6 +832,9 @@ class WriterBaseCommand(BaseCommand):
|
|||||||
try:
|
try:
|
||||||
global raise_if
|
global raise_if
|
||||||
raise_if = old_raise_if
|
raise_if = old_raise_if
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
WozReader(tmpfile)
|
WozReader(tmpfile)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sys.stderr.write("WozInternalError: refusing to write an invalid .woz file (this is the developer's fault)\n")
|
sys.stderr.write("WozInternalError: refusing to write an invalid .woz file (this is the developer's fault)\n")
|
||||||
@ -861,7 +885,11 @@ requires_machine, notes, side, side_name, contributor, image_date. Other keys ar
|
|||||||
for i in self.args.info or ():
|
for i in self.args.info or ():
|
||||||
k, v = i.split(":", 1)
|
k, v = i.split(":", 1)
|
||||||
if k == "disk_type":
|
if k == "disk_type":
|
||||||
self.output.info["disk_type"] = self.output.validate_info_disk_type(v)
|
old_disk_type = self.output.info["disk_type"]
|
||||||
|
new_disk_type = self.output.validate_info_disk_type(v)
|
||||||
|
if old_disk_type != new_disk_type:
|
||||||
|
self.output.info["disk_type"] = new_disk_type
|
||||||
|
self.output.info["optimal_bit_timing"] = kDefaultBitTiming[new_disk_type]
|
||||||
|
|
||||||
# then update all other info fields
|
# then update all other info fields
|
||||||
for i in self.args.info or ():
|
for i in self.args.info or ():
|
||||||
@ -941,10 +969,7 @@ class CommandImport(WriterBaseCommand):
|
|||||||
def update(self):
|
def update(self):
|
||||||
self.output.from_json(sys.stdin.read())
|
self.output.from_json(sys.stdin.read())
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def parse_args(args):
|
||||||
import sys
|
|
||||||
old_raise_if = raise_if
|
|
||||||
# raise_if = lambda cond, e, s="": cond and sys.exit("%s: %s" % (e.__name__, s))
|
|
||||||
cmds = [CommandDump(), CommandVerify(), CommandEdit(), CommandRemove(), CommandExport(), CommandImport()]
|
cmds = [CommandDump(), CommandVerify(), CommandEdit(), CommandRemove(), CommandExport(), CommandImport()]
|
||||||
parser = argparse.ArgumentParser(prog=__progname__,
|
parser = argparse.ArgumentParser(prog=__progname__,
|
||||||
description="""A multi-purpose tool for manipulating .woz disk images.
|
description="""A multi-purpose tool for manipulating .woz disk images.
|
||||||
@ -955,5 +980,10 @@ See '""" + __progname__ + """ <command> -h' for help on individual commands.""",
|
|||||||
sp = parser.add_subparsers(dest="command", help="command")
|
sp = parser.add_subparsers(dest="command", help="command")
|
||||||
for command in cmds:
|
for command in cmds:
|
||||||
command.setup(sp)
|
command.setup(sp)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args(args)
|
||||||
args.action(args)
|
args.action(args)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
old_raise_if = raise_if
|
||||||
|
raise_if = lambda cond, e, s="": cond and sys.exit("%s: %s" % (e.__name__, s))
|
||||||
|
parse_args(sys.argv[1:])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user