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 9808e8f28e
9 changed files with 315 additions and 33 deletions

29
fixtures_git/exc.py Normal file
View File

@ -0,0 +1,29 @@
#
# 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):
"""Base exception for this package"""
pass
class InvalidGraphFormat(FixturesGitException):
"""Specified graph does not match any schema"""
pass
class BadGraphDefinition(FixturesGitException):
"""Problem in walking the graph definition"""
pass

View File

@ -13,13 +13,16 @@
# under the License.
#
import logging
import os
import tempfile
import faker
import git
import voluptuous
from fixtures_git import _utils
from fixtures_git import exc
class GitTree(object):
@ -70,7 +73,24 @@ 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 +164,45 @@ class GitTree(object):
if branches is None:
branches = []
def validate_graph(validator, err_msg):
try:
for node in graph_def:
validator(node)
except (voluptuous.Invalid, voluptuous.MultipleInvalid) as err:
self.logger.error(
"Validation of '%s' failed with error '%s'", node, err
)
raise exc.InvalidGraphFormat(
"Invalid graph structure: %s" % err_msg
)
# validate graph definition
node1 = graph_def[0]
if isinstance(node1, dict):
validate_graph(
self.explicit_schema_validator,
"Does not conform to explicit style format."
)
# need to generate a suitable graph_def to walk
raise RuntimeError("Explicit schema not yet handled")
elif isinstance(node1, list):
validate_graph(
self.simple_schema_validator,
"Does not conform to simple style format."
)
# Or convert simple format to explicit here
else:
raise exc.InvalidGraphFormat(
"Unknown graph format or unable to guess from '%s'" % node1
)
# 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 exc.BadGraphDefinition(
"No root commit defined in test graph"
)
for node, parents in _utils.reverse_toposort(graph_def):
if not parents:
@ -171,7 +226,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,73 @@
#
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()
self.logger = logging.getLogger(
"%s.%s" % (__name__, self.__class__.__name__)
)
# capture stdout/stderr for tests to inspect easily
self.stdout = self.useFixture(ChannelFixture('stdout'))
self.stderr = self.useFixture(ChannelFixture('stderr'))
self.logger_output = 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.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'],
)