From 0fd03cfb020f2cd32afc57f4d7a2441fdbcd3903 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Sat, 23 Nov 2013 14:19:56 -0500 Subject: [PATCH] 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. --- .../contentstore/features/transcripts.feature | 3 +- .../contentstore/features/transcripts.py | 30 ++- cms/djangoapps/contentstore/features/video.py | 5 +- .../contentstore/features/youtube_setup.py | 45 ---- cms/envs/acceptance.py | 22 +- cms/envs/common.py | 7 + cms/envs/test.py | 11 + common/djangoapps/terrain/__init__.py | 1 + common/djangoapps/terrain/start_stubs.py | 37 +++ .../terrain/stubs}/__init__.py | 0 common/djangoapps/terrain/stubs/http.py | 181 +++++++++++++++ .../terrain/stubs/tests}/__init__.py | 0 .../terrain/stubs/tests/test_http.py | 58 +++++ .../terrain/stubs/tests/test_xqueue_stub.py | 74 ++++++ .../terrain/stubs/tests/test_youtube_stub.py | 58 +++++ common/djangoapps/terrain/stubs/xqueue.py | 122 ++++++++++ common/djangoapps/terrain/stubs/youtube.py | 85 +++++++ common/djangoapps/terrain/ui_helpers.py | 4 +- .../mock_youtube_server.py | 125 ----------- .../test_mock_youtube_server.py | 77 ------- common/lib/xmodule/xmodule/video_module.py | 8 +- .../courseware/features/lti_setup.py | 4 +- .../courseware/features/problems.py | 2 +- .../courseware/features/video.feature | 2 +- lms/djangoapps/courseware/features/video.py | 2 +- .../courseware/features/xqueue_setup.py | 35 --- .../courseware/features/youtube_setup.py | 46 ---- .../mock_xqueue_server/mock_xqueue_server.py | 211 ------------------ .../test_mock_xqueue_server.py | 84 ------- lms/envs/acceptance.py | 27 +-- lms/envs/common.py | 5 + lms/envs/test.py | 10 + 32 files changed, 682 insertions(+), 699 deletions(-) delete mode 100644 cms/djangoapps/contentstore/features/youtube_setup.py create mode 100644 common/djangoapps/terrain/start_stubs.py rename common/{lib/xmodule/xmodule/util/mock_youtube_server => djangoapps/terrain/stubs}/__init__.py (100%) create mode 100644 common/djangoapps/terrain/stubs/http.py rename {lms/djangoapps/courseware/mock_xqueue_server => common/djangoapps/terrain/stubs/tests}/__init__.py (100%) create mode 100644 common/djangoapps/terrain/stubs/tests/test_http.py create mode 100644 common/djangoapps/terrain/stubs/tests/test_xqueue_stub.py create mode 100644 common/djangoapps/terrain/stubs/tests/test_youtube_stub.py create mode 100644 common/djangoapps/terrain/stubs/xqueue.py create mode 100644 common/djangoapps/terrain/stubs/youtube.py delete mode 100644 common/lib/xmodule/xmodule/util/mock_youtube_server/mock_youtube_server.py delete mode 100644 common/lib/xmodule/xmodule/util/mock_youtube_server/test_mock_youtube_server.py delete mode 100644 lms/djangoapps/courseware/features/xqueue_setup.py delete mode 100644 lms/djangoapps/courseware/features/youtube_setup.py delete mode 100644 lms/djangoapps/courseware/mock_xqueue_server/mock_xqueue_server.py delete mode 100644 lms/djangoapps/courseware/mock_xqueue_server/test_mock_xqueue_server.py diff --git a/cms/djangoapps/contentstore/features/transcripts.feature b/cms/djangoapps/contentstore/features/transcripts.feature index bf5022dd8a..9ca8f349ab 100644 --- a/cms/djangoapps/contentstore/features/transcripts.feature +++ b/cms/djangoapps/contentstore/features/transcripts.feature @@ -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 diff --git a/cms/djangoapps/contentstore/features/transcripts.py b/cms/djangoapps/contentstore/features/transcripts.py index c6060803ba..58f0cd6067 100644 --- a/cms/djangoapps/contentstore/features/transcripts.py +++ b/cms/djangoapps/contentstore/features/transcripts.py @@ -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() diff --git a/cms/djangoapps/contentstore/features/video.py b/cms/djangoapps/contentstore/features/video.py index c97dba10b9..a293b13727 100644 --- a/cms/djangoapps/contentstore/features/video.py +++ b/cms/djangoapps/contentstore/features/video.py @@ -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 "([^"]*)"$') diff --git a/cms/djangoapps/contentstore/features/youtube_setup.py b/cms/djangoapps/contentstore/features/youtube_setup.py deleted file mode 100644 index 875b9f2286..0000000000 --- a/cms/djangoapps/contentstore/features/youtube_setup.py +++ /dev/null @@ -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() diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 67ecfa5689..b47596c9c5 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -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) diff --git a/cms/envs/common.py b/cms/envs/common.py index daea3a3f0e..d4513900e6 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -359,6 +359,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 = ( diff --git a/cms/envs/test.py b/cms/envs/test.py index a35824ed52..24f9e08a0a 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -150,6 +150,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 = ( diff --git a/common/djangoapps/terrain/__init__.py b/common/djangoapps/terrain/__init__.py index 3445a01d17..2740ff326b 100644 --- a/common/djangoapps/terrain/__init__.py +++ b/common/djangoapps/terrain/__init__.py @@ -4,3 +4,4 @@ from terrain.browser import * from terrain.steps import * from terrain.factories import * +from terrain.start_stubs import * diff --git a/common/djangoapps/terrain/start_stubs.py b/common/djangoapps/terrain/start_stubs.py new file mode 100644 index 0000000000..aec88a1cc1 --- /dev/null +++ b/common/djangoapps/terrain/start_stubs.py @@ -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() diff --git a/common/lib/xmodule/xmodule/util/mock_youtube_server/__init__.py b/common/djangoapps/terrain/stubs/__init__.py similarity index 100% rename from common/lib/xmodule/xmodule/util/mock_youtube_server/__init__.py rename to common/djangoapps/terrain/stubs/__init__.py diff --git a/common/djangoapps/terrain/stubs/http.py b/common/djangoapps/terrain/stubs/http.py new file mode 100644 index 0000000000..14dcf8ac2f --- /dev/null +++ b/common/djangoapps/terrain/stubs/http.py @@ -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 + diff --git a/lms/djangoapps/courseware/mock_xqueue_server/__init__.py b/common/djangoapps/terrain/stubs/tests/__init__.py similarity index 100% rename from lms/djangoapps/courseware/mock_xqueue_server/__init__.py rename to common/djangoapps/terrain/stubs/tests/__init__.py diff --git a/common/djangoapps/terrain/stubs/tests/test_http.py b/common/djangoapps/terrain/stubs/tests/test_http.py new file mode 100644 index 0000000000..fb09bad173 --- /dev/null +++ b/common/djangoapps/terrain/stubs/tests/test_http.py @@ -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) diff --git a/common/djangoapps/terrain/stubs/tests/test_xqueue_stub.py b/common/djangoapps/terrain/stubs/tests/test_xqueue_stub.py new file mode 100644 index 0000000000..222792ebb3 --- /dev/null +++ b/common/djangoapps/terrain/stubs/tests/test_xqueue_stub.py @@ -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': '
'} + ) + + 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) diff --git a/common/djangoapps/terrain/stubs/tests/test_youtube_stub.py b/common/djangoapps/terrain/stubs/tests/test_youtube_stub.py new file mode 100644 index 0000000000..a3f048482c --- /dev/null +++ b/common/djangoapps/terrain/stubs/tests/test_youtube_stub.py @@ -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([ + '', + '', + 'Equal transcripts' + ]), response.content + ) + + def test_transcript_url_not_equal(self): + response = requests.get( + self.url + 'test_transcripts_youtube/t_neq_exist', + ) + + self.assertEqual( + "".join([ + '', + '', + 'Transcripts sample, different that on server', + '' + ]), response.content + ) + + def test_transcript_not_found(self): + response = requests.get(self.url + 'test_transcripts_youtube/some_id') + self.assertEqual(404, response.status_code) diff --git a/common/djangoapps/terrain/stubs/xqueue.py b/common/djangoapps/terrain/stubs/xqueue.py new file mode 100644 index 0000000000..96e8e78f7b --- /dev/null +++ b/common/djangoapps/terrain/stubs/xqueue.py @@ -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
tags to ensure that it is valid XML + if isinstance(grade_response, dict) and 'msg' in grade_response: + grade_response['msg'] = "
{0}
".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 diff --git a/common/djangoapps/terrain/stubs/youtube.py b/common/djangoapps/terrain/stubs/youtube.py new file mode 100644 index 0000000000..07e4b3c92a --- /dev/null +++ b/common/djangoapps/terrain/stubs/youtube.py @@ -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([ + '', + '', + 'Equal transcripts' + ]) + + self.send_response( + 200, content=status_message, headers={'Content-type': 'application/xml'} + ) + + elif 't_neq_exist' in self.path: + status_message = "".join([ + '', + '', + 'Transcripts sample, different that on server', + '' + ]) + + 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 diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 2e6a6efc3b..bc61c0eed6 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -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 diff --git a/common/lib/xmodule/xmodule/util/mock_youtube_server/mock_youtube_server.py b/common/lib/xmodule/xmodule/util/mock_youtube_server/mock_youtube_server.py deleted file mode 100644 index 3fc7c8285e..0000000000 --- a/common/lib/xmodule/xmodule/util/mock_youtube_server/mock_youtube_server.py +++ /dev/null @@ -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 = """Equal transcripts""" - self._send_head() - self._send_transcripts_response(status_message) - elif 't_neq_exist' in self.path: - status_message = """Transcripts sample, different that on server""" - 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() diff --git a/common/lib/xmodule/xmodule/util/mock_youtube_server/test_mock_youtube_server.py b/common/lib/xmodule/xmodule/util/mock_youtube_server/test_mock_youtube_server.py deleted file mode 100644 index bb65760905..0000000000 --- a/common/lib/xmodule/xmodule/util/mock_youtube_server/test_mock_youtube_server.py +++ /dev/null @@ -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( - 'Equal transcripts', - response.content - ) - - # transcripts test url - response = requests.get( - 'http://127.0.0.1:8034/test_transcripts_youtube/t_neq_exist', - ) - self.assertEqual( - 'Transcripts sample, different that on server', - 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) diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index b7ca37b16e..576c32782e 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -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 }) diff --git a/lms/djangoapps/courseware/features/lti_setup.py b/lms/djangoapps/courseware/features/lti_setup.py index e86aa78ed2..40fb2ee969 100644 --- a/lms/djangoapps/courseware/features/lti_setup.py +++ b/lms/djangoapps/courseware/features/lti_setup.py @@ -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) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 0be5b72284..278855a761 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -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"') diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature index 7518bf6bdb..1ca82b02a1 100644 --- a/lms/djangoapps/courseware/features/video.feature +++ b/lms/djangoapps/courseware/features/video.feature @@ -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 diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py index 57fc8a0a9a..3635373437 100644 --- a/lms/djangoapps/courseware/features/video.py +++ b/lms/djangoapps/courseware/features/video.py @@ -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$') diff --git a/lms/djangoapps/courseware/features/xqueue_setup.py b/lms/djangoapps/courseware/features/xqueue_setup.py deleted file mode 100644 index 90a68961ee..0000000000 --- a/lms/djangoapps/courseware/features/xqueue_setup.py +++ /dev/null @@ -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() diff --git a/lms/djangoapps/courseware/features/youtube_setup.py b/lms/djangoapps/courseware/features/youtube_setup.py deleted file mode 100644 index b9e536c5fe..0000000000 --- a/lms/djangoapps/courseware/features/youtube_setup.py +++ /dev/null @@ -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() diff --git a/lms/djangoapps/courseware/mock_xqueue_server/mock_xqueue_server.py b/lms/djangoapps/courseware/mock_xqueue_server/mock_xqueue_server.py deleted file mode 100644 index 7bc1b95662..0000000000 --- a/lms/djangoapps/courseware/mock_xqueue_server/mock_xqueue_server.py +++ /dev/null @@ -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
tags to ensure that it is valid XML - grade_response_dict['msg'] = "
%s
" % grade_response_dict['msg'] - - # Save the response dictionary - self._grade_response = grade_response_dict diff --git a/lms/djangoapps/courseware/mock_xqueue_server/test_mock_xqueue_server.py b/lms/djangoapps/courseware/mock_xqueue_server/test_mock_xqueue_server.py deleted file mode 100644 index 3f9a8e5b42..0000000000 --- a/lms/djangoapps/courseware/mock_xqueue_server/test_mock_xqueue_server.py +++ /dev/null @@ -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': '
'}) - expected_callback_dict = {'xqueue_header': grade_header, - 'xqueue_body': xqueue_body} - MockXQueueRequestHandler.post_to_url.assert_called_with(callback_url, - expected_callback_dict) diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 6b4b5f7665..a10ff9a164 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -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) diff --git a/lms/envs/common.py b/lms/envs/common.py index 893e537f77..0ca13d9f8c 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -911,6 +911,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 = ( diff --git a/lms/envs/test.py b/lms/envs/test.py index 31636f6aad..eb08cef9bf 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -239,6 +239,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/