diff --git a/Makefile b/Makefile index 3e1ab6da0..58787c076 100644 --- a/Makefile +++ b/Makefile @@ -201,7 +201,7 @@ pao-functests: cluster-label-worker-cnf pao-functests-only pao-functests-only: @echo "Cluster Version" hack/show-cluster-version.sh - hack/run-test.sh -t "test/e2e/performanceprofile/functests/0_config test/e2e/performanceprofile/functests/1_performance test/e2e/performanceprofile/functests/6_mustgather_testing" -p "-v -r --fail-fast --flake-attempts=2 --junit-report=report.xml" -m "Running Functional Tests" + hack/run-test.sh -t "test/e2e/performanceprofile/functests/0_config test/e2e/performanceprofile/functests/1_performance test/e2e/performanceprofile/functests/6_mustgather_testing test/e2e/performanceprofile/functests/10_performance_ppc" -p "-v -r --fail-fast --flake-attempts=2 --junit-report=report.xml" -m "Running Functional Tests" .PHONY: pao-functests-updating-profile pao-functests-updating-profile: cluster-label-worker-cnf pao-functests-update-only diff --git a/test/e2e/performanceprofile/functests/10_performance_ppc/10_ppc_suite_test.go b/test/e2e/performanceprofile/functests/10_performance_ppc/10_ppc_suite_test.go new file mode 100644 index 000000000..cd9960fdc --- /dev/null +++ b/test/e2e/performanceprofile/functests/10_performance_ppc/10_ppc_suite_test.go @@ -0,0 +1,39 @@ +package __performance_ppc + +import ( + "fmt" + "os/exec" + "testing" + + . "github.com/onsi/ginkgo/v2" + + . "github.com/onsi/gomega" + testutils "github.com/openshift/cluster-node-tuning-operator/test/e2e/performanceprofile/functests/utils" + testlog "github.com/openshift/cluster-node-tuning-operator/test/e2e/performanceprofile/functests/utils/log" +) + +const ( + DefaultPodmanBinaryPath = "/usr/bin/podman" +) + +var _ = BeforeSuite(func() { + var err error + By("Check podman binary exists") + path, err := exec.LookPath(DefaultPodmanBinaryPath) + if err != nil { + Skip(fmt.Sprintf("%s doesn't exists", DefaultPodmanBinaryPath)) + } + testlog.Infof("Podman binary executed from path %s", path) + By("Checking Environment Variables") + testlog.Infof("NTO Image used: %s", testutils.NTOImage) + if testutils.MustGatherDir == "" { + Skip("set env variable MUSTGATHER_DIR to ocp mustgather directory") + } + testlog.Infof("Mustgather Directory used: %s", testutils.MustGatherDir) + +}) + +func TestPPC(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "PPC Suite") +} diff --git a/test/e2e/performanceprofile/functests/10_performance_ppc/ppc.go b/test/e2e/performanceprofile/functests/10_performance_ppc/ppc.go new file mode 100644 index 000000000..a8fd61080 --- /dev/null +++ b/test/e2e/performanceprofile/functests/10_performance_ppc/ppc.go @@ -0,0 +1,198 @@ +package __performance_ppc + +import ( + "fmt" + "os/exec" + "regexp" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + performancev2 "github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2" + testutils "github.com/openshift/cluster-node-tuning-operator/test/e2e/performanceprofile/functests/utils" + testlog "github.com/openshift/cluster-node-tuning-operator/test/e2e/performanceprofile/functests/utils/log" + "k8s.io/kubernetes/pkg/kubelet/cm/cpuset" + "sigs.k8s.io/yaml" +) + +type PPCTest struct { + PodmanMakeOptions func(args []string) []string + PodmanBinary string +} + +type PPCSession struct { + *gexec.Session +} + +func (p *PPCTest) MakeOptions(args []string) []string { + return p.PodmanMakeOptions(args) +} + +func (p *PPCTest) PodmanAsUserBase(args []string, noEvents, noCache bool) (*PPCSession, error) { + var command *exec.Cmd + podmanOptions := p.MakeOptions(args) + podmanBinary := p.PodmanBinary + fmt.Printf("Running: %s %s\n", podmanBinary, strings.Join(podmanOptions, " ")) + command = exec.Command(podmanBinary, podmanOptions...) + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + if err != nil { + return nil, err + } + + return &PPCSession{session}, nil +} + +type PPCTestIntegration struct { + PPCTest +} + +func (p *PPCTestIntegration) MakeOptions(args []string) []string { + return args +} +func PPCTestCreateUtil() *PPCTestIntegration { + p := &PPCTestIntegration{ + PPCTest: PPCTest{ + PodmanBinary: DefaultPodmanBinaryPath, + }, + } + p.PodmanMakeOptions = p.MakeOptions + return p +} + +var _ = Describe("[rfe_id: 38968] PerformanceProfile setup helper and platform awareness", func() { + mustgatherDir := testutils.MustGatherDir + ntoImage := testutils.NTOImage + Context("PPC Sanity Tests", func() { + ppcIntgTest := PPCTestCreateUtil() + It("[test_id:40940] Performance Profile regression tests", func() { + pp := &performancev2.PerformanceProfile{} + var reservedCpuCount = 2 + defaultArgs := []string{ + "run", + "--entrypoint", + "performance-profile-creator", + "-v", + } + + cmdArgs := []string{ + fmt.Sprintf("%s:%s:z", mustgatherDir, mustgatherDir), + ntoImage, + "--mcp-name=worker", + fmt.Sprintf("--reserved-cpu-count=%d", reservedCpuCount), + fmt.Sprintf("--rt-kernel=%t", true), + fmt.Sprintf("--power-consumption-mode=%s", "low-latency"), + fmt.Sprintf("--split-reserved-cpus-across-numa=%t", false), + fmt.Sprintf("--must-gather-dir-path=%s", mustgatherDir), + } + podmanArgs := []string{} + podmanArgs = append(defaultArgs, cmdArgs...) + session, err := ppcIntgTest.PodmanAsUserBase(podmanArgs, false, false) + Expect(err).ToNot(HaveOccurred(), "Podman command failed") + output := session.Wait(20).Out.Contents() + err = yaml.Unmarshal(output, pp) + Expect(err).ToNot(HaveOccurred(), "Unable to marshal the ppc output") + reservedCpus, err := cpuset.Parse(string(*pp.Spec.CPU.Reserved)) + Expect(err).ToNot(HaveOccurred(), "Unable to parse cpus") + totalReservedCpus := reservedCpus.Size() + Expect(totalReservedCpus).To(Equal(reservedCpuCount)) + Expect(*pp.Spec.RealTimeKernel.Enabled).To(BeTrue()) + Expect(*pp.Spec.WorkloadHints.RealTime).To(BeTrue()) + Expect(*pp.Spec.NUMA.TopologyPolicy).To(Equal("restricted")) + Eventually(session).Should(gexec.Exit(0)) + }) + It("[test_id:41405] Verify PPC script fails when the splitting of reserved cpus and single numa-node policy is specified", func() { + defaultArgs := []string{ + "run", + "--entrypoint", + "performance-profile-creator", + "-v", + } + cmdArgs := []string{ + fmt.Sprintf("%s:%s:z", mustgatherDir, mustgatherDir), + ntoImage, + "--mcp-name=worker", + fmt.Sprintf("--reserved-cpu-count=%d", 2), + fmt.Sprintf("--rt-kernel=%t", true), + fmt.Sprintf("--power-consumption-mode=%s", "low-latency"), + fmt.Sprintf("--split-reserved-cpus-across-numa=%t", true), + fmt.Sprintf("--topology-manager-policy=%s", "single-numa-node"), + fmt.Sprintf("--must-gather-dir-path=%s", mustgatherDir), + } + podmanArgs := []string{} + podmanArgs = append(defaultArgs, cmdArgs...) + session, err := ppcIntgTest.PodmanAsUserBase(podmanArgs, false, false) + Expect(err).ToNot(HaveOccurred(), "Podman command failed") + output := session.Wait(20).Err.Contents() + errString := "Error: failed to obtain data from flags not appropriate to split reserved CPUs in case of topology-manager-policy: single-numa-node" + ok, err := regexp.MatchString(errString, string(output)) + if ok { + testlog.Info(errString) + } + Eventually(session).Should(gexec.Exit(1)) + }) + + It("[test_id:41419] Verify PPC script fails when reserved cpu count is 2 and requires to split across numa nodes", func() { + defaultArgs := []string{ + "run", + "--entrypoint", + "performance-profile-creator", + "-v", + } + cmdArgs := []string{ + fmt.Sprintf("%s:%s:z", mustgatherDir, mustgatherDir), + ntoImage, + "--mcp-name=worker", + fmt.Sprintf("--reserved-cpu-count=%d", 2), + fmt.Sprintf("--rt-kernel=%t", true), + fmt.Sprintf("--power-consumption-mode=%s", "low-latency"), + fmt.Sprintf("--split-reserved-cpus-across-numa=%t", true), + fmt.Sprintf("--must-gather-dir-path=%s", mustgatherDir), + } + podmanArgs := []string{} + podmanArgs = append(defaultArgs, cmdArgs...) + session, err := ppcIntgTest.PodmanAsUserBase(podmanArgs, false, false) + Expect(err).ToNot(HaveOccurred(), "Podman command failed") + output := session.Wait(20).Err.Contents() + errString := "Error: failed to compute the reserved and isolated CPUs: can't allocate odd number of CPUs from a NUMA Node" + ok, err := regexp.MatchString(errString, string(output)) + Expect(err).ToNot(HaveOccurred(), "did not fail with Expected:%s failure", errString) + if ok { + testlog.Info(errString) + } + Eventually(session).Should(gexec.Exit(1)) + }) + + It("[test_id:41420] Verify PPC script fails when reserved cpu count is more than available cpus", func() { + defaultArgs := []string{ + "run", + "--entrypoint", + "performance-profile-creator", + "-v", + } + cmdArgs := []string{ + fmt.Sprintf("%s:%s:z", mustgatherDir, mustgatherDir), + ntoImage, + "--mcp-name=worker", + fmt.Sprintf("--reserved-cpu-count=%d", 100), + fmt.Sprintf("--rt-kernel=%t", true), + fmt.Sprintf("--power-consumption-mode=%s", "low-latency"), + fmt.Sprintf("--split-reserved-cpus-across-numa=%t", true), + fmt.Sprintf("--must-gather-dir-path=%s", mustgatherDir), + } + podmanArgs := []string{} + podmanArgs = append(defaultArgs, cmdArgs...) + session, err := ppcIntgTest.PodmanAsUserBase(podmanArgs, false, false) + Expect(err).ToNot(HaveOccurred(), "Podman command failed") + output := session.Wait(20).Err.Contents() + errString := "Error: failed to compute the reserved and isolated CPUs: please specify the reserved CPU count in the range [1,3]" + ok, err := regexp.MatchString(errString, string(output)) + Expect(err).ToNot(HaveOccurred(), "did not fail with Expected:%s failure", errString) + if ok { + testlog.Info(errString) + } + Eventually(session).Should(gexec.Exit(1)) + }) + + }) +}) diff --git a/test/e2e/performanceprofile/functests/utils/consts.go b/test/e2e/performanceprofile/functests/utils/consts.go index 6144cafc7..f2299ec14 100644 --- a/test/e2e/performanceprofile/functests/utils/consts.go +++ b/test/e2e/performanceprofile/functests/utils/consts.go @@ -24,6 +24,12 @@ var NodesSelector string // ProfileNotFound is true when discovery mode is enabled and no valid profile was found var ProfileNotFound bool +// NtoImage represents NTO Image location which is either quay.io or any other internal registry +var NTOImage string + +// MustGatherDir represents Mustgather directory created using oc adm mustgather +var MustGatherDir string + func init() { RoleWorkerCNF = os.Getenv("ROLE_WORKER_CNF") if RoleWorkerCNF == "" { @@ -41,6 +47,14 @@ func init() { fmt.Sprintf("%s/%s", LabelRole, RoleWorkerCNF): "", } + NTOImage = os.Getenv("NTO_IMAGE") + + if NTOImage == "" { + NTOImage = "quay.io/openshift/origin-cluster-node-tuning-operator:latest" + } + + MustGatherDir = os.Getenv("MUSTGATHER_DIR") + if discovery.Enabled() { profile, err := discovery.GetDiscoveryPerformanceProfile(NodesSelector) if err == discovery.ErrProfileNotFound { diff --git a/vendor/github.com/onsi/gomega/gbytes/buffer.go b/vendor/github.com/onsi/gomega/gbytes/buffer.go new file mode 100644 index 000000000..6f77b2371 --- /dev/null +++ b/vendor/github.com/onsi/gomega/gbytes/buffer.go @@ -0,0 +1,255 @@ +/* +Package gbytes provides a buffer that supports incrementally detecting input. + +You use gbytes.Buffer with the gbytes.Say matcher. When Say finds a match, it fastforwards the buffer's read cursor to the end of that match. + +Subsequent matches against the buffer will only operate against data that appears *after* the read cursor. + +The read cursor is an opaque implementation detail that you cannot access. You should use the Say matcher to sift through the buffer. You can always +access the entire buffer's contents with Contents(). + +*/ +package gbytes + +import ( + "errors" + "fmt" + "io" + "regexp" + "sync" + "time" +) + +/* +gbytes.Buffer implements an io.Writer and can be used with the gbytes.Say matcher. + +You should only use a gbytes.Buffer in test code. It stores all writes in an in-memory buffer - behavior that is inappropriate for production code! +*/ +type Buffer struct { + contents []byte + readCursor uint64 + lock *sync.Mutex + detectCloser chan interface{} + closed bool +} + +/* +NewBuffer returns a new gbytes.Buffer +*/ +func NewBuffer() *Buffer { + return &Buffer{ + lock: &sync.Mutex{}, + } +} + +/* +BufferWithBytes returns a new gbytes.Buffer seeded with the passed in bytes +*/ +func BufferWithBytes(bytes []byte) *Buffer { + return &Buffer{ + lock: &sync.Mutex{}, + contents: bytes, + } +} + +/* +BufferReader returns a new gbytes.Buffer that wraps a reader. The reader's contents are read into +the Buffer via io.Copy +*/ +func BufferReader(reader io.Reader) *Buffer { + b := &Buffer{ + lock: &sync.Mutex{}, + } + + go func() { + io.Copy(b, reader) + b.Close() + }() + + return b +} + +/* +Write implements the io.Writer interface +*/ +func (b *Buffer) Write(p []byte) (n int, err error) { + b.lock.Lock() + defer b.lock.Unlock() + + if b.closed { + return 0, errors.New("attempt to write to closed buffer") + } + + b.contents = append(b.contents, p...) + return len(p), nil +} + +/* +Read implements the io.Reader interface. It advances the +cursor as it reads. +*/ +func (b *Buffer) Read(d []byte) (int, error) { + b.lock.Lock() + defer b.lock.Unlock() + + if uint64(len(b.contents)) <= b.readCursor { + return 0, io.EOF + } + + n := copy(d, b.contents[b.readCursor:]) + b.readCursor += uint64(n) + + return n, nil +} + +/* +Clear clears out the buffer's contents +*/ +func (b *Buffer) Clear() error { + b.lock.Lock() + defer b.lock.Unlock() + + if b.closed { + return errors.New("attempt to clear closed buffer") + } + + b.contents = []byte{} + b.readCursor = 0 + return nil +} + +/* +Close signifies that the buffer will no longer be written to +*/ +func (b *Buffer) Close() error { + b.lock.Lock() + defer b.lock.Unlock() + + b.closed = true + + return nil +} + +/* +Closed returns true if the buffer has been closed +*/ +func (b *Buffer) Closed() bool { + b.lock.Lock() + defer b.lock.Unlock() + + return b.closed +} + +/* +Contents returns all data ever written to the buffer. +*/ +func (b *Buffer) Contents() []byte { + b.lock.Lock() + defer b.lock.Unlock() + + contents := make([]byte, len(b.contents)) + copy(contents, b.contents) + return contents +} + +/* +Detect takes a regular expression and returns a channel. + +The channel will receive true the first time data matching the regular expression is written to the buffer. +The channel is subsequently closed and the buffer's read-cursor is fast-forwarded to just after the matching region. + +You typically don't need to use Detect and should use the ghttp.Say matcher instead. Detect is useful, however, in cases where your code must +be branch and handle different outputs written to the buffer. + +For example, consider a buffer hooked up to the stdout of a client library. You may (or may not, depending on state outside of your control) need to authenticate the client library. + +You could do something like: + +select { +case <-buffer.Detect("You are not logged in"): + //log in +case <-buffer.Detect("Success"): + //carry on +case <-time.After(time.Second): + //welp +} +buffer.CancelDetects() + +You should always call CancelDetects after using Detect. This will close any channels that have not detected and clean up the goroutines that were spawned to support them. + +Finally, you can pass detect a format string followed by variadic arguments. This will construct the regexp using fmt.Sprintf. +*/ +func (b *Buffer) Detect(desired string, args ...interface{}) chan bool { + formattedRegexp := desired + if len(args) > 0 { + formattedRegexp = fmt.Sprintf(desired, args...) + } + re := regexp.MustCompile(formattedRegexp) + + b.lock.Lock() + defer b.lock.Unlock() + + if b.detectCloser == nil { + b.detectCloser = make(chan interface{}) + } + + closer := b.detectCloser + response := make(chan bool) + go func() { + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + defer close(response) + for { + select { + case <-ticker.C: + b.lock.Lock() + data, cursor := b.contents[b.readCursor:], b.readCursor + loc := re.FindIndex(data) + b.lock.Unlock() + + if loc != nil { + response <- true + b.lock.Lock() + newCursorPosition := cursor + uint64(loc[1]) + if newCursorPosition >= b.readCursor { + b.readCursor = newCursorPosition + } + b.lock.Unlock() + return + } + case <-closer: + return + } + } + }() + + return response +} + +/* +CancelDetects cancels any pending detects and cleans up their goroutines. You should always call this when you're done with a set of Detect channels. +*/ +func (b *Buffer) CancelDetects() { + b.lock.Lock() + defer b.lock.Unlock() + + close(b.detectCloser) + b.detectCloser = nil +} + +func (b *Buffer) didSay(re *regexp.Regexp) (bool, []byte) { + b.lock.Lock() + defer b.lock.Unlock() + + unreadBytes := b.contents[b.readCursor:] + copyOfUnreadBytes := make([]byte, len(unreadBytes)) + copy(copyOfUnreadBytes, unreadBytes) + + loc := re.FindIndex(unreadBytes) + + if loc != nil { + b.readCursor += uint64(loc[1]) + return true, copyOfUnreadBytes + } + return false, copyOfUnreadBytes +} diff --git a/vendor/github.com/onsi/gomega/gbytes/io_wrappers.go b/vendor/github.com/onsi/gomega/gbytes/io_wrappers.go new file mode 100644 index 000000000..a41ad6232 --- /dev/null +++ b/vendor/github.com/onsi/gomega/gbytes/io_wrappers.go @@ -0,0 +1,85 @@ +package gbytes + +import ( + "errors" + "io" + "time" +) + +// ErrTimeout is returned by TimeoutCloser, TimeoutReader, and TimeoutWriter when the underlying Closer/Reader/Writer does not return within the specified timeout +var ErrTimeout = errors.New("timeout occurred") + +// TimeoutCloser returns an io.Closer that wraps the passed-in io.Closer. If the underlying Closer fails to close within the allotted timeout ErrTimeout is returned. +func TimeoutCloser(c io.Closer, timeout time.Duration) io.Closer { + return timeoutReaderWriterCloser{c: c, d: timeout} +} + +// TimeoutReader returns an io.Reader that wraps the passed-in io.Reader. If the underlying Reader fails to read within the allotted timeout ErrTimeout is returned. +func TimeoutReader(r io.Reader, timeout time.Duration) io.Reader { + return timeoutReaderWriterCloser{r: r, d: timeout} +} + +// TimeoutWriter returns an io.Writer that wraps the passed-in io.Writer. If the underlying Writer fails to write within the allotted timeout ErrTimeout is returned. +func TimeoutWriter(w io.Writer, timeout time.Duration) io.Writer { + return timeoutReaderWriterCloser{w: w, d: timeout} +} + +type timeoutReaderWriterCloser struct { + c io.Closer + w io.Writer + r io.Reader + d time.Duration +} + +func (t timeoutReaderWriterCloser) Close() error { + done := make(chan struct{}) + var err error + + go func() { + err = t.c.Close() + close(done) + }() + + select { + case <-done: + return err + case <-time.After(t.d): + return ErrTimeout + } +} + +func (t timeoutReaderWriterCloser) Read(p []byte) (int, error) { + done := make(chan struct{}) + var n int + var err error + + go func() { + n, err = t.r.Read(p) + close(done) + }() + + select { + case <-done: + return n, err + case <-time.After(t.d): + return 0, ErrTimeout + } +} + +func (t timeoutReaderWriterCloser) Write(p []byte) (int, error) { + done := make(chan struct{}) + var n int + var err error + + go func() { + n, err = t.w.Write(p) + close(done) + }() + + select { + case <-done: + return n, err + case <-time.After(t.d): + return 0, ErrTimeout + } +} diff --git a/vendor/github.com/onsi/gomega/gbytes/say_matcher.go b/vendor/github.com/onsi/gomega/gbytes/say_matcher.go new file mode 100644 index 000000000..0763f5e2d --- /dev/null +++ b/vendor/github.com/onsi/gomega/gbytes/say_matcher.go @@ -0,0 +1,106 @@ +// untested sections: 1 + +package gbytes + +import ( + "fmt" + "regexp" + + "github.com/onsi/gomega/format" +) + +//Objects satisfying the BufferProvider can be used with the Say matcher. +type BufferProvider interface { + Buffer() *Buffer +} + +/* +Say is a Gomega matcher that operates on gbytes.Buffers: + + Expect(buffer).Should(Say("something")) + +will succeed if the unread portion of the buffer matches the regular expression "something". + +When Say succeeds, it fast forwards the gbytes.Buffer's read cursor to just after the successful match. +Thus, subsequent calls to Say will only match against the unread portion of the buffer + +Say pairs very well with Eventually. To assert that a buffer eventually receives data matching "[123]-star" within 3 seconds you can: + + Eventually(buffer, 3).Should(Say("[123]-star")) + +Ditto with consistently. To assert that a buffer does not receive data matching "never-see-this" for 1 second you can: + + Consistently(buffer, 1).ShouldNot(Say("never-see-this")) + +In addition to bytes.Buffers, Say can operate on objects that implement the gbytes.BufferProvider interface. +In such cases, Say simply operates on the *gbytes.Buffer returned by Buffer() + +If the buffer is closed, the Say matcher will tell Eventually to abort. +*/ +func Say(expected string, args ...interface{}) *sayMatcher { + if len(args) > 0 { + expected = fmt.Sprintf(expected, args...) + } + return &sayMatcher{ + re: regexp.MustCompile(expected), + } +} + +type sayMatcher struct { + re *regexp.Regexp + receivedSayings []byte +} + +func (m *sayMatcher) buffer(actual interface{}) (*Buffer, bool) { + var buffer *Buffer + + switch x := actual.(type) { + case *Buffer: + buffer = x + case BufferProvider: + buffer = x.Buffer() + default: + return nil, false + } + + return buffer, true +} + +func (m *sayMatcher) Match(actual interface{}) (success bool, err error) { + buffer, ok := m.buffer(actual) + if !ok { + return false, fmt.Errorf("Say must be passed a *gbytes.Buffer or BufferProvider. Got:\n%s", format.Object(actual, 1)) + } + + didSay, sayings := buffer.didSay(m.re) + m.receivedSayings = sayings + + return didSay, nil +} + +func (m *sayMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf( + "Got stuck at:\n%s\nWaiting for:\n%s", + format.IndentString(string(m.receivedSayings), 1), + format.IndentString(m.re.String(), 1), + ) +} + +func (m *sayMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf( + "Saw:\n%s\nWhich matches the unexpected:\n%s", + format.IndentString(string(m.receivedSayings), 1), + format.IndentString(m.re.String(), 1), + ) +} + +func (m *sayMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + switch x := actual.(type) { + case *Buffer: + return !x.Closed() + case BufferProvider: + return !x.Buffer().Closed() + default: + return true + } +} diff --git a/vendor/github.com/onsi/gomega/gexec/build.go b/vendor/github.com/onsi/gomega/gexec/build.go new file mode 100644 index 000000000..0ddb21076 --- /dev/null +++ b/vendor/github.com/onsi/gomega/gexec/build.go @@ -0,0 +1,233 @@ +// untested sections: 5 + +package gexec + +import ( + "errors" + "fmt" + "go/build" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" + "sync" + + "github.com/onsi/gomega/internal/gutil" +) + +var ( + mu sync.Mutex + tmpDir string +) + +/* +Build uses go build to compile the package at packagePath. The resulting binary is saved off in a temporary directory. +A path pointing to this binary is returned. + +Build uses the $GOPATH set in your environment. If $GOPATH is not set and you are using Go 1.8+, +it will use the default GOPATH instead. It passes the variadic args on to `go build`. +*/ +func Build(packagePath string, args ...string) (compiledPath string, err error) { + return doBuild(build.Default.GOPATH, packagePath, nil, args...) +} + +/* +BuildWithEnvironment is identical to Build but allows you to specify env vars to be set at build time. +*/ +func BuildWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error) { + return doBuild(build.Default.GOPATH, packagePath, env, args...) +} + +/* +BuildIn is identical to Build but allows you to specify a custom $GOPATH (the first argument). +*/ +func BuildIn(gopath string, packagePath string, args ...string) (compiledPath string, err error) { + return doBuild(gopath, packagePath, nil, args...) +} + +func doBuild(gopath, packagePath string, env []string, args ...string) (compiledPath string, err error) { + executable, err := newExecutablePath(gopath, packagePath) + if err != nil { + return "", err + } + + cmdArgs := append([]string{"build"}, args...) + cmdArgs = append(cmdArgs, "-o", executable, packagePath) + + build := exec.Command("go", cmdArgs...) + build.Env = replaceGoPath(os.Environ(), gopath) + build.Env = append(build.Env, env...) + + output, err := build.CombinedOutput() + if err != nil { + return "", fmt.Errorf("Failed to build %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output)) + } + + return executable, nil +} + +/* +CompileTest uses go test to compile the test package at packagePath. The resulting binary is saved off in a temporary directory. +A path pointing to this binary is returned. + +CompileTest uses the $GOPATH set in your environment. If $GOPATH is not set and you are using Go 1.8+, +it will use the default GOPATH instead. It passes the variadic args on to `go test`. +*/ +func CompileTest(packagePath string, args ...string) (compiledPath string, err error) { + return doCompileTest(build.Default.GOPATH, packagePath, nil, args...) +} + +/* +GetAndCompileTest is identical to CompileTest but `go get` the package before compiling tests. +*/ +func GetAndCompileTest(packagePath string, args ...string) (compiledPath string, err error) { + if err := getForTest(build.Default.GOPATH, packagePath, []string{"GO111MODULE=off"}); err != nil { + return "", err + } + + return doCompileTest(build.Default.GOPATH, packagePath, []string{"GO111MODULE=off"}, args...) +} + +/* +CompileTestWithEnvironment is identical to CompileTest but allows you to specify env vars to be set at build time. +*/ +func CompileTestWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error) { + return doCompileTest(build.Default.GOPATH, packagePath, env, args...) +} + +/* +GetAndCompileTestWithEnvironment is identical to GetAndCompileTest but allows you to specify env vars to be set at build time. +*/ +func GetAndCompileTestWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error) { + if err := getForTest(build.Default.GOPATH, packagePath, append(env, "GO111MODULE=off")); err != nil { + return "", err + } + + return doCompileTest(build.Default.GOPATH, packagePath, append(env, "GO111MODULE=off"), args...) +} + +/* +CompileTestIn is identical to CompileTest but allows you to specify a custom $GOPATH (the first argument). +*/ +func CompileTestIn(gopath string, packagePath string, args ...string) (compiledPath string, err error) { + return doCompileTest(gopath, packagePath, nil, args...) +} + +/* +GetAndCompileTestIn is identical to GetAndCompileTest but allows you to specify a custom $GOPATH (the first argument). +*/ +func GetAndCompileTestIn(gopath string, packagePath string, args ...string) (compiledPath string, err error) { + if err := getForTest(gopath, packagePath, []string{"GO111MODULE=off"}); err != nil { + return "", err + } + + return doCompileTest(gopath, packagePath, []string{"GO111MODULE=off"}, args...) +} + +func isLocalPackage(packagePath string) bool { + return strings.HasPrefix(packagePath, ".") +} + +func getForTest(gopath, packagePath string, env []string) error { + if isLocalPackage(packagePath) { + return nil + } + + return doGet(gopath, packagePath, env, "-t") +} + +func doGet(gopath, packagePath string, env []string, args ...string) error { + args = append(args, packagePath) + args = append([]string{"get"}, args...) + + goGet := exec.Command("go", args...) + goGet.Dir = gopath + goGet.Env = replaceGoPath(os.Environ(), gopath) + goGet.Env = append(goGet.Env, env...) + + output, err := goGet.CombinedOutput() + if err != nil { + return fmt.Errorf("Failed to get %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output)) + } + + return nil +} + +func doCompileTest(gopath, packagePath string, env []string, args ...string) (compiledPath string, err error) { + executable, err := newExecutablePath(gopath, packagePath, ".test") + if err != nil { + return "", err + } + + cmdArgs := append([]string{"test", "-c"}, args...) + cmdArgs = append(cmdArgs, "-o", executable, packagePath) + + build := exec.Command("go", cmdArgs...) + build.Env = replaceGoPath(os.Environ(), gopath) + build.Env = append(build.Env, env...) + + output, err := build.CombinedOutput() + if err != nil { + return "", fmt.Errorf("Failed to build %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output)) + } + + return executable, nil +} + +func replaceGoPath(environ []string, newGoPath string) []string { + newEnviron := []string{} + for _, v := range environ { + if !strings.HasPrefix(v, "GOPATH=") { + newEnviron = append(newEnviron, v) + } + } + return append(newEnviron, "GOPATH="+newGoPath) +} + +func newExecutablePath(gopath, packagePath string, suffixes ...string) (string, error) { + tmpDir, err := temporaryDirectory() + if err != nil { + return "", err + } + + if len(gopath) == 0 { + return "", errors.New("$GOPATH not provided when building " + packagePath) + } + + executable := filepath.Join(tmpDir, path.Base(packagePath)) + + if runtime.GOOS == "windows" { + executable += ".exe" + } + + return executable, nil +} + +/* +You should call CleanupBuildArtifacts before your test ends to clean up any temporary artifacts generated by +gexec. In Ginkgo this is typically done in an AfterSuite callback. +*/ +func CleanupBuildArtifacts() { + mu.Lock() + defer mu.Unlock() + if tmpDir != "" { + os.RemoveAll(tmpDir) + tmpDir = "" + } +} + +func temporaryDirectory() (string, error) { + var err error + mu.Lock() + defer mu.Unlock() + if tmpDir == "" { + tmpDir, err = gutil.MkdirTemp("", "gexec_artifacts") + if err != nil { + return "", err + } + } + + return gutil.MkdirTemp(tmpDir, "g") +} diff --git a/vendor/github.com/onsi/gomega/gexec/exit_matcher.go b/vendor/github.com/onsi/gomega/gexec/exit_matcher.go new file mode 100644 index 000000000..6e70de68d --- /dev/null +++ b/vendor/github.com/onsi/gomega/gexec/exit_matcher.go @@ -0,0 +1,88 @@ +// untested sections: 2 + +package gexec + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +/* +The Exit matcher operates on a session: + + Expect(session).Should(Exit()) + +Exit passes if the session has already exited. + +If no status code is provided, then Exit will succeed if the session has exited regardless of exit code. +Otherwise, Exit will only succeed if the process has exited with the provided status code. + +Note that the process must have already exited. To wait for a process to exit, use Eventually: + + Eventually(session, 3).Should(Exit(0)) +*/ +func Exit(optionalExitCode ...int) *exitMatcher { + exitCode := -1 + if len(optionalExitCode) > 0 { + exitCode = optionalExitCode[0] + } + + return &exitMatcher{ + exitCode: exitCode, + } +} + +type exitMatcher struct { + exitCode int + didExit bool + actualExitCode int +} + +type Exiter interface { + ExitCode() int +} + +func (m *exitMatcher) Match(actual interface{}) (success bool, err error) { + exiter, ok := actual.(Exiter) + if !ok { + return false, fmt.Errorf("Exit must be passed a gexec.Exiter (Missing method ExitCode() int) Got:\n%s", format.Object(actual, 1)) + } + + m.actualExitCode = exiter.ExitCode() + + if m.actualExitCode == -1 { + return false, nil + } + + if m.exitCode == -1 { + return true, nil + } + return m.exitCode == m.actualExitCode, nil +} + +func (m *exitMatcher) FailureMessage(actual interface{}) (message string) { + if m.actualExitCode == -1 { + return "Expected process to exit. It did not." + } + return format.Message(m.actualExitCode, "to match exit code:", m.exitCode) +} + +func (m *exitMatcher) NegatedFailureMessage(actual interface{}) (message string) { + if m.actualExitCode == -1 { + return "you really shouldn't be able to see this!" + } else { + if m.exitCode == -1 { + return "Expected process not to exit. It did." + } + return format.Message(m.actualExitCode, "not to match exit code:", m.exitCode) + } +} + +func (m *exitMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + session, ok := actual.(*Session) + if ok { + return session.ExitCode() == -1 + } + return true +} diff --git a/vendor/github.com/onsi/gomega/gexec/prefixed_writer.go b/vendor/github.com/onsi/gomega/gexec/prefixed_writer.go new file mode 100644 index 000000000..feb6620c5 --- /dev/null +++ b/vendor/github.com/onsi/gomega/gexec/prefixed_writer.go @@ -0,0 +1,55 @@ +// untested sections: 1 + +package gexec + +import ( + "io" + "sync" +) + +/* +PrefixedWriter wraps an io.Writer, emitting the passed in prefix at the beginning of each new line. +This can be useful when running multiple gexec.Sessions concurrently - you can prefix the log output of each +session by passing in a PrefixedWriter: + +gexec.Start(cmd, NewPrefixedWriter("[my-cmd] ", GinkgoWriter), NewPrefixedWriter("[my-cmd] ", GinkgoWriter)) +*/ +type PrefixedWriter struct { + prefix []byte + writer io.Writer + lock *sync.Mutex + atStartOfLine bool +} + +func NewPrefixedWriter(prefix string, writer io.Writer) *PrefixedWriter { + return &PrefixedWriter{ + prefix: []byte(prefix), + writer: writer, + lock: &sync.Mutex{}, + atStartOfLine: true, + } +} + +func (w *PrefixedWriter) Write(b []byte) (int, error) { + w.lock.Lock() + defer w.lock.Unlock() + + toWrite := []byte{} + + for _, c := range b { + if w.atStartOfLine { + toWrite = append(toWrite, w.prefix...) + } + + toWrite = append(toWrite, c) + + w.atStartOfLine = c == '\n' + } + + _, err := w.writer.Write(toWrite) + if err != nil { + return 0, err + } + + return len(b), nil +} diff --git a/vendor/github.com/onsi/gomega/gexec/session.go b/vendor/github.com/onsi/gomega/gexec/session.go new file mode 100644 index 000000000..be4877ac9 --- /dev/null +++ b/vendor/github.com/onsi/gomega/gexec/session.go @@ -0,0 +1,305 @@ +/* +Package gexec provides support for testing external processes. +*/ + +// untested sections: 1 + +package gexec + +import ( + "io" + "os" + "os/exec" + "sync" + "syscall" + + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" +) + +const INVALID_EXIT_CODE = 254 + +type Session struct { + //The wrapped command + Command *exec.Cmd + + //A *gbytes.Buffer connected to the command's stdout + Out *gbytes.Buffer + + //A *gbytes.Buffer connected to the command's stderr + Err *gbytes.Buffer + + //A channel that will close when the command exits + Exited <-chan struct{} + + lock *sync.Mutex + exitCode int +} + +/* +Start starts the passed-in *exec.Cmd command. It wraps the command in a *gexec.Session. + +The session pipes the command's stdout and stderr to two *gbytes.Buffers available as properties on the session: session.Out and session.Err. +These buffers can be used with the gbytes.Say matcher to match against unread output: + + Expect(session.Out).Should(gbytes.Say("foo-out")) + Expect(session.Err).Should(gbytes.Say("foo-err")) + +In addition, Session satisfies the gbytes.BufferProvider interface and provides the stdout *gbytes.Buffer. This allows you to replace the first line, above, with: + + Expect(session).Should(gbytes.Say("foo-out")) + +When outWriter and/or errWriter are non-nil, the session will pipe stdout and/or stderr output both into the session *gybtes.Buffers and to the passed-in outWriter/errWriter. +This is useful for capturing the process's output or logging it to screen. In particular, when using Ginkgo it can be convenient to direct output to the GinkgoWriter: + + session, err := Start(command, GinkgoWriter, GinkgoWriter) + +This will log output when running tests in verbose mode, but - otherwise - will only log output when a test fails. + +The session wrapper is responsible for waiting on the *exec.Cmd command. You *should not* call command.Wait() yourself. +Instead, to assert that the command has exited you can use the gexec.Exit matcher: + + Expect(session).Should(gexec.Exit()) + +When the session exits it closes the stdout and stderr gbytes buffers. This will short circuit any +Eventuallys waiting for the buffers to Say something. +*/ +func Start(command *exec.Cmd, outWriter io.Writer, errWriter io.Writer) (*Session, error) { + exited := make(chan struct{}) + + session := &Session{ + Command: command, + Out: gbytes.NewBuffer(), + Err: gbytes.NewBuffer(), + Exited: exited, + lock: &sync.Mutex{}, + exitCode: -1, + } + + var commandOut, commandErr io.Writer + + commandOut, commandErr = session.Out, session.Err + + if outWriter != nil { + commandOut = io.MultiWriter(commandOut, outWriter) + } + + if errWriter != nil { + commandErr = io.MultiWriter(commandErr, errWriter) + } + + command.Stdout = commandOut + command.Stderr = commandErr + + err := command.Start() + if err == nil { + go session.monitorForExit(exited) + trackedSessionsMutex.Lock() + defer trackedSessionsMutex.Unlock() + trackedSessions = append(trackedSessions, session) + } + + return session, err +} + +/* +Buffer implements the gbytes.BufferProvider interface and returns s.Out +This allows you to make gbytes.Say matcher assertions against stdout without having to reference .Out: + + Eventually(session).Should(gbytes.Say("foo")) +*/ +func (s *Session) Buffer() *gbytes.Buffer { + return s.Out +} + +/* +ExitCode returns the wrapped command's exit code. If the command hasn't exited yet, ExitCode returns -1. + +To assert that the command has exited it is more convenient to use the Exit matcher: + + Eventually(s).Should(gexec.Exit()) + +When the process exits because it has received a particular signal, the exit code will be 128+signal-value +(See http://www.tldp.org/LDP/abs/html/exitcodes.html and http://man7.org/linux/man-pages/man7/signal.7.html) +*/ +func (s *Session) ExitCode() int { + s.lock.Lock() + defer s.lock.Unlock() + return s.exitCode +} + +/* +Wait waits until the wrapped command exits. It can be passed an optional timeout. +If the command does not exit within the timeout, Wait will trigger a test failure. + +Wait returns the session, making it possible to chain: + + session.Wait().Out.Contents() + +will wait for the command to exit then return the entirety of Out's contents. + +Wait uses eventually under the hood and accepts the same timeout/polling intervals that eventually does. +*/ +func (s *Session) Wait(timeout ...interface{}) *Session { + EventuallyWithOffset(1, s, timeout...).Should(Exit()) + return s +} + +/* +Kill sends the running command a SIGKILL signal. It does not wait for the process to exit. + +If the command has already exited, Kill returns silently. + +The session is returned to enable chaining. +*/ +func (s *Session) Kill() *Session { + return s.Signal(syscall.SIGKILL) +} + +/* +Interrupt sends the running command a SIGINT signal. It does not wait for the process to exit. + +If the command has already exited, Interrupt returns silently. + +The session is returned to enable chaining. +*/ +func (s *Session) Interrupt() *Session { + return s.Signal(syscall.SIGINT) +} + +/* +Terminate sends the running command a SIGTERM signal. It does not wait for the process to exit. + +If the command has already exited, Terminate returns silently. + +The session is returned to enable chaining. +*/ +func (s *Session) Terminate() *Session { + return s.Signal(syscall.SIGTERM) +} + +/* +Signal sends the running command the passed in signal. It does not wait for the process to exit. + +If the command has already exited, Signal returns silently. + +The session is returned to enable chaining. +*/ +func (s *Session) Signal(signal os.Signal) *Session { + if s.processIsAlive() { + s.Command.Process.Signal(signal) + } + return s +} + +func (s *Session) monitorForExit(exited chan<- struct{}) { + err := s.Command.Wait() + s.lock.Lock() + s.Out.Close() + s.Err.Close() + status := s.Command.ProcessState.Sys().(syscall.WaitStatus) + if status.Signaled() { + s.exitCode = 128 + int(status.Signal()) + } else { + exitStatus := status.ExitStatus() + if exitStatus == -1 && err != nil { + s.exitCode = INVALID_EXIT_CODE + } + s.exitCode = exitStatus + } + s.lock.Unlock() + + close(exited) +} + +func (s *Session) processIsAlive() bool { + return s.ExitCode() == -1 && s.Command.Process != nil +} + +var trackedSessions = []*Session{} +var trackedSessionsMutex = &sync.Mutex{} + +/* +Kill sends a SIGKILL signal to all the processes started by Run, and waits for them to exit. +The timeout specified is applied to each process killed. + +If any of the processes already exited, KillAndWait returns silently. +*/ +func KillAndWait(timeout ...interface{}) { + trackedSessionsMutex.Lock() + defer trackedSessionsMutex.Unlock() + for _, session := range trackedSessions { + session.Kill().Wait(timeout...) + } + trackedSessions = []*Session{} +} + +/* +Kill sends a SIGTERM signal to all the processes started by Run, and waits for them to exit. +The timeout specified is applied to each process killed. + +If any of the processes already exited, TerminateAndWait returns silently. +*/ +func TerminateAndWait(timeout ...interface{}) { + trackedSessionsMutex.Lock() + defer trackedSessionsMutex.Unlock() + for _, session := range trackedSessions { + session.Terminate().Wait(timeout...) + } +} + +/* +Kill sends a SIGKILL signal to all the processes started by Run. +It does not wait for the processes to exit. + +If any of the processes already exited, Kill returns silently. +*/ +func Kill() { + trackedSessionsMutex.Lock() + defer trackedSessionsMutex.Unlock() + for _, session := range trackedSessions { + session.Kill() + } +} + +/* +Terminate sends a SIGTERM signal to all the processes started by Run. +It does not wait for the processes to exit. + +If any of the processes already exited, Terminate returns silently. +*/ +func Terminate() { + trackedSessionsMutex.Lock() + defer trackedSessionsMutex.Unlock() + for _, session := range trackedSessions { + session.Terminate() + } +} + +/* +Signal sends the passed in signal to all the processes started by Run. +It does not wait for the processes to exit. + +If any of the processes already exited, Signal returns silently. +*/ +func Signal(signal os.Signal) { + trackedSessionsMutex.Lock() + defer trackedSessionsMutex.Unlock() + for _, session := range trackedSessions { + session.Signal(signal) + } +} + +/* +Interrupt sends the SIGINT signal to all the processes started by Run. +It does not wait for the processes to exit. + +If any of the processes already exited, Interrupt returns silently. +*/ +func Interrupt() { + trackedSessionsMutex.Lock() + defer trackedSessionsMutex.Unlock() + for _, session := range trackedSessions { + session.Interrupt() + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 88ca7d9fd..bf712e73f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -431,6 +431,8 @@ github.com/onsi/ginkgo/v2/types ## explicit; go 1.18 github.com/onsi/gomega github.com/onsi/gomega/format +github.com/onsi/gomega/gbytes +github.com/onsi/gomega/gexec github.com/onsi/gomega/gstruct github.com/onsi/gomega/gstruct/errors github.com/onsi/gomega/internal