Move client into melange namespace.

Convert bin/melange into console_scripts entry_point
Tests run through tox

Change-Id: I7608194771478aaa5e3e4666f67a4f679abc6f33
This commit is contained in:
Jason Kölker 2012-01-20 15:37:29 -06:00
parent 484c7313ec
commit 67d1da329e
39 changed files with 269 additions and 865 deletions

5
.gitignore vendored
View File

@ -4,9 +4,8 @@ local_settings.py
keeper
build/*
build-stamp
melange.egg-info
.melange-venv
.venv
python_melangeclient.egg-info
.tox
*.sqlite
*.log
tags

2
MANIFEST.in Normal file
View File

@ -0,0 +1,2 @@
include README.rst
include melange/client/views/*.tpl

11
README
View File

@ -1,11 +0,0 @@
To run unit tests:
melange_client_dir> ./run_tests.sh melange_client.tests.unit
To run functional tests:
1. Start the melange server
2. Update the configuration values in
melange_client/tests/functional/tests.conf
to point to the melange server
3. Run the tests:
melange_client_dir> ./run_tests.sh melange_client.tests.functional

16
README.rst Normal file
View File

@ -0,0 +1,16 @@
Python bindings to the OpenStack Melange API
============================================
This is a client for the Openstack Melange API. It contains a Python API
(the ``melange.client`` module), and a command-line script (``melange``).
Running the tests
-----------------
Currently the test suite requires a running melange-server running on
http://localhost:9898.
Tests are run under `tox <http://tox.testrun.org/latest/>`_. First install
``tox`` using pip or your distribution's packages then run ``tox`` from
the distribution directory to run the tests in isolated virtual
environments.

20
melange/__init__.py Normal file
View File

@ -0,0 +1,20 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# 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 pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)

View File

@ -16,18 +16,15 @@
# under the License.
import gettext
import os
from melange.client.client import HTTPClient
from melange.client.client import AuthorizationClient
# NOTE(jkoelker) should this be melange.client? Are translations going
# to be separate?
gettext.install('melange', unicode=1)
def melange_root_path():
return os.path.dirname(__file__)
def melange_bin_path(filename="."):
return os.path.join(melange_root_path(), "..", "bin", filename)
def melange_etc_path(filename="."):
return os.path.join(melange_root_path(), "..", "etc", "melange", filename)
__all__ = [HTTPClient,
AuthorizationClient]

45
bin/melange → melange/client/cli.py Executable file → Normal file
View File

@ -1,4 +1,3 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
@ -29,22 +28,11 @@ from os import environ as env
import sys
import yaml
# If ../melange_client/__init__.py exists, add ../ to Python search path, so
# 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]),
os.pardir,
os.pardir))
if os.path.exists(os.path.join(possible_topdir,
'melange_client',
'__init__.py')):
sys.path.insert(0, possible_topdir)
import melange_client
from melange_client import client as base_client
from melange_client import exception
from melange_client import inspector
from melange_client import ipam_client
from melange_client import template
from melange.client import client as base_client
from melange.client import exception
from melange.client import inspector
from melange.client import ipam_client
from melange.client import template
def create_options(parser):
@ -164,7 +152,10 @@ def auth_client(options):
def view(data, template_name):
data = data or {}
try:
view_path = os.path.join(melange_client.melange_root_path(), 'views')
# TODO(jkoelker) Templates should be using the PEP302 get_data api
melange_client_file = sys.modules['melange.client'].__file__
melange_path = os.path.dirname(melange_client_file)
view_path = os.path.join(melange_path, 'views')
return template.template(template_name,
template_lookup=[view_path], **data)
except exception.TemplateNotFoundError:
@ -179,17 +170,19 @@ def args_to_dict(args):
"of the form of field=value")
def main():
def main(script_name=None, argv=None):
if argv is None:
argv = sys.argv[1:]
if script_name is None:
script_name = os.path.basename(sys.argv[0])
oparser = optparse.OptionParser(version='%%prog 0.1',
usage=usage())
create_options(oparser)
(options, args) = parse_options(oparser, sys.argv[1:])
(options, args) = parse_options(oparser, argv)
script_name = os.path.basename(sys.argv[0])
category = args.pop(0)
http_client = base_client.HTTPClient(options.host,
options.port,
options.timeout)
factory = ipam_client.Factory(options.host,
options.port,
@ -246,7 +239,3 @@ def main():
else:
print _("Command failed, please check log for more info")
sys.exit(2)
if __name__ == '__main__':
main()

View File

@ -15,14 +15,15 @@
# License for the specific language governing permissions and limitations
# under the License.
import httplib
import httplib2
import json
import socket
import urllib
import urlparse
from melange_client import exception
from melange.client import exception
from melange.client import utils
class HTTPClient(object):
@ -75,7 +76,7 @@ class AuthorizationClient(httplib2.Http):
if self.auth_token:
return self.auth_token
headers = {'content-type': 'application/json'}
request_body = json.dumps({"passwordCredentials":
request_body = utils.dumps({"passwordCredentials":
{"username": self.username,
'password': self.access_key}})
res, body = self.request(self.url, "POST", headers=headers,
@ -83,4 +84,4 @@ class AuthorizationClient(httplib2.Http):
if int(res.status) >= 400:
raise Exception(_("Error occured while retrieving token : %s")
% body)
return json.loads(body)['auth']['token']['id']
return utils.loads(body)['auth']['token']['id']

View File

@ -16,10 +16,10 @@
# License for the specific language governing permissions and limitations
# under the License.
from openstack.common import exception as openstack_exception
ClientConnectionError = openstack_exception.ClientConnectionError
class ClientConnectionError(Exception):
"""Error resulting from a client connecting to a server"""
pass
class MelangeClientError(Exception):

View File

@ -15,12 +15,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
import sys
import urlparse
from melange_client import client
from melange_client import utils
from melange.client import client
from melange.client import utils
class Factory(object):
@ -71,12 +70,12 @@ class Resource(object):
def create(self, **kwargs):
return self.request("POST",
self.path,
body=json.dumps({self.name: kwargs}))
body=utils.dumps({self.name: kwargs}))
def update(self, id, **kwargs):
return self.request("PUT",
self._member_path(id),
body=json.dumps(
body=utils.dumps(
{self.name: utils.remove_nones(kwargs)}))
def all(self, **params):
@ -99,7 +98,7 @@ class Resource(object):
kwargs['headers']['X-AUTH-TOKEN'] = self.auth_client.get_token()
result = self.client.do_request(method, path, **kwargs).read()
if result:
return json.loads(result)
return utils.loads(result)
class BaseClient(object):

View File

@ -41,12 +41,9 @@ import cgi
import re
import os
import functools
import time
import tokenize
import mimetypes
import datetime
from melange_client import exception
from melange.client import exception
TEMPLATES = {}
DEBUG = False

View File

@ -18,6 +18,7 @@
import unittest
# TODO(jkoelker) Convert this to mock
import mox

View File

@ -0,0 +1,59 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# 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 ConfigParser
import cStringIO
import os
import sys
from melange.client import cli
def run(command, **kwargs):
config = ConfigParser.ConfigParser()
functional_path = os.path.dirname(os.path.realpath(__file__))
config.read(os.path.join(functional_path, "tests.conf"))
stdout = sys.stdout
stderr = sys.stderr
exit = sys.exit
mystdout = cStringIO.StringIO()
mystderr = cStringIO.StringIO()
exitcode = {'code': 0}
def myexit(code):
exitcode['code'] = code
sys.stdout = mystdout
sys.stderr = mystderr
sys.exit = myexit
argv = ['--host=%s' % config.get('DEFAULT', 'server_name'),
'--port=%s' % config.get('DEFAULT', 'server_port'),
'-v']
argv.extend(command.strip().split(' '))
cli.main(script_name='melange', argv=argv)
sys.stdout = stdout
sys.stderr = stderr
sys.exit = exit
return {'exitcode': exitcode['code'],
'out': mystdout.getvalue(),
'err': mystderr.getvalue()}

View File

@ -18,7 +18,7 @@
import uuid
import yaml
from melange_client.tests import functional
from melange.client.tests import functional
def create_policy(tenant_id="123"):

View File

@ -15,15 +15,14 @@
# License for the specific language governing permissions and limitations
# under the License.
import re
import uuid
import yaml
from melange_client import tests
from melange_client import utils
from melange_client.tests import functional
from melange_client.tests.functional import template_test_helper
from melange_client.tests.functional import factory
from melange.client import tests
from melange.client import utils
from melange.client.tests import functional
from melange.client.tests.functional import template_test_helper
from melange.client.tests.functional import factory
class TestBaseCLI(tests.BaseTest, template_test_helper.TemplateTestHelper):
@ -109,9 +108,8 @@ class TestIpBlockCLI(TestBaseCLI):
sorted(factory.model('ip_blocks', list_res)))
def test_list_without_tenant_id_should_error_out(self):
self.assertRaises(RuntimeError,
functional.run,
"ip_block list")
err_res = functional.run("ip_block list")
self.assertTrue(0 != err_res['exitcode'])
class TestSubnetCLI(TestBaseCLI):

View File

@ -16,8 +16,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from melange_client import tests
from melange_client.views.helpers import table
from melange.client import tests
from melange.client.views.helpers import table
class TestTable(tests.BaseTest):

View File

@ -16,14 +16,20 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
try:
import simplejson
json = simplejson
except ImportError:
import json
import urlparse
import httplib2
# TODO(jkoelker) Convert this to mock
import mox
from melange_client import client
from melange_client import tests
from melange.client import client
from melange.client import tests
class TestAuthorizationClient(tests.BaseTest):

View File

@ -16,8 +16,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from melange_client import tests
from melange_client import inspector
from melange.client import tests
from melange.client import inspector
class TestMethodInspector(tests.BaseTest):

View File

@ -16,8 +16,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from melange_client import ipam_client
from melange_client import tests
from melange.client import ipam_client
from melange.client import tests
class TestFactory(tests.BaseTest):

View File

@ -15,8 +15,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from melange_client import tests
from melange_client import utils
from melange.client import tests
from melange.client import utils
class TestUtils(tests.BaseTest):

View File

@ -15,20 +15,29 @@
# License for the specific language governing permissions and limitations
# under the License.
import ConfigParser
import os
import re
import melange_client
from melange_client import utils
try:
# For Python < 2.6 or people using a newer version of simplejson
import simplejson
json = simplejson
except ImportError:
# For Python >= 2.6
import json
def run(command, **kwargs):
config = ConfigParser.ConfigParser()
config.read(os.path.join(melange_client.melange_root_path(),
"tests/functional/tests.conf"))
full_command = "{0} --host={1} --port={2} {3} -v ".format(
melange_client.melange_bin_path('melange'),
config.get('DEFAULT', 'server_name'),
config.get('DEFAULT', 'server_port'),
command)
return utils.execute(full_command, **kwargs)
def loads(s):
return json.loads(s)
def dumps(o):
return json.dumps(o)
def camelize(string):
return re.sub(r"(?:^|_)(.)", lambda x: x.group(0)[-1].upper(), string)
def remove_nones(hash):
return dict((key, value)
for key, value in hash.iteritems() if value is not None)

View File

@ -1,4 +1,4 @@
%from melange_client.views.helpers import table
%from melange.client.views.helpers import table
{{table.row_view(table.padded_keys(ip_routes).iteritems())}}
%for route in ip_routes:
{{table.row_view(map(lambda (key,value): (route[key], value), table.padded_keys(ip_routes).iteritems()))}}

View File

@ -1,65 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# 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 re
import os
import subprocess
import melange_client
def camelize(string):
return re.sub(r"(?:^|_)(.)", lambda x: x.group(0)[-1].upper(), string)
def remove_nones(hash):
return dict((key, value)
for key, value in hash.iteritems() if value is not None)
def execute(cmd, raise_error=True):
"""Executes a command in a subprocess.
Returns a tuple of (exitcode, out, err), where out is the string output
from stdout and err is the string output from stderr when
executing the command.
:param cmd: Command string to execute
:param raise_error: If returncode is not 0 (success), then
raise a RuntimeError? Default: True)
"""
env = os.environ.copy()
# Make sure that we use the programs in the
# current source directory's bin/ directory.
env['PATH'] = melange_client.melange_bin_path() + ':' + env['PATH']
process = subprocess.Popen(cmd,
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env)
(out, err) = process.communicate()
exitcode = process.returncode
if process.returncode != 0 and raise_error:
msg = "Command %(cmd)s did not succeed. Returned an exit "\
"code of %(exitcode)d."\
"\n\nSTDOUT: %(out)s"\
"\n\nSTDERR: %(err)s" % locals()
raise RuntimeError(msg)
return {'exitcode': exitcode, 'out': out, 'err': err}

View File

@ -1,367 +0,0 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# 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 Nova.
To run all tests
python run_tests.py
To run a single test:
python run_tests.py test_compute:ComputeTestCase.test_run_terminate
To run a single test module:
python run_tests.py test_compute
or
python run_tests.py api.test_wsgi
"""
import gettext
import heapq
import logging
import os
import unittest
import sys
import time
gettext.install('melange', unicode=1)
from nose import config
from nose import core
from nose import result
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:
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):
from win32console import GetStdHandle, STD_OUT_HANDLE, \
FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \
FOREGROUND_INTENSITY
red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN,
FOREGROUND_BLUE, FOREGROUND_INTENSITY)
self.stream = stream
self.screenBuffer = GetStdHandle(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 MelangeTestResult(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 MelangeTestRunner(core.TextTestRunner):
def __init__(self, *args, **kwargs):
self.show_elapsed = kwargs.pop('show_elapsed')
core.TextTestRunner.__init__(self, *args, **kwargs)
def _makeResult(self):
return MelangeTestResult(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_
if __name__ == '__main__':
logger = logging.getLogger()
hdlr = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
hdlr.setFormatter(formatter)
logger.addHandler(hdlr)
logger.setLevel(logging.DEBUG)
# If any argument looks like a test name but doesn't have "melange.tests" in
# front of it, automatically add that so we don't have to type as much
show_elapsed = True
argv = []
for x in sys.argv:
if x.startswith('test_'):
argv.append('melange.tests.%s' % x)
elif x.startswith('--hide-elapsed'):
show_elapsed = False
else:
argv.append(x)
testdir = os.path.abspath(os.path.join("melange_client", "tests"))
c = config.Config(stream=sys.stdout,
env=os.environ,
verbosity=3,
workingDir=testdir,
plugins=core.DefaultPluginManager())
runner = MelangeTestRunner(stream=c.stream,
verbosity=c.verbosity,
config=c,
show_elapsed=show_elapsed)
sys.exit(not core.run(config=c, testRunner=runner, argv=argv))

View File

@ -1,168 +0,0 @@
#!/bin/bash
set -eu
function usage {
echo "Usage: $0 [OPTION]..."
echo "Run Melange's test suite(s)"
echo ""
echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment"
echo " -r, --recreate-db Recreate the test database (deprecated, as this is now the default)."
echo " -n, --no-recreate-db Don't recreate the test database."
echo " -x, --stop Stop running tests after the first error or failure."
echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
echo " -p, --pep8 Just run pep8"
echo " -P, --no-pep8 Don't run pep8"
echo " -c, --coverage Generate coverage report"
echo " -h, --help Print this usage message"
echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list"
echo ""
echo "Note: with no options specified, the script will try to run the tests in a virtual environment,"
echo " If no virtualenv is found, the script will ask if you would like to create one. If you "
echo " prefer to run tests NOT in a virtual environment, simply pass the -N option."
exit
}
function process_option {
case "$1" in
-h|--help) usage;;
-V|--virtual-env) always_venv=1; never_venv=0;;
-N|--no-virtual-env) always_venv=0; never_venv=1;;
-r|--recreate-db) recreate_db=1;;
-n|--no-recreate-db) recreate_db=0;;
-f|--force) force=1;;
-p|--pep8) just_pep8=1;;
-P|--no-pep8) no_pep8=1;;
-c|--coverage) coverage=1;;
-*) noseopts="$noseopts $1";;
*) noseargs="$noseargs $1"
esac
}
venv=.venv
with_venv=tools/with_venv.sh
always_venv=0
never_venv=0
force=0
noseargs=
noseopts=
wrapper=""
just_pep8=0
no_pep8=0
coverage=0
recreate_db=1
for arg in "$@"; do
process_option $arg
done
# If enabled, tell nose to collect coverage data
if [ $coverage -eq 1 ]; then
noseopts="$noseopts --with-coverage --cover-package=melange"
fi
function run_tests {
# Just run the test suites in current environment
${wrapper} $NOSETESTS 2> run_tests.log
# If we get some short import error right away, print the error log directly
RESULT=$?
if [ "$RESULT" -ne "0" ];
then
ERRSIZE=`wc -l run_tests.log | awk '{print \$1}'`
if [ "$ERRSIZE" -lt "40" ];
then
cat run_tests.log
fi
fi
return $RESULT
}
function run_pep8 {
echo "Running pep8 ..."
# Opt-out files from pep8
ignore_scripts="*.sh:*melange-debug:*clean-vlans"
ignore_files="*eventlet-patch:*pip-requires"
GLOBIGNORE="$ignore_scripts:$ignore_files"
srcfiles=`find bin -type f ! -name "melange.conf*"`
srcfiles+=" `find tools/*`"
srcfiles+=" bin melange_client"
# Just run PEP8 in current environment
#
# NOTE(sirp): W602 (deprecated 3-arg raise) is being ignored for the
# following reasons:
#
# 1. It's needed to preserve traceback information when re-raising
# exceptions; this is needed b/c Eventlet will clear exceptions when
# switching contexts.
#
# 2. There doesn't appear to be an alternative, "pep8-tool" compatible way of doing this
# in Python 2 (in Python 3 `with_traceback` could be used).
#
# 3. Can find no corroborating evidence that this is deprecated in Python 2
# other than what the PEP8 tool claims. It is deprecated in Python 3, so,
# perhaps the mistake was thinking that the deprecation applied to Python 2
# as well.
${wrapper} pep8 --repeat --show-pep8 --show-source \
--ignore=E202,W602 \
--exclude=vcsversion.py ${srcfiles}
}
NOSETESTS="python run_tests.py $noseopts $noseargs"
if [ $never_venv -eq 0 ]
then
# Remove the virtual environment if --force used
if [ $force -eq 1 ]; then
echo "Cleaning virtualenv..."
rm -rf ${venv}
fi
if [ -e ${venv} ]; then
wrapper="${with_venv}"
else
if [ $always_venv -eq 1 ]; then
# Automatically install the virtualenv
python tools/install_venv.py
wrapper="${with_venv}"
else
echo -e "No virtual environment found...create one? (Y/n) \c"
read use_ve
if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then
# Install the virtualenv and run the test suite in it
python tools/install_venv.py
wrapper=${with_venv}
fi
fi
fi
fi
# Delete old coverage data from previous runs
if [ $coverage -eq 1 ]; then
${wrapper} coverage erase
fi
if [ $just_pep8 -eq 1 ]; then
run_pep8
exit
fi
if [ $recreate_db -eq 1 ]; then
rm -f tests.sqlite
fi
run_tests
# NOTE(sirp): we only want to run pep8 when we're running the full-test suite,
# not when we're running tests individually. To handle this, we need to
# distinguish between options (noseopts), which begin with a '-', and
# arguments (noseargs).
if [ -z "$noseargs" ]; then
if [ $no_pep8 -eq 0 ]; then
run_pep8
fi
fi
if [ $coverage -eq 1 ]; then
echo "Generating coverage report in covhtml/"
${wrapper} coverage html -d covhtml -i
fi

15
setup.cfg Normal file
View File

@ -0,0 +1,15 @@
[nosetests]
verbosity=2
detailed-errors=1
with-tissue=1
tissue-repeat=1
tissue-show-pep8=1
tissue-show-source=1
tissue-inclusive=1
tissue-color=1
tissue-package=melange.client
with-openstack=1
openstack-red=0.1
openstack-yellow=0.075
openstack-show-elapsed=1
openstack-color=1

57
setup.py Normal file
View File

@ -0,0 +1,57 @@
# Copyright 2011 OpenStack, LLC
#
# 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
import sys
import setuptools
version = "0.1"
install_requires = ["httplib2", "pyyaml"]
if sys.version_info < (2, 6):
install_requires.append("simplejson")
classifiers = ["Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python",
]
console_scripts = ["melange = melange.client.cli:main"]
def read_file(file_name):
return open(os.path.join(os.path.dirname(__file__),
file_name)).read()
setuptools.setup(name="python-melangeclient",
version=version,
description="Client library for OpenStack Melange API.",
long_description=read_file("README.rst"),
license="Apache License, Version 2.0",
url="https://github.com/openstack/python-melangeclient",
classifiers=classifiers,
author="Openstack Melange Team",
author_email="openstack@lists.launchpad.net",
include_package_data=True,
packages=setuptools.find_packages(exclude=["tests"]),
install_requires=install_requires,
entry_points = {"console_scripts": console_scripts},
zip_safe=False,
)

View File

@ -1,145 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2010 OpenStack, LLC
#
# 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.
"""
Installation script for Melange client's development virtualenv.
"""
import os
import subprocess
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
VENV = os.path.join(ROOT, '.venv')
PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires')
PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
def die(message, *args):
print >> sys.stderr, message % args
sys.exit(1)
def check_python_version():
if sys.version_info < (2, 6):
die("Need Python Version >= 2.6")
def run_command(cmd, redirect_output=True, check_exit_code=True):
"""
Runs a command in an out-of-process shell, returning the
output of that command. Working directory is ROOT.
"""
if redirect_output:
stdout = subprocess.PIPE
else:
stdout = None
proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout)
output = proc.communicate()[0]
if check_exit_code and proc.returncode != 0:
die('Command "%s" failed.\n%s', ' '.join(cmd), output)
return output
HAS_EASY_INSTALL = bool(run_command(['which', 'easy_install'],
check_exit_code=False).strip())
HAS_VIRTUALENV = bool(run_command(['which', 'virtualenv'],
check_exit_code=False).strip())
def check_dependencies():
"""Make sure virtualenv is in the path."""
if not HAS_VIRTUALENV:
print 'not found.'
# Try installing it via easy_install...
if HAS_EASY_INSTALL:
print 'Installing virtualenv via easy_install...',
if not (run_command(['which', 'easy_install']) and
run_command(['easy_install', 'virtualenv'])):
die('ERROR: virtualenv not found.\n\nMelange client'
' development requires virtualenv, please install it'
' using your favorite package management tool')
print 'done.'
print 'done.'
def create_virtualenv(venv=VENV):
"""Creates the virtual environment and installs PIP only into the
virtual environment
"""
print 'Creating venv...',
run_command(['virtualenv', '-q', '--no-site-packages', VENV])
print 'done.'
print 'Installing pip in virtualenv...',
if not run_command(['tools/with_venv.sh', 'easy_install', 'pip']).strip():
die("Failed to install pip.")
print 'done.'
def install_dependencies(venv=VENV):
print 'Installing dependencies with pip (this can take a while)...'
# Install greenlet by hand - just listing it in the requires file does not
# get it in stalled in the right order
run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv,
'greenlet'], redirect_output=False)
run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv, '-r',
PIP_REQUIRES], redirect_output=False)
# Tell the virtual env how to "import melange"
pthfile = os.path.join(venv, "lib", PY_VERSION, "site-packages",
"melange.pth")
f = open(pthfile, 'w')
f.write("%s\n" % ROOT)
def print_help():
help = """
Melange client development environment setup is complete.
Melange client development uses virtualenv to track and manage Python
dependencies while in development and testing.
To activate the Melange client virtualenv for the extent of your current
shell session you can run:
$ source .venv/bin/activate
Or, if you prefer, you can run commands in the virtualenv on a case by case
basis by running:
$ tools/with_venv.sh <your command>
Also, make test will automatically use the virtualenv.
"""
print help
def main(argv):
check_python_version()
check_dependencies()
create_virtualenv()
install_dependencies()
print_help()
if __name__ == '__main__':
main(sys.argv)

View File

@ -1,10 +0,0 @@
pep8
pylint
mox
nose
sphinx
coverage
nosexcover
httplib2
pyyaml
-e git+https://github.com/jkoelker/openstack-common.git@melange_compat#egg=openstack.common

View File

@ -1,4 +0,0 @@
#!/bin/bash
TOOLS=`dirname $0`
VENV=$TOOLS/../.venv
source $VENV/bin/activate && $@

9
tox.ini Normal file
View File

@ -0,0 +1,9 @@
[tox]
envlist = py26,py27
[testenv]
deps= nose
mox
tissue
openstack.nose_plugin
commands=nosetests []