diff --git a/fixtures_git/exc.py b/fixtures_git/exc.py new file mode 100644 index 0000000..df3a544 --- /dev/null +++ b/fixtures_git/exc.py @@ -0,0 +1,26 @@ +# +# Copyright (c) 2018 Hewlett Packard Enterprise Development Company LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +class FixturesGitException(Exception): + pass + + +class InvalidGraphFormat(FixturesGitException): + pass + + +class BadGraphDefinition(FixturesGitException): + pass diff --git a/fixtures_git/gittree.py b/fixtures_git/gittree.py index 9a17e3a..6662fd4 100644 --- a/fixtures_git/gittree.py +++ b/fixtures_git/gittree.py @@ -13,12 +13,15 @@ # under the License. # +import logging import os import tempfile import faker import git +import voluptuous +from fixtures_git import exc from fixtures_git import _utils @@ -70,7 +73,25 @@ class GitTree(object): .. _git-upstream: https://pypi.python.org/pypi/git-upstream """ + explicit_schema_validator = voluptuous.Schema({ + voluptuous.Required('name'): str, + 'message': str, + 'files': dict, + 'author': str, + 'parents': list, + } + ) + + simple_schema_validator = voluptuous.Schema( + voluptuous.ExactSequence( + [str, voluptuous.Schema([str])] + ) + ) + def __init__(self, repo, tree, branches): + self.logger = logging.getLogger( + "%s.%s" % (__name__, self.__class__.__name__) + ) self.graph = {} self.repo = repo @@ -144,10 +165,38 @@ class GitTree(object): if branches is None: branches = [] + # validate graph definition + node1 = graph_def[0] + if isinstance(node1, dict): + validator = self.explicit_schema_validator + err_msg = "Does not conform to explicit style format." + elif isinstance(node1, list): + validator = self.simple_schema_validator + err_msg = "Does not conform to simple style format." + else: + raise exc.InvalidGraphFormat( + "Unknown graph format or unable to guess from '%s'" % node1 + ) + + try: + for c in graph_def: + validator(c) + except (voluptuous.Invalid, voluptuous.MultipleInvalid) as err: + self.logger.error( + "Validation of '%s' failed with error '%s'", c, err + ) + raise exc.InvalidGraphFormat( + "Invalid graph structure: %s" % err_msg + ) + + if isinstance(graph_def[0], dict): + # need to generate a suitable graph_def to walk + raise RuntimeError("Explicit schema not yet handled") + # require that graphs must have at least 1 node with no # parents, which is a root commit in git if not any([True for _, parents in graph_def if not parents]): - assert "No root commit defined in test graph" + raise BadGraphDefinition("No root commit defined in test graph") for node, parents in _utils.reverse_toposort(graph_def): if not parents: @@ -171,7 +220,20 @@ class GitTree(object): self._commit(node) self.graph[node] = self.repo.commit() - for name, node in branches: + # handle branches being specified in explicit or simple + if isinstance(branches, dict): + # explicit format + list_branches = branches.items() + else: + # simple format + list_branches = branches + for name, node in list_branches: + if isinstance(node, list): + name, node = node + else: + node = node + if node in branches: + node = branches[node] self.repo.git.branch(name, str(self.graph[node]), f=True) # return to master diff --git a/requirements.txt b/requirements.txt index f41870e..0d99e08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ faker>=0.8.16 fixtures>=3.0.0 GitPython>=1.0.1 +voluptuous==0.11.5 diff --git a/tests/acceptance/__init__.py b/tests/acceptance/__init__.py index edff0f2..e69de29 100644 --- a/tests/acceptance/__init__.py +++ b/tests/acceptance/__init__.py @@ -1,28 +0,0 @@ -# -# Copyright (c) 2018 Hewlett Packard Enterprise Development Company LP -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import logging - -import fixtures -import testtools - - -class BaseTestCase(testtools.TestCase): - - def setUp(self): - super(BaseTestCase, self).setUp() - self.logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) diff --git a/tests/acceptance/test_gitfixture.py b/tests/acceptance/test_gitfixture.py index e554520..dd59535 100644 --- a/tests/acceptance/test_gitfixture.py +++ b/tests/acceptance/test_gitfixture.py @@ -17,10 +17,10 @@ from testtools import matchers from fixtures_git.gitfixture import GitFixture -from tests import acceptance +from tests import base -class TestGitFixture(acceptance.BaseTestCase): +class TestGitFixture(base.BaseTestCase): def test_basic(self): gitfixture = self.useFixture( diff --git a/tests/base.py b/tests/base.py index 70bebd5..4a11a22 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Hewlett Packard Enterprise Development LP +# Copyright (c) 2018-2019 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,70 @@ # +import logging +import os + +import fixtures +import testtools + + +class ChannelFixture(fixtures.Fixture): + + def __init__(self, channel='stdout', object=None): + + self._channel = channel + if object is None: + self._object = 'sys.%s' % channel + else: + self._object = object + + def _setUp(self): + string_fixture = self.useFixture(fixtures.StringStream(self._channel)) + self.stream = string_fixture.stream + self.useFixture( + fixtures.MonkeyPatch(self._object, self.stream) + ) + + def getvalue(self): + self.stream.seek(0) + return self.stream.read() + + +class BaseTestCase(testtools.TestCase): + + def setUp(self): + super(BaseTestCase, self).setUp() + + # capture stdout/stderr for tests to inspect easily + self.stdout = self.useFixture(ChannelFixture('stdout')) + self.stderr = self.useFixture(ChannelFixture('stderr')) + self.logger = self.useFixture( + fixtures.FakeLogger( + level=logging.DEBUG, + format="%(levelname)s:%(name)s:%(message)s" + ) + ) + + def get_testfile(self, ext): + + *path_parts, clsname, testname = self.id().split('.') + testname = testname.replace("test_", '', 1) + possible_test_fixtures = ( + "%s.%s.%s.%s" % (path_parts[-1], clsname, testname, ext), + "%s.%s.%s" % (path_parts[-1], clsname, ext), + "%s.%s" % (path_parts[-1], ext), + ) + for fpath in possible_test_fixtures: + testdatafile = os.path.join(*(path_parts[:-1]), "fixtures", fpath) + if os.path.exists(testdatafile): + return testdatafile + else: + self.test_logger.warn( + "No file with test data found from patterns (%s) for test " + "id %s" % (", ".join(possible_test_fixtures), self.id()) + ) + + class IsOrderedSubsetOfMismatch(object): def __init__(self, subset, set): self.subset = list(subset) diff --git a/tests/unit/fixtures/test_gittree.TestGitTree.tree_graph.yaml b/tests/unit/fixtures/test_gittree.TestGitTree.tree_graph.yaml new file mode 100644 index 0000000..1255ee9 --- /dev/null +++ b/tests/unit/fixtures/test_gittree.TestGitTree.tree_graph.yaml @@ -0,0 +1,29 @@ +# +# (c) Copyright 2019 Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +--- +branches: + master: E + upstream/master: G + somebranch: master + +tree: +- [A, []] +- [B, []] +- [C, [A, B]] +- [D, [C]] +- [E, [D]] +- [F, [A]] +- [G, [F]] diff --git a/tests/unit/fixtures/test_gittree.TestGitTree.tree_old_style_graph.yaml b/tests/unit/fixtures/test_gittree.TestGitTree.tree_old_style_graph.yaml new file mode 100644 index 0000000..edc38b0 --- /dev/null +++ b/tests/unit/fixtures/test_gittree.TestGitTree.tree_old_style_graph.yaml @@ -0,0 +1,28 @@ +# +# (c) Copyright 2019 Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +--- +branches: + head: [master, E] + upstream: [upstream/master, G] + +tree: +- [A, []] +- [B, []] +- [C, [A, B]] +- [D, [C]] +- [E, [D]] +- [F, [A]] +- [G, [F]] diff --git a/tests/unit/test_gittree.py b/tests/unit/test_gittree.py new file mode 100644 index 0000000..1907b0d --- /dev/null +++ b/tests/unit/test_gittree.py @@ -0,0 +1,88 @@ +# Copyright (c) 2018 Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os.path +import shutil + +import fixtures +import git +import yaml + +from tests import base + +from fixtures_git import gittree + + +class TestGitTree(base.BaseTestCase): + + def setUp(self): + super(TestGitTree, self).setUp() + + tempdir = self.useFixture(fixtures.TempDir()) + self.addCleanup(shutil.rmtree, tempdir.path) + self.path = os.path.join(tempdir.path, 'git') + + os.mkdir(self.path) + g = git.Git(self.path) + g.init() + + user = { + 'name': 'Example User', + 'email': 'user@example.com', + } + + self.repo = git.Repo(self.path) + self.repo.git.config('user.email', user['email']) + self.repo.git.config('user.name', user['name']) + + testdatafile = self.get_testfile('yaml') + if testdatafile: + with open(testdatafile) as yfile: + self.data = yaml.load(yfile, Loader=yaml.SafeLoader) + else: + self.data = {} + self.repo.git.commit(m="Initialize empty repo", allow_empty=True) + + def test_tree_graph(self): + """ + Test providing a basic tree with simple nodes can be specified + """ + + tree = gittree.GitTree( + self.repo, self.data['tree'], self.data['branches'] + ) + # test that master points to the right commit + self.assertEqual( + tree.repo.branches['master'].commit, + tree.graph['E'], + ) + # make sure that somebranch resolved to the same commit as master + self.assertEqual( + tree.repo.branches['somebranch'].commit, + tree.graph['E'], + ) + + def test_tree_simple_style_graph(self): + """ + Make sure the simple format for specifying branches remains working + """ + tree = gittree.GitTree( + self.repo, self.data['tree'], self.data['branches'] + ) + self.assertEqual( + tree.repo.branches['master'].commit, + tree.graph['E'], + )