Merge lp:~adeuring/charm-tools/check-config-yaml into lp:~charmers/charm-tools/trunk

Proposed by Abel Deuring
Status: Rejected
Rejected by: Abel Deuring
Proposed branch: lp:~adeuring/charm-tools/check-config-yaml
Merge into: lp:~charmers/charm-tools/trunk
Diff against target: 775 lines (+639/-0)
23 files modified
scripts/lib/proof.py (+89/-0)
tests/charms/broken-categories/config.yaml (+5/-0)
tests/charms/broken-config/README (+1/-0)
tests/charms/broken-config/config.yaml (+5/-0)
tests/charms/broken-config/copyright (+17/-0)
tests/charms/broken-config/hooks/install (+5/-0)
tests/charms/broken-config/hooks/relation-a-relation-joined (+5/-0)
tests/charms/broken-config/hooks/start (+4/-0)
tests/charms/broken-config/hooks/stop (+7/-0)
tests/charms/broken-config/icon.svg (+198/-0)
tests/charms/broken-config/metadata.yaml (+10/-0)
tests/charms/broken-config/revision (+1/-0)
tests/charms/broken-maintainer/config.yaml (+5/-0)
tests/charms/broken-subordinate/config.yaml (+5/-0)
tests/charms/broken-subordinate2/config.yaml (+5/-0)
tests/charms/empty-requires/config.yaml (+5/-0)
tests/charms/icon-template/config.yaml (+5/-0)
tests/charms/missing-maintainer/config.yaml (+5/-0)
tests/charms/mod-spdy/config.yaml (+5/-0)
tests/charms/test/config.yaml (+5/-0)
tests/charms/unknown-metadata/config.yaml (+5/-0)
tests/proof/expected/broken-config (+1/-0)
tests/proof/test_proof.py (+246/-0)
To merge this branch: bzr merge lp:~adeuring/charm-tools/check-config-yaml
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+186066@code.launchpad.net

Description of the change

This branch adds a method check_config_file() to class Linter
of the proof tool.

Working on manage.jujucharms.com, we noticed a few errors related
for problems in the config.yaml files in the job that "ingests"
all charms from Launchpad.

The ingest job does not do a thorough check of the config file --
it justs tries to parse the YAML data in order to store it in the
Mongo DB. So, only parsing errors are currently recorded and presented
on the pages for charm details.

The changes in this branch are based on this specification:

http://bazaar.launchpad.net/~charmers/juju/docs/view/head:/source/service-config.rst

(I could not find any more recent documentation of config.yaml.)

The main points I took from this doc: If the content of config.yaml
is represented as a Python object,

  - this object should be a dictionary with one key, 'options'.
  - config['options'] should in turn also be a dictionary, with
    arbitrary keys. (OK -- it might make sense to check id the
    keys strings.)
  - the values of the dict config['options'] shoule also be a
    dictionary, with three keys: 'description', 'type', 'default'.
  - 'description' is required, 'type' and 'default' are optional
  - 'type' may be 'str', 'int' or 'float'. If no type is specified,
    'str' is assumed.
    Running a script similar to the changes in this branch over a
    local copy of all public charms known in Launchpad, I noticed
    that many charms use the type 'boolean' too, so the changes in
    this branch allow this too.

I added a new method, check_config_file(), to class Linter in
scripts/lib/proof.py that checks the things mentioned above and
generates warnings or errors for those problems in config.yaml I
could imagine. (I would claim though that the check is indeed
complete...)

My first attempt to test the new method by adding charms with
test data to tests/charms turned quickly out to be really
cumbersome: Each simple test would require its own subdirectory with
a bunch of files in order to limit the errors generated when
tests/proof/test.sh is run to those relevant for the given test.

I added one charm with an error in config.yaml nevertheless, just
to have some kind of integration test. And the existing test charms
now also have a file config.yaml -- while this bloats the diff of this
branch considerably, the alternative would have been to add an error
message about a missing config file to each file in
tests/proof/expected/, thius adding just noise to the expected data...

Most tests are done in the new file tests/proof/test_proof.py.
The setup of the test is a bit cumbersome, especially the manipulation
of sys.path, but the setup of a better infrastructure for unit tests
is something for another branch, I think.

To post a comment you must log in.
Revision history for this message
Marco Ceppi (marcoceppi) wrote :

Thanks for this work. However lp:charm-tools is about to be massively updated today with work done in the lp:~marcoceppi/charm-tools/python-port branch. Could you move your changes to proof.py and test work to that branch and re-propose the merge there?

