#!/usr/bin/env python3 # Measurement probe for the 99-python3-intelhex suite. # # ONE python3 start, many measurements. Every line is `key=value'; the suite # asserts each value with check_eq against a literal it carries. Nothing here # asserts -- a probe that raised would take every case below it with it, so each # measurement is wrapped and a failure is reported as the value of its own key. # # Every fixture is written by this file. Nothing is read from the image except # the intelhex package itself, and the only writes are under $TMPDIR. import os import sys import tempfile N = [0] def emit(k, v): # Printed as it is measured, not buffered to the end: a probe that dies at # measurement 150 must still hand the suite the 149 before it, or one # unexpected exception costs every case in the file. N[0] += 1 sys.stdout.write('%s=%s\n' % (k, v)) def m(k, fn): """Measure fn(); emit its value, or `EXC:' if it raised.""" try: emit(k, fn()) except Exception as e: # noqa: BLE001 emit(k, 'EXC:%s' % type(e).__name__) def raises(k, fn): """Emit the exception type fn() raised, or `NORAISE:' if it did not.""" try: r = fn() emit(k, 'NORAISE:%r' % (r,)) except Exception as e: # noqa: BLE001 emit(k, type(e).__name__) def raises_msg(k, fn): """Emit `|' -- the documented message is part of the API.""" try: r = fn() emit(k, 'NORAISE:%r' % (r,)) except Exception as e: # noqa: BLE001 emit(k, '%s|%s' % (type(e).__name__, e)) TMP = tempfile.mkdtemp(prefix='bst-intelhex.') import intelhex # noqa: E402 from intelhex import ( # noqa: E402 IntelHex, IntelHex16bit, IntelHexError, HexReaderError, HexRecordError, RecordLengthError, RecordTypeError, RecordChecksumError, EOFRecordError, ExtendedAddressRecordError, ExtendedSegmentAddressRecordError, ExtendedLinearAddressRecordError, StartAddressRecordError, StartSegmentAddressRecordError, StartLinearAddressRecordError, DuplicateStartAddressRecordError, InvalidStartAddressValueError, AddressOverlapError, NotEnoughDataError, BadAccess16bit, EmptyIntelHexError, Record, hex2bin, bin2hex, diff_dumps, ) from intelhex import compat # noqa: E402 from intelhex.compat import StringIO # noqa: E402 # =========================================================================== # 1. WHAT WAS IMPORTED # =========================================================================== emit('module_file', intelhex.__file__) emit('is_package', 'yes' if os.path.basename(intelhex.__file__) == '__init__.py' else 'no') emit('package_dir', os.path.dirname(intelhex.__file__)) # `import intelhex' does NOT bind a version: __init__.py imports neither # __version__ nor anything from it, so `intelhex.__version__' is an # AttributeError until the SUBMODULE of that name is imported -- after which the # attribute is the module, never the string a caller expects. emit('bare_version_attr', 'yes' if hasattr(intelhex, '__version__') else 'no') from intelhex import __version__ as _ihver # noqa: E402 emit('version_attr_is_module', 'yes' if type(_ihver).__name__ == 'module' else 'no') emit('version_str', _ihver.version_str) emit('version_info', '.'.join(str(i) for i in _ihver.version_info)) emit('version_info_is_tuple', 'yes' if isinstance(_ihver.version_info, tuple) else 'no') emit('docformat', intelhex.__docformat__) emit('has_dunder_all', 'yes' if hasattr(intelhex, '__all__') else 'no') emit('compat_python', compat.Python) emit('compat_strtype', compat.StrType.__name__) emit('compat_inttypes', ','.join(t.__name__ for t in compat.IntTypes)) emit('submodule_compat', 'yes' if compat.__name__ == 'intelhex.compat' else 'no') m('submodule_getsizeof', lambda: __import__('intelhex.getsizeof', fromlist=['x']).__name__) m('submodule_test_import', lambda: __import__('intelhex.test', fromlist=['x']).__name__) m('submodule_bench_import', lambda: __import__('intelhex.bench', fromlist=['x']).__name__) # =========================================================================== # 2. THE RECORD TYPES, ONE AT A TIME # # Each fixture is written here as text and parsed through StringIO, so the # expectation is a statement about the decoder and not about this image's # filesystem. # =========================================================================== def load(text): ih = IntelHex() ih.loadhex(StringIO(text)) return ih EOF_REC = ':00000001FF\n' # --- type 00, a data record at a low address DATA = ':0410000041424344E2\n' + EOF_REC m('data_len', lambda: len(load(DATA))) m('data_minaddr', lambda: '0x%04X' % load(DATA).minaddr()) m('data_maxaddr', lambda: '0x%04X' % load(DATA).maxaddr()) m('data_bytes', lambda: load(DATA).tobinstr().decode('latin1')) m('data_first', lambda: '0x%02X' % load(DATA)[0x1000]) m('data_addresses', lambda: ','.join('%X' % a for a in load(DATA).addresses())) m('data_start_addr', lambda: repr(load(DATA).start_addr)) # --- type 04, extended linear address moves the base above 64 KiB ELA = ':020000040001F9\n:0400000041424344F2\n' + EOF_REC m('ela_minaddr', lambda: '0x%08X' % load(ELA).minaddr()) m('ela_maxaddr', lambda: '0x%08X' % load(ELA).maxaddr()) m('ela_byte', lambda: '0x%02X' % load(ELA)[0x10000]) m('ela_len', lambda: len(load(ELA))) emit('ela_above_64k', 'yes' if load(ELA).minaddr() > 0xFFFF else 'no') # --- type 02, extended segment address is a x16 offset, not a x65536 one ESA = ':020000021000EC\n:0400000041424344F2\n' + EOF_REC m('esa_minaddr', lambda: '0x%08X' % load(ESA).minaddr()) emit('esa_offset_is_x16', 'yes' if load(ESA).minaddr() == 0x1000 * 16 else 'no') # --- type 03, start segment address SSA = ':0400000300100020C9\n:0410000041424344E2\n' + EOF_REC m('ssa_keys', lambda: ','.join(sorted(load(SSA).start_addr))) m('ssa_cs', lambda: '0x%04X' % load(SSA).start_addr['CS']) m('ssa_ip', lambda: '0x%04X' % load(SSA).start_addr['IP']) # --- type 05, start linear address SLA = ':0400000508000000EF\n:0410000041424344E2\n' + EOF_REC m('sla_keys', lambda: ','.join(sorted(load(SLA).start_addr))) m('sla_eip', lambda: '0x%08X' % load(SLA).start_addr['EIP']) emit('sla_present', 'yes' if load(SLA).start_addr is not None else 'no') # --- type 01, the end-of-file record stops the reader mid-file AFTER_EOF = ':0410000041424344E2\n' + EOF_REC + ':0420000041424344D2\n' m('eof_stops_reader', lambda: len(load(AFTER_EOF))) m('eof_stops_addrs', lambda: ','.join('%X' % a for a in load(AFTER_EOF).addresses())) # =========================================================================== # 3. THE ROUND TRIP -- the backbone # =========================================================================== def sparse(): ih = IntelHex() ih[0x0000] = 0x01 ih[0x0001] = 0x02 ih[0x0002] = 0x03 # a hole at 0x0003..0x000F ih[0x0010] = 0x10 ih[0x0011] = 0x11 # a second hole, and data above 64 KiB so an ELA record has to be written ih[0x12345] = 0xAA ih[0x12346] = 0xBB ih.start_addr = {'EIP': 0x08000000} return ih def roundtrip(obj, **kw): sio = StringIO() obj.write_hex_file(sio, **kw) text = sio.getvalue() back = IntelHex() back.loadhex(StringIO(text)) return text, back _txt, _back = roundtrip(sparse()) emit('rt_text_lines', len(_txt.splitlines())) emit('rt_dict_identical', 'yes' if _back.todict() == sparse().todict() else 'no') emit('rt_len', len(_back)) emit('rt_minaddr', '0x%08X' % _back.minaddr()) emit('rt_maxaddr', '0x%08X' % _back.maxaddr()) emit('rt_addresses', ','.join('%X' % a for a in _back.addresses())) emit('rt_start_addr', repr(_back.start_addr)) emit('rt_holes_preserved', 'yes' if 0x0003 not in _back.todict() else 'no') emit('rt_hole_reads_padding', '0x%02X' % _back[0x0003]) emit('rt_first_record', _txt.splitlines()[0]) emit('rt_last_record', _txt.splitlines()[-1]) emit('rt_has_ela', 'yes' if any(l[7:9] == '04' for l in _txt.splitlines()) else 'no') emit('rt_segments', repr(_back.segments())) emit('rt_segments_min_gap_32', repr(_back.segments(min_gap=32))) emit('rt_text_sha_len', len(_txt)) emit('rt_eol_native', 'LF' if '\r' not in _txt else 'CRLF') # the same object written with CRLF, on a platform that is not win32 _txt_crlf, _ = roundtrip(sparse(), eolstyle='CRLF') emit('rt_crlf_has_cr', 'yes' if '\r\n' in _txt_crlf else 'no') emit('rt_crlf_same_records', 'yes' if _txt_crlf.replace('\r\n', '\n') == _txt else 'no') raises('rt_eolstyle_bogus', lambda: sparse().write_hex_file(StringIO(), eolstyle='LF')) # byte_count controls the data field width, and is bounded _txt1, _b1 = roundtrip(sparse(), byte_count=1) emit('rt_bc1_dict_identical', 'yes' if _b1.todict() == sparse().todict() else 'no') emit('rt_bc1_lines', len(_txt1.splitlines())) _txt255, _b255 = roundtrip(sparse(), byte_count=255) emit('rt_bc255_dict_identical', 'yes' if _b255.todict() == sparse().todict() else 'no') raises('rt_bc_zero', lambda: sparse().write_hex_file(StringIO(), byte_count=0)) raises('rt_bc_256', lambda: sparse().write_hex_file(StringIO(), byte_count=256)) raises_msg('rt_bc_zero_msg', lambda: sparse().write_hex_file(StringIO(), byte_count=0)) # write_start_addr=False drops the start-address record and nothing else _txt_ns, _b_ns = roundtrip(sparse(), write_start_addr=False) emit('rt_nostart_start_addr', repr(_b_ns.start_addr)) emit('rt_nostart_dict_data', 'yes' if _b_ns._buf == sparse()._buf else 'no') emit('rt_nostart_lines', len(_txt_ns.splitlines())) # a file round-tripped through the filesystem, not only through StringIO _hexpath = os.path.join(TMP, 'rt.hex') _binpath = os.path.join(TMP, 'rt.bin') sparse().write_hex_file(_hexpath) _fromfile = IntelHex(_hexpath) emit('file_rt_dict_identical', 'yes' if _fromfile.todict() == sparse().todict() else 'no') emit('file_rt_exists', 'yes' if os.path.isfile(_hexpath) else 'no') # =========================================================================== # 4. tobinstr / tobinarray OVER A SPARSE IMAGE WITH A HOLE # =========================================================================== def holed(): ih = IntelHex() ih[0x00] = 0xDE ih[0x01] = 0xAD # hole 0x02..0x03 ih[0x04] = 0xBE ih[0x05] = 0xEF return ih m('bin_default_pad', lambda: '0x%02X' % holed().padding) m('bin_str_default', lambda: holed().tobinstr().hex().upper()) m('bin_arr_default', lambda: ','.join('%02X' % b for b in holed().tobinarray())) m('bin_arr_typecode', lambda: holed().tobinarray().typecode) m('bin_len', lambda: len(holed().tobinstr())) def padded(p): ih = holed() ih.padding = p return ih.tobinstr().hex().upper() m('bin_pad_00', lambda: padded(0x00)) m('bin_pad_5A', lambda: padded(0x5A)) m('bin_str_range', lambda: holed().tobinstr(start=0x01, end=0x04).hex().upper()) m('bin_str_size', lambda: holed().tobinstr(start=0x00, size=3).hex().upper()) m('bin_str_size_from_end', lambda: holed().tobinstr(end=0x05, size=2).hex().upper()) raises('bin_size_and_both', lambda: holed().tobinstr(start=0, end=5, size=2)) raises('bin_size_zero', lambda: holed().tobinstr(start=0, size=0)) raises('bin_size_negative', lambda: holed().tobinstr(start=0, size=-1)) m('bin_reversed_range', lambda: holed().tobinstr(start=0x05, end=0x00).hex().upper()) m('bin_empty_object', lambda: repr(IntelHex().tobinstr())) m('bin_empty_with_range', lambda: IntelHex().tobinstr(start=0, end=2).hex().upper()) # there is NO frombinstr; the inverse of tobinstr is frombytes/puts/loadbin emit('has_frombinstr', 'yes' if hasattr(IntelHex, 'frombinstr') else 'no') emit('has_frombytes', 'yes' if hasattr(IntelHex, 'frombytes') else 'no') emit('has_loadbin', 'yes' if hasattr(IntelHex, 'loadbin') else 'no') emit('has_puts', 'yes' if hasattr(IntelHex, 'puts') else 'no') def frombytes_rt(): ih = IntelHex() ih.frombytes(bytearray(b'\xde\xad\xbe\xef'), offset=0x10) return '%s@%X' % (ih.tobinstr().hex().upper(), ih.minaddr()) m('frombytes_rt', frombytes_rt) def loadbin_rt(): with open(_binpath, 'wb') as f: f.write(b'\x01\x02\x03\x04') ih = IntelHex() ih.loadbin(_binpath, offset=0x20) return '%s@%X' % (ih.tobinstr().hex().upper(), ih.minaddr()) m('loadbin_rt', loadbin_rt) # the deprecated `pad' argument STILL WORKS and prints its notice on STDOUT def deprecated_pad(): import io as _io old, sys.stdout = sys.stdout, _io.StringIO() try: v = holed().tobinstr(start=0x00, end=0x05, pad=0x11).hex().upper() noise = sys.stdout.getvalue() finally: sys.stdout = old return '%s|lines=%d|first=%s' % (v, len(noise.splitlines()), noise.splitlines()[0] if noise else '') m('deprecated_pad', deprecated_pad) def deprecated_pad_none(): import io as _io old, sys.stdout = sys.stdout, _io.StringIO() try: v = holed().tobinstr(start=0x00, end=0x05, pad=None).hex().upper() noise = sys.stdout.getvalue() finally: sys.stdout = old return '%s|lines=%d' % (v, len(noise.splitlines())) m('deprecated_pad_none', deprecated_pad_none) def undeprecated_quiet(): import io as _io old, sys.stdout = sys.stdout, _io.StringIO() try: holed().tobinstr(start=0x00, end=0x05) noise = sys.stdout.getvalue() finally: sys.stdout = old return 'lines=%d' % len(noise.splitlines()) m('undeprecated_quiet', undeprecated_quiet) # =========================================================================== # 5. THE ERROR PATHS. Each is a case of its own, and each names the class. # =========================================================================== raises('err_bad_checksum', lambda: load(':0410000041424344E3\n' + EOF_REC)) raises_msg('err_bad_checksum_msg', lambda: load(':0410000041424344E3\n' + EOF_REC)) raises('err_bad_length', lambda: load(':0510000041424344E2\n' + EOF_REC)) raises_msg('err_bad_length_msg', lambda: load(':0510000041424344E2\n' + EOF_REC)) raises('err_truncated', lambda: load(':04100000\n' + EOF_REC)) raises_msg('err_truncated_msg', lambda: load(':04100000\n' + EOF_REC)) raises('err_too_short', lambda: load(':0000\n' + EOF_REC)) raises('err_odd_digits', lambda: load(':0410000041424344E2F\n' + EOF_REC)) raises('err_no_colon', lambda: load('0410000041424344E2\n' + EOF_REC)) raises('err_garbage_line', lambda: load('this is not a hex file\n')) raises('err_bad_rectype', lambda: load(':0410000641424344DC\n' + EOF_REC)) raises_msg('err_bad_rectype_msg', lambda: load(':0410000641424344DC\n' + EOF_REC)) raises('err_eof_with_data', lambda: load(':0100000141BD\n')) raises('err_esa_bad_len', lambda: load(':03000002100000EB\n' + EOF_REC)) raises('err_esa_bad_offset', lambda: load(':020001021000EB\n' + EOF_REC)) raises('err_ela_bad_len', lambda: load(':03000004000100F8\n' + EOF_REC)) raises('err_ssa_bad_len', lambda: load(':03000003001000EA\n' + EOF_REC)) raises('err_sla_bad_len', lambda: load(':03000005080000F0\n' + EOF_REC)) raises('err_overlap', lambda: load(':0410000041424344E2\n:0410000045464748D2\n' + EOF_REC)) raises_msg('err_overlap_msg', lambda: load(':0410000041424344E2\n:0410000045464748D2\n' + EOF_REC)) raises('err_dup_start_sla', lambda: load(':0400000508000000EF\n:0400000508000000EF\n' + EOF_REC)) raises('err_dup_start_ssa', lambda: load(':0400000300100020C9\n:0400000300100020C9\n' + EOF_REC)) # THE RECORD-TYPE CHECK RUNS BEFORE THE CHECKSUM CHECK. A record that is wrong # in both ways reports the type, never the checksum, so a caller diagnosing a # corrupt file by the exception class is told the wrong thing about it. raises('err_type_beats_checksum', lambda: load(':0410000641424344DD\n' + EOF_REC)) # ... and the LENGTH check runs before both. raises('err_length_beats_type', lambda: load(':0510000641424344DD\n' + EOF_REC)) # A HEX FILE WITH NO END-OF-FILE RECORD PARSES CLEANLY. loadhex's loop simply # runs out of lines; nothing checks that _EndOfFile was ever raised, so the # terminator is optional in practice however the format defines it. NO_EOF = ':0410000041424344E2\n' def no_eof_raises(): try: load(NO_EOF) return 'no' except Exception as e: # noqa: BLE001 return type(e).__name__ m('no_eof_record_len', lambda: len(load(NO_EOF))) m('no_eof_record_bytes', lambda: load(NO_EOF).tobinstr().decode('latin1')) m('no_eof_record_raises', no_eof_raises) m('empty_file_len', lambda: len(load(''))) m('blank_lines_ignored', lambda: len(load('\n\n:0410000041424344E2\n\n' + EOF_REC))) m('crlf_input_accepted', lambda: len(load(':0410000041424344E2\r\n:00000001FF\r\n'))) # gets / getsz over a hole raises('err_gets_hole', lambda: holed().gets(0x00, 6)) raises_msg('err_gets_hole_msg', lambda: holed().gets(0x00, 6)) m('gets_ok', lambda: holed().gets(0x00, 2).hex().upper()) def getsz(): ih = IntelHex() ih.putsz(0x00, b'hi') return '%s|%d|%02X' % (ih.getsz(0x00).decode('latin1'), len(ih), ih[0x02]) m('putsz_getsz', getsz) raises('err_getsz_no_zero', lambda: holed().getsz(0x00)) # indexing raises('err_getitem_negative', lambda: IntelHex()[-1]) raises_msg('err_getitem_negative_msg', lambda: IntelHex()[-1]) raises('err_getitem_str', lambda: IntelHex()['x']) raises('err_setitem_negative', lambda: holed().__setitem__(-1, 0)) m('getitem_absent_is_padding', lambda: '0x%02X' % IntelHex()[0x99]) # empty-object operations # EmptyIntelHexError IS DEFINED AND, IN 2.3.0, UNREACHABLE. Every caller of # _get_start_end -- the only site that raises it -- is guarded by an earlier # `if self._buf == {} and None in (start, end): return bin', so the empty object # whose documentation promises the exception answers with an empty array. m('empty_tobinarray_returns', lambda: repr(IntelHex().tobinarray())) m('empty_tobinarray_len', lambda: len(IntelHex().tobinarray())) m('empty_tobinarray_with_size', lambda: repr(IntelHex().tobinarray(size=5))) emit('empty_error_class_exists', 'yes' if issubclass(EmptyIntelHexError, IntelHexError) else 'no') emit('empty_error_fmt', EmptyIntelHexError._fmt) m('empty_minaddr', lambda: repr(IntelHex().minaddr())) m('empty_maxaddr', lambda: repr(IntelHex().maxaddr())) m('empty_len', lambda: len(IntelHex())) m('empty_segments', lambda: repr(IntelHex().segments())) m('empty_addresses', lambda: repr(IntelHex().addresses())) # =========================================================================== # 6. THE EXCEPTION HIERARCHY, AS THE MODULE DOCUMENTS IT # =========================================================================== HIER = [ ('HexReaderError', HexReaderError, IntelHexError), ('AddressOverlapError', AddressOverlapError, HexReaderError), ('HexRecordError', HexRecordError, HexReaderError), ('RecordLengthError', RecordLengthError, HexRecordError), ('RecordTypeError', RecordTypeError, HexRecordError), ('RecordChecksumError', RecordChecksumError, HexRecordError), ('EOFRecordError', EOFRecordError, HexRecordError), ('ExtendedAddressRecordError', ExtendedAddressRecordError, HexRecordError), ('ExtendedSegmentAddressRecordError', ExtendedSegmentAddressRecordError, ExtendedAddressRecordError), ('ExtendedLinearAddressRecordError', ExtendedLinearAddressRecordError, ExtendedAddressRecordError), ('StartAddressRecordError', StartAddressRecordError, HexRecordError), ('StartSegmentAddressRecordError', StartSegmentAddressRecordError, StartAddressRecordError), ('StartLinearAddressRecordError', StartLinearAddressRecordError, StartAddressRecordError), ('DuplicateStartAddressRecordError', DuplicateStartAddressRecordError, StartAddressRecordError), ('InvalidStartAddressValueError', InvalidStartAddressValueError, StartAddressRecordError), ('NotEnoughDataError', NotEnoughDataError, IntelHexError), ('BadAccess16bit', BadAccess16bit, NotEnoughDataError), ('EmptyIntelHexError', EmptyIntelHexError, IntelHexError), ] for _name, _cls, _base in HIER: emit('hier_%s' % _name, 'yes' if issubclass(_cls, _base) else 'no') emit('hier_base_is_exception', 'yes' if issubclass(IntelHexError, Exception) else 'no') emit('hier_all_from_base', 'yes' if all(issubclass(c, IntelHexError) for _, c, _ in HIER) else 'no') emit('hier_endoffile_is_error', 'yes' if issubclass(intelhex._EndOfFile, IntelHexError) else 'no') emit('err_base_fmt', IntelHexError._fmt) emit('err_base_str', str(IntelHexError())) emit('err_msg_wins', str(IntelHexError('a message', line=7))) emit('err_kw_becomes_attr', str(getattr(IntelHexError(line=7), 'line'))) emit('err_unprintable', str(RecordLengthError())[:26]) # =========================================================================== # 7. THE QUIRKS -- what a caller gets wrong # =========================================================================== # 7a. dump() on a start_addr that is neither {CS,IP} nor {EIP} reaches an # undefined name. Upstream defect, reachable from public API. def dump_mixed(): ih = IntelHex() ih[0] = 0x41 ih.start_addr = {'CS': 0x0010} sio = StringIO() ih.dump(sio) return 'NORAISE:%r' % sio.getvalue() raises('quirk_dump_partial_start', dump_mixed) def dump_eip(): ih = IntelHex() ih[0] = 0x41 ih.start_addr = {'EIP': 0x08000000} sio = StringIO() ih.dump(sio) return sio.getvalue().splitlines()[0] m('dump_eip_header', dump_eip) def dump_csip(): ih = IntelHex() ih[0] = 0x41 ih.start_addr = {'CS': 0x0010, 'IP': 0x0020} sio = StringIO() ih.dump(sio) return sio.getvalue().splitlines()[0] m('dump_csip_header', dump_csip) def dump_body(): sio = StringIO() holed().dump(sio) return sio.getvalue().splitlines()[-1] m('dump_body_line', dump_body) def dump_padded(): sio = StringIO() holed().dump(sio, withpadding=True) return sio.getvalue().splitlines()[-1] m('dump_body_withpadding', dump_padded) raises('dump_bad_width', lambda: holed().dump(StringIO(), width=0)) # 7b. writing a start_addr that is neither shape raises at write time def write_bad_start(): ih = IntelHex() ih[0] = 0x41 ih.start_addr = {'CS': 0x10} sio = StringIO() ih.write_hex_file(sio) return 'NORAISE' raises('quirk_write_partial_start', write_bad_start) raises_msg('quirk_write_partial_start_msg', write_bad_start) # 7c. NO ADDRESS WRAP after a type-02 record: the decoder's own FIXME. A data # record that runs past the 64 KiB boundary keeps counting linearly rather # than wrapping to the segment base. NOWRAP = ':02000002F0000C\n:04FFFE0041424344F5\n' + EOF_REC m('nowrap_addresses', lambda: ','.join('%X' % a for a in load(NOWRAP).addresses())) m('nowrap_maxaddr', lambda: '0x%08X' % load(NOWRAP).maxaddr()) emit('nowrap_is_linear', 'yes' if load(NOWRAP).maxaddr() == 0xF0000 + 0xFFFE + 3 else 'no') # 7d. merge def merge(mode, pre=None): a = IntelHex() a[0] = 0x01 a[1] = 0x02 b = IntelHex() b[1] = 0xFF b[2] = 0x03 if pre: a.start_addr, b.start_addr = pre a.merge(b, overlap=mode) return ','.join('%X:%02X' % (k, v) for k, v in sorted(a.todict().items()) if k != 'start_addr') m('merge_replace', lambda: merge('replace')) m('merge_ignore', lambda: merge('ignore')) raises('merge_error', lambda: merge('error')) raises('merge_bad_mode', lambda: merge('clobber')) def merge_self(): a = holed() a.merge(a) return 'NORAISE' raises('merge_self', merge_self) raises('merge_not_intelhex', lambda: IntelHex().merge({})) def merge_start_from_other(): a = IntelHex() a[0] = 1 b = IntelHex() b[5] = 2 b.start_addr = {'EIP': 0x1234} a.merge(b) return repr(a.start_addr) m('merge_start_adopted', merge_start_from_other) def merge_start_conflict(mode): a = IntelHex() a[0] = 1 a.start_addr = {'EIP': 1} b = IntelHex() b[5] = 2 b.start_addr = {'EIP': 2} a.merge(b, overlap=mode) return repr(a.start_addr) raises('merge_start_conflict_error', lambda: merge_start_conflict('error')) m('merge_start_conflict_replace', lambda: merge_start_conflict('replace')) m('merge_start_conflict_ignore', lambda: merge_start_conflict('ignore')) # 7e. find / segments / slices m('find_hit', lambda: '0x%02X' % holed().find(b'\xbe\xef')) m('find_miss', lambda: holed().find(b'\x00\x01')) m('find_across_hole', lambda: holed().find(b'\xad\xbe')) m('slice_get', lambda: ','.join('%X:%02X' % (k, v) for k, v in sorted(holed()[0x00:0x02].todict().items()))) def slice_set(): ih = IntelHex() ih[0x10:0x13] = [1, 2, 3] return ','.join('%X:%02X' % (k, v) for k, v in sorted(ih.todict().items())) m('slice_set', slice_set) raises('slice_set_bad_len', lambda: IntelHex().__setitem__(slice(0, 3), [1, 2])) raises('slice_set_not_seq', lambda: IntelHex().__setitem__(slice(0, 3), 'abc')) raises('slice_set_open', lambda: IntelHex().__setitem__(slice(None, None), [1])) def slice_del(): ih = holed() del ih[0x00:0x02] return ','.join('%X' % a for a in ih.addresses()) m('slice_del', slice_del) def item_del(): ih = holed() del ih[0x00] return ','.join('%X' % a for a in ih.addresses()) m('item_del', item_del) raises('item_del_absent', lambda: holed().__delitem__(0x02)) # 7f. IntelHex16bit def h16(): ih = IntelHex() ih[0] = 0x34 ih[1] = 0x12 return IntelHex16bit(ih) m('h16_word', lambda: '0x%04X' % h16()[0]) m('h16_padding_extended', lambda: '0x%04X' % h16().padding) m('h16_minaddr', lambda: h16().minaddr()) m('h16_maxaddr', lambda: h16().maxaddr()) m('h16_absent_is_padding', lambda: '0x%04X' % h16()[0x99]) m('h16_typecode', lambda: h16().tobinarray().typecode) m('h16_tobinarray', lambda: ','.join('%04X' % w for w in h16().tobinarray())) def h16_halfword(): ih = IntelHex() ih[0] = 0x34 return IntelHex16bit(ih)[0] raises('h16_half_word_raises', h16_halfword) def h16_setitem(): x = h16() x[2] = 0xBEEF return '%02X%02X' % (x._buf[4], x._buf[5]) m('h16_setitem_little_endian', h16_setitem) raises('h16_from_dict', lambda: IntelHex16bit({0: 1})) def h16_padding_kept(): ih = IntelHex() ih.padding = 0x00 ih[0] = 0x34 ih[1] = 0x12 return '0x%04X' % IntelHex16bit(ih).padding m('h16_padding_not_extended', h16_padding_kept) # 7g. Record helpers emit('rec_eof', Record.eof()) emit('rec_data', Record.data(0x1000, [0x41, 0x42, 0x43, 0x44])) emit('rec_esa', Record.extended_segment_address(0x1000)) emit('rec_ela', Record.extended_linear_address(0x0001)) emit('rec_ssa', Record.start_segment_address(0x0010, 0x0020)) emit('rec_sla', Record.start_linear_address(0x08000000)) emit('rec_data_roundtrips', 'yes' if len(load(Record.data(0x1000, [0x41]) + '\n' + EOF_REC)) == 1 else 'no') raises('rec_data_empty', lambda: Record.data(0, [])) raises('rec_data_offset_high', lambda: Record.data(0x10000, [1])) # 7h. the private file:start:end notation hexmerge.py is built on m('far_plain', lambda: repr(intelhex._get_file_and_addr_range('a.hex'))) m('far_range', lambda: repr(intelhex._get_file_and_addr_range('a.hex:0:FF'))) m('far_open_start', lambda: repr(intelhex._get_file_and_addr_range('a.hex::FF'))) m('far_open_end', lambda: repr(intelhex._get_file_and_addr_range('a.hex:10:'))) raises('far_two_parts', lambda: intelhex._get_file_and_addr_range('a.hex:10')) raises('far_bad_hex', lambda: intelhex._get_file_and_addr_range('a.hex:zz:FF')) m('far_drive_letter', lambda: repr(intelhex._get_file_and_addr_range('C:\\a.hex:1:A', True))) # 7i. the module-level convertor functions RETURN a status and PRINT the error def hex2bin_ok(): src = os.path.join(TMP, 'c.hex') dst = os.path.join(TMP, 'c.bin') sparse().write_hex_file(src) rc = hex2bin(src, dst) with open(dst, 'rb') as f: n = len(f.read()) return 'rc=%s size=%d' % (rc, n) m('hex2bin_ok', hex2bin_ok) def hex2bin_bad(): import io as _io src = os.path.join(TMP, 'bad.hex') with open(src, 'w') as f: f.write(':0400100041424344A4\n') old, sys.stdout = sys.stdout, _io.StringIO() try: rc = hex2bin(src, os.path.join(TMP, 'bad.bin')) noise = sys.stdout.getvalue().strip() finally: sys.stdout = old return 'rc=%s out=%s' % (rc, noise) m('hex2bin_bad_returns', hex2bin_bad) def bin2hex_ok(): src = os.path.join(TMP, 'd.bin') dst = os.path.join(TMP, 'd.hex') with open(src, 'wb') as f: f.write(b'ABCD') rc = bin2hex(src, dst, offset=0x100) with open(dst) as f: lines = f.read().splitlines() return 'rc=%s lines=%d first=%s' % (rc, len(lines), lines[0]) m('bin2hex_ok', bin2hex_ok) def diff_dumps_out(): a = holed() b = holed() b[0x00] = 0x00 sio = StringIO() diff_dumps(a, b, sio, name1='a', name2='b') lines = [l for l in sio.getvalue().splitlines() if l] return 'n=%d head=%s' % (len(lines), lines[0]) m('diff_dumps', diff_dumps_out) # 7j. constructing from the other accepted sources m('from_dict', lambda: ','.join('%X:%02X' % (k, v) for k, v in sorted(IntelHex({0: 1, 1: 2}).todict().items()))) m('from_dict_start', lambda: repr(IntelHex({0: 1, 'start_addr': {'EIP': 9}}).start_addr)) raises('from_dict_bad_key', lambda: IntelHex({'x': 1})) raises('from_dict_negative_key', lambda: IntelHex({-1: 1})) m('from_intelhex', lambda: ','.join('%X' % a for a in IntelHex(holed()).addresses())) raises('from_bad_type', lambda: IntelHex(3.5)) m('todict_roundtrip', lambda: 'yes' if IntelHex(sparse().todict()).todict() == sparse().todict() else 'no') m('fromfile_hex', lambda: len(IntelHex(_hexpath))) raises('loadfile_bad_format', lambda: IntelHex().loadfile(_hexpath, 'srec')) raises('tofile_bad_format', lambda: holed().tofile(StringIO(), 'srec')) m('get_memory_size_positive', lambda: 'yes' if holed().get_memory_size() > 0 else 'no') emit('probe_tmpdir_exists', 'yes' if os.path.isdir(TMP) else 'no') emit('emitted_count', N[0]) emit('probe_done', 'yes') sys.stdout.flush()