Merge pull request #14165 from edx/ormsbee/suppress_publish_experiment2
Test Speedup: Isolate Modulestore Signals
This commit is contained in:
@@ -13,6 +13,8 @@ class TestHandleItemDeleted(ModuleStoreTestCase, MilestonesTestCaseMixin):
|
||||
"""
|
||||
Test case for handle_score_changed django signal handler
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Initial data setup
|
||||
|
||||
@@ -181,13 +181,13 @@ class ContentStoreImportTest(SignalDisconnectTestMixin, ModuleStoreTestCase):
|
||||
# we try to refresh the inheritance tree for each update_item in the import
|
||||
with check_exact_number_of_calls(store, 'refresh_cached_metadata_inheritance_tree', 28):
|
||||
|
||||
# _get_cached_metadata_inheritance_tree should be called twice (once for import, once on publish)
|
||||
with check_exact_number_of_calls(store, '_get_cached_metadata_inheritance_tree', 2):
|
||||
# _get_cached_metadata_inheritance_tree should be called once
|
||||
with check_exact_number_of_calls(store, '_get_cached_metadata_inheritance_tree', 1):
|
||||
|
||||
# with bulk-edit in progress, the inheritance tree should be recomputed only at the end of the import
|
||||
# NOTE: On Jenkins, with memcache enabled, the number of calls here is only 1.
|
||||
# Locally, without memcache, the number of calls is actually 2 (once more during the publish step)
|
||||
with check_number_of_calls(store, '_compute_metadata_inheritance_tree', 2):
|
||||
# NOTE: On Jenkins, with memcache enabled, the number of calls here is 1.
|
||||
# Locally, without memcache, the number of calls is 1 (publish no longer counted)
|
||||
with check_number_of_calls(store, '_compute_metadata_inheritance_tree', 1):
|
||||
self.load_test_import_course(create_if_not_present=False, module_store=store)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
|
||||
@@ -100,8 +100,8 @@ class TestOrphan(TestOrphanBase):
|
||||
self.assertIn(unicode(location), orphans)
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.split, 9, 6),
|
||||
(ModuleStoreEnum.Type.mongo, 34, 13),
|
||||
(ModuleStoreEnum.Type.split, 9, 5),
|
||||
(ModuleStoreEnum.Type.mongo, 34, 12),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_delete_orphans(self, default_store, max_mongo_calls, min_mongo_calls):
|
||||
|
||||
@@ -331,6 +331,8 @@ class TestCourseOutline(CourseTestCase):
|
||||
"""
|
||||
Unit tests for the course outline.
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up the for the course outline tests.
|
||||
@@ -579,6 +581,8 @@ class TestCourseReIndex(CourseTestCase):
|
||||
"""
|
||||
SUCCESSFUL_RESPONSE = _("Course has been successfully reindexed.")
|
||||
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up the for the course outline tests.
|
||||
|
||||
@@ -17,6 +17,7 @@ class TestSubsectionGating(CourseTestCase):
|
||||
Tests for the subsection gating feature
|
||||
"""
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
ENABLED_SIGNALS = ['item_deleted']
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
|
||||
@@ -142,6 +142,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
|
||||
OTHER_EMAIL = "jane@example.com"
|
||||
|
||||
ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache']
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
""" Create a course and user, then log in. """
|
||||
|
||||
@@ -29,6 +29,8 @@ class TestCourseListing(ModuleStoreTestCase, MilestonesTestCaseMixin):
|
||||
"""
|
||||
Unit tests for getting the list of courses for a logged in user
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_deleted']
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Add a student & teacher
|
||||
|
||||
@@ -229,6 +229,7 @@ class DashboardTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for dashboard utility functions
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
super(DashboardTest, self).setUp()
|
||||
@@ -434,10 +435,11 @@ class DashboardTest(ModuleStoreTestCase):
|
||||
# If user has a certificate with valid linked-in config then Add Certificate to LinkedIn button
|
||||
# should be visible. and it has URL value with valid parameters.
|
||||
self.client.login(username="jack", password="test")
|
||||
LinkedInAddToProfileConfiguration(
|
||||
|
||||
LinkedInAddToProfileConfiguration.objects.create(
|
||||
company_identifier='0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9',
|
||||
enabled=True
|
||||
).save()
|
||||
)
|
||||
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
@@ -474,6 +476,7 @@ class DashboardTest(ModuleStoreTestCase):
|
||||
u'pfCertificationUrl=www.edx.org&'
|
||||
u'source=o'
|
||||
).format(platform=quote(settings.PLATFORM_NAME.encode('utf-8')))
|
||||
|
||||
self.assertContains(response, escape(expected_url))
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
|
||||
@@ -57,6 +57,86 @@ log = logging.getLogger(__name__)
|
||||
ASSET_IGNORE_REGEX = getattr(settings, "ASSET_IGNORE_REGEX", r"(^\._.*$)|(^\.DS_Store$)|(^.*~$)")
|
||||
|
||||
|
||||
class SwitchedSignal(django.dispatch.Signal):
|
||||
"""
|
||||
SwitchedSignal is like a normal Django signal, except that you can turn it
|
||||
on and off. This is especially useful for tests where we want to be able to
|
||||
isolate signals and disable expensive operations that are irrelevant to
|
||||
what's being tested (like everything that triggers off of a course publish).
|
||||
|
||||
SwitchedSignals default to being on. You should be very careful if you ever
|
||||
turn one off -- the only instances of this class are shared class attributes
|
||||
of `SignalHandler`. You have to make sure that you re-enable the signal when
|
||||
you're done, or else you may permanently turn that signal off for that
|
||||
process. I can't think of any reason you'd want to disable signals outside
|
||||
of running tests.
|
||||
"""
|
||||
def __init__(self, name, *args, **kwargs):
|
||||
"""
|
||||
The `name` parameter exists only to make debugging more convenient.
|
||||
|
||||
All other args are passed to the constructor for django.dispatch.Signal.
|
||||
"""
|
||||
super(SwitchedSignal, self).__init__(*args, **kwargs)
|
||||
self.name = name
|
||||
self._allow_signals = True
|
||||
|
||||
def disable(self):
|
||||
"""
|
||||
Turn off signal sending.
|
||||
|
||||
All calls to send/send_robust will no-op.
|
||||
"""
|
||||
self._allow_signals = False
|
||||
|
||||
def enable(self):
|
||||
"""
|
||||
Turn on signal sending.
|
||||
|
||||
Calls to send/send_robust will behave like normal Django Signals.
|
||||
"""
|
||||
self._allow_signals = True
|
||||
|
||||
def send(self, *args, **kwargs):
|
||||
"""
|
||||
See `django.dispatch.Signal.send()`
|
||||
|
||||
This method will no-op and return an empty list if the signal has been
|
||||
disabled.
|
||||
"""
|
||||
log.debug(
|
||||
"SwitchedSignal %s's send() called with args %s, kwargs %s - %s",
|
||||
self.name,
|
||||
args,
|
||||
kwargs,
|
||||
"ALLOW" if self._allow_signals else "BLOCK"
|
||||
)
|
||||
if self._allow_signals:
|
||||
return super(SwitchedSignal, self).send(*args, **kwargs)
|
||||
return []
|
||||
|
||||
def send_robust(self, *args, **kwargs):
|
||||
"""
|
||||
See `django.dispatch.Signal.send_robust()`
|
||||
|
||||
This method will no-op and return an empty list if the signal has been
|
||||
disabled.
|
||||
"""
|
||||
log.debug(
|
||||
"SwitchedSignal %s's send_robust() called with args %s, kwargs %s - %s",
|
||||
self.name,
|
||||
args,
|
||||
kwargs,
|
||||
"ALLOW" if self._allow_signals else "BLOCK"
|
||||
)
|
||||
if self._allow_signals:
|
||||
return super(SwitchedSignal, self).send_robust(*args, **kwargs)
|
||||
return []
|
||||
|
||||
def __repr__(self):
|
||||
return u"SwitchedSignal('{}')".format(self.name)
|
||||
|
||||
|
||||
class SignalHandler(object):
|
||||
"""
|
||||
This class is to allow the modulestores to emit signals that can be caught
|
||||
@@ -88,23 +168,34 @@ class SignalHandler(object):
|
||||
almost no work. Its main job is to kick off the celery task that will
|
||||
do the actual work.
|
||||
"""
|
||||
pre_publish = django.dispatch.Signal(providing_args=["course_key"])
|
||||
course_published = django.dispatch.Signal(providing_args=["course_key"])
|
||||
course_deleted = django.dispatch.Signal(providing_args=["course_key"])
|
||||
library_updated = django.dispatch.Signal(providing_args=["library_key"])
|
||||
item_deleted = django.dispatch.Signal(providing_args=["usage_key", "user_id"])
|
||||
|
||||
# If you add a new signal, please don't forget to add it to the _mapping
|
||||
# as well.
|
||||
pre_publish = SwitchedSignal("pre_publish", providing_args=["course_key"])
|
||||
course_published = SwitchedSignal("course_published", providing_args=["course_key"])
|
||||
course_deleted = SwitchedSignal("course_deleted", providing_args=["course_key"])
|
||||
library_updated = SwitchedSignal("library_updated", providing_args=["library_key"])
|
||||
item_deleted = SwitchedSignal("item_deleted", providing_args=["usage_key", "user_id"])
|
||||
|
||||
_mapping = {
|
||||
"pre_publish": pre_publish,
|
||||
"course_published": course_published,
|
||||
"course_deleted": course_deleted,
|
||||
"library_updated": library_updated,
|
||||
"item_deleted": item_deleted,
|
||||
signal.name: signal
|
||||
for signal
|
||||
in [pre_publish, course_published, course_deleted, library_updated, item_deleted]
|
||||
}
|
||||
|
||||
def __init__(self, modulestore_class):
|
||||
self.modulestore_class = modulestore_class
|
||||
|
||||
@classmethod
|
||||
def all_signals(cls):
|
||||
"""Return a list with all our signals in it."""
|
||||
return cls._mapping.values()
|
||||
|
||||
@classmethod
|
||||
def signal_by_name(cls, signal_name):
|
||||
"""Given a signal name, return the appropriate signal."""
|
||||
return cls._mapping[signal_name]
|
||||
|
||||
def send(self, signal_name, **kwargs):
|
||||
"""
|
||||
Send the signal to the receivers.
|
||||
|
||||
@@ -194,7 +194,46 @@ TEST_DATA_SPLIT_MODULESTORE = functools.partial(
|
||||
)
|
||||
|
||||
|
||||
class ModuleStoreIsolationMixin(CacheIsolationMixin):
|
||||
class SignalIsolationMixin(object):
|
||||
"""
|
||||
Simple utility mixin class to toggle ModuleStore signals on and off. This
|
||||
class operates on `SwitchedSignal` objects on the modulestore's
|
||||
`SignalHandler`.
|
||||
"""
|
||||
@classmethod
|
||||
def disable_all_signals(cls):
|
||||
"""Disable all signals in the modulestore's SignalHandler."""
|
||||
for signal in SignalHandler.all_signals():
|
||||
signal.disable()
|
||||
|
||||
@classmethod
|
||||
def enable_all_signals(cls):
|
||||
"""Enable all signals in the modulestore's SignalHandler."""
|
||||
for signal in SignalHandler.all_signals():
|
||||
signal.enable()
|
||||
|
||||
@classmethod
|
||||
def enable_signals_by_name(cls, *signal_names):
|
||||
"""
|
||||
Enable specific signals in the modulestore's SignalHandler.
|
||||
|
||||
Arguments:
|
||||
signal_names (list of `str`): Names of signals to enable.
|
||||
"""
|
||||
for signal_name in signal_names:
|
||||
try:
|
||||
signal = SignalHandler.signal_by_name(signal_name)
|
||||
except KeyError:
|
||||
all_signal_names = sorted(s.name for s in SignalHandler.all_signals())
|
||||
err_msg = (
|
||||
"You tried to enable signal '{}', but I don't recognize that "
|
||||
"signal name. Did you mean one of these?: {}"
|
||||
)
|
||||
raise ValueError(err_msg.format(signal_name, all_signal_names))
|
||||
signal.enable()
|
||||
|
||||
|
||||
class ModuleStoreIsolationMixin(CacheIsolationMixin, SignalIsolationMixin):
|
||||
"""
|
||||
A mixin to be used by TestCases that want to isolate their use of the
|
||||
Modulestore.
|
||||
@@ -205,6 +244,8 @@ class ModuleStoreIsolationMixin(CacheIsolationMixin):
|
||||
|
||||
MODULESTORE = <settings for the modulestore to test>
|
||||
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def my_test(self):
|
||||
self.start_modulestore_isolation()
|
||||
self.addCleanup(self.end_modulestore_isolation)
|
||||
@@ -213,10 +254,21 @@ class ModuleStoreIsolationMixin(CacheIsolationMixin):
|
||||
...
|
||||
|
||||
"""
|
||||
|
||||
MODULESTORE = functools.partial(mixed_store_config, mkdtemp_clean(), {})
|
||||
CONTENTSTORE = functools.partial(contentstore_config)
|
||||
ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache']
|
||||
|
||||
# List of modulestore signals enabled for this test. Defaults to an empty
|
||||
# list. The list of signals available is found on the SignalHandler class,
|
||||
# in /common/lib/xmodule/xmodule/modulestore/django.py
|
||||
#
|
||||
# You must use the signal itself, and not its name. So for example:
|
||||
#
|
||||
# class MyPublishTestCase(ModuleStoreTestCase):
|
||||
# ENABLED_SIGNALS = ['course_published', 'pre_publish']
|
||||
#
|
||||
ENABLED_SIGNALS = []
|
||||
|
||||
__settings_overrides = []
|
||||
__old_modulestores = []
|
||||
__old_contentstores = []
|
||||
@@ -228,6 +280,8 @@ class ModuleStoreIsolationMixin(CacheIsolationMixin):
|
||||
:py:meth:`end_modulestore_isolation` is called, this modulestore will
|
||||
be flushed (all content will be deleted).
|
||||
"""
|
||||
cls.disable_all_signals()
|
||||
cls.enable_signals_by_name(*cls.ENABLED_SIGNALS)
|
||||
cls.start_cache_isolation()
|
||||
override = override_settings(
|
||||
MODULESTORE=cls.MODULESTORE(),
|
||||
@@ -256,6 +310,7 @@ class ModuleStoreIsolationMixin(CacheIsolationMixin):
|
||||
assert settings.MODULESTORE == cls.__old_modulestores.pop()
|
||||
assert settings.CONTENTSTORE == cls.__old_contentstores.pop()
|
||||
cls.end_cache_isolation()
|
||||
cls.enable_all_signals()
|
||||
|
||||
|
||||
class SharedModuleStoreTestCase(ModuleStoreIsolationMixin, CacheIsolationTestCase):
|
||||
@@ -384,6 +439,14 @@ class ModuleStoreTestCase(ModuleStoreIsolationMixin, TestCase):
|
||||
# Tell Django to clean out all databases, not just default
|
||||
multi_db = True
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(ModuleStoreTestCase, cls).setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(ModuleStoreTestCase, cls).tearDownClass()
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Creates a test User if `self.CREATE_USER` is True.
|
||||
@@ -400,8 +463,6 @@ class ModuleStoreTestCase(ModuleStoreIsolationMixin, TestCase):
|
||||
|
||||
super(ModuleStoreTestCase, self).setUp()
|
||||
|
||||
SignalHandler.course_published.disconnect(trigger_update_xblocks_cache_task)
|
||||
|
||||
self.store = modulestore()
|
||||
|
||||
uname = 'testuser'
|
||||
|
||||
@@ -117,6 +117,8 @@ class PreRequisiteCourseCatalog(ModuleStoreTestCase, LoginEnrollmentTestCase, Mi
|
||||
Test to simulate and verify fix for disappearing courses in
|
||||
course catalog when using pre-requisite courses
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_PREREQUISITE_COURSES': True})
|
||||
def test_course_with_prereq(self):
|
||||
"""
|
||||
@@ -160,6 +162,8 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase):
|
||||
"""
|
||||
Test for Index page course cards sorting
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
super(IndexPageCourseCardsSortingTests, self).setUp()
|
||||
self.starting_later = CourseFactory.create(
|
||||
|
||||
@@ -52,7 +52,6 @@ from student.roles import (
|
||||
)
|
||||
from student.tests.factories import AdminFactory
|
||||
|
||||
|
||||
USER_PASSWORD = 'test'
|
||||
AUTH_ATTRS = ('auth', 'auth_header_oauth2_provider')
|
||||
|
||||
@@ -183,6 +182,8 @@ class CcxListTest(CcxRestApiTest):
|
||||
"""
|
||||
Test for the CCX REST APIs
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CcxListTest, cls).setUpClass()
|
||||
@@ -892,6 +893,8 @@ class CcxDetailTest(CcxRestApiTest):
|
||||
"""
|
||||
Test for the CCX REST APIs
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up tests
|
||||
|
||||
@@ -25,9 +25,10 @@ from lms.djangoapps.ccx.tasks import send_ccx_course_published
|
||||
class TestSendCCXCoursePublished(ModuleStoreTestCase):
|
||||
"""unit tests for the send ccx course published task
|
||||
"""
|
||||
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up tests
|
||||
|
||||
@@ -75,6 +75,8 @@ class TestGetCourseChapters(CcxTestCase):
|
||||
"""
|
||||
Tests for the `get_course_chapters` util function
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up tests
|
||||
|
||||
@@ -88,6 +88,7 @@ class WebCertificateTestMixin(object):
|
||||
@attr(shard=1)
|
||||
class CertificateDownloadableStatusTests(WebCertificateTestMixin, ModuleStoreTestCase):
|
||||
"""Tests for the `certificate_downloadable_status` helper function. """
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
super(CertificateDownloadableStatusTests, self).setUp()
|
||||
@@ -457,6 +458,7 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu
|
||||
"""Tests for generating certificates for students. """
|
||||
|
||||
ERROR_REASON = "Kaboom!"
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super(GenerateUserCertificatesTest, self).setUp('certificates.api.tracker')
|
||||
|
||||
@@ -69,6 +69,7 @@ class CertificateManagementTest(ModuleStoreTestCase):
|
||||
@ddt.ddt
|
||||
class ResubmitErrorCertificatesTest(CertificateManagementTest):
|
||||
"""Tests for the resubmit_error_certificates management command. """
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
@ddt.data(CourseMode.HONOR, CourseMode.VERIFIED)
|
||||
def test_resubmit_error_certificate(self, mode):
|
||||
|
||||
@@ -105,6 +105,8 @@ class TestGetBlocksQueryCounts(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Tests query counts for the get_blocks function.
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
super(TestGetBlocksQueryCounts, self).setUp()
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ class CourseDetailTestMixin(CourseApiTestMixin):
|
||||
"""
|
||||
Common functionality for course_detail tests
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def _make_api_call(self, requesting_user, target_user, course_key):
|
||||
"""
|
||||
Call the `course_detail` api endpoint to get information on the course
|
||||
@@ -109,6 +111,7 @@ class TestGetCourseList(CourseListTestMixin, SharedModuleStoreTestCase):
|
||||
"""
|
||||
Test the behavior of the `list_courses` api function.
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -150,6 +153,7 @@ class TestGetCourseListMultipleCourses(CourseListTestMixin, ModuleStoreTestCase)
|
||||
Test the behavior of the `list_courses` api function (with tests that
|
||||
modify the courseware).
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
super(TestGetCourseListMultipleCourses, self).setUp()
|
||||
@@ -207,6 +211,8 @@ class TestGetCourseListExtras(CourseListTestMixin, ModuleStoreTestCase):
|
||||
Tests of course_list api function that require alternative configurations
|
||||
of created courses.
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestGetCourseListExtras, cls).setUpClass()
|
||||
|
||||
@@ -31,6 +31,8 @@ class TestCourseSerializer(CourseApiFactoryMixin, ModuleStoreTestCase):
|
||||
maxDiff = 5000 # long enough to show mismatched dicts, in case of error
|
||||
serializer_class = CourseSerializer
|
||||
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
super(TestCourseSerializer, self).setUp()
|
||||
self.staff_user = self.create_user('staff', is_staff=True)
|
||||
|
||||
@@ -101,6 +101,7 @@ class CourseListViewTestCaseMultipleCourses(CourseApiTestViewMixin, ModuleStoreT
|
||||
Test responses returned from CourseListView (with tests that modify the
|
||||
courseware).
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
super(CourseListViewTestCaseMultipleCourses, self).setUp()
|
||||
|
||||
@@ -38,6 +38,7 @@ class CommandsTestBase(SharedModuleStoreTestCase):
|
||||
"""
|
||||
__test__ = False
|
||||
url_name = '2012_Fall'
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
||||
@@ -47,6 +47,7 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
@ddt.ddt
|
||||
class CoursesTest(ModuleStoreTestCase):
|
||||
"""Test methods related to fetching courses."""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
@override_settings(CMS_BASE=CMS_BASE_TEST)
|
||||
def test_get_cms_course_block_link(self):
|
||||
|
||||
@@ -58,81 +58,82 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
|
||||
'entrance_exam_enabled': True,
|
||||
}
|
||||
)
|
||||
self.chapter = ItemFactory.create(
|
||||
parent=self.course,
|
||||
display_name='Overview'
|
||||
)
|
||||
self.welcome = ItemFactory.create(
|
||||
parent=self.chapter,
|
||||
display_name='Welcome'
|
||||
)
|
||||
ItemFactory.create(
|
||||
parent=self.course,
|
||||
category='chapter',
|
||||
display_name="Week 1"
|
||||
)
|
||||
self.chapter_subsection = ItemFactory.create(
|
||||
parent=self.chapter,
|
||||
category='sequential',
|
||||
display_name="Lesson 1"
|
||||
)
|
||||
chapter_vertical = ItemFactory.create(
|
||||
parent=self.chapter_subsection,
|
||||
category='vertical',
|
||||
display_name='Lesson 1 Vertical - Unit 1'
|
||||
)
|
||||
ItemFactory.create(
|
||||
parent=chapter_vertical,
|
||||
category="problem",
|
||||
display_name="Problem - Unit 1 Problem 1"
|
||||
)
|
||||
ItemFactory.create(
|
||||
parent=chapter_vertical,
|
||||
category="problem",
|
||||
display_name="Problem - Unit 1 Problem 2"
|
||||
)
|
||||
with self.store.bulk_operations(self.course.id):
|
||||
self.chapter = ItemFactory.create(
|
||||
parent=self.course,
|
||||
display_name='Overview'
|
||||
)
|
||||
self.welcome = ItemFactory.create(
|
||||
parent=self.chapter,
|
||||
display_name='Welcome'
|
||||
)
|
||||
ItemFactory.create(
|
||||
parent=self.course,
|
||||
category='chapter',
|
||||
display_name="Week 1"
|
||||
)
|
||||
self.chapter_subsection = ItemFactory.create(
|
||||
parent=self.chapter,
|
||||
category='sequential',
|
||||
display_name="Lesson 1"
|
||||
)
|
||||
chapter_vertical = ItemFactory.create(
|
||||
parent=self.chapter_subsection,
|
||||
category='vertical',
|
||||
display_name='Lesson 1 Vertical - Unit 1'
|
||||
)
|
||||
ItemFactory.create(
|
||||
parent=chapter_vertical,
|
||||
category="problem",
|
||||
display_name="Problem - Unit 1 Problem 1"
|
||||
)
|
||||
ItemFactory.create(
|
||||
parent=chapter_vertical,
|
||||
category="problem",
|
||||
display_name="Problem - Unit 1 Problem 2"
|
||||
)
|
||||
|
||||
ItemFactory.create(
|
||||
category="instructor",
|
||||
parent=self.course,
|
||||
data="Instructor Tab",
|
||||
display_name="Instructor"
|
||||
)
|
||||
self.entrance_exam = ItemFactory.create(
|
||||
parent=self.course,
|
||||
category="chapter",
|
||||
display_name="Entrance Exam Section - Chapter 1",
|
||||
is_entrance_exam=True,
|
||||
in_entrance_exam=True
|
||||
)
|
||||
self.exam_1 = ItemFactory.create(
|
||||
parent=self.entrance_exam,
|
||||
category='sequential',
|
||||
display_name="Exam Sequential - Subsection 1",
|
||||
graded=True,
|
||||
in_entrance_exam=True
|
||||
)
|
||||
subsection = ItemFactory.create(
|
||||
parent=self.exam_1,
|
||||
category='vertical',
|
||||
display_name='Exam Vertical - Unit 1'
|
||||
)
|
||||
problem_xml = MultipleChoiceResponseXMLFactory().build_xml(
|
||||
question_text='The correct answer is Choice 3',
|
||||
choices=[False, False, True, False],
|
||||
choice_names=['choice_0', 'choice_1', 'choice_2', 'choice_3']
|
||||
)
|
||||
self.problem_1 = ItemFactory.create(
|
||||
parent=subsection,
|
||||
category="problem",
|
||||
display_name="Exam Problem - Problem 1",
|
||||
data=problem_xml
|
||||
)
|
||||
self.problem_2 = ItemFactory.create(
|
||||
parent=subsection,
|
||||
category="problem",
|
||||
display_name="Exam Problem - Problem 2"
|
||||
)
|
||||
ItemFactory.create(
|
||||
category="instructor",
|
||||
parent=self.course,
|
||||
data="Instructor Tab",
|
||||
display_name="Instructor"
|
||||
)
|
||||
self.entrance_exam = ItemFactory.create(
|
||||
parent=self.course,
|
||||
category="chapter",
|
||||
display_name="Entrance Exam Section - Chapter 1",
|
||||
is_entrance_exam=True,
|
||||
in_entrance_exam=True
|
||||
)
|
||||
self.exam_1 = ItemFactory.create(
|
||||
parent=self.entrance_exam,
|
||||
category='sequential',
|
||||
display_name="Exam Sequential - Subsection 1",
|
||||
graded=True,
|
||||
in_entrance_exam=True
|
||||
)
|
||||
subsection = ItemFactory.create(
|
||||
parent=self.exam_1,
|
||||
category='vertical',
|
||||
display_name='Exam Vertical - Unit 1'
|
||||
)
|
||||
problem_xml = MultipleChoiceResponseXMLFactory().build_xml(
|
||||
question_text='The correct answer is Choice 3',
|
||||
choices=[False, False, True, False],
|
||||
choice_names=['choice_0', 'choice_1', 'choice_2', 'choice_3']
|
||||
)
|
||||
self.problem_1 = ItemFactory.create(
|
||||
parent=subsection,
|
||||
category="problem",
|
||||
display_name="Exam Problem - Problem 1",
|
||||
data=problem_xml
|
||||
)
|
||||
self.problem_2 = ItemFactory.create(
|
||||
parent=subsection,
|
||||
category="problem",
|
||||
display_name="Exam Problem - Problem 2"
|
||||
)
|
||||
|
||||
add_entrance_exam_milestone(self.course, self.entrance_exam)
|
||||
|
||||
@@ -295,7 +296,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
|
||||
"""
|
||||
# One query is for getting the list of disabled XBlocks (which is
|
||||
# then stored in the request).
|
||||
with self.assertNumQueries(2):
|
||||
with self.assertNumQueries(1):
|
||||
exam_score = get_entrance_exam_score(self.request, self.course)
|
||||
self.assertEqual(exam_score, 0)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ class TestSites(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
|
||||
STUDENT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')]
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
||||
@@ -1138,6 +1138,7 @@ class ProgressPageTests(ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
ENABLED_CACHES = ['default', 'mongo_modulestore_inheritance', 'loc_cache']
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
super(ProgressPageTests, self).setUp()
|
||||
@@ -2032,7 +2033,6 @@ class TestRenderXBlock(RenderXBlockTestMixin, ModuleStoreTestCase):
|
||||
This class overrides the get_response method, which is used by
|
||||
the tests defined in RenderXBlockTestMixin.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
reload_django_url_config()
|
||||
super(TestRenderXBlock, self).setUp()
|
||||
@@ -2060,7 +2060,6 @@ class TestRenderXBlockSelfPaced(TestRenderXBlock):
|
||||
Test rendering XBlocks for a self-paced course. Relies on the query
|
||||
count assertions in the tests defined by RenderXBlockMixin.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestRenderXBlockSelfPaced, self).setUp()
|
||||
SelfPacedConfiguration(enabled=True).save()
|
||||
|
||||
@@ -145,9 +145,9 @@ class RenderXBlockTestMixin(object):
|
||||
return response
|
||||
|
||||
@ddt.data(
|
||||
('vertical_block', ModuleStoreEnum.Type.mongo, 11),
|
||||
('vertical_block', ModuleStoreEnum.Type.mongo, 10),
|
||||
('vertical_block', ModuleStoreEnum.Type.split, 6),
|
||||
('html_block', ModuleStoreEnum.Type.mongo, 12),
|
||||
('html_block', ModuleStoreEnum.Type.mongo, 11),
|
||||
('html_block', ModuleStoreEnum.Type.split, 6),
|
||||
)
|
||||
@ddt.unpack
|
||||
|
||||
@@ -356,6 +356,7 @@ class ViewsQueryCountTestCase(
|
||||
|
||||
CREATE_USER = False
|
||||
ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache']
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
def setUp(self):
|
||||
|
||||
@@ -210,6 +210,8 @@ class CachedDiscussionIdMapTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests that using the cache of discussion id mappings has the same behavior as searching through the course.
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
super(CachedDiscussionIdMapTestCase, self).setUp()
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ class GradesAccessIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreT
|
||||
"""
|
||||
Tests integration between grading and block access.
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(GradesAccessIntegrationTest, cls).setUpClass()
|
||||
|
||||
@@ -26,6 +26,8 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
|
||||
Tests integration between the eventing in various layers
|
||||
of the grading infrastructure.
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
@classmethod
|
||||
def reset_course(cls):
|
||||
"""
|
||||
|
||||
@@ -37,6 +37,8 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Ensures that the recalculate subsection grade task functions as expected when run.
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published', 'pre_publish']
|
||||
|
||||
def setUp(self):
|
||||
super(RecalculateSubsectionGradeTest, self).setUp()
|
||||
self.user = UserFactory()
|
||||
|
||||
@@ -29,6 +29,8 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
|
||||
TRANSFORMER_CLASS_TO_TEST = GradesTransformer
|
||||
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
problem_metadata = {
|
||||
u'graded': True,
|
||||
u'weight': 1,
|
||||
|
||||
@@ -412,6 +412,6 @@ class TestInstructorDashboardPerformance(ModuleStoreTestCase, LoginEnrollmentTes
|
||||
|
||||
# check MongoDB calls count
|
||||
url = reverse('spoc_gradebook', kwargs={'course_id': self.course.id})
|
||||
with check_mongo_calls(7):
|
||||
with check_mongo_calls(9):
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -87,6 +87,7 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
|
||||
NEXT_WEEK = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=7)
|
||||
LAST_WEEK = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=7)
|
||||
ADVERTISED_START = "Spring 2016"
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
@patch.dict(settings.FEATURES, {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
def setUp(self, *args, **kwargs):
|
||||
@@ -468,6 +469,8 @@ class TestCourseEnrollmentSerializer(MobileAPITestCase, MilestonesTestCaseMixin)
|
||||
"""
|
||||
Test the course enrollment serializer
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
super(TestCourseEnrollmentSerializer, self).setUp()
|
||||
self.login_and_enroll()
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
from django.core.cache import cache
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from xmodule.modulestore.tests.factories import (check_mongo_calls, CourseFactory)
|
||||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
||||
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
|
||||
from student.models import anonymous_id_for_user
|
||||
from student.models import UserProfile
|
||||
from student.roles import (CourseInstructorRole, CourseStaffRole, GlobalStaff,
|
||||
OrgInstructorRole, OrgStaffRole)
|
||||
from student.tests.factories import UserFactory, UserProfileFactory
|
||||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
||||
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import (check_mongo_calls, CourseFactory)
|
||||
|
||||
|
||||
# Will also run default tests for IDTokens and UserInfo
|
||||
@@ -19,6 +19,7 @@ from edx_oauth2_provider.tests import IDTokenTestCase, UserInfoTestCase
|
||||
|
||||
class BaseTestMixin(ModuleStoreTestCase):
|
||||
profile = None
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
super(BaseTestMixin, self).setUp()
|
||||
|
||||
@@ -43,7 +43,6 @@ class BookmarksAPITests(BookmarkApiEventTestMixin, BookmarksTestsBase):
|
||||
"""
|
||||
These tests cover the parts of the API methods.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(BookmarksAPITests, self).setUp()
|
||||
|
||||
@@ -123,7 +122,7 @@ class BookmarksAPITests(BookmarkApiEventTestMixin, BookmarksTestsBase):
|
||||
"""
|
||||
self.assertEqual(len(api.get_bookmarks(user=self.user, course_key=self.course.id)), 2)
|
||||
|
||||
with self.assertNumQueries(10):
|
||||
with self.assertNumQueries(9):
|
||||
bookmark_data = api.create_bookmark(user=self.user, usage_key=self.vertical_2.location)
|
||||
|
||||
self.assert_bookmark_event_emitted(
|
||||
@@ -144,7 +143,7 @@ class BookmarksAPITests(BookmarkApiEventTestMixin, BookmarksTestsBase):
|
||||
"""
|
||||
self.assertEqual(len(api.get_bookmarks(user=self.user, course_key=self.course.id)), 2)
|
||||
|
||||
with self.assertNumQueries(10):
|
||||
with self.assertNumQueries(9):
|
||||
bookmark_data = api.create_bookmark(user=self.user, usage_key=self.vertical_2.location)
|
||||
|
||||
self.assert_bookmark_event_emitted(
|
||||
|
||||
@@ -253,10 +253,10 @@ class BookmarkModelTests(BookmarksTestsBase):
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.mongo, 'course', [], 3),
|
||||
(ModuleStoreEnum.Type.mongo, 'chapter_1', [], 4),
|
||||
(ModuleStoreEnum.Type.mongo, 'sequential_1', ['chapter_1'], 6),
|
||||
(ModuleStoreEnum.Type.mongo, 'vertical_1', ['chapter_1', 'sequential_1'], 8),
|
||||
(ModuleStoreEnum.Type.mongo, 'html_1', ['chapter_1', 'sequential_2', 'vertical_2'], 10),
|
||||
(ModuleStoreEnum.Type.mongo, 'chapter_1', [], 3),
|
||||
(ModuleStoreEnum.Type.mongo, 'sequential_1', ['chapter_1'], 4),
|
||||
(ModuleStoreEnum.Type.mongo, 'vertical_1', ['chapter_1', 'sequential_1'], 6),
|
||||
(ModuleStoreEnum.Type.mongo, 'html_1', ['chapter_1', 'sequential_2', 'vertical_2'], 7),
|
||||
(ModuleStoreEnum.Type.split, 'course', [], 3),
|
||||
(ModuleStoreEnum.Type.split, 'chapter_1', [], 2),
|
||||
(ModuleStoreEnum.Type.split, 'sequential_1', ['chapter_1'], 2),
|
||||
|
||||
@@ -68,7 +68,7 @@ class BookmarksServiceTests(BookmarksTestsBase):
|
||||
self.bookmark_service.set_bookmarked(usage_key=UsageKey.from_string("i4x://ed/ed/ed/interactive"))
|
||||
)
|
||||
|
||||
with self.assertNumQueries(10):
|
||||
with self.assertNumQueries(9):
|
||||
self.assertTrue(self.bookmark_service.set_bookmarked(usage_key=self.vertical_2.location))
|
||||
|
||||
def test_unset_bookmarked(self):
|
||||
|
||||
@@ -20,6 +20,7 @@ class XBlockCacheTaskTests(BookmarksTestsBase):
|
||||
"""
|
||||
Test the XBlockCache model.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(XBlockCacheTaskTests, self).setUp()
|
||||
|
||||
@@ -157,12 +158,6 @@ class XBlockCacheTaskTests(BookmarksTestsBase):
|
||||
"""
|
||||
course = getattr(self, course_attr)
|
||||
|
||||
if settings.ROOT_URLCONF == 'lms.urls':
|
||||
# When the tests run under LMS, there is a certificates course_published
|
||||
# signal handler that runs and causes the number of queries to be one more
|
||||
# (due to the check for disabled_xblocks in django.py).
|
||||
expected_sql_queries += 1
|
||||
|
||||
with self.assertNumQueries(expected_sql_queries):
|
||||
_update_xblocks_cache(course.id)
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ class CourseBlocksSignalTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the Course Blocks signal
|
||||
"""
|
||||
ENABLED_SIGNALS = ['course_deleted', 'course_published']
|
||||
|
||||
def setUp(self):
|
||||
super(CourseBlocksSignalTest, self).setUp()
|
||||
self.course = CourseFactory.create()
|
||||
|
||||
@@ -52,6 +52,8 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
|
||||
|
||||
COURSE_OVERVIEW_TABS = {'courseware', 'info', 'textbooks', 'discussion', 'wiki', 'progress'}
|
||||
|
||||
ENABLED_SIGNALS = ['course_deleted', 'course_published']
|
||||
|
||||
def check_course_overview_against_course(self, course):
|
||||
"""
|
||||
Compares a CourseOverview object against its corresponding
|
||||
|
||||
@@ -18,6 +18,8 @@ class CourseStructureApiTests(ModuleStoreTestCase):
|
||||
|
||||
ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache']
|
||||
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Test setup
|
||||
|
||||
@@ -263,8 +263,8 @@ class TestCohorts(ModuleStoreTestCase):
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
(True, 3),
|
||||
(False, 7),
|
||||
(True, 2),
|
||||
(False, 6),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_get_cohort_sql_queries(self, use_cached, num_sql_queries):
|
||||
|
||||
@@ -240,6 +240,8 @@ class WriteOnPublishTest(ModuleStoreTestCase):
|
||||
"""
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
ENABLED_SIGNALS = ['course_published', 'pre_publish']
|
||||
|
||||
@patch.dict(settings.FEATURES, {"ENABLE_COURSEWARE_INDEX": False})
|
||||
def setUp(self):
|
||||
super(WriteOnPublishTest, self).setUp()
|
||||
|
||||
Reference in New Issue
Block a user