Merge lp:~adeuring/launchpad/bug-460935-hwdb-better-consistency-check-udev-usb-devices into lp:launchpad/db-devel

Proposed by Abel Deuring
Status: Merged
Merged at revision: not available
Proposed branch: lp:~adeuring/launchpad/bug-460935-hwdb-better-consistency-check-udev-usb-devices
Merge into: lp:launchpad/db-devel
Diff against target: 538 lines
4 files modified
lib/canonical/launchpad/scripts/hwdbsubmissions.py (+11/-4)
lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py (+121/-96)
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-460935-hwdb-better-consistency-check-udev-usb-devices
Reviewer Review Type Date Requested Status
Eleanor Berger (community) code Approve
Review via email: mp+13942@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) wrote :

When a HWDB submission is processed, many sanity checks are run. One of them is to see if a udev node where the property SUBSYSTEM is 'usb' also provides the properties DEVTYPE, PRODUCT and TYPE. (SUBSYSTEM tells us the "coarse" type of a udev node for "kernel viewpoint"; DEVTYPE, if defined, specifies a more fine-grained type; PRODUCT contains the vendor and product ID of a USB device; TYPE tell us the "class" of a USB device: printer/sound/input etc.

The udev data is quite fine-grained, especially it contains for many devices more than one node, where each node describes a different "aspect" of a physical device.

Writing the method SubmissionPasresr.checkUdevUsbProerties(), I assumed that each udev node where the property SUBSYSTEM is 'usb' also defined the properties DEVTYPE, PRODUCT and TYPE. Running the script for a few hundred real-world HWDB submissions, it turned out that this assumption is wrong: One sub-node of USB printers and USB input devices does not have these three properties, so I relaxed the sanity check: Each USB related node must now either provide all three properties or none of them.

test: ./bin/test --test=test_hwdb_submission_parser

= 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

== 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

== 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 etree complaint is not related to my changes.

Revision history for this message
Abel Deuring (adeuring) wrote :

This branch is based on
lp:~adeuring/launchpad/bug-458160-fix-submissionparser-checkconsistency, which I did not yet land.

the diff against that branch:

--- lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py 2009-10-23 08:02:11 +0000
+++ lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py 2009-10-26 09:59:21 +0000
@@ -1929,27 +1929,41 @@
             "'/devices/pci0000:00/0000:00:1f.2'")

     def testCheckUdevUsbProperties(self):
- """Test of SubmissionParser.checkUdevUsbProperties()."""
+ """Test of SubmissionParser.checkUdevUsbProperties().
+
+ udev nodes for USB devices must define the three properties
+ DEVTYPE, PRODUCT, TYPE or none of them.
+ """
         parser = SubmissionParser()
         self.assertTrue(parser.checkUdevUsbProperties(
             [self.udev_root_device, self.udev_usb_device,
              self.udev_usb_interface]))

+ for property_name in ('DEVTYPE', 'PRODUCT', 'TYPE'):
+ del self.udev_usb_device['E'][property_name]
+ self.assertTrue(parser.checkUdevUsbProperties(
+ [self.udev_root_device, self.udev_usb_device,
+ self.udev_usb_interface]))
+
     def testCheckUdevUsbProperties_missing_required_property(self):
         """Test of SubmissionParser.checkUdevUsbProperties().

- A USB device that does not have a required property makes a
- submission invalid.
+ A USB device where some but not all of the properties DEVTYPE,
+ PRODUCT, TYPE are defined makes a submission invalid.
         """
