diff options
author | Richard Maw <richard.maw@codethink.co.uk> | 2014-01-07 16:47:54 +0000 |
---|---|---|
committer | Richard Maw <richard.maw@codethink.co.uk> | 2014-01-16 16:58:24 +0000 |
commit | 7fb78e54f4c7206ff391fd4edb0ece200109f1e7 (patch) | |
tree | c7746afa751caab8866efeddad188bd3d1111f34 | |
parent | bea188ba004eb1b8d3057a5cfa2c1a167ef72d14 (diff) | |
download | morph-7fb78e54f4c7206ff391fd4edb0ece200109f1e7.tar.gz |
MorphologyLoader: Validate new fields
-rw-r--r-- | morphlib/morphloader.py | 116 | ||||
-rw-r--r-- | morphlib/morphloader_tests.py | 94 |
2 files changed, 204 insertions, 6 deletions
diff --git a/morphlib/morphloader.py b/morphlib/morphloader.py index e1ec15bd..637544be 100644 --- a/morphlib/morphloader.py +++ b/morphlib/morphloader.py @@ -44,16 +44,33 @@ class UnknownKindError(morphlib.Error): class MissingFieldError(morphlib.Error): - def __init__(self, field, morphology): + def __init__(self, field, morphology_name): + self.field = field + self.morphology_name = morphology_name self.msg = ( - 'Missing field %s from morphology %s' % (field, morphology)) + 'Missing field %s from morphology %s' % (field, morphology_name)) class InvalidFieldError(morphlib.Error): - def __init__(self, field, morphology): + def __init__(self, field, morphology_name): + self.field = field + self.morphology_name = morphology_name self.msg = ( - 'Field %s not allowed in morphology %s' % (field, morphology)) + 'Field %s not allowed in morphology %s' % (field, morphology_name)) + + +class InvalidTypeError(morphlib.Error): + + def __init__(self, field, expected, actual, morphology_name): + self.field = field + self.expected = expected + self.actual = actual + self.morphology_name = morphology_name + self.msg = ( + 'Field %s expected type %s, got %s in morphology %s' % + (field, expected, actual, morphology_name)) + class ObsoleteFieldsError(morphlib.Error): @@ -140,6 +157,16 @@ class EmptySystemError(morphlib.Error): self, 'System %(system_name)s has no strata.' % locals()) +class MultipleValidationErrors(morphlib.Error): + + def __init__(self, name, errors): + self.name = name + self.errors = errors + self.msg = 'Multiple errors when validating %(name)s:' + for error in errors: + self.msg += ('\t' + str(error)) + + class MorphologyLoader(object): '''Load morphologies from disk, or save them back to disk.''' @@ -193,6 +220,7 @@ class MorphologyLoader(object): 'chunks': [], 'description': '', 'build-depends': [], + 'products': [], }, 'system': { 'description': '', @@ -356,8 +384,84 @@ class MorphologyLoader(object): spec.get('alias', spec['name']), morph.filename) - def _validate_chunk(self, morph): - pass + @classmethod + def _validate_chunk(cls, morphology): + errors = [] + + if 'products' in morphology: + cls._validate_products(morphology['name'], + morphology['products'], errors) + + if len(errors) == 1: + raise errors[0] + elif errors: + raise MultipleValidationErrors(morphology['name'], errors) + + @classmethod + def _validate_products(cls, morphology_name, products, errors): + '''Validate the products field is of the correct type.''' + if (not isinstance(products, collections.Iterable) + or isinstance(products, collections.Mapping)): + raise InvalidTypeError('products', list, + type(products), morphology_name) + + for spec_index, spec in enumerate(products): + + if not isinstance(spec, collections.Mapping): + e = InvalidTypeError('products[%d]' % spec_index, + dict, type(spec), morphology_name) + errors.append(e) + continue + + cls._validate_products_spec_fields_exist(morphology_name, + spec_index, spec, errors) + + if 'include' in spec: + cls._validate_products_specs_include( + morphology_name, spec_index, spec['include'], errors) + + product_spec_required_fields = ('artifact', 'include') + @classmethod + def _validate_products_spec_fields_exist( + cls, morphology_name, spec_index, spec, errors): + + given_fields = sorted(spec.iterkeys()) + missing = (field for field in cls.product_spec_required_fields + if field not in given_fields) + for field in missing: + e = MissingFieldError('products[%d].%s' % (spec_index, field), + morphology_name) + errors.append(e) + unexpected = (field for field in given_fields + if field not in cls.product_spec_required_fields) + for field in unexpected: + e = InvalidFieldError('products[%d].%s' % (spec_index, field), + morphology_name) + errors.append(e) + + @classmethod + def _validate_products_specs_include(cls, morphology_name, spec_index, + include_patterns, errors): + '''Validate that products' include field is a list of strings.''' + # Allow include to be most iterables, but not a mapping + # or a string, since iter of a mapping is just the keys, + # and the iter of a string is a 1 character length string, + # which would also validate as an iterable of strings. + if (not isinstance(include_patterns, collections.Iterable) + or isinstance(include_patterns, collections.Mapping) + or isinstance(include_patterns, basestring)): + + e = InvalidTypeError('products[%d].include' % spec_index, list, + type(include_patterns), morphology_name) + errors.append(e) + else: + for pattern_index, pattern in enumerate(include_patterns): + pattern_path = ('products[%d].include[%d]' % + (spec_index, pattern_index)) + if not isinstance(pattern, basestring): + e = InvalidTypeError(pattern_path, str, + type(pattern), morphology_name) + errors.append(e) def _require_field(self, field, morphology): if field not in morphology: diff --git a/morphlib/morphloader_tests.py b/morphlib/morphloader_tests.py index 907f3762..c2fbc5e8 100644 --- a/morphlib/morphloader_tests.py +++ b/morphlib/morphloader_tests.py @@ -78,6 +78,99 @@ build-system: dummy self.assertRaises( morphlib.morphloader.InvalidFieldError, self.loader.validate, m) + def test_validate_requires_products_list(self): + m = morphlib.morph3.Morphology( + kind='chunk', + name='foo', + products={ + 'foo-runtime': ['.'], + 'foo-devel': ['.'], + }) + with self.assertRaises(morphlib.morphloader.InvalidTypeError) as cm: + self.loader.validate(m) + e = cm.exception + self.assertEqual(e.field, 'products') + self.assertEqual(e.expected, list) + self.assertEqual(e.actual, dict) + self.assertEqual(e.morphology_name, 'foo') + + def test_validate_requires_products_list_of_mappings(self): + m = morphlib.morph3.Morphology( + kind='chunk', + name='foo', + products=[ + 'foo-runtime', + ]) + with self.assertRaises(morphlib.morphloader.InvalidTypeError) as cm: + self.loader.validate(m) + e = cm.exception + self.assertEqual(e.field, 'products[0]') + self.assertEqual(e.expected, dict) + self.assertEqual(e.actual, str) + self.assertEqual(e.morphology_name, 'foo') + + def test_validate_requires_products_list_required_fields(self): + m = morphlib.morph3.Morphology( + kind='chunk', + name='foo', + products=[ + { + 'factiart': 'foo-runtime', + 'cludein': [], + } + ]) + with self.assertRaises(morphlib.morphloader.MultipleValidationErrors) \ + as cm: + self.loader.validate(m) + exs = cm.exception.errors + self.assertEqual(type(exs[0]), morphlib.morphloader.MissingFieldError) + self.assertEqual(exs[0].field, 'products[0].artifact') + self.assertEqual(type(exs[1]), morphlib.morphloader.MissingFieldError) + self.assertEqual(exs[1].field, 'products[0].include') + self.assertEqual(type(exs[2]), morphlib.morphloader.InvalidFieldError) + self.assertEqual(exs[2].field, 'products[0].cludein') + self.assertEqual(type(exs[3]), morphlib.morphloader.InvalidFieldError) + self.assertEqual(exs[3].field, 'products[0].factiart') + + def test_validate_requires_products_list_include_is_list(self): + m = morphlib.morph3.Morphology( + kind='chunk', + name='foo', + products=[ + { + 'artifact': 'foo-runtime', + 'include': '.*', + } + ]) + with self.assertRaises(morphlib.morphloader.InvalidTypeError) as cm: + self.loader.validate(m) + ex = cm.exception + self.assertEqual(ex.field, 'products[0].include') + self.assertEqual(ex.expected, list) + self.assertEqual(ex.actual, str) + self.assertEqual(ex.morphology_name, 'foo') + + def test_validate_requires_products_list_include_is_list_of_strings(self): + m = morphlib.morph3.Morphology( + kind='chunk', + name='foo', + products=[ + { + 'artifact': 'foo-runtime', + 'include': [ + 123, + ] + } + ]) + with self.assertRaises(morphlib.morphloader.InvalidTypeError) as cm: + self.loader.validate(m) + ex = cm.exception + self.assertEqual(ex.field, 'products[0].include[0]') + self.assertEqual(ex.expected, str) + self.assertEqual(ex.actual, int) + self.assertEqual(ex.morphology_name, 'foo') + + def test_fails_to_validate_stratum_with_no_fields(self): m = morphlib.morph3.Morphology({ 'kind': 'stratum', @@ -491,6 +584,7 @@ name: foo 'build-depends': [], }, ], + 'products': [], }) def test_unsets_defaults_for_strata(self): |