Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions packages/keploy-framework/src/keploy_framework/recorder.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""Recording session utilities."""

import httpx
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import AsyncIterator
from types import TracebackType

import httpx


class RecordingSession:
Expand All @@ -26,7 +28,12 @@ async def __aenter__(self) -> "RecordingSession":
self.client = httpx.AsyncClient(base_url=self.api_url)
return self

async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore[no-untyped-def]
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
"""Exit recording context."""
if self.client:
await self.client.aclose()
Expand Down
36 changes: 23 additions & 13 deletions packages/keploy-framework/src/keploy_framework/test_runner.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Test runner with validation and reporting."""

import asyncio
import html
import subprocess
import json
from pathlib import Path
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from rich.console import Console
from rich.table import Table

Expand Down Expand Up @@ -72,12 +72,16 @@ async def run_all_tests(
"docker",
"run",
"--rm",
"--network", "host",
"-v", f"{self.keploy_dir.absolute()}:/keploy",
"--network",
"host",
"-v",
f"{self.keploy_dir.absolute()}:/keploy",
self.docker_image,
"test",
"-c", self.api_url,
"--delay", "5",
"-c",
self.api_url,
"--delay",
"5",
]

try:
Expand Down Expand Up @@ -164,9 +168,7 @@ def _validate_results(self, results: TestResults) -> None:
if results.is_success:
console.print("[bold green]✅ All tests passed![/bold green]")
else:
console.print(
f"[bold yellow]⚠️ {results.failed} test(s) failed[/bold yellow]"
)
console.print(f"[bold yellow]⚠️ {results.failed} test(s) failed[/bold yellow]")

def _generate_report(self, results: TestResults) -> None:
"""Generate HTML test report.
Expand All @@ -176,7 +178,15 @@ def _generate_report(self, results: TestResults) -> None:
"""
report_path = self.keploy_dir / "test-report.html"

html = f"""
# Generate table rows with proper HTML escaping
table_rows = "".join(
f"<tr><td>{html.escape(tc['name'])}</td>"
f'<td class="{html.escape(tc["status"])}">'
f"{html.escape(tc['status'])}</td></tr>"
for tc in results.test_cases
)

html_content = f"""
<!DOCTYPE html>
<html>
<head>
Expand Down Expand Up @@ -206,11 +216,11 @@ def _generate_report(self, results: TestResults) -> None:
<th>Test Name</th>
<th>Status</th>
</tr>
{"".join(f'<tr><td>{tc["name"]}</td><td class="{tc["status"]}">{tc["status"]}</td></tr>' for tc in results.test_cases)}
{table_rows}
</table>
</body>
</html>
"""

report_path.write_text(html)
report_path.write_text(html_content)
console.print(f"[bold green]📊 Report generated: {report_path}[/bold green]")
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ def __init__(
# Track label cardinality
self._label_combinations: set[tuple[str, ...]] = set()

# Track last reported values to calculate increments for Counters
self._last_request_total: dict[tuple[str, str], float] = {}
self._last_cost_total: dict[tuple[str, str], float] = {}
self._last_savings_total: dict[str, float] = {}

# Initialize Prometheus metrics
self._init_metrics()

Expand Down Expand Up @@ -254,45 +259,42 @@ def update_metrics(self) -> None:
)

if self._check_cardinality(labels_success):
# Note: Counter can only increase, so we set to total
# If request_total is a Counter, increment by the difference
# Note: Counter can only increase, so we increment by the difference
counter = self.request_total.labels(primitive_name=name, status="success")
current_value = getattr(counter, '_value', None)
if current_value is not None:
increment = throughput_metrics.total_requests - current_value.get()
if increment > 0:
counter.inc(increment)
else:
# Fallback: just inc by total_requests (first time)
counter.inc(throughput_metrics.total_requests)
# Track last reported value to calculate increment
key = (name, "success")
current_total = throughput_metrics.total_requests
last_total = self._last_request_total.get(key, 0.0)

increment = current_total - last_total
if increment > 0:
self.request_total.labels(primitive_name=name, status="success").inc(increment)
self._last_request_total[key] = current_total

# Update cost metrics
for name, cost_metrics in collector._cost_metrics.items():
for operation, cost in cost_metrics.cost_by_operation.items():
labels_cost = (name, operation)
if self._check_cardinality(labels_cost):
# Note: Counter can only increase, so we increment by the difference
counter = self.cost_total.labels(primitive_name=name, operation=operation)
current_value = getattr(counter, '_value', None)
if current_value is not None:
increment = cost - current_value.get()
if increment > 0:
counter.inc(increment)
else:
counter.inc(cost)
# Track last reported value to calculate increment
key = (name, operation)
last_cost = self._last_cost_total.get(key, 0.0)

increment = cost - last_cost
if increment > 0:
self.cost_total.labels(primitive_name=name, operation=operation).inc(
increment
)
self._last_cost_total[key] = cost

labels_savings = (name,)
if self._check_cardinality(labels_savings):
# Note: Counter can only increase, so we increment by the difference
counter = self.savings_total.labels(primitive_name=name)
current_value = getattr(counter, '_value', None)
if current_value is not None:
increment = cost_metrics.total_savings - current_value.get()
if increment > 0:
counter.inc(increment)
else:
counter.inc(cost_metrics.total_savings)
# Track last reported value to calculate increment
current_savings = cost_metrics.total_savings
last_savings = self._last_savings_total.get(name, 0.0)

increment = current_savings - last_savings
if increment > 0:
self.savings_total.labels(primitive_name=name).inc(increment)
self._last_savings_total[name] = current_savings

def export(self) -> bytes:
"""
Expand Down
12 changes: 7 additions & 5 deletions packages/tta-dev-primitives/src/tta_dev_primitives/paf_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
stored in PAFCORE.md and validates code against these immutable facts.
"""

import re
from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum
Expand Down Expand Up @@ -143,11 +144,12 @@ def _load_pafs(self) -> None:

# Parse PAF entries (e.g., "- **LANG-001**: Description")
if line.strip().startswith("- **") and current_category:
# Extract PAF ID and description
parts = line.split("**:", 1)
if len(parts) == 2:
paf_id_part = parts[0].replace("- **", "").strip()
description = parts[1].strip()
# Use regex for more robust parsing
# Pattern: "- **CATEGORY-NNN**: Description"
match = re.match(r"- \*\*([A-Z]+-\d+)\*\*:\s*(.+)", line.strip())
if match:
paf_id_part = match.group(1)
description = match.group(2)

# Check if deprecated
status = PAFStatus.ACTIVE
Expand Down
Loading
Loading