From ae3d263e65fea7acf0255cb558bdc815f1164d28 Mon Sep 17 00:00:00 2001 From: Michael Marchetti Date: Mon, 16 Oct 2023 15:02:30 -0400 Subject: [PATCH] verify Python is runnable --- internal/environment/python.go | 23 ++++++++- internal/environment/python_test.go | 73 +++++++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/internal/environment/python.go b/internal/environment/python.go index d16e56e23..9f26fa428 100644 --- a/internal/environment/python.go +++ b/internal/environment/python.go @@ -55,6 +55,12 @@ func NewPythonInspector(projectDir util.Path, pythonPath util.Path, log logging. } } +func (i *defaultPythonInspector) validatePythonExecutable(pythonExecutable string) error { + args := []string{"--version"} + _, err := i.executor.runPythonCommand(pythonExecutable, args) + return err +} + func (i *defaultPythonInspector) getPythonExecutable(exec util.PathLooker) (string, error) { if i.pythonPath.Path() != "" { // User-provided python executable @@ -71,10 +77,23 @@ func (i *defaultPythonInspector) getPythonExecutable(exec util.PathLooker) (stri } else { // Use whatever is on PATH path, err := exec.LookPath("python3") + if err == nil { + // Ensure the Python is actually runnable. This is especially + // needed on Windows, where `python3` is (by default) + // an app execution alias. Also, installing Python from + // python.org does not disable the built-in app execution aliases. + err = i.validatePythonExecutable(path) + } if err != nil { - return exec.LookPath("python") + path, err = exec.LookPath("python") + if err == nil { + err = i.validatePythonExecutable(path) + } + } + if err != nil { + return "", err } - return path, err + return path, nil } } diff --git a/internal/environment/python_test.go b/internal/environment/python_test.go index 37bd8df6c..29aa6d0c1 100644 --- a/internal/environment/python_test.go +++ b/internal/environment/python_test.go @@ -108,16 +108,83 @@ func (s *PythonSuite) TestGetPythonVersionFromRealDefaultPython() { s.True(strings.HasPrefix(version, "3.")) } +type mockPythonExecutor struct { + mock.Mock +} + +var _ pythonExecutor = &mockPythonExecutor{} + +func (m *mockPythonExecutor) runPythonCommand(pythonExecutable string, args []string) ([]byte, error) { + mockArgs := m.Called(pythonExecutable, args) + out := mockArgs.Get(0) + if out != nil { + return out.([]byte), mockArgs.Error(1) + } else { + return nil, mockArgs.Error(1) + } +} + func (s *PythonSuite) TestGetPythonExecutableFallbackPython() { + // python3 does not exist + // python exists and is runnable log := logging.New() - inspector := NewPythonInspector(util.Path{}, util.Path{}, log) + executor := &mockPythonExecutor{} + executor.On("runPythonCommand", "/some/python", mock.Anything).Return(nil, nil) + inspector := &defaultPythonInspector{ + executor: executor, + log: log, + } exec := util.NewMockPathLooker() exec.On("LookPath", "python3").Return("", os.ErrNotExist) - exec.On("LookPath", "python").Return("/usr/bin/python", nil) + exec.On("LookPath", "python").Return("/some/python", nil) + executable, err := inspector.getPythonExecutable(exec) + s.NoError(err) + s.Equal("/some/python", executable) +} + +func (s *PythonSuite) TestGetPythonExecutablePython3NotRunnable() { + // python3 exists but is not runnable + // python exists and is runnable + log := logging.New() + executor := &mockPythonExecutor{} + testError := errors.New("exit status 9009") + executor.On("runPythonCommand", "/some/python3", mock.Anything).Return(nil, testError) + executor.On("runPythonCommand", "/some/python", mock.Anything).Return(nil, nil) + + inspector := &defaultPythonInspector{ + executor: executor, + log: log, + } + + exec := util.NewMockPathLooker() + exec.On("LookPath", "python3").Return("/some/python3", nil) + exec.On("LookPath", "python").Return("/some/python", nil) executable, err := inspector.getPythonExecutable(exec) s.NoError(err) - s.Equal("/usr/bin/python", executable) + s.Equal("/some/python", executable) +} + +func (s *PythonSuite) TestGetPythonExecutableNoRunnablePython() { + // python3 exists but is not runnable + // python exists but is not runnable + log := logging.New() + executor := &mockPythonExecutor{} + testError := errors.New("exit status 9009") + executor.On("runPythonCommand", "/some/python3", mock.Anything).Return(nil, testError) + executor.On("runPythonCommand", "/some/python", mock.Anything).Return(nil, testError) + + inspector := &defaultPythonInspector{ + executor: executor, + log: log, + } + + exec := util.NewMockPathLooker() + exec.On("LookPath", "python3").Return("/some/python3", nil) + exec.On("LookPath", "python").Return("/some/python", nil) + executable, err := inspector.getPythonExecutable(exec) + s.NotNil(err) + s.Equal("", executable) } func (s *PythonSuite) TestGetRequirementsFromFile() {