feat: create v2-prep branch with comprehensive planning
Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
630
orig/pkg/security/anomaly_detector_test.go
Normal file
630
orig/pkg/security/anomaly_detector_test.go
Normal file
@@ -0,0 +1,630 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
func TestNewAnomalyDetector(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
|
||||
// Test with default config
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
assert.NotNil(t, ad)
|
||||
assert.NotNil(t, ad.config)
|
||||
assert.Equal(t, 2.5, ad.config.ZScoreThreshold)
|
||||
|
||||
// Test with custom config
|
||||
customConfig := &AnomalyConfig{
|
||||
ZScoreThreshold: 3.0,
|
||||
VolumeThreshold: 4.0,
|
||||
BaselineWindow: 12 * time.Hour,
|
||||
EnableVolumeDetection: false,
|
||||
}
|
||||
|
||||
ad2 := NewAnomalyDetector(logger, customConfig)
|
||||
assert.NotNil(t, ad2)
|
||||
assert.Equal(t, 3.0, ad2.config.ZScoreThreshold)
|
||||
assert.Equal(t, 4.0, ad2.config.VolumeThreshold)
|
||||
assert.Equal(t, 12*time.Hour, ad2.config.BaselineWindow)
|
||||
assert.False(t, ad2.config.EnableVolumeDetection)
|
||||
}
|
||||
|
||||
func TestAnomalyDetectorStartStop(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
// Test start
|
||||
err := ad.Start()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ad.running)
|
||||
|
||||
// Test start when already running
|
||||
err = ad.Start()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test stop
|
||||
err = ad.Stop()
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, ad.running)
|
||||
|
||||
// Test stop when already stopped
|
||||
err = ad.Stop()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRecordMetric(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
// Record some normal values
|
||||
metricName := "test_metric"
|
||||
values := []float64{10.0, 12.0, 11.0, 13.0, 9.0, 14.0, 10.5, 11.5}
|
||||
|
||||
for _, value := range values {
|
||||
ad.RecordMetric(metricName, value)
|
||||
}
|
||||
|
||||
// Check pattern was created
|
||||
ad.mu.RLock()
|
||||
pattern, exists := ad.patterns[metricName]
|
||||
ad.mu.RUnlock()
|
||||
|
||||
assert.True(t, exists)
|
||||
assert.NotNil(t, pattern)
|
||||
assert.Equal(t, metricName, pattern.MetricName)
|
||||
assert.Equal(t, len(values), len(pattern.Observations))
|
||||
assert.Greater(t, pattern.Mean, 0.0)
|
||||
assert.Greater(t, pattern.StandardDev, 0.0)
|
||||
}
|
||||
|
||||
func TestRecordTransaction(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
// Create test transaction
|
||||
record := &TransactionRecord{
|
||||
Hash: common.HexToHash("0x123"),
|
||||
From: common.HexToAddress("0xabc"),
|
||||
To: &common.Address{},
|
||||
Value: 1.5,
|
||||
GasPrice: 20.0,
|
||||
GasUsed: 21000,
|
||||
Timestamp: time.Now(),
|
||||
BlockNumber: 12345,
|
||||
Success: true,
|
||||
}
|
||||
|
||||
ad.RecordTransaction(record)
|
||||
|
||||
// Check transaction was recorded
|
||||
ad.mu.RLock()
|
||||
assert.Equal(t, 1, len(ad.transactionLog))
|
||||
assert.Equal(t, record.Hash, ad.transactionLog[0].Hash)
|
||||
assert.Greater(t, ad.transactionLog[0].AnomalyScore, 0.0)
|
||||
ad.mu.RUnlock()
|
||||
}
|
||||
|
||||
func TestPatternStatistics(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
// Create pattern with known values
|
||||
pattern := &PatternBaseline{
|
||||
MetricName: "test",
|
||||
Observations: []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
|
||||
Percentiles: make(map[int]float64),
|
||||
SeasonalPatterns: make(map[string]float64),
|
||||
}
|
||||
|
||||
ad.updatePatternStatistics(pattern)
|
||||
|
||||
// Check statistics
|
||||
assert.Equal(t, 5.5, pattern.Mean)
|
||||
assert.Equal(t, 1.0, pattern.Min)
|
||||
assert.Equal(t, 10.0, pattern.Max)
|
||||
assert.Greater(t, pattern.StandardDev, 0.0)
|
||||
assert.Greater(t, pattern.Variance, 0.0)
|
||||
|
||||
// Check percentiles
|
||||
assert.NotEmpty(t, pattern.Percentiles)
|
||||
assert.Contains(t, pattern.Percentiles, 50)
|
||||
assert.Contains(t, pattern.Percentiles, 95)
|
||||
}
|
||||
|
||||
func TestZScoreCalculation(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
pattern := &PatternBaseline{
|
||||
Mean: 10.0,
|
||||
StandardDev: 2.0,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
value float64
|
||||
expected float64
|
||||
}{
|
||||
{10.0, 0.0}, // At mean
|
||||
{12.0, 1.0}, // 1 std dev above
|
||||
{8.0, -1.0}, // 1 std dev below
|
||||
{16.0, 3.0}, // 3 std devs above
|
||||
{4.0, -3.0}, // 3 std devs below
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
zScore := ad.calculateZScore(tc.value, pattern)
|
||||
assert.Equal(t, tc.expected, zScore, "Z-score for value %.1f", tc.value)
|
||||
}
|
||||
|
||||
// Test with zero standard deviation
|
||||
pattern.StandardDev = 0
|
||||
zScore := ad.calculateZScore(15.0, pattern)
|
||||
assert.Equal(t, 0.0, zScore)
|
||||
}
|
||||
|
||||
func TestAnomalyDetection(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
config := &AnomalyConfig{
|
||||
ZScoreThreshold: 2.0,
|
||||
VolumeThreshold: 2.0,
|
||||
EnableVolumeDetection: true,
|
||||
EnableBehavioralAD: true,
|
||||
EnablePatternDetection: true,
|
||||
}
|
||||
ad := NewAnomalyDetector(logger, config)
|
||||
|
||||
// Build baseline with normal values
|
||||
normalValues := []float64{100, 105, 95, 110, 90, 115, 85, 120, 80, 125}
|
||||
for _, value := range normalValues {
|
||||
ad.RecordMetric("transaction_value", value)
|
||||
}
|
||||
|
||||
// Record anomalous value
|
||||
anomalousValue := 500.0 // Way above normal
|
||||
ad.RecordMetric("transaction_value", anomalousValue)
|
||||
|
||||
// Check if alert was generated
|
||||
select {
|
||||
case alert := <-ad.GetAlerts():
|
||||
assert.NotNil(t, alert)
|
||||
assert.Equal(t, AnomalyTypeStatistical, alert.Type)
|
||||
assert.Equal(t, "transaction_value", alert.MetricName)
|
||||
assert.Equal(t, anomalousValue, alert.ObservedValue)
|
||||
assert.Greater(t, alert.Score, 2.0)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("Expected anomaly alert but none received")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolumeAnomalyDetection(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
config := &AnomalyConfig{
|
||||
VolumeThreshold: 2.0,
|
||||
EnableVolumeDetection: true,
|
||||
}
|
||||
ad := NewAnomalyDetector(logger, config)
|
||||
|
||||
// Build baseline
|
||||
for i := 0; i < 20; i++ {
|
||||
record := &TransactionRecord{
|
||||
Hash: common.HexToHash("0x" + string(rune(i))),
|
||||
From: common.HexToAddress("0x123"),
|
||||
Value: 1.0, // Normal value
|
||||
GasPrice: 20.0,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
ad.RecordTransaction(record)
|
||||
}
|
||||
|
||||
// Record anomalous transaction
|
||||
anomalousRecord := &TransactionRecord{
|
||||
Hash: common.HexToHash("0xanomaly"),
|
||||
From: common.HexToAddress("0x456"),
|
||||
Value: 50.0, // Much higher than normal
|
||||
GasPrice: 20.0,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
ad.RecordTransaction(anomalousRecord)
|
||||
|
||||
// Check for alert
|
||||
select {
|
||||
case alert := <-ad.GetAlerts():
|
||||
assert.NotNil(t, alert)
|
||||
assert.Equal(t, AnomalyTypeVolume, alert.Type)
|
||||
assert.Equal(t, 50.0, alert.ObservedValue)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
// Volume detection might not trigger with insufficient baseline
|
||||
// This is acceptable behavior
|
||||
}
|
||||
}
|
||||
|
||||
func TestBehavioralAnomalyDetection(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
config := &AnomalyConfig{
|
||||
EnableBehavioralAD: true,
|
||||
}
|
||||
ad := NewAnomalyDetector(logger, config)
|
||||
|
||||
sender := common.HexToAddress("0x123")
|
||||
|
||||
// Record normal transactions from sender
|
||||
for i := 0; i < 10; i++ {
|
||||
record := &TransactionRecord{
|
||||
Hash: common.HexToHash("0x" + string(rune(i))),
|
||||
From: sender,
|
||||
Value: 1.0,
|
||||
GasPrice: 20.0, // Normal gas price
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
ad.RecordTransaction(record)
|
||||
}
|
||||
|
||||
// Record anomalous gas price transaction
|
||||
anomalousRecord := &TransactionRecord{
|
||||
Hash: common.HexToHash("0xanomaly"),
|
||||
From: sender,
|
||||
Value: 1.0,
|
||||
GasPrice: 200.0, // 10x higher gas price
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
ad.RecordTransaction(anomalousRecord)
|
||||
|
||||
// Check for alert
|
||||
select {
|
||||
case alert := <-ad.GetAlerts():
|
||||
assert.NotNil(t, alert)
|
||||
assert.Equal(t, AnomalyTypeBehavioral, alert.Type)
|
||||
assert.Equal(t, sender.Hex(), alert.Source)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
// Behavioral detection might not trigger immediately
|
||||
// This is acceptable behavior
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeverityCalculation(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
testCases := []struct {
|
||||
zScore float64
|
||||
expected AnomalySeverity
|
||||
}{
|
||||
{1.5, AnomalySeverityLow},
|
||||
{2.5, AnomalySeverityMedium},
|
||||
{3.5, AnomalySeverityHigh},
|
||||
{4.5, AnomalySeverityCritical},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
severity := ad.calculateSeverity(tc.zScore)
|
||||
assert.Equal(t, tc.expected, severity, "Severity for Z-score %.1f", tc.zScore)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfidenceCalculation(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
// Test with different Z-scores and sample sizes
|
||||
testCases := []struct {
|
||||
zScore float64
|
||||
sampleSize int
|
||||
minConf float64
|
||||
maxConf float64
|
||||
}{
|
||||
{2.0, 10, 0.0, 1.0},
|
||||
{5.0, 100, 0.5, 1.0},
|
||||
{1.0, 200, 0.0, 1.0},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
confidence := ad.calculateConfidence(tc.zScore, tc.sampleSize)
|
||||
assert.GreaterOrEqual(t, confidence, tc.minConf)
|
||||
assert.LessOrEqual(t, confidence, tc.maxConf)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrendCalculation(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
// Test increasing trend
|
||||
increasing := []float64{1, 2, 3, 4, 5}
|
||||
trend := ad.calculateTrend(increasing)
|
||||
assert.Greater(t, trend, 0.0)
|
||||
|
||||
// Test decreasing trend
|
||||
decreasing := []float64{5, 4, 3, 2, 1}
|
||||
trend = ad.calculateTrend(decreasing)
|
||||
assert.Less(t, trend, 0.0)
|
||||
|
||||
// Test stable trend
|
||||
stable := []float64{5, 5, 5, 5, 5}
|
||||
trend = ad.calculateTrend(stable)
|
||||
assert.Equal(t, 0.0, trend)
|
||||
|
||||
// Test edge cases
|
||||
empty := []float64{}
|
||||
trend = ad.calculateTrend(empty)
|
||||
assert.Equal(t, 0.0, trend)
|
||||
|
||||
single := []float64{5}
|
||||
trend = ad.calculateTrend(single)
|
||||
assert.Equal(t, 0.0, trend)
|
||||
}
|
||||
|
||||
func TestAnomalyReport(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
// Add some data
|
||||
ad.RecordMetric("test_metric1", 10.0)
|
||||
ad.RecordMetric("test_metric2", 20.0)
|
||||
|
||||
record := &TransactionRecord{
|
||||
Hash: common.HexToHash("0x123"),
|
||||
From: common.HexToAddress("0xabc"),
|
||||
Value: 1.0,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
ad.RecordTransaction(record)
|
||||
|
||||
// Generate report
|
||||
report := ad.GetAnomalyReport()
|
||||
assert.NotNil(t, report)
|
||||
assert.Greater(t, report.PatternsTracked, 0)
|
||||
assert.Greater(t, report.TransactionsAnalyzed, 0)
|
||||
assert.NotNil(t, report.PatternSummaries)
|
||||
assert.NotNil(t, report.SystemHealth)
|
||||
assert.NotZero(t, report.Timestamp)
|
||||
}
|
||||
|
||||
func TestPatternSummaries(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
// Create patterns with different trends
|
||||
ad.RecordMetric("increasing", 1.0)
|
||||
ad.RecordMetric("increasing", 2.0)
|
||||
ad.RecordMetric("increasing", 3.0)
|
||||
ad.RecordMetric("increasing", 4.0)
|
||||
ad.RecordMetric("increasing", 5.0)
|
||||
|
||||
ad.RecordMetric("stable", 10.0)
|
||||
ad.RecordMetric("stable", 10.0)
|
||||
ad.RecordMetric("stable", 10.0)
|
||||
|
||||
summaries := ad.getPatternSummaries()
|
||||
assert.NotEmpty(t, summaries)
|
||||
|
||||
for name, summary := range summaries {
|
||||
assert.NotEmpty(t, summary.MetricName)
|
||||
assert.Equal(t, name, summary.MetricName)
|
||||
assert.GreaterOrEqual(t, summary.SampleCount, int64(0))
|
||||
assert.Contains(t, []string{"INCREASING", "DECREASING", "STABLE"}, summary.Trend)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemHealth(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
health := ad.calculateSystemHealth()
|
||||
assert.NotNil(t, health)
|
||||
assert.GreaterOrEqual(t, health.AlertChannelSize, 0)
|
||||
assert.GreaterOrEqual(t, health.ProcessingLatency, 0.0)
|
||||
assert.GreaterOrEqual(t, health.MemoryUsage, int64(0))
|
||||
assert.GreaterOrEqual(t, health.ErrorRate, 0.0)
|
||||
assert.Contains(t, []string{"HEALTHY", "WARNING", "DEGRADED", "CRITICAL"}, health.OverallHealth)
|
||||
}
|
||||
|
||||
func TestTransactionHistoryLimit(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
config := &AnomalyConfig{
|
||||
MaxTransactionHistory: 5, // Small limit for testing
|
||||
}
|
||||
ad := NewAnomalyDetector(logger, config)
|
||||
|
||||
// Add more transactions than the limit
|
||||
for i := 0; i < 10; i++ {
|
||||
record := &TransactionRecord{
|
||||
Hash: common.HexToHash("0x" + string(rune(i))),
|
||||
From: common.HexToAddress("0x123"),
|
||||
Value: float64(i),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
ad.RecordTransaction(record)
|
||||
}
|
||||
|
||||
// Check that history is limited
|
||||
ad.mu.RLock()
|
||||
assert.LessOrEqual(t, len(ad.transactionLog), config.MaxTransactionHistory)
|
||||
ad.mu.RUnlock()
|
||||
}
|
||||
|
||||
func TestPatternHistoryLimit(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
config := &AnomalyConfig{
|
||||
MaxPatternHistory: 3, // Small limit for testing
|
||||
}
|
||||
ad := NewAnomalyDetector(logger, config)
|
||||
|
||||
metricName := "test_metric"
|
||||
|
||||
// Add more observations than the limit
|
||||
for i := 0; i < 10; i++ {
|
||||
ad.RecordMetric(metricName, float64(i))
|
||||
}
|
||||
|
||||
// Check that pattern history is limited
|
||||
ad.mu.RLock()
|
||||
pattern := ad.patterns[metricName]
|
||||
assert.LessOrEqual(t, len(pattern.Observations), config.MaxPatternHistory)
|
||||
ad.mu.RUnlock()
|
||||
}
|
||||
|
||||
func TestTimeAnomalyScore(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
// Test business hours (should be normal)
|
||||
businessTime := time.Date(2023, 1, 1, 14, 0, 0, 0, time.UTC) // 2 PM
|
||||
score := ad.calculateTimeAnomalyScore(businessTime)
|
||||
assert.Equal(t, 0.0, score)
|
||||
|
||||
// Test late night (should be suspicious)
|
||||
nightTime := time.Date(2023, 1, 1, 2, 0, 0, 0, time.UTC) // 2 AM
|
||||
score = ad.calculateTimeAnomalyScore(nightTime)
|
||||
assert.Greater(t, score, 0.5)
|
||||
|
||||
// Test evening (should be medium suspicion)
|
||||
eveningTime := time.Date(2023, 1, 1, 20, 0, 0, 0, time.UTC) // 8 PM
|
||||
score = ad.calculateTimeAnomalyScore(eveningTime)
|
||||
assert.Greater(t, score, 0.0)
|
||||
assert.Less(t, score, 0.5)
|
||||
}
|
||||
|
||||
func TestSenderFrequencyCalculation(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
sender := common.HexToAddress("0x123")
|
||||
now := time.Now()
|
||||
|
||||
// Add recent transactions
|
||||
for i := 0; i < 5; i++ {
|
||||
record := &TransactionRecord{
|
||||
Hash: common.HexToHash("0x" + string(rune(i))),
|
||||
From: sender,
|
||||
Value: 1.0,
|
||||
Timestamp: now.Add(-time.Duration(i) * time.Minute),
|
||||
}
|
||||
ad.RecordTransaction(record)
|
||||
}
|
||||
|
||||
// Add old transaction (should not count)
|
||||
oldRecord := &TransactionRecord{
|
||||
Hash: common.HexToHash("0xold"),
|
||||
From: sender,
|
||||
Value: 1.0,
|
||||
Timestamp: now.Add(-2 * time.Hour),
|
||||
}
|
||||
ad.RecordTransaction(oldRecord)
|
||||
|
||||
frequency := ad.calculateSenderFrequency(sender)
|
||||
assert.Equal(t, 5.0, frequency) // Should only count recent transactions
|
||||
}
|
||||
|
||||
func TestAverageGasPriceCalculation(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
transactions := []*TransactionRecord{
|
||||
{GasPrice: 10.0},
|
||||
{GasPrice: 20.0},
|
||||
{GasPrice: 30.0},
|
||||
}
|
||||
|
||||
avgGasPrice := ad.calculateAverageGasPrice(transactions)
|
||||
assert.Equal(t, 20.0, avgGasPrice)
|
||||
|
||||
// Test empty slice
|
||||
emptyAvg := ad.calculateAverageGasPrice([]*TransactionRecord{})
|
||||
assert.Equal(t, 0.0, emptyAvg)
|
||||
}
|
||||
|
||||
func TestMeanAndStdDevCalculation(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
values := []float64{1, 2, 3, 4, 5}
|
||||
mean := ad.calculateMean(values)
|
||||
assert.Equal(t, 3.0, mean)
|
||||
|
||||
stdDev := ad.calculateStdDev(values, mean)
|
||||
expectedStdDev := math.Sqrt(2.0) // For this specific sequence
|
||||
assert.InDelta(t, expectedStdDev, stdDev, 0.001)
|
||||
|
||||
// Test empty slice
|
||||
emptyMean := ad.calculateMean([]float64{})
|
||||
assert.Equal(t, 0.0, emptyMean)
|
||||
|
||||
emptyStdDev := ad.calculateStdDev([]float64{}, 0.0)
|
||||
assert.Equal(t, 0.0, emptyStdDev)
|
||||
}
|
||||
|
||||
func TestAlertGeneration(t *testing.T) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
// Test alert ID generation
|
||||
id1 := ad.generateAlertID()
|
||||
id2 := ad.generateAlertID()
|
||||
assert.NotEqual(t, id1, id2)
|
||||
assert.Contains(t, id1, "anomaly_")
|
||||
|
||||
// Test description generation
|
||||
pattern := &PatternBaseline{
|
||||
Mean: 10.0,
|
||||
}
|
||||
desc := ad.generateAnomalyDescription("test_metric", 15.0, pattern, 2.5)
|
||||
assert.Contains(t, desc, "test_metric")
|
||||
assert.Contains(t, desc, "15.00")
|
||||
assert.Contains(t, desc, "10.00")
|
||||
assert.Contains(t, desc, "2.5")
|
||||
|
||||
// Test recommendations generation
|
||||
recommendations := ad.generateRecommendations("transaction_value", 3.5)
|
||||
assert.NotEmpty(t, recommendations)
|
||||
assert.Contains(t, recommendations[0], "investigation")
|
||||
}
|
||||
|
||||
func BenchmarkRecordTransaction(b *testing.B) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
record := &TransactionRecord{
|
||||
Hash: common.HexToHash("0x123"),
|
||||
From: common.HexToAddress("0xabc"),
|
||||
Value: 1.0,
|
||||
GasPrice: 20.0,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ad.RecordTransaction(record)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRecordMetric(b *testing.B) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ad.RecordMetric("test_metric", float64(i))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCalculateZScore(b *testing.B) {
|
||||
logger := logger.New("info", "text", "")
|
||||
ad := NewAnomalyDetector(logger, nil)
|
||||
|
||||
pattern := &PatternBaseline{
|
||||
Mean: 10.0,
|
||||
StandardDev: 2.0,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ad.calculateZScore(float64(i), pattern)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user