Unmerged revisions

184. By Abel Deuring

call Linter.check_config_file() in proof.py; add test files for tests/proof/test.sh

183. By Abel Deuring

check of config file completed.

182. By Abel Deuring

config.yaml: checks for valid option keys added.

181. By Abel Deuring

proof.py: start checking config.yaml; unit tests for config.yaml checks added

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'scripts/lib/proof.py'
2--- scripts/lib/proof.py 2013-05-02 13:08:15 +0000
3+++ scripts/lib/proof.py 2013-09-17 15:05:38 +0000
4@@ -46,6 +46,17 @@
5 os.path.join(
6 __file__, '..', '..', '..', 'templates', 'charm', 'icon.svg'))
7
8+KNOWN_OPTION_KEYS = set(('description', 'type', 'default'))
9+
10+REQUIRED_OPTION_KEYS = set(('description', ))
11+
12+KNOWN_OPTION_TYPES = {
13+ 'str': basestring,
14+ 'int': int,
15+ 'float': float,
16+ 'boolean': bool,
17+}
18+
19
20 class RelationError(Exception):
21 pass
22@@ -150,6 +161,83 @@
23 if not has_one and not subordinate:
24 self.info("relation " + r + " has no hooks")
25
26+ def check_config_file(self, charm_path):
27+ config_path = os.path.join(charm_path, 'config.yaml')
28+ if not os.path.isfile(config_path):
29+ self.warn('File config.yaml not found.')
30+ return
31+ try:
32+ with open(config_path) as config_file:
33+ config = yaml.load(config_file.read())
34+ except Exception, error:
35+ self.err('Cannot parse config.yaml: %s' % error)
36+ return
37+ if not isinstance(config, dict):
38+ self.err('config.yaml not parsed into a dictionary.')
39+ return
40+ if 'options' not in config:
41+ self.err('config.yaml must have an "options" key.')
42+ return
43+ if len(config) > 1:
44+ wrong_keys = sorted(config)
45+ wrong_keys.pop(wrong_keys.index('options'))
46+ self.warn('Ignored keys in config.yaml: %s' % wrong_keys)
47+
48+ options = config['options']
49+ if not isinstance(options, dict):
50+ self.err(
51+ 'config.yaml: options section is not parsed as a dictionary')
52+ return
53+
54+ for option_name, option_value in options.items():
55+ if not isinstance(option_value, dict):
56+ self.err(
57+ 'config.yaml: data for option %s is not a dict'
58+ % option_name)
59+ continue
60+ existing_keys = set(option_value)
61+ missing_keys = KNOWN_OPTION_KEYS - existing_keys
62+ missing_required_keys = REQUIRED_OPTION_KEYS & missing_keys
63+ missing_optional_keys = missing_keys - missing_required_keys
64+ if missing_required_keys:
65+ self.err(
66+ 'config.yaml: option %s does not have the required keys: '
67+ '%s' % (
68+ option_name, ', '.join(sorted(missing_required_keys))))
69+ if missing_optional_keys:
70+ self.warn(
71+ 'config.yaml: option %s does not have the optional keys: '
72+ '%s' % (
73+ option_name, ', '.join(sorted(missing_optional_keys))))
74+ invalid_keys = existing_keys - KNOWN_OPTION_KEYS
75+ if invalid_keys:
76+ invalid_keys = [str(key) for key in sorted(invalid_keys)]
77+ self.warn(
78+ 'config.yaml: option %s as unknown keys: %s' % (
79+ option_name, ', '.join(invalid_keys)))
80+
81+ if 'description' in existing_keys:
82+ if not isinstance(option_value['description'], basestring):
83+ self.warn(
84+ 'config.yaml: description of option %s should be a '
85+ 'string' % option_name)
86+ option_type = option_value.get('type', 'str')
87+ if option_type not in KNOWN_OPTION_TYPES:
88+ self.warn('config.yaml: option %s has an invalid type (%s)'
89+ % (option_name, option_type))
90+ elif 'default' in option_value:
91+ expected_type = KNOWN_OPTION_TYPES[option_value['type']]
92+ if not isinstance(option_value['default'], expected_type):
93+ self.err(
94+ 'config.yaml: type of option %s is specified as '
95+ '%s, but the type of the default value is %s'
96+ % (option_name, option_value['type'],
97+ type(option_value['default']).__name__))
98+ else:
99+ # Nothing to do: the option type is valid but no default
100+ # value exists.
101+ pass
102+
103
104 def get_args():
105 parser = argparse.ArgumentParser(
106@@ -360,6 +448,7 @@
107 except ValueError:
108 lint.err("revision file contains non-numeric data")
109
110+ lint.check_config_file(charm_path)
111 return lint.lint, lint.exit_code
112
113
114
115=== added file 'tests/charms/broken-categories/config.yaml'
116--- tests/charms/broken-categories/config.yaml 1970-01-01 00:00:00 +0000
117+++ tests/charms/broken-categories/config.yaml 2013-09-17 15:05:38 +0000
118@@ -0,0 +1,5 @@
119+options:
120+ foo:
121+ type: int
122+ description: bar
123+ default: 1
124
125=== added directory 'tests/charms/broken-config'
126=== added file 'tests/charms/broken-config/README'
127--- tests/charms/broken-config/README 1970-01-01 00:00:00 +0000
128+++ tests/charms/broken-config/README 2013-09-17 15:05:38 +0000
129@@ -0,0 +1,1 @@
130+just a test
131
132=== added file 'tests/charms/broken-config/config.yaml'
133--- tests/charms/broken-config/config.yaml 1970-01-01 00:00:00 +0000
134+++ tests/charms/broken-config/config.yaml 2013-09-17 15:05:38 +0000
135@@ -0,0 +1,5 @@
136+options:
137+ foo:
138+ type: integer
139+ description: bar
140+ default: 1
141
142=== added file 'tests/charms/broken-config/copyright'
143--- tests/charms/broken-config/copyright 1970-01-01 00:00:00 +0000
144+++ tests/charms/broken-config/copyright 2013-09-17 15:05:38 +0000
145@@ -0,0 +1,17 @@
146+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
147+
148+Files: *
149+Copyright: Copyright 2012, Canonical Ltd., All Rights Reserved.
150+License: GPL-3
151+ This program is free software: you can redistribute it and/or modify
152+ it under the terms of the GNU General Public License as published by
153+ the Free Software Foundation, either version 3 of the License, or
154+ (at your option) any later version.
155+ .
156+ This program is distributed in the hope that it will be useful,
157+ but WITHOUT ANY WARRANTY; without even the implied warranty of
158+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
159+ GNU General Public License for more details.
160+ .
161+ You should have received a copy of the GNU General Public License
162+ along with this program. If not, see <http://www.gnu.org/licenses/>.
163
164=== added directory 'tests/charms/broken-config/hooks'
165=== added file 'tests/charms/broken-config/hooks/install'
166--- tests/charms/broken-config/hooks/install 1970-01-01 00:00:00 +0000
167+++ tests/charms/broken-config/hooks/install 2013-09-17 15:05:38 +0000
168@@ -0,0 +1,5 @@
169+#!/bin/bash
170+# Here do anything needed to install the service
171+# i.e. apt-get install -y foo or bzr branch http://myserver/mycode /srv/webroot
172+
173+apt-get install -y test
174
175=== added file 'tests/charms/broken-config/hooks/relation-a-relation-joined'
176--- tests/charms/broken-config/hooks/relation-a-relation-joined 1970-01-01 00:00:00 +0000
177+++ tests/charms/broken-config/hooks/relation-a-relation-joined 2013-09-17 15:05:38 +0000
178@@ -0,0 +1,5 @@
179+#!/bin/sh
180+# This must be renamed to the name of the relation. The goal here is to
181+# affect any change needed by relationships being formed
182+# This script should be idempotent.
183+juju-log $JUJU_REMOTE_UNIT joined
184
185=== added file 'tests/charms/broken-config/hooks/start'
186--- tests/charms/broken-config/hooks/start 1970-01-01 00:00:00 +0000
187+++ tests/charms/broken-config/hooks/start 2013-09-17 15:05:38 +0000
188@@ -0,0 +1,4 @@
189+#!/bin/bash
190+# Here put anything that is needed to start the service.
191+# Note that currently this is run directly after install
192+# i.e. 'service apache2 start'
193
194=== added file 'tests/charms/broken-config/hooks/stop'
195--- tests/charms/broken-config/hooks/stop 1970-01-01 00:00:00 +0000
196+++ tests/charms/broken-config/hooks/stop 2013-09-17 15:05:38 +0000
197@@ -0,0 +1,7 @@
198+#!/bin/bash
199+# This will be run when the service is being torn down, allowing you to disable
200+# it in various ways..
201+# For example, if your web app uses a text file to signal to the load balancer
202+# that it is live... you could remove it and sleep for a bit to allow the load
203+# balancer to stop sending traffic.
204+# rm /srv/webroot/server-live.txt && sleep 30
205
206=== added file 'tests/charms/broken-config/icon.svg'
207--- tests/charms/broken-config/icon.svg 1970-01-01 00:00:00 +0000
208+++ tests/charms/broken-config/icon.svg 2013-09-17 15:05:38 +0000
209@@ -0,0 +1,198 @@
210+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
211+<!-- Created with Inkscape (http://www.inkscape.org/) -->
212+
213+<svg
214+ xmlns:dc="http://purl.org/dc/elements/1.1/"
215+ xmlns:cc="http://creativecommons.org/ns#"
216+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
217+ xmlns:svg="http://www.w3.org/2000/svg"
218+ xmlns="http://www.w3.org/2000/svg"
219+ xmlns:xlink="http://www.w3.org/1999/xlink"
220+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
221+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
222+ width="96"
223+ height="96"
224+ id="svg6687"
225+ version="1.1"
226+ inkscape:version="0.48.4 r9939"
227+ sodipodi:docname="icon.svg">
228+ <defs
229+ id="defs6689">
230+ <filter
231+ color-interpolation-filters="sRGB"
232+ inkscape:collect="always"
233+ id="filter4951">
234+ <feGaussianBlur
235+ inkscape:collect="always"
236+ stdDeviation="0.44250052"
237+ id="feGaussianBlur4953" />
238+ </filter>
239+ <linearGradient
240+ inkscape:collect="always"
241+ xlink:href="#linearGradient4123"
242+ id="linearGradient4537"
243+ gradientUnits="userSpaceOnUse"
244+ x1="0"
245+ y1="970.29498"
246+ x2="144"
247+ y2="970.29498" />
248+ <linearGradient
249+ id="linearGradient4123">
250+ <stop
251+ style="stop-color:#dd4814;stop-opacity:1;"
252+ offset="0"
253+ id="stop4125" />
254+ <stop
255+ style="stop-color:#ef774d;stop-opacity:1;"
256+ offset="1"
257+ id="stop4127" />
258+ </linearGradient>
259+ <linearGradient
260+ inkscape:collect="always"
261+ xlink:href="#linearGradient4176"
262+ id="linearGradient4378"
263+ gradientUnits="userSpaceOnUse"
264+ x1="0"
265+ y1="970.29498"
266+ x2="144"
267+ y2="970.29498" />
268+ <linearGradient
269+ id="linearGradient4176">
270+ <stop
271+ id="stop4178"
272+ offset="0"
273+ style="stop-color:#505050;stop-opacity:1;" />
274+ <stop
275+ id="stop4180"
276+ offset="1"
277+ style="stop-color:#646464;stop-opacity:1;" />
278+ </linearGradient>
279+ <clipPath
280+ clipPathUnits="userSpaceOnUse"
281+ id="clipPath4046-4">
282+ <g
283+ style="fill:#ff00ff;fill-opacity:1;stroke:none"
284+ inkscape:label="Layer 1"
285+ id="g4048-1"
286+ transform="matrix(0,-0.69444445,0.69379664,0,36.812604,681)">
287+ <path
288+ sodipodi:nodetypes="sssssssss"
289+ inkscape:connector-curvature="0"
290+ id="path4050-4"
291+ d="m 46.702703,898.22775 50.594594,0 C 138.16216,898.22775 144,904.06497 144,944.92583 l 0,50.73846 c 0,40.86071 -5.83784,46.69791 -46.702703,46.69791 l -50.594594,0 C 5.8378378,1042.3622 0,1036.525 0,995.66429 l 0,-50.73846 c 0,-40.86086 5.8378378,-46.69808 46.702703,-46.69808 z"
292+ style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline" />
293+ </g>
294+ </clipPath>
295+ <filter
296+ color-interpolation-filters="sRGB"
297+ inkscape:collect="always"
298+ id="filter4064-5">
299+ <feGaussianBlur
300+ inkscape:collect="always"
301+ stdDeviation="1.6357812"
302+ id="feGaussianBlur4066-7" />
303+ </filter>
304+ <linearGradient
305+ inkscape:collect="always"
306+ xlink:href="#linearGradient4176"
307+ id="linearGradient3818"
308+ gradientUnits="userSpaceOnUse"
309+ x1="0"
310+ y1="970.29498"
311+ x2="144"
312+ y2="970.29498" />
313+ <linearGradient
314+ inkscape:collect="always"
315+ xlink:href="#linearGradient4123"
316+ id="linearGradient3820"
317+ gradientUnits="userSpaceOnUse"
318+ x1="0"
319+ y1="970.29498"
320+ x2="144"
321+ y2="970.29498" />
322+ <linearGradient
323+ inkscape:collect="always"
324+ xlink:href="#linearGradient4123"
325+ id="linearGradient3823"
326+ gradientUnits="userSpaceOnUse"
327+ x1="0"
328+ y1="970.29498"
329+ x2="144"
330+ y2="970.29498"
331+ gradientTransform="matrix(0,-0.66666669,0.6660448,0,-171.9742,714.14791)" />
332+ </defs>
333+ <sodipodi:namedview
334+ id="base"
335+ pagecolor="#ffffff"
336+ bordercolor="#666666"
337+ borderopacity="1.0"
338+ inkscape:pageopacity="0.0"
339+ inkscape:pageshadow="2"
340+ inkscape:zoom="2.0861626"
341+ inkscape:cx="-12.487669"
342+ inkscape:cy="-1.1120668"
343+ inkscape:document-units="px"
344+ inkscape:current-layer="layer1"
345+ showgrid="false"
346+ fit-margin-top="0"
347+ fit-margin-left="0"
348+ fit-margin-right="0"
349+ fit-margin-bottom="0"
350+ inkscape:window-width="1440"
351+ inkscape:window-height="876"
352+ inkscape:window-x="0"
353+ inkscape:window-y="24"
354+ inkscape:window-maximized="1" />
355+ <metadata
356+ id="metadata6692">
357+ <rdf:RDF>
358+ <cc:Work
359+ rdf:about="">
360+ <dc:format>image/svg+xml</dc:format>
361+ <dc:type
362+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
363+ <dc:title />
364+ </cc:Work>
365+ </rdf:RDF>
366+ </metadata>
367+ <g
368+ inkscape:label="Layer 1"
369+ inkscape:groupmode="layer"
370+ id="layer1"
371+ transform="translate(-426.28572,-618.14791)">
372+ <g
373+ id="g3825">
374+ <path
375+ style="fill:url(#linearGradient3823);fill-opacity:1.0;fill-rule:nonzero;stroke:none;display:inline"
376+ d="m 426.28572,683.01277 0,-33.72973 c 0,-27.24324 3.88785,-31.13513 31.10302,-31.13513 l 33.79408,0 c 27.21507,0 31.1029,3.89189 31.1029,31.13513 l 0,33.72973 c 0,27.24325 -3.88783,31.13514 -31.1029,31.13514 l -33.79408,0 c -27.21517,0 -31.10302,-3.89189 -31.10302,-31.13514 z"
377+ id="path3107-2"
378+ inkscape:connector-curvature="0"
379+ sodipodi:nodetypes="sssssssss" />
380+ <path
381+ transform="matrix(0.96000003,0,0,0.96000003,-207.3143,60.387891)"
382+ inkscape:connector-curvature="0"
383+ clip-path="url(#clipPath4046-4)"
384+ id="rect4038-0"
385+ d="m 625.15625,551.28125 0,159.75 167.40625,0 0,-159.75 -167.40625,0 z m 67.25,31.71875 35.1875,0 C 755.94278,583 760,587.05912 760,615.4375 l 0,35.125 C 760,678.94088 755.94278,683 727.59375,683 l -35.1875,0 C 664.05712,683 660,678.94088 660,650.5625 l 0,-35.125 C 660,587.05912 664.05712,583 692.40625,583 z"
386+ style="opacity:0.6;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter4064-5);enable-background:accumulate" />
387+ </g>
388+ <path
389+ sodipodi:type="star"
390+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.47999999;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
391+ id="path3835"
392+ sodipodi:sides="5"
393+ sodipodi:cx="36.909874"
394+ sodipodi:cy="32.246578"
395+ sodipodi:r1="34.566349"
396+ sodipodi:r2="17.283173"
397+ sodipodi:arg1="0.98279372"
398+ sodipodi:arg2="1.6111123"
399+ inkscape:flatsided="false"
400+ inkscape:rounded="0"
401+ inkscape:randomized="0"
402+ d="M 56.083835,61.007519 36.213275,49.515707 15.481673,59.369718 20.270696,36.920527 4.4925571,20.24866 27.322895,17.866109 38.303071,-2.291683 47.623974,18.68501 70.188234,22.898677 53.11853,38.245538 z"
403+ transform="translate(436.94532,636.78999)"
404+ inkscape:transform-center-x="-0.43052153"
405+ inkscape:transform-center-y="-2.8886602" />
406+ </g>
407+</svg>
408
409=== added file 'tests/charms/broken-config/metadata.yaml'
410--- tests/charms/broken-config/metadata.yaml 1970-01-01 00:00:00 +0000
411+++ tests/charms/broken-config/metadata.yaml 2013-09-17 15:05:38 +0000
412@@ -0,0 +1,10 @@
413+name: broken-config
414+summary: <Fill in summary here>
415+maintainer: test@testhost
416+categories:
417+ - misc
418+description: |
419+ <Multi-line description here>
420+provides:
421+ relation-a:
422+ interface: foo
423
424=== added file 'tests/charms/broken-config/revision'
425--- tests/charms/broken-config/revision 1970-01-01 00:00:00 +0000
426+++ tests/charms/broken-config/revision 2013-09-17 15:05:38 +0000
427@@ -0,0 +1,1 @@
428+1
429
430=== added file 'tests/charms/broken-maintainer/config.yaml'
431--- tests/charms/broken-maintainer/config.yaml 1970-01-01 00:00:00 +0000
432+++ tests/charms/broken-maintainer/config.yaml 2013-09-17 15:05:38 +0000
433@@ -0,0 +1,5 @@
434+options:
435+ foo:
436+ type: int
437+ description: bar
438+ default: 1
439
440=== added file 'tests/charms/broken-subordinate/config.yaml'
441--- tests/charms/broken-subordinate/config.yaml 1970-01-01 00:00:00 +0000
442+++ tests/charms/broken-subordinate/config.yaml 2013-09-17 15:05:38 +0000
443@@ -0,0 +1,5 @@
444+options:
445+ foo:
446+ type: int
447+ description: bar
448+ default: 1
449
450=== added file 'tests/charms/broken-subordinate2/config.yaml'
451--- tests/charms/broken-subordinate2/config.yaml 1970-01-01 00:00:00 +0000
452+++ tests/charms/broken-subordinate2/config.yaml 2013-09-17 15:05:38 +0000
453@@ -0,0 +1,5 @@
454+options:
455+ foo:
456+ type: int
457+ description: bar
458+ default: 1
459
460=== added file 'tests/charms/empty-requires/config.yaml'
461--- tests/charms/empty-requires/config.yaml 1970-01-01 00:00:00 +0000
462+++ tests/charms/empty-requires/config.yaml 2013-09-17 15:05:38 +0000
463@@ -0,0 +1,5 @@
464+options:
465+ foo:
466+ type: int
467+ description: bar
468+ default: 1
469
470=== added file 'tests/charms/icon-template/config.yaml'
471--- tests/charms/icon-template/config.yaml 1970-01-01 00:00:00 +0000
472+++ tests/charms/icon-template/config.yaml 2013-09-17 15:05:38 +0000
473@@ -0,0 +1,5 @@
474+options:
475+ foo:
476+ type: int
477+ description: bar
478+ default: 1
479
480=== added file 'tests/charms/missing-maintainer/config.yaml'
481--- tests/charms/missing-maintainer/config.yaml 1970-01-01 00:00:00 +0000
482+++ tests/charms/missing-maintainer/config.yaml 2013-09-17 15:05:38 +0000
483@@ -0,0 +1,5 @@
484+options:
485+ foo:
486+ type: int
487+ description: bar
488+ default: 1
489
490=== added file 'tests/charms/mod-spdy/config.yaml'
491--- tests/charms/mod-spdy/config.yaml 1970-01-01 00:00:00 +0000
492+++ tests/charms/mod-spdy/config.yaml 2013-09-17 15:05:38 +0000
493@@ -0,0 +1,5 @@
494+options:
495+ foo:
496+ type: int
497+ description: bar
498+ default: 1
499
500=== added file 'tests/charms/test/config.yaml'
501--- tests/charms/test/config.yaml 1970-01-01 00:00:00 +0000
502+++ tests/charms/test/config.yaml 2013-09-17 15:05:38 +0000
503@@ -0,0 +1,5 @@
504+options:
505+ foo:
506+ type: int
507+ description: bar
508+ default: 1
509
510=== added file 'tests/charms/unknown-metadata/config.yaml'
511--- tests/charms/unknown-metadata/config.yaml 1970-01-01 00:00:00 +0000
512+++ tests/charms/unknown-metadata/config.yaml 2013-09-17 15:05:38 +0000
513@@ -0,0 +1,5 @@
514+options:
515+ foo:
516+ type: int
517+ description: bar
518+ default: 1
519
520=== added file 'tests/proof/expected/broken-config'
521--- tests/proof/expected/broken-config 1970-01-01 00:00:00 +0000
522+++ tests/proof/expected/broken-config 2013-09-17 15:05:38 +0000
523@@ -0,0 +1,1 @@
524+W: config.yaml: option foo has an invalid type (integer)
525
526=== added file 'tests/proof/test_proof.py'
527--- tests/proof/test_proof.py 1970-01-01 00:00:00 +0000
528+++ tests/proof/test_proof.py 2013-09-17 15:05:38 +0000
529@@ -0,0 +1,246 @@
530+#!/usr/bin/python
531+
532+# Copyright (C) 2013 Canonical Ltd.
533+#
534+# This program is free software: you can redistribute it and/or modify
535+# it under the terms of the GNU General Public License as published by
536+# the Free Software Foundation, either version 3 of the License, or
537+# (at your option) any later version.
538+#
539+# This program is distributed in the hope that it will be useful,
540+# but WITHOUT ANY WARRANTY; without even the implied warranty of
541+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
542+# GNU General Public License for more details.
543+#
544+# You should have received a copy of the GNU General Public License
545+# along with this program. If not, see <http://www.gnu.org/licenses/>.
546+
547+from os.path import abspath, dirname, join
548+from shutil import rmtree
549+import sys
550+from tempfile import mkdtemp
551+from textwrap import dedent
552+from unittest import main, TestCase
553+
554+proof_path = dirname(dirname(dirname(abspath(__file__))))
555+proof_path = join(proof_path, 'scripts')
556+sys.path.append(proof_path)
557+from lib.proof import Linter
558+
559+
560+class TestProof(TestCase):
561+
562+ def setUp(self):
563+ self.charm_dir = mkdtemp()
564+ self.config_path = join(self.charm_dir, 'config.yaml')
565+ self.linter = Linter()
566+
567+ def tearDown(self):
568+ rmtree(self.charm_dir)
569+
570+ def write_config(self, text):
571+ with open(join(self.charm_dir, 'config.yaml'), 'w') as f:
572+ f.write(dedent(text))
573+
574+ def test_config_yaml_missing(self):
575+ self.linter.check_config_file(self.charm_dir)
576+ self.assertEqual(
577+ ['W: File config.yaml not found.'], self.linter.lint)
578+
579+ def test_clean_config(self):
580+ self.write_config("""
581+ options:
582+ string_opt:
583+ type: str
584+ description: A string option
585+ default: some text
586+ int_opt:
587+ type: int
588+ description: An int option
589+ default: 2
590+ float_opt:
591+ type: float
592+ default: 4.2
593+ description: This is a float option.
594+ bool_opt:
595+ type: boolean
596+ default: True
597+ description: This is a boolean option.
598+ """)
599+ self.linter.check_config_file(self.charm_dir)
600+ self.assertEqual([], self.linter.lint)
601+
602+ def test_config_with_invalid_yaml(self):
603+ self.write_config("""
604+ options:
605+ foo: 42
606+ bar
607+ """)
608+ self.linter.check_config_file(self.charm_dir)
609+ self.assertEqual(1, len(self.linter.lint))
610+ message = self.linter.lint[0]
611+ self.assertTrue(message.startswith(
612+ 'E: Cannot parse config.yaml: while scanning a simple key'),
613+ 'wrong lint message: %s' % message)
614+
615+ def test_config_no_root_dict(self):
616+ self.write_config("""
617+ this is not a dictionary
618+ """)
619+ self.linter.check_config_file(self.charm_dir)
620+ self.assertEqual(1, len(self.linter.lint))
621+ self.assertEqual(
622+ 'E: config.yaml not parsed into a dictionary.',
623+ self.linter.lint[0])
624+
625+ def test_options_key_missing(self):
626+ self.write_config("""
627+ foo: bar
628+ """)
629+ self.linter.check_config_file(self.charm_dir)
630+ self.assertEqual(1, len(self.linter.lint))
631+ self.assertEqual(
632+ 'E: config.yaml must have an "options" key.',
633+ self.linter.lint[0])
634+
635+ def test_ignored_root_keys(self):
636+ self.write_config("""
637+ options:
638+ string_opt:
639+ type: str
640+ description: whatever
641+ default: blah
642+ noise: The art of - in visible silence
643+ """)
644+ self.linter.check_config_file(self.charm_dir)
645+ self.assertEqual(1, len(self.linter.lint))
646+ self.assertEqual(
647+ "W: Ignored keys in config.yaml: ['noise']",
648+ self.linter.lint[0])
649+
650+ def test_options_is_not_dict(self):
651+ self.write_config("""
652+ options: a string instead of a dict
653+ """)
654+ self.linter.check_config_file(self.charm_dir)
655+ self.assertEqual(1, len(self.linter.lint))
656+ self.assertEqual(
657+ 'E: config.yaml: options section is not parsed as a dictionary',
658+ self.linter.lint[0])
659+
660+ def test_option_data_not_a_dict(self):
661+ self.write_config("""
662+ options:
663+ foo: just a string
664+ """)
665+ self.linter.check_config_file(self.charm_dir)
666+ self.assertEqual(1, len(self.linter.lint))
667+ self.assertEqual(
668+ 'E: config.yaml: data for option foo is not a dict',
669+ self.linter.lint[0])
670+
671+ def test_option_data_with_subset_of_allowed_keys(self):
672+ self.write_config("""
673+ options:
674+ foo:
675+ type: int
676+ description: whatever
677+ """)
678+ self.linter.check_config_file(self.charm_dir)
679+ self.assertEqual(1, len(self.linter.lint))
680+ expected = (
681+ 'W: config.yaml: option foo does not have the optional keys: '
682+ 'default')
683+ self.assertEqual(expected, self.linter.lint[0])
684+
685+ def test_option_data_misses_required_key(self):
686+ self.write_config("""
687+ options:
688+ foo:
689+ type: int
690+ default: 3
691+ """)
692+ self.linter.check_config_file(self.charm_dir)
693+ self.assertEqual(1, len(self.linter.lint))
694+ expected = (
695+ 'E: config.yaml: option foo does not have the required keys: '
696+ 'description')
697+ self.assertEqual(expected, self.linter.lint[0])
698+
699+ def test_option_data_with_unknown_key(self):
700+ self.write_config("""
701+ options:
702+ foo:
703+ type: int
704+ default: 3
705+ description: whatever
706+ something: completely different
707+ 42: the answer
708+ """)
709+ self.linter.check_config_file(self.charm_dir)
710+ self.assertEqual(1, len(self.linter.lint))
711+ expected = (
712+ 'W: config.yaml: option foo as unknown keys: 42, something')
713+ self.assertEqual(expected, self.linter.lint[0])
714+
715+ def test_option_data_with_invalid_descr_type(self):
716+ self.write_config("""
717+ options:
718+ foo:
719+ type: int
720+ default: 3
721+ description: 1
722+ """)
723+ self.linter.check_config_file(self.charm_dir)
724+ self.assertEqual(1, len(self.linter.lint))
725+ expected = (
726+ 'W: config.yaml: description of option foo should be a string')
727+ self.assertEqual(expected, self.linter.lint[0])
728+
729+ def test_option_data_with_invalid_option_type(self):
730+ self.write_config("""
731+ options:
732+ foo:
733+ type: strr
734+ default: foo
735+ description: blah
736+ """)
737+ self.linter.check_config_file(self.charm_dir)
738+ self.assertEqual(1, len(self.linter.lint))
739+ expected = (
740+ 'W: config.yaml: option foo has an invalid type (strr)')
741+ self.assertEqual(expected, self.linter.lint[0])
742+
743+ def test_option_type_str_conflict_with_default_value(self):
744+ self.write_config("""
745+ options:
746+ foo:
747+ type: str
748+ default: 17
749+ description: blah
750+ """)
751+ self.linter.check_config_file(self.charm_dir)
752+ self.assertEqual(1, len(self.linter.lint))
753+ expected = (
754+ 'E: config.yaml: type of option foo is specified as str, but '
755+ 'the type of the default value is int')
756+ self.assertEqual(expected, self.linter.lint[0])
757+
758+ def test_option_type_int_conflict_with_default_value(self):
759+ self.write_config("""
760+ options:
761+ foo:
762+ type: int
763+ default: foo
764+ description: blah
765+ """)
766+ self.linter.check_config_file(self.charm_dir)
767+ self.assertEqual(1, len(self.linter.lint))
768+ expected = (
769+ 'E: config.yaml: type of option foo is specified as int, but '
770+ 'the type of the default value is str')
771+ self.assertEqual(expected, self.linter.lint[0])
772+
773+
774+if __name__ == '__main__':
775+ main()

Subscribers

People subscribed via source and target branches