* Copyright (c) 2026 Huawei Technologies Co., Ltd.
* openFuyao is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package analyzer
import (
"os"
"path/filepath"
"testing"
"many-core-orchestrator/api/v1alpha1"
olog "gopkg.openfuyao.cn/common-modules/ologger/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestDetectionRulesEvaluateAndCompute(t *testing.T) {
rules := DefaultDetectionRules()
metrics := NodeMetrics{
LLCMissRate: 0.30,
LLCOccupancy: 0.80,
BioLatencyP99Ms: 60,
PSIAvailable: true,
PSIIOAvg10: 25,
PSIMemAvg10: 12,
PSIMemFullAvg10: 1,
}
detections := rules.Evaluate(metrics)
if len(detections) != 3 {
t.Fatalf("detections=%v, want cache/io/memory", detections)
}
if got := rules.Compute(metrics); got <= 0 || got > 1 {
t.Fatalf("interference level=%f, want within (0,1]", got)
}
}
func TestDefaultRuleEngineAndWeightedCalculator(t *testing.T) {
metrics := NodeMetrics{LLCMissRate: 0.3, LLCOccupancy: 0.8, PSIAvailable: true}
if len((DefaultRuleEngine{}).Evaluate(metrics)) == 0 {
t.Fatal("default rule engine should detect cache pollution")
}
if got := (WeightedCalculator{}).Compute(metrics); got <= 0 {
t.Fatalf("weighted calculator=%f", got)
}
}
func TestRuleConfigStoreEvaluateAndCompute(t *testing.T) {
store := NewRuleConfigStore(Config{}, olog.With("test", "rules"))
metrics := NodeMetrics{LLCMissRate: 0.3, LLCOccupancy: 0.8, PSIAvailable: true}
if len(store.Evaluate(metrics)) == 0 {
t.Fatal("store should evaluate using current rules")
}
if got := store.Compute(metrics); got <= 0 {
t.Fatalf("store compute=%f", got)
}
}
func TestDetectionRulesDegradedPath(t *testing.T) {
rules := DefaultDetectionRules()
metrics := NodeMetrics{
LLCMissRate: 0.10,
LLCOccupancy: 0.50,
BioLatencyP99Ms: 35,
PSIAvailable: false,
MemAvailRatio: 0.75,
PageScanRate: 6400,
}
detections := rules.Evaluate(metrics)
if len(detections) != 0 {
t.Fatalf("detections=%v", detections)
}
got := rules.Compute(metrics)
if got < 0.45 || got > 0.46 {
t.Fatalf("degraded interference level=%f, want about 0.453", got)
}
}
func TestDetectionRulesDegradedMemoryPressureThresholds(t *testing.T) {
rules := DefaultDetectionRules()
tests := []struct {
name string
metrics NodeMetrics
}{
{
name: "low memory available",
metrics: NodeMetrics{
PSIAvailable: false,
MemAvailRatio: 0.69,
PageScanRate: 0,
},
},
{
name: "high page scan rate",
metrics: NodeMetrics{
PSIAvailable: false,
MemAvailRatio: 0.90,
PageScanRate: 10000,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
detections := rules.Evaluate(tt.metrics)
if len(detections) != 1 || detections[0].Type != "memory-pressure" {
t.Fatalf("detections=%v, want memory-pressure", detections)
}
})
}
}
func TestLoadDetectionRulesSpecAppliesDefaults(t *testing.T) {
rules, err := LoadDetectionRulesSpec([]byte(`{"weights":{"cache":0.5},"thresholds":{"cacheMissRate":0.9}}`))
if err != nil {
t.Fatal(err)
}
if rules.Weights.Cache != 0.5 || rules.Weights.Memory != 0.30 || rules.Weights.IO != 0.40 {
t.Fatalf("weights=%+v", rules.Weights)
}
if rules.Thresholds.CacheMissRate != 0.9 || rules.Thresholds.LLCOccupancy != 0.70 {
t.Fatalf("thresholds=%+v", rules.Thresholds)
}
if _, err := LoadDetectionRulesSpec([]byte(`{bad-json`)); err == nil {
t.Fatal("expected invalid json error")
}
}
func TestLoadDetectionRulesFromYAML(t *testing.T) {
path := filepath.Join(t.TempDir(), "rules.yaml")
if err := os.WriteFile(path, []byte("weights:\n cache: 0.6\n"), 0o644); err != nil {
t.Fatal(err)
}
rules, hash, err := LoadDetectionRules(path)
if err != nil {
t.Fatal(err)
}
if rules.Weights.Cache != 0.6 || hash == "" {
t.Fatalf("rules=%+v hash=%q", rules.Weights, hash)
}
if _, _, err := LoadDetectionRules(filepath.Join(t.TempDir(), "missing.yaml")); err == nil {
t.Fatal("expected missing file error")
}
}
func TestRuleConfigStoreApplyTargetAndIgnoreNonTarget(t *testing.T) {
store := NewRuleConfigStore(Config{
DetectionRulesGroup: "mco.many-core.io",
DetectionRulesVersion: "v1alpha1",
DetectionRulesResource: "detectionruleconfigs",
DetectionRulesNamespace: "mco-system",
DetectionRulesName: "cluster",
}, olog.With("test", "rules"))
ignored := &v1alpha1.DetectionRuleConfig{ObjectMeta: metav1.ObjectMeta{Name: "other", Namespace: "mco-system"}}
if err := store.ApplyDetectionRuleConfig("test", ignored); err != nil {
t.Fatal(err)
}
if meta := store.Metadata(); meta.Hash != "" {
t.Fatalf("ignored config should not set metadata: %+v", meta)
}
target := &v1alpha1.DetectionRuleConfig{
ObjectMeta: metav1.ObjectMeta{Name: "cluster", Namespace: "mco-system"},
Spec: v1alpha1.DetectionRuleConfigSpec{
Weights: v1alpha1.DetectionRuleWeights{Cache: 0.7},
},
}
if err := store.ApplyDetectionRuleConfig("test", target); err != nil {
t.Fatal(err)
}
if got := store.rules().Weights.Cache; got != 0.7 {
t.Fatalf("cache weight=%f, want 0.7", got)
}
if meta := store.Metadata(); meta.Hash == "" || meta.LastLoadedTime.IsZero() {
t.Fatalf("metadata not set: %+v", meta)
}
store.KeepLastValidAfterDelete()
if got := store.rules().Weights.Cache; got != 0.7 {
t.Fatalf("delete should keep last valid config, got %f", got)
}
}