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:
Braden MacDonald
2021-07-30 14:16:18 -07:00
committed by Braden MacDonald
parent daac2f7585
commit da09bcadc5
17 changed files with 114 additions and 328 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) == {}

View File

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

View File

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

View File

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

View File

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