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 pathunittest_base.py
More file actions
228 lines (205 loc) · 8.66 KB
/
unittest_base.py
File metadata and controls
228 lines (205 loc) · 8.66 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
# *************************************************************************
# ``unittest_base.py`` - Base classes for RunestoneComponents test fixtures
# *************************************************************************
#
# 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
# -------------------
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import pytest
from pyvirtualdisplay import Display
logging.basicConfig(level=logging.WARN)
mylogger = logging.getLogger()
# Local imports
# -------------
# None
# 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")
# Provide access to the currently-active ModuleFixture object.
mf = None
# Define `module fixtures <https://docs.python.org/2/library/unittest.html#setupmodule-and-teardownmodule>`_ 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.Popen(
["runestone", "build", "--all"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
self.build_stdout_data, self.build_stderr_data = p.communicate()
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"],
universal_newlines=True,
stdout=subprocess.PIPE,
).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)],
universal_newlines=True,
stdout=subprocess.PIPE,
).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 serve`` 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", "serve", "--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:
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.
# Make this accessible
global mf
mf = self
# 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.quit()
if self.display:
self.display.stop()
# Shut down the server.
self.runestone_server.kill()
# Restore the directory.
os.chdir(self.old_cwd)
global mf
mf = None
# Without this, Python 2.7 produces errors when running unit tests:
#
# .. code::
# :number-lines:
#
# python -m unittest discover
#
# ImportError: Failed to import test module: runestone.tabbedStuff.test.test_tabbedStuff
# Traceback (most recent call last): (omitted)
# ValueError: no such test method in <class 'runestone.unittest_base.ModuleFixture'>: runTest
def runTest(self):
pass
# Provide a simple way to instantiante a ModuleFixture in a test module. Typical use:
#
# .. code:: Python
# :number-lines:
#
# from unittest_base import module_fixture_maker
# setUpModule, tearDownModule = module_fixture_maker(__file__)
def module_fixture_maker(module_path, return_mf=False, exit_status_success=True):
mf = ModuleFixture(module_path, exit_status_success)
if return_mf:
return mf, mf.setUpModule, mf.tearDownModule
else:
return mf.setUpModule, mf.tearDownModule
# Provide a base test case which sets up the `Selenium <http://selenium-python.readthedocs.io/>`_ driver.
class RunestoneTestCase(unittest.TestCase):
def setUp(self):
# Use the shared module-wide driver.
self.driver = mf.driver
self.host = HOST_URL
def tearDown(self):
# Clear as much as possible, to present an almost-fresh instance of a browser for the next test. (Shutting down then starting up a browswer is very slow.)
self.driver.execute_script("window.localStorage.clear();")
self.driver.execute_script("window.sessionStorage.clear();")
self.driver.delete_all_cookies()