Merge pull request #768 from edx/will/use-mixed-modulestore-in-tests

Will/use mixed modulestore in tests
This commit is contained in:
Will Daly
2013-08-23 08:38:09 -07:00
40 changed files with 426 additions and 350 deletions

View File

@@ -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):
"""

View File

@@ -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):

View File

@@ -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

View File

@@ -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()

View 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

View File

@@ -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)

View File

@@ -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)