Skip to content

Commit 5ebfcfa

Browse files
committed
improve coverage
1 parent 4abc905 commit 5ebfcfa

File tree

2 files changed

+79
-11
lines changed

2 files changed

+79
-11
lines changed

unpythonic/dynassign.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ def __init__(self):
5555
super().__init__(self)
5656
self._refresh()
5757
_getobservers()[id(self)] = self
58-
def __del__(self):
58+
# TODO: __del__ most certainly runs during test_dynassign (as can be
59+
# evidenced by placing a debug print inside it), but coverage fails
60+
# to report it as covered.
61+
def __del__(self): # pragma: no cover
5962
# No idea how, but our REPL server can trigger a KeyError here
6063
# if the user views `help()`, which causes the client to get stuck.
6164
# Then pressing `q` in the server console to quit the help, and then

unpythonic/test/test_dynassign.py

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
11
# -*- coding: utf-8 -*-
22

33
from ..syntax import macros, test, test_raises # noqa: F401
4-
from .fixtures import session, testset
4+
from .fixtures import session, testset, returns_normally
55

66
import threading
77
from queue import Queue
8+
import gc
89

910
from ..dynassign import dyn, make_dynvar
11+
from ..misc import slurp
1012

1113
def runtests():
14+
# various parts of unpythonic use dynvars, so get what's there before we insert anything for testing
15+
implicits = [k for k in dyn]
16+
def noimplicits(kvs):
17+
return tuple(sorted((k, v) for k, v in kvs if k not in implicits))
18+
def noimplicits_keys(keys):
19+
return tuple(sorted(k for k in keys if k not in implicits))
20+
21+
# some test data
22+
D = {"a": 1, "b": 2}
23+
D2 = {"c": 3, "d": 4}
24+
1225
def f():
1326
test[dyn.a == 2] # no a in lexical scope
1427

15-
def runtest():
28+
def basictests():
1629
with testset("basic usage"):
1730
with dyn.let(a=2, b="foo"):
1831
test[dyn.a == 2]
@@ -46,14 +59,9 @@ def threadtest(q):
4659
t2.join()
4760
err = comm.get()
4861
test[err is not None]
49-
runtest()
62+
basictests()
5063

5164
with testset("syntactic sugar"):
52-
# various parts of unpythonic use dynvars, so get what's there before we insert anything for testing
53-
implicits = [k for k in dyn]
54-
def noimplicits(kvs):
55-
return tuple(sorted((k, v) for k, v in kvs if k not in implicits))
56-
D = {"a": 1, "b": 2}
5765
with dyn.let(**D):
5866
# membership test
5967
test["a" in dyn]
@@ -66,7 +74,6 @@ def noimplicits(kvs):
6674
test[noimplicits(dyn.items()) == ()]
6775

6876
with testset("update existing bindings"):
69-
D2 = {"c": 3, "d": 4}
7077
with dyn.let(**D):
7178
with dyn.let(**D2):
7279
test[noimplicits(dyn.items()) == (("a", 1), ("b", 2), ("c", 3), ("d", 4))]
@@ -76,7 +83,12 @@ def noimplicits(kvs):
7683
test[noimplicits(dyn.items()) == (("a", 42), ("b", 2), ("c", 23), ("d", 4))]
7784
with test_raises(AttributeError, "should not be able to update unbound dynamic variable"):
7885
dyn.e = 5
79-
test[noimplicits(dyn.items()) == (("a", 42), ("b", 2))]
86+
87+
# subscript notation also works for updating
88+
with test:
89+
dyn["a"] = 9001
90+
test[dyn.a == 9001]
91+
test[noimplicits(dyn.items()) == (("a", 9001), ("b", 2))]
8092
test[noimplicits(dyn.items()) == ()]
8193

8294
with testset("update in presence of name shadowing"):
@@ -100,8 +112,43 @@ def noimplicits(kvs):
100112
test[noimplicits(dyn.items()) == (("a", 10), ("b", 20))]
101113
test[noimplicits(dyn.items()) == ()]
102114

115+
with testset("mass update with multithreading"):
116+
comm = Queue()
117+
def worker():
118+
# test[] itself is thread-safe, but the worker threads don't have a
119+
# surrounding testset to catch failures, since we don't want to print in them.
120+
try:
121+
local_successes = 0
122+
with dyn.let(**D):
123+
with dyn.let(**D2):
124+
if noimplicits(dyn.items()) == (("a", 1), ("b", 2), ("c", 3), ("d", 4)):
125+
local_successes += 1
126+
dyn.update(a=-1, b=-2, c=-3, d=-4)
127+
if noimplicits(dyn.items()) == (("a", -1), ("b", -2), ("c", -3), ("d", -4)):
128+
local_successes += 1
129+
if noimplicits(dyn.items()) == (("a", -1), ("b", -2)):
130+
local_successes += 1
131+
dyn.update(a=10, b=20)
132+
if noimplicits(dyn.items()) == (("a", 10), ("b", 20)):
133+
local_successes += 1
134+
if noimplicits(dyn.items()) == ():
135+
local_successes += 1
136+
if local_successes == 5:
137+
comm.put(1)
138+
except Exception: # pragma: no cover, only happens if the test fails.
139+
pass
140+
n = 100
141+
threads = [threading.Thread(target=worker) for _ in range(n)]
142+
for t in threads:
143+
t.start()
144+
for t in threads:
145+
t.join()
146+
successes = sum(slurp(comm))
147+
test[successes == n]
148+
103149
with testset("make_dynvar (default values)"):
104150
make_dynvar(im_always_there=True)
151+
test[dyn.im_always_there is True]
105152
with dyn.let(a=1, b=2):
106153
test[noimplicits(dyn.items()) == (("a", 1), ("b", 2),
107154
("im_always_there", True))]
@@ -114,6 +161,8 @@ def noimplicits(kvs):
114161
with dyn.let(a=1, b=2):
115162
test[noimplicits(view.items()) == (("a", 1), ("b", 2),
116163
("im_always_there", True))]
164+
del view
165+
gc.collect()
117166

118167
# as does dyn.items() (it's an abbreviation for dyn.asdict().items())
119168
items = dyn.items()
@@ -122,6 +171,22 @@ def noimplicits(kvs):
122171
test[noimplicits(items) == (("a", 1), ("b", 2),
123172
("im_always_there", True))]
124173

174+
# the rest of the Mapping API
175+
keys = dyn.keys() # live!
176+
with dyn.let(a=1, b=2):
177+
test[noimplicits_keys(keys) == ("a", "b", "im_always_there")]
178+
179+
test[dyn.get("a") == 1]
180+
test[dyn.get("c") is None] # default
181+
182+
d = dict(items)
183+
test[dyn == d]
184+
185+
# Not much we can do with the output so let's just check these don't crash.
186+
test[returns_normally(dyn.values())]
187+
test[returns_normally(len(dyn))]
188+
189+
125190
if __name__ == '__main__': # pragma: no cover
126191
with session(__file__):
127192
runtests()

0 commit comments

Comments
 (0)