Merge lp:~bigkevmcd/offspring/add-metrics into lp:offspring

Proposed by Kevin McDermott
Status: Superseded
Proposed branch: lp:~bigkevmcd/offspring/add-metrics
Merge into: lp:offspring
Diff against target: 463 lines (+308/-58)
5 files modified
lib/offspring/web/queuemanager/management/commands/build_metrics.py (+32/-10)
lib/offspring/web/queuemanager/management/commands/tests/test_build_metrics.py (+71/-4)
lib/offspring/web/queuemanager/metrics.py (+64/-4)
lib/offspring/web/queuemanager/tests/test_metrics.py (+139/-40)
requirements/requirements.web.txt (+2/-0)
To merge this branch: bzr merge lp:~bigkevmcd/offspring/add-metrics
Reviewer Review Type Date Requested Status
David Murphy (community) Approve
Review via email: mp+167534@code.launchpad.net

This proposal has been superseded by a proposal from 2013-06-06.

Description of the change

This adds the metrics per the feature request.

To post a comment you must log in.
Revision history for this message
David Murphy (schwuk) wrote :

The (calculated) number of days is consistently one out: https://pastebin.canonical.com/92199/ and https://pastebin.canonical.com/92201/

Trying to specify the same start and end month of February results in an error: https://pastebin.canonical.com/92200/. I know the bug states "the end date is greater than the start date" but what was actually meant by that is that --start=2013-02 --end=2013-01 would be invalid, however due to date expansion rules laid out in the bug, --start=2013-02 --end=2013-02 would be expanded to --start=2013-02-01 00:00:00 --end=2013-02-28 23:59:59 and therefore be valid.

review: Needs Fixing
Revision history for this message
David Murphy (schwuk) wrote :

...and the validation for end date being greater than start date doesn't appear to be working: https://pastebin.canonical.com/92208/

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

> The (calculated) number of days is consistently one out:
> https://pastebin.canonical.com/92199/ and
> https://pastebin.canonical.com/92201/

As explained, this isn't quite right...the bug explicitly asks for less than a day...

> Trying to specify the same start and end month of February results in an
> error: https://pastebin.canonical.com/92200/. I know the bug states "the end
> date is greater than the start date" but what was actually meant by that is
> that --start=2013-02 --end=2013-01 would be invalid, however due to date
> expansion rules laid out in the bug, --start=2013-02 --end=2013-02 would be
> expanded to --start=2013-02-01 00:00:00 --end=2013-02-28 23:59:59 and
> therefore be valid.

So, should we fix this or not?

Revision history for this message
David Murphy (schwuk) wrote :

On the other hand, I like that the date parsing is not strict (despite the request for handling invalid formats in the bug): https://pastebin.canonical.com/92209/

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

> On the other hand, I like that the date parsing is not strict (despite the
> request for handling invalid formats in the bug):
> https://pastebin.canonical.com/92209/

Ummm...you do know that , is valid in an ISO 8601 date? as is / :-)

lp:~bigkevmcd/offspring/add-metrics updated
165. By Kevin McDermott

Fix the start less than end bug.

166. By Kevin McDermott

Implement calculate_days.

167. By Kevin McDermott

Change the method of calcuating the date-diff to round up.

168. By Kevin McDermott

Failing tests for pseudo_iso8601.

169. By Kevin McDermott

Implement parsing with the pseudo iso8601 parser.

170. By Kevin McDermott

Rename year variable.

Revision history for this message
David Murphy (schwuk) wrote :

Looks good. Thanks.

review: Approve
Revision history for this message
David Murphy (schwuk) wrote :

