Merge lp:~bigkevmcd/offspring/non-scheduled-builds into lp:offspring

Proposed by Kevin McDermott
Status: Superseded
Proposed branch: lp:~bigkevmcd/offspring/non-scheduled-builds
Merge into: lp:offspring
Diff against target: 583 lines (+479/-14)
7 files modified
.bzrignore (+1/-0)
lib/offspring/web/queuemanager/management/commands/build_metrics.py (+15/-0)
lib/offspring/web/queuemanager/management/commands/tests/test_build_metrics.py (+28/-0)
lib/offspring/web/queuemanager/metrics.py (+109/-0)
lib/offspring/web/queuemanager/models.py (+7/-11)
lib/offspring/web/queuemanager/tests/factory.py (+3/-3)
lib/offspring/web/queuemanager/tests/test_metrics.py (+316/-0)
To merge this branch: bzr merge lp:~bigkevmcd/offspring/non-scheduled-builds
Reviewer Review Type Date Requested Status
Offspring Committers Pending
Offspring Committers Pending
Review via email: mp+85618@code.launchpad.net

This proposal has been superseded by a proposal from 2011-12-14.

Description of the change

This adds metrics for requested builds.

To post a comment you must log in.
128. By Kevin McDermott

Merge forward.

129. By Kevin McDermott

Merge forward.

130. By Kevin McDermott

Merge forward.

131. By Kevin McDermott

Merge forward.

132. By Kevin McDermott

Merge forward.

133. By Kevin McDermott

