1 package slowcooker
2
3 import (
4 "context"
5 "embed"
6 "encoding/json"
7 "fmt"
8 "io"
9 "io/fs"
10 "os"
11 "regexp"
12 "strconv"
13 "strings"
14 "testing"
15 "time"
16
17 "github.com/stretchr/testify/assert"
18 "github.com/stretchr/testify/require"
19 "gotest.tools/v3/assert/cmp"
20 "gotest.tools/v3/poll"
21 batchv1 "k8s.io/api/batch/v1"
22 corev1 "k8s.io/api/core/v1"
23 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24 k8stypes "k8s.io/apimachinery/pkg/types"
25 "sigs.k8s.io/controller-runtime/pkg/client"
26
27 "edge-infra.dev/pkg/k8s/testing/kmp"
28 "edge-infra.dev/pkg/k8s/unstructured"
29 "edge-infra.dev/test/f2"
30 "edge-infra.dev/test/f2/integration"
31 "edge-infra.dev/test/f2/x/ktest"
32 "edge-infra.dev/test/f2/x/ktest/envtest"
33 "edge-infra.dev/test/f2/x/ktest/kustomization"
34 )
35
36
37 var manifests embed.FS
38
39 var f f2.Framework
40
41 var slowcookerManifests []byte
42
43 const slowcooker = "slow-cooker"
44
45 type Result struct {
46 P50 int `json:"p50"`
47 P75 int `json:"p75"`
48 P90 int `json:"p90"`
49 P95 int `json:"p95"`
50 P99 int `json:"p99"`
51 P999 int `json:"p999"`
52 }
53
54 func TestMain(m *testing.M) {
55 f = f2.New(
56 context.Background(),
57 f2.WithExtensions(
58 ktest.New(
59 ktest.SkipNamespaceCreation(),
60 ktest.WithEnvtestOptions(
61 envtest.WithoutCRDs(),
62 ),
63 ),
64 ),
65 ).
66 Setup(func(ctx f2.Context) (f2.Context, error) {
67 if !integration.IsL2() {
68 return ctx, fmt.Errorf("%w: requires L2 integration test level", f2.ErrSkip)
69 }
70 return ctx, nil
71 }).
72 Setup(func(ctx f2.Context) (f2.Context, error) {
73
74 var err error
75 slowcookerManifests, err = fs.ReadFile(manifests, "testdata/slow-cooker_manifests.yaml")
76 if err != nil {
77 return ctx, err
78 }
79 return ctx, nil
80 }).
81 WithLabel("dsds", "true").
82 WithLabel("performance", "true").
83 Slow()
84 os.Exit(f.Run(m))
85 }
86
87 func TestSlowcooker(t *testing.T) {
88 var iterations int
89 var podName string
90 var logs string
91 var k *ktest.K8s
92 var manifests []*unstructured.Unstructured
93 var err error
94 feature := f2.NewFeature("slowcooker").
95 Setup("apply slowcooker manifests", func(ctx f2.Context, t *testing.T) f2.Context {
96 k = ktest.FromContextT(ctx, t)
97 manifests, err = kustomization.ProcessManifests(ctx.RunID, slowcookerManifests, slowcooker)
98 require.NoError(t, err)
99
100 for _, manifest := range manifests {
101 require.NoError(t, k.Client.Create(ctx, manifest))
102 }
103 return ctx
104 }).
105 Setup("wait for slowcooker manifests", func(ctx f2.Context, t *testing.T) f2.Context {
106 job := &batchv1.Job{}
107 for _, manifest := range manifests {
108 if manifest.GetKind() == "Job" && manifest.GetName() == slowcooker {
109 require.NoError(t, unstructured.FromUnstructured(manifest, job))
110 for _, arg := range job.Spec.Template.Spec.Containers[0].Args {
111 if strings.Contains(arg, "iterations") {
112 iterations, err = strconv.Atoi(strings.TrimPrefix(arg, "--iterations="))
113 require.NoError(t, err)
114 }
115 }
116 k.WaitOn(t, k.Check(manifest, kmp.IsCurrent()), poll.WithTimeout(time.Minute))
117 }
118 }
119 return ctx
120 }).
121 Setup("wait for slowcooker pod to be ready", func(ctx f2.Context, t *testing.T) f2.Context {
122 podName, err = getPodName(ctx, k)
123 require.NoError(t, err)
124
125 pod := &corev1.Pod{}
126 require.NoError(t, k.Client.Get(ctx, k8stypes.NamespacedName{Name: podName, Namespace: slowcooker}, pod))
127
128 k.WaitOn(t, k.Check(pod, func(_ client.Object) cmp.Result {
129 require.NoError(t, k.Client.Get(ctx, k8stypes.NamespacedName{Name: podName, Namespace: slowcooker}, pod))
130 if isPodReady(pod) {
131 return cmp.ResultSuccess
132 }
133 return cmp.ResultFailure("slowcooker pod is not ready")
134 }), poll.WithTimeout(time.Minute*2))
135
136 return ctx
137 }).
138 Setup("get slowcooker logs", func(ctx f2.Context, t *testing.T) f2.Context {
139 pod := &corev1.Pod{}
140 require.NoError(t, k.Client.Get(ctx, k8stypes.NamespacedName{Name: podName, Namespace: slowcooker}, pod))
141
142 k.WaitOn(t, k.Check(pod, func(_ client.Object) cmp.Result {
143 require.NoError(t, k.Client.Get(ctx, k8stypes.NamespacedName{Name: podName, Namespace: slowcooker}, pod))
144 if !isPodReady(pod) {
145 return cmp.ResultSuccess
146 }
147 return cmp.ResultFailure("slow cooker container still running")
148 }), poll.WithTimeout(time.Minute*time.Duration(iterations+2)))
149
150 logs, err = getLogs(ctx, k, podName)
151 require.NoError(t, err)
152 fmt.Println(logs)
153 return ctx
154 }).
155 Test("over 99 percent average throughput", func(ctx f2.Context, t *testing.T) f2.Context {
156 averageThroughput, err := calculateAverageThroughput(logs, iterations)
157 require.NoError(t, err)
158 fmt.Printf("Average throughput: %v%%\n", averageThroughput)
159 assert.GreaterOrEqual(t, averageThroughput, float32(99.0))
160 return ctx
161 }).
162 Test("over 95 percent successful requests", func(ctx f2.Context, t *testing.T) f2.Context {
163 successfulRequests, err := calculateSuccessfulRequests(logs)
164 require.NoError(t, err)
165 fmt.Printf("Successful requests: %v%%\n", successfulRequests)
166 assert.GreaterOrEqual(t, successfulRequests, float32(95.0))
167 return ctx
168 }).
169 Test("99th percentile latency less than 2ms", func(ctx f2.Context, t *testing.T) f2.Context {
170 latency, err := getHighestLatency(logs)
171 require.NoError(t, err)
172 fmt.Printf("Latency: %vms\n", latency)
173 assert.LessOrEqual(t, latency, 2)
174 return ctx
175 }).
176 Teardown("delete namespace", func(ctx f2.Context, t *testing.T) f2.Context {
177 require.NoError(t, k.Client.Delete(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: slowcooker}}))
178 return ctx
179 }).Feature()
180 f.Test(t, feature)
181 }
182
183 func getPodName(ctx context.Context, k *ktest.K8s) (string, error) {
184 podList := &corev1.PodList{}
185 err := k.Client.List(ctx, podList, &client.ListOptions{Namespace: slowcooker})
186 if err != nil {
187 return "", err
188 }
189 for _, pod := range podList.Items {
190 if strings.Contains(pod.Name, slowcooker) {
191 return pod.Name, nil
192 }
193 }
194 return "", fmt.Errorf("slow-cooker pod not found")
195 }
196
197 func isPodReady(pod *corev1.Pod) bool {
198 for _, condition := range pod.Status.Conditions {
199 if condition.Type == "Ready" && condition.Status == corev1.ConditionTrue {
200 return true
201 }
202 }
203 return false
204 }
205
206 func getLogs(ctx context.Context, k *ktest.K8s, podName string) (string, error) {
207 logStream, err := k.GetContainerLogs(ctx, podName, slowcooker, slowcooker)
208 if err != nil {
209 return "", err
210 }
211
212 logsData, err := io.ReadAll(logStream)
213 if err != nil {
214 return "", err
215 }
216 return string(logsData), nil
217 }
218
219 func getHighestLatency(logs string) (int, error) {
220 result := &Result{}
221 err := json.Unmarshal([]byte(fmt.Sprintf("{%s", strings.SplitN(logs, "{", 2)[1])), result)
222 if err != nil {
223 return 0, err
224 }
225 return result.P99, nil
226 }
227
228 func calculateSuccessfulRequests(logs string) (float32, error) {
229 totalGood := 0
230 totalBad := 0
231 totalFailed := 0
232
233 lines := strings.Split(logs, "\n")
234 for _, line := range lines {
235 re := regexp.MustCompile("[0-9]+/[0-9]+/[0-9]+")
236 requests := string(re.Find([]byte(line)))
237 if requests != "" {
238 good, err := strconv.Atoi(strings.Split(requests, "/")[0])
239 if err != nil {
240 return 0, err
241 }
242 totalGood += good
243
244 bad, err := strconv.Atoi(strings.Split(requests, "/")[1])
245 if err != nil {
246 return 0, err
247 }
248 totalBad += bad
249
250 failed, err := strconv.Atoi(strings.Split(requests, "/")[2])
251 if err != nil {
252 return 0, err
253 }
254 totalFailed += failed
255 }
256 }
257 totalRequests := totalGood + totalBad + totalFailed
258 successfulRequests := float32(totalGood) / float32(totalRequests) * 100
259 return successfulRequests, nil
260 }
261
262 func calculateAverageThroughput(logs string, iterations int) (float32, error) {
263 totalThroughput := 0
264
265 lines := strings.Split(logs, "\n")
266 for _, line := range lines {
267 re := regexp.MustCompile("[0-9]+%")
268 throughput := strings.TrimSuffix(string(re.Find([]byte(line))), "%")
269 if throughput != "" {
270 throughput, err := strconv.Atoi(throughput)
271 if err != nil {
272 return 0, err
273 }
274 totalThroughput += throughput
275 }
276 }
277 return float32(totalThroughput) / float32(iterations), nil
278 }
279
View as plain text