refactor: run modulestore tests in common/lib/... using Django
Does 3 things: (1) Use django for modulestore tests (2) Use normal LMS settings for modulestore tests instead of openedx/tests/settings.py (3) Simplify some TestCase subclasses by converting them to use ModuleStoreTestCase Details and rationale: (1) Currently parts of the modulestore test suite are designed to run "without django", although there is still a lot of django functionality imported at times, and many of the tests do in fact use django. But for the upcoming PR #27565 (moving split's course indexes from MongoDB to MySQL), we will need to always have Django enabled. So this commit paves the way for that change. (2) The previous tests that did use Django used a special settings file, openedx/tests/settings.py which made some debugging confusing because those tests had quite different django settings than other tests. This change deletes that file and runs the tests using the LMS test settings. (3) The test suite also contains many different ways of initializing and testing a modulestore, with significant differences in their configuration, and also a lot of repetition. I find this makes understanding, debugging and writing tests more difficult. So this commit also reduces the number of different "test case using modulestore" base classes: * Simplifies MixedWithOptionsTestCase and MixedSplitTestCase by making them simple subclasses of ModuleStoreTestCase. * Removes PureModulestoreTestCase.
This commit is contained in:
committed by
Braden MacDonald
parent
daac2f7585
commit
da09bcadc5
@@ -1,14 +1,11 @@
|
||||
"""
|
||||
Testing indexing of the courseware as it is changed
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from unittest import skip
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
|
||||
import ddt
|
||||
import pytest
|
||||
@@ -33,25 +30,14 @@ from openedx.core.djangoapps.models.course_details import CourseDetails
|
||||
from xmodule.library_tools import normalize_key_for_search
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import SignalHandler, modulestore
|
||||
from xmodule.modulestore.edit_info import EditInfoMixin
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.modulestore.mixed import MixedModuleStore
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase,
|
||||
TEST_DATA_MONGO_MODULESTORE,
|
||||
TEST_DATA_SPLIT_MODULESTORE,
|
||||
SharedModuleStoreTestCase
|
||||
SharedModuleStoreTestCase,
|
||||
)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, LibraryFactory
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
from xmodule.modulestore.tests.utils import (
|
||||
LocationMixin,
|
||||
MixedSplitTestCase,
|
||||
MongoContentstoreBuilder,
|
||||
create_modulestore_instance
|
||||
)
|
||||
from xmodule.partitions.partitions import UserPartition
|
||||
from xmodule.tests import DATA_DIR
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
COURSE_CHILD_STRUCTURE = {
|
||||
"course": "chapter",
|
||||
@@ -93,46 +79,9 @@ def create_large_course(store, load_factor):
|
||||
return course, child_count
|
||||
|
||||
|
||||
class MixedWithOptionsTestCase(MixedSplitTestCase):
|
||||
class MixedWithOptionsTestCase(ModuleStoreTestCase):
|
||||
""" Base class for test cases within this file """
|
||||
HOST = MONGO_HOST
|
||||
PORT = MONGO_PORT_NUM
|
||||
DATABASE = 'test_mongo_%s' % uuid4().hex[:5]
|
||||
COLLECTION = 'modulestore'
|
||||
ASSET_COLLECTION = 'assetstore'
|
||||
DEFAULT_CLASS = 'xmodule.hidden_module.HiddenDescriptor'
|
||||
RENDER_TEMPLATE = lambda t_n, d, ctx=None, nsp='main': ''
|
||||
modulestore_options = {
|
||||
'default_class': DEFAULT_CLASS,
|
||||
'fs_root': DATA_DIR,
|
||||
'render_template': RENDER_TEMPLATE,
|
||||
'xblock_mixins': (EditInfoMixin, InheritanceMixin, LocationMixin, XModuleMixin),
|
||||
}
|
||||
DOC_STORE_CONFIG = {
|
||||
'host': HOST,
|
||||
'port': PORT,
|
||||
'db': DATABASE,
|
||||
'collection': COLLECTION,
|
||||
'asset_collection': ASSET_COLLECTION,
|
||||
}
|
||||
OPTIONS = {
|
||||
'stores': [
|
||||
{
|
||||
'NAME': 'draft',
|
||||
'ENGINE': 'xmodule.modulestore.mongo.draft.DraftModuleStore',
|
||||
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
|
||||
'OPTIONS': modulestore_options
|
||||
},
|
||||
{
|
||||
'NAME': 'split',
|
||||
'ENGINE': 'xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore',
|
||||
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
|
||||
'OPTIONS': modulestore_options
|
||||
},
|
||||
],
|
||||
'xblock_mixins': modulestore_options['xblock_mixins'],
|
||||
}
|
||||
|
||||
CREATE_USER = False
|
||||
INDEX_NAME = None
|
||||
|
||||
def setup_course_base(self, store):
|
||||
@@ -155,18 +104,10 @@ class MixedWithOptionsTestCase(MixedSplitTestCase):
|
||||
|
||||
def _perform_test_using_store(self, store_type, test_to_perform):
|
||||
""" Helper method to run a test function that uses a specific store """
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
store = MixedModuleStore(
|
||||
contentstore=contentstore,
|
||||
create_modulestore_instance=create_modulestore_instance,
|
||||
mappings={},
|
||||
**self.OPTIONS
|
||||
)
|
||||
self.addCleanup(store.close_all_connections)
|
||||
|
||||
with store.default_store(store_type):
|
||||
self.setup_course_base(store)
|
||||
test_to_perform(store)
|
||||
store = modulestore()
|
||||
with store.default_store(store_type):
|
||||
self.setup_course_base(store)
|
||||
test_to_perform(store)
|
||||
|
||||
def publish_item(self, store, item_location):
|
||||
""" publish the item at the given location """
|
||||
@@ -190,6 +131,7 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase):
|
||||
""" Tests the operation of the CoursewareSearchIndexer """
|
||||
|
||||
WORKS_WITH_STORES = (ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
ENABLED_SIGNALS = ['course_deleted']
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -264,11 +206,8 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase):
|
||||
|
||||
def _test_indexing_course(self, store):
|
||||
""" indexing course tests """
|
||||
response = self.search()
|
||||
self.assertEqual(response["total"], 0)
|
||||
|
||||
# Only published modules should be in the index
|
||||
added_to_index = self.reindex_course(store)
|
||||
added_to_index = self.reindex_course(store) # This reindex may not be necessary (it may already be indexed)
|
||||
self.assertEqual(added_to_index, 3)
|
||||
response = self.search()
|
||||
self.assertEqual(response["total"], 3)
|
||||
@@ -311,17 +250,15 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase):
|
||||
Test that course will also be delete from search_index after course deletion.
|
||||
"""
|
||||
self.searcher = SearchEngine.get_search_engine(CourseAboutSearchIndexer.INDEX_NAME)
|
||||
response = self.search()
|
||||
self.assertEqual(response["total"], 0)
|
||||
|
||||
# index the course in search_index
|
||||
# index the course in search_index (it may already be indexed)
|
||||
self.reindex_course(store)
|
||||
response = self.search()
|
||||
self.assertEqual(response["total"], 1)
|
||||
|
||||
# delete the course and look course in search_index
|
||||
modulestore().delete_course(self.course.id, ModuleStoreEnum.UserID.test)
|
||||
self.assertIsNone(modulestore().get_course(self.course.id))
|
||||
store.delete_course(self.course.id, ModuleStoreEnum.UserID.test)
|
||||
self.assertIsNone(store.get_course(self.course.id))
|
||||
# Now, because of contentstore.signals.handlers.listen_for_course_delete, the index should already be updated:
|
||||
response = self.search()
|
||||
self.assertEqual(response["total"], 0)
|
||||
|
||||
@@ -776,6 +713,7 @@ class TestTaskExecution(SharedModuleStoreTestCase):
|
||||
self.assertFalse(mock_index.called)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@ddt.ddt
|
||||
class TestLibrarySearchIndexer(MixedWithOptionsTestCase):
|
||||
""" Tests the operation of the CoursewareSearchIndexer """
|
||||
@@ -929,7 +867,7 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase):
|
||||
"""
|
||||
Tests indexing of content groups on course modules using mongo modulestore.
|
||||
"""
|
||||
|
||||
CREATE_USER = True
|
||||
MODULESTORE = TEST_DATA_MONGO_MODULESTORE
|
||||
INDEX_NAME = CoursewareSearchIndexer.INDEX_NAME
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[pytest]
|
||||
DJANGO_SETTINGS_MODULE = openedx.tests.settings
|
||||
# Use the LMS settings for these tests; should work with CMS just as well though:
|
||||
DJANGO_SETTINGS_MODULE = lms.envs.test
|
||||
addopts = --nomigrations --reuse-db --durations=20 --json-report --json-report-omit keywords streams collectors log traceback tests --json-report-file=none
|
||||
# Enable default handling for all warnings, including those that are ignored by default;
|
||||
# but hide rate-limit warnings (because we deliberately don't throttle test user logins)
|
||||
|
||||
@@ -265,7 +265,7 @@ def create_modulestore_instance(
|
||||
_options['create_modulestore_instance'] = create_modulestore_instance
|
||||
|
||||
if issubclass(class_, BranchSettingMixin):
|
||||
_options['branch_setting_func'] = _get_modulestore_branch_setting
|
||||
_options.setdefault('branch_setting_func', _get_modulestore_branch_setting)
|
||||
|
||||
if HAS_USER_SERVICE and not user_service:
|
||||
xb_user_service = DjangoXBlockUserService(get_current_user())
|
||||
|
||||
@@ -12,6 +12,7 @@ import zlib
|
||||
from contextlib import contextmanager
|
||||
from time import time
|
||||
|
||||
from django.core.cache import caches, InvalidCacheBackendError
|
||||
import pymongo
|
||||
import pytz
|
||||
from mongodb_proxy import autoretry_read
|
||||
@@ -22,11 +23,6 @@ from xmodule.modulestore import BlockData
|
||||
from xmodule.modulestore.split_mongo import BlockKey
|
||||
from xmodule.mongo_utils import connect_to_mongodb, create_collection_index
|
||||
|
||||
try:
|
||||
from django.core.cache import caches, InvalidCacheBackendError
|
||||
DJANGO_AVAILABLE = True
|
||||
except ImportError:
|
||||
DJANGO_AVAILABLE = False
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -198,11 +194,10 @@ class CourseStructureCache:
|
||||
"""
|
||||
def __init__(self):
|
||||
self.cache = None
|
||||
if DJANGO_AVAILABLE:
|
||||
try:
|
||||
self.cache = get_cache('course_structure_cache')
|
||||
except InvalidCacheBackendError:
|
||||
pass
|
||||
try:
|
||||
self.cache = get_cache('course_structure_cache')
|
||||
except InvalidCacheBackendError:
|
||||
pass
|
||||
|
||||
def get(self, key, course_context=None):
|
||||
"""Pull the compressed, pickled struct data from cache and deserialize."""
|
||||
|
||||
@@ -46,7 +46,7 @@ class StoreConstructors:
|
||||
draft, split = list(range(2))
|
||||
|
||||
|
||||
def mixed_store_config(data_dir, mappings, store_order=None):
|
||||
def mixed_store_config(data_dir, mappings, store_order=None, modulestore_options=None):
|
||||
"""
|
||||
Return a `MixedModuleStore` configuration, which provides
|
||||
access to both Mongo-backed courses.
|
||||
@@ -71,9 +71,17 @@ def mixed_store_config(data_dir, mappings, store_order=None):
|
||||
if store_order is None:
|
||||
store_order = [StoreConstructors.draft, StoreConstructors.split]
|
||||
|
||||
options = {
|
||||
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
|
||||
'fs_root': data_dir,
|
||||
'render_template': 'common.djangoapps.edxmako.shortcuts.render_to_string',
|
||||
}
|
||||
if modulestore_options:
|
||||
options.update(modulestore_options)
|
||||
|
||||
store_constructors = {
|
||||
StoreConstructors.split: split_mongo_store_config(data_dir)['default'],
|
||||
StoreConstructors.draft: draft_mongo_store_config(data_dir)['default'],
|
||||
StoreConstructors.split: split_mongo_store_config(options)['default'],
|
||||
StoreConstructors.draft: draft_mongo_store_config(options)['default'],
|
||||
}
|
||||
|
||||
store = {
|
||||
@@ -88,17 +96,10 @@ def mixed_store_config(data_dir, mappings, store_order=None):
|
||||
return store
|
||||
|
||||
|
||||
def draft_mongo_store_config(data_dir):
|
||||
def draft_mongo_store_config(modulestore_options):
|
||||
"""
|
||||
Defines default module store using DraftMongoModuleStore.
|
||||
"""
|
||||
|
||||
modulestore_options = {
|
||||
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
|
||||
'fs_root': data_dir,
|
||||
'render_template': 'common.djangoapps.edxmako.shortcuts.render_to_string'
|
||||
}
|
||||
|
||||
store = {
|
||||
'default': {
|
||||
'NAME': 'draft',
|
||||
@@ -116,16 +117,10 @@ def draft_mongo_store_config(data_dir):
|
||||
return store
|
||||
|
||||
|
||||
def split_mongo_store_config(data_dir):
|
||||
def split_mongo_store_config(modulestore_options):
|
||||
"""
|
||||
Defines split module store.
|
||||
"""
|
||||
modulestore_options = {
|
||||
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
|
||||
'fs_root': data_dir,
|
||||
'render_template': 'common.djangoapps.edxmako.shortcuts.render_to_string',
|
||||
}
|
||||
|
||||
store = {
|
||||
'default': {
|
||||
'NAME': 'draft',
|
||||
@@ -207,6 +202,16 @@ TEST_DATA_SPLIT_MODULESTORE = functools.partial(
|
||||
store_order=[StoreConstructors.split, StoreConstructors.draft]
|
||||
)
|
||||
|
||||
# Tests that use mixed modulestore and split, but don't load/use draft modulestore.
|
||||
# This also enables "draft preferred" mode, like Studio.
|
||||
TEST_DATA_ONLY_SPLIT_MODULESTORE_DRAFT_PREFERRED = functools.partial(
|
||||
mixed_store_config,
|
||||
mkdtemp_clean(),
|
||||
{},
|
||||
store_order=[StoreConstructors.split],
|
||||
modulestore_options={'branch_setting_func': lambda: ModuleStoreEnum.Branch.draft_preferred},
|
||||
)
|
||||
|
||||
|
||||
class SignalIsolationMixin:
|
||||
"""
|
||||
|
||||
@@ -51,13 +51,12 @@ from xmodule.modulestore.tests.factories import check_exact_number_of_calls, che
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
from xmodule.modulestore.tests.test_asides import AsideTestType
|
||||
from xmodule.modulestore.tests.utils import (
|
||||
LocationMixin,
|
||||
MongoContentstoreBuilder,
|
||||
create_modulestore_instance,
|
||||
mock_tab_from_json
|
||||
)
|
||||
from xmodule.modulestore.xml_exporter import export_course_to_xml
|
||||
from xmodule.modulestore.xml_importer import import_course_from_xml
|
||||
from xmodule.modulestore.xml_importer import LocationMixin, import_course_from_xml
|
||||
from xmodule.tests import DATA_DIR, CourseComparisonTest
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@ from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.modulestore.mongo import MongoKeyValueStore
|
||||
from xmodule.modulestore.mongo.base import as_draft
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
from xmodule.modulestore.tests.utils import LocationMixin, mock_tab_from_json
|
||||
from xmodule.modulestore.tests.utils import mock_tab_from_json
|
||||
from xmodule.modulestore.xml_exporter import export_course_to_xml
|
||||
from xmodule.modulestore.xml_importer import import_course_from_xml, perform_xlint
|
||||
from xmodule.modulestore.xml_importer import LocationMixin, import_course_from_xml, perform_xlint
|
||||
from xmodule.tests import DATA_DIR
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
|
||||
@@ -19,7 +19,11 @@ from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.utils import SPLIT_MODULESTORE_SETUP, MongoModulestoreBuilder, PureModulestoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase,
|
||||
TEST_DATA_MONGO_MODULESTORE,
|
||||
TEST_DATA_SPLIT_MODULESTORE,
|
||||
)
|
||||
|
||||
DETACHED_BLOCK_TYPES = dict(XBlock.load_tagged_classes('detached'))
|
||||
|
||||
@@ -40,7 +44,7 @@ class AsideTest(XBlockAside):
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class DirectOnlyCategorySemantics(PureModulestoreTestCase):
|
||||
class DirectOnlyCategorySemantics(ModuleStoreTestCase):
|
||||
"""
|
||||
Verify the behavior of Direct Only items
|
||||
blocks intended to store snippets of course content.
|
||||
@@ -377,8 +381,8 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase):
|
||||
fields={'data': child_data},
|
||||
)
|
||||
|
||||
if child_published:
|
||||
self.store.publish(child_usage_key, ModuleStoreEnum.UserID.test)
|
||||
if child_published:
|
||||
self.store.publish(child_usage_key, ModuleStoreEnum.UserID.test)
|
||||
|
||||
self.assertCoursePointsToBlock(block_usage_key)
|
||||
|
||||
@@ -417,7 +421,7 @@ class TestSplitDirectOnlyCategorySemantics(DirectOnlyCategorySemantics):
|
||||
"""
|
||||
Verify DIRECT_ONLY_CATEGORY semantics against the SplitMongoModulestore.
|
||||
"""
|
||||
MODULESTORE = SPLIT_MODULESTORE_SETUP
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
__test__ = True
|
||||
|
||||
@ddt.data(*TESTABLE_BLOCK_TYPES)
|
||||
@@ -451,5 +455,5 @@ class TestMongoDirectOnlyCategorySemantics(DirectOnlyCategorySemantics):
|
||||
"""
|
||||
Verify DIRECT_ONLY_CATEGORY semantics against the MongoModulestore
|
||||
"""
|
||||
MODULESTORE = MongoModulestoreBuilder()
|
||||
MODULESTORE = TEST_DATA_MONGO_MODULESTORE
|
||||
__test__ = True
|
||||
|
||||
@@ -19,6 +19,7 @@ from opaque_keys.edx.locator import BlockUsageLocator, CourseKey, CourseLocator,
|
||||
from path import Path as path
|
||||
from xblock.fields import Reference, ReferenceList, ReferenceValueDict
|
||||
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationMixin
|
||||
from openedx.core.lib import tempdir
|
||||
from openedx.core.lib.tests import attr
|
||||
from xmodule.course_module import CourseBlock
|
||||
@@ -833,19 +834,15 @@ class SplitModuleCourseTests(SplitModuleTest):
|
||||
assert root_block_key.block_id == 'course'
|
||||
|
||||
|
||||
class TestCourseStructureCache(SplitModuleTest):
|
||||
class TestCourseStructureCache(CacheIsolationMixin, SplitModuleTest):
|
||||
"""Tests for the CourseStructureCache"""
|
||||
|
||||
# CacheIsolationMixin will reset the cache between test cases
|
||||
|
||||
# We'll use the "default" cache as a valid cache, and the "course_structure_cache" as a dummy cache
|
||||
ENABLED_CACHES = ["default"]
|
||||
|
||||
def setUp(self):
|
||||
# use the default cache, since the `course_structure_cache`
|
||||
# is a dummy cache during testing
|
||||
self.cache = caches['default']
|
||||
|
||||
# make sure we clear the cache before every test...
|
||||
self.cache.clear()
|
||||
# ... and after
|
||||
self.addCleanup(self.cache.clear)
|
||||
|
||||
# make a new course:
|
||||
self.user = random.getrandbits(32)
|
||||
self.new_course = modulestore().create_course(
|
||||
@@ -858,7 +855,8 @@ class TestCourseStructureCache(SplitModuleTest):
|
||||
def test_course_structure_cache(self, mock_get_cache):
|
||||
# force get_cache to return the default cache so we can test
|
||||
# its caching behavior
|
||||
mock_get_cache.return_value = self.cache
|
||||
enabled_cache = caches['default']
|
||||
mock_get_cache.return_value = enabled_cache
|
||||
|
||||
with check_mongo_calls(1):
|
||||
not_cached_structure = self._get_structure(self.new_course)
|
||||
@@ -872,7 +870,7 @@ class TestCourseStructureCache(SplitModuleTest):
|
||||
|
||||
# If data is corrupted, get it from mongo again.
|
||||
cache_key = self.new_course.id.version_guid
|
||||
self.cache.set(cache_key, b"bad_data")
|
||||
enabled_cache.set(cache_key, b"bad_data")
|
||||
with check_mongo_calls(1):
|
||||
not_corrupt_structure = self._get_structure(self.new_course)
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
"""
|
||||
Helper classes and methods for running modulestore tests without Django.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from importlib import import_module
|
||||
from shutil import rmtree
|
||||
from tempfile import mkdtemp
|
||||
from unittest import TestCase
|
||||
from uuid import uuid4
|
||||
|
||||
from contextlib2 import ExitStack
|
||||
@@ -16,16 +13,15 @@ from path import Path as path
|
||||
|
||||
from xmodule.contentstore.mongo import MongoContentStore
|
||||
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
|
||||
from xmodule.modulestore.edit_info import EditInfoMixin
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.modulestore.mixed import MixedModuleStore
|
||||
from xmodule.modulestore.mongo.base import ModuleStoreEnum
|
||||
from xmodule.modulestore.mongo.draft import DraftModuleStore
|
||||
from xmodule.modulestore.split_mongo.split_draft import DraftVersioningModuleStore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, TEST_DATA_ONLY_SPLIT_MODULESTORE_DRAFT_PREFERRED
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
from xmodule.modulestore.xml_importer import LocationMixin
|
||||
from xmodule.tests import DATA_DIR
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
@@ -97,35 +93,16 @@ def remove_temp_files_from_list(file_list, dir): # lint-amnesty, pylint: disabl
|
||||
os.remove(file_path)
|
||||
|
||||
|
||||
class MixedSplitTestCase(TestCase):
|
||||
class MixedSplitTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
Stripped-down version of ModuleStoreTestCase that can be used without Django
|
||||
(i.e. for testing in common/lib/ ). Sets up MixedModuleStore and Split.
|
||||
A minimal version of ModuleStoreTestCase for testing in common/lib/ that sets up MixedModuleStore and Split (only).
|
||||
|
||||
It also enables "draft preferred" mode, like Studio uses.
|
||||
|
||||
Draft/old mongo modulestore is not initialized.
|
||||
"""
|
||||
RENDER_TEMPLATE = lambda t_n, d, ctx=None, nsp='main': '{}: {}, {}'.format(t_n, repr(d), repr(ctx))
|
||||
modulestore_options = {
|
||||
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
|
||||
'fs_root': DATA_DIR,
|
||||
'render_template': RENDER_TEMPLATE,
|
||||
'xblock_mixins': (EditInfoMixin, InheritanceMixin, LocationMixin, XModuleMixin),
|
||||
}
|
||||
DOC_STORE_CONFIG = {
|
||||
'host': MONGO_HOST,
|
||||
'port': MONGO_PORT_NUM,
|
||||
'db': f'test_mongo_libs_{os.getpid()}',
|
||||
'collection': 'modulestore',
|
||||
'asset_collection': 'assetstore',
|
||||
}
|
||||
MIXED_OPTIONS = {
|
||||
'stores': [
|
||||
{
|
||||
'NAME': 'split',
|
||||
'ENGINE': 'xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore',
|
||||
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
|
||||
'OPTIONS': modulestore_options
|
||||
},
|
||||
]
|
||||
}
|
||||
CREATE_USER = False
|
||||
MODULESTORE = TEST_DATA_ONLY_SPLIT_MODULESTORE_DRAFT_PREFERRED
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
@@ -134,15 +111,6 @@ class MixedSplitTestCase(TestCase):
|
||||
super().setUp()
|
||||
self.user_id = ModuleStoreEnum.UserID.test
|
||||
|
||||
self.store = MixedModuleStore(
|
||||
None,
|
||||
create_modulestore_instance=create_modulestore_instance,
|
||||
mappings={},
|
||||
**self.MIXED_OPTIONS
|
||||
)
|
||||
self.addCleanup(self.store.close_all_connections)
|
||||
self.addCleanup(self.store._drop_database) # pylint: disable=protected-access
|
||||
|
||||
def make_block(self, category, parent_block, **kwargs):
|
||||
"""
|
||||
Create a block of type `category` as a child of `parent_block`, in any
|
||||
@@ -505,19 +473,3 @@ DOT_FILES_DICT = {
|
||||
TILDA_FILES_DICT = {
|
||||
"example.txt~": "RED"
|
||||
}
|
||||
|
||||
|
||||
class PureModulestoreTestCase(TestCase):
|
||||
"""
|
||||
A TestCase designed to make testing Modulestore implementations without using Django
|
||||
easier.
|
||||
"""
|
||||
|
||||
MODULESTORE = None
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
builder = self.MODULESTORE.build()
|
||||
self.assets, self.store = builder.__enter__()
|
||||
self.addCleanup(builder.__exit__, None, None, None)
|
||||
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.conf import settings
|
||||
from fs.memoryfs import MemoryFS
|
||||
from lxml import etree
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
@@ -239,6 +240,7 @@ class ConditionalBlockXmlTest(unittest.TestCase):
|
||||
return courses[0]
|
||||
|
||||
@patch('xmodule.x_module.descriptor_global_local_resource_url')
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_EDXNOTES': False})
|
||||
def test_conditional_module(self, _):
|
||||
"""Make sure that conditional module works"""
|
||||
|
||||
|
||||
@@ -432,6 +432,13 @@ class ProctoringProviderTestCase(unittest.TestCase):
|
||||
# since there are no validation errors or missing data
|
||||
assert self.proctoring_provider.from_json(default_provider) == default_provider
|
||||
|
||||
@override_settings(
|
||||
PROCTORING_BACKENDS={
|
||||
'DEFAULT': 'mock',
|
||||
'mock': {},
|
||||
'mock_proctoring_without_rules': {}
|
||||
}
|
||||
)
|
||||
def test_from_json_with_invalid_provider(self):
|
||||
"""
|
||||
Test that an invalid provider (i.e. not one configured at the platform level)
|
||||
@@ -451,7 +458,8 @@ class ProctoringProviderTestCase(unittest.TestCase):
|
||||
Test that a value with no provider will inherit the default provider
|
||||
from the platform defaults.
|
||||
"""
|
||||
default_provider = 'mock'
|
||||
default_provider = settings.PROCTORING_BACKENDS.get('DEFAULT')
|
||||
assert default_provider is not None
|
||||
|
||||
assert self.proctoring_provider.from_json(None) == default_provider
|
||||
|
||||
|
||||
@@ -59,15 +59,19 @@ class TestSettingsService(unittest.TestCase):
|
||||
with pytest.raises(ValueError):
|
||||
self.settings_service.get_settings_bucket(None)
|
||||
|
||||
@override_settings()
|
||||
def test_get_return_default_if_xblock_settings_is_missing(self):
|
||||
""" Test that returns default (or None if default not set) if XBLOCK_SETTINGS is not set """
|
||||
assert not hasattr(settings, 'XBLOCK_SETTINGS')
|
||||
# Per django docs, using override_settings() plus 'del' is how to test the absence of a setting:
|
||||
del settings.XBLOCK_SETTINGS
|
||||
# precondition check
|
||||
assert self.settings_service.get_settings_bucket(self.xblock_mock, 'zzz') == 'zzz'
|
||||
|
||||
@override_settings()
|
||||
def test_get_return_empty_dictionary_if_xblock_settings_and_default_is_missing(self):
|
||||
""" Test that returns default (or None if default not set) if XBLOCK_SETTINGS is not set """
|
||||
assert not hasattr(settings, 'XBLOCK_SETTINGS')
|
||||
# Per django docs, using override_settings() plus 'del' is how to test the absence of a setting:
|
||||
del settings.XBLOCK_SETTINGS
|
||||
# precondition check
|
||||
assert self.settings_service.get_settings_bucket(self.xblock_mock) == {}
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ def edxnotes(cls):
|
||||
generate_uid, get_edxnotes_id_token, get_public_endpoint, get_token_url, is_feature_enabled
|
||||
)
|
||||
|
||||
if not settings.FEATURES.get("ENABLE_EDXNOTES"):
|
||||
return original_get_html(self, *args, **kwargs)
|
||||
|
||||
runtime = getattr(self, 'descriptor', self).runtime
|
||||
if not hasattr(runtime, 'modulestore'):
|
||||
return original_get_html(self, *args, **kwargs)
|
||||
|
||||
@@ -188,7 +188,15 @@ class CompletionServiceTestCase(CompletionWaffleTestMixin, SharedModuleStoreTest
|
||||
ItemFactory.create(parent=library, category='problem', publish_item=False, user_id=self.user.id)
|
||||
ItemFactory.create(parent=library, category='problem', publish_item=False, user_id=self.user.id)
|
||||
ItemFactory.create(parent=library, category='problem', publish_item=False, user_id=self.user.id)
|
||||
lib_vertical = ItemFactory.create(parent=self.sequence, category='vertical', publish_item=False)
|
||||
# Create a new vertical to hold the library content block
|
||||
# It is very important that we use parent_location=self.sequence.location (and not parent=self.sequence), since
|
||||
# sequence is a class attribute and passing it by value will update its .children=[] which will then leak into
|
||||
# other tests and cause errors if the children no longer exist.
|
||||
lib_vertical = ItemFactory.create(
|
||||
parent_location=self.sequence.location,
|
||||
category='vertical',
|
||||
publish_item=False,
|
||||
)
|
||||
library_content_block = ItemFactory.create(
|
||||
parent=lib_vertical,
|
||||
category='library_content',
|
||||
@@ -234,7 +242,11 @@ class CompletionServiceTestCase(CompletionWaffleTestMixin, SharedModuleStoreTest
|
||||
assert self.completion_service.vertical_is_complete(lib_vertical)
|
||||
|
||||
def test_vertical_completion_with_nested_children(self):
|
||||
parent_vertical = ItemFactory(parent=self.sequence, category='vertical')
|
||||
# Create a new vertical.
|
||||
# It is very important that we use parent_location=self.sequence.location (and not parent=self.sequence), since
|
||||
# sequence is a class attribute and passing it by value will update its .children=[] which will then leak into
|
||||
# other tests and cause errors if the children no longer exist.
|
||||
parent_vertical = ItemFactory(parent_location=self.sequence.location, category='vertical')
|
||||
extra_vertical = ItemFactory(parent=parent_vertical, category='vertical')
|
||||
problem = ItemFactory(parent=extra_vertical, category='problem')
|
||||
parent_vertical = self.store.get_item(parent_vertical.location)
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
"""
|
||||
Minimal Django settings for tests of common/lib.
|
||||
Required in Django 1.9+ due to imports of models in stock Django apps.
|
||||
"""
|
||||
import tempfile
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
ALL_LANGUAGES = []
|
||||
|
||||
BLOCK_STRUCTURES_SETTINGS = dict(
|
||||
COURSE_PUBLISH_TASK_DELAY=30,
|
||||
TASK_DEFAULT_RETRY_DELAY=30,
|
||||
TASK_MAX_RETRIES=5,
|
||||
)
|
||||
|
||||
COURSE_KEY_PATTERN = r'(?P<course_key_string>[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+)'
|
||||
COURSE_ID_PATTERN = COURSE_KEY_PATTERN.replace('course_key_string', 'course_id')
|
||||
USAGE_KEY_PATTERN = r'(?P<usage_key_string>(?:i4x://?[^/]+/[^/]+/[^/]+/[^@]+(?:@[^/]+)?)|(?:[^/]+))'
|
||||
|
||||
|
||||
COURSE_MODE_DEFAULTS = {
|
||||
'bulk_sku': None,
|
||||
'currency': 'usd',
|
||||
'description': None,
|
||||
'expiration_datetime': None,
|
||||
'min_price': 0,
|
||||
'name': 'Audit',
|
||||
'sku': None,
|
||||
'slug': 'audit',
|
||||
'suggested_prices': '',
|
||||
}
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': 'default.db',
|
||||
'USER': '',
|
||||
'PASSWORD': '',
|
||||
'HOST': '',
|
||||
'PORT': '',
|
||||
}
|
||||
}
|
||||
|
||||
PROCTORING_BACKENDS = {
|
||||
'DEFAULT': 'mock',
|
||||
'mock': {},
|
||||
'mock_proctoring_without_rules': {},
|
||||
}
|
||||
|
||||
FEATURES = {}
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django_sites_extensions',
|
||||
'lti_consumer',
|
||||
'openedx.core.djangoapps.django_comment_common',
|
||||
'openedx.core.djangoapps.discussions',
|
||||
'openedx.core.djangoapps.video_config',
|
||||
'openedx.core.djangoapps.video_pipeline',
|
||||
'openedx.core.djangoapps.bookmarks.apps.BookmarksConfig',
|
||||
'edxval',
|
||||
'lms.djangoapps.courseware',
|
||||
'lms.djangoapps.instructor_task',
|
||||
'common.djangoapps.student',
|
||||
'openedx.core.djangoapps.site_configuration',
|
||||
'lms.djangoapps.grades.apps.GradesConfig',
|
||||
'lms.djangoapps.certificates.apps.CertificatesConfig',
|
||||
'openedx.core.djangoapps.user_api',
|
||||
'common.djangoapps.course_modes.apps.CourseModesConfig',
|
||||
'lms.djangoapps.verify_student.apps.VerifyStudentConfig',
|
||||
'openedx.core.djangoapps.content_libraries',
|
||||
'openedx.core.djangoapps.dark_lang',
|
||||
'openedx.core.djangoapps.content.course_overviews.apps.CourseOverviewsConfig',
|
||||
'openedx.core.djangoapps.content.block_structure.apps.BlockStructureConfig',
|
||||
'openedx.core.djangoapps.catalog',
|
||||
'openedx.core.djangoapps.self_paced',
|
||||
'openedx.core.djangoapps.schedules.apps.SchedulesConfig',
|
||||
'openedx.core.djangoapps.theming.apps.ThemingConfig',
|
||||
'openedx.core.djangoapps.external_user_ids',
|
||||
'openedx.core.djangoapps.demographics',
|
||||
'openedx.core.djangoapps.agreements',
|
||||
|
||||
'lms.djangoapps.experiments',
|
||||
'openedx.features.content_type_gating',
|
||||
'openedx.features.course_duration_limits',
|
||||
'openedx.features.discounts',
|
||||
'milestones',
|
||||
'celery_utils',
|
||||
'waffle',
|
||||
'edx_when',
|
||||
'rest_framework_jwt',
|
||||
|
||||
# Django 1.11 demands to have imported models supported by installed apps.
|
||||
'completion',
|
||||
'common.djangoapps.entitlements',
|
||||
'organizations',
|
||||
)
|
||||
|
||||
LMS_ROOT_URL = "http://localhost:8000"
|
||||
|
||||
MEDIA_ROOT = tempfile.mkdtemp()
|
||||
|
||||
RECALCULATE_GRADES_ROUTING_KEY = 'edx.core.default'
|
||||
POLICY_CHANGE_GRADES_ROUTING_KEY = 'edx.core.default'
|
||||
POLICY_CHANGE_TASK_RATE_LIMIT = '300/h'
|
||||
|
||||
|
||||
SECRET_KEY = 'insecure-secret-key'
|
||||
SITE_ID = 1
|
||||
SITE_NAME = "localhost"
|
||||
PLATFORM_NAME = _('Your Platform Name Here')
|
||||
DEFAULT_FROM_EMAIL = 'registration@example.com'
|
||||
TRACK_MAX_EVENT = 50000
|
||||
USE_TZ = True
|
||||
|
||||
RETIREMENT_SERVICE_WORKER_USERNAME = 'RETIREMENT_SERVICE_USER'
|
||||
RETIRED_USERNAME_PREFIX = 'retired__user_'
|
||||
|
||||
PROCTORING_SETTINGS = {}
|
||||
|
||||
ROOT_URLCONF = None
|
||||
RUN_BLOCKSTORE_TESTS = False
|
||||
|
||||
# Software Secure request retry settings
|
||||
# Time in seconds before a retry of the task should be 60 mints.
|
||||
SOFTWARE_SECURE_REQUEST_RETRY_DELAY = 60 * 60
|
||||
# Maximum of 6 retries before giving up.
|
||||
SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS = 6
|
||||
@@ -308,10 +308,7 @@ class LibTestSuite(PytestSuite):
|
||||
xdist_remote_processes = self.processes
|
||||
for ip in self.xdist_ip_addresses.split(','):
|
||||
# Propogate necessary env vars to xdist containers
|
||||
if 'pavelib/paver_tests' in self.test_id:
|
||||
django_env_var_cmd = "export DJANGO_SETTINGS_MODULE='lms.envs.test'"
|
||||
else:
|
||||
django_env_var_cmd = "export DJANGO_SETTINGS_MODULE='openedx.tests.settings'"
|
||||
django_env_var_cmd = "export DJANGO_SETTINGS_MODULE='lms.envs.test'"
|
||||
|
||||
env_var_cmd = '{} DISABLE_COURSEENROLLMENT_HISTORY={}' \
|
||||
.format(django_env_var_cmd, self.disable_courseenrollment_history)
|
||||
|
||||
Reference in New Issue
Block a user