Skip to content

Commit

Permalink
Merge pull request #1 from CharString/add-hypothesis-tests
Browse files Browse the repository at this point in the history
Add hypothesis tests and bounds checks on Integers
  • Loading branch information
tannewt authored Jul 18, 2024
2 parents 9d2687b + 2c365ac commit e590447
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 20 deletions.
33 changes: 16 additions & 17 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,30 @@ name: Python package

on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]

jobs:
build:

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11"]

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- uses: pre-commit/[email protected]
- name: Test with pytest
run: |
pytest
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install ".[test]"
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- uses: pre-commit/[email protected]
- name: Test with pytest
run: |
pytest
29 changes: 28 additions & 1 deletion circuitmatter/tlv.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import enum
import math
from typing import Optional, Type, Any
import struct
from typing import Any, Optional, Type, Union
from typing import Literal

# As a byte string to save space.
TAG_LENGTH = b"\x00\x01\x02\x04\x02\x04\x06\x08"
Expand Down Expand Up @@ -270,6 +271,19 @@ def __init__(self, tag, _format, optional=False):
print(f"{self._element_type:x}")
super().__init__(tag, optional)

def __set__(self, obj, value):
if self.integer:
octets = 2 ** INT_SIZE.index(self.format.upper()[-1])
bits = 8 * octets
max_size = (2 ** (bits - 1) if self.signed else 2**bits) - 1
min_size = -max_size - 1 if self.signed else 0
if not min_size <= value <= max_size:
raise ValueError(
f"Out of bounds for {octets} octet {'' if self.signed else 'un'}signed int"
)

super().__set__(obj, value)

def decode(self, buffer, length, offset=0):
if self.integer:
encoded_format = INT_SIZE[int(math.log(length, 2))]
Expand Down Expand Up @@ -297,6 +311,19 @@ def encode_value_into(self, value, buffer, offset) -> int:
return offset + self.max_value_length


IntOctetCount = Union[Literal[1], Literal[2], Literal[4], Literal[8]]


class IntMember(NumberMember):
def __init__(
self, tag, /, signed: bool = True, octets: IntOctetCount = 1, optional=False
):
uformat = INT_SIZE[int(math.log2(octets))]
# little-endian
self.format = f"<{uformat.lower() if signed else uformat}"
super().__init__(tag, _format=self.format, optional=optional)


class BoolMember(Member):
max_value_length = 0

Expand Down
15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ dynamic = ["version", "description"]

[project.urls]
Home = "https://github.com/adafruit/circuitmatter"
"Bug Tracker" = "https://github.com/adafruit/circuitmatter/issues"

[project.optional-dependencies]
test = [
"hypothesis",
"pytest",
"pytest-cov",
# "typing_extensions",
]

[tool.coverage.run]
branch = true
source = [
"circuitmatter",
]

