test_field_override_performance.py 9.45 KB
Newer Older
1 2 3 4 5
# coding=UTF-8
"""
Performance tests for field overrides.
"""
import ddt
6
import itertools
7
import mock
8
from nose.plugins.skip import SkipTest
9

10
from courseware.views.views import progress
11
from courseware.field_overrides import OverrideFieldData
12
from courseware.testutils import FieldOverrideTestMixin
13
from datetime import datetime
14
from django.conf import settings
15
from django.core.cache import caches
16 17 18
from django.test.client import RequestFactory
from django.test.utils import override_settings
from nose.plugins.attrib import attr
Nimisha Asthagiri committed
19
from opaque_keys.edx.keys import CourseKey
20
from pytz import UTC
21
from request_cache.middleware import RequestCache
22
from student.models import CourseEnrollment
23
from student.tests.factories import UserFactory
24
from xblock.core import XBlock
25 26
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, \
    TEST_DATA_SPLIT_MODULESTORE, TEST_DATA_MONGO_MODULESTORE
Nimisha Asthagiri committed
27
from xmodule.modulestore.tests.factories import check_mongo_calls, CourseFactory, check_sum_of_calls
28
from xmodule.modulestore.tests.utils import ProceduralCourseTestMixin
29
from ccx_keys.locator import CCXLocator
30
from lms.djangoapps.ccx.tests.factories import CcxFactory
31
from openedx.core.djangoapps.content.block_structure.api import get_course_in_cache
32 33


34
@attr(shard=3)
35
@mock.patch.dict(
36 37 38 39
    'django.conf.settings.FEATURES',
    {
        'ENABLE_XBLOCK_VIEW_ENDPOINT': True,
    }
40 41
)
@ddt.ddt
42
class FieldOverridePerformanceTestCase(FieldOverrideTestMixin, ProceduralCourseTestMixin, ModuleStoreTestCase):
43 44 45 46
    """
    Base class for instrumenting SQL queries and Mongo reads for field override
    providers.
    """
47
    __test__ = False
48 49
    # Tell Django to clean out all databases, not just default
    multi_db = True
50 51 52 53

    # TEST_DATA must be overridden by subclasses
    TEST_DATA = None

54 55 56 57 58 59 60 61 62
    def setUp(self):
        """
        Create a test client, course, and user.
        """
        super(FieldOverridePerformanceTestCase, self).setUp()

        self.request_factory = RequestFactory()
        self.student = UserFactory.create()
        self.request = self.request_factory.get("foo")
63
        self.request.session = {}
64
        self.request.user = self.student
65 66 67 68

        patcher = mock.patch('edxmako.request_context.get_current_request', return_value=self.request)
        patcher.start()
        self.addCleanup(patcher.stop)
69
        self.course = None
70
        self.ccx = None
71

72
    def setup_course(self, size, enable_ccx, view_as_ccx):
73 74 75
        """
        Build a gradable course where each node has `size` children.
        """
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
        grading_policy = {
            "GRADER": [
                {
                    "drop_count": 2,
                    "min_count": 12,
                    "short_label": "HW",
                    "type": "Homework",
                    "weight": 0.15
                },
                {
                    "drop_count": 2,
                    "min_count": 12,
                    "type": "Lab",
                    "weight": 0.15
                },
                {
                    "drop_count": 0,
                    "min_count": 1,
                    "short_label": "Midterm",
                    "type": "Midterm Exam",
                    "weight": 0.3
                },
                {
                    "drop_count": 0,
                    "min_count": 1,
                    "short_label": "Final",
                    "type": "Final Exam",
                    "weight": 0.4
                }
            ],
            "GRADE_CUTOFFS": {
                "Pass": 0.5
            }
        }

        self.course = CourseFactory.create(
            graded=True,
            start=datetime.now(UTC),
114 115
            grading_policy=grading_policy,
            enable_ccx=enable_ccx,
116 117 118
        )
        self.populate_course(size)

119 120 121 122 123 124
        course_key = self.course.id
        if enable_ccx:
            self.ccx = CcxFactory.create(course_id=self.course.id)
            if view_as_ccx:
                course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id)

125 126
        CourseEnrollment.enroll(
            self.student,
127
            course_key
128
        )
Nimisha Asthagiri committed
129
        return CourseKey.from_string(unicode(course_key))
130

Nimisha Asthagiri committed
131
    def grade_course(self, course_key):
132 133 134 135 136
        """
        Renders the progress page for the given course.
        """
        return progress(
            self.request,
137
            course_id=unicode(course_key),
138 139 140
            student_id=self.student.id
        )

141 142 143 144 145
    def assertMongoCallCount(self, calls):
        """
        Assert that mongodb is queried ``calls`` times in the surrounded
        context.
        """
Nimisha Asthagiri committed
146
        return check_mongo_calls(calls)
147 148 149 150 151 152 153 154

    def assertXBlockInstantiations(self, instantiations):
        """
        Assert that exactly ``instantiations`` XBlocks are instantiated in
        the surrounded context.
        """
        return check_sum_of_calls(XBlock, ['__init__'], instantiations, instantiations, include_arguments=False)

155 156
    def instrument_course_progress_render(
            self, course_width, enable_ccx, view_as_ccx,
Nimisha Asthagiri committed
157
            sql_queries, mongo_reads,
158
    ):
159 160 161
        """
        Renders the progress page, instrumenting Mongo reads and SQL queries.
        """
Nimisha Asthagiri committed
162
        course_key = self.setup_course(course_width, enable_ccx, view_as_ccx)
163

164 165
        # Switch to published-only mode to simulate the LMS
        with self.settings(MODULESTORE_BRANCH='published-only'):
166 167
            # Clear all caches before measuring
            for cache in settings.CACHES:
168
                caches[cache].clear()
169

170
            # Refill the metadata inheritance cache
Nimisha Asthagiri committed
171
            get_course_in_cache(course_key)
172 173 174

            # We clear the request cache to simulate a new request in the LMS.
            RequestCache.clear_request_cache()
175

176 177 178 179
            # Reset the list of provider classes, so that our django settings changes
            # can actually take affect.
            OverrideFieldData.provider_classes = None

Nimisha Asthagiri committed
180 181 182 183 184
            with self.assertNumQueries(sql_queries, using='default'):
                with self.assertNumQueries(0, using='student_module_history'):
                    with self.assertMongoCallCount(mongo_reads):
                        with self.assertXBlockInstantiations(1):
                            self.grade_course(course_key)
185

186
    @ddt.data(*itertools.product(('no_overrides', 'ccx'), range(1, 4), (True, False), (True, False)))
187 188
    @ddt.unpack
    @override_settings(
189 190
        XBLOCK_FIELD_DATA_WRAPPERS=[],
        MODULESTORE_FIELD_OVERRIDE_PROVIDERS=[],
191
    )
192
    def test_field_overrides(self, overrides, course_width, enable_ccx, view_as_ccx):
193 194 195
        """
        Test without any field overrides.
        """
196 197 198 199
        providers = {
            'no_overrides': (),
            'ccx': ('ccx.overrides.CustomCoursesForEdxOverrideProvider',)
        }
200 201 202 203 204 205 206 207 208
        if overrides == 'no_overrides' and view_as_ccx:
            raise SkipTest("Can't view a ccx course if field overrides are disabled.")

        if not enable_ccx and view_as_ccx:
            raise SkipTest("Can't view a ccx course if ccx is disabled on the course")

        if self.MODULESTORE == TEST_DATA_MONGO_MODULESTORE and view_as_ccx:
            raise SkipTest("Can't use a MongoModulestore test as a CCX course")

209 210 211 212
        with self.settings(
            XBLOCK_FIELD_DATA_WRAPPERS=['lms.djangoapps.courseware.field_overrides:OverrideModulestoreFieldData.wrap'],
            MODULESTORE_FIELD_OVERRIDE_PROVIDERS=providers[overrides],
        ):
Nimisha Asthagiri committed
213
            sql_queries, mongo_reads = self.TEST_DATA[
214 215 216
                (overrides, course_width, enable_ccx, view_as_ccx)
            ]
            self.instrument_course_progress_render(
Nimisha Asthagiri committed
217
                course_width, enable_ccx, view_as_ccx, sql_queries, mongo_reads,
218
            )
219 220 221 222 223 224 225


class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
    """
    Test cases for instrumenting field overrides against the Mongo modulestore.
    """
    MODULESTORE = TEST_DATA_MONGO_MODULESTORE
226
    __test__ = True
227

228
    TEST_DATA = {
229 230 231 232
        # (providers, course_width, enable_ccx, view_as_ccx): (
        #     # of sql queries to default,
        #     # of mongo queries,
        # )
233 234 235 236 237 238 239 240 241 242 243 244
        ('no_overrides', 1, True, False): (21, 6),
        ('no_overrides', 2, True, False): (21, 6),
        ('no_overrides', 3, True, False): (21, 6),
        ('ccx', 1, True, False): (21, 6),
        ('ccx', 2, True, False): (21, 6),
        ('ccx', 3, True, False): (21, 6),
        ('no_overrides', 1, False, False): (21, 6),
        ('no_overrides', 2, False, False): (21, 6),
        ('no_overrides', 3, False, False): (21, 6),
        ('ccx', 1, False, False): (21, 6),
        ('ccx', 2, False, False): (21, 6),
        ('ccx', 3, False, False): (21, 6),
245
    }
246 247 248 249 250 251 252


class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
    """
    Test cases for instrumenting field overrides against the Split modulestore.
    """
    MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
253 254 255
    __test__ = True

    TEST_DATA = {
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
        ('no_overrides', 1, True, False): (21, 3),
        ('no_overrides', 2, True, False): (21, 3),
        ('no_overrides', 3, True, False): (21, 3),
        ('ccx', 1, True, False): (21, 3),
        ('ccx', 2, True, False): (21, 3),
        ('ccx', 3, True, False): (21, 3),
        ('ccx', 1, True, True): (22, 3),
        ('ccx', 2, True, True): (22, 3),
        ('ccx', 3, True, True): (22, 3),
        ('no_overrides', 1, False, False): (21, 3),
        ('no_overrides', 2, False, False): (21, 3),
        ('no_overrides', 3, False, False): (21, 3),
        ('ccx', 1, False, False): (21, 3),
        ('ccx', 2, False, False): (21, 3),
        ('ccx', 3, False, False): (21, 3),
271
    }