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/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
- return given.query(when, initBindings=bindings).graph
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:
@@ -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 write_result_diff_to_log, get_triple_store_graph, get_triple_stores
38
- from mustrd.mustrd import Specification, SpecSkipped, validate_specs, get_specs, SpecPassed, run_spec
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
- import logging
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(MustrdTestPlugin(config.getoption("mdpath"),
89
- Path(config.getoption("configpath")), config.getoption("secrets")))
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='none'
109
+ inference="none",
106
110
  )
107
111
  if not conforms:
108
- raise ValueError(f"Mustrd test configuration not conform to the shapes. SHACL report: {results_text}",
109
- results_graph)
110
-
111
- for test_config_subject in config_graph.subjects(predicate=RDF.type, object=MUSTRDTEST.MustrdTest):
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
- filter_on_tripleStore = tuple(config_graph.objects(subject=test_config_subject,
121
- predicate=MUSTRDTEST.filterOnTripleStore))
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 = root_path / \
128
- Path(triplestore_spec_path) if triplestore_spec_path else None
129
-
130
- test_configs.append(TestConfig(spec_path=spec_path, data_path=data_path,
131
- triplestore_spec_path=triplestore_spec_path,
132
- pytest_path=pytest_path,
133
- filter_on_tripleStore=filter_on_tripleStore))
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
- logger.info("Used arguments: " + str(args))
185
- self.selected_tests = list(map(lambda arg: Path(arg.split("::")[0]).resolve(),
186
- # By default the current directory is given as argument
187
- # Remove it as it is not a test
188
- filter(lambda arg: arg != os.getcwd() and "::" in arg, args)))
189
-
190
- self.path_filter = args[0] if len(
191
- args) == 1 and args[0] != os.getcwd() and not "::" in args[0] else None
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 = [str(self.test_config_file.resolve())]
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
- mustrd_file = MustrdFile.from_parent(
204
- parent, fspath=path, mustrd_plugin=self)
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(Path(os.path.join(mustrd_root, "model/mustrdShapes.ttl")))
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
- valid_spec_uris, spec_graph, invalid_spec_results = validate_specs(config, triple_stores,
214
- shacl_graph, ont_graph, file_name or "*")
215
-
216
- specs, skipped_spec_results = \
217
- get_specs(valid_spec_uris, spec_graph, triple_stores, config)
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 + invalid_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['type']
229
- triple_store_name = triple_store.replace(
230
- "https://mustrd.com/model/", "")
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(get_triple_store_graph(test_config.triplestore_spec_path,
239
- self.secrets))
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(f"""Triplestore configuration parsing failed {test_config.triplestore_spec_path}.
242
- Only rdflib will be executed""", e)
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
- {'type': TRIPLESTORE.RdfLib, 'uri': TRIPLESTORE.RdfLib}]
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(filter(lambda triple_store: (triple_store["uri"] in test_config.filter_on_tripleStore),
252
- triple_stores))
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 == 'call':
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 = test_conf.name.replace(
284
- class_name, "").replace("[", "").replace("]", "")
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(TestResult(
294
- test_name, class_name, module_name, result.outcome, is_mustrd))
295
-
296
- result_list = ResultList(None, get_result_list(test_results,
297
- lambda result: result.type,
298
- lambda result: is_mustrd and result.test_name.split("@")[1]),
299
- False)
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, 'w') as file:
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.debug(f"Collecting tests from file: {self.fspath}")
312
- test_configs = parse_config(self.fspath)
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
- # Skip if there is a path filter and it is not in the pytest path
315
- if self.mustrd_plugin.path_filter is not None and self.mustrd_plugin.path_filter not in test_config.pytest_path:
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
- triple_stores = self.mustrd_plugin.get_triple_stores_from_file(
318
- test_config)
319
-
320
- if test_config.filter_on_tripleStore and not triple_stores:
321
- specs = list(map(
322
- lambda triple_store:
323
- SpecSkipped(MUST.TestSpec, triple_store,
324
- "No triplestore found"),
325
- test_config.filter_on_tripleStore))
326
- else:
327
- specs = self.mustrd_plugin.generate_tests_for_config({"spec_path": test_config.spec_path,
328
- "data_path": test_config.data_path},
329
- triple_stores, None)
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
- # Check if the current test is in the selected tests in arguments
332
- if spec.spec_source_file.resolve() in self.mustrd_plugin.selected_tests \
333
- or self.mustrd_plugin.selected_tests == []:
334
- item = MustrdItem.from_parent(
335
- self, name=test_config.pytest_path + "/" + spec.spec_file_name, spec=spec)
336
- self.mustrd_plugin.items.append(item)
337
- yield item
338
- except BaseException as e:
339
- # Catch error here otherwise it will be lost
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.info(f"Creating item: {name}")
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
- return f"{self.name} failed: {excinfo.value}"
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"]