Merge pull request #1965 from edx/will/fake-server-refactor
Stub Server Port Conflicts and Refactor
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
@shard_3
|
||||
Feature: Video Component Editor
|
||||
As a course author, I want to be able to create video components.
|
||||
|
||||
@@ -668,7 +669,7 @@ Feature: Video Component Editor
|
||||
And I edit the component
|
||||
|
||||
And I open tab "Advanced"
|
||||
And I revert the transcript field"HTML5 Transcript"
|
||||
And I revert the transcript field "HTML5 Transcript"
|
||||
|
||||
And I save changes
|
||||
Then when I view the video it does not show the captions
|
||||
|
||||
@@ -49,25 +49,18 @@ TRANSCRIPTS_BUTTONS = {
|
||||
}
|
||||
|
||||
|
||||
def _clear_field(index):
|
||||
world.css_fill(SELECTORS['url_inputs'], '', index)
|
||||
# In some reason chromeDriver doesn't trigger 'input' event after filling
|
||||
# field by an empty value. That's why we trigger it manually via jQuery.
|
||||
world.trigger_event(SELECTORS['url_inputs'], event='input', index=index)
|
||||
|
||||
|
||||
@step('I clear fields$')
|
||||
def clear_fields(_step):
|
||||
js_str = '''
|
||||
|
||||
# Clear the input fields and trigger an 'input' event
|
||||
script = """
|
||||
$('{selector}')
|
||||
.eq({index})
|
||||
.prop('disabled', false)
|
||||
.removeClass('is-disabled');
|
||||
'''
|
||||
for index in range(1, 4):
|
||||
js = js_str.format(selector=SELECTORS['url_inputs'], index=index - 1)
|
||||
world.browser.execute_script(js)
|
||||
_clear_field(index)
|
||||
.removeClass('is-disabled')
|
||||
.val('')
|
||||
.trigger('input');
|
||||
""".format(selector=SELECTORS['url_inputs'])
|
||||
world.browser.execute_script(script)
|
||||
|
||||
world.wait(DELAY)
|
||||
world.wait_for_ajax_complete()
|
||||
@@ -76,7 +69,12 @@ def clear_fields(_step):
|
||||
@step('I clear field number (.+)$')
|
||||
def clear_field(_step, index):
|
||||
index = int(index) - 1
|
||||
_clear_field(index)
|
||||
world.css_fill(SELECTORS['url_inputs'], '', index)
|
||||
|
||||
# For some reason ChromeDriver doesn't trigger an 'input' event after filling
|
||||
# the field with an empty value. That's why we trigger it manually via jQuery.
|
||||
world.trigger_event(SELECTORS['url_inputs'], event='input', index=index)
|
||||
|
||||
world.wait(DELAY)
|
||||
world.wait_for_ajax_complete()
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def i_created_a_video_component(step):
|
||||
|
||||
world.wait_for_present('.is-initialized')
|
||||
world.wait(DELAY)
|
||||
assert not world.css_visible(SELECTORS['spinner'])
|
||||
world.wait_for_invisible(SELECTORS['spinner'])
|
||||
|
||||
|
||||
@step('I have created a Video component with subtitles$')
|
||||
@@ -59,8 +59,7 @@ def i_created_a_video_with_subs_with_name(_step, sub_id):
|
||||
world.disable_jquery_animations()
|
||||
|
||||
world.wait_for_present('.is-initialized')
|
||||
world.wait(DELAY)
|
||||
assert not world.css_visible(SELECTORS['spinner'])
|
||||
world.wait_for_invisible(SELECTORS['spinner'])
|
||||
|
||||
|
||||
@step('I have uploaded subtitles "([^"]*)"$')
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
from xmodule.util.mock_youtube_server.mock_youtube_server import MockYoutubeServer
|
||||
from lettuce import before, after, world
|
||||
from django.conf import settings
|
||||
import threading
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
@before.all
|
||||
def setup_mock_youtube_server():
|
||||
server_host = '127.0.0.1'
|
||||
|
||||
server_port = settings.VIDEO_PORT
|
||||
|
||||
address = (server_host, server_port)
|
||||
|
||||
# Create the mock server instance
|
||||
server = MockYoutubeServer(address)
|
||||
logger.debug("Youtube server started at {} port".format(str(server_port)))
|
||||
|
||||
server.time_to_response = 0.1 # seconds
|
||||
|
||||
server.address = address
|
||||
|
||||
# Start the server running in a separate daemon thread
|
||||
# Because the thread is a daemon, it will terminate
|
||||
# when the main thread terminates.
|
||||
server_thread = threading.Thread(target=server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
# Store the server instance in lettuce's world
|
||||
# so that other steps can access it
|
||||
# (and we can shut it down later)
|
||||
world.youtube_server = server
|
||||
|
||||
|
||||
@after.all
|
||||
def teardown_mock_youtube_server(total):
|
||||
|
||||
# Stop the LTI server and free up the port
|
||||
world.youtube_server.shutdown()
|
||||
@@ -114,22 +114,6 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Because an override for where to run will affect which ports to use,
|
||||
# set this up after the local overrides.
|
||||
if LETTUCE_SELENIUM_CLIENT == 'saucelabs':
|
||||
LETTUCE_SERVER_PORT = choice(PORTS)
|
||||
else:
|
||||
LETTUCE_SERVER_PORT = randint(1024, 65535)
|
||||
|
||||
|
||||
# Set up Video information so that the cms will send
|
||||
# requests to a mock Youtube server running locally
|
||||
if LETTUCE_SELENIUM_CLIENT == 'saucelabs':
|
||||
VIDEO_PORT = choice(PORTS)
|
||||
PORTS.remove(VIDEO_PORT)
|
||||
else:
|
||||
VIDEO_PORT = randint(1024, 65535)
|
||||
|
||||
# for testing Youtube
|
||||
YOUTUBE_API['url'] = "http://127.0.0.1:" + str(VIDEO_PORT) + '/test_transcripts_youtube/'
|
||||
|
||||
# Point the URL used to test YouTube availability to our stub YouTube server
|
||||
YOUTUBE_TEST_URL = "http://127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT)
|
||||
YOUTUBE_API['url'] = "http://127.0.0.1:{0}/test_transcripts_youtube/".format(YOUTUBE_PORT)
|
||||
|
||||
@@ -368,6 +368,13 @@ CELERY_QUEUES = {
|
||||
DEFAULT_PRIORITY_QUEUE: {}
|
||||
}
|
||||
|
||||
|
||||
############################## Video ##########################################
|
||||
|
||||
# URL to test YouTube availability
|
||||
YOUTUBE_TEST_URL = 'https://gdata.youtube.com/feeds/api/videos/'
|
||||
|
||||
|
||||
############################ APPS #####################################
|
||||
|
||||
INSTALLED_APPS = (
|
||||
|
||||
@@ -155,6 +155,17 @@ CELERY_ALWAYS_EAGER = True
|
||||
CELERY_RESULT_BACKEND = 'cache'
|
||||
BROKER_TRANSPORT = 'memory'
|
||||
|
||||
|
||||
########################### Server Ports ###################################
|
||||
|
||||
# These ports are carefully chosen so that if the browser needs to
|
||||
# access them, they will be available through the SauceLabs SSH tunnel
|
||||
LETTUCE_SERVER_PORT = 8003
|
||||
XQUEUE_PORT = 8040
|
||||
YOUTUBE_PORT = 8031
|
||||
LTI_PORT = 8765
|
||||
|
||||
|
||||
################### Make tests faster
|
||||
# http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/
|
||||
PASSWORD_HASHERS = (
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from terrain.browser import *
|
||||
from terrain.steps import *
|
||||
from terrain.factories import *
|
||||
from terrain.start_stubs import *
|
||||
|
||||
37
common/djangoapps/terrain/start_stubs.py
Normal file
37
common/djangoapps/terrain/start_stubs.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Initialize and teardown fake HTTP services for use in acceptance tests.
|
||||
"""
|
||||
|
||||
from lettuce import before, after, world
|
||||
from django.conf import settings
|
||||
from terrain.stubs.youtube import StubYouTubeService
|
||||
from terrain.stubs.xqueue import StubXQueueService
|
||||
|
||||
|
||||
USAGE = "USAGE: python -m fakes.start SERVICE_NAME PORT_NUM"
|
||||
|
||||
SERVICES = {
|
||||
"youtube": {"port": settings.YOUTUBE_PORT, "class": StubYouTubeService},
|
||||
"xqueue": {"port": settings.XQUEUE_PORT, "class": StubXQueueService},
|
||||
}
|
||||
|
||||
|
||||
@before.all
|
||||
def start_stubs():
|
||||
"""
|
||||
Start each stub service running on a local port.
|
||||
"""
|
||||
for name, service in SERVICES.iteritems():
|
||||
fake_server = service['class'](port_num=service['port'])
|
||||
setattr(world, name, fake_server)
|
||||
|
||||
|
||||
@after.all
|
||||
def stop_stubs(_):
|
||||
"""
|
||||
Shut down each stub service.
|
||||
"""
|
||||
for name in SERVICES.keys():
|
||||
stub_server = getattr(world, name, None)
|
||||
if stub_server is not None:
|
||||
stub_server.shutdown()
|
||||
181
common/djangoapps/terrain/stubs/http.py
Normal file
181
common/djangoapps/terrain/stubs/http.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
Stub implementation of an HTTP service.
|
||||
"""
|
||||
|
||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||
import urlparse
|
||||
import threading
|
||||
import json
|
||||
from lazy import lazy
|
||||
|
||||
from logging import getLogger
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class StubHttpRequestHandler(BaseHTTPRequestHandler, object):
|
||||
"""
|
||||
Handler for the stub HTTP service.
|
||||
"""
|
||||
|
||||
protocol = "HTTP/1.0"
|
||||
|
||||
def log_message(self, format_str, *args):
|
||||
"""
|
||||
Redirect messages to keep the test console clean.
|
||||
"""
|
||||
|
||||
msg = "{0} - - [{1}] {2}\n".format(
|
||||
self.client_address[0],
|
||||
self.log_date_time_string(),
|
||||
format_str % args
|
||||
)
|
||||
|
||||
LOGGER.debug(msg)
|
||||
|
||||
@lazy
|
||||
def request_content(self):
|
||||
"""
|
||||
Retrieve the content of the request.
|
||||
"""
|
||||
try:
|
||||
length = int(self.headers.getheader('content-length'))
|
||||
|
||||
except (TypeError, ValueError):
|
||||
return ""
|
||||
else:
|
||||
return self.rfile.read(length)
|
||||
|
||||
@lazy
|
||||
def post_dict(self):
|
||||
"""
|
||||
Retrieve the request POST parameters from the client as a dictionary.
|
||||
If no POST parameters can be interpreted, return an empty dict.
|
||||
"""
|
||||
contents = self.request_content
|
||||
|
||||
# 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
|
||||
try:
|
||||
post_dict = urlparse.parse_qs(contents, keep_blank_values=True)
|
||||
return {
|
||||
key: list_val[0]
|
||||
for key, list_val in post_dict.items()
|
||||
}
|
||||
|
||||
except:
|
||||
return dict()
|
||||
|
||||
@lazy
|
||||
def get_params(self):
|
||||
"""
|
||||
Return the GET parameters (querystring in the URL).
|
||||
"""
|
||||
return urlparse.parse_qs(self.path)
|
||||
|
||||
def do_PUT(self):
|
||||
"""
|
||||
Allow callers to configure the stub server using the /set_config URL.
|
||||
"""
|
||||
if self.path == "/set_config" or self.path == "/set_config/":
|
||||
|
||||
for key, value in self.post_dict.iteritems():
|
||||
self.log_message("Set config '{0}' to '{1}'".format(key, value))
|
||||
|
||||
try:
|
||||
value = json.loads(value)
|
||||
|
||||
except ValueError:
|
||||
self.log_message(u"Could not parse JSON: {0}".format(value))
|
||||
self.send_response(400)
|
||||
|
||||
else:
|
||||
self.server.set_config(unicode(key, 'utf-8'), value)
|
||||
self.send_response(200)
|
||||
|
||||
else:
|
||||
self.send_response(404)
|
||||
|
||||
def send_response(self, status_code, content=None, headers=None):
|
||||
"""
|
||||
Send a response back to the client with the HTTP `status_code` (int),
|
||||
`content` (str) and `headers` (dict).
|
||||
"""
|
||||
self.log_message(
|
||||
"Sent HTTP response: {0} with content '{1}' and headers {2}".format(status_code, content, headers)
|
||||
)
|
||||
|
||||
if headers is None:
|
||||
headers = dict()
|
||||
|
||||
BaseHTTPRequestHandler.send_response(self, status_code)
|
||||
|
||||
for (key, value) in headers.items():
|
||||
self.send_header(key, value)
|
||||
|
||||
if len(headers) > 0:
|
||||
self.end_headers()
|
||||
|
||||
if content is not None:
|
||||
self.wfile.write(content)
|
||||
|
||||
|
||||
class StubHttpService(HTTPServer, object):
|
||||
"""
|
||||
Stub HTTP service implementation.
|
||||
"""
|
||||
|
||||
# Subclasses override this to provide the handler class to use.
|
||||
# Should be a subclass of `StubHttpRequestHandler`
|
||||
HANDLER_CLASS = StubHttpRequestHandler
|
||||
|
||||
def __init__(self, port_num=0):
|
||||
"""
|
||||
Configure the server to listen on localhost.
|
||||
Default is to choose an arbitrary open port.
|
||||
"""
|
||||
address = ('127.0.0.1', port_num)
|
||||
HTTPServer.__init__(self, address, self.HANDLER_CLASS)
|
||||
|
||||
# Create a dict to store configuration values set by the client
|
||||
self._config = dict()
|
||||
|
||||
# Start the server in a separate thread
|
||||
server_thread = threading.Thread(target=self.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
# Log the port we're using to help identify port conflict errors
|
||||
LOGGER.debug('Starting service on port {0}'.format(self.port))
|
||||
|
||||
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()
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
"""
|
||||
Return the port that the service is listening on.
|
||||
"""
|
||||
_, port = self.server_address
|
||||
return port
|
||||
|
||||
def config(self, key, default=None):
|
||||
"""
|
||||
Return the configuration value for `key`. If this
|
||||
value has not been set, return `default` instead.
|
||||
"""
|
||||
return self._config.get(key, default)
|
||||
|
||||
def set_config(self, key, value):
|
||||
"""
|
||||
Set the configuration `value` for `key`.
|
||||
"""
|
||||
self._config[key] = value
|
||||
|
||||
58
common/djangoapps/terrain/stubs/tests/test_http.py
Normal file
58
common/djangoapps/terrain/stubs/tests/test_http.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Unit tests for stub HTTP server base class.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import requests
|
||||
import json
|
||||
from terrain.stubs.http import StubHttpService
|
||||
|
||||
|
||||
class StubHttpServiceTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.server = StubHttpService()
|
||||
self.addCleanup(self.server.shutdown)
|
||||
|
||||
def test_configure(self):
|
||||
"""
|
||||
All HTTP stub servers have an end-point that allows
|
||||
clients to configure how the server responds.
|
||||
"""
|
||||
params = {
|
||||
'test_str': 'This is only a test',
|
||||
'test_int': 12345,
|
||||
'test_float': 123.45,
|
||||
'test_unicode': u'\u2603 the snowman',
|
||||
'test_dict': { 'test_key': 'test_val' }
|
||||
}
|
||||
|
||||
for key, val in params.iteritems():
|
||||
post_params = {key: json.dumps(val)}
|
||||
response = requests.put(
|
||||
"http://127.0.0.1:{0}/set_config".format(self.server.port),
|
||||
data=post_params
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that the expected values were set in the configuration
|
||||
for key, val in params.iteritems():
|
||||
self.assertEqual(self.server.config(key), val)
|
||||
|
||||
def test_default_config(self):
|
||||
self.assertEqual(self.server.config('not_set', default=42), 42)
|
||||
|
||||
def test_bad_json(self):
|
||||
response = requests.put(
|
||||
"http://127.0.0.1:{0}/set_config".format(self.server.port),
|
||||
data="{,}"
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_unknown_path(self):
|
||||
response = requests.put(
|
||||
"http://127.0.0.1:{0}/invalid_url".format(self.server.port),
|
||||
data="{}"
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
74
common/djangoapps/terrain/stubs/tests/test_xqueue_stub.py
Normal file
74
common/djangoapps/terrain/stubs/tests/test_xqueue_stub.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Unit tests for stub XQueue implementation.
|
||||
"""
|
||||
|
||||
import mock
|
||||
import unittest
|
||||
import json
|
||||
import urllib
|
||||
import time
|
||||
from terrain.stubs.xqueue import StubXQueueService
|
||||
|
||||
|
||||
class StubXQueueServiceTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.server = StubXQueueService()
|
||||
self.url = "http://127.0.0.1:{0}".format(self.server.port)
|
||||
self.addCleanup(self.server.shutdown)
|
||||
|
||||
# For testing purposes, do not delay the grading response
|
||||
self.server.set_config('response_delay', 0)
|
||||
|
||||
@mock.patch('requests.post')
|
||||
def test_grade_request(self, post):
|
||||
|
||||
# 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.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)
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
# Wait for the server to POST back to the callback URL
|
||||
# Time out if it takes too long
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < 5:
|
||||
if post.called:
|
||||
break
|
||||
|
||||
# Check that the POST request was made with the correct params
|
||||
post.assert_called_with(callback_url, data=expected_callback_dict)
|
||||
58
common/djangoapps/terrain/stubs/tests/test_youtube_stub.py
Normal file
58
common/djangoapps/terrain/stubs/tests/test_youtube_stub.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Unit test for stub YouTube implementation.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import requests
|
||||
from terrain.stubs.youtube import StubYouTubeService
|
||||
|
||||
|
||||
class StubYouTubeServiceTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.server = StubYouTubeService()
|
||||
self.url = "http://127.0.0.1:{0}/".format(self.server.port)
|
||||
self.server.set_config('time_to_response', 0.0)
|
||||
self.addCleanup(self.server.shutdown)
|
||||
|
||||
def test_unused_url(self):
|
||||
response = requests.get(self.url + 'unused_url')
|
||||
self.assertEqual("Unused url", response.content)
|
||||
|
||||
def test_video_url(self):
|
||||
response = requests.get(
|
||||
self.url + 'test_youtube/OEoXaMPEzfM?v=2&alt=jsonc&callback=callback_func'
|
||||
)
|
||||
|
||||
self.assertEqual('callback_func({"message": "I\'m youtube."})', response.content)
|
||||
|
||||
def test_transcript_url_equal(self):
|
||||
response = requests.get(
|
||||
self.url + 'test_transcripts_youtube/t__eq_exist'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"".join([
|
||||
'<?xml version="1.0" encoding="utf-8" ?>',
|
||||
'<transcript><text start="1.0" dur="1.0">',
|
||||
'Equal transcripts</text></transcript>'
|
||||
]), response.content
|
||||
)
|
||||
|
||||
def test_transcript_url_not_equal(self):
|
||||
response = requests.get(
|
||||
self.url + 'test_transcripts_youtube/t_neq_exist',
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"".join([
|
||||
'<?xml version="1.0" encoding="utf-8" ?>',
|
||||
'<transcript><text start="1.1" dur="5.5">',
|
||||
'Transcripts sample, different that on server',
|
||||
'</text></transcript>'
|
||||
]), response.content
|
||||
)
|
||||
|
||||
def test_transcript_not_found(self):
|
||||
response = requests.get(self.url + 'test_transcripts_youtube/some_id')
|
||||
self.assertEqual(404, response.status_code)
|
||||
122
common/djangoapps/terrain/stubs/xqueue.py
Normal file
122
common/djangoapps/terrain/stubs/xqueue.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
Stub implementation of XQueue for acceptance tests.
|
||||
"""
|
||||
|
||||
from .http import StubHttpRequestHandler, StubHttpService
|
||||
import json
|
||||
import requests
|
||||
import threading
|
||||
|
||||
|
||||
class StubXQueueHandler(StubHttpRequestHandler):
|
||||
"""
|
||||
A handler for XQueue POST requests.
|
||||
"""
|
||||
|
||||
DEFAULT_RESPONSE_DELAY = 2
|
||||
DEFAULT_GRADE_RESPONSE = {'correct': True, 'score': 1, 'msg': ''}
|
||||
|
||||
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.
|
||||
"""
|
||||
msg = "XQueue received POST request {0} to path {1}".format(self.post_dict, self.path)
|
||||
self.log_message(msg)
|
||||
|
||||
# Respond only to grading requests
|
||||
if self._is_grade_request():
|
||||
try:
|
||||
xqueue_header = json.loads(self.post_dict['xqueue_header'])
|
||||
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
|
||||
)
|
||||
|
||||
threading.Timer(
|
||||
self.server.config('response_delay', default=self.DEFAULT_RESPONSE_DELAY),
|
||||
delayed_grade_func
|
||||
).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_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}
|
||||
)
|
||||
|
||||
if self._is_grade_request():
|
||||
self.send_response(
|
||||
200, content=response_str, headers={'Content-type': 'text/plain'}
|
||||
)
|
||||
self.log_message("XQueue: sent response {0}".format(response_str))
|
||||
|
||||
else:
|
||||
self.send_response(500)
|
||||
|
||||
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
|
||||
"""
|
||||
# Get the grade response from the server configuration
|
||||
grade_response = self.server.config('grade_response', default=self.DEFAULT_GRADE_RESPONSE)
|
||||
|
||||
# Wrap the message in <div> tags to ensure that it is valid XML
|
||||
if isinstance(grade_response, dict) and 'msg' in grade_response:
|
||||
grade_response['msg'] = "<div>{0}</div>".format(grade_response['msg'])
|
||||
|
||||
data = {
|
||||
'xqueue_header': json.dumps(xqueue_header),
|
||||
'xqueue_body': json.dumps(grade_response)
|
||||
}
|
||||
|
||||
requests.post(postback_url, data=data)
|
||||
self.log_message("XQueue: sent grading response {0}".format(data))
|
||||
|
||||
def _is_grade_request(self):
|
||||
return 'xqueue/submit' in self.path
|
||||
|
||||
|
||||
class StubXQueueService(StubHttpService):
|
||||
"""
|
||||
A stub XQueue grading server that responds to POST requests to localhost.
|
||||
"""
|
||||
|
||||
HANDLER_CLASS = StubXQueueHandler
|
||||
85
common/djangoapps/terrain/stubs/youtube.py
Normal file
85
common/djangoapps/terrain/stubs/youtube.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Stub implementation of YouTube for acceptance tests.
|
||||
"""
|
||||
|
||||
from .http import StubHttpRequestHandler, StubHttpService
|
||||
import json
|
||||
import time
|
||||
import requests
|
||||
|
||||
|
||||
class StubYouTubeHandler(StubHttpRequestHandler):
|
||||
"""
|
||||
A handler for Youtube GET requests.
|
||||
"""
|
||||
|
||||
# Default number of seconds to delay the response to simulate network latency.
|
||||
DEFAULT_DELAY_SEC = 0.5
|
||||
|
||||
def do_GET(self):
|
||||
"""
|
||||
Handle a GET request from the client and sends response back.
|
||||
"""
|
||||
|
||||
self.log_message(
|
||||
"Youtube provider received GET request to path {}".format(self.path)
|
||||
)
|
||||
|
||||
if 'test_transcripts_youtube' in self.path:
|
||||
|
||||
if 't__eq_exist' in self.path:
|
||||
status_message = "".join([
|
||||
'<?xml version="1.0" encoding="utf-8" ?>',
|
||||
'<transcript><text start="1.0" dur="1.0">',
|
||||
'Equal transcripts</text></transcript>'
|
||||
])
|
||||
|
||||
self.send_response(
|
||||
200, content=status_message, headers={'Content-type': 'application/xml'}
|
||||
)
|
||||
|
||||
elif 't_neq_exist' in self.path:
|
||||
status_message = "".join([
|
||||
'<?xml version="1.0" encoding="utf-8" ?>',
|
||||
'<transcript><text start="1.1" dur="5.5">',
|
||||
'Transcripts sample, different that on server',
|
||||
'</text></transcript>'
|
||||
])
|
||||
|
||||
self.send_response(
|
||||
200, content=status_message, headers={'Content-type': 'application/xml'}
|
||||
)
|
||||
|
||||
else:
|
||||
self.send_response(404)
|
||||
|
||||
elif 'test_youtube' in self.path:
|
||||
self._send_video_response("I'm youtube.")
|
||||
|
||||
else:
|
||||
self.send_response(
|
||||
404, content="Unused url", headers={'Content-type': 'text/plain'}
|
||||
)
|
||||
|
||||
def _send_video_response(self, message):
|
||||
"""
|
||||
Send message back to the client for video player requests.
|
||||
Requires sending back callback id.
|
||||
"""
|
||||
# Delay the response to simulate network latency
|
||||
time.sleep(self.server.config('time_to_response', self.DEFAULT_DELAY_SEC))
|
||||
|
||||
# Construct the response content
|
||||
callback = self.get_params['callback'][0]
|
||||
response = callback + '({})'.format(json.dumps({'message': message}))
|
||||
|
||||
self.send_response(200, content=response, headers={'Content-type': 'text/html'})
|
||||
self.log_message("Youtube: sent response {}".format(message))
|
||||
|
||||
|
||||
class StubYouTubeService(StubHttpService):
|
||||
"""
|
||||
A stub Youtube provider server that responds to GET requests to localhost.
|
||||
"""
|
||||
|
||||
HANDLER_CLASS = StubYouTubeHandler
|
||||
@@ -10,7 +10,7 @@ from textwrap import dedent
|
||||
from urllib import quote_plus
|
||||
from selenium.common.exceptions import (
|
||||
WebDriverException, TimeoutException,
|
||||
StaleElementReferenceException)
|
||||
StaleElementReferenceException, InvalidElementStateException)
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
@@ -581,7 +581,7 @@ def trigger_event(css_selector, event='change', index=0):
|
||||
|
||||
|
||||
@world.absorb
|
||||
def retry_on_exception(func, max_attempts=5, ignored_exceptions=StaleElementReferenceException):
|
||||
def retry_on_exception(func, max_attempts=5, ignored_exceptions=(StaleElementReferenceException, InvalidElementStateException)):
|
||||
"""
|
||||
Retry the interaction, ignoring the passed exceptions.
|
||||
By default ignore StaleElementReferenceException, which happens often in our application
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||
import json
|
||||
import mock
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urlparse
|
||||
from logging import getLogger
|
||||
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
class MockYoutubeRequestHandler(BaseHTTPRequestHandler):
|
||||
'''
|
||||
A handler for Youtube GET requests.
|
||||
'''
|
||||
|
||||
protocol = "HTTP/1.0"
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""Log an arbitrary message."""
|
||||
# Code copied from BaseHTTPServer.py. Changed to write to sys.stdout
|
||||
# so that messages won't pollute test output.
|
||||
sys.stdout.write("%s - - [%s] %s\n" %
|
||||
(self.client_address[0],
|
||||
self.log_date_time_string(),
|
||||
format % args))
|
||||
|
||||
def do_HEAD(self):
|
||||
code = 200
|
||||
if 'test_transcripts_youtube' in self.path:
|
||||
if not 'trans_exist' in self.path:
|
||||
code = 404
|
||||
self._send_head(code)
|
||||
|
||||
def do_GET(self):
|
||||
'''
|
||||
Handle a GET request from the client and sends response back.
|
||||
'''
|
||||
logger.debug("Youtube provider received GET request to path {}".format(
|
||||
self.path)
|
||||
) # Log the request
|
||||
|
||||
if 'test_transcripts_youtube' in self.path:
|
||||
if 't__eq_exist' in self.path:
|
||||
status_message = """<?xml version="1.0" encoding="utf-8" ?><transcript><text start="1.0" dur="1.0">Equal transcripts</text></transcript>"""
|
||||
self._send_head()
|
||||
self._send_transcripts_response(status_message)
|
||||
elif 't_neq_exist' in self.path:
|
||||
status_message = """<?xml version="1.0" encoding="utf-8" ?><transcript><text start="1.1" dur="5.5">Transcripts sample, different that on server</text></transcript>"""
|
||||
self._send_head()
|
||||
self._send_transcripts_response(status_message)
|
||||
else:
|
||||
self._send_head(404)
|
||||
elif 'test_youtube' in self.path:
|
||||
self._send_head()
|
||||
#testing videoplayers
|
||||
status_message = "I'm youtube."
|
||||
response_timeout = float(self.server.time_to_response)
|
||||
|
||||
# threading timer produces TypeError: 'NoneType' object is not callable here
|
||||
# so we use time.sleep, as we already in separate thread.
|
||||
time.sleep(response_timeout)
|
||||
self._send_video_response(status_message)
|
||||
else:
|
||||
# unused url
|
||||
self._send_head()
|
||||
self._send_transcripts_response('Unused url')
|
||||
logger.debug("Request to unused url.")
|
||||
|
||||
def _send_head(self, code=200):
|
||||
'''
|
||||
Send the response code and MIME headers
|
||||
'''
|
||||
|
||||
self.send_response(code)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
|
||||
def _send_transcripts_response(self, message):
|
||||
'''
|
||||
Send message back to the client for transcripts ajax requests.
|
||||
'''
|
||||
response = message
|
||||
# Log the response
|
||||
logger.debug("Youtube: sent response {}".format(message))
|
||||
|
||||
self.wfile.write(response)
|
||||
|
||||
def _send_video_response(self, message):
|
||||
'''
|
||||
Send message back to the client for video player requests.
|
||||
Requires sending back callback id.
|
||||
'''
|
||||
callback = urlparse.parse_qs(self.path)['callback'][0]
|
||||
response = callback + '({})'.format(json.dumps({'message': message}))
|
||||
# Log the response
|
||||
logger.debug("Youtube: sent response {}".format(message))
|
||||
|
||||
self.wfile.write(response)
|
||||
|
||||
|
||||
class MockYoutubeServer(HTTPServer):
|
||||
'''
|
||||
A mock Youtube provider server that responds
|
||||
to GET requests to localhost.
|
||||
'''
|
||||
|
||||
def __init__(self, address):
|
||||
'''
|
||||
Initialize the mock XQueue server instance.
|
||||
|
||||
*address* is the (host, host's port to listen to) tuple.
|
||||
'''
|
||||
handler = MockYoutubeRequestHandler
|
||||
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()
|
||||
@@ -1,77 +0,0 @@
|
||||
"""
|
||||
Test for Mock_Youtube_Server
|
||||
"""
|
||||
import unittest
|
||||
import threading
|
||||
import requests
|
||||
from mock_youtube_server import MockYoutubeServer
|
||||
|
||||
|
||||
class MockYoutubeServerTest(unittest.TestCase):
|
||||
'''
|
||||
A mock version of the YouTube provider server that listens on a local
|
||||
port and responds with jsonp.
|
||||
|
||||
Used for lettuce BDD tests in lms/courseware/features/video.feature
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
|
||||
# Create the server
|
||||
server_port = 8034
|
||||
server_host = '127.0.0.1'
|
||||
address = (server_host, server_port)
|
||||
self.server = MockYoutubeServer(address, )
|
||||
self.server.time_to_response = 0.5
|
||||
# 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_request(self):
|
||||
"""
|
||||
Tests that Youtube server processes request with right program
|
||||
path, and responses with incorrect signature.
|
||||
"""
|
||||
# GET request
|
||||
|
||||
# unused url
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/some url',
|
||||
)
|
||||
self.assertEqual("Unused url", response.content)
|
||||
|
||||
# video player test url, callback shoud be presented in url params
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/test_youtube/OEoXaMPEzfM?v=2&alt=jsonc&callback=callback_func',
|
||||
)
|
||||
self.assertEqual("""callback_func({"message": "I\'m youtube."})""", response.content)
|
||||
|
||||
# transcripts test url
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/test_transcripts_youtube/t__eq_exist',
|
||||
)
|
||||
self.assertEqual(
|
||||
'<?xml version="1.0" encoding="utf-8" ?><transcript><text start="1.0" dur="1.0">Equal transcripts</text></transcript>',
|
||||
response.content
|
||||
)
|
||||
|
||||
# transcripts test url
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/test_transcripts_youtube/t_neq_exist',
|
||||
)
|
||||
self.assertEqual(
|
||||
'<?xml version="1.0" encoding="utf-8" ?><transcript><text start="1.1" dur="5.5">Transcripts sample, different that on server</text></transcript>',
|
||||
response.content
|
||||
)
|
||||
|
||||
# transcripts test url, not trans_exist youtube_id, so 404 should be returned
|
||||
response = requests.get(
|
||||
'http://127.0.0.1:8034/test_transcripts_youtube/some_id',
|
||||
)
|
||||
self.assertEqual(404, response.status_code)
|
||||
@@ -167,12 +167,6 @@ class VideoModule(VideoFields, XModule):
|
||||
sources = {get_ext(src): src for src in self.html5_sources}
|
||||
sources['main'] = self.source
|
||||
|
||||
# for testing Youtube timeout in acceptance tests
|
||||
if getattr(settings, 'VIDEO_PORT', None):
|
||||
yt_test_url = "http://127.0.0.1:" + str(settings.VIDEO_PORT) + '/test_youtube/'
|
||||
else:
|
||||
yt_test_url = 'https://gdata.youtube.com/feeds/api/videos/'
|
||||
|
||||
return self.system.render_template('video.html', {
|
||||
'youtube_streams': _create_youtube_string(self),
|
||||
'id': self.location.html_id(),
|
||||
@@ -191,7 +185,7 @@ class VideoModule(VideoFields, XModule):
|
||||
# TODO: Later on the value 1500 should be taken from some global
|
||||
# configuration setting field.
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_test_url': yt_test_url
|
||||
'yt_test_url': settings.YOUTUBE_TEST_URL
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -14,9 +14,7 @@ logger = getLogger(__name__)
|
||||
def setup_mock_lti_server():
|
||||
|
||||
server_host = '127.0.0.1'
|
||||
|
||||
# Add +1 to XQUEUE random port number
|
||||
server_port = settings.XQUEUE_PORT + 1
|
||||
server_port = settings.LTI_PORT
|
||||
|
||||
address = (server_host, server_port)
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ def set_external_grader_response(step, correctness):
|
||||
|
||||
# Set the fake xqueue server to always respond
|
||||
# correct/incorrect when asked to grade a problem
|
||||
world.xqueue_server.set_grade_response(response_dict)
|
||||
world.xqueue.set_config('grade_response', response_dict)
|
||||
|
||||
|
||||
@step(u'I answer a "([^"]*)" problem "([^"]*)ly"')
|
||||
|
||||
@@ -18,7 +18,7 @@ Feature: LMS.Video component
|
||||
# 3
|
||||
# Youtube testing
|
||||
Scenario: Video component is fully rendered in the LMS in Youtube mode with HTML5 sources
|
||||
Given youtube server is up and response time is 0.4 seconds
|
||||
Given youtube server is up and response time is 0.4 seconds
|
||||
And the course has a Video component in Youtube_HTML5 mode
|
||||
Then when I view the video it has rendered in Youtube mode
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ def add_video_to_course(course, player_mode):
|
||||
|
||||
@step('youtube server is up and response time is (.*) seconds$')
|
||||
def set_youtube_response_timeout(_step, time):
|
||||
world.youtube_server.time_to_response = time
|
||||
world.youtube.set_config('time_to_response', float(time))
|
||||
|
||||
|
||||
@step('when I view the video it has rendered in (.*) mode$')
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from courseware.mock_xqueue_server.mock_xqueue_server import MockXQueueServer
|
||||
from lettuce import before, after, world
|
||||
from django.conf import settings
|
||||
import threading
|
||||
|
||||
@before.all
|
||||
def setup_mock_xqueue_server():
|
||||
|
||||
# Retrieve the local port from settings
|
||||
server_port = settings.XQUEUE_PORT
|
||||
|
||||
# Create the mock server instance
|
||||
server = MockXQueueServer(server_port)
|
||||
|
||||
# Start the server running in a separate daemon thread
|
||||
# Because the thread is a daemon, it will terminate
|
||||
# when the main thread terminates.
|
||||
server_thread = threading.Thread(target=server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
# Store the server instance in lettuce's world
|
||||
# so that other steps can access it
|
||||
# (and we can shut it down later)
|
||||
world.xqueue_server = server
|
||||
|
||||
|
||||
@after.all
|
||||
def teardown_mock_xqueue_server(total):
|
||||
|
||||
# Stop the xqueue server and free up the port
|
||||
world.xqueue_server.shutdown()
|
||||
@@ -1,46 +0,0 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
from xmodule.util.mock_youtube_server.mock_youtube_server import MockYoutubeServer
|
||||
from lettuce import before, after, world
|
||||
from django.conf import settings
|
||||
import threading
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
@before.all
|
||||
def setup_mock_youtube_server():
|
||||
# import ipdb; ipdb.set_trace()
|
||||
server_host = '127.0.0.1'
|
||||
|
||||
server_port = settings.VIDEO_PORT
|
||||
|
||||
address = (server_host, server_port)
|
||||
|
||||
# Create the mock server instance
|
||||
server = MockYoutubeServer(address)
|
||||
logger.debug("Youtube server started at {} port".format(str(server_port)))
|
||||
|
||||
server.time_to_response = 1 # seconds
|
||||
|
||||
server.address = address
|
||||
|
||||
# Start the server running in a separate daemon thread
|
||||
# Because the thread is a daemon, it will terminate
|
||||
# when the main thread terminates.
|
||||
server_thread = threading.Thread(target=server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
# Store the server instance in lettuce's world
|
||||
# so that other steps can access it
|
||||
# (and we can shut it down later)
|
||||
world.youtube_server = server
|
||||
|
||||
|
||||
@after.all
|
||||
def teardown_mock_youtube_server(total):
|
||||
|
||||
# Stop the LTI server and free up the port
|
||||
world.youtube_server.shutdown()
|
||||
@@ -1,211 +0,0 @@
|
||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||
import json
|
||||
import urllib
|
||||
import urlparse
|
||||
import threading
|
||||
|
||||
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
|
||||
@@ -1,84 +0,0 @@
|
||||
import mock
|
||||
import unittest
|
||||
import threading
|
||||
import json
|
||||
import urllib
|
||||
import time
|
||||
from mock_xqueue_server import MockXQueueServer, MockXQueueRequestHandler
|
||||
|
||||
from nose.plugins.skip import SkipTest
|
||||
|
||||
|
||||
class MockXQueueServerTest(unittest.TestCase):
|
||||
'''
|
||||
A mock version of the XQueue server that listens on a local
|
||||
port and responds with pre-defined grade messages.
|
||||
|
||||
Used for lettuce BDD tests in lms/courseware/features/problems.feature
|
||||
and lms/courseware/features/problems.py
|
||||
|
||||
This is temporary and will be removed when XQueue is
|
||||
rewritten using celery.
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
|
||||
# This is a test of the test setup,
|
||||
# so it does not need to run as part of the unit test suite
|
||||
# You can re-enable it by commenting out the line below
|
||||
raise SkipTest
|
||||
|
||||
# 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)
|
||||
@@ -152,6 +152,7 @@ SELENIUM_GRID = {
|
||||
'BROWSER': LETTUCE_BROWSER,
|
||||
}
|
||||
|
||||
|
||||
#####################################################################
|
||||
# See if the developer has any local overrides.
|
||||
try:
|
||||
@@ -161,22 +162,9 @@ except ImportError:
|
||||
|
||||
# Because an override for where to run will affect which ports to use,
|
||||
# set these up after the local overrides.
|
||||
if LETTUCE_SELENIUM_CLIENT == 'saucelabs':
|
||||
LETTUCE_SERVER_PORT = choice(PORTS)
|
||||
PORTS.remove(LETTUCE_SERVER_PORT)
|
||||
else:
|
||||
LETTUCE_SERVER_PORT = randint(1024, 65535)
|
||||
|
||||
# Set up XQueue information so that the lms will send
|
||||
# requests to a mock XQueue server running locally
|
||||
if LETTUCE_SELENIUM_CLIENT == 'saucelabs':
|
||||
XQUEUE_PORT = choice(PORTS)
|
||||
PORTS.remove(XQUEUE_PORT)
|
||||
else:
|
||||
XQUEUE_PORT = randint(1024, 65535)
|
||||
|
||||
# Configure XQueue interface to use our stub XQueue server
|
||||
XQUEUE_INTERFACE = {
|
||||
"url": "http://127.0.0.1:%d" % XQUEUE_PORT,
|
||||
"url": "http://127.0.0.1:{0:d}".format(XQUEUE_PORT),
|
||||
"django_auth": {
|
||||
"username": "lms",
|
||||
"password": "***REMOVED***"
|
||||
@@ -184,10 +172,5 @@ XQUEUE_INTERFACE = {
|
||||
"basic_auth": ('anant', 'agarwal'),
|
||||
}
|
||||
|
||||
# Set up Video information so that the lms will send
|
||||
# requests to a mock Youtube server running locally
|
||||
if LETTUCE_SELENIUM_CLIENT == 'saucelabs':
|
||||
VIDEO_PORT = choice(PORTS)
|
||||
PORTS.remove(VIDEO_PORT)
|
||||
else:
|
||||
VIDEO_PORT = randint(1024, 65535)
|
||||
# Point the URL used to test YouTube availability to our stub YouTube server
|
||||
YOUTUBE_TEST_URL = "http://127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT)
|
||||
|
||||
@@ -921,6 +921,11 @@ BULK_EMAIL_LOG_SENT_EMAILS = False
|
||||
BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS = 0.02
|
||||
|
||||
|
||||
############################## Video ##########################################
|
||||
|
||||
# URL to test YouTube availability
|
||||
YOUTUBE_TEST_URL = 'https://gdata.youtube.com/feeds/api/videos/'
|
||||
|
||||
|
||||
################################### APPS ######################################
|
||||
INSTALLED_APPS = (
|
||||
|
||||
@@ -248,6 +248,16 @@ FILE_UPLOAD_HANDLERS = (
|
||||
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
|
||||
)
|
||||
|
||||
########################### Server Ports ###################################
|
||||
|
||||
# These ports are carefully chosen so that if the browser needs to
|
||||
# access them, they will be available through the SauceLabs SSH tunnel
|
||||
LETTUCE_SERVER_PORT = 8003
|
||||
XQUEUE_PORT = 8040
|
||||
YOUTUBE_PORT = 8031
|
||||
LTI_PORT = 8765
|
||||
|
||||
|
||||
################### Make tests faster
|
||||
|
||||
#http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/
|
||||
|
||||
Reference in New Issue
Block a user