300 lines
6.5 KiB
Go
300 lines
6.5 KiB
Go
/*
|
|
Copyright 2025 The Crossplane Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package gate_test
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
"github.com/crossplane/crossplane-runtime/pkg/gate"
|
|
)
|
|
|
|
func TestGateRegister(t *testing.T) {
|
|
type args struct {
|
|
depends []string
|
|
}
|
|
|
|
type want struct {
|
|
called bool
|
|
}
|
|
|
|
cases := map[string]struct {
|
|
reason string
|
|
args args
|
|
want want
|
|
}{
|
|
"NoDependencies": {
|
|
reason: "Should immediately call function when no dependencies are required",
|
|
args: args{
|
|
depends: []string{},
|
|
},
|
|
want: want{
|
|
called: true,
|
|
},
|
|
},
|
|
"SingleDependency": {
|
|
reason: "Should not call function when dependency is not met",
|
|
args: args{
|
|
depends: []string{"condition1"},
|
|
},
|
|
want: want{
|
|
called: false,
|
|
},
|
|
},
|
|
"MultipleDependencies": {
|
|
reason: "Should not call function when multiple dependencies are not met",
|
|
args: args{
|
|
depends: []string{"condition1", "condition2"},
|
|
},
|
|
want: want{
|
|
called: false,
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
g := new(gate.Gate[string])
|
|
|
|
called := false
|
|
|
|
g.Register(func() {
|
|
called = true
|
|
}, tc.args.depends...)
|
|
|
|
// Give some time for goroutine to execute
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
if diff := cmp.Diff(tc.want.called, called); diff != "" {
|
|
t.Errorf("\n%s\nRegister(...): -want called, +got called:\n%s", tc.reason, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGateIntegration(t *testing.T) {
|
|
type want struct {
|
|
called bool
|
|
}
|
|
|
|
cases := map[string]struct {
|
|
reason string
|
|
setup func(g *gate.Gate[string]) chan bool
|
|
want want
|
|
}{
|
|
"SingleDependencyMet": {
|
|
reason: "Should call function when single dependency is met",
|
|
setup: func(g *gate.Gate[string]) chan bool {
|
|
called := make(chan bool, 1)
|
|
g.Register(func() {
|
|
called <- true
|
|
}, "condition1")
|
|
|
|
// Set condition to true (will be initialized as false first)
|
|
g.Set("condition1", true)
|
|
return called
|
|
},
|
|
want: want{
|
|
called: true,
|
|
},
|
|
},
|
|
"MultipleDependenciesMet": {
|
|
reason: "Should call function when all dependencies are met",
|
|
setup: func(g *gate.Gate[string]) chan bool {
|
|
called := make(chan bool, 1)
|
|
g.Register(func() {
|
|
called <- true
|
|
}, "condition1", "condition2")
|
|
|
|
// Set both conditions to true
|
|
g.Set("condition1", true)
|
|
g.Set("condition2", true)
|
|
return called
|
|
},
|
|
want: want{
|
|
called: true,
|
|
},
|
|
},
|
|
"PartialDependenciesMet": {
|
|
reason: "Should not call function when only some dependencies are met",
|
|
setup: func(g *gate.Gate[string]) chan bool {
|
|
called := make(chan bool, 1)
|
|
g.Register(func() {
|
|
called <- true
|
|
}, "condition1", "condition2")
|
|
|
|
// Set only one condition to true
|
|
g.Set("condition1", true)
|
|
return called
|
|
},
|
|
want: want{
|
|
called: false,
|
|
},
|
|
},
|
|
"DependenciesAlreadyMet": {
|
|
reason: "Should call function when dependencies are already met",
|
|
setup: func(g *gate.Gate[string]) chan bool {
|
|
called := make(chan bool, 1)
|
|
|
|
g.Set("condition1", true)
|
|
g.Set("condition2", true)
|
|
|
|
g.Register(func() {
|
|
called <- true
|
|
}, "condition1", "condition2")
|
|
|
|
return called
|
|
},
|
|
want: want{
|
|
called: true,
|
|
},
|
|
},
|
|
"DependencySetThenUnset": {
|
|
reason: "Should call function when dependency is met, even if unset later",
|
|
setup: func(g *gate.Gate[string]) chan bool {
|
|
called := make(chan bool, 1)
|
|
g.Register(func() {
|
|
called <- true
|
|
}, "condition1")
|
|
|
|
// Set condition to true then false (function already called when true)
|
|
g.Set("condition1", true)
|
|
g.Set("condition1", false)
|
|
return called
|
|
},
|
|
want: want{
|
|
called: true,
|
|
},
|
|
},
|
|
"FunctionCalledOnlyOnce": {
|
|
reason: "Should call function only once even if conditions change after",
|
|
setup: func(g *gate.Gate[string]) chan bool {
|
|
called := make(chan bool, 2) // Buffer for potential multiple calls
|
|
g.Register(func() {
|
|
called <- true
|
|
}, "condition1")
|
|
|
|
// Set condition multiple times
|
|
g.Set("condition1", true)
|
|
g.Set("condition1", false)
|
|
g.Set("condition1", true)
|
|
return called
|
|
},
|
|
want: want{
|
|
called: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
g := new(gate.Gate[string])
|
|
|
|
callChannel := tc.setup(g)
|
|
|
|
var got bool
|
|
select {
|
|
case got = <-callChannel:
|
|
case <-time.After(100 * time.Millisecond):
|
|
got = false
|
|
}
|
|
|
|
if diff := cmp.Diff(tc.want.called, got); diff != "" {
|
|
t.Errorf("\n%s\nIntegration test: -want called, +got called:\n%s", tc.reason, diff)
|
|
}
|
|
|
|
// For the "only once" test, ensure no additional calls
|
|
if name == "FunctionCalledOnlyOnce" && tc.want.called {
|
|
select {
|
|
case <-callChannel:
|
|
t.Errorf("\n%s\nFunction was called more than once", tc.reason)
|
|
case <-time.After(50 * time.Millisecond):
|
|
// Good - no additional calls
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGateConcurrency(t *testing.T) {
|
|
g := new(gate.Gate[string])
|
|
|
|
const numGoroutines = 100
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
callCount := make(chan struct{}, numGoroutines)
|
|
|
|
// Register functions concurrently
|
|
for range numGoroutines {
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
g.Register(func() {
|
|
callCount <- struct{}{}
|
|
}, "shared-condition")
|
|
}()
|
|
}
|
|
|
|
// Wait for all registrations
|
|
wg.Wait()
|
|
|
|
// Set condition to true once
|
|
g.Set("shared-condition", true)
|
|
|
|
// Give some time for goroutines to execute
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Count how many functions were called
|
|
close(callCount)
|
|
|
|
count := 0
|
|
for range callCount {
|
|
count++
|
|
}
|
|
|
|
if count != numGoroutines {
|
|
t.Errorf("Expected %d function calls, got %d", numGoroutines, count)
|
|
}
|
|
}
|
|
|
|
func TestGateTypeSafety(t *testing.T) {
|
|
intGate := new(gate.Gate[int])
|
|
|
|
called := false
|
|
|
|
intGate.Register(func() {
|
|
called = true
|
|
}, 1, 2, 3)
|
|
|
|
intGate.Set(1, true)
|
|
intGate.Set(2, true)
|
|
intGate.Set(3, true)
|
|
|
|
// Give some time for goroutine to execute
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
if !called {
|
|
t.Error("Function should have been called when all int conditions were met")
|
|
}
|
|
}
|