Merge "Shelve name of tests using a heat stack"
This commit is contained in:
commit
4d0bedc353
|
@ -30,6 +30,7 @@ from tobiko.common import _operation
|
|||
from tobiko.common import _os
|
||||
from tobiko.common import _retry
|
||||
from tobiko.common import _select
|
||||
from tobiko.common import _shelves
|
||||
from tobiko.common import _skip
|
||||
from tobiko.common import _time
|
||||
from tobiko.common import _utils
|
||||
|
@ -134,6 +135,12 @@ select_uniques = _select.select_uniques
|
|||
ObjectNotFound = _select.ObjectNotFound
|
||||
MultipleObjectsFound = _select.MultipleObjectsFound
|
||||
|
||||
addme_to_shared_resource = _shelves.addme_to_shared_resource
|
||||
removeme_from_shared_resource = _shelves.removeme_from_shared_resource
|
||||
remove_test_from_all_shared_resources = (
|
||||
_shelves.remove_test_from_all_shared_resources)
|
||||
initialize_shelves = _shelves.initialize_shelves
|
||||
|
||||
SkipException = _skip.SkipException
|
||||
skip_if = _skip.skip_if
|
||||
skip_on_error = _skip.skip_on_error
|
||||
|
|
|
@ -150,6 +150,7 @@ def enter_test_case(case: TestCase,
|
|||
yield
|
||||
finally:
|
||||
assert case is manager.pop_test_case()
|
||||
tobiko.remove_test_from_all_shared_resources(case.id())
|
||||
|
||||
|
||||
def test_case(case: TestCase = None,
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
# Copyright 2022 Red Hat
|
||||
#
|
||||
# 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 __future__ import absolute_import
|
||||
|
||||
import dbm
|
||||
import os
|
||||
import shelve
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
import tobiko
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
TEST_RUN_SHELF = 'test_run'
|
||||
|
||||
|
||||
def get_shelves_dir():
|
||||
# ensure the directory exists
|
||||
from tobiko import config
|
||||
shelves_dir = os.path.expanduser(config.CONF.tobiko.common.shelves_dir)
|
||||
return shelves_dir
|
||||
|
||||
|
||||
def get_shelf_path(shelf):
|
||||
return os.path.join(get_shelves_dir(), shelf)
|
||||
|
||||
|
||||
def addme_to_shared_resource(shelf, resource):
|
||||
shelf_path = get_shelf_path(shelf)
|
||||
# this is needed for unit tests
|
||||
resource = str(resource)
|
||||
testcase_id = tobiko.get_test_case().id()
|
||||
for attempt in tobiko.retry(timeout=10.0,
|
||||
interval=0.5):
|
||||
try:
|
||||
with shelve.open(shelf_path) as db:
|
||||
if db.get(resource) is None:
|
||||
db[resource] = set()
|
||||
# the add and remove methods do not work directly on the db
|
||||
auxset = db[resource]
|
||||
auxset.add(testcase_id)
|
||||
db[resource] = auxset
|
||||
return db[resource]
|
||||
except dbm.error:
|
||||
LOG.exception(f"Error accessing shelf {shelf}")
|
||||
if attempt.is_last:
|
||||
raise
|
||||
|
||||
|
||||
def removeme_from_shared_resource(shelf, resource):
|
||||
shelf_path = get_shelf_path(shelf)
|
||||
# this is needed for unit tests
|
||||
resource = str(resource)
|
||||
testcase_id = tobiko.get_test_case().id()
|
||||
for attempt in tobiko.retry(timeout=10.0,
|
||||
interval=0.5):
|
||||
try:
|
||||
with shelve.open(shelf_path) as db:
|
||||
# the add and remove methods do not work directly on the db
|
||||
db[resource] = db.get(resource) or set()
|
||||
if testcase_id in db[resource]:
|
||||
auxset = db[resource]
|
||||
auxset.remove(testcase_id)
|
||||
db[resource] = auxset
|
||||
return db[resource]
|
||||
except dbm.error:
|
||||
LOG.exception(f"Error accessing shelf {shelf}")
|
||||
if attempt.is_last:
|
||||
raise
|
||||
|
||||
|
||||
def remove_test_from_shelf_resources(testcase_id, shelf):
|
||||
shelf_path = get_shelf_path(shelf)
|
||||
for attempt in tobiko.retry(timeout=10.0,
|
||||
interval=0.5):
|
||||
try:
|
||||
with shelve.open(shelf_path) as db:
|
||||
if not db:
|
||||
return
|
||||
for resource in db.keys():
|
||||
if testcase_id in db[resource]:
|
||||
auxset = db[resource]
|
||||
auxset.remove(testcase_id)
|
||||
db[resource] = auxset
|
||||
return db
|
||||
except dbm.error as err:
|
||||
LOG.exception(f"Error accessing shelf {shelf}")
|
||||
if "db type could not be determined" in str(err):
|
||||
# remove the filename extension, which depends on the specific
|
||||
# DBM implementation
|
||||
shelf_path = '.'.join(shelf_path.split('.')[:-1])
|
||||
if attempt.is_last:
|
||||
raise
|
||||
|
||||
|
||||
def remove_test_from_all_shared_resources(testcase_id):
|
||||
LOG.debug(f'Removing test {testcase_id} from all shelf resources')
|
||||
shelves_dir = get_shelves_dir()
|
||||
for filename in os.listdir(shelves_dir):
|
||||
if TEST_RUN_SHELF not in filename:
|
||||
remove_test_from_shelf_resources(testcase_id, filename)
|
||||
|
||||
|
||||
def initialize_shelves():
|
||||
shelves_dir = get_shelves_dir()
|
||||
shelf_path = os.path.join(shelves_dir, TEST_RUN_SHELF)
|
||||
id_key = 'PYTEST_XDIST_TESTRUNUID'
|
||||
test_run_uid = os.environ.get(id_key)
|
||||
|
||||
tobiko.makedirs(shelves_dir)
|
||||
|
||||
# if no PYTEST_XDIST_TESTRUNUID ->
|
||||
# pytest was executed with only one worker
|
||||
# if tobiko.initialize_shelves() == True ->
|
||||
# this is the first pytest worker running cleanup_shelves
|
||||
# then, cleanup the shelves directory
|
||||
# else, another worker did it before
|
||||
for attempt in tobiko.retry(timeout=15.0,
|
||||
interval=0.5):
|
||||
try:
|
||||
with shelve.open(shelf_path) as db:
|
||||
if test_run_uid is None:
|
||||
LOG.debug("Only one pytest worker - Initializing shelves")
|
||||
elif test_run_uid == db.get(id_key):
|
||||
LOG.debug("Another pytest worker already initialized "
|
||||
"the shelves")
|
||||
return
|
||||
else:
|
||||
LOG.debug("Initializing shelves for the "
|
||||
"test run uid %s", test_run_uid)
|
||||
db[id_key] = test_run_uid
|
||||
for filename in os.listdir(shelves_dir):
|
||||
if TEST_RUN_SHELF not in filename:
|
||||
file_path = os.path.join(shelves_dir, filename)
|
||||
os.unlink(file_path)
|
||||
return
|
||||
except dbm.error:
|
||||
LOG.exception(f"Error accessing shelf {TEST_RUN_SHELF}")
|
||||
if attempt.is_last:
|
||||
raise
|
|
@ -69,7 +69,6 @@ HTTP_OPTIONS = [
|
|||
cfg.StrOpt('no_proxy',
|
||||
help="Don't use proxy server to connect to listed hosts")]
|
||||
|
||||
|
||||
TESTCASE_CONF_GROUP_NAME = "testcase"
|
||||
|
||||
TESTCASE_OPTIONS = [
|
||||
|
@ -82,6 +81,14 @@ TESTCASE_OPTIONS = [
|
|||
help=("Timeout (in seconds) used for interrupting test "
|
||||
"runner execution"))]
|
||||
|
||||
COMMON_GROUP_NAME = 'common'
|
||||
|
||||
COMMON_OPTIONS = [
|
||||
cfg.StrOpt('shelves_dir',
|
||||
default='~/.tobiko/cache/shelves',
|
||||
help=("Default directory where to look for shelves.")),
|
||||
]
|
||||
|
||||
|
||||
def workspace_config_files(project=None, prog=None):
|
||||
project = project or 'tobiko'
|
||||
|
@ -217,6 +224,9 @@ def register_tobiko_options(conf):
|
|||
conf.register_opts(
|
||||
group=cfg.OptGroup(TESTCASE_CONF_GROUP_NAME), opts=TESTCASE_OPTIONS)
|
||||
|
||||
conf.register_opts(
|
||||
group=cfg.OptGroup(COMMON_GROUP_NAME), opts=COMMON_OPTIONS)
|
||||
|
||||
for module_name in CONFIG_MODULES:
|
||||
module = importlib.import_module(module_name)
|
||||
if hasattr(module, 'register_tobiko_options'):
|
||||
|
@ -235,9 +245,16 @@ def list_testcase_options():
|
|||
]
|
||||
|
||||
|
||||
def list_common_options():
|
||||
return [
|
||||
(COMMON_GROUP_NAME, itertools.chain(COMMON_OPTIONS))
|
||||
]
|
||||
|
||||
|
||||
def list_tobiko_options():
|
||||
all_options = (list_http_options() +
|
||||
list_testcase_options())
|
||||
list_testcase_options() +
|
||||
list_common_options())
|
||||
|
||||
for module_name in CONFIG_MODULES:
|
||||
module = importlib.import_module(module_name)
|
||||
|
|
|
@ -188,7 +188,9 @@ class HeatStackFixture(tobiko.SharedFixture):
|
|||
self.user = keystone.get_user_id(session=self.session)
|
||||
|
||||
def setup_stack(self) -> stacks.Stack:
|
||||
return self.create_stack()
|
||||
stack = self.create_stack()
|
||||
tobiko.addme_to_shared_resource(__name__, stack.stack_name)
|
||||
return stack
|
||||
|
||||
def get_stack_parameters(self):
|
||||
return tobiko.reset_fixture(self.parameters).values
|
||||
|
@ -210,6 +212,8 @@ class HeatStackFixture(tobiko.SharedFixture):
|
|||
if attempt.is_last:
|
||||
raise
|
||||
|
||||
# the stack shelf counter does not need to be decreased
|
||||
# here, because it was not increased yet
|
||||
self.delete_stack()
|
||||
|
||||
# It uses a random time sleep to make conflicting
|
||||
|
@ -252,6 +256,8 @@ class HeatStackFixture(tobiko.SharedFixture):
|
|||
LOG.error(f"Stack '{self.stack_name}' (id='{stack.id}') "
|
||||
f"found in '{stack_status}' status (reason="
|
||||
f"'{stack.stack_status_reason}'). Deleting it...")
|
||||
# the stack shelf counter does not need to be decreased here,
|
||||
# because it was not increased yet
|
||||
self.delete_stack(stack_id=stack.id)
|
||||
|
||||
self.wait_until_stack_deleted()
|
||||
|
@ -286,12 +292,16 @@ class HeatStackFixture(tobiko.SharedFixture):
|
|||
except InvalidStackError as ex:
|
||||
LOG.debug(f'Deleting invalid stack (name={self.stack_name}, "'
|
||||
f'"id={stack_id}): {ex}')
|
||||
# the stack shelf counter does not need to be decreased here,
|
||||
# because it was not increased yet
|
||||
self.delete_stack(stack_id=stack_id)
|
||||
raise
|
||||
|
||||
if stack_id != stack.id:
|
||||
LOG.debug(f'Deleting duplicate stack (name={self.stack_name}, "'
|
||||
f'"id={stack_id})')
|
||||
# the stack shelf counter does not need to be decreased here,
|
||||
# because it was not increased yet
|
||||
self.delete_stack(stack_id=stack_id)
|
||||
del stack_id
|
||||
|
||||
|
@ -312,8 +322,14 @@ class HeatStackFixture(tobiko.SharedFixture):
|
|||
return resources
|
||||
|
||||
def cleanup_fixture(self):
|
||||
self.setup_client()
|
||||
self.cleanup_stack()
|
||||
n_tests_using_stack = len(tobiko.removeme_from_shared_resource(
|
||||
__name__, self.stack_name))
|
||||
if n_tests_using_stack == 0:
|
||||
self.setup_client()
|
||||
self.cleanup_stack()
|
||||
else:
|
||||
LOG.info('Stack %r not deleted because %d tests are using it',
|
||||
self.stack_name, n_tests_using_stack)
|
||||
|
||||
def cleanup_stack(self):
|
||||
self.delete_stack()
|
||||
|
|
|
@ -59,6 +59,7 @@ class TestResult(unittest.TextTestResult):
|
|||
super().stopTestRun()
|
||||
actual_test = tobiko.pop_test_case()
|
||||
assert actual_test == test
|
||||
tobiko.remove_test_from_all_shared_resources(test.id())
|
||||
|
||||
|
||||
class TextIOWrapper(io.TextIOWrapper):
|
||||
|
|
|
@ -231,3 +231,8 @@ def pytest_runtest_call(item):
|
|||
# pylint: disable=unused-argument
|
||||
check_test_runner_timeout()
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def cleanup_shelves():
|
||||
tobiko.initialize_shelves()
|
||||
|
|
|
@ -261,9 +261,12 @@ class HeatStackFixtureTest(openstack.OpenstackTest):
|
|||
def test_cleanup(self):
|
||||
client = MockClient()
|
||||
client.stacks.get.return_value = None
|
||||
stack = MyStack(client=client)
|
||||
stack = MyStackWithStackName(client=client)
|
||||
stack_name = stack.stack_name
|
||||
tobiko.addme_to_shared_resource(
|
||||
'tobiko.openstack.heat._stack', stack_name)
|
||||
stack.cleanUp()
|
||||
client.stacks.delete.assert_called_once_with(stack.stack_name)
|
||||
client.stacks.delete.assert_called_once_with(stack_name)
|
||||
|
||||
def test_outputs(self):
|
||||
stack = mock_stack(status='CREATE_COMPLETE',
|
||||
|
|
Loading…
Reference in New Issue