fix: finish progressive Target detection
This commit is contained in:
@@ -3,8 +3,10 @@ package app_test
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"proxyctl/internal/app"
|
||||
@@ -67,7 +69,7 @@ func TestApplicationStreamsIndependentTargetDetectionInProductOrder(t *testing.T
|
||||
|
||||
close(secondRelease)
|
||||
second := <-updates
|
||||
if second.Result.TargetID != "second" || second.Result.Phase != app.TargetReady || second.Result.Inspection.State != target.External {
|
||||
if second.TargetID != "second" || second.Snapshot.Targets[1].DetectionPhase != app.TargetReady || second.Snapshot.Targets[1].State != target.External {
|
||||
t.Fatalf("second completion = %#v", second)
|
||||
}
|
||||
assertOnlyDetectionChanged(t, checking, second.Snapshot, "second")
|
||||
@@ -75,14 +77,14 @@ func TestApplicationStreamsIndependentTargetDetectionInProductOrder(t *testing.T
|
||||
|
||||
close(failedRelease)
|
||||
failed := <-updates
|
||||
if failed.Result.TargetID != "failed" || failed.Result.Phase != app.TargetDetectionFailed || failed.Result.Err == nil {
|
||||
if failed.TargetID != "failed" || failed.Snapshot.Targets[2].DetectionPhase != app.TargetDetectionFailed || failed.Snapshot.Targets[2].DetectionErr == nil {
|
||||
t.Fatalf("failed completion = %#v", failed)
|
||||
}
|
||||
assertOnlyDetectionChanged(t, second.Snapshot, failed.Snapshot, "failed")
|
||||
|
||||
close(firstRelease)
|
||||
first := <-updates
|
||||
if first.Result.TargetID != "first" || first.Result.Phase != app.TargetReady || first.Result.Inspection.State != target.Enabled {
|
||||
if first.TargetID != "first" || first.Snapshot.Targets[0].DetectionPhase != app.TargetReady || first.Snapshot.Targets[0].State != target.Enabled {
|
||||
t.Fatalf("first completion = %#v", first)
|
||||
}
|
||||
assertOnlyDetectionChanged(t, failed.Snapshot, first.Snapshot, "first")
|
||||
@@ -97,6 +99,168 @@ func TestApplicationStreamsIndependentTargetDetectionInProductOrder(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
type detectionRequest struct {
|
||||
result chan target.Detection
|
||||
}
|
||||
|
||||
type overlappingDetectionTarget struct {
|
||||
requests chan detectionRequest
|
||||
}
|
||||
|
||||
func (t *overlappingDetectionTarget) ID() string { return "overlap" }
|
||||
func (t *overlappingDetectionTarget) Description() target.Description {
|
||||
return target.Description{Name: "Overlap", Summary: "controlled overlapping detection"}
|
||||
}
|
||||
func (t *overlappingDetectionTarget) Availability(context.Context) target.Availability {
|
||||
return target.Availability{Available: true}
|
||||
}
|
||||
func (t *overlappingDetectionTarget) Inspect(context.Context, *profile.Profile) target.Inspection {
|
||||
return target.Inspection{State: target.Disabled}
|
||||
}
|
||||
func (t *overlappingDetectionTarget) Confirmation(bool) string { return "" }
|
||||
func (t *overlappingDetectionTarget) SetDesired(context.Context, bool, *profile.Profile) target.OperationResult {
|
||||
return target.OperationResult{}
|
||||
}
|
||||
func (t *overlappingDetectionTarget) Detect(context.Context, *profile.Profile) target.Detection {
|
||||
request := detectionRequest{result: make(chan target.Detection, 1)}
|
||||
t.requests <- request
|
||||
return <-request.result
|
||||
}
|
||||
|
||||
func TestOverlappingTargetDetectionDiscardsLateOlderGeneration(t *testing.T) {
|
||||
adapter := &overlappingDetectionTarget{requests: make(chan detectionRequest, 2)}
|
||||
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, olderUpdates := application.BeginTargetDetection()
|
||||
older := <-adapter.requests
|
||||
_, newerUpdates := application.BeginTargetDetection()
|
||||
newer := <-adapter.requests
|
||||
|
||||
newer.result <- target.Detection{
|
||||
Availability: target.Availability{Available: true, Evidence: "new availability"},
|
||||
Inspection: target.Inspection{State: target.Enabled, Evidence: "new inspection"},
|
||||
}
|
||||
for range newerUpdates {
|
||||
}
|
||||
older.result <- target.Detection{
|
||||
Availability: target.Availability{Evidence: "old availability"},
|
||||
Inspection: target.Inspection{State: target.Disabled, Evidence: "old inspection"},
|
||||
}
|
||||
staleUpdates := 0
|
||||
for range olderUpdates {
|
||||
staleUpdates++
|
||||
}
|
||||
if staleUpdates != 0 {
|
||||
t.Fatalf("older generation published %d stale authoritative update(s)", staleUpdates)
|
||||
}
|
||||
|
||||
got := application.Snapshot().Targets[0]
|
||||
if !got.Available || got.State != target.Enabled || got.Evidence != "new inspection" {
|
||||
t.Fatalf("late older generation overwrote newer result: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
type mutationInteractionTarget struct {
|
||||
mu sync.Mutex
|
||||
enabled bool
|
||||
release chan struct{}
|
||||
}
|
||||
|
||||
func (t *mutationInteractionTarget) ID() string { return "mutation" }
|
||||
func (t *mutationInteractionTarget) Description() target.Description {
|
||||
return target.Description{Name: "Mutation", Summary: "detection and mutation interaction"}
|
||||
}
|
||||
func (t *mutationInteractionTarget) Availability(context.Context) target.Availability {
|
||||
return target.Availability{Available: true}
|
||||
}
|
||||
func (t *mutationInteractionTarget) Inspect(context.Context, *profile.Profile) target.Inspection {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
state := target.Disabled
|
||||
if t.enabled {
|
||||
state = target.Enabled
|
||||
}
|
||||
return target.Inspection{State: state, Evidence: "current mutation state"}
|
||||
}
|
||||
func (t *mutationInteractionTarget) Confirmation(bool) string { return "" }
|
||||
func (t *mutationInteractionTarget) SetDesired(context.Context, bool, *profile.Profile) target.OperationResult {
|
||||
t.mu.Lock()
|
||||
t.enabled = true
|
||||
t.mu.Unlock()
|
||||
return target.OperationResult{Inspection: target.Inspection{State: target.Enabled, Evidence: "mutation completed"}}
|
||||
}
|
||||
func (t *mutationInteractionTarget) Detect(context.Context, *profile.Profile) target.Detection {
|
||||
<-t.release
|
||||
return target.Detection{
|
||||
Availability: target.Availability{Available: true, Evidence: "stale detection"},
|
||||
Inspection: target.Inspection{State: target.Disabled, Evidence: "stale detection"},
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchMutationInvalidatesInFlightTargetDetection(t *testing.T) {
|
||||
adapter := &mutationInteractionTarget{release: make(chan struct{})}
|
||||
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
created := application.Dispatch(app.Intent{Kind: app.CreateProfile, Profile: profile.Profile{Name: "office", SingleURL: "http://proxy.example:8080"}})
|
||||
if created.Err != nil {
|
||||
t.Fatal(created.Err)
|
||||
}
|
||||
if activated := application.Dispatch(app.Intent{Kind: app.ActivateProfile, Name: "office"}); activated.Err != nil {
|
||||
t.Fatal(activated.Err)
|
||||
}
|
||||
|
||||
_, updates := application.BeginTargetDetection()
|
||||
result := application.Dispatch(app.Intent{Kind: app.SetTargetDesired, TargetID: adapter.ID(), Enabled: true})
|
||||
if result.Err != nil || result.Snapshot.Targets[0].State != target.Enabled {
|
||||
t.Fatalf("mutation result = %#v", result)
|
||||
}
|
||||
close(adapter.release)
|
||||
for range updates {
|
||||
}
|
||||
if got := application.Snapshot().Targets[0]; got.State != target.Enabled || got.Evidence != "mutation completed" {
|
||||
t.Fatalf("in-flight detection overwrote mutation: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnapshotIsSafeDuringConcurrentStateMutation(t *testing.T) {
|
||||
application, err := app.New(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
const count = 40
|
||||
start := make(chan struct{})
|
||||
var readers sync.WaitGroup
|
||||
for range 4 {
|
||||
readers.Add(1)
|
||||
go func() {
|
||||
defer readers.Done()
|
||||
<-start
|
||||
for range count * 4 {
|
||||
_ = application.Snapshot()
|
||||
}
|
||||
}()
|
||||
}
|
||||
close(start)
|
||||
for i := range count {
|
||||
result := application.Dispatch(app.Intent{Kind: app.CreateProfile, Profile: profile.Profile{
|
||||
Name: fmt.Sprintf("profile-%02d", i), SingleURL: "http://proxy.example:8080",
|
||||
}})
|
||||
if result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
}
|
||||
readers.Wait()
|
||||
if got := len(application.Snapshot().Profiles); got != count {
|
||||
t.Fatalf("Profile count = %d, want %d", got, count)
|
||||
}
|
||||
}
|
||||
|
||||
func assertTargetOrder(t *testing.T, snapshot app.Snapshot, want []string) {
|
||||
t.Helper()
|
||||
got := make([]string, len(snapshot.Targets))
|
||||
|
||||
Reference in New Issue
Block a user