e828acf175
CI / lint (push) Successful in 1m32s
CI / typecheck (push) Successful in 1m38s
CI / lint (pull_request) Successful in 1m30s
CI / typecheck (pull_request) Successful in 1m30s
CI / behave (3.11) (pull_request) Successful in 1m39s
CI / behave (3.12) (pull_request) Successful in 1m39s
CI / build (pull_request) Successful in 1m29s
CI / behave (3.13) (pull_request) Successful in 1m39s
CI / behave (3.11) (push) Successful in 1m39s
CI / behave (3.12) (push) Successful in 1m41s
CI / behave (3.13) (push) Successful in 1m37s
CI / build (push) Successful in 1m30s
ISSUES CLOSED: #1
1198 lines
44 KiB
Python
1198 lines
44 KiB
Python
"""
|
|
Step definitions for graph loading feature tests.
|
|
|
|
This module implements tests for the GraphLoader class,
|
|
which uses the Chain of Responsibility pattern for handling different URI schemes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from behave import given, then, when
|
|
from rdflib import Graph, Literal, Namespace, URIRef
|
|
from rdflib.namespace import OWL, RDF, RDFS
|
|
|
|
from cleverrdf_lib.core.exceptions import OntologyLoaderFailedException
|
|
from cleverrdf_lib.utils.default_graph_loader_http_handler import DefaultGraphLoaderHTTPHandler
|
|
from cleverrdf_lib.utils.graph_loader import GraphLoader
|
|
from cleverrdf_lib.utils.graph_loader_remapper_handler import GraphLoaderRemapperHandler
|
|
|
|
TEST_NS = Namespace("http://example.org/test#")
|
|
|
|
|
|
@given("I have a GraphLoader instance")
|
|
def step_have_graph_loader(context: Any) -> None:
|
|
"""
|
|
Create a GraphLoader instance for testing.
|
|
|
|
This initializes the GraphLoader with default handlers.
|
|
"""
|
|
context.graph_loader = GraphLoader()
|
|
context.loaded_graph = None
|
|
context.load_error = None
|
|
|
|
|
|
@given('I have a valid RDF file at "{file_path}"')
|
|
def step_have_valid_rdf_file(context: Any, file_path: str) -> None:
|
|
"""
|
|
Create a valid RDF file at the specified path.
|
|
|
|
This creates a minimal valid RDF/XML file for testing.
|
|
"""
|
|
full_path = Path(context.test_data_dir) / file_path
|
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
graph = Graph()
|
|
graph.bind("test", TEST_NS)
|
|
graph.bind("owl", OWL)
|
|
graph.bind("rdf", RDF)
|
|
graph.bind("rdfs", RDFS)
|
|
|
|
ontology_uri = URIRef(f"file://{full_path}")
|
|
graph.add((ontology_uri, RDF.type, OWL.Ontology))
|
|
|
|
test_class = TEST_NS.TestClass
|
|
graph.add((test_class, RDF.type, OWL.Class))
|
|
graph.add((test_class, RDFS.label, Literal("Test Class")))
|
|
|
|
graph.serialize(destination=str(full_path), format="xml")
|
|
|
|
context.test_file_path = str(full_path)
|
|
|
|
|
|
@given('I have a Turtle file at "{file_path}"')
|
|
def step_have_turtle_file(context: Any, file_path: str) -> None:
|
|
"""
|
|
Create a Turtle format RDF file at the specified path.
|
|
|
|
This creates a minimal valid Turtle file for testing.
|
|
"""
|
|
full_path = Path(context.test_data_dir) / file_path
|
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
graph = Graph()
|
|
graph.bind("test", TEST_NS)
|
|
graph.bind("owl", OWL)
|
|
graph.bind("rdf", RDF)
|
|
|
|
ontology_uri = URIRef(f"file://{full_path}")
|
|
graph.add((ontology_uri, RDF.type, OWL.Ontology))
|
|
|
|
test_class = TEST_NS.TestClass
|
|
graph.add((test_class, RDF.type, OWL.Class))
|
|
|
|
graph.serialize(destination=str(full_path), format="turtle")
|
|
|
|
context.test_file_path = str(full_path)
|
|
|
|
|
|
@given('I have a valid RDF URL at "{url}"')
|
|
def step_have_valid_rdf_url(context: Any, url: str) -> None:
|
|
"""
|
|
Set a valid RDF URL for testing.
|
|
|
|
Note: This URL may not actually exist, so tests should handle failures gracefully.
|
|
"""
|
|
context.test_url = url
|
|
|
|
|
|
@given('I have a file with unsupported format at "{file_path}"')
|
|
def step_have_unsupported_format_file(context: Any, file_path: str) -> None:
|
|
"""
|
|
Create a file with an unsupported format.
|
|
|
|
This creates a file that cannot be parsed as RDF.
|
|
"""
|
|
full_path = Path(context.test_data_dir) / file_path
|
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(full_path, "w") as f:
|
|
f.write("This is not a valid RDF file\nInvalid content")
|
|
|
|
context.test_file_path = str(full_path)
|
|
|
|
|
|
@given('I have a non-existent file path "{file_path}"')
|
|
def step_have_nonexistent_file(context: Any, file_path: str) -> None:
|
|
"""
|
|
Set a non-existent file path for testing error handling.
|
|
|
|
This tests file not found scenarios.
|
|
Fails fast if the file actually exists.
|
|
|
|
Args:
|
|
file_path: The file path that should not exist
|
|
|
|
Raises:
|
|
AssertionError: If the file actually exists
|
|
"""
|
|
from pathlib import Path
|
|
|
|
# Resolve the full path (handling relative paths)
|
|
if Path(file_path).is_absolute():
|
|
full_path = Path(file_path)
|
|
else:
|
|
# If test_data_dir is available, use it; otherwise use current directory
|
|
if hasattr(context, "test_data_dir"):
|
|
full_path = Path(context.test_data_dir) / file_path
|
|
else:
|
|
full_path = Path(file_path)
|
|
|
|
# Fail fast if the file actually exists
|
|
assert not full_path.exists(), (
|
|
f"File path '{file_path}' (resolved to '{full_path}') should not exist, "
|
|
f"but it does. Use a path that doesn't exist for testing file not found scenarios."
|
|
)
|
|
|
|
context.test_file_path = file_path
|
|
|
|
|
|
@given('I have an unreachable URL at "{url}"')
|
|
def step_have_unreachable_url(context: Any, url: str) -> None:
|
|
"""
|
|
Set an unreachable URL for testing error handling.
|
|
|
|
This tests network error scenarios.
|
|
Fails fast if the URL is actually reachable (for file:// URLs) or if it's a valid local file.
|
|
|
|
Args:
|
|
url: The URL that should be unreachable
|
|
|
|
Raises:
|
|
AssertionError: If the URL is actually reachable (for file:// URLs) or points to an existing file
|
|
"""
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
|
|
parsed = urlparse(url)
|
|
|
|
# For file:// URLs, verify the file doesn't exist
|
|
if parsed.scheme == "file" or parsed.scheme == "":
|
|
# Extrac t file path from URL
|
|
if parsed.scheme == "file":
|
|
file_path = parsed.path
|
|
else:
|
|
# No scheme, treat as file path
|
|
file_path = url
|
|
|
|
# Resolve the full path
|
|
if Path(file_path).is_absolute():
|
|
full_path = Path(file_path)
|
|
else:
|
|
if hasattr(context, "test_data_dir"):
|
|
full_path = Path(context.test_data_dir) / file_path
|
|
else:
|
|
full_path = Path(file_path)
|
|
|
|
# Fail fast if the file exists
|
|
assert not full_path.exists(), (
|
|
f"URL '{url}' points to an existing file at '{full_path}'. "
|
|
f"Use a URL that points to a non-existent file or an unreachable network resource."
|
|
)
|
|
elif parsed.scheme in ("http", "https"):
|
|
# For HTTP/HTTPS URLs, we can't easily verify reachability without making a network request
|
|
# which would be slow and might fail for other reasons. Instead, we verify:
|
|
# 1. It's not a localhost URL (which might be reachable)
|
|
# 2. It's not pointing to a known reachable domain
|
|
hostname = parsed.hostname or ""
|
|
|
|
# Check if it's localhost or 127.0.0.1 (which might be reachable)
|
|
if hostname in ("localhost", "127.0.0.1", "::1") or hostname.startswith("127."):
|
|
# This is a warning but not a failure - localhost might be unreachable if no server is running
|
|
pass # Allow it, but it's not ideal
|
|
|
|
# For other domains, we assume they're unreachable (e.g., unreachable.example.com)
|
|
# This is the intended behavior for testing network errors
|
|
pass
|
|
else:
|
|
# For unknown schemes, fail fast - we can't verify reachability and it's unclear what the test expects
|
|
assert False, (
|
|
f"Unknown URL scheme '{parsed.scheme}' in URL '{url}'. "
|
|
f"Only 'file://', 'http://', 'https://', or no scheme (file path) are supported for unreachable URL testing. "
|
|
f"Use a file path or HTTP/HTTPS URL for testing unreachable resources."
|
|
)
|
|
|
|
context.test_url = url
|
|
|
|
|
|
@given("I have a GraphLoader with multiple handlers")
|
|
def step_have_graph_loader_with_handlers(context: Any) -> None:
|
|
"""
|
|
Create a GraphLoader with multiple handlers configured.
|
|
|
|
This tests the Chain of Responsibility pattern.
|
|
"""
|
|
context.graph_loader = GraphLoader()
|
|
# The loader already has file and HTTP handlers by default
|
|
# Create a test file for loading
|
|
if not hasattr(context, "test_file_path"):
|
|
from pathlib import Path
|
|
|
|
from rdflib import Graph, Literal, Namespace, URIRef
|
|
from rdflib.namespace import OWL, RDF, RDFS
|
|
|
|
TEST_NS = Namespace("http://example.org/test#")
|
|
test_file = Path(context.test_data_dir) / "test_data" / "simple.rdf"
|
|
test_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
graph = Graph()
|
|
graph.bind("test", TEST_NS)
|
|
graph.bind("owl", OWL)
|
|
graph.bind("rdf", RDF)
|
|
graph.bind("rdfs", RDFS)
|
|
|
|
ontology_uri = URIRef(f"file://{test_file}")
|
|
graph.add((ontology_uri, RDF.type, OWL.Ontology))
|
|
|
|
test_class = TEST_NS.TestClass
|
|
graph.add((test_class, RDF.type, OWL.Class))
|
|
graph.add((test_class, RDFS.label, Literal("Test Class")))
|
|
|
|
graph.serialize(destination=str(test_file), format="xml")
|
|
context.test_file_path = str(test_file)
|
|
|
|
|
|
@given("I have a GraphLoader with remapper handler")
|
|
def step_have_graph_loader_with_remapper(context: Any) -> None:
|
|
"""
|
|
Create a GraphLoader with a remapper handler.
|
|
|
|
This tests URI remapping functionality.
|
|
"""
|
|
from cleverrdf_lib.core.interfaces.interfaces import RemapRule
|
|
|
|
context.graph_loader = GraphLoader()
|
|
|
|
# Create a simple remap rule for testing
|
|
class SimpleRemapRule(RemapRule):
|
|
def matches(
|
|
self, uri_scheme: str, uri_path: str, uri_full: str, serialization_format: str | None = None
|
|
) -> bool:
|
|
return uri_full.startswith("http://example.org/old")
|
|
|
|
def remap(
|
|
self, uri_scheme: str, uri_path: str, uri_full: str, serialization_format: str | None = None
|
|
) -> tuple[str, str, str, str | None]:
|
|
new_uri = uri_full.replace("http://example.org/old", "http://example.org/new")
|
|
return (
|
|
"http",
|
|
new_uri.replace("http://", "").split("/", 1)[1] if "/" in new_uri.replace("http://", "") else "",
|
|
new_uri,
|
|
serialization_format,
|
|
)
|
|
|
|
def get_name(self) -> str:
|
|
return "SimpleRemapRule"
|
|
|
|
context.remap_rule = SimpleRemapRule()
|
|
context.remapper_handler = GraphLoaderRemapperHandler([context.remap_rule])
|
|
|
|
|
|
@given("I have configured URI remapping rules")
|
|
def step_configure_uri_remapping(context: Any) -> None:
|
|
"""
|
|
Configure URI remapping rules for testing.
|
|
|
|
This sets up URI redirection rules.
|
|
"""
|
|
# Remapping rules are already configured in step_have_graph_loader_with_remapper
|
|
if not hasattr(context, "remapper_handler"):
|
|
from cleverrdf_lib.core.interfaces import RemapRule
|
|
|
|
class SimpleRemapRule(RemapRule):
|
|
def matches(
|
|
self, uri_scheme: str, uri_path: str, uri_full: str, serialization_format: str | None = None
|
|
) -> bool:
|
|
return uri_full.startswith("http://example.org/old")
|
|
|
|
def remap(
|
|
self, uri_scheme: str, uri_path: str, uri_full: str, serialization_format: str | None = None
|
|
) -> tuple[str, str, str, str | None]:
|
|
new_uri = uri_full.replace("http://example.org/old", "http://example.org/new")
|
|
return (
|
|
"http",
|
|
new_uri.replace("http://", "").split("/", 1)[1] if "/" in new_uri.replace("http://", "") else "",
|
|
new_uri,
|
|
serialization_format,
|
|
)
|
|
|
|
def get_name(self) -> str:
|
|
return "SimpleRemapRule"
|
|
|
|
context.remap_rule = SimpleRemapRule()
|
|
context.remapper_handler = GraphLoaderRemapperHandler([context.remap_rule])
|
|
|
|
# Set up remapping rules dict for step_load_graph_with_remapping
|
|
context.remapping_rules = {"http://example.org/old": "http://example.org/new"}
|
|
|
|
|
|
@given("I have a GraphLoader with a handler chain")
|
|
def step_have_graph_loader_with_chain(context: Any) -> None:
|
|
"""
|
|
Create a GraphLoader with a handler chain.
|
|
|
|
This tests handler chain manipulation.
|
|
"""
|
|
context.graph_loader = GraphLoader()
|
|
|
|
|
|
@given("I have a new handler to insert")
|
|
def step_have_new_handler(context: Any) -> None:
|
|
"""
|
|
Create a new handler for insertion into the chain.
|
|
|
|
This tests handler insertion functionality.
|
|
"""
|
|
from cleverrdf_lib.utils.default_graph_loader_file_handler import DefaultGraphLoaderFileHandler
|
|
|
|
context.new_handler = DefaultGraphLoaderFileHandler()
|
|
|
|
|
|
@given("I have a new handler")
|
|
def step_have_new_handler_simple(context: Any) -> None:
|
|
"""
|
|
Create a new handler for chain reset.
|
|
|
|
This tests handler reset functionality.
|
|
"""
|
|
from cleverrdf_lib.utils.default_graph_loader_file_handler import DefaultGraphLoaderFileHandler
|
|
|
|
context.new_handler = DefaultGraphLoaderFileHandler()
|
|
|
|
|
|
@given("I have a handler ID to insert after")
|
|
def step_have_handler_id(context: Any) -> None:
|
|
"""
|
|
Set a handler ID to insert after.
|
|
|
|
This tests insertion by ID functionality.
|
|
"""
|
|
# Get the ID of the first handler in the chain using get_id() method
|
|
assert context.graph_loader.graph_loaders_chain is not None, "Handler chain should exist"
|
|
context.handler_id = context.graph_loader.graph_loaders_chain.get_id()
|
|
|
|
|
|
@when("I load the graph from the file path")
|
|
@when("I load the graph without specifying format")
|
|
@when("I attempt to load the graph")
|
|
@when("I attempt to load the graph from the file path")
|
|
@when("I load a graph from a file path")
|
|
def step_load_graph_from_file(context: Any) -> None:
|
|
"""
|
|
Load a graph from a file path.
|
|
|
|
This tests file path loading functionality.
|
|
Handles errors gracefully for error testing scenarios.
|
|
"""
|
|
try:
|
|
context.loaded_graph = context.graph_loader.load_from_uri(context.test_file_path)
|
|
context.load_error = None
|
|
except Exception as e:
|
|
context.load_error = e
|
|
context.loaded_graph = None
|
|
|
|
|
|
@when("I load the graph from the file:// URI")
|
|
def step_load_graph_from_file_uri(context: Any) -> None:
|
|
"""
|
|
Load a graph from a file:// URI.
|
|
|
|
This tests file:// URI loading functionality.
|
|
"""
|
|
file_uri = f"file://{context.test_file_path}"
|
|
try:
|
|
context.loaded_graph = context.graph_loader.load_from_uri(file_uri)
|
|
context.load_error = None
|
|
except Exception as e:
|
|
context.load_error = e
|
|
context.loaded_graph = None
|
|
|
|
|
|
@when("I load the graph from the HTTP URL")
|
|
@when("I load the graph from the HTTPS URL")
|
|
def step_load_graph_from_http(context: Any) -> None:
|
|
"""
|
|
Load a graph from an HTTP or HTTPS URL.
|
|
|
|
This tests HTTP/HTTPS URL loading functionality.
|
|
Note: The URL may not exist, so we handle failures gracefully.
|
|
"""
|
|
try:
|
|
context.loaded_graph = context.graph_loader.load_from_uri(context.test_url)
|
|
context.load_error = None
|
|
except Exception as e:
|
|
context.load_error = e
|
|
context.loaded_graph = None
|
|
# For HTTP/HTTPS URLs that don't exist, this is expected behavior
|
|
|
|
|
|
@when('I load the graph with format "{format}"')
|
|
def step_load_graph_with_format(context: Any, format: str) -> None:
|
|
"""
|
|
Load a graph with an explicit format.
|
|
|
|
This tests format specification functionality.
|
|
"""
|
|
try:
|
|
context.loaded_graph = context.graph_loader.load_from_uri(context.test_file_path, format)
|
|
context.load_error = None
|
|
except Exception as e:
|
|
context.load_error = e
|
|
context.loaded_graph = None
|
|
|
|
|
|
@when("I attempt to load the graph from the URL")
|
|
def step_attempt_load_from_url(context: Any) -> None:
|
|
"""
|
|
Attempt to load a graph from a URL (may fail).
|
|
|
|
This tests network error handling.
|
|
"""
|
|
try:
|
|
context.loaded_graph = context.graph_loader.load_from_uri(context.test_url)
|
|
context.load_error = None
|
|
except Exception as e:
|
|
context.load_error = e
|
|
context.loaded_graph = None
|
|
|
|
|
|
@when("I load a graph with a URI that matches a remapping rule")
|
|
def step_load_graph_with_remapping(context: Any) -> None:
|
|
"""
|
|
Load a graph with a URI that matches a remapping rule.
|
|
|
|
This tests URI remapping functionality.
|
|
"""
|
|
# Insert the remapper handler into the chain
|
|
context.graph_loader.insert_handler_in_chain(context.remapper_handler, 0)
|
|
|
|
# Use a URI that matches the remapping rule
|
|
old_uri = list(context.remapping_rules.keys())[0]
|
|
try:
|
|
context.loaded_graph = context.graph_loader.load_from_uri(old_uri)
|
|
context.load_error = None
|
|
except Exception as e:
|
|
context.load_error = e
|
|
context.loaded_graph = None
|
|
|
|
|
|
@when("I insert the handler at position {position:d}")
|
|
def step_insert_handler_at_position(context: Any, position: int) -> None:
|
|
"""
|
|
Insert a handler at a specific position.
|
|
|
|
This tests handler insertion by position.
|
|
"""
|
|
context.graph_loader.insert_handler_in_chain(context.new_handler, position)
|
|
|
|
|
|
@when("I insert the handler after the specified ID")
|
|
def step_insert_handler_after_id(context: Any) -> None:
|
|
"""
|
|
Insert a handler after a specified handler ID.
|
|
|
|
This tests handler insertion by ID.
|
|
"""
|
|
context.graph_loader.insert_handler_in_chain(context.new_handler, context.handler_id)
|
|
|
|
|
|
@when("I reset the chain to the new handler")
|
|
def step_reset_chain_to_handler(context: Any) -> None:
|
|
"""
|
|
Reset the handler chain to a single handler.
|
|
|
|
This tests chain reset functionality.
|
|
"""
|
|
context.graph_loader.reset_chain_to_handler(context.new_handler)
|
|
|
|
|
|
@then("the graph loading should succeed")
|
|
def step_loading_should_succeed_graph(context: Any) -> None:
|
|
"""
|
|
Verify that graph loading succeeded.
|
|
|
|
This checks that loading completed without errors.
|
|
"""
|
|
assert context.load_error is None, f"Loading should succeed, got error: {context.load_error}"
|
|
assert context.loaded_graph is not None, "Loaded graph should not be None"
|
|
|
|
|
|
@then("the graph should not be empty")
|
|
def step_graph_not_empty(context: Any) -> None:
|
|
"""
|
|
Verify that the loaded graph is not empty.
|
|
|
|
This checks that data was actually loaded.
|
|
"""
|
|
assert context.loaded_graph is not None, "Graph should exist"
|
|
assert len(context.loaded_graph) > 0, "Graph should not be empty"
|
|
|
|
|
|
@then("the graph loading may fail for non-existent URLs")
|
|
def step_graph_loading_may_fail(context: Any) -> None:
|
|
"""
|
|
Verify that graph loading may fail for non-existent URLs.
|
|
|
|
This checks that HTTP URL loading handles failures gracefully.
|
|
"""
|
|
# For HTTP URLs, loading may fail if the URL doesn't exist
|
|
# This is expected behavior, so we just verify the attempt was made
|
|
assert hasattr(context, "load_error") or hasattr(context, "loaded_graph"), "Loading attempt should be made"
|
|
|
|
|
|
@then("if successful, the graph should not be empty")
|
|
def step_graph_not_empty_if_successful(context: Any) -> None:
|
|
"""
|
|
Verify that if loading succeeded, the graph should not be empty.
|
|
|
|
This checks graph content when loading succeeds.
|
|
"""
|
|
if context.load_error is None and context.loaded_graph is not None:
|
|
assert len(context.loaded_graph) > 0, "Graph should not be empty if loading succeeded"
|
|
|
|
|
|
@then("if successful, the graph should contain RDF triples")
|
|
def step_graph_contains_triples_if_successful(context: Any) -> None:
|
|
"""
|
|
Verify that if loading succeeded, the graph should contain RDF triples.
|
|
|
|
This checks graph content when loading succeeds.
|
|
"""
|
|
if context.load_error is None and context.loaded_graph is not None:
|
|
assert len(context.loaded_graph) > 0, "Graph should contain RDF triples if loading succeeded"
|
|
|
|
|
|
@then("the graph should contain RDF triples")
|
|
def step_graph_contains_triples(context: Any) -> None:
|
|
"""
|
|
Verify that the graph contains RDF triples.
|
|
|
|
This checks that valid RDF data was loaded.
|
|
"""
|
|
assert context.loaded_graph is not None, "Graph should exist"
|
|
assert len(context.loaded_graph) > 0, "Graph should contain triples"
|
|
# Verify it's a valid RDF graph by checking for at least one triple
|
|
triples = list(context.loaded_graph.triples((None, None, None)))
|
|
assert len(triples) > 0, "Graph should contain at least one triple"
|
|
|
|
|
|
@then("the graph should be parsed as {format} format")
|
|
def step_graph_parsed_as_format(context: Any, format: str) -> None:
|
|
"""
|
|
Verify that the graph was parsed in the specified format.
|
|
|
|
This checks format handling.
|
|
"""
|
|
assert context.loaded_graph is not None, "Graph should exist"
|
|
# The format is implicit in successful parsing, so we just verify the graph loaded
|
|
assert len(context.loaded_graph) > 0, "Graph should contain data"
|
|
|
|
|
|
@then("the format should be auto-detected from extension")
|
|
def step_format_auto_detected(context: Any) -> None:
|
|
"""
|
|
Verify that format was auto-detected from file extension.
|
|
|
|
This checks format auto-detection functionality.
|
|
"""
|
|
assert context.loaded_graph is not None, "Graph should exist"
|
|
# Format detection is implicit in successful loading
|
|
assert len(context.loaded_graph) > 0, "Graph should contain data"
|
|
|
|
|
|
@then("the loading should fail with an appropriate error")
|
|
def step_loading_should_fail(context: Any) -> None:
|
|
"""
|
|
Verify that loading failed with an error.
|
|
|
|
This checks error handling for invalid formats.
|
|
"""
|
|
assert context.load_error is not None, "Loading should have failed with an error"
|
|
|
|
|
|
@then("the loading should fail")
|
|
def step_loading_should_fail_simple(context: Any) -> None:
|
|
"""
|
|
Verify that loading failed.
|
|
|
|
This checks error handling.
|
|
"""
|
|
assert context.load_error is not None, "Loading should have failed"
|
|
|
|
|
|
@then("the error should indicate the format issue")
|
|
def step_error_indicates_format_issue(context: Any) -> None:
|
|
"""
|
|
Verify that the error indicates a format issue.
|
|
|
|
This checks error message content.
|
|
"""
|
|
assert context.load_error is not None, "Error should exist"
|
|
error_msg = str(context.load_error).lower()
|
|
# Check for format-related error indicators (syntax error, parse error, etc.)
|
|
assert (
|
|
"format" in error_msg
|
|
or "parse" in error_msg
|
|
or "serialization" in error_msg
|
|
or "syntax error" in error_msg
|
|
or "failed to load" in error_msg
|
|
), f"Error should indicate format issue, got: {error_msg}"
|
|
|
|
|
|
@then("the error should indicate file not found")
|
|
def step_error_indicates_file_not_found(context: Any) -> None:
|
|
"""
|
|
Verify that the error indicates file not found.
|
|
|
|
This checks file not found error handling.
|
|
"""
|
|
assert context.load_error is not None, "Error should exist"
|
|
error_msg = str(context.load_error).lower()
|
|
# Check for file not found indicators
|
|
assert (
|
|
"not found" in error_msg
|
|
or "no such file" in error_msg
|
|
or "file not found" in error_msg
|
|
or "filenotfound" in error_msg
|
|
), f"Error should indicate file not found, got: {error_msg}"
|
|
|
|
|
|
@then("the error should indicate network or connection issues")
|
|
def step_error_indicates_network_issues(context: Any) -> None:
|
|
"""
|
|
Verify that the error indicates network or connection issues.
|
|
|
|
This checks network error handling.
|
|
"""
|
|
assert context.load_error is not None, "Error should exist"
|
|
error_msg = str(context.load_error).lower()
|
|
# Check for network-related error indicators
|
|
assert (
|
|
"network" in error_msg
|
|
or "connection" in error_msg
|
|
or "timeout" in error_msg
|
|
or "unreachable" in error_msg
|
|
or "404" in error_msg
|
|
or "not found" in error_msg
|
|
), f"Error should indicate network issues, got: {error_msg}"
|
|
|
|
|
|
@then("the file handler should process the request")
|
|
def step_file_handler_processes(context: Any) -> None:
|
|
"""
|
|
Verify that the file handler processed the request.
|
|
|
|
This checks Chain of Responsibility traversal.
|
|
"""
|
|
assert context.loaded_graph is not None, "Graph should be loaded by file handler"
|
|
assert len(context.loaded_graph) > 0, "Graph should contain data"
|
|
|
|
|
|
@then("the handler chain should be traversed correctly")
|
|
def step_handler_chain_traversed(context: Any) -> None:
|
|
"""
|
|
Verify that the handler chain was traversed correctly.
|
|
|
|
This checks Chain of Responsibility pattern implementation.
|
|
"""
|
|
assert context.loaded_graph is not None, "Graph should be loaded"
|
|
# The chain traversal is implicit in successful loading
|
|
|
|
|
|
@then("the URI should be remapped according to the rule")
|
|
def step_uri_remapped(context: Any) -> None:
|
|
"""
|
|
Verify that the URI was remapped according to the rule.
|
|
|
|
This checks URI remapping functionality.
|
|
"""
|
|
# Remapping is handled internally, so we verify the handler was used
|
|
assert hasattr(context, "remapper_handler"), "Remapper handler should be configured"
|
|
|
|
|
|
@then("the remapped URI should be used for loading")
|
|
def step_remapped_uri_used(context: Any) -> None:
|
|
"""
|
|
Verify that the remapped URI was used for loading.
|
|
|
|
This checks that remapping was applied.
|
|
"""
|
|
# Remapping is handled internally, so we verify the handler was configured
|
|
assert hasattr(context, "remapper_handler"), "Remapper handler should be configured"
|
|
|
|
|
|
@then("the handler should be inserted at the beginning")
|
|
def step_handler_inserted_beginning(context: Any) -> None:
|
|
"""
|
|
Verify that the handler was inserted at the beginning of the chain.
|
|
|
|
This checks handler insertion by position by verifying:
|
|
- The handler chain exists
|
|
- The new handler is at position 0 (beginning of chain)
|
|
"""
|
|
assert context.graph_loader.graph_loaders_chain is not None, "Handler chain should exist"
|
|
assert hasattr(context, "new_handler"), "new_handler must exist"
|
|
assert context.new_handler is not None, "new_handler must not be None"
|
|
|
|
# Verify the handler is at position 0 (beginning)
|
|
first_handler = context.graph_loader.graph_loaders_chain
|
|
assert first_handler.get_id() == context.new_handler.get_id(), (
|
|
f"Handler should be at the beginning (position 0), but first handler has ID: {first_handler.get_id()}, "
|
|
f"expected: {context.new_handler.get_id()}"
|
|
)
|
|
|
|
|
|
@then("the chain should maintain correct order")
|
|
def step_chain_maintains_order(context: Any) -> None:
|
|
"""
|
|
Verify that the handler chain maintains correct order.
|
|
|
|
This checks chain integrity after insertion by verifying:
|
|
- The handler chain exists
|
|
- The new handler is in the correct position (already verified by previous step)
|
|
- All handlers appear exactly once (no duplicates)
|
|
- The relative order of original handlers is preserved
|
|
- The chain is properly linked (each handler has a next handler except the last)
|
|
"""
|
|
assert context.graph_loader.graph_loaders_chain is not None, "Handler chain should exist"
|
|
assert hasattr(context, "new_handler"), "new_handler must exist"
|
|
assert context.new_handler is not None, "new_handler must not be None"
|
|
|
|
# Collect all handler IDs in order
|
|
handler = context.graph_loader.graph_loaders_chain
|
|
handler_ids = []
|
|
handler_instances = []
|
|
while handler:
|
|
handler_ids.append(handler.get_id())
|
|
handler_instances.append(handler)
|
|
handler = handler.get_next()
|
|
|
|
# Chain should have at least one handler
|
|
assert len(handler_ids) > 0, "Chain should contain at least one handler"
|
|
|
|
# Verify no duplicate handlers (each handler instance should appear only once)
|
|
handler_instance_set = set(id(h) for h in handler_instances)
|
|
assert len(handler_instance_set) == len(handler_instances), (
|
|
f"Chain should not contain duplicate handlers. Found {len(handler_instances)} handlers but "
|
|
f"{len(handler_instance_set)} unique instances. Handler IDs in order: {handler_ids}"
|
|
)
|
|
|
|
# Verify the new handler instance appears exactly once (by object identity, not ID)
|
|
new_handler_id = context.new_handler.get_id()
|
|
new_handler_instance_count = sum(1 for h in handler_instances if h is context.new_handler)
|
|
assert new_handler_instance_count == 1, (
|
|
f"New handler instance should appear exactly once in chain, "
|
|
f"but found {new_handler_instance_count} times. Handler IDs in order: {handler_ids}"
|
|
)
|
|
|
|
# Find the position of the new handler instance (by object identity)
|
|
new_handler_position = next((i for i, h in enumerate(handler_instances) if h is context.new_handler), None)
|
|
assert new_handler_position is not None, (
|
|
f"New handler instance should be found in chain. Handler IDs in order: {handler_ids}"
|
|
)
|
|
|
|
# Check if we have context about where it should be
|
|
if hasattr(context, "handler_id"):
|
|
# Inserted after handler_id - verify the new handler comes after the target
|
|
target_handler_id = context.handler_id
|
|
assert target_handler_id in handler_ids, (
|
|
f"Target handler with ID '{target_handler_id}' should exist in chain. Handler IDs in order: {handler_ids}"
|
|
)
|
|
target_position = handler_ids.index(target_handler_id)
|
|
assert new_handler_position == target_position + 1, (
|
|
f"New handler should be immediately after target handler '{target_handler_id}' "
|
|
f"(at position {target_position + 1}), but found at position {new_handler_position}. "
|
|
f"Handler IDs in order: {handler_ids}"
|
|
)
|
|
else:
|
|
# Likely inserted at beginning (position 0) - verify it's first
|
|
# This is a reasonable assumption based on the test scenarios
|
|
assert new_handler_position == 0, (
|
|
f"New handler should be at the beginning (position 0) when no target handler_id is specified, "
|
|
f"but found at position {new_handler_position}. Handler IDs in order: {handler_ids}"
|
|
)
|
|
|
|
# Verify original default handlers maintain their relative order
|
|
# Default order: DefaultGraphLoaderFileHandler -> DefaultGraphLoaderHTTPHandler
|
|
# This means FileHandler should always come before HTTPHandler in the chain
|
|
# (allowing for other handlers to be inserted between them)
|
|
default_file_handler_id = "DefaultGraphLoaderFileHandler"
|
|
default_http_handler_id = "DefaultGraphLoaderHTTPHandler"
|
|
|
|
if default_file_handler_id in handler_ids and default_http_handler_id in handler_ids:
|
|
file_handler_pos = handler_ids.index(default_file_handler_id)
|
|
http_handler_pos = handler_ids.index(default_http_handler_id)
|
|
# FileHandler should come before HTTPHandler to maintain original relative order
|
|
assert file_handler_pos < http_handler_pos, (
|
|
f"Original handler relative order is broken: '{default_http_handler_id}' (position {http_handler_pos}) "
|
|
f"comes before '{default_file_handler_id}' (position {file_handler_pos}). "
|
|
f"The original order should be FileHandler -> HTTPHandler. "
|
|
f"Handler IDs in order: {handler_ids}"
|
|
)
|
|
|
|
|
|
@then("the handler should be inserted after the specified handler")
|
|
def step_handler_inserted_after(context: Any) -> None:
|
|
"""
|
|
Verify that the handler was inserted after the specified handler.
|
|
|
|
This checks handler insertion by ID by verifying:
|
|
- The handler chain exists
|
|
- The target handler (context.handler_id) exists
|
|
- The new handler is immediately after the target handler
|
|
"""
|
|
assert context.graph_loader.graph_loaders_chain is not None, "Handler chain should exist"
|
|
assert hasattr(context, "handler_id"), "handler_id must exist"
|
|
assert hasattr(context, "new_handler"), "new_handler must exist"
|
|
assert context.handler_id is not None, "handler_id must not be None"
|
|
assert context.new_handler is not None, "new_handler must not be None"
|
|
|
|
# Verify the handler is after the specified handler by traversing the chain
|
|
handler = context.graph_loader.graph_loaders_chain
|
|
found_target = False
|
|
found_new = False
|
|
while handler:
|
|
if handler.get_id() == context.handler_id:
|
|
found_target = True
|
|
# Check if the next handler is our new handler
|
|
next_handler = handler.get_next()
|
|
if next_handler and next_handler.get_id() == context.new_handler.get_id():
|
|
found_new = True
|
|
break
|
|
handler = handler.get_next()
|
|
|
|
assert found_target, f"Target handler with ID {context.handler_id} should exist in chain"
|
|
assert found_new, (
|
|
f"New handler with ID {context.new_handler.get_id()} should be inserted immediately after "
|
|
f"handler with ID {context.handler_id}"
|
|
)
|
|
|
|
|
|
@then("the chain should contain only the new handler")
|
|
def step_chain_contains_only_new(context: Any) -> None:
|
|
"""
|
|
Verify that the chain contains only the new handler.
|
|
|
|
This checks chain reset functionality by verifying:
|
|
- The handler chain exists
|
|
- The chain contains only the new handler (no next handler)
|
|
- Handler ID matches the new handler
|
|
"""
|
|
assert context.graph_loader.graph_loaders_chain is not None, "Handler chain should exist"
|
|
assert hasattr(context, "new_handler"), "new_handler must exist"
|
|
assert context.new_handler is not None, "new_handler must not be None"
|
|
|
|
# Verify the chain contains only the new handler
|
|
first_handler = context.graph_loader.graph_loaders_chain
|
|
assert first_handler.get_id() == context.new_handler.get_id(), (
|
|
f"Chain should contain only the new handler, but first handler has ID: {first_handler.get_id()}, "
|
|
f"expected: {context.new_handler.get_id()}"
|
|
)
|
|
|
|
# Verify there are no additional handlers in the chain
|
|
next_handler = first_handler.get_next()
|
|
assert next_handler is None, (
|
|
f"Chain should contain only the new handler, but found additional handler with ID: {next_handler.get_id()}"
|
|
)
|
|
|
|
|
|
@then("previous handlers should be removed")
|
|
def step_previous_handlers_removed(context: Any) -> None:
|
|
"""
|
|
Verify that previous handlers were removed.
|
|
|
|
This checks chain reset functionality by verifying:
|
|
- The handler chain exists
|
|
- The chain contains only the new handler (no next handler)
|
|
- No other handlers remain in the chain
|
|
"""
|
|
assert context.graph_loader.graph_loaders_chain is not None, "Handler chain should exist"
|
|
assert hasattr(context, "new_handler"), "new_handler must exist"
|
|
assert context.new_handler is not None, "new_handler must not be None"
|
|
|
|
# Verify the chain contains only the new handler (no next handler)
|
|
first_handler = context.graph_loader.graph_loaders_chain
|
|
assert first_handler.get_id() == context.new_handler.get_id(), (
|
|
f"Chain should contain only the new handler, but first handler has ID: {first_handler.get_id()}, "
|
|
f"expected: {context.new_handler.get_id()}"
|
|
)
|
|
|
|
# Verify there are no additional handlers in the chain
|
|
next_handler = first_handler.get_next()
|
|
assert next_handler is None, (
|
|
f"Previous handlers should be removed, but found additional handler with ID: {next_handler.get_id()}"
|
|
)
|
|
|
|
|
|
@when("I load a graph with a non-HTTP scheme using HTTP handler")
|
|
def step_load_graph_non_http_scheme(context: Any) -> None:
|
|
"""
|
|
Load a graph with a non-HTTP scheme using the HTTP handler.
|
|
|
|
This tests the fall-through behavior when the handler can't handle the scheme.
|
|
"""
|
|
# Create an HTTP handler and try to load a file:// URI
|
|
# The handler should fall through to the next handler
|
|
handler = DefaultGraphLoaderHTTPHandler()
|
|
# Insert it into the chain
|
|
context.graph_loader.insert_handler_in_chain(handler, 0)
|
|
# Try to load a file:// URI - the HTTP handler should fall through
|
|
try:
|
|
context.loaded_graph = context.graph_loader.load_from_uri(context.test_file_path)
|
|
context.load_error = None
|
|
except Exception as e:
|
|
context.load_error = e
|
|
context.loaded_graph = None
|
|
|
|
|
|
@then("the HTTP handler should fall through to the next handler")
|
|
def step_http_handler_falls_through(context: Any) -> None:
|
|
"""
|
|
Verify that the HTTP handler falls through to the next handler.
|
|
|
|
This checks that handlers correctly delegate when they can't handle a scheme.
|
|
"""
|
|
# The handler should fall through, so loading should succeed via the file handler
|
|
assert context.load_error is None, f"Handler should fall through, got error: {context.load_error}"
|
|
assert context.loaded_graph is not None, "Graph should be loaded by next handler"
|
|
|
|
|
|
@then("the graph should be loaded successfully")
|
|
def step_graph_loaded_successfully(context: Any) -> None:
|
|
"""
|
|
Verify that the graph was loaded successfully.
|
|
|
|
This checks that the graph loading completed without errors.
|
|
"""
|
|
assert context.loaded_graph is not None, "Graph should be loaded"
|
|
assert len(context.loaded_graph) > 0, "Graph should not be empty"
|
|
|
|
|
|
@given("I have a handler with no next handler")
|
|
def step_have_handler_no_next(context: Any) -> None:
|
|
"""
|
|
Create a handler with no next handler in the chain.
|
|
|
|
This sets up testing for chain termination error.
|
|
"""
|
|
from cleverrdf_lib.utils.default_graph_loader_http_handler import DefaultGraphLoaderHTTPHandler
|
|
|
|
context.isolated_handler = DefaultGraphLoaderHTTPHandler()
|
|
# Ensure no next handler is set
|
|
context.isolated_handler._next_handler = None
|
|
|
|
|
|
@when("I attempt to load with a handler that cannot handle the URI")
|
|
def step_attempt_load_with_unhandled_uri(context: Any) -> None:
|
|
"""
|
|
Attempt to load with a handler that cannot handle the URI and has no next handler.
|
|
|
|
This tests chain termination error handling.
|
|
"""
|
|
# Try to load a file:// URI with an HTTP handler that has no next handler
|
|
# This should trigger the exception in GraphLoaderBaseHandler.handle_graph_load
|
|
try:
|
|
context.isolated_handler.handle_graph_load("file", "/path/to/file.rdf", "file:///path/to/file.rdf", None)
|
|
context.load_exception = None
|
|
except OntologyLoaderFailedException as e:
|
|
context.load_exception = e
|
|
except Exception as e:
|
|
context.load_exception = e
|
|
|
|
|
|
@then("a handler chain exception should be raised")
|
|
def step_handler_chain_exception_raised(context: Any) -> None:
|
|
"""
|
|
Verify that OntologyLoaderFailedException was raised for handler chain termination.
|
|
|
|
This checks exception handling for chain termination.
|
|
"""
|
|
assert context.load_exception is not None, "Exception should be raised"
|
|
assert isinstance(context.load_exception, OntologyLoaderFailedException), (
|
|
f"Should be OntologyLoaderFailedException, got {type(context.load_exception).__name__}"
|
|
)
|
|
|
|
|
|
@given("I have an HTTP URL with Content-Type header")
|
|
def step_have_http_url_with_content_type(context: Any) -> None:
|
|
"""
|
|
Create an HTTP URL with a Content-Type header for format detection testing.
|
|
|
|
This sets up a mock HTTP response with a Content-Type header.
|
|
"""
|
|
from pathlib import Path
|
|
from unittest.mock import Mock
|
|
|
|
from features.steps.common_steps import step_have_test_data_directory
|
|
|
|
if not hasattr(context, "test_data_dir"):
|
|
step_have_test_data_directory(context)
|
|
|
|
# Create a test RDF file
|
|
test_file = Path(context.test_data_dir) / "content_type_test.ttl"
|
|
test_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
from rdflib import Graph, Literal, Namespace
|
|
from rdflib.namespace import RDF, RDFS
|
|
|
|
TEST_NS = Namespace("http://example.org/test#")
|
|
|
|
graph = Graph()
|
|
graph.bind("test", TEST_NS)
|
|
graph.bind("rdf", RDF)
|
|
graph.bind("rdfs", RDFS)
|
|
|
|
resource = TEST_NS.Resource1
|
|
graph.add((resource, RDF.type, RDFS.Resource))
|
|
graph.add((resource, RDFS.label, Literal("Test Resource")))
|
|
|
|
graph.serialize(destination=str(test_file), format="turtle")
|
|
|
|
# Mock requests.get to return a response with Content-Type header
|
|
mock_response = Mock()
|
|
mock_response.text = test_file.read_text()
|
|
mock_response.headers = {"Content-Type": "text/turtle; charset=utf-8"}
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
context.mock_response = mock_response
|
|
context.test_http_url = "http://example.com/test.ttl"
|
|
context.expected_format = "turtle"
|
|
|
|
|
|
@when("I load the graph from the HTTP URL without format")
|
|
def step_load_graph_from_http_without_format(context: Any) -> None:
|
|
"""
|
|
Load a graph from an HTTP URL without specifying format.
|
|
|
|
This tests format detection from Content-Type header.
|
|
"""
|
|
from unittest.mock import patch
|
|
|
|
from cleverrdf_lib.utils.graph_loader import GraphLoader
|
|
|
|
loader = GraphLoader()
|
|
|
|
with patch("requests.get", return_value=context.mock_response):
|
|
try:
|
|
context.loaded_graph = loader.load_from_uri(context.test_http_url)
|
|
context.load_error = None
|
|
except Exception as e:
|
|
context.load_error = e
|
|
context.loaded_graph = None
|
|
|
|
|
|
@then("the format should be detected from Content-Type")
|
|
def step_format_detected_from_content_type(context: Any) -> None:
|
|
"""
|
|
Verify that the format was detected from the Content-Type header.
|
|
|
|
This checks format detection functionality.
|
|
"""
|
|
# The format detection happens internally, so we verify the graph was loaded
|
|
assert context.loaded_graph is not None, "Graph should be loaded"
|
|
assert context.load_error is None, f"Loading should succeed, got: {context.load_error}"
|
|
|
|
|
|
@then("the graph should be parsed with the detected format")
|
|
def step_graph_parsed_with_detected_format(context: Any) -> None:
|
|
"""
|
|
Verify that the graph was parsed with the detected format.
|
|
|
|
This checks that format detection works correctly.
|
|
"""
|
|
assert context.loaded_graph is not None, "Graph should be loaded"
|
|
assert len(context.loaded_graph) > 0, "Graph should not be empty"
|
|
|
|
|
|
@when("I insert the handler at the end of the chain")
|
|
def step_insert_handler_at_end(context: Any) -> None:
|
|
"""
|
|
Insert a handler at the end of the chain (position None).
|
|
|
|
This tests handler insertion with position=None.
|
|
"""
|
|
# Insert at end by passing None as position
|
|
context.graph_loader.insert_handler_in_chain(context.new_handler, None)
|
|
|
|
|
|
@then("the handler should be inserted at the end")
|
|
def step_handler_inserted_at_end(context: Any) -> None:
|
|
"""
|
|
Verify that the handler was inserted at the end of the chain.
|
|
|
|
This checks insertion at the end.
|
|
"""
|
|
# Verify the handler is at the end by traversing the chain
|
|
handler = context.graph_loader.graph_loaders_chain
|
|
while handler.get_next():
|
|
handler = handler.get_next()
|
|
assert handler.get_id() == context.new_handler.get_id(), "Handler should be at the end of the chain"
|
|
|
|
|
|
@when("I insert the handler at integer position {position:d}")
|
|
def step_insert_handler_at_integer_position(context: Any, position: int) -> None:
|
|
"""
|
|
Insert a handler at a specific integer position.
|
|
|
|
This tests handler insertion with integer position.
|
|
"""
|
|
context.graph_loader.insert_handler_in_chain(context.new_handler, position)
|
|
|
|
|
|
@then("the handler should be inserted at position {position:d}")
|
|
def step_handler_inserted_at_position(context: Any, position: int) -> None:
|
|
"""
|
|
Verify that the handler was inserted at the specified position.
|
|
|
|
This checks insertion at a specific position.
|
|
"""
|
|
# Verify the handler is at the correct position by traversing the chain
|
|
handler = context.graph_loader.graph_loaders_chain
|
|
idx = 0
|
|
found = False
|
|
while handler:
|
|
if idx == position and handler.get_id() == context.new_handler.get_id():
|
|
found = True
|
|
break
|
|
handler = handler.get_next()
|
|
idx += 1
|
|
assert found, f"Handler should be at position {position}"
|
|
|
|
|
|
@when('I insert the handler after handler with ID "{handler_id}"')
|
|
def step_insert_handler_after_id(context: Any, handler_id: str) -> None:
|
|
"""
|
|
Insert a handler after a handler with a specific ID.
|
|
|
|
This tests handler insertion with string position (handler ID).
|
|
"""
|
|
context.graph_loader.insert_handler_in_chain(context.new_handler, handler_id)
|
|
|
|
|
|
@then('the handler should be inserted after handler with ID "{handler_id}"')
|
|
def step_handler_inserted_after_id(context: Any, handler_id: str) -> None:
|
|
"""
|
|
Verify that the handler was inserted after the handler with the specified ID.
|
|
|
|
This checks insertion after a specific handler ID.
|
|
"""
|
|
# Verify the handler is after the specified handler by traversing the chain
|
|
handler = context.graph_loader.graph_loaders_chain
|
|
found_target = False
|
|
found_new = False
|
|
while handler:
|
|
if handler.get_id() == handler_id:
|
|
found_target = True
|
|
# Check if the next handler is our new handler
|
|
if handler.get_next() and handler.get_next().get_id() == context.new_handler.get_id():
|
|
found_new = True
|
|
break
|
|
handler = handler.get_next()
|
|
assert found_target, f"Target handler with ID {handler_id} should exist"
|
|
assert found_new, f"New handler should be inserted after handler with ID {handler_id}"
|
|
|
|
|
|
# Removed duplicate - using the ones defined earlier
|