Hi Salgado, Yay for progress on this issue. I entirely support the postponing of all the difficult stuff :-) I have some quibbles about the tests, but nothing serious I hope. > === modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py' > --- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-11-17 22:18:34 +0000 > +++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-11-25 22:50:47 +0000 > @@ -34,6 +34,10 @@ > ) > from lp.blueprints.interfaces.specification import ISpecification > from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch > +from lp.blueprints.interfaces.specificationtarget import ( > + IHasSpecifications, > + ISpecificationTarget, > + ) > from lp.bugs.interfaces.bug import ( > IBug, > IFrontPageBugAddForm, > @@ -516,3 +520,13 @@ > > # IProductSeries > patch_reference_property(IProductSeries, 'product', IProduct) > + > +# ISpecificationTarget > +patch_entry_return_type( > + ISpecificationTarget, 'getSpecification', ISpecification) > + > +# IHasSpecifications > +patch_collection_property( > + IHasSpecifications, 'all_specifications', ISpecification) > +patch_collection_property( > + IHasSpecifications, 'valid_specifications', ISpecification) It's a shame that we don't have the app specific circular import files yet... oh well. > === modified file 'lib/lp/blueprints/interfaces/specification.py' > --- lib/lp/blueprints/interfaces/specification.py 2010-11-23 20:19:24 +0000 > +++ lib/lp/blueprints/interfaces/specification.py 2010-11-25 22:50:47 +0000 > @@ -19,7 +19,12 @@ > ] > > > -from lazr.restful.declarations import export_as_webservice_entry > +from lazr.restful.declarations import ( > + exported, > + export_as_webservice_entry, > + ) > +from lazr.restful.fields import ReferenceChoice > + > from zope.component import getUtility > from zope.interface import ( > Attribute, > @@ -44,9 +49,13 @@ > SpecificationLifecycleStatus, > SpecificationPriority, > ) > -from lp.blueprints.interfaces.specificationtarget import IHasSpecifications > +from lp.blueprints.interfaces.specificationtarget import ( > + IHasSpecifications, > + ISpecificationTarget, > + ) > from lp.blueprints.interfaces.sprint import ISprint > from lp.code.interfaces.branchlink import IHasLinkedBranches > +from lp.registry.interfaces.milestone import IMilestone > from lp.registry.interfaces.projectgroup import IProjectGroup > from lp.registry.interfaces.role import IHasOwner > from lp.services.fields import ( > @@ -119,48 +128,69 @@ > class INewSpecification(Interface): > """A schema for a new specification.""" > > - name = SpecNameField( > - title=_('Name'), required=True, readonly=False, > - description=_( > - "May contain lower-case letters, numbers, and dashes. " > - "It will be used in the specification url. " > - "Examples: mozilla-type-ahead-find, postgres-smart-serial.")) > - title = Title( > - title=_('Title'), required=True, description=_( > - "Describe the feature as clearly as possible in up to 70 " > - "characters. This title is displayed in every feature " > - "list or report.")) > - specurl = SpecURLField( > - title=_('Specification URL'), required=False, > - description=_( > - "The URL of the specification. This is usually a wiki page."), > - constraint=valid_webref) > - summary = Summary( > - title=_('Summary'), required=True, description=_( > - "A single-paragraph description of the feature. " > - "This will also be displayed in most feature listings.")) > - definition_status = Choice( > - title=_('Definition Status'), > - vocabulary=SpecificationDefinitionStatus, > - default=SpecificationDefinitionStatus.NEW, > - description=_( > - "The current status of the process to define the " > - "feature and get approval for the implementation plan.")) > - assignee = PublicPersonChoice( > - title=_('Assignee'), required=False, > - description=_("The person responsible for implementing the feature."), > - vocabulary='ValidPersonOrTeam') > - drafter = PublicPersonChoice( > - title=_('Drafter'), required=False, > - description=_( > - "The person responsible for drafting the specification."), > - vocabulary='ValidPersonOrTeam') > - approver = PublicPersonChoice( > - title=_('Approver'), required=False, > - description=_( > - "The person responsible for approving the specification, " > - "and for reviewing the code when it's ready to be landed."), > - vocabulary='ValidPersonOrTeam') > + name = exported( > + SpecNameField( > + title=_('Name'), required=True, readonly=False, > + description=_( > + "May contain lower-case letters, numbers, and dashes. " > + "It will be used in the specification url. " > + "Examples: mozilla-type-ahead-find, postgres-smart-serial.")), > + ('devel', dict(exported=True)), exported=False) > + title = exported( > + Title( > + title=_('Title'), required=True, description=_( > + "Describe the feature as clearly as possible in up to 70 " > + "characters. This title is displayed in every feature " > + "list or report.")), > + ('devel', dict(exported=True)), exported=False) > + specurl = exported( > + SpecURLField( > + title=_('Specification URL'), required=False, > + description=_( > + "The URL of the specification. This is usually a wiki page."), > + constraint=valid_webref), > + ('devel', dict(exported=True, exported_as='specification_url')), > + exported=False) > + summary = exported( > + Summary( > + title=_('Summary'), required=True, description=_( > + "A single-paragraph description of the feature. " > + "This will also be displayed in most feature listings.")), > + ('devel', dict(exported=True)), exported=False) > + # XXX: salgado, 2010-11-25, bug=680880: We need a method for changing the > + # definition_status because when that happens we may need to call > + # updateLifecycleStatus(). > + definition_status = exported( > + Choice( > + title=_('Definition Status'), > + vocabulary=SpecificationDefinitionStatus, > + default=SpecificationDefinitionStatus.NEW, > + description=_( > + "The current status of the process to define the " > + "feature and get approval for the implementation plan.")), > + ('devel', dict(exported=True, readonly=True)), exported=False) > + assignee = exported( > + PublicPersonChoice( > + title=_('Assignee'), required=False, > + description=_( > + "The person responsible for implementing the feature."), > + vocabulary='ValidPersonOrTeam'), > + ('devel', dict(exported=True)), exported=False) > + drafter = exported( > + PublicPersonChoice( > + title=_('Drafter'), required=False, > + description=_( > + "The person responsible for drafting the specification."), > + vocabulary='ValidPersonOrTeam'), > + ('devel', dict(exported=True)), exported=False) > + approver = exported( > + PublicPersonChoice( > + title=_('Approver'), required=False, > + description=_( > + "The person responsible for approving the specification, " > + "and for reviewing the code when it's ready to be landed."), > + vocabulary='ValidPersonOrTeam'), > + ('devel', dict(exported=True)), exported=False) > > > class INewSpecificationProjectTarget(Interface): > @@ -201,10 +231,15 @@ > > Requires the user to specify a distribution or a product as a target. > """ > - target = Choice(title=_("For"), > - description=_("The project for which this proposal is " > - "being made."), > - required=True, vocabulary='DistributionOrProduct') > + # Exported as readonly for simplicity, but could be exported as read-write > + # using setTarget() as the mutator. > + target = exported( > + ReferenceChoice( > + title=_('For'), required=True, vocabulary='DistributionOrProduct', > + description=_( > + "The project for which this proposal is being made."), > + schema=ISpecificationTarget), > + ('devel', dict(exported=True, readonly=True)), exported=False) > > > class ISpecificationEditRestricted(Interface): > @@ -235,40 +270,54 @@ > # referencing it. > id = Int(title=_("Database ID"), required=True, readonly=True) > > - priority = Choice( > - title=_('Priority'), vocabulary=SpecificationPriority, > - default=SpecificationPriority.UNDEFINED, required=True) > - datecreated = Datetime( > - title=_('Date Created'), required=True, readonly=True) > - owner = PublicPersonChoice( > - title=_('Owner'), required=True, readonly=True, > - vocabulary='ValidPersonOrTeam') > - # target > + priority = exported( > + Choice( > + title=_('Priority'), vocabulary=SpecificationPriority, > + default=SpecificationPriority.UNDEFINED, required=True), > + ('devel', dict(exported=True)), exported=False) > + datecreated = exported( > + Datetime( > + title=_('Date Created'), required=True, readonly=True), > + ('devel', dict(exported=True, exported_as='date_created')), > + exported=False) > + owner = exported( > + PublicPersonChoice( > + title=_('Owner'), required=True, readonly=True, > + vocabulary='ValidPersonOrTeam'), > + ('devel', dict(exported=True)), exported=False) > + > product = Choice(title=_('Project'), required=False, > - vocabulary='Product') > + vocabulary='Product') > distribution = Choice(title=_('Distribution'), required=False, > - vocabulary='Distribution') > + vocabulary='Distribution') > > - # series > - productseries = Choice(title=_('Series Goal'), required=False, > + productseries = Choice( > + title=_('Series Goal'), required=False, > vocabulary='FilteredProductSeries', > description=_( > - "Choose a series in which you would like to deliver " > - "this feature. Selecting '(no value)' will clear the goal.")) > - distroseries = Choice(title=_('Series Goal'), required=False, > + "Choose a series in which you would like to deliver " > + "this feature. Selecting '(no value)' will clear the goal.")) > + distroseries = Choice( > + title=_('Series Goal'), required=False, > vocabulary='FilteredDistroSeries', > description=_( > "Choose a series in which you would like to deliver " > "this feature. Selecting '(no value)' will clear the goal.")) > > # milestone > - milestone = Choice( > - title=_('Milestone'), required=False, vocabulary='Milestone', > - description=_( > - "The milestone in which we would like this feature to be " > - "delivered.")) > + milestone = exported( > + ReferenceChoice( > + title=_('Milestone'), required=False, vocabulary='Milestone', > + description=_( > + "The milestone in which we would like this feature to be " > + "delivered."), > + schema=IMilestone), > + ('devel', dict(exported=True)), exported=False) > > # nomination to a series for release management > + # XXX: It'd be nice to export goal as read-only, but it's tricky because > + # users will need to be aware of goalstatus as what's returned by .goal > + # may not be the accepted goal. > goal = Attribute("The series for which this feature is a goal.") > goalstatus = Choice( > title=_('Goal Acceptance'), vocabulary=SpecificationGoalStatus, > @@ -283,10 +332,12 @@ > date_goal_decided = Attribute("The date the spec was approved " > "or declined as a goal.") > > - whiteboard = Text(title=_('Status Whiteboard'), required=False, > - description=_( > - "Any notes on the status of this spec you would like to make. " > - "Your changes will override the current text.")) > + whiteboard = exported( > + Text(title=_('Status Whiteboard'), required=False, > + description=_( > + "Any notes on the status of this spec you would like to " > + "make. Your changes will override the current text.")), > + ('devel', dict(exported=True)), exported=False) > direction_approved = Bool(title=_('Basic direction approved?'), > required=False, default=False, description=_("Check this to " > "indicate that the drafter and assignee have satisfied the " > === added file 'lib/lp/blueprints/tests/test_webservice.py' > --- lib/lp/blueprints/tests/test_webservice.py 1970-01-01 00:00:00 +0000 > +++ lib/lp/blueprints/tests/test_webservice.py 2010-11-25 22:50:47 +0000 > @@ -0,0 +1,410 @@ > +# Copyright 2009 Canonical Ltd. This software is licensed under the > +# GNU Affero General Public License version 3 (see the file LICENSE). > + > +"""Webservice unit tests related to Launchpad blueprints.""" > + > +__metaclass__ = type > + > +from canonical.testing import DatabaseFunctionalLayer > +from canonical.launchpad.testing.pages import webservice_for_person > +from lp.blueprints.interfaces.specification import ( > + SpecificationDefinitionStatus, > + SpecificationPriority) > +from lp.testing import ( > + launchpadlib_for, TestCaseWithFactory) > + > + > +class SpecificationWebserviceTestCase(TestCaseWithFactory): > + > + def makeProduct(self): > + return self.factory.makeProduct(name="fooix") > + > + def makeDistribution(self): > + return self.factory.makeDistribution(name="foobuntu") I don't particularly like these helpers. Why can't the tests that care about the names specify the names? > + def getLaunchpadlib(self): > + user = self.factory.makePerson() > + return launchpadlib_for("testing", user, version='devel') > + > + def getSpecOnWebservice(self, spec_object): > + launchpadlib = self.getLaunchpadlib() > + if spec_object.product is not None: > + pillar_name = spec_object.product.name > + else: > + pillar_name = spec_object.distribution.name I think you can spell this spec_object.target.name can't you? > + return launchpadlib.load( > + str(launchpadlib._root_uri) + '/%s/+spec/%s' > + % (pillar_name, spec_object.name)) > + > + def getPillarOnWebservice(self, pillar_obj): > + launchpadlib = self.getLaunchpadlib() > + return launchpadlib.load( > + str(launchpadlib._root_uri) + '/' + pillar_obj.name) I'm pretty sure you can pass just '/' + pillar_obj.name here. > +class SpecificationAttributeWebserviceTests(SpecificationWebserviceTestCase): > + """Test accessing specification attributes over the webservice.""" > + layer = DatabaseFunctionalLayer > + > + def makeSimpleSpecification(self): > + self.name = "some-spec" > + self.title = "some-title" > + self.url = "http://example.org/some_url" > + self.summary = "Some summary." > + status = SpecificationDefinitionStatus.PENDINGAPPROVAL > + self.definition_status = status.title > + self.assignee_name = "james-w" > + assignee = self.factory.makePerson(name=self.assignee_name) > + self.drafter_name = "jml" > + drafter = self.factory.makePerson(name=self.drafter_name) > + self.approver_name = "bob" > + approver = self.factory.makePerson(name=self.approver_name) > + self.owner_name = "mary" > + owner = self.factory.makePerson(name=self.owner_name) > + priority = SpecificationPriority.HIGH > + self.priority = priority.title > + self.whiteboard = "Some whiteboard" > + self.product = self.factory.makeProduct() > + return self.factory.makeSpecification( > + product=self.product, name=self.name, > + title=self.title, specurl=self.url, > + summary=self.summary, > + status=status, > + assignee=assignee, drafter=drafter, approver=approver, > + priority=priority, > + owner=owner, whiteboard=self.whiteboard) > + > + def getSimpleSpecificationResponse(self): > + self.spec_object = self.makeSimpleSpecification() > + return self.getSpecOnWebservice(self.spec_object) > + > + def test_representation_is_empty_on_1_dot_0(self): > + # ISpecification is exposed on the 1.0 version so that they can be > + # linked against branches, but none of its fields is exposed on that > + # version as we expect it to undergo significant refactorings before > + # it's ready for prime time. > + spec = self.makeSimpleSpecification() > + user = self.factory.makePerson() > + webservice = webservice_for_person(user) > + response = webservice.get( > + '/%s/+spec/%s' % (spec.product.name, spec.name)) > + expected_keys = sorted( > + [u'self_link', u'http_etag', u'resource_type_link']) > + self.assertEqual(response.status, 200) > + self.assertEqual(sorted(response.jsonBody().keys()), expected_keys) > + > + def test_representation_contains_name(self): > + spec = self.getSimpleSpecificationResponse() > + self.assertEqual(self.name, spec.name) Argh, I hate this style of test. It's only one line longer to write it as: def test_representation_contains_name(self): spec_object = self.factory.makeSpecification() spec_webservice = self.getSpecOnWebservice(spec_object) self.assertEqual(spec_object.name, spec_webservice.name) and then it avoids the action at a distance effects of these tests. Can you change them all? If you like, I'll write you a sed script or something to do it :-) > + def test_representation_contains_target(self): > + spec = self.getSimpleSpecificationResponse() > + self.assertEqual(self.product.name, spec.target.name) > + > + def test_representation_contains_title(self): > + spec = self.getSimpleSpecificationResponse() > + self.assertEqual(self.title, spec.title) > + > + def test_representation_contains_specification_url(self): > + spec = self.getSimpleSpecificationResponse() > + self.assertEqual(self.url, spec.specification_url) > + > + def test_representation_contains_summary(self): > + spec = self.getSimpleSpecificationResponse() > + self.assertEqual(self.summary, spec.summary) > + > + def test_representation_contains_definition_status(self): > + spec = self.getSimpleSpecificationResponse() > + self.assertEqual( > + self.definition_status, spec.definition_status) > + > + def test_representation_contains_assignee(self): > + spec = self.getSimpleSpecificationResponse() > + self.assertEqual(self.assignee_name, spec.assignee.name) > + > + def test_representation_contains_drafter(self): > + spec = self.getSimpleSpecificationResponse() > + self.assertEqual(self.drafter_name, spec.drafter.name) > + > + def test_representation_contains_approver(self): > + spec = self.getSimpleSpecificationResponse() > + self.assertEqual(self.approver_name, spec.approver.name) > + > + def test_representation_contains_owner(self): > + spec = self.getSimpleSpecificationResponse() > + self.assertEqual(self.owner_name, spec.owner.name) > + > + def test_representation_contains_priority(self): > + spec = self.getSimpleSpecificationResponse() > + self.assertEqual(self.priority, spec.priority) > + > + def test_representation_contains_date_created(self): > + spec = self.getSimpleSpecificationResponse() > + self.assertEqual(self.spec_object.datecreated, spec.date_created) > + > + def test_representation_contains_whiteboard(self): > + spec = self.getSimpleSpecificationResponse() > + self.assertEqual(self.whiteboard, spec.whiteboard) > + > + def test_representation_contains_milestone(self): > + product = self.makeProduct() > + productseries = self.factory.makeProductSeries(product=product) > + milestone = self.factory.makeMilestone( > + name="1.0", product=product, productseries=productseries) > + spec_object = self.factory.makeSpecification( > + product=product, goal=productseries, milestone=milestone) > + spec = self.getSpecOnWebservice(spec_object) > + self.assertEqual("1.0", spec.milestone.name) > + > + > +class SpecificationTargetTests(SpecificationWebserviceTestCase): > + """Tests for accessing specifications via their targets.""" > + layer = DatabaseFunctionalLayer > + > + def test_get_specification_on_product(self): > + product = self.makeProduct() > + spec_object = self.factory.makeSpecification( > + product=product, name="some-spec") > + product_on_webservice = self.getPillarOnWebservice(product) > + spec = product_on_webservice.getSpecification(name="some-spec") > + self.assertEqual("some-spec", spec.name) > + self.assertEqual("fooix", spec.target.name) > + > + def test_get_specification_on_distribution(self): > + distribution = self.makeDistribution() > + spec_object = self.factory.makeSpecification( > + distribution=distribution, name="some-spec") > + distro_on_webservice = self.getPillarOnWebservice(distribution) > + spec = distro_on_webservice.getSpecification(name="some-spec") > + self.assertEqual("some-spec", spec.name) > + self.assertEqual("foobuntu", spec.target.name) > + > + def test_get_specification_on_productseries(self): > + product = self.makeProduct() > + productseries = self.factory.makeProductSeries( > + product=product, name="fooix-dev") > + spec_object = self.factory.makeSpecification( > + product=product, name="some-spec", goal=productseries) > + product_on_webservice = self.getPillarOnWebservice(product) > + productseries_on_webservice = product_on_webservice.getSeries( > + name="fooix-dev") > + spec = productseries_on_webservice.getSpecification(name="some-spec") > + self.assertEqual("some-spec", spec.name) > + self.assertEqual("fooix", spec.target.name) > + > + def test_get_specification_on_distroseries(self): > + distribution = self.makeDistribution() > + distroseries = self.factory.makeDistroSeries( > + distribution=distribution, name="maudlin") > + spec_object = self.factory.makeSpecification( > + distribution=distribution, name="some-spec", > + goal=distroseries) > + distro_on_webservice = self.getPillarOnWebservice(distribution) > + distroseries_on_webservice = distro_on_webservice.getSeries( > + name_or_version="maudlin") > + spec = distroseries_on_webservice.getSpecification(name="some-spec") > + self.assertEqual("some-spec", spec.name) > + self.assertEqual("foobuntu", spec.target.name) > + > + def test_get_specification_not_found(self): > + product = self.makeProduct() > + product_on_webservice = self.getPillarOnWebservice(product) > + spec = product_on_webservice.getSpecification(name="nonexistant") > + self.assertEqual(None, spec) I'd rather these tests didn't depend on names chosen way up the file too :/ > +class IHasSpecificationsTests(SpecificationWebserviceTestCase): > + """Tests for accessing IHasSpecifications methods over the webservice.""" > + layer = DatabaseFunctionalLayer > + > + def assertNamesOfSpecificationsAre(self, expected_names, specifications): > + names = [s.name for s in specifications] > + self.assertEqual(sorted(expected_names), sorted(names)) > + > + def test_product_all_specifications(self): > + product = self.makeProduct() > + self.factory.makeSpecification(product=product, name="spec1") > + self.factory.makeSpecification(product=product, name="spec2") > + product_on_webservice = self.getPillarOnWebservice(product) > + self.assertNamesOfSpecificationsAre( > + ["spec1", "spec2"], product_on_webservice.all_specifications) > + > + def test_product_valid_specifications(self): > + product = self.makeProduct() > + self.factory.makeSpecification(product=product, name="spec1") > + self.factory.makeSpecification( > + product=product, name="spec2", > + status=SpecificationDefinitionStatus.OBSOLETE) > + product_on_webservice = self.getPillarOnWebservice(product) > + self.assertNamesOfSpecificationsAre( > + ["spec1"], product_on_webservice.valid_specifications) > + > + def test_distribution_all_specifications(self): > + distribution = self.makeDistribution() > + self.factory.makeSpecification( > + distribution=distribution, name="spec1") > + self.factory.makeSpecification( > + distribution=distribution, name="spec2") > + distro_on_webservice = self.getPillarOnWebservice(distribution) > + self.assertNamesOfSpecificationsAre( > + ["spec1", "spec2"], distro_on_webservice.all_specifications) > + > + def test_distribution_valid_specifications(self): > + distribution = self.makeDistribution() > + self.factory.makeSpecification( > + distribution=distribution, name="spec1") > + self.factory.makeSpecification( > + distribution=distribution, name="spec2", > + status=SpecificationDefinitionStatus.OBSOLETE) > + distro_on_webservice = self.getPillarOnWebservice(distribution) > + self.assertNamesOfSpecificationsAre( > + ["spec1"], distro_on_webservice.valid_specifications) > + > + def test_distroseries_all_specifications(self): > + distribution = self.makeDistribution() > + distroseries = self.factory.makeDistroSeries( > + name='maudlin', distribution=distribution) > + self.factory.makeSpecification( > + distribution=distribution, name="spec1", > + goal=distroseries) > + self.factory.makeSpecification( > + distribution=distribution, name="spec2", > + goal=distroseries) > + self.factory.makeSpecification( > + distribution=distribution, name="spec3") > + distro_on_webservice = self.getPillarOnWebservice(distribution) > + distroseries_on_webservice = distro_on_webservice.getSeries( > + name_or_version="maudlin") > + self.assertNamesOfSpecificationsAre( > + ["spec1", "spec2"], > + distroseries_on_webservice.all_specifications) > + > + # XXX: salgado, 2010-11-25, bug=681432: Test disabled because > + # DistroSeries.valid_specifications is broken. > + def disabled_test_distroseries_valid_specifications(self): > + distribution = self.makeDistribution() > + distroseries = self.factory.makeDistroSeries( > + name='maudlin', distribution=distribution) > + self.factory.makeSpecification( > + distribution=distribution, name="spec1", > + goal=distroseries) > + self.factory.makeSpecification( > + distribution=distribution, name="spec2", > + goal=distroseries) > + self.factory.makeSpecification( > + distribution=distribution, name="spec3", > + goal=distroseries, > + status=SpecificationDefinitionStatus.OBSOLETE) > + self.factory.makeSpecification( > + distribution=distribution, name="spec4") > + distro_on_webservice = self.getPillarOnWebservice(distribution) > + distroseries_on_webservice = distro_on_webservice.getSeries( > + name_or_version="maudlin") > + self.assertNamesOfSpecificationsAre( > + ["spec1", "spec2"], > + distroseries_on_webservice.valid_specifications) > + > + def test_productseries_all_specifications(self): > + product = self.makeProduct() > + productseries = self.factory.makeProductSeries( > + product=product, name="fooix-dev") > + self.factory.makeSpecification( > + product=product, name="spec1", goal=productseries) > + self.factory.makeSpecification( > + product=product, name="spec2", goal=productseries) > + self.factory.makeSpecification(product=product, name="spec3") > + product_on_webservice = self.getPillarOnWebservice(product) > + series_on_webservice = product_on_webservice.getSeries( > + name="fooix-dev") > + self.assertNamesOfSpecificationsAre( > + ["spec1", "spec2"], series_on_webservice.all_specifications) > + > + def test_productseries_valid_specifications(self): > + product = self.makeProduct() > + productseries = self.factory.makeProductSeries( > + product=product, name="fooix-dev") > + self.factory.makeSpecification( > + product=product, name="spec1", goal=productseries) > + self.factory.makeSpecification( > + product=product, name="spec2", goal=productseries) > + self.factory.makeSpecification( > + product=product, name="spec3", goal=productseries, > + status=SpecificationDefinitionStatus.OBSOLETE) > + self.factory.makeSpecification(product=product, name="spec4") > + product_on_webservice = self.getPillarOnWebservice(product) > + series_on_webservice = product_on_webservice.getSeries( > + name="fooix-dev") > + # Should this be different to the results for distroseries? > + self.assertNamesOfSpecificationsAre( > + ["spec1", "spec2"], > + series_on_webservice.valid_specifications) > + > + def test_projectgroup_all_specifications(self): > + projectgroup = self.factory.makeProject() > + other_projectgroup = self.factory.makeProject() > + product1 = self.factory.makeProduct(project=projectgroup) > + product2 = self.factory.makeProduct(project=projectgroup) > + product3 = self.factory.makeProduct(project=other_projectgroup) > + self.factory.makeSpecification( > + product=product1, name="spec1") > + self.factory.makeSpecification( > + product=product2, name="spec2", > + status=SpecificationDefinitionStatus.OBSOLETE) > + self.factory.makeSpecification( > + product=product3, name="spec3") > + projectgroup_on_webservice = self.getPillarOnWebservice(projectgroup) > + # Should this be different to the results for distroseries? > + self.assertNamesOfSpecificationsAre( > + ["spec1", "spec2"], > + projectgroup_on_webservice.all_specifications) > + > + def test_projectgroup_valid_specifications(self): > + projectgroup = self.factory.makeProject() > + other_projectgroup = self.factory.makeProject() > + product1 = self.factory.makeProduct(project=projectgroup) > + product2 = self.factory.makeProduct(project=projectgroup) > + product3 = self.factory.makeProduct(project=other_projectgroup) > + self.factory.makeSpecification( > + product=product1, name="spec1") > + self.factory.makeSpecification( > + product=product2, name="spec2", > + status=SpecificationDefinitionStatus.OBSOLETE) > + self.factory.makeSpecification( > + product=product3, name="spec3") > + projectgroup_on_webservice = self.getPillarOnWebservice(projectgroup) > + # Should this be different to the results for distroseries? > + self.assertNamesOfSpecificationsAre( > + ["spec1", "spec2"], > + projectgroup_on_webservice.valid_specifications) > + > + def test_person_all_specifications(self): > + person = self.factory.makePerson(name="james-w") > + product = self.factory.makeProduct() > + self.factory.makeSpecification( > + product=product, name="spec1", drafter=person) > + self.factory.makeSpecification( > + product=product, name="spec2", approver=person, > + status=SpecificationDefinitionStatus.OBSOLETE) > + self.factory.makeSpecification( > + product=product, name="spec3") > + launchpadlib = self.getLaunchpadlib() > + person_on_webservice = launchpadlib.load( > + str(launchpadlib._root_uri) + '/~james-w') > + self.assertNamesOfSpecificationsAre( > + ["spec1", "spec2"], person_on_webservice.all_specifications) > + > + def test_person_valid_specifications(self): > + person = self.factory.makePerson(name="james-w") > + product = self.factory.makeProduct() > + self.factory.makeSpecification( > + product=product, name="spec1", drafter=person) > + self.factory.makeSpecification( > + product=product, name="spec2", approver=person, > + status=SpecificationDefinitionStatus.OBSOLETE) > + self.factory.makeSpecification( > + product=product, name="spec3") > + launchpadlib = self.getLaunchpadlib() > + person_on_webservice = launchpadlib.load( > + str(launchpadlib._root_uri) + '/~james-w') > + self.assertNamesOfSpecificationsAre( > + ["spec1"], person_on_webservice.valid_specifications) I think these tests are fine, but they seem to violate layering a bit -- surely we test the all_specifications property at least this well in other code? Cheers, mwh