summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <lars.wirzenius@codethink.co.uk>2013-08-07 13:53:36 +0000
committerLars Wirzenius <lars.wirzenius@codethink.co.uk>2013-08-14 16:32:04 +0000
commit4bea99229dfa1ee7bffeefc4d194c34eb4752509 (patch)
treed892c903716268bc555a7a27b8cd4924c4b5297a
parent5d95df761c8335711f27e9ab062c0a20ddd332b1 (diff)
downloaddefinitions-4bea99229dfa1ee7bffeefc4d194c34eb4752509.tar.gz
Add new morphology abstraction and morphology loading/saving
The old code is somewhat weird. The new code is meant to be cleaner and more straightforward to understand and to use. For example, the old code has setting of defaults in both the Morphology and MorphologyFactory classes. The new code has a minimally simple Morphology class, and has all the logic to validate and set defaults in the MorphologyLoader class. Further, the new code makes it possible to load an invalid morphology, which will be useful later.
-rw-r--r--morphlib/__init__.py2
-rw-r--r--morphlib/morph3.py45
-rw-r--r--morphlib/morph3_tests.py48
-rw-r--r--morphlib/morphloader.py343
-rw-r--r--morphlib/morphloader_tests.py474
5 files changed, 912 insertions, 0 deletions
diff --git a/morphlib/__init__.py b/morphlib/__init__.py
index 544dcd09..b9b9924b 100644
--- a/morphlib/__init__.py
+++ b/morphlib/__init__.py
@@ -65,6 +65,8 @@ import localrepocache
import mountableimage
import morph2
import morphologyfactory
+import morph3
+import morphloader
import remoteartifactcache
import remoterepocache
import repoaliasresolver
diff --git a/morphlib/morph3.py b/morphlib/morph3.py
new file mode 100644
index 00000000..477cac1a
--- /dev/null
+++ b/morphlib/morph3.py
@@ -0,0 +1,45 @@
+# Copyright (C) 2013 Codethink Limited
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# =*= License: GPL-2 =*=
+
+
+import UserDict
+
+
+class Morphology(UserDict.IterableUserDict):
+
+ '''A container for a morphology, plus its metadata.
+
+ A morphology is, basically, a dict. This class acts as that dict,
+ plus stores additional metadata about the morphology, such as where
+ it came from, and the ref that was used for it. It also has a dirty
+ attribute, to indicate whether the morphology has had changes done
+ to it, but does not itself set that attribute: the caller has to
+ maintain the flag themselves.
+
+ This class does NO validation of the data, nor does it parse the
+ morphology text, or produce a textual form of itself. For those
+ things, see MorphologyLoader.
+
+ '''
+
+ def __init__(self, *args, **kwargs):
+ UserDict.IterableUserDict.__init__(self, *args, **kwargs)
+ self.repo_url = None
+ self.ref = None
+ self.filename = None
+ self.dirty = None
+
diff --git a/morphlib/morph3_tests.py b/morphlib/morph3_tests.py
new file mode 100644
index 00000000..e150bf33
--- /dev/null
+++ b/morphlib/morph3_tests.py
@@ -0,0 +1,48 @@
+# Copyright (C) 2013 Codethink Limited
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# =*= License: GPL-2 =*=
+
+
+import unittest
+
+import morphlib
+
+
+class MorphologyTests(unittest.TestCase):
+
+ def setUp(self):
+ self.morph = morphlib.morph3.Morphology()
+
+ def test_has_repo_url_attribute(self):
+ self.assertEqual(self.morph.repo_url, None)
+ self.morph.repo_url = 'foo'
+ self.assertEqual(self.morph.repo_url, 'foo')
+
+ def test_has_ref_attribute(self):
+ self.assertEqual(self.morph.ref, None)
+ self.morph.ref = 'foo'
+ self.assertEqual(self.morph.ref, 'foo')
+
+ def test_has_filename_attribute(self):
+ self.assertEqual(self.morph.filename, None)
+ self.morph.filename = 'foo'
+ self.assertEqual(self.morph.filename, 'foo')
+
+ def test_has_dirty_attribute(self):
+ self.assertEqual(self.morph.dirty, None)
+ self.morph.dirty = True
+ self.assertEqual(self.morph.dirty, True)
+
diff --git a/morphlib/morphloader.py b/morphlib/morphloader.py
new file mode 100644
index 00000000..843c6957
--- /dev/null
+++ b/morphlib/morphloader.py
@@ -0,0 +1,343 @@
+# Copyright (C) 2013 Codethink Limited
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# =*= License: GPL-2 =*=
+
+
+import logging
+import StringIO
+import yaml
+
+import morphlib
+
+
+class MorphologySyntaxError(morphlib.Error):
+
+ def __init__(self, morphology):
+ self.msg = 'Syntax error in morphology %s' % morphology
+
+
+class NotADictionaryError(morphlib.Error):
+
+ def __init__(self, morphology):
+ self.msg = 'Not a dictionary: morphology %s' % morphology
+
+
+class UnknownKindError(morphlib.Error):
+
+ def __init__(self, kind, morphology):
+ self.msg = (
+ 'Unknown kind %s in morphology %s' % (kind, morphology))
+
+
+class MissingFieldError(morphlib.Error):
+
+ def __init__(self, field, morphology):
+ self.msg = (
+ 'Missing field %s from morphology %s' % (field, morphology))
+
+
+class InvalidFieldError(morphlib.Error):
+
+ def __init__(self, field, morphology):
+ self.msg = (
+ 'Field %s not allowed in morphology %s' % (field, morphology))
+
+
+class UnknownArchitectureError(morphlib.Error):
+
+ def __init__(self, arch, morphology):
+ self.msg = (
+ 'Unknown architecture %s in morphology %s' % (arch, morphology))
+
+
+class InvalidSystemKindError(morphlib.Error):
+
+ def __init__(self, system_kind, morphology):
+ self.msg = (
+ 'system-kind %s not allowed (must be rootfs-tarball), in %s' %
+ (system_kind, morphology))
+
+
+class NoBuildDependenciesError(morphlib.Error):
+
+ def __init__(self, stratum_name, chunk_name, morphology):
+ self.msg = (
+ 'Stratum %s has no build dependencies for chunk %s in %s' %
+ (stratum_name, chunk_name, morphology))
+
+
+class NoStratumBuildDependenciesError(morphlib.Error):
+
+ def __init__(self, stratum_name, morphology):
+ self.msg = (
+ 'Stratum %s has no build dependencies in %s' %
+ (stratum_name, morphology))
+
+
+class EmptyStratumError(morphlib.Error):
+
+ def __init__(self, stratum_name, morphology):
+ self.msg = (
+ 'Stratum %s has no chunks in %s' %
+ (stratum_name, morphology))
+
+
+class MorphologyLoader(object):
+
+ '''Load morphologies from disk, or save them back to disk.'''
+
+ _required_fields = {
+ 'chunk': [
+ 'name',
+ ],
+ 'stratum': [
+ 'name',
+ ],
+ 'system': [
+ 'name',
+ 'arch',
+ ],
+ }
+
+ _static_defaults = {
+ 'chunk': {
+ 'description': '',
+ 'pre-configure-commands': [],
+ 'configure-commands': [],
+ 'post-configure-commands': [],
+ 'pre-build-commands': [],
+ 'build-commands': [],
+ 'post-build-commands': [],
+ 'pre-test-commands': [],
+ 'test-commands': [],
+ 'post-test-commands': [],
+ 'pre-install-commands': [],
+ 'install-commands': [],
+ 'post-install-commands': [],
+ 'devices': [],
+ 'chunks': [],
+ 'max-jobs': None,
+ 'build-system': 'manual',
+ },
+ 'stratum': {
+ 'chunks': [],
+ 'description': '',
+ 'build-depends': [],
+ },
+ 'system': {
+ 'strata': [],
+ 'description': '',
+ 'arch': None,
+ 'system-kind': 'rootfs-tarball',
+ 'configuration-extensions': [],
+ 'disk-size': '1G',
+ },
+ }
+
+ def parse_from_string(self, string, whence):
+ '''Parse a textual morphology.
+
+ Return the new Morphology object, or raise an error indicating
+ the problem. This method does minimal validation: a syntactically
+ correct morphology is fine, even if none of the fields are
+ valid. It also does not set any default values for any of the
+ fields. See validate and set_defaults.
+
+ whence is where the morphology text came from. It is used
+ in exception error messages.
+
+ '''
+
+ try:
+ obj = yaml.safe_load(StringIO.StringIO(string))
+ except yaml.error.YAMLError as e:
+ logging.error('Could not load morphology as YAML:\n%s' % str(e))
+ raise MorphologySyntaxError(whence)
+
+ if type(obj) != dict:
+ raise NotADictionaryError(whence)
+
+ return morphlib.morph3.Morphology(obj)
+
+ def load_from_string(self, string, filename='string'):
+ '''Load a morphology from a string.
+
+ Return the Morphology object.
+
+ '''
+
+ m = self.parse_from_string(string, filename)
+ m.filename = filename
+ self.validate(m)
+ self.set_defaults(m)
+ return m
+
+ def load_from_file(self, filename):
+ '''Load a morphology from a named file.
+
+ Return the Morphology object.
+
+ '''
+
+ with open(filename) as f:
+ text = f.read()
+ return self.load_from_string(text, filename=filename)
+
+ def save_to_string(self, morphology):
+ '''Return normalised textual form of morphology.'''
+
+ f = StringIO.StringIO()
+ yaml.safe_dump(morphology.data, stream=f, default_flow_style=False)
+ return f.getvalue()
+
+ def save_to_file(self, filename, morphology):
+ '''Save a morphology object to a named file.'''
+
+ text = self.save_to_string(morphology)
+ with morphlib.savefile.SaveFile(filename, 'w') as f:
+ f.write(text)
+
+ def validate(self, morph):
+ '''Validate a morphology.'''
+
+ # Validate that the kind field is there.
+ self._require_field('kind', morph)
+
+ # The rest of the validation is dependent on the kind.
+
+ kind = morph['kind']
+ if kind not in ('system', 'stratum', 'chunk'):
+ raise UnknownKindError(morph['kind'], morph.filename)
+
+ required = ['kind'] + self._required_fields[kind]
+ allowed = self._static_defaults[kind].keys()
+ self._require_fields(required, morph)
+ self._deny_unknown_fields(required + allowed, morph)
+
+ if kind == 'system':
+ self._validate_system(morph)
+ elif kind == 'stratum':
+ self._validate_stratum(morph)
+ else:
+ assert kind == 'chunk'
+ self._validate_chunk(morph)
+
+ def _validate_system(self, morph):
+ # All stratum names should be unique within a system.
+ names = set()
+ for info in morph['strata']:
+ name = info.get('alias', info['morph'])
+ if name in names:
+ raise ValueError('Duplicate stratum "%s"' % name)
+ names.add(name)
+
+ # We allow the ARMv7 little-endian architecture to be specified
+ # as armv7 and armv7l. Normalise.
+ if morph['arch'] == 'armv7':
+ morph['arch'] = 'armv7l'
+
+ # Architecture name must be known.
+ if morph['arch'] not in morphlib.valid_archs:
+ raise UnknownArchitectureError(morph['arch'], morph.filename)
+
+ # If system-kind is present, it must be rootfs-tarball.
+ if 'system-kind' in morph:
+ if morph['system-kind'] not in (None, 'rootfs-tarball'):
+ raise InvalidSystemKindError(
+ morph['system-kind'], morph.filename)
+
+ def _validate_stratum(self, morph):
+ # All chunk names must be unique within a stratum.
+ names = set()
+ for info in morph['chunks']:
+ name = info.get('alias', info['name'])
+ if name in names:
+ raise ValueError('Duplicate chunk "%s"' % name)
+ names.add(name)
+
+ # Require build-dependencies for the stratum itself, unless
+ # it has chunks built in bootstrap mode.
+ if 'build-depends' not in morph:
+ for spec in morph['chunks']:
+ if spec.get('build-mode') == 'bootstrap':
+ break
+ else:
+ raise NoStratumBuildDependenciesError(
+ morph['name'], morph.filename)
+
+ # Require build-dependencies for each chunk.
+ for spec in morph['chunks']:
+ if 'build-depends' not in spec:
+ raise NoBuildDependenciesError(
+ morph['name'],
+ spec.get('alias', spec['name']),
+ morph.filename)
+
+ # Require at least one chunk.
+ if len(morph.get('chunks', [])) == 0:
+ raise EmptyStratumError(morph['name'], morph.filename)
+
+ def _validate_chunk(self, morph):
+ pass
+
+ def _require_field(self, field, morphology):
+ if field not in morphology:
+ raise MissingFieldError(field, morphology.filename)
+
+ def _require_fields(self, fields, morphology):
+ for field in fields:
+ self._require_field(field, morphology)
+
+ def _deny_unknown_fields(self, allowed, morphology):
+ for field in morphology:
+ if field not in allowed:
+ raise InvalidFieldError(field, morphology.filename)
+
+ def set_defaults(self, morphology):
+ '''Set all missing fields in the morpholoy to their defaults.
+
+ The morphology is assumed to be valid.
+
+ '''
+
+ kind = morphology['kind']
+ defaults = self._static_defaults[kind]
+ for key in defaults:
+ if key not in morphology:
+ morphology[key] = defaults[key]
+
+ if kind == 'system':
+ self._set_system_defaults(morphology)
+ elif kind == 'stratum':
+ self._set_stratum_defaults(morphology)
+ else:
+ assert kind == 'chunk'
+ self._set_chunk_defaults(morphology)
+
+ def _set_system_defaults(self, morph):
+ pass
+
+ def _set_stratum_defaults(self, morph):
+ for spec in morph['chunks']:
+ if 'repo' not in spec:
+ spec['repo'] = spec['name']
+ if 'morph' not in spec:
+ spec['morph'] = spec['name']
+
+ def _set_chunk_defaults(self, morph):
+ if morph['max-jobs'] is not None:
+ morph['max-jobs'] = int(morph['max-jobs'])
+
diff --git a/morphlib/morphloader_tests.py b/morphlib/morphloader_tests.py
new file mode 100644
index 00000000..7a1b2c0c
--- /dev/null
+++ b/morphlib/morphloader_tests.py
@@ -0,0 +1,474 @@
+# Copyright (C) 2013 Codethink Limited
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# =*= License: GPL-2 =*=
+
+
+import os
+import shutil
+import tempfile
+import unittest
+
+import morphlib
+
+
+class MorphologyLoaderTests(unittest.TestCase):
+
+ def setUp(self):
+ self.loader = morphlib.morphloader.MorphologyLoader()
+ self.tempdir = tempfile.mkdtemp()
+ self.filename = os.path.join(self.tempdir, 'foo.morph')
+
+ def tearDown(self):
+ shutil.rmtree(self.tempdir)
+
+ def test_parses_yaml_from_string(self):
+ string = '''\
+name: foo
+kind: chunk
+build-system: dummy
+'''
+ morph = self.loader.parse_from_string(string, 'test')
+ self.assertEqual(morph['kind'], 'chunk')
+ self.assertEqual(morph['name'], 'foo')
+ self.assertEqual(morph['build-system'], 'dummy')
+
+ def test_fails_to_parse_utter_garbage(self):
+ self.assertRaises(
+ morphlib.morphloader.MorphologySyntaxError,
+ self.loader.parse_from_string, ',,,', 'test')
+
+ def test_fails_to_parse_non_dict(self):
+ self.assertRaises(
+ morphlib.morphloader.NotADictionaryError,
+ self.loader.parse_from_string, '- item1\n- item2\n', 'test')
+
+ def test_fails_to_validate_dict_without_kind(self):
+ m = morphlib.morph3.Morphology({
+ 'invalid': 'field',
+ })
+ self.assertRaises(
+ morphlib.morphloader.MissingFieldError, self.loader.validate, m)
+
+ def test_fails_to_validate_chunk_with_no_fields(self):
+ m = morphlib.morph3.Morphology({
+ 'kind': 'chunk',
+ })
+ self.assertRaises(
+ morphlib.morphloader.MissingFieldError, self.loader.validate, m)
+
+ def test_fails_to_validate_chunk_with_invalid_field(self):
+ m = morphlib.morph3.Morphology({
+ 'kind': 'chunk',
+ 'name': 'foo',
+ 'invalid': 'field',
+ })
+ self.assertRaises(
+ morphlib.morphloader.InvalidFieldError, self.loader.validate, m)
+
+ def test_fails_to_validate_stratum_with_no_fields(self):
+ m = morphlib.morph3.Morphology({
+ 'kind': 'stratum',
+ })
+ self.assertRaises(
+ morphlib.morphloader.MissingFieldError, self.loader.validate, m)
+
+ def test_fails_to_validate_stratum_with_invalid_field(self):
+ m = morphlib.morph3.Morphology({
+ 'kind': 'stratum',
+ 'name': 'foo',
+ 'invalid': 'field',
+ })
+ self.assertRaises(
+ morphlib.morphloader.InvalidFieldError, self.loader.validate, m)
+
+ def test_fails_to_validate_system_with_no_fields(self):
+ m = morphlib.morph3.Morphology({
+ 'kind': 'system',
+ })
+ self.assertRaises(
+ morphlib.morphloader.MissingFieldError, self.loader.validate, m)
+
+ def test_fails_to_validate_system_with_invalid_field(self):
+ m = morphlib.morph3.Morphology({
+ 'kind': 'system',
+ 'name': 'name',
+ 'arch': 'x86_64',
+ 'invalid': 'field',
+ })
+ self.assertRaises(
+ morphlib.morphloader.InvalidFieldError, self.loader.validate, m)
+
+ def test_fails_to_validate_morphology_with_unknown_kind(self):
+ m = morphlib.morph3.Morphology({
+ 'kind': 'invalid',
+ })
+ self.assertRaises(
+ morphlib.morphloader.UnknownKindError, self.loader.validate, m)
+
+ def test_validate_requires_unique_stratum_names_within_a_system(self):
+ m = morphlib.morph3.Morphology(
+ {
+ "kind": "system",
+ "name": "foo",
+ "arch": "x86-64",
+ "strata": [
+ {
+ "morph": "stratum",
+ "repo": "test1",
+ "ref": "ref"
+ },
+ {
+ "morph": "stratum",
+ "repo": "test2",
+ "ref": "ref"
+ }
+ ]
+ })
+ self.assertRaises(ValueError, self.loader.validate, m)
+
+ def test_validate_requires_unique_chunk_names_within_a_stratum(self):
+ m = morphlib.morph3.Morphology(
+ {
+ "kind": "stratum",
+ "name": "foo",
+ "chunks": [
+ {
+ "name": "chunk",
+ "repo": "test1",
+ "ref": "ref"
+ },
+ {
+ "name": "chunk",
+ "repo": "test2",
+ "ref": "ref"
+ }
+ ]
+ })
+ self.assertRaises(ValueError, self.loader.validate, m)
+
+ def test_validate_requires_a_valid_architecture(self):
+ m = morphlib.morph3.Morphology(
+ {
+ "kind": "system",
+ "name": "foo",
+ "arch": "blah",
+ "strata": [],
+ })
+ self.assertRaises(
+ morphlib.morphloader.UnknownArchitectureError,
+ self.loader.validate, m)
+
+ def test_validate_normalises_architecture_armv7_to_armv7l(self):
+ m = morphlib.morph3.Morphology(
+ {
+ "kind": "system",
+ "name": "foo",
+ "arch": "armv7",
+ "strata": [],
+ })
+ self.loader.validate(m)
+ self.assertEqual(m['arch'], 'armv7l')
+
+ def test_validate_requires_system_kind_to_be_tarball_if_present(self):
+ m = morphlib.morph3.Morphology(
+ {
+ "kind": "system",
+ "name": "foo",
+ "arch": "armv7l",
+ "strata": [],
+ "system-kind": "blah",
+ })
+
+ self.assertRaises(
+ morphlib.morphloader.InvalidSystemKindError,
+ self.loader.validate, m)
+ m['system-kind'] = 'rootfs-tarball'
+ self.loader.validate(m)
+
+ def test_validate_requires_build_deps_for_chunks_in_strata(self):
+ m = morphlib.morph3.Morphology(
+ {
+ "kind": "stratum",
+ "name": "foo",
+ "chunks": [
+ {
+ "name": "foo",
+ "repo": "foo",
+ "ref": "foo",
+ "morph": "foo",
+ "build-mode": "bootstrap",
+ }
+ ],
+ })
+
+ self.assertRaises(
+ morphlib.morphloader.NoBuildDependenciesError,
+ self.loader.validate, m)
+
+ def test_validate_requires_build_deps_or_bootstrap_mode_for_strata(self):
+ m = morphlib.morph3.Morphology(
+ {
+ "name": "stratum-no-bdeps-no-bootstrap",
+ "kind": "stratum",
+ "chunks": [
+ {
+ "name": "chunk",
+ "repo": "test:repo",
+ "ref": "sha1",
+ "build-depends": []
+ }
+ ]
+ })
+
+ self.assertRaises(
+ morphlib.morphloader.NoStratumBuildDependenciesError,
+ self.loader.validate, m)
+
+ m['build-depends'] = [
+ {
+ "repo": "foo",
+ "ref": "foo",
+ "morph": "foo",
+ },
+ ]
+ self.loader.validate(m)
+
+ del m['build-depends']
+ m['chunks'][0]['build-mode'] = 'bootstrap'
+ self.loader.validate(m)
+
+ def test_validate_requires_chunks_in_strata(self):
+ m = morphlib.morph3.Morphology(
+ {
+ "name": "stratum",
+ "kind": "stratum",
+ "chunks": [
+ ],
+ "build-depends": [
+ {
+ "repo": "foo",
+ "ref": "foo",
+ "morph": "foo",
+ },
+ ],
+ })
+
+ self.assertRaises(
+ morphlib.morphloader.EmptyStratumError,
+ self.loader.validate, m)
+
+ def test_loads_yaml_from_string(self):
+ string = '''\
+name: foo
+kind: chunk
+build-system: dummy
+'''
+ morph = self.loader.load_from_string(string)
+ self.assertEqual(morph['kind'], 'chunk')
+ self.assertEqual(morph['name'], 'foo')
+ self.assertEqual(morph['build-system'], 'dummy')
+
+ def test_loads_json_from_string(self):
+ string = '''\
+{
+ "name": "foo",
+ "kind": "chunk",
+ "build-system": "dummy"
+}
+'''
+ morph = self.loader.load_from_string(string)
+ self.assertEqual(morph['kind'], 'chunk')
+ self.assertEqual(morph['name'], 'foo')
+ self.assertEqual(morph['build-system'], 'dummy')
+
+ def test_loads_from_file(self):
+ with open(self.filename, 'w') as f:
+ f.write('''\
+name: foo
+kind: chunk
+build-system: dummy
+''')
+ morph = self.loader.load_from_file(self.filename)
+ self.assertEqual(morph['kind'], 'chunk')
+ self.assertEqual(morph['name'], 'foo')
+ self.assertEqual(morph['build-system'], 'dummy')
+
+ def test_saves_to_string(self):
+ morph = morphlib.morph3.Morphology({
+ 'name': 'foo',
+ 'kind': 'chunk',
+ 'build-system': 'dummy',
+ })
+ text = self.loader.save_to_string(morph)
+
+ # The following verifies that the YAML is written in a normalised
+ # fashion.
+ self.assertEqual(text, '''\
+build-system: dummy
+kind: chunk
+name: foo
+''')
+
+ def test_saves_to_file(self):
+ morph = morphlib.morph3.Morphology({
+ 'name': 'foo',
+ 'kind': 'chunk',
+ 'build-system': 'dummy',
+ })
+ self.loader.save_to_file(self.filename, morph)
+
+ with open(self.filename) as f:
+ text = f.read()
+
+ # The following verifies that the YAML is written in a normalised
+ # fashion.
+ self.assertEqual(text, '''\
+build-system: dummy
+kind: chunk
+name: foo
+''')
+
+ def test_validate_does_not_set_defaults(self):
+ m = morphlib.morph3.Morphology({
+ 'kind': 'chunk',
+ 'name': 'foo',
+ })
+ self.loader.validate(m)
+ self.assertEqual(sorted(m.keys()), sorted(['kind', 'name']))
+
+ def test_sets_defaults_for_chunks(self):
+ m = morphlib.morph3.Morphology({
+ 'kind': 'chunk',
+ 'name': 'foo',
+ })
+ self.loader.set_defaults(m)
+ self.loader.validate(m)
+ self.assertEqual(
+ dict(m),
+ {
+ 'kind': 'chunk',
+ 'name': 'foo',
+ 'description': '',
+ 'build-system': 'manual',
+
+ 'configure-commands': [],
+ 'pre-configure-commands': [],
+ 'post-configure-commands': [],
+
+ 'build-commands': [],
+ 'pre-build-commands': [],
+ 'post-build-commands': [],
+
+ 'test-commands': [],
+ 'pre-test-commands': [],
+ 'post-test-commands': [],
+
+ 'install-commands': [],
+ 'pre-install-commands': [],
+ 'post-install-commands': [],
+
+ 'chunks': [],
+ 'devices': [],
+ 'max-jobs': None,
+ })
+
+ def test_sets_defaults_for_strata(self):
+ m = morphlib.morph3.Morphology({
+ 'kind': 'stratum',
+ 'name': 'foo',
+ 'chunks': [
+ {
+ 'name': 'bar',
+ 'repo': 'bar',
+ 'ref': 'bar',
+ 'morph': 'bar',
+ 'build-mode': 'bootstrap',
+ 'build-depends': [],
+ },
+ ],
+ })
+ self.loader.set_defaults(m)
+ self.loader.validate(m)
+ self.assertEqual(
+ dict(m),
+ {
+ 'kind': 'stratum',
+ 'name': 'foo',
+ 'description': '',
+ 'build-depends': [],
+ 'chunks': [
+ {
+ 'name': 'bar',
+ "repo": "bar",
+ "ref": "bar",
+ "morph": "bar",
+ 'build-mode': 'bootstrap',
+ 'build-depends': [],
+ },
+ ],
+ })
+
+ def test_sets_defaults_for_system(self):
+ m = morphlib.morph3.Morphology({
+ 'kind': 'system',
+ 'name': 'foo',
+ 'arch': 'x86_64',
+ })
+ self.loader.set_defaults(m)
+ self.loader.validate(m)
+ self.assertEqual(
+ dict(m),
+ {
+ 'kind': 'system',
+ 'system-kind': 'rootfs-tarball',
+ 'name': 'foo',
+ 'description': '',
+ 'arch': 'x86_64',
+ 'strata': [],
+ 'configuration-extensions': [],
+ 'disk-size': '1G',
+ })
+
+ def test_sets_stratum_chunks_repo_and_morph_from_name(self):
+ m = morphlib.morph3.Morphology(
+ {
+ "name": "foo",
+ "kind": "stratum",
+ "chunks": [
+ {
+ "name": "le-chunk",
+ "ref": "ref",
+ "build-depends": [],
+ }
+ ]
+ })
+
+ self.loader.set_defaults(m)
+ self.loader.validate(m)
+ self.assertEqual(m['chunks'][0]['repo'], 'le-chunk')
+ self.assertEqual(m['chunks'][0]['morph'], 'le-chunk')
+
+ def test_convertes_max_jobs_to_an_integer(self):
+ m = morphlib.morph3.Morphology(
+ {
+ "name": "foo",
+ "kind": "chunk",
+ "max-jobs": "42"
+ })
+ self.loader.set_defaults(m)
+ self.assertEqual(m['max-jobs'], 42)
+
+