Add @skip_if_disabled decorator to test.utils and integrate

it into the base functional API test case. The S3 functional
test case now uses test_api.TestApi as its base class and
the setUp() method sets the disabled and disabled_message
attributes that the @skip_if_disabled decorator uses.

The S3 test case now tests all the methods that are tested
for the filesystem store driver in test_api.TestApi

Removed the test.functional.test_s3.conf file as it was no
longer needed.

Change-Id: I308d7a655c8c26339d0e2560634421935bbc8b5d
This commit is contained in:
Jay Pipes 2011-08-08 12:56:30 -04:00
parent de78df9020
commit 72e2e31fea
6 changed files with 61 additions and 391 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
*.pyc *.pyc
*.swp
*.log *.log
.glance-venv .glance-venv
tests.sqlite tests.sqlite

View File

@ -252,6 +252,8 @@ class FunctionalTest(unittest.TestCase):
servers and clients and not just the stubbed out interfaces servers and clients and not just the stubbed out interfaces
""" """
disabled = False
def setUp(self): def setUp(self):
self.test_id = random.randint(0, 100000) self.test_id = random.randint(0, 100000)
@ -276,6 +278,7 @@ class FunctionalTest(unittest.TestCase):
self.files_to_destroy = [] self.files_to_destroy = []
def tearDown(self): def tearDown(self):
if not self.disabled:
self.cleanup() self.cleanup()
# We destroy the test data store between each test case, # We destroy the test data store between each test case,
# and recreate it, which ensures that we have no side-effects # and recreate it, which ensures that we have no side-effects

View File

@ -24,16 +24,17 @@ import os
import tempfile import tempfile
from glance.tests import functional from glance.tests import functional
from glance.tests.utils import execute from glance.tests.utils import execute, skip_if_disabled
FIVE_KB = 5 * 1024 FIVE_KB = 5 * 1024
FIVE_GB = 5 * 1024 * 1024 * 1024 FIVE_GB = 5 * 1024 * 1024 * 1024
class TestApiHttplib2(functional.FunctionalTest): class TestApi(functional.FunctionalTest):
"""Functional tests using httplib2 against the API server""" """Functional tests using httplib2 against the API server"""
@skip_if_disabled
def test_get_head_simple_post(self): def test_get_head_simple_post(self):
""" """
We test the following sequential series of actions: We test the following sequential series of actions:
@ -273,6 +274,7 @@ class TestApiHttplib2(functional.FunctionalTest):
self.stop_servers() self.stop_servers()
@skip_if_disabled
def test_queued_process_flow(self): def test_queued_process_flow(self):
""" """
We test the process flow where a user registers an image We test the process flow where a user registers an image
@ -391,6 +393,7 @@ class TestApiHttplib2(functional.FunctionalTest):
self.stop_servers() self.stop_servers()
@skip_if_disabled
def test_version_variations(self): def test_version_variations(self):
""" """
We test that various calls to the images and root endpoints are We test that various calls to the images and root endpoints are
@ -549,6 +552,7 @@ class TestApiHttplib2(functional.FunctionalTest):
self.stop_servers() self.stop_servers()
@skip_if_disabled
def test_size_greater_2G_mysql(self): def test_size_greater_2G_mysql(self):
""" """
A test against the actual datastore backend for the registry A test against the actual datastore backend for the registry
@ -592,6 +596,7 @@ class TestApiHttplib2(functional.FunctionalTest):
self.stop_servers() self.stop_servers()
@skip_if_disabled
def test_traceback_not_consumed(self): def test_traceback_not_consumed(self):
""" """
A test that errors coming from the POST API do not A test that errors coming from the POST API do not
@ -620,6 +625,7 @@ class TestApiHttplib2(functional.FunctionalTest):
self.stop_servers() self.stop_servers()
@skip_if_disabled
def test_filtered_images(self): def test_filtered_images(self):
""" """
Set up four test images and ensure each query param filter works Set up four test images and ensure each query param filter works
@ -852,6 +858,7 @@ class TestApiHttplib2(functional.FunctionalTest):
self.stop_servers() self.stop_servers()
@skip_if_disabled
def test_limited_images(self): def test_limited_images(self):
""" """
Ensure marker and limit query params work Ensure marker and limit query params work
@ -940,6 +947,7 @@ class TestApiHttplib2(functional.FunctionalTest):
self.stop_servers() self.stop_servers()
@skip_if_disabled
def test_ordered_images(self): def test_ordered_images(self):
""" """
Set up three test images and ensure each query param filter works Set up three test images and ensure each query param filter works
@ -1050,6 +1058,7 @@ class TestApiHttplib2(functional.FunctionalTest):
self.stop_servers() self.stop_servers()
@skip_if_disabled
def test_duplicate_image_upload(self): def test_duplicate_image_upload(self):
""" """
Upload initial image, then attempt to upload duplicate image Upload initial image, then attempt to upload duplicate image

