Skip to content

Commit 334c644

Browse files
authored
Merge pull request #1188 from emqx/test/e2e/mutation-tests
test(e2e): add simple stress test suite
2 parents ee6cd43 + 18e0cda commit 334c644

16 files changed

Lines changed: 892 additions & 507 deletions

File tree

.github/workflows/test.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,52 @@ jobs:
5858
if-no-files-found: ignore
5959
retention-days: 1
6060

61+
test-stress:
62+
name: E2E Stress Tests
63+
strategy:
64+
fail-fast: true
65+
matrix:
66+
include:
67+
- profile: short
68+
steps: 5
69+
interval: 5s
70+
- profile: long
71+
steps: 15
72+
interval: 10s
73+
74+
runs-on: ubuntu-latest
75+
env:
76+
TEST_E2E_DIAGNOSTIC_REPORT_PATH: ${{ github.workspace }}/test-report
77+
steps:
78+
- uses: actions/checkout@v4
79+
80+
- uses: actions/setup-go@v5
81+
with:
82+
go-version: ${{ env.GO_VERSION }}
83+
84+
- run: go mod tidy
85+
86+
- run: make test-e2e-stress
87+
env:
88+
TEST_E2E_STRESS_STEPS: ${{ matrix.steps }}
89+
TEST_E2E_STRESS_STEP_INTERVAL: ${{ matrix.interval }}
90+
91+
- uses: actions/upload-artifact@v4
92+
if: failure()
93+
with:
94+
name: test-report-stress-${{ matrix.profile }}
95+
path: ${{ env.TEST_E2E_DIAGNOSTIC_REPORT_PATH }}
96+
if-no-files-found: ignore
97+
98+
- run: go tool covdata textfmt -i /tmp/kind/data/emqx-operator-system/test-e2e-coverage -o ${{ github.workspace }}/cover.stress.out
99+
100+
- uses: actions/upload-artifact@v4
101+
with:
102+
name: coverprofile-stress-${{ matrix.profile }}
103+
path: ${{ github.workspace }}/cover.stress.out
104+
if-no-files-found: ignore
105+
retention-days: 1
106+
61107
test-upgrade:
62108
name: E2E Upgrade Tests - ${{ matrix.profile }}
63109
strategy:

Makefile

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,24 +82,31 @@ lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
8282
TEST_E2E_UPGRADE_IMAGE_INITIAL ?= emqx/emqx:5.10.2
8383
TEST_E2E_UPGRADE_IMAGE_UPGRADE ?= emqx/emqx:6.1.0
8484

85+
TEST_E2E_STRESS_STEPS ?= 8
86+
TEST_E2E_STRESS_STEP_INTERVAL ?= 5s
87+
TEST_E2E_STRESS_IMAGE ?= emqx/emqx:6.1.0
88+
8589
.PHONY: test
8690
test: manifests generate fmt vet envtest ## Run tests.
8791
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test -v $$(go list ./... | grep -v /e2e) -coverprofile ./cover.out
8892

89-
# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
90-
# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
91-
# Prometheus and CertManager are installed by default; skip with:
92-
# - PROMETHEUS_INSTALL_SKIP=true
93-
# - CERT_MANAGER_INSTALL_SKIP=true
93+
# Prometheus is installed by default; skip with:
94+
# - TEST_E2E_SKIP_PROMETHEUS_INSTALL=true
9495
.PHONY: test-e2e
9596
test-e2e: manifests generate e2e-test-cluster ## Run general E2E tests. Expected an isolated environment using Kind.
9697
go test ./test/e2e/ -v -ginkgo.v -timeout 60m
9798

9899
test-e2e-upgrade: manifests generate e2e-test-cluster ## Run E2E upgrade tests. Expected an isolated environment using Kind.
99-
go test ./test/e2e/ -v -ginkgo.v -timeout 20m -ginkgo.focus="EMQX Upgrade Test" \
100+
go test ./test/e2e/upgrade -v -ginkgo.v -timeout 20m \
100101
-emqx-image-initial=$(TEST_E2E_UPGRADE_IMAGE_INITIAL) \
101102
-emqx-image-upgrade=$(TEST_E2E_UPGRADE_IMAGE_UPGRADE)
102103

