diff --git a/dashmips/debugger.py b/dashmips/debugger.py index 1375cd8..44d9ccc 100644 --- a/dashmips/debugger.py +++ b/dashmips/debugger.py @@ -101,3 +101,8 @@ def checkfile(f: str) -> bool: local_breakpoints.append(-1) return remote_breakpoints, local_breakpoints + + +def debug_error(program: MipsProgram, params): + """Raise an error on the interpreter's end.""" + raise MipsException("".join(params)) diff --git a/dashmips/debuggerserver.py b/dashmips/debuggerserver.py index 4709c2b..ec9b374 100644 --- a/dashmips/debuggerserver.py +++ b/dashmips/debuggerserver.py @@ -6,24 +6,79 @@ import logging as log import signal import socketserver +from socket import socket +import re +from .utils import MipsException from .models import MipsProgram +HEADER_REGEX = re.compile(r'{\s*"size"\s*:\s*\d+\s*}') + + class ProgramExit(Exception): """Program exited normally.""" pass -def client_loop(message: str, commands: dict): +def receive_dashmips_message(client: socket) -> dict: + r"""Receive a dashmips debugger message. + + Dashmips communicates in a modified JSON-RPC format. + An example message looks like (in regex form): + {"size": \d+}{"method": "\w+"} + Two concatenated JSON objects, the first reporting the size of the second object. + """ + message = client.recv(30).decode("utf8") # { "size": 9007199254740991 } <- largest message with some padding + + if message == "": + # Client is disconnected! maybe we should just exit? + return {"method": "stop"} + + header_search = HEADER_REGEX.match(message) + if header_search: + header = header_search[0] + else: + raise MipsException(f"Header not included in message: {message}") + + msg_size = json.loads(header)["size"] # must be valid json {size: \d+} + command = message[len(header) :] + + remaining_bytes = msg_size - len(command) + if remaining_bytes > 0: + command += client.recv(remaining_bytes).decode("utf8") + + return json.loads(command) + + +def send_dashmips_message(client: socket, data: str): + r"""Send a dashmips debugger message. + + Size is calculated from the utf8 encoding of data. + """ + data_encoded = bytes(data, "utf8") + size = len(data_encoded) + size_header = bytes(json.dumps({"size": size}), "utf8") + out_message = size_header + data_encoded + client.sendall(out_message) + + +def run_method(request: dict, commands: dict): """Message loop handler.""" - log.info(f"Recv `{message}`") + log.info(f"Recv `{request}`") + + if "method" not in request: + raise MipsException("Must specify 'method' in debug messages.") + + method = request["method"] + params = request.get("params", []) - request = json.loads(message) + if method not in commands: + raise MipsException(f"Unsupported method '{method}'.") - command = commands[request["method"]] - result = command(params=request["params"]) + command = commands[method] + result = command(params=params) response = json.dumps({"method": request["method"], "result": result}) @@ -65,28 +120,20 @@ def setup(self): def handle(self): # self.request is the TCP socket connected to the client - while True: - header = b"" - while True: - header += self.request.recv(1) - if header and chr(header[-1]) == "}": - break - if len(header) >= 1000: - log.error("Communication error between client and server") - break + client: socket = self.request - msg_size = int(header[8:-1]) - command = self.request.recv(msg_size) + while True: # Enter the loop that will continuously chat with the debugger client + command = receive_dashmips_message(client) log.info(f"{self.client_address[0]} wrote: {command}") try: - response = client_loop(command, self.commands) + response = run_method(command, self.commands) except ProgramExit: log.info("Program exited normally") break - self.request.sendall(bytes(json.dumps({"size": len(response)}), "ascii") + bytes(response, "ascii")) + send_dashmips_message(client, response) # Allows server to reuse address to prevent crash socketserver.TCPServer.allow_reuse_address = True diff --git a/poetry.lock b/poetry.lock index bb91253..107fac9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -265,7 +265,18 @@ description = "Alternative regular expression module, to replace re." name = "regex" optional = false python-versions = "*" -version = "2020.6.8" +version = "2020.7.14" + +[[package]] +category = "dev" +description = "a python refactoring library..." +name = "rope" +optional = false +python-versions = "*" +version = "0.17.0" + +[package.extras] +dev = ["pytest"] [[package]] category = "dev" @@ -327,7 +338,8 @@ version = "0.34.2" test = ["pytest (>=3.0.0)", "pytest-cov"] [metadata] -content-hash = "bba2d28b86400b6c05037f830f3884bdc65520da5a43df21b6667ccc71a867d2" +content-hash = "5040c247568668d625ddf2f247a4f617205474f4adfa33b4727ece035b0de9d1" +lock-version = "1.0" python-versions = "^3.8" [metadata.files] @@ -429,27 +441,30 @@ pytest-pydocstyle = [ {file = "pytest-pydocstyle-2.1.3.tar.gz", hash = "sha256:fa3332df8387f6d7756649cd4c5c873ccde14bbd2832b84d3ff19a4bd6c908c7"}, ] regex = [ - {file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"}, - {file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"}, - {file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"}, - {file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"}, - {file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"}, - {file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"}, - {file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"}, - {file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"}, - {file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"}, + {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, + {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, + {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, + {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, + {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, + {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, + {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, + {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, + {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, +] +rope = [ + {file = "rope-0.17.0.tar.gz", hash = "sha256:658ad6705f43dcf3d6df379da9486529cf30e02d9ea14c5682aa80eb33b649e1"}, ] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, diff --git a/poetry.toml b/poetry.toml index 99efa6f..0ed866b 100644 --- a/poetry.toml +++ b/poetry.toml @@ -1,3 +1,4 @@ [virtualenvs] create = true in-project = true +path = "./.venv" diff --git a/pyproject.toml b/pyproject.toml index 3ae399d..c65f464 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,47 +1,48 @@ -[tool.poetry] -name = "dashmips" -version = "0.1.6" -description = "Mips Interpreter" -authors = ["Neal Beeken "] -license = "MIT" -readme = "README.md" -repository = "https://github.com/nbbeeken/dashmips" -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Education", - "Topic :: Software Development :: Assemblers", - "Programming Language :: Assembly", - "License :: OSI Approved :: MIT License" -] - -[tool.poetry.scripts] -dashmips = "dashmips.__main__:main" - -[tool.poetry.dependencies] -"python" = "^3.8" -"dataclasses" = { version = "^0.6", python = "3.6" } -"typing-extensions" = "3.7.4.2" - -[tool.poetry.dev-dependencies] -"pip" = "^20.1.1" -"wheel" = "^0.34.2" -"mypy" = "^0.782" -"mypy-extensions" = "^0.4.1" -"pycodestyle" = "^2.5.0" -"pydocstyle" = "^5.0.2" -"black" = "^19.10b0" -"pytest" = "^5.0.1" -"pytest-pycodestyle" = "^2.0.1" -"pytest-pydocstyle" = "^2.0.1" -"pytest-mypy" = "^0.6.2" -"pytest-black" = "^0.3.9" - -[tool.black] -"line-length" = 160 - -[metadata] -license_files = "LICENSE" - -[build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" +[tool.poetry] +name = "dashmips" +version = "0.1.6" +description = "Mips Interpreter" +authors = ["Neal Beeken "] +license = "MIT" +readme = "README.md" +repository = "https://github.com/nbbeeken/dashmips" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Education", + "Topic :: Software Development :: Assemblers", + "Programming Language :: Assembly", + "License :: OSI Approved :: MIT License" +] + +[tool.poetry.scripts] +dashmips = "dashmips.__main__:main" + +[tool.poetry.dependencies] +"python" = "^3.8" +"dataclasses" = { version = "^0.6", python = "3.6" } +"typing-extensions" = "3.7.4.2" + +[tool.poetry.dev-dependencies] +"pip" = "^20.1.1" +"wheel" = "^0.34.2" +"mypy" = "^0.782" +"mypy-extensions" = "^0.4.1" +"pycodestyle" = "^2.5.0" +"pydocstyle" = "^5.0.2" +"black" = "^19.10b0" +"pytest" = "^5.0.1" +"pytest-pycodestyle" = "^2.0.1" +"pytest-pydocstyle" = "^2.0.1" +"pytest-mypy" = "^0.6.2" +"pytest-black" = "^0.3.9" +"rope" = "^0.17.0" + +[tool.black] +"line-length" = 160 + +[metadata] +license_files = "LICENSE" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/setup.cfg b/setup.cfg index 83effd4..31e7e5a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ docstyle_ignore = D202,D213 [pycodestyle] max-line-length = 160 -ignore = E251,E701,E203,E731 +ignore = E251,E701,E203,E731,E203 statistics = True [pydocstyle] diff --git a/tests/test_debugger/debugserver_tests.py b/tests/test_debugger/debugserver_tests.py deleted file mode 100644 index 7ee196c..0000000 --- a/tests/test_debugger/debugserver_tests.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Tests for Mips Debug Server.""" -from typing import Dict, Any, cast - -import json -import unittest -from pprint import pformat -from shlex import split -from socket import socket -from subprocess import PIPE, Popen, run -from time import sleep - -SERVER = Popen(split("python -m dashmips debug -l")) -sleep(0.1) # Should be plenty of time to start and bind - - -def compile_file(filename: str) -> Dict[str, Any]: - """Run compiler on filename.""" - proc = run(split(f"python -m dashmips c {filename} -j"), capture_output=True, encoding="utf8") - return cast(Dict[str, Any], json.loads(proc.stdout.strip())) - - -def communicate(msg: Dict[str, Any]) -> Any: - """Send json encoded message.""" - s = socket() - s.connect(("localhost", 9999)) - wfile = s.makefile("w", encoding="utf8", buffering=1) - rfile = s.makefile("r", encoding="utf8", buffering=1) - wfile.write(json.dumps(msg) + "\r\n") - wfile.flush() - received = rfile.readline().strip() - resp = json.loads(received) - s.close() - return resp - - -def recv() -> Dict[str, Any]: - """Recv json encoded message.""" - s = socket() - s.connect(("localhost", 9999)) - rfile = s.makefile("r", encoding="utf8", buffering=1) - received = rfile.readline().strip() - resp = json.loads(received) - s.close() - return cast(Dict[str, Any], resp) - - -class TestMipsDebugServer(unittest.TestCase): - """Testing for mips debug server.""" - - def test_start(self): - """Test start command.""" - program = compile_file("test.mips") - - resp = communicate({"command": "start", "program": program}) - - program = resp["program"] - - self.assertIn("command", resp, pformat(resp)) - self.assertIn("program", resp, pformat(resp)) - self.assertEqual(resp["command"], "start", pformat(resp)) - self.assertEqual(program["labels"]["main"]["value"], program["registers"]["pc"], pformat(resp)) - self.assertLessEqual(program["registers"]["pc"], len(program["source"]), pformat(resp)) - self.stop_debugging() - - def stop_debugging(self): - """Send a proper stop command to the debugger.""" - SERVER.kill() - - -if __name__ == "__main__": - try: - unittest.main() - finally: - pass - SERVER.kill() diff --git a/tests/test_debugger/test_networking.py b/tests/test_debugger/test_networking.py new file mode 100644 index 0000000..7ce7347 --- /dev/null +++ b/tests/test_debugger/test_networking.py @@ -0,0 +1,54 @@ +"""Tests for the socket server.""" + +from typing import Optional +from subprocess import PIPE, Popen, run +from shlex import split +import time +import pytest +import socket as net +import json + +from dashmips.debuggerserver import send_dashmips_message, receive_dashmips_message + +SHOULD_START_SERVER = True +ADDRESS = ("localhost", 2390) + + +@pytest.fixture() +def server(): + """Start dashmips server.""" + if SHOULD_START_SERVER: + debugger = Popen(split("dashmips debug -i localhost -l tests/test_mips/smallest.mips")) + assert debugger.returncode is None, "dashmips exited before we could test" + time.sleep(0.2) # sleep so we can connect + return debugger + return None + + +def prepend_header(message: str) -> bytes: + """Prepends dashmips protocol header.""" + return bytes(json.dumps({"size": len(bytes(message, "utf8"))}) + message, "utf8") + + +def test_connect(server): + """Test that the socket is available for connecting.""" + if server: + assert server.returncode is None, "Dashmips Exited before we could test!!" + s = net.create_connection(ADDRESS) + assert s is not None + + +def test_handshake(server): + """Test that the socket is available for connecting.""" + s = net.create_connection(ADDRESS) + assert s is not None + + send_dashmips_message(s, '{"method": "start"}') + response = receive_dashmips_message(s) + + assert "result" in response + assert "pid" in response["result"] + if server: + assert server.pid == response["result"]["pid"] + + send_dashmips_message(s, json.dumps({"method": "stop"})) diff --git a/tests/test_mips/args.mips b/tests/test_mips/args.mips index 9bbf19e..50445f7 100644 --- a/tests/test_mips/args.mips +++ b/tests/test_mips/args.mips @@ -77,4 +77,5 @@ main: # main($a0 = argc, $a1 = argv) j loop end_loop: li $v0, 10 + move $a0, $zero syscall diff --git a/tests/test_mips/smallest.mips b/tests/test_mips/smallest.mips new file mode 100644 index 0000000..3c39bd1 --- /dev/null +++ b/tests/test_mips/smallest.mips @@ -0,0 +1,5 @@ +.text +main: + li $a0, 0 # set exit code + li $v0, 10 + syscall diff --git a/tests/test_mips/test_mips.py b/tests/test_mips/test_mips.py new file mode 100644 index 0000000..c2f1f2e --- /dev/null +++ b/tests/test_mips/test_mips.py @@ -0,0 +1,22 @@ +"""Integration Tests.""" + +import os +from subprocess import run, PIPE +from shlex import split + + +TEST_MIPS_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) + + +def run_mips_file(filename: str) -> str: + """Run mips file.""" + proc = run(split(f"python -m dashmips run {TEST_MIPS_DIRECTORY}/{filename}"), stdout=PIPE, stderr=PIPE, encoding="utf8") + assert proc.stderr == "" + assert proc.returncode == 0 + return proc.stdout + + +def test_args_mips(): + """Test args.mips.""" + stdout = run_mips_file("args.mips") + assert "argc" in stdout