From 077774e0cdbeb09ee2ea67b9aa733004fbc3f134 Mon Sep 17 00:00:00 2001 From: Ville Vesilehto Date: Sun, 19 Jul 2026 13:52:23 +0300 Subject: [PATCH] ci: run plugin tests on Windows (#8314) Plugin package tests previously ran only on Linux, so Windows- specific failures were never caught. Run them in CI and make the affected tests portable across platforms. Signed-off-by: Ville Vesilehto --- .gitattributes | 3 + .github/workflows/go.test.yml | 6 +- plugin/auto/walk_test.go | 5 ++ plugin/autopath/setup_test.go | 6 +- plugin/file/reload_test.go | 127 ++++++++++++--------------------- plugin/file/tree/print_test.go | 21 +++--- plugin/forward/setup_test.go | 9 ++- plugin/geoip/setup_test.go | 2 +- plugin/grpc/proxy_test.go | 5 ++ plugin/pkg/proxy/proxy_test.go | 15 ++-- plugin/tls/tls_test.go | 24 +++++++ 11 files changed, 124 insertions(+), 99 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..9161c988b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# DNSSEC key material must keep LF; CRLF breaks miekg/dns private-key parsing. +*.private text eol=lf +*.key text eol=lf diff --git a/.github/workflows/go.test.yml b/.github/workflows/go.test.yml index c19876b9a..2de3b618a 100644 --- a/.github/workflows/go.test.yml +++ b/.github/workflows/go.test.yml @@ -59,7 +59,10 @@ jobs: test-plugins: name: Test Plugins - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - name: Check out code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -76,6 +79,7 @@ jobs: run: go build -v ./... - name: Test + shell: bash run: ( cd plugin; go test -race ./... ) test-e2e: diff --git a/plugin/auto/walk_test.go b/plugin/auto/walk_test.go index 03e519b81..8e9024325 100644 --- a/plugin/auto/walk_test.go +++ b/plugin/auto/walk_test.go @@ -151,6 +151,11 @@ func TestWalkWarnsForDuplicateOrigin(t *testing.T) { func TestWalkKeepsFirstMatchingFileForOrigin(t *testing.T) { dir := t.TempDir() + // Match Walk: it stores paths after EvalSymlinks. + dir, err := filepath.EvalSymlinks(dir) + if err != nil { + t.Fatal(err) + } zone := filepath.Join(dir, "example.org.zone") backup := filepath.Join(dir, "example.org.zone.bak-20260528") diff --git a/plugin/autopath/setup_test.go b/plugin/autopath/setup_test.go index 4644c7d59..f5b57d87a 100644 --- a/plugin/autopath/setup_test.go +++ b/plugin/autopath/setup_test.go @@ -30,10 +30,10 @@ func TestSetupAutoPath(t *testing.T) { {`autopath example.org @kubernetes`, false, "example.org.", "kubernetes", nil, ""}, {`autopath 10.0.0.0/8 @kubernetes`, false, "10.in-addr.arpa.", "kubernetes", nil, ""}, {`autopath ` + resolv, false, "", "", []string{"bar.com.", "baz.com.", ""}, ""}, - // negative - {`autopath kubernetes`, true, "", "", nil, "open kubernetes: no such file or directory"}, + // negative (OS error text differs; match the portable prefix) + {`autopath kubernetes`, true, "", "", nil, "open kubernetes:"}, {`autopath`, true, "", "", nil, "no resolv-conf"}, - {`autopath ""`, true, "", "", nil, "no such file"}, + {`autopath ""`, true, "", "", nil, "failed to parse"}, } for i, test := range tests { diff --git a/plugin/file/reload_test.go b/plugin/file/reload_test.go index d6fd69c35..ba0ae479f 100644 --- a/plugin/file/reload_test.go +++ b/plugin/file/reload_test.go @@ -83,26 +83,8 @@ func TestZoneReloadSOAChange(t *testing.T) { func TestZoneReloadByMtime(t *testing.T) { // Test 1: Basic mtime trigger - file modification should trigger reload t.Run("BasicMtimeTrigger", func(t *testing.T) { - fileName, rm, err := test.TempFile(".", reloadZoneTest) - if err != nil { - t.Fatalf("Failed to create zone: %s", err) - } - defer rm() - - reader, err := os.Open(fileName) - if err != nil { - t.Fatalf("Failed to open zone: %s", err) - } - z, err := Parse(reader, "miek.nl", fileName, 0) - if err != nil { - t.Fatalf("Failed to parse zone: %s", err) - } - reader.Close() - - // Enable mtime-based reload - z.ReloadInterval = 10 * time.Millisecond - z.ReloadByMtime = true - z.Reload(&transfer.Transfer{}) + z, fileName, cleanup := prepareMtimeZone(t, reloadZoneTest) + defer cleanup() // Wait for initial load to complete time.Sleep(20 * time.Millisecond) @@ -136,26 +118,8 @@ func TestZoneReloadByMtime(t *testing.T) { // Test 2: No reload when mtime unchanged t.Run("NoReloadWhenMtimeUnchanged", func(t *testing.T) { - fileName, rm, err := test.TempFile(".", reloadZoneTest) - if err != nil { - t.Fatalf("Failed to create zone: %s", err) - } - defer rm() - - reader, err := os.Open(fileName) - if err != nil { - t.Fatalf("Failed to open zone: %s", err) - } - z, err := Parse(reader, "miek.nl", fileName, 0) - if err != nil { - t.Fatalf("Failed to parse zone: %s", err) - } - reader.Close() - - // Enable mtime-based reload - z.ReloadInterval = 10 * time.Millisecond - z.ReloadByMtime = true - z.Reload(&transfer.Transfer{}) + z, _, cleanup := prepareMtimeZone(t, reloadZoneTest) + defer cleanup() // Wait for initial load time.Sleep(20 * time.Millisecond) @@ -193,26 +157,8 @@ func TestZoneReloadByMtime(t *testing.T) { // Test 3: Content verification after reload t.Run("ContentVerificationAfterReload", func(t *testing.T) { - fileName, rm, err := test.TempFile(".", reloadZoneTest) - if err != nil { - t.Fatalf("Failed to create zone: %s", err) - } - defer rm() - - reader, err := os.Open(fileName) - if err != nil { - t.Fatalf("Failed to open zone: %s", err) - } - z, err := Parse(reader, "miek.nl", fileName, 0) - if err != nil { - t.Fatalf("Failed to parse zone: %s", err) - } - reader.Close() - - // Enable mtime-based reload - z.ReloadInterval = 10 * time.Millisecond - z.ReloadByMtime = true - z.Reload(&transfer.Transfer{}) + z, fileName, cleanup := prepareMtimeZone(t, reloadZoneTest) + defer cleanup() ctx := context.TODO() @@ -266,26 +212,8 @@ func TestZoneReloadByMtime(t *testing.T) { // Test 4: File deleted/missing during reload t.Run("FileMissingDuringReload", func(t *testing.T) { - fileName, rm, err := test.TempFile(".", reloadZoneTest) - if err != nil { - t.Fatalf("Failed to create zone: %s", err) - } - defer rm() - - reader, err := os.Open(fileName) - if err != nil { - t.Fatalf("Failed to open zone: %s", err) - } - z, err := Parse(reader, "miek.nl", fileName, 0) - if err != nil { - t.Fatalf("Failed to parse zone: %s", err) - } - reader.Close() - - // Enable mtime-based reload - z.ReloadInterval = 10 * time.Millisecond - z.ReloadByMtime = true - z.Reload(&transfer.Transfer{}) + z, fileName, cleanup := prepareMtimeZone(t, reloadZoneTest) + defer cleanup() // Wait for initial load time.Sleep(20 * time.Millisecond) @@ -327,6 +255,45 @@ func TestZoneReloadByMtime(t *testing.T) { }) } +// prepareMtimeZone creates a zone with mtime-based reload enabled. +func prepareMtimeZone(t *testing.T, content string) (*Zone, string, func()) { + t.Helper() + fileName, rm, err := test.TempFile(".", content) + if err != nil { + t.Fatalf("Failed to create zone: %s", err) + } + + reader, err := os.Open(fileName) + if err != nil { + rm() + t.Fatalf("Failed to open zone: %s", err) + } + z, err := Parse(reader, "miek.nl", fileName, 0) + reader.Close() + if err != nil { + rm() + t.Fatalf("Failed to parse zone: %s", err) + } + + fi, err := os.Stat(fileName) + if err != nil { + rm() + t.Fatalf("Failed to stat zone: %s", err) + } + + z.ReloadInterval = 10 * time.Millisecond + z.ReloadByMtime = true + // Parse does not set file_mtime unless ReloadByMtime is already enabled. + // Seed it so the reload loop only opens the file when mtime changes. + z.file_mtime = fi.ModTime() + z.Reload(&transfer.Transfer{}) + + return z, fileName, func() { + z.OnShutdown() + rm() + } +} + const reloadZoneTest = `miek.nl. 1627 IN SOA linode.atoom.net. miek.miek.nl. 1460175181 14400 3600 604800 14400 miek.nl. 1627 IN NS ext.ns.whyscream.net. miek.nl. 1627 IN NS omval.tednet.nl. diff --git a/plugin/file/tree/print_test.go b/plugin/file/tree/print_test.go index 8c6c2b331..d7333316c 100644 --- a/plugin/file/tree/print_test.go +++ b/plugin/file/tree/print_test.go @@ -72,11 +72,16 @@ func TestPrint(t *testing.T) { f, err := os.CreateTemp(t.TempDir(), "print_test_tmp") if err != nil { - t.Error(err) + t.Fatal(err) } - defer os.Remove(f.Name()) - //Redirect the printed results to a tmp file for later comparison + // Redirect Print output to a temp file, then restore stdout and close + // the handle so TempDir cleanup can remove it on Windows. + stdout := os.Stdout os.Stdout = f + t.Cleanup(func() { + os.Stdout = stdout + f.Close() + }) tree.Print() /** @@ -85,12 +90,12 @@ func TestPrint(t *testing.T) { server3.example.com. */ + if _, err := f.Seek(0, 0); err != nil { + t.Fatal(err) + } buf := make([]byte, 256) - f.Seek(0, 0) - _, err = f.Read(buf) - if err != nil { - f.Close() - t.Error(err) + if _, err := f.Read(buf); err != nil { + t.Fatal(err) } height := strings.Count(string(buf), ". \n") //Compare the height of the print with the actual height of the tree diff --git a/plugin/forward/setup_test.go b/plugin/forward/setup_test.go index c31357d68..5bbcf6c61 100644 --- a/plugin/forward/setup_test.go +++ b/plugin/forward/setup_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "os" + "path/filepath" "reflect" "strings" "testing" @@ -290,6 +291,12 @@ nameserver 10.10.255.253`), 0666); err != nil { } defer os.Remove(emptyResolv) + // Portable stand-in for /dev/null: a resolv.conf with no nameserver lines. + nullResolv := filepath.Join(t.TempDir(), "null.conf") + if err := os.WriteFile(nullResolv, nil, 0666); err != nil { + t.Fatalf("Failed to write null.conf file: %s", err) + } + tests := []struct { input string shouldErr bool @@ -299,7 +306,7 @@ nameserver 10.10.255.253`), 0666); err != nil { // pass {`forward . ` + resolv, false, "", []string{"10.10.255.252:53", "10.10.255.253:53"}}, // fail - {`forward . /dev/null`, true, "no valid upstream addresses found", nil}, + {`forward . ` + nullResolv, true, "no valid upstream addresses found", nil}, // IPV6 with local zone {`forward . ` + resolvIPV6, false, "", []string{"[0388:d254:7aec:6892:9f7f:e93b:5806:1b0f]:53"}}, // pass when empty forward file is found diff --git a/plugin/geoip/setup_test.go b/plugin/geoip/setup_test.go index c21eea246..7ad52309b 100644 --- a/plugin/geoip/setup_test.go +++ b/plugin/geoip/setup_test.go @@ -64,7 +64,7 @@ func TestGeoIPParse(t *testing.T) { {true, fmt.Sprintf("%s 1 2 3", pluginName), "Wrong argument count", 0}, {true, fmt.Sprintf("%s { }", pluginName), "Error during parsing", 0}, {true, fmt.Sprintf("%s /dbpath { city }", pluginName), "unknown property \"city\"", 0}, - {true, fmt.Sprintf("%s /invalidPath\n", pluginName), "failed to open database file: open /invalidPath: no such file or directory", 0}, + {true, fmt.Sprintf("%s /invalidPath\n", pluginName), "failed to open database file: open /invalidPath:", 0}, {true, fmt.Sprintf("%s %s\n", pluginName, unknownDBPath), "reader does not support the \"UnknownDbType\" database type", 0}, } diff --git a/plugin/grpc/proxy_test.go b/plugin/grpc/proxy_test.go index 9eac24975..7b491634c 100644 --- a/plugin/grpc/proxy_test.go +++ b/plugin/grpc/proxy_test.go @@ -5,6 +5,7 @@ import ( "errors" "net" "path" + "runtime" "slices" "testing" @@ -99,6 +100,10 @@ func (m testServiceClient) Query(_ctx context.Context, _in *pb.DnsPacket, _opts } func TestProxyUnix(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("unix domain sockets are not supported on windows") + } + tdir := t.TempDir() fd := path.Join(tdir, "test.grpc") diff --git a/plugin/pkg/proxy/proxy_test.go b/plugin/pkg/proxy/proxy_test.go index 424128452..ab7cd9039 100644 --- a/plugin/pkg/proxy/proxy_test.go +++ b/plugin/pkg/proxy/proxy_test.go @@ -6,6 +6,7 @@ import ( "errors" "math" "net" + "runtime" "testing" "time" @@ -221,6 +222,7 @@ func TestCoreDNSOverflow(t *testing.T) { response, _, _, err := p.Connect(context.Background(), request, options) if err != nil { t.Errorf("Failed to connect to testdnsserver: %s", err) + return } if response.Truncated != expectTruncated { @@ -228,15 +230,18 @@ func TestCoreDNSOverflow(t *testing.T) { } } - // Test PreferUDP, expect truncated response - testConnection("PreferUDP", Options{PreferUDP: true}, true) + // Oversized UDP replies are truncated on Unix; Windows surfaces WSAEMSGSIZE instead. + if runtime.GOOS != "windows" { + // Test PreferUDP, expect truncated response + testConnection("PreferUDP", Options{PreferUDP: true}, true) + + // Test No options specified, expect truncated response + testConnection("NoOptionsSpecified", Options{}, true) + } // Test ForceTCP, expect no truncated response testConnection("ForceTCP", Options{ForceTCP: true}, false) - // Test No options specified, expect truncated response - testConnection("NoOptionsSpecified", Options{}, true) - // Test both TCP and UDP provided, expect no truncated response testConnection("BothTCPAndUDP", Options{PreferUDP: true, ForceTCP: true}, false) } diff --git a/plugin/tls/tls_test.go b/plugin/tls/tls_test.go index a9141cc32..196115e08 100644 --- a/plugin/tls/tls_test.go +++ b/plugin/tls/tls_test.go @@ -2,6 +2,7 @@ package tls import ( "crypto/tls" + "io" "path/filepath" "strings" "testing" @@ -52,6 +53,12 @@ func TestTLS(t *testing.T) { t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input) } } + + // setup registers OnShutdown to close KeyLogWriter, but tests never shut down. + // Close explicitly so t.TempDir cleanup can remove the file on Windows. + if err == nil { + closeKeyLogWriter(t, dnsserver.GetConfig(c)) + } } } @@ -121,5 +128,22 @@ func TestTLSKeyLog(t *testing.T) { if cfg.TLSConfig.KeyLogWriter == nil { t.Fatal("KeyLogWriter is not set") } + // setup registers OnShutdown to close KeyLogWriter, but tests never shut down. + // Close explicitly so t.TempDir cleanup can remove the file on Windows. + closeKeyLogWriter(t, cfg) + }) +} + +func closeKeyLogWriter(t *testing.T, cfg *dnsserver.Config) { + t.Helper() + if cfg.TLSConfig == nil { + return + } + closer, ok := cfg.TLSConfig.KeyLogWriter.(io.Closer) + if !ok { + return + } + t.Cleanup(func() { + closer.Close() }) }