github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/testhelpers/e2e/session.go (about)

     1  package e2e
     2  
     3  import (
     4  	"fmt"
     5  	"io/fs"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"regexp"
    10  	"runtime"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/ActiveState/cli/internal/subshell"
    16  	"github.com/ActiveState/cli/pkg/projectfile"
    17  	"github.com/ActiveState/termtest"
    18  	"github.com/go-openapi/strfmt"
    19  	"github.com/google/uuid"
    20  	"github.com/phayes/permbits"
    21  	"github.com/stretchr/testify/require"
    22  
    23  	"github.com/ActiveState/cli/internal/condition"
    24  	"github.com/ActiveState/cli/internal/config"
    25  	"github.com/ActiveState/cli/internal/constants"
    26  	"github.com/ActiveState/cli/internal/environment"
    27  	"github.com/ActiveState/cli/internal/errs"
    28  	"github.com/ActiveState/cli/internal/fileutils"
    29  	"github.com/ActiveState/cli/internal/installation"
    30  	"github.com/ActiveState/cli/internal/logging"
    31  	"github.com/ActiveState/cli/internal/osutils"
    32  	"github.com/ActiveState/cli/internal/osutils/stacktrace"
    33  	"github.com/ActiveState/cli/internal/rtutils/singlethread"
    34  	"github.com/ActiveState/cli/internal/strutils"
    35  	"github.com/ActiveState/cli/internal/subshell/bash"
    36  	"github.com/ActiveState/cli/internal/subshell/sscommon"
    37  	"github.com/ActiveState/cli/internal/testhelpers/tagsuite"
    38  	"github.com/ActiveState/cli/pkg/platform/api"
    39  	"github.com/ActiveState/cli/pkg/platform/api/mono"
    40  	"github.com/ActiveState/cli/pkg/platform/api/mono/mono_client/users"
    41  	"github.com/ActiveState/cli/pkg/platform/api/mono/mono_models"
    42  	"github.com/ActiveState/cli/pkg/platform/authentication"
    43  	"github.com/ActiveState/cli/pkg/platform/model"
    44  	"github.com/ActiveState/cli/pkg/project"
    45  )
    46  
    47  // Session represents an end-to-end testing session during which several console process can be spawned and tested
    48  // It provides a consistent environment (environment variables and temporary
    49  // directories) that is shared by processes spawned during this session.
    50  // The session is approximately the equivalent of a terminal session, with the
    51  // main difference processes in this session are not spawned by a shell.
    52  type Session struct {
    53  	Dirs            *Dirs
    54  	Env             []string
    55  	retainDirs      bool
    56  	createdProjects []*project.Namespaced
    57  	// users created during session
    58  	users           []string
    59  	T               *testing.T
    60  	Exe             string
    61  	SvcExe          string
    62  	ExecutorExe     string
    63  	spawned         []*SpawnedCmd
    64  	ignoreLogErrors bool
    65  }
    66  
    67  var (
    68  	PersistentUsername string
    69  	PersistentPassword string
    70  	PersistentToken    string
    71  
    72  	defaultTimeout         = 40 * time.Second
    73  	RuntimeSourcingTimeout = 3 * time.Minute
    74  )
    75  
    76  func init() {
    77  	PersistentUsername = os.Getenv("INTEGRATION_TEST_USERNAME")
    78  	PersistentPassword = os.Getenv("INTEGRATION_TEST_PASSWORD")
    79  	PersistentToken = os.Getenv("INTEGRATION_TEST_TOKEN")
    80  
    81  	// Get username / password from `state secrets` so we can run tests without needing special env setup
    82  	if PersistentUsername == "" {
    83  		out, stderr, err := osutils.ExecSimpleFromDir(environment.GetRootPathUnsafe(), "state", []string{"secrets", "get", "project.INTEGRATION_TEST_USERNAME"}, []string{})
    84  		if err != nil {
    85  			fmt.Printf("WARNING!!! Could not retrieve username via state secrets: %v, stdout/stderr: %v\n%v\n", err, out, stderr)
    86  		}
    87  		PersistentUsername = strings.TrimSpace(out)
    88  	}
    89  	if PersistentPassword == "" {
    90  		out, stderr, err := osutils.ExecSimpleFromDir(environment.GetRootPathUnsafe(), "state", []string{"secrets", "get", "project.INTEGRATION_TEST_PASSWORD"}, []string{})
    91  		if err != nil {
    92  			fmt.Printf("WARNING!!! Could not retrieve password via state secrets: %v, stdout/stderr: %v\n%v\n", err, out, stderr)
    93  		}
    94  		PersistentPassword = strings.TrimSpace(out)
    95  	}
    96  	if PersistentToken == "" {
    97  		out, stderr, err := osutils.ExecSimpleFromDir(environment.GetRootPathUnsafe(), "state", []string{"secrets", "get", "project.INTEGRATION_TEST_TOKEN"}, []string{})
    98  		if err != nil {
    99  			fmt.Printf("WARNING!!! Could not retrieve token via state secrets: %v, stdout/stderr: %v\n%v\n", err, out, stderr)
   100  		}
   101  		PersistentToken = strings.TrimSpace(out)
   102  	}
   103  
   104  	if PersistentUsername == "" || PersistentPassword == "" || PersistentToken == "" {
   105  		fmt.Println("WARNING!!! Environment variables INTEGRATION_TEST_USERNAME, INTEGRATION_TEST_PASSWORD INTEGRATION_TEST_TOKEN and should be defined!")
   106  	}
   107  
   108  }
   109  
   110  // ExecutablePath returns the path to the state tool that we want to test
   111  func (s *Session) ExecutablePath() string {
   112  	return s.Exe
   113  }
   114  
   115  func (s *Session) CopyExeToDir(from, to string) string {
   116  	var err error
   117  	to, err = filepath.Abs(filepath.Join(to, filepath.Base(from)))
   118  	if err != nil {
   119  		s.T.Fatal(err)
   120  	}
   121  	if fileutils.TargetExists(to) {
   122  		return to
   123  	}
   124  
   125  	err = fileutils.CopyFile(from, to)
   126  	require.NoError(s.T, err, "Could not copy %s to %s", from, to)
   127  
   128  	// Ensure modTime is the same as source exe
   129  	stat, err := os.Stat(from)
   130  	require.NoError(s.T, err)
   131  	t := stat.ModTime()
   132  	require.NoError(s.T, os.Chtimes(to, t, t))
   133  
   134  	permissions, _ := permbits.Stat(to)
   135  	permissions.SetUserExecute(true)
   136  	require.NoError(s.T, permbits.Chmod(to, permissions))
   137  	return to
   138  }
   139  
   140  func (s *Session) copyExeToBinDir(executable string) string {
   141  	return s.CopyExeToDir(executable, s.Dirs.Bin)
   142  }
   143  
   144  // executablePaths returns the paths to the executables that we want to test
   145  func executablePaths(t *testing.T) (string, string, string) {
   146  	root := environment.GetRootPathUnsafe()
   147  	buildDir := fileutils.Join(root, "build")
   148  
   149  	stateExec := filepath.Join(buildDir, constants.StateCmd+osutils.ExeExtension)
   150  	svcExec := filepath.Join(buildDir, constants.StateSvcCmd+osutils.ExeExtension)
   151  	executorExec := filepath.Join(buildDir, constants.StateExecutorCmd+osutils.ExeExtension)
   152  
   153  	if !fileutils.FileExists(stateExec) {
   154  		t.Fatal("E2E tests require a State Tool binary. Run `state run build`.")
   155  	}
   156  	if !fileutils.FileExists(svcExec) {
   157  		t.Fatal("E2E tests require a state-svc binary. Run `state run build-svc`.")
   158  	}
   159  	if !fileutils.FileExists(executorExec) {
   160  		t.Fatal("E2E tests require a state-exec binary. Run `state run build-exec`.")
   161  	}
   162  
   163  	return stateExec, svcExec, executorExec
   164  }
   165  
   166  func New(t *testing.T, retainDirs bool, extraEnv ...string) *Session {
   167  	return new(t, retainDirs, true, extraEnv...)
   168  }
   169  
   170  func new(t *testing.T, retainDirs, updatePath bool, extraEnv ...string) *Session {
   171  	dirs, err := NewDirs("")
   172  	require.NoError(t, err)
   173  	env := sandboxedTestEnvironment(t, dirs, updatePath, extraEnv...)
   174  
   175  	session := &Session{Dirs: dirs, Env: env, retainDirs: retainDirs, T: t}
   176  
   177  	// Mock installation directory
   178  	exe, svcExe, execExe := executablePaths(t)
   179  	session.Exe = session.copyExeToBinDir(exe)
   180  	session.SvcExe = session.copyExeToBinDir(svcExe)
   181  	session.ExecutorExe = session.copyExeToBinDir(execExe)
   182  
   183  	err = fileutils.Touch(filepath.Join(dirs.Base, installation.InstallDirMarker))
   184  	require.NoError(session.T, err)
   185  
   186  	cfg, err := config.New()
   187  	require.NoError(session.T, err)
   188  
   189  	if err := cfg.Set(constants.SecurityPromptConfig, false); err != nil {
   190  		require.NoError(session.T, err)
   191  	}
   192  
   193  	return session
   194  }
   195  
   196  func NewNoPathUpdate(t *testing.T, retainDirs bool, extraEnv ...string) *Session {
   197  	return new(t, retainDirs, false, extraEnv...)
   198  }
   199  
   200  func (s *Session) SetT(t *testing.T) {
   201  	s.T = t
   202  }
   203  
   204  func (s *Session) ClearCache() error {
   205  	return os.RemoveAll(s.Dirs.Cache)
   206  }
   207  
   208  // Spawn spawns the state tool executable to be tested with arguments
   209  func (s *Session) Spawn(args ...string) *SpawnedCmd {
   210  	return s.SpawnCmdWithOpts(s.Exe, OptArgs(args...))
   211  }
   212  
   213  // SpawnWithOpts spawns the state tool executable to be tested with arguments
   214  func (s *Session) SpawnWithOpts(opts ...SpawnOptSetter) *SpawnedCmd {
   215  	return s.SpawnCmdWithOpts(s.Exe, opts...)
   216  }
   217  
   218  // SpawnCmd executes an executable in a pseudo-terminal for integration tests
   219  func (s *Session) SpawnCmd(cmdName string, args ...string) *SpawnedCmd {
   220  	return s.SpawnCmdWithOpts(cmdName, OptArgs(args...))
   221  }
   222  
   223  // SpawnShellWithOpts spawns the given shell and options in interactive mode.
   224  func (s *Session) SpawnShellWithOpts(shell Shell, opts ...SpawnOptSetter) *SpawnedCmd {
   225  	if shell != Cmd {
   226  		opts = append(opts, OptAppendEnv("SHELL="+string(shell)))
   227  	}
   228  	opts = append(opts, OptRunInsideShell(false))
   229  	return s.SpawnCmdWithOpts(string(shell), opts...)
   230  }
   231  
   232  // SpawnCmdWithOpts executes an executable in a pseudo-terminal for integration tests
   233  // Arguments and other parameters can be specified by specifying SpawnOptSetter
   234  func (s *Session) SpawnCmdWithOpts(exe string, optSetters ...SpawnOptSetter) *SpawnedCmd {
   235  	spawnOpts := NewSpawnOpts()
   236  	spawnOpts.Env = s.Env
   237  	spawnOpts.Dir = s.Dirs.Work
   238  
   239  	spawnOpts.TermtestOpts = append(spawnOpts.TermtestOpts,
   240  		termtest.OptErrorHandler(func(tt *termtest.TermTest, err error) error {
   241  			s.T.Fatal(s.DebugMessage(errs.JoinMessage(err)))
   242  			return err
   243  		}),
   244  		termtest.OptDefaultTimeout(defaultTimeout),
   245  		termtest.OptCols(140),
   246  		termtest.OptRows(30), // Needs to be able to accommodate most JSON output
   247  	)
   248  
   249  	// TTYs output newlines in two steps: '\r' (CR) to move the caret to the beginning of the line,
   250  	// and '\n' (LF) to move the caret one line down. Terminal emulators do the same thing, so the
   251  	// raw terminal output will contain "\r\n". Since our multi-line expectation messages often use
   252  	// '\n', normalize line endings to that for convenience, regardless of platform ('\n' for Linux
   253  	// and macOS, "\r\n" for Windows).
   254  	// More info: https://superuser.com/a/1774370
   255  	spawnOpts.TermtestOpts = append(spawnOpts.TermtestOpts,
   256  		termtest.OptNormalizedLineEnds(true),
   257  	)
   258  
   259  	for _, optSet := range optSetters {
   260  		optSet(&spawnOpts)
   261  	}
   262  
   263  	var shell string
   264  	var args []string
   265  	if spawnOpts.RunInsideShell {
   266  		switch runtime.GOOS {
   267  		case "windows":
   268  			shell = Cmd
   269  			// /C = next argument is command that will be ran
   270  			args = []string{"/C"}
   271  		case "darwin":
   272  			shell = "zsh"
   273  			// -i = interactive mode
   274  			// -c = next argument is command that will be ran
   275  			args = []string{"-i", "-c"}
   276  		default:
   277  			shell = "bash"
   278  			args = []string{"-i", "-c"}
   279  		}
   280  		if len(spawnOpts.Args) == 0 {
   281  			args = append(args, fmt.Sprintf(`"%s"`, exe))
   282  		} else {
   283  			if shell == Cmd {
   284  				aa := spawnOpts.Args
   285  				for i, a := range aa {
   286  					aa[i] = strings.ReplaceAll(a, " ", "^ ")
   287  				}
   288  				// Windows is weird, it doesn't seem to let you quote arguments, so instead we need to escape spaces
   289  				// which on Windows is done with the '^' character.
   290  				args = append(args, fmt.Sprintf(`%s %s`, strings.ReplaceAll(exe, " ", "^ "), strings.Join(aa, " ")))
   291  			} else {
   292  				args = append(args, fmt.Sprintf(`"%s" "%s"`, exe, strings.Join(spawnOpts.Args, `" "`)))
   293  			}
   294  		}
   295  	} else {
   296  		shell = exe
   297  		args = spawnOpts.Args
   298  	}
   299  
   300  	cmd := exec.Command(shell, args...)
   301  
   302  	cmd.Env = spawnOpts.Env
   303  	if spawnOpts.Dir != "" {
   304  		cmd.Dir = spawnOpts.Dir
   305  	}
   306  
   307  	tt, err := termtest.New(cmd, spawnOpts.TermtestOpts...)
   308  	require.NoError(s.T, err)
   309  
   310  	spawn := &SpawnedCmd{tt, spawnOpts}
   311  
   312  	s.spawned = append(s.spawned, spawn)
   313  
   314  	cmdArgs := spawnOpts.Args
   315  	if spawnOpts.HideCmdArgs {
   316  		cmdArgs = []string{"<hidden>"}
   317  	}
   318  	logging.Debug("Spawning CMD: %s, args: %v", exe, cmdArgs)
   319  
   320  	return spawn
   321  }
   322  
   323  // PrepareActiveStateYAML creates an activestate.yaml in the session's work directory from the
   324  // given YAML contents.
   325  func (s *Session) PrepareActiveStateYAML(contents string) {
   326  	require.NoError(s.T, fileutils.WriteFile(filepath.Join(s.Dirs.Work, constants.ConfigFileName), []byte(contents)))
   327  }
   328  
   329  func (s *Session) PrepareCommitIdFile(commitID string) {
   330  	pjfile, err := projectfile.Parse(filepath.Join(s.Dirs.Work, constants.ConfigFileName))
   331  	require.NoError(s.T, err)
   332  	require.NoError(s.T, pjfile.SetLegacyCommit(commitID))
   333  }
   334  
   335  // CommitID is used to grab the current commit ID for the project in our working directory.
   336  // For integration tests you should use this function instead of localcommit.Get() and pjfile.LegacyCommitID() as it
   337  // is guaranteed to give a fresh result from disk, whereas the ones above use caching which tests don't like.
   338  func (s *Session) CommitID() string {
   339  	pjfile, err := projectfile.Parse(filepath.Join(s.Dirs.Work, constants.ConfigFileName))
   340  	require.NoError(s.T, err)
   341  	return pjfile.LegacyCommitID()
   342  }
   343  
   344  // PrepareProject creates a very simple activestate.yaml file for the given org/project and, if a
   345  // commit ID is given, an .activestate/commit file.
   346  func (s *Session) PrepareProject(namespace, commitID string) {
   347  	s.PrepareActiveStateYAML(fmt.Sprintf("project: https://%s/%s", constants.DefaultAPIHost, namespace))
   348  	if commitID != "" {
   349  		s.PrepareCommitIdFile(commitID)
   350  	}
   351  }
   352  
   353  // PrepareFile writes a file to path with contents, expecting no error
   354  func (s *Session) PrepareFile(path, contents string) {
   355  	errMsg := fmt.Sprintf("cannot setup file %q", path)
   356  
   357  	contents = strings.TrimSpace(contents)
   358  
   359  	err := os.MkdirAll(filepath.Dir(path), 0770)
   360  	require.NoError(s.T, err, errMsg)
   361  
   362  	bs := append([]byte(contents), '\n')
   363  
   364  	err = os.WriteFile(path, bs, 0660)
   365  	require.NoError(s.T, err, errMsg)
   366  }
   367  
   368  // LoginAsPersistentUser is a common test case after which an integration test user should be logged in to the platform
   369  func (s *Session) LoginAsPersistentUser() {
   370  	p := s.SpawnWithOpts(
   371  		OptArgs(tagsuite.Auth, "--username", PersistentUsername, "--password", PersistentPassword),
   372  		// as the command line includes a password, we do not print the executed command, so the password does not get logged
   373  		OptHideArgs(),
   374  	)
   375  
   376  	p.Expect("logged in", termtest.OptExpectTimeout(defaultTimeout))
   377  	p.ExpectExitCode(0)
   378  }
   379  
   380  func (s *Session) LogoutUser() {
   381  	p := s.Spawn(tagsuite.Auth, "logout")
   382  
   383  	p.Expect("logged out")
   384  	p.ExpectExitCode(0)
   385  }
   386  
   387  func (s *Session) CreateNewUser() *mono_models.UserEditable {
   388  	uid, err := uuid.NewRandom()
   389  	require.NoError(s.T, err)
   390  
   391  	username := fmt.Sprintf("user-%s", uid.String()[0:8])
   392  	password := uid.String()[8:]
   393  	email := fmt.Sprintf("%s@test.tld", username)
   394  	user := &mono_models.UserEditable{
   395  		Username: username,
   396  		Password: password,
   397  		Name:     username,
   398  		Email:    email,
   399  	}
   400  
   401  	params := users.NewAddUserParams()
   402  	params.SetUser(user)
   403  
   404  	// The default mono API client host is "testing.tld" inside unit tests.
   405  	// Since we actually want to create production users, we need to manually instantiate a mono API
   406  	// client with the right host.
   407  	serviceURL := api.GetServiceURL(api.ServiceMono)
   408  	host := os.Getenv(constants.APIHostEnvVarName)
   409  	if host == "" {
   410  		host = constants.DefaultAPIHost
   411  	}
   412  	serviceURL.Host = strings.Replace(serviceURL.Host, string(api.ServiceMono)+api.TestingPlatform, host, 1)
   413  	_, err = mono.Init(serviceURL, nil).Users.AddUser(params)
   414  	require.NoError(s.T, err, "Error creating new user")
   415  
   416  	p := s.Spawn(tagsuite.Auth, "--username", username, "--password", password)
   417  	p.Expect("logged in")
   418  	p.ExpectExitCode(0)
   419  
   420  	s.users = append(s.users, username)
   421  
   422  	return user
   423  }
   424  
   425  // NotifyProjectCreated indicates that the given project was created on the Platform and needs to
   426  // be deleted when the session is closed.
   427  // This only needs to be called for projects created by PersistentUsername, not projects created by
   428  // users created with CreateNewUser(). Created users' projects are auto-deleted.
   429  func (s *Session) NotifyProjectCreated(org, name string) {
   430  	s.createdProjects = append(s.createdProjects, project.NewNamespace(org, name, ""))
   431  }
   432  
   433  const deleteUUIDProjects = "__delete_uuid_projects" // some unique project name
   434  
   435  // DeleteUUIDProjects indicates that all projects with UUID names (i.e. autogenerated) for the given
   436  // org should be deleted when the session is closed.
   437  // This should not be called from generic integration tests. Use NotifyProjectCreated() instead,
   438  // because there could be race conditions if multiple platforms are creating and using UUID
   439  // projects.
   440  func (s *Session) DeleteUUIDProjects(org string) {
   441  	s.NotifyProjectCreated(org, deleteUUIDProjects)
   442  }
   443  
   444  func (s *Session) DebugMessage(prefix string) string {
   445  	var sectionStart, sectionEnd string
   446  	sectionStart = "\n=== "
   447  	if os.Getenv("GITHUB_ACTIONS") == "true" {
   448  		sectionStart = "##[group]"
   449  		sectionEnd = "##[endgroup]"
   450  	}
   451  
   452  	if prefix != "" {
   453  		prefix = prefix + "\n"
   454  	}
   455  
   456  	output := map[string]string{}
   457  	for _, spawn := range s.spawned {
   458  		name := spawn.Cmd().String()
   459  		if spawn.opts.HideCmdArgs {
   460  			name = spawn.Cmd().Path
   461  		}
   462  		output[name] = strings.TrimSpace(spawn.Snapshot())
   463  	}
   464  
   465  	v, err := strutils.ParseTemplate(`
   466  {{.Prefix}}Stack:
   467  {{.Stacktrace}}
   468  {{range $title, $value := .Outputs}}
   469  {{$.A}}Snapshot for Cmd '{{$title}}':
   470  {{$value}}
   471  {{$.Z}}
   472  {{end}}
   473  {{range $title, $value := .Logs}}
   474  {{$.A}}Log '{{$title}}':
   475  {{$value}}
   476  {{$.Z}}
   477  {{else}}
   478  No logs
   479  {{end}}
   480  `, map[string]interface{}{
   481  		"Prefix":     prefix,
   482  		"Stacktrace": stacktrace.Get().String(),
   483  		"Outputs":    output,
   484  		"Logs":       s.DebugLogs(),
   485  		"A":          sectionStart,
   486  		"Z":          sectionEnd,
   487  	}, nil)
   488  	if err != nil {
   489  		s.T.Fatalf("Parsing template failed: %s", errs.JoinMessage(err))
   490  	}
   491  
   492  	return v
   493  }
   494  
   495  // Close removes the temporary directory unless RetainDirs is specified
   496  func (s *Session) Close() error {
   497  	// stop service if it exists
   498  	if fileutils.TargetExists(s.SvcExe) {
   499  		cp := s.SpawnCmd(s.SvcExe, "stop")
   500  		cp.ExpectExitCode(0)
   501  	}
   502  
   503  	cfg, err := config.NewCustom(s.Dirs.Config, singlethread.New(), true)
   504  	require.NoError(s.T, err, "Could not read e2e session configuration: %s", errs.JoinMessage(err))
   505  
   506  	if !s.retainDirs {
   507  		defer s.Dirs.Close()
   508  	}
   509  
   510  	s.spawned = []*SpawnedCmd{}
   511  
   512  	if os.Getenv("PLATFORM_API_TOKEN") == "" {
   513  		s.T.Log("PLATFORM_API_TOKEN env var not set, not running suite tear down")
   514  		return nil
   515  	}
   516  
   517  	auth := authentication.New(cfg)
   518  
   519  	if os.Getenv(constants.APIHostEnvVarName) == "" {
   520  		err := os.Setenv(constants.APIHostEnvVarName, constants.DefaultAPIHost)
   521  		if err != nil {
   522  			return err
   523  		}
   524  		defer func() {
   525  			os.Unsetenv(constants.APIHostEnvVarName)
   526  		}()
   527  	}
   528  
   529  	err = auth.AuthenticateWithModel(&mono_models.Credentials{
   530  		Token: os.Getenv("PLATFORM_API_TOKEN"),
   531  	})
   532  	if err != nil {
   533  		return err
   534  	}
   535  
   536  	if len(s.createdProjects) > 0 && s.createdProjects[0].Project == deleteUUIDProjects {
   537  		org := s.createdProjects[0].Owner
   538  		s.createdProjects = make([]*project.Namespaced, 0) // reset
   539  		// When deleting UUID projects, only do it on one platform in order to avoid race conditions.
   540  		if runtime.GOOS == "linux" {
   541  			projects, err := getProjects(org, auth)
   542  			if err != nil {
   543  				s.T.Errorf("Could not fetch projects: %v", errs.JoinMessage(err))
   544  			}
   545  			for _, proj := range projects {
   546  				if strfmt.IsUUID(proj.Name) {
   547  					s.NotifyProjectCreated(org, proj.Name)
   548  				}
   549  			}
   550  		}
   551  	}
   552  
   553  	for _, proj := range s.createdProjects {
   554  		err := model.DeleteProject(proj.Owner, proj.Project, auth)
   555  		if err != nil {
   556  			s.T.Errorf("Could not delete project %s: %v", proj.Project, errs.JoinMessage(err))
   557  		}
   558  	}
   559  
   560  	for _, user := range s.users {
   561  		err := cleanUser(s.T, user, auth)
   562  		if err != nil {
   563  			s.T.Errorf("Could not delete user %s: %v", user, errs.JoinMessage(err))
   564  		}
   565  	}
   566  
   567  	// Add back the release state tool installation to the bash RC file.
   568  	// This was done on session creation to ensure that the release state tool
   569  	// does not appear on the PATH when a new subshell is started. This is a
   570  	// workaround to be addressed in: https://activestatef.atlassian.net/browse/DX-2285
   571  	if runtime.GOOS != "windows" {
   572  		installPath, err := installation.InstallPathForChannel("release")
   573  		if err != nil {
   574  			s.T.Errorf("Could not get install path: %v", errs.JoinMessage(err))
   575  		}
   576  		binDir := filepath.Join(installPath, "bin")
   577  
   578  		ss := bash.SubShell{}
   579  		err = ss.WriteUserEnv(cfg, map[string]string{"PATH": binDir}, sscommon.InstallID, false)
   580  		if err != nil {
   581  			s.T.Errorf("Could not clean user env: %v", errs.JoinMessage(err))
   582  		}
   583  	}
   584  
   585  	if !s.ignoreLogErrors {
   586  		s.detectLogErrors()
   587  	}
   588  
   589  	return nil
   590  }
   591  
   592  func (s *Session) InstallerLog() string {
   593  	logDir := filepath.Join(s.Dirs.Config, "logs")
   594  	if !fileutils.DirExists(logDir) {
   595  		return ""
   596  	}
   597  	files, err := fileutils.ListDirSimple(logDir, false)
   598  	if err != nil {
   599  		return fmt.Sprintf("Could not list log dir: %v", err)
   600  	}
   601  	for _, file := range files {
   602  		if !strings.HasPrefix(filepath.Base(file), "state-installer") {
   603  			continue
   604  		}
   605  		b := fileutils.ReadFileUnsafe(file)
   606  		return string(b) + "\n\nCurrent time: " + time.Now().String()
   607  	}
   608  
   609  	return fmt.Sprintf("Could not find state-installer log, checked under %s, found: \n, files: \n%v\n", logDir, files)
   610  }
   611  
   612  func (s *Session) SvcLog() string {
   613  	logDir := filepath.Join(s.Dirs.Config, "logs")
   614  	if !fileutils.DirExists(logDir) {
   615  		return ""
   616  	}
   617  	files, err := fileutils.ListDirSimple(logDir, false)
   618  	if err != nil {
   619  		return fmt.Sprintf("Could not list log dir: %v", err)
   620  	}
   621  	lines := []string{}
   622  	for _, file := range files {
   623  		if !strings.HasPrefix(filepath.Base(file), "state-svc") {
   624  			continue
   625  		}
   626  		b := fileutils.ReadFileUnsafe(file)
   627  		lines = append(lines, filepath.Base(file)+":"+strings.Split(string(b), "\n")[0])
   628  		if !strings.Contains(string(b), fmt.Sprintf("state-svc%s foreground", osutils.ExeExtension)) {
   629  			continue
   630  		}
   631  
   632  		return string(b) + "\n\nCurrent time: " + time.Now().String()
   633  	}
   634  
   635  	return fmt.Sprintf("Could not find state-svc log, checked under %s, found: \n%v\n, files: \n%v\n", logDir, lines, files)
   636  }
   637  
   638  func (s *Session) LogFiles() []string {
   639  	result := []string{}
   640  	logDir := filepath.Join(s.Dirs.Config, "logs")
   641  	if !fileutils.DirExists(logDir) {
   642  		return result
   643  	}
   644  
   645  	err := filepath.WalkDir(logDir, func(path string, f fs.DirEntry, err error) error {
   646  		if err != nil {
   647  			panic(err)
   648  		}
   649  		if f.IsDir() {
   650  			return nil
   651  		}
   652  
   653  		result = append(result, path)
   654  		return nil
   655  	})
   656  	if err != nil {
   657  		fmt.Printf("Error walking log dir: %v", err)
   658  	}
   659  
   660  	return result
   661  }
   662  
   663  func (s *Session) DebugLogs() map[string]string {
   664  	result := map[string]string{}
   665  
   666  	logDir := filepath.Join(s.Dirs.Config, "logs")
   667  	if !fileutils.DirExists(logDir) {
   668  		return result
   669  	}
   670  
   671  	for _, path := range s.LogFiles() {
   672  		result[filepath.Base(path)] = string(fileutils.ReadFileUnsafe(path))
   673  	}
   674  
   675  	return result
   676  }
   677  
   678  func (s *Session) DebugLogsDump() string {
   679  	logs := s.DebugLogs()
   680  
   681  	if len(logs) == 0 {
   682  		return "No logs found in " + filepath.Join(s.Dirs.Config, "logs")
   683  	}
   684  
   685  	var sectionStart, sectionEnd string
   686  	sectionStart = "\n=== "
   687  	if os.Getenv("GITHUB_ACTIONS") == "true" {
   688  		sectionStart = "##[group]"
   689  		sectionEnd = "##[endgroup]"
   690  	}
   691  
   692  	result := "Logs:\n"
   693  	for name, log := range logs {
   694  		result += fmt.Sprintf("%s%s:\n%s%s\n", sectionStart, name, log, sectionEnd)
   695  	}
   696  
   697  	return result
   698  }
   699  
   700  // IgnoreLogErrors disables log error checking after the session closes.
   701  // Normally, logged errors automatically cause test failures, so calling this is needed for tests
   702  // with expected errors.
   703  func (s *Session) IgnoreLogErrors() {
   704  	s.ignoreLogErrors = true
   705  }
   706  
   707  var errorOrPanicRegex = regexp.MustCompile(`(?:\[ERR |\[CRT |Panic:)`)
   708  
   709  func (s *Session) detectLogErrors() {
   710  	var sectionStart, sectionEnd string
   711  	sectionStart = "\n=== "
   712  	if os.Getenv("GITHUB_ACTIONS") == "true" {
   713  		sectionStart = "##[group]"
   714  		sectionEnd = "##[endgroup]"
   715  	}
   716  	for _, path := range s.LogFiles() {
   717  		if !strings.HasPrefix(filepath.Base(path), "state-") {
   718  			continue
   719  		}
   720  		if contents := string(fileutils.ReadFileUnsafe(path)); errorOrPanicRegex.MatchString(contents) {
   721  			s.T.Errorf("%sFound error and/or panic in log file %s\nIf this was expected, call session.IgnoreLogErrors() to avoid this check\nLog contents:\n%s%s",
   722  				sectionStart, path, contents, sectionEnd)
   723  		}
   724  	}
   725  }
   726  
   727  func (s *Session) SetupRCFile() {
   728  	if runtime.GOOS == "windows" {
   729  		return
   730  	}
   731  	s.T.Setenv("HOME", s.Dirs.HomeDir)
   732  	defer s.T.Setenv("HOME", os.Getenv("HOME"))
   733  
   734  	cfg, err := config.New()
   735  	require.NoError(s.T, err)
   736  
   737  	s.SetupRCFileCustom(subshell.New(cfg))
   738  }
   739  
   740  func (s *Session) SetupRCFileCustom(subshell subshell.SubShell) {
   741  	if runtime.GOOS == "windows" {
   742  		return
   743  	}
   744  
   745  	rcFile, err := subshell.RcFile()
   746  	require.NoError(s.T, err)
   747  
   748  	if fileutils.TargetExists(filepath.Join(s.Dirs.HomeDir, filepath.Base(rcFile))) {
   749  		err = fileutils.CopyFile(rcFile, filepath.Join(s.Dirs.HomeDir, filepath.Base(rcFile)))
   750  	} else {
   751  		err = fileutils.Touch(filepath.Join(s.Dirs.HomeDir, filepath.Base(rcFile)))
   752  	}
   753  	require.NoError(s.T, err)
   754  }
   755  
   756  func RunningOnCI() bool {
   757  	return condition.OnCI()
   758  }