Skip to content

Commit d798059

Browse files
test(e2e): add scheduler policy E2E suite (#2793)
* test(e2e): add scheduler policy E2E suite Add a Ginkgo E2E suite that asserts deterministic placement for HAMi's node/GPU binpack and spread policies, comma-separated policy chains, and per-Pod device scoring weight overrides. Adds a `make e2e-policy-test` target and `hack/e2e-policy-test.sh` runner. The suite waits on the scheduler's in-memory cache (via /metrics) so placement assertions never observe stale utilization: pods are confirmed released from every scheduler replica's cache after deletion and namespace cleanup. Metrics are scraped from all running scheduler-extender replicas rather than a guessed leader, so the suite is correct under HA and across leader changes. GPU allocation decoding tolerates the trailing-separator quirk of EncodePodSingleDevice instead of hard-coding the decoded length. 🤖 Assisted-by: Claude Code Signed-off-by: Tim <tim.wang03@sap.com> * address latest comment Signed-off-by: Tim <tim.wang03@sap.com> * fix(e2e): wait for scheduler GPU registration after device-plugin ready After labeling a node and waiting for the device-plugin to become Ready, the scheduler still needs time to process the node update and register the node's GPU capacity. Without this wait, pods may fail to schedule with '1 node unregistered' errors. Add schedulerCanScheduleGPU check in ensureGPUNodeLabeled to verify the node has GPU resources in its Capacity/Allocatable before proceeding. Signed-off-by: Tim <tim.wang03@sap.com> * fix(e2e): abort immediately on terminal pod phases in policy tests waitForAllocatedPod previously polled for the full 5-minute timeout even when a pod reached the terminal Failed phase (which can never transition to Running). This caused the first policy spec to block for 300 seconds before reporting failure, and the Ordered/Serial suite then skipped all remaining specs. Use gomega.StopTrying to abort the Eventually loop as soon as the pod enters Failed or Succeeded, and surface container exit codes and termination reasons in the failure message so the root cause is immediately visible in CI logs. Signed-off-by: Tim <tim.wang03@sap.com> * fix(e2e): harden policy suite against device-plugin transitions The policy E2E suite fails when the previous pod suite's cleanup removes the gpu=on label and the policy suite re-adds it within ~133ms. The DaemonSet controller may terminate the device-plugin pod after our readiness checks pass, causing container startup failures. Three layered fixes: 1. Add hasTerminatingDevicePlugin check to ensureGPUNodeLabeled and registeredGPUNodes — blocks test start while a device-plugin pod is still being torn down. 2. Switch RestartPolicy from Never to OnFailure — kubelet retries container startup through transient device-plugin unavailability instead of parking in permanent Failed phase. 3. Add StopTrying + podTerminalReason in waitForAllocatedPod — if a pod still reaches terminal Failed, abort in ~2s with exit codes and termination reasons instead of burning the full 5min timeout. Signed-off-by: Tim <tim.wang03@sap.com> * fix(e2e): wait for device-plugin healthy devices before creating GPU pods The Pod E2E suite creates GPU pods immediately after adding the gpu=on label, without waiting for the device-plugin to register healthy devices with kubelet. This causes intermittent 'no healthy devices present' admission errors when the device-plugin hasn't finished initializing. Add WaitForDevicePluginReady to test/utils that polls until: - At least one device-plugin pod is Running and Ready on the node - No device-plugin pods are terminating (DaemonSet revision transition) - The node reports GPU resources in Allocatable (healthy device registration) Call it from the Pod E2E BeforeAll after labeling the node. Signed-off-by: Tim <tim.wang03@sap.com> --------- Signed-off-by: Tim <tim.wang03@sap.com>
1 parent 9754da5 commit d798059

6 files changed

Lines changed: 1022 additions & 13 deletions

File tree

‎Makefile‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,7 @@ e2e-mig-test:
122122
.PHONY: e2e-mig-smoke-test
123123
e2e-mig-smoke-test:
124124
./hack/e2e-mig-smoke-test.sh "${KUBE_CONF}"
125+
126+
.PHONY: e2e-policy-test
127+
e2e-policy-test:
128+
HAMI_E2E_REQUIRE_POLICY_TOPOLOGY=true bash ./hack/e2e-policy-test.sh "${KUBE_CONF}"

‎hack/e2e-policy-test.sh‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env bash
2+
# Copyright 2026 The HAMi Authors.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
set -o errexit
17+
set -o nounset
18+
set -o pipefail
19+
20+
KUBE_CONF=${1:-"${HOME}/.kube/config"}
21+
export KUBE_CONF
22+
23+
REPO_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
24+
cd "${REPO_ROOT}"
25+
26+
if [ ! -f "${KUBE_CONF}" ]; then
27+
echo "Error: kubeconfig file not found at ${KUBE_CONF}" >&2
28+
exit 1
29+
fi
30+
31+
if ! kubectl --kubeconfig "${KUBE_CONF}" get nodes >/dev/null; then
32+
echo "Error: cannot reach Kubernetes API server via ${KUBE_CONF}" >&2
33+
exit 1
34+
fi
35+
36+
GINKGO_VERSION=$(go list -m -f '{{.Version}}' github.com/onsi/ginkgo/v2)
37+
if [ -z "${GINKGO_VERSION}" ]; then
38+
echo "Error: could not determine Ginkgo version from go.mod" >&2
39+
exit 1
40+
fi
41+
42+
go run "github.com/onsi/ginkgo/v2/ginkgo@${GINKGO_VERSION}" \
43+
run -v --procs=1 ./test/e2e/policy/ -- --kubeconfig="${KUBE_CONF}"

‎test/e2e/pod/test_pod.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ var _ = ginkgo.Describe("Pod E2E Tests", ginkgo.Ordered, func() {
5757
ginkgo.By("Adding node labeling")
5858
_, err = utils.AddNodeLabel(clientSet, nodeName, NodeLabelKey, NodeLabelValue)
5959
gomega.Expect(err).NotTo(gomega.HaveOccurred())
60+
61+
ginkgo.By("Waiting for device-plugin to register healthy GPU devices")
62+
err = utils.WaitForDevicePluginReady(clientSet, nodeName, 5*time.Minute, 2*time.Second)
63+
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "device-plugin did not become ready on node %s", nodeName)
6064
})
6165

6266
ginkgo.AfterEach(func() {

0 commit comments

Comments
 (0)