diff options
Diffstat (limited to 'morphlib')
-rw-r--r-- | morphlib/__init__.py | 27 | ||||
-rw-r--r-- | morphlib/builder.py | 117 | ||||
-rw-r--r-- | morphlib/execute.py | 87 | ||||
-rw-r--r-- | morphlib/execute_tests.py | 51 | ||||
-rw-r--r-- | morphlib/morphology.py | 196 | ||||
-rw-r--r-- | morphlib/morphology_tests.py | 458 | ||||
-rw-r--r-- | morphlib/tempdir.py | 58 | ||||
-rw-r--r-- | morphlib/tempdir_tests.py | 69 | ||||
-rw-r--r-- | morphlib/util.py | 32 | ||||
-rw-r--r-- | morphlib/util_tests.py | 36 |
10 files changed, 1131 insertions, 0 deletions
diff --git a/morphlib/__init__.py b/morphlib/__init__.py new file mode 100644 index 00000000..bd318978 --- /dev/null +++ b/morphlib/__init__.py @@ -0,0 +1,27 @@ +# Copyright (C) 2011 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; either 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. + + +'''Baserock library.''' + + +__version__ = '0.0' + + +import builder +import execute +import morphology +import tempdir +import util diff --git a/morphlib/builder.py b/morphlib/builder.py new file mode 100644 index 00000000..befe8485 --- /dev/null +++ b/morphlib/builder.py @@ -0,0 +1,117 @@ +# Copyright (C) 2011 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; either 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. + + +import logging +import os + +import morphlib + + +class Builder(object): + + '''Build binary objects for Baserock. + + The objects may be chunks or strata.''' + + def __init__(self, tempdir, msg): + self.tempdir = tempdir + self.msg = msg + + def build(self, morph): + '''Build a binary based on a morphology.''' + if morph.kind == 'chunk': + self.build_chunk(morph) + elif morph.kind == 'stratum': + self.build_stratum(morph) + else: + raise Exception('Unknown kind of morphology: %s' % morph.kind) + + def build_chunk(self, morph): + '''Build a chunk from a morphology.''' + logging.debug('Building chunk') + self.ex = morphlib.execute.Execute(self._build, self.msg) + self.ex.env['WORKAREA'] = self.tempdir.dirname + self.ex.env['DESTDIR'] = self._inst + '/' + self.create_build_tree(morph) + self.ex.run(morph.configure_commands) + self.ex.run(morph.build_commands) + self.ex.run(morph.test_commands) + self.ex.run(morph.install_commands) + self.create_chunk(morph) + self.tempdir.clear() + + def create_build_tree(self, morph): + '''Export sources from git into the ``self._build`` directory.''' + + logging.debug('Creating build tree at %s' % self._build) + tarball = self.tempdir.join('sources.tar') + self.ex.runv(['git', 'archive', + '--output', tarball, + '--remote', morph.source['repo'], + morph.source['ref']]) + os.mkdir(self._build) + self.ex.runv(['tar', '-C', self._build, '-xf', tarball]) + os.remove(tarball) + + def create_chunk(self, morph): + '''Create a Baserock chunk from the ``self._inst`` directory. + + The directory must be filled in with all the relevant files already. + + ''' + + dirname = os.path.dirname(morph.filename) + filename = os.path.join(dirname, '%s.chunk' % morph.name) + logging.debug('Creating chunk %s at %s' % (morph.name, filename)) + self.ex.runv(['tar', '-C', self._inst, '-czf', filename, '.']) + + def build_stratum(self, morph): + '''Build a stratum from a morphology.''' + os.mkdir(self._inst) + self.ex = morphlib.execute.Execute(self.tempdir.dirname, self.msg) + for chunk_name in morph.sources: + filename = self._chunk_filename(morph, chunk_name) + self.unpack_chunk(filename) + self.create_stratum(morph) + self.tempdir.clear() + + def unpack_chunk(self, filename): + self.ex.runv(['tar', '-C', self._inst, '-xf', filename]) + + def create_stratum(self, morph): + '''Create a Baserock stratum from the ``self._inst`` directory. + + The directory must be filled in with all the relevant files already. + + ''' + + dirname = os.path.dirname(morph.filename) + filename = os.path.join(dirname, '%s.stratum' % morph.name) + logging.debug('Creating stratum %s at %s' % (morph.name, filename)) + self.ex.runv(['tar', '-C', self._inst, '-czf', filename, '.']) + + @property + def _build(self): + return self.tempdir.join('build') + + @property + def _inst(self): + return self.tempdir.join('inst') + + def _chunk_filename(self, morph, chunk_name): + dirname = os.path.dirname(morph.filename) + return os.path.join(dirname, '%s.chunk' % chunk_name) + diff --git a/morphlib/execute.py b/morphlib/execute.py new file mode 100644 index 00000000..9a279591 --- /dev/null +++ b/morphlib/execute.py @@ -0,0 +1,87 @@ +# Copyright (C) 2011 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; either 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. + + +import logging +import os +import subprocess + +import morphlib + + +class CommandFailure(Exception): + + pass + + +class Execute(object): + + '''Execute commands for morph.''' + + def __init__(self, dirname, msg): + self._setup_env() + self.dirname = dirname + self.msg = msg + + def _setup_env(self): + self.env = dict(os.environ) + + def run(self, commands): + '''Execute a list of commands. + + If a command fails (returns non-zero exit code), the rest are + not run, and CommandFailure is returned. + + ''' + + stdouts = [] + for command in commands: + self.msg('# %s' % command) + p = subprocess.Popen([command], shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=self.env, + cwd=self.dirname) + out, err = p.communicate() + logging.debug('Exit code: %d' % p.returncode) + logging.debug('Standard output:\n%s' % morphlib.util.indent(out)) + logging.debug('Standard error:\n%s' % morphlib.util.indent(err)) + if p.returncode != 0: + raise CommandFailure('Command failed: %s\n%s' % + (command, morphlib.util.indent(err))) + stdouts.append(out) + return stdouts + + def runv(self, argv): + '''Run a command given as a list of argv elements. + + Return standard output. Raise ``CommandFailure`` if the command + fails. Log standard output and error in any case. + + ''' + + self.msg('# %s' % ' '.join(argv)) + p = subprocess.Popen(argv, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = p.communicate() + + logging.debug('Exit code: %d' % p.returncode) + logging.debug('Standard output:\n%s' % morphlib.util.indent(out)) + logging.debug('Standard error:\n%s' % morphlib.util.indent(err)) + if p.returncode != 0: + raise CommandFailure('Command failed: %s\n%s' % + (argv, morphlib.util.indent(err))) + return out + diff --git a/morphlib/execute_tests.py b/morphlib/execute_tests.py new file mode 100644 index 00000000..721d0807 --- /dev/null +++ b/morphlib/execute_tests.py @@ -0,0 +1,51 @@ +# Copyright (C) 2011 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; either 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. + + +import os +import unittest + +import morphlib + + +class ExecuteTests(unittest.TestCase): + + def setUp(self): + self.e = morphlib.execute.Execute('/') + + def test_has_same_path_as_environment(self): + self.assertEqual(self.e.env['PATH'], os.environ['PATH']) + + def test_executes_true_ok(self): + self.assertEqual(self.e.run(['true']), ['']) + + def test_raises_commandfailure_for_false(self): + self.assertRaises(morphlib.execute.CommandFailure, + self.e.run, ['false']) + + def test_returns_stdout_from_all_commands(self): + self.assertEqual(self.e.run(['echo foo', 'echo bar']), + ['foo\n', 'bar\n']) + + def test_sets_working_directory(self): + self.assertEqual(self.e.run(['pwd']), ['/\n']) + + def test_executes_argv(self): + self.assertEqual(self.e.runv(['echo', 'foo']), 'foo\n') + + def test_raises_error_when_argv_fails(self): + self.assertRaises(morphlib.execute.CommandFailure, + self.e.runv, ['false']) + diff --git a/morphlib/morphology.py b/morphlib/morphology.py new file mode 100644 index 00000000..d31a000c --- /dev/null +++ b/morphlib/morphology.py @@ -0,0 +1,196 @@ +# Copyright (C) 2011 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; either 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. + + +import json +import logging + + +class SchemaError(Exception): + + pass + + +class Morphology(object): + + '''Represent a morphology: description of how to build binaries.''' + + def __init__(self, fp, baseurl=None): + self._fp = fp + self._baseurl = baseurl or '' + self._load() + + def _load(self): + logging.debug('Loading morphology %s' % self._fp.name) + self._dict = json.load(self._fp) + + if 'name' not in self._dict: + raise self._error('must contain "name"') + + if not self.name: + raise self._error('"name" must not be empty') + + if 'kind' not in self._dict: + raise self._error('must contain "kind"') + + if self.kind == 'chunk': + self._validate_chunk() + self.source['repo'] = self._join_with_baseurl(self.source['repo']) + elif self.kind == 'stratum': + self._validate_stratum() + for source in self.sources.itervalues(): + source['repo'] = self._join_with_baseurl(source['repo']) + else: + raise self._error('kind must be chunk or stratum, not %s' % + self.kind) + + self.filename = self._fp.name + + def _validate_chunk(self): + valid_toplevel_keys = ['name', 'kind', 'source', 'configure-commands', + 'build-commands', 'test-commands', + 'install-commands'] + + if 'source' not in self._dict: + raise self._error('chunks must have "source" field') + + if type(self.source) != dict: + raise self._error('"source" must be a dictionary') + + if len(self.source) == 0: + raise self._error('"source" must not be empty') + + if 'repo' not in self.source: + raise self._error('"source" must contain "repo"') + + if not self.source['repo']: + raise self._error('"source" must contain non-empty "repo"') + + if 'ref' not in self.source: + raise self._error('"source" must contain "ref"') + + if not self.source['ref']: + raise self._error('"source" must contain non-empty "ref"') + + for key in self.source.keys(): + if key not in ('repo', 'ref'): + raise self._error('unknown key "%s" in "source"' % key) + + cmdlists = [ + (self.configure_commands, 'configure-commands'), + (self.build_commands, 'build-commands'), + (self.test_commands, 'test-commands'), + (self.install_commands, 'install-commands'), + ] + for value, name in cmdlists: + if type(value) != list: + raise self._error('"%s" must be a list' % name) + for x in value: + if type(x) != unicode: + raise self._error('"%s" must contain strings' % name) + + for key in self._dict.keys(): + if key not in valid_toplevel_keys: + raise self._error('unknown key "%s"' % key) + + def _validate_stratum(self): + valid_toplevel_keys = ['name', 'kind', 'sources'] + + if 'sources' not in self._dict: + raise self._error('stratum must contain "sources"') + + if type(self.sources) != dict: + raise self._error('"sources" must be a dict') + + if len(self.sources) == 0: + raise self._error('"sources" must not be empty') + + for name, source in self.sources.iteritems(): + if type(source) != dict: + raise self._error('"sources" must contain dicts') + if 'repo' not in source: + raise self._error('sources must have "repo"') + if type(source['repo']) != unicode: + raise self._error('"repo" must be a string') + if not source['repo']: + raise self._error('"repo" must be a non-empty string') + if 'ref' not in source: + raise self._error('sources must have "ref"') + if type(source['ref']) != unicode: + raise self._error('"ref" must be a string') + if not source['ref']: + raise self._error('"ref" must be a non-empty string') + for key in source: + if key not in ('repo', 'ref'): + raise self._error('unknown key "%s" in sources' % key) + + for key in self._dict.keys(): + if key not in valid_toplevel_keys: + raise self._error('unknown key "%s"' % key) + + @property + def name(self): + return self._dict['name'] + + @property + def kind(self): + return self._dict['kind'] + + @property + def source(self): + return self._dict['source'] + + @property + def sources(self): + return self._dict['sources'] + + @property + def configure_commands(self): + return self._dict.get('configure-commands', []) + + @property + def build_commands(self): + return self._dict.get('build-commands', []) + + @property + def test_commands(self): + return self._dict.get('test-commands', []) + + @property + def install_commands(self): + return self._dict.get('install-commands', []) + + @property + def manifest(self): + if self.kind == 'chunk': + return [(self.source['repo'], self.source['ref'])] + else: + return [(source['repo'], source['ref']) + for source in self.sources.itervalues()] + + def _join_with_baseurl(self, url): + is_relative = (':' not in url or + '/' not in url or + url.find('/') < url.find(':')) + if is_relative: + if not url.endswith('/'): + url += '/' + return self._baseurl + url + else: + return url + + def _error(self, msg): + return SchemaError('Morphology %s: %s' % (self._fp.name, msg)) + diff --git a/morphlib/morphology_tests.py b/morphlib/morphology_tests.py new file mode 100644 index 00000000..7a38bff4 --- /dev/null +++ b/morphlib/morphology_tests.py @@ -0,0 +1,458 @@ +# Copyright (C) 2011 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; either 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. + + +import json +import StringIO +import unittest + +import morphlib + + +class MockFile(StringIO.StringIO): + + def __init__(self, *args, **kwargs): + StringIO.StringIO.__init__(self, *args, **kwargs) + self.name = 'mockfile' + + +class MorphologyTests(unittest.TestCase): + + def assertRaisesSchemaError(self, morph_dict): + f = MockFile(json.dumps(morph_dict)) + self.assertRaises(morphlib.morphology.SchemaError, + morphlib.morphology.Morphology, f) + + def test_raises_exception_for_empty_file(self): + self.assertRaises(ValueError, + morphlib.morphology.Morphology, + MockFile()) + + def test_raises_exception_for_file_without_kind_field(self): + self.assertRaisesSchemaError({}) + + def test_raises_exception_for_chunk_with_unknown_keys_only(self): + self.assertRaisesSchemaError({ 'x': 'y' }) + + def test_raises_exception_if_name_only(self): + self.assertRaisesSchemaError({ 'name': 'hello' }) + + def test_raises_exception_if_name_is_empty(self): + self.assertRaisesSchemaError({ 'name': '', 'kind': 'chunk', + 'sources': { 'repo': 'x', 'ref': 'y' }}) + + def test_raises_exception_if_kind_only(self): + self.assertRaisesSchemaError({ 'kind': 'chunk' }) + + def test_raises_exception_for_kind_that_has_unknown_kind(self): + self.assertRaisesSchemaError({ 'name': 'hello', 'kind': 'x' }) + + def test_raises_exception_for_chunk_without_source(self): + self.assertRaisesSchemaError({ 'name': 'hello', 'kind': 'chunk' }) + + def test_raises_exception_for_chunk_with_nondict_source(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': [], + }) + + def test_raises_exception_for_chunk_with_empty_source(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': {}, + }) + + def test_raises_exception_for_chunk_without_repo_in_source(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': { + 'x': 'y' + }, + }) + + def test_raises_exception_for_chunk_with_empty_repo_in_source(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': { + 'repo': '', + 'ref': 'master' + }, + }) + + def test_raises_exception_for_chunk_without_ref_in_source(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': { + 'repo': 'foo', + }, + }) + + def test_raises_exception_for_chunk_with_empty_ref_in_source(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': { + 'repo': 'foo', + 'ref': '' + }, + }) + + def test_raises_exception_for_chunk_with_unknown_keys_in_source(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': { + 'repo': 'foo', + 'ref': 'master', + 'x': 'y' + }, + }) + + def test_raises_exception_for_chunk_with_unknown_keys(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': { + 'repo': 'foo', + 'ref': 'master' + }, + 'x': 'y' + }) + + def test_raises_exception_for_nonlist_configure_commands(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': { + 'repo': 'foo', + 'ref': 'master' + }, + 'configure-commands': 0, + }) + + def test_raises_exception_for_list_of_nonstring_configure_commands(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': { + 'repo': 'foo', + 'ref': 'master' + }, + 'configure-commands': [0], + }) + + def test_raises_exception_for_nonlist_build_commands(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': { + 'repo': 'foo', + 'ref': 'master' + }, + 'build-commands': 0, + }) + + def test_raises_exception_for_list_of_nonstring_build_commands(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': { + 'repo': 'foo', + 'ref': 'master' + }, + 'build-commands': [0], + }) + + def test_raises_exception_for_nonlist_test_commands(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': { + 'repo': 'foo', + 'ref': 'master' + }, + 'test-commands': 0, + }) + + def test_raises_exception_for_list_of_nonstring_test_commands(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': { + 'repo': 'foo', + 'ref': 'master' + }, + 'build-commands': [0], + }) + + def test_raises_exception_for_nonlist_install_commands(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': { + 'repo': 'foo', + 'ref': 'master' + }, + 'install-commands': 0, + }) + + def test_raises_exception_for_list_of_nonstring_install_commands(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'chunk', + 'source': { + 'repo': 'foo', + 'ref': 'master' + }, + 'install-commands': [0], + }) + + def test_accepts_valid_chunk_morphology(self): + chunk = morphlib.morphology.Morphology( + MockFile(''' + { + "name": "hello", + "kind": "chunk", + "source": + { + "repo": "foo", + "ref": "ref" + }, + "configure-commands": ["./configure"], + "build-commands": ["make"], + "test-commands": ["make check"], + "install-commands": ["make install"] + }''')) + self.assertEqual(chunk.kind, 'chunk') + self.assertEqual(chunk.filename, 'mockfile') + + def test_raises_exception_for_stratum_without_sources(self): + self.assertRaisesSchemaError({ 'name': 'hello', 'kind': 'stratum' }) + + def test_raises_exception_for_stratum_with_nondict_sources(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'stratum', + 'sources': [], + }) + + def test_raises_exception_for_stratum_with_empty_sources(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'stratum', + 'sources': {}, + }) + + def test_raises_exception_for_stratum_with_bad_children_in_sources(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'stratum', + 'sources': { + 'foo': 0, + }, + }) + + def test_raises_exception_for_stratum_without_repo_in_sources(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'stratum', + 'sources': { + 'foo': { + 'ref': 'master' + } + }, + }) + + def test_raises_exception_for_stratum_with_empty_repo_in_sources(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'stratum', + 'sources': { + 'foo': { + 'repo': '', + 'ref': 'master' + } + }, + }) + + def test_raises_exception_for_stratum_with_nonstring_repo_in_sources(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'stratum', + 'sources': { + 'foo': { + 'repo': 0, + 'ref': 'master' + } + }, + }) + + def test_raises_exception_for_stratum_without_ref_in_sources(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'stratum', + 'sources': { + 'foo': { + 'repo': 'foo', + } + }, + }) + + def test_raises_exception_for_stratum_with_empty_ref_in_sources(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'stratum', + 'sources': { + 'foo': { + 'repo': 'foo', + 'ref': '' + } + }, + }) + + def test_raises_exception_for_stratum_with_nonstring_ref_in_sources(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'stratum', + 'sources': { + 'foo': { + 'repo': 'foo', + 'ref': 0 + } + }, + }) + + def test_raises_exception_for_stratum_with_unknown_keys_in_sources(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'stratum', + 'sources': { + 'foo': { + 'repo': 'foo', + 'ref': 'master', + 'x': 'y' + } + }, + }) + + def test_raises_exception_for_stratum_with_unknown_keys(self): + self.assertRaisesSchemaError({ + 'name': 'hello', + 'kind': 'stratum', + 'sources': { + 'foo': { + 'repo': 'foo', + 'ref': 'master' + } + }, + 'x': 'y' + }) + + def test_accepts_valid_stratum_morphology(self): + morph = morphlib.morphology.Morphology( + MockFile(''' + { + "name": "hello", + "kind": "stratum", + "sources": + { + "foo": { + "repo": "foo", + "ref": "ref" + } + } + }''')) + self.assertEqual(morph.kind, 'stratum') + self.assertEqual(morph.filename, 'mockfile') + + +class ChunkRepoTests(unittest.TestCase): + + def chunk(self, repo): + return morphlib.morphology.Morphology( + MockFile(''' + { + "name": "hello", + "kind": "chunk", + "source": + { + "repo": "%s", + "ref": "HEAD" + }, + "configure-commands": ["./configure"], + "build-commands": ["make"], + "test-commands": ["make check"], + "install-commands": ["make install"] + }''' % repo), + baseurl='git://git.baserock.org/') + + def test_returns_repo_with_schema_as_is(self): + self.assertEqual(self.chunk('git://git.baserock.org/foo/').manifest, + [('git://git.baserock.org/foo/', 'HEAD')]) + + def test_prepends_baseurl_to_repo_without_schema(self): + self.assertEqual(self.chunk('foo').manifest, + [('git://git.baserock.org/foo/', 'HEAD')]) + + def test_leaves_absolute_repo_in_source_dict_as_is(self): + chunk = self.chunk('git://git.baserock.org/foo/') + self.assertEqual(chunk.source['repo'], 'git://git.baserock.org/foo/') + + def test_makes_relative_repo_url_absolute_in_source_dict(self): + chunk = self.chunk('foo') + self.assertEqual(chunk.source['repo'], 'git://git.baserock.org/foo/') + + +class StratumRepoTests(unittest.TestCase): + + def stratum(self, repo): + return morphlib.morphology.Morphology( + MockFile(''' + { + "name": "hello", + "kind": "stratum", + "sources": + { + "foo": { + "repo": "%s", + "ref": "HEAD" + } + } + }''' % repo), + baseurl='git://git.baserock.org/') + + def test_returns_repo_with_schema_as_is(self): + self.assertEqual(self.stratum('git://git.baserock.org/foo/').manifest, + [('git://git.baserock.org/foo/', 'HEAD')]) + + def test_prepends_baseurl_to_repo_without_schema(self): + self.assertEqual(self.stratum('foo').manifest, + [('git://git.baserock.org/foo/', 'HEAD')]) + + def test_leaves_absolute_repo_in_source_dict_as_is(self): + stratum = self.stratum('git://git.baserock.org/foo/') + self.assertEqual(stratum.sources['foo']['repo'], + 'git://git.baserock.org/foo/') + + def test_makes_relative_repo_url_absolute_in_source_dict(self): + stratum = self.stratum('foo') + self.assertEqual(stratum.sources['foo']['repo'], + 'git://git.baserock.org/foo/') + diff --git a/morphlib/tempdir.py b/morphlib/tempdir.py new file mode 100644 index 00000000..7f58cdc3 --- /dev/null +++ b/morphlib/tempdir.py @@ -0,0 +1,58 @@ +# Copyright (C) 2011 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; either 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. + + +import logging +import os +import shutil +import tempfile + + +class Tempdir(object): + + '''Temporary file handling for morph.''' + + def __init__(self, parent=None): + self.dirname = tempfile.mkdtemp(dir=parent) + logging.debug('Created temporary directory %s' % self.dirname) + + def remove(self): + '''Remove the temporary directory.''' + logging.debug('Removing temporary directory %s' % self.dirname) + shutil.rmtree(self.dirname) + self.dirname = None + + def clear(self): + '''Clear temporary directory of everything.''' + for x in os.listdir(self.dirname): + filename = self.join(x) + if os.path.isdir(filename): + shutil.rmtree(filename) + else: + os.remove(filename) + + def join(self, relative): + '''Return full path to file in temporary directory. + + The relative path is given appended to the name of the + temporary directory. If the relative path is actually absolute, + it is forced to become relative. + + The returned path is normalized. + + ''' + + return os.path.normpath(os.path.join(self.dirname, './' + relative)) + diff --git a/morphlib/tempdir_tests.py b/morphlib/tempdir_tests.py new file mode 100644 index 00000000..6572b70a --- /dev/null +++ b/morphlib/tempdir_tests.py @@ -0,0 +1,69 @@ +# Copyright (C) 2011 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; either 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. + + +import os +import shutil +import unittest + +import morphlib + + +class TempdirTests(unittest.TestCase): + + def setUp(self): + self.parent = os.path.abspath('unittest-tempdir') + os.mkdir(self.parent) + self.tempdir = morphlib.tempdir.Tempdir(parent=self.parent) + + def tearDown(self): + shutil.rmtree(self.parent) + + def test_creates_the_directory(self): + self.assert_(os.path.isdir(self.tempdir.dirname)) + + def test_creates_subdirectory_of_parent(self): + self.assert_(self.tempdir.dirname.startswith(self.parent + '/')) + + def test_uses_default_if_parent_not_specified(self): + t = morphlib.tempdir.Tempdir() + shutil.rmtree(t.dirname) + self.assertNotEqual(t.dirname, None) + + def test_removes_itself(self): + dirname = self.tempdir.dirname + self.tempdir.remove() + self.assertEqual(self.tempdir.dirname, None) + self.assertFalse(os.path.exists(dirname)) + + def test_joins_filename(self): + self.assertEqual(self.tempdir.join('foo'), + os.path.join(self.tempdir.dirname, 'foo')) + + def test_joins_absolute_filename(self): + self.assertEqual(self.tempdir.join('/foo'), + os.path.join(self.tempdir.dirname, 'foo')) + + def test_clears_when_empty(self): + self.tempdir.clear() + self.assertEqual(os.listdir(self.tempdir.dirname), []) + + def test_clears_when_not_empty(self): + os.mkdir(self.tempdir.join('foo')) + with open(self.tempdir.join('bar'), 'w') as f: + f.write('bar') + self.tempdir.clear() + self.assertEqual(os.listdir(self.tempdir.dirname), []) + diff --git a/morphlib/util.py b/morphlib/util.py new file mode 100644 index 00000000..a181ff29 --- /dev/null +++ b/morphlib/util.py @@ -0,0 +1,32 @@ +# Copyright (C) 2011 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; either 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. + + +'''Utility functions for morph.''' + + +def indent(string, spaces=4): + '''Return ``string`` indented by ``spaces`` spaces. + + The final line is not terminated by a newline. This makes it easy + to use this function for indenting long text for logging: the + logging library adds a newline, so not including it in the indented + text avoids a spurious empty line in the log file. + + ''' + + return '\n'.join('%*s%s' % (spaces, '', line) + for line in string.splitlines()) + diff --git a/morphlib/util_tests.py b/morphlib/util_tests.py new file mode 100644 index 00000000..d27de771 --- /dev/null +++ b/morphlib/util_tests.py @@ -0,0 +1,36 @@ +# Copyright (C) 2011 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; either 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. + + +import unittest + +import morphlib + + +class IndentTests(unittest.TestCase): + + def test_returns_empty_string_for_empty_string(self): + self.assertEqual(morphlib.util.indent(''), '') + + def test_indents_single_line(self): + self.assertEqual(morphlib.util.indent('foo'), ' foo') + + def test_obeys_spaces_setting(self): + self.assertEqual(morphlib.util.indent('foo', spaces=2), ' foo') + + def test_indents_multiple_lines(self): + self.assertEqual(morphlib.util.indent('foo\nbar\n'), + ' foo\n bar') + |