Skip to content
Draft
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"python.testing.unittestEnabled": false,
"python.testing.nosetestsEnabled": false,
"python.testing.pytestArgs": [
"--mustrd", "--ignore-focus", "--config=test/test_config_local.ttl" , "--log-cli-level=DEBUG"
"--mustrd", "--config=test/test_config_local.ttl" , "--log-cli-level=DEBUG", "--log-format=%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s"
],
"workbench.colorCustomizations": {
"activityBar.background": "#183323",
Expand Down
36 changes: 21 additions & 15 deletions mustrd/logger_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

import logging
import sys
from colorlog import ColoredFormatter

Check failure on line 27 in mustrd/logger_setup.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/logger_setup.py#L27

'colorlog.ColoredFormatter' imported but unused (F401)


LOG_LEVEL = logging.INFO
Expand All @@ -32,24 +32,30 @@


def setup_logger(name: str) -> logging.Logger:
log = logging.getLogger(name)
log.setLevel(LOG_LEVEL)


stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setLevel(logging.ERROR)
log.addHandler(stderr_handler)

ch = logging.StreamHandler(sys.stdout)
ch.setLevel(LOG_LEVEL)
ch.setFormatter(ColoredFormatter(LOG_FORMAT))
log.addHandler(ch)

return log

logger = logging.getLogger(name)
if not logger.hasHandlers():
handler = logging.StreamHandler()
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
return logger

# Ensure the root logger captures all logs
root_logger = logging.getLogger()

Check failure on line 47 in mustrd/logger_setup.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/logger_setup.py#L47

Expected 2 blank lines after class or function definition, found 1 (E305)
if not root_logger.hasHandlers():
root_handler = logging.StreamHandler()
root_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
)
root_handler.setFormatter(root_formatter)
root_logger.addHandler(root_handler)
root_logger.setLevel(logging.DEBUG)

def flush():

Check failure on line 57 in mustrd/logger_setup.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/logger_setup.py#L57

Expected 2 blank lines, found 1 (E302)
logging.shutdown()
sys.stdout.flush()

logging.getLogger("edn_format").setLevel(logging.WARNING)

Check failure on line 61 in mustrd/logger_setup.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/logger_setup.py#L61

Expected 2 blank lines after class or function definition, found 1 (E305)
169 changes: 143 additions & 26 deletions mustrd/mustrdTestPlugin.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import logging
from dataclasses import dataclass
import pprint
import pytest
import os
from pathlib import Path
from rdflib.namespace import Namespace
from rdflib import Graph, RDF
from pytest import Session
from collections import defaultdict

from mustrd import logger_setup
from mustrd.TestResult import ResultList, TestResult, get_result_list
Expand Down Expand Up @@ -379,27 +381,19 @@
self.mustrd_plugin = mustrd_plugin
super(pytest.File, self).__init__(*args, **kwargs)

def collect(self):

Check failure on line 384 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L384

'MustrdFile.collect' is too complex (17) (C901)
try:
logger.info(f"{self.mustrd_plugin.test_config_file}: Collecting tests from file: {self.path=}")
# Only process the specific mustrd config file we were given

# if not str(self.fspath).endswith(".ttl"):
# return []
# Only process the specific mustrd config file we were given
# if str(self.fspath) != str(self.mustrd_plugin.test_config_file):
# logger.info(f"Skipping non-config file: {self.fspath}")
# return []

test_configs = parse_config(self.path)
from collections import defaultdict
pytest_path_grouped = defaultdict(list)
nested_grouped = defaultdict(lambda: defaultdict(dict)) # Adjusted to support deeper nesting

for test_config in test_configs:
if (
self.mustrd_plugin.path_filter is not None
and not str(test_config.pytest_path).startswith(str(self.mustrd_plugin.path_filter))
):
logger.info(f"Skipping test config due to path filter: {test_config.pytest_path=} {self.mustrd_plugin.path_filter=}")

