From 8869eac4072bd01dcfe1be4c3f25c64caf19372f Mon Sep 17 00:00:00 2001 From: Benjamin Heatwole Date: Thu, 14 Feb 2019 08:41:36 -0500 Subject: [PATCH 1/9] Added support for go modules outside of GOPATH --- internal/gopath/gopath.go | 69 +++++++++++++++++++++++++++++++++- internal/gopath/gopath_test.go | 30 +++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/internal/gopath/gopath.go b/internal/gopath/gopath.go index c9b66167a3..9325554b9d 100644 --- a/internal/gopath/gopath.go +++ b/internal/gopath/gopath.go @@ -3,11 +3,15 @@ package gopath import ( "fmt" "go/build" + "io/ioutil" + "os" "path/filepath" + "regexp" "strings" ) var NotFound = fmt.Errorf("not on GOPATH") +var ModuleNameRegexp = regexp.MustCompile(`module\s+(.*)`) // Contains returns true if the given directory is in the GOPATH func Contains(dir string) bool { @@ -24,7 +28,32 @@ func Dir2Import(dir string) (string, error) { return dir[len(gopath)+1:], nil } } - return "", NotFound + + // The following code handles the go modules in a manner that does not break existing code. However, it is not + // efficient because it requires several round trips to the filesystem. It also should technically be the first + // method we try (before GOPATH) because if someone creates a 'go mod' project that also happens to be in the + // GOPATH, the import path will be calculated from GOPATH and not from the go.mod file. + // + // Possible Fixes: + // 1) Cache the results of scanning the filesystem. The code is typically called at compile-time, so its unlikely + // for a go.mod file to be added or altered during compile. + // 2) Add an optional switch that turns off GOPATH checks entirely when go.mod is used on a project. + // 3) Rewrite gqlgen to make use of 'golang.org/x/tools/go/packages' for determining import paths instead. + + // Scan the path tree for a directory that contains a 'go.mod' file and read the module name from it + modDirectory := findGoMod(dir) + if 0 == len(modDirectory) { + return "", NotFound + } + modName := moduleName(filepath.Join(modDirectory, "go.mod")) + if 0 == len(modName) { + return "", NotFound + } + + // At this point 'dir' looks something like '/root/path/to/some/dir', 'modDirectory' looks like '/root/path', + // and 'modName' looks like 'grabhub.com/myname/vunderprojekt'. The correct import path is: + // 'grabhub.com/myname/vunderprojekt/to/some/dir' + return fmt.Sprintf("%s%s", modName, strings.TrimPrefix(dir, filepath.ToSlash(modDirectory))), nil } // MustDir2Import takes an *absolute* path and returns a golang import path for the package, and panics if it isn't on the gopath @@ -35,3 +64,41 @@ func MustDir2Import(dir string) string { } return pkg } + +// Returns the path to the first go.mod file in the parent tree starting with the specified directory. Returns "" if not found +func findGoMod(srcDir string) string { + abs, err := filepath.Abs(srcDir) + if err != nil { + return "" + } + for { + info, err := os.Stat(filepath.Join(abs, "go.mod")) + if err == nil && !info.IsDir() { + break + } + d := filepath.Dir(abs) + if len(d) >= len(abs) { + return "" // reached top of file system, no go.mod + } + abs = d + } + + return abs +} + +// Returns the main module name from a go.mod file. Returns "" if it cannot be found +func moduleName(file string) string { + data, err := ioutil.ReadFile(file) + if nil != err { + return "" + } + + // Search for `module some/name` + matches := ModuleNameRegexp.FindSubmatch(data) + if len(matches) < 2 { + return "" + } + + // The first element is the whole line, the second is the capture group we specified for the module name. + return string(matches[1]) +} diff --git a/internal/gopath/gopath_test.go b/internal/gopath/gopath_test.go index 847ad1e856..4088399fe0 100644 --- a/internal/gopath/gopath_test.go +++ b/internal/gopath/gopath_test.go @@ -2,6 +2,9 @@ package gopath import ( "go/build" + "io/ioutil" + "os" + "path/filepath" "runtime" "testing" @@ -12,6 +15,13 @@ func TestContains(t *testing.T) { origBuildContext := build.Default defer func() { build.Default = origBuildContext }() + // Make a temporary directory and add a go.mod file for the package 'foo' + fooDir, err := ioutil.TempDir("", "gopath") + assert.Nil(t, err) + defer os.RemoveAll(fooDir) + err = ioutil.WriteFile(filepath.Join(fooDir, "go.mod"), []byte("module foo\n\nrequire ()"), 0644) + assert.Nil(t, err) + if runtime.GOOS == "windows" { build.Default.GOPATH = `C:\go;C:\Users\user\go` @@ -22,6 +32,10 @@ func TestContains(t *testing.T) { assert.False(t, Contains(`C:\tmp`)) assert.False(t, Contains(`C:\Users\user`)) assert.False(t, Contains(`C:\Users\another\go`)) + + // C:/Users/someone/AppData/Local/Temp/gopath123456/bar + assert.True(t, Contains(filepath.Join(fooDir, "bar"))) + } else { build.Default.GOPATH = "/go:/home/user/go" @@ -31,6 +45,9 @@ func TestContains(t *testing.T) { assert.False(t, Contains("/tmp")) assert.False(t, Contains("/home/user")) assert.False(t, Contains("/home/another/go")) + + // /tmp/gopath123456/bar + assert.True(t, Contains(filepath.Join(fooDir, "bar"))) } } @@ -38,6 +55,13 @@ func TestDir2Package(t *testing.T) { origBuildContext := build.Default defer func() { build.Default = origBuildContext }() + // Make a temporary directory and add a go.mod file for the package 'foo' + fooDir, err := ioutil.TempDir("", "gopath") + assert.Nil(t, err) + defer os.RemoveAll(fooDir) + err = ioutil.WriteFile(filepath.Join(fooDir, "go.mod"), []byte("module foo\n\nrequire ()"), 0644) + assert.Nil(t, err) + if runtime.GOOS == "windows" { build.Default.GOPATH = "C:/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;C:/a/y;C:/b/" @@ -49,6 +73,9 @@ func TestDir2Package(t *testing.T) { assert.PanicsWithValue(t, NotFound, func() { MustDir2Import("C:/tmp/foo") }) + + // C:/Users/someone/AppData/Local/Temp/gopath123456/bar + assert.Equal(t, "foo/bar", MustDir2Import(filepath.Join(fooDir, "bar"))) } else { build.Default.GOPATH = "/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:/a/y:/b/" @@ -58,5 +85,8 @@ func TestDir2Package(t *testing.T) { assert.PanicsWithValue(t, NotFound, func() { MustDir2Import("/tmp/foo") }) + + // /tmp/gopath123456/bar + assert.Equal(t, "foo/bar", MustDir2Import(filepath.Join(fooDir, "bar"))) } } From 02afc00928e412cf5e8578c2070fb41dd9aa52a7 Mon Sep 17 00:00:00 2001 From: Benjamin Heatwole Date: Thu, 14 Feb 2019 11:04:21 -0500 Subject: [PATCH 2/9] Updated Getting Started to remove 'dep' and use go modules --- docs/content/getting-started.md | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/docs/content/getting-started.md b/docs/content/getting-started.md index 9ea218afc2..6155df7e9d 100644 --- a/docs/content/getting-started.md +++ b/docs/content/getting-started.md @@ -16,18 +16,13 @@ You can find the finished code for this tutorial [here](https://github.com/vekta ## Install gqlgen -This article uses [`dep`](https://github.com/golang/dep) to install gqlgen. [Follow the instructions for your environment](https://github.com/golang/dep) to install. - -Assuming you already have a working [Go environment](https://golang.org/doc/install), create a directory for the project in your `$GOPATH`: +Assuming you already have a working [Go environment](https://golang.org/doc/install) using Go 1.11 or higher, create a directory for the project: ```sh -$ mkdir -p $GOPATH/src/github.com/[username]/gqlgen-todos +$ mkdir -p ~/github.com/[username]/gqlgen-todos +$ cd ~/github.com/[username]/gqlgen-todos ``` -> Go Modules -> -> Currently `gqlgen` does not support Go Modules. This is due to the [`loader`](https://godoc.org/golang.org/x/tools/go/loader) package, that also does not yet support Go Modules. We are looking at solutions to this and the issue is tracked in Github. - Add the following file to your project under `scripts/gqlgen.go`: ```go @@ -42,12 +37,6 @@ func main() { } ``` -Lastly, initialise dep. This will inspect any imports you have in your project, and pull down the latest tagged release. - -```sh -$ dep init -``` - ## Building the server ### Define the schema @@ -85,6 +74,7 @@ type Mutation { ### Create the project skeleton ```bash +$ mkdir server $ go run scripts/gqlgen.go init ``` @@ -96,11 +86,6 @@ This has created an empty skeleton with all files you need: - `resolver.go` — This is where your application code lives. `generated.go` will call into this to get the data the user has requested. - `server/server.go` — This is a minimal entry point that sets up an `http.Handler` to the generated GraphQL server. - Now run dep ensure, so that we can ensure that the newly generated code's dependencies are all present: - - ```sh - $ dep ensure - ``` ### Create the database models From f91c1a306946b8fe9a5071a753e1464852e7c873 Mon Sep 17 00:00:00 2001 From: Benjamin Heatwole Date: Thu, 14 Feb 2019 11:10:58 -0500 Subject: [PATCH 3/9] Added go.mod file for current version of gqlgen --- go.mod | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 go.mod diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000..a931da7c0c --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module github.com/99designs/gqlgen + +require ( + github.com/agnivade/levenshtein v0.0.0-20180303095733-1787a73e302c + github.com/davecgh/go-spew v1.1.0 + github.com/go-chi/chi v3.3.2+incompatible + github.com/gogo/protobuf v1.0.0 + github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f + github.com/gorilla/mux v1.6.1 + github.com/gorilla/websocket v1.2.0 + github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47 + github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 + github.com/opentracing/basictracer-go v1.0.0 + github.com/opentracing/opentracing-go v1.0.2 + github.com/pkg/errors v0.8.0 + github.com/pmezard/go-difflib v1.0.0 + github.com/rs/cors v1.6.0 + github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 + github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0 + github.com/stretchr/testify v1.2.1 + github.com/urfave/cli v1.20.0 + github.com/vektah/dataloaden v0.0.0-20180713105249-314ac81052ee + github.com/vektah/gqlparser v1.1.0 + golang.org/x/net v0.0.0-20180404174746-b3c676e531a6 + golang.org/x/tools v0.0.0-20180215025520-ce871d178848 + gopkg.in/yaml.v2 v2.2.1 + sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755 + sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67 +) From e10d85a4bd91d073350abcd698c7138cf3198064 Mon Sep 17 00:00:00 2001 From: Benjamin Heatwole Date: Thu, 14 Feb 2019 11:17:05 -0500 Subject: [PATCH 4/9] Fixed missing 'go mod init' line --- docs/content/getting-started.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/content/getting-started.md b/docs/content/getting-started.md index 6155df7e9d..4bc9c56a5b 100644 --- a/docs/content/getting-started.md +++ b/docs/content/getting-started.md @@ -21,6 +21,7 @@ Assuming you already have a working [Go environment](https://golang.org/doc/inst ```sh $ mkdir -p ~/github.com/[username]/gqlgen-todos $ cd ~/github.com/[username]/gqlgen-todos +$ go mod init github.com/[username]/gqlgen-todos ``` Add the following file to your project under `scripts/gqlgen.go`: From 40bc329ff31c1104625def37be877522efc497fa Mon Sep 17 00:00:00 2001 From: Benjamin Heatwole Date: Thu, 14 Feb 2019 12:06:03 -0500 Subject: [PATCH 5/9] Fixed linting issue --- internal/gopath/gopath_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/gopath/gopath_test.go b/internal/gopath/gopath_test.go index 4088399fe0..710ffae70e 100644 --- a/internal/gopath/gopath_test.go +++ b/internal/gopath/gopath_test.go @@ -18,7 +18,7 @@ func TestContains(t *testing.T) { // Make a temporary directory and add a go.mod file for the package 'foo' fooDir, err := ioutil.TempDir("", "gopath") assert.Nil(t, err) - defer os.RemoveAll(fooDir) + defer func() { err := os.RemoveAll(fooDir); assert.Nil(t, err) }() err = ioutil.WriteFile(filepath.Join(fooDir, "go.mod"), []byte("module foo\n\nrequire ()"), 0644) assert.Nil(t, err) @@ -58,7 +58,7 @@ func TestDir2Package(t *testing.T) { // Make a temporary directory and add a go.mod file for the package 'foo' fooDir, err := ioutil.TempDir("", "gopath") assert.Nil(t, err) - defer os.RemoveAll(fooDir) + defer func() { err := os.RemoveAll(fooDir); assert.Nil(t, err) }() err = ioutil.WriteFile(filepath.Join(fooDir, "go.mod"), []byte("module foo\n\nrequire ()"), 0644) assert.Nil(t, err) From 8f44e53bc6be927dab694cb9c19ae6590872c274 Mon Sep 17 00:00:00 2001 From: Benjamin Heatwole Date: Fri, 15 Feb 2019 10:48:59 -0500 Subject: [PATCH 6/9] Fixed additional lint issue --- internal/gopath/gopath_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/gopath/gopath_test.go b/internal/gopath/gopath_test.go index 710ffae70e..12a57f3c0d 100644 --- a/internal/gopath/gopath_test.go +++ b/internal/gopath/gopath_test.go @@ -18,7 +18,7 @@ func TestContains(t *testing.T) { // Make a temporary directory and add a go.mod file for the package 'foo' fooDir, err := ioutil.TempDir("", "gopath") assert.Nil(t, err) - defer func() { err := os.RemoveAll(fooDir); assert.Nil(t, err) }() + defer func() { e := os.RemoveAll(fooDir); assert.Nil(t, e) }() err = ioutil.WriteFile(filepath.Join(fooDir, "go.mod"), []byte("module foo\n\nrequire ()"), 0644) assert.Nil(t, err) @@ -58,7 +58,7 @@ func TestDir2Package(t *testing.T) { // Make a temporary directory and add a go.mod file for the package 'foo' fooDir, err := ioutil.TempDir("", "gopath") assert.Nil(t, err) - defer func() { err := os.RemoveAll(fooDir); assert.Nil(t, err) }() + defer func() { e := os.RemoveAll(fooDir); assert.Nil(t, e) }() err = ioutil.WriteFile(filepath.Join(fooDir, "go.mod"), []byte("module foo\n\nrequire ()"), 0644) assert.Nil(t, err) From ba1ee03b94b47230781e41afcdbfc919e8fde43e Mon Sep 17 00:00:00 2001 From: Benjamin Heatwole Date: Fri, 15 Feb 2019 11:01:52 -0500 Subject: [PATCH 7/9] Fixed test that randomly fails due to variablity of goroutines --- codegen/testserver/generated_test.go | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/codegen/testserver/generated_test.go b/codegen/testserver/generated_test.go index 704869a081..ddf8fd3e35 100644 --- a/codegen/testserver/generated_test.go +++ b/codegen/testserver/generated_test.go @@ -9,17 +9,15 @@ import ( "net/http" "net/http/httptest" "reflect" - "runtime" "sort" "sync" "testing" - "time" - - "github.com/99designs/gqlgen/graphql/introspection" "github.com/99designs/gqlgen/client" "github.com/99designs/gqlgen/graphql" + "github.com/99designs/gqlgen/graphql/introspection" "github.com/99designs/gqlgen/handler" + "github.com/fortytw2/leaktest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -125,7 +123,7 @@ func TestGeneratedServer(t *testing.T) { t.Run("subscriptions", func(t *testing.T) { t.Run("wont leak goroutines", func(t *testing.T) { - initialGoroutineCount := runtime.NumGoroutine() + defer leaktest.Check(t)() sub := c.Websocket(`subscription { updated }`) @@ -141,14 +139,6 @@ func TestGeneratedServer(t *testing.T) { require.NoError(t, err) require.Equal(t, "message", msg.resp.Updated) sub.Close() - - // need a little bit of time for goroutines to settle - start := time.Now() - for time.Since(start).Seconds() < 2 && initialGoroutineCount != runtime.NumGoroutine() { - time.Sleep(5 * time.Millisecond) - } - - require.Equal(t, initialGoroutineCount, runtime.NumGoroutine()) }) t.Run("will parse init payload", func(t *testing.T) { From 5caf1d5196b7fb35d60ba9fa23756126deb96a27 Mon Sep 17 00:00:00 2001 From: Benjamin Heatwole Date: Fri, 15 Feb 2019 11:46:59 -0500 Subject: [PATCH 8/9] Added dep for leaktest --- Gopkg.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Gopkg.toml b/Gopkg.toml index 7876142a7e..2552933030 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -27,3 +27,7 @@ required = ["github.com/vektah/dataloaden"] [[constraint]] name = "github.com/rs/cors" version = "1.6.0" + +[[constraint]] + name = "github.com/fortytw2/leaktest" + version = "1.3.0" From e365696a423952c93e9ad78041db44824c725923 Mon Sep 17 00:00:00 2001 From: Benjamin Heatwole Date: Fri, 15 Feb 2019 14:23:09 -0500 Subject: [PATCH 9/9] Added leaktest --- Gopkg.lock | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Gopkg.lock b/Gopkg.lock index ba7c4bd22b..a50c265d18 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -17,6 +17,13 @@ revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" +[[projects]] + name = "github.com/fortytw2/leaktest" + packages = ["."] + pruneopts = "UT" + revision = "9a23578d06a26ec1b47bfc8965bf5e7011df8bd6" + version = "v1.3.0" + [[projects]] digest = "1:66ddebb274faa160a4a23394a17ad3c8e15fee9bf5408d13f77d22b61bc7f072" name = "github.com/go-chi/chi" @@ -242,6 +249,7 @@ analyzer-name = "dep" analyzer-version = 1 input-imports = [ + "github.com/fortytw2/leaktest", "github.com/go-chi/chi", "github.com/gorilla/websocket", "github.com/hashicorp/golang-lru",