Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
223 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,5 +7,6 @@ pytest | |
path.py | ||
pytest-cov | ||
mock | ||
pytest-mock | ||
setuptools | ||
pip |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
class AWSBase(object): | ||
def __init__(self, session): | ||
self.session = session | ||
|
||
def cf_client(self): | ||
return self.session.client('cloudformation') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import collections | ||
import re | ||
from deepdiff import DeepDiff | ||
from texttable import Texttable | ||
|
||
from formica.aws_base import AWSBase | ||
from formica.loader import Loader | ||
import click | ||
|
||
try: | ||
basestring | ||
except NameError: | ||
basestring = str | ||
|
||
|
||
class Change(): | ||
def __init__(self, path, before, after, type): | ||
self.path = path | ||
self.before = before | ||
self.after = after | ||
self.type = type | ||
|
||
|
||
def convert(data): | ||
if isinstance(data, basestring): | ||
return str(data) | ||
elif isinstance(data, collections.Mapping): | ||
return dict(map(convert, data.items())) | ||
elif isinstance(data, collections.Iterable): | ||
return type(data)(map(convert, data)) | ||
else: | ||
return data | ||
|
||
|
||
class Diff(AWSBase): | ||
def __init__(self, session): | ||
super(Diff, self).__init__(session) | ||
|
||
def run(self, stack): | ||
client = self.cf_client() | ||
|
||
result = client.get_template( | ||
StackName=stack, | ||
) | ||
|
||
loader = Loader() | ||
loader.load() | ||
|
||
changes = DeepDiff(convert(result['TemplateBody']), convert(loader.template_dictionary()), ignore_order=False, | ||
report_repetition=True, | ||
verbose_level=2, view='tree') | ||
|
||
table = Texttable(max_width=200) | ||
table.add_rows([['Path', 'From', 'To', 'Change Type']]) | ||
print_diff = False | ||
|
||
processed_changes = self.__collect_changes(changes) | ||
|
||
for change in processed_changes: | ||
print_diff = True | ||
path = re.findall("\['?(\w+)'?\]", change.path) | ||
table.add_row( | ||
[ | ||
' > '.join(path), | ||
change.before, | ||
change.after, | ||
change.type.title().replace('_', ' ') | ||
] | ||
) | ||
|
||
if print_diff: | ||
click.echo(table.draw() + "\n") | ||
else: | ||
click.echo('No Changes found') | ||
|
||
def __collect_changes(self, changes): | ||
results = [] | ||
for key, value in changes.items(): | ||
for change in list(value): | ||
results.append(Change(path=change.path(), before=change.t1, after=change.t2, type=key)) | ||
return sorted(results, key=lambda x: x.path) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import pytest | ||
import re | ||
from click.testing import CliRunner | ||
from formica import cli | ||
|
||
from formica.diff import Diff | ||
from tests.unit.constants import STACK | ||
|
||
|
||
@pytest.fixture | ||
def client(mocker): | ||
return mocker.Mock() | ||
|
||
|
||
@pytest.fixture | ||
def session(client, mocker): | ||
mock = mocker.Mock() | ||
mock.client.return_value = client | ||
return mock | ||
|
||
|
||
@pytest.fixture | ||
def diff(session): | ||
return Diff(session) | ||
|
||
|
||
@pytest.fixture | ||
def loader(mocker): | ||
return mocker.patch('formica.diff.Loader') | ||
|
||
|
||
def template_return(client, template): | ||
client.get_template.return_value = {'TemplateBody': template} | ||
|
||
|
||
def loader_return(loader, template): | ||
loader.return_value.template_dictionary.return_value = template | ||
|
||
|
||
def check_echo(click, args): | ||
print(click.echo.call_args[0][0]) | ||
regex = '\s+\|\s+'.join(args) | ||
assert re.search(regex, click.echo.call_args[0][0]) | ||
|
||
|
||
@pytest.fixture | ||
def click(mocker): | ||
return mocker.patch('formica.diff.click') | ||
|
||
|
||
def test_unicode_string_no_diff(loader, client, diff, click): | ||
loader_return(loader, {'Resources': u'1234'}) | ||
template_return(client, {'Resources': '1234'}) | ||
diff.run(STACK) | ||
click.echo.assert_called_with('No Changes found') | ||
|
||
|
||
def test_values_changed(loader, client, diff, click): | ||
template_return(client, {'Resources': '1234'}) | ||
loader_return(loader, {'Resources': '5678'}) | ||
diff.run(STACK) | ||
check_echo(click, ['Resources', '1234', '5678', 'Values Changed']) | ||
|
||
|
||
def test_dictionary_item_added(loader, client, diff, click): | ||
loader_return(loader, {'Resources': '5678'}) | ||
template_return(client, {}) | ||
diff.run(STACK) | ||
check_echo(click, ['Resources', 'Not Present', '5678', 'Dictionary Item Added']) | ||
|
||
|
||
def test_dictionary_item_removed(loader, client, diff, click): | ||
loader_return(loader, {}) | ||
template_return(client, {'Resources': '5678'}) | ||
diff.run(STACK) | ||
check_echo(click, ['Resources', '5678', 'Not Present', 'Dictionary Item Removed']) | ||
|
||
|
||
def test_type_changed(loader, client, diff, click): | ||
template_return(client, {'Resources': 'abcde'}) | ||
loader_return(loader, {'Resources': 5}) | ||
diff.run(STACK) | ||
check_echo(click, ['Resources', 'abcde', '5', 'Type Changes']) | ||
|
||
|
||
def test_iterable_item_added(loader, client, diff, click): | ||
template_return(client, {'Resources': [1]}) | ||
loader_return(loader, {'Resources': [1, 2]}) | ||
diff.run(STACK) | ||
check_echo(click, ['Resources > 1', 'Not Present', '2', 'Iterable Item Added']) | ||
|
||
|
||
def test_iterable_item_removed(loader, client, diff, click): | ||
template_return(client, {'Resources': [1, 2]}) | ||
loader_return(loader, {'Resources': [1]}) | ||
diff.run(STACK) | ||
check_echo(click, ['Resources > 1', '2', 'Not Present', 'Iterable Item Removed']) | ||
|
||
|
||
def test_diff_cli_call(mocker, session): | ||
aws = mocker.patch('formica.cli.AWS') | ||
aws.current_session.return_value = session | ||
print(session) | ||
|
||
diff = mocker.patch('formica.cli.Diff') | ||
|
||
result = CliRunner().invoke(cli.diff, ['--stack', STACK]) | ||
|
||
assert result.exit_code == 0 | ||
|
||
diff.assert_called_with(session) | ||
diff.return_value.run.assert_called_with(STACK) |