Commit dedd88cc by Nimisha Asthagiri

Merge pull request #4977 from edx/split/paver

Split/paver LMS-11211
parents 3ec3bbbe 2930e29a
......@@ -20,7 +20,8 @@ def has_course_access(user, course_key, role=CourseStaffRole):
return True
if OrgStaffRole(org=course_key.org).has_user(user):
return True
return auth.has_access(user, role(course_key))
# temporary to ensure we give universal access given a course until we impl branch specific perms
return auth.has_access(user, role(course_key.for_branch(None)))
def get_user_role(user, course_id):
......
......@@ -567,7 +567,11 @@ def _create_new_course(request, org, number, run, fields):
fields.update(definition_data)
store = modulestore()
with store.default_store(settings.FEATURES.get('DEFAULT_STORE_FOR_NEW_COURSE', 'mongo')):
store_for_new_course = (
settings.FEATURES.get('DEFAULT_STORE_FOR_NEW_COURSE') or
store.default_modulestore.get_modulestore_type()
)
with store.default_store(store_for_new_course):
# Creating the course raises DuplicateCourseError if an existing course with this org/name is found
new_course = store.create_course(
org,
......@@ -584,7 +588,8 @@ def _create_new_course(request, org, number, run, fields):
initialize_permissions(new_course.id, request.user)
return JsonResponse({
'url': reverse_course_url('course_handler', new_course.id)
'url': reverse_course_url('course_handler', new_course.id),
'course_key': unicode(new_course.id),
})
......
......@@ -480,10 +480,16 @@ class TestEditItem(ItemTest):
display_name = 'chapter created'
resp = self.create_xblock(display_name=display_name, category='chapter')
chap_usage_key = self.response_usage_key(resp)
# create 2 sequentials
resp = self.create_xblock(parent_usage_key=chap_usage_key, category='sequential')
self.seq_usage_key = self.response_usage_key(resp)
self.seq_update_url = reverse_usage_url("xblock_handler", self.seq_usage_key)
resp = self.create_xblock(parent_usage_key=chap_usage_key, category='sequential')
self.seq2_usage_key = self.response_usage_key(resp)
self.seq2_update_url = reverse_usage_url("xblock_handler", self.seq2_usage_key)
# create problem w/ boilerplate
template_id = 'multiplechoice.yaml'
resp = self.create_xblock(parent_usage_key=self.seq_usage_key, category='problem', boilerplate=template_id)
......@@ -557,11 +563,8 @@ class TestEditItem(ItemTest):
self.assertIn(chapter2_usage_key, course.children)
# Remove one child from the course.
resp = self.client.ajax_post(
self.course_update_url,
data={'children': [unicode(chapter2_usage_key)]}
)
self.assertEqual(resp.status_code, 200)
resp = self.client.delete(reverse_usage_url("xblock_handler", chapter1_usage_key))
self.assertEqual(resp.status_code, 204)
# Verify that the child is removed.
course = self.get_item_from_modulestore(self.usage_key)
......@@ -597,6 +600,79 @@ class TestEditItem(ItemTest):
self.assertEqual(unit1_usage_key, children[2])
self.assertEqual(unit2_usage_key, children[1])
def test_move_parented_child(self):
"""
Test moving a child from one Section to another
"""
unit_1_key = self.response_usage_key(
self.create_xblock(parent_usage_key=self.seq_usage_key, category='vertical', display_name='unit 1')
)
unit_2_key = self.response_usage_key(
self.create_xblock(parent_usage_key=self.seq2_usage_key, category='vertical', display_name='unit 2')
)
# move unit 1 from sequential1 to sequential2
resp = self.client.ajax_post(
self.seq2_update_url,
data={'children': [unicode(unit_1_key), unicode(unit_2_key)]}
)
self.assertEqual(resp.status_code, 200)
# verify children
self.assertListEqual(
self.get_item_from_modulestore(self.seq2_usage_key).children,
[unit_1_key, unit_2_key],
)
self.assertListEqual(
self.get_item_from_modulestore(self.seq_usage_key).children,
[self.problem_usage_key], # problem child created in setUp
)
def test_move_orphaned_child_error(self):
"""
Test moving an orphan returns an error
"""
unit_1_key = self.store.create_item(self.user.id, self.course_key, 'vertical', 'unit1').location
# adding orphaned unit 1 should return an error
resp = self.client.ajax_post(
self.seq2_update_url,
data={'children': [unicode(unit_1_key)]}
)
self.assertEqual(resp.status_code, 400)
self.assertIn("Invalid data, possibly caused by concurrent authors", resp.content)
# verify children
self.assertListEqual(
self.get_item_from_modulestore(self.seq2_usage_key).children,
[]
)
def test_move_child_creates_orphan_error(self):
"""
Test creating an orphan returns an error
"""
unit_1_key = self.response_usage_key(
self.create_xblock(parent_usage_key=self.seq2_usage_key, category='vertical', display_name='unit 1')
)
unit_2_key = self.response_usage_key(
self.create_xblock(parent_usage_key=self.seq2_usage_key, category='vertical', display_name='unit 2')
)
# remove unit 2 should return an error
resp = self.client.ajax_post(
self.seq2_update_url,
data={'children': [unicode(unit_1_key)]}
)
self.assertEqual(resp.status_code, 400)
self.assertIn("Invalid data, possibly caused by concurrent authors", resp.content)
# verify children
self.assertListEqual(
self.get_item_from_modulestore(self.seq2_usage_key).children,
[unit_1_key, unit_2_key]
)
def _is_location_published(self, location):
"""
Returns whether or not the item with given location has a published version.
......@@ -954,44 +1030,6 @@ class TestEditSplitModule(ItemTest):
self.assertEqual(2, len(split_test.children))
self.assertEqual(initial_group_id_to_child, split_test.group_id_to_child)
def test_delete_children(self):
"""
Test that deleting a child in the group_id_to_child map updates the map.
Also test that deleting a child not in the group_id_to_child_map behaves properly.
"""
# Set to first group configuration.
self._update_partition_id(0)
split_test = self._assert_children(2)
vertical_1_usage_key = split_test.children[1]
# Add an extra child to the split_test
resp = self.create_xblock(category='html', parent_usage_key=self.split_test_usage_key)
extra_child_usage_key = self.response_usage_key(resp)
self._assert_children(3)
# Remove the first child (which is part of the group configuration).
resp = self.client.ajax_post(
self.split_test_update_url,
data={'children': [unicode(vertical_1_usage_key), unicode(extra_child_usage_key)]}
)
self.assertEqual(resp.status_code, 200)
split_test = self._assert_children(2)
# Check that group_id_to_child was updated appropriately
group_id_to_child = split_test.group_id_to_child
self.assertEqual(1, len(group_id_to_child))
self.assertEqual(vertical_1_usage_key, group_id_to_child['1'])
# Remove the "extra" child and make sure that group_id_to_child did not change.
resp = self.client.ajax_post(
self.split_test_update_url,
data={'children': [unicode(vertical_1_usage_key)]}
)
self.assertEqual(resp.status_code, 200)
split_test = self._assert_children(1)
self.assertEqual(group_id_to_child, split_test.group_id_to_child)
def test_add_groups(self):
"""
Test the "fix up behavior" when groups are missing (after a group is added to a group configuration).
......
......@@ -50,7 +50,8 @@ update_module_store_settings(
module_store_options={
'default_class': 'xmodule.raw_module.RawDescriptor',
'fs_root': TEST_ROOT / "data",
}
},
default_store=os.environ.get('DEFAULT_STORE', 'draft'),
)
CONTENTSTORE = {
......
......@@ -36,6 +36,7 @@ update_module_store_settings(
xml_store_options={
'data_dir': (TEST_ROOT / "data").abspath(),
},
default_store=os.environ.get('DEFAULT_STORE', 'draft'),
)
# Enable django-pipeline and staticfiles
......
......@@ -106,7 +106,7 @@ FEATURES = {
'ADVANCED_SECURITY': False,
# Modulestore to use for new courses
'DEFAULT_STORE_FOR_NEW_COURSE': 'mongo',
'DEFAULT_STORE_FOR_NEW_COURSE': None,
}
ENABLE_JASMINE = False
......
......@@ -323,18 +323,15 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel
}, null, {
clientX: $('#unit-1').offset().left
});
expect(requests.length).toEqual(2);
expect(requests.length).toEqual(1);
expect(this.savingSpies.constructor).toHaveBeenCalled();
expect(this.savingSpies.show).toHaveBeenCalled();
expect(this.savingSpies.hide).not.toHaveBeenCalled();
savingOptions = this.savingSpies.constructor.mostRecentCall.args[0];
expect(savingOptions.title).toMatch(/Saving/);
expect($('#unit-1')).toHaveClass('was-dropped');
expect(requests[0].requestBody).toEqual('{"children":["second-unit-id","third-unit-id"]}');
expect(requests[0].requestBody).toEqual('{"children":["fourth-unit-id","first-unit-id"]}');
requests[0].respond(200);
expect(this.savingSpies.hide).not.toHaveBeenCalled();
expect(requests[1].requestBody).toEqual('{"children":["fourth-unit-id","first-unit-id"]}');
requests[1].respond(200);
expect(this.savingSpies.hide).toHaveBeenCalled();
this.clock.tick(1001);
expect($('#unit-1')).not.toHaveClass('was-dropped');
......
......@@ -288,17 +288,6 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
if (_.isFunction(refresh)) { refresh(collapsed); }
};
// If the parent has changed, update the children of the old parent.
if (newParentLocator !== oldParentLocator) {
// Find the old parent element.
oldParentEle = $(parentSelector).filter(function () {
return $(this).data('locator') === oldParentLocator;
});
this.saveItem(oldParentEle, childrenSelector, function () {
element.data('parent', newParentLocator);
refreshParent(oldParentEle);
});
}
saving = new NotificationView.Mini({
title: gettext('Saving…')
});
......@@ -310,7 +299,18 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
}, 1000);
this.saveItem(newParentEle, childrenSelector, function () {
saving.hide();
// Refresh new parent.
refreshParent(newParentEle);
// Refresh old parent.
if (newParentLocator !== oldParentLocator) {
oldParentEle = $(parentSelector).filter(function () {
return $(this).data('locator') === oldParentLocator;
});
refreshParent(oldParentEle);
element.data('parent', newParentLocator);
}
});
},
......
......@@ -34,6 +34,7 @@ def convert_module_store_setting_if_needed(module_store_setting):
if module_store_setting is None:
return None
# Convert to Mixed, if needed
if module_store_setting['default']['ENGINE'] != 'xmodule.modulestore.mixed.MixedModuleStore':
warnings.warn("Direct access to a modulestore is deprecated. Please use MixedModuleStore.", DeprecationWarning)
......@@ -54,7 +55,8 @@ def convert_module_store_setting_if_needed(module_store_setting):
)
module_store_setting = new_module_store_setting
elif isinstance(module_store_setting['default']['OPTIONS']['stores'], dict):
# Convert from dict, if needed
elif isinstance(get_mixed_stores(module_store_setting), dict):
warnings.warn(
"Using a dict for the Stores option in the MixedModuleStore is deprecated. Please use a list instead.",
DeprecationWarning
......@@ -62,13 +64,13 @@ def convert_module_store_setting_if_needed(module_store_setting):
# convert old-style (unordered) dict to (an ordered) list
module_store_setting['default']['OPTIONS']['stores'] = convert_old_stores_into_list(
module_store_setting['default']['OPTIONS']['stores']
get_mixed_stores(module_store_setting)
)
assert isinstance(get_mixed_stores(module_store_setting), list)
assert isinstance(module_store_setting['default']['OPTIONS']['stores'], list)
# Add Split, if needed
# If Split is not defined but the DraftMongoModuleStore is configured, add Split as a copy of Draft
mixed_stores = module_store_setting['default']['OPTIONS']['stores']
mixed_stores = get_mixed_stores(module_store_setting)
is_split_defined = any((store['ENGINE'].endswith('.DraftVersioningModuleStore')) for store in mixed_stores)
if not is_split_defined:
# find first setting of mongo store
......@@ -95,10 +97,14 @@ def update_module_store_settings(
doc_store_settings=None,
module_store_options=None,
xml_store_options=None,
default_store=None,
):
"""
Updates the settings for each store defined in the given module_store_setting settings
with the given doc store configuration and options, overwriting existing keys.
If default_store is specified, the given default store is moved to the top of the
list of stores.
"""
for store in module_store_setting['default']['OPTIONS']['stores']:
if store['NAME'] == 'xml':
......@@ -106,3 +112,20 @@ def update_module_store_settings(
else:
module_store_options and store['OPTIONS'].update(module_store_options)
doc_store_settings and store['DOC_STORE_CONFIG'].update(doc_store_settings)
if default_store:
mixed_stores = get_mixed_stores(module_store_setting)
for store in mixed_stores:
if store['NAME'] == default_store:
# move the found store to the top of the list
mixed_stores.remove(store)
mixed_stores.insert(0, store)
return
raise Exception("Could not find setting for requested default store: {}".format(default_store))
def get_mixed_stores(mixed_setting):
"""
Helper for accessing stores in a configuration setting for the Mixed modulestore.
"""
return mixed_setting["default"]["OPTIONS"]["stores"]
......@@ -2101,7 +2101,8 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
def _serialize_fields(self, category, fields):
"""
Convert any references to their serialized form.
Convert any references to their serialized form. Handle some references already being unicoded
because the client passed them that way and nothing above this layer did the necessary deserialization.
Remove any fields which split or its kvs computes or adds but does not want persisted.
......@@ -2111,17 +2112,26 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
xblock_class = XBlock.load_class(category, self.default_class)
xblock_class = self.mixologist.mix(xblock_class)
def reference_block_id(reference):
"""
Handle client possibly setting field to strings rather than keys to get the block_id
"""
# perhaps replace by fixing the views or Field Reference*.from_json to return a Key
if isinstance(reference, basestring):
reference = BlockUsageLocator.from_string(reference)
return BlockKey.from_usage_key(reference)
for field_name, value in fields.iteritems():
if value is not None:
if isinstance(xblock_class.fields[field_name], Reference):
fields[field_name] = BlockKey.from_usage_key(value)
fields[field_name] = reference_block_id(value)
elif isinstance(xblock_class.fields[field_name], ReferenceList):
fields[field_name] = [
BlockKey.from_usage_key(ele) for ele in value
reference_block_id(ele) for ele in value
]
elif isinstance(xblock_class.fields[field_name], ReferenceValueDict):
for key, subvalue in value.iteritems():
value[key] = BlockKey.from_usage_key(subvalue)
value[key] = reference_block_id(subvalue)
# should this recurse down dicts and lists just in case they contain datetime?
elif not isinstance(value, datetime.datetime): # don't convert datetimes!
fields[field_name] = xblock_class.fields[field_name].to_json(value)
......
......@@ -2,10 +2,16 @@
Tests for testing the modulestore settings migration code.
"""
import copy
import ddt
from unittest import TestCase
from xmodule.modulestore.modulestore_settings import convert_module_store_setting_if_needed
from xmodule.modulestore.modulestore_settings import (
convert_module_store_setting_if_needed,
update_module_store_settings,
get_mixed_stores,
)
@ddt.ddt
class ModuleStoreSettingsMigration(TestCase):
"""
Tests for the migration code for the module store settings
......@@ -108,12 +114,6 @@ class ModuleStoreSettingsMigration(TestCase):
}
def _get_mixed_stores(self, mixed_setting):
"""
Helper for accessing stores in a configuration setting for the Mixed modulestore.
"""
return mixed_setting["default"]["OPTIONS"]["stores"]
def assertStoreValuesEqual(self, store_setting1, store_setting2):
"""
Tests whether the fields in the given store_settings are equal.
......@@ -134,7 +134,7 @@ class ModuleStoreSettingsMigration(TestCase):
self.assertEqual(new_mixed_setting["default"]["ENGINE"], "xmodule.modulestore.mixed.MixedModuleStore")
# check whether the stores are in an ordered list
new_stores = self._get_mixed_stores(new_mixed_setting)
new_stores = get_mixed_stores(new_mixed_setting)
self.assertIsInstance(new_stores, list)
return new_mixed_setting, new_stores[0]
......@@ -143,7 +143,7 @@ class ModuleStoreSettingsMigration(TestCase):
"""
Tests whether the split module store is configured in the given setting.
"""
stores = self._get_mixed_stores(mixed_setting)
stores = get_mixed_stores(mixed_setting)
split_settings = [store for store in stores if store['ENGINE'].endswith('.DraftVersioningModuleStore')]
if len(split_settings):
# there should only be one setting for split
......@@ -178,8 +178,8 @@ class ModuleStoreSettingsMigration(TestCase):
self.assertTrue(self.is_split_configured(new_mixed_setting))
# exclude split when comparing old and new, since split was added as part of the migration
new_stores = [store for store in self._get_mixed_stores(new_mixed_setting) if store['NAME'] != 'split']
old_stores = self._get_mixed_stores(self.OLD_MIXED_CONFIG_WITH_DICT)
new_stores = [store for store in get_mixed_stores(new_mixed_setting) if store['NAME'] != 'split']
old_stores = get_mixed_stores(self.OLD_MIXED_CONFIG_WITH_DICT)
# compare each store configured in mixed
self.assertEqual(len(new_stores), len(old_stores))
......@@ -192,3 +192,14 @@ class ModuleStoreSettingsMigration(TestCase):
new_mixed_setting, new_default_store_setting = self.assertMigrated(old_mixed_setting)
self.assertTrue(self.is_split_configured(new_mixed_setting))
self.assertEquals(old_mixed_setting, new_mixed_setting)
@ddt.data('draft', 'split')
def test_update_settings(self, default_store):
mixed_setting = self.ALREADY_UPDATED_MIXED_CONFIG
update_module_store_settings(mixed_setting, default_store=default_store)
self.assertTrue(get_mixed_stores(mixed_setting)[0]['NAME'] == default_store)
def test_update_settings_error(self):
mixed_setting = self.ALREADY_UPDATED_MIXED_CONFIG
with self.assertRaises(Exception):
update_module_store_settings(mixed_setting, default_store='non-existent store')
......@@ -11,6 +11,7 @@ from textwrap import dedent
from collections import namedtuple
from path import path
from lazy import lazy
from opaque_keys.edx.keys import CourseKey
from . import STUDIO_BASE_URL
......@@ -204,6 +205,7 @@ class CourseFixture(StudioApiFixture):
self.children = []
self._assets = []
self._advanced_settings = {}
self._course_key = None
def __str__(self):
"""
......@@ -264,18 +266,16 @@ class CourseFixture(StudioApiFixture):
return self
@property
def _course_key(self):
"""
Return the locator string for the course.
"""
return "{org}/{number}/{run}".format(**self._course_dict)
@property
def _course_location(self):
"""
Return the locator string for the course.
"""
return "i4x://{org}/{number}/course/{run}".format(**self._course_dict)
course_key = CourseKey.from_string(self._course_key)
if getattr(course_key, 'deprecated', False):
block_id = self._course_dict['run']
else:
block_id = 'course'
return unicode(course_key.make_usage_key('course', block_id))
@property
def _assets_url(self):
......@@ -289,7 +289,8 @@ class CourseFixture(StudioApiFixture):
"""
Return the locator string for the course handouts
"""
return "i4x://{org}/{number}/course_info/handouts".format(**self._course_dict)
course_key = CourseKey.from_string(self._course_key)
return unicode(course_key.make_usage_key('course_info', 'handouts'))
def _create_course(self):
"""
......@@ -315,7 +316,9 @@ class CourseFixture(StudioApiFixture):
if err is not None:
raise CourseFixtureError("Could not create course {0}. Error message: '{1}'".format(self, err))
if not response.ok:
if response.ok:
self._course_key = response.json()['course_key']
else:
raise CourseFixtureError(
"Could not create course {0}. Status was {1}".format(
self._course_dict, response.status_code))
......
......@@ -5,8 +5,10 @@ import json
import unittest
import functools
import requests
import os
from path import path
from bok_choy.web_app_test import WebAppTest
from opaque_keys.edx.locator import CourseLocator
def skip_if_browser(browser):
......@@ -170,8 +172,6 @@ class UniqueCourseTest(WebAppTest):
Test that provides a unique course ID.
"""
COURSE_ID_SEPARATOR = "/"
def __init__(self, *args, **kwargs):
"""
Create a unique course ID.
......@@ -190,11 +190,18 @@ class UniqueCourseTest(WebAppTest):
@property
def course_id(self):
return self.COURSE_ID_SEPARATOR.join([
"""
Returns the serialized course_key for the test
"""
# TODO - is there a better way to make this agnostic to the underlying default module store?
default_store = os.environ.get('DEFAULT_STORE', 'draft')
course_key = CourseLocator(
self.course_info['org'],
self.course_info['number'],
self.course_info['run']
])
self.course_info['run'],
deprecated=(default_store == 'draft')
)
return unicode(course_key)
class YouTubeConfigError(Exception):
......
......@@ -274,6 +274,14 @@ To put a debugging breakpoint in a test use:
from nose.tools import set_trace; set_trace()
By default, all bokchoy tests are run with the 'split' ModuleStore.
To override the modulestore that is used, use the default_store option. The currently supported stores are:
'split' (xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore) and
'draft' (xmodule.modulestore.mongo.DraftMongoModuleStore).
For example:
paver test_bokchoy --default_store='draft'
### Running Lettuce Acceptance Tests
......@@ -309,6 +317,14 @@ To start the debugger on failure, add the `--pdb` option to extra_args:
To run tests faster by not collecting static files, you can use
`paver test_acceptance -s lms --fasttest` and `paver test_acceptance -s cms --fasttest`.
By default, all acceptance tests are run with the 'draft' ModuleStore.
To override the modulestore that is used, use the default_store option. Currently, the possible stores for acceptance tests are:
'split' (xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore) and
'draft' (xmodule.modulestore.mongo.DraftMongoModuleStore).
For example:
paver test_acceptance --default_store='draft'
Note, however, all acceptance tests currently do not pass with 'split'.
Acceptance tests will run on a randomized port and can be run in the background of paver cms and lms or unit tests.
To specify the port, change the LETTUCE_SERVER_PORT constant in cms/envs/acceptance.py and lms/envs/acceptance.py
as well as the port listed in cms/djangoapps/contentstore/feature/upload.py
......
......@@ -50,7 +50,8 @@ update_module_store_settings(
},
module_store_options={
'fs_root': TEST_ROOT / "data",
}
},
default_store=os.environ.get('DEFAULT_STORE', 'draft'),
)
CONTENTSTORE = {
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
......
......@@ -39,6 +39,7 @@ update_module_store_settings(
xml_store_options={
'data_dir': (TEST_ROOT / "data").abspath(),
},
default_store=os.environ.get('DEFAULT_STORE', 'draft'),
)
# Configure the LMS to use our stub XQueue implementation
......
......@@ -102,7 +102,7 @@
location.href = xhr.responseText;
}
} else if (xhr.status == 403) {
location.href = "${reverse('register_user')}?course_id=${course.id.to_deprecated_string()}&enrollment_action=enroll";
location.href = "${reverse('register_user')}?course_id=${course.id | u}&enrollment_action=enroll";
} else {
$('#register_error').html(
(xhr.responseText ? xhr.responseText : 'An error occurred. Please try again later.')
......
......@@ -19,6 +19,7 @@ __test__ = False # do not collect
)
@cmdopts([
("system=", "s", "System to act on"),
("default_store=", "m", "Default modulestore to use for course creation"),
("fasttest", "a", "Run without collectstatic"),
("extra_args=", "e", "adds as extra args to the test command"),
make_option("--verbose", action="store_const", const=2, dest="verbosity"),
......@@ -32,6 +33,7 @@ def test_acceptance(options):
opts = {
'fasttest': getattr(options, 'fasttest', False),
'system': getattr(options, 'system', None),
'default_store': getattr(options, 'default_store', None),
'verbosity': getattr(options, 'verbosity', 3),
'extra_args': getattr(options, 'extra_args', ''),
}
......@@ -42,6 +44,12 @@ def test_acceptance(options):
'No system specified, running tests for both cms and lms.'
)
print(msg)
if opts['default_store'] not in ['draft', 'split']:
msg = colorize(
'red',
'No modulestore specified, running tests for both draft and split.'
)
print(msg)
suite = AcceptanceTestSuite('{} acceptance'.format(opts['system']), **opts)
suite.run()
......@@ -22,6 +22,7 @@ __test__ = False # do not collect
('test_spec=', 't', 'Specific test to run'),
('fasttest', 'a', 'Skip some setup'),
('extra_args=', 'e', 'adds as extra args to the test command'),
('default_store=', 's', 'Default modulestore'),
make_option("--verbose", action="store_const", const=2, dest="verbosity"),
make_option("-q", "--quiet", action="store_const", const=0, dest="verbosity"),
make_option("-v", "--verbosity", action="count", dest="verbosity"),
......@@ -45,13 +46,12 @@ def test_bokchoy(options):
opts = {
'test_spec': getattr(options, 'test_spec', None),
'fasttest': getattr(options, 'fasttest', False),
'default_store': getattr(options, 'default_store', None),
'verbosity': getattr(options, 'verbosity', 2),
'extra_args': getattr(options, 'extra_args', ''),
'test_dir': 'tests',
}
test_suite = BokChoyTestSuite('bok-choy', **opts)
test_suite.run()
run_bokchoy(**opts)
@task
......@@ -60,6 +60,7 @@ def test_bokchoy(options):
('test_spec=', 't', 'Specific test to run'),
('fasttest', 'a', 'Skip some setup'),
('imports_dir=', 'd', 'Directory containing (un-archived) courses to be imported'),
('default_store=', 's', 'Default modulestore'),
make_option("--verbose", action="store_const", const=2, dest="verbosity"),
make_option("-q", "--quiet", action="store_const", const=0, dest="verbosity"),
make_option("-v", "--verbosity", action="count", dest="verbosity"),
......@@ -72,13 +73,33 @@ def perf_report_bokchoy(options):
'test_spec': getattr(options, 'test_spec', None),
'fasttest': getattr(options, 'fasttest', False),
'imports_dir': getattr(options, 'imports_dir', None),
'default_store': getattr(options, 'default_store', None),
'verbosity': getattr(options, 'verbosity', 2),
'test_dir': 'performance',
'ptests': True,
}
run_bokchoy(**opts)
def run_bokchoy(**opts):
"""
Runs BokChoyTestSuite with the given options.
If a default store is not specified, runs the test suite for 'split' as the default store.
"""
if opts['default_store'] not in ['draft', 'split']:
msg = colorize(
'red',
'No modulestore specified, running tests for split.'
)
print(msg)
stores = ['split']
else:
stores = [opts['default_store']]
test_suite = BokChoyTestSuite('bok-choy', **opts)
test_suite.run()
for store in stores:
opts['default_store'] = store
test_suite = BokChoyTestSuite('bok-choy', **opts)
test_suite.run()
@task
......
......@@ -18,7 +18,7 @@ except ImportError:
__test__ = False # do not collect
def start_servers():
def start_servers(default_store):
"""
Start the servers we will run tests on, returns PIDs for servers.
"""
......@@ -33,9 +33,11 @@ def start_servers():
for service, info in Env.BOK_CHOY_SERVERS.iteritems():
address = "0.0.0.0:{}".format(info['port'])
cmd = (
"DEFAULT_STORE={default_store} "
"coverage run --rcfile={coveragerc} -m "
"manage {service} --settings bok_choy runserver "
"{address} --traceback --noreload".format(
default_store=default_store,
coveragerc=Env.BOK_CHOY_COVERAGERC,
service=service,
address=address,
......
......@@ -17,7 +17,8 @@ class AcceptanceTest(TestSuite):
super(AcceptanceTest, self).__init__(*args, **kwargs)
self.report_dir = Env.REPORT_DIR / 'acceptance'
self.fasttest = kwargs.get('fasttest', False)
self.system = kwargs.get('system', None)
self.system = kwargs.get('system')
self.default_store = kwargs.get('default_store')
self.extra_args = kwargs.get('extra_args', '')
def __enter__(self):
......@@ -35,9 +36,10 @@ class AcceptanceTest(TestSuite):
report_file = self.report_dir / "{}.xml".format(self.system)
report_args = "--with-xunit --xunit-file {}".format(report_file)
cmd = (
"./manage.py {system} --settings acceptance harvest --traceback "
cmd = (
"DEFAULT_STORE={default_store} ./manage.py {system} --settings acceptance harvest --traceback "
"--debug-mode --verbosity {verbosity} {report_args} {extra_args}".format(
default_store=self.default_store,
system=self.system,
verbosity=self.verbosity,
report_args=report_args,
......@@ -65,24 +67,31 @@ class AcceptanceTestSuite(TestSuite):
self.root = 'acceptance'
self.db = Env.REPO_ROOT / 'test_root/db/test_edx.db'
self.db_cache = Env.REPO_ROOT / 'common/test/db_cache/lettuce.db'
self.system = kwargs.get('system', None)
self.fasttest = kwargs.get('fasttest', False)
if self.system:
self.subsuites = [
AcceptanceTest('{} acceptance'.format(self.system), **kwargs),
]
if kwargs.get('system'):
systems = [kwargs['system']]
else:
kwargs['system'] = 'lms'
lms = AcceptanceTest('lms acceptance', **kwargs)
kwargs['system'] = 'cms'
cms = AcceptanceTest('cms acceptance', **kwargs)
self.subsuites = [lms, cms]
systems = ['lms', 'cms']
if kwargs.get('default_store'):
stores = [kwargs['default_store']]
else:
# TODO fix Acceptance tests with Split (LMS-11300)
# stores = ['split', 'draft']
stores = ['draft']
self.subsuites = []
for system in systems:
for default_store in stores:
kwargs['system'] = system
kwargs['default_store'] = default_store
self.subsuites.append(AcceptanceTest('{} acceptance using {}'.format(system, default_store), **kwargs))
def __enter__(self):
super(AcceptanceTestSuite, self).__enter__()
test_utils.clean_test_files()
test_utils.clean_test_files()
if not self.fasttest:
self._setup_acceptance_db()
......@@ -104,7 +113,7 @@ class AcceptanceTestSuite(TestSuite):
if self.db.isfile():
# Since we are using SQLLite, we can reset the database by deleting it on disk.
self.db.remove()
if self.db_cache.isfile():
# To speed up migrations, we check for a cached database file and start from that.
# The cached database file should be checked into the repo
......
......@@ -28,6 +28,7 @@ class BokChoyTestSuite(TestSuite):
self.cache = Env.BOK_CHOY_CACHE
self.fasttest = kwargs.get('fasttest', False)
self.test_spec = kwargs.get('test_spec', None)
self.default_store = kwargs.get('default_store')
self.verbosity = kwargs.get('verbosity', 2)
self.extra_args = kwargs.get('extra_args', '')
self.ptests = kwargs.get('ptests', False)
......@@ -64,17 +65,26 @@ class BokChoyTestSuite(TestSuite):
self.cache.flush_all()
sh(
"./manage.py lms --settings bok_choy loaddata --traceback"
" common/test/db_fixtures/*.json"
"DEFAULT_STORE={default_store}"
" ./manage.py lms --settings bok_choy loaddata --traceback"
" common/test/db_fixtures/*.json".format(
default_store=self.default_store,
)
)
if self.imports_dir:
sh("./manage.py cms --settings=bok_choy import {}".format(self.imports_dir))
sh(
"DEFAULT_STORE={default_store}"
" ./manage.py cms --settings=bok_choy import {import_dir}".format(
default_store=self.default_store,
import_dir=self.imports_dir
)
)
# Ensure the test servers are available
msg = colorize('green', "Starting test servers...")
print(msg)
bokchoy_utils.start_servers()
bokchoy_utils.start_servers(self.default_store)
msg = colorize('green', "Waiting for servers to start...")
print(msg)
......@@ -101,6 +111,7 @@ class BokChoyTestSuite(TestSuite):
# Construct the nosetests command, specifying where to save
# screenshots and XUnit XML reports
cmd = [
"DEFAULT_STORE={}".format(self.default_store),
"SCREENSHOT_DIR='{}'".format(self.log_dir),
"HAR_DIR='{}'".format(self.har_dir),
"SELENIUM_DRIVER_LOG_DIR='{}'".format(self.log_dir),
......
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