Merge lp:~bigkevmcd/offspring/fix-properties-on-builder-and-project-group into lp:offspring

Proposed by Kevin McDermott
Status: Superseded
Proposed branch: lp:~bigkevmcd/offspring/fix-properties-on-builder-and-project-group
Merge into: lp:offspring
Prerequisite: lp:~bigkevmcd/offspring/migrate-metrics-to-manager
Diff against target: 692 lines (+285/-95)
12 files modified
Makefile (+1/-1)
lib/offspring/web/queuemanager/managers.py (+3/-2)
lib/offspring/web/queuemanager/models.py (+11/-54)
lib/offspring/web/queuemanager/templatetags/format_seconds.py (+40/-0)
lib/offspring/web/queuemanager/templatetags/tests/test_format_seconds.py (+66/-0)
lib/offspring/web/queuemanager/tests/test_managers.py (+37/-25)
lib/offspring/web/queuemanager/tests/test_models.py (+116/-9)
lib/offspring/web/templates/queuemanager/builder_details.html (+2/-1)
lib/offspring/web/templates/queuemanager/builders.html (+4/-1)
lib/offspring/web/templates/queuemanager/project_details.html (+2/-1)
lib/offspring/web/templates/queuemanager/projectgroup_details.html (+2/-1)
setup_test_database.sh (+1/-0)
To merge this branch: bzr merge lp:~bigkevmcd/offspring/fix-properties-on-builder-and-project-group
Reviewer Review Type Date Requested Status
Cody A.W. Somerville Needs Fixing
Offspring Committers Pending
Review via email: mp+85848@code.launchpad.net

This proposal supersedes a proposal from 2011-12-15.

This proposal has been superseded by a proposal from 2012-01-18.

Description of the change

This removes the previously untested code for calculating the metrics for ProjectGroup and Project, and delegates them to the metrics Manager provided on the BuildResult, also adds a formatting filter to output the time, and updates the uses of average_build_time to filter through the time filter.

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

Failing test for Lexbuilder.completion_time.

163. By Kevin McDermott

Tests for Lexbuilder.completion_time.

164. By Kevin McDermott

Failing tests for metrics returning None if there are no matching BuildResults to count.

165. By Kevin McDermott

Update the Manager to return a None if there are no results.

166. By Kevin McDermott

Failing test for template filter outputting None.

167. By Kevin McDermott

Fix the template filter.

168. By Kevin McDermott

Failing test for passing through strings in seconds conversion filter.

169. By Kevin McDermott

And implement.

170. By Kevin McDermott

All tests pass.

171. By Kevin McDermott

Update HTML output.

172. By Kevin McDermott

Some lint tidyups.

173. By Kevin McDermott

Failing test for returning ASAP when completion is over the expected time.

174. By Kevin McDermott

Return ASAP if the current elapsed build time is greater than the average.

175. By Kevin McDermott

Merge forward from trunk.

176. By Kevin McDermott

Ensure we're merged up to date.

177. By Kevin McDermott

Merge forward.

178. By Kevin McDermott

Merge forward, fix conflicts.

Revision history for this message
Cody A.W. Somerville (cody-somerville) wrote :
Download full text (7.0 KiB)

>>> === modified file 'Makefile'
>>> --- Makefile 2012-01-11 18:22:50 +0000
>>> +++ Makefile 2012-01-16 18:45:54 +0000
>>> @@ -79,7 +79,7 @@
>>> ./bin/offspring-web test offspring.web.queuemanager -s --settings=offspring.web.settings_test
>>>
>>> test-web-postgres: web web-test install-test-runner
>>> - ./bin/offspring-web test offspring.web.queuemanager -s --settings=offspring.web.settings_devel
>>> + ./bin/offspring-web test offspring.web.queuemanager --settings=offspring.web.settings_devel
>>>
>>> test: master slave web install-test-runner test-web master-test
>>> ./.virtualenv/bin/nosetests -e 'queuemanager.*'
>>>

Why is this change required?

<snip>
>>> === modified file 'lib/offspring/web/queuemanager/models.py'
>>> --- lib/offspring/web/queuemanager/models.py 2012-01-16 17:28:38 +0000
>>> +++ lib/offspring/web/queuemanager/models.py 2012-01-16 18:45:54 +0000
<snip>
>>> @@ -357,24 +322,16 @@
>>> @property
>>> def estimated_completion(self):
>>> if self.current_job is not None:
>>> - #XXX: Duplication of code in Project to get average build time
>>> - cursor = connection.cursor()
>>> - cursor.execute("SELECT SUM(finished_at - started_at) / count(*) from buildresults WHERE project_name = %s AND result = %s", [self.current_job.project.name, "COMPLETED"])
>>> - row = cursor.fetchone()
>>> - if row[0] is not None:
>>> - averageBuildTime = row[0]
>>> - currentElapsedTime = datetime.now() - self.current_job.started_at
>>> - estimatedTimeRemaining = averageBuildTime - currentElapsedTime
>>> - #XXX: Duplication of code in Project to format output
>>> - result = str(estimatedTimeRemaining)
>>> - if result.startswith("-"):
>>> + average_build_time = BuildResult.metrics.get_average_build_time(
>>> + project=self.current_job.project)

