diff --git a/Gemfile b/Gemfile index 7f9d08da29..9b78098524 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ gem 'neat', '~> 1.4.0' gem 'colorize', '~> 0.5.8' gem 'launchy', '~> 2.1.2' gem 'sys-proctable', '~> 0.9.3' +gem 'dalli', '~> 2.6.4' # These gems aren't actually required; they are used by Linux and Mac to # detect when files change. If these gems are not installed, the system # will fall back to polling files. diff --git a/Gemfile.lock b/Gemfile.lock index 75602a8642..d8b4eca093 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,6 +6,7 @@ GEM sass (>= 3.2.0) thor colorize (0.5.8) + dalli (2.6.4) ffi (1.9.0) launchy (2.1.2) addressable (~> 2.3) @@ -26,6 +27,7 @@ PLATFORMS DEPENDENCIES bourbon (~> 3.1.8) colorize (~> 0.5.8) + dalli (~> 2.6.4) launchy (~> 2.1.2) neat (~> 1.4.0) rake (~> 10.0.3) diff --git a/cms/envs/bok_choy.auth.json b/cms/envs/bok_choy.auth.json new file mode 100644 index 0000000000..528549f1e5 --- /dev/null +++ b/cms/envs/bok_choy.auth.json @@ -0,0 +1,125 @@ +{ + "ANALYTICS_API_KEY": "", + "AWS_ACCESS_KEY_ID": "", + "AWS_SECRET_ACCESS_KEY": "", + "CELERY_BROKER_PASSWORD": "celery", + "CELERY_BROKER_USER": "celery", + "CONTENTSTORE": { + "DOC_STORE_CONFIG": { + "collection": "modulestore", + "db": "test", + "host": [ + "localhost" + ], + "password": "password", + "port": 27017, + "user": "edxapp" + }, + "ENGINE": "xmodule.contentstore.mongo.MongoContentStore", + "OPTIONS": { + "db": "test", + "host": [ + "localhost" + ], + "password": "password", + "port": 27017, + "user": "edxapp" + } + }, + "DATABASES": { + "default": { + "ENGINE": "django.db.backends.mysql", + "HOST": "localhost", + "NAME": "test", + "PASSWORD": "", + "PORT": "3306", + "USER": "root" + } + }, + "DOC_STORE_CONFIG": { + "collection": "modulestore", + "db": "test", + "host": [ + "localhost" + ], + "password": "password", + "port": 27017, + "user": "edxapp" + }, + "MODULESTORE": { + "default": { + "DOC_STORE_CONFIG": { + "collection": "modulestore", + "db": "test", + "host": [ + "localhost" + ], + "password": "password", + "port": 27017, + "user": "edxapp" + }, + "ENGINE": "xmodule.modulestore.mongo.DraftMongoModuleStore", + "OPTIONS": { + "collection": "modulestore", + "db": "test", + "default_class": "xmodule.hidden_module.HiddenDescriptor", + "fs_root": "** OVERRIDDEN **", + "host": [ + "localhost" + ], + "password": "password", + "port": 27017, + "render_template": "edxmako.shortcuts.render_to_string", + "user": "edxapp" + } + }, + "direct": { + "DOC_STORE_CONFIG": { + "collection": "modulestore", + "db": "test", + "host": [ + "localhost" + ], + "password": "password", + "port": 27017, + "user": "edxapp" + }, + "ENGINE": "xmodule.modulestore.mongo.MongoModuleStore", + "OPTIONS": { + "collection": "modulestore", + "db": "test", + "default_class": "xmodule.hidden_module.HiddenDescriptor", + "fs_root": "** OVERRIDDEN **", + "host": [ + "localhost" + ], + "password": "password", + "port": 27017, + "render_template": "edxmako.shortcuts.render_to_string", + "user": "edxapp" + } + } + }, + "OPEN_ENDED_GRADING_INTERFACE": { + "grading_controller": "grading_controller", + "password": "password", + "peer_grading": "peer_grading", + "staff_grading": "staff_grading", + "url": "http://localhost:18060/", + "username": "lms" + }, + "SECRET_KEY": "", + "XQUEUE_INTERFACE": { + "basic_auth": [ + "edx", + "edx" + ], + "django_auth": { + "password": "password", + "username": "lms" + }, + "url": "http://localhost:18040" + }, + "ZENDESK_API_KEY": "", + "ZENDESK_USER": "" +} diff --git a/cms/envs/bok_choy.env.json b/cms/envs/bok_choy.env.json new file mode 100644 index 0000000000..0902499563 --- /dev/null +++ b/cms/envs/bok_choy.env.json @@ -0,0 +1,99 @@ +{ + "ANALYTICS_SERVER_URL": "", + "BOOK_URL": "", + "BUGS_EMAIL": "bugs@example.com", + "BULK_EMAIL_DEFAULT_FROM_EMAIL": "no-reply@example.com", + "CACHES": { + "celery": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "KEY_FUNCTION": "util.memcache.safe_key", + "KEY_PREFIX": "integration_celery", + "LOCATION": [ + "localhost:11211" + ] + }, + "default": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "KEY_FUNCTION": "util.memcache.safe_key", + "KEY_PREFIX": "sandbox_default", + "LOCATION": [ + "localhost:11211" + ] + }, + "general": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "KEY_FUNCTION": "util.memcache.safe_key", + "KEY_PREFIX": "sandbox_general", + "LOCATION": [ + "localhost:11211" + ] + }, + "mongo_metadata_inheritance": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "KEY_FUNCTION": "util.memcache.safe_key", + "KEY_PREFIX": "integration_mongo_metadata_inheritance", + "LOCATION": [ + "localhost:11211" + ] + }, + "staticfiles": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "KEY_FUNCTION": "util.memcache.safe_key", + "KEY_PREFIX": "integration_static_files", + "LOCATION": [ + "localhost:11211" + ] + } + }, + "CELERY_BROKER_HOSTNAME": "localhost", + "CELERY_BROKER_TRANSPORT": "amqp", + "CERT_QUEUE": "certificates", + "CMS_BASE": "", + "CODE_JAIL": { + "limits": { + "REALTIME": 3, + "VMEM": 0 + } + }, + "COMMENTS_SERVICE_KEY": "password", + "COMMENTS_SERVICE_URL": "http://localhost:4567", + "CONTACT_EMAIL": "info@example.com", + "DEFAULT_FEEDBACK_EMAIL": "feedback@example.com", + "DEFAULT_FROM_EMAIL": "registration@example.com", + "EMAIL_BACKEND": "django.core.mail.backends.smtp.EmailBackend", + "FEATURES": { + "AUTH_USE_OPENID_PROVIDER": true, + "CERTIFICATES_ENABLED": true, + "ENABLE_DISCUSSION_SERVICE": true, + "ENABLE_INSTRUCTOR_ANALYTICS": true, + "ENABLE_S3_GRADE_DOWNLOADS": true, + "PREVIEW_LMS_BASE": "", + "SUBDOMAIN_BRANDING": false, + "SUBDOMAIN_COURSE_LISTINGS": false + }, + "FEEDBACK_SUBMISSION_EMAIL": "", + "GITHUB_REPO_ROOT": "** OVERRIDDEN **", + "GRADES_DOWNLOAD": { + "BUCKET": "edx-grades", + "ROOT_PATH": "/tmp/edx-s3/grades", + "STORAGE_TYPE": "localfs" + }, + "LMS_BASE": "", + "LOCAL_LOGLEVEL": "INFO", + "LOGGING_ENV": "sandbox", + "LOG_DIR": "** OVERRIDDEN **", + "MEDIA_URL": "", + "MKTG_URL_LINK_MAP": {}, + "PLATFORM_NAME": "edX", + "SEGMENT_IO_LMS": true, + "SERVER_EMAIL": "devops@example.com", + "SESSION_COOKIE_DOMAIN": null, + "SITE_NAME": "localhost", + "STATIC_ROOT_BASE": "** OVERRIDDEN **", + "STATIC_URL_BASE": "/static/", + "SYSLOG_SERVER": "", + "TECH_SUPPORT_EMAIL": "technical@example.com", + "THEME_NAME": "", + "TIME_ZONE": "America/New_York", + "WIKI_ENABLED": true +} diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py new file mode 100644 index 0000000000..78573e83bd --- /dev/null +++ b/cms/envs/bok_choy.py @@ -0,0 +1,46 @@ +# Settings for bok choy tests + +import os +from path import path + + +########################## Prod-like settings ################################### +# These should be as close as possible to the settings we use in production. +# As in prod, we read in environment and auth variables from JSON files. +# Unlike in prod, we use the JSON files stored in this repo. +# This is a convenience for ensuring (a) that we can consistently find the files +# and (b) that the files are the same in Jenkins as in local dev. +os.environ['SERVICE_VARIANT'] = 'bok_choy' +os.environ['CONFIG_ROOT'] = path(__file__).abspath().dirname() + +from aws import * # pylint: disable=W0401, W0614 + + +######################### Testing overrides #################################### + +# Needed for the `reset_db` management command +INSTALLED_APPS += ('django_extensions',) + +# Redirect to the test_root folder within the repo +TEST_ROOT = CONFIG_ROOT.dirname().dirname() / "test_root" +GITHUB_REPO_ROOT = (TEST_ROOT / "data").abspath() +LOG_DIR = (TEST_ROOT / "log").abspath() + +# Configure Mongo modulestore to use the test folder within the repo +for store in ["default", "direct"]: + MODULESTORE[store]['OPTIONS']['fs_root'] = (TEST_ROOT / "data").abspath() + +# Enable django-pipeline and staticfiles +STATIC_ROOT = (TEST_ROOT / "staticfiles").abspath() +PIPELINE = True + +# Silence noisy logs +import logging +LOG_OVERRIDES = [ + ('track.middleware', logging.CRITICAL) +] +for log_name, log_level in LOG_OVERRIDES: + logging.getLogger(log_name).setLevel(log_level) + +# Unfortunately, we need to use debug mode to serve staticfiles +DEBUG = True diff --git a/common/test/bok_choy/edxapp_pages/lms/__init__.py b/common/test/bok_choy/edxapp_pages/lms/__init__.py index fe4a325a5e..1a22747f06 100644 --- a/common/test/bok_choy/edxapp_pages/lms/__init__.py +++ b/common/test/bok_choy/edxapp_pages/lms/__init__.py @@ -1,4 +1,4 @@ import os # Get the URL of the instance under test -BASE_URL = os.environ.get('test_url', '') +BASE_URL = os.environ.get('test_url', 'http://localhost:8003') diff --git a/common/test/bok_choy/edxapp_pages/studio/__init__.py b/common/test/bok_choy/edxapp_pages/studio/__init__.py index fe4a325a5e..1965a7f353 100644 --- a/common/test/bok_choy/edxapp_pages/studio/__init__.py +++ b/common/test/bok_choy/edxapp_pages/studio/__init__.py @@ -1,4 +1,4 @@ import os # Get the URL of the instance under test -BASE_URL = os.environ.get('test_url', '') +BASE_URL = os.environ.get('test_url', 'http://localhost:8031') diff --git a/common/test/bok_choy/tests/__init__.py b/common/test/bok_choy/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/test/bok_choy/tests/test_info_pages.py b/common/test/bok_choy/tests/test_info_pages.py new file mode 100644 index 0000000000..c7bceb1d53 --- /dev/null +++ b/common/test/bok_choy/tests/test_info_pages.py @@ -0,0 +1,20 @@ +""" +Very simple test case to verify bok-choy integration. +""" + +from bok_choy.web_app_test import WebAppTest +from edxapp_pages.lms.info import InfoPage + + +class InfoPageTest(WebAppTest): + """ + Test that the top-level pages in the LMS load. + """ + + @property + def page_object_classes(self): + return [InfoPage] + + def test_info(self): + for section_name in InfoPage.sections(): + self.ui.visit('lms.info', section=section_name) diff --git a/jenkins/all-tests.sh b/jenkins/all-tests.sh index d9aaa2678c..d63366f3d4 100755 --- a/jenkins/all-tests.sh +++ b/jenkins/all-tests.sh @@ -20,6 +20,7 @@ set -e # because we couldn't think of a better place to put it) # - "lms-acceptance": Run the acceptance (Selenium) tests for the LMS # - "cms-acceptance": Run the acceptance (Selenium) tests for Studio +# - "bok-choy": Run acceptance tests that use the bok-choy framework # # `SHARD` is a number (1, 2, or 3) indicating which subset of the tests # to build. Currently, "lms-acceptance" has two shards (1 and 2), @@ -95,4 +96,8 @@ END rake test:acceptance:cms["-v 3 --tag shard_${SHARD}"] ;; + "bok-choy") + rake test:bok_choy + ;; + esac diff --git a/lms/envs/bok_choy.auth.json b/lms/envs/bok_choy.auth.json new file mode 100644 index 0000000000..858a6e154d --- /dev/null +++ b/lms/envs/bok_choy.auth.json @@ -0,0 +1,114 @@ +{ + "ANALYTICS_API_KEY": "", + "AWS_ACCESS_KEY_ID": "", + "AWS_SECRET_ACCESS_KEY": "", + "CELERY_BROKER_PASSWORD": "celery", + "CELERY_BROKER_USER": "celery", + "CONTENTSTORE": { + "DOC_STORE_CONFIG": { + "collection": "modulestore", + "db": "edxapp", + "host": [ + "localhost" + ], + "password": "password", + "port": 27017, + "user": "edxapp" + }, + "ENGINE": "xmodule.contentstore.mongo.MongoContentStore", + "OPTIONS": { + "db": "edxapp", + "host": [ + "localhost" + ], + "password": "password", + "port": 27017, + "user": "edxapp" + } + }, + "DATABASES": { + "default": { + "ENGINE": "django.db.backends.mysql", + "HOST": "localhost", + "NAME": "test", + "PASSWORD": "", + "PORT": "3306", + "USER": "root" + } + }, + "DOC_STORE_CONFIG": { + "collection": "modulestore", + "db": "test", + "host": [ + "localhost" + ], + "password": "password", + "port": 27017, + "user": "edxapp" + }, + "MODULESTORE": { + "default": { + "ENGINE": "xmodule.modulestore.mixed.MixedModuleStore", + "OPTIONS": { + "mappings": {}, + "stores": { + "default": { + "DOC_STORE_CONFIG": { + "collection": "modulestore", + "db": "test", + "host": [ + "localhost" + ], + "password": "password", + "port": 27017, + "user": "edxapp" + }, + "ENGINE": "xmodule.modulestore.mongo.MongoModuleStore", + "OPTIONS": { + "collection": "modulestore", + "db": "test", + "default_class": "xmodule.hidden_module.HiddenDescriptor", + "fs_root": "** OVERRIDDEN **", + "host": [ + "localhost" + ], + "password": "password", + "port": 27017, + "render_template": "edxmako.shortcuts.render_to_string", + "user": "edxapp" + } + }, + "xml": { + "ENGINE": "xmodule.modulestore.xml.XMLModuleStore", + "OPTIONS": { + "data_dir": "** OVERRIDDEN **", + "default_class": "xmodule.hidden_module.HiddenDescriptor" + } + } + } + } + } + }, + "OPEN_ENDED_GRADING_INTERFACE": { + "grading_controller": "grading_controller", + "password": "password", + "peer_grading": "peer_grading", + "staff_grading": "staff_grading", + "url": "http://localhost:18060/", + "username": "lms" + }, + "SECRET_KEY": "", + "XQUEUE_INTERFACE": { + "basic_auth": [ + "edx", + "edx" + ], + "django_auth": { + "password": "password", + "username": "lms" + }, + "url": "http://localhost:18040" + }, + "ZENDESK_API_KEY": "", + "ZENDESK_USER": "" +} diff --git a/lms/envs/bok_choy.env.json b/lms/envs/bok_choy.env.json new file mode 100644 index 0000000000..0902499563 --- /dev/null +++ b/lms/envs/bok_choy.env.json @@ -0,0 +1,99 @@ +{ + "ANALYTICS_SERVER_URL": "", + "BOOK_URL": "", + "BUGS_EMAIL": "bugs@example.com", + "BULK_EMAIL_DEFAULT_FROM_EMAIL": "no-reply@example.com", + "CACHES": { + "celery": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "KEY_FUNCTION": "util.memcache.safe_key", + "KEY_PREFIX": "integration_celery", + "LOCATION": [ + "localhost:11211" + ] + }, + "default": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "KEY_FUNCTION": "util.memcache.safe_key", + "KEY_PREFIX": "sandbox_default", + "LOCATION": [ + "localhost:11211" + ] + }, + "general": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "KEY_FUNCTION": "util.memcache.safe_key", + "KEY_PREFIX": "sandbox_general", + "LOCATION": [ + "localhost:11211" + ] + }, + "mongo_metadata_inheritance": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "KEY_FUNCTION": "util.memcache.safe_key", + "KEY_PREFIX": "integration_mongo_metadata_inheritance", + "LOCATION": [ + "localhost:11211" + ] + }, + "staticfiles": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "KEY_FUNCTION": "util.memcache.safe_key", + "KEY_PREFIX": "integration_static_files", + "LOCATION": [ + "localhost:11211" + ] + } + }, + "CELERY_BROKER_HOSTNAME": "localhost", + "CELERY_BROKER_TRANSPORT": "amqp", + "CERT_QUEUE": "certificates", + "CMS_BASE": "", + "CODE_JAIL": { + "limits": { + "REALTIME": 3, + "VMEM": 0 + } + }, + "COMMENTS_SERVICE_KEY": "password", + "COMMENTS_SERVICE_URL": "http://localhost:4567", + "CONTACT_EMAIL": "info@example.com", + "DEFAULT_FEEDBACK_EMAIL": "feedback@example.com", + "DEFAULT_FROM_EMAIL": "registration@example.com", + "EMAIL_BACKEND": "django.core.mail.backends.smtp.EmailBackend", + "FEATURES": { + "AUTH_USE_OPENID_PROVIDER": true, + "CERTIFICATES_ENABLED": true, + "ENABLE_DISCUSSION_SERVICE": true, + "ENABLE_INSTRUCTOR_ANALYTICS": true, + "ENABLE_S3_GRADE_DOWNLOADS": true, + "PREVIEW_LMS_BASE": "", + "SUBDOMAIN_BRANDING": false, + "SUBDOMAIN_COURSE_LISTINGS": false + }, + "FEEDBACK_SUBMISSION_EMAIL": "", + "GITHUB_REPO_ROOT": "** OVERRIDDEN **", + "GRADES_DOWNLOAD": { + "BUCKET": "edx-grades", + "ROOT_PATH": "/tmp/edx-s3/grades", + "STORAGE_TYPE": "localfs" + }, + "LMS_BASE": "", + "LOCAL_LOGLEVEL": "INFO", + "LOGGING_ENV": "sandbox", + "LOG_DIR": "** OVERRIDDEN **", + "MEDIA_URL": "", + "MKTG_URL_LINK_MAP": {}, + "PLATFORM_NAME": "edX", + "SEGMENT_IO_LMS": true, + "SERVER_EMAIL": "devops@example.com", + "SESSION_COOKIE_DOMAIN": null, + "SITE_NAME": "localhost", + "STATIC_ROOT_BASE": "** OVERRIDDEN **", + "STATIC_URL_BASE": "/static/", + "SYSLOG_SERVER": "", + "TECH_SUPPORT_EMAIL": "technical@example.com", + "THEME_NAME": "", + "TIME_ZONE": "America/New_York", + "WIKI_ENABLED": true +} diff --git a/lms/envs/bok_choy.py b/lms/envs/bok_choy.py new file mode 100644 index 0000000000..6674988e47 --- /dev/null +++ b/lms/envs/bok_choy.py @@ -0,0 +1,54 @@ +# Settings for bok choy tests + +import os +from path import path + + +CONFIG_ROOT = path(__file__).abspath().dirname() +TEST_ROOT = CONFIG_ROOT.dirname().dirname() / "test_root" + +########################## Prod-like settings ################################### +# These should be as close as possible to the settings we use in production. +# As in prod, we read in environment and auth variables from JSON files. +# Unlike in prod, we use the JSON files stored in this repo. +# This is a convenience for ensuring (a) that we can consistently find the files +# and (b) that the files are the same in Jenkins as in local dev. +os.environ['SERVICE_VARIANT'] = 'bok_choy' +os.environ['CONFIG_ROOT'] = CONFIG_ROOT + +from aws import * # pylint: disable=W0401, W0614 + + +######################### Testing overrides #################################### + +# Needed for the `reset_db` management command +INSTALLED_APPS += ('django_extensions',) + +# Redirect to the test_root folder within the repo +GITHUB_REPO_ROOT = (TEST_ROOT / "data").abspath() +LOG_DIR = (TEST_ROOT / "log").abspath() + +# Configure Mongo modulestore to use the test folder within the repo +MONGO_MODULESTORE = MODULESTORE['default']['OPTIONS']['stores']['default'] +MONGO_MODULESTORE['OPTIONS']['fs_root'] = (TEST_ROOT / "data").abspath() + +# Configure XML modulestore to use test root data dir +XML_MODULESTORE = MODULESTORE['default']['OPTIONS']['stores']['xml'] +XML_MODULESTORE['OPTIONS']['data_dir'] = (TEST_ROOT / "data").abspath() + +# Enable django-pipeline and staticfiles +STATIC_ROOT = (TEST_ROOT / "staticfiles").abspath() +PIPELINE = True + +# Silence noisy logs +import logging +LOG_OVERRIDES = [ + ('track.middleware', logging.CRITICAL), + ('edxmako.shortcuts', logging.ERROR), + ('dd.dogapi', logging.ERROR) +] +for log_name, log_level in LOG_OVERRIDES: + logging.getLogger(log_name).setLevel(log_level) + +# Unfortunately, we need to use debug mode to serve staticfiles +DEBUG = True diff --git a/rakelib/bok_choy.rake b/rakelib/bok_choy.rake new file mode 100644 index 0000000000..b7e330f475 --- /dev/null +++ b/rakelib/bok_choy.rake @@ -0,0 +1,187 @@ +# Run acceptance tests that use the bok-choy framework +# http://bok-choy.readthedocs.org/en/latest/ +require 'dalli' + + +# Mongo databases that will be dropped before/after the tests run +BOK_CHOY_MONGO_DATABASE = "test" + +# Control parallel test execution with environment variables +# Process timeout is the maximum amount of time to wait for results from a particular test case +BOK_CHOY_NUM_PARALLEL = ENV.fetch('NUM_PARALLEL', 1).to_i +BOK_CHOY_TEST_TIMEOUT = ENV.fetch("TEST_TIMEOUT", 300).to_f + +# Ensure that we have a directory to put logs and reports +BOK_CHOY_DIR = File.join(REPO_ROOT, "common", "test", "bok_choy") +BOK_CHOY_TEST_DIR = File.join(BOK_CHOY_DIR, "tests") +BOK_CHOY_LOG_DIR = File.join(REPO_ROOT, "test_root", "log") +directory BOK_CHOY_LOG_DIR + +BOK_CHOY_SERVERS = { + :lms => { :port => 8003, :log => File.join(BOK_CHOY_LOG_DIR, "bok_choy_lms.log") }, + :cms => { :port => 8031, :log => File.join(BOK_CHOY_LOG_DIR, "bok_choy_studio.log") } +} + +BOK_CHOY_CACHE = Dalli::Client.new('localhost:11211') + + +# Start the server we will run tests on +def start_servers() + BOK_CHOY_SERVERS.each do | service, info | + address = "0.0.0.0:#{info[:port]}" + singleton_process( + django_admin(service, 'bok_choy', 'runserver', address), + logfile=info[:log] + ) + end +end + + +# Wait until we get a successful response from the servers or time out +def wait_for_test_servers() + BOK_CHOY_SERVERS.each do | service, info | + ready = wait_for_server("0.0.0.0", info[:port]) + if not ready + fail("Could not contact #{service} test server") + end + end +end + + +def is_mongo_running() + # The mongo command will connect to the service, + # failing with a non-zero exit code if it cannot connect. + output = `mongo --eval "print('running')"` + return (output and output.include? "running") +end + + +def is_memcache_running() + # We use a Ruby memcache client to attempt to set a key + # in memcache. If we cannot do so because the service is not + # available, then this will raise an exception. + BOK_CHOY_CACHE.set('test', 'test') + return true +rescue Dalli::DalliError + return false +end + + +def is_mysql_running() + # We use the MySQL CLI client and capture its stderr + # If the client cannot connect successfully, stderr will be non-empty + output = `mysql -e "" 2>&1` + return output == "" +end + + +def nose_cmd(test_spec) + cmd = ["PYTHONPATH=#{BOK_CHOY_DIR}:$PYTHONPATH", "SCREENSHOT_DIR=#{BOK_CHOY_LOG_DIR}", "nosetests", test_spec] + if BOK_CHOY_NUM_PARALLEL > 1 + cmd += ["--processes=#{BOK_CHOY_NUM_PARALLEL}", "--process-timeout=#{BOK_CHOY_TEST_TIMEOUT}"] + end + return cmd.join(" ") +end + + +# Run the bok choy tests +# `test_spec` is a nose-style test specifier relative to the test directory +# Examples: +# - path/to/test.py +# - path/to/test.py:TestFoo +# - path/to/test.py:TestFoo.test_bar +# It can also be left blank to run all tests in the suite. +def run_bok_choy(test_spec) + if test_spec.nil? + sh(nose_cmd(BOK_CHOY_TEST_DIR)) + else + sh(nose_cmd(File.join(BOK_CHOY_TEST_DIR, test_spec))) + end +end + + +def clear_mongo() + sh("mongo #{BOK_CHOY_MONGO_DATABASE} --eval 'db.dropDatabase()' > /dev/null") +end + + +# Clean up data we created in the databases +def cleanup() + sh(django_admin('lms', 'bok_choy', 'flush', '--noinput')) + clear_mongo() +end + + +namespace :'test:bok_choy' do + + # Check that required services are running + task :check_services do + if not is_mongo_running() + fail("Mongo is not running locally.") + end + + if not is_memcache_running() + fail("Memcache is not running locally.") + end + + if not is_mysql_running() + fail("MySQL is not running locally.") + end + end + + desc "Process assets and set up database for bok-choy tests" + task :setup => [:check_services, :install_prereqs, BOK_CHOY_LOG_DIR] do + + # Clear any test data already in Mongo + clear_mongo() + + # Invalidate the cache + BOK_CHOY_CACHE.flush() + + # HACK: Since the CMS depends on the existence of some database tables + # that are now in common but used to be in LMS (Role/Permissions for Forums) + # we need to create/migrate the database tables defined in the LMS. + # We might be able to address this by moving out the migrations from + # lms/django_comment_client, but then we'd have to repair all the existing + # migrations from the upgrade tables in the DB. + # But for now for either system (lms or cms), use the lms + # definitions to sync and migrate. + sh(django_admin('lms', 'bok_choy', 'reset_db', '--noinput')) + sh(django_admin('lms', 'bok_choy', 'syncdb', '--noinput')) + sh(django_admin('lms', 'bok_choy', 'migrate', '--noinput')) + + # Collect static assets + Rake::Task["gather_assets"].invoke('lms', 'bok_choy') + Rake::Task["gather_assets"].reenable + Rake::Task["gather_assets"].invoke('cms', 'bok_choy') + end + + desc "Run acceptance tests that use the bok-choy framework but skip setup" + task :fast, [:test_spec] => [:check_services, BOK_CHOY_LOG_DIR] do |t, args| + + # Ensure the test servers are available + puts "Starting test servers...".red + start_servers() + puts "Waiting for servers to start...".red + wait_for_test_servers() + + begin + puts "Running test suite...".red + run_bok_choy(args.test_spec) + rescue + puts "Tests failed!".red + exit 1 + ensure + puts "Cleaning up databases...".red + cleanup() + end + end + +end + + +# Default: set up and run the tests +desc "Run acceptance tests that use the bok-choy framework" +task :'test:bok_choy', [:test_spec] => [:'test:bok_choy:setup'] do |t, args| + Rake::Task["test:bok_choy:fast"].invoke(args.test_spec) +end diff --git a/rakelib/helpers.rb b/rakelib/helpers.rb index 18473739b5..99126df583 100644 --- a/rakelib/helpers.rb +++ b/rakelib/helpers.rb @@ -2,6 +2,7 @@ require 'digest/md5' require 'sys/proctable' require 'colorize' require 'timeout' +require 'net/http' def find_executable(exec) path = %x(which #{exec}).strip @@ -55,38 +56,28 @@ end def background_process(command, logfile=nil) spawn_opts = {:pgroup => true} if !logfile.nil? - puts "Running '#{command.join(' ')}', redirecting output to #{logfile}".red + puts "Running '#{command.join(' ')}', redirecting output to #{logfile}" spawn_opts[[:err, :out]] = [logfile, 'a'] end pid = Process.spawn({}, *command, spawn_opts) command = [*command] at_exit do - puts "Ending process and children" pgid = Process.getpgid(pid) begin Timeout.timeout(5) do - puts "Interrupting process group #{pgid}" Process.kill(:SIGINT, -pgid) - puts "Waiting on process group #{pgid}" Process.wait(-pgid) - puts "Done waiting on process group #{pgid}" end rescue Timeout::Error begin Timeout.timeout(5) do - puts "Terminating process group #{pgid}" Process.kill(:SIGTERM, -pgid) - puts "Waiting on process group #{pgid}" Process.wait(-pgid) - puts "Done waiting on process group #{pgid}" end rescue Timeout::Error - puts "Killing process group #{pgid}" Process.kill(:SIGKILL, -pgid) - puts "Waiting on process group #{pgid}" Process.wait(-pgid) - puts "Done waiting on process group #{pgid}" end end end @@ -103,6 +94,25 @@ def singleton_process(command, logfile=nil) end end +# Wait for a server to respond with status 200 at "/" +def wait_for_server(server, port) + attempts = 0 + begin + http = Net::HTTP.start(server, port, {open_timeout: 10, read_timeout: 10}) + response = http.head("/") + response.code == "200" + true + rescue + sleep(1) + attempts += 1 + if attempts < 20 + retry + else + false + end + end +end + def environments(system) Dir["#{system}/envs/**/*.py"].select{|file| ! (/__init__.py$/ =~ file)}.map do |env_file| env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.') @@ -135,4 +145,3 @@ if !ENV['TESTS_FAIL_FAST'] Rake.application.top_level_tasks << :fail_tests end - diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index e8dcb9191b..16cd5ecd64 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -16,6 +16,7 @@ dealer==0.2.3 distribute>=0.6.28, <0.7 django-celery==3.0.17 django-countries==1.5 +django-extensions==1.2.5 django-filter==0.6.0 django-followit==0.0.3 django-keyedcache==1.4-6