Merge lp:~leonardr/lazr.restful/forbid-reference-to-entry-not-published-in-this-version into lp:lazr.restful
- forbid-reference-to-entry-not-published-in-this-version
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 183 |
Proposed branch: | lp:~leonardr/lazr.restful/forbid-reference-to-entry-not-published-in-this-version |
Merge into: | lp:lazr.restful |
Prerequisite: | lp:~leonardr/lazr.restful/entry-introduced-in-version |
Diff against target: |
512 lines (+224/-42) 4 files modified
src/lazr/restful/metazcml.py (+112/-16) src/lazr/restful/publisher.py (+0/-1) src/lazr/restful/testing/webservice.py (+28/-0) src/lazr/restful/tests/test_declarations.py (+84/-25) |
To merge this branch: | bzr merge lp:~leonardr/lazr.restful/forbid-reference-to-entry-not-published-in-this-version |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tim Penhey (community) | Needs Fixing | ||
Review via email: mp+53918@code.launchpad.net |
Commit message
Description of the change
This branch makes it impossible to define a Reference to an unpublished interface. This might be an interface that's not published *at all*, because you forgot export_
The sanity checking happens after all the registrations are done. (This is necessary because IFoo may be defined after IBar, and the Reference schema patched.) I gather information about each published interface as it's registered. After they've all been registered, I get a list of all the IEntry registrations for different versions. (In practice, these two lists should be the same, but I don't know if I want to rely on that--an application might do some manual registrations or other trickery.)
Then, for each version, for each Reference field in a published interface, I see if the Reference points to some Interface that's registered for that version.
The sanity check code also keeps track of named operation registrations. This is for the next phase of the work, in which you will be prohibited from publishing a named operation in 1.0 if it has arguments or a return value of a type not published in 1.0.
I suspect I have not come up with all the tests necessary to show that this works in general; let me know if you can come up with more.
- 209. By Leonard Richardson
-
Replaced _assertRaises with a copy of assertRaises. Made a test pass for the right reasons. Change a complicated string interpolation to use a dict mapping.
- 210. By Leonard Richardson
-
Added explanation of why my 'success' test worked right away even though I didn't expect it to.
Leonard Richardson (leonardr) wrote : | # |
I figured out why I was worried about not having all the tests. Basically, I didn't expect test_reference_
The answer is that lazr.restful creates and registers an IEntry adapter for each published entry for each version. It's not smart enough to see that an entry hasn't changed since the last version. Now that lack of optimization has paid off.
Preview Diff
1 | === modified file 'src/lazr/restful/metazcml.py' |
2 | --- src/lazr/restful/metazcml.py 2011-03-10 14:07:30 +0000 |
3 | +++ src/lazr/restful/metazcml.py 2011-03-18 12:22:30 +0000 |
4 | @@ -9,24 +9,41 @@ |
5 | import inspect |
6 | import itertools |
7 | |
8 | -from zope.component import getUtility |
9 | +from zope.component import ( |
10 | + getGlobalSiteManager, |
11 | + getUtility, |
12 | + ) |
13 | from zope.component.zcml import handler |
14 | from zope.configuration.fields import GlobalObject |
15 | from zope.interface import Interface |
16 | from zope.interface.interfaces import IInterface |
17 | |
18 | - |
19 | from lazr.restful.declarations import ( |
20 | - COLLECTION_TYPE, ENTRY_TYPE, LAZR_WEBSERVICE_EXPORTED, OPERATION_TYPES, |
21 | - REMOVED_OPERATION_TYPE, generate_collection_adapter, |
22 | - generate_entry_adapters, generate_entry_interfaces, |
23 | - generate_operation_adapter) |
24 | + COLLECTION_TYPE, |
25 | + ENTRY_TYPE, |
26 | + LAZR_WEBSERVICE_EXPORTED, |
27 | + OPERATION_TYPES, |
28 | + REMOVED_OPERATION_TYPE, |
29 | + generate_collection_adapter, |
30 | + generate_entry_adapters, |
31 | + generate_entry_interfaces, |
32 | + generate_operation_adapter, |
33 | + ) |
34 | from lazr.restful.error import WebServiceExceptionView |
35 | |
36 | from lazr.restful.interfaces import ( |
37 | - ICollection, IEntry, IResourceDELETEOperation, IResourceGETOperation, |
38 | - IResourcePOSTOperation, IWebServiceClientRequest, |
39 | - IWebServiceConfiguration, IWebServiceVersion) |
40 | + ICollection, |
41 | + IEntry, |
42 | + IReference, |
43 | + IResourceDELETEOperation, |
44 | + IResourceGETOperation, |
45 | + IResourcePOSTOperation, |
46 | + IWebServiceClientRequest, |
47 | + IWebServiceConfiguration, |
48 | + IWebServiceVersion, |
49 | + ) |
50 | + |
51 | +from lazr.restful.utils import VersionedObject |
52 | |
53 | |
54 | class IRegisterDirective(Interface): |
55 | @@ -36,7 +53,8 @@ |
56 | title=u'Module which will be inspected for webservice declarations') |
57 | |
58 | |
59 | -def generate_and_register_entry_adapters(interface, info, contributors): |
60 | +def generate_and_register_entry_adapters(interface, info, contributors, |
61 | + repository=None): |
62 | """Generate an entry adapter for every version of the web service. |
63 | |
64 | This code generates an IEntry subinterface for every version, each |
65 | @@ -54,9 +72,9 @@ |
66 | interface, contributors, *versions) |
67 | web_factories = generate_entry_adapters( |
68 | interface, contributors, web_interfaces) |
69 | - for i in range(0, len(web_interfaces)): |
70 | - interface_version, web_interface = web_interfaces[i] |
71 | - factory_version, factory = web_factories[i] |
72 | + for index, (interface_version, web_interface) in ( |
73 | + enumerate(web_interfaces)): |
74 | + factory_version, factory = web_factories[index] |
75 | assert factory_version==interface_version, ( |
76 | "Generated interface and factory versions don't match up! " |
77 | '%s vs. %s' % (factory_version, interface_version)) |
78 | @@ -69,6 +87,11 @@ |
79 | register_adapter_for_version(factory, interface, interface_version, |
80 | IEntry, '', info) |
81 | |
82 | + # If we were given a repository, add the interface and version |
83 | + # to it. |
84 | + if repository is not None: |
85 | + repository.append(VersionedObject(interface_version, web_interface)) |
86 | + |
87 | |
88 | def ensure_correct_version_ordering(name, version_list): |
89 | """Make sure that a list mentions versions from earliest to latest. |
90 | @@ -146,6 +169,7 @@ |
91 | handler('registerAdapter', factory, (interface, marker), |
92 | provides, name, info) |
93 | |
94 | + |
95 | def _is_exported_interface(member): |
96 | """Helper for find_exported_interfaces; a predicate to inspect.getmembers. |
97 | |
98 | @@ -163,6 +187,7 @@ |
99 | return True |
100 | return False |
101 | |
102 | + |
103 | def find_exported_interfaces(module): |
104 | """Find all the interfaces in a module marked for export. |
105 | |
106 | @@ -173,6 +198,7 @@ |
107 | return (interface for name, interface |
108 | in inspect.getmembers(module, _is_exported_interface)) |
109 | |
110 | + |
111 | def find_interfaces_and_contributors(module): |
112 | """Find the interfaces and its contributors marked for export. |
113 | |
114 | @@ -254,6 +280,11 @@ |
115 | module, type(module)) |
116 | interfaces_with_contributors = find_interfaces_and_contributors(module) |
117 | |
118 | + # Keep track of entry and operation registrations so we can |
119 | + # sanity-check them later. |
120 | + registered_entries = [] |
121 | + registered_operations = [] |
122 | + |
123 | for interface, contributors in interfaces_with_contributors.items(): |
124 | if issubclass(interface, Exception): |
125 | register_exception_view(context, interface) |
126 | @@ -264,7 +295,8 @@ |
127 | context.action( |
128 | discriminator=('webservice entry interface', interface), |
129 | callable=generate_and_register_entry_adapters, |
130 | - args=(interface, context.info, contributors), |
131 | + args=(interface, context.info, contributors, |
132 | + registered_entries), |
133 | ) |
134 | elif tag['type'] == COLLECTION_TYPE: |
135 | for version in tag['collection_default_content'].keys(): |
136 | @@ -282,12 +314,74 @@ |
137 | raise AssertionError('Unknown export type: %s' % tag['type']) |
138 | context.action( |
139 | discriminator=('webservice versioned operations', interface), |
140 | - args=(context, interface, contributors), |
141 | + args=(context, interface, contributors, registered_operations), |
142 | callable=generate_and_register_webservice_operations) |
143 | |
144 | + # Now that all the adapters and operations are registered, run a |
145 | + # sanity check to make sure no version of the web service |
146 | + # references a feature not available in that version |
147 | + context.action( |
148 | + discriminator=('webservice sanity checks'), |
149 | + args=(registered_entries, registered_operations), |
150 | + callable=webservice_sanity_checks) |
151 | + |
152 | + |
153 | +def webservice_sanity_checks(entries, operations): |
154 | + """Ensure the web service contains no references to unpublished objects. |
155 | + |
156 | + We are worried about fields that link to unpublished objects, and |
157 | + operations that have arguments or return values that are |
158 | + unpublished. An unpublished object may not be published at all, or |
159 | + it may not be published in the same version in which it's |
160 | + referenced. |
161 | + """ |
162 | + |
163 | + # Create a mapping of marker interfaces to version names. |
164 | + versions = getUtility(IWebServiceConfiguration).active_versions |
165 | + version_for_marker = { IWebServiceClientRequest: versions[0] } |
166 | + for version in versions: |
167 | + marker_interface = getUtility(IWebServiceVersion, name=version) |
168 | + version_for_marker[marker_interface] = version |
169 | + |
170 | + # For each version, build a list of all of the IEntries published |
171 | + # in that version's web service. This works because every IEntry |
172 | + # is explicitly registered for every version, even if there's been |
173 | + # no change since the last version. For the sake of performance, |
174 | + # the list is stored as a set of VersionedObject 2-tuples. |
175 | + available_registrations = set() |
176 | + registrations = getGlobalSiteManager().registeredAdapters() |
177 | + for registration in registrations: |
178 | + if (not IInterface.providedBy(registration.provided) |
179 | + or not registration.provided.isOrExtends(IEntry)): |
180 | + continue |
181 | + interface, version_marker = registration.required |
182 | + available_registrations.add(VersionedObject( |
183 | + version_for_marker[version_marker], interface)) |
184 | + |
185 | + # Check every Reference field in every IEntry interface, making |
186 | + # sure that it's a Reference to another IEntry published in that |
187 | + # version. |
188 | + for version, interface in entries: |
189 | + if version is None: |
190 | + version = versions[0] |
191 | + for name, field in interface.namesAndDescriptions(): |
192 | + if IReference.providedBy(field): |
193 | + referenced_interface = field.schema |
194 | + to_check = VersionedObject(version, referenced_interface) |
195 | + if to_check not in available_registrations: |
196 | + raise ValueError( |
197 | + "In version %(version)s, %(interface)s.%(field)s " |
198 | + "is a Reference to %(reference)s, but version " |
199 | + "%(version)s of the web service does not publish " |
200 | + "%(reference)s as an entry. (It may not be published " |
201 | + "at all.)" % dict( |
202 | + version=version, interface=interface.__name__, |
203 | + field=field.__name__, |
204 | + reference=referenced_interface.__name__)) |
205 | + |
206 | |
207 | def generate_and_register_webservice_operations( |
208 | - context, interface, contributors): |
209 | + context, interface, contributors, repository=None): |
210 | """Create and register adapters for all exported methods. |
211 | |
212 | Different versions of the web service may publish the same |
213 | @@ -458,6 +552,8 @@ |
214 | # the operation registration for this mutator |
215 | # will need to be blocked. |
216 | mutator_operation_needs_to_be_blocked = True |
217 | + if repository is not None: |
218 | + repository.append(VersionedObject(version, method)) |
219 | previous_operation_name = operation_name |
220 | previous_operation_provides = operation_provides |
221 | |
222 | |
223 | === modified file 'src/lazr/restful/publisher.py' |
224 | --- src/lazr/restful/publisher.py 2011-01-26 19:32:05 +0000 |
225 | +++ src/lazr/restful/publisher.py 2011-03-18 12:22:30 +0000 |
226 | @@ -9,7 +9,6 @@ |
227 | __metaclass__ = type |
228 | __all__ = [ |
229 | 'browser_request_to_web_service_request', |
230 | - 'TraverseWithGet', |
231 | 'WebServicePublicationMixin', |
232 | 'WebServiceRequestTraversal', |
233 | ] |
234 | |
235 | === modified file 'src/lazr/restful/testing/webservice.py' |
236 | --- src/lazr/restful/testing/webservice.py 2011-03-10 13:30:32 +0000 |
237 | +++ src/lazr/restful/testing/webservice.py 2011-03-18 12:22:30 +0000 |
238 | @@ -472,6 +472,34 @@ |
239 | sm.registerUtility( |
240 | IWebServiceTestRequest20, IWebServiceVersion, name='2.0') |
241 | |
242 | + def assertRaises(self, excClass, callableObj, *args, **kwargs): |
243 | + """Fail unless an exception of class excClass is thrown |
244 | + by callableObj when invoked with arguments args and keyword |
245 | + arguments kwargs. If a different type of exception is |
246 | + thrown, it will not be caught, and the test case will be |
247 | + deemed to have suffered an error, exactly as for an |
248 | + unexpected exception. |
249 | + |
250 | + This is a copy of assertRaises from testtools. |
251 | + """ |
252 | + try: |
253 | + ret = callableObj(*args, **kwargs) |
254 | + except excClass: |
255 | + return sys.exc_info()[1] |
256 | + else: |
257 | + excName = self._formatTypes(excClass) |
258 | + self.fail("%s not raised, %r returned instead." % (excName, ret)) |
259 | + |
260 | + def _formatTypes(self, classOrIterable): |
261 | + """Format a class or a bunch of classes for display in an error. |
262 | + |
263 | + This is a copy of _formatTypes from testtools. |
264 | + """ |
265 | + className = getattr(classOrIterable, '__name__', None) |
266 | + if className is None: |
267 | + className = ', '.join(klass.__name__ for klass in classOrIterable) |
268 | + return className |
269 | + |
270 | def fake_request(self, version): |
271 | request = FakeRequest(version=version) |
272 | alsoProvides( |
273 | |
274 | === modified file 'src/lazr/restful/tests/test_declarations.py' |
275 | --- src/lazr/restful/tests/test_declarations.py 2011-03-18 12:22:30 +0000 |
276 | +++ src/lazr/restful/tests/test_declarations.py 2011-03-18 12:22:30 +0000 |
277 | @@ -1,3 +1,4 @@ |
278 | +from zope.configuration.config import ConfigurationExecutionError |
279 | from zope.component import ( |
280 | adapts, |
281 | getMultiAdapter, |
282 | @@ -102,7 +103,7 @@ |
283 | # with its original name whereas for the '2.0' version it's exported |
284 | # as 'development_branch_20'. |
285 | self.product._dev_branch = Branch('A product branch') |
286 | - register_test_module('testmod', IProduct, IHasBranches) |
287 | + register_test_module('testmod', IBranch, IProduct, IHasBranches) |
288 | adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry) |
289 | self.assertEqual(adapter.development_branch, self.product._dev_branch) |
290 | |
291 | @@ -116,7 +117,7 @@ |
292 | # development_branch field, but on version '2.0' that field can be |
293 | # modified as we define a mutator for it. |
294 | self.product._dev_branch = Branch('A product branch') |
295 | - register_test_module('testmod', IProduct, IHasBranches) |
296 | + register_test_module('testmod', IBranch, IProduct, IHasBranches) |
297 | adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry) |
298 | try: |
299 | adapter.development_branch = None |
300 | @@ -151,7 +152,8 @@ |
301 | # from both IHasBugs and IHasBranches. |
302 | self.product._bug_count = 10 |
303 | self.product._dev_branch = Branch('A product branch') |
304 | - register_test_module('testmod', IProduct, IHasBugs, IHasBranches) |
305 | + register_test_module( |
306 | + 'testmod', IBranch, IProduct, IHasBugs, IHasBranches) |
307 | adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry) |
308 | self.assertEqual(adapter.bug_count, 10) |
309 | self.assertEqual(adapter.development_branch, self.product._dev_branch) |
310 | @@ -172,7 +174,8 @@ |
311 | # When looking up an entry's redacted_fields, we take into account the |
312 | # interface where the field is defined and adapt the context to that |
313 | # interface before accessing that field. |
314 | - register_test_module('testmod', IProduct, IHasBugs, IHasBranches) |
315 | + register_test_module( |
316 | + 'testmod', IBranch, IProduct, IHasBugs, IHasBranches) |
317 | entry_resource = EntryResource(self.product, self.one_zero_request) |
318 | self.assertEquals([], entry_resource.redacted_fields) |
319 | |
320 | @@ -180,7 +183,8 @@ |
321 | # When looking up an entry's redacted_fields for an object which is |
322 | # security proxied, we use the security checker for the interface |
323 | # where the field is defined. |
324 | - register_test_module('testmod', IProduct, IHasBugs, IHasBranches) |
325 | + register_test_module( |
326 | + 'testmod', IBranch, IProduct, IHasBugs, IHasBranches) |
327 | newInteraction() |
328 | try: |
329 | secure_product = ProxyFactory( |
330 | @@ -194,7 +198,8 @@ |
331 | def test_duplicate_contributed_attributes(self): |
332 | # We do not allow a given attribute to be contributed to a given |
333 | # interface by more than one contributing interface. |
334 | - testmod = create_test_module('testmod', IProduct, IHasBugs, IHasBugs2) |
335 | + testmod = create_test_module( |
336 | + 'testmod', IBranch, IProduct, IHasBugs, IHasBugs2) |
337 | self.assertRaises( |
338 | ConflictInContributingInterfaces, |
339 | find_interfaces_and_contributors, testmod) |
340 | @@ -205,7 +210,7 @@ |
341 | class DummyHasBranches: |
342 | implements(IHasBranches) |
343 | dummy = DummyHasBranches() |
344 | - register_test_module('testmod', IProduct, IHasBranches) |
345 | + register_test_module('testmod', IBranch, IProduct, IHasBranches) |
346 | self.assertRaises( |
347 | ComponentLookupError, |
348 | getMultiAdapter, (dummy, self.one_zero_request), IEntry) |
349 | @@ -318,6 +323,7 @@ |
350 | |
351 | |
352 | class IBranch(Interface): |
353 | + export_as_webservice_entry() |
354 | name = TextLine(title=u'The branch name') |
355 | |
356 | |
357 | @@ -552,16 +558,6 @@ |
358 | self.utility = getUtility(IWebServiceConfiguration) |
359 | self.utility.require_explicit_versions = True |
360 | |
361 | - def _assertRaises(self, exception, func, *args, **kwargs): |
362 | - # Approximate the behavior of testtools assertRaises, which |
363 | - # returns the raised exception. I couldn't get |
364 | - # testtools.TestCase to play nicely with zope's Cleanup class. |
365 | - self.assertRaises(exception, func, *args, **kwargs) |
366 | - try: |
367 | - func(*args, **kwargs) |
368 | - except Exception, e: |
369 | - return e |
370 | - |
371 | def test_entry_exported_with_as_of_succeeds(self): |
372 | # An entry exported using as_of is present in the as_of_version |
373 | # and in subsequent versions. |
374 | @@ -582,7 +578,7 @@ |
375 | self.assertEquals(interfaces[0].version, '2.0') |
376 | |
377 | def test_entry_exported_without_as_of_fails(self): |
378 | - exception = self._assertRaises( |
379 | + exception = self.assertRaises( |
380 | ValueError, generate_entry_interfaces, |
381 | IEntryExportedWithoutAsOf, [], |
382 | *self.utility.active_versions) |
383 | @@ -618,7 +614,7 @@ |
384 | def test_field_not_exported_using_as_of_fails(self): |
385 | # If you export a field without specifying as_of, you get an |
386 | # error. |
387 | - exception = self._assertRaises( |
388 | + exception = self.assertRaises( |
389 | ValueError, generate_entry_interfaces, |
390 | IFieldExportedWithoutAsOf, [], *self.utility.active_versions) |
391 | self.assertEquals( |
392 | @@ -631,7 +627,7 @@ |
393 | def test_field_cannot_be_both_exported_and_not_exported(self): |
394 | # If you use the as_of keyword argument, you can't also set |
395 | # the exported keyword argument to False. |
396 | - exception = self._assertRaises( |
397 | + exception = self.assertRaises( |
398 | ValueError, exported, TextLine(), as_of='1.0', exported=False) |
399 | self.assertEquals( |
400 | str(exception), |
401 | @@ -640,7 +636,7 @@ |
402 | |
403 | def test_field_exported_as_of_nonexistent_version_fails(self): |
404 | # You can't export a field as_of a nonexistent version. |
405 | - exception = self._assertRaises( |
406 | + exception = self.assertRaises( |
407 | ValueError, generate_entry_interfaces, |
408 | IFieldAsOfNonexistentVersion, [], |
409 | *self.utility.active_versions) |
410 | @@ -652,7 +648,7 @@ |
411 | def test_field_exported_with_duplicate_attributes_fails(self): |
412 | # You can't provide a dictionary of attributes for the |
413 | # version specified in as_of. |
414 | - exception = self._assertRaises( |
415 | + exception = self.assertRaises( |
416 | ValueError, generate_entry_interfaces, |
417 | IFieldDoubleDefinition, [], *self.utility.active_versions) |
418 | self.assertEquals( |
419 | @@ -663,7 +659,7 @@ |
420 | def test_field_with_annotations_that_precede_as_of_fails(self): |
421 | # You can't provide a dictionary of attributes for a version |
422 | # preceding the version specified in as_of. |
423 | - exception = self._assertRaises( |
424 | + exception = self.assertRaises( |
425 | ValueError, generate_entry_interfaces, |
426 | IFieldDefiningAttributesBeforeAsOf, [], |
427 | *self.utility.active_versions) |
428 | @@ -682,7 +678,7 @@ |
429 | method = [method for name, method in |
430 | IFieldImplicitOperationDefinition.namesAndDescriptions() |
431 | if name == 'implicitly_in_10'][0] |
432 | - exception = self._assertRaises( |
433 | + exception = self.assertRaises( |
434 | ValueError, generate_operation_adapter, method, None) |
435 | self.assertEquals( |
436 | str(exception), |
437 | @@ -694,7 +690,7 @@ |
438 | |
439 | def test_operation_implicitly_exported_in_earliest_version_fails(self): |
440 | # You can't implicitly define an operation for the earliest version. |
441 | - exception = self._assertRaises( |
442 | + exception = self.assertRaises( |
443 | ValueError, generate_and_register_webservice_operations, |
444 | None, IFieldImplicitOperationDefinition, []) |
445 | self.assertEquals( |
446 | @@ -720,3 +716,66 @@ |
447 | self.assertEquals( |
448 | adapter.__class__.__name__, |
449 | 'GET_IFieldExplicitOperationDefinition_explicitly_in_10_1_0') |
450 | + |
451 | + |
452 | +# Classes for TestSanityChecking |
453 | + |
454 | +class INotPublished(Interface): |
455 | + pass |
456 | + |
457 | + |
458 | +class IReferencesUnpublishedObject(Interface): |
459 | + export_as_webservice_entry() |
460 | + field = exported(Reference(schema=INotPublished)) |
461 | + |
462 | + |
463 | +class IPublishedTooLate(Interface): |
464 | + export_as_webservice_entry(as_of='2.0') |
465 | + |
466 | + |
467 | +class IReferencesPublishedTooLate(Interface): |
468 | + export_as_webservice_entry(as_of='1.0') |
469 | + field = exported(Reference(schema=IPublishedTooLate)) |
470 | + |
471 | + |
472 | +class IPublishedEarly(Interface): |
473 | + export_as_webservice_entry(as_of='1.0') |
474 | + |
475 | + |
476 | +class IPublishedLate(Interface): |
477 | + export_as_webservice_entry(as_of='2.0') |
478 | + field = exported(Reference(schema=IPublishedEarly)) |
479 | + |
480 | + |
481 | +class TestSanityChecking(TestCaseWithWebServiceFixtures): |
482 | + """Test lazr.restful's sanity checking upon web service registration.""" |
483 | + |
484 | + def test_reference_to_unpublished_object_fails(self): |
485 | + exception = self.assertRaises( |
486 | + ConfigurationExecutionError, register_test_module, 'testmod', |
487 | + INotPublished, IReferencesUnpublishedObject) |
488 | + self.assertTrue( |
489 | + ("In version 1.0, IReferencesUnpublishedObjectEntry_1_0.field " |
490 | + "is a Reference to INotPublished, but version 1.0 of the web " |
491 | + "service does not publish INotPublished as an entry. " |
492 | + "(It may not be published at all.)") in str(exception)) |
493 | + |
494 | + def test_reference_to_object_published_later_fails(self): |
495 | + exception = self.assertRaises( |
496 | + ConfigurationExecutionError, register_test_module, 'testmod', |
497 | + IPublishedTooLate, IReferencesPublishedTooLate) |
498 | + self.assertTrue( |
499 | + ("In version 1.0, IReferencesPublishedTooLateEntry_1_0.field is " |
500 | + "a Reference to IPublishedTooLate, but version 1.0 of the web " |
501 | + "service does not publish IPublishedTooLate as an entry. (It " |
502 | + "may not be published at all.)") in str(exception)) |
503 | + |
504 | + def test_reference_to_object_published_earlier_succeeds(self): |
505 | + # It's okay for an object defined in 2.0 to reference an |
506 | + # object first defined in 1.0, so long as the referenced |
507 | + # object is also present in 2.0. |
508 | + |
509 | + # We'll call this test a success if it doesn't raise an exception. |
510 | + module = register_test_module( |
511 | + 'testmod', IPublishedEarly, IPublishedLate) |
512 | + |
A simple docstring for webservice_ sanity_ checks would be nice.
Perhaps a dict string mapping would be better here:
version, interface.__name__, field.__name__,
referenced _interface. __name_ _, version,
referenced _interface. __name_ _)) s.%(field) s is a Reference "
'version' : version,
'field' : field.__name__,
'interface ': interface.__name__,
'reference ': referenced_ interface. __name_ _}
raise ValueError(
"In version %s, %s.%s is a Reference to %s, "
"but version %s of the web service does not publish "
"%s as an entry. (It may not be published "
"at all.)" % (
becomes
raise ValueError(
"In version %(version)s, %(interface)
"to %(reference)s, but version %(version)s of the web service "
"does not publish %(reference)s as an entry. "
"(It may not be published at all.)" % {
For your assert raises test, why not grab the copy from testtools?
def assertRaises(self, excClass, callableObj, *args, **kwargs):
arguments kwargs. If a different type of exception is
unexpected exception. es(excClass)
self. fail("% s not raised, %r returned instead." % (excName, ret))
"""Fail unless an exception of class excClass is thrown
by callableObj when invoked with arguments args and keyword
thrown, it will not be caught, and the test case will be
deemed to have suffered an error, exactly as for an
"""
try:
ret = callableObj(*args, **kwargs)
except excClass:
return sys.exc_info()[1]
else:
excName = self._formatTyp
You'd need this too:
def _formatTypes(self, classOrIterable): classOrIterable , '__name__', None)
className = ', '.join( klass._ _name__ for klass in classOrIterable)
"""Format a class or a bunch of classes for display in an error."""
className = getattr(
if className is None:
return className
test_reference_ to_object_ published_ later_fails exception string test
will always pass as you are only checking that the string isn't empty.
On the whole I think the implementation is fine.