Example #1
0
class Hook(object, metaclass=registry.make_registry_metaclass(_HOOKS)):
    """Common interface all Hooks will inherit from."""

    REGISTERED_NAME = registry.LEAVE_UNREGISTERED

    def __init__(self, hook_logger, fixture, description):
        """Initialize the Hook with the specified fixture."""

        self.logger = hook_logger
        self.fixture = fixture
        self.description = description

    def before_suite(self, test_report):
        """Test runner calls this exactly once before they start running the suite."""
        pass

    def after_suite(self, test_report):
        """Invoke by test runner calls this exactly once after all tests have finished executing.

        Be sure to reset the behavior back to its original state so that it can be run again.
        """
        pass

    def before_test(self, test, test_report):
        """Each test will call this before it executes."""
        pass

    def after_test(self, test, test_report):
        """Each test will call this after it executes."""
        pass
Example #2
0
class FixtureBuilder(ABC, metaclass=registry.make_registry_metaclass(_BUILDERS, type(ABC))):  # pylint: disable=invalid-metaclass
    """
    ABC for fixture builders.

    If any fixture has special logic for assembling different components
    (e.g. for multiversion), define a builder to handle it.
    """

    # For any subclass, set a REGISTERED_NAME corresponding to the fixture the class builds.
    REGISTERED_NAME = "Builder"

    @abstractmethod
    def build_fixture(self, logger, job_num, fixturelib, *args, **kwargs):
        """Abstract method to build a fixture."""
        return
Example #3
0
class APIVersion(object,
                 metaclass=registry.make_registry_metaclass(_VERSIONS)):  # pylint: disable=invalid-metaclass
    """Class storing fixture API version info."""

    REGISTERED_NAME = "APIVersion"

    FIXTURE_API_VERSION = "0.1.0"

    @classmethod
    def check_api_version(cls, actual):
        """Check that we are compatible with the actual API version."""
        def to_major(version):
            return int(version.split(".")[0])

        def to_minor(version):
            return int(version.split(".")[1])

        expected = cls.FIXTURE_API_VERSION
        return to_major(expected) == to_major(
            actual) and to_minor(expected) <= to_minor(actual)
Example #4
0
class Hook(object, metaclass=registry.make_registry_metaclass(_HOOKS)):  # pylint: disable=invalid-metaclass
    """Common interface all Hooks will inherit from."""

    REGISTERED_NAME = registry.LEAVE_UNREGISTERED

    # Whether the hook runs in the background of a test. Typically background jobs start their own threads,
    # except for Server-side background activity like initial sync, which is also considered background.
    IS_BACKGROUND = None

    def __init__(self, hook_logger, fixture, description):
        """Initialize the Hook with the specified fixture."""

        self.logger = hook_logger
        self.fixture = fixture
        self.description = description

        if self.IS_BACKGROUND is None:
            raise ValueError(
                "Concrete Hook subclasses must override the IS_BACKGROUND class property"
            )

    def before_suite(self, test_report):
        """Test runner calls this exactly once before they start running the suite."""
        pass

    def after_suite(self, test_report, teardown_flag=None):
        """Invoke by test runner calls this exactly once after all tests have finished executing.

        Be sure to reset the behavior back to its original state so that it can be run again.
        Hook failures in this function should set 'teardown_flag' since there is no testcase available.
        """
        pass

    def before_test(self, test, test_report):
        """Each test will call this before it executes."""
        pass

    def after_test(self, test, test_report):
        """Each test will call this after it executes."""
        pass
Example #5
0
class Fixture(object, metaclass=registry.make_registry_metaclass(_FIXTURES)):  # pylint: disable=invalid-metaclass
    """Base class for all fixtures."""

    # We explicitly set the 'REGISTERED_NAME' attribute so that PyLint realizes that the attribute
    # is defined for all subclasses of Fixture.
    REGISTERED_NAME = "Fixture"

    _LAST_LTS_FCV = multiversion.LAST_LTS_FCV
    _LATEST_FCV = multiversion.LATEST_FCV
    _LAST_LTS_BIN_VERSION = multiversion.LAST_LTS_BIN_VERSION

    def __init__(self, logger, job_num, dbpath_prefix=None):
        """Initialize the fixture with a logger instance."""

        if not isinstance(logger, logging.Logger):
            raise TypeError("logger must be a Logger instance")

        if not isinstance(job_num, int):
            raise TypeError("job_num must be an integer")
        elif job_num < 0:
            raise ValueError("job_num must be a nonnegative integer")

        self.logger = logger
        self.job_num = job_num

        dbpath_prefix = utils.default_if_none(config.DBPATH_PREFIX,
                                              dbpath_prefix)
        dbpath_prefix = utils.default_if_none(dbpath_prefix,
                                              config.DEFAULT_DBPATH_PREFIX)
        self._dbpath_prefix = os.path.join(dbpath_prefix,
                                           "job{}".format(self.job_num))

    def pids(self):
        """Return any pids owned by this fixture."""
        raise NotImplementedError(
            "pids must be implemented by Fixture subclasses %s" % self)

    def setup(self):
        """Create the fixture."""
        pass

    def await_ready(self):
        """Block until the fixture can be used for testing."""
        pass

    def teardown(self, finished=False, mode=None):  # noqa
        """Destroy the fixture.

        The fixture's logging handlers are closed if 'finished' is true,
        which should happen when setup() won't be called again.

        Raises:
            errors.ServerFailure: If the teardown is not successful.
        """

        try:
            self._do_teardown(mode=mode)
        finally:
            if finished:
                for handler in self.logger.handlers:
                    # We ignore the cancellation token returned by close_later() since we always
                    # want the logs to eventually get flushed.
                    logging.flush.close_later(handler)

    def _do_teardown(self, mode=None):  # noqa
        """Destroy the fixture.

        This method must be implemented by subclasses.

        Raises:
            errors.ServerFailure: If the teardown is not successful.
        """
        pass

    def is_running(self):  # pylint: disable=no-self-use
        """Return true if the fixture is still operating and more tests and can be run."""
        return True

    def get_node_info(self):  # pylint: disable=no-self-use
        """Return a list of NodeInfo objects."""
        return []

    def get_dbpath_prefix(self):
        """Return dbpath prefix."""
        return self._dbpath_prefix

    def get_internal_connection_string(self):
        """Return the connection string for this fixture.

        This is NOT a driver connection string, but a connection string of the format
        expected by the mongo::ConnectionString class.
        """
        raise NotImplementedError(
            "get_internal_connection_string must be implemented by Fixture subclasses"
        )

    def get_driver_connection_url(self):
        """Return the mongodb connection string as defined below.

        https://docs.mongodb.com/manual/reference/connection-string/
        """
        raise NotImplementedError(
            "get_driver_connection_url must be implemented by Fixture subclasses"
        )

    def mongo_client(self,
                     read_preference=pymongo.ReadPreference.PRIMARY,
                     timeout_millis=30000):
        """Return a pymongo.MongoClient connecting to this fixture with specified 'read_preference'.

        The PyMongo driver will wait up to 'timeout_millis' milliseconds
        before concluding that the server is unavailable.
        """

        kwargs = {"connectTimeoutMS": timeout_millis}
        if pymongo.version_tuple[0] >= 3:
            kwargs["serverSelectionTimeoutMS"] = timeout_millis
            kwargs["connect"] = True

        return pymongo.MongoClient(host=self.get_driver_connection_url(),
                                   read_preference=read_preference,
                                   **kwargs)

    def __str__(self):
        return "%s (Job #%d)" % (self.__class__.__name__, self.job_num)

    def __repr__(self):
        return "%r(%r, %r)" % (self.__class__.__name__, self.logger,
                               self.job_num)
Example #6
0
class TestCase(unittest.TestCase, metaclass=registry.make_registry_metaclass(_TEST_CASES)):  # pylint: disable=too-many-instance-attributes, invalid-metaclass
    """A test case to execute."""

    REGISTERED_NAME = registry.LEAVE_UNREGISTERED

    def __init__(self, logger, test_kind, test_name, dynamic=False):
        """Initialize the TestCase with the name of the test."""
        unittest.TestCase.__init__(self, methodName="run_test")

        if not isinstance(logger, logging.Logger):
            raise TypeError("logger must be a Logger instance")

        if not isinstance(test_kind, str):
            raise TypeError("test_kind must be a string")

        if not isinstance(test_name, str):
            raise TypeError("test_name must be a string")

        self._id = uuid.uuid4()

        # When the TestCase is created by the TestSuiteExecutor (through a call to make_test_case())
        # logger is an instance of TestQueueLogger. When the TestCase is created by a hook
        # implementation it is an instance of BaseLogger.
        self.logger = logger
        # Used to store the logger when overridden by a test logger in Report.start_test().
        self._original_logger = None

        self.test_kind = test_kind
        self.test_name = test_name
        self.dynamic = dynamic

        self.fixture = None
        self.return_code = None
        self.propagate_error = None

        self.is_configured = False

    def long_name(self):
        """Return the path to the test, relative to the current working directory."""
        return os.path.relpath(self.test_name)

    def basename(self):
        """Return the basename of the test."""
        return os.path.basename(self.test_name)

    def short_name(self):
        """Return the basename of the test without the file extension."""
        return os.path.splitext(self.basename())[0]

    def id(self):
        """Return the id of the test."""
        return self._id

    def short_description(self):
        """Return the short_description of the test."""
        return "%s %s" % (self.test_kind, self.test_name)

    def override_logger(self, new_logger):
        """Override this instance's logger with a new logger.

        This method is used by the repport to set the test logger.
        """
        assert not self._original_logger, "Logger already overridden"
        self._original_logger = self.logger
        self.logger = new_logger

    def reset_logger(self):
        """Reset this instance's logger to its original value."""
        assert self._original_logger, "Logger was not overridden"
        self.logger = self._original_logger
        self._original_logger = None

    def configure(self, fixture, *args, **kwargs):  # pylint: disable=unused-argument
        """Store 'fixture' as an attribute for later use during execution."""
        if self.is_configured:
            raise RuntimeError("configure can only be called once")

        self.is_configured = True
        self.fixture = fixture

    def run_test(self):
        """Run the specified test."""
        raise NotImplementedError("run_test must be implemented by TestCase subclasses")

    def as_command(self):  # pylint: disable=no-self-use
        """Return the command invocation used to run the test or None."""
        return None
Example #7
0
class Historic(ABC,
               metaclass=registry.make_registry_metaclass(
                   _HISTORICS, type(ABC))):  # pylint: disable=invalid-metaclass
    """ABC for classes that have trackable historic state."""
    def __init__(self):
        """Initialize subscriber list."""
        self._subscribers = []

    def subscribe(self, subscriber, key):
        """
        Subscribe to the Historic object.

        The subscriber's accept_write is called on an update.
        """
        if not isinstance(subscriber, Historic):
            raise ValueError(
                "Subscribers should inherit from the Historic ABC.")

        self._subscribers.append(Subscriber(obj=subscriber, key=key))

    def unsubscribe(self, subscriber):
        """Allow a subscriber to unsubscribe from notifications."""
        self._subscribers = [
            sub for sub in self._subscribers if sub.obj is not subscriber
        ]

    def notify_subscriber_write(self):
        """Notify the subscribers that a write has happened."""
        for subscriber in self._subscribers:
            subscriber.obj.accept_write(subscriber.key)

    @abstractmethod
    def to_storable_dict(self):
        """
        Convert this object to a dict that can be stored in yaml.

        Note that if a Historic stores history data itself, this is allowed to
        be lost in the storage/retrieval of subordinate Historics.
        """
        return

    @staticmethod
    @abstractmethod
    def from_storable_dict(raw_dict):
        """Create a new object from the raw dict returned by to_storable_dict."""
        return

    @staticmethod
    @abstractmethod
    def from_python_obj(obj):
        """
        Create a new Historic from the given python object.

        If inheriting from this in a class that wraps a python object,
        include a REGISTERED_NAME string attribute for the type it converts from.
        Otherwise, override this function to just return obj.
        """
        return

    def accept_write(self, key):  # pylint: disable=unused-argument
        """
        Update state based on a subscriber's write.

        Override this method if a class also tracks historic state.
        """
        self.notify_subscriber_write()
Example #8
0
class Fixture(object, metaclass=registry.make_registry_metaclass(_FIXTURES)):  # pylint: disable=invalid-metaclass
    """Base class for all fixtures."""

    # Error response codes copied from mongo/base/error_codes.yml.
    _WRITE_CONCERN_FAILED = 64
    _NODE_NOT_FOUND = 74
    _NEW_REPLICA_SET_CONFIGURATION_INCOMPATIBLE = 103
    _CONFIGURATION_IN_PROGRESS = 109
    _CURRENT_CONFIG_NOT_COMMITTED_YET = 308
    _INTERRUPTED_DUE_TO_REPL_STATE_CHANGE = 11602

    # We explicitly set the 'REGISTERED_NAME' attribute so that PyLint realizes that the attribute
    # is defined for all subclasses of Fixture.
    REGISTERED_NAME = "Fixture"

    AWAIT_READY_TIMEOUT_SECS = 300

    def __init__(self, logger, job_num, fixturelib, dbpath_prefix=None):
        """Initialize the fixture with a logger instance."""

        self.fixturelib = fixturelib
        self.config = self.fixturelib.get_config()

        self.fixturelib.assert_logger(logger)

        if not isinstance(job_num, int):
            raise TypeError("job_num must be an integer")
        elif job_num < 0:
            raise ValueError("job_num must be a nonnegative integer")

        self.logger = logger
        self.job_num = job_num

        dbpath_prefix = self.fixturelib.default_if_none(
            self.config.DBPATH_PREFIX, dbpath_prefix)
        dbpath_prefix = self.fixturelib.default_if_none(
            dbpath_prefix, self.config.DEFAULT_DBPATH_PREFIX)
        self._dbpath_prefix = os.path.join(dbpath_prefix,
                                           "job{}".format(self.job_num))

    def pids(self):
        """Return any pids owned by this fixture."""
        raise NotImplementedError(
            "pids must be implemented by Fixture subclasses %s" % self)

    def setup(self):
        """Create the fixture."""
        pass

    def await_ready(self):
        """Block until the fixture can be used for testing."""
        pass

    def teardown(self, finished=False, mode=None):  # noqa
        """Destroy the fixture.

        The fixture's logging handlers are closed if 'finished' is true,
        which should happen when setup() won't be called again.

        Raises:
            errors.ServerFailure: If the teardown is not successful.
        """

        try:
            self._do_teardown(mode=mode)
        finally:
            if finished:
                for handler in self.logger.handlers:
                    # We ignore the cancellation token returned by close_later() since we always
                    # want the logs to eventually get flushed.
                    self.fixturelib.close_loggers(handler)

    def _do_teardown(self, mode=None):  # noqa
        """Destroy the fixture.

        This method must be implemented by subclasses.

        Raises:
            errors.ServerFailure: If the teardown is not successful.
        """
        pass

    def is_running(self):  # pylint: disable=no-self-use
        """Return true if the fixture is still operating and more tests and can be run."""
        return True

    def get_node_info(self):  # pylint: disable=no-self-use
        """Return a list of NodeInfo objects."""
        return []

    def get_dbpath_prefix(self):
        """Return dbpath prefix."""
        return self._dbpath_prefix

    def get_path_for_archival(self):
        """
        Return the dbpath for archival that includes all possible directories.

        This includes directories for resmoke fixtures and fixtures spawned by the shell.
        """
        return self._dbpath_prefix

    def get_internal_connection_string(self):
        """Return the connection string for this fixture.

        This is NOT a driver connection string, but a connection string of the format
        expected by the mongo::ConnectionString class.
        """
        raise NotImplementedError(
            "get_internal_connection_string must be implemented by Fixture subclasses"
        )

    def get_driver_connection_url(self):
        """Return the mongodb connection string as defined below.

        https://docs.mongodb.com/manual/reference/connection-string/
        """
        raise NotImplementedError(
            "get_driver_connection_url must be implemented by Fixture subclasses"
        )

    def mongo_client(self,
                     read_preference=pymongo.ReadPreference.PRIMARY,
                     timeout_millis=30000):
        """Return a pymongo.MongoClient connecting to this fixture with specified 'read_preference'.

        The PyMongo driver will wait up to 'timeout_millis' milliseconds
        before concluding that the server is unavailable.
        """

        kwargs = {"connectTimeoutMS": timeout_millis}
        if pymongo.version_tuple[0] >= 3:
            kwargs["serverSelectionTimeoutMS"] = timeout_millis
            kwargs["connect"] = True

        return pymongo.MongoClient(host=self.get_driver_connection_url(),
                                   read_preference=read_preference,
                                   **kwargs)

    def __str__(self):
        return "%s (Job #%d)" % (self.__class__.__name__, self.job_num)

    def __repr__(self):
        return "%r(%r, %r)" % (self.__class__.__name__, self.logger,
                               self.job_num)