Skip to content

Commit 3c5ee85

Browse files
committed
Merge remote-tracking branch 'origin/bduffany-test-upload' into development
2 parents aac0a73 + ba245e7 commit 3c5ee85

40 files changed

+271
-35
lines changed

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,3 @@ Once you've got a Selenium server running, simply run `$ tox` from within the
1313
repo. If you don't have tox, run `$ sudo pip3 install -r requirements-dev.txt`
1414
from within the repo to install it.
1515

16-
When running tox, you might get a `pkg_resources.DistributionNotFound` error
17-
with reference to `virtualenv`. This is likely due to an out of date setuptools.
18-
To fix this issue, run `$ sudo pip3 install -U setuptools`.

batch/__init__.py

Whitespace-only changes.

batch/fetch_projects.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env python3
2+
"""A script to downloads all projects for a particular user."""
3+
4+
# This is necessary in order to run the script; otherwise the script needs to
5+
# be run as a python module (which is inconvenient).
6+
if __name__ == '__main__' and __package__ is None:
7+
from os import sys, path
8+
sys.path.append(path.join(path.dirname(path.abspath(__file__)), '..'))
9+
10+
11+
from urllib.request import urlopen
12+
from urllib.request import urlretrieve
13+
import argparse
14+
import os
15+
16+
from codebender_testing.config import LIVE_SITE_URL
17+
18+
from lxml import html
19+
20+
21+
def download_projects(url, user, path):
22+
connection = urlopen('/'.join([url, 'user', user]))
23+
dom = html.fromstring(connection.read().decode('utf8'))
24+
os.chdir(path)
25+
for link in dom.xpath('//table[@id="user_projects"]//a'):
26+
project_name = link.xpath('text()')[0]
27+
sketch_num = link.xpath('@href')[0].split(':')[-1]
28+
print("Downloading %s (sketch %s)" % (project_name, sketch_num))
29+
urlretrieve('%s/utilities/download/%s' % (url, sketch_num),
30+
os.path.join(path, '%s.zip' % project_name))
31+
32+
33+
if __name__ == "__main__":
34+
parser = argparse.ArgumentParser()
35+
parser.add_argument("user", help="the user whose projects we want to download")
36+
parser.add_argument("-u", "--url", help="url of the codebender site to use",
37+
default=LIVE_SITE_URL)
38+
parser.add_argument("-d", "--directory", help="output directory of the downloaded projects",
39+
default=".")
40+
args = parser.parse_args()
41+
download_projects(args.url, args.user, args.directory)

codebender_testing/config.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
from selenium import webdriver
44

55

6-
# URL of the site to be used for testing
6+
def _rel_path(*args):
7+
"""Forms a path relative to this file's directory."""
8+
return os.path.join(os.path.dirname(__file__), *args)
9+
10+
# URL of the default site to be used for testing
711
BASE_URL = "http://localhost"
12+
# URL of the actual Codebender website
13+
LIVE_SITE_URL = "http://codebender.cc"
814

915
# User whose projects we'd like to compile in our compile_tester
1016
# test case(s).
@@ -13,23 +19,24 @@
1319
# The prefix for all filenames of log files.
1420
# Note that it is given as a time format string, which will
1521
# be formatted appropriately.
16-
LOGFILE_PREFIX = os.path.join("logs", "%Y-%m-%d_%H-%M-%S-{log_name}.json")
22+
LOGFILE_PREFIX = _rel_path("..", "logs", "%Y-%m-%d_%H-%M-%S-{log_name}.json")
1723

1824
# Logfile for COMPILE_TESTER compilation results
1925
COMPILE_TESTER_LOGFILE = LOGFILE_PREFIX.format(log_name="cb_compile_tester")
2026

2127
# Logfile for /libraries compilation results
2228
LIBRARIES_TEST_LOGFILE = LOGFILE_PREFIX.format(log_name="libraries_test")
2329

24-
# URL of the actual Codebender website
25-
LIVE_SITE_URL = "http://codebender.cc"
26-
27-
_EXTENSIONS_DIR = 'extensions'
30+
_EXTENSIONS_DIR = _rel_path('..', 'extensions')
2831
_FIREFOX_EXTENSION_FNAME = 'codebender.xpi'
2932

3033
# Files used for testing
31-
TEST_DATA_DIR = 'test_data'
34+
TEST_DATA_DIR = _rel_path('..', 'test_data')
3235
TEST_DATA_BLANK_PROJECT = os.path.join(TEST_DATA_DIR, 'blank_project.ino')
36+
TEST_DATA_BLANK_PROJECT_ZIP = os.path.join(TEST_DATA_DIR, 'blank_project.zip')
37+
38+
# Directory in which the local compile tester files are stored.
39+
COMPILE_TESTER_DIR = os.path.join(TEST_DATA_DIR, 'cb_compile_tester')
3340

3441
# Set up Selenium Webdrivers to be used for selenium tests
3542

codebender_testing/utils.py

Lines changed: 162 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
from contextlib import contextmanager
12
from time import gmtime
23
from time import strftime
34
import json
5+
import os
46
import re
7+
import tempfile
58

6-
from selenium import webdriver
79
from selenium.common.exceptions import NoSuchElementException
810
from selenium.common.exceptions import StaleElementReferenceException
911
from selenium.common.exceptions import WebDriverException
@@ -38,6 +40,24 @@
3840
}
3941
"""
4042

43+
_TEST_INPUT_ID = "_cb_test_input"
44+
45+
# Creates an input into which we can upload files using Selenium.
46+
_CREATE_INPUT_SCRIPT = """
47+
var input = window.$('<input id="{input_id}" type="file" style="position: fixed">');
48+
window.$('body').append(input);
49+
""".format(input_id=_TEST_INPUT_ID)
50+
51+
# After the file is chosen via Selenium, this script moves the file object
52+
# (in the DOM) to the Dropzone.
53+
def _move_file_to_dropzone_script(dropzone_selector):
54+
return """
55+
var fileInput = document.getElementById('{input_id}');
56+
var file = fileInput.files[0];
57+
var dropzone = Dropzone.forElement('{selector}');
58+
dropzone.drop({{ dataTransfer: {{ files: [file] }} }});
59+
""".format(input_id=_TEST_INPUT_ID, selector=dropzone_selector)
60+
4161
# How long (in seconds) to wait before assuming that an example
4262
# has failed to compile
4363
VERIFY_TIMEOUT = 15
@@ -47,28 +67,75 @@
4767
VERIFICATION_FAILED_MESSAGE = "Verification failed."
4868

4969

50-
class SeleniumTestCase(object):
51-
"""Base class for all Selenium tests."""
70+
@contextmanager
71+
def temp_copy(fname):
72+
"""Creates a temporary copy of the file `fname`.
73+
This is useful for testing features that derive certain properties
74+
from the filename, and we want a unique filename each time we run the
75+
test (in case, for example, there is leftover garbage from previous
76+
tests with the same name).
77+
"""
78+
extension = fname.split('.')[-1]
79+
with tempfile.NamedTemporaryFile(mode='w+b', suffix='.%s' % extension) as copy:
80+
with open(fname, 'r') as original:
81+
for line in original:
82+
copy.write(line)
83+
copy.flush()
84+
yield copy
85+
86+
87+
class CodebenderSeleniumBot(object):
88+
"""Contains various utilities for navigating the Codebender website."""
5289

5390
# This can be configured on a per-test case basis to use a different
5491
# URL for testing; e.g., http://localhost, or http://codebender.cc.
5592
# It is set via command line option in _testcase_attrs (below)
5693
site_url = None
5794

58-
@classmethod
59-
@pytest.fixture(scope="class", autouse=True)
60-
def _testcase_attrs(cls, webdriver, testing_url):
61-
"""Sets up any class attributes to be used by any SeleniumTestCase.
62-
Here, we just store fixtures as class attributes. This allows us to avoid
63-
the pytest boilerplate of getting a fixture value, and instead just
64-
refer to the fixture as `self.<fixture>`.
95+
def start(url=None, webdriver=None):
96+
"""Create the selenium webdriver, operating on `url`. We can't do this
97+
in an __init__ method, otherwise py.test complains about
98+
SeleniumTestCase having an init method.
99+
The webdriver that is created is specified as a key into the WEBDRIVERS
100+
dict (in codebender_testing.config)
65101
"""
66-
cls.driver = webdriver
67-
cls.site_url = testing_url
102+
if webdriver is None:
103+
webdriver = WEBDRIVERS.keys()[0]
104+
self.driver = WEBDRIVERS[webdriver]
68105

