Merge lp:~benji/testtools/modernize-tsfr into lp:~testtools-committers/testtools/trunk

Proposed by Benji York
Status: Rejected
Rejected by: Jonathan Lange
Proposed branch: lp:~benji/testtools/modernize-tsfr
Merge into: lp:~testtools-committers/testtools/trunk
Prerequisite: lp:~jml/testtools/forward-current-tags
Diff against target: 266 lines (+51/-138)
2 files modified
testtools/testresult/real.py (+24/-62)
testtools/tests/test_testresult.py (+27/-76)
To merge this branch: bzr merge lp:~benji/testtools/modernize-tsfr
Reviewer Review Type Date Requested Status
Jonathan Lange Disapprove
Review via email: mp+101749@code.launchpad.net

Commit message

Modernise ThreadSafeForwardingResult by simplifying it and adding some tests.

Description of the change

The ThreadSafeForwardingResult attempts to manage tag state itself
because the wrapped test result did not previously manage tags
correctly. The recent ~jml/testtools/forward-current-tags branch fixes
those issues so TSFR needed to be updated. This branch does that by
simplifying TSFR, removing some now unneeded tests, and adding some new
tests.

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

Hey Benji,

First up, thanks so much for the contribution, and for valiantly persisting in cleaning up this somewhat unclear code. Sadly, I'm afraid it's back to the drawing with this one. I'm afraid it's our fault for not clearly communicating the intent of TSFR.

TSFR's main purpose is to send all of the events for a single test in an atomic unit, preventing the destination result from receiving input like this:

  startTest(foo)
  startTest(bar)
  addSuccess(foo)
  addSuccess(bar)
  stopTest(bar)
  stopTest(foo)

And instead guaranteeing input like this:

  startTest(bar)
  addSuccess(bar)
  stopTest(bar)
  startTest(foo)
  addSuccess(foo)
  stopTest(foo)

If I understand this patch correctly, it wraps each individual event in a semaphore, which isn't enough to guarantee this per-test atomicity.

I'll try to sketch out, or possibly even implement, a solution here.

jml

review: Disapprove
Revision history for this message
Robert Collins (lifeless) wrote :

In fact TSFR *must* manage tag state itself; it has to supply either
the delta (by inspection) to what its state is at each synchronisation
point, or it has to supply a back-out-its-own-state command at the end
of each synchronisation point.

I'd be inclined to do the latter.

Do you have a SSCCE of the problem?

-Rob

Unmerged revisions

270. By Benji York

tweak comment

269. By Benji York

add a test that demonstrates ThreadsafeForwardingResult's use of semaphores

268. By Benji York

simplify tests some more and fix recently-introduced bug in wasSuccessful

267. By Benji York

wasSuccessful might do anything, so wrap it in the semaphore too

266. By Benji York

regularize semaphore use

265. By Benji York

cleanup some unneeded concepts that I recently introduced

264. By Benji York

get ThreadsafeForwardingResult tests passing

263. By Benji York

