Merge lp:~leonardr/lazr.restful/forbid-reference-to-entry-not-published-in-this-version into lp:lazr.restful

Proposed by Leonard Richardson
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
Reviewer Review Type Date Requested Status
Tim Penhey (community) Needs Fixing
Review via email: mp+53918@code.launchpad.net

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_as_webservice_entry(): a common mistake and one that I even found in our test suite. Or it might be an interface that is published in some versions but not others. If IFoo shows up starting in 2.0, you can't get IBar to publish a reference to an IFoo in 1.0.

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.

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

A simple docstring for webservice_sanity_checks would be nice.

Perhaps a dict string mapping would be better here:
    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.)" % (
            version, interface.__name__, field.__name__,
            referenced_interface.__name__, version,
            referenced_interface.__name__))
becomes
    raise ValueError(
        "In version %(version)s, %(interface)s.%(field)s is a Reference "
        "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.)" % {
            'version': version,
            'field': field.__name__,
            'interface': interface.__name__,
            'reference': referenced_interface.__name__}

For your assert raises test, why not grab the copy from testtools?

    def assertRaises(self, excClass, callableObj, *args, **kwargs):
        """Fail unless an exception of class excClass is thrown
           by callableObj when invoked with arguments args and keyword
           arguments kwargs. If a different type of exception is
           thrown, it will not be caught, and the test case will be
           deemed to have suffered an error, exactly as for an
           unexpected exception.
        """
        try:
            ret = callableObj(*args, **kwargs)
        except excClass:
            return sys.exc_info()[1]
        else:
            excName = self._formatTypes(excClass)
            self.fail("%s not raised, %r returned instead." % (excName, ret))

You'd need this too:

    def _formatTypes(self, classOrIterable):
        """Format a class or a bunch of classes for display in an error."""
        className = getattr(classOrIterable, '__name__', None)
        if className is None:
            className = ', '.join(klass.__name__ for klass in classOrIterable)
        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.

review: Needs Fixing
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.

Revision history for this message
Leonard Richardson (leonardr) wrote :

I figured out why I was worried about not having all the tests. Basically, I didn't expect test_reference_to_object_published_earlier_succeeds() to work and I was surprised that it did. I didn't understand why the code I wrote would find IPublishedEarly in 2.0 when it was defined in 1.0 and not changed afterwards.

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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/lazr/restful/metazcml.py'
--- src/lazr/restful/metazcml.py 2011-03-10 14:07:30 +0000
+++ src/lazr/restful/metazcml.py 2011-03-18 12:22:30 +0000
@@ -9,24 +9,41 @@
9import inspect9import inspect
10import itertools10import itertools
1111
12from zope.component import getUtility12from zope.component import (
13 getGlobalSiteManager,
14 getUtility,
15 )
13from zope.component.zcml import handler16from zope.component.zcml import handler
14from zope.configuration.fields import GlobalObject17from zope.configuration.fields import GlobalObject
15from zope.interface import Interface18from zope.interface import Interface
16from zope.interface.interfaces import IInterface19from zope.interface.interfaces import IInterface
1720
18
19from lazr.restful.declarations import (21from lazr.restful.declarations import (
20 COLLECTION_TYPE, ENTRY_TYPE, LAZR_WEBSERVICE_EXPORTED, OPERATION_TYPES,22 COLLECTION_TYPE,
21 REMOVED_OPERATION_TYPE, generate_collection_adapter,23 ENTRY_TYPE,
22 generate_entry_adapters, generate_entry_interfaces,24 LAZR_WEBSERVICE_EXPORTED,
23 generate_operation_adapter)25 OPERATION_TYPES,
26 REMOVED_OPERATION_TYPE,
27 generate_collection_adapter,
28 generate_entry_adapters,
29 generate_entry_interfaces,
30 generate_operation_adapter,
31 )
24from lazr.restful.error import WebServiceExceptionView32from lazr.restful.error import WebServiceExceptionView
2533
26from lazr.restful.interfaces import (34from lazr.restful.interfaces import (
27 ICollection, IEntry, IResourceDELETEOperation, IResourceGETOperation,35 ICollection,
28 IResourcePOSTOperation, IWebServiceClientRequest,36 IEntry,
29 IWebServiceConfiguration, IWebServiceVersion)37 IReference,
38 IResourceDELETEOperation,
39 IResourceGETOperation,
40 IResourcePOSTOperation,
41 IWebServiceClientRequest,
42 IWebServiceConfiguration,
43 IWebServiceVersion,
44 )
45
46from lazr.restful.utils import VersionedObject
3047
3148
32class IRegisterDirective(Interface):49class IRegisterDirective(Interface):
@@ -36,7 +53,8 @@
36 title=u'Module which will be inspected for webservice declarations')53 title=u'Module which will be inspected for webservice declarations')
3754
3855
39def generate_and_register_entry_adapters(interface, info, contributors):56def generate_and_register_entry_adapters(interface, info, contributors,
57 repository=None):
40 """Generate an entry adapter for every version of the web service.58 """Generate an entry adapter for every version of the web service.
4159
42 This code generates an IEntry subinterface for every version, each60 This code generates an IEntry subinterface for every version, each
@@ -54,9 +72,9 @@
54 interface, contributors, *versions)72 interface, contributors, *versions)
55 web_factories = generate_entry_adapters(73 web_factories = generate_entry_adapters(
56 interface, contributors, web_interfaces)74 interface, contributors, web_interfaces)
57 for i in range(0, len(web_interfaces)):75 for index, (interface_version, web_interface) in (
58 interface_version, web_interface = web_interfaces[i]76 enumerate(web_interfaces)):
59 factory_version, factory = web_factories[i]77 factory_version, factory = web_factories[index]
60 assert factory_version==interface_version, (78 assert factory_version==interface_version, (
61 "Generated interface and factory versions don't match up! "79 "Generated interface and factory versions don't match up! "
62 '%s vs. %s' % (factory_version, interface_version))80 '%s vs. %s' % (factory_version, interface_version))
@@ -69,6 +87,11 @@
69 register_adapter_for_version(factory, interface, interface_version,87 register_adapter_for_version(factory, interface, interface_version,
70 IEntry, '', info)88 IEntry, '', info)
7189
90 # If we were given a repository, add the interface and version
91 # to it.
92 if repository is not None:
93 repository.append(VersionedObject(interface_version, web_interface))
94
7295
73def ensure_correct_version_ordering(name, version_list):96def ensure_correct_version_ordering(name, version_list):
74 """Make sure that a list mentions versions from earliest to latest.97 """Make sure that a list mentions versions from earliest to latest.
@@ -146,6 +169,7 @@
146 handler('registerAdapter', factory, (interface, marker),169 handler('registerAdapter', factory, (interface, marker),
147 provides, name, info)170 provides, name, info)
148171
172
149def _is_exported_interface(member):173def _is_exported_interface(member):
150 """Helper for find_exported_interfaces; a predicate to inspect.getmembers.174 """Helper for find_exported_interfaces; a predicate to inspect.getmembers.
151175
@@ -163,6 +187,7 @@
163 return True187 return True
164 return False188 return False
165189
190
166def find_exported_interfaces(module):191def find_exported_interfaces(module):
167 """Find all the interfaces in a module marked for export.192 """Find all the interfaces in a module marked for export.
168193
@@ -173,6 +198,7 @@
173 return (interface for name, interface198 return (interface for name, interface
174 in inspect.getmembers(module, _is_exported_interface))199 in inspect.getmembers(module, _is_exported_interface))
175200
201
176def find_interfaces_and_contributors(module):202def find_interfaces_and_contributors(module):
177 """Find the interfaces and its contributors marked for export.203 """Find the interfaces and its contributors marked for export.
178204
@@ -254,6 +280,11 @@
254 module, type(module))280 module, type(module))
255 interfaces_with_contributors = find_interfaces_and_contributors(module)281 interfaces_with_contributors = find_interfaces_and_contributors(module)
256282
283 # Keep track of entry and operation registrations so we can
284 # sanity-check them later.
285 registered_entries = []
286 registered_operations = []
287
257 for interface, contributors in interfaces_with_contributors.items():288 for interface, contributors in interfaces_with_contributors.items():
258 if issubclass(interface, Exception):289 if issubclass(interface, Exception):
259 register_exception_view(context, interface)290 register_exception_view(context, interface)
@@ -264,7 +295,8 @@
264 context.action(295 context.action(
265 discriminator=('webservice entry interface', interface),296 discriminator=('webservice entry interface', interface),
266 callable=generate_and_register_entry_adapters,297 callable=generate_and_register_entry_adapters,
267 args=(interface, context.info, contributors),298 args=(interface, context.info, contributors,
299 registered_entries),
268 )300 )
269 elif tag['type'] == COLLECTION_TYPE:301 elif tag['type'] == COLLECTION_TYPE:
270 for version in tag['collection_default_content'].keys():302 for version in tag['collection_default_content'].keys():
@@ -282,12 +314,74 @@
282 raise AssertionError('Unknown export type: %s' % tag['type'])314 raise AssertionError('Unknown export type: %s' % tag['type'])
283 context.action(315 context.action(
284 discriminator=('webservice versioned operations', interface),316 discriminator=('webservice versioned operations', interface),
285 args=(context, interface, contributors),317 args=(context, interface, contributors, registered_operations),
286 callable=generate_and_register_webservice_operations)318 callable=generate_and_register_webservice_operations)
287319
320 # Now that all the adapters and operations are registered, run a
321 # sanity check to make sure no version of the web service
322 # references a feature not available in that version
323 context.action(
324 discriminator=('webservice sanity checks'),
325 args=(registered_entries, registered_operations),
326 callable=webservice_sanity_checks)
327
328
329def webservice_sanity_checks(entries, operations):
330 """Ensure the web service contains no references to unpublished objects.
331
332 We are worried about fields that link to unpublished objects, and
333 operations that have arguments or return values that are
334 unpublished. An unpublished object may not be published at all, or
335 it may not be published in the same version in which it's
336 referenced.
337 """
338
339 # Create a mapping of marker interfaces to version names.
340 versions = getUtility(IWebServiceConfiguration).active_versions
341 version_for_marker = { IWebServiceClientRequest: versions[0] }
342 for version in versions:
343 marker_interface = getUtility(IWebServiceVersion, name=version)
344 version_for_marker[marker_interface] = version
345
346 # For each version, build a list of all of the IEntries published
347 # in that version's web service. This works because every IEntry
348 # is explicitly registered for every version, even if there's been
349 # no change since the last version. For the sake of performance,
350 # the list is stored as a set of VersionedObject 2-tuples.
351 available_registrations = set()
352 registrations = getGlobalSiteManager().registeredAdapters()
353 for registration in registrations:
354 if (not IInterface.providedBy(registration.provided)
355 or not registration.provided.isOrExtends(IEntry)):
356 continue
357 interface, version_marker = registration.required
358 available_registrations.add(VersionedObject(
359 version_for_marker[version_marker], interface))
360
361 # Check every Reference field in every IEntry interface, making
362 # sure that it's a Reference to another IEntry published in that
363 # version.
364 for version, interface in entries:
365 if version is None:
366 version = versions[0]
367 for name, field in interface.namesAndDescriptions():
368 if IReference.providedBy(field):
369 referenced_interface = field.schema
370 to_check = VersionedObject(version, referenced_interface)
371 if to_check not in available_registrations:
372 raise ValueError(
373 "In version %(version)s, %(interface)s.%(field)s "
374 "is a Reference to %(reference)s, but version "
375 "%(version)s of the web service does not publish "
376 "%(reference)s as an entry. (It may not be published "
377 "at all.)" % dict(
378 version=version, interface=interface.__name__,
379 field=field.__name__,
380 reference=referenced_interface.__name__))
381
288382
289def generate_and_register_webservice_operations(383def generate_and_register_webservice_operations(
290 context, interface, contributors):384 context, interface, contributors, repository=None):
291 """Create and register adapters for all exported methods.385 """Create and register adapters for all exported methods.
292386
293 Different versions of the web service may publish the same387 Different versions of the web service may publish the same
@@ -458,6 +552,8 @@
458 # the operation registration for this mutator552 # the operation registration for this mutator
459 # will need to be blocked.553 # will need to be blocked.
460 mutator_operation_needs_to_be_blocked = True554 mutator_operation_needs_to_be_blocked = True
555 if repository is not None:
556 repository.append(VersionedObject(version, method))
461 previous_operation_name = operation_name557 previous_operation_name = operation_name
462 previous_operation_provides = operation_provides558 previous_operation_provides = operation_provides
463559
464560
=== modified file 'src/lazr/restful/publisher.py'
--- src/lazr/restful/publisher.py 2011-01-26 19:32:05 +0000
+++ src/lazr/restful/publisher.py 2011-03-18 12:22:30 +0000
@@ -9,7 +9,6 @@
9__metaclass__ = type9__metaclass__ = type
10__all__ = [10__all__ = [
11 'browser_request_to_web_service_request',11 'browser_request_to_web_service_request',
12 'TraverseWithGet',
13 'WebServicePublicationMixin',12 'WebServicePublicationMixin',
14 'WebServiceRequestTraversal',13 'WebServiceRequestTraversal',
15 ]14 ]
1615
=== modified file 'src/lazr/restful/testing/webservice.py'
--- src/lazr/restful/testing/webservice.py 2011-03-10 13:30:32 +0000
+++ src/lazr/restful/testing/webservice.py 2011-03-18 12:22:30 +0000
@@ -472,6 +472,34 @@
472 sm.registerUtility(472 sm.registerUtility(
473 IWebServiceTestRequest20, IWebServiceVersion, name='2.0')473 IWebServiceTestRequest20, IWebServiceVersion, name='2.0')
474474
475 def assertRaises(self, excClass, callableObj, *args, **kwargs):
476 """Fail unless an exception of class excClass is thrown
477 by callableObj when invoked with arguments args and keyword
478 arguments kwargs. If a different type of exception is
479 thrown, it will not be caught, and the test case will be
480 deemed to have suffered an error, exactly as for an
481 unexpected exception.
482
483 This is a copy of assertRaises from testtools.
484 """
485 try:
486 ret = callableObj(*args, **kwargs)
487 except excClass:
488 return sys.exc_info()[1]
489 else:
490 excName = self._formatTypes(excClass)
491 self.fail("%s not raised, %r returned instead." % (excName, ret))
492
493 def _formatTypes(self, classOrIterable):
494 """Format a class or a bunch of classes for display in an error.
495
496 This is a copy of _formatTypes from testtools.
497 """
498 className = getattr(classOrIterable, '__name__', None)
499 if className is None:
500 className = ', '.join(klass.__name__ for klass in classOrIterable)
501 return className
502
475 def fake_request(self, version):503 def fake_request(self, version):
476 request = FakeRequest(version=version)504 request = FakeRequest(version=version)
477 alsoProvides(505 alsoProvides(
478506
=== modified file 'src/lazr/restful/tests/test_declarations.py'
--- src/lazr/restful/tests/test_declarations.py 2011-03-18 12:22:30 +0000
+++ src/lazr/restful/tests/test_declarations.py 2011-03-18 12:22:30 +0000
@@ -1,3 +1,4 @@
1from zope.configuration.config import ConfigurationExecutionError
1from zope.component import (2from zope.component import (
2 adapts,3 adapts,
3 getMultiAdapter,4 getMultiAdapter,
@@ -102,7 +103,7 @@
102 # with its original name whereas for the '2.0' version it's exported103 # with its original name whereas for the '2.0' version it's exported
103 # as 'development_branch_20'.104 # as 'development_branch_20'.
104 self.product._dev_branch = Branch('A product branch')105 self.product._dev_branch = Branch('A product branch')
105 register_test_module('testmod', IProduct, IHasBranches)106 register_test_module('testmod', IBranch, IProduct, IHasBranches)
106 adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)107 adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
107 self.assertEqual(adapter.development_branch, self.product._dev_branch)108 self.assertEqual(adapter.development_branch, self.product._dev_branch)
108109
@@ -116,7 +117,7 @@
116 # development_branch field, but on version '2.0' that field can be117 # development_branch field, but on version '2.0' that field can be
117 # modified as we define a mutator for it.118 # modified as we define a mutator for it.
118 self.product._dev_branch = Branch('A product branch')119 self.product._dev_branch = Branch('A product branch')
119 register_test_module('testmod', IProduct, IHasBranches)120 register_test_module('testmod', IBranch, IProduct, IHasBranches)
120 adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)121 adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
121 try:122 try:
122 adapter.development_branch = None123 adapter.development_branch = None
@@ -151,7 +152,8 @@
151 # from both IHasBugs and IHasBranches.152 # from both IHasBugs and IHasBranches.
152 self.product._bug_count = 10153 self.product._bug_count = 10
153 self.product._dev_branch = Branch('A product branch')154 self.product._dev_branch = Branch('A product branch')
154 register_test_module('testmod', IProduct, IHasBugs, IHasBranches)155 register_test_module(
156 'testmod', IBranch, IProduct, IHasBugs, IHasBranches)
155 adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)157 adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
156 self.assertEqual(adapter.bug_count, 10)158 self.assertEqual(adapter.bug_count, 10)
157 self.assertEqual(adapter.development_branch, self.product._dev_branch)159 self.assertEqual(adapter.development_branch, self.product._dev_branch)
@@ -172,7 +174,8 @@
172 # When looking up an entry's redacted_fields, we take into account the174 # When looking up an entry's redacted_fields, we take into account the
173 # interface where the field is defined and adapt the context to that175 # interface where the field is defined and adapt the context to that
174 # interface before accessing that field.176 # interface before accessing that field.
175 register_test_module('testmod', IProduct, IHasBugs, IHasBranches)177 register_test_module(
178 'testmod', IBranch, IProduct, IHasBugs, IHasBranches)
176 entry_resource = EntryResource(self.product, self.one_zero_request)179 entry_resource = EntryResource(self.product, self.one_zero_request)
177 self.assertEquals([], entry_resource.redacted_fields)180 self.assertEquals([], entry_resource.redacted_fields)
178181
@@ -180,7 +183,8 @@
180 # When looking up an entry's redacted_fields for an object which is183 # When looking up an entry's redacted_fields for an object which is
181 # security proxied, we use the security checker for the interface184 # security proxied, we use the security checker for the interface
182 # where the field is defined.185 # where the field is defined.
183 register_test_module('testmod', IProduct, IHasBugs, IHasBranches)186 register_test_module(
187 'testmod', IBranch, IProduct, IHasBugs, IHasBranches)
184 newInteraction()188 newInteraction()
185 try:189 try:
186 secure_product = ProxyFactory(190 secure_product = ProxyFactory(
@@ -194,7 +198,8 @@
194 def test_duplicate_contributed_attributes(self):198 def test_duplicate_contributed_attributes(self):
195 # We do not allow a given attribute to be contributed to a given199 # We do not allow a given attribute to be contributed to a given
196 # interface by more than one contributing interface.200 # interface by more than one contributing interface.
197 testmod = create_test_module('testmod', IProduct, IHasBugs, IHasBugs2)201 testmod = create_test_module(
202 'testmod', IBranch, IProduct, IHasBugs, IHasBugs2)
198 self.assertRaises(203 self.assertRaises(
199 ConflictInContributingInterfaces,204 ConflictInContributingInterfaces,
200 find_interfaces_and_contributors, testmod)205 find_interfaces_and_contributors, testmod)
@@ -205,7 +210,7 @@
205 class DummyHasBranches:210 class DummyHasBranches:
206 implements(IHasBranches)211 implements(IHasBranches)
207 dummy = DummyHasBranches()212 dummy = DummyHasBranches()
208 register_test_module('testmod', IProduct, IHasBranches)213 register_test_module('testmod', IBranch, IProduct, IHasBranches)
209 self.assertRaises(214 self.assertRaises(
210 ComponentLookupError,215 ComponentLookupError,
211 getMultiAdapter, (dummy, self.one_zero_request), IEntry)216 getMultiAdapter, (dummy, self.one_zero_request), IEntry)
@@ -318,6 +323,7 @@
318323
319324
320class IBranch(Interface):325class IBranch(Interface):
326 export_as_webservice_entry()
321 name = TextLine(title=u'The branch name')327 name = TextLine(title=u'The branch name')
322328
323329
@@ -552,16 +558,6 @@
552 self.utility = getUtility(IWebServiceConfiguration)558 self.utility = getUtility(IWebServiceConfiguration)
553 self.utility.require_explicit_versions = True559 self.utility.require_explicit_versions = True
554560
555 def _assertRaises(self, exception, func, *args, **kwargs):
556 # Approximate the behavior of testtools assertRaises, which
557 # returns the raised exception. I couldn't get
558 # testtools.TestCase to play nicely with zope's Cleanup class.
559 self.assertRaises(exception, func, *args, **kwargs)
560 try:
561 func(*args, **kwargs)
562 except Exception, e:
563 return e
564
565 def test_entry_exported_with_as_of_succeeds(self):561 def test_entry_exported_with_as_of_succeeds(self):
566 # An entry exported using as_of is present in the as_of_version562 # An entry exported using as_of is present in the as_of_version
567 # and in subsequent versions.563 # and in subsequent versions.
@@ -582,7 +578,7 @@
582 self.assertEquals(interfaces[0].version, '2.0')578 self.assertEquals(interfaces[0].version, '2.0')
583579
584 def test_entry_exported_without_as_of_fails(self):580 def test_entry_exported_without_as_of_fails(self):
585 exception = self._assertRaises(581 exception = self.assertRaises(
586 ValueError, generate_entry_interfaces,582 ValueError, generate_entry_interfaces,
587 IEntryExportedWithoutAsOf, [],583 IEntryExportedWithoutAsOf, [],
588 *self.utility.active_versions)584 *self.utility.active_versions)
@@ -618,7 +614,7 @@
618 def test_field_not_exported_using_as_of_fails(self):614 def test_field_not_exported_using_as_of_fails(self):
619 # If you export a field without specifying as_of, you get an615 # If you export a field without specifying as_of, you get an
620 # error.616 # error.
621 exception = self._assertRaises(617 exception = self.assertRaises(
622 ValueError, generate_entry_interfaces,618 ValueError, generate_entry_interfaces,
623 IFieldExportedWithoutAsOf, [], *self.utility.active_versions)619 IFieldExportedWithoutAsOf, [], *self.utility.active_versions)
624 self.assertEquals(620 self.assertEquals(
@@ -631,7 +627,7 @@
631 def test_field_cannot_be_both_exported_and_not_exported(self):627 def test_field_cannot_be_both_exported_and_not_exported(self):
632 # If you use the as_of keyword argument, you can't also set628 # If you use the as_of keyword argument, you can't also set
633 # the exported keyword argument to False.629 # the exported keyword argument to False.
634 exception = self._assertRaises(630 exception = self.assertRaises(
635 ValueError, exported, TextLine(), as_of='1.0', exported=False)631 ValueError, exported, TextLine(), as_of='1.0', exported=False)
636 self.assertEquals(632 self.assertEquals(
637 str(exception),633 str(exception),
@@ -640,7 +636,7 @@
640636
641 def test_field_exported_as_of_nonexistent_version_fails(self):637 def test_field_exported_as_of_nonexistent_version_fails(self):
642 # You can't export a field as_of a nonexistent version.638 # You can't export a field as_of a nonexistent version.
643 exception = self._assertRaises(639 exception = self.assertRaises(
644 ValueError, generate_entry_interfaces,640 ValueError, generate_entry_interfaces,
645 IFieldAsOfNonexistentVersion, [],641 IFieldAsOfNonexistentVersion, [],
646 *self.utility.active_versions)642 *self.utility.active_versions)
@@ -652,7 +648,7 @@
652 def test_field_exported_with_duplicate_attributes_fails(self):648 def test_field_exported_with_duplicate_attributes_fails(self):
653 # You can't provide a dictionary of attributes for the649 # You can't provide a dictionary of attributes for the
654 # version specified in as_of.650 # version specified in as_of.
655 exception = self._assertRaises(651 exception = self.assertRaises(
656 ValueError, generate_entry_interfaces,652 ValueError, generate_entry_interfaces,
657 IFieldDoubleDefinition, [], *self.utility.active_versions)653 IFieldDoubleDefinition, [], *self.utility.active_versions)
658 self.assertEquals(654 self.assertEquals(
@@ -663,7 +659,7 @@
663 def test_field_with_annotations_that_precede_as_of_fails(self):659 def test_field_with_annotations_that_precede_as_of_fails(self):
664 # You can't provide a dictionary of attributes for a version660 # You can't provide a dictionary of attributes for a version
665 # preceding the version specified in as_of.661 # preceding the version specified in as_of.
666 exception = self._assertRaises(662 exception = self.assertRaises(
667 ValueError, generate_entry_interfaces,663 ValueError, generate_entry_interfaces,
668 IFieldDefiningAttributesBeforeAsOf, [],664 IFieldDefiningAttributesBeforeAsOf, [],
669 *self.utility.active_versions)665 *self.utility.active_versions)
@@ -682,7 +678,7 @@
682 method = [method for name, method in678 method = [method for name, method in
683 IFieldImplicitOperationDefinition.namesAndDescriptions()679 IFieldImplicitOperationDefinition.namesAndDescriptions()
684 if name == 'implicitly_in_10'][0]680 if name == 'implicitly_in_10'][0]
685 exception = self._assertRaises(681 exception = self.assertRaises(
686 ValueError, generate_operation_adapter, method, None)682 ValueError, generate_operation_adapter, method, None)
687 self.assertEquals(683 self.assertEquals(
688 str(exception),684 str(exception),
@@ -694,7 +690,7 @@
694690
695 def test_operation_implicitly_exported_in_earliest_version_fails(self):691 def test_operation_implicitly_exported_in_earliest_version_fails(self):
696 # You can't implicitly define an operation for the earliest version.692 # You can't implicitly define an operation for the earliest version.
697 exception = self._assertRaises(693 exception = self.assertRaises(
698 ValueError, generate_and_register_webservice_operations,694 ValueError, generate_and_register_webservice_operations,
699 None, IFieldImplicitOperationDefinition, [])695 None, IFieldImplicitOperationDefinition, [])
700 self.assertEquals(696 self.assertEquals(
@@ -720,3 +716,66 @@
720 self.assertEquals(716 self.assertEquals(
721 adapter.__class__.__name__,717 adapter.__class__.__name__,
722 'GET_IFieldExplicitOperationDefinition_explicitly_in_10_1_0')718 'GET_IFieldExplicitOperationDefinition_explicitly_in_10_1_0')
719
720
721# Classes for TestSanityChecking
722
723class INotPublished(Interface):
724 pass
725
726
727class IReferencesUnpublishedObject(Interface):
728 export_as_webservice_entry()
729 field = exported(Reference(schema=INotPublished))
730
731
732class IPublishedTooLate(Interface):
733 export_as_webservice_entry(as_of='2.0')
734
735
736class IReferencesPublishedTooLate(Interface):
737 export_as_webservice_entry(as_of='1.0')
738 field = exported(Reference(schema=IPublishedTooLate))
739
740
741class IPublishedEarly(Interface):
742 export_as_webservice_entry(as_of='1.0')
743
744
745class IPublishedLate(Interface):
746 export_as_webservice_entry(as_of='2.0')
747 field = exported(Reference(schema=IPublishedEarly))
748
749
750class TestSanityChecking(TestCaseWithWebServiceFixtures):
751 """Test lazr.restful's sanity checking upon web service registration."""
752
753 def test_reference_to_unpublished_object_fails(self):
754 exception = self.assertRaises(
755 ConfigurationExecutionError, register_test_module, 'testmod',
756 INotPublished, IReferencesUnpublishedObject)
757 self.assertTrue(
758 ("In version 1.0, IReferencesUnpublishedObjectEntry_1_0.field "
759 "is a Reference to INotPublished, but version 1.0 of the web "
760 "service does not publish INotPublished as an entry. "
761 "(It may not be published at all.)") in str(exception))
762
763 def test_reference_to_object_published_later_fails(self):
764 exception = self.assertRaises(
765 ConfigurationExecutionError, register_test_module, 'testmod',
766 IPublishedTooLate, IReferencesPublishedTooLate)
767 self.assertTrue(
768 ("In version 1.0, IReferencesPublishedTooLateEntry_1_0.field is "
769 "a Reference to IPublishedTooLate, but version 1.0 of the web "
770 "service does not publish IPublishedTooLate as an entry. (It "
771 "may not be published at all.)") in str(exception))
772
773 def test_reference_to_object_published_earlier_succeeds(self):
774 # It's okay for an object defined in 2.0 to reference an
775 # object first defined in 1.0, so long as the referenced
776 # object is also present in 2.0.
777
778 # We'll call this test a success if it doesn't raise an exception.
779 module = register_test_module(
780 'testmod', IPublishedEarly, IPublishedLate)
781

Subscribers

People subscribed via source and target branches