@ -18,12 +18,34 @@ import (
"tailscale.com/tstest"
"tailscale.com/tstime"
"tailscale.com/types/opt"
"tailscale.com/util/eventbus"
"tailscale.com/util/eventbus/eventbustest"
"tailscale.com/util/usermetric"
"tailscale.com/version"
)
func wantChange ( c Change ) func ( c Change ) ( bool , error ) {
return func ( cEv Change ) ( bool , error ) {
if cEv . ControlHealthChanged != c . ControlHealthChanged {
return false , fmt . Errorf ( "expected ControlHealthChanged %t, got %t" , c . ControlHealthChanged , cEv . ControlHealthChanged )
}
if cEv . WarnableChanged != c . WarnableChanged {
return false , fmt . Errorf ( "expected WarnableChanged %t, got %t" , c . WarnableChanged , cEv . WarnableChanged )
}
if c . Warnable != nil && ( cEv . Warnable == nil || cEv . Warnable != c . Warnable ) {
return false , fmt . Errorf ( "expected Warnable %+v, got %+v" , c . Warnable , cEv . Warnable )
}
if c . UnhealthyState != nil {
panic ( "comparison of UnhealthyState is not yet supported" )
}
return true , nil
}
}
func TestAppendWarnableDebugFlags ( t * testing . T ) {
var tr Tracker
tr := NewTracker ( eventbustest . NewBus ( t ) )
for i := range 10 {
w := Register ( & Warnable {
@ -68,7 +90,9 @@ func TestNilMethodsDontCrash(t *testing.T) {
}
func TestSetUnhealthyWithDuplicateThenHealthyAgain ( t * testing . T ) {
ht := Tracker { }
bus := eventbustest . NewBus ( t )
watcher := eventbustest . NewWatcher ( t , bus )
ht := NewTracker ( bus )
if len ( ht . Strings ( ) ) != 0 {
t . Fatalf ( "before first insertion, len(newTracker.Strings) = %d; want = 0" , len ( ht . Strings ( ) ) )
}
@ -92,10 +116,20 @@ func TestSetUnhealthyWithDuplicateThenHealthyAgain(t *testing.T) {
if ! reflect . DeepEqual ( ht . Strings ( ) , want ) {
t . Fatalf ( "after setting the healthy, newTracker.Strings() = %v; want = %v" , ht . Strings ( ) , want )
}
if err := eventbustest . ExpectExactly ( watcher ,
wantChange ( Change { WarnableChanged : true , Warnable : testWarnable } ) ,
wantChange ( Change { WarnableChanged : true , Warnable : testWarnable } ) ,
wantChange ( Change { WarnableChanged : true , Warnable : testWarnable } ) ,
) ; err != nil {
t . Fatalf ( "expected events, got %q" , err )
}
}
func TestRemoveAllWarnings ( t * testing . T ) {
ht := Tracker { }
bus := eventbustest . NewBus ( t )
watcher := eventbustest . NewWatcher ( t , bus )
ht := NewTracker ( bus )
if len ( ht . Strings ( ) ) != 0 {
t . Fatalf ( "before first insertion, len(newTracker.Strings) = %d; want = 0" , len ( ht . Strings ( ) ) )
}
@ -109,67 +143,105 @@ func TestRemoveAllWarnings(t *testing.T) {
if len ( ht . Strings ( ) ) != 0 {
t . Fatalf ( "after RemoveAll, len(newTracker.Strings) = %d; want = 0" , len ( ht . Strings ( ) ) )
}
if err := eventbustest . ExpectExactly ( watcher ,
wantChange ( Change { WarnableChanged : true , Warnable : testWarnable } ) ,
wantChange ( Change { WarnableChanged : true , Warnable : testWarnable } ) ,
) ; err != nil {
t . Fatalf ( "expected events, got %q" , err )
}
}
// TestWatcher tests that a registered watcher function gets called with the correct
// Warnable and non-nil/nil UnhealthyState upon setting a Warnable to unhealthy/healthy.
func TestWatcher ( t * testing . T ) {
ht := Tracker { }
wantText := "Hello world"
becameUnhealthy := make ( chan struct { } )
becameHealthy := make ( chan struct { } )
watcherFunc := func ( c Change ) {
w := c . Warnable
us := c . UnhealthyState
if w != testWarnable {
t . Fatalf ( "watcherFunc was called, but with an unexpected Warnable: %v, want: %v" , w , testWarnable )
}
tests := [ ] struct {
name string
preFunc func ( t * testing . T , ht * Tracker , bus * eventbus . Bus , fn func ( Change ) )
} {
{
name : "with-callbacks" ,
preFunc : func ( t * testing . T , tht * Tracker , _ * eventbus . Bus , fn func ( c Change ) ) {
t . Cleanup ( tht . RegisterWatcher ( fn ) )
if len ( tht . watchers ) != 1 {
t . Fatalf ( "after RegisterWatcher, len(newTracker.watchers) = %d; want = 1" , len ( tht . watchers ) )
}
} ,
} ,
{
name : "with-eventbus" ,
preFunc : func ( _ * testing . T , _ * Tracker , bus * eventbus . Bus , fn func ( c Change ) ) {
client := bus . Client ( "healthwatchertestclient" )
sub := eventbus . Subscribe [ Change ] ( client )
go func ( ) {
for {
select {
case <- sub . Done ( ) :
return
case change := <- sub . Events ( ) :
fn ( change )
}
}
} ( )
} ,
} ,
}
if us != nil {
if us . Text != wantText {
t . Fatalf ( "unexpected us.Text: %s, want: %s" , us . Text , wantText )
}
if us . Args [ ArgError ] != wantText {
t . Fatalf ( "unexpected us.Args[ArgError]: %s, want: %s" , us . Args [ ArgError ] , wantText )
for _ , tt := range tests {
t . Run ( tt . name , func ( * testing . T ) {
bus := eventbustest . NewBus ( t )
ht := NewTracker ( bus )
wantText := "Hello world"
becameUnhealthy := make ( chan struct { } )
becameHealthy := make ( chan struct { } )
watcherFunc := func ( c Change ) {
w := c . Warnable
us := c . UnhealthyState
if w != testWarnable {
t . Fatalf ( "watcherFunc was called, but with an unexpected Warnable: %v, want: %v" , w , testWarnable )
}
if us != nil {
if us . Text != wantText {
t . Fatalf ( "unexpected us.Text: %q, want: %s" , us . Text , wantText )
}
if us . Args [ ArgError ] != wantText {
t . Fatalf ( "unexpected us.Args[ArgError]: %q, want: %s" , us . Args [ ArgError ] , wantText )
}
becameUnhealthy <- struct { } { }
} else {
becameHealthy <- struct { } { }
}
}
becameUnhealthy <- struct { } { }
} else {
becameHealthy <- struct { } { }
}
}
unregisterFunc := ht . RegisterWatcher ( watcherFunc )
if len ( ht . watchers ) != 1 {
t . Fatalf ( "after RegisterWatcher, len(newTracker.watchers) = %d; want = 1" , len ( ht . watchers ) )
}
ht . SetUnhealthy ( testWarnable , Args { ArgError : wantText } )
// Set up test
tt . preFunc ( t , ht , bus , watcherFunc )
select {
case <- becameUnhealthy :
// Test passed because the watcher got notified of an unhealthy state
case <- becameHealthy :
// Test failed because the watcher got of a healthy state instead of an unhealthy one
t . Fatalf ( "watcherFunc was called with a healthy state" )
case <- time . After ( 1 * time . Second ) :
t . Fatalf ( "watcherFunc didn't get called upon calling SetUnhealthy" )
}
// Start running actual test
ht . SetUnhealthy ( testWarnable , Args { ArgError : wantText } )
ht . SetHealthy ( testWarnable )
select {
case <- becameUnhealthy :
// Test passed because the watcher got notified of an unhealthy state
case <- becameHealthy :
// Test failed because the watcher got of a healthy state instead of an unhealthy one
t . Fatalf ( "watcherFunc was called with a healthy state" )
case <- time . After ( 5 * time . Second ) :
t . Fatalf ( "watcherFunc didn't get called upon calling SetUnhealthy" )
}
select {
case <- becameUnhealthy :
// Test failed because the watcher got of an unhealthy state instead of a healthy one
t . Fatalf ( "watcherFunc was called with an unhealthy state" )
case <- becameHealthy :
// Test passed because the watcher got notified of a healthy state
case <- time . After ( 1 * time . Second ) :
t . Fatalf ( "watcherFunc didn't get called upon calling SetUnhealthy" )
}
ht . SetHealthy ( testWarnable )
unregisterFunc ( )
if len ( ht . watchers ) != 0 {
t . Fatalf ( "after unregisterFunc, len(newTracker.watchers) = %d; want = 0" , len ( ht . watchers ) )
select {
case <- becameUnhealthy :
// Test failed because the watcher got of an unhealthy state instead of a healthy one
t . Fatalf ( "watcherFunc was called with an unhealthy state" )
case <- becameHealthy :
// Test passed because the watcher got notified of a healthy state
case <- time . After ( 5 * time . Second ) :
t . Fatalf ( "watcherFunc didn't get called upon calling SetUnhealthy" )
}
} )
}
}
@ -178,45 +250,81 @@ func TestWatcher(t *testing.T) {
// has a TimeToVisible set, which means that a watcher should only be notified of an unhealthy state after
// the TimeToVisible duration has passed.
func TestSetUnhealthyWithTimeToVisible ( t * testing . T ) {
ht := Tracker { }
mw := Register ( & Warnable {
Code : "test-warnable-3-secs-to-visible" ,
Title : "Test Warnable with 3 seconds to visible" ,
Text : StaticMessage ( "Hello world" ) ,
TimeToVisible : 2 * time . Second ,
ImpactsConnectivity : true ,
} )
defer unregister ( mw )
becameUnhealthy := make ( chan struct { } )
becameHealthy := make ( chan struct { } )
tests := [ ] struct {
name string
preFunc func ( t * testing . T , ht * Tracker , bus * eventbus . Bus , fn func ( Change ) )
} {
{
name : "with-callbacks" ,
preFunc : func ( t * testing . T , tht * Tracker , _ * eventbus . Bus , fn func ( c Change ) ) {
t . Cleanup ( tht . RegisterWatcher ( fn ) )
if len ( tht . watchers ) != 1 {
t . Fatalf ( "after RegisterWatcher, len(newTracker.watchers) = %d; want = 1" , len ( tht . watchers ) )
}
} ,
} ,
{
name : "with-eventbus" ,
preFunc : func ( _ * testing . T , _ * Tracker , bus * eventbus . Bus , fn func ( c Change ) ) {
client := bus . Client ( "healthwatchertestclient" )
sub := eventbus . Subscribe [ Change ] ( client )
go func ( ) {
for {
select {
case <- sub . Done ( ) :
return
case change := <- sub . Events ( ) :
fn ( change )
}
}
} ( )
} ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( * testing . T ) {
bus := eventbustest . NewBus ( t )
ht := NewTracker ( bus )
mw := Register ( & Warnable {
Code : "test-warnable-3-secs-to-visible" ,
Title : "Test Warnable with 3 seconds to visible" ,
Text : StaticMessage ( "Hello world" ) ,
TimeToVisible : 2 * time . Second ,
ImpactsConnectivity : true ,
} )
watchFunc := func ( c Change ) {
w := c . Warnable
us := c . UnhealthyState
if w != mw {
t . Fatalf ( "watcherFunc was called, but with an unexpected Warnable: %v, want: %v" , w , w )
}
becameUnhealthy := make ( chan struct { } )
becameHealthy := make ( chan struct { } )
if us != nil {
becameUnhealthy <- struct { } { }
} else {
becameHealthy <- struct { } { }
}
}
watchFunc := func ( c Change ) {
w := c . Warnable
us := c . UnhealthyState
if w != mw {
t . Fatalf ( "watcherFunc was called, but with an unexpected Warnable: %v, want: %v" , w , w )
}
ht . RegisterWatcher ( watchFunc )
ht . SetUnhealthy ( mw , Args { ArgError : "Hello world" } )
if us != nil {
becameUnhealthy <- struct { } { }
} else {
becameHealthy <- struct { } { }
}
}
select {
case <- becameUnhealthy :
// Test failed because the watcher got notified of an unhealthy state
t . Fatalf ( "watcherFunc was called with an unhealthy state" )
case <- becameHealthy :
// Test failed because the watcher got of a healthy state
t . Fatalf ( "watcherFunc was called with a healthy state" )
case <- time . After ( 1 * time . Second ) :
// As expected, watcherFunc still had not been called after 1 second
tt . preFunc ( t , ht , bus , watchFunc )
ht . SetUnhealthy ( mw , Args { ArgError : "Hello world" } )
select {
case <- becameUnhealthy :
// Test failed because the watcher got notified of an unhealthy state
t . Fatalf ( "watcherFunc was called with an unhealthy state" )
case <- becameHealthy :
// Test failed because the watcher got of a healthy state
t . Fatalf ( "watcherFunc was called with a healthy state" )
case <- time . After ( 1 * time . Second ) :
// As expected, watcherFunc still had not been called after 1 second
}
unregister ( mw )
} )
}
}
@ -242,7 +350,7 @@ func TestRegisterWarnablePanicsWithDuplicate(t *testing.T) {
// TestCheckDependsOnAppearsInUnhealthyState asserts that the DependsOn field in the UnhealthyState
// is populated with the WarnableCode(s) of the Warnable(s) that a warning depends on.
func TestCheckDependsOnAppearsInUnhealthyState ( t * testing . T ) {
ht := Tracker { }
ht := NewTracker ( eventbustest . NewBus ( t ) )
w1 := Register ( & Warnable {
Code : "w1" ,
Text : StaticMessage ( "W1 Text" ) ,
@ -352,11 +460,11 @@ func TestShowUpdateWarnable(t *testing.T) {
}
for _ , tt := range tests {
t . Run ( tt . desc , func ( t * testing . T ) {
tr := & Tracker {
checkForUpdates : tt . check ,
applyUpdates : tt . apply ,
latestVersion : tt . cv ,
}
tr := NewTracker ( eventbustest . NewBus ( t ) )
tr . checkForUpdates = tt . check
tr . applyUpdates = tt . apply
tr . latestVersion = tt . cv
gotWarnable , gotShow := tr . showUpdateWarnable ( )
if gotWarnable != tt . wantWarnable {
t . Errorf ( "got warnable: %v, want: %v" , gotWarnable , tt . wantWarnable )
@ -401,11 +509,10 @@ func TestHealthMetric(t *testing.T) {
}
for _ , tt := range tests {
t . Run ( tt . desc , func ( t * testing . T ) {
tr := & Tracker {
checkForUpdates : tt . check ,
applyUpdates : tt . apply ,
latestVersion : tt . cv ,
}
tr := NewTracker ( eventbustest . NewBus ( t ) )
tr . checkForUpdates = tt . check
tr . applyUpdates = tt . apply
tr . latestVersion = tt . cv
tr . SetMetricsRegistry ( & usermetric . Registry { } )
if val := tr . metricHealthMessage . Get ( metricHealthMessageLabel { Type : MetricLabelWarning } ) . String ( ) ; val != strconv . Itoa ( tt . wantMetricCount ) {
t . Fatalf ( "metric value: %q, want: %q" , val , strconv . Itoa ( tt . wantMetricCount ) )
@ -426,9 +533,8 @@ func TestNoDERPHomeWarnable(t *testing.T) {
Start : time . Unix ( 123 , 0 ) ,
FollowRealTime : false ,
} )
ht := & Tracker {
testClock : clock ,
}
ht := NewTracker ( eventbustest . NewBus ( t ) )
ht . testClock = clock
ht . SetIPNState ( "NeedsLogin" , true )
// Advance 30 seconds to get past the "recentlyLoggedIn" check.
@ -448,7 +554,7 @@ func TestNoDERPHomeWarnable(t *testing.T) {
// but doesn't use tstest.Clock so avoids the deadlock
// I hit: https://github.com/tailscale/tailscale/issues/14798
func TestNoDERPHomeWarnableManual ( t * testing . T ) {
ht := & Tracker { }
ht := NewTracker ( eventbustest . NewBus ( t ) )
ht . SetIPNState ( "NeedsLogin" , true )
// Avoid wantRunning:
@ -462,7 +568,7 @@ func TestNoDERPHomeWarnableManual(t *testing.T) {
}
func TestControlHealth ( t * testing . T ) {
ht := Tracker { }
ht := NewTracker ( eventbustest . NewBus ( t ) )
ht . SetIPNState ( "NeedsLogin" , true )
ht . GotStreamedMapResponse ( )
@ -620,7 +726,7 @@ func TestControlHealthNotifies(t *testing.T) {
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
ht := Tracker { }
ht := NewTracker ( eventbustest . NewBus ( t ) )
ht . SetIPNState ( "NeedsLogin" , true )
ht . GotStreamedMapResponse ( )
@ -643,7 +749,7 @@ func TestControlHealthNotifies(t *testing.T) {
}
func TestControlHealthIgnoredOutsideMapPoll ( t * testing . T ) {
ht := Tracker { }
ht := NewTracker ( eventbustest . NewBus ( t ) )
ht . SetIPNState ( "NeedsLogin" , true )
gotNotified := false
@ -671,7 +777,7 @@ func TestControlHealthIgnoredOutsideMapPoll(t *testing.T) {
// created from Control health & returned by [Tracker.CurrentState] is different
// when the details of the [tailcfg.DisplayMessage] are different.
func TestCurrentStateETagControlHealth ( t * testing . T ) {
ht := Tracker { }
ht := NewTracker ( eventbustest . NewBus ( t ) )
ht . SetIPNState ( "NeedsLogin" , true )
ht . GotStreamedMapResponse ( )
@ -776,9 +882,8 @@ func TestCurrentStateETagControlHealth(t *testing.T) {
// when the details of the Warnable are different.
func TestCurrentStateETagWarnable ( t * testing . T ) {
newTracker := func ( clock tstime . Clock ) * Tracker {
ht := & Tracker {
testClock : clock ,
}
ht := NewTracker ( eventbustest . NewBus ( t ) )
ht . testClock = clock
ht . SetIPNState ( "NeedsLogin" , true )
ht . GotStreamedMapResponse ( )
return ht