Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Yaml tests #606

Merged
merged 1 commit into from
Jun 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/blueprints.rst
Original file line number Diff line number Diff line change
Expand Up @@ -411,3 +411,33 @@ stacker_blueprints repo. For example, see the tests used to test the
.. _output results: https://github.com/cloudtools/stacker_blueprints/tree/master/tests/fixtures/blueprints
.. _Resource Type: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html
.. _Property Type: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-product-property-reference.html

Yaml (stacker) format tests
---------------------------

In order to wrap the `BlueprintTestCase` tests in a format similar to stacker's
stack format, the `YamlDirTestGenerator` class is provided. When subclassed in
a directory, it will search for yaml files in that directory with certain
structure and execute a test case for it. As an example:

.. code-block:: yaml
---
namespace: test
stacks:
- name: test_stack
class_path: stacker_blueprints.s3.Buckets
variables:
var1: val1

When run from nosetests, this will create a template fixture file called
test_stack.json containing the output from the `stacker_blueprints.s3.Buckets`
template.

Examples of using the `YamlDirTestGenerator` class can be found in the
stacker_blueprints repo. For example, see the tests used to test the
`s3.Buckets`_ class and the accompanying `fixture`_. These are
generated from a `subclass of YamlDirTestGenerator`_.

.. _s3.Buckets: https://github.com/cloudtools/stacker_blueprints/blob/yaml-tests/tests/test_s3.yaml
.. _fixture: https://github.com/cloudtools/stacker_blueprints/tree/yaml-tests/tests/fixtures/blueprints/s3_static_website.json
.. _subclass of YamlDirTestGenerator: https://github.com/cloudtools/stacker_blueprints/tree/yaml-tests/tests/__init__.py
122 changes: 122 additions & 0 deletions stacker/blueprints/testutil.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import difflib
import json
import unittest
import os.path
from glob import glob

from stacker.config import parse as parse_config
from stacker.context import Context
from stacker.util import load_object_from_string
from stacker.variables import Variable


def diff(a, b):
Expand Down Expand Up @@ -33,3 +40,118 @@ def assertRenderedBlueprint(self, blueprint): # noqa: N802

self.assertEquals(rendered_dict, expected_dict,
diff(rendered_text, expected_text))


class YamlDirTestGenerator(object):
"""Generate blueprint tests from yaml config files.

This class creates blueprint tests from yaml files with a syntax similar to
stackers' configuration syntax. For example,

---
namespace: test
stacks:
- name: test_sample
class_path: stacker_blueprints.test.Sample
variables:
var1: value1

will create a test for the specified blueprint, passing that variable as
part of the test.

The test will generate a .json file for this blueprint, and compare it with
the stored result.


By default, the generator looks for files named 'test_*.yaml' in its same
directory. In order to use it, subclass it in a directory containing such
tests, and name the class with a pattern that will include it in nosetests'
tests (for example, TestGenerator).

The subclass may override some properties:

@property base_class: by default, the generated tests are subclasses of
stacker.blueprints.testutil.BlueprintTestCase. In order to change this,
set this property to the desired base class.

@property yaml_dirs: by default, the directory where the generator is
subclassed is searched for test files. Override this array for specifying
more directories. These must be relative to the directory in which the
subclass lives in. Globs may be used.
Default: [ '.' ]. Example override: [ '.', 'tests/*/' ]

@property yaml_filename: by default, the generator looks for files named
'test_*.yaml'. Use this to change this pattern. Globs may be used.


There's an example of this use in the tests/ subdir of stacker_blueprints.

"""

def __init__(self):
self.classdir = os.path.relpath(
self.__class__.__module__.replace('.', '/'))
if not os.path.isdir(self.classdir):
self.classdir = os.path.dirname(self.classdir)

# These properties can be overriden from the test generator subclass.
@property
def base_class(self):
return BlueprintTestCase

@property
def yaml_dirs(self):
return ['.']

@property
def yaml_filename(self):
return 'test_*.yaml'

def test_generator(self):
# Search for tests in given paths
configs = []
for d in self.yaml_dirs:
configs.extend(
glob('%s/%s/%s' % (self.classdir, d, self.yaml_filename)))

class ConfigTest(self.base_class):
def __init__(self, config, stack, filepath):
self.config = config
self.stack = stack
self.description = "%s (%s)" % (stack.name, filepath)

def __call__(self):
# Use the context property of the baseclass, if present.
# If not, default to a basic context.
try:
ctx = self.context
except AttributeError:
ctx = Context(config=self.config,
environment={'environment': 'test'})

configvars = self.stack.variables or {}
variables = [Variable(k, v) for k, v in configvars.iteritems()]

blueprint_class = load_object_from_string(
self.stack.class_path)
blueprint = blueprint_class(self.stack.name, ctx)
blueprint.resolve_variables(variables or [])
blueprint.setup_parameters()
blueprint.create_template()
self.assertRenderedBlueprint(blueprint)

def assertEquals(self, a, b, msg): # noqa: N802
assert a == b, msg

for f in configs:
with open(f) as test:
config = parse_config(test.read())
config.validate()

for stack in config.stacks:
# Nosetests supports "test generators", which allows us to
# yield a callable object which will be wrapped as a test
# case.
#
# http://nose.readthedocs.io/en/latest/writing_tests.html#test-generators
yield ConfigTest(config, stack, filepath=f)