diff --git a/.gitignore b/.gitignore index f673a20..5e7e6c7 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ coverage.xml .hypothesis/ .pytest_cache/ cover/ +coverage/ tests/coverage_report/ # Translations diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index b6e4148..126958f 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -37,17 +37,33 @@ done pdm install ``` -### perform unit tests: +### testing + +The entire test suite (unit tests & compatibility tests) can be triggered +by running ```SHELL -pdm run pytest +scripts/run_tests ``` -### perform compatibility tests: +You have successfully run all tests when the output ends with: -```SHELL -tox ``` +---------------------------------------------------------------------- +If you reached this far you passed. Congratulations! +---------------------------------------------------------------------- +``` + +If the unit tests have run successfully coverage file in LCOV format is +generated and stored at `coverage/lcov.info`. This file can be used by +your IDE to indicate code coverage from within your editor window. +- We recommend the Visual Studio Code extension + [Code Coverage](https://marketplace.visualstudio.com/items?itemName=markis.code-coverage). +- Special care should be taken when modifying code that is not covered by unit tests! + +Branches that do not pass the test harness (e.g. due to failing unit tests +or lowering the code coverage beneath the desired threshold) should not be +pull-requested. ### use the demo script diff --git a/feeph/i2c/burst_handler.py b/feeph/i2c/burst_handler.py old mode 100644 new mode 100755 index fc2fa20..ed007f5 --- a/feeph/i2c/burst_handler.py +++ b/feeph/i2c/burst_handler.py @@ -66,19 +66,18 @@ def read_register(self, register: int, byte_count: int = 1, max_tries: int = 5) buf_r = bytearray(byte_count) buf_r[0] = register buf_w = bytearray(byte_count) + cur_try = 0 for cur_try in range(1, 1 + max_tries): try: self._i2c_bus.writeto_then_readfrom(address=self._i2c_adr, buffer_out=buf_r, buffer_in=buf_w) return convert_bytearry_to_uint(buf_w) - except OSError as e: + # protect against sporadic errors on actual devices + # (maybe we can do something to prevent these errors?) + except (OSError, RuntimeError) as e: # [Errno 121] Remote I/O error - LH.warning("[%s] Failed to read register 0x%02X (%i/%i): %s", __name__, register, cur_try, max_tries, e) - time.sleep(0.001) - except RuntimeError as e: LH.warning("[%s] Unable to read register 0x%02X (%i/%i): %s", __name__, register, cur_try, max_tries, e) time.sleep(0.001) - else: - raise RuntimeError(f"Unable to read register 0x{register:02X} after {cur_try} attempts. Giving up.") + raise RuntimeError(f"Unable to read register 0x{register:02X} after {cur_try} attempts. Giving up.") def write_register(self, register: int, value: int, byte_count: int = 1, max_tries: int = 3): """ @@ -91,23 +90,21 @@ def write_register(self, register: int, value: int, byte_count: int = 1, max_tri """ _validate_register_address(register) ba = convert_uint_to_bytearry(value, byte_count) - buf = bytearray(1 + len(ba)) - buf[0] = register - for i in range(len(ba)): - buf[1+i] = ba[i] + buf = bytearray([register]) + for byte in ba: + buf.append(byte) + cur_try = 0 for cur_try in range(1, 1 + max_tries): try: self._i2c_bus.writeto(address=self._i2c_adr, buffer=buf) return - except OSError as e: + # protect against sporadic errors on actual devices + # (maybe we can do something to prevent these errors?) + except (OSError, RuntimeError) as e: # [Errno 121] Remote I/O error - LH.warning("[%s] Failed to read register 0x%02X (%i/%i): %s", __name__, register, cur_try, max_tries, e) + LH.warning("[%s] Unable to write register 0x%02X (%i/%i): %s", __name__, register, cur_try, max_tries, e) time.sleep(0.1) - except RuntimeError as e: - LH.warning("[%s] Unable to read register 0x%02X (%i/%i): %s", __name__, register, cur_try, max_tries, e) - time.sleep(0.1) - else: - raise RuntimeError(f"Unable to read register 0x{register:02X} after {cur_try} attempts. Giving up.") + raise RuntimeError(f"Unable to read register 0x{register:02X} after {cur_try} attempts. Giving up.") # it is unclear if it's possible to have a multi-byte state registers # (a register write looks exactly like a multi-byte state write) @@ -124,19 +121,18 @@ def get_state(self, byte_count: int = 1, max_tries: int = 5) -> int: LH.warning("Multi byte reads are not implemented yet! Returning a single byte instead.") byte_count = 1 buf = bytearray(byte_count) + cur_try = 0 for cur_try in range(1, 1 + max_tries): try: self._i2c_bus.readfrom_into(address=self._i2c_adr, buffer=buf) return buf[0] - except OSError as e: + # protect against sporadic errors on actual devices + # (maybe we can do something to prevent these errors?) + except (OSError, RuntimeError) as e: # [Errno 121] Remote I/O error - LH.warning("[%s] Failed to read state (%i/%i): %s", __name__, cur_try, max_tries, e) - time.sleep(0.001) - except RuntimeError as e: LH.warning("[%s] Unable to read state (%i/%i): %s", __name__, cur_try, max_tries, e) time.sleep(0.001) - else: - raise RuntimeError(f"Unable to read state after {cur_try} attempts. Giving up.") + raise RuntimeError(f"Unable to read state after {cur_try} attempts. Giving up.") def set_state(self, value: int, byte_count: int = 1, max_tries: int = 3): """ @@ -149,19 +145,18 @@ def set_state(self, value: int, byte_count: int = 1, max_tries: int = 3): LH.warning("Multi byte writes are not implemented yet! Returning a single byte instead.") byte_count = 1 buf = convert_uint_to_bytearry(value, byte_count) + cur_try = 0 for cur_try in range(1, 1 + max_tries): try: self._i2c_bus.writeto(address=self._i2c_adr, buffer=buf) return - except OSError as e: + # protect against sporadic errors on actual devices + # (maybe we can do something to prevent these errors?) + except (OSError, RuntimeError) as e: # [Errno 121] Remote I/O error - LH.warning("[%s] Failed to write state (%i/%i): %s", __name__, cur_try, max_tries, e) + LH.warning("[%s] Unable to write state (%i/%i): %s", __name__, cur_try, max_tries, e) time.sleep(0.1) - except RuntimeError as e: - LH.warning("[%s] Unable to write state 0x%02X (%i/%i): %s", __name__, cur_try, max_tries, e) - time.sleep(0.1) - else: - raise RuntimeError(f"Unable to write state after {cur_try} attempts. Giving up.") + raise RuntimeError(f"Unable to write state after {cur_try} attempts. Giving up.") class BurstHandler: @@ -182,6 +177,8 @@ def __init__(self, i2c_bus: busio.I2C, i2c_adr: int, timeout_ms: int | None = 50 self._timeout_ms = timeout_ms else: raise ValueError("Provided timeout is not a positive integer or 'None'!") + # register '_timestart_ns' - we will populate it later on + self._timestart_ns = 0 def __enter__(self) -> BurstHandle: """ diff --git a/feeph/i2c/emulation.py b/feeph/i2c/emulation.py index e4b6ed2..2f341e0 100755 --- a/feeph/i2c/emulation.py +++ b/feeph/i2c/emulation.py @@ -21,6 +21,11 @@ class EmulatedI2C(busio.I2C): (e.g. duplicated registers with multiple addresses) """ + # We intentionally do not call super().init() because we don't want to + # call any of the hardware initialization code. We are only interested + # in being able to claim we are an instance of 'busio.I2C' in case + # something else tries to validate that or has logic tied to it. + # pylint: disable=super-init-not-called def __init__(self, state: dict[int, dict[int, int]], lock_chance: int = 100): """ initialize a simulated I2C bus @@ -39,7 +44,7 @@ def __init__(self, state: dict[int, dict[int, int]], lock_chance: int = 100): def try_lock(self) -> bool: # may randomly fail to acquire a lock - return (random.randint(0, 100) < self._lock_chance) + return random.randint(0, 100) < self._lock_chance def unlock(self): pass @@ -48,24 +53,41 @@ def unlock(self): # being read/written since the register address must be positive in the # range 0 ≤ x ≤ 255. - def readfrom_into(self, address, buffer, *, start=0, end=None, stop=True): + # replicate the signature of busio.I2C + # pylint: disable=too-many-arguments + def readfrom_into(self, address: int, buffer: bytearray, *, start=0, end=None, stop=True): """ read device state (buffer is used as an output parameter) """ + # make sure that buffer truly is a bytearray + # (we really want this to be a bytearray because bytearray performs + # its own input validation and automatically guarantees we're not + # getting negative byte values or other unexpected data as input) + if not isinstance(buffer, bytearray): + raise ValueError("buffer must be of type 'bytearray'") i2c_device_address = address i2c_device_register = -1 value = self._state[i2c_device_address][i2c_device_register] ba = convert_uint_to_bytearry(value, len(buffer)) # copy computed result to output parameter + # pylint: disable=consider-using-enumerate for i in range(len(buffer)): buffer[i] = ba[i] + # replicate the signature of busio.I2C + # pylint: disable=too-many-arguments def writeto(self, address: int, buffer: bytearray, *, start=0, end=None): """ write device state or register """ + # make sure that buffer truly is a bytearray + # (we really want this to be a bytearray because bytearray performs + # its own input validation and automatically guarantees we're not + # getting negative byte values or other unexpected data as input) + if not isinstance(buffer, bytearray): + raise ValueError("buffer must be of type 'bytearray'") if len(buffer) == 1: # device status i2c_device_address = address @@ -75,8 +97,6 @@ def writeto(self, address: int, buffer: bytearray, *, start=0, end=None): # device register i2c_device_address = address i2c_device_register = buffer[0] - if i2c_device_register < 0: - raise ValueError("device register can't be negative") value = convert_bytearry_to_uint(buffer[1:]) self._state[i2c_device_address][i2c_device_register] = value @@ -86,12 +106,19 @@ def writeto_then_readfrom(self, address: int, buffer_out: bytearray, buffer_in: (buffer_in is used as an output parameter) """ + # make sure that buffer_in and buffer_out truly are a bytearray + # (we really want this to be a bytearray because bytearray performs + # its own input validation and automatically guarantees we're not + # getting negative byte values or other unexpected data as input) + if not isinstance(buffer_in, bytearray): + raise ValueError("buffer_in must be of type 'bytearray'") + if not isinstance(buffer_out, bytearray): + raise ValueError("buffer_out must be of type 'bytearray'") i2c_device_address = address i2c_device_register = buffer_out[0] - if i2c_device_register < 0: - raise ValueError("device register can't be negative") value = self._state[i2c_device_address][i2c_device_register] ba = convert_uint_to_bytearry(value, len(buffer_in)) # copy computed result to output parameter + # pylint: disable=consider-using-enumerate for i in range(len(buffer_in)): buffer_in[i] = ba[i] diff --git a/feeph/i2c/utility.py b/feeph/i2c/utility.py deleted file mode 100755 index 791792c..0000000 --- a/feeph/i2c/utility.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -""" -""" - -import time - -# module busio provides no type hints -import busio # type: ignore - - -def try_lock_with_timeout(i2c_bus: busio.I2C, timeout_ms: int, sleep_time_ms: int = 1): - """ - Try to acquire a lock for exclusive access on the I²C bus. - - Raises a RuntimeError if it wasn't possible to acquire the lock within - the given timeout. - """ - if not isinstance(timeout_ms, int) or timeout_ms < 0: - raise ValueError("Timeout must be a positive integer.") - # 0.001 = 1 millisecond - # 0.000_001 = 1 microsecond - # 0.000_000_001 = 1 nanosecond - timeout_ns = timeout_ms * 1000 * 1000 - sleep_time = float(sleep_time_ms) / 1000 - deadline = time.monotonic_ns() + timeout_ns - while not i2c_bus.try_lock(): - if time.monotonic_ns() <= deadline: - # I²C bus was busy, wait and retry - time.sleep(sleep_time) # sleep for 1 milliseconds - else: - # unable to acquire the I²C bus - raise RuntimeError("timed out while waiting on I²C bus to become available") diff --git a/pdm.lock b/pdm.lock index 8d7d389..462c88f 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev", "tools"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:dbd8cfd49efe867ed31d24015c2e39a716286d6a666d3f532bcf8acfbb521f6e" +content_hash = "sha256:e6d89df59d2681ecd02606d48016d222e5adee84c6d2b043eb431194545ef3b7" [[metadata.targets]] requires_python = ">=3.10,<3.13" @@ -164,6 +164,21 @@ files = [ {file = "binho_host_adapter-0.1.6-py3-none-any.whl", hash = "sha256:f71ca176c1e2fc1a5dce128beb286da217555c6c7c805f2ed282a6f3507ec277"}, ] +[[package]] +name = "click" +version = "8.1.7" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +groups = ["tools"] +dependencies = [ + "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + [[package]] name = "colorama" version = "0.4.6" @@ -243,6 +258,22 @@ files = [ {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, ] +[[package]] +name = "coverage-lcov" +version = "0.3.0" +requires_python = "<4.0.0,>=3.6.3" +summary = "A simple .coverage to LCOV converter" +groups = ["tools"] +dependencies = [ + "click>=7.1.2", + "coverage[toml]>=5.5; python_full_version < \"3.8.0\"", + "coverage[toml]>=7.0; python_full_version >= \"3.8.0\"", +] +files = [ + {file = "coverage-lcov-0.3.0.tar.gz", hash = "sha256:0b352da0c72eacd555de2e10fa75ec165b8849ab6827218abc3ef6be2ec861f6"}, + {file = "coverage_lcov-0.3.0-py3-none-any.whl", hash = "sha256:23ae34a535f1ed3fa4cdf9682f6c1fe1a40aa98c94aa05614512dbf2da82e749"}, +] + [[package]] name = "coverage" version = "7.6.1" diff --git a/pyproject.toml b/pyproject.toml index 54f3b69..d35f56b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,13 +97,14 @@ source-includes = ["tests/"] dev = [ ] tools = [ - "autopep8 ~= 2.2", - "copier ~= 9.3", - "flake8 ~= 7.0", - "mypy ~= 1.10", - "pylint ~= 3.2", - "pytest-cov ~= 5.0", - "pytest-sugar ~= 1.0", + "autopep8 ~= 2.2", + "copier ~= 9.3", + "coverage-lcov ~= 0.3", + "flake8 ~= 7.0", + "mypy ~= 1.10", + "pylint ~= 3.2", + "pytest-cov ~= 5.0", + "pytest-sugar ~= 1.0", ] # during development: fetch all packages in our namespace from TestPyPI diff --git a/requirements-dev.txt b/requirements-dev.txt index 419707a..3a0612f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,6 +10,9 @@ astroid==3.2.4 \ autopep8==2.3.1 \ --hash=sha256:8d6c87eba648fdcfc83e29b788910b8643171c395d9c4bcf115ece035b9c9dda \ --hash=sha256:a203fe0fcad7939987422140ab17a930f684763bf7335bdb6709991dd7ef6c2d +click==8.1.7 \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 @@ -49,6 +52,9 @@ coverage==7.6.1 \ --hash=sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959 \ --hash=sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234 \ --hash=sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc +coverage-lcov==0.3.0 \ + --hash=sha256:0b352da0c72eacd555de2e10fa75ec165b8849ab6827218abc3ef6be2ec861f6 \ + --hash=sha256:23ae34a535f1ed3fa4cdf9682f6c1fe1a40aa98c94aa05614512dbf2da82e749 coverage[toml]==7.6.1 \ --hash=sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d \ --hash=sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6 \ diff --git a/scripts/run_tests b/scripts/run_tests index 3bcc981..22e7ee0 100755 --- a/scripts/run_tests +++ b/scripts/run_tests @@ -23,6 +23,25 @@ if [[ -n $FILES ]] ; then pdm run mypy $FILES fi +# Generate a coverage report in lcov format even if pytest fails due to +# lacking coverage. The report can then be used by an IDE to indicate +# coverage directly in your editor window. Special care must be taken +# when working on code that isn't covered by unit tests. +# +# recommended extension for VS Code: +# https://marketplace.visualstudio.com/items?itemName=markis.code-coverage +generate_lcov() { + mkdir -p coverage + # generate coverage report in lcov and HTML format + pdm run coverage-lcov --output_file_path=coverage/lcov.info + pdm run coverage html +} + +trap generate_lcov EXIT +pdm run pytest --cov=feeph.i2c --cov-report=term-missing --no-header --quiet tests/ + +tox + echo "----------------------------------------------------------------------" echo "If you reached this far you passed. Congratulations!" echo "----------------------------------------------------------------------" diff --git a/tests/test_burst_handler.py b/tests/test_burst_handler.py old mode 100644 new mode 100755 index fb35ab9..7626e03 --- a/tests/test_burst_handler.py +++ b/tests/test_burst_handler.py @@ -8,6 +8,7 @@ import feeph.i2c as sut # sytem under test +# pylint: disable=protected-access,too-many-public-methods class TestBurstHandler(unittest.TestCase): def test_read_device_register(self): @@ -38,6 +39,15 @@ def test_read_device_register_multibyte(self): # ----------------------------------------------------------------- self.assertEqual(computed, expected) + def test_read_device_register_insufficient_tries(self): + i2c_bus = sut.EmulatedI2C(state={}) + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + with sut.BurstHandler(i2c_bus=i2c_bus, i2c_adr=0x4C) as bh: + # under realistic circumstances the max_tries would be a positive + # value but we're intentionally setting it to 0 to force an error + self.assertRaises(RuntimeError, bh.read_register, 0x00, max_tries=0) + def test_read_device_registers(self): state = { 0x4C: { @@ -104,6 +114,15 @@ def test_write_device_register_multibyte(self): # ----------------------------------------------------------------- self.assertEqual(computed, expected) + def test_write_device_register_insufficient_tries(self): + i2c_bus = sut.EmulatedI2C(state={}) + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + with sut.BurstHandler(i2c_bus=i2c_bus, i2c_adr=0x4C) as bh: + # under realistic circumstances the max_tries would be a positive + # value but we're intentionally setting it to 0 to force an error + self.assertRaises(RuntimeError, bh.write_register, 0x00, value=0x12, max_tries=0) + def test_write_device_registers(self): state = { 0x4C: { @@ -187,6 +206,15 @@ def test_get_state_multibyte(self): # ----------------------------------------------------------------- self.assertEqual(computed, expected) + def test_get_state_insufficient_tries(self): + i2c_bus = sut.EmulatedI2C(state={}) + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + with sut.BurstHandler(i2c_bus=i2c_bus, i2c_adr=0x4C) as bh: + # under realistic circumstances the max_tries would be a positive + # value but we're intentionally setting it to 0 to force an error + self.assertRaises(RuntimeError, bh.get_state, max_tries=0) + def test_set_state(self): state = { 0x70: {-1: 0x00}, @@ -205,11 +233,49 @@ def test_set_state_multibyte(self): # ----------------------------------------------------------------- # ----------------------------------------------------------------- with sut.BurstHandler(i2c_bus=i2c_bus, i2c_adr=0x70) as bh: - self.assertRaises(ValueError, bh.set_state, value=0x0102) + self.assertRaises(ValueError, bh.set_state, value=0x0102, byte_count=2) + + def test_set_state_insufficient_tries(self): + i2c_bus = sut.EmulatedI2C(state={}) + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + with sut.BurstHandler(i2c_bus=i2c_bus, i2c_adr=0x4C) as bh: + # under realistic circumstances the max_tries would be a positive + # value but we're intentionally setting it to 0 to force an error + self.assertRaises(RuntimeError, bh.set_state, value=0x12, max_tries=0) # --------------------------------------------------------------------- - def test_no_timeout(self): + def test_invalid_device_address(self): + # this code tests the equivalent of: + # with sut.BurstHandler(i2c_bus=i2c_bus, i2c_adr=0xFFFF) as bh: + # ... + i2c_bus = sut.EmulatedI2C(state={}) + bh = sut.BurstHandler(i2c_bus=i2c_bus, i2c_adr=0xFFFF) + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + self.assertRaises(ValueError, bh.__enter__) + + def test_invalid_device_register(self): + # this code tests the equivalent of: + # with sut.BurstHandler(i2c_bus=i2c_bus, i2c_adr=0xFFFF) as bh: + # ... + i2c_bus = sut.EmulatedI2C(state={}) + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + with sut.BurstHandler(i2c_bus=i2c_bus, i2c_adr=0x70) as bh: + self.assertRaises(ValueError, bh.read_register, register=0xFFFF) + + def test_invalid_timeout(self): + # this code tests the equivalent of: + # with sut.BurstHandler(i2c_bus=i2c_bus, i2c_adr=0x4C, timeout_ms=0) as bh: + # ... + i2c_bus = sut.EmulatedI2C(state={}, lock_chance=1) + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + self.assertRaises(ValueError, sut.BurstHandler, i2c_bus=i2c_bus, i2c_adr=0x4C, timeout_ms=0) + + def test_hard_to_lock(self): state = { 0x4C: { 0x00: 0x12, @@ -224,3 +290,13 @@ def test_no_timeout(self): expected = 0x12 # ----------------------------------------------------------------- self.assertEqual(computed, expected) + + def test_unable_to_lock(self): + # this code tests the equivalent of: + # with sut.BurstHandler(i2c_bus=i2c_bus, i2c_adr=0x4C) as bh: + # ... + i2c_bus = sut.EmulatedI2C(state={}, lock_chance=0) # impossible to acquire a lock + bh = sut.BurstHandler(i2c_bus=i2c_bus, i2c_adr=0x4C) + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + self.assertRaises(RuntimeError, bh.__enter__) diff --git a/tests/test_emulation.py b/tests/test_emulation.py new file mode 100755 index 0000000..ba7cf17 --- /dev/null +++ b/tests/test_emulation.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +""" +perform I²C bus related tests +""" + +import unittest + +import feeph.i2c as sut # sytem under test + + +# pylint: disable=protected-access +class TestEmulatedI2C(unittest.TestCase): + + def test_input_validation1(self): + i2c_bus = sut.EmulatedI2C(state={}) + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + self.assertRaises(ValueError, i2c_bus.readfrom_into, 0x12, [-1, 0x00]) + + def test_input_validation2(self): + i2c_bus = sut.EmulatedI2C(state={}) + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + self.assertRaises(ValueError, i2c_bus.writeto, 0x12, [-1, 0x00]) + + def test_input_validation3(self): + i2c_bus = sut.EmulatedI2C(state={}) + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + self.assertRaises(ValueError, i2c_bus.writeto_then_readfrom, 0x12, bytearray(0), [-1, 0x00]) + + def test_input_validation4(self): + i2c_bus = sut.EmulatedI2C(state={}) + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + self.assertRaises(ValueError, i2c_bus.writeto_then_readfrom, 0x12, [-1, 0x00], bytearray(0)) diff --git a/tox.ini b/tox.ini index 9968f4b..a0f1654 100644 --- a/tox.ini +++ b/tox.ini @@ -28,9 +28,7 @@ requires = env_list = lint type - coverage_clean py{310,311,312} - coverage_report ; pytest & coverage ; https://pytest-cov.readthedocs.io/en/latest/tox.html @@ -70,28 +68,14 @@ commands = [testenv] description = install pytest in a virtual environment and invoke it on the tests folder -depends = - {py310,py311,312}: coverage_clean - coverage_report: py310,py311,312 set_env = VIRTUALENV_DISCOVERY = pyenv deps = --global-option="$(python-config --includes)" -r requirements.txt --global-option="$(python-config --includes)" -r requirements-dev.txt -# generate a coverage report but do not mesurements and do not fail -# (we will combine all measurements into a single report later on) +# do not generate a coverage report from tox +# if we would the files would be relative to the tox virtual environments +# and prefixed with '.tox/py*/lib/python*/site-packages/' - which makes +# them interesting to look at but useless for consumption by other tools commands = - pytest --cov=feeph.i2c --cov-append --cov-report= --cov-fail-under=0 {posargs: tests/} - -[testenv:coverage_clean] -deps = coverage -skip_install = true -commands = - coverage erase - -[testenv:coverage_report] -deps = coverage -skip_install = true -commands = - coverage report --show-missing - coverage html + pytest --no-cov {posargs: tests/}