Check failure on line 396 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L396

Line too long (137 > 127 characters) (E501)
continue

triple_stores = self.mustrd_plugin.get_triple_stores_from_file(test_config)
Expand All @@ -424,30 +418,118 @@
)
for triple_store in (triple_stores or test_config.filter_on_tripleStore)
]
pytest_path = getattr(test_config, "pytest_path", "unknown")
for spec in specs:
pytest_path_grouped[pytest_path].append(spec)

for pytest_path, specs_for_path in pytest_path_grouped.items():
logger.info(f"pytest_path group: {pytest_path} ({len(specs_for_path)} specs)")

yield MustrdPytestPathCollector.from_parent(
self,
name=str(pytest_path),
pytest_path=pytest_path,
specs=specs_for_path,
mustrd_plugin=self.mustrd_plugin,
)
# Process each spec's source file path to build the correct nested structure
for spec in specs:
nested_structure = build_nested_structure(spec.spec_source_file, self.path)
logger.debug(f"Nested structure for {pprint.pformat(spec.spec_source_file)}: {pprint.pformat(nested_structure)}")

Check failure on line 425 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L425

Line too long (133 > 127 characters) (E501)

current_level = nested_grouped
# Navigate through the directory structure except the last one
for i, level in enumerate(nested_structure):
logger.debug(f"Processing level {i}: {level}, current structure: {current_level}")

Check failure on line 431 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L431

Blank line contains whitespace (W293)
# Get or create the current level's content
if level not in current_level:
if i == len(nested_structure) - 1: # Last level - should be a list
current_level[level] = []
else: # Intermediate level - should be a dict
current_level[level] = defaultdict(lambda: defaultdict(dict))
elif i < len(nested_structure) - 1: # Not at last level but exists
if isinstance(current_level[level], list):
# Convert list to dict and preserve files
existing_files = current_level[level]
current_level[level] = defaultdict(lambda: defaultdict(dict))
current_level[level]['__files__'] = existing_files

Check failure on line 444 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L444

Blank line contains whitespace (W293)
# If this is the last level, add the spec to the list
if i == len(nested_structure) - 1:
if isinstance(current_level[level], dict):
# Has subdirectories, store under __files__
if '__files__' not in current_level[level]:
current_level[level]['__files__'] = []
current_level[level]['__files__'].append(spec)
else:
# No subdirectories, just append to list
current_level[level].append(spec)
else:
# Move to next level if not at the end
current_level = current_level[level]
logger.debug(f"Moving to next level: {current_level}")

Check failure on line 459 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L459

Blank line contains whitespace (W293)
# Already added spec in the loop above

Check failure on line 461 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L461

Blank line contains whitespace (W293)
logger.debug(f"Final structure at this point: {pprint.pformat(nested_grouped)}")

logger.debug(f"Updated nested_grouped structure: {pprint.pformat(nested_grouped)}")

for folder, subfolders in nested_grouped.items():
logger.debug(f"Creating collector for folder: {folder} with subfolders: {subfolders}")
yield self._create_nested_collectors(folder, subfolders)
except Exception as e:
self.mustrd_plugin.collect_error = e
logger.error(f"Error during collection {self.path}: {type(e)} {e} {traceback.format_exc()}")
raise e

def _create_nested_collectors(self, name, subfolders):
if isinstance(subfolders, list):
# If subfolders is a list, it contains specs
return MustrdSubfolderCollector.from_parent(
self,
name=name,
subfolder=name,
specs=subfolders,
mustrd_plugin=self.mustrd_plugin,
)
else:
# If subfolders is a dict, it might contain both files and directories
# First check for files at this level
files = subfolders.get('__files__', [])
if files and len(subfolders) == 1: # Only has __files__
return MustrdSubfolderCollector.from_parent(
self,
name=name,
subfolder=name,
specs=files,
mustrd_plugin=self.mustrd_plugin,
)

