Skip to content

Commit 91a07bf

Browse files
committed
refactor(python): use Os interface in ActivateVenvIfPresent for testability
Add Unsetenv to the Os interface/implementation/mock and refactor ActivateVenvIfPresent to accept types.Os instead of calling the os package directly. This allows the test to use mocks instead of mutating the real process environment.
1 parent adbda1b commit 91a07bf

7 files changed

Lines changed: 36 additions & 23 deletions

File tree

internal/runtime/python/python.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"context"
2020
_ "embed"
2121
"fmt"
22-
"os"
2322
"path/filepath"
2423
"regexp"
2524
"runtime"
@@ -95,21 +94,23 @@ func getVenvBinDir(venvPath string) string {
9594
// the same approach. Sourcing the shell-specific activate script (activate,
9695
// activate.fish, Activate.ps1) would be higher maintenance because it varies
9796
// by shell.
98-
func ActivateVenvIfPresent(fs afero.Fs, projectDir string) (bool, error) {
97+
func ActivateVenvIfPresent(fs afero.Fs, osClient types.Os, projectDir string) (bool, error) {
9998
venvPath := getVenvPath(projectDir)
10099
if !venvExists(fs, venvPath) {
101100
return false, nil
102101
}
103102

104103
binDir := getVenvBinDir(venvPath)
105104

106-
if err := os.Setenv("VIRTUAL_ENV", venvPath); err != nil {
105+
if err := osClient.Setenv("VIRTUAL_ENV", venvPath); err != nil {
107106
return false, err
108107
}
109-
if err := os.Setenv("PATH", binDir+string(filepath.ListSeparator)+os.Getenv("PATH")); err != nil {
108+
if err := osClient.Setenv("PATH", binDir+string(filepath.ListSeparator)+osClient.Getenv("PATH")); err != nil {
109+
return false, err
110+
}
111+
if err := osClient.Unsetenv("PYTHONHOME"); err != nil {
110112
return false, err
111113
}
112-
os.Unsetenv("PYTHONHOME")
113114

114115
return true, nil
115116
}

internal/runtime/python/python_test.go

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package python
1717
import (
1818
"encoding/json"
1919
"fmt"
20-
"os"
2120
"path/filepath"
2221
"runtime"
2322
"testing"
@@ -342,14 +341,13 @@ func Test_ActivateVenvIfPresent(t *testing.T) {
342341
for name, tc := range tests {
343342
t.Run(name, func(t *testing.T) {
344343
fs := slackdeps.NewFsMock()
344+
osMock := slackdeps.NewOsMock()
345345
projectDir := "/path/to/project"
346346
venvPath := filepath.Join(projectDir, ".venv")
347347

348-
// Save and restore env vars
349-
t.Setenv("VIRTUAL_ENV", "")
350-
t.Setenv("PYTHONHOME", "original-pythonhome")
351-
originalPath := os.Getenv("PATH")
352-
t.Setenv("PATH", originalPath)
348+
originalPath := "/usr/bin:/bin"
349+
osMock.On("Getenv", "PATH").Return(originalPath)
350+
osMock.AddDefaultMocks()
353351

354352
if tc.createVenv {
355353
// Create the pip executable so venvExists returns true
@@ -360,21 +358,18 @@ func Test_ActivateVenvIfPresent(t *testing.T) {
360358
require.NoError(t, err)
361359
}
362360

363-
activated, err := ActivateVenvIfPresent(fs, projectDir)
361+
activated, err := ActivateVenvIfPresent(fs, osMock, projectDir)
364362
require.NoError(t, err)
365363
require.Equal(t, tc.expectedActivated, activated)
366364

367365
if tc.expectedActivated {
368-
require.Equal(t, venvPath, os.Getenv("VIRTUAL_ENV"))
369366
expectedBinDir := getVenvBinDir(venvPath)
370-
require.Contains(t, os.Getenv("PATH"), expectedBinDir+string(filepath.ListSeparator))
371-
// PYTHONHOME should be unset (empty)
372-
_, pythonHomeSet := os.LookupEnv("PYTHONHOME")
373-
require.False(t, pythonHomeSet)
367+
osMock.AssertCalled(t, "Setenv", "VIRTUAL_ENV", venvPath)
368+
osMock.AssertCalled(t, "Setenv", "PATH", expectedBinDir+string(filepath.ListSeparator)+originalPath)
369+
osMock.AssertCalled(t, "Unsetenv", "PYTHONHOME")
374370
} else {
375-
require.Equal(t, "", os.Getenv("VIRTUAL_ENV"))
376-
require.Equal(t, originalPath, os.Getenv("PATH"))
377-
require.Equal(t, "original-pythonhome", os.Getenv("PYTHONHOME"))
371+
osMock.AssertNotCalled(t, "Setenv", mock.Anything, mock.Anything)
372+
osMock.AssertNotCalled(t, "Unsetenv", mock.Anything)
378373
}
379374
})
380375
}

internal/runtime/runtime.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ func New(runtimeName string) (Runtime, error) {
7070
// exists in the given project directory. This sets VIRTUAL_ENV, prepends the
7171
// venv bin directory to PATH, and unsets PYTHONHOME on the current process so
7272
// that child processes (hook scripts) inherit the activated venv.
73-
func ActivatePythonVenvIfPresent(fs afero.Fs, projectDir string) (bool, error) {
74-
return python.ActivateVenvIfPresent(fs, projectDir)
73+
func ActivatePythonVenvIfPresent(fs afero.Fs, os types.Os, projectDir string) (bool, error) {
74+
return python.ActivateVenvIfPresent(fs, os, projectDir)
7575
}
7676

7777
// NewDetectProject returns a new Runtime based on the project type of dirPath

internal/shared/clients.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ func (c *ClientFactory) InitSDKConfig(ctx context.Context, dirPath string) error
226226
}
227227
// Activate Python virtual environment if present, so hook scripts
228228
// can resolve the venv's Python and installed packages.
229-
if activated, err := runtime.ActivatePythonVenvIfPresent(c.Fs, dirPath); err != nil {
229+
if activated, err := runtime.ActivatePythonVenvIfPresent(c.Fs, c.Os, dirPath); err != nil {
230230
c.IO.PrintDebug(ctx, "failed to activate Python virtual environment: %s", err)
231231
} else if activated {
232232
c.IO.PrintDebug(ctx, "Activated Python virtual environment .venv")

internal/shared/types/slackdeps.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ type Os interface {
3838
// Setenv defaults to `os.Setenv` and can be mocked to test
3939
Setenv(key string, value string) error
4040

41+
// Unsetenv defaults to `os.Unsetenv` and can be mocked to test
42+
Unsetenv(key string) error
43+
4144
// Getwd defaults to `os.Getwd` and can be mocked to test
4245
Getwd() (dir string, err error)
4346

internal/slackdeps/os.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ func (c *Os) Setenv(key string, value string) error {
5353
return os.Setenv(key, value)
5454
}
5555

56+
// Unsetenv defaults to `os.Unsetenv` and can be mocked to test
57+
func (c *Os) Unsetenv(key string) error {
58+
return os.Unsetenv(key)
59+
}
60+
5661
// Getwd defaults to `os.Getwd` and can be mocked to test
5762
func (c *Os) Getwd() (dir string, err error) {
5863
return os.Getwd()

internal/slackdeps/os_mock.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func (m *OsMock) AddDefaultMocks() {
4646
m.On("LookPath", mock.Anything).Return("", nil)
4747
m.On("LookupEnv", mock.Anything).Return("", false)
4848
m.On("Setenv", mock.Anything, mock.Anything).Return(nil)
49+
m.On("Unsetenv", mock.Anything).Return(nil)
4950
m.On("Getwd").Return(MockWorkingDirectory, nil)
5051
m.On("UserHomeDir").Return(MockHomeDirectory, nil)
5152
m.On("GetExecutionDir").Return(MockHomeDirectory, nil)
@@ -83,6 +84,14 @@ func (m *OsMock) Setenv(key string, value string) error {
8384
return args.Error(0)
8485
}
8586

87+
// Unsetenv mocks the unsetting of an environment variable
88+
func (m *OsMock) Unsetenv(key string) error {
89+
m.On("Getenv", key).Return("")
90+
m.On("LookupEnv", key).Return("", false)
91+
args := m.Called(key)
92+
return args.Error(0)
93+
}
94+
8695
// Getwd mocks returning the working directory.
8796
func (m *OsMock) Getwd() (_dir string, _err error) {
8897
args := m.Called()

0 commit comments

Comments
 (0)