diff --git a/.travis.yml b/.travis.yml index e846610..db65b1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,12 +17,11 @@ matrix: os: osx osx_image: xcode10.2 language: shell -before_install: -- make build install: -- pip3 install dist/*.whl +- make install script: - make lint +- make tests deploy: provider: pypi user: adahlberg diff --git a/Makefile b/Makefile index 79ef181..e6a5479 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ PYTHON = python3 PIP = pip3 CQC_DIR = cqc EXAMPLES = examples +TESTS = tests clean: _clear_pyc _clear_build @@ -9,15 +10,24 @@ _clear_pyc: @find . -name '*.pyc' -delete lint: - @${PYTHON} -m flake8 ${CQC_DIR} ${EXAMPLES} + @${PYTHON} -m flake8 ${CQC_DIR} ${EXAMPLES} ${TESTS} python-deps: - @cat requirements.txt | xargs -n 1 -L 1 $(PIP) install + @${PIP} install -r requirements.txt + +test-deps: + @${PIP} install -r test_requirements.txt _verified: @echo "CQC-Python is verified!" -verify: clean python-deps lint _verified +tests: + @${PYTHON} -m pytest ${TESTS} + +verify: clean python-deps lint tests _verified + +install: test-deps build + @${PIP} install dist/*whl _remove_build: @rm -f -r build @@ -35,4 +45,4 @@ _build: build: _clear_build _build -.PHONY: clean lint python-deps verify build +.PHONY: clean lint python-deps verify build tests diff --git a/cqc/pythonLib.py b/cqc/pythonLib.py index a4eee3e..e242175 100644 --- a/cqc/pythonLib.py +++ b/cqc/pythonLib.py @@ -674,7 +674,7 @@ def allocate_qubits(self, num_qubits, notify=True, block=True): return qubits - def release_qubits(self, qubits, notify=True, block=False, action=False): + def release_qubits(self, qubits, notify=True, block=True, action=False): """ Release qubits so backend can free them up for other uses :param qubits: a list of qubits to be released @@ -2339,7 +2339,7 @@ def reset(self, notify=True, block=True): message = self._cqc.readMessage() self._cqc.print_CQC_msg(message) - def release(self, notify=True, block=False): + def release(self, notify=True, block=True): """ Release the current qubit :param notify: Do we wish to be notified when done diff --git a/requirements.txt b/requirements.txt index aa63a16..ed6ab0e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy>=1.14.0 -bitstring>=3.1.5 -flake8>=3.6.0 -twisted>=18.7.0 +numpy>=1.14.0,<1.18.0 +bitstring>=3.1.5,<4.0.0 +twisted>=19.7.0,<20.0.0 +anytree>=2.7.2,<3.0.0 diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..682c6bb --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,2 @@ +flake8>=3.6.0,<4.0.0 +pytest>=5.2.1,<6.0.0 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1c62e4f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,102 @@ +import pytest +import inspect +import socket +from collections import namedtuple + +from cqc.pythonLib import CQCConnection + +Call = namedtuple("Call", ["name", "args", "kwargs"]) + + +def _spy_wrapper(method): + """Wraps a method to be able to spy on it""" + def new_method(self, *args, **kwargs): + if method.__name__ == '__init__': + self.calls = [] + call = Call(method.__name__, args, kwargs) + self.calls.append(call) + return method(self, *args, **kwargs) + + return new_method + + +def spy_on_class(cls): + """Spies on all calls to the methods of a class""" + for method_name, method in inspect.getmembers(cls, predicate=inspect.isfunction): + setattr(cls, method_name, _spy_wrapper(method)) + return cls + + +@spy_on_class +class MockSocket: + def __init__(self, *args, **kwargs): + pass + + def connect(self, *args, **kwargs): + pass + + def send(self, *args, **kwargs): + pass + + def recv(self, *args, **kwargs): + pass + + def close(self, *args, **kwargs): + pass + + +@pytest.fixture +def mock_socket(monkeypatch): + def get_mocked_socket(*args, **kwargs): + mock_socket = MockSocket(*args, **kwargs) + return mock_socket + + monkeypatch.setattr(socket, "socket", get_mocked_socket) + + +class MockedFirstMessage: + """Mocks the first header returned by CQCConnection.readMessage""" + class MockedTypeEntry: + def __eq__(self, other): + """This type will be equal to any integer.""" + return isinstance(other, int) + + @property + def tp(self): + return self.MockedTypeEntry() + + +class MockedOtherMessage: + """Mocks the second header returned by CQCConnection.readMessage""" + next_qubit_id = 0 + + @property + def qubit_id(self): + qid = self.next_qubit_id + self.next_qubit_id += 1 + return qid + + @property + def outcome(self): + return 0 + + @property + def datetime(self): + return 0 + + +@pytest.fixture +def mock_read_message(monkeypatch): + """Mock the readMessage, check_error and print_CQC_msg from CQCConnection when testing.""" + def mocked_readMessage(self): + return [MockedFirstMessage(), MockedOtherMessage()] + + def mocked_print_CQC_msg(self, message): + pass + + def mocked_check_error(self, hdr): + pass + + monkeypatch.setattr(CQCConnection, "readMessage", mocked_readMessage) + monkeypatch.setattr(CQCConnection, "print_CQC_msg", mocked_print_CQC_msg) + monkeypatch.setattr(CQCConnection, "check_error", mocked_check_error) diff --git a/tests/test_cqcconnection.py b/tests/test_cqcconnection.py new file mode 100644 index 0000000..9d5b657 --- /dev/null +++ b/tests/test_cqcconnection.py @@ -0,0 +1,79 @@ +import pytest + +from cqc.pythonLib import CQCConnection, qubit +from cqc.cqcHeader import CQCHeader, CQCCmdHeader, CQC_TP_COMMAND,\ + CQC_CMD_HDR_LENGTH, CQC_CMD_H, CQC_CMD_NEW, CQC_CMD_RELEASE + +from utilities import get_header + + +def get_expected_headers_simple_h(): + """What headers we expect""" + hdr_tp_cmd = get_header( + CQCHeader, + version=2, + tp=CQC_TP_COMMAND, + app_id=0, + length=CQC_CMD_HDR_LENGTH, + ) + hdr_cmd_new = get_header( + CQCCmdHeader, + qubit_id=0, + instr=CQC_CMD_NEW, + notify=True, + action=False, + block=True, + ) + hdr_cmd_h = get_header( + CQCCmdHeader, + qubit_id=0, + instr=CQC_CMD_H, + notify=True, + action=False, + block=True, + ) + hdr_cmd_release = get_header( + CQCCmdHeader, + qubit_id=0, + instr=CQC_CMD_RELEASE, + notify=True, + action=False, + block=True, + ) + + expected_headers = [ + hdr_tp_cmd, + hdr_cmd_new, + hdr_tp_cmd, + hdr_cmd_h, + hdr_tp_cmd + hdr_cmd_release, + ] + + return expected_headers + + +def commands_to_apply_simple_h(cqc): + """What to do with the CQCConnection""" + q = qubit(cqc) + q.H() + + +@pytest.mark.parametrize("commands_to_apply, get_expected_headers", [ + (commands_to_apply_simple_h, get_expected_headers_simple_h), +]) +def test_commands(commands_to_apply, get_expected_headers, monkeypatch, mock_socket, mock_read_message): + + with CQCConnection("Test", socket_address=('localhost', 8000), use_classical_communication=False) as cqc: + commands_to_apply(cqc) + + expected_headers = get_expected_headers() + + commands_sent = list(filter(lambda call: call.name == 'send', cqc._s.calls)) + assert len(expected_headers) == len(commands_sent) + for command, expected in zip(commands_sent, expected_headers): + print(command.args[0]) + print(expected) + print() + # Excluding None gives the opportunity to not specify all expected headers but still check the number of them + if expected is not None: + assert command.args[0] == expected diff --git a/tests/utilities.py b/tests/utilities.py new file mode 100644 index 0000000..1a331bc --- /dev/null +++ b/tests/utilities.py @@ -0,0 +1,5 @@ +def get_header(header_class, *args, **kwargs): + """Construct and packs a given header""" + hdr = header_class() + hdr.setVals(*args, **kwargs) + return hdr.pack()