github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/test/integration/install_scripts_int_test.go (about)

     1  package integration
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"path/filepath"
     7  	"runtime"
     8  	"testing"
     9  
    10  	"github.com/ActiveState/cli/internal/testhelpers/suite"
    11  	"github.com/stretchr/testify/require"
    12  	"github.com/thoas/go-funk"
    13  
    14  	anaConst "github.com/ActiveState/cli/internal/analytics/constants"
    15  	"github.com/ActiveState/cli/internal/condition"
    16  	"github.com/ActiveState/cli/internal/constants"
    17  	"github.com/ActiveState/cli/internal/environment"
    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/testhelpers/e2e"
    23  	"github.com/ActiveState/cli/internal/testhelpers/tagsuite"
    24  )
    25  
    26  type InstallScriptsIntegrationTestSuite struct {
    27  	tagsuite.Suite
    28  }
    29  
    30  func (suite *InstallScriptsIntegrationTestSuite) TestInstall() {
    31  	suite.OnlyRunForTags(tagsuite.InstallScripts, tagsuite.Critical)
    32  
    33  	tests := []struct {
    34  		Name              string
    35  		Version           string
    36  		Channel           string
    37  		Activate          string
    38  		ActivateByCommand string
    39  	}{
    40  		// {"install-release-latest", "", "release", "", ""},
    41  		{"install-prbranch", "", "", "", ""},
    42  		{"install-prbranch-with-version", constants.Version, constants.ChannelName, "", ""},
    43  		{"install-prbranch-and-activate", "", constants.ChannelName, "ActiveState-CLI/small-python", ""},
    44  		{"install-prbranch-and-activate-by-command", "", constants.ChannelName, "", "ActiveState-CLI/small-python"},
    45  	}
    46  
    47  	for _, tt := range tests {
    48  		suite.Run(fmt.Sprintf("%s (%s@%s)", tt.Name, tt.Version, tt.Channel), func() {
    49  			ts := e2e.New(suite.T(), false)
    50  			defer ts.Close()
    51  
    52  			// Determine URL of install script.
    53  			baseUrl := "https://state-tool.s3.amazonaws.com/update/state/"
    54  			scriptBaseName := "install."
    55  			if runtime.GOOS != "windows" {
    56  				scriptBaseName += "sh"
    57  			} else {
    58  				scriptBaseName += "ps1"
    59  			}
    60  			scriptUrl := baseUrl + constants.ChannelName + "/" + scriptBaseName
    61  
    62  			// Fetch it.
    63  			b, err := httputil.GetDirect(scriptUrl)
    64  			suite.Require().NoError(err)
    65  			script := filepath.Join(ts.Dirs.Work, scriptBaseName)
    66  			suite.Require().NoError(fileutils.WriteFile(script, b))
    67  
    68  			// Construct installer command to execute.
    69  			installDir := filepath.Join(ts.Dirs.Work, "install")
    70  			argsPlain := []string{script}
    71  			argsPlain = append(argsPlain, "-t", installDir)
    72  			argsPlain = append(argsPlain, "-n")
    73  			if tt.Channel != "" {
    74  				argsPlain = append(argsPlain, "-b", tt.Channel)
    75  			}
    76  			if tt.Version != "" {
    77  				argsPlain = append(argsPlain, "-v", tt.Version)
    78  			}
    79  
    80  			argsWithActive := append(argsPlain, "-f")
    81  			if tt.Activate != "" {
    82  				argsWithActive = append(argsWithActive, "--activate", tt.Activate)
    83  			}
    84  			if tt.ActivateByCommand != "" {
    85  				cmd := fmt.Sprintf("state activate %s", tt.ActivateByCommand)
    86  				if runtime.GOOS == "windows" {
    87  					cmd = "'" + cmd + "'"
    88  				}
    89  				argsWithActive = append(argsWithActive, "-c", cmd)
    90  			}
    91  
    92  			// Make the directory to install to.
    93  			appInstallDir := filepath.Join(ts.Dirs.Work, "app")
    94  			suite.NoError(fileutils.Mkdir(appInstallDir))
    95  
    96  			// Perform the installation.
    97  			cmd := "bash"
    98  			opts := []e2e.SpawnOptSetter{
    99  				e2e.OptArgs(argsWithActive...),
   100  				e2e.OptAppendEnv(constants.DisableRuntime + "=false"),
   101  				e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.AppInstallDirOverrideEnvVarName, appInstallDir)),
   102  				e2e.OptAppendEnv(fmt.Sprintf("%s=FOO", constants.OverrideSessionTokenEnvVarName)),
   103  				e2e.OptAppendEnv(fmt.Sprintf("%s=false", constants.DisableActivateEventsEnvVarName)),
   104  			}
   105  			if runtime.GOOS == "windows" {
   106  				cmd = "powershell.exe"
   107  				opts = append(opts, e2e.OptAppendEnv("SHELL="))
   108  			}
   109  			cp := ts.SpawnCmdWithOpts(cmd, opts...)
   110  			cp.Expect("Preparing Installer for State Tool Package Manager")
   111  			cp.Expect("Installation Complete", e2e.RuntimeSourcingTimeoutOpt)
   112  
   113  			if tt.Activate != "" || tt.ActivateByCommand != "" {
   114  				cp.Expect("Creating a Virtual Environment")
   115  				cp.Expect("Quick Start", e2e.RuntimeSourcingTimeoutOpt)
   116  				// ensure that shell is functional
   117  				cp.ExpectInput()
   118  
   119  				cp.SendLine("python3 -c \"import sys; print(sys.copyright)\"")
   120  				cp.Expect("ActiveState")
   121  			}
   122  
   123  			cp.SendLine("state --version")
   124  			cp.Expect("Version " + constants.Version)
   125  			cp.Expect("Channel " + constants.ChannelName)
   126  			cp.Expect("Built")
   127  			cp.SendLine("exit")
   128  
   129  			cp.ExpectExitCode(0)
   130  
   131  			stateExec, err := installation.StateExecFromDir(installDir)
   132  			suite.NoError(err)
   133  			suite.FileExists(stateExec)
   134  
   135  			suite.assertBinDirContents(filepath.Join(installDir, "bin"))
   136  			suite.assertCorrectVersion(ts, installDir, tt.Version, tt.Channel)
   137  			suite.assertAnalytics(ts)
   138  			suite.DirExists(ts.Dirs.Config)
   139  
   140  			// Verify that can install overtop
   141  			if runtime.GOOS != "windows" {
   142  				cp = ts.SpawnCmdWithOpts("bash", e2e.OptArgs(argsPlain...))
   143  			} else {
   144  				cp = ts.SpawnCmdWithOpts("powershell.exe", e2e.OptArgs(argsPlain...),
   145  					e2e.OptAppendEnv("SHELL="),
   146  				)
   147  			}
   148  			cp.Expect("successfully installed")
   149  			cp.ExpectInput()
   150  			cp.SendLine("exit")
   151  			cp.ExpectExitCode(0)
   152  			if runtime.GOOS == "windows" {
   153  				ts.IgnoreLogErrors() // Follow-up DX-2678
   154  			}
   155  		})
   156  	}
   157  }
   158  
   159  func (suite *InstallScriptsIntegrationTestSuite) TestInstall_NonEmptyTarget() {
   160  	suite.OnlyRunForTags(tagsuite.InstallScripts)
   161  	ts := e2e.New(suite.T(), false)
   162  	defer ts.Close()
   163  
   164  	script := scriptPath(suite.T(), ts.Dirs.Work)
   165  	argsPlain := []string{script, "-t", ts.Dirs.Work, "-n"}
   166  	argsPlain = append(argsPlain, "-b", constants.ChannelName)
   167  	var cp *e2e.SpawnedCmd
   168  	if runtime.GOOS != "windows" {
   169  		cp = ts.SpawnCmdWithOpts("bash", e2e.OptArgs(argsPlain...))
   170  	} else {
   171  		cp = ts.SpawnCmdWithOpts("powershell.exe", e2e.OptArgs(argsPlain...), e2e.OptAppendEnv("SHELL="))
   172  	}
   173  	cp.Expect("Installation path must be an empty directory")
   174  
   175  	// Originally this was ExpectExitCode(1), but particularly on Windows this turned out to be unreliable. Probably
   176  	// because of powershell.
   177  	// Since we asserted the expected error above we don't need to go on a wild goose chase here.
   178  	cp.ExpectExit()
   179  	ts.IgnoreLogErrors()
   180  }
   181  
   182  func (suite *InstallScriptsIntegrationTestSuite) TestInstall_VersionDoesNotExist() {
   183  	suite.OnlyRunForTags(tagsuite.InstallScripts)
   184  	ts := e2e.New(suite.T(), false)
   185  	defer ts.Close()
   186  
   187  	script := scriptPath(suite.T(), ts.Dirs.Work)
   188  	args := []string{script, "-t", ts.Dirs.Work, "-n"}
   189  	args = append(args, "-v", "does-not-exist")
   190  	var cp *e2e.SpawnedCmd
   191  	if runtime.GOOS != "windows" {
   192  		cp = ts.SpawnCmdWithOpts("bash", e2e.OptArgs(args...))
   193  	} else {
   194  		cp = ts.SpawnCmdWithOpts("powershell.exe", e2e.OptArgs(args...), e2e.OptAppendEnv("SHELL="))
   195  	}
   196  	if !condition.OnCI() || runtime.GOOS == "windows" {
   197  		// For some reason on Linux and macOS, there is no terminal output on CI. It works locally though.
   198  		cp.Expect("Could not download")
   199  	}
   200  	cp.ExpectExitCode(1)
   201  	ts.IgnoreLogErrors()
   202  }
   203  
   204  // scriptPath returns the path to an installation script copied to targetDir, if useTestUrl is true, the install script is modified to download from the local test server instead
   205  func scriptPath(t *testing.T, targetDir string) string {
   206  	ext := ".ps1"
   207  	if runtime.GOOS != "windows" {
   208  		ext = ".sh"
   209  	}
   210  	name := "install" + ext
   211  	root := environment.GetRootPathUnsafe()
   212  	subdir := "installers"
   213  
   214  	source := filepath.Join(root, subdir, name)
   215  	if !fileutils.FileExists(source) {
   216  		t.Fatalf("Could not find install script %s", source)
   217  	}
   218  
   219  	target := filepath.Join(targetDir, filepath.Base(source))
   220  	err := fileutils.CopyFile(source, target)
   221  	require.NoError(t, err)
   222  
   223  	return target
   224  }
   225  
   226  // assertBinDirContents checks if given files are or are not in the bin directory
   227  func (suite *InstallScriptsIntegrationTestSuite) assertBinDirContents(dir string) {
   228  	binFiles := suite.listFilesOnly(dir)
   229  	suite.Contains(binFiles, "state"+osutils.ExeExtension)
   230  	suite.Contains(binFiles, "state-svc"+osutils.ExeExtension)
   231  }
   232  
   233  // listFilesOnly is a helper function for assertBinDirContents filtering a directory recursively for base filenames
   234  // It allows for simple and coarse checks if a file exists or does not exist in the directory structure
   235  func (suite *InstallScriptsIntegrationTestSuite) listFilesOnly(dir string) []string {
   236  	files, err := fileutils.ListDirSimple(dir, true)
   237  	suite.Require().NoError(err)
   238  	files = funk.Filter(files, func(f string) bool {
   239  		return !fileutils.IsDir(f)
   240  	}).([]string)
   241  	return funk.Map(files, filepath.Base).([]string)
   242  }
   243  
   244  func (suite *InstallScriptsIntegrationTestSuite) assertCorrectVersion(ts *e2e.Session, installDir, expectedVersion, expectedChannel string) {
   245  	type versionData struct {
   246  		Version string `json:"version"`
   247  		Channel string `json:"channel"`
   248  	}
   249  
   250  	stateExec, err := installation.StateExecFromDir(installDir)
   251  	suite.NoError(err)
   252  
   253  	cp := ts.SpawnCmd(stateExec, "--version", "--output=json")
   254  	cp.ExpectExitCode(0)
   255  	actual := versionData{}
   256  	out := cp.StrippedSnapshot()
   257  	suite.Require().NoError(json.Unmarshal([]byte(out), &actual))
   258  
   259  	if expectedVersion != "" {
   260  		suite.Equal(expectedVersion, actual.Version)
   261  	}
   262  	if expectedChannel != "" {
   263  		suite.Equal(expectedChannel, actual.Channel)
   264  	}
   265  }
   266  
   267  func (suite *InstallScriptsIntegrationTestSuite) assertAnalytics(ts *e2e.Session) {
   268  	// Verify analytics reported a non-empty sessionToken.
   269  	sessionTokenFound := false
   270  	events := parseAnalyticsEvents(suite, ts)
   271  	suite.Require().NotEmpty(events)
   272  	for _, event := range events {
   273  		if event.Category == anaConst.CatInstallerFunnel && event.Dimensions != nil {
   274  			suite.Assert().NotEmpty(*event.Dimensions.SessionToken)
   275  			sessionTokenFound = true
   276  			break
   277  		}
   278  	}
   279  	suite.Assert().True(sessionTokenFound, "sessionToken was not found in analytics")
   280  }
   281  
   282  func TestInstallScriptsIntegrationTestSuite(t *testing.T) {
   283  	suite.Run(t, new(InstallScriptsIntegrationTestSuite))
   284  }