diff --git a/pkg/projectfile/projectfile.go b/pkg/projectfile/projectfile.go index 59fbe68735..c89b2857e4 100644 --- a/pkg/projectfile/projectfile.go +++ b/pkg/projectfile/projectfile.go @@ -758,6 +758,10 @@ func (p *Project) SetBranch(branch string) error { } // GetProjectFilePath returns the path to the project activestate.yaml +// It considers projects in the following order: +// 1. Environment variable (e.g. `state shell` sets one) +// 2. Working directory (i.e. walk up directory tree looking for activestate.yaml) +// 3. Fall back on default project func GetProjectFilePath() (string, error) { defer profile.Measure("GetProjectFilePath", time.Now()) lookup := []func() (string, error){ diff --git a/pkg/projectfile/projectfile_test.go b/pkg/projectfile/projectfile_test.go index 6f2e497e07..b3b9725d32 100644 --- a/pkg/projectfile/projectfile_test.go +++ b/pkg/projectfile/projectfile_test.go @@ -12,6 +12,7 @@ import ( "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/environment" "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/language" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/osutils" @@ -207,59 +208,95 @@ func TestSave(t *testing.T) { os.Remove(tmpfile.Name()) } -// Call getProjectFilePath func TestGetProjectFilePath(t *testing.T) { Reset() - - root, err := environment.GetRootPath() - assert.NoError(t, err, "Should detect root path") - cwd, err := osutils.Getwd() - assert.NoError(t, err, "Should fetch cwd") - defer os.Chdir(cwd) // restore - os.Chdir(filepath.Join(root, "pkg", "projectfile", "testdata")) - - configPath, err := GetProjectFilePath() - require.Nil(t, err) - expectedPath := filepath.Join(root, "pkg", "projectfile", "testdata", constants.ConfigFileName) - assert.Equal(t, expectedPath, configPath, "Project path is properly detected") - os.Chdir(cwd) // restore - - defer os.Unsetenv(constants.ProjectEnvVarName) - - os.Setenv(constants.ProjectEnvVarName, "/some/path") - configPath, err = GetProjectFilePath() - errt := &ErrorNoProjectFromEnv{} - require.ErrorAs(t, err, &errt) - - expectedPath = filepath.Join(root, "pkg", "projectfile", "testdata", constants.ConfigFileName) - os.Setenv(constants.ProjectEnvVarName, expectedPath) - configPath, err = GetProjectFilePath() - require.Nil(t, err) - assert.Equal(t, expectedPath, configPath, "Project path is properly detected using the ProjectEnvVarName") - - os.Unsetenv(constants.ProjectEnvVarName) + currentDir, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(currentDir) + + rootDir, err := fileutils.ResolvePath(fileutils.TempDirUnsafe()) + assert.NoError(t, err) + defer os.RemoveAll(rootDir) + + // First, set up a new project with a subproject. + projectDir := filepath.Join(rootDir, "project") + require.NoError(t, fileutils.Mkdir(projectDir)) + projectYaml := filepath.Join(projectDir, constants.ConfigFileName) + require.NoError(t, fileutils.Touch(projectYaml)) + subprojectDir := filepath.Join(projectDir, "subproject") + require.NoError(t, fileutils.Mkdir(subprojectDir)) + subprojectYaml := filepath.Join(subprojectDir, constants.ConfigFileName) + require.NoError(t, fileutils.Mkdir(subprojectDir)) + require.NoError(t, fileutils.Touch(subprojectYaml)) + + // Then set up a separate, default project. + defaultDir := filepath.Join(rootDir, "default") + require.NoError(t, fileutils.Mkdir(defaultDir)) + defaultYaml := filepath.Join(defaultDir, constants.ConfigFileName) + require.NoError(t, fileutils.Touch(defaultYaml)) cfg, err := config.New() require.NoError(t, err) defer func() { require.NoError(t, cfg.Close()) }() - cfg.Set(constants.GlobalDefaultPrefname, "") // ensure it is unset - tmpDir, err := ioutil.TempDir("", "") - assert.NoError(t, err, "Should create temp dir") - defer os.RemoveAll(tmpDir) - os.Chdir(tmpDir) - _, err = GetProjectFilePath() - assert.Error(t, err, "GetProjectFilePath should fail") - cfg.Set(constants.GlobalDefaultPrefname, expectedPath) - configPath, err = GetProjectFilePath() - assert.NoError(t, err, "GetProjectFilePath should succeed") - assert.Equal(t, expectedPath, configPath, "Project path is properly detected using default path from config") - - // The activestate.yaml for an activated project should be used no matter what. - defer os.Unsetenv(constants.ActivatedStateEnvVarName) - os.Setenv(constants.ActivatedStateEnvVarName, filepath.Dir(expectedPath)) - configPath, err = GetProjectFilePath() - require.Nil(t, err) - assert.Equal(t, expectedPath, configPath, "Project path is properly detected using the ActivatedStateEnvVarName") - os.Unsetenv(constants.ActivatedStateEnvVarName) + cfg.Set(constants.GlobalDefaultPrefname, defaultDir) + + // Now set up an empty directory. + emptyDir := filepath.Join(rootDir, "empty") + require.NoError(t, fileutils.Mkdir(emptyDir)) + + // Now change to the project directory and assert GetProjectFilePath() returns it over the + // default project. + require.NoError(t, os.Chdir(projectDir)) + path, err := GetProjectFilePath() + assert.NoError(t, err) + assert.Equal(t, projectYaml, path) + + // `state shell` sets an environment variable, so run `state shell` in this project and then + // change to the subproject directory. Assert GetProjectFilePath() still returns the parent + // project. + require.NoError(t, os.Setenv(constants.ActivatedStateEnvVarName, projectDir)) + defer os.Unsetenv(constants.ProfileEnvVarName) + require.NoError(t, os.Chdir(subprojectDir)) + path, err = GetProjectFilePath() + assert.NoError(t, err) + assert.Equal(t, projectYaml, path) + + // If the project were to not exist, GetProjectFilePath() should return a typed error. + require.NoError(t, os.Setenv(constants.ActivatedStateEnvVarName, filepath.Join(rootDir, "does-not-exist"))) + path, err = GetProjectFilePath() + errNoProjectFromEnv := &ErrorNoProjectFromEnv{} + assert.ErrorAs(t, err, &errNoProjectFromEnv) + + // After exiting out of the shell, the environment variable is no longer set. Assert + // GetProjectFilePath() returns the subproject. + require.NoError(t, os.Unsetenv(constants.ActivatedStateEnvVarName)) + path, err = GetProjectFilePath() + assert.NoError(t, err) + assert.Equal(t, subprojectYaml, path) + + // If a project's subdirectory does not contain an activestate.yaml file, GetProjectFilePath() + // should walk up the tree until it finds one. + require.NoError(t, os.Remove(subprojectYaml)) + path, err = GetProjectFilePath() + assert.NoError(t, err) + assert.Equal(t, projectYaml, path) + + // Change to an empty directory and assert GetProjectFilePath() returns the default project. + require.NoError(t, os.Chdir(emptyDir)) + path, err = GetProjectFilePath() + assert.NoError(t, err) + assert.Equal(t, defaultYaml, path) + + // If the default project no longer exists, GetProjectFilePath() should return a typed error. + cfg.Set(constants.GlobalDefaultPrefname, filepath.Join(rootDir, "does-not-exist")) + path, err = GetProjectFilePath() + errNoDefaultProject := &ErrorNoDefaultProject{} + assert.ErrorAs(t, err, &errNoDefaultProject) + + // If none of the above, GetProjectFilePath() should return a typed error. + cfg.Set(constants.GlobalDefaultPrefname, "") + path, err = GetProjectFilePath() + errNoProject := &ErrorNoProject{} + assert.ErrorAs(t, err, &errNoProject) } // TestGet the config diff --git a/test/integration/shell_int_test.go b/test/integration/shell_int_test.go index 631ee47e4a..d954e9623b 100644 --- a/test/integration/shell_int_test.go +++ b/test/integration/shell_int_test.go @@ -365,6 +365,111 @@ func (suite *ShellIntegrationTestSuite) TestPs1() { cp.ExpectExitCode(0) } +func (suite *ShellIntegrationTestSuite) TestProjectOrder() { + suite.OnlyRunForTags(tagsuite.Critical, tagsuite.Shell) + ts := e2e.New(suite.T(), false) + defer ts.Close() + + // First, set up a new project with a subproject. + cp := ts.Spawn("checkout", "ActiveState-CLI/Perl-5.32", "project") + cp.Expect("Skipping runtime setup") + cp.Expect("Checked out project") + cp.ExpectExitCode(0) + projectDir := filepath.Join(ts.Dirs.Work, "project") + + cp = ts.SpawnWithOpts( + e2e.OptArgs("checkout", "ActiveState-CLI/Perl-5.32", "subproject"), + e2e.OptWD(projectDir), + ) + cp.Expect("Skipping runtime setup") + cp.Expect("Checked out project") + cp.ExpectExitCode(0) + subprojectDir := filepath.Join(projectDir, "subproject") + + // Then set up a separate project and make it the default. + cp = ts.Spawn("checkout", "ActiveState-CLI/Perl-5.32", "default") + cp.Expect("Skipping runtime setup") + cp.Expect("Checked out project") + cp.ExpectExitCode(0) + defaultDir := filepath.Join(ts.Dirs.Work, "default") + + cp = ts.SpawnWithOpts( + e2e.OptArgs("use"), + e2e.OptWD(defaultDir), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), + ) + cp.Expect("Setting Up Runtime", e2e.RuntimeSourcingTimeoutOpt) + cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt) + cp.Expect(defaultDir) + cp.ExpectExitCode(0) + + // Now set up an empty directory. + emptyDir := filepath.Join(ts.Dirs.Work, "empty") + suite.Require().NoError(fileutils.Mkdir(emptyDir)) + + // Now change to the project directory and assert that project is used instead of the default + // project. + cp = ts.SpawnWithOpts( + e2e.OptArgs("refresh"), + e2e.OptWD(projectDir), + ) + cp.Expect(projectDir) + cp.ExpectExitCode(0) + + // Run `state shell` in this project, change to the subproject directory, and assert the parent + // project is used instead of the subproject. + cp = ts.SpawnWithOpts( + e2e.OptArgs("shell"), + e2e.OptWD(projectDir), + e2e.OptAppendEnv(constants.DisableRuntime+"=false"), + ) + cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) + cp.Expect(projectDir) + cp.SendLine("cd subproject") + cp.SendLine("state refresh") + cp.Expect(projectDir) // not subprojectDir + cp.SendLine("exit") + cp.Expect("Deactivated") + cp.ExpectExit() // exit code varies depending on shell; just assert the shell exited + + // After exiting the shell, assert the subproject is used instead of the parent project. + cp = ts.SpawnWithOpts( + e2e.OptArgs("refresh"), + e2e.OptWD(subprojectDir), + ) + cp.Expect(subprojectDir) + cp.ExpectExitCode(0) + + // If a project subdirectory does not contain an activestate.yaml file, assert the project that + // owns the subdirectory will be used. + nestedDir := filepath.Join(subprojectDir, "nested") + suite.Require().NoError(fileutils.Mkdir(nestedDir)) + cp = ts.SpawnWithOpts( + e2e.OptArgs("refresh"), + e2e.OptWD(nestedDir), + ) + cp.Expect(subprojectDir) + cp.ExpectExitCode(0) + + // Change to an empty directory and assert the default project is used. + cp = ts.SpawnWithOpts( + e2e.OptArgs("refresh"), + e2e.OptWD(emptyDir), + ) + cp.Expect(defaultDir) + cp.ExpectExitCode(0) + + // If none of the above, assert an error. + cp = ts.Spawn("use", "reset", "-n") + cp.ExpectExitCode(0) + + cp = ts.SpawnWithOpts( + e2e.OptArgs("refresh"), + e2e.OptWD(emptyDir), + ) + cp.ExpectNotExitCode(0) +} + func TestShellIntegrationTestSuite(t *testing.T) { suite.Run(t, new(ShellIntegrationTestSuite)) }