Fix "mutable" object as default value

This patch fix the coding issue where "mutable" objects like list, dict
are being used as default value of param.

Current functions with mutable object as default value can create issues
if they will be called in more random order.
For example - first call with non default value and then with default
value. In this case original default will be overridden by the first
function call (as mutable object keep their state among function calls).

This commit also add the hacking check for the same and its
corresponding test.

Closes-Bug: #1330322

Change-Id: I251b316ef6d37f4b95c5e8d73a20a39005c22870
This commit is contained in:
Ghanshyam 2014-06-16 13:54:22 +09:00
parent 6e418039c5
commit 2a180b8398
9 changed files with 89 additions and 22 deletions

View File

@ -12,6 +12,7 @@ Tempest Specific Commandments
- [T104] Scenario tests require a services decorator - [T104] Scenario tests require a services decorator
- [T105] Unit tests cannot use setUpClass - [T105] Unit tests cannot use setUpClass
- [T106] vim configuration should not be kept in source files. - [T106] vim configuration should not be kept in source files.
- [N322] Method's default argument shouldn't be mutable
Test Data/Configuration Test Data/Configuration
----------------------- -----------------------

View File

@ -271,10 +271,10 @@ class BaseComputeTest(tempest.test.BaseTestCase):
return resp, body return resp, body
@classmethod @classmethod
def create_test_server_group(cls, name="", policy=[]): def create_test_server_group(cls, name="", policy=None):
if not name: if not name:
name = data_utils.rand_name(cls.__name__ + "-Server-Group") name = data_utils.rand_name(cls.__name__ + "-Server-Group")
if not policy: if policy is None:
policy = ['affinity'] policy = ['affinity']
resp, body = cls.servers_client.create_server_group(name, policy) resp, body = cls.servers_client.create_server_group(name, policy)
cls.server_groups.append(body['id']) cls.server_groups.append(body['id'])

View File

@ -64,8 +64,10 @@ class BaseOrchestrationTest(tempest.test.BaseTestCase):
return admin_client return admin_client
@classmethod @classmethod
def create_stack(cls, stack_name, template_data, parameters={}, def create_stack(cls, stack_name, template_data, parameters=None,
environment=None, files=None): environment=None, files=None):
if parameters is None:
parameters = {}
resp, body = cls.client.create_stack( resp, body = cls.client.create_stack(
stack_name, stack_name,
template=template_data, template=template_data,

View File

@ -248,8 +248,10 @@ class RestClient(object):
return resp[i] return resp[i]
return "" return ""
def _log_request_start(self, method, req_url, req_headers={}, def _log_request_start(self, method, req_url, req_headers=None,
req_body=None): req_body=None):
if req_headers is None:
req_headers = {}
caller_name = misc_utils.find_test_caller() caller_name = misc_utils.find_test_caller()
trace_regex = CONF.debug.trace_requests trace_regex = CONF.debug.trace_requests
if trace_regex and re.search(trace_regex, caller_name): if trace_regex and re.search(trace_regex, caller_name):
@ -257,8 +259,10 @@ class RestClient(object):
(caller_name, method, req_url)) (caller_name, method, req_url))
def _log_request(self, method, req_url, resp, def _log_request(self, method, req_url, resp,
secs="", req_headers={}, secs="", req_headers=None,
req_body=None, resp_body=None): req_body=None, resp_body=None):
if req_headers is None:
req_headers = {}
# if we have the request id, put it in the right part of the log # if we have the request id, put it in the right part of the log
extra = dict(request_id=self._get_request_id(resp)) extra = dict(request_id=self._get_request_id(resp))
# NOTE(sdague): while we still have 6 callers to this function # NOTE(sdague): while we still have 6 callers to this function

View File

