pytest_suite.py 8.58 KB
Newer Older
1
"""
2
Classes used for defining and running pytest test suites
3 4
"""
import os
5
from glob import glob
6
from pavelib.utils.test import utils as test_utils
7
from pavelib.utils.test.suites.suite import TestSuite
8 9
from pavelib.utils.envs import Env

10 11 12 13 14
try:
    from pygments.console import colorize
except ImportError:
    colorize = lambda color, text: text

15 16 17
__test__ = False  # do not collect


18
class PytestSuite(TestSuite):
19 20
    """
    A subclass of TestSuite with extra methods that are specific
21
    to pytest tests
22 23
    """
    def __init__(self, *args, **kwargs):
24
        super(PytestSuite, self).__init__(*args, **kwargs)
25 26 27
        self.failed_only = kwargs.get('failed_only', False)
        self.fail_fast = kwargs.get('fail_fast', False)
        self.run_under_coverage = kwargs.get('with_coverage', True)
28 29 30 31
        django_version = kwargs.get('django_version', None)
        if django_version is None:
            self.django_toxenv = None
        else:
32
            self.django_toxenv = 'py27-django{}'.format(django_version.replace('.', ''))
33
        self.disable_capture = kwargs.get('disable_capture', None)
34
        self.report_dir = Env.REPORT_DIR / self.root
35 36 37 38 39 40 41 42

        # If set, put reports for run in "unique" directories.
        # The main purpose of this is to ensure that the reports can be 'slurped'
        # in the main jenkins flow job without overwriting the reports from other
        # build steps. For local development/testing, this shouldn't be needed.
        if os.environ.get("SHARD", None):
            shard_str = "shard_{}".format(os.environ.get("SHARD"))
            self.report_dir = self.report_dir / shard_str
43
        self.xunit_report = self.report_dir / "nosetests.xml"
44

45
        self.cov_args = kwargs.get('cov_args', '')
46 47

    def __enter__(self):
48
        super(PytestSuite, self).__enter__()
49 50 51 52 53 54
        self.report_dir.makedirs_p()

    def __exit__(self, exc_type, exc_value, traceback):
        """
        Cleans mongo afer the tests run.
        """
55
        super(PytestSuite, self).__exit__(exc_type, exc_value, traceback)
56 57 58 59 60 61 62 63 64
        test_utils.clean_mongo()

    def _under_coverage_cmd(self, cmd):
        """
        If self.run_under_coverage is True, it returns the arg 'cmd'
        altered to be run under coverage. It returns the command
        unaltered otherwise.
        """
        if self.run_under_coverage:
65 66
            cmd.append('--cov')
            cmd.append('--cov-report=')
67 68 69

        return cmd

70 71 72 73 74 75 76 77
    @staticmethod
    def is_success(exit_code):
        """
        An exit code of zero means all tests passed, 5 means no tests were
        found.
        """
        return exit_code in [0, 5]

78 79 80 81 82 83
    @property
    def test_options_flags(self):
        """
        Takes the test options and returns the appropriate flags
        for the command.
        """
84
        opts = []
85 86 87

        # Handle "--failed" as a special case: we want to re-run only
        # the tests that failed within our Django apps
88 89
        # This sets the --last-failed flag for the pytest command, so this
        # functionality is the same as described in the pytest documentation
90
        if self.failed_only:
91
            opts.append("--last-failed")
92

93
        # This makes it so we use pytest's fail-fast feature in two cases.
94
        # Case 1: --fail-fast is passed as an arg in the paver command
95 96 97 98 99 100
        # Case 2: The environment variable TESTS_FAIL_FAST is set as True
        env_fail_fast_set = (
            'TESTS_FAIL_FAST' in os.environ and os.environ['TEST_FAIL_FAST']
        )

        if self.fail_fast or env_fail_fast_set:
101
            opts.append("--exitfirst")
102

103 104 105
        return opts


106
class SystemTestSuite(PytestSuite):
107
    """
108
    TestSuite for lms and cms python unit tests
109 110 111
    """
    def __init__(self, *args, **kwargs):
        super(SystemTestSuite, self).__init__(*args, **kwargs)
112
        self.eval_attr = kwargs.get('eval_attr', None)
113 114 115
        self.test_id = kwargs.get('test_id', self._default_test_id)
        self.fasttest = kwargs.get('fasttest', False)

116 117
        self.processes = kwargs.get('processes', None)
        self.randomize = kwargs.get('randomize', None)
118
        self.settings = kwargs.get('settings', Env.TEST_SETTINGS)
119 120

        if self.processes is None:
121
            # Don't use multiprocessing by default
122
            self.processes = 0
123

