Adds End To End Tests

This patchset adds the end to end tests for Poppy.
The test steps include,
1) Deploying a WP site on a cloud server
2) Creating a Poppy Service for the WP site
3) Reporting page load metrics using webpagtest

Change-Id: I5f96cd21ac59806310f314e27da4f07642d3cfb0
This commit is contained in:
Malini Kamalambal 2014-11-25 16:27:07 -05:00
parent 92b8e69942
commit 8a0e22fb60
11 changed files with 908 additions and 1 deletions

View File

91
tests/endtoend/base.py Normal file
View File

@ -0,0 +1,91 @@
# Copyright (c) 2014 Rackspace, 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 BeautifulSoup
from cafe.drivers.unittest import fixtures
import requests
from tests.api.utils import client
from tests.endtoend.utils import config
from tests.endtoend.utils import heatclient
from tests.endtoend.utils import wptclient
class TestBase(fixtures.BaseTestFixture):
"""Base class for End To End CDN Tests
The tests do the following,
1. Spins up a wordpress site on a cloud server.
2. Create a Poppy service via API call using the origin & domain
feom Step 1.
3. Measures the pageload performance of the CDN enabled website.
"""
@classmethod
def setUpClass(cls):
super(TestBase, cls).setUpClass()
cls.auth_config = config.AuthConfig()
cls.auth_client = client.AuthClient()
auth_token, project_id = cls.auth_client.authenticate_user(
cls.auth_config.base_url,
cls.auth_config.user_name,
cls.auth_config.api_key)
cls.poppy_config = config.PoppyConfig()
cls.url = cls.poppy_config.base_url
cls.poppy_client = client.PoppyClient(
cls.url, auth_token, project_id,
serialize_format='json',
deserialize_format='json')
cls.test_config = config.TestConfig()
cls.heat_config = config.OrchestrationConfig()
heat_url = cls.heat_config.base_url + '/' + project_id
cls.heat_client = heatclient.HeatClient(heat_url=heat_url,
token=auth_token)
cls.wpt_config = config.WebPageTestConfig()
cls.wpt_client = wptclient.WebpageTestClient(
wpt_url=cls.wpt_config.base_url, api_key=cls.wpt_config.api_key)
def get_content(self, url):
"""Get content from the url
:param url: url to get content from
:returns: content fetched from the url
"""
response = requests.get(url)
content = BeautifulSoup.BeautifulSoup(response.text)
return content.findAll()
def assertSameContent(self, origin_url, access_url):
"""Asserts that the origin & access_url serve the same content
:param origin: Origin website
:param access_url: CDN enabled url of the origin website
:returns: True/False
"""
origin_content = self.get_content(url=origin_url)
cdn_content = self.get_content(url=access_url)
self.assertEqual(origin_content, cdn_content)
@classmethod
def tearDownClass(cls):
"""Deletes the added resources."""
super(TestBase, cls).tearDownClass()

View File

