This repository was archived by the owner on Jun 7, 2023. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 222
Expand file tree
/
Copy pathconftest.py
More file actions
238 lines (211 loc) · 9.84 KB
/
conftest.py
File metadata and controls
238 lines (211 loc) · 9.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# ***************************************
# |docname| - pytest fixtures for testing
# ***************************************
# This defines fixtures specific to the client for test. These same fixtures are defined differently on the server to accommodate the different setup these.
#
#
# Imports
# =======
# These are listed in the order prescribed by `PEP 8
# <http://www.python.org/dev/peps/pep-0008/#imports>`_.
#
# Standard library
# ----------------
import logging
import os
import platform
import signal
import time
import subprocess
import sys
import unittest
from urllib.request import urlopen
from urllib.error import URLError
# Third-party imports
# -------------------
import pytest
from pyvirtualdisplay import Display
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# Local imports
# -------------
# This is necessary to bring in the shared pytest fixture.
from runestone.shared_conftest import _SeleniumUtils, selenium_driver # noqa: F401
logging.basicConfig(level=logging.WARN)
# Globals
# =======
# Select an unused port for serving web pages to the test suite.
PORT = "8081"
# Use the localhost for testing.
HOST_ADDRESS = "127.0.0.1:" + PORT
HOST_URL = "http://" + HOST_ADDRESS
# Define the platform.
IS_WINDOWS = platform.system() == "Windows"
IS_LINUX = sys.platform.startswith("linux")
mylogger = logging.getLogger()
# Fixtures
# ========
# Run this once, before all tests, to update the webpacked JS.
@pytest.fixture(scope="session", autouse=True)
def run_webpack():
# Note that Windows requires ``shell=True``, since the command to execute is ``npm.cmd``. Use the ``--`` to pass following args to the script (webpack), per the `npm docs <https://docs.npmjs.com/cli/v7/commands/npm-run-script>`_. Use ``--env test`` to tell webpack to do a test build of the Runestone Components (see `RAND_FUNC <RAND_FUNC>`).
p = subprocess.run(["npm", "run", "build", "--", "--env", "test"], text=True, shell=IS_WINDOWS, capture_output=True)
print(p.stderr + p.stdout)
assert not p.returncode
# .. _selenium_module_fixture:
#
# ``selenium_module_fixture``
# ---------------------------
# Provide access to the Selenium module fixture, for tests with specific needs.
@pytest.fixture(scope="module")
def selenium_module_fixture(request):
# Allow modules to specify the ``exit_status_success`` parameter passed to the ``ModuleFixture`` constructor by adding the statement ``pytestmark = pytest.mark.exit_status_success(False)``. (Since this is a module-scoped fixture, applying this mark to an individual test has no effect. Marking it True instead is equivalent to the unmarked, default value.) See the `example <https://docs.pytest.org/en/stable/fixture.html#using-markers-to-pass-data-to-fixtures>`_ (which applies only to function-scoped marks, not module-scoped marks), `marking whole classes or modules <https://docs.pytest.org/en/6.2.x/example/markers.html#marking-whole-classes-or-modules>`_, and the `API docs <https://docs.pytest.org/en/stable/reference.html#pytest.nodes.Node.get_closest_marker>`_.
exit_status_success_mark = request.node.get_closest_marker("exit_status_success")
exit_status_success = True if exit_status_success_mark is None else exit_status_success_mark.args[0]
mf = ModuleFixture(request.fspath, exit_status_success)
mf.setUpModule()
yield mf
mf.tearDownModule()
# Provide access to the Selenium driver.
@pytest.fixture(scope="module")
def selenium_driver_session(selenium_module_fixture):
return selenium_module_fixture.driver
# Extend the Selenium driver with client-specific methods.
class _SeleniumClientUtils(_SeleniumUtils):
def inject_random_values(self, value_array):
self.driver.execute_script("""
rs_test_rand = function() {
let index = 0;
return () => [%s][index++];
}();
""" % (", ".join([str(i) for i in value_array])))
# Present ``_SeleniumUser`` as a fixture.
@pytest.fixture
def selenium_utils(selenium_driver): # noqa: F811
return _SeleniumClientUtils(selenium_driver, HOST_URL)
# Provide a fixture which loads the ``index.html`` page.
@pytest.fixture
def selenium_utils_get(selenium_utils):
selenium_utils.get("index.html")
return selenium_utils
# Utility class
# =============
# Define a class to build the test Runestone project, run the server, then shut it down when the tests complete.
class ModuleFixture(unittest.TestCase):
def __init__(
self,
# The path to the Python module in which the test resides. This provides a simple way to determine the path in which to run runestone build/serve.
module_path,
# True if the sphinx-build process must exit with status of 0 (success)
exit_status_success=True,
):
super(ModuleFixture, self).__init__()
self.base_path = os.path.dirname(module_path)
self.exit_status_success = exit_status_success
# Windows Compatability
if IS_WINDOWS and self.base_path == "":
self.base_path = "."
def setUpModule(self):
# Change to this directory for running Runestone.
self.old_cwd = os.getcwd()
os.chdir(self.base_path)
# Compile the docs. Save the stdout and stderr for examination.
p = subprocess.run(
[sys.executable, "-m", "runestone", "build", "--all"], capture_output=True, text=True,
)
self.build_stdout_data = p.stdout
self.build_stderr_data = p.stderr
print(self.build_stdout_data + self.build_stderr_data)
if self.exit_status_success:
self.assertFalse(p.returncode)
# Make sure any older servers on port 8081 are killed.
if IS_WINDOWS:
netstat_output = subprocess.run(
# Flags are:
#
# -n: Display addresses numerically. Looking up names is slow.
# -o: Include the PID for each connection.
["netstat", "-no"],
capture_output=True,
text=True,
).stdout
# Skip the first four lines, which are headings.
for connection in netstat_output.splitlines()[4:]:
# Typical output is:
## Proto Local Address Foreign Address State PID
## TCP 127.0.0.1:1277 127.0.0.1:49971 ESTABLISHED 4624
proto, local_address, foreign_address, state, pid = connection.split()
pid = int(pid)
if local_address == HOST_ADDRESS and pid != 0:
os.kill(pid, 0)
else:
lsof_output = subprocess.run(
["lsof", "-i", ":{0}".format(PORT)], capture_output=True, text=True,
).stdout
for process in lsof_output.split("\n")[1:]:
data = [x for x in process.split(" ") if x != ""]
if len(data) <= 1:
continue
ptokill = int(data[1])
mylogger.warn(
"Attempting to kill a stale runestone serve process: {}".format(
ptokill
)
)
os.kill(ptokill, signal.SIGKILL)
time.sleep(2) # give the old process a couple seconds to clear out
try:
os.kill(ptokill, 0) # will throw an Error if process gone
pytest.exit(
"Stale runestone server can't kill process: {}".format(ptokill)
)
except ProcessLookupError:
# The process was killed
pass
except PermissionError:
pytest.exit(
"Another server is using port {} process: {}".format(
PORT, ptokill
)
)
except Exception:
pytest.exit(
"Unknown error while trying to kill stale runestone server"
)
# Run the server. Simply calling ``runestone preview`` fails, since the process killed isn't the actual server, but probably a setuptools-created launcher.
self.runestone_server = subprocess.Popen(
[sys.executable, "-m", "runestone", "preview", "--port", PORT]
)
# Testing time in dominated by browser startup/shutdown. So, simply run all tests in a module in a single browser instance to speed things up. See ``RunestoneTestCase.setUp`` for additional code to (mostly) clear the browser between tests.
#
# `PyVirtualDisplay <http://pyvirtualdisplay.readthedocs.io/en/latest/>`_ only runs on X-windows, meaning Linux. Mac seems to have `some support <https://support.apple.com/en-us/HT201341>`_. Windows is out of the question.
if IS_LINUX and not os.environ.get("DISPLAY"):
self.display = Display(visible=0, size=(1280, 1024))
self.display.start()
else:
self.display = None
# self.driver = webdriver.PhantomJS() # use this for Jenkins auto testing
options = Options()
options.add_argument("--window-size=1200,800")
options.add_argument("--no-sandbox")
self.driver = webdriver.Chrome(options=options) # good for development.
# Wait for the webserver to come up.
for tries in range(50):
try:
urlopen(HOST_URL, timeout=5)
except URLError:
# Wait for the server to come up.
time.sleep(0.1)
else:
# The server is up. We're done.
break
def tearDownModule(self):
# Shut down Selenium.
self.driver.close()
self.driver.quit()
if self.display:
self.display.stop()
# Shut down the server.
self.runestone_server.kill()
# Restore the directory.
os.chdir(self.old_cwd)