Skip to content

Commit a5531d1

Browse files
authored
[Framework] Add flow tests (#73)
This PR adds unit tests for the flow described in the related Jira ticket: https://iguazio.atlassian.net/browse/NUC-455 . The tests ensure coverage of the newly implemented logic and validate its behavior under expected conditions. In addition, this PR includes test coverage for the changes introduced in [#72](#72), providing verification for that logic as well. The unit tests failure are fixed in a separated PR - #72
1 parent d97ecfc commit a5531d1

File tree

1 file changed

+201
-0
lines changed

1 file changed

+201
-0
lines changed

pkg/ingresscache/ingresscache_test.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ such restriction.
2121
package ingresscache
2222

2323
import (
24+
"fmt"
25+
"slices"
26+
"sync"
2427
"testing"
2528

2629
"github.com/nuclio/logger"
@@ -253,6 +256,166 @@ func (suite *IngressCacheTestSuite) TestDelete() {
253256
}
254257
}
255258

259+
// --- IngressCacheTestSuite flow tests ---
260+
261+
func (suite *IngressCacheTestSuite) TestAllThreeMainFunctionalitiesWithTheSameHostAndPath() {
262+
// This test verifies the flow of setting a function name in an empty IngressCache, then getting it, and finally deleting it.
263+
// It ensures that the IngressCache behaves correctly when performing these operations sequentially.
264+
265+
testIngressCache := suite.getTestIngressCache([]ingressCacheTestInitialState{})
266+
var err error
267+
var getResult []string
268+
269+
// get when cache is empty
270+
getResult, err = testIngressCache.Get("example.com", "/test/path")
271+
suite.Require().Error(err)
272+
suite.Require().ErrorContains(err, "cache get failed: host does not exist")
273+
suite.Require().Nil(getResult, "Expected no function names for empty cache")
274+
275+
// Set a function name in an empty cache
276+
err = testIngressCache.Set("example.com", "/test/path", "test-function-name-1")
277+
suite.Require().NoError(err, "Expected no error when setting a function name in an empty cache")
278+
getResult, err = testIngressCache.Get("example.com", "/test/path")
279+
suite.Require().NoError(err, "Expected no error when getting function names after setting")
280+
suite.Require().Equal([]string{"test-function-name-1"}, getResult, "Expected to get the function name that was just set")
281+
flattenTestResult := suite.flattenIngressCache(testIngressCache)
282+
suite.Require().Equal(flattenTestResult, map[string]map[string]FunctionTarget{
283+
"example.com": {"/test/path": SingleTarget("test-function-name-1")},
284+
})
285+
286+
// Set another function name for the same host and path
287+
err = testIngressCache.Set("example.com", "/test/path", "test-function-name-2")
288+
suite.Require().NoError(err, "Expected no error when setting another function name for the same host and path")
289+
getResult, err = testIngressCache.Get("example.com", "/test/path")
290+
suite.Require().NoError(err, "Expected no error when getting function names after setting another function name")
291+
suite.Require().Equal([]string{"test-function-name-1", "test-function-name-2"}, getResult, "Expected to get the new function name that was just set")
292+
flattenTestResult = suite.flattenIngressCache(testIngressCache)
293+
suite.Require().Equal(flattenTestResult, map[string]map[string]FunctionTarget{
294+
"example.com": {"/test/path": &CanaryTarget{[2]string{"test-function-name-1", "test-function-name-2"}}},
295+
})
296+
297+
// Delete the first function name
298+
err = testIngressCache.Delete("example.com", "/test/path", "test-function-name-1")
299+
suite.Require().NoError(err, "Expected no error when deleting the first function name")
300+
getResult, err = testIngressCache.Get("example.com", "/test/path")
301+
suite.Require().NoError(err, "Expected no error when getting function names after deleting the first function name")
302+
suite.Require().Equal(getResult, []string{"test-function-name-2"}, "Expected to get the remaining function name after deletion")
303+
flattenTestResult = suite.flattenIngressCache(testIngressCache)
304+
suite.Require().Equal(flattenTestResult, map[string]map[string]FunctionTarget{
305+
"example.com": {"/test/path": SingleTarget("test-function-name-2")},
306+
})
307+
308+
// Delete the second function name and validate that the cache is empty
309+
err = testIngressCache.Delete("example.com", "/test/path", "test-function-name-2")
310+
suite.Require().NoError(err, "Expected no error when deleting the second function name")
311+
getResult, err = testIngressCache.Get("example.com", "/test/path")
312+
suite.Require().Error(err)
313+
suite.Require().ErrorContains(err, "cache get failed: host does not exist")
314+
suite.Require().Nil(getResult, "Expected no function names for empty cache")
315+
flattenTestResult = suite.flattenIngressCache(testIngressCache)
316+
suite.Require().Equal(flattenTestResult, map[string]map[string]FunctionTarget{})
317+
}
318+
319+
func (suite *IngressCacheTestSuite) TestParallelSetForTheSameHostAndDifferentPath() {
320+
// This test simulates a scenario where multiple goroutines try to set the same host and different paths in the IngressCache concurrently.
321+
// The expected behavior is that the IngressCache should handle concurrent writes without any errors and end up with a canaryTarget for each path.
322+
323+
testIngressCache := suite.getTestIngressCache([]ingressCacheTestInitialState{})
324+
wg := sync.WaitGroup{}
325+
for i := range 20 {
326+
wg.Add(2)
327+
path := fmt.Sprintf("/test/path/%d", i)
328+
329+
// first goroutine set test-function-name-1
330+
go func(ingressCache *IngressCache, wg *sync.WaitGroup, path string) {
331+
defer wg.Done()
332+
err := ingressCache.Set("example.com", path, "test-function-name-1")
333+
suite.Require().NoError(err, "Expected no error when setting a function name in an empty cache")
334+
}(testIngressCache, &wg, path)
335+
336+
// second goroutine set test-function-name-2
337+
go func(ingressCache *IngressCache, wg *sync.WaitGroup, path string) {
338+
defer wg.Done()
339+
err := ingressCache.Set("example.com", path, "test-function-name-2")
340+
suite.Require().NoError(err, "Expected no error when setting a function name in an empty cache")
341+
}(testIngressCache, &wg, path)
342+
}
343+
wg.Wait()
344+
345+
// After all goroutines finished, check that the expected result matches the IngressCache state
346+
flattenTestResult := suite.flattenIngressCache(testIngressCache)
347+
expectedResult := suite.generateExpectedResult(20, false)
348+
suite.compareIngressHostCache(expectedResult, flattenTestResult)
349+
}
350+
351+
func (suite *IngressCacheTestSuite) TestParallelSetForDifferentHosts() {
352+
// This test simulates a scenario where multiple goroutines try to set different hosts and paths in the IngressCache concurrently.
353+
// The expected behavior is that the IngressCache should handle concurrent writes without any errors and end up with a canaryTarget for each host and path.
354+
355+
testIngressCache := suite.getTestIngressCache([]ingressCacheTestInitialState{})
356+
wg := sync.WaitGroup{}
357+
for i := range 200 {
358+
wg.Add(2)
359+
host := fmt.Sprintf("example-%d.com", i)
360+
path := fmt.Sprintf("/test/path/%d", i)
361+
362+
// first goroutine set test-function-name-1
363+
go func(ingressCache *IngressCache, wg *sync.WaitGroup, host, path string) {
364+
defer wg.Done()
365+
err := ingressCache.Set(host, path, "test-function-name-1")
366+
suite.Require().NoError(err, "Expected no error when setting a function name in an empty cache")
367+
}(testIngressCache, &wg, host, path)
368+
369+
// second goroutine set test-function-name-2
370+
go func(ingressCache *IngressCache, wg *sync.WaitGroup, host, path string) {
371+
defer wg.Done()
372+
err := ingressCache.Set(host, path, "test-function-name-2")
373+
suite.Require().NoError(err, "Expected no error when setting a function name in an empty cache")
374+
}(testIngressCache, &wg, host, path)
375+
}
376+
wg.Wait()
377+
378+
// After all goroutines finished, check that the expected result matches the IngressCache state
379+
flattenTestResult := suite.flattenIngressCache(testIngressCache)
380+
expectedResult := suite.generateExpectedResult(200, true)
381+
suite.compareIngressHostCache(expectedResult, flattenTestResult)
382+
}
383+
384+
func (suite *IngressCacheTestSuite) TestParallelSetForSameHostAndSamePath() {
385+
// This test simulates a scenario where multiple goroutines try to set the same host and path in the IngressCache concurrently.
386+
// The expected behavior is that the IngressCache should handle concurrent writes without any errors and end up with a single entry for the host and path
387+
388+
testIngressCache := suite.getTestIngressCache([]ingressCacheTestInitialState{})
389+
wg := sync.WaitGroup{}
390+
for range 20 {
391+
wg.Add(2)
392+
393+
// first goroutine set test-function-name-1
394+
go func(ingressCache *IngressCache, wg *sync.WaitGroup) {
395+
defer wg.Done()
396+
err := ingressCache.Set("example.com", "/test/path", "test-function-name-1")
397+
suite.Require().NoError(err, "Expected no error when setting a function name in an empty cache")
398+
}(testIngressCache, &wg)
399+
400+
// second goroutine set test-function-name-2
401+
go func(ingressCache *IngressCache, wg *sync.WaitGroup) {
402+
defer wg.Done()
403+
err := ingressCache.Set("example.com", "/test/path", "test-function-name-2")
404+
suite.Require().NoError(err, "Expected no error when setting a function name in an empty cache")
405+
}(testIngressCache, &wg)
406+
}
407+
wg.Wait()
408+
409+
// After all goroutines finished, check that the expected result matches the IngressCache state
410+
flattenTestResult := suite.flattenIngressCache(testIngressCache)
411+
expectedResult := map[string]map[string]FunctionTarget{
412+
"example.com": {
413+
"/test/path": &CanaryTarget{[2]string{"test-function-name-1", "test-function-name-2"}},
414+
},
415+
}
416+
suite.compareIngressHostCache(expectedResult, flattenTestResult)
417+
}
418+
256419
// --- IngressCacheTestSuite suite methods ---
257420

258421
// getTestIngressCache creates a IngressCache instance and sets the provided initial state
@@ -284,6 +447,44 @@ func (suite *IngressCacheTestSuite) flattenIngressCache(testIngressCache *Ingres
284447
return output
285448
}
286449

450+
func (suite *IngressCacheTestSuite) generateExpectedResult(num int, differentHosts bool) map[string]map[string]FunctionTarget {
451+
output := make(map[string]map[string]FunctionTarget)
452+
for i := range num {
453+
path := fmt.Sprintf("/test/path/%d", i)
454+
host := "example.com"
455+
if differentHosts {
456+
host = fmt.Sprintf("example-%d.com", i)
457+
}
458+
459+
if output[host] == nil {
460+
output[host] = map[string]FunctionTarget{}
461+
}
462+
463+
output[host][path] = &CanaryTarget{functionNames: [2]string{"test-function-name-1", "test-function-name-2"}}
464+
}
465+
466+
return output
467+
}
468+
469+
// compareIngressHostCache compares the expected result with the test result
470+
func (suite *IngressCacheTestSuite) compareIngressHostCache(expectedResult, testResult map[string]map[string]FunctionTarget) {
471+
suite.Require().Equal(len(expectedResult), len(testResult))
472+
473+
// Because the values in the map are pointers, we need to compare the values
474+
for host, paths := range testResult {
475+
suite.Require().Contains(expectedResult, host, "Expected host %s to be in the result", host)
476+
for path, functionNames := range paths {
477+
suite.Require().Contains(expectedResult[host], path, "Expected path %s to be in the result for host %s", path, host)
478+
expectedFunctionNames := expectedResult[host][path].ToSliceString()
479+
slices.Sort(expectedFunctionNames)
480+
sortedFunctionNames := functionNames.ToSliceString()
481+
slices.Sort(sortedFunctionNames)
482+
suite.Require().Equal(expectedFunctionNames, sortedFunctionNames,
483+
"Expected function names for host %s and path %s to match", host, path)
484+
}
485+
}
486+
}
487+
287488
func TestIngressCache(t *testing.T) {
288489
suite.Run(t, new(IngressCacheTestSuite))
289490
}

0 commit comments

Comments
 (0)