Skip to content

Commit c35a5f2

Browse files
authored
Merge pull request #64 from LabNConsulting/chopps/watchlog-and-retry-assert-ok
Watchlog and @Retry features
2 parents ab9ea60 + 3a6a004 commit c35a5f2

File tree

4 files changed

+102
-17
lines changed

4 files changed

+102
-17
lines changed

munet/testing/util.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ def pause_test(desc=""):
5959
asyncio.run(async_pause_test(desc))
6060

6161

62-
def retry(retry_timeout, initial_wait=0, retry_sleep=2, expected=True):
62+
def retry(
63+
retry_timeout, initial_wait=0, retry_sleep=2, expected=True, assert_is_except=True
64+
):
6365
"""Retry decorated function until it returns None, raises an exception, or timeout.
6466
6567
* `retry_timeout`: Retry for at least this many seconds; after waiting
@@ -68,12 +70,19 @@ def retry(retry_timeout, initial_wait=0, retry_sleep=2, expected=True):
6870
* `retry_sleep`: The time to sleep between retries.
6971
* `expected`: if False then the return logic is inverted, except for exceptions,
7072
(i.e., a non None ends the retry loop, and returns that value)
73+
* `assert_is_except`: If True (the default) then an AssertionError raised by the
74+
wrapped function will be treated as an excpetion. If False then
75+
an assertion raised by the wrapped fucntion is treated as
76+
non-None result it is treated as an exception. This is
77+
important for handling the expected=False case. Exceptions are
78+
always treated as failures even when expected is False.
7179
"""
7280

7381
def _retry(func):
7482
@functools.wraps(func)
7583
def func_retry(*args, **kwargs):
7684
# Allow the wrapped function's args to override the fixtures
85+
_assert_is_except = kwargs.pop("assert_is_except", assert_is_except)
7786
_retry_sleep = float(kwargs.pop("retry_sleep", retry_sleep))
7887
_retry_timeout = kwargs.pop("retry_timeout", retry_timeout)
7988
_expected = kwargs.pop("expected", expected)
@@ -90,13 +99,19 @@ def func_retry(*args, **kwargs):
9099
seconds_left = (retry_until - datetime.datetime.now()).total_seconds()
91100
try:
92101
try:
93-
ret = func(*args, seconds_left=seconds_left, **kwargs)
94-
except TypeError as error:
95-
if "seconds_left" not in str(error):
102+
try:
103+
ret = func(*args, seconds_left=seconds_left, **kwargs)
104+
except TypeError as error:
105+
if "seconds_left" not in str(error):
106+
raise
107+
ret = func(*args, **kwargs)
108+
except AssertionError as error:
109+
if _assert_is_except:
96110
raise
97-
ret = func(*args, **kwargs)
98-
99-
logging.debug("Function returned %s", ret)
111+
logging.info('Function returned assertion: "%s"', error)
112+
ret = error
113+
else:
114+
logging.debug("Function returned %s", ret)
100115

101116
positive_result = ret is None
102117
if _expected == positive_result:

munet/watchlog.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
class MatchFoundError(Exception):
1717
"""An error raised when a match is not found."""
18+
1819
def __init__(self, watchlog, match):
1920
self.watchlog = watchlog
2021
self.match = match
@@ -139,12 +140,16 @@ def from_mark(self, mark=None):
139140
140141
If ``mark`` is None then return content since last ``set_mark`` was called.
141142
143+
If the file has been replaced (inode changes) then the marks will reset to 0.
144+
142145
Args:
143146
mark: the mark in the content to return file content from.
144147
145148
Return:
146149
returns the content between ``mark`` and the end of content.
147150
"""
151+
if mark is None:
152+
mark = self.last_user_mark
148153
return self.content[mark:]
149154

150155
def set_mark(self):
@@ -156,8 +161,8 @@ def set_mark(self):
156161
def snapshot(self):
157162
"""Update the file content and return new text.
158163
159-
Returns any new text added since the last snapshot,
160-
also updates the snapshot mark.
164+
Returns any new text added since the last snapshot and updates the snapshot
165+
mark.
161166
162167
Return:
163168
Newly added text.
@@ -168,3 +173,15 @@ def snapshot(self):
168173
last_mark = self.last_snap_mark
169174
self.last_snap_mark = len(self.content)
170175
return self.content[last_mark:]
176+
177+
def snapshot_refresh(self):
178+
"""Update the file content and return the snapshot with this new content.
179+
180+
If the file has been replaced (inode changes) then the marks will reset to 0.
181+
182+
Return:
183+
returns the content from the last snapshot plus any new content
184+
added since ``snapshot()`` was called.
185+
"""
186+
self.update_content()
187+
return self.from_mark(self.last_snap_mark)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "munet"
3-
version = "0.15.5"
3+
version = "0.15.6"
44
description = "A package to facilitate network simulations"
55
authors = ["Christian Hopps <chopps@labn.net>"]
66
license = "GPL-2.0-or-later"

