Add some basic REST API testing

Mostly negative testing. There is a single positive test which can
still succeed even if IPA is not configured due to the imports test
in novajoin/ipa.py.
This commit is contained in:
Rob Crittenden 2016-11-09 19:40:18 +00:00
parent ef2c9baa36
commit 87561619df
18 changed files with 825 additions and 0 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
build
tools/lintstack.head.py

8
.testr.conf Normal file
View File

@ -0,0 +1,8 @@
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./novajoin/tests} $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

60
novajoin/test.py Normal file
View File

@ -0,0 +1,60 @@
# Copyright 2016 Red Hat, Inc.
#
# 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.
"""Base classes for all unit tests."""
import os
import fixtures
from oslo_config import cfg
from oslo_utils import strutils
from oslo_utils import timeutils
import testtools
CONF = cfg.CONF
class TestingException(Exception):
pass
class TestCase(testtools.TestCase):
"""Test case base class for all unit tests."""
def setUp(self):
"""Run before each test method to initialize test environment."""
super(TestCase, self).setUp()
test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
try:
test_timeout = int(test_timeout)
except ValueError:
# If timeout value is invalid do not set a timeout.
test_timeout = 0
if test_timeout > 0:
self.useFixture(fixtures.Timeout(test_timeout, gentle=True))
self.useFixture(fixtures.NestedTempfile())
self.useFixture(fixtures.TempHomeDir())
environ_enabled = (lambda var_name:
strutils.bool_from_string(os.environ.get(var_name)))
if environ_enabled('OS_STDOUT_CAPTURE'):
stdout = self.useFixture(fixtures.StringStream('stdout')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
if environ_enabled('OS_STDERR_CAPTURE'):
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
self.start = timeutils.utcnow()

View File

View File

@ -0,0 +1,22 @@
# Copyright 2016 Red Hat, Inc.
#
# 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 eventlet
from six.moves import builtins
eventlet.monkey_patch()
# See http://code.google.com/p/python-nose/issues/detail?id=373
# The code below enables nosetests to work with i18n _() blocks
setattr(builtins, '_', lambda x: x)

View File

View File

@ -0,0 +1,44 @@
# Copyright 2016 Red Hat, Inc.
#
# 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 webob
from novajoin import base
from novajoin import context
from novajoin.join import Join
from novajoin.tests.unit import fake_constants as fake
class FakeRequestContext(context.RequestContext):
def __init__(self, *args, **kwargs):
kwargs['auth_token'] = kwargs.get(fake.USER_ID, fake.PROJECT_ID)
super(FakeRequestContext, self).__init__(*args, **kwargs)
class HTTPRequest(webob.Request):
@classmethod
def blank(cls, *args, **kwargs):
if args is not None:
if 'v1' in args[0]:
kwargs['base_url'] = 'http://localhost/v1'
use_admin_context = kwargs.pop('use_admin_context', False)
version = kwargs.pop('version', '1.0')
out = base.Request.blank(*args, **kwargs)
out.environ['cinder.context'] = FakeRequestContext(
fake.USER_ID,
fake.PROJECT_ID,
is_admin=use_admin_context)
out.api_version_request = Join(version)
return out

View File

View File

@ -0,0 +1,121 @@
# Copyright 2016 Red Hat, Inc.
#
# 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.
from oslo_serialization import jsonutils
from testtools.matchers import MatchesRegex
from novajoin.base import Fault
from novajoin import join
from novajoin import test
from novajoin.tests.unit.api import fakes
class JoinTest(test.TestCase):
def setUp(self):
self.join_controller = join.JoinController()
super(JoinTest, self).setUp()
def test_no_body(self):
body = None
req = fakes.HTTPRequest.blank('/v1/')
req.method = 'POST'
req.content_type = "application/json"
# Not using assertRaises because the exception is wrapped as
# a Fault
try:
self.join_controller.create(req, body)
except Fault as fault:
assert fault.status_int == 400
def test_no_instanceid(self):
body = {"metadata": {"ipa_enroll": "True"},
"image-id": "b8c88e01-c820-40f6-b026-00926706e374",
"hostname": "test"}
req = fakes.HTTPRequest.blank('/v1/')
req.method = 'POST'
req.content_type = "application/json"
# Not using assertRaises because the exception is wrapped as
# a Fault
try:
self.join_controller.create(req, body)
except Fault as fault:
assert fault.status_int == 400
def test_no_imageid(self):
body = {"metadata": {"ipa_enroll": "True"},
"instance-id": "e4274dc8-325a-409b-92fd-cfdfdd65ae8b",
"hostname": "test"}
req = fakes.HTTPRequest.blank('/v1/')
req.method = 'POST'
req.content_type = "application/json"
# Not using assertRaises because the exception is wrapped as
# a Fault
try:
self.join_controller.create(req, body)
except Fault as fault:
assert fault.status_int == 400
def test_no_hostname(self):
body = {"metadata": {"ipa_enroll": "True"},
"instance-id": "e4274dc8-325a-409b-92fd-cfdfdd65ae8b",
"image-id": "b8c88e01-c820-40f6-b026-00926706e374"}
req = fakes.HTTPRequest.blank('/v1/')
req.method = 'POST'
req.content_type = "application/json"
# Not using assertRaises because the exception is wrapped as
# a Fault
try:
self.join_controller.create(req, body)
except Fault as fault:
assert fault.status_int == 400
def test_request_no_enrollment(self):
body = {"metadata": {"ipa_enroll": "False"},
"instance-id": "e4274dc8-325a-409b-92fd-cfdfdd65ae8b",
"image-id": "b8c88e01-c820-40f6-b026-00926706e374",
"hostname": "test"}
expected = {}
req = fakes.HTTPRequest.blank('/v1')
req.method = 'POST'
req.content_type = "application/json"
req.body = jsonutils.dump_as_bytes(body)
res_dict = self.join_controller.create(req, body)
self.assertEqual(expected, res_dict)
def test_request(self):
body = {"metadata": {"ipa_enroll": "True"},
"instance-id": "e4274dc8-325a-409b-92fd-cfdfdd65ae8b",
"image-id": "b8c88e01-c820-40f6-b026-00926706e374",
"hostname": "test"}
req = fakes.HTTPRequest.blank('/v1')
req.method = 'POST'
req.content_type = "application/json"
req.body = jsonutils.dump_as_bytes(body)
res_dict = self.join_controller.create(req, body)
# Manually check the response dict for an OTP pattern and
# what the default hostname should be.
self.assertThat(res_dict.get('ipaotp'),
MatchesRegex('^[a-z0-9]{32}'))
self.assertEqual(len(res_dict.get('ipaotp', 0)), 32)
self.assertEqual(res_dict.get('hostname'), 'test.test')
# Note that on failures this will generate to stdout a Krb5Error
# because in all likelihood the keytab cannot be read (and
# probably doesn't exist. This can be ignored.

View File

@ -0,0 +1,2 @@
PROJECT_ID = '89afd400-b646-4bbc-b12b-c0a4d63e5bd3'
USER_ID = 'c853ca26-e8ea-4797-8a52-ee124a013d0e'

View File

@ -0,0 +1,65 @@
# Copyright 2016 Red Hat, Inc.
#
# 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.
"""
Unit Tests for WSGI server
"""
import mock
import testtools
from novajoin import wsgi
from oslo_config import cfg
test_service_opts = [
cfg.StrOpt("test_service_listen",
help="Host to bind test service to"),
cfg.IntOpt("test_service_listen_port",
default=0,
help="Port number to bind test service to"), ]
CONF = cfg.CONF
CONF.register_opts(test_service_opts)
class TestWSGIService(testtools.TestCase):
def setUp(self):
super(TestWSGIService, self).setUp()
@mock.patch('oslo_service.wsgi.Loader')
def test_service_random_port(self, mock_loader):
test_service = wsgi.WSGIService("test_service")
self.assertEqual(0, test_service.port)
test_service.start()
self.assertNotEqual(0, test_service.port)
test_service.stop()
self.assertTrue(mock_loader.called)
@mock.patch('oslo_service.wsgi.Loader')
def test_reset_pool_size_to_default(self, mock_loader):
test_service = wsgi.WSGIService("test_service")
test_service.start()
# Stopping the service, which in turn sets pool size to 0
test_service.stop()
self.assertEqual(0, test_service.server._pool.size)
# Resetting pool size to default
test_service.reset()
test_service.start()
self.assertEqual(cfg.CONF.wsgi_default_pool_size,
test_service.server._pool.size)
self.assertTrue(mock_loader.called)

41
pylintrc Normal file
View File

@ -0,0 +1,41 @@
# The format of this file isn't really documented; just use --generate-rcfile
[Messages Control]
# NOTE(justinsb): We might want to have a 2nd strict pylintrc in future
# C0111: Don't require docstrings on every method
# W0511: TODOs in code comments are fine.
# W0142: *args and **kwargs are fine.
# W0622: Redefining id is fine.
disable=C0111,W0511,W0142,W0622
[Basic]
# Variable names can be 1 to 31 characters long, with lowercase and underscores
variable-rgx=[a-z_][a-z0-9_]{0,30}$
# Argument names can be 2 to 31 characters long, with lowercase and underscores
argument-rgx=[a-z_][a-z0-9_]{1,30}$
# Method names should be at least 3 characters long
# and be lowercased with underscores
method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|tearDown)$
# Module names matching cinder-* are ok (files in bin/)
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+)|(cinder-[a-z0-9_-]+))$
# Don't require docstrings on tests.
no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$
[Design]
max-public-methods=100
min-public-methods=0
max-args=6
[Variables]
dummy-variables-rgx=_
[Typecheck]
# Disable warnings on the HTTPSConnection classes because pylint doesn't
# support importing from six.moves yet, see:
# https://bitbucket.org/logilab/pylint/issue/550/
ignored-classes=HTTPSConnection

