github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/test/integration/shell_int_test.go (about)

     1  package integration
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"runtime"
     8  	"strings"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/ActiveState/termtest"
    13  
    14  	"github.com/ActiveState/cli/internal/config"
    15  	"github.com/ActiveState/cli/internal/constants"
    16  	"github.com/ActiveState/cli/internal/fileutils"
    17  	"github.com/ActiveState/cli/internal/subshell"
    18  	"github.com/ActiveState/cli/internal/subshell/bash"
    19  	"github.com/ActiveState/cli/internal/subshell/sscommon"
    20  	"github.com/ActiveState/cli/internal/subshell/zsh"
    21  	"github.com/ActiveState/cli/internal/testhelpers/e2e"
    22  	"github.com/ActiveState/cli/internal/testhelpers/suite"
    23  	"github.com/ActiveState/cli/internal/testhelpers/tagsuite"
    24  )
    25  
    26  type ShellIntegrationTestSuite struct {
    27  	tagsuite.Suite
    28  }
    29  
    30  func (suite *ShellIntegrationTestSuite) TestShell() {
    31  	suite.OnlyRunForTags(tagsuite.Shell)
    32  
    33  	ts := e2e.New(suite.T(), false)
    34  	defer ts.Close()
    35  
    36  	cp := ts.SpawnWithOpts(
    37  		e2e.OptArgs("checkout", "ActiveState-CLI/small-python"),
    38  		e2e.OptAppendEnv(constants.DisableRuntime+"=false"),
    39  	)
    40  	cp.Expect("Checked out project", e2e.RuntimeSourcingTimeoutOpt)
    41  	cp.ExpectExitCode(0)
    42  
    43  	args := []string{"small-python", "ActiveState-CLI/small-python"}
    44  	for _, arg := range args {
    45  		cp := ts.SpawnWithOpts(
    46  			e2e.OptArgs("shell", arg),
    47  			e2e.OptAppendEnv(constants.DisableRuntime+"=false"),
    48  		)
    49  		cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt)
    50  		cp.ExpectInput()
    51  
    52  		cp.SendLine("python3 --version")
    53  		cp.Expect("Python 3")
    54  		cp.SendLine("exit")
    55  		cp.Expect("Deactivated")
    56  		cp.ExpectExitCode(0)
    57  	}
    58  
    59  	// Both Windows and MacOS can run into path comparison issues with symlinks and long paths.
    60  	projectName := "small-python"
    61  	if runtime.GOOS == "linux" {
    62  		projectDir := filepath.Join(ts.Dirs.Work, projectName)
    63  		// projectDir, err := fileutils.SymlinkTarget(projectDir)
    64  		// suite.Require().NoError(err)
    65  		err := os.RemoveAll(projectDir)
    66  		suite.Require().NoError(err)
    67  
    68  		cp = ts.Spawn("shell", projectName)
    69  		cp.Expect(fmt.Sprintf("Could not load project %s from path: %s", projectName, projectDir))
    70  	}
    71  
    72  	// Check for project not checked out.
    73  	args = []string{"Python-3.9", "ActiveState-CLI/Python-3.9"}
    74  	for _, arg := range args {
    75  		cp := ts.SpawnWithOpts(
    76  			e2e.OptArgs("shell", arg),
    77  		)
    78  		cp.Expect("Cannot find the Python-3.9 project")
    79  		cp.ExpectExitCode(1)
    80  	}
    81  }
    82  
    83  func (suite *ShellIntegrationTestSuite) TestDefaultShell() {
    84  	suite.OnlyRunForTags(tagsuite.Shell)
    85  
    86  	ts := e2e.New(suite.T(), false)
    87  	defer ts.Close()
    88  
    89  	// Checkout.
    90  	cp := ts.SpawnWithOpts(e2e.OptArgs("checkout", "ActiveState-CLI/small-python"))
    91  	cp.Expect("Skipping runtime setup")
    92  	cp.Expect("Checked out project")
    93  	cp.ExpectExitCode(0)
    94  
    95  	// Use.
    96  	cp = ts.SpawnWithOpts(
    97  		e2e.OptArgs("use", "ActiveState-CLI/small-python"),
    98  		e2e.OptAppendEnv(constants.DisableRuntime+"=false"),
    99  	)
   100  	cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt)
   101  	cp.ExpectExitCode(0)
   102  
   103  	cp = ts.SpawnWithOpts(
   104  		e2e.OptArgs("shell"),
   105  	)
   106  	cp.Expect("Activated")
   107  	cp.ExpectInput()
   108  	cp.SendLine("exit")
   109  	cp.ExpectExitCode(0)
   110  }
   111  
   112  func (suite *ShellIntegrationTestSuite) TestCwdShell() {
   113  	suite.OnlyRunForTags(tagsuite.Shell)
   114  
   115  	ts := e2e.New(suite.T(), false)
   116  	defer ts.Close()
   117  
   118  	cp := ts.SpawnWithOpts(
   119  		e2e.OptArgs("activate", "ActiveState-CLI/small-python"),
   120  	)
   121  	cp.Expect("Activated")
   122  	cp.ExpectInput()
   123  	cp.SendLine("exit")
   124  	cp.ExpectExitCode(0)
   125  
   126  	cp = ts.SpawnWithOpts(
   127  		e2e.OptArgs("shell"),
   128  		e2e.OptWD(filepath.Join(ts.Dirs.Work, "small-python")),
   129  	)
   130  	cp.Expect("Activated")
   131  	cp.ExpectInput()
   132  	cp.SendLine("exit")
   133  	cp.ExpectExitCode(0)
   134  }
   135  
   136  func (suite *ShellIntegrationTestSuite) TestCd() {
   137  	suite.OnlyRunForTags(tagsuite.Shell)
   138  
   139  	ts := e2e.New(suite.T(), false)
   140  	defer ts.Close()
   141  
   142  	cp := ts.SpawnWithOpts(
   143  		e2e.OptArgs("activate", "ActiveState-CLI/small-python"),
   144  	)
   145  	cp.Expect("Activated")
   146  	cp.ExpectInput()
   147  	cp.SendLine("exit")
   148  	cp.ExpectExitCode(0)
   149  
   150  	subdir := filepath.Join(ts.Dirs.Work, "foo", "bar", "baz")
   151  	err := fileutils.Mkdir(subdir)
   152  	suite.Require().NoError(err)
   153  
   154  	cp = ts.SpawnWithOpts(
   155  		e2e.OptArgs("shell", "ActiveState-CLI/small-python"),
   156  		e2e.OptWD(subdir),
   157  	)
   158  	cp.Expect("Activated")
   159  	cp.ExpectInput()
   160  	if runtime.GOOS != "windows" {
   161  		cp.SendLine("pwd")
   162  	} else {
   163  		cp.SendLine("echo %cd%")
   164  	}
   165  	cp.Expect(subdir)
   166  	cp.SendLine("exit")
   167  
   168  	cp = ts.SpawnWithOpts(
   169  		e2e.OptArgs("shell", "ActiveState-CLI/small-python", "--cd"),
   170  		e2e.OptWD(subdir),
   171  	)
   172  	cp.Expect("Activated")
   173  	cp.ExpectInput()
   174  	if runtime.GOOS != "windows" {
   175  		cp.SendLine("ls")
   176  	} else {
   177  		cp.SendLine("dir")
   178  	}
   179  	cp.Expect("activestate.yaml")
   180  	cp.SendLine("exit")
   181  
   182  	cp.ExpectExitCode(0)
   183  }
   184  
   185  func (suite *ShellIntegrationTestSuite) TestDefaultNoLongerExists() {
   186  	suite.OnlyRunForTags(tagsuite.Shell)
   187  
   188  	ts := e2e.New(suite.T(), false)
   189  	defer ts.Close()
   190  
   191  	cp := ts.SpawnWithOpts(e2e.OptArgs("checkout", "ActiveState-CLI/Python3"))
   192  	cp.Expect("Skipping runtime setup")
   193  	cp.Expect("Checked out project")
   194  	cp.ExpectExitCode(0)
   195  
   196  	cp = ts.SpawnWithOpts(
   197  		e2e.OptArgs("use", "ActiveState-CLI/Python3"),
   198  		e2e.OptAppendEnv(constants.DisableRuntime+"=false"),
   199  	)
   200  	cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt)
   201  	cp.ExpectExitCode(0)
   202  
   203  	err := os.RemoveAll(filepath.Join(ts.Dirs.Work, "Python3"))
   204  	suite.Require().NoError(err)
   205  
   206  	cp = ts.SpawnWithOpts(e2e.OptArgs("shell"))
   207  	cp.Expect("Cannot find your project")
   208  	cp.ExpectExitCode(1)
   209  }
   210  
   211  func (suite *ShellIntegrationTestSuite) TestUseShellUpdates() {
   212  	suite.OnlyRunForTags(tagsuite.Shell)
   213  
   214  	ts := e2e.New(suite.T(), false)
   215  	defer ts.Close()
   216  
   217  	suite.SetupRCFile(ts)
   218  	suite.T().Setenv("ACTIVESTATE_HOME", ts.Dirs.HomeDir)
   219  
   220  	cp := ts.Spawn("checkout", "ActiveState-CLI/Python3")
   221  	cp.Expect("Checked out project")
   222  	cp.ExpectExitCode(0)
   223  
   224  	// Create a zsh RC file
   225  	var zshRcFile string
   226  	var err error
   227  	if runtime.GOOS != "windows" {
   228  		zsh := &zsh.SubShell{}
   229  		zshRcFile, err = zsh.RcFile()
   230  		suite.NoError(err)
   231  	}
   232  
   233  	cp = ts.SpawnWithOpts(
   234  		e2e.OptArgs("use", "ActiveState-CLI/Python3"),
   235  		e2e.OptAppendEnv("SHELL=bash"),
   236  		e2e.OptAppendEnv(constants.DisableRuntime+"=false"),
   237  	)
   238  	cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt)
   239  	cp.ExpectExitCode(0)
   240  
   241  	// Ensure both bash and zsh RC files are updated
   242  	cfg, err := config.New()
   243  	suite.NoError(err)
   244  	rcfile, err := subshell.New(cfg).RcFile()
   245  	if runtime.GOOS != "windows" && fileutils.FileExists(rcfile) {
   246  		suite.NoError(err)
   247  		suite.Contains(string(fileutils.ReadFileUnsafe(rcfile)), ts.Dirs.DefaultBin, "PATH does not have your project in it")
   248  		suite.Contains(string(fileutils.ReadFileUnsafe(zshRcFile)), ts.Dirs.DefaultBin, "PATH does not have your project in it")
   249  	}
   250  }
   251  
   252  func (suite *ShellIntegrationTestSuite) TestJSON() {
   253  	suite.OnlyRunForTags(tagsuite.Shell, tagsuite.JSON)
   254  	ts := e2e.New(suite.T(), false)
   255  	defer ts.Close()
   256  
   257  	cp := ts.Spawn("shell", "--output", "json")
   258  	cp.Expect(`"error":"This command does not support the 'json' output format`, termtest.OptExpectTimeout(5*time.Second))
   259  	cp.ExpectExitCode(1)
   260  	AssertValidJSON(suite.T(), cp)
   261  }
   262  
   263  func (suite *ShellIntegrationTestSuite) SetupRCFile(ts *e2e.Session) {
   264  	if runtime.GOOS == "windows" {
   265  		return
   266  	}
   267  
   268  	ts.SetupRCFile()
   269  	ts.SetupRCFileCustom(&zsh.SubShell{})
   270  }
   271  
   272  func (suite *ShellIntegrationTestSuite) TestRuby() {
   273  	suite.OnlyRunForTags(tagsuite.Shell)
   274  	ts := e2e.New(suite.T(), false)
   275  	defer ts.Close()
   276  
   277  	cp := ts.Spawn("checkout", "ActiveState-CLI-Testing/Ruby", ".")
   278  	cp.Expect("Checked out project")
   279  	cp.ExpectExitCode(0)
   280  
   281  	cp = ts.SpawnWithOpts(
   282  		e2e.OptArgs("shell"),
   283  		e2e.OptAppendEnv(constants.DisableRuntime+"=false"),
   284  	)
   285  	cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt)
   286  	cp.ExpectInput()
   287  	cp.SendLine("ruby -v")
   288  	cp.Expect("ActiveState")
   289  }
   290  
   291  func (suite *ShellIntegrationTestSuite) TestNestedShellNotification() {
   292  	if runtime.GOOS == "windows" {
   293  		return // cmd.exe does not have an RC file to check for nested shells in
   294  	}
   295  	suite.OnlyRunForTags(tagsuite.Shell)
   296  	ts := e2e.New(suite.T(), false)
   297  	defer ts.Close()
   298  
   299  	var ss subshell.SubShell
   300  	var rcFile string
   301  	env := []string{"ACTIVESTATE_CLI_DISABLE_RUNTIME=false"}
   302  	switch runtime.GOOS {
   303  	case "darwin":
   304  		ss = &zsh.SubShell{}
   305  		ss.SetBinary("zsh")
   306  		rcFile = filepath.Join(ts.Dirs.HomeDir, ".zshrc")
   307  		suite.Require().NoError(sscommon.WriteRcFile("zshrc_append.sh", rcFile, sscommon.DefaultID, nil))
   308  		env = append(env, "SHELL=zsh") // override since CI tests are running on bash
   309  	case "linux":
   310  		ss = &bash.SubShell{}
   311  		ss.SetBinary("bash")
   312  		rcFile = filepath.Join(ts.Dirs.HomeDir, ".bashrc")
   313  		suite.Require().NoError(sscommon.WriteRcFile("bashrc_append.sh", rcFile, sscommon.DefaultID, nil))
   314  	default:
   315  		suite.Fail("Unsupported OS")
   316  	}
   317  	suite.Require().Equal(filepath.Dir(rcFile), ts.Dirs.HomeDir, "rc file not in test suite homedir")
   318  	suite.Require().Contains(string(fileutils.ReadFileUnsafe(rcFile)), "State Tool is operating on project")
   319  
   320  	cp := ts.Spawn("checkout", "ActiveState-CLI/small-python")
   321  	cp.Expect("Checked out project")
   322  	cp.ExpectExitCode(0)
   323  
   324  	cp = ts.SpawnWithOpts(
   325  		e2e.OptArgs("shell", "small-python"),
   326  		e2e.OptAppendEnv(env...))
   327  	cp.Expect("Activated")
   328  	suite.Assert().NotContains(cp.Snapshot(), "State Tool is operating on project")
   329  	cp.SendLine(fmt.Sprintf(`export HOME="%s"`, ts.Dirs.HomeDir)) // some shells do not forward this
   330  
   331  	cp.SendLine(ss.Binary()) // platform-specific shell (zsh on macOS, bash on Linux, etc.)
   332  	cp.Expect("State Tool is operating on project ActiveState-CLI/small-python")
   333  	cp.SendLine("exit") // subshell within a subshell
   334  	cp.SendLine("exit")
   335  	cp.ExpectExitCode(0)
   336  }
   337  
   338  func (suite *ShellIntegrationTestSuite) TestPs1() {
   339  	if runtime.GOOS == "windows" {
   340  		return // cmd.exe does not have a PS1 to modify
   341  	}
   342  	suite.OnlyRunForTags(tagsuite.Shell)
   343  	ts := e2e.New(suite.T(), false)
   344  	defer ts.Close()
   345  
   346  	cp := ts.Spawn("checkout", "ActiveState-CLI/small-python")
   347  	cp.Expect("Checked out project")
   348  	cp.ExpectExitCode(0)
   349  
   350  	cp = ts.SpawnWithOpts(
   351  		e2e.OptArgs("shell", "small-python"),
   352  		e2e.OptAppendEnv(constants.DisableRuntime+"=false"),
   353  	)
   354  	cp.Expect("Activated")
   355  	cp.Expect("[ActiveState-CLI/small-python]")
   356  	cp.SendLine("exit")
   357  	cp.ExpectExitCode(0)
   358  
   359  	cp = ts.Spawn("config", "set", constants.PreservePs1ConfigKey, "true")
   360  	cp.ExpectExitCode(0)
   361  
   362  	cp = ts.Spawn("shell", "small-python")
   363  	cp.Expect("Activated")
   364  	suite.Assert().NotContains(cp.Snapshot(), "[ActiveState-CLI/small-python]")
   365  	cp.SendLine("exit")
   366  	cp.ExpectExitCode(0)
   367  }
   368  
   369  func (suite *ShellIntegrationTestSuite) TestProjectOrder() {
   370  	suite.OnlyRunForTags(tagsuite.Critical, tagsuite.Shell)
   371  	ts := e2e.New(suite.T(), false)
   372  	defer ts.Close()
   373  
   374  	// First, set up a new project with a subproject.
   375  	cp := ts.Spawn("checkout", "ActiveState-CLI/Perl-5.32", "project")
   376  	cp.Expect("Skipping runtime setup")
   377  	cp.Expect("Checked out project")
   378  	cp.ExpectExitCode(0)
   379  	projectDir := filepath.Join(ts.Dirs.Work, "project")
   380  
   381  	cp = ts.SpawnWithOpts(
   382  		e2e.OptArgs("checkout", "ActiveState-CLI/Perl-5.32", "subproject"),
   383  		e2e.OptWD(projectDir),
   384  	)
   385  	cp.Expect("Skipping runtime setup")
   386  	cp.Expect("Checked out project")
   387  	cp.ExpectExitCode(0)
   388  	subprojectDir := filepath.Join(projectDir, "subproject")
   389  
   390  	// Then set up a separate project and make it the default.
   391  	cp = ts.Spawn("checkout", "ActiveState-CLI/Perl-5.32", "default")
   392  	cp.Expect("Skipping runtime setup")
   393  	cp.Expect("Checked out project")
   394  	cp.ExpectExitCode(0)
   395  	defaultDir := filepath.Join(ts.Dirs.Work, "default")
   396  
   397  	cp = ts.SpawnWithOpts(
   398  		e2e.OptArgs("use"),
   399  		e2e.OptWD(defaultDir),
   400  		e2e.OptAppendEnv(constants.DisableRuntime+"=false"),
   401  	)
   402  	cp.Expect("Setting Up Runtime", e2e.RuntimeSourcingTimeoutOpt)
   403  	cp.Expect("Switched to project", e2e.RuntimeSourcingTimeoutOpt)
   404  	cp.Expect(defaultDir)
   405  	cp.ExpectExitCode(0)
   406  
   407  	// Now set up an empty directory.
   408  	emptyDir := filepath.Join(ts.Dirs.Work, "empty")
   409  	suite.Require().NoError(fileutils.Mkdir(emptyDir))
   410  
   411  	// Now change to the project directory and assert that project is used instead of the default
   412  	// project.
   413  	cp = ts.SpawnWithOpts(
   414  		e2e.OptArgs("refresh"),
   415  		e2e.OptWD(projectDir),
   416  	)
   417  	cp.Expect(projectDir)
   418  	cp.ExpectExitCode(0)
   419  
   420  	// Run `state shell` in this project, change to the subproject directory, and assert the parent
   421  	// project is used instead of the subproject.
   422  	cp = ts.SpawnWithOpts(
   423  		e2e.OptArgs("shell"),
   424  		e2e.OptWD(projectDir),
   425  		e2e.OptAppendEnv(constants.DisableRuntime+"=false"),
   426  	)
   427  	cp.Expect("Opening shell", e2e.RuntimeSourcingTimeoutOpt)
   428  	cp.Expect(projectDir)
   429  	cp.SendLine("cd subproject")
   430  	cp.SendLine("state refresh")
   431  	cp.Expect(projectDir) // not subprojectDir
   432  	cp.SendLine("exit")
   433  	cp.Expect("Deactivated")
   434  	cp.ExpectExit() // exit code varies depending on shell; just assert the shell exited
   435  
   436  	// After exiting the shell, assert the subproject is used instead of the parent project.
   437  	cp = ts.SpawnWithOpts(
   438  		e2e.OptArgs("refresh"),
   439  		e2e.OptWD(subprojectDir),
   440  	)
   441  	cp.Expect(subprojectDir)
   442  	cp.ExpectExitCode(0)
   443  
   444  	// If a project subdirectory does not contain an activestate.yaml file, assert the project that
   445  	// owns the subdirectory will be used.
   446  	nestedDir := filepath.Join(subprojectDir, "nested")
   447  	suite.Require().NoError(fileutils.Mkdir(nestedDir))
   448  	cp = ts.SpawnWithOpts(
   449  		e2e.OptArgs("refresh"),
   450  		e2e.OptWD(nestedDir),
   451  	)
   452  	cp.Expect(subprojectDir)
   453  	cp.ExpectExitCode(0)
   454  
   455  	// Change to an empty directory and assert the default project is used.
   456  	cp = ts.SpawnWithOpts(
   457  		e2e.OptArgs("refresh"),
   458  		e2e.OptWD(emptyDir),
   459  	)
   460  	cp.Expect(defaultDir)
   461  	cp.ExpectExitCode(0)
   462  
   463  	// If none of the above, assert an error.
   464  	cp = ts.Spawn("use", "reset", "-n")
   465  	cp.ExpectExitCode(0)
   466  
   467  	cp = ts.SpawnWithOpts(
   468  		e2e.OptArgs("refresh"),
   469  		e2e.OptWD(emptyDir),
   470  	)
   471  	cp.ExpectNotExitCode(0)
   472  }
   473  
   474  func (suite *ShellIntegrationTestSuite) TestScriptAlias() {
   475  	suite.OnlyRunForTags(tagsuite.Critical, tagsuite.Shell)
   476  	ts := e2e.New(suite.T(), false)
   477  	defer ts.Close()
   478  
   479  	cp := ts.Spawn("checkout", "ActiveState-CLI/Perl-5.32", ".")
   480  	cp.Expect("Skipping runtime setup")
   481  	cp.Expect("Checked out project")
   482  	cp.ExpectExitCode(0)
   483  
   484  	suite.NoError(fileutils.WriteFile(filepath.Join(ts.Dirs.Work, "testargs.pl"), []byte(`
   485  printf "Argument: '%s'.\n", $ARGV[0];
   486  `)))
   487  
   488  	// Append a run script to activestate.yaml.
   489  	asyFilename := filepath.Join(ts.Dirs.Work, constants.ConfigFileName)
   490  	contents := string(fileutils.ReadFileUnsafe(asyFilename))
   491  	lang := "bash"
   492  	splat := "$@"
   493  	if runtime.GOOS == "windows" {
   494  		lang = "powershell"
   495  		splat = "@args"
   496  	}
   497  	contents = strings.Replace(contents, "events:", fmt.Sprintf(`
   498    - name: args
   499      language: %s
   500      value: perl testargs.pl %s
   501  
   502  events:`, lang, splat), 1)
   503  	suite.Require().NoError(fileutils.WriteFile(asyFilename, []byte(contents)))
   504  
   505  	// Verify that running a script as a command with an argument containing special characters works.
   506  	cp = ts.SpawnWithOpts(
   507  		e2e.OptArgs("shell"),
   508  		e2e.OptAppendEnv(constants.DisableRuntime+"=false"),
   509  	)
   510  	cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt)
   511  	cp.ExpectInput()
   512  	cp.SendLine(`args "<3"`)
   513  	cp.Expect("Argument: '<3'", termtest.OptExpectTimeout(5*time.Second))
   514  	cp.SendLine("exit")
   515  	cp.Expect("Deactivated")
   516  	cp.ExpectExit() // exit code varies depending on shell; just assert the shell exited
   517  }
   518  
   519  func TestShellIntegrationTestSuite(t *testing.T) {
   520  	suite.Run(t, new(ShellIntegrationTestSuite))
   521  }