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 <ville@vesilehto.fi>
This commit is contained in:
Ville Vesilehto
2026-07-19 13:52:23 +03:00
committed by GitHub
parent 96ec17d5c6
commit 077774e0cd
11 changed files with 124 additions and 99 deletions

3
.gitattributes vendored Normal file
View File

@@ -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

View File

@@ -59,7 +59,10 @@ jobs:
test-plugins: test-plugins:
name: Test Plugins name: Test Plugins
runs-on: ubuntu-latest strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -76,6 +79,7 @@ jobs:
run: go build -v ./... run: go build -v ./...
- name: Test - name: Test
shell: bash
run: ( cd plugin; go test -race ./... ) run: ( cd plugin; go test -race ./... )
test-e2e: test-e2e:

View File

@@ -151,6 +151,11 @@ func TestWalkWarnsForDuplicateOrigin(t *testing.T) {
func TestWalkKeepsFirstMatchingFileForOrigin(t *testing.T) { func TestWalkKeepsFirstMatchingFileForOrigin(t *testing.T) {
dir := t.TempDir() 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") zone := filepath.Join(dir, "example.org.zone")
backup := filepath.Join(dir, "example.org.zone.bak-20260528") backup := filepath.Join(dir, "example.org.zone.bak-20260528")

View File

@@ -30,10 +30,10 @@ func TestSetupAutoPath(t *testing.T) {
{`autopath example.org @kubernetes`, false, "example.org.", "kubernetes", nil, ""}, {`autopath example.org @kubernetes`, false, "example.org.", "kubernetes", nil, ""},
{`autopath 10.0.0.0/8 @kubernetes`, false, "10.in-addr.arpa.", "kubernetes", nil, ""}, {`autopath 10.0.0.0/8 @kubernetes`, false, "10.in-addr.arpa.", "kubernetes", nil, ""},
{`autopath ` + resolv, false, "", "", []string{"bar.com.", "baz.com.", ""}, ""}, {`autopath ` + resolv, false, "", "", []string{"bar.com.", "baz.com.", ""}, ""},
// negative // negative (OS error text differs; match the portable prefix)
{`autopath kubernetes`, true, "", "", nil, "open kubernetes: no such file or directory"}, {`autopath kubernetes`, true, "", "", nil, "open kubernetes:"},
{`autopath`, true, "", "", nil, "no resolv-conf"}, {`autopath`, true, "", "", nil, "no resolv-conf"},
{`autopath ""`, true, "", "", nil, "no such file"}, {`autopath ""`, true, "", "", nil, "failed to parse"},
} }
for i, test := range tests { for i, test := range tests {

View File

@@ -83,26 +83,8 @@ func TestZoneReloadSOAChange(t *testing.T) {
func TestZoneReloadByMtime(t *testing.T) { func TestZoneReloadByMtime(t *testing.T) {
// Test 1: Basic mtime trigger - file modification should trigger reload // Test 1: Basic mtime trigger - file modification should trigger reload
t.Run("BasicMtimeTrigger", func(t *testing.T) { t.Run("BasicMtimeTrigger", func(t *testing.T) {
fileName, rm, err := test.TempFile(".", reloadZoneTest) z, fileName, cleanup := prepareMtimeZone(t, reloadZoneTest)
if err != nil { defer cleanup()
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{})
// Wait for initial load to complete // Wait for initial load to complete
time.Sleep(20 * time.Millisecond) time.Sleep(20 * time.Millisecond)
@@ -136,26 +118,8 @@ func TestZoneReloadByMtime(t *testing.T) {
// Test 2: No reload when mtime unchanged // Test 2: No reload when mtime unchanged
t.Run("NoReloadWhenMtimeUnchanged", func(t *testing.T) { t.Run("NoReloadWhenMtimeUnchanged", func(t *testing.T) {
fileName, rm, err := test.TempFile(".", reloadZoneTest) z, _, cleanup := prepareMtimeZone(t, reloadZoneTest)
if err != nil { defer cleanup()
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{})
// Wait for initial load // Wait for initial load
time.Sleep(20 * time.Millisecond) time.Sleep(20 * time.Millisecond)
@@ -193,26 +157,8 @@ func TestZoneReloadByMtime(t *testing.T) {
// Test 3: Content verification after reload // Test 3: Content verification after reload
t.Run("ContentVerificationAfterReload", func(t *testing.T) { t.Run("ContentVerificationAfterReload", func(t *testing.T) {
fileName, rm, err := test.TempFile(".", reloadZoneTest) z, fileName, cleanup := prepareMtimeZone(t, reloadZoneTest)
if err != nil { defer cleanup()
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{})
ctx := context.TODO() ctx := context.TODO()
@@ -266,26 +212,8 @@ func TestZoneReloadByMtime(t *testing.T) {
// Test 4: File deleted/missing during reload // Test 4: File deleted/missing during reload
t.Run("FileMissingDuringReload", func(t *testing.T) { t.Run("FileMissingDuringReload", func(t *testing.T) {
fileName, rm, err := test.TempFile(".", reloadZoneTest) z, fileName, cleanup := prepareMtimeZone(t, reloadZoneTest)
if err != nil { defer cleanup()
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{})
// Wait for initial load // Wait for initial load
time.Sleep(20 * time.Millisecond) 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 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 ext.ns.whyscream.net.
miek.nl. 1627 IN NS omval.tednet.nl. miek.nl. 1627 IN NS omval.tednet.nl.

View File

@@ -72,11 +72,16 @@ func TestPrint(t *testing.T) {
f, err := os.CreateTemp(t.TempDir(), "print_test_tmp") f, err := os.CreateTemp(t.TempDir(), "print_test_tmp")
if err != nil { if err != nil {
t.Error(err) t.Fatal(err)
} }
defer os.Remove(f.Name()) // Redirect Print output to a temp file, then restore stdout and close
//Redirect the printed results to a tmp file for later comparison // the handle so TempDir cleanup can remove it on Windows.
stdout := os.Stdout
os.Stdout = f os.Stdout = f
t.Cleanup(func() {
os.Stdout = stdout
f.Close()
})
tree.Print() tree.Print()
/** /**
@@ -85,12 +90,12 @@ func TestPrint(t *testing.T) {
server3.example.com. server3.example.com.
*/ */
if _, err := f.Seek(0, 0); err != nil {
t.Fatal(err)
}
buf := make([]byte, 256) buf := make([]byte, 256)
f.Seek(0, 0) if _, err := f.Read(buf); err != nil {
_, err = f.Read(buf) t.Fatal(err)
if err != nil {
f.Close()
t.Error(err)
} }
height := strings.Count(string(buf), ". \n") height := strings.Count(string(buf), ". \n")
//Compare the height of the print with the actual height of the tree //Compare the height of the print with the actual height of the tree

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net" "net"
"os" "os"
"path/filepath"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
@@ -290,6 +291,12 @@ nameserver 10.10.255.253`), 0666); err != nil {
} }
defer os.Remove(emptyResolv) 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 { tests := []struct {
input string input string
shouldErr bool shouldErr bool
@@ -299,7 +306,7 @@ nameserver 10.10.255.253`), 0666); err != nil {
// pass // pass
{`forward . ` + resolv, false, "", []string{"10.10.255.252:53", "10.10.255.253:53"}}, {`forward . ` + resolv, false, "", []string{"10.10.255.252:53", "10.10.255.253:53"}},
// fail // fail
{`forward . /dev/null`, true, "no valid upstream addresses found", nil}, {`forward . ` + nullResolv, true, "no valid upstream addresses found", nil},
// IPV6 with local zone // IPV6 with local zone
{`forward . ` + resolvIPV6, false, "", []string{"[0388:d254:7aec:6892:9f7f:e93b:5806:1b0f]:53"}}, {`forward . ` + resolvIPV6, false, "", []string{"[0388:d254:7aec:6892:9f7f:e93b:5806:1b0f]:53"}},
// pass when empty forward file is found // pass when empty forward file is found

View File

@@ -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 1 2 3", pluginName), "Wrong argument count", 0},
{true, fmt.Sprintf("%s { }", pluginName), "Error during parsing", 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 /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}, {true, fmt.Sprintf("%s %s\n", pluginName, unknownDBPath), "reader does not support the \"UnknownDbType\" database type", 0},
} }

View File

@@ -5,6 +5,7 @@ import (
"errors" "errors"
"net" "net"
"path" "path"
"runtime"
"slices" "slices"
"testing" "testing"
@@ -99,6 +100,10 @@ func (m testServiceClient) Query(_ctx context.Context, _in *pb.DnsPacket, _opts
} }
func TestProxyUnix(t *testing.T) { func TestProxyUnix(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("unix domain sockets are not supported on windows")
}
tdir := t.TempDir() tdir := t.TempDir()
fd := path.Join(tdir, "test.grpc") fd := path.Join(tdir, "test.grpc")

View File

@@ -6,6 +6,7 @@ import (
"errors" "errors"
"math" "math"
"net" "net"
"runtime"
"testing" "testing"
"time" "time"
@@ -221,6 +222,7 @@ func TestCoreDNSOverflow(t *testing.T) {
response, _, _, err := p.Connect(context.Background(), request, options) response, _, _, err := p.Connect(context.Background(), request, options)
if err != nil { if err != nil {
t.Errorf("Failed to connect to testdnsserver: %s", err) t.Errorf("Failed to connect to testdnsserver: %s", err)
return
} }
if response.Truncated != expectTruncated { if response.Truncated != expectTruncated {
@@ -228,15 +230,18 @@ func TestCoreDNSOverflow(t *testing.T) {
} }
} }
// Test PreferUDP, expect truncated response // Oversized UDP replies are truncated on Unix; Windows surfaces WSAEMSGSIZE instead.
testConnection("PreferUDP", Options{PreferUDP: true}, true) 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 // Test ForceTCP, expect no truncated response
testConnection("ForceTCP", Options{ForceTCP: true}, false) 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 // Test both TCP and UDP provided, expect no truncated response
testConnection("BothTCPAndUDP", Options{PreferUDP: true, ForceTCP: true}, false) testConnection("BothTCPAndUDP", Options{PreferUDP: true, ForceTCP: true}, false)
} }

View File

@@ -2,6 +2,7 @@ package tls
import ( import (
"crypto/tls" "crypto/tls"
"io"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "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) 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 { if cfg.TLSConfig.KeyLogWriter == nil {
t.Fatal("KeyLogWriter is not set") 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()
}) })
} }