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

Use go-cmp instead of reflect.DeepEqual #155

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from

Conversation

hslatman
Copy link

As discussed in #99, the branch had to be cleared from merge conflicts before go-cmp support could be integrated. That's what I did in this PR.

@googlebot
Copy link
Collaborator

All (the pull request submitter and all commit authors) CLAs are signed, but one or more commits were authored or co-authored by someone other than the pull request submitter.

We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that by leaving a comment that contains only @googlebot I consent. in this pull request.

Note to project maintainer: There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent. In those cases, you can manually confirm consent of the commit author(s), and set the cla label to yes (if enabled on your project).

ℹ️ Googlers: Go here for more info.

@hslatman
Copy link
Author

@googlebot I consent.

@coveralls
Copy link

coveralls commented Mar 24, 2021

Coverage Status

Coverage increased (+0.06%) to 95.678% when pulling 0a209c7 on hslatman:go-cmp into 90387e1 on cweill:develop.

@butuzov
Copy link
Contributor

butuzov commented Mar 27, 2021

Hi @hslatman can you merge develop into your feature? I believe I brought some conflicts to your code.

@hslatman
Copy link
Author

@butuzov I've merged develop and regenerated the bindata.

While the tests run successfully on GitHub Actions I have two failing tests locally. Is there something special I need to do to to run the tests locally successfully too?

@butuzov
Copy link
Contributor

butuzov commented Mar 28, 2021

I would suggest do not run them with -race or concurrently atm. I have also run tests and non failed.

@cweill can you also check this PR?

Copy link
Contributor

@butuzov butuzov left a comment

Choose a reason for hiding this comment

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

Maybe we %s placeholders can be used instead of %v in all places now?

