diff --git a/cms/djangoapps/contentstore/tests/test_courseware_index.py b/cms/djangoapps/contentstore/tests/test_courseware_index.py index 3866b7afd2..22b05eaff9 100644 --- a/cms/djangoapps/contentstore/tests/test_courseware_index.py +++ b/cms/djangoapps/contentstore/tests/test_courseware_index.py @@ -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 diff --git a/common/lib/pytest.ini b/common/lib/pytest.ini index 93c230eab2..bed2897487 100644 --- a/common/lib/pytest.ini +++ b/common/lib/pytest.ini @@ -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) diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py index d96f8ee428..0be451cc41 100644 --- a/common/lib/xmodule/xmodule/modulestore/django.py +++ b/common/lib/xmodule/xmodule/modulestore/django.py @@ -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()) diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/mongo_connection.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/mongo_connection.py index d2ead8e8ca..c868ee89f2 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/mongo_connection.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/mongo_connection.py @@ -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.""" diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 9aa56b83ec..38912a4354 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -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: """ diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py index 653dad78e6..27b014a3a1 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py @@ -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 diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index 08da4cfa75..f4bbbfdbb1 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -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 diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_semantics.py b/common/lib/xmodule/xmodule/modulestore/tests/test_semantics.py index e5fcfbc10f..d74d9f4cc4 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_semantics.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_semantics.py @@ -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 diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py index 8117b2c331..1739f48761 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py @@ -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) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/utils.py b/common/lib/xmodule/xmodule/modulestore/tests/utils.py index 92dbbfd8e0..f59d7b6b75 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/utils.py @@ -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) diff --git a/common/lib/xmodule/xmodule/tests/test_conditional.py b/common/lib/xmodule/xmodule/tests/test_conditional.py index 696a022ffe..49c8603be3 100644 --- a/common/lib/xmodule/xmodule/tests/test_conditional.py +++ b/common/lib/xmodule/xmodule/tests/test_conditional.py @@ -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""" diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py index f4c8dc8e49..b54d5291a1 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_module.py +++ b/common/lib/xmodule/xmodule/tests/test_course_module.py @@ -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 diff --git a/common/lib/xmodule/xmodule/tests/test_services.py b/common/lib/xmodule/xmodule/tests/test_services.py index c5b5373774..3e571756ca 100644 --- a/common/lib/xmodule/xmodule/tests/test_services.py +++ b/common/lib/xmodule/xmodule/tests/test_services.py @@ -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) == {} diff --git a/lms/djangoapps/edxnotes/decorators.py b/lms/djangoapps/edxnotes/decorators.py index ef039061b3..f92ca2939b 100644 --- a/lms/djangoapps/edxnotes/decorators.py +++ b/lms/djangoapps/edxnotes/decorators.py @@ -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) diff --git a/openedx/tests/completion_integration/test_services.py b/openedx/tests/completion_integration/test_services.py index fd23895d5a..94fdf4e46b 100644 --- a/openedx/tests/completion_integration/test_services.py +++ b/openedx/tests/completion_integration/test_services.py @@ -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) diff --git a/openedx/tests/settings.py b/openedx/tests/settings.py deleted file mode 100644 index cdaa79ceb1..0000000000 --- a/openedx/tests/settings.py +++ /dev/null @@ -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_ID_PATTERN = COURSE_KEY_PATTERN.replace('course_key_string', 'course_id') -USAGE_KEY_PATTERN = r'(?P(?: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 diff --git a/pavelib/utils/test/suites/pytest_suite.py b/pavelib/utils/test/suites/pytest_suite.py index 8d49f0cae7..40744ce513 100644 --- a/pavelib/utils/test/suites/pytest_suite.py +++ b/pavelib/utils/test/suites/pytest_suite.py @@ -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)