@ -0,0 +1,99 @@
# coding= utf-8
# Copyright (c) 2014 Rackspace, 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 random
import string
import uuid
from tests.endtoend import base
class TestWebsiteCDN(base.TestBase):
"""Tests for CDN enabling a website."""
def setUp(self):
super(TestWebsiteCDN, self).setUp()
def _random_string(length=12):
return ''.join([random.choice(string.ascii_letters)
for _ in range(length)])
self.stack_name = _random_string()
self.domain_name = 'TestCDN-' + _random_string() + '.org'
# Deploys a test website to a cloud server
self.heat_client.create_stack(yaml_path=self.heat_config.yaml_path,
stack_name=self.stack_name,
domain_name=self.domain_name)
print('Stack Name', self.stack_name)
print('Domain Name', self.domain_name)
self.heat_client.wait_for_stack_status(stack_name=self.stack_name)
self.origin = self.heat_client.get_server_ip(
stack_name=self.stack_name)
print('Origin', self.origin)
def test_enable_cdn(self):
# Create a Poppy Service for the test website
domain_list = [{"domain": self.domain_name}]
origin_list = [{"origin": self.origin,
"port": 80,
"ssl": False}]
caching_list = []
self.service_name = str(uuid.uuid1())
resp = self.poppy_client.create_service(
service_name=self.service_name,
domain_list=domain_list,
origin_list=origin_list,
caching_list=caching_list,
flavor_id=self.poppy_config.flavor)
self.assertEqual(resp.status_code, 202)
self.poppy_client.wait_for_service_status(
service_name=self.service_name,
status='DEPLOYED')
resp = self.poppy_client.get_service(service_name=self.service_name)
links = resp.json()['links']
access_url = [link['href'] for link in links if
link['rel'] == 'access_url']
access_url = 'http://' + access_url[0]
origin_url = 'http://' + self.origin
self.assertSameContent(origin_url=origin_url, access_url=access_url)
# Benchmark page load metrics for the CDN enabled website
wpt_test_results = {}
for location in self.wpt_config.test_locations:
wpt_test_url = self.wpt_client.start_test(access_url=access_url,
test_location=location,
runs=2)
wpt_test_results[location] = wpt_test_url
'''self.wpt_client.wait_for_test_status(status='COMPLETE',
test_url=wpt_test_url)
wpt_test_results[location] = self.wpt_client.get_test_details(
test_url=wpt_test_url)
'''
print(wpt_test_results)
def tearDown(self):
self.heat_client.delete_stack(stack_name=self.stack_name)
self.poppy_client.delete_service(service_name=self.service_name)
super(TestWebsiteCDN, self).tearDown()

View File

View File

@ -0,0 +1,106 @@
# Copyright (c) 2014 Rackspace, 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 cafe.engine.models import data_interfaces
class PoppyConfig(data_interfaces.ConfigSectionInterface):
"""Defines the config values for poppy."""
SECTION_NAME = 'poppy'
@property
def base_url(self):
"""poppy endpoint."""
return self.get('base_url')
@property
def flavor(self):
"""poppy flavor to use in create service call."""
return self.get('flavor')
class TestConfig(data_interfaces.ConfigSectionInterface):
"""Defines the config values specific to test execution."""
SECTION_NAME = 'test_configuration'
@property
def provider_validation(self):
"""Boolean value indicating if tests verify provider side details."""
return self.get_boolean('provider_validation')
@property
def status_check_retry_interval(self):
"""Int value to set retry intervals for status check."""
return int(self.get('status_check_retry_interval'))
@property
def status_check_retry_timeout(self):
"""Int value to set timeout for status check."""
return int(self.get('status_check_retry_timeout'))
class AuthConfig(data_interfaces.ConfigSectionInterface):
"""Defines the auth config values."""
SECTION_NAME = 'auth'
@property
def base_url(self):
"""Auth endpoint."""
return self.get('base_url')
@property
def user_name(self):
"""The name of the user, if applicable."""
return self.get('user_name')
@property
def api_key(self):
"""The user's api key, if applicable."""
return self.get_raw('api_key')
class OrchestrationConfig(data_interfaces.ConfigSectionInterface):
"""Defines the Rackspace orchestration config values."""
SECTION_NAME = 'orchestration'
@property
def base_url(self):
"""Orchestration base url."""
return self.get('base_url')
@property
def yaml_path(self):
"""path to the heat yaml file."""
return self.get('yaml_path')
class WebPageTestConfig(data_interfaces.ConfigSectionInterface):
"""Defines the webpage test config values."""
SECTION_NAME = 'webpagetest'
@property
def base_url(self):
"""Auth endpoint."""
return self.get('base_url')
@property
def api_key(self):
"""The user's api key."""
return self.get_raw('api_key')
@property
def test_locations(self):
"""Locations from which to test page load."""
return self.get('test_locations').split(',')

View File

