Merge lp:~abentley/launchpad/daily-builds-ui into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Merge reported by: Aaron Bentley
Merged at revision: not available
Proposed branch: lp:~abentley/launchpad/daily-builds-ui
Merge into: lp:launchpad
Prerequisite: lp:~abentley/launchpad/daily-builds-score
Diff against target: 1420 lines (+201/-632)
32 files modified
Makefile (+1/-1)
cronscripts/calculate-bug-heat.py (+0/-33)
cronscripts/publishing/maintenance-check.py (+1/-1)
database/replication/helpers.py (+10/-4)
database/replication/new-slave.py (+5/-0)
database/schema/comments.sql (+2/-0)
database/schema/fti.py (+10/-13)
database/schema/patch-2207-60-1.sql (+10/-0)
database/schema/patch-2207-61-0.sql (+13/-0)
database/schema/patch-2207-62-0.sql (+14/-0)
database/schema/security.cfg (+1/-0)
database/schema/trusted.sql (+8/-1)
lib/canonical/launchpad/scripts/garbo.py (+0/-1)
lib/lp/bugs/browser/bugtask.py (+1/-1)
lib/lp/bugs/browser/tests/test_bugtask.py (+9/-8)
lib/lp/bugs/configure.zcml (+0/-12)
lib/lp/bugs/doc/bugtask-status-workflow.txt (+11/-1)
lib/lp/bugs/interfaces/bugjob.py (+1/-11)
lib/lp/bugs/interfaces/bugtask.py (+11/-2)
lib/lp/bugs/model/bug.py (+0/-1)
lib/lp/bugs/model/bugheat.py (+0/-54)
lib/lp/bugs/scripts/bugheat.py (+0/-108)
lib/lp/bugs/scripts/tests/test_bugheat.py (+0/-256)
lib/lp/bugs/tests/bugs-emailinterface.txt (+1/-1)
lib/lp/bugs/tests/bugtarget-bugcount.txt (+2/-0)
lib/lp/bugs/tests/test_bugheat.py (+1/-102)
lib/lp/code/browser/sourcepackagerecipe.py (+13/-2)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+39/-10)
lib/lp/code/configure.zcml (+1/-0)
lib/lp/code/interfaces/sourcepackagerecipe.py (+1/-1)
lib/lp/code/templates/sourcepackagerecipe-index.pt (+13/-0)
utilities/report-database-stats.py (+22/-8)
To merge this branch: bzr merge lp:~abentley/launchpad/daily-builds-ui
Reviewer Review Type Date Requested Status
Paul Hummer (community) code Approve
Review via email: mp+26334@code.launchpad.net

Commit message

Provide UI for daily builds

Description of the change

= Summary =
Fix bug #586944: Launchpad should provide a ui for daily builds.

== Proposed fix ==
Allow users to specify that a recipe should be built daily using the web UI.

== Pre-implementation notes ==
None

== Implementation details ==
Allow user to control build_daily and daily_build_archive settings

== Tests ==
bin/test -t test_create_recipe_no_distroseries

== Demo and Q/A ==
Create a recipe. Enable daily builds. Select an archive of your choice.

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  cronscripts/request_daily_builds.py
  lib/lp/code/model/tests/test_sourcepackagerecipebuild.py
  lib/lp/code/interfaces/sourcepackagerecipebuild.py
  lib/lp/code/configure.zcml
  lib/canonical/config/schema-lazr.conf
  lib/lp/soyuz/model/publishing.py
  lib/lp/code/scripts/tests/test_request_daily_builds.py
  database/schema/security.cfg
  lib/lp/soyuz/doc/buildd-slavescanner.txt
  lib/lp/soyuz/stories/soyuz/xx-build-record.txt
  lib/lp/code/model/tests/test_sourcepackagerecipe.py
  lib/lp/testing/factory.py
  lib/lp/code/model/sourcepackagerecipebuild.py
  lib/lp/code/templates/sourcepackagerecipe-index.pt
  lib/lp/buildmaster/model/buildqueue.py
  lib/lp/soyuz/doc/buildd-dispatching.txt
  lib/lp/code/browser/sourcepackagerecipe.py
  lib/lp/code/model/sourcepackagerecipe.py
  lib/lp/code/browser/tests/test_sourcepackagerecipe.py
  configs/testrunner/launchpad-lazr.conf
  lib/lp/code/interfaces/sourcepackagerecipe.py
  lib/lp/registry/model/person.py
  lib/lp/soyuz/doc/build.txt

== Pyflakes Doctest notices ==

lib/lp/soyuz/doc/buildd-slavescanner.txt
    689: local variable 'pub_binaries' is assigned to but never used

== Pyflakes notices ==

cronscripts/request_daily_builds.py
    19: 'canonical' imported but unused

    ^^^ fix for circular imports

== Pylint notices ==

cronscripts/request_daily_builds.py
    19: [W0611] Unused import canonical

    ^^^ fix for circular imports

lib/lp/code/model/sourcepackagerecipebuild.py
    207: [W0702, SourcePackageRecipeBuild.makeDailyBuilds] No exception type(s) specified

    ^^^ expected; this is a generic error handler.

lib/lp/code/interfaces/sourcepackagerecipe.py
    150: [C0322, ISourcePackageRecipe.requestBuild] Operator not preceded by a space
    distroseries=Reference(schema=IDistroSeries),
    ^
    )
    @export_write_operation()
    def requestBuild(archive, distroseries, requester, pocket):

    ^^^ bogus

lib/lp/registry/model/person.py
    1265: [W0104, Person.addMember] Statement seems to have no effect

    ^^^ read to force a flush.

To post a comment you must log in.
Revision history for this message
Paul Hummer (rockstar) wrote :