Merge forward.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2011-11-18 14:33:37 +0000
3+++ .bzrignore 2011-12-14 09:29:35 +0000
4@@ -7,3 +7,4 @@
5 ./config/launchpad.oauth
6 ./builds/
7 ./scripts/
8+.coverage
9
10=== added directory 'lib/offspring/web/queuemanager/management'
11=== added file 'lib/offspring/web/queuemanager/management/__init__.py'
12=== added directory 'lib/offspring/web/queuemanager/management/commands'
13=== added file 'lib/offspring/web/queuemanager/management/commands/__init__.py'
14=== added file 'lib/offspring/web/queuemanager/management/commands/build_metrics.py'
15--- lib/offspring/web/queuemanager/management/commands/build_metrics.py 1970-01-01 00:00:00 +0000
16+++ lib/offspring/web/queuemanager/management/commands/build_metrics.py 2011-12-14 09:29:35 +0000
17@@ -0,0 +1,15 @@
18+# Copyright 2010-2011 Canonical Ltd.
19+
20+from django.core.management.base import NoArgsCommand
21+
22+from offspring.web.queuemanager.metrics import get_build_result_metrics
23+
24+
25+class Command(NoArgsCommand):
26+ help = "Outputs some basic metrics."
27+
28+ def handle_noargs(self, **options):
29+ metrics = get_build_result_metrics()
30+
31+ for item, value in metrics:
32+ self.stdout.write("%s = %s\n" % (item, value))
33
34=== added directory 'lib/offspring/web/queuemanager/management/commands/tests'
35=== added file 'lib/offspring/web/queuemanager/management/commands/tests/__init__.py'
36=== added file 'lib/offspring/web/queuemanager/management/commands/tests/test_build_metrics.py'
37--- lib/offspring/web/queuemanager/management/commands/tests/test_build_metrics.py 1970-01-01 00:00:00 +0000
38+++ lib/offspring/web/queuemanager/management/commands/tests/test_build_metrics.py 2011-12-14 09:29:35 +0000
39@@ -0,0 +1,28 @@
40+# Copyright 2010-2011 Canonical Ltd.
41+from cStringIO import StringIO
42+
43+from mocker import MockerTestCase
44+
45+from offspring.web.queuemanager.metrics import get_build_result_metrics
46+from offspring.web.queuemanager.management.commands.build_metrics import (
47+ Command)
48+
49+
50+class TestBuildResultMetricsCommand(MockerTestCase):
51+
52+ def test_calls_get_build_result_metrics(self):
53+ """
54+ When called, the build_metrics command should fetch the list of metrics
55+ available for offspring.web.queuemanager and output them.
56+ """
57+ build_result_metrics_mock = self.mocker.replace(get_build_result_metrics)
58+ build_result_metrics_mock()
59+ self.mocker.result([("Description of value", 200)])
60+ self.mocker.replay()
61+
62+ command = Command()
63+ stdout = StringIO()
64+ command.stdout = stdout
65+ command.handle_noargs()
66+
67+ self.assertEqual("Description of value = 200\n", stdout.getvalue())
68
69=== added file 'lib/offspring/web/queuemanager/metrics.py'
70--- lib/offspring/web/queuemanager/metrics.py 1970-01-01 00:00:00 +0000
71+++ lib/offspring/web/queuemanager/metrics.py 2011-12-14 09:29:35 +0000
72@@ -0,0 +1,109 @@
73+from datetime import datetime, timedelta
74+
75+from offspring.web.queuemanager.models import BuildResult
76+from offspring.enums import ProjectBuildStates
77+
78+
79+def get_average_time(query_params, columns, project=None, automated=None):
80+
81+ """
82+ Return the average time between the two named columns.
83+
84+ The columns must be listed as the start_time and finish_time for the
85+ metric.
86+
87+ If no Project is supplied, average the times across all projects.
88+
89+ query_params: A dictionary of Djanqo query filters.
90+ columns: Columns to be queried, must be start_time, end_time.
91+
92+ project: A Project to filter BuildResults with, if none supplied, will use
93+ all Projects
94+ automated: If not None, filter BuildRequests using the requestor, if True,
95+ only BuildRequests with no requestor will be used, otherwise
96+ BuildRequests with the requestor field populated will be used.
97+ """
98+ # FIXME: This should really be done using PostgreSQL queries, but the tests
99+ # don't run using PostgreSQL...
100+ def delta_to_minutes(delta):
101+ """
102+ Return the total amount of time represented by a timedelta.
103+ """
104+ return ((delta.microseconds + (delta.seconds + delta.days
105+ * 24 * 3600)
106+ * 10 ** 6) / 10 ** 6)
107+ if project is not None:
108+ query_params["project"] = project
109+
110+ if automated is not None:
111+ query_params["requestor__isnull"] = automated
112+ build_results = BuildResult.objects.filter(**query_params)
113+ time_stamps = build_results.values_list(*columns)
114+ if not time_stamps:
115+ return 0.0
116+ times = [delta_to_minutes(end_time - start_time)
117+ for (start_time, end_time) in time_stamps]
118+ return float(sum(times)) / len(time_stamps)
119+
120+
121+def get_average_build_time(project=None, automated=None, period=None):
122+ """
123+ Return the average time taken between BuildResult.started_at and
124+ BuildResult.finished_at in seconds for all BuildResults associated with the
125+ provided Project.
126+
127+ period: A tuple of datetime objects (start_time, end_time)
128+
129+ See get_average_time for the project and automated parameters.
130+ """
131+ query_params = {"finished_at__isnull": False,
132+ "result__exact": ProjectBuildStates.SUCCESS}
133+ if period is not None:
134+ query_params["finished_at__range"] = period
135+ return get_average_time(query_params, ("started_at", "finished_at"),
136+ project=project, automated=automated)
137+
138+
139+def get_average_queue_time(project=None, automated=None, period=None):
140+ """
141+ Return the average time taken between BuildResult.requested_at and
142+ BuildResult.dispatched_at in seconds for all BuildResults associated with the
143+ provided Project.
144+
145+ period: A tuple of datetime objects (start_time, end_time)
146+
147+ See get_average_time for the project and automated parameters.
148+ """
149+ query_params = {}
150+ if period is not None:
151+ query_params["dispatched_at__range"] = period
152+ return get_average_time(query_params, ("requested_at", "dispatched_at"),
153+ project=project, automated=automated)
154+
155+
156+def get_build_result_metrics():
157+ """
158+ Return some simple metrics, in a format suitable for iterating over and
159+ displaying in a command-line tool.
160+ """
161+ results = []
162+ start_date = datetime.utcnow()
163+
164+ seven_days = (start_date - timedelta(days=7), start_date)
165+ thirty_days = (start_date - timedelta(days=30), start_date)
166+ ninety_days = (start_date - timedelta(days=90), start_date)
167+
168+ build_types = [("scheduled", True), ("requested", False)]
169+ build_periods = [("7 days", seven_days), ("30 days", thirty_days),
170+ ("90 days", ninety_days)]
171+
172+ for build_type, automated in build_types:
173+ for function, metric_type in [(get_average_build_time, "build"),
174+ (get_average_queue_time, "queue")]:
175+ for text, period in build_periods:
176+ metric = "Average %s %s time (%s)" % (build_type, metric_type,
177+ text)
178+ results.append((metric,
179+ function(period=period, automated=automated)))
180+
181+ return results
182
183=== modified file 'lib/offspring/web/queuemanager/models.py'
184--- lib/offspring/web/queuemanager/models.py 2011-12-08 16:27:50 +0000
185+++ lib/offspring/web/queuemanager/models.py 2011-12-14 09:29:35 +0000
186@@ -1,15 +1,7 @@
187 # Copyright 2010 Canonical Ltd. This software is licensed under the
188 # GNU Affero General Public License version 3 (see the file LICENSE).
189
190-__all__ = ["Project", "ProjectGroup", "Lexbuilder", "BuildRequest", \
191- "BuildResult", "DailyBuildOrder", "LaunchpadProject", "LaunchpadProjectMilestone", \
192- "ProjectNotificationSubscription", 'Release']
193-
194-from datetime import (
195- date,
196- datetime,
197- timedelta
198-)
199+from datetime import date, datetime, timedelta
200 import math
201
202 from django.conf import settings
203@@ -25,6 +17,10 @@
204 AccessManager)
205 from offspring.enums import ProjectBuildStates
206
207+__all__ = ["Project", "ProjectGroup", "Lexbuilder", "BuildRequest",
208+ "BuildResult", "DailyBuildOrder", "LaunchpadProject",
209+ "LaunchpadProjectMilestone", "ProjectNotificationSubscription",
210+ "Release"]
211
212 ARCH_CHOICES = (
213 (u'i386', u'i386'),
214@@ -82,7 +78,7 @@
215 project_group=self)
216
217 @property
218- def builder(self):
219+ def builder(self):
220 return None
221
222 @property
223@@ -386,7 +382,7 @@
224 requestor = models.ForeignKey(User, db_column="requestor_id", blank=True, null=True, editable=False)
225 reason = models.CharField('request reason', blank=True, null=True, max_length=200, editable=False)
226 requested_at = models.DateTimeField('date created', auto_now_add=True, blank=True, null=True, editable=False)
227- dispatched_at = models.DateTimeField('date dispatched', editable=False, null=True, blank=True)
228+ dispatched_at = models.DateTimeField('date dispatched', editable=False, null=True, blank=True)
229 notes = models.CharField(max_length=200, null=True, blank=True)
230
231 access_relation = 'project'
232
233=== modified file 'lib/offspring/web/queuemanager/tests/factory.py'
234--- lib/offspring/web/queuemanager/tests/factory.py 2011-12-02 13:55:56 +0000
235+++ lib/offspring/web/queuemanager/tests/factory.py 2011-12-14 09:29:35 +0000
236@@ -59,7 +59,6 @@
237 project_group=project_group)
238 for group in access_groups:
239 project.access_groups.add(group)
240- project.save()
241 return project
242
243 def make_project_group(self, name=None, title=None):
244@@ -73,7 +72,8 @@
245 title = self.get_unique_string()
246 return ProjectGroup.objects.create(name=name, title=title)
247
248- def make_build_result(self, project=None, name=None, result=None):
249+ def make_build_result(self, project=None, name=None, result=None,
250+ requestor=None):
251 """
252 Create a BuildResult with the given project or a newly created one if
253 none is given.
254@@ -83,7 +83,7 @@
255 if project is None:
256 project = self.make_project()
257 return BuildResult.objects.create(
258- project=project, name=name, result=result)
259+ project=project, name=name, result=result, requestor=requestor)
260
261 def make_release(self, build=None, name=None, creator=None):
262 """
263
264=== added file 'lib/offspring/web/queuemanager/tests/test_metrics.py'
265--- lib/offspring/web/queuemanager/tests/test_metrics.py 1970-01-01 00:00:00 +0000
266+++ lib/offspring/web/queuemanager/tests/test_metrics.py 2011-12-14 09:29:35 +0000
267@@ -0,0 +1,316 @@
268+from datetime import datetime, timedelta, date, time
269+
270+from django.test import TestCase
271+
272+from offspring.enums import ProjectBuildStates
273+from offspring.web.queuemanager.models import User
274+from offspring.web.queuemanager.metrics import (
275+ get_average_build_time, get_average_queue_time,
276+ get_build_result_metrics)
277+
278+from offspring.web.queuemanager.tests.factory import factory
279+
280+
281+def create_build_results_with_durations(project, durations,
282+ requestor=None,
283+ build_date=date.today(),
284+ result=ProjectBuildStates.SUCCESS):
285+ """
286+ Create BuildResults with durations specified.
287+
288+ project: The Project to associate the BuildResults with.
289+ durations: A sequence of time periods in minutes.
290+ """
291+ finished_at = datetime.combine(build_date, time())
292+ for minutes in durations:
293+ build_result = factory.make_build_result(
294+ project=project, requestor=requestor, result=result)
295+ build_result.finished_at = finished_at
296+ build_result.started_at = finished_at - timedelta(minutes=minutes)
297+ build_result.save()
298+
299+
300+def create_build_results_with_queue_times(project, queue_times, requestor=None,
301+ build_date=date.today()):
302+ """
303+ Create BuildResults with queue times specified.
304+
305+ project: The Project to associate the BuildResults with.
306+ queue_times: A sequence of time periods in minutes.
307+ """
308+ dispatched_at = datetime.combine(build_date, time())
309+ for minutes in queue_times:
310+ build_result = factory.make_build_result(
311+ project=project, requestor=requestor)
312+ build_result.dispatched_at = dispatched_at
313+ build_result.requested_at = dispatched_at - timedelta(minutes=minutes)
314+ build_result.save()
315+
316+
317+class BuildResultMetricsTests(TestCase):
318+
319+ def setUp(self):
320+ self.project = factory.make_project()
321+
322+ def test_get_average_build_time_single_build(self):
323+ """
324+ get_average_build_time should return the average time taken to build a
325+ project.
326+ """
327+ build_result = factory.make_build_result(
328+ project=self.project, result=ProjectBuildStates.SUCCESS)
329+ finished_at = datetime.now()
330+ build_result.finished_at = finished_at
331+ build_result.started_at = finished_at - timedelta(minutes=10)
332+ build_result.save()
333+ self.assertEqual(10*60, get_average_build_time(self.project))
334+
335+ def test_get_average_build_time_only_includes_completed_builds(self):
336+ """
337+ get_average_build_time should only include builds that are in a
338+ completed state.
339+ """
340+ create_build_results_with_durations(
341+ self.project, [10, 20], result=ProjectBuildStates.UNKNOWN)
342+ create_build_results_with_durations(
343+ self.project, [10, 20], result=ProjectBuildStates.FAILED)
344+ create_build_results_with_durations(
345+ self.project, [10, 20], result=ProjectBuildStates.PENDING)
346+
347+ self.assertEqual(0.0, get_average_build_time(self.project))
348+
349+ def test_get_average_build_time_no_builds(self):
350+ """
351+ If there are no builds, we should get an average of 0.0.
352+ """
353+ self.assertEqual(0.0, get_average_build_time(self.project))
354+
355+ def test_get_average_build_time_unfinished_build(self):
356+ """
357+ If a build has started, but not yet finished, this we should get 0.0
358+ for the average for this entry.
359+ """
360+ build_result = factory.make_build_result(project=self.project)
361+ build_result.started_at = datetime.now() - timedelta(minutes=20)
362+ build_result.save()
363+ self.assertEqual(0.0, get_average_build_time(self.project))
364+
365+ def test_get_average_build_time_multiple_builds(self):
366+ """
367+ The time for each BuildResult should be averaged together.
368+ """
369+ create_build_results_with_durations(self.project, [10, 20])
370+ self.assertEqual(15*60, get_average_build_time(self.project))
371+
372+ def test_get_average_build_time_multiple_builds_ignores_unfinished(self):
373+ """
374+ Any unfinished BuildResults should be discounted from the calculation.
375+ """
376+ create_build_results_with_durations(self.project, [10, 20])
377+
378+ # Create an additional unfinished build.
379+ build_result = factory.make_build_result(project=self.project)
380+ build_result.started_at = datetime.now() - timedelta(minutes=20)
381+ build_result.save()
382+
383+ self.assertEqual(15*60, get_average_build_time(self.project))
384+
385+ def test_get_average_build_time_ignores_other_projects(self):
386+ """
387+ BuildResults associated with other projects should not be included in
388+ the calculation.
389+ """
390+ create_build_results_with_durations(self.project, [10, 20])
391+ create_build_results_with_durations(factory.make_project(), [50, 100])
392+ self.assertEqual(15*60, get_average_build_time(self.project))
393+
394+ def test_get_average_build_time_no_project(self):
395+ """
396+ If get_average_build_time is not supplied with a Project, then it
397+ should calculate the average across all projects.
398+ """
399+ create_build_results_with_durations(self.project, [10, 20])
400+ create_build_results_with_durations(
401+ factory.make_project(), [50, 100])
402+ # 10 + 20 + 50 + 100 = 180 / 4 = 45 minutes...
403+ self.assertEqual(45*60, get_average_build_time())
404+
405+ def test_get_average_build_time_with_automated_flag(self):
406+ """
407+ Calling get_average_build_time with automated=True should filter the
408+ results to only include automated builds, which are defined as builds
409+ with a requestor.
410+ """
411+ user = User.objects.create_user(u"Testing", u"testing@example.com",
412+ u"testing")
413+ create_build_results_with_durations(self.project, [10, 20])
414+ create_build_results_with_durations(
415+ factory.make_project(), [50, 100], requestor=user)
416+
417+ def test_get_average_build_time_filters_by_date(self):
418+ """
419+ It should be possible to restrict the date queried to a specific time
420+ frame.
421+
422+ Any BuildResults finishing outwith the time period specified shouldn't
423+ be included.
424+ """
425+ build_date = datetime.utcnow() - timedelta(days=30)
426+ create_build_results_with_durations(
427+ self.project, [10, 20], build_date=build_date)
428+
429+ # From the day after we create our builds 'til today.
430+ time_period = (build_date + timedelta(days=1), datetime.utcnow())
431+ self.assertEqual(0.0,
432+ get_average_build_time(period=time_period))
433+
434+ def test_get_average_build_time_includes_by_date(self):
435+ """
436+ It should be possible to restrict the date queried to a specific time
437+ frame, specifying a period should only include those BuildResults
438+ within that period.
439+ """
440+ build_date = datetime.utcnow() - timedelta(days=30)
441+ create_build_results_with_durations(
442+ self.project, [10, 20], build_date=build_date)
443+
444+ create_build_results_with_durations(
445+ factory.make_project(), [50, 100],
446+ build_date=build_date + timedelta(days=2))
447+
448+ # From the day after we create our builds 'til today.
449+ time_period = (build_date + timedelta(days=1), datetime.utcnow())
450+ self.assertEqual(75*60, get_average_build_time(period=time_period))
451+
452+ def test_get_average_build_time_filters_by_date_and_automated(self):
453+ """
454+ It should be possible to restrict the date queried to a specific time
455+ frame, specifying a period and the automated flag should filter the
456+ applicable BuildResults.
457+ """
458+ user = User.objects.create_user(u"Testing", u"testing@example.com",
459+ u"testing")
460+ build_date = datetime.utcnow() - timedelta(days=30)
461+ create_build_results_with_durations(
462+ self.project, [10, 20], build_date=build_date,
463+ requestor=user)
464+ create_build_results_with_durations(
465+ factory.make_project(), [40, 40],
466+ build_date=build_date + timedelta(days=2))
467+
468+ # From the day after we create our builds 'til today.
469+ time_period = (build_date + timedelta(days=1), datetime.utcnow())
470+ self.assertEqual(40*60,
471+ get_average_build_time(period=time_period,
472+ automated=True))
473+
474+
475+class QueueTimeMetricsTests(TestCase):
476+
477+ def setUp(self):
478+ self.project = factory.make_project()
479+
480+ def test_average_queue_time_single_build(self):
481+ """
482+ get_average_queue_time should return the average time a build waits
483+ before being dispatched.
484+ """
485+ build_result = factory.make_build_result(project=self.project)
486+ dispatched_at = datetime.now()
487+ build_result.dispatched_at = dispatched_at
488+ build_result.requested_at = dispatched_at - timedelta(minutes=30)
489+ build_result.save()
490+ self.assertEqual(30*60, get_average_queue_time(self.project))
491+
492+ def test_average_queue_time_no_builds(self):
493+ """
494+ If there are no builds, we should get an average of 0.0.
495+ """
496+ self.assertEqual(0.0*60, get_average_queue_time(self.project))
497+
498+ def test_average_queue_time_includes_by_date(self):
499+ """
500+ It should be possible to restrict the date queried to a specific time
501+ frame, specifying a period should only include those BuildResults
502+ within that period.
503+ """
504+ build_date = datetime.utcnow() - timedelta(days=30)
505+ create_build_results_with_queue_times(
506+ self.project, [10, 20], build_date=build_date)
507+
508+ create_build_results_with_queue_times(
509+ factory.make_project(), [50, 100],
510+ build_date=build_date + timedelta(days=2))
511+
512+ # From the day after we create our builds 'til today.
513+ time_period = (build_date + timedelta(days=1), datetime.utcnow())
514+ self.assertEqual(75*60,
515+ get_average_queue_time(period=time_period))
516+
517+
518+class GetBuildResultsTests(TestCase):
519+
520+ def setUp(self):
521+ self.project = factory.make_project()
522+
523+ def test_get_build_result_metrics(self):
524+ """
525+ get_build_result_metrics should return a list of tuples with the metric
526+ being measured, and the time in minutes.
527+ """
528+ def create_test_data(user=None, offset=0):
529+ start_date = datetime.utcnow()
530+ last_week = start_date - timedelta(days=8)
531+ last_month = start_date - timedelta(days=35)
532+ # Two this week, average = 10 + 20/2 = 15
533+ create_build_results_with_durations(
534+ self.project, [10 + offset, 20], build_date=start_date,
535+ requestor=user)
536+
537+ # Two this week, average = 30 + 18/2 = 24.0
538+ create_build_results_with_queue_times(
539+ self.project, [30 + offset, 18], build_date=start_date,
540+ requestor=user)
541+
542+ # Two last week, 10 + 10 + 10 + 20/4 = 12.5
543+ create_build_results_with_durations(
544+ self.project, [10 + offset, 10], build_date=last_week,
545+ requestor=user)
546+
547+ # Two last week, average = 30 + 18 + 14 11/4 = 73
548+ create_build_results_with_queue_times(
549+ self.project, [14+offset, 11], build_date=last_week,
550+ requestor=user)
551+
552+ # Two last month, 10 + 10 + 10 + 20 + 30 + 40/6 = 20
553+ create_build_results_with_durations(
554+ self.project, [30+offset, 40], build_date=last_month,
555+ requestor=user)
556+
557+ # Two last month, average = 30 + 18 + 14 11 + 30 + 20/6 = 82
558+ create_build_results_with_queue_times(
559+ self.project, [30+offset, 20], build_date=last_month,
560+ requestor=user)
561+
562+ # Scheduled builds
563+ # Requested builds
564+
565+ create_test_data()
566+ user = User.objects.create_user(u"Testing", u"testing@example.com",
567+ u"testing")
568+ create_test_data(user=user, offset=10)
569+
570+ self.assertEqual(
571+ [("Average scheduled build time (7 days)", 15.0*60),
572+ ("Average scheduled build time (30 days)", 12.5*60),
573+ ("Average scheduled build time (90 days)", 20.0*60),
574+ ("Average scheduled queue time (7 days)", 24.0*60),
575+ ("Average scheduled queue time (30 days)", 18.25*60),
576+ ("Average scheduled queue time (90 days)", 20.5*60),
577+ ("Average requested build time (7 days)", 20.0*60),
578+ ("Average requested build time (30 days)", 17.5*60),
579+ ("Average requested build time (90 days)", 25.0*60),
580+ ("Average requested queue time (7 days)", 29.0*60),
581+ ("Average requested queue time (30 days)", 23.25*60),
582+ ("Average requested queue time (90 days)", 25.5*60)],
583+ get_build_result_metrics())

Subscribers

People subscribed via source and target branches