Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to abort Eventually/Consistently also for functions #386

Closed
vespian opened this issue May 12, 2020 · 15 comments
Closed

Option to abort Eventually/Consistently also for functions #386

vespian opened this issue May 12, 2020 · 15 comments
Labels

Comments

@vespian
Copy link

vespian commented May 12, 2020

Documentation mentions that:

Note: Eventually and Consistently only exercise the MatchMayChangeInTheFuture method if they are passed a bare value. If they are passed functions to be polled it is not possible to guarantee that the return value of the function will not change between polling intervals. In this case, MatchMayChangeInTheFuture is not called and the polling continues until either a match is found or the timeout elapses.

which is reflected in the code:

if assertion.actualInputIsAFunction() {
return true
}

There are use-cases where aborting a function can still be usefull - imagine spinning EKS cluster on Amazon, with Eventually checking output of the function. Once the cluster transitions into "CREATE_FAILED" state - it will not heal itself, and Gomega forces users to wait the timeout as there is no way to abort the test.

Can it be changed so that if the MatchMayChangeInTheFuture is defined for the matcher, it is executed no matter if the actual is a function or a value so that people can decide on their own?

Thanks in advance.

@blgm
Copy link
Collaborator

blgm commented May 27, 2020

I think I understand what you're trying to achieve. I agree that there's no point waiting for a timeout when the result cannot change.

I think the rationale of the current implementation is that an Eventually() or Consistently() will poll until the timeout, unless it can be absolutely certain that the result will not change. If there is any doubt then it will continue polling. If we change that, it could allow for subtle bugs. For instance the Exit matcher knows that the result cannot change once the processes has finished. But a function in the Eventually() does not guarantee to return the same process each time, so the assumption could be wrong. Similarly the Receive() matcher knows that the result cannot change once a channel is closed. But a function may return a different channel each time, so the assumption is not safe.

Could something like this work?

deploymentFinished := func() bool { ... }
deploymentSuccessful := func() bool { ... }

Eventually(deploymentFinished).Should(BeTrue())
Expect(deploymentSuccessful).To(BeTrue())

That way the logic about whether the result can change is handled by the user-defined function.

@vespian
Copy link
Author

vespian commented May 28, 2020

I understand where you are coming from and the workaround you suggested would work in the use case I have. Thank you very much for the detailed explanation!

Just as a note, I am of the opinion that users should be given freedom, even when there is a chance of fatal mistakes. E.g. lots of Linux tools offer --yes option even though it also allows users to make fat-finger kind of mistakes and wipe their hard drives out. But again - I just wanted to share a different point of view :)

Would it be possible to put the justification you have given and the workaround code into the Gomega documentation? Somewhere near the entry for MatchMayChangeInTheFuture? I find it super helpful and for users like me who just started with Gomega, it could save a lot of time.

Thanks in advance!

@blgm
Copy link
Collaborator

blgm commented Jun 1, 2020

Thank you @vespian. I'll improve the docs.

@matthchr
Copy link
Contributor

For whatever it's worth, I just ran into this issue as well, and the example above (EKS cluster) is a great analog for the case I was testing.

I'll use the workaround for now but it would be nice if Eventually (or an equivalent function?) had the ability to break out early if the function being called instructed it to do so.

@onsi
Copy link
Owner

onsi commented Oct 20, 2020 via email

@matthchr
Copy link
Contributor

@onsi that seems a big hammer to use just to fail a test? I'm definitely not an expert on best practices though, so open to being corrected on that understanding.

@dlipovetsky
Copy link

I've wondered about this in the past. One question I have is: If there is a terminal error inside an Eventually, should the test fail? If the answer is yes, then here's an example of "failing fast" when reaching such an error. Note that cleanup can be handled in a deferred function in the test spec, or in an AfterEach.

package example_test

