Commit 8dc7f342 by Calen Pennington Committed by Andy Armstrong

Improve debuggability when call-count numbers don't match up

parent 5c6a8467
...@@ -867,7 +867,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): ...@@ -867,7 +867,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
# so we don't need to make an extra query to compute it. # so we don't need to make an extra query to compute it.
# set the branch to 'publish' in order to prevent extra lookups of draft versions # set the branch to 'publish' in order to prevent extra lookups of draft versions
with mongo_store.branch_setting(ModuleStoreEnum.Branch.published_only): with mongo_store.branch_setting(ModuleStoreEnum.Branch.published_only):
with check_mongo_calls(mongo_store, 4, 0): with check_mongo_calls(4, 0):
course = mongo_store.get_course(course_id, depth=2) course = mongo_store.get_course(course_id, depth=2)
# make sure we pre-fetched a known sequential which should be at depth=2 # make sure we pre-fetched a known sequential which should be at depth=2
...@@ -879,7 +879,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): ...@@ -879,7 +879,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
# Now, test with the branch set to draft. No extra round trips b/c it doesn't go deep enough to get # Now, test with the branch set to draft. No extra round trips b/c it doesn't go deep enough to get
# beyond direct only categories # beyond direct only categories
with mongo_store.branch_setting(ModuleStoreEnum.Branch.draft_preferred): with mongo_store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
with check_mongo_calls(mongo_store, 4, 0): with check_mongo_calls(4, 0):
mongo_store.get_course(course_id, depth=2) mongo_store.get_course(course_id, depth=2)
def test_export_course_without_content_store(self): def test_export_course_without_content_store(self):
......
...@@ -201,10 +201,11 @@ class TestCourseListing(ModuleStoreTestCase): ...@@ -201,10 +201,11 @@ class TestCourseListing(ModuleStoreTestCase):
# Now count the db queries # Now count the db queries
store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo) store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo)
with check_mongo_calls(store, USER_COURSES_COUNT): with check_mongo_calls(USER_COURSES_COUNT):
_accessible_courses_list_from_groups(self.request) _accessible_courses_list_from_groups(self.request)
with check_mongo_calls(store, 1): # TODO: LMS-11220: Document why this takes 6 calls
with check_mongo_calls(6):
_accessible_courses_list(self.request) _accessible_courses_list(self.request)
def test_get_course_list_with_same_course_id(self): def test_get_course_list_with_same_course_id(self):
......
import pprint
import pymongo.message
from factory import Factory, lazy_attribute_sequence, lazy_attribute from factory import Factory, lazy_attribute_sequence, lazy_attribute
from factory.containers import CyclicDefinitionError from factory.containers import CyclicDefinitionError
from uuid import uuid4 from uuid import uuid4
...@@ -223,79 +226,74 @@ def check_exact_number_of_calls(object_with_method, method, num_calls, method_na ...@@ -223,79 +226,74 @@ def check_exact_number_of_calls(object_with_method, method, num_calls, method_na
yield yield
@contextmanager def check_number_of_calls(object_with_method, method_name, maximum_calls, minimum_calls=1):
def check_number_of_calls(object_with_method, method, maximum_calls, minimum_calls=1, method_name=None):
""" """
Instruments the given method on the given object to verify the number of calls to the method is Instruments the given method on the given object to verify the number of calls to the method is
less than or equal to the expected maximum_calls and greater than or equal to the expected minimum_calls. less than or equal to the expected maximum_calls and greater than or equal to the expected minimum_calls.
""" """
method_wrap = Mock(wraps=method) return check_sum_of_calls(object_with_method, [method_name], maximum_calls, minimum_calls)
wrap_patch = patch.object(object_with_method, method_name or method.__name__, method_wrap)
try: @contextmanager
wrap_patch.start() def check_sum_of_calls(object_, methods, maximum_calls, minimum_calls=1):
"""
Instruments the given methods on the given object to verify that the total sum of calls made to the
methods falls between minumum_calls and maximum_calls.
"""
mocks = {
method: Mock(wraps=getattr(object_, method))
for method in methods
}
with patch.multiple(object_, **mocks):
yield yield
finally: call_count = sum(mock.call_count for mock in mocks.values())
wrap_patch.stop() calls = pprint.pformat({
method_name: mock.call_args_list
for method_name, mock in mocks.items()
})
# Assertion errors don't handle multi-line values, so pretty-print to std-out instead
if not minimum_calls <= call_count <= maximum_calls:
print "Expected between {} and {} calls, {} were made. Calls: {}".format(
minimum_calls,
maximum_calls,
call_count,
calls,
)
# verify the counter actually worked by ensuring we have counted greater than (or equal to) the minimum calls # verify the counter actually worked by ensuring we have counted greater than (or equal to) the minimum calls
assert_greater_equal(method_wrap.call_count, minimum_calls) assert_greater_equal(call_count, minimum_calls)
# now verify the number of actual calls is less than (or equal to) the expected maximum # now verify the number of actual calls is less than (or equal to) the expected maximum
assert_less_equal(method_wrap.call_count, maximum_calls) assert_less_equal(call_count, maximum_calls)
@contextmanager @contextmanager
def check_mongo_calls(mongo_store, num_finds=0, num_sends=None): def check_mongo_calls(num_finds=0, num_sends=None):
""" """
Instruments the given store to count the number of calls to find (incl find_one) and the number Instruments the given store to count the number of calls to find (incl find_one) and the number
of calls to send_message which is for insert, update, and remove (if you provide num_sends). At the of calls to send_message which is for insert, update, and remove (if you provide num_sends). At the
end of the with statement, it compares the counts to the num_finds and num_sends. end of the with statement, it compares the counts to the num_finds and num_sends.
:param mongo_store: the MongoModulestore or subclass to watch or a SplitMongoModuleStore
:param num_finds: the exact number of find calls expected :param num_finds: the exact number of find calls expected
:param num_sends: If none, don't instrument the send calls. If non-none, count and compare to :param num_sends: If none, don't instrument the send calls. If non-none, count and compare to
the given int value. the given int value.
""" """
if mongo_store.get_modulestore_type() == ModuleStoreEnum.Type.mongo: with check_sum_of_calls(
with check_exact_number_of_calls(mongo_store.collection, mongo_store.collection.find, num_finds): pymongo.message,
if num_sends is not None: ['query', 'get_more'],
with check_exact_number_of_calls( num_finds,
mongo_store.database.connection, num_finds
mongo_store.database.connection._send_message, # pylint: disable=protected-access ):
num_sends, if num_sends is not None:
): with check_sum_of_calls(
yield pymongo.message,
else: ['insert', 'update', 'delete'],
num_sends,
num_sends
):
yield yield
elif mongo_store.get_modulestore_type() == ModuleStoreEnum.Type.split: else:
collections = [ yield
mongo_store.db_connection.course_index,
mongo_store.db_connection.structures,
mongo_store.db_connection.definitions,
]
# could add else clause which raises exception or just rely on the below to suss that out
try:
find_wraps = []
wrap_patches = []
for collection in collections:
find_wrap = Mock(wraps=collection.find)
find_wraps.append(find_wrap)
wrap_patch = patch.object(collection, 'find', find_wrap)
wrap_patches.append(wrap_patch)
wrap_patch.start()
if num_sends is not None:
connection = mongo_store.db_connection.database.connection
with check_exact_number_of_calls(
connection,
connection._send_message, # pylint: disable=protected-access
num_sends,
):
yield
else:
yield
finally:
map(lambda wrap_patch: wrap_patch.stop(), wrap_patches)
call_count = sum([find_wrap.call_count for find_wrap in find_wraps])
assert_equal(call_count, num_finds)
...@@ -270,8 +270,13 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -270,8 +270,13 @@ class TestMixedModuleStore(unittest.TestCase):
with self.assertRaises(DuplicateCourseError): with self.assertRaises(DuplicateCourseError):
self.store.create_course('org_x', 'course_y', 'run_z', self.user_id) self.store.create_course('org_x', 'course_y', 'run_z', self.user_id)
# Draft:
# - One lookup to locate an item that exists
# - Two lookups to determine an item doesn't exist (one to check mongo, one to check split)
# split has one lookup for the course and then one for the course items # split has one lookup for the course and then one for the course items
@ddt.data(('draft', 1, 0), ('split', 2, 0)) # TODO: LMS-11220: Document why draft find count is [1, 1]
# TODO: LMS-11220: Document why split find count is [2, 2]
@ddt.data(('draft', [1, 1], 0), ('split', [2, 2], 0))
@ddt.unpack @ddt.unpack
def test_has_item(self, default_ms, max_find, max_send): def test_has_item(self, default_ms, max_find, max_send):
self.initdb(default_ms) self.initdb(default_ms)
...@@ -279,15 +284,14 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -279,15 +284,14 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertTrue(self.store.has_item(self.course_locations[self.XML_COURSEID1])) self.assertTrue(self.store.has_item(self.course_locations[self.XML_COURSEID1]))
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID)) with check_mongo_calls(max_find.pop(0), max_send):
with check_mongo_calls(mongo_store, max_find, max_send):
self.assertTrue(self.store.has_item(self.problem_x1a_1)) self.assertTrue(self.store.has_item(self.problem_x1a_1))
# try negative cases # try negative cases
self.assertFalse(self.store.has_item( self.assertFalse(self.store.has_item(
self.course_locations[self.XML_COURSEID1].replace(name='not_findable', category='problem') self.course_locations[self.XML_COURSEID1].replace(name='not_findable', category='problem')
)) ))
with check_mongo_calls(mongo_store, max_find, max_send): with check_mongo_calls(max_find.pop(0), max_send):
self.assertFalse(self.store.has_item(self.fake_location)) self.assertFalse(self.store.has_item(self.fake_location))
# verify that an error is raised when the revision is not valid # verify that an error is raised when the revision is not valid
...@@ -296,7 +300,9 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -296,7 +300,9 @@ class TestMixedModuleStore(unittest.TestCase):
# draft is 2 to compute inheritance # draft is 2 to compute inheritance
# split is 2 (would be 3 on course b/c it looks up the wiki_slug in definitions) # split is 2 (would be 3 on course b/c it looks up the wiki_slug in definitions)
@ddt.data(('draft', 2, 0), ('split', 2, 0)) # TODO: LMS-11220: Document why draft find count is [2, 2]
# TODO: LMS-11220: Document why split find count is [3, 3]
@ddt.data(('draft', [2, 2], 0), ('split', [2, 2], 0))
@ddt.unpack @ddt.unpack
def test_get_item(self, default_ms, max_find, max_send): def test_get_item(self, default_ms, max_find, max_send):
self.initdb(default_ms) self.initdb(default_ms)
...@@ -304,8 +310,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -304,8 +310,7 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertIsNotNone(self.store.get_item(self.course_locations[self.XML_COURSEID1])) self.assertIsNotNone(self.store.get_item(self.course_locations[self.XML_COURSEID1]))
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID)) with check_mongo_calls(max_find.pop(0), max_send):
with check_mongo_calls(mongo_store, max_find, max_send):
self.assertIsNotNone(self.store.get_item(self.problem_x1a_1)) self.assertIsNotNone(self.store.get_item(self.problem_x1a_1))
# try negative cases # try negative cases
...@@ -313,7 +318,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -313,7 +318,7 @@ class TestMixedModuleStore(unittest.TestCase):
self.store.get_item( self.store.get_item(
self.course_locations[self.XML_COURSEID1].replace(name='not_findable', category='problem') self.course_locations[self.XML_COURSEID1].replace(name='not_findable', category='problem')
) )
with check_mongo_calls(mongo_store, max_find, max_send): with check_mongo_calls(max_find.pop(0), max_send):
with self.assertRaises(ItemNotFoundError): with self.assertRaises(ItemNotFoundError):
self.store.get_item(self.fake_location) self.store.get_item(self.fake_location)
...@@ -322,6 +327,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -322,6 +327,7 @@ class TestMixedModuleStore(unittest.TestCase):
self.store.get_item(self.fake_location, revision=ModuleStoreEnum.RevisionOption.draft_preferred) self.store.get_item(self.fake_location, revision=ModuleStoreEnum.RevisionOption.draft_preferred)
# compared to get_item for the course, draft asks for both draft and published # compared to get_item for the course, draft asks for both draft and published
# TODO: LMS-11220: Document why split find count is 3
@ddt.data(('draft', 8, 0), ('split', 2, 0)) @ddt.data(('draft', 8, 0), ('split', 2, 0))
@ddt.unpack @ddt.unpack
def test_get_items(self, default_ms, max_find, max_send): def test_get_items(self, default_ms, max_find, max_send):
...@@ -334,9 +340,8 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -334,9 +340,8 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertEqual(len(modules), 1) self.assertEqual(len(modules), 1)
self.assertEqual(modules[0].location, course_locn) self.assertEqual(modules[0].location, course_locn)
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
course_locn = self.course_locations[self.MONGO_COURSEID] course_locn = self.course_locations[self.MONGO_COURSEID]
with check_mongo_calls(mongo_store, max_find, max_send): with check_mongo_calls(max_find, max_send):
# NOTE: use get_course if you just want the course. get_items is expensive # NOTE: use get_course if you just want the course. get_items is expensive
modules = self.store.get_items(course_locn.course_key, qualifiers={'category': 'problem'}) modules = self.store.get_items(course_locn.course_key, qualifiers={'category': 'problem'})
self.assertEqual(len(modules), 6) self.assertEqual(len(modules), 6)
...@@ -352,7 +357,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -352,7 +357,7 @@ class TestMixedModuleStore(unittest.TestCase):
# split: 3 to get the course structure & the course definition (show_calculator is scope content) # split: 3 to get the course structure & the course definition (show_calculator is scope content)
# before the change. 1 during change to refetch the definition. 3 afterward (b/c it calls get_item to return the "new" object). # before the change. 1 during change to refetch the definition. 3 afterward (b/c it calls get_item to return the "new" object).
# 2 sends to update index & structure (calculator is a setting field) # 2 sends to update index & structure (calculator is a setting field)
@ddt.data(('draft', 7, 5), ('split', 6, 2)) @ddt.data(('draft', 11, 5), ('split', 6, 2))
@ddt.unpack @ddt.unpack
def test_update_item(self, default_ms, max_find, max_send): def test_update_item(self, default_ms, max_find, max_send):
""" """
...@@ -368,12 +373,11 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -368,12 +373,11 @@ class TestMixedModuleStore(unittest.TestCase):
self.store.update_item(course, self.user_id) self.store.update_item(course, self.user_id)
# now do it for a r/w db # now do it for a r/w db
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
problem = self.store.get_item(self.problem_x1a_1) problem = self.store.get_item(self.problem_x1a_1)
# if following raised, then the test is really a noop, change it # if following raised, then the test is really a noop, change it
self.assertNotEqual(problem.max_attempts, 2, "Default changed making test meaningless") self.assertNotEqual(problem.max_attempts, 2, "Default changed making test meaningless")
problem.max_attempts = 2 problem.max_attempts = 2
with check_mongo_calls(mongo_store, max_find, max_send): with check_mongo_calls(max_find, max_send):
problem = self.store.update_item(problem, self.user_id) problem = self.store.update_item(problem, self.user_id)
self.assertEqual(problem.max_attempts, 2, "Update didn't persist") self.assertEqual(problem.max_attempts, 2, "Update didn't persist")
...@@ -434,7 +438,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -434,7 +438,7 @@ class TestMixedModuleStore(unittest.TestCase):
component = self.store.publish(component.location, self.user_id) component = self.store.publish(component.location, self.user_id)
self.assertFalse(self.store.has_changes(component)) self.assertFalse(self.store.has_changes(component))
@ddt.data(('draft', 7, 2), ('split', 13, 4)) @ddt.data(('draft', 8, 2), ('split', 13, 4))
@ddt.unpack @ddt.unpack
def test_delete_item(self, default_ms, max_find, max_send): def test_delete_item(self, default_ms, max_find, max_send):
""" """
...@@ -446,14 +450,14 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -446,14 +450,14 @@ class TestMixedModuleStore(unittest.TestCase):
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
self.store.delete_item(self.xml_chapter_location, self.user_id) self.store.delete_item(self.xml_chapter_location, self.user_id)
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID)) with check_mongo_calls(max_find, max_send):
with check_mongo_calls(mongo_store, max_find, max_send):
self.store.delete_item(self.writable_chapter_location, self.user_id) self.store.delete_item(self.writable_chapter_location, self.user_id)
# verify it's gone # verify it's gone
with self.assertRaises(ItemNotFoundError): with self.assertRaises(ItemNotFoundError):
self.store.get_item(self.writable_chapter_location) self.store.get_item(self.writable_chapter_location)
@ddt.data(('draft', 8, 2), ('split', 13, 4)) @ddt.data(('draft', 9, 2), ('split', 13, 4))
@ddt.unpack @ddt.unpack
def test_delete_private_vertical(self, default_ms, max_find, max_send): def test_delete_private_vertical(self, default_ms, max_find, max_send):
""" """
...@@ -484,8 +488,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -484,8 +488,7 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertIn(vert_loc, course.children) self.assertIn(vert_loc, course.children)
# delete the vertical and ensure the course no longer points to it # delete the vertical and ensure the course no longer points to it
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID)) with check_mongo_calls(max_find, max_send):
with check_mongo_calls(mongo_store, max_find, max_send):
self.store.delete_item(vert_loc, self.user_id) self.store.delete_item(vert_loc, self.user_id)
course = self.store.get_course(self.course_locations[self.MONGO_COURSEID].course_key, 0) course = self.store.get_course(self.course_locations[self.MONGO_COURSEID].course_key, 0)
if hasattr(private_vert.location, 'version_guid'): if hasattr(private_vert.location, 'version_guid'):
...@@ -499,7 +502,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -499,7 +502,7 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertFalse(self.store.has_item(leaf_loc)) self.assertFalse(self.store.has_item(leaf_loc))
self.assertNotIn(vert_loc, course.children) self.assertNotIn(vert_loc, course.children)
@ddt.data(('draft', 4, 1), ('split', 5, 2)) @ddt.data(('draft', 5, 1), ('split', 5, 2))
@ddt.unpack @ddt.unpack
def test_delete_draft_vertical(self, default_ms, max_find, max_send): def test_delete_draft_vertical(self, default_ms, max_find, max_send):
""" """
...@@ -528,24 +531,24 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -528,24 +531,24 @@ class TestMixedModuleStore(unittest.TestCase):
self.store.publish(private_vert.location, self.user_id) self.store.publish(private_vert.location, self.user_id)
private_leaf.display_name = 'change me' private_leaf.display_name = 'change me'
private_leaf = self.store.update_item(private_leaf, self.user_id) private_leaf = self.store.update_item(private_leaf, self.user_id)
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
# test succeeds if delete succeeds w/o error # test succeeds if delete succeeds w/o error
with check_mongo_calls(mongo_store, max_find, max_send): with check_mongo_calls(max_find, max_send):
self.store.delete_item(private_leaf.location, self.user_id) self.store.delete_item(private_leaf.location, self.user_id)
@ddt.data(('draft', 2, 0), ('split', 3, 0)) # TODO: LMS-11220: Document why split find count is 5
# TODO: LMS-11220: Document why draft find count is 4
@ddt.data(('draft', 4, 0), ('split', 4, 0))
@ddt.unpack @ddt.unpack
def test_get_courses(self, default_ms, max_find, max_send): def test_get_courses(self, default_ms, max_find, max_send):
self.initdb(default_ms) self.initdb(default_ms)
# we should have 3 total courses across all stores # we should have 3 total courses across all stores
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID)) with check_mongo_calls(max_find, max_send):
with check_mongo_calls(mongo_store, max_find, max_send):
courses = self.store.get_courses() courses = self.store.get_courses()
course_ids = [course.location for course in courses] course_ids = [course.location for course in courses]
self.assertEqual(len(courses), 3, "Not 3 courses: {}".format(course_ids)) self.assertEqual(len(courses), 3, "Not 3 courses: {}".format(course_ids))
self.assertIn(self.course_locations[self.MONGO_COURSEID], course_ids) self.assertIn(self.course_locations[self.MONGO_COURSEID], course_ids)
self.assertIn(self.course_locations[self.XML_COURSEID1], course_ids) self.assertIn(self.course_locations[self.XML_COURSEID1], course_ids)
self.assertIn(self.course_locations[self.XML_COURSEID2], course_ids) self.assertIn(self.course_locations[self.XML_COURSEID2], course_ids)
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred): with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
draft_courses = self.store.get_courses(remove_branch=True) draft_courses = self.store.get_courses(remove_branch=True)
...@@ -588,8 +591,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -588,8 +591,7 @@ class TestMixedModuleStore(unittest.TestCase):
of getting an item whose scope.content fields are looked at. of getting an item whose scope.content fields are looked at.
""" """
self.initdb(default_ms) self.initdb(default_ms)
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID)) with check_mongo_calls(max_find, max_send):
with check_mongo_calls(mongo_store, max_find, max_send):
course = self.store.get_item(self.course_locations[self.MONGO_COURSEID]) course = self.store.get_item(self.course_locations[self.MONGO_COURSEID])
self.assertEqual(course.id, self.course_locations[self.MONGO_COURSEID].course_key) self.assertEqual(course.id, self.course_locations[self.MONGO_COURSEID].course_key)
...@@ -598,7 +600,8 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -598,7 +600,8 @@ class TestMixedModuleStore(unittest.TestCase):
# notice this doesn't test getting a public item via draft_preferred which draft would have 2 hits (split # notice this doesn't test getting a public item via draft_preferred which draft would have 2 hits (split
# still only 2) # still only 2)
@ddt.data(('draft', 1, 0), ('split', 2, 0)) # TODO: LMS-11220: Document why draft find count is 2
@ddt.data(('draft', 2, 0), ('split', 2, 0))
@ddt.unpack @ddt.unpack
def test_get_parent_locations(self, default_ms, max_find, max_send): def test_get_parent_locations(self, default_ms, max_find, max_send):
""" """
...@@ -607,10 +610,9 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -607,10 +610,9 @@ class TestMixedModuleStore(unittest.TestCase):
self.initdb(default_ms) self.initdb(default_ms)
self._create_block_hierarchy() self._create_block_hierarchy()
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID)) with check_mongo_calls(max_find, max_send):
with check_mongo_calls(mongo_store, max_find, max_send):
parent = self.store.get_parent_location(self.problem_x1a_1) parent = self.store.get_parent_location(self.problem_x1a_1)
self.assertEqual(parent, self.vertical_x1a) self.assertEqual(parent, self.vertical_x1a)
parent = self.store.get_parent_location(self.xml_chapter_location) parent = self.store.get_parent_location(self.xml_chapter_location)
self.assertEqual(parent, self.course_locations[self.XML_COURSEID1]) self.assertEqual(parent, self.course_locations[self.XML_COURSEID1])
...@@ -692,7 +694,21 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -692,7 +694,21 @@ class TestMixedModuleStore(unittest.TestCase):
(child_to_delete_location, None, ModuleStoreEnum.RevisionOption.published_only), (child_to_delete_location, None, ModuleStoreEnum.RevisionOption.published_only),
]) ])
@ddt.data(('draft', [10, 3], 0), ('split', [14, 6], 0)) # Mongo reads:
# First location:
# - count problem (1)
# - For each level of ancestors: (5)
# - Count ancestor
# - retrieve ancestor
# - compute inheritable data
# Second location:
# - load vertical
# - load inheritance data
# TODO: LMS-11220: Document why draft send count is 5
# TODO: LMS-11220: Document why draft find count is 18
# TODO: LMS-11220: Document why split find count is 16
@ddt.data(('draft', [18, 5], 0), ('split', [14, 6], 0))
@ddt.unpack @ddt.unpack
def test_path_to_location(self, default_ms, num_finds, num_sends): def test_path_to_location(self, default_ms, num_finds, num_sends):
""" """
...@@ -713,7 +729,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -713,7 +729,7 @@ class TestMixedModuleStore(unittest.TestCase):
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID)) mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
for location, expected in should_work: for location, expected in should_work:
with check_mongo_calls(mongo_store, num_finds.pop(0), num_sends): with check_mongo_calls(num_finds.pop(0), num_sends):
self.assertEqual(path_to_location(self.store, location), expected) self.assertEqual(path_to_location(self.store, location), expected)
not_found = ( not_found = (
...@@ -881,8 +897,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -881,8 +897,7 @@ class TestMixedModuleStore(unittest.TestCase):
block_id=location.block_id block_id=location.block_id
) )
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID)) with check_mongo_calls(max_find, max_send):
with check_mongo_calls(mongo_store, max_find, max_send):
found_orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key) found_orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key)
self.assertEqual(set(found_orphans), set(orphan_locations)) self.assertEqual(set(found_orphans), set(orphan_locations))
...@@ -924,7 +939,9 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -924,7 +939,9 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertEqual(self.user_id, block.subtree_edited_by) self.assertEqual(self.user_id, block.subtree_edited_by)
self.assertGreater(datetime.datetime.now(UTC), block.subtree_edited_on) self.assertGreater(datetime.datetime.now(UTC), block.subtree_edited_on)
@ddt.data(('draft', 1, 0), ('split', 1, 0)) # TODO: LMS-11220: Document why split find count is 2
# TODO: LMS-11220: Document why draft find count is 2
@ddt.data(('draft', 2, 0), ('split', 2, 0))
@ddt.unpack @ddt.unpack
def test_get_courses_for_wiki(self, default_ms, max_find, max_send): def test_get_courses_for_wiki(self, default_ms, max_find, max_send):
""" """
...@@ -941,8 +958,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -941,8 +958,7 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertIn(self.course_locations[self.XML_COURSEID2].course_key, wiki_courses) self.assertIn(self.course_locations[self.XML_COURSEID2].course_key, wiki_courses)
# Test Mongo wiki # Test Mongo wiki
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID)) with check_mongo_calls(max_find, max_send):
with check_mongo_calls(mongo_store, max_find, max_send):
wiki_courses = self.store.get_courses_for_wiki('999') wiki_courses = self.store.get_courses_for_wiki('999')
self.assertEqual(len(wiki_courses), 1) self.assertEqual(len(wiki_courses), 1)
self.assertIn( self.assertIn(
...@@ -953,7 +969,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -953,7 +969,7 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertEqual(len(self.store.get_courses_for_wiki('edX.simple.2012_Fall')), 0) self.assertEqual(len(self.store.get_courses_for_wiki('edX.simple.2012_Fall')), 0)
self.assertEqual(len(self.store.get_courses_for_wiki('no_such_wiki')), 0) self.assertEqual(len(self.store.get_courses_for_wiki('no_such_wiki')), 0)
@ddt.data(('draft', 2, 6), ('split', 7, 2)) @ddt.data(('draft', 3, 6), ('split', 7, 2))
@ddt.unpack @ddt.unpack
def test_unpublish(self, default_ms, max_find, max_send): def test_unpublish(self, default_ms, max_find, max_send):
""" """
...@@ -971,8 +987,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -971,8 +987,7 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertIsNotNone(published_xblock) self.assertIsNotNone(published_xblock)
# unpublish # unpublish
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID)) with check_mongo_calls(max_find, max_send):
with check_mongo_calls(mongo_store, max_find, max_send):
self.store.unpublish(self.vertical_x1a, self.user_id) self.store.unpublish(self.vertical_x1a, self.user_id)
with self.assertRaises(ItemNotFoundError): with self.assertRaises(ItemNotFoundError):
...@@ -1000,8 +1015,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -1000,8 +1015,7 @@ class TestMixedModuleStore(unittest.TestCase):
# start off as Private # start off as Private
item = self.store.create_child(self.user_id, self.writable_chapter_location, 'problem', 'test_compute_publish_state') item = self.store.create_child(self.user_id, self.writable_chapter_location, 'problem', 'test_compute_publish_state')
item_location = item.location item_location = item.location
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID)) with check_mongo_calls(max_find, max_send):
with check_mongo_calls(mongo_store, max_find, max_send):
self.assertEquals(self.store.compute_publish_state(item), PublishState.private) self.assertEquals(self.store.compute_publish_state(item), PublishState.private)
# Private -> Public # Private -> Public
......
...@@ -19,23 +19,35 @@ class TestPublish(SplitWMongoCourseBoostrapper): ...@@ -19,23 +19,35 @@ class TestPublish(SplitWMongoCourseBoostrapper):
# There are 12 created items and 7 parent updates # There are 12 created items and 7 parent updates
# create course: finds: 1 to verify uniqueness, 1 to find parents # create course: finds: 1 to verify uniqueness, 1 to find parents
# sends: 1 to create course, 1 to create overview # sends: 1 to create course, 1 to create overview
with check_mongo_calls(self.draft_mongo, 5, 2): with check_mongo_calls(5, 2):
super(TestPublish, self)._create_course(split=False) # 2 inserts (course and overview) super(TestPublish, self)._create_course(split=False) # 2 inserts (course and overview)
# with bulk will delay all inheritance computations which won't be added into the mongo_calls # with bulk will delay all inheritance computations which won't be added into the mongo_calls
with self.draft_mongo.bulk_write_operations(self.old_course_key): with self.draft_mongo.bulk_write_operations(self.old_course_key):
# finds: 1 for parent to add child # finds: 1 for parent to add child
# sends: 1 for insert, 1 for parent (add child) # sends: 1 for insert, 1 for parent (add child)
with check_mongo_calls(self.draft_mongo, 1, 2): with check_mongo_calls(1, 2):
self._create_item('chapter', 'Chapter1', {}, {'display_name': 'Chapter 1'}, 'course', 'runid', split=False) self._create_item('chapter', 'Chapter1', {}, {'display_name': 'Chapter 1'}, 'course', 'runid', split=False)
with check_mongo_calls(self.draft_mongo, 2, 2): with check_mongo_calls(2, 2):
self._create_item('chapter', 'Chapter2', {}, {'display_name': 'Chapter 2'}, 'course', 'runid', split=False) self._create_item('chapter', 'Chapter2', {}, {'display_name': 'Chapter 2'}, 'course', 'runid', split=False)
# update info propagation is 2 levels. create looks for draft and then published and then creates # For each vertical (2) created:
with check_mongo_calls(self.draft_mongo, 8, 6): # - load draft
# - load non-draft
# - get last error
# - load parent
# - load inheritable data
with check_mongo_calls(10, 6):
self._create_item('vertical', 'Vert1', {}, {'display_name': 'Vertical 1'}, 'chapter', 'Chapter1', split=False) self._create_item('vertical', 'Vert1', {}, {'display_name': 'Vertical 1'}, 'chapter', 'Chapter1', split=False)
self._create_item('vertical', 'Vert2', {}, {'display_name': 'Vertical 2'}, 'chapter', 'Chapter1', split=False) self._create_item('vertical', 'Vert2', {}, {'display_name': 'Vertical 2'}, 'chapter', 'Chapter1', split=False)
with check_mongo_calls(self.draft_mongo, 20, 12): # For each (4) item created
# - load draft
# - load non-draft
# - get last error
# - load parent
# - load inheritable data
# - load parent
with check_mongo_calls(24, 12):
self._create_item('html', 'Html1', "<p>Goodbye</p>", {'display_name': 'Parented Html'}, 'vertical', 'Vert1', split=False) self._create_item('html', 'Html1', "<p>Goodbye</p>", {'display_name': 'Parented Html'}, 'vertical', 'Vert1', split=False)
self._create_item( self._create_item(
'discussion', 'Discussion1', 'discussion', 'Discussion1',
...@@ -63,7 +75,7 @@ class TestPublish(SplitWMongoCourseBoostrapper): ...@@ -63,7 +75,7 @@ class TestPublish(SplitWMongoCourseBoostrapper):
split=False split=False
) )
with check_mongo_calls(self.draft_mongo, 0, 2): with check_mongo_calls(0, 2):
# 2 finds b/c looking for non-existent parents # 2 finds b/c looking for non-existent parents
self._create_item('static_tab', 'staticuno', "<p>tab</p>", {'display_name': 'Tab uno'}, None, None, split=False) self._create_item('static_tab', 'staticuno', "<p>tab</p>", {'display_name': 'Tab uno'}, None, None, split=False)
self._create_item('course_info', 'updates', "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, None, None, split=False) self._create_item('course_info', 'updates', "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, None, None, split=False)
...@@ -76,10 +88,11 @@ class TestPublish(SplitWMongoCourseBoostrapper): ...@@ -76,10 +88,11 @@ class TestPublish(SplitWMongoCourseBoostrapper):
vert_location = self.old_course_key.make_usage_key('vertical', block_id='Vert1') vert_location = self.old_course_key.make_usage_key('vertical', block_id='Vert1')
item = self.draft_mongo.get_item(vert_location, 2) item = self.draft_mongo.get_item(vert_location, 2)
# Vert1 has 3 children; so, publishes 4 nodes which may mean 4 inserts & 1 bulk remove # Vert1 has 3 children; so, publishes 4 nodes which may mean 4 inserts & 1 bulk remove
# TODO: LMS-11220: Document why find count is 25
# 25-June-2014 find calls are 19. Probably due to inheritance recomputation? # 25-June-2014 find calls are 19. Probably due to inheritance recomputation?
# 02-July-2014 send calls are 7. 5 from above, plus 2 for updating subtree edit info for Chapter1 and course # 02-July-2014 send calls are 7. 5 from above, plus 2 for updating subtree edit info for Chapter1 and course
# find calls are 22. 19 from above, plus 3 for finding the parent of Vert1, Chapter1, and course # find calls are 22. 19 from above, plus 3 for finding the parent of Vert1, Chapter1, and course
with check_mongo_calls(self.draft_mongo, 22, 7): with check_mongo_calls(25, 7):
self.draft_mongo.publish(item.location, self.user_id) self.draft_mongo.publish(item.location, self.user_id)
# verify status # verify status
......
...@@ -607,7 +607,7 @@ def _import_course_draft( ...@@ -607,7 +607,7 @@ def _import_course_draft(
_import_module(descriptor) _import_module(descriptor)
except Exception: except Exception:
logging.exception('There while importing draft descriptor %s', descriptor) logging.exception('while importing draft descriptor %s', descriptor)
def allowed_metadata_by_category(category): def allowed_metadata_by_category(category):
......
...@@ -326,7 +326,7 @@ class TestTOC(ModuleStoreTestCase): ...@@ -326,7 +326,7 @@ class TestTOC(ModuleStoreTestCase):
self.request = factory.get(chapter_url) self.request = factory.get(chapter_url)
self.request.user = UserFactory() self.request.user = UserFactory()
self.modulestore = self.store._get_modulestore_for_courseid(self.course_key) self.modulestore = self.store._get_modulestore_for_courseid(self.course_key)
with check_mongo_calls(self.modulestore, num_finds, num_sends): with check_mongo_calls(num_finds, num_sends):
self.toy_course = self.store.get_course(self.toy_loc, depth=2) self.toy_course = self.store.get_course(self.toy_loc, depth=2)
self.field_data_cache = FieldDataCache.cache_for_descriptor_descendents( self.field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.toy_loc, self.request.user, self.toy_course, depth=2) self.toy_loc, self.request.user, self.toy_course, depth=2)
...@@ -352,7 +352,7 @@ class TestTOC(ModuleStoreTestCase): ...@@ -352,7 +352,7 @@ class TestTOC(ModuleStoreTestCase):
'format': '', 'due': None, 'active': False}], 'format': '', 'due': None, 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) 'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
with check_mongo_calls(self.modulestore, 0, 0): with check_mongo_calls(0, 0):
actual = render.toc_for_course( actual = render.toc_for_course(
self.request.user, self.request, self.toy_course, self.chapter, None, self.field_data_cache self.request.user, self.request, self.toy_course, self.chapter, None, self.field_data_cache
) )
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment