Skip to content

Commit 7ca9ffe

Browse files
authored
fix (azure-iot-device): PipelineNucleus connection state is now the single source of truth regarding connection (#1014)
* Removed the boolean `.connected` attribute being set on the `PipelineNucleus` by the `PipelineRootStage` * `.connected` is now a property that is derived from the `connection_state` attribute, which is set by the `ConnectionStateStage` * This is now the single source of truth for the connection state * Added additional testing fixtures to allow for easy mocking of the connection state * Note that changes made to the `ConnectionLockStage` are merely a stopgap until the entire stage can be removed
1 parent 6d08c98 commit 7ca9ffe

File tree

14 files changed

+610
-412
lines changed

14 files changed

+610
-412
lines changed

azure-iot-device/azure/iot/device/common/pipeline/pipeline_nucleus.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,26 @@
44
# license information.
55
# --------------------------------------------------------------------------
66

7+
from enum import Enum
8+
79

810
class PipelineNucleus(object):
911
"""Contains data and information shared across the pipeline"""
1012

1113
def __init__(self, pipeline_configuration):
1214
self.pipeline_configuration = pipeline_configuration
13-
self.connected = False
15+
self.connection_state = ConnectionState.DISCONNECTED
16+
17+
@property
18+
def connected(self):
19+
# Only return True if fully connected
20+
return self.connection_state is ConnectionState.CONNECTED
21+
22+
23+
class ConnectionState(Enum):
24+
CONNECTED = "CONNECTED" # Client is connected (as far as it knows)
25+
DISCONNECTED = "DISCONNECTED" # Client is disconnected
26+
CONNECTING = "CONNECTING" # Client is in the process of connecting
27+
DISCONNECTING = "DISCONNECTING" # Client is in the process of disconnecting
28+
REAUTHORIZING = "REAUTHORIZING" # Client is in the process of reauthorizing
29+
# NOTE: Reauthorizing is the process of doing a disconnect, then a connect at transport level

azure-iot-device/azure/iot/device/common/pipeline/pipeline_stages_base.py

Lines changed: 69 additions & 67 deletions
Large diffs are not rendered by default.

azure-iot-device/tests/common/pipeline/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@
99
arbitrary_op,
1010
fake_pipeline_thread,
1111
fake_non_pipeline_thread,
12+
pipeline_connected_mock,
13+
nucleus,
1214
)

azure-iot-device/tests/common/pipeline/fixtures.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from azure.iot.device.common.pipeline import (
99
pipeline_events_base,
1010
pipeline_ops_base,
11+
pipeline_nucleus,
1112
)
1213

1314

@@ -33,6 +34,46 @@ def arbitrary_op(mocker):
3334
return op
3435

3536

37+
@pytest.fixture
38+
def pipeline_connected_mock(mocker):
39+
"""This mock can have it's return value altered by any test to indicate whether or not the
40+
pipeline is connected (boolean).
41+
42+
Because this fixture is used by the nucleus fixture, and the nucleus is the single source of
43+
truth for connection, changing this fixture's return value will change the connection state
44+
of any other aspect of the pipeline (assuming it is using the nucleus fixture).
45+
46+
This has to be it's own fixture, because due to how PropertyMocks work, you can't access them
47+
on an instance of an object like you can, say, the mocked settings on a PipelineConfiguration
48+
"""
49+
p = mocker.PropertyMock()
50+
return p
51+
52+
53+
@pytest.fixture
54+
def nucleus(mocker, pipeline_connected_mock):
55+
"""This fixture can be used to configure stages. Connection status can be mocked
56+
via the above pipeline_connected_mock, but by default .connected will return a real value.
57+
This nucleus will also come configured with a mocked pipeline configuration, which can be
58+
overridden if necessary
59+
"""
60+
# Need to use a mock for pipeline config because we don't know
61+
# what type of config is being used since these are common
62+
nucleus = pipeline_nucleus.PipelineNucleus(pipeline_configuration=mocker.MagicMock())
63+
64+
# By default, set the connected mock to return the real connected value
65+
# (this can be overridden by changing the return value of pipeline_connected_mock)
66+
def dynamic_return():
67+
if not isinstance(pipeline_connected_mock.return_value, mocker.Mock):
68+
return pipeline_connected_mock.return_value
69+
return nucleus.connection_state is pipeline_nucleus.ConnectionState.CONNECTED
70+
71+
pipeline_connected_mock.side_effect = dynamic_return
72+
type(nucleus).connected = pipeline_connected_mock
73+
74+
return nucleus
75+
76+
3677
@pytest.fixture
3778
def fake_pipeline_thread():
3879
"""

azure-iot-device/tests/common/pipeline/test_pipeline_nucleus.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# --------------------------------------------------------------------------
66
import pytest
77
import logging
8-
from azure.iot.device.common.pipeline.pipeline_nucleus import PipelineNucleus
8+
from azure.iot.device.common.pipeline.pipeline_nucleus import PipelineNucleus, ConnectionState
99

1010

1111
logging.basicConfig(level=logging.DEBUG)
@@ -24,7 +24,39 @@ def test_pipeline_config(self, pipeline_config):
2424
nucleus = PipelineNucleus(pipeline_configuration=pipeline_config)
2525
assert nucleus.pipeline_configuration is pipeline_config
2626

27-
@pytest.mark.it("Instantiates with the 'connected' attribute set to False")
27+
@pytest.mark.it("Instantiates with the 'connection_state' attribute set to DISCONNECTED")
2828
def test_connected(self, pipeline_config):
2929
nucleus = PipelineNucleus(pipeline_config)
30-
assert nucleus.connected is False
30+
assert nucleus.connection_state is ConnectionState.DISCONNECTED
31+
32+
33+
@pytest.mark.describe("PipelineNucleus - PROPERTY .connected")
34+
class TestPipelineNucleusPROPERTYConnected(object):
35+
@pytest.fixture
36+
def nucleus(self, mocker):
37+
pl_cfg = mocker.MagicMock()
38+
return PipelineNucleus(pl_cfg)
39+
40+
@pytest.mark.it("Is a read-only property")
41+
def test_read_only(self, nucleus):
42+
with pytest.raises(AttributeError):
43+
nucleus.connected = False
44+
45+
@pytest.mark.it("Returns True if the '.connection_state' attribute is CONNECTED")
46+
def test_connected(self, nucleus):
47+
nucleus.connection_state = ConnectionState.CONNECTED
48+
assert nucleus.connected
49+
50+
@pytest.mark.it("Returns False if the '.connection_state' attribute has any other value")
51+
@pytest.mark.parametrize(
52+
"state",
53+
[
54+
ConnectionState.DISCONNECTED,
55+
ConnectionState.CONNECTING,
56+
ConnectionState.DISCONNECTING,
57+
ConnectionState.REAUTHORIZING,
58+
],
59+
)
60+
def test_not_connected(self, nucleus, state):
61+
nucleus.connection_state = state
62+
assert not nucleus.connected

0 commit comments

Comments
 (0)