launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #19480
[Merge] lp:~wgrant/launchpad/xref-buglinks into lp:launchpad
William Grant has proposed merging lp:~wgrant/launchpad/xref-buglinks into lp:launchpad with lp:~wgrant/launchpad/xref-model as a prerequisite.
Commit message:
BugCve/QuestionBug/SpecificationBug -> XRef, part 1: write to both schemas, read from either.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~wgrant/launchpad/xref-buglinks/+merge/272591
BugCve/QuestionBug/SpecificationBug -> XRef, part 1: write to both schemas, read from either.
There's also a garbo job to backfill XRef, and a test-only feature flag to disable writing to the old schema.
There are a couple of XXXs about filling in XRef.creator, but the old schema doesn't store that data, and this branch is big enough as is. creator comes later.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/xref-buglinks into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2015-09-29 04:00:42 +0000
+++ database/schema/security.cfg 2015-09-29 04:00:42 +0000
@@ -654,6 +654,7 @@
public.validpersoncache = SELECT
public.validpersonorteamcache = SELECT
public.wikiname = SELECT, INSERT
+public.xref = SELECT, INSERT
type=user
[branchscanner]
@@ -945,6 +946,7 @@
public.translationgroup = SELECT
public.validpersoncache = SELECT
public.validpersonorteamcache = SELECT
+public.xref = SELECT, INSERT
type=user
[fiera]
@@ -1295,6 +1297,7 @@
public.teamparticipation = SELECT
public.validpersoncache = SELECT
public.validpersonorteamcache = SELECT
+public.xref = SELECT
type=user
[expire_questions]
@@ -1436,6 +1439,7 @@
public.validpersoncache = SELECT
public.validpersonorteamcache = SELECT
public.wikiname = SELECT, INSERT
+public.xref = SELECT, INSERT
type=group
[queued]
@@ -1545,6 +1549,7 @@
public.teamparticipation = SELECT, INSERT
public.validpersoncache = SELECT
public.validpersonorteamcache = SELECT
+public.xref = SELECT, INSERT
type=user
[process_accepted]
@@ -1625,6 +1630,7 @@
public.teamparticipation = SELECT
public.validpersoncache = SELECT
public.validpersonorteamcache = SELECT
+public.xref = SELECT
type=user
[personnotification]
@@ -1835,6 +1841,7 @@
public.teamparticipation = SELECT
public.validpersoncache = SELECT
public.validpersonorteamcache = SELECT
+public.xref = SELECT, INSERT
type=user
[mlist-sync]
@@ -2337,6 +2344,7 @@
public.translationmessage = SELECT, DELETE
public.translationtemplateitem = SELECT, DELETE
public.webhookjob = SELECT, DELETE
+public.xref = SELECT, INSERT
type=user
[garbo_daily]
=== modified file 'lib/lp/answers/browser/tests/views.txt'
--- lib/lp/answers/browser/tests/views.txt 2014-04-24 02:53:05 +0000
+++ lib/lp/answers/browser/tests/views.txt 2015-09-29 04:00:42 +0000
@@ -334,8 +334,8 @@
... 'field.description': 'Bug description.'})
>>> request.method = 'POST'
>>> makebug = getMultiAdapter((question_three, request), name='+makebug')
- >>> question_three.bugs.count() == 0
- True
+ >>> question_three.bugs
+ []
>>> makebug.initialize()
>>> print question_three.bugs[0].title
=== modified file 'lib/lp/answers/model/question.py'
--- lib/lp/answers/model/question.py 2015-09-28 07:57:17 +0000
+++ lib/lp/answers/model/question.py 2015-09-29 04:00:42 +0000
@@ -94,12 +94,14 @@
IProductSet,
)
from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
+from lp.services.database import bulk
from lp.services.database.constants import (
DEFAULT,
UTC_NOW,
)
from lp.services.database.datetimecol import UtcDateTimeCol
from lp.services.database.enumcol import EnumCol
+from lp.services.database.interfaces import IStore
from lp.services.database.nl_search import nl_phrase_search
from lp.services.database.sqlbase import (
cursor,
@@ -108,6 +110,7 @@
sqlvalues,
)
from lp.services.database.stormexpr import rank_by_fti
+from lp.services.features import getFeatureFlag
from lp.services.mail.notificationrecipientset import NotificationRecipientSet
from lp.services.messages.interfaces.message import IMessage
from lp.services.messages.model.message import (
@@ -119,6 +122,7 @@
from lp.services.worlddata.helpers import is_english_variant
from lp.services.worlddata.interfaces.language import ILanguage
from lp.services.worlddata.model.language import Language
+from lp.services.xref.interfaces import IXRefSet
class notify_question_modified:
@@ -210,8 +214,6 @@
subscribers = SQLRelatedJoin('Person',
joinColumn='question', otherColumn='person',
intermediateTable='QuestionSubscription', orderBy='name')
- bugs = SQLRelatedJoin('Bug', joinColumn='question', otherColumn='bug',
- intermediateTable='QuestionBug', orderBy='id')
messages = SQLMultipleJoin('QuestionMessage', joinColumn='question',
prejoins=['message'], orderBy=['QuestionMessage.id'])
reopenings = SQLMultipleJoin('QuestionReopening', orderBy='datecreated',
@@ -660,6 +662,20 @@
self.status = new_status
return tktmsg
+ @property
+ def bugs(self):
+ from lp.bugs.model.bug import Bug
+ if getFeatureFlag('bugs.xref_buglinks.query'):
+ bug_ids = [
+ int(id) for _, id in getUtility(IXRefSet).findFrom(
+ (u'question', unicode(self.id)), types=[u'bug'])]
+ else:
+ bug_ids = list(IStore(QuestionBug).find(
+ QuestionBug,
+ QuestionBug.question == self).values(QuestionBug.bugID))
+ return list(sorted(
+ bulk.load(Bug, bug_ids), key=operator.attrgetter('id')))
+
# IBugLinkTarget implementation
def linkBug(self, bug, user=None):
"""See `IBugLinkTarget`."""
@@ -677,11 +693,18 @@
def createBugLink(self, bug):
"""See BugLinkTargetMixin."""
- QuestionBug(question=self, bug=bug)
+ if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'):
+ QuestionBug(question=self, bug=bug)
+ # XXX: Should set creator.
+ getUtility(IXRefSet).create(
+ {(u'question', unicode(self.id)): {(u'bug', unicode(bug.id)): {}}})
def deleteBugLink(self, bug):
"""See BugLinkTargetMixin."""
- Store.of(self).find(QuestionBug, question=self, bug=bug).remove()
+ if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'):
+ Store.of(self).find(QuestionBug, question=self, bug=bug).remove()
+ getUtility(IXRefSet).delete(
+ {(u'question', unicode(self.id)): [(u'bug', unicode(bug.id))]})
def setCommentVisibility(self, user, comment_number, visible):
"""See `IQuestion`."""
@@ -707,24 +730,38 @@
# This query joins to bugtasks that are not BugTaskStatus.INVALID
# because there are many bugtasks to one question. A question is
# included when BugTask.status IS NULL.
- return Question.select("""
- id in (SELECT Question.id
- FROM Question
+ if getFeatureFlag('bugs.xref_buglinks.query'):
+ bugtask_join = """
+ LEFT OUTER JOIN XRef ON (
+ XRef.from_type = 'question'
+ AND XRef.from_id_int = Question.id
+ AND XRef.to_type = 'bug')
+ LEFT OUTER JOIN BugTask ON (
+ BugTask.bug = XRef.to_id_int
+ AND BugTask.status != %s)
+ """
+ else:
+ bugtask_join = """
LEFT OUTER JOIN QuestionBug
ON Question.id = QuestionBug.question
- LEFT OUTER JOIN BugTask
- ON QuestionBug.bug = BugTask.bug
- AND BugTask.status != %s
+ LEFT OUTER JOIN BugTask ON (
+ BugTask.bug = QuestionBug.bug
+ AND BugTask.status != %s)
+ """
+ return Question.select(("""
+ id in (SELECT Question.id
+ FROM Question
+ %s
WHERE
- Question.status IN (%s, %s)
+ Question.status IN (%%s, %%s)
AND (Question.datelastresponse IS NULL
OR Question.datelastresponse < (CURRENT_TIMESTAMP
- AT TIME ZONE 'UTC' - interval '%s days'))
+ AT TIME ZONE 'UTC' - interval '%%s days'))
AND Question.datelastquery < (CURRENT_TIMESTAMP
- AT TIME ZONE 'UTC' - interval '%s days')
+ AT TIME ZONE 'UTC' - interval '%%s days')
AND Question.assignee IS NULL
AND BugTask.status IS NULL)
- """ % sqlvalues(
+ """ % bugtask_join) % sqlvalues(
BugTaskStatus.INVALID,
QuestionStatus.OPEN, QuestionStatus.NEEDSINFO,
days_before_expiration, days_before_expiration))
=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py 2015-09-28 07:57:17 +0000
+++ lib/lp/blueprints/model/specification.py 2015-09-29 04:00:42 +0000
@@ -11,6 +11,8 @@
'SpecificationSet',
]
+import operator
+
from lazr.lifecycle.event import (
ObjectCreatedEvent,
ObjectModifiedEvent,
@@ -86,6 +88,7 @@
from lp.registry.interfaces.person import validate_public_person
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.productseries import IProductSeries
+from lp.services.database import bulk
from lp.services.database.constants import (
DEFAULT,
UTC_NOW,
@@ -99,12 +102,14 @@
SQLBase,
sqlvalues,
)
+from lp.services.features import getFeatureFlag
from lp.services.mail.helpers import get_contact_email_addresses
from lp.services.propertycache import (
cachedproperty,
get_property_cache,
)
from lp.services.webapp.interfaces import ILaunchBag
+from lp.services.xref.interfaces import IXRefSet
def recursive_blocked_query(user):
@@ -239,9 +244,6 @@
sprints = SQLRelatedJoin('Sprint', orderBy='name',
joinColumn='specification', otherColumn='sprint',
intermediateTable='SprintSpecification')
- bugs = SQLRelatedJoin('Bug',
- joinColumn='specification', otherColumn='bug',
- intermediateTable='SpecificationBug', orderBy='id')
spec_dependency_links = SQLMultipleJoin('SpecificationDependency',
joinColumn='specification', orderBy='id')
@@ -791,14 +793,38 @@
return bool(self.subscription(person))
+ @property
+ def bugs(self):
+ from lp.bugs.model.bug import Bug
+ if getFeatureFlag('bugs.xref_buglinks.query'):
+ bug_ids = [
+ int(id) for _, id in getUtility(IXRefSet).findFrom(
+ (u'specification', unicode(self.id)), types=[u'bug'])]
+ else:
+ bug_ids = list(IStore(SpecificationBug).find(
+ SpecificationBug,
+ SpecificationBug.specification == self).values(
+ SpecificationBug.bugID))
+ return list(sorted(
+ bulk.load(Bug, bug_ids), key=operator.attrgetter('id')))
+
def createBugLink(self, bug):
"""See BugLinkTargetMixin."""
- SpecificationBug(specification=self, bug=bug)
+ if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'):
+ SpecificationBug(specification=self, bug=bug)
+ # XXX: Should set creator.
+ getUtility(IXRefSet).create(
+ {(u'specification', unicode(self.id)):
+ {(u'bug', unicode(bug.id)): {}}})
def deleteBugLink(self, bug):
"""See BugLinkTargetMixin."""
- Store.of(self).find(
- SpecificationBug, specification=self, bug=bug).remove()
+ if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'):
+ Store.of(self).find(
+ SpecificationBug, specification=self, bug=bug).remove()
+ getUtility(IXRefSet).delete(
+ {(u'specification', unicode(self.id)):
+ [(u'bug', unicode(bug.id))]})
# sprint linking
def linkSprint(self, sprint, user):
=== modified file 'lib/lp/blueprints/tests/test_specification.py'
--- lib/lp/blueprints/tests/test_specification.py 2015-09-28 07:39:28 +0000
+++ lib/lp/blueprints/tests/test_specification.py 2015-09-29 04:00:42 +0000
@@ -59,6 +59,7 @@
EditSpecificationByRelatedPeople,
ViewSpecification,
)
+from lp.services.features.testing import FeatureFixture
from lp.services.propertycache import get_property_cache
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.interaction import ANONYMOUS
@@ -876,3 +877,19 @@
self.assertContentEqual([bug1], spec2.bugs)
self.assertContentEqual([spec2], bug1.specifications)
self.assertContentEqual([], bug2.specifications)
+
+
+class TestBugLinksWithXRef(TestBugLinks):
+
+ def setUp(self):
+ super(TestBugLinksWithXRef, self).setUp()
+ self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'}))
+
+
+class TestBugLinksWithXRefAndNoOld(TestBugLinks):
+
+ def setUp(self):
+ super(TestBugLinksWithXRefAndNoOld, self).setUp()
+ self.useFixture(FeatureFixture({
+ 'bugs.xref_buglinks.query': 'true',
+ 'bugs.xref_buglinks.write_old.disabled': 'true'}))
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2015-06-30 01:10:06 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2015-09-29 04:00:42 +0000
@@ -125,7 +125,7 @@
0, 10, login_method=lambda: login(ADMIN_EMAIL))
# This may seem large: it is; there is easily another 25% fat in
# there.
- self.assertThat(recorder1, HasQueryCount(LessThan(81)))
+ self.assertThat(recorder1, HasQueryCount(LessThan(83)))
self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
def test_rendered_query_counts_constant_with_attachments(self):
@@ -136,7 +136,7 @@
lambda: self.getUserBrowser(url, person),
lambda: self.factory.makeBugAttachment(bug=task.bug),
1, 9, login_method=lambda: login(ADMIN_EMAIL))
- self.assertThat(recorder1, HasQueryCount(LessThan(82)))
+ self.assertThat(recorder1, HasQueryCount(LessThan(84)))
self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
def makeLinkedBranchMergeProposal(self, sourcepackage, bug, owner):
@@ -171,7 +171,7 @@
recorder1, recorder2 = record_two_runs(
lambda: self.getUserBrowser(url, owner),
make_merge_proposals, 0, 1)
- self.assertThat(recorder1, HasQueryCount(LessThan(87)))
+ self.assertThat(recorder1, HasQueryCount(LessThan(89)))
# Ideally this should be much fewer, but this tries to keep a win of
# removing more than half of these.
self.assertThat(
@@ -217,7 +217,7 @@
lambda: self.getUserBrowser(url, person),
lambda: add_activity("description", self.factory.makePerson()),
1, 20, login_method=lambda: login(ADMIN_EMAIL))
- self.assertThat(recorder1, HasQueryCount(LessThan(82)))
+ self.assertThat(recorder1, HasQueryCount(LessThan(84)))
self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
def test_rendered_query_counts_constant_with_milestones(self):
@@ -227,7 +227,7 @@
with celebrity_logged_in('admin'):
browses_under_limit = BrowsesWithQueryLimit(
- 82, self.factory.makePerson())
+ 84, self.factory.makePerson())
self.assertThat(bug, browses_under_limit)
=== modified file 'lib/lp/bugs/doc/cve.txt'
--- lib/lp/bugs/doc/cve.txt 2015-09-25 09:48:57 +0000
+++ lib/lp/bugs/doc/cve.txt 2015-09-29 04:00:42 +0000
@@ -75,16 +75,16 @@
Let's add the new CVE:
- >>> b.cves.count()
+ >>> len(b.cves)
1
>>> b.linkCVE(cve, no_priv)
- >>> b.cves.count()
+ >>> len(b.cves)
2
Ah, but that was a bad idea. Let's unlink it.
>>> b.unlinkCVE(cve, user=no_priv)
- >>> b.cves.count()
+ >>> len(b.cves)
1
Alternatively, we can link CVEs to bugs by looking for CVEs in a
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2015-09-25 10:23:48 +0000
+++ lib/lp/bugs/model/bug.py 2015-09-29 04:00:42 +0000
@@ -99,11 +99,6 @@
from lp.app.interfaces.services import IService
from lp.app.model.launchpad import InformationTypeMixin
from lp.app.validators import LaunchpadValidationError
-from lp.blueprints.model.specification import Specification
-from lp.blueprints.model.specificationbug import SpecificationBug
-from lp.blueprints.model.specificationsearch import (
- get_specification_privacy_filter,
- )
from lp.bugs.adapters.bug import convert_to_information_type
from lp.bugs.adapters.bugchange import (
BranchLinkedToBug,
@@ -198,6 +193,7 @@
from lp.registry.model.pillar import pillar_sort_key
from lp.registry.model.teammembership import TeamParticipation
from lp.services.config import config
+from lp.services.database import bulk
from lp.services.database.constants import UTC_NOW
from lp.services.database.datetimecol import UtcDateTimeCol
from lp.services.database.decoratedresultset import DecoratedResultSet
@@ -208,6 +204,7 @@
sqlvalues,
)
from lp.services.database.stormbase import StormBase
+from lp.services.features import getFeatureFlag
from lp.services.fields import DuplicateBug
from lp.services.helpers import shortlist
from lp.services.librarian.interfaces import ILibraryFileAliasSet
@@ -234,6 +231,7 @@
from lp.services.webapp.publisher import (
get_raw_form_value_from_current_request,
)
+from lp.services.xref.interfaces import IXRefSet
def snapshot_bug_params(bug_params):
@@ -363,15 +361,7 @@
'BugMessage', joinColumn='bug', orderBy='index')
watches = SQLMultipleJoin(
'BugWatch', joinColumn='bug', orderBy=['bugtracker', 'remotebug'])
- cves = SQLRelatedJoin('Cve', intermediateTable='BugCve',
- orderBy='sequence', joinColumn='bug', otherColumn='cve')
duplicates = SQLMultipleJoin('Bug', joinColumn='duplicateof', orderBy='id')
- specifications = SQLRelatedJoin(
- 'Specification', joinColumn='bug', otherColumn='specification',
- intermediateTable='SpecificationBug', orderBy='-datecreated')
- questions = SQLRelatedJoin('Question', joinColumn='bug',
- otherColumn='question', intermediateTable='QuestionBug',
- orderBy='-datecreated')
linked_branches = SQLMultipleJoin(
'BugBranch', joinColumn='bug', orderBy='id')
date_last_message = UtcDateTimeCol(default=None)
@@ -383,12 +373,63 @@
heat_last_updated = UtcDateTimeCol(default=None)
latest_patch_uploaded = UtcDateTimeCol(default=None)
+ @property
+ def cves(self):
+ from lp.bugs.model.bugcve import BugCve
+ from lp.bugs.model.cve import Cve
+ if getFeatureFlag('bugs.xref_buglinks.query'):
+ xref_cve_sequences = [
+ sequence for _, sequence in getUtility(IXRefSet).findFrom(
+ (u'bug', unicode(self.id)), types=[u'cve'])]
+ expr = Cve.sequence.is_in(xref_cve_sequences)
+ else:
+ old_cve_ids = list(IStore(BugCve).find(
+ BugCve,
+ BugCve.bug == self).values(BugCve.cveID))
+ expr = Cve.id.is_in(old_cve_ids)
+ return list(sorted(
+ IStore(Cve).find(Cve, expr), key=operator.attrgetter('sequence')))
+
+ @property
+ def questions(self):
+ from lp.answers.model.question import Question
+ from lp.coop.answersbugs.model import QuestionBug
+ if getFeatureFlag('bugs.xref_buglinks.query'):
+ question_ids = [
+ int(id) for _, id in getUtility(IXRefSet).findFrom(
+ (u'bug', unicode(self.id)), types=[u'question'])]
+ else:
+ question_ids = list(IStore(QuestionBug).find(
+ QuestionBug,
+ QuestionBug.bug == self).values(QuestionBug.questionID))
+ return list(sorted(
+ bulk.load(Question, question_ids), key=operator.attrgetter('id')))
+
+ @property
+ def specifications(self):
+ from lp.blueprints.model.specification import Specification
+ from lp.blueprints.model.specificationbug import SpecificationBug
+ if getFeatureFlag('bugs.xref_buglinks.query'):
+ spec_ids = [
+ int(id) for _, id in getUtility(IXRefSet).findFrom(
+ (u'bug', unicode(self.id)), types=[u'specification'])]
+ else:
+ spec_ids = list(IStore(SpecificationBug).find(
+ SpecificationBug,
+ SpecificationBug.bug == self).values(
+ SpecificationBug.specificationID))
+ return list(sorted(
+ bulk.load(Specification, spec_ids), key=operator.attrgetter('id')))
+
def getSpecifications(self, user):
"""See `IBug`."""
- return IStore(SpecificationBug).find(
+ from lp.blueprints.model.specification import Specification
+ from lp.blueprints.model.specificationsearch import (
+ get_specification_privacy_filter,
+ )
+ return IStore(Specification).find(
Specification,
- SpecificationBug.bugID == self.id,
- SpecificationBug.specificationID == Specification.id,
+ Specification.id.is_in(spec.id for spec in self.specifications),
*get_specification_privacy_filter(user))
@property
=== modified file 'lib/lp/bugs/model/bugcve.py'
--- lib/lp/bugs/model/bugcve.py 2015-09-25 10:15:37 +0000
+++ lib/lp/bugs/model/bugcve.py 2015-09-29 04:00:42 +0000
@@ -6,6 +6,8 @@
from sqlobject import ForeignKey
+from lp.services.database.constants import UTC_NOW
+from lp.services.database.datetimecol import UtcDateTimeCol
from lp.services.database.sqlbase import SQLBase
@@ -17,3 +19,4 @@
# db field names
bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True)
cve = ForeignKey(dbName='cve', foreignKey='Cve', notNull=True)
+ date_created = UtcDateTimeCol(notNull=True, default=UTC_NOW)
=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py 2015-07-08 16:05:11 +0000
+++ lib/lp/bugs/model/bugtask.py 2015-09-29 04:00:42 +0000
@@ -139,10 +139,12 @@
SQLBase,
sqlvalues,
)
+from lp.services.features import getFeatureFlag
from lp.services.helpers import shortlist
from lp.services.propertycache import get_property_cache
from lp.services.searchbuilder import any
from lp.services.webapp.interfaces import ILaunchBag
+from lp.services.xref.interfaces import IXRefSet
def bugtask_sort_key(bugtask):
@@ -1388,9 +1390,15 @@
from lp.bugs.model.bugbranch import BugBranch
bug_ids = set(bugtask.bugID for bugtask in bugtasks)
- bug_ids_with_specifications = set(IStore(SpecificationBug).find(
- SpecificationBug.bugID,
- SpecificationBug.bugID.is_in(bug_ids)))
+ if getFeatureFlag('bugs.xref_buglinks.query'):
+ bug_ids_with_specifications = set(
+ int(id) for _, id in getUtility(IXRefSet).findFromMany(
+ [(u'bug', unicode(bug_id)) for bug_id in bug_ids],
+ types=[u'specification']).keys())
+ else:
+ bug_ids_with_specifications = set(IStore(SpecificationBug).find(
+ SpecificationBug.bugID,
+ SpecificationBug.bugID.is_in(bug_ids)))
bug_ids_with_branches = set(IStore(BugBranch).find(
BugBranch.bugID, BugBranch.bugID.is_in(bug_ids)))
# Badging looks up milestones too : eager load into the storm cache.
=== modified file 'lib/lp/bugs/model/bugtasksearch.py'
--- lib/lp/bugs/model/bugtasksearch.py 2015-09-28 12:33:22 +0000
+++ lib/lp/bugs/model/bugtasksearch.py 2015-09-29 04:00:42 +0000
@@ -95,6 +95,7 @@
rank_by_fti,
Unnest,
)
+from lp.services.features import getFeatureFlag
from lp.services.propertycache import get_property_cache
from lp.services.searchbuilder import (
all,
@@ -103,6 +104,7 @@
not_equals,
NULL,
)
+from lp.services.xref.model import XRef
from lp.soyuz.enums import PackagePublishingStatus
from lp.soyuz.model.publishing import SourcePackagePublishingHistory
@@ -421,9 +423,18 @@
BugTaskFlat.productseries == None))
if params.has_cve:
- extra_clauses.append(
- BugTaskFlat.bug_id.is_in(
- Select(BugCve.bugID, tables=[BugCve])))
+ if getFeatureFlag('bugs.xref_buglinks.query'):
+ where = [
+ XRef.from_type == u'bug',
+ XRef.from_id_int == BugTaskFlat.bug_id,
+ XRef.to_type == u'cve',
+ ]
+ extra_clauses.append(Exists(Select(
+ 1, tables=[XRef], where=And(*where))))
+ else:
+ extra_clauses.append(
+ BugTaskFlat.bug_id.is_in(
+ Select(BugCve.bugID, tables=[BugCve])))
if params.attachmenttype is not None:
if params.attachmenttype == BugAttachmentType.PATCH:
@@ -1013,12 +1024,26 @@
linked_blueprints = params.linked_blueprints
def make_clause(blueprints=None):
- where = [SpecificationBug.bugID == BugTaskFlat.bug_id]
- if blueprints is not None:
- where.append(
- search_value_to_storm_where_condition(
- SpecificationBug.specificationID, blueprints))
- return Exists(Select(1, tables=[SpecificationBug], where=And(*where)))
+ if getFeatureFlag('bugs.xref_buglinks.query'):
+ where = [
+ XRef.from_type == u'bug',
+ XRef.from_id_int == BugTaskFlat.bug_id,
+ XRef.to_type == u'specification',
+ ]
+ if blueprints is not None:
+ where.append(
+ search_value_to_storm_where_condition(
+ XRef.to_id_int, blueprints))
+ return Exists(Select(
+ 1, tables=[XRef], where=And(*where)))
+ else:
+ where = [SpecificationBug.bugID == BugTaskFlat.bug_id]
+ if blueprints is not None:
+ where.append(
+ search_value_to_storm_where_condition(
+ SpecificationBug.specificationID, blueprints))
+ return Exists(Select(
+ 1, tables=[SpecificationBug], where=And(*where)))
if linked_blueprints is None:
return None
=== modified file 'lib/lp/bugs/model/cve.py'
--- lib/lp/bugs/model/cve.py 2015-09-28 07:57:17 +0000
+++ lib/lp/bugs/model/cve.py 2015-09-29 04:00:42 +0000
@@ -8,16 +8,16 @@
'CveSet',
]
-# SQL imports
+import operator
+
from sqlobject import (
SQLMultipleJoin,
SQLObjectNotFound,
- SQLRelatedJoin,
StringCol,
)
from storm.expr import In
from storm.store import Store
-# Zope
+from zope.component import getUtility
from zope.interface import implementer
from lp.app.validators.cve import (
@@ -34,12 +34,16 @@
from lp.bugs.model.bugcve import BugCve
from lp.bugs.model.buglinktarget import BugLinkTargetMixin
from lp.bugs.model.cvereference import CveReference
-from lp.services.database.bulk import load_related
+from lp.services.database import bulk
from lp.services.database.constants import UTC_NOW
from lp.services.database.datetimecol import UtcDateTimeCol
from lp.services.database.enumcol import EnumCol
+from lp.services.database.interfaces import IStore
from lp.services.database.sqlbase import SQLBase
from lp.services.database.stormexpr import fti_search
+from lp.services.features import getFeatureFlag
+from lp.services.xref.interfaces import IXRefSet
+from lp.services.xref.model import XRef
@implementer(ICve, IBugLinkTarget)
@@ -54,10 +58,6 @@
datecreated = UtcDateTimeCol(notNull=True, default=UTC_NOW)
datemodified = UtcDateTimeCol(notNull=True, default=UTC_NOW)
- # joins
- bugs = SQLRelatedJoin('Bug', intermediateTable='BugCve',
- joinColumn='cve', otherColumn='bug', orderBy='id')
- bug_links = SQLMultipleJoin('BugCve', joinColumn='cve', orderBy='id')
references = SQLMultipleJoin(
'CveReference', joinColumn='cve', orderBy='id')
@@ -75,6 +75,19 @@
def title(self):
return 'CVE-%s (%s)' % (self.sequence, self.status.title)
+ @property
+ def bugs(self):
+ if getFeatureFlag('bugs.xref_buglinks.query'):
+ bug_ids = [
+ int(id) for _, id in getUtility(IXRefSet).findFrom(
+ (u'cve', self.sequence), types=[u'bug'])]
+ else:
+ bug_ids = list(IStore(BugCve).find(
+ BugCve,
+ BugCve.cve == self).values(BugCve.bugID))
+ return list(sorted(
+ bulk.load(Bug, bug_ids), key=operator.attrgetter('id')))
+
# CveReference's
def createReference(self, source, content, url=None):
"""See ICveReference."""
@@ -87,11 +100,18 @@
def createBugLink(self, bug):
"""See BugLinkTargetMixin."""
- BugCve(cve=self, bug=bug)
+ if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'):
+ BugCve(cve=self, bug=bug)
+ # XXX: Should set creator.
+ getUtility(IXRefSet).create(
+ {(u'cve', self.sequence): {(u'bug', unicode(bug.id)): {}}})
def deleteBugLink(self, bug):
"""See BugLinkTargetMixin."""
- Store.of(self).find(BugCve, cve=self, bug=bug).remove()
+ if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'):
+ Store.of(self).find(BugCve, cve=self, bug=bug).remove()
+ getUtility(IXRefSet).delete(
+ {(u'cve', self.sequence): [(u'bug', unicode(bug.id))]})
@implementer(ICveSet)
@@ -177,39 +197,52 @@
def getBugCvesForBugTasks(self, bugtasks, cve_mapper=None):
"""See ICveSet."""
- bugs = load_related(Bug, bugtasks, ('bugID', ))
+ bugs = bulk.load_related(Bug, bugtasks, ('bugID', ))
if len(bugs) == 0:
return []
- bug_ids = [bug.id for bug in bugs]
-
- # Do not use BugCve instances: Storm may need a very long time
- # to look up the bugs and CVEs referenced by a BugCve instance
- # when the +cve view of a distroseries is rendered: There may
- # be a few thousand (bug, CVE) tuples, while the number of bugs
- # and CVEs is in the order of hundred. It is much more efficient
- # to retrieve just (bug_id, cve_id) from the BugCve table and
- # to map this to (Bug, CVE) here, instead of letting Storm
- # look up the CVE and bug for a BugCve instance, even if bugs
- # and CVEs are bulk loaded.
store = Store.of(bugtasks[0])
- bugcve_ids = store.find(
- (BugCve.bugID, BugCve.cveID), In(BugCve.bugID, bug_ids))
- bugcve_ids.order_by(BugCve.bugID, BugCve.cveID)
- bugcve_ids = list(bugcve_ids)
-
- cve_ids = set(cve_id for bug_id, cve_id in bugcve_ids)
- cves = store.find(Cve, In(Cve.id, list(cve_ids)))
+
+ if getFeatureFlag('bugs.xref_buglinks.query'):
+ xrefs = getUtility(IXRefSet).findFromMany(
+ [(u'bug', unicode(bug.id)) for bug in bugs], types=[u'cve'])
+ bugcve_ids = set()
+ for bug_key in xrefs:
+ for cve_key in xrefs[bug_key]:
+ bugcve_ids.add((int(bug_key[1]), cve_key[1]))
+ else:
+ # Do not use BugCve instances: Storm may need a very long time
+ # to look up the bugs and CVEs referenced by a BugCve instance
+ # when the +cve view of a distroseries is rendered: There may
+ # be a few thousand (bug, CVE) tuples, while the number of bugs
+ # and CVEs is in the order of hundred. It is much more efficient
+ # to retrieve just (Bug.id, Cve.sequence) from the BugCve
+ # table and to map this to (Bug, CVE) here, instead of
+ # letting Storm look up the CVE and bug for a BugCve
+ # instance, even if bugs and CVEs are bulk loaded.
+ bug_ids = [bug.id for bug in bugs]
+ bugcve_ids = store.find(
+ (BugCve.bugID, Cve.sequence),
+ Cve.id == BugCve.cveID, In(BugCve.bugID, bug_ids))
+
+ bugcve_ids = list(sorted(bugcve_ids))
+
+ cves = store.find(
+ Cve, In(Cve.sequence, [seq for _, seq in bugcve_ids]))
if cve_mapper is None:
- cvemap = dict((cve.id, cve) for cve in cves)
+ cvemap = dict((cve.sequence, cve) for cve in cves)
else:
- cvemap = dict((cve.id, cve_mapper(cve)) for cve in cves)
+ cvemap = dict((cve.sequence, cve_mapper(cve)) for cve in cves)
bugmap = dict((bug.id, bug) for bug in bugs)
return [
- (bugmap[bug_id], cvemap[cve_id])
- for bug_id, cve_id in bugcve_ids
+ (bugmap[bug_id], cvemap[cve_sequence])
+ for bug_id, cve_sequence in bugcve_ids
]
def getBugCveCount(self):
"""See ICveSet."""
- return BugCve.select().count()
+ if getFeatureFlag('bugs.xref_buglinks.query'):
+ return IStore(XRef).find(
+ XRef, XRef.from_type == u'bug', XRef.to_type == u'cve').count()
+ else:
+ return BugCve.select().count()
=== modified file 'lib/lp/bugs/model/tests/test_bugtask.py'
--- lib/lp/bugs/model/tests/test_bugtask.py 2014-07-08 09:53:50 +0000
+++ lib/lp/bugs/model/tests/test_bugtask.py 2015-09-29 04:00:42 +0000
@@ -526,6 +526,22 @@
])
+class TestBugTaskBadgesWithXRef(TestBugTaskBadges):
+
+ def setUp(self):
+ super(TestBugTaskBadges, self).setUp()
+ self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'}))
+
+
+class TestBugTaskBadgesWithXRefAndNoOld(TestBugTaskBadges):
+
+ def setUp(self):
+ super(TestBugTaskBadges, self).setUp()
+ self.useFixture(FeatureFixture({
+ 'bugs.xref_buglinks.query': 'true',
+ 'bugs.xref_buglinks.write_old.disabled': 'true'}))
+
+
class TestBugTaskPrivacy(TestCaseWithFactory):
"""Verify that the bug is either private or public.
=== modified file 'lib/lp/bugs/model/tests/test_bugtasksearch.py'
--- lib/lp/bugs/model/tests/test_bugtasksearch.py 2015-09-28 12:33:22 +0000
+++ lib/lp/bugs/model/tests/test_bugtasksearch.py 2015-09-29 04:00:42 +0000
@@ -67,6 +67,7 @@
from lp.registry.model.person import Person
from lp.services.database.interfaces import IStore
from lp.services.database.sqlbase import convert_storm_clause_to_string
+from lp.services.features.testing import FeatureFixture
from lp.services.searchbuilder import (
all,
any,
@@ -419,6 +420,16 @@
BugBlueprintSearch.BUGS_WITHOUT_BLUEPRINTS))
self.assertSearchFinds(params, self.bugtasks[1:])
+ def test_blueprints_linked_with_xref(self):
+ self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'}))
+ self.test_blueprints_linked()
+
+ def test_blueprints_linked_with_xref_and_no_old(self):
+ self.useFixture(FeatureFixture({
+ 'bugs.xref_buglinks.query': 'true',
+ 'bugs.xref_buglinks.write_old.disabled': 'true'}))
+ self.test_blueprints_linked()
+
def test_limit_search_to_one_bug(self):
# Search results can be limited to a given bug.
params = self.getBugTaskSearchParams(
@@ -492,6 +503,16 @@
params = self.getBugTaskSearchParams(user=None, has_cve=True)
self.assertSearchFinds(params, self.bugtasks[:1])
+ def test_has_cve_with_xref(self):
+ self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'}))
+ self.test_has_cve()
+
+ def test_has_cve_with_xref_and_no_old(self):
+ self.useFixture(FeatureFixture({
+ 'bugs.xref_buglinks.query': 'true',
+ 'bugs.xref_buglinks.write_old.disabled': 'true'}))
+ self.test_has_cve()
+
def test_sort_by_milestone_name(self):
expected = self.setUpMilestoneSorting()
params = self.getBugTaskSearchParams(
=== modified file 'lib/lp/bugs/tests/test_cve.py'
--- lib/lp/bugs/tests/test_cve.py 2015-09-28 07:39:28 +0000
+++ lib/lp/bugs/tests/test_cve.py 2015-09-29 04:00:42 +0000
@@ -7,6 +7,7 @@
from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
from lp.bugs.interfaces.cve import ICveSet
+from lp.services.features.testing import FeatureFixture
from lp.testing import (
login_person,
person_logged_in,
@@ -77,6 +78,42 @@
u'CVE-2000-0004']
self.assertEqual(expected, cve_data)
+ def test_getBugCveCount(self):
+ login_person(self.factory.makePerson())
+
+ base = getUtility(ICveSet).getBugCveCount()
+ bug1 = self.factory.makeBug()
+ bug2 = self.factory.makeBug()
+ cve1 = self.factory.makeCVE(sequence='2099-1234')
+ cve2 = self.factory.makeCVE(sequence='2099-2468')
+ self.assertEqual(base, getUtility(ICveSet).getBugCveCount())
+ cve1.linkBug(bug1)
+ self.assertEqual(base + 1, getUtility(ICveSet).getBugCveCount())
+ cve1.linkBug(bug2)
+ self.assertEqual(base + 2, getUtility(ICveSet).getBugCveCount())
+ cve2.linkBug(bug1)
+ self.assertEqual(base + 3, getUtility(ICveSet).getBugCveCount())
+ cve1.unlinkBug(bug1)
+ cve1.unlinkBug(bug2)
+ cve2.unlinkBug(bug1)
+ self.assertEqual(base, getUtility(ICveSet).getBugCveCount())
+
+
+class TestCveSetWithXRef(TestCveSet):
+
+ def setUp(self):
+ self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'}))
+ super(TestCveSetWithXRef, self).setUp()
+
+
+class TestCveSetWithXRefAndNoOld(TestCveSet):
+
+ def setUp(self):
+ self.useFixture(FeatureFixture({
+ 'bugs.xref_buglinks.query': 'true',
+ 'bugs.xref_buglinks.write_old.disabled': 'true'}))
+ super(TestCveSetWithXRefAndNoOld, self).setUp()
+
class TestBugLinks(TestCaseWithFactory):
@@ -113,3 +150,19 @@
self.assertContentEqual([bug1], cve2.bugs)
self.assertContentEqual([cve2], bug1.cves)
self.assertContentEqual([], bug2.cves)
+
+
+class TestBugLinksWithXRef(TestBugLinks):
+
+ def setUp(self):
+ super(TestBugLinksWithXRef, self).setUp()
+ self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'}))
+
+
+class TestBugLinksWithXRefAndNoOld(TestBugLinks):
+
+ def setUp(self):
+ super(TestBugLinksWithXRefAndNoOld, self).setUp()
+ self.useFixture(FeatureFixture({
+ 'bugs.xref_buglinks.query': 'true',
+ 'bugs.xref_buglinks.write_old.disabled': 'true'}))
=== modified file 'lib/lp/coop/answersbugs/model.py'
--- lib/lp/coop/answersbugs/model.py 2015-09-25 10:15:37 +0000
+++ lib/lp/coop/answersbugs/model.py 2015-09-29 04:00:42 +0000
@@ -9,6 +9,8 @@
from sqlobject import ForeignKey
+from lp.services.database.constants import UTC_NOW
+from lp.services.database.datetimecol import UtcDateTimeCol
from lp.services.database.sqlbase import SQLBase
@@ -20,3 +22,4 @@
question = ForeignKey(
dbName='question', foreignKey='Question', notNull=True)
bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True)
+ date_created = UtcDateTimeCol(notNull=True, default=UTC_NOW)
=== modified file 'lib/lp/coop/answersbugs/tests/test_questionbug.py'
--- lib/lp/coop/answersbugs/tests/test_questionbug.py 2015-09-28 07:39:28 +0000
+++ lib/lp/coop/answersbugs/tests/test_questionbug.py 2015-09-29 04:00:42 +0000
@@ -1,6 +1,7 @@
# Copyright 2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
+from lp.services.features.testing import FeatureFixture
from lp.testing import (
login_person,
TestCaseWithFactory,
@@ -43,3 +44,19 @@
self.assertContentEqual([bug1], question2.bugs)
self.assertContentEqual([question2], bug1.questions)
self.assertContentEqual([], bug2.questions)
+
+
+class TestQuestionBugLinksWithXRef(TestQuestionBugLinks):
+
+ def setUp(self):
+ super(TestQuestionBugLinksWithXRef, self).setUp()
+ self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'}))
+
+
+class TestQuestionBugLinksWithXRefAndNoOld(TestQuestionBugLinks):
+
+ def setUp(self):
+ super(TestQuestionBugLinksWithXRefAndNoOld, self).setUp()
+ self.useFixture(FeatureFixture({
+ 'bugs.xref_buglinks.query': 'true',
+ 'bugs.xref_buglinks.write_old.disabled': 'true'}))
=== modified file 'lib/lp/scripts/garbo.py'
--- lib/lp/scripts/garbo.py 2015-07-22 07:09:12 +0000
+++ lib/lp/scripts/garbo.py 2015-09-29 04:00:42 +0000
@@ -81,6 +81,7 @@
from lp.services.database.bulk import (
create,
dbify_value,
+ load_related,
)
from lp.services.database.constants import UTC_NOW
from lp.services.database.interfaces import IMasterStore
@@ -1678,6 +1679,66 @@
transaction.abort()
+class BugXRefMigrator(TunableLoop):
+ """Creates an XRef record for each former IBugLink."""
+
+ maximum_chunk_size = 5000
+
+ def __init__(self, log, abort_time=None):
+ super(BugXRefMigrator, self).__init__(log, abort_time)
+ self.start_at = 1
+ self.store = IMasterStore(Bug)
+
+ def findBugs(self):
+ if not getFeatureFlag('bugs.xref_buglinks.garbo.enabled'):
+ return EmptyResultSet()
+ return self.store.find(
+ Bug, Bug.id >= self.start_at).order_by(Bug.id)
+
+ def isDone(self):
+ return self.findBugs().is_empty()
+
+ def __call__(self, chunk_size):
+ # Grab a chunk of Bug IDs.
+ # Find all QuestionBugs, SpecificationBugs and BugCves for each
+ # of those bugs.
+ # Compose a list of link IDs that should exist.
+ # Perform a bulk XRef find for all of those.
+ # Delete any extras, create any missing.
+ from lp.blueprints.model.specificationbug import SpecificationBug
+ from lp.bugs.model.bugcve import BugCve
+ from lp.bugs.model.cve import Cve
+ from lp.coop.answersbugs.model import QuestionBug
+ from lp.services.xref.interfaces import IXRefSet
+ bug_ids = list(self.findBugs()[:chunk_size].values(Bug.id))
+ qbs = list(self.store.find(
+ QuestionBug, QuestionBug.bugID.is_in(bug_ids)))
+ sbs = list(self.store.find(
+ SpecificationBug, SpecificationBug.bugID.is_in(bug_ids)))
+ bcs = list(self.store.find(BugCve, BugCve.bugID.is_in(bug_ids)))
+ wanted = {(u'bug', unicode(bug_id)): {} for bug_id in bug_ids}
+ for qb in qbs:
+ wanted[(u'bug', unicode(qb.bugID))][
+ (u'question', unicode(qb.questionID))] = {
+ 'date_created': qb.date_created}
+ for sb in sbs:
+ wanted[(u'bug', unicode(sb.bugID))][
+ (u'specification', unicode(sb.specificationID))] = {}
+ load_related(Cve, bcs, ['cveID'])
+ for bc in bcs:
+ wanted[(u'bug', unicode(bc.bugID))][
+ (u'cve', unicode(bc.cve.sequence))] = {}
+ existing = getUtility(IXRefSet).findFromMany(wanted.keys())
+ needed = {
+ bug: {
+ other: meta for other, meta in others.iteritems()
+ if other not in existing.get(bug, {})}
+ for bug, others in wanted.iteritems() if others}
+ getUtility(IXRefSet).create(needed)
+ self.start_at = bug_ids[-1] + 1
+ transaction.commit()
+
+
class FrequentDatabaseGarbageCollector(BaseDatabaseGarbageCollector):
"""Run every 5 minutes.
@@ -1714,6 +1775,7 @@
DuplicateSessionPruner,
RevisionCachePruner,
UnusedSessionPruner,
+ BugXRefMigrator,
]
experimental_tunable_loops = []
=== modified file 'lib/lp/scripts/tests/test_garbo.py'
--- lib/lp/scripts/tests/test_garbo.py 2015-07-22 07:09:12 +0000
+++ lib/lp/scripts/tests/test_garbo.py 2015-09-29 04:00:42 +0000
@@ -1433,6 +1433,94 @@
for person in people_enf_true:
_assert_enf_by_person(person, True)
+ def test_BugXRefMigrator(self):
+ from testtools.matchers import (
+ Equals,
+ Is,
+ MatchesDict,
+ Not,
+ )
+
+ from lp.bugs.model.bug import Bug
+ from lp.bugs.model.bugcve import BugCve
+ from lp.blueprints.model.specificationbug import SpecificationBug
+ from lp.coop.answersbugs.model import QuestionBug
+ from lp.services.database.interfaces import IStore
+ from lp.services.xref.interfaces import IXRefSet
+
+ switch_dbuser('testadmin')
+ self.useFixture(FeatureFixture(
+ {'bugs.xref_buglinks.garbo.enabled': 'on'}))
+ store = IStore(Bug)
+
+ # The first bug has a spec and a question.
+ bug1 = self.factory.makeBug()
+ spec1 = self.factory.makeSpecification()
+ sb1 = SpecificationBug(specification=spec1, bug=bug1)
+ store.add(sb1)
+ question1 = self.factory.makeQuestion()
+ qb1 = QuestionBug(question=question1, bug=bug1)
+ store.add(qb1)
+
+ # A second bug has a question and a CVE.
+ bug2 = self.factory.makeBug()
+ question2 = self.factory.makeQuestion()
+ qb2 = QuestionBug(question=question2, bug=bug2)
+ store.add(qb2)
+ cve2 = self.factory.makeCVE(sequence='2099-1234')
+ bc2 = BugCve(bug=bug2, cve=cve2)
+ store.add(bc2)
+
+ # Bug the third is all alone.
+ bug3 = self.factory.makeBug()
+
+ # Bug four has just a spec.
+ bug4 = self.factory.makeBug()
+ spec4 = self.factory.makeSpecification()
+ sb4 = SpecificationBug(specification=spec4, bug=bug4)
+ store.add(sb4)
+
+ # Initially the new XRef table has no links for the bugs.
+ self.assertEqual(
+ {},
+ getUtility(IXRefSet).findFromMany(
+ (u'bug', unicode(bug.id)) for bug in (bug1, bug2, bug3, bug4)))
+
+ # Garbo fills in links for each QuestionBug, SpecificationBug
+ # and BugCve.
+ self.runHourly()
+ matches_expected = MatchesDict({
+ (u'bug', unicode(bug1.id)): MatchesDict({
+ (u'specification', unicode(spec1.id)): MatchesDict({
+ 'metadata': Is(None), 'creator': Is(None),
+ 'date_created': Not(Is(None))}),
+ (u'question', unicode(question1.id)): MatchesDict({
+ 'metadata': Is(None), 'creator': Is(None),
+ 'date_created': Equals(qb1.date_created)}),
+ }),
+ (u'bug', unicode(bug2.id)): MatchesDict({
+ (u'question', unicode(question2.id)): MatchesDict({
+ 'metadata': Is(None), 'creator': Is(None),
+ 'date_created': Equals(qb2.date_created)}),
+ (u'cve', cve2.sequence): MatchesDict({
+ 'metadata': Is(None), 'creator': Is(None),
+ 'date_created': Not(Is(None))}),
+ }),
+ (u'bug', unicode(bug4.id)): MatchesDict({
+ (u'specification', unicode(spec4.id)): MatchesDict({
+ 'metadata': Is(None), 'creator': Is(None),
+ 'date_created': Not(Is(None))}),
+ }),
+ })
+ self.assertThat(
+ getUtility(IXRefSet).findFromMany(
+ (u'bug', unicode(bug.id)) for bug in (bug1, bug2, bug3, bug4)),
+ matches_expected)
+
+ # A second run is harmless.
+ self.runHourly()
+
+
class TestGarboTasks(TestCaseWithFactory):
layer = LaunchpadZopelessLayer
Follow ups