gotests_test.go Outdated
@@ -918,7 +919,7 @@ func TestGenerateTests(t *testing.T) {
return
}
if got := string(gts[0].Output); got != tt.want {
t.Errorf("%q. GenerateTests(%v) = \n%v, want \n%v", tt.name, tt.args.srcPath, got, tt.want)
t.Errorf("%q. GenerateTests(%v) = diff=%v", tt.name, tt.args.srcPath, cmp.Diff(got, tt.want))
Copy link
Contributor

@butuzov butuzov Mar 28, 2021

Choose a reason for hiding this comment

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

Would it be possible to create maybe a custom reporter so we can get got/want with a lines of the diff instead -/+ ?

also %v now can be %s

Copy link
Author

Choose a reason for hiding this comment

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

The custom reporter approach certainly looks interesting. So far I just updated @mekegi's code, but I could give integrating the custom reporter a try.

So your goal would be to print the output of the custom reporter instead of the default cmp.Diff(x, y), right? It seems doable for this specific line of code, i.e. in the gotests_test.go it should be fairly trivial to add.

Do you mean to also use the custom reporter for the generated tests? And by extension, making that configurable by the user of gotests too? Or am I way off and did you mean to just put it in gotests_test.go?

Copy link
Author

@hslatman hslatman Mar 29, 2021

Choose a reason for hiding this comment

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

Taking the example DiffReporter and changing the lines to this:

var r reporter.DiffReporter
if got := string(gts[0].Output); !cmp.Equal(got, tt.want, cmp.Reporter(&r)) {
    t.Errorf("%q. GenerateTests(%v) = diff=%s", tt.name, tt.args.srcPath, r.String())
    outputResult(t, tmp, tt.name, gts[0].Output)
}

// basic Report() function, slightly altered from the example
func (r *DiffReporter) Report(rs cmp.Result) {
	if !rs.Equal() {
		vx, vy := r.path.Last().Values()
		r.diffs = append(r.diffs, fmt.Sprintf("\n\nGot:\n\n%+v\n\nWant:\n\n%+v\n", vx, vy))
	}
}

Results in the following output:

    gotests_test.go:924: "Receiver is indirect imported struct". GenerateTests(testdata/test037.go) = diff=

        Got:

        package testdata

        import "testing"

        func Test_someIndirectImportedStruct_Foo037(t *testing.T) {
        	tests := []struct {
        		name string
        		smtg *someIndirectImportedStruct
        	}{
        		// TODO: Add test cases.
        	}
        	for _, tt := range tests {
        		tt.smtg.Foo037()
        	}
        }


        Want:

        package testdata

        import "testing"

        func Test_someIndirectImportedStruct_Foo037(t *testing.T) {
        	tests := []struct {
        		name string
        		smtg *someIndirectImportedStruct
        	}{
        		// TODO: Add test cases.
        	}
        	for _, tt := range tests {
        		smtg := &someIndirectImportedStruct{}
        		smtg.Foo037()
        	}
        }

    gotests_test.go:958: /var/folders/m1/h8xf_67j5fqccwr1277f3_zc0000gn/T/gotests_test230212921/receiver_is_indirect_imported_struct.go

This doesn't seem really useful, but it may be possible to customize it down to the actual lines that are different.

If I understand you correctly, you want to show just got/want and the corresponding lines, correct? I find the -/+ quite easy to use because the differences are closer to each other, so I'm not sure if we should drop that, unless you want it to become configurable for a user of gotests?

Copy link
Contributor

Choose a reason for hiding this comment

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

I was talking only about generated tests and you are right without granular line detection it does not seem really useful.

Copy link
Owner

Choose a reason for hiding this comment

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

+1 to keeping the -/+. Without them it's very difficult to know what the delta is.

Copy link
Author

@hslatman hslatman Apr 1, 2021

Choose a reason for hiding this comment

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

I've only scratched the surface of what may be possible using the custom reporter. It may very well be possible to print a compact version of the -/+ delta, but that'll likely require becoming a bit more acquainted with the go-cmp reporting code. Could be something for the future, perhaps?

gotests.go Outdated
Comment on lines 19 to 24
var (
cmpImport = &models.Import{
Name: "",
Path: `"github.com/google/go-cmp/cmp"`,
}
)
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: is there any other data it should be grouped in the future?

Suggested change
var (
cmpImport = &models.Import{
Name: "",
Path: `"github.com/google/go-cmp/cmp"`,
}
)
var cmpImport = &models.Import{
Name: "",
Path: `"github.com/google/go-cmp/cmp"`,
}

Copy link
Contributor

@butuzov butuzov left a comment

Choose a reason for hiding this comment

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

really minor nit

@butuzov
Copy link
Contributor

butuzov commented Mar 30, 2021

ping @cweill

@@ -35,6 +35,9 @@ jobs:
- name: Get Dependencies (github.com/golang.org/x/tools/imports)
run: go get -v ./...

- name: Get go-cmp
run: go get github.com/google/go-cmp/cmp
Copy link
Owner

Choose a reason for hiding this comment

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

Why do we need to manually fetch this package? If a user forgets to do this will gotests not work for them? IMO go get -v ./... should cover installing this dependency.

Copy link
Author

Choose a reason for hiding this comment

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

Without this explicit call the tests will fail in GitHub Actions for Go 1.9 - 1.12:

Run go test -v ./...
  go test -v ./...
  shell: /usr/bin/bash -e {0}
  env:
    GOPATH: /home/runner/work/gotests/gotests
    GO111MODULE: auto
    GOROOT: /opt/hostedtoolcache/go/1.12.17/x64
# github.com/cweill/gotests
FAIL	github.com/cweill/gotests [setup failed]
gotests_test.go:16:2: cannot find package "github.com/google/go-cmp/cmp" in any of:
	/opt/hostedtoolcache/go/1.12.17/x64/src/github.com/google/go-cmp/cmp (from $GOROOT)
	/home/runner/work/gotests/gotests/src/github.com/google/go-cmp/cmp (from $GOPATH)
FAIL	github.com/cweill/gotests/gotests [setup failed]

Other solutions to make this work may exist. The GO111MODULE env var could be set to on, but that'll likely only work for Go 1.11 and up. To me this seemed the easiest solution to make it work across versions without having to make the CI configuration more complex.

I tried the go-cmp version of gotests locally on a sample project. It resulted in a _test.go file with github.com/google/go-cmp/cmp in the import section. Running the tests for the first time results in go-cmp being inserted into go.mod. The go.mod of gotests itself does contain go-cmp, so that should work correctly too.

Copy link
Owner

Choose a reason for hiding this comment

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

My concern is for who don't have go-cmp installed, will this change break them unless they manually install the package? Is there any way we can install it automatically when they install gotests?

@cweill
Copy link
Owner

cweill commented Apr 1, 2021

@mekegi Could you please reply @googlebot I consent. to this PR?

@mekegi
Copy link
Contributor

mekegi commented Apr 1, 2021

@googlebot I consent

@googlebot
Copy link
Collaborator

CLAs look good, thanks!

ℹ️ Googlers: Go here for more info.

@googlebot googlebot added cla: yes Contributor signed Google CLA and removed cla: no labels Apr 1, 2021
@hslatman
Copy link
Author

hslatman commented Apr 1, 2021

I'm curious to know if the output format is OK for you as it is now. It looks like this now:

        diff=  (*client.Client)(
        - 	&{
        - 		key:               "key",
        - 		httpClient:        &http.Client{Timeout: s"30s"},
        - 		baseURL:           s"https://example.com/api",
        - 		contentTypeHeader: "application/json",
        - 		acceptHeader:      "application/json",
        - 	},
        + 	nil,
          )

One other thing that I found out about is that having unexported fields in structs will result in panics when running tests if they are not explicitly allowed, as mentioned in the third point in the go-cmp repo:

Unlike reflect.DeepEqual, unexported fields are not compared by default; they result in panics unless suppressed by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared using the AllowUnexported option.

One would thus have to change the generated !cmp.Equal(got, tt.want) and cmp.Diff(got, tt.want) to something like the below (or one of the other options mentioned above):

if !cmp.Equal(got, tt.want, cmpopts.IgnoreUnexported(StructUnderTest{})) {
	t.Errorf("%q. New() = %v, want %v\ndiff=%s", tt.name, got, tt.want, cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(StructUnderTest{})))
}

