Skip to content

Commit 742982a

Browse files
author
Steve Martinelli
committed
Add functional tests to osc
Create a script that kicks off function tests that exercise openstackclient commands against a cloud. If no keystone/openstack process is detected, a devstack instance is spun up and the tests are run against that. There is also a hook added to tox.ini so that we can run these tests easily from a gate job. Change-Id: I3cc8b2b800de7ca74af506d2c7e8ee481fa985f0
1 parent 02320a5 commit 742982a

10 files changed

Lines changed: 253 additions & 0 deletions

File tree

functional/__init__.py

Whitespace-only changes.

functional/common/__init__.py

Whitespace-only changes.

functional/common/exceptions.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
14+
class CommandFailed(Exception):
15+
def __init__(self, returncode, cmd, output, stderr):
16+
super(CommandFailed, self).__init__()
17+
self.returncode = returncode
18+
self.cmd = cmd
19+
self.stdout = output
20+
self.stderr = stderr
21+
22+
def __str__(self):
23+
return ("Command '%s' returned non-zero exit status %d.\n"
24+
"stdout:\n%s\n"
25+
"stderr:\n%s" % (self.cmd, self.returncode,
26+
self.stdout, self.stderr))

functional/common/test.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
import re
14+
import shlex
15+
import subprocess
16+
import testtools
17+
18+
import six
19+
20+
from functional.common import exceptions
21+
22+
23+
def execute(cmd, action, flags='', params='', fail_ok=False,
24+
merge_stderr=False):
25+
"""Executes specified command for the given action."""
26+
cmd = ' '.join([cmd, flags, action, params])
27+
cmd = shlex.split(cmd.encode('utf-8'))
28+
result = ''
29+
result_err = ''
30+
stdout = subprocess.PIPE
31+
stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE
32+
proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
33+
result, result_err = proc.communicate()
34+
if not fail_ok and proc.returncode != 0:
35+
raise exceptions.CommandFailed(proc.returncode, cmd, result,
36+
result_err)
37+
return result
38+
39+
40+
class TestCase(testtools.TestCase):
41+
42+
delimiter_line = re.compile('^\+\-[\+\-]+\-\+$')
43+
44+
def openstack(self, action, flags='', params='', fail_ok=False):
45+
"""Executes openstackclient command for the given action."""
46+
return execute('openstack', action, flags, params, fail_ok)
47+
48+
def assert_table_structure(self, items, field_names):
49+
"""Verify that all items have keys listed in field_names."""
50+
for item in items:
51+
for field in field_names:
52+
self.assertIn(field, item)
53+
54+
def assert_show_fields(self, items, field_names):
55+
"""Verify that all items have keys listed in field_names."""
56+
for item in items:
57+
for key in six.iterkeys(item):
58+
self.assertIn(key, field_names)
59+
60+
def parse_show(self, raw_output):
61+
"""Return list of dicts with item values parsed from cli output."""
62+
63+
items = []
64+
table_ = self.table(raw_output)
65+
for row in table_['values']:
66+
item = {}
67+
item[row[0]] = row[1]
68+
items.append(item)
69+
return items
70+
71+
def parse_listing(self, raw_output):
72+
"""Return list of dicts with basic item parsed from cli output."""
73+
74+
items = []
75+
table_ = self.table(raw_output)
76+
for row in table_['values']:
77+
item = {}
78+
for col_idx, col_key in enumerate(table_['headers']):
79+
item[col_key] = row[col_idx]
80+
items.append(item)
81+
return items
82+
83+
def table(self, output_lines):
84+
"""Parse single table from cli output.
85+
86+
Return dict with list of column names in 'headers' key and
87+
rows in 'values' key.
88+
"""
89+
table_ = {'headers': [], 'values': []}
90+
columns = None
91+
92+
if not isinstance(output_lines, list):
93+
output_lines = output_lines.split('\n')
94+
95+
if not output_lines[-1]:
96+
# skip last line if empty (just newline at the end)
97+
output_lines = output_lines[:-1]
98+
99+
for line in output_lines:
100+
if self.delimiter_line.match(line):
101+
columns = self._table_columns(line)
102+
continue
103+
if '|' not in line:
104+
continue
105+
row = []
106+
for col in columns:
107+
row.append(line[col[0]:col[1]].strip())
108+
if table_['headers']:
109+
table_['values'].append(row)
110+
else:
111+
table_['headers'] = row
112+
113+
return table_
114+
115+
def _table_columns(self, first_table_row):
116+
"""Find column ranges in output line.
117+
118+
Return list of tuples (start,end) for each column
119+
detected by plus (+) characters in delimiter line.
120+
"""
121+
positions = []
122+
start = 1 # there is '+' at 0
123+
while start < len(first_table_row):
124+
end = first_table_row.find('+', start)
125+
if end == -1:
126+
break
127+
positions.append((start, end))
128+
start = end + 1
129+
return positions

