github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/test/integration/update_int_test.go (about) 1 package integration 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "path/filepath" 8 "runtime" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/ActiveState/cli/internal/testhelpers/suite" 14 "github.com/ActiveState/termtest" 15 16 "github.com/ActiveState/cli/internal/config" 17 "github.com/ActiveState/cli/internal/constants" 18 "github.com/ActiveState/cli/internal/fileutils" 19 "github.com/ActiveState/cli/internal/httputil" 20 "github.com/ActiveState/cli/internal/installation" 21 "github.com/ActiveState/cli/internal/osutils" 22 "github.com/ActiveState/cli/internal/rtutils/singlethread" 23 "github.com/ActiveState/cli/internal/testhelpers/e2e" 24 "github.com/ActiveState/cli/internal/testhelpers/tagsuite" 25 ) 26 27 type UpdateIntegrationTestSuite struct { 28 tagsuite.Suite 29 } 30 31 type matcherFunc func(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool 32 33 // Todo https://www.pivotaltracker.com/story/show/177863116 34 // Update to release channel when possible 35 var ( 36 targetChannel = "beta" 37 oldUpdateVersion = "beta@0.32.2-SHA3e1d435" 38 ) 39 40 func init() { 41 if constants.ChannelName == targetChannel { 42 targetChannel = "master" 43 } 44 } 45 46 // env prepares environment variables for the test 47 // disableUpdates prevents all update code from running 48 // testUpdate directs to the locally running update directory and requires that a test update bundles has been generated with `state run generate-test-update` 49 func (suite *UpdateIntegrationTestSuite) env(disableUpdates, forceUpdate bool) []string { 50 env := []string{} 51 52 if disableUpdates { 53 env = append(env, constants.DisableUpdates+"=true") 54 } else { 55 env = append(env, constants.DisableUpdates+"=false") 56 } 57 58 if forceUpdate { 59 env = append(env, constants.TestAutoUpdateEnvVarName+"=true") 60 env = append(env, constants.ForceUpdateEnvVarName+"=true") 61 } 62 63 dir, err := os.MkdirTemp("", "system*") 64 suite.NoError(err) 65 env = append(env, fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir)) 66 67 return env 68 } 69 70 func (suite *UpdateIntegrationTestSuite) versionCompare(ts *e2e.Session, expected string, matcher matcherFunc) { 71 type versionData struct { 72 Version string `json:"version"` 73 } 74 75 cp := ts.SpawnWithOpts(e2e.OptArgs("--version", "--output=json"), e2e.OptAppendEnv(suite.env(true, false)...)) 76 cp.ExpectExitCode(0) 77 78 version := versionData{} 79 out := cp.StrippedSnapshot() 80 err := json.Unmarshal([]byte(out), &version) 81 suite.NoError(err) 82 83 matcher(expected, version.Version, fmt.Sprintf("Version could not be matched, output:\n\n%s", out)) 84 } 85 86 func (suite *UpdateIntegrationTestSuite) channelCompare(ts *e2e.Session, expected string, matcher matcherFunc) { 87 type channelData struct { 88 Channel string `json:"channel"` 89 } 90 91 cp := ts.SpawnWithOpts(e2e.OptArgs("--version", "--output=json"), e2e.OptAppendEnv(suite.env(true, false)...)) 92 cp.ExpectExitCode(0, termtest.OptExpectTimeout(30*time.Second)) 93 94 channel := channelData{} 95 out := cp.StrippedSnapshot() 96 err := json.Unmarshal([]byte(out), &channel) 97 suite.NoError(err) 98 99 matcher(expected, channel.Channel, fmt.Sprintf("Channel could not be matched, output:\n\n%s", out)) 100 } 101 102 func (suite *UpdateIntegrationTestSuite) TestUpdateAvailable() { 103 suite.OnlyRunForTags(tagsuite.Update, tagsuite.Critical) 104 105 ts := e2e.New(suite.T(), false) 106 defer ts.Close() 107 108 cfg, err := config.NewCustom(ts.Dirs.Config, singlethread.New(), true) 109 suite.Require().NoError(err) 110 defer cfg.Close() 111 err = cfg.Set(constants.AutoUpdateConfigKey, "false") 112 suite.Require().NoError(err) 113 114 search, found := "Update Available", false 115 for i := 0; i < 4; i++ { 116 if i > 0 { 117 time.Sleep(time.Second * 3) 118 } 119 120 cp := ts.SpawnWithOpts(e2e.OptArgs("--version"), e2e.OptAppendEnv(suite.env(false, true)...)) 121 cp.ExpectExitCode(0) 122 123 if strings.Contains(cp.Snapshot(), search) { 124 found = true 125 break 126 } 127 } 128 129 suite.Require().True(found, "Expecting to find %q", search) 130 } 131 132 func (suite *UpdateIntegrationTestSuite) TestUpdate() { 133 suite.OnlyRunForTags(tagsuite.Update, tagsuite.Critical) 134 135 ts := e2e.New(suite.T(), true) 136 defer ts.Close() 137 138 suite.testUpdate(ts, filepath.Dir(ts.Dirs.Bin)) 139 } 140 141 func (suite *UpdateIntegrationTestSuite) testUpdate(ts *e2e.Session, baseDir string, opts ...e2e.SpawnOptSetter) { 142 cfg, err := config.NewCustom(ts.Dirs.Config, singlethread.New(), true) 143 suite.Require().NoError(err) 144 defer cfg.Close() 145 146 spawnOpts := []e2e.SpawnOptSetter{ 147 e2e.OptArgs("update"), 148 e2e.OptAppendEnv(suite.env(false, true)...), 149 } 150 if opts != nil { 151 spawnOpts = append(spawnOpts, opts...) 152 } 153 154 stateExec, err := installation.StateExecFromDir(baseDir) 155 suite.NoError(err) 156 157 searchA, searchB, found := "Updating State Tool to", "Installing Update", false 158 for i := 0; i < 4; i++ { 159 if i > 0 { 160 time.Sleep(time.Second * 3) 161 } 162 163 cp := ts.SpawnCmdWithOpts(stateExec, spawnOpts...) 164 cp.ExpectExitCode(0) 165 166 snap := cp.Snapshot() 167 if strings.Contains(snap, searchA) && strings.Contains(snap, searchB) { 168 found = true 169 break 170 } 171 } 172 173 suite.Require().True(found, "Expecting to find %q and %q", searchA, searchB) 174 } 175 176 func (suite *UpdateIntegrationTestSuite) TestUpdate_Repair() { 177 suite.OnlyRunForTags(tagsuite.Update) 178 ts := e2e.New(suite.T(), true) 179 defer ts.Close() 180 181 cfg, err := config.NewCustom(ts.Dirs.Config, singlethread.New(), true) 182 suite.Require().NoError(err) 183 defer cfg.Close() 184 185 subBinDir := filepath.Join(ts.Dirs.Bin, "bin") 186 files, err := os.ReadDir(ts.Dirs.Bin) 187 suite.NoError(err) 188 for _, f := range files { 189 err = fileutils.CopyFile(filepath.Join(ts.Dirs.Bin, f.Name()), filepath.Join(subBinDir, f.Name())) 190 suite.NoError(err) 191 } 192 193 stateExePath := filepath.Join(ts.Dirs.Bin, filepath.Base(ts.Exe)) 194 195 spawnOpts := []e2e.SpawnOptSetter{ 196 e2e.OptArgs("update"), 197 e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultInstallationPathEnvVarName, ts.Dirs.Bin)), 198 e2e.OptAppendEnv(suite.env(false, true)...), 199 } 200 201 searchA, searchB, found := "Updating State Tool to version", "Installing Update", false 202 for i := 0; i < 4; i++ { 203 if i > 0 { 204 time.Sleep(time.Second * 3) 205 } 206 207 cp := ts.SpawnCmdWithOpts(stateExePath, spawnOpts...) 208 cp.ExpectExitCode(0, termtest.OptExpectTimeout(time.Minute)) 209 210 snap := cp.Snapshot() 211 if strings.Contains(snap, searchA) && strings.Contains(snap, searchB) { 212 found = true 213 break 214 } 215 } 216 217 suite.Require().True(found, "Expecting to find %q and %q", searchA, searchB) 218 219 suite.NoFileExists(filepath.Join(ts.Dirs.Bin, constants.StateCmd+osutils.ExeExtension), "State Tool executable at install dir should no longer exist") 220 suite.NoFileExists(filepath.Join(ts.Dirs.Bin, constants.StateSvcCmd+osutils.ExeExtension), "State Service executable at install dir should no longer exist") 221 } 222 223 func (suite *UpdateIntegrationTestSuite) TestUpdateChannel() { 224 suite.OnlyRunForTags(tagsuite.Update, tagsuite.Critical) 225 226 tests := []struct { 227 Name string 228 Channel string 229 }{ 230 {"release-channel", "release"}, 231 {"specific-update", targetChannel}, 232 } 233 234 for _, tt := range tests { 235 suite.Run(tt.Name, func() { 236 // TODO: Update targetChannel and specificVersion after a v0.34.0 release 237 suite.T().Skip("Skipping these tests for now as the update changes need to be available in an older version of the state tool.") 238 ts := e2e.New(suite.T(), false) 239 defer ts.Close() 240 241 updateArgs := []string{"update", "--set-channel", tt.Channel} 242 env := []string{fmt.Sprintf("%s=%s", constants.OverwriteDefaultInstallationPathEnvVarName, ts.Dirs.Bin)} 243 env = append(env, suite.env(false, false)...) 244 cp := ts.SpawnWithOpts( 245 e2e.OptArgs(updateArgs...), 246 e2e.OptAppendEnv(env...), 247 ) 248 cp.Expect("Updating") 249 cp.ExpectExitCode(0, termtest.OptExpectTimeout(1*time.Minute)) 250 251 suite.channelCompare(ts, tt.Channel, suite.Equal) 252 }) 253 } 254 } 255 256 func (suite *UpdateIntegrationTestSuite) TestUpdateTags() { 257 // Disabled, waiting for - https://www.pivotaltracker.com/story/show/179646813 258 suite.T().Skip("Disabled for now") 259 suite.OnlyRunForTags(tagsuite.Update) 260 261 tests := []struct { 262 name string 263 tagged bool 264 expectSuccess bool 265 }{ 266 {"update-to-tag", false, true}, 267 {"update-with-tag", true, false}, 268 } 269 270 for _, tt := range tests { 271 suite.Run(tt.name, func() { 272 ts := e2e.New(suite.T(), false) 273 defer ts.Close() 274 }) 275 } 276 } 277 278 func TestUpdateIntegrationTestSuite(t *testing.T) { 279 if testing.Short() { 280 t.Skip("skipping integration test in short mode.") 281 } 282 suite.Run(t, new(UpdateIntegrationTestSuite)) 283 } 284 285 func lockedProjectURL() string { 286 return fmt.Sprintf("https://%s/string/string", constants.PlatformURL) 287 } 288 289 func (suite *UpdateIntegrationTestSuite) TestAutoUpdate() { 290 // suite.T().Skip("Test will not work until v0.34.0") 291 suite.OnlyRunForTags(tagsuite.Update, tagsuite.Critical) 292 293 ts := e2e.New(suite.T(), true) 294 defer ts.Close() 295 296 suite.testAutoUpdate(ts, filepath.Dir(ts.Dirs.Bin)) 297 } 298 299 func (suite *UpdateIntegrationTestSuite) testAutoUpdate(ts *e2e.Session, baseDir string, opts ...e2e.SpawnOptSetter) { 300 fakeHome := filepath.Join(ts.Dirs.Work, "home") 301 suite.Require().NoError(fileutils.Mkdir(fakeHome)) 302 303 spawnOpts := []e2e.SpawnOptSetter{ 304 e2e.OptArgs("--version"), 305 e2e.OptAppendEnv(suite.env(false, true)...), 306 e2e.OptAppendEnv(fmt.Sprintf("HOME=%s", fakeHome)), 307 } 308 if opts != nil { 309 spawnOpts = append(spawnOpts, opts...) 310 } 311 312 stateExec, err := installation.StateExecFromDir(baseDir) 313 suite.NoError(err) 314 315 search, found := "Updating State Tool", false 316 for i := 0; i < 4; i++ { 317 if i > 0 { 318 time.Sleep(time.Second * 4) 319 } 320 321 cp := ts.SpawnCmdWithOpts(stateExec, spawnOpts...) 322 cp.ExpectExitCode(0, termtest.OptExpectTimeout(time.Minute)) 323 324 if strings.Contains(cp.Snapshot(), search) { 325 found = true 326 break 327 } 328 } 329 330 suite.Require().True(found, "Expecting to find %q", search) 331 } 332 333 func (suite *UpdateIntegrationTestSuite) installLatestReleaseVersion(ts *e2e.Session, dir string) { 334 var cp *e2e.SpawnedCmd 335 if runtime.GOOS != "windows" { 336 oneLiner := fmt.Sprintf("sh <(curl -q https://platform.activestate.com/dl/cli/pdli01/install.sh) -f -n -t %s", dir) 337 cp = ts.SpawnCmdWithOpts( 338 "bash", e2e.OptArgs("-c", oneLiner), 339 ) 340 } else { 341 b, err := httputil.GetDirect("https://platform.activestate.com/dl/cli/pdli01/install.ps1") 342 suite.Require().NoError(err) 343 344 ps1File := filepath.Join(ts.Dirs.Work, "install.ps1") 345 suite.Require().NoError(fileutils.WriteFile(ps1File, b)) 346 347 cp = ts.SpawnCmdWithOpts("powershell.exe", e2e.OptArgs(ps1File, "-f", "-n", "-t", dir), 348 e2e.OptAppendEnv("SHELL="), 349 ) 350 } 351 cp.Expect("Installation Complete", termtest.OptExpectTimeout(5*time.Minute)) 352 353 stateExec, err := installation.StateExecFromDir(dir) 354 suite.NoError(err) 355 356 suite.FileExists(stateExec) 357 } 358 359 func (suite *UpdateIntegrationTestSuite) TestAutoUpdateToCurrent() { 360 suite.OnlyRunForTags(tagsuite.Update, tagsuite.Critical) 361 362 ts := e2e.New(suite.T(), true) 363 defer ts.Close() 364 365 installDir := filepath.Join(ts.Dirs.Work, "install") 366 err := fileutils.MkdirUnlessExists(installDir) 367 suite.NoError(err) 368 369 suite.installLatestReleaseVersion(ts, installDir) 370 371 suite.testAutoUpdate(ts, installDir, e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.UpdateChannelEnvVarName, constants.ChannelName))) 372 } 373 374 func (suite *UpdateIntegrationTestSuite) TestUpdateToCurrent() { 375 if strings.HasPrefix(constants.Version, "0.30") { 376 // Feel free to drop this once the release channel is no longer on 0.29 377 suite.T().Skip("Updating from release 0.29 to 0.30 is not covered due to how 0.29 did updates (async)") 378 } 379 suite.OnlyRunForTags(tagsuite.Update, tagsuite.Critical) 380 381 ts := e2e.New(suite.T(), true) 382 defer ts.Close() 383 384 installDir := filepath.Join(ts.Dirs.Work, "install") 385 err := fileutils.MkdirUnlessExists(installDir) 386 suite.Require().NoError(err) 387 388 suite.installLatestReleaseVersion(ts, installDir) 389 390 suite.testUpdate(ts, installDir, e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.UpdateChannelEnvVarName, constants.ChannelName))) 391 }