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

Add useful test wrapper functions to FakeExec #14

Closed
wants to merge 1 commit into from

Conversation

danwinship
Copy link
Contributor

Migrated from kubernetes/kubernetes#46537...


The fake implementation of pkg/util/exec for test programs is kind of really annoying to use. This PR adds some wrapper functions to make it easier, AND to automatically check that the passed-in command was what you expected (which some tests were doing but most weren't).

So:

fcmd := fakeexec.FakeCmd{
        CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{
                // iptables version check
                func() ([]byte, error) { return []byte("iptables v1.9.22"), nil },
                // iptables-restore version check
                func() ([]byte, error) { return []byte("iptables-restore v1.9.22"), nil },
                // Success.
                func() ([]byte, error) { return []byte{}, nil },
        },
}
fexec := fakeexec.FakeExec{
        CommandScript: []fakeexec.FakeCommandAction{
                func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
                func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
                func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
        },
}

becomes

fexec := fakeexec.NewFakeExec(t, nil)
fexec.AddCommand("iptables", "--version").
	SetCombinedOutput("iptables v1.9.22", nil)
fexec.AddCommand("iptables-restore", "--version").
	SetCombinedOutput("iptables-restore v1.9.22", nil)
fexec.AddCommand("iptables", "-w2", "-N", "FOOBAR", "-t", "nat").
	SetCombinedOutput("", nil)

There's some discussion on the original kubernetes bug (which I'm going to rebase on top of this to cover only the "porting pkg/util to the new interface" part). The June 27 comment about it not passing "make verify" no longer seems to be a problem with the migration of fakeexec into a separate subdirectory.

@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Aug 9, 2017

// SetCombinedOutput provides the result of calling CombinedOutput() on a FakeCmd.
// output can be either a []byte or a string (which will be converted to a []byte).
func (fcmd *FakeCmd) SetCombinedOutput(output interface{}, err error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you absolutely want output to be []byte, why don't you take a []byte type instead? It's not difficult to convert string to []byte for the caller anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Literally every existing caller in kubernetes wants to pass a string rather than a []byte, so if you don't want it to accept both then I'd change the API to just take a string instead, and say people can use the lower-level API if they need to use []byte

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I just don't really like the interface{}. string is fine.

}

// AssertExpectedCommands ensures that all of the commands added to fake via AddCommand were actually executed
func (fake *FakeExec) AssertExpectedCommands() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should probably be with other FakeExec function rather than here (if we keep it, see below)

Also, I'm really not a big fan of embedding testing.T in that class in the first place, and all the variables are public anyway, so why not leave that code inside the test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convenience. That's all any of this is about; making FakeExec more complicated so that test programs can be more simple.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, then it should probably panic instead, and the name could be better

}

// SetRunOutput provides the result of calling Run() on a FakeCmd.
// stdout and stderr can be either []byte or string (which will be converted to []byte).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as below, pass a []byte if that's what you want

return fcmd
}

