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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { }", 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},
}

View File

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

View File

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

View File

@@ -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()
})
}