psdi-data-conversion 0.0.23__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/__init__.py +11 -0
- psdi_data_conversion/app.py +242 -0
- psdi_data_conversion/bin/linux/atomsk +0 -0
- psdi_data_conversion/bin/linux/c2x +0 -0
- psdi_data_conversion/bin/mac/atomsk +0 -0
- psdi_data_conversion/bin/mac/c2x +0 -0
- psdi_data_conversion/constants.py +185 -0
- psdi_data_conversion/converter.py +459 -0
- psdi_data_conversion/converters/__init__.py +6 -0
- psdi_data_conversion/converters/atomsk.py +32 -0
- psdi_data_conversion/converters/base.py +702 -0
- psdi_data_conversion/converters/c2x.py +32 -0
- psdi_data_conversion/converters/openbabel.py +239 -0
- psdi_data_conversion/database.py +1064 -0
- psdi_data_conversion/dist.py +87 -0
- psdi_data_conversion/file_io.py +216 -0
- psdi_data_conversion/log_utility.py +241 -0
- psdi_data_conversion/main.py +776 -0
- psdi_data_conversion/scripts/atomsk.sh +32 -0
- psdi_data_conversion/scripts/c2x.sh +26 -0
- psdi_data_conversion/security.py +38 -0
- psdi_data_conversion/static/content/accessibility.htm +254 -0
- psdi_data_conversion/static/content/convert.htm +121 -0
- psdi_data_conversion/static/content/convertato.htm +65 -0
- psdi_data_conversion/static/content/convertc2x.htm +65 -0
- psdi_data_conversion/static/content/documentation.htm +94 -0
- psdi_data_conversion/static/content/feedback.htm +53 -0
- psdi_data_conversion/static/content/header-links.html +8 -0
- psdi_data_conversion/static/content/index-versions/header-links.html +8 -0
- psdi_data_conversion/static/content/index-versions/psdi-common-footer.html +99 -0
- psdi_data_conversion/static/content/index-versions/psdi-common-header.html +28 -0
- psdi_data_conversion/static/content/psdi-common-footer.html +99 -0
- psdi_data_conversion/static/content/psdi-common-header.html +28 -0
- psdi_data_conversion/static/content/report.htm +103 -0
- psdi_data_conversion/static/data/data.json +143940 -0
- psdi_data_conversion/static/img/colormode-toggle-dm.svg +3 -0
- psdi_data_conversion/static/img/colormode-toggle-lm.svg +3 -0
- psdi_data_conversion/static/img/psdi-icon-dark.svg +136 -0
- psdi_data_conversion/static/img/psdi-icon-light.svg +208 -0
- psdi_data_conversion/static/img/psdi-logo-darktext.png +0 -0
- psdi_data_conversion/static/img/psdi-logo-lighttext.png +0 -0
- psdi_data_conversion/static/img/social-logo-bluesky-black.svg +4 -0
- psdi_data_conversion/static/img/social-logo-bluesky-white.svg +4 -0
- psdi_data_conversion/static/img/social-logo-instagram-black.svg +1 -0
- psdi_data_conversion/static/img/social-logo-instagram-white.svg +1 -0
- psdi_data_conversion/static/img/social-logo-linkedin-black.png +0 -0
- psdi_data_conversion/static/img/social-logo-linkedin-white.png +0 -0
- psdi_data_conversion/static/img/social-logo-mastodon-black.svg +4 -0
- psdi_data_conversion/static/img/social-logo-mastodon-white.svg +4 -0
- psdi_data_conversion/static/img/social-logo-x-black.svg +3 -0
- psdi_data_conversion/static/img/social-logo-x-white.svg +3 -0
- psdi_data_conversion/static/img/social-logo-youtube-black.png +0 -0
- psdi_data_conversion/static/img/social-logo-youtube-white.png +0 -0
- psdi_data_conversion/static/img/ukri-epsr-logo-darktext.png +0 -0
- psdi_data_conversion/static/img/ukri-epsr-logo-lighttext.png +0 -0
- psdi_data_conversion/static/img/ukri-logo-darktext.png +0 -0
- psdi_data_conversion/static/img/ukri-logo-lighttext.png +0 -0
- psdi_data_conversion/static/javascript/accessibility.js +196 -0
- psdi_data_conversion/static/javascript/common.js +42 -0
- psdi_data_conversion/static/javascript/convert.js +296 -0
- psdi_data_conversion/static/javascript/convert_common.js +252 -0
- psdi_data_conversion/static/javascript/convertato.js +107 -0
- psdi_data_conversion/static/javascript/convertc2x.js +107 -0
- psdi_data_conversion/static/javascript/data.js +176 -0
- psdi_data_conversion/static/javascript/format.js +611 -0
- psdi_data_conversion/static/javascript/load_accessibility.js +89 -0
- psdi_data_conversion/static/javascript/psdi-common.js +177 -0
- psdi_data_conversion/static/javascript/report.js +381 -0
- psdi_data_conversion/static/styles/format.css +147 -0
- psdi_data_conversion/static/styles/psdi-common.css +705 -0
- psdi_data_conversion/templates/index.htm +114 -0
- psdi_data_conversion/testing/__init__.py +5 -0
- psdi_data_conversion/testing/constants.py +12 -0
- psdi_data_conversion/testing/conversion_callbacks.py +394 -0
- psdi_data_conversion/testing/conversion_test_specs.py +208 -0
- psdi_data_conversion/testing/utils.py +522 -0
- psdi_data_conversion-0.0.23.dist-info/METADATA +663 -0
- psdi_data_conversion-0.0.23.dist-info/RECORD +81 -0
- psdi_data_conversion-0.0.23.dist-info/WHEEL +4 -0
- psdi_data_conversion-0.0.23.dist-info/entry_points.txt +2 -0
- psdi_data_conversion-0.0.23.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,522 @@
|
|
1
|
+
"""
|
2
|
+
# utils.py
|
3
|
+
|
4
|
+
This module defines general classes and methods used for unit tests.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
from dataclasses import dataclass, field
|
10
|
+
from collections.abc import Callable, Iterable
|
11
|
+
from math import isclose
|
12
|
+
import os
|
13
|
+
import shlex
|
14
|
+
import sys
|
15
|
+
from tempfile import TemporaryDirectory
|
16
|
+
from typing import Any
|
17
|
+
from unittest.mock import patch
|
18
|
+
|
19
|
+
import py
|
20
|
+
import pytest
|
21
|
+
|
22
|
+
from psdi_data_conversion.constants import CONVERTER_DEFAULT, GLOBAL_LOG_FILENAME, LOG_NONE, OUTPUT_LOG_EXT
|
23
|
+
from psdi_data_conversion.converter import run_converter
|
24
|
+
from psdi_data_conversion.converters.openbabel import COORD_GEN_KEY, COORD_GEN_QUAL_KEY
|
25
|
+
from psdi_data_conversion.dist import LINUX_LABEL, get_dist
|
26
|
+
from psdi_data_conversion.file_io import is_archive, split_archive_ext
|
27
|
+
from psdi_data_conversion.main import main as data_convert_main
|
28
|
+
from psdi_data_conversion.testing.constants import INPUT_TEST_DATA_LOC
|
29
|
+
|
30
|
+
|
31
|
+
@dataclass
|
32
|
+
class ConversionTestInfo:
|
33
|
+
"""Information about a tested conversion."""
|
34
|
+
|
35
|
+
test_spec: SingleConversionTestSpec
|
36
|
+
"""The specification of the test conversion which was run to produce this"""
|
37
|
+
|
38
|
+
input_dir: str
|
39
|
+
"""The directory used to store input data for the test"""
|
40
|
+
|
41
|
+
output_dir: str
|
42
|
+
"""The directory used to create output data in for the test"""
|
43
|
+
|
44
|
+
success: bool = True
|
45
|
+
"""Whether or not the conversion was successful"""
|
46
|
+
|
47
|
+
captured_stdout: str | None = None
|
48
|
+
"""Any output to stdout while the test was run"""
|
49
|
+
|
50
|
+
captured_stderr: str | None = None
|
51
|
+
"""Any output to stderr while the test was run"""
|
52
|
+
|
53
|
+
@property
|
54
|
+
def qualified_in_filename(self):
|
55
|
+
"""Get the fully-qualified name of the input file"""
|
56
|
+
return os.path.join(self.input_dir, self.test_spec.filename)
|
57
|
+
|
58
|
+
@property
|
59
|
+
def qualified_out_filename(self):
|
60
|
+
"""Get the fully-qualified name of the output file"""
|
61
|
+
return os.path.join(self.output_dir, self.test_spec.out_filename)
|
62
|
+
|
63
|
+
@property
|
64
|
+
def qualified_log_filename(self):
|
65
|
+
"""Get the fully-qualified name of the log file"""
|
66
|
+
return os.path.join(self.output_dir, self.test_spec.log_filename)
|
67
|
+
|
68
|
+
@property
|
69
|
+
def qualified_global_log_filename(self):
|
70
|
+
"""Get the fully-qualified name of the log file"""
|
71
|
+
return self.test_spec.global_log_filename
|
72
|
+
|
73
|
+
|
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
|
+
@dataclass
|
97
|
+
class ConversionTestSpec:
|
98
|
+
"""Class providing a specification for a test file conversion.
|
99
|
+
|
100
|
+
All attributes of this class can be provided either as a single value or a list of values. In the case that a list
|
101
|
+
is provided for one or more attributes, the lists must all be the same length, and they will be iterated through
|
102
|
+
(as if using zip on the multiple lists) to test each element in turn.
|
103
|
+
"""
|
104
|
+
|
105
|
+
filename: str | Iterable[str] = "nacl.cif"
|
106
|
+
"""The name of the input file, relative to the input test data location, or a list thereof"""
|
107
|
+
|
108
|
+
to_format: str | Iterable[str] = "pdb"
|
109
|
+
"""The format to test converting the input file to, or a list thereof"""
|
110
|
+
|
111
|
+
name: str | Iterable[str] = CONVERTER_DEFAULT
|
112
|
+
"""The name of the converter to be used for the test, or a list thereof"""
|
113
|
+
|
114
|
+
conversion_kwargs: dict[str, Any] | Iterable[dict[str, Any]] = field(default_factory=dict)
|
115
|
+
"""Any keyword arguments to be provided to the call to `run_converter`, aside from those listed above, or a list
|
116
|
+
thereof"""
|
117
|
+
|
118
|
+
expect_success: bool | Iterable[bool] = True
|
119
|
+
"""Whether or not to expect the test to succeed"""
|
120
|
+
|
121
|
+
callback: (Callable[[ConversionTestInfo], str] |
|
122
|
+
Iterable[Callable[[ConversionTestInfo], str]] | None) = None
|
123
|
+
"""Function to be called after the conversion is performed to check in detail whether results are as expected. It
|
124
|
+
should take as its only argument a `ConversionTestInfo` and return a string. The string should be empty if the check
|
125
|
+
is passed and should explain the failure otherwise."""
|
126
|
+
|
127
|
+
def __post_init__(self):
|
128
|
+
"""Regularize the lengths of all attribute lists, in case some were provided as single values and others as
|
129
|
+
lists, and set up initial values
|
130
|
+
"""
|
131
|
+
|
132
|
+
# To ease maintainability, we get the list of this class's attributes automatically from its __dict__, excluding
|
133
|
+
# any which start with an underscore
|
134
|
+
self._l_attr_names: list[str] = [attr_name for attr_name in self.__dict__ if not attr_name.startswith("_")]
|
135
|
+
|
136
|
+
l_single_val_attrs = []
|
137
|
+
self._len: int = 1
|
138
|
+
|
139
|
+
# Check if each attribute of this class is provided as a list, and if any are, make sure that all lists are
|
140
|
+
# the same length
|
141
|
+
for attr_name in self._l_attr_names:
|
142
|
+
val = getattr(self, attr_name)
|
143
|
+
|
144
|
+
val_len = 1
|
145
|
+
|
146
|
+
# Check first if the attr is a str or a dict, which are iterable, but are single-values for the purpose
|
147
|
+
# of values here
|
148
|
+
if isinstance(val, (str, dict)):
|
149
|
+
l_single_val_attrs.append(attr_name)
|
150
|
+
else:
|
151
|
+
# It's not a str or a dict, so test if we can get the length of it, which indicates it is iterable
|
152
|
+
try:
|
153
|
+
val_len = len(val)
|
154
|
+
# If it's a single value in a list, unpack it for now
|
155
|
+
if val_len == 1:
|
156
|
+
# Pylint for some reason thinks `Any` objects aren't subscriptable, but here we know it is
|
157
|
+
val: Iterable[Any]
|
158
|
+
setattr(self, attr_name, val[0])
|
159
|
+
except TypeError:
|
160
|
+
l_single_val_attrs.append(attr_name)
|
161
|
+
|
162
|
+
# Check if there are any conflicts with some lists being provided as different lengths
|
163
|
+
if (self._len > 1) and (val_len > 1) and (val_len != self._len):
|
164
|
+
raise ValueError("All lists of values which are set as attributes for a `ConversionTestSpec` must be "
|
165
|
+
"the same length.")
|
166
|
+
if val_len > 1:
|
167
|
+
self._len = val_len
|
168
|
+
|
169
|
+
# At this point, self._len will be either 1 if all attrs are single values, or the length of the lists for attrs
|
170
|
+
# that aren't. To keep everything regularised, we make everything a list of this length
|
171
|
+
for attr_name in self._l_attr_names:
|
172
|
+
if attr_name in l_single_val_attrs:
|
173
|
+
setattr(self, attr_name, [getattr(self, attr_name)]*self._len)
|
174
|
+
|
175
|
+
def __len__(self):
|
176
|
+
"""Get the length from the member - valid only after `__post_init__` has been called"""
|
177
|
+
return self._len
|
178
|
+
|
179
|
+
def __iter__(self):
|
180
|
+
"""Allow to iterate over the class, getting a `SingleConversionTestSpec` for each value
|
181
|
+
"""
|
182
|
+
l_l_attr_vals = zip(*[getattr(self, attr_name) for attr_name in self._l_attr_names])
|
183
|
+
for l_attr_vals in l_l_attr_vals:
|
184
|
+
yield SingleConversionTestSpec(**dict(zip(self._l_attr_names, l_attr_vals)))
|
185
|
+
|
186
|
+
|
187
|
+
@dataclass
|
188
|
+
class SingleConversionTestSpec:
|
189
|
+
"""Class providing a specification for a single test file conversion, produced by iterating over a
|
190
|
+
`ConversionTestSpec` object
|
191
|
+
"""
|
192
|
+
|
193
|
+
filename: str
|
194
|
+
"""The name of the input file, relative to the input test data location"""
|
195
|
+
|
196
|
+
to_format: str
|
197
|
+
"""The format to test converting the input file to"""
|
198
|
+
|
199
|
+
name: str | Iterable[str] = CONVERTER_DEFAULT
|
200
|
+
"""The name of the converter to be used for the test"""
|
201
|
+
|
202
|
+
conversion_kwargs: dict[str, Any] = field(default_factory=dict)
|
203
|
+
"""Any keyword arguments to be provided to the call to `run_converter`, aside from those listed above and
|
204
|
+
`upload_dir` and `download_dir` (for which temporary directories are used)"""
|
205
|
+
|
206
|
+
expect_success: bool = True
|
207
|
+
"""Whether or not to expect the test to succeed"""
|
208
|
+
|
209
|
+
callback: (Callable[[ConversionTestInfo], str] | None) = None
|
210
|
+
"""Function to be called after the conversion is performed to check in detail whether results are as expected. It
|
211
|
+
should take as its only argument a `ConversionTestInfo` and return a string. The string should be empty if the check
|
212
|
+
is passed and should explain the failure otherwise."""
|
213
|
+
|
214
|
+
@property
|
215
|
+
def out_filename(self) -> str:
|
216
|
+
"""The unqualified name of the output file which should have been created by the conversion."""
|
217
|
+
if not is_archive(self.filename):
|
218
|
+
return f"{os.path.splitext(self.filename)[0]}.{self.to_format}"
|
219
|
+
else:
|
220
|
+
filename_base, ext = split_archive_ext(os.path.basename(self.filename))
|
221
|
+
return f"{filename_base}-{self.to_format}{ext}"
|
222
|
+
|
223
|
+
@property
|
224
|
+
def log_filename(self) -> str:
|
225
|
+
"""The unqualified name of the log file which should have been created by the conversion."""
|
226
|
+
return f"{split_archive_ext(self.filename)[0]}{OUTPUT_LOG_EXT}"
|
227
|
+
|
228
|
+
@property
|
229
|
+
def global_log_filename(self) -> str:
|
230
|
+
"""The unqualified name of the global log file which stores info on all conversions."""
|
231
|
+
return GLOBAL_LOG_FILENAME
|
232
|
+
|
233
|
+
|
234
|
+
def run_test_conversion_with_library(test_spec: ConversionTestSpec):
|
235
|
+
"""Runs a test conversion or series thereof through a call to the python library's `run_converter` function.
|
236
|
+
|
237
|
+
Parameters
|
238
|
+
----------
|
239
|
+
test_spec : ConversionTestSpec
|
240
|
+
The specification for the test or series of tests to be run
|
241
|
+
"""
|
242
|
+
# Make temporary directories for the input and output files to be stored in
|
243
|
+
with TemporaryDirectory("_input") as input_dir, TemporaryDirectory("_output") as output_dir:
|
244
|
+
# Iterate over the test spec to run each individual test it defines
|
245
|
+
for single_test_spec in test_spec:
|
246
|
+
_run_single_test_conversion_with_library(test_spec=single_test_spec,
|
247
|
+
input_dir=input_dir,
|
248
|
+
output_dir=output_dir)
|
249
|
+
|
250
|
+
|
251
|
+
def _run_single_test_conversion_with_library(test_spec: SingleConversionTestSpec,
|
252
|
+
input_dir: str,
|
253
|
+
output_dir: str):
|
254
|
+
"""Runs a single test conversion through a call to the python library's `run_converter` function.
|
255
|
+
|
256
|
+
Parameters
|
257
|
+
----------
|
258
|
+
test_spec : _SingleConversionTestSpec
|
259
|
+
The specification for the test to be run
|
260
|
+
input_dir : str
|
261
|
+
A directory which can be used to store input data
|
262
|
+
output_dir : str
|
263
|
+
A directory which can be used to create output data
|
264
|
+
"""
|
265
|
+
|
266
|
+
# Symlink the input file to the input directory
|
267
|
+
qualified_in_filename = os.path.join(input_dir, test_spec.filename)
|
268
|
+
try:
|
269
|
+
os.symlink(os.path.join(INPUT_TEST_DATA_LOC, test_spec.filename),
|
270
|
+
qualified_in_filename)
|
271
|
+
except FileExistsError:
|
272
|
+
pass
|
273
|
+
|
274
|
+
# Capture stdout and stderr while we run this test. We use a try block to stop capturing as soon as testing finishes
|
275
|
+
try:
|
276
|
+
stdouterr = py.io.StdCaptureFD(in_=False)
|
277
|
+
|
278
|
+
exc_info: pytest.ExceptionInfo | None = None
|
279
|
+
if test_spec.expect_success:
|
280
|
+
run_converter(filename=qualified_in_filename,
|
281
|
+
to_format=test_spec.to_format,
|
282
|
+
name=test_spec.name,
|
283
|
+
upload_dir=input_dir,
|
284
|
+
download_dir=output_dir,
|
285
|
+
**test_spec.conversion_kwargs)
|
286
|
+
success = True
|
287
|
+
else:
|
288
|
+
with pytest.raises(Exception) as exc_info:
|
289
|
+
run_converter(filename=qualified_in_filename,
|
290
|
+
to_format=test_spec.to_format,
|
291
|
+
name=test_spec.name,
|
292
|
+
upload_dir=input_dir,
|
293
|
+
download_dir=output_dir,
|
294
|
+
**test_spec.conversion_kwargs)
|
295
|
+
success = False
|
296
|
+
|
297
|
+
finally:
|
298
|
+
stdout, stderr = stdouterr.reset() # Grab stdout and stderr
|
299
|
+
# Reset stdout and stderr capture
|
300
|
+
stdouterr.done()
|
301
|
+
|
302
|
+
# Compile output info for the test and call the callback function if one is provided
|
303
|
+
if test_spec.callback:
|
304
|
+
test_info = LibraryConversionTestInfo(test_spec=test_spec,
|
305
|
+
input_dir=input_dir,
|
306
|
+
output_dir=output_dir,
|
307
|
+
success=success,
|
308
|
+
captured_stdout=stdout,
|
309
|
+
captured_stderr=stderr,
|
310
|
+
exc_info=exc_info)
|
311
|
+
callback_msg = test_spec.callback(test_info)
|
312
|
+
assert not callback_msg, callback_msg
|
313
|
+
|
314
|
+
|
315
|
+
def run_test_conversion_with_cla(test_spec: ConversionTestSpec):
|
316
|
+
"""Runs a test conversion or series thereof through the command-line application.
|
317
|
+
|
318
|
+
Parameters
|
319
|
+
----------
|
320
|
+
test_spec : ConversionTestSpec
|
321
|
+
The specification for the test or series of tests to be run
|
322
|
+
"""
|
323
|
+
# Make temporary directories for the input and output files to be stored in
|
324
|
+
with TemporaryDirectory("_input") as input_dir, TemporaryDirectory("_output") as output_dir:
|
325
|
+
# Iterate over the test spec to run each individual test it defines
|
326
|
+
for single_test_spec in test_spec:
|
327
|
+
_run_single_test_conversion_with_cla(test_spec=single_test_spec,
|
328
|
+
input_dir=input_dir,
|
329
|
+
output_dir=output_dir)
|
330
|
+
|
331
|
+
|
332
|
+
def _run_single_test_conversion_with_cla(test_spec: SingleConversionTestSpec,
|
333
|
+
input_dir: str,
|
334
|
+
output_dir: str):
|
335
|
+
"""Runs a single test conversion through the command-line application.
|
336
|
+
|
337
|
+
Parameters
|
338
|
+
----------
|
339
|
+
test_spec : _SingleConversionTestSpec
|
340
|
+
The specification for the test to be run
|
341
|
+
input_dir : str
|
342
|
+
A directory which can be used to store input data
|
343
|
+
output_dir : str
|
344
|
+
A directory which can be used to create output data
|
345
|
+
"""
|
346
|
+
|
347
|
+
# Symlink the input file to the input directory
|
348
|
+
qualified_in_filename = os.path.join(input_dir, test_spec.filename)
|
349
|
+
try:
|
350
|
+
os.symlink(os.path.join(INPUT_TEST_DATA_LOC, test_spec.filename),
|
351
|
+
qualified_in_filename)
|
352
|
+
except FileExistsError:
|
353
|
+
pass
|
354
|
+
|
355
|
+
# Capture stdout and stderr while we run this test. We use a try block to stop capturing as soon as testing finishes
|
356
|
+
try:
|
357
|
+
stdouterr = py.io.StdCaptureFD(in_=False)
|
358
|
+
|
359
|
+
if test_spec.expect_success:
|
360
|
+
run_converter_through_cla(filename=qualified_in_filename,
|
361
|
+
to_format=test_spec.to_format,
|
362
|
+
name=test_spec.name,
|
363
|
+
input_dir=input_dir,
|
364
|
+
output_dir=output_dir,
|
365
|
+
log_file=os.path.join(output_dir, test_spec.log_filename),
|
366
|
+
**test_spec.conversion_kwargs)
|
367
|
+
success = True
|
368
|
+
else:
|
369
|
+
with pytest.raises(SystemExit) as exc_info:
|
370
|
+
run_converter_through_cla(filename=qualified_in_filename,
|
371
|
+
to_format=test_spec.to_format,
|
372
|
+
name=test_spec.name,
|
373
|
+
input_dir=input_dir,
|
374
|
+
output_dir=output_dir,
|
375
|
+
log_file=os.path.join(output_dir, test_spec.log_filename),
|
376
|
+
**test_spec.conversion_kwargs)
|
377
|
+
# Get the success from whether or not the exit code is 0
|
378
|
+
success = not exc_info.value.code
|
379
|
+
|
380
|
+
qualified_out_filename = os.path.join(output_dir, test_spec.out_filename)
|
381
|
+
|
382
|
+
# Determine success based on whether or not the output file exists with non-zero size
|
383
|
+
if not os.path.isfile(qualified_out_filename) or os.path.getsize(qualified_out_filename) == 0:
|
384
|
+
success = False
|
385
|
+
|
386
|
+
finally:
|
387
|
+
stdout, stderr = stdouterr.reset() # Grab stdout and stderr
|
388
|
+
# Reset stdout and stderr capture
|
389
|
+
stdouterr.done()
|
390
|
+
|
391
|
+
# Compile output info for the test and call the callback function if one is provided
|
392
|
+
if test_spec.callback:
|
393
|
+
test_info = CLAConversionTestInfo(test_spec=test_spec,
|
394
|
+
input_dir=input_dir,
|
395
|
+
output_dir=output_dir,
|
396
|
+
success=success,
|
397
|
+
captured_stdout=stdout,
|
398
|
+
captured_stderr=stderr)
|
399
|
+
callback_msg = test_spec.callback(test_info)
|
400
|
+
assert not callback_msg, callback_msg
|
401
|
+
|
402
|
+
|
403
|
+
def run_converter_through_cla(filename: str,
|
404
|
+
to_format: str,
|
405
|
+
name: str,
|
406
|
+
input_dir: str,
|
407
|
+
output_dir: str,
|
408
|
+
log_file: str,
|
409
|
+
**conversion_kwargs):
|
410
|
+
"""Runs a test conversion through the command-line interface
|
411
|
+
|
412
|
+
This function constructs an argument string to be passed to the script, which is called with the
|
413
|
+
`run_with_arg_string` function defined below.
|
414
|
+
|
415
|
+
Parameters
|
416
|
+
----------
|
417
|
+
filename : str
|
418
|
+
The (unqualified) name of the input file to be converted
|
419
|
+
to_format : str
|
420
|
+
The format to convert the input file to
|
421
|
+
name : str
|
422
|
+
The name of the converter to use
|
423
|
+
input_dir : str
|
424
|
+
The directory which contains the input file
|
425
|
+
output_dir : str
|
426
|
+
The directory which contains the output file
|
427
|
+
log_file : str
|
428
|
+
The desired name of the log file
|
429
|
+
"""
|
430
|
+
|
431
|
+
# Start the argument string with the arguments we will always include
|
432
|
+
arg_string = f"{filename} -i {input_dir} -t {to_format} -o {output_dir} -w {name} --log-file {log_file}"
|
433
|
+
|
434
|
+
# For each argument in the conversion kwargs, convert it to the appropriate argument to be provided to the
|
435
|
+
# argument string
|
436
|
+
for key, val in conversion_kwargs.items():
|
437
|
+
if key == "from_format":
|
438
|
+
arg_string += f" -f {val}"
|
439
|
+
elif key == "log_mode":
|
440
|
+
if val == LOG_NONE:
|
441
|
+
arg_string += " -q"
|
442
|
+
else:
|
443
|
+
arg_string += f" --log-mode {val}"
|
444
|
+
elif key == "delete_input":
|
445
|
+
if val:
|
446
|
+
arg_string += " --delete-input"
|
447
|
+
elif key == "strict":
|
448
|
+
if val:
|
449
|
+
arg_string += " --strict"
|
450
|
+
elif key == "max_file_size":
|
451
|
+
if val != 0:
|
452
|
+
assert False, ("Test specification imposes a maximum file size, which isn't compatible with the "
|
453
|
+
"command-line application.")
|
454
|
+
elif key == "data":
|
455
|
+
for subkey, subval in val.items():
|
456
|
+
if subkey == "from_flags":
|
457
|
+
arg_string += f" --from-flags {subval}"
|
458
|
+
elif subkey == "to_flags":
|
459
|
+
arg_string += f" --to-flags {subval}"
|
460
|
+
elif subkey == "from_options":
|
461
|
+
arg_string += f" --from-options '{subval}'"
|
462
|
+
elif subkey == "to_options":
|
463
|
+
arg_string += f" --to-options '{subval}'"
|
464
|
+
elif subkey == COORD_GEN_KEY:
|
465
|
+
arg_string += f" --coord-gen {subval}"
|
466
|
+
if COORD_GEN_QUAL_KEY in val:
|
467
|
+
arg_string += f" {val[COORD_GEN_QUAL_KEY]}"
|
468
|
+
elif subkey == COORD_GEN_QUAL_KEY:
|
469
|
+
# Handled alongside COORD_GEN_KEY above
|
470
|
+
pass
|
471
|
+
else:
|
472
|
+
assert False, (f"The key 'data[\"{subkey}\"]' was passed to `conversion_kwargs` but could not be "
|
473
|
+
"interpreted")
|
474
|
+
else:
|
475
|
+
assert False, f"The key '{key}' was passed to `conversion_kwargs` but could not be interpreted"
|
476
|
+
|
477
|
+
run_with_arg_string(arg_string)
|
478
|
+
|
479
|
+
|
480
|
+
def run_with_arg_string(arg_string: str):
|
481
|
+
"""Runs the convert script with the provided argument string
|
482
|
+
"""
|
483
|
+
l_args = shlex.split("test " + arg_string)
|
484
|
+
with patch.object(sys, 'argv', l_args):
|
485
|
+
data_convert_main()
|
486
|
+
|
487
|
+
|
488
|
+
def check_file_match(filename: str, ex_filename: str) -> str:
|
489
|
+
"""Check that the contents of two files match without worrying about whitespace or negligible numerical differences.
|
490
|
+
"""
|
491
|
+
|
492
|
+
# Read in both files
|
493
|
+
text = open(filename, "r").read()
|
494
|
+
ex_text = open(ex_filename, "r").read()
|
495
|
+
|
496
|
+
# We want to check they're the same without worrying about whitespace (which doesn't matter for this format),
|
497
|
+
# so we accomplish this by using the string's `split` method, which splits on whitespace by default
|
498
|
+
l_words, l_ex_words = text.split(), ex_text.split()
|
499
|
+
|
500
|
+
# And we also want to avoid spurious false negatives from numerical comparisons (such as one file having
|
501
|
+
# negative zero and the other positive zero - yes, this happened), so we convert words to floats if possible
|
502
|
+
|
503
|
+
# We allow greater tolerance for numerical inaccuracy on platforms other than Linux, which is where the expected
|
504
|
+
# files were originally created
|
505
|
+
rel_tol = 0.001
|
506
|
+
abs_tol = 1e-6
|
507
|
+
if get_dist() != LINUX_LABEL:
|
508
|
+
rel_tol = 0.2
|
509
|
+
abs_tol = 0.01
|
510
|
+
|
511
|
+
for word, ex_word in zip(l_words, l_ex_words):
|
512
|
+
try:
|
513
|
+
val, ex_val = float(word), float(ex_word)
|
514
|
+
|
515
|
+
if not isclose(val, ex_val, rel_tol=rel_tol, abs_tol=abs_tol):
|
516
|
+
return (f"File comparison failed: {val} != {ex_val} with rel_tol={rel_tol} and abs_tol={abs_tol} "
|
517
|
+
f"when comparing files {filename} and {ex_filename}")
|
518
|
+
except ValueError:
|
519
|
+
# If it can't be converted to a float, treat it as a string and require an exact match
|
520
|
+
if not word == ex_word:
|
521
|
+
return f"File comparison failed: {word} != {ex_word} when comparing files {filename} and {ex_filename}"
|
522
|
+
return ""
|