mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
A successful macOS upgrade printed "could not get interface error=interface not found iface=en5" before reporting success, because the DNS cleanup logged every interface lookup failure at error level. An interface that is gone, an unplugged adapter or a torn down tether, has no DNS settings left to restore, so the skip is now a debug diagnostic naming the interface and the work it skipped. Only that condition is quiet. netInterface returns a sentinel error for a missing interface and no longer reports a failed enumeration as one, so any other lookup failure and any restoration failure on an interface that does exist stay at error level.
142 lines
4.8 KiB
Go
142 lines
4.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
// Test_logIfaceLookupFailure is a regression test for issue-608: a macOS
|
|
// upgrade that succeeded still printed
|
|
//
|
|
// ERR could not get interface error="interface not found" iface=en5
|
|
//
|
|
// because the DNS cleanup after the upgrade logged every interface-lookup
|
|
// failure at error level, including the interface it had been bound to simply
|
|
// being gone. Only that positively identified condition may be downgraded.
|
|
func Test_logIfaceLookupFailure(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
skipping string
|
|
err error
|
|
wantLevel string
|
|
wantMsg string
|
|
}{
|
|
{
|
|
name: "the previous interface is gone",
|
|
skipping: "DNS restoration",
|
|
err: errInterfaceNotFound,
|
|
wantLevel: "debug",
|
|
wantMsg: "Skipping DNS restoration: previous interface is no longer present",
|
|
},
|
|
{
|
|
name: "the missing interface is reported through a wrapped error",
|
|
skipping: "DNS restoration",
|
|
err: fmt.Errorf("looking up %q: %w", "en5", errInterfaceNotFound),
|
|
wantLevel: "debug",
|
|
wantMsg: "Skipping DNS restoration: previous interface is no longer present",
|
|
},
|
|
{
|
|
// The caller names the work it skipped, so the helper stays usable
|
|
// outside the DNS reset path.
|
|
name: "another caller names its own work",
|
|
skipping: "interface snapshot",
|
|
err: errInterfaceNotFound,
|
|
wantLevel: "debug",
|
|
wantMsg: "Skipping interface snapshot: previous interface is no longer present",
|
|
},
|
|
{
|
|
name: "the lookup failed for another reason",
|
|
skipping: "DNS restoration",
|
|
err: fmt.Errorf("patching interface name: %w", os.ErrPermission),
|
|
wantLevel: "error",
|
|
wantMsg: "could not get interface",
|
|
},
|
|
{
|
|
// A restoration failure reaches the same logger, and must stay
|
|
// visible: the interface exists and its DNS was left wrong.
|
|
name: "restoring DNS on an existing interface failed",
|
|
skipping: "DNS restoration",
|
|
err: fmt.Errorf("failed to restore static DNS config on interface %q: %w", "en0", os.ErrPermission),
|
|
wantLevel: "error",
|
|
wantMsg: "could not get interface",
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var buf syncBuffer
|
|
logger := zerolog.New(&buf).With().Str("iface", "en5").Logger()
|
|
|
|
logIfaceLookupFailure(&logger, tc.skipping, tc.err)
|
|
|
|
entry := soleLogEntry(t, buf.String())
|
|
if got := entry["level"]; got != tc.wantLevel {
|
|
t.Errorf("level = %v, want %q (line: %s)", got, tc.wantLevel, strings.TrimSpace(buf.String()))
|
|
}
|
|
if got := entry["message"]; got != tc.wantMsg {
|
|
t.Errorf("message = %v, want %q", got, tc.wantMsg)
|
|
}
|
|
if got := entry["iface"]; got != "en5" {
|
|
t.Errorf("iface = %v, want %q: the diagnostic must name the interface", got, "en5")
|
|
}
|
|
if tc.wantLevel == "error" {
|
|
if got := entry["error"]; got == nil {
|
|
t.Error("error field missing: a genuine failure must keep its cause")
|
|
}
|
|
} else if got, ok := entry["error"]; ok {
|
|
t.Errorf("error = %v, want no error field on an expected skip", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test_resetDNSForRunningIfaceMissingIface drives the real cleanup path with an
|
|
// interface that does not exist, which is what an upgrade sees after the
|
|
// adapter ctrld was bound to went away. It changes no DNS settings: the lookup
|
|
// fails before any restoration runs.
|
|
func Test_resetDNSForRunningIfaceMissingIface(t *testing.T) {
|
|
level := zerolog.GlobalLevel()
|
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
|
t.Cleanup(func() { zerolog.SetGlobalLevel(level) })
|
|
|
|
old := mainLog.Load()
|
|
var buf syncBuffer
|
|
logger := zerolog.New(&buf)
|
|
mainLog.Store(&logger)
|
|
t.Cleanup(func() { mainLog.Store(old) })
|
|
|
|
// A name no host assigns, so netInterface reports it as missing rather
|
|
// than restoring DNS on a real adapter.
|
|
p := &prog{runningIface: "ctrld-absent-iface0"}
|
|
|
|
if got := p.resetDNSForRunningIface(false, true); got != nil {
|
|
t.Errorf("resetDNSForRunningIface returned %v, want nil for a missing interface", got)
|
|
}
|
|
|
|
entry := soleLogEntry(t, buf.String())
|
|
if got := entry["level"]; got != "debug" {
|
|
t.Errorf("level = %v, want %q: a missing interface must not make a successful upgrade look broken (line: %s)",
|
|
got, "debug", strings.TrimSpace(buf.String()))
|
|
}
|
|
if got := entry["iface"]; got != p.runningIface {
|
|
t.Errorf("iface = %v, want %q", got, p.runningIface)
|
|
}
|
|
}
|
|
|
|
// soleLogEntry decodes the single JSON log line in out.
|
|
func soleLogEntry(t *testing.T, out string) map[string]any {
|
|
t.Helper()
|
|
lines := strings.Split(strings.TrimSpace(out), "\n")
|
|
if len(lines) != 1 || lines[0] == "" {
|
|
t.Fatalf("want exactly one log line, got %d:\n%s", len(lines), out)
|
|
}
|
|
var entry map[string]any
|
|
if err := json.Unmarshal([]byte(lines[0]), &entry); err != nil {
|
|
t.Fatalf("decoding log line %q: %v", lines[0], err)
|
|
}
|
|
return entry
|
|
}
|