func outputAsBytes(output interface{}) []byte {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below, remove this

// t.Fatal.) cmdsInPath is a list of command names for which LookPath will return a match
// ("/fake-bin/commandname"); if your code does not use LookPath you can pass nil. (If you
// need more complex behavior, you can override LookPathFunc on the returned FakeExec.)
func NewFakeExec(t *testing.T, cmdsInPath []string) *FakeExec {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need more complex behavior, you can override LookPathFunc on the returned FakeExec.

Why would I call this function in the first place if I want to change LookPathFunc, considering that's pretty much the only added value of this function?

I think my point is: the value of this function is to provide a default LookPathFunc. Can you define a sort of LookPathFunc function somewhere that one could use (decouple creation of FakeExec from LookPath selection):

func MakeFakeBinLookPathFunc(cmdsInPath []string) func(cmd string) (string, error) {
    return func(cmd string) (string, error) {
        for _, pathCmd := range cmdsInPath {
            if pathCmd == cmd {
                return "/fake-bin/" + cmd, nil
            }
        }
        return "", &osexec.Error{Name: cmd, Err: osexec.ErrNotFound}
    }
}

func MyTest(t *testing.T) {
     fexec := myFakeExec{LookPathFunc: MakeFakeBinLookPathFunc([]string{ ... })}
     ...
}

Edit: I'm bad at naming things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would I call this function in the first place if I want to change LookPathFunc, considering that's pretty much the only added value of this function?

I guess I like having New methods rather than manually constructing somebody-else's-struct-type by hand. I had added func NewFakeExec(t *testing.T) *FakeExec before adding the cmdsInPath arg.

But yes, I could do it the way you suggested instead.

}

type FakeCommandAction func(cmd string, args ...string) exec.Cmd

func (fake *FakeExec) Command(cmd string, args ...string) exec.Cmd {
if fake.CommandCalls > len(fake.CommandScript)-1 {
panic(fmt.Sprintf("ran out of Command() actions. Could not handle command [%d]: %s args: %v", fake.CommandCalls, cmd, args))
fake.fail("ran out of Command() actions at %s executing %v", getCaller(1), append([]string{cmd}, args...))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we accumulate the errors in a Errors list inside the FakeExec instead? So that we don't depend on testing.T and we don't have to panic, and the test can check Errors however it wants to?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 prefer leaving the "fail" and the error reporting to the client.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, but the Command still has to do something when the code being tested calls CombinedOutput()/Run() on it. So you could have it return an error to the caller, but then you're testing how your code responds when it gets an error that doesn't correspond to anything it could actually get in the real world.

If you hit this point (or any of the other panic/fail points), then there is a bug in your test case, not in the code that it's testing. So the only reasonable behavior is to abort the test case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't disagree with that, but then it should just probably just panic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(If using testing.T is absolutely unacceptable, then we can go back to using panic... I just preferred the t.Fatalf solution because it doesn't spew a gigantic pile of stack traces at you like panic does.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(fwiw, wrote that last comment before seeing your response)


// AddCommand adds a Cmd to be returned by a future fake.Command(...) call. Call
// SetCombinedOutput or SetRunOutput on the result to specify its result.
func (fake *FakeExec) AddCommand(cmd string, args ...string) *FakeCmd {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that this makes things easier, but I'd decouple the command creation a little bit.

Can we have:

// Hides the logic of appending commands
func (fake *FakeExec) AddCommand(cmd *FakeCmd) { ... }

And something else to create the command

// Hides the pain of creating commands
func NewMatchingArgsCommand(cmd string, args ...string) *FakeCmd {
    return func(cmd string, args ...string) exec.Cmd {
        // Some of the logic you have in your command
    }
}

Then it means that:

  1. I can add command easily, but they don't have to follow your logic
  2. I can invent new types of commands

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again +1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to add a command in a different way, you can just continue to use the existing FakeExec API. There is currently only 1 test program in all of kubernetes that wants to do something other than what the API above supports. (pkg/volume/git_repo/git_repo_test.go)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but I think this would make it much easier to compose and re-use the code

@apelisse
Copy link
Member

apelisse commented Aug 9, 2017

Also, this is complicated enough that we may want to test it?

@danwinship
Copy link
Contributor Author

Also, this is complicated enough that we may want to test it?

Yes, although ironically, the code using testing.T can't really be tested. Though the panic-ing versions of those codepaths could be.

@apelisse
Copy link
Member

Yes, although ironically, the code using testing.T can't really be tested.

;-)

@fejta-bot
Copy link

Issues go stale after 90d of inactivity.
Mark the issue as fresh with /remove-lifecycle stale.
Stale issues rot after an additional 30d of inactivity and eventually close.

Prevent issues from auto-closing with an /lifecycle frozen comment.

If this issue is safe to close now please do so with /close.

Send feedback to sig-testing, kubernetes/test-infra and/or @fejta.
/lifecycle stale

@k8s-ci-robot k8s-ci-robot added the lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. label Jan 2, 2018
@thockin
Copy link
Member

thockin commented Jan 2, 2018

fejta-bot is coming after PRs now!

@fejta-bot
Copy link

Resistance is futile. Your issues and PRs will be assimilated. Their historical and technological distinctiveness will be added to our org.

@fejta-bot
Copy link

Stale issues rot after 30d of inactivity.
Mark the issue as fresh with /remove-lifecycle rotten.
Rotten issues close after an additional 30d of inactivity.

If this issue is safe to close now please do so with /close.

Send feedback to sig-testing, kubernetes/test-infra and/or fejta.
/lifecycle rotten
/remove-lifecycle stale

@k8s-ci-robot k8s-ci-robot added lifecycle/rotten Denotes an issue or PR that has aged beyond stale and will be auto-closed. and removed lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. labels Feb 7, 2018
@thockin
Copy link
Member

thockin commented Feb 20, 2018

@danwinship is this still alive?

@fejta-bot
Copy link

Rotten issues close after 30d of inactivity.
Reopen the issue with /reopen.
Mark the issue as fresh with /remove-lifecycle rotten.

Send feedback to sig-testing, kubernetes/test-infra and/or fejta.
/close

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lifecycle/rotten Denotes an issue or PR that has aged beyond stale and will be auto-closed.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants