Merge lp:~adeuring/launchpad/bug-458160-fix-submissionparser-checkconsistency into lp:launchpad

Proposed by Abel Deuring
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
Reviewer Review Type Date Requested Status
Guilherme Salgado (community) Approve
Review via email: mp+13827@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) wrote :

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 SubmissionParser.checkConsistency() in l/c/l/scripts/hwdbsubmissions.py did not specify enough parameters when it called SubmissionParser.checkConsistentUdevDeviceData(). 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 checkConsistentUdevDeviceData() 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.

Revision history for this message
Abel Deuring (adeuring) wrote :
Download full text (19.3 KiB)

sigh, forgot half of the cover letter:

tests:

./bin/test --test=test_hwdb_submission_parser
./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/launchpad/scripts/hwdbsubmissions.py
  lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py
  lib/lp/testing/__init__.py
  lib/lp/testing/tests/test_inlinetests.py

== Pyflakes notices ==

lib/canonical/launchpad/scripts/hwdbsubmissions.py
    22: redefinition of unused 'etree' from line 20

lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py
    10: redefinition of unused 'etree' from line 8

lib/lp/testing/__init__.py
    20: 'InvalidURLJoin' imported but unused
    43: 'is_logged_in' imported but unused
    45: 'test_tales' imported but unused

== Pylint notices ==

lib/canonical/launchpad/scripts/hwdbsubmissions.py
    20: [F0401] Unable to import 'xml.etree.cElementTree' (No module named etree)

lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py
    8: [F0401] Unable to import 'xml.etree.cElementTree' (No module named etree)

The imported but unused "thingies" in lib/lp/testing/__init__.py are elsewhere imported from lp.testing.

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/launchpad/scripts/hwdbsubmissions.py'
--- lib/canonical/launchpad/scripts/hwdbsubmissions.py 2009-10-22 11:08:18 +0000
+++ lib/canonical/launchpad/scripts/hwdbsubmissions.py 2009-10-22 19:40:00 +0000
@@ -1365,7 +1365,8 @@
         if ('udev' in parsed_data['hardware']
             and not self.checkConsistentUdevDeviceData(
                 parsed_data['hardware']['udev'],
- parsed_data['hardware']['sysfs-attributes'])):
+ parsed_data['hardware']['sysfs-attributes'],
+ parsed_data['hardware']['dmi'],)):
             return False
         duplicate_ids = self.findDuplicateIDs(parsed_data)
         if duplicate_ids:

=== modified file 'lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py'
--- lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py 2009-10-21 16:49:03 +0000
+++ lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py 2009-10-22 19:59:45 +0000
@@ -23,6 +23,7 @@
     ROOT_UDI)
 from canonical.testing import BaseLayer

+from lp.testing import validate_mock_class

 class SubmissionParserTestParseSoftware(SubmissionParser):
     """A Variant used to test SubmissionParser._parseSoftware.
@@ -2102,7 +2103,7 @@

         All shortcut methods return True.
         """
- def checkUdevDictsHavePathKey(self, udev_data):
+ def checkUdevDictsHavePathKey(self, udev_nodes):
             """See `Submissio...

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (3.4 KiB)

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://www.voidspace.org.uk/python/mock/
    http://labix.org/mocker
    etc

On Fri, 2009-10-23 at 08:54 +0000, Abel Deuring wrote:
[...]
> === modified file 'lib/lp/testing/__init__.py'
> --- lib/lp/testing/__init__.py 2009-10-20 01:55:17 +0000
> +++ lib/lp/testing/__init__.py 2009-10-23 08:01:11 +0000
> @@ -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_mock_class(mock_class):
> + """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_mock_class(B)
> +
> + 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_mock_class(C)
> + 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_mock_class(D)
> + Traceback (most recent call last):
> + ...
> + AssertionError: Different method signature for method_one:...
> +
> + If validate_mock_class() for anything but a class, we get an
> + AssertionError.
> +
> + >>> validate_mock_class('a string')
> + Traceback (most recent call last):
> + ...
> + AssertionError: validate_mock_class() must be called for a class
> + """
> + assert isclass(mock_class), (
> + "validate_mock_class() must be called for a class")
> + base_classes = getmro(mock_class)
> + for name, obj in getmembers(mock_class):
> + if ismethod(obj):
> + for base_class in base_classes[1:]:
> + if (name in base_class.__dict__):

No need for the parenthesis here.

> + mock_args = getargspec(obj)
> + real_args = getargspec(base_class.__dict__[name])
> + if mock_args != real_args:
> + raise AssertionError(
> + ...

Read more...

review: Approve
Revision history for this message
Abel Deuring (adeuring) wrote :
Download full text (3.5 KiB)

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://www.voidspace.org.uk/python/mock/
> http://labix.org/mocker
> etc
>
> On Fri, 2009-10-23 at 08:54 +0000, Abel Deuring wrote:
> [...]
>> === modified file 'lib/lp/testing/__init__.py'
>> --- lib/lp/testing/__init__.py 2009-10-20 01:55:17 +0000
>> +++ lib/lp/testing/__init__.py 2009-10-23 08:01:11 +0000
>> @@ -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_mock_class(mock_class):
>> + """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_mock_class(B)
>> +
>> + 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_mock_class(C)
>> + 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_mock_class(D)
>> + Traceback (most recent call last):
>> + ...
>> + AssertionError: Different method signature for method_one:...
>> +
>> + If validate_mock_class() for anything but a class, we get an
>> + AssertionError.
>> +
>> + >>> validate_mock_class('a string')
>> + Traceback (most recent call last):
>> + ...
>> + AssertionError: validate_mock_class() must be called for a class
>> + """
>> + assert isclas...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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+