Coverage for nova/test.py: 89%
405 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
1# Copyright 2010 United States Government as represented by the
2# Administrator of the National Aeronautics and Space Administration.
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
17"""Base classes for our unit tests.
19Allows overriding of flags for use of fakes, and some black magic for
20inline callbacks.
22"""
24import nova.monkey_patch # noqa
26import abc
27import builtins
28import collections
29import contextlib
30import copy
31import datetime
32import inspect
33import itertools
34import os
35import os.path
36import pprint
37import sys
38from unittest import mock
40import fixtures
41from oslo_cache import core as cache
42from oslo_concurrency import lockutils
43from oslo_config import cfg
44from oslo_config import fixture as config_fixture
45from oslo_log.fixture import logging_error as log_fixture
46from oslo_log import log as logging
47from oslo_serialization import jsonutils
48from oslo_utils.fixture import uuidsentinel as uuids
49from oslo_utils import timeutils
50from oslo_versionedobjects import fixture as ovo_fixture
51from oslotest import base
52from oslotest import mock_fixture
53from sqlalchemy.dialects import sqlite
54import testtools
56from nova.api.openstack import wsgi_app
57from nova.compute import rpcapi as compute_rpcapi
58from nova import context
59import nova.crypto
60from nova.db.main import api as db_api
61from nova import exception
62from nova import objects
63from nova.objects import base as objects_base
64from nova import quota
65from nova.scheduler.client import report
66from nova.scheduler import utils as scheduler_utils
67from nova.tests import fixtures as nova_fixtures
68from nova.tests.unit import matchers
69from nova import utils
70from nova.virt import images
72CONF = cfg.CONF
74logging.register_options(CONF)
75CONF.set_override('use_stderr', False)
76logging.setup(CONF, 'nova')
77cache.configure(CONF)
78LOG = logging.getLogger(__name__)
80_TRUE_VALUES = ('True', 'true', '1', 'yes')
81CELL1_NAME = 'cell1'
84# For compatibility with the large number of tests which use test.nested
85nested = utils.nested_contexts
88class TestingException(Exception):
89 pass
92# NOTE(claudiub): this needs to be called before any mock.patch calls are
93# being done, and especially before any other test classes load. This fixes
94# the mock.patch autospec issue:
95# https://github.com/testing-cabal/mock/issues/396
96mock_fixture.patch_mock_module()
99def _poison_unfair_compute_resource_semaphore_locking():
100 """Ensure that every locking on COMPUTE_RESOURCE_SEMAPHORE is called with
101 fair=True.
102 """
103 orig_synchronized = utils.synchronized
105 def poisoned_synchronized(*args, **kwargs):
106 # Only check fairness if the decorator is used with
107 # COMPUTE_RESOURCE_SEMAPHORE. But the name of the semaphore can be
108 # passed as args or as kwargs.
109 # Note that we cannot import COMPUTE_RESOURCE_SEMAPHORE as that would
110 # apply the decorators we want to poison here.
111 if len(args) >= 1: 111 ↛ 114line 111 didn't jump to line 114 because the condition on line 111 was always true
112 name = args[0]
113 else:
114 name = kwargs.get("name")
115 if name == "compute_resources" and not kwargs.get("fair", False):
116 raise AssertionError(
117 'Locking on COMPUTE_RESOURCE_SEMAPHORE should always be fair. '
118 'See bug 1864122.')
119 # go and act like the original decorator
120 return orig_synchronized(*args, **kwargs)
122 # replace the synchronized decorator factory with our own that checks the
123 # params passed in
124 utils.synchronized = poisoned_synchronized
127# NOTE(gibi): This poisoning needs to be done in import time as decorators are
128# applied in import time on the ResourceTracker
129_poison_unfair_compute_resource_semaphore_locking()
132class NovaExceptionReraiseFormatError(object):
133 real_log_exception = exception.NovaException._log_exception
135 @classmethod
136 def patch(cls):
137 exception.NovaException._log_exception = cls._wrap_log_exception
139 @staticmethod
140 def _wrap_log_exception(self):
141 exc_info = sys.exc_info()
142 NovaExceptionReraiseFormatError.real_log_exception(self)
143 raise exc_info[1]
146# NOTE(melwitt) This needs to be done at import time in order to also catch
147# NovaException format errors that are in mock decorators. In these cases, the
148# errors will be raised during test listing, before tests actually run.
149NovaExceptionReraiseFormatError.patch()
152class TestCase(base.BaseTestCase):
153 """Test case base class for all unit tests.
155 Due to the slowness of DB access, please consider deriving from
156 `NoDBTestCase` first.
157 """
158 # USES_DB is set to False for tests that inherit from NoDBTestCase.
159 USES_DB = True
160 # USES_DB_SELF is set to True in tests that specifically want to use the
161 # database but need to configure it themselves, for example to setup the
162 # API DB but not the cell DB. In those cases the test will override
163 # USES_DB_SELF = True but inherit from the NoDBTestCase class so it does
164 # not get the default fixture setup when using a database (which is the
165 # API and cell DBs, and adding the default flavors).
166 USES_DB_SELF = False
167 REQUIRES_LOCKING = False
169 # Setting to True makes the test use the RPCFixture.
170 STUB_RPC = True
172 # The number of non-cell0 cells to create. This is only used in the
173 # base class when USES_DB is True.
174 NUMBER_OF_CELLS = 1
176 # The stable compute id stuff is intentionally singleton-ish, which makes
177 # it a nightmare for testing multiple host/node combinations in tests like
178 # we do. So, mock it out by default, unless the test is specifically
179 # designed to handle it.
180 STUB_COMPUTE_ID = True
182 def setUp(self):
183 """Run before each test method to initialize test environment."""
184 # Ensure BaseTestCase's ConfigureLogging fixture is disabled since
185 # we're using our own (StandardLogging).
186 with fixtures.EnvironmentVariable('OS_LOG_CAPTURE', '0'):
187 super(TestCase, self).setUp()
189 self.useFixture(
190 nova_fixtures.PropagateTestCaseIdToChildEventlets(self.id()))
192 # How many of which service we've started. {$service-name: $count}
193 self._service_fixture_count = collections.defaultdict(int)
195 self.useFixture(nova_fixtures.OpenStackSDKFixture())
196 self.useFixture(nova_fixtures.IsolatedGreenPoolFixture(self.id()))
198 self.useFixture(log_fixture.get_logging_handle_error_fixture())
200 self.stdlog = self.useFixture(nova_fixtures.StandardLogging())
202 # NOTE(sdague): because of the way we were using the lock
203 # wrapper we ended up with a lot of tests that started
204 # relying on global external locking being set up for them. We
205 # consider all of these to be *bugs*. Tests should not require
206 # global external locking, or if they do, they should
207 # explicitly set it up themselves.
208 #
209 # The following REQUIRES_LOCKING class parameter is provided
210 # as a bridge to get us there. No new tests should be added
211 # that require it, and existing classes and tests should be
212 # fixed to not need it.
213 if self.REQUIRES_LOCKING:
214 lock_path = self.useFixture(fixtures.TempDir()).path
215 self.fixture = self.useFixture(
216 config_fixture.Config(lockutils.CONF))
217 self.fixture.config(lock_path=lock_path,
218 group='oslo_concurrency')
220 self.useFixture(nova_fixtures.ConfFixture(CONF))
222 if self.STUB_RPC:
223 self.useFixture(nova_fixtures.RPCFixture('nova.test'))
225 # we cannot set this in the ConfFixture as oslo only registers the
226 # notification opts at the first instantiation of a Notifier that
227 # happens only in the RPCFixture
228 CONF.set_default('driver', ['test'],
229 group='oslo_messaging_notifications')
231 # NOTE(danms): Make sure to reset us back to non-remote objects
232 # for each test to avoid interactions. Also, backup the object
233 # registry.
234 objects_base.NovaObject.indirection_api = None
235 self._base_test_obj_backup = copy.copy(
236 objects_base.NovaObjectRegistry._registry._obj_classes)
237 self.addCleanup(self._restore_obj_registry)
238 objects.Service.clear_min_version_cache()
240 # NOTE(danms): Reset the cached list of cells
241 from nova.compute import api
242 api.CELLS = []
243 context.CELL_CACHE = {}
244 context.CELLS = []
246 self.computes = {}
247 self.cell_mappings = {}
248 self.host_mappings = {}
249 # NOTE(danms): If the test claims to want to set up the database
250 # itself, then it is responsible for all the mapping stuff too.
251 if self.USES_DB:
252 # NOTE(danms): Full database setup involves a cell0, cell1,
253 # and the relevant mappings.
254 self.useFixture(nova_fixtures.Database(database='api'))
255 self._setup_cells()
256 self.useFixture(nova_fixtures.DefaultFlavorsFixture())
257 elif not self.USES_DB_SELF:
258 # NOTE(danms): If not using the database, we mock out the
259 # mapping stuff and effectively collapse everything to a
260 # single cell.
261 self.useFixture(nova_fixtures.SingleCellSimple())
262 self.useFixture(nova_fixtures.DatabasePoisonFixture())
264 # NOTE(blk-u): WarningsFixture must be after the Database fixture
265 # because sqlalchemy-migrate messes with the warnings filters.
266 self.useFixture(nova_fixtures.WarningsFixture())
268 self.useFixture(ovo_fixture.StableObjectJsonFixture())
270 # Reset the global QEMU version flag.
271 images.QEMU_VERSION = None
273 # Reset the compute RPC API globals (mostly the _ROUTER).
274 compute_rpcapi.reset_globals()
276 self.addCleanup(self._clear_attrs)
277 self.useFixture(fixtures.EnvironmentVariable('http_proxy'))
278 self.policy = self.useFixture(nova_fixtures.PolicyFixture())
280 self.useFixture(nova_fixtures.PoisonFunctions())
282 self.useFixture(nova_fixtures.ForbidNewLegacyNotificationFixture())
284 # NOTE(mikal): make sure we don't load a privsep helper accidentally
285 self.useFixture(nova_fixtures.PrivsepNoHelperFixture())
286 self.useFixture(mock_fixture.MockAutospecFixture())
288 # FIXME(danms): Disable this for all tests by default to avoid breaking
289 # any that depend on default/previous ordering
290 self.flags(build_failure_weight_multiplier=0.0,
291 group='filter_scheduler')
293 # NOTE(melwitt): Reset the cached set of projects
294 quota.UID_QFD_POPULATED_CACHE_BY_PROJECT = set()
295 quota.UID_QFD_POPULATED_CACHE_ALL = False
297 self.useFixture(nova_fixtures.GenericPoisonFixture())
298 self.useFixture(nova_fixtures.SysFsPoisonFixture())
300 # Additional module names can be added to this set if needed
301 self.useFixture(nova_fixtures.ImportModulePoisonFixture(
302 set(['guestfs', 'libvirt'])))
304 # make sure that the wsgi app is fully initialized for all testcase
305 # instead of only once initialized for test worker
306 wsgi_app.init_global_data.reset()
307 wsgi_app.init_application.reset()
309 # Reset the placement client singleton
310 report.PLACEMENTCLIENT = None
312 # Reset our local node uuid cache (and avoid writing to the
313 # local filesystem when we generate a new one).
314 if self.STUB_COMPUTE_ID:
315 self.useFixture(nova_fixtures.ComputeNodeIdFixture())
317 # Reset globals indicating affinity filter support. Some tests may set
318 # self.flags(enabled_filters=...) which could make the affinity filter
319 # support globals get set to a non-default configuration which affects
320 # all other tests.
321 scheduler_utils.reset_globals()
323 # Wait for bare greenlets spawn_n()'ed from a GreenThreadPoolExecutor
324 # to finish before moving on from the test. When greenlets from a
325 # previous test remain running, they may attempt to access structures
326 # (like the database) that have already been torn down and can cause
327 # the currently running test to fail.
328 self.useFixture(nova_fixtures.GreenThreadPoolShutdownWait())
330 # Reset the global key manager
331 nova.crypto._KEYMGR = None
333 # Reset the global identity client
334 nova.limit.utils.IDENTITY_CLIENT = None
336 def _setup_cells(self):
337 """Setup a normal cellsv2 environment.
339 This sets up the CellDatabase fixture with two cells, one cell0
340 and one normal cell. CellMappings are created for both so that
341 cells-aware code can find those two databases.
342 """
343 celldbs = nova_fixtures.CellDatabases()
345 ctxt = context.get_context()
346 fake_transport = 'fake://nowhere/'
348 c0 = objects.CellMapping(
349 context=ctxt,
350 uuid=objects.CellMapping.CELL0_UUID,
351 name='cell0',
352 transport_url=fake_transport,
353 database_connection=objects.CellMapping.CELL0_UUID)
354 c0.create()
355 self.cell_mappings[c0.name] = c0
356 celldbs.add_cell_database(objects.CellMapping.CELL0_UUID)
358 for x in range(self.NUMBER_OF_CELLS):
359 name = 'cell%i' % (x + 1)
360 uuid = getattr(uuids, name)
361 cell = objects.CellMapping(
362 context=ctxt,
363 uuid=uuid,
364 name=name,
365 transport_url=fake_transport,
366 database_connection=uuid)
367 cell.create()
368 self.cell_mappings[name] = cell
369 # cell1 is the default cell
370 celldbs.add_cell_database(uuid, default=(x == 0))
372 self.useFixture(celldbs)
374 def _restore_obj_registry(self):
375 objects_base.NovaObjectRegistry._registry._obj_classes = \
376 self._base_test_obj_backup
378 def _clear_attrs(self):
379 # Delete attributes that don't start with _ so they don't pin
380 # memory around unnecessarily for the duration of the test
381 # suite
382 for key in [k for k in self.__dict__.keys() if k[0] != '_']:
383 # NOTE(gmann): Skip attribute 'id' because if tests are being
384 # generated using testscenarios then, 'id' attribute is being
385 # added during cloning the tests. And later that 'id' attribute
386 # is being used by test suite to generate the results for each
387 # newly generated tests by testscenarios.
388 if key != 'id':
389 del self.__dict__[key]
391 def stub_out(self, old, new):
392 """Replace a function for the duration of the test.
394 Use the monkey patch fixture to replace a function for the
395 duration of a test. Useful when you want to provide fake
396 methods instead of mocks during testing.
397 """
398 self.useFixture(fixtures.MonkeyPatch(old, new))
400 @staticmethod
401 def patch_exists(patched_path, result, other=None):
402 """Provide a static method version of patch_exists(), which if you
403 haven't already imported nova.test can be slightly easier to
404 use as a context manager within a test method via:
406 def test_something(self):
407 with self.patch_exists(path, True):
408 ...
409 """
410 return patch_exists(patched_path, result, other)
412 @staticmethod
413 def patch_open(patched_path, read_data):
414 """Provide a static method version of patch_open() which is easier to
415 use as a context manager within a test method via:
417 def test_something(self):
418 with self.patch_open(path, "fake contents of file"):
419 ...
420 """
421 return patch_open(patched_path, read_data)
423 def flags(self, **kw):
424 """Override flag variables for a test."""
425 group = kw.pop('group', None)
426 for k, v in kw.items():
427 CONF.set_override(k, v, group)
429 def reset_flags(self, *k, **kw):
430 """Reset flag variables for a test."""
431 group = kw.pop('group')
432 for flag in k:
433 CONF.clear_override(flag, group)
435 def enforce_fk_constraints(self, engine=None):
436 if engine is None: 436 ↛ 437line 436 didn't jump to line 437 because the condition on line 436 was never true
437 engine = db_api.get_engine()
438 dialect = engine.url.get_dialect()
439 if dialect == sqlite.dialect: 439 ↛ exitline 439 didn't return from function 'enforce_fk_constraints' because the condition on line 439 was always true
440 engine.connect().exec_driver_sql("PRAGMA foreign_keys = ON")
442 def start_service(self, name, host=None, cell_name=None, **kwargs):
443 # Disallow starting multiple scheduler services
444 if name == 'scheduler' and self._service_fixture_count[name]: 444 ↛ 445line 444 didn't jump to line 445 because the condition on line 444 was never true
445 raise TestingException("Duplicate start_service(%s)!" % name)
447 cell = None
448 # if the host is None then the CONF.host remains defaulted to
449 # 'fake-mini' (originally done in ConfFixture)
450 if host is not None:
451 # Make sure that CONF.host is relevant to the right hostname
452 self.useFixture(nova_fixtures.ConfPatcher(host=host))
454 if name == 'compute' and self.USES_DB:
455 # NOTE(danms): We need to create the HostMapping first, because
456 # otherwise we'll fail to update the scheduler while running
457 # the compute node startup routines below.
458 ctxt = context.get_context()
459 cell_name = cell_name or CELL1_NAME
460 cell = self.cell_mappings[cell_name]
461 if (host or name) not in self.host_mappings: 461 ↛ 469line 461 didn't jump to line 469 because the condition on line 461 was always true
462 # NOTE(gibi): If the HostMapping does not exists then this is
463 # the first start of the service so we create the mapping.
464 hm = objects.HostMapping(context=ctxt,
465 host=host or name,
466 cell_mapping=cell)
467 hm.create()
468 self.host_mappings[hm.host] = hm
469 svc = self.useFixture(
470 nova_fixtures.ServiceFixture(name, host, cell=cell, **kwargs))
472 # Keep track of how many instances of this service are running.
473 self._service_fixture_count[name] += 1
474 real_stop = svc.service.stop
476 # Make sure stopping the service decrements the active count, so that
477 # start,stop,start doesn't trigger the "Duplicate start_service"
478 # exception.
479 def patch_stop(*a, **k):
480 self._service_fixture_count[name] -= 1
481 return real_stop(*a, **k)
482 self.useFixture(fixtures.MockPatchObject(
483 svc.service, 'stop', patch_stop))
485 return svc.service
487 def _start_compute(self, host, cell_name=None):
488 """Start a nova compute service on the given host
490 :param host: the name of the host that will be associated to the
491 compute service.
492 :param cell_name: optional name of the cell in which to start the
493 compute service
494 :return: the nova compute service object
495 """
496 compute = self.start_service('compute', host=host, cell_name=cell_name)
497 self.computes[host] = compute
498 return compute
500 def _run_periodics(self, raise_on_error=False):
501 """Run the update_available_resource task on every compute manager
503 This runs periodics on the computes in an undefined order; some child
504 class redefine this function to force a specific order.
505 """
507 ctx = context.get_admin_context()
508 for host, compute in self.computes.items():
509 LOG.info('Running periodic for compute (%s)', host)
510 # Make sure the context is targeted to the proper cell database
511 # for multi-cell tests.
512 with context.target_cell(
513 ctx, self.host_mappings[host].cell_mapping) as cctxt:
514 compute.manager.update_available_resource(cctxt)
516 if raise_on_error:
517 if 'Traceback (most recent call last' in self.stdlog.logger.output:
518 # Get the last line of the traceback, for example:
519 # TypeError: virNodeDeviceLookupByName() argument 2 must be
520 # str or None, not Proxy
521 last_tb_line = self.stdlog.logger.output.splitlines()[-1]
522 raise TestingException(last_tb_line)
524 LOG.info('Finished with periodics')
526 def restart_compute_service(self, compute, keep_hypervisor_state=True):
527 """Stops the service and starts a new one to have realistic restart
529 :param:compute: the nova-compute service to be restarted
530 :param:keep_hypervisor_state: If true then already defined instances
531 will survive the compute service restart.
532 If false then the new service will see
533 an empty hypervisor
534 :returns: a new compute service instance serving the same host and
535 and node
536 """
538 # NOTE(gibi): The service interface cannot be used to simulate a real
539 # service restart as the manager object will not be recreated after a
540 # service.stop() and service.start() therefore the manager state will
541 # survive. For example the resource tracker will not be recreated after
542 # a stop start. The service.kill() call cannot help as it deletes
543 # the service from the DB which is unrealistic and causes that some
544 # operation that refers to the killed host (e.g. evacuate) fails.
545 # So this helper method will stop the original service and then starts
546 # a brand new compute service for the same host and node. This way
547 # a new ComputeManager instance will be created and initialized during
548 # the service startup.
549 compute.stop()
551 # this service was running previously so we have to make sure that
552 # we restart it in the same cell
553 cell_name = self.host_mappings[compute.host].cell_mapping.name
555 if keep_hypervisor_state:
556 # NOTE(gibi): FakeDriver does not provide a meaningful way to
557 # define some servers that exists already on the hypervisor when
558 # the driver is (re)created during the service startup. This means
559 # that we cannot simulate that the definition of a server
560 # survives a nova-compute service restart on the hypervisor.
561 # Instead here we save the FakeDriver instance that knows about
562 # the defined servers and inject that driver into the new Manager
563 # class during the startup of the compute service.
564 old_driver = compute.manager.driver
565 with mock.patch(
566 'nova.virt.driver.load_compute_driver') as load_driver:
567 load_driver.return_value = old_driver
568 new_compute = self.start_service(
569 'compute', host=compute.host, cell_name=cell_name)
570 else:
571 new_compute = self.start_service(
572 'compute', host=compute.host, cell_name=cell_name)
574 return new_compute
576 def assertJsonEqual(self, expected, observed, message=''):
577 """Asserts that 2 complex data structures are json equivalent.
579 We use data structures which serialize down to json throughout
580 the code, and often times we just need to know that these are
581 json equivalent. This means that list order is not important,
582 and should be sorted.
584 Because this is a recursive set of assertions, when failure
585 happens we want to expose both the local failure and the
586 global view of the 2 data structures being compared. So a
587 MismatchError which includes the inner failure as the
588 mismatch, and the passed in expected / observed as matchee /
589 matcher.
591 """
592 if isinstance(expected, str):
593 expected = jsonutils.loads(expected)
594 if isinstance(observed, str):
595 observed = jsonutils.loads(observed)
597 def sort_key(x):
598 if isinstance(x, (set, list)) or isinstance(x, datetime.datetime):
599 return str(x)
600 if isinstance(x, dict):
601 items = ((sort_key(key), sort_key(value))
602 for key, value in x.items())
603 return sorted(items)
604 return x
606 def inner(expected, observed, path='root'):
607 if isinstance(expected, dict) and isinstance(observed, dict):
608 self.assertEqual(
609 len(expected), len(observed),
610 ('path: %s. Different dict key sets\n'
611 'expected=%s\n'
612 'observed=%s\n'
613 'difference=%s') %
614 (path,
615 sorted(expected.keys()),
616 sorted(observed.keys()),
617 list(set(expected.keys()).symmetric_difference(
618 set(observed.keys())))))
619 expected_keys = sorted(expected)
620 observed_keys = sorted(observed)
621 self.assertEqual(
622 expected_keys, observed_keys,
623 'path: %s. Dict keys are not equal' % path)
624 for key in expected:
625 inner(expected[key], observed[key], path + '.%s' % key)
626 elif (isinstance(expected, (list, tuple, set)) and
627 isinstance(observed, (list, tuple, set))):
628 self.assertEqual(
629 len(expected), len(observed),
630 ('path: %s. Different list items\n'
631 'expected=%s\n'
632 'observed=%s\n'
633 'difference=%s') %
634 (path,
635 sorted(expected, key=sort_key),
636 sorted(observed, key=sort_key),
637 [a for a in itertools.chain(expected, observed) if
638 (a not in expected) or (a not in observed)]))
640 expected_values_iter = iter(sorted(expected, key=sort_key))
641 observed_values_iter = iter(sorted(observed, key=sort_key))
643 for i in range(len(expected)):
644 inner(next(expected_values_iter),
645 next(observed_values_iter), path + '[%s]' % i)
646 else:
647 self.assertEqual(expected, observed, 'path: %s' % path)
649 try:
650 inner(expected, observed)
651 except testtools.matchers.MismatchError as e:
652 difference = e.mismatch.describe()
653 if message:
654 message = 'message: %s\n' % message
655 msg = "\nexpected:\n%s\nactual:\n%s\ndifference:\n%s\n%s" % (
656 pprint.pformat(expected),
657 pprint.pformat(observed),
658 difference,
659 message)
660 error = AssertionError(msg)
661 error.expected = expected
662 error.observed = observed
663 error.difference = difference
664 raise error
666 def assertXmlEqual(self, expected, observed, **options):
667 self.assertThat(observed, matchers.XMLMatches(expected, **options))
669 def assertPublicAPISignatures(self, baseinst, inst):
670 def get_public_apis(inst):
671 methods = {}
673 def findmethods(object):
674 return inspect.ismethod(object) or inspect.isfunction(object)
676 for (name, value) in inspect.getmembers(inst, findmethods):
677 if name.startswith("_"):
678 continue
679 methods[name] = value
680 return methods
682 baseclass = baseinst.__class__.__name__
683 basemethods = get_public_apis(baseinst)
684 implmethods = get_public_apis(inst)
686 extranames = []
687 for name in sorted(implmethods.keys()):
688 if name not in basemethods: 688 ↛ 689line 688 didn't jump to line 689 because the condition on line 688 was never true
689 extranames.append(name)
691 self.assertEqual([], extranames,
692 "public APIs not listed in base class %s" %
693 baseclass)
695 for name in sorted(implmethods.keys()):
696 # NOTE(stephenfin): We ignore type annotations
697 baseargs = inspect.getfullargspec(basemethods[name])[:-1]
698 implargs = inspect.getfullargspec(implmethods[name])[:-1]
700 self.assertEqual(baseargs, implargs,
701 "%s args don't match base class %s" %
702 (name, baseclass))
705class APICoverage(object):
707 cover_api = None
709 def test_api_methods(self):
710 self.assertIsNotNone(self.cover_api)
711 api_methods = [x for x in dir(self.cover_api)
712 if not x.startswith('_')]
713 test_methods = [x[5:] for x in dir(self)
714 if x.startswith('test_')]
715 self.assertThat(
716 test_methods,
717 testtools.matchers.ContainsAll(api_methods))
720class SubclassSignatureTestCase(testtools.TestCase, metaclass=abc.ABCMeta):
721 """Ensure all overridden methods of all subclasses of the class
722 under test exactly match the signature of the base class.
724 A subclass of SubclassSignatureTestCase should define a method
725 _get_base_class which:
727 * Returns a base class whose subclasses will all be checked
728 * Ensures that all subclasses to be tested have been imported
730 SubclassSignatureTestCase defines a single test, test_signatures,
731 which does a recursive, depth-first check of all subclasses, ensuring
732 that their method signatures are identical to those of the base class.
733 """
734 @abc.abstractmethod
735 def _get_base_class(self):
736 raise NotImplementedError()
738 def setUp(self):
739 self.useFixture(nova_fixtures.ConfFixture(CONF))
740 self.base = self._get_base_class()
742 super(SubclassSignatureTestCase, self).setUp()
744 @staticmethod
745 def _get_argspecs(cls):
746 """Return a dict of method_name->argspec for every method of cls."""
747 argspecs = {}
749 # getmembers returns all members, including members inherited from
750 # the base class. It's redundant for us to test these, but as
751 # they'll always pass it's not worth the complexity to filter them out.
752 for (name, method) in inspect.getmembers(cls, inspect.isfunction):
753 # Subclass __init__ methods can usually be legitimately different
754 if name == '__init__':
755 continue
757 # Skip subclass private functions
758 if name.startswith('_'):
759 continue
761 while hasattr(method, '__wrapped__'):
762 # This is a wrapped function. The signature we're going to
763 # see here is that of the wrapper, which is almost certainly
764 # going to involve varargs and kwargs, and therefore is
765 # unlikely to be what we want. If the wrapper manipulates the
766 # arguments taken by the wrapped function, the wrapped function
767 # isn't what we want either. In that case we're just stumped:
768 # if it ever comes up, add more knobs here to work round it (or
769 # stop using a dynamic language).
770 #
771 # Here we assume the wrapper doesn't manipulate the arguments
772 # to the wrapped function and inspect the wrapped function
773 # instead.
774 method = getattr(method, '__wrapped__')
776 argspecs[name] = inspect.getfullargspec(method)
778 return argspecs
780 @staticmethod
781 def _clsname(cls):
782 """Return the fully qualified name of cls."""
783 return "%s.%s" % (cls.__module__, cls.__name__)
785 def _test_signatures_recurse(self, base, base_argspecs):
786 for sub in base.__subclasses__():
787 sub_argspecs = self._get_argspecs(sub)
789 # Check that each subclass method matches the signature of the
790 # base class
791 for (method, sub_argspec) in sub_argspecs.items():
792 # Methods which don't override methods in the base class
793 # are good.
794 if method in base_argspecs: 794 ↛ 791line 794 didn't jump to line 791 because the condition on line 794 was always true
795 self.assertEqual(base_argspecs[method], sub_argspec,
796 'Signature of %(sub)s.%(method)s '
797 'differs from superclass %(base)s' %
798 {'base': self._clsname(base),
799 'sub': self._clsname(sub),
800 'method': method})
802 # Recursively check this subclass
803 self._test_signatures_recurse(sub, sub_argspecs)
805 def test_signatures(self):
806 self._test_signatures_recurse(self.base, self._get_argspecs(self.base))
809class TimeOverride(fixtures.Fixture):
810 """Fixture to start and remove time override."""
812 def __init__(self, override_time=None):
813 self.override_time = override_time
815 def setUp(self):
816 super(TimeOverride, self).setUp()
817 timeutils.set_time_override(override_time=self.override_time)
818 self.addCleanup(timeutils.clear_time_override)
821class NoDBTestCase(TestCase):
822 """`NoDBTestCase` differs from TestCase in that DB access is not supported.
823 This makes tests run significantly faster. If possible, all new tests
824 should derive from this class.
825 """
826 USES_DB = False
829class MatchType(object):
830 """Matches any instance of a specified type
832 The MatchType class is a helper for use with the mock.assert_called_with()
833 method that lets you assert that a particular parameter has a specific data
834 type. It enables stricter checking than the built in mock.ANY helper.
836 Example usage could be:
838 mock_some_method.assert_called_once_with(
839 "hello",
840 MatchType(objects.Instance),
841 mock.ANY,
842 "world",
843 MatchType(objects.KeyPair))
844 """
846 def __init__(self, wanttype):
847 self.wanttype = wanttype
849 def __eq__(self, other):
850 return type(other) is self.wanttype
852 def __ne__(self, other):
853 return type(other) is not self.wanttype
855 def __repr__(self):
856 return "<MatchType:" + str(self.wanttype) + ">"
859class MatchObjPrims(object):
860 """Matches objects with equal primitives."""
862 def __init__(self, want_obj):
863 self.want_obj = want_obj
865 def __eq__(self, other):
866 return objects_base.obj_equal_prims(other, self.want_obj)
868 def __ne__(self, other):
869 return not other == self.want_obj
871 def __repr__(self):
872 return '<MatchObjPrims:' + str(self.want_obj) + '>'
875class ContainKeyValue(object):
876 """Checks whether a key/value pair is in a dict parameter.
878 The ContainKeyValue class is a helper for use with the mock.assert_*()
879 method that lets you assert that a particular dict contain a key/value
880 pair. It enables stricter checking than the built in mock.ANY helper.
882 Example usage could be:
884 mock_some_method.assert_called_once_with(
885 "hello",
886 ContainKeyValue('foo', bar),
887 mock.ANY,
888 "world",
889 ContainKeyValue('hello', world))
890 """
892 def __init__(self, wantkey, wantvalue):
893 self.wantkey = wantkey
894 self.wantvalue = wantvalue
896 def __eq__(self, other):
897 try:
898 return other[self.wantkey] == self.wantvalue
899 except (KeyError, TypeError):
900 return False
902 def __ne__(self, other):
903 try:
904 return other[self.wantkey] != self.wantvalue
905 except (KeyError, TypeError):
906 return True
908 def __repr__(self):
909 return "<ContainKeyValue: key " + str(self.wantkey) + \
910 " and value " + str(self.wantvalue) + ">"
913@contextlib.contextmanager
914def patch_exists(patched_path, result, other=None):
915 """Selectively patch os.path.exists() so that if it's called with
916 patched_path, return result. Calls with any other path are passed
917 through to the real os.path.exists() function if other is not provided.
918 If other is provided then that will be the result of the call on paths
919 other than patched_path.
921 Either import and use as a decorator / context manager, or use the
922 nova.TestCase.patch_exists() static method as a context manager.
924 Currently it is *not* recommended to use this if any of the
925 following apply:
927 - You want to patch via decorator *and* make assertions about how the
928 mock is called (since using it in the decorator form will not make
929 the mock available to your code).
931 - You want the result of the patched exists() call to be determined
932 programmatically (e.g. by matching substrings of patched_path).
934 - You expect exists() to be called multiple times on the same path
935 and return different values each time.
937 Additionally within unit tests which only test a very limited code
938 path, it may be possible to ensure that the code path only invokes
939 exists() once, in which case it's slightly overkill to do
940 selective patching based on the path. In this case something like
941 like this may be more appropriate:
943 @mock.patch('os.path.exists', return_value=True)
944 def test_my_code(self, mock_exists):
945 ...
946 mock_exists.assert_called_once_with(path)
947 """
948 real_exists = os.path.exists
950 def fake_exists(path):
951 if path == patched_path:
952 return result
953 elif other is not None:
954 return other
955 else:
956 return real_exists(path)
958 with mock.patch.object(os.path, "exists") as mock_exists:
959 mock_exists.side_effect = fake_exists
960 yield mock_exists
963@contextlib.contextmanager
964def patch_open(patched_path, read_data):
965 """Selectively patch open() so that if it's called with patched_path,
966 return a mock which makes it look like the file contains
967 read_data. Calls with any other path are passed through to the
968 real open() function.
970 Either import and use as a decorator, or use the
971 nova.TestCase.patch_open() static method as a context manager.
973 Currently it is *not* recommended to use this if any of the
974 following apply:
976 - The code under test will attempt to write to patched_path.
978 - You want to patch via decorator *and* make assertions about how the
979 mock is called (since using it in the decorator form will not make
980 the mock available to your code).
982 - You want the faked file contents to be determined
983 programmatically (e.g. by matching substrings of patched_path).
985 - You expect open() to be called multiple times on the same path
986 and return different file contents each time.
988 Additionally within unit tests which only test a very limited code
989 path, it may be possible to ensure that the code path only invokes
990 open() once, in which case it's slightly overkill to do
991 selective patching based on the path. In this case something like
992 like this may be more appropriate:
994 @mock.patch('builtins.open')
995 def test_my_code(self, mock_open):
996 ...
997 mock_open.assert_called_once_with(path)
998 """
999 real_open = builtins.open
1000 m = mock.mock_open(read_data=read_data)
1002 def selective_fake_open(path, *args, **kwargs):
1003 if path == patched_path:
1004 return m(patched_path)
1005 return real_open(path, *args, **kwargs)
1007 with mock.patch('builtins.open') as mock_open:
1008 mock_open.side_effect = selective_fake_open
1009 yield m