github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/cmd/state-installer/test/integration/installer_int_test.go (about) 1 package integration 2 3 import ( 4 "fmt" 5 "os" 6 "os/exec" 7 "path/filepath" 8 "runtime" 9 "strings" 10 "testing" 11 12 "github.com/ActiveState/cli/internal/config" 13 "github.com/ActiveState/cli/internal/constants" 14 "github.com/ActiveState/cli/internal/environment" 15 "github.com/ActiveState/cli/internal/fileutils" 16 "github.com/ActiveState/cli/internal/installation" 17 "github.com/ActiveState/cli/internal/osutils" 18 "github.com/ActiveState/cli/internal/subshell" 19 "github.com/ActiveState/cli/internal/testhelpers/e2e" 20 "github.com/ActiveState/cli/internal/testhelpers/suite" 21 "github.com/ActiveState/cli/internal/testhelpers/tagsuite" 22 "github.com/ActiveState/cli/pkg/sysinfo" 23 ) 24 25 type InstallerIntegrationTestSuite struct { 26 tagsuite.Suite 27 installerExe string 28 } 29 30 func (suite *InstallerIntegrationTestSuite) TestInstallFromLocalSource() { 31 suite.OnlyRunForTags(tagsuite.Installer, tagsuite.Critical) 32 ts := e2e.New(suite.T(), false) 33 defer ts.Close() 34 35 ts.SetupRCFile() 36 suite.T().Setenv(constants.HomeEnvVarName, ts.Dirs.HomeDir) 37 38 dir, err := os.MkdirTemp("", "system*") 39 suite.NoError(err) 40 41 // Run installer with source-path flag (ie. install from this local path) 42 cp := ts.SpawnCmdWithOpts( 43 suite.installerExe, 44 e2e.OptArgs(installationDir(ts), "-n"), 45 e2e.OptAppendEnv(constants.DisableUpdates+"=false"), 46 e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), 47 ) 48 49 // Assert output 50 cp.Expect("Installing State Tool") 51 cp.Expect("Done") 52 cp.Expect("successfully installed") 53 suite.NotContains(cp.Output(), "Downloading State Tool") 54 cp.ExpectInput() 55 cp.SendLine("exit") 56 cp.ExpectExitCode(0) 57 58 // Ensure installing overtop doesn't result in errors 59 cp = ts.SpawnCmdWithOpts( 60 suite.installerExe, 61 e2e.OptArgs(installationDir(ts), "-n"), 62 e2e.OptAppendEnv(constants.DisableUpdates+"=false"), 63 e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), 64 ) 65 cp.Expect("successfully installed") 66 cp.ExpectInput() 67 cp.SendLine("exit") 68 cp.ExpectExitCode(0) 69 70 // Again ensure installing overtop doesn't result in errors, but mock an older state tool format where 71 // the marker has no contents 72 suite.Require().NoError(fileutils.WriteFile(filepath.Join(installationDir(ts), installation.InstallDirMarker), []byte{})) 73 cp = ts.SpawnCmdWithOpts( 74 suite.installerExe, 75 e2e.OptArgs(installationDir(ts), "-n"), 76 e2e.OptAppendEnv(constants.DisableUpdates+"=false"), 77 e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), 78 ) 79 cp.Expect("successfully installed") 80 81 installDir := installationDir(ts) 82 83 stateExec, err := installation.StateExecFromDir(installDir) 84 suite.Contains(stateExec, installDir, "Ensure we're not grabbing state tool from integration test bin dir") 85 suite.NoError(err) 86 87 stateExecResolved, err := fileutils.ResolvePath(stateExec) 88 suite.Require().NoError(err) 89 90 serviceExec, err := installation.ServiceExecFromDir(installDir) 91 suite.NoError(err) 92 93 // Verify that launched subshell has State tool on PATH 94 cp.ExpectInput() 95 cp.SendLine("state --version") 96 cp.Expect("Version") 97 cp.ExpectInput() 98 99 if runtime.GOOS == "windows" { 100 cp.SendLine("where state") 101 } else { 102 cp.SendLine("which state") 103 } 104 cp.ExpectInput() 105 cp.SendLine("exit") 106 cp.ExpectExitCode(0) 107 108 snapshot := strings.Replace(cp.Output(), "\n", "", -1) 109 if !strings.Contains(snapshot, stateExec) && !strings.Contains(snapshot, stateExecResolved) { 110 suite.Fail(fmt.Sprintf("Snapshot does not include '%s' or '%s', snapshot:\n %s", stateExec, stateExecResolved, snapshot)) 111 } 112 113 // Assert expected files were installed (note this didn't use an update payload, so there's no bin directory) 114 suite.FileExists(stateExec) 115 suite.FileExists(serviceExec) 116 117 // Run state tool so test doesn't panic trying to find the log file 118 cp = ts.SpawnCmd(stateExec, "--version") 119 cp.Expect("Version") 120 121 // Assert that the config was written (ie. RC files or windows registry) 122 suite.AssertConfig(ts) 123 if runtime.GOOS == "windows" { 124 ts.IgnoreLogErrors() // Shortcut creation can intermittently fail on Windows CI, follow-up on DX-2678 125 } 126 } 127 128 func (suite *InstallerIntegrationTestSuite) TestInstallIncompatible() { 129 if runtime.GOOS != "windows" { 130 suite.T().Skip("Only Windows has incompatibility logic") 131 } 132 suite.OnlyRunForTags(tagsuite.Installer, tagsuite.Compatibility, tagsuite.Critical) 133 ts := e2e.New(suite.T(), false) 134 defer ts.Close() 135 136 // Run installer with source-path flag (ie. install from this local path) 137 cp := ts.SpawnCmdWithOpts( 138 suite.installerExe, 139 e2e.OptArgs(installationDir(ts), "-n"), 140 e2e.OptAppendEnv(constants.DisableUpdates+"=false", sysinfo.VersionOverrideEnvVar+"=10.0.0"), 141 ) 142 143 // Assert output 144 cp.Expect("not compatible") 145 cp.ExpectExitCode(1) 146 ts.IgnoreLogErrors() 147 } 148 149 func (suite *InstallerIntegrationTestSuite) TestInstallNoErrorTips() { 150 suite.OnlyRunForTags(tagsuite.Installer, tagsuite.Critical) 151 ts := e2e.New(suite.T(), false) 152 defer ts.Close() 153 154 dir, err := os.MkdirTemp("", "system*") 155 suite.NoError(err) 156 157 cp := ts.SpawnCmdWithOpts( 158 suite.installerExe, 159 e2e.OptArgs(installationDir(ts), "--activate", "ActiveState/DoesNotExist", "-n"), 160 e2e.OptAppendEnv(constants.DisableUpdates+"=true"), 161 e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), 162 ) 163 164 cp.ExpectExitCode(1) 165 suite.Assert().NotContains(cp.Output(), "Need More Help?", "error tips should not be displayed when invoking installer") 166 ts.IgnoreLogErrors() 167 } 168 169 func (suite *InstallerIntegrationTestSuite) TestInstallErrorTips() { 170 suite.OnlyRunForTags(tagsuite.Installer, tagsuite.Critical) 171 ts := e2e.New(suite.T(), false) 172 defer ts.Close() 173 174 dir, err := os.MkdirTemp("", "system*") 175 suite.NoError(err) 176 177 cp := ts.SpawnCmdWithOpts( 178 suite.installerExe, 179 e2e.OptArgs(installationDir(ts), "--activate", "ActiveState-CLI/Python3", "-n"), 180 e2e.OptAppendEnv(constants.DisableUpdates+"=true"), 181 e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), 182 ) 183 184 cp.ExpectInput() 185 cp.SendLine("state command-does-not-exist") 186 cp.ExpectInput() 187 cp.SendLine("exit") 188 cp.ExpectExit() 189 suite.Assert().Contains(cp.Output(), "Need More Help?", 190 "error tips should be displayed in shell created by installer") 191 ts.IgnoreLogErrors() 192 } 193 194 func (suite *InstallerIntegrationTestSuite) TestInstallerOverwriteServiceApp() { 195 suite.OnlyRunForTags(tagsuite.Installer) 196 if runtime.GOOS != "darwin" { 197 suite.T().Skip("Only macOS has the service app") 198 } 199 200 ts := e2e.New(suite.T(), false) 201 defer ts.Close() 202 203 appInstallDir := filepath.Join(ts.Dirs.Work, "app") 204 err := fileutils.Mkdir(appInstallDir) 205 suite.Require().NoError(err) 206 207 cp := ts.SpawnCmdWithOpts( 208 suite.installerExe, 209 e2e.OptArgs(installationDir(ts), "-n"), 210 e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.AppInstallDirOverrideEnvVarName, appInstallDir)), 211 ) 212 cp.Expect("Done") 213 cp.SendLine("exit") 214 cp.ExpectExit() // the return code can vary depending on shell (e.g. zsh vs. bash); just assert the installer shell exited 215 216 // State Service.app should be overwritten cleanly without error. 217 cp = ts.SpawnCmdWithOpts( 218 suite.installerExe, 219 e2e.OptArgs(installationDir(ts)+"2", "-n"), 220 e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.AppInstallDirOverrideEnvVarName, appInstallDir)), 221 ) 222 cp.Expect("Done") 223 cp.SendLine("exit") 224 cp.ExpectExit() // the return code can vary depending on shell (e.g. zsh vs. bash); just assert the installer shell exited 225 } 226 227 func (suite *InstallerIntegrationTestSuite) TestInstallWhileInUse() { 228 suite.OnlyRunForTags(tagsuite.Installer) 229 if runtime.GOOS != "windows" { 230 suite.T().Skip("Only windows can have issues with copying over files in use") 231 } 232 233 ts := e2e.New(suite.T(), false) 234 defer ts.Close() 235 236 dir, err := os.MkdirTemp("", "system*") 237 suite.NoError(err) 238 239 cp := ts.SpawnCmdWithOpts( 240 suite.installerExe, 241 e2e.OptArgs(installationDir(ts), "-n"), 242 e2e.OptAppendEnv(constants.DisableUpdates+"=true"), 243 e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), 244 ) 245 cp.Expect("successfully installed", e2e.RuntimeSourcingTimeoutOpt) 246 cp.ExpectInput() 247 cp.SendLine("state checkout ActiveState/Perl-5.32") 248 cp.Expect("Checked out") 249 cp.SendLine("state shell Perl-5.32") 250 cp.Expect("Activated") // state.exe remains active 251 252 // On Windows we cannot delete files/executables in use. Instead, the installer copies new 253 // executables into the target directory with the ".new" suffix and renames them to the target 254 // executables. Verify that this works without error. 255 cp2 := ts.SpawnCmdWithOpts( 256 suite.installerExe, 257 e2e.OptArgs(installationDir(ts), "-f", "-n"), 258 e2e.OptAppendEnv(constants.DisableUpdates+"=true"), 259 e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)), 260 ) 261 cp2.Expect("successfully installed", e2e.RuntimeSourcingTimeoutOpt) 262 cp2.ExpectInput() 263 cp2.SendLine("exit") 264 cp2.ExpectExit() // the return code can vary depending on shell (e.g. zsh vs. bash); just assert the installer shell exited 265 266 oldStateExeFound := false 267 files, err := fileutils.ListDirSimple(filepath.Join(installationDir(ts), "bin"), false) 268 suite.Require().NoError(err) 269 270 for _, file := range files { 271 if strings.Contains(file, "state.exe") && strings.HasSuffix(file, ".old") { 272 oldStateExeFound = true 273 break 274 } 275 } 276 suite.Assert().True(oldStateExeFound, "the state.exe currently in use was not copied to a '.old' file") 277 278 cp.SendLine("exit") // state shell 279 cp.SendLine("exit") // installer shell 280 cp.ExpectExit() // the return code can vary depending on shell (e.g. zsh vs. bash); just assert the installer shell exited 281 } 282 283 func (suite *InstallerIntegrationTestSuite) AssertConfig(ts *e2e.Session) { 284 if runtime.GOOS != "windows" { 285 // Test bashrc 286 cfg, err := config.New() 287 suite.Require().NoError(err) 288 289 subshell := subshell.New(cfg) 290 rcFile, err := subshell.RcFile() 291 suite.Require().NoError(err) 292 293 bashContents := fileutils.ReadFileUnsafe(rcFile) 294 suite.Contains(string(bashContents), constants.RCAppendInstallStartLine, "rc file should contain our RC Append Start line") 295 suite.Contains(string(bashContents), constants.RCAppendInstallStopLine, "rc file should contain our RC Append Stop line") 296 suite.Contains(string(bashContents), filepath.Join(ts.Dirs.Work), "rc file should contain our target dir") 297 } else { 298 // Test registry 299 out, err := exec.Command("reg", "query", `HKLM\SYSTEM\ControlSet001\Control\Session Manager\Environment`, "/v", "Path").Output() 300 suite.Require().NoError(err) 301 302 // we need to look for the short and the long version of the target PATH, because Windows translates between them arbitrarily 303 shortPath, err := fileutils.GetShortPathName(ts.Dirs.Work) 304 suite.Require().NoError(err) 305 longPath, err := fileutils.GetLongPathName(ts.Dirs.Work) 306 suite.Require().NoError(err) 307 if !strings.Contains(string(out), shortPath) && !strings.Contains(string(out), longPath) && !strings.Contains(string(out), ts.Dirs.Work) { 308 suite.T().Errorf("registry PATH \"%s\" does not contain \"%s\", \"%s\" or \"%s\"", out, ts.Dirs.Work, shortPath, longPath) 309 } 310 } 311 } 312 313 func installationDir(ts *e2e.Session) string { 314 return filepath.Join(ts.Dirs.Work, "installation") 315 } 316 317 func (suite *InstallerIntegrationTestSuite) SetupSuite() { 318 rootPath := environment.GetRootPathUnsafe() 319 localPayload := filepath.Join(rootPath, "build", "payload", constants.LegacyToplevelInstallArchiveDir) 320 suite.Require().DirExists(localPayload, "locally generated payload exists") 321 322 installerExe := filepath.Join(localPayload, constants.StateInstallerCmd+osutils.ExeExtension) 323 suite.Require().FileExists(installerExe, "locally generated installer exists") 324 325 suite.installerExe = installerExe 326 } 327 328 func TestInstallerIntegrationTestSuite(t *testing.T) { 329 suite.Run(t, new(InstallerIntegrationTestSuite)) 330 }