Merge lp:~adeuring/launchpad/bug-458160-fix-submissionparser-checkconsistency into lp:launchpad
- bug-458160-fix-submissionparser-checkconsistency
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Guilherme Salgado |
Approved revision: | no longer in the source branch. |
Merged at revision: | not available |
Proposed branch: | lp:~adeuring/launchpad/bug-458160-fix-submissionparser-checkconsistency |
Merge into: | lp:launchpad |
Diff against target: |
793 lines 5 files modified
lib/canonical/launchpad/scripts/hwdbsubmissions.py (+16/-11) lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py (+95/-84) lib/canonical/launchpad/scripts/tests/test_hwdb_submission_processing.py (+186/-34) lib/lp/testing/__init__.py (+67/-0) lib/lp/testing/tests/test_inlinetests.py (+20/-0) |
To merge this branch: | bzr merge lp:~adeuring/launchpad/bug-458160-fix-submissionparser-checkconsistency |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Guilherme Salgado (community) | Approve | ||
Review via email: mp+13827@code.launchpad.net |
Commit message
Description of the change
Abel Deuring (adeuring) wrote : | # |
Abel Deuring (adeuring) wrote : | # |
sigh, forgot half of the cover letter:
tests:
./bin/test --test=
./bin/test -m lp.testing
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files:
lib/canonical
lib/canonical
lib/lp/
lib/lp/
== Pyflakes notices ==
lib/canonical/
22: redefinition of unused 'etree' from line 20
lib/canonical/
10: redefinition of unused 'etree' from line 8
lib/lp/
20: 'InvalidURLJoin' imported but unused
43: 'is_logged_in' imported but unused
45: 'test_tales' imported but unused
== Pylint notices ==
lib/canonical/
20: [F0401] Unable to import 'xml.etree.
lib/canonical/
8: [F0401] Unable to import 'xml.etree.
The imported but unused "thingies" in lib/lp/
hwdbsubmisisons,py tries to import cElementTree from two different locations. One of them is needed for Python 2.4, the other one for 2.5.
This branch is based on lp:~adeuring/launchpad/bug-458029-kernel-package-name-for-udev-submissions, which is reveiwed but has not yet landed. (ec2testing for that branch got stuck somehow; I killed the ec2 instance yesterday evening after 7 or 8 hours.)
The diff against the base branch:
=== modified file 'lib/canonical/
--- lib/canonical/
+++ lib/canonical/
@@ -1365,7 +1365,8 @@
if ('udev' in parsed_
and not self.checkConsi
- parsed_
+ parsed_
+ parsed_
return False
if duplicate_ids:
=== modified file 'lib/canonical/
--- lib/canonical/
+++ lib/canonical/
@@ -23,6 +23,7 @@
ROOT_UDI)
from canonical.testing import BaseLayer
+from lp.testing import validate_mock_class
class SubmissionParse
"""A Variant used to test SubmissionParse
@@ -2102,7 +2103,7 @@
All shortcut methods return True.
"""
- def checkUdevDictsH
+ def checkUdevDictsH
"""See `Submissio...
Guilherme Salgado (salgado) wrote : | # |
Hi Abel,
Your tests are certainly more readable *and* robust now, but I was left
wondering if there isn't a mock library [1] that already does what we
want. Did you check that?
Other than that, I have only a couple suggestions below, but I think
this is a nice improvement as it is, so I'm approving it.
status approved
[1] http://
http://
etc
On Fri, 2009-10-23 at 08:54 +0000, Abel Deuring wrote:
[...]
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -8,6 +8,7 @@
> from datetime import datetime, timedelta
> from pprint import pformat
> import copy
> +from inspect import getargspec, getmembers, getmro, isclass, ismethod
> import os
> import shutil
> import subprocess
> @@ -744,3 +745,69 @@
> tree.unlock()
>
> return contents
> +
> +def validate_
> + """Validate method signatures in mock classes derived from real classes.
> +
> + We often use mock classes in tests which are derived from real
> + classes.
> +
> + This decorator ensures that methods redefined in the mock
> + class have the same signature as the corresponding methods of
> + the base class.
This is not really a decorator, right? It'd be nice if it was a class
decorator, though. Did you consider that?
> +
> + >>> class A:
> + ...
> + ... def method_one(self, a):
> + ... pass
> +
> + >>>
> + >>> class B(A):
> + ... def method_one(self, a):
> + ... pass
> + >>> validate_
> +
> + If a class derived from A defines method_one with a different
> + signature, we get an AssertionError.
> +
> + >>> class C(A):
> + ... def method_one(self, a, b):
> + ... pass
> + >>> validate_
> + Traceback (most recent call last):
> + ...
> + AssertionError: Different method signature for method_one:...
> +
> + Even a parameter name must not be modified.
> +
> + >>> class D(A):
> + ... def method_one(self, b):
> + ... pass
> + >>> validate_
> + Traceback (most recent call last):
> + ...
> + AssertionError: Different method signature for method_one:...
> +
> + If validate_
> + AssertionError.
> +
> + >>> validate_
> + Traceback (most recent call last):
> + ...
> + AssertionError: validate_
> + """
> + assert isclass(
> + "validate_
> + base_classes = getmro(mock_class)
> + for name, obj in getmembers(
> + if ismethod(obj):
> + for base_class in base_classes[1:]:
> + if (name in base_class.
No need for the parenthesis here.
> + mock_args = getargspec(obj)
> + real_args = getargspec(
> + if mock_args != real_args:
> + raise AssertionError(
> + ...
Abel Deuring (adeuring) wrote : | # |
Hi Guilherme,
thanks for your reveiw!
On 23.10.2009 15:12, Guilherme Salgado wrote:
> Review: Approve
> Hi Abel,
>
> Your tests are certainly more readable *and* robust now, but I was left
> wondering if there isn't a mock library [1] that already does what we
> want. Did you check that?
No, I was not aware of this library. After a quick check I think that it
is a very useful tool, but it would not solve the problem I was having.
(Though I may of course have missed something.)
>
> Other than that, I have only a couple suggestions below, but I think
> this is a nice improvement as it is, so I'm approving it.
>
> status approved
>
> [1] http://
> http://
> etc
>
> On Fri, 2009-10-23 at 08:54 +0000, Abel Deuring wrote:
> [...]
>> === modified file 'lib/lp/
>> --- lib/lp/
>> +++ lib/lp/
>> @@ -8,6 +8,7 @@
>> from datetime import datetime, timedelta
>> from pprint import pformat
>> import copy
>> +from inspect import getargspec, getmembers, getmro, isclass, ismethod
>> import os
>> import shutil
>> import subprocess
>> @@ -744,3 +745,69 @@
>> tree.unlock()
>>
>> return contents
>> +
>> +def validate_
>> + """Validate method signatures in mock classes derived from real classes.
>> +
>> + We often use mock classes in tests which are derived from real
>> + classes.
>> +
>> + This decorator ensures that methods redefined in the mock
>> + class have the same signature as the corresponding methods of
>> + the base class.
>
> This is not really a decorator, right? It'd be nice if it was a class
> decorator, though. Did you consider that?
Argh, I forgot to change the doc string... I _wanted_ to use it as a
decorator, but then discovered that we cannot use class decorators in
Python 2.4 and 2.5.
>
>> +
>> + >>> class A:
>> + ...
>> + ... def method_one(self, a):
>> + ... pass
>> +
>> + >>>
>> + >>> class B(A):
>> + ... def method_one(self, a):
>> + ... pass
>> + >>> validate_
>> +
>> + If a class derived from A defines method_one with a different
>> + signature, we get an AssertionError.
>> +
>> + >>> class C(A):
>> + ... def method_one(self, a, b):
>> + ... pass
>> + >>> validate_
>> + Traceback (most recent call last):
>> + ...
>> + AssertionError: Different method signature for method_one:...
>> +
>> + Even a parameter name must not be modified.
>> +
>> + >>> class D(A):
>> + ... def method_one(self, b):
>> + ... pass
>> + >>> validate_
>> + Traceback (most recent call last):
>> + ...
>> + AssertionError: Different method signature for method_one:...
>> +
>> + If validate_
>> + AssertionError.
>> +
>> + >>> validate_
>> + Traceback (most recent call last):
>> + ...
>> + AssertionError: validate_
>> + """
>> + assert isclas...
Preview Diff
1 | === modified file 'lib/canonical/launchpad/scripts/hwdbsubmissions.py' |
2 | --- lib/canonical/launchpad/scripts/hwdbsubmissions.py 2009-10-21 16:49:03 +0000 |
3 | +++ lib/canonical/launchpad/scripts/hwdbsubmissions.py 2009-10-23 18:56:14 +0000 |
4 | @@ -33,6 +33,7 @@ |
5 | |
6 | from canonical.lazr.xml import RelaxNGValidator |
7 | |
8 | +from canonical.cachedproperty import cachedproperty |
9 | from canonical.config import config |
10 | from canonical.librarian.interfaces import LibrarianServerError |
11 | from canonical.launchpad.interfaces.hwdb import ( |
12 | @@ -1364,7 +1365,8 @@ |
13 | if ('udev' in parsed_data['hardware'] |
14 | and not self.checkConsistentUdevDeviceData( |
15 | parsed_data['hardware']['udev'], |
16 | - parsed_data['hardware']['sysfs-attributes'])): |
17 | + parsed_data['hardware']['sysfs-attributes'], |
18 | + parsed_data['hardware']['dmi'],)): |
19 | return False |
20 | duplicate_ids = self.findDuplicateIDs(parsed_data) |
21 | if duplicate_ids: |
22 | @@ -1470,15 +1472,20 @@ |
23 | del self.devices['/devices'] |
24 | return True |
25 | |
26 | - def getKernelPackageName(self): |
27 | - """Return the kernel package name of the submission,""" |
28 | - root_hal_device = self.devices[ROOT_UDI] |
29 | - kernel_version = root_hal_device.getProperty('system.kernel.version') |
30 | + @cachedproperty |
31 | + def kernel_package_name(self): |
32 | + """The kernel package name for the submission.""" |
33 | + if ROOT_UDI in self.devices: |
34 | + root_hal_device = self.devices[ROOT_UDI] |
35 | + kernel_version = root_hal_device.getProperty( |
36 | + 'system.kernel.version') |
37 | + else: |
38 | + kernel_version = self.parsed_data['summary'].get('kernel-release') |
39 | if kernel_version is None: |
40 | self._logWarning( |
41 | 'Submission does not provide property system.kernel.version ' |
42 | - 'for /org/freedesktop/Hal/devices/computer.', |
43 | - WARNING_NO_HAL_KERNEL_VERSION) |
44 | + 'for /org/freedesktop/Hal/devices/computer or a summary ' |
45 | + 'sub-node <kernel-release>.') |
46 | return None |
47 | kernel_package_name = 'linux-image-' + kernel_version |
48 | packages = self.parsed_data['software']['packages'] |
49 | @@ -1490,8 +1497,7 @@ |
50 | 'Inconsistent kernel version data: According to HAL the ' |
51 | 'kernel is %s, but the submission does not know about a ' |
52 | 'kernel package %s' |
53 | - % (kernel_version, kernel_package_name), |
54 | - WARNING_NO_HAL_KERNEL_VERSION) |
55 | + % (kernel_version, kernel_package_name)) |
56 | return None |
57 | return kernel_package_name |
58 | |
59 | @@ -2185,10 +2191,9 @@ |
60 | # drivers, so there is currently no need to search for |
61 | # for user space printer drivers, for example. |
62 | if self.driver_name is not None: |
63 | - kernel_package_name = self.parser.getKernelPackageName() |
64 | db_driver_set = getUtility(IHWDriverSet) |
65 | return db_driver_set.getOrCreate( |
66 | - kernel_package_name, self.driver_name) |
67 | + self.parser.kernel_package_name, self.driver_name) |
68 | else: |
69 | return None |
70 | |
71 | |
72 | === modified file 'lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py' |
73 | --- lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py 2009-10-21 16:49:03 +0000 |
74 | +++ lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py 2009-10-23 18:56:14 +0000 |
75 | @@ -23,6 +23,7 @@ |
76 | ROOT_UDI) |
77 | from canonical.testing import BaseLayer |
78 | |
79 | +from lp.testing import validate_mock_class |
80 | |
81 | class SubmissionParserTestParseSoftware(SubmissionParser): |
82 | """A Variant used to test SubmissionParser._parseSoftware. |
83 | @@ -2102,7 +2103,7 @@ |
84 | |
85 | All shortcut methods return True. |
86 | """ |
87 | - def checkUdevDictsHavePathKey(self, udev_data): |
88 | + def checkUdevDictsHavePathKey(self, udev_nodes): |
89 | """See `SubmissionParser`.""" |
90 | return True |
91 | |
92 | @@ -2114,7 +2115,7 @@ |
93 | """See `SubmissionParser`.""" |
94 | return True |
95 | |
96 | - def checkUdevScsiProperties(self, udev_data, syfs_data): |
97 | + def checkUdevScsiProperties(self, udev_data, sysfs_data): |
98 | """See `SubmissionParser`.""" |
99 | return True |
100 | |
101 | @@ -2122,6 +2123,8 @@ |
102 | """See `SubmissionParser`.""" |
103 | return True |
104 | |
105 | + validate_mock_class(UdevTestSubmissionParser) |
106 | + |
107 | def testCheckConsistentUdevDeviceData(self): |
108 | """Test of SubmissionParser.checkConsistentUdevDeviceData(),""" |
109 | parser = self.UdevTestSubmissionParser() |
110 | @@ -2137,10 +2140,12 @@ |
111 | self.UdevTestSubmissionParser): |
112 | """A SubmissionPaser where checkUdevDictsHavePathKey() fails.""" |
113 | |
114 | - def checkUdevDictsHavePathKey(self, udev_data): |
115 | + def checkUdevDictsHavePathKey(self, udev_nodes): |
116 | """See `SubmissionParser`.""" |
117 | return False |
118 | |
119 | + validate_mock_class(SubmissionParserUdevPathCheckFails) |
120 | + |
121 | parser = SubmissionParserUdevPathCheckFails() |
122 | self.assertFalse(parser.checkConsistentUdevDeviceData( |
123 | None, None, None)) |
124 | @@ -2158,6 +2163,8 @@ |
125 | """See `SubmissionParser`.""" |
126 | return False |
127 | |
128 | + validate_mock_class(SubmissionParserUdevPciCheckFails) |
129 | + |
130 | parser = SubmissionParserUdevPciCheckFails() |
131 | self.assertFalse(parser.checkConsistentUdevDeviceData( |
132 | None, None, None)) |
133 | @@ -2175,6 +2182,8 @@ |
134 | """See `SubmissionParser`.""" |
135 | return False |
136 | |
137 | + validate_mock_class(SubmissionParserUdevUsbCheckFails) |
138 | + |
139 | parser = SubmissionParserUdevUsbCheckFails() |
140 | self.assertFalse(parser.checkConsistentUdevDeviceData( |
141 | None, None, None)) |
142 | @@ -2192,6 +2201,8 @@ |
143 | """See `SubmissionParser`.""" |
144 | return False |
145 | |
146 | + validate_mock_class(SubmissionParserUdevUsbCheckFails) |
147 | + |
148 | parser = SubmissionParserUdevUsbCheckFails() |
149 | self.assertFalse(parser.checkConsistentUdevDeviceData( |
150 | None, None, None)) |
151 | @@ -2209,55 +2220,38 @@ |
152 | """See `SubmissionParser`.""" |
153 | return False |
154 | |
155 | + validate_mock_class(SubmissionParserUdevUsbCheckFails) |
156 | + |
157 | parser = SubmissionParserUdevUsbCheckFails() |
158 | self.assertFalse(parser.checkConsistentUdevDeviceData( |
159 | None, None, None)) |
160 | |
161 | - def _setupConsistencyCheckParser(self): |
162 | - """Prepare and return a SubmissionParser instance. |
163 | + class MockSubmissionParser(SubmissionParser): |
164 | + """A SubmissionParser variant for testing checkCOnsistentData() |
165 | |
166 | All "method substitutes" return a valid result. |
167 | """ |
168 | - test = self |
169 | + |
170 | def findDuplicateIDs(self, parsed_data): |
171 | - test.assertTrue(isinstance(self, SubmissionParser)) |
172 | return set() |
173 | |
174 | def findInvalidIDReferences(self, parsed_data): |
175 | - test.assertTrue(isinstance(self, SubmissionParser)) |
176 | return set() |
177 | |
178 | def getUDIDeviceMap(self, devices): |
179 | - test.assertTrue(isinstance(self, SubmissionParser)) |
180 | return {} |
181 | |
182 | def getUDIChildren(self, udi_device_map): |
183 | - test.assertTrue(isinstance(self, SubmissionParser)) |
184 | return {} |
185 | |
186 | - def checkHALDevicesParentChildConsistency(self, devices): |
187 | - test.assertTrue(isinstance(self, SubmissionParser)) |
188 | + def checkHALDevicesParentChildConsistency(self, udi_children): |
189 | return [] |
190 | |
191 | - def checkConsistentUdevDeviceData(self, udev_data, sysfs_data): |
192 | + def checkConsistentUdevDeviceData( |
193 | + self, udev_data, sysfs_data, dmi_data): |
194 | return True |
195 | |
196 | - parser = SubmissionParser(self.log) |
197 | - parser.findDuplicateIDs = ( |
198 | - lambda parsed_data: findDuplicateIDs(parser, parsed_data)) |
199 | - parser.findInvalidIDReferences = ( |
200 | - lambda parsed_data: findInvalidIDReferences(parser, parsed_data)) |
201 | - parser.getUDIDeviceMap = ( |
202 | - lambda devices: getUDIDeviceMap(parser, devices)) |
203 | - parser.getUDIChildren = ( |
204 | - lambda udi_device_map: getUDIChildren(parser, udi_device_map)) |
205 | - parser.checkHALDevicesParentChildConsistency = ( |
206 | - lambda udi_children: checkHALDevicesParentChildConsistency( |
207 | - parser, udi_children)) |
208 | - parser.checkConsistentUdevDeviceData = ( |
209 | - lambda udev_data, sysfs_data: checkConsistentUdevDeviceData( |
210 | - parser, udev_data, sysfs_data)) |
211 | - return parser |
212 | + validate_mock_class(MockSubmissionParser) |
213 | |
214 | def assertErrorMessage(self, submission_key, log_message): |
215 | """Search for message in the log entries for submission_key. |
216 | @@ -2309,7 +2303,7 @@ |
217 | |
218 | def testConsistencyCheck(self): |
219 | """Test of SubmissionParser.checkConsistency.""" |
220 | - parser = self._setupConsistencyCheckParser() |
221 | + parser = self.MockSubmissionParser() |
222 | result = parser.checkConsistency({'hardware': |
223 | {'hal': {'devices': []}}}) |
224 | self.assertEqual(result, True, |
225 | @@ -2318,45 +2312,53 @@ |
226 | |
227 | def testConsistencyCheckValidUdevData(self): |
228 | """Test of SubmissionParser.checkConsistency.""" |
229 | - parser = self._setupConsistencyCheckParser() |
230 | + parser = self.MockSubmissionParser() |
231 | self.assertTrue(parser.checkConsistency( |
232 | { |
233 | 'hardware': { |
234 | - 'udev': [], |
235 | - 'sysfs-attributes': [] |
236 | + 'udev': None, |
237 | + 'sysfs-attributes': None, |
238 | + 'dmi': None, |
239 | } |
240 | } |
241 | )) |
242 | |
243 | def testConsistencyCheck_invalid_udev_data(self): |
244 | """Test of SubmissionParser.checkConsistency.""" |
245 | - def checkConsistentUdevDeviceData(self, udev_data, sysfs_data): |
246 | - return False |
247 | - |
248 | - parser = self._setupConsistencyCheckParser() |
249 | - parser.checkConsistentUdevDeviceData = ( |
250 | - lambda udev_data, sysfs_data: checkConsistentUdevDeviceData( |
251 | - parser, udev_data, sysfs_data)) |
252 | + class MockSubmissionParserBadUdevDeviceData( |
253 | + self.MockSubmissionParser): |
254 | + """A parser where checkConsistentUdevDeviceData() fails.""" |
255 | + |
256 | + def checkConsistentUdevDeviceData(self, udev_data, sysfs_data, |
257 | + dmi_data): |
258 | + return False |
259 | + |
260 | + validate_mock_class(MockSubmissionParserBadUdevDeviceData) |
261 | + |
262 | + parser = MockSubmissionParserBadUdevDeviceData() |
263 | self.assertFalse(parser.checkConsistency( |
264 | { |
265 | 'hardware': { |
266 | - 'udev': [{}], |
267 | - 'sysfs-attributes': [] |
268 | + 'udev': None, |
269 | + 'sysfs-attributes': None, |
270 | + 'dmi': None, |
271 | } |
272 | } |
273 | )) |
274 | |
275 | def testConsistencyCheckWithDuplicateIDs(self): |
276 | """SubmissionParser.checkConsistency detects duplicate IDs.""" |
277 | - test = self |
278 | - def findDuplicateIDs(self, parsed_data): |
279 | - test.assertTrue(isinstance(self, SubmissionParser)) |
280 | - return set([1]) |
281 | - |
282 | - parser = self._setupConsistencyCheckParser() |
283 | + class MockSubmissionParserDuplicateIds( |
284 | + self.MockSubmissionParser): |
285 | + """A parser where findDuplicateIDs() fails.""" |
286 | + |
287 | + def findDuplicateIDs(self, parsed_data): |
288 | + return set([1]) |
289 | + |
290 | + validate_mock_class(MockSubmissionParserDuplicateIds) |
291 | + |
292 | + parser = MockSubmissionParserDuplicateIds(self.log) |
293 | parser.submission_key = 'Consistency check detects duplicate IDs' |
294 | - parser.findDuplicateIDs = ( |
295 | - lambda parsed_data: findDuplicateIDs(parser, parsed_data)) |
296 | result = parser.checkConsistency({'hardware': |
297 | {'hal': {'devices': []}}}) |
298 | self.assertEqual(result, False, |
299 | @@ -2366,15 +2368,16 @@ |
300 | |
301 | def testConsistencyCheckWithInvalidIDReferences(self): |
302 | """SubmissionParser.checkConsistency detects invalid ID references.""" |
303 | - test = self |
304 | - def findInvalidIDReferences(self, parsed_data): |
305 | - test.assertTrue(isinstance(self, SubmissionParser)) |
306 | - return set([1]) |
307 | - |
308 | - parser = self._setupConsistencyCheckParser() |
309 | + class MockSubmissionParserInvalidIDReferences( |
310 | + self.MockSubmissionParser): |
311 | + """A parser where findInvalidIDReferences() fails.""" |
312 | + def findInvalidIDReferences(self, parsed_data): |
313 | + return set([1]) |
314 | + |
315 | + validate_mock_class(MockSubmissionParserInvalidIDReferences) |
316 | + |
317 | + parser = MockSubmissionParserInvalidIDReferences(self.log) |
318 | parser.submission_key = 'Consistency check detects invalid ID refs' |
319 | - parser.findInvalidIDReferences = ( |
320 | - lambda parsed_data: findInvalidIDReferences(parser, parsed_data)) |
321 | result = parser.checkConsistency({'hardware': |
322 | {'hal': {'devices': []}}}) |
323 | self.assertEqual(result, False, |
324 | @@ -2384,16 +2387,18 @@ |
325 | |
326 | def testConsistencyCheckWithDuplicateUDI(self): |
327 | """SubmissionParser.checkConsistency detects duplicate UDIs.""" |
328 | - test = self |
329 | - def getUDIDeviceMap(self, parsed_data): |
330 | - test.assertTrue(isinstance(self, SubmissionParser)) |
331 | - raise ValueError( |
332 | - 'Duplicate UDI: /org/freedesktop/Hal/devices/computer') |
333 | - |
334 | - parser = self._setupConsistencyCheckParser() |
335 | + class MockSubmissionParserUDIDeviceMapFails( |
336 | + self.MockSubmissionParser): |
337 | + """A parser where getUDIDeviceMap() fails.""" |
338 | + |
339 | + def getUDIDeviceMap(self, devices): |
340 | + raise ValueError( |
341 | + 'Duplicate UDI: /org/freedesktop/Hal/devices/computer') |
342 | + |
343 | + validate_mock_class(MockSubmissionParserUDIDeviceMapFails) |
344 | + |
345 | + parser = MockSubmissionParserUDIDeviceMapFails(self.log) |
346 | parser.submission_key = 'Consistency check detects invalid ID refs' |
347 | - parser.getUDIDeviceMap = ( |
348 | - lambda devices: getUDIDeviceMap(parser, devices)) |
349 | result = parser.checkConsistency({'hardware': |
350 | {'hal': {'devices': []}}}) |
351 | self.assertEqual(result, False, |
352 | @@ -2404,15 +2409,17 @@ |
353 | |
354 | def testConsistencyCheckChildUDIWithoutParent(self): |
355 | """SubmissionParser.checkConsistency detects "orphaned" devices.""" |
356 | - test = self |
357 | - def getUDIChildren(self, udi_device_map): |
358 | - test.assertTrue(isinstance(self, SubmissionParser)) |
359 | - raise ValueError('Unknown parent UDI /foo in <device id="3">') |
360 | - |
361 | - parser = self._setupConsistencyCheckParser() |
362 | + class MockSubmissionParserUDIChildrenFails( |
363 | + self.MockSubmissionParser): |
364 | + """A parser where getUDIChildren() fails.""" |
365 | + |
366 | + def getUDIChildren(self, udi_device_map): |
367 | + raise ValueError('Unknown parent UDI /foo in <device id="3">') |
368 | + |
369 | + validate_mock_class(MockSubmissionParserUDIChildrenFails) |
370 | + |
371 | + parser = MockSubmissionParserUDIChildrenFails(self.log) |
372 | parser.submission_key = 'Consistency check detects invalid ID refs' |
373 | - parser.getUDIChildren = ( |
374 | - lambda udi_device_map: getUDIChildren(parser, udi_device_map)) |
375 | result = parser.checkConsistency({'hardware': |
376 | {'hal': {'devices': []}}}) |
377 | self.assertEqual(result, False, |
378 | @@ -2423,17 +2430,21 @@ |
379 | |
380 | def testConsistencyCheckCircularParentChildRelation(self): |
381 | """SubmissionParser.checkConsistency detects "orphaned" devices.""" |
382 | - test = self |
383 | - def checkHALDevicesParentChildConsistency(self, devices): |
384 | - test.assertTrue(isinstance(self, SubmissionParser)) |
385 | - return ['/foo', '/bar'] |
386 | - |
387 | - parser = self._setupConsistencyCheckParser() |
388 | + class MockSubmissionParserHALDevicesParentChildConsistency( |
389 | + self.MockSubmissionParser): |
390 | + """A parser where checkHALDevicesParentChildConsistency() fails. |
391 | + """ |
392 | + |
393 | + def checkHALDevicesParentChildConsistency(self, udi_children): |
394 | + return ['/foo', '/bar'] |
395 | + |
396 | + validate_mock_class( |
397 | + MockSubmissionParserHALDevicesParentChildConsistency) |
398 | + |
399 | + parser = MockSubmissionParserHALDevicesParentChildConsistency( |
400 | + self.log) |
401 | parser.submission_key = ('Consistency check detects circular ' |
402 | 'parent-child relationships') |
403 | - parser.checkHALDevicesParentChildConsistency = ( |
404 | - lambda devices: checkHALDevicesParentChildConsistency( |
405 | - parser, devices)) |
406 | result = parser.checkConsistency({'hardware': |
407 | {'hal': {'devices': []}}}) |
408 | self.assertEqual(result, False, |
409 | |
410 | === modified file 'lib/canonical/launchpad/scripts/tests/test_hwdb_submission_processing.py' |
411 | --- lib/canonical/launchpad/scripts/tests/test_hwdb_submission_processing.py 2009-10-22 09:07:26 +0000 |
412 | +++ lib/canonical/launchpad/scripts/tests/test_hwdb_submission_processing.py 2009-10-23 18:56:14 +0000 |
413 | @@ -360,8 +360,8 @@ |
414 | self.assertErrorMessage( |
415 | parser.submission_key, "No udev root device defined") |
416 | |
417 | - def testKernelPackageName(self): |
418 | - """Test of SubmissionParser.getKernelPackageName. |
419 | + def test_kernel_package_name_hal_data(self): |
420 | + """Test of SubmissionParser.kernel_package_name. |
421 | |
422 | Regular case. |
423 | """ |
424 | @@ -388,17 +388,19 @@ |
425 | }, |
426 | } |
427 | parser.buildDeviceList(parser.parsed_data) |
428 | - kernel_package = parser.getKernelPackageName() |
429 | - self.assertEqual(kernel_package, self.KERNEL_PACKAGE, |
430 | - 'Unexpected result of SubmissionParser.getKernelPackageName. ' |
431 | + kernel_package = parser.kernel_package_name |
432 | + self.assertEqual( |
433 | + self.KERNEL_PACKAGE, kernel_package, |
434 | + 'Unexpected value of SubmissionParser.kernel_package_name. ' |
435 | 'Expected linux-image-2.6.24-19-generic, got %r' % kernel_package) |
436 | |
437 | - self.assertEqual(len(self.handler.records), 0, |
438 | + self.assertEqual( |
439 | + 0, len(self.handler.records), |
440 | 'One or more warning messages were logged by ' |
441 | - 'getKernelPackageName, where zero was expected.') |
442 | + 'SubmissionParser.kernel_package_name, where zero was expected.') |
443 | |
444 | - def testKernelPackageNameInconsistent(self): |
445 | - """Test of SubmissionParser.getKernelPackageName. |
446 | + def test_kernel_package_hal_data_name_inconsistent(self): |
447 | + """Test of SubmissionParser.kernel_package_name. |
448 | |
449 | Test a name inconsistency. |
450 | """ |
451 | @@ -426,24 +428,24 @@ |
452 | } |
453 | parser.submission_key = 'Test of inconsistent kernel package name' |
454 | parser.buildDeviceList(parser.parsed_data) |
455 | - kernel_package = parser.getKernelPackageName() |
456 | - self.assertEqual(kernel_package, None, |
457 | - 'Unexpected result of SubmissionParser.getKernelPackageName. ' |
458 | - 'Expected None, got %r' % kernel_package) |
459 | - self.assertWarningMessage(parser.submission_key, |
460 | + kernel_package = parser.kernel_package_name |
461 | + self.assertIs(None, kernel_package) |
462 | + self.assertWarningMessage( |
463 | + parser.submission_key, |
464 | 'Inconsistent kernel version data: According to HAL the ' |
465 | 'kernel is 2.6.24-19-generic, but the submission does not ' |
466 | 'know about a kernel package linux-image-2.6.24-19-generic') |
467 | # The warning appears only once per submission, even if the |
468 | - # SubmissionParser.getKernelPackageName is called more than once. |
469 | + # property kernel_package_name is accessed more than once. |
470 | num_warnings = len(self.handler.records) |
471 | - parser.getKernelPackageName() |
472 | - self.assertEqual(num_warnings, len(self.handler.records), |
473 | + test = parser.kernel_package_name |
474 | + self.assertEqual( |
475 | + num_warnings, len(self.handler.records), |
476 | 'Warning for missing HAL property system.kernel.version ' |
477 | 'repeated.') |
478 | |
479 | - def testKernelPackageNameNoHALData(self): |
480 | - """Test of SubmissionParser.getKernelPackageName. |
481 | + def test_kernel_package_name_hal_data_no_kernel_version_in_hal_data(self): |
482 | + """Test of SubmissionParser.kernel_package_name. |
483 | |
484 | Test without HAL property system.kernel.version. |
485 | """ |
486 | @@ -469,26 +471,28 @@ |
487 | } |
488 | parser.submission_key = 'Test: missing property system.kernel.version' |
489 | parser.buildDeviceList(parser.parsed_data) |
490 | - kernel_package = parser.getKernelPackageName() |
491 | - self.assertEqual(kernel_package, None, |
492 | - 'Unexpected result of SubmissionParser.getKernelPackageName. ' |
493 | - 'Expected None, got %r' % kernel_package) |
494 | - self.assertWarningMessage(parser.submission_key, |
495 | + self.assertIs(None, parser.kernel_package_name) |
496 | + self.assertWarningMessage( |
497 | + parser.submission_key, |
498 | 'Submission does not provide property system.kernel.version ' |
499 | - 'for /org/freedesktop/Hal/devices/computer.') |
500 | + 'for /org/freedesktop/Hal/devices/computer or a summary ' |
501 | + 'sub-node <kernel-release>.') |
502 | # The warning appears only once per submission, even if the |
503 | - # SubmissionParser.getKernelPackageName is called more than once. |
504 | + # property kernel_package_name is accessed more than once. |
505 | num_warnings = len(self.handler.records) |
506 | - parser.getKernelPackageName() |
507 | - self.assertEqual(num_warnings, len(self.handler.records), |
508 | + test = parser.kernel_package_name |
509 | + self.assertEqual( |
510 | + num_warnings, len(self.handler.records), |
511 | 'Warning for missing HAL property system.kernel.version ' |
512 | 'repeated.') |
513 | |
514 | - def testKernelPackageNameNoPackageData(self): |
515 | - """Test of SubmissionParser.getKernelPackageName. |
516 | + def test_kernel_package_name_hal_data_no_package_data(self): |
517 | + """Test of SubmissionParser.kernel_package_name. |
518 | |
519 | - Test without any package data. getKernelPackageName returns |
520 | - the property system.kernel.version without any further checking. |
521 | + Test without any package data. In this case, |
522 | + SubmissionParser.kernel_package_name is the value of the property |
523 | + system.kernel.version if the root HAL device. No further checks |
524 | + are done. |
525 | """ |
526 | parser = SubmissionParser(self.log) |
527 | devices = [ |
528 | @@ -512,8 +516,156 @@ |
529 | } |
530 | parser.submission_key = 'Test: missing property system.kernel.version' |
531 | parser.buildDeviceList(parser.parsed_data) |
532 | - kernel_package = parser.getKernelPackageName() |
533 | - self.assertEqual(kernel_package, self.KERNEL_PACKAGE, |
534 | + kernel_package = parser.kernel_package_name |
535 | + self.assertEqual( |
536 | + self.KERNEL_PACKAGE, kernel_package, |
537 | + 'Unexpected result of SubmissionParser.getKernelPackageName, ' |
538 | + 'test without any package data. Expected None, got %r' |
539 | + % kernel_package) |
540 | + |
541 | + def test_kernel_package_name_udev_data(self): |
542 | + """Test of SubmissionParser.kernel_package_name for udev data. |
543 | + |
544 | + Variant for udev data, regular case. |
545 | + """ |
546 | + parser = SubmissionParser(self.log) |
547 | + parser.parsed_data = { |
548 | + 'hardware': { |
549 | + 'udev': [ |
550 | + {'P': '/devices/LNXSYSTM:00'} |
551 | + ], |
552 | + 'sysfs-attributes': {}, |
553 | + 'dmi': {}, |
554 | + }, |
555 | + 'software': { |
556 | + 'packages': { |
557 | + self.KERNEL_PACKAGE: {}, |
558 | + }, |
559 | + }, |
560 | + 'summary': { |
561 | + 'kernel-release': self.KERNEL_VERSION, |
562 | + }, |
563 | + } |
564 | + parser.buildDeviceList(parser.parsed_data) |
565 | + kernel_package = parser.kernel_package_name |
566 | + self.assertEqual( |
567 | + self.KERNEL_PACKAGE, kernel_package, |
568 | + 'Unexpected value of SubmissionParser.kernel_package_name. ' |
569 | + 'Expected linux-image-2.6.24-19-generic, got %r' % kernel_package) |
570 | + |
571 | + self.assertEqual( |
572 | + 0, len(self.handler.records), |
573 | + 'One or more warning messages were logged by ' |
574 | + 'SubmissionParser.kernel_package_name, where zero was expected.') |
575 | + |
576 | + def test_kernel_package_udev_data_name_inconsistent(self): |
577 | + """Test of SubmissionParser.kernel_package_name. |
578 | + |
579 | + Variant for udev data, name inconsistency. |
580 | + """ |
581 | + parser = SubmissionParser(self.log) |
582 | + parser.parsed_data = { |
583 | + 'hardware': { |
584 | + 'udev': [ |
585 | + {'P': '/devices/LNXSYSTM:00'} |
586 | + ], |
587 | + 'sysfs-attributes': {}, |
588 | + 'dmi': {}, |
589 | + }, |
590 | + 'software': { |
591 | + 'packages': { |
592 | + 'linux-image-from-obscure-external-source': {}, |
593 | + }, |
594 | + }, |
595 | + 'summary': { |
596 | + 'kernel-release': self.KERNEL_VERSION, |
597 | + }, |
598 | + } |
599 | + parser.submission_key = 'Test of inconsistent kernel package name' |
600 | + parser.buildDeviceList(parser.parsed_data) |
601 | + kernel_package = parser.kernel_package_name |
602 | + self.assertIs(None, kernel_package) |
603 | + self.assertWarningMessage( |
604 | + parser.submission_key, |
605 | + 'Inconsistent kernel version data: According to HAL the ' |
606 | + 'kernel is 2.6.24-19-generic, but the submission does not ' |
607 | + 'know about a kernel package linux-image-2.6.24-19-generic') |
608 | + # The warning appears only once per submission, even if the |
609 | + # property kernel_package_name is accessed more than once. |
610 | + num_warnings = len(self.handler.records) |
611 | + test = parser.kernel_package_name |
612 | + self.assertEqual( |
613 | + num_warnings, len(self.handler.records), |
614 | + 'Warning for missing HAL property system.kernel.version ' |
615 | + 'repeated.') |
616 | + |
617 | + def test_kernel_package_name_udev_data_no_kernel_version_in_summary(self): |
618 | + """Test of SubmissionParser.kernel_package_name. |
619 | + |
620 | + Test without the summary sub-node <kernel-release>. |
621 | + """ |
622 | + parser = SubmissionParser(self.log) |
623 | + parser.parsed_data = { |
624 | + 'hardware': { |
625 | + 'udev': [ |
626 | + {'P': '/devices/LNXSYSTM:00'} |
627 | + ], |
628 | + 'sysfs-attributes': {}, |
629 | + 'dmi': {}, |
630 | + }, |
631 | + 'software': { |
632 | + 'packages': { |
633 | + self.KERNEL_PACKAGE: {}, |
634 | + }, |
635 | + }, |
636 | + 'summary': {}, |
637 | + } |
638 | + parser.submission_key = 'Test: missing property system.kernel.version' |
639 | + parser.buildDeviceList(parser.parsed_data) |
640 | + self.assertIs(None, parser.kernel_package_name) |
641 | + self.assertWarningMessage( |
642 | + parser.submission_key, |
643 | + 'Submission does not provide property system.kernel.version ' |
644 | + 'for /org/freedesktop/Hal/devices/computer or a summary ' |
645 | + 'sub-node <kernel-release>.') |
646 | + # The warning appears only once per submission, even if the |
647 | + # property kernel_package_name is accessed more than once. |
648 | + num_warnings = len(self.handler.records) |
649 | + test = parser.kernel_package_name |
650 | + self.assertEqual( |
651 | + num_warnings, len(self.handler.records), |
652 | + 'Warning for missing HAL property system.kernel.version ' |
653 | + 'repeated.') |
654 | + |
655 | + def test_kernel_package_name_udev_data_no_package_data(self): |
656 | + """Test of SubmissionParser.kernel_package_name. |
657 | + |
658 | + Variant for udev data, test without any package data. In this case, |
659 | + SubmissionParser.kernel_package_name is the value of the property |
660 | + system.kernel.version if the root HAL device. No further checks |
661 | + are done. |
662 | + """ |
663 | + parser = SubmissionParser(self.log) |
664 | + parser.parsed_data = { |
665 | + 'hardware': { |
666 | + 'udev': [ |
667 | + {'P': '/devices/LNXSYSTM:00'}, |
668 | + ], |
669 | + 'sysfs-attributes': {}, |
670 | + 'dmi': {}, |
671 | + }, |
672 | + 'software': { |
673 | + 'packages': {}, |
674 | + }, |
675 | + 'summary': { |
676 | + 'kernel-release': self.KERNEL_VERSION, |
677 | + }, |
678 | + } |
679 | + parser.submission_key = 'Test: missing property system.kernel.version' |
680 | + parser.buildDeviceList(parser.parsed_data) |
681 | + kernel_package = parser.kernel_package_name |
682 | + self.assertEqual( |
683 | + self.KERNEL_PACKAGE, kernel_package, |
684 | 'Unexpected result of SubmissionParser.getKernelPackageName, ' |
685 | 'test without any package data. Expected None, got %r' |
686 | % kernel_package) |
687 | |
688 | === modified file 'lib/lp/testing/__init__.py' |
689 | --- lib/lp/testing/__init__.py 2009-10-20 01:55:17 +0000 |
690 | +++ lib/lp/testing/__init__.py 2009-10-23 18:56:14 +0000 |
691 | @@ -8,6 +8,7 @@ |
692 | from datetime import datetime, timedelta |
693 | from pprint import pformat |
694 | import copy |
695 | +from inspect import getargspec, getmembers, getmro, isclass, ismethod |
696 | import os |
697 | import shutil |
698 | import subprocess |
699 | @@ -744,3 +745,69 @@ |
700 | tree.unlock() |
701 | |
702 | return contents |
703 | + |
704 | +def validate_mock_class(mock_class): |
705 | + """Validate method signatures in mock classes derived from real classes. |
706 | + |
707 | + We often use mock classes in tests which are derived from real |
708 | + classes. |
709 | + |
710 | + This function ensures that methods redefined in the mock |
711 | + class have the same signature as the corresponding methods of |
712 | + the base class. |
713 | + |
714 | + >>> class A: |
715 | + ... |
716 | + ... def method_one(self, a): |
717 | + ... pass |
718 | + |
719 | + >>> |
720 | + >>> class B(A): |
721 | + ... def method_one(self, a): |
722 | + ... pass |
723 | + >>> validate_mock_class(B) |
724 | + |
725 | + If a class derived from A defines method_one with a different |
726 | + signature, we get an AssertionError. |
727 | + |
728 | + >>> class C(A): |
729 | + ... def method_one(self, a, b): |
730 | + ... pass |
731 | + >>> validate_mock_class(C) |
732 | + Traceback (most recent call last): |
733 | + ... |
734 | + AssertionError: Different method signature for method_one:... |
735 | + |
736 | + Even a parameter name must not be modified. |
737 | + |
738 | + >>> class D(A): |
739 | + ... def method_one(self, b): |
740 | + ... pass |
741 | + >>> validate_mock_class(D) |
742 | + Traceback (most recent call last): |
743 | + ... |
744 | + AssertionError: Different method signature for method_one:... |
745 | + |
746 | + If validate_mock_class() for anything but a class, we get an |
747 | + AssertionError. |
748 | + |
749 | + >>> validate_mock_class('a string') |
750 | + Traceback (most recent call last): |
751 | + ... |
752 | + AssertionError: validate_mock_class() must be called for a class |
753 | + """ |
754 | + assert isclass(mock_class), ( |
755 | + "validate_mock_class() must be called for a class") |
756 | + base_classes = getmro(mock_class) |
757 | + for name, obj in getmembers(mock_class): |
758 | + if ismethod(obj): |
759 | + for base_class in base_classes[1:]: |
760 | + if name in base_class.__dict__: |
761 | + mock_args = getargspec(obj) |
762 | + real_args = getargspec(base_class.__dict__[name]) |
763 | + if mock_args != real_args: |
764 | + raise AssertionError( |
765 | + 'Different method signature for %s: %r %r' % ( |
766 | + name, mock_args, real_args)) |
767 | + else: |
768 | + break |
769 | |
770 | === added file 'lib/lp/testing/tests/test_inlinetests.py' |
771 | --- lib/lp/testing/tests/test_inlinetests.py 1970-01-01 00:00:00 +0000 |
772 | +++ lib/lp/testing/tests/test_inlinetests.py 2009-10-23 18:56:14 +0000 |
773 | @@ -0,0 +1,20 @@ |
774 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
775 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
776 | + |
777 | +"""Run the doc string tests.""" |
778 | + |
779 | +import doctest |
780 | + |
781 | +from zope.testing.doctest import NORMALIZE_WHITESPACE, ELLIPSIS |
782 | + |
783 | +from canonical.launchpad.testing.systemdocs import LayeredDocFileSuite |
784 | +from canonical.testing import BaseLayer |
785 | +from lp import testing |
786 | + |
787 | +def test_suite(): |
788 | + suite = LayeredDocFileSuite( |
789 | + layer=BaseLayer) |
790 | + suite.addTest(doctest.DocTestSuite( |
791 | + testing, optionflags=NORMALIZE_WHITESPACE|ELLIPSIS)) |
792 | + return suite |
793 | + |
A two-line change or "real" code together with 100 lines of a new test helper (including tests of the helper) and ~450 lines of changed/ improved/ fixed tests...
The method SubmissionParse r.checkConsiste ncy() in l/c/l/scripts/ hwdbsubmissions .py did not specify enough parameters when it called SubmissionParse r.checkConsiste ntUdevDeviceDat a(). This bug was introduced in a previous branch when I modified the latter method to do more checks which required one more parameter (dmi_data).
The bug in checkConsistency() was not detected because of the way this method was tested: All tests of it created a regular instance of SubmissionParser and then monkey-patched the methods called by checkConsistency(), thus avoiding to set up lots of quite convoluted looking test data required for the non-monkey-patched versions of these methods. The problem is that the surrogate of checkConsistent UdevDeviceData( ) did not get the new parameter...
I assume that such an error can occur in other tests too, so my idea to avoid a similar problem in the future was this: Don't monkey-patch class SubmissionParser, but use instead a mock class, derived from the real class, and ensure that the methods of mock class have the same signatures as the methods of the base class.
Such a test can be made by calling the new function validate_ mock_class( ) in lp.testing. The test is quite simple: It compares the signature of methods defined in the mock class with the signature of the first method with same name found in the base classes. Even the names of method parameters must not be changed. I think this not a serious problem: having identical parameters names isn't that bad, and allowing differernt names would require a somewhat more complicated comparison, which isn't worth the effort, I think.
the "doc string tests" in lp.testing.__init__ were not tested at all, so I added a test suite for them.