Skip to content

Commit eb578e1

Browse files
authored
fix: improve code quality and testing procedure (#57)
2 parents feb9653 + fecc8b8 commit eb578e1

File tree

12 files changed

+266
-104
lines changed

12 files changed

+266
-104
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ coverage.xml
5050
.hypothesis/
5151
.pytest_cache/
5252
cover/
53+
coverage/
5354
tests/coverage_report/
5455

5556
# Translations

docs/CONTRIBUTING.md

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,33 @@ done
3737
pdm install
3838
```
3939

40-
### perform unit tests:
40+
### testing
41+
42+
The entire test suite (unit tests & compatibility tests) can be triggered
43+
by running
4144

4245
```SHELL
43-
pdm run pytest
46+
scripts/run_tests
4447
```
4548

46-
### perform compatibility tests:
49+
You have successfully run all tests when the output ends with:
4750

48-
```SHELL
49-
tox
5051
```
52+
----------------------------------------------------------------------
53+
If you reached this far you passed. Congratulations!
54+
----------------------------------------------------------------------
55+
```
56+
57+
If the unit tests have run successfully coverage file in LCOV format is
58+
generated and stored at `coverage/lcov.info`. This file can be used by
59+
your IDE to indicate code coverage from within your editor window.
60+
- We recommend the Visual Studio Code extension
61+
[Code Coverage](https://marketplace.visualstudio.com/items?itemName=markis.code-coverage).
62+
- Special care should be taken when modifying code that is not covered by unit tests!
63+
64+
Branches that do not pass the test harness (e.g. due to failing unit tests
65+
or lowering the code coverage beneath the desired threshold) should not be
66+
pull-requested.
5167

5268
### use the demo script
5369

feeph/i2c/burst_handler.py

100644100755
Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,18 @@ def read_register(self, register: int, byte_count: int = 1, max_tries: int = 5)
6666
buf_r = bytearray(byte_count)
6767
buf_r[0] = register
6868
buf_w = bytearray(byte_count)
69+
cur_try = 0
6970
for cur_try in range(1, 1 + max_tries):
7071
try:
7172
self._i2c_bus.writeto_then_readfrom(address=self._i2c_adr, buffer_out=buf_r, buffer_in=buf_w)
7273
return convert_bytearry_to_uint(buf_w)
73-
except OSError as e:
74+
# protect against sporadic errors on actual devices
75+
# (maybe we can do something to prevent these errors?)
76+
except (OSError, RuntimeError) as e:
7477
# [Errno 121] Remote I/O error
75-
LH.warning("[%s] Failed to read register 0x%02X (%i/%i): %s", __name__, register, cur_try, max_tries, e)
76-
time.sleep(0.001)
77-
except RuntimeError as e:
7878
LH.warning("[%s] Unable to read register 0x%02X (%i/%i): %s", __name__, register, cur_try, max_tries, e)
7979
time.sleep(0.001)
80-
else:
81-
raise RuntimeError(f"Unable to read register 0x{register:02X} after {cur_try} attempts. Giving up.")
80+
raise RuntimeError(f"Unable to read register 0x{register:02X} after {cur_try} attempts. Giving up.")
8281

8382
def write_register(self, register: int, value: int, byte_count: int = 1, max_tries: int = 3):
8483
"""
@@ -91,23 +90,21 @@ def write_register(self, register: int, value: int, byte_count: int = 1, max_tri
9190
"""
9291
_validate_register_address(register)
9392
ba = convert_uint_to_bytearry(value, byte_count)
94-
buf = bytearray(1 + len(ba))
95-
buf[0] = register
96-
for i in range(len(ba)):
97-
buf[1+i] = ba[i]
93+
buf = bytearray([register])
94+
for byte in ba:
95+
buf.append(byte)
96+
cur_try = 0
9897
for cur_try in range(1, 1 + max_tries):
9998
try:
10099
self._i2c_bus.writeto(address=self._i2c_adr, buffer=buf)
101100
return
102-
except OSError as e:
101+
# protect against sporadic errors on actual devices
102+
# (maybe we can do something to prevent these errors?)
103+
except (OSError, RuntimeError) as e:
103104
# [Errno 121] Remote I/O error
104-
LH.warning("[%s] Failed to read register 0x%02X (%i/%i): %s", __name__, register, cur_try, max_tries, e)
105+
LH.warning("[%s] Unable to write register 0x%02X (%i/%i): %s", __name__, register, cur_try, max_tries, e)
105106
time.sleep(0.1)
106-
except RuntimeError as e:
107-
LH.warning("[%s] Unable to read register 0x%02X (%i/%i): %s", __name__, register, cur_try, max_tries, e)
108-
time.sleep(0.1)
109-
else:
110-
raise RuntimeError(f"Unable to read register 0x{register:02X} after {cur_try} attempts. Giving up.")
107+
raise RuntimeError(f"Unable to read register 0x{register:02X} after {cur_try} attempts. Giving up.")
111108

112109
# it is unclear if it's possible to have a multi-byte state registers
113110
# (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:
124121
LH.warning("Multi byte reads are not implemented yet! Returning a single byte instead.")
125122
byte_count = 1
126123
buf = bytearray(byte_count)
124+
cur_try = 0
127125
for cur_try in range(1, 1 + max_tries):
128126
try:
129127
self._i2c_bus.readfrom_into(address=self._i2c_adr, buffer=buf)
130128
return buf[0]
131-
except OSError as e:
129+
# protect against sporadic errors on actual devices
130+
# (maybe we can do something to prevent these errors?)
131+
except (OSError, RuntimeError) as e:
132132
# [Errno 121] Remote I/O error
133-
LH.warning("[%s] Failed to read state (%i/%i): %s", __name__, cur_try, max_tries, e)
134-
time.sleep(0.001)
135-
except RuntimeError as e:
136133
LH.warning("[%s] Unable to read state (%i/%i): %s", __name__, cur_try, max_tries, e)
137134
time.sleep(0.001)
138-
else:
139-
raise RuntimeError(f"Unable to read state after {cur_try} attempts. Giving up.")
135+
raise RuntimeError(f"Unable to read state after {cur_try} attempts. Giving up.")
140136

141137
def set_state(self, value: int, byte_count: int = 1, max_tries: int = 3):
142138
"""
@@ -149,19 +145,18 @@ def set_state(self, value: int, byte_count: int = 1, max_tries: int = 3):
149145
LH.warning("Multi byte writes are not implemented yet! Returning a single byte instead.")
150146
byte_count = 1
151147
buf = convert_uint_to_bytearry(value, byte_count)
148+
cur_try = 0
152149
for cur_try in range(1, 1 + max_tries):
153150
try:
154151
self._i2c_bus.writeto(address=self._i2c_adr, buffer=buf)
155152
return
156-
except OSError as e:
153+
# protect against sporadic errors on actual devices
154+
# (maybe we can do something to prevent these errors?)
155+
except (OSError, RuntimeError) as e:
157156
# [Errno 121] Remote I/O error
158-
LH.warning("[%s] Failed to write state (%i/%i): %s", __name__, cur_try, max_tries, e)
157+
LH.warning("[%s] Unable to write state (%i/%i): %s", __name__, cur_try, max_tries, e)
159158
time.sleep(0.1)
160-
except RuntimeError as e:
161-
LH.warning("[%s] Unable to write state 0x%02X (%i/%i): %s", __name__, cur_try, max_tries, e)
162-
time.sleep(0.1)
163-
else:
164-
raise RuntimeError(f"Unable to write state after {cur_try} attempts. Giving up.")
159+
raise RuntimeError(f"Unable to write state after {cur_try} attempts. Giving up.")
165160

166161

167162
class BurstHandler:
@@ -182,6 +177,8 @@ def __init__(self, i2c_bus: busio.I2C, i2c_adr: int, timeout_ms: int | None = 50
182177
self._timeout_ms = timeout_ms
183178
else:
184179
raise ValueError("Provided timeout is not a positive integer or 'None'!")
180+
# register '_timestart_ns' - we will populate it later on
181+
self._timestart_ns = 0
185182

186183
def __enter__(self) -> BurstHandle:
187184
"""

feeph/i2c/emulation.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ class EmulatedI2C(busio.I2C):
2121
(e.g. duplicated registers with multiple addresses)
2222
"""
2323

24+
# We intentionally do not call super().init() because we don't want to
25+
# call any of the hardware initialization code. We are only interested
26+
# in being able to claim we are an instance of 'busio.I2C' in case
27+
# something else tries to validate that or has logic tied to it.
28+
# pylint: disable=super-init-not-called
2429
def __init__(self, state: dict[int, dict[int, int]], lock_chance: int = 100):
2530
"""
2631
initialize a simulated I2C bus
@@ -39,7 +44,7 @@ def __init__(self, state: dict[int, dict[int, int]], lock_chance: int = 100):
3944

4045
def try_lock(self) -> bool:
4146
# may randomly fail to acquire a lock
42-
return (random.randint(0, 100) < self._lock_chance)
47+
return random.randint(0, 100) < self._lock_chance
4348

4449
def unlock(self):
4550
pass
@@ -48,24 +53,41 @@ def unlock(self):
4853
# being read/written since the register address must be positive in the
4954
# range 0 ≤ x ≤ 255.
5055

51-
def readfrom_into(self, address, buffer, *, start=0, end=None, stop=True):
56+
# replicate the signature of busio.I2C
57+
# pylint: disable=too-many-arguments
58+
def readfrom_into(self, address: int, buffer: bytearray, *, start=0, end=None, stop=True):
5259
"""
5360
read device state
5461
5562
(buffer is used as an output parameter)
5663
"""
64+
# make sure that buffer truly is a bytearray
65+
# (we really want this to be a bytearray because bytearray performs
66+
# its own input validation and automatically guarantees we're not
67+
# getting negative byte values or other unexpected data as input)
68+
if not isinstance(buffer, bytearray):
69+
raise ValueError("buffer must be of type 'bytearray'")
5770
i2c_device_address = address
5871
i2c_device_register = -1
5972
value = self._state[i2c_device_address][i2c_device_register]
6073
ba = convert_uint_to_bytearry(value, len(buffer))
6174
# copy computed result to output parameter
75+
# pylint: disable=consider-using-enumerate
6276
for i in range(len(buffer)):
6377
buffer[i] = ba[i]
6478

79+
# replicate the signature of busio.I2C
80+
# pylint: disable=too-many-arguments
6581
def writeto(self, address: int, buffer: bytearray, *, start=0, end=None):
6682
"""
6783
write device state or register
6884
"""
85+
# make sure that buffer truly is a bytearray
86+
# (we really want this to be a bytearray because bytearray performs
87+
# its own input validation and automatically guarantees we're not
88+
# getting negative byte values or other unexpected data as input)
89+
if not isinstance(buffer, bytearray):
90+
raise ValueError("buffer must be of type 'bytearray'")
6991
if len(buffer) == 1:
7092
# device status
7193
i2c_device_address = address
@@ -75,8 +97,6 @@ def writeto(self, address: int, buffer: bytearray, *, start=0, end=None):
7597
# device register
7698
i2c_device_address = address
7799
i2c_device_register = buffer[0]
78-
if i2c_device_register < 0:
79-
raise ValueError("device register can't be negative")
80100
value = convert_bytearry_to_uint(buffer[1:])
81101
self._state[i2c_device_address][i2c_device_register] = value
82102

@@ -86,12 +106,19 @@ def writeto_then_readfrom(self, address: int, buffer_out: bytearray, buffer_in:
86106
87107
(buffer_in is used as an output parameter)
88108
"""
109+
# make sure that buffer_in and buffer_out truly are a bytearray
110+
# (we really want this to be a bytearray because bytearray performs
111+
# its own input validation and automatically guarantees we're not
112+
# getting negative byte values or other unexpected data as input)
113+
if not isinstance(buffer_in, bytearray):
114+
raise ValueError("buffer_in must be of type 'bytearray'")
115+
if not isinstance(buffer_out, bytearray):
116+
raise ValueError("buffer_out must be of type 'bytearray'")
89117
i2c_device_address = address
90118
i2c_device_register = buffer_out[0]
91-
if i2c_device_register < 0:
92-
raise ValueError("device register can't be negative")
93119
value = self._state[i2c_device_address][i2c_device_register]
94120
ba = convert_uint_to_bytearry(value, len(buffer_in))
95121
# copy computed result to output parameter
122+
# pylint: disable=consider-using-enumerate
96123
for i in range(len(buffer_in)):
97124
buffer_in[i] = ba[i]

feeph/i2c/utility.py

Lines changed: 0 additions & 32 deletions
This file was deleted.

pdm.lock

Lines changed: 32 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,14 @@ source-includes = ["tests/"]
9797
dev = [
9898
]
9999
tools = [
100-
"autopep8 ~= 2.2",
101-
"copier ~= 9.3",
102-
"flake8 ~= 7.0",
103-
"mypy ~= 1.10",
104-
"pylint ~= 3.2",
105-
"pytest-cov ~= 5.0",
106-
"pytest-sugar ~= 1.0",
100+
"autopep8 ~= 2.2",
101+
"copier ~= 9.3",
102+
"coverage-lcov ~= 0.3",
103+
"flake8 ~= 7.0",
104+
"mypy ~= 1.10",
105+
"pylint ~= 3.2",
106+
"pytest-cov ~= 5.0",
107+
"pytest-sugar ~= 1.0",
107108
]
108109

109110
# during development: fetch all packages in our namespace from TestPyPI

requirements-dev.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ astroid==3.2.4 \
1010
autopep8==2.3.1 \
1111
--hash=sha256:8d6c87eba648fdcfc83e29b788910b8643171c395d9c4bcf115ece035b9c9dda \
1212
--hash=sha256:a203fe0fcad7939987422140ab17a930f684763bf7335bdb6709991dd7ef6c2d
13+
click==8.1.7 \
14+
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
15+
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
1316
colorama==0.4.6 \
1417
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
1518
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
@@ -49,6 +52,9 @@ coverage==7.6.1 \
4952
--hash=sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959 \
5053
--hash=sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234 \
5154
--hash=sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc
55+
coverage-lcov==0.3.0 \
56+
--hash=sha256:0b352da0c72eacd555de2e10fa75ec165b8849ab6827218abc3ef6be2ec861f6 \
57+
--hash=sha256:23ae34a535f1ed3fa4cdf9682f6c1fe1a40aa98c94aa05614512dbf2da82e749
5258
coverage[toml]==7.6.1 \
5359
--hash=sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d \
5460
--hash=sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6 \

0 commit comments

Comments
 (0)