psdi-data-conversion 0.0.36__py3-none-any.whl → 0.0.37__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- psdi_data_conversion/app.py +94 -14
- psdi_data_conversion/converters/base.py +2 -2
- psdi_data_conversion/static/content/download.htm +22 -9
- psdi_data_conversion/static/styles/psdi-common.css +5 -4
- psdi_data_conversion/templates/index.htm +6 -1
- psdi_data_conversion/testing/constants.py +6 -3
- psdi_data_conversion/testing/conversion_callbacks.py +7 -6
- psdi_data_conversion/testing/conversion_test_specs.py +107 -18
- psdi_data_conversion/testing/gui.py +366 -0
- psdi_data_conversion/testing/utils.py +86 -44
- {psdi_data_conversion-0.0.36.dist-info → psdi_data_conversion-0.0.37.dist-info}/METADATA +73 -57
- {psdi_data_conversion-0.0.36.dist-info → psdi_data_conversion-0.0.37.dist-info}/RECORD +15 -14
- {psdi_data_conversion-0.0.36.dist-info → psdi_data_conversion-0.0.37.dist-info}/WHEEL +1 -1
- {psdi_data_conversion-0.0.36.dist-info → psdi_data_conversion-0.0.37.dist-info}/entry_points.txt +1 -0
- {psdi_data_conversion-0.0.36.dist-info → psdi_data_conversion-0.0.37.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,366 @@
|
|
1
|
+
"""
|
2
|
+
# gui.py
|
3
|
+
|
4
|
+
Utilities to aid in testing of the GUI
|
5
|
+
"""
|
6
|
+
|
7
|
+
|
8
|
+
import os
|
9
|
+
import shutil
|
10
|
+
from tempfile import TemporaryDirectory
|
11
|
+
|
12
|
+
import time
|
13
|
+
import pytest
|
14
|
+
from selenium.common.exceptions import TimeoutException
|
15
|
+
from selenium.webdriver.common.alert import Alert
|
16
|
+
from selenium.webdriver.common.by import By
|
17
|
+
from selenium.webdriver.firefox.webdriver import WebDriver
|
18
|
+
from selenium.webdriver.support import expected_conditions as EC
|
19
|
+
from selenium.webdriver.support.select import Select
|
20
|
+
from selenium.webdriver.support.ui import WebDriverWait
|
21
|
+
|
22
|
+
from psdi_data_conversion.constants import STATUS_CODE_GENERAL
|
23
|
+
from psdi_data_conversion.converters.base import (FileConverterAbortException, FileConverterException,
|
24
|
+
FileConverterInputException)
|
25
|
+
from psdi_data_conversion.converters.openbabel import (COORD_GEN_KEY, COORD_GEN_QUAL_KEY, DEFAULT_COORD_GEN_QUAL,
|
26
|
+
L_ALLOWED_COORD_GEN_QUALS, L_ALLOWED_COORD_GENS)
|
27
|
+
from psdi_data_conversion.file_io import split_archive_ext
|
28
|
+
from psdi_data_conversion.testing.utils import (ConversionTestInfo, ConversionTestSpec, SingleConversionTestSpec,
|
29
|
+
get_input_test_data_loc)
|
30
|
+
|
31
|
+
# Standard timeout at 10 seconds
|
32
|
+
TIMEOUT = 10
|
33
|
+
|
34
|
+
|
35
|
+
def wait_for_element(root: WebDriver | EC.WebElement, xpath: str, by=By.XPATH):
|
36
|
+
"""Shortcut for boilerplate to wait until a web element is visible"""
|
37
|
+
WebDriverWait(root, TIMEOUT).until(EC.element_to_be_clickable((by, xpath)))
|
38
|
+
|
39
|
+
|
40
|
+
def wait_and_find_element(root: WebDriver | EC.WebElement, xpath: str, by=By.XPATH) -> EC.WebElement:
|
41
|
+
"""Finds a web element, after first waiting to ensure it's visible"""
|
42
|
+
wait_for_element(root, xpath, by=by)
|
43
|
+
return root.find_element(by, xpath)
|
44
|
+
|
45
|
+
|
46
|
+
def run_test_conversion_with_gui(test_spec: ConversionTestSpec,
|
47
|
+
driver: WebDriver,
|
48
|
+
origin: str):
|
49
|
+
"""Runs a test conversion or series thereof through the GUI. Note that this requires the server to be started before
|
50
|
+
this is called.
|
51
|
+
|
52
|
+
Parameters
|
53
|
+
----------
|
54
|
+
test_spec : ConversionTestSpec
|
55
|
+
The specification for the test or series of tests to be run
|
56
|
+
driver : WebDriver
|
57
|
+
The WebDriver to be used for testing
|
58
|
+
origin : str
|
59
|
+
The address of the homepage of the testing server
|
60
|
+
"""
|
61
|
+
# Make temporary directories for the input and output files to be stored in
|
62
|
+
with TemporaryDirectory("_input") as input_dir, TemporaryDirectory("_output") as output_dir:
|
63
|
+
# Iterate over the test spec to run each individual test it defines
|
64
|
+
for single_test_spec in test_spec:
|
65
|
+
if single_test_spec.skip:
|
66
|
+
print(f"Skipping single test spec {single_test_spec}")
|
67
|
+
continue
|
68
|
+
print(f"Running single test spec: {single_test_spec}")
|
69
|
+
_run_single_test_conversion_with_gui(test_spec=single_test_spec,
|
70
|
+
input_dir=input_dir,
|
71
|
+
output_dir=output_dir,
|
72
|
+
driver=driver,
|
73
|
+
origin=origin)
|
74
|
+
print(f"Success for test spec: {single_test_spec}")
|
75
|
+
|
76
|
+
|
77
|
+
def _run_single_test_conversion_with_gui(test_spec: SingleConversionTestSpec,
|
78
|
+
input_dir: str,
|
79
|
+
output_dir: str,
|
80
|
+
driver: WebDriver,
|
81
|
+
origin: str):
|
82
|
+
"""Runs a single test conversion through the GUI.
|
83
|
+
|
84
|
+
Parameters
|
85
|
+
----------
|
86
|
+
test_spec : SingleConversionTestSpec
|
87
|
+
The specification for the test to be run
|
88
|
+
input_dir : str
|
89
|
+
A directory which can be used to store input data before uploading
|
90
|
+
output_dir : str
|
91
|
+
A directory which can be used to create output data after downloading
|
92
|
+
driver : WebDriver
|
93
|
+
The WebDriver to be used for testing
|
94
|
+
origin : str
|
95
|
+
The address of the homepage of the testing server
|
96
|
+
"""
|
97
|
+
|
98
|
+
exc_info: pytest.ExceptionInfo | None = None
|
99
|
+
if test_spec.expect_success:
|
100
|
+
try:
|
101
|
+
run_converter_through_gui(test_spec=test_spec,
|
102
|
+
input_dir=input_dir,
|
103
|
+
output_dir=output_dir,
|
104
|
+
driver=driver,
|
105
|
+
origin=origin,
|
106
|
+
**test_spec.conversion_kwargs)
|
107
|
+
success = True
|
108
|
+
except Exception:
|
109
|
+
print(f"Unexpected exception raised for single test spec {test_spec}")
|
110
|
+
raise
|
111
|
+
else:
|
112
|
+
with pytest.raises(FileConverterException) as exc_info:
|
113
|
+
run_converter_through_gui(test_spec=test_spec,
|
114
|
+
input_dir=input_dir,
|
115
|
+
output_dir=output_dir,
|
116
|
+
driver=driver,
|
117
|
+
origin=origin,
|
118
|
+
**test_spec.conversion_kwargs)
|
119
|
+
success = False
|
120
|
+
|
121
|
+
# Compile output info for the test and call the callback function if one is provided
|
122
|
+
if test_spec.callback:
|
123
|
+
test_info = ConversionTestInfo(run_type="gui",
|
124
|
+
test_spec=test_spec,
|
125
|
+
input_dir=input_dir,
|
126
|
+
output_dir=output_dir,
|
127
|
+
success=success,
|
128
|
+
exc_info=exc_info)
|
129
|
+
callback_msg = test_spec.callback(test_info)
|
130
|
+
assert not callback_msg, callback_msg
|
131
|
+
|
132
|
+
|
133
|
+
def run_converter_through_gui(test_spec: SingleConversionTestSpec,
|
134
|
+
input_dir: str,
|
135
|
+
output_dir: str,
|
136
|
+
driver: WebDriver,
|
137
|
+
origin: str,
|
138
|
+
**conversion_kwargs):
|
139
|
+
"""_summary_
|
140
|
+
|
141
|
+
Parameters
|
142
|
+
----------
|
143
|
+
test_spec : SingleConversionTestSpec
|
144
|
+
The specification for the test to be run
|
145
|
+
input_dir : str
|
146
|
+
The directory which contains the input file
|
147
|
+
output_dir : str
|
148
|
+
The directory which contains the output file
|
149
|
+
driver : WebDriver
|
150
|
+
The WebDriver to be used for testing
|
151
|
+
origin : str
|
152
|
+
The address of the homepage of the testing server
|
153
|
+
"""
|
154
|
+
|
155
|
+
# Get just the local filename
|
156
|
+
filename = os.path.split(test_spec.filename)[1]
|
157
|
+
|
158
|
+
# Default options for conversion
|
159
|
+
base_filename, from_format = split_archive_ext(filename)
|
160
|
+
strict = True
|
161
|
+
from_flags: str | None = None
|
162
|
+
to_flags: str | None = None
|
163
|
+
from_options: str | None = None
|
164
|
+
to_options: str | None = None
|
165
|
+
coord_gen = None
|
166
|
+
coord_gen_qual = None
|
167
|
+
|
168
|
+
# For each argument in the conversion kwargs, interpret it as the appropriate option for this conversion, overriding
|
169
|
+
# defaults set above
|
170
|
+
for key, val in conversion_kwargs.items():
|
171
|
+
if key == "from_format":
|
172
|
+
from_format = val
|
173
|
+
elif key == "log_mode":
|
174
|
+
raise ValueError(f"The conversion kwarg {key} is not valid with conversions through the GUI")
|
175
|
+
elif key == "delete_input":
|
176
|
+
raise ValueError(f"The conversion kwarg {key} is not valid with conversions through the GUI")
|
177
|
+
elif key == "strict":
|
178
|
+
strict = val
|
179
|
+
elif key == "max_file_size":
|
180
|
+
raise ValueError(f"The conversion kwarg {key} is not valid with conversions through the GUI")
|
181
|
+
elif key == "data":
|
182
|
+
for subkey, subval in val.items():
|
183
|
+
if subkey == "from_flags":
|
184
|
+
from_flags = subval
|
185
|
+
elif subkey == "to_flags":
|
186
|
+
to_flags = subval
|
187
|
+
elif subkey == "from_options":
|
188
|
+
from_options = subval
|
189
|
+
elif subkey == "to_options":
|
190
|
+
to_options = subval
|
191
|
+
elif subkey == COORD_GEN_KEY:
|
192
|
+
coord_gen = subval
|
193
|
+
if COORD_GEN_QUAL_KEY in val:
|
194
|
+
coord_gen_qual = val[COORD_GEN_QUAL_KEY]
|
195
|
+
else:
|
196
|
+
coord_gen_qual = DEFAULT_COORD_GEN_QUAL
|
197
|
+
elif subkey == COORD_GEN_QUAL_KEY:
|
198
|
+
# Handled alongside COORD_GEN_KEY above
|
199
|
+
pass
|
200
|
+
else:
|
201
|
+
pytest.fail(f"The key 'data[\"{subkey}\"]' was passed to `conversion_kwargs` but could not be "
|
202
|
+
"interpreted")
|
203
|
+
else:
|
204
|
+
pytest.fail(f"The key '{key}' was passed to `conversion_kwargs` but could not be interpreted")
|
205
|
+
|
206
|
+
# Cleanup of arguments
|
207
|
+
if from_format.startswith("."):
|
208
|
+
from_format = from_format[1:]
|
209
|
+
|
210
|
+
# Set up the input file where we expect it to be
|
211
|
+
source_input_file = os.path.realpath(os.path.join(get_input_test_data_loc(), test_spec.filename))
|
212
|
+
input_file = os.path.join(input_dir, test_spec.filename)
|
213
|
+
if (os.path.isfile(input_file)):
|
214
|
+
os.unlink(input_file)
|
215
|
+
os.symlink(source_input_file, input_file)
|
216
|
+
|
217
|
+
# Remove test files from Downloads directory if they exist.
|
218
|
+
|
219
|
+
log_file = os.path.realpath(os.path.join(os.path.expanduser("~/Downloads"), test_spec.log_filename))
|
220
|
+
if (os.path.isfile(log_file)):
|
221
|
+
os.remove(log_file)
|
222
|
+
|
223
|
+
output_file = os.path.realpath(os.path.join(os.path.expanduser("~/Downloads"), test_spec.out_filename))
|
224
|
+
if (os.path.isfile(output_file)):
|
225
|
+
os.remove(output_file)
|
226
|
+
|
227
|
+
# Get the homepage
|
228
|
+
driver.get(f"{origin}/")
|
229
|
+
|
230
|
+
wait_for_element(driver, "//select[@id='fromList']/option")
|
231
|
+
|
232
|
+
# Select from_format from the 'from' list.
|
233
|
+
driver.find_element(By.XPATH, f"//select[@id='fromList']/option[starts-with(.,'{from_format}:')]").click()
|
234
|
+
|
235
|
+
# Select to_format from the 'to' list.
|
236
|
+
driver.find_element(By.XPATH, f"//select[@id='toList']/option[starts-with(.,'{test_spec.to_format}:')]").click()
|
237
|
+
|
238
|
+
# Select converter from the available conversion options list.
|
239
|
+
driver.find_element(By.XPATH, f"//select[@id='success']/option[contains(.,'{test_spec.converter_name}')]").click()
|
240
|
+
|
241
|
+
# Click on the "Yes" button to accept the converter and go to the conversion page
|
242
|
+
driver.find_element(By.XPATH, "//input[@id='yesButton']").click()
|
243
|
+
|
244
|
+
# Request non-strict filename checking if desired
|
245
|
+
if not strict:
|
246
|
+
wait_and_find_element(driver, "//input[@id='extCheck']").click()
|
247
|
+
|
248
|
+
# Select the input file
|
249
|
+
wait_and_find_element(driver, "//input[@id='fileToUpload']").send_keys(str(input_file))
|
250
|
+
|
251
|
+
# An alert may be present here, which we check for using a try block
|
252
|
+
try:
|
253
|
+
WebDriverWait(driver, 0.2).until(EC.alert_is_present())
|
254
|
+
alert = Alert(driver)
|
255
|
+
alert_text = alert.text
|
256
|
+
alert.dismiss()
|
257
|
+
raise FileConverterInputException(alert_text)
|
258
|
+
except TimeoutException:
|
259
|
+
pass
|
260
|
+
|
261
|
+
# Request the log file
|
262
|
+
wait_and_find_element(driver, "//input[@id='requestLog']").click()
|
263
|
+
|
264
|
+
# Select appropriate format args. The args only have a text attribute, so we need to find the one that starts with
|
265
|
+
# each flag - since we don't have too many, iterating over all possible combinations is the easiest way
|
266
|
+
for (l_flags, select_id) in ((from_flags, "inFlags"),
|
267
|
+
(to_flags, "outFlags")):
|
268
|
+
if not l_flags:
|
269
|
+
continue
|
270
|
+
flags_select = Select(wait_and_find_element(driver, f"//select[@id='{select_id}']"))
|
271
|
+
for flag in l_flags:
|
272
|
+
found = False
|
273
|
+
for option in flags_select.options:
|
274
|
+
if option.text.startswith(f"{flag}:"):
|
275
|
+
flags_select.select_by_visible_text(option.text)
|
276
|
+
found = True
|
277
|
+
break
|
278
|
+
if not found:
|
279
|
+
raise ValueError(f"Flag {flag} was not found in {select_id} selection box for conversion from "
|
280
|
+
f"{from_format} to {test_spec.to_format} with converter {test_spec.converter_name}")
|
281
|
+
|
282
|
+
for (options_string, table_id) in ((from_options, "in_argFlags"),
|
283
|
+
(to_options, "out_argFlags")):
|
284
|
+
if not options_string:
|
285
|
+
continue
|
286
|
+
|
287
|
+
# Split each option into words, of which the first letter of each is the key and the remainder is the value
|
288
|
+
l_options = options_string.split()
|
289
|
+
|
290
|
+
# Get the rows in the options table
|
291
|
+
options_table = wait_and_find_element(driver, f"//table[@id='{table_id}']")
|
292
|
+
l_rows = options_table.find_elements(By.XPATH, "./tr")
|
293
|
+
|
294
|
+
# Look for and set each option
|
295
|
+
for option in l_options:
|
296
|
+
found = False
|
297
|
+
for row in l_rows:
|
298
|
+
l_items = row.find_elements(By.XPATH, "./td")
|
299
|
+
label = l_items[1]
|
300
|
+
if not label.text.startswith(option[0]):
|
301
|
+
continue
|
302
|
+
|
303
|
+
# Select the option by clicking the box at the first element in the row to make the input appear
|
304
|
+
l_items[0].click()
|
305
|
+
|
306
|
+
# Input the option in the input box that appears in the third position in the row
|
307
|
+
input_box = wait_and_find_element(l_items[2], "./input")
|
308
|
+
input_box.send_keys(option[1:])
|
309
|
+
|
310
|
+
found = True
|
311
|
+
break
|
312
|
+
|
313
|
+
if not found:
|
314
|
+
raise ValueError(f"Option {option} was not found in {table_id} options table for conversion from "
|
315
|
+
f"{from_format} to {test_spec.to_format} with converter {test_spec.converter_name}")
|
316
|
+
|
317
|
+
# If radio-button settings are supplied, apply them now
|
318
|
+
for setting, name, l_allowed in ((coord_gen, "coord_gen", L_ALLOWED_COORD_GENS),
|
319
|
+
(coord_gen_qual, "coord_gen_qual", L_ALLOWED_COORD_GEN_QUALS)):
|
320
|
+
if not setting:
|
321
|
+
continue
|
322
|
+
|
323
|
+
if setting not in l_allowed:
|
324
|
+
raise ValueError(f"Invalid {name} value supplied: {setting}. Allowed values are: " +
|
325
|
+
str(l_allowed))
|
326
|
+
|
327
|
+
setting_radio = wait_and_find_element(driver, f"//input[@value='{setting}']")
|
328
|
+
setting_radio.click()
|
329
|
+
|
330
|
+
# Click on the "Convert" button.
|
331
|
+
wait_and_find_element(driver, "//input[@id='uploadButton']").click()
|
332
|
+
|
333
|
+
# Handle alert box.
|
334
|
+
WebDriverWait(driver, TIMEOUT).until(EC.alert_is_present())
|
335
|
+
alert = Alert(driver)
|
336
|
+
alert_text = alert.text
|
337
|
+
alert.dismiss()
|
338
|
+
|
339
|
+
if alert_text.startswith("ERROR:"):
|
340
|
+
# Raise an appropriate exception type depending on if it's a recognised input issue or not
|
341
|
+
if "unexpected exception" in alert_text:
|
342
|
+
raise FileConverterAbortException(STATUS_CODE_GENERAL, alert_text)
|
343
|
+
raise FileConverterInputException(alert_text)
|
344
|
+
|
345
|
+
# Wait until the log file exists, since it's downloaded second
|
346
|
+
time_elapsed = 0
|
347
|
+
while not os.path.isfile(log_file):
|
348
|
+
time.sleep(1)
|
349
|
+
time_elapsed += 1
|
350
|
+
if time_elapsed > TIMEOUT:
|
351
|
+
pytest.fail(f"Download of {output_file} and {log_file} timed out")
|
352
|
+
|
353
|
+
time.sleep(1)
|
354
|
+
|
355
|
+
if not os.path.isfile(output_file):
|
356
|
+
raise FileConverterAbortException("ERROR: No output file was produced. Log contents:\n" +
|
357
|
+
open(log_file, "r").read())
|
358
|
+
|
359
|
+
# Move the output file and log file to the expected locations
|
360
|
+
for qual_filename in output_file, log_file:
|
361
|
+
base_filename = os.path.split(qual_filename)[1]
|
362
|
+
target_filename = os.path.join(output_dir, base_filename)
|
363
|
+
if os.path.isfile(target_filename):
|
364
|
+
os.remove(target_filename)
|
365
|
+
if os.path.isfile(qual_filename):
|
366
|
+
shutil.move(qual_filename, target_filename)
|
@@ -19,19 +19,55 @@ from unittest.mock import patch
|
|
19
19
|
import py
|
20
20
|
import pytest
|
21
21
|
|
22
|
+
import psdi_data_conversion
|
22
23
|
from psdi_data_conversion.constants import CONVERTER_DEFAULT, GLOBAL_LOG_FILENAME, LOG_NONE, OUTPUT_LOG_EXT
|
23
24
|
from psdi_data_conversion.converter import run_converter
|
24
25
|
from psdi_data_conversion.converters.openbabel import COORD_GEN_KEY, COORD_GEN_QUAL_KEY
|
25
26
|
from psdi_data_conversion.dist import LINUX_LABEL, get_dist
|
26
27
|
from psdi_data_conversion.file_io import is_archive, split_archive_ext
|
27
28
|
from psdi_data_conversion.main import main as data_convert_main
|
28
|
-
from psdi_data_conversion.testing.constants import
|
29
|
+
from psdi_data_conversion.testing.constants import (INPUT_TEST_DATA_LOC_IN_PROJECT, OUTPUT_TEST_DATA_LOC_IN_PROJECT,
|
30
|
+
TEST_DATA_LOC_IN_PROJECT)
|
31
|
+
|
32
|
+
|
33
|
+
def get_path_in_project(filename):
|
34
|
+
"""Get the realpath to a file contained within the project, given its project-relative path"""
|
35
|
+
|
36
|
+
old_cwd = os.getcwd()
|
37
|
+
|
38
|
+
try:
|
39
|
+
os.chdir(os.path.join(psdi_data_conversion.__path__[0], ".."))
|
40
|
+
realpath = os.path.realpath(filename)
|
41
|
+
|
42
|
+
finally:
|
43
|
+
# Change back to the previous directory
|
44
|
+
os.chdir(old_cwd)
|
45
|
+
|
46
|
+
return realpath
|
47
|
+
|
48
|
+
|
49
|
+
def get_test_data_loc():
|
50
|
+
"""Get the realpath of the base directory containing all data for tests"""
|
51
|
+
return get_path_in_project(TEST_DATA_LOC_IN_PROJECT)
|
52
|
+
|
53
|
+
|
54
|
+
def get_input_test_data_loc():
|
55
|
+
"""Get the realpath of the base directory containing input data for tests"""
|
56
|
+
return get_path_in_project(INPUT_TEST_DATA_LOC_IN_PROJECT)
|
57
|
+
|
58
|
+
|
59
|
+
def get_output_test_data_loc():
|
60
|
+
"""Get the realpath of the base directory containing expected output data for tests"""
|
61
|
+
return get_path_in_project(OUTPUT_TEST_DATA_LOC_IN_PROJECT)
|
29
62
|
|
30
63
|
|
31
64
|
@dataclass
|
32
65
|
class ConversionTestInfo:
|
33
66
|
"""Information about a tested conversion."""
|
34
67
|
|
68
|
+
run_type: str
|
69
|
+
"""One of "library", "cla", or "gui", describing which type of test run was performed"""
|
70
|
+
|
35
71
|
test_spec: SingleConversionTestSpec
|
36
72
|
"""The specification of the test conversion which was run to produce this"""
|
37
73
|
|
@@ -50,20 +86,23 @@ class ConversionTestInfo:
|
|
50
86
|
captured_stderr: str | None = None
|
51
87
|
"""Any output to stderr while the test was run"""
|
52
88
|
|
89
|
+
exc_info: pytest.ExceptionInfo | None = None
|
90
|
+
"""If the test conversion raised an exception, that exception's info, otherwise None"""
|
91
|
+
|
53
92
|
@property
|
54
93
|
def qualified_in_filename(self):
|
55
94
|
"""Get the fully-qualified name of the input file"""
|
56
|
-
return os.path.join(self.input_dir, self.test_spec.filename)
|
95
|
+
return os.path.realpath(os.path.join(self.input_dir, self.test_spec.filename))
|
57
96
|
|
58
97
|
@property
|
59
98
|
def qualified_out_filename(self):
|
60
99
|
"""Get the fully-qualified name of the output file"""
|
61
|
-
return os.path.join(self.output_dir, self.test_spec.out_filename)
|
100
|
+
return os.path.realpath(os.path.join(self.output_dir, self.test_spec.out_filename))
|
62
101
|
|
63
102
|
@property
|
64
103
|
def qualified_log_filename(self):
|
65
104
|
"""Get the fully-qualified name of the log file"""
|
66
|
-
return os.path.join(self.output_dir, self.test_spec.log_filename)
|
105
|
+
return os.path.realpath(os.path.join(self.output_dir, self.test_spec.log_filename))
|
67
106
|
|
68
107
|
@property
|
69
108
|
def qualified_global_log_filename(self):
|
@@ -71,28 +110,6 @@ class ConversionTestInfo:
|
|
71
110
|
return self.test_spec.global_log_filename
|
72
111
|
|
73
112
|
|
74
|
-
@dataclass
|
75
|
-
class LibraryConversionTestInfo(ConversionTestInfo):
|
76
|
-
"""Information about a tested conversion, specifically for when it was tested through a call to the library"""
|
77
|
-
|
78
|
-
exc_info: pytest.ExceptionInfo | None = None
|
79
|
-
"""If the test conversion raised an exception, that exception's info, otherwise None"""
|
80
|
-
|
81
|
-
|
82
|
-
@dataclass
|
83
|
-
class CLAConversionTestInfo(ConversionTestInfo):
|
84
|
-
"""Information about a tested conversion, specifically for when it was tested through a the command-line
|
85
|
-
application (CLA)
|
86
|
-
"""
|
87
|
-
|
88
|
-
|
89
|
-
@dataclass
|
90
|
-
class GUIConversionTestInfo(ConversionTestInfo):
|
91
|
-
"""Information about a tested conversion, specifically for when it was tested through the GUI (the local version of
|
92
|
-
the web app)
|
93
|
-
"""
|
94
|
-
|
95
|
-
|
96
113
|
@dataclass
|
97
114
|
class ConversionTestSpec:
|
98
115
|
"""Class providing a specification for a test file conversion.
|
@@ -121,6 +138,11 @@ class ConversionTestSpec:
|
|
121
138
|
expect_success: bool | Iterable[bool] = True
|
122
139
|
"""Whether or not to expect the test to succeed"""
|
123
140
|
|
141
|
+
skip: bool | Iterable[bool] = False
|
142
|
+
"""If set to true, this test will be skipped and not run. Can also be set individually for certain tests within an
|
143
|
+
array. This should typically only be used when debugging to skip working tests to more easily focus on non-working
|
144
|
+
tests"""
|
145
|
+
|
124
146
|
callback: (Callable[[ConversionTestInfo], str] |
|
125
147
|
Iterable[Callable[[ConversionTestInfo], str]] | None) = None
|
126
148
|
"""Function to be called after the conversion is performed to check in detail whether results are as expected. It
|
@@ -187,6 +209,9 @@ class ConversionTestSpec:
|
|
187
209
|
if attr_name in l_single_val_attrs:
|
188
210
|
setattr(self, attr_name, [getattr(self, attr_name)]*self._len)
|
189
211
|
|
212
|
+
# Check if all tests should be skipped
|
213
|
+
self.skip_all = all(self.skip)
|
214
|
+
|
190
215
|
def __len__(self):
|
191
216
|
"""Get the length from the member - valid only after `__post_init__` has been called"""
|
192
217
|
return self._len
|
@@ -221,6 +246,9 @@ class SingleConversionTestSpec:
|
|
221
246
|
expect_success: bool = True
|
222
247
|
"""Whether or not to expect the test to succeed"""
|
223
248
|
|
249
|
+
skip: bool = False
|
250
|
+
"""If set to True, this test will be skipped, always returning success"""
|
251
|
+
|
224
252
|
callback: (Callable[[ConversionTestInfo], str] | None) = None
|
225
253
|
"""Function to be called after the conversion is performed to check in detail whether results are as expected. It
|
226
254
|
should take as its only argument a `ConversionTestInfo` and return a string. The string should be empty if the check
|
@@ -258,9 +286,14 @@ def run_test_conversion_with_library(test_spec: ConversionTestSpec):
|
|
258
286
|
with TemporaryDirectory("_input") as input_dir, TemporaryDirectory("_output") as output_dir:
|
259
287
|
# Iterate over the test spec to run each individual test it defines
|
260
288
|
for single_test_spec in test_spec:
|
289
|
+
if single_test_spec.skip:
|
290
|
+
print(f"Skipping single test spec {single_test_spec}")
|
291
|
+
continue
|
292
|
+
print(f"Running single test spec: {single_test_spec}")
|
261
293
|
_run_single_test_conversion_with_library(test_spec=single_test_spec,
|
262
294
|
input_dir=input_dir,
|
263
295
|
output_dir=output_dir)
|
296
|
+
print(f"Success for test spec: {single_test_spec}")
|
264
297
|
|
265
298
|
|
266
299
|
def _run_single_test_conversion_with_library(test_spec: SingleConversionTestSpec,
|
@@ -279,9 +312,9 @@ def _run_single_test_conversion_with_library(test_spec: SingleConversionTestSpec
|
|
279
312
|
"""
|
280
313
|
|
281
314
|
# Symlink the input file to the input directory
|
282
|
-
qualified_in_filename = os.path.join(input_dir, test_spec.filename)
|
315
|
+
qualified_in_filename = os.path.realpath(os.path.join(input_dir, test_spec.filename))
|
283
316
|
try:
|
284
|
-
os.symlink(os.path.join(
|
317
|
+
os.symlink(os.path.join(get_input_test_data_loc(), test_spec.filename),
|
285
318
|
qualified_in_filename)
|
286
319
|
except FileExistsError:
|
287
320
|
pass
|
@@ -316,13 +349,14 @@ def _run_single_test_conversion_with_library(test_spec: SingleConversionTestSpec
|
|
316
349
|
|
317
350
|
# Compile output info for the test and call the callback function if one is provided
|
318
351
|
if test_spec.callback:
|
319
|
-
test_info =
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
352
|
+
test_info = ConversionTestInfo(run_type="library",
|
353
|
+
test_spec=test_spec,
|
354
|
+
input_dir=input_dir,
|
355
|
+
output_dir=output_dir,
|
356
|
+
success=success,
|
357
|
+
captured_stdout=stdout,
|
358
|
+
captured_stderr=stderr,
|
359
|
+
exc_info=exc_info)
|
326
360
|
callback_msg = test_spec.callback(test_info)
|
327
361
|
assert not callback_msg, callback_msg
|
328
362
|
|
@@ -339,9 +373,14 @@ def run_test_conversion_with_cla(test_spec: ConversionTestSpec):
|
|
339
373
|
with TemporaryDirectory("_input") as input_dir, TemporaryDirectory("_output") as output_dir:
|
340
374
|
# Iterate over the test spec to run each individual test it defines
|
341
375
|
for single_test_spec in test_spec:
|
376
|
+
if single_test_spec.skip:
|
377
|
+
print(f"Skipping single test spec {single_test_spec}")
|
378
|
+
continue
|
379
|
+
print(f"Running single test spec: {single_test_spec}")
|
342
380
|
_run_single_test_conversion_with_cla(test_spec=single_test_spec,
|
343
381
|
input_dir=input_dir,
|
344
382
|
output_dir=output_dir)
|
383
|
+
print(f"Success for test spec: {single_test_spec}")
|
345
384
|
|
346
385
|
|
347
386
|
def _run_single_test_conversion_with_cla(test_spec: SingleConversionTestSpec,
|
@@ -360,9 +399,9 @@ def _run_single_test_conversion_with_cla(test_spec: SingleConversionTestSpec,
|
|
360
399
|
"""
|
361
400
|
|
362
401
|
# Symlink the input file to the input directory
|
363
|
-
qualified_in_filename = os.path.join(input_dir, test_spec.filename)
|
402
|
+
qualified_in_filename = os.path.realpath(os.path.join(input_dir, test_spec.filename))
|
364
403
|
try:
|
365
|
-
os.symlink(os.path.join(
|
404
|
+
os.symlink(os.path.join(get_input_test_data_loc(), test_spec.filename),
|
366
405
|
qualified_in_filename)
|
367
406
|
except FileExistsError:
|
368
407
|
pass
|
@@ -392,7 +431,7 @@ def _run_single_test_conversion_with_cla(test_spec: SingleConversionTestSpec,
|
|
392
431
|
# Get the success from whether or not the exit code is 0
|
393
432
|
success = not exc_info.value.code
|
394
433
|
|
395
|
-
qualified_out_filename = os.path.join(output_dir, test_spec.out_filename)
|
434
|
+
qualified_out_filename = os.path.realpath(os.path.join(output_dir, test_spec.out_filename))
|
396
435
|
|
397
436
|
# Determine success based on whether or not the output file exists with non-zero size
|
398
437
|
if not os.path.isfile(qualified_out_filename) or os.path.getsize(qualified_out_filename) == 0:
|
@@ -405,12 +444,13 @@ def _run_single_test_conversion_with_cla(test_spec: SingleConversionTestSpec,
|
|
405
444
|
|
406
445
|
# Compile output info for the test and call the callback function if one is provided
|
407
446
|
if test_spec.callback:
|
408
|
-
test_info =
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
447
|
+
test_info = ConversionTestInfo(run_type="cla",
|
448
|
+
test_spec=test_spec,
|
449
|
+
input_dir=input_dir,
|
450
|
+
output_dir=output_dir,
|
451
|
+
success=success,
|
452
|
+
captured_stdout=stdout,
|
453
|
+
captured_stderr=stderr)
|
414
454
|
callback_msg = test_spec.callback(test_info)
|
415
455
|
assert not callback_msg, callback_msg
|
416
456
|
|
@@ -441,6 +481,8 @@ def run_converter_through_cla(filename: str,
|
|
441
481
|
The directory which contains the output file
|
442
482
|
log_file : str
|
443
483
|
The desired name of the log file
|
484
|
+
conversion_kwargs : Any
|
485
|
+
Additional arguments describing the conversion
|
444
486
|
"""
|
445
487
|
|
446
488
|
# Start the argument string with the arguments we will always include
|