...

Source file src/github.com/redhat-developer/odo/tests/helper/helper_dev.go

Documentation: github.com/redhat-developer/odo/tests/helper

     1  package helper
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"regexp"
     7  	"time"
     8  
     9  	"github.com/ActiveState/termtest/expect"
    10  	"github.com/onsi/gomega"
    11  	. "github.com/onsi/gomega"
    12  	"github.com/onsi/gomega/gexec"
    13  )
    14  
    15  // DevSession represents a session running `odo dev`
    16  /*
    17  	It can be used in different ways:
    18  
    19  	# Starting a session for a series of tests and stopping the session after the tests:
    20  
    21  	This format can be used when you want to run several independent tests
    22  	when the `odo dev` command is running in the background
    23  
    24  	```
    25  	When("running dev session", func() {
    26  		var devSession DevSession
    27  		var outContents []byte
    28  		var errContents []byte
    29  		BeforeEach(func() {
    30  			devSession, outContents, errContents = helper.StartDevMode(nil)
    31  		})
    32  		AfterEach(func() {
    33  			devSession.Stop()
    34  		})
    35  
    36  		It("...", func() {
    37  			// Test with `dev odo` running in the background
    38  			// outContents and errContents are contents of std/err output when dev mode is started
    39  		})
    40  		It("...", func() {
    41  			// Test with `dev odo` running in the background
    42  		})
    43  	})
    44  
    45  	# Starting a session and stopping it cleanly
    46  
    47  	This format can be used to test the behaviour of `odo dev` when it is stopped cleanly
    48  
    49  	When("running dev session and stopping it with cleanup", func() {
    50  		var devSession DevSession
    51  		var outContents []byte
    52  		var errContents []byte
    53  		BeforeEach(func() {
    54  			devSession, outContents, errContents = helper.StartDevMode(nil)
    55  			defer devSession.Stop()
    56  			[...]
    57  		})
    58  
    59  		It("...", func() {
    60  			// Test after `odo dev` has been stopped cleanly
    61  			// outContents and errContents are contents of std/err output when dev mode is started
    62  		})
    63  		It("...", func() {
    64  			// Test after `odo dev` has been stopped cleanly
    65  		})
    66  	})
    67  
    68  	# Starting a session and stopping it immediately without cleanup
    69  
    70  	This format can be used to test the behaviour of `odo dev` when it is stopped with a KILL signal
    71  
    72  	When("running dev session and stopping it without cleanup", func() {
    73  		var devSession DevSession
    74  		var outContents []byte
    75  		var errContents []byte
    76  		BeforeEach(func() {
    77  			devSession, outContents, errContents = helper.StartDevMode(nil)
    78  			defer devSession.Kill()
    79  			[...]
    80  		})
    81  
    82  		It("...", func() {
    83  			// Test after `odo dev` has been killed
    84  			// outContents and errContents are contents of std/err output when dev mode is started
    85  		})
    86  		It("...", func() {
    87  			// Test after `odo dev` has been killed
    88  		})
    89  	})
    90  
    91  
    92  	# Running a dev session and executing some tests inside this session
    93  
    94  	This format can be used to run a series of related tests in dev mode
    95  	All tests will be ran in the same session (ideal for e2e tests)
    96  	To run independent tests, previous formats should be used instead.
    97  
    98  	It("should do ... in dev mode", func() {
    99  		helper.RunDevMode(func(session *gexec.Session, outContents []byte, errContents []byte, ports map[string]string) {
   100  			// test on dev mode
   101  			// outContents and errContents are contents of std/err output when dev mode is started
   102  			// ports contains a map where keys are container ports and associated values are local IP:port redirecting to these local ports
   103  		})
   104  	})
   105  
   106  	# Waiting for file synchronisation to finish
   107  
   108  	The method session.WaitSync() can be used to wait for the synchronization of files to finish.
   109  	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.
   110  */
   111  
   112  type DevSession struct {
   113  	session           *gexec.Session
   114  	stopped           bool
   115  	console           *expect.Console
   116  	address           string
   117  	StdOut            string
   118  	ErrOut            string
   119  	Endpoints         map[string]string
   120  	APIServerEndpoint string
   121  }
   122  
   123  type DevSessionOpts struct {
   124  	EnvVars          []string
   125  	CmdlineArgs      []string
   126  	RunOnPodman      bool
   127  	TimeoutInSeconds int
   128  	NoRandomPorts    bool
   129  	NoWatch          bool
   130  	NoCommands       bool
   131  	CustomAddress    string
   132  	StartAPIServer   bool
   133  	APIServerPort    int
   134  	SyncGitDir       bool
   135  	ShowLogs         bool
   136  	VerboseLevel     string
   137  }
   138  
   139  // StartDevMode starts a dev session with `odo dev`
   140  // It returns a session structure, the contents of the standard and error outputs
   141  // and the redirections endpoints to access ports opened by component
   142  // when the dev mode is completely started
   143  func StartDevMode(options DevSessionOpts) (devSession DevSession, err error) {
   144  	if options.RunOnPodman {
   145  		options.CmdlineArgs = append(options.CmdlineArgs, "--platform", "podman")
   146  	}
   147  	c, err := expect.NewConsole(expect.WithStdout(os.Stdout))
   148  	if err != nil {
   149  		return DevSession{}, err
   150  	}
   151  
   152  	env := append([]string{}, options.EnvVars...)
   153  	args := []string{"dev"}
   154  	if options.NoCommands {
   155  		args = append(args, "--no-commands")
   156  	}
   157  	if !options.NoRandomPorts {
   158  		args = append(args, "--random-ports")
   159  	}
   160  	if options.NoWatch {
   161  		args = append(args, "--no-watch")
   162  	}
   163  	if options.CustomAddress != "" {
   164  		args = append(args, "--address", options.CustomAddress)
   165  	}
   166  	if !options.StartAPIServer {
   167  		args = append(args, "--api-server=false")
   168  	}
   169  	if options.APIServerPort != 0 {
   170  		args = append(args, fmt.Sprintf("--api-server-port=%d", options.APIServerPort))
   171  	}
   172  	if options.SyncGitDir {
   173  		args = append(args, "--sync-git-dir")
   174  	}
   175  	if options.ShowLogs {
   176  		args = append(args, "--logs")
   177  	}
   178  	if options.VerboseLevel != "" {
   179  		args = append(args, "-v", options.VerboseLevel)
   180  	}
   181  	args = append(args, options.CmdlineArgs...)
   182  	cmd := Cmd("odo", args...)
   183  	cmd.Cmd.Stdin = c.Tty()
   184  	cmd.Cmd.Stdout = c.Tty()
   185  	cmd.Cmd.Stderr = c.Tty()
   186  
   187  	session := cmd.AddEnv(env...).Runner().session
   188  	timeoutInSeconds := 420
   189  	if options.TimeoutInSeconds != 0 {
   190  		timeoutInSeconds = options.TimeoutInSeconds
   191  	}
   192  	WaitForOutputToContain("[Ctrl+c] - Exit", timeoutInSeconds, 10, session)
   193  	result := DevSession{
   194  		session: session,
   195  		console: c,
   196  		address: options.CustomAddress,
   197  	}
   198  	outContents := session.Out.Contents()
   199  	errContents := session.Err.Contents()
   200  	err = session.Out.Clear()
   201  	if err != nil {
   202  		return DevSession{}, err
   203  	}
   204  	err = session.Err.Clear()
   205  	if err != nil {
   206  		return DevSession{}, err
   207  	}
   208  	result.StdOut = string(outContents)
   209  	result.ErrOut = string(errContents)
   210  	result.Endpoints = getPorts(string(outContents), options.CustomAddress)
   211  	if options.StartAPIServer {
   212  		result.APIServerEndpoint = getAPIServerPort(string(outContents))
   213  	}
   214  	return result, nil
   215  
   216  }
   217  
   218  // Kill a Dev session abruptly, without handling any cleanup
   219  func (o DevSession) Kill() {
   220  	if o.console != nil {
   221  		err := o.console.Close()
   222  		gomega.Expect(err).NotTo(gomega.HaveOccurred())
   223  	}
   224  	o.session.Kill()
   225  }
   226  
   227  func (o DevSession) PID() int {
   228  	return o.session.Command.Process.Pid
   229  }
   230  
   231  // Stop a Dev session cleanly (equivalent as hitting Ctrl-c)
   232  func (o *DevSession) Stop() {
   233  	if o.session == nil {
   234  		return
   235  	}
   236  	if o.console != nil {
   237  		err := o.console.Close()
   238  		gomega.Expect(err).NotTo(gomega.HaveOccurred())
   239  	}
   240  	if o.stopped {
   241  		return
   242  	}
   243  
   244  	if o.session.ExitCode() == -1 {
   245  		err := terminateProc(o.session)
   246  		gomega.Expect(err).NotTo(gomega.HaveOccurred())
   247  	}
   248  	o.stopped = true
   249  }
   250  
   251  func (o *DevSession) PressKey(p byte) {
   252  	if o.console == nil || o.session == nil {
   253  		return
   254  	}
   255  	_, err := o.console.Write([]byte{p})
   256  	Expect(err).ToNot(HaveOccurred())
   257  }
   258  
   259  func (o DevSession) WaitEnd() {
   260  	if o.session == nil {
   261  		return
   262  	}
   263  	o.session.Wait(3 * time.Minute)
   264  }
   265  
   266  func (o DevSession) GetExitCode() int {
   267  	if o.session == nil {
   268  		return -1
   269  	}
   270  	return o.session.ExitCode()
   271  }
   272  
   273  // WaitSync waits for the synchronization of files to be finished
   274  // It returns the contents of the standard and error outputs
   275  // and the list of forwarded ports
   276  // since the end of the dev mode or the last time WaitSync/UpdateInfo has been called
   277  func (o *DevSession) WaitSync() error {
   278  	WaitForOutputToContainOne([]string{"Pushing files...", "Updating Component..."}, 180, 10, o.session)
   279  	WaitForOutputToContain("Dev mode", 240, 10, o.session)
   280  	return o.UpdateInfo()
   281  }
   282  
   283  func (o *DevSession) WaitRestartPortforward() error {
   284  	WaitForOutputToContain("Forwarding from", 240, 10, o.session)
   285  	return o.UpdateInfo()
   286  }
   287  
   288  // UpdateInfo returns the contents of the standard and error outputs
   289  // and the list of forwarded ports
   290  // since the end of the dev mode or the last time WaitSync/UpdateInfo has been called
   291  func (o *DevSession) UpdateInfo() error {
   292  	outContents := o.session.Out.Contents()
   293  	errContents := o.session.Err.Contents()
   294  	var err error
   295  	if !o.session.Out.Closed() {
   296  		err = o.session.Out.Clear()
   297  		if err != nil {
   298  			return err
   299  		}
   300  	}
   301  	if !o.session.Err.Closed() {
   302  		err = o.session.Err.Clear()
   303  		if err != nil {
   304  			return err
   305  		}
   306  	}
   307  	o.StdOut = string(outContents)
   308  	o.ErrOut = string(errContents)
   309  	endpoints := getPorts(o.StdOut, o.address)
   310  	if len(endpoints) != 0 {
   311  		// when pod was restarted and port forwarding is done again
   312  		o.Endpoints = endpoints
   313  	}
   314  	return nil
   315  }
   316  
   317  func (o DevSession) CheckNotSynced(timeout time.Duration) {
   318  	Consistently(func() string {
   319  		return string(o.session.Out.Contents())
   320  	}, timeout).ShouldNot(ContainSubstring("Pushing files..."))
   321  }
   322  
   323  // RunDevMode runs a dev session and executes the `inside` code when the dev mode is completely started
   324  // The inside handler is passed the internal session pointer, the contents of the standard and error outputs,
   325  // and a slice of strings - ports - giving the redirections in the form localhost:<port_number> to access ports opened by component
   326  func RunDevMode(options DevSessionOpts, inside func(session *gexec.Session, outContents string, errContents string, ports map[string]string)) error {
   327  
   328  	session, err := StartDevMode(options)
   329  	if err != nil {
   330  		return err
   331  	}
   332  	defer func() {
   333  		session.Stop()
   334  		session.WaitEnd()
   335  	}()
   336  	inside(session.session, session.StdOut, session.ErrOut, session.Endpoints)
   337  	return nil
   338  }
   339  
   340  // WaitForDevModeToContain runs `odo dev` until it contains a given substring in output or errOut(depending on checkErrOut arg).
   341  // `odo dev` runs in an infinite reconciliation loop, and hence running it with Cmd will not work for a lot of failing cases,
   342  // this function is helpful in such cases.
   343  // If stopSessionAfter is false, it is up to the caller to stop the DevSession returned.
   344  // TODO(pvala): Modify StartDevMode to take substring arg into account, and replace this method with it.
   345  func WaitForDevModeToContain(options DevSessionOpts, substring string, stopSessionAfter bool, checkErrOut bool) (DevSession, error) {
   346  	args := []string{"dev", "--random-ports"}
   347  	args = append(args, options.CmdlineArgs...)
   348  	if options.RunOnPodman {
   349  		args = append(args, "--platform", "podman")
   350  	}
   351  	if options.CustomAddress != "" {
   352  		args = append(args, "--address", options.CustomAddress)
   353  	}
   354  	session := Cmd("odo", args...).AddEnv(options.EnvVars...).Runner().session
   355  	if checkErrOut {
   356  		WaitForErroutToContain(substring, 360, 10, session)
   357  	} else {
   358  		WaitForOutputToContain(substring, 360, 10, session)
   359  	}
   360  	result := DevSession{
   361  		session: session,
   362  		address: options.CustomAddress,
   363  	}
   364  	if stopSessionAfter {
   365  		defer func() {
   366  			result.Stop()
   367  			result.WaitEnd()
   368  		}()
   369  	}
   370  
   371  	outContents := session.Out.Contents()
   372  	errContents := session.Err.Contents()
   373  	err := session.Out.Clear()
   374  	if err != nil {
   375  		return DevSession{}, err
   376  	}
   377  	err = session.Err.Clear()
   378  	if err != nil {
   379  		return DevSession{}, err
   380  	}
   381  	result.StdOut = string(outContents)
   382  	result.ErrOut = string(errContents)
   383  	result.Endpoints = getPorts(result.StdOut, options.CustomAddress)
   384  	return result, nil
   385  }
   386  
   387  // getPorts returns a map of ports redirected depending on the information in s
   388  //
   389  //	`- Forwarding from 127.0.0.1:20001 -> 3000` will return { "3000": "127.0.0.1:20001" }
   390  func getPorts(s, address string) map[string]string {
   391  	if address == "" {
   392  		address = "127.0.0.1"
   393  	}
   394  	result := map[string]string{}
   395  	re := regexp.MustCompile(fmt.Sprintf("(%s:[0-9]+) -> ([0-9]+)", address))
   396  	matches := re.FindAllStringSubmatch(s, -1)
   397  	for _, match := range matches {
   398  		result[match[2]] = match[1]
   399  	}
   400  	return result
   401  }
   402  
   403  // getAPIServerPort returns the address at which api server is running
   404  func getAPIServerPort(s string) string {
   405  	re := regexp.MustCompile(`API Server started at http://(localhost:[0-9]+\/api\/v1)`)
   406  	matches := re.FindStringSubmatch(s)
   407  	return matches[1]
   408  }
   409  

View as plain text