mirror of
				https://github.com/redhat-developer/odo.git
				synced 2025-10-19 03:06:19 +03:00 
			
		
		
		
	 0f828ec99f
			
		
	
	0f828ec99f
	
	
	
		
			
			* 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
		
			
				
	
	
		
			409 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			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]
 | |
| }
 |