nat-engine 1__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.
- mannf/__init__.py +33 -0
- mannf/__main__.py +10 -0
- mannf/_version.py +8 -0
- mannf/agents/__init__.py +7 -0
- mannf/agents/analyzer_agent.py +9 -0
- mannf/agents/base.py +9 -0
- mannf/agents/bdi_agent.py +9 -0
- mannf/agents/belief_state.py +9 -0
- mannf/agents/coordinator_agent.py +9 -0
- mannf/agents/executor_agent.py +9 -0
- mannf/agents/monitor_agent.py +9 -0
- mannf/agents/oracle_agent.py +9 -0
- mannf/agents/planner_agent.py +9 -0
- mannf/agents/test_agent.py +9 -0
- mannf/anomaly/__init__.py +7 -0
- mannf/anomaly/enhanced_detector.py +9 -0
- mannf/cli.py +9 -0
- mannf/core/__init__.py +26 -0
- mannf/core/agents/__init__.py +52 -0
- mannf/core/agents/accessibility_scanner_agent.py +245 -0
- mannf/core/agents/analyzer_agent.py +224 -0
- mannf/core/agents/autonomous_loop_agent.py +1086 -0
- mannf/core/agents/autonomous_loop_models.py +62 -0
- mannf/core/agents/autonomous_run_differ.py +427 -0
- mannf/core/agents/base.py +128 -0
- mannf/core/agents/bdi_agent.py +330 -0
- mannf/core/agents/belief_state.py +202 -0
- mannf/core/agents/browser_coordinator_agent.py +224 -0
- mannf/core/agents/browser_executor_agent.py +410 -0
- mannf/core/agents/coordinator_agent.py +262 -0
- mannf/core/agents/executor_agent.py +222 -0
- mannf/core/agents/monitor_agent.py +188 -0
- mannf/core/agents/oracle_agent.py +150 -0
- mannf/core/agents/performance_testing_agent.py +279 -0
- mannf/core/agents/planner_agent.py +128 -0
- mannf/core/agents/test_agent.py +249 -0
- mannf/core/agents/visual_regression_agent.py +311 -0
- mannf/core/agents/web_crawler_agent.py +510 -0
- mannf/core/agents/worker_pool.py +366 -0
- mannf/core/anomaly/__init__.py +14 -0
- mannf/core/anomaly/enhanced_detector.py +541 -0
- mannf/core/browser/__init__.py +63 -0
- mannf/core/browser/accessibility_scanner.py +424 -0
- mannf/core/browser/discovery_model.py +178 -0
- mannf/core/browser/dom_snapshot.py +349 -0
- mannf/core/browser/ingestor_bridge.py +371 -0
- mannf/core/browser/performance_metrics.py +217 -0
- mannf/core/browser/reflection_analyzer.py +442 -0
- mannf/core/browser/scenario_generator.py +1100 -0
- mannf/core/browser/security_scenario_generator.py +695 -0
- mannf/core/browser/visual_comparer.py +159 -0
- mannf/core/diagnostics/__init__.py +28 -0
- mannf/core/diagnostics/failure_clusterer.py +211 -0
- mannf/core/diagnostics/flake_detector.py +233 -0
- mannf/core/diagnostics/root_cause_analyzer.py +273 -0
- mannf/core/distributed/__init__.py +16 -0
- mannf/core/distributed/endpoint.py +139 -0
- mannf/core/distributed/system_under_test.py +207 -0
- mannf/core/functional_orchestrator.py +428 -0
- mannf/core/messaging/__init__.py +11 -0
- mannf/core/messaging/bus.py +113 -0
- mannf/core/messaging/messages.py +89 -0
- mannf/core/nat_orchestrator.py +342 -0
- mannf/core/neural/__init__.py +183 -0
- mannf/core/orchestrator.py +272 -0
- mannf/core/prioritization/__init__.py +17 -0
- mannf/core/prioritization/adaptive_controller.py +509 -0
- mannf/core/prioritization/belief_prioritizer.py +231 -0
- mannf/core/prioritization/risk_scorer.py +430 -0
- mannf/core/reporting/__init__.py +12 -0
- mannf/core/reporting/unified_report.py +664 -0
- mannf/core/testing/__init__.py +17 -0
- mannf/core/testing/adaptive_controller.py +149 -0
- mannf/core/testing/models.py +179 -0
- mannf/core/validation/__init__.py +10 -0
- mannf/core/validation/self_validation_runner.py +180 -0
- mannf/dashboard/__init__.py +7 -0
- mannf/dashboard/app.py +9 -0
- mannf/dashboard/models.py +9 -0
- mannf/dashboard/static/index.html +2538 -0
- mannf/dashboard/telemetry.py +9 -0
- mannf/distributed/__init__.py +7 -0
- mannf/distributed/endpoint.py +9 -0
- mannf/distributed/system_under_test.py +9 -0
- mannf/healing/__init__.py +7 -0
- mannf/healing/graphql_schema_diff.py +9 -0
- mannf/healing/healer.py +9 -0
- mannf/healing/models.py +9 -0
- mannf/healing/schema_diff.py +9 -0
- mannf/integrations/__init__.py +7 -0
- mannf/integrations/auth.py +9 -0
- mannf/integrations/graphql_parser.py +9 -0
- mannf/integrations/graphql_sut.py +9 -0
- mannf/integrations/http_sut.py +9 -0
- mannf/integrations/openapi_parser.py +9 -0
- mannf/integrations/postman_parser.py +9 -0
- mannf/llm/__init__.py +7 -0
- mannf/llm/anthropic_provider.py +9 -0
- mannf/llm/base.py +9 -0
- mannf/llm/config.py +9 -0
- mannf/llm/factory.py +9 -0
- mannf/llm/openai_provider.py +9 -0
- mannf/llm/prompts.py +9 -0
- mannf/messaging/__init__.py +7 -0
- mannf/messaging/bus.py +9 -0
- mannf/messaging/messages.py +9 -0
- mannf/nat_orchestrator.py +9 -0
- mannf/neural/__init__.py +7 -0
- mannf/orchestrator.py +9 -0
- mannf/prioritization/__init__.py +7 -0
- mannf/prioritization/adaptive_controller.py +9 -0
- mannf/prioritization/belief_prioritizer.py +9 -0
- mannf/prioritization/risk_scorer.py +9 -0
- mannf/product/__init__.py +29 -0
- mannf/product/admin/__init__.py +3 -0
- mannf/product/admin/routes.py +514 -0
- mannf/product/auth/__init__.py +5 -0
- mannf/product/auth/saml.py +212 -0
- mannf/product/billing/__init__.py +5 -0
- mannf/product/billing/audit.py +160 -0
- mannf/product/billing/feature_gates.py +180 -0
- mannf/product/billing/metering.py +179 -0
- mannf/product/billing/notifications.py +181 -0
- mannf/product/billing/plans.py +133 -0
- mannf/product/billing/rate_limits.py +35 -0
- mannf/product/billing/stripe_billing.py +906 -0
- mannf/product/billing/tenant_auth.py +233 -0
- mannf/product/billing/tenant_manager.py +873 -0
- mannf/product/cli.py +3900 -0
- mannf/product/cli_admin.py +408 -0
- mannf/product/dashboard/__init__.py +61 -0
- mannf/product/dashboard/app.py +3567 -0
- mannf/product/dashboard/models.py +460 -0
- mannf/product/dashboard/static/index.html +6347 -0
- mannf/product/dashboard/static/manifest.json +25 -0
- mannf/product/dashboard/static/pwa-icon-192.png +0 -0
- mannf/product/dashboard/static/pwa-icon-512.png +0 -0
- mannf/product/dashboard/static/sw.js +64 -0
- mannf/product/dashboard/telemetry.py +547 -0
- mannf/product/database.py +145 -0
- mannf/product/demo.py +844 -0
- mannf/product/doctor.py +509 -0
- mannf/product/exporters/__init__.py +65 -0
- mannf/product/exporters/azuredevops_exporter.py +257 -0
- mannf/product/exporters/base.py +307 -0
- mannf/product/exporters/bugzilla_exporter.py +200 -0
- mannf/product/exporters/dedup.py +275 -0
- mannf/product/exporters/finding_adapter.py +216 -0
- mannf/product/exporters/github_exporter.py +197 -0
- mannf/product/exporters/gitlab_exporter.py +215 -0
- mannf/product/exporters/jira_exporter.py +180 -0
- mannf/product/exporters/linear_exporter.py +195 -0
- mannf/product/exporters/loader.py +233 -0
- mannf/product/exporters/pagerduty_exporter.py +363 -0
- mannf/product/exporters/sentry_exporter.py +322 -0
- mannf/product/exporters/servicenow_exporter.py +240 -0
- mannf/product/exporters/shortcut_exporter.py +231 -0
- mannf/product/exporters/webhook_exporter.py +383 -0
- mannf/product/formatters/__init__.py +18 -0
- mannf/product/formatters/allure_formatter.py +161 -0
- mannf/product/formatters/ctrf_formatter.py +149 -0
- mannf/product/healing/__init__.py +30 -0
- mannf/product/healing/graphql_schema_diff.py +152 -0
- mannf/product/healing/healer.py +141 -0
- mannf/product/healing/models.py +175 -0
- mannf/product/healing/schema_diff.py +251 -0
- mannf/product/ingestors/__init__.py +77 -0
- mannf/product/ingestors/base.py +256 -0
- mannf/product/ingestors/bgstm_ingestor.py +764 -0
- mannf/product/ingestors/curl_ingestor.py +1019 -0
- mannf/product/ingestors/cypress_ingestor.py +487 -0
- mannf/product/ingestors/gherkin_ingestor.py +967 -0
- mannf/product/ingestors/graphql_ingestor.py +845 -0
- mannf/product/ingestors/grpc_ingestor.py +591 -0
- mannf/product/ingestors/har_ingestor.py +976 -0
- mannf/product/ingestors/loader.py +284 -0
- mannf/product/ingestors/models.py +146 -0
- mannf/product/ingestors/openapi_ingestor.py +606 -0
- mannf/product/ingestors/playwright_ingestor.py +449 -0
- mannf/product/ingestors/postman_ingestor.py +631 -0
- mannf/product/ingestors/traffic_ingestor.py +679 -0
- mannf/product/ingestors/websocket_ingestor.py +526 -0
- mannf/product/integrations/__init__.py +21 -0
- mannf/product/integrations/auth.py +190 -0
- mannf/product/integrations/graphql_parser.py +436 -0
- mannf/product/integrations/graphql_sut.py +247 -0
- mannf/product/integrations/grpc_sut.py +469 -0
- mannf/product/integrations/http_sut.py +237 -0
- mannf/product/integrations/kafka_adapter.py +342 -0
- mannf/product/integrations/openapi_parser.py +513 -0
- mannf/product/integrations/postman_parser.py +467 -0
- mannf/product/integrations/webhook_receiver.py +344 -0
- mannf/product/integrations/websocket_sut.py +434 -0
- mannf/product/llm/__init__.py +25 -0
- mannf/product/llm/anthropic_provider.py +94 -0
- mannf/product/llm/base.py +267 -0
- mannf/product/llm/config.py +48 -0
- mannf/product/llm/factory.py +42 -0
- mannf/product/llm/openai_provider.py +93 -0
- mannf/product/llm/prompts.py +403 -0
- mannf/product/llm/root_cause_service.py +311 -0
- mannf/product/llm/test_plan_models.py +78 -0
- mannf/product/metrics.py +149 -0
- mannf/product/middleware/__init__.py +3 -0
- mannf/product/middleware/audit_middleware.py +112 -0
- mannf/product/middleware/tenant_isolation.py +114 -0
- mannf/product/models.py +347 -0
- mannf/product/notifications/__init__.py +24 -0
- mannf/product/notifications/dispatcher.py +411 -0
- mannf/product/onboarding.py +190 -0
- mannf/product/orchestration/__init__.py +39 -0
- mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
- mannf/product/orchestration/pipeline.py +401 -0
- mannf/product/orchestrator.py +987 -0
- mannf/product/orchestrator_models.py +269 -0
- mannf/product/regression/__init__.py +36 -0
- mannf/product/regression/differ.py +172 -0
- mannf/product/regression/masking.py +100 -0
- mannf/product/regression/models.py +232 -0
- mannf/product/regression/recorder.py +124 -0
- mannf/product/regression/replayer.py +168 -0
- mannf/product/reports/__init__.py +10 -0
- mannf/product/reports/pdf.py +132 -0
- mannf/product/scheduling/__init__.py +57 -0
- mannf/product/scheduling/cron_utils.py +251 -0
- mannf/product/scheduling/engine.py +473 -0
- mannf/product/scheduling/models.py +86 -0
- mannf/product/scheduling/queue.py +894 -0
- mannf/product/scheduling/store.py +235 -0
- mannf/product/security/__init__.py +21 -0
- mannf/product/security/belief_guided.py +143 -0
- mannf/product/security/checks/__init__.py +55 -0
- mannf/product/security/checks/base.py +69 -0
- mannf/product/security/checks/bfla.py +77 -0
- mannf/product/security/checks/bola.py +77 -0
- mannf/product/security/checks/bopla.py +80 -0
- mannf/product/security/checks/broken_auth.py +86 -0
- mannf/product/security/checks/graphql_security.py +299 -0
- mannf/product/security/checks/inventory.py +70 -0
- mannf/product/security/checks/misconfig.py +158 -0
- mannf/product/security/checks/resource_consumption.py +70 -0
- mannf/product/security/checks/sensitive_flows.py +80 -0
- mannf/product/security/checks/ssrf.py +101 -0
- mannf/product/security/checks/unsafe_consumption.py +120 -0
- mannf/product/security/models.py +92 -0
- mannf/product/security/plugin_loader.py +182 -0
- mannf/product/security/reporter.py +92 -0
- mannf/product/security/scanner.py +183 -0
- mannf/product/server.py +6220 -0
- mannf/product/setup_wizard.py +873 -0
- mannf/product/status.py +404 -0
- mannf/product/storage/__init__.py +10 -0
- mannf/product/storage/artifact_store.py +343 -0
- mannf/product/telemetry.py +300 -0
- mannf/product/uninstall.py +169 -0
- mannf/product/upgrade.py +139 -0
- mannf/product/weights/__init__.py +13 -0
- mannf/product/weights/blob_store.py +299 -0
- mannf/product/weights/factory.py +42 -0
- mannf/product/weights/registry.py +159 -0
- mannf/product/weights/store.py +210 -0
- mannf/regression/__init__.py +7 -0
- mannf/regression/differ.py +9 -0
- mannf/regression/masking.py +9 -0
- mannf/regression/models.py +9 -0
- mannf/regression/recorder.py +9 -0
- mannf/regression/replayer.py +9 -0
- mannf/security/__init__.py +7 -0
- mannf/security/belief_guided.py +9 -0
- mannf/security/checks/__init__.py +7 -0
- mannf/security/checks/base.py +9 -0
- mannf/security/checks/bfla.py +9 -0
- mannf/security/checks/bola.py +9 -0
- mannf/security/checks/bopla.py +9 -0
- mannf/security/checks/broken_auth.py +9 -0
- mannf/security/checks/graphql_security.py +9 -0
- mannf/security/checks/inventory.py +9 -0
- mannf/security/checks/misconfig.py +9 -0
- mannf/security/checks/resource_consumption.py +9 -0
- mannf/security/checks/sensitive_flows.py +9 -0
- mannf/security/checks/ssrf.py +9 -0
- mannf/security/checks/unsafe_consumption.py +9 -0
- mannf/security/models.py +9 -0
- mannf/security/reporter.py +9 -0
- mannf/security/scanner.py +9 -0
- mannf/server.py +9 -0
- mannf/testing/__init__.py +7 -0
- mannf/testing/adaptive_controller.py +9 -0
- mannf/testing/models.py +9 -0
- mannf/weights/__init__.py +7 -0
- mannf/weights/registry.py +9 -0
- mannf/weights/store.py +9 -0
- nat_engine-1.dist-info/METADATA +555 -0
- nat_engine-1.dist-info/RECORD +299 -0
- nat_engine-1.dist-info/WHEEL +5 -0
- nat_engine-1.dist-info/entry_points.txt +4 -0
- nat_engine-1.dist-info/licenses/LICENSE +651 -0
- nat_engine-1.dist-info/licenses/NOTICE +178 -0
- nat_engine-1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Copyright (C) 2026 Brad Guider
|
|
2
|
+
# This file is part of NAT (Neural Agent Testing Framework).
|
|
3
|
+
# Licensed under the AGPL-3.0. See LICENSE for details.
|
|
4
|
+
# Commercial licensing available — see COMMERCIAL_LICENSE.md.
|
|
5
|
+
|
|
6
|
+
"""Kafka consumer/producer test adapter.
|
|
7
|
+
|
|
8
|
+
:class:`KafkaAdapter` provides a test adapter for Apache Kafka topics that
|
|
9
|
+
implements the same ``execute()`` / ``health_check()`` interface as
|
|
10
|
+
:class:`~mannf.product.integrations.http_sut.HttpApiSUT`.
|
|
11
|
+
|
|
12
|
+
Supports:
|
|
13
|
+
|
|
14
|
+
* **Producer tests** — publish a message to a topic and verify no errors.
|
|
15
|
+
* **Consumer tests** — subscribe to a topic and verify a message arrives
|
|
16
|
+
within a configurable timeout.
|
|
17
|
+
* **Round-trip tests** — produce a message, then consume from a reply topic
|
|
18
|
+
to verify end-to-end delivery.
|
|
19
|
+
|
|
20
|
+
The adapter uses ``kafka-python`` when available. When not installed, all
|
|
21
|
+
execute() calls return a skip result so NAT continues functioning.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import logging
|
|
28
|
+
import time
|
|
29
|
+
import uuid
|
|
30
|
+
from typing import Any, Dict, List, Optional
|
|
31
|
+
|
|
32
|
+
from mannf.core.distributed.endpoint import Endpoint, EndpointRegistry
|
|
33
|
+
from mannf.core.distributed.system_under_test import SystemUnderTest
|
|
34
|
+
from mannf.core.testing.models import TestCase, TestResult
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _kafka_available() -> bool:
|
|
40
|
+
"""Return True if the ``kafka-python`` package is installed."""
|
|
41
|
+
try:
|
|
42
|
+
import kafka # noqa: F401
|
|
43
|
+
return True
|
|
44
|
+
except ImportError:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _bytes_passthrough(value: bytes) -> bytes:
|
|
49
|
+
"""Identity serializer for Kafka producers — pass bytes through unchanged."""
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class KafkaAdapter(SystemUnderTest):
|
|
54
|
+
"""Execute Kafka topic tests (produce / consume / round-trip).
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
bootstrap_servers:
|
|
59
|
+
Kafka broker address(es) (e.g. ``"localhost:9092"`` or a list).
|
|
60
|
+
timeout:
|
|
61
|
+
Per-operation timeout in seconds (default: 10).
|
|
62
|
+
consumer_group:
|
|
63
|
+
Consumer group ID (default: ``"nat-test-group-<uuid>"``)
|
|
64
|
+
to avoid interfering with production consumers.
|
|
65
|
+
producer_config:
|
|
66
|
+
Additional ``kafka-python`` producer configuration overrides.
|
|
67
|
+
consumer_config:
|
|
68
|
+
Additional ``kafka-python`` consumer configuration overrides.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
bootstrap_servers: str | list[str] = "localhost:9092",
|
|
74
|
+
timeout: float = 10.0,
|
|
75
|
+
consumer_group: Optional[str] = None,
|
|
76
|
+
producer_config: Optional[Dict[str, Any]] = None,
|
|
77
|
+
consumer_config: Optional[Dict[str, Any]] = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
if isinstance(bootstrap_servers, str):
|
|
80
|
+
self.bootstrap_servers = [bootstrap_servers]
|
|
81
|
+
else:
|
|
82
|
+
self.bootstrap_servers = list(bootstrap_servers)
|
|
83
|
+
|
|
84
|
+
self.timeout = timeout
|
|
85
|
+
self.consumer_group = consumer_group or f"nat-test-group-{uuid.uuid4().hex[:8]}"
|
|
86
|
+
self.producer_config: Dict[str, Any] = producer_config or {}
|
|
87
|
+
self.consumer_config: Dict[str, Any] = consumer_config or {}
|
|
88
|
+
|
|
89
|
+
host = self.bootstrap_servers[0].split(":")[0]
|
|
90
|
+
try:
|
|
91
|
+
port = int(self.bootstrap_servers[0].split(":")[1])
|
|
92
|
+
except (IndexError, ValueError):
|
|
93
|
+
port = 9092
|
|
94
|
+
|
|
95
|
+
self._registry = EndpointRegistry()
|
|
96
|
+
self._registry.register(
|
|
97
|
+
Endpoint(
|
|
98
|
+
name="default",
|
|
99
|
+
host=host,
|
|
100
|
+
port=port,
|
|
101
|
+
protocol="kafka",
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
# SystemUnderTest interface
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def registry(self) -> EndpointRegistry:
|
|
111
|
+
return self._registry
|
|
112
|
+
|
|
113
|
+
async def execute(self, test_case: TestCase) -> TestResult:
|
|
114
|
+
"""Execute a Kafka test described by *test_case*.
|
|
115
|
+
|
|
116
|
+
``test_case.target`` is the Kafka **topic** name.
|
|
117
|
+
|
|
118
|
+
``test_case.inputs`` may contain:
|
|
119
|
+
|
|
120
|
+
* ``action`` — one of ``"produce"`` (default), ``"consume"``,
|
|
121
|
+
``"round_trip"``.
|
|
122
|
+
* ``payload`` — dict or string to publish (for produce/round_trip).
|
|
123
|
+
* ``reply_topic`` — topic to consume from in round_trip mode.
|
|
124
|
+
* ``max_poll_records`` — max records to consume (default: 1).
|
|
125
|
+
"""
|
|
126
|
+
if not _kafka_available():
|
|
127
|
+
return TestResult(
|
|
128
|
+
test_case_id=test_case.id,
|
|
129
|
+
passed=False,
|
|
130
|
+
actual_output=None,
|
|
131
|
+
execution_time_ms=0.0,
|
|
132
|
+
error=(
|
|
133
|
+
"kafka-python package is not installed. "
|
|
134
|
+
"Install it with: pip install kafka-python"
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
action: str = str(test_case.inputs.get("action", "produce")).lower()
|
|
139
|
+
topic: str = test_case.target
|
|
140
|
+
|
|
141
|
+
if action == "produce":
|
|
142
|
+
return await self._action_produce(test_case, topic)
|
|
143
|
+
elif action == "consume":
|
|
144
|
+
return await self._action_consume(test_case, topic)
|
|
145
|
+
elif action == "round_trip":
|
|
146
|
+
return await self._action_round_trip(test_case, topic)
|
|
147
|
+
else:
|
|
148
|
+
return TestResult(
|
|
149
|
+
test_case_id=test_case.id,
|
|
150
|
+
passed=False,
|
|
151
|
+
actual_output=None,
|
|
152
|
+
execution_time_ms=0.0,
|
|
153
|
+
error=f"Unknown Kafka action: {action!r}",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
async def health_check(self) -> bool:
|
|
157
|
+
"""Check that the Kafka broker is reachable."""
|
|
158
|
+
if not _kafka_available():
|
|
159
|
+
return False
|
|
160
|
+
try:
|
|
161
|
+
from kafka import KafkaAdminClient
|
|
162
|
+
|
|
163
|
+
admin = KafkaAdminClient(
|
|
164
|
+
bootstrap_servers=self.bootstrap_servers,
|
|
165
|
+
request_timeout_ms=int(min(self.timeout, 5.0) * 1000),
|
|
166
|
+
)
|
|
167
|
+
admin.close()
|
|
168
|
+
return True
|
|
169
|
+
except Exception: # noqa: BLE001
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
# ------------------------------------------------------------------
|
|
173
|
+
# Action helpers
|
|
174
|
+
# ------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
async def _action_produce(self, test_case: TestCase, topic: str) -> TestResult:
|
|
177
|
+
"""Publish a message to *topic*."""
|
|
178
|
+
from kafka import KafkaProducer
|
|
179
|
+
from kafka.errors import KafkaError
|
|
180
|
+
|
|
181
|
+
payload = test_case.inputs.get("payload") or {}
|
|
182
|
+
if isinstance(payload, dict):
|
|
183
|
+
value_bytes = json.dumps(payload).encode("utf-8")
|
|
184
|
+
else:
|
|
185
|
+
value_bytes = str(payload).encode("utf-8")
|
|
186
|
+
|
|
187
|
+
start = time.monotonic()
|
|
188
|
+
try:
|
|
189
|
+
producer = KafkaProducer(
|
|
190
|
+
bootstrap_servers=self.bootstrap_servers,
|
|
191
|
+
value_serializer=_bytes_passthrough,
|
|
192
|
+
request_timeout_ms=int(self.timeout * 1000),
|
|
193
|
+
**self.producer_config,
|
|
194
|
+
)
|
|
195
|
+
future = producer.send(topic, value=value_bytes)
|
|
196
|
+
record_metadata = future.get(timeout=self.timeout)
|
|
197
|
+
producer.close()
|
|
198
|
+
elapsed_ms = (time.monotonic() - start) * 1000.0
|
|
199
|
+
return TestResult(
|
|
200
|
+
test_case_id=test_case.id,
|
|
201
|
+
passed=True,
|
|
202
|
+
actual_output={
|
|
203
|
+
"topic": record_metadata.topic,
|
|
204
|
+
"partition": record_metadata.partition,
|
|
205
|
+
"offset": record_metadata.offset,
|
|
206
|
+
},
|
|
207
|
+
execution_time_ms=elapsed_ms,
|
|
208
|
+
error=None,
|
|
209
|
+
)
|
|
210
|
+
except Exception as exc: # noqa: BLE001
|
|
211
|
+
elapsed_ms = (time.monotonic() - start) * 1000.0
|
|
212
|
+
return TestResult(
|
|
213
|
+
test_case_id=test_case.id,
|
|
214
|
+
passed=False,
|
|
215
|
+
actual_output=None,
|
|
216
|
+
execution_time_ms=elapsed_ms,
|
|
217
|
+
error=f"Kafka produce error: {exc}",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
async def _action_consume(self, test_case: TestCase, topic: str) -> TestResult:
|
|
221
|
+
"""Consume at least one message from *topic* within the timeout."""
|
|
222
|
+
from kafka import KafkaConsumer
|
|
223
|
+
|
|
224
|
+
max_records = int(test_case.inputs.get("max_poll_records") or 1)
|
|
225
|
+
start = time.monotonic()
|
|
226
|
+
try:
|
|
227
|
+
consumer = KafkaConsumer(
|
|
228
|
+
topic,
|
|
229
|
+
bootstrap_servers=self.bootstrap_servers,
|
|
230
|
+
group_id=self.consumer_group,
|
|
231
|
+
auto_offset_reset="latest",
|
|
232
|
+
consumer_timeout_ms=int(self.timeout * 1000),
|
|
233
|
+
max_poll_records=max_records,
|
|
234
|
+
**self.consumer_config,
|
|
235
|
+
)
|
|
236
|
+
records: list[dict] = []
|
|
237
|
+
for msg in consumer:
|
|
238
|
+
try:
|
|
239
|
+
value = json.loads(msg.value.decode("utf-8"))
|
|
240
|
+
except Exception: # noqa: BLE001
|
|
241
|
+
value = msg.value.decode("utf-8", errors="replace")
|
|
242
|
+
records.append({
|
|
243
|
+
"topic": msg.topic,
|
|
244
|
+
"partition": msg.partition,
|
|
245
|
+
"offset": msg.offset,
|
|
246
|
+
"value": value,
|
|
247
|
+
})
|
|
248
|
+
if len(records) >= max_records:
|
|
249
|
+
break
|
|
250
|
+
consumer.close()
|
|
251
|
+
elapsed_ms = (time.monotonic() - start) * 1000.0
|
|
252
|
+
|
|
253
|
+
if not records:
|
|
254
|
+
return TestResult(
|
|
255
|
+
test_case_id=test_case.id,
|
|
256
|
+
passed=False,
|
|
257
|
+
actual_output={"records": []},
|
|
258
|
+
execution_time_ms=elapsed_ms,
|
|
259
|
+
error="No messages received within timeout",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return TestResult(
|
|
263
|
+
test_case_id=test_case.id,
|
|
264
|
+
passed=True,
|
|
265
|
+
actual_output={"records": records, "count": len(records)},
|
|
266
|
+
execution_time_ms=elapsed_ms,
|
|
267
|
+
error=None,
|
|
268
|
+
)
|
|
269
|
+
except Exception as exc: # noqa: BLE001
|
|
270
|
+
elapsed_ms = (time.monotonic() - start) * 1000.0
|
|
271
|
+
return TestResult(
|
|
272
|
+
test_case_id=test_case.id,
|
|
273
|
+
passed=False,
|
|
274
|
+
actual_output=None,
|
|
275
|
+
execution_time_ms=elapsed_ms,
|
|
276
|
+
error=f"Kafka consume error: {exc}",
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
async def _action_round_trip(self, test_case: TestCase, topic: str) -> TestResult:
|
|
280
|
+
"""Produce to *topic* then consume from a reply topic."""
|
|
281
|
+
from kafka import KafkaConsumer, KafkaProducer
|
|
282
|
+
|
|
283
|
+
reply_topic = str(test_case.inputs.get("reply_topic") or f"{topic}.reply")
|
|
284
|
+
payload = test_case.inputs.get("payload") or {}
|
|
285
|
+
|
|
286
|
+
# Embed correlation ID for matching
|
|
287
|
+
correlation_id = uuid.uuid4().hex
|
|
288
|
+
if isinstance(payload, dict):
|
|
289
|
+
payload = {**payload, "_nat_correlation_id": correlation_id}
|
|
290
|
+
value_bytes = json.dumps(payload).encode("utf-8")
|
|
291
|
+
|
|
292
|
+
start = time.monotonic()
|
|
293
|
+
try:
|
|
294
|
+
producer = KafkaProducer(
|
|
295
|
+
bootstrap_servers=self.bootstrap_servers,
|
|
296
|
+
value_serializer=_bytes_passthrough,
|
|
297
|
+
request_timeout_ms=int(self.timeout * 1000),
|
|
298
|
+
**self.producer_config,
|
|
299
|
+
)
|
|
300
|
+
future = producer.send(topic, value=value_bytes)
|
|
301
|
+
future.get(timeout=self.timeout)
|
|
302
|
+
producer.close()
|
|
303
|
+
|
|
304
|
+
# Now consume from reply topic
|
|
305
|
+
consumer = KafkaConsumer(
|
|
306
|
+
reply_topic,
|
|
307
|
+
bootstrap_servers=self.bootstrap_servers,
|
|
308
|
+
group_id=f"{self.consumer_group}-reply",
|
|
309
|
+
auto_offset_reset="latest",
|
|
310
|
+
consumer_timeout_ms=int(self.timeout * 1000),
|
|
311
|
+
max_poll_records=10,
|
|
312
|
+
**self.consumer_config,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
reply_found = False
|
|
316
|
+
for msg in consumer:
|
|
317
|
+
try:
|
|
318
|
+
val = json.loads(msg.value.decode("utf-8"))
|
|
319
|
+
if isinstance(val, dict) and val.get("_nat_correlation_id") == correlation_id:
|
|
320
|
+
reply_found = True
|
|
321
|
+
break
|
|
322
|
+
except Exception: # noqa: BLE001
|
|
323
|
+
pass
|
|
324
|
+
consumer.close()
|
|
325
|
+
|
|
326
|
+
elapsed_ms = (time.monotonic() - start) * 1000.0
|
|
327
|
+
return TestResult(
|
|
328
|
+
test_case_id=test_case.id,
|
|
329
|
+
passed=reply_found,
|
|
330
|
+
actual_output={"round_trip_complete": reply_found},
|
|
331
|
+
execution_time_ms=elapsed_ms,
|
|
332
|
+
error=None if reply_found else "Reply message not received within timeout",
|
|
333
|
+
)
|
|
334
|
+
except Exception as exc: # noqa: BLE001
|
|
335
|
+
elapsed_ms = (time.monotonic() - start) * 1000.0
|
|
336
|
+
return TestResult(
|
|
337
|
+
test_case_id=test_case.id,
|
|
338
|
+
passed=False,
|
|
339
|
+
actual_output=None,
|
|
340
|
+
execution_time_ms=elapsed_ms,
|
|
341
|
+
error=f"Kafka round-trip error: {exc}",
|
|
342
|
+
)
|