View File

@ -1,21 +0,0 @@
[DEFAULT]
# Set the following to Amazon S3 credentials that you want
# to use while functional testing the S3 backend.
# Address where the S3 authentication service lives
s3_store_host = s3.amazonaws.com
# User to authenticate against the S3 authentication service
s3_store_access_key = <20-char AWS access key>
# Auth key for the user authenticating against the
# S3 authentication service
s3_store_secret_key = <40-char AWS secret key>
# Container within the account that the account should use
# for storing images in S3. Note that S3 has a flat namespace,
# so you need a unique bucket name for your glance images. An
# easy way to do this is append your lower-cased AWS access key
# to "glance"
s3_store_bucket = <20-char AWS access key - lowercased>glance

View File

@ -36,23 +36,21 @@ import os
import tempfile import tempfile
import unittest import unittest
from glance.tests import functional from glance.tests.functional import test_api
from glance.tests.utils import execute from glance.tests.utils import execute
from glance.tests import utils
FIVE_KB = 5 * 1024 FIVE_KB = 5 * 1024
FIVE_GB = 5 * 1024 * 1024 * 1024 FIVE_GB = 5 * 1024 * 1024 * 1024
class TestS3(functional.FunctionalTest): class TestS3(test_api.TestApi):
"""Functional tests for the S3 backend""" """Functional tests for the S3 backend"""
# Test machines can set the GLANCE_TEST_MIGRATIONS_CONF variable # Test machines can set the GLANCE_TEST_S3_CONF variable
# to override the location of the config file for migration testing # to override the location of the config file for migration testing
CONFIG_FILE_PATH = os.environ.get('GLANCE_TEST_S3_CONF', CONFIG_FILE_PATH = os.environ.get('GLANCE_TEST_S3_CONF')
os.path.join('glance', 'tests',
'functional',
'test_s3.conf'))
def setUp(self): def setUp(self):
""" """
@ -61,11 +59,16 @@ class TestS3(functional.FunctionalTest):
If the connection fails, mark all tests to skip. If the connection fails, mark all tests to skip.
""" """
self.inited = False self.inited = False
self.skip_tests = True self.disabled = True
if self.inited: if self.inited:
return return
if not self.CONFIG_FILE_PATH:
self.disabled_message = "GLANCE_TEST_S3_CONF environ not set."
self.inited = True
return
if os.path.exists(TestS3.CONFIG_FILE_PATH): if os.path.exists(TestS3.CONFIG_FILE_PATH):
cp = ConfigParser.RawConfigParser() cp = ConfigParser.RawConfigParser()
try: try:
@ -74,8 +77,8 @@ class TestS3(functional.FunctionalTest):
for key, value in defaults.items(): for key, value in defaults.items():
self.__dict__[key] = value self.__dict__[key] = value
except ConfigParser.ParsingError, e: except ConfigParser.ParsingError, e:
print ("Failed to read test_s3.conf config file. " self.disabled_message = ("Failed to read test_s3.conf config "
"Got error: %s" % e) "file. Got error: %s" % e)
super(TestS3, self).setUp() super(TestS3, self).setUp()
self.inited = True self.inited = True
return return
@ -89,8 +92,8 @@ class TestS3(functional.FunctionalTest):
secret_key = self.s3_store_secret_key secret_key = self.s3_store_secret_key
bucket_name = self.s3_store_bucket bucket_name = self.s3_store_bucket
except AttributeError, e: except AttributeError, e:
print ("Failed to find required configuration options for " self.disabled_message = ("Failed to find required configuration "
"S3 store. Got error: %s" % e) "options for S3 store. Got error: %s" % e)
self.inited = True self.inited = True
super(TestS3, self).setUp() super(TestS3, self).setUp()
return return
@ -104,16 +107,17 @@ class TestS3(functional.FunctionalTest):
if bucket.name == bucket_name: if bucket.name == bucket_name:
self.bucket = bucket self.bucket = bucket
except S3ResponseError, e: except S3ResponseError, e:
print ("Failed to connect to S3 with credentials," self.disabled_message = ("Failed to connect to S3 with "
"to find bucket. Got error: %s" % e) "credentials, to find bucket. "
"Got error: %s" % e)
self.inited = True self.inited = True
super(TestS3, self).setUp() super(TestS3, self).setUp()
return return
except TypeError, e: except TypeError, e:
# This hack is necessary because of a bug in boto 1.9b: # This hack is necessary because of a bug in boto 1.9b:
# http://code.google.com/p/boto/issues/detail?id=540 # http://code.google.com/p/boto/issues/detail?id=540
print ("Failed to connect to S3 with credentials. " self.disabled_message = ("Failed to connect to S3 with "
"Got error: %s" % e) "credentials. Got error: %s" % e)
self.inited = True self.inited = True
super(TestS3, self).setUp() super(TestS3, self).setUp()
return return
@ -124,21 +128,22 @@ class TestS3(functional.FunctionalTest):
try: try:
self.bucket = s3_conn.create_bucket(bucket_name) self.bucket = s3_conn.create_bucket(bucket_name)
except boto.exception.S3ResponseError, e: except boto.exception.S3ResponseError, e:
print ("Failed to create bucket. Got error: %s" % e) self.disabled_message = ("Failed to create bucket. "
"Got error: %s" % e)
self.inited = True self.inited = True
super(TestS3, self).setUp() super(TestS3, self).setUp()
return return
else: else:
self.clear_bucket() self.clear_bucket()
self.skip_tests = False self.disabled = False
self.inited = True self.inited = True
self.default_store = 's3' self.default_store = 's3'
super(TestS3, self).setUp() super(TestS3, self).setUp()
def tearDown(self): def tearDown(self):
if not self.skip_tests: if not self.disabled:
self.clear_bucket() self.clear_bucket()
super(TestS3, self).tearDown() super(TestS3, self).tearDown()
@ -150,346 +155,7 @@ class TestS3(functional.FunctionalTest):
for key in keys: for key in keys:
key.delete() key.delete()
def test_add_list_delete_list(self): @utils.skip_if_disabled
"""
We test the following:
0. GET /images
- Verify no public images
1. GET /images/detail
- Verify no public images
2. HEAD /images/1
- Verify 404 returned
3. POST /images with public image named Image1 with a location
attribute and no custom properties
- Verify 201 returned
4. HEAD /images/1
- Verify HTTP headers have correct information we just added
5. GET /images/1
- Verify all information on image we just added is correct
6. GET /images
- Verify the image we just added is returned
7. GET /images/detail
- Verify the image we just added is returned
8. PUT /images/1 with custom properties of "distro" and "arch"
- Verify 200 returned
9. GET /images/1
- Verify updated information about image was stored
10. PUT /images/1
- Remove a previously existing property.
11. PUT /images/1
- Add a previously deleted property.
"""
if self.skip_tests:
return True
self.cleanup()
self.start_servers(**self.__dict__.copy())
api_port = self.api_port
registry_port = self.registry_port
# 0. GET /images
# Verify no public images
cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertEqual('{"images": []}', out.strip())
# 1. GET /images/detail
# Verify no public images
cmd = "curl http://0.0.0.0:%d/v1/images/detail" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertEqual('{"images": []}', out.strip())
# 2. HEAD /images/1
# Verify 404 returned
cmd = "curl -i -X HEAD http://0.0.0.0:%d/v1/images/1" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\r\n")
status_line = lines[0]
self.assertEqual("HTTP/1.1 404 Not Found", status_line)
# 3. POST /images with public image named Image1
# attribute and no custom properties. Verify a 200 OK is returned
image_data = "*" * FIVE_KB
cmd = ("curl -i -X POST "
"-H 'Expect: ' " # Necessary otherwise sends 100 Continue
"-H 'Content-Type: application/octet-stream' "
"-H 'X-Image-Meta-Name: Image1' "
"-H 'X-Image-Meta-Is-Public: True' "
"--data-binary \"%s\" "
"http://0.0.0.0:%d/v1/images") % (image_data, api_port)
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\r\n")
status_line = lines[0]
self.assertEqual("HTTP/1.1 201 Created", status_line, out)
# 4. HEAD /images
# Verify image found now
cmd = "curl -i -X HEAD http://0.0.0.0:%d/v1/images/1" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\r\n")
status_line = lines[0]
self.assertEqual("HTTP/1.1 200 OK", status_line)
self.assertTrue("X-Image-Meta-Name: Image1" in out)
# 5. GET /images/1
# Verify all information on image we just added is correct
cmd = "curl -i http://0.0.0.0:%d/v1/images/1" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\r\n")
self.assertEqual("HTTP/1.1 200 OK", lines.pop(0))
# Handle the headers
image_headers = {}
std_headers = {}
other_lines = []
for line in lines:
if line.strip() == '':
continue
if line.startswith("X-Image"):
pieces = line.split(":")
key = pieces[0].strip()
value = ":".join(pieces[1:]).strip()
image_headers[key] = value
elif ':' in line:
pieces = line.split(":")
key = pieces[0].strip()
value = ":".join(pieces[1:]).strip()
std_headers[key] = value
else:
other_lines.append(line)
expected_image_headers = {
'X-Image-Meta-Id': '1',
'X-Image-Meta-Name': 'Image1',
'X-Image-Meta-Is_public': 'True',
'X-Image-Meta-Status': 'active',
'X-Image-Meta-Disk_format': '',
'X-Image-Meta-Container_format': '',
'X-Image-Meta-Size': str(FIVE_KB),
'X-Image-Meta-Location': 's3://%s:%s@%s/%s/1' % (
self.s3_store_access_key,
self.s3_store_secret_key,
self.s3_store_host,
self.s3_store_bucket)}
expected_std_headers = {
'Content-Length': str(FIVE_KB),
'Content-Type': 'application/octet-stream'}
for expected_key, expected_value in expected_image_headers.items():
self.assertTrue(expected_key in image_headers,
"Failed to find key %s in image_headers"
% expected_key)
self.assertEqual(expected_value, image_headers[expected_key],
"For key '%s' expected header value '%s'. Got '%s'"
% (expected_key,
expected_value,
image_headers[expected_key]))
for expected_key, expected_value in expected_std_headers.items():
self.assertTrue(expected_key in std_headers,
"Failed to find key %s in std_headers"
% expected_key)
self.assertEqual(expected_value, std_headers[expected_key],
"For key '%s' expected header value '%s'. Got '%s'"
% (expected_key,
expected_value,
std_headers[expected_key]))
# Now the image data...
expected_image_data = "*" * FIVE_KB
# Should only be a single "line" left, and
# that's the image data
self.assertEqual(1, len(other_lines))
self.assertEqual(expected_image_data, other_lines[0])
# 6. GET /images
# Verify no public images
cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
expected_result = {"images": [
{"container_format": None,
"disk_format": None,
"id": 1,
"name": "Image1",
"checksum": "c2e5db72bd7fd153f53ede5da5a06de3",
"size": 5120}]}
self.assertEqual(expected_result, json.loads(out.strip()))
# 7. GET /images/detail
# Verify image and all its metadata
cmd = "curl http://0.0.0.0:%d/v1/images/detail" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
expected_image = {
"status": "active",
"name": "Image1",
"deleted": False,
"container_format": None,
"disk_format": None,
"id": 1,
'location': 's3://%s:%s@%s/%s/1' % (
self.s3_store_access_key,
self.s3_store_secret_key,
self.s3_store_host,
self.s3_store_bucket),
"is_public": True,
"deleted_at": None,
"properties": {},
"size": 5120}
image = json.loads(out.strip())['images'][0]
for expected_key, expected_value in expected_image.items():
self.assertTrue(expected_key in image,
"Failed to find key %s in image"
% expected_key)
self.assertEqual(expected_value, expected_image[expected_key],
"For key '%s' expected header value '%s'. Got '%s'"
% (expected_key,
expected_value,
image[expected_key]))
# 8. PUT /images/1 with custom properties of "distro" and "arch"
# Verify 200 returned
cmd = ("curl -i -X PUT "
"-H 'X-Image-Meta-Property-Distro: Ubuntu' "
"-H 'X-Image-Meta-Property-Arch: x86_64' "
"http://0.0.0.0:%d/v1/images/1") % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\r\n")
status_line = lines[0]
self.assertEqual("HTTP/1.1 200 OK", status_line)
# 9. GET /images/detail
# Verify image and all its metadata
cmd = "curl http://0.0.0.0:%d/v1/images/detail" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
expected_image = {
"status": "active",
"name": "Image1",
"deleted": False,
"container_format": None,
"disk_format": None,
"id": 1,
'location': 's3://%s:%s@%s/%s/1' % (
self.s3_store_access_key,
self.s3_store_secret_key,
self.s3_store_host,
self.s3_store_bucket),
"is_public": True,
"deleted_at": None,
"properties": {'distro': 'Ubuntu', 'arch': 'x86_64'},
"size": 5120}
image = json.loads(out.strip())['images'][0]
for expected_key, expected_value in expected_image.items():
self.assertTrue(expected_key in image,
"Failed to find key %s in image"
% expected_key)
self.assertEqual(expected_value, image[expected_key],
"For key '%s' expected header value '%s'. Got '%s'"
% (expected_key,
expected_value,
image[expected_key]))
# 10. PUT /images/1 and remove a previously existing property.
cmd = ("curl -i -X PUT "
"-H 'X-Image-Meta-Property-Arch: x86_64' "
"http://0.0.0.0:%d/v1/images/1") % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\r\n")
status_line = lines[0]
self.assertEqual("HTTP/1.1 200 OK", status_line)
cmd = "curl http://0.0.0.0:%d/v1/images/detail" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
image = json.loads(out.strip())['images'][0]
self.assertEqual(1, len(image['properties']))
self.assertEqual('x86_64', image['properties']['arch'])
# 11. PUT /images/1 and add a previously deleted property.
cmd = ("curl -i -X PUT "
"-H 'X-Image-Meta-Property-Distro: Ubuntu' "
"-H 'X-Image-Meta-Property-Arch: x86_64' "
"http://0.0.0.0:%d/v1/images/1") % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\r\n")
status_line = lines[0]
self.assertEqual("HTTP/1.1 200 OK", status_line)
cmd = "curl http://0.0.0.0:%d/v1/images/detail" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
image = json.loads(out.strip())['images'][0]
self.assertEqual(2, len(image['properties']))
self.assertEqual('x86_64', image['properties']['arch'])
self.assertEqual('Ubuntu', image['properties']['distro'])
self.stop_servers()
def test_delete_not_existing(self): def test_delete_not_existing(self):
""" """
We test the following: We test the following:
@ -499,9 +165,6 @@ class TestS3(functional.FunctionalTest):
1. DELETE /images/1 1. DELETE /images/1
- Verify 404 - Verify 404
""" """
if self.skip_tests:
return True
self.cleanup() self.cleanup()
self.start_servers(**self.__dict__.copy()) self.start_servers(**self.__dict__.copy())

View File

@ -17,6 +17,7 @@
"""Common utilities used in testing""" """Common utilities used in testing"""
import functools
import os import os
import socket import socket
import subprocess import subprocess
@ -39,7 +40,7 @@ class skip_test(object):
class skip_if(object): class skip_if(object):
"""Decorator that skips a test if contition is true.""" """Decorator that skips a test if condition is true."""
def __init__(self, condition, msg): def __init__(self, condition, msg):
self.condition = condition self.condition = condition
self.message = msg self.message = msg
@ -72,6 +73,20 @@ class skip_unless(object):
return _skipper return _skipper
def skip_if_disabled(func):
"""Decorator that skips a test if test case is disabled."""
@functools.wraps(func)
def wrapped(*a, **kwargs):
func.__test__ = False
test_obj = a[0]
message = getattr(test_obj, 'disabled_message',
'Test disabled')
if test_obj.disabled:
raise nose.SkipTest(message)
func(*a, **kwargs)
return wrapped
def execute(cmd, raise_error=True): def execute(cmd, raise_error=True):
""" """
Executes a command in a subprocess. Returns a tuple Executes a command in a subprocess. Returns a tuple