104+
test-e2e-stress: manifests generate e2e-test-cluster ## Run E2E stress tests. Expected an isolated environment using Kind.
105+
go test ./test/e2e/stress -v -ginkgo.v -timeout 20m \
106+
-stress-steps=$(TEST_E2E_STRESS_STEPS) \
107+
-step-interval=$(TEST_E2E_STRESS_STEP_INTERVAL) \
108+
-emqx-image=$(TEST_E2E_STRESS_IMAGE)
109+
103110
.PHONY: test-e2e-helm
104111
test-e2e-helm: e2e-test-cluster ## Run Helm chart E2E tests. Expected an isolated environment using Kind.
105112
go test ./test/e2e-helm/ -v -ginkgo.v -timeout 20m
@@ -143,11 +150,11 @@ doc-crd-v3: ## Generate documentation for the `apps.emqx.io/v3beta1` CRD.
143150
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
144151
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
145152
.PHONY: docker-build
146-
docker-build: ## Build docker image with the manager.
153+
docker-build: generate ## Build docker image with the manager.
147154
$(CONTAINER_TOOL) build -t ${OPERATOR_IMAGE} .
148155

149156
.PHONY: docker-build-coverage
150-
docker-build-coverage: Dockerfile.coverage ## Build docker image with the manager and code coverage enabled.
157+
docker-build-coverage: Dockerfile.coverage generate ## Build docker image with the manager and code coverage enabled.
151158
$(CONTAINER_TOOL) build -t ${OPERATOR_IMAGE} -f Dockerfile.coverage .
152159

153160
.PHONY: docker-push
@@ -162,7 +169,7 @@ docker-push: ## Push docker image with the manager.
162169
# OPERATOR_IMAGE=<myregistry/image:<tag>> then the export will fail)
163170
PLATFORMS ?= linux/arm64,linux/amd64
164171
.PHONY: docker-buildx
165-
docker-buildx: Dockerfile.cross ## Build and push docker image for the manager for cross-platform support
172+
docker-buildx: Dockerfile.cross generate ## Build and push docker image for the manager for cross-platform support
166173
- $(CONTAINER_TOOL) buildx create --name emqx-operator-builder
167174
$(CONTAINER_TOOL) buildx use emqx-operator-builder
168175
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${OPERATOR_IMAGE} -f Dockerfile.cross .

internal/controller/load_state.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,6 @@ func (r *reconcileState) updateReplicantSet(instance *crd.EMQX) *appsv1.ReplicaS
151151
return nil
152152
}
153153

154-
// partOfReplicantSets checks if a pod belongs to any replicant ReplicaSet.
155-
func (r *reconcileState) partOfReplicantSet(pod *corev1.Pod) bool {
156-
for _, rs := range r.replicantSets {
157-
if util.IsPodManagedBy(pod, rs) {
158-
return true
159-
}
160-
}
161-
return false
162-
}
163-
164154
// outdatedReplicantReplicaSets returns all replicant ReplicaSets except the update revision set,
165155
// sorted by creation timestamp (oldest first).
166156
func (r *reconcileState) outdatedReplicantSets(instance *crd.EMQX) []*appsv1.ReplicaSet {

internal/controller/sync_core_set.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -372,27 +372,28 @@ func (s *syncCoreSet) startEvacuation(
372372
// migrationTargetNodes returns the list of EMQX nodes to migrate workloads to.
373373
// * In core-only cluster, targets are pods of the core set.
374374
// * In core-replicant cluster, targets are:
375-
// - pods in the "update" replicant set, if it has at least 1 ready replica,
375+
// - pods in the "update" replicant set, if it has at least 1 node up and running,
376376
// - pods in any replicant set otherwise.
377377
func migrationTargetNodes(r *reconcileRound, instance *crd.EMQX) []string {
378378
targets := []string{}
379+
fallback := []string{}
379380
if instance.Spec.HasReplicants() {
380381
updateReplicantSet := r.state.updateReplicantSet(instance)
381382
if updateReplicantSet == nil {
382383
return targets
383384
}
384-
updateReady := updateReplicantSet.Status.ReadyReplicas > 0
385385
for _, node := range instance.Status.ReplicantNodes {
386386
pod := r.state.podWithName(node.PodName)
387387
if pod == nil {
388388
continue
389389
}
390-
if updateReady && util.IsPodManagedBy(pod, updateReplicantSet) {
391-
targets = append(targets, node.Name)
392-
}
393-
if r.state.partOfReplicantSet(pod) {
390+
if util.IsPodManagedBy(pod, updateReplicantSet) {
394391
targets = append(targets, node.Name)
395392
}
393+
fallback = append(fallback, node.Name)
394+
}
395+
if len(targets) == 0 {
396+
return fallback
396397
}
397398
} else {
398399
for _, node := range instance.Status.CoreNodes {

test/e2e/emqx.go renamed to test/e2e/assert.go

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
Copyright 2026.
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+
117
package e2e
218

319
import (
@@ -8,7 +24,7 @@ import (
824
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
925
)
1026

11-
func checkEMQXReady(g Gomega, afterTime ...metav1.Time) {
27+
func EMQXReady(g Gomega, afterTime ...metav1.Time) {
1228
var cond metav1.Condition
1329
g.Expect(KubectlOut("get", "emqx", "emqx", "-o", "jsonpath={.status.conditions[?(@.type==\"Ready\")]}")).
1430
To(UnmarshalInto(&cond), "Failed to get emqx status")
@@ -24,7 +40,7 @@ func checkEMQXReady(g Gomega, afterTime ...metav1.Time) {
2440
}
2541
}
2642

27-
func checkEMQXStatus(g Gomega, coreReplicas int) {
43+
func CoresStable(g Gomega, coreReplicas int) {
2844
var status crd.CoreNodesStatus
2945
var nodes []crd.EMQXNode
3046
var podList corev1.PodList
@@ -75,7 +91,7 @@ func checkEMQXStatus(g Gomega, coreReplicas int) {
7591
)
7692
}
7793

78-
func checkNoReplicants(g Gomega) {
94+
func NoReplicants(g Gomega) {
7995
g.Expect(KubectlOut("get", "emqx", "emqx",
8096
"-o", "jsonpath={.status.replicantNodesStatus.currentReplicas}",
8197
)).To(Equal("0"), "EMQX cluster status has replicant replicas")
@@ -84,7 +100,7 @@ func checkNoReplicants(g Gomega) {
84100
)).To(BeEmpty(), "EMQX cluster status lists replicant nodes")
85101
}
86102

87-
func checkReplicantStatus(g Gomega, replicantReplicas int) {
103+
func ReplicantsStable(g Gomega, replicantReplicas int) {
88104
var status crd.ReplicantNodesStatus
89105
g.Expect(KubectlOut("get", "emqx", "emqx", "-o", "jsonpath={.status.replicantNodesStatus}")).
90106
To(UnmarshalInto(&status), "Failed to get EMQX replicant nodes status")
@@ -96,10 +112,6 @@ func checkReplicantStatus(g Gomega, replicantReplicas int) {
96112
),
97113
"EMQX status does not have expected number of replicant nodes",
98114
)
99-
checkReplicantNodesStatusRevision(g, status, replicantReplicas)
100-
}
101-
102-
func checkReplicantNodesStatusRevision(g Gomega, status crd.ReplicantNodesStatus, replicas int) {
103115
var podList corev1.PodList
104116
g.Expect(status).To(
105117
And(
@@ -118,12 +130,12 @@ func checkReplicantNodesStatusRevision(g Gomega, status crd.ReplicantNodesStatus
118130
"-o", "json",
119131
)).To(UnmarshalInto(&podList), "Failed to list replicant pods")
120132
g.Expect(podList.Items).To(
121-
HaveLen(replicas),
122-
"EMQX cluster does not have %d current revision replicant pods", replicas,
133+
HaveLen(replicantReplicas),
134+
"EMQX cluster does not have %d current revision replicant pods", replicantReplicas,
123135
)
124136
}
125137

126-
func checkDSReplicationStatus(g Gomega, coreReplicas int) {
138+
func DSReplicationStable(g Gomega, coreReplicas int) {
127139
status := &crd.DSReplicationStatus{}
128140
replicationFactor := min(3, coreReplicas)
129141
g.Expect(KubectlOut("get", "emqx", "emqx", "-o", "jsonpath={.status.dsReplication}")).
@@ -146,7 +158,7 @@ func checkDSReplicationStatus(g Gomega, coreReplicas int) {
146158
)
147159
}
148160

149-
func checkDSReplicationHealthy(g Gomega) {
161+
func DSReplicationHealthy(g Gomega) {
150162
g.Expect(KubectlOut("exec", "service/emqx-listeners", "--", "emqx", "ctl", "ds", "info")).
151163
NotTo(
152164
ContainSubstring("(!)"),

test/e2e/const.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
Copyright 2026.
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+
17+
package e2e
18+
19+
const (
20+
// namespace where the project is deployed in
21+
Namespace = "emqx-operator-system"
22+
)

0 commit comments

Comments
 (0)