github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/test/integration/deploy_int_test.go (about) 1 package integration 2 3 import ( 4 "fmt" 5 "os" 6 "os/exec" 7 "path/filepath" 8 "runtime" 9 "testing" 10 11 "github.com/ActiveState/cli/internal/testhelpers/suite" 12 "github.com/google/uuid" 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/testhelpers/e2e" 19 "github.com/ActiveState/cli/internal/testhelpers/tagsuite" 20 ) 21 22 type DeployIntegrationTestSuite struct { 23 tagsuite.Suite 24 } 25 26 var symlinkExt = "" 27 28 func init() { 29 if runtime.GOOS == "windows" { 30 symlinkExt = ".lnk" 31 } 32 } 33 34 func (suite *DeployIntegrationTestSuite) deploy(ts *e2e.Session, prj string, targetPath string, targetID string) { 35 var cp *e2e.SpawnedCmd 36 switch runtime.GOOS { 37 case "windows": 38 cp = ts.SpawnWithOpts( 39 e2e.OptArgs("deploy", prj, "--path", targetPath), 40 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 41 ) 42 case "darwin": 43 // On MacOS the command is the same as Linux, however some binaries 44 // already exist at /usr/local/bin so we use the --force flag 45 cp = ts.SpawnWithOpts( 46 e2e.OptArgs("deploy", prj, "--path", targetPath, "--force"), 47 e2e.OptAppendEnv("SHELL=bash"), 48 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 49 ) 50 default: 51 cp = ts.SpawnWithOpts( 52 e2e.OptArgs("deploy", prj, "--path", targetPath), 53 e2e.OptAppendEnv("SHELL=bash"), 54 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 55 ) 56 } 57 58 cp.Expect("Installing", e2e.RuntimeSourcingTimeoutOpt) 59 cp.Expect("Configuring") 60 if runtime.GOOS != "windows" { 61 cp.Expect("Symlinking") 62 } 63 cp.Expect("Deployment Information") 64 cp.Expect(targetID) // expect bin dir 65 if runtime.GOOS == "windows" { 66 cp.Expect("log out") 67 } else { 68 cp.Expect("restart") 69 } 70 cp.ExpectExitCode(0) 71 } 72 73 func (suite *DeployIntegrationTestSuite) TestDeployPerl() { 74 suite.OnlyRunForTags(tagsuite.Perl, tagsuite.Deploy) 75 if !e2e.RunningOnCI() { 76 suite.T().Skipf("Skipping DeployIntegrationTestSuite when not running on CI, as it modifies bashrc/registry") 77 } 78 79 if runtime.GOOS == "darwin" { 80 suite.T().Skip("Perl is not supported on Mac OS yet.") 81 } 82 83 ts := e2e.New(suite.T(), false) 84 defer ts.Close() 85 86 targetID, err := uuid.NewUUID() 87 suite.Require().NoError(err) 88 targetPath, err := fileutils.ResolveUniquePath(filepath.Join(ts.Dirs.Work, targetID.String())) 89 suite.Require().NoError(err) 90 91 suite.deploy(ts, "ActiveState-CLI/Perl", targetPath, targetID.String()) 92 93 suite.checkSymlink("perl", ts.Dirs.Bin, targetID.String()) 94 95 var cp *e2e.SpawnedCmd 96 if runtime.GOOS == "windows" { 97 cp = ts.SpawnCmdWithOpts( 98 "cmd.exe", 99 e2e.OptArgs("/k", filepath.Join(targetPath, "bin", "shell.bat")), 100 e2e.OptAppendEnv("PATHEXT=.COM;.EXE;.BAT;.LNK", "SHELL="), 101 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 102 ) 103 } else { 104 cp = ts.SpawnCmdWithOpts( 105 "/bin/bash", 106 e2e.OptAppendEnv("PROMPT_COMMAND="), 107 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 108 ) 109 cp.SendLine(fmt.Sprintf("source %s\n", filepath.Join(targetPath, "bin", "shell.sh"))) 110 } 111 112 errorLevel := "echo $?" 113 if runtime.GOOS == "windows" { 114 errorLevel = `echo %ERRORLEVEL%` 115 } 116 // check that some of the installed binaries are use-able 117 cp.SendLine("perl --version") 118 cp.Expect("This is perl 5") 119 cp.SendLine(errorLevel) 120 cp.Expect("0") 121 122 cp.SendLine("ptar -h") 123 cp.Expect("a tar-like program written in perl") 124 125 cp.SendLine("exit 0") 126 cp.ExpectExitCode(0) 127 } 128 129 func (suite *DeployIntegrationTestSuite) checkSymlink(name string, binDir, targetID string) { 130 if runtime.GOOS != "Linux" { 131 return 132 } 133 // Linux symlinks to /usr/local/bin or the first write-able directory in PATH, so we can verify right away 134 execPath, err := exec.LookPath(name) 135 // If not on PATH it needs to exist in the temporary directory 136 var execDir string 137 if err == nil { 138 execDir, _ = filepath.Split(execPath) 139 } 140 if err != nil || (execDir != "/usr/local/bin/" && execDir != "/usr/bin/") { 141 execPath = filepath.Join(binDir, name) 142 if !fileutils.FileExists(execPath) { 143 suite.Fail("Expected to find %s on PATH", name) 144 } 145 } 146 link, err := os.Readlink(execPath) 147 suite.Require().NoError(err) 148 suite.Contains(link, targetID, "%s executable resolves to the one on our target dir", name) 149 } 150 151 func (suite *DeployIntegrationTestSuite) TestDeployPython() { 152 suite.OnlyRunForTags(tagsuite.Deploy, tagsuite.Python, tagsuite.Critical) 153 if !e2e.RunningOnCI() { 154 suite.T().Skipf("Skipping DeployIntegrationTestSuite when not running on CI, as it modifies bashrc/registry") 155 } 156 157 ts := e2e.New(suite.T(), false) 158 defer ts.Close() 159 160 ts.SetupRCFile() 161 suite.T().Setenv("ACTIVESTATE_HOME", ts.Dirs.HomeDir) 162 163 targetID, err := uuid.NewUUID() 164 suite.Require().NoError(err) 165 targetPath, err := fileutils.ResolveUniquePath(filepath.Join(ts.Dirs.Work, targetID.String())) 166 suite.Require().NoError(err) 167 168 suite.deploy(ts, "ActiveState-CLI/Python3", targetPath, targetID.String()) 169 170 suite.checkSymlink("python3", ts.Dirs.Bin, targetID.String()) 171 172 var cp *e2e.SpawnedCmd 173 if runtime.GOOS == "windows" { 174 cp = ts.SpawnCmdWithOpts( 175 "cmd.exe", 176 e2e.OptArgs("/k", filepath.Join(targetPath, "bin", "shell.bat")), 177 e2e.OptAppendEnv("PATHEXT=.COM;.EXE;.BAT;.LNK", "SHELL="), 178 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 179 ) 180 } else { 181 cp = ts.SpawnCmdWithOpts( 182 "/bin/bash", 183 e2e.OptAppendEnv("PROMPT_COMMAND="), 184 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 185 ) 186 cp.SendLine(fmt.Sprintf("source %s\n", filepath.Join(targetPath, "bin", "shell.sh"))) 187 } 188 189 errorLevel := "echo $?" 190 if runtime.GOOS == "windows" { 191 errorLevel = `echo %ERRORLEVEL%` 192 } 193 194 cp.SendLine("python3 --version") 195 cp.Expect("Python 3") 196 cp.SendLine(errorLevel) 197 cp.Expect("0") 198 199 cp.SendLine("pip3 --version") 200 cp.Expect("pip") 201 cp.SendLine(errorLevel) 202 cp.Expect("0") 203 204 if runtime.GOOS == "darwin" { 205 // This is kept as a regression test, pyvenv used to have a relocation problem on MacOS 206 cp.SendLine("pyvenv -h") 207 cp.SendLine("echo $?") 208 cp.Expect("0") 209 } 210 211 cp.SendLine("python3 -m pytest --version") 212 cp.Expect("pytest") 213 214 cp.SendLine("exit") 215 cp.ExpectExitCode(0) 216 217 suite.AssertConfig(ts, targetID.String()) 218 } 219 220 func (suite *DeployIntegrationTestSuite) TestDeployInstall() { 221 suite.OnlyRunForTags(tagsuite.Deploy) 222 if !e2e.RunningOnCI() { 223 suite.T().Skipf("Skipping DeployIntegrationTestSuite when not running on CI, as it modifies bashrc/registry") 224 } 225 226 ts := e2e.New(suite.T(), false) 227 defer ts.Close() 228 229 targetDir, err := fileutils.ResolveUniquePath(filepath.Join(ts.Dirs.Work, "target")) 230 suite.Require().NoError(err) 231 if fileutils.TargetExists(targetDir) { 232 isEmpty, err := fileutils.IsEmptyDir(targetDir) 233 suite.Require().NoError(err) 234 suite.True(isEmpty, "Target dir should be empty before we start") 235 } 236 237 suite.InstallAndAssert(ts, targetDir) 238 239 isEmpty, err := fileutils.IsEmptyDir(targetDir) 240 suite.Require().NoError(err) 241 suite.False(isEmpty, "Target dir should have artifacts written to it") 242 } 243 244 func (suite *DeployIntegrationTestSuite) InstallAndAssert(ts *e2e.Session, targetPath string) { 245 cp := ts.SpawnWithOpts( 246 e2e.OptArgs("deploy", "install", "ActiveState-CLI/Python3", "--path", targetPath), 247 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 248 ) 249 250 cp.Expect("Installing Runtime") 251 cp.Expect("Installing", e2e.RuntimeSourcingTimeoutOpt) 252 cp.Expect("Installation completed", e2e.RuntimeSourcingTimeoutOpt) 253 cp.ExpectExitCode(0) 254 } 255 256 func (suite *DeployIntegrationTestSuite) TestDeployConfigure() { 257 suite.OnlyRunForTags(tagsuite.Deploy) 258 if !e2e.RunningOnCI() { 259 suite.T().Skipf("Skipping TestDeployConfigure when not running on CI, as it modifies bashrc/registry") 260 } 261 ts := e2e.New(suite.T(), false) 262 defer ts.Close() 263 264 ts.SetupRCFile() 265 suite.T().Setenv("ACTIVESTATE_HOME", ts.Dirs.HomeDir) 266 267 targetID, err := uuid.NewUUID() 268 suite.Require().NoError(err) 269 targetPath, err := fileutils.ResolveUniquePath(filepath.Join(ts.Dirs.Work, targetID.String())) 270 suite.Require().NoError(err) 271 272 // Install step is required 273 cp := ts.SpawnWithOpts( 274 e2e.OptArgs("deploy", "configure", "ActiveState-CLI/Python3", "--path", targetPath), 275 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 276 ) 277 cp.Expect("need to run the install step") 278 cp.ExpectExitCode(1) 279 ts.IgnoreLogErrors() 280 suite.InstallAndAssert(ts, targetPath) 281 282 if runtime.GOOS != "windows" { 283 cp = ts.SpawnWithOpts( 284 e2e.OptArgs("deploy", "configure", "ActiveState-CLI/Python3", "--path", targetPath), 285 e2e.OptAppendEnv("SHELL=bash"), 286 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 287 ) 288 } else { 289 cp = ts.SpawnWithOpts( 290 e2e.OptArgs("deploy", "configure", "ActiveState-CLI/Python3", "--path", targetPath), 291 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 292 ) 293 } 294 295 cp.Expect("Configuring shell", e2e.RuntimeSourcingTimeoutOpt) 296 cp.ExpectExitCode(0) 297 suite.AssertConfig(ts, targetID.String()) 298 299 if runtime.GOOS == "windows" { 300 cp = ts.SpawnWithOpts( 301 e2e.OptArgs("deploy", "configure", "ActiveState-CLI/Python3", "--path", targetPath, "--user"), 302 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 303 ) 304 cp.Expect("Configuring shell", e2e.RuntimeSourcingTimeoutOpt) 305 cp.ExpectExitCode(0) 306 307 out, err := exec.Command("reg", "query", `HKCU\Environment`, "/v", "Path").Output() 308 suite.Require().NoError(err) 309 suite.Contains(string(out), targetID.String(), "Windows user PATH should contain our target dir") 310 } 311 } 312 313 func (suite *DeployIntegrationTestSuite) AssertConfig(ts *e2e.Session, targetID string) { 314 if runtime.GOOS != "windows" { 315 // Test config file 316 cfg, err := config.New() 317 suite.Require().NoError(err) 318 319 subshell := subshell.New(cfg) 320 rcFile, err := subshell.RcFile() 321 suite.Require().NoError(err) 322 323 bashContents := fileutils.ReadFileUnsafe(rcFile) 324 suite.Contains(string(bashContents), constants.RCAppendDeployStartLine, "config file should contain our RC Append Start line") 325 suite.Contains(string(bashContents), constants.RCAppendDeployStopLine, "config file should contain our RC Append Stop line") 326 suite.Contains(string(bashContents), targetID, "config file should contain our target dir") 327 } else { 328 // Test registry 329 out, err := exec.Command("reg", "query", `HKLM\SYSTEM\ControlSet001\Control\Session Manager\Environment`, "/v", "Path").Output() 330 suite.Require().NoError(err) 331 suite.Contains(string(out), targetID, "bashrc should contain our target dir") 332 } 333 } 334 335 func (suite *DeployIntegrationTestSuite) TestDeploySymlink() { 336 suite.OnlyRunForTags(tagsuite.Deploy) 337 if runtime.GOOS != "windows" && !e2e.RunningOnCI() { 338 suite.T().Skipf("Skipping TestDeploySymlink when not running on CI, as it modifies PATH") 339 } 340 341 ts := e2e.New(suite.T(), false, "SHELL=") 342 defer ts.Close() 343 344 targetID, err := uuid.NewUUID() 345 suite.Require().NoError(err) 346 targetPath, err := fileutils.ResolveUniquePath(filepath.Join(ts.Dirs.Work, targetID.String())) 347 suite.Require().NoError(err) 348 349 // Install step is required 350 cp := ts.SpawnWithOpts( 351 e2e.OptArgs("deploy", "symlink", "ActiveState-CLI/Python3", "--path", targetPath), 352 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 353 ) 354 cp.Expect("need to run the install step") 355 cp.ExpectExitCode(1) 356 ts.IgnoreLogErrors() 357 suite.InstallAndAssert(ts, targetPath) 358 359 if runtime.GOOS != "darwin" { 360 cp = ts.SpawnWithOpts( 361 e2e.OptArgs("deploy", "symlink", "ActiveState-CLI/Python3", "--path", targetPath), 362 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 363 ) 364 } else { 365 cp = ts.SpawnWithOpts( 366 e2e.OptArgs("deploy", "symlink", "ActiveState-CLI/Python3", "--path", targetPath, "--force"), 367 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 368 ) 369 } 370 371 if runtime.GOOS != "windows" { 372 cp.Expect("Symlinking executables") 373 } else { 374 cp.Expect("Skipped") 375 } 376 cp.ExpectExitCode(0) 377 378 suite.checkSymlink("python3", ts.Dirs.Bin, targetID.String()) 379 } 380 381 func (suite *DeployIntegrationTestSuite) TestDeployReport() { 382 suite.OnlyRunForTags(tagsuite.Deploy) 383 ts := e2e.New(suite.T(), false) 384 defer ts.Close() 385 386 targetID, err := uuid.NewUUID() 387 suite.Require().NoError(err) 388 targetPath, err := fileutils.ResolveUniquePath(filepath.Join(ts.Dirs.Work, targetID.String())) 389 suite.Require().NoError(err) 390 391 // Install step is required 392 cp := ts.SpawnWithOpts( 393 e2e.OptArgs("deploy", "report", "ActiveState-CLI/Python3", "--path", targetPath), 394 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 395 ) 396 cp.Expect("need to run the install step") 397 cp.ExpectExitCode(1) 398 ts.IgnoreLogErrors() 399 suite.InstallAndAssert(ts, targetPath) 400 401 cp = ts.SpawnWithOpts( 402 e2e.OptArgs("deploy", "report", "ActiveState-CLI/Python3", "--path", targetPath), 403 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 404 ) 405 cp.Expect("Deployment Information") 406 cp.Expect(targetID.String()) // expect bin dir 407 if runtime.GOOS == "windows" { 408 cp.Expect("log out") 409 } else { 410 cp.Expect("restart") 411 } 412 cp.ExpectExitCode(0) 413 } 414 415 func (suite *DeployIntegrationTestSuite) TestDeployTwice() { 416 suite.OnlyRunForTags(tagsuite.Deploy) 417 if runtime.GOOS == "darwin" || !e2e.RunningOnCI() { 418 suite.T().Skipf("Skipping TestDeployTwice when not running on CI or on MacOS, as it modifies PATH") 419 } 420 421 ts := e2e.New(suite.T(), false) 422 defer ts.Close() 423 424 targetPath, err := fileutils.ResolveUniquePath(filepath.Join(ts.Dirs.Work, "target")) 425 suite.Require().NoError(err) 426 427 suite.InstallAndAssert(ts, targetPath) 428 429 pathDir := fileutils.TempDirUnsafe() 430 defer os.RemoveAll(pathDir) 431 cp := ts.SpawnWithOpts( 432 e2e.OptArgs("deploy", "symlink", "ActiveState-CLI/Python3", "--path", targetPath), 433 e2e.OptAppendEnv(fmt.Sprintf("PATH=%s", pathDir)), // Avoid conflicts 434 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 435 ) 436 cp.ExpectExitCode(0) 437 438 // we do not symlink on windows anymore 439 if runtime.GOOS != "windows" { 440 suite.True(fileutils.FileExists(filepath.Join(targetPath, "bin", "python3"+symlinkExt)), "Python3 symlink should have been written") 441 } 442 443 // Running deploy a second time should not cause any errors (cache is properly picked up) 444 cpx := ts.SpawnWithOpts( 445 e2e.OptArgs("deploy", "symlink", "ActiveState-CLI/Python3", "--path", targetPath), 446 e2e.OptAppendEnv(fmt.Sprintf("PATH=%s", pathDir)), // Avoid conflicts 447 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 448 ) 449 cpx.ExpectExitCode(0) 450 } 451 452 func (suite *DeployIntegrationTestSuite) TestDeployUninstall() { 453 suite.OnlyRunForTags(tagsuite.Deploy) 454 if !e2e.RunningOnCI() { 455 suite.T().Skipf("Skipping TestDeployUninstall when not running on CI, as it modifies bashrc/registry") 456 } 457 458 ts := e2e.New(suite.T(), false) 459 defer ts.Close() 460 461 targetDir := filepath.Join(ts.Dirs.Work, "target") 462 if fileutils.TargetExists(targetDir) { 463 isEmpty, err := fileutils.IsEmptyDir(targetDir) 464 suite.Require().NoError(err) 465 suite.True(isEmpty, "Target dir should be empty before we start") 466 } 467 468 suite.InstallAndAssert(ts, targetDir) 469 470 // Uninstall deployed runtime. 471 cp := ts.SpawnWithOpts( 472 e2e.OptArgs("deploy", "uninstall", "--path", filepath.Join(ts.Dirs.Work, "target")), 473 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 474 ) 475 cp.Expect("Uninstall Deployed Runtime") 476 cp.Expect("Successful") 477 cp.ExpectExitCode(0) 478 suite.False(fileutils.TargetExists(filepath.Join(ts.Dirs.Work, "target")), "Deploy dir was not deleted") 479 suite.True(fileutils.IsDir(ts.Dirs.Work), "Work dir was unexpectedly deleted") 480 481 // Trying to uninstall again should fail 482 cp = ts.SpawnWithOpts( 483 e2e.OptArgs("deploy", "uninstall", "--path", filepath.Join(ts.Dirs.Work, "target")), 484 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 485 ) 486 cp.Expect("no deployed runtime") 487 cp.ExpectExitCode(1) 488 ts.IgnoreLogErrors() 489 suite.True(fileutils.IsDir(ts.Dirs.Work), "Work dir was unexpectedly deleted") 490 491 // Trying to uninstall in a non-deployment directory should fail. 492 cp = ts.SpawnWithOpts( 493 e2e.OptArgs("deploy", "uninstall"), 494 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 495 ) 496 cp.Expect("no deployed runtime") 497 cp.ExpectExitCode(1) 498 suite.True(fileutils.IsDir(ts.Dirs.Work), "Work dir was unexpectedly deleted") 499 500 // Trying to uninstall in a non-deployment directory should not delete that directory. 501 cp = ts.SpawnWithOpts( 502 e2e.OptArgs("deploy", "uninstall", "--path", ts.Dirs.Work), 503 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 504 ) 505 cp.Expect("no deployed runtime") 506 cp.ExpectExitCode(1) 507 suite.True(fileutils.IsDir(ts.Dirs.Work), "Work dir was unexpectedly deleted") 508 } 509 510 func TestDeployIntegrationTestSuite(t *testing.T) { 511 suite.Run(t, new(DeployIntegrationTestSuite)) 512 }