Merge launchpad:stable into launchpad:db-devel
- Git
- lp:launchpad
- stable
- Merge into db-devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | f92b6e0bb2624f8a9125e3f45b7621c6e230c5ff |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | launchpad:stable |
Merge into: | launchpad:db-devel |
Diff against target: |
1977 lines (+959/-49) 34 files modified
Makefile (+61/-9) database/sampledata/current-dev.sql (+2/-2) database/sampledata/current.sql (+2/-2) lib/lp/_schema_circular_imports.py (+10/-1) lib/lp/archivepublisher/publishing.py (+4/-2) lib/lp/archivepublisher/tests/test_publisher.py (+40/-0) lib/lp/code/browser/configure.zcml (+6/-0) lib/lp/code/browser/gitrepository.py (+13/-0) lib/lp/code/configure.zcml (+34/-0) lib/lp/code/enums.py (+50/-0) lib/lp/code/interfaces/gitrepository.py (+195/-0) lib/lp/code/interfaces/webservice.py (+2/-0) lib/lp/code/model/gitrepository.py (+158/-1) lib/lp/code/model/tests/test_gitrepository.py (+197/-0) lib/lp/registry/configure.zcml (+2/-1) lib/lp/registry/doc/distribution-mirror.txt (+12/-5) lib/lp/registry/interfaces/distroseries.py (+10/-1) lib/lp/registry/model/distributionmirror.py (+5/-4) lib/lp/registry/model/distroseries.py (+10/-0) lib/lp/registry/model/person.py (+1/-1) lib/lp/registry/tests/test_distributionmirror.py (+15/-7) lib/lp/registry/tests/test_distroseries.py (+11/-1) lib/lp/security.py (+19/-0) lib/lp/services/config/schema-lazr.conf (+4/-0) lib/lp/services/librarianserver/swift.py (+1/-0) lib/lp/services/librarianserver/tests/test_swift.py (+9/-0) lib/lp/services/webapp/tests/test_servers.py (+8/-0) lib/lp/services/webservice/configuration.py (+19/-4) lib/lp/soyuz/scripts/initialize_distroseries.py (+4/-1) lib/lp/soyuz/scripts/tests/test_initialize_distroseries.py (+3/-1) lib/lp/testing/factory.py (+29/-1) lib/lp/testing/swift/fakeswift.py (+2/-0) lib/lp/translations/scripts/po_import.py (+2/-5) lib/lp/translations/scripts/tests/test_translations_import.py (+19/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+412895@code.launchpad.net |
Commit message
Manually merge from stable to fix time-dependent tests
Description of the change
This is needed on db-devel to make tests pass again; see https:/
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/Makefile b/Makefile |
2 | index b2daade..90a640e 100644 |
3 | --- a/Makefile |
4 | +++ b/Makefile |
5 | @@ -96,15 +96,19 @@ PIP_BIN = \ |
6 | bin/with-xvfb |
7 | |
8 | # DO NOT ALTER : this should just build by default |
9 | +.PHONY: default |
10 | default: inplace |
11 | |
12 | +.PHONY: schema |
13 | schema: build |
14 | $(MAKE) -C database/schema |
15 | $(RM) -r /var/tmp/fatsam |
16 | |
17 | +.PHONY: newsampledata |
18 | newsampledata: |
19 | $(MAKE) -C database/schema newsampledata |
20 | |
21 | +.PHONY: hosted_branches |
22 | hosted_branches: $(PY) |
23 | $(PY) ./utilities/make-dummy-hosted-branches |
24 | |
25 | @@ -115,6 +119,7 @@ $(API_INDEX): $(VERSION_INFO) $(PY) |
26 | --force "$(APIDOC_TMPDIR)" |
27 | mv $(APIDOC_TMPDIR) $(APIDOC_DIR) |
28 | |
29 | +.PHONY: apidoc |
30 | ifdef LP_MAKE_NO_WADL |
31 | apidoc: |
32 | @echo "Skipping WADL generation." |
33 | @@ -123,15 +128,18 @@ apidoc: compile $(API_INDEX) |
34 | endif |
35 | |
36 | # Used to generate HTML developer documentation for Launchpad. |
37 | +.PHONY: doc |
38 | doc: |
39 | $(MAKE) -C doc/ html |
40 | |
41 | # Run by PQM. |
42 | +.PHONY: check_config |
43 | check_config: build |
44 | bin/test -m lp.services.config.tests -vvt test_config |
45 | |
46 | # Clean before running the test suite, since the build might fail depending |
47 | # what source changes happened. (e.g. apidoc depends on interfaces) |
48 | +.PHONY: check |
49 | check: clean build |
50 | # Run all tests. test_on_merge.py takes care of setting up the |
51 | # database. |
52 | @@ -141,6 +149,7 @@ check: clean build |
53 | logs: |
54 | mkdir logs |
55 | |
56 | +.PHONY: codehosting-dir |
57 | codehosting-dir: |
58 | mkdir -p $(CODEHOSTING_ROOT) |
59 | mkdir -p $(CODEHOSTING_ROOT)/mirrors |
60 | @@ -155,11 +164,13 @@ ifneq ($(SUDO_UID),) |
61 | fi |
62 | endif |
63 | |
64 | +.PHONY: inplace |
65 | inplace: build logs clean_logs codehosting-dir |
66 | if [ -d /srv/launchpad.test ]; then \ |
67 | ln -sfn $(WD)/build/js $(CONVOY_ROOT); \ |
68 | fi |
69 | |
70 | +.PHONY: build |
71 | build: compile apidoc jsbuild css_combine |
72 | |
73 | # LP_SOURCEDEPS_PATH should point to the sourcecode directory, but we |
74 | @@ -174,6 +185,7 @@ else |
75 | @exit 1 |
76 | endif |
77 | |
78 | +.PHONY: css_combine |
79 | css_combine: jsbuild_widget_css |
80 | ${SHHH} bin/sprite-util create-image |
81 | ${SHHH} bin/sprite-util create-css |
82 | @@ -184,18 +196,20 @@ css_combine: jsbuild_widget_css |
83 | # XXX 2020-06-12 twom This should have `--output-style compressed`. Removed for debugging purposes |
84 | SASS_BINARY_PATH=$(NODE_SASS_BINARY) $(YARN) run node-sass --include-path $(WD)/$(ICING) --follow --output $(WD)/$(ICING) $(WD)/$(ICING)/combo.scss |
85 | |
86 | +.PHONY: css_watch |
87 | css_watch: jsbuild_widget_css |
88 | ${SHHH} bin/sprite-util create-image |
89 | ${SHHH} bin/sprite-util create-css |
90 | ln -sfn ../../../../yarn/node_modules/yui $(ICING)/yui |
91 | SASS_BINARY_PATH=$(NODE_SASS_BINARY) $(YARN) run node-sass --include-path $(WD)/$(ICING) --follow --output $(WD)/$(ICING) $(WD)/$(ICING)/ --watch --recursive |
92 | |
93 | - |
94 | +.PHONY: jsbuild_widget_css |
95 | jsbuild_widget_css: bin/jsbuild |
96 | ${SHHH} bin/jsbuild \ |
97 | --srcdir lib/lp/app/javascript \ |
98 | --builddir $(LP_BUILT_JS_ROOT) |
99 | |
100 | +.PHONY: jsbuild_watch |
101 | jsbuild_watch: |
102 | $(PY) bin/watch_jsbuild |
103 | |
104 | @@ -218,6 +232,7 @@ $(JS_BUILD_DIR)/.production: yarn/package.json | $(YARN_BUILD) |
105 | $(YUI_SYMLINK): $(JS_BUILD_DIR)/.production |
106 | ln -sfn ../../yarn/node_modules/yui $@ |
107 | |
108 | +.PHONY: $(LP_JS_BUILD) |
109 | $(LP_JS_BUILD): | $(JS_BUILD_DIR) |
110 | mkdir -p $@/services |
111 | for jsdir in lib/lp/*/javascript lib/lp/services/*/javascript; do \ |
112 | @@ -227,6 +242,7 @@ $(LP_JS_BUILD): | $(JS_BUILD_DIR) |
113 | find $@ -name 'tests' -type d | xargs rm -rf |
114 | LC_ALL=C.UTF-8 bin/lpjsmin -p $@ |
115 | |
116 | +.PHONY: jsbuild |
117 | jsbuild: $(LP_JS_BUILD) $(YUI_SYMLINK) |
118 | LC_ALL=C.UTF-8 utilities/js-deps -n LP_MODULES -s build/js/lp \ |
119 | -x '-min.js' -o build/js/lp/meta.js >/dev/null |
120 | @@ -252,6 +268,7 @@ requirements/combined.txt: \ |
121 | # It doesn't seem to be straightforward to build a wheelhouse of all our |
122 | # dependencies without also building a useless wheel of Launchpad itself; |
123 | # fortunately that doesn't take too long, and we just remove it afterwards. |
124 | +.PHONY: build_wheels |
125 | build_wheels: $(PIP_BIN) requirements/combined.txt |
126 | $(RM) -r wheelhouse wheels |
127 | $(SHHH) $(PIP) wheel \ |
128 | @@ -261,6 +278,7 @@ build_wheels: $(PIP_BIN) requirements/combined.txt |
129 | $(MAKE) clean_pip |
130 | |
131 | # Compatibility |
132 | +.PHONY: build_eggs |
133 | build_eggs: build_wheels |
134 | |
135 | # setuptools won't touch files that would have the same contents, but for |
136 | @@ -290,6 +308,7 @@ $(subst $(VENV_PYTHON),,$(PIP_BIN)): $(VENV_PYTHON) |
137 | |
138 | # Explicitly update version-info.py rather than declaring $(VERSION_INFO) as |
139 | # a prerequisite, to make sure it's up to date when doing deployments. |
140 | +.PHONY: compile |
141 | compile: $(VENV_PYTHON) |
142 | ${SHHH} utilities/relocate-virtualenv env |
143 | $(PYTHON) utilities/link-system-packages.py \ |
144 | @@ -297,22 +316,28 @@ compile: $(VENV_PYTHON) |
145 | ${SHHH} bin/build-twisted-plugin-cache |
146 | scripts/update-version-info.sh |
147 | |
148 | +.PHONY: test_build |
149 | test_build: build |
150 | bin/test $(TESTFLAGS) $(TESTOPTS) |
151 | |
152 | +.PHONY: test_inplace |
153 | test_inplace: inplace |
154 | bin/test $(TESTFLAGS) $(TESTOPTS) |
155 | |
156 | +.PHONY: ftest_build |
157 | ftest_build: build |
158 | bin/test -f $(TESTFLAGS) $(TESTOPTS) |
159 | |
160 | +.PHONY: ftest_inplace |
161 | ftest_inplace: inplace |
162 | bin/test -f $(TESTFLAGS) $(TESTOPTS) |
163 | |
164 | +.PHONY: run |
165 | run: build inplace stop |
166 | bin/run -r librarian,bing-webservice,memcached,rabbitmq \ |
167 | -i $(LPCONFIG) |
168 | |
169 | +.PHONY: run-testapp |
170 | run-testapp: LPCONFIG=testrunner-appserver |
171 | run-testapp: build inplace stop |
172 | LPCONFIG=$(LPCONFIG) INTERACTIVE_TESTS=1 bin/run-testapp \ |
173 | @@ -321,40 +346,50 @@ run-testapp: build inplace stop |
174 | run.gdb: |
175 | echo 'run' > run.gdb |
176 | |
177 | +.PHONY: start-gdb |
178 | start-gdb: build inplace stop support_files run.gdb |
179 | nohup gdb -x run.gdb --args bin/run -i $(LPCONFIG) \ |
180 | -r librarian,bing-webservice |
181 | > ${LPCONFIG}-nohup.out 2>&1 & |
182 | |
183 | +.PHONY: run_all |
184 | run_all: build inplace stop |
185 | bin/run \ |
186 | -r librarian,sftp,codebrowse,bing-webservice,\ |
187 | memcached,rabbitmq -i $(LPCONFIG) |
188 | |
189 | +.PHONY: run_codebrowse |
190 | run_codebrowse: compile |
191 | BRZ_PLUGIN_PATH=brzplugins $(PY) scripts/start-loggerhead.py |
192 | |
193 | +.PHONY: start_codebrowse |
194 | start_codebrowse: compile |
195 | BRZ_PLUGIN_PATH=$(shell pwd)/brzplugins $(PY) scripts/start-loggerhead.py --daemon |
196 | |
197 | +.PHONY: stop_codebrowse |
198 | stop_codebrowse: |
199 | $(PY) scripts/stop-loggerhead.py |
200 | |
201 | +.PHONY: run_codehosting |
202 | run_codehosting: build inplace stop |
203 | bin/run -r librarian,sftp,codebrowse,rabbitmq -i $(LPCONFIG) |
204 | |
205 | +.PHONY: start_librarian |
206 | start_librarian: compile |
207 | bin/start_librarian |
208 | |
209 | +.PHONY: stop_librarian |
210 | stop_librarian: |
211 | bin/killservice librarian |
212 | |
213 | $(VERSION_INFO): |
214 | scripts/update-version-info.sh |
215 | |
216 | +.PHONY: support_files |
217 | support_files: $(API_INDEX) $(VERSION_INFO) |
218 | |
219 | # Intended for use on developer machines |
220 | +.PHONY: start |
221 | start: inplace stop support_files initscript-start |
222 | |
223 | # Run as a daemon - hack using nohup until we move back to using zdaemon |
224 | @@ -363,42 +398,52 @@ start: inplace stop support_files initscript-start |
225 | # will not work as expected. For use on production servers, where |
226 | # we know we don't need the extra steps in a full "make start" |
227 | # because of how the code is deployed/built. |
228 | +.PHONY: initscript-start |
229 | initscript-start: |
230 | nohup bin/run -i $(LPCONFIG) > ${LPCONFIG}-nohup.out 2>&1 & |
231 | |
232 | # Intended for use on developer machines |
233 | +.PHONY: stop |
234 | stop: build initscript-stop |
235 | |
236 | # Kill launchpad last - other services will probably shutdown with it, |
237 | # so killing them after is a race condition. For use on production |
238 | # servers, where we know we don't need the extra steps in a full |
239 | # "make stop" because of how the code is deployed/built. |
240 | +.PHONY: initscript-stop |
241 | initscript-stop: |
242 | bin/killservice librarian launchpad |
243 | |
244 | +.PHONY: shutdown |
245 | shutdown: scheduleoutage stop |
246 | $(RM) +maintenancetime.txt |
247 | |
248 | +.PHONY: scheduleoutage |
249 | scheduleoutage: |
250 | echo Scheduling outage in ${MINS_TO_SHUTDOWN} mins |
251 | date --iso-8601=minutes -u -d +${MINS_TO_SHUTDOWN}mins > +maintenancetime.txt |
252 | echo Sleeping ${MINS_TO_SHUTDOWN} mins |
253 | sleep ${MINS_TO_SHUTDOWN}m |
254 | |
255 | +.PHONY: harness |
256 | harness: bin/harness |
257 | bin/harness |
258 | |
259 | +.PHONY: iharness |
260 | iharness: bin/iharness |
261 | bin/iharness |
262 | |
263 | +.PHONY: rebuildfti |
264 | rebuildfti: |
265 | @echo Rebuilding FTI indexes on launchpad_dev database |
266 | $(PY) database/schema/fti.py -d launchpad_dev --force |
267 | |
268 | +.PHONY: clean_js |
269 | clean_js: |
270 | $(RM) -r $(JS_BUILD_DIR) |
271 | $(RM) -r yarn/node_modules |
272 | |
273 | +.PHONY: clean_pip |
274 | clean_pip: |
275 | $(RM) -r build |
276 | if [ -d $(CONVOY_ROOT) ]; then $(RM) -r $(CONVOY_ROOT) ; fi |
277 | @@ -408,11 +453,14 @@ clean_pip: |
278 | $(RM) .installed.cfg |
279 | |
280 | # Compatibility. |
281 | +.PHONY: clean_buildout |
282 | clean_buildout: clean_pip |
283 | |
284 | +.PHONY: clean_logs |
285 | clean_logs: |
286 | $(RM) logs/thread*.request |
287 | |
288 | +.PHONY: lxc-clean |
289 | lxc-clean: clean_js clean_pip clean_logs |
290 | # XXX: BradCrittenden 2012-05-25 bug=1004514: |
291 | # It is important for parallel tests inside LXC that the |
292 | @@ -446,31 +494,38 @@ lxc-clean: clean_js clean_pip clean_logs |
293 | $(RM) -r /var/tmp/launchpad_mailqueue; \ |
294 | fi |
295 | |
296 | +.PHONY: clean |
297 | clean: lxc-clean |
298 | $(RM) -r $(CODEHOSTING_ROOT) |
299 | |
300 | +.PHONY: realclean |
301 | realclean: clean |
302 | $(RM) TAGS tags |
303 | |
304 | +.PHONY: potemplates |
305 | potemplates: launchpad.pot |
306 | |
307 | # Generate launchpad.pot by extracting message ids from the source |
308 | # XXX cjwatson 2017-09-04: This was previously done using i18nextract from |
309 | # z3c.recipe.i18n, but has been broken for some time. The place to start in |
310 | # putting this together again is probably zope.app.locales. |
311 | +.PHONY: launchpad.pot |
312 | launchpad.pot: |
313 | echo "POT generation not currently supported; help us fix this!" >&2 |
314 | exit 1 |
315 | |
316 | # Called by the rocketfuel-setup script. You probably don't want to run this |
317 | # on its own. |
318 | +.PHONY: install |
319 | install: reload-apache |
320 | |
321 | +.PHONY: copy-certificates |
322 | copy-certificates: |
323 | mkdir -p /etc/apache2/ssl |
324 | cp configs/$(LPCONFIG)/launchpad.crt /etc/apache2/ssl/ |
325 | cp configs/$(LPCONFIG)/launchpad.key /etc/apache2/ssl/ |
326 | |
327 | +.PHONY: copy-apache-config |
328 | copy-apache-config: codehosting-dir |
329 | # Byte-compile scripts/_pythonpath.py first, otherwise Apache may do |
330 | # so as root and cause permission problems. |
331 | @@ -494,19 +549,23 @@ copy-apache-config: codehosting-dir |
332 | chown $(SUDO_UID):$(SUDO_GID) /srv/launchpad.test; \ |
333 | fi |
334 | |
335 | +.PHONY: enable-apache-launchpad |
336 | enable-apache-launchpad: copy-apache-config copy-certificates |
337 | [ ! -e /etc/apache2/mods-available/version.load ] || a2enmod version |
338 | a2ensite local-launchpad |
339 | |
340 | +.PHONY: reload-apache |
341 | reload-apache: enable-apache-launchpad |
342 | service apache2 restart |
343 | |
344 | +.PHONY: TAGS |
345 | TAGS: compile |
346 | # emacs tags |
347 | ctags -R -e --languages=-JavaScript --python-kinds=-i -f $@.new \ |
348 | $(CURDIR)/lib "$(SITE_PACKAGES)" |
349 | mv $@.new $@ |
350 | |
351 | +.PHONY: tags |
352 | tags: compile |
353 | # vi tags |
354 | ctags -R --languages=-JavaScript --python-kinds=-i -f $@.new \ |
355 | @@ -516,16 +575,9 @@ tags: compile |
356 | PYDOCTOR = pydoctor |
357 | PYDOCTOR_OPTIONS = |
358 | |
359 | +.PHONY: pydoctor |
360 | pydoctor: |
361 | $(PYDOCTOR) --make-html --html-output=apidocs --add-package=lib/lp \ |
362 | --add-package=lib/canonical --project-name=Launchpad \ |
363 | --docformat restructuredtext --verbose-about epytext-summary \ |
364 | $(PYDOCTOR_OPTIONS) |
365 | - |
366 | -.PHONY: apidoc build_eggs build_wheels check check_config \ |
367 | - clean clean_buildout clean_js clean_logs clean_pip compile \ |
368 | - css_combine debug default doc ftest_build ftest_inplace \ |
369 | - hosted_branches jsbuild jsbuild_widget_css launchpad.pot \ |
370 | - pydoctor realclean reload-apache run run-testapp runner schema \ |
371 | - sprite_css sprite_image start stop TAGS tags test_build \ |
372 | - test_inplace $(LP_JS_BUILD) |
373 | diff --git a/database/sampledata/current-dev.sql b/database/sampledata/current-dev.sql |
374 | index 1a40702..56cdf56 100644 |
375 | --- a/database/sampledata/current-dev.sql |
376 | +++ b/database/sampledata/current-dev.sql |
377 | @@ -3266,8 +3266,8 @@ ALTER TABLE codereviewvote ENABLE TRIGGER ALL; |
378 | |
379 | ALTER TABLE commercialsubscription DISABLE TRIGGER ALL; |
380 | |
381 | -INSERT INTO commercialsubscription (id, date_created, date_last_modified, date_starts, date_expires, status, product, registrant, purchaser, whiteboard, sales_system_id) VALUES (1, '2012-10-08 11:09:37.75983', '2012-10-08 11:09:37.75983', '2012-10-08 11:09:37.838229', '2022-01-01 00:00:00', 10, 16, 65, 65, 'Complimentary 30 day subscription. -- Launchpad 2012-10-08', 'complimentary-30-day-2012-10-08 11:09:37.838229+00:00'); |
382 | -INSERT INTO commercialsubscription (id, date_created, date_last_modified, date_starts, date_expires, status, product, registrant, purchaser, whiteboard, sales_system_id) VALUES (2, '2012-10-08 11:09:46.861812', '2012-10-08 11:09:46.861812', '2012-10-08 11:09:46.912691', '2022-01-01 00:00:00', 10, 17, 65, 65, 'Complimentary 30 day subscription. -- Launchpad 2012-10-08', 'complimentary-30-day-2012-10-08 11:09:46.912691+00:00'); |
383 | +INSERT INTO commercialsubscription (id, date_created, date_last_modified, date_starts, date_expires, status, product, registrant, purchaser, whiteboard, sales_system_id) VALUES (1, '2012-10-08 11:09:37.75983', '2012-10-08 11:09:37.75983', '2012-10-08 11:09:37.838229', '2032-01-01 00:00:00', 10, 16, 65, 65, 'Complimentary 30 day subscription. -- Launchpad 2012-10-08', 'complimentary-30-day-2012-10-08 11:09:37.838229+00:00'); |
384 | +INSERT INTO commercialsubscription (id, date_created, date_last_modified, date_starts, date_expires, status, product, registrant, purchaser, whiteboard, sales_system_id) VALUES (2, '2012-10-08 11:09:46.861812', '2012-10-08 11:09:46.861812', '2012-10-08 11:09:46.912691', '2032-01-01 00:00:00', 10, 17, 65, 65, 'Complimentary 30 day subscription. -- Launchpad 2012-10-08', 'complimentary-30-day-2012-10-08 11:09:46.912691+00:00'); |
385 | |
386 | |
387 | ALTER TABLE commercialsubscription ENABLE TRIGGER ALL; |
388 | diff --git a/database/sampledata/current.sql b/database/sampledata/current.sql |
389 | index 9e0310f..df050c1 100644 |
390 | --- a/database/sampledata/current.sql |
391 | +++ b/database/sampledata/current.sql |
392 | @@ -3198,8 +3198,8 @@ ALTER TABLE codereviewvote ENABLE TRIGGER ALL; |
393 | |
394 | ALTER TABLE commercialsubscription DISABLE TRIGGER ALL; |
395 | |
396 | -INSERT INTO commercialsubscription (id, date_created, date_last_modified, date_starts, date_expires, status, product, registrant, purchaser, whiteboard, sales_system_id) VALUES (1, '2012-10-08 11:09:37.75983', '2012-10-08 11:09:37.75983', '2012-10-08 11:09:37.838229', '2022-01-01 00:00:00', 10, 16, 65, 65, 'Complimentary 30 day subscription. -- Launchpad 2012-10-08', 'complimentary-30-day-2012-10-08 11:09:37.838229+00:00'); |
397 | -INSERT INTO commercialsubscription (id, date_created, date_last_modified, date_starts, date_expires, status, product, registrant, purchaser, whiteboard, sales_system_id) VALUES (2, '2012-10-08 11:09:46.861812', '2012-10-08 11:09:46.861812', '2012-10-08 11:09:46.912691', '2022-01-01 00:00:00', 10, 17, 65, 65, 'Complimentary 30 day subscription. -- Launchpad 2012-10-08', 'complimentary-30-day-2012-10-08 11:09:46.912691+00:00'); |
398 | +INSERT INTO commercialsubscription (id, date_created, date_last_modified, date_starts, date_expires, status, product, registrant, purchaser, whiteboard, sales_system_id) VALUES (1, '2012-10-08 11:09:37.75983', '2012-10-08 11:09:37.75983', '2012-10-08 11:09:37.838229', '2032-01-01 00:00:00', 10, 16, 65, 65, 'Complimentary 30 day subscription. -- Launchpad 2012-10-08', 'complimentary-30-day-2012-10-08 11:09:37.838229+00:00'); |
399 | +INSERT INTO commercialsubscription (id, date_created, date_last_modified, date_starts, date_expires, status, product, registrant, purchaser, whiteboard, sales_system_id) VALUES (2, '2012-10-08 11:09:46.861812', '2012-10-08 11:09:46.861812', '2012-10-08 11:09:46.912691', '2032-01-01 00:00:00', 10, 17, 65, 65, 'Complimentary 30 day subscription. -- Launchpad 2012-10-08', 'complimentary-30-day-2012-10-08 11:09:46.912691+00:00'); |
400 | |
401 | |
402 | ALTER TABLE commercialsubscription ENABLE TRIGGER ALL; |
403 | diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py |
404 | index af21a01..09fe6e4 100644 |
405 | --- a/lib/lp/_schema_circular_imports.py |
406 | +++ b/lib/lp/_schema_circular_imports.py |
407 | @@ -64,7 +64,10 @@ from lp.code.interfaces.codereviewcomment import ICodeReviewComment |
408 | from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference |
409 | from lp.code.interfaces.diff import IPreviewDiff |
410 | from lp.code.interfaces.gitref import IGitRef |
411 | -from lp.code.interfaces.gitrepository import IGitRepository |
412 | +from lp.code.interfaces.gitrepository import ( |
413 | + IGitRepository, |
414 | + IRevisionStatusReport, |
415 | + ) |
416 | from lp.code.interfaces.gitrule import ( |
417 | IGitNascentRule, |
418 | IGitNascentRuleGrant, |
419 | @@ -517,6 +520,8 @@ patch_entry_return_type(IGitRepository, 'subscribe', IGitSubscription) |
420 | patch_entry_return_type(IGitRepository, 'getSubscription', IGitSubscription) |
421 | patch_reference_property(IGitRepository, 'code_import', ICodeImport) |
422 | patch_entry_return_type(IGitRepository, 'getRefByPath', IGitRef) |
423 | +patch_collection_return_type( |
424 | + IGitRepository, 'getStatusReports', IRevisionStatusReport) |
425 | patch_collection_property( |
426 | IGitRepository, '_api_landing_targets', IBranchMergeProposal) |
427 | patch_collection_property( |
428 | @@ -528,6 +533,10 @@ patch_collection_return_type( |
429 | patch_list_parameter_type( |
430 | IGitRepository, 'setRules', 'rules', InlineObject(schema=IGitNascentRule)) |
431 | |
432 | +# IRevisionStatusReport |
433 | +patch_reference_property( |
434 | + IRevisionStatusReport, 'git_repository', IGitRepository) |
435 | + |
436 | # ILiveFSFile |
437 | patch_reference_property(ILiveFSFile, 'livefsbuild', ILiveFSBuild) |
438 | |
439 | diff --git a/lib/lp/archivepublisher/publishing.py b/lib/lp/archivepublisher/publishing.py |
440 | index f18ca9f..a5a0cc4 100644 |
441 | --- a/lib/lp/archivepublisher/publishing.py |
442 | +++ b/lib/lp/archivepublisher/publishing.py |
443 | @@ -1235,8 +1235,10 @@ class Publisher: |
444 | release_file["Components"] = " ".join( |
445 | reorder_components(all_components)) |
446 | release_file["Description"] = drsummary |
447 | - if (pocket == PackagePublishingPocket.BACKPORTS and |
448 | - distroseries.backports_not_automatic): |
449 | + if ((pocket == PackagePublishingPocket.BACKPORTS and |
450 | + distroseries.backports_not_automatic) or |
451 | + (pocket == PackagePublishingPocket.PROPOSED and |
452 | + distroseries.proposed_not_automatic)): |
453 | release_file["NotAutomatic"] = "yes" |
454 | release_file["ButAutomaticUpgrades"] = "yes" |
455 | |
456 | diff --git a/lib/lp/archivepublisher/tests/test_publisher.py b/lib/lp/archivepublisher/tests/test_publisher.py |
457 | index 6ff8cb2..c0891dd 100644 |
458 | --- a/lib/lp/archivepublisher/tests/test_publisher.py |
459 | +++ b/lib/lp/archivepublisher/tests/test_publisher.py |
460 | @@ -116,6 +116,7 @@ from lp.testing.matchers import FileContainsBytes |
461 | |
462 | |
463 | RELEASE = PackagePublishingPocket.RELEASE |
464 | +PROPOSED = PackagePublishingPocket.PROPOSED |
465 | BACKPORTS = PackagePublishingPocket.BACKPORTS |
466 | |
467 | |
468 | @@ -2064,6 +2065,45 @@ class TestPublisher(TestPublisherBase): |
469 | self.assertIn("NotAutomatic: yes", get_release(BACKPORTS)) |
470 | self.assertIn("ButAutomaticUpgrades: yes", get_release(BACKPORTS)) |
471 | |
472 | + def testReleaseFileForNotAutomaticProposed(self): |
473 | + # Test Release file writing for series with NotAutomatic -proposed. |
474 | + publisher = Publisher( |
475 | + self.logger, self.config, self.disk_pool, |
476 | + self.ubuntutest.main_archive) |
477 | + self.getPubSource(filecontent=b'Hello world', pocket=RELEASE) |
478 | + self.getPubSource(filecontent=b'Hello world', pocket=PROPOSED) |
479 | + |
480 | + # Make everything other than breezy-autotest OBSOLETE so that they |
481 | + # aren't republished. |
482 | + for series in self.ubuntutest.series: |
483 | + if series.name != "breezy-autotest": |
484 | + series.status = SeriesStatus.OBSOLETE |
485 | + |
486 | + publisher.A_publish(True) |
487 | + publisher.C_writeIndexes(False) |
488 | + |
489 | + def get_release(pocket): |
490 | + release_path = os.path.join( |
491 | + publisher._config.distsroot, |
492 | + 'breezy-autotest%s' % pocketsuffix[pocket], 'Release') |
493 | + with open(release_path) as release_file: |
494 | + return release_file.read().splitlines() |
495 | + |
496 | + # When proposed_not_automatic is unset, no Release files have |
497 | + # NotAutomatic: yes. |
498 | + self.assertEqual(False, self.breezy_autotest.proposed_not_automatic) |
499 | + publisher.D_writeReleaseFiles(False) |
500 | + self.assertNotIn("NotAutomatic: yes", get_release(RELEASE)) |
501 | + self.assertNotIn("NotAutomatic: yes", get_release(PROPOSED)) |
502 | + |
503 | + # But with the flag set, -proposed Release files gain |
504 | + # NotAutomatic: yes and ButAutomaticUpgrades: yes. |
505 | + self.breezy_autotest.proposed_not_automatic = True |
506 | + publisher.D_writeReleaseFiles(False) |
507 | + self.assertNotIn("NotAutomatic: yes", get_release(RELEASE)) |
508 | + self.assertIn("NotAutomatic: yes", get_release(PROPOSED)) |
509 | + self.assertIn("ButAutomaticUpgrades: yes", get_release(PROPOSED)) |
510 | + |
511 | def testReleaseFileForI18n(self): |
512 | """Test Release file writing for translated package descriptions.""" |
513 | publisher = Publisher( |
514 | diff --git a/lib/lp/code/browser/configure.zcml b/lib/lp/code/browser/configure.zcml |
515 | index f7bab39..31b3938 100644 |
516 | --- a/lib/lp/code/browser/configure.zcml |
517 | +++ b/lib/lp/code/browser/configure.zcml |
518 | @@ -963,6 +963,12 @@ |
519 | factory="lp.code.browser.gitrepository.GitRepositoryBreadcrumb" |
520 | permission="zope.Public"/> |
521 | |
522 | + <browser:url |
523 | + for="lp.code.interfaces.gitrepository.IRevisionStatusReport" |
524 | + path_expression="string:+status/${id}" |
525 | + attribute_to_parent="git_repository" |
526 | + rootsite="code"/> |
527 | + |
528 | <browser:defaultView |
529 | for="lp.code.interfaces.gitref.IGitRef" |
530 | name="+index"/> |
531 | diff --git a/lib/lp/code/browser/gitrepository.py b/lib/lp/code/browser/gitrepository.py |
532 | index 92a1b14..ddd79ca 100644 |
533 | --- a/lib/lp/code/browser/gitrepository.py |
534 | +++ b/lib/lp/code/browser/gitrepository.py |
535 | @@ -106,6 +106,7 @@ from lp.code.interfaces.gitrepository import ( |
536 | ContributorGitIdentity, |
537 | IGitRepository, |
538 | IGitRepositorySet, |
539 | + IRevisionStatusReportSet, |
540 | ) |
541 | from lp.code.vocabularies.gitrule import GitPermissionsVocabulary |
542 | from lp.registry.interfaces.person import ( |
543 | @@ -190,6 +191,18 @@ class GitRepositoryNavigation(WebhookTargetNavigationMixin, Navigation): |
544 | |
545 | usedfor = IGitRepository |
546 | |
547 | + @stepthrough('+status') |
548 | + def traverse_status(self, id): |
549 | + try: |
550 | + report_id = int(id) |
551 | + except ValueError: |
552 | + raise NotFoundError(report_id) |
553 | + report = getUtility( |
554 | + IRevisionStatusReportSet).getByID(report_id) |
555 | + if report is None: |
556 | + raise NotFoundError(report_id) |
557 | + return report |
558 | + |
559 | @stepto("+ref") |
560 | def traverse_ref(self): |
561 | segments = list(self.request.getTraversalStack()) |
562 | diff --git a/lib/lp/code/configure.zcml b/lib/lp/code/configure.zcml |
563 | index 6417c89..fb8e0d5 100644 |
564 | --- a/lib/lp/code/configure.zcml |
565 | +++ b/lib/lp/code/configure.zcml |
566 | @@ -953,6 +953,40 @@ |
567 | <allow interface="lp.code.interfaces.gitref.IGitRefRemoteSet" /> |
568 | </securedutility> |
569 | |
570 | + <!-- RevisionStatusReport --> |
571 | + |
572 | + <class class="lp.code.model.gitrepository.RevisionStatusReport"> |
573 | + <require |
574 | + permission="launchpad.View" |
575 | + interface="lp.code.interfaces.gitrepository.IRevisionStatusReportView |
576 | + lp.code.interfaces.gitrepository.IRevisionStatusReportEditableAttributes" /> |
577 | + <require |
578 | + permission="launchpad.Edit" |
579 | + interface="lp.code.interfaces.gitrepository.IRevisionStatusReportEdit" |
580 | + set_schema="lp.code.interfaces.gitrepository.IRevisionStatusReportEditableAttributes" /> |
581 | + </class> |
582 | + <class class="lp.code.model.gitrepository.RevisionStatusArtifact"> |
583 | + <require |
584 | + permission="launchpad.View" |
585 | + interface="lp.code.interfaces.gitrepository.IRevisionStatusArtifact" /> |
586 | + </class> |
587 | + <class class="lp.code.model.gitrepository.RevisionStatusReportSet"> |
588 | + <allow interface="lp.code.interfaces.gitrepository.IRevisionStatusReportSet" /> |
589 | + </class> |
590 | + <securedutility |
591 | + class="lp.code.model.gitrepository.RevisionStatusReportSet" |
592 | + provides="lp.code.interfaces.gitrepository.IRevisionStatusReportSet"> |
593 | + <allow interface="lp.code.interfaces.gitrepository.IRevisionStatusReportSet" /> |
594 | + </securedutility> |
595 | + <class class="lp.code.model.gitrepository.RevisionStatusArtifactSet"> |
596 | + <allow interface="lp.code.interfaces.gitrepository.IRevisionStatusArtifactSet" /> |
597 | + </class> |
598 | + <securedutility |
599 | + class="lp.code.model.gitrepository.RevisionStatusArtifactSet" |
600 | + provides="lp.code.interfaces.gitrepository.IRevisionStatusArtifactSet"> |
601 | + <allow interface="lp.code.interfaces.gitrepository.IRevisionStatusArtifactSet" /> |
602 | + </securedutility> |
603 | + |
604 | <!-- Git repository access rules --> |
605 | |
606 | <class class="lp.code.model.gitrule.GitRule"> |
607 | diff --git a/lib/lp/code/enums.py b/lib/lp/code/enums.py |
608 | index 3765754..2dbd287 100644 |
609 | --- a/lib/lp/code/enums.py |
610 | +++ b/lib/lp/code/enums.py |
611 | @@ -29,6 +29,8 @@ __all__ = [ |
612 | 'GitRepositoryType', |
613 | 'NON_CVS_RCS_TYPES', |
614 | 'RevisionControlSystems', |
615 | + 'RevisionStatusArtifactType', |
616 | + 'RevisionStatusResult', |
617 | 'TargetRevisionControlSystems', |
618 | ] |
619 | |
620 | @@ -252,6 +254,54 @@ class GitPermissionType(EnumeratedType): |
621 | CAN_FORCE_PUSH = Item("Can force-push") |
622 | |
623 | |
624 | +class RevisionStatusArtifactType(DBEnumeratedType): |
625 | + LOG = DBItem(0, """ |
626 | + Log |
627 | + |
628 | + The log produced by the check job. |
629 | + """) |
630 | + |
631 | + |
632 | +class RevisionStatusResult(DBEnumeratedType): |
633 | + """Revision Status Result""" |
634 | + |
635 | + WAITING = DBItem(0, """ |
636 | + Waiting |
637 | + |
638 | + The check job is waiting to be run. |
639 | + """) |
640 | + |
641 | + RUNNING = DBItem(1, """ |
642 | + Running |
643 | + |
644 | + The check job is currently running. |
645 | + """) |
646 | + |
647 | + SUCCEEDED = DBItem(2, """ |
648 | + Succeeded |
649 | + |
650 | + The check job ran successfully. |
651 | + """) |
652 | + |
653 | + FAILED = DBItem(3, """ |
654 | + Failed |
655 | + |
656 | + The check job failed. |
657 | + """) |
658 | + |
659 | + SKIPPED = DBItem(4, """ |
660 | + Skipped |
661 | + |
662 | + The check job was skipped. |
663 | + """) |
664 | + |
665 | + CANCELLED = DBItem(5, """ |
666 | + Cancelled |
667 | + |
668 | + The check job was cancelled. |
669 | + """) |
670 | + |
671 | + |
672 | class BranchLifecycleStatusFilter(EnumeratedType): |
673 | """Branch Lifecycle Status Filter |
674 | |
675 | diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py |
676 | index cb12a5d..aae1b90 100644 |
677 | --- a/lib/lp/code/interfaces/gitrepository.py |
678 | +++ b/lib/lp/code/interfaces/gitrepository.py |
679 | @@ -13,9 +13,15 @@ __all__ = [ |
680 | 'IGitRepositoryExpensiveRequest', |
681 | 'IGitRepositorySet', |
682 | 'IHasGitRepositoryURL', |
683 | + 'IRevisionStatusArtifact', |
684 | + 'IRevisionStatusArtifactSet', |
685 | + 'IRevisionStatusReport', |
686 | + 'IRevisionStatusReportSet', |
687 | + 'RevisionStatusReportsFeatureDisabled', |
688 | 'user_has_special_git_repository_access', |
689 | ] |
690 | |
691 | +import http.client |
692 | import re |
693 | from textwrap import dedent |
694 | |
695 | @@ -23,6 +29,7 @@ from lazr.lifecycle.snapshot import doNotSnapshot |
696 | from lazr.restful.declarations import ( |
697 | call_with, |
698 | collection_default_content, |
699 | + error_status, |
700 | export_destructor_operation, |
701 | export_factory_operation, |
702 | export_operation_as, |
703 | @@ -37,6 +44,7 @@ from lazr.restful.declarations import ( |
704 | operation_returns_collection_of, |
705 | operation_returns_entry, |
706 | REQUEST_USER, |
707 | + scoped, |
708 | ) |
709 | from lazr.restful.fields import ( |
710 | CollectionField, |
711 | @@ -50,6 +58,7 @@ from zope.interface import ( |
712 | ) |
713 | from zope.schema import ( |
714 | Bool, |
715 | + Bytes, |
716 | Choice, |
717 | Datetime, |
718 | Int, |
719 | @@ -57,10 +66,12 @@ from zope.schema import ( |
720 | Text, |
721 | TextLine, |
722 | ) |
723 | +from zope.security.interfaces import Unauthorized |
724 | |
725 | from lp import _ |
726 | from lp.app.enums import InformationType |
727 | from lp.app.validators import LaunchpadValidationError |
728 | +from lp.app.validators.attachment import attachment_size_constraint |
729 | from lp.code.enums import ( |
730 | BranchMergeProposalStatus, |
731 | BranchSubscriptionDiffSize, |
732 | @@ -69,6 +80,8 @@ from lp.code.enums import ( |
733 | GitListingSort, |
734 | GitRepositoryStatus, |
735 | GitRepositoryType, |
736 | + RevisionStatusArtifactType, |
737 | + RevisionStatusResult, |
738 | ) |
739 | from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository |
740 | from lp.code.interfaces.hasgitrepositories import IHasGitRepositories |
741 | @@ -91,6 +104,7 @@ from lp.services.fields import ( |
742 | InlineObject, |
743 | PersonChoice, |
744 | PublicPersonChoice, |
745 | + URIField, |
746 | ) |
747 | from lp.services.webhooks.interfaces import IWebhookTarget |
748 | |
749 | @@ -813,6 +827,155 @@ class IGitRepositoryExpensiveRequest(Interface): |
750 | that is not an admin or a registry expert.""" |
751 | |
752 | |
753 | +@error_status(http.client.UNAUTHORIZED) |
754 | +class RevisionStatusReportsFeatureDisabled(Unauthorized): |
755 | + """Only certain users can access APIs for revision status reports.""" |
756 | + |
757 | + def __init__(self): |
758 | + super(RevisionStatusReportsFeatureDisabled, self).__init__( |
759 | + "You do not have permission to create revision status reports") |
760 | + |
761 | + |
762 | +class IRevisionStatusReportView(Interface): |
763 | + """`IRevisionStatusReport` attributes that require launchpad.View.""" |
764 | + |
765 | + id = Int(title=_("ID"), required=True, readonly=True) |
766 | + |
767 | + date_created = exported(Datetime( |
768 | + title=_("When the report was created."), required=True, readonly=True)) |
769 | + date_started = exported(Datetime( |
770 | + title=_("When the report was started.")), readonly=False) |
771 | + date_finished = exported(Datetime( |
772 | + title=_("When the report has finished.")), readonly=False) |
773 | + |
774 | + |
775 | +class IRevisionStatusReportEditableAttributes(Interface): |
776 | + """`IRevisionStatusReport` attributes that can be edited. |
777 | + |
778 | + These attributes need launchpad.View to see, and launchpad.Edit to change. |
779 | + """ |
780 | + |
781 | + title = exported(TextLine( |
782 | + title=_("A short title for the report."), required=True)) |
783 | + |
784 | + git_repository = exported(Reference( |
785 | + title=_("The Git repository for which this report is built."), |
786 | + # Really IGitRepository, patched in _schema_circular_imports.py. |
787 | + schema=Interface, required=True, readonly=True)) |
788 | + |
789 | + commit_sha1 = exported(TextLine( |
790 | + title=_("The Git commit for which this report is built."), |
791 | + required=True, readonly=True)) |
792 | + |
793 | + url = exported(URIField(title=_("URL"), required=False, readonly=True, |
794 | + description=_("The external url of the report."))) |
795 | + |
796 | + result_summary = exported(TextLine( |
797 | + title=_("A short summary of the result."), required=False)) |
798 | + |
799 | + result = exported(Choice( |
800 | + title=_('Result of the report'), readonly=True, |
801 | + required=False, vocabulary=RevisionStatusResult)) |
802 | + |
803 | + @mutator_for(result) |
804 | + @operation_parameters(result=copy_field(result)) |
805 | + @export_write_operation() |
806 | + @operation_for_version("devel") |
807 | + def transitionToNewResult(result): |
808 | + """Set the RevisionStatusReport result. |
809 | + |
810 | + Set the revision status report result.""" |
811 | + |
812 | + |
813 | +class IRevisionStatusReportEdit(Interface): |
814 | + """`IRevisionStatusReport` attributes that require launchpad.Edit.""" |
815 | + |
816 | + @operation_parameters( |
817 | + log_data=Bytes(title=_("The content of the artifact in bytes."), |
818 | + constraint=attachment_size_constraint)) |
819 | + @scoped(AccessTokenScope.REPOSITORY_BUILD_STATUS.title) |
820 | + @export_write_operation() |
821 | + @export_operation_as(name="setLog") |
822 | + @operation_for_version("devel") |
823 | + def api_setLog(log_data): |
824 | + """Set a new log on an existing status report. |
825 | + |
826 | + :param log_data: The contents (in bytes) of the log. |
827 | + """ |
828 | + |
829 | + |
830 | +@exported_as_webservice_entry(as_of="beta") |
831 | +class IRevisionStatusReport(IRevisionStatusReportView, |
832 | + IRevisionStatusReportEditableAttributes, |
833 | + IRevisionStatusReportEdit): |
834 | + """An revision status report for a Git commit.""" |
835 | + |
836 | + |
837 | +class IRevisionStatusReportSet(Interface): |
838 | + """The set of all revision status reports.""" |
839 | + |
840 | + def new(creator, title, git_repository, commit_sha1, date_created=None, |
841 | + url=None, result_summary=None, result=None, date_started=None, |
842 | + date_finished=None, log=None): |
843 | + """Return a new revision status report. |
844 | + |
845 | + :param title: A text string. |
846 | + :param git_repository: An `IGitRepository` for which the report |
847 | + is being created. |
848 | + :param commit_sha1: The sha1 of the commit for which the report |
849 | + is being created. |
850 | + :param date_created: The date when the report is being created. |
851 | + :param url: External URL to view result of report. |
852 | + :param result_summary: A short summary of the result. |
853 | + :param result: The result of the check job for this revision. |
854 | + :param date_started: DateTime that report was started. |
855 | + :param date_finished: DateTime that report was completed. |
856 | + :param log: Stores the content of the artifact for this report. |
857 | + """ |
858 | + |
859 | + def getByID(id): |
860 | + """Returns the RevisionStatusReport for a given ID.""" |
861 | + |
862 | + def findByRepository(repository): |
863 | + """Returns all `RevisionStatusReport` for a repository.""" |
864 | + |
865 | + def findByCommit(repository, commit_sha1): |
866 | + """Returns all `RevisionStatusReport` for a repository and commit.""" |
867 | + |
868 | + |
869 | +class IRevisionStatusArtifactSet(Interface): |
870 | + """The set of all revision status artifacts.""" |
871 | + |
872 | + def new(lfa, report): |
873 | + """Return a new revision status artifact. |
874 | + |
875 | + :param lfa: An `ILibraryFileAlias`. |
876 | + :param report: An `IRevisionStatusReport` for which the |
877 | + artifact is being created. |
878 | + """ |
879 | + |
880 | + def getByID(id): |
881 | + """Returns the RevisionStatusArtifact for a given ID.""" |
882 | + |
883 | + def findByReport(report): |
884 | + """Returns the set of artifacts for a given report.""" |
885 | + |
886 | + |
887 | +class IRevisionStatusArtifact(Interface): |
888 | + id = Int(title=_("ID"), required=True, readonly=True) |
889 | + |
890 | + report = Attribute( |
891 | + "The `RevisionStatusReport` that this artifact is linked to.") |
892 | + |
893 | + library_file = Attribute( |
894 | + "The `LibraryFileAlias` object containing information for " |
895 | + "a revision status report.") |
896 | + |
897 | + artifact_type = Choice( |
898 | + title=_('The type of artifact, only log for now.'), |
899 | + vocabulary=RevisionStatusArtifactType) |
900 | + |
901 | + |
902 | class IGitRepositoryEdit(IWebhookTarget, IAccessTokenTarget): |
903 | """IGitRepository methods that require launchpad.Edit permission.""" |
904 | |
905 | @@ -1015,6 +1178,38 @@ class IGitRepositoryEdit(IWebhookTarget, IAccessTokenTarget): |
906 | :raise: CannotDeleteGitRepository if the repository cannot be deleted. |
907 | """ |
908 | |
909 | + @operation_parameters( |
910 | + title=copy_field(IRevisionStatusReport["title"]), |
911 | + commit_sha1=copy_field(IRevisionStatusReport["commit_sha1"]), |
912 | + url=copy_field(IRevisionStatusReport["url"]), |
913 | + result_summary=copy_field(IRevisionStatusReport["result_summary"]), |
914 | + result=copy_field(IRevisionStatusReport["result"])) |
915 | + @scoped(AccessTokenScope.REPOSITORY_BUILD_STATUS.title) |
916 | + @call_with(user=REQUEST_USER) |
917 | + @export_factory_operation(IRevisionStatusReport, []) |
918 | + @operation_for_version("devel") |
919 | + def newStatusReport(title, commit_sha1, url, result_summary, result, user): |
920 | + """Create a new status report. |
921 | + |
922 | + :param title: The name of the new report. |
923 | + :param commit_sha1: The commit sha1 for the report. |
924 | + :param url: The external link of the status report. |
925 | + :param result_summary: The description of the new report. |
926 | + :param result: The result of the new report. |
927 | + """ |
928 | + |
929 | + @operation_parameters( |
930 | + commit_sha1=copy_field(IRevisionStatusReport["commit_sha1"])) |
931 | + @scoped(AccessTokenScope.REPOSITORY_BUILD_STATUS.title) |
932 | + @operation_returns_collection_of(Interface) |
933 | + @export_read_operation() |
934 | + @operation_for_version("devel") |
935 | + def getStatusReports(commit_sha1): |
936 | + """Retrieves the list of reports that exist for a commit. |
937 | + |
938 | + :param commit_sha1: The commit sha1 for the report. |
939 | + """ |
940 | + |
941 | |
942 | # XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL |
943 | # generation working. Individual attributes must set their version to |
944 | diff --git a/lib/lp/code/interfaces/webservice.py b/lib/lp/code/interfaces/webservice.py |
945 | index 02546e9..bd94298 100644 |
946 | --- a/lib/lp/code/interfaces/webservice.py |
947 | +++ b/lib/lp/code/interfaces/webservice.py |
948 | @@ -31,6 +31,7 @@ __all__ = [ |
949 | 'IGitSubscription', |
950 | 'IHasGitRepositories', |
951 | 'IPreviewDiff', |
952 | + 'IRevisionStatusReport', |
953 | 'ISourcePackageRecipe', |
954 | 'ISourcePackageRecipeBuild', |
955 | ] |
956 | @@ -66,6 +67,7 @@ from lp.code.interfaces.gitref import IGitRef |
957 | from lp.code.interfaces.gitrepository import ( |
958 | IGitRepository, |
959 | IGitRepositorySet, |
960 | + IRevisionStatusReport, |
961 | ) |
962 | from lp.code.interfaces.gitsubscription import IGitSubscription |
963 | from lp.code.interfaces.hasgitrepositories import IHasGitRepositories |
964 | diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py |
965 | index 812f1d7..43a005a 100644 |
966 | --- a/lib/lp/code/model/gitrepository.py |
967 | +++ b/lib/lp/code/model/gitrepository.py |
968 | @@ -6,6 +6,7 @@ __all__ = [ |
969 | 'GitRepository', |
970 | 'GitRepositorySet', |
971 | 'parse_git_commits', |
972 | + 'RevisionStatusReport', |
973 | ] |
974 | |
975 | from collections import ( |
976 | @@ -19,6 +20,7 @@ from datetime import ( |
977 | import email |
978 | from fnmatch import fnmatch |
979 | from functools import partial |
980 | +import io |
981 | from itertools import ( |
982 | chain, |
983 | groupby, |
984 | @@ -102,6 +104,8 @@ from lp.code.enums import ( |
985 | GitPermissionType, |
986 | GitRepositoryStatus, |
987 | GitRepositoryType, |
988 | + RevisionStatusArtifactType, |
989 | + RevisionStatusResult, |
990 | ) |
991 | from lp.code.errors import ( |
992 | CannotDeleteGitRepository, |
993 | @@ -131,6 +135,10 @@ from lp.code.interfaces.gitrepository import ( |
994 | GitIdentityMixin, |
995 | IGitRepository, |
996 | IGitRepositorySet, |
997 | + IRevisionStatusArtifactSet, |
998 | + IRevisionStatusReport, |
999 | + IRevisionStatusReportSet, |
1000 | + RevisionStatusReportsFeatureDisabled, |
1001 | user_has_special_git_repository_access, |
1002 | ) |
1003 | from lp.code.interfaces.gitrule import ( |
1004 | @@ -205,6 +213,7 @@ from lp.services.identity.interfaces.account import ( |
1005 | ) |
1006 | from lp.services.job.interfaces.job import JobStatus |
1007 | from lp.services.job.model.job import Job |
1008 | +from lp.services.librarian.interfaces import ILibraryFileAliasSet |
1009 | from lp.services.macaroons.interfaces import IMacaroonIssuer |
1010 | from lp.services.macaroons.model import MacaroonIssuerBase |
1011 | from lp.services.mail.notificationrecipientset import NotificationRecipientSet |
1012 | @@ -219,8 +228,9 @@ from lp.services.webhooks.model import WebhookTargetMixin |
1013 | from lp.snappy.interfaces.snap import ISnapSet |
1014 | |
1015 | |
1016 | -logger = logging.getLogger(__name__) |
1017 | +REVISION_STATUS_REPORT_ALLOW_CREATE = 'revision_status_report.allow_create' |
1018 | |
1019 | +logger = logging.getLogger(__name__) |
1020 | |
1021 | object_type_map = { |
1022 | "commit": GitObjectType.COMMIT, |
1023 | @@ -292,6 +302,139 @@ def git_repository_modified(repository, event): |
1024 | send_git_repository_modified_notifications(repository, event) |
1025 | |
1026 | |
1027 | +@implementer(IRevisionStatusReport) |
1028 | +class RevisionStatusReport(StormBase): |
1029 | + __storm_table__ = 'RevisionStatusReport' |
1030 | + |
1031 | + id = Int(primary=True) |
1032 | + |
1033 | + creator_id = Int(name="creator", allow_none=False) |
1034 | + creator = Reference(creator_id, "Person.id") |
1035 | + |
1036 | + title = Unicode(name='name', allow_none=False) |
1037 | + |
1038 | + git_repository_id = Int(name='git_repository', allow_none=False) |
1039 | + git_repository = Reference(git_repository_id, 'GitRepository.id') |
1040 | + |
1041 | + commit_sha1 = Unicode(name='commit_sha1', allow_none=False) |
1042 | + |
1043 | + url = Unicode(name='url', allow_none=True) |
1044 | + |
1045 | + result_summary = Unicode(name='description', allow_none=True) |
1046 | + |
1047 | + result = DBEnum(name='result', allow_none=True, enum=RevisionStatusResult) |
1048 | + |
1049 | + date_created = DateTime( |
1050 | + name='date_created', tzinfo=pytz.UTC, allow_none=False) |
1051 | + |
1052 | + date_started = DateTime(name='date_started', tzinfo=pytz.UTC, |
1053 | + allow_none=True) |
1054 | + date_finished = DateTime(name='date_finished', tzinfo=pytz.UTC, |
1055 | + allow_none=True) |
1056 | + |
1057 | + def __init__(self, git_repository, user, title, commit_sha1, |
1058 | + url, result_summary, result): |
1059 | + super().__init__() |
1060 | + self.creator = user |
1061 | + self.git_repository = git_repository |
1062 | + self.title = title |
1063 | + self.commit_sha1 = commit_sha1 |
1064 | + self.url = url |
1065 | + self.result_summary = result_summary |
1066 | + self.result = result |
1067 | + self.date_created = UTC_NOW |
1068 | + |
1069 | + def api_setLog(self, log_data): |
1070 | + filename = '%s-%s.txt' % (self.title, self.commit_sha1) |
1071 | + |
1072 | + lfa = getUtility(ILibraryFileAliasSet).create( |
1073 | + name=filename, size=len(log_data), |
1074 | + file=io.BytesIO(log_data), contentType='text/plain') |
1075 | + |
1076 | + getUtility(IRevisionStatusArtifactSet).new(lfa, self) |
1077 | + |
1078 | + def transitionToNewResult(self, result): |
1079 | + if self.result == RevisionStatusResult.WAITING: |
1080 | + if result == RevisionStatusResult.RUNNING: |
1081 | + self.date_started == UTC_NOW |
1082 | + else: |
1083 | + self.date_finished = UTC_NOW |
1084 | + self.result = result |
1085 | + |
1086 | + |
1087 | +@implementer(IRevisionStatusReportSet) |
1088 | +class RevisionStatusReportSet: |
1089 | + |
1090 | + def new(self, creator, title, git_repository, commit_sha1, |
1091 | + url=None, result_summary=None, result=None, |
1092 | + date_started=None, date_finished=None, log=None): |
1093 | + """See `IRevisionStatusReportSet`.""" |
1094 | + store = IStore(RevisionStatusReport) |
1095 | + report = RevisionStatusReport(git_repository, creator, title, |
1096 | + commit_sha1, url, result_summary, |
1097 | + result) |
1098 | + store.add(report) |
1099 | + return report |
1100 | + |
1101 | + def getByID(self, id): |
1102 | + return IStore( |
1103 | + RevisionStatusReport).find(RevisionStatusReport, id=id).one() |
1104 | + |
1105 | + def findByRepository(self, repository): |
1106 | + return IStore(RevisionStatusReport).find( |
1107 | + RevisionStatusReport, |
1108 | + RevisionStatusReport.git_repository == repository) |
1109 | + |
1110 | + def findByCommit(self, repository, commit_sha1): |
1111 | + """Returns all `RevisionStatusReport` for a repository and commit.""" |
1112 | + return IStore(RevisionStatusReport).find( |
1113 | + RevisionStatusReport, |
1114 | + git_repository=repository, |
1115 | + commit_sha1=commit_sha1) |
1116 | + |
1117 | + |
1118 | +class RevisionStatusArtifact(StormBase): |
1119 | + __storm_table__ = 'RevisionStatusArtifact' |
1120 | + |
1121 | + id = Int(primary=True) |
1122 | + |
1123 | + library_file_id = Int(name='library_file', allow_none=False) |
1124 | + library_file = Reference(library_file_id, 'LibraryFileAlias.id') |
1125 | + |
1126 | + report_id = Int(name='report', allow_none=False) |
1127 | + report = Reference(report_id, 'RevisionStatusReport.id') |
1128 | + |
1129 | + artifact_type = DBEnum(name='type', allow_none=False, |
1130 | + enum=RevisionStatusArtifactType) |
1131 | + |
1132 | + def __init__(self, library_file, report): |
1133 | + super().__init__() |
1134 | + self.library_file = library_file |
1135 | + self.report = report |
1136 | + self.artifact_type = RevisionStatusArtifactType.LOG |
1137 | + |
1138 | + |
1139 | +@implementer(IRevisionStatusArtifactSet) |
1140 | +class RevisionStatusArtifactSet: |
1141 | + |
1142 | + def new(self, lfa, report): |
1143 | + """See `IRevisionStatusArtifactSet`.""" |
1144 | + store = IStore(RevisionStatusArtifact) |
1145 | + artifact = RevisionStatusArtifact(lfa, report) |
1146 | + store.add(artifact) |
1147 | + return artifact |
1148 | + |
1149 | + def getById(self, id): |
1150 | + return IStore(RevisionStatusArtifact).find( |
1151 | + RevisionStatusArtifact, |
1152 | + RevisionStatusArtifact.id == id).one() |
1153 | + |
1154 | + def findByReport(self, report): |
1155 | + return IStore(RevisionStatusArtifact).find( |
1156 | + RevisionStatusArtifact, |
1157 | + RevisionStatusArtifact.report == report) |
1158 | + |
1159 | + |
1160 | @implementer(IGitRepository, IHasOwner, IPrivacy, IInformationType) |
1161 | class GitRepository(StormBase, WebhookTargetMixin, AccessTokenTargetMixin, |
1162 | GitIdentityMixin): |
1163 | @@ -501,6 +644,20 @@ class GitRepository(StormBase, WebhookTargetMixin, AccessTokenTargetMixin, |
1164 | def collectGarbage(self): |
1165 | getUtility(IGitHostingClient).collectGarbage(self.getInternalPath()) |
1166 | |
1167 | + def newStatusReport(self, user, title, commit_sha1, url=None, |
1168 | + result_summary=None, result=None): |
1169 | + |
1170 | + if not getFeatureFlag(REVISION_STATUS_REPORT_ALLOW_CREATE): |
1171 | + raise RevisionStatusReportsFeatureDisabled() |
1172 | + |
1173 | + report = RevisionStatusReport(self, user, title, commit_sha1, |
1174 | + url, result_summary, result) |
1175 | + return report |
1176 | + |
1177 | + def getStatusReports(self, commit_sha1): |
1178 | + return getUtility( |
1179 | + IRevisionStatusReportSet).findByCommit(self, commit_sha1) |
1180 | + |
1181 | @property |
1182 | def namespace(self): |
1183 | """See `IGitRepository`.""" |
1184 | diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py |
1185 | index 252b535..6a759be 100644 |
1186 | --- a/lib/lp/code/model/tests/test_gitrepository.py |
1187 | +++ b/lib/lp/code/model/tests/test_gitrepository.py |
1188 | @@ -10,6 +10,7 @@ from datetime import ( |
1189 | import email |
1190 | from functools import partial |
1191 | import hashlib |
1192 | +import io |
1193 | import json |
1194 | |
1195 | from breezy import urlutils |
1196 | @@ -65,6 +66,7 @@ from lp.code.enums import ( |
1197 | GitObjectType, |
1198 | GitRepositoryStatus, |
1199 | GitRepositoryType, |
1200 | + RevisionStatusResult, |
1201 | TargetRevisionControlSystems, |
1202 | ) |
1203 | from lp.code.errors import ( |
1204 | @@ -95,6 +97,8 @@ from lp.code.interfaces.gitrepository import ( |
1205 | IGitRepository, |
1206 | IGitRepositorySet, |
1207 | IGitRepositoryView, |
1208 | + IRevisionStatusArtifactSet, |
1209 | + IRevisionStatusReportSet, |
1210 | ) |
1211 | from lp.code.interfaces.gitrule import ( |
1212 | IGitNascentRule, |
1213 | @@ -121,6 +125,7 @@ from lp.code.model.gitrepository import ( |
1214 | DeletionCallable, |
1215 | DeletionOperation, |
1216 | GitRepository, |
1217 | + REVISION_STATUS_REPORT_ALLOW_CREATE, |
1218 | ) |
1219 | from lp.code.tests.helpers import GitHostingFixture |
1220 | from lp.code.xmlrpc.git import GitAPI |
1221 | @@ -571,6 +576,24 @@ class TestGitRepository(TestCaseWithFactory): |
1222 | self.assertThat(recorder2, HasQueryCount.byEquality(recorder1)) |
1223 | self.assertEqual(7, recorder1.count) |
1224 | |
1225 | + def test_findRevisionStatusReport(self): |
1226 | + repository = removeSecurityProxy(self.factory.makeGitRepository()) |
1227 | + title = self.factory.getUniqueUnicode('report-title') |
1228 | + commit_sha1 = hashlib.sha1(b"Some content").hexdigest() |
1229 | + result_summary = "120/120 tests passed" |
1230 | + |
1231 | + report = self.factory.makeRevisionStatusReport( |
1232 | + user=repository.owner, git_repository=repository, |
1233 | + title=title, commit_sha1=commit_sha1, |
1234 | + result_summary=result_summary, |
1235 | + result=RevisionStatusResult.SUCCEEDED) |
1236 | + |
1237 | + with person_logged_in(repository.owner): |
1238 | + result = getUtility( |
1239 | + IRevisionStatusReportSet).getByID( |
1240 | + report.id) |
1241 | + self.assertEqual(report, result) |
1242 | + |
1243 | |
1244 | class TestGitIdentityMixin(TestCaseWithFactory): |
1245 | """Test the defaults and identities provided by GitIdentityMixin.""" |
1246 | @@ -4241,6 +4264,126 @@ class TestGitRepositoryWebservice(TestCaseWithFactory): |
1247 | self.assertEqual( |
1248 | InformationType.PUBLIC, repository_db.information_type) |
1249 | |
1250 | + def test_newRevisionStatusReport_featureFlagDisabled(self): |
1251 | + repository = self.factory.makeGitRepository() |
1252 | + requester = repository.owner |
1253 | + webservice = webservice_for_person(None, default_api_version="devel") |
1254 | + with person_logged_in(requester): |
1255 | + repository_url = api_url(repository) |
1256 | + |
1257 | + secret, _ = self.factory.makeAccessToken( |
1258 | + owner=requester, target=repository, |
1259 | + scopes=[AccessTokenScope.REPOSITORY_BUILD_STATUS]) |
1260 | + header = {'Authorization': 'Token %s' % secret} |
1261 | + |
1262 | + response = webservice.named_post( |
1263 | + repository_url, "newStatusReport", |
1264 | + headers=header, title="CI", |
1265 | + commit_sha1=hashlib.sha1( |
1266 | + self.factory.getUniqueBytes()).hexdigest(), |
1267 | + url='https://launchpad.net/', |
1268 | + result_summary="120/120 tests passed", |
1269 | + result="Succeeded") |
1270 | + |
1271 | + self.assertEqual(401, response.status) |
1272 | + self.assertIn( |
1273 | + b'You do not have permission to create revision status reports', |
1274 | + response.body) |
1275 | + |
1276 | + def test_newRevisionStatusReport(self): |
1277 | + repository = self.factory.makeGitRepository() |
1278 | + requester = repository.owner |
1279 | + webservice = webservice_for_person(None, default_api_version="devel") |
1280 | + with person_logged_in(requester): |
1281 | + repository_url = api_url(repository) |
1282 | + |
1283 | + secret, _ = self.factory.makeAccessToken( |
1284 | + owner=requester, target=repository, |
1285 | + scopes=[AccessTokenScope.REPOSITORY_BUILD_STATUS]) |
1286 | + header = {'Authorization': 'Token %s' % secret} |
1287 | + |
1288 | + self.useFixture(FeatureFixture( |
1289 | + {REVISION_STATUS_REPORT_ALLOW_CREATE: "on"})) |
1290 | + |
1291 | + response = webservice.named_post( |
1292 | + repository_url, "newStatusReport", |
1293 | + headers=header, title="CI", |
1294 | + commit_sha1=hashlib.sha1( |
1295 | + self.factory.getUniqueBytes()).hexdigest(), |
1296 | + url='https://launchpad.net/', |
1297 | + result_summary="120/120 tests passed", |
1298 | + result="Succeeded") |
1299 | + self.assertEqual(201, response.status) |
1300 | + |
1301 | + with person_logged_in(requester): |
1302 | + results = getUtility( |
1303 | + IRevisionStatusReportSet).findByRepository(repository) |
1304 | + reports = list(results) |
1305 | + urls = [webservice.getAbsoluteUrl('%s/+status/%s' % ( |
1306 | + api_url(repository), |
1307 | + report.id)) for report in reports] |
1308 | + self.assertIn(response.getHeader("Location"), urls) |
1309 | + |
1310 | + def test_getRevisionStatusReports(self): |
1311 | + repository = self.factory.makeGitRepository() |
1312 | + repository2 = self.factory.makeGitRepository() |
1313 | + requester = repository.owner |
1314 | + title = self.factory.getUniqueUnicode('report-title') |
1315 | + result_summary = "120/120 tests passed" |
1316 | + commit_sha1s = [hashlib.sha1( |
1317 | + self.factory.getUniqueBytes()).hexdigest() for _ in range(2)] |
1318 | + |
1319 | + result_summary2 = "Lint" |
1320 | + title2 = "Invalid import in test_file.py" |
1321 | + |
1322 | + report1 = self.factory.makeRevisionStatusReport( |
1323 | + user=repository.owner, git_repository=repository, |
1324 | + title=title, commit_sha1=commit_sha1s[0], |
1325 | + result_summary=result_summary, |
1326 | + result=RevisionStatusResult.SUCCEEDED) |
1327 | + |
1328 | + report2 = self.factory.makeRevisionStatusReport( |
1329 | + user=repository.owner, git_repository=repository, |
1330 | + title=title2, commit_sha1=commit_sha1s[0], |
1331 | + result_summary=result_summary2, |
1332 | + result=RevisionStatusResult.FAILED) |
1333 | + |
1334 | + self.factory.makeRevisionStatusReport( |
1335 | + user=repository.owner, git_repository=repository, |
1336 | + title=title2, commit_sha1=commit_sha1s[1], |
1337 | + result_summary=result_summary2, |
1338 | + result=RevisionStatusResult.FAILED) |
1339 | + |
1340 | + self.factory.makeRevisionStatusReport( |
1341 | + user=repository.owner, git_repository=repository2, |
1342 | + title=title2, commit_sha1=commit_sha1s[0], |
1343 | + result_summary=result_summary2, |
1344 | + result=RevisionStatusResult.FAILED) |
1345 | + |
1346 | + webservice = webservice_for_person(None, default_api_version="devel") |
1347 | + with person_logged_in(requester): |
1348 | + repository_url = api_url(repository) |
1349 | + |
1350 | + secret, _ = self.factory.makeAccessToken( |
1351 | + owner=requester, target=repository, |
1352 | + scopes=[AccessTokenScope.REPOSITORY_BUILD_STATUS]) |
1353 | + header = {'Authorization': 'Token %s' % secret} |
1354 | + |
1355 | + self.useFixture(FeatureFixture( |
1356 | + {REVISION_STATUS_REPORT_ALLOW_CREATE: "on"})) |
1357 | + |
1358 | + response = webservice.named_get( |
1359 | + repository_url, "getStatusReports", |
1360 | + headers=header, |
1361 | + commit_sha1=commit_sha1s[0]) |
1362 | + self.assertEqual(200, response.status) |
1363 | + with person_logged_in(requester): |
1364 | + result = getUtility( |
1365 | + IRevisionStatusReportSet).findByCommit( |
1366 | + repository, commit_sha1s[0]) |
1367 | + |
1368 | + self.assertContentEqual([report1, report2], result) |
1369 | + |
1370 | def test_set_target(self): |
1371 | # The repository owner can move the repository to another target; |
1372 | # this redirects to the new location. |
1373 | @@ -4831,6 +4974,60 @@ class TestGitRepositoryWebservice(TestCaseWithFactory): |
1374 | response.body) |
1375 | |
1376 | |
1377 | +class TestRevisionStatusReportWebservice(TestCaseWithFactory): |
1378 | + layer = LaunchpadFunctionalLayer |
1379 | + |
1380 | + def setUp(self): |
1381 | + super(TestRevisionStatusReportWebservice, self).setUp() |
1382 | + self.repository = self.factory.makeGitRepository() |
1383 | + self.requester = self.repository.owner |
1384 | + title = self.factory.getUniqueUnicode('report-title') |
1385 | + commit_sha1 = hashlib.sha1(b"Some content").hexdigest() |
1386 | + result_summary = "120/120 tests passed" |
1387 | + |
1388 | + self.report = self.factory.makeRevisionStatusReport( |
1389 | + user=self.repository.owner, git_repository=self.repository, |
1390 | + title=title, commit_sha1=commit_sha1, |
1391 | + result_summary=result_summary, |
1392 | + result=RevisionStatusResult.SUCCEEDED) |
1393 | + |
1394 | + self.webservice = webservice_for_person( |
1395 | + None, default_api_version="devel") |
1396 | + with person_logged_in(self.requester): |
1397 | + self.report_url = api_url(self.report) |
1398 | + |
1399 | + secret, _ = self.factory.makeAccessToken( |
1400 | + owner=self.requester, target=self.repository, |
1401 | + scopes=[AccessTokenScope.REPOSITORY_BUILD_STATUS]) |
1402 | + self.header = {'Authorization': 'Token %s' % secret} |
1403 | + |
1404 | + def test_setLogOnRevisionStatusReport(self): |
1405 | + content = b'log_content_data' |
1406 | + filesize = len(content) |
1407 | + sha1 = hashlib.sha1(content).hexdigest() |
1408 | + md5 = hashlib.md5(content).hexdigest() |
1409 | + response = self.webservice.named_post( |
1410 | + self.report_url, "setLog", |
1411 | + headers=self.header, |
1412 | + log_data=io.BytesIO(content)) |
1413 | + self.assertEqual(200, response.status) |
1414 | + |
1415 | + # A report may have multiple artifacts. |
1416 | + # We verify that the content we just submitted via API now |
1417 | + # matches one of the artifacts in the DB for the report. |
1418 | + with person_logged_in(self.requester): |
1419 | + artifacts = list(getUtility( |
1420 | + IRevisionStatusArtifactSet).findByReport(self.report)) |
1421 | + lfcs = [artifact.library_file.content for artifact in artifacts] |
1422 | + sha1_of_all_artifacts = [lfc.sha1 for lfc in lfcs] |
1423 | + md5_of_all_artifacts = [lfc.md5 for lfc in lfcs] |
1424 | + filesizes_of_all_artifacts = [lfc.filesize for lfc in lfcs] |
1425 | + |
1426 | + self.assertIn(sha1, sha1_of_all_artifacts) |
1427 | + self.assertIn(md5, md5_of_all_artifacts) |
1428 | + self.assertIn(filesize, filesizes_of_all_artifacts) |
1429 | + |
1430 | + |
1431 | class TestGitRepositoryMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory): |
1432 | """Test GitRepository macaroon issuing and verification.""" |
1433 | |
1434 | diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml |
1435 | index 9d07930..0ccf396 100644 |
1436 | --- a/lib/lp/registry/configure.zcml |
1437 | +++ b/lib/lp/registry/configure.zcml |
1438 | @@ -1,4 +1,4 @@ |
1439 | -<!-- Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
1440 | +<!-- Copyright 2009-2021 Canonical Ltd. This software is licensed under the |
1441 | GNU Affero General Public License version 3 (see the file LICENSE). |
1442 | --> |
1443 | |
1444 | @@ -326,6 +326,7 @@ |
1445 | description |
1446 | driver |
1447 | backports_not_automatic |
1448 | + proposed_not_automatic |
1449 | include_long_descriptions |
1450 | index_compressors |
1451 | publish_by_hash |
1452 | diff --git a/lib/lp/registry/doc/distribution-mirror.txt b/lib/lp/registry/doc/distribution-mirror.txt |
1453 | index 8939a83..2c7cac5 100644 |
1454 | --- a/lib/lp/registry/doc/distribution-mirror.txt |
1455 | +++ b/lib/lp/registry/doc/distribution-mirror.txt |
1456 | @@ -367,11 +367,14 @@ up on the public mirror listings. |
1457 | >>> import email |
1458 | >>> from lp.services.mail import stub |
1459 | >>> len(stub.test_emails) |
1460 | - 2 |
1461 | + 3 |
1462 | >>> stub.test_emails.sort(key=lambda e: sorted(e[1])) # sort by to_addr |
1463 | >>> from_addr, to_addrs, raw_message = stub.test_emails.pop(0) |
1464 | >>> print(pretty(sorted(to_addrs))) |
1465 | - ['karl@canonical.com', 'mark@example.com'] |
1466 | + ['karl@canonical.com'] |
1467 | + >>> from_addr, to_addrs, raw_message = stub.test_emails.pop(0) |
1468 | + >>> print(pretty(sorted(to_addrs))) |
1469 | + ['mark@example.com'] |
1470 | >>> from_addr, to_addrs, raw_message = stub.test_emails.pop(0) |
1471 | >>> print(pretty(sorted(to_addrs))) |
1472 | ['mark@example.com'] |
1473 | @@ -401,10 +404,14 @@ single notification to the distribution's mirror admins. |
1474 | >>> valid_mirror.disable(notify_owner=False, log=log) |
1475 | >>> transaction.commit() |
1476 | >>> len(stub.test_emails) |
1477 | - 1 |
1478 | - >>> from_addr, to_addrs, raw_message = stub.test_emails.pop() |
1479 | + 2 |
1480 | + >>> stub.test_emails.sort(key=lambda e: sorted(e[1])) # sort by to_addr |
1481 | + >>> from_addr, to_addrs, raw_message = stub.test_emails.pop(0) |
1482 | >>> print(pretty(sorted(to_addrs))) |
1483 | - ['karl@canonical.com', 'mark@example.com'] |
1484 | + ['karl@canonical.com'] |
1485 | + >>> from_addr, to_addrs, raw_message = stub.test_emails.pop(0) |
1486 | + >>> print(pretty(sorted(to_addrs))) |
1487 | + ['mark@example.com'] |
1488 | |
1489 | Now we delete the MirrorProbeRecord we've just created, to make |
1490 | sure this mirror is probed by our prober script. |
1491 | diff --git a/lib/lp/registry/interfaces/distroseries.py b/lib/lp/registry/interfaces/distroseries.py |
1492 | index 848cf6d..da0e19e 100644 |
1493 | --- a/lib/lp/registry/interfaces/distroseries.py |
1494 | +++ b/lib/lp/registry/interfaces/distroseries.py |
1495 | @@ -1,4 +1,4 @@ |
1496 | -# Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
1497 | +# Copyright 2009-2021 Canonical Ltd. This software is licensed under the |
1498 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1499 | |
1500 | """Interfaces including and related to IDistroSeries.""" |
1501 | @@ -373,6 +373,15 @@ class IDistroSeriesPublic( |
1502 | automatically upgrade within backports, but not into it. |
1503 | """)) |
1504 | |
1505 | + proposed_not_automatic = Bool( |
1506 | + title=_("Don't upgrade to proposed updates automatically"), |
1507 | + required=True, |
1508 | + description=_(""" |
1509 | + Set NotAutomatic: yes and ButAutomaticUpgrades: yes in Release |
1510 | + files generated for the proposed pocket. This tells apt to |
1511 | + automatically upgrade within proposed, but not into it. |
1512 | + """)) |
1513 | + |
1514 | include_long_descriptions = exported( |
1515 | Bool( |
1516 | title=_( |
1517 | diff --git a/lib/lp/registry/model/distributionmirror.py b/lib/lp/registry/model/distributionmirror.py |
1518 | index 8d3bd82..2454187 100644 |
1519 | --- a/lib/lp/registry/model/distributionmirror.py |
1520 | +++ b/lib/lp/registry/model/distributionmirror.py |
1521 | @@ -385,13 +385,14 @@ class DistributionMirror(SQLBase): |
1522 | message = template % replacements |
1523 | subject = "Launchpad: Verification of %s failed" % self.name |
1524 | |
1525 | - mirror_admin_address = get_contact_email_addresses( |
1526 | + mirror_admin_addresses = get_contact_email_addresses( |
1527 | self.distribution.mirror_admin) |
1528 | - simple_sendmail(fromaddress, mirror_admin_address, subject, message) |
1529 | + for admin_address in mirror_admin_addresses: |
1530 | + simple_sendmail(fromaddress, admin_address, subject, message) |
1531 | |
1532 | if notify_owner: |
1533 | - owner_address = get_contact_email_addresses(self.owner) |
1534 | - if len(owner_address) > 0: |
1535 | + owner_addresses = get_contact_email_addresses(self.owner) |
1536 | + for owner_address in owner_addresses: |
1537 | simple_sendmail(fromaddress, owner_address, subject, message) |
1538 | |
1539 | def newProbeRecord(self, log_file): |
1540 | diff --git a/lib/lp/registry/model/distroseries.py b/lib/lp/registry/model/distroseries.py |
1541 | index 2ad609c..167aa8e 100644 |
1542 | --- a/lib/lp/registry/model/distroseries.py |
1543 | +++ b/lib/lp/registry/model/distroseries.py |
1544 | @@ -279,6 +279,7 @@ class DistroSeries(SQLBase, BugTargetBase, HasSpecificationsMixin, |
1545 | if "publishing_options" not in kwargs: |
1546 | kwargs["publishing_options"] = { |
1547 | "backports_not_automatic": False, |
1548 | + "proposed_not_automatic": False, |
1549 | "include_long_descriptions": True, |
1550 | "index_compressors": [ |
1551 | compressor.title |
1552 | @@ -828,6 +829,15 @@ class DistroSeries(SQLBase, BugTargetBase, HasSpecificationsMixin, |
1553 | self.publishing_options["backports_not_automatic"] = value |
1554 | |
1555 | @property |
1556 | + def proposed_not_automatic(self): |
1557 | + return self.publishing_options.get("proposed_not_automatic", False) |
1558 | + |
1559 | + @proposed_not_automatic.setter |
1560 | + def proposed_not_automatic(self, value): |
1561 | + assert isinstance(value, bool) |
1562 | + self.publishing_options["proposed_not_automatic"] = value |
1563 | + |
1564 | + @property |
1565 | def include_long_descriptions(self): |
1566 | return self.publishing_options.get("include_long_descriptions", True) |
1567 | |
1568 | diff --git a/lib/lp/registry/model/person.py b/lib/lp/registry/model/person.py |
1569 | index 36be643..ad7fd53 100644 |
1570 | --- a/lib/lp/registry/model/person.py |
1571 | +++ b/lib/lp/registry/model/person.py |
1572 | @@ -2661,7 +2661,7 @@ class Person( |
1573 | for address in all_addresses: |
1574 | # Delete all email addresses that are not the preferred email |
1575 | # address, or the team's email address. If this method was called |
1576 | - # with None, and there is no mailing list, then this condidition |
1577 | + # with None, and there is no mailing list, then this condition |
1578 | # is (None, None), causing all email addresses to be deleted. |
1579 | if address not in (email, mailing_list_email): |
1580 | address.destroySelf() |
1581 | diff --git a/lib/lp/registry/tests/test_distributionmirror.py b/lib/lp/registry/tests/test_distributionmirror.py |
1582 | index 08dae43..8f1423d 100644 |
1583 | --- a/lib/lp/registry/tests/test_distributionmirror.py |
1584 | +++ b/lib/lp/registry/tests/test_distributionmirror.py |
1585 | @@ -170,14 +170,22 @@ class TestDistributionMirror(TestCaseWithFactory): |
1586 | mirror.disable(notify_owner=True, log=log) |
1587 | # A notification was sent to the owner and other to the mirror admins. |
1588 | transaction.commit() |
1589 | - self.assertEqual(len(stub.test_emails), 2) |
1590 | + self.assertEqual(len(stub.test_emails), 3) |
1591 | + |
1592 | + # In order to prevent data disclosure, emails have to be sent to one |
1593 | + # person each, ie it is not allowed to have multiple recipients in an |
1594 | + # email's `to` field. |
1595 | + for email in stub.test_emails: |
1596 | + number_of_to_addresses = len(email[1]) |
1597 | + self.assertLess(number_of_to_addresses, 2) |
1598 | + |
1599 | stub.test_emails = [] |
1600 | |
1601 | mirror.disable(notify_owner=True, log=log) |
1602 | # Again, a notification was sent to the owner and other to the mirror |
1603 | # admins. |
1604 | transaction.commit() |
1605 | - self.assertEqual(len(stub.test_emails), 2) |
1606 | + self.assertEqual(len(stub.test_emails), 3) |
1607 | stub.test_emails = [] |
1608 | |
1609 | # For mirrors that have been probed more than once, we'll only notify |
1610 | @@ -187,7 +195,7 @@ class TestDistributionMirror(TestCaseWithFactory): |
1611 | mirror.disable(notify_owner=True, log=log) |
1612 | # A notification was sent to the owner and other to the mirror admins. |
1613 | transaction.commit() |
1614 | - self.assertEqual(len(stub.test_emails), 2) |
1615 | + self.assertEqual(len(stub.test_emails), 3) |
1616 | stub.test_emails = [] |
1617 | |
1618 | # We can always disable notifications to the owner by passing |
1619 | @@ -195,7 +203,7 @@ class TestDistributionMirror(TestCaseWithFactory): |
1620 | mirror.enabled = True |
1621 | mirror.disable(notify_owner=False, log=log) |
1622 | transaction.commit() |
1623 | - self.assertEqual(len(stub.test_emails), 1) |
1624 | + self.assertEqual(len(stub.test_emails), 2) |
1625 | stub.test_emails = [] |
1626 | |
1627 | mirror.enabled = False |
1628 | @@ -217,12 +225,12 @@ class TestDistributionMirror(TestCaseWithFactory): |
1629 | transaction.commit() |
1630 | stub.test_emails = [] |
1631 | |
1632 | - # Disabling the mirror results in a single notification to the |
1633 | - # mirror admins. |
1634 | + # Disabling the mirror results in one notification for each of |
1635 | + # the three mirror admins. |
1636 | self.factory.makeMirrorProbeRecord(mirror) |
1637 | mirror.disable(notify_owner=True, log="It broke.") |
1638 | transaction.commit() |
1639 | - self.assertEqual(len(stub.test_emails), 1) |
1640 | + self.assertEqual(len(stub.test_emails), 3) |
1641 | |
1642 | def test_permissions_for_resubmit(self): |
1643 | self.assertRaises( |
1644 | diff --git a/lib/lp/registry/tests/test_distroseries.py b/lib/lp/registry/tests/test_distroseries.py |
1645 | index 1543eca..d67e499 100644 |
1646 | --- a/lib/lp/registry/tests/test_distroseries.py |
1647 | +++ b/lib/lp/registry/tests/test_distroseries.py |
1648 | @@ -1,4 +1,4 @@ |
1649 | -# Copyright 2009-2016 Canonical Ltd. This software is licensed under the |
1650 | +# Copyright 2009-2021 Canonical Ltd. This software is licensed under the |
1651 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1652 | |
1653 | """Tests for distroseries.""" |
1654 | @@ -358,6 +358,16 @@ class TestDistroSeries(TestCaseWithFactory): |
1655 | self.assertTrue( |
1656 | naked_distroseries.publishing_options["backports_not_automatic"]) |
1657 | |
1658 | + def test_proposed_not_automatic(self): |
1659 | + distroseries = self.factory.makeDistroSeries() |
1660 | + self.assertFalse(distroseries.proposed_not_automatic) |
1661 | + with admin_logged_in(): |
1662 | + distroseries.proposed_not_automatic = True |
1663 | + self.assertTrue(distroseries.proposed_not_automatic) |
1664 | + naked_distroseries = removeSecurityProxy(distroseries) |
1665 | + self.assertTrue( |
1666 | + naked_distroseries.publishing_options["proposed_not_automatic"]) |
1667 | + |
1668 | def test_include_long_descriptions(self): |
1669 | distroseries = self.factory.makeDistroSeries() |
1670 | self.assertTrue(distroseries.include_long_descriptions) |
1671 | diff --git a/lib/lp/security.py b/lib/lp/security.py |
1672 | index 8f66fbb..8435167 100644 |
1673 | --- a/lib/lp/security.py |
1674 | +++ b/lib/lp/security.py |
1675 | @@ -99,6 +99,8 @@ from lp.code.interfaces.gitcollection import IGitCollection |
1676 | from lp.code.interfaces.gitref import IGitRef |
1677 | from lp.code.interfaces.gitrepository import ( |
1678 | IGitRepository, |
1679 | + IRevisionStatusArtifact, |
1680 | + IRevisionStatusReport, |
1681 | user_has_special_git_repository_access, |
1682 | ) |
1683 | from lp.code.interfaces.gitrule import ( |
1684 | @@ -683,6 +685,23 @@ class EditSpecificationByRelatedPeople(AuthorizationBase): |
1685 | self.obj, ['owner', 'drafter', 'assignee', 'approver'])) |
1686 | |
1687 | |
1688 | +class EditRevisionStatusReport(AuthorizationBase): |
1689 | + """The owner of a Git repository can edit its status reports.""" |
1690 | + permission = 'launchpad.Edit' |
1691 | + usedfor = IRevisionStatusReport |
1692 | + |
1693 | + def checkAuthenticated(self, user): |
1694 | + return user.isOwner(self.obj.git_repository) |
1695 | + |
1696 | + |
1697 | +class EditRevisionStatusArtifact(DelegatedAuthorization): |
1698 | + permission = 'launchpad.Edit' |
1699 | + usedfor = IRevisionStatusArtifact |
1700 | + |
1701 | + def __init__(self, obj): |
1702 | + super().__init__(obj, obj.report, 'launchpad.Edit') |
1703 | + |
1704 | + |
1705 | class AdminSpecification(AuthorizationBase): |
1706 | permission = 'launchpad.Admin' |
1707 | usedfor = ISpecification |
1708 | diff --git a/lib/lp/services/config/schema-lazr.conf b/lib/lp/services/config/schema-lazr.conf |
1709 | index 5472dbc..73bfb70 100644 |
1710 | --- a/lib/lp/services/config/schema-lazr.conf |
1711 | +++ b/lib/lp/services/config/schema-lazr.conf |
1712 | @@ -1332,6 +1332,10 @@ old_os_tenant_name: none |
1713 | # datatype: string |
1714 | old_os_auth_version: 2.0 |
1715 | |
1716 | +# Time in seconds to wait for a response from Swift. |
1717 | +# datatype: integer |
1718 | +swift_timeout: 15 |
1719 | + |
1720 | |
1721 | # Mailman configuration. Most of this is configured in |
1722 | # https://git.launchpad.net/lp-mailman instead; the entries here are only |
1723 | diff --git a/lib/lp/services/librarianserver/swift.py b/lib/lp/services/librarianserver/swift.py |
1724 | index d844d50..4a1139d 100644 |
1725 | --- a/lib/lp/services/librarianserver/swift.py |
1726 | +++ b/lib/lp/services/librarianserver/swift.py |
1727 | @@ -426,6 +426,7 @@ class ConnectionPool: |
1728 | key=self.os_password, |
1729 | tenant_name=self.os_tenant_name, |
1730 | auth_version=self.os_auth_version, |
1731 | + timeout=float(config.librarian_server.swift_timeout), |
1732 | ) |
1733 | |
1734 | |
1735 | diff --git a/lib/lp/services/librarianserver/tests/test_swift.py b/lib/lp/services/librarianserver/tests/test_swift.py |
1736 | index 98595b6..ecdb6d9 100644 |
1737 | --- a/lib/lp/services/librarianserver/tests/test_swift.py |
1738 | +++ b/lib/lp/services/librarianserver/tests/test_swift.py |
1739 | @@ -363,6 +363,15 @@ class TestFeedSwift(TestCase): |
1740 | finally: |
1741 | swift_client.close() |
1742 | |
1743 | + def test_swift_timeout(self): |
1744 | + # The librarian's Swift connections honour the configured timeout. |
1745 | + self.pushConfig( |
1746 | + 'librarian_server', os_username='timeout', swift_timeout=0.1) |
1747 | + swift_client = self.swift_fixture.connect() |
1748 | + self.assertRaises( |
1749 | + swiftclient.ClientException, |
1750 | + swift_client.get_object, 'container', 'name') |
1751 | + |
1752 | |
1753 | class TestHashStream(TestCase): |
1754 | layer = BaseLayer |
1755 | diff --git a/lib/lp/services/webapp/tests/test_servers.py b/lib/lp/services/webapp/tests/test_servers.py |
1756 | index a5ffe0a..7b288fe 100644 |
1757 | --- a/lib/lp/services/webapp/tests/test_servers.py |
1758 | +++ b/lib/lp/services/webapp/tests/test_servers.py |
1759 | @@ -897,6 +897,14 @@ class TestWebServiceAccessTokens(TestCaseWithFactory): |
1760 | repository, |
1761 | ["repository:build_status", "repository:another_scope"]) |
1762 | |
1763 | + def test_checkRequest_contains_context(self): |
1764 | + [ref] = self.factory.makeGitRefs() |
1765 | + self._makeAccessTokenVerifiedRequest( |
1766 | + target=ref.repository, |
1767 | + scopes=[AccessTokenScope.REPOSITORY_BUILD_STATUS]) |
1768 | + getUtility(IWebServiceConfiguration).checkRequest( |
1769 | + ref, ["repository:build_status", "repository:another_scope"]) |
1770 | + |
1771 | def test_checkRequest_bad_context(self): |
1772 | repository = self.factory.makeGitRepository() |
1773 | self._makeAccessTokenVerifiedRequest( |
1774 | diff --git a/lib/lp/services/webservice/configuration.py b/lib/lp/services/webservice/configuration.py |
1775 | index 8b56842..665c64e 100644 |
1776 | --- a/lib/lp/services/webservice/configuration.py |
1777 | +++ b/lib/lp/services/webservice/configuration.py |
1778 | @@ -15,8 +15,13 @@ from zope.security.interfaces import Unauthorized |
1779 | from lp.app import versioninfo |
1780 | from lp.services.config import config |
1781 | from lp.services.database.sqlbase import block_implicit_flushes |
1782 | +from lp.services.webapp.canonicalurl import nearest_adapter |
1783 | from lp.services.webapp.interaction import get_interaction_extras |
1784 | -from lp.services.webapp.interfaces import ILaunchBag |
1785 | +from lp.services.webapp.interfaces import ( |
1786 | + ILaunchBag, |
1787 | + ILaunchpadContainer, |
1788 | + ) |
1789 | +from lp.services.webapp.publisher import canonical_url |
1790 | from lp.services.webapp.servers import ( |
1791 | WebServiceClientRequest, |
1792 | WebServicePublication, |
1793 | @@ -102,9 +107,19 @@ class LaunchpadWebServiceConfiguration(BaseWebServiceConfiguration): |
1794 | access_token = get_interaction_extras().access_token |
1795 | if access_token is None: |
1796 | return |
1797 | - if access_token.target != context: |
1798 | - raise Unauthorized( |
1799 | - "Current authentication does not allow access to this object.") |
1800 | + |
1801 | + # The access token must be for a target that either exactly matches |
1802 | + # or contains the context object. |
1803 | + if access_token.target == context: |
1804 | + pass |
1805 | + else: |
1806 | + container = nearest_adapter(context, ILaunchpadContainer) |
1807 | + if not container.isWithin( |
1808 | + canonical_url(access_token.target, force_local_path=True)): |
1809 | + raise Unauthorized( |
1810 | + "Current authentication does not allow access to this " |
1811 | + "object.") |
1812 | + |
1813 | if not required_scopes: |
1814 | raise Unauthorized( |
1815 | "Current authentication only allows calling scoped methods.") |
1816 | diff --git a/lib/lp/soyuz/scripts/initialize_distroseries.py b/lib/lp/soyuz/scripts/initialize_distroseries.py |
1817 | index 1ca534d..5a773bf 100644 |
1818 | --- a/lib/lp/soyuz/scripts/initialize_distroseries.py |
1819 | +++ b/lib/lp/soyuz/scripts/initialize_distroseries.py |
1820 | @@ -1,4 +1,4 @@ |
1821 | -# Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
1822 | +# Copyright 2009-2021 Canonical Ltd. This software is licensed under the |
1823 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1824 | |
1825 | """Initialize a distroseries from its parent distroseries.""" |
1826 | @@ -383,6 +383,9 @@ class InitializeDistroSeries: |
1827 | self.distroseries.backports_not_automatic = any( |
1828 | parent.backports_not_automatic |
1829 | for parent in self.derivation_parents) |
1830 | + self.distroseries.proposed_not_automatic = any( |
1831 | + parent.proposed_not_automatic |
1832 | + for parent in self.derivation_parents) |
1833 | self.distroseries.include_long_descriptions = any( |
1834 | parent.include_long_descriptions |
1835 | for parent in self.derivation_parents) |
1836 | diff --git a/lib/lp/soyuz/scripts/tests/test_initialize_distroseries.py b/lib/lp/soyuz/scripts/tests/test_initialize_distroseries.py |
1837 | index 4079947..b3edb46 100644 |
1838 | --- a/lib/lp/soyuz/scripts/tests/test_initialize_distroseries.py |
1839 | +++ b/lib/lp/soyuz/scripts/tests/test_initialize_distroseries.py |
1840 | @@ -1,4 +1,4 @@ |
1841 | -# Copyright 2010-2020 Canonical Ltd. This software is licensed under the |
1842 | +# Copyright 2010-2021 Canonical Ltd. This software is licensed under the |
1843 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1844 | |
1845 | """Test the initialize_distroseries script machinery.""" |
1846 | @@ -93,6 +93,7 @@ class InitializationHelperTestCase(TestCaseWithFactory): |
1847 | if existing_format_selection is None: |
1848 | spfss_utility.add(parent, format_selection) |
1849 | parent.backports_not_automatic = True |
1850 | + parent.proposed_not_automatic = True |
1851 | parent.include_long_descriptions = False |
1852 | parent.index_compressors = [IndexCompressionType.XZ] |
1853 | parent.publish_by_hash = True |
1854 | @@ -685,6 +686,7 @@ class TestInitializeDistroSeries(InitializationHelperTestCase): |
1855 | SourcePackageFormat.FORMAT_1_0)) |
1856 | # Other configuration bits are copied too. |
1857 | self.assertTrue(child.backports_not_automatic) |
1858 | + self.assertTrue(child.proposed_not_automatic) |
1859 | self.assertFalse(child.include_long_descriptions) |
1860 | self.assertEqual([IndexCompressionType.XZ], child.index_compressors) |
1861 | self.assertTrue(child.publish_by_hash) |
1862 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
1863 | index 9d59ba8..db48ed3 100644 |
1864 | --- a/lib/lp/testing/factory.py |
1865 | +++ b/lib/lp/testing/factory.py |
1866 | @@ -131,7 +131,10 @@ from lp.code.interfaces.gitref import ( |
1867 | IGitRef, |
1868 | IGitRefRemoteSet, |
1869 | ) |
1870 | -from lp.code.interfaces.gitrepository import IGitRepository |
1871 | +from lp.code.interfaces.gitrepository import ( |
1872 | + IGitRepository, |
1873 | + IRevisionStatusArtifactSet, |
1874 | + ) |
1875 | from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch |
1876 | from lp.code.interfaces.revision import IRevisionSet |
1877 | from lp.code.interfaces.sourcepackagerecipe import ( |
1878 | @@ -146,6 +149,7 @@ from lp.code.model.diff import ( |
1879 | Diff, |
1880 | PreviewDiff, |
1881 | ) |
1882 | +from lp.code.model.gitrepository import IRevisionStatusReportSet |
1883 | from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet |
1884 | from lp.oci.interfaces.ocirecipe import IOCIRecipeSet |
1885 | from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet |
1886 | @@ -1843,6 +1847,30 @@ class BareLaunchpadObjectFactory(ObjectFactory): |
1887 | grantee, grantor, can_create=can_create, can_push=can_push, |
1888 | can_force_push=can_force_push) |
1889 | |
1890 | + def makeRevisionStatusReport(self, user=None, title=None, |
1891 | + git_repository=None, commit_sha1=None, |
1892 | + result_summary=None, url=None, result=None): |
1893 | + """Create a new RevisionStatusReport.""" |
1894 | + if title is None: |
1895 | + title = self.getUniqueUnicode() |
1896 | + if git_repository is None: |
1897 | + git_repository = self.makeGitRepository() |
1898 | + if user is None: |
1899 | + user = git_repository.owner |
1900 | + if commit_sha1 is None: |
1901 | + commit_sha1 = hashlib.sha1(self.getUniqueBytes()).hexdigest() |
1902 | + return getUtility(IRevisionStatusReportSet).new( |
1903 | + user, title, git_repository, commit_sha1, result_summary, |
1904 | + url, result) |
1905 | + |
1906 | + def makeRevisionStatusArtifact(self, lfa=None, report=None): |
1907 | + """Create a new RevisionStatusArtifact.""" |
1908 | + if lfa is None: |
1909 | + lfa = self.makeLibraryFileAlias() |
1910 | + if report is None: |
1911 | + report = self.makeRevisionStatusReport() |
1912 | + return getUtility(IRevisionStatusArtifactSet).new(lfa, report) |
1913 | + |
1914 | def makeBug(self, target=None, owner=None, bug_watch_url=None, |
1915 | information_type=None, date_closed=None, title=None, |
1916 | date_created=None, description=None, comment=None, |
1917 | diff --git a/lib/lp/testing/swift/fakeswift.py b/lib/lp/testing/swift/fakeswift.py |
1918 | index e3dd5e7..f0134d8 100644 |
1919 | --- a/lib/lp/testing/swift/fakeswift.py |
1920 | +++ b/lib/lp/testing/swift/fakeswift.py |
1921 | @@ -135,6 +135,8 @@ class FakeKeystone(resource.Resource): |
1922 | if tenant_name not in self.root.tenants: |
1923 | request.setResponseCode(http.FORBIDDEN) |
1924 | return b"" |
1925 | + if username == "timeout": |
1926 | + time.sleep(60) |
1927 | if username not in self.users: |
1928 | request.setResponseCode(http.FORBIDDEN) |
1929 | return b"" |
1930 | diff --git a/lib/lp/translations/scripts/po_import.py b/lib/lp/translations/scripts/po_import.py |
1931 | index 0cb9200..8b7d9b7 100644 |
1932 | --- a/lib/lp/translations/scripts/po_import.py |
1933 | +++ b/lib/lp/translations/scripts/po_import.py |
1934 | @@ -140,11 +140,8 @@ class TranslationsImport(LaunchpadCronScript): |
1935 | katie = getUtility(ILaunchpadCelebrities).katie |
1936 | if entry.importer == katie: |
1937 | # Email import state to Debian imports email. |
1938 | - to_email = None |
1939 | - else: |
1940 | - to_email = get_contact_email_addresses(entry.importer) |
1941 | - |
1942 | - if to_email: |
1943 | + return |
1944 | + for to_email in get_contact_email_addresses(entry.importer): |
1945 | text = MailWrapper().format(mail_body) |
1946 | simple_sendmail(from_email, to_email, mail_subject, text) |
1947 | |
1948 | diff --git a/lib/lp/translations/scripts/tests/test_translations_import.py b/lib/lp/translations/scripts/tests/test_translations_import.py |
1949 | index 07d8776..ffc61fd 100644 |
1950 | --- a/lib/lp/translations/scripts/tests/test_translations_import.py |
1951 | +++ b/lib/lp/translations/scripts/tests/test_translations_import.py |
1952 | @@ -188,6 +188,25 @@ class TestTranslationsImport(TestCaseWithFactory): |
1953 | self.assertEqual( |
1954 | [self.owner.preferredemail.email], self._getEmailRecipients()) |
1955 | |
1956 | + def test_notifies_uploader_separately(self): |
1957 | + """Do not put several addresses into one `to`-field""" |
1958 | + p1 = self.factory.makePerson() |
1959 | + p2 = self.factory.makePerson() |
1960 | + uploader = self.factory.makeTeam( |
1961 | + members=[p1, p2], |
1962 | + ) |
1963 | + entry = self._makeApprovedEntry(uploader=uploader) |
1964 | + transaction.commit() |
1965 | + self.script._importEntry(entry) |
1966 | + transaction.commit() |
1967 | + |
1968 | + # three emails get generated |
1969 | + # and each has only one recipient |
1970 | + self.assertEqual(3, len(stub.test_emails)) |
1971 | + for email in stub.test_emails: |
1972 | + number_of_to_addresses = len(email[1]) |
1973 | + self.assertEqual(1, number_of_to_addresses) |
1974 | + |
1975 | def test_does_not_notify_vcs_imports(self): |
1976 | vcs_imports = getUtility(ILaunchpadCelebrities).vcs_imports |
1977 | entry = self._makeApprovedEntry(vcs_imports) |