master
go 517 lines 12.4 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package functions
4
5 import (
6 "bufio"
7 "context"
8 "fmt"
9 "sort"
10 "strings"
11 "sync"
12 "testing"
13 "time"
14
15 "github.com/stretchr/testify/assert"
16 )
17
18 const (
19 managerTestPermissions = "0xFFFF"
20 managerTestSource = "method=api,role=test"
21 )
22
23 func TestNewManager(t *testing.T) {
24 mgr := NewManager()
25
26 assert.NotNilf(t, mgr.input, "Input")
27 assert.NotNilf(t, mgr.functionRegistry, "FunctionRegistry")
28 }
29
30 func TestManager_Register(t *testing.T) {
31 type testInputFn struct {
32 name string
33 invalid bool
34 }
35 tests := map[string]struct {
36 input []testInputFn
37 expected []string
38 }{
39 "valid registration": {
40 input: []testInputFn{
41 {name: "fn1"},
42 {name: "fn2"},
43 },
44 expected: []string{"fn1", "fn2"},
45 },
46 "registration with duplicates": {
47 input: []testInputFn{
48 {name: "fn1"},
49 {name: "fn2"},
50 {name: "fn1"},
51 },
52 expected: []string{"fn1", "fn2"},
53 },
54 "registration with nil functions": {
55 input: []testInputFn{
56 {name: "fn1"},
57 {name: "fn2", invalid: true},
58 },
59 expected: []string{"fn1"},
60 },
61 }
62
63 for name, test := range tests {
64 t.Run(name, func(t *testing.T) {
65 mgr := NewManager()
66
67 for _, v := range test.input {
68 if v.invalid {
69 mgr.Register(v.name, nil)
70 } else {
71 mgr.Register(v.name, func(Function) {})
72 }
73 }
74
75 var got []string
76 for name := range mgr.functionRegistry {
77 got = append(got, name)
78 }
79 sort.Strings(got)
80 sort.Strings(test.expected)
81
82 assert.Equal(t, test.expected, got)
83 })
84 }
85 }
86
87 func TestManager_RegisterPrefix(t *testing.T) {
88 type inputFn struct {
89 name string
90 prefix string
91 invalid bool
92 }
93
94 tests := map[string]struct {
95 input []inputFn
96 expected []string // flattened as "name:prefix"
97 }{
98 "valid registration (two prefixes under same name)": {
99 input: []inputFn{
100 {name: "config", prefix: "collector:"},
101 {name: "config", prefix: "vnode:"},
102 },
103 expected: []string{"config:collector:", "config:vnode:"},
104 },
105 "registration with duplicates (same name+prefix)": {
106 input: []inputFn{
107 {name: "config", prefix: "collector:"},
108 {name: "config", prefix: "collector:"}, // duplicate should overwrite, not duplicate
109 {name: "config", prefix: "vnode:"},
110 },
111 expected: []string{"config:collector:", "config:vnode:"},
112 },
113 "registration across multiple names": {
114 input: []inputFn{
115 {name: "config", prefix: "collector:"},
116 {name: "status", prefix: "node:"},
117 },
118 expected: []string{"config:collector:", "status:node:"},
119 },
120 "registration with nil functions is ignored": {
121 input: []inputFn{
122 {name: "config", prefix: "collector:"},
123 {name: "config", prefix: "vnode:", invalid: true}, // nil fn -> ignored
124 },
125 expected: []string{"config:collector:"},
126 },
127 "overlapping prefix is rejected (short first)": {
128 input: []inputFn{
129 {name: "config", prefix: "collector:"},
130 {name: "config", prefix: "collector:job:"},
131 },
132 expected: []string{"config:collector:"},
133 },
134 "overlapping prefix is rejected (long first)": {
135 input: []inputFn{
136 {name: "config", prefix: "collector:job:"},
137 {name: "config", prefix: "collector:"},
138 },
139 expected: []string{"config:collector:job:"},
140 },
141 }
142
143 for name, test := range tests {
144 t.Run(name, func(t *testing.T) {
145 mgr := NewManager()
146
147 for _, v := range test.input {
148 if v.invalid {
149 mgr.RegisterPrefix(v.name, v.prefix, nil)
150 } else {
151 mgr.RegisterPrefix(v.name, v.prefix, func(Function) {})
152 }
153 }
154
155 var got []string
156 for fname, fs := range mgr.functionRegistry {
157 if fs == nil || len(fs.prefixes) == 0 {
158 continue
159 }
160 for p := range fs.prefixes {
161 got = append(got, fmt.Sprintf("%s:%s", fname, p))
162 }
163 }
164 sort.Strings(got)
165 sort.Strings(test.expected)
166
167 assert.Equal(t, test.expected, got)
168 })
169 }
170 }
171
172 func TestManager_UnregisterPrefix(t *testing.T) {
173 type regFn struct {
174 name string
175 prefix string
176 }
177 type unregFn struct {
178 name string
179 prefix string
180 }
181
182 tests := map[string]struct {
183 register []regFn
184 unreg []unregFn
185 expected []string // flattened as "name:prefix"
186 }{
187 "remove one of multiple prefixes keeps the other": {
188 register: []regFn{
189 {name: "config", prefix: "collector:"},
190 {name: "config", prefix: "vnode:"},
191 },
192 unreg: []unregFn{
193 {name: "config", prefix: "collector:"},
194 },
195 expected: []string{"config:vnode:"},
196 },
197 "remove last prefix deletes the name entry": {
198 register: []regFn{
199 {name: "config", prefix: "collector:"},
200 },
201 unreg: []unregFn{
202 {name: "config", prefix: "collector:"},
203 },
204 expected: nil,
205 },
206 "unregister non-existing prefix is a no-op": {
207 register: []regFn{
208 {name: "config", prefix: "collector:"},
209 },
210 unreg: []unregFn{
211 {name: "config", prefix: "vnode:"}, // doesn't exist
212 },
213 expected: []string{"config:collector:"},
214 },
215 "unregister on unknown name is a no-op": {
216 register: []regFn{
217 {name: "config", prefix: "collector:"},
218 },
219 unreg: []unregFn{
220 {name: "status", prefix: "node:"}, // name not present
221 },
222 expected: []string{"config:collector:"},
223 },
224 }
225
226 for name, test := range tests {
227 t.Run(name, func(t *testing.T) {
228 mgr := NewManager()
229
230 // initial registrations
231 for _, r := range test.register {
232 mgr.RegisterPrefix(r.name, r.prefix, func(Function) {})
233 }
234
235 // perform unregistrations
236 for _, u := range test.unreg {
237 mgr.UnregisterPrefix(u.name, u.prefix)
238 }
239
240 var got []string
241 for fname, fs := range mgr.functionRegistry {
242 if fs == nil || len(fs.prefixes) == 0 {
243 continue
244 }
245 for p := range fs.prefixes {
246 got = append(got, fmt.Sprintf("%s:%s", fname, p))
247 }
248 }
249 sort.Strings(got)
250 sort.Strings(test.expected)
251
252 assert.Equal(t, test.expected, got)
253 })
254 }
255 }
256
257 func TestManager_Run(t *testing.T) {
258 tests := map[string]struct {
259 register []string
260 input string
261 expected []Function
262 }{
263 "valid function: single": {
264 register: []string{"fn1"},
265 input: fmt.Sprintf(`
266 FUNCTION UID 1 "fn1 arg1 arg2" %s "%s"
267 `, managerTestPermissions, managerTestSource),
268 expected: []Function{
269 {
270 key: lineFunction,
271 UID: "UID",
272 Timeout: time.Second,
273 Name: "fn1",
274 Args: []string{"arg1", "arg2"},
275 Permissions: managerTestPermissions,
276 Source: managerTestSource,
277 ContentType: "",
278 Payload: nil,
279 },
280 },
281 },
282 "valid function: multiple": {
283 register: []string{"fn1", "fn2"},
284 input: fmt.Sprintf(`
285 FUNCTION UID1 1 "fn1 arg1 arg2" %s "%s"
286 FUNCTION UID2 1 "fn2 arg1 arg2" %s "%s"
287 `, managerTestPermissions, managerTestSource, managerTestPermissions, managerTestSource),
288 expected: []Function{
289 {
290 key: lineFunction,
291 UID: "UID1",
292 Timeout: time.Second,
293 Name: "fn1",
294 Args: []string{"arg1", "arg2"},
295 Permissions: managerTestPermissions,
296 Source: managerTestSource,
297 ContentType: "",
298 Payload: nil,
299 },
300 {
301 key: lineFunction,
302 UID: "UID2",
303 Timeout: time.Second,
304 Name: "fn2",
305 Args: []string{"arg1", "arg2"},
306 Permissions: managerTestPermissions,
307 Source: managerTestSource,
308 ContentType: "",
309 Payload: nil,
310 },
311 },
312 },
313 "valid function: single with payload": {
314 register: []string{"fn1", "fn2"},
315 input: fmt.Sprintf(`
316 FUNCTION_PAYLOAD UID 1 "fn1 arg1 arg2" %s "%s" application/json
317 payload line1
318 payload line2
319 FUNCTION_PAYLOAD_END
320 `, managerTestPermissions, managerTestSource),
321 expected: []Function{
322 {
323 key: lineFunctionPayload,
324 UID: "UID",
325 Timeout: time.Second,
326 Name: "fn1",
327 Args: []string{"arg1", "arg2"},
328 Permissions: managerTestPermissions,
329 Source: managerTestSource,
330 ContentType: "application/json",
331 Payload: []byte("payload line1\npayload line2"),
332 },
333 },
334 },
335 "valid function: multiple with payload": {
336 register: []string{"fn1", "fn2"},
337 input: fmt.Sprintf(`
338 FUNCTION_PAYLOAD UID1 1 "fn1 arg1 arg2" %s "%s" application/json
339 payload line1
340 payload line2
341 FUNCTION_PAYLOAD_END
342
343 FUNCTION_PAYLOAD UID2 1 "fn2 arg1 arg2" %s "%s" application/json
344 payload line3
345 payload line4
346 FUNCTION_PAYLOAD_END
347 `, managerTestPermissions, managerTestSource, managerTestPermissions, managerTestSource),
348 expected: []Function{
349 {
350 key: lineFunctionPayload,
351 UID: "UID1",
352 Timeout: time.Second,
353 Name: "fn1",
354 Args: []string{"arg1", "arg2"},
355 Permissions: managerTestPermissions,
356 Source: managerTestSource,
357 ContentType: "application/json",
358 Payload: []byte("payload line1\npayload line2"),
359 },
360 {
361 key: lineFunctionPayload,
362 UID: "UID2",
363 Timeout: time.Second,
364 Name: "fn2",
365 Args: []string{"arg1", "arg2"},
366 Permissions: managerTestPermissions,
367 Source: managerTestSource,
368 ContentType: "application/json",
369 Payload: []byte("payload line3\npayload line4"),
370 },
371 },
372 },
373 "valid function: multiple with and without payload": {
374 register: []string{"fn1", "fn2", "fn3", "fn4"},
375 input: fmt.Sprintf(`
376 FUNCTION_PAYLOAD UID1 1 "fn1 arg1 arg2" %s "%s" application/json
377 payload line1
378 payload line2
379 FUNCTION_PAYLOAD_END
380
381 FUNCTION UID2 1 "fn2 arg1 arg2" %s "%s"
382 FUNCTION UID3 1 "fn3 arg1 arg2" %s "%s"
383
384 FUNCTION_PAYLOAD UID4 1 "fn4 arg1 arg2" %s "%s" application/json
385 payload line3
386 payload line4
387 FUNCTION_PAYLOAD_END
388 `, managerTestPermissions, managerTestSource,
389 managerTestPermissions, managerTestSource,
390 managerTestPermissions, managerTestSource,
391 managerTestPermissions, managerTestSource),
392 expected: []Function{
393 {
394 key: lineFunctionPayload,
395 UID: "UID1",
396 Timeout: time.Second,
397 Name: "fn1",
398 Args: []string{"arg1", "arg2"},
399 Permissions: managerTestPermissions,
400 Source: managerTestSource,
401 ContentType: "application/json",
402 Payload: []byte("payload line1\npayload line2"),
403 },
404 {
405 key: lineFunction,
406 UID: "UID2",
407 Timeout: time.Second,
408 Name: "fn2",
409 Args: []string{"arg1", "arg2"},
410 Permissions: managerTestPermissions,
411 Source: managerTestSource,
412 ContentType: "",
413 Payload: nil,
414 },
415 {
416 key: lineFunction,
417 UID: "UID3",
418 Timeout: time.Second,
419 Name: "fn3",
420 Args: []string{"arg1", "arg2"},
421 Permissions: managerTestPermissions,
422 Source: managerTestSource,
423 ContentType: "",
424 Payload: nil,
425 },
426 {
427 key: lineFunctionPayload,
428 UID: "UID4",
429 Timeout: time.Second,
430 Name: "fn4",
431 Args: []string{"arg1", "arg2"},
432 Permissions: managerTestPermissions,
433 Source: managerTestSource,
434 ContentType: "application/json",
435 Payload: []byte("payload line3\npayload line4"),
436 },
437 },
438 },
439 }
440
441 for name, test := range tests {
442 for workerProfile, workerCount := range map[string]int{
443 "single-worker": 1,
444 "multi-worker": 4,
445 } {
446 t.Run(name+"/"+workerProfile, func(t *testing.T) {
447 mgr := NewManager()
448 mgr.workerCount = workerCount
449
450 mgr.input = newMockInput(test.input)
451
452 mock := &mockFunctionExecutor{}
453 for _, v := range test.register {
454 mgr.Register(v, mock.execute)
455 }
456
457 testTime := time.Second * 5
458 ctx, cancel := context.WithTimeout(context.Background(), testTime)
459 defer cancel()
460
461 done := make(chan struct{})
462
463 go func() { defer close(done); mgr.Run(ctx, nil) }()
464
465 timeout := testTime + time.Second*2
466 tk := time.NewTimer(timeout)
467 defer tk.Stop()
468
469 select {
470 case <-done:
471 assert.ElementsMatch(t, test.expected, mock.snapshot())
472 case <-tk.C:
473 t.Errorf("timed out after %s", timeout)
474 }
475 })
476 }
477 }
478 }
479
480 type mockFunctionExecutor struct {
481 mu sync.Mutex
482 executed []Function
483 }
484
485 func (m *mockFunctionExecutor) execute(fn Function) {
486 m.mu.Lock()
487 defer m.mu.Unlock()
488 m.executed = append(m.executed, fn)
489 }
490
491 func (m *mockFunctionExecutor) snapshot() []Function {
492 m.mu.Lock()
493 defer m.mu.Unlock()
494 out := make([]Function, len(m.executed))
495 copy(out, m.executed)
496 return out
497 }
498
499 func newMockInput(data string) *mockInput {
500 m := &mockInput{chLines: make(chan string)}
501 sc := bufio.NewScanner(strings.NewReader(data))
502 go func() {
503 for sc.Scan() {
504 m.chLines <- sc.Text()
505 }
506 close(m.chLines)
507 }()
508 return m
509 }
510
511 type mockInput struct {
512 chLines chan string
513 }
514
515 func (m *mockInput) lines() <-chan string {
516 return m.chLines
517 }