@cweill
Copy link
Owner

cweill commented Apr 1, 2021

I'm curious to know if the output format is OK for you as it is now. It looks like this now:

Looks good to me.

One other thing that I found out about is that having unexported fields in structs will result in panics when running tests if they are not explicitly allowed, as mentioned in the third point in the go-cmp repo:

Unlike reflect.DeepEqual, unexported fields are not compared by default; they result in panics unless suppressed by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared using the AllowUnexported option.

One would thus have to change the generated !cmp.Equal(got, tt.want) and cmp.Diff(got, tt.want) to something like the below (or one of the other options mentioned above):

I think we should stick to what's simplest, if that means it can panic, at least users can modify the generated boilerplate code to match what they need.

A bigger thing is, I'm not sure if using go-cmp by the default is the right approach. The Golang philosophy is to use the stdlib for most things, and only third-party packages if necessary. Since go-cmp is a third-party package, I'd suggest flag protecting it behind a flag like --use_go_cmp, and using DeepEqual by default (see #99 (comment)). This way gotests users can opt into go-cmp only if they want it. In a future release we can consider making go-cmp the default.

@@ -35,6 +35,9 @@ jobs:
- name: Get Dependencies (github.com/golang.org/x/tools/imports)
run: go get -v ./...

- name: Get go-cmp
run: go get github.com/google/go-cmp/cmp
Copy link
Owner

Choose a reason for hiding this comment

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

My concern is for who don't have go-cmp installed, will this change break them unless they manually install the package? Is there any way we can install it automatically when they install gotests?

@mrsufgi
Copy link

mrsufgi commented Mar 9, 2022

Any updates on this?

@hslatman
Copy link
Author

It's been on the backburner for me for quite a while, but I may be able to give this some attention again sometime soon.

When the `--use_go_cmp` flag is provided, equality checks will
use `cmp.Equal` (https://pkg.go.dev/github.com/google/go-cmp/cmp)
instead of `reflect.DeepEqual`. Differences between two objects
will be reported using `cmp.Diff`.

The tests that used `reflect.DeepEqual` have been duplicated, so
that all behavior that was tested is now also tested when using
`google/go-cmp`.
@hslatman
Copy link
Author

hslatman commented Mar 12, 2022

@cweill: can you start the tests?

Added the -use_go_cmp flag, so usage is now conditional. Reverted old test files to using reflect.DeepEqual again and made copies of relevant tests to use cmp.Equal and cmp.Diff. I've switched around the order of want and got for cmp.Diff, because I think the output is easier to interpret this way.

About having to install github.com/google/go-cmp: the changes in the workflow are necessary because I've also changed usages of reflect.DeepEqual used in the tests to cmp.Equal. @mekegi already created logic for adding the github.com/google/co-cmp import to the header only when it's required. When a user first runs gotests and creates tests with -use_go_cmp enabled, it's sufficient to then run go mod tidy manually once afterwards to ensure go-cmp can be used. It may be possible to automate that, but I don't think gotests is currently aware of the context it's executed in, so that it knows if it has to run go mod tidy or not. I think that the message shown by the Go tooling when running the generated tests for the first time is quite clear also:

go test ./...
# trygotest
t1_test.go:6:2: no required module provides package github.com/google/go-cmp/cmp; to add it:
	go get github.com/google/go-cmp/cmp
FAIL	trygotest [setup failed]
FAIL

Simple example failing test output:

type c struct {
	A int
	B string
}

func t1(b string) c {
	return c{
		A: 1,
		B: b,
	}
}

func Test_t1(t *testing.T) {
	type args struct {
		b string
	}
	tests := []struct {
		name string
		args args
		want c
	}{
		{
			name: "bla",
			args: args{
				b: "d",
			},
			want: c{
				A: 2,
				B: "e",
			},
		},
	}
	for _, tt := range tests {
		if got := t1(tt.args.b); !cmp.Equal(tt.want, got) {
			t.Errorf("%q. t1() = %v, want %v\ndiff=%s", tt.name, got, tt.want, cmp.Diff(tt.want, got))
		}
	}
}
go test ./...
--- FAIL: Test_t1 (0.00s)
    t1_test.go:31: "bla". t1() = {1 d}, want {2 e}
        diff=  main.c{
        - 	A: 2,
        + 	A: 1,
        - 	B: "e",
        + 	B: "d",
          }
FAIL
FAIL	trygotest	3.995s
FAIL

@hslatman
Copy link
Author

hslatman commented Mar 13, 2022

@cweill: I've added another commit that removes some old Go versions from the CI (as well as add 1.17.x), which makes the tests pass again: https://github.com/hslatman/gotests/actions/runs/1976091466.

The error I got is the same as in the issue that @davidhsingyuchen reported in #168.

@edgio-mwodrich
Copy link

Digging into cmp.Diff a little, it runs cmp.Equal internally itself, and will produce string output if and only if cmp.Equal is false (if they ever disagree, it panics), so instead of

if !cmp.Equal(got, tt.want, opts) {
    t.Errorf("Function diff(got, want):\n%s", cmp.Diff(got, tt.want, opts))
}

a simplification for performance and maintainability I've adopted in gotests I've converted to use cmp.Diff is to do something like

if diff := cmp.Diff(got, tt.want, opts); diff != "" {
    t.Errorf("Function diff(got, want):\n%s", diff)
}

to avoid redundant runs of cmp.Equal and having to keep separate cmp.Equal and cmp.Diff invocations in sync, especially when using cmpopts.

Either way, I'd be very excited to use this type of functionality in my use of gotests, and I hope that folks might have time to move it forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes Contributor signed Google CLA
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants