Skip to content

Commit aac0a73

Browse files
committed
Merge remote-tracking branch 'origin/bduffany-compile-tests' into development
2 parents 6c754fc + acb213a commit aac0a73

File tree

8 files changed

+219
-3
lines changed

8 files changed

+219
-3
lines changed

codebender_testing/config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,21 @@
66
# URL of the site to be used for testing
77
BASE_URL = "http://localhost"
88

9+
# User whose projects we'd like to compile in our compile_tester
10+
# test case(s).
11+
COMPILE_TESTER_URL = "/user/cb_compile_tester"
12+
13+
# The prefix for all filenames of log files.
14+
# Note that it is given as a time format string, which will
15+
# be formatted appropriately.
16+
LOGFILE_PREFIX = os.path.join("logs", "%Y-%m-%d_%H-%M-%S-{log_name}.json")
17+
18+
# Logfile for COMPILE_TESTER compilation results
19+
COMPILE_TESTER_LOGFILE = LOGFILE_PREFIX.format(log_name="cb_compile_tester")
20+
21+
# Logfile for /libraries compilation results
22+
LIBRARIES_TEST_LOGFILE = LOGFILE_PREFIX.format(log_name="libraries_test")
23+
924
# URL of the actual Codebender website
1025
LIVE_SITE_URL = "http://codebender.cc"
1126

codebender_testing/utils.py

Lines changed: 145 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
from time import gmtime
2+
from time import strftime
3+
import json
14
import re
25

36
from selenium import webdriver
47
from selenium.common.exceptions import NoSuchElementException
8+
from selenium.common.exceptions import StaleElementReferenceException
9+
from selenium.common.exceptions import WebDriverException
510
from selenium.webdriver.common.by import By
611
from selenium.webdriver.common.keys import Keys
712
from selenium.webdriver.support import expected_conditions
@@ -15,18 +20,51 @@
1520
from codebender_testing.config import WEBDRIVERS
1621

1722

23+
# Time to wait until we give up on a DOM property becoming available.
24+
DOM_PROPERTY_DEFINED_TIMEOUT = 10
25+
26+
# JavaScript snippet to extract all the links to sketches on the current page.
27+
# `selector` is a CSS selector selecting these links.
28+
_GET_SKETCHES_SCRIPT = \
29+
"return $('{selector}').map(function() {{ return this.href; }}).toArray();"
30+
31+
# JavaScript snippet to verify the code on the current page.
32+
_VERIFY_SCRIPT = """
33+
if (window.compilerflasher !== undefined) {
34+
compilerflasher.verify();
35+
} else {
36+
// BACHELOR
37+
verify();
38+
}
39+
"""
40+
41+
# How long (in seconds) to wait before assuming that an example
42+
# has failed to compile
43+
VERIFY_TIMEOUT = 15
44+
45+
# Messages displayed to the user after verifying a sketch.
46+
VERIFICATION_SUCCESSFUL_MESSAGE = "Verification Successful"
47+
VERIFICATION_FAILED_MESSAGE = "Verification failed."
48+
49+
1850
class SeleniumTestCase(object):
1951
"""Base class for all Selenium tests."""
2052

53+
# This can be configured on a per-test case basis to use a different
54+
# URL for testing; e.g., http://localhost, or http://codebender.cc.
55+
# It is set via command line option in _testcase_attrs (below)
56+
site_url = None
57+
2158
@classmethod
2259
@pytest.fixture(scope="class", autouse=True)
23-
def _testcase_attrs(cls, webdriver):
60+
def _testcase_attrs(cls, webdriver, testing_url):
2461
"""Sets up any class attributes to be used by any SeleniumTestCase.
2562
Here, we just store fixtures as class attributes. This allows us to avoid
2663
the pytest boilerplate of getting a fixture value, and instead just
2764
refer to the fixture as `self.<fixture>`.
2865
"""
2966
cls.driver = webdriver
67+
cls.site_url = testing_url
3068

3169
@pytest.fixture(scope="class")
3270
def tester_login(self):
@@ -40,12 +78,12 @@ def open(self, url=None):
4078
"""
4179
if url is None:
4280
url = ''
43-
if re.match(".+?://^", url):
81+
if re.match(".+?://", url):
4482
# url specifies an absolute path.
4583
return self.driver.get(url)
4684
else:
4785
url = url.lstrip('/')
48-
return self.driver.get("%s/%s" % (BASE_URL, url))
86+
return self.driver.get("%s/%s" % (self.site_url, url))
4987

5088
def open_project(self, project_name=None):
5189
"""Opens the project specified by `name`, bringing the driver to the
@@ -91,3 +129,107 @@ def delete_project(self, project_name):
91129
delete_button.click()
92130
popup_delete_button = self.get_element(By.ID, 'deleteProjectButton')
93131
popup_delete_button.click()
132+
133+
def compile_sketch(self, url, iframe=False):
134+
"""Compiles the sketch located at `url`, or an iframe within the page
135+
referred to by `url`. Raises an exception if it does not compile.
136+
"""
137+
self.open(url)
138+
if iframe:
139+
# Note: here, we simply get the first iframe on the page.
140+
# There's a slightly less awkward way to do this that involves
141+
# giving this iframe a meaningful name in the HTML (TODO?)
142+
self.driver.switch_to_frame(0)
143+
self.execute_script(_VERIFY_SCRIPT)
144+
# In the BACHELOR site the id is 'operation_output', but in the live
145+
# site the id is 'cb_cf_operation_output'. The [id$=operation_output]
146+
# here selects an id that _ends_ with 'operation_output'.
147+
compile_result = WebDriverWait(self.driver, VERIFY_TIMEOUT).until(
148+
any_text_to_be_present_in_element((By.CSS_SELECTOR, "[id$=operation_output]"),
149+
VERIFICATION_SUCCESSFUL_MESSAGE, VERIFICATION_FAILED_MESSAGE))
150+
if compile_result != VERIFICATION_SUCCESSFUL_MESSAGE:
151+
raise VerificationError(compile_result)
152+
153+
154+
def compile_all_sketches(self, url, selector, iframe=False, logfile=None):
155+
"""Compiles all projects on the page at `url`. `selector` is a CSS selector
156+
that should select all relevant <a> tags containing links to sketches.
157+
`logfile` specifies a path to a file to which test results will be
158+
logged. If it is not `None`, compile errors will not cause the test
159+
to halt, but rather be logged to the given file. `logfile` may be a time
160+
format string, which will be formatted appropriately.
161+
`iframe` specifies whether the urls pointed to by `selector` are contained
162+
within an iframe.
163+
"""
164+
self.open(url)
165+
sketches = self.execute_script(_GET_SKETCHES_SCRIPT.format(selector=selector))
166+
assert len(sketches) > 0
167+
168+
if logfile is None:
169+
for sketch in sketches:
170+
self.compile_sketch(sketch, iframe=iframe)
171+
else:
172+
log_entry = {'url': self.site_url, 'succeeded': [], 'failed': []}
173+
for sketch in sketches:
174+
try:
175+
self.compile_sketch(sketch, iframe=iframe)
176+
log_entry['succeeded'].append(sketch)
177+
except (VerificationError, WebDriverException) as e:
178+
log_entry['failed'].append({
179+
'sketch': sketch,
180+
'exception': "%s; %s" % (type(e).__name__, str(e))
181+
# TODO?: is it possible to get the actual compiler error?
182+
})
183+
# Dump the test results to `logfile`.
184+
f = open(strftime(logfile, gmtime()), 'w')
185+
json.dump(log_entry, f)
186+
f.close()
187+
188+
189+
def execute_script(self, script, *deps):
190+
"""Waits for all JavaScript variables in `deps` to be defined, then
191+
executes the given script. Especially useful for waiting for things like
192+
jQuery to become available for use."""
193+
if len(deps) > 0:
194+
WebDriverWait(self.driver, DOM_PROPERTY_DEFINED_TIMEOUT).until(
195+
dom_properties_defined(*deps))
196+
return self.driver.execute_script(script)
197+
198+
199+
class VerificationError(Exception):
200+
"""An exception representing a failed verification of a sketch."""
201+
pass
202+
203+
204+
class dom_properties_defined(object):
205+
"""An expectation for the given DOM properties to be defined.
206+
See selenium.webdriver.support.expected_conditions for more on how this
207+
type of class works.
208+
"""
209+
210+
def __init__(self, *properties):
211+
self._properties = properties
212+
213+
def __call__(self, driver):
214+
return all(
215+
driver.execute_script("return window.%s !== undefined" % prop)
216+
for prop in self._properties)
217+
218+
219+
class any_text_to_be_present_in_element(object):
220+
"""An expectation for checking if any of the given strings are present in
221+
the specified element. Returns the string that was present.
222+
"""
223+
def __init__(self, locator, *texts):
224+
self.locator = locator
225+
self.texts = texts
226+
227+
def __call__(self, driver):
228+
try :
229+
element_text = expected_conditions._find_element(driver, self.locator).text
230+
for text in self.texts:
231+
if text in element_text:
232+
return text
233+
return False
234+
except StaleElementReferenceException:
235+
return False

