Merge lp:~javier.collado/utah/bug1165175 into lp:utah
- bug1165175
- Merge into dev
Status: | Merged |
---|---|
Approved by: | Javier Collado |
Approved revision: | 902 |
Merged at revision: | 878 |
Proposed branch: | lp:~javier.collado/utah/bug1165175 |
Merge into: | lp:utah |
Diff against target: |
1264 lines (+693/-209) 19 files modified
debian/changelog (+1/-0) debian/control (+3/-3) docs/source/development.rst (+16/-14) utah/client/common.py (+23/-9) utah/client/examples/examples/tslist.run (+3/-3) utah/client/examples/master.run (+0/-4) utah/client/examples/pass.run (+0/-4) utah/client/examples/utah_tests/tslist.run (+1/-1) utah/client/examples/utah_tests_sample/tslist.run (+2/-1) utah/client/exceptions.py (+0/-7) utah/client/result.py (+1/-6) utah/client/runner.py (+60/-79) utah/client/testcase.py (+19/-34) utah/client/tests/common.py (+0/-4) utah/client/tests/test_jsonschema.py (+1/-16) utah/client/tests/test_runner.py (+235/-1) utah/client/tests/test_testcase.py (+167/-17) utah/client/tests/test_testsuite.py (+135/-1) utah/client/testsuite.py (+26/-5) |
To merge this branch: | bzr merge lp:~javier.collado/utah/bug1165175 |
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andy Doan (community) | Approve | ||
Javier Collado (community) | Needs Resubmitting | ||
Review via email:
|
Commit message
Description of the change
This branch:
- Fixes DefaultValidator to raise ValidationError exceptions when a problem is
found in a runlist/control file.
- Updates python-jsonschema dependencies to the latest version
- Updates schemas to jsonschema draftv4
- Adds unit test cases to verify required properties, default values, etc.
Regarding the schemas updates, note that the old schema for the master runlist
has been removed and that values that in test case properties in the test suite
runlist are all of them under the 'overrides' property.
I've been able to successfully run the that `pass.run` runlist in a VM with
this branch, but I have to reboot the VM manually for the test to complete.
Anyway, this something that I've seen today with other branches, so it's not a
problem introduce in this branch.
Note that to test the changes here, you'll need the latest version (1.3.0) of
the jsonschema library:
https:/
The main advantage in this new version is that the error messages are way
better than in the older one and that the draft4 version of the jsonschema
specification can be used.
I've uploaded a package to my ppa:
https:/
that you can use in case to run the test cases and test your own runlists. I'll
copy it to the UTAH PPA once this branch is merged.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andy Doan (doanac) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andy Doan (doanac) wrote : | # |
On 04/26/2013 10:24 AM, Javier Collado wrote:
> https:/
> === modified file 'utah/client/
> CONTROL_SCHEMA = {
> + '$schema': 'http://
out of curiosity - what does this do?
> + 'required': [
> + 'description',
> + 'dependencies',
> + 'action',
> + 'expected_results',
> + 'command',
> + 'run_as',
> + ],
if you look at our current code. run_as isn't actually being required.
Additionally - a lot of people probably don't want to have to declare
this. ie - do you make it "jenkins", "utah", etc? So to prevent possible
regressions, I think we should make run_as be optional.
> === modified file 'utah/client/
> === modified file 'utah/client/
nice set of test cases!
> === modified file 'utah/client/
> + def test_run_
> + """Run as is a required property."""
> + with self.assertRais
> + self.validate({
> + 'description': 'description',
> + 'dependencies': 'dependencies',
> + 'action': 'action',
> + 'expected_results': 'expected_results',
> + 'command': 'command',
> + })
as per my other comment - this can probably be removed.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
@Andy
> > === modified file 'utah/client/
> > CONTROL_SCHEMA = {
> > + '$schema': 'http://
>
> out of curiosity - what does this do?
>
From the documentation:
---
The "$schema" keyword is both used as a JSON Schema version identifier and the location of a resource which is itself a JSON Schema, which describes any schema written for this particular version.
---
You can find the whole information here:
http://
> if you look at our current code. run_as isn't actually being required.
I've talked to Joe about this and it seems that the code has a default value for the case in which the value isn't provided, but certainly according to the old version of the schema, it's required:
---
'run_as': {
'type': 'string',
'required': True,
},
---
> Additionally - a lot of people probably don't want to have to declare
> this. ie - do you make it "jenkins", "utah", etc? So to prevent possible
> regressions, I think we should make run_as be optional.
Maybe the reason to make it required in the past was to make explicit the user the should be used for each command.
I guess I can make "run_as" optional and default to "utah", but for now there won't be any regression if it's required. We can discuss this and create another merge proposal to address this.
Some other thing that Joe has reminded me that we should think about, is that the build, setup and cleanup commands are run with the same user as the utah client. One proposal at the time we talked about this, was to create a mini-schema for all the commands so that the command and the user could be specified for every command.
> as per my other comment - this can probably be removed.
The safer approach for now should be follow what it was required in the old schema and discuss making it optional and what default value to use.
- 900. By Javier Collado
-
Fixed typo detected by Andy
- 901. By Javier Collado
-
Made run_as option in tc_control file
- 902. By Javier Collado
-
Removed MissingData exception
Now that DefaultValidator is working again and tests cases have been addded to
verify this, there's no need to run custom tests in the code that duplicate the
validator functionality.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
@Andy
After the talk we had, I've updated "run_as" in the schema to be optional and removed the "MissingData" exception code.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andy Doan (doanac) : | # |
Preview Diff
1 | === modified file 'debian/changelog' |
2 | --- debian/changelog 2013-04-26 12:29:41 +0000 |
3 | +++ debian/changelog 2013-04-29 21:07:35 +0000 |
4 | @@ -7,6 +7,7 @@ |
5 | |
6 | [ Javier Collado ] |
7 | * Use socket timeout in SSH commands instead of SIGALRM (LP: #1169510) |
8 | + * Fixed DefaultValidator and updated schemas to draftv4 (LP: #1165175) |
9 | |
10 | [ Max Brustkern ] |
11 | * Added syslog output (including real time test output) to client |
12 | |
13 | === modified file 'debian/control' |
14 | --- debian/control 2013-04-19 19:08:00 +0000 |
15 | +++ debian/control 2013-04-29 21:07:35 +0000 |
16 | @@ -5,7 +5,7 @@ |
17 | Maintainer: Max Brustkern <max@canonical.com> |
18 | Build-Depends: debhelper (>= 7.0.50~), devscripts, gawk, |
19 | python-all, python-bzrlib, python-netifaces, python-psutil, |
20 | - python-sphinx, python-jsonschema (>= 0.5~) |
21 | + python-sphinx, python-jsonschema (>= 1.3~) |
22 | Standards-Version: 3.9.3 |
23 | Homepage: https://code.launchpad.net/utah |
24 | Vcs-Bzr: https://code.launchpad.net/utah |
25 | @@ -49,7 +49,7 @@ |
26 | Package: utah-client |
27 | Architecture: all |
28 | Depends: ${misc:Depends}, ${python:Depends}, |
29 | - bzr, python-jsonschema (>= 0.5~), python-psutil, python-yaml, sbsigntool, utah-common |
30 | + bzr, python-jsonschema (>= 1.3~), python-psutil, python-yaml, sbsigntool, utah-common |
31 | Recommends: git |
32 | Description: Ubuntu Test Automation Harness Client |
33 | Automation framework for testing in Ubuntu, client portion |
34 | @@ -63,6 +63,6 @@ |
35 | Package: utah-parser |
36 | Architecture: all |
37 | Depends: ${misc:Depends}, ${python:Depends}, |
38 | - python-jsonschema (>= 0.5~), python-yaml, utah-common |
39 | + python-jsonschema (>= 1.3~), python-yaml, utah-common |
40 | Description: Ubuntu Test Automation Harness Parser |
41 | Automation framework for testing in Ubuntu, result parser |
42 | |
43 | === modified file 'docs/source/development.rst' |
44 | --- docs/source/development.rst 2012-10-26 11:01:06 +0000 |
45 | +++ docs/source/development.rst 2013-04-29 21:07:35 +0000 |
46 | @@ -85,20 +85,21 @@ |
47 | |
48 | :: |
49 | |
50 | - - name: testsuite1 |
51 | - fetch_method: git |
52 | - fetch_location: repo |
53 | - include_tests: |
54 | - - t1 |
55 | - - t2 |
56 | - - t3... |
57 | + - testsuites: |
58 | + - name: testsuite1 |
59 | + fetch_method: git |
60 | + fetch_location: repo |
61 | + include_tests: |
62 | + - t1 |
63 | + - t2 |
64 | + - t3... |
65 | |
66 | - - name: testsuite2 |
67 | - fetch_method: bzr |
68 | - fetch_location: lp:utah/dev/ |
69 | - exclude_tests: |
70 | - - st4 |
71 | - - st5 |
72 | + - name: testsuite2 |
73 | + fetch_method: bzr |
74 | + fetch_location: lp:utah/dev/ |
75 | + exclude_tests: |
76 | + - st4 |
77 | + - st5 |
78 | |
79 | The only required fields are ``name``, ``fetch_method`` and ``fetch_location``. |
80 | ``name`` must correspond to the name of the top-level testsuite directory. |
81 | @@ -136,7 +137,8 @@ |
82 | :: |
83 | |
84 | - test: t1 # directory |
85 | - run_as: nobody # user that runs the test |
86 | + overrides: |
87 | + - run_as: nobody # user that runs the test |
88 | |
89 | - test: t2 # directory/binary |
90 | overrides: # array of control properties to override |
91 | |
92 | === modified file 'utah/client/common.py' |
93 | --- utah/client/common.py 2013-04-18 18:17:37 +0000 |
94 | +++ utah/client/common.py 2013-04-29 21:07:35 +0000 |
95 | @@ -363,7 +363,7 @@ |
96 | try: |
97 | pids = subprocess.check_output(['ps', '--no-headers', '-o', 'pid', |
98 | '--ppid', str(pid)]).split() |
99 | - return [int(pid) for pid in pids] |
100 | + return [int(process_id) for process_id in pids] |
101 | except subprocess.CalledProcessError: |
102 | return [] |
103 | |
104 | @@ -404,7 +404,8 @@ |
105 | |
106 | # Validator that sets values to defaults as explained in: |
107 | # https://github.com/Julian/jsonschema/issues/4 |
108 | -class DefaultValidator(jsonschema.Validator): |
109 | +@jsonschema.validates('draft4') |
110 | +class DefaultValidator(jsonschema.Draft4Validator): |
111 | |
112 | """jsonschema validator that sets default values. |
113 | |
114 | @@ -413,13 +414,26 @@ |
115 | |
116 | """ |
117 | |
118 | + @classmethod |
119 | + def check_schema(self, schema): |
120 | + jsonschema.Draft4Validator.check_schema(schema) |
121 | + |
122 | def validate_properties(self, properties, instance, schema): |
123 | """Set missing properties to default value.""" |
124 | - (super(DefaultValidator, self) |
125 | - .validate_properties(properties, instance, schema)) |
126 | - instance.update((k, v['default']) |
127 | - for k, v in properties.iteritems() |
128 | - if k not in instance and 'default' in v) |
129 | + if not self.is_type(instance, 'object'): |
130 | + return |
131 | + |
132 | + errors = (super(DefaultValidator, self) |
133 | + .validate_properties(properties, instance, schema)) |
134 | + for error in errors: |
135 | + yield error |
136 | + |
137 | + default_values = [ |
138 | + (k, v['default']) |
139 | + for k, v in properties.iteritems() |
140 | + if k not in instance and 'default' in v] |
141 | + |
142 | + instance.update(default_values) |
143 | |
144 | |
145 | def parse_control_file(filename, schema): |
146 | @@ -438,8 +452,8 @@ |
147 | |
148 | """ |
149 | control_data = parse_yaml_file(filename) |
150 | - validator = DefaultValidator() |
151 | - validator.validate(control_data, schema) |
152 | + validator = DefaultValidator(schema) |
153 | + validator.validate(control_data) |
154 | return control_data |
155 | |
156 | |
157 | |
158 | === modified file 'utah/client/examples/examples/tslist.run' |
159 | --- utah/client/examples/examples/tslist.run 2012-08-08 21:10:05 +0000 |
160 | +++ utah/client/examples/examples/tslist.run 2013-04-29 21:07:35 +0000 |
161 | @@ -1,11 +1,11 @@ |
162 | - test: test_one |
163 | - command: python test_one.py |
164 | - run_as: nobody |
165 | overrides: |
166 | build_cmd: ./build.sh |
167 | timeout: 3 |
168 | + command: python test_one.py |
169 | + run_as: nobody |
170 | |
171 | - test: test_two |
172 | - command: python test_two.py |
173 | overrides: |
174 | timeout: 300 |
175 | + command: python test_two.py |
176 | |
177 | === modified file 'utah/client/examples/master.run' |
178 | --- utah/client/examples/master.run 2012-10-10 14:44:00 +0000 |
179 | +++ utah/client/examples/master.run 2013-04-29 21:07:35 +0000 |
180 | @@ -1,8 +1,4 @@ |
181 | --- |
182 | -publish: |
183 | - url: http://dashboard.local |
184 | - token: testtoken |
185 | - name: precise-server-amd64 |
186 | testsuites: |
187 | - name: utah_tests |
188 | fetch_method: bzr-export |
189 | |
190 | === modified file 'utah/client/examples/pass.run' |
191 | --- utah/client/examples/pass.run 2012-10-26 10:31:36 +0000 |
192 | +++ utah/client/examples/pass.run 2013-04-29 21:07:35 +0000 |
193 | @@ -1,8 +1,4 @@ |
194 | --- |
195 | -publish: |
196 | - url: http://dashboard.local |
197 | - token: testtoken |
198 | - name: example-pass |
199 | testsuites: |
200 | - name: utah_tests_sample |
201 | fetch_method: dev |
202 | |
203 | === modified file 'utah/client/examples/utah_tests/tslist.run' |
204 | --- utah/client/examples/utah_tests/tslist.run 2012-08-13 16:22:08 +0000 |
205 | +++ utah/client/examples/utah_tests/tslist.run 2013-04-29 21:07:35 +0000 |
206 | @@ -1,7 +1,7 @@ |
207 | - test: test_one |
208 | - run_as: nobody |
209 | overrides: |
210 | timeout: 3 |
211 | + run_as: nobody |
212 | |
213 | - test: test_two |
214 | overrides: |
215 | |
216 | === modified file 'utah/client/examples/utah_tests_sample/tslist.run' |
217 | --- utah/client/examples/utah_tests_sample/tslist.run 2012-04-10 20:23:50 +0000 |
218 | +++ utah/client/examples/utah_tests_sample/tslist.run 2013-04-29 21:07:35 +0000 |
219 | @@ -1,2 +1,3 @@ |
220 | - test: sample_one |
221 | - command: python sample.py |
222 | + overrides: |
223 | + command: python sample.py |
224 | |
225 | === modified file 'utah/client/exceptions.py' |
226 | --- utah/client/exceptions.py 2013-04-08 19:12:25 +0000 |
227 | +++ utah/client/exceptions.py 2013-04-29 21:07:35 +0000 |
228 | @@ -73,10 +73,3 @@ |
229 | """Used to provide additional information when schema validation fails.""" |
230 | |
231 | pass |
232 | - |
233 | - |
234 | -class MissingData(UTAHClientError): |
235 | - |
236 | - """Raised when there is missing data required for an object to proceed.""" |
237 | - |
238 | - pass |
239 | |
240 | === modified file 'utah/client/result.py' |
241 | --- utah/client/result.py 2013-04-05 18:39:43 +0000 |
242 | +++ utah/client/result.py 2013-04-29 21:07:35 +0000 |
243 | @@ -33,15 +33,13 @@ |
244 | """Result collection class.""" |
245 | |
246 | def __init__(self, name=None, testsuite=None, testcase=None, |
247 | - runlist=None, publish_type=None, install_type=None): |
248 | + runlist=None, install_type=None): |
249 | self.results = [] |
250 | self.status = 'PASS' |
251 | self.name = name |
252 | self.testsuite = testsuite |
253 | self.testcase = testcase |
254 | self.runlist = runlist |
255 | - self.publish_type = publish_type |
256 | - self.publish = None |
257 | self.install_type = install_type |
258 | self.start_battery = None |
259 | self.end_battery = None |
260 | @@ -187,9 +185,6 @@ |
261 | 'name': self.name, |
262 | } |
263 | |
264 | - if self.publish is not None: |
265 | - data['publish'] = self.publish |
266 | - |
267 | if self.start_battery or self.end_battery: |
268 | data['battery'] = {} |
269 | if self.start_battery: |
270 | |
271 | === modified file 'utah/client/runner.py' |
272 | --- utah/client/runner.py 2013-04-15 14:11:42 +0000 |
273 | +++ utah/client/runner.py 2013-04-29 21:07:35 +0000 |
274 | @@ -73,86 +73,71 @@ |
275 | status = "NOTRUN" |
276 | utah_exec_path = '/usr/bin/utah' |
277 | |
278 | - TESTSUITE_ENTRY_SCHEMA = { |
279 | - 'type': 'object', |
280 | - 'properties': { |
281 | - 'name': { |
282 | - 'type': 'string', |
283 | - 'required': True, |
284 | - }, |
285 | - 'fetch_method': { |
286 | - 'type': 'string', |
287 | - 'enum': ['bzr', 'bzr-export', 'dev', 'git'], |
288 | - 'required': True, |
289 | - }, |
290 | - 'fetch_location': { |
291 | - 'type': 'string', |
292 | - 'required': True, |
293 | - }, |
294 | - 'include_tests': { |
295 | - 'type': 'array', |
296 | - 'items': {'type': 'string'}, |
297 | - }, |
298 | - 'exclude_tests': { |
299 | - 'type': 'array', |
300 | - 'items': {'type': 'string'}, |
301 | - }, |
302 | - }, |
303 | - } |
304 | - |
305 | - TESTSUITE_INCLUDE_SCHEMA = { |
306 | - 'type': 'object', |
307 | - 'properties': { |
308 | - 'include': { |
309 | - 'type': 'string', |
310 | - 'required': True, |
311 | - }, |
312 | - }, |
313 | - } |
314 | - |
315 | - MASTER_RUNLIST_SCHEMA_ORIG = { |
316 | - 'type': 'array', |
317 | - 'items': { |
318 | - 'type': [TESTSUITE_ENTRY_SCHEMA, TESTSUITE_INCLUDE_SCHEMA], |
319 | - 'required': True, |
320 | - }, |
321 | - } |
322 | - |
323 | - MASTER_RUNLIST_SCHEMA_NEW = { |
324 | - 'type': 'object', |
325 | - 'properties': { |
326 | - "timeout": { |
327 | - "type": "integer", |
328 | - "minimum": 0, |
329 | - }, |
330 | - "repeat_count": { |
331 | - "type": "integer", |
332 | - "minimum": 0, |
333 | - }, |
334 | - "type": { |
335 | - 'type': 'string', |
336 | - 'enum': ['smoke', 'kernel-sru', 'bootspeed', 'upgrade'], |
337 | - }, |
338 | - "name": { |
339 | - 'type': 'string', |
340 | - }, |
341 | - "testsuites": { |
342 | - # must be a list to accept a schema rather than a simple type |
343 | - "type": [MASTER_RUNLIST_SCHEMA_ORIG], |
344 | - "required": True, |
345 | + MASTER_RUNLIST_SCHEMA = { |
346 | + 'type': 'object', |
347 | + 'properties': { |
348 | + 'testsuites': { |
349 | + 'type': 'array', |
350 | + 'items': { |
351 | + 'type': 'object', |
352 | + 'oneOf': [ |
353 | + {'$ref': '#/definitions/testsuite_fetch'}, |
354 | + {'$ref': '#/definitions/testsuite_file'}, |
355 | + ], |
356 | + }, |
357 | + 'minItems': 1, |
358 | }, |
359 | 'battery_measurements': { |
360 | 'type': 'boolean', |
361 | 'default': False, |
362 | - } |
363 | + }, |
364 | + 'timeout': { |
365 | + 'type': 'integer', |
366 | + 'minimum': 1, |
367 | + }, |
368 | + 'repeat_count': { |
369 | + 'type': 'integer', |
370 | + 'minimum': 0, |
371 | + 'default': 0, |
372 | + }, |
373 | + }, |
374 | + 'required': ['testsuites'], |
375 | + 'additionalProperties': False, |
376 | + 'definitions': { |
377 | + 'testsuite_fetch': { |
378 | + 'type': 'object', |
379 | + 'properties': { |
380 | + 'name': {'type': 'string'}, |
381 | + 'fetch_method': { |
382 | + 'type': 'string', |
383 | + 'enum': ['dev', 'bzr', 'bzr-export', 'git'], |
384 | + }, |
385 | + 'fetch_location': {'type': 'string'}, |
386 | + 'include_tests': { |
387 | + 'type': 'array', |
388 | + 'items': {'type': 'string'}, |
389 | + 'minItems': 1, |
390 | + }, |
391 | + 'exclude_tests': { |
392 | + 'type': 'array', |
393 | + 'items': {'type': 'string'}, |
394 | + 'minItems': 1, |
395 | + }, |
396 | + }, |
397 | + 'required': ['name', 'fetch_method', 'fetch_location'], |
398 | + 'additionalProperties': False, |
399 | + }, |
400 | + 'testsuite_file': { |
401 | + 'type': 'object', |
402 | + 'properties': { |
403 | + 'include': {'type': 'string'}, |
404 | + }, |
405 | + 'required': ['include'], |
406 | + 'additionalProperties': False, |
407 | + }, |
408 | } |
409 | } |
410 | |
411 | - MASTER_RUNLIST_SCHEMA = { |
412 | - 'type': [MASTER_RUNLIST_SCHEMA_ORIG, MASTER_RUNLIST_SCHEMA_NEW], |
413 | - 'required': True, |
414 | - } |
415 | - |
416 | def __init__(self, install_type, runlist=None, result_class=Result, |
417 | testdir=UTAH_DIR, state_agent=None, |
418 | resume=False, old_results=None): |
419 | @@ -429,9 +414,9 @@ |
420 | .format(runlist, err)) |
421 | |
422 | data = parse_yaml_file(local_filename) |
423 | - validator = DefaultValidator() |
424 | + validator = DefaultValidator(self.MASTER_RUNLIST_SCHEMA) |
425 | try: |
426 | - validator.validate(data, self.MASTER_RUNLIST_SCHEMA) |
427 | + validator.validate(data) |
428 | except jsonschema.ValidationError as exception: |
429 | raise exceptions.ValidationError( |
430 | 'Master runlist failed to validate: {!r}\n' |
431 | @@ -446,10 +431,6 @@ |
432 | else: |
433 | self.name = 'unnamed' |
434 | |
435 | - # Add publish metatdata to the result object |
436 | - if 'publish' in data: |
437 | - self.result.publish = data['publish'] |
438 | - |
439 | self.battery_measurements = data['battery_measurements'] |
440 | |
441 | seen = [] |
442 | |
443 | === modified file 'utah/client/testcase.py' |
444 | --- utah/client/testcase.py 2013-04-15 13:09:55 +0000 |
445 | +++ utah/client/testcase.py 2013-04-29 21:07:35 +0000 |
446 | @@ -15,7 +15,6 @@ |
447 | |
448 | """Testcase specific code.""" |
449 | |
450 | - |
451 | import os |
452 | import syslog |
453 | |
454 | @@ -31,7 +30,6 @@ |
455 | run_cmd, |
456 | ) |
457 | from utah.client.exceptions import ( |
458 | - MissingData, |
459 | MissingFile, |
460 | ValidationError, |
461 | ) |
462 | @@ -56,38 +54,24 @@ |
463 | type = 'userland' |
464 | |
465 | CONTROL_SCHEMA = { |
466 | + '$schema': 'http://json-schema.org/draft-04/schema#', |
467 | 'type': 'object', |
468 | 'properties': { |
469 | - 'description': { |
470 | - 'type': 'string', |
471 | - 'required': True, |
472 | - }, |
473 | - 'dependencies': { |
474 | - 'type': 'string', |
475 | - 'required': True, |
476 | - }, |
477 | - 'action': { |
478 | - 'type': 'string', |
479 | - 'required': True, |
480 | - }, |
481 | - 'expected_results': { |
482 | - 'type': 'string', |
483 | - 'required': True, |
484 | - }, |
485 | + 'description': {'type': 'string'}, |
486 | + 'dependencies': {'type': 'string'}, |
487 | + 'action': {'type': 'string'}, |
488 | + 'expected_results': {'type': 'string'}, |
489 | 'type': { |
490 | 'type': 'string', |
491 | 'enum': ['userland'] |
492 | }, |
493 | - 'timeout': {'type': 'integer'}, |
494 | + 'timeout': { |
495 | + 'type': 'integer', |
496 | + 'minimum': 1, |
497 | + }, |
498 | 'build_cmd': {'type': 'string'}, |
499 | - 'command': { |
500 | - 'type': 'string', |
501 | - 'required': True, |
502 | - }, |
503 | - 'run_as': { |
504 | - 'type': 'string', |
505 | - 'required': True, |
506 | - }, |
507 | + 'command': {'type': 'string'}, |
508 | + 'run_as': {'type': 'string'}, |
509 | 'tc_setup': {'type': 'string'}, |
510 | 'tc_cleanup': {'type': 'string'}, |
511 | 'reboot': { |
512 | @@ -95,6 +79,14 @@ |
513 | 'enum': ['always', 'pass', 'never'], |
514 | }, |
515 | }, |
516 | + 'required': [ |
517 | + 'description', |
518 | + 'dependencies', |
519 | + 'action', |
520 | + 'expected_results', |
521 | + 'command', |
522 | + ], |
523 | + 'minProperties': 1, |
524 | } |
525 | |
526 | def __init__(self, name, path, result, command=None, timeout=None, |
527 | @@ -215,13 +207,6 @@ |
528 | if self.is_done(): |
529 | return 'PASS' |
530 | |
531 | - # Do not proceed if we are missing data |
532 | - if (self.description is None |
533 | - or self.dependencies is None |
534 | - or self.action is None |
535 | - or self.expected_results is None): |
536 | - raise MissingData |
537 | - |
538 | # Return value to indicate whether processing of a TestSuite should |
539 | # continue. This is to avoid a shutdown race on reboot cases. |
540 | keep_going = True |
541 | |
542 | === modified file 'utah/client/tests/common.py' |
543 | --- utah/client/tests/common.py 2013-04-04 13:42:40 +0000 |
544 | +++ utah/client/tests/common.py 2013-04-29 21:07:35 +0000 |
545 | @@ -45,10 +45,6 @@ |
546 | master_runlist_content = """# utah/self_test.py master runlist |
547 | # needed for utah/self_test.py runs |
548 | --- |
549 | -publish: |
550 | - url: http://dashboard.local/api/add_result/ |
551 | - token: testtoken |
552 | - name: precise-server-amd64 |
553 | testsuites: |
554 | - name: examples |
555 | fetch_method: bzr-export |
556 | |
557 | === modified file 'utah/client/tests/test_jsonschema.py' |
558 | --- utah/client/tests/test_jsonschema.py 2013-04-04 14:17:14 +0000 |
559 | +++ utah/client/tests/test_jsonschema.py 2013-04-29 21:07:35 +0000 |
560 | @@ -25,15 +25,6 @@ |
561 | |
562 | |
563 | yaml_content = """--- |
564 | -- name: testsuite1 |
565 | - fetch_method: bzr |
566 | - fetch_location: lp:utah |
567 | -- name: testsuite2 |
568 | - fetch_method: bzr |
569 | - fetch_location: lp:utah |
570 | -""" |
571 | - |
572 | -yaml_content_new = """--- |
573 | timeout: 101 |
574 | repeat_count: 99 |
575 | testsuites: |
576 | @@ -85,15 +76,9 @@ |
577 | |
578 | """Test schema validation.""" |
579 | |
580 | - def test_orig_schema(self): |
581 | - """Test that the original master.run validates correctly.""" |
582 | - data = yaml.load(yaml_content) |
583 | - print("data: {}".format(data)) |
584 | - jsonschema.validate(data, Runner.MASTER_RUNLIST_SCHEMA) |
585 | - |
586 | def test_multi_schema(self): |
587 | """Test that the new master.run validates correctly.""" |
588 | - data_new = yaml.load(yaml_content_new) |
589 | + data_new = yaml.load(yaml_content) |
590 | print("data_new: {}".format(data_new)) |
591 | jsonschema.validate(data_new, Runner.MASTER_RUNLIST_SCHEMA) |
592 | |
593 | |
594 | === modified file 'utah/client/tests/test_runner.py' |
595 | --- utah/client/tests/test_runner.py 2013-04-04 16:48:03 +0000 |
596 | +++ utah/client/tests/test_runner.py 2013-04-29 21:07:35 +0000 |
597 | @@ -21,10 +21,15 @@ |
598 | import unittest |
599 | from mock import patch |
600 | |
601 | +import jsonschema |
602 | + |
603 | from utah.client.result import ResultYAML |
604 | from utah.client.state_agent import StateAgentYAML |
605 | from utah.client.runner import Runner |
606 | -from utah.client.common import ReturnCodes |
607 | +from utah.client.common import ( |
608 | + DefaultValidator, |
609 | + ReturnCodes, |
610 | +) |
611 | from utah.client import exceptions |
612 | |
613 | from utah.client.tests.common import ( # NOQA |
614 | @@ -183,3 +188,232 @@ |
615 | ['test_a', 'test_b', 'test_c']) |
616 | self.assertListEqual(kwargs['excludes'], |
617 | ['test_1', 'test_2', 'test_3']) |
618 | + |
619 | + |
620 | +class TestRunnerMasterRunlistSchema(unittest.TestCase): |
621 | + """Master runlist schema test cases.""" |
622 | + def setUp(self): |
623 | + schema = Runner.MASTER_RUNLIST_SCHEMA |
624 | + DefaultValidator.check_schema(schema) |
625 | + self.validator = DefaultValidator(schema) |
626 | + |
627 | + def validate(self, data): |
628 | + return self.validator.validate(data) |
629 | + |
630 | + def test_empty_string_invalid(self): |
631 | + """An empty string doesn't validate.""" |
632 | + with self.assertRaises(jsonschema.ValidationError): |
633 | + self.validate('') |
634 | + |
635 | + def test_empty_dict_invalid(self): |
636 | + """An empty dictionary is invalid.""" |
637 | + with self.assertRaises(jsonschema.ValidationError): |
638 | + self.validate({}) |
639 | + |
640 | + def test_valid(self): |
641 | + """An example of valid data.""" |
642 | + self.validate({ |
643 | + 'testsuites': [{ |
644 | + 'name': 'name', |
645 | + 'fetch_method': 'dev', |
646 | + 'fetch_location': 'location', |
647 | + }] |
648 | + }) |
649 | + |
650 | + def test_testsuites_required(self): |
651 | + """A list of test suites is required.""" |
652 | + with self.assertRaises(jsonschema.ValidationError): |
653 | + self.validate({ |
654 | + 'battery_measurements': True, |
655 | + }) |
656 | + |
657 | + def test_testsuites_empty_invalid(self): |
658 | + """An empty list of test suites is invalid.""" |
659 | + with self.assertRaises(jsonschema.ValidationError): |
660 | + self.validate({ |
661 | + 'testsuites': [], |
662 | + }) |
663 | + |
664 | + def test_testsuite_name_required(self): |
665 | + """Test suite name is required.""" |
666 | + with self.assertRaises(jsonschema.ValidationError): |
667 | + self.validate({ |
668 | + 'testsuites': [{ |
669 | + 'fetch_method': 'dev', |
670 | + 'fetch_location': 'fetch location', |
671 | + }], |
672 | + }) |
673 | + |
674 | + def test_testsuite_fetch_method_required(self): |
675 | + """Test suite fetch method is required.""" |
676 | + with self.assertRaises(jsonschema.ValidationError): |
677 | + self.validate({ |
678 | + 'testsuites': [{ |
679 | + 'name': 'name', |
680 | + 'fetch_location': 'fetch location', |
681 | + }], |
682 | + }) |
683 | + |
684 | + def test_testsuite_fetch_method_known_values(self): |
685 | + """Test suite fetch method known values are valid.""" |
686 | + testsuite = { |
687 | + 'name': 'name', |
688 | + 'fetch_location': 'fetch location', |
689 | + } |
690 | + |
691 | + data = {'testsuites': [testsuite]} |
692 | + |
693 | + for fetch_method in ['dev', 'bzr', 'bzr-export', 'git']: |
694 | + testsuite['fetch_method'] = fetch_method |
695 | + self.validate(data) |
696 | + |
697 | + def test_testsuite_fetch_method_unknown_value(self): |
698 | + """Test suite fetch method unknown values are valid.""" |
699 | + with self.assertRaises(jsonschema.ValidationError): |
700 | + self.validate({ |
701 | + 'testsuites': [{ |
702 | + 'name': 'name', |
703 | + 'fetch_method': 'unknown', |
704 | + 'fetch_location': 'fetch location', |
705 | + }], |
706 | + }) |
707 | + |
708 | + def test_testsuite_fetch_location_required(self): |
709 | + """Test suite fetch location is required.""" |
710 | + with self.assertRaises(jsonschema.ValidationError): |
711 | + self.validate({ |
712 | + 'testsuites': [{ |
713 | + 'name': 'name', |
714 | + 'fetch_method': 'dev', |
715 | + }], |
716 | + }) |
717 | + |
718 | + def test_testsuite_include_tests_empty_invalid(self): |
719 | + """Empty list of included tests is invalid.""" |
720 | + with self.assertRaises(jsonschema.ValidationError): |
721 | + self.validate({ |
722 | + 'testsuites': [{ |
723 | + 'name': 'name', |
724 | + 'fetch_method': 'dev', |
725 | + 'fetch_location': 'fetch location', |
726 | + 'include_tests': [], |
727 | + }], |
728 | + }) |
729 | + |
730 | + def test_testsuite_include_tests_string(self): |
731 | + """List of strings as included tests is valid.""" |
732 | + self.validate({ |
733 | + 'testsuites': [{ |
734 | + 'name': 'name', |
735 | + 'fetch_method': 'dev', |
736 | + 'fetch_location': 'fetch location', |
737 | + 'include_tests': ['test_1', 'test_2', 'test_3'], |
738 | + }], |
739 | + }) |
740 | + |
741 | + def test_testsuite_exclude_tests_empty_invalid(self): |
742 | + """Empty list of excluded tests is invalid.""" |
743 | + with self.assertRaises(jsonschema.ValidationError): |
744 | + self.validate({ |
745 | + 'testsuites': [{ |
746 | + 'name': 'name', |
747 | + 'fetch_method': 'dev', |
748 | + 'fetch_location': 'fetch location', |
749 | + 'exclude_tests': [], |
750 | + }], |
751 | + }) |
752 | + |
753 | + def test_testsuite_exclude_tests_string(self): |
754 | + """List of strings as excluded tests is valid.""" |
755 | + self.validate({ |
756 | + 'testsuites': [{ |
757 | + 'name': 'name', |
758 | + 'fetch_method': 'dev', |
759 | + 'fetch_location': 'fetch location', |
760 | + 'exclude_tests': ['test_1', 'test_2', 'test_3'], |
761 | + }], |
762 | + }) |
763 | + |
764 | + def test_battery_measurements_false_by_default(self): |
765 | + """Battery measurements is false by default""" |
766 | + data = { |
767 | + 'testsuites': [{ |
768 | + 'name': 'name', |
769 | + 'fetch_method': 'dev', |
770 | + 'fetch_location': 'fetch location', |
771 | + }], |
772 | + } |
773 | + self.validate(data) |
774 | + self.assertFalse(data['battery_measurements']) |
775 | + |
776 | + def test_battery_measurements_string_invalid(self): |
777 | + """A battery measurements string value is invalid""" |
778 | + with self.assertRaises(jsonschema.ValidationError): |
779 | + self.validate({ |
780 | + 'testsuites': [{ |
781 | + 'name': 'name', |
782 | + 'fetch_method': 'dev', |
783 | + 'fetch_location': 'fetch location', |
784 | + }], |
785 | + 'battery_measurements': 'value', |
786 | + }) |
787 | + |
788 | + def test_battery_measurements_boolean_valid(self): |
789 | + """A battery measurements boolean value is valid""" |
790 | + self.validate({ |
791 | + 'testsuites': [{ |
792 | + 'name': 'name', |
793 | + 'fetch_method': 'dev', |
794 | + 'fetch_location': 'fetch location', |
795 | + }], |
796 | + 'battery_measurements': True, |
797 | + }) |
798 | + |
799 | + def test_zero_timeout_invalid(self): |
800 | + """A zero timeout is invalid""" |
801 | + with self.assertRaises(jsonschema.ValidationError): |
802 | + self.validate({ |
803 | + 'testsuites': [{ |
804 | + 'name': 'name', |
805 | + 'fetch_method': 'dev', |
806 | + 'fetch_location': 'fetch location', |
807 | + }], |
808 | + 'timeout': 0, |
809 | + }) |
810 | + |
811 | + def test_negative_timeout_invalid(self): |
812 | + """A negative timeout is invalid""" |
813 | + with self.assertRaises(jsonschema.ValidationError): |
814 | + self.validate({ |
815 | + 'testsuites': [{ |
816 | + 'name': 'name', |
817 | + 'fetch_method': 'dev', |
818 | + 'fetch_location': 'fetch location', |
819 | + }], |
820 | + 'timeout': -1, |
821 | + }) |
822 | + |
823 | + def test_repeat_count_zero_by_default(self): |
824 | + """Repeat count is zero by default""" |
825 | + data = { |
826 | + 'testsuites': [{ |
827 | + 'name': 'name', |
828 | + 'fetch_method': 'dev', |
829 | + 'fetch_location': 'fetch location', |
830 | + }], |
831 | + } |
832 | + |
833 | + self.validate(data) |
834 | + self.assertEqual(data['repeat_count'], 0) |
835 | + |
836 | + def test_negative_repeat_count_invalid(self): |
837 | + """A negative repeat count is invalid""" |
838 | + with self.assertRaises(jsonschema.ValidationError): |
839 | + self.validate({ |
840 | + 'testsuites': [{ |
841 | + 'name': 'name', |
842 | + 'fetch_method': 'dev', |
843 | + 'fetch_location': 'fetch location', |
844 | + }], |
845 | + 'repeat_count': -1, |
846 | + }) |
847 | |
848 | === modified file 'utah/client/tests/test_testcase.py' |
849 | --- utah/client/tests/test_testcase.py 2013-04-08 19:12:25 +0000 |
850 | +++ utah/client/tests/test_testcase.py 2013-04-29 21:07:35 +0000 |
851 | @@ -19,11 +19,13 @@ |
852 | import os |
853 | import unittest |
854 | |
855 | +import jsonschema |
856 | + |
857 | from utah.client import testcase |
858 | from utah.client.common import ( |
859 | + DefaultValidator, |
860 | UTAH_DIR, |
861 | ) |
862 | -from utah.client.exceptions import MissingData |
863 | from utah.client.result import ResultYAML |
864 | from utah.client.tests.common import ( # NOQA |
865 | setUp, # Used by nosetests |
866 | @@ -139,21 +141,169 @@ |
867 | |
868 | self.assertTrue(case.is_done()) |
869 | |
870 | - def test_bad_control_data(self): |
871 | - """Test that bad control_data does raise an exception.""" |
872 | - control_data = { |
873 | - 'description': 'a test case', |
874 | - 'command': '/bin/true', |
875 | + |
876 | +class TestTestCaseControlSchema(unittest.TestCase): |
877 | + """Test case control schema test cases.""" |
878 | + def setUp(self,): |
879 | + schema = testcase.TestCase.CONTROL_SCHEMA |
880 | + DefaultValidator.check_schema(schema) |
881 | + self.validator = DefaultValidator(schema) |
882 | + |
883 | + def validate(self, data): |
884 | + schema = testcase.TestCase.CONTROL_SCHEMA |
885 | + return jsonschema.validate(data, schema) |
886 | + |
887 | + def test_empty_string_invalid(self): |
888 | + """An empty string doesn't validate.""" |
889 | + with self.assertRaises(jsonschema.ValidationError): |
890 | + self.validate('') |
891 | + |
892 | + def test_empty_dict_invalid(self): |
893 | + """An empty dictionary is valid.""" |
894 | + with self.assertRaises(jsonschema.ValidationError): |
895 | + self.validate({}) |
896 | + |
897 | + def test_valid(self): |
898 | + """An example of valid data.""" |
899 | + self.validate({ |
900 | + 'description': 'description', |
901 | + 'dependencies': 'dependencies', |
902 | + 'action': 'action', |
903 | + 'expected_results': 'expected_results', |
904 | + 'command': 'command', |
905 | + 'run_as': 'run_as', |
906 | + }) |
907 | + |
908 | + def test_description_required(self): |
909 | + """Description is a required property.""" |
910 | + with self.assertRaises(jsonschema.ValidationError): |
911 | + self.validate({ |
912 | + 'dependencies': 'dependencies', |
913 | + 'action': 'action', |
914 | + 'expected_results': 'expected_results', |
915 | + 'command': 'command', |
916 | + 'run_as': 'run_as', |
917 | + }) |
918 | + |
919 | + def test_dependencies_required(self): |
920 | + """Dependencies is a required property.""" |
921 | + with self.assertRaises(jsonschema.ValidationError): |
922 | + self.validate({ |
923 | + 'description': 'description', |
924 | + 'action': 'action', |
925 | + 'expected_results': 'expected_results', |
926 | + 'command': 'command', |
927 | + 'run_as': 'run_as', |
928 | + }) |
929 | + |
930 | + def test_action_required(self): |
931 | + """Action is a required property.""" |
932 | + with self.assertRaises(jsonschema.ValidationError): |
933 | + self.validate({ |
934 | + 'description': 'description', |
935 | + 'dependencies': 'dependencies', |
936 | + 'expected_results': 'expected_results', |
937 | + 'command': 'command', |
938 | + 'run_as': 'run_as', |
939 | + }) |
940 | + |
941 | + def test_expected_results_required(self): |
942 | + """Expected results is a required property.""" |
943 | + with self.assertRaises(jsonschema.ValidationError): |
944 | + self.validate({ |
945 | + 'description': 'description', |
946 | + 'dependencies': 'dependencies', |
947 | + 'action': 'action', |
948 | + 'command': 'command', |
949 | + 'run_as': 'run_as', |
950 | + }) |
951 | + |
952 | + def test_command_required(self): |
953 | + """Command is a required property.""" |
954 | + with self.assertRaises(jsonschema.ValidationError): |
955 | + self.validate({ |
956 | + 'description': 'description', |
957 | + 'dependencies': 'dependencies', |
958 | + 'action': 'action', |
959 | + 'expected_results': 'expected_results', |
960 | + 'run_as': 'run_as', |
961 | + }) |
962 | + |
963 | + def test_zero_timeout_invalid(self): |
964 | + """A zero timeout is invalid""" |
965 | + with self.assertRaises(jsonschema.ValidationError): |
966 | + self.validate({ |
967 | + 'description': 'description', |
968 | + 'dependencies': 'dependencies', |
969 | + 'action': 'action', |
970 | + 'expected_results': 'expected_results', |
971 | + 'command': 'command', |
972 | + 'run_as': 'run_as', |
973 | + 'timeout': 0, |
974 | + }) |
975 | + |
976 | + def test_negative_timeout_invalid(self): |
977 | + """A negative timeout is invalid""" |
978 | + with self.assertRaises(jsonschema.ValidationError): |
979 | + self.validate({ |
980 | + 'description': 'description', |
981 | + 'dependencies': 'dependencies', |
982 | + 'action': 'action', |
983 | + 'expected_results': 'expected_results', |
984 | + 'command': 'command', |
985 | + 'run_as': 'run_as', |
986 | + 'timeout': -1 |
987 | + }) |
988 | + |
989 | + def test_type_userland_valid(self): |
990 | + """Userland type is valid.""" |
991 | + self.validate({ |
992 | + 'description': 'description', |
993 | + 'dependencies': 'dependencies', |
994 | + 'action': 'action', |
995 | + 'expected_results': 'expected_results', |
996 | + 'command': 'command', |
997 | + 'run_as': 'run_as', |
998 | + 'type': 'userland', |
999 | + }) |
1000 | + |
1001 | + def test_type_unknown_invalid(self): |
1002 | + """Unknown type is valid.""" |
1003 | + with self.assertRaises(jsonschema.ValidationError): |
1004 | + self.validate({ |
1005 | + 'description': 'description', |
1006 | + 'dependencies': 'dependencies', |
1007 | + 'action': 'action', |
1008 | + 'expected_results': 'expected_results', |
1009 | + 'command': 'command', |
1010 | + 'run_as': 'run_as', |
1011 | + 'type': 'unknown', |
1012 | + }) |
1013 | + |
1014 | + def test_reboot_known_valid(self): |
1015 | + """Reboot known values are valid.""" |
1016 | + data = { |
1017 | + 'description': 'description', |
1018 | + 'dependencies': 'dependencies', |
1019 | + 'action': 'action', |
1020 | + 'expected_results': 'expected_results', |
1021 | + 'command': 'command', |
1022 | + 'run_as': 'run_as', |
1023 | } |
1024 | |
1025 | - result = ResultYAML() |
1026 | - |
1027 | - case = testcase.TestCase( |
1028 | - name=self.name, |
1029 | - path=self.path, |
1030 | - result=result, |
1031 | - _control_data=control_data, |
1032 | - ) |
1033 | - |
1034 | - with self.assertRaises(MissingData): |
1035 | - case.run() |
1036 | + for reboot in ['always', 'pass', 'never']: |
1037 | + data['reboot'] = reboot |
1038 | + self.validate(data) |
1039 | + |
1040 | + def test_unknown_reboot_invalid(self): |
1041 | + """Reboot unknown value is invalid.""" |
1042 | + with self.assertRaises(jsonschema.ValidationError): |
1043 | + self.validate({ |
1044 | + 'description': 'description', |
1045 | + 'dependencies': 'dependencies', |
1046 | + 'action': 'action', |
1047 | + 'expected_results': 'expected_results', |
1048 | + 'command': 'command', |
1049 | + 'run_as': 'run_as', |
1050 | + 'reboot': 'unknown', |
1051 | + }) |
1052 | |
1053 | === modified file 'utah/client/tests/test_testsuite.py' |
1054 | --- utah/client/tests/test_testsuite.py 2013-04-04 17:07:22 +0000 |
1055 | +++ utah/client/tests/test_testsuite.py 2013-04-29 21:07:35 +0000 |
1056 | @@ -19,11 +19,14 @@ |
1057 | import os |
1058 | import unittest |
1059 | |
1060 | +import jsonschema |
1061 | + |
1062 | +from utah.client import testsuite |
1063 | from utah.client.common import ( |
1064 | debug_print, |
1065 | + DefaultValidator, |
1066 | ) |
1067 | from utah.client.result import ResultYAML |
1068 | -from utah.client import testsuite |
1069 | |
1070 | from utah.client.tests.common import ( # NOQA |
1071 | setUp, # Used by nosetests |
1072 | @@ -157,3 +160,134 @@ |
1073 | case = suite.tests[0] |
1074 | self.assertEqual(case.name, included_test_name) |
1075 | self.assertNotEqual(case.name, excluded_test_name) |
1076 | + |
1077 | + |
1078 | +class TestTestSuiteRunlistSchema(unittest.TestCase): |
1079 | + """Test suite run list schema test cases.""" |
1080 | + |
1081 | + def setUp(self): |
1082 | + schema = testsuite.TestSuite.RUNLIST_SCHEMA |
1083 | + DefaultValidator.check_schema(schema) |
1084 | + self.validator = DefaultValidator(schema) |
1085 | + |
1086 | + def validate(self, data): |
1087 | + return self.validator.validate(data) |
1088 | + |
1089 | + def test_empty_string_invalid(self): |
1090 | + """An empty string doesn't validate.""" |
1091 | + with self.assertRaises(jsonschema.ValidationError): |
1092 | + self.validate('') |
1093 | + |
1094 | + def test_empty_dict_invalid(self): |
1095 | + """An empty dictionary doesn't validate.""" |
1096 | + with self.assertRaises(jsonschema.ValidationError): |
1097 | + self.validate({}) |
1098 | + |
1099 | + def test_empty_list_invalid(self): |
1100 | + """An empty list doesn't validate.""" |
1101 | + with self.assertRaises(jsonschema.ValidationError): |
1102 | + self.validate([]) |
1103 | + |
1104 | + def test_string_list_invalid(self): |
1105 | + """A list of strings is invalid.""" |
1106 | + with self.assertRaises(jsonschema.ValidationError): |
1107 | + self.validate(['a', 'b', 'c']) |
1108 | + |
1109 | + def test_empty_dict_list_invalid(self): |
1110 | + """A list of empty dictionaries is invalid.""" |
1111 | + with self.assertRaises(jsonschema.ValidationError): |
1112 | + self.validate([{}, {}, {}]) |
1113 | + |
1114 | + def test_unknown_test_property_invalid(self): |
1115 | + """An unknown property in a test object invalid.""" |
1116 | + with self.assertRaises(jsonschema.ValidationError): |
1117 | + self.validate( |
1118 | + [{'test': 'test name', |
1119 | + 'unknown-property': 'value', |
1120 | + }]) |
1121 | + |
1122 | + def test_empty_overrides_invalid(self): |
1123 | + """An empty overrides section is invalid.""" |
1124 | + with self.assertRaises(jsonschema.ValidationError): |
1125 | + self.validate([{'test': 'test name', |
1126 | + 'overrides': {}}]) |
1127 | + |
1128 | + def test_unknown_property_in_overrides_invalid(self): |
1129 | + """An unknown property in overrides section is invalid.""" |
1130 | + with self.assertRaises(jsonschema.ValidationError): |
1131 | + self.validate( |
1132 | + [{'test': 'test name', |
1133 | + 'overrides': {'unknown-property': 'value'} |
1134 | + }]) |
1135 | + |
1136 | + def test_zero_timeout_invalid(self): |
1137 | + """A zero timeout is invalid""" |
1138 | + with self.assertRaises(jsonschema.ValidationError): |
1139 | + self.validate([{'test': 'test name', |
1140 | + 'overrides': {'timeout': 0}}]) |
1141 | + |
1142 | + def test_negative_timeout_invalid(self): |
1143 | + """A negative timeout is invalid""" |
1144 | + with self.assertRaises(jsonschema.ValidationError): |
1145 | + self.validate([{'test': 'test name', |
1146 | + 'overrides': {'timeout': -1}}]) |
1147 | + |
1148 | + def test_list_valid(self): |
1149 | + """A list of test cases is valid.""" |
1150 | + self.validate( |
1151 | + [{'test': 'test name'}, |
1152 | + {'test': 'test name 2'}]) |
1153 | + |
1154 | + def test_with_overrides_list_valid(self): |
1155 | + """A list of test case names with overrides is valid.""" |
1156 | + self.validate([ |
1157 | + {'test': 'test name', |
1158 | + 'overrides': {'timeout': 1}, |
1159 | + }, |
1160 | + {'test': 'test name 2', |
1161 | + 'overrides': {'timeout': 1} |
1162 | + }, |
1163 | + ]) |
1164 | + |
1165 | + |
1166 | +class TestTestSuiteControlSchema(unittest.TestCase): |
1167 | + """Test suite control schema test cases.""" |
1168 | + def setUp(self): |
1169 | + schema = testsuite.TestSuite.CONTROL_SCHEMA |
1170 | + DefaultValidator.check_schema(schema) |
1171 | + self.validator = DefaultValidator(schema) |
1172 | + |
1173 | + def validate(self, data): |
1174 | + return self.validator.validate(data) |
1175 | + |
1176 | + def test_empty_string_invalid(self): |
1177 | + """An empty string doesn't validate.""" |
1178 | + with self.assertRaises(jsonschema.ValidationError): |
1179 | + self.validate('') |
1180 | + |
1181 | + def test_empty_dict_valid(self): |
1182 | + """An empty dictionary is valid.""" |
1183 | + self.validate({}) |
1184 | + |
1185 | + def test_unknown_property_invalid(self): |
1186 | + """An unknown control property is invalid.""" |
1187 | + with self.assertRaises(jsonschema.ValidationError): |
1188 | + self.validate({'unknown-property': 'value'}) |
1189 | + |
1190 | + def test_zero_timeout_invalid(self): |
1191 | + """A zero timeout is invalid.""" |
1192 | + with self.assertRaises(jsonschema.ValidationError): |
1193 | + self.validate({'timeout': 0}) |
1194 | + |
1195 | + def test_negative_timeout_invalid(self): |
1196 | + """A negative timeout is invalid.""" |
1197 | + with self.assertRaises(jsonschema.ValidationError): |
1198 | + self.validate({'timeout': -1}) |
1199 | + |
1200 | + def test_valid(self): |
1201 | + """An example of valid control file contents.""" |
1202 | + self.validate({'build_cmd': 'command', |
1203 | + 'ts_setup': 'command', |
1204 | + 'ts_cleanup': 'command', |
1205 | + 'timeout': 1, |
1206 | + }) |
1207 | |
1208 | === modified file 'utah/client/testsuite.py' |
1209 | --- utah/client/testsuite.py 2013-04-15 13:09:55 +0000 |
1210 | +++ utah/client/testsuite.py 2013-04-29 21:07:35 +0000 |
1211 | @@ -65,27 +65,48 @@ |
1212 | save_state_callback = do_nothing |
1213 | |
1214 | RUNLIST_SCHEMA = { |
1215 | + '$schema': 'http://json-schema.org/draft-04/schema#', |
1216 | 'type': 'array', |
1217 | + 'minItems': 1, |
1218 | 'items': { |
1219 | 'type': 'object', |
1220 | 'properties': { |
1221 | 'test': { |
1222 | 'type': 'string', |
1223 | - 'required': True, |
1224 | - }, |
1225 | - 'overrides': {'type': 'object'}, |
1226 | + }, |
1227 | + 'overrides': { |
1228 | + 'type': 'object', |
1229 | + 'properties': { |
1230 | + 'timeout': { |
1231 | + 'type': 'integer', |
1232 | + 'minimum': 1, |
1233 | + }, |
1234 | + 'build_cmd': {'type': 'string'}, |
1235 | + 'command': {'type': 'string'}, |
1236 | + 'run_as': {'type': 'string'}, |
1237 | + }, |
1238 | + 'minProperties': 1, |
1239 | + 'additionalProperties': False, |
1240 | + }, |
1241 | }, |
1242 | + 'required': ['test'], |
1243 | + 'additionalProperties': False, |
1244 | }, |
1245 | } |
1246 | |
1247 | CONTROL_SCHEMA = { |
1248 | + '$schema': 'http://json-schema.org/draft-04/schema#', |
1249 | 'type': 'object', |
1250 | 'properties': { |
1251 | + 'timeout': { |
1252 | + 'type': 'integer', |
1253 | + 'minimum': 1, |
1254 | + }, |
1255 | 'build_cmd': {'type': 'string'}, |
1256 | - 'timeout': {'type': 'integer'}, |
1257 | 'ts_setup': {'type': 'string'}, |
1258 | 'ts_cleanup': {'type': 'string'}, |
1259 | - } |
1260 | + }, |
1261 | + 'additionalProperties': False, |
1262 | } |
1263 | |
1264 | def __init__(self, name, path, result, runlist_file=DEFAULT_TSLIST, |
591 + def test_empty_ dict_invalid( self): es(jsonschema. ValidationError ):
592 + """An empty dictionary is valid."""
593 + with self.assertRais
594 + self.validate({})
i think 592 should be "An empty dictionary isn't valid."