@ -0,0 +1,116 @@
# Copyright (c) 2014 Rackspace, 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 json
import time
import requests
class HeatClient(object):
def __init__(self, heat_url, token):
self.heat_url = heat_url
self.headers = {
'content-type': 'application/json',
'Accept': 'application/json',
'X-Auth-Token': token
}
def create_stack(self, yaml_path, stack_name, domain_name):
"""Creates a HEAT stack
:param yaml_path: path to the uaml file relative to endtoend directory
(eg. endtoend/heat_file.yaml)
:param stack_name: name of the heat stack containing the test site
:param domain_name: domain name to use for the deployed website
:returns: response to HEAT API call
"""
url = self.heat_url + '/stacks'
template_file = open(yaml_path, 'r+')
template = template_file.read()
template = template.replace('example.com', domain_name)
request_data = {
"stack_name": stack_name,
"disable_rollback": True,
"parameters": {},
"template": template,
"timeout_mins": 60
}
response = requests.post(url,
data=json.dumps(request_data),
headers=self.headers)
return response
def get_stack_details(self, stack_name):
"""Gets details of the stack
:param stack_name: name of the stack to be queried
:returns: response containing stack details
"""
url = self.heat_url + '/stacks/' + stack_name
response = requests.get(url, headers=self.headers)
return response
def wait_for_stack_status(self, stack_name, status='CREATE_COMPLETE',
retry_timeout=6000, retry_interval=10):
"""Wait for the stack to reach the specified status
:param stack_name: name of the stack to be queried
:param status: desired stack status
:param retry_interval: how frequently to ping for status change(sec)
:param retry_interval: max number of seconds to wait for status change
:returns: None
"""
current_status = ''
start_time = int(time.time())
stop_time = start_time + retry_timeout
while current_status != status:
time.sleep(retry_interval)
stack_details = self.get_stack_details(stack_name=stack_name)
body = stack_details.json()
current_status = body['stack']['stack_status']
if (current_status == status):
return
current_time = int(time.time())
if current_time > stop_time:
return
def get_server_ip(self, stack_name):
"""Get the cloud server ip for the stack_name
:param stack_name: name of the stack
:returns: ip of the server that is part of the stack
"""
stack_details = self.get_stack_details(stack_name)
stack_details = stack_details.json()
outputs = stack_details['stack']['outputs']
server_ip = [output['output_value'] for
output in outputs if
output['output_key'] == 'server_ip']
return server_ip[0]
def delete_stack(self, stack_name):
"""Delete specified stack
:param stack_name: name of the stack
:returns: DELETE response
"""
url = self.heat_url + '/stacks/' + stack_name
response = requests.delete(url, headers=self.headers)
return response

View File

@ -0,0 +1,76 @@
# Copyright (c) 2014 Rackspace, 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 time
import requests
class WebpageTestClient(object):
def __init__(self, wpt_url, api_key):
self.wpt_url = wpt_url
self.api_key = api_key
def start_test(self, access_url, test_location, runs=5):
"""Starts webpage test
:param access_url: the url which needs performance metrics
:param test_location: location to run the test on
:param runs: number of test runs
:returns: url to access the test details
"""
start_url = (self.wpt_url + '/runtest.php?url=' + access_url +
'&k=' + self.api_key + '&location=' + test_location +
'&runs=' + str(runs) + '&f=json')
response = requests.post(start_url)
test = response.json()
return test['data']['jsonUrl']
def get_test_details(self, test_url):
"""Gets test results in json format
:param test_url: the url pointing to json test results
:returns: test details json
"""
response = requests.get(test_url)
return response.json()
def wait_for_test_status(self, test_url, status='COMPLETE',
retry_timeout=6000, retry_interval=10):
"""Waits for the wpt test to be completed.
:param test_url: the url pointing to json test results
:param status: expected status from WPT server
200 (COMPLETE) indicates test is completed.
1XX means the test is still in progress.
And 4XX indicates some error.
"""
current_status = ''
start_time = int(time.time())
stop_time = start_time + retry_timeout
if status == 'COMPLETE':
status_code = 200
while current_status != status:
time.sleep(retry_interval)
test_details = self.get_test_details(test_url=test_url)
current_status = test_details['statusCode']
print('status', test_details)
if (current_status == status_code):
return
current_time = int(time.time())
if current_time > stop_time:
return

