Skip to content

Commit c01c943

Browse files
committed
Merge remote-tracking branch 'origin/bduffany-misc' into development
Conflicts: tests/home/test_home.py
2 parents 3c5ee85 + e3e6ad7 commit c01c943

File tree

7 files changed

+196
-46
lines changed

7 files changed

+196
-46
lines changed

README.md

Lines changed: 123 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,130 @@
11
# Codebender Selenium Tests
22

3-
This repo contains Selenium tests for the codebender website.
4-
The tests are written in Python 3.
3+
This repo contains Selenium tests for the codebender website. The tests are
4+
written in Python 3, and utilize pytest as a testing framework and Selenium
5+
for browser automation.
56

67
## Running Tests
78

8-
To run tests locally, you'll need to be running a selenium server. See
9-
[here](https://selenium-python.readthedocs.org/installation.html#downloading-selenium-server)
10-
for instructions.
9+
### Dependencies
1110

12-
Once you've got a Selenium server running, simply run `$ tox` from within the
13-
repo. If you don't have tox, run `$ sudo pip3 install -r requirements-dev.txt`
14-
from within the repo to install it.
11+
To run these tests, you'll need to have Python 3 installed. In addition, it is
12+
advantageous to have pip, a package manager for Python, in order to install
13+
dependencies.
14+
15+
Notably, the pip2 (for Python 2) and pip3 (for Python 3) packages both attempt
16+
to link `/usr/local/bin/pip` to the `pip2` or `pip3` executable, respectively.
17+
To deal with this, you could explicitly type out `pip3` or `pip2` instead of
18+
`pip` whenever you use pip via the command line. (It may be best to just remove
19+
`/usr/local/bin/pip` entirely).
20+
21+
To install `pip` in Ubuntu, run `$ sudo apt-get install python3
22+
python3-setuptools`, then `$ sudo easy_install3 pip`.
23+
24+
After getting set up with pip and cloning the seleniumTests repo, you should
25+
make sure to install all the seleniumTests dependencies by `cd`ing to your local
26+
clone of the repo and running `$ sudo pip3 install -r requirements-dev.txt`.
27+
28+
### Invoking Tests via `tox`
29+
30+
After installing dependencies, you should have the `tox` command available. To
31+
run all of the tests, you can simply run `$ tox` from within the cloned repo.
32+
33+
You can also run individual tests by providing the appropriate directory or
34+
filename as an argument, for example: `$ tox tests/sketch`.
35+
36+
Invoking tox will also run `flake8`, which is essentially a lint checker for
37+
Python. It is best to fix any issues reported by `flake8` before committing
38+
to the repo. It can be run on its own via the command `$ flake8`.
39+
40+
#### Specifying a URL for Tests
41+
42+
Tests can either be run for the
43+
[bachelor](https://github.com/codebendercc/bachelor) version of the site,
44+
running locally, or for the live site. The version of the site that is running
45+
is inferred from the `--url` parameter. You can run `$ tox --url
46+
http://localhost` to run the tests for a locally running bachelor site (this is
47+
the default url), or `$ tox --url http://codebender.cc` to run the tests for the
48+
live site.
49+
50+
Certain tests are specially written for one site or the other. This is
51+
implemented with a custom `pytest` marker. Tests that require a certain `--url`
52+
are decorated with `@pytest.mark.requires_url(<url>)`.
53+
54+
### Changing Test Configuration
55+
56+
Various global configuration parameters are specified in
57+
`codebender_testing/config.py`. Such parameters include URLs and site endpoints
58+
which are subject to change. This is also where the webdrivers (Firefox and
59+
Chrome) are specified.
60+
61+
## Compilation Logs
62+
63+
Certain tests exist to iterate through groups of sketches and compile them
64+
one-by-one. Since these tests take a long time, they are not run in full by
65+
default. You can run them by specifying the `--full` option; for example: `$ tox
66+
tests/cb_compile_tester --full`.
67+
68+
The following test cases are compile tests that generate such logs:
69+
- `tests/libraries/test_libraries.py::TestLibraryExamples`
70+
- `tests/compile_tester/test_compile_tester_projects.py::TestCompileTester`
71+
72+
The generated logs are placed in the `logs` directory. They give detailed output
73+
in JSON format containing the codebender site URL that was used to run the
74+
tests, along with the URLs of the individual sketches that were compiled, and
75+
whether they succeeded or failed to compile.
76+
77+
## Framework Overview
78+
79+
The following outlines the structure of the repository as well as important
80+
framework components.
81+
82+
### Directory Structure
83+
84+
#### `tests/`
85+
86+
The `tests/` directory contains all of the actual unit tests for the codebender
87+
site. That is, all of the tests discovered by `py.test` should come from this
88+
directory.
89+
90+
**`tests/conftest.py`** contains the global configuration for pytest,
91+
including specifying the webdriver fixtures as well as the available command
92+
line arguments.
93+
94+
#### `codebender_testing/`
95+
96+
This is where all major components of the testing framework live. All of the
97+
unit tests rely on the files in this directory.
98+
99+
**`codebender_testing/config.py`** specifies global configuration parameters for
100+
testing (see "Changing Test Configuration" above).
101+
102+
**`codebender_testing/utils.py`** defines codebender-specific utilities used to
103+
test the site. These mostly consist of abstractions to the Selenium framework.
104+
The most important class is `SeleniumTestCase`, which all of the unit test cases
105+
inherit from. This grants them access (via `self`) to a number of methods and
106+
attributes that are useful for performing codebender-specific actions.
107+
108+
#### `batch/`
109+
110+
The `batch/` directory contains any executable scripts not directly used to
111+
perform tests. For example, it contains a script `fetch_projects.py` which can
112+
be used to download all of the public projects of a particular codebender user.
113+
114+
#### `extensions/`
115+
116+
The `extensions/` directory contains the codebender browser extensions to be
117+
used by the Selenium webdrivers.
118+
119+
#### `test_data/`
120+
121+
The `test_data/` directory contains any data used for testing. For example, it
122+
contains example projects that we should successfully be able to upload and
123+
compile.
124+
125+
#### `logs/`
126+
127+
The `logs/` directory contains the results of running certain tests, e.g.
128+
whether certain sets of sketches have compiled successfully (see "Compilation
129+
Logs").
15130

codebender_testing/config.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ def _rel_path(*args):
3838
# Directory in which the local compile tester files are stored.
3939
COMPILE_TESTER_DIR = os.path.join(TEST_DATA_DIR, 'cb_compile_tester')
4040

41-
# Set up Selenium Webdrivers to be used for selenium tests
4241

42+
# Set up Selenium Webdrivers to be used for selenium tests
4343
def _get_firefox_profile():
4444
"""Returns the Firefox profile to be used for the FF webdriver.
4545
Specifically, we're equipping the webdriver with the Codebender
@@ -51,8 +51,15 @@ def _get_firefox_profile():
5151
)
5252
return firefox_profile
5353

54+
# Webdrivers to be used for testing. Specifying additional webdrivers here
55+
# will cause every test to be re-run using that webdriver.
56+
# These webdrivers are specified as lambdas to allow for "lazy" evaluation.
57+
# The lambda invocation will return the actual webdriver and open up a
58+
# browser window, and we don't want a browser window to open whenever this
59+
# module is imported (hence the need for lazy evaluation).
5460
WEBDRIVERS = {
55-
"firefox": webdriver.Firefox(firefox_profile=_get_firefox_profile())
61+
"firefox": lambda: webdriver.Firefox(firefox_profile=_get_firefox_profile()),
62+
# "chrome": lambda: webdriver.Chrome()
5663
}
5764

5865
# Credentials to use when logging into the site via selenium

codebender_testing/utils.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import json
55
import os
66
import re
7+
import shutil
78
import tempfile
89

910
from selenium.common.exceptions import NoSuchElementException
@@ -77,9 +78,8 @@ def temp_copy(fname):
7778
"""
7879
extension = fname.split('.')[-1]
7980
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)
81+
with open(fname, 'rb') as original:
82+
shutil.copyfileobj(original, copy)
8383
copy.flush()
8484
yield copy
8585

@@ -101,7 +101,7 @@ def start(url=None, webdriver=None):
101101
"""
102102
if webdriver is None:
103103
webdriver = WEBDRIVERS.keys()[0]
104-
self.driver = WEBDRIVERS[webdriver]
104+
self.driver = WEBDRIVERS[webdriver]()
105105

106106
if url is None:
107107
url = BASE_URL
@@ -203,6 +203,15 @@ def login(self):
203203
# 'Log In' is not displayed, so we're already logged in.
204204
pass
205205

206+
def logout(self):
207+
"""Logs out of the site."""
208+
try:
209+
logout_button = self.driver.find_element_by_id("logout")
210+
logout_button.send_keys(Keys.ENTER)
211+
except NoSuchElementException:
212+
# 'Log out' is not displayed, so we're already logged out.
213+
pass
214+
206215
def get_element(self, *locator):
207216
"""Waits for an element specified by *locator (a tuple of
208217
(By.<something>, str)), then returns it if it is found."""
@@ -217,11 +226,11 @@ def get_elements(self, *locator):
217226
expected_conditions.visibility_of_all_elements_located_by(locator))
218227
return self.driver.find_elements(*locator)
219228

220-
def get(self, selector):
229+
def find(self, selector):
221230
"""Alias for `self.get_element(By.CSS_SELECTOR, selector)`."""
222231
return self.get_element(By.CSS_SELECTOR, selector)
223232

224-
def get_all(self, selector):
233+
def find_all(self, selector):
225234
"""Alias for `self.get_elements(By.CSS_SELECTOR, selector)`."""
226235
return self.get_elements(By.CSS_SELECTOR, selector)
227236

@@ -285,9 +294,13 @@ def compile_sketches(self, sketches, iframe=False, logfile=None):
285294
format string, which will be formatted appropriately.
286295
`iframe` specifies whether the urls pointed to by `selector` are contained
287296
within an iframe.
297+
If the `--full` argument is provided (and hence
298+
`self.run_full_compile_tests` is `True`, we do not log, and limit the
299+
number of sketches compiled to 1.
288300
"""
289-
if logfile is None:
290-
for sketch in sketches:
301+
sketch_limit = None if self.run_full_compile_tests else 1
302+
if logfile is None or not self.run_full_compile_tests:
303+
for sketch in sketches[:sketch_limit]:
291304
self.compile_sketch(sketch, iframe=iframe)
292305
else:
293306
log_entry = {'url': self.site_url, 'succeeded': [], 'failed': []}
@@ -320,19 +333,24 @@ class SeleniumTestCase(CodebenderSeleniumBot):
320333

321334
@classmethod
322335
@pytest.fixture(scope="class", autouse=True)
323-
def _testcase_attrs(cls, webdriver, testing_url):
336+
def _testcase_attrs(cls, webdriver, testing_url, testing_full):
324337
"""Sets up any class attributes to be used by any SeleniumTestCase.
325338
Here, we just store fixtures as class attributes. This allows us to avoid
326339
the pytest boilerplate of getting a fixture value, and instead just
327340
refer to the fixture as `self.<fixture>`.
328341
"""
329342
cls.driver = webdriver
330343
cls.site_url = testing_url
344+
cls.run_full_compile_tests = testing_full
331345

332346
@pytest.fixture(scope="class")
333347
def tester_login(self):
334348
self.login()
335349

350+
@pytest.fixture(scope="class")
351+
def tester_logout(self):
352+
self.logout()
353+
336354

337355
class VerificationError(Exception):
338356
"""An exception representing a failed verification of a sketch."""

tests/compile_tester/test_compile_tester_projects.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ def test_compile_local_files(self, tester_login):
2828
"""Tests that we can upload all of cb_compile_tester's projects
2929
(stored locally in test_data/cb_compile_tester), compile them,
3030
and finally delete them."""
31+
upload_limit = None if self.run_full_compile_tests else 1
32+
3133
test_files = [os.path.join(COMPILE_TESTER_DIR, name)
32-
for name in next(os.walk(COMPILE_TESTER_DIR))[2]]
34+
for name in next(os.walk(COMPILE_TESTER_DIR))[2]][:upload_limit]
3335
projects = [self.upload_project(fname) for fname in test_files]
3436
project_names, project_urls = zip(*projects)
3537

tests/conftest.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
"""The configuration file for `py.test`.
2+
3+
This file specifies global test fixtures, which include the selenium
4+
webdrivers.
5+
6+
This is also where command-line arguments are defined.
7+
"""
8+
19
import pytest
210

311
from codebender_testing.config import BASE_URL
@@ -10,24 +18,34 @@ def webdriver(request):
1018
and registers a finalizer to close the browser once the session is
1119
complete. The entire test session is repeated once per driver.
1220
"""
13-
driver = WEBDRIVERS[request.param]
21+
driver = WEBDRIVERS[request.param]()
1422
request.addfinalizer(lambda: driver.quit())
1523
return driver
1624

1725
def pytest_addoption(parser):
1826
"""Adds command line options to py.test."""
1927
parser.addoption("--url", action="store", default=BASE_URL,
2028
help="URL to use for testing, e.g. http://localhost, http://codebender.cc")
29+
parser.addoption("--full", action="store_true", default=False,
30+
help="Whether to run the complete set of compile tests.")
2131

2232
@pytest.fixture(scope="class")
2333
def testing_url(request):
34+
"""A fixture to get the --url parameter."""
2435
return request.config.getoption("--url")
2536

37+
@pytest.fixture(scope="class")
38+
def testing_full(request):
39+
"""A fixture to get the --full parameter."""
40+
return request.config.getoption("--full")
41+
2642
@pytest.fixture(autouse=True)
2743
def skip_by_site(request, testing_url):
2844
"""Skips tests that require a certain site URL in order to run properly."""
2945
if request.node.get_marker('requires_url'):
3046
required_url = request.node.get_marker('requires_url').args[0]
3147
if required_url.rstrip('/') != testing_url.rstrip('/'):
32-
pytest.skip('skipped test that requires --url=%s')
48+
pytest.skip('skipped test that requires --url=%s' % required_url)
49+
50+
# TODO: add a --full option to run the complete test suite.
3351

0 commit comments

Comments
 (0)