github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/cmd/state-svc/test/integration/svc_int_test.go (about) 1 package integration 2 3 import ( 4 "fmt" 5 "net" 6 "os" 7 "path/filepath" 8 "regexp" 9 "runtime" 10 "strings" 11 "syscall" 12 "testing" 13 "time" 14 15 "github.com/ActiveState/cli/internal/condition" 16 "github.com/ActiveState/cli/internal/constants" 17 "github.com/ActiveState/cli/internal/fileutils" 18 "github.com/ActiveState/cli/internal/logging" 19 "github.com/ActiveState/cli/internal/osutils" 20 "github.com/ActiveState/cli/internal/svcctl" 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 "github.com/shirou/gopsutil/v3/process" 25 ) 26 27 type SvcIntegrationTestSuite struct { 28 tagsuite.Suite 29 } 30 31 func (suite *SvcIntegrationTestSuite) TestStartStop() { 32 // Disable test until we can fix console output on Windows 33 // See issue here: https://activestatef.atlassian.net/browse/DX-1311 34 suite.T().SkipNow() 35 suite.OnlyRunForTags(tagsuite.Service) 36 ts := e2e.New(suite.T(), false) 37 defer ts.Close() 38 39 cp := ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("stop")) 40 cp.ExpectExitCode(0) 41 42 cp = ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("status")) 43 cp.Expect("Service cannot be reached") 44 cp.ExpectExitCode(1) 45 ts.IgnoreLogErrors() 46 47 cp = ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("start")) 48 cp.Expect("Starting") 49 cp.ExpectExitCode(0) 50 51 cp = ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("status")) 52 cp.Expect("Checking") 53 54 // Verify the server is running on its reported port. 55 cp.ExpectRe(`Port:\s+:\d+\s`) 56 portRe := regexp.MustCompile(`Port:\s+:(\d+)`) 57 port := portRe.FindStringSubmatch(cp.Output())[1] 58 _, err := net.Listen("tcp", "localhost:"+port) 59 suite.Error(err) 60 61 // Verify it created and wrote to its reported log file. 62 cp.ExpectRe(`Log:\s+(.+?\.log)`) 63 logRe := regexp.MustCompile(`Log:\s+(.+?\.log)`) 64 logFile := logRe.FindStringSubmatch(cp.Output())[1] 65 suite.True(fileutils.FileExists(logFile), "log file '"+logFile+"' does not exist") 66 suite.True(len(fileutils.ReadFileUnsafe(logFile)) > 0, "log file is empty") 67 68 cp.ExpectExitCode(0) 69 70 cp = ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("stop")) 71 cp.Expect("Stopping") 72 cp.ExpectExitCode(0) 73 time.Sleep(500 * time.Millisecond) // wait for service to stop 74 75 // Verify the port is free. 76 server, err := net.Listen("tcp", "localhost:"+port) 77 suite.NoError(err) 78 server.Close() 79 } 80 81 func (suite *SvcIntegrationTestSuite) TestSignals() { 82 if condition.OnCI() { 83 // https://activestatef.atlassian.net/browse/DX-964 84 // https://activestatef.atlassian.net/browse/DX-980 85 suite.T().Skip("Signal handling on CI is unstable and unreliable") 86 } 87 88 if runtime.GOOS == "windows" { 89 suite.T().Skip("Windows does not support signal sending.") 90 } 91 92 suite.OnlyRunForTags(tagsuite.Service) 93 ts := e2e.New(suite.T(), false) 94 ts.IgnoreLogErrors() 95 defer ts.Close() 96 97 // SIGINT (^C) 98 cp := ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("foreground")) 99 cp.Expect("Starting") 100 time.Sleep(1 * time.Second) // wait for the service to start up 101 err := cp.Cmd().Process.Signal(syscall.SIGINT) 102 suite.NoError(err) 103 cp.Expect("caught a signal: interrupt") 104 cp.ExpectNotExitCode(0) 105 106 cp = ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("status")) 107 cp.Expect("Service cannot be reached") 108 cp.ExpectExitCode(1) 109 110 sockFile := svcctl.NewIPCSockPathFromGlobals().String() 111 suite.False(fileutils.TargetExists(sockFile), "socket file was not deleted") 112 113 // SIGTERM 114 cp = ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("foreground")) 115 cp.Expect("Starting") 116 time.Sleep(1 * time.Second) // wait for the service to start up 117 err = cp.Cmd().Process.Signal(syscall.SIGTERM) 118 suite.NoError(err) 119 suite.NotContains(cp.Output(), "caught a signal") 120 cp.ExpectExitCode(0) // should exit gracefully 121 122 cp = ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("status")) 123 cp.Expect("Service cannot be reached") 124 cp.ExpectExitCode(1) 125 126 suite.False(fileutils.TargetExists(sockFile), "socket file was not deleted") 127 } 128 129 func (suite *SvcIntegrationTestSuite) TestStartDuplicateErrorOutput() { 130 // https://activestatef.atlassian.net/browse/DX-1136 131 suite.OnlyRunForTags(tagsuite.Service) 132 if runtime.GOOS == "windows" { 133 suite.T().Skip("Windows doesn't seem to read from svc at the moment") 134 } 135 136 ts := e2e.New(suite.T(), false) 137 defer ts.Close() 138 139 cp := ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("stop")) 140 cp.ExpectExitCode(0) 141 142 cp = ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("status")) 143 cp.ExpectNotExitCode(0) 144 145 cp = ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("start")) 146 cp.ExpectExitCode(0) 147 148 cp = ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("foreground")) 149 cp.Expect("An existing server instance appears to be in use") 150 cp.ExpectExitCode(1) 151 ts.IgnoreLogErrors() 152 153 cp = ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("stop")) 154 cp.ExpectExitCode(0) 155 } 156 157 func (suite *SvcIntegrationTestSuite) TestSingleSvc() { 158 suite.OnlyRunForTags(tagsuite.Service) 159 ts := e2e.New(suite.T(), false) 160 defer ts.Close() 161 162 ts.SpawnCmdWithOpts(ts.SvcExe, e2e.OptArgs("stop")) 163 time.Sleep(2 * time.Second) // allow for some time to stop the existing available process 164 165 oldCount := suite.GetNumStateSvcProcesses() // may be non-zero due to non-test state-svc processes (using different sock file) 166 for i := 1; i <= 10; i++ { 167 go ts.SpawnCmdWithOpts(ts.Exe, e2e.OptArgs("--version")) 168 time.Sleep(50 * time.Millisecond) // do not spam CPU 169 } 170 time.Sleep(2 * time.Second) // allow for some time to spawn the processes 171 172 for attempts := 100; attempts > 0; attempts-- { 173 suite.T().Log("iters left:", attempts, "procs:", suite.GetNumStateSvcProcesses()) 174 if suite.GetNumStateSvcProcesses() == oldCount+1 { 175 break 176 } 177 time.Sleep(2 * time.Second) // keep waiting 178 } 179 180 newCount := suite.GetNumStateSvcProcesses() 181 if newCount > oldCount+1 { 182 // We only care if we end up with more services than anticipated. We can actually end up with less than we started 183 // with due to other integration tests not always waiting for state-svc to have fully shut down before running the next test 184 suite.Fail(fmt.Sprintf("spawning multiple state processes should only result in one more state-svc process at most, newCount: %d, oldCount: %d", newCount, oldCount)) 185 } 186 } 187 188 func (suite *SvcIntegrationTestSuite) GetNumStateSvcProcesses() int { 189 procs, err := process.Processes() 190 suite.NoError(err) 191 192 count := 0 193 for _, p := range procs { 194 if name, err := p.Name(); err == nil { 195 name = filepath.Base(name) // just in case an absolute path is returned 196 if svcName := constants.ServiceCommandName + osutils.ExeExtension; name == svcName { 197 count++ 198 } 199 } 200 } 201 202 return count 203 } 204 205 func (suite *SvcIntegrationTestSuite) TestAutostartConfigEnableDisable() { 206 suite.OnlyRunForTags(tagsuite.Service) 207 ts := e2e.New(suite.T(), false) 208 defer ts.Close() 209 210 // Toggle it via state tool config. 211 cp := ts.SpawnWithOpts( 212 e2e.OptArgs("config", "set", constants.AutostartSvcConfigKey, "false"), 213 ) 214 cp.ExpectExitCode(0) 215 cp = ts.SpawnWithOpts(e2e.OptArgs("config", "get", constants.AutostartSvcConfigKey)) 216 cp.Expect("false") 217 cp.ExpectExitCode(0) 218 219 // Toggle it again via state tool config. 220 cp = ts.SpawnWithOpts( 221 e2e.OptArgs("config", "set", constants.AutostartSvcConfigKey, "true"), 222 ) 223 cp.ExpectExitCode(0) 224 cp = ts.SpawnWithOpts(e2e.OptArgs("config", "get", constants.AutostartSvcConfigKey)) 225 cp.Expect("true") 226 cp.ExpectExitCode(0) 227 } 228 229 func (suite *SvcIntegrationTestSuite) TestLogRotation() { 230 suite.OnlyRunForTags(tagsuite.Service) 231 ts := e2e.New(suite.T(), true) 232 defer ts.Close() 233 234 logDir := filepath.Join(ts.Dirs.Config, "logs") 235 236 // Create a tranche of 30-day old dummy log files. 237 numFooFiles := 50 238 thirtyDaysOld := time.Now().Add(-24 * time.Hour * 30) 239 for i := 1; i <= numFooFiles; i++ { 240 logFile := filepath.Join(logDir, fmt.Sprintf("foo-%d%s", i, logging.FileNameSuffix)) 241 err := fileutils.Touch(logFile) 242 suite.Require().NoError(err, "could not create dummy log file") 243 err = os.Chtimes(logFile, thirtyDaysOld, thirtyDaysOld) 244 suite.Require().NoError(err, "must be able to change file modification times") 245 } 246 247 // Override state-svc log rotation interval from 1 minute to 4 seconds for this test. 248 logRotateInterval := 4 * time.Second 249 os.Setenv(constants.SvcLogRotateIntervalEnvVarName, fmt.Sprintf("%d", logRotateInterval.Milliseconds())) 250 defer os.Unsetenv(constants.SvcLogRotateIntervalEnvVarName) 251 252 // We want the side-effect of spawning state-svc. 253 cp := ts.Spawn("--version") 254 cp.Expect("ActiveState CLI") 255 cp.ExpectExitCode(0) 256 257 initialWait := 2 * time.Second 258 time.Sleep(initialWait) // wait for state-svc to perform initial log rotation 259 260 // Verify the log rotation pruned the dummy log files. 261 files, err := os.ReadDir(logDir) 262 suite.Require().NoError(err) 263 remainingFooFiles := 0 264 for _, file := range files { 265 if strings.HasPrefix(file.Name(), "foo-") { 266 remainingFooFiles++ 267 } 268 } 269 suite.Less(remainingFooFiles, numFooFiles, "no foo.log files were cleaned up; expected at least one to be") 270 271 // state-svc is still running, along with its log rotation timer. 272 // Re-create another tranche of 30-day old dummy log files for when the timer fires again. 273 numFooFiles += remainingFooFiles 274 for i := remainingFooFiles + 1; i <= numFooFiles; i++ { 275 logFile := filepath.Join(logDir, fmt.Sprintf("foo-%d%s", i, logging.FileNameSuffix)) 276 err := fileutils.Touch(logFile) 277 suite.Require().NoError(err, "could not create dummy log file") 278 err = os.Chtimes(logFile, thirtyDaysOld, thirtyDaysOld) 279 suite.Require().NoError(err, "must be able to change file modification times") 280 } 281 282 time.Sleep(logRotateInterval - initialWait) // wait for another log rotation 283 284 // Verify that another log rotation pruned the dummy log files. 285 files, err = os.ReadDir(logDir) 286 suite.Require().NoError(err) 287 remainingFooFiles = 0 288 for _, file := range files { 289 if strings.HasPrefix(file.Name(), "foo-") { 290 remainingFooFiles++ 291 } 292 } 293 suite.Less(remainingFooFiles, numFooFiles, "no more foo.log files were cleaned up (on timer); expected at least one to be") 294 } 295 296 func TestSvcIntegrationTestSuite(t *testing.T) { 297 suite.Run(t, new(SvcIntegrationTestSuite)) 298 }