* 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 node
import (
"context"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
warmupv1alpha1 "github.com/openfuyao/weight-dispatcher/api/v1alpha1"
)
func TestResolverResolveExplicitNodeNamesDeterministically(t *testing.T) {
t.Parallel()
resolver := NewResolver(fakeClient(t,
nodeFixture("node-b", nil, "10.0.0.2"),
nodeFixture("node-a", nil, "10.0.0.1"),
))
nodes, err := resolver.Resolve(context.Background(), warmupv1alpha1.WarmupTargetSpec{
NodeNames: []string{"node-b", "node-a"},
})
if err != nil {
t.Fatalf("Resolve returned error: %v", err)
}
if len(nodes) != 2 || nodes[0].Name != "node-a" || nodes[1].Name != "node-b" {
t.Fatalf("expected sorted explicit nodes, got %+v", nodes)
}
}
func TestResolverResolveSelectorReturnsSortedNodes(t *testing.T) {
t.Parallel()
resolver := NewResolver(fakeClient(t,
nodeFixture("node-c", map[string]string{"role": "worker"}, "10.0.0.3"),
nodeFixture("node-a", map[string]string{"role": "worker"}, "10.0.0.1"),
nodeFixture("node-b", map[string]string{"role": "infra"}, "10.0.0.2"),
))
nodes, err := resolver.Resolve(context.Background(), warmupv1alpha1.WarmupTargetSpec{
NodeSelector: map[string]string{"role": "worker"},
})
if err != nil {
t.Fatalf("Resolve returned error: %v", err)
}
if len(nodes) != 2 || nodes[0].Name != "node-a" || nodes[1].Name != "node-c" {
t.Fatalf("expected sorted selector nodes, got %+v", nodes)
}
}
func TestResolverGetNodeWrapsMissingNodeError(t *testing.T) {
t.Parallel()
_, err := NewResolver(fakeClient(t)).GetNode(context.Background(), "missing")
if err == nil {
t.Fatalf("expected missing node to fail")
}
}
func TestExtractNodeInternalIPPrefersInternalThenFallback(t *testing.T) {
t.Parallel()
internal := nodeFixture("node-a", nil, "10.0.0.1")
internal.Status.Addresses = append([]corev1.NodeAddress{{
Type: corev1.NodeExternalIP,
Address: "1.1.1.1",
}}, internal.Status.Addresses...)
ip, err := ExtractNodeInternalIP(internal)
if err != nil {
t.Fatalf("ExtractNodeInternalIP returned error: %v", err)
}
if ip != "10.0.0.1" {
t.Fatalf("expected InternalIP to win, got %q", ip)
}
fallback := &corev1.Node{
ObjectMeta: metav1.ObjectMeta{Name: "node-b"},
Status: corev1.NodeStatus{Addresses: []corev1.NodeAddress{{
Type: corev1.NodeHostName,
Address: "node-b.local",
}}},
}
ip, err = ExtractNodeInternalIP(fallback)
if err != nil {
t.Fatalf("ExtractNodeInternalIP fallback returned error: %v", err)
}
if ip != "node-b.local" {
t.Fatalf("expected fallback address, got %q", ip)
}
if _, err := ExtractNodeInternalIP(&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node-c"}}); err == nil {
t.Fatalf("expected node without addresses to fail")
}
}
func fakeClient(t *testing.T, nodes ...*corev1.Node) client.Client {
t.Helper()
scheme := runtime.NewScheme()
if err := corev1.AddToScheme(scheme); err != nil {
t.Fatalf("AddToScheme returned error: %v", err)
}
builder := fake.NewClientBuilder().WithScheme(scheme)
for _, node := range nodes {
builder = builder.WithObjects(node)
}
return builder.Build()
}
func nodeFixture(name string, labels map[string]string, internalIP string) *corev1.Node {
return &corev1.Node{
ObjectMeta: metav1.ObjectMeta{Name: name, Labels: labels},
Status: corev1.NodeStatus{Addresses: []corev1.NodeAddress{{
Type: corev1.NodeInternalIP,
Address: internalIP,
}}},
}
}