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 }