Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,7 @@ jobs:
- name: Build dbc
run: go build ./cmd/dbc
- name: Run Tests
# the file with `test_registry` also requires windows, so we can safely add this tag
# without it running on non-windows OSes
run: go test -v -tags test_registry ./...
run: go test -v ./...

# User and System level tests
- name: Run Tests (User)
Expand All @@ -79,5 +77,8 @@ jobs:
- name: Run Tests (System, Windows)
if: runner.os == 'Windows'
run: go test -v ./...
# Disposable runner only: exercises real HKLM/Program Files (and the
# registry suite's real HKCU) while deleting ADBC registrations/files.
env:
DBC_TEST_LEVEL_SYSTEM: 1
DBC_TEST_WINDOWS_REAL_LOCATIONS: 1
14 changes: 8 additions & 6 deletions cmd/dbc/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build windows && test_registry
//go:build windows

package main

Expand All @@ -27,13 +27,15 @@ import (

tea "charm.land/bubbletea/v2"
"github.com/columnar-tech/dbc/config"
"github.com/columnar-tech/dbc/internal/winregroot"
"github.com/stretchr/testify/suite"
"golang.org/x/sys/windows/registry"
)

// This test suite is only run when the "test_registry" build tag is set.
// Only run these tests if you're able to modify the windows registry and won't be broken
// if the ADBC registry keys are cleared/modified/etc.
// This suite runs in the default Windows test set. The Windows test main uses
// process-private fixtures by default; DBC_TEST_WINDOWS_REAL_LOCATIONS=1 bypasses
// them, in which case these tests modify and remove the current user's ADBC
// registry keys and driver files.
type RegistryTestSuite struct {
suite.Suite

Expand Down Expand Up @@ -62,7 +64,7 @@ func (s *RegistryTestSuite) run(m tea.Model) string {

func (s *RegistryTestSuite) clearRegistry() {
// Clear out any existing ADBC registry keys to ensure a clean slate.
k, err := registry.OpenKey(registry.CURRENT_USER, "SOFTWARE\\ADBC\\Drivers", registry.ALL_ACCESS)
k, err := registry.OpenKey(winregroot.UserRoot(), "SOFTWARE\\ADBC\\Drivers", registry.ALL_ACCESS)
if errors.Is(err, registry.ErrNotExist) {
return
}
Expand Down Expand Up @@ -100,7 +102,7 @@ func (s *RegistryTestSuite) TestInstallDriver() {
out := s.run(m)
s.Equal("\nInstalled test-driver-1 1.1.0 to "+s.cfgUserPath, out)

k, err := registry.OpenKey(registry.CURRENT_USER, "SOFTWARE\\ADBC\\Drivers\\test-driver-1", registry.READ)
k, err := registry.OpenKey(winregroot.UserRoot(), "SOFTWARE\\ADBC\\Drivers\\test-driver-1", registry.READ)
s.Require().NoError(err)
defer k.Close()

Expand Down
7 changes: 4 additions & 3 deletions cmd/dbc/subcommand_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,21 @@ import (
"os"

"github.com/columnar-tech/dbc/config"
"github.com/columnar-tech/dbc/internal/winregroot"
"golang.org/x/sys/windows/registry"
)

func (suite *SubcommandTestSuite) TearDownTest() {
// Clean up the registry and filesystem after each test
// Clean up the selected registry roots and filesystem roots after each test.
_, user := os.LookupEnv("DBC_TEST_LEVEL_USER")
_, system := os.LookupEnv("DBC_TEST_LEVEL_SYSTEM")

if user {
suite.Require().NoError(deleteRegistryKeyRecursive(registry.CURRENT_USER, "SOFTWARE\\ADBC\\Drivers"))
suite.Require().NoError(deleteRegistryKeyRecursive(winregroot.UserRoot(), "SOFTWARE\\ADBC\\Drivers"))
suite.Require().NoError(os.RemoveAll(config.Get()[config.ConfigUser].Location))
}
if system {
suite.Require().NoError(deleteRegistryKeyRecursive(registry.LOCAL_MACHINE, "SOFTWARE\\ADBC\\Drivers"))
suite.Require().NoError(deleteRegistryKeyRecursive(winregroot.SystemRoot(), "SOFTWARE\\ADBC\\Drivers"))
suite.Require().NoError(os.RemoveAll(config.Get()[config.ConfigSystem].Location))
}
}
Expand Down
286 changes: 286 additions & 0 deletions cmd/dbc/test_isolation_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
// Copyright 2026 Columnar Technologies Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build windows

package main

import (
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"

"github.com/columnar-tech/dbc/config"
"github.com/columnar-tech/dbc/internal/winregroot"
"github.com/columnar-tech/dbc/internal/wintest"
"golang.org/x/sys/windows/registry"
)

const (
windowsTestIsolationChildEnv = "DBC_WINDOWS_TEST_ISOLATION_CHILD"
windowsTestRealLocationsEnv = wintest.RealLocationsEnvironment
windowsTestIsolationUserEnv = "DBC_WINDOWS_TEST_ISOLATION_PARENT_USER"
windowsTestIsolationSysEnv = "DBC_WINDOWS_TEST_ISOLATION_PARENT_SYSTEM"
windowsTestIsolationDriverID = "dbc-process-isolation-sentinel"
windowsTestIsolationRelative = ".dbc-package-g-process-isolation/driver.dll"
)

func TestMain(m *testing.M) {
if os.Getenv(windowsTestRealLocationsEnv) == "1" {
os.Exit(m.Run())
}

isolation, err := wintest.Setup()
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "set up isolated Windows CLI tests: %v\n", err)
os.Exit(1)
}
if err := verifyWindowsCLIConfigRoots(isolation); err != nil {
cleanupErr := isolation.Cleanup()
_, _ = fmt.Fprintf(os.Stderr, "verify isolated Windows CLI roots: %v\n", errors.Join(err, cleanupErr))
os.Exit(1)
}

code := m.Run()
if err := isolation.Cleanup(); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "clean up isolated Windows CLI tests: %v\n", err)
code = 1
}
os.Exit(code)
}

