Merge lp:~bigkevmcd/offspring/add-metrics into lp:offspring
- add-metrics
- Merge into trunk
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 |
Related bugs: |
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.
Commit message
Description of the change
This adds the metrics per the feature request.
David Murphy (schwuk) wrote : | # |
...and the validation for end date being greater than start date doesn't appear to be working: https:/
Kevin McDermott (bigkevmcd) wrote : | # |
> The (calculated) number of days is consistently one out:
> https:/
> https:/
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:/
> 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?
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:/
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:/
Ummm...you do know that , is valid in an ISO 8601 date? as is / :-)
- 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.
David Murphy (schwuk) wrote : | # |
Looks good. Thanks.
David Murphy (schwuk) wrote : | # |
Damn - wrong MP.
Unmerged revisions
Preview Diff
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 |
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.