mustrd 0.3.3a1__py3-none-any.whl → 0.3.4a1__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/model/mustrdShapes.ttl +1 -0
- mustrd/mustrd.py +28 -48
- mustrd/mustrdAnzo.py +15 -6
- mustrd/mustrdTestPlugin.py +22 -70
- mustrd/spec_component.py +1 -3
- {mustrd-0.3.3a1.dist-info → mustrd-0.3.4a1.dist-info}/METADATA +79 -16
- {mustrd-0.3.3a1.dist-info → mustrd-0.3.4a1.dist-info}/RECORD +10 -11
- {mustrd-0.3.3a1.dist-info → mustrd-0.3.4a1.dist-info}/WHEEL +1 -1
- mustrd/shared_utils.py +0 -0
- {mustrd-0.3.3a1.dist-info → mustrd-0.3.4a1.dist-info}/LICENSE +0 -0
- {mustrd-0.3.3a1.dist-info → mustrd-0.3.4a1.dist-info}/entry_points.txt +0 -0
mustrd/model/mustrdShapes.ttl
CHANGED
mustrd/mustrd.py
CHANGED
@@ -134,7 +134,7 @@ class TripleStoreConnectionError(SpecResult):
|
|
134
134
|
|
135
135
|
|
136
136
|
@dataclass
|
137
|
-
class
|
137
|
+
class SpecInvalid(SpecResult):
|
138
138
|
message: str
|
139
139
|
spec_file_name: str = "default.mustrd.ttl"
|
140
140
|
spec_source_file: Path = Path("default.mustrd.ttl")
|
@@ -204,6 +204,12 @@ def validate_specs(
|
|
204
204
|
f"Could not extract spec from {file} due to exception of type "
|
205
205
|
f"{type(e).__name__} when parsing file"
|
206
206
|
]
|
207
|
+
invalid_specs += [
|
208
|
+
SpecInvalid(
|
209
|
+
"urn:invalid_spec_file", triple_store["type"], message, file.name, file
|
210
|
+
)
|
211
|
+
for triple_store in triple_stores
|
212
|
+
]
|
207
213
|
continue
|
208
214
|
|
209
215
|
# run shacl validation
|
@@ -310,7 +316,7 @@ def add_spec_validation(
|
|
310
316
|
error_messages.sort()
|
311
317
|
error_message = "\n".join(msg for msg in error_messages)
|
312
318
|
invalid_specs += [
|
313
|
-
|
319
|
+
SpecInvalid(
|
314
320
|
subject_uri, triple_store["type"], error_message, file.name, file
|
315
321
|
)
|
316
322
|
for triple_store in triple_stores
|
@@ -324,15 +330,15 @@ def get_specs(
|
|
324
330
|
run_config: dict,
|
325
331
|
):
|
326
332
|
specs = []
|
327
|
-
|
333
|
+
invalid_spec = []
|
328
334
|
try:
|
329
335
|
for triple_store in triple_stores:
|
330
336
|
if "error" in triple_store:
|
331
337
|
log.error(
|
332
338
|
f"{triple_store['error']}. No specs run for this triple store."
|
333
339
|
)
|
334
|
-
|
335
|
-
|
340
|
+
invalid_spec += [
|
341
|
+
SpecInvalid(
|
336
342
|
spec_uri,
|
337
343
|
triple_store["type"],
|
338
344
|
triple_store["error"],
|
@@ -360,8 +366,8 @@ def get_specs(
|
|
360
366
|
)
|
361
367
|
or "unknown"
|
362
368
|
)
|
363
|
-
|
364
|
-
|
369
|
+
invalid_spec += [
|
370
|
+
SpecInvalid(
|
365
371
|
spec_uri,
|
366
372
|
triple_store["type"],
|
367
373
|
str(e),
|
@@ -380,7 +386,7 @@ def get_specs(
|
|
380
386
|
log.error("No specifications will be run.")
|
381
387
|
|
382
388
|
log.info(f"Extracted {len(specs)} specifications that will be run")
|
383
|
-
return specs,
|
389
|
+
return specs, invalid_spec
|
384
390
|
|
385
391
|
|
386
392
|
def run_specs(specs) -> List[SpecResult]:
|
@@ -505,7 +511,7 @@ def run_spec(spec: Specification) -> SpecResult:
|
|
505
511
|
upload_given(triple_store, spec.given)
|
506
512
|
else:
|
507
513
|
if triple_store["type"] == TRIPLESTORE.RdfLib:
|
508
|
-
return
|
514
|
+
return SpecInvalid(
|
509
515
|
spec_uri,
|
510
516
|
triple_store["type"],
|
511
517
|
"Unable to run Inherited State tests on Rdflib",
|
@@ -526,7 +532,6 @@ def run_spec(spec: Specification) -> SpecResult:
|
|
526
532
|
except NotImplementedError as ex:
|
527
533
|
log.error(f"NotImplementedError {ex}")
|
528
534
|
raise ex
|
529
|
-
# return SpecSkipped(spec_uri, triple_store["type"], ex.args[0])
|
530
535
|
return check_result(spec, result)
|
531
536
|
except (ConnectionError, TimeoutError, HTTPError, ConnectTimeout, OSError) as e:
|
532
537
|
# close_connection = False
|
@@ -548,38 +553,13 @@ def run_spec(spec: Specification) -> SpecResult:
|
|
548
553
|
|
549
554
|
|
550
555
|
def get_triple_store_graph(triple_store_graph_path: Path, secrets: str):
|
551
|
-
graph = Graph()
|
552
|
-
# Parse the main triple store graph file
|
553
|
-
try:
|
554
|
-
graph.parse(triple_store_graph_path)
|
555
|
-
except Exception as e:
|
556
|
-
log.error(f"Failed to parse triple store graph file '{triple_store_graph_path}': {e}")
|
557
|
-
raise
|
558
|
-
|
559
|
-
# Parse secrets, either from string or from file
|
560
556
|
if secrets:
|
561
|
-
|
562
|
-
log.info("" + secrets)
|
563
|
-
try:
|
564
|
-
graph.parse(data=secrets)
|
565
|
-
except Exception as e:
|
566
|
-
log.error(f"Failed to parse secrets data for triple store graph: {e}")
|
567
|
-
raise
|
557
|
+
return Graph().parse(triple_store_graph_path).parse(data=secrets)
|
568
558
|
else:
|
569
|
-
secret_path = triple_store_graph_path.
|
559
|
+
secret_path = triple_store_graph_path.parent / Path(
|
570
560
|
triple_store_graph_path.stem + "_secrets" + triple_store_graph_path.suffix
|
571
561
|
)
|
572
|
-
|
573
|
-
if secret_path.exists():
|
574
|
-
try:
|
575
|
-
graph.parse(secret_path)
|
576
|
-
except Exception as e:
|
577
|
-
log.error(f"Failed to parse secrets file '{secret_path}': {e}")
|
578
|
-
raise
|
579
|
-
else:
|
580
|
-
log.info(f"No secrets file found at '{secret_path}', continuing without it.")
|
581
|
-
|
582
|
-
return graph
|
562
|
+
return Graph().parse(triple_store_graph_path).parse(secret_path)
|
583
563
|
|
584
564
|
|
585
565
|
# Parse and validate triple store configuration
|
@@ -973,8 +953,8 @@ def write_result_diff_to_log(res, info):
|
|
973
953
|
):
|
974
954
|
info(f"{Fore.RED}Failed {res.spec_uri} {res.triple_store}")
|
975
955
|
info(res.exception)
|
976
|
-
if isinstance(res,
|
977
|
-
info(f"{Fore.
|
956
|
+
if isinstance(res, SpecInvalid):
|
957
|
+
info(f"{Fore.RED} Invalid {res.spec_uri} {res.triple_store}")
|
978
958
|
info(res.message)
|
979
959
|
|
980
960
|
|
@@ -1071,7 +1051,7 @@ def review_results(results: List[SpecResult], verbose: bool) -> None:
|
|
1071
1051
|
colours = {
|
1072
1052
|
SpecPassed: Fore.GREEN,
|
1073
1053
|
SpecPassedWithWarning: Fore.YELLOW,
|
1074
|
-
|
1054
|
+
SpecInvalid: Fore.RED,
|
1075
1055
|
}
|
1076
1056
|
# Populate dictionaries from results
|
1077
1057
|
for result in results:
|
@@ -1128,12 +1108,12 @@ def review_results(results: List[SpecResult], verbose: bool) -> None:
|
|
1128
1108
|
|
1129
1109
|
pass_count = statuses.count(SpecPassed)
|
1130
1110
|
warning_count = statuses.count(SpecPassedWithWarning)
|
1131
|
-
|
1111
|
+
invalid_count = statuses.count(SpecInvalid)
|
1132
1112
|
fail_count = len(
|
1133
1113
|
list(
|
1134
1114
|
filter(
|
1135
1115
|
lambda status: status
|
1136
|
-
not in [SpecPassed, SpecPassedWithWarning,
|
1116
|
+
not in [SpecPassed, SpecPassedWithWarning, SpecInvalid],
|
1137
1117
|
statuses,
|
1138
1118
|
)
|
1139
1119
|
)
|
@@ -1141,18 +1121,18 @@ def review_results(results: List[SpecResult], verbose: bool) -> None:
|
|
1141
1121
|
|
1142
1122
|
if fail_count:
|
1143
1123
|
overview_colour = Fore.RED
|
1144
|
-
elif warning_count or
|
1124
|
+
elif warning_count or invalid_count:
|
1145
1125
|
overview_colour = Fore.YELLOW
|
1146
1126
|
else:
|
1147
1127
|
overview_colour = Fore.GREEN
|
1148
1128
|
|
1149
1129
|
logger_setup.flush()
|
1150
1130
|
log.info(
|
1151
|
-
f"{overview_colour}===== {fail_count} failures, {
|
1131
|
+
f"{overview_colour}===== {fail_count} failures, {invalid_count} invalid, {Fore.GREEN}{pass_count} passed, "
|
1152
1132
|
f"{overview_colour}{warning_count} passed with warnings ====="
|
1153
1133
|
)
|
1154
1134
|
|
1155
|
-
if verbose and (fail_count or warning_count or
|
1135
|
+
if verbose and (fail_count or warning_count or invalid_count):
|
1156
1136
|
display_verbose(results)
|
1157
1137
|
|
1158
1138
|
|
@@ -1190,8 +1170,8 @@ def display_verbose(results: List[SpecResult]):
|
|
1190
1170
|
):
|
1191
1171
|
log.info(f"{Fore.RED}Failed {res.spec_uri} {res.triple_store}")
|
1192
1172
|
log.info(res.exception)
|
1193
|
-
if isinstance(res,
|
1194
|
-
log.info(f"{Fore.YELLOW}
|
1173
|
+
if isinstance(res, SpecInvalid):
|
1174
|
+
log.info(f"{Fore.YELLOW}Invalid {res.spec_uri} {res.triple_store}")
|
1195
1175
|
log.info(res.message)
|
1196
1176
|
|
1197
1177
|
|
mustrd/mustrdAnzo.py
CHANGED
@@ -98,8 +98,11 @@ def get_query_from_step(triple_store: dict, query_step_uri: URIRef) -> str:
|
|
98
98
|
?stepUri a <http://cambridgesemantics.com/ontologies/Graphmarts#Step>;
|
99
99
|
<http://cambridgesemantics.com/ontologies/Graphmarts#transformQuery> ?query
|
100
100
|
}}"""
|
101
|
-
|
102
|
-
|
101
|
+
result = json_to_dictlist(query_configuration(anzo_config=triple_store, query=query))
|
102
|
+
if len(result) == 0:
|
103
|
+
raise FileNotFoundError(
|
104
|
+
f"Querynot found for step {query_step_uri}")
|
105
|
+
return result[0].get("query")
|
103
106
|
|
104
107
|
def get_queries_from_templated_step(triple_store: dict, query_step_uri: URIRef) -> dict:
|
105
108
|
query = f"""SELECT ?param_query ?query_template WHERE {{
|
@@ -109,8 +112,11 @@ def get_queries_from_templated_step(triple_store: dict, query_step_uri: URIRef)
|
|
109
112
|
<http://cambridgesemantics.com/ontologies/Graphmarts#template> ?query_template .
|
110
113
|
}}
|
111
114
|
"""
|
112
|
-
|
113
|
-
|
115
|
+
result = json_to_dictlist(query_configuration(anzo_config=triple_store, query=query))
|
116
|
+
if len(result) == 0:
|
117
|
+
raise FileNotFoundError(
|
118
|
+
f"Templated query not found for {query_step_uri}")
|
119
|
+
return result[0]
|
114
120
|
|
115
121
|
def get_queries_for_layer(triple_store: dict, graphmart_layer_uri: URIRef):
|
116
122
|
query = f"""PREFIX graphmarts: <http://cambridgesemantics.com/ontologies/Graphmarts#>
|
@@ -129,8 +135,11 @@ SELECT ?query ?param_query ?query_template
|
|
129
135
|
. }}
|
130
136
|
}}
|
131
137
|
ORDER BY ?index"""
|
132
|
-
|
133
|
-
|
138
|
+
result = json_to_dictlist(query_configuration(anzo_config=triple_store, query=query))
|
139
|
+
if len(result) == 0:
|
140
|
+
raise FileNotFoundError(
|
141
|
+
f"Queries not found for graphmart layer {graphmart_layer_uri}")
|
142
|
+
return result
|
134
143
|
|
135
144
|
def upload_given(triple_store: dict, given: Graph):
|
136
145
|
logging.debug(f"upload_given {triple_store} {given}")
|
mustrd/mustrdTestPlugin.py
CHANGED
@@ -2,7 +2,7 @@ import logging
|
|
2
2
|
from dataclasses import dataclass
|
3
3
|
import pytest
|
4
4
|
import os
|
5
|
-
from pathlib import Path
|
5
|
+
from pathlib import Path
|
6
6
|
from rdflib.namespace import Namespace
|
7
7
|
from rdflib import Graph, RDF
|
8
8
|
from pytest import Session
|
@@ -11,20 +11,16 @@ from mustrd import logger_setup
|
|
11
11
|
from mustrd.TestResult import ResultList, TestResult, get_result_list
|
12
12
|
from mustrd.utils import get_mustrd_root
|
13
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
14
|
validate_specs,
|
22
15
|
get_specs,
|
23
16
|
SpecPassed,
|
24
17
|
run_spec,
|
18
|
+
write_result_diff_to_log,
|
19
|
+
get_triple_store_graph,
|
20
|
+
get_triple_stores,
|
21
|
+
SpecInvalid
|
25
22
|
)
|
26
23
|
from mustrd.namespace import MUST, TRIPLESTORE, MUSTRDTEST
|
27
|
-
from typing import Union
|
28
24
|
from pyshacl import validate
|
29
25
|
|
30
26
|
import pathlib
|
@@ -171,13 +167,6 @@ class TestConfig:
|
|
171
167
|
filter_on_tripleStore: str = None
|
172
168
|
|
173
169
|
|
174
|
-
@dataclass(frozen=True)
|
175
|
-
class TestParamWrapper:
|
176
|
-
id: str
|
177
|
-
test_config: TestConfig
|
178
|
-
unit_test: Union[Specification, SpecSkipped]
|
179
|
-
|
180
|
-
|
181
170
|
# Configure logging
|
182
171
|
logger = logger_setup.setup_logger(__name__)
|
183
172
|
|
@@ -187,7 +176,6 @@ class MustrdTestPlugin:
|
|
187
176
|
test_config_file: Path
|
188
177
|
selected_tests: list
|
189
178
|
secrets: str
|
190
|
-
unit_tests: Union[Specification, SpecSkipped]
|
191
179
|
items: list
|
192
180
|
path_filter: str
|
193
181
|
collect_error: BaseException
|
@@ -201,18 +189,17 @@ class MustrdTestPlugin:
|
|
201
189
|
@pytest.hookimpl(tryfirst=True)
|
202
190
|
def pytest_collection(self, session):
|
203
191
|
logger.info("Starting test collection")
|
204
|
-
|
205
|
-
self.unit_tests = []
|
192
|
+
|
206
193
|
args = session.config.args
|
207
|
-
|
194
|
+
|
208
195
|
# Split args into mustrd and regular pytest args
|
209
196
|
mustrd_args = [arg for arg in args if ".mustrd.ttl" in arg]
|
210
197
|
pytest_args = [arg for arg in args if arg != os.getcwd() and ".mustrd.ttl" not in arg]
|
211
|
-
|
198
|
+
|
212
199
|
self.selected_tests = list(
|
213
200
|
map(
|
214
201
|
lambda arg: Path(arg.split("::")[0]),
|
215
|
-
mustrd_args
|
202
|
+
mustrd_args
|
216
203
|
)
|
217
204
|
)
|
218
205
|
logger.info(f"selected_tests is: {self.selected_tests}")
|
@@ -237,7 +224,7 @@ class MustrdTestPlugin:
|
|
237
224
|
|
238
225
|
def get_file_name_from_arg(self, arg):
|
239
226
|
if arg and len(arg) > 0 and "[" in arg and ".mustrd.ttl " in arg:
|
240
|
-
return arg[arg.index("[") + 1
|
227
|
+
return arg[arg.index("[") + 1: arg.index(".mustrd.ttl ")]
|
241
228
|
return None
|
242
229
|
|
243
230
|
@pytest.hookimpl
|
@@ -247,9 +234,9 @@ class MustrdTestPlugin:
|
|
247
234
|
if not str(path).endswith('.ttl'):
|
248
235
|
return None
|
249
236
|
if Path(path).resolve() != Path(self.test_config_file).resolve():
|
250
|
-
|
251
|
-
|
252
|
-
|
237
|
+
logger.debug(f"{self.test_config_file}: Skipping non-matching-config file: {path}")
|
238
|
+
return None
|
239
|
+
|
253
240
|
mustrd_file = MustrdFile.from_parent(parent, path=pathlib.Path(path), mustrd_plugin=self)
|
254
241
|
mustrd_file.mustrd_plugin = self
|
255
242
|
return mustrd_file
|
@@ -264,7 +251,7 @@ class MustrdTestPlugin:
|
|
264
251
|
logger.debug("Generating tests for config: " + str(config))
|
265
252
|
logger.debug(f"selected_tests {self.selected_tests}")
|
266
253
|
|
267
|
-
valid_spec_uris, spec_graph,
|
254
|
+
valid_spec_uris, spec_graph, invalid_specs = validate_specs(
|
268
255
|
config,
|
269
256
|
triple_stores,
|
270
257
|
shacl_graph,
|
@@ -272,17 +259,6 @@ class MustrdTestPlugin:
|
|
272
259
|
file_name or "*",
|
273
260
|
selected_test_files=self.selected_tests,
|
274
261
|
)
|
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
262
|
|
287
263
|
specs, skipped_spec_results = get_specs(
|
288
264
|
valid_spec_uris, spec_graph, triple_stores, config
|
@@ -291,18 +267,6 @@ class MustrdTestPlugin:
|
|
291
267
|
# Return normal specs + skipped results
|
292
268
|
return specs + skipped_spec_results + invalid_specs
|
293
269
|
|
294
|
-
# Function called to generate the name of the test
|
295
|
-
def get_test_name(self, spec):
|
296
|
-
# FIXME: SpecSkipped should have the same structure?
|
297
|
-
if isinstance(spec, SpecSkipped):
|
298
|
-
triple_store = spec.triple_store
|
299
|
-
else:
|
300
|
-
triple_store = spec.triple_store["type"]
|
301
|
-
|
302
|
-
triple_store_name = triple_store.replace("https://mustrd.com/model/", "")
|
303
|
-
test_name = spec.spec_uri.replace(spnamespace, "").replace("_", " ")
|
304
|
-
return spec.spec_file_name + " : " + triple_store_name + ": " + test_name
|
305
|
-
|
306
270
|
# Get triple store configuration or default
|
307
271
|
def get_triple_stores_from_file(self, test_config):
|
308
272
|
if test_config.triplestore_spec_path:
|
@@ -397,13 +361,6 @@ class MustrdTestPlugin:
|
|
397
361
|
with open(self.md_path, "w") as file:
|
398
362
|
file.write(md)
|
399
363
|
|
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
|
407
364
|
|
408
365
|
class MustrdFile(pytest.File):
|
409
366
|
mustrd_plugin: MustrdTestPlugin
|
@@ -417,14 +374,14 @@ class MustrdFile(pytest.File):
|
|
417
374
|
try:
|
418
375
|
logger.info(f"{self.mustrd_plugin.test_config_file}: Collecting tests from file: {self.path=}")
|
419
376
|
# Only process the specific mustrd config file we were given
|
420
|
-
|
377
|
+
|
421
378
|
# if not str(self.fspath).endswith(".ttl"):
|
422
379
|
# return []
|
423
380
|
# Only process the specific mustrd config file we were given
|
424
381
|
# if str(self.fspath) != str(self.mustrd_plugin.test_config_file):
|
425
382
|
# logger.info(f"Skipping non-config file: {self.fspath}")
|
426
383
|
# return []
|
427
|
-
|
384
|
+
|
428
385
|
test_configs = parse_config(self.path)
|
429
386
|
from collections import defaultdict
|
430
387
|
pytest_path_grouped = defaultdict(list)
|
@@ -435,7 +392,7 @@ class MustrdFile(pytest.File):
|
|
435
392
|
):
|
436
393
|
logger.info(f"Skipping test config due to path filter: {test_config.pytest_path=} {self.mustrd_plugin.path_filter=}")
|
437
394
|
continue
|
438
|
-
|
395
|
+
|
439
396
|
triple_stores = self.mustrd_plugin.get_triple_stores_from_file(test_config)
|
440
397
|
try:
|
441
398
|
specs = self.mustrd_plugin.generate_tests_for_config(
|
@@ -521,7 +478,7 @@ class MustrdItem(pytest.Item):
|
|
521
478
|
f"Error: \n{excinfo.value}\n"
|
522
479
|
f"Traceback:\n{tb_str}"
|
523
480
|
)
|
524
|
-
|
481
|
+
|
525
482
|
def reportinfo(self):
|
526
483
|
r = "", 0, f"mustrd test: {self.name}"
|
527
484
|
return r
|
@@ -531,9 +488,6 @@ class MustrdItem(pytest.Item):
|
|
531
488
|
def run_test_spec(test_spec):
|
532
489
|
logger = logging.getLogger("mustrd.test")
|
533
490
|
logger.info(f"Running test spec: {getattr(test_spec, 'spec_uri', test_spec)}")
|
534
|
-
if isinstance(test_spec, SpecSkipped):
|
535
|
-
logger.warning(f"Test skipped: {test_spec.message}")
|
536
|
-
pytest.skip(f"Invalid configuration, error : {test_spec.message}")
|
537
491
|
try:
|
538
492
|
result = run_spec(test_spec)
|
539
493
|
logger.info(f"Result type: {type(result)} for spec: {getattr(test_spec, 'spec_uri', test_spec)}")
|
@@ -544,13 +498,11 @@ def run_test_spec(test_spec):
|
|
544
498
|
|
545
499
|
if isinstance(test_spec, SpecInvalid):
|
546
500
|
logger.error(f"Invalid test specification: {test_spec.message} {test_spec}")
|
547
|
-
|
548
|
-
if
|
549
|
-
logger.warning("Test skipped due to unsupported configuration")
|
550
|
-
pytest.skip("Unsupported configuration")
|
551
|
-
if type(result) != SpecPassed:
|
501
|
+
pytest.fail(f"Invalid test specification: {test_spec.message} {test_spec}")
|
502
|
+
if not isinstance(result, SpecPassed):
|
552
503
|
write_result_diff_to_log(result, logger.info)
|
553
504
|
log_lines = []
|
505
|
+
|
554
506
|
def log_to_string(message):
|
555
507
|
log_lines.append(message)
|
556
508
|
try:
|
@@ -562,4 +514,4 @@ def run_test_spec(test_spec):
|
|
562
514
|
raise AssertionError("Test failed: " + "\n".join(log_lines))
|
563
515
|
|
564
516
|
logger.info(f"Test PASSED: {getattr(test_spec, 'spec_uri', test_spec)}")
|
565
|
-
return
|
517
|
+
return isinstance(result, SpecPassed)
|
mustrd/spec_component.py
CHANGED
@@ -179,8 +179,6 @@ def get_file_absolute_path(spec_component_details: SpecComponentDetails, relativ
|
|
179
179
|
|
180
180
|
|
181
181
|
def get_spec_component_type(spec_components: List[SpecComponent]) -> Type[SpecComponent]:
|
182
|
-
if not spec_components:
|
183
|
-
raise ValueError("spec_components list is empty")
|
184
182
|
# Get the type of the first object in the list
|
185
183
|
spec_type = type(spec_components[0])
|
186
184
|
# Loop through the remaining objects in the list and check their types
|
@@ -677,7 +675,7 @@ def get_spec_component_from_file(path: Path) -> str:
|
|
677
675
|
raise ValueError(f"Path {path} is a directory, expected a file")
|
678
676
|
|
679
677
|
try:
|
680
|
-
content = path.read_text()
|
678
|
+
content = path.read_text(encoding='utf-8')
|
681
679
|
except FileNotFoundError:
|
682
680
|
raise
|
683
681
|
return str(content)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: mustrd
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.4a1
|
4
4
|
Summary: A Spec By Example framework for RDF and SPARQL, Inspired by Cucumber.
|
5
5
|
License: MIT
|
6
6
|
Author: John Placek
|
@@ -34,13 +34,13 @@ Requires-Dist: urllib3 (==1.26.19)
|
|
34
34
|
Project-URL: Repository, https://github.com/Semantic-partners/mustrd
|
35
35
|
Description-Content-Type: text/markdown
|
36
36
|
|
37
|
-
#
|
37
|
+
# MustRD
|
38
38
|
|
39
39
|
**"MustRD: Validate your SPARQL queries and transformations with precision and confidence, using BDD and Given-When-Then principles."**
|
40
40
|
|
41
|
-
[
|
41
|
+
[](https://github.com/Semantic-partners/mustrd/tree/python-coverage-comment-action-data)
|
42
42
|
|
43
|
-
|
43
|
+
## Why?
|
44
44
|
|
45
45
|
SPARQL is a powerful query language for RDF data, but how can you ensure your queries and transformations are doing what you intend? Whether you're working on a pipeline or a standalone query, certainty is key.
|
46
46
|
|
@@ -48,11 +48,11 @@ While RDF and SPARQL offer great flexibility, we noticed a gap in tooling to val
|
|
48
48
|
|
49
49
|
With MustRD, you can:
|
50
50
|
|
51
|
-
|
52
|
-
|
53
|
-
|
51
|
+
- Define data scenarios and verify that queries produce the expected results.
|
52
|
+
- Test edge cases to ensure your queries remain reliable.
|
53
|
+
- Isolate small SPARQL enrichment or transformation steps and confirm you're only inserting what you intend.
|
54
54
|
|
55
|
-
|
55
|
+
## What?
|
56
56
|
|
57
57
|
MustRD is a Spec-By-Example ontology with a reference Python implementation, inspired by tools like Cucumber. It uses the Given-When-Then approach to define and validate SPARQL queries and transformations.
|
58
58
|
|
@@ -62,23 +62,85 @@ MustRD is designed to be triplestore/SPARQL engine agnostic, leveraging open sta
|
|
62
62
|
|
63
63
|
MustRD is not an alternative to SHACL. While SHACL validates data structures, MustRD focuses on validating data transformations and query results.
|
64
64
|
|
65
|
-
|
65
|
+
## How?
|
66
66
|
|
67
67
|
You define your specs in Turtle (`.ttl`) or TriG (`.trig`) files using the Given-When-Then approach:
|
68
68
|
|
69
|
-
|
70
|
-
|
71
|
-
|
69
|
+
- **Given**: Define the starting dataset.
|
70
|
+
- **When**: Specify the action (e.g., a SPARQL query).
|
71
|
+
- **Then**: Outline the expected results.
|
72
72
|
|
73
73
|
Depending on the type of SPARQL query (CONSTRUCT, SELECT, INSERT/DELETE), MustRD runs the query and compares the results against the expectations defined in the spec.
|
74
74
|
|
75
75
|
Expectations can also be defined as:
|
76
76
|
|
77
|
-
|
78
|
-
|
79
|
-
|
77
|
+
- INSERT queries.
|
78
|
+
- SELECT queries.
|
79
|
+
- Higher-order expectation languages, similar to those used in various platforms.
|
80
80
|
|
81
|
-
|
81
|
+
## Example
|
82
|
+
|
83
|
+
### Configuration File
|
84
|
+
|
85
|
+
You'll have a configuration `.ttl` file, which acts as a suite of tests. It tells MustRD where to look for test specifications and any triplestore configurations you might have:
|
86
|
+
|
87
|
+
```ttl
|
88
|
+
:test_example a :MustrdTest;
|
89
|
+
:hasSpecPath "test/specs/";
|
90
|
+
:hasDataPath "test/data/";
|
91
|
+
:hasPytestPath "example";
|
92
|
+
:triplestoreSpecPath "test/triplestore_config/triplestores.ttl";
|
93
|
+
:filterOnTripleStore triplestore:example_test .
|
94
|
+
```
|
95
|
+
|
96
|
+
### Test Specification
|
97
|
+
|
98
|
+
In the directory specified by `:hasSpecPath`, you'll have one or more `.mustrd.ttl` files. These can be organized in a directory structure. MustRD collects them and reports results to your test runner.
|
99
|
+
|
100
|
+
```ttl
|
101
|
+
:test_example :given [ a :FileDataset ;
|
102
|
+
:file "test/data/given.ttl" ] ;
|
103
|
+
:when [ a :TextSparqlSource ;
|
104
|
+
:queryText "SELECT ?s ?p ?o WHERE { ?s ?p ?o }" ;
|
105
|
+
:queryType :SelectSparql ] ;
|
106
|
+
:then [ a :OrderedTableDataset ;
|
107
|
+
:hasRow [ :variable "s" ; :boundValue "example:subject" ;
|
108
|
+
:variable "p" ; :boundValue "example:predicate" ;
|
109
|
+
:variable "o" ; :boundValue "example:object" ] ].
|
110
|
+
```
|
111
|
+
|
112
|
+
And you will have a `'test/data/given.ttl'` which contains the given ttl.
|
113
|
+
|
114
|
+
```ttl
|
115
|
+
example:subject example:predicate example:object .
|
116
|
+
```
|
117
|
+
|
118
|
+
### Running Tests
|
119
|
+
|
120
|
+
Run the test using the MustRD Pytest plugin:
|
121
|
+
|
122
|
+
```bash
|
123
|
+
poetry run pytest --mustrd --config=test/mustrd_configuration.ttl --md=render/github_job_summary.md
|
124
|
+
```
|
125
|
+
|
126
|
+
This will validate your SPARQL queries against the defined dataset and expected results, ensuring your transformations behave as intended.
|
127
|
+
|
128
|
+
You can refer to SPARQL inline, in files, or in Anzo Graphmarts, Steps, or Layers. See `GETSTARTED.adoc` for more details.
|
129
|
+
|
130
|
+
#### Integrating with Visual Studio Code (vscode)
|
131
|
+
We have a pytest plugin.
|
132
|
+
1. Choose a python interpreter (probably a venv)
|
133
|
+
2. `pip install mustrd ` in it.
|
134
|
+
3. add to your settings.json
|
135
|
+
```json
|
136
|
+
"python.testing.pytestArgs": [
|
137
|
+
"--mustrd", "--md=junit/github_job_summary.md", "--config=test/test_config_local.ttl"
|
138
|
+
],
|
139
|
+
```
|
140
|
+
4. VS Code should auto discover your tests and they'll show up in the flask icon 'tab'.
|
141
|
+

|
142
|
+
|
143
|
+
## When?
|
82
144
|
|
83
145
|
MustRD is a work in progress, built to meet the needs of our projects across multiple clients and vendor stacks. While we find it useful, it may not meet your needs out of the box.
|
84
146
|
|
@@ -89,3 +151,4 @@ We invite you to try it, raise issues, or contribute via pull requests. If you n
|
|
89
151
|
Semantic Partners is a specialist consultancy in Semantic Technology. If you need more support, contact us at info@semanticpartners.com or mustrd@semanticpartners.com.
|
90
152
|
|
91
153
|
|
154
|
+
|
@@ -5,29 +5,28 @@ mustrd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
mustrd/anzo_utils.py,sha256=CKNSF_oTFKc4v64EDXJzeirP1GxOIOCUKNfaJKk0oEc,4677
|
6
6
|
mustrd/logger_setup.py,sha256=kuzrFaW8Ar-q5Bvjg1OqKn9l4U5MVDyfOQF13SYk8nQ,1780
|
7
7
|
mustrd/model/catalog-v001.xml,sha256=IEtaw3FK4KGQWKancEe1HqzUQTrKdk89vNxnoLKGF4o,381
|
8
|
-
mustrd/model/mustrdShapes.ttl,sha256=
|
8
|
+
mustrd/model/mustrdShapes.ttl,sha256=OtnyOMZVUdwfUC7-F6FizSXf3P-9ubsFznYMe7gdTzY,10197
|
9
9
|
mustrd/model/mustrdTestOntology.ttl,sha256=W7IRbPKrhpYFweZvc9QH8gdjBiuZMHKETIuHBepLnYw,2034
|
10
10
|
mustrd/model/mustrdTestShapes.ttl,sha256=5PUpU9AzPSeLpF_UzNBVnACLOhs3hfoQpiz-ybsrbj8,818
|
11
11
|
mustrd/model/ontology.ttl,sha256=orb8VgPiVrmzp3dKHc53hgkq_9ic9SbqIAyLZ_cHv38,17079
|
12
12
|
mustrd/model/test-resources/resources.ttl,sha256=1Dsp1nuNxauj9bxeX-HShQsiO-CVy5Irwm2y2x0cdjI,1498
|
13
13
|
mustrd/model/triplestoreOntology.ttl,sha256=9K5gj0hDOolRYjHc58UT4igex8cUnq9h7SUe4ToYbdw,5834
|
14
14
|
mustrd/model/triplestoreshapes.ttl,sha256=G1kdgASdPa8s5JVGXL4KM2ewp-F5Vmbdist0f77VTBc,1706
|
15
|
-
mustrd/mustrd.py,sha256=
|
16
|
-
mustrd/mustrdAnzo.py,sha256=
|
15
|
+
mustrd/mustrd.py,sha256=89DVyFWHkPkKdItGWUXTGBi5qpsKlTPeJUi2WicCal8,41157
|
16
|
+
mustrd/mustrdAnzo.py,sha256=Vicc8B-zP3EwO2TF6hh7Cz-JHPNGLzG2M9KrfjzATxE,7882
|
17
17
|
mustrd/mustrdGraphDb.py,sha256=Ro_fxDPFl64r-FAM18awhZydydEY1-IXO0zdKpvZD3U,5405
|
18
18
|
mustrd/mustrdRdfLib.py,sha256=1dYoyohjDhonKItYMNkFybySFt9lgez3zYN2kU9mW-I,2369
|
19
|
-
mustrd/mustrdTestPlugin.py,sha256=
|
19
|
+
mustrd/mustrdTestPlugin.py,sha256=S2KCdK5bZZBCmA6cQC7OQQw4brfIh1jXyv1VMJP4B3M,18785
|
20
20
|
mustrd/namespace.py,sha256=1l8RJDFI7rYkWvmRokaTvSvqrDJEdRNIkq3lmPb0xpI,3854
|
21
21
|
mustrd/run.py,sha256=5xZUgKPMBQ-03cWROAnwtbOs2Nb0Vat6n8Fi6EyfS-k,4257
|
22
|
-
mustrd/
|
23
|
-
mustrd/spec_component.py,sha256=TTqF27JcIQyuXlIVYpMHfy2TajYbyJg9eb_OQ9fSp_c,38713
|
22
|
+
mustrd/spec_component.py,sha256=W4EWp-IJCp5CGG4Cy6m3D2-cViPoi0NrJCcSZiSqT2U,38643
|
24
23
|
mustrd/steprunner.py,sha256=JnajkFay-oeSJnBH3OxFVGfz5WhKjxEH1PV4WUoRPRE,10752
|
25
24
|
mustrd/templates/md_ResultList_leaf_template.jinja,sha256=IzwZjliCx7-viipATDQK6MQg_5q1kLMKdeNSZg1sXXY,508
|
26
25
|
mustrd/templates/md_ResultList_template.jinja,sha256=_8joJ7vtw_qoqxv3HhUtBgRfhOeqmgfaRFwEo4MROvQ,203
|
27
26
|
mustrd/templates/md_stats_template.jinja,sha256=96W62cMWu9UGLNv65ZQ8RYLjkxKHhJy-FlUtXgud6XY,155
|
28
27
|
mustrd/utils.py,sha256=OGdLvw7GvjrFgTJo0J97Xwdh-_ZgSmapmOistrEchO0,1387
|
29
|
-
mustrd-0.3.
|
30
|
-
mustrd-0.3.
|
31
|
-
mustrd-0.3.
|
32
|
-
mustrd-0.3.
|
33
|
-
mustrd-0.3.
|
28
|
+
mustrd-0.3.4a1.dist-info/LICENSE,sha256=r8nmh5fUct9h2w8_RDl13EIscvmwCLoarPr1kg35MnA,1078
|
29
|
+
mustrd-0.3.4a1.dist-info/METADATA,sha256=_JiUgYfLwXdnrs9H0V7toubBxy2Zu6aNU8NFWyU79bY,6425
|
30
|
+
mustrd-0.3.4a1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
31
|
+
mustrd-0.3.4a1.dist-info/entry_points.txt,sha256=v7V7sN0_L1aB4Ug_9io5axlQSeJ1C0tNrQWwdXdV58s,50
|
32
|
+
mustrd-0.3.4a1.dist-info/RECORD,,
|
mustrd/shared_utils.py
DELETED
File without changes
|
File without changes
|
File without changes
|