func verifyWindowsCLIConfigRoots(isolation *wintest.Isolation) error {
if got, want := filepath.Clean(config.ConfigUser.ConfigLocation()), filepath.Clean(isolation.UserRoot()); !strings.EqualFold(got, want) {
return fmt.Errorf("ConfigUser root = %q, want private root %q", got, want)
}
if got, want := filepath.Clean(config.ConfigSystem.ConfigLocation()), filepath.Clean(isolation.SystemRoot()); !strings.EqualFold(got, want) {
return fmt.Errorf("ConfigSystem root = %q, want private root %q", got, want)
}
return nil
}

func TestWindowsTestIsolationAcrossProcesses(t *testing.T) {
if os.Getenv(windowsTestRealLocationsEnv) == "1" {
t.Skip("destructive process-isolation test requires private registry and filesystem roots")
}
if os.Getenv(windowsTestIsolationChildEnv) == "1" {
runWindowsTestIsolationChild(t)
return
}

parentUserRoot := config.ConfigUser.ConfigLocation()
parentSystemRoot := config.ConfigSystem.ConfigLocation()
parentUserPath := windowsIsolationPayload(parentUserRoot)
parentSystemPath := windowsIsolationPayload(parentSystemRoot)
parentUserBytes := []byte("parent user registry and payload sentinel")
parentSystemBytes := []byte("parent system registry and payload sentinel")
writeIsolationPayload(t, parentUserPath, parentUserBytes)
writeIsolationPayload(t, parentSystemPath, parentSystemBytes)
writeIsolationRegistration(t, winregroot.UserRoot(), parentUserPath)
writeIsolationRegistration(t, winregroot.SystemRoot(), parentSystemPath)
t.Setenv("ADBC_DRIVER_PATH", parentUserRoot)
t.Setenv("VIRTUAL_ENV", parentSystemRoot)
t.Setenv("CONDA_PREFIX", parentSystemRoot)
t.Cleanup(func() {
var cleanupErr error
cleanupErr = errors.Join(cleanupErr, deleteRegistryKeyRecursive(winregroot.UserRoot(), "SOFTWARE\\ADBC\\Drivers\\"+windowsTestIsolationDriverID))
cleanupErr = errors.Join(cleanupErr, deleteRegistryKeyRecursive(winregroot.SystemRoot(), "SOFTWARE\\ADBC\\Drivers\\"+windowsTestIsolationDriverID))
cleanupErr = errors.Join(cleanupErr, os.RemoveAll(filepath.Dir(parentUserPath)))
cleanupErr = errors.Join(cleanupErr, os.RemoveAll(filepath.Dir(parentSystemPath)))
if cleanupErr != nil {
t.Errorf("remove parent isolation sentinels: %v", cleanupErr)
}
})

child := exec.Command(os.Args[0], "-test.run=^TestWindowsTestIsolationAcrossProcesses$")
child.Env = append(os.Environ(),
windowsTestIsolationChildEnv+"=1",
windowsTestIsolationUserEnv+"="+parentUserRoot,
windowsTestIsolationSysEnv+"="+parentSystemRoot,
)
output, err := child.CombinedOutput()
if err != nil {
t.Fatalf("isolated child process failed: %v\n%s", err, output)
}

assertIsolationPayload(t, parentUserPath, parentUserBytes)
assertIsolationPayload(t, parentSystemPath, parentSystemBytes)
assertIsolationRegistration(t, winregroot.UserRoot(), parentUserPath)
assertIsolationRegistration(t, winregroot.SystemRoot(), parentSystemPath)
}

