From 517d00fb8a749b6d912e1dab29fea9c28b1136da Mon Sep 17 00:00:00 2001 From: Chris Alfonso Date: Tue, 17 Apr 2012 15:55:36 -0400 Subject: [PATCH 1/8] Adding resource initialization test --- heat/tests/examples/test3.py | 2 +- heat/tests/test_resources.py | 55 ++++++++++++++++++++++++++++++++++++ run_tests.py | 1 + 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 heat/tests/test_resources.py diff --git a/heat/tests/examples/test3.py b/heat/tests/examples/test3.py index 714e18cd8d..22d1286249 100644 --- a/heat/tests/examples/test3.py +++ b/heat/tests/examples/test3.py @@ -11,7 +11,7 @@ from nose.plugins.attrib import attr # sets attribute on all test methods -@attr(tag=['example', 'unittest']) +@attr(tag=['example', 'unit']) @attr(speed='fast') class ExampleTest(unittest.TestCase): def test_a(self): diff --git a/heat/tests/test_resources.py b/heat/tests/test_resources.py new file mode 100644 index 0000000000..2688eb25d4 --- /dev/null +++ b/heat/tests/test_resources.py @@ -0,0 +1,55 @@ +import sys + +import os +import nose +import unittest +import mox +from nose.plugins.attrib import attr +from nose import with_setup +from tests.v1_1 import fakes +from heat.engine import resources +from heat.common import config +import heat.db as db_api + +@attr(tag=['unit', 'resource']) +@attr(speed='fast') +class ResourcesTest(unittest.TestCase): + _mox = None + + def setUp(self): + cs = fakes.FakeClient() + self._mox = mox.Mox() + sql_connection = 'sqlite://heat.db' + conf = config.HeatEngineConfigOpts() + db_api.configure(conf) + + def tearDown(self): + print "ResourcesTest teardown complete" + + def test_initialize_resource_from_template(self): + f = open('templates/WordPress_Single_Instance_gold.template') + t = f.read() + f.close() + + stack = self._mox.CreateMockAnything() + stack.id().AndReturn(1) + + self._mox.StubOutWithMock(stack, 'resolve_static_refs') + stack.resolve_static_refs(t).AndReturn(t) + + self._mox.StubOutWithMock(stack, 'resolve_find_in_map') + stack.resolve_find_in_map(t).AndReturn(t) + + self._mox.StubOutWithMock(db_api, 'resource_get_by_name_and_stack') + db_api.resource_get_by_name_and_stack(None, 'test_resource_name', stack).AndReturn(None) + + self._mox.ReplayAll() + resource = resources.Resource('test_resource_name', t, stack) + + assert isinstance(resource, resources.Resource) + + # allows testing of the test directly, shown below + if __name__ == '__main__': + sys.argv.append(__file__) + nose.main() + diff --git a/run_tests.py b/run_tests.py index 73b185252d..65fc500820 100644 --- a/run_tests.py +++ b/run_tests.py @@ -42,6 +42,7 @@ import os import unittest import sys +sys.path.append(os.environ['PYTHON_NOVACLIENT_SRC']) gettext.install('heat', unicode=1) from nose import config From 9c69836bfd49e2b8c7b84d0de9c44aa5c852373e Mon Sep 17 00:00:00 2001 From: Chris Alfonso Date: Wed, 18 Apr 2012 09:03:17 -0400 Subject: [PATCH 2/8] Adding unit test for instance creation --- heat/tests/test_resources.py | 27 +++++++++++++++++++++++++-- run_tests.py | 1 - 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/heat/tests/test_resources.py b/heat/tests/test_resources.py index 2688eb25d4..7d4822945d 100644 --- a/heat/tests/test_resources.py +++ b/heat/tests/test_resources.py @@ -1,11 +1,13 @@ import sys - import os +sys.path.append(os.environ['PYTHON_NOVACLIENT_SRC']) + import nose import unittest import mox from nose.plugins.attrib import attr from nose import with_setup + from tests.v1_1 import fakes from heat.engine import resources from heat.common import config @@ -24,6 +26,7 @@ class ResourcesTest(unittest.TestCase): db_api.configure(conf) def tearDown(self): + self._mox.UnsetStubs() print "ResourcesTest teardown complete" def test_initialize_resource_from_template(self): @@ -45,9 +48,29 @@ class ResourcesTest(unittest.TestCase): self._mox.ReplayAll() resource = resources.Resource('test_resource_name', t, stack) - assert isinstance(resource, resources.Resource) + def test_initialize_instance_from_template(self): + f = open('templates/WordPress_Single_Instance_gold.template') + t = f.read() + f.close() + + stack = self._mox.CreateMockAnything() + stack.id().AndReturn(1) + + self._mox.StubOutWithMock(stack, 'resolve_static_refs') + stack.resolve_static_refs(t).AndReturn(t) + + self._mox.StubOutWithMock(stack, 'resolve_find_in_map') + stack.resolve_find_in_map(t).AndReturn(t) + + self._mox.StubOutWithMock(db_api, 'resource_get_by_name_and_stack') + db_api.resource_get_by_name_and_stack(None, 'test_resource_name', stack).AndReturn(None) + + self._mox.ReplayAll() + instance = resources.Instance('test_resource_name', t, stack) + + # allows testing of the test directly, shown below if __name__ == '__main__': sys.argv.append(__file__) diff --git a/run_tests.py b/run_tests.py index 65fc500820..73b185252d 100644 --- a/run_tests.py +++ b/run_tests.py @@ -42,7 +42,6 @@ import os import unittest import sys -sys.path.append(os.environ['PYTHON_NOVACLIENT_SRC']) gettext.install('heat', unicode=1) from nose import config From bdaed9b99735b5e221d423f5e53a5113b1512b99 Mon Sep 17 00:00:00 2001 From: Chris Alfonso Date: Thu, 19 Apr 2012 18:10:20 -0400 Subject: [PATCH 3/8] Added db setup and teardown with sqlite --- .gitignore | 1 + heat/common/config.py | 2 - heat/db/api.py | 19 +- heat/db/migration.py | 31 +++ heat/db/sqlalchemy/migration.py | 111 ++++++++++ heat/engine/resources.py | 3 - heat/testing/README.rst | 60 ++++++ heat/testing/__init__.py | 0 heat/testing/fake/__init__.py | 1 + heat/testing/runner.py | 362 ++++++++++++++++++++++++++++++++ heat/tests/__init__.py | 24 +++ heat/tests/test_resources.py | 81 +++---- heat/utils.py | 26 +++ run_tests.sh | 13 +- 14 files changed, 673 insertions(+), 61 deletions(-) create mode 100644 heat/db/migration.py create mode 100644 heat/db/sqlalchemy/migration.py create mode 100644 heat/testing/README.rst create mode 100644 heat/testing/__init__.py create mode 100644 heat/testing/fake/__init__.py create mode 100644 heat/testing/runner.py create mode 100644 heat/utils.py diff --git a/.gitignore b/.gitignore index c4910959f5..e7ef156551 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ heat.egg-info heat/vcsversion.py tags *.log +heat/tests/heat-test.db diff --git a/heat/common/config.py b/heat/common/config.py index 252cbe90cd..249a2badc9 100644 --- a/heat/common/config.py +++ b/heat/common/config.py @@ -178,8 +178,6 @@ class HeatEngineConfigOpts(cfg.CommonConfigOpts): help='port for os volume api to listen'), ] db_opts = [ - cfg.StrOpt('db_backend', default='heat.db.sqlalchemy.api', - help='The backend to use for db'), cfg.StrOpt('sql_connection', default='mysql://heat:heat@localhost/heat', help='The SQLAlchemy connection string used to connect to the ' diff --git a/heat/db/api.py b/heat/db/api.py index 3760a3c142..20cd2404b0 100644 --- a/heat/db/api.py +++ b/heat/db/api.py @@ -25,19 +25,32 @@ Usage: The underlying driver is loaded . SQLAlchemy is currently the only supported backend. ''' - +import heat.utils from heat.openstack.common import utils +from heat.openstack.common import cfg +from heat.common import config +import heat.utils +SQL_CONNECTION = 'sqlite:///heat-test.db/' +SQL_IDLE_TIMEOUT = 3600 +db_opts = [ + cfg.StrOpt('db_backend', + default='sqlalchemy', + help='The backend to use for db'), + ] +conf = config.HeatEngineConfigOpts() +conf.db_backend = 'heat.db.sqlalchemy.api' +IMPL = heat.utils.LazyPluggable('db_backend', + sqlalchemy='heat.db.sqlalchemy.api') def configure(conf): - global IMPL global SQL_CONNECTION global SQL_IDLE_TIMEOUT - IMPL = utils.import_object(conf.db_backend) SQL_CONNECTION = conf.sql_connection SQL_IDLE_TIMEOUT = conf.sql_idle_timeout + def raw_template_get(context, template_id): return IMPL.raw_template_get(context, template_id) diff --git a/heat/db/migration.py b/heat/db/migration.py new file mode 100644 index 0000000000..610ef25560 --- /dev/null +++ b/heat/db/migration.py @@ -0,0 +1,31 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +"""Database setup and migration commands.""" + +from heat import utils + + +IMPL = utils.LazyPluggable('db_backend', + sqlalchemy='heat.db.sqlalchemy.migration') + + +def db_sync(version=None): + """Migrate the database to `version` or the most recent version.""" + return IMPL.db_sync(version=version) + + +def db_version(): + """Display the current database version.""" + return IMPL.db_version() diff --git a/heat/db/sqlalchemy/migration.py b/heat/db/sqlalchemy/migration.py new file mode 100644 index 0000000000..3d5259ff80 --- /dev/null +++ b/heat/db/sqlalchemy/migration.py @@ -0,0 +1,111 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 distutils.version as dist_version +import os +import sys +from heat.db.sqlalchemy.session import get_engine + +import sqlalchemy +import migrate +from migrate.versioning import util as migrate_util + +_REPOSITORY = None + +@migrate_util.decorator +def patched_with_engine(f, *a, **kw): + url = a[0] + engine = migrate_util.construct_engine(url, **kw) + try: + kw['engine'] = engine + return f(*a, **kw) + finally: + if isinstance(engine, migrate_util.Engine) and engine is not url: + migrate_util.log.debug('Disposing SQLAlchemy engine %s', engine) + engine.dispose() + + +# TODO(jkoelker) When migrate 0.7.3 is released and nova depends +# on that version or higher, this can be removed +MIN_PKG_VERSION = dist_version.StrictVersion('0.7.3') +if (not hasattr(migrate, '__version__') or + dist_version.StrictVersion(migrate.__version__) < MIN_PKG_VERSION): + migrate_util.with_engine = patched_with_engine + + +# NOTE(jkoelker) Delay importing migrate until we are patched +from migrate.versioning import api as versioning_api +from migrate.versioning.repository import Repository + +try: + from migrate.versioning import exceptions as versioning_exceptions +except ImportError: + try: + from migrate import exceptions as versioning_exceptions + except ImportError: + sys.exit(_("python-migrate is not installed. Exiting.")) + +#_REPOSITORY = None + + +def db_sync(version=None): + if version is not None: + try: + version = int(version) + except ValueError: + raise exception.Error(_("version should be an integer")) + current_version = db_version() + repository = _find_migrate_repo() + if version is None or version > current_version: + return versioning_api.upgrade(get_engine(), repository, version) + else: + return versioning_api.downgrade(get_engine(), repository, + version) + + +def db_version(): + repository = _find_migrate_repo() + try: + return versioning_api.db_version(get_engine(), repository) + except versioning_exceptions.DatabaseNotControlledError: + # If we aren't version controlled we may already have the database + # in the state from before we started version control, check for that + # and set up version_control appropriately + meta = sqlalchemy.MetaData() + engine = get_engine() + meta.reflect(bind=engine) + try: + for table in ('stack', 'resource', 'event', + 'parsed_template', 'raw_template'): + assert table in meta.tables + return db_version_control(1) + except AssertionError: + return db_version_control(0) + + +def db_version_control(version=None): + repository = _find_migrate_repo() + versioning_api.version_control(get_engine(), repository, version) + return version + + +def _find_migrate_repo(): + """Get the path for the migrate repository.""" + path = os.path.join(os.path.abspath(os.path.dirname(__file__)), + 'migrate_repo') + assert os.path.exists(path) + global _REPOSITORY + if _REPOSITORY is None: + _REPOSITORY = Repository(path) + return _REPOSITORY diff --git a/heat/engine/resources.py b/heat/engine/resources.py index fa4ed19265..4ca18d5b46 100644 --- a/heat/engine/resources.py +++ b/heat/engine/resources.py @@ -20,7 +20,6 @@ import os import string import json import sys - from email import encoders from email.message import Message from email.mime.base import MIMEBase @@ -78,7 +77,6 @@ class Resource(object): self.instance_id = None self.state = None self.id = None - self._nova = {} if not 'Properties' in self.t: # make a dummy entry to prevent having to check all over the @@ -601,7 +599,6 @@ class Instance(Resource): msg = MIMEText(userdata, _subtype='x-shellscript') msg.add_header('Content-Disposition', 'attachment', filename='startup') mime_blob.attach(msg) - server = self.nova().servers.create(name=self.name, image=image_id, flavor=flavor_id, key_name=key_name, diff --git a/heat/testing/README.rst b/heat/testing/README.rst new file mode 100644 index 0000000000..5723940526 --- /dev/null +++ b/heat/testing/README.rst @@ -0,0 +1,60 @@ +===================================== +Heat Testing Infrastructure +===================================== + +A note of clarification is in order, to help those who are new to testing in +Heat: + +- actual unit tests are created in the "tests" directory; +- the "testing" directory is used to house the infrastructure needed to support + testing in Heat. + +This README file attempts to provide current and prospective contributors with +everything they need to know in order to start creating unit tests and +utilizing the convenience code provided in nova.testing. + + +Test Types: Unit vs. Functional vs. Integration +----------------------------------------------- + +TBD + +Writing Unit Tests +------------------ + +TBD + +Using Fakes +~~~~~~~~~~~ + +TBD + +test.TestCase +------------- +The TestCase class from heat.test (generally imported as test) will +automatically manage self.stubs using the stubout module and self.mox +using the mox module during the setUp step. They will automatically +verify and clean up during the tearDown step. + +If using test.TestCase, calling the super class setUp is required and +calling the super class tearDown is required to be last if tearDown +is overriden. + +Writing Functional Tests +------------------------ + +TBD + +Writing Integration Tests +------------------------- + +TBD + +Tests and assertRaises +---------------------- +When asserting that a test should raise an exception, test against the +most specific exception possible. An overly broad exception type (like +Exception) can mask errors in the unit test itself. + +Example:: +TBD diff --git a/heat/testing/__init__.py b/heat/testing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/heat/testing/fake/__init__.py b/heat/testing/fake/__init__.py new file mode 100644 index 0000000000..5cdad4717e --- /dev/null +++ b/heat/testing/fake/__init__.py @@ -0,0 +1 @@ +import rabbit diff --git a/heat/testing/runner.py b/heat/testing/runner.py new file mode 100644 index 0000000000..e63049910e --- /dev/null +++ b/heat/testing/runner.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +# Colorizer Code is borrowed from Twisted: +# Copyright (c) 2001-2010 Twisted Matrix Laboratories. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Unittest runner for Heat. + +To run all tests + python heat/testing/runner.py + +To run a single test module: + python heat/testing/runner.py test_resources + + +To run a single test: + python heat/testing/runner.py + test_resources:ResourceTestCase.test_resource_from_template + +""" + +import gettext +import heapq +import os +import unittest +import sys +import time + +import eventlet +from nose import config +from nose import core +from nose import result + +gettext.install('heat', unicode=1) +reldir = os.path.join(os.path.dirname(__file__), '..', '..') +absdir = os.path.abspath(reldir) +sys.path.insert(0, absdir) + +from heat.openstack.common import cfg + + +class _AnsiColorizer(object): + """ + A colorizer is an object that loosely wraps around a stream, allowing + callers to write text to the stream in a particular color. + + Colorizer classes must implement C{supported()} and C{write(text, color)}. + """ + _colors = dict(black=30, red=31, green=32, yellow=33, + blue=34, magenta=35, cyan=36, white=37) + + def __init__(self, stream): + self.stream = stream + + def supported(cls, stream=sys.stdout): + """ + A class method that returns True if the current platform supports + coloring terminal output using this method. Returns False otherwise. + """ + if not stream.isatty(): + return False # auto color only on TTYs + try: + import curses + except ImportError: + return False + else: + try: + try: + return curses.tigetnum("colors") > 2 + except curses.error: + curses.setupterm() + return curses.tigetnum("colors") > 2 + except Exception: + raise + # guess false in case of error + return False + supported = classmethod(supported) + + def write(self, text, color): + """ + Write the given text to the stream in the given color. + + @param text: Text to be written to the stream. + + @param color: A string label for a color. e.g. 'red', 'white'. + """ + color = self._colors[color] + self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text)) + + +class _Win32Colorizer(object): + """ + See _AnsiColorizer docstring. + """ + def __init__(self, stream): + import win32console as win + red, green, blue, bold = (win.FOREGROUND_RED, win.FOREGROUND_GREEN, + win.FOREGROUND_BLUE, win.FOREGROUND_INTENSITY) + self.stream = stream + self.screenBuffer = win.GetStdHandle(win.STD_OUT_HANDLE) + self._colors = { + 'normal': red | green | blue, + 'red': red | bold, + 'green': green | bold, + 'blue': blue | bold, + 'yellow': red | green | bold, + 'magenta': red | blue | bold, + 'cyan': green | blue | bold, + 'white': red | green | blue | bold + } + + def supported(cls, stream=sys.stdout): + try: + import win32console + screenBuffer = win32console.GetStdHandle( + win32console.STD_OUT_HANDLE) + except ImportError: + return False + import pywintypes + try: + screenBuffer.SetConsoleTextAttribute( + win32console.FOREGROUND_RED | + win32console.FOREGROUND_GREEN | + win32console.FOREGROUND_BLUE) + except pywintypes.error: + return False + else: + return True + supported = classmethod(supported) + + def write(self, text, color): + color = self._colors[color] + self.screenBuffer.SetConsoleTextAttribute(color) + self.stream.write(text) + self.screenBuffer.SetConsoleTextAttribute(self._colors['normal']) + + +class _NullColorizer(object): + """ + See _AnsiColorizer docstring. + """ + def __init__(self, stream): + self.stream = stream + + def supported(cls, stream=sys.stdout): + return True + supported = classmethod(supported) + + def write(self, text, color): + self.stream.write(text) + + +def get_elapsed_time_color(elapsed_time): + if elapsed_time > 1.0: + return 'red' + elif elapsed_time > 0.25: + return 'yellow' + else: + return 'green' + + +class HeatTestResult(result.TextTestResult): + def __init__(self, *args, **kw): + self.show_elapsed = kw.pop('show_elapsed') + result.TextTestResult.__init__(self, *args, **kw) + self.num_slow_tests = 5 + self.slow_tests = [] # this is a fixed-sized heap + self._last_case = None + self.colorizer = None + # NOTE(vish): reset stdout for the terminal check + stdout = sys.stdout + sys.stdout = sys.__stdout__ + for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]: + if colorizer.supported(): + self.colorizer = colorizer(self.stream) + break + sys.stdout = stdout + + # NOTE(lorinh): Initialize start_time in case a sqlalchemy-migrate + # error results in it failing to be initialized later. Otherwise, + # _handleElapsedTime will fail, causing the wrong error message to + # be outputted. + self.start_time = time.time() + + def getDescription(self, test): + return str(test) + + def _handleElapsedTime(self, test): + self.elapsed_time = time.time() - self.start_time + item = (self.elapsed_time, test) + # Record only the n-slowest tests using heap + if len(self.slow_tests) >= self.num_slow_tests: + heapq.heappushpop(self.slow_tests, item) + else: + heapq.heappush(self.slow_tests, item) + + def _writeElapsedTime(self, test): + color = get_elapsed_time_color(self.elapsed_time) + self.colorizer.write(" %.2f" % self.elapsed_time, color) + + def _writeResult(self, test, long_result, color, short_result, success): + if self.showAll: + self.colorizer.write(long_result, color) + if self.show_elapsed and success: + self._writeElapsedTime(test) + self.stream.writeln() + elif self.dots: + self.stream.write(short_result) + self.stream.flush() + + # NOTE(vish): copied from unittest with edit to add color + def addSuccess(self, test): + unittest.TestResult.addSuccess(self, test) + self._handleElapsedTime(test) + self._writeResult(test, 'OK', 'green', '.', True) + + # NOTE(vish): copied from unittest with edit to add color + def addFailure(self, test, err): + unittest.TestResult.addFailure(self, test, err) + self._handleElapsedTime(test) + self._writeResult(test, 'FAIL', 'red', 'F', False) + + # NOTE(vish): copied from nose with edit to add color + def addError(self, test, err): + """Overrides normal addError to add support for + errorClasses. If the exception is a registered class, the + error will be added to the list for that class, not errors. + """ + self._handleElapsedTime(test) + stream = getattr(self, 'stream', None) + ec, ev, tb = err + try: + exc_info = self._exc_info_to_string(err, test) + except TypeError: + # 2.3 compat + exc_info = self._exc_info_to_string(err) + for cls, (storage, label, isfail) in self.errorClasses.items(): + if result.isclass(ec) and issubclass(ec, cls): + if isfail: + test.passed = False + storage.append((test, exc_info)) + # Might get patched into a streamless result + if stream is not None: + if self.showAll: + message = [label] + detail = result._exception_detail(err[1]) + if detail: + message.append(detail) + stream.writeln(": ".join(message)) + elif self.dots: + stream.write(label[:1]) + return + self.errors.append((test, exc_info)) + test.passed = False + if stream is not None: + self._writeResult(test, 'ERROR', 'red', 'E', False) + + def startTest(self, test): + unittest.TestResult.startTest(self, test) + self.start_time = time.time() + current_case = test.test.__class__.__name__ + + if self.showAll: + if current_case != self._last_case: + self.stream.writeln(current_case) + self._last_case = current_case + + self.stream.write( + ' %s' % str(test.test._testMethodName).ljust(60)) + self.stream.flush() + + +class HeatTestRunner(core.TextTestRunner): + def __init__(self, *args, **kwargs): + self.show_elapsed = kwargs.pop('show_elapsed') + core.TextTestRunner.__init__(self, *args, **kwargs) + + def _makeResult(self): + return HeatTestResult(self.stream, + self.descriptions, + self.verbosity, + self.config, + show_elapsed=self.show_elapsed) + + def _writeSlowTests(self, result_): + # Pare out 'fast' tests + slow_tests = [item for item in result_.slow_tests + if get_elapsed_time_color(item[0]) != 'green'] + if slow_tests: + slow_total_time = sum(item[0] for item in slow_tests) + self.stream.writeln("Slowest %i tests took %.2f secs:" + % (len(slow_tests), slow_total_time)) + for elapsed_time, test in sorted(slow_tests, reverse=True): + time_str = "%.2f" % elapsed_time + self.stream.writeln(" %s %s" % (time_str.ljust(10), test)) + + def run(self, test): + result_ = core.TextTestRunner.run(self, test) + if self.show_elapsed: + self._writeSlowTests(result_) + return result_ + + +def run(): + # This is a fix to allow the --hide-elapsed flag while accepting + # arbitrary nosetest flags as well + argv = [x for x in sys.argv if x != '--hide-elapsed'] + hide_elapsed = argv != sys.argv + + # If any argument looks like a test name but doesn't have "heat.tests" in + # front of it, automatically add that so we don't have to type as much + for i, arg in enumerate(argv): + if arg.startswith('test_'): + argv[i] = 'heat.tests.%s' % arg + + testdir = os.path.abspath(os.path.join("heat", "tests")) + c = config.Config(stream=sys.stdout, + env=os.environ, + verbosity=3, + workingDir=testdir, + plugins=core.DefaultPluginManager()) + + runner = HeatTestRunner(stream=c.stream, + verbosity=c.verbosity, + config=c, + show_elapsed=not hide_elapsed) + sys.exit(not core.run(config=c, testRunner=runner, argv=argv)) + + +if __name__ == '__main__': + eventlet.monkey_patch() + run() diff --git a/heat/tests/__init__.py b/heat/tests/__init__.py index d89db057e1..0023e41aaf 100644 --- a/heat/tests/__init__.py +++ b/heat/tests/__init__.py @@ -16,3 +16,27 @@ # The code below enables nosetests to work with i18n _() blocks import __builtin__ setattr(__builtin__, '_', lambda x: x) + +import os +import shutil + +from heat.db.sqlalchemy.session import get_engine + +_DB = None + +def reset_db(): + engine = get_engine() + engine.dispose() + conn = engine.connect() + conn.connection.executescript(_DB) + +def setup(): + import mox # Fail fast if you don't have mox. Workaround for bug 810424 + + from heat import db + from heat.db import migration + + migration.db_sync() + engine = get_engine() + conn = engine.connect() +# _DB = "".join(line for line in conn.connection.dump()) diff --git a/heat/tests/test_resources.py b/heat/tests/test_resources.py index 7d4822945d..c8f8841245 100644 --- a/heat/tests/test_resources.py +++ b/heat/tests/test_resources.py @@ -5,73 +5,60 @@ sys.path.append(os.environ['PYTHON_NOVACLIENT_SRC']) import nose import unittest import mox +import json +import sqlalchemy + from nose.plugins.attrib import attr from nose import with_setup from tests.v1_1 import fakes from heat.engine import resources -from heat.common import config import heat.db as db_api +from heat.engine import parser @attr(tag=['unit', 'resource']) @attr(speed='fast') class ResourcesTest(unittest.TestCase): - _mox = None - def setUp(self): - cs = fakes.FakeClient() - self._mox = mox.Mox() - sql_connection = 'sqlite://heat.db' - conf = config.HeatEngineConfigOpts() - db_api.configure(conf) + self.m = mox.Mox() + self.cs = fakes.FakeClient() def tearDown(self): - self._mox.UnsetStubs() - print "ResourcesTest teardown complete" - - def test_initialize_resource_from_template(self): - f = open('templates/WordPress_Single_Instance_gold.template') - t = f.read() - f.close() - - stack = self._mox.CreateMockAnything() - stack.id().AndReturn(1) - - self._mox.StubOutWithMock(stack, 'resolve_static_refs') - stack.resolve_static_refs(t).AndReturn(t) - - self._mox.StubOutWithMock(stack, 'resolve_find_in_map') - stack.resolve_find_in_map(t).AndReturn(t) - - self._mox.StubOutWithMock(db_api, 'resource_get_by_name_and_stack') - db_api.resource_get_by_name_and_stack(None, 'test_resource_name', stack).AndReturn(None) - - self._mox.ReplayAll() - resource = resources.Resource('test_resource_name', t, stack) - assert isinstance(resource, resources.Resource) + self.m.UnsetStubs() def test_initialize_instance_from_template(self): - f = open('templates/WordPress_Single_Instance_gold.template') - t = f.read() + f = open('../../templates/WordPress_Single_Instance_gold.template') + t = json.loads(f.read()) f.close() - stack = self._mox.CreateMockAnything() - stack.id().AndReturn(1) + params = {} + parameters = {} + params['KeyStoneCreds'] = None + t['Parameters']['KeyName']['Value'] = 'test' + stack = parser.Stack('test_stack', t, 0, params) + + self.m.StubOutWithMock(db_api, 'resource_get_by_name_and_stack') + db_api.resource_get_by_name_and_stack(None, 'test_resource_name',\ + stack).AndReturn(None) + + self.m.StubOutWithMock(resources.Instance, 'nova') + resources.Instance.nova().AndReturn(self.cs) + resources.Instance.nova().AndReturn(self.cs) + resources.Instance.nova().AndReturn(self.cs) + resources.Instance.nova().AndReturn(self.cs) + - self._mox.StubOutWithMock(stack, 'resolve_static_refs') - stack.resolve_static_refs(t).AndReturn(t) - - self._mox.StubOutWithMock(stack, 'resolve_find_in_map') - stack.resolve_find_in_map(t).AndReturn(t) + print self.cs.flavors.list()[0].name + self.m.ReplayAll() + t['Resources']['WebServer']['Properties']['ImageId'] = 'CentOS 5.2' + t['Resources']['WebServer']['Properties']['InstanceType'] = '256 MB Server' + instance = resources.Instance('test_resource_name',\ + t['Resources']['WebServer'], stack) - self._mox.StubOutWithMock(db_api, 'resource_get_by_name_and_stack') - db_api.resource_get_by_name_and_stack(None, 'test_resource_name', stack).AndReturn(None) + instance.itype_oflavor['256 MB Server'] = '256 MB Server' + instance.create() - self._mox.ReplayAll() - instance = resources.Instance('test_resource_name', t, stack) - - - # allows testing of the test directly, shown below + # allows testing of the test directly, shown below if __name__ == '__main__': sys.argv.append(__file__) nose.main() diff --git a/heat/utils.py b/heat/utils.py new file mode 100644 index 0000000000..61af797152 --- /dev/null +++ b/heat/utils.py @@ -0,0 +1,26 @@ +class LazyPluggable(object): + """A pluggable backend loaded lazily based on some value.""" + + def __init__(self, pivot, **backends): + self.__backends = backends + self.__pivot = pivot + self.__backend = None + + def __get_backend(self): + if not self.__backend: + print self.__backends.values() + backend_name = 'sqlalchemy' + backend = self.__backends[backend_name] + if isinstance(backend, tuple): + name = backend[0] + fromlist = backend[1] + else: + name = backend + fromlist = backend + + self.__backend = __import__(name, None, None, fromlist) + return self.__backend + + def __getattr__(self, key): + backend = self.__get_backend() + return getattr(backend, key) diff --git a/run_tests.sh b/run_tests.sh index 71e8319cd1..8909d8831a 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -42,9 +42,10 @@ for arg in "$@"; do process_option $arg done -NOSETESTS="python run_tests.py $noseargs" +NOSETESTS="python heat/testing/runner.py $noseopts $noseargs" function run_tests { + echo 'Running tests' # Just run the test suites in current environment ${wrapper} $NOSETESTS 2> run_tests.err.log } @@ -52,7 +53,7 @@ function run_tests { function run_pep8 { echo "Running pep8 ..." PEP8_OPTIONS="--exclude=$PEP8_EXCLUDE --repeat" - PEP8_INCLUDE="bin/heat bin/heat-api bin/heat-engine heat tools setup.py run_tests.py" + PEP8_INCLUDE="bin/heat bin/heat-api bin/heat-engine heat tools setup.py heat/testing/runner.py" ${wrapper} pep8 $PEP8_OPTIONS $PEP8_INCLUDE } @@ -87,9 +88,9 @@ if [ $just_pep8 -eq 1 ]; then exit fi -run_tests || exit +run_tests -if [ -z "$noseargs" ]; then - run_pep8 -fi +#if [ -z "$noseargs" ]; then +# run_pep8 +#fi From 2af14f5abf1e57b3b9a27362f63ad7bb85b25be1 Mon Sep 17 00:00:00 2001 From: Chris Alfonso Date: Thu, 19 Apr 2012 19:24:28 -0400 Subject: [PATCH 4/8] Adding instance creation test --- heat/engine/resources.py | 57 ++++++++++++++++++------------------ heat/tests/test_resources.py | 26 +++++++++++----- 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/heat/engine/resources.py b/heat/engine/resources.py index 4ca18d5b46..438666655a 100644 --- a/heat/engine/resources.py +++ b/heat/engine/resources.py @@ -35,7 +35,6 @@ from heat.db import api as db_api from heat.common.config import HeatEngineConfigOpts logger = logging.getLogger('heat.engine.resources') - # If ../heat/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), @@ -520,6 +519,33 @@ class Instance(Resource): logger.info('%s.GetAtt(%s) == %s' % (self.name, key, res)) return unicode(res) + def _build_userdata(self, userdata): + # Build mime multipart data blob for cloudinit userdata + mime_blob = MIMEMultipart() + fp = open('%s/%s' % (cloudinit_path, 'config'), 'r') + msg = MIMEText(fp.read(), _subtype='cloud-config') + fp.close() + msg.add_header('Content-Disposition', 'attachment', + filename='cloud-config') + mime_blob.attach(msg) + + fp = open('%s/%s' % (cloudinit_path, 'part-handler.py'), 'r') + msg = MIMEText(fp.read(), _subtype='part-handler') + fp.close() + msg.add_header('Content-Disposition', 'attachment', + filename='part-handler.py') + mime_blob.attach(msg) + + msg = MIMEText(json.dumps(self.t['Metadata']), + _subtype='x-cfninitdata') + msg.add_header('Content-Disposition', 'attachment', + filename='cfn-init-data') + mime_blob.attach(msg) + + msg = MIMEText(userdata, _subtype='x-shellscript') + msg.add_header('Content-Disposition', 'attachment', filename='startup') + return mime_blob.attach(msg) + def create(self): def _null_callback(p, n, out): """ @@ -531,7 +557,6 @@ class Instance(Resource): return self.state_set(self.CREATE_IN_PROGRESS) Resource.create(self) - props = self.t['Properties'] if not 'KeyName' in props: raise exception.UserParameterMissing(key='KeyName') @@ -574,36 +599,12 @@ class Instance(Resource): if o.name == flavor: flavor_id = o.id - # Build mime multipart data blob for cloudinit userdata - mime_blob = MIMEMultipart() - fp = open('%s/%s' % (cloudinit_path, 'config'), 'r') - msg = MIMEText(fp.read(), _subtype='cloud-config') - fp.close() - msg.add_header('Content-Disposition', 'attachment', - filename='cloud-config') - mime_blob.attach(msg) - - fp = open('%s/%s' % (cloudinit_path, 'part-handler.py'), 'r') - msg = MIMEText(fp.read(), _subtype='part-handler') - fp.close() - msg.add_header('Content-Disposition', 'attachment', - filename='part-handler.py') - mime_blob.attach(msg) - - msg = MIMEText(json.dumps(self.t['Metadata']), - _subtype='x-cfninitdata') - msg.add_header('Content-Disposition', 'attachment', - filename='cfn-init-data') - mime_blob.attach(msg) - - msg = MIMEText(userdata, _subtype='x-shellscript') - msg.add_header('Content-Disposition', 'attachment', filename='startup') - mime_blob.attach(msg) + server_userdata = self._build_userdata(userdata) server = self.nova().servers.create(name=self.name, image=image_id, flavor=flavor_id, key_name=key_name, security_groups=security_groups, - userdata=mime_blob.as_string()) + userdata=server_userdata) while server.status == 'BUILD': server.get() eventlet.sleep(1) diff --git a/heat/tests/test_resources.py b/heat/tests/test_resources.py index c8f8841245..ccb6e2eae6 100644 --- a/heat/tests/test_resources.py +++ b/heat/tests/test_resources.py @@ -21,7 +21,7 @@ from heat.engine import parser class ResourcesTest(unittest.TestCase): def setUp(self): self.m = mox.Mox() - self.cs = fakes.FakeClient() + self.fc = fakes.FakeClient() def tearDown(self): self.m.UnsetStubs() @@ -42,18 +42,30 @@ class ResourcesTest(unittest.TestCase): stack).AndReturn(None) self.m.StubOutWithMock(resources.Instance, 'nova') - resources.Instance.nova().AndReturn(self.cs) - resources.Instance.nova().AndReturn(self.cs) - resources.Instance.nova().AndReturn(self.cs) - resources.Instance.nova().AndReturn(self.cs) + resources.Instance.nova().AndReturn(self.fc) + resources.Instance.nova().AndReturn(self.fc) + resources.Instance.nova().AndReturn(self.fc) + resources.Instance.nova().AndReturn(self.fc) + + #Need to find an easier way + userdata = t['Resources']['WebServer']['Properties']['UserData'] - - print self.cs.flavors.list()[0].name self.m.ReplayAll() + + t['Resources']['WebServer']['Properties']['ImageId'] = 'CentOS 5.2' t['Resources']['WebServer']['Properties']['InstanceType'] = '256 MB Server' instance = resources.Instance('test_resource_name',\ t['Resources']['WebServer'], stack) + + server_userdata = instance._build_userdata(json.dumps(userdata)) + self.m.StubOutWithMock(self.fc.servers, 'create') + self.fc.servers.create(image=1, flavor=1, key_name='test',\ + name='test_resource_name', security_groups=None,\ + userdata=server_userdata).\ + AndReturn(self.fc.servers.list()[1]) + self.m.ReplayAll() + instance.itype_oflavor['256 MB Server'] = '256 MB Server' instance.create() From 7f44cf82bf8255a336104ac40191e51a0aa7ec52 Mon Sep 17 00:00:00 2001 From: Chris Alfonso Date: Tue, 17 Apr 2012 15:55:36 -0400 Subject: [PATCH 5/8] Adding resource initialization test --- heat/tests/test_resources.py | 2 +- run_tests.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/heat/tests/test_resources.py b/heat/tests/test_resources.py index ccb6e2eae6..b67b444fa1 100644 --- a/heat/tests/test_resources.py +++ b/heat/tests/test_resources.py @@ -70,7 +70,7 @@ class ResourcesTest(unittest.TestCase): instance.itype_oflavor['256 MB Server'] = '256 MB Server' instance.create() - # allows testing of the test directly, shown below + # allows testing of the test directly, shown below if __name__ == '__main__': sys.argv.append(__file__) nose.main() diff --git a/run_tests.py b/run_tests.py index 73b185252d..65fc500820 100644 --- a/run_tests.py +++ b/run_tests.py @@ -42,6 +42,7 @@ import os import unittest import sys +sys.path.append(os.environ['PYTHON_NOVACLIENT_SRC']) gettext.install('heat', unicode=1) from nose import config From bad1eb37a5b3659e888a5840ed931aa11fc0e37b Mon Sep 17 00:00:00 2001 From: Chris Alfonso Date: Wed, 18 Apr 2012 09:03:17 -0400 Subject: [PATCH 6/8] Adding unit test for instance creation --- heat/tests/test_resources.py | 23 ++++++++++++++++++++++- run_tests.py | 1 - 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/heat/tests/test_resources.py b/heat/tests/test_resources.py index b67b444fa1..3c3d61353b 100644 --- a/heat/tests/test_resources.py +++ b/heat/tests/test_resources.py @@ -25,6 +25,7 @@ class ResourcesTest(unittest.TestCase): def tearDown(self): self.m.UnsetStubs() + print "ResourcesTest teardown complete" def test_initialize_instance_from_template(self): f = open('../../templates/WordPress_Single_Instance_gold.template') @@ -52,7 +53,6 @@ class ResourcesTest(unittest.TestCase): self.m.ReplayAll() - t['Resources']['WebServer']['Properties']['ImageId'] = 'CentOS 5.2' t['Resources']['WebServer']['Properties']['InstanceType'] = '256 MB Server' instance = resources.Instance('test_resource_name',\ @@ -70,6 +70,27 @@ class ResourcesTest(unittest.TestCase): instance.itype_oflavor['256 MB Server'] = '256 MB Server' instance.create() + def test_initialize_instance_from_template(self): + f = open('templates/WordPress_Single_Instance_gold.template') + t = f.read() + f.close() + + stack = self._mox.CreateMockAnything() + stack.id().AndReturn(1) + + self._mox.StubOutWithMock(stack, 'resolve_static_refs') + stack.resolve_static_refs(t).AndReturn(t) + + self._mox.StubOutWithMock(stack, 'resolve_find_in_map') + stack.resolve_find_in_map(t).AndReturn(t) + + self._mox.StubOutWithMock(db_api, 'resource_get_by_name_and_stack') + db_api.resource_get_by_name_and_stack(None, 'test_resource_name', stack).AndReturn(None) + + self._mox.ReplayAll() + instance = resources.Instance('test_resource_name', t, stack) + + # allows testing of the test directly, shown below if __name__ == '__main__': sys.argv.append(__file__) diff --git a/run_tests.py b/run_tests.py index 65fc500820..73b185252d 100644 --- a/run_tests.py +++ b/run_tests.py @@ -42,7 +42,6 @@ import os import unittest import sys -sys.path.append(os.environ['PYTHON_NOVACLIENT_SRC']) gettext.install('heat', unicode=1) from nose import config From 7b039d4a7523e3468c3bad7eeea6f30ddf428cca Mon Sep 17 00:00:00 2001 From: Chris Alfonso Date: Thu, 19 Apr 2012 18:10:20 -0400 Subject: [PATCH 7/8] Added db setup and teardown with sqlite --- heat/tests/test_resources.py | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/heat/tests/test_resources.py b/heat/tests/test_resources.py index 3c3d61353b..731557b532 100644 --- a/heat/tests/test_resources.py +++ b/heat/tests/test_resources.py @@ -69,29 +69,18 @@ class ResourcesTest(unittest.TestCase): instance.itype_oflavor['256 MB Server'] = '256 MB Server' instance.create() - - def test_initialize_instance_from_template(self): - f = open('templates/WordPress_Single_Instance_gold.template') - t = f.read() - f.close() - - stack = self._mox.CreateMockAnything() - stack.id().AndReturn(1) - self._mox.StubOutWithMock(stack, 'resolve_static_refs') - stack.resolve_static_refs(t).AndReturn(t) - - self._mox.StubOutWithMock(stack, 'resolve_find_in_map') - stack.resolve_find_in_map(t).AndReturn(t) + print self.cs.flavors.list()[0].name + self.m.ReplayAll() + t['Resources']['WebServer']['Properties']['ImageId'] = 'CentOS 5.2' + t['Resources']['WebServer']['Properties']['InstanceType'] = '256 MB Server' + instance = resources.Instance('test_resource_name',\ + t['Resources']['WebServer'], stack) - self._mox.StubOutWithMock(db_api, 'resource_get_by_name_and_stack') - db_api.resource_get_by_name_and_stack(None, 'test_resource_name', stack).AndReturn(None) + instance.itype_oflavor['256 MB Server'] = '256 MB Server' + instance.create() - self._mox.ReplayAll() - instance = resources.Instance('test_resource_name', t, stack) - - - # allows testing of the test directly, shown below + # allows testing of the test directly, shown below if __name__ == '__main__': sys.argv.append(__file__) nose.main() From 29c0f9a74f74c75478b81994d5a9f77ecf52f75e Mon Sep 17 00:00:00 2001 From: Chris Alfonso Date: Thu, 19 Apr 2012 19:24:28 -0400 Subject: [PATCH 8/8] Adding instance creation test Signed-off-by: Chris Alfonso --- heat/tests/test_resources.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/heat/tests/test_resources.py b/heat/tests/test_resources.py index 731557b532..8eb52347d6 100644 --- a/heat/tests/test_resources.py +++ b/heat/tests/test_resources.py @@ -70,12 +70,7 @@ class ResourcesTest(unittest.TestCase): instance.itype_oflavor['256 MB Server'] = '256 MB Server' instance.create() - print self.cs.flavors.list()[0].name self.m.ReplayAll() - t['Resources']['WebServer']['Properties']['ImageId'] = 'CentOS 5.2' - t['Resources']['WebServer']['Properties']['InstanceType'] = '256 MB Server' - instance = resources.Instance('test_resource_name',\ - t['Resources']['WebServer'], stack) instance.itype_oflavor['256 MB Server'] = '256 MB Server' instance.create()