~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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# Copyright 2010 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""Tests for `IBuildFarmJob`."""

__metaclass__ = type

from datetime import (
    datetime,
    timedelta,
    )

import pytz
from storm.store import Store
from zope.component import getUtility
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy

from canonical.database.sqlbase import flush_database_updates
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from canonical.testing.layers import (
    DatabaseFunctionalLayer,
    LaunchpadFunctionalLayer,
    )
from lp.app.errors import NotFoundError
from lp.buildmaster.enums import (
    BuildFarmJobType,
    BuildStatus,
    )
from lp.buildmaster.interfaces.buildfarmjob import (
    IBuildFarmJob,
    IBuildFarmJobSet,
    IBuildFarmJobSource,
    InconsistentBuildFarmJobError,
    )
from lp.buildmaster.model.buildfarmjob import BuildFarmJob
from lp.testing import (
    login,
    TestCaseWithFactory,
    )


class TestBuildFarmJobMixin:

    layer = DatabaseFunctionalLayer

    def setUp(self):
        """Create a build farm job with which to test."""
        super(TestBuildFarmJobMixin, self).setUp()
        self.build_farm_job = self.makeBuildFarmJob()

    def makeBuildFarmJob(self, builder=None,
                         job_type=BuildFarmJobType.PACKAGEBUILD,
                         status=BuildStatus.NEEDSBUILD,
                         date_finished=None):
        """A factory method for creating PackageBuilds.

        This is not included in the launchpad test factory because
        a build farm job should never be instantiated outside the
        context of a derived class (such as a BinaryPackageBuild
        or eventually a SPRecipeBuild).
        """
        build_farm_job = getUtility(IBuildFarmJobSource).new(
            job_type=job_type, status=status)
        removeSecurityProxy(build_farm_job).builder = builder
        removeSecurityProxy(build_farm_job).date_started = date_finished
        removeSecurityProxy(build_farm_job).date_finished = date_finished
        return build_farm_job


