"""Step definitions for domain-specific analyzers feature. Covers the four built-in analyzers (Python, Markdown, PostgreSQL, Docker Compose), the ``AnalyzerRegistry``, and protocol compliance checks. Step patterns are prefixed or worded to avoid collisions with existing step definitions in ``uko_analyzers_steps.py``. Triple-assertion steps use ``use_step_matcher("re")`` to avoid ``parse``-format ambiguity. """ from __future__ import annotations from behave import given, then, use_step_matcher, when from behave.runner import Context from cleveragents.domain.models.acms.analyzers import ( AnalyzerProtocol, AnalyzerRegistry, UKOTriple, ) from cleveragents.domain.models.acms.docker_compose_analyzer import ( DockerComposeAnalyzer, ) from cleveragents.domain.models.acms.markdown_analyzer import MarkdownAnalyzer from cleveragents.domain.models.acms.postgresql_analyzer import PostgreSQLAnalyzer from cleveragents.domain.models.acms.python_analyzer import PythonAnalyzer __all__: list[str] = [] # --------------------------------------------------------------------------- # Analyzer type mapping # --------------------------------------------------------------------------- _ANALYZER_CLASSES: dict[str, type] = { "PythonAnalyzer": PythonAnalyzer, "MarkdownAnalyzer": MarkdownAnalyzer, "PostgreSQLAnalyzer": PostgreSQLAnalyzer, "DockerComposeAnalyzer": DockerComposeAnalyzer, } _ALL_ANALYZERS = ( PythonAnalyzer, MarkdownAnalyzer, PostgreSQLAnalyzer, DockerComposeAnalyzer, ) # Sample content for each analyzer (cross-analyzer scenarios). _SAMPLE_CONTENT: dict[str, str] = { "PythonAnalyzer": "class Example:\n pass\n", "MarkdownAnalyzer": "# Title\nSome text.\n", "PostgreSQLAnalyzer": ("CREATE TABLE sample (\n id SERIAL PRIMARY KEY\n);\n"), "DockerComposeAnalyzer": "services:\n web:\n image: nginx\n", } _SAMPLE_URI = "uko://test/resource" # --------------------------------------------------------------------------- # Given steps # --------------------------------------------------------------------------- @given("a {analyzer_type} analyzer") def step_given_named_analyzer(context: Context, analyzer_type: str) -> None: cls = _ANALYZER_CLASSES.get(analyzer_type) if cls is None: raise ValueError( f"Unknown analyzer type {analyzer_type!r}. " f"Known types: {sorted(_ANALYZER_CLASSES)}" ) context.analyzer = cls() # Track all created analyzers for cross-analyzer scenarios. if not hasattr(context, "analyzers"): context.analyzers = {} context.analyzers[analyzer_type] = context.analyzer @given("a fresh analyzer registry") def step_given_fresh_registry(context: Context) -> None: context.registry = AnalyzerRegistry() @given("a fresh analyzer registry with all domain analyzers registered") def step_given_fresh_registry_with_all(context: Context) -> None: context.registry = AnalyzerRegistry() for cls in _ALL_ANALYZERS: context.registry.register(cls()) # --------------------------------------------------------------------------- # When steps # --------------------------------------------------------------------------- @when("I register all 4 domain analyzers") def step_when_register_all_four(context: Context) -> None: for cls in _ALL_ANALYZERS: context.registry.register(cls()) @when("I register a PythonAnalyzer") def step_when_register_python(context: Context) -> None: context.registry.register(PythonAnalyzer()) @when('I register a duplicate analyzer that also claims ".py"') def step_when_register_duplicate_py(context: Context) -> None: """Register a second analyzer whose supported_extensions include .py.""" class _DuplicatePyAnalyzer: @property def supported_extensions(self) -> frozenset[str]: return frozenset({".py"}) @property def domain(self) -> str: return "duplicate" def analyze(self, content: str, resource_uri: str) -> list[UKOTriple]: return [] # pragma: no cover _dup: type[AnalyzerProtocol] = _DuplicatePyAnalyzer del _dup # only used for static type-checking assertion context.registry.register(_DuplicatePyAnalyzer()) @when('I look up extension "{ext}" in the registry') def step_when_lookup_extension_in_registry(context: Context, ext: str) -> None: result = context.registry.get_for_extension(ext) context.lookup_result = result if result is not None: context.analyzer = result @when("I analyze the Python content:") def step_when_analyze_python(context: Context) -> None: context.triples = context.analyzer.analyze(context.text, _SAMPLE_URI) @when("I analyze the Markdown content:") def step_when_analyze_markdown(context: Context) -> None: context.triples = context.analyzer.analyze(context.text, _SAMPLE_URI) @when("I analyze the SQL content:") def step_when_analyze_sql(context: Context) -> None: context.triples = context.analyzer.analyze(context.text, _SAMPLE_URI) @when("I analyze the Docker Compose content:") def step_when_analyze_docker_compose(context: Context) -> None: context.triples = context.analyzer.analyze(context.text, _SAMPLE_URI) @when("each analyzer processes its sample content") def step_when_each_analyzer_processes_sample(context: Context) -> None: all_triples: list[UKOTriple] = [] for name, analyzer in context.analyzers.items(): sample = _SAMPLE_CONTENT[name] triples = analyzer.analyze(sample, _SAMPLE_URI) all_triples.extend(triples) context.triples = all_triples # --------------------------------------------------------------------------- # Then steps — protocol compliance # --------------------------------------------------------------------------- @then("the analyzer should satisfy the AnalyzerProtocol") def step_then_satisfies_protocol(context: Context) -> None: assert isinstance(context.analyzer, AnalyzerProtocol), ( f"{type(context.analyzer).__name__} does not satisfy AnalyzerProtocol" ) @then('the current analyzer domain should be "{domain}"') def step_then_current_analyzer_domain(context: Context, domain: str) -> None: actual = context.analyzer.domain assert actual == domain, f"Expected domain={domain!r}, got {actual!r}" # --------------------------------------------------------------------------- # Then steps — registry assertions # --------------------------------------------------------------------------- @then("the registry should contain {count:d} analyzers") def step_then_registry_count(context: Context, count: int) -> None: actual = len(context.registry) assert actual == count, f"Expected {count} analyzers in registry, got {actual}" @then("the extension lookup result should be None") def step_then_extension_lookup_none(context: Context) -> None: assert context.lookup_result is None, ( f"Expected None, got {context.lookup_result!r}" ) @then('the registry extensions should include "{ext}"') def step_then_registry_extensions_include(context: Context, ext: str) -> None: extensions = context.registry.list_extensions() assert ext in extensions, ( f"Extension {ext!r} not in registered extensions {extensions}" ) # --------------------------------------------------------------------------- # Then steps — triple count assertions # --------------------------------------------------------------------------- @then("no triples should be returned") def step_then_no_triples(context: Context) -> None: actual = len(context.triples) assert actual == 0, f"Expected 0 triples, got {actual}.\nTriples: {context.triples}" @then("the number of Table type triples should be {count:d}") def step_then_table_type_count(context: Context, count: int) -> None: actual = sum( 1 for t in context.triples if t.predicate == "rdf:type" and t.object_uri == "uko-data:Table" ) assert actual == count, f"Expected {count} Table type triples, got {actual}" # ========================================================================= # Switch to regex matcher for triple-assertion steps to avoid ambiguity # that behave's ``parse`` format causes when patterns share a prefix # (e.g. "{pred}" greedily consuming "… and object_uri …"). # ========================================================================= use_step_matcher("re") @then( r'the triples should include predicate "(?P[^"]+)"' r' with object_uri "(?P[^"]+)"' ) def step_then_triple_pred_obj_uri(context: Context, pred: str, obj: str) -> None: found = any(t.predicate == pred and t.object_uri == obj for t in context.triples) assert found, ( f"No triple with predicate={pred!r} object_uri={obj!r} in {context.triples}" ) @then( r'the triples should include predicate "(?P[^"]+)"' r' with object_value "(?P[^"]+)"' ) def step_then_triple_pred_obj_val(context: Context, pred: str, val: str) -> None: found = any(t.predicate == pred and t.object_value == val for t in context.triples) assert found, ( f"No triple with predicate={pred!r} object_value={val!r} in {context.triples}" ) @then( r'the triples should include predicate "(?P[^"]+)"' r' with object_uri containing "(?P[^"]+)"' ) def step_then_triple_pred_obj_uri_contains( context: Context, pred: str, fragment: str ) -> None: found = any( t.predicate == pred and t.object_uri is not None and fragment in t.object_uri for t in context.triples ) assert found, ( f"No triple with predicate={pred!r} and object_uri containing " f"{fragment!r} in {context.triples}" ) @then( r'the triples should include predicate "(?P[^"]+)"' r' linking subject "(?P[^"]+)"' r' to object "(?P[^"]+)"' ) def step_then_triple_links_subject_to_object( context: Context, pred: str, s_frag: str, o_frag: str ) -> None: found = any( t.predicate == pred and s_frag in t.subject_uri and ( (t.object_uri is not None and o_frag in t.object_uri) or o_frag in t.object_value ) for t in context.triples ) assert found, ( f"No triple with predicate={pred!r} linking subject containing " f"{s_frag!r} to object containing {o_frag!r} in {context.triples}" ) @then( r'the triples should not include predicate "(?P[^"]+)"' r' with object_uri "(?P[^"]+)"' ) def step_then_no_triple_pred_obj_uri(context: Context, pred: str, obj: str) -> None: found = any(t.predicate == pred and t.object_uri == obj for t in context.triples) assert not found, ( f"Unexpected triple with predicate={pred!r} object_uri={obj!r} " f"found in {context.triples}" ) @then( r'the triples should not include predicate "(?P[^"]+)"' r' with object_value "(?P[^"]+)"' ) def step_then_no_triple_pred_obj_val(context: Context, pred: str, val: str) -> None: found = any(t.predicate == pred and t.object_value == val for t in context.triples) assert not found, ( f"Unexpected triple with predicate={pred!r} object_value={val!r} " f"found in {context.triples}" ) @then(r'the triples should include predicate "(?P[^"]+)"') def step_then_triple_has_predicate(context: Context, pred: str) -> None: found = any(t.predicate == pred for t in context.triples) assert found, f"No triple with predicate={pred!r} in {context.triples}" # Switch back to parse matcher for remaining steps. use_step_matcher("parse") # --------------------------------------------------------------------------- # Then steps — error handling # --------------------------------------------------------------------------- @then("analyzing empty content with this analyzer should raise ValueError") def step_then_empty_content_raises(context: Context) -> None: try: context.analyzer.analyze("", _SAMPLE_URI) except ValueError: return # Expected raise AssertionError( f"{type(context.analyzer).__name__}.analyze('', ...) did not raise ValueError" ) @then("analyzing whitespace-only content with this analyzer should raise ValueError") def step_then_whitespace_only_raises(context: Context) -> None: try: context.analyzer.analyze(" \n\t ", _SAMPLE_URI) except ValueError: return # Expected raise AssertionError( f"{type(context.analyzer).__name__}.analyze(' \\n\\t ', ...) " "did not raise ValueError" ) @then("analyzing content with empty resource_uri should raise ValueError") def step_then_empty_resource_uri_raises(context: Context) -> None: # Use a minimal valid content for each analyzer domain. _domain_sample: dict[str, str] = { "python": "class X:\n pass\n", "markdown": "# Title\n", "postgresql": "CREATE TABLE t (id SERIAL);\n", "docker-compose": "services:\n web:\n image: nginx\n", } sample = _domain_sample.get(context.analyzer.domain, "content") try: context.analyzer.analyze(sample, "") except ValueError: return # Expected raise AssertionError( f"{type(context.analyzer).__name__}.analyze(..., '') did not raise ValueError" ) # --------------------------------------------------------------------------- # Then steps — URI scheme and confidence # --------------------------------------------------------------------------- @then('every triple subject_uri should start with "{prefix}"') def step_then_all_subject_uris_start_with(context: Context, prefix: str) -> None: for triple in context.triples: assert triple.subject_uri.startswith(prefix), ( f"Triple subject_uri {triple.subject_uri!r} does not start with {prefix!r}" ) @then("every triple confidence should be {score:g}") def step_then_all_confidence_scores(context: Context, score: float) -> None: for triple in context.triples: assert triple.confidence == score, ( f"Triple {triple!r} has confidence={triple.confidence}, expected {score}" )