import (
	"fmt"
	"os"
	"time"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("When running", func() {

	It("should fail fast from Eventually", func() {
		defer func() {
			fmt.Fprintf(GinkgoWriter, "cleaning up in a deferred function\n")
		}()
		Eventually(func() bool {
			if _, ok := os.LookupEnv("FAIL"); ok {
				Fail("detected a terminal error, retrying is a waste of time\n")
			}
			fmt.Fprintf(GinkgoWriter, "trying again...\n")
			return false
		}, 5*time.Second, 1*time.Second).Should(BeTrue())
	})

	AfterEach(func() {
		fmt.Fprintf(GinkgoWriter, "cleaning up in AfterEach\n")
	})
})

Run with simulated terminal error:

> FAIL=true ginkgo ./eventually
Running Suite: Example
======================
Random Seed: 1611100252
Will run 1 of 1 specs

cleaning up in a deferred function
cleaning up in AfterEach
• Failure [0.001 seconds]
When running
/home/dlipovetsky/projects/ginkgo-experiments/eventually/example_test.go:12
  should fail fast from Eventually [It]
  /home/dlipovetsky/projects/ginkgo-experiments/eventually/example_test.go:14

  detected a terminal error, retrying is a waste of time
  

  /home/dlipovetsky/projects/ginkgo-experiments/eventually/example_test.go:20
------------------------------


Summarizing 1 Failure:

[Fail] When running [It] should fail fast from Eventually 
/home/dlipovetsky/projects/ginkgo-experiments/eventually/example_test.go:20

Ran 1 of 1 Specs in 0.001 seconds
FAIL! -- 0 Passed | 1 Failed | 0 Pending | 0 Skipped
--- FAIL: TestExample (0.00s)
FAIL

Ginkgo ran 1 suite in 560.373574ms
Test Suite Failed

Run without simulated terminal error:

> ginkgo ./eventually
Running Suite: Example
======================
Random Seed: 1611101262
Will run 1 of 1 specs

trying again...
trying again...
trying again...
trying again...
trying again...
cleaning up in a deferred function
cleaning up in AfterEach
• Failure [5.002 seconds]
When running
/home/dlipovetsky/projects/ginkgo-experiments/eventually/example_test.go:12
  should fail fast from Eventually [It]
  /home/dlipovetsky/projects/ginkgo-experiments/eventually/example_test.go:14

  Timed out after 5.001s.
  Expected
      <bool>: false
  to be true

  /home/dlipovetsky/projects/ginkgo-experiments/eventually/example_test.go:24
------------------------------


Summarizing 1 Failure:

[Fail] When running [It] should fail fast from Eventually 
/home/dlipovetsky/projects/ginkgo-experiments/eventually/example_test.go:24

Ran 1 of 1 Specs in 5.003 seconds
FAIL! -- 0 Passed | 1 Failed | 0 Pending | 0 Skipped
--- FAIL: TestExample (5.00s)
FAIL

Ginkgo ran 1 suite in 5.594551988s
Test Suite Failed

@ramakrishnan-sundaramoorthy

instead of setting an env variable and reading it, can we do the check based on the returned error.
like "nil" representing "success - continue" can we not have an err for "failure - continue/abort" and "failure - retry"

i am just starting here, please correct me if i am wrong

@pohly
Copy link

pohly commented Sep 24, 2022

I also feel that aborting early based on a special error is the most elegant solution for this. I recently implemented such a "final" error for Kubernetes polling functions (not based on gomega):

// FinalError constructs an error that indicates to a poll function that
// polling can be stopped immediately because some permanent error has been
// encountered that is not going to go away.
func FinalError(err error) error {
	return &FinalErr{Err: err}
}

type FinalErr struct {
	Err error
}

func (err *FinalErr) Error() string {
	if err.Err != nil {
		return fmt.Sprintf("final error: %s", err.Err.Error())
	}
	return "final error, exact problem unknown"
}

func (err *FinalErr) Unwrap() error {
	return err.Err
}

// IsFinal checks whether the error was marked as final by wrapping some error
// with FinalError.
func IsFinal(err error) bool {
	var finalErr *FinalErr
	return errors.As(err, &finalErr)
}

When Eventually is passed a function which returns a value and an error, then Eventually could use IsFinal to abort early and log that error as failure.

@onsi
Copy link
Owner

onsi commented Oct 11, 2022

Hey all sorry for the extended delay here. The latest commit on master now introduces StopTrying("message") which is documented here. It should be out in a versioned release soon.

You can return StopTrying("message") as an error from your polled function or you can call StopTrying("message").Now() to immediately end execution without having to thread a new error return value through.

@pohly
Copy link

pohly commented Oct 11, 2022

What isn't clear from the description is whether gomega handles wrapping of the StopTrying error. I haven't looked at the implementation.

In other words, will this work?

return fmt.Errorf("some operation: %w", StopWriting("stop"))

@onsi
Copy link
Owner

onsi commented Oct 11, 2022

Ah good catch - it will not work currently but I should be able to use errors.As to get around that. I'll take a look.

@onsi
Copy link
Owner

onsi commented Oct 11, 2022

yep - I had to do the errors.As dance. The code is written and tested and I'll cut a patch release later today.

@grosser
Copy link
Contributor

grosser commented Oct 26, 2024

issue can be closed @onsi ?

@grosser
Copy link
Contributor

grosser commented Oct 26, 2024

opened a new issue for "successful StopTring()" #786

@onsi onsi closed this as completed Oct 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

8 participants