21
requirements.txt Normal file
View File

@ -0,0 +1,21 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
WebOb>=1.2.3 # MIT
Paste # MIT
Routes!=2.0,!=2.1,!=2.3.0,>=1.12.3;python_version=='2.7' # MIT
Routes!=2.0,!=2.3.0,>=1.12.3;python_version!='2.7' # MIT
six>=1.9.0 # MIT
python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0
keystoneauth1>=2.7.0 # Apache-2.0
oslo.concurrency>=3.8.0 # Apache-2.0
oslo.messaging>=5.2.0 # Apache-2.0
oslo.policy>=1.9.0 # Apache-2.0
oslo.serialization>=1.10.0 # Apache-2.0
oslo.service>=1.10.0 # Apache-2.0
oslo.utils>=3.14.0 # Apache-2.0
python-neutronclient>=4.2.0 # Apache-2.0
python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0
python-cinderclient!=1.7.0,!=1.7.1,>=1.6.0 # Apache-2.0
python-glanceclient>=2.0.0 # Apache-2.0

19
test-requirements.txt Normal file
View File

@ -0,0 +1,19 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
# Install bounded pep8/pyflakes first, then let flake8 install
hacking<0.11,>=0.10.0
anyjson>=0.3.3 # BSD
coverage>=3.6 # Apache-2.0
fixtures>=3.0.0 # Apache-2.0/BSD
mock>=2.0 # BSD
python-subunit>=0.0.18 # Apache-2.0/BSD
testtools>=1.4.0 # MIT
testrepository>=0.0.18 # Apache-2.0/BSD
testresources>=0.2.4 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
os-testr>=0.7.0 # Apache-2.0
tempest-lib>=0.14.0 # Apache-2.0
bandit>=1.0.1 # Apache-2.0

