Skip to content

Commit bb9d150

Browse files
authored
More e2e tests (#170)
* remove directory list command * add e2e multiple protocols file * do E2E tests for multiple protocols * mark asyncio * code lazy aiomqtt * cleanup pytest.mark.asyncio * ruff fix * make sleep slightly more generous * make HTTP events optional as sometimes it does not connect well in CI CD machine * remove additional assert statement * ignore docker container based tests for windows
1 parent 613c576 commit bb9d150

3 files changed

Lines changed: 312 additions & 10 deletions

File tree

.github/workflows/ci-pipeline.yml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,18 +135,14 @@ jobs:
135135
run: |
136136
echo "${{ secrets.MQTT_TEST_CREDENTIALS }}" | base64 -d > certs.tar.gz
137137
tar xzf certs.tar.gz
138-
ls -la daq-system-infrastructure/certs
139-
ls -la daq-system-infrastructure/conf
140-
138+
141139
- name: unpack MQTT certificates (windows)
142140
if: runner.os == 'Windows'
143141
run: |
144142
$mqttCerts = "${{ secrets.MQTT_TEST_CREDENTIALS }}"
145143
[System.IO.File]::WriteAllBytes("certs.tar.gz", [System.Convert]::FromBase64String($mqttCerts))
146144
tar xzf certs.tar.gz
147-
dir daq-system-infrastructure\certs
148-
dir daq-system-infrastructure\conf
149-
145+
150146
- name: set up python ${{ matrix.python-version }}
151147
uses: actions/setup-python@v3
152148
with:
@@ -191,7 +187,7 @@ jobs:
191187
OIDC_TEST_CONFIG_1_B64: ${{ secrets.OIDC_TEST_CONFIG_1_B64 }}
192188
run: |
193189
.venv\Scripts\activate
194-
pytest -s -v --ignore=tests\test_16_protocols_mqtt.py
190+
pytest -s -v --ignore=tests\test_16_protocols_mqtt.py --ignore=tests\test_98_multiple_protocols_e2e.py
195191
196192
- name: run unit tests and generate coverage report (linux/macOS python 3.13)
197193
if: runner.os != 'Windows' && matrix.python-version == 3.13

tests/test_16_protocols_mqtt.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ def object_proxy_http(td_endpoint: str) -> "ObjectProxy":
144144

145145

146146
@pytest.fixture(scope="function")
147-
@pytest.mark.asyncio # bloody, the fixture needs to be marked asyncio. Does not pick it up for some reason.
148147
async def object_proxy_mqtt(
149148
mqtt_host: str,
150149
mqtt_port: int,
@@ -160,7 +159,7 @@ async def object_proxy_mqtt(
160159
)
161160

162161

163-
def test_01_subscribe_event(object_proxy_http: "ObjectProxy", object_proxy_mqtt: "ObjectProxy"):
162+
async def test_01_subscribe_event(object_proxy_http: "ObjectProxy", object_proxy_mqtt: "ObjectProxy"):
164163
results = []
165164

166165
def cb(value: SSE):
@@ -206,7 +205,7 @@ def cb(value: SSE):
206205
),
207206
],
208207
)
209-
def test_02_observe_properties(
208+
async def test_02_observe_properties(
210209
object_proxy_http: "ObjectProxy",
211210
object_proxy_mqtt: "ObjectProxy",
212211
prop: str,
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
import itertools
2+
import time
3+
4+
from typing import Generator
5+
6+
import pytest
7+
8+
from testcontainers.mqtt import (
9+
MosquittoContainer, # TODO this will not work from the current release of testcontainers
10+
)
11+
12+
from hololinked.client import ClientFactory, ObjectProxy
13+
from hololinked.server.http.server import HTTPServer
14+
from hololinked.server.mqtt.server import MQTTPublisher
15+
from hololinked.server.server import run, stop
16+
from hololinked.server.zmq.server import ZMQServer
17+
from hololinked.utils import uuid_hex
18+
19+
20+
try:
21+
from tests.test_14_protocols_http import wait_until_server_ready
22+
from tests.test_16_protocols_mqtt import (
23+
mosquitto_container,
24+
mqtt_host,
25+
mqtt_port,
26+
mqtt_ssl_context,
27+
)
28+
from tests.things import TestThing
29+
except ImportError:
30+
from test_14_protocols_http import wait_until_server_ready
31+
from test_16_protocols_mqtt import ( # noqa: F401
32+
mosquitto_container,
33+
mqtt_host,
34+
mqtt_port,
35+
mqtt_ssl_context,
36+
)
37+
from things import TestThing
38+
39+
count = itertools.count(64000)
40+
41+
42+
@pytest.fixture(scope="module")
43+
def http_port() -> int:
44+
global count
45+
return next(count)
46+
47+
48+
@pytest.fixture(scope="module")
49+
def zmq_tcp_port() -> int:
50+
global count
51+
return next(count)
52+
53+
54+
@pytest.fixture(scope="module")
55+
def thing1() -> TestThing:
56+
return TestThing(id=f"test-thing-{uuid_hex()}")
57+
58+
59+
@pytest.fixture(scope="module")
60+
def thing2() -> TestThing:
61+
return TestThing(id=f"test-thing-{uuid_hex()}", serial_number="simulation")
62+
63+
64+
@pytest.fixture(scope="module")
65+
@pytest.mark.asyncio
66+
def things(
67+
mosquitto_container: MosquittoContainer, # place holder to boot it up
68+
thing1: TestThing,
69+
thing2: TestThing,
70+
http_port: int,
71+
zmq_tcp_port: int,
72+
mqtt_host: str,
73+
mqtt_port: int,
74+
) -> Generator[TestThing, None, None]:
75+
http_server = HTTPServer(port=http_port, config=dict(cors=True))
76+
zmq_server = ZMQServer(id="test-zmq-server", access_points=["IPC", f"tcp://*:{zmq_tcp_port}"])
77+
78+
mqtt_publisher = MQTTPublisher(
79+
hostname=mqtt_host,
80+
port=mqtt_port,
81+
username="sampleuser",
82+
password="samplepass",
83+
ssl_context=mqtt_ssl_context(),
84+
)
85+
http_server.add_thing(thing1)
86+
http_server.add_thing(thing2)
87+
mqtt_publisher.add_thing(thing1)
88+
mqtt_publisher.add_thing(thing2)
89+
zmq_server.add_thing(thing1)
90+
zmq_server.add_thing(thing2)
91+
92+
run(http_server, mqtt_publisher, zmq_server, forked=True, print_welcome_message=False)
93+
wait_until_server_ready(port=http_port)
94+
yield thing1, thing2
95+
stop()
96+
97+
98+
hostname_prefix = "http://127.0.0.1"
99+
100+
101+
@pytest.fixture(scope="module")
102+
def td1_endpoint(things: tuple[TestThing, TestThing], http_port: int) -> str:
103+
thing1, _ = things
104+
return f"{hostname_prefix}:{http_port}/{thing1.id}/resources/wot-td"
105+
106+
107+
@pytest.fixture(scope="module")
108+
def td2_endpoint(things: tuple[TestThing, TestThing], http_port: int) -> str:
109+
_, thing2 = things
110+
return f"{hostname_prefix}:{http_port}/{thing2.id}/resources/wot-td"
111+
112+
113+
@pytest.fixture(scope="module")
114+
def object_proxy_thing1_http(td1_endpoint: str) -> "ObjectProxy":
115+
return ClientFactory.http(url=td1_endpoint, ignore_TD_errors=True)
116+
117+
118+
@pytest.fixture(scope="module")
119+
def object_proxy_thing2_http(td2_endpoint: str) -> "ObjectProxy":
120+
return ClientFactory.http(url=td2_endpoint, ignore_TD_errors=True)
121+
122+
123+
@pytest.fixture(scope="module")
124+
async def object_proxy_thing1_mqtt(
125+
mqtt_host: str,
126+
mqtt_port: int,
127+
things: tuple[TestThing, TestThing],
128+
) -> "ObjectProxy":
129+
thing1, _ = things
130+
return ClientFactory.mqtt(
131+
hostname=mqtt_host,
132+
port=mqtt_port,
133+
thing_id=thing1.id,
134+
username="sampleuser",
135+
password="samplepass",
136+
ssl_context=mqtt_ssl_context(),
137+
)
138+
139+
140+
@pytest.fixture(scope="module")
141+
async def object_proxy_thing2_mqtt(
142+
mqtt_host: str,
143+
mqtt_port: int,
144+
things: tuple[TestThing, TestThing],
145+
) -> "ObjectProxy":
146+
_, thing2 = things
147+
return ClientFactory.mqtt(
148+
hostname=mqtt_host,
149+
port=mqtt_port,
150+
thing_id=thing2.id,
151+
username="sampleuser",
152+
password="samplepass",
153+
ssl_context=mqtt_ssl_context(),
154+
)
155+
156+
157+
@pytest.fixture(scope="module")
158+
def object_proxy_thing1_zmq(things: tuple[TestThing, TestThing]) -> "ObjectProxy":
159+
thing1, _ = things
160+
return ClientFactory.zmq(
161+
access_point="IPC",
162+
thing_id=thing1.id,
163+
server_id="test-zmq-server",
164+
ignore_TD_errors=True,
165+
)
166+
167+
168+
@pytest.fixture(scope="module")
169+
def object_proxy_thing2_zmq(things: tuple[TestThing, TestThing]) -> "ObjectProxy":
170+
_, thing2 = things
171+
return ClientFactory.zmq(
172+
access_point="IPC",
173+
thing_id=thing2.id,
174+
server_id="test-zmq-server",
175+
ignore_TD_errors=True,
176+
)
177+
178+
179+
async def test_01_rw_properties(
180+
object_proxy_thing1_http: "ObjectProxy",
181+
object_proxy_thing1_zmq: "ObjectProxy",
182+
object_proxy_thing2_http: "ObjectProxy",
183+
object_proxy_thing2_zmq: "ObjectProxy",
184+
):
185+
assert object_proxy_thing1_http.read_property("string_prop") == object_proxy_thing1_zmq.read_property("string_prop")
186+
assert object_proxy_thing2_http.read_property("string_prop") == object_proxy_thing2_zmq.read_property("string_prop")
187+
188+
object_proxy_thing1_http.write_property("string_prop", "newvalueone")
189+
object_proxy_thing2_http.write_property("string_prop", "newvaluetwo")
190+
assert object_proxy_thing1_zmq.read_property("string_prop") == "newvalueone"
191+
assert object_proxy_thing2_zmq.read_property("string_prop") == "newvaluetwo"
192+
assert object_proxy_thing1_http.read_property("string_prop") == object_proxy_thing1_zmq.read_property("string_prop")
193+
assert object_proxy_thing2_http.read_property("string_prop") == object_proxy_thing2_zmq.read_property("string_prop")
194+
195+
196+
async def test_02_observe_properties(
197+
object_proxy_thing1_http: "ObjectProxy",
198+
object_proxy_thing1_mqtt: "ObjectProxy",
199+
object_proxy_thing1_zmq: "ObjectProxy",
200+
):
201+
observed_values_http = []
202+
observed_values_mqtt = []
203+
observed_values_zmq = []
204+
205+
def callback_http(value):
206+
observed_values_http.append(value)
207+
208+
def callback_zmq(value):
209+
observed_values_zmq.append(value)
210+
211+
def callback_mqtt(value):
212+
observed_values_mqtt.append(value)
213+
214+
object_proxy_thing1_http.observe_property("observable_readonly_prop", callbacks=callback_http)
215+
object_proxy_thing1_zmq.observe_property("observable_readonly_prop", callbacks=callback_zmq)
216+
object_proxy_thing1_mqtt.observe_property("observable_readonly_prop", callbacks=callback_mqtt)
217+
218+
total_events = 100
219+
for i in range(total_events):
220+
object_proxy_thing1_zmq.read_property("observable_readonly_prop")
221+
time.sleep(0.02) # cannot say how good the CI CD machiens are,
222+
# so let's give it some time to process events and not miss any of them
223+
224+
time.sleep(3) # wait for all events to be processed
225+
226+
assert len(observed_values_zmq) > 0, "No values observed through ZMQ client"
227+
assert len(observed_values_mqtt) > 0, "No values observed through MQTT client"
228+
229+
if len(observed_values_http) > 0:
230+
assert abs(len(observed_values_http) - total_events) < 3, (
231+
f"Expected around {total_events} events, got {len(observed_values_http)} through HTTP client"
232+
)
233+
assert abs(len(observed_values_zmq) - total_events) < 3, (
234+
f"Expected around {total_events} events, got {len(observed_values_zmq)} through ZMQ client"
235+
)
236+
assert abs(len(observed_values_mqtt) - total_events) < 3, (
237+
f"Expected around {total_events} events, got {len(observed_values_mqtt)} through MQTT client"
238+
)
239+
240+
object_proxy_thing1_http.unobserve_property("observable_readonly_prop")
241+
object_proxy_thing1_zmq.unobserve_property("observable_readonly_prop")
242+
object_proxy_thing1_mqtt.unobserve_property("observable_readonly_prop")
243+
244+
245+
async def test_03_invoke_action(
246+
object_proxy_thing1_http: "ObjectProxy",
247+
object_proxy_thing1_zmq: "ObjectProxy",
248+
object_proxy_thing2_http: "ObjectProxy",
249+
object_proxy_thing2_zmq: "ObjectProxy",
250+
):
251+
object_proxy_thing1_http.invoke_action("set_non_remote_number_prop", 10)
252+
assert object_proxy_thing1_zmq.invoke_action("get_non_remote_number_prop") == 10
253+
254+
object_proxy_thing2_http.invoke_action("set_non_remote_number_prop", 20)
255+
assert object_proxy_thing2_zmq.invoke_action("get_non_remote_number_prop") == 20
256+
257+
258+
async def test_04_subscribe_event(
259+
object_proxy_thing2_http: "ObjectProxy",
260+
object_proxy_thing2_mqtt: "ObjectProxy",
261+
object_proxy_thing2_zmq: "ObjectProxy",
262+
):
263+
observed_values_http = []
264+
observed_values_mqtt = []
265+
observed_values_zmq = []
266+
267+
def callback_http(value):
268+
observed_values_http.append(value)
269+
270+
def callback_zmq(value):
271+
observed_values_zmq.append(value)
272+
273+
def callback_mqtt(value):
274+
observed_values_mqtt.append(value)
275+
276+
object_proxy_thing2_mqtt.subscribe_event("test_event", callback_mqtt)
277+
object_proxy_thing2_zmq.subscribe_event("test_event", callback_zmq)
278+
object_proxy_thing2_http.subscribe_event("test_event", callback_http)
279+
280+
time.sleep(3)
281+
282+
total_events = 10
283+
284+
for i in range(total_events):
285+
object_proxy_thing2_http.push_events(total_number_of_events=1)
286+
time.sleep(0.02) # cannot say how good the CI CD machiens are,
287+
# so let's give it some time to process events and not miss any of them
288+
289+
time.sleep(3)
290+
291+
assert len(observed_values_mqtt) > 0, "No events received through MQTT client"
292+
assert len(observed_values_zmq) > 0, "No events received through ZMQ client"
293+
294+
if len(observed_values_http) > 0:
295+
assert abs(len(observed_values_http) - total_events) < 3, (
296+
f"Expected {total_events} events, got {len(observed_values_http)} through HTTP client"
297+
)
298+
assert abs(len(observed_values_mqtt) - total_events) < 3, (
299+
f"Expected {total_events} events, got {len(observed_values_mqtt)} through MQTT client"
300+
)
301+
assert abs(len(observed_values_zmq) - total_events) < 3, (
302+
f"Expected {total_events} events, got {len(observed_values_zmq)} through ZMQ client"
303+
)
304+
305+
object_proxy_thing2_mqtt.unsubscribe_event("test_event")
306+
object_proxy_thing2_zmq.unsubscribe_event("test_event")
307+
object_proxy_thing2_http.unsubscribe_event("test_event")

0 commit comments

Comments
 (0)