Man, these make for better diffs...

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2010-06-11 18:57:02 +0000
3+++ Makefile 2010-06-14 20:18:33 +0000
4@@ -250,7 +250,7 @@
5 bin/run -r librarian,sftp,codebrowse -i $(LPCONFIG)
6
7
8-start_librarian: build
9+start_librarian: compile
10 bin/start_librarian
11
12 stop_librarian:
13
14=== removed file 'cronscripts/calculate-bug-heat.py'
15--- cronscripts/calculate-bug-heat.py 2010-04-27 19:48:39 +0000
16+++ cronscripts/calculate-bug-heat.py 1970-01-01 00:00:00 +0000
17@@ -1,33 +0,0 @@
18-#!/usr/bin/python -S
19-#
20-# Copyright 2010 Canonical Ltd. This software is licensed under the
21-# GNU Affero General Public License version 3 (see the file LICENSE).
22-
23-# pylint: disable-msg=W0403
24-
25-"""Calculate bug heat."""
26-
27-__metaclass__ = type
28-
29-import _pythonpath
30-
31-from canonical.launchpad.webapp import errorlog
32-
33-from lp.services.job.runner import JobCronScript
34-from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource
35-
36-
37-class RunCalculateBugHeat(JobCronScript):
38- """Run BranchScanJob jobs."""
39-
40- config_name = 'calculate_bug_heat'
41- source_interface = ICalculateBugHeatJobSource
42-
43- def main(self):
44- errorlog.globalErrorUtility.configure(self.config_name)
45- return super(RunCalculateBugHeat, self).main()
46-
47-
48-if __name__ == '__main__':
49- script = RunCalculateBugHeat()
50- script.lock_and_run()
51
52=== modified file 'cronscripts/publishing/maintenance-check.py'
53--- cronscripts/publishing/maintenance-check.py 2010-04-23 13:43:19 +0000
54+++ cronscripts/publishing/maintenance-check.py 2010-06-14 20:18:33 +0000
55@@ -350,7 +350,7 @@
56 except:
57 logging.exception("can not parse line '%s'" % line)
58 except urllib2.HTTPError, e:
59- if e.getcode() != 404:
60+ if e.code != 404:
61 raise
62 sys.stderr.write("hints-file: %s gave 404 error\n" % hints_file)
63
64
65=== modified file 'database/replication/helpers.py'
66--- database/replication/helpers.py 2010-04-29 12:38:05 +0000
67+++ database/replication/helpers.py 2010-06-14 20:18:33 +0000
68@@ -44,7 +44,6 @@
69 ('public', 'nameblacklist'),
70 ('public', 'openidconsumerassociation'),
71 ('public', 'openidconsumernonce'),
72- ('public', 'oauthnonce'),
73 ('public', 'codeimportmachine'),
74 ('public', 'scriptactivity'),
75 ('public', 'standardshipitrequest'),
76@@ -71,6 +70,8 @@
77 # Database statistics
78 'public.databasetablestats',
79 'public.databasecpustats',
80+ # Don't replicate OAuthNonce - too busy and no real gain.
81+ 'public.oauthnonce',
82 # Ubuntu SSO database. These tables where created manually by ISD
83 # and the Launchpad scripts should not mess with them. Eventually
84 # these tables will be in a totally separate database.
85@@ -353,6 +354,9 @@
86
87 A replication set must contain all tables linked by foreign key
88 reference to the given table, and sequences used to generate keys.
89+ Tables and sequences can be added to the IGNORED_TABLES and
90+ IGNORED_SEQUENCES lists for cases where we known can safely ignore
91+ this restriction.
92
93 :param seeds: [(namespace, tablename), ...]
94
95@@ -420,7 +424,8 @@
96 """ % sqlvalues(namespace, tablename))
97 for namespace, tablename in cur.fetchall():
98 key = (namespace, tablename)
99- if key not in tables and key not in pending_tables:
100+ if (key not in tables and key not in pending_tables
101+ and '%s.%s' % (namespace, tablename) not in IGNORED_TABLES):
102 pending_tables.add(key)
103
104 # Generate the set of sequences that are linked to any of our set of
105@@ -441,8 +446,9 @@
106 ) AS whatever
107 WHERE seq IS NOT NULL;
108 """ % sqlvalues(fqn(namespace, tablename), namespace, tablename))
109- for row in cur.fetchall():
110- sequences.add(row[0])
111+ for sequence, in cur.fetchall():
112+ if sequence not in IGNORED_SEQUENCES:
113+ sequences.add(sequence)
114
115 # We can't easily convert the sequence name to (namespace, name) tuples,
116 # so we might as well convert the tables to dot notation for consistancy.
117
118=== modified file 'database/replication/new-slave.py'
119--- database/replication/new-slave.py 2010-05-19 18:07:56 +0000
120+++ database/replication/new-slave.py 2010-06-14 20:18:33 +0000
121@@ -188,6 +188,9 @@
122
123 script += dedent("""\
124 } on error { echo 'Failed.'; exit 1; }
125+
126+ echo 'You may need to restart the Slony daemons now. If the first';
127+ echo 'of the following syncs passes then there is no need.';
128 """)
129
130 full_sync = []
131@@ -200,6 +203,7 @@
132 wait for event (
133 origin = @%(nickname)s, confirmed=ALL,
134 wait on = @%(nickname)s, timeout=0);
135+ echo 'Ok. Replication syncing fine with new node.';
136 """ % {'nickname': nickname}))
137 full_sync = '\n'.join(full_sync)
138 script += full_sync
139@@ -210,6 +214,7 @@
140 subscribe set (
141 id=%d, provider=@master_node, receiver=@new_node, forward=yes);
142 echo 'Waiting for subscribe to start processing.';
143+ echo 'This will block on long running transactions.';
144 sync (id = @master_node);
145 wait for event (
146 origin = @master_node, confirmed = ALL,
147
148=== modified file 'database/schema/comments.sql'
149--- database/schema/comments.sql 2010-05-27 22:18:16 +0000
150+++ database/schema/comments.sql 2010-06-14 20:18:33 +0000
151@@ -1350,6 +1350,7 @@
152 COMMENT ON COLUMN SourcePackageRecipe.owner IS 'The person or team who can edit this recipe.';
153 COMMENT ON COLUMN SourcePackageRecipe.name IS 'The name of the recipe in the web/URL.';
154 COMMENT ON COLUMN SourcePackageRecipe.build_daily IS 'If true, this recipe should be built daily.';
155+COMMENT ON COLUMN SourcePackageRecipe.is_stale IS 'True if this recipe has not been built since a branch was updated.';
156
157 COMMENT ON COLUMN SourcePackageREcipe.daily_build_archive IS 'The archive to build into for daily builds.';
158
159@@ -1371,6 +1372,7 @@
160 COMMENT ON COLUMN SourcePackageRecipeBuild.date_first_dispatched IS 'The instant the build was dispatched the first time. This value will not get overridden if the build is retried.';
161 COMMENT ON COLUMN SourcePackageRecipeBuild.requester IS 'Who requested the build.';
162 COMMENT ON COLUMN SourcePackageRecipeBuild.recipe IS 'The recipe being processed.';
163+COMMENT ON COLUMN SourcePackageRecipeBuild.manifest IS 'The evaluated recipe that was built.';
164 COMMENT ON COLUMN SourcePackageRecipeBuild.archive IS 'The archive the source package will be built in and uploaded to.';
165 COMMENT ON COLUMN SourcePackageRecipeBuild.pocket IS 'The pocket the source package will be built in and uploaded to.';
166 COMMENT ON COLUMN SourcePackageRecipeBuild.dependencies IS 'The missing build dependencies, if any.';
167
168=== modified file 'database/schema/fti.py'
169--- database/schema/fti.py 2010-05-19 18:07:56 +0000
170+++ database/schema/fti.py 2010-06-14 20:18:33 +0000
171@@ -14,10 +14,10 @@
172 import _pythonpath
173
174 from distutils.version import LooseVersion
175-import sys
176 import os.path
177 from optparse import OptionParser
178-import popen2
179+import subprocess
180+import sys
181 from tempfile import NamedTemporaryFile
182 from textwrap import dedent
183 import time
184@@ -319,18 +319,15 @@
185 cmd += ' -h %s' % lp.dbhost
186 if options.dbuser:
187 cmd += ' -U %s' % options.dbuser
188- p = popen2.Popen4(cmd)
189- c = p.tochild
190- print >> c, "SET client_min_messages=ERROR;"
191- print >> c, "CREATE SCHEMA ts2;"
192- print >> c, open(tsearch2_sql_path).read().replace(
193- 'public;','ts2, public;'
194- )
195- p.tochild.close()
196- rv = p.wait()
197- if rv != 0:
198+ p = subprocess.Popen(
199+ cmd.split(' '), stdin=subprocess.PIPE,
200+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
201+ out, err = p.communicate(
202+ "SET client_min_messages=ERROR; CREATE SCHEMA ts2;"
203+ + open(tsearch2_sql_path).read().replace('public;','ts2, public;'))
204+ if p.returncode != 0:
205 log.fatal('Error executing %s:', cmd)
206- log.debug(p.fromchild.read())
207+ log.debug(out)
208 sys.exit(rv)
209
210 # Create ftq helper and its sibling _ftq.
211
212=== added file 'database/schema/patch-2207-60-1.sql'
213--- database/schema/patch-2207-60-1.sql 1970-01-01 00:00:00 +0000
214+++ database/schema/patch-2207-60-1.sql 2010-06-14 20:18:33 +0000
215@@ -0,0 +1,10 @@
216+SET client_min_messages=ERROR;
217+
218+CREATE INDEX archive__require_virtualized__idx
219+ON Archive(require_virtualized);
220+
221+CREATE INDEX buildfarmjob__status__idx
222+ON BuildFarmJob(status);
223+
224+INSERT INTO LaunchpadDatabaseRevision VALUES (2207, 60, 1);
225+
226
227=== added file 'database/schema/patch-2207-61-0.sql'
228--- database/schema/patch-2207-61-0.sql 1970-01-01 00:00:00 +0000
229+++ database/schema/patch-2207-61-0.sql 2010-06-14 20:18:33 +0000
230@@ -0,0 +1,13 @@
231+-- Copyright 2010 Canonical Ltd. This software is licensed under the
232+-- GNU Affero General Public License version 3 (see the file LICENSE).
233+
234+SET client_min_messages=ERROR;
235+ALTER TABLE SourcePackageRecipe ADD COLUMN is_stale BOOLEAN NOT NULL DEFAULT TRUE;
236+ALTER TABLE SourcePackageRecipeBuild ADD COLUMN manifest INTEGER REFERENCES SourcePackageRecipeData;
237+
238+CREATE INDEX sourcepackagerecipe__is_stale__build_daily__idx
239+ON SourcepackageRecipe(is_stale, build_daily);
240+
241+CREATE INDEX sourcepackagerecipebuild__manifest__idx ON SourcepackageRecipeBuild(manifest);
242+
243+INSERT INTO LaunchpadDatabaseRevision VALUES (2207, 61, 0);
244
245=== added file 'database/schema/patch-2207-62-0.sql'
246--- database/schema/patch-2207-62-0.sql 1970-01-01 00:00:00 +0000
247+++ database/schema/patch-2207-62-0.sql 2010-06-14 20:18:33 +0000
248@@ -0,0 +1,14 @@
249+SET client_min_messages=ERROR;
250+
251+-- Bug #49717
252+ALTER TABLE SourcePackageRelease ALTER component SET NOT NULL;
253+
254+-- We are taking OAuthNonce out of replication, so we make the foreign
255+-- key reference ON DELETE CASCADE so things don't explode when we
256+-- shuffle the lpmain master around.
257+ALTER TABLE OAuthNonce DROP CONSTRAINT oauthnonce__access_token__fk;
258+ALTER TABLE OAuthNonce ADD CONSTRAINT oauthnonce__access_token__fk
259+ FOREIGN KEY (access_token) REFERENCES OAuthAccessToken
260+ ON DELETE CASCADE;
261+
262+INSERT INTO LaunchpadDatabaseRevision VALUES (2207, 62, 0);
263
264=== modified file 'database/schema/security.cfg'
265--- database/schema/security.cfg 2010-06-08 15:13:20 +0000
266+++ database/schema/security.cfg 2010-06-14 20:18:33 +0000
267@@ -1847,6 +1847,7 @@
268 type=user
269 public.archive = SELECT
270 public.buildfarmjob = SELECT
271+public.databasereplicationlag = SELECT
272 public.packagebuild = SELECT
273 public.binarypackagebuild = SELECT
274 public.buildqueue = SELECT
275
276=== modified file 'database/schema/trusted.sql'
277--- database/schema/trusted.sql 2010-05-28 10:36:08 +0000
278+++ database/schema/trusted.sql 2010-06-14 20:18:33 +0000
279@@ -144,6 +144,12 @@
280 LIMIT 1
281 """, 1).nrows() > 0
282 if stats_reset:
283+ # The database stats have been reset. We cannot calculate
284+ # deltas because we do not know when this happened. So we trash
285+ # our records as they are now useless to us. We could be more
286+ # sophisticated about this, but this should only happen
287+ # when an admin explicitly resets the statistics or if the
288+ # database is rebuilt.
289 plpy.notice("Stats wraparound. Purging DatabaseTableStats")
290 plpy.execute("DELETE FROM DatabaseTableStats")
291 else:
292@@ -158,7 +164,8 @@
293 SELECT
294 CURRENT_TIMESTAMP AT TIME ZONE 'UTC',
295 schemaname, relname, seq_scan, seq_tup_read,
296- idx_scan, idx_tup_fetch, n_tup_ins, n_tup_upd, n_tup_del,
297+ coalesce(idx_scan, 0), coalesce(idx_tup_fetch, 0),
298+ n_tup_ins, n_tup_upd, n_tup_del,
299 n_tup_hot_upd, n_live_tup, n_dead_tup, last_vacuum,
300 last_autovacuum, last_analyze, last_autoanalyze
301 FROM pg_catalog.pg_stat_user_tables;
302
303=== modified file 'lib/canonical/launchpad/scripts/garbo.py'
304--- lib/canonical/launchpad/scripts/garbo.py 2010-06-11 07:26:03 +0000
305+++ lib/canonical/launchpad/scripts/garbo.py 2010-06-14 20:18:33 +0000
306@@ -33,7 +33,6 @@
307 from lp.bugs.interfaces.bug import IBugSet
308 from lp.bugs.model.bug import Bug
309 from lp.bugs.model.bugattachment import BugAttachment
310-from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource
311 from lp.bugs.model.bugnotification import BugNotification
312 from lp.bugs.model.bugwatch import BugWatch
313 from lp.bugs.scripts.checkwatches.scheduler import (
314
315=== modified file 'lib/lp/bugs/browser/bugtask.py'
316--- lib/lp/bugs/browser/bugtask.py 2010-05-25 16:45:26 +0000
317+++ lib/lp/bugs/browser/bugtask.py 2010-06-14 20:18:33 +0000
318@@ -2646,7 +2646,7 @@
319 dict(
320 value=term.token, title=term.title or term.token,
321 checked=term.value in default_values))
322- return helpers.shortlist(widget_values, longest_expected=11)
323+ return helpers.shortlist(widget_values, longest_expected=12)
324
325 def getStatusWidgetValues(self):
326 """Return data used to render the status checkboxes."""
327
328=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
329--- lib/lp/bugs/browser/tests/test_bugtask.py 2010-05-25 14:50:42 +0000
330+++ lib/lp/bugs/browser/tests/test_bugtask.py 2010-06-14 20:18:33 +0000
331@@ -245,8 +245,8 @@
332 self.bug.default_bugtask, LaunchpadTestRequest())
333 view.initialize()
334 self.assertEqual(
335- ['New', 'Incomplete', 'Invalid', 'Confirmed', 'In Progress',
336- 'Fix Committed', 'Fix Released'],
337+ ['New', 'Incomplete', 'Opinion', 'Invalid', 'Confirmed',
338+ 'In Progress', 'Fix Committed', 'Fix Released'],
339 self.getWidgetOptionTitles(view.form_fields['status']))
340
341 def test_status_field_privileged_persons(self):
342@@ -260,8 +260,9 @@
343 self.bug.default_bugtask, LaunchpadTestRequest())
344 view.initialize()
345 self.assertEqual(
346- ['New', 'Incomplete', 'Invalid', "Won't Fix", 'Confirmed',
347- 'Triaged', 'In Progress', 'Fix Committed', 'Fix Released'],
348+ ['New', 'Incomplete', 'Opinion', 'Invalid', "Won't Fix",
349+ 'Confirmed', 'Triaged', 'In Progress', 'Fix Committed',
350+ 'Fix Released'],
351 self.getWidgetOptionTitles(view.form_fields['status']),
352 'Unexpected set of settable status options for %s'
353 % user.name)
354@@ -278,8 +279,8 @@
355 self.bug.default_bugtask, LaunchpadTestRequest())
356 view.initialize()
357 self.assertEqual(
358- ['New', 'Incomplete', 'Invalid', 'Confirmed', 'In Progress',
359- 'Fix Committed', 'Fix Released', 'Unknown'],
360+ ['New', 'Incomplete', 'Opinion', 'Invalid', 'Confirmed',
361+ 'In Progress', 'Fix Committed', 'Fix Released', 'Unknown'],
362 self.getWidgetOptionTitles(view.form_fields['status']))
363
364 def test_status_field_bug_task_in_status_expired(self):
365@@ -292,8 +293,8 @@
366 self.bug.default_bugtask, LaunchpadTestRequest())
367 view.initialize()
368 self.assertEqual(
369- ['New', 'Incomplete', 'Invalid', 'Expired', 'Confirmed',
370- 'In Progress', 'Fix Committed', 'Fix Released'],
371+ ['New', 'Incomplete', 'Opinion', 'Invalid', 'Expired',
372+ 'Confirmed', 'In Progress', 'Fix Committed', 'Fix Released'],
373 self.getWidgetOptionTitles(view.form_fields['status']))
374
375
376
377=== modified file 'lib/lp/bugs/configure.zcml'
378--- lib/lp/bugs/configure.zcml 2010-06-04 09:31:21 +0000
379+++ lib/lp/bugs/configure.zcml 2010-06-14 20:18:33 +0000
380@@ -969,18 +969,6 @@
381 factory="lp.bugs.browser.bugtarget.BugsVHostBreadcrumb"
382 permission="zope.Public"/>
383
384- <!-- CalculateBugHeatJobs -->
385- <class class="lp.bugs.model.bugheat.CalculateBugHeatJob">
386- <allow interface="lp.bugs.interfaces.bugjob.IBugJob" />
387- <allow interface="lp.bugs.interfaces.bugjob.ICalculateBugHeatJob"/>
388- </class>
389- <securedutility
390- component="lp.bugs.model.bugheat.CalculateBugHeatJob"
391- provides="lp.bugs.interfaces.bugjob.ICalculateBugHeatJobSource">
392- <allow
393- interface="lp.bugs.interfaces.bugjob.ICalculateBugHeatJobSource"/>
394- </securedutility>
395-
396 <!-- ProcessApportBlobJobs -->
397 <class class="lp.bugs.model.apportjob.ProcessApportBlobJob">
398 <allow interface="lp.bugs.interfaces.apportjob.IApportJob" />
399
400=== modified file 'lib/lp/bugs/doc/bugtask-status-workflow.txt'
401--- lib/lp/bugs/doc/bugtask-status-workflow.txt 2010-04-15 15:28:22 +0000
402+++ lib/lp/bugs/doc/bugtask-status-workflow.txt 2010-06-14 20:18:33 +0000
403@@ -145,7 +145,7 @@
404 >>> ubuntu_firefox_task.date_inprogress is None
405 True
406
407-Marking the bug Triaged sets `date_triged`.
408+Marking the bug Triaged sets `date_triaged`.
409
410 >>> print ubuntu_firefox_task.date_triaged
411 None
412@@ -188,6 +188,16 @@
413
414 >>> ubuntu_firefox_task.transitionToStatus(
415 ... BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
416+ >>> ubuntu_firefox_task.date_closed is None
417+ True
418+
419+ >>> ubuntu_firefox_task.transitionToStatus(
420+ ... BugTaskStatus.OPINION, getUtility(ILaunchBag).user)
421+ >>> ubuntu_firefox_task.date_closed
422+ datetime.datetime...
423+
424+ >>> ubuntu_firefox_task.transitionToStatus(
425+ ... BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
426 >>> ubuntu_firefox_task.date_inprogress is None
427 True
428 >>> ubuntu_firefox_task.transitionToStatus(
429
430=== modified file 'lib/lp/bugs/interfaces/bugjob.py'
431--- lib/lp/bugs/interfaces/bugjob.py 2010-01-22 21:44:19 +0000
432+++ lib/lp/bugs/interfaces/bugjob.py 2010-06-14 20:18:33 +0000
433@@ -8,8 +8,6 @@
434 'BugJobType',
435 'IBugJob',
436 'IBugJobSource',
437- 'ICalculateBugHeatJob',
438- 'ICalculateBugHeatJobSource',
439 ]
440
441 from zope.interface import Attribute, Interface
442@@ -19,7 +17,7 @@
443
444 from lazr.enum import DBEnumeratedType, DBItem
445 from lp.bugs.interfaces.bug import IBug
446-from lp.services.job.interfaces.job import IJob, IJobSource, IRunnableJob
447+from lp.services.job.interfaces.job import IJob, IJobSource
448
449
450 class BugJobType(DBEnumeratedType):
451@@ -57,11 +55,3 @@
452
453 def create(bug):
454 """Create a new IBugJob for a bug."""
455-
456-
457-class ICalculateBugHeatJob(IRunnableJob):
458- """A Job to calculate bug heat."""
459-
460-
461-class ICalculateBugHeatJobSource(IBugJobSource):
462- """Interface for acquiring CalculateBugHeatJobs."""
463
464=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
465--- lib/lp/bugs/interfaces/bugtask.py 2010-06-07 19:48:29 +0000
466+++ lib/lp/bugs/interfaces/bugtask.py 2010-06-14 20:18:33 +0000
467@@ -159,6 +159,14 @@
468 the user was visiting when the bug occurred, etc.
469 """)
470
471+ OPINION = DBItem(16, """
472+ Opinion
473+
474+ The bug remains open for discussion only. This status is usually
475+ used where there is disagreement over whether the bug is relevant
476+ to the current target and whether it should be fixed.
477+ """)
478+
479 INVALID = DBItem(17, """
480 Invalid
481
482@@ -235,8 +243,8 @@
483
484 sort_order = (
485 'NEW', 'INCOMPLETE_WITH_RESPONSE', 'INCOMPLETE_WITHOUT_RESPONSE',
486- 'INCOMPLETE', 'INVALID', 'WONTFIX', 'EXPIRED', 'CONFIRMED', 'TRIAGED',
487- 'INPROGRESS', 'FIXCOMMITTED', 'FIXRELEASED')
488+ 'INCOMPLETE', 'OPINION', 'INVALID', 'WONTFIX', 'EXPIRED',
489+ 'CONFIRMED', 'TRIAGED', 'INPROGRESS', 'FIXCOMMITTED', 'FIXRELEASED')
490
491 INCOMPLETE_WITH_RESPONSE = DBItem(35, """
492 Incomplete (with response)
493@@ -312,6 +320,7 @@
494
495 RESOLVED_BUGTASK_STATUSES = (
496 BugTaskStatus.FIXRELEASED,
497+ BugTaskStatus.OPINION,
498 BugTaskStatus.INVALID,
499 BugTaskStatus.WONTFIX,
500 BugTaskStatus.EXPIRED)
501
502=== modified file 'lib/lp/bugs/model/bug.py'
503--- lib/lp/bugs/model/bug.py 2010-06-10 18:55:22 +0000
504+++ lib/lp/bugs/model/bug.py 2010-06-14 20:18:33 +0000
505@@ -83,7 +83,6 @@
506 from lp.bugs.interfaces.bugtracker import BugTrackerType
507 from lp.bugs.interfaces.bugwatch import IBugWatchSet
508 from lp.bugs.interfaces.cve import ICveSet
509-from lp.bugs.scripts.bugheat import BugHeatConstants
510 from lp.bugs.model.bugattachment import BugAttachment
511 from lp.bugs.model.bugbranch import BugBranch
512 from lp.bugs.model.bugcve import BugCve
513
514=== removed file 'lib/lp/bugs/model/bugheat.py'
515--- lib/lp/bugs/model/bugheat.py 2010-01-21 20:46:03 +0000
516+++ lib/lp/bugs/model/bugheat.py 1970-01-01 00:00:00 +0000
517@@ -1,54 +0,0 @@
518-# Copyright 2010 Canonical Ltd. This software is licensed under the
519-# GNU Affero General Public License version 3 (see the file LICENSE).
520-
521-"""Job classes related to BugJobs are in here."""
522-
523-__metaclass__ = type
524-__all__ = [
525- 'CalculateBugHeatJob',
526- ]
527-
528-from zope.component import getUtility
529-from zope.interface import classProvides, implements
530-
531-from canonical.launchpad.webapp.interfaces import (
532- DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE)
533-
534-from lp.bugs.interfaces.bugjob import (
535- BugJobType, ICalculateBugHeatJob, ICalculateBugHeatJobSource)
536-from lp.bugs.model.bugjob import BugJob, BugJobDerived
537-from lp.bugs.scripts.bugheat import BugHeatCalculator
538-from lp.services.job.model.job import Job
539-
540-
541-class CalculateBugHeatJob(BugJobDerived):
542- """A Job to calculate bug heat."""
543- implements(ICalculateBugHeatJob)
544-
545- class_job_type = BugJobType.UPDATE_HEAT
546- classProvides(ICalculateBugHeatJobSource)
547-
548- def run(self):
549- """See `IRunnableJob`."""
550- calculator = BugHeatCalculator(self.bug)
551- calculated_heat = calculator.getBugHeat()
552- self.bug.setHeat(calculated_heat)
553-
554- @classmethod
555- def create(cls, bug):
556- """See `ICalculateBugHeatJobSource`."""
557- # If there's already a job for the bug, don't create a new one.
558- store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
559- job_for_bug = store.find(
560- BugJob,
561- BugJob.bug == bug,
562- BugJob.job_type == cls.class_job_type,
563- BugJob.job == Job.id,
564- Job.id.is_in(Job.ready_jobs)
565- ).any()
566-
567- if job_for_bug is not None:
568- return cls(job_for_bug)
569- else:
570- return super(CalculateBugHeatJob, cls).create(bug)
571-
572
573=== removed file 'lib/lp/bugs/scripts/bugheat.py'
574--- lib/lp/bugs/scripts/bugheat.py 2010-04-29 11:31:49 +0000
575+++ lib/lp/bugs/scripts/bugheat.py 1970-01-01 00:00:00 +0000
576@@ -1,108 +0,0 @@
577-# Copyright 2010 Canonical Ltd. This software is licensed under the
578-# GNU Affero General Public License version 3 (see the file LICENSE).
579-
580-"""The innards of the Bug Heat cronscript."""
581-
582-__metaclass__ = type
583-__all__ = [
584- 'BugHeatCalculator',
585- 'BugHeatConstants',
586- ]
587-
588-from datetime import datetime
589-
590-from lp.bugs.interfaces.bugtask import RESOLVED_BUGTASK_STATUSES
591-
592-class BugHeatConstants:
593-
594- PRIVACY = 150
595- SECURITY = 250
596- DUPLICATE = 6
597- AFFECTED_USER = 4
598- SUBSCRIBER = 2
599-
600-
601-class BugHeatCalculator:
602- """A class to calculate the heat for a bug."""
603- # If you change the way that bug heat is calculated, remember to update
604- # the description of how it is calculated at
605- # /lib/lp/bugs/help/bug-heat.html and
606- # https://help.launchpad.net/Bugs/BugHeat
607-
608- def __init__(self, bug):
609- self.bug = bug
610-
611- def _getHeatFromPrivacy(self):
612- """Return the heat generated by the bug's `private` attribute."""
613- if self.bug.private:
614- return BugHeatConstants.PRIVACY
615- else:
616- return 0
617-
618- def _getHeatFromSecurity(self):
619- """Return the heat generated if the bug is security related."""
620- if self.bug.security_related:
621- return BugHeatConstants.SECURITY
622- else:
623- return 0
624-
625- def _getHeatFromDuplicates(self):
626- """Return the heat generated by the bug's duplicates."""
627- return self.bug.duplicates.count() * BugHeatConstants.DUPLICATE
628-
629- def _getHeatFromAffectedUsers(self):
630- """Return the heat generated by the bug's affected users."""
631- return (
632- self.bug.users_affected_count_with_dupes *
633- BugHeatConstants.AFFECTED_USER)
634-
635- def _getHeatFromSubscribers(self):
636- """Return the heat generated by the bug's subscribers."""
637- direct_subscribers = self.bug.getDirectSubscribers()
638- subscribers_from_dupes = self.bug.getSubscribersFromDuplicates()
639-
640- subscriber_count = (
641- len(direct_subscribers) + len(subscribers_from_dupes))
642- return subscriber_count * BugHeatConstants.SUBSCRIBER
643-
644- def _bugIsComplete(self):
645- """Are all the tasks for this bug resolved?"""
646- return all([(task.status in RESOLVED_BUGTASK_STATUSES)
647- for task in self.bug.bugtasks])
648-
649- def getBugHeat(self):
650- """Return the total heat for the current bug."""
651- if self._bugIsComplete():
652- return 0
653-
654- total_heat = sum([
655- self._getHeatFromAffectedUsers(),
656- self._getHeatFromDuplicates(),
657- self._getHeatFromPrivacy(),
658- self._getHeatFromSecurity(),
659- self._getHeatFromSubscribers(),
660- ])
661-
662- # Bugs decay over time. Every day the bug isn't touched its heat
663- # decreases by 1%.
664- days = (
665- datetime.utcnow() -
666- self.bug.date_last_updated.replace(tzinfo=None)).days
667- total_heat = int(total_heat * (0.99 ** days))
668-
669- if days > 0:
670- # Bug heat increases by a quarter of the maximum bug heat divided
671- # by the number of days since the bug's creation date.
672- days_since_last_activity = (
673- datetime.utcnow() -
674- max(self.bug.date_last_updated.replace(tzinfo=None),
675- self.bug.date_last_message.replace(tzinfo=None))).days
676- days_since_created = (
677- datetime.utcnow() - self.bug.datecreated.replace(tzinfo=None)).days
678- max_heat = max(
679- task.target.max_bug_heat for task in self.bug.bugtasks)
680- if max_heat is not None and days_since_created > 0:
681- total_heat = total_heat + (max_heat * 0.25 / days_since_created)
682-
683- return int(total_heat)
684-
685
686=== removed file 'lib/lp/bugs/scripts/tests/test_bugheat.py'
687--- lib/lp/bugs/scripts/tests/test_bugheat.py 2010-04-29 11:31:49 +0000
688+++ lib/lp/bugs/scripts/tests/test_bugheat.py 1970-01-01 00:00:00 +0000
689@@ -1,256 +0,0 @@
690-# Copyright 2010 Canonical Ltd. This software is licensed under the
691-# GNU Affero General Public License version 3 (see the file LICENSE).
692-
693-"""Module docstring goes here."""
694-
695-__metaclass__ = type
696-
697-import unittest
698-
699-from datetime import datetime, timedelta
700-
701-from canonical.testing import LaunchpadZopelessLayer
702-
703-from lp.bugs.interfaces.bugtask import BugTaskStatus
704-from lp.bugs.scripts.bugheat import BugHeatCalculator, BugHeatConstants
705-from lp.testing import TestCaseWithFactory
706-
707-from zope.security.proxy import removeSecurityProxy
708-
709-
710-class TestBugHeatCalculator(TestCaseWithFactory):
711- """Tests for the BugHeatCalculator class."""
712- # If you change the way that bug heat is calculated, remember to update
713- # the description of how it is calculated at
714- # /lib/lp/bugs/help/bug-heat.html and
715- # https://help.launchpad.net/Bugs/BugHeat
716-
717- layer = LaunchpadZopelessLayer
718-
719- def setUp(self):
720- super(TestBugHeatCalculator, self).setUp()
721- self.bug = self.factory.makeBug()
722- self.calculator = BugHeatCalculator(self.bug)
723-
724- def test__getHeatFromDuplicates(self):
725- # BugHeatCalculator._getHeatFromDuplicates() returns the bug
726- # heat generated by duplicates of a bug.
727- # By default, the bug has no heat from dupes
728- self.assertEqual(0, self.calculator._getHeatFromDuplicates())
729-
730- # If adding duplicates, the heat generated by them will be n *
731- # BugHeatConstants.DUPLICATE, where n is the number of
732- # duplicates.
733- for i in range(5):
734- dupe = self.factory.makeBug()
735- dupe.duplicateof = self.bug
736-
737- expected_heat = BugHeatConstants.DUPLICATE * 5
738- actual_heat = self.calculator._getHeatFromDuplicates()
739- self.assertEqual(
740- expected_heat, actual_heat,
741- "Heat from duplicates does not match expected heat. "
742- "Expected %s, got %s" % (expected_heat, actual_heat))
743-
744- def test__getHeatFromAffectedUsers(self):
745- # BugHeatCalculator._getHeatFromAffectedUsers() returns the bug
746- # heat generated by users affected by the bug and by duplicate bugs.
747- # By default, the heat will be BugHeatConstants.AFFECTED_USER, since
748- # there will be one affected user (the user who filed the bug).
749- self.assertEqual(
750- BugHeatConstants.AFFECTED_USER,
751- self.calculator._getHeatFromAffectedUsers())
752-
753- # As the number of affected users increases, the heat generated
754- # will be n * BugHeatConstants.AFFECTED_USER, where n is the number
755- # of affected users.
756- for i in range(5):
757- person = self.factory.makePerson()
758- self.bug.markUserAffected(person)
759-
760- expected_heat = BugHeatConstants.AFFECTED_USER * 6
761- actual_heat = self.calculator._getHeatFromAffectedUsers()
762- self.assertEqual(
763- expected_heat, actual_heat,
764- "Heat from affected users does not match expected heat. "
765- "Expected %s, got %s" % (expected_heat, actual_heat))
766-
767- # When our bug has duplicates, users affected by these duplicates
768- # are included in _getHeatFromAffectedUsers() of the main bug.
769- for i in range(3):
770- dupe = self.factory.makeBug()
771- dupe.duplicateof = self.bug
772- # Each bug reporter is by default also marked as being affected
773- # by the bug, so we have three additional affected users.
774- expected_heat += BugHeatConstants.AFFECTED_USER * 3
775-
776- person = self.factory.makePerson()
777- dupe.markUserAffected(person)
778- expected_heat += BugHeatConstants.AFFECTED_USER
779- actual_heat = self.calculator._getHeatFromAffectedUsers()
780- self.assertEqual(
781- expected_heat, actual_heat,
782- "Heat from users affected by duplicate bugs does not match "
783- "expected heat. Expected %s, got %s"
784- % (expected_heat, actual_heat))
785-
786- def test__getHeatFromSubscribers(self):
787- # BugHeatCalculator._getHeatFromSubscribers() returns the bug
788- # heat generated by users subscribed tothe bug.
789- # By default, the heat will be BugHeatConstants.SUBSCRIBER,
790- # since there will be one direct subscriber (the user who filed
791- # the bug).
792- self.assertEqual(
793- BugHeatConstants.SUBSCRIBER,
794- self.calculator._getHeatFromSubscribers())
795-
796- # As the number of subscribers increases, the heat generated
797- # will be n * BugHeatConstants.SUBSCRIBER, where n is the number
798- # of subscribers.
799- for i in range(5):
800- person = self.factory.makePerson()
801- self.bug.subscribe(person, person)
802-
803- expected_heat = BugHeatConstants.SUBSCRIBER * 6
804- actual_heat = self.calculator._getHeatFromSubscribers()
805- self.assertEqual(
806- expected_heat, actual_heat,
807- "Heat from subscribers does not match expected heat. "
808- "Expected %s, got %s" % (expected_heat, actual_heat))
809-
810- # Subscribers from duplicates are included in the heat returned
811- # by _getHeatFromSubscribers()
812- dupe = self.factory.makeBug()
813- dupe.duplicateof = self.bug
814- expected_heat = BugHeatConstants.SUBSCRIBER * 7
815- actual_heat = self.calculator._getHeatFromSubscribers()
816- self.assertEqual(
817- expected_heat, actual_heat,
818- "Heat from subscribers (including duplicate-subscribers) "
819- "does not match expected heat. Expected %s, got %s" %
820- (expected_heat, actual_heat))
821-
822- # Seting the bug to private will increase its heat from
823- # subscribers by 1 * BugHeatConstants.SUBSCRIBER, as the project
824- # owner will now be directly subscribed to it.
825- self.bug.setPrivate(True, self.bug.owner)
826- expected_heat = BugHeatConstants.SUBSCRIBER * 8
827- actual_heat = self.calculator._getHeatFromSubscribers()
828- self.assertEqual(
829- expected_heat, actual_heat,
830- "Heat from subscribers to private bug does not match expected "
831- "heat. Expected %s, got %s" % (expected_heat, actual_heat))
832-
833- def test__getHeatFromPrivacy(self):
834- # BugHeatCalculator._getHeatFromPrivacy() returns the heat
835- # generated by the bug's private attribute. If the bug is
836- # public, this will be 0.
837- self.assertEqual(0, self.calculator._getHeatFromPrivacy())
838-
839- # However, if the bug is private, _getHeatFromPrivacy() will
840- # return BugHeatConstants.PRIVACY.
841- self.bug.setPrivate(True, self.bug.owner)
842- self.assertEqual(
843- BugHeatConstants.PRIVACY, self.calculator._getHeatFromPrivacy())
844-
845- def test__getHeatFromSecurity(self):
846- # BugHeatCalculator._getHeatFromSecurity() returns the heat
847- # generated by the bug's security_related attribute. If the bug
848- # is not security related, _getHeatFromSecurity() will return 0.
849- self.assertEqual(0, self.calculator._getHeatFromPrivacy())
850-
851-
852- # If, on the other hand, the bug is security_related,
853- # _getHeatFromSecurity() will return BugHeatConstants.SECURITY
854- self.bug.setSecurityRelated(True)
855- self.assertEqual(
856- BugHeatConstants.SECURITY, self.calculator._getHeatFromSecurity())
857-
858- def test_getBugHeat(self):
859- # BugHeatCalculator.getBugHeat() returns the total heat for a
860- # given bug as the sum of the results of all _getHeatFrom*()
861- # methods.
862- # By default this will be (BugHeatConstants.AFFECTED_USER +
863- # BugHeatConstants.SUBSCRIBER) since there will be one
864- # subscriber and one affected user only.
865- expected_heat = (
866- BugHeatConstants.AFFECTED_USER + BugHeatConstants.SUBSCRIBER)
867- actual_heat = self.calculator.getBugHeat()
868- self.assertEqual(
869- expected_heat, actual_heat,
870- "Expected bug heat did not match actual bug heat. "
871- "Expected %s, got %s" % (expected_heat, actual_heat))
872-
873- # Adding a duplicate and making the bug private and security
874- # related will increase its heat.
875- dupe = self.factory.makeBug()
876- dupe.duplicateof = self.bug
877- self.bug.setPrivate(True, self.bug.owner)
878- self.bug.setSecurityRelated(True)
879-
880- expected_heat += (
881- BugHeatConstants.DUPLICATE +
882- BugHeatConstants.PRIVACY +
883- BugHeatConstants.SECURITY +
884- BugHeatConstants.AFFECTED_USER
885- )
886-
887- # Adding the duplicate and making the bug private means it gets
888- # two new subscribers, the project owner and the duplicate's
889- # direct subscriber.
890- expected_heat += BugHeatConstants.SUBSCRIBER * 2
891- actual_heat = self.calculator.getBugHeat()
892- self.assertEqual(
893- expected_heat, actual_heat,
894- "Expected bug heat did not match actual bug heat. "
895- "Expected %s, got %s" % (expected_heat, actual_heat))
896-
897- def test_getBugHeat_complete_bugs(self):
898- # Bug which are in a resolved status don't have heat at all.
899- complete_bug = self.factory.makeBug()
900- heat = BugHeatCalculator(complete_bug).getBugHeat()
901- self.assertNotEqual(
902- 0, heat,
903- "Expected bug heat did not match actual bug heat. "
904- "Expected a positive value, got 0")
905- complete_bug.bugtasks[0].transitionToStatus(
906- BugTaskStatus.INVALID, complete_bug.owner)
907- heat = BugHeatCalculator(complete_bug).getBugHeat()
908- self.assertEqual(
909- 0, heat,
910- "Expected bug heat did not match actual bug heat. "
911- "Expected %s, got %s" % (0, heat))
912-
913- def test_getBugHeat_decay(self):
914- # Every day, a bug that wasn't touched has its heat reduced by 1%.
915- aging_bug = self.factory.makeBug()
916- fresh_heat = BugHeatCalculator(aging_bug).getBugHeat()
917- aging_bug.date_last_updated = (
918- aging_bug.date_last_updated - timedelta(days=1))
919- expected = int(fresh_heat * 0.99)
920- heat = BugHeatCalculator(aging_bug).getBugHeat()
921- self.assertEqual(
922- expected, heat,
923- "Expected bug heat did not match actual bug heat. "
924- "Expected %s, got %s" % (expected, heat))
925-
926- def test_getBugHeat_activity(self):
927- # Bug heat increases by a quarter of the maximum bug heat divided by
928- # the number of days between the bug's creating and its last activity.
929- active_bug = removeSecurityProxy(self.factory.makeBug())
930- fresh_heat = BugHeatCalculator(active_bug).getBugHeat()
931- active_bug.date_last_updated = (
932- active_bug.date_last_updated - timedelta(days=10))
933- active_bug.datecreated = (active_bug.datecreated - timedelta(days=20))
934- active_bug.default_bugtask.target.setMaxBugHeat(100)
935- expected = int((fresh_heat * (0.99 ** 20)) + (100 * 0.25 / 20))
936- heat = BugHeatCalculator(active_bug).getBugHeat()
937- self.assertEqual(
938- expected, heat,
939- "Expected bug heat did not match actual bug heat. "
940- "Expected %s, got %s" % (expected, heat))
941-
942-
943-
944-def test_suite():
945- return unittest.TestLoader().loadTestsFromName(__name__)
946
947=== modified file 'lib/lp/bugs/tests/bugs-emailinterface.txt'
948--- lib/lp/bugs/tests/bugs-emailinterface.txt 2010-05-27 13:51:06 +0000
949+++ lib/lp/bugs/tests/bugs-emailinterface.txt 2010-06-14 20:18:33 +0000
950@@ -1435,7 +1435,7 @@
951 status foo
952 ...
953 The 'status' command expects any of the following arguments:
954- new, incomplete, invalid, wontfix, expired, confirmed, triaged, inprogress, fixcommitted, fixreleased
955+ new, incomplete, opinion, invalid, wontfix, expired, confirmed, triaged, inprogress, fixcommitted, fixreleased
956 <BLANKLINE>
957 For example:
958 <BLANKLINE>
959
960=== modified file 'lib/lp/bugs/tests/bugtarget-bugcount.txt'
961--- lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-04-15 13:26:33 +0000
962+++ lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-06-14 20:18:33 +0000
963@@ -11,6 +11,7 @@
964 ... print status.name
965 NEW
966 INCOMPLETE
967+ OPINION
968 INVALID
969 WONTFIX
970 EXPIRED
971@@ -54,6 +55,7 @@
972 ... print_count_difference(new_bug_counts, old_counts, status)
973 NEW: 5 bug(s) more
974 INCOMPLETE: 5 bug(s) more
975+ OPINION: 5 bug(s) more
976 INVALID: 5 bug(s) more
977 WONTFIX: 5 bug(s) more
978 EXPIRED: 5 bug(s) more
979
980=== modified file 'lib/lp/bugs/tests/test_bugheat.py'
981--- lib/lp/bugs/tests/test_bugheat.py 2010-05-27 13:56:03 +0000
982+++ lib/lp/bugs/tests/test_bugheat.py 2010-06-14 20:18:33 +0000
983@@ -5,114 +5,13 @@
984
985 __metaclass__ = type
986
987-import pytz
988-import transaction
989 import unittest
990-from datetime import datetime
991-
992-from zope.component import getUtility
993-
994-from canonical.launchpad.scripts.tests import run_script
995+
996 from canonical.testing import LaunchpadZopelessLayer
997
998-from lp.bugs.adapters.bugchange import BugDescriptionChange
999-from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource
1000-from lp.bugs.model.bugheat import CalculateBugHeatJob
1001-from lp.bugs.scripts.bugheat import BugHeatCalculator
1002-from lp.testing import TestCaseWithFactory
1003 from lp.testing.factory import LaunchpadObjectFactory
1004
1005
1006-class CalculateBugHeatJobTestCase(TestCaseWithFactory):
1007- """Test case for CalculateBugHeatJob."""
1008-
1009- layer = LaunchpadZopelessLayer
1010-
1011- def setUp(self):
1012- super(CalculateBugHeatJobTestCase, self).setUp()
1013- self.bug = self.factory.makeBug()
1014-
1015- # NB: This looks like it should go in the teardown, however
1016- # creating the bug causes a job to be added for it. We clear
1017- # this out so that our tests are consistent.
1018- self._completeJobsAndAssertQueueEmpty()
1019-
1020- def _completeJobsAndAssertQueueEmpty(self):
1021- """Make sure that all the CalculateBugHeatJobs are completed."""
1022- for bug_job in getUtility(ICalculateBugHeatJobSource).iterReady():
1023- bug_job.job.start()
1024- bug_job.job.complete()
1025- self.assertEqual(0, self._getJobCount())
1026-
1027- def _getJobCount(self):
1028- """Return the number of CalculateBugHeatJobs in the queue."""
1029- return len(self._getJobs())
1030-
1031- def _getJobs(self):
1032- """Return the pending CalculateBugHeatJobs as a list."""
1033- return list(CalculateBugHeatJob.iterReady())
1034-
1035- def test_run(self):
1036- # CalculateBugHeatJob.run() sets calculates and sets the heat
1037- # for a bug.
1038- job = CalculateBugHeatJob.create(self.bug)
1039- bug_heat_calculator = BugHeatCalculator(self.bug)
1040-
1041- job.run()
1042- self.assertEqual(
1043- bug_heat_calculator.getBugHeat(), self.bug.heat)
1044-
1045- def test_utility(self):
1046- # CalculateBugHeatJobSource is a utility for acquiring
1047- # CalculateBugHeatJobs.
1048- utility = getUtility(ICalculateBugHeatJobSource)
1049- self.assertTrue(
1050- ICalculateBugHeatJobSource.providedBy(utility))
1051-
1052- def test_create_only_creates_one(self):
1053- # If there's already a CalculateBugHeatJob for a bug,
1054- # CalculateBugHeatJob.create() won't create a new one.
1055- job = CalculateBugHeatJob.create(self.bug)
1056-
1057- # There will now be one job in the queue.
1058- self.assertEqual(1, self._getJobCount())
1059-
1060- new_job = CalculateBugHeatJob.create(self.bug)
1061-
1062- # The two jobs will in fact be the same job.
1063- self.assertEqual(job, new_job)
1064-
1065- # And the queue will still have a length of 1.
1066- self.assertEqual(1, self._getJobCount())
1067-
1068- def test_cronscript_succeeds(self):
1069- # The calculate-bug-heat cronscript will run all pending
1070- # CalculateBugHeatJobs.
1071- CalculateBugHeatJob.create(self.bug)
1072- transaction.commit()
1073-
1074- retcode, stdout, stderr = run_script(
1075- 'cronscripts/calculate-bug-heat.py', [],
1076- expect_returncode=0)
1077- self.assertEqual('', stdout)
1078- self.assertIn(
1079- 'INFO Ran 1 CalculateBugHeatJob jobs.\n', stderr)
1080-
1081- def test_getOopsVars(self):
1082- # BugJobDerived.getOopsVars() returns the variables to be used
1083- # when logging an OOPS for a bug job. We test this using
1084- # CalculateBugHeatJob because BugJobDerived doesn't let us
1085- # create() jobs.
1086- job = CalculateBugHeatJob.create(self.bug)
1087- vars = job.getOopsVars()
1088-
1089- # The Bug ID, BugJob ID and BugJob type will be returned by
1090- # getOopsVars().
1091- self.assertIn(('bug_id', self.bug.id), vars)
1092- self.assertIn(('bug_job_id', job.context.id), vars)
1093- self.assertIn(('bug_job_type', job.context.job_type.title), vars)
1094-
1095-
1096 class MaxHeatByTargetBase:
1097 """Base class for testing a bug target's max_bug_heat attribute."""
1098
1099
1100=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
1101--- lib/lp/code/browser/sourcepackagerecipe.py 2010-06-12 13:34:11 +0000
1102+++ lib/lp/code/browser/sourcepackagerecipe.py 2010-06-14 20:18:33 +0000
1103@@ -309,7 +309,10 @@
1104 'name',
1105 'description',
1106 'owner',
1107+ 'build_daily'
1108 ])
1109+ daily_build_archive = Choice(vocabulary='TargetPPAs',
1110+ title=u'Daily build archive')
1111 distros = List(
1112 Choice(vocabulary='BuildableDistroSeries'),
1113 title=u'Default Distribution series')
1114@@ -318,10 +321,16 @@
1115 description=u'The text of the recipe.')
1116
1117
1118+
1119 class RecipeTextValidatorMixin:
1120 """Class to validate that the Source Package Recipe text is valid."""
1121
1122 def validate(self, data):
1123+ if data['build_daily']:
1124+ if len(data['distros']) == 0:
1125+ self.setFieldError(
1126+ 'distros',
1127+ 'You must specify at least one series for daily builds.')
1128 try:
1129 parser = RecipeParser(data['recipe_text'])
1130 parser.parse()
1131@@ -343,7 +352,8 @@
1132 def initial_values(self):
1133 return {
1134 'recipe_text': MINIMAL_RECIPE_TEXT % self.context.bzr_identity,
1135- 'owner': self.user}
1136+ 'owner': self.user,
1137+ 'build_daily': False}
1138
1139 @property
1140 def cancel_url(self):
1141@@ -357,7 +367,8 @@
1142 source_package_recipe = getUtility(
1143 ISourcePackageRecipeSource).new(
1144 self.user, self.user, data['name'], recipe,
1145- data['description'], data['distros'])
1146+ data['description'], data['distros'],
1147+ data['daily_build_archive'], data['build_daily'])
1148 except ForbiddenInstruction:
1149 # XXX: bug=592513 We shouldn't be hardcoding "run" here.
1150 self.setFieldError(
1151
1152=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
1153--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-06-12 13:52:31 +0000
1154+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-06-14 20:18:33 +0000
1155@@ -69,6 +69,14 @@
1156
1157 layer = DatabaseFunctionalLayer
1158
1159+ def makeBranch(self):
1160+ product = self.factory.makeProduct(
1161+ name='ratatouille', displayname='Ratatouille')
1162+ branch = self.factory.makeBranch(
1163+ owner=self.chef, product=product, name='veggies')
1164+ self.factory.makeSourcePackage(sourcepackagename='ratatouille')
1165+ return branch
1166+
1167 def test_create_new_recipe_not_logged_in(self):
1168 from canonical.launchpad.testing.pages import setupBrowser
1169 product = self.factory.makeProduct(
1170@@ -85,11 +93,7 @@
1171 Unauthorized, browser.getLink('Create packaging recipe').click)
1172
1173 def test_create_new_recipe(self):
1174- product = self.factory.makeProduct(
1175- name='ratatouille', displayname='Ratatouille')
1176- branch = self.factory.makeBranch(
1177- owner=self.chef, product=product, name='veggies')
1178-
1179+ branch = self.makeBranch()
1180 # A new recipe can be created from the branch page.
1181 browser = self.getUserBrowser(canonical_url(branch), user=self.chef)
1182 browser.getLink('Create packaging recipe').click()
1183@@ -97,6 +101,7 @@
1184 browser.getControl(name='field.name').value = 'daily'
1185 browser.getControl('Description').value = 'Make some food!'
1186 browser.getControl('Secret Squirrel').click()
1187+ browser.getControl('Build daily').click()
1188 browser.getControl('Create Recipe').click()
1189
1190 pattern = """\
1191@@ -107,9 +112,11 @@
1192 Make some food!
1193
1194 Recipe information
1195+ Build daily: True
1196 Owner: Master Chef
1197 Base branch: lp://dev/~chef/ratatouille/veggies
1198 Debian version: 1.0
1199+ Daily build archive: Secret PPA
1200 Distribution series: Secret Squirrel
1201 .*
1202
1203@@ -168,10 +175,7 @@
1204 def test_create_recipe_bad_text(self):
1205 # If a user tries to create source package recipe with bad text, they
1206 # should get an error.
1207- product = self.factory.makeProduct(
1208- name='ratatouille', displayname='Ratatouille')
1209- branch = self.factory.makeBranch(
1210- owner=self.chef, product=product, name='veggies')
1211+ branch = self.makeBranch()
1212
1213 # A new recipe can be created from the branch page.
1214 browser = self.getUserBrowser(canonical_url(branch), user=self.chef)
1215@@ -186,6 +190,17 @@
1216 extract_text(find_tags_by_class(browser.contents, 'message')[1]),
1217 'The recipe text is not a valid bzr-builder recipe.')
1218
1219+ def test_create_recipe_no_distroseries(self):
1220+ browser = self.getViewBrowser(self.makeBranch(), '+new-recipe')
1221+ browser.getControl(name='field.name').value = 'daily'
1222+ browser.getControl('Description').value = 'Make some food!'
1223+
1224+ browser.getControl('Build daily').click()
1225+ browser.getControl('Create Recipe').click()
1226+ self.assertEqual(
1227+ extract_text(find_tags_by_class(browser.contents, 'message')[1]),
1228+ 'You must specify at least one series for daily builds.')
1229+
1230 def test_create_dupe_recipe(self):
1231 # You shouldn't be able to create a duplicate recipe owned by the same
1232 # person with the same name.
1233@@ -228,7 +243,11 @@
1234 recipe = self.factory.makeSourcePackageRecipe(
1235 owner=self.chef, registrant=self.chef,
1236 name=u'things', description=u'This is a recipe',
1237- distroseries=self.squirrel, branches=[veggie_branch])
1238+ distroseries=self.squirrel, branches=[veggie_branch],
1239+ daily_build_archive=self.ppa)
1240+ self.factory.makeArchive(
1241+ distribution=self.ppa.distribution, name='ppa2',
1242+ displayname="PPA 2", owner=self.chef)
1243
1244 meat_path = meat_branch.bzr_identity
1245
1246@@ -240,6 +259,7 @@
1247 MINIMAL_RECIPE_TEXT % meat_path)
1248 browser.getControl('Secret Squirrel').click()
1249 browser.getControl('Mumbly Midget').click()
1250+ browser.getControl('PPA 2').click()
1251 browser.getControl('Update Recipe').click()
1252
1253 pattern = """\
1254@@ -250,9 +270,12 @@
1255 This is stuff
1256
1257 Recipe information
1258+ Build daily: False
1259 Owner: Master Chef
1260 Base branch: lp://dev/~chef/ratatouille/meat
1261 Debian version: 1.0
1262+ Daily build archive:
1263+ PPA 2
1264 Distribution series: Mumbly Midget
1265 .*
1266
1267@@ -357,9 +380,13 @@
1268 This is stuff
1269
1270 Recipe information
1271+ Build daily:
1272+ False
1273 Owner: Master Chef
1274 Base branch: lp://dev/~chef/ratatouille/meat
1275 Debian version: 1.0
1276+ Daily build archive:
1277+ Secret PPA
1278 Distribution series: Mumbly Midget
1279 .*
1280
1281@@ -389,9 +416,11 @@
1282 This recipe .*changes.
1283
1284 Recipe information
1285+ Build daily: False
1286 Owner: Master Chef
1287 Base branch: lp://dev/~chef/chocolate/cake
1288 Debian version: 1.0
1289+ Daily build archive: Secret PPA
1290 Distribution series: Secret Squirrel
1291
1292 Build records
1293
1294=== modified file 'lib/lp/code/configure.zcml'
1295--- lib/lp/code/configure.zcml 2010-06-10 07:55:54 +0000
1296+++ lib/lp/code/configure.zcml 2010-06-14 20:18:33 +0000
1297@@ -1052,6 +1052,7 @@
1298 set_attributes="
1299 build_daily
1300 builder_recipe
1301+ daily_build_archive
1302 date_last_modified
1303 description
1304 distroseries
1305
1306=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
1307--- lib/lp/code/interfaces/sourcepackagerecipe.py 2010-06-11 05:05:52 +0000
1308+++ lib/lp/code/interfaces/sourcepackagerecipe.py 2010-06-14 20:18:33 +0000
1309@@ -95,7 +95,7 @@
1310 " build a source package for"),
1311 readonly=False)
1312 build_daily = Bool(
1313- title=_("If true, the recipe should be built daily."))
1314+ title=_("Build daily"))
1315
1316 name = exported(TextLine(
1317 title=_("Name"), required=True,
1318
1319=== modified file 'lib/lp/code/templates/sourcepackagerecipe-index.pt'
1320--- lib/lp/code/templates/sourcepackagerecipe-index.pt 2010-04-28 21:18:13 +0000
1321+++ lib/lp/code/templates/sourcepackagerecipe-index.pt 2010-06-14 20:18:33 +0000
1322@@ -36,6 +36,11 @@
1323 <div class="portlet">
1324 <h2>Recipe information</h2>
1325 <div class="two-column-list">
1326+ <dl id="build_daily">
1327+ <dt>Build daily:</dt>
1328+ <dd tal:content="context/build_daily" />
1329+ </dl>
1330+
1331 <dl id="owner">
1332 <dt>Owner:</dt>
1333 <dd tal:content="structure context/owner/fmt:link" />
1334@@ -48,6 +53,14 @@
1335 <dt>Debian version:</dt>
1336 <dd tal:content="context/deb_version_template" />
1337 </dl>
1338+ <dl id="daily_build_archive">
1339+ <dt>Daily build archive:</dt>
1340+ <dd tal:content="structure context/daily_build_archive/fmt:link"
1341+ tal:condition="context/daily_build_archive">
1342+ </dd>
1343+ <dd tal:condition="not: context/daily_build_archive">None</dd>
1344+ </dl>
1345+
1346 <dl id="distros">
1347 <dt>Distribution series:</dt>
1348 <dd>
1349
1350=== modified file 'utilities/report-database-stats.py'
1351--- utilities/report-database-stats.py 2010-04-29 12:38:05 +0000
1352+++ utilities/report-database-stats.py 2010-06-14 20:18:33 +0000
1353@@ -72,12 +72,22 @@
1354
1355
1356 def get_cpu_stats(cur, options):
1357+ # This query calculates the averate cpu utilization from the
1358+ # samples. It assumes samples are taken at regular intervals over
1359+ # the period.
1360 query = """
1361- SELECT avg(cpu), username FROM DatabaseCpuStats
1362+ SELECT (
1363+ CAST(SUM(cpu) AS float) / (
1364+ SELECT COUNT(DISTINCT date_created) FROM DatabaseCpuStats
1365+ WHERE
1366+ date_created >= (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
1367+ - CAST (%s AS interval))
1368+ ) AS avg_cpu, username
1369+ FROM DatabaseCpuStats
1370 WHERE date_created >= (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
1371 - CAST(%s AS interval))
1372 GROUP BY username
1373- """ % sqlvalues(options.since_interval)
1374+ """ % sqlvalues(options.since_interval, options.since_interval)
1375
1376 cur.execute(query)
1377
1378@@ -107,16 +117,20 @@
1379 tables = get_table_stats(cur, options)
1380 arbitrary_table = list(tables)[0]
1381 interval = arbitrary_table.date_end - arbitrary_table.date_start
1382- per_minute = interval.days * 24 * 60 + interval.seconds / 60.0
1383+ per_second = float(interval.days * 24 * 60 * 60 + interval.seconds)
1384
1385 print "== Most Read Tables =="
1386 print
1387+ # These match the pg_user_table_stats view. schemaname is the
1388+ # namespace (normally 'public'), relname is the table (relation)
1389+ # name. total_tup_red is the total number of rows read.
1390+ # idx_tup_fetch is the number of rows looked up using an index.
1391 tables_sort = ['total_tup_read', 'idx_tup_fetch', 'schemaname', 'relname']
1392 most_read_tables = sorted(
1393 tables, key=attrgetter(*tables_sort), reverse=True)
1394 for table in most_read_tables[:options.limit]:
1395- print "%40s || %10.2f tuples/min" % (
1396- table.relname, table.total_tup_read / per_minute)
1397+ print "%40s || %10.2f tuples/sec" % (
1398+ table.relname, table.total_tup_read / per_second)
1399 print
1400
1401 print "== Most Written Tables =="
1402@@ -126,15 +140,15 @@
1403 most_written_tables = sorted(
1404 tables, key=attrgetter(*tables_sort), reverse=True)
1405 for table in most_written_tables[:options.limit]:
1406- print "%40s || %10.2f tuples/min" % (
1407- table.relname, table.total_tup_written / per_minute)
1408+ print "%40s || %10.2f tuples/sec" % (
1409+ table.relname, table.total_tup_written / per_second)
1410 print
1411
1412 user_cpu = get_cpu_stats(cur, options)
1413 print "== Most Active Users =="
1414 print
1415 for cpu, username in sorted(user_cpu, reverse=True)[:options.limit]:
1416- print "%40s || %6.2f%% CPU" % (username, float(cpu) / 100)
1417+ print "%40s || %10.2f%% CPU" % (username, float(cpu) / 10)
1418
1419
1420 if __name__ == '__main__':