Update.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -Wall -Wextra -pedantic -std=c11 -O2
|
||||
CFLAGS += -I../src/libtikker/include -I../src/third_party -Iunit
|
||||
LDFLAGS := -L../build/lib -ltikker -lsqlite3 -lm
|
||||
|
||||
UNIT_TESTS := $(wildcard unit/test_*.c)
|
||||
UNIT_TARGETS := $(UNIT_TESTS:unit/test_%.c=unit/test_%)
|
||||
INTEGRATION_TESTS := $(wildcard integration/test_*.c)
|
||||
INTEGRATION_TARGETS := $(INTEGRATION_TESTS:integration/test_%.c=integration/test_%)
|
||||
|
||||
.PHONY: test unit integration clean help
|
||||
|
||||
test: unit integration
|
||||
@echo "✓ All tests completed"
|
||||
|
||||
unit: $(UNIT_TARGETS)
|
||||
@echo "Running unit tests..."
|
||||
@for test in $(UNIT_TARGETS); do \
|
||||
if [ -f $$test ]; then \
|
||||
$$test || exit 1; \
|
||||
fi; \
|
||||
done
|
||||
@echo "✓ Unit tests passed"
|
||||
|
||||
integration: $(INTEGRATION_TARGETS)
|
||||
@echo "Running integration tests..."
|
||||
@for test in $(INTEGRATION_TARGETS); do \
|
||||
if [ -f $$test ]; then \
|
||||
$$test || exit 1; \
|
||||
fi; \
|
||||
done
|
||||
@echo "✓ Integration tests passed"
|
||||
|
||||
unit/test_%: unit/test_%.c
|
||||
@echo "Building test: $@"
|
||||
@$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
|
||||
|
||||
integration/test_%: integration/test_%.c
|
||||
@echo "Building integration test: $@"
|
||||
@$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
@rm -f unit/test_*
|
||||
@rm -f integration/test_*
|
||||
@find . -name "*.o" -delete
|
||||
@echo "✓ Tests cleaned"
|
||||
|
||||
help:
|
||||
@echo "Test suite targets:"
|
||||
@echo " make test - Run all tests"
|
||||
@echo " make unit - Run unit tests"
|
||||
@echo " make integration - Run integration tests"
|
||||
@echo " make clean - Remove test artifacts"
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests package for Tikker services."""
|
||||
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Pytest Configuration for Service Tests
|
||||
|
||||
Provides fixtures and configuration for integration testing.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root / "src" / "api"))
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_config():
|
||||
"""Provide test configuration."""
|
||||
return {
|
||||
"api_host": "http://localhost:8000",
|
||||
"ai_host": "http://localhost:8001",
|
||||
"viz_host": "http://localhost:8002",
|
||||
"ml_host": "http://localhost:8003",
|
||||
"timeout": 30
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api_client():
|
||||
"""Create API test client."""
|
||||
try:
|
||||
from api_c_integration import app
|
||||
return TestClient(app)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load API: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ai_client():
|
||||
"""Create AI service test client."""
|
||||
try:
|
||||
from ai_service import app
|
||||
return TestClient(app)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load AI service: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def viz_client():
|
||||
"""Create visualization service test client."""
|
||||
try:
|
||||
from viz_service import app
|
||||
return TestClient(app)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load visualization service: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ml_client():
|
||||
"""Create ML service test client."""
|
||||
try:
|
||||
from ml_service import app
|
||||
return TestClient(app)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load ML service: {e}")
|
||||
return None
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
|
||||
BUILD_BIN="/home/retoor/projects/tikker/build/bin"
|
||||
|
||||
echo "=== Tikker CLI Tools Integration Tests ==="
|
||||
echo
|
||||
|
||||
passed=0
|
||||
failed=0
|
||||
|
||||
# Test 1: Decoder help
|
||||
echo -n "Testing decoder --help... "
|
||||
if $BUILD_BIN/tikker-decoder --help 2>&1 | grep -q "Usage:"; then
|
||||
echo "✓ PASS"
|
||||
((passed++))
|
||||
else
|
||||
echo "✗ FAIL"
|
||||
((failed++))
|
||||
fi
|
||||
|
||||
# Test 2: Indexer help
|
||||
echo -n "Testing indexer --help... "
|
||||
if $BUILD_BIN/tikker-indexer --help 2>&1 | grep -q "Usage:"; then
|
||||
echo "✓ PASS"
|
||||
((passed++))
|
||||
else
|
||||
echo "✗ FAIL"
|
||||
((failed++))
|
||||
fi
|
||||
|
||||
# Test 3: Aggregator help
|
||||
echo -n "Testing aggregator --help... "
|
||||
if $BUILD_BIN/tikker-aggregator --help 2>&1 | grep -q "Usage:"; then
|
||||
echo "✓ PASS"
|
||||
((passed++))
|
||||
else
|
||||
echo "✗ FAIL"
|
||||
((failed++))
|
||||
fi
|
||||
|
||||
# Test 4: Report help
|
||||
echo -n "Testing report --help... "
|
||||
if $BUILD_BIN/tikker-report --help 2>&1 | grep -q "Usage:"; then
|
||||
echo "✓ PASS"
|
||||
((passed++))
|
||||
else
|
||||
echo "✗ FAIL"
|
||||
((failed++))
|
||||
fi
|
||||
|
||||
# Test 5: Decoder exists and is executable
|
||||
echo -n "Testing decoder binary... "
|
||||
if [ -x $BUILD_BIN/tikker-decoder ]; then
|
||||
echo "✓ PASS"
|
||||
((passed++))
|
||||
else
|
||||
echo "✗ FAIL"
|
||||
((failed++))
|
||||
fi
|
||||
|
||||
# Test 6: Indexer exists and is executable
|
||||
echo -n "Testing indexer binary... "
|
||||
if [ -x $BUILD_BIN/tikker-indexer ]; then
|
||||
echo "✓ PASS"
|
||||
((passed++))
|
||||
else
|
||||
echo "✗ FAIL"
|
||||
((failed++))
|
||||
fi
|
||||
|
||||
# Test 7: Aggregator exists and is executable
|
||||
echo -n "Testing aggregator binary... "
|
||||
if [ -x $BUILD_BIN/tikker-aggregator ]; then
|
||||
echo "✓ PASS"
|
||||
((passed++))
|
||||
else
|
||||
echo "✗ FAIL"
|
||||
((failed++))
|
||||
fi
|
||||
|
||||
# Test 8: Report exists and is executable
|
||||
echo -n "Testing report binary... "
|
||||
if [ -x $BUILD_BIN/tikker-report ]; then
|
||||
echo "✓ PASS"
|
||||
((passed++))
|
||||
else
|
||||
echo "✗ FAIL"
|
||||
((failed++))
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== Test Summary ==="
|
||||
echo "Passed: $passed"
|
||||
echo "Failed: $failed"
|
||||
|
||||
if [ $failed -eq 0 ]; then
|
||||
echo "✓ All tests passed!"
|
||||
exit 0
|
||||
else
|
||||
echo "✗ Some tests failed"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,416 @@
|
||||
"""
|
||||
ML Service Tests
|
||||
|
||||
Tests for machine learning analytics endpoints.
|
||||
Covers pattern detection, anomaly detection, and behavioral analysis.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
class TestMLServiceHealth:
|
||||
"""Tests for ML service health and basic functionality."""
|
||||
|
||||
def test_ml_health_check(self, ml_client):
|
||||
"""Test ML service health check endpoint."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
response = ml_client.get("/health")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "healthy"
|
||||
assert "ml_available" in data
|
||||
|
||||
def test_ml_root_endpoint(self, ml_client):
|
||||
"""Test ML service root endpoint."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
response = ml_client.get("/")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == "Tikker ML Service"
|
||||
assert "endpoints" in data
|
||||
|
||||
|
||||
class TestPatternDetection:
|
||||
"""Tests for keystroke pattern detection."""
|
||||
|
||||
@staticmethod
|
||||
def _create_keystroke_events(count: int = 100, wpm: float = 50) -> List[Dict]:
|
||||
"""Create mock keystroke events."""
|
||||
events = []
|
||||
interval = int((60000 / (wpm * 5)))
|
||||
|
||||
for i in range(count):
|
||||
events.append({
|
||||
"timestamp": i * interval,
|
||||
"key_code": 65 + (i % 26),
|
||||
"event_type": "press"
|
||||
})
|
||||
|
||||
return events
|
||||
|
||||
def test_detect_fast_typing_pattern(self, ml_client):
|
||||
"""Test detection of fast typing pattern."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
fast_events = self._create_keystroke_events(count=150, wpm=80)
|
||||
|
||||
payload = {
|
||||
"events": fast_events,
|
||||
"user_id": "test_user"
|
||||
}
|
||||
|
||||
response = ml_client.post("/patterns/detect", json=payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
pattern_names = [p["name"] for p in data]
|
||||
assert any("fast" in name for name in pattern_names)
|
||||
|
||||
def test_detect_slow_typing_pattern(self, ml_client):
|
||||
"""Test detection of slow typing pattern."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
slow_events = self._create_keystroke_events(count=50, wpm=20)
|
||||
|
||||
payload = {
|
||||
"events": slow_events,
|
||||
"user_id": "test_user"
|
||||
}
|
||||
|
||||
response = ml_client.post("/patterns/detect", json=payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
pattern_names = [p["name"] for p in data]
|
||||
assert any("slow" in name for name in pattern_names)
|
||||
|
||||
def test_pattern_detection_empty_events(self, ml_client):
|
||||
"""Test pattern detection with empty events."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
payload = {
|
||||
"events": [],
|
||||
"user_id": "test_user"
|
||||
}
|
||||
|
||||
response = ml_client.post("/patterns/detect", json=payload)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
class TestAnomalyDetection:
|
||||
"""Tests for keystroke anomaly detection."""
|
||||
|
||||
@staticmethod
|
||||
def _create_keystroke_events(count: int = 100, wpm: float = 50) -> List[Dict]:
|
||||
"""Create mock keystroke events."""
|
||||
events = []
|
||||
interval = int((60000 / (wpm * 5)))
|
||||
|
||||
for i in range(count):
|
||||
events.append({
|
||||
"timestamp": i * interval,
|
||||
"key_code": 65 + (i % 26),
|
||||
"event_type": "press"
|
||||
})
|
||||
|
||||
return events
|
||||
|
||||
def test_detect_typing_speed_anomaly(self, ml_client):
|
||||
"""Test detection of typing speed anomaly."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
normal_events = self._create_keystroke_events(count=100, wpm=50)
|
||||
|
||||
payload = {
|
||||
"events": normal_events,
|
||||
"user_id": "test_user_anom"
|
||||
}
|
||||
|
||||
response = ml_client.post("/anomalies/detect", json=payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
def test_anomaly_detection_empty_events(self, ml_client):
|
||||
"""Test anomaly detection with empty events."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
payload = {
|
||||
"events": [],
|
||||
"user_id": "test_user"
|
||||
}
|
||||
|
||||
response = ml_client.post("/anomalies/detect", json=payload)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
class TestBehavioralProfile:
|
||||
"""Tests for behavioral profile building."""
|
||||
|
||||
@staticmethod
|
||||
def _create_keystroke_events(count: int = 200) -> List[Dict]:
|
||||
"""Create mock keystroke events."""
|
||||
events = []
|
||||
|
||||
for i in range(count):
|
||||
events.append({
|
||||
"timestamp": i * 100,
|
||||
"key_code": 65 + (i % 26),
|
||||
"event_type": "press"
|
||||
})
|
||||
|
||||
return events
|
||||
|
||||
def test_build_behavioral_profile(self, ml_client):
|
||||
"""Test building behavioral profile from events."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
events = self._create_keystroke_events(count=200)
|
||||
|
||||
payload = {
|
||||
"events": events,
|
||||
"user_id": "profile_test_user"
|
||||
}
|
||||
|
||||
response = ml_client.post("/profile/build", json=payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
assert "user_id" in data
|
||||
assert "avg_typing_speed" in data
|
||||
assert "peak_hours" in data
|
||||
assert "common_words" in data
|
||||
assert "consistency_score" in data
|
||||
assert "patterns" in data
|
||||
|
||||
assert data["user_id"] == "profile_test_user"
|
||||
assert data["consistency_score"] >= 0
|
||||
assert data["consistency_score"] <= 1
|
||||
|
||||
def test_profile_empty_events(self, ml_client):
|
||||
"""Test profile building with empty events."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
payload = {
|
||||
"events": [],
|
||||
"user_id": "test_user"
|
||||
}
|
||||
|
||||
response = ml_client.post("/profile/build", json=payload)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
class TestAuthenticityCheck:
|
||||
"""Tests for user authenticity verification."""
|
||||
|
||||
@staticmethod
|
||||
def _create_keystroke_events(count: int = 100, wpm: float = 50) -> List[Dict]:
|
||||
"""Create mock keystroke events."""
|
||||
events = []
|
||||
interval = int((60000 / (wpm * 5)))
|
||||
|
||||
for i in range(count):
|
||||
events.append({
|
||||
"timestamp": i * interval,
|
||||
"key_code": 65 + (i % 26),
|
||||
"event_type": "press"
|
||||
})
|
||||
|
||||
return events
|
||||
|
||||
def test_authenticity_check_unknown_user(self, ml_client):
|
||||
"""Test authenticity check for unknown user."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
events = self._create_keystroke_events(count=100)
|
||||
|
||||
payload = {
|
||||
"events": events,
|
||||
"user_id": "unknown_user_123"
|
||||
}
|
||||
|
||||
response = ml_client.post("/authenticity/check", json=payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert "authenticity_score" in data
|
||||
assert "verdict" in data
|
||||
assert data["verdict"] == "unknown"
|
||||
|
||||
def test_authenticity_check_established_user(self, ml_client):
|
||||
"""Test authenticity check for user with established profile."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
user_id = "established_user_test"
|
||||
events = self._create_keystroke_events(count=100, wpm=50)
|
||||
|
||||
build_payload = {
|
||||
"events": events,
|
||||
"user_id": user_id
|
||||
}
|
||||
|
||||
build_response = ml_client.post("/profile/build", json=build_payload)
|
||||
|
||||
if build_response.status_code == 200:
|
||||
check_payload = {
|
||||
"events": events,
|
||||
"user_id": user_id
|
||||
}
|
||||
|
||||
check_response = ml_client.post("/authenticity/check", json=check_payload)
|
||||
|
||||
if check_response.status_code == 200:
|
||||
data = check_response.json()
|
||||
assert "authenticity_score" in data
|
||||
assert "verdict" in data
|
||||
|
||||
|
||||
class TestTemporalAnalysis:
|
||||
"""Tests for temporal pattern analysis."""
|
||||
|
||||
def test_temporal_analysis_default_range(self, ml_client):
|
||||
"""Test temporal analysis with default date range."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
payload = {"date_range_days": 7}
|
||||
|
||||
response = ml_client.post("/temporal/analyze", json=payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert "trend" in data
|
||||
assert "date_range_days" in data or "error" in data
|
||||
if "date_range_days" in data:
|
||||
assert data["date_range_days"] == 7
|
||||
|
||||
def test_temporal_analysis_custom_range(self, ml_client):
|
||||
"""Test temporal analysis with custom date range."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
payload = {"date_range_days": 30}
|
||||
|
||||
response = ml_client.post("/temporal/analyze", json=payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert "date_range_days" in data or "error" in data
|
||||
if "date_range_days" in data:
|
||||
assert data["date_range_days"] == 30
|
||||
|
||||
|
||||
class TestModelTraining:
|
||||
"""Tests for ML model training."""
|
||||
|
||||
def test_train_model_default(self, ml_client):
|
||||
"""Test training ML model with default parameters."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
response = ml_client.post("/model/train")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert data["status"] == "trained"
|
||||
assert "samples" in data
|
||||
assert "features" in data
|
||||
assert "accuracy" in data
|
||||
|
||||
def test_train_model_custom_size(self, ml_client):
|
||||
"""Test training ML model with custom sample size."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
response = ml_client.post("/model/train?sample_size=500")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert data["samples"] == 500
|
||||
|
||||
|
||||
class TestBehaviorPrediction:
|
||||
"""Tests for behavior prediction."""
|
||||
|
||||
@staticmethod
|
||||
def _create_keystroke_events(count: int = 100) -> List[Dict]:
|
||||
"""Create mock keystroke events."""
|
||||
events = []
|
||||
|
||||
for i in range(count):
|
||||
events.append({
|
||||
"timestamp": i * 100,
|
||||
"key_code": 65 + (i % 26),
|
||||
"event_type": "press"
|
||||
})
|
||||
|
||||
return events
|
||||
|
||||
def test_predict_behavior_untrained_model(self, ml_client):
|
||||
"""Test behavior prediction with untrained model."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
events = self._create_keystroke_events(count=100)
|
||||
|
||||
payload = {
|
||||
"events": events,
|
||||
"user_id": "test_user"
|
||||
}
|
||||
|
||||
response = ml_client.post("/behavior/predict", json=payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert "behavior_category" in data or "status" in data
|
||||
|
||||
def test_predict_behavior_after_training(self, ml_client):
|
||||
"""Test behavior prediction after model training."""
|
||||
if not ml_client:
|
||||
pytest.skip("ML client not available")
|
||||
|
||||
train_response = ml_client.post("/model/train?sample_size=100")
|
||||
|
||||
if train_response.status_code == 200:
|
||||
events = self._create_keystroke_events(count=100)
|
||||
|
||||
payload = {
|
||||
"events": events,
|
||||
"user_id": "test_user"
|
||||
}
|
||||
|
||||
predict_response = ml_client.post("/behavior/predict", json=payload)
|
||||
|
||||
if predict_response.status_code == 200:
|
||||
data = predict_response.json()
|
||||
assert "behavior_category" in data
|
||||
assert "confidence" in data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ml_client():
|
||||
"""Create ML service test client."""
|
||||
from fastapi.testclient import TestClient
|
||||
try:
|
||||
from ml_service import app
|
||||
return TestClient(app)
|
||||
except:
|
||||
return None
|
||||
@@ -0,0 +1,343 @@
|
||||
"""
|
||||
Performance Testing for Tikker Services
|
||||
|
||||
Measures response times, throughput, and resource usage.
|
||||
Identifies bottlenecks and optimization opportunities.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
import json
|
||||
from typing import Dict, List, Tuple
|
||||
import statistics
|
||||
|
||||
|
||||
class PerformanceMetrics:
|
||||
"""Collect and analyze performance metrics."""
|
||||
|
||||
def __init__(self):
|
||||
self.measurements: Dict[str, List[float]] = {}
|
||||
|
||||
def record(self, name: str, value: float):
|
||||
"""Record a measurement."""
|
||||
if name not in self.measurements:
|
||||
self.measurements[name] = []
|
||||
self.measurements[name].append(value)
|
||||
|
||||
def summary(self, name: str) -> Dict[str, float]:
|
||||
"""Get summary statistics for measurements."""
|
||||
if name not in self.measurements:
|
||||
return {}
|
||||
|
||||
values = self.measurements[name]
|
||||
return {
|
||||
"count": len(values),
|
||||
"min": min(values),
|
||||
"max": max(values),
|
||||
"avg": statistics.mean(values),
|
||||
"median": statistics.median(values),
|
||||
"stdev": statistics.stdev(values) if len(values) > 1 else 0
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def metrics():
|
||||
"""Provide metrics collector."""
|
||||
return PerformanceMetrics()
|
||||
|
||||
|
||||
class TestAPIPerformance:
|
||||
"""Tests for API performance characteristics."""
|
||||
|
||||
def test_health_check_latency(self, api_client, metrics):
|
||||
"""Measure health check endpoint latency."""
|
||||
if not api_client:
|
||||
pytest.skip("API client not available")
|
||||
|
||||
for _ in range(10):
|
||||
start = time.time()
|
||||
response = api_client.get("/health")
|
||||
elapsed = (time.time() - start) * 1000
|
||||
|
||||
assert response.status_code == 200
|
||||
metrics.record("health_check_latency", elapsed)
|
||||
|
||||
summary = metrics.summary("health_check_latency")
|
||||
assert summary["avg"] < 100, "Health check should be < 100ms"
|
||||
assert summary["max"] < 500, "Health check max should be < 500ms"
|
||||
|
||||
def test_daily_stats_latency(self, api_client, metrics):
|
||||
"""Measure daily stats endpoint latency."""
|
||||
if not api_client:
|
||||
pytest.skip("API client not available")
|
||||
|
||||
for _ in range(5):
|
||||
start = time.time()
|
||||
response = api_client.get("/api/stats/daily")
|
||||
elapsed = (time.time() - start) * 1000
|
||||
|
||||
if response.status_code == 200:
|
||||
metrics.record("daily_stats_latency", elapsed)
|
||||
|
||||
if "daily_stats_latency" in metrics.measurements:
|
||||
summary = metrics.summary("daily_stats_latency")
|
||||
assert summary["avg"] < 200, "Daily stats should be < 200ms"
|
||||
|
||||
def test_top_words_latency(self, api_client, metrics):
|
||||
"""Measure top words endpoint latency."""
|
||||
if not api_client:
|
||||
pytest.skip("API client not available")
|
||||
|
||||
for limit in [10, 50, 100]:
|
||||
for _ in range(3):
|
||||
start = time.time()
|
||||
response = api_client.get(f"/api/words/top?limit={limit}")
|
||||
elapsed = (time.time() - start) * 1000
|
||||
|
||||
if response.status_code == 200:
|
||||
metrics.record(f"top_words_latency_{limit}", elapsed)
|
||||
|
||||
for limit in [10, 50, 100]:
|
||||
key = f"top_words_latency_{limit}"
|
||||
if key in metrics.measurements:
|
||||
summary = metrics.summary(key)
|
||||
assert summary["avg"] < 500, f"Top words (limit={limit}) should be < 500ms"
|
||||
|
||||
def test_concurrent_requests(self, api_client, metrics):
|
||||
"""Test API under concurrent load."""
|
||||
if not api_client:
|
||||
pytest.skip("API client not available")
|
||||
|
||||
endpoints = [
|
||||
"/health",
|
||||
"/api/stats/daily",
|
||||
"/api/words/top?limit=10"
|
||||
]
|
||||
|
||||
times = []
|
||||
for endpoint in endpoints:
|
||||
start = time.time()
|
||||
response = api_client.get(endpoint)
|
||||
elapsed = (time.time() - start) * 1000
|
||||
times.append(elapsed)
|
||||
|
||||
if response.status_code == 200:
|
||||
metrics.record("concurrent_request_latency", elapsed)
|
||||
|
||||
avg_time = statistics.mean(times)
|
||||
assert avg_time < 300, "Average concurrent request latency should be < 300ms"
|
||||
|
||||
|
||||
class TestAIPerformance:
|
||||
"""Tests for AI service performance."""
|
||||
|
||||
def test_health_check_latency(self, ai_client, metrics):
|
||||
"""Measure AI health check latency."""
|
||||
if not ai_client:
|
||||
pytest.skip("AI client not available")
|
||||
|
||||
for _ in range(5):
|
||||
start = time.time()
|
||||
response = ai_client.get("/health")
|
||||
elapsed = (time.time() - start) * 1000
|
||||
|
||||
assert response.status_code == 200
|
||||
metrics.record("ai_health_latency", elapsed)
|
||||
|
||||
summary = metrics.summary("ai_health_latency")
|
||||
assert summary["avg"] < 100, "AI health check should be < 100ms"
|
||||
|
||||
def test_analysis_latency(self, ai_client, metrics):
|
||||
"""Measure text analysis latency."""
|
||||
if not ai_client:
|
||||
pytest.skip("AI client not available")
|
||||
|
||||
payload = {
|
||||
"text": "This is a test message for analysis of keystroke patterns",
|
||||
"analysis_type": "general"
|
||||
}
|
||||
|
||||
for _ in range(3):
|
||||
start = time.time()
|
||||
response = ai_client.post("/analyze", json=payload)
|
||||
elapsed = (time.time() - start) * 1000
|
||||
|
||||
if response.status_code == 200:
|
||||
metrics.record("ai_analysis_latency", elapsed)
|
||||
|
||||
if "ai_analysis_latency" in metrics.measurements:
|
||||
summary = metrics.summary("ai_analysis_latency")
|
||||
print(f"\nAI Analysis latency: {summary}")
|
||||
|
||||
|
||||
class TestVizPerformance:
|
||||
"""Tests for visualization service performance."""
|
||||
|
||||
def test_health_check_latency(self, viz_client, metrics):
|
||||
"""Measure visualization health check latency."""
|
||||
if not viz_client:
|
||||
pytest.skip("Visualization client not available")
|
||||
|
||||
for _ in range(5):
|
||||
start = time.time()
|
||||
response = viz_client.get("/health")
|
||||
elapsed = (time.time() - start) * 1000
|
||||
|
||||
assert response.status_code == 200
|
||||
metrics.record("viz_health_latency", elapsed)
|
||||
|
||||
summary = metrics.summary("viz_health_latency")
|
||||
assert summary["avg"] < 100, "Viz health check should be < 100ms"
|
||||
|
||||
def test_chart_generation_latency(self, viz_client, metrics):
|
||||
"""Measure chart generation latency."""
|
||||
if not viz_client:
|
||||
pytest.skip("Visualization client not available")
|
||||
|
||||
for chart_type in ["bar", "line", "pie"]:
|
||||
payload = {
|
||||
"title": f"Test {chart_type} Chart",
|
||||
"data": {f"Item{i}": i*100 for i in range(10)},
|
||||
"chart_type": chart_type
|
||||
}
|
||||
|
||||
for _ in range(2):
|
||||
start = time.time()
|
||||
response = viz_client.post("/chart", json=payload)
|
||||
elapsed = (time.time() - start) * 1000
|
||||
|
||||
if response.status_code == 200:
|
||||
metrics.record(f"chart_{chart_type}_latency", elapsed)
|
||||
|
||||
for chart_type in ["bar", "line", "pie"]:
|
||||
key = f"chart_{chart_type}_latency"
|
||||
if key in metrics.measurements:
|
||||
summary = metrics.summary(key)
|
||||
assert summary["avg"] < 1000, f"{chart_type} chart should be < 1000ms"
|
||||
|
||||
|
||||
class TestThroughput:
|
||||
"""Tests for service throughput."""
|
||||
|
||||
def test_sequential_requests(self, api_client):
|
||||
"""Test sequential request throughput."""
|
||||
if not api_client:
|
||||
pytest.skip("API client not available")
|
||||
|
||||
start = time.time()
|
||||
count = 0
|
||||
|
||||
while time.time() - start < 5:
|
||||
response = api_client.get("/health")
|
||||
if response.status_code == 200:
|
||||
count += 1
|
||||
|
||||
elapsed = time.time() - start
|
||||
throughput = count / elapsed
|
||||
|
||||
print(f"\nSequential throughput: {throughput:.2f} req/s")
|
||||
assert throughput > 10, "Throughput should be > 10 req/s"
|
||||
|
||||
def test_word_search_throughput(self, api_client):
|
||||
"""Test word search throughput."""
|
||||
if not api_client:
|
||||
pytest.skip("API client not available")
|
||||
|
||||
words = ["the", "and", "test", "python", "data"]
|
||||
start = time.time()
|
||||
count = 0
|
||||
|
||||
while time.time() - start < 5:
|
||||
for word in words:
|
||||
response = api_client.get(f"/api/words/find?word={word}")
|
||||
if response.status_code in [200, 404]:
|
||||
count += 1
|
||||
|
||||
elapsed = time.time() - start
|
||||
throughput = count / elapsed
|
||||
|
||||
print(f"\nWord search throughput: {throughput:.2f} req/s")
|
||||
|
||||
|
||||
class TestMemoryUsage:
|
||||
"""Tests for memory consumption patterns."""
|
||||
|
||||
def test_large_data_response(self, api_client):
|
||||
"""Test API with large data response."""
|
||||
if not api_client:
|
||||
pytest.skip("API client not available")
|
||||
|
||||
response = api_client.get("/api/words/top?limit=100")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
size_mb = len(json.dumps(data)) / (1024 * 1024)
|
||||
print(f"\nResponse size: {size_mb:.2f} MB")
|
||||
assert size_mb < 10, "Response should be < 10 MB"
|
||||
|
||||
def test_repeated_requests(self, api_client):
|
||||
"""Test for memory leaks with repeated requests."""
|
||||
if not api_client:
|
||||
pytest.skip("API client not available")
|
||||
|
||||
for _ in range(100):
|
||||
response = api_client.get("/health")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestResponseQuality:
|
||||
"""Tests for response quality metrics."""
|
||||
|
||||
def test_daily_stats_response_structure(self, api_client):
|
||||
"""Verify daily stats response structure."""
|
||||
if not api_client:
|
||||
pytest.skip("API client not available")
|
||||
|
||||
response = api_client.get("/api/stats/daily")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
required_fields = ["presses", "releases", "repeats", "total"]
|
||||
for field in required_fields:
|
||||
assert field in data, f"Missing field: {field}"
|
||||
|
||||
def test_top_words_response_structure(self, api_client):
|
||||
"""Verify top words response structure."""
|
||||
if not api_client:
|
||||
pytest.skip("API client not available")
|
||||
|
||||
response = api_client.get("/api/words/top?limit=5")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert isinstance(data, list), "Response should be a list"
|
||||
if len(data) > 0:
|
||||
word = data[0]
|
||||
required_fields = ["rank", "word", "count", "percentage"]
|
||||
for field in required_fields:
|
||||
assert field in word, f"Missing field in word: {field}"
|
||||
|
||||
|
||||
class TestErrorRecovery:
|
||||
"""Tests for error handling and recovery."""
|
||||
|
||||
def test_invalid_parameter_handling(self, api_client, metrics):
|
||||
"""Test handling of invalid parameters."""
|
||||
if not api_client:
|
||||
pytest.skip("API client not available")
|
||||
|
||||
start = time.time()
|
||||
response = api_client.get("/api/words/find?word=")
|
||||
elapsed = (time.time() - start) * 1000
|
||||
|
||||
metrics.record("invalid_param_latency", elapsed)
|
||||
assert response.status_code in [200, 400]
|
||||
assert elapsed < 100, "Error response should be quick"
|
||||
|
||||
def test_missing_required_parameter(self, api_client):
|
||||
"""Test missing required parameter."""
|
||||
if not api_client:
|
||||
pytest.skip("API client not available")
|
||||
|
||||
response = api_client.get("/api/stats/hourly")
|
||||
assert response.status_code in [400, 422, 200]
|
||||
@@ -0,0 +1,307 @@
|
||||
"""
|
||||
Service Integration Tests
|
||||
|
||||
Tests for API, AI, and visualization microservices.
|
||||
Verifies service health, endpoints, and inter-service communication.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
class TestMainAPIService:
|
||||
"""Tests for main API service with C tools integration."""
|
||||
|
||||
def test_api_health_check(self, api_client):
|
||||
"""Test main API health check endpoint."""
|
||||
response = api_client.get("/health")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] in ["healthy", "ok"]
|
||||
assert "tools" in data or "message" in data
|
||||
|
||||
def test_api_root_endpoint(self, api_client):
|
||||
"""Test main API root endpoint."""
|
||||
response = api_client.get("/")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == "Tikker API"
|
||||
assert "version" in data
|
||||
assert "endpoints" in data
|
||||
|
||||
def test_get_daily_stats(self, api_client):
|
||||
"""Test daily statistics endpoint."""
|
||||
response = api_client.get("/api/stats/daily")
|
||||
assert response.status_code in [200, 503]
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert "presses" in data or "status" in data
|
||||
|
||||
def test_get_top_words(self, api_client):
|
||||
"""Test top words endpoint."""
|
||||
response = api_client.get("/api/words/top?limit=10")
|
||||
assert response.status_code in [200, 503]
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert isinstance(data, list) or isinstance(data, dict)
|
||||
|
||||
def test_decode_file_endpoint(self, api_client):
|
||||
"""Test file decoding endpoint."""
|
||||
payload = {
|
||||
"input_file": "test_input.txt",
|
||||
"output_file": "test_output.txt",
|
||||
"verbose": False
|
||||
}
|
||||
response = api_client.post("/api/decode", json=payload)
|
||||
assert response.status_code in [200, 400, 404, 503]
|
||||
|
||||
def test_api_health_timeout(self, api_client):
|
||||
"""Test API health endpoint response time."""
|
||||
import time
|
||||
start = time.time()
|
||||
response = api_client.get("/health")
|
||||
elapsed = time.time() - start
|
||||
assert elapsed < 5.0
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestAIService:
|
||||
"""Tests for AI microservice."""
|
||||
|
||||
def test_ai_health_check(self, ai_client):
|
||||
"""Test AI service health check."""
|
||||
response = ai_client.get("/health")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "healthy"
|
||||
assert "ai_available" in data
|
||||
|
||||
def test_ai_root_endpoint(self, ai_client):
|
||||
"""Test AI service root endpoint."""
|
||||
response = ai_client.get("/")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == "Tikker AI Service"
|
||||
assert "endpoints" in data
|
||||
|
||||
def test_ai_analyze_endpoint(self, ai_client):
|
||||
"""Test AI text analysis endpoint."""
|
||||
payload = {
|
||||
"text": "This is a test message for analysis",
|
||||
"analysis_type": "general"
|
||||
}
|
||||
response = ai_client.post("/analyze", json=payload)
|
||||
assert response.status_code in [200, 503]
|
||||
|
||||
def test_ai_analyze_activity(self, ai_client):
|
||||
"""Test AI activity analysis."""
|
||||
payload = {
|
||||
"text": "typing keyboard input keystroke logs",
|
||||
"analysis_type": "activity"
|
||||
}
|
||||
response = ai_client.post("/analyze", json=payload)
|
||||
assert response.status_code in [200, 503]
|
||||
|
||||
def test_ai_empty_text_validation(self, ai_client):
|
||||
"""Test AI service rejects empty text."""
|
||||
payload = {
|
||||
"text": "",
|
||||
"analysis_type": "general"
|
||||
}
|
||||
response = ai_client.post("/analyze", json=payload)
|
||||
if response.status_code == 503:
|
||||
pass
|
||||
else:
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
class TestVizService:
|
||||
"""Tests for visualization microservice."""
|
||||
|
||||
def test_viz_health_check(self, viz_client):
|
||||
"""Test visualization service health check."""
|
||||
response = viz_client.get("/health")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "healthy"
|
||||
assert "viz_available" in data
|
||||
|
||||
def test_viz_root_endpoint(self, viz_client):
|
||||
"""Test visualization service root endpoint."""
|
||||
response = viz_client.get("/")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == "Tikker Visualization Service"
|
||||
assert "supported_charts" in data
|
||||
|
||||
def test_viz_bar_chart(self, viz_client):
|
||||
"""Test bar chart generation."""
|
||||
payload = {
|
||||
"title": "Test Bar Chart",
|
||||
"data": {"A": 10, "B": 20, "C": 15},
|
||||
"chart_type": "bar",
|
||||
"width": 10,
|
||||
"height": 6
|
||||
}
|
||||
response = viz_client.post("/chart", json=payload)
|
||||
assert response.status_code in [200, 503]
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
assert data["status"] == "success"
|
||||
assert data["chart_type"] == "bar"
|
||||
assert "image_base64" in data
|
||||
|
||||
def test_viz_line_chart(self, viz_client):
|
||||
"""Test line chart generation."""
|
||||
payload = {
|
||||
"title": "Test Line Chart",
|
||||
"data": {"Jan": 100, "Feb": 120, "Mar": 140},
|
||||
"chart_type": "line"
|
||||
}
|
||||
response = viz_client.post("/chart", json=payload)
|
||||
assert response.status_code in [200, 503]
|
||||
|
||||
def test_viz_pie_chart(self, viz_client):
|
||||
"""Test pie chart generation."""
|
||||
payload = {
|
||||
"title": "Test Pie Chart",
|
||||
"data": {"Category1": 30, "Category2": 40, "Category3": 30},
|
||||
"chart_type": "pie"
|
||||
}
|
||||
response = viz_client.post("/chart", json=payload)
|
||||
assert response.status_code in [200, 503]
|
||||
|
||||
def test_viz_chart_download(self, viz_client):
|
||||
"""Test chart download endpoint."""
|
||||
payload = {
|
||||
"title": "Download Test",
|
||||
"data": {"X": 50, "Y": 75},
|
||||
"chart_type": "bar"
|
||||
}
|
||||
response = viz_client.post("/chart/download", json=payload)
|
||||
assert response.status_code in [200, 503]
|
||||
|
||||
def test_viz_invalid_chart_type(self, viz_client):
|
||||
"""Test invalid chart type handling."""
|
||||
payload = {
|
||||
"title": "Invalid Chart",
|
||||
"data": {"A": 10},
|
||||
"chart_type": "invalid"
|
||||
}
|
||||
response = viz_client.post("/chart", json=payload)
|
||||
if response.status_code == 503:
|
||||
pass
|
||||
else:
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
class TestServiceIntegration:
|
||||
"""Tests for service-to-service communication."""
|
||||
|
||||
def test_all_services_healthy(self, api_client, ai_client, viz_client):
|
||||
"""Test all services report healthy status."""
|
||||
api_response = api_client.get("/health")
|
||||
ai_response = ai_client.get("/health")
|
||||
viz_response = viz_client.get("/health")
|
||||
|
||||
assert api_response.status_code == 200
|
||||
assert ai_response.status_code == 200
|
||||
assert viz_response.status_code == 200
|
||||
|
||||
def test_api_to_ai_communication(self, api_client, ai_client):
|
||||
"""Test API can communicate with AI service."""
|
||||
api_health = api_client.get("/health")
|
||||
ai_health = ai_client.get("/health")
|
||||
|
||||
assert api_health.status_code == 200
|
||||
assert ai_health.status_code == 200
|
||||
|
||||
def test_api_to_viz_communication(self, api_client, viz_client):
|
||||
"""Test API can communicate with visualization service."""
|
||||
api_health = api_client.get("/health")
|
||||
viz_health = viz_client.get("/health")
|
||||
|
||||
assert api_health.status_code == 200
|
||||
assert viz_health.status_code == 200
|
||||
|
||||
def test_concurrent_service_requests(self, api_client, ai_client, viz_client):
|
||||
"""Test multiple concurrent requests to different services."""
|
||||
responses = {
|
||||
"api": api_client.get("/health"),
|
||||
"ai": ai_client.get("/health"),
|
||||
"viz": viz_client.get("/health")
|
||||
}
|
||||
|
||||
for service, response in responses.items():
|
||||
assert response.status_code == 200, f"{service} service failed"
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
"""Tests for error handling and edge cases."""
|
||||
|
||||
def test_api_invalid_endpoint(self, api_client):
|
||||
"""Test API handles invalid endpoints."""
|
||||
response = api_client.get("/api/invalid")
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_ai_invalid_endpoint(self, ai_client):
|
||||
"""Test AI service handles invalid endpoints."""
|
||||
response = ai_client.get("/invalid")
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_viz_invalid_endpoint(self, viz_client):
|
||||
"""Test visualization service handles invalid endpoints."""
|
||||
response = viz_client.get("/invalid")
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_api_malformed_json(self, api_client):
|
||||
"""Test API handles malformed JSON."""
|
||||
response = api_client.post(
|
||||
"/api/decode",
|
||||
content="invalid json",
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
assert response.status_code in [400, 422]
|
||||
|
||||
def test_ai_malformed_json(self, ai_client):
|
||||
"""Test AI service handles malformed JSON."""
|
||||
response = ai_client.post(
|
||||
"/analyze",
|
||||
content="invalid json",
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
assert response.status_code in [400, 422]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api_client():
|
||||
"""Create API test client."""
|
||||
from fastapi.testclient import TestClient
|
||||
try:
|
||||
from api_c_integration import app
|
||||
return TestClient(app)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ai_client():
|
||||
"""Create AI service test client."""
|
||||
from fastapi.testclient import TestClient
|
||||
try:
|
||||
from ai_service import app
|
||||
return TestClient(app)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def viz_client():
|
||||
"""Create visualization service test client."""
|
||||
from fastapi.testclient import TestClient
|
||||
try:
|
||||
from viz_service import app
|
||||
return TestClient(app)
|
||||
except:
|
||||
return None
|
||||
@@ -0,0 +1,101 @@
|
||||
#ifndef TEST_FRAMEWORK_H
|
||||
#define TEST_FRAMEWORK_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
typedef struct {
|
||||
int passed;
|
||||
int failed;
|
||||
int total;
|
||||
const char *current_test;
|
||||
} test_state_t;
|
||||
|
||||
static test_state_t test_state = {0, 0, 0, NULL};
|
||||
|
||||
#define TEST_ASSERT(condition, message) \
|
||||
do { \
|
||||
if (!(condition)) { \
|
||||
fprintf(stderr, " ✗ FAIL: %s\n", message); \
|
||||
test_state.failed++; \
|
||||
} else { \
|
||||
test_state.passed++; \
|
||||
} \
|
||||
test_state.total++; \
|
||||
} while(0)
|
||||
|
||||
#define ASSERT_EQ(a, b) \
|
||||
do { \
|
||||
if ((a) != (b)) { \
|
||||
fprintf(stderr, " ✗ FAIL: %ld != %ld\n", (long)(a), (long)(b)); \
|
||||
test_state.failed++; \
|
||||
} else { \
|
||||
test_state.passed++; \
|
||||
} \
|
||||
test_state.total++; \
|
||||
} while(0)
|
||||
|
||||
#define ASSERT_EQ_STR(a, b) \
|
||||
do { \
|
||||
if (strcmp((a), (b)) != 0) { \
|
||||
fprintf(stderr, " ✗ FAIL: '%s' != '%s'\n", (a), (b)); \
|
||||
test_state.failed++; \
|
||||
} else { \
|
||||
test_state.passed++; \
|
||||
} \
|
||||
test_state.total++; \
|
||||
} while(0)
|
||||
|
||||
#define ASSERT_NULL(ptr) \
|
||||
do { \
|
||||
if ((ptr) != NULL) { \
|
||||
fprintf(stderr, " ✗ FAIL: pointer is not NULL\n"); \
|
||||
test_state.failed++; \
|
||||
} else { \
|
||||
test_state.passed++; \
|
||||
} \
|
||||
test_state.total++; \
|
||||
} while(0)
|
||||
|
||||
#define ASSERT_NOT_NULL(ptr) \
|
||||
do { \
|
||||
if ((ptr) == NULL) { \
|
||||
fprintf(stderr, " ✗ FAIL: pointer is NULL\n"); \
|
||||
test_state.failed++; \
|
||||
} else { \
|
||||
test_state.passed++; \
|
||||
} \
|
||||
test_state.total++; \
|
||||
} while(0)
|
||||
|
||||
#define TEST_BEGIN(name) \
|
||||
do { \
|
||||
test_state.current_test = (name); \
|
||||
printf("TEST: %s\n", (name)); \
|
||||
} while(0)
|
||||
|
||||
#define TEST_END \
|
||||
do { \
|
||||
printf("\n"); \
|
||||
} while(0)
|
||||
|
||||
#define TEST_SUMMARY \
|
||||
do { \
|
||||
printf("\n========================================\n"); \
|
||||
printf("Test Summary:\n"); \
|
||||
printf(" Passed: %d\n", test_state.passed); \
|
||||
printf(" Failed: %d\n", test_state.failed); \
|
||||
printf(" Total: %d\n", test_state.total); \
|
||||
printf("========================================\n"); \
|
||||
if (test_state.failed > 0) { \
|
||||
printf("❌ %d test(s) failed\n", test_state.failed); \
|
||||
return 1; \
|
||||
} else { \
|
||||
printf("✓ All tests passed\n"); \
|
||||
return 0; \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user