diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 0d602e3..d4bfba9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -59,3 +59,18 @@ jobs: # 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 ./... + - name: Run User Integration Tests (Unix) + if: runner.os != 'Windows' + env: + DBC_RUN_INTEGRATION_TESTS: 1 + run: go test -v -tags test_integration,test_user -run TestIntegration ./cmd/dbc + - name: Run System Integration Tests (Unix) + if: runner.os != 'Windows' + env: + DBC_RUN_INTEGRATION_TESTS: 1 + run: sudo go test -v -tags test_integration,test_system -run TestIntegration ./cmd/dbc + - name: Run Integration Tests (Windows) + if: runner.os == 'Windows' + env: + DBC_RUN_INTEGRATION_TESTS: 1 + run: go test -v -tags test_integration -run TestIntegration ./cmd/dbc diff --git a/cmd/dbc/integation_user_test.go b/cmd/dbc/integation_user_test.go new file mode 100644 index 0000000..b01f24c --- /dev/null +++ b/cmd/dbc/integation_user_test.go @@ -0,0 +1,35 @@ +// Copyright 2025 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 test_integration && test_user + +package main + +import ( + "path/filepath" + + "github.com/columnar-tech/dbc/config" +) + +func (suite *IntegrationTestSuite) TestInstallUser() { + m := InstallCmd{Driver: "test-driver-1", Level: config.ConfigUser}. + GetModelCustom(baseModel{getDriverRegistry: getTestDriverRegistry, downloadPkg: downloadTestPkg}) + out := suite.run(m) + + loc := config.GetLocation(config.ConfigUser) + suite.Equal("\nInstalled test-driver-1 1.1.0 to "+loc+"\n", out) + suite.driverIsInstalled(config.ConfigUser, "test-driver-1") + suite.FileExists(filepath.Join(loc, "test-driver-1.1", "test-driver-1-not-valid.so")) + suite.FileExists(filepath.Join(loc, "test-driver-1.1", "test-driver-1-not-valid.so.sig")) +} diff --git a/cmd/dbc/integration_system_test.go b/cmd/dbc/integration_system_test.go new file mode 100644 index 0000000..f7aa87e --- /dev/null +++ b/cmd/dbc/integration_system_test.go @@ -0,0 +1,35 @@ +// Copyright 2025 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 test_integration && test_system + +package main + +import ( + "path/filepath" + + "github.com/columnar-tech/dbc/config" +) + +func (suite *IntegrationTestSuite) TestInstallSystem() { + m := InstallCmd{Driver: "test-driver-1", Level: config.ConfigSystem}. + GetModelCustom(baseModel{getDriverRegistry: getTestDriverRegistry, downloadPkg: downloadTestPkg}) + out := suite.run(m) + + loc := config.GetLocation(config.ConfigSystem) + suite.Equal("\nInstalled test-driver-1 1.1.0 to "+loc+"\n", out) + suite.driverIsInstalled(config.ConfigSystem, "test-driver-1") + suite.FileExists(filepath.Join(loc, "test-driver-1.1", "test-driver-1-not-valid.so")) + suite.FileExists(filepath.Join(loc, "test-driver-1.1", "test-driver-1-not-valid.so.sig")) +} diff --git a/cmd/dbc/integration_test.go b/cmd/dbc/integration_test.go new file mode 100644 index 0000000..abe67ef --- /dev/null +++ b/cmd/dbc/integration_test.go @@ -0,0 +1,66 @@ +// Copyright 2025 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 test_integration + +package main + +import ( + "bytes" + "context" + "os" + "testing" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/stretchr/testify/suite" +) + +type IntegrationTestSuite struct { + suite.Suite +} + +func (suite *IntegrationTestSuite) SetupSuite() { + // Integration tests require both the build tag AND environment variable + if os.Getenv("DBC_RUN_INTEGRATION_TESTS") == "" { + suite.T().Skip("Set DBC_RUN_INTEGRATION_TESTS=1 to run integration tests") + } +} + +func (s *IntegrationTestSuite) run(m tea.Model) string { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var out bytes.Buffer + p := tea.NewProgram(m, tea.WithInput(nil), tea.WithOutput(&out), + tea.WithContext(ctx), tea.WithoutRenderer()) + + var err error + m, err = p.Run() + s.Require().NoError(err) + s.Equal(0, m.(HasStatus).Status(), "exited with a non-zero status") + + var extra string + if fo, ok := m.(HasFinalOutput); ok { + extra = fo.FinalOutput() + } + return out.String() + extra +} + +// This test suite only runs when the "test_integration" build tag is set. These +// tests are intended to only be run in VMs or on CI because they may modify +// user and system files and the Windows registry. +func TestIntegration(t *testing.T) { + suite.Run(t, new(IntegrationTestSuite)) +} diff --git a/cmd/dbc/integration_test_unix.go b/cmd/dbc/integration_test_unix.go new file mode 100644 index 0000000..4efc09a --- /dev/null +++ b/cmd/dbc/integration_test_unix.go @@ -0,0 +1,42 @@ +//go:build test_integration && !windows + +// Copyright 2025 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. + +package main + +import ( + "os" + + "github.com/columnar-tech/dbc/config" +) + +func (suite *IntegrationTestSuite) TearDownTest() { + os.RemoveAll(config.GetLocation(config.ConfigUser)) + os.RemoveAll(config.GetLocation(config.ConfigSystem)) +} + +func (suite *IntegrationTestSuite) driverIsInstalled(level config.ConfigLevel, driverID string) { + cfg := config.Config{ + Level: level, + Location: config.GetLocation(level), + } + + driverInfo, err := config.GetDriver(cfg, driverID) + suite.Require().NoError(err, "should be able to load driver from manifest") + + // Get and verify driver path exists + driverPath := driverInfo.Driver.Shared.Get(config.PlatformTuple()) + suite.FileExists(driverPath) +} diff --git a/cmd/dbc/integration_test_windows.go b/cmd/dbc/integration_test_windows.go new file mode 100644 index 0000000..aa23ab0 --- /dev/null +++ b/cmd/dbc/integration_test_windows.go @@ -0,0 +1,58 @@ +// Copyright 2025 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 test_integration && windows + +package main + +import ( + "os" + + "github.com/columnar-tech/dbc/config" + "golang.org/x/sys/windows/registry" +) + +func (suite *IntegrationTestSuite) TearDownTest() { + // Clean up the registry and filesystem between each test + // + // `regKeyADBC` isn't exported, we redefine it here + regKeyADBC := "SOFTWARE\\ADBC\\Drivers" + // We ignore registry errors. The `registry` package doesn't have a method to + // explicitly check key existence we could either check the error or just try + // and ignore + registry.DeleteKey(registry.CURRENT_USER, regKeyADBC) + registry.DeleteKey(registry.LOCAL_MACHINE, regKeyADBC) + + // Clean up filesystem + os.RemoveAll(config.GetLocation(config.ConfigUser)) + os.RemoveAll(config.GetLocation(config.ConfigSystem)) +} + +func (suite *IntegrationTestSuite) driverIsInstalled(level config.ConfigLevel, driverID string) { + var rootKey registry.Key + if level == config.ConfigUser { + rootKey = registry.CURRENT_USER + } else { + rootKey = registry.LOCAL_MACHINE + } + + k, err := registry.OpenKey(rootKey, `SOFTWARE\ADBC\Drivers\`+driverID, registry.QUERY_VALUE) + suite.Require().NoError(err, "registry key should exist") + defer k.Close() + + // Get and verify driver path exists + driverPath, _, err := k.GetStringValue("driver") + suite.Require().NoError(err) + suite.FileExists(driverPath) +} diff --git a/config/config.go b/config/config.go index 992143c..5094e67 100644 --- a/config/config.go +++ b/config/config.go @@ -97,6 +97,11 @@ func (c ConfigLevel) String() string { } } +// GetLocation returns the filesystem path for the given config level. +func GetLocation(level ConfigLevel) string { + return level.configLocation() +} + func (c *ConfigLevel) UnmarshalText(b []byte) error { switch strings.ToLower(strings.TrimSpace(string(b))) { case "system":