@ -27,6 +27,7 @@ TEST_DEFINITION = re.compile(r'^\s*def test.*')
SETUPCLASS_DEFINITION = re.compile(r'^\s*def setUpClass') SETUPCLASS_DEFINITION = re.compile(r'^\s*def setUpClass')
SCENARIO_DECORATOR = re.compile(r'\s*@.*services\((.*)\)') SCENARIO_DECORATOR = re.compile(r'\s*@.*services\((.*)\)')
VI_HEADER_RE = re.compile(r"^#\s+vim?:.+") VI_HEADER_RE = re.compile(r"^#\s+vim?:.+")
mutable_default_args = re.compile(r"^\s*def .+\((.+=\{\}|.+=\[\])")
def import_no_clients_in_api(physical_line, filename): def import_no_clients_in_api(physical_line, filename):
@ -119,6 +120,16 @@ def no_official_client_manager_in_api_tests(physical_line, filename):
'tests') 'tests')
def no_mutable_default_args(logical_line):
"""Check that mutable object isn't used as default argument
N322: Method's default argument shouldn't be mutable
"""
msg = "N322: Method's default argument shouldn't be mutable!"
if mutable_default_args.match(logical_line):
yield (0, msg)
def factory(register): def factory(register):
register(import_no_clients_in_api) register(import_no_clients_in_api)
register(scenario_tests_need_service_tags) register(scenario_tests_need_service_tags)
@ -126,3 +137,4 @@ def factory(register):
register(no_vi_headers) register(no_vi_headers)
register(service_tags_not_in_module_path) register(service_tags_not_in_module_path)
register(no_official_client_manager_in_api_tests) register(no_official_client_manager_in_api_tests)
register(no_mutable_default_args)

View File

