Files
ctrld/cmd/cli/net_windows_test.go
T
Anthony Wong 8ae09ac98e feat(cli): put the network state of the host in the log journal
The journal records the network state of the host and each change of
that state. The support team reads one file to see the interfaces, the
routes, the resolvers, and the recoveries around an incident. The same
file shows the health of the query path. A change that touches only
AirDrop or virtual interfaces runs no DNS work.
2026-09-28 16:49:05 +07:00

184 lines
5.7 KiB
Go

package cli
import (
"bufio"
"bytes"
"slices"
"strings"
"sync"
"testing"
"time"
)
func Test_validInterfaces(t *testing.T) {
verbose = 3
initConsoleLogging()
start := time.Now()
ifaces := validInterfaces()
t.Logf("Using Windows API takes: %d", time.Since(start).Milliseconds())
start = time.Now()
ifacesPowershell := validInterfacesPowershell()
t.Logf("Using Powershell takes: %d", time.Since(start).Milliseconds())
slices.Sort(ifaces)
slices.Sort(ifacesPowershell)
if !slices.Equal(ifaces, ifacesPowershell) {
t.Fatalf("result mismatch, want: %v, got: %v", ifacesPowershell, ifaces)
}
}
func validInterfacesPowershell() []string {
out, err := powershell("Get-NetAdapter -Physical | Select-Object -ExpandProperty Name")
if err != nil {
return nil
}
var res []string
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
ifaceName := strings.TrimSpace(scanner.Text())
res = append(res, ifaceName)
}
return res
}
func Test_adapterClass(t *testing.T) {
tests := []struct {
name string
adapter adapterInfo
want string
}{
{"virtual without a connector", adapterInfo{Virtual: true}, "virtual"},
{"virtual with a connector", adapterInfo{Virtual: true, ConnectorPresent: true}, "hardware"},
{"physical adapter", adapterInfo{ConnectorPresent: true, Hardware: true}, "hardware"},
{"unknown adapter", adapterInfo{}, "hardware"},
{"tap driver", adapterInfo{Description: "TAP-Windows Adapter V9", Virtual: true}, "tunnel"},
{"wintun driver", adapterInfo{Description: "Wintun Userspace Tunnel", Virtual: true}, "tunnel"},
{"wireguard driver", adapterInfo{Description: "WireGuard Tunnel #1", Virtual: true}, "tunnel"},
{"vpn description", adapterInfo{Description: "Cisco AnyConnect VPN Adapter", ConnectorPresent: true}, "tunnel"},
{"lower case description", adapterInfo{Description: "openvpn tap-windows6"}, "tunnel"},
{"ethernet adapter", adapterInfo{Description: "Intel(R) Ethernet Connection I219-V", ConnectorPresent: true, Hardware: true}, "hardware"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := adapterClass(tc.adapter); got != tc.want {
t.Errorf("adapterClass(%+v) = %q, want %q", tc.adapter, got, tc.want)
}
})
}
}
// stubAdapters replaces the WMI and clock seams of the adapter cache, and
// restores them with the cache itself.
func stubAdapters(t *testing.T, read func() []adapterInfo, now func() time.Time) {
t.Helper()
readBefore, nowBefore := readAdaptersFn, adapterCacheNowFn
adapterCacheMu.Lock()
cacheBefore, readAtBefore := adapterCache, adapterCacheReadAt
adapterCache, adapterCacheReadAt = nil, time.Time{}
adapterCacheMu.Unlock()
t.Cleanup(func() {
readAdaptersFn, adapterCacheNowFn = readBefore, nowBefore
adapterCacheMu.Lock()
adapterCache, adapterCacheReadAt = cacheBefore, readAtBefore
adapterCacheMu.Unlock()
})
readAdaptersFn, adapterCacheNowFn = read, now
}
func Test_adapterCache(t *testing.T) {
now := time.Unix(1700000000, 0)
reads := 0
stubAdapters(t, func() []adapterInfo {
reads++
return []adapterInfo{
{Name: "Ethernet", Description: "Intel I219-V", ConnectorPresent: true, Hardware: true},
{Name: "Hyper-V Switch", Description: "Hyper-V Virtual Switch", Virtual: true},
}
}, func() time.Time { return now })
if got := validInterfaces(); !slices.Equal(got, []string{"Ethernet"}) {
t.Fatalf("validInterfaces = %v, want [Ethernet]", got)
}
now = now.Add(59 * time.Second)
validInterfacesMap()
if reads != 1 {
t.Fatalf("reads within the cache window = %d, want 1", reads)
}
now = now.Add(2 * time.Second)
adapters()
if reads != 2 {
t.Fatalf("reads after the cache window = %d, want 2", reads)
}
refreshInterfaceMeta()
if reads != 2 {
t.Fatalf("reads for a refresh alone = %d, want 2", reads)
}
adapters()
if reads != 3 {
t.Fatalf("reads after a refresh = %d, want 3", reads)
}
virtual := platformVirtualInterfaces()
if _, ok := virtual["Hyper-V Switch"]; !ok || len(virtual) != 1 {
t.Fatalf("platformVirtualInterfaces = %v, want the virtual switch only", virtual)
}
class, hardwarePort, service := platformInterfaceMeta("Ethernet")
if class != "hardware" || hardwarePort != "Intel I219-V" || service != "" {
t.Fatalf("platformInterfaceMeta = %q, %q, %q, want hardware, Intel I219-V, empty", class, hardwarePort, service)
}
}
func Test_adapterCacheReadsOnceForTwoCallers(t *testing.T) {
now := time.Unix(1700000000, 0)
var mu sync.Mutex
reads := 0
stubAdapters(t, func() []adapterInfo {
mu.Lock()
reads++
mu.Unlock()
time.Sleep(10 * time.Millisecond)
return []adapterInfo{{Name: "Ethernet", Description: "Intel I219-V", ConnectorPresent: true, Hardware: true}}
}, func() time.Time { return now })
var wg sync.WaitGroup
for range 4 {
wg.Add(1)
go func() {
defer wg.Done()
adapters()
}()
}
wg.Wait()
mu.Lock()
defer mu.Unlock()
if reads != 1 {
t.Fatalf("queries for four callers = %d, want 1", reads)
}
}
func Test_adapterCacheKeepsTheRemovedAdapter(t *testing.T) {
now := time.Unix(1700000000, 0)
removed := false
stubAdapters(t, func() []adapterInfo {
read := []adapterInfo{{Name: "Ethernet", Description: "Intel I219-V", ConnectorPresent: true, Hardware: true}}
if removed {
return read
}
return append(read, adapterInfo{Name: "WireGuard", Description: "WireGuard Tunnel #1", Virtual: true})
}, func() time.Time { return now })
adapters()
removed = true
refreshInterfaceMeta()
class, hardwarePort, _ := platformInterfaceMeta("WireGuard")
if class != "tunnel" || hardwarePort != "WireGuard Tunnel #1" {
t.Fatalf("meta of the removed adapter = %q, %q, want tunnel, WireGuard Tunnel #1", class, hardwarePort)
}
if class, _, _ := platformInterfaceMeta("Absent"); class != "" {
t.Fatalf("meta of an adapter that no read named = %q, want empty", class)
}
}