From a2d8c233e76662326bcc2e253f45253686fb7708 Mon Sep 17 00:00:00 2001 From: Richard Dymond Date: Thu, 20 Jun 2024 18:00:31 -0300 Subject: [PATCH] Add support to tapinfo.py for PZX files --- skoolkit/tape.py | 110 +++++++++++++++- skoolkit/tapinfo.py | 20 +-- sphinx/source/changelog.rst | 1 + sphinx/source/commands.rst | 9 +- sphinx/source/conf.py | 2 +- sphinx/source/man/tapinfo.py.rst | 2 +- tests/skoolkittest.py | 109 ++++++++++++++++ tests/test_tapinfo.py | 209 ++++++++++++++++++++++++++++++- 8 files changed, 445 insertions(+), 17 deletions(-) diff --git a/skoolkit/tape.py b/skoolkit/tape.py index 69ce4c0a..edd66469 100644 --- a/skoolkit/tape.py +++ b/skoolkit/tape.py @@ -227,7 +227,7 @@ def __init__(self, blocks, version=None, warnings=()): self.warnings = warnings class TapeBlock: - def __init__(self, number, tape_data, timings=None, block_data=None, block_id=None, name=None, info=()): + def __init__(self, number, tape_data, timings=None, block_data=None, block_id=None, name=None, info=(), standard=True): self.number = number self.data = tape_data self.timings = timings @@ -235,6 +235,7 @@ def __init__(self, number, tape_data, timings=None, block_data=None, block_id=No self.block_id = block_id self.name = name self.info = info + self.standard = standard class TapeBlockTimings: def __init__(self, pilot_len=0, pilot=0, sync=(), zero=0, one=0, pause=0, used_bits=8, pulses=(), error=None): @@ -275,6 +276,94 @@ def _format_text(prefix, data, start, length, dump=False): lines.append(f'{indent} {line}') return lines +def _get_pzx_block(data, i, block_num, prev_rom_pilot): + # http://zxds.raxoft.cz/docs/pzx.txt + block_id = ''.join(chr(b) for b in data[i:i + 4]) + block_len = get_dword(data, i + 4) + tape_data = None + standard = False + rom_pilot = False + info = [] + if block_id == 'PZXT': + name = 'PZX header block' + info.append(f'Version: {data[i + 8]}.{data[i + 9]}') + pairs = ['Title', ''] + j = i + 10 + while j < i + 8 + block_len: + b = data[j] + if b: + pairs[-1] += chr(b) + else: + if len(pairs) == 2: + info.append(f'{pairs[0]}: {pairs[1]}') + pairs.clear() + pairs.append('') + j += 1 + if len(pairs) == 2 and pairs[1]: + info.append(f'{pairs[0]}: {pairs[1]}') + elif block_id == 'PULS': + name = 'Pulse sequence' + pulses = [] + j = i + 8 + while j < i + 8 + block_len: + count = 1 + duration = get_word(data, j) + j += 2 + if duration > 0x8000: + count = duration % 0x8000 + duration = get_word(data, j) + j += 2 + if duration >= 0x8000: + duration = (duration % 0x8000) * 65536 + get_word(data, j) + j += 2 + info.append(f'{count} x {duration} T-states') + pulses.append((count, duration)) + rom_pilot = len(pulses) == 3 and pulses[0] in ((3223, 2168), (8063, 2168)) and pulses[1:] == [(1, 667), (1, 735)] + elif block_id == 'DATA': + name = 'Data block' + count = get_dword(data, i + 8) + bits = count % 0x80000000 + num_bytes = bits // 8 + used_bits = bits % 8 + tail = get_word(data, i + 12) + p0, p1 = data[i + 14:i + 16] + j = i + 16 + s0 = [get_word(data, k) for k in range(j, j + 2 * p0, 2)] + j += 2 * p0 + s1 = [get_word(data, k) for k in range(j, j + 2 * p1, 2)] + j += 2 * p1 + tape_data = data[j:j + num_bytes + int(used_bits > 0)] + standard = prev_rom_pilot and p0 == 2 and p1 == 2 and s0 == [855, 855] and s1 == [1710, 1710] + if used_bits: + info.append(f'Bits: {bits} ({num_bytes} bytes + {used_bits} bits)') + else: + info.append(f'Bits: {bits} ({num_bytes} bytes)') + info.extend(( + f'Initial pulse level: {count >> 31}', + '0-bit pulse sequence: {} (T-states)'.format(', '.join(str(p) for p in s0)), + '1-bit pulse sequence: {} (T-states)'.format(', '.join(str(p) for p in s1)), + f'Tail pulse: {tail} T-states' + )) + elif block_id == 'PAUS': + name = 'Pause' + duration = get_dword(data, i + 8) + info.extend(( + f'Duration: {duration % 0x80000000} T-states', + f'Initial pulse level: {duration >> 31}', + )) + elif block_id == 'BRWS': + name = 'Browse point' + info.append(''.join(chr(b) for b in data[i + 8:i + 8 + block_len])) + elif block_id == 'STOP': + name = 'Stop tape command' + flags = get_word(data, i + 8) + mode = ('Always', '48K only')[flags & 1] + info.append(f'Mode: {mode}') + else: + name = block_id + block = TapeBlock(block_num, tape_data, block_id=block_id, name=name, info=info, standard=standard) + return i + 8 + block_len, block, rom_pilot + def _get_tzx_block(data, i, block_num, get_info, get_timings): # https://worldofspectrum.net/features/TZXformat.html block_id = data[i] @@ -282,6 +371,7 @@ def _get_tzx_block(data, i, block_num, get_info, get_timings): tape_data = None info = [] timings = None + standard = False i += 1 if block_id == 0x10: # Standard speed data block @@ -289,6 +379,7 @@ def _get_tzx_block(data, i, block_num, get_info, get_timings): pause = get_word(data, i) length = get_word(data, i + 2) tape_data = data[i + 4:i + 4 + length] + standard = True if get_info: info.append(f'Pause: {pause}ms') if get_timings and tape_data: @@ -571,7 +662,7 @@ def _get_tzx_block(data, i, block_num, get_info, get_timings): i += 9 else: raise SkoolKitError(f'Unknown TZX block ID: 0x{block_id:02X}') - return i, TapeBlock(block_num, tape_data, timings, block_data, block_id, header, info) + return i, TapeBlock(block_num, tape_data, timings, block_data, block_id, header, info, standard) def hex_dump(data, row_size=16): lines = [] @@ -582,6 +673,21 @@ def hex_dump(data, row_size=16): lines.append(f'{index:04X} {values_hex} {values_text}') return lines +def parse_pzx(pzx): + if isinstance(pzx, str): + pzx = read_bin_file(pzx) + if pzx[:4] != b'PZXT': + raise SkoolKitError('Not a PZX file') + blocks = [] + block_num = 1 + rom_pilot = False + i = 0 + while i < len(pzx): + i, block, rom_pilot = _get_pzx_block(pzx, i, block_num, rom_pilot) + blocks.append(block) + block_num += 1 + return Tape(blocks) + def parse_tap(tap, start=1, stop=0): if isinstance(tap, str): tap = read_bin_file(tap) diff --git a/skoolkit/tapinfo.py b/skoolkit/tapinfo.py index 1497d053..56730e08 100644 --- a/skoolkit/tapinfo.py +++ b/skoolkit/tapinfo.py @@ -19,7 +19,7 @@ from skoolkit import SkoolKitError, get_word, get_int_param, warn, VERSION from skoolkit.basic import BasicLister, TextReader -from skoolkit.tape import hex_dump, parse_tap, parse_tzx +from skoolkit.tape import hex_dump, parse_pzx, parse_tap, parse_tzx def _bytes_to_str(data): return ', '.join(str(b) for b in data) @@ -27,14 +27,16 @@ def _bytes_to_str(data): def _print_info(text): print(' ' + text) -def _print_block(index, data, show_data, text_reader, info=(), block_id=None, header=None): +def _print_block(index, data, show_data, text_reader, info, block_id, header, standard): if block_id is None: - print("{}:".format(index)) + print(f'{index}:') + elif isinstance(block_id, int): + print(f'{index}: {header} (0x{block_id:02X})') else: - print("{}: {} (0x{:02X})".format(index, header, block_id)) + print(f'{index}: {header}') for line in info: _print_info(line) - if data and block_id in (None, 16): + if data and standard: data_type = "Unknown" name_str = None line = 0xC000 @@ -101,14 +103,14 @@ def _analyse_tape(tape, basic_block, text_reader, show_data): if basic_block: _list_basic(block.number, block.data, *basic_block) else: - _print_block(block.number, block.data, show_data, text_reader, block.info, block.block_id, block.name) + _print_block(block.number, block.data, show_data, text_reader, block.info, block.block_id, block.name, block.standard) for msg in tape.warnings: warn(msg) def main(args): parser = argparse.ArgumentParser( usage="tapinfo.py FILE", - description="Show the blocks in a TAP or TZX file.", + description="Show the blocks in a PZX, TAP or TZX file.", add_help=False ) parser.add_argument('infile', help=argparse.SUPPRESS, nargs='?') @@ -125,7 +127,9 @@ def main(args): basic_block = _get_basic_block(namespace.basic) text_reader = TextReader() tape_type = namespace.infile.lower()[-4:] - if tape_type == '.tap': + if tape_type == '.pzx': + tape = parse_pzx(namespace.infile) + elif tape_type == '.tap': tape = parse_tap(namespace.infile) elif tape_type == '.tzx': tape = parse_tzx(namespace.infile) diff --git a/sphinx/source/changelog.rst b/sphinx/source/changelog.rst index a713c6a6..0067ec58 100644 --- a/sphinx/source/changelog.rst +++ b/sphinx/source/changelog.rst @@ -3,6 +3,7 @@ Changelog 9.3b1 ----- +* Added support to :ref:`tapinfo.py` for PZX files * Added support to :ref:`tap2sna.py ` for the ``m`` (memory) replacement field in the ``TraceLine`` configuration parameter * Added support to :ref:`trace.py ` for the ``m`` (memory) diff --git a/sphinx/source/commands.rst b/sphinx/source/commands.rst index 85430571..22795212 100644 --- a/sphinx/source/commands.rst +++ b/sphinx/source/commands.rst @@ -1927,7 +1927,7 @@ Configuration parameters may also be set on the command line by using the tapinfo.py ---------- -`tapinfo.py` shows information on the blocks in a TAP or TZX file. For +`tapinfo.py` shows information on the blocks in a PZX, TAP or TZX file. For example:: $ tapinfo.py game.tzx @@ -1936,7 +1936,7 @@ To list the options supported by `tapinfo.py`, run it with no arguments:: usage: tapinfo.py FILE - Show the blocks in a TAP or TZX file. + Show the blocks in a PZX, TAP or TZX file. Options: -b N[,A], --basic N[,A] @@ -1948,8 +1948,9 @@ To list the options supported by `tapinfo.py`, run it with no arguments:: +---------+-------------------------------------------------------------------+ | Version | Changes | +=========+===================================================================+ -| 9.3 | Shows info for TZX block type 0x18 (CSW recording); recognises | -| | deprecated TZX block types 0x16, 0x17, 0x34 and 0x40 | +| 9.3 | Added support for PZX files; shows info for TZX block type 0x18 | +| | (CSW recording); recognises deprecated TZX block types 0x16, | +| | 0x17, 0x34 and 0x40 | +---------+-------------------------------------------------------------------+ | 9.2 | Shows info for TZX block type 0x15 (direct recording) | +---------+-------------------------------------------------------------------+ diff --git a/sphinx/source/conf.py b/sphinx/source/conf.py index e005fe4d..2d7a7c4f 100644 --- a/sphinx/source/conf.py +++ b/sphinx/source/conf.py @@ -250,7 +250,7 @@ ('man/tap2sna.py', 'tap2sna.py', 'convert a TAP or TZX file into a snapshot file', _authors, 1), ('man/tapinfo.py', 'tapinfo.py', - 'show the blocks in a TAP or TZX file', _authors, 1), + 'show the blocks in a PZX, TAP or TZX file', _authors, 1), ('man/trace.py', 'trace.py', 'simulate code execution in a 48K or 128K memory snapshot', _authors, 1) ] diff --git a/sphinx/source/man/tapinfo.py.rst b/sphinx/source/man/tapinfo.py.rst index f5b6df61..a61ed1d1 100644 --- a/sphinx/source/man/tapinfo.py.rst +++ b/sphinx/source/man/tapinfo.py.rst @@ -10,7 +10,7 @@ SYNOPSIS DESCRIPTION =========== -``tapinfo.py`` shows information on the blocks in a TAP or TZX file. +``tapinfo.py`` shows information on the blocks in a PZX, TAP or TZX file. OPTIONS ======= diff --git a/tests/skoolkittest.py b/tests/skoolkittest.py index 64de078a..37144bfa 100644 --- a/tests/skoolkittest.py +++ b/tests/skoolkittest.py @@ -27,6 +27,15 @@ def as_dword(num): return (num % 256, (num >> 8) % 256, (num >> 16) % 256, (num >> 24) % 256) +def as_word(num): + return (num % 256, (num >> 8) % 256) + +def as_words(values): + data = [] + for v in values: + data.extend(as_word(v)) + return data + def get_parity(data): parity = 0 for b in data: @@ -341,6 +350,106 @@ def data(self): data.extend(input_recording) return data +class PZX: + def __init__(self, major=1, minor=0, info=(), null=True): + info_bytes = [] + for key, value in info: + if key: + info_bytes.extend(ord(c) for c in key) + info_bytes.append(0) + info_bytes.extend(ord(c) for c in value) + info_bytes.append(0) + if info_bytes and info_bytes[-1] == 0 and not null: + info_bytes.pop() + length = 2 + len(info_bytes) + self.data = [ + 80, 90, 88, 84, # PZXT + *as_dword(length), # Block length + major, minor, # Version + *info_bytes # Tape info + ] + + def _encode_pulses(self, count, duration): + if count == 1 and duration < 0x8000: + return as_word(duration) + if duration < 0x8000: + return (*as_word(0x8000 + count), *as_word(duration)) + return (*as_word(0x8000 + count), *as_word(0x8000 + (duration >> 16)), *as_word(duration % 65536)) + + def add_puls(self, standard=1, pulses=(), pulse_data=None): + if pulse_data is None: + pulse_data = [] + if pulses: + count = 1 + prev_p = pulses[0] + for p in pulses[1:] + [None]: + if p == prev_p: + count += 1 + else: + while count > 0: + pulse_data.extend(self._encode_pulses(min(count, 0x7FFF), prev_p)) + count -= 0x7FFF + count = 1 + prev_p = p + else: + pulse_data.extend(self._encode_pulses(3223 + 4840 * (standard > 1), 2168)) + pulse_data.extend(self._encode_pulses(1, 667)) + pulse_data.extend(self._encode_pulses(1, 735)) + length = len(pulse_data) + self.data.extend(( + 80, 85, 76, 83, # PULS + *as_dword(length), # Block length + *pulse_data # Pulses + )) + + def add_data(self, data, s0=(855, 855), s1=(1710, 1710), tail=945, used_bits=0, polarity=0): + length = 8 + 2 * (len(s0) + len(s1)) + len(data) + bits = 8 * len(data) + if used_bits: + bits += used_bits - 8 + bits += polarity * 0x80000000 + self.data.extend(( + 68, 65, 84, 65, # DATA + *as_dword(length), # Block length + *as_dword(bits), # Polarity and bit count + *as_word(tail), # Tail pulse + len(s0), # p0 + len(s1), # p1 + *as_words(s0), # s0 + *as_words(s1), # s1 + *data # Data + )) + + def add_paus(self, duration=3500000, polarity=0): + value = polarity * 0x80000000 + (duration % 0x80000000) + self.data.extend(( + 80, 65, 85, 83, # PAUS + *as_dword(4), # Block length + *as_dword(value) # Duration + )) + + def add_brws(self, text): + text_bytes = [ord(c) for c in text] + length = len(text) + self.data.extend(( + 66, 82, 87, 83, # BRWS + *as_dword(length), # Block length + *text_bytes # Text + )) + + def add_stop(self, always=True): + flags = int(not always) + self.data.extend(( + 83, 84, 79, 80, # STOP + *as_dword(2), # Block length + *as_word(flags) # Flags + )) + + def add_block(self, name, data): + block = [32, 32, 32, 32, *as_dword(len(data)), *data] + block[:len(name)] = [ord(c) for c in name[:4]] + self.data.extend(block) + class SkoolKitTestCase(TestCase): stdout_binary = False diff --git a/tests/test_tapinfo.py b/tests/test_tapinfo.py index 9ff7dac3..ffc178af 100644 --- a/tests/test_tapinfo.py +++ b/tests/test_tapinfo.py @@ -1,7 +1,7 @@ from textwrap import dedent from unittest.mock import patch -from skoolkittest import (SkoolKitTestCase, create_data_block, +from skoolkittest import (SkoolKitTestCase, PZX, create_data_block, create_tap_header_block, create_tap_data_block, create_tzx_header_block, create_tzx_data_block) from skoolkit import SkoolKitError, tapinfo, get_word, VERSION @@ -61,6 +61,177 @@ def test_unrecognised_tape_type(self): self.run_tapinfo('unknown.tape') self.assertEqual(cm.exception.args[0], 'Unrecognised tape type') + def test_pzx_file(self): + info = ( + ('', 'Flip Flap'), + ('Publisher', 'Software Inc.'), + ('Author', 'J. Bloggs'), + ('Author', 'J. Doe'), + ('Year', '1982'), + ('Language', 'Strong'), + ('Type', 'Game'), + ('Price', '5.99'), + ('Protection', 'None'), + ('Origin', 'Uncertain'), + ('Comment', 'No comment') + ) + pzx = PZX(0, 3, info, False) + pzx.add_puls(pulses=[40, 40000, 400000]) + pzx.add_data([1, 2, 3, 4], s0=(400, 405), s1=(800, 807), tail=1024, used_bits=5, polarity=1) + pzx.add_paus(1234567, polarity=1) + pzx.add_brws('End of tape') + pzx.add_stop(False) + pzxfile = self.write_bin_file(pzx.data, suffix='.pzx') + output, error = self.run_tapinfo(pzxfile) + self.assertEqual(error, '') + exp_output = """ + 1: PZX header block + Version: 0.3 + Title: Flip Flap + Publisher: Software Inc. + Author: J. Bloggs + Author: J. Doe + Year: 1982 + Language: Strong + Type: Game + Price: 5.99 + Protection: None + Origin: Uncertain + Comment: No comment + 2: Pulse sequence + 1 x 40 T-states + 1 x 40000 T-states + 1 x 400000 T-states + 3: Data block + Bits: 29 (3 bytes + 5 bits) + Initial pulse level: 1 + 0-bit pulse sequence: 400, 405 (T-states) + 1-bit pulse sequence: 800, 807 (T-states) + Tail pulse: 1024 T-states + Length: 4 + Data: 1, 2, 3, 4 + 4: Pause + Duration: 1234567 T-states + Initial pulse level: 1 + 5: Browse point + End of tape + 6: Stop tape command + Mode: 48K only + """ + self.assertEqual(dedent(exp_output).lstrip(), output) + + def test_pzx_with_standard_speed_blocks(self): + data0 = create_tap_header_block('test_pzx00', 100, 200, 0) + data1 = create_tap_header_block('test_pzx01', length=4, data_type=1) + data2 = create_tap_header_block('test_pzx02', length=5, data_type=2) + data3 = create_tap_header_block('test_pzx03', 32768, 3, 3) + data4 = create_tap_data_block([1, 2, 3]) + pzx = PZX() + for standard, data in ((2, data0), (2, data1), (2, data2), (2, data3), (1, data4)): + pzx.add_puls(standard) + pzx.add_data(data[2:]) + pzxfile = self.write_bin_file(pzx.data, suffix='.pzx') + output, error = self.run_tapinfo(pzxfile) + self.assertEqual(error, '') + exp_output = """ + 1: PZX header block + Version: 1.0 + 2: Pulse sequence + 8063 x 2168 T-states + 1 x 667 T-states + 1 x 735 T-states + 3: Data block + Bits: 152 (19 bytes) + Initial pulse level: 0 + 0-bit pulse sequence: 855, 855 (T-states) + 1-bit pulse sequence: 1710, 1710 (T-states) + Tail pulse: 945 T-states + Type: Header block + Program: test_pzx00 + LINE: 100 + Length: 19 + Data: 0, 0, 116, 101, 115, 116, 95 ... 200, 0, 100, 0, 200, 0, 95 + 4: Pulse sequence + 8063 x 2168 T-states + 1 x 667 T-states + 1 x 735 T-states + 5: Data block + Bits: 152 (19 bytes) + Initial pulse level: 0 + 0-bit pulse sequence: 855, 855 (T-states) + 1-bit pulse sequence: 1710, 1710 (T-states) + Tail pulse: 945 T-states + Type: Header block + Number array: test_pzx01 + Length: 19 + Data: 0, 1, 116, 101, 115, 116, 95 ... 4, 0, 0, 0, 0, 0, 63 + 6: Pulse sequence + 8063 x 2168 T-states + 1 x 667 T-states + 1 x 735 T-states + 7: Data block + Bits: 152 (19 bytes) + Initial pulse level: 0 + 0-bit pulse sequence: 855, 855 (T-states) + 1-bit pulse sequence: 1710, 1710 (T-states) + Tail pulse: 945 T-states + Type: Header block + Character array: test_pzx02 + Length: 19 + Data: 0, 2, 116, 101, 115, 116, 95 ... 5, 0, 0, 0, 0, 0, 62 + 8: Pulse sequence + 8063 x 2168 T-states + 1 x 667 T-states + 1 x 735 T-states + 9: Data block + Bits: 152 (19 bytes) + Initial pulse level: 0 + 0-bit pulse sequence: 855, 855 (T-states) + 1-bit pulse sequence: 1710, 1710 (T-states) + Tail pulse: 945 T-states + Type: Header block + Bytes: test_pzx03 + CODE: 32768,3 + Length: 19 + Data: 0, 3, 116, 101, 115, 116, 95 ... 3, 0, 0, 128, 0, 0, 184 + 10: Pulse sequence + 3223 x 2168 T-states + 1 x 667 T-states + 1 x 735 T-states + 11: Data block + Bits: 40 (5 bytes) + Initial pulse level: 0 + 0-bit pulse sequence: 855, 855 (T-states) + 1-bit pulse sequence: 1710, 1710 (T-states) + Tail pulse: 945 T-states + Type: Data block + Length: 5 + Data: 255, 1, 2, 3, 255 + """ + self.assertEqual(dedent(exp_output).lstrip(), output) + + def test_pzx_with_unknown_block(self): + pzx = PZX() + pzx.add_block('????', [255]) + pzx.add_stop() + pzxfile = self.write_bin_file(pzx.data, suffix='.pzx') + output, error = self.run_tapinfo(pzxfile) + self.assertEqual(error, '') + exp_output = """ + 1: PZX header block + Version: 1.0 + 2: ???? + 3: Stop tape command + Mode: Always + """ + self.assertEqual(dedent(exp_output).lstrip(), output) + + def test_invalid_pzx_file(self): + invalid_pzx = self.write_text_file('This is not a PZX file', suffix='.pzx') + with self.assertRaises(SkoolKitError) as cm: + self.run_tapinfo(invalid_pzx) + self.assertEqual(cm.exception.args[0], 'Not a PZX file') + def test_tap_file(self): tap_data = create_tap_header_block('program_01', 100, 200, 0) data = [1, 2, 4] @@ -693,6 +864,19 @@ def test_names_containing_basic_tokens(self): """ self.assertEqual(dedent(exp_output).lstrip(), output) + @patch.object(tapinfo, 'BasicLister', MockBasicLister) + def test_option_b_pzx(self): + prog = [10] * 10 + data = create_tap_data_block(prog)[2:] + pzx = PZX() + pzx.add_data(data) + pzxfile = self.write_bin_file(pzx.data, suffix='.pzx') + exp_snapshot = [0] * 23755 + prog + output, error = self.run_tapinfo(f'-b 2 {pzxfile}') + self.assertEqual(error, '') + self.assertEqual(output, 'BASIC DONE!\n') + self.assertEqual(exp_snapshot, mock_basic_lister.snapshot) + @patch.object(tapinfo, 'BasicLister', MockBasicLister) def test_option_b_tap(self): prog = [10] * 10 @@ -739,6 +923,29 @@ def test_option_b_with_invalid_block_spec(self): self._test_bad_spec('-b', '1,2,3', exp_error) self._test_bad_spec('--basic', '?,+', exp_error) + def test_option_data_with_pzx_file(self): + pzx = PZX() + pzx.add_data(list(range(32, 94))) + pzxfile = self.write_bin_file(pzx.data, suffix='.pzx') + output, error = self.run_tapinfo(f'--data {pzxfile}') + self.assertEqual(error, '') + exp_output = r""" + 1: PZX header block + Version: 1.0 + 2: Data block + Bits: 496 (62 bytes) + Initial pulse level: 0 + 0-bit pulse sequence: 855, 855 (T-states) + 1-bit pulse sequence: 1710, 1710 (T-states) + Tail pulse: 945 T-states + Length: 62 + 0000 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F !"#$%&'()*+,-./ + 0010 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 0123456789:;<=>? + 0020 40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F @ABCDEFGHIJKLMNO + 0030 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D PQRSTUVWXYZ[\] + """ + self.assertEqual(dedent(exp_output).lstrip(), output) + def test_option_d_with_tap_file(self): data = [1, 2, 4, 8] tap_data = create_tap_header_block('test_tap02', 49152, len(data))