checkpoint - all tag tests pass

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'testtools/testresult/real.py'
2--- testtools/testresult/real.py 2012-04-12 15:07:32 +0000
3+++ testtools/testresult/real.py 2012-04-12 15:07:32 +0000
4@@ -376,110 +376,72 @@
5 TestResult.__init__(self)
6 self.result = ExtendedToOriginalDecorator(target)
7 self.semaphore = semaphore
8- self._test_start = None
9 self._global_tags = set(), set()
10 self._test_tags = set(), set()
11
12 def __repr__(self):
13 return '<%s %r>' % (self.__class__.__name__, self.result)
14
15- def _not_empty_tags(self, tags):
16- return bool(tags[0] and tags[1])
17+ @property
18+ def current_tags(self):
19+ self.semaphore.acquire()
20+ try:
21+ return self.result.current_tags
22+ finally:
23+ self.semaphore.release()
24
25- def _add_result_with_semaphore(self, method, test, *args, **kwargs):
26- now = self._now()
27+ def _call_with_semaphore(self, method, *args, **kwargs):
28 self.semaphore.acquire()
29 try:
30- self.result.time(self._test_start)
31- if self._not_empty_tags(self._global_tags):
32- self.result.tags(*self._global_tags)
33- try:
34- self.result.startTest(test)
35- self.result.time(now)
36- if self._not_empty_tags(self._test_tags):
37- self.result.tags(*self._test_tags)
38- try:
39- method(test, *args, **kwargs)
40- finally:
41- self.result.stopTest(test)
42- finally:
43- if self._not_empty_tags(self._global_tags):
44- self.result.tags(*reversed(self._global_tags))
45+ return method(*args, **kwargs)
46 finally:
47 self.semaphore.release()
48- self._test_start = None
49
50 def addError(self, test, err=None, details=None):
51- self._add_result_with_semaphore(self.result.addError,
52+ self._call_with_semaphore(self.result.addError,
53 test, err, details=details)
54
55 def addExpectedFailure(self, test, err=None, details=None):
56- self._add_result_with_semaphore(self.result.addExpectedFailure,
57+ self._call_with_semaphore(self.result.addExpectedFailure,
58 test, err, details=details)
59
60 def addFailure(self, test, err=None, details=None):
61- self._add_result_with_semaphore(self.result.addFailure,
62+ self._call_with_semaphore(self.result.addFailure,
63 test, err, details=details)
64
65 def addSkip(self, test, reason=None, details=None):
66- self._add_result_with_semaphore(self.result.addSkip,
67+ self._call_with_semaphore(self.result.addSkip,
68 test, reason, details=details)
69
70 def addSuccess(self, test, details=None):
71- self._add_result_with_semaphore(self.result.addSuccess,
72+ self._call_with_semaphore(self.result.addSuccess,
73 test, details=details)
74
75 def addUnexpectedSuccess(self, test, details=None):
76- self._add_result_with_semaphore(self.result.addUnexpectedSuccess,
77+ self._call_with_semaphore(self.result.addUnexpectedSuccess,
78 test, details=details)
79
80 def startTestRun(self):
81- super(ThreadsafeForwardingResult, self).startTestRun()
82- self.semaphore.acquire()
83- try:
84- self.result.startTestRun()
85- finally:
86- self.semaphore.release()
87+ self._call_with_semaphore(self.result.startTestRun)
88
89 def stopTestRun(self):
90- self.semaphore.acquire()
91- try:
92- self.result.stopTestRun()
93- finally:
94- self.semaphore.release()
95+ self._call_with_semaphore(self.result.stopTestRun)
96
97 def done(self):
98- self.semaphore.acquire()
99- try:
100- self.result.done()
101- finally:
102- self.semaphore.release()
103+ self._call_with_semaphore(self.result.done)
104
105 def startTest(self, test):
106- self._test_start = self._now()
107- super(ThreadsafeForwardingResult, self).startTest(test)
108+ self._call_with_semaphore(self.result.startTest, test)
109+
110+ def stopTest(self, test):
111+ self._call_with_semaphore(self.result.stopTest, test)
112
113 def wasSuccessful(self):
114- return self.result.wasSuccessful()
115+ return self._call_with_semaphore(self.result.wasSuccessful)
116
117 def tags(self, new_tags, gone_tags):
118 """See `TestResult`."""
119- super(ThreadsafeForwardingResult, self).tags(new_tags, gone_tags)
120- if self._test_start is not None:
121- self._test_tags = self._merge_tags(
122- self._test_tags, new_tags, gone_tags)
123- else:
124- self._global_tags = self._merge_tags(
125- self._global_tags, new_tags, gone_tags)
126-
127- def _merge_tags(self, existing, new_tags, gone_tags):
128- result_new = set(existing[0])
129- result_gone = set(existing[1])
130- result_new.update(new_tags)
131- result_new.difference_update(gone_tags)
132- result_gone.update(gone_tags)
133- result_gone.difference_update(new_tags)
134- return result_new, result_gone
135+ self._call_with_semaphore(self.result.tags, new_tags, gone_tags)
136
137
138 class ExtendedToOriginalDecorator(object):
139
140=== modified file 'testtools/tests/test_testresult.py'
141--- testtools/tests/test_testresult.py 2012-04-12 15:07:32 +0000
142+++ testtools/tests/test_testresult.py 2012-04-12 15:07:32 +0000
143@@ -103,6 +103,18 @@
144 return sys.exc_info()
145
146
147+class SemaphoreDouble(object):
148+
149+ def __init__(self, log):
150+ self.log = log.append
151+
152+ def acquire(self):
153+ self.log('acquired')
154+
155+ def release(self):
156+ self.log('released')
157+
158+
159 class Python26Contract(object):
160
161 def test_fresh_result_is_successful(self):
162@@ -766,13 +778,6 @@
163 self.result1 = ThreadsafeForwardingResult(self.target,
164 self.result_semaphore)
165
166- def test_nonforwarding_methods(self):
167- # startTest and stopTest are not forwarded because they need to be
168- # batched.
169- self.result1.startTest(self)
170- self.result1.stopTest(self)
171- self.assertEqual([], self.target._events)
172-
173 def test_startTestRun(self):
174 self.result1.startTestRun()
175 self.result2 = ThreadsafeForwardingResult(self.target,
176@@ -787,75 +792,21 @@
177 self.result2.stopTestRun()
178 self.assertEqual(["stopTestRun", "stopTestRun"], self.target._events)
179
180- def test_forwarding_methods(self):
181- # error, failure, skip and success are forwarded in batches.
182- exc_info1 = make_exception_info(RuntimeError, 'error')
183- starttime1 = datetime.datetime.utcfromtimestamp(1.489)
184- endtime1 = datetime.datetime.utcfromtimestamp(51.476)
185- self.result1.time(starttime1)
186- self.result1.startTest(self)
187- self.result1.time(endtime1)
188- self.result1.addError(self, exc_info1)
189- exc_info2 = make_exception_info(AssertionError, 'failure')
190- starttime2 = datetime.datetime.utcfromtimestamp(2.489)
191- endtime2 = datetime.datetime.utcfromtimestamp(3.476)
192- self.result1.time(starttime2)
193- self.result1.startTest(self)
194- self.result1.time(endtime2)
195- self.result1.addFailure(self, exc_info2)
196- reason = _u("Skipped for some reason")
197- starttime3 = datetime.datetime.utcfromtimestamp(4.489)
198- endtime3 = datetime.datetime.utcfromtimestamp(5.476)
199- self.result1.time(starttime3)
200- self.result1.startTest(self)
201- self.result1.time(endtime3)
202- self.result1.addSkip(self, reason)
203- starttime4 = datetime.datetime.utcfromtimestamp(6.489)
204- endtime4 = datetime.datetime.utcfromtimestamp(7.476)
205- self.result1.time(starttime4)
206- self.result1.startTest(self)
207- self.result1.time(endtime4)
208- self.result1.addSuccess(self)
209- self.assertEqual([
210- ('time', starttime1),
211- ('startTest', self),
212- ('time', endtime1),
213- ('addError', self, exc_info1),
214- ('stopTest', self),
215- ('time', starttime2),
216- ('startTest', self),
217- ('time', endtime2),
218- ('addFailure', self, exc_info2),
219- ('stopTest', self),
220- ('time', starttime3),
221- ('startTest', self),
222- ('time', endtime3),
223- ('addSkip', self, reason),
224- ('stopTest', self),
225- ('time', starttime4),
226- ('startTest', self),
227- ('time', endtime4),
228- ('addSuccess', self),
229- ('stopTest', self),
230- ], self.target._events)
231-
232- def test_tags_helper(self):
233- expected = set(['present']), set(['missing', 'going'])
234- input = set(['present']), set(['missing'])
235- self.assertEqual(
236- expected, self.result1._merge_tags(input, set(), set(['going'])))
237- expected = set(['present']), set(['missing', 'going'])
238- input = set(['present', 'going']), set(['missing'])
239- self.assertEqual(
240- expected, self.result1._merge_tags(input, set(), set(['going'])))
241- expected = set(['coming', 'present']), set(['missing'])
242- input = set(['present']), set(['missing'])
243- self.assertEqual(
244- expected, self.result1._merge_tags(input, set(['coming']), set()))
245- expected = set(['coming', 'present']), set(['missing'])
246- input = set(['present']), set(['coming', 'missing'])
247- self.assertEqual(
248- expected, self.result1._merge_tags(input, set(['coming']), set()))
249+ def test_semaphore_aquired(self):
250+ # The methods (and properties) of TSFR (ThreadsafeForwardingResult)
251+ # are protected by a semaphore so only one thread may access the
252+ # underlying test result at any one time. This can be demonstrated by
253+ # inserting our own logging semaphore double and watching its
254+ # behavior.
255+ log = []
256+ result = LoggingResult(log)
257+ semaphore = SemaphoreDouble(log)
258+ tsfr = ThreadsafeForwardingResult(result, semaphore)
259+ # Now if we make calls on the TSFR the underlying result will be
260+ # called too and the semaphore will be acquired and released around
261+ # the calls.
262+ tsfr.startTestRun()
263+ self.assertEqual(['acquired', 'startTestRun', 'released'], log)
264
265
266 class TestExtendedToOriginalResultDecoratorBase(TestCase):

Subscribers

People subscribed via source and target branches