diff --git a/.github/workflows/manual-run.yml b/.github/workflows/manual-run.yml index 3e60c80..07903b7 100644 --- a/.github/workflows/manual-run.yml +++ b/.github/workflows/manual-run.yml @@ -8,10 +8,10 @@ on: workflow_dispatch jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.9] + python-version: [3.8, 3.9] steps: - name: Send building notification @@ -34,10 +34,12 @@ jobs: - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names - flake8 test/ notecard/ examples/ --count --ignore=E722,F401,F403,W503 --show-source --statistics + make flake8 + # flake8 test/ notecard/ examples/ --count --ignore=E722,F401,F403,W503,E501 --show-source --statistics - name: Lint Docs with Pydocstyle run: | - pydocstyle notecard/ examples/ + make docstyle + # pydocstyle notecard/ examples/ - name: Send running tests notification run: | curl --request POST \ @@ -47,7 +49,7 @@ jobs: --data '{"req":"note.add","file":"build_results.qi","body":{"result":"running_tests"}}' - name: Test with pytest run: | - pytest + make test - name: Check if the job has succeeded if: ${{ success() }} run: | diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9985ddf..93652de 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -12,7 +12,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: python-version: [3.6, 3.7, 3.8, 3.9] @@ -38,10 +38,10 @@ jobs: - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names - flake8 test/ notecard/ examples/ --count --ignore=E722,F401,F403,W503 --show-source --statistics + make flake8 - name: Lint Docs with Pydocstyle run: | - pydocstyle notecard/ examples/ + make docstyle - name: Send running tests notification run: | curl --request POST \ diff --git a/Makefile b/Makefile index 9c2c2d3..47dcc30 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,40 @@ +# define VENV_NAME to use a specific virtual environment. It defaults to `env`. VENV_NAME?=env VENV_ACTIVATE=. $(VENV_NAME)/bin/activate -PYTHON=${VENV_NAME}/bin/python3 +PYTHON=python +VENV = -default: test +# check if the VENV file exists +ifneq ("$(wildcard $(PVENV_ACTIVATE))","") + VENV = venv + PYTHON = ${VENV_NAME}/bin/python3 +endif + +default: docstyle flake8 test venv: $(VENV_NAME)/bin/activate -test: venv - ${PYTHON} -m pydocstyle notecard/ examples/ - ${PYTHON} -m flake8 test/ notecard/ examples/ --count --ignore=E722,F401,F403,W503 --show-source --statistics +test: $(VENV) ${PYTHON} -m pytest test --cov=notecard -coverage: venv +docstyle: $(VENV) + ${PYTHON} -m pydocstyle notecard/ examples/ + +flake8: $(VENV) + # E722 Do not use bare except, specify exception instead https://www.flake8rules.com/rules/E722.html + # F401 Module imported but unused https://www.flake8rules.com/rules/F401.html + # F403 'from module import *' used; unable to detect undefined names https://www.flake8rules.com/rules/F403.html + # W503 Line break occurred before a binary operator https://www.flake8rules.com/rules/W503.html + # E501 Line too long (>79 characters) https://www.flake8rules.com/rules/E501.html + ${PYTHON} -m flake8 test/ notecard/ examples/ --count --ignore=E722,F401,F403,W503,E501 --show-source --statistics + +coverage: $(VENV) ${PYTHON} -m pytest test --doctest-modules --junitxml=junit/test-results.xml --cov=notecard --cov-report=xml --cov-report=html -run_build: +run_build: $(VENV) ${PYTHON} -m setup sdist bdist_wheel -deploy: +deploy: $(VENV) ${PYTHON} -m twine upload -r "pypi" --config-file .pypirc 'dist/*' + +.PHONY: venv test coverage run_build deploy diff --git a/test/test_notecard.py b/test/test_notecard.py index cb1c2d1..3837b6a 100644 --- a/test/test_notecard.py +++ b/test/test_notecard.py @@ -1,13 +1,11 @@ import os import sys -import serial -import periphery import pytest - from unittest.mock import Mock, MagicMock, patch +import periphery sys.path.insert(0, os.path.abspath( - os.path.join(os.path.dirname(__file__), '..'))) + os.path.join(os.path.dirname(__file__), '..'))) import notecard # noqa: E402 from notecard import card, hub, note, env, file # noqa: E402 @@ -34,449 +32,385 @@ def get_i2c_and_port(): return (nCard, port) -def test_get_user_agent(): - nCard, _ = get_serial_and_port() - userAgent = nCard.GetUserAgent() - - assert userAgent['agent'] == 'note-python' - assert userAgent['os_name'] is not None - assert userAgent['os_platform'] is not None - assert userAgent['os_version'] is not None - assert userAgent['os_family'] is not None - - -def test_user_agent_is_i2c_when_i2c_used(): - periphery = Mock() # noqa: F811 - port = periphery.I2C("dev/i2c-foo") - port.try_lock.return_value = True - - nCard = notecard.OpenI2C(port, 0x17, 255) - - userAgent = nCard.GetUserAgent() - - assert userAgent['req_interface'] == 'i2c' - assert userAgent['req_port'] is not None - - -def test_user_agent_is_serial_when_serial_used(): - nCard, _ = get_serial_and_port() - userAgent = nCard.GetUserAgent() - - assert userAgent['req_interface'] == 'serial' - assert userAgent['req_port'] is not None - - -def test_open_serial(): - nCard, _ = get_serial_and_port() - - assert nCard.uart is not None - - -def test_open_i2c(): - nCard, _ = get_i2c_and_port() - - assert nCard.i2c is not None - - -def test_transaction(): - nCard, port = get_serial_and_port() - - port.readline.return_value = "{\"connected\":true}\r\n" - response = nCard.Transaction({"req": "hub.status"}) - - assert "connected" in response - assert response["connected"] is True - - -def test_command(): - nCard, port = get_serial_and_port() - - response = nCard.Command({"cmd": "card.sleep"}) - - assert response is None - - -def test_command_fail_if_req(): - nCard, port = get_serial_and_port() - - with pytest.raises(Exception, match="Please use 'cmd' instead of 'req'"): - nCard.Command({"req": "card.sleep"}) - - -def test_hub_set(): - nCard, port = get_serial_and_port() - - port.readline.return_value = "{}\r\n" - response = hub.set(nCard, product="com.blues.tester", - sn="foo", - mode="continuous", - outbound=2, - inbound=60, - duration=5, - sync=True, - align=True, - voutbound="2.3", - vinbound="3.3", - host="http://hub.blues.foo") +class NotecardTest: - assert response == {} + def test_get_user_agent(self): + nCard, _ = self.get_port() + userAgent = nCard.GetUserAgent() + assert userAgent['agent'] == 'note-python' + assert userAgent['os_name'] is not None + assert userAgent['os_platform'] is not None + assert userAgent['os_version'] is not None + assert userAgent['os_family'] is not None -def test_user_agent_sent_is_false_before_hub_set(): - nCard, _ = get_serial_and_port() + def test_transaction(self): + nCard, port = self.get_port("{\"connected\":true}\r\n") - assert nCard.UserAgentSent() is False + response = nCard.Transaction({"req": "hub.status"}) + assert "connected" in response + assert response["connected"] is True -def test_send_user_agent_in_hub_set_helper(): - nCard, port = get_serial_and_port() + def test_command(self): + nCard, port = self.get_port() - port.readline.return_value = "{}\r\n" - hub.set(nCard, product="com.blues.tester", - sn="foo", - mode="continuous", - outbound=2, - inbound=60, - duration=5, - sync=True, - align=True, - voutbound="2.3", - vinbound="3.3", - host="http://hub.blues.foo") + response = nCard.Command({"cmd": "card.sleep"}) - assert nCard.UserAgentSent() is True + assert response is None + def test_command_fail_if_req(self): + nCard, port = self.get_port() -def test_send_user_agent_in_hub_set_transaction(): - nCard, port = get_serial_and_port() + with pytest.raises(Exception, match="Please use 'cmd' instead of 'req'"): + nCard.Command({"req": "card.sleep"}) - port.readline.return_value = "{\"connected\":true}\r\n" - nCard.Transaction({"req": "hub.set"}) + def test_hub_set(self): + nCard, port = self.get_port("{}\r\n") - assert nCard.UserAgentSent() is True + response = hub.set(nCard, product="com.blues.tester", + sn="foo", + mode="continuous", + outbound=2, + inbound=60, + duration=5, + sync=True, + align=True, + voutbound="2.3", + vinbound="3.3", + host="http://hub.blues.foo") + assert response == {} -def test_hub_set_invalid_card(): - with pytest.raises(Exception, match="Notecard object required"): - hub.set(None, product="com.blues.tester") + def test_user_agent_sent_is_false_before_hub_set(self): + nCard, _ = self.get_port() + assert nCard.UserAgentSent() is False -def test_hub_sync(): - nCard, port = get_serial_and_port() + def test_send_user_agent_in_hub_set_helper(self): + nCard, port = self.get_port("{}\r\n") - port.readline.return_value = "{}\r\n" - response = hub.sync(nCard) + hub.set(nCard, product="com.blues.tester", + sn="foo", + mode="continuous", + outbound=2, + inbound=60, + duration=5, + sync=True, + align=True, + voutbound="2.3", + vinbound="3.3", + host="http://hub.blues.foo") - assert response == {} + assert nCard.UserAgentSent() is True + def test_send_user_agent_in_hub_set_transaction(self): + nCard, port = self.get_port("{\"connected\":true}\r\n") -def test_hub_sync_status(): - nCard, port = get_serial_and_port() + nCard.Transaction({"req": "hub.set"}) - port.readline.return_value = "{\"status\":\"connected\"}\r\n" + assert nCard.UserAgentSent() is True - response = hub.syncStatus(nCard, True) + def test_hub_set_invalid_card(self): + with pytest.raises(Exception, match="Notecard object required"): + hub.set(None, product="com.blues.tester") - assert "status" in response - assert response["status"] == "connected" + def test_hub_sync(self): + nCard, port = self.get_port("{}\r\n") + response = hub.sync(nCard) -def test_hub_status(): - nCard, port = get_serial_and_port() + assert response == {} - port.readline.return_value = "{\"connected\":true}\r\n" + def test_hub_sync_status(self): + nCard, port = self.get_port("{\"status\":\"connected\"}\r\n") - response = hub.status(nCard) + response = hub.syncStatus(nCard, True) - assert "connected" in response - assert response["connected"] is True + assert "status" in response + assert response["status"] == "connected" + def test_hub_status(self): + nCard, port = self.get_port("{\"connected\":true}\r\n") -def test_hub_log(): - nCard, port = get_serial_and_port() + response = hub.status(nCard) - port.readline.return_value = "{}\r\n" + assert "connected" in response + assert response["connected"] is True - response = hub.log(nCard, "there's been an issue!", False) + def test_hub_log(self): + nCard, port = self.get_port("{}\r\n") - assert response == {} + response = hub.log(nCard, "there's been an issue!", False) + assert response == {} -def test_hub_get(): - nCard, port = get_serial_and_port() + def test_hub_get(self): + nCard, port = self.get_port("{\"mode\":\"continuous\"}\r\n") - port.readline.return_value = "{\"mode\":\"continuous\"}\r\n" + response = hub.get(nCard) - response = hub.get(nCard) + assert "mode" in response + assert response["mode"] == "continuous" - assert "mode" in response - assert response["mode"] == "continuous" + def test_card_time(self): + nCard, port = self.get_port("{\"time\":1592490375}\r\n") + response = card.time(nCard) -def test_card_time(): - nCard, port = get_serial_and_port() + assert "time" in response + assert response["time"] == 1592490375 - port.readline.return_value = "{\"time\":1592490375}\r\n" + def test_card_status(self): + nCard, port = self.get_port("{\"usb\":true,\"status\":\"{normal}\"}\r\n") - response = card.time(nCard) + response = card.status(nCard) - assert "time" in response - assert response["time"] == 1592490375 + assert "status" in response + assert response["status"] == "{normal}" + def test_card_temp(self): + nCard, port = self.get_port("{\"value\":33.625,\"calibration\":-3.0}\r\n") -def test_card_status(): - nCard, port = get_serial_and_port() + response = card.temp(nCard, minutes=20) - port.readline.return_value = "{\"usb\":true,\"status\":\"{normal}\"}\r\n" + assert "value" in response + assert response["value"] == 33.625 - response = card.status(nCard) + def test_card_attn(self): + nCard, port = self.get_port("{\"set\":true}\r\n") - assert "status" in response - assert response["status"] == "{normal}" + response = card.attn(nCard, mode="arm, files", + files=["sensors.qo"], + seconds=10, payload={"foo": "bar"}, + start=True) + assert "set" in response + assert response["set"] is True -def test_card_temp(): - nCard, port = get_serial_and_port() + def test_card_attn_with_invalid_card(self): + with pytest.raises(Exception, match="Notecard object required"): + card.attn(None, mode="arm") - port.readline.return_value = "{\"value\":33.625,\"calibration\":-3.0}\r\n" + def test_card_voltage(self): + nCard, port = self.get_port("{\"hours\":707}\r\n") - response = card.temp(nCard, minutes=20) + response = card.voltage(nCard, hours=24, offset=5, vmax=4, vmin=3) - assert "value" in response - assert response["value"] == 33.625 + assert "hours" in response + assert response["hours"] == 707 + def test_card_wireless(self): + nCard, port = self.get_port("{\"status\":\"{modem-off}\",\"count\":1}\r\n") -def test_card_attn(): - nCard, port = get_serial_and_port() + response = card.wireless(nCard, mode="auto", apn="-") - port.readline.return_value = "{\"set\":true}\r\n" + assert "status" in response + assert response["status"] == "{modem-off}" - response = card.attn(nCard, mode="arm, files", - files=["sensors.qo"], - seconds=10, payload={"foo": "bar"}, - start=True) + def test_card_version(self): + nCard, port = self.get_port("{\"version\":\"notecard-1.2.3.9950\"}\r\n") - assert "set" in response - assert response["set"] is True + response = card.version(nCard) + assert "version" in response + assert response["version"] == "notecard-1.2.3.9950" -def test_card_attn_with_invalid_card(): - with pytest.raises(Exception, match="Notecard object required"): - card.attn(None, mode="arm") + def test_note_add(self): + nCard, port = self.get_port("{\"total\":1}\r\n") + response = note.add(nCard, file="sensors.qo", + body={"temp": 72.22}, + payload="b64==", + sync=True) -def test_card_voltage(): - nCard, port = get_serial_and_port() + assert "total" in response + assert response["total"] == 1 - port.readline.return_value = "{\"hours\":707}\r\n" + def test_note_get(self): + nCard, port = self.get_port("{\"note\":\"s\",\"body\":{\"s\":\"foo\"}}\r\n") - response = card.voltage(nCard, hours=24, offset=5, vmax=4, vmin=3) + response = note.get(nCard, file="settings.db", + note_id="s", + delete=True, + deleted=False) - assert "hours" in response - assert response["hours"] == 707 + assert "note" in response + assert response["note"] == "s" + def test_note_delete(self): + nCard, port = self.get_port("{}\r\n") -def test_card_wireless(): - nCard, port = get_serial_and_port() + response = note.delete(nCard, file="settings.db", note_id="s") - port.readline.return_value = "{\"status\":\"{modem-off}\",\"count\":1}\r\n" + assert response == {} - response = card.wireless(nCard, mode="auto", apn="-") + def test_note_update(self): + nCard, port = self.get_port("{}\r\n") - assert "status" in response - assert response["status"] == "{modem-off}" + response = note.update(nCard, file="settings.db", note_id="s", + body={"foo": "bar"}, payload="123dfb==") + assert response == {} -def test_card_version(): - nCard, port = get_serial_and_port() + def test_note_changes(self): + nCard, port = self.get_port("{\"changes\":5,\"total\":15}\r\n") - port.readline.return_value = "{\"version\":\"notecard-1.2.3.9950\"}\r\n" + response = note.changes(nCard, file="sensors.qo", + tracker="123", + maximum=10, + start=True, + stop=False, + deleted=False, + delete=True) - response = card.version(nCard) + assert "changes" in response + assert response["changes"] == 5 - assert "version" in response - assert response["version"] == "notecard-1.2.3.9950" + def test_note_template(self): + nCard, port = self.get_port("{\"bytes\":40}\r\n") + response = note.template(nCard, file="sensors.qo", + body={"temp": 1.1, "hu": 1}, + length=5) -def test_note_add(): - nCard, port = get_serial_and_port() + assert "bytes" in response + assert response["bytes"] == 40 - port.readline.return_value = "{\"total\":1}\r\n" + def test_env_default(self): + nCard, port = self.get_port("{}\r\n") - response = note.add(nCard, file="sensors.qo", - body={"temp": 72.22}, - payload="b64==", - sync=True) + response = env.default(nCard, name="pump", text="on") - assert "total" in response - assert response["total"] == 1 + assert response == {} + def test_env_set(self): + nCard, port = self.get_port("{}\r\n") -def test_note_get(): - nCard, port = get_serial_and_port() - - port.readline.return_value = \ - "{\"note\":\"s\",\"body\":{\"s\":\"foo\"}}\r\n" - - response = note.get(nCard, file="settings.db", - note_id="s", - delete=True, - deleted=False) - - assert "note" in response - assert response["note"] == "s" - - -def test_note_delete(): - nCard, port = get_serial_and_port() - - port.readline.return_value = "{}\r\n" - - response = note.delete(nCard, file="settings.db", note_id="s") - - assert response == {} - - -def test_note_update(): - nCard, port = get_serial_and_port() - - port.readline.return_value = "{}\r\n" - - response = note.update(nCard, file="settings.db", note_id="s", - body={"foo": "bar"}, payload="123dfb==") - - assert response == {} - - -def test_note_changes(): - nCard, port = get_serial_and_port() - - port.readline.return_value = "{\"changes\":5,\"total\":15}\r\n" - - response = note.changes(nCard, file="sensors.qo", - tracker="123", - maximum=10, - start=True, - stop=False, - deleted=False, - delete=True) - - assert "changes" in response - assert response["changes"] == 5 - - -def test_note_template(): - nCard, port = get_serial_and_port() - - port.readline.return_value = "{\"bytes\":40}\r\n" - - response = note.template(nCard, file="sensors.qo", - body={"temp": 1.1, "hu": 1}, - length=5) - - assert "bytes" in response - assert response["bytes"] == 40 - - -def test_debug_mode_on_serial(): - serial = Mock() # noqa: F811 - port = serial.Serial("/dev/tty.foo", 9600) - port.read.side_effect = [b'\r', b'\n', None] - - nCard = notecard.OpenSerial(port, debug=True) - - assert nCard._debug - - -def test_debug_mode_on_i2c(): - periphery = Mock() # noqa: F811 - port = periphery.I2C("dev/i2c-foo") - port.try_lock.return_value = True - - nCard = notecard.OpenI2C(port, 0x17, 255, debug=True) + response = env.set(nCard, name="pump", text="on") - assert nCard._debug + assert response == {} + def test_env_get(self): + nCard, port = self.get_port("{}\r\n") -def test_env_default(): - nCard, port = get_serial_and_port() + response = env.get(nCard, name="pump") - port.readline.return_value = "{}\r\n" + assert response == {} - response = env.default(nCard, name="pump", text="on") + def test_env_modified(self): + nCard, port = self.get_port("{\"time\": 1605814493}\r\n") - assert response == {} + response = env.modified(nCard) + assert "time" in response + assert response["time"] == 1605814493 -def test_env_set(): - nCard, port = get_serial_and_port() + def test_file_delete(self): + nCard, port = self.get_port("{}\r\n") - port.readline.return_value = "{}\r\n" + response = file.delete(nCard, files=["sensors.qo"]) - response = env.set(nCard, name="pump", text="on") + assert response == {} - assert response == {} + def test_file_changes(self): + nCard, port = self.get_port("{\"total\":5}\r\n") + response = file.changes(nCard, tracker="123", files=["sensors.qo"]) -def test_env_get(): - nCard, port = get_serial_and_port() + assert "total" in response + assert response["total"] == 5 - port.readline.return_value = "{}\r\n" + def test_file_stats(self): + nCard, port = self.get_port("{\"total\":24}\r\n") - response = env.get(nCard, name="pump") + response = file.stats(nCard) - assert response == {} + assert "total" in response + assert response["total"] == 24 + def test_file_pendingChanges(self): + nCard, port = self.get_port("{\"changes\":1}\r\n") -def test_env_modified(): - nCard, port = get_serial_and_port() + response = file.pendingChanges(nCard) - port.readline.return_value = "{\"time\": 1605814493}\r\n" + assert "changes" in response + assert response["changes"] == 1 - response = env.modified(nCard) + def get_port(self, response=None): + raise NotImplementedError("subclasses must implement `get_port()`") - assert "time" in response - assert response["time"] == 1605814493 +class TestNotecardMockSerial(NotecardTest): + def get_port(self, response=None): + nCard, port = get_serial_and_port() + if response is not None: + port.readline.return_value = response + return (nCard, port) -def test_file_delete(): - nCard, port = get_serial_and_port() + def test_user_agent_is_serial_when_serial_used(self): + nCard, _ = self.get_port() + userAgent = nCard.GetUserAgent() - port.readline.return_value = "{}\r\n" + assert userAgent['req_interface'] == 'serial' + assert userAgent['req_port'] is not None - response = file.delete(nCard, files=["sensors.qo"]) + def test_open_serial(self): + nCard, _ = get_serial_and_port() - assert response == {} + assert nCard.uart is not None + def test_debug_mode_on_serial(self): + serial = Mock() # noqa: F811 + port = serial.Serial("/dev/tty.foo", 9600) + port.read.side_effect = [b'\r', b'\n', None] -def test_file_changes(): - nCard, port = get_serial_and_port() + nCard = notecard.OpenSerial(port, debug=True) - port.readline.return_value = "{\"total\":5}\r\n" + assert nCard._debug - response = file.changes(nCard, tracker="123", files=["sensors.qo"]) - assert "total" in response - assert response["total"] == 5 +class TestNotecardMockI2C(NotecardTest): + def get_port(self, response=None): + nCard, port = get_i2c_and_port() + if response is not None: + chunklen = 0 + tosend = bytes(response, 'utf-8') + def writeto_then_readfrom(addr, write, read): + nonlocal chunklen, tosend + read[0] = len(tosend) + read[1] = chunklen + read[2:2 + chunklen] = tosend[0:chunklen] + tosend = tosend[chunklen:] + chunklen = len(tosend) -def test_file_stats(): - nCard, port = get_serial_and_port() + def transfer(addr, messages: periphery.I2C.Message): + if len(messages) == 2 and messages[1].read: + read = messages[1].data + writeto_then_readfrom(addr, messages[0].data, read) - port.readline.return_value = "{\"total\":24}\r\n" + port.writeto_then_readfrom = writeto_then_readfrom + port.transfer = transfer + return (nCard, port) - response = file.stats(nCard) + def test_open_i2c(self): + nCard, _ = get_i2c_and_port() - assert "total" in response - assert response["total"] == 24 + assert nCard.i2c is not None + def test_user_agent_is_i2c_when_i2c_used(self): + nCard, _ = self.get_port() + userAgent = nCard.GetUserAgent() -def test_file_pendingChanges(): - nCard, port = get_serial_and_port() + assert userAgent['req_interface'] == 'i2c' + assert userAgent['req_port'] is not None - port.readline.return_value = "{\"changes\":1}\r\n" + def test_debug_mode_on_i2c(self): + periphery = Mock() # noqa: F811 + port = periphery.I2C("dev/i2c-foo") + port.try_lock.return_value = True - response = file.pendingChanges(nCard) + nCard = notecard.OpenI2C(port, 0x17, 255, debug=True) - assert "changes" in response - assert response["changes"] == 1 + assert nCard._debug