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