Begin implementing explicit control of commit support

Start implementation of supporting more explicitly defined git tree
graph that allows for controlling individual commit settings such as
specific files, message contents and author details through providing
basic input data format detection and validation

Change-Id: I6075aecd869eb58aa276d313a1f0ebd878e99ff9
This commit is contained in:
Darragh Bailey 2019-06-07 14:25:25 +01:00
parent bb831cb4c4
commit 4371fb019e
9 changed files with 303 additions and 33 deletions

26
fixtures_git/exc.py Normal file
View File

@ -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

View File

@ -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

View File

@ -1,3 +1,4 @@
faker>=0.8.16
fixtures>=3.0.0
GitPython>=1.0.1
voluptuous==0.11.5

View File

@ -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))

View File

@ -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(

View File

@ -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)

View File

@ -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]]

View File

@ -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]]

View File

@ -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'],
)