logs/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Ignore everything in this directory
2+
*
3+
4+
# Except this file
5+
!.gitignore
6+
7+
# We need this gitignore since the logs directory is required for storing logs,
8+
# but we don't want to commit the logs themselves.

tests/compile_tester/__init__.py

Whitespace-only changes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import pytest
2+
3+
from codebender_testing.config import COMPILE_TESTER_LOGFILE
4+
from codebender_testing.config import COMPILE_TESTER_URL
5+
from codebender_testing.config import LIVE_SITE_URL
6+
from codebender_testing.utils import SeleniumTestCase
7+
8+
9+
class TestCompileTester(SeleniumTestCase):
10+
11+
# Here, we require the LIVE_SITE_URL since the compiler tester user
12+
# does not exist in the bachelor version.
13+
@pytest.mark.requires_url(LIVE_SITE_URL)
14+
def test_compile_all_user_projects(self):
15+
"""Tests that all library examples compile successfully."""
16+
self.compile_all_sketches(COMPILE_TESTER_URL, '#user_projects tbody a',
17+
iframe=True, log_file=COMPILE_TESTER_LOGFILE)
18+

tests/conftest.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22

3+
from codebender_testing.config import BASE_URL
34
from codebender_testing.config import WEBDRIVERS
45

56

@@ -13,3 +14,20 @@ def webdriver(request):
1314
request.addfinalizer(lambda: driver.quit())
1415
return driver
1516

17+
def pytest_addoption(parser):
18+
"""Adds command line options to py.test."""
19+
parser.addoption("--url", action="store", default=BASE_URL,
20+
help="URL to use for testing, e.g. http://localhost, http://codebender.cc")
21+
22+
@pytest.fixture(scope="class")
23+
def testing_url(request):
24+
return request.config.getoption("--url")
25+
26+
@pytest.fixture(autouse=True)
27+
def skip_by_site(request, testing_url):
28+
"""Skips tests that require a certain site URL in order to run properly."""
29+
if request.node.get_marker('requires_url'):
30+
required_url = request.node.get_marker('requires_url').args[0]
31+
if required_url.rstrip('/') != testing_url.rstrip('/'):
32+
pytest.skip('skipped test that requires --url=%s')
33+

tests/libraries/__init__.py

Whitespace-only changes.

tests/libraries/test_libraries.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from codebender_testing.config import LIBRARIES_TEST_LOGFILE
2+
from codebender_testing.config import LIVE_SITE_URL
3+
from codebender_testing.utils import SeleniumTestCase
4+
5+
6+
class TestLibraryExamples(SeleniumTestCase):
7+
8+
# Overrides the site_url in SeleniumTestCase.
9+
site_url = LIVE_SITE_URL
10+
11+
def test_compile_all_libraries(self):
12+
"""Tests that all library examples compile successfully."""
13+
self.compile_all_sketches('/libraries', '.accordion li a',
14+
logfile=LIBRARIES_TEST_LOGFILE)
15+

0 commit comments

Comments
 (0)