/* Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments Copyright (C) ITsysCOM GmbH This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see */ package utils import ( "errors" "reflect" "strings" "testing" "time" ) func TestTrendProfileClone(t *testing.T) { original := &TrendProfile{ Tenant: "cgrates.org", ID: "ID", Schedule: "Schedule", StatID: "StatID", Metrics: []string{"metric1", "metric2"}, TTL: 10 * time.Minute, QueueLength: 100, MinItems: 10, CorrelationType: "average", Tolerance: 0.05, Stored: true, ThresholdIDs: []string{"thresh1", "thresh2"}, } cloned := original.Clone() if cloned.Tenant != original.Tenant { t.Errorf("Expected Tenant %s, but got %s", original.Tenant, cloned.Tenant) } if cloned.ID != original.ID { t.Errorf("Expected ID %s, but got %s", original.ID, cloned.ID) } if cloned.Schedule != original.Schedule { t.Errorf("Expected Schedule %s, but got %s", original.Schedule, cloned.Schedule) } if cloned.StatID != original.StatID { t.Errorf("Expected StatID %s, but got %s", original.StatID, cloned.StatID) } if cloned.QueueLength != original.QueueLength { t.Errorf("Expected QueueLength %d, but got %d", original.QueueLength, cloned.QueueLength) } if cloned.TTL != original.TTL { t.Errorf("Expected TTL %v, but got %v", original.TTL, cloned.TTL) } if cloned.MinItems != original.MinItems { t.Errorf("Expected MinItems %d, but got %d", original.MinItems, cloned.MinItems) } if cloned.CorrelationType != original.CorrelationType { t.Errorf("Expected CorrelationType %s, but got %s", original.CorrelationType, cloned.CorrelationType) } if cloned.Tolerance != original.Tolerance { t.Errorf("Expected Tolerance %f, but got %f", original.Tolerance, cloned.Tolerance) } if cloned.Stored != original.Stored { t.Errorf("Expected Stored %v, but got %v", original.Stored, cloned.Stored) } if !reflect.DeepEqual(cloned.Metrics, original.Metrics) { t.Errorf("Expected Metrics %v, but got %v", original.Metrics, cloned.Metrics) } if !reflect.DeepEqual(cloned.ThresholdIDs, original.ThresholdIDs) { t.Errorf("Expected ThresholdIDs %v, but got %v", original.ThresholdIDs, cloned.ThresholdIDs) } if len(cloned.Metrics) > 0 && &cloned.Metrics[0] == &original.Metrics[0] { t.Errorf("Metrics slice was not deep copied") } if len(cloned.ThresholdIDs) > 0 && &cloned.ThresholdIDs[0] == &original.ThresholdIDs[0] { t.Errorf("ThresholdIDs slice was not deep copied") } } func TestTrendProfileTenantIDAndTrendProfileWithAPIOpts(t *testing.T) { tp := &TrendProfile{ Tenant: "cgrates.org", ID: "trend1", Schedule: "*/5 * * * *", StatID: "StatID", Metrics: []string{"metric1", "metric2"}, TTL: 10 * time.Minute, QueueLength: 100, MinItems: 10, CorrelationType: "average", Tolerance: 0.05, Stored: true, ThresholdIDs: []string{"thresh1", "thresh2"}, } tenantID := tp.TenantID() expectedTenantID := "cgrates.org" + ConcatenatedKeySep + "trend1" if tenantID != expectedTenantID { t.Errorf("Expected TenantID %s, but got %s", expectedTenantID, tenantID) } apiOpts := map[string]any{ "option1": "value1", "option2": 42, } tpWithAPIOpts := &TrendProfileWithAPIOpts{ TrendProfile: tp, APIOpts: apiOpts, } if tpWithAPIOpts.Tenant != "cgrates.org" { t.Errorf("Expected Tenant %s, but got %s", "cgrates.org", tpWithAPIOpts.Tenant) } if tpWithAPIOpts.ID != "trend1" { t.Errorf("Expected ID %s, but got %s", "trend1", tpWithAPIOpts.ID) } expectedAPIOpts := map[string]any{ "option1": "value1", "option2": 42, } if !reflect.DeepEqual(tpWithAPIOpts.APIOpts, expectedAPIOpts) { t.Errorf("Expected APIOpts %v, but got %v", expectedAPIOpts, tpWithAPIOpts.APIOpts) } } func TestIndexesAppendMetric(t *testing.T) { trend := &Trend{ mLast: make(map[string]time.Time), mCounts: make(map[string]int), mTotals: make(map[string]float64), } metric1 := &MetricWithTrend{ID: "metric1", Value: 5.0} metric2 := &MetricWithTrend{ID: "metric2", Value: 3.0} rTime1 := time.Now() rTime2 := rTime1.Add(10 * time.Minute) trend.IndexesAppendMetric(metric1, rTime1) trend.IndexesAppendMetric(metric2, rTime2) trend.IndexesAppendMetric(metric1, rTime2) expectedMLast := map[string]time.Time{ "metric1": rTime2, "metric2": rTime2, } if !reflect.DeepEqual(trend.mLast, expectedMLast) { t.Errorf("Expected mLast %v, but got %v", expectedMLast, trend.mLast) } expectedMCounts := map[string]int{ "metric1": 2, "metric2": 1, } if !reflect.DeepEqual(trend.mCounts, expectedMCounts) { t.Errorf("Expected mCounts %v, but got %v", expectedMCounts, trend.mCounts) } expectedMTotals := map[string]float64{ "metric1": 10.0, "metric2": 3.0, } if !reflect.DeepEqual(trend.mTotals, expectedMTotals) { t.Errorf("Expected mTotals %v, but got %v", expectedMTotals, trend.mTotals) } } func TestTrendTenantID(t *testing.T) { trend := &Trend{ Tenant: "cgrates.org", ID: "ID", RunTimes: []time.Time{ time.Now(), time.Now().Add(-1 * time.Hour), }, Metrics: map[time.Time]map[string]*MetricWithTrend{ time.Now(): { "metric1": {ID: "metric1", Value: 1.5}, "metric2": {ID: "metric2", Value: 2.0}, }, time.Now().Add(-1 * time.Hour): { "metric1": {ID: "metric1", Value: 1.0}, }, }, CompressedMetrics: []byte{0x00, 0x01}, mLast: map[string]time.Time{ "metric1": time.Now(), "metric2": time.Now().Add(-1 * time.Hour), }, mCounts: map[string]int{ "metric1": 2, "metric2": 1, }, mTotals: map[string]float64{ "metric1": 2.5, "metric2": 2.0, }, tPrfl: &TrendProfile{ Tenant: "cgrates.org", ID: "trendProfileID", Schedule: "0 * * * *", StatID: "statID1", QueueLength: 10, TTL: 5 * time.Minute, MinItems: 1, CorrelationType: "average", Tolerance: 0.1, Stored: true, ThresholdIDs: []string{"threshold1", "threshold2"}, }, } tenantID := trend.TenantID() expectedTenantID := "cgrates.org:ID" if tenantID != expectedTenantID { t.Errorf("Expected TenantID %v, but got %v", expectedTenantID, tenantID) } if len(trend.RunTimes) != 2 { t.Errorf("Expected 2 run times, but got %d", len(trend.RunTimes)) } if len(trend.Metrics) != 2 { t.Errorf("Expected 2 metrics time entries, but got %d", len(trend.Metrics)) } if trend.tPrfl.QueueLength != 10 { t.Errorf("Expected QueueLength 10, but got %d", trend.tPrfl.QueueLength) } } func TestTComputeIndexes(t *testing.T) { runTime1 := time.Now() runTime2 := runTime1.Add(time.Minute) trend := &Trend{ RunTimes: []time.Time{runTime1, runTime2}, Metrics: map[time.Time]map[string]*MetricWithTrend{ runTime1: { "metric1": &MetricWithTrend{ID: "metric1", Value: 10.0}, "metric2": &MetricWithTrend{ID: "metric2", Value: 20.0}, }, runTime2: { "metric1": &MetricWithTrend{ID: "metric1", Value: 15.0}, }, }, } trend.computeIndexes() if trend.mLast["metric1"] != runTime2 { t.Errorf("Expected last time for metric1 to be %v, got %v", runTime2, trend.mLast["metric1"]) } if trend.mCounts["metric1"] != 2 { t.Errorf("Expected count for metric1 to be 2, got %d", trend.mCounts["metric1"]) } if trend.mTotals["metric1"] != 25.0 { t.Errorf("Expected total for metric1 to be 25.0, got %f", trend.mTotals["metric1"]) } if trend.mLast["metric2"] != runTime1 { t.Errorf("Expected last time for metric2 to be %v, got %v", runTime1, trend.mLast["metric2"]) } if trend.mCounts["metric2"] != 1 { t.Errorf("Expected count for metric2 to be 1, got %d", trend.mCounts["metric2"]) } if trend.mTotals["metric2"] != 20.0 { t.Errorf("Expected total for metric2 to be 20.0, got %f", trend.mTotals["metric2"]) } } func TestGetTrendLabel(t *testing.T) { tests := []struct { tGrowth float64 tolerance float64 expected string }{ {1.0, 0.5, MetaPositive}, {-1.0, 0.5, MetaNegative}, {0.0, 0.5, MetaConstant}, {0.3, 0.5, MetaConstant}, {-0.3, 0.5, MetaConstant}, {0.6, 0.5, MetaPositive}, {-0.6, 0.5, MetaNegative}, } for _, test := range tests { result := GetTrendLabel(test.tGrowth, test.tolerance) if result != test.expected { t.Errorf("For tGrowth: %f and tolerance: %f, expected %s, got %s", test.tGrowth, test.tolerance, test.expected, result) } } } func TestGetTrendGrowth(t *testing.T) { trend := Trend{ mLast: map[string]time.Time{}, Metrics: map[time.Time]map[string]*MetricWithTrend{}, mTotals: map[string]float64{}, mCounts: map[string]int{}, } _, err := trend.GetTrendGrowth("unknownID", 100, MetaLast, 2) if !errors.Is(err, ErrNotFound) { t.Errorf("Expected error ErrNotFound, got: %v", err) } now := time.Now() trend.mLast["metric1"] = now _, err = trend.GetTrendGrowth("metric1", 100, MetaLast, 2) if !errors.Is(err, ErrNotFound) { t.Errorf("Expected error ErrNotFound, got: %v", err) } trend.Metrics = map[time.Time]map[string]*MetricWithTrend{ now: { "metric1": {ID: "metric1", Value: 80}, }, } got, err := trend.GetTrendGrowth("metric1", 100, MetaLast, 2) if err != nil || got != 25.0 { t.Errorf("Mismatch for MetaLast correlation. Got: %v, expected: %v", got, 25.0) } trend.mTotals = map[string]float64{ "metric1": 400, } trend.mCounts = map[string]int{ "metric1": 4, } got, err = trend.GetTrendGrowth("metric1", 120, MetaAverage, 2) if err != nil || got != 20.0 { t.Errorf("Mismatch for MetaAverage correlation. Got: %v, expected: %v", got, 20.0) } _, err = trend.GetTrendGrowth("metric1", 100, "invalidCorrelation", 2) if !errors.Is(err, ErrCorrelationUndefined) { t.Errorf("Expected error ErrCorrelationUndefined, got: %v", err) } } func TestNewTrendFromProfile(t *testing.T) { profile := &TrendProfile{ Tenant: "cgrates.org", ID: "trendProfileID", Schedule: "@every 1sec", StatID: "statID1", QueueLength: 10, TTL: 5 * time.Minute, MinItems: 1, CorrelationType: "average", Tolerance: 0.1, Stored: true, ThresholdIDs: []string{"threshold1", "threshold2"}, } trend := NewTrendFromProfile(profile) if trend.Tenant != profile.Tenant { t.Errorf("Expected Tenant %s, got %s", profile.Tenant, trend.Tenant) } if trend.ID != profile.ID { t.Errorf("Expected ID %s, got %s", profile.ID, trend.ID) } if trend.RunTimes == nil { t.Errorf("Expected RunTimes to be initialized, got nil") } if len(trend.RunTimes) != 0 { t.Errorf("Expected RunTimes to be empty, got length %d", len(trend.RunTimes)) } if trend.Metrics == nil { t.Errorf("Expected Metrics to be initialized, got nil") } if len(trend.Metrics) != 0 { t.Errorf("Expected Metrics to be empty, got length %d", len(trend.Metrics)) } if trend.tPrfl != profile { t.Errorf("Expected tPrfl to point to the original profile, got a different value") } } func TestTrendProfileFieldAsString(t *testing.T) { tests := []struct { name string fldPath []string err error val any }{ {ID, []string{ID}, nil, "Trend1"}, {Tenant, []string{Tenant}, nil, "cgrates.org"}, {Schedule, []string{Schedule}, nil, "@every 1m"}, {StatID, []string{StatID}, nil, "Stat1"}, {Metrics, []string{Metrics + "[0]"}, nil, "*acc"}, {Metrics, []string{Metrics + "[1]"}, nil, "*tcd"}, {TTL, []string{TTL}, nil, 10 * time.Minute}, {QueueLength, []string{QueueLength}, nil, 100}, {MinItems, []string{MinItems}, nil, 10}, {CorrelationType, []string{CorrelationType}, nil, "*average"}, {Tolerance, []string{Tolerance}, nil, 0.05}, {Stored, []string{Stored}, nil, true}, {ThresholdIDs, []string{ThresholdIDs + "[0]"}, nil, "Thresh1"}, {ThresholdIDs, []string{ThresholdIDs + "[1]"}, nil, "Thresh2"}, {"NonExistingField", []string{"Field1"}, ErrNotFound, nil}, } rp := &TrendProfile{ Tenant: "cgrates.org", ID: "Trend1", Schedule: "@every 1m", StatID: "Stat1", Metrics: []string{"*acc", "*tcd"}, TTL: 10 * time.Minute, QueueLength: 100, MinItems: 10, CorrelationType: "*average", Tolerance: 0.05, Stored: true, ThresholdIDs: []string{"Thresh1", "Thresh2"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { val, err := rp.FieldAsInterface(tc.fldPath) if tc.err != nil { if err == nil { t.Error("expect to receive an error") } if tc.err != err { t.Errorf("expected %v,received %v", tc.err, err) } return } if err != nil { t.Errorf("unexpected error %v", err) } if val != tc.val { t.Errorf("expected %v,received %v", tc.val, val) } }) } } func TestTrendCleanUp(t *testing.T) { tm := time.Now().Add(-19 * time.Second) tm2 := tm.Add(15 * time.Second) tm3 := time.Now().Add(1) tm4 := time.Now().Add(-5 * time.Second) tm5 := time.Now().Add(-3 * time.Second) tr := &Trend{ Tenant: "cgrates.org", ID: "TREND1", RunTimes: []time.Time{ tm, tm2, tm3, tm4, tm5, }, Metrics: map[time.Time]map[string]*MetricWithTrend{ tm: { MetaTCC: {ID: MetaTCC, Value: 13, TrendGrowth: -1, TrendLabel: NotAvailable}, MetaACC: {ID: MetaACC, Value: 13, TrendGrowth: -1, TrendLabel: NotAvailable}, }, tm2: { MetaTCC: {ID: MetaTCC, Value: 30, TrendGrowth: 120, TrendLabel: MetaPositive}, MetaACC: {ID: MetaACC, Value: 15, TrendGrowth: 4, TrendLabel: MetaPositive}, }, tm3: { MetaTCC: {ID: MetaTCC, Value: 30, TrendGrowth: 120, TrendLabel: MetaPositive}, MetaACC: {ID: MetaACC, Value: 15, TrendGrowth: 4, TrendLabel: MetaPositive}, }, tm4: { MetaTCC: {ID: MetaTCC, Value: 30, TrendGrowth: 120, TrendLabel: MetaPositive}, MetaACC: {ID: MetaACC, Value: 15, TrendGrowth: 4, TrendLabel: MetaPositive}, }, tm5: { MetaTCC: {ID: MetaTCC, Value: 30, TrendGrowth: 120, TrendLabel: MetaPositive}, MetaACC: {ID: MetaACC, Value: 15, TrendGrowth: 4, TrendLabel: MetaPositive}, }, }, } tests := []struct { name string ttl time.Duration tr *Trend qLength int altered bool runtimesmetriclens int }{ {"TTLlgThan0", 10 * time.Second, tr, 3, true, 3}, {"QueueLenLg0", -1, tr, 2, true, 2}, {"TLLExpiredAll", 2 * time.Second, tr, 0, true, 0}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { altered := tc.tr.cleanup(tc.ttl, tc.qLength) if tc.altered { if !altered { t.Errorf("expected trend to be altered") } if len(tc.tr.RunTimes) != tc.runtimesmetriclens || len(tc.tr.Metrics) != tc.runtimesmetriclens { t.Errorf("expected len to be %d,got %d metrics,%d runtimes", tc.runtimesmetriclens, len(tc.tr.Metrics), len(tc.tr.RunTimes)) } return } if altered { t.Error("expected trend to not be altered") } }) } } func TestTrendProfileString(t *testing.T) { profile := &TrendProfile{ Tenant: "cgrates.org", ID: "Trend1", Schedule: "@every 1m", StatID: "Stat1", Metrics: []string{"*acc", "*tcd"}, TTL: 10 * time.Minute, QueueLength: 100, MinItems: 10, CorrelationType: "*average", Tolerance: 0.05, Stored: true, ThresholdIDs: []string{"Thresh1", "Thresh2"}, } jsonStr := profile.String() if jsonStr == "" { t.Errorf("Expected non-empty JSON string representation of TrendProfile") } expectedFields := []string{ `"cgrates.org"`, `"Trend1"`, `"@every 1m"`, `"Stat1"`, `"*acc"`, `"*tcd"`, `"*average"`, `true`, `"Thresh1"`, `"Thresh2"`, } for _, field := range expectedFields { if !strings.Contains(jsonStr, field) { t.Errorf("Expected JSON output to contain field: %s", field) } } } func TestTrendProfileFieldAssString(t *testing.T) { tests := []struct { name string fldPath []string expected string hasError bool }{ { name: "Valid Tenant Field", fldPath: []string{"Tenant"}, expected: "cgrates.org", hasError: false, }, { name: "Valid ID Field", fldPath: []string{"ID"}, expected: "Trend1", hasError: false, }, { name: "Valid Schedule Field", fldPath: []string{"Schedule"}, expected: "@every 1m", hasError: false, }, { name: "Invalid Field Path", fldPath: []string{"NonExistentField"}, expected: "", hasError: true, }, } tp := &TrendProfile{ Tenant: "cgrates.org", ID: "Trend1", Schedule: "@every 1m", StatID: "Stat1", Metrics: []string{"*acc", "*tcd"}, TTL: 10 * time.Minute, QueueLength: 100, MinItems: 10, CorrelationType: "*average", Tolerance: 0.05, Stored: true, ThresholdIDs: []string{"Thresh1", "Thresh2"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := tp.FieldAsString(tt.fldPath) if tt.hasError && err == nil { t.Errorf("Expected error for fldPath %v, but got none", tt.fldPath) } else if !tt.hasError && err != nil { t.Errorf("Expected no error for fldPath %v, but got: %v", tt.fldPath, err) } if result != tt.expected { t.Errorf("For fldPath %v, expected %v, but got %v", tt.fldPath, tt.expected, result) } }) } } func TestTrendProfileSet(t *testing.T) { tp := &TrendProfile{ Tenant: "cgrates.org", ID: "Trend1", Schedule: "@every 1m", StatID: "Stat1", Metrics: []string{"*acc", "*tcd"}, TTL: 10 * time.Minute, QueueLength: 100, MinItems: 10, CorrelationType: "*average", Tolerance: 0.05, Stored: true, ThresholdIDs: []string{"Thresh1", "Thresh2"}, } tests := []struct { name string path []string val any expected any hasError bool }{ { name: "Set Tenant", path: []string{Tenant}, val: "newTenant", expected: "newTenant", hasError: false, }, { name: "Set ID", path: []string{ID}, val: "newID", expected: "newID", hasError: false, }, { name: "Set Schedule", path: []string{Schedule}, val: "@every 2m", expected: "@every 2m", hasError: false, }, { name: "Set StatID", path: []string{StatID}, val: "newStatID", expected: "newStatID", hasError: false, }, { name: "Set Metrics", path: []string{Metrics}, val: []string{"*newMetric"}, expected: []string{"*acc", "*tcd", "*newMetric"}, hasError: false, }, { name: "Set TTL", path: []string{TTL}, val: "15m", expected: 15 * time.Minute, hasError: false, }, { name: "Set QueueLength", path: []string{QueueLength}, val: 50, expected: 50, hasError: false, }, { name: "Set MinItems", path: []string{MinItems}, val: 20, expected: 20, hasError: false, }, { name: "Set CorrelationType", path: []string{CorrelationType}, val: "*sum", expected: "*sum", hasError: false, }, { name: "Set Tolerance", path: []string{Tolerance}, val: 0.1, expected: 0.1, hasError: false, }, { name: "Set Stored", path: []string{Stored}, val: false, expected: false, hasError: false, }, { name: "Set ThresholdIDs", path: []string{ThresholdIDs}, val: []string{"Thresh3", "Thresh4"}, expected: []string{"Thresh1", "Thresh2", "Thresh3", "Thresh4"}, hasError: false, }, { name: "Set Invalid Path", path: []string{"InvalidPath"}, val: "invalid", expected: nil, hasError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tp.Set(tt.path, tt.val, false) if tt.hasError && err == nil { t.Errorf("Expected error for path %v, but got none", tt.path) } else if !tt.hasError && err != nil { t.Errorf("Expected no error for path %v, but got: %v", tt.path, err) } switch tt.path[0] { case Tenant: if tp.Tenant != tt.expected { t.Errorf("For path %v, expected %v, but got %v", tt.path, tt.expected, tp.Tenant) } case ID: if tp.ID != tt.expected { t.Errorf("For path %v, expected %v, but got %v", tt.path, tt.expected, tp.ID) } case Schedule: if tp.Schedule != tt.expected { t.Errorf("For path %v, expected %v, but got %v", tt.path, tt.expected, tp.Schedule) } case StatID: if tp.StatID != tt.expected { t.Errorf("For path %v, expected %v, but got %v", tt.path, tt.expected, tp.StatID) } case Metrics: if len(tp.Metrics) != len(tt.expected.([]string)) { t.Errorf("For path %v, expected %v, but got %v", tt.path, tt.expected, tp.Metrics) } else { for i := range tp.Metrics { if tp.Metrics[i] != tt.expected.([]string)[i] { t.Errorf("For path %v, expected %v, but got %v", tt.path, tt.expected, tp.Metrics) break } } } case TTL: if tp.TTL != tt.expected { t.Errorf("For path %v, expected %v, but got %v", tt.path, tt.expected, tp.TTL) } case QueueLength: if tp.QueueLength != tt.expected { t.Errorf("For path %v, expected %v, but got %v", tt.path, tt.expected, tp.QueueLength) } case MinItems: if tp.MinItems != tt.expected { t.Errorf("For path %v, expected %v, but got %v", tt.path, tt.expected, tp.MinItems) } case CorrelationType: if tp.CorrelationType != tt.expected { t.Errorf("For path %v, expected %v, but got %v", tt.path, tt.expected, tp.CorrelationType) } case Tolerance: if tp.Tolerance != tt.expected { t.Errorf("For path %v, expected %v, but got %v", tt.path, tt.expected, tp.Tolerance) } case Stored: if tp.Stored != tt.expected { t.Errorf("For path %v, expected %v, but got %v", tt.path, tt.expected, tp.Stored) } case ThresholdIDs: if len(tp.ThresholdIDs) != len(tt.expected.([]string)) { t.Errorf("For path %v, expected %v, but got %v", tt.path, tt.expected, tp.ThresholdIDs) } else { for i := range tp.ThresholdIDs { if tp.ThresholdIDs[i] != tt.expected.([]string)[i] { t.Errorf("For path %v, expected %v, but got %v", tt.path, tt.expected, tp.ThresholdIDs) break } } } } }) } } func TestTrendProfileMergeV2(t *testing.T) { tp1 := &TrendProfile{ Tenant: "tenant1", ID: "id1", Schedule: "schedule1", StatID: "stat1", Metrics: []string{"metric1", "metric2"}, ThresholdIDs: []string{"threshold1"}, Stored: true, TTL: 10, QueueLength: 100, MinItems: 5, CorrelationType: "type1", Tolerance: 0.5, } tp2 := &TrendProfile{ Tenant: "tenant2", ID: "id2", Schedule: "schedule2", StatID: "stat2", Metrics: []string{"metric3", "metric4"}, ThresholdIDs: []string{"threshold2"}, Stored: true, TTL: 20, QueueLength: 200, MinItems: 10, CorrelationType: "type2", Tolerance: 1.5, } tp1.Merge(tp2) if tp1.Tenant != "tenant2" { t.Errorf("Expected Tenant to be 'tenant2', but got: %s", tp1.Tenant) } if tp1.ID != "id2" { t.Errorf("Expected ID to be 'id2', but got: %s", tp1.ID) } if tp1.Schedule != "schedule2" { t.Errorf("Expected Schedule to be 'schedule2', but got: %s", tp1.Schedule) } if tp1.StatID != "stat2" { t.Errorf("Expected StatID to be 'stat2', but got: %s", tp1.StatID) } expectedMetrics := []string{"metric1", "metric2", "metric3", "metric4"} if len(tp1.Metrics) != len(expectedMetrics) { t.Errorf("Expected Metrics to be %v, but got: %v", expectedMetrics, tp1.Metrics) } expectedThresholdIDs := []string{"threshold1", "threshold2"} if len(tp1.ThresholdIDs) != len(expectedThresholdIDs) { t.Errorf("Expected ThresholdIDs to be %v, but got: %v", expectedThresholdIDs, tp1.ThresholdIDs) } if tp1.Stored != true { t.Errorf("Expected Stored to be 'true', but got: %v", tp1.Stored) } if tp1.TTL != 20 { t.Errorf("Expected TTL to be 20, but got: %d", tp1.TTL) } if tp1.QueueLength != 200 { t.Errorf("Expected QueueLength to be 200, but got: %d", tp1.QueueLength) } if tp1.MinItems != 10 { t.Errorf("Expected MinItems to be 10, but got: %d", tp1.MinItems) } if tp1.CorrelationType != "type2" { t.Errorf("Expected CorrelationType to be 'type2', but got: %s", tp1.CorrelationType) } if tp1.Tolerance != 1.5 { t.Errorf("Expected Tolerance to be 1.5, but got: %f", tp1.Tolerance) } }