mustrd 0.2.7a0__py3-none-any.whl → 0.3.1a0__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.md +2 -0
- mustrd/anzo_utils.py +8 -5
- mustrd/logger_setup.py +3 -0
- mustrd/model/mustrdShapes.ttl +25 -6
- mustrd/model/ontology.ttl +6 -2
- mustrd/mustrd.py +508 -235
- mustrd/mustrdAnzo.py +3 -2
- mustrd/mustrdRdfLib.py +8 -1
- mustrd/mustrdTestPlugin.py +299 -128
- mustrd/namespace.py +10 -1
- mustrd/spec_component.py +238 -58
- mustrd/steprunner.py +78 -20
- mustrd-0.3.1a0.dist-info/METADATA +96 -0
- {mustrd-0.2.7a0.dist-info → mustrd-0.3.1a0.dist-info}/RECORD +17 -17
- mustrd-0.2.7a0.dist-info/METADATA +0 -96
- {mustrd-0.2.7a0.dist-info → mustrd-0.3.1a0.dist-info}/LICENSE +0 -0
- {mustrd-0.2.7a0.dist-info → mustrd-0.3.1a0.dist-info}/WHEEL +0 -0
- {mustrd-0.2.7a0.dist-info → mustrd-0.3.1a0.dist-info}/entry_points.txt +0 -0
mustrd/mustrdAnzo.py
CHANGED
@@ -29,6 +29,7 @@ from mustrd.anzo_utils import query_azg, query_graphmart
|
|
29
29
|
from mustrd.anzo_utils import query_configuration, json_to_dictlist, ttl_to_graph
|
30
30
|
|
31
31
|
|
32
|
+
|
32
33
|
def execute_select(triple_store: dict, when: str, bindings: dict = None) -> str:
|
33
34
|
try:
|
34
35
|
if bindings:
|
@@ -39,7 +40,7 @@ def execute_select(triple_store: dict, when: str, bindings: dict = None) -> str
|
|
39
40
|
f"FROM <{triple_store['input_graph']}>\nFROM <{triple_store['output_graph']}>").replace(
|
40
41
|
"${targetGraph}", f"<{triple_store['output_graph']}>")
|
41
42
|
# TODO: manage results here
|
42
|
-
return query_azg(anzo_config=triple_store, query=when)
|
43
|
+
return query_azg(anzo_config=triple_store, query=when, data_layers=[triple_store['input_graph']])
|
43
44
|
except (ConnectionError, TimeoutError, HTTPError, ConnectTimeout):
|
44
45
|
raise
|
45
46
|
|
@@ -58,7 +59,7 @@ USING <{triple_store['output_graph']}>""").replace(
|
|
58
59
|
"${targetGraph}", f"<{output_graph}>")
|
59
60
|
|
60
61
|
response = query_azg(anzo_config=triple_store, query=substituted_query, is_update=True,
|
61
|
-
data_layers=input_graph, format="ttl")
|
62
|
+
data_layers=[input_graph, output_graph], format="ttl")
|
62
63
|
logging.debug(f'response {response}')
|
63
64
|
# TODO: deal with error responses
|
64
65
|
new_graph = ttl_to_graph(query_azg(anzo_config=triple_store, query="construct {?s ?p ?o} { ?s ?p ?o }",
|
mustrd/mustrdRdfLib.py
CHANGED
@@ -25,6 +25,7 @@ SOFTWARE.
|
|
25
25
|
from pyparsing import ParseException
|
26
26
|
from rdflib import Graph
|
27
27
|
from requests import RequestException
|
28
|
+
import logging
|
28
29
|
|
29
30
|
|
30
31
|
def execute_select(triple_store: dict, given: Graph, when: str, bindings: dict = None) -> str:
|
@@ -38,7 +39,13 @@ def execute_select(triple_store: dict, given: Graph, when: str, bindings: dict =
|
|
38
39
|
|
39
40
|
def execute_construct(triple_store: dict, given: Graph, when: str, bindings: dict = None) -> Graph:
|
40
41
|
try:
|
41
|
-
|
42
|
+
logger = logging.getLogger(__name__)
|
43
|
+
logger.debug(f"Executing CONSTRUCT query: {when} with bindings: {bindings}")
|
44
|
+
|
45
|
+
|
46
|
+
result_graph = given.query(when, initBindings=bindings).graph
|
47
|
+
logger.debug(f"CONSTRUCT query executed successfully, resulting graph has {len(result_graph)} triples.")
|
48
|
+
return result_graph
|
42
49
|
except ParseException:
|
43
50
|
raise
|
44
51
|
except Exception as e:
|
mustrd/mustrdTestPlugin.py
CHANGED
@@ -1,32 +1,8 @@
|
|
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
1
|
import logging
|
26
2
|
from dataclasses import dataclass
|
27
3
|
import pytest
|
28
4
|
import os
|
29
|
-
from pathlib import Path
|
5
|
+
from pathlib import Path, PosixPath
|
30
6
|
from rdflib.namespace import Namespace
|
31
7
|
from rdflib import Graph, RDF
|
32
8
|
from pytest import Session
|
@@ -34,12 +10,26 @@ from pytest import Session
|
|
34
10
|
from mustrd import logger_setup
|
35
11
|
from mustrd.TestResult import ResultList, TestResult, get_result_list
|
36
12
|
from mustrd.utils import get_mustrd_root
|
37
|
-
from mustrd.mustrd import
|
38
|
-
|
13
|
+
from mustrd.mustrd import (
|
14
|
+
write_result_diff_to_log,
|
15
|
+
get_triple_store_graph,
|
16
|
+
get_triple_stores,
|
17
|
+
)
|
18
|
+
from mustrd.mustrd import (
|
19
|
+
Specification,
|
20
|
+
SpecSkipped,
|
21
|
+
validate_specs,
|
22
|
+
get_specs,
|
23
|
+
SpecPassed,
|
24
|
+
run_spec,
|
25
|
+
)
|
39
26
|
from mustrd.namespace import MUST, TRIPLESTORE, MUSTRDTEST
|
40
27
|
from typing import Union
|
41
28
|
from pyshacl import validate
|
42
|
-
|
29
|
+
|
30
|
+
import pathlib
|
31
|
+
import traceback
|
32
|
+
|
43
33
|
spnamespace = Namespace("https://semanticpartners.com/data/test/")
|
44
34
|
|
45
35
|
mustrd_root = get_mustrd_root()
|
@@ -79,64 +69,96 @@ def pytest_addoption(parser):
|
|
79
69
|
default=None,
|
80
70
|
help="Give the secrets by command line in order to be able to store secrets safely in CI tools",
|
81
71
|
)
|
72
|
+
group.addoption(
|
73
|
+
"--pytest-path",
|
74
|
+
action="store",
|
75
|
+
dest="pytest_path",
|
76
|
+
metavar="PytestPath",
|
77
|
+
default=None,
|
78
|
+
help="Filter tests based on the pytest_path property in .mustrd.ttl files.",
|
79
|
+
)
|
82
80
|
return
|
83
81
|
|
84
82
|
|
85
83
|
def pytest_configure(config) -> None:
|
86
84
|
# Read configuration file
|
87
85
|
if config.getoption("mustrd") and config.getoption("configpath"):
|
88
|
-
config.pluginmanager.register(
|
89
|
-
|
86
|
+
config.pluginmanager.register(
|
87
|
+
MustrdTestPlugin(
|
88
|
+
config.getoption("mdpath"),
|
89
|
+
Path(config.getoption("configpath")),
|
90
|
+
config.getoption("secrets"),
|
91
|
+
)
|
92
|
+
)
|
90
93
|
|
91
94
|
|
92
95
|
def parse_config(config_path):
|
93
96
|
test_configs = []
|
94
|
-
print(f"{config_path=}")
|
95
97
|
config_graph = Graph().parse(config_path)
|
96
98
|
shacl_graph = Graph().parse(
|
97
|
-
Path(os.path.join(mustrd_root, "model/mustrdTestShapes.ttl"))
|
99
|
+
Path(os.path.join(mustrd_root, "model/mustrdTestShapes.ttl"))
|
100
|
+
)
|
98
101
|
ont_graph = Graph().parse(
|
99
|
-
Path(os.path.join(mustrd_root, "model/mustrdTestOntology.ttl"))
|
102
|
+
Path(os.path.join(mustrd_root, "model/mustrdTestOntology.ttl"))
|
103
|
+
)
|
100
104
|
conforms, results_graph, results_text = validate(
|
101
105
|
data_graph=config_graph,
|
102
106
|
shacl_graph=shacl_graph,
|
103
107
|
ont_graph=ont_graph,
|
104
108
|
advanced=True,
|
105
|
-
inference=
|
109
|
+
inference="none",
|
106
110
|
)
|
107
111
|
if not conforms:
|
108
|
-
raise ValueError(
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
+
raise ValueError(
|
113
|
+
f"Mustrd test configuration not conform to the shapes. SHACL report: {results_text}",
|
114
|
+
results_graph,
|
115
|
+
)
|
116
|
+
|
117
|
+
for test_config_subject in config_graph.subjects(
|
118
|
+
predicate=RDF.type, object=MUSTRDTEST.MustrdTest
|
119
|
+
):
|
112
120
|
spec_path = get_config_param(
|
113
|
-
config_graph, test_config_subject, MUSTRDTEST.hasSpecPath, str
|
121
|
+
config_graph, test_config_subject, MUSTRDTEST.hasSpecPath, str
|
122
|
+
)
|
114
123
|
data_path = get_config_param(
|
115
|
-
config_graph, test_config_subject, MUSTRDTEST.hasDataPath, str
|
124
|
+
config_graph, test_config_subject, MUSTRDTEST.hasDataPath, str
|
125
|
+
)
|
116
126
|
triplestore_spec_path = get_config_param(
|
117
|
-
config_graph, test_config_subject, MUSTRDTEST.triplestoreSpecPath, str
|
127
|
+
config_graph, test_config_subject, MUSTRDTEST.triplestoreSpecPath, str
|
128
|
+
)
|
118
129
|
pytest_path = get_config_param(
|
119
|
-
config_graph, test_config_subject, MUSTRDTEST.hasPytestPath, str
|
120
|
-
|
121
|
-
|
130
|
+
config_graph, test_config_subject, MUSTRDTEST.hasPytestPath, str
|
131
|
+
)
|
132
|
+
filter_on_tripleStore = tuple(
|
133
|
+
config_graph.objects(
|
134
|
+
subject=test_config_subject, predicate=MUSTRDTEST.filterOnTripleStore
|
135
|
+
)
|
136
|
+
)
|
122
137
|
|
123
138
|
# Root path is the mustrd test config path
|
124
139
|
root_path = Path(config_path).parent
|
125
140
|
spec_path = root_path / Path(spec_path) if spec_path else None
|
126
141
|
data_path = root_path / Path(data_path) if data_path else None
|
127
|
-
triplestore_spec_path =
|
128
|
-
Path(triplestore_spec_path) if triplestore_spec_path else None
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
142
|
+
triplestore_spec_path = (
|
143
|
+
root_path / Path(triplestore_spec_path) if triplestore_spec_path else None
|
144
|
+
)
|
145
|
+
|
146
|
+
test_configs.append(
|
147
|
+
TestConfig(
|
148
|
+
spec_path=spec_path,
|
149
|
+
data_path=data_path,
|
150
|
+
triplestore_spec_path=triplestore_spec_path,
|
151
|
+
pytest_path=pytest_path,
|
152
|
+
filter_on_tripleStore=filter_on_tripleStore,
|
153
|
+
)
|
154
|
+
)
|
134
155
|
return test_configs
|
135
156
|
|
136
157
|
|
137
158
|
def get_config_param(config_graph, config_subject, config_param, convert_function):
|
138
159
|
raw_value = config_graph.value(
|
139
|
-
subject=config_subject, predicate=config_param, any=True
|
160
|
+
subject=config_subject, predicate=config_param, any=True
|
161
|
+
)
|
140
162
|
return convert_function(raw_value) if raw_value else None
|
141
163
|
|
142
164
|
|
@@ -179,45 +201,95 @@ class MustrdTestPlugin:
|
|
179
201
|
@pytest.hookimpl(tryfirst=True)
|
180
202
|
def pytest_collection(self, session):
|
181
203
|
logger.info("Starting test collection")
|
204
|
+
|
182
205
|
self.unit_tests = []
|
183
206
|
args = session.config.args
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
207
|
+
|
208
|
+
# Split args into mustrd and regular pytest args
|
209
|
+
mustrd_args = [arg for arg in args if ".mustrd.ttl" in arg]
|
210
|
+
pytest_args = [arg for arg in args if arg != os.getcwd() and ".mustrd.ttl" not in arg]
|
211
|
+
|
212
|
+
self.selected_tests = list(
|
213
|
+
map(
|
214
|
+
lambda arg: Path(arg.split("::")[0]),
|
215
|
+
mustrd_args
|
216
|
+
)
|
217
|
+
)
|
218
|
+
logger.info(f"selected_tests is: {self.selected_tests}")
|
219
|
+
|
220
|
+
self.path_filter = session.config.getoption("pytest_path") or None
|
221
|
+
|
222
|
+
logger.info(f"path_filter is: {self.path_filter}")
|
223
|
+
logger.info(f"Args: {args}")
|
224
|
+
logger.info(f"Mustrd Args: {mustrd_args}")
|
225
|
+
logger.info(f"Pytest Args: {pytest_args}")
|
226
|
+
logger.info(f"Path Filter: {self.path_filter}")
|
227
|
+
|
228
|
+
# Only modify args if we have mustrd tests to run
|
229
|
+
if self.selected_tests:
|
230
|
+
# Keep original pytest args and add config file for mustrd
|
231
|
+
session.config.args = pytest_args + [str(self.test_config_file.resolve())]
|
232
|
+
else:
|
233
|
+
# Keep original args unchanged for regular pytest
|
234
|
+
session.config.args = args
|
192
235
|
|
193
|
-
session.config.args
|
236
|
+
logger.info(f"Final session.config.args: {session.config.args}")
|
194
237
|
|
195
238
|
def get_file_name_from_arg(self, arg):
|
196
239
|
if arg and len(arg) > 0 and "[" in arg and ".mustrd.ttl " in arg:
|
197
|
-
return arg[arg.index("[") + 1: arg.index(".mustrd.ttl ")]
|
240
|
+
return arg[arg.index("[") + 1 : arg.index(".mustrd.ttl ")]
|
198
241
|
return None
|
199
242
|
|
200
243
|
@pytest.hookimpl
|
201
244
|
def pytest_collect_file(self, parent, path):
|
202
245
|
logger.debug(f"Collecting file: {path}")
|
203
|
-
|
204
|
-
|
246
|
+
# Only collect .ttl files that are mustrd suite config files
|
247
|
+
if not str(path).endswith('.ttl'):
|
248
|
+
return None
|
249
|
+
if Path(path).resolve() != Path(self.test_config_file).resolve():
|
250
|
+
logger.debug(f"{self.test_config_file}: Skipping non-matching-config file: {path}")
|
251
|
+
return None
|
252
|
+
|
253
|
+
mustrd_file = MustrdFile.from_parent(parent, path=pathlib.Path(path), mustrd_plugin=self)
|
205
254
|
mustrd_file.mustrd_plugin = self
|
206
255
|
return mustrd_file
|
207
256
|
|
208
257
|
# Generate test for each triple store available
|
209
258
|
def generate_tests_for_config(self, config, triple_stores, file_name):
|
210
|
-
|
211
|
-
shacl_graph = Graph().parse(
|
259
|
+
logger.debug(f"generate_tests_for_config {config=} {self=} {dir(self)}")
|
260
|
+
shacl_graph = Graph().parse(
|
261
|
+
Path(os.path.join(mustrd_root, "model/mustrdShapes.ttl"))
|
262
|
+
)
|
212
263
|
ont_graph = Graph().parse(Path(os.path.join(mustrd_root, "model/ontology.ttl")))
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
264
|
+
logger.debug("Generating tests for config: " + str(config))
|
265
|
+
logger.debug(f"selected_tests {self.selected_tests}")
|
266
|
+
|
267
|
+
valid_spec_uris, spec_graph, invalid_spec_results = validate_specs(
|
268
|
+
config,
|
269
|
+
triple_stores,
|
270
|
+
shacl_graph,
|
271
|
+
ont_graph,
|
272
|
+
file_name or "*",
|
273
|
+
selected_test_files=self.selected_tests,
|
274
|
+
)
|
275
|
+
# Convert invalid specs to SpecInvalid instead of SpecSkipped
|
276
|
+
invalid_specs = [
|
277
|
+
SpecInvalid(
|
278
|
+
spec.spec_uri,
|
279
|
+
spec.triple_store,
|
280
|
+
spec.message,
|
281
|
+
spec.spec_file_name,
|
282
|
+
spec.spec_source_file
|
283
|
+
) for spec in invalid_spec_results
|
284
|
+
]
|
285
|
+
|
286
|
+
|
287
|
+
specs, skipped_spec_results = get_specs(
|
288
|
+
valid_spec_uris, spec_graph, triple_stores, config
|
289
|
+
)
|
218
290
|
|
219
291
|
# Return normal specs + skipped results
|
220
|
-
return specs + skipped_spec_results +
|
292
|
+
return specs + skipped_spec_results + invalid_specs
|
221
293
|
|
222
294
|
# Function called to generate the name of the test
|
223
295
|
def get_test_name(self, spec):
|
@@ -225,9 +297,9 @@ class MustrdTestPlugin:
|
|
225
297
|
if isinstance(spec, SpecSkipped):
|
226
298
|
triple_store = spec.triple_store
|
227
299
|
else:
|
228
|
-
triple_store = spec.triple_store[
|
229
|
-
|
230
|
-
|
300
|
+
triple_store = spec.triple_store["type"]
|
301
|
+
|
302
|
+
triple_store_name = triple_store.replace("https://mustrd.com/model/", "")
|
231
303
|
test_name = spec.spec_uri.replace(spnamespace, "").replace("_", " ")
|
232
304
|
return spec.spec_file_name + " : " + triple_store_name + ": " + test_name
|
233
305
|
|
@@ -235,21 +307,33 @@ class MustrdTestPlugin:
|
|
235
307
|
def get_triple_stores_from_file(self, test_config):
|
236
308
|
if test_config.triplestore_spec_path:
|
237
309
|
try:
|
238
|
-
triple_stores = get_triple_stores(
|
239
|
-
|
310
|
+
triple_stores = get_triple_stores(
|
311
|
+
get_triple_store_graph(
|
312
|
+
test_config.triplestore_spec_path, self.secrets
|
313
|
+
)
|
314
|
+
)
|
240
315
|
except Exception as e:
|
241
|
-
print(
|
242
|
-
|
316
|
+
print(
|
317
|
+
f"""Triplestore configuration parsing failed {test_config.triplestore_spec_path}.
|
318
|
+
Only rdflib will be executed""",
|
319
|
+
e,
|
320
|
+
)
|
243
321
|
triple_stores = [
|
244
|
-
{
|
322
|
+
{"type": TRIPLESTORE.RdfLib, "uri": TRIPLESTORE.RdfLib}
|
323
|
+
]
|
245
324
|
else:
|
246
325
|
print("No triple store configuration required: using embedded rdflib")
|
247
|
-
triple_stores = [
|
248
|
-
{'type': TRIPLESTORE.RdfLib, 'uri': TRIPLESTORE.RdfLib}]
|
326
|
+
triple_stores = [{"type": TRIPLESTORE.RdfLib, "uri": TRIPLESTORE.RdfLib}]
|
249
327
|
|
250
328
|
if test_config.filter_on_tripleStore:
|
251
|
-
triple_stores = list(
|
252
|
-
|
329
|
+
triple_stores = list(
|
330
|
+
filter(
|
331
|
+
lambda triple_store: (
|
332
|
+
triple_store["uri"] in test_config.filter_on_tripleStore
|
333
|
+
),
|
334
|
+
triple_stores,
|
335
|
+
)
|
336
|
+
)
|
253
337
|
return triple_stores
|
254
338
|
|
255
339
|
# Hook function. Initialize the list of result in session
|
@@ -264,7 +348,7 @@ class MustrdTestPlugin:
|
|
264
348
|
outcome = yield
|
265
349
|
result = outcome.get_result()
|
266
350
|
|
267
|
-
if result.when ==
|
351
|
+
if result.when == "call":
|
268
352
|
# Add the result of the test to the session
|
269
353
|
item.session.results[item] = result
|
270
354
|
|
@@ -280,8 +364,11 @@ class MustrdTestPlugin:
|
|
280
364
|
if test_conf.originalname != test_conf.name:
|
281
365
|
module_name = test_conf.parent.name
|
282
366
|
class_name = test_conf.originalname
|
283
|
-
test_name =
|
284
|
-
|
367
|
+
test_name = (
|
368
|
+
test_conf.name.replace(class_name, "")
|
369
|
+
.replace("[", "")
|
370
|
+
.replace("]", "")
|
371
|
+
)
|
285
372
|
is_mustrd = True
|
286
373
|
# Case normal unit tests
|
287
374
|
else:
|
@@ -290,64 +377,132 @@ class MustrdTestPlugin:
|
|
290
377
|
test_name = test_conf.originalname
|
291
378
|
is_mustrd = False
|
292
379
|
|
293
|
-
test_results.append(
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
380
|
+
test_results.append(
|
381
|
+
TestResult(
|
382
|
+
test_name, class_name, module_name, result.outcome, is_mustrd
|
383
|
+
)
|
384
|
+
)
|
385
|
+
|
386
|
+
result_list = ResultList(
|
387
|
+
None,
|
388
|
+
get_result_list(
|
389
|
+
test_results,
|
390
|
+
lambda result: result.type,
|
391
|
+
lambda result: is_mustrd and result.test_name.split("@")[1],
|
392
|
+
),
|
393
|
+
False,
|
394
|
+
)
|
300
395
|
|
301
396
|
md = result_list.render()
|
302
|
-
with open(self.md_path,
|
397
|
+
with open(self.md_path, "w") as file:
|
303
398
|
file.write(md)
|
304
399
|
|
400
|
+
@dataclass(frozen=True)
|
401
|
+
class SpecInvalid:
|
402
|
+
spec_uri: str
|
403
|
+
triple_store: str
|
404
|
+
message: str
|
405
|
+
spec_file_name: str = None
|
406
|
+
spec_source_file: Path = None
|
305
407
|
|
306
408
|
class MustrdFile(pytest.File):
|
307
409
|
mustrd_plugin: MustrdTestPlugin
|
308
410
|
|
411
|
+
def __init__(self, *args, mustrd_plugin, **kwargs):
|
412
|
+
logger.debug(f"Creating MustrdFile with args: {args}, kwargs: {kwargs}")
|
413
|
+
self.mustrd_plugin = mustrd_plugin
|
414
|
+
super(pytest.File, self).__init__(*args, **kwargs)
|
415
|
+
|
309
416
|
def collect(self):
|
310
417
|
try:
|
311
|
-
logger.
|
312
|
-
|
418
|
+
logger.info(f"{self.mustrd_plugin.test_config_file}: Collecting tests from file: {self.path=}")
|
419
|
+
# Only process the specific mustrd config file we were given
|
420
|
+
|
421
|
+
# if not str(self.fspath).endswith(".ttl"):
|
422
|
+
# return []
|
423
|
+
# Only process the specific mustrd config file we were given
|
424
|
+
# if str(self.fspath) != str(self.mustrd_plugin.test_config_file):
|
425
|
+
# logger.info(f"Skipping non-config file: {self.fspath}")
|
426
|
+
# return []
|
427
|
+
|
428
|
+
test_configs = parse_config(self.path)
|
429
|
+
from collections import defaultdict
|
430
|
+
pytest_path_grouped = defaultdict(list)
|
313
431
|
for test_config in test_configs:
|
314
|
-
|
315
|
-
|
432
|
+
if (
|
433
|
+
self.mustrd_plugin.path_filter is not None
|
434
|
+
and not str(test_config.pytest_path).startswith(str(self.mustrd_plugin.path_filter))
|
435
|
+
):
|
436
|
+
logger.info(f"Skipping test config due to path filter: {test_config.pytest_path=} {self.mustrd_plugin.path_filter=}")
|
316
437
|
continue
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
438
|
+
|
439
|
+
triple_stores = self.mustrd_plugin.get_triple_stores_from_file(test_config)
|
440
|
+
try:
|
441
|
+
specs = self.mustrd_plugin.generate_tests_for_config(
|
442
|
+
{
|
443
|
+
"spec_path": test_config.spec_path,
|
444
|
+
"data_path": test_config.data_path,
|
445
|
+
},
|
446
|
+
triple_stores,
|
447
|
+
None,
|
448
|
+
)
|
449
|
+
except Exception as e:
|
450
|
+
logger.error(f"Error generating tests: {e}\n{traceback.format_exc()}")
|
451
|
+
specs = [
|
452
|
+
SpecInvalid(
|
453
|
+
MUST.TestSpec,
|
454
|
+
triple_store["uri"] if isinstance(triple_store, dict) else triple_store,
|
455
|
+
f"Test generation failed: {str(e)}",
|
456
|
+
spec_file_name=str(test_config.spec_path.name) if test_config.spec_path else "unknown.mustrd.ttl",
|
457
|
+
spec_source_file=self.path if test_config.spec_path else Path("unknown.mustrd.ttl"),
|
458
|
+
)
|
459
|
+
for triple_store in (triple_stores or test_config.filter_on_tripleStore)
|
460
|
+
]
|
461
|
+
pytest_path = getattr(test_config, "pytest_path", "unknown")
|
330
462
|
for spec in specs:
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
463
|
+
pytest_path_grouped[pytest_path].append(spec)
|
464
|
+
|
465
|
+
for pytest_path, specs_for_path in pytest_path_grouped.items():
|
466
|
+
logger.info(f"pytest_path group: {pytest_path} ({len(specs_for_path)} specs)")
|
467
|
+
|
468
|
+
yield MustrdPytestPathCollector.from_parent(
|
469
|
+
self,
|
470
|
+
name=str(pytest_path),
|
471
|
+
pytest_path=pytest_path,
|
472
|
+
specs=specs_for_path,
|
473
|
+
mustrd_plugin=self.mustrd_plugin,
|
474
|
+
)
|
475
|
+
except Exception as e:
|
340
476
|
self.mustrd_plugin.collect_error = e
|
341
|
-
logger.error(f"Error during collection: {e}")
|
477
|
+
logger.error(f"Error during collection {self.path}: {type(e)} {e} {traceback.format_exc()}")
|
342
478
|
raise e
|
343
479
|
|
344
480
|
|
481
|
+
class MustrdPytestPathCollector(pytest.Class):
|
482
|
+
def __init__(self, name, parent, pytest_path, specs, mustrd_plugin):
|
483
|
+
super().__init__(name, parent)
|
484
|
+
self.pytest_path = pytest_path
|
485
|
+
self.specs = specs
|
486
|
+
self.mustrd_plugin = mustrd_plugin
|
487
|
+
|
488
|
+
def collect(self):
|
489
|
+
for spec in self.specs:
|
490
|
+
item = MustrdItem.from_parent(
|
491
|
+
self,
|
492
|
+
name=spec.spec_file_name,
|
493
|
+
spec=spec,
|
494
|
+
)
|
495
|
+
self.mustrd_plugin.items.append(item)
|
496
|
+
yield item
|
497
|
+
|
498
|
+
|
345
499
|
class MustrdItem(pytest.Item):
|
346
500
|
def __init__(self, name, parent, spec):
|
347
|
-
logging.
|
501
|
+
logging.debug(f"Creating item: {name}")
|
348
502
|
super().__init__(name, parent)
|
349
503
|
self.spec = spec
|
350
504
|
self.fspath = spec.spec_source_file
|
505
|
+
self.originalname = name
|
351
506
|
|
352
507
|
def runtest(self):
|
353
508
|
result = run_test_spec(self.spec)
|
@@ -355,11 +510,20 @@ class MustrdItem(pytest.Item):
|
|
355
510
|
raise AssertionError(f"Test {self.name} failed")
|
356
511
|
|
357
512
|
def repr_failure(self, excinfo):
|
358
|
-
|
359
|
-
|
513
|
+
# excinfo.value is the exception instance
|
514
|
+
# You can add more context here
|
515
|
+
tb_lines = traceback.format_exception(excinfo.type, excinfo.value, excinfo.tb)
|
516
|
+
tb_str = "".join(tb_lines)
|
517
|
+
return (
|
518
|
+
f"{self.name} failed:\n"
|
519
|
+
f"Spec: {self.spec.spec_uri}\n"
|
520
|
+
f"File: {self.spec.spec_source_file}\n"
|
521
|
+
f"Error: \n{excinfo.value}\n"
|
522
|
+
f"Traceback:\n{tb_str}"
|
523
|
+
)
|
524
|
+
|
360
525
|
def reportinfo(self):
|
361
526
|
r = "", 0, f"mustrd test: {self.name}"
|
362
|
-
logger.debug(f"Reporting info for {self.name} {r=}")
|
363
527
|
return r
|
364
528
|
|
365
529
|
|
@@ -368,11 +532,18 @@ def run_test_spec(test_spec):
|
|
368
532
|
if isinstance(test_spec, SpecSkipped):
|
369
533
|
pytest.skip(f"Invalid configuration, error : {test_spec.message}")
|
370
534
|
result = run_spec(test_spec)
|
371
|
-
|
372
535
|
result_type = type(result)
|
536
|
+
if isinstance(test_spec, SpecInvalid):
|
537
|
+
raise ValueError(f"Invalid test specification: {test_spec.message} {test_spec}")
|
373
538
|
if result_type == SpecSkipped:
|
374
539
|
# FIXME: Better exception management
|
375
540
|
pytest.skip("Unsupported configuration")
|
376
541
|
if result_type != SpecPassed:
|
377
|
-
write_result_diff_to_log(result)
|
542
|
+
write_result_diff_to_log(result, logger.info)
|
543
|
+
log_lines = []
|
544
|
+
def log_to_string(message):
|
545
|
+
log_lines.append(message)
|
546
|
+
write_result_diff_to_log(result, log_to_string)
|
547
|
+
raise AssertionError("Test failed: " + "\n".join(log_lines))
|
548
|
+
|
378
549
|
return result_type == SpecPassed
|
mustrd/namespace.py
CHANGED
@@ -38,13 +38,15 @@ class MUST(DefinedNamespace):
|
|
38
38
|
AnzoQueryDrivenUpdateSparql: URIRef
|
39
39
|
AskSparql: URIRef
|
40
40
|
DescribeSparql: URIRef
|
41
|
-
|
41
|
+
SpadeEdnGroupSource: URIRef
|
42
|
+
|
42
43
|
# Specification properties
|
43
44
|
given: URIRef
|
44
45
|
when: URIRef
|
45
46
|
then: URIRef
|
46
47
|
dataSource: URIRef
|
47
48
|
file: URIRef
|
49
|
+
fileurl: URIRef
|
48
50
|
fileName: URIRef
|
49
51
|
queryFolder: URIRef
|
50
52
|
queryName: URIRef
|
@@ -124,3 +126,10 @@ class MUSTRDTEST(DefinedNamespace):
|
|
124
126
|
triplestoreSpecPath: URIRef
|
125
127
|
hasPytestPath: URIRef
|
126
128
|
filterOnTripleStore: URIRef
|
129
|
+
|
130
|
+
from rdflib import Namespace
|
131
|
+
|
132
|
+
MUST = Namespace("https://mustrd.com/model/")
|
133
|
+
|
134
|
+
# Add SpadeEdnGroupSource to the namespace
|
135
|
+
MUST.SpadeEdnGroupSource = MUST["SpadeEdnGroupSource"]
|