From 1870d65982015664eacf903d27221d0f1cc1c3e4 Mon Sep 17 00:00:00 2001 From: Mike Naberezny Date: Fri, 12 Apr 2024 12:24:29 -0700 Subject: [PATCH] Fix interactive assembly on Python 3 Closes #81 Closes #78 Closes #65 Closes #64 Closes #63 --- .github/workflows/main.yml | 20 +++++++-- CHANGES.rst | 2 + py65/assembler.py | 2 +- py65/compat.py | 21 +++++++++ py65/tests/end_to_end.py | 88 ++++++++++++++++++++++++++++++++++++++ py65/utils/console.py | 17 ++++---- 6 files changed, 137 insertions(+), 13 deletions(-) create mode 100644 py65/compat.py create mode 100644 py65/tests/end_to_end.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a92e8ec..0e486f7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,11 +21,14 @@ jobs: run: python -V - name: Install dependencies - run: $PIP install setuptools + run: $PIP install setuptools pexpect - name: Run the tests run: python setup.py test -q + - name: Run the end-to-end tests + run: END_TO_END=1 python setup.py test -q + build_py34: runs-on: ubuntu-20.04 container: python:3.4 @@ -44,11 +47,14 @@ jobs: run: python -V - name: Install dependencies - run: $PIP install setuptools + run: $PIP install setuptools pexpect - name: Run the tests run: python setup.py test -q + - name: Run the end-to-end tests + run: END_TO_END=1 python setup.py test -q + build_py35: runs-on: ubuntu-20.04 container: python:3.5 @@ -62,11 +68,14 @@ jobs: run: python -V - name: Install dependencies - run: $PIP install setuptools + run: $PIP install setuptools pexpect - name: Run the tests run: python setup.py test -q + - name: Run the end-to-end tests + run: END_TO_END=1 python setup.py test -q + build_py3x: strategy: fail-fast: false @@ -88,7 +97,10 @@ jobs: run: python -V - name: Install dependencies - run: $PIP install setuptools + run: $PIP install setuptools pexpect - name: Run the tests run: python setup.py test -q + + - name: Run the end-to-end tests + run: END_TO_END=1 python setup.py test -q diff --git a/CHANGES.rst b/CHANGES.rst index 29d0a84..564d52b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,8 @@ dropped when pasting in larger amounts of text. This makes it possible to paste programs into EhBASIC and Taliforth. Patch by SamCoVT. +- Fixed interactive assembly on Python 3. + - Fixed regular expression warnings on Python 3.12. - The ``fill`` command in the monitor now shows an error message if an diff --git a/py65/assembler.py b/py65/assembler.py index 0685dc8..67f18be 100644 --- a/py65/assembler.py +++ b/py65/assembler.py @@ -92,7 +92,7 @@ class Assembler: and parsing the address part using AddressParser. The result of the normalization is a tuple of two strings (opcode, operand). """ - statement = ' '.join(str.split(statement)) + statement = ' '.join(statement.split()) # normalize target in operand match = self.Statement.match(statement) diff --git a/py65/compat.py b/py65/compat.py new file mode 100644 index 0000000..87ec30d --- /dev/null +++ b/py65/compat.py @@ -0,0 +1,21 @@ +import sys + +PY2 = sys.version_info[0] == 2 + +if PY2: + unicode = unicode + + def as_string(s, encoding='utf-8'): + if isinstance(s, unicode): + return s + else: + return s.decode(encoding) + +else: + unicode = str + + def as_string(s, encoding='utf-8'): + if isinstance(s, str): + return s + else: + return s.decode(encoding) diff --git a/py65/tests/end_to_end.py b/py65/tests/end_to_end.py new file mode 100644 index 0000000..feed750 --- /dev/null +++ b/py65/tests/end_to_end.py @@ -0,0 +1,88 @@ +import os +import signal +import sys +import tempfile +import unittest + +from py65.compat import unicode + +# end-to-test tests are slow so only run them when asked +if 'END_TO_END' in os.environ: + if sys.platform == "win32": + raise NotImplementedError() + else: + import pexpect + BaseTestCase = unittest.TestCase +else: + BaseTestCase = object + + +class EndToEndTests(BaseTestCase): + + def _spawn(self): + mon = pexpect.spawn( + sys.executable, + ['-u', '-m', 'py65.monitor'], + encoding='utf-8' + ) + mon.expect_exact(unicode("Py65 Monitor")) + self.addCleanup(mon.kill, signal.SIGINT) + return mon + + def test_putc(self): + mon = self._spawn() + + mon.sendline(unicode("add_label f001 putc")) + + mon.sendline(unicode("a c000 lda #'H")) + mon.sendline(unicode("a c002 sta putc")) + mon.sendline(unicode("a c005 lda #'I")) + mon.sendline(unicode("a c007 sta putc")) + mon.sendline(unicode("a c00a brk")) + + mon.sendline(unicode("g c000")) + mon.expect_exact(unicode("HI")) + mon.sendline(unicode("q")) + + def test_getc(self): + mon = self._spawn() + + mon.sendline(unicode("add_label f004 getc")) + + mon.sendline(unicode("a c000 ldx #0")) + mon.sendline(unicode("a c002 lda getc")) + mon.sendline(unicode("a c005 beq c002")) + mon.sendline(unicode("a c007 cmp #'!")) + mon.sendline(unicode("a c009 bne c00c")) + mon.sendline(unicode("a c00b brk")) + mon.sendline(unicode("a c00c sta 1000,x")) + mon.sendline(unicode("a c00f inx")) + mon.sendline(unicode("a c010 jmp c002")) + + mon.sendline(unicode("g c000")) + mon.send(unicode("HELLO!")) + mon.expect_exact(unicode("6502:")) + mon.sendline(unicode("m 1000:1004")) + mon.expect_exact(unicode("48 45 4c 4c 4f")) + + def test_assemble_interactive(self): + mon = self._spawn() + + mon.sendline(unicode("assemble 0")) + mon.expect_exact(unicode("$0000")) + + mon.sendline(unicode("lda $1234")) + mon.expect_exact(unicode("ad 34 12")) + + mon.expect_exact(unicode("$0003")) + mon.sendline(unicode("sta $4567")) + mon.expect_exact(unicode("8d 67 45")) + + mon.sendline(unicode("invalid")) + mon.expect_exact(unicode("?Syntax")) + + mon.sendline() + mon.sendline(unicode("quit")) + +if __name__ == '__main__': + unittest.main() diff --git a/py65/utils/console.py b/py65/utils/console.py index cc9987f..91dd4a7 100644 --- a/py65/utils/console.py +++ b/py65/utils/console.py @@ -1,5 +1,7 @@ import sys +from py65.compat import as_string + if sys.platform[:3] == "win": import msvcrt @@ -24,10 +26,7 @@ if sys.platform[:3] == "win": is available. Does not echo the character. The stdin argument is for function signature compatibility and is ignored. """ - c = msvcrt.getch() - if isinstance(c, bytes): # Python 3 - c = c.decode('latin-1') - return c + return as_string(msvcrt.getch()) def getch_noblock(stdin): """ Read one character from the Windows console without blocking. @@ -36,8 +35,8 @@ if sys.platform[:3] == "win": available, an empty string is returned. """ if msvcrt.kbhit(): - return getch(stdin) - return '' + return as_string(getch(stdin)) + return u'' else: import termios @@ -157,7 +156,7 @@ else: # use select to make sure there is at least one char to read. rd,wr,er = select([stdin], [], [], 0.01) if rd != []: - char = stdin.read(1) + char = as_string(stdin.read(1)) except KeyboardInterrupt: # Pass along a CTRL-C interrupt. raise @@ -180,7 +179,7 @@ else: # use select to make sure there is at least one char to read. rd,wr,er = select([stdin], [], [], 0.01) if rd != []: - char = stdin.read(1) + char = as_string(stdin.read(1)) except KeyboardInterrupt: # Pass along a CTRL-C interrupt. raise @@ -200,6 +199,7 @@ def line_input(prompt='', stdin=sys.stdin, stdout=sys.stdout): is useful in modes like the interactive assembler. """ stdout.write(prompt) + stdout.flush() line = '' while True: char = getch(stdin) @@ -211,6 +211,7 @@ def line_input(prompt='', stdin=sys.stdin, stdout=sys.stdout): line = line[:-1] stdout.write("\r%s\r%s%s" % (' ' * (len(prompt + line) + 5), prompt, line)) + stdout.flush() elif code == 0x1b: # escape pass else: