Revision 8311618451f6923a0e06f3ecf8cf39251d5df183 authored by iguerNL@Functori on 13 December 2022, 07:47:16 UTC, committed by Marge Bot on 13 December 2022, 10:36:18 UTC
1 parent e9dcec4
Raw File
conftest.py
"""Hooks and fixtures.

A fixture defines code to be run before and after a (sequence of) test,
E.g. start and stop a server. The fixture is simply specified as a parameter
in the test function, and the yielded values is then accessible with this
parameter.
"""
import os
import sys
from typing import Optional, Iterator, Dict, Set
from collections import namedtuple

import pytest

# Should only be used for type hints
import _pytest

from pytest_regtest import (
    register_converter_pre,
    deregister_converter_pre,
    _std_conversion,
    RegTestFixture,
)
from launchers.sandbox import Sandbox
from tools import constants, paths, utils
from tools.client_regression import ClientRegression
from tools.utils import bake
from client.client import Client

pytest_plugins = ("pytest_plugins.job_selection",)


@pytest.fixture(scope="session", autouse=True)
def sanity_check(request) -> None:
    """Sanity checks before running the tests."""
    log_dir = request.config.getoption("--log-dir")
    if not (log_dir is None or os.path.isdir(log_dir)):
        pytest.exit(f"{log_dir} doesn't exist")


@pytest.fixture(scope="session")
def log_dir(request) -> str:
    """Retrieve user-provided logging directory on the command line."""
    return request.config.getoption("--log-dir")


@pytest.fixture(scope="session")
def singleprocess(request) -> Iterator[bool]:
    """Retrieve user-provided single process mode on the command line."""
    yield request.config.getoption("--singleprocess")


@pytest.fixture(scope="class")
def session() -> Iterator[dict]:
    """Dictionary to store data between tests."""
    yield {}


def pytest_runtest_makereport(item, call) -> None:
    # hook for incremental test
    # from https://docs.pytest.org/en/latest/example/simple.html
    if "incremental" in item.keywords:
        if call.excinfo is not None:
            parent = item.parent
            # TODO can we do without this hack?
            parent._previousfailed = item  # pylint: disable=protected-access


def pytest_runtest_setup(item) -> None:
    if "incremental" in item.keywords:
        previousfailed = getattr(item.parent, "_previousfailed", None)
        if previousfailed is not None:
            pytest.xfail("previous test failed (%s)" % previousfailed.name)


DEAD_DAEMONS_WARN = '''
It seems some daemons terminated unexpectedly, or didn't launch properly.
You can investigate daemon logs by running this test using the
`--log-dir=LOG_DIR` option.'''


@pytest.fixture(scope="class")
def sandbox(log_dir: Optional[str], singleprocess: bool) -> Iterator[Sandbox]:
    """Sandboxed network of nodes.

    Nodes, bakers and endorsers are added/removed dynamically."""
    # log_dir is None if not provided on command-line
    # singleprocess is false if not provided on command-line
    with Sandbox(
        paths.TEZOS_HOME,
        constants.IDENTITIES,
        log_dir=log_dir,
        singleprocess=singleprocess,
    ) as sandbox:
        yield sandbox
        assert sandbox.are_daemons_alive(), DEAD_DAEMONS_WARN


@pytest.fixture(scope="function")
def client_regtest(
    client_regtest_bis: ClientRegression, regtest
) -> Iterator[Client]:
    """The client for one node with protocol alpha, with a function level
    regression test fixture."""
    deregister_converter_pre(_std_conversion)
    client_regtest_bis.set_regtest(regtest)
    register_converter_pre(utils.client_always_output_converter)
    yield client_regtest_bis
    deregister_converter_pre(utils.client_always_output_converter)


@pytest.fixture(scope="function")
def client_regtest_scrubbed(
    client_regtest: ClientRegression,
) -> Iterator[Client]:
    """One node with protocol alpha, regression test and scrubbing enabled."""
    register_converter_pre(utils.client_output_converter)
    yield client_regtest
    deregister_converter_pre(utils.client_output_converter)


@pytest.fixture(scope="function")
def client_regtest_custom_scrubber(
    client_regtest: ClientRegression, request
) -> Iterator[Client]:
    """One node with protocol alpha, regression test and custom scrubbing.

    The custom scrubbing is configured by in direct parameterization. For
    example, to replace all `foo` with `bar` and `baz` to `gaz`:

    @pytest.mark.parametrize('client_regtest_custom_scrubber', [
        [(r'foo', 'bar'), (r'baz', 'gaz')]
    ], indirect=True)"""

    def scrubber(string):
        print(request.param)
        return utils.suball(request.param, string)

    register_converter_pre(scrubber)
    yield client_regtest
    deregister_converter_pre(scrubber)


class CollectionLogger:
    '''Used to log messages during item collection. It just makes sure
    that an empty line is printed before any other warnings are
    emitted. This is necessary to avoid truncating the first
    message. An object is needed simply to wrap the output_started
    variable.
    '''

    output_started = False

    def warn(self, msg):
        if not self.output_started:
            print()
            self.output_started = True
        print(msg)


