Skip to content
This repository was archived by the owner on Oct 11, 2023. It is now read-only.

Commit baa0660

Browse files
authored
Test Revisions for Requirements output (#101)
*Adjusted device-iothub client tests (except for auth - that's a future PR) *Adjusted device-common tests *Added additional tests *Made minor adjustments to implementation
1 parent 7f376b0 commit baa0660

18 files changed

+1257
-343
lines changed

azure-iot-device/azure/iot/device/common/async_adapter.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010

1111

1212
def emulate_async(fn):
13-
"""Apply as a decorator to emulate async behavior with a sync function/method
14-
via usage of multithreading.
13+
"""Returns a coroutine function that calls a given function with emulated asynchronous
14+
behavior via use of mulithreading.
15+
16+
Can be applied as a decorator.
1517
1618
:param fn: The sync function to be run in async.
1719
:returns: A coroutine function that will call the given sync function.

azure-iot-device/azure/iot/device/common/sastoken.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
import time
1212
import six.moves.urllib as urllib
1313

14-
__all__ = ["SasToken", "SasTokenError"]
15-
1614

1715
class SasTokenError(Exception):
1816
"""Error in SasToken"""
@@ -55,7 +53,7 @@ def __init__(self, uri, key, key_name=None, ttl=3600):
5553
self.ttl = ttl
5654
self.refresh()
5755

58-
def __repr__(self):
56+
def __str__(self):
5957
return self._token
6058

6159
def refresh(self):

azure-iot-device/azure/iot/device/iothub/aio/async_clients.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ def sync_callback():
243243

244244
callback = async_adapter.AwaitableCallback(sync_callback)
245245

246-
await send_output_event_async(message, callback)
246+
await send_output_event_async(message, callback=callback)
247247
await callback.completion()
248248

249249
async def receive_input_message(self, input_name):

azure-iot-device/azure/iot/device/iothub/models/message.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99

10+
# TODO: Revise this class. Does all of this REALLY need to be here?
1011
class Message(object):
1112
"""Represents a message to or from IoTHub
1213
@@ -52,3 +53,6 @@ def __init__(
5253
self.content_encoding = content_encoding
5354
self.content_type = content_type
5455
self.output_name = output_name
56+
57+
def __str__(self):
58+
return str(self.data)

azure-iot-device/azure/iot/device/iothub/sync_clients.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"""
99

1010
import logging
11-
from threading import Event
11+
import threading
1212
from .abstract_clients import (
1313
AbstractIoTHubClient,
1414
AbstractIoTHubDeviceClient,
@@ -66,7 +66,7 @@ def connect(self):
6666
"""
6767
logger.info("Connecting to Hub...")
6868

69-
connect_complete = Event()
69+
connect_complete = threading.Event()
7070

7171
def callback():
7272
connect_complete.set()
@@ -83,7 +83,7 @@ def disconnect(self):
8383
"""
8484
logger.info("Disconnecting from Hub...")
8585

86-
disconnect_complete = Event()
86+
disconnect_complete = threading.Event()
8787

8888
def callback():
8989
disconnect_complete.set()
@@ -108,7 +108,7 @@ def send_event(self, message):
108108
message = Message(message)
109109

110110
logger.info("Sending message to Hub...")
111-
send_complete = Event()
111+
send_complete = threading.Event()
112112

113113
def callback():
114114
send_complete.set()
@@ -155,7 +155,7 @@ def send_method_response(self, method_response):
155155
:type method_response: MethodResponse
156156
"""
157157
logger.info("Sending method response to Hub...")
158-
send_complete = Event()
158+
send_complete = threading.Event()
159159

160160
def callback():
161161
send_complete.set()
@@ -174,7 +174,7 @@ def _enable_feature(self, feature_name):
174174
See azure.iot.device.common.pipeline.constant for possible values
175175
"""
176176
logger.info("Enabling feature:" + feature_name + "...")
177-
enable_complete = Event()
177+
enable_complete = threading.Event()
178178

179179
def callback():
180180
enable_complete.set()
@@ -312,13 +312,13 @@ def send_to_output(self, message, output_name):
312312
message.output_name = output_name
313313

314314
logger.info("Sending message to output:" + output_name + "...")
315-
send_complete = Event()
315+
send_complete = threading.Event()
316316

317317
def callback():
318318
logger.info("Successfully sent message to output: " + output_name)
319319
send_complete.set()
320320

321-
self._pipeline.send_output_event(message, callback)
321+
self._pipeline.send_output_event(message, callback=callback)
322322
send_complete.wait()
323323

324324
def receive_input_message(self, input_name, block=True, timeout=None):

azure-iot-device/tests/common/test_async_adapter.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,16 @@ def mock_function(mocker, dummy_value):
2424
return mock_fn
2525

2626

27+
@pytest.mark.describe("emulate_async()")
2728
class TestEmulateAsync(object):
29+
@pytest.mark.it("Returns a coroutine function when given a function")
2830
async def test_returns_coroutine(self, mock_function):
2931
async_fn = async_adapter.emulate_async(mock_function)
3032
assert inspect.iscoroutinefunction(async_fn)
3133

32-
async def test_coroutine_has_input_function_docstring(self, mock_function):
33-
async_fn = async_adapter.emulate_async(mock_function)
34-
assert async_fn.__doc__ == mock_function.__doc__
35-
34+
@pytest.mark.it(
35+
"Returns a coroutine function that returns the result of the input function when called"
36+
)
3637
async def test_coroutine_returns_input_function_result(
3738
self, mocker, mock_function, dummy_value
3839
):
@@ -42,8 +43,34 @@ async def test_coroutine_returns_input_function_result(
4243
assert mock_function.call_args == mocker.call(dummy_value)
4344
assert result == mock_function.return_value
4445

46+
@pytest.mark.it("Copies the input function docstring to resulting coroutine function")
47+
async def test_coroutine_has_input_function_docstring(self, mock_function):
48+
async_fn = async_adapter.emulate_async(mock_function)
49+
assert async_fn.__doc__ == mock_function.__doc__
4550

51+
@pytest.mark.it("Can be applied as a decorator")
52+
async def test_applied_as_decorator(self):
53+
54+
# Define a function with emulate_async applied as a decorator
55+
@async_adapter.emulate_async
56+
def some_function():
57+
return "foo"
58+
59+
# Call the function as a coroutine
60+
result = await some_function()
61+
assert result == "foo"
62+
63+
64+
@pytest.mark.describe("AwaitableCallback")
4665
class TestAwaitableCallback(object):
66+
@pytest.mark.it("Instantiates from a provided callback function")
67+
async def test_instantiates(self, mock_function):
68+
callback = async_adapter.AwaitableCallback(mock_function)
69+
assert isinstance(callback, async_adapter.AwaitableCallback)
70+
71+
@pytest.mark.it(
72+
"Invokes the callback function associated with an instance and returns its result when a call is invoked the instance"
73+
)
4774
async def test_calling_object_calls_input_function_and_returns_result(
4875
self, mocker, mock_function
4976
):
@@ -53,27 +80,31 @@ async def test_calling_object_calls_input_function_and_returns_result(
5380
assert mock_function.call_args == mocker.call()
5481
assert result == mock_function.return_value
5582

83+
@pytest.mark.it("Completes the instance Future when a call is invoked on the instance")
5684
async def test_calling_object_completes_future(self, mock_function):
5785
callback = async_adapter.AwaitableCallback(mock_function)
5886
assert not callback.future.done()
5987
callback()
6088
await asyncio.sleep(0.1) # wait to give time to complete the callback
6189
assert callback.future.done()
6290

63-
async def test_can_be_called_using_args(self, mocker, mock_function):
91+
@pytest.mark.it("Can be called using positional arguments")
92+
async def test_can_be_called_using_positional_args(self, mocker, mock_function):
6493
callback = async_adapter.AwaitableCallback(mock_function)
6594
result = callback(1, 2, 3)
6695
assert mock_function.call_count == 1
6796
assert mock_function.call_args == mocker.call(1, 2, 3)
6897
assert result == mock_function.return_value
6998

70-
async def test_can_be_called_using_kwargs(self, mocker, mock_function):
99+
@pytest.mark.it("Can be called using explicit keyword arguments")
100+
async def test_can_be_called_using_explicit_kwargs(self, mocker, mock_function):
71101
callback = async_adapter.AwaitableCallback(mock_function)
72102
result = callback(a=1, b=2, c=3)
73103
assert mock_function.call_count == 1
74104
assert mock_function.call_args == mocker.call(a=1, b=2, c=3)
75105
assert result == mock_function.return_value
76106

107+
@pytest.mark.it("Can have its callback completion awaited upon")
77108
async def test_awaiting_completion_of_callback_returns_result(self, mock_function):
78109
callback = async_adapter.AwaitableCallback(mock_function)
79110
callback()

azure-iot-device/tests/common/test_asyncio_compat.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ async def coro():
2020
return coro
2121

2222

23+
@pytest.mark.describe("get_running_loop()")
2324
class TestGetRunningLoop(object):
25+
@pytest.mark.it("Returns the currently running Event Loop in Python 3.7 or higher")
2426
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Requires Python 3.7+")
2527
async def test_returns_currently_running_event_loop_(self, mocker, event_loop):
2628
spy_get_running_loop = mocker.spy(asyncio, "get_running_loop")
@@ -29,12 +31,16 @@ async def test_returns_currently_running_event_loop_(self, mocker, event_loop):
2931
assert spy_get_running_loop.call_count == 1
3032
assert spy_get_running_loop.call_args == mocker.call()
3133

34+
@pytest.mark.it(
35+
"Raises a RuntimeError if there is no running Event Loop in Python 3.7 or higher"
36+
)
3237
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Requires Python 3.7+")
3338
async def test_raises_runtime_error_if_no_running_event_loop(self, mocker):
3439
mocker.patch.object(asyncio, "get_running_loop", side_effect=RuntimeError)
3540
with pytest.raises(RuntimeError):
3641
asyncio_compat.get_running_loop()
3742

43+
@pytest.mark.it("Returns the currently running Event Loop in Python 3.6 or below")
3844
@pytest.mark.skipif(sys.version_info >= (3, 7), reason="Requires Python 3.6 or below")
3945
async def test_returns_currently_running_event_loop_py36orless_compat(self, mocker, event_loop):
4046
spy_get_event_loop = mocker.spy(asyncio, "_get_running_loop")
@@ -43,14 +49,21 @@ async def test_returns_currently_running_event_loop_py36orless_compat(self, mock
4349
assert spy_get_event_loop.call_count == 1
4450
assert spy_get_event_loop.call_args == mocker.call()
4551

52+
@pytest.mark.it(
53+
"Raises a RuntimeError if there is no running Event Loop in Python 3.6 or below"
54+
)
4655
@pytest.mark.skipif(sys.version_info >= (3, 7), reason="Requires Python 3.6 or below")
4756
async def test_raises_runtime_error_if_no_running_event_loop_py36orless_compat(self, mocker):
4857
mocker.patch.object(asyncio, "_get_running_loop", return_value=None)
4958
with pytest.raises(RuntimeError, message="Expecting Runtime Error"):
5059
asyncio_compat.get_running_loop()
5160

5261

62+
@pytest.mark.describe("create_task()")
5363
class TestCreateTask(object):
64+
@pytest.mark.it(
65+
"Returns a Task that wraps a given coroutine, and schedules its execution, in Python 3.7 or higher"
66+
)
5467
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Requires Python 3.7+")
5568
async def test_returns_task_wrapping_given_coroutine(self, mocker, dummy_coroutine):
5669
spy_create_task = mocker.spy(asyncio, "create_task")
@@ -60,6 +73,9 @@ async def test_returns_task_wrapping_given_coroutine(self, mocker, dummy_corouti
6073
assert spy_create_task.call_count == 1
6174
assert spy_create_task.call_args == mocker.call(coro_obj)
6275

76+
@pytest.mark.it(
77+
"Returns a Task that wraps a given coroutine, and schedules its execution, in Python 3.6 or below"
78+
)
6379
@pytest.mark.skipif(sys.version_info >= (3, 7), reason="Requires Python 3.6 or below")
6480
async def test_returns_task_wrapping_given_coroutine_py36orless_compat(
6581
self, mocker, dummy_coroutine
@@ -72,7 +88,11 @@ async def test_returns_task_wrapping_given_coroutine_py36orless_compat(
7288
assert spy_ensure_future.call_args == mocker.call(coro_obj)
7389

7490

91+
@pytest.mark.describe("create_future()")
7592
class TestCreateFuture(object):
93+
@pytest.mark.it(
94+
"Returns a new Future object attached to the given Event Loop, in Python 3.5.2 or higher"
95+
)
7696
@pytest.mark.skipif(sys.version_info < (3, 5, 2), reason="Requires Python 3.5.2+")
7797
async def test_create_future_for_given_loop(self, mocker, event_loop):
7898
spy_create_future = mocker.spy(event_loop, "create_future")
@@ -82,6 +102,9 @@ async def test_create_future_for_given_loop(self, mocker, event_loop):
82102
assert spy_create_future.call_count == 1
83103
assert spy_create_future.call_args == mocker.call()
84104

105+
@pytest.mark.it(
106+
"Returns a new Future object attached to the given Event Loop, in Python 3.5.1 or below"
107+
)
85108
@pytest.mark.skipif(sys.version_info >= (3, 5, 1), reason="Requires Python 3.5.1 or below")
86109
async def test_create_future_for_given_loop_py351orless_compat(self, mocker, event_loop):
87110
spy_future = mocker.spy(asyncio, "Future")

0 commit comments

Comments
 (0)