- del self.udev_usb_device['E']['DEVTYPE']
- parser = SubmissionParser(self.log)
- parser.submission_key = 'USB device without DEVTYPE property'
- self.assertFalse(parser.checkUdevUsbProperties(
- [self.udev_root_device, self.udev_usb_device]))
- self.assertErrorMessage(
- parser.submission_key,
- "USB udev device found without required properties: "
- "set(['DEVTYPE']) '/devices/pci0000:00/0000:00:1d.1/usb3/3-2'")
+ for property_name in ('DEVTYPE', 'PRODUCT', 'TYPE'):
+ saved_property = self.udev_usb_device['E'].pop(property_name)
+ parser = SubmissionParser(self.log)
+ parser.submission_key = (
+ 'USB device without %s property' % property_name)
+ self.assertFalse(parser.checkUdevUsbProperties(
+ [self.udev_root_device, self.udev_usb_device]))
+ self.assertErrorMessage(
+ parser.submission_key,
+ "USB udev device found without required properties: "
+ "set(['%s']) '/devices/pci0000:00/0000:00:1d.1/usb3/3-2'"
+ % property_name)
+ self.udev_usb_device['E'][property_name] = saved_property

     def testCheckUdevUsbProperties_with_invalid_product_id(self):
         """Test of SubmissionParser.checkUdevUsbProperties().

Revision history for this message
Eleanor Berger (intellectronica) :
review: Approve (code)

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-22 11:08:18 +0000
3+++ lib/canonical/launchpad/scripts/hwdbsubmissions.py 2009-10-26 10:45:33 +0000
4@@ -1228,9 +1228,11 @@
5 def checkUdevUsbProperties(self, udev_data):
6 """Validation of udev USB devices.
7
8- USB devices must have the properties DEVTYPE (value
9- 'usb_device' or 'usb_interface'), PRODUCT and TYPE. PRODUCT
10- must be a tuple of three integers in hexadecimal
11+ USB devices must either have the three properties DEVTYPE
12+ (value 'usb_device' or 'usb_interface'), PRODUCT and TYPE,
13+ or they must have none of them.
14+
15+ PRODUCT must be a tuple of three integers in hexadecimal
16 representation, separates by '/'. TYPE must be a a tuple of
17 three integers in decimal representation, separated by '/'.
18 usb_interface nodes must additionally have a property
19@@ -1245,6 +1247,10 @@
20 property_names = set(properties)
21 existing_usb_properties = property_names.intersection(
22 UDEV_USB_DEVICE_PROPERTIES)
23+
24+ if len(existing_usb_properties) == 0:
25+ continue
26+
27 if existing_usb_properties != UDEV_USB_DEVICE_PROPERTIES:
28 missing_properties = UDEV_USB_DEVICE_PROPERTIES.difference(
29 existing_usb_properties)
30@@ -1365,7 +1371,8 @@
31 if ('udev' in parsed_data['hardware']
32 and not self.checkConsistentUdevDeviceData(
33 parsed_data['hardware']['udev'],
34- parsed_data['hardware']['sysfs-attributes'])):
35+ parsed_data['hardware']['sysfs-attributes'],
36+ parsed_data['hardware']['dmi'],)):
37 return False
38 duplicate_ids = self.findDuplicateIDs(parsed_data)
39 if duplicate_ids:
40
41=== modified file 'lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py'
42--- lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py 2009-10-21 16:49:03 +0000
43+++ lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py 2009-10-26 10:45:33 +0000
44@@ -23,6 +23,7 @@
45 ROOT_UDI)
46 from canonical.testing import BaseLayer
47
48+from lp.testing import validate_mock_class
49
50 class SubmissionParserTestParseSoftware(SubmissionParser):
51 """A Variant used to test SubmissionParser._parseSoftware.
52@@ -1928,27 +1929,41 @@
53 "'/devices/pci0000:00/0000:00:1f.2'")
54
55 def testCheckUdevUsbProperties(self):
56- """Test of SubmissionParser.checkUdevUsbProperties()."""
57+ """Test of SubmissionParser.checkUdevUsbProperties().
58+
59+ udev nodes for USB devices must define the three properties
60+ DEVTYPE, PRODUCT, TYPE or none of them.
61+ """
62 parser = SubmissionParser()
63 self.assertTrue(parser.checkUdevUsbProperties(
64 [self.udev_root_device, self.udev_usb_device,
65 self.udev_usb_interface]))
66
67+ for property_name in ('DEVTYPE', 'PRODUCT', 'TYPE'):
68+ del self.udev_usb_device['E'][property_name]
69+ self.assertTrue(parser.checkUdevUsbProperties(
70+ [self.udev_root_device, self.udev_usb_device,
71+ self.udev_usb_interface]))
72+
73 def testCheckUdevUsbProperties_missing_required_property(self):
74 """Test of SubmissionParser.checkUdevUsbProperties().
75
76- A USB device that does not have a required property makes a
77- submission invalid.
78+ A USB device where some but not all of the properties DEVTYPE,
79+ PRODUCT, TYPE are defined makes a submission invalid.
80 """
81- del self.udev_usb_device['E']['DEVTYPE']
82- parser = SubmissionParser(self.log)
83- parser.submission_key = 'USB device without DEVTYPE property'
84- self.assertFalse(parser.checkUdevUsbProperties(
85- [self.udev_root_device, self.udev_usb_device]))
86- self.assertErrorMessage(
87- parser.submission_key,
88- "USB udev device found without required properties: "
89- "set(['DEVTYPE']) '/devices/pci0000:00/0000:00:1d.1/usb3/3-2'")
90+ for property_name in ('DEVTYPE', 'PRODUCT', 'TYPE'):
91+ saved_property = self.udev_usb_device['E'].pop(property_name)
92+ parser = SubmissionParser(self.log)
93+ parser.submission_key = (
94+ 'USB device without %s property' % property_name)
95+ self.assertFalse(parser.checkUdevUsbProperties(
96+ [self.udev_root_device, self.udev_usb_device]))
97+ self.assertErrorMessage(
98+ parser.submission_key,
99+ "USB udev device found without required properties: "
100+ "set(['%s']) '/devices/pci0000:00/0000:00:1d.1/usb3/3-2'"
101+ % property_name)
102+ self.udev_usb_device['E'][property_name] = saved_property
103
104 def testCheckUdevUsbProperties_with_invalid_product_id(self):
105 """Test of SubmissionParser.checkUdevUsbProperties().
106@@ -2102,7 +2117,7 @@
107
108 All shortcut methods return True.
109 """
110- def checkUdevDictsHavePathKey(self, udev_data):
111+ def checkUdevDictsHavePathKey(self, udev_nodes):
112 """See `SubmissionParser`."""
113 return True
114
115@@ -2114,7 +2129,7 @@
116 """See `SubmissionParser`."""
117 return True
118
119- def checkUdevScsiProperties(self, udev_data, syfs_data):
120+ def checkUdevScsiProperties(self, udev_data, sysfs_data):
121 """See `SubmissionParser`."""
122 return True
123
124@@ -2122,6 +2137,8 @@
125 """See `SubmissionParser`."""
126 return True
127
128+ validate_mock_class(UdevTestSubmissionParser)
129+
130 def testCheckConsistentUdevDeviceData(self):
131 """Test of SubmissionParser.checkConsistentUdevDeviceData(),"""
132 parser = self.UdevTestSubmissionParser()
133@@ -2137,10 +2154,12 @@
134 self.UdevTestSubmissionParser):
135 """A SubmissionPaser where checkUdevDictsHavePathKey() fails."""
136
137- def checkUdevDictsHavePathKey(self, udev_data):
138+ def checkUdevDictsHavePathKey(self, udev_nodes):
139 """See `SubmissionParser`."""
140 return False
141
142+ validate_mock_class(SubmissionParserUdevPathCheckFails)
143+
144 parser = SubmissionParserUdevPathCheckFails()
145 self.assertFalse(parser.checkConsistentUdevDeviceData(
146 None, None, None))
147@@ -2158,6 +2177,8 @@
148 """See `SubmissionParser`."""
149 return False
150
151+ validate_mock_class(SubmissionParserUdevPciCheckFails)
152+
153 parser = SubmissionParserUdevPciCheckFails()
154 self.assertFalse(parser.checkConsistentUdevDeviceData(
155 None, None, None))
156@@ -2175,6 +2196,8 @@
157 """See `SubmissionParser`."""
158 return False
159
160+ validate_mock_class(SubmissionParserUdevUsbCheckFails)
161+
162 parser = SubmissionParserUdevUsbCheckFails()
163 self.assertFalse(parser.checkConsistentUdevDeviceData(
164 None, None, None))
165@@ -2192,6 +2215,8 @@
166 """See `SubmissionParser`."""
167 return False
168
169+ validate_mock_class(SubmissionParserUdevUsbCheckFails)
170+
171 parser = SubmissionParserUdevUsbCheckFails()
172 self.assertFalse(parser.checkConsistentUdevDeviceData(
173 None, None, None))
174@@ -2209,55 +2234,38 @@
175 """See `SubmissionParser`."""
176 return False
177
178+ validate_mock_class(SubmissionParserUdevUsbCheckFails)
179+
180 parser = SubmissionParserUdevUsbCheckFails()
181 self.assertFalse(parser.checkConsistentUdevDeviceData(
182 None, None, None))
183
184- def _setupConsistencyCheckParser(self):
185- """Prepare and return a SubmissionParser instance.
186+ class MockSubmissionParser(SubmissionParser):
187+ """A SubmissionParser variant for testing checkCOnsistentData()
188
189 All "method substitutes" return a valid result.
190 """
191- test = self
192+
193 def findDuplicateIDs(self, parsed_data):
194- test.assertTrue(isinstance(self, SubmissionParser))
195 return set()
196
197 def findInvalidIDReferences(self, parsed_data):
198- test.assertTrue(isinstance(self, SubmissionParser))
199 return set()
200
201 def getUDIDeviceMap(self, devices):
202- test.assertTrue(isinstance(self, SubmissionParser))
203 return {}
204
205 def getUDIChildren(self, udi_device_map):
206- test.assertTrue(isinstance(self, SubmissionParser))
207 return {}
208
209- def checkHALDevicesParentChildConsistency(self, devices):
210- test.assertTrue(isinstance(self, SubmissionParser))
211+ def checkHALDevicesParentChildConsistency(self, udi_children):
212 return []
213
214- def checkConsistentUdevDeviceData(self, udev_data, sysfs_data):
215+ def checkConsistentUdevDeviceData(
216+ self, udev_data, sysfs_data, dmi_data):
217 return True
218
219- parser = SubmissionParser(self.log)
220- parser.findDuplicateIDs = (
221- lambda parsed_data: findDuplicateIDs(parser, parsed_data))
222- parser.findInvalidIDReferences = (
223- lambda parsed_data: findInvalidIDReferences(parser, parsed_data))
224- parser.getUDIDeviceMap = (
225- lambda devices: getUDIDeviceMap(parser, devices))
226- parser.getUDIChildren = (
227- lambda udi_device_map: getUDIChildren(parser, udi_device_map))
228- parser.checkHALDevicesParentChildConsistency = (
229- lambda udi_children: checkHALDevicesParentChildConsistency(
230- parser, udi_children))
231- parser.checkConsistentUdevDeviceData = (
232- lambda udev_data, sysfs_data: checkConsistentUdevDeviceData(
233- parser, udev_data, sysfs_data))
234- return parser
235+ validate_mock_class(MockSubmissionParser)
236
237 def assertErrorMessage(self, submission_key, log_message):
238 """Search for message in the log entries for submission_key.
239@@ -2309,7 +2317,7 @@
240
241 def testConsistencyCheck(self):
242 """Test of SubmissionParser.checkConsistency."""
243- parser = self._setupConsistencyCheckParser()
244+ parser = self.MockSubmissionParser()
245 result = parser.checkConsistency({'hardware':
246 {'hal': {'devices': []}}})
247 self.assertEqual(result, True,
248@@ -2318,45 +2326,53 @@
249
250 def testConsistencyCheckValidUdevData(self):
251 """Test of SubmissionParser.checkConsistency."""
252- parser = self._setupConsistencyCheckParser()
253+ parser = self.MockSubmissionParser()
254 self.assertTrue(parser.checkConsistency(
255 {
256 'hardware': {
257- 'udev': [],
258- 'sysfs-attributes': []
259+ 'udev': None,
260+ 'sysfs-attributes': None,
261+ 'dmi': None,
262 }
263 }
264 ))
265
266 def testConsistencyCheck_invalid_udev_data(self):
267 """Test of SubmissionParser.checkConsistency."""
268- def checkConsistentUdevDeviceData(self, udev_data, sysfs_data):
269- return False
270-
271- parser = self._setupConsistencyCheckParser()
272- parser.checkConsistentUdevDeviceData = (
273- lambda udev_data, sysfs_data: checkConsistentUdevDeviceData(
274- parser, udev_data, sysfs_data))
275+ class MockSubmissionParserBadUdevDeviceData(
276+ self.MockSubmissionParser):
277+ """A parser where checkConsistentUdevDeviceData() fails."""
278+
279+ def checkConsistentUdevDeviceData(self, udev_data, sysfs_data,
280+ dmi_data):
281+ return False
282+
283+ validate_mock_class(MockSubmissionParserBadUdevDeviceData)
284+
285+ parser = MockSubmissionParserBadUdevDeviceData()
286 self.assertFalse(parser.checkConsistency(
287 {
288 'hardware': {
289- 'udev': [{}],
290- 'sysfs-attributes': []
291+ 'udev': None,
292+ 'sysfs-attributes': None,
293+ 'dmi': None,
294 }
295 }
296 ))
297
298 def testConsistencyCheckWithDuplicateIDs(self):
299 """SubmissionParser.checkConsistency detects duplicate IDs."""
300- test = self
301- def findDuplicateIDs(self, parsed_data):
302- test.assertTrue(isinstance(self, SubmissionParser))
303- return set([1])
304-
305- parser = self._setupConsistencyCheckParser()
306+ class MockSubmissionParserDuplicateIds(
307+ self.MockSubmissionParser):
308+ """A parser where findDuplicateIDs() fails."""
309+
310+ def findDuplicateIDs(self, parsed_data):
311+ return set([1])
312+
313+ validate_mock_class(MockSubmissionParserDuplicateIds)
314+
315+ parser = MockSubmissionParserDuplicateIds(self.log)
316 parser.submission_key = 'Consistency check detects duplicate IDs'
317- parser.findDuplicateIDs = (
318- lambda parsed_data: findDuplicateIDs(parser, parsed_data))
319 result = parser.checkConsistency({'hardware':
320 {'hal': {'devices': []}}})
321 self.assertEqual(result, False,
322@@ -2366,15 +2382,16 @@
323
324 def testConsistencyCheckWithInvalidIDReferences(self):
325 """SubmissionParser.checkConsistency detects invalid ID references."""
326- test = self
327- def findInvalidIDReferences(self, parsed_data):
328- test.assertTrue(isinstance(self, SubmissionParser))
329- return set([1])
330-
331- parser = self._setupConsistencyCheckParser()
332+ class MockSubmissionParserInvalidIDReferences(
333+ self.MockSubmissionParser):
334+ """A parser where findInvalidIDReferences() fails."""
335+ def findInvalidIDReferences(self, parsed_data):
336+ return set([1])
337+
338+ validate_mock_class(MockSubmissionParserInvalidIDReferences)
339+
340+ parser = MockSubmissionParserInvalidIDReferences(self.log)
341 parser.submission_key = 'Consistency check detects invalid ID refs'
342- parser.findInvalidIDReferences = (
343- lambda parsed_data: findInvalidIDReferences(parser, parsed_data))
344 result = parser.checkConsistency({'hardware':
345 {'hal': {'devices': []}}})
346 self.assertEqual(result, False,
347@@ -2384,16 +2401,18 @@
348
349 def testConsistencyCheckWithDuplicateUDI(self):
350 """SubmissionParser.checkConsistency detects duplicate UDIs."""
351- test = self
352- def getUDIDeviceMap(self, parsed_data):
353- test.assertTrue(isinstance(self, SubmissionParser))
354- raise ValueError(
355- 'Duplicate UDI: /org/freedesktop/Hal/devices/computer')
356-
357- parser = self._setupConsistencyCheckParser()
358+ class MockSubmissionParserUDIDeviceMapFails(
359+ self.MockSubmissionParser):
360+ """A parser where getUDIDeviceMap() fails."""
361+
362+ def getUDIDeviceMap(self, devices):
363+ raise ValueError(
364+ 'Duplicate UDI: /org/freedesktop/Hal/devices/computer')
365+
366+ validate_mock_class(MockSubmissionParserUDIDeviceMapFails)
367+
368+ parser = MockSubmissionParserUDIDeviceMapFails(self.log)
369 parser.submission_key = 'Consistency check detects invalid ID refs'
370- parser.getUDIDeviceMap = (
371- lambda devices: getUDIDeviceMap(parser, devices))
372 result = parser.checkConsistency({'hardware':
373 {'hal': {'devices': []}}})
374 self.assertEqual(result, False,
375@@ -2404,15 +2423,17 @@
376
377 def testConsistencyCheckChildUDIWithoutParent(self):
378 """SubmissionParser.checkConsistency detects "orphaned" devices."""
379- test = self
380- def getUDIChildren(self, udi_device_map):
381- test.assertTrue(isinstance(self, SubmissionParser))
382- raise ValueError('Unknown parent UDI /foo in <device id="3">')
383-
384- parser = self._setupConsistencyCheckParser()
385+ class MockSubmissionParserUDIChildrenFails(
386+ self.MockSubmissionParser):
387+ """A parser where getUDIChildren() fails."""
388+
389+ def getUDIChildren(self, udi_device_map):
390+ raise ValueError('Unknown parent UDI /foo in <device id="3">')
391+
392+ validate_mock_class(MockSubmissionParserUDIChildrenFails)
393+
394+ parser = MockSubmissionParserUDIChildrenFails(self.log)
395 parser.submission_key = 'Consistency check detects invalid ID refs'
396- parser.getUDIChildren = (
397- lambda udi_device_map: getUDIChildren(parser, udi_device_map))
398 result = parser.checkConsistency({'hardware':
399 {'hal': {'devices': []}}})
400 self.assertEqual(result, False,
401@@ -2423,17 +2444,21 @@
402
403 def testConsistencyCheckCircularParentChildRelation(self):
404 """SubmissionParser.checkConsistency detects "orphaned" devices."""
405- test = self
406- def checkHALDevicesParentChildConsistency(self, devices):
407- test.assertTrue(isinstance(self, SubmissionParser))
408- return ['/foo', '/bar']
409-
410- parser = self._setupConsistencyCheckParser()
411+ class MockSubmissionParserHALDevicesParentChildConsistency(
412+ self.MockSubmissionParser):
413+ """A parser where checkHALDevicesParentChildConsistency() fails.
414+ """
415+
416+ def checkHALDevicesParentChildConsistency(self, udi_children):
417+ return ['/foo', '/bar']
418+
419+ validate_mock_class(
420+ MockSubmissionParserHALDevicesParentChildConsistency)
421+
422+ parser = MockSubmissionParserHALDevicesParentChildConsistency(
423+ self.log)
424 parser.submission_key = ('Consistency check detects circular '
425 'parent-child relationships')
426- parser.checkHALDevicesParentChildConsistency = (
427- lambda devices: checkHALDevicesParentChildConsistency(
428- parser, devices))
429 result = parser.checkConsistency({'hardware':
430 {'hal': {'devices': []}}})
431 self.assertEqual(result, False,
432
433=== modified file 'lib/lp/testing/__init__.py'
434--- lib/lp/testing/__init__.py 2009-10-23 11:07:32 +0000
435+++ lib/lp/testing/__init__.py 2009-10-26 10:45:33 +0000
436@@ -8,6 +8,7 @@
437 from datetime import datetime, timedelta
438 from pprint import pformat
439 import copy
440+from inspect import getargspec, getmembers, getmro, isclass, ismethod
441 import os
442 import shutil
443 import subprocess
444@@ -793,3 +794,69 @@
445 tree.unlock()
446
447 return contents
448+
449+def validate_mock_class(mock_class):
450+ """Validate method signatures in mock classes derived from real classes.
451+
452+ We often use mock classes in tests which are derived from real
453+ classes.
454+
455+ This function ensures that methods redefined in the mock
456+ class have the same signature as the corresponding methods of
457+ the base class.
458+
459+ >>> class A:
460+ ...
461+ ... def method_one(self, a):
462+ ... pass
463+
464+ >>>
465+ >>> class B(A):
466+ ... def method_one(self, a):
467+ ... pass
468+ >>> validate_mock_class(B)
469+
470+ If a class derived from A defines method_one with a different
471+ signature, we get an AssertionError.
472+
473+ >>> class C(A):
474+ ... def method_one(self, a, b):
475+ ... pass
476+ >>> validate_mock_class(C)
477+ Traceback (most recent call last):
478+ ...
479+ AssertionError: Different method signature for method_one:...
480+
481+ Even a parameter name must not be modified.
482+
483+ >>> class D(A):
484+ ... def method_one(self, b):
485+ ... pass
486+ >>> validate_mock_class(D)
487+ Traceback (most recent call last):
488+ ...
489+ AssertionError: Different method signature for method_one:...
490+
491+ If validate_mock_class() for anything but a class, we get an
492+ AssertionError.
493+
494+ >>> validate_mock_class('a string')
495+ Traceback (most recent call last):
496+ ...
497+ AssertionError: validate_mock_class() must be called for a class
498+ """
499+ assert isclass(mock_class), (
500+ "validate_mock_class() must be called for a class")
501+ base_classes = getmro(mock_class)
502+ for name, obj in getmembers(mock_class):
503+ if ismethod(obj):
504+ for base_class in base_classes[1:]:
505+ if name in base_class.__dict__:
506+ mock_args = getargspec(obj)
507+ real_args = getargspec(base_class.__dict__[name])
508+ if mock_args != real_args:
509+ raise AssertionError(
510+ 'Different method signature for %s: %r %r' % (
511+ name, mock_args, real_args))
512+ else:
513+ break
514
515=== added file 'lib/lp/testing/tests/test_inlinetests.py'
516--- lib/lp/testing/tests/test_inlinetests.py 1970-01-01 00:00:00 +0000
517+++ lib/lp/testing/tests/test_inlinetests.py 2009-10-26 10:45:33 +0000
518@@ -0,0 +1,20 @@
519+# Copyright 2009 Canonical Ltd. This software is licensed under the
520+# GNU Affero General Public License version 3 (see the file LICENSE).
521+
522+"""Run the doc string tests."""
523+
524+import doctest
525+
526+from zope.testing.doctest import NORMALIZE_WHITESPACE, ELLIPSIS
527+
528+from canonical.launchpad.testing.systemdocs import LayeredDocFileSuite
529+from canonical.testing import BaseLayer
530+from lp import testing
531+
532+def test_suite():
533+ suite = LayeredDocFileSuite(
534+ layer=BaseLayer)
535+ suite.addTest(doctest.DocTestSuite(
536+ testing, optionflags=NORMALIZE_WHITESPACE|ELLIPSIS))
537+ return suite
538+

Subscribers

People subscribed via source and target branches

to status/vote changes: