Files
edx-platform/lms/djangoapps/courseware/features/mock_xqueue_server.py
Will Daly 66434d7d05 Moved XQueue mock from terrain into LMS, so that the
server is not created for cms tests.
2013-03-19 15:55:05 -04:00

279 lines
9.0 KiB
Python

from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
import json
import urllib
import urlparse
import time
from logging import getLogger
logger = getLogger(__name__)
class MockXQueueRequestHandler(BaseHTTPRequestHandler):
'''
A handler for XQueue POST requests.
'''
protocol = "HTTP/1.0"
def do_HEAD(self):
self._send_head()
def do_POST(self):
'''
Handle a POST request from the client
Sends back an immediate success/failure response.
It then POSTS back to the client
with grading results, as configured in MockXQueueServer.
'''
self._send_head()
# Retrieve the POST data
post_dict = self._post_dict()
# Log the request
logger.debug("XQueue received POST request %s to path %s" %
(str(post_dict), self.path))
# Respond only to grading requests
if self._is_grade_request():
try:
xqueue_header = json.loads(post_dict['xqueue_header'])
xqueue_body = json.loads(post_dict['xqueue_body'])
callback_url = xqueue_header['lms_callback_url']
except KeyError:
# If the message doesn't have a header or body,
# then it's malformed.
# Respond with failure
error_msg = "XQueue received invalid grade request"
self._send_immediate_response(False, message=error_msg)
except ValueError:
# If we could not decode the body or header,
# respond with failure
error_msg = "XQueue could not decode grade request"
self._send_immediate_response(False, message=error_msg)
else:
# Send an immediate response of success
# The grade request is formed correctly
self._send_immediate_response(True)
# Wait a bit before POSTing back to the callback url with the
# grade result configured by the server
# Otherwise, the problem will not realize it's
# queued and it will keep waiting for a response
# indefinitely
delayed_grade_func = lambda: self._send_grade_response(callback_url,
xqueue_header)
timer = threading.Timer(2, delayed_grade_func)
timer.start()
# If we get a request that's not to the grading submission
# URL, return an error
else:
error_message = "Invalid request URL"
self._send_immediate_response(False, message=error_message)
def _send_head(self):
'''
Send the response code and MIME headers
'''
if self._is_grade_request():
self.send_response(200)
else:
self.send_response(500)
self.send_header('Content-type', 'text/plain')
self.end_headers()
def _post_dict(self):
'''
Retrieve the POST parameters from the client as a dictionary
'''
try:
length = int(self.headers.getheader('content-length'))
post_dict = urlparse.parse_qs(self.rfile.read(length))
# The POST dict will contain a list of values
# for each key.
# None of our parameters are lists, however,
# so we map [val] --> val
# If the list contains multiple entries,
# we pick the first one
post_dict = dict(map(lambda (key, list_val): (key, list_val[0]),
post_dict.items()))
except:
# We return an empty dict here, on the assumption
# that when we later check that the request has
# the correct fields, it won't find them,
# and will therefore send an error response
return {}
return post_dict
def _send_immediate_response(self, success, message=""):
'''
Send an immediate success/failure message
back to the client
'''
# Send the response indicating success/failure
response_str = json.dumps({'return_code': 0 if success else 1,
'content': message})
# Log the response
logger.debug("XQueue: sent response %s" % response_str)
self.wfile.write(response_str)
def _send_grade_response(self, postback_url, xqueue_header):
'''
POST the grade response back to the client
using the response provided by the server configuration
'''
response_dict = {'xqueue_header': json.dumps(xqueue_header),
'xqueue_body': json.dumps(self.server.grade_response())}
# Log the response
logger.debug("XQueue: sent grading response %s" % str(response_dict))
MockXQueueRequestHandler.post_to_url(postback_url, response_dict)
def _is_grade_request(self):
return 'xqueue/submit' in self.path
@staticmethod
def post_to_url(url, param_dict):
'''
POST *param_dict* to *url*
We make this a separate function so we can easily patch
it during testing.
'''
urllib.urlopen(url, urllib.urlencode(param_dict))
class MockXQueueServer(HTTPServer):
'''
A mock XQueue grading server that responds
to POST requests to localhost.
'''
def __init__(self, port_num,
grade_response_dict={'correct':True, 'score': 1, 'msg': ''}):
'''
Initialize the mock XQueue server instance.
*port_num* is the localhost port to listen to
*grade_response_dict* is a dictionary that will be JSON-serialized
and sent in response to XQueue grading requests.
'''
self.set_grade_response(grade_response_dict)
handler = MockXQueueRequestHandler
address = ('', port_num)
HTTPServer.__init__(self, address, handler)
def shutdown(self):
'''
Stop the server and free up the port
'''
# First call superclass shutdown()
HTTPServer.shutdown(self)
# We also need to manually close the socket
self.socket.close()
def grade_response(self):
return self._grade_response
def set_grade_response(self, grade_response_dict):
# Check that the grade response has the right keys
assert('correct' in grade_response_dict and
'score' in grade_response_dict and
'msg' in grade_response_dict)
# Wrap the message in <div> tags to ensure that it is valid XML
grade_response_dict['msg'] = "<div>%s</div>" % grade_response_dict['msg']
# Save the response dictionary
self._grade_response = grade_response_dict
# ----------------------------
# Tests
import mock
import threading
import unittest
class MockXQueueServerTest(unittest.TestCase):
def setUp(self):
# Create the server
server_port = 8034
self.server_url = 'http://127.0.0.1:%d' % server_port
self.server = MockXQueueServer(server_port,
{'correct': True, 'score': 1, 'msg': ''})
# Start the server in a separate daemon thread
server_thread = threading.Thread(target=self.server.serve_forever)
server_thread.daemon = True
server_thread.start()
def tearDown(self):
# Stop the server, freeing up the port
self.server.shutdown()
def test_grade_request(self):
# Patch post_to_url() so we can intercept
# outgoing POST requests from the server
MockXQueueRequestHandler.post_to_url = mock.Mock()
# Send a grade request
callback_url = 'http://127.0.0.1:8000/test_callback'
grade_header = json.dumps({'lms_callback_url': callback_url,
'lms_key': 'test_queuekey',
'queue_name': 'test_queue'})
grade_body = json.dumps({'student_info': 'test',
'grader_payload': 'test',
'student_response': 'test'})
grade_request = {'xqueue_header': grade_header,
'xqueue_body': grade_body}
response_handle = urllib.urlopen(self.server_url + '/xqueue/submit',
urllib.urlencode(grade_request))
response_dict = json.loads(response_handle.read())
# Expect that the response is success
self.assertEqual(response_dict['return_code'], 0)
# Wait a bit before checking that the server posted back
time.sleep(3)
# Expect that the server tries to post back the grading info
xqueue_body = json.dumps({'correct': True, 'score': 1,
'msg': '<div></div>'})
expected_callback_dict = {'xqueue_header': grade_header,
'xqueue_body': xqueue_body }
MockXQueueRequestHandler.post_to_url.assert_called_with(callback_url,
expected_callback_dict)