~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

# pylint: disable-msg=E0611,W0212

__metaclass__ = type

__all__ = [
    'RecipeBuildRecord',
    'RecipeBuildRecordSet',
    ]

from collections import namedtuple
from datetime import (
    datetime,
    timedelta,
    )

import pytz
from storm import Undef
from storm.expr import (
    Desc,
    Join,
    Max,
    Select,
    )
from zope.interface import implements

from lp.buildmaster.enums import BuildStatus
from lp.buildmaster.model.buildfarmjob import BuildFarmJob
from lp.buildmaster.model.packagebuild import PackageBuild
from lp.code.interfaces.recipebuild import IRecipeBuildRecordSet
from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild
from lp.registry.model.person import Person
from lp.registry.model.sourcepackagename import SourcePackageName
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.lpstorm import ISlaveStore
from lp.services.database.stormexpr import CountDistinct
from lp.services.webapp.publisher import canonical_url
from lp.soyuz.model.archive import Archive
from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease


class RecipeBuildRecord(namedtuple(
    'RecipeBuildRecord',
    """sourcepackagename, recipeowner, archive, recipe,
        most_recent_build_time""")):

    def __new__(cls, sourcepackagename, recipeowner, archive, recipe,
                most_recent_build_time):
        # Ensure that a valid (not None) recipe is used. This may change in
        # future if we support build records with no recipe.
        assert recipe is not None, "RecipeBuildRecord requires a recipe."
        self = super(RecipeBuildRecord, cls).__new__(
            cls, sourcepackagename, recipeowner, archive, recipe,
            most_recent_build_time)
        return self

    # We need to implement our own equality check since __eq__ is broken on
    # SourcePackageRecipe. It's broken there because __eq__ is broken,
    # or not supported, on storm's ReferenceSet implementation.
    def __eq__(self, other):
        return (self.sourcepackagename == other.sourcepackagename
            and self.recipeowner == other.recipeowner
            and self.recipe.name == other.recipe.name
            and self.archive == other.archive
            and self.most_recent_build_time == other.most_recent_build_time)

    def __hash__(self):
        return (
            hash(self.sourcepackagename.name) ^
            hash(self.recipeowner.name) ^
            hash(self.recipe.name) ^
            hash(self.archive.name) ^
            hash(self.most_recent_build_time))

    @property
    def distro_source_package(self):
        return self.archive.distribution.getSourcePackage(
            self.sourcepackagename)

    @property
    def recipe_name(self):
        return self.recipe.name

    @property
    def recipe_url(self):
        return canonical_url(self.recipe, rootsite='code')


class RecipeBuildRecordSet:
    """See `IRecipeBuildRecordSet`."""

    implements(IRecipeBuildRecordSet)

    def findCompletedDailyBuilds(self, epoch_days=30):
        """See `IRecipeBuildRecordSet`."""

        store = ISlaveStore(SourcePackageRecipe)
        tables = [
            SourcePackageRecipe,
            Join(SourcePackageRecipeBuild,
                 SourcePackageRecipeBuild.recipe_id ==
                 SourcePackageRecipe.id),
            Join(SourcePackageRelease,
                 SourcePackageRecipeBuild.id ==
                 SourcePackageRelease.source_package_recipe_build_id),
            Join(SourcePackageName,
                 SourcePackageRelease.sourcepackagename ==
                 SourcePackageName.id),
            Join(BinaryPackageBuild,
                 BinaryPackageBuild.source_package_release_id ==
                    SourcePackageRelease.id),
            Join(PackageBuild,
                 PackageBuild.id ==
                 BinaryPackageBuild.package_build_id),
            Join(BuildFarmJob,
                 BuildFarmJob.id ==
                 PackageBuild.build_farm_job_id),
        ]

        where = [BuildFarmJob.status == BuildStatus.FULLYBUILT,
                    SourcePackageRecipe.build_daily]
        if epoch_days is not None:
            epoch = datetime.now(pytz.UTC) - timedelta(days=epoch_days)
            where.append(BuildFarmJob.date_finished >= epoch)

        # We include SourcePackageName directly in the query instead of just
        # selecting its id and fetching the objects later. This is because
        # SourcePackageName only has an id and and a name and we use the name
        # for the order by so there's no benefit in introducing another query
        # for eager fetching later. If SourcePackageName gets new attributes,
        # this can be re-evaluated.
        result_set = store.using(*tables).find(
                (SourcePackageRecipe.id,
                    SourcePackageName,
                    Max(BuildFarmJob.date_finished),
                    ),
                *where
            ).group_by(
                SourcePackageRecipe.id,
                SourcePackageName,
            ).order_by(
                SourcePackageName.name,
                Desc(Max(BuildFarmJob.date_finished)),
            )

        def _makeRecipeBuildRecord(values):
            (recipe_id, sourcepackagename, date_finished) = values
            recipe = store.get(SourcePackageRecipe, recipe_id)
            return RecipeBuildRecord(
                sourcepackagename, recipe.owner,
                recipe.daily_build_archive, recipe,
                date_finished)

        to_recipes_ids = lambda rows: [row[0] for row in rows]

        def eager_load_recipes(recipe_ids):
            if not recipe_ids:
                return []
            return list(store.find(
                SourcePackageRecipe,
                SourcePackageRecipe.id.is_in(recipe_ids)))

        def eager_load_owners(recipes):
            owner_ids = set(recipe.owner_id for recipe in recipes)
            owner_ids.discard(None)
            if not owner_ids:
                return
            list(store.find(Person, Person.id.is_in(owner_ids)))

        def eager_load_archives(recipes):
            archive_ids = set(
            recipe.daily_build_archive_id for recipe in recipes)
            archive_ids.discard(None)
            if not archive_ids:
                return
            list(store.find(Archive, Archive.id.is_in(archive_ids)))

        def _prefetchRecipeBuildData(rows):
            recipe_ids = set(to_recipes_ids(rows))
            recipe_ids.discard(None)
            recipes = eager_load_recipes(recipe_ids)
            eager_load_owners(recipes)
            eager_load_archives(recipes)

        return RecipeBuildRecordResultSet(
            result_set, _makeRecipeBuildRecord,
            pre_iter_hook=_prefetchRecipeBuildData)


class RecipeBuildRecordResultSet(DecoratedResultSet):
    """A ResultSet which can count() queries with group by."""

    def count(self, expr=Undef, distinct=True):
        """This count() knows how to handle result sets with group by."""

        # We don't support distinct=False for this result set
        select = Select(
            columns=CountDistinct(self.result_set._group_by),
            tables=self.result_set._tables,
            where=self.result_set._where,
            )
        result = self.result_set._store.execute(select)
        return result.get_one()[0]