124 125 126 127 128 129 130 131
        self.processes = int(self.processes)

    def __enter__(self):
        super(SystemTestSuite, self).__enter__()

    @property
    def cmd(self):

132 133 134
        if self.django_toxenv:
            cmd = ['tox', '-e', self.django_toxenv, '--']
        else:
135
            cmd = []
136
        cmd.extend([
137 138 139 140
            'python',
            '-Wd',
            '-m',
            'pytest',
141
            '--ds={}'.format('{}.envs.{}'.format(self.root, self.settings)),
142
            "--junitxml={}".format(self.xunit_report),
143 144
        ])
        cmd.extend(self.test_options_flags)
145 146 147 148 149
        if self.verbosity < 1:
            cmd.append("--quiet")
        elif self.verbosity > 1:
            cmd.append("--verbose")

150 151 152
        if self.disable_capture:
            cmd.append("-s")

153 154 155 156 157 158 159 160 161 162 163
        if self.processes == -1:
            cmd.append('-n auto')
            cmd.append('--dist=loadscope')
        elif self.processes != 0:
            cmd.append('-n {}'.format(self.processes))
            cmd.append('--dist=loadscope')

        if not self.randomize:
            cmd.append('-p no:randomly')
        if self.eval_attr:
            cmd.append("-a '{}'".format(self.eval_attr))
164

165
        cmd.extend(self.passthrough_options)
166
        cmd.append(self.test_id)
167

168
        return self._under_coverage_cmd(cmd)
169 170

    @property
171 172 173 174 175 176 177 178
    def _default_test_id(self):
        """
        If no test id is provided, we need to limit the test runner
        to the Djangoapps we want to test.  Otherwise, it will
        run tests on all installed packages. We do this by
        using a default test id.
        """
        # We need to use $DIR/*, rather than just $DIR so that
179
        # pytest will import them early in the test process,
180 181
        # thereby making sure that we load any django models that are
        # only defined in test files.
182 183 184 185 186 187 188
        default_test_globs = [
            "{system}/djangoapps/*".format(system=self.root),
            "common/djangoapps/*",
            "openedx/core/djangoapps/*",
            "openedx/tests/*",
            "openedx/core/lib/*",
        ]
189
        if self.root in ('lms', 'cms'):
190
            default_test_globs.append("{system}/lib/*".format(system=self.root))
191 192

        if self.root == 'lms':
193 194 195 196 197 198 199 200
            default_test_globs.append("{system}/tests.py".format(system=self.root))
            default_test_globs.append("openedx/core/djangolib/*")
            default_test_globs.append("openedx/features")

        def included(path):
            """
            Should this path be included in the pytest arguments?
            """
201
            if path.endswith(Env.IGNORED_TEST_DIRS):
202 203 204 205 206 207 208 209 210 211 212 213 214
                return False
            return path.endswith('.py') or os.path.isdir(path)

        default_test_paths = []
        for path_glob in default_test_globs:
            if '*' in path_glob:
                default_test_paths += [path for path in glob(path_glob) if included(path)]
            else:
                default_test_paths += [path_glob]
        return ' '.join(default_test_paths)


class LibTestSuite(PytestSuite):
215
    """
216
    TestSuite for edx-platform/common/lib python unit tests
217 218 219
    """
    def __init__(self, *args, **kwargs):
        super(LibTestSuite, self).__init__(*args, **kwargs)
220
        self.append_coverage = kwargs.get('append_coverage', False)
221 222 223 224
        self.test_id = kwargs.get('test_id', self.root)

    @property
    def cmd(self):
225 226 227
        if self.django_toxenv:
            cmd = ['tox', '-e', self.django_toxenv, '--']
        else:
228
            cmd = []
229
        cmd.extend([
230 231 232 233 234 235 236
            'python',
            '-Wd',
            '-m',
            'pytest',
            '-p',
            'no:randomly',
            '--junitxml={}'.format(self.xunit_report),
237 238
        ])
        cmd.extend(self.passthrough_options + self.test_options_flags)
239 240 241 242
        if self.verbosity < 1:
            cmd.append("--quiet")
        elif self.verbosity > 1:
            cmd.append("--verbose")
243 244
        if self.disable_capture:
            cmd.append("-s")
245
        cmd.append(self.test_id)
246 247

        return self._under_coverage_cmd(cmd)
248 249 250 251 252 253 254 255 256 257 258 259 260 261

    def _under_coverage_cmd(self, cmd):
        """
        If self.run_under_coverage is True, it returns the arg 'cmd'
        altered to be run under coverage. It returns the command
        unaltered otherwise.
        """
        if self.run_under_coverage:
            cmd.append('--cov')
            if self.append_coverage:
                cmd.append('--cov-append')
            cmd.append('--cov-report=')

        return cmd