diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..6044be0 Binary files /dev/null and b/.coverage differ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f5c3b26 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +name: Test + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install . + pip install .[dev] + - name: Test with pytest + run: | + python -m pytest tests/ --cov --cov-report term-missing diff --git a/.gitignore b/.gitignore index 6fb8897..ab85883 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,7 @@ __pycache__ # build metadata / files *build -python_thingset.egg-info \ No newline at end of file +*egg-info + +# test metadata +.coverage \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 9a05b97..68b55b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,9 +21,9 @@ dependencies = [ [project.optional-dependencies] dev = [ - "mkdocs==1.6.1", - "mkdocs-material==9.5.49", - "mkdocstrings-python==1.13.0" + "pytest==8.3.5", + "pytest-cov==5.0.0", + "ruff==0.6.7" ] [project.urls] @@ -33,3 +33,8 @@ documentation = "https://gitlab.com/Brill-Power/python-thingset/" [project.scripts] thingset = "python_thingset.cli:run_cli" + +[tool.coverage.run] +omit = [ + "*/tests/*", +] diff --git a/python_thingset/backends/serial.py b/python_thingset/backends/serial.py index dddc6f9..213b04c 100644 --- a/python_thingset/backends/serial.py +++ b/python_thingset/backends/serial.py @@ -105,7 +105,7 @@ def _send(self, data: bytes, _: Union[int, None]) -> None: self._serial.send(data) def _recv(self) -> bytes: - self._serial.get_message() + return self._serial.get_message() @property def port(self) -> str: diff --git a/python_thingset/encoders/binary.py b/python_thingset/encoders/binary.py index 3abe3ff..2f0db07 100644 --- a/python_thingset/encoders/binary.py +++ b/python_thingset/encoders/binary.py @@ -28,7 +28,7 @@ def __init__(self): 18 41 # CBOR uint: 0x41 (object ID) """ - def encode_fetch(self, parent_id: int, value_ids: List[int]) -> bytes: + def encode_fetch(self, parent_id: int, value_ids: List[Union[int, None]]) -> bytes: req = bytearray() req.append(ThingSetRequest.FETCH) req += cbor2.dumps(parent_id, canonical=True) @@ -43,7 +43,7 @@ def encode_fetch(self, parent_id: int, value_ids: List[int]) -> bytes: def encode_get(self, value_id: int) -> bytes: return bytes([ThingSetRequest.GET] + list(cbor2.dumps(value_id))) - def encode_exec(self, value_id: int, args: Union[Any, None]) -> bytes: + def encode_exec(self, value_id: int, args: List[Union[Any, None]]) -> bytes: p_args = list() for a in args: diff --git a/python_thingset/encoders/text.py b/python_thingset/encoders/text.py index 2f7188c..afa5b75 100644 --- a/python_thingset/encoders/text.py +++ b/python_thingset/encoders/text.py @@ -10,7 +10,7 @@ class ThingSetTextEncoder(object): def __init__(self): pass - def encode_fetch(self, parent_id: int, ids: List[str]) -> bytes: + def encode_fetch(self, parent_id: str, ids: List[str]) -> bytes: children = "null" if len(ids) > 0: @@ -26,25 +26,19 @@ def encode_fetch(self, parent_id: int, ids: List[str]) -> bytes: def encode_get(self, value_id: str) -> bytes: return f"thingset ?{value_id}\n".encode() - def encode_exec(self, value_id: str, args: Union[Any, None]) -> bytes: + def encode_exec(self, value_id: str, args: List[Union[Any, None]]) -> bytes: """properly format strings for transmission, add args to stringified list""" processed_args = "[" """ leave numeric values as is, surround strings with escape chars """ for a in args: - try: - int(a) + if isinstance(a, int): processed_args += f"{a}," continue - except ValueError: - pass - try: - float(a) + if isinstance(a, float): processed_args += f"{a}," continue - except ValueError: - pass processed_args += f'\\"{a}\\",' @@ -58,16 +52,11 @@ def encode_update(self, parent_id: None, value_id: str, value: Any) -> bytes: val = None - try: + if isinstance(value, int): val = int(value) - except ValueError: - pass - if val is None: - try: - val = float(value) - except ValueError: - pass + if isinstance(value, float): + val = float(value) if val is None: val = f'\\"{value}\\"' diff --git a/tests/encoders/binary/test_enc_bin_exec.py b/tests/encoders/binary/test_enc_bin_exec.py new file mode 100644 index 0000000..b5a2fb3 --- /dev/null +++ b/tests/encoders/binary/test_enc_bin_exec.py @@ -0,0 +1,39 @@ +from python_thingset.encoders import ThingSetBinaryEncoder + + +encoder = ThingSetBinaryEncoder() + + +def test_exec_no_args(): + encoded = encoder.encode_exec(0xF09, []) + assert encoded == b"\x02\x19\x0f\t\x80" + + +def test_exec_one_int(): + encoded = encoder.encode_exec(0xF09, [2]) + assert encoded == b"\x02\x19\x0f\t\x81\x02" + + +def test_exec_one_float(): + encoded = encoder.encode_exec(0xF09, [3.14]) + assert encoded == b"\x02\x19\x0f\t\x81\xfa@H\xf5\xc3" + + +def test_exec_one_str(): + encoded = encoder.encode_exec(0xF09, ["hello"]) + assert encoded == b"\x02\x19\x0f\t\x81ehello" + + +def test_exec_one_bool_true(): + encoded = encoder.encode_exec(0xF09, ["true"]) + assert encoded == b"\x02\x19\x0f\t\x81\xf5" + + +def test_exec_one_bool_false(): + encoded = encoder.encode_exec(0xF09, ["false"]) + assert encoded == b"\x02\x19\x0f\t\x81\xf4" + + +def test_exec_multiple_args(): + encoded = encoder.encode_exec(0xF09, ["false", 5, "age", 7.89, "true"]) + assert encoded == b"\x02\x19\x0f\t\x85\xf4\x05cage\xfa@\xfcz\xe1\xf5" diff --git a/tests/encoders/binary/test_enc_bin_fetch.py b/tests/encoders/binary/test_enc_bin_fetch.py new file mode 100644 index 0000000..79525e0 --- /dev/null +++ b/tests/encoders/binary/test_enc_bin_fetch.py @@ -0,0 +1,19 @@ +from python_thingset.encoders import ThingSetBinaryEncoder + + +encoder = ThingSetBinaryEncoder() + + +def test_fetch_no_children(): + encoded = encoder.encode_fetch(0xF01, []) + assert encoded == b"\x05\x19\x0f\x01\xf6" + + +def test_fetch_one_child(): + encoded = encoder.encode_fetch(0xF, [0xF01]) + assert encoded == b"\x05\x0f\x81\x19\x0f\x01" + + +def test_fetch_two_children(): + encoded = encoder.encode_fetch(0xF, [0xF01, 0xF02]) + assert encoded == b"\x05\x0f\x82\x19\x0f\x01\x19\x0f\x02" diff --git a/tests/encoders/binary/test_enc_bin_get.py b/tests/encoders/binary/test_enc_bin_get.py new file mode 100644 index 0000000..5c9cfba --- /dev/null +++ b/tests/encoders/binary/test_enc_bin_get.py @@ -0,0 +1,9 @@ +from python_thingset.encoders import ThingSetBinaryEncoder + + +encoder = ThingSetBinaryEncoder() + + +def test_get(): + encoded = encoder.encode_get(0xF01) + assert encoded == b"\x01\x19\x0f\x01" diff --git a/tests/encoders/binary/test_enc_bin_get_paths.py b/tests/encoders/binary/test_enc_bin_get_paths.py new file mode 100644 index 0000000..3d95a49 --- /dev/null +++ b/tests/encoders/binary/test_enc_bin_get_paths.py @@ -0,0 +1,9 @@ +from python_thingset.encoders import ThingSetBinaryEncoder + + +encoder = ThingSetBinaryEncoder() + + +def test_enc_get_paths(): + encoded = encoder.encode_get_path(0xF) + assert encoded == b"\x05\x17\x81\x0f" diff --git a/tests/encoders/binary/test_enc_bin_update.py b/tests/encoders/binary/test_enc_bin_update.py new file mode 100644 index 0000000..c2dd17f --- /dev/null +++ b/tests/encoders/binary/test_enc_bin_update.py @@ -0,0 +1,29 @@ +from python_thingset.encoders import ThingSetBinaryEncoder + + +encoder = ThingSetBinaryEncoder() + + +def test_update_int(): + encoded = encoder.encode_update(0x0, 0x4F, 1) + assert encoded == b"\x07\x00\xa1\x18O\x01" + + +def test_update_float(): + encoded = encoder.encode_update(0x0, 0x4F, 3.14) + assert encoded == b"\x07\x00\xa1\x18O\xfa@H\xf5\xc3" + + +def test_update_str(): + encoded = encoder.encode_update(0x0, 0x4F, "hello") + assert encoded == b"\x07\x00\xa1\x18Oehello" + + +def test_update_bool_true(): + encoded = encoder.encode_update(0x0, 0x4F, "true") + assert encoded == b"\x07\x00\xa1\x18O\xf5" + + +def test_update_bool_false(): + encoded = encoder.encode_update(0x0, 0x4F, "false") + assert encoded == b"\x07\x00\xa1\x18O\xf4" diff --git a/tests/encoders/text/test_enc_txt_exec.py b/tests/encoders/text/test_enc_txt_exec.py new file mode 100644 index 0000000..6a52612 --- /dev/null +++ b/tests/encoders/text/test_enc_txt_exec.py @@ -0,0 +1,29 @@ +from python_thingset.encoders import ThingSetTextEncoder + + +encoder = ThingSetTextEncoder() + + +def test_exec_no_args(): + encoded = encoder.encode_exec("Func", []) + assert encoded == "thingset !Func []\n".encode() + + +def test_exec_int_arg(): + encoded = encoder.encode_exec("Func", [7]) + assert encoded == "thingset !Func [7,]\n".encode() + + +def test_exec_float_arg(): + encoded = encoder.encode_exec("Func", [3.14]) + assert encoded == "thingset !Func [3.14,]\n".encode() + + +def test_exec_str_arg(): + encoded = encoder.encode_exec("Func", ["a_text-arg"]) + assert encoded == """thingset !Func [\\"a_text-arg\\",]\n""".encode() + + +def test_exec_one_of_each_arg(): + encoded = encoder.encode_exec("Func", [1, 2.34, "moretext"]) + assert encoded == """thingset !Func [1,2.34,\\"moretext\\",]\n""".encode() diff --git a/tests/encoders/text/test_enc_txt_fetch.py b/tests/encoders/text/test_enc_txt_fetch.py new file mode 100644 index 0000000..0ebfc9c --- /dev/null +++ b/tests/encoders/text/test_enc_txt_fetch.py @@ -0,0 +1,24 @@ +from python_thingset.encoders import ThingSetTextEncoder + + +encoder = ThingSetTextEncoder() + + +def test_fetch_root(): + encoded = encoder.encode_fetch("", []) + assert encoded == "thingset ? null\n".encode() + + +def test_fetch_root_one_child(): + encoded = encoder.encode_fetch("", ["One"]) + assert encoded == """thingset ? [\\"One\\",]\n""".encode() + + +def test_fetch_root_two_children(): + encoded = encoder.encode_fetch("", ["One", "Two"]) + assert encoded == """thingset ? [\\"One\\",\\"Two\\",]\n""".encode() + + +def test_fetch_not_root_one_child(): + encoded = encoder.encode_fetch("NotRoot", ["One"]) + assert encoded == """thingset ?NotRoot [\\"One\\",]\n""".encode() diff --git a/tests/encoders/text/test_enc_txt_get.py b/tests/encoders/text/test_enc_txt_get.py new file mode 100644 index 0000000..1b4832f --- /dev/null +++ b/tests/encoders/text/test_enc_txt_get.py @@ -0,0 +1,19 @@ +from python_thingset.encoders import ThingSetTextEncoder + + +encoder = ThingSetTextEncoder() + + +def test_get_root(): + encoded = encoder.encode_get("") + assert encoded == "thingset ?\n".encode() + + +def test_get_depth_one(): + encoded = encoder.encode_get("One") + assert encoded == "thingset ?One\n".encode() + + +def test_get_depth_two(): + encoded = encoder.encode_get("One/Two") + assert encoded == "thingset ?One/Two\n".encode() diff --git a/tests/encoders/text/test_enc_txt_update.py b/tests/encoders/text/test_enc_txt_update.py new file mode 100644 index 0000000..b4318d1 --- /dev/null +++ b/tests/encoders/text/test_enc_txt_update.py @@ -0,0 +1,29 @@ +from python_thingset.encoders import ThingSetTextEncoder + + +encoder = ThingSetTextEncoder() + + +def test_update_value_at_root_int(): + encoded = encoder.encode_update(None, "Value", [1]) + assert encoded == """thingset = {\\"Value\\":1}\n""".encode() + + +def test_update_value_at_root_float(): + encoded = encoder.encode_update(None, "Value", [3.14]) + assert encoded == """thingset = {\\"Value\\":3.14}\n""".encode() + + +def test_update_value_at_root_str(): + encoded = encoder.encode_update(None, "Value", ["sometext"]) + assert encoded == """thingset = {\\"Value\\":\\"sometext\\"}\n""".encode() + + +def test_update_value_at_depth_one(): + encoded = encoder.encode_update(None, "One/Value", [1]) + assert encoded == """thingset =One {\\"Value\\":1}\n""".encode() + + +def test_update_value_at_depth_two(): + encoded = encoder.encode_update(None, "One/Two/Value", [1]) + assert encoded == """thingset =One/Two {\\"Value\\":1}\n""".encode()