@@ -21,6 +21,9 @@ such restriction.
2121package ingresscache
2222
2323import (
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+
287488func TestIngressCache (t * testing.T ) {
288489 suite .Run (t , new (IngressCacheTestSuite ))
289490}
0 commit comments