func runWindowsTestIsolationChild(t *testing.T) {
for _, name := range []string{"ADBC_DRIVER_PATH", "VIRTUAL_ENV", "CONDA_PREFIX"} {
if value, present := os.LookupEnv(name); present {
t.Fatalf("child inherited unsafe %s value %q", name, value)
}
}
assertLocalHTTPRoundTrip(t)

parentUserRoot := os.Getenv(windowsTestIsolationUserEnv)
parentSystemRoot := os.Getenv(windowsTestIsolationSysEnv)
childUserRoot := config.ConfigUser.ConfigLocation()
childSystemRoot := config.ConfigSystem.ConfigLocation()
if sameWindowsPath(childUserRoot, parentUserRoot) || sameWindowsPath(childSystemRoot, parentSystemRoot) {
t.Fatalf("child fixture roots are not private: User=%q parent=%q; System=%q parent=%q", childUserRoot, parentUserRoot, childSystemRoot, parentSystemRoot)
}

childUserPath := windowsIsolationPayload(childUserRoot)
childSystemPath := windowsIsolationPayload(childSystemRoot)
writeIsolationPayload(t, childUserPath, []byte("child user payload"))
writeIsolationPayload(t, childSystemPath, []byte("child system payload"))
writeIsolationRegistration(t, winregroot.UserRoot(), childUserPath)
writeIsolationRegistration(t, winregroot.SystemRoot(), childSystemPath)

if err := deleteRegistryKeyRecursive(winregroot.UserRoot(), "SOFTWARE\\ADBC\\Drivers"); err != nil {
t.Fatal(err)
}
if err := os.RemoveAll(childUserRoot); err != nil {
t.Fatalf("remove child User driver root: %v", err)
}
assertIsolationRegistrationRemoved(t, winregroot.UserRoot())
assertIsolationPathRemoved(t, childUserRoot)
assertIsolationPayload(t, childSystemPath, []byte("child system payload"))
assertIsolationRegistration(t, winregroot.SystemRoot(), childSystemPath)

if err := deleteRegistryKeyRecursive(winregroot.SystemRoot(), "SOFTWARE\\ADBC\\Drivers"); err != nil {
t.Fatal(err)
}
if err := os.RemoveAll(childSystemRoot); err != nil {
t.Fatalf("remove child System driver root: %v", err)
}
assertIsolationRegistrationRemoved(t, winregroot.SystemRoot())
assertIsolationPathRemoved(t, childSystemRoot)
}

