Merge pull request #768 from edx/will/use-mixed-modulestore-in-tests
Will/use mixed modulestore in tests
This commit is contained in:
@@ -8,19 +8,20 @@ from course_groups.models import CourseUserGroup
|
||||
from course_groups.cohorts import (get_cohort, get_course_cohorts,
|
||||
is_commentable_cohorted, get_cohort_by_name)
|
||||
|
||||
from xmodule.modulestore.django import modulestore, _MODULESTORES
|
||||
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import xml_store_config
|
||||
from xmodule.modulestore.tests.django_utils import mixed_store_config
|
||||
|
||||
# NOTE: running this with the lms.envs.test config works without
|
||||
# manually overriding the modulestore. However, running with
|
||||
# cms.envs.test doesn't.
|
||||
|
||||
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
|
||||
TEST_MAPPING = {'edX/toy/2012_Fall': 'xml'}
|
||||
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, TEST_MAPPING)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class TestCohorts(django.test.TestCase):
|
||||
|
||||
@staticmethod
|
||||
@@ -82,9 +83,7 @@ class TestCohorts(django.test.TestCase):
|
||||
"""
|
||||
Make sure that course is reloaded every time--clear out the modulestore.
|
||||
"""
|
||||
# don't like this, but don't know a better way to undo all changes made
|
||||
# to course. We don't have a course.clone() method.
|
||||
_MODULESTORES.clear()
|
||||
clear_existing_modulestores()
|
||||
|
||||
def test_get_cohort(self):
|
||||
"""
|
||||
|
||||
@@ -14,11 +14,9 @@ from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_store_config
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from xmodule.modulestore.django import editable_modulestore
|
||||
|
||||
from external_auth.models import ExternalAuthMap
|
||||
from external_auth.views import shib_login, course_specific_login, course_specific_register
|
||||
@@ -27,6 +25,8 @@ from student.views import create_account, change_enrollment
|
||||
from student.models import UserProfile, Registration, CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {})
|
||||
|
||||
# Shib is supposed to provide 'REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider'
|
||||
# attributes via request.META. We can count on 'Shib-Identity-Provider', and 'REMOTE_USER' being present
|
||||
# b/c of how mod_shib works but should test the behavior with the rest of the attributes present/missing
|
||||
@@ -64,7 +64,7 @@ def gen_all_identities():
|
||||
yield _build_identity_dict(mail, given_name, surname)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache')
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache')
|
||||
class ShibSPTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the Shibboleth SP, which communicates via request.META
|
||||
@@ -73,7 +73,7 @@ class ShibSPTest(ModuleStoreTestCase):
|
||||
request_factory = RequestFactory()
|
||||
|
||||
def setUp(self):
|
||||
self.store = modulestore()
|
||||
self.store = editable_modulestore()
|
||||
|
||||
@unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True)
|
||||
def test_exception_shib_login(self):
|
||||
|
||||
@@ -161,9 +161,10 @@ def reset_databases(scenario):
|
||||
mongo = MongoClient()
|
||||
mongo.drop_database(settings.CONTENTSTORE['OPTIONS']['db'])
|
||||
_CONTENTSTORE.clear()
|
||||
modulestore = xmodule.modulestore.django.modulestore()
|
||||
|
||||
modulestore = xmodule.modulestore.django.editable_modulestore()
|
||||
modulestore.collection.drop()
|
||||
xmodule.modulestore.django._MODULESTORES.clear()
|
||||
xmodule.modulestore.django.clear_existing_modulestores()
|
||||
|
||||
|
||||
# Uncomment below to trigger a screenshot on error
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.contrib.auth import authenticate, login
|
||||
from django.contrib.auth.middleware import AuthenticationMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.django import editable_modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from urllib import quote_plus
|
||||
|
||||
@@ -60,11 +60,9 @@ def register_by_course_id(course_id, is_staff=False):
|
||||
@world.absorb
|
||||
def clear_courses():
|
||||
# Flush and initialize the module store
|
||||
# It needs the templates because it creates new records
|
||||
# by cloning from the template.
|
||||
# Note that if your test module gets in some weird state
|
||||
# (though it shouldn't), do this manually
|
||||
# from the bash shell to drop it:
|
||||
# $ mongo test_xmodule --eval "db.dropDatabase()"
|
||||
modulestore().collection.drop()
|
||||
editable_modulestore().collection.drop()
|
||||
contentstore().fs_files.drop()
|
||||
|
||||
0
common/lib/xmodule/xmodule/js/libpeerconnection.log
Normal file
0
common/lib/xmodule/xmodule/js/libpeerconnection.log
Normal file
@@ -55,6 +55,7 @@ def modulestore(name='default'):
|
||||
|
||||
return _MODULESTORES[name]
|
||||
|
||||
|
||||
_loc_singleton = None
|
||||
def loc_mapper():
|
||||
"""
|
||||
@@ -69,3 +70,42 @@ def loc_mapper():
|
||||
_loc_singleton = LocMapperStore(settings.modulestore_options)
|
||||
return _loc_singleton
|
||||
|
||||
|
||||
def clear_existing_modulestores():
|
||||
"""
|
||||
Clear the existing modulestore instances, causing
|
||||
them to be re-created when accessed again.
|
||||
|
||||
This is useful for flushing state between unit tests.
|
||||
"""
|
||||
_MODULESTORES.clear()
|
||||
|
||||
|
||||
def editable_modulestore(name='default'):
|
||||
"""
|
||||
Retrieve a modulestore that we can modify.
|
||||
This is useful for tests that need to insert test
|
||||
data into the modulestore.
|
||||
|
||||
Currently, only Mongo-backed modulestores can be modified.
|
||||
Returns `None` if no editable modulestore is available.
|
||||
"""
|
||||
|
||||
# Try to retrieve the ModuleStore
|
||||
# Depending on the settings, this may or may not
|
||||
# be editable.
|
||||
store = modulestore(name)
|
||||
|
||||
# If this is a `MixedModuleStore`, then we will need
|
||||
# to retrieve the actual Mongo instance.
|
||||
# We assume that the default is Mongo.
|
||||
if hasattr(store, 'modulestores'):
|
||||
store = store.modulestores['default']
|
||||
|
||||
# At this point, we either have the ability to create
|
||||
# items in the store, or we do not.
|
||||
if hasattr(store, 'create_xmodule'):
|
||||
return store
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -1,11 +1,46 @@
|
||||
"""
|
||||
Modulestore configuration for test cases.
|
||||
"""
|
||||
|
||||
import copy
|
||||
from uuid import uuid4
|
||||
from django.test import TestCase
|
||||
from xmodule.modulestore.django import editable_modulestore, \
|
||||
clear_existing_modulestores
|
||||
|
||||
from django.conf import settings
|
||||
import xmodule.modulestore.django
|
||||
from unittest.util import safe_repr
|
||||
|
||||
def mixed_store_config(data_dir, mappings):
|
||||
"""
|
||||
Return a `MixedModuleStore` configuration, which provides
|
||||
access to both Mongo- and XML-backed courses.
|
||||
|
||||
`data_dir` is the directory from which to load XML-backed courses.
|
||||
`mappings` is a dictionary mapping course IDs to modulestores, for example:
|
||||
|
||||
{
|
||||
'MITx/2.01x/2013_Spring': 'xml',
|
||||
'edx/999/2013_Spring': 'default'
|
||||
}
|
||||
|
||||
where 'xml' and 'default' are the two options provided by this configuration,
|
||||
mapping (respectively) to XML-backed and Mongo-backed modulestores..
|
||||
"""
|
||||
mongo_config = mongo_store_config(data_dir)
|
||||
xml_config = xml_store_config(data_dir)
|
||||
|
||||
store = {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore',
|
||||
'OPTIONS': {
|
||||
'mappings': mappings,
|
||||
'stores': {
|
||||
'default': mongo_config['default'],
|
||||
'xml': xml_config['default']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
store['direct'] = store['default']
|
||||
return store
|
||||
|
||||
|
||||
def mongo_store_config(data_dir):
|
||||
@@ -27,6 +62,7 @@ def mongo_store_config(data_dir):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
store['direct'] = store['default']
|
||||
return store
|
||||
|
||||
@@ -45,23 +81,22 @@ def draft_mongo_store_config(data_dir):
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string'
|
||||
}
|
||||
|
||||
return {
|
||||
store = {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.draft.DraftModuleStore',
|
||||
'OPTIONS': modulestore_options
|
||||
},
|
||||
'direct': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
||||
'OPTIONS': modulestore_options
|
||||
}
|
||||
}
|
||||
|
||||
store['direct'] = store['default']
|
||||
return store
|
||||
|
||||
|
||||
def xml_store_config(data_dir):
|
||||
"""
|
||||
Defines default module store using XMLModuleStore.
|
||||
"""
|
||||
return {
|
||||
store = {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
|
||||
'OPTIONS': {
|
||||
@@ -71,12 +106,48 @@ def xml_store_config(data_dir):
|
||||
}
|
||||
}
|
||||
|
||||
store['direct'] = store['default']
|
||||
return store
|
||||
|
||||
|
||||
class ModuleStoreTestCase(TestCase):
|
||||
""" Subclass for any test case that uses the mongodb
|
||||
module store. This populates a uniquely named modulestore
|
||||
collection with templates before running the TestCase
|
||||
and drops it they are finished. """
|
||||
"""
|
||||
Subclass for any test case that uses a ModuleStore.
|
||||
Ensures that the ModuleStore is cleaned before/after each test.
|
||||
|
||||
Usage:
|
||||
|
||||
1. Create a subclass of `ModuleStoreTestCase`
|
||||
2. Use Django's @override_settings decorator to use
|
||||
the desired modulestore configuration.
|
||||
|
||||
For example:
|
||||
|
||||
MIXED_CONFIG = mixed_store_config(data_dir, mappings)
|
||||
|
||||
@override_settings(MODULESTORE=MIXED_CONFIG)
|
||||
class FooTest(ModuleStoreTestCase):
|
||||
# ...
|
||||
|
||||
3. Use factories (e.g. `CourseFactory`, `ItemFactory`) to populate
|
||||
the modulestore with test data.
|
||||
|
||||
NOTE:
|
||||
* For Mongo-backed courses (created with `CourseFactory`),
|
||||
the state of the course will be reset before/after each
|
||||
test method executes.
|
||||
|
||||
* For XML-backed courses, the course state will NOT
|
||||
reset between test methods (although it will reset
|
||||
between test classes)
|
||||
|
||||
The reason is: XML courses are not editable, so to reset
|
||||
a course you have to reload it from disk, which is slow.
|
||||
|
||||
If you do need to reset an XML course, use
|
||||
`clear_existing_modulestores()` directly in
|
||||
your `setUp()` method.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def update_course(course, data):
|
||||
@@ -89,107 +160,68 @@ class ModuleStoreTestCase(TestCase):
|
||||
|
||||
'data' is a dictionary with an entry for each CourseField we want to update.
|
||||
"""
|
||||
store = xmodule.modulestore.django.modulestore()
|
||||
store = editable_modulestore('direct')
|
||||
store.update_metadata(course.location, data)
|
||||
updated_course = store.get_instance(course.id, course.location)
|
||||
return updated_course
|
||||
|
||||
@staticmethod
|
||||
def flush_mongo_except_templates():
|
||||
def drop_mongo_collection():
|
||||
"""
|
||||
Delete everything in the module store except templates.
|
||||
If using a Mongo-backed modulestore, drop the collection.
|
||||
"""
|
||||
modulestore = xmodule.modulestore.django.modulestore()
|
||||
|
||||
# This query means: every item in the collection
|
||||
# that is not a template
|
||||
query = {"_id.course": {"$ne": "templates"}}
|
||||
# This will return the mongo-backed modulestore
|
||||
# even if we're using a mixed modulestore
|
||||
store = editable_modulestore()
|
||||
|
||||
# Remove everything except templates
|
||||
modulestore.collection.remove(query)
|
||||
modulestore.collection.drop()
|
||||
if hasattr(store, 'collection'):
|
||||
store.collection.drop()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Flush the mongo store and set up templates.
|
||||
Delete the existing modulestores, causing them to be reloaded.
|
||||
"""
|
||||
|
||||
# Use a uuid to differentiate
|
||||
# the mongo collections on jenkins.
|
||||
cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE)
|
||||
if 'direct' not in settings.MODULESTORE:
|
||||
settings.MODULESTORE['direct'] = settings.MODULESTORE['default']
|
||||
|
||||
settings.MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
|
||||
settings.MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
|
||||
xmodule.modulestore.django._MODULESTORES.clear()
|
||||
|
||||
print settings.MODULESTORE
|
||||
|
||||
# Clear out any existing modulestores,
|
||||
# which will cause them to be re-created
|
||||
# the next time they are accessed.
|
||||
clear_existing_modulestores()
|
||||
TestCase.setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""
|
||||
Revert to the old modulestore settings.
|
||||
Drop the existing modulestores, causing them to be reloaded.
|
||||
Clean up any data stored in Mongo.
|
||||
"""
|
||||
# Clean up by flushing the Mongo modulestore
|
||||
cls.drop_mongo_collection()
|
||||
|
||||
# Clean up by dropping the collection
|
||||
modulestore = xmodule.modulestore.django.modulestore()
|
||||
modulestore.collection.drop()
|
||||
# Clear out the existing modulestores,
|
||||
# which will cause them to be re-created
|
||||
# the next time they are accessed.
|
||||
# We do this at *both* setup and teardown just to be safe.
|
||||
clear_existing_modulestores()
|
||||
|
||||
xmodule.modulestore.django._MODULESTORES.clear()
|
||||
|
||||
# Restore the original modulestore settings
|
||||
settings.MODULESTORE = cls.orig_modulestore
|
||||
TestCase.tearDownClass()
|
||||
|
||||
def _pre_setup(self):
|
||||
"""
|
||||
Remove everything but the templates before each test.
|
||||
Flush the ModuleStore before each test.
|
||||
"""
|
||||
|
||||
# Flush anything that is not a template
|
||||
ModuleStoreTestCase.flush_mongo_except_templates()
|
||||
# Flush the Mongo modulestore
|
||||
ModuleStoreTestCase.drop_mongo_collection()
|
||||
|
||||
# Call superclass implementation
|
||||
super(ModuleStoreTestCase, self)._pre_setup()
|
||||
|
||||
def _post_teardown(self):
|
||||
"""
|
||||
Flush everything we created except the templates.
|
||||
Flush the ModuleStore after each test.
|
||||
"""
|
||||
# Flush anything that is not a template
|
||||
ModuleStoreTestCase.flush_mongo_except_templates()
|
||||
ModuleStoreTestCase.drop_mongo_collection()
|
||||
|
||||
# Call superclass implementation
|
||||
super(ModuleStoreTestCase, self)._post_teardown()
|
||||
|
||||
|
||||
def assert2XX(self, status_code, msg=None):
|
||||
"""
|
||||
Assert that the given value is a success status (between 200 and 299)
|
||||
"""
|
||||
msg = self._formatMessage(msg, "%s is not a success status" % safe_repr(status_code))
|
||||
self.assertTrue(status_code >= 200 and status_code < 300, msg=msg)
|
||||
|
||||
def assert3XX(self, status_code, msg=None):
|
||||
"""
|
||||
Assert that the given value is a redirection status (between 300 and 399)
|
||||
"""
|
||||
msg = self._formatMessage(msg, "%s is not a redirection status" % safe_repr(status_code))
|
||||
self.assertTrue(status_code >= 300 and status_code < 400, msg=msg)
|
||||
|
||||
def assert4XX(self, status_code, msg=None):
|
||||
"""
|
||||
Assert that the given value is a client error status (between 400 and 499)
|
||||
"""
|
||||
msg = self._formatMessage(msg, "%s is not a client error status" % safe_repr(status_code))
|
||||
self.assertTrue(status_code >= 400 and status_code < 500, msg=msg)
|
||||
|
||||
def assert5XX(self, status_code, msg=None):
|
||||
"""
|
||||
Assert that the given value is a server error status (between 500 and 599)
|
||||
"""
|
||||
msg = self._formatMessage(msg, "%s is not a server error status" % safe_repr(status_code))
|
||||
self.assertTrue(status_code >= 500 and status_code < 600, msg=msg)
|
||||
|
||||
@@ -5,11 +5,12 @@ from uuid import uuid4
|
||||
from pytz import UTC
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.django import editable_modulestore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xblock.core import Scope
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
|
||||
|
||||
class XModuleCourseFactory(Factory):
|
||||
"""
|
||||
Factory for XModule courses.
|
||||
@@ -25,10 +26,7 @@ class XModuleCourseFactory(Factory):
|
||||
display_name = kwargs.pop('display_name', None)
|
||||
location = Location('i4x', org, number, 'course', Location.clean(display_name))
|
||||
|
||||
try:
|
||||
store = modulestore('direct')
|
||||
except KeyError:
|
||||
store = modulestore()
|
||||
store = editable_modulestore('direct')
|
||||
|
||||
# Write the data to the mongo datastore
|
||||
new_course = store.create_xmodule(location)
|
||||
@@ -117,7 +115,7 @@ class XModuleItemFactory(Factory):
|
||||
if not isinstance(data, basestring):
|
||||
data.update(template.get('data'))
|
||||
|
||||
store = modulestore('direct')
|
||||
store = editable_modulestore('direct')
|
||||
|
||||
# This code was based off that in cms/djangoapps/contentstore/views.py
|
||||
parent = store.get_item(parent_location)
|
||||
|
||||
Reference in New Issue
Block a user