338 lines
12 KiB
Python
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
|