func assertLocalHTTPRoundTrip(t *testing.T) {
t.Helper()
// Exercise loopback networking in a fresh process after changing only dbc's
// registry roots. This guards against regressions like the earlier global
// registry override, which was associated with Windows network-init failures.
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = io.WriteString(w, "local loopback works")
}))
defer server.Close()

client := server.Client()
client.Timeout = 5 * time.Second
response, err := client.Get(server.URL)
if err != nil {
t.Fatalf("GET local httptest server: %v", err)
}
body, readErr := io.ReadAll(response.Body)
closeErr := response.Body.Close()
if err := errors.Join(readErr, closeErr); err != nil {
t.Fatalf("read local httptest response: %v", err)
}
if response.StatusCode != http.StatusOK || string(body) != "local loopback works" {
t.Fatalf("local HTTP response = %d %q", response.StatusCode, body)
}
}

func windowsIsolationPayload(root string) string {
return filepath.Join(root, filepath.FromSlash(windowsTestIsolationRelative))
}

func writeIsolationPayload(t *testing.T, path string, contents []byte) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, contents, 0o600); err != nil {
t.Fatal(err)
}
}

func writeIsolationRegistration(t *testing.T, root registry.Key, driver string) {
t.Helper()
key, _, err := registry.CreateKey(root, "SOFTWARE\\ADBC\\Drivers\\"+windowsTestIsolationDriverID, registry.ALL_ACCESS)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := key.Close(); err != nil {
t.Errorf("close isolation registration: %v", err)
}
}()
for name, value := range map[string]string{
"driver": driver,
"name": "Process Isolation Sentinel",
"version": "1.0.0",
} {
if err := key.SetStringValue(name, value); err != nil {
t.Fatal(err)
}
}
}

func assertIsolationRegistration(t *testing.T, root registry.Key, want string) {
t.Helper()
key, err := registry.OpenKey(root, "SOFTWARE\\ADBC\\Drivers\\"+windowsTestIsolationDriverID, registry.READ)
if err != nil {
t.Fatal(err)
}
got, _, readErr := key.GetStringValue("driver")
closeErr := key.Close()
if err := errors.Join(readErr, closeErr); err != nil {
t.Fatal(err)
}
if got != want {
t.Fatalf("isolation registration driver = %q, want %q", got, want)
}
}

func assertIsolationRegistrationRemoved(t *testing.T, root registry.Key) {
t.Helper()
key, err := registry.OpenKey(root, "SOFTWARE\\ADBC\\Drivers\\"+windowsTestIsolationDriverID, registry.READ)
if errors.Is(err, registry.ErrNotExist) {
return
}
if err != nil {
t.Fatalf("check removed isolation registration: %v", err)
}
closeErr := key.Close()
t.Fatalf("isolation registration remains after cleanup (close error: %v)", closeErr)
}

func assertIsolationPayload(t *testing.T, path string, want []byte) {
t.Helper()
got, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read isolation payload %q: %v", path, err)
}
if string(got) != string(want) {
t.Fatalf("isolation payload %q = %q, want %q", path, got, want)
}
}

func assertIsolationPathRemoved(t *testing.T, path string) {
t.Helper()
if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("isolation path %q remains after cleanup or could not be checked: %v", path, err)
}
}

func sameWindowsPath(left, right string) bool {
return strings.EqualFold(filepath.Clean(left), filepath.Clean(right))
}
Loading
Loading