tests/testing/test_testing.py

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,22 @@ def test_stepfunction(stepf):
2727
stepf("the second step")
2828

2929

30+
# Assert twice then succeed
3031
@retry(retry_timeout=2, retry_sleep=0.1)
3132
def retrying_test_assert():
3233
if not hasattr(retrying_test_assert, "count"):
3334
retrying_test_assert.count = 0
3435
else:
3536
retrying_test_assert.count += 1
3637
assert retrying_test_assert.count == 2, "count not 2"
37-
retrying_test_assert.count = 0
3838

3939

40+
def test_retry_assert(caplog):
41+
retrying_test_assert()
42+
assert caplog.text.count("Sleeping") == 2
43+
44+
45+
# Fail twice then succeed
4046
@retry(retry_timeout=2, retry_sleep=0.1)
4147
def retrying_test_string():
4248
if not hasattr(retrying_test_string, "count"):
@@ -53,24 +59,71 @@ def test_retry_string(caplog):
5359
assert caplog.text.count("Sleeping") == 2
5460

5561

56-
def test_retry_fail(caplog):
62+
# Succeed twice then fail
63+
@retry(retry_timeout=2, retry_sleep=0.1)
64+
def retrying_test_fail_first():
65+
if not hasattr(retrying_test_fail_first, "count"):
66+
retrying_test_fail_first.count = 0
67+
else:
68+
retrying_test_fail_first.count += 1
69+
if retrying_test_fail_first.count != 2:
70+
return None
71+
return "count is 2"
72+
73+
74+
def test_retry_expected_fail_first(caplog):
75+
retrying_test_fail_first(expected=False)
76+
assert caplog.text.count("Sleeping") == 2
77+
78+
79+
def test_retry_expected_fail(caplog):
5780
@retry(retry_timeout=1, retry_sleep=0.1)
5881
def retrying_fail():
5982
return "Fail"
6083

61-
retrying_fail(expected=False)
84+
ret = retrying_fail(expected=False)
85+
assert ret == "Fail"
6286
assert caplog.text.count("Sleeping") == 0
6387

6488

65-
def test_retry_assert_fail(caplog):
89+
def test_retry_assert_exception(caplog):
6690
@retry(retry_timeout=1, retry_sleep=0.2)
6791
def retrying_assert():
6892
assert False, "Fail"
6993

7094
try:
71-
# Expected does not consider assert an expected failure
95+
# Expected does not consider assert an expected failure by default
7296
retrying_assert(expected=False)
7397
except AssertionError:
74-
assert caplog.text.count("Sleeping") == 5
98+
# Should have retried before ultimately failing
99+
assert caplog.text.count("Sleeping") > 0
100+
else:
101+
assert False, "Failed b/c no exception raised"
102+
103+
104+
def test_retry_assert_expected(caplog):
105+
@retry(retry_timeout=1, retry_sleep=0.2, assert_is_except=False)
106+
def retrying_assert():
107+
assert False, "Fail"
108+
109+
# Expected does not consider assert an expected failure by default
110+
ret = retrying_assert(expected=False)
111+
assert isinstance(ret, AssertionError)
112+
assert caplog.text.count("Sleeping") == 0
113+
114+
115+
# Succeed twice then assert
116+
@retry(retry_timeout=2, retry_sleep=0.1, assert_is_except=False)
117+
def retrying_test_fail_first_assert():
118+
if not hasattr(retrying_test_fail_first_assert, "count"):
119+
retrying_test_fail_first_assert.count = 0
75120
else:
76-
assert False, "Failed b/c succeeded"
121+
retrying_test_fail_first_assert.count += 1
122+
assert retrying_test_fail_first_assert.count != 2, "count is 2"
123+
124+
125+
def test_retry_assert_expected_succeed_first(caplog):
126+
# Expected does not consider assert an expected failure by default
127+
ret = retrying_test_fail_first_assert(expected=False)
128+
assert isinstance(ret, AssertionError)
129+
assert caplog.text.count("Sleeping") == 2

0 commit comments

Comments
 (0)