Merge lp:~bigkevmcd/offspring/fix-properties-on-builder-and-project-group into lp:offspring
- fix-properties-on-builder-and-project-group
- Merge into trunk
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 |
Related bugs: |
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.
Commit message
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.
- 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.
Cody A.W. Somerville (cody-somerville) wrote : | # |
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_
Good idea, fixed.
>
> API wise, I think we should return a datetime object of when we expect the
> build to complete - 'self.current_
> datetime.
> 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_
Changed...
> Instead of having magic numbers, you should calculate the average of the list
> you pass to create_
Good idea, create_
>
> 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_
> both code pathways use. Would it not be better to have tests for the behaviour
> of get_average_
> BuildResultMetr
> specific behaviours of BuildResultMetr
> BuildResultMetr
> basically doing the same tests twice.>
So, there isn't actually as much repetition of the tests as it appears, also, the tests for BuldResultsMetr
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
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 |
>>> === modified file 'Makefile' web.queuemanage r -s --settings= offspring. web.settings_ test web.queuemanage r -s --settings= offspring. web.settings_ devel web.queuemanage r --settings= offspring. web.settings_ devel bin/nosetests -e 'queuemanager.*'
>>> --- 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.
>>>
>>> test-web-postgres: web web-test install-test-runner
>>> - ./bin/offspring-web test offspring.
>>> + ./bin/offspring-web test offspring.
>>>
>>> test: master slave web install-test-runner test-web master-test
>>> ./.virtualenv/
>>>
Why is this change required?
<snip> web/queuemanage r/models. py' web/queuemanage r/models. py 2012-01-16 17:28:38 +0000 web/queuemanage r/models. py 2012-01-16 18:45:54 +0000 completion( self): execute( "SELECT SUM(finished_at - started_at) / count(*) from buildresults WHERE project_name = %s AND result = %s", [self.current_ job.project. name, "COMPLETED"]) job.started_ at maining = averageBuildTime - currentElapsedTime meRemaining) startswith( "-"): metrics. get_average_ build_time( self.current_ job.project)
>>> === modified file 'lib/offspring/
>>> --- lib/offspring/
>>> +++ lib/offspring/
<snip>
>>> @@ -357,24 +322,16 @@
>>> @property
>>> def estimated_
>>> if self.current_job is not None:
>>> - #XXX: Duplication of code in Project to get average build time
>>> - cursor = connection.cursor()
>>> - cursor.
>>> - row = cursor.fetchone()
>>> - if row[0] is not None:
>>> - averageBuildTime = row[0]
>>> - currentElapsedTime = datetime.now() - self.current_
>>> - estimatedTimeRe
>>> - #XXX: Duplication of code in Project to format output
>>> - result = str(estimatedTi
>>> - if result.
>>> + average_build_time = BuildResult.
>>> + 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: job.started_ at) completion =(average_ build_time - time.seconds) completion < 0.0: float(seconds) )) float(seconds) )) completion
>>> + elapsed_time = (datetime.now() - self.current_
>>> + estimated_
>>> + elapsed_
>>> + if estimated_
>>> return "ASAP"
>>> else:
>>> - hours, minutes, seconds = result.split(":")
>>> - if int(hours) < 1:
>>> - return "%s mins, %s secs" % (minutes, math.floor(
>>> - else:
>>> - return "%s hrs, %s mins, %s secs" % (hours, minutes, math.floor(
>>> + return estimated_
>>> 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...