Check failure on line 496 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L496

Blank line contains whitespace (W293)
# Create a folder collector that will handle both files and subdirs
return MustrdFolderCollector.from_parent(
self,
name=name,
folder=name,
subfolders=subfolders,
mustrd_plugin=self.mustrd_plugin,
)

class MustrdFolderCollector(pytest.Class):

Check failure on line 506 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L506

Expected 2 blank lines, found 1 (E302)
def __init__(self, name, parent, folder, subfolders, mustrd_plugin):
super().__init__(name, parent)
self.folder = folder
self.subfolders = subfolders
self.mustrd_plugin = mustrd_plugin

class MustrdPytestPathCollector(pytest.Class):
def __init__(self, name, parent, pytest_path, specs, mustrd_plugin):
def collect(self):
# First yield any files at this level
if '__files__' in self.subfolders:
yield MustrdSubfolderCollector.from_parent(
self.parent,
name=self.folder,
subfolder=self.folder,
specs=self.subfolders['__files__'],
mustrd_plugin=self.mustrd_plugin,
)

Check failure on line 523 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L523

Blank line contains whitespace (W293)
# Then yield all subdirectories
for subfolder, specs_or_subfolders in self.subfolders.items():
if subfolder != '__files__': # Skip the files we already processed
yield self.parent._create_nested_collectors(subfolder, specs_or_subfolders)

class MustrdSubfolderCollector(pytest.Class):

Check failure on line 529 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L529

Expected 2 blank lines, found 1 (E302)
def __init__(self, name, parent, subfolder, specs, mustrd_plugin):
super().__init__(name, parent)
self.pytest_path = pytest_path
self.subfolder = subfolder
self.specs = specs
self.mustrd_plugin = mustrd_plugin

Expand Down Expand Up @@ -524,3 +606,38 @@

logger.info(f"Test PASSED: {getattr(test_spec, 'spec_uri', test_spec)}")
return isinstance(result, SpecPassed)

def build_nested_structure(path, config_path):

Check failure on line 610 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L610

Expected 2 blank lines, found 1 (E302)
"""Build a nested structure of directories relative to the config file."""
try:
logger.info(f"Building nested structure for path: {path} with config_path: {config_path}")
# Convert both paths to absolute Path objects
path = Path(path).resolve()
config_parent = Path(config_path).parent.resolve()

Check failure on line 617 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L617

Blank line contains whitespace (W293)
# Always try to make the path relative to config parent first
try:
rel_path = path.relative_to(config_parent)
logger.debug(f"Successfully made path relative to config parent: {rel_path}")
except ValueError:
# If we can't make it relative to config parent, use the actual path
rel_path = path

Check failure on line 625 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L625

Blank line contains whitespace (W293)
# Get all directory parts (exclude the file name if it's a file)
if path.is_file():
path_parts = path.parent.parts
else:
path_parts = path.parts

Check failure on line 631 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L631

Blank line contains whitespace (W293)
# Remove drive or root prefix if present
if Path(path).is_absolute():
path_parts = path_parts[1:]

Check failure on line 635 in mustrd/mustrdTestPlugin.py

View workflow job for this annotation

GitHub Actions / Flake8

mustrd/mustrdTestPlugin.py#L635

Blank line contains whitespace (W293)
# Filter out empty parts and create tuple
parts = tuple(part for part in path_parts if part != '')
logger.debug(f"Final directory structure: {parts}")
return parts
except Exception as e:
logger.error(f"Error building nested structure: {e}")
# In case of any errors, return just the immediate parent directory name
return (str(Path(path).parent.name),)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "mustrd"
version = "0.5.1"
version = "0.6.0a3"
description = "A Spec By Example framework for RDF and SPARQL, Inspired by Cucumber."
authors = [
{ name = "John Placek", email = "john.placek@semanticpartners.com" },
Expand Down
Loading
Loading