@ -136,8 +136,8 @@ class ScenarioTest(tempest.test.BaseTestCase):
pass pass
def addCleanup_with_wait(self, waiter_callable, thing_id, thing_id_param, def addCleanup_with_wait(self, waiter_callable, thing_id, thing_id_param,
cleanup_callable, cleanup_args=[], cleanup_callable, cleanup_args=None,
cleanup_kwargs={}, ignore_error=True): cleanup_kwargs=None, ignore_error=True):
"""Adds wait for ansyc resource deletion at the end of cleanups """Adds wait for ansyc resource deletion at the end of cleanups
@param waiter_callable: callable to wait for the resource to delete @param waiter_callable: callable to wait for the resource to delete
@ -147,6 +147,10 @@ class ScenarioTest(tempest.test.BaseTestCase):
the following *cleanup_args, **cleanup_kwargs. the following *cleanup_args, **cleanup_kwargs.
usually a delete method. usually a delete method.
""" """
if cleanup_args is None:
cleanup_args = []
if cleanup_kwargs is None:
cleanup_kwargs = {}
self.addCleanup(cleanup_callable, *cleanup_args, **cleanup_kwargs) self.addCleanup(cleanup_callable, *cleanup_args, **cleanup_kwargs)
wait_dict = { wait_dict = {
'waiter_callable': waiter_callable, 'waiter_callable': waiter_callable,
@ -181,7 +185,7 @@ class ScenarioTest(tempest.test.BaseTestCase):
def create_server(self, name=None, image=None, flavor=None, def create_server(self, name=None, image=None, flavor=None,
wait_on_boot=True, wait_on_delete=True, wait_on_boot=True, wait_on_delete=True,
create_kwargs={}): create_kwargs=None):
"""Creates VM instance. """Creates VM instance.
@param image: image from which to create the instance @param image: image from which to create the instance
@ -196,6 +200,8 @@ class ScenarioTest(tempest.test.BaseTestCase):
image = CONF.compute.image_ref image = CONF.compute.image_ref
if flavor is None: if flavor is None:
flavor = CONF.compute.flavor_ref flavor = CONF.compute.flavor_ref
if create_kwargs is None:
create_kwargs = {}
fixed_network_name = CONF.compute.fixed_network_name fixed_network_name = CONF.compute.fixed_network_name
if 'nics' not in create_kwargs and fixed_network_name: if 'nics' not in create_kwargs and fixed_network_name:
@ -338,7 +344,9 @@ class ScenarioTest(tempest.test.BaseTestCase):
return linux_client return linux_client
def _image_create(self, name, fmt, path, properties={}): def _image_create(self, name, fmt, path, properties=None):
if properties is None:
properties = {}
name = data_utils.rand_name('%s-' % name) name = data_utils.rand_name('%s-' % name)
image_file = open(path, 'rb') image_file = open(path, 'rb')
self.addCleanup(image_file.close) self.addCleanup(image_file.close)
@ -531,8 +539,8 @@ class OfficialClientTest(tempest.test.BaseTestCase):
def addCleanup_with_wait(self, things, thing_id, def addCleanup_with_wait(self, things, thing_id,
error_status='ERROR', error_status='ERROR',
exc_type=nova_exceptions.NotFound, exc_type=nova_exceptions.NotFound,
cleanup_callable=None, cleanup_args=[], cleanup_callable=None, cleanup_args=None,
cleanup_kwargs={}): cleanup_kwargs=None):
"""Adds wait for ansyc resource deletion at the end of cleanups """Adds wait for ansyc resource deletion at the end of cleanups
@param things: type of the resource to delete @param things: type of the resource to delete
@ -544,6 +552,10 @@ class OfficialClientTest(tempest.test.BaseTestCase):
usually a delete method. if not used, will try to use: usually a delete method. if not used, will try to use:
things.delete(thing_id) things.delete(thing_id)
""" """
if cleanup_args is None:
cleanup_args = []
if cleanup_kwargs is None:
cleanup_kwargs = {}
if cleanup_callable is None: if cleanup_callable is None:
LOG.debug("no delete method passed. using {rclass}.delete({id}) as" LOG.debug("no delete method passed. using {rclass}.delete({id}) as"
" default".format(rclass=things, id=thing_id)) " default".format(rclass=things, id=thing_id))
@ -725,7 +737,7 @@ class OfficialClientTest(tempest.test.BaseTestCase):
def create_server(self, client=None, name=None, image=None, flavor=None, def create_server(self, client=None, name=None, image=None, flavor=None,
wait_on_boot=True, wait_on_delete=True, wait_on_boot=True, wait_on_delete=True,
create_kwargs={}): create_kwargs=None):
"""Creates VM instance. """Creates VM instance.
@param client: compute client to create the instance @param client: compute client to create the instance
@ -743,6 +755,8 @@ class OfficialClientTest(tempest.test.BaseTestCase):
image = CONF.compute.image_ref image = CONF.compute.image_ref
if flavor is None: if flavor is None:
flavor = CONF.compute.flavor_ref flavor = CONF.compute.flavor_ref
if create_kwargs is None:
create_kwargs = {}
fixed_network_name = CONF.compute.fixed_network_name fixed_network_name = CONF.compute.fixed_network_name
if 'nics' not in create_kwargs and fixed_network_name: if 'nics' not in create_kwargs and fixed_network_name:
@ -872,7 +886,9 @@ class OfficialClientTest(tempest.test.BaseTestCase):
self.status_timeout( self.status_timeout(
self.volume_client.volumes, volume_id, status) self.volume_client.volumes, volume_id, status)
def _image_create(self, name, fmt, path, properties={}): def _image_create(self, name, fmt, path, properties=None):
if properties is None:
properties = {}
name = data_utils.rand_name('%s-' % name) name = data_utils.rand_name('%s-' % name)
image_file = open(path, 'rb') image_file = open(path, 'rb')
self.addCleanup(image_file.close) self.addCleanup(image_file.close)
@ -1866,12 +1882,17 @@ class SwiftScenarioTest(ScenarioTest):
self._list_and_check_container_objects(container_name, self._list_and_check_container_objects(container_name,
not_present_obj=[filename]) not_present_obj=[filename])
def _list_and_check_container_objects(self, container_name, present_obj=[], def _list_and_check_container_objects(self, container_name,
not_present_obj=[]): present_obj=None,
not_present_obj=None):
""" """
List objects for a given container and assert which are present and List objects for a given container and assert which are present and
which are not. which are not.
""" """
if present_obj is None:
present_obj = []
if not_present_obj is None:
not_present_obj = []
_, object_list = self.container_client.list_container_contents( _, object_list = self.container_client.list_container_contents(
container_name) container_name)
if present_obj: if present_obj:

View File

@ -32,11 +32,15 @@ class AccountClient(rest_client.RestClient):
def create_account(self, data=None, def create_account(self, data=None,
params=None, params=None,
metadata={}, metadata=None,
remove_metadata={}, remove_metadata=None,
metadata_prefix='X-Account-Meta-', metadata_prefix='X-Account-Meta-',
remove_metadata_prefix='X-Remove-Account-Meta-'): remove_metadata_prefix='X-Remove-Account-Meta-'):
"""Create an account.""" """Create an account."""
if metadata is None:
metadata = {}
if remove_metadata is None:
remove_metadata = {}
url = '' url = ''
if params: if params:
url += '?%s' % urllib.urlencode(params) url += '?%s' % urllib.urlencode(params)

View File

@ -45,9 +45,11 @@ class OrchestrationClient(rest_client.RestClient):
body = json.loads(body) body = json.loads(body)
return resp, body['stacks'] return resp, body['stacks']
def create_stack(self, name, disable_rollback=True, parameters={}, def create_stack(self, name, disable_rollback=True, parameters=None,
timeout_mins=60, template=None, template_url=None, timeout_mins=60, template=None, template_url=None,
environment=None, files=None): environment=None, files=None):
if parameters is None:
parameters = {}
headers, body = self._prepare_update_create( headers, body = self._prepare_update_create(
name, name,
disable_rollback, disable_rollback,
@ -63,8 +65,10 @@ class OrchestrationClient(rest_client.RestClient):
return resp, body return resp, body
def update_stack(self, stack_identifier, name, disable_rollback=True, def update_stack(self, stack_identifier, name, disable_rollback=True,
parameters={}, timeout_mins=60, template=None, parameters=None, timeout_mins=60, template=None,
template_url=None, environment=None, files=None): template_url=None, environment=None, files=None):
if parameters is None:
parameters = {}
headers, body = self._prepare_update_create( headers, body = self._prepare_update_create(
name, name,
disable_rollback, disable_rollback,
@ -80,9 +84,11 @@ class OrchestrationClient(rest_client.RestClient):
return resp, body return resp, body
def _prepare_update_create(self, name, disable_rollback=True, def _prepare_update_create(self, name, disable_rollback=True,
parameters={}, timeout_mins=60, parameters=None, timeout_mins=60,
template=None, template_url=None, template=None, template_url=None,
environment=None, files=None): environment=None, files=None):
if parameters is None:
parameters = {}
post_body = { post_body = {
"stack_name": name, "stack_name": name,
"disable_rollback": disable_rollback, "disable_rollback": disable_rollback,
@ -264,16 +270,20 @@ class OrchestrationClient(rest_client.RestClient):
body = json.loads(body) body = json.loads(body)
return resp, body return resp, body
def validate_template(self, template, parameters={}): def validate_template(self, template, parameters=None):
"""Returns the validation result for a template with parameters.""" """Returns the validation result for a template with parameters."""
if parameters is None:
parameters = {}
post_body = { post_body = {
'template': template, 'template': template,
'parameters': parameters, 'parameters': parameters,
} }
return self._validate_template(post_body) return self._validate_template(post_body)
def validate_template_url(self, template_url, parameters={}): def validate_template_url(self, template_url, parameters=None):
"""Returns the validation result for a template with parameters.""" """Returns the validation result for a template with parameters."""
if parameters is None:
parameters = {}
post_body = { post_body = {
'template_url': template_url, 'template_url': template_url,
'parameters': parameters, 'parameters': parameters,

View File

@ -107,3 +107,16 @@ class HackingTestCase(base.TestCase):
self.assertFalse(checks.no_official_client_manager_in_api_tests( self.assertFalse(checks.no_official_client_manager_in_api_tests(
"cls.official_client = clients.OfficialClientManager(credentials)", "cls.official_client = clients.OfficialClientManager(credentials)",
"tempest/scenario/fake_test.py")) "tempest/scenario/fake_test.py"))
def test_no_mutable_default_args(self):
self.assertEqual(1, len(list(checks.no_mutable_default_args(
" def function1(para={}):"))))
self.assertEqual(1, len(list(checks.no_mutable_default_args(
"def function2(para1, para2, para3=[])"))))
self.assertEqual(0, len(list(checks.no_mutable_default_args(
"defined = []"))))
self.assertEqual(0, len(list(checks.no_mutable_default_args(
"defined, undefined = [], {}"))))