class TestBuildFarmJob(TestBuildFarmJobMixin, TestCaseWithFactory):
    """Tests for the build farm job object."""

    def test_providesInterface(self):
        # BuildFarmJob provides IBuildFarmJob
        self.assertProvides(self.build_farm_job, IBuildFarmJob)

    def test_saves_record(self):
        # A build farm job can be stored in the database.
        flush_database_updates()
        store = Store.of(self.build_farm_job)
        retrieved_job = store.find(
            BuildFarmJob,
            BuildFarmJob.id == self.build_farm_job.id).one()
        self.assertEqual(self.build_farm_job, retrieved_job)

    def test_default_values(self):
        # We flush the database updates to ensure sql defaults
        # are set for various attributes.
        flush_database_updates()
        self.assertEqual(
            BuildStatus.NEEDSBUILD, self.build_farm_job.status)
        # The date_created is set automatically.
        self.assertTrue(self.build_farm_job.date_created is not None)
        # The job type is required to create a build farm job.
        self.assertEqual(
            BuildFarmJobType.PACKAGEBUILD, self.build_farm_job.job_type)
        # Failure count defaults to zero.
        self.assertEqual(0, self.build_farm_job.failure_count)
        # Other attributes are unset by default.
        self.assertEqual(None, self.build_farm_job.processor)
        self.assertEqual(None, self.build_farm_job.virtualized)
        self.assertEqual(None, self.build_farm_job.date_started)
        self.assertEqual(None, self.build_farm_job.date_finished)
        self.assertEqual(None, self.build_farm_job.date_first_dispatched)
        self.assertEqual(None, self.build_farm_job.builder)
        self.assertEqual(None, self.build_farm_job.log)
        self.assertEqual(None, self.build_farm_job.log_url)
        self.assertEqual(None, self.build_farm_job.buildqueue_record)

    def test_unimplemented_methods(self):
        # A build farm job leaves the implementation of various
        # methods for derived classes.
        self.assertRaises(NotImplementedError, self.build_farm_job.score)
        self.assertRaises(NotImplementedError, self.build_farm_job.getName)
        self.assertRaises(NotImplementedError, self.build_farm_job.getTitle)
        self.assertRaises(NotImplementedError, self.build_farm_job.makeJob)

    def test_jobStarted(self):
        # Starting a job sets the date_started and status, as well as
        # the date first dispatched, if it is the first dispatch of
        # this job.
        self.build_farm_job.jobStarted()
        self.assertTrue(self.build_farm_job.date_first_dispatched is not None)
        self.assertTrue(self.build_farm_job.date_started is not None)
        self.assertEqual(
            BuildStatus.BUILDING, self.build_farm_job.status)

    def test_jobReset(self):
        # Resetting a job sets its status back to NEEDSBUILD and unsets
        # the date_started.
        self.build_farm_job.jobStarted()
        self.build_farm_job.jobReset()
        self.failUnlessEqual(
            BuildStatus.NEEDSBUILD, self.build_farm_job.status)
        self.failUnless(self.build_farm_job.date_started is None)

    def test_jobAborted(self):
        # Aborting a job sets its status back to NEEDSBUILD and unsets
        # the date_started.
        self.build_farm_job.jobStarted()
        self.build_farm_job.jobAborted()
        self.failUnlessEqual(
            BuildStatus.NEEDSBUILD, self.build_farm_job.status)
        self.failUnless(self.build_farm_job.date_started is None)

    def test_title(self):
        # The default title simply uses the job type's title.
        self.assertEqual(
            self.build_farm_job.job_type.title,
            self.build_farm_job.title)

    def test_duration_none(self):
        # If either start or finished is none, the duration will be
        # none.
        self.build_farm_job.jobStarted()
        self.failUnlessEqual(None, self.build_farm_job.duration)

        self.build_farm_job.jobAborted()
        removeSecurityProxy(self.build_farm_job).date_finished = (
            datetime.now(pytz.UTC))
        self.failUnlessEqual(None, self.build_farm_job.duration)

    def test_duration_set(self):
        # If both start and finished are defined, the duration will be
        # returned.
        now = datetime.now(pytz.UTC)
        duration = timedelta(1)
        naked_bfj = removeSecurityProxy(self.build_farm_job)
        naked_bfj.date_started = now
        naked_bfj.date_finished = now + duration
        self.failUnlessEqual(duration, self.build_farm_job.duration)

    def test_date_created(self):
        # date_created can be passed optionally when creating a
        # bulid farm job to ensure we don't get identical timestamps
        # when transactions are committed.
        ten_years_ago = datetime.now(pytz.UTC) - timedelta(365*10)
        build_farm_job = getUtility(IBuildFarmJobSource).new(
            job_type=BuildFarmJobType.PACKAGEBUILD,
            date_created=ten_years_ago)
        self.failUnlessEqual(ten_years_ago, build_farm_job.date_created)

    def test_getSpecificJob_none(self):
        # An exception is raised if there is no related specific job.
        self.assertRaises(
            InconsistentBuildFarmJobError, self.build_farm_job.getSpecificJob)

    def test_getSpecificJob_unimplemented_type(self):
        # An `IBuildFarmJob` with an unimplemented type results in an
        # exception.
        removeSecurityProxy(self.build_farm_job).job_type = (
            BuildFarmJobType.RECIPEBRANCHBUILD)

        self.assertRaises(
            InconsistentBuildFarmJobError, self.build_farm_job.getSpecificJob)


class TestBuildFarmJobSecurity(TestBuildFarmJobMixin, TestCaseWithFactory):

    def test_view_build_farm_job(self):
        # Anonymous access can read public builds, but not edit.
        self.failUnlessEqual(
            BuildStatus.NEEDSBUILD, self.build_farm_job.status)
        self.assertRaises(
            Unauthorized, setattr, self.build_farm_job,
            'status', BuildStatus.FULLYBUILT)

    def test_edit_build_farm_job(self):
        # Users with edit access can update attributes.
        login('admin@canonical.com')
        self.build_farm_job.status = BuildStatus.FULLYBUILT
        self.failUnlessEqual(
            BuildStatus.FULLYBUILT, self.build_farm_job.status)


class TestBuildFarmJobSet(TestBuildFarmJobMixin, TestCaseWithFactory):

    layer = LaunchpadFunctionalLayer

    def setUp(self):
        super(TestBuildFarmJobSet, self).setUp()
        self.builder = self.factory.makeBuilder()
        self.build_farm_job_set = getUtility(IBuildFarmJobSet)

    def test_getBuildsForBuilder_all(self):
        # The default call without arguments returns all builds for the
        # builder, and not those for other builders.
        build1 = self.makeBuildFarmJob(builder=self.builder)
        build2 = self.makeBuildFarmJob(builder=self.builder)
        self.makeBuildFarmJob(builder=self.factory.makeBuilder())

        result = self.build_farm_job_set.getBuildsForBuilder(self.builder)

        self.assertContentEqual([build1, build2], result)

    def test_getBuildsForBuilder_by_status(self):
        # If the status arg is used, the results will be filtered by
        # status.
        successful_builds = [
            self.makeBuildFarmJob(
                builder=self.builder, status=BuildStatus.FULLYBUILT),
            self.makeBuildFarmJob(
                builder=self.builder, status=BuildStatus.FULLYBUILT),
            ]
        self.makeBuildFarmJob(builder=self.builder)

        query_by_status = self.build_farm_job_set.getBuildsForBuilder(
                self.builder, status=BuildStatus.FULLYBUILT)

        self.assertContentEqual(successful_builds, query_by_status)

    def _makePrivateAndNonPrivateBuilds(self, owning_team=None):
        """Return a tuple of a private and non-private build farm job."""
        if owning_team is None:
            owning_team = self.factory.makeTeam()
        archive = self.factory.makeArchive(owner=owning_team, private=True)
        private_build = self.factory.makeBinaryPackageBuild(
            archive=archive, builder=self.builder)
        private_build = removeSecurityProxy(private_build).build_farm_job
        other_build = self.makeBuildFarmJob(builder=self.builder)
        return (private_build, other_build)

    def test_getBuildsForBuilder_hides_private_from_anon(self):
        # If no user is passed, all private builds are filtered out.
        private_build, other_build = self._makePrivateAndNonPrivateBuilds()

        result = self.build_farm_job_set.getBuildsForBuilder(self.builder)

        self.assertContentEqual([other_build], result)

    def test_getBuildsForBuilder_hides_private_other_users(self):
        # Private builds are not returned for users without permission
        # to view them.
        private_build, other_build = self._makePrivateAndNonPrivateBuilds()

        result = self.build_farm_job_set.getBuildsForBuilder(
            self.builder, user=self.factory.makePerson())

        self.assertContentEqual([other_build], result)

    def test_getBuildsForBuilder_shows_private_to_admin(self):
        # Admin users can see private builds.
        admin_team = getUtility(ILaunchpadCelebrities).admin
        private_build, other_build = self._makePrivateAndNonPrivateBuilds()

        result = self.build_farm_job_set.getBuildsForBuilder(
            self.builder, user=admin_team.teamowner)

        self.assertContentEqual([private_build, other_build], result)

    def test_getBuildsForBuilder_shows_private_to_authorised(self):
        # Similarly, if the user is in the owning team they can see it.
        owning_team = self.factory.makeTeam()
        private_build, other_build = self._makePrivateAndNonPrivateBuilds(
            owning_team=owning_team)

        result = self.build_farm_job_set.getBuildsForBuilder(
            self.builder,
            user=owning_team.teamowner)

        self.assertContentEqual([private_build, other_build], result)

    def test_getBuildsForBuilder_ordered_by_date_finished(self):
        # Results are returned with the oldest build last.
        build_1 = self.makeBuildFarmJob(
            builder=self.builder,
            date_finished=datetime(2008, 10, 10, tzinfo=pytz.UTC))
        build_2 = self.makeBuildFarmJob(
            builder=self.builder,
            date_finished=datetime(2008, 11, 10, tzinfo=pytz.UTC))

        result = self.build_farm_job_set.getBuildsForBuilder(self.builder)
        self.assertEqual([build_2, build_1], list(result))

        removeSecurityProxy(build_2).date_finished = (
            datetime(2008, 8, 10, tzinfo=pytz.UTC))
        result = self.build_farm_job_set.getBuildsForBuilder(self.builder)

        self.assertEqual([build_1, build_2], list(result))

    def test_getByID(self):
        # getByID returns a job by id.
        build_1 = self.makeBuildFarmJob(
            builder=self.builder,
            date_finished=datetime(2008, 10, 10, tzinfo=pytz.UTC))
        flush_database_updates()
        self.assertEquals(
            build_1, self.build_farm_job_set.getByID(build_1.id))

    def test_getByID_nonexistant(self):
        # getByID raises NotFoundError for unknown job ids.
        self.assertRaises(NotFoundError,
            self.build_farm_job_set.getByID, 423432432432)