Merge lp:~robru/bileto/publish-job into lp:bileto
- publish-job
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 680 |
Proposed branch: | lp:~robru/bileto/publish-job |
Merge into: | lp:bileto |
Diff against target: |
940 lines (+459/-54) 17 files modified
Makefile (+3/-1) bileto/actions.py (+6/-2) bileto/static/index.html (+1/-1) bileto/streams.py (+3/-0) bileto/templates/loglist.html (+3/-0) bileto/worker/manager.py (+33/-7) bileto/worker/manual.py (+11/-0) bileto/worker/merge.py (+10/-2) bileto/worker/package.py (+124/-14) files/rsyncd.conf (+7/-0) scripts/vcs.sh (+5/-7) tests/test_actions.py (+3/-8) tests/test_streams.py (+18/-0) tests/test_worker_manager.py (+71/-11) tests/test_worker_manual.py (+28/-0) tests/test_worker_merge.py (+11/-1) tests/test_worker_package.py (+122/-0) |
To merge this branch: | bzr merge lp:~robru/bileto/publish-job |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robert Bruce Park (community) | Approve | ||
Martin Pitt | Needs Fixing | ||
Review via email: mp+302105@code.launchpad.net |
Commit message
Implement publish job in Bileto, obsoleting jenkins entirely.
Description of the change
(note this depends on charm changes and must be deployed with "mojo upgrade-charm")
- 703. By Robert Bruce Park
-
Temporarily disable some checks for testing.
- 704. By Robert Bruce Park
-
Temporarily disable some more checks for testing.
- 705. By Robert Bruce Park
-
Temporarily disable even more checks for testing.
- 706. By Robert Bruce Park
-
Fix publishing.
- 707. By Robert Bruce Park
-
Re-enable all checks.
Robert Bruce Park (robru) wrote : | # |
Replies inline,thanks Martin
Martin Pitt (pitti) wrote : | # |
Answered to your replies, thanks!
The flashplugin-nonfree component issue is still a blocker. The rsync race looks like an actual bug, but not that important (so not a blocker). Do with the code style/obfuscation comments what you want.
Robert Bruce Park (robru) : | # |
- 708. By Robert Bruce Park
-
Rename __getattr__ to call_on_
all_packages for pitti. - 709. By Robert Bruce Park
-
Ensure os.rename() stays within filesystem.
- 710. By Robert Bruce Park
-
Clarify docstring.
Robert Bruce Park (robru) wrote : | # |
Ok I've pushed commits to resolve the issues I agree with, I'm choosing not to block on the two things that are historical cruft that I've inherited (flashplugin-
- 711. By Robert Bruce Park
-
Fix tests.
Martin Pitt (pitti) wrote : | # |
Robert Bruce Park [2016-08-22 16:06 -0000]:
> Can you elaborate what you mean by "recursively call the method on its direct children"?
Well, I don't know what this is doing, but you said that __getattr__()
has some magic to do that kind of iteration.
> The reason that __getattr__ is defined to pass methods down to
> children is that this is called from dozens of places (and many of
> those cases it isn't overloaded at all, it's just a direct pass
> through), so I'm not going to reimplement that in every single place
> that needs to pass through. I'll probably rename __getattr__() to
> call_on_
Doing "__getattr__ = call_on_
calls with the other makes things even *more*
confusing/
consistently.
> There is an rsync server running continuously on the bileto
> instance, and snakefruit polls it every 5 minutes looking for a new
> file to read. Bileto doesn't "start" rsync.
So I suggest using a lock file (in both the rsync process and bileto)
to ensure to not run rsync while bileto is updating that file tree.
Another common pattern is to do something like this:
cp -al cur_tree new_tree
<change new_tree>
mv cur_tree old_tree; mv new_tree cur_tree
You can't atomically rename an existing (non-empty) directory, but at
least this is fairly quick and thus will work in almost every case.
However, lock files are cheaper, simpler, and completely robust.
> As I said this was decided by infinity and I'm not sure what would be correct.
Please ask him again what this should mean and add a big fat comment
about it. The concept of "upload flashplugin-nonfree into universe"
simply does not make sense -- asking questions about it might be some
weird LP quirk, but it should at least be documented. You yourself
don't even know what it does any more :-)
Robert Bruce Park (robru) wrote : | # |
On Aug 22, 2016 10:15 PM, "Martin Pitt" <email address hidden> wrote:
>
> Robert Bruce Park [2016-08-22 16:06 -0000]:
> > Can you elaborate what you mean by "recursively call the method on its
direct children"?
>
> Well, I don't know what this is doing, but you said that __getattr__()
> has some magic to do that kind of iteration.
So a ticket can have many packages. When you "build a ticket" you want to
build each package. The manager class exists to ensure the classes
representing the packages are correctly instantiated and then passes
messages back and forth. So when you call manager.build() you're really
calling .build() on every package instance. Manager.
the logic for calling the method, in parallel, on each instance.
In some cases there's some "global" logic that operates once on the whole
ticket rather than per package, and that's where you see those manager
methods that call __getattr__, they're processing the return values that
came back from each package, etc. But there are other cases where you might
call "manager.foo()" and foo doesn't exist at all, it goes straight to
package.foo() on each package. The idea was that the manager is a sort of
transparent wrapper around the package instances, you can call package
methods on it and it passes the calls down to each instance. Eg, it's an
explicit design goal that you would often call methods on the manager
instance that do not exist, which is why __getattr__ is defined. It's just
unfortunate that so many methods require a bit of intervention before /
after the __getattr__ logic that you see so many methods calling
__gettattr__ with their own method name.
>
> > The reason that __getattr__ is defined to pass methods down to
> > children is that this is called from dozens of places (and many of
> > those cases it isn't overloaded at all, it's just a direct pass
> > through), so I'm not going to reimplement that in every single place
> > that needs to pass through. I'll probably rename __getattr__() to
> > call_on_
>
> Doing "__getattr__ = call_on_
> calls with the other makes things even *more*
> confusing/
> consistently.
What do you mean replacing half the calls? Nothing is calling
"self._
is kept for the cases when a nonexistent method is called on the manager
class. That has to stay because they chain together, eg, you can call
"manager.
Getting rid of __getattr__ would result in code like
"manager.
redundant slop.
>
> > There is an rsync server running continuously on the bileto
> > instance, and snakefruit polls it every 5 minutes looking for a new
> > file to read. Bileto doesn't "start" rsync.
>
> So I suggest using a lock file (in both the rsync process and bileto)
> to ensure to not run rsync while bileto is updating that file tree.
> Another common pattern is to do something like this:
>
> cp -al cur_tree new_tree
> <change new_tree>
> mv cur_t...
Preview Diff
1 | === modified file 'Makefile' |
2 | --- Makefile 2016-07-11 21:41:48 +0000 |
3 | +++ Makefile 2016-08-22 16:49:27 +0000 |
4 | @@ -31,8 +31,10 @@ |
5 | |
6 | install: install-common-deps |
7 | # Need bzr-builddeb for merging debian/changelogs. |
8 | - $(apt) uuid tree devscripts patchutils diffstat quilt libdistro-info-perl fakeroot dput python3-yaml python3-amqplib python3-psycopg2 python3-gevent gunicorn3 supervisor postgresql-client python-swiftclient bzr-builddeb |
9 | + $(apt) rsync uuid tree devscripts patchutils diffstat quilt libdistro-info-perl fakeroot dput python3-yaml python3-amqplib python3-psycopg2 python3-gevent gunicorn3 supervisor postgresql-client python-swiftclient bzr-builddeb |
10 | $(envsubst) <files/supervisor.conf >/etc/supervisor/conf.d/bileto.conf |
11 | + cp files/rsyncd.conf /etc/rsync-juju.d/020-bileto.conf |
12 | + service rsync restart |
13 | # Branch branches if necessary |
14 | [ -d "$(britney_path)" ] || git clone $(britney_branch) $(britney_path) |
15 | [ -d "$(britney_hints_path)" ] || bzr branch $(britney_hints) $(britney_hints_path) |
16 | |
17 | === modified file 'bileto/actions.py' |
18 | --- bileto/actions.py 2016-07-08 17:57:20 +0000 |
19 | +++ bileto/actions.py 2016-08-22 16:49:27 +0000 |
20 | @@ -15,6 +15,11 @@ |
21 | finalize='Merging to trunk.', |
22 | abandon='Abandoning ticket.', |
23 | ) |
24 | + options = dict( |
25 | + publish=dict( |
26 | + ACK_PACKAGING='Check this option if you have reviewed and ' |
27 | + 'approved all the packaging diffs for this ticket.'), |
28 | + ) |
29 | |
30 | @staticmethod |
31 | def status(manager): |
32 | @@ -39,8 +44,7 @@ |
33 | @staticmethod |
34 | def publish(manager): |
35 | """Copy Packages to Destination Archive""" |
36 | - manager.checkstatus().validate().checkupload().ackaging() |
37 | - manager.checkdest().unapproved().unbuilt().publish() |
38 | + manager.validate().prepublish().publish() |
39 | |
40 | @staticmethod |
41 | def finalize(manager): |
42 | |
43 | === modified file 'bileto/static/index.html' |
44 | --- bileto/static/index.html 2016-08-04 17:07:10 +0000 |
45 | +++ bileto/static/index.html 2016-08-22 16:49:27 +0000 |
46 | @@ -229,7 +229,7 @@ |
47 | <td ng-if="!editing"><a href="/log/{{req.request_id}}/build" target="_blank">Build</a></td> |
48 | <td ng-if="req.siloname && !editing"><a href="/log/{{req.request_id}}/diff" target="_blank">Diff</a></td> |
49 | <td ng-if="!editing"><a href="/log/{{req.request_id}}/revert" target="_blank">Revert</a></td> |
50 | -<td ng-if="req.siloname && !editing"><a href="{{meta.jenkins}}/job/{{req.siloname.replace('/', '-')}}-2-publish/build" target="_blank">Publish</a></td> |
51 | +<td ng-if="req.siloname && !editing"><a href="/log/{{req.request_id}}/publish" target="_blank">Publish</a></td> |
52 | <td ng-if="!editing"><a href="/log/{{req.request_id}}/finalize" target="_blank">Finalize</a></td> |
53 | <td ng-if="!editing"><a href="/log/{{req.request_id}}/abandon" target="_blank">Abandon</a></td> |
54 | <td ng-if="editing"><a ng-click="cancel()">Cancel Edit</a></td> |
55 | |
56 | === modified file 'bileto/streams.py' |
57 | --- bileto/streams.py 2016-08-16 16:36:52 +0000 |
58 | +++ bileto/streams.py 2016-08-22 16:49:27 +0000 |
59 | @@ -127,6 +127,7 @@ |
60 | post = join(request.path, 'latest/') |
61 | packages = list_packages(action, ticket) |
62 | message = getattr(Actions, action).__doc__ |
63 | + options = Actions.options.get(action, {}) |
64 | leaf = 'success_*False' if failures else 'info' |
65 | paths = glob(join(Config.HOME, 'log', str(ticket), action, '[0-9]*', leaf)) |
66 | meta = dict() |
67 | @@ -156,6 +157,8 @@ |
68 | REQUESTID=str(ticket), |
69 | TEAMS=COMMA.join(session.get('teams', [])), |
70 | ) |
71 | + for key in Actions.options.get(action, {}): |
72 | + env[key] = str(request.form.get(key)) |
73 | path = join(Config.ROOT, 'bileto', 'worker', 'main.py') |
74 | Popen((path,), env=env) |
75 | sleep(5) |
76 | |
77 | === modified file 'bileto/templates/loglist.html' |
78 | --- bileto/templates/loglist.html 2016-07-30 00:09:36 +0000 |
79 | +++ bileto/templates/loglist.html 2016-08-22 16:49:27 +0000 |
80 | @@ -8,6 +8,9 @@ |
81 | <div id="trigger"> |
82 | {% if session.nickname %} |
83 | <form action="{{ post }}" method="post"> |
84 | +{% for option, description in options.items() %} |
85 | +<p><label><input type="checkbox" name="{{ option }}" value="{{ option }}"><b>{{ option }}:</b> {{ description }}</label></p> |
86 | +{% endfor %} |
87 | {% if packages %} |
88 | <p>Include these packages (leave blank to build all):</p> |
89 | <p> |
90 | |
91 | === modified file 'bileto/worker/manager.py' |
92 | --- bileto/worker/manager.py 2016-08-17 20:59:59 +0000 |
93 | +++ bileto/worker/manager.py 2016-08-22 16:49:27 +0000 |
94 | @@ -12,7 +12,7 @@ |
95 | |
96 | import re |
97 | |
98 | -from os import environ |
99 | +from os import environ, makedirs, rename |
100 | from os.path import getmtime, join |
101 | |
102 | from time import time |
103 | @@ -57,6 +57,7 @@ |
104 | SLASH = '/' |
105 | SPACE = ' ' |
106 | EMPTY = '' |
107 | +DASH = '-' |
108 | PLUS = '+' |
109 | N = '\n' |
110 | |
111 | @@ -205,8 +206,8 @@ |
112 | pretty_instances(Package.instances.items()) |
113 | return Package.instances |
114 | |
115 | - def __getattr__(self, attr): |
116 | - """Pass method calls down to all instances.""" |
117 | + def call_on_all_packages(self, attr): |
118 | + """Pass method calls down to all Package instances.""" |
119 | def passthrough(*args, **kwargs): |
120 | """Pass arguments down to methods.""" |
121 | log.abort_if_cancelled() |
122 | @@ -216,6 +217,7 @@ |
123 | for name, package in self.packages().items()]) |
124 | return self |
125 | return passthrough |
126 | + __getattr__ = call_on_all_packages |
127 | |
128 | def assign(self): |
129 | """Assign a PPA to this ticket, if possible.""" |
130 | @@ -237,20 +239,20 @@ |
131 | raise BiletoError('Reverting MPs is not supported.') |
132 | self.manual_class = Revert |
133 | log.vars(self.types(), 'manager.types()') |
134 | - return self.__getattr__('collect')() |
135 | + return self.call_on_all_packages('collect')() |
136 | |
137 | def upload(self): |
138 | """Prevent cancellation once uploading begins.""" |
139 | log.cancellable = False |
140 | log.info('Point of no return! Job can no longer be cancelled.') |
141 | - return self.__getattr__('upload')() |
142 | + return self.call_on_all_packages('upload')() |
143 | |
144 | def diff(self): |
145 | """Force diffing of all packages.""" |
146 | environ.update(INCLUDES=self.ticket.sources) |
147 | for func in (Manager.names, Manager.types, Manager.packages): |
148 | func.cache_clear() |
149 | - self.__getattr__('diff')() |
150 | + self.call_on_all_packages('diff')() |
151 | set_swift_creds() |
152 | stdout = parallelize([VCS.swift_upload_()])[0] |
153 | log.info('Uploaded diffs:\n%s', stdout) |
154 | @@ -262,7 +264,7 @@ |
155 | assert_lockfile_age(self.ticket.lockfile) |
156 | lock = open('{}-status'.format(self.ticket.lockfile), 'w') |
157 | flock(lock, LOCK_EX | LOCK_NB) |
158 | - self.__getattr__('status')() |
159 | + self.call_on_all_packages('status')() |
160 | assert_lockfile_age(self.ticket.lockfile) |
161 | states = defaultdict(list) |
162 | versions = defaultdict(dict) |
163 | @@ -284,6 +286,30 @@ |
164 | Actions.finalize(self) |
165 | return self |
166 | |
167 | + def prepublish(self): |
168 | + """Abort publication if any safety checks failed.""" |
169 | + self.call_on_all_packages('prepublish')() |
170 | + msgs = defaultdict(list) |
171 | + for source_id, message in sorted(self.retvals): |
172 | + if message: |
173 | + msgs[message].append(source_id) |
174 | + if msgs: |
175 | + raise BiletoError('Publish failed: {}'.format(pretty_states(msgs))) |
176 | + return self |
177 | + |
178 | + def publish(self): |
179 | + """Write package manifest to disk.""" |
180 | + self.call_on_all_packages('publish')() |
181 | + filename = 'packagelist_rsync_{}'.format( |
182 | + self.ticket.siloname.replace(SLASH, DASH)) |
183 | + tmpfile = join(Config.HOME, filename) |
184 | + lines = sorted(Package.manifest) |
185 | + log.vars(lines, 'Manifest lines') |
186 | + with open(tmpfile, 'w', encoding='utf-8') as manifest: |
187 | + manifest.writelines(lines) |
188 | + makedirs(join(Config.HOME, 'publish'), exist_ok=True) |
189 | + rename(filename, join(Config.HOME, 'publish', filename)) |
190 | + |
191 | def nuke(self, status): |
192 | """Delete PPA contents.""" |
193 | # TODO: Eventually delete ephemeral PPAs here |
194 | |
195 | === modified file 'bileto/worker/manual.py' |
196 | --- bileto/worker/manual.py 2016-03-10 01:47:08 +0000 |
197 | +++ bileto/worker/manual.py 2016-08-22 16:49:27 +0000 |
198 | @@ -3,6 +3,8 @@ |
199 | Defines steps unique to manually-built source packages. |
200 | """ |
201 | |
202 | +from os import environ |
203 | + |
204 | from asyncio import coroutine |
205 | |
206 | from bileto.worker.package import Package |
207 | @@ -26,3 +28,12 @@ |
208 | @coroutine |
209 | def expose(self): |
210 | """NOP; Users will expose the source package themselves.""" |
211 | + |
212 | + @coroutine |
213 | + def check_package_ack(self, source=None): |
214 | + """User must **always** have upload rights for manual sources.""" |
215 | + if not self.authorized: |
216 | + return '{} not authorized to upload {}'.format( |
217 | + environ['BLAME'], self.name) |
218 | + result = yield from super().check_package_ack(source=source) |
219 | + return result |
220 | |
221 | === modified file 'bileto/worker/merge.py' |
222 | --- bileto/worker/merge.py 2016-07-28 20:10:13 +0000 |
223 | +++ bileto/worker/merge.py 2016-08-22 16:49:27 +0000 |
224 | @@ -98,6 +98,7 @@ |
225 | """Define the steps unique to packages built from merge proposals.""" |
226 | all_mps = {} |
227 | target = None |
228 | + acceptable = BUILDABLE_STATES |
229 | |
230 | @classmethod |
231 | def prepare_all(cls, urls): |
232 | @@ -163,14 +164,14 @@ |
233 | return 'PPA/bzr version mismatch' |
234 | |
235 | @coroutine |
236 | - def check_merge_states(self, source=None, acceptable=BUILDABLE_STATES): |
237 | + def check_merge_states(self, source=None): |
238 | """Check that merges are in acceptable states.""" |
239 | # (\/) (^,,,^) (\/) |
240 | your_merges_are_bad_and_you_should_feel_bad = False |
241 | for merge in self.merges: |
242 | url = getattr(merge, 'web_link', str(merge)) |
243 | state = getattr(merge, 'queue_status', 'Not even a merge') |
244 | - if state not in acceptable: |
245 | + if state not in self.acceptable: |
246 | log.error('%s: %s', state, url) |
247 | your_merges_are_bad_and_you_should_feel_bad = True |
248 | if your_merges_are_bad_and_you_should_feel_bad: |
249 | @@ -268,3 +269,10 @@ |
250 | def push(self): |
251 | """Push our final release to target trunk.""" |
252 | yield from VCS.push_to_trunk(self.target, **self.env) |
253 | + |
254 | + @coroutine |
255 | + def prepublish(self): |
256 | + """Change check_merge_states to consider what is publishable.""" |
257 | + self.acceptable = PUBLISHABLE_STATES |
258 | + result = yield from super().prepublish() |
259 | + return result |
260 | |
261 | === modified file 'bileto/worker/package.py' |
262 | --- bileto/worker/package.py 2016-08-15 16:38:33 +0000 |
263 | +++ bileto/worker/package.py 2016-08-22 16:49:27 +0000 |
264 | @@ -3,27 +3,45 @@ |
265 | Defines steps common to all types of packages that we handle. |
266 | """ |
267 | |
268 | +from os import environ |
269 | from os.path import join |
270 | + |
271 | from contextlib import suppress |
272 | from asyncio import coroutine, gather |
273 | |
274 | +from lazr.restfulclient.errors import ClientError |
275 | + |
276 | from bileto.settings import Config |
277 | from bileto.models import BUILD_FAILURES, BUILD_STATES |
278 | |
279 | from bileto.worker.vcs import VCS |
280 | from bileto.worker.log import log |
281 | +from bileto.worker.version import V |
282 | |
283 | +DIFF = '{}_{}_{{}}.diff' |
284 | SLASH = '/' |
285 | EMPTY = '' |
286 | +TAB = '\t' |
287 | N = '\n' |
288 | |
289 | |
290 | +@coroutine |
291 | +def run_checks(checks, *args, **kwargs): |
292 | + """Run checks and return first message found.""" |
293 | + for check in checks: |
294 | + message = yield from check(*args, **kwargs) |
295 | + if message: |
296 | + return message |
297 | + |
298 | + |
299 | # pylint: disable=too-many-instance-attributes |
300 | class Package: |
301 | """Represent the steps necessary to build & publish a package.""" |
302 | + authorized = None |
303 | artifacts = '' |
304 | published = None |
305 | instances = {} |
306 | + manifest = [] |
307 | |
308 | def __init__(self, name, distro, series, dest, ppa): |
309 | self.id = join(series.name, name) |
310 | @@ -32,9 +50,14 @@ |
311 | self.name = name |
312 | self.dest = dest |
313 | self.ppa = ppa |
314 | + diff = DIFF.format(series.name, name) |
315 | + self.content_diff = diff.format('content') |
316 | + self.packaging_diff = diff.format('packaging_changes') |
317 | self.env = dict( |
318 | SOURCE=name, |
319 | SERIES=series.name, |
320 | + CONTENT_DIFF=self.content_diff, |
321 | + PACKAGING_DIFF=self.packaging_diff, |
322 | ) |
323 | self.instances[self.id] = self |
324 | |
325 | @@ -52,10 +75,7 @@ |
326 | self.check_diff_exists, |
327 | self.check_destination_version, |
328 | ) |
329 | - for check in checks: |
330 | - message = yield from check(source=source) |
331 | - if message: |
332 | - break |
333 | + message = yield from run_checks(checks, source=source) |
334 | return (self.id, message or 'Successfully built', self.published) |
335 | |
336 | @coroutine |
337 | @@ -85,7 +105,7 @@ |
338 | def check_archive_pocket(self, source): |
339 | """Check if our package is in any archive pocket.""" |
340 | version = source.source_package_version |
341 | - dest = self.get_source(self.dest, version) |
342 | + dest = self.get_source(self.dest, version=version) |
343 | if dest: |
344 | results = yield from gather( |
345 | self.archive_id(source), self.archive_id(dest)) |
346 | @@ -148,8 +168,7 @@ |
347 | @coroutine |
348 | def check_diff_exists(self, source): |
349 | """Ensure that this package has a diff in the ticket artifacts.""" |
350 | - diff_path = '{}_{}_content.diff'.format(self.series.name, self.name) |
351 | - if diff_path not in self.artifacts: |
352 | + if self.content_diff not in self.artifacts: |
353 | return 'Diff missing' |
354 | |
355 | @coroutine |
356 | @@ -157,6 +176,37 @@ |
357 | """Not built from merges; skipping changelog check.""" |
358 | |
359 | @coroutine |
360 | + def check_upload_rights(self, source): |
361 | + """Ensure publisher has permission to upload.""" |
362 | + blame = environ['BLAME'] |
363 | + main = self.get_source(self.series.main_archive, pocket='Release') |
364 | + # Default to flashplugin-nonfree/universe in case this package is NEW |
365 | + kwargs = dict( |
366 | + distroseries=self.series, |
367 | + sourcepackagename='flashplugin-nonfree', |
368 | + component='universe', |
369 | + pocket='Proposed', |
370 | + person=blame) |
371 | + if main: |
372 | + kwargs.update( |
373 | + component=main.component_name, |
374 | + sourcepackagename=self.name) |
375 | + log.vars(kwargs, 'calling checkUpload') |
376 | + try: |
377 | + self.series.main_archive.checkUpload(**kwargs) |
378 | + self.authorized = True |
379 | + except ClientError: |
380 | + self.authorized = False |
381 | + log.debug('Can %s upload %s? %s', blame, self.name, self.authorized) |
382 | + |
383 | + @coroutine |
384 | + def check_package_ack(self, source): |
385 | + """Abort publication if packaging diffs have not been acknowledged.""" |
386 | + if self.packaging_diff in self.artifacts: |
387 | + if 'ACK_PACKAGING' not in environ or not self.authorized: |
388 | + return 'Packaging diff requires ACK' |
389 | + |
390 | + @coroutine |
391 | def build(self): |
392 | """Build source package.""" |
393 | yield from VCS.build_(**self.env) |
394 | @@ -177,23 +227,21 @@ |
395 | def expose(self): |
396 | """NOP; Subclasses may optionally implement this.""" |
397 | |
398 | - def get_sources(self, archive, version=None): |
399 | + def get_sources(self, archive, **kwargs): |
400 | """Get a list of lplib objects representing versions in archive.""" |
401 | - kwargs = dict( |
402 | + kwargs.update( |
403 | distro_series=self.series, |
404 | source_name=self.name, |
405 | order_by_date=True, |
406 | exact_match=True) |
407 | - if version: |
408 | - kwargs.update(version=version) |
409 | return [ |
410 | source for source in archive.getPublishedSources(**kwargs) |
411 | if source.status != 'Deleted'] |
412 | |
413 | - def get_source(self, archive, version=None): |
414 | + def get_source(self, archive, **kwargs): |
415 | """Get the lplib object representing this source.""" |
416 | with suppress(IndexError): |
417 | - return self.get_sources(archive, version)[0] |
418 | + return self.get_sources(archive, **kwargs)[0] |
419 | |
420 | def find_dsc_url(self, source): |
421 | """Identify the URL to a DSC file for this package in an archive.""" |
422 | @@ -214,11 +262,73 @@ |
423 | COMPONENT=dest_source.component_name if dest_source else 'NEW', |
424 | ) |
425 | dest_dsc = self.find_dsc_url(dest_source or self.get_source( |
426 | - self.dest.distribution.main_archive)) |
427 | + self.series.main_archive)) |
428 | yield from VCS.diff_dest_(DSC_URL=dest_dsc, **self.env) |
429 | log.info('Diffed %s.', self.id) |
430 | |
431 | @coroutine |
432 | + def prepublish(self): |
433 | + """Abort publication if problems discovered.""" |
434 | + source = self.get_source(self.ppa) |
435 | + checks = ( |
436 | + self.check_ppa_version, |
437 | + self.check_build_status, |
438 | + self.check_diff_exists, |
439 | + self.check_upload_rights, |
440 | + self.check_package_ack, |
441 | + self.check_destination_version, |
442 | + self.check_merge_states, |
443 | + self.check_new_commits, |
444 | + ) |
445 | + message = yield from run_checks(checks, source=source) |
446 | + return (self.id, message) |
447 | + |
448 | + def copy_to_dest(self, source): |
449 | + """Copy this source package to the destination PPA.""" |
450 | + log.info('Copying %s to %s.', source.display_name, self.dest.web_link) |
451 | + self.dest.copyPackage( |
452 | + from_archive=self.ppa, |
453 | + from_pocket=source.pocket, |
454 | + from_series=self.series.name, |
455 | + include_binaries=True, |
456 | + to_pocket='Release', |
457 | + to_series=self.series.name, |
458 | + source_name=self.name, |
459 | + version=source.source_package_version) |
460 | + |
461 | + def write_manifest(self, source, dest_version): |
462 | + """Add a line in the manifest representing this package.""" |
463 | + distro, series = source.distro_series_link.split(SLASH)[-2:] |
464 | + self.manifest.append(TAB.join([ |
465 | + SLASH.join([Config.ppa_team, distro, self.ppa.name]), |
466 | + source.pocket, |
467 | + series, |
468 | + 'Proposed', |
469 | + series, |
470 | + self.name, |
471 | + source.source_package_version, |
472 | + dest_version, |
473 | + environ['BLAME'], |
474 | + distro, |
475 | + ]) + N) |
476 | + |
477 | + @coroutine |
478 | + def publish(self): |
479 | + """Publish packages from PPA to dest archive.""" |
480 | + source = self.get_source(self.ppa) |
481 | + dest = self.get_source(self.dest) |
482 | + dest_version = dest.source_package_version if dest else '0' |
483 | + if V(source.source_package_version) > dest_version: |
484 | + if self.dest.self_link == self.series.main_archive_link: |
485 | + self.write_manifest(source, dest_version) |
486 | + else: |
487 | + self.copy_to_dest(source) |
488 | + else: |
489 | + log.warning('Source: %s', source.display_name) |
490 | + log.warning('Dest: %s', dest.display_name) |
491 | + log.warning('Source version not higher than dest, skipping!') |
492 | + |
493 | + @coroutine |
494 | def merge(self): |
495 | """NOP; implemented by subclasses.""" |
496 | |
497 | |
498 | === added file 'files/rsyncd.conf' |
499 | --- files/rsyncd.conf 1970-01-01 00:00:00 +0000 |
500 | +++ files/rsyncd.conf 2016-08-22 16:49:27 +0000 |
501 | @@ -0,0 +1,7 @@ |
502 | +[publish] |
503 | +comment = Bileto Publication Data |
504 | +path = /var/www/publish/ |
505 | +read only = no |
506 | +list = yes |
507 | +uid = www-data |
508 | +group = www-data |
509 | |
510 | === modified file 'scripts/vcs.sh' |
511 | --- scripts/vcs.sh 2016-08-17 19:13:08 +0000 |
512 | +++ scripts/vcs.sh 2016-08-22 16:49:27 +0000 |
513 | @@ -205,20 +205,18 @@ |
514 | cd "$WORKPARENT" |
515 | our_dsc="$(echo ${SOURCE}_*.dsc)" |
516 | [ -f "$our_dsc" ] || die_gently "Diff failed: No DSC found, was it ever built?" |
517 | - content="${SERIES}_${SOURCE}_content.diff" |
518 | - packaging="${SERIES}_${SOURCE}_packaging_changes.diff" |
519 | - raw="$packaging.raw" |
520 | - touch "$content" |
521 | + raw="$PACKAGING_DIFF.raw" |
522 | + touch "$CONTENT_DIFF" |
523 | if [ -n "$DSC_URL" ]; then |
524 | download_from_dsc "$DSC_URL" |
525 | dest_dsc="$(basename "$DSC_URL")" |
526 | - debdiff_wrapper "$dest_dsc" "$our_dsc" "$content" |
527 | - stdout filterdiff --clean --include '*debian/*' --exclude '*changelog' <"$content" >"$raw" || die "Failed to discover packaging changes." |
528 | + debdiff_wrapper "$dest_dsc" "$our_dsc" "$CONTENT_DIFF" |
529 | + stdout filterdiff --clean --include '*debian/*' --exclude '*changelog' <"$CONTENT_DIFF" >"$raw" || die "Failed to discover packaging changes." |
530 | else |
531 | echo '---NEW SOURCE PACKAGE---' >"$raw" |
532 | fi |
533 | if [ -s "$raw" ]; then |
534 | - _build_packaging_diff "$content" "$raw" >"$packaging" |
535 | + _build_packaging_diff "$CONTENT_DIFF" "$raw" >"$PACKAGING_DIFF" |
536 | fi |
537 | } |
538 | |
539 | |
540 | === modified file 'tests/test_actions.py' |
541 | --- tests/test_actions.py 2016-06-30 18:01:09 +0000 |
542 | +++ tests/test_actions.py 2016-08-22 16:49:27 +0000 |
543 | @@ -45,14 +45,9 @@ |
544 | manager = Mock() |
545 | Actions.publish(manager) |
546 | self.assertSequenceEqual(manager.mock_calls, [ |
547 | - call.checkstatus(), |
548 | - call.checkstatus().validate(), |
549 | - call.checkstatus().validate().checkupload(), |
550 | - call.checkstatus().validate().checkupload().ackaging(), |
551 | - call.checkdest(), |
552 | - call.checkdest().unapproved(), |
553 | - call.checkdest().unapproved().unbuilt(), |
554 | - call.checkdest().unapproved().unbuilt().publish(), |
555 | + call.validate(), |
556 | + call.validate().prepublish(), |
557 | + call.validate().prepublish().publish(), |
558 | ]) |
559 | |
560 | def test_actions_finalize(self): |
561 | |
562 | === modified file 'tests/test_streams.py' |
563 | --- tests/test_streams.py 2016-07-29 21:10:56 +0000 |
564 | +++ tests/test_streams.py 2016-08-22 16:49:27 +0000 |
565 | @@ -178,6 +178,24 @@ |
566 | self.assertEqual(env['ACTION'], 'build') |
567 | self.assertEqual(env['REQUEST_PATH'], '/log/1/build/latest/') |
568 | |
569 | + @patch('bileto.streams.Popen') |
570 | + def test_run_subprocess_form_env(self, popen): |
571 | + """Ensure forms are converted to env vars safely.""" |
572 | + ret = self.app.post('/log/1/publish/latest/', data=dict( |
573 | + ACK_PACKAGING='ACK_PACKAGING', |
574 | + MALICIOUS_VAR='REMOTE CODE EXPLOIT HERE', |
575 | + )) |
576 | + self.assertIn(b'Redirecting...', ret.data) |
577 | + self.assertEqual(ret.status_code, 303) |
578 | + env = popen.mock_calls[0][2]['env'] |
579 | + self.assertSequenceEqual(popen.mock_calls, [ |
580 | + call((join(Config.ROOT, 'bileto', 'worker', 'main.py'),), env=env), |
581 | + ]) |
582 | + self.assertEqual(env['ACTION'], 'publish') |
583 | + self.assertEqual(env['REQUEST_PATH'], '/log/1/publish/latest/') |
584 | + self.assertEqual(env['ACK_PACKAGING'], 'ACK_PACKAGING') |
585 | + self.assertNotIn('MALICIOUS_VAR', env) |
586 | + |
587 | def test_cancel(self): |
588 | """Test that jobs can be cancelled.""" |
589 | root = join(self.tempdir, 'log') |
590 | |
591 | === modified file 'tests/test_worker_manager.py' |
592 | --- tests/test_worker_manager.py 2016-08-17 20:59:59 +0000 |
593 | +++ tests/test_worker_manager.py 2016-08-22 16:49:27 +0000 |
594 | @@ -129,8 +129,8 @@ |
595 | |
596 | @patch('bileto.worker.manager.Manager.names') |
597 | @patch('bileto.worker.manager.Manager.merges') |
598 | - @patch('bileto.worker.manager.Manager.__getattr__') |
599 | - def test_manager_types_revert(self, attr, merges, names): |
600 | + @patch('bileto.worker.manager.Manager.call_on_all_packages') |
601 | + def test_manager_types_revert(self, call_on_all, merges, names): |
602 | """Correctly choose what revert types to build with.""" |
603 | merges.return_value = dict() |
604 | names.return_value = {'foo', 'bar'} |
605 | @@ -141,12 +141,12 @@ |
606 | Merge: set(), |
607 | Revert: {'foo', 'bar'}, |
608 | }) |
609 | - attr.assert_called_once_with('collect') |
610 | + call_on_all.assert_called_once_with('collect') |
611 | |
612 | @patch('bileto.worker.manager.Manager.names') |
613 | @patch('bileto.worker.manager.Manager.merges') |
614 | - @patch('bileto.worker.manager.Manager.__getattr__') |
615 | - def test_manager_types_revert_no_MPs(self, attr, merges, names): |
616 | + @patch('bileto.worker.manager.Manager.call_on_all_packages') |
617 | + def test_manager_types_revert_no_MPs(self, call_on_all, merges, names): |
618 | """Do not attempt to revert merges.""" |
619 | merges.return_value = dict(foo=[Mock()]) |
620 | names.return_value = {'foo', 'bar'} |
621 | @@ -257,14 +257,14 @@ |
622 | call.validate(), |
623 | ]) |
624 | |
625 | - @patch('bileto.worker.manager.Manager.__getattr__') |
626 | - def test_manager_upload(self, getattr_mock): |
627 | + @patch('bileto.worker.manager.Manager.call_on_all_packages') |
628 | + def test_manager_upload(self, call_on_all_packages): |
629 | """Prevent cancellation after uploading.""" |
630 | log.cancellable = True |
631 | ticket = Mock() |
632 | manager = Manager(ticket) |
633 | manager.upload() |
634 | - getattr_mock.assert_called_once_with('upload') |
635 | + call_on_all_packages.assert_called_once_with('upload') |
636 | assert not log.cancellable |
637 | |
638 | @patch('bileto.worker.manager.Package') |
639 | @@ -311,7 +311,9 @@ |
640 | ('xenial/media-hub', 'Successfully built', None), |
641 | ('yakkety/media-hub', 'Successfully built', None), |
642 | ] |
643 | - ticket = Mock(status='Preparing packages.') |
644 | + ticket = Mock( |
645 | + status='Preparing packages.', |
646 | + lockfile=join(self.tempdir, 'lockfile')) |
647 | manager = Manager(ticket) |
648 | manager.status() |
649 | ticket.set_status.assert_called_once_with( |
650 | @@ -347,7 +349,9 @@ |
651 | ('xenial/media-hub', 'Successfully built', None), |
652 | ('yakkety/media-hub', 'Successfully built', None), |
653 | ] |
654 | - ticket = Mock(status='Preparing packages.') |
655 | + ticket = Mock( |
656 | + status='Preparing packages.', |
657 | + lockfile=join(self.tempdir, 'lockfile')) |
658 | manager = Manager(ticket) |
659 | manager.status() |
660 | ticket.set_status.assert_called_once_with( |
661 | @@ -368,7 +372,9 @@ |
662 | ('xenial/media-hub', 'Release pocket', '2'), |
663 | ('yakkety/media-hub', 'Updates pocket', '3'), |
664 | ] |
665 | - ticket = Mock(status='Preparing packages.') |
666 | + ticket = Mock( |
667 | + status='Preparing packages.', |
668 | + lockfile=join(self.tempdir, 'lockfile')) |
669 | manager = Manager(ticket) |
670 | manager.status() |
671 | ticket.set_status.assert_called_once_with( |
672 | @@ -378,6 +384,60 @@ |
673 | '"2", "yakkety": "3"}}') |
674 | Acts.finalize.assert_called_once_with(manager) |
675 | |
676 | + @patch('bileto.worker.manager.parallelize') |
677 | + @patch('bileto.worker.manager.Manager.packages') |
678 | + def test_manager_prepublish(self, packages, parallelize): |
679 | + """Allow publish to proceed.""" |
680 | + packages.return_value = {} |
681 | + parallelize.return_value = [ |
682 | + ('vivid/media-hub', None), |
683 | + ('xenial/media-hub', None), |
684 | + ('yakkety/media-hub', None), |
685 | + ] |
686 | + ticket = Mock() |
687 | + manager = Manager(ticket) |
688 | + self.assertEqual(manager.prepublish(), manager) # No exception raised |
689 | + |
690 | + @patch('bileto.worker.manager.parallelize') |
691 | + @patch('bileto.worker.manager.Manager.packages') |
692 | + def test_manager_prepublish_abort(self, packages, parallelize): |
693 | + """Prevent publication when problems found.""" |
694 | + packages.return_value = {} |
695 | + parallelize.return_value = [ |
696 | + ('vivid/media-hub', 'Diff missing'), |
697 | + ('xenial/media-hub', 'Diff missing'), |
698 | + ('yakkety/media-hub', 'Dependency wait'), |
699 | + ] |
700 | + ticket = Mock() |
701 | + manager = Manager(ticket) |
702 | + with self.assertRaisesRegex(BiletoError, 'Publish failed: Dep'): |
703 | + manager.prepublish() |
704 | + |
705 | + @patch('bileto.worker.manager.rename') |
706 | + @patch('bileto.worker.manager.makedirs') |
707 | + @patch('bileto.worker.manager.parallelize') |
708 | + @patch('bileto.worker.manager.Manager.packages') |
709 | + @patch('bileto.worker.manager.Package.manifest', ['a\n', 'b\n', 'c\n']) |
710 | + def test_manager_publish(self, packages, parallelize, makedirs, rename): |
711 | + """Do publishy things.""" |
712 | + packages.return_value = {} |
713 | + parallelize.return_value = [ |
714 | + ('vivid/media-hub', None), |
715 | + ('xenial/media-hub', None), |
716 | + ('yakkety/media-hub', None), |
717 | + ] |
718 | + ticket = Mock(siloname='ubuntu/landing-555') |
719 | + manager = Manager(ticket) |
720 | + manager.publish() |
721 | + makedirs.assert_called_once_with( |
722 | + join(self.tempdir, 'publish'), exist_ok=True) |
723 | + filename = 'packagelist_rsync_ubuntu-landing-555' |
724 | + tmpfile = join(self.tempdir, filename) |
725 | + path = join(self.tempdir, 'publish', filename) |
726 | + rename.assert_called_once_with(filename, path) |
727 | + with open(tmpfile, encoding='utf-8') as data: |
728 | + self.assertEqual(data.read().strip(), 'a\nb\nc') |
729 | + |
730 | @patch('bileto.worker.manager.Manager.ppa') |
731 | @patch('bileto.worker.vcs.create_subprocess_exec', POST_NUKE) |
732 | def test_manager_nuke(self, ppa): |
733 | |
734 | === added file 'tests/test_worker_manual.py' |
735 | --- tests/test_worker_manual.py 1970-01-01 00:00:00 +0000 |
736 | +++ tests/test_worker_manual.py 2016-08-22 16:49:27 +0000 |
737 | @@ -0,0 +1,28 @@ |
738 | +"""Bileto Manual Test |
739 | + |
740 | +Test that manual packages behave correctly. |
741 | +""" |
742 | + |
743 | +from os import environ |
744 | + |
745 | +from unittest.mock import patch |
746 | + |
747 | +from tests.tests import WorkerTestCase |
748 | + |
749 | +from bileto.worker.manual import Manual |
750 | + |
751 | + |
752 | +class ManualTestCase(WorkerTestCase): |
753 | + """Test Manual Packages.""" |
754 | + |
755 | + @patch('bileto.worker.package.Package.check_package_ack') |
756 | + def test_manual_check_package_ack(self, check_package_ack): |
757 | + """Require upload rights for all manual sources.""" |
758 | + environ['BLAME'] = 'robru' |
759 | + manual = Manual('oxide', self.distro, self.series, self.dest, self.ppa) |
760 | + manual.authorized = True |
761 | + self.assertEqual(self.run_coro(manual.check_package_ack()), None) |
762 | + manual.authorized = False |
763 | + self.assertEqual( |
764 | + self.run_coro(manual.check_package_ack()), |
765 | + 'robru not authorized to upload oxide') |
766 | |
767 | === modified file 'tests/test_worker_merge.py' |
768 | --- tests/test_worker_merge.py 2016-07-21 17:10:54 +0000 |
769 | +++ tests/test_worker_merge.py 2016-08-22 16:49:27 +0000 |
770 | @@ -11,7 +11,7 @@ |
771 | from tests.tests import WorkerTestCase, fake_subproc |
772 | |
773 | from bileto.settings import Config |
774 | -from bileto.worker.merge import Merge |
775 | +from bileto.worker.merge import BUILDABLE_STATES, PUBLISHABLE_STATES, Merge |
776 | |
777 | |
778 | STD = (b'', b'') |
779 | @@ -309,3 +309,13 @@ |
780 | call(join(Config.ROOT, 'scripts', 'vcs.sh'), |
781 | 'lp:ofono', env=env, stdout=PIPE, stderr=PIPE), |
782 | ]) |
783 | + |
784 | + @patch('bileto.worker.package.Package.prepublish') |
785 | + def test_merge_prepublish(self, prepublish): |
786 | + """Set acceptable merge states for publishing.""" |
787 | + Merge.all_mps = dict(ofono=[self.mp]) |
788 | + merge = Merge('ofono', self.distro, self.series, self.dest, self.ppa) |
789 | + self.assertEqual(merge.acceptable, BUILDABLE_STATES) |
790 | + self.run_coro(merge.prepublish()) |
791 | + self.assertEqual(merge.acceptable, PUBLISHABLE_STATES) |
792 | + prepublish.assert_called_once_with() |
793 | |
794 | === modified file 'tests/test_worker_package.py' |
795 | --- tests/test_worker_package.py 2016-07-21 17:10:54 +0000 |
796 | +++ tests/test_worker_package.py 2016-08-22 16:49:27 +0000 |
797 | @@ -3,10 +3,14 @@ |
798 | Test the steps for building a package. |
799 | """ |
800 | |
801 | +from os import environ |
802 | from os.path import join |
803 | + |
804 | from subprocess import PIPE |
805 | from unittest.mock import Mock, call, patch |
806 | |
807 | +from lazr.restfulclient.errors import ClientError |
808 | + |
809 | from tests.tests import WorkerTestCase, fake_subproc |
810 | |
811 | from bileto.settings import Config |
812 | @@ -37,6 +41,10 @@ |
813 | class PackageTestCase(WorkerTestCase): |
814 | """Test Packages.""" |
815 | |
816 | + def setUp(self): |
817 | + super().setUp() |
818 | + Package.manifest.clear() |
819 | + |
820 | def test_package_status_ready_to_build(self): |
821 | """Report Ready to build when no package in PPA.""" |
822 | self.ppa.getPublishedSources.return_value = [] |
823 | @@ -243,3 +251,117 @@ |
824 | call(source_name='dif', distro_series=self.series, |
825 | exact_match=True, order_by_date=True), |
826 | ]) |
827 | + |
828 | + def test_package_prepublish_ready_to_build(self): |
829 | + """Report Ready to build when no package in PPA.""" |
830 | + self.ppa.getPublishedSources.return_value = [] |
831 | + package = Package('zap', self.distro, self.series, self.dest, self.ppa) |
832 | + self.assertEqual( |
833 | + self.run_coro(package.prepublish()), |
834 | + ('xenial/zap', 'Ready to build')) |
835 | + |
836 | + def test_package_prepublish_package_ack_unauthorized(self): |
837 | + """Report when user not authorized to ack.""" |
838 | + environ['BLAME'] = 'robru' |
839 | + environ['ACK_PACKAGING'] = 'yes' |
840 | + self.ppa.getPublishedSources.return_value = [ |
841 | + Mock(source_package_version='42')] |
842 | + self.dest.getPublishedSources.return_value = [] |
843 | + i386 = Mock(arch_tag='i386', buildstate='Successfully built') |
844 | + source = Mock(source_package_version='42') |
845 | + source.getBuilds.return_value = [i386] |
846 | + self.ppa.getPublishedSources.return_value = [source] |
847 | + self.series.main_archive.getPublishedSources.return_value = [] |
848 | + self.series.getPackageUploads.return_value = Listish() |
849 | + package = Package('zap', self.distro, self.series, self.dest, self.ppa) |
850 | + package.artifacts = ( |
851 | + 'time/xenial_zap_content.diff (1 MILLION LINES)\n' |
852 | + 'time/xenial_zap_packaging_changes.diff (1 THOUSAND LINES)\n') |
853 | + package.series.main_archive.checkUpload.side_effect = ClientError(0, 0) |
854 | + self.assertEqual( |
855 | + self.run_coro(package.prepublish()), |
856 | + ('xenial/zap', 'Packaging diff requires ACK')) |
857 | + self.assertFalse(package.authorized) |
858 | + |
859 | + def test_package_prepublish_package_ack_authorized(self): |
860 | + """Report when user not authorized to ack.""" |
861 | + environ['BLAME'] = 'robru' |
862 | + environ['ACK_PACKAGING'] = 'yes' |
863 | + self.ppa.getPublishedSources.return_value = [ |
864 | + Mock(source_package_version='42')] |
865 | + self.dest.getPublishedSources.return_value = [] |
866 | + i386 = Mock(arch_tag='i386', buildstate='Successfully built') |
867 | + source = Mock(source_package_version='42') |
868 | + source.getBuilds.return_value = [i386] |
869 | + source.getPublishedBinaries.return_value = [ |
870 | + Mock(architecture_specific=1, distro_arch_series_link='./i386'), |
871 | + ] |
872 | + self.ppa.getPublishedSources.return_value = [source] |
873 | + self.series.main_archive.getPublishedSources.return_value = [source] |
874 | + self.series.getPackageUploads.return_value = Listish() |
875 | + package = Package('zap', self.distro, self.series, self.dest, self.ppa) |
876 | + package.artifacts = ( |
877 | + 'time/xenial_zap_content.diff (1 MILLION LINES)\n' |
878 | + 'time/xenial_zap_packaging_changes.diff (1 THOUSAND LINES)\n') |
879 | + package.series.main_archive.checkUpload.return_value = True |
880 | + self.assertEqual( |
881 | + self.run_coro(package.prepublish()), |
882 | + ('xenial/zap', None)) |
883 | + self.assertTrue(package.authorized) |
884 | + |
885 | + @patch('bileto.worker.package.Config.ppa_team', 'glort') |
886 | + def test_package_publish_manifest(self): |
887 | + """Publish a package to ubuntu archive.""" |
888 | + environ['BLAME'] = 'robobert' |
889 | + source = Mock(source_package_version='90') |
890 | + source.distro_series_link = 'foo/bar/ubuntu/yakkety' |
891 | + source.pocket = 'Release' |
892 | + self.ppa.getPublishedSources.return_value = [source] |
893 | + dest = Mock(source_package_version='80') |
894 | + self.dest.getPublishedSources.return_value = [dest] |
895 | + self.dest.self_link = self.series.main_archive_link = 'ubuntu archive' |
896 | + package = Package('zap', self.distro, self.series, self.dest, self.ppa) |
897 | + self.run_coro(package.publish()) |
898 | + self.assertEqual(package.manifest, [ |
899 | + 'glort/ubuntu/12321\tRelease\tyakkety\tProposed\tyakkety\tzap\t90' |
900 | + '\t80\trobobert\tubuntu\n']) |
901 | + |
902 | + @patch('bileto.worker.package.Config.ppa_team', 'glort') |
903 | + def test_package_publish_copypackage(self): |
904 | + """Publish a package to overlay ppa.""" |
905 | + environ['BLAME'] = 'robobert' |
906 | + source = Mock(source_package_version='90') |
907 | + source.distro_series_link = 'foo/bar/ubuntu/yakkety' |
908 | + source.pocket = 'Release' |
909 | + self.ppa.getPublishedSources.return_value = [source] |
910 | + dest = Mock(source_package_version='80') |
911 | + self.dest.getPublishedSources.return_value = [dest] |
912 | + self.series.main_archive_link = 'ubuntu archive' |
913 | + self.dest.self_link = 'some other place' |
914 | + package = Package('zap', self.distro, self.series, self.dest, self.ppa) |
915 | + self.run_coro(package.publish()) |
916 | + self.assertEqual(package.manifest, []) |
917 | + self.dest.copyPackage.assert_called_once_with( |
918 | + from_archive=self.ppa, |
919 | + from_pocket='Release', |
920 | + from_series='xenial', |
921 | + include_binaries=True, |
922 | + source_name='zap', |
923 | + to_pocket='Release', |
924 | + to_series='xenial', |
925 | + version='90') |
926 | + |
927 | + @patch('bileto.worker.package.Config.ppa_team', 'glort') |
928 | + def test_package_publish_skipped(self): |
929 | + """Skip publishing because version is lower than dest.""" |
930 | + environ['BLAME'] = 'robobert' |
931 | + source = Mock(source_package_version='70') |
932 | + source.distro_series_link = 'foo/bar/ubuntu/yakkety' |
933 | + source.pocket = 'Release' |
934 | + self.ppa.getPublishedSources.return_value = [source] |
935 | + dest = Mock(source_package_version='80') |
936 | + self.dest.getPublishedSources.return_value = [dest] |
937 | + package = Package('zap', self.distro, self.series, self.dest, self.ppa) |
938 | + self.run_coro(package.publish()) |
939 | + self.assertEqual(package.manifest, []) |
940 | + self.assertEqual(self.dest.copyPackage.mock_calls, []) |
Mostly code style issues and nitpicks, but two or three more serious issues.