Commit 331fbcd5 by Will Daly

Merge pull request #2612 from edx/will/fix-xqueue-stub-test

Fixed intermittent failure in XQueue stub unit test
parents b4086504 b8187fb1
...@@ -11,6 +11,17 @@ import copy ...@@ -11,6 +11,17 @@ import copy
from ..xqueue import StubXQueueService, StubXQueueHandler from ..xqueue import StubXQueueService, StubXQueueHandler
class FakeTimer(object):
"""
Fake timer implementation that executes immediately.
"""
def __init__(self, delay, func):
self.func = func
def start(self):
self.func()
class StubXQueueServiceTest(unittest.TestCase): class StubXQueueServiceTest(unittest.TestCase):
def setUp(self): def setUp(self):
...@@ -18,11 +29,18 @@ class StubXQueueServiceTest(unittest.TestCase): ...@@ -18,11 +29,18 @@ class StubXQueueServiceTest(unittest.TestCase):
self.url = "http://127.0.0.1:{0}/xqueue/submit".format(self.server.port) self.url = "http://127.0.0.1:{0}/xqueue/submit".format(self.server.port)
self.addCleanup(self.server.shutdown) self.addCleanup(self.server.shutdown)
# For testing purposes, do not delay the grading response # Patch the timer async calls
self.server.config['response_delay'] = 0 patcher = mock.patch('terrain.stubs.xqueue.post')
self.post = patcher.start()
self.addCleanup(patcher.stop)
# Patch POST requests
patcher = mock.patch('terrain.stubs.xqueue.Timer')
timer = patcher.start()
timer.side_effect = FakeTimer
self.addCleanup(patcher.stop)
@mock.patch('terrain.stubs.xqueue.post') def test_grade_request(self):
def test_grade_request(self, post):
# Post a submission to the stub XQueue # Post a submission to the stub XQueue
callback_url = 'http://127.0.0.1:8000/test_callback' callback_url = 'http://127.0.0.1:8000/test_callback'
...@@ -38,10 +56,9 @@ class StubXQueueServiceTest(unittest.TestCase): ...@@ -38,10 +56,9 @@ class StubXQueueServiceTest(unittest.TestCase):
# Check the response we receive # Check the response we receive
# (Should be the default grading response) # (Should be the default grading response)
expected_body = json.dumps({'correct': True, 'score': 1, 'msg': '<div></div>'}) expected_body = json.dumps({'correct': True, 'score': 1, 'msg': '<div></div>'})
self._check_grade_response(post, callback_url, expected_header, expected_body) self._check_grade_response(callback_url, expected_header, expected_body)
@mock.patch('terrain.stubs.xqueue.post') def test_configure_default_response(self):
def test_configure_default_response(self, post):
# Configure the default response for submissions to any queue # Configure the default response for submissions to any queue
response_content = {'test_response': 'test_content'} response_content = {'test_response': 'test_content'}
...@@ -60,12 +77,9 @@ class StubXQueueServiceTest(unittest.TestCase): ...@@ -60,12 +77,9 @@ class StubXQueueServiceTest(unittest.TestCase):
# Check the response we receive # Check the response we receive
# (Should be the default grading response) # (Should be the default grading response)
self._check_grade_response( self._check_grade_response(callback_url, expected_header, json.dumps(response_content))
post, callback_url, expected_header, json.dumps(response_content)
)
@mock.patch('terrain.stubs.xqueue.post') def test_configure_specific_response(self):
def test_configure_specific_response(self, post):
# Configure the XQueue stub response to any submission to the test queue # Configure the XQueue stub response to any submission to the test queue
response_content = {'test_response': 'test_content'} response_content = {'test_response': 'test_content'}
...@@ -79,12 +93,9 @@ class StubXQueueServiceTest(unittest.TestCase): ...@@ -79,12 +93,9 @@ class StubXQueueServiceTest(unittest.TestCase):
) )
# Check that we receive the response we configured # Check that we receive the response we configured
self._check_grade_response( self._check_grade_response(callback_url, expected_header, json.dumps(response_content))
post, callback_url, expected_header, json.dumps(response_content)
)
@mock.patch('terrain.stubs.xqueue.post') def test_multiple_response_matches(self):
def test_multiple_response_matches(self, post):
# Configure the XQueue stub with two responses that # Configure the XQueue stub with two responses that
# match the same submission # match the same submission
...@@ -100,17 +111,12 @@ class StubXQueueServiceTest(unittest.TestCase): ...@@ -100,17 +111,12 @@ class StubXQueueServiceTest(unittest.TestCase):
json.dumps({'submission': 'test_1 and test_2'}) json.dumps({'submission': 'test_1 and test_2'})
) )
# Wait for the delayed grade response
self._wait_for_mock_called(logger.error)
# Expect that we do NOT receive a response # Expect that we do NOT receive a response
# and that an error message is logged # and that an error message is logged
self.assertFalse(post.called) self.assertFalse(self.post.called)
self.assertTrue(logger.error.called) self.assertTrue(logger.error.called)
@mock.patch('terrain.stubs.xqueue.post') def test_register_submission_url(self):
def test_register_submission_url(self, post):
# Configure the XQueue stub to notify another service # Configure the XQueue stub to notify another service
# when it receives a submission. # when it receives a submission.
register_url = 'http://127.0.0.1:8000/register_submission' register_url = 'http://127.0.0.1:8000/register_submission'
...@@ -118,12 +124,10 @@ class StubXQueueServiceTest(unittest.TestCase): ...@@ -118,12 +124,10 @@ class StubXQueueServiceTest(unittest.TestCase):
callback_url = 'http://127.0.0.1:8000/test_callback' callback_url = 'http://127.0.0.1:8000/test_callback'
submission = json.dumps({'grader_payload': 'test payload'}) submission = json.dumps({'grader_payload': 'test payload'})
self._post_submission( self._post_submission(callback_url, 'test_queuekey', 'test_queue', submission)
callback_url, 'test_queuekey', 'test_queue', submission
)
# Check that a notification was sent # Check that a notification was sent
post.assert_any_call(register_url, data={'grader_payload': u'test payload'}) self.post.assert_any_call(register_url, data={'grader_payload': u'test payload'})
def _post_submission(self, callback_url, lms_key, queue_name, xqueue_body): def _post_submission(self, callback_url, lms_key, queue_name, xqueue_body):
""" """
...@@ -155,22 +159,17 @@ class StubXQueueServiceTest(unittest.TestCase): ...@@ -155,22 +159,17 @@ class StubXQueueServiceTest(unittest.TestCase):
# Return back the header, so we can authenticate the response we receive # Return back the header, so we can authenticate the response we receive
return grade_request['xqueue_header'] return grade_request['xqueue_header']
def _check_grade_response(self, post_mock, callback_url, expected_header, expected_body): def _check_grade_response(self, callback_url, expected_header, expected_body):
""" """
Verify that the stub sent a POST request back to us Verify that the stub sent a POST request back to us
with the expected data. with the expected data.
`post_mock` is our mock for `requests.post`
`callback_url` is the URL we expect the stub to POST to `callback_url` is the URL we expect the stub to POST to
`expected_header` is the header (a string) we expect to receive with the grade. `expected_header` is the header (a string) we expect to receive with the grade.
`expected_body` is the content (a string) we expect to receive with the grade. `expected_body` is the content (a string) we expect to receive with the grade.
Raises an `AssertionError` if the check fails. Raises an `AssertionError` if the check fails.
""" """
# Wait for the server to POST back to the callback URL
# If it takes too long, continue anyway
self._wait_for_mock_called(post_mock)
# Check the response posted back to us # Check the response posted back to us
# This is the default response # This is the default response
expected_callback_dict = { expected_callback_dict = {
...@@ -179,15 +178,4 @@ class StubXQueueServiceTest(unittest.TestCase): ...@@ -179,15 +178,4 @@ class StubXQueueServiceTest(unittest.TestCase):
} }
# Check that the POST request was made with the correct params # Check that the POST request was made with the correct params
post_mock.assert_called_with(callback_url, data=expected_callback_dict) self.post.assert_called_with(callback_url, data=expected_callback_dict)
def _wait_for_mock_called(self, mock_obj, max_time=120):
"""
Wait for `mock` (a `Mock` object) to be called.
If seconds elapsed exceeds `max_time`, continue without error.
"""
start_time = time.time()
while time.time() - start_time < max_time:
if mock_obj.called:
break
time.sleep(1)
...@@ -13,7 +13,7 @@ from .http import StubHttpRequestHandler, StubHttpService, require_params ...@@ -13,7 +13,7 @@ from .http import StubHttpRequestHandler, StubHttpService, require_params
import json import json
import copy import copy
from requests import post from requests import post
import threading from threading import Timer
class StubXQueueHandler(StubHttpRequestHandler): class StubXQueueHandler(StubHttpRequestHandler):
...@@ -70,10 +70,8 @@ class StubXQueueHandler(StubHttpRequestHandler): ...@@ -70,10 +70,8 @@ class StubXQueueHandler(StubHttpRequestHandler):
callback_url, xqueue_header, self.post_dict['xqueue_body'] callback_url, xqueue_header, self.post_dict['xqueue_body']
) )
threading.Timer( delay = self.server.config.get('response_delay', self.DEFAULT_RESPONSE_DELAY)
self.server.config.get('response_delay', self.DEFAULT_RESPONSE_DELAY), Timer(delay, delayed_grade_func).start()
delayed_grade_func
).start()
# If we get a request that's not to the grading submission # If we get a request that's not to the grading submission
# URL, return an error # URL, return an error
......
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