Merge ~twom/launchpad:oci-ocirecipebuild into launchpad:master
- Git
- lp:~twom/launchpad
- oci-ocirecipebuild
- Merge into master
Proposed by
Tom Wardill
Status: | Merged |
---|---|
Approved by: | Tom Wardill |
Approved revision: | e923bacc3f895810d1eab6b9708ee026a9946249 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~twom/launchpad:oci-ocirecipebuild |
Merge into: | launchpad:master |
Prerequisite: | ~twom/launchpad:concrete-oci-projects |
Diff against target: |
517 lines (+336/-11) 6 files modified
lib/lp/oci/interfaces/ocirecipe.py (+3/-0) lib/lp/oci/interfaces/ocirecipebuild.py (+47/-2) lib/lp/oci/model/ocirecipe.py (+17/-0) lib/lp/oci/model/ocirecipebuild.py (+75/-6) lib/lp/oci/tests/test_ocirecipebuild.py (+168/-0) lib/lp/testing/factory.py (+26/-3) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+376431@code.launchpad.net |
Commit message
Implement more of OCIRecipeBuild
Description of the change
Using SnapBuild as a model, implement more of the required methods to meet the interfaces of IOCIRecipeBuild and IPackageBuild.
Ensure queueBuild works, for further work in to OCIRecipeBuildJob and BuildBehaviour
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
- b8a2b9a... by Tom Wardill
-
Include virtualized tests
- 1e7cb5c... by Tom Wardill
-
Remove duplicate attributes
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
- 130aaf6... by Tom Wardill
-
Rename method, fix comment
- 9178c04... by Tom Wardill
-
Format imports
- 9a3042d... by Tom Wardill
-
Fix variable name
- 0427d6c... by Tom Wardill
-
Better filename tests
- e923bac... by Tom Wardill
-
Better virtualized tests for ocirecipebuild
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py |
2 | index 340adfc..c6ce359 100644 |
3 | --- a/lib/lp/oci/interfaces/ocirecipe.py |
4 | +++ b/lib/lp/oci/interfaces/ocirecipe.py |
5 | @@ -240,3 +240,6 @@ class IOCIRecipeSet(Interface): |
6 | |
7 | def findByOwner(owner): |
8 | """Return all OCI Recipes with the given `owner`.""" |
9 | + |
10 | + def preloadDataForOCIRecipes(recipes, user): |
11 | + """Load the data reloated to a list of OCI Recipes.""" |
12 | diff --git a/lib/lp/oci/interfaces/ocirecipebuild.py b/lib/lp/oci/interfaces/ocirecipebuild.py |
13 | index 5ee6b46..95af3f6 100644 |
14 | --- a/lib/lp/oci/interfaces/ocirecipebuild.py |
15 | +++ b/lib/lp/oci/interfaces/ocirecipebuild.py |
16 | @@ -7,13 +7,14 @@ from __future__ import absolute_import, print_function, unicode_literals |
17 | |
18 | __metaclass__ = type |
19 | __all__ = [ |
20 | + 'IOCIFile', |
21 | 'IOCIRecipeBuild', |
22 | 'IOCIRecipeBuildSet', |
23 | ] |
24 | |
25 | from lazr.restful.fields import Reference |
26 | from zope.interface import Interface |
27 | -from zope.schema import Text |
28 | +from zope.schema import TextLine |
29 | |
30 | from lp import _ |
31 | from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource |
32 | @@ -21,11 +22,20 @@ from lp.buildmaster.interfaces.packagebuild import IPackageBuild |
33 | from lp.oci.interfaces.ocirecipe import IOCIRecipe |
34 | from lp.services.database.constants import DEFAULT |
35 | from lp.services.fields import PublicPersonChoice |
36 | +from lp.services.librarian.interfaces import ILibraryFileAlias |
37 | |
38 | |
39 | class IOCIRecipeBuildEdit(Interface): |
40 | + |
41 | # XXX twom 2020-02-10 This will probably need cancel() implementing |
42 | - pass |
43 | + |
44 | + def addFile(lfa, layer_file_digest): |
45 | + """Add an OCI file to this build. |
46 | + |
47 | + :param lfa: An `ILibraryFileAlias`. |
48 | + :param layer_file_digest: Digest for this file, used for image layers. |
49 | + :return: An `IOCILayerFile`. |
50 | + """ |
51 | |
52 | |
53 | class IOCIRecipeBuildView(IPackageBuild): |
54 | @@ -41,6 +51,21 @@ class IOCIRecipeBuildView(IPackageBuild): |
55 | required=True, |
56 | readonly=True) |
57 | |
58 | + def getByFileName(): |
59 | + """Retrieve a file by filename |
60 | + |
61 | + :return: A result set of (`IOCIFile`, `ILibraryFileAlias`, |
62 | + `ILibraryFileContent`). |
63 | + """ |
64 | + |
65 | + def getLayerFileByDigest(layer_file_digest): |
66 | + """Retrieve a layer file by the digest. |
67 | + |
68 | + :param layer_file_digest: The digest to look up. |
69 | + :raises NotFoundError: if no file exists with the given digest. |
70 | + :return: The corresponding `ILibraryFileAlias`. |
71 | + """ |
72 | + |
73 | |
74 | class IOCIRecipeBuildAdmin(Interface): |
75 | # XXX twom 2020-02-10 This will probably need rescore() implementing |
76 | @@ -61,3 +86,23 @@ class IOCIRecipeBuildSet(ISpecificBuildFarmJobSource): |
77 | |
78 | def preloadBuildsData(builds): |
79 | """Load the data related to a list of OCI recipe builds.""" |
80 | + |
81 | + |
82 | +class IOCIFile(Interface): |
83 | + """A link between an OCI recipe build and a file in the librarian.""" |
84 | + |
85 | + build = Reference( |
86 | + IOCIRecipeBuild, |
87 | + title=_("The OCI recipe build producing this file."), |
88 | + required=True, readonly=True) |
89 | + |
90 | + library_file = Reference( |
91 | + ILibraryFileAlias, title=_("A file in the librarian."), |
92 | + required=True, readonly=True) |
93 | + |
94 | + layer_file_digest = TextLine( |
95 | + title=_("Content-addressable hash of the file''s contents, " |
96 | + "used for reassembling image layers when pushing " |
97 | + "a build to a registry. This hash is in an opaque format " |
98 | + "generated by the OCI build tool."), |
99 | + required=False, readonly=True) |
100 | diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py |
101 | index 0d4d20f..8a01b2d 100644 |
102 | --- a/lib/lp/oci/model/ocirecipe.py |
103 | +++ b/lib/lp/oci/model/ocirecipe.py |
104 | @@ -30,11 +30,14 @@ from storm.locals import ( |
105 | from zope.component import getUtility |
106 | from zope.event import notify |
107 | from zope.interface import implementer |
108 | +from zope.security.proxy import removeSecurityProxy |
109 | |
110 | from lp.buildmaster.enums import BuildStatus |
111 | from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet |
112 | from lp.buildmaster.model.buildfarmjob import BuildFarmJob |
113 | from lp.buildmaster.model.buildqueue import BuildQueue |
114 | +from lp.code.model.gitcollection import GenericGitCollection |
115 | +from lp.code.model.gitrepository import GitRepository |
116 | from lp.oci.interfaces.ocirecipe import ( |
117 | DuplicateOCIRecipeName, |
118 | IOCIRecipe, |
119 | @@ -46,6 +49,8 @@ from lp.oci.interfaces.ocirecipe import ( |
120 | ) |
121 | from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet |
122 | from lp.oci.model.ocirecipebuild import OCIRecipeBuild |
123 | +from lp.registry.interfaces.person import IPersonSet |
124 | +from lp.services.database.bulk import load_related |
125 | from lp.services.database.constants import DEFAULT |
126 | from lp.services.database.decoratedresultset import DecoratedResultSet |
127 | from lp.services.database.interfaces import ( |
128 | @@ -293,3 +298,15 @@ class OCIRecipeSet: |
129 | def findByOwner(self, owner): |
130 | """See `IOCIRecipe`.""" |
131 | return IStore(OCIRecipe).find(OCIRecipe, OCIRecipe.owner == owner) |
132 | + |
133 | + def preloadDataForOCIRecipes(self, recipes, user=None): |
134 | + """See `IOCIRecipeSet`.""" |
135 | + recipes = [removeSecurityProxy(recipe) for recipe in recipes] |
136 | + |
137 | + person_ids = set() |
138 | + for recipe in recipes: |
139 | + person_ids.add(recipe.registrant_id) |
140 | + person_ids.add(recipe.owner_id) |
141 | + |
142 | + list(getUtility(IPersonSet).getPrecachedPersonsFromIDs( |
143 | + person_ids, need_validity=True)) |
144 | diff --git a/lib/lp/oci/model/ocirecipebuild.py b/lib/lp/oci/model/ocirecipebuild.py |
145 | index d069800..307fbd2 100644 |
146 | --- a/lib/lp/oci/model/ocirecipebuild.py |
147 | +++ b/lib/lp/oci/model/ocirecipebuild.py |
148 | @@ -7,6 +7,7 @@ from __future__ import absolute_import, print_function, unicode_literals |
149 | |
150 | __metaclass__ = type |
151 | __all__ = [ |
152 | + 'OCIFile', |
153 | 'OCIRecipeBuild', |
154 | 'OCIRecipeBuildSet', |
155 | ] |
156 | @@ -28,6 +29,7 @@ from storm.store import EmptyResultSet |
157 | from zope.component import getUtility |
158 | from zope.interface import implementer |
159 | |
160 | +from lp.app.errors import NotFoundError |
161 | from lp.buildmaster.enums import ( |
162 | BuildFarmJobType, |
163 | BuildStatus, |
164 | @@ -35,10 +37,14 @@ from lp.buildmaster.enums import ( |
165 | from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource |
166 | from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin |
167 | from lp.buildmaster.model.packagebuild import PackageBuildMixin |
168 | +from lp.oci.interfaces.ocirecipe import IOCIRecipeSet |
169 | from lp.oci.interfaces.ocirecipebuild import ( |
170 | + IOCIFile, |
171 | IOCIRecipeBuild, |
172 | IOCIRecipeBuildSet, |
173 | ) |
174 | +from lp.registry.model.person import Person |
175 | +from lp.services.database.bulk import load_related |
176 | from lp.services.database.constants import DEFAULT |
177 | from lp.services.database.decoratedresultset import DecoratedResultSet |
178 | from lp.services.database.enumcol import DBEnum |
179 | @@ -46,6 +52,33 @@ from lp.services.database.interfaces import ( |
180 | IMasterStore, |
181 | IStore, |
182 | ) |
183 | +from lp.services.librarian.model import ( |
184 | + LibraryFileAlias, |
185 | + LibraryFileContent, |
186 | + ) |
187 | + |
188 | + |
189 | +@implementer(IOCIFile) |
190 | +class OCIFile(Storm): |
191 | + |
192 | + __storm_table__ = 'OCIFile' |
193 | + |
194 | + id = Int(name='id', primary=True) |
195 | + |
196 | + build_id = Int(name='build', allow_none=False) |
197 | + build = Reference(build_id, 'OCIRecipeBuild.id') |
198 | + |
199 | + library_file_id = Int(name='library_file', allow_none=False) |
200 | + library_file = Reference(library_file_id, 'LibraryFileAlias.id') |
201 | + |
202 | + layer_file_digest = Unicode(name='layer_file_digest', allow_none=True) |
203 | + |
204 | + def __init__(self, build, library_file, layer_file_digest=None): |
205 | + """Construct a `OCIFile`.""" |
206 | + super(OCIFile, self).__init__() |
207 | + self.build = build |
208 | + self.library_file = library_file |
209 | + self.layer_file_digest = layer_file_digest |
210 | |
211 | |
212 | @implementer(IOCIRecipeBuild) |
213 | @@ -93,6 +126,11 @@ class OCIRecipeBuild(PackageBuildMixin, Storm): |
214 | build_farm_job_id = Int(name='build_farm_job', allow_none=False) |
215 | build_farm_job = Reference(build_farm_job_id, 'BuildFarmJob.id') |
216 | |
217 | + # Stub attributes to match the IPackageBuild interface that we |
218 | + # are not using in this implementation at this time. |
219 | + pocket = None |
220 | + distro_series = None |
221 | + |
222 | def __init__(self, build_farm_job, requester, recipe, |
223 | processor, virtualized, date_created): |
224 | |
225 | @@ -130,6 +168,34 @@ class OCIRecipeBuild(PackageBuildMixin, Storm): |
226 | durations.sort() |
227 | return durations[len(durations) // 2] |
228 | |
229 | + def getByFileName(self, filename): |
230 | + result = Store.of(self).find( |
231 | + (OCIFile, LibraryFileAlias, LibraryFileContent), |
232 | + OCIFile.build == self.id, |
233 | + LibraryFileAlias.id == OCIFile.library_file_id, |
234 | + LibraryFileContent.id == LibraryFileAlias.contentID, |
235 | + LibraryFileAlias.filename == filename).one() |
236 | + if result is not None: |
237 | + return result |
238 | + raise NotFoundError(filename) |
239 | + |
240 | + def getLayerFileByDigest(self, layer_file_digest): |
241 | + file_object = Store.of(self).find( |
242 | + (OCIFile, LibraryFileAlias, LibraryFileContent), |
243 | + OCIFile.build == self.id, |
244 | + LibraryFileAlias.id == OCIFile.library_file_id, |
245 | + LibraryFileContent.id == LibraryFileAlias.contentID, |
246 | + OCIFile.layer_file_digest == layer_file_digest).one() |
247 | + if file_object is not None: |
248 | + return file_object |
249 | + raise NotFoundError(layer_file_digest) |
250 | + |
251 | + def addFile(self, lfa, layer_file_digest=None): |
252 | + oci_file = OCIFile( |
253 | + build=self, library_file=lfa, layer_file_digest=layer_file_digest) |
254 | + IMasterStore(OCIFile).add(oci_file) |
255 | + return oci_file |
256 | + |
257 | @property |
258 | def archive(self): |
259 | # XXX twom 2019-12-05 This may need to change when an OCIProject |
260 | @@ -142,11 +208,6 @@ class OCIRecipeBuild(PackageBuildMixin, Storm): |
261 | # pillar isn't just a distribution |
262 | return self.recipe.oci_project.distribution |
263 | |
264 | - # Stub attributes to match the IPackageBuild interface that we |
265 | - # will not use in this implementation. |
266 | - pocket = None |
267 | - distro_series = None |
268 | - |
269 | |
270 | @implementer(IOCIRecipeBuildSet) |
271 | class OCIRecipeBuildSet(SpecificBuildFarmJobSourceMixin): |
272 | @@ -171,7 +232,15 @@ class OCIRecipeBuildSet(SpecificBuildFarmJobSourceMixin): |
273 | |
274 | def preloadBuildsData(self, builds): |
275 | """See `IOCIRecipeBuildSet`.""" |
276 | - # XXX twom 2019-12-02 Currently a no-op skeleton, to be filled in |
277 | + # Circular import. |
278 | + from lp.oci.model.ocirecipe import OCIRecipe |
279 | + load_related(Person, builds, ["requester_id"]) |
280 | + lfas = load_related(LibraryFileAlias, builds, ["log_id"]) |
281 | + load_related(LibraryFileContent, lfas, ["contentID"]) |
282 | + recipes = load_related(OCIRecipe, builds, ["recipe_id"]) |
283 | + getUtility(IOCIRecipeSet).preloadDataForOCIRecipes(recipes) |
284 | + # XXX twom 2019-12-05 This needs to be extended to include |
285 | + # OCIRecipeBuildJob when that exists. |
286 | return |
287 | |
288 | def getByID(self, build_id): |
289 | diff --git a/lib/lp/oci/tests/test_ocirecipebuild.py b/lib/lp/oci/tests/test_ocirecipebuild.py |
290 | new file mode 100644 |
291 | index 0000000..3d51de2 |
292 | --- /dev/null |
293 | +++ b/lib/lp/oci/tests/test_ocirecipebuild.py |
294 | @@ -0,0 +1,168 @@ |
295 | +# Copyright 2019 Canonical Ltd. This software is licensed under the |
296 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
297 | + |
298 | +"""Tests for OCI image building recipe functionality.""" |
299 | + |
300 | +from __future__ import absolute_import, print_function, unicode_literals |
301 | + |
302 | +from datetime import timedelta |
303 | + |
304 | +import six |
305 | +from zope.component import getUtility |
306 | +from zope.security.proxy import removeSecurityProxy |
307 | + |
308 | +from lp.app.errors import NotFoundError |
309 | +from lp.buildmaster.enums import BuildStatus |
310 | +from lp.buildmaster.interfaces.buildqueue import IBuildQueue |
311 | +from lp.buildmaster.interfaces.packagebuild import IPackageBuild |
312 | +from lp.oci.interfaces.ocirecipebuild import ( |
313 | + IOCIRecipeBuild, |
314 | + IOCIRecipeBuildSet, |
315 | + ) |
316 | +from lp.oci.model.ocirecipebuild import OCIRecipeBuildSet |
317 | +from lp.testing import ( |
318 | + admin_logged_in, |
319 | + TestCaseWithFactory, |
320 | + ) |
321 | +from lp.testing.layers import ( |
322 | + DatabaseFunctionalLayer, |
323 | + LaunchpadZopelessLayer, |
324 | + ) |
325 | + |
326 | + |
327 | +class TestOCIRecipeBuild(TestCaseWithFactory): |
328 | + |
329 | + layer = LaunchpadZopelessLayer |
330 | + |
331 | + def setUp(self): |
332 | + super(TestOCIRecipeBuild, self).setUp() |
333 | + self.build = self.factory.makeOCIRecipeBuild() |
334 | + |
335 | + def test_implements_interface(self): |
336 | + with admin_logged_in(): |
337 | + self.assertProvides(self.build, IOCIRecipeBuild) |
338 | + self.assertProvides(self.build, IPackageBuild) |
339 | + |
340 | + def test_addFile(self): |
341 | + lfa = self.factory.makeLibraryFileAlias() |
342 | + self.build.addFile(lfa) |
343 | + _, result_lfa, _ = self.build.getByFileName(lfa.filename) |
344 | + self.assertEqual(result_lfa, lfa) |
345 | + |
346 | + def test_getByFileName(self): |
347 | + files = [self.factory.makeOCIFile(build=self.build) for x in range(3)] |
348 | + result, _, _ = self.build.getByFileName( |
349 | + files[0].library_file.filename) |
350 | + self.assertEqual(result, files[0]) |
351 | + |
352 | + def test_getByFileName_missing(self): |
353 | + self.assertRaises( |
354 | + NotFoundError, |
355 | + self.build.getByFileName, |
356 | + "missing") |
357 | + |
358 | + def test_getLayerFileByDigest(self): |
359 | + files = [self.factory.makeOCIFile( |
360 | + build=self.build, layer_file_digest=six.text_type(x)) |
361 | + for x in range(3)] |
362 | + result, _, _ = self.build.getLayerFileByDigest( |
363 | + files[0].layer_file_digest) |
364 | + self.assertEqual(result, files[0]) |
365 | + |
366 | + def test_getLayerFileByDigest_missing(self): |
367 | + [self.factory.makeOCIFile( |
368 | + build=self.build, layer_file_digest=six.text_type(x)) |
369 | + for x in range(3)] |
370 | + self.assertRaises( |
371 | + NotFoundError, |
372 | + self.build.getLayerFileByDigest, |
373 | + 'missing') |
374 | + |
375 | + def test_estimateDuration(self): |
376 | + # Without previous builds, the default time estimate is 30m. |
377 | + self.assertEqual(1800, self.build.estimateDuration().seconds) |
378 | + |
379 | + def test_estimateDuration_with_history(self): |
380 | + # Previous successful builds of the same OCI recipe are used for |
381 | + # estimates. |
382 | + oci_build = self.factory.makeOCIRecipeBuild( |
383 | + status=BuildStatus.FULLYBUILT, duration=timedelta(seconds=335)) |
384 | + for i in range(3): |
385 | + self.factory.makeOCIRecipeBuild( |
386 | + requester=oci_build.requester, recipe=oci_build.recipe, |
387 | + status=BuildStatus.FAILEDTOBUILD, |
388 | + duration=timedelta(seconds=20)) |
389 | + self.assertEqual(335, oci_build.estimateDuration().seconds) |
390 | + |
391 | + def test_queueBuild(self): |
392 | + # OCIRecipeBuild can create the queue entry for itself. |
393 | + bq = self.build.queueBuild() |
394 | + self.assertProvides(bq, IBuildQueue) |
395 | + self.assertEqual( |
396 | + self.build.build_farm_job, removeSecurityProxy(bq)._build_farm_job) |
397 | + self.assertEqual(self.build, bq.specific_build) |
398 | + self.assertEqual(self.build.virtualized, bq.virtualized) |
399 | + self.assertIsNotNone(bq.processor) |
400 | + self.assertEqual(bq, self.build.buildqueue_record) |
401 | + |
402 | + |
403 | +class TestOCIRecipeBuildSet(TestCaseWithFactory): |
404 | + |
405 | + layer = DatabaseFunctionalLayer |
406 | + |
407 | + def test_implements_interface(self): |
408 | + target = OCIRecipeBuildSet() |
409 | + with admin_logged_in(): |
410 | + self.assertProvides(target, IOCIRecipeBuildSet) |
411 | + |
412 | + def test_new(self): |
413 | + requester = self.factory.makePerson() |
414 | + recipe = self.factory.makeOCIRecipe() |
415 | + distro_arch_series = self.factory.makeDistroArchSeries() |
416 | + target = getUtility(IOCIRecipeBuildSet).new( |
417 | + requester, recipe, distro_arch_series) |
418 | + with admin_logged_in(): |
419 | + self.assertProvides(target, IOCIRecipeBuild) |
420 | + |
421 | + def test_getByID(self): |
422 | + builds = [self.factory.makeOCIRecipeBuild() for x in range(3)] |
423 | + result = getUtility(IOCIRecipeBuildSet).getByID(builds[1].id) |
424 | + self.assertEqual(result, builds[1]) |
425 | + |
426 | + def test_getByBuildFarmJob(self): |
427 | + builds = [self.factory.makeOCIRecipeBuild() for x in range(3)] |
428 | + result = getUtility(IOCIRecipeBuildSet).getByBuildFarmJob( |
429 | + builds[1].build_farm_job) |
430 | + self.assertEqual(result, builds[1]) |
431 | + |
432 | + def test_getByBuildFarmJobs(self): |
433 | + builds = [self.factory.makeOCIRecipeBuild() for x in range(3)] |
434 | + self.assertContentEqual( |
435 | + builds, |
436 | + getUtility(IOCIRecipeBuildSet).getByBuildFarmJobs( |
437 | + [build.build_farm_job for build in builds])) |
438 | + |
439 | + def test_getByBuildFarmJobs_empty(self): |
440 | + self.assertContentEqual( |
441 | + [], getUtility(IOCIRecipeBuildSet).getByBuildFarmJobs([])) |
442 | + |
443 | + def test_virtualized_recipe_requires(self): |
444 | + recipe = self.factory.makeOCIRecipe(require_virtualized=True) |
445 | + target = self.factory.makeOCIRecipeBuild(recipe=recipe) |
446 | + self.assertTrue(target.virtualized) |
447 | + |
448 | + def test_virtualized_processor_requires(self): |
449 | + distro_arch_series = self.factory.makeDistroArchSeries() |
450 | + distro_arch_series.processor.supports_nonvirtualized = False |
451 | + recipe = self.factory.makeOCIRecipe(require_virtualized=False) |
452 | + target = self.factory.makeOCIRecipeBuild( |
453 | + distro_arch_series=distro_arch_series, recipe=recipe) |
454 | + self.assertTrue(target.virtualized) |
455 | + |
456 | + def test_virtualized_no_support(self): |
457 | + recipe = self.factory.makeOCIRecipe(require_virtualized=False) |
458 | + distro_arch_series = self.factory.makeDistroArchSeries() |
459 | + distro_arch_series.processor.supports_nonvirtualized = True |
460 | + target = self.factory.makeOCIRecipeBuild( |
461 | + recipe=recipe, distro_arch_series=distro_arch_series) |
462 | + self.assertFalse(target.virtualized) |
463 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
464 | index d0ecd18..f0788d8 100644 |
465 | --- a/lib/lp/testing/factory.py |
466 | +++ b/lib/lp/testing/factory.py |
467 | @@ -160,6 +160,7 @@ from lp.hardwaredb.interfaces.hwdb import ( |
468 | from lp.oci.interfaces.ocirecipe import IOCIRecipeSet |
469 | from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet |
470 | from lp.oci.model.ocirecipe import OCIRecipeArch |
471 | +from lp.oci.model.ocirecipebuild import OCIFile |
472 | from lp.registry.enums import ( |
473 | BranchSharingPolicy, |
474 | BugSharingPolicy, |
475 | @@ -4979,7 +4980,9 @@ class BareLaunchpadObjectFactory(ObjectFactory): |
476 | return OCIRecipeArch(recipe, processor) |
477 | |
478 | def makeOCIRecipeBuild(self, requester=None, recipe=None, |
479 | - distro_arch_series=None, date_created=DEFAULT): |
480 | + distro_arch_series=None, date_created=DEFAULT, |
481 | + status=BuildStatus.NEEDSBUILD, builder=None, |
482 | + duration=None): |
483 | """Make a new OCIRecipeBuild.""" |
484 | if requester is None: |
485 | requester = self.makePerson() |
486 | @@ -4987,9 +4990,29 @@ class BareLaunchpadObjectFactory(ObjectFactory): |
487 | distro_arch_series = self.makeDistroArchSeries() |
488 | if recipe is None: |
489 | recipe = self.makeOCIRecipe() |
490 | - |
491 | - return getUtility(IOCIRecipeBuildSet).new( |
492 | + oci_build = getUtility(IOCIRecipeBuildSet).new( |
493 | requester, recipe, distro_arch_series, date_created) |
494 | + if duration is not None: |
495 | + removeSecurityProxy(oci_build).updateStatus( |
496 | + BuildStatus.BUILDING, builder=builder, |
497 | + date_started=oci_build.date_created) |
498 | + removeSecurityProxy(oci_build).updateStatus( |
499 | + status, builder=builder, |
500 | + date_finished=oci_build.date_started + duration) |
501 | + else: |
502 | + removeSecurityProxy(oci_build).updateStatus( |
503 | + status, builder=builder) |
504 | + return oci_build |
505 | + |
506 | + def makeOCIFile(self, build=None, library_file=None, |
507 | + layer_file_digest=None): |
508 | + """Make a new OCIFile.""" |
509 | + if build is None: |
510 | + build = self.makeOCIRecipeBuild() |
511 | + if library_file is None: |
512 | + library_file = self.makeLibraryFileAlias() |
513 | + return OCIFile(build=build, library_file=library_file, |
514 | + layer_file_digest=layer_file_digest) |
515 | |
516 | |
517 | # Some factory methods return simple Python types. We don't add |