69-
@pytest.fixture(scope="class")
70-
def tester_login(self):
71-
self.login()
106+
if url is None:
107+
url = BASE_URL
108+
self.site_url = url
109+
110+
@classmethod
111+
@contextmanager
112+
def session(cls, **kwargs):
113+
"""Start a new session with a new webdriver. Regardless of whether an
114+
exception is raised, the webdriver is guaranteed to quit.
115+
The keyword arguments should be interpreted as in `start`.
116+
117+
Sample usage:
118+
119+
```
120+
with CodebenderSeleniumBot.session(url="localhost",
121+
webdriver="firefox") as bot:
122+
# The browser is now open
123+
bot.open("/")
124+
assert "Codebender" in bot.driver.title
125+
# The browser is now closed
126+
```
127+
128+
Test cases shouldn't need to use this method; it's mostly useful for
129+
scripts, automation, etc.
130+
"""
131+
try:
132+
bot = cls()
133+
bot.start(**kwargs)
134+
yield bot
135+
bot.driver.quit()
136+
except:
137+
bot.driver.quit()
138+
raise
72139

73140
def open(self, url=None):
74141
"""Open the resource specified by `url`.
@@ -95,6 +162,30 @@ def open_project(self, project_name=None):
95162
project_link = self.driver.find_element_by_link_text(project_name)
96163
project_link.send_keys(Keys.ENTER)
97164

165+
def upload_project(self, test_fname, project_name=None):
166+
"""Tests that we can successfully upload `test_fname`.
167+
`project_name` is the expected name of the project; by
168+
default it is inferred from the file name.
169+
Returns a pair of (the name of the project, the url of the project sketch)
170+
"""
171+
# A tempfile is used here since we want the name to be
172+
# unique; if the file has already been successfully uploaded
173+
# then the test might give a false-positive.
174+
with temp_copy(test_fname) as test_file:
175+
self.dropzone_upload("#dropzoneForm", test_file.name)
176+
if project_name is None:
177+
project_name = os.path.split(test_file.name)[-1].split('.')[0]
178+
179+
# The upload was successful <==> we get a green "check" on its
180+
# Dropzone upload indicator
181+
self.get_element(By.CSS_SELECTOR, '#dropzoneForm .dz-success')
182+
183+
# Make sure the project shows up in the Projects list
184+
last_project = self.get_element(By.CSS_SELECTOR,
185+
'#sidebar-list-main li:last-child .project_link')
186+
187+
return last_project.text, last_project.get_attribute('href')
188+
98189
def login(self):
99190
"""Performs a login."""
100191
try:
@@ -119,6 +210,21 @@ def get_element(self, *locator):
119210
expected_conditions.visibility_of_element_located(locator))
120211
return self.driver.find_element(*locator)
121212

213+
def get_elements(self, *locator):
214+
"""Like `get_element`, but returns a list of all elements matching
215+
the selector."""
216+
WebDriverWait(self.driver, ELEMENT_FIND_TIMEOUT).until(
217+
expected_conditions.visibility_of_all_elements_located_by(locator))
218+
return self.driver.find_elements(*locator)
219+
220+
def get(self, selector):
221+
"""Alias for `self.get_element(By.CSS_SELECTOR, selector)`."""
222+
return self.get_element(By.CSS_SELECTOR, selector)
223+
224+
def get_all(self, selector):
225+
"""Alias for `self.get_elements(By.CSS_SELECTOR, selector)`."""
226+
return self.get_elements(By.CSS_SELECTOR, selector)
227+
122228
def delete_project(self, project_name):
123229
"""Deletes the project specified by `project_name`. Note that this will
124230
navigate to the user's homepage."""
@@ -151,20 +257,35 @@ def compile_sketch(self, url, iframe=False):
151257
raise VerificationError(compile_result)
152258

