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  }