diff --git a/.travis.yml b/.travis.yml index 0f39be1..4f34c2c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,22 +2,20 @@ language: python sudo: false matrix: include: - - python: 2.6 - env: TOXENV=py26 - python: 2.7 env: TOXENV=py27 - - python: 3.2 - env: TOXENV=py32 - - python: 3.3 - env: TOXENV=py33 - python: 3.4 env: TOXENV=py34 - python: 3.5 env: TOXENV=py35 + - python: 3.6 + env: TOXENV=py36 + - python: 3.7 + env: TOXENV=py37 + dist: xenial # required for Python >= 3.7 - python: pypy env: TOXENV=pypy install: - # "virtualenv<14.0.0" is needed for Python 3.2 compat - - travis_retry pip install "virtualenv<14.0.0" tox + - travis_retry pip install virtualenv tox script: - travis_retry tox diff --git a/CHANGES.txt b/CHANGES.txt index edabb21..1b9e9b5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,26 @@ -0.25.dev0 (Next Release) ------------------------- +2.0.0.dev0 (Next Release) +------------------------- + + - Support for some older Python versions has been dropped. On Python 3, + Py65 now requires Python 3.4 or later. On Python 2, Py65 now requires + Python 2.7. + +1.1.0 (2018-07-01) +------------------ + + - The ``Monitor`` class now allows the default memory to be supplied in + the constructor. Patch by Irmen de Jong. + + - Fixed a bug where setting the MPU via ``py65mon`` command line arguments + would have no effect. Reported by Michael A. Morris, patch by Ed Spittles. + + - The ``itoa()`` function in ``conversions.py`` now raises an error when an + unsupported base is given. Patch by Scot W. Stevenson. + + - The unused hexdump loader utility has been removed. + +1.0.0 (2017-05-11) +------------------ - Fixed a bug where the ordering of ``py65mon`` command line arguments produced different results. Arguments can now be specified in any @@ -13,10 +34,12 @@ the Z flag correctly. Thanks to Kris Kennaway for reporting it. 0.24 (2015-03-31) +----------------- - Released as a universal wheel. 0.23 (2015-02-10) +----------------- - Added a workaround to $F001 output to catch encoding errors and display a "?" instead of crashing. This condition can occur if @@ -24,11 +47,13 @@ the terminal's character encoding. 0.22 (2015-02-09) +----------------- - Fixed a bug where ``py65mon --rom`` would raise an exception on Python 3. 0.21 (2015-02-09) +----------------- - Added support for breakpoints in the monitor. Contributed by Alessandro Gatti. @@ -39,12 +64,14 @@ - Fixed console input when run under Python 3 on Windows. Closes #27. 0.20 (2014-05-08) +----------------- - Page wrapping for indexed indirect (X) operations on 65C02 has been restored. This reverts the change introduced in 0.18. We now believe that this mode works the same on the 65C02 as it does on the 6502. 0.19 (2014-03-12) +----------------- - Fixed 65C02 opcode $D2: CMP Zero Page, Indirect. @@ -54,6 +81,7 @@ on $F005 have been changed to use $F004. 0.18 (2014-01-30) +----------------- - Fixed a bug in RTS where popping $FFFF off the stack would cause the program counter to overflow to $10000. It now wraps around @@ -69,15 +97,18 @@ - Removed page wrap bug from indexed indirect (X) operations on 65C02. 0.17 (2013-10-26) +----------------- - Added support for Python 3.2 and 3.3 based on work by David Beazley. 0.16 (2013-04-03) +----------------- - Fixed a bug in the monitor that caused loading from the command line with "--rom" to crash. 0.15 (2013-01-06) +----------------- - Disassembling can now wrap around memory if the start address given is greater than the end address. @@ -86,6 +117,7 @@ "start:end+1" for consistency with the other commands. 0.14 (2012-11-30) +----------------- - Assembling now detects syntax errors, overflows, and bad labels separately and shows specific error messages. @@ -109,11 +141,13 @@ the top of memory would not be displayed properly. 0.13 (2012-11-15) +----------------- - Fixed a bug where negative numbers could be entered for addresses in the monitor. 0.12 (2012-02-16) +----------------- - Fixed a bug that caused ``help cd`` to raise an exception in the monitor. @@ -128,6 +162,7 @@ - Added "h" as a monitor shortcut for "help". 0.11 (2012-01-07) +----------------- - Added a new 65Org16 MPU simulation written by Ed Spittles. @@ -139,6 +174,7 @@ - Python versions earlier than 2.6 are no longer supported. 0.10 (2011-08-27) +----------------- - Fixed long-standing bugs in relative branch calculations in the assembler and disassembler. Based on a patch by Ed Spittles. @@ -147,6 +183,7 @@ Patch by Martti Kühne. 0.9 (2011-03-27) +---------------- - Fixed two monitor tests that were broken under Windows. Thanks to Oscar Lindberg for reporting this. @@ -158,6 +195,7 @@ the decimal handling code. 0.8 (2010-03-08) +---------------- - Fixed deprecation warnings on Python 2.6 @@ -174,6 +212,7 @@ consistency with VICE. 0.7 (2009-09-03) +---------------- - When using the monitor, the nonblocking character input at $F004 should now work on the Microsoft Windows platform. @@ -213,6 +252,7 @@ a range of memory to a binary file. 0.6 (2009-08-11) +---------------- - Added monitor shortcut "a" for "assemble". @@ -226,6 +266,7 @@ Closes #3. 0.5 (2009-08-06) +---------------- - Fixed signatures of getc/putc callbacks in monitor that were broken when the ObservableMemory interface changed in 0.3. Closes #1. @@ -233,10 +274,12 @@ - Fixed that ROL would not properly set the Z flag. Closes #2. 0.4 (2009-06-06) +---------------- - Added ez_setup.py to bootstrap setuptools installation. 0.3 (2009-06-03) +---------------- - Added shortcuts for monitor commands such as "m" for "memory". These are mostly the same as the VICE monitor shortcuts. @@ -266,6 +309,7 @@ - Python 2.4 or later is now required. 0.2 (2008-11-09) +---------------- - Added a new "disassemble" command to the monitor. It can disassemble any range of memory ("disassemble c000:c010"). If labels have been @@ -292,5 +336,6 @@ command "help command" for help on any command. 0.1 (2008-11-21) +---------------- - First release. diff --git a/LICENSE.txt b/LICENSE.txt index 520884b..7d29071 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,25 +1,30 @@ -Copyright (c) 2008-2014, Mike Naberezny and contributors. +BSD 3-Clause License + +Copyright (c) 2008-2018, Mike Naberezny and contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of Mike Naberezny nor the names of the contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/docs/conf.py b/docs/conf.py index 794718f..6054383 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,8 +13,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import os -import sys from datetime import date # If your extensions are in another directory, add it here. If the directory @@ -51,7 +49,7 @@ copyright = u'2008-%d, Mike Naberezny and contributors' % year # built documents. # # The short X.Y version. -version = '0.25.dev0' +version = '1.1.0.dev0' # The full version, including alpha/beta/rc tags. release = version diff --git a/py65/devices/mpu6502.py b/py65/devices/mpu6502.py index 3ca4cef..b7954e5 100644 --- a/py65/devices/mpu6502.py +++ b/py65/devices/mpu6502.py @@ -76,7 +76,7 @@ class MPU: self.a = 0 self.x = 0 self.y = 0 - self.p = self.p = self.BREAK | self.UNUSED + self.p = self.BREAK | self.UNUSED self.processorCycles = 0 # Helpers for addressing modes diff --git a/py65/disassembler.py b/py65/disassembler.py index 902c816..9b8caf8 100644 --- a/py65/disassembler.py +++ b/py65/disassembler.py @@ -127,4 +127,8 @@ class Disassembler: disasm += ' %s,Y' % address_or_label length = 2 + else: + msg = "Addressing mode: %r" % addressing + raise NotImplementedError(msg) + return (length, disasm) diff --git a/py65/monitor.py b/py65/monitor.py index 5d2679c..38b1bd9 100644 --- a/py65/monitor.py +++ b/py65/monitor.py @@ -42,20 +42,39 @@ class Monitor(cmd.Cmd): Microprocessors = {'6502': NMOS6502, '65C02': CMOS65C02, '65Org16': V65Org16} - def __init__(self, mpu_type=NMOS6502, completekey='tab', stdin=None, - stdout=None, argv=None): + def __init__(self, argv=None, stdin=None, stdout=None, + mpu_type=NMOS6502, memory=None, + putc_addr=0xF001, getc_addr=0xF004): self.mpu_type = mpu_type - self.putc_addr = 0xF001 - self.getc_addr = 0xF004 - if argv is None: - argv = sys.argv + self.memory = memory + self.putc_addr = putc_addr + self.getc_addr = getc_addr self._breakpoints = [] self._width = 78 self.prompt = "." self._add_shortcuts() - cmd.Cmd.__init__(self, completekey, stdin, stdout) - self._parse_args(argv) - self._reset(self.mpu_type,self.getc_addr,self.putc_addr) + cmd.Cmd.__init__(self, stdin=stdin, stdout=stdout) + + if argv is None: + argv = sys.argv + load, rom, goto = self._parse_args(argv) + + self._reset(self.mpu_type, self.getc_addr, self.putc_addr) + + if load is not None: + self.do_load("%r" % load) + + if goto is not None: + self.do_goto(goto) + + if rom is not None: + # load a ROM and run from the reset vector + self.do_load("%r top" % rom) + physMask = self._mpu.memory.physMask + reset = self._mpu.RESET & physMask + dest = self._mpu.memory[reset] + \ + (self._mpu.memory[reset + 1] << self.byteWidth) + self.do_goto("$%x" % dest) def _parse_args(self, argv): try: @@ -67,10 +86,7 @@ class Monitor(cmd.Cmd): self._usage() self._exit(1) - load = None - rom = None - goto = None - mpu = None + load, rom, goto = None, None, None for opt, value in options: if opt in ('-i', '--input'): @@ -79,6 +95,19 @@ class Monitor(cmd.Cmd): if opt in ('-o', '--output'): self.putc_addr = int(value, 16) + if opt in ('-m', '--mpu'): + mpu_type = self._get_mpu(value) + if mpu_type is None: + mpus = sorted(self.Microprocessors.keys()) + msg = "Fatal: no such MPU. Available MPUs: %s" + self._output(msg % ', '.join(mpus)) + sys.exit(1) + self.mpu_type = mpu_type + + if opt in ("-h", "--help"): + self._usage() + self._exit(0) + if opt in ('-l', '--load'): load = value @@ -88,43 +117,7 @@ class Monitor(cmd.Cmd): if opt in ('-g', '--goto'): goto = value - if opt in ('-m', '--mpu'): - mpu = value - - elif opt in ("-h", "--help"): - self._usage() - self._exit(0) - - if (mpu is not None) or (rom is not None): - if mpu is None: - mpu = "6502" - if self._get_mpu(mpu) is None: - mpus = list(self.Microprocessors.keys()) - mpus.sort() - msg = "Fatal: no such MPU. Available MPUs: %s" - self._output(msg % ', '.join(mpus)) - sys.exit(1) - cmd = "mpu %s" % mpu - self.onecmd(cmd) - - if load is not None: - cmd = "load %s" % load - self.onecmd(cmd) - - if goto is not None: - cmd = "goto %s" % goto - self.onecmd(cmd) - - if rom is not None: - # load a ROM and run from the reset vector - cmd = "load '%s' top" % rom - self.onecmd(cmd) - physMask = self._mpu.memory.physMask - reset = self._mpu.RESET & physMask - dest = self._mpu.memory[reset] + \ - (self._mpu.memory[reset + 1] << self.byteWidth) - cmd = "goto %08x" % dest - self.onecmd(cmd) + return load, rom, goto def _usage(self): usage = __doc__ % sys.argv[0] @@ -148,15 +141,16 @@ class Monitor(cmd.Cmd): return result - def _reset(self, mpu_type,getc_addr=0xF004,putc_addr=0xF001): - self._mpu = mpu_type() + def _reset(self, mpu_type, getc_addr=0xF004, putc_addr=0xF001): + self._mpu = mpu_type(memory=self.memory) self.addrWidth = self._mpu.ADDR_WIDTH self.byteWidth = self._mpu.BYTE_WIDTH self.addrFmt = self._mpu.ADDR_FORMAT self.byteFmt = self._mpu.BYTE_FORMAT self.addrMask = self._mpu.addrMask self.byteMask = self._mpu.byteMask - self._install_mpu_observers(getc_addr,putc_addr) + if getc_addr and putc_addr: + self._install_mpu_observers(getc_addr, putc_addr) self._address_parser = AddressParser() self._disassembler = Disassembler(self._mpu, self._address_parser) self._assembler = Assembler(self._mpu, self._address_parser) @@ -229,7 +223,7 @@ class Monitor(cmd.Cmd): break return mpu - def _install_mpu_observers(self,getc_addr,putc_addr): + def _install_mpu_observers(self, getc_addr, putc_addr): def putc(address, value): try: self.stdout.write(chr(value)) @@ -245,7 +239,7 @@ class Monitor(cmd.Cmd): byte = 0 return byte - m = ObservableMemory(addrWidth=self.addrWidth) + m = ObservableMemory(subject=self.memory, addrWidth=self.addrWidth) m.subscribe_to_write([self.putc_addr], putc) m.subscribe_to_read([self.getc_addr], getc) diff --git a/py65/tests/devices/test_mpu6502.py b/py65/tests/devices/test_mpu6502.py index d83aab5..30031c0 100644 --- a/py65/tests/devices/test_mpu6502.py +++ b/py65/tests/devices/test_mpu6502.py @@ -1985,6 +1985,137 @@ class Common6502Tests: self.assertEqual(0x0001, mpu.pc) self.assertEqual(0, mpu.p & mpu.OVERFLOW) + # Compare instructions + + # See http://6502.org/tutorials/compare_instructions.html + # and http://www.6502.org/tutorials/compare_beyond.html + # Cheat sheet: + # + # - Comparison is actually subtraction "register - memory" + # - Z contains equality result (1 equal, 0 not equal) + # - C contains result of unsigned comparison (0 if A=m) + # - N holds MSB of subtraction result (*NOT* of signed subtraction) + # - V is not affected by comparison + # - D has no effect on comparison + + # CMP Immediate + + def test_cmp_imm_sets_zero_carry_clears_neg_flags_if_equal(self): + """Comparison: A == m""" + mpu = self._make_mpu() + # $0000 CMP #10 , A will be 10 + self._write(mpu.memory, 0x0000, (0xC9, 10)) + mpu.a = 10 + mpu.step() + self.assertEqual(0x0002, mpu.pc) + self.assertEqual(0, mpu.p & mpu.NEGATIVE) + self.assertEqual(mpu.ZERO, mpu.p & mpu.ZERO) + self.assertEqual(mpu.CARRY, mpu.p & mpu.CARRY) + + def test_cmp_imm_clears_zero_carry_takes_neg_if_less_unsigned(self): + """Comparison: A < m (unsigned)""" + mpu = self._make_mpu() + # $0000 CMP #10 , A will be 1 + self._write(mpu.memory, 0x0000, (0xC9, 10)) + mpu.a = 1 + mpu.step() + self.assertEqual(0x0002, mpu.pc) + self.assertEqual(mpu.NEGATIVE, mpu.p & mpu.NEGATIVE) # 0x01-0x0A=0xF7 + self.assertEqual(0, mpu.p & mpu.ZERO) + self.assertEqual(0, mpu.p & mpu.CARRY) + + def test_cmp_imm_clears_zero_sets_carry_takes_neg_if_less_signed(self): + """Comparison: A < #nn (signed), A negative""" + mpu = self._make_mpu() + # $0000 CMP #1, A will be -1 (0xFF) + self._write(mpu.memory, 0x0000, (0xC9, 1)) + mpu.a = 0xFF + mpu.step() + self.assertEqual(0x0002, mpu.pc) + self.assertEqual(mpu.NEGATIVE, mpu.p & mpu.NEGATIVE) # 0xFF-0x01=0xFE + self.assertEqual(0, mpu.p & mpu.ZERO) + self.assertEqual(mpu.CARRY, mpu.p & mpu.CARRY) # A>m unsigned + + def test_cmp_imm_clears_zero_carry_takes_neg_if_less_signed_nega(self): + """Comparison: A < m (signed), A and m both negative""" + mpu = self._make_mpu() + # $0000 CMP #0xFF (-1), A will be -2 (0xFE) + self._write(mpu.memory, 0x0000, (0xC9, 0xFF)) + mpu.a = 0xFE + mpu.step() + self.assertEqual(0x0002, mpu.pc) + self.assertEqual(mpu.NEGATIVE, mpu.p & mpu.NEGATIVE) # 0xFE-0xFF=0xFF + self.assertEqual(0, mpu.p & mpu.ZERO) + self.assertEqual(0, mpu.p & mpu.CARRY) # A m (unsigned)""" + mpu = self._make_mpu() + # $0000 CMP #1 , A will be 10 + self._write(mpu.memory, 0x0000, (0xC9, 1)) + mpu.a = 10 + mpu.step() + self.assertEqual(0x0002, mpu.pc) + self.assertEqual(0, mpu.p & mpu.NEGATIVE) # 0x0A-0x01 = 0x09 + self.assertEqual(0, mpu.p & mpu.ZERO) + self.assertEqual(mpu.CARRY, mpu.p & mpu.CARRY) # A>m unsigned + + def test_cmp_imm_clears_zero_carry_takes_neg_if_more_signed(self): + """Comparison: A > m (signed), memory negative""" + mpu = self._make_mpu() + # $0000 CMP #$FF (-1), A will be 2 + self._write(mpu.memory, 0x0000, (0xC9, 0xFF)) + mpu.a = 2 + mpu.step() + self.assertEqual(0x0002, mpu.pc) + self.assertEqual(0, mpu.p & mpu.NEGATIVE) # 0x02-0xFF=0x01 + self.assertEqual(0, mpu.p & mpu.ZERO) + self.assertEqual(0, mpu.p & mpu.CARRY) # A m (signed), A and m both negative""" + mpu = self._make_mpu() + # $0000 CMP #$FE (-2), A will be -1 (0xFF) + self._write(mpu.memory, 0x0000, (0xC9, 0xFE)) + mpu.a = 0xFF + mpu.step() + self.assertEqual(0x0002, mpu.pc) + self.assertEqual(0, mpu.p & mpu.NEGATIVE) # 0xFF-0xFE=0x01 + self.assertEqual(0, mpu.p & mpu.ZERO) + self.assertEqual(mpu.CARRY, mpu.p & mpu.CARRY) # A>m unsigned + + + # CPX Immediate + + def test_cpx_imm_sets_zero_carry_clears_neg_flags_if_equal(self): + """Comparison: X == m""" + mpu = self._make_mpu() + # $0000 CPX #$20 + self._write(mpu.memory, 0x0000, (0xE0, 0x20)) + mpu.x = 0x20 + mpu.step() + self.assertEqual(0x0002, mpu.pc) + self.assertEqual(mpu.ZERO, mpu.p & mpu.ZERO) + self.assertEqual(mpu.CARRY, mpu.p & mpu.CARRY) + self.assertEqual(0, mpu.p & mpu.NEGATIVE) + + + # CPY Immediate + + def test_cpy_imm_sets_zero_carry_clears_neg_flags_if_equal(self): + """Comparison: Y == m""" + mpu = self._make_mpu() + # $0000 CPY #$30 + self._write(mpu.memory, 0x0000, (0xC0, 0x30)) + mpu.y = 0x30 + mpu.step() + self.assertEqual(0x0002, mpu.pc) + self.assertEqual(mpu.ZERO, mpu.p & mpu.ZERO) + self.assertEqual(mpu.CARRY, mpu.p & mpu.CARRY) + self.assertEqual(0, mpu.p & mpu.NEGATIVE) + + + # DEC Absolute def test_dec_abs_decrements_memory(self): diff --git a/py65/tests/devices/test_mpu65c02.py b/py65/tests/devices/test_mpu65c02.py index 79e779e..bd8affb 100644 --- a/py65/tests/devices/test_mpu65c02.py +++ b/py65/tests/devices/test_mpu65c02.py @@ -11,6 +11,16 @@ class MPUTests(unittest.TestCase, Common6502Tests): mpu = self._make_mpu() self.assertTrue('65C02' in repr(mpu)) + # Reset + + def test_reset_clears_decimal_flag(self): + # W65C02S Datasheet, Apr 14 2009, Table 7-1 Operational Enhancements + # NMOS 6502 decimal flag = indetermine after reset, CMOS 65C02 = 0 + mpu = self._make_mpu() + mpu.p = mpu.DECIMAL + mpu.reset() + self.assertEqual(0, mpu.p & mpu.DECIMAL) + # ADC Zero Page, Indirect def test_adc_bcd_off_zp_ind_carry_clear_in_accumulator_zeroes(self): diff --git a/py65/tests/test_monitor.py b/py65/tests/test_monitor.py index 928c9d1..8919aeb 100644 --- a/py65/tests/test_monitor.py +++ b/py65/tests/test_monitor.py @@ -158,7 +158,7 @@ class MonitorTests(unittest.TestCase): stdout = StringIO() mon = Monitor(stdout=stdout) mon.do_add_label('c000 base') - mon.do_assemble('c000 rts') + mon.do_assemble('base rts') mpu = mon._mpu self.assertEqual(0x60, mpu.memory[0xC000]) @@ -1144,6 +1144,107 @@ class MonitorTests(unittest.TestCase): out = stdout.getvalue() self.assertTrue(out.startswith("width ")) + def test_external_memory(self): + stdout = StringIO() + memory = bytearray(65536) + memory[10] = 0xff + mon = Monitor(memory=memory, stdout=stdout, putc_addr=None, getc_addr=None) + self.assertEqual(0xff, memory[10], "memory must remain untouched") + mon.do_mem('0008:000c') + mon.do_fill('0000:0020 ab') + self.assertEqual(0xab, memory[10], "memory must have been modified") + out = stdout.getvalue() + self.assertTrue(out.startswith('0008: 00 00 ff 00 00'), "monitor must see pre-initialized memory") + + # command line options + + def test_argv_mpu(self): + argv = ['py65mon', '--mpu', '65c02'] + stdout = StringIO() + mon = Monitor(argv=argv, stdout=stdout) + self.assertEqual('65C02', mon._mpu.name) + + def test_argv_mpu_invalid(self): + argv = ['py65mon', '--mpu', 'bad'] + stdout = StringIO() + try: + Monitor(argv=argv, stdout=stdout) + except SystemExit as exc: + self.assertEqual(1, exc.code) + self.assertTrue("Fatal: no such MPU." in stdout.getvalue()) + + def test_argv_goto(self): + argv = ['py65mon', '--goto', 'c000'] + stdout = StringIO() + memory = bytearray(0x10000) + memory[0xc000] = 0xea # c000 nop + memory[0xc001] = 0xea # c001 nop + memory[0xc002] = 0x00 # c002 brk + mon = Monitor(argv=argv, stdout=stdout, memory=memory) + self.assertEqual(0xc002, mon._mpu.pc) + + def test_argv_load(self): + with tempfile.NamedTemporaryFile('wb+') as f: + data = bytearray([0xab, 0xcd]) + f.write(data) + f.flush() + + argv = ['py65mon', '--load', f.name] + stdout = StringIO() + mon = Monitor(argv=argv, stdout=stdout) + self.assertEqual(list(data), mon._mpu.memory[:len(data)]) + + def test_argv_rom(self): + with tempfile.NamedTemporaryFile('wb+') as f: + rom = bytearray(4096) + rom[0] = 0xea # f000 nop + rom[1] = 0xea # f001 nop + rom[2] = 0x00 # f002 brk + rom[-2] = 0xf000 & 0xff # fffc reset vector low + rom[-3] = 0xf000 >> 8 # fffd reset vector high + f.write(rom) + f.flush() + + argv = ['py65mon', '--rom', f.name] + stdout = StringIO() + mon = Monitor(argv=argv, stdout=stdout) + self.assertEqual(list(rom), mon._mpu.memory[-len(rom):]) + self.assertEqual(0xf002, mon._mpu.pc) + + def test_argv_input(self): + argv = ['py65mon', '--input', 'abcd'] + stdout = StringIO() + mon = Monitor(argv=argv, stdout=stdout) + read_subscribers = mon._mpu.memory._read_subscribers + self.assertEqual(1, len(read_subscribers)) + self.assertTrue('getc' in repr(read_subscribers[0xabcd])) + + def test_argv_output(self): + argv = ['py65mon', '--output', 'dcba'] + stdout = StringIO() + mon = Monitor(argv=argv, stdout=stdout) + write_subscribers = mon._mpu.memory._write_subscribers + self.assertEqual(1, len(write_subscribers)) + self.assertTrue('putc' in repr(write_subscribers[0xdcba])) + + def test_argv_combination_rom_mpu(self): + with tempfile.NamedTemporaryFile('wb+') as f: + rom = bytearray(4096) + rom[0] = 0xea # f000 nop + rom[1] = 0xea # f001 nop + rom[2] = 0x00 # f002 brk + rom[-2] = 0xf000 & 0xff # fffc reset vector low + rom[-3] = 0xf000 >> 8 # fffd reset vector high + f.write(rom) + f.flush() + + argv = ['py65mon', '--rom', f.name, '--mpu', '65c02',] + stdout = StringIO() + mon = Monitor(argv=argv, stdout=stdout) + self.assertEqual('65C02', mon._mpu.name) + self.assertEqual(list(rom), mon._mpu.memory[-len(rom):]) + self.assertEqual(0xf002, mon._mpu.pc) + def test_suite(): return unittest.findTestCases(sys.modules[__name__]) diff --git a/py65/tests/utils/test_conversions.py b/py65/tests/utils/test_conversions.py index 4b780de..2aadd1a 100644 --- a/py65/tests/utils/test_conversions.py +++ b/py65/tests/utils/test_conversions.py @@ -16,6 +16,9 @@ class ConversionsTopLevelTests(unittest.TestCase): self.assertEqual('1010', itoa(10, base=2)) self.assertEqual('-1010', itoa(-10, base=2)) + def test_itoa_unsupported_base(self): + self.assertRaises(ValueError, itoa, 0, base=17) + def test_convert_to_bin(self): self.assertEqual(0, convert_to_bin(0)) self.assertEqual(99, convert_to_bin(0x99)) diff --git a/py65/tests/utils/test_hexdump.py b/py65/tests/utils/test_hexdump.py deleted file mode 100644 index da84992..0000000 --- a/py65/tests/utils/test_hexdump.py +++ /dev/null @@ -1,129 +0,0 @@ -import unittest -import sys -from py65.utils.hexdump import load, Loader - - -class TopLevelHexdumpTests(unittest.TestCase): - def test_load(self): - text = 'c000: aa bb' - start, data = load(text) - self.assertEqual(0xC000, start) - self.assertEqual([0xAA, 0xBB], data) - - -class HexdumpLoaderTests(unittest.TestCase): - def test_empty_string_does_nothing(self): - text = '' - loader = Loader(text) - self.assertEqual(None, loader.start_address) - self.assertEqual([], loader.data) - - def test_all_whitespace_does_nothing(self): - text = " \r\n \t \n" - loader = Loader(text) - self.assertEqual(None, loader.start_address) - self.assertEqual([], loader.data) - - def test_raises_when_start_address_not_found(self): - text = 'aa bb cc' - try: - Loader(text) - self.fail() - except ValueError as exc: - msg = 'Start address was not found in data' - self.assertEqual(msg, str(exc)) - - def test_raises_when_start_address_is_invalid(self): - text = 'oops: aa bb cc' - try: - Loader(text) - self.fail() - except ValueError as exc: - msg = 'Could not parse address: oops' - self.assertEqual(msg, str(exc)) - - def test_raises_when_start_address_is_too_short(self): - text = '01: aa bb cc' - try: - Loader(text) - self.fail() - except ValueError as exc: - msg = 'Expected address to be 2 bytes, got 1' - self.assertEqual(msg, str(exc)) - - def test_raises_when_start_address_is_too_long(self): - text = '010304: aa bb cc' - try: - Loader(text) - self.fail() - except ValueError as exc: - msg = 'Expected address to be 2 bytes, got 3' - self.assertEqual(msg, str(exc)) - - def test_raises_when_next_address_is_unexpected(self): - text = "c000: aa\nc002: cc" - try: - Loader(text) - self.fail() - except ValueError as exc: - msg = 'Non-contigous block detected. Expected next ' \ - 'address to be $c001, label was $c002' - self.assertEqual(msg, str(exc)) - - def test_raises_when_data_is_invalid(self): - text = 'c000: foo' - try: - Loader(text) - self.fail() - except ValueError as exc: - msg = 'Could not parse data: foo' - self.assertEqual(msg, str(exc)) - - def test_loads_data_without_dollar_signs(self): - text = 'c000: aa bb' - load = Loader(text) - self.assertEqual(0xC000, load.start_address) - self.assertEqual([0xAA, 0xBB], load.data) - - def test_loads_data_with_some_dollar_signs(self): - text = '$c000: aa $bb' - load = Loader(text) - self.assertEqual(0xC000, load.start_address) - self.assertEqual([0xAA, 0xBB], load.data) - - def test_loads_multiline_data_with_unix_endings(self): - text = '$c000: aa bb\n$c002: cc' - load = Loader(text) - self.assertEqual(0xC000, load.start_address) - self.assertEqual([0xAA, 0xBB, 0xCC], load.data) - - def test_loads_multiline_data_with_dos_endings(self): - text = '$c000: aa bb\r\n$c002: cc' - load = Loader(text) - self.assertEqual(0xC000, load.start_address) - self.assertEqual([0xAA, 0xBB, 0xCC], load.data) - - def test_ignores_semicolon_comments(self): - text = 'c000: aa bb ;comment' - load = Loader(text) - self.assertEqual(0xC000, load.start_address) - self.assertEqual([0xAA, 0xBB], load.data) - - def test_ignores_double_dash_comments(self): - text = 'c000: aa bb -- comment' - load = Loader(text) - self.assertEqual(0xC000, load.start_address) - self.assertEqual([0xAA, 0xBB], load.data) - - def test_ignores_pound_comments(self): - text = 'c000: aa bb # comment' - load = Loader(text) - self.assertEqual(0xC000, load.start_address) - self.assertEqual([0xAA, 0xBB], load.data) - - -def test_suite(): - return unittest.findTestCases(sys.modules[__name__]) - -if __name__ == '__main__': - unittest.main(defaultTest='test_suite') diff --git a/py65/utils/conversions.py b/py65/utils/conversions.py index 36bfba5..c24e21a 100644 --- a/py65/utils/conversions.py +++ b/py65/utils/conversions.py @@ -1,20 +1,12 @@ - - def itoa(num, base=10): - """ Convert a decimal number to its equivalent in another base. - This is essentially the inverse of int(num, base). + """Convert a decimal number to its equivalent in base 2 or 16; base 10 + is silently passed through. Other bases raise a ValueError. Returns a + string with hex digits lowercase. """ - negative = num < 0 - if negative: - num = -num - digits = [] - while num > 0: - num, last_digit = divmod(num, base) - digits.append('0123456789abcdefghijklmnopqrstuvwxyz'[last_digit]) - if negative: - digits.append('-') - digits.reverse() - return ''.join(digits) + fmt = _itoa_fmts.get(base) + if fmt is None: + raise ValueError("Unsupported base: %r" % base) + return fmt.format(num) def convert_to_bin(bcd): @@ -25,6 +17,13 @@ def convert_to_bcd(bin): return _bin2bcd[bin] +_itoa_fmts = { + 2: "{0:b}", + 10: "{0}", + 16: "{0:x}" +} + + _bcd2bin = ( 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 20, 21, 22, 23, 24, 25, diff --git a/py65/utils/hexdump.py b/py65/utils/hexdump.py deleted file mode 100644 index 02e032f..0000000 --- a/py65/utils/hexdump.py +++ /dev/null @@ -1,90 +0,0 @@ -from binascii import a2b_hex - - -def load(text): - load = Loader(text) - return (load.start_address, load.data) - - -class Loader: - def __init__(self, text): - self.load(text) - - def load(self, text): - self._reset() - - for line in text.splitlines(): - self._parse_line(line) - - def _reset(self): - self.data = [] - self.start_address = None - self.current_address = None - - def _parse_line(self, line): - line = self._remove_comments(line) - pieces = line.strip().split() - - for piece in pieces: - if piece.startswith('$'): - piece = piece[1:] - - if piece.endswith(':'): - self._parse_address(piece[:-1]) - - else: - self._parse_bytes(piece) - - def _remove_comments(self, line): - for delimiter in (';', '--', '#'): - pos = line.find(delimiter) - if pos != -1: - line = line[:pos] - return line - - def _parse_address(self, piece): - try: - binstr = a2b_hex(piece.encode('utf-8')) - if isinstance(binstr, str): - addr_bytes = [ ord(b) for b in binstr ] - else: # Python 3 - addr_bytes = [ b for b in binstr ] - except (TypeError, ValueError): - msg = "Could not parse address: %s" % piece - raise ValueError(msg) - - if len(addr_bytes) != 2: - msg = "Expected address to be 2 bytes, got %d" % ( - len(addr_bytes)) - raise ValueError(msg) - - address = (addr_bytes[0] << 8) + addr_bytes[1] - - if self.start_address is None: - self.start_address = address - self.current_address = address - - elif address != (self.current_address): - msg = "Non-contigous block detected. Expected next address " \ - "to be $%04x, label was $%04x" % (self.current_address, - address) - raise ValueError(msg) - - def _parse_bytes(self, piece): - if self.start_address is None: - msg = "Start address was not found in data" - raise ValueError(msg) - - else: - try: - binstr = a2b_hex(piece.encode('utf-8')) - if isinstance(binstr, str): - data_bytes = [ ord(b) for b in binstr ] - else: # Python 3 - data_bytes = [ b for b in binstr ] - except (TypeError, ValueError): - msg = "Could not parse data: %s" % piece - raise ValueError(msg) - - self.current_address += len(data_bytes) - self.data.extend(data_bytes) diff --git a/setup.py b/setup.py index fda84d2..9148fa5 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -__version__ = '0.25.dev0' +__version__ = '2.0.0.dev0' import sys @@ -6,11 +6,11 @@ py_version = sys.version_info[:2] PY3 = py_version[0] == 3 if PY3: - if py_version < (3, 2): - raise RuntimeError('On Python 3, Py65 requires Python 3.2 or later') + if py_version < (3, 4): + raise RuntimeError('On Python 3, Py65 requires Python 3.4 or later') else: - if py_version < (2, 6): - raise RuntimeError('On Python 2, Py65 requires Python 2.6 or later') + if py_version < (2, 7): + raise RuntimeError('On Python 2, Py65 requires Python 2.7 or later') from setuptools import setup, find_packages @@ -25,12 +25,12 @@ CLASSIFIERS = [ 'Operating System :: POSIX', 'Programming Language :: Python', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Assembly', 'Topic :: Software Development :: Assemblers', 'Topic :: Software Development :: Disassemblers', @@ -59,7 +59,6 @@ setup( tests_require=[], include_package_data=True, zip_safe=False, - namespace_packages=['py65'], test_suite="py65.tests", entry_points={ 'console_scripts': [ diff --git a/tox.ini b/tox.ini index 539b178..c75642d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py26,py27,py32,py33,py34,py35,pypy + py27,py34,py35,py36,py37,pypy [testenv] commands =