Files
odo/tests/helper/helper_dev.go
Philippe Martin 0f828ec99f Ignore devstate when existing process name is not odo + delete devstate files with odo delete component (#7090)
* Ignore devstate when existing process name is not odo

* Delete orphan devstate files with odo delete component

* Update unit tests

* Create fake system

* Add unit tests for odo delete component

* Integration tests for odo dev

* Troubleshooting

* First process on Windows is 4

* Use go-ps lib for pidExists
2023-09-20 14:20:53 +02:00

409 lines
12 KiB
Go

package helper
import (
"fmt"
"os"
"regexp"
"time"
"github.com/ActiveState/termtest/expect"
"github.com/onsi/gomega"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec"
)
// DevSession represents a session running `odo dev`
/*
It can be used in different ways:
# Starting a session for a series of tests and stopping the session after the tests:
This format can be used when you want to run several independent tests
when the `odo dev` command is running in the background
```
When("running dev session", func() {
var devSession DevSession
var outContents []byte
var errContents []byte
BeforeEach(func() {
devSession, outContents, errContents = helper.StartDevMode(nil)
})
AfterEach(func() {
devSession.Stop()
})
It("...", func() {
// Test with `dev odo` running in the background
// outContents and errContents are contents of std/err output when dev mode is started
})
It("...", func() {
// Test with `dev odo` running in the background
})
})
# Starting a session and stopping it cleanly
This format can be used to test the behaviour of `odo dev` when it is stopped cleanly
When("running dev session and stopping it with cleanup", func() {
var devSession DevSession
var outContents []byte
var errContents []byte
BeforeEach(func() {
devSession, outContents, errContents = helper.StartDevMode(nil)
defer devSession.Stop()
[...]
})
It("...", func() {
// Test after `odo dev` has been stopped cleanly
// outContents and errContents are contents of std/err output when dev mode is started
})
It("...", func() {
// Test after `odo dev` has been stopped cleanly
})
})
# Starting a session and stopping it immediately without cleanup
This format can be used to test the behaviour of `odo dev` when it is stopped with a KILL signal
When("running dev session and stopping it without cleanup", func() {
var devSession DevSession
var outContents []byte
var errContents []byte
BeforeEach(func() {
devSession, outContents, errContents = helper.StartDevMode(nil)
defer devSession.Kill()
[...]
})
It("...", func() {
// Test after `odo dev` has been killed
// outContents and errContents are contents of std/err output when dev mode is started
})
It("...", func() {
// Test after `odo dev` has been killed
})
})
# Running a dev session and executing some tests inside this session
This format can be used to run a series of related tests in dev mode
All tests will be ran in the same session (ideal for e2e tests)
To run independent tests, previous formats should be used instead.
It("should do ... in dev mode", func() {
helper.RunDevMode(func(session *gexec.Session, outContents []byte, errContents []byte, ports map[string]string) {
// test on dev mode
// outContents and errContents are contents of std/err output when dev mode is started
// ports contains a map where keys are container ports and associated values are local IP:port redirecting to these local ports
})
})
# Waiting for file synchronisation to finish
The method session.WaitSync() can be used to wait for the synchronization of files to finish.
The method returns the contents of std/err output since the end of the dev mode started or previous sync, and until the end of the synchronization.
*/
type DevSession struct {
session *gexec.Session
stopped bool
console *expect.Console
address string
StdOut string
ErrOut string
Endpoints map[string]string
APIServerEndpoint string
}
type DevSessionOpts struct {
EnvVars []string
CmdlineArgs []string
RunOnPodman bool
TimeoutInSeconds int
NoRandomPorts bool
NoWatch bool
NoCommands bool
CustomAddress string
StartAPIServer bool
APIServerPort int
SyncGitDir bool
ShowLogs bool
VerboseLevel string
}
// StartDevMode starts a dev session with `odo dev`
// It returns a session structure, the contents of the standard and error outputs
// and the redirections endpoints to access ports opened by component
// when the dev mode is completely started
func StartDevMode(options DevSessionOpts) (devSession DevSession, err error) {
if options.RunOnPodman {
options.CmdlineArgs = append(options.CmdlineArgs, "--platform", "podman")
}
c, err := expect.NewConsole(expect.WithStdout(os.Stdout))
if err != nil {
return DevSession{}, err
}
env := append([]string{}, options.EnvVars...)
args := []string{"dev"}
if options.NoCommands {
args = append(args, "--no-commands")
}
if !options.NoRandomPorts {
args = append(args, "--random-ports")
}
if options.NoWatch {
args = append(args, "--no-watch")
}
if options.CustomAddress != "" {
args = append(args, "--address", options.CustomAddress)
}
if !options.StartAPIServer {
args = append(args, "--api-server=false")
}
if options.APIServerPort != 0 {
args = append(args, fmt.Sprintf("--api-server-port=%d", options.APIServerPort))
}
if options.SyncGitDir {
args = append(args, "--sync-git-dir")
}
if options.ShowLogs {
args = append(args, "--logs")
}
if options.VerboseLevel != "" {
args = append(args, "-v", options.VerboseLevel)
}
args = append(args, options.CmdlineArgs...)
cmd := Cmd("odo", args...)
cmd.Cmd.Stdin = c.Tty()
cmd.Cmd.Stdout = c.Tty()
cmd.Cmd.Stderr = c.Tty()
session := cmd.AddEnv(env...).Runner().session
timeoutInSeconds := 420
if options.TimeoutInSeconds != 0 {
timeoutInSeconds = options.TimeoutInSeconds
}
WaitForOutputToContain("[Ctrl+c] - Exit", timeoutInSeconds, 10, session)
result := DevSession{
session: session,
console: c,
address: options.CustomAddress,
}
outContents := session.Out.Contents()
errContents := session.Err.Contents()
err = session.Out.Clear()
if err != nil {
return DevSession{}, err
}
err = session.Err.Clear()
if err != nil {
return DevSession{}, err
}
result.StdOut = string(outContents)
result.ErrOut = string(errContents)
result.Endpoints = getPorts(string(outContents), options.CustomAddress)
if options.StartAPIServer {
result.APIServerEndpoint = getAPIServerPort(string(outContents))
}
return result, nil
}
// Kill a Dev session abruptly, without handling any cleanup
func (o DevSession) Kill() {
if o.console != nil {
err := o.console.Close()
gomega.Expect(err).NotTo(gomega.HaveOccurred())
}
o.session.Kill()
}
func (o DevSession) PID() int {
return o.session.Command.Process.Pid
}
// Stop a Dev session cleanly (equivalent as hitting Ctrl-c)
func (o *DevSession) Stop() {
if o.session == nil {
return
}
if o.console != nil {
err := o.console.Close()
gomega.Expect(err).NotTo(gomega.HaveOccurred())
}
if o.stopped {
return
}
if o.session.ExitCode() == -1 {
err := terminateProc(o.session)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
}
o.stopped = true
}
func (o *DevSession) PressKey(p byte) {
if o.console == nil || o.session == nil {
return
}
_, err := o.console.Write([]byte{p})
Expect(err).ToNot(HaveOccurred())
}
func (o DevSession) WaitEnd() {
if o.session == nil {
return
}
o.session.Wait(3 * time.Minute)
}
func (o DevSession) GetExitCode() int {
if o.session == nil {
return -1
}
return o.session.ExitCode()
}
// WaitSync waits for the synchronization of files to be finished
// It returns the contents of the standard and error outputs
// and the list of forwarded ports
// since the end of the dev mode or the last time WaitSync/UpdateInfo has been called
func (o *DevSession) WaitSync() error {
WaitForOutputToContainOne([]string{"Pushing files...", "Updating Component..."}, 180, 10, o.session)
WaitForOutputToContain("Dev mode", 240, 10, o.session)
return o.UpdateInfo()
}
func (o *DevSession) WaitRestartPortforward() error {
WaitForOutputToContain("Forwarding from", 240, 10, o.session)
return o.UpdateInfo()
}
// UpdateInfo returns the contents of the standard and error outputs
// and the list of forwarded ports
// since the end of the dev mode or the last time WaitSync/UpdateInfo has been called
func (o *DevSession) UpdateInfo() error {
outContents := o.session.Out.Contents()
errContents := o.session.Err.Contents()
var err error
if !o.session.Out.Closed() {
err = o.session.Out.Clear()
if err != nil {
return err
}
}
if !o.session.Err.Closed() {
err = o.session.Err.Clear()
if err != nil {
return err
}
}
o.StdOut = string(outContents)
o.ErrOut = string(errContents)
endpoints := getPorts(o.StdOut, o.address)
if len(endpoints) != 0 {
// when pod was restarted and port forwarding is done again
o.Endpoints = endpoints
}
return nil
}
func (o DevSession) CheckNotSynced(timeout time.Duration) {
Consistently(func() string {
return string(o.session.Out.Contents())
}, timeout).ShouldNot(ContainSubstring("Pushing files..."))
}
// RunDevMode runs a dev session and executes the `inside` code when the dev mode is completely started
// The inside handler is passed the internal session pointer, the contents of the standard and error outputs,
// and a slice of strings - ports - giving the redirections in the form localhost:<port_number> to access ports opened by component
func RunDevMode(options DevSessionOpts, inside func(session *gexec.Session, outContents string, errContents string, ports map[string]string)) error {
session, err := StartDevMode(options)
if err != nil {
return err
}
defer func() {
session.Stop()
session.WaitEnd()
}()
inside(session.session, session.StdOut, session.ErrOut, session.Endpoints)
return nil
}
// WaitForDevModeToContain runs `odo dev` until it contains a given substring in output or errOut(depending on checkErrOut arg).
// `odo dev` runs in an infinite reconciliation loop, and hence running it with Cmd will not work for a lot of failing cases,
// this function is helpful in such cases.
// If stopSessionAfter is false, it is up to the caller to stop the DevSession returned.
// TODO(pvala): Modify StartDevMode to take substring arg into account, and replace this method with it.
func WaitForDevModeToContain(options DevSessionOpts, substring string, stopSessionAfter bool, checkErrOut bool) (DevSession, error) {
args := []string{"dev", "--random-ports"}
args = append(args, options.CmdlineArgs...)
if options.RunOnPodman {
args = append(args, "--platform", "podman")
}
if options.CustomAddress != "" {
args = append(args, "--address", options.CustomAddress)
}
session := Cmd("odo", args...).AddEnv(options.EnvVars...).Runner().session
if checkErrOut {
WaitForErroutToContain(substring, 360, 10, session)
} else {
WaitForOutputToContain(substring, 360, 10, session)
}
result := DevSession{
session: session,
address: options.CustomAddress,
}
if stopSessionAfter {
defer func() {
result.Stop()
result.WaitEnd()
}()
}
outContents := session.Out.Contents()
errContents := session.Err.Contents()
err := session.Out.Clear()
if err != nil {
return DevSession{}, err
}
err = session.Err.Clear()
if err != nil {
return DevSession{}, err
}
result.StdOut = string(outContents)
result.ErrOut = string(errContents)
result.Endpoints = getPorts(result.StdOut, options.CustomAddress)
return result, nil
}
// getPorts returns a map of ports redirected depending on the information in s
//
// `- Forwarding from 127.0.0.1:20001 -> 3000` will return { "3000": "127.0.0.1:20001" }
func getPorts(s, address string) map[string]string {
if address == "" {
address = "127.0.0.1"
}
result := map[string]string{}
re := regexp.MustCompile(fmt.Sprintf("(%s:[0-9]+) -> ([0-9]+)", address))
matches := re.FindAllStringSubmatch(s, -1)
for _, match := range matches {
result[match[2]] = match[1]
}
return result
}
// getAPIServerPort returns the address at which api server is running
func getAPIServerPort(s string) string {
re := regexp.MustCompile(`API Server started at http://(localhost:[0-9]+\/api\/v1)`)
matches := re.FindStringSubmatch(s)
return matches[1]
}