You should get the average_build_time via the average_build_time property on self.current_job.project.

>>> + if average_build_time is not None:
>>> + elapsed_time = (datetime.now() - self.current_job.started_at)
>>> + estimated_completion =(average_build_time -
>>> + elapsed_time.seconds)
>>> + if estimated_completion < 0.0:
>>> return "ASAP"
>>> else:
>>> - hours, minutes, seconds = result.split(":")
>>> - if int(hours) < 1:
>>> - return "%s mins, %s secs" % (minutes, math.floor(float(seconds)))
>>> - else:
>>> - return "%s hrs, %s mins, %s secs" % (hours, minutes, math.floor(float(seconds)))
>>> + return estimated_completion
>>> else:
>>> return "Unknown"
>>> else:
>>>

API wise, I think we should return a datetime object of when we expect the build to complete - 'self.current_job.started_at + datetime.timedelta(seconds=self.current_job.project.average_build_time)'. Then we can just use the Django template filter 'timeuntil' inst...

Read more...

review: Needs Fixing
Revision history for this message
Kevin McDermott (bigkevmcd) wrote :

>
> Why is this change required?

I think this was a mismerge, I've been merging forward as branches have landed.

> You should get the average_build_time via the average_build_time property on
> self.current_job.project.

Good idea, fixed.

>
> API wise, I think we should return a datetime object of when we expect the
> build to complete - 'self.current_job.started_at +
> datetime.timedelta(seconds=self.current_job.project.average_build_time)'. Then
> we can just use the Django template filter 'timeuntil' instead of writing and
> maintaining our own filter.

This would substantially change the current behaviour, also, timeuntil would trigger an error for the Unknown and ASAP cases...which isn't very good...

> This docstring needs to be reworded.
Done

>
> I think a better name might be
> 'test_get_average_build_time_of_automated_builds_only'.

Changed...

> Instead of having magic numbers, you should calculate the average of the list
> you pass to create_build_results_with_durations and make 60.0 a constant.
Good idea, create_build_results/create_build_durations both now return the average, am not sure that SECONDS_IN_MINUTE is any clearer than 60?

>
> On a small tangent, I question the value of some of these tests. For example,
> you have a similar battery of tests for average build time as you do for
> average queue time. You had to make the same changes in both sets of tests
> because of a change you made to a function get_average_for_timeperiods that
> both code pathways use. Would it not be better to have tests for the behaviour
> of get_average_for_timeperiods /
> BuildResultMetricsManager._get_average_time_for_query and tests for the
> specific behaviours of BuildResultMetricsManager.get_average_build_time and
> BuildResultMetricsManager.get_average_queue_time? - that way you don't end up
> basically doing the same tests twice.>

So, there isn't actually as much repetition of the tests as it appears, also, the tests for BuldResultsMetricsManager._get_average_time_for_query would need to operate on some model, they might as well operate on a working one...

Also, given that both calculations use different query parameters, they do need to be tested.

> This appears to be a syntax error.

Oops...fixed.

thanks for the review

> Thank you for your work on this!!