def pytest_collection_modifyitems(config, items):
    """Adapted from pytest-fixture-marker: adds the regression marker
    to all tests that use the regtest fixture. Handles unknown
    regression traces according to the '--on-unknown-regression-files'
    option.
    """
    # pylint: disable=too-many-branches

    # Stores the regression test outputs of all collected test items
    # per directory.
    trace_files_by_dir: Dict[str, Set[str]] = {}

    for item in items:
        if 'regtest' in item.fixturenames:
            item.add_marker('regression')

            dirname = item.fspath.dirname

            # Obtain the path of this regression tests output file. We
            # do so by reading the RegTestFixture.result_file property
            # directly.

            # In this way, we avoid modifying the pytest_regtest
            # plugin directly. But to do so, we must create a phony
            # RegTestFixture. To do so, we need to create a phony
            # Request object. This is an ugly hack, but this code
            # should be temporary.
            Request = namedtuple('Request', 'fspath')
            request = Request(fspath=item.fspath)
            regtest_fixture = RegTestFixture(request, item.nodeid)
            trace_file = regtest_fixture.result_file

            if dirname in trace_files_by_dir:
                trace_files_by_dir[dirname].add(trace_file)
            else:
                trace_files_by_dir[dirname] = {trace_file}

    on_unknown_regression_files = config.getoption(
        '--on-unknown-regression-files'
    )
    if on_unknown_regression_files == 'ignore':
        return

    log = CollectionLogger()
    unknown_traces_found = False
    for dirname, registered_trace_files in trace_files_by_dir.items():
        trace_dir = os.path.join(dirname, '_regtest_outputs')
        stored_trace_files = {
            os.path.join(trace_dir, f)
            for f in os.listdir(trace_dir)
            if os.path.isfile(os.path.join(trace_dir, f))
        }

        unknown_traces = stored_trace_files - registered_trace_files
        unknown_traces_found = unknown_traces_found or len(unknown_traces) > 0
        for unknown_trace_file in unknown_traces:
            if on_unknown_regression_files == 'delete':
                log.warn("removing unknown trace file: " + unknown_trace_file)
                os.unlink(unknown_trace_file)
            else:
                log.warn("unknown trace file: " + unknown_trace_file)

    if on_unknown_regression_files == 'fail' and unknown_traces_found:
        # Calling sys.exit makes an ugly error trace, but I found no
        # easy but graceful way of doing this.
        sys.exit(1)
    if on_unknown_regression_files == 'check':
        if unknown_traces_found:
            sys.exit(1)
        else:
            # Run no tests but exit normally
            log.warn("No unknown traces found 👍")
            items[:] = []
    elif on_unknown_regression_files == 'delete':
        if not unknown_traces_found:
            log.warn("No unknown traces found")
            sys.exit(1)
        # Run no tests but exit normally
        items[:] = []


def pytest_sessionfinish(session, exitstatus):
    '''Another little hack. By default, pytest exits with error status
    pytest.ExitCode.NO_TESTS_COLLECTED != 0 when no tests are
    collected.  If '--on-unknown-regression-files' has set the tests
    to [], then we want to exit with status code 0.
    '''
    on_unknown_regression_files = session.config.getoption(
        '--on-unknown-regression-files'
    )
    if (
        on_unknown_regression_files in ['check', 'delete']
        and exitstatus == pytest.ExitCode.NO_TESTS_COLLECTED
    ):
        session.exitstatus = 0


def _wrap_path(binary: str) -> str:
    res = os.path.join(paths.TEZOS_HOME, binary)
    assert os.path.isfile(res), f'{res} is not a file'
    return res


CLIENT = 'octez-client'
CLIENT_ADMIN = 'octez-admin-client'


@pytest.fixture(scope="class")
def nodeless_client():
    """
    A client that is suited for being used in tests that do not
    require a node
    """
    client_path = _wrap_path(CLIENT)
    client_admin_path = _wrap_path(CLIENT_ADMIN)
    client = Client(client_path, client_admin_path, endpoint=None)
    yield client
    client.cleanup()


@pytest.fixture(scope="class")
def encrypted_account_with_tez(client: Client):
    """
    Import an encrypted account with some tez
    """
    client.import_secret_key(
        "encrypted_account",
        (
            "encrypted:edesk1n2uGpPtVaeyhWkZzTEcaPRzkQHrqkw5pk8VkZv"
            "p3rM5KSc3mYNH5cJEuNcfB91B3G3JakKzfLQSmrgF4ht"
        ),
        "password",
    )
    client.transfer(
        100, "bootstrap1", "encrypted_account", ["--burn-cap", "1.0"]
    )
    bake(client, bake_for="bootstrap1")


def pytest_addoption(parser: _pytest.config.argparsing.Parser) -> None:
    parser.addoption("--log-dir", action="store", help="specify log directory")
    parser.addoption(
        "--singleprocess",
        action='store_true',
        default=False,
        help="the node validates blocks using only one process,\
            useful for debugging",
    )
    parser.addoption(
        '--on-unknown-regression-files',
        choices=['ignore', 'warn', 'fail', 'check', 'delete'],
        default='ignore',
        help='''How to handle regression test outputs that are not
        declared by any of the collected test. MODE should be either
        'ignore' (default), 'warn', 'fail' or 'delete'.

        If set to 'ignore', ignore unknown output files.
        If set to 'warn', emit a warning for unknown output files.
        If set to 'fail', terminate execution with exit code 1 and
        without running any further action when unknown output files
        are found.
        If set to 'check', terminate execution with exit code 1 and
        without running any further action when unknown output files
        are found. If no unknown output files are found, run no further
        action and terminate execution with exit code 0.
        If set to 'delete', delete unknown output
        files. To check which files would be deleted, run with this
        option set to 'warn'.

        Note that outputs that belong to uncollected tests (i.e. tests
        not specified on the command-line) will be considered as
        unknown. Therefore, setting any value than 'ignore' gives
        confusing results unless full test directories
        (i.e. 'tests_alpha' or 'tests_XXX') are specified on the
        command-line.''',
    )
back to top