153259

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
260+
def dropzone_upload(self, selector, fname):
261+
"""Uploads a file specified by `fname` via the Dropzone within the
262+
element specified by `selector`. (Dropzone refers to Dropzone.js)
263+
"""
264+
# Create an artificial file input.
265+
self.execute_script(_CREATE_INPUT_SCRIPT)
266+
test_input = self.get_element(By.ID, _TEST_INPUT_ID)
267+
test_input.send_keys(fname)
268+
self.execute_script(_move_file_to_dropzone_script(selector))
269+
270+
def compile_all_sketches(self, url, selector, **kwargs):
271+
"""Compiles all sketches on the page at `url`. `selector` is a CSS selector
156272
that should select all relevant <a> tags containing links to sketches.
273+
See `compile_sketches` for the possible keyword arguments that can be specified.
274+
"""
275+
self.open(url)
276+
sketches = self.execute_script(_GET_SKETCHES_SCRIPT.format(selector=selector))
277+
assert len(sketches) > 0
278+
self.compile_sketches(sketches, **kwargs)
279+
280+
def compile_sketches(self, sketches, iframe=False, logfile=None):
281+
"""Compiles the sketches with URLs given by the `sketches` list.
157282
`logfile` specifies a path to a file to which test results will be
158283
logged. If it is not `None`, compile errors will not cause the test
159284
to halt, but rather be logged to the given file. `logfile` may be a time
160285
format string, which will be formatted appropriately.
161286
`iframe` specifies whether the urls pointed to by `selector` are contained
162287
within an iframe.
163288
"""
164-
self.open(url)
165-
sketches = self.execute_script(_GET_SKETCHES_SCRIPT.format(selector=selector))
166-
assert len(sketches) > 0
167-
168289
if logfile is None:
169290
for sketch in sketches:
170291
self.compile_sketch(sketch, iframe=iframe)
@@ -185,17 +306,34 @@ def compile_all_sketches(self, url, selector, iframe=False, logfile=None):
185306
json.dump(log_entry, f)
186307
f.close()
187308

188-
189309
def execute_script(self, script, *deps):
190310
"""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."""
311+
executes the given script."""
193312
if len(deps) > 0:
194313
WebDriverWait(self.driver, DOM_PROPERTY_DEFINED_TIMEOUT).until(
195314
dom_properties_defined(*deps))
196315
return self.driver.execute_script(script)
197316

198317

318+
class SeleniumTestCase(CodebenderSeleniumBot):
319+
"""Base class for all Selenium tests."""
320+
321+
@classmethod
322+
@pytest.fixture(scope="class", autouse=True)
323+
def _testcase_attrs(cls, webdriver, testing_url):
324+
"""Sets up any class attributes to be used by any SeleniumTestCase.
325+
Here, we just store fixtures as class attributes. This allows us to avoid
326+
the pytest boilerplate of getting a fixture value, and instead just
327+
refer to the fixture as `self.<fixture>`.
328+
"""
329+
cls.driver = webdriver
330+
cls.site_url = testing_url
331+
332+
@pytest.fixture(scope="class")
333+
def tester_login(self):
334+
self.login()
335+
336+
199337
class VerificationError(Exception):
200338
"""An exception representing a failed verification of a sketch."""
201339
pass

requirements-dev.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
flake8
2+
lxml
23
pytest
4+
setuptools>=12.1
35
tox
46
virtualenv

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
flake8
12
pytest==2.6.4
23
selenium==2.44.0

test_data/blank_project.zip

275 Bytes
Binary file not shown.
775 Bytes
Binary file not shown.
1.32 KB
Binary file not shown.

0 commit comments

Comments
 (0)