thanks for the attention to detail.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2012-01-11 18:22:50 +0000
3+++ Makefile 2012-01-16 18:45:54 +0000
4@@ -79,7 +79,7 @@
5 ./bin/offspring-web test offspring.web.queuemanager -s --settings=offspring.web.settings_test
6
7 test-web-postgres: web web-test install-test-runner
8- ./bin/offspring-web test offspring.web.queuemanager -s --settings=offspring.web.settings_devel
9+ ./bin/offspring-web test offspring.web.queuemanager --settings=offspring.web.settings_devel
10
11 test: master slave web install-test-runner test-web master-test
12 ./.virtualenv/bin/nosetests -e 'queuemanager.*'
13
14=== modified file 'lib/offspring/web/queuemanager/managers.py'
15--- lib/offspring/web/queuemanager/managers.py 2011-12-15 08:50:53 +0000
16+++ lib/offspring/web/queuemanager/managers.py 2012-01-16 18:45:54 +0000
17@@ -16,7 +16,8 @@
18
19 def get_average_for_timeperiods(queryset, columns):
20 """
21- Return the average time between two columns in a QuerySet.A
22+ Return the average time between two columns in a QuerySet, or None if there
23+ are no matching results.
24
25 queryset: A QuerySet of BuildResults to be averaged.
26 columns: A sequence of two column names to be extracted, this must be
27@@ -24,7 +25,7 @@
28 """
29 time_stamps = queryset.values_list(*columns)
30 if not time_stamps:
31- return 0.0
32+ return None
33
34 times = [delta_to_seconds(end_time - start_time)
35 for (start_time, end_time) in time_stamps]
36
37=== modified file 'lib/offspring/web/queuemanager/models.py'
38--- lib/offspring/web/queuemanager/models.py 2012-01-16 17:28:38 +0000
39+++ lib/offspring/web/queuemanager/models.py 2012-01-16 18:45:54 +0000
40@@ -1,7 +1,6 @@
41 # Copyright 2010 Canonical Ltd. This software is licensed under the
42 # GNU Affero General Public License version 3 (see the file LICENSE).
43
44-import math
45 from datetime import date, datetime, timedelta
46
47 from django.contrib.auth.models import AnonymousUser, User
48@@ -93,24 +92,7 @@
49
50 @property
51 def average_build_time(self):
52- cursor = connection.cursor()
53- cursor.execute("SELECT SUM(finished_at - started_at) / count(*) from buildresults br, projects WHERE br.project_name = projects.name AND projects.project_group_id = %s AND br.result = %s", [self.name, "COMPLETED"])
54- row = cursor.fetchone()
55- if row[0] is not None:
56- result = str(row[0])
57- hours, minutes, seconds = result.split(":")
58- #XXX: The following can't handle builds that have an average
59- #XXX: duration of greater then 23 hours and 59 minutes and 59
60- #XXX: seconds (e.g. if a build gets stuck and lasts over a day).
61- try:
62- if int(hours) < 1:
63- return "%s mins, %s secs" % (minutes, math.floor(float(seconds)))
64- else:
65- return "%s hrs, %s mins, %s secs" % (hours, minutes, math.floor(float(seconds)))
66- except ValueError:
67- #XXX: We'll potentially incorrectly report >1 day until we fix above.
68- return ">1 Day"
69- return "UNKNOWN"
70+ return BuildResult.metrics.get_average_build_time(project_group=self)
71
72
73 class PublicOnlyObjectsManager(models.Manager):
74@@ -265,7 +247,7 @@
75 def display_name(self):
76 return "%s (%s)" % (self.title, self.name)
77
78- def builder(self):
79+ def builder(self):
80 return Lexbuilder.objects.filter(current_job__project = self)[0:1].get()
81
82 @property
83@@ -274,24 +256,7 @@
84
85 @property
86 def average_build_time(self):
87- cursor = connection.cursor()
88- cursor.execute("SELECT SUM(finished_at - started_at) / count(*) from buildresults WHERE project_name = %s AND result = %s", [self.name, "COMPLETED"])
89- row = cursor.fetchone()
90- if row[0] is not None:
91- result = str(row[0])
92- hours, minutes, seconds = result.split(":")
93- #XXX: The following can't handle builds that have an average
94- #XXX: duration of greater then 23 hours and 59 minutes and 59
95- #XXX: seconds (e.g. if a build gets stuck and lasts over a day).
96- try:
97- if int(hours) < 1:
98- return "%s mins, %s secs" % (minutes, math.floor(float(seconds)))
99- else:
100- return "%s hrs, %s mins, %s secs" % (hours, minutes, math.floor(float(seconds)))
101- except ValueError:
102- #XXX: We'll potentially incorrectly report >1 day until we fix above.
103- return ">1 Day"
104- return "UNKNOWN"
105+ return BuildResult.metrics.get_average_build_time(project=self)
106
107 def latest_build(self):
108 try:
109@@ -357,24 +322,16 @@
110 @property
111 def estimated_completion(self):
112 if self.current_job is not None:
113- #XXX: Duplication of code in Project to get average build time
114- cursor = connection.cursor()
115- cursor.execute("SELECT SUM(finished_at - started_at) / count(*) from buildresults WHERE project_name = %s AND result = %s", [self.current_job.project.name, "COMPLETED"])
116- row = cursor.fetchone()
117- if row[0] is not None:
118- averageBuildTime = row[0]
119- currentElapsedTime = datetime.now() - self.current_job.started_at
120- estimatedTimeRemaining = averageBuildTime - currentElapsedTime
121- #XXX: Duplication of code in Project to format output
122- result = str(estimatedTimeRemaining)
123- if result.startswith("-"):
124+ average_build_time = BuildResult.metrics.get_average_build_time(
125+ project=self.current_job.project)
126+ if average_build_time is not None:
127+ elapsed_time = (datetime.now() - self.current_job.started_at)
128+ estimated_completion =(average_build_time -
129+ elapsed_time.seconds)
130+ if estimated_completion < 0.0:
131 return "ASAP"
132 else:
133- hours, minutes, seconds = result.split(":")
134- if int(hours) < 1:
135- return "%s mins, %s secs" % (minutes, math.floor(float(seconds)))
136- else:
137- return "%s hrs, %s mins, %s secs" % (hours, minutes, math.floor(float(seconds)))
138+ return estimated_completion
139 else:
140 return "Unknown"
141 else:
142
143=== added file 'lib/offspring/web/queuemanager/templatetags/format_seconds.py'
144--- lib/offspring/web/queuemanager/templatetags/format_seconds.py 1970-01-01 00:00:00 +0000
145+++ lib/offspring/web/queuemanager/templatetags/format_seconds.py 2012-01-16 18:45:54 +0000
146@@ -0,0 +1,40 @@
147+# Copyright 2010 Canonical Ltd. This software is licensed under the
148+# GNU Affero General Public License version 3 (see the file LICENSE).
149+"""Time in seconds formatting."""
150+
151+from django.template import Library
152+
153+register = Library()
154+
155+ONE_MINUTE = 60
156+ONE_HOUR = 60 * ONE_MINUTE
157+ONE_DAY = 24 * ONE_HOUR
158+
159+
160+@register.filter
161+def seconds_to_time(value):
162+ """
163+ Converts seconds to a time representation.
164+
165+ If value is None, return "Unknown"
166+ """
167+ if value is None:
168+ return "Unknown"
169+
170+ try:
171+ value = float(value)
172+ days, remainder = divmod(value, ONE_DAY)
173+ hours, remainder = divmod(remainder, ONE_HOUR)
174+ minutes, seconds = divmod(remainder, ONE_MINUTE)
175+
176+ times = [(days, "days"), (hours, "hours"), (minutes, "minutes"),
177+ (seconds, "seconds")]
178+
179+ periods = ["%d %s" % (time, period) for time, period in times
180+ if time > 0]
181+
182+ return ", ".join(periods)
183+ except ValueError:
184+ return value
185+
186+seconds_to_time.is_safe = True
187
188=== added directory 'lib/offspring/web/queuemanager/templatetags/tests'
189=== added file 'lib/offspring/web/queuemanager/templatetags/tests/__init__.py'
190=== added file 'lib/offspring/web/queuemanager/templatetags/tests/test_format_seconds.py'
191--- lib/offspring/web/queuemanager/templatetags/tests/test_format_seconds.py 1970-01-01 00:00:00 +0000
192+++ lib/offspring/web/queuemanager/templatetags/tests/test_format_seconds.py 2012-01-16 18:45:54 +0000
193@@ -0,0 +1,66 @@
194+# Copyright 2010 Canonical Ltd. This software is licensed under the
195+# GNU Affero General Public License version 3 (see the file LICENSE).
196+
197+from django.template import Template, Context
198+from django.test import TestCase
199+
200+from offspring.web.queuemanager.templatetags.format_seconds import (
201+ seconds_to_time)
202+
203+
204+class TestSecondsToTimeFilter(TestCase):
205+
206+ def test_seconds_to_time_seconds(self):
207+ """
208+ seconds_to_time should render the number of seconds accurately.
209+ """
210+ self.assertEqual("35 seconds", seconds_to_time(35))
211+
212+ def test_seconds_to_time_with_none(self):
213+ """
214+ seconds_to_time should return Unknown if the time period is None.
215+ """
216+ self.assertEqual("Unknown", seconds_to_time(None))
217+
218+ def test_seconds_to_time_with_string(self):
219+ """
220+ seconds_to_time should pass strings that can't be converted to floating
221+ point numbers unchanged.
222+ """
223+ self.assertEqual("Unknown", seconds_to_time("Unknown"))
224+
225+ def test_seconds_to_time_minutes(self):
226+ """
227+ seconds_to_time should render the number of minutes accurately.
228+ """
229+ self.assertEqual("10 minutes", seconds_to_time(600))
230+
231+ def test_seconds_to_time_hours(self):
232+ """
233+ seconds_to_time should render the number of hours accurately.
234+ """
235+ self.assertEqual("2 hours", seconds_to_time(7200))
236+
237+ def test_seconds_to_time_days(self):
238+ """
239+ seconds_to_time should render the number of hours accurately.
240+ """
241+ self.assertEqual("3 days", seconds_to_time(72 * 60 * 60))
242+
243+ def test_seconds_to_time_period(self):
244+ """
245+ seconds_to_time should render the time period accurately.
246+ """
247+ # 2 days, 2 hours, 10 minutes, 35 seconds
248+ seconds = (2 * 24 * 60 * 60) + 7200 + 600 + 35
249+ self.assertEqual("2 days, 2 hours, 10 minutes, 35 seconds",
250+ seconds_to_time(seconds))
251+
252+ def test_seconds_to_time_filter_in_template(self):
253+ """
254+ The seconds_to_time filter converts an integer value for a number of
255+ seconds to a human readable duration.
256+ """
257+ rendered = Template("{% load format_seconds %}"
258+ "{{ 600|seconds_to_time }}").render(Context())
259+ self.assertTrue("10 minutes" in rendered, "Time incorrectly rendered.")
260
261=== modified file 'lib/offspring/web/queuemanager/tests/test_managers.py'
262--- lib/offspring/web/queuemanager/tests/test_managers.py 2012-01-12 08:29:41 +0000
263+++ lib/offspring/web/queuemanager/tests/test_managers.py 2012-01-16 18:45:54 +0000
264@@ -43,27 +43,27 @@
265 create_build_results_with_durations(
266 self.project, [10, 20], result=ProjectBuildStates.PENDING)
267
268- self.assertEqual(0.0,
269+ self.assertEqual(None,
270 BuildResult.metrics.get_average_build_time(
271 self.project))
272
273 def test_get_average_build_time_no_builds(self):
274 """
275- If there are no builds, we should get an average of 0.0.
276+ If there are no builds, we should get an average of None.
277 """
278- self.assertEqual(0.0,
279+ self.assertEqual(None,
280 BuildResult.metrics.get_average_build_time(
281 self.project))
282
283 def test_get_average_build_time_unfinished_build(self):
284 """
285- If a build has started, but not yet finished, this we should get 0.0
286+ If a build has started, but not yet finished, this we should get None,
287 for the average for this entry.
288 """
289 started_at = datetime.utcnow()
290 build_result = factory.make_build_result(project=self.project,
291 started_at=started_at)
292- self.assertEqual(0.0,
293+ self.assertEqual(None,
294 BuildResult.metrics.get_average_build_time(
295 self.project))
296
297@@ -114,22 +114,29 @@
298 self.assertEqual(45.0 * 60,
299 BuildResult.metrics.get_average_build_time())
300
301- def test_get_average_build_time_with_automated_flag(self):
302+ def test_get_average_build_time_with_automated_builds(self):
303 """
304 Calling get_average_build_time with automated=True should filter the
305 results to only include automated builds, which are defined as builds
306+ without a requestor.
307+ """
308+ create_build_results_with_durations(self.project, [10, 20])
309+ self.assertEqual(
310+ 15 * 60.0,
311+ BuildResult.metrics.get_average_build_time(automated=True))
312+
313+ def test_get_average_build_time_with_non_automated_builds(self):
314+ """
315+ Calling get_average_build_time with automated=False should filter the
316+ results to only include manual builds, which are defined as builds
317 with a requestor.
318 """
319 user = factory.make_user()
320- create_build_results_with_durations(self.project, [10, 20])
321 create_build_results_with_durations(
322 factory.make_project(), [50, 100], requestor=user)
323- self.assertEqual(15 * 60.0,
324- BuildResult.metrics.get_average_build_time(
325- automated=True))
326- self.assertEqual(75 * 60.0,
327- BuildResult.metrics.get_average_build_time(
328- automated=False))
329+ self.assertEqual(
330+ 75 * 60.0,
331+ BuildResult.metrics.get_average_build_time(automated=False))
332
333 def test_get_average_build_time_filters_by_date(self):
334 """
335@@ -146,7 +153,7 @@
336 # From the day after we create our builds 'til today.
337 period = (build_date + timedelta(days=1), datetime.utcnow())
338 self.assertEqual(
339- 0.0,
340+ None,
341 BuildResult.metrics.get_average_build_time(period=period))
342
343 def test_get_average_build_time_includes_by_date(self):
344@@ -207,10 +214,10 @@
345
346 def test_average_queue_time_no_builds(self):
347 """
348- If there are no builds, we should get an average of 0.0.
349+ If there are no builds, we should get an average of None.
350 """
351 self.assertEqual(
352- 0.0,
353+ None,
354 BuildResult.metrics.get_average_queue_time(self.project))
355
356 def test_average_queue_time_includes_by_date(self):
357@@ -266,22 +273,27 @@
358 self.assertEqual(45.0 * 60,
359 BuildResult.metrics.get_average_queue_time())
360
361- def test_get_average_queue_time_with_automated_flag(self):
362+ def test_get_average_queue_time_with_automated_builds(self):
363 """
364 Calling get_average_queue_time with automated=True should filter the
365 results to only include automated builds, which are defined as builds
366+ without a requestor.
367+ """
368+ create_build_results_with_queue_times(self.project, [10, 20])
369+ self.assertEqual(
370+ 15 * 60.0,
371+ BuildResult.metrics.get_average_queue_time(automated=True))
372+
373+ def test_get_average_queue_time_with_non_automated_builds(self):
374+ """
375+ Calling get_average_queue_time with automated=False should filter the
376+ results to only include manual builds, which are defined as builds
377 with a requestor.
378 """
379- user = factory.make_user()
380 create_build_results_with_queue_times(self.project, [10, 20])
381- create_build_results_with_queue_times(
382- factory.make_project(), [50, 100], requestor=user)
383 self.assertEqual(
384 15 * 60.0,
385 BuildResult.metrics.get_average_queue_time(automated=True))
386- self.assertEqual(
387- 75 * 60.0,
388- BuildResult.metrics.get_average_queue_time(automated=False))
389
390 def test_get_average_queue_time_filters_by_date(self):
391 """
392@@ -298,7 +310,7 @@
393 # From the day after we create our builds 'til today.
394 period = (build_date + timedelta(days=1), datetime.utcnow())
395 self.assertEqual(
396- 0.0,
397+ None,
398 BuildResult.metrics.get_average_queue_time(period=period))
399
400 def test_get_average_queue_time_includes_by_date(self):
401@@ -318,7 +330,7 @@
402 # From the day after we create our builds 'til today.
403 period = (build_date + timedelta(days=1), datetime.utcnow())
404 self.assertEqual(
405- 75 * 60,
406+ 75.0 * 60,
407 BuildResult.metrics.get_average_queue_time(period=period))
408
409 def test_get_average_queue_time_filters_by_date_and_automated(self):
410
411=== modified file 'lib/offspring/web/queuemanager/tests/test_models.py'
412--- lib/offspring/web/queuemanager/tests/test_models.py 2012-01-16 17:28:38 +0000
413+++ lib/offspring/web/queuemanager/tests/test_models.py 2012-01-16 18:45:54 +0000
414@@ -1,4 +1,7 @@
415-from datetime import datetime
416+# Copyright 2010 Canonical Ltd. This software is licensed under the
417+# GNU Affero General Public License version 3 (see the file LICENSE).
418+from datetime import datetime, timedelta
419+
420 from operator import attrgetter
421
422 from django.contrib.auth.models import AnonymousUser
423@@ -7,7 +10,11 @@
424 from offspring.enums import ProjectBuildStates
425 from offspring.web.queuemanager.models import (
426 BuildRequest, BuildResult, Lexbuilder, Project, Release)
427+
428 from offspring.web.queuemanager.tests.factory import factory
429+from offspring.web.queuemanager.tests.helpers import (
430+ create_build_results_with_durations)
431+from mocker import MockerTestCase
432
433
434 class ProjectTests(TestCase):
435@@ -72,7 +79,7 @@
436 Check that the .objects model manager return only public projects.
437 """
438 project = factory.make_project(is_private=False)
439- project2 = factory.make_project(is_private=True)
440+ factory.make_project(is_private=True)
441 self.assertEqual([project], list(Project.objects.all()))
442
443 def test_all_objects_model_manager(self):
444@@ -127,6 +134,15 @@
445 objects = Project.all_objects.accessible_by_user(user)
446 self.assertEqual([project], list(objects))
447
448+ def test_average_build_time(self):
449+ """
450+ ProjectGroup.average_build_time should use the BuildResult metric.
451+ """
452+ project = factory.make_project()
453+ create_build_results_with_durations(
454+ project, [50, 50], result=ProjectBuildStates.SUCCESS)
455+ self.assertEqual(50 * 60.0, project.average_build_time)
456+
457
458 class ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin(object):
459 """A mixin with tests that work for the following models:
460@@ -213,8 +229,7 @@
461 """
462 Check that the .objects model manager return only public objects.
463 """
464- private_object = self.factoryMethod(
465- factory.make_project(is_private=True))
466+ self.factoryMethod(factory.make_project(is_private=True))
467 public_object = self.factoryMethod(
468 factory.make_project(is_private=False))
469 self.assertEqual([public_object], list(self.model.objects.all()))
470@@ -246,8 +261,7 @@
471 factory.make_project(is_private=True, access_groups=[group]))
472 # This object is linked to a private project which has no access
473 # groups set up, so the user cannot see it.
474- invisible_object = self.factoryMethod(
475- project=factory.make_project(is_private=True))
476+ self.factoryMethod(project=factory.make_project(is_private=True))
477 self.assertEqual(
478 [visible_object],
479 list(self.model.all_objects.accessible_by_user(user)))
480@@ -263,13 +277,97 @@
481
482
483 class LexbuilderTests(
484- TestCase, ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin):
485+ TestCase, ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin,
486+ MockerTestCase):
487 model = Lexbuilder
488
489 def factoryMethod(self, project):
490 return factory.make_lexbuilder(
491 current_job=factory.make_build_result(project=project))
492
493+ def test_estimated_completion(self):
494+ """
495+ Lexbuilder.estimated_completion_time should be calculated as:
496+ average build time - (current time - start time)
497+ """
498+ project = factory.make_project()
499+
500+ create_build_results_with_durations(
501+ project=project, durations=[25, 25],
502+ result=ProjectBuildStates.SUCCESS)
503+
504+ build_result = factory.make_build_result(
505+ project=project, result=ProjectBuildStates.PENDING)
506+ started_at = datetime.now() - timedelta(minutes=10)
507+ build_result.started_at = started_at
508+ build_result.save()
509+
510+ builder = Lexbuilder.objects.create(current_job=build_result)
511+
512+ datetime_mock = self.mocker.replace(datetime)
513+ datetime_mock.now()
514+ self.mocker.result(started_at + timedelta(minutes=5))
515+ self.mocker.replay()
516+
517+ # Average time = 25 minutes
518+ # Started 5 minutes ago (started_at + timedelta(minutes=5))
519+ # 20 minutes remaining
520+
521+ self.assertEqual(20 * 60, builder.estimated_completion)
522+
523+ def test_estimated_completion_with_no_average_time(self):
524+ """
525+ If the average cannot be determined because there are no builds, we
526+ should get "Unknown" as the estimated completion time.
527+ """
528+ project = factory.make_project()
529+
530+ build_result = factory.make_build_result(
531+ project=project, result=ProjectBuildStates.PENDING)
532+ started_at = datetime.now() - timedelta(minutes=10)
533+ build_result.started_at = started_at
534+ build_result.save()
535+
536+ builder = Lexbuilder.objects.create(current_job=build_result)
537+
538+ self.assertEqual("Unknown", builder.estimated_completion)
539+
540+ def test_estimated_completion_with_no_current_build(self):
541+ """
542+ If the average cannot be determined because there are no builds, we
543+ should get "Unknown" as the estimated completion time.
544+ """
545+ builder = Lexbuilder.objects.create()
546+ self.assertEqual("No build in progress", builder.estimated_completion)
547+
548+ def test_estimated_completion_negative_time(self):
549+ """
550+ If the current elapsed time is higher than the average, we should get
551+ "ASAP" as the estimated completion.
552+ """
553+ project = factory.make_project()
554+
555+ create_build_results_with_durations(
556+ project=project, durations=[5, 5],
557+ result=ProjectBuildStates.SUCCESS)
558+
559+ build_result = factory.make_build_result(
560+ project=project, result=ProjectBuildStates.PENDING)
561+ started_at = datetime.now() - timedelta(minutes=10)
562+ build_result.started_at = started_at
563+ build_result.save()
564+
565+ builder = Lexbuilder.objects.create(current_job=build_result)
566+
567+ datetime_mock = self.mocker.replace(datetime)
568+ datetime_mock.now()
569+ self.mocker.result(started_at + timedelta(minutes=10))
570+ self.mocker.replay()
571+
572+ # Average time = 5 minutes
573+ # Started 10 minutes ago (started_at + timedelta(minutes=10))
574+ self.assertEqual("ASAP", builder.estimated_completion)
575+
576 def test_lexbuilder_without_current_job(self):
577 """
578 A Lexbuilder with no current_job is not considered private and thus
579@@ -370,9 +468,18 @@
580 is_private=False, project_group=group)
581 private_project_visible_to_user = factory.make_project(
582 is_private=True, project_group=group, owner=user)
583- private_project = factory.make_project(
584- is_private=True, project_group=group)
585+ factory.make_project(is_private=True, project_group=group)
586 self.assertEqual(
587 sorted([public_project, private_project_visible_to_user],
588 key=attrgetter("name")),
589 sorted(group.get_projects(user), key=attrgetter("name")))
590+
591+ def test_average_build_time(self):
592+ """
593+ ProjectGroup.average_build_time should use the BuildResult metric.
594+ """
595+ project_group = factory.make_project_group()
596+ project = factory.make_project(project_group=project_group)
597+ create_build_results_with_durations(
598+ project, [10, 20], result=ProjectBuildStates.SUCCESS)
599+ self.assertEqual(15 * 60.0, project_group.average_build_time)
600
601=== modified file 'lib/offspring/web/templates/queuemanager/builder_details.html'
602--- lib/offspring/web/templates/queuemanager/builder_details.html 2011-12-02 17:04:03 +0000
603+++ lib/offspring/web/templates/queuemanager/builder_details.html 2012-01-16 18:45:54 +0000
604@@ -1,4 +1,5 @@
605 {% extends "base.html" %}
606+{% load format_seconds %}
607
608 {% block header_css %}
609 <link rel="stylesheet" type="text/css" href="/assets/css/multi-column-lists.css"/>
610@@ -65,7 +66,7 @@
611
612 <dl>
613 <dt>Estimated Completion:</dt>
614- <dd>{{builder.estimated_completion}}</dd>
615+ <dd>{{builder.estimated_completion|seconds_to_time}}</dd>
616 </dl>
617
618 <dl style="clear: both;">
619
620=== modified file 'lib/offspring/web/templates/queuemanager/builders.html'
621--- lib/offspring/web/templates/queuemanager/builders.html 2011-12-02 15:44:50 +0000
622+++ lib/offspring/web/templates/queuemanager/builders.html 2012-01-16 18:45:54 +0000
623@@ -1,5 +1,7 @@
624 {% extends "base.html" %}
625 {% load lexbuilder_helpers %}
626+{% load format_seconds %}
627+
628
629 {% block title %}
630 Build Farm
631@@ -25,7 +27,8 @@
632 {{builder.machine_type}}
633 </td>
634 <td align="center">
635- <span title="{{builder.name}} is currently {{builder.current_state}}{% ifequal builder.current_state "BUILDING" %}, estimated completion {{builder.estimated_completion}}{% endifequal %}">{{builder.current_state}}</span>
636+ <span title="{{builder.name}} is currently
637+{{builder.current_state}}{% ifequal builder.current_state "BUILDING" %}, estimated completion {{builder.estimated_completion|seconds_to_time}}{% endifequal %}">{{builder.current_state}}</span>
638 </td>
639 <td align="center">
640 {% if not build_visible %}
641
642=== modified file 'lib/offspring/web/templates/queuemanager/project_details.html'
643--- lib/offspring/web/templates/queuemanager/project_details.html 2011-12-02 14:23:30 +0000
644+++ lib/offspring/web/templates/queuemanager/project_details.html 2012-01-16 18:45:54 +0000
645@@ -1,5 +1,6 @@
646 {% extends "base.html" %}
647 {% load lexbuilder_helpers %}
648+{% load format_seconds %}
649
650 {% block header_css %}
651 <link rel="stylesheet" type="text/css" href="/assets/css/project_details.css"/>
652@@ -832,7 +833,7 @@
653
654 isc.Label.create({
655 ID: "projectBuildHistorySummary",
656- contents: "<i>{{ project_build_results|length }} builds; {{ build_stats.success_count|default:"0" }} successful and {{ build_stats.fail_count|default:"0" }} failed. Average build time: {{ project.average_build_time }}</i>",
657+ contents: "<i>{{ project_build_results|length }} builds; {{ build_stats.success_count|default:"0" }} successful and {{ build_stats.fail_count|default:"0" }} failed. Average build time:{{ project.average_build_time|seconds_to_time }}</i>",
658 position: null,
659 autoDraw: false,
660 height: 20,
661
662=== modified file 'lib/offspring/web/templates/queuemanager/projectgroup_details.html'
663--- lib/offspring/web/templates/queuemanager/projectgroup_details.html 2011-11-18 15:45:06 +0000
664+++ lib/offspring/web/templates/queuemanager/projectgroup_details.html 2012-01-16 18:45:54 +0000
665@@ -1,5 +1,6 @@
666 {% extends "base.html" %}
667 {% load lexbuilder_helpers %}
668+{% load format_seconds %}
669
670 {% block header %}
671 <link rel="stylesheet" type="text/css" href="/assets/css/project_details.css"/>
672@@ -15,7 +16,7 @@
673 <tr><th><h2>General Information</h2></th><th><h2>Build Information</h2></th></tr>
674 </table>
675 <table width="60%" style="table-layout: fixed; float: right;text-align: right">
676- <tr><td width="40%"><b>Average Build:</b></td><td>{{ projectGroup.average_build_time }}</td></tr>
677+ <tr><td width="40%"><b>Average Build:</b></td><td>{{projectGroup.average_build_time|seconds_to_time }}</td></tr>
678 <tr><td><b>Build Count:</b></td><td>{{ projectgroup_build_results|length }}</td></tr>
679 <tr><td><b>Build Results:</b></td><td>{{ build_stats.success_count|default:"0" }} successful build{{ build_stats.success_count|pluralize }} and {{ build_stats.fail_count|default:"0" }} failed. </td></tr>
680 </table>
681
682=== modified file 'setup_test_database.sh'
683--- setup_test_database.sh 2011-11-30 08:19:58 +0000
684+++ setup_test_database.sh 2012-01-16 18:45:54 +0000
685@@ -4,6 +4,7 @@
686 CREATE ROLE offspring_test
687 CREATEROLE INHERIT LOGIN
688 PASSWORD 'temp1234'
689+ WITH CREATEDB
690 END
691
692 sudo su postgres -c psql <<END

Subscribers

People subscribed via source and target branches