mustrd 0.2.0__py3-none-any.whl → 0.2.1__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.
- mustrd/README.adoc +210 -210
- mustrd/TestResult.py +136 -136
- mustrd/logger_setup.py +48 -48
- mustrd/model/catalog-v001.xml +5 -5
- mustrd/model/mustrdShapes.ttl +253 -253
- mustrd/model/mustrdTestShapes.ttl +24 -24
- mustrd/model/ontology.ttl +494 -494
- mustrd/model/test-resources/resources.ttl +60 -60
- mustrd/model/triplestoreOntology.ttl +174 -174
- mustrd/model/triplestoreshapes.ttl +41 -41
- mustrd/mustrd.py +787 -787
- mustrd/mustrdAnzo.py +236 -220
- mustrd/mustrdGraphDb.py +125 -125
- mustrd/mustrdRdfLib.py +56 -56
- mustrd/mustrdTestPlugin.py +327 -328
- mustrd/namespace.py +125 -125
- mustrd/run.py +106 -106
- mustrd/spec_component.py +690 -690
- mustrd/steprunner.py +166 -166
- mustrd/templates/md_ResultList_leaf_template.jinja +18 -18
- mustrd/templates/md_ResultList_template.jinja +8 -8
- mustrd/templates/md_stats_template.jinja +2 -2
- mustrd/test/test_mustrd.py +4 -4
- mustrd/utils.py +38 -38
- {mustrd-0.2.0.dist-info → mustrd-0.2.1.dist-info}/LICENSE +21 -21
- {mustrd-0.2.0.dist-info → mustrd-0.2.1.dist-info}/METADATA +4 -2
- mustrd-0.2.1.dist-info/RECORD +31 -0
- mustrd/mustrdQueryProcessor.py +0 -136
- mustrd-0.2.0.dist-info/RECORD +0 -32
- {mustrd-0.2.0.dist-info → mustrd-0.2.1.dist-info}/WHEEL +0 -0
- {mustrd-0.2.0.dist-info → mustrd-0.2.1.dist-info}/entry_points.txt +0 -0
mustrd/mustrdTestPlugin.py
CHANGED
@@ -1,328 +1,327 @@
|
|
1
|
-
"""
|
2
|
-
MIT License
|
3
|
-
|
4
|
-
Copyright (c) 2023 Semantic Partners Ltd
|
5
|
-
|
6
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
-
of this software and associated documentation files (the "Software"), to deal
|
8
|
-
in the Software without restriction, including without limitation the rights
|
9
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
-
copies of the Software, and to permit persons to whom the Software is
|
11
|
-
furnished to do so, subject to the following conditions:
|
12
|
-
|
13
|
-
The above copyright notice and this permission notice shall be included in all
|
14
|
-
copies or substantial portions of the Software.
|
15
|
-
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22
|
-
SOFTWARE.
|
23
|
-
"""
|
24
|
-
|
25
|
-
from dataclasses import dataclass
|
26
|
-
import pytest
|
27
|
-
import os
|
28
|
-
from pathlib import Path
|
29
|
-
from rdflib.namespace import Namespace
|
30
|
-
from rdflib import Graph, RDF
|
31
|
-
from pytest import Session
|
32
|
-
|
33
|
-
from mustrd.TestResult import ResultList, TestResult, get_result_list
|
34
|
-
from mustrd.utils import get_mustrd_root
|
35
|
-
from mustrd.mustrd import get_triple_store_graph, get_triple_stores
|
36
|
-
from mustrd.mustrd import Specification, SpecSkipped, validate_specs, get_specs, SpecPassed, run_spec
|
37
|
-
from mustrd.namespace import MUST, TRIPLESTORE, MUSTRDTEST
|
38
|
-
from typing import Union
|
39
|
-
from pyshacl import validate
|
40
|
-
|
41
|
-
spnamespace = Namespace("https://semanticpartners.com/data/test/")
|
42
|
-
|
43
|
-
mustrd_root = get_mustrd_root()
|
44
|
-
|
45
|
-
MUSTRD_PYTEST_PATH = "mustrd_tests/"
|
46
|
-
|
47
|
-
|
48
|
-
def pytest_addoption(parser):
|
49
|
-
group = parser.getgroup("mustrd option")
|
50
|
-
group.addoption(
|
51
|
-
"--mustrd",
|
52
|
-
action="store_true",
|
53
|
-
dest="mustrd",
|
54
|
-
help="Activate/deactivate mustrd test generation.",
|
55
|
-
)
|
56
|
-
group.addoption(
|
57
|
-
"--md",
|
58
|
-
action="store",
|
59
|
-
dest="mdpath",
|
60
|
-
metavar="pathToMdSummary",
|
61
|
-
default=None,
|
62
|
-
help="create md summary file at that path.",
|
63
|
-
)
|
64
|
-
group.addoption(
|
65
|
-
"--config",
|
66
|
-
action="store",
|
67
|
-
dest="configpath",
|
68
|
-
metavar="pathToTestConfig",
|
69
|
-
default=None,
|
70
|
-
help="Ttl file containing the list of test to construct.",
|
71
|
-
)
|
72
|
-
group.addoption(
|
73
|
-
"--secrets",
|
74
|
-
action="store",
|
75
|
-
dest="secrets",
|
76
|
-
metavar="Secrets",
|
77
|
-
default=None,
|
78
|
-
help="Give the secrets by command line in order to be able to store secrets safely in CI tools",
|
79
|
-
)
|
80
|
-
return
|
81
|
-
|
82
|
-
|
83
|
-
def pytest_configure(config) -> None:
|
84
|
-
# Read configuration file
|
85
|
-
if config.getoption("mustrd"):
|
86
|
-
test_configs = parse_config(config.getoption("configpath"))
|
87
|
-
config.pluginmanager.register(MustrdTestPlugin(config.getoption("mdpath"),
|
88
|
-
test_configs, config.getoption("secrets")))
|
89
|
-
|
90
|
-
def parse_config(config_path):
|
91
|
-
test_configs = []
|
92
|
-
config_graph = Graph().parse(config_path)
|
93
|
-
shacl_graph = Graph().parse(Path(os.path.join(mustrd_root, "model/mustrdTestShapes.ttl")))
|
94
|
-
ont_graph = Graph().parse(Path(os.path.join(mustrd_root, "model/mustrdTestOntology.ttl")))
|
95
|
-
conforms, results_graph, results_text = validate(
|
96
|
-
data_graph= config_graph,
|
97
|
-
shacl_graph = shacl_graph,
|
98
|
-
ont_graph = ont_graph,
|
99
|
-
advanced= True,
|
100
|
-
inference= 'none'
|
101
|
-
)
|
102
|
-
if not conforms:
|
103
|
-
raise ValueError(f"Mustrd test configuration not conform to the shapes. SHACL report: {results_text}", results_graph)
|
104
|
-
|
105
|
-
for test_config_subject in config_graph.subjects(predicate=RDF.type, object=MUSTRDTEST.MustrdTest):
|
106
|
-
spec_path = get_config_param(config_graph, test_config_subject, MUSTRDTEST.hasSpecPath, str)
|
107
|
-
data_path = get_config_param(config_graph, test_config_subject, MUSTRDTEST.hasDataPath, str)
|
108
|
-
triplestore_spec_path = get_config_param(config_graph, test_config_subject, MUSTRDTEST.triplestoreSpecPath, str)
|
109
|
-
pytest_path = get_config_param(config_graph, test_config_subject, MUSTRDTEST.hasPytestPath, str)
|
110
|
-
filter_on_tripleStore = list(config_graph.objects(subject=test_config_subject,
|
111
|
-
predicate=MUSTRDTEST.filterOnTripleStore))
|
112
|
-
|
113
|
-
test_configs.append(TestConfig(spec_path=spec_path, data_path=data_path,
|
114
|
-
triplestore_spec_path=triplestore_spec_path,
|
115
|
-
pytest_path = pytest_path,
|
116
|
-
filter_on_tripleStore=filter_on_tripleStore))
|
117
|
-
return test_configs
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
def get_config_param(config_graph, config_subject, config_param, convert_function):
|
122
|
-
raw_value = config_graph.value(subject=config_subject, predicate=config_param, any=True)
|
123
|
-
return convert_function(raw_value) if raw_value else None
|
124
|
-
|
125
|
-
|
126
|
-
@dataclass
|
127
|
-
class TestConfig:
|
128
|
-
spec_path: str
|
129
|
-
data_path: str
|
130
|
-
triplestore_spec_path: str
|
131
|
-
pytest_path: str
|
132
|
-
filter_on_tripleStore: str = None
|
133
|
-
|
134
|
-
|
135
|
-
@dataclass
|
136
|
-
class TestParamWrapper:
|
137
|
-
test_config: TestConfig
|
138
|
-
unit_test: Union[Specification, SpecSkipped]
|
139
|
-
|
140
|
-
class MustrdTestPlugin:
|
141
|
-
md_path: str
|
142
|
-
test_configs: list
|
143
|
-
secrets: str
|
144
|
-
unit_tests: Union[Specification, SpecSkipped]
|
145
|
-
items: list
|
146
|
-
|
147
|
-
def __init__(self, md_path, test_configs, secrets):
|
148
|
-
self.md_path = md_path
|
149
|
-
self.test_configs = test_configs
|
150
|
-
self.secrets = secrets
|
151
|
-
self.items = []
|
152
|
-
|
153
|
-
@pytest.hookimpl(tryfirst=True)
|
154
|
-
def pytest_collection(self, session):
|
155
|
-
self.unit_tests = []
|
156
|
-
args = session.config.args
|
157
|
-
if len(args) > 0:
|
158
|
-
file_name = self.get_file_name_from_arg(args[0])
|
159
|
-
# Filter test to collect only specified path
|
160
|
-
config_to_collect = list(filter(lambda config:
|
161
|
-
# Case we want to collect everything
|
162
|
-
MUSTRD_PYTEST_PATH not in args[0]
|
163
|
-
# Case we want to collect a test or sub test
|
164
|
-
or (config.pytest_path or "") in args[0]
|
165
|
-
# Case we want to collect a whole test folder
|
166
|
-
or args[0].replace(f"./{MUSTRD_PYTEST_PATH}", "") in config.pytest_path,
|
167
|
-
self.test_configs))
|
168
|
-
|
169
|
-
# Redirect everything to test_mustrd.py, no need to filter on specified test: Only specified test will be collected anyway
|
170
|
-
session.config.args[0] = os.path.join(mustrd_root, "test/test_mustrd.py")
|
171
|
-
# Collecting only relevant tests
|
172
|
-
|
173
|
-
for one_test_config in config_to_collect:
|
174
|
-
triple_stores = self.get_triple_stores_from_file(one_test_config)
|
175
|
-
|
176
|
-
if one_test_config.filter_on_tripleStore and not triple_stores:
|
177
|
-
self.unit_tests.extend(list(map(lambda triple_store:
|
178
|
-
TestParamWrapper(test_config = one_test_config, unit_test=SpecSkipped(MUST.TestSpec, triple_store, "No triplestore found")),
|
179
|
-
one_test_config.filter_on_tripleStore)))
|
180
|
-
else:
|
181
|
-
specs = self.generate_tests_for_config({"spec_path": Path(one_test_config.spec_path),
|
182
|
-
"data_path": Path(one_test_config.data_path)},
|
183
|
-
triple_stores, file_name)
|
184
|
-
self.unit_tests.extend(list(map(lambda spec: TestParamWrapper(test_config = one_test_config, unit_test=spec),specs)))
|
185
|
-
|
186
|
-
def get_file_name_from_arg(self, arg):
|
187
|
-
if arg and len(arg) > 0 and "[" in arg and ".mustrd.ttl@" in arg:
|
188
|
-
return arg[arg.index("[") + 1: arg.index(".mustrd.ttl@")]
|
189
|
-
return None
|
190
|
-
|
191
|
-
|
192
|
-
@pytest.hookimpl(hookwrapper=True)
|
193
|
-
def pytest_pycollect_makeitem(self, collector, name, obj):
|
194
|
-
report = yield
|
195
|
-
if name == "test_unit":
|
196
|
-
items = report.get_result()
|
197
|
-
new_results = []
|
198
|
-
for item in items:
|
199
|
-
virtual_path = MUSTRD_PYTEST_PATH + (item.callspec.params["unit_tests"].test_config.pytest_path or "default")
|
200
|
-
item.fspath = Path(virtual_path)
|
201
|
-
item._nodeid = virtual_path + "::" + item.name
|
202
|
-
self.items.append(item)
|
203
|
-
new_results.append(item)
|
204
|
-
return new_results
|
205
|
-
|
206
|
-
|
207
|
-
# Hook called at collection time: reads the configuration of the tests, and generate pytests from it
|
208
|
-
def pytest_generate_tests(self, metafunc):
|
209
|
-
if len(metafunc.fixturenames) > 0:
|
210
|
-
if metafunc.function.__name__ == "test_unit":
|
211
|
-
# Create the test in itself
|
212
|
-
if self.unit_tests:
|
213
|
-
metafunc.parametrize(metafunc.fixturenames[0], self.unit_tests,
|
214
|
-
ids=lambda test_param: (test_param.unit_test.spec_file_name or "") + "@" +
|
215
|
-
(test_param.test_config.pytest_path or ""))
|
216
|
-
else:
|
217
|
-
metafunc.parametrize(metafunc.fixturenames[0],
|
218
|
-
[SpecSkipped(MUST.TestSpec, None, "No triplestore found")],
|
219
|
-
ids=lambda x: "No configuration found for this test")
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
# Generate test for each triple store available
|
225
|
-
def generate_tests_for_config(self, config, triple_stores, file_name):
|
226
|
-
|
227
|
-
shacl_graph = Graph().parse(Path(os.path.join(mustrd_root, "model/mustrdShapes.ttl")))
|
228
|
-
ont_graph = Graph().parse(Path(os.path.join(mustrd_root, "model/ontology.ttl")))
|
229
|
-
valid_spec_uris, spec_graph, invalid_spec_results = validate_specs(config, triple_stores,
|
230
|
-
shacl_graph, ont_graph, file_name or "*")
|
231
|
-
|
232
|
-
specs, skipped_spec_results = \
|
233
|
-
get_specs(valid_spec_uris, spec_graph, triple_stores, config)
|
234
|
-
|
235
|
-
# Return normal specs + skipped results
|
236
|
-
return specs + skipped_spec_results + invalid_spec_results
|
237
|
-
|
238
|
-
# Function called to generate the name of the test
|
239
|
-
def get_test_name(self, spec):
|
240
|
-
# FIXME: SpecSkipped should have the same structure?
|
241
|
-
if isinstance(spec, SpecSkipped):
|
242
|
-
triple_store = spec.triple_store
|
243
|
-
else:
|
244
|
-
triple_store = spec.triple_store['type']
|
245
|
-
triple_store_name = triple_store.replace("https://mustrd.com/model/", "")
|
246
|
-
test_name = spec.spec_uri.replace(spnamespace, "").replace("_", " ")
|
247
|
-
return triple_store_name + ": " + test_name
|
248
|
-
|
249
|
-
# Get triple store configuration or default
|
250
|
-
def get_triple_stores_from_file(self, test_config):
|
251
|
-
if test_config.triplestore_spec_path:
|
252
|
-
try:
|
253
|
-
triple_stores = get_triple_stores(get_triple_store_graph(Path(test_config.triplestore_spec_path),
|
254
|
-
self.secrets))
|
255
|
-
except Exception as e:
|
256
|
-
print(f"""Triplestore configuration parsing failed {test_config.triplestore_spec_path}.
|
257
|
-
Only rdflib will be executed""", e)
|
258
|
-
triple_stores = [{'type': TRIPLESTORE.RdfLib, 'uri': TRIPLESTORE.RdfLib}]
|
259
|
-
else:
|
260
|
-
print("No triple store configuration required: using embedded rdflib")
|
261
|
-
triple_stores = [{'type': TRIPLESTORE.RdfLib, 'uri': TRIPLESTORE.RdfLib}]
|
262
|
-
|
263
|
-
if test_config.filter_on_tripleStore:
|
264
|
-
triple_stores = list(filter(lambda triple_store: (triple_store["uri"] in test_config.filter_on_tripleStore),
|
265
|
-
triple_stores))
|
266
|
-
return triple_stores
|
267
|
-
|
268
|
-
# Hook function. Initialize the list of result in session
|
269
|
-
def pytest_sessionstart(self, session):
|
270
|
-
session.results = dict()
|
271
|
-
|
272
|
-
# Hook function called each time a report is generated by a test
|
273
|
-
# The report is added to a list in the session
|
274
|
-
# so it can be used later in pytest_sessionfinish to generate the global report md file
|
275
|
-
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
276
|
-
def pytest_runtest_makereport(self, item, call):
|
277
|
-
outcome = yield
|
278
|
-
result = outcome.get_result()
|
279
|
-
|
280
|
-
if result.when == 'call':
|
281
|
-
# Add the result of the test to the session
|
282
|
-
item.session.results[item] = result
|
283
|
-
|
284
|
-
# Take all the test results in session, parse them, split them in mustrd and standard pytest and generate md file
|
285
|
-
def pytest_sessionfinish(self, session: Session, exitstatus):
|
286
|
-
# if md path has not been defined in argument, then do not generate md file
|
287
|
-
if not self.md_path:
|
288
|
-
return
|
289
|
-
|
290
|
-
test_results = []
|
291
|
-
for test_conf, result in session.results.items():
|
292
|
-
# Case auto generated tests
|
293
|
-
if test_conf.originalname != test_conf.name:
|
294
|
-
module_name = test_conf.parent.name
|
295
|
-
class_name = test_conf.originalname
|
296
|
-
test_name = test_conf.name.replace(class_name, "").replace("[", "").replace("]", "")
|
297
|
-
is_mustrd = True
|
298
|
-
# Case normal unit tests
|
299
|
-
else:
|
300
|
-
module_name = test_conf.parent.parent.name
|
301
|
-
class_name = test_conf.parent.name
|
302
|
-
test_name = test_conf.originalname
|
303
|
-
is_mustrd = False
|
304
|
-
|
305
|
-
test_results.append(TestResult(test_name, class_name, module_name, result.outcome, is_mustrd))
|
306
|
-
|
307
|
-
result_list = ResultList(None, get_result_list(test_results,
|
308
|
-
lambda result: result.type,
|
309
|
-
lambda result: result.
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
result_type
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
return result_type == SpecPassed
|
1
|
+
"""
|
2
|
+
MIT License
|
3
|
+
|
4
|
+
Copyright (c) 2023 Semantic Partners Ltd
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
8
|
+
in the Software without restriction, including without limitation the rights
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
11
|
+
furnished to do so, subject to the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
14
|
+
copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22
|
+
SOFTWARE.
|
23
|
+
"""
|
24
|
+
|
25
|
+
from dataclasses import dataclass
|
26
|
+
import pytest
|
27
|
+
import os
|
28
|
+
from pathlib import Path
|
29
|
+
from rdflib.namespace import Namespace
|
30
|
+
from rdflib import Graph, RDF
|
31
|
+
from pytest import Session
|
32
|
+
|
33
|
+
from mustrd.TestResult import ResultList, TestResult, get_result_list
|
34
|
+
from mustrd.utils import get_mustrd_root
|
35
|
+
from mustrd.mustrd import get_triple_store_graph, get_triple_stores
|
36
|
+
from mustrd.mustrd import Specification, SpecSkipped, validate_specs, get_specs, SpecPassed, run_spec
|
37
|
+
from mustrd.namespace import MUST, TRIPLESTORE, MUSTRDTEST
|
38
|
+
from typing import Union
|
39
|
+
from pyshacl import validate
|
40
|
+
|
41
|
+
spnamespace = Namespace("https://semanticpartners.com/data/test/")
|
42
|
+
|
43
|
+
mustrd_root = get_mustrd_root()
|
44
|
+
|
45
|
+
MUSTRD_PYTEST_PATH = "mustrd_tests/"
|
46
|
+
|
47
|
+
|
48
|
+
def pytest_addoption(parser):
|
49
|
+
group = parser.getgroup("mustrd option")
|
50
|
+
group.addoption(
|
51
|
+
"--mustrd",
|
52
|
+
action="store_true",
|
53
|
+
dest="mustrd",
|
54
|
+
help="Activate/deactivate mustrd test generation.",
|
55
|
+
)
|
56
|
+
group.addoption(
|
57
|
+
"--md",
|
58
|
+
action="store",
|
59
|
+
dest="mdpath",
|
60
|
+
metavar="pathToMdSummary",
|
61
|
+
default=None,
|
62
|
+
help="create md summary file at that path.",
|
63
|
+
)
|
64
|
+
group.addoption(
|
65
|
+
"--config",
|
66
|
+
action="store",
|
67
|
+
dest="configpath",
|
68
|
+
metavar="pathToTestConfig",
|
69
|
+
default=None,
|
70
|
+
help="Ttl file containing the list of test to construct.",
|
71
|
+
)
|
72
|
+
group.addoption(
|
73
|
+
"--secrets",
|
74
|
+
action="store",
|
75
|
+
dest="secrets",
|
76
|
+
metavar="Secrets",
|
77
|
+
default=None,
|
78
|
+
help="Give the secrets by command line in order to be able to store secrets safely in CI tools",
|
79
|
+
)
|
80
|
+
return
|
81
|
+
|
82
|
+
|
83
|
+
def pytest_configure(config) -> None:
|
84
|
+
# Read configuration file
|
85
|
+
if config.getoption("mustrd"):
|
86
|
+
test_configs = parse_config(config.getoption("configpath"))
|
87
|
+
config.pluginmanager.register(MustrdTestPlugin(config.getoption("mdpath"),
|
88
|
+
test_configs, config.getoption("secrets")))
|
89
|
+
|
90
|
+
def parse_config(config_path):
|
91
|
+
test_configs = []
|
92
|
+
config_graph = Graph().parse(config_path)
|
93
|
+
shacl_graph = Graph().parse(Path(os.path.join(mustrd_root, "model/mustrdTestShapes.ttl")))
|
94
|
+
ont_graph = Graph().parse(Path(os.path.join(mustrd_root, "model/mustrdTestOntology.ttl")))
|
95
|
+
conforms, results_graph, results_text = validate(
|
96
|
+
data_graph= config_graph,
|
97
|
+
shacl_graph = shacl_graph,
|
98
|
+
ont_graph = ont_graph,
|
99
|
+
advanced= True,
|
100
|
+
inference= 'none'
|
101
|
+
)
|
102
|
+
if not conforms:
|
103
|
+
raise ValueError(f"Mustrd test configuration not conform to the shapes. SHACL report: {results_text}", results_graph)
|
104
|
+
|
105
|
+
for test_config_subject in config_graph.subjects(predicate=RDF.type, object=MUSTRDTEST.MustrdTest):
|
106
|
+
spec_path = get_config_param(config_graph, test_config_subject, MUSTRDTEST.hasSpecPath, str)
|
107
|
+
data_path = get_config_param(config_graph, test_config_subject, MUSTRDTEST.hasDataPath, str)
|
108
|
+
triplestore_spec_path = get_config_param(config_graph, test_config_subject, MUSTRDTEST.triplestoreSpecPath, str)
|
109
|
+
pytest_path = get_config_param(config_graph, test_config_subject, MUSTRDTEST.hasPytestPath, str)
|
110
|
+
filter_on_tripleStore = list(config_graph.objects(subject=test_config_subject,
|
111
|
+
predicate=MUSTRDTEST.filterOnTripleStore))
|
112
|
+
|
113
|
+
test_configs.append(TestConfig(spec_path=spec_path, data_path=data_path,
|
114
|
+
triplestore_spec_path=triplestore_spec_path,
|
115
|
+
pytest_path = pytest_path,
|
116
|
+
filter_on_tripleStore=filter_on_tripleStore))
|
117
|
+
return test_configs
|
118
|
+
|
119
|
+
|
120
|
+
|
121
|
+
def get_config_param(config_graph, config_subject, config_param, convert_function):
|
122
|
+
raw_value = config_graph.value(subject=config_subject, predicate=config_param, any=True)
|
123
|
+
return convert_function(raw_value) if raw_value else None
|
124
|
+
|
125
|
+
|
126
|
+
@dataclass
|
127
|
+
class TestConfig:
|
128
|
+
spec_path: str
|
129
|
+
data_path: str
|
130
|
+
triplestore_spec_path: str
|
131
|
+
pytest_path: str
|
132
|
+
filter_on_tripleStore: str = None
|
133
|
+
|
134
|
+
|
135
|
+
@dataclass
|
136
|
+
class TestParamWrapper:
|
137
|
+
test_config: TestConfig
|
138
|
+
unit_test: Union[Specification, SpecSkipped]
|
139
|
+
|
140
|
+
class MustrdTestPlugin:
|
141
|
+
md_path: str
|
142
|
+
test_configs: list
|
143
|
+
secrets: str
|
144
|
+
unit_tests: Union[Specification, SpecSkipped]
|
145
|
+
items: list
|
146
|
+
|
147
|
+
def __init__(self, md_path, test_configs, secrets):
|
148
|
+
self.md_path = md_path
|
149
|
+
self.test_configs = test_configs
|
150
|
+
self.secrets = secrets
|
151
|
+
self.items = []
|
152
|
+
|
153
|
+
@pytest.hookimpl(tryfirst=True)
|
154
|
+
def pytest_collection(self, session):
|
155
|
+
self.unit_tests = []
|
156
|
+
args = session.config.args
|
157
|
+
if len(args) > 0:
|
158
|
+
file_name = self.get_file_name_from_arg(args[0])
|
159
|
+
# Filter test to collect only specified path
|
160
|
+
config_to_collect = list(filter(lambda config:
|
161
|
+
# Case we want to collect everything
|
162
|
+
MUSTRD_PYTEST_PATH not in args[0]
|
163
|
+
# Case we want to collect a test or sub test
|
164
|
+
or (config.pytest_path or "") in args[0]
|
165
|
+
# Case we want to collect a whole test folder
|
166
|
+
or args[0].replace(f"./{MUSTRD_PYTEST_PATH}", "") in config.pytest_path,
|
167
|
+
self.test_configs))
|
168
|
+
|
169
|
+
# Redirect everything to test_mustrd.py, no need to filter on specified test: Only specified test will be collected anyway
|
170
|
+
session.config.args[0] = os.path.join(mustrd_root, "test/test_mustrd.py")
|
171
|
+
# Collecting only relevant tests
|
172
|
+
|
173
|
+
for one_test_config in config_to_collect:
|
174
|
+
triple_stores = self.get_triple_stores_from_file(one_test_config)
|
175
|
+
|
176
|
+
if one_test_config.filter_on_tripleStore and not triple_stores:
|
177
|
+
self.unit_tests.extend(list(map(lambda triple_store:
|
178
|
+
TestParamWrapper(test_config = one_test_config, unit_test=SpecSkipped(MUST.TestSpec, triple_store, "No triplestore found")),
|
179
|
+
one_test_config.filter_on_tripleStore)))
|
180
|
+
else:
|
181
|
+
specs = self.generate_tests_for_config({"spec_path": Path(one_test_config.spec_path),
|
182
|
+
"data_path": Path(one_test_config.data_path)},
|
183
|
+
triple_stores, file_name)
|
184
|
+
self.unit_tests.extend(list(map(lambda spec: TestParamWrapper(test_config = one_test_config, unit_test=spec),specs)))
|
185
|
+
|
186
|
+
def get_file_name_from_arg(self, arg):
|
187
|
+
if arg and len(arg) > 0 and "[" in arg and ".mustrd.ttl@" in arg:
|
188
|
+
return arg[arg.index("[") + 1: arg.index(".mustrd.ttl@")]
|
189
|
+
return None
|
190
|
+
|
191
|
+
|
192
|
+
@pytest.hookimpl(hookwrapper=True)
|
193
|
+
def pytest_pycollect_makeitem(self, collector, name, obj):
|
194
|
+
report = yield
|
195
|
+
if name == "test_unit":
|
196
|
+
items = report.get_result()
|
197
|
+
new_results = []
|
198
|
+
for item in items:
|
199
|
+
virtual_path = MUSTRD_PYTEST_PATH + (item.callspec.params["unit_tests"].test_config.pytest_path or "default")
|
200
|
+
item.fspath = Path(virtual_path)
|
201
|
+
item._nodeid = virtual_path + "::" + item.name
|
202
|
+
self.items.append(item)
|
203
|
+
new_results.append(item)
|
204
|
+
return new_results
|
205
|
+
|
206
|
+
|
207
|
+
# Hook called at collection time: reads the configuration of the tests, and generate pytests from it
|
208
|
+
def pytest_generate_tests(self, metafunc):
|
209
|
+
if len(metafunc.fixturenames) > 0:
|
210
|
+
if metafunc.function.__name__ == "test_unit":
|
211
|
+
# Create the test in itself
|
212
|
+
if self.unit_tests:
|
213
|
+
metafunc.parametrize(metafunc.fixturenames[0], self.unit_tests,
|
214
|
+
ids=lambda test_param: (test_param.unit_test.spec_file_name or "") + "@" +
|
215
|
+
(test_param.test_config.pytest_path or ""))
|
216
|
+
else:
|
217
|
+
metafunc.parametrize(metafunc.fixturenames[0],
|
218
|
+
[SpecSkipped(MUST.TestSpec, None, "No triplestore found")],
|
219
|
+
ids=lambda x: "No configuration found for this test")
|
220
|
+
|
221
|
+
|
222
|
+
|
223
|
+
|
224
|
+
# Generate test for each triple store available
|
225
|
+
def generate_tests_for_config(self, config, triple_stores, file_name):
|
226
|
+
|
227
|
+
shacl_graph = Graph().parse(Path(os.path.join(mustrd_root, "model/mustrdShapes.ttl")))
|
228
|
+
ont_graph = Graph().parse(Path(os.path.join(mustrd_root, "model/ontology.ttl")))
|
229
|
+
valid_spec_uris, spec_graph, invalid_spec_results = validate_specs(config, triple_stores,
|
230
|
+
shacl_graph, ont_graph, file_name or "*")
|
231
|
+
|
232
|
+
specs, skipped_spec_results = \
|
233
|
+
get_specs(valid_spec_uris, spec_graph, triple_stores, config)
|
234
|
+
|
235
|
+
# Return normal specs + skipped results
|
236
|
+
return specs + skipped_spec_results + invalid_spec_results
|
237
|
+
|
238
|
+
# Function called to generate the name of the test
|
239
|
+
def get_test_name(self, spec):
|
240
|
+
# FIXME: SpecSkipped should have the same structure?
|
241
|
+
if isinstance(spec, SpecSkipped):
|
242
|
+
triple_store = spec.triple_store
|
243
|
+
else:
|
244
|
+
triple_store = spec.triple_store['type']
|
245
|
+
triple_store_name = triple_store.replace("https://mustrd.com/model/", "")
|
246
|
+
test_name = spec.spec_uri.replace(spnamespace, "").replace("_", " ")
|
247
|
+
return triple_store_name + ": " + test_name
|
248
|
+
|
249
|
+
# Get triple store configuration or default
|
250
|
+
def get_triple_stores_from_file(self, test_config):
|
251
|
+
if test_config.triplestore_spec_path:
|
252
|
+
try:
|
253
|
+
triple_stores = get_triple_stores(get_triple_store_graph(Path(test_config.triplestore_spec_path),
|
254
|
+
self.secrets))
|
255
|
+
except Exception as e:
|
256
|
+
print(f"""Triplestore configuration parsing failed {test_config.triplestore_spec_path}.
|
257
|
+
Only rdflib will be executed""", e)
|
258
|
+
triple_stores = [{'type': TRIPLESTORE.RdfLib, 'uri': TRIPLESTORE.RdfLib}]
|
259
|
+
else:
|
260
|
+
print("No triple store configuration required: using embedded rdflib")
|
261
|
+
triple_stores = [{'type': TRIPLESTORE.RdfLib, 'uri': TRIPLESTORE.RdfLib}]
|
262
|
+
|
263
|
+
if test_config.filter_on_tripleStore:
|
264
|
+
triple_stores = list(filter(lambda triple_store: (triple_store["uri"] in test_config.filter_on_tripleStore),
|
265
|
+
triple_stores))
|
266
|
+
return triple_stores
|
267
|
+
|
268
|
+
# Hook function. Initialize the list of result in session
|
269
|
+
def pytest_sessionstart(self, session):
|
270
|
+
session.results = dict()
|
271
|
+
|
272
|
+
# Hook function called each time a report is generated by a test
|
273
|
+
# The report is added to a list in the session
|
274
|
+
# so it can be used later in pytest_sessionfinish to generate the global report md file
|
275
|
+
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
276
|
+
def pytest_runtest_makereport(self, item, call):
|
277
|
+
outcome = yield
|
278
|
+
result = outcome.get_result()
|
279
|
+
|
280
|
+
if result.when == 'call':
|
281
|
+
# Add the result of the test to the session
|
282
|
+
item.session.results[item] = result
|
283
|
+
|
284
|
+
# Take all the test results in session, parse them, split them in mustrd and standard pytest and generate md file
|
285
|
+
def pytest_sessionfinish(self, session: Session, exitstatus):
|
286
|
+
# if md path has not been defined in argument, then do not generate md file
|
287
|
+
if not self.md_path:
|
288
|
+
return
|
289
|
+
|
290
|
+
test_results = []
|
291
|
+
for test_conf, result in session.results.items():
|
292
|
+
# Case auto generated tests
|
293
|
+
if test_conf.originalname != test_conf.name:
|
294
|
+
module_name = test_conf.parent.name
|
295
|
+
class_name = test_conf.originalname
|
296
|
+
test_name = test_conf.name.replace(class_name, "").replace("[", "").replace("]", "")
|
297
|
+
is_mustrd = True
|
298
|
+
# Case normal unit tests
|
299
|
+
else:
|
300
|
+
module_name = test_conf.parent.parent.name
|
301
|
+
class_name = test_conf.parent.name
|
302
|
+
test_name = test_conf.originalname
|
303
|
+
is_mustrd = False
|
304
|
+
|
305
|
+
test_results.append(TestResult(test_name, class_name, module_name, result.outcome, is_mustrd))
|
306
|
+
|
307
|
+
result_list = ResultList(None, get_result_list(test_results,
|
308
|
+
lambda result: result.type,
|
309
|
+
lambda result: is_mustrd and result.test_name.split("@")[1]),
|
310
|
+
False)
|
311
|
+
|
312
|
+
md = result_list.render()
|
313
|
+
with open(self.md_path, 'w') as file:
|
314
|
+
file.write(md)
|
315
|
+
|
316
|
+
|
317
|
+
# Function called in the test to actually run it
|
318
|
+
def run_test_spec(test_spec):
|
319
|
+
if isinstance(test_spec, SpecSkipped):
|
320
|
+
pytest.skip(f"Invalid configuration, error : {test_spec.message}")
|
321
|
+
result = run_spec(test_spec)
|
322
|
+
|
323
|
+
result_type = type(result)
|
324
|
+
if result_type == SpecSkipped:
|
325
|
+
# FIXME: Better exception management
|
326
|
+
pytest.skip("Unsupported configuration")
|
327
|
+
return result_type == SpecPassed
|