Moved stub servers to terrain
Refactored stub services for style and DRY Added unit tests for stub implementations Updated acceptance tests that depend on stubs. Updated Studio acceptance tests to use YouTube stub server; fixed failing tests in devstack.
This commit is contained in:
@@ -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
|
||||
|
||||
0
common/djangoapps/terrain/stubs/tests/__init__.py
Normal file
0
common/djangoapps/terrain/stubs/tests/__init__.py
Normal file
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
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user