View File

@ -0,0 +1,390 @@
heat_template_version: 2013-05-23
description: |
This is a Heat template to deploy a single Linux server running WordPress.
parameter_groups:
- label: Server Settings
parameters:
- server_hostname
- image
- flavor
- label: WordPress Settings
parameters:
- domain
- username
- label: rax-dev-params
# These are parameters that will not be displayed in the portal. The purpose
# of these parameters are for users who are developing or testing newer or
# different setups. If any of these parameters are changed, there is a good
# chance this stack will fail to properly deploy.
parameters:
- kitchen
- chef_version
- version
- prefix
parameters:
# Server settings
server_hostname:
label: Server Name
description: Hostname to use for the server that's built.
type: string
default: CDN-Test-WordPress
constraints:
- length:
min: 1
max: 64
- allowed_pattern: "^[a-zA-Z][a-zA-Z0-9-]*$"
description: |
Must begin with a letter and contain only alphanumeric characters.
image:
label: Operating System
description: |
Required: Server image used for all servers that are created as a part of
this deployment.
type: string
default: Ubuntu 12.04 LTS (Precise Pangolin) (PVHVM)
constraints:
- allowed_values:
- Ubuntu 12.04 LTS (Precise Pangolin) (PVHVM)
description: Must be a supported operating system.
flavor:
label: Server Size
description: |
Required: Rackspace Cloud Server flavor to use. The size is based on the
amount of RAM for the provisioned server.
type: string
default: 4 GB General Purpose v1
constraints:
- allowed_values:
- 1 GB General Purpose v1
- 2 GB General Purpose v1
- 4 GB General Purpose v1
- 8 GB General Purpose v1
- 15 GB I/O v1
- 30 GB I/O v1
- 1GB Standard Instance
- 2GB Standard Instance
- 4GB Standard Instance
- 8GB Standard Instance
- 15GB Standard Instance
- 30GB Standard Instance
description: |
Must be a valid Rackspace Cloud Server flavor for the region you have
selected to deploy into.
# WordPress settings
domain:
label: Site Domain
description: Domain to be used with WordPress site
type: string
default: "example.com"
constraints:
- allowed_pattern: "^[a-zA-Z0-9.-]{1,255}.[a-zA-Z]{2,15}$"
description: Must be a valid domain name
version:
label: WordPress Version
description: Version of WordPress to install
type: string
default: "3.9.2"
constraints:
- allowed_values:
- "3.9.2"
# Optional Apache settings (SSL certs)
# ssl_private_key:
# description: Private SSL key
# type: string
# default: ""
# constraints:
# - allowed_pattern: "^(.){0,5000}$"
# description: "Key values must be under 5,000 characters"
# ssl_certificate:
# description: Public SSL key
# type: string
# default: ""
# constraints:
# - allowed_pattern: "^(.){0,5000}$"
# description: "Certificate values must be under 5,000 characters"
# ssl_intermediate_key:
# description: Intermediate SSL key
# type: string
# default: ""
# constraints:
# - allowed_pattern: "^(.){0,5000}$"
# description: "Intermediate values must be under 5,000 characters."
# Database and system user configuration
prefix:
label: Database Prefix
description: Prefix to use for WordPress database tables
type: string
default: wp_
constraints:
- allowed_pattern: "^[0-9a-zA-Z$_]{0,10}$"
description: |
Prefix must be shorter than 10 characters, and can only include
letters, numbers, $, and/or underscores.
database_name:
label: Database Name
description: WordPress database name
type: string
default: wordpress
constraints:
- allowed_pattern: "^[0-9a-zA-Z$_]{1,64}$"
description: |
Maximum length of 64 characters, may only contain letters, numbers, and
underscores.
username:
label: Username
description: "Username for system, database, and WordPress logins."
type: string
default: wp_user
constraints:
- allowed_pattern: "^[a-zA-Z0-9 _.@-]{1,16}$"
description: |
Must be shorter than 16 characters and may only contain alphanumeric
characters, ' ', '_', '.', '@', and/or '-'.
kitchen:
label: Kitchen URL
description: "URL for a git repo containing required cookbooks"
type: string
default: https://github.com/rackspace-orchestration-templates/wordpress-single.git
chef_version:
label: Chef Version
description: Version of chef client to use
type: string
default: 11.16.2
resources:
# Random password generation
database_password:
type: "OS::Heat::RandomString"
properties:
length: 16
sequence: lettersdigits
mysql_root_password:
type: "OS::Heat::RandomString"
properties:
length: 16
sequence: lettersdigits
mysql_repl_password:
type: "OS::Heat::RandomString"
properties:
length: 16
sequence: lettersdigits
mysql_debian_password:
type: "OS::Heat::RandomString"
properties:
length: 16
sequence: lettersdigits
# Random strings for WP salting
wp_auth:
type: "OS::Heat::RandomString"
properties:
length: 32
sequence: hexdigits
wp_logged_in:
type: "OS::Heat::RandomString"
properties:
length: 32
sequence: hexdigits
wp_nonce:
type: "OS::Heat::RandomString"
properties:
length: 32
sequence: hexdigits
wp_secure_auth:
type: "OS::Heat::RandomString"
properties:
length: 32
sequence: hexdigits
# SSH KEYS
sync_key:
type: "OS::Nova::KeyPair"
properties:
name:
str_replace:
template: "%stack_id%-sync"
params:
"%stack_id%": { get_param: "OS::stack_id" }
save_private_key: true
ssh_key:
type: "OS::Nova::KeyPair"
properties:
name: { get_param: "OS::stack_id" }
save_private_key: true
# Server resources
wordpress_server:
type: "Rackspace::Cloud::Server"
properties:
name: { get_param: server_hostname }
flavor: { get_param: flavor }
image: { get_param: image }
key_name: { get_resource: ssh_key }
metadata:
rax-heat: { get_param: "OS::stack_id" }
# Chef resources
wordpress_setup:
type: "OS::Heat::ChefSolo"
depends_on: wordpress_server
properties:
username: root
private_key: { get_attr: [ssh_key, private_key] }
host: { get_attr: [wordpress_server, accessIPv4] }
kitchen: { get_param: kitchen }
chef_version: { get_param: chef_version }
node:
apache:
listen_ports: [8080]
timeout: 30
serversignature: "Off"
traceenable: "Off"
hollandbackup:
main:
backup_directory: "/var/lib/mysqlbackup"
mysqldump:
user: "root"
host: "localhost"
password: { get_attr: [mysql_root_password, value] }
lsyncd:
interval: 5
memcached:
listen: "127.0.0.1"
monit:
notify_email: 'root@localhost'
mail_format:
from: 'monit@localhost'
mysql:
bind_address: "127.0.0.1"
server_root_password: { get_attr: [mysql_root_password, value] }
server_repl_password: { get_attr: [mysql_repl_password, value] }
server_debian_password: { get_attr: [mysql_debian_password, value] }
remove_test_database: true
remove_anonymous_users: true
sysctl:
values:
fs.inotify.max_user_watches: 1000000
varnish:
version: "3.0"
listen_port: "80"
vsftpd:
ipaddress: ''
write_enable: true
local_umask: "002"
chroot_local_user: false
hide_ids: false
ssl_enable: true
ssl_ciphers: "AES256-SHA"
wordpress:
version: { get_param: version }
server_aliases: [{ get_param: domain }]
dir:
str_replace:
template: "/var/www/vhosts/%domain%"
params:
"%domain%": { get_param: domain }
db:
name: { get_param: database_name }
user: { get_param: username }
pass: { get_attr: [database_password, value] }
host: "127.0.0.1"
keys:
auth: { get_attr: [wp_auth, value] }
logged_in: { get_attr: [wp_logged_in, value] }
nonce_key: { get_attr: [wp_nonce, value] }
secure_auth_key: { get_attr: [wp_secure_auth, value] }
rax:
apache:
# ssl_private_key: { get_param: ssl_private_key }
# ssl_certificate: { get_param: ssl_certificate }
# ssl_intermediate_certs: { get_param: ssl_intermediate_certs }
domain: { get_param: domain }
lsyncd:
ssh:
private_key: { get_attr: [sync_key, private_key] }
packages:
- php5-imagick
varnish:
master_backend: "localhost"
wordpress:
admin_user: { get_param: username }
admin_pass: { get_attr: [database_password, value] }
user:
name: { get_param: username }
group: { get_param: username }
run_list: ["recipe[apt]",
"recipe[build-essential]",
"recipe[rax-wordpress::apache-prep]",
"recipe[sysctl::attribute_driver]",
"recipe[mysql::server]",
"recipe[rax-wordpress::mysql]",
"recipe[hollandbackup]",
"recipe[hollandbackup::mysqldump]",
"recipe[hollandbackup::main]",
"recipe[hollandbackup::backupsets]",
"recipe[hollandbackup::cron]",
"recipe[rax-wordpress::x509]",
"recipe[memcached]",
"recipe[php]",
"recipe[rax-install-packages]",
"recipe[wordpress]",
"recipe[rax-wordpress::wp-setup]",
"recipe[rax-wordpress::user]",
"recipe[rax-wordpress::memcache]",
"recipe[lsyncd]",
"recipe[vsftpd]",
"recipe[rax-wordpress::vsftpd]",
"recipe[varnish::repo]",
"recipe[varnish]",
"recipe[rax-wordpress::apache]",
"recipe[rax-wordpress::varnish]",
"recipe[rax-wordpress::firewall]",
"recipe[rax-wordpress::vsftpd-firewall]",
"recipe[rax-wordpress::lsyncd]"]
outputs:
private_key:
description: SSH Private Key
value: { get_attr: [ssh_key, private_key] }
server_ip:
description: Server IP
value: { get_attr: [wordpress_server, accessIPv4] }
wordpress_user:
description: WordPress User
value: { get_param: username }
wordpress_password:
description: WordPress Password
value: { get_attr: [database_password, value] }
mysql_root_password:
description: MySQL Root Password
value: { get_attr: [mysql_root_password, value] }

27
tests/etc/endtoend.conf Normal file
View File

@ -0,0 +1,27 @@
#=============================================================================
# Configuration file to execute End To End tests.
#=============================================================================
[auth]
user_name={user name of the cloud account}
api_key={api key for this user name}
base_url=https://identity.api.rackspacecloud.com/v2.0
[test_configuration]
provider_validation=False
status_check_retry_interval=2
status_check_retry_timeout=30
[orchestration]
base_url=https://iad.orchestration.api.rackspacecloud.com/v1/{project_id}/
yaml_path=endtoend/wordpress-single.yaml
[poppy]
base_url=http://0.0.0.0:8888
flavor=standard
[webpagetest]
base_url=http://www.webpagetest.org/
api_key={api key for webpagetest instance}
# GET http://www.webpagetest.org/getLocations.php - Use <id> tag
test_locations=Wellington:Chrome, Indore:Firefox, Stockholm:Safari, Dulles:Firefox, Miami:Chrome

View File

@ -8,3 +8,5 @@ openstack.nose_plugin
oslotest
requests
testtools
python-heatclient
beautifulsoup4

View File

@ -18,7 +18,7 @@ deps = -r{toxinidir}/requirements/requirements.txt
-r{toxinidir}/tests/test-requirements.txt
commands = pip install git+https://github.com/stackforge/opencafe.git#egg=cafe
pip install git+https://github.com/tonytan4ever/python-maxcdn.git#egg=maxcdn
nosetests {posargs:--exclude=api --nologcapture}
nosetests {posargs:--exclude=api --exclude=endtoend --nologcapture}
[tox:jenkins]
downloadcache = ~/cache/pip