Skip to content

Implement new zigpy radio API #123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
repos:
- repo: https://github.com/psf/black
rev: 19.10b0
rev: 22.3.0
hooks:
- id: black
args:
- --safe
- --quiet
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9
rev: 4.0.1
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-isort
rev: v4.3.21
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
hooks:
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
rev: v4.1.0
hooks:
- id: no-commit-to-branch
args:
Expand Down
7 changes: 4 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ force_grid_wrap=0
use_parentheses=True
line_length=88
indent = " "
# by default isort don't check module indexes
not_skip = __init__.py
# will group `import x` and `from x import` of the same module.
force_sort_within_sections = true
sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
default_section = THIRDPARTY
known_first_party = zigpy_xbee,tests
forced_separate = tests
combine_as_imports = true

[tool:pytest]
asyncio_mode = auto
12 changes: 4 additions & 8 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
"""Setup module for zigpy-xbee"""

import os
import pathlib

from setuptools import find_packages, setup

import zigpy_xbee

this_directory = os.path.join(os.path.abspath(os.path.dirname(__file__)))
with open(os.path.join(this_directory, "README.md"), encoding="utf-8") as f:
long_description = f.read()

setup(
name="zigpy-xbee",
version=zigpy_xbee.__version__,
description="A library which communicates with XBee radios for zigpy",
long_description=long_description,
long_description=(pathlib.Path(__file__).parent / "README.md").read_text(),
long_description_content_type="text/markdown",
url="http://github.com/zigpy/zigpy-xbee",
author="Russell Cloran",
author_email="rcloran@gmail.com",
license="GPL-3.0",
packages=find_packages(exclude=["*.tests"]),
install_requires=["pyserial-asyncio", "zigpy>= 0.23.0"],
tests_require=["pytest"],
install_requires=["pyserial-asyncio", "zigpy>=0.47.0"],
tests_require=["pytest", "asynctest", "pytest-asyncio"],
)
Empty file added tests/__init__.py
Empty file.
9 changes: 9 additions & 0 deletions tests/async_mock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Mock utilities that are async aware."""
import sys

if sys.version_info[:2] < (3, 8):
from asynctest.mock import * # noqa

AsyncMock = CoroutineMock # noqa: F405
else:
from unittest.mock import * # noqa
67 changes: 17 additions & 50 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import asyncio
import logging

from asynctest import CoroutineMock, mock
import pytest
import serial
import zigpy.exceptions
Expand All @@ -10,6 +9,8 @@
import zigpy_xbee.config
from zigpy_xbee.zigbee.application import ControllerApplication

import tests.async_mock as mock

DEVICE_CONFIG = zigpy_xbee.config.SCHEMA_DEVICE(
{zigpy_xbee.config.CONF_DEVICE_PATH: "/dev/null"}
)
Expand All @@ -22,10 +23,9 @@ def api():
return api


@pytest.mark.asyncio
async def test_connect(monkeypatch):
api = xbee_api.XBee(DEVICE_CONFIG)
monkeypatch.setattr(uart, "connect", CoroutineMock())
monkeypatch.setattr(uart, "connect", mock.AsyncMock())
await api.connect()


Expand All @@ -52,7 +52,6 @@ def test_commands():
assert reply is None or isinstance(reply, int)


@pytest.mark.asyncio
async def test_command(api):
def mock_api_frame(name, *args):
c = xbee_api.COMMAND_REQUESTS[name]
Expand Down Expand Up @@ -90,7 +89,6 @@ def mock_api_frame(name, *args):
api._uart.send.reset_mock()


@pytest.mark.asyncio
async def test_command_not_connected(api):
api._uart = None

Expand Down Expand Up @@ -135,20 +133,17 @@ def mock_command(name, *args):
api._command.reset_mock()


@pytest.mark.asyncio
async def test_at_command(api, monkeypatch):
await _test_at_or_queued_at_command(api, api._at_command, monkeypatch)


@pytest.mark.asyncio
async def test_at_command_no_response(api, monkeypatch):
with pytest.raises(asyncio.TimeoutError):
await _test_at_or_queued_at_command(
api, api._at_command, monkeypatch, do_reply=False
)


@pytest.mark.asyncio
async def test_queued_at_command(api, monkeypatch):
await _test_at_or_queued_at_command(api, api._queued_at, monkeypatch)

Expand Down Expand Up @@ -191,12 +186,10 @@ def mock_command(name, *args):
api._command.reset_mock()


@pytest.mark.asyncio
async def test_remote_at_cmd(api, monkeypatch):
await _test_remote_at_command(api, monkeypatch)


@pytest.mark.asyncio
async def test_remote_at_cmd_no_rsp(api, monkeypatch):
monkeypatch.setattr(xbee_api, "REMOTE_AT_COMMAND_TIMEOUT", 0.1)
with pytest.raises(asyncio.TimeoutError):
Expand Down Expand Up @@ -417,7 +410,6 @@ def test_handle_tx_status_duplicate(api):
assert send_fut.set_exception.call_count == 0


@pytest.mark.asyncio
async def test_command_mode_at_cmd(api):
command = "+++"

Expand All @@ -430,7 +422,6 @@ def cmd_mode_send(cmd):
assert result


@pytest.mark.asyncio
async def test_command_mode_at_cmd_timeout(api):
command = "+++"

Expand Down Expand Up @@ -462,21 +453,15 @@ def test_handle_command_mode_rsp(api):
assert api._cmd_mode_future.result() == data


@pytest.mark.asyncio
async def test_enter_at_command_mode(api):
api.command_mode_at_cmd = mock.MagicMock(
side_effect=asyncio.coroutine(lambda x: mock.sentinel.at_response)
)
api.command_mode_at_cmd = mock.AsyncMock(return_value=mock.sentinel.at_response)

res = await api.enter_at_command_mode()
assert res == mock.sentinel.at_response


@pytest.mark.asyncio
async def test_api_mode_at_commands(api):
api.command_mode_at_cmd = mock.MagicMock(
side_effect=asyncio.coroutine(lambda x: mock.sentinel.api_mode)
)
api.command_mode_at_cmd = mock.AsyncMock(return_value=mock.sentinel.api_mode)

res = await api.api_mode_at_commands(57600)
assert res is True
Expand All @@ -491,20 +476,15 @@ async def mock_at_cmd(cmd):
assert res is None


@pytest.mark.asyncio
async def test_init_api_mode(api, monkeypatch):
monkeypatch.setattr(api._uart, "baudrate", 57600)
api.enter_at_command_mode = mock.MagicMock(
side_effect=asyncio.coroutine(mock.MagicMock(return_value=True))
)
api.enter_at_command_mode = mock.AsyncMock(return_value=True)

res = await api.init_api_mode()
assert res is None
assert api.enter_at_command_mode.call_count == 1

api.enter_at_command_mode = mock.MagicMock(
side_effect=asyncio.coroutine(mock.MagicMock(return_value=False))
)
api.enter_at_command_mode = mock.AsyncMock(return_value=False)

res = await api.init_api_mode()
assert res is False
Expand All @@ -517,9 +497,7 @@ async def enter_at_mode():

api._uart.baudrate = 57600
api.enter_at_command_mode = mock.MagicMock(side_effect=enter_at_mode)
api.api_mode_at_commands = mock.MagicMock(
side_effect=asyncio.coroutine(mock.MagicMock(return_value=True))
)
api.api_mode_at_commands = mock.AsyncMock(return_value=True)

res = await api.init_api_mode()
assert res is True
Expand All @@ -542,21 +520,16 @@ def test_handle_many_to_one_rri(api):
api._handle_many_to_one_rri(ieee, nwk, 0)


@pytest.mark.asyncio
async def test_reconnect_multiple_disconnects(monkeypatch, caplog):
api = xbee_api.XBee(DEVICE_CONFIG)
connect_mock = CoroutineMock()
connect_mock.return_value = asyncio.Future()
connect_mock.return_value.set_result(True)
connect_mock = mock.AsyncMock(return_value=True)
monkeypatch.setattr(uart, "connect", connect_mock)

await api.connect()

caplog.set_level(logging.DEBUG)
connected = asyncio.Future()
connected.set_result(mock.sentinel.uart_reconnect)
connect_mock.reset_mock()
connect_mock.side_effect = [asyncio.Future(), connected]
connect_mock.side_effect = [OSError, mock.sentinel.uart_reconnect]
api.connection_lost("connection lost")
await asyncio.sleep(0.3)
api.connection_lost("connection lost 2")
Expand All @@ -567,21 +540,20 @@ async def test_reconnect_multiple_disconnects(monkeypatch, caplog):
assert connect_mock.call_count == 2


@pytest.mark.asyncio
async def test_reconnect_multiple_attempts(monkeypatch, caplog):
api = xbee_api.XBee(DEVICE_CONFIG)
connect_mock = CoroutineMock()
connect_mock.return_value = asyncio.Future()
connect_mock.return_value.set_result(True)
connect_mock = mock.AsyncMock(return_value=True)
monkeypatch.setattr(uart, "connect", connect_mock)

await api.connect()

caplog.set_level(logging.DEBUG)
connected = asyncio.Future()
connected.set_result(mock.sentinel.uart_reconnect)
connect_mock.reset_mock()
connect_mock.side_effect = [asyncio.TimeoutError, OSError, connected]
connect_mock.side_effect = [
asyncio.TimeoutError,
OSError,
mock.sentinel.uart_reconnect,
]

with mock.patch("asyncio.sleep"):
api.connection_lost("connection lost")
Expand All @@ -591,8 +563,7 @@ async def test_reconnect_multiple_attempts(monkeypatch, caplog):
assert connect_mock.call_count == 3


@pytest.mark.asyncio
@mock.patch.object(xbee_api.XBee, "_at_command", new_callable=CoroutineMock)
@mock.patch.object(xbee_api.XBee, "_at_command", new_callable=mock.AsyncMock)
@mock.patch.object(uart, "connect")
async def test_probe_success(mock_connect, mock_at_cmd):
"""Test device probing."""
Expand All @@ -606,7 +577,6 @@ async def test_probe_success(mock_connect, mock_at_cmd):
assert mock_connect.return_value.close.call_count == 1


@pytest.mark.asyncio
@mock.patch.object(xbee_api.XBee, "init_api_mode", return_value=True)
@mock.patch.object(xbee_api.XBee, "_at_command", side_effect=asyncio.TimeoutError)
@mock.patch.object(uart, "connect")
Expand All @@ -623,7 +593,6 @@ async def test_probe_success_api_mode(mock_connect, mock_at_cmd, mock_api_mode):
assert mock_connect.return_value.close.call_count == 1


@pytest.mark.asyncio
@mock.patch.object(xbee_api.XBee, "init_api_mode")
@mock.patch.object(xbee_api.XBee, "_at_command", side_effect=asyncio.TimeoutError)
@mock.patch.object(uart, "connect")
Expand All @@ -648,7 +617,6 @@ async def test_probe_fail(mock_connect, mock_at_cmd, mock_api_mode, exception):
assert mock_connect.return_value.close.call_count == 1


@pytest.mark.asyncio
@mock.patch.object(xbee_api.XBee, "init_api_mode", return_value=False)
@mock.patch.object(xbee_api.XBee, "_at_command", side_effect=asyncio.TimeoutError)
@mock.patch.object(uart, "connect")
Expand All @@ -668,7 +636,6 @@ async def test_probe_fail_api_mode(mock_connect, mock_at_cmd, mock_api_mode):
assert mock_connect.return_value.close.call_count == 1


@pytest.mark.asyncio
@mock.patch.object(xbee_api.XBee, "connect")
async def test_xbee_new(conn_mck):
"""Test new class method."""
Expand Down
Loading