42
tools/check_exec.py Executable file
View File

@ -0,0 +1,42 @@
#!/usr/bin/python
#
# 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.
# Print a list and return with error if any executable files are found.
# Compatible with both python 2 and 3.
import os.path
import stat
import sys
if len(sys.argv) < 2:
print("Usage: %s <directory>" % sys.argv[0])
sys.exit(1)
directory = sys.argv[1]
executable = []
for root, mydir, myfile in os.walk(directory):
for f in myfile:
path = os.path.join(root, f)
mode = os.lstat(path).st_mode
if stat.S_IXUSR & mode:
executable.append(path)
if executable:
print("Executable files found:")
for f in executable:
print(f)
sys.exit(1)

268
tools/lintstack.py Executable file
View File

@ -0,0 +1,268 @@
#!/usr/bin/env python
# Copyright (c) 2013, AT&T Labs, Yun Mao <yunmao@gmail.com>
# 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.
"""pylint error checking."""
from __future__ import print_function
import json
import re
import sys
from pylint import lint
from pylint.reporters import text
from six.moves import cStringIO as StringIO
ignore_codes = [
# Note(maoy): E1103 is error code related to partial type inference
"E1103"
]
ignore_messages = [
# Note(maoy): this error message is the pattern of E0202. It should be
# ignored for cinder.tests modules
"An attribute affected in cinder.tests",
# Note(fengqian): this error message is the pattern of [E0611].
"No name 'urllib' in module '_MovedItems'",
# Note(e0ne): this error message is for SQLAlchemy update() calls
# It should be ignored because use six module to keep py3.X compatibility.
# in DB schema migrations.
"No value passed for parameter 'dml'",
# Note(xyang): these error messages are for the code [E1101].
# They should be ignored because 'sha256' and 'sha224' are functions in
# 'hashlib'.
"Module 'hashlib' has no 'sha256' member",
"Module 'hashlib' has no 'sha224' member",
# Note(aarefiev): this error message is for SQLAlchemy rename calls in
# DB migration(033_add_encryption_unique_key).
"Instance of 'Table' has no 'rename' member",
# NOTE(geguileo): these error messages are for code [E1101], and they can
# be ignored because a SQLAlchemy ORM class will have __table__ member
# during runtime.
"Class 'ConsistencyGroup' has no '__table__' member",
"Class 'Cgsnapshot' has no '__table__' member",
]
# Note(maoy): We ignore cinder.tests for now due to high false
# positive rate.
ignore_modules = ["cinder/tests/"]
# Note(thangp): E0213, E1101, and E1102 should be ignored for only
# cinder.object modules. E0213 and E1102 are error codes related to
# the first argument of a method, but should be ignored because the method
# is a remotable class method. E1101 is error code related to accessing a
# non-existent member of an object, but should be ignored because the object
# member is created dynamically.
objects_ignore_codes = ["E0213", "E1101", "E1102"]
# Note(thangp): The error messages are for codes [E1120, E1101] appearing in
# the cinder code base using objects. E1120 is an error code related no value
# passed for a parameter in function call, but should be ignored because it is
# reporting false positives. E1101 is error code related to accessing a
# non-existent member of an object, but should be ignored because the object
# member is created dynamically.
objects_ignore_messages = [
"No value passed for parameter 'id' in function call",
"Module 'cinder.objects' has no 'Backup' member",
"Module 'cinder.objects' has no 'BackupImport' member",
"Module 'cinder.objects' has no 'BackupList' member",
"Module 'cinder.objects' has no 'CGSnapshot' member",
"Module 'cinder.objects' has no 'CGSnapshotList' member",
"Module 'cinder.objects' has no 'ConsistencyGroup' member",
"Module 'cinder.objects' has no 'ConsistencyGroupList' member",
"Module 'cinder.objects' has no 'Service' member",
"Module 'cinder.objects' has no 'ServiceList' member",
"Module 'cinder.objects' has no 'Snapshot' member",
"Module 'cinder.objects' has no 'SnapshotList' member",
"Module 'cinder.objects' has no 'Volume' member",
"Module 'cinder.objects' has no 'VolumeList' member",
]
objects_ignore_modules = ["cinder/objects/"]
KNOWN_PYLINT_EXCEPTIONS_FILE = "tools/pylint_exceptions"
class LintOutput(object):
_cached_filename = None
_cached_content = None
def __init__(self, filename, lineno, line_content, code, message,
lintoutput):
self.filename = filename
self.lineno = lineno
self.line_content = line_content
self.code = code
self.message = message
self.lintoutput = lintoutput
@classmethod
def from_line(cls, line):
m = re.search(r"(\S+):(\d+): \[(\S+)(, \S+)?] (.*)", line)
matched = m.groups()
filename, lineno, code, message = (matched[0], int(matched[1]),
matched[2], matched[-1])
if cls._cached_filename != filename:
with open(filename) as f:
cls._cached_content = list(f.readlines())
cls._cached_filename = filename
line_content = cls._cached_content[lineno - 1].rstrip()
return cls(filename, lineno, line_content, code, message,
line.rstrip())
@classmethod
def from_msg_to_dict(cls, msg):
"""From the output of pylint msg, to a dict, where each key
is a unique error identifier, value is a list of LintOutput
"""
result = {}
for line in msg.splitlines():
obj = cls.from_line(line)
if obj.is_ignored():
continue
key = obj.key()
if key not in result:
result[key] = []
result[key].append(obj)
return result
def is_ignored(self):
if self.code in ignore_codes:
return True
if any(self.filename.startswith(name) for name in ignore_modules):
return True
if any(msg in self.message for msg in
(ignore_messages + objects_ignore_messages)):
return True
if (self.code in objects_ignore_codes and
any(self.filename.startswith(name)
for name in objects_ignore_modules)):
return True
if (self.code in objects_ignore_codes and
any(self.filename.startswith(name)
for name in objects_ignore_modules)):
return True
return False
def key(self):
if self.code in ["E1101", "E1103"]:
# These two types of errors are like Foo class has no member bar.
# We discard the source code so that the error will be ignored
# next time another Foo.bar is encountered.
return self.message, ""
return self.message, self.line_content.strip()
def json(self):
return json.dumps(self.__dict__)
def review_str(self):
return ("File %(filename)s\nLine %(lineno)d:%(line_content)s\n"
"%(code)s: %(message)s" % self.__dict__)
class ErrorKeys(object):
@classmethod
def print_json(cls, errors, output=sys.stdout):
print("# automatically generated by tools/lintstack.py", file=output)
for i in sorted(errors.keys()):
print(json.dumps(i), file=output)
@classmethod
def from_file(cls, filename):
keys = set()
for line in open(filename):
if line and line[0] != "#":
d = json.loads(line)
keys.add(tuple(d))
return keys
def run_pylint():
buff = StringIO()
reporter = text.ParseableTextReporter(output=buff)
args = ["--include-ids=y", "-E", "cinder"]
lint.Run(args, reporter=reporter, exit=False)
val = buff.getvalue()
buff.close()
return val
def generate_error_keys(msg=None):
print("Generating", KNOWN_PYLINT_EXCEPTIONS_FILE)
if msg is None:
msg = run_pylint()
errors = LintOutput.from_msg_to_dict(msg)
with open(KNOWN_PYLINT_EXCEPTIONS_FILE, "w") as f:
ErrorKeys.print_json(errors, output=f)
def validate(newmsg=None):
print("Loading", KNOWN_PYLINT_EXCEPTIONS_FILE)
known = ErrorKeys.from_file(KNOWN_PYLINT_EXCEPTIONS_FILE)
if newmsg is None:
print("Running pylint. Be patient...")
newmsg = run_pylint()
errors = LintOutput.from_msg_to_dict(newmsg)
print("Unique errors reported by pylint: was %d, now %d."
% (len(known), len(errors)))
passed = True
for err_key, err_list in errors.items():
for err in err_list:
if err_key not in known:
print(err.lintoutput)
print()
passed = False
if passed:
print("Congrats! pylint check passed.")
redundant = known - set(errors.keys())
if redundant:
print("Extra credit: some known pylint exceptions disappeared.")
for i in sorted(redundant):
print(json.dumps(i))
print("Consider regenerating the exception file if you will.")
else:
print("Please fix the errors above. If you believe they are false "
"positives, run 'tools/lintstack.py generate' to overwrite.")
sys.exit(1)
def usage():
print("""Usage: tools/lintstack.py [generate|validate]
To generate pylint_exceptions file: tools/lintstack.py generate
To validate the current commit: tools/lintstack.py
""")
def main():
option = "validate"
if len(sys.argv) > 1:
option = sys.argv[1]
if option == "generate":
generate_error_keys()
elif option == "validate":
validate()
else:
usage()
if __name__ == "__main__":
main()

