launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #12148
[Merge] lp:~adeuring/launchpad/specifications-sharing-service into lp:launchpad
Abel Deuring has proposed merging lp:~adeuring/launchpad/specifications-sharing-service into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~adeuring/launchpad/specifications-sharing-service/+merge/124978
This branch updates several methods of the class SharingService so that
they can deal with specifications in addition to bugs and branches.
changed methods:
getSharedArtifacts()
getVisibleArtifacts()
new method:
getSharedSpecifications()
The diff contains some "noise" because some changed methods returned
a tuples (bugs, branches) before this change, and now return a tuple
(bugs, branches, specifications). This requires of course changes in
all callsites.
The method getVisibleArtifacts() uses IBugTaskSet.search() to find
"the right" bugs, and IAllBranches to find "the right" branches; we
do not have yet anything similar for specs, so I iimpkemented the
SQL query for the permission check directly in the class SharingService.
I am a bit concerned about the performance of the SQL query, but
it will for now only be used in a test: my main goal right now is to
make it possible to revoke access grants for specifications, and
I need the changes of getVisibleArtifacts() for a test of
SharingService.revokeAccessGrants().
test:
./bin/test -vvt lp.registry.services.tests.test_sharingservice
no lint
--
https://code.launchpad.net/~adeuring/launchpad/specifications-sharing-service/+merge/124978
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/specifications-sharing-service into lp:launchpad.
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2012-09-07 20:25:51 +0000
+++ lib/lp/bugs/model/bug.py 2012-09-18 16:33:28 +0000
@@ -811,7 +811,7 @@
# there is at least one bugtask for which access can be checked.
if self.default_bugtask:
service = getUtility(IService, 'sharing')
- bugs, ignored = service.getVisibleArtifacts(
+ bugs, ignored, ignored = service.getVisibleArtifacts(
person, bugs=[self], ignore_permissions=True)
if not bugs:
service.ensureAccessGrants(
=== modified file 'lib/lp/code/browser/branchsubscription.py'
--- lib/lp/code/browser/branchsubscription.py 2012-09-05 21:51:06 +0000
+++ lib/lp/code/browser/branchsubscription.py 2012-09-18 16:33:28 +0000
@@ -294,7 +294,7 @@
url = canonical_url(self.branch)
# If the subscriber can no longer see the branch, redirect them away.
service = getUtility(IService, 'sharing')
- ignored, branches = service.getVisibleArtifacts(
+ ignored, branches, ignored = service.getVisibleArtifacts(
self.person, branches=[self.branch], ignore_permissions=True)
if not branches:
url = canonical_url(self.branch.target)
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2012-09-18 04:13:42 +0000
+++ lib/lp/code/model/branch.py 2012-09-18 16:33:28 +0000
@@ -896,7 +896,7 @@
subscription.review_level = code_review_level
# Grant the subscriber access if they can't see the branch.
service = getUtility(IService, 'sharing')
- ignored, branches = service.getVisibleArtifacts(
+ ignored, branches, ignored = service.getVisibleArtifacts(
person, branches=[self], ignore_permissions=True)
if not branches:
service.ensureAccessGrants(
=== modified file 'lib/lp/code/model/tests/test_branchsubscription.py'
--- lib/lp/code/model/tests/test_branchsubscription.py 2012-07-18 10:44:24 +0000
+++ lib/lp/code/model/tests/test_branchsubscription.py 2012-09-18 16:33:28 +0000
@@ -133,7 +133,7 @@
None, CodeReviewNotificationLevel.NOEMAIL, owner)
# The stacked on branch should be visible.
service = getUtility(IService, 'sharing')
- ignored, visible_branches = service.getVisibleArtifacts(
+ ignored, visible_branches, ignored = service.getVisibleArtifacts(
grantee, branches=[private_stacked_on_branch])
self.assertContentEqual(
[private_stacked_on_branch], visible_branches)
@@ -161,7 +161,7 @@
grantee, BranchSubscriptionNotificationLevel.NOEMAIL,
None, CodeReviewNotificationLevel.NOEMAIL, owner)
# The stacked on branch should not be visible.
- ignored, visible_branches = service.getVisibleArtifacts(
+ ignored, visible_branches, ignored = service.getVisibleArtifacts(
grantee, branches=[private_stacked_on_branch])
self.assertContentEqual([], visible_branches)
self.assertIn(
=== modified file 'lib/lp/registry/browser/pillar.py'
--- lib/lp/registry/browser/pillar.py 2012-09-14 01:03:41 +0000
+++ lib/lp/registry/browser/pillar.py 2012-09-18 16:33:28 +0000
@@ -421,7 +421,7 @@
def _loadSharedArtifacts(self):
# As a concrete can by linked via more than one policy, we use sets to
# filter out dupes.
- self.bugtasks, self.branches = (
+ self.bugtasks, self.branches, self.specifications = (
self.sharing_service.getSharedArtifacts(
self.pillar, self.person, self.user))
bug_ids = set([bugtask.bug.id for bugtask in self.bugtasks])
=== modified file 'lib/lp/registry/interfaces/sharingservice.py'
--- lib/lp/registry/interfaces/sharingservice.py 2012-09-16 12:49:11 +0000
+++ lib/lp/registry/interfaces/sharingservice.py 2012-09-18 16:33:28 +0000
@@ -79,7 +79,7 @@
:param user: the user making the request. Only artifacts visible to the
user will be included in the result.
- :return: a (bugtasks, branches) tuple
+ :return: a (bugtasks, branches, specifications) tuple
"""
@export_read_operation()
@@ -116,6 +116,21 @@
:return: a collection of branches
"""
+ @export_read_operation()
+ @call_with(user=REQUEST_USER)
+ @operation_parameters(
+ pillar=Reference(IPillar, title=_('Pillar'), required=True),
+ person=Reference(IPerson, title=_('Person'), required=True))
+ @operation_returns_collection_of(IBranch)
+ @operation_for_version('devel')
+ def getSharedSpecifications(pillar, person, user):
+ """Return the specifications shared between the pillar and person.
+
+ :param user: the user making the request. Only branches visible to the
+ user will be included in the result.
+ :return: a collection of branches
+ """
+
def getVisibleArtifacts(person, branches=None, bugs=None):
"""Return the artifacts shared with person.
=== modified file 'lib/lp/registry/services/sharingservice.py'
--- lib/lp/registry/services/sharingservice.py 2012-09-14 13:25:18 +0000
+++ lib/lp/registry/services/sharingservice.py 2012-09-18 16:33:28 +0000
@@ -17,15 +17,18 @@
Count,
In,
Join,
+ LeftJoin,
Or,
Select,
)
+from storm.store import Store
from zope.component import getUtility
from zope.interface import implements
from zope.security.interfaces import Unauthorized
from zope.traversing.browser.absoluteurl import absoluteURL
from lp.app.browser.tales import ObjectImageDisplayAPI
+from lp.blueprints.model.specification import Specification
from lp.bugs.interfaces.bugtask import IBugTaskSet
from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
from lp.code.interfaces.branchcollection import IAllBranches
@@ -51,13 +54,16 @@
)
from lp.registry.interfaces.sharingservice import ISharingService
from lp.registry.model.accesspolicy import (
+ AccessArtifact,
AccessArtifactGrant,
AccessPolicy,
AccessPolicyArtifact,
AccessPolicyGrant,
+ AccessPolicyGrantFlat,
)
from lp.registry.model.person import Person
from lp.registry.model.teammembership import TeamParticipation
+from lp.services.database.bulk import load
from lp.services.database.lpstorm import IStore
from lp.services.database.stormexpr import ColumnSelect
from lp.services.searchbuilder import any
@@ -115,17 +121,20 @@
@available_with_permission('launchpad.Driver', 'pillar')
def getSharedArtifacts(self, pillar, person, user, include_bugs=True,
- include_branches=True):
+ include_branches=True, include_specifications=True):
"""See `ISharingService`."""
policies = getUtility(IAccessPolicySource).findByPillar([pillar])
flat_source = getUtility(IAccessPolicyGrantFlatSource)
bug_ids = set()
branch_ids = set()
+ specification_ids = set()
for artifact in flat_source.findArtifactsByGrantee(person, policies):
if artifact.bug_id and include_bugs:
bug_ids.add(artifact.bug_id)
elif artifact.branch_id and include_branches:
branch_ids.add(artifact.branch_id)
+ elif artifact.specification_id and include_specifications:
+ specification_ids.add(artifact.specification_id)
# Load the bugs.
bugtasks = []
@@ -140,25 +149,72 @@
wanted_branches = all_branches.visibleByUser(user).withIds(
*branch_ids)
branches = list(wanted_branches.getBranches())
+ specifications = []
+ if specification_ids:
+ specifications = load(Specification, specification_ids)
- return bugtasks, branches
+ return bugtasks, branches, specifications
@available_with_permission('launchpad.Driver', 'pillar')
def getSharedBugs(self, pillar, person, user):
"""See `ISharingService`."""
- bugtasks, ignore = self.getSharedArtifacts(
- pillar, person, user, include_branches=False)
+ bugtasks, ignore, ignore = self.getSharedArtifacts(
+ pillar, person, user, include_branches=False,
+ include_specifications=False)
return bugtasks
@available_with_permission('launchpad.Driver', 'pillar')
def getSharedBranches(self, pillar, person, user):
"""See `ISharingService`."""
- ignore, branches = self.getSharedArtifacts(
- pillar, person, user, include_bugs=False)
+ ignore, branches, ignore = self.getSharedArtifacts(
+ pillar, person, user, include_bugs=False,
+ include_specifications=False)
return branches
+ @available_with_permission('launchpad.Driver', 'pillar')
+ def getSharedSpecifications(self, pillar, person, user):
+ """See `ISharingService`."""
+ ignore, ignore, specifications = self.getSharedArtifacts(
+ pillar, person, user, include_bugs=False,
+ include_branches=False)
+ return specifications
+
+ def _getVisiblePrivateSpecificationIDs(self, person, specifications):
+ store = Store.of(specifications[0])
+ tables = (
+ Specification,
+ Join(
+ AccessPolicy,
+ And(
+ Or(
+ Specification.distributionID ==
+ AccessPolicy.distribution_id,
+ Specification.productID ==
+ AccessPolicy.product_id),
+ AccessPolicy.type == Specification.information_type)),
+ Join(
+ AccessPolicyGrantFlat,
+ AccessPolicy.id == AccessPolicyGrantFlat.policy_id
+ ),
+ LeftJoin(
+ AccessArtifact,
+ AccessArtifact.id ==
+ AccessPolicyGrantFlat.abstract_artifact_id),
+ Join(
+ TeamParticipation,
+ TeamParticipation.teamID ==
+ AccessPolicyGrantFlat.grantee_id))
+ spec_ids = [spec.id for spec in specifications]
+ return set(store.using(*tables).find(
+ Specification.id,
+ Or(
+ AccessPolicyGrantFlat.abstract_artifact_id == None,
+ AccessArtifact.specification == Specification.id),
+ TeamParticipation.personID == person.id,
+ In(Specification.id, spec_ids)))
+
def getVisibleArtifacts(self, person, branches=None, bugs=None,
- ignore_permissions=False):
+ specifications=None, ignore_permissions=False):
"""See `ISharingService`."""
bugs_by_id = {}
branches_by_id = {}
@@ -172,6 +228,10 @@
and not check_permission('launchpad.View', branch)):
raise Unauthorized
branches_by_id[branch.id] = branch
+ for spec in specifications or []:
+ if (not ignore_permissions
+ and not check_permission('launchpad.View', spec)):
+ raise Unauthorized
# Load the bugs.
visible_bug_ids = []
@@ -189,7 +249,15 @@
*branches_by_id.keys())
visible_branches = list(wanted_branches.getBranches())
- return visible_bugs, visible_branches
+ visible_specs = []
+ if specifications:
+ visible_private_spec_ids = self._getVisiblePrivateSpecificationIDs(
+ person, specifications)
+ visible_specs = [
+ spec for spec in specifications
+ if spec.id in visible_private_spec_ids or not spec.private]
+
+ return visible_bugs, visible_branches, visible_specs
def getInvisibleArtifacts(self, person, branches=None, bugs=None):
"""See `ISharingService`."""
=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
--- lib/lp/registry/services/tests/test_sharingservice.py 2012-09-16 12:49:11 +0000
+++ lib/lp/registry/services/tests/test_sharingservice.py 2012-09-18 16:33:28 +0000
@@ -980,8 +980,8 @@
# Check that grantees have expected access grants and subscriptions.
for person in [team_grantee, person_grantee]:
- visible_bugs, visible_branches = self.service.getVisibleArtifacts(
- person, branches, bugs)
+ visible_bugs, visible_branches, visible_specs = (
+ self.service.getVisibleArtifacts(person, branches, bugs))
self.assertContentEqual(bugs or [], visible_bugs)
self.assertContentEqual(branches or [], visible_branches)
for person in [team_grantee, person_grantee]:
@@ -1004,8 +1004,8 @@
for person in [team_grantee, person_grantee]:
for bug in bugs or []:
self.assertNotIn(person, bug.getDirectSubscribers())
- visible_bugs, visible_branches = self.service.getVisibleArtifacts(
- person, branches, bugs)
+ visible_bugs, visible_branches, visible_specs = (
+ self.service.getVisibleArtifacts(person, branches, bugs))
self.assertContentEqual([], visible_bugs)
self.assertContentEqual([], visible_branches)
@@ -1120,8 +1120,8 @@
owner = self.factory.makePerson()
product = self.factory.makeProduct(
owner=owner,
- specification_sharing_policy=
- SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY)
+ specification_sharing_policy=(
+ SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
login_person(owner)
specification = self.factory.makeSpecification(
product=product, owner=owner)
@@ -1232,6 +1232,12 @@
product=product, owner=product.owner,
information_type=InformationType.USERDATA)
branches.append(branch)
+ specs = []
+ for x in range(0, 10):
+ spec = self.factory.makeSpecification(
+ product=product, owner=product.owner,
+ information_type=InformationType.PROPRIETARY)
+ specs.append(spec)
# Grant access to grantee as well as the person who will be doing the
# query. The person who will be doing the query is not granted access
@@ -1251,32 +1257,39 @@
grant_access(bug, i == 9)
for i, branch in enumerate(branches):
grant_access(branch, i == 9)
- return bug_tasks, branches
+ getUtility(IService, 'sharing').ensureAccessGrants(
+ [grantee], product.owner, specifications=specs[:9])
+ return bug_tasks, branches, specs
def test_getSharedArtifacts(self):
# Test the getSharedArtifacts method.
owner = self.factory.makePerson()
- product = self.factory.makeProduct(owner=owner)
+ product = self.factory.makeProduct(
+ owner=owner, specification_sharing_policy=(
+ SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
login_person(owner)
grantee = self.factory.makePerson()
user = self.factory.makePerson()
- bug_tasks, branches = self.create_shared_artifacts(
+ bug_tasks, branches, specs = self.create_shared_artifacts(
product, grantee, user)
# Check the results.
- shared_bugtasks, shared_branches = self.service.getSharedArtifacts(
- product, grantee, user)
+ shared_bugtasks, shared_branches, shared_specs = (
+ self.service.getSharedArtifacts(product, grantee, user))
self.assertContentEqual(bug_tasks[:9], shared_bugtasks)
self.assertContentEqual(branches[:9], shared_branches)
+ self.assertContentEqual(specs[:9], shared_specs)
def test_getSharedBugs(self):
# Test the getSharedBugs method.
owner = self.factory.makePerson()
- product = self.factory.makeProduct(owner=owner)
+ product = self.factory.makeProduct(
+ owner=owner, specification_sharing_policy=(
+ SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
login_person(owner)
grantee = self.factory.makePerson()
user = self.factory.makePerson()
- bug_tasks, ignored = self.create_shared_artifacts(
+ bug_tasks, ignored, ignored = self.create_shared_artifacts(
product, grantee, user)
# Check the results.
@@ -1286,11 +1299,13 @@
def test_getSharedBranches(self):
# Test the getSharedBranches method.
owner = self.factory.makePerson()
- product = self.factory.makeProduct(owner=owner)
+ product = self.factory.makeProduct(
+ owner=owner, specification_sharing_policy=(
+ SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
login_person(owner)
grantee = self.factory.makePerson()
user = self.factory.makePerson()
- ignored, branches = self.create_shared_artifacts(
+ ignored, branches, ignored = self.create_shared_artifacts(
product, grantee, user)
# Check the results.
@@ -1298,6 +1313,23 @@
product, grantee, user)
self.assertContentEqual(branches[:9], shared_branches)
+ def test_getSharedSpecifications(self):
+ # Test the getSharedSpecifications method.
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct(
+ owner=owner, specification_sharing_policy=(
+ SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
+ login_person(owner)
+ grantee = self.factory.makePerson()
+ user = self.factory.makePerson()
+ ignored, ignored, specifications = self.create_shared_artifacts(
+ product, grantee, user)
+
+ # Check the results.
+ shared_specifications = self.service.getSharedSpecifications(
+ product, grantee, user)
+ self.assertContentEqual(specifications[:9], shared_specifications)
+
def test_getPeopleWithAccessBugs(self):
# Test the getPeopleWithoutAccess method with bugs.
owner = self.factory.makePerson()
@@ -1354,7 +1386,10 @@
def _make_Artifacts(self):
# Make artifacts for test (in)visible artifact methods.
owner = self.factory.makePerson()
- product = self.factory.makeProduct(owner=owner)
+ product = self.factory.makeProduct(
+ owner=owner,
+ specification_sharing_policy=(
+ SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
grantee = self.factory.makePerson()
login_person(owner)
@@ -1371,6 +1406,13 @@
information_type=InformationType.USERDATA)
branches.append(branch)
+ specifications = []
+ for x in range(0, 10):
+ spec = self.factory.makeSpecification(
+ product=product, owner=owner,
+ information_type=InformationType.PROPRIETARY)
+ specifications.append(spec)
+
def grant_access(artifact):
access_artifact = self.factory.makeAccessArtifact(
concrete=artifact)
@@ -1383,20 +1425,33 @@
grant_access(bug)
for branch in branches[:5]:
grant_access(branch)
- return grantee, branches, bugs
+ for spec in specifications[:5]:
+ grant_access(spec)
+ return grantee, owner, branches, bugs, specifications
def test_getVisibleArtifacts(self):
# Test the getVisibleArtifacts method.
- grantee, branches, bugs = self._make_Artifacts()
+ grantee, ignore, branches, bugs, specs = self._make_Artifacts()
# Check the results.
- shared_bugs, shared_branches = self.service.getVisibleArtifacts(
- grantee, branches, bugs)
+ shared_bugs, shared_branches, shared_specs = (
+ self.service.getVisibleArtifacts(grantee, branches, bugs, specs))
self.assertContentEqual(bugs[:5], shared_bugs)
self.assertContentEqual(branches[:5], shared_branches)
+ self.assertContentEqual(specs[:5], shared_specs)
+
+ def test_getVisibleArtifacts_grant_on_pillar(self):
+ # getVisibleArtifacts() returns private specifications if
+ # user has a policy grant for the pillar of the specification.
+ ignore, owner, branches, bugs, specs = self._make_Artifacts()
+ shared_bugs, shared_branches, shared_specs = (
+ self.service.getVisibleArtifacts(owner, branches, bugs, specs))
+ self.assertContentEqual(bugs, shared_bugs)
+ self.assertContentEqual(branches, shared_branches)
+ self.assertContentEqual(specs, shared_specs)
def test_getInvisibleArtifacts(self):
# Test the getInvisibleArtifacts method.
- grantee, branches, bugs = self._make_Artifacts()
+ grantee, ignore, branches, bugs, specs = self._make_Artifacts()
# Check the results.
not_shared_bugs, not_shared_branches = (
self.service.getInvisibleArtifacts(grantee, branches, bugs))
@@ -1423,16 +1478,16 @@
information_type=InformationType.USERDATA)
bugs.append(bug)
- shared_bugs, shared_branches = self.service.getVisibleArtifacts(
- grantee, bugs=bugs)
+ shared_bugs, shared_branches, shared_specs = (
+ self.service.getVisibleArtifacts(grantee, bugs=bugs))
self.assertContentEqual(bugs, shared_bugs)
# Change some bugs.
for x in range(0, 5):
change_callback(bugs[x], owner)
# Check the results.
- shared_bugs, shared_branches = self.service.getVisibleArtifacts(
- grantee, bugs=bugs)
+ shared_bugs, shared_branches, shared_specs = (
+ self.service.getVisibleArtifacts(grantee, bugs=bugs))
self.assertContentEqual(bugs[5:], shared_bugs)
def test_getVisibleArtifacts_bug_policy_change(self):
@@ -1519,7 +1574,9 @@
def setUp(self):
super(ApiTestMixin, self).setUp()
self.owner = self.factory.makePerson(name='thundercat')
- self.pillar = self.factory.makeProduct(owner=self.owner)
+ self.pillar = self.factory.makeProduct(
+ owner=self.owner, specification_sharing_policy=(
+ SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
self.grantee = self.factory.makePerson(name='grantee')
self.grantor = self.factory.makePerson()
self.grantee_uri = canonical_url(self.grantee, force_local_path=True)
@@ -1530,11 +1587,16 @@
self.branch = self.factory.makeBranch(
owner=self.owner, product=self.pillar,
information_type=InformationType.PRIVATESECURITY)
+ self.spec = self.factory.makeSpecification(
+ product=self.pillar, owner=self.owner,
+ information_type=InformationType.PROPRIETARY)
login_person(self.owner)
self.bug.subscribe(self.grantee, self.owner)
self.branch.subscribe(
self.grantee, BranchSubscriptionNotificationLevel.NOEMAIL,
None, CodeReviewNotificationLevel.NOEMAIL, self.owner)
+ getUtility(IService, 'sharing').ensureAccessGrants(
+ [self.grantee], self.grantor, specifications=[self.spec])
transaction.commit()
def test_getPillarGranteeData(self):
@@ -1546,7 +1608,8 @@
self.assertEqual(
{InformationType.USERDATA.name: SharingPermission.ALL.name,
InformationType.PRIVATESECURITY.name:
- SharingPermission.SOME.name},
+ SharingPermission.SOME.name,
+ InformationType.PROPRIETARY.name: SharingPermission.SOME.name},
grantee_data['permissions'])
@@ -1628,7 +1691,7 @@
self.assertEqual(bugtasks[0].title, self.bug.default_bugtask.title)
def test_getSharedBranches(self):
- # Test the exported getSharedArtifacts() method.
+ # Test the exported getSharedBranches() method.
ws_pillar = ws_object(self.launchpad, self.pillar)
ws_grantee = ws_object(self.launchpad, self.grantee)
branches = self.service.getSharedBranches(
@@ -1636,13 +1699,23 @@
self.assertEqual(1, len(branches))
self.assertEqual(branches[0].unique_name, self.branch.unique_name)
+ def test_getSharedSpecifications(self):
+ # Test the exported getSharedSpecifications() method.
+ ws_pillar = ws_object(self.launchpad, self.pillar)
+ ws_grantee = ws_object(self.launchpad, self.grantee)
+ specifications = self.service.getSharedSpecifications(
+ pillar=ws_pillar, person=ws_grantee)
+ self.assertEqual(1, len(specifications))
+ self.assertEqual(specifications[0].name, self.spec.name)
+
def test_getSharedArtifacts(self):
# Test the exported getSharedArtifacts() method.
ws_pillar = ws_object(self.launchpad, self.pillar)
ws_grantee = ws_object(self.launchpad, self.grantee)
- (bugtasks, branches) = self.service.getSharedArtifacts(
+ (bugtasks, branches, specs) = self.service.getSharedArtifacts(
pillar=ws_pillar, person=ws_grantee)
self.assertEqual(1, len(bugtasks))
self.assertEqual(1, len(branches))
+ self.assertEqual(1, len(specs))
self.assertEqual(bugtasks[0]['title'], self.bug.default_bugtask.title)
self.assertEqual(branches[0]['unique_name'], self.branch.unique_name)
Follow ups