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  }