Merge lp:~javier.collado/utah/bug1165175 into lp:utah

Proposed by Javier Collado
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
Reviewer Review Type Date Requested Status
Andy Doan (community) Approve
Javier Collado (community) Needs Resubmitting
Review via email: mp+161168@code.launchpad.net

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://github.com/Julian/jsonschema

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://launchpad.net/~javier.collado/+archive/ppa/

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.

To post a comment you must log in.
Revision history for this message
Andy Doan (doanac) wrote :

591 + def test_empty_dict_invalid(self):
592 + """An empty dictionary is valid."""
593 + with self.assertRaises(jsonschema.ValidationError):
594 + self.validate({})

i think 592 should be "An empty dictionary isn't valid."

Revision history for this message
Andy Doan (doanac) wrote :

On 04/26/2013 10:24 AM, Javier Collado wrote:
> https://code.launchpad.net/~javier.collado/utah/bug1165175/+merge/161168

> === modified file 'utah/client/testcase.py'
> CONTROL_SCHEMA = {
> + '$schema': 'http://json-schema.org/draft-04/schema#',

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/tests/test_jsonschema.py'
> === modified file 'utah/client/tests/test_runner.py'

nice set of test cases!

> === modified file 'utah/client/tests/test_testcase.py'

> + def test_run_as_required(self):
> + """Run as is a required property."""
> + with self.assertRaises(jsonschema.ValidationError):
> + self.validate({
> + 'description': 'description',
> + 'dependencies': 'dependencies',
> + 'action': 'action',
> + 'expected_results': 'expected_results',
> + 'command': 'command',
> + })

as per my other comment - this can probably be removed.

Revision history for this message
Javier Collado (javier.collado) wrote :

@Andy

> > === modified file 'utah/client/testcase.py'
> > CONTROL_SCHEMA = {
> > + '$schema': 'http://json-schema.org/draft-04/schema#',
>
> 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://json-schema.org/latest/json-schema-core.html#anchor22

> 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.

lp:~javier.collado/utah/bug1165175 updated
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.

Revision history for this message
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.

review: Needs Resubmitting
Revision history for this message
Andy Doan (doanac) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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,

Subscribers

People subscribed via source and target branches