Files
edx-platform/pavelib/utils/test/suites/bokchoy_suite.py
2021-03-12 14:14:53 +05:00

338 lines
12 KiB
Python

"""
Class used for defining and running Bok Choy acceptance test suite
"""
import os
from time import sleep
from paver.easy import call_task, cmdopts, dry, might_call, needs, sh, task
from common.test.acceptance.fixtures.course import CourseFixture, FixtureError
from pavelib.database import update_local_bokchoy_db_from_s3
from pavelib.utils.envs import Env
from pavelib.utils.test import utils as test_utils
from pavelib.utils.test.bokchoy_options import (
BOKCHOY_DEFAULT_STORE,
BOKCHOY_DEFAULT_STORE_DEPR,
BOKCHOY_FASTTEST,
BOKCHOY_IMPORTS_DIR,
BOKCHOY_IMPORTS_DIR_DEPR
)
from pavelib.utils.test.bokchoy_utils import check_services, clear_mongo, start_servers, wait_for_test_servers
from pavelib.utils.test.suites.suite import TestSuite
from pavelib.utils.timer import timed
try:
from pygments.console import colorize
except ImportError:
colorize = lambda color, text: text
__test__ = False # do not collect
DEFAULT_NUM_PROCESSES = 1
DEFAULT_VERBOSITY = 2
@task
@cmdopts([BOKCHOY_DEFAULT_STORE, BOKCHOY_DEFAULT_STORE_DEPR])
@timed
def load_bok_choy_data(options):
"""
Loads data into database from db_fixtures
"""
print('Loading data from json fixtures in db_fixtures directory')
sh(
"DEFAULT_STORE={default_store}"
" ./manage.py lms --settings {settings} loaddata --traceback"
" common/test/db_fixtures/*.json".format(
default_store=options.default_store,
settings=Env.SETTINGS
)
)
@task
@cmdopts([
BOKCHOY_IMPORTS_DIR, BOKCHOY_IMPORTS_DIR_DEPR, BOKCHOY_DEFAULT_STORE,
BOKCHOY_DEFAULT_STORE_DEPR
])
@timed
def load_courses(options):
"""
Loads courses from options.imports_dir.
Note: options.imports_dir is the directory that contains the directories
that have courses in them. For example, if the course is located in
`test_root/courses/test-example-course/`, options.imports_dir should be
`test_root/courses/`.
"""
if 'imports_dir' in options:
msg = colorize('green', f"Importing courses from {options.imports_dir}...")
print(msg)
sh(
"DEFAULT_STORE={default_store}"
" ./manage.py cms --settings={settings} import {import_dir}".format(
default_store=options.default_store,
import_dir=options.imports_dir,
settings=Env.SETTINGS
)
)
else:
print(colorize('blue', "--imports-dir not set, skipping import"))
@task
@timed
def update_fixtures():
"""
Use the correct domain for the current test environment in each Site
fixture. This currently differs between devstack cms, devstack lms,
and Jenkins.
"""
msg = colorize('green', "Updating the Site fixture domains...")
print(msg)
sh(
" ./manage.py lms --settings={settings} update_fixtures".format(
settings=Env.SETTINGS
)
)
@task
@timed
def reset_test_database():
"""
Reset the database used by the bokchoy tests.
Use the database cache automation defined in pavelib/database.py
"""
update_local_bokchoy_db_from_s3() # pylint: disable=no-value-for-parameter
@task
@needs(['reset_test_database', 'clear_mongo', 'load_bok_choy_data', 'load_courses', 'update_fixtures'])
@might_call('start_servers')
@cmdopts([BOKCHOY_FASTTEST], share_with=['start_servers'])
@timed
def prepare_bokchoy_run(options):
"""
Sets up and starts servers for a Bok Choy run. If --fasttest is not
specified then static assets are collected
"""
if not options.get('fasttest', False):
print(colorize('green', "Generating optimized static assets..."))
if options.get('log_dir') is None:
call_task('update_assets', args=['--settings', 'test_static_optimized'])
else:
call_task('update_assets', args=[
'--settings', 'test_static_optimized',
'--collect-log', options.log_dir
])
# Ensure the test servers are available
msg = colorize('green', "Confirming servers are running...")
print(msg)
start_servers() # pylint: disable=no-value-for-parameter
class BokChoyTestSuite(TestSuite):
"""
TestSuite for running Bok Choy tests
Properties (below is a subset):
test_dir - parent directory for tests
log_dir - directory for test output
report_dir - directory for reports (e.g., coverage) related to test execution
xunit_report - directory for xunit-style output (xml)
fasttest - when set, skip various set-up tasks (e.g., collectstatic)
serversonly - prepare and run the necessary servers, only stopping when interrupted with Ctrl-C
testsonly - assume servers are running (as per above) and run tests with no setup or cleaning of environment
test_spec - when set, specifies test files, classes, cases, etc. See platform doc.
default_store - modulestore to use when running tests (split or draft)
eval_attr - only run tests matching given attribute expression
num_processes - number of processes or threads to use in tests. Recommendation is that this
is less than or equal to the number of available processors.
verify_xss - when set, check for XSS vulnerabilities in the page HTML.
See pytest documentation: https://docs.pytest.org/en/latest/
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.test_dir = Env.BOK_CHOY_DIR / kwargs.get('test_dir', 'tests')
self.log_dir = Env.BOK_CHOY_LOG_DIR
self.report_dir = kwargs.get('report_dir', Env.BOK_CHOY_REPORT_DIR)
self.xunit_report = self.report_dir / "xunit.xml"
self.cache = Env.BOK_CHOY_CACHE
self.fasttest = kwargs.get('fasttest', False)
self.serversonly = kwargs.get('serversonly', False)
self.testsonly = kwargs.get('testsonly', False)
self.test_spec = kwargs.get('test_spec', None)
self.default_store = kwargs.get('default_store', None)
self.eval_attr = kwargs.get('eval_attr', None)
self.verbosity = kwargs.get('verbosity', DEFAULT_VERBOSITY)
self.num_processes = kwargs.get('num_processes', DEFAULT_NUM_PROCESSES)
self.verify_xss = kwargs.get('verify_xss', os.environ.get('VERIFY_XSS', True))
self.extra_args = kwargs.get('extra_args', '')
self.har_dir = self.log_dir / 'hars'
self.a11y_file = Env.BOK_CHOY_A11Y_CUSTOM_RULES_FILE
self.imports_dir = kwargs.get('imports_dir', None)
self.coveragerc = kwargs.get('coveragerc', None)
self.save_screenshots = kwargs.get('save_screenshots', False)
def __enter__(self):
super().__enter__()
# Ensure that we have a directory to put logs and reports
self.log_dir.makedirs_p()
self.har_dir.makedirs_p()
self.report_dir.makedirs_p()
test_utils.clean_reports_dir() # pylint: disable=no-value-for-parameter
# Set the environment so that webpack understands where to compile its resources.
# This setting is expected in other environments, so we are setting it for the
# bok-choy test run.
os.environ['EDX_PLATFORM_SETTINGS'] = 'test_static_optimized'
if not (self.fasttest or self.skip_clean or self.testsonly):
test_utils.clean_test_files()
msg = colorize('green', "Checking for mongo, memchache, and mysql...")
print(msg)
check_services()
if not self.testsonly:
call_task('prepare_bokchoy_run', options={'log_dir': self.log_dir, 'coveragerc': self.coveragerc})
else:
# load data in db_fixtures
load_bok_choy_data() # pylint: disable=no-value-for-parameter
update_fixtures()
msg = colorize('green', "Confirming servers have started...")
print(msg)
wait_for_test_servers()
try:
# Create course in order to seed forum data underneath. This is
# a workaround for a race condition. The first time a course is created;
# role permissions are set up for forums.
dry(
"Installing course fixture for forums",
CourseFixture('foobar_org', '1117', 'seed_forum', 'seed_foo').install
)
print('Forums permissions/roles data has been seeded')
except FixtureError:
# this means it's already been done
pass
if self.serversonly:
self.run_servers_continuously()
def __exit__(self, exc_type, exc_value, traceback):
super().__exit__(exc_type, exc_value, traceback)
# Using testsonly will leave all fixtures in place (Note: the db will also be dirtier.)
if self.testsonly:
msg = colorize('green', 'Running in testsonly mode... SKIPPING database cleanup.')
print(msg)
else:
# Clean up data we created in the databases
msg = colorize('green', "Cleaning up databases...")
print(msg)
sh(f"./manage.py lms --settings {Env.SETTINGS} flush --traceback --noinput")
clear_mongo()
@property
def verbosity_processes_command(self):
"""
Construct the proper combination of multiprocessing, XUnit XML file, color, and verbosity for use with pytest.
"""
command = [f"--junitxml={self.xunit_report}"]
if self.num_processes != 1:
# Construct "multiprocess" pytest command
command += [
f"-n {self.num_processes}",
"--color=no",
]
if self.verbosity < 1:
command.append("--quiet")
elif self.verbosity > 1:
command.append("--verbose")
if self.eval_attr:
command.append(f"-a '{self.eval_attr}'")
return command
def run_servers_continuously(self):
"""
Infinite loop. Servers will continue to run in the current session unless interrupted.
"""
print('Bok-choy servers running. Press Ctrl-C to exit...\n')
print('Note: pressing Ctrl-C multiple times can corrupt system state. Just press it once.\n')
while True:
try:
sleep(10000)
except KeyboardInterrupt:
print("Stopping bok-choy servers.\n")
break
@property
def cmd(self):
"""
This method composes the pytest command to send to the terminal. If pytest isn't being run,
the command returns None.
"""
# Default to running all tests if no specific test is specified
if not self.test_spec:
test_spec = self.test_dir
else:
test_spec = self.test_dir / self.test_spec
# Skip any additional commands (such as pytest) if running in
# servers only mode
if self.serversonly:
return None
# Construct the pytest command, specifying where to save
# screenshots and XUnit XML reports
cmd = [
f"DEFAULT_STORE={self.default_store}",
f"SAVED_SOURCE_DIR='{self.log_dir}'",
f"SCREENSHOT_DIR='{self.log_dir}'",
f"BOK_CHOY_HAR_DIR='{self.har_dir}'",
f"BOKCHOY_A11Y_CUSTOM_RULES_FILE='{self.a11y_file}'",
f"SELENIUM_DRIVER_LOG_DIR='{self.log_dir}'",
f"VERIFY_XSS='{self.verify_xss}'",
]
if self.save_screenshots:
cmd.append("NEEDLE_SAVE_BASELINE=True")
if self.coveragerc:
cmd += [
"coverage",
"run",
]
cmd.append(f"--rcfile={self.coveragerc}")
else:
cmd += [
"python",
"-Wd",
]
cmd += [
"-m",
"pytest",
]
if self.coveragerc:
cmd.extend([
'-p',
'openedx.testing.coverage_context_listener.pytest_plugin',
])
cmd.append(test_spec)
cmd.extend(self.verbosity_processes_command)
if self.extra_args:
cmd.append(self.extra_args)
cmd.extend(self.passthrough_options)
return cmd