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 }