functional/harpoon.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/bash
2+
3+
FUNCTIONAL_TEST_DIR=$(cd $(dirname "$0") && pwd)
4+
source $FUNCTIONAL_TEST_DIR/harpoonrc
5+
6+
OPENSTACKCLIENT_DIR=$FUNCTIONAL_TEST_DIR/..
7+
8+
if [[ -z $DEVSTACK_DIR ]]; then
9+
echo "guessing location of devstack"
10+
DEVSTACK_DIR=$OPENSTACKCLIENT_DIR/../devstack
11+
fi
12+
13+
function setup_credentials {
14+
RC_FILE=$DEVSTACK_DIR/accrc/$HARPOON_USER/$HARPOON_TENANT
15+
source $RC_FILE
16+
echo 'sourcing' $RC_FILE
17+
echo 'running tests with'
18+
env | grep OS
19+
}
20+
21+
function run_tests {
22+
cd $FUNCTIONAL_TEST_DIR
23+
python -m testtools.run discover
24+
rvalue=$?
25+
cd $OPENSTACKCLIENT_DIR
26+
exit $rvalue
27+
}
28+
29+
setup_credentials
30+
run_tests

functional/harpoonrc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Global options
2+
#RECLONE=yes
3+
4+
# Devstack options
5+
#ADMIN_PASSWORD=openstack
6+
#MYSQL_PASSWORD=openstack
7+
#RABBIT_PASSWORD=openstack
8+
#SERVICE_TOKEN=openstack
9+
#SERVICE_PASSWORD=openstack
10+
11+
# Harpoon options
12+
HARPOON_USER=admin
13+
HARPOON_TENANT=admin
14+
#DEVSTACK_DIR=/opt/stack/devstack

functional/tests/__init__.py

Whitespace-only changes.

functional/tests/test_identity.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
from functional.common import exceptions
14+
from functional.common import test
15+
16+
17+
class IdentityV2Tests(test.TestCase):
18+
"""Functional tests for Identity V2 commands. """
19+
20+
def test_user_list(self):
21+
field_names = ['ID', 'Name']
22+
raw_output = self.openstack('user list')
23+
items = self.parse_listing(raw_output)
24+
self.assert_table_structure(items, field_names)
25+
26+
def test_user_get(self):
27+
field_names = ['email', 'enabled', 'id', 'name',
28+
'project_id', 'username']
29+
raw_output = self.openstack('user show admin')
30+
items = self.parse_show(raw_output)
31+
self.assert_show_fields(items, field_names)
32+
33+
def test_bad_user_command(self):
34+
self.assertRaises(exceptions.CommandFailed,
35+
self.openstack, 'user unlist')

post_test_hook.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/bash
2+
3+
# This is a script that kicks off a series of functional tests against an
4+
# OpenStack cloud. It will attempt to create an instance if one is not
5+
# available. Do not run this script unless you know what you're doing.
6+
# For more information refer to:
7+
# http://docs.openstack.org/developer/python-openstackclient/
8+
9+
set -xe
10+
11+
OPENSTACKCLIENT_DIR=$(cd $(dirname "$0") && pwd)
12+
13+
cd $OPENSTACKCLIENT_DIR
14+
echo "Running openstackclient functional test suite"
15+
sudo -H -u stack tox -e functional

tox.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ setenv = VIRTUAL_ENV={envdir}
1111
deps = -r{toxinidir}/requirements.txt
1212
-r{toxinidir}/test-requirements.txt
1313
commands = python setup.py testr --testr-args='{posargs}'
14+
whitelist_externals = bash
1415

1516
[testenv:pep8]
1617
commands = flake8
1718

19+
[testenv:functional]
20+
commands = bash -x {toxinidir}/functional/harpoon.sh
21+
1822
[testenv:venv]
1923
commands = {posargs}
2024

0 commit comments

Comments
 (0)