Refactor instructor_task tests, and add handling for general errors in bulk_email subtasks.
This commit is contained in:
@@ -166,7 +166,7 @@ def perform_delegate_email_batches(entry_id, course_id, task_input, action_name)
|
||||
to_list = recipient_sublist[i * chunk:i * chunk + chunk]
|
||||
subtask_id = str(uuid4())
|
||||
subtask_id_list.append(subtask_id)
|
||||
subtask_progress = _course_email_result(None, 0, 0, 0)
|
||||
subtask_progress = update_subtask_result(None, 0, 0, 0)
|
||||
task_list.append(send_course_email.subtask((
|
||||
entry_id,
|
||||
email_id,
|
||||
@@ -259,14 +259,14 @@ def _update_subtask_status(entry_id, current_task_id, status, subtask_result):
|
||||
|
||||
log.info("Task output updated to %s for email subtask %s of instructor task %d",
|
||||
entry.task_output, current_task_id, entry_id)
|
||||
# TODO: temporary -- switch to debug
|
||||
# TODO: temporary -- switch to debug once working
|
||||
log.info("about to save....")
|
||||
entry.save()
|
||||
except:
|
||||
log.exception("Unexpected error while updating InstructorTask.")
|
||||
transaction.rollback()
|
||||
else:
|
||||
# TODO: temporary -- switch to debug
|
||||
# TODO: temporary -- switch to debug once working
|
||||
log.info("about to commit....")
|
||||
transaction.commit()
|
||||
|
||||
@@ -289,40 +289,69 @@ def send_course_email(entry_id, email_id, to_list, global_email_context, subtask
|
||||
current_task_id = _get_current_task().request.id
|
||||
retry_index = _get_current_task().request.retries
|
||||
|
||||
log.info("Preparing to send email as subtask %s for instructor task %d",
|
||||
current_task_id, entry_id)
|
||||
log.info("Preparing to send email as subtask %s for instructor task %d, retry %d",
|
||||
current_task_id, entry_id, retry_index)
|
||||
|
||||
try:
|
||||
course_title = global_email_context['course_title']
|
||||
course_email_result_value = None
|
||||
send_exception = None
|
||||
with dog_stats_api.timer('course_email.single_task.time.overall', tags=[_statsd_tag(course_title)]):
|
||||
course_email_result_value = _send_course_email(email_id, to_list, global_email_context, subtask_progress, retry_index)
|
||||
# Assume that if we get here without a raise, the task was successful.
|
||||
# Update the InstructorTask object that is storing its progress.
|
||||
_update_subtask_status(entry_id, current_task_id, SUCCESS, course_email_result_value)
|
||||
course_email_result_value, send_exception = _send_course_email(
|
||||
current_task_id,
|
||||
email_id,
|
||||
to_list,
|
||||
global_email_context,
|
||||
subtask_progress,
|
||||
retry_index,
|
||||
)
|
||||
if send_exception is None:
|
||||
# Update the InstructorTask object that is storing its progress.
|
||||
_update_subtask_status(entry_id, current_task_id, SUCCESS, course_email_result_value)
|
||||
else:
|
||||
log.error("background task (%s) failed: %s", current_task_id, send_exception)
|
||||
_update_subtask_status(entry_id, current_task_id, FAILURE, course_email_result_value)
|
||||
raise send_exception
|
||||
|
||||
except Exception:
|
||||
# try to write out the failure to the entry before failing
|
||||
_, exception, traceback = exc_info()
|
||||
traceback_string = format_exc(traceback) if traceback is not None else ''
|
||||
log.warning("background task (%s) failed: %s %s", current_task_id, exception, traceback_string)
|
||||
log.error("background task (%s) failed: %s %s", current_task_id, exception, traceback_string)
|
||||
_update_subtask_status(entry_id, current_task_id, FAILURE, subtask_progress)
|
||||
raise
|
||||
|
||||
return course_email_result_value
|
||||
|
||||
|
||||
def _send_course_email(email_id, to_list, global_email_context, subtask_progress, retry_index):
|
||||
def _send_course_email(task_id, email_id, to_list, global_email_context, subtask_progress, retry_index):
|
||||
"""
|
||||
Performs the email sending task.
|
||||
|
||||
Returns a tuple of two values:
|
||||
* First value is a dict which represents current progress. Keys are:
|
||||
|
||||
'attempted': number of emails attempted
|
||||
'succeeded': number of emails succeeded
|
||||
'skipped': number of emails skipped (due to optout)
|
||||
'failed': number of emails not sent because of some failure
|
||||
|
||||
* Second value is an exception returned by the innards of the method, indicating a fatal error.
|
||||
In this case, the number of recipients that were not sent have already been added to the
|
||||
'failed' count above.
|
||||
"""
|
||||
throttle = retry_index > 0
|
||||
|
||||
num_optout = 0
|
||||
num_sent = 0
|
||||
num_error = 0
|
||||
|
||||
try:
|
||||
course_email = CourseEmail.objects.get(id=email_id)
|
||||
except CourseEmail.DoesNotExist:
|
||||
log.exception("Could not find email id:{} to send.".format(email_id))
|
||||
raise
|
||||
except CourseEmail.DoesNotExist as exc:
|
||||
log.exception("Task %s: could not find email id:%s to send.", task_id, email_id)
|
||||
num_error += len(to_list)
|
||||
return update_subtask_result(subtask_progress, num_sent, num_error, num_optout), exc
|
||||
|
||||
# exclude optouts (if not a retry):
|
||||
# Note that we don't have to do the optout logic at all if this is a retry,
|
||||
@@ -330,7 +359,6 @@ def _send_course_email(email_id, to_list, global_email_context, subtask_progress
|
||||
# attempt. Anyone on the to_list on a retry has already passed the filter
|
||||
# that existed at that time, and we don't need to keep checking for changes
|
||||
# in the Optout list.
|
||||
num_optout = 0
|
||||
if retry_index == 0:
|
||||
optouts = (Optout.objects.filter(course_id=course_email.course_id,
|
||||
user__in=[i['pk'] for i in to_list])
|
||||
@@ -359,8 +387,6 @@ def _send_course_email(email_id, to_list, global_email_context, subtask_progress
|
||||
|
||||
course_email_template = CourseEmailTemplate.get_template()
|
||||
|
||||
num_sent = 0
|
||||
num_error = 0
|
||||
try:
|
||||
connection = get_connection()
|
||||
connection.open()
|
||||
@@ -413,45 +439,47 @@ def _send_course_email(email_id, to_list, global_email_context, subtask_progress
|
||||
raise exc
|
||||
else:
|
||||
# This will fall through and not retry the message, since it will be popped
|
||||
log.warning('Email with id %s not delivered to %s due to error %s', email_id, email, exc.smtp_error)
|
||||
|
||||
log.warning('Task %s: email with id %s not delivered to %s due to error %s', task_id, email_id, email, exc.smtp_error)
|
||||
dog_stats_api.increment('course_email.error', tags=[_statsd_tag(course_title)])
|
||||
|
||||
num_error += 1
|
||||
|
||||
to_list.pop()
|
||||
|
||||
except (SMTPDataError, SMTPConnectError, SMTPServerDisconnected) as exc:
|
||||
# Error caught here cause the email to be retried. The entire task is actually retried without popping the list
|
||||
# Reasoning is that all of these errors may be temporary condition.
|
||||
# TODO: figure out what this means. Presumably we have popped the list with those that have succeeded
|
||||
# and failed, rather than those needing a later retry.
|
||||
log.warning('Email with id %d not delivered due to temporary error %s, retrying send to %d recipients',
|
||||
email_id, exc, len(to_list))
|
||||
# Errors caught here cause the email to be retried. The entire task is actually retried
|
||||
# without popping the current recipient off of the existing list.
|
||||
# Errors caught are those that indicate a temporary condition that might succeed on retry.
|
||||
connection.close()
|
||||
log.warning('Task %s: email with id %d not delivered due to temporary error %s, retrying send to %d recipients',
|
||||
task_id, email_id, exc, len(to_list))
|
||||
raise send_course_email.retry(
|
||||
arg=[
|
||||
email_id,
|
||||
to_list,
|
||||
global_email_context,
|
||||
_course_email_result(subtask_progress, num_sent, num_error, num_optout),
|
||||
update_subtask_result(subtask_progress, num_sent, num_error, num_optout),
|
||||
],
|
||||
exc=exc,
|
||||
countdown=(2 ** retry_index) * 15
|
||||
)
|
||||
except:
|
||||
log.exception('Email with id %d caused send_course_email task to fail with uncaught exception. To list: %s',
|
||||
email_id,
|
||||
[i['email'] for i in to_list])
|
||||
# Close the connection before we exit
|
||||
except Exception as exc:
|
||||
|
||||
# If we have a general exception for this request, we need to figure out what to do with it.
|
||||
# If we're going to just mark it as failed
|
||||
# And the log message below should indicate which task_id is failing, so we have a chance to
|
||||
# reconstruct the problems.
|
||||
connection.close()
|
||||
raise
|
||||
log.exception('Task %s: email with id %d caused send_course_email task to fail with uncaught exception. To list: %s',
|
||||
task_id, email_id, [i['email'] for i in to_list])
|
||||
num_error += len(to_list)
|
||||
return update_subtask_result(subtask_progress, num_sent, num_error, num_optout), exc
|
||||
else:
|
||||
connection.close()
|
||||
# Add current progress to any progress stemming from previous retries:
|
||||
return _course_email_result(subtask_progress, num_sent, num_error, num_optout)
|
||||
connection.close()
|
||||
return update_subtask_result(subtask_progress, num_sent, num_error, num_optout), None
|
||||
|
||||
|
||||
def _course_email_result(previous_result, new_num_sent, new_num_error, new_num_optout):
|
||||
def update_subtask_result(previous_result, new_num_sent, new_num_error, new_num_optout):
|
||||
"""Return the result of course_email sending as a dict (not a string)."""
|
||||
attempted = new_num_sent + new_num_error
|
||||
current_result = {'attempted': attempted, 'succeeded': new_num_sent, 'skipped': new_num_optout, 'failed': new_num_error}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
"""
|
||||
Unit tests for sending course email
|
||||
"""
|
||||
from mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -12,13 +14,7 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from student.tests.factories import UserFactory, GroupFactory, CourseEnrollmentFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from instructor_task.models import InstructorTask
|
||||
from instructor_task.tests.factories import InstructorTaskFactory
|
||||
|
||||
from bulk_email.tasks import send_course_email
|
||||
from bulk_email.models import CourseEmail, Optout
|
||||
|
||||
from mock import patch
|
||||
from bulk_email.models import Optout
|
||||
|
||||
STAFF_COUNT = 3
|
||||
STUDENT_COUNT = 10
|
||||
@@ -32,13 +28,13 @@ class MockCourseEmailResult(object):
|
||||
"""
|
||||
emails_sent = 0
|
||||
|
||||
def get_mock_course_email_result(self):
|
||||
def get_mock_update_subtask_result(self):
|
||||
"""Wrapper for mock email function."""
|
||||
def mock_course_email_result(prev_results, sent, failed, output, **kwargs): # pylint: disable=W0613
|
||||
def mock_update_subtask_result(prev_results, sent, failed, output, **kwargs): # pylint: disable=W0613
|
||||
"""Increments count of number of emails sent."""
|
||||
self.emails_sent += sent
|
||||
return True
|
||||
return mock_course_email_result
|
||||
return mock_update_subtask_result
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
@@ -247,13 +243,13 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase):
|
||||
)
|
||||
|
||||
@override_settings(EMAILS_PER_TASK=3, EMAILS_PER_QUERY=7)
|
||||
@patch('bulk_email.tasks._course_email_result')
|
||||
@patch('bulk_email.tasks.update_subtask_result')
|
||||
def test_chunked_queries_send_numerous_emails(self, email_mock):
|
||||
"""
|
||||
Test sending a large number of emails, to test the chunked querying
|
||||
"""
|
||||
mock_factory = MockCourseEmailResult()
|
||||
email_mock.side_effect = mock_factory.get_mock_course_email_result()
|
||||
email_mock.side_effect = mock_factory.get_mock_update_subtask_result()
|
||||
added_users = []
|
||||
for _ in xrange(LARGE_NUM_EMAILS):
|
||||
user = UserFactory()
|
||||
@@ -283,24 +279,3 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase):
|
||||
[s.email for s in self.students] +
|
||||
[s.email for s in added_users if s not in optouts])
|
||||
self.assertItemsEqual(outbox_contents, should_send_contents)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class TestEmailSendExceptions(ModuleStoreTestCase):
|
||||
"""
|
||||
Test that exceptions are handled correctly.
|
||||
"""
|
||||
def test_no_instructor_task(self):
|
||||
with self.assertRaises(InstructorTask.DoesNotExist):
|
||||
send_course_email(100, 101, [], {}, False)
|
||||
|
||||
def test_no_course_title(self):
|
||||
entry = InstructorTaskFactory.create(task_key='', task_id='dummy')
|
||||
with self.assertRaises(KeyError):
|
||||
send_course_email(entry.id, 101, [], {}, False)
|
||||
|
||||
def test_no_course_email_obj(self):
|
||||
# Make sure send_course_email handles CourseEmail.DoesNotExist exception.
|
||||
entry = InstructorTaskFactory.create(task_key='', task_id='dummy')
|
||||
with self.assertRaises(CourseEmail.DoesNotExist):
|
||||
send_course_email(entry.id, 101, [], {'course_title': 'Test'}, False)
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
Unit tests for handling email sending errors
|
||||
"""
|
||||
from itertools import cycle
|
||||
from mock import patch, Mock
|
||||
from smtplib import SMTPDataError, SMTPServerDisconnected, SMTPConnectError
|
||||
from unittest import skip
|
||||
|
||||
from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -16,9 +21,6 @@ from bulk_email.models import CourseEmail
|
||||
from bulk_email.tasks import perform_delegate_email_batches
|
||||
from instructor_task.models import InstructorTask
|
||||
|
||||
from mock import patch, Mock
|
||||
from smtplib import SMTPDataError, SMTPServerDisconnected, SMTPConnectError
|
||||
|
||||
|
||||
class EmailTestException(Exception):
|
||||
"""Mock exception for email testing."""
|
||||
@@ -65,14 +67,15 @@ class TestEmailErrors(ModuleStoreTestCase):
|
||||
self.assertIsInstance(exc, SMTPDataError)
|
||||
|
||||
@patch('bulk_email.tasks.get_connection', autospec=True)
|
||||
@patch('bulk_email.tasks.course_email_result')
|
||||
@patch('bulk_email.tasks.update_subtask_result')
|
||||
@patch('bulk_email.tasks.send_course_email.retry')
|
||||
def test_data_err_fail(self, retry, result, get_conn):
|
||||
"""
|
||||
Test that celery handles permanent SMTPDataErrors by failing and not retrying.
|
||||
"""
|
||||
# have every fourth email fail due to blacklisting:
|
||||
get_conn.return_value.send_messages.side_effect = cycle([SMTPDataError(554, "Email address is blacklisted"),
|
||||
None])
|
||||
None, None, None])
|
||||
students = [UserFactory() for _ in xrange(settings.EMAILS_PER_TASK)]
|
||||
for student in students:
|
||||
CourseEnrollmentFactory.create(user=student, course_id=self.course.id)
|
||||
@@ -88,10 +91,10 @@ class TestEmailErrors(ModuleStoreTestCase):
|
||||
# We shouldn't retry when hitting a 5xx error
|
||||
self.assertFalse(retry.called)
|
||||
# Test that after the rejected email, the rest still successfully send
|
||||
((sent, fail, optouts), _) = result.call_args
|
||||
((_, sent, fail, optouts), _) = result.call_args
|
||||
self.assertEquals(optouts, 0)
|
||||
self.assertEquals(fail, settings.EMAILS_PER_TASK / 2)
|
||||
self.assertEquals(sent, settings.EMAILS_PER_TASK / 2)
|
||||
self.assertEquals(fail, settings.EMAILS_PER_TASK / 4)
|
||||
self.assertEquals(sent, 3 * settings.EMAILS_PER_TASK / 4)
|
||||
|
||||
@patch('bulk_email.tasks.get_connection', autospec=True)
|
||||
@patch('bulk_email.tasks.send_course_email.retry')
|
||||
@@ -134,10 +137,11 @@ class TestEmailErrors(ModuleStoreTestCase):
|
||||
exc = kwargs['exc']
|
||||
self.assertIsInstance(exc, SMTPConnectError)
|
||||
|
||||
@patch('bulk_email.tasks.course_email_result')
|
||||
@patch('bulk_email.tasks.update_subtask_result')
|
||||
@patch('bulk_email.tasks.send_course_email.retry')
|
||||
@patch('bulk_email.tasks.log')
|
||||
@patch('bulk_email.tasks.get_connection', Mock(return_value=EmailTestException))
|
||||
@skip
|
||||
def test_general_exception(self, mock_log, retry, result):
|
||||
"""
|
||||
Tests the if the error is not SMTP-related, we log and reraise
|
||||
@@ -148,19 +152,29 @@ class TestEmailErrors(ModuleStoreTestCase):
|
||||
'subject': 'test subject for myself',
|
||||
'message': 'test message for myself'
|
||||
}
|
||||
# TODO: This whole test is flawed. Figure out how to make it work correctly,
|
||||
# possibly moving it elsewhere. It's hitting the wrong exception.
|
||||
# For some reason (probably the weirdness of testing with celery tasks) assertRaises doesn't work here
|
||||
# so we assert on the arguments of log.exception
|
||||
# TODO: This is way too fragile, because if any additional log statement is added anywhere in the flow,
|
||||
# this test will break.
|
||||
self.client.post(self.url, test_email)
|
||||
((log_str, email_id, to_list), _) = mock_log.exception.call_args
|
||||
# ((log_str, email_id, to_list), _) = mock_log.exception.call_args
|
||||
# instead, use call_args_list[-1] to get the last call?
|
||||
self.assertTrue(mock_log.exception.called)
|
||||
self.assertIn('caused send_course_email task to fail with uncaught exception.', log_str)
|
||||
self.assertEqual(email_id, 1)
|
||||
self.assertEqual(to_list, [self.instructor.email])
|
||||
# self.assertIn('caused send_course_email task to fail with uncaught exception.', log_str)
|
||||
# self.assertEqual(email_id, 1)
|
||||
# self.assertEqual(to_list, [self.instructor.email])
|
||||
self.assertFalse(retry.called)
|
||||
self.assertFalse(result.called)
|
||||
# TODO: cannot use the result method to determine if a result was generated,
|
||||
# because we now call the particular method as part of all subtask calls.
|
||||
# So use result.called_count to track this...
|
||||
# self.assertFalse(result.called)
|
||||
# call_args_list = result.call_args_list
|
||||
num_calls = result.called_count
|
||||
self.assertTrue(num_calls == 2)
|
||||
|
||||
@patch('bulk_email.tasks.course_email_result')
|
||||
# @patch('bulk_email.tasks.delegate_email_batches.retry')
|
||||
@patch('bulk_email.tasks.update_subtask_result')
|
||||
@patch('bulk_email.tasks.log')
|
||||
def test_nonexist_email(self, mock_log, result):
|
||||
"""
|
||||
|
||||
@@ -190,7 +190,6 @@ def submit_bulk_course_email(request, course_id, email_id):
|
||||
"""
|
||||
# check arguments: make sure that the course is defined?
|
||||
# TODO: what is the right test here?
|
||||
# modulestore().get_instance(course_id, problem_url)
|
||||
|
||||
# This should also make sure that the email exists.
|
||||
# We can also pull out the To argument here, so that is displayed in
|
||||
@@ -200,10 +199,10 @@ def submit_bulk_course_email(request, course_id, email_id):
|
||||
|
||||
task_type = 'bulk_course_email'
|
||||
task_class = send_bulk_course_email
|
||||
# TODO: figure out if we need to encode in a standard way, or if we can get away
|
||||
# with doing this manually. Shouldn't be hard to make the encode call explicitly,
|
||||
# and allow no problem_url or student to be defined. Like this:
|
||||
# task_input, task_key = encode_problem_and_student_input()
|
||||
# Pass in the to_option as a separate argument, even though it's (currently)
|
||||
# in the CourseEmail. That way it's visible in the progress status.
|
||||
# (At some point in the future, we might take the recipient out of the CourseEmail,
|
||||
# so that the same saved email can be sent to different recipients, as it is tested.)
|
||||
task_input = {'email_id': email_id, 'to_option': to_option}
|
||||
task_key_stub = "{email_id}_{to_option}".format(email_id=email_id, to_option=to_option)
|
||||
# create the key value by using MD5 hash:
|
||||
|
||||
@@ -6,16 +6,21 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from courseware.tests.factories import UserFactory
|
||||
|
||||
from instructor_task.api import (get_running_instructor_tasks,
|
||||
get_instructor_task_history,
|
||||
submit_rescore_problem_for_all_students,
|
||||
submit_rescore_problem_for_student,
|
||||
submit_reset_problem_attempts_for_all_students,
|
||||
submit_delete_problem_state_for_all_students)
|
||||
from bulk_email.models import CourseEmail, SEND_TO_ALL
|
||||
from instructor_task.api import (
|
||||
get_running_instructor_tasks,
|
||||
get_instructor_task_history,
|
||||
submit_rescore_problem_for_all_students,
|
||||
submit_rescore_problem_for_student,
|
||||
submit_reset_problem_attempts_for_all_students,
|
||||
submit_delete_problem_state_for_all_students,
|
||||
submit_bulk_course_email,
|
||||
)
|
||||
|
||||
from instructor_task.api_helper import AlreadyRunningError
|
||||
from instructor_task.models import InstructorTask, PROGRESS
|
||||
from instructor_task.tests.test_base import (InstructorTaskTestCase,
|
||||
InstructorTaskCourseTestCase,
|
||||
InstructorTaskModuleTestCase,
|
||||
TEST_COURSE_ID)
|
||||
|
||||
@@ -46,8 +51,8 @@ class InstructorTaskReportTest(InstructorTaskTestCase):
|
||||
self.assertEquals(set(task_ids), set(expected_ids))
|
||||
|
||||
|
||||
class InstructorTaskSubmitTest(InstructorTaskModuleTestCase):
|
||||
"""Tests API methods that involve the submission of background tasks."""
|
||||
class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase):
|
||||
"""Tests API methods that involve the submission of module-based background tasks."""
|
||||
|
||||
def setUp(self):
|
||||
self.initialize_course()
|
||||
@@ -136,3 +141,28 @@ class InstructorTaskSubmitTest(InstructorTaskModuleTestCase):
|
||||
|
||||
def test_submit_delete_all(self):
|
||||
self._test_submit_task(submit_delete_problem_state_for_all_students)
|
||||
|
||||
|
||||
class InstructorTaskCourseSubmitTest(InstructorTaskCourseTestCase):
|
||||
"""Tests API methods that involve the submission of course-based background tasks."""
|
||||
|
||||
def setUp(self):
|
||||
self.initialize_course()
|
||||
self.student = UserFactory.create(username="student", email="student@edx.org")
|
||||
self.instructor = UserFactory.create(username="instructor", email="instructor@edx.org")
|
||||
|
||||
def _define_course_email(self):
|
||||
course_email = CourseEmail.create(self.course.id, self.instructor, SEND_TO_ALL, "Test Subject", "<p>This is a test message</p>")
|
||||
return course_email.id
|
||||
|
||||
def test_submit_bulk_email_all(self):
|
||||
email_id = self._define_course_email()
|
||||
instructor_task = submit_bulk_course_email(self.create_task_request(self.instructor), self.course.id, email_id)
|
||||
|
||||
# test resubmitting, by updating the existing record:
|
||||
instructor_task = InstructorTask.objects.get(id=instructor_task.id)
|
||||
instructor_task.task_state = PROGRESS
|
||||
instructor_task.save()
|
||||
|
||||
with self.assertRaises(AlreadyRunningError):
|
||||
instructor_task = submit_bulk_course_email(self.create_task_request(self.instructor), self.course.id, email_id)
|
||||
|
||||
@@ -96,10 +96,10 @@ class InstructorTaskTestCase(TestCase):
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class InstructorTaskModuleTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
class InstructorTaskCourseTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
"""
|
||||
Base test class for InstructorTask-related tests that require
|
||||
the setup of a course and problem in order to access StudentModule state.
|
||||
the setup of a course.
|
||||
"""
|
||||
course = None
|
||||
current_user = None
|
||||
@@ -149,6 +149,31 @@ class InstructorTaskModuleTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase)
|
||||
"""Creates a student for the test course."""
|
||||
return self._create_user(username, is_staff=False)
|
||||
|
||||
@staticmethod
|
||||
def get_task_status(task_id):
|
||||
"""Use api method to fetch task status, using mock request."""
|
||||
mock_request = Mock()
|
||||
mock_request.REQUEST = {'task_id': task_id}
|
||||
response = instructor_task_status(mock_request)
|
||||
status = json.loads(response.content)
|
||||
return status
|
||||
|
||||
def create_task_request(self, requester_username):
|
||||
"""Generate request that can be used for submitting tasks"""
|
||||
request = Mock()
|
||||
request.user = User.objects.get(username=requester_username)
|
||||
request.get_host = Mock(return_value="testhost")
|
||||
request.META = {'REMOTE_ADDR': '0:0:0:0', 'SERVER_NAME': 'testhost'}
|
||||
request.is_secure = Mock(return_value=False)
|
||||
return request
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class InstructorTaskModuleTestCase(InstructorTaskCourseTestCase):
|
||||
"""
|
||||
Base test class for InstructorTask-related tests that require
|
||||
the setup of a course and problem in order to access StudentModule state.
|
||||
"""
|
||||
@staticmethod
|
||||
def problem_location(problem_url_name):
|
||||
"""
|
||||
@@ -192,21 +217,3 @@ class InstructorTaskModuleTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase)
|
||||
module_type=descriptor.location.category,
|
||||
module_state_key=descriptor.location.url(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_task_status(task_id):
|
||||
"""Use api method to fetch task status, using mock request."""
|
||||
mock_request = Mock()
|
||||
mock_request.REQUEST = {'task_id': task_id}
|
||||
response = instructor_task_status(mock_request)
|
||||
status = json.loads(response.content)
|
||||
return status
|
||||
|
||||
def create_task_request(self, requester_username):
|
||||
"""Generate request that can be used for submitting tasks"""
|
||||
request = Mock()
|
||||
request.user = User.objects.get(username=requester_username)
|
||||
request.get_host = Mock(return_value="testhost")
|
||||
request.META = {'REMOTE_ADDR': '0:0:0:0', 'SERVER_NAME': 'testhost'}
|
||||
request.is_secure = Mock(return_value=False)
|
||||
return request
|
||||
|
||||
@@ -8,23 +8,23 @@ paths actually work.
|
||||
import json
|
||||
from uuid import uuid4
|
||||
from unittest import skip
|
||||
from functools import partial
|
||||
|
||||
from mock import Mock, patch
|
||||
from mock import Mock, MagicMock, patch
|
||||
|
||||
from celery.states import SUCCESS, FAILURE
|
||||
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from courseware.model_data import StudentModule
|
||||
from courseware.models import StudentModule
|
||||
from courseware.tests.factories import StudentModuleFactory
|
||||
from student.tests.factories import UserFactory
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
|
||||
from instructor_task.models import InstructorTask
|
||||
from instructor_task.tests.test_base import InstructorTaskModuleTestCase
|
||||
from instructor_task.tests.factories import InstructorTaskFactory
|
||||
from instructor_task.tasks import rescore_problem, reset_problem_attempts, delete_problem_state
|
||||
from instructor_task.tasks_helper import UpdateProblemModuleStateError
|
||||
|
||||
from instructor_task.tasks_helper import UpdateProblemModuleStateError, run_main_task, perform_module_state_update, UPDATE_STATUS_SUCCEEDED
|
||||
|
||||
PROBLEM_URL_NAME = "test_urlname"
|
||||
|
||||
@@ -34,20 +34,27 @@ class TestTaskFailure(Exception):
|
||||
|
||||
|
||||
class TestInstructorTasks(InstructorTaskModuleTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(InstructorTaskModuleTestCase, self).setUp()
|
||||
self.initialize_course()
|
||||
self.instructor = self.create_instructor('instructor')
|
||||
self.problem_url = InstructorTaskModuleTestCase.problem_location(PROBLEM_URL_NAME)
|
||||
|
||||
def _create_input_entry(self, student_ident=None):
|
||||
def _create_input_entry(self, student_ident=None, use_problem_url=True, course_id=None, task_input=None):
|
||||
"""Creates a InstructorTask entry for testing."""
|
||||
task_id = str(uuid4())
|
||||
task_input = {'problem_url': self.problem_url}
|
||||
if task_input is None:
|
||||
task_input = {}
|
||||
else:
|
||||
task_input = dict(task_input)
|
||||
if use_problem_url:
|
||||
task_input['problem_url'] = self.problem_url
|
||||
if student_ident is not None:
|
||||
task_input['student'] = student_ident
|
||||
|
||||
instructor_task = InstructorTaskFactory.create(course_id=self.course.id,
|
||||
course_id = course_id or self.course.id
|
||||
instructor_task = InstructorTaskFactory.create(course_id=course_id,
|
||||
requester=self.instructor,
|
||||
task_input=json.dumps(task_input),
|
||||
task_key='dummy value',
|
||||
@@ -80,14 +87,11 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
|
||||
with self.assertRaises(UpdateProblemModuleStateError):
|
||||
task_function(task_entry.id, self._get_xmodule_instance_args())
|
||||
|
||||
def test_rescore_missing_current_task(self):
|
||||
self._test_missing_current_task(rescore_problem)
|
||||
|
||||
def test_reset_missing_current_task(self):
|
||||
self._test_missing_current_task(reset_problem_attempts)
|
||||
|
||||
def test_delete_missing_current_task(self):
|
||||
self._test_missing_current_task(delete_problem_state)
|
||||
def _test_undefined_course(self, task_function):
|
||||
# run with celery, but no course defined
|
||||
task_entry = self._create_input_entry(course_id="bogus/course/id")
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
self._run_task_with_mock_celery(task_function, task_entry.id, task_entry.task_id)
|
||||
|
||||
def _test_undefined_problem(self, task_function):
|
||||
"""Run with celery, but no problem defined."""
|
||||
@@ -95,15 +99,6 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
self._run_task_with_mock_celery(task_function, task_entry.id, task_entry.task_id)
|
||||
|
||||
def test_rescore_undefined_problem(self):
|
||||
self._test_undefined_problem(rescore_problem)
|
||||
|
||||
def test_reset_undefined_problem(self):
|
||||
self._test_undefined_problem(reset_problem_attempts)
|
||||
|
||||
def test_delete_undefined_problem(self):
|
||||
self._test_undefined_problem(delete_problem_state)
|
||||
|
||||
def _test_run_with_task(self, task_function, action_name, expected_num_succeeded):
|
||||
"""Run a task and check the number of StudentModules processed."""
|
||||
task_entry = self._create_input_entry()
|
||||
@@ -124,16 +119,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
|
||||
self.define_option_problem(PROBLEM_URL_NAME)
|
||||
self._test_run_with_task(task_function, action_name, 0)
|
||||
|
||||
def test_rescore_with_no_state(self):
|
||||
self._test_run_with_no_state(rescore_problem, 'rescored')
|
||||
|
||||
def test_reset_with_no_state(self):
|
||||
self._test_run_with_no_state(reset_problem_attempts, 'reset')
|
||||
|
||||
def test_delete_with_no_state(self):
|
||||
self._test_run_with_no_state(delete_problem_state, 'deleted')
|
||||
|
||||
def _create_students_with_state(self, num_students, state=None):
|
||||
def _create_students_with_state(self, num_students, state=None, grade=0, max_grade=1):
|
||||
"""Create students, a problem, and StudentModule objects for testing"""
|
||||
self.define_option_problem(PROBLEM_URL_NAME)
|
||||
students = [
|
||||
@@ -141,9 +127,12 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
|
||||
for i in xrange(num_students)
|
||||
]
|
||||
for student in students:
|
||||
CourseEnrollmentFactory.create(course_id=self.course.id, user=student)
|
||||
StudentModuleFactory.create(course_id=self.course.id,
|
||||
module_state_key=self.problem_url,
|
||||
student=student,
|
||||
grade=grade,
|
||||
max_grade=max_grade,
|
||||
state=state)
|
||||
return students
|
||||
|
||||
@@ -156,6 +145,175 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
|
||||
state = json.loads(module.state)
|
||||
self.assertEquals(state['attempts'], num_attempts)
|
||||
|
||||
def _test_run_with_failure(self, task_function, expected_message):
|
||||
"""Run a task and trigger an artificial failure with the given message."""
|
||||
task_entry = self._create_input_entry()
|
||||
self.define_option_problem(PROBLEM_URL_NAME)
|
||||
with self.assertRaises(TestTaskFailure):
|
||||
self._run_task_with_mock_celery(task_function, task_entry.id, task_entry.task_id, expected_message)
|
||||
# compare with entry in table:
|
||||
entry = InstructorTask.objects.get(id=task_entry.id)
|
||||
self.assertEquals(entry.task_state, FAILURE)
|
||||
output = json.loads(entry.task_output)
|
||||
self.assertEquals(output['exception'], 'TestTaskFailure')
|
||||
self.assertEquals(output['message'], expected_message)
|
||||
|
||||
def _test_run_with_long_error_msg(self, task_function):
|
||||
"""
|
||||
Run with an error message that is so long it will require
|
||||
truncation (as well as the jettisoning of the traceback).
|
||||
"""
|
||||
task_entry = self._create_input_entry()
|
||||
self.define_option_problem(PROBLEM_URL_NAME)
|
||||
expected_message = "x" * 1500
|
||||
with self.assertRaises(TestTaskFailure):
|
||||
self._run_task_with_mock_celery(task_function, task_entry.id, task_entry.task_id, expected_message)
|
||||
# compare with entry in table:
|
||||
entry = InstructorTask.objects.get(id=task_entry.id)
|
||||
self.assertEquals(entry.task_state, FAILURE)
|
||||
self.assertGreater(1023, len(entry.task_output))
|
||||
output = json.loads(entry.task_output)
|
||||
self.assertEquals(output['exception'], 'TestTaskFailure')
|
||||
self.assertEquals(output['message'], expected_message[:len(output['message']) - 3] + "...")
|
||||
self.assertTrue('traceback' not in output)
|
||||
|
||||
def _test_run_with_short_error_msg(self, task_function):
|
||||
"""
|
||||
Run with an error message that is short enough to fit
|
||||
in the output, but long enough that the traceback won't.
|
||||
Confirm that the traceback is truncated.
|
||||
"""
|
||||
task_entry = self._create_input_entry()
|
||||
self.define_option_problem(PROBLEM_URL_NAME)
|
||||
expected_message = "x" * 900
|
||||
with self.assertRaises(TestTaskFailure):
|
||||
self._run_task_with_mock_celery(task_function, task_entry.id, task_entry.task_id, expected_message)
|
||||
# compare with entry in table:
|
||||
entry = InstructorTask.objects.get(id=task_entry.id)
|
||||
self.assertEquals(entry.task_state, FAILURE)
|
||||
self.assertGreater(1023, len(entry.task_output))
|
||||
output = json.loads(entry.task_output)
|
||||
self.assertEquals(output['exception'], 'TestTaskFailure')
|
||||
self.assertEquals(output['message'], expected_message)
|
||||
self.assertEquals(output['traceback'][-3:], "...")
|
||||
|
||||
|
||||
class TestGeneralInstructorTask(TestInstructorTasks):
|
||||
"""Tests instructor task mechanism using custom tasks"""
|
||||
|
||||
def test_successful_result_too_long(self):
|
||||
# while we don't expect the existing tasks to generate output that is too
|
||||
# long, we can test the framework will handle such an occurrence.
|
||||
task_entry = self._create_input_entry()
|
||||
self.define_option_problem(PROBLEM_URL_NAME)
|
||||
action_name = 'x' * 1000
|
||||
# define a custom task that does nothing:
|
||||
update_fcn = lambda(_module_descriptor, _student_module): UPDATE_STATUS_SUCCEEDED
|
||||
visit_fcn = partial(perform_module_state_update, update_fcn, None)
|
||||
task_function = (lambda entry_id, xmodule_instance_args:
|
||||
run_main_task(entry_id, visit_fcn, action_name))
|
||||
# run the task:
|
||||
with self.assertRaises(ValueError):
|
||||
self._run_task_with_mock_celery(task_function, task_entry.id, task_entry.task_id)
|
||||
# compare with entry in table:
|
||||
entry = InstructorTask.objects.get(id=task_entry.id)
|
||||
self.assertEquals(entry.task_state, FAILURE)
|
||||
self.assertGreater(1023, len(entry.task_output))
|
||||
output = json.loads(entry.task_output)
|
||||
self.assertEquals(output['exception'], 'ValueError')
|
||||
self.assertTrue("Length of task output is too long" in output['message'])
|
||||
self.assertTrue('traceback' not in output)
|
||||
|
||||
|
||||
class TestRescoreInstructorTask(TestInstructorTasks):
|
||||
"""Tests problem-rescoring instructor task."""
|
||||
|
||||
def test_rescore_missing_current_task(self):
|
||||
self._test_missing_current_task(rescore_problem)
|
||||
|
||||
def test_rescore_undefined_course(self):
|
||||
self._test_undefined_course(rescore_problem)
|
||||
|
||||
def test_rescore_undefined_problem(self):
|
||||
self._test_undefined_problem(rescore_problem)
|
||||
|
||||
def test_rescore_with_no_state(self):
|
||||
self._test_run_with_no_state(rescore_problem, 'rescored')
|
||||
|
||||
def test_rescore_with_failure(self):
|
||||
self._test_run_with_failure(rescore_problem, 'We expected this to fail')
|
||||
|
||||
def test_rescore_with_long_error_msg(self):
|
||||
self._test_run_with_long_error_msg(rescore_problem)
|
||||
|
||||
def test_rescore_with_short_error_msg(self):
|
||||
self._test_run_with_short_error_msg(rescore_problem)
|
||||
|
||||
@skip
|
||||
def test_rescoring_unrescorable(self):
|
||||
input_state = json.dumps({'done': True})
|
||||
num_students = 1
|
||||
self._create_students_with_state(num_students, input_state)
|
||||
task_entry = self._create_input_entry()
|
||||
mock_instance = MagicMock()
|
||||
del mock_instance.rescore_problem
|
||||
# TODO: figure out why this patch isn't working
|
||||
# with patch('courseware.module_render.get_module_for_descriptor_internal') as mock_get_module:
|
||||
with patch('courseware.module_render.get_module_for_descriptor_internal') as mock_get_module:
|
||||
mock_get_module.return_value = mock_instance
|
||||
with self.assertRaises(UpdateProblemModuleStateError):
|
||||
self._run_task_with_mock_celery(rescore_problem, task_entry.id, task_entry.task_id)
|
||||
# check values stored in table:
|
||||
entry = InstructorTask.objects.get(id=task_entry.id)
|
||||
output = json.loads(entry.task_output)
|
||||
self.assertEquals(output['exception'], "UpdateProblemModuleStateError")
|
||||
self.assertEquals(output['message'], "Specified problem does not support rescoring.")
|
||||
self.assertGreater(len(output['traceback']), 0)
|
||||
|
||||
def test_rescoring_success(self):
|
||||
input_state = json.dumps({'done': True})
|
||||
num_students = 10
|
||||
self._create_students_with_state(num_students, input_state)
|
||||
task_entry = self._create_input_entry()
|
||||
mock_instance = Mock()
|
||||
mock_instance.rescore_problem = Mock({'success': 'correct'})
|
||||
with patch('courseware.module_render.get_module_for_descriptor_internal') as mock_get_module:
|
||||
mock_get_module.return_value = mock_instance
|
||||
self._run_task_with_mock_celery(rescore_problem, task_entry.id, task_entry.task_id)
|
||||
# check return value
|
||||
entry = InstructorTask.objects.get(id=task_entry.id)
|
||||
output = json.loads(entry.task_output)
|
||||
self.assertEquals(output.get('attempted'), num_students)
|
||||
self.assertEquals(output.get('succeeded'), num_students)
|
||||
self.assertEquals(output.get('total'), num_students)
|
||||
self.assertEquals(output.get('action_name'), 'rescored')
|
||||
self.assertGreater('duration_ms', 0)
|
||||
|
||||
|
||||
class TestResetAttemptsInstructorTask(TestInstructorTasks):
|
||||
"""Tests instructor task that resets problem attempts."""
|
||||
|
||||
def test_reset_missing_current_task(self):
|
||||
self._test_missing_current_task(reset_problem_attempts)
|
||||
|
||||
def test_reset_undefined_course(self):
|
||||
self._test_undefined_course(reset_problem_attempts)
|
||||
|
||||
def test_reset_undefined_problem(self):
|
||||
self._test_undefined_problem(reset_problem_attempts)
|
||||
|
||||
def test_reset_with_no_state(self):
|
||||
self._test_run_with_no_state(reset_problem_attempts, 'reset')
|
||||
|
||||
def test_reset_with_failure(self):
|
||||
self._test_run_with_failure(reset_problem_attempts, 'We expected this to fail')
|
||||
|
||||
def test_reset_with_long_error_msg(self):
|
||||
self._test_run_with_long_error_msg(reset_problem_attempts)
|
||||
|
||||
def test_reset_with_short_error_msg(self):
|
||||
self._test_run_with_short_error_msg(reset_problem_attempts)
|
||||
|
||||
def test_reset_with_some_state(self):
|
||||
initial_attempts = 3
|
||||
input_state = json.dumps({'attempts': initial_attempts})
|
||||
@@ -168,24 +326,6 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
|
||||
# check that entries were reset
|
||||
self._assert_num_attempts(students, 0)
|
||||
|
||||
def test_delete_with_some_state(self):
|
||||
# This will create StudentModule entries -- we don't have to worry about
|
||||
# the state inside them.
|
||||
num_students = 10
|
||||
students = self._create_students_with_state(num_students)
|
||||
# check that entries were created correctly
|
||||
for student in students:
|
||||
StudentModule.objects.get(course_id=self.course.id,
|
||||
student=student,
|
||||
module_state_key=self.problem_url)
|
||||
self._test_run_with_task(delete_problem_state, 'deleted', num_students)
|
||||
# confirm that no state can be found anymore:
|
||||
for student in students:
|
||||
with self.assertRaises(StudentModule.DoesNotExist):
|
||||
StudentModule.objects.get(course_id=self.course.id,
|
||||
student=student,
|
||||
module_state_key=self.problem_url)
|
||||
|
||||
def _test_reset_with_student(self, use_email):
|
||||
"""Run a reset task for one student, with several StudentModules for the problem defined."""
|
||||
num_students = 10
|
||||
@@ -234,144 +374,45 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
|
||||
def test_reset_with_student_email(self):
|
||||
self._test_reset_with_student(True)
|
||||
|
||||
def _test_run_with_failure(self, task_function, expected_message):
|
||||
"""Run a task and trigger an artificial failure with give message."""
|
||||
task_entry = self._create_input_entry()
|
||||
self.define_option_problem(PROBLEM_URL_NAME)
|
||||
with self.assertRaises(TestTaskFailure):
|
||||
self._run_task_with_mock_celery(task_function, task_entry.id, task_entry.task_id, expected_message)
|
||||
# compare with entry in table:
|
||||
entry = InstructorTask.objects.get(id=task_entry.id)
|
||||
self.assertEquals(entry.task_state, FAILURE)
|
||||
output = json.loads(entry.task_output)
|
||||
self.assertEquals(output['exception'], 'TestTaskFailure')
|
||||
self.assertEquals(output['message'], expected_message)
|
||||
|
||||
def test_rescore_with_failure(self):
|
||||
self._test_run_with_failure(rescore_problem, 'We expected this to fail')
|
||||
class TestDeleteStateInstructorTask(TestInstructorTasks):
|
||||
"""Tests instructor task that deletes problem state."""
|
||||
|
||||
def test_reset_with_failure(self):
|
||||
self._test_run_with_failure(reset_problem_attempts, 'We expected this to fail')
|
||||
def test_delete_missing_current_task(self):
|
||||
self._test_missing_current_task(delete_problem_state)
|
||||
|
||||
def test_delete_undefined_course(self):
|
||||
self._test_undefined_course(delete_problem_state)
|
||||
|
||||
def test_delete_undefined_problem(self):
|
||||
self._test_undefined_problem(delete_problem_state)
|
||||
|
||||
def test_delete_with_no_state(self):
|
||||
self._test_run_with_no_state(delete_problem_state, 'deleted')
|
||||
|
||||
def test_delete_with_failure(self):
|
||||
self._test_run_with_failure(delete_problem_state, 'We expected this to fail')
|
||||
|
||||
def _test_run_with_long_error_msg(self, task_function):
|
||||
"""
|
||||
Run with an error message that is so long it will require
|
||||
truncation (as well as the jettisoning of the traceback).
|
||||
"""
|
||||
task_entry = self._create_input_entry()
|
||||
self.define_option_problem(PROBLEM_URL_NAME)
|
||||
expected_message = "x" * 1500
|
||||
with self.assertRaises(TestTaskFailure):
|
||||
self._run_task_with_mock_celery(task_function, task_entry.id, task_entry.task_id, expected_message)
|
||||
# compare with entry in table:
|
||||
entry = InstructorTask.objects.get(id=task_entry.id)
|
||||
self.assertEquals(entry.task_state, FAILURE)
|
||||
self.assertGreater(1023, len(entry.task_output))
|
||||
output = json.loads(entry.task_output)
|
||||
self.assertEquals(output['exception'], 'TestTaskFailure')
|
||||
self.assertEquals(output['message'], expected_message[:len(output['message']) - 3] + "...")
|
||||
self.assertTrue('traceback' not in output)
|
||||
|
||||
def test_rescore_with_long_error_msg(self):
|
||||
self._test_run_with_long_error_msg(rescore_problem)
|
||||
|
||||
def test_reset_with_long_error_msg(self):
|
||||
self._test_run_with_long_error_msg(reset_problem_attempts)
|
||||
|
||||
def test_delete_with_long_error_msg(self):
|
||||
self._test_run_with_long_error_msg(delete_problem_state)
|
||||
|
||||
def _test_run_with_short_error_msg(self, task_function):
|
||||
"""
|
||||
Run with an error message that is short enough to fit
|
||||
in the output, but long enough that the traceback won't.
|
||||
Confirm that the traceback is truncated.
|
||||
"""
|
||||
task_entry = self._create_input_entry()
|
||||
self.define_option_problem(PROBLEM_URL_NAME)
|
||||
expected_message = "x" * 900
|
||||
with self.assertRaises(TestTaskFailure):
|
||||
self._run_task_with_mock_celery(task_function, task_entry.id, task_entry.task_id, expected_message)
|
||||
# compare with entry in table:
|
||||
entry = InstructorTask.objects.get(id=task_entry.id)
|
||||
self.assertEquals(entry.task_state, FAILURE)
|
||||
self.assertGreater(1023, len(entry.task_output))
|
||||
output = json.loads(entry.task_output)
|
||||
self.assertEquals(output['exception'], 'TestTaskFailure')
|
||||
self.assertEquals(output['message'], expected_message)
|
||||
self.assertEquals(output['traceback'][-3:], "...")
|
||||
|
||||
def test_rescore_with_short_error_msg(self):
|
||||
self._test_run_with_short_error_msg(rescore_problem)
|
||||
|
||||
def test_reset_with_short_error_msg(self):
|
||||
self._test_run_with_short_error_msg(reset_problem_attempts)
|
||||
|
||||
def test_delete_with_short_error_msg(self):
|
||||
self._test_run_with_short_error_msg(delete_problem_state)
|
||||
|
||||
def teDONTst_successful_result_too_long(self):
|
||||
# while we don't expect the existing tasks to generate output that is too
|
||||
# long, we can test the framework will handle such an occurrence.
|
||||
task_entry = self._create_input_entry()
|
||||
self.define_option_problem(PROBLEM_URL_NAME)
|
||||
action_name = 'x' * 1000
|
||||
update_fcn = lambda(_module_descriptor, _student_module, _xmodule_instance_args): True
|
||||
# task_function = (lambda entry_id, xmodule_instance_args:
|
||||
# update_problem_module_state(entry_id,
|
||||
# update_fcn, action_name, filter_fcn=None,
|
||||
# xmodule_instance_args=None))
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
self._run_task_with_mock_celery(task_function, task_entry.id, task_entry.task_id)
|
||||
# compare with entry in table:
|
||||
entry = InstructorTask.objects.get(id=task_entry.id)
|
||||
self.assertEquals(entry.task_state, FAILURE)
|
||||
self.assertGreater(1023, len(entry.task_output))
|
||||
output = json.loads(entry.task_output)
|
||||
self.assertEquals(output['exception'], 'ValueError')
|
||||
self.assertTrue("Length of task output is too long" in output['message'])
|
||||
self.assertTrue('traceback' not in output)
|
||||
|
||||
@skip
|
||||
def test_rescoring_unrescorable(self):
|
||||
# TODO: this test needs to have Mako templates initialized
|
||||
# to make sure that the creation of an XModule works.
|
||||
input_state = json.dumps({'done': True})
|
||||
num_students = 1
|
||||
self._create_students_with_state(num_students, input_state)
|
||||
task_entry = self._create_input_entry()
|
||||
with self.assertRaises(UpdateProblemModuleStateError):
|
||||
self._run_task_with_mock_celery(rescore_problem, task_entry.id, task_entry.task_id)
|
||||
# check values stored in table:
|
||||
entry = InstructorTask.objects.get(id=task_entry.id)
|
||||
output = json.loads(entry.task_output)
|
||||
self.assertEquals(output['exception'], "UpdateProblemModuleStateError")
|
||||
self.assertEquals(output['message'], "Specified problem does not support rescoring.")
|
||||
self.assertGreater(len(output['traceback']), 0)
|
||||
|
||||
@skip
|
||||
def test_rescoring_success(self):
|
||||
# TODO: this test needs to have Mako templates initialized
|
||||
# to make sure that the creation of an XModule works.
|
||||
input_state = json.dumps({'done': True})
|
||||
def test_delete_with_some_state(self):
|
||||
# This will create StudentModule entries -- we don't have to worry about
|
||||
# the state inside them.
|
||||
num_students = 10
|
||||
self._create_students_with_state(num_students, input_state)
|
||||
task_entry = self._create_input_entry()
|
||||
mock_instance = Mock()
|
||||
mock_instance.rescore_problem = Mock({'success': 'correct'})
|
||||
# TODO: figure out why this mock is not working....
|
||||
with patch('courseware.module_render.get_module_for_descriptor_internal') as mock_get_module:
|
||||
mock_get_module.return_value = mock_instance
|
||||
self._run_task_with_mock_celery(rescore_problem, task_entry.id, task_entry.task_id)
|
||||
# check return value
|
||||
entry = InstructorTask.objects.get(id=task_entry.id)
|
||||
output = json.loads(entry.task_output)
|
||||
self.assertEquals(output.get('attempted'), num_students)
|
||||
self.assertEquals(output.get('succeeded'), num_students)
|
||||
self.assertEquals(output.get('total'), num_students)
|
||||
self.assertEquals(output.get('action_name'), 'rescored')
|
||||
self.assertGreater('duration_ms', 0)
|
||||
students = self._create_students_with_state(num_students)
|
||||
# check that entries were created correctly
|
||||
for student in students:
|
||||
StudentModule.objects.get(course_id=self.course.id,
|
||||
student=student,
|
||||
module_state_key=self.problem_url)
|
||||
self._test_run_with_task(delete_problem_state, 'deleted', num_students)
|
||||
# confirm that no state can be found anymore:
|
||||
for student in students:
|
||||
with self.assertRaises(StudentModule.DoesNotExist):
|
||||
StudentModule.objects.get(course_id=self.course.id,
|
||||
student=student,
|
||||
module_state_key=self.problem_url)
|
||||
|
||||
Reference in New Issue
Block a user