[tool.pytest.ini_options]
pythonpath = [
Expand Down
117 changes: 115 additions & 2 deletions tests/test_tlv.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from circuitmatter import tlv
from hypothesis import given, strategies as st
import pytest

import math

Expand All @@ -20,12 +22,12 @@ class TestBool:
def test_bool_false_decode(self):
s = Bool(b"\x08")
assert str(s) == "{\n b = false\n}"
assert not s.b
assert s.b is False

def test_bool_true_decode(self):
s = Bool(b"\x09")
assert str(s) == "{\n b = true\n}"
assert s.b
assert s.b is True

def test_bool_false_encode(self):
s = Bool()
Expand Down Expand Up @@ -115,6 +117,30 @@ def test_signed_int_40000000000_encode(self):
s.i = 40000000000
assert s.encode().tobytes() == b"\x03\x00\x90\x2f\x50\x09\x00\x00\x00"

@pytest.mark.parametrize(
"octets,lower,upper",
[
(1, -128, 127),
(2, -32_768, 32_767),
(4, -2_147_483_648, 2_147_483_647),
(8, -9_223_372_036_854_775_808, 9_223_372_036_854_775_807),
],
)
def test_bounds_checks(self, octets, lower, upper):
class SignedIntStruct(tlv.TLVStructure):
i = tlv.IntMember(None, signed=True, octets=octets)

s = SignedIntStruct()

with pytest.raises(ValueError):
s.i = lower - 1

with pytest.raises(ValueError):
s.i = upper + 1

s.i = lower
s.i = upper


class UnsignedIntOneOctet(tlv.TLVStructure):
i = tlv.NumberMember(None, "B")
Expand All @@ -133,6 +159,41 @@ def test_unsigned_int_42_encode(self):
s.i = 42
assert s.encode().tobytes() == b"\x04\x2a"

@pytest.mark.parametrize(
"octets,lower,upper",
[
(1, 0, 255),
(2, 0, 65_535),
(4, 0, 4_294_967_295),
(8, 0, 18_446_744_073_709_551_615),
],
)
def test_bounds_checks(self, octets, lower, upper):
class UnsignedIntStruct(tlv.TLVStructure):
i = tlv.IntMember(None, signed=False, octets=octets)

s = UnsignedIntStruct()

with pytest.raises(ValueError):
s.i = lower - 1

with pytest.raises(ValueError):
s.i = upper + 1

s.i = lower
s.i = upper

@given(v=st.integers(min_value=0, max_value=255))
def test_roundtrip(self, v: int):
s = UnsignedIntOneOctet()
s.i = v
buffer = s.encode().tobytes()

s2 = UnsignedIntOneOctet(buffer)

assert s2.i == s.i
assert str(s2) == str(s)


# UTF-8 String, 1-octet length, "Hello!"
# 0c 06 48 65 6c 6c 6f 21
Expand Down Expand Up @@ -163,6 +224,17 @@ def test_utf8_string_tschs_encode(self):
s.s = "Tschüs"
assert s.encode().tobytes() == b"\x0c\x07Tsch\xc3\xbcs"

@given(v=...)
def test_roundtrip(self, v: str):
s = UTF8StringOneOctet()
s.s = v
buffer = s.encode().tobytes()

s2 = UTF8StringOneOctet(buffer)

assert s2.s == s.s
assert str(s2) == str(s)


# Octet String, 1-octet length, octets 00 01 02 03 04
# encoded: 10 05 00 01 02 03 04
Expand All @@ -181,6 +253,17 @@ def test_octet_string_encode(self):
s.s = b"\x00\x01\x02\x03\x04"
assert s.encode().tobytes() == b"\x10\x05\x00\x01\x02\x03\x04"

@given(v=...)
def test_roundtrip(self, v: bytes):
s = OctetStringOneOctet()
s.s = v
buffer = s.encode().tobytes()

s2 = OctetStringOneOctet(buffer)

assert s2.s == s.s
assert str(s2) == str(s)


# Null
# 14
Expand Down Expand Up @@ -283,6 +366,21 @@ def test_precision_float_negative_infinity_encode(self):
s.f = float("-inf")
assert s.encode().tobytes() == b"\x0a\x00\x00\x80\xff"

@given(v=...)
def test_roundtrip(self, v: float):
s = FloatSingle()
s.f = v
buffer = s.encode().tobytes()

s2 = FloatSingle(buffer)

assert (
(math.isnan(s.f) and math.isnan(s2.f))
or (s.f > 3.4028235e38 and s2.f == float("inf"))
or (s.f < -3.4028235e38 and s2.f == float("-inf"))
or math.isclose(s2.f, s.f, rel_tol=1e-7, abs_tol=1e-9)
)


class TestFloatDouble:
def test_precision_float_0_0_decode(self):
Expand Down Expand Up @@ -336,6 +434,21 @@ def test_precision_float_negative_infinity_encode(self):
s.f = float("-inf")
assert s.encode().tobytes() == b"\x0b\x00\x00\x00\x00\x00\x00\xf0\xff"

@given(v=...)
def test_roundtrip(self, v: float):
s = FloatDouble()
s.f = v
buffer = s.encode().tobytes()

s2 = FloatDouble(buffer)

assert (
(math.isnan(s.f) and math.isnan(s2.f))
or (s.f > 1.8e308 and s2.f == float("inf"))
or (s.f < -1.8e308 and s2.f == float("-inf"))
or math.isclose(s2.f, s.f, rel_tol=2.22e-16, abs_tol=1e-15)
)


class InnerStruct(tlv.TLVStructure):
a = tlv.NumberMember(0, "<i", optional=True)
Expand Down

0 comments on commit e590447

Please sign in to comment.