59
tools/lintstack.sh Executable file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Copyright (c) 2012-2013, AT&T Labs, Yun Mao <yunmao@gmail.com>
# 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.
# Use lintstack.py to compare pylint errors.
# We run pylint twice, once on HEAD, once on the code before the latest
# commit for review.
set -e
TOOLS_DIR=$(cd $(dirname "$0") && pwd)
# Get the current branch name.
GITHEAD=`git rev-parse --abbrev-ref HEAD`
if [[ "$GITHEAD" == "HEAD" ]]; then
# In detached head mode, get revision number instead
GITHEAD=`git rev-parse HEAD`
echo "Currently we are at commit $GITHEAD"
else
echo "Currently we are at branch $GITHEAD"
fi
cp -f $TOOLS_DIR/lintstack.py $TOOLS_DIR/lintstack.head.py
if git rev-parse HEAD^2 2>/dev/null; then
# The HEAD is a Merge commit. Here, the patch to review is
# HEAD^2, the master branch is at HEAD^1, and the patch was
# written based on HEAD^2~1.
PREV_COMMIT=`git rev-parse HEAD^2~1`
git checkout HEAD~1
# The git merge is necessary for reviews with a series of patches.
# If not, this is a no-op so won't hurt either.
git merge $PREV_COMMIT
else
# The HEAD is not a merge commit. This won't happen on gerrit.
# Most likely you are running against your own patch locally.
# We assume the patch to examine is HEAD, and we compare it against
# HEAD~1
git checkout HEAD~1
fi
# First generate tools/pylint_exceptions from HEAD~1
$TOOLS_DIR/lintstack.head.py generate
# Then use that as a reference to compare against HEAD
git checkout $GITHEAD
$TOOLS_DIR/lintstack.head.py
echo "Check passed. FYI: the pylint exceptions are:"
cat $TOOLS_DIR/pylint_exceptions

52
tox.ini Normal file
View File

@ -0,0 +1,52 @@
[tox]
minversion = 1.8
skipsdist = True
envlist = py27,pep8
[testenv]
setenv = VIRTUAL_ENV={envdir}
usedevelop = True
install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
deps = -r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt
oslo.versionedobjects[fixtures]
#sitepackages=True
# By default ostestr will set concurrency
# to ncpu, to specify something else use
# the concurrency=<n> option.
# call ie: 'tox -epy27 -- --concurrency=4'
commands =
find . -type f -name "*.pyc" -delete
ostestr {posargs}
whitelist_externals =
bash
find
passenv = *_proxy *_PROXY
[testenv:pep8]
commands =
flake8 {posargs} .
{toxinidir}/tools/check_exec.py {toxinidir}/novajoin
[testenv:pylint]
deps = -r{toxinidir}/requirements.txt
pylint==0.26.0
commands = bash tools/lintstack.sh
[testenv:bandit]
# Skip B104 hardcoded_bind_all_interfaces
deps = -r{toxinidir}/test-requirements.txt
commands = bandit -r novajoin -n5 -x tests -ll -s B104
[flake8]
# Following checks are ignored on purpose.
#
# E251 unexpected spaces around keyword / parameter equals
# reason: no improvement in readability
ignore = E251,D100,D101,D102,D202,D208
exclude = .git,.venv,.tox,dist,tools,doc,*egg,build
max-complexity=30