Damn - wrong MP.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/offspring/web/queuemanager/management/commands/build_metrics.py'
2--- lib/offspring/web/queuemanager/management/commands/build_metrics.py 2011-12-14 07:58:45 +0000
3+++ lib/offspring/web/queuemanager/management/commands/build_metrics.py 2013-06-06 09:47:40 +0000
4@@ -1,15 +1,37 @@
5-# Copyright 2010-2011 Canonical Ltd.
6-
7-from django.core.management.base import NoArgsCommand
8-
9-from offspring.web.queuemanager.metrics import get_build_result_metrics
10-
11-
12-class Command(NoArgsCommand):
13+# Copyright 2010-2013 Canonical Ltd.
14+from optparse import make_option
15+
16+from django.core.management.base import BaseCommand, CommandError
17+from django.db import transaction
18+
19+from offspring.web.queuemanager.metrics import (
20+ get_build_result_metrics, calculate_start_and_end)
21+
22+
23+class Command(BaseCommand):
24 help = "Outputs some basic metrics."
25
26- def handle_noargs(self, **options):
27- metrics = get_build_result_metrics()
28+ option_list = BaseCommand.option_list + (
29+ make_option(
30+ "--end", dest="end",
31+ help="End date e.g. 2013-06-05"),
32+ make_option(
33+ "--start", dest="start",
34+ help="Start date e.g. 2013-06-20"),
35+ )
36+
37+ def handle(self, *args, **options):
38+ if options.get("start") and options.get("end"):
39+ start, end = calculate_start_and_end(
40+ options.get("start"), options.get("end"))
41+ if not end > start:
42+ raise CommandError("start date must be before end date")
43+ metrics = get_build_result_metrics(start, end)
44+ elif (options.get("start") and not options.get("end")) or (
45+ options.get("end") and not options.get("start")):
46+ raise CommandError("--start and --end must be supplied")
47+ else:
48+ metrics = get_build_result_metrics()
49
50 for item, value in metrics:
51 self.stdout.write("%s = %s\n" % (item, value))
52
53=== modified file 'lib/offspring/web/queuemanager/management/commands/tests/test_build_metrics.py'
54--- lib/offspring/web/queuemanager/management/commands/tests/test_build_metrics.py 2011-12-15 08:50:53 +0000
55+++ lib/offspring/web/queuemanager/management/commands/tests/test_build_metrics.py 2013-06-06 09:47:40 +0000
56@@ -1,7 +1,9 @@
57 # Copyright 2010-2011 Canonical Ltd.
58 from cStringIO import StringIO
59+from datetime import datetime
60
61 from mocker import MockerTestCase
62+from django.core.management.base import CommandError
63
64 from offspring.web.queuemanager.metrics import get_build_result_metrics
65 from offspring.web.queuemanager.management.commands.build_metrics import (
66@@ -23,7 +25,72 @@
67
68 command = Command()
69 stdout = StringIO()
70- command.stdout = stdout
71- command.handle_noargs()
72-
73- self.assertEqual("Description of value = 200\n", stdout.getvalue())
74+ command.execute(stdout=stdout)
75+
76+ self.assertEqual("Description of value = 200\n", stdout.getvalue())
77+
78+ def test_calls_get_build_result_metrics_with_start_and_end(self):
79+ """
80+ If --start and --end are provided, we should use them to calculate the
81+ period of the metrics.
82+ """
83+ build_result_metrics_mock = self.mocker.replace(
84+ get_build_result_metrics)
85+ build_result_metrics_mock(
86+ datetime(2013, 06, 05, 0, 0, 0),
87+ datetime(2013, 06, 05, 23, 59, 59))
88+
89+ self.mocker.result([("Description of value", 200)])
90+ self.mocker.replay()
91+
92+ command = Command()
93+ stdout = StringIO()
94+ command.execute(start="2013-06-05", end="2013-06-05", stdout=stdout)
95+ self.assertEqual("Description of value = 200\n", stdout.getvalue())
96+
97+ def test_error_with_missing_end_parameters(self):
98+ """
99+ If --start is provided without --end, then we should get an
100+ appropriate error.
101+ """
102+ exit_mock = self.mocker.replace("sys.exit")
103+ exit_mock(1)
104+ self.mocker.replay()
105+
106+ command = Command()
107+ stderr = StringIO()
108+ command.execute(start="2013-06-05", stderr=stderr)
109+
110+ self.assertTrue(
111+ "Error: --start and --end must be supplied" in stderr.getvalue())
112+
113+ def test_error_with_missing_start_parameters(self):
114+ """
115+ If --end is provided without --start, then we should get an
116+ appropriate error.
117+ """
118+ exit_mock = self.mocker.replace("sys.exit")
119+ exit_mock(1)
120+ self.mocker.replay()
121+
122+ command = Command()
123+ stderr = StringIO()
124+ command.execute(end="2013-06-05", stderr=stderr)
125+
126+ self.assertTrue(
127+ "Error: --start and --end must be supplied" in stderr.getvalue())
128+
129+ def test_error_with_end_before_start(self):
130+ """
131+ If the end date is not before the start datae we should get an error.
132+ """
133+ exit_mock = self.mocker.replace("sys.exit")
134+ exit_mock(1)
135+ self.mocker.replay()
136+
137+ command = Command()
138+ stderr = StringIO()
139+ command.execute(start="2013-06-05", end="2013-05-05", stderr=stderr)
140+
141+ self.assertTrue(
142+ "Error: start date must be before end date" in stderr.getvalue())
143
144=== modified file 'lib/offspring/web/queuemanager/metrics.py'
145--- lib/offspring/web/queuemanager/metrics.py 2012-01-12 08:29:41 +0000
146+++ lib/offspring/web/queuemanager/metrics.py 2013-06-06 09:47:40 +0000
147@@ -1,11 +1,14 @@
148 # Copyright 2010 Canonical Ltd. This software is licensed under the
149 # GNU Affero General Public License version 3 (see the file LICENSE).
150-from datetime import datetime, timedelta
151+import re
152+from datetime import datetime, timedelta, time
153+
154+import dateutil.parser
155
156 from offspring.web.queuemanager.models import BuildResult
157
158
159-def get_build_result_metrics():
160+def get_build_result_metrics(start=None, end=None):
161 """
162 Return some simple metrics, in a format suitable for iterating over and
163 displaying in a command-line tool.
164@@ -18,8 +21,13 @@
165 ninety_days = (start_date - timedelta(days=90), start_date)
166
167 build_types = [("scheduled", True), ("requested", False)]
168- build_periods = [("7 days", seven_days), ("30 days", thirty_days),
169- ("90 days", ninety_days)]
170+
171+ if start and end:
172+ days = calculate_days(start, end)
173+ build_periods = [("%d days" % days, (start, end))]
174+ else:
175+ build_periods = [("7 days", seven_days), ("30 days", thirty_days),
176+ ("90 days", ninety_days)]
177 metrics = [(BuildResult.metrics.get_average_build_time, "build"),
178 (BuildResult.metrics.get_average_queue_time, "queue")]
179
180@@ -32,3 +40,55 @@
181 function(period=period, automated=automated)))
182
183 return results
184+
185+
186+def calculate_days(start, end):
187+ """
188+ Returns the number of days between two datetimes, rounded up.
189+ """
190+ delta = end - start
191+ seconds = 86400 * delta.days
192+ seconds += delta.seconds
193+ return round(seconds / 86400.0, 0)
194+
195+
196+def calculate_start_and_end(start, end):
197+ """
198+ Given up to two date strings, return a tuple of start/end date to query
199+ metrics from.
200+
201+ The start date defaults to YYYY-01-01:00:00:00 of the current year.
202+ The end date defaults to YYYY-12-31:23:59:59 of the current year.
203+ """
204+ try:
205+ start_date = parse_pseudo_iso8601(start)
206+ except ValueError:
207+ raise ValueError("Invalid start date %s" % start)
208+ try:
209+ end_date = parse_pseudo_iso8601(end, end=True)
210+ except ValueError:
211+ raise ValueError("Invalid end date %s" % end)
212+ return (start_date, end_date)
213+
214+
215+def parse_pseudo_iso8601(date, end=False):
216+ """
217+ Parses a date from Murphy's pseudo-ISO 8601 format.
218+ """
219+ parsed_date = dateutil.parser.parse(date)
220+ element_count = len(re.split("[,-]", date))
221+ if element_count == 2:
222+ if end:
223+ start_of_next_month = datetime(parsed_date.year, parsed_date.month+1, 1)
224+ parsed_date = start_of_next_month - timedelta(days=1)
225+ else:
226+ parsed_date = datetime(parsed_date.year, parsed_date.month, 1)
227+ elif element_count == 1:
228+ if end:
229+ start_of_next_month = datetime(parsed_date.year + 1, 1, 1)
230+ parsed_date = start_of_next_month - timedelta(days=1)
231+ else:
232+ parsed_date = datetime(parsed_date.year, 1, 1)
233+ if end:
234+ parsed_date = datetime.combine(parsed_date.date(), time(23, 59, 59))
235+ return parsed_date
236
237=== modified file 'lib/offspring/web/queuemanager/tests/test_metrics.py'
238--- lib/offspring/web/queuemanager/tests/test_metrics.py 2012-03-14 15:21:44 +0000
239+++ lib/offspring/web/queuemanager/tests/test_metrics.py 2013-06-06 09:47:40 +0000
240@@ -1,11 +1,13 @@
241 # Copyright 2010 Canonical Ltd. This software is licensed under the
242 # GNU Affero General Public License version 3 (see the file LICENSE).
243+import unittest
244 from datetime import datetime, timedelta
245
246 from django.test import TestCase
247
248 from offspring.web.queuemanager.metrics import (
249- get_build_result_metrics)
250+ get_build_result_metrics, calculate_start_and_end,
251+ calculate_days, parse_pseudo_iso8601)
252
253 from offspring.web.queuemanager.models import BuildResult
254 from offspring.enums import ProjectBuildStates
255@@ -19,51 +21,54 @@
256
257 def setUp(self):
258 self.project = factory.make_project()
259+ self.user = factory.make_user()
260+
261+ def create_test_data(self, user=None, offset=0):
262+ """
263+ Creates tests data with the specified requestor user and datetime offset.
264+ """
265+ start_date = datetime.utcnow()
266+ last_week = start_date - timedelta(days=8)
267+ last_month = start_date - timedelta(days=35)
268+ # Two this week, average = 10 + 20/2 = 15
269+ create_build_results_with_durations(
270+ self.project, [10 + offset, 20],
271+ build_date=start_date - timedelta(days=1),
272+ requestor=user)
273+
274+ # Two this week, average = 30 + 18/2 = 24.0
275+ create_build_results_with_queue_times(
276+ self.project, [30 + offset, 18],
277+ build_date=start_date - timedelta(days=1),
278+ requestor=user)
279+
280+ # Two last week, 10 + 10 + 10 + 20/4 = 12.5
281+ create_build_results_with_durations(
282+ self.project, [10 + offset, 10], build_date=last_week,
283+ requestor=user)
284+
285+ # Two last week, average = 30 + 18 + 14 11/4 = 73
286+ create_build_results_with_queue_times(
287+ self.project, [14 + offset, 11], build_date=last_week,
288+ requestor=user)
289+
290+ # Two last month, 10 + 10 + 10 + 20 + 30 + 40/6 = 20
291+ create_build_results_with_durations(
292+ self.project, [30 + offset, 40], build_date=last_month,
293+ requestor=user)
294+
295+ # Two last month, average = 30 + 18 + 14 11 + 30 + 20/6 = 82
296+ create_build_results_with_queue_times(
297+ self.project, [30 + offset, 20], build_date=last_month,
298+ requestor=user)
299
300 def test_get_build_result_metrics(self):
301 """
302 get_build_result_metrics should return a list of tuples with the metric
303 being measured, and the time in minutes.
304 """
305- def create_test_data(user=None, offset=0):
306- start_date = datetime.utcnow()
307- last_week = start_date - timedelta(days=8)
308- last_month = start_date - timedelta(days=35)
309- # Two this week, average = 10 + 20/2 = 15
310- create_build_results_with_durations(
311- self.project, [10 + offset, 20],
312- build_date=start_date - timedelta(days=1),
313- requestor=user)
314-
315- # Two this week, average = 30 + 18/2 = 24.0
316- create_build_results_with_queue_times(
317- self.project, [30 + offset, 18],
318- build_date=start_date - timedelta(days=1),
319- requestor=user)
320-
321- # Two last week, 10 + 10 + 10 + 20/4 = 12.5
322- create_build_results_with_durations(
323- self.project, [10 + offset, 10], build_date=last_week,
324- requestor=user)
325-
326- # Two last week, average = 30 + 18 + 14 11/4 = 73
327- create_build_results_with_queue_times(
328- self.project, [14 + offset, 11], build_date=last_week,
329- requestor=user)
330-
331- # Two last month, 10 + 10 + 10 + 20 + 30 + 40/6 = 20
332- create_build_results_with_durations(
333- self.project, [30 + offset, 40], build_date=last_month,
334- requestor=user)
335-
336- # Two last month, average = 30 + 18 + 14 11 + 30 + 20/6 = 82
337- create_build_results_with_queue_times(
338- self.project, [30 + offset, 20], build_date=last_month,
339- requestor=user)
340-
341- create_test_data()
342- user = factory.make_user()
343- create_test_data(user=user, offset=10)
344+ self.create_test_data()
345+ self.create_test_data(user=self.user, offset=10)
346
347 self.assertEqual(
348 [("Average scheduled build time (7 days)", 15.0 * 60),
349@@ -80,6 +85,24 @@
350 ("Average requested queue time (90 days)", 25.5 * 60)],
351 get_build_result_metrics())
352
353+ def test_get_build_result_metrics_with_start_and_end_date(self):
354+ """
355+ get_build_result_metrics should return a list of tuples with the metric
356+ being measured, and the time in minutes.
357+ """
358+ self.create_test_data()
359+ self.create_test_data(user=self.user, offset=10)
360+
361+ end_date = datetime.utcnow()
362+ start_date = end_date - timedelta(days=10)
363+
364+ self.assertEqual(
365+ [("Average scheduled build time (10 days)", 12.5 * 60),
366+ ("Average scheduled queue time (10 days)", 18.25 * 60),
367+ ("Average requested build time (10 days)", 17.5 * 60),
368+ ("Average requested queue time (10 days)", 23.25 * 60),],
369+ get_build_result_metrics(start_date, end_date))
370+
371
372 class ProjectGroupBuildMetricsTests(TestCase):
373
374@@ -141,3 +164,79 @@
375 15 * 60,
376 BuildResult.metrics.get_average_queue_time(
377 project_group=self.project_group))
378+
379+
380+class MetricsDateCalculatorTestCase(unittest.TestCase):
381+
382+ def test_start_end_dates(self):
383+ """
384+ * with a start value of YYYY-MM-DD, the actual value will be YYYY-MM-DD
385+ 00:00:00
386+ * with a start value of YYYY, the actual value will be YYYY-01-01 00:00:00
387+ * with an end value of YYYY-MM-DD, the actual value will be YYYY-MM-DD
388+ 23:59:59
389+ * with a end value of YYYY, the actual value will be YYYY-12-31 23:59:59
390+ """
391+ self.assertEqual(
392+ (datetime(2013, 06, 05, 0, 0, 0), datetime(2013, 06, 20, 23, 59, 59)),
393+ calculate_start_and_end("2013-06-05", "2013-06-20"))
394+ self.assertEqual(
395+ (datetime(2013, 01, 01, 0, 0, 0), datetime(2013, 12, 31, 23, 59, 59)),
396+ calculate_start_and_end("2013", "2013"))
397+ self.assertEqual(
398+ (datetime(2013, 05, 01, 0, 0, 0), datetime(2013, 10, 31, 23, 59, 59)),
399+ calculate_start_and_end("2013-05", "2013-10"))
400+ self.assertEqual(
401+ (datetime(2013, 06, 05, 0, 0, 0), datetime(2013, 06, 05, 23, 59, 59)),
402+ calculate_start_and_end("2013-06-05", "2013-06-05"))
403+
404+ def test_parse_errors(self):
405+ """
406+ calculate_start_and_end should return a sensible message if an error
407+ occurs during parsing.
408+ """
409+ with self.assertRaises(ValueError) as cm:
410+ calculate_start_and_end("TESTING", "2013-06-05")
411+ self.assertEqual(cm.exception.message, "Invalid start date TESTING")
412+
413+ with self.assertRaises(ValueError) as cm:
414+ calculate_start_and_end("2013-06-05", "TESTING")
415+ self.assertEqual(cm.exception.message, "Invalid end date TESTING")
416+
417+ def test_calculate_days(self):
418+ """
419+ calculate_days should calculate the number of days between two
420+ timestamps, rounding up, rather than rounding down as timedeltas do.
421+ """
422+ start = datetime(2013, 01, 01, 00, 00, 00)
423+ end = datetime(2013, 12, 31, 23, 59, 59)
424+ self.assertEqual(365, calculate_days(start, end))
425+
426+
427+class ParsePseudoISO8601TestCase(unittest.TestCase):
428+
429+ def test_parse_pseudo_iso8601_start(self):
430+ """
431+ We should calculate the date in various permutations.
432+ """
433+ self.assertEqual(
434+ datetime(2013, 06, 05, 0, 0, 0), parse_pseudo_iso8601("2013-06-05"))
435+ self.assertEqual(
436+ datetime(2013, 06, 01, 0, 0, 0), parse_pseudo_iso8601("2013-06"))
437+ self.assertEqual(
438+ datetime(2013, 01, 01, 0, 0, 0), parse_pseudo_iso8601("2013"))
439+
440+ def test_parse_pseudo_iso8601_end(self):
441+ """
442+ We should calculate the date in various permutations, if end is true, then
443+ the date calculated should be the end of the period specified.
444+ """
445+ self.assertEqual(
446+ datetime(2013, 06, 05, 23, 59, 59),
447+ parse_pseudo_iso8601("2013-06-05", True))
448+ self.assertEqual(
449+ datetime(2013, 06, 30, 23, 59, 59),
450+ parse_pseudo_iso8601("2013-06", True))
451+ self.assertEqual(
452+ datetime(2013, 12, 31, 23, 59, 59),
453+ parse_pseudo_iso8601("2013", True))
454
455=== modified file 'requirements/requirements.web.txt'
456--- requirements/requirements.web.txt 2013-05-17 13:49:22 +0000
457+++ requirements/requirements.web.txt 2013-06-06 09:47:40 +0000
458@@ -14,3 +14,5 @@
459 celery>=2.3.1
460 django-celery>=2.3.0
461 django-kombu>=0.9.4
462+# Need 1.5 because of Python 2.x compatibility.
463+python-dateutil==1.5

Subscribers

People subscribed via source and target branches