Repository: goadesign/goa
Pull request #2182 (2019-07-06 10:50:51)

V3: Fix to use the version of goa specified in go.mod file, when 'goa gen'/'goa example'
Fix issue #2181

raphael: Thank you for the great issue report and the PR! A couple of questions:

1. Would it make sense to search for "go.mod" recursively going up the directories to support the case where a single Go module hosts multiple Goa projects, e.g.:
```
root
├── go.mod
├── project1
│   └── design
└── project2
    └── design
```
Here the `goa` command would be run within `project1` and `project2` and it would be nice if it could pickup the `go.mod` file under `root` like the `go` command does.

2. Instead of adding a dependency on a third party package ("github.com/sirkon/goproxy/gomod") would it be better to just copy the `modfile` package from the Go source code: https://github.com/golang/go/tree/master/src/cmd/go/internal/modfile. It's unfortunate that it is an internal package but it seems better to copy and give proper attribution than to add a dependency on another package.

Thank you again, this is a great contribution!
ikawaha: Thank you for your comment !

1.
The location of go.mod to be used can be found by `go env`.
(cf. https://dave.cheney.net/2013/06/14/you-dont-need-to-set-goroot-really)

```
$ echo $GOMOD

$ go env GOMOD
/Users/ikawaha/go/src/github.com/ikawaha/calc/go.mod
```
So, I will fix to use `go env MOD` instead of "go.mod".

2. 
That's right.  
However, go/internal/modfile (It can not be used alone. I think there needs modfetch, modload etc) seems a bit complicated. I think that it is best to be porting go/internal, but how about using this package until then?

raphael: Thinking a bit more about parsing `go.mod`: we really don't need a full parser. All we need is to find the line that starts with `goa.design/goa/v3` and capture what's after. Seems a little heuristic here might be better than bringing in all these dependencies. Thank you!
ikawaha: I replaced the third party package with a heuristic parser.
Does it make sense?
ikawaha: It may be better to output the version of goa when go get. 🤔 
raphael: The parser looks great! Thank you for making the change. I'm happy to merge the PR as is.

Patch:
diff --git a/cmd/goa/gen.go b/cmd/goa/gen.go
index d57a3bd8..f132e95c 100644
--- a/cmd/goa/gen.go
+++ b/cmd/goa/gen.go
@@ -1,9 +1,11 @@
 package main
 
 import (
+	"bufio"
 	"fmt"
 	"go/parser"
 	"go/token"
+	"io"
 	"io/ioutil"
 	"os"
 	"os/exec"
@@ -141,11 +143,59 @@ func (g *Generator) Write(debug bool) error {
 	return err
 }
 
+const goModEnvKey = "GOMOD"
+
+func findGoMod() string {
+	env := os.Getenv(goModEnvKey)
+	if _, err := exec.LookPath("go"); err != nil {
+		return env
+	}
+	mod, err := exec.Command("go", "env", goModEnvKey).Output()
+	if err != nil {
+		return env
+	}
+	return strings.TrimSpace(string(mod))
+}
+
+func (g *Generator) goaPackage() (string, error) {
+	goaPkg := "goa.design/goa"
+	if g.DesignVersion < 3 {
+		return goaPkg, nil
+	}
+	goaPkg = fmt.Sprintf("goa.design/goa/v%d", g.DesignVersion)
+	path := findGoMod()
+	if _, err := os.Stat(path); err != nil {
+		return goaPkg, nil
+	}
+	fp, err := os.Open(path)
+	if err != nil {
+		return "", err
+	}
+	defer fp.Close()
+	return parseGoModGoaPackage(goaPkg, fp)
+}
+
+var reMod = regexp.MustCompile(`^\s*(?:require )?\s*(goa\.design/goa/v\d+?)\s+([^\/]\S+?)\s*(?:\/\/.+)?$`)
+
+func parseGoModGoaPackage(pkg string, r io.Reader) (string, error) {
+	s := bufio.NewScanner(r)
+	for s.Scan() {
+		match := reMod.FindStringSubmatch(s.Text())
+		if len(match) == 3 && match[1] == pkg {
+			return match[1] + "@" + match[2], nil
+		}
+	}
+	if err := s.Err(); err != nil {
+		return "", fmt.Errorf("scan error, %v", err)
+	}
+	return pkg, nil
+}
+
 // Compile compiles the generator.
 func (g *Generator) Compile() error {
-	goaPkg := "goa.design/goa"
-	if g.DesignVersion > 2 {
-		goaPkg = fmt.Sprintf("goa.design/goa/v%d", g.DesignVersion)
+	goaPkg, err := g.goaPackage()
+	if err != nil {
+		return err
 	}
 	if err := g.runGoCmd("get", goaPkg); err != nil {
 		return err
diff --git a/grpc/codegen/client.go b/grpc/codegen/client.go
index 42fa0340..9e1a53f8 100644
--- a/grpc/codegen/client.go
+++ b/grpc/codegen/client.go
@@ -240,7 +240,10 @@ func Build{{ .Method.VarName }}Func(grpccli {{ .PkgName }}.{{ .ClientInterface }
 		for _, opt := range cliopts {
 			opts = append(opts, opt)
 		}
-		return grpccli.{{ .Method.VarName }}(ctx{{ if not .Method.StreamingPayload }}, reqpb.({{ .Request.ClientConvert.TgtRef }}){{ end }}, opts...)
+		if reqpb != nil {
+			return grpccli.{{ .Method.VarName }}(ctx{{ if not .Method.StreamingPayload }}, reqpb.({{ .Request.ClientConvert.TgtRef }}){{ end }}, opts...)
+		}
+		return grpccli.{{ .Method.VarName }}(ctx{{ if not .Method.StreamingPayload }}, &{{ .Request.ClientConvert.TgtName }}{}{{ end }}, opts...)
 	}
 }
 `


Test patch:
diff --git a/cmd/goa/gen_test.go b/cmd/goa/gen_test.go
new file mode 100644
index 00000000..46f6df6a
--- /dev/null
+++ b/cmd/goa/gen_test.go
@@ -0,0 +1,210 @@
+package main
+
+import (
+	"reflect"
+	"strings"
+	"testing"
+)
+
+func TestGenerator_goaPackage(t *testing.T) {
+	cases := []struct {
+		Name     string
+		Version  int
+		Expected string
+	}{
+		{
+			Name:     "specify v2",
+			Version:  2,
+			Expected: "goa.design/goa",
+		},
+		{
+			Name:     "specify v3, but go.mod file does not exist",
+			Version:  3,
+			Expected: "goa.design/goa/v3",
+		},
+	}
+	for _, c := range cases {
+		t.Run(c.Name, func(t *testing.T) {
+			pkg, err := (&Generator{
+				DesignVersion: c.Version,
+			}).goaPackage()
+			if err != nil {
+				t.Fatalf("unexpected error, %v", err)
+			}
+			if pkg != c.Expected {
+				t.Errorf("expected %v, got %v", c.Expected, pkg)
+			}
+		})
+	}
+}
+
+func TestParseGoModGoaPackage(t *testing.T) {
+	cases := []struct {
+		Name     string
+		Mod      string
+		Package  string
+		Expected string
+	}{
+		{
+			Name:     "simple require",
+			Mod:      "require    goa.design/goa/v3   v3.0.3-0.20190704022140-85024ebc66dc",
+			Package:  "goa.design/goa/v3",
+			Expected: "goa.design/goa/v3@v3.0.3-0.20190704022140-85024ebc66dc",
+		},
+		{
+			Name: "in require block",
+			Mod: `module calc
+go 1.12
+require (
+        github.com/ikawaha/kagome v1.0.0 // indirect
+        goa.design/goa/v3 v3.0.3-0.20190704022140-85024ebc66dc
+        goa.design/plugins/v3 v3.0.1
+)
+`,
+			Package:  "goa.design/goa/v3",
+			Expected: "goa.design/goa/v3@v3.0.3-0.20190704022140-85024ebc66dc",
+		},
+		{
+			Name: "not found",
+			Mod: `module calc
+go 1.12
+require (
+        github.com/ikawaha/kagome v1.0.0 // indirect
+        goa.design/plugins/v3 v3.0.1
+)
+`,
+			Package:  "goa.design/goa/v3",
+			Expected: "goa.design/goa/v3",
+		},
+		{
+			Name: "with replace",
+			Mod: `module calc
+go 1.12
+replace goa.design/goa/v3 => ../../../goa.design/goa
+require (
+        github.com/ikawaha/kagome v1.0.0 // indirect
+        goa.design/goa/v3 v3.0.3-0.20190704022140-85024ebc66dc
+        goa.design/plugins/v3 v3.0.1
+)
+`,
+			Package:  "goa.design/goa/v3",
+			Expected: "goa.design/goa/v3@v3.0.3-0.20190704022140-85024ebc66dc",
+		},
+		{
+			Name: "with comment",
+			Mod: `module calc
+go 1.12
+replace goa.design/goa/v3 => ../../../goa.design/goa
+require (
+        github.com/ikawaha/kagome v1.0.0 // indirect
+        goa.design/goa/v3 v3.0.3-0.20190704022140-85024ebc66dc // indirect // comment
+        goa.design/plugins/v3 v3.0.1
+)
+`,
+			Package:  "goa.design/goa/v3",
+			Expected: "goa.design/goa/v3@v3.0.3-0.20190704022140-85024ebc66dc",
+		},
+		{
+			Name:     "require with comment",
+			Mod:      " require goa.design/goa/v3 v3.0.3-0.20190704022140-85024ebc66dc// indirect // comment",
+			Package:  "goa.design/goa/v3",
+			Expected: "goa.design/goa/v3@v3.0.3-0.20190704022140-85024ebc66dc",
+		},
+		{
+			Name: "comment out",
+			Mod: `module calc
+go 1.12
+replace goa.design/goa/v3 => ../../../goa.design/goa
+require (
+        github.com/ikawaha/kagome v1.0.0 // indirect
+        // goa.design/goa/v3 v3.0.3-0.20190704022140-85024ebc66dc
+        goa.design/plugins/v3 v3.0.1
+)
+`,
+			Package:  "goa.design/goa/v3",
+			Expected: "goa.design/goa/v3",
+		},
+		{
+			Name:     "without version",
+			Mod:      "     goa.design/goa/v3//v3.0.3-0.20190704022140-85024ebc66dc",
+			Package:  "goa.design/goa/v3",
+			Expected: "goa.design/goa/v3",
+		},
+		{
+			Name:     "different version",
+			Mod:      "goa.design/goa/v2 v2.0.3-0.20190704022140-85024ebc66dc // comment",
+			Package:  "goa.design/goa/v3",
+			Expected: "goa.design/goa/v3",
+		},
+	}
+	for _, c := range cases {
+		t.Run(c.Name, func(t *testing.T) {
+			pkg, err := parseGoModGoaPackage("goa.design/goa/v3", strings.NewReader(c.Mod))
+			if err != nil {
+				t.Fatalf("unexpected error, %v", err)
+			}
+			if pkg != c.Expected {
+				t.Errorf("expected %v, got %v", c.Expected, pkg)
+			}
+		})
+	}
+}
+
+func TestModPatternRegexp(t *testing.T) {
+	cases := []struct {
+		Name     string
+		Input    string
+		Expected []string
+	}{
+		{
+			Name:     "goa v3 release version",
+			Input:    "goa.design/goa/v3 v3.0.2",
+			Expected: []string{"goa.design/goa/v3 v3.0.2", "goa.design/goa/v3", "v3.0.2"},
+		},
+		{
+			Name:     "goa v3 commit number",
+			Input:    "goa.design/goa/v3 02c84c9b39d845611bfe6e8a96465d698ea374b1",
+			Expected: []string{"goa.design/goa/v3 02c84c9b39d845611bfe6e8a96465d698ea374b1", "goa.design/goa/v3", "02c84c9b39d845611bfe6e8a96465d698ea374b1"},
+		},
+		{
+			Name:     "goa v3 with comment",
+			Input:    "goa.design/goa/v3   v3.0.2 //comment1 //comment2 //comment3",
+			Expected: []string{"goa.design/goa/v3   v3.0.2 //comment1 //comment2 //comment3", "goa.design/goa/v3", "v3.0.2"},
+		},
+		{
+			Name:     "goa v3 with require and comment",
+			Input:    "require goa.design/goa/v3 v3.0.2 //comment",
+			Expected: []string{"require goa.design/goa/v3 v3.0.2 //comment", "goa.design/goa/v3", "v3.0.2"},
+		},
+		{
+			Name:     "goa v2",
+			Input:    "goa.design/goa/v2 v2.0.0",
+			Expected: []string{"goa.design/goa/v2 v2.0.0", "goa.design/goa/v2", "v2.0.0"},
+		},
+		{
+			Name:     "goa v123",
+			Input:    "   goa.design/goa/v123    v123.4.5   ",
+			Expected: []string{"   goa.design/goa/v123    v123.4.5   ", "goa.design/goa/v123", "v123.4.5"},
+		},
+		{
+			Name:     "without version",
+			Input:    "goa.design/goa/v3",
+			Expected: nil,
+		},
+		{
+			Name:     "comment out",
+			Input:    "// goa.design/goa/v3 v3.0.0",
+			Expected: nil,
+		},
+		{
+			Name:     "irrelevant package",
+			Input:    "github.com/ikawaha/kagome v1.10.0",
+			Expected: nil,
+		},
+	}
+	for _, c := range cases {
+		if got := reMod.FindStringSubmatch(c.Input); !reflect.DeepEqual(c.Expected, got) {
+			t.Errorf("expected %+v, got %+v", c.Expected, got)
+		}
+	}
+}
diff --git a/grpc/middleware/xray/middleware_test.go b/grpc/middleware/xray/middleware_test.go
index dbbb0446..8b61b73f 100644
--- a/grpc/middleware/xray/middleware_test.go
+++ b/grpc/middleware/xray/middleware_test.go
@@ -10,6 +10,7 @@ import (
 	grpcm "goa.design/goa/v3/grpc/middleware"
 	"goa.design/goa/v3/middleware"
 	"goa.design/goa/v3/middleware/xray"
+	"goa.design/goa/v3/middleware/xray/xraytest"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/metadata"
@@ -174,7 +175,7 @@ func TestUnaryServerMiddleware(t *testing.T) {
 				ctx = peer.NewContext(ctx, &peer.Peer{Addr: &mockAddr{c.Request.RemoteAddr}})
 			}
 
-			messages := xray.ReadUDP(t, udplisten, expMsgs, func() {
+			messages := xraytest.ReadUDP(t, udplisten, expMsgs, func() {
 				m(ctx, &wrappers.StringValue{Value: "request"}, unary, handler)
 			})
 			if expMsgs == 0 {
@@ -182,13 +183,13 @@ func TestUnaryServerMiddleware(t *testing.T) {
 			}
 
 			// expect the first message is InProgress
-			s := xray.ExtractSegment(t, messages[0])
+			s := xraytest.ExtractSegment(t, messages[0])
 			if !s.InProgress {
 				t.Fatal("expected first segment to be InProgress but it was not")
 			}
 
 			// second message
-			s = xray.ExtractSegment(t, messages[1])
+			s = xraytest.ExtractSegment(t, messages[1])
 			if s.Name != "service" {
 				t.Errorf("unexpected segment name, expected \"service\" - got %q", s.Name)
 			}
@@ -320,7 +321,7 @@ func TestStreamServerMiddleware(t *testing.T) {
 			}
 			wss := grpcm.NewWrappedServerStream(ctx, &testServerStream{})
 
-			messages := xray.ReadUDP(t, udplisten, expMsgs, func() {
+			messages := xraytest.ReadUDP(t, udplisten, expMsgs, func() {
 				m(nil, wss, streamInfo, handler)
 			})
 			if expMsgs == 0 {
@@ -328,13 +329,13 @@ func TestStreamServerMiddleware(t *testing.T) {
 			}
 
 			// expect the first message is InProgress
-			s := xray.ExtractSegment(t, messages[0])
+			s := xraytest.ExtractSegment(t, messages[0])
 			if !s.InProgress {
 				t.Fatal("expected first segment to be InProgress but it was not")
 			}
 
 			// second message
-			s = xray.ExtractSegment(t, messages[1])
+			s = xraytest.ExtractSegment(t, messages[1])
 			if s.Name != "service" {
 				t.Errorf("unexpected segment name, expected \"service\" - got %q", s.Name)
 			}
@@ -437,7 +438,7 @@ func TestUnaryClient(t *testing.T) {
 				ctx = context.WithValue(ctx, xray.SegKey, segment)
 			}
 
-			messages := xray.ReadUDP(t, udplisten, expMsgs, func() {
+			messages := xraytest.ReadUDP(t, udplisten, expMsgs, func() {
 				UnaryClient(host)(ctx, "Test.Test", req, resp, nil, invoker)
 			})
 			if expMsgs == 0 {
@@ -445,13 +446,13 @@ func TestUnaryClient(t *testing.T) {
 			}
 
 			// expect the first message is InProgress
-			s := xray.ExtractSegment(t, messages[0])
+			s := xraytest.ExtractSegment(t, messages[0])
 			if !s.InProgress {
 				t.Fatal("expected first segment to be InProgress but it was not")
 			}
 
 			// second message
-			s = xray.ExtractSegment(t, messages[1])
+			s = xraytest.ExtractSegment(t, messages[1])
 			if s.Name != host {
 				t.Errorf("unexpected segment name: expected %q, got %q", host, s.Name)
 			}
@@ -548,7 +549,7 @@ func TestStreamClient(t *testing.T) {
 				ctx = context.WithValue(ctx, xray.SegKey, segment)
 			}
 
-			messages := xray.ReadUDP(t, udplisten, expMsgs, func() {
+			messages := xraytest.ReadUDP(t, udplisten, expMsgs, func() {
 				cs, err := StreamClient(host)(ctx, nil, nil, "Test.Test", streamer)
 				errored := err != nil
 				if tc.RequestError != errored {
@@ -568,13 +569,13 @@ func TestStreamClient(t *testing.T) {
 			}
 
 			// expect the first message is InProgress
-			s := xray.ExtractSegment(t, messages[0])
+			s := xraytest.ExtractSegment(t, messages[0])
 			if !s.InProgress {
 				t.Fatal("expected first segment to be InProgress but it was not")
 			}
 
 			// second message
-			s = xray.ExtractSegment(t, messages[1])
+			s = xraytest.ExtractSegment(t, messages[1])
 			if s.Name != host {
 				t.Errorf("unexpected segment name: expected %q, got %q", host, s.Name)
 			}
diff --git a/http/middleware/xray/middleware_test.go b/http/middleware/xray/middleware_test.go
index ed8c4a54..13dbb7a4 100644
--- a/http/middleware/xray/middleware_test.go
+++ b/http/middleware/xray/middleware_test.go
@@ -10,6 +10,7 @@ import (
 
 	"goa.design/goa/v3/middleware"
 	"goa.design/goa/v3/middleware/xray"
+	"goa.design/goa/v3/middleware/xray/xraytest"
 )
 
 const (
@@ -157,18 +158,18 @@ func TestMiddleware(t *testing.T) {
 				req.Host = c.Request.Host
 			}
 			req = req.WithContext(ctx)
-			messages := xray.ReadUDP(t, udplisten, 2, func() {
+			messages := xraytest.ReadUDP(t, udplisten, 2, func() {
 				m(h).ServeHTTP(rw, req)
 			})
 
 			// expect the first message is InProgress
-			s := xray.ExtractSegment(t, messages[0])
+			s := xraytest.ExtractSegment(t, messages[0])
 			if !s.InProgress {
 				t.Fatal("expected first segment to be InProgress but it was not")
 			}
 
 			// second message
-			s = xray.ExtractSegment(t, messages[1])
+			s = xraytest.ExtractSegment(t, messages[1])
 			if s.Name != "service" {
 				t.Errorf("unexpected segment name, expected service - got %s", s.Name)
 			}
diff --git a/http/middleware/xray/wrap_doer_test.go b/http/middleware/xray/wrap_doer_test.go
index fb1192e5..01478c38 100644
--- a/http/middleware/xray/wrap_doer_test.go
+++ b/http/middleware/xray/wrap_doer_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"goa.design/goa/v3/middleware/xray"
+	"goa.design/goa/v3/middleware/xray/xraytest"
 )
 
 // testDoer simply tests if the request context is set with X-Ray segment
@@ -60,7 +61,7 @@ func TestWrapDoer(t *testing.T) {
 			}
 
 			doer := newTestDoer(t, tc.Segment, tc.StatusCode)
-			messages := xray.ReadUDP(t, udplisten, expMsgs, func() {
+			messages := xraytest.ReadUDP(t, udplisten, expMsgs, func() {
 				WrapDoer(doer).Do(req)
 			})
 			if expMsgs == 0 {
@@ -68,13 +69,13 @@ func TestWrapDoer(t *testing.T) {
 			}
 
 			// expect the first message is InProgress
-			s := xray.ExtractSegment(t, messages[0])
+			s := xraytest.ExtractSegment(t, messages[0])
 			if !s.InProgress {
 				t.Fatal("expected first segment to be InProgress but it was not")
 			}
 
 			// second message
-			s = xray.ExtractSegment(t, messages[1])
+			s = xraytest.ExtractSegment(t, messages[1])
 			if s.Name != host {
 				t.Fatalf("unexpected segment name: expected %q, got %q", host, s.Name)
 			}
diff --git a/http/middleware/xray/wrap_transport_test.go b/http/middleware/xray/wrap_transport_test.go
index 480cfd7e..b5dfa12f 100644
--- a/http/middleware/xray/wrap_transport_test.go
+++ b/http/middleware/xray/wrap_transport_test.go
@@ -12,6 +12,7 @@ import (
 	"testing"
 
 	"goa.design/goa/v3/middleware/xray"
+	"goa.design/goa/v3/middleware/xray/xraytest"
 )
 
 type mockRoundTripper struct {
@@ -56,7 +57,7 @@ func TestTransportExample(t *testing.T) {
 	// Setting context on request
 	req = req.WithContext(ctx)
 
-	messages := xray.ReadUDP(t, udplisten, 2, func() {
+	messages := xraytest.ReadUDP(t, udplisten, 2, func() {
 		resp, err := httpClient.Do(req)
 		if err != nil {
 			t.Fatalf("failed to make request - %s", err)
@@ -67,13 +68,13 @@ func TestTransportExample(t *testing.T) {
 	})
 
 	// expect the first message is InProgress
-	s := xray.ExtractSegment(t, messages[0])
+	s := xraytest.ExtractSegment(t, messages[0])
 	if !s.InProgress {
 		t.Fatal("expected first segment to be InProgress but it was not")
 	}
 
 	// second message
-	s = xray.ExtractSegment(t, messages[1])
+	s = xraytest.ExtractSegment(t, messages[1])
 	url, _ := url.Parse(server.URL)
 	if s.Name != url.Host {
 		t.Errorf("unexpected segment name, expected %q - got %q", url.Host, s.Name)
@@ -215,7 +216,7 @@ func TestTransport(t *testing.T) {
 				req.Host = c.Request.Host
 			}
 
-			messages := xray.ReadUDP(t, udplisten, 2, func() {
+			messages := xraytest.ReadUDP(t, udplisten, 2, func() {
 				resp, err := WrapTransport(rt).RoundTrip(req)
 				if c.Segment.Exception == "" && err != nil {
 					t.Errorf("expected no error got %s", err)
@@ -226,13 +227,13 @@ func TestTransport(t *testing.T) {
 			})
 
 			// expect the first message is InProgress
-			s := xray.ExtractSegment(t, messages[0])
+			s := xraytest.ExtractSegment(t, messages[0])
 			if !s.InProgress {
 				t.Fatal("expected first segment to be InProgress but it was not")
 			}
 
 			// second message
-			s = xray.ExtractSegment(t, messages[1])
+			s = xraytest.ExtractSegment(t, messages[1])
 			if s.Name != host {
 				t.Errorf("unexpected segment name, expected %q - got %q", host, s.Name)
 			}
diff --git a/middleware/xray/segment_test.go b/middleware/xray/segment_test.go
index 03f9eeda..910b410a 100644
--- a/middleware/xray/segment_test.go
+++ b/middleware/xray/segment_test.go
@@ -1,10 +1,12 @@
-package xray
+package xray_test
 
 import (
 	"net"
 	"regexp"
-	"sync"
 	"testing"
+
+	"goa.design/goa/v3/middleware/xray"
+	"goa.design/goa/v3/middleware/xray/xraytest"
 )
 
 const (
@@ -19,8 +21,8 @@ func TestSegment_NewSubsegment(t *testing.T) {
 	}
 	var (
 		name   = "sub"
-		s      = &Segment{Mutex: &sync.Mutex{}, conn: conn}
-		before = now()
+		s      = xray.NewSegment("", "", "", conn)
+		before = s.StartTime
 		ss     = s.NewSubsegment(name)
 	)
 	if ss.ID == "" {
@@ -50,10 +52,10 @@ func TestSegment_SubmitInProgress(t *testing.T) {
 			t.Fatalf("failed to connect to daemon - %s", err)
 		}
 
-		segment := NewSegment("hello", NewTraceID(), NewID(), conn)
+		segment := xray.NewSegment("hello", xray.NewTraceID(), xray.NewID(), conn)
 
 		// call SubmitInProgress() twice, then Close it
-		messages := ReadUDP(t, udplisten, 2, func() {
+		messages := xraytest.ReadUDP(t, udplisten, 2, func() {
 			segment.Namespace = "1"
 			segment.SubmitInProgress()
 			segment.Namespace = "2"
@@ -63,7 +65,7 @@ func TestSegment_SubmitInProgress(t *testing.T) {
 		})
 
 		// verify the In-Progress segment
-		s := ExtractSegment(t, messages[0])
+		s := xraytest.ExtractSegment(t, messages[0])
 		if !s.InProgress {
 			t.Errorf("expected segment to be InProgress, but it's not")
 		}
@@ -72,7 +74,7 @@ func TestSegment_SubmitInProgress(t *testing.T) {
 		}
 
 		// verify the final segment (the second In-Progress segment would not have been sent)
-		s = ExtractSegment(t, messages[1])
+		s = xraytest.ExtractSegment(t, messages[1])
 		if s.InProgress {
 			t.Errorf("expected segment to not be InProgress, but it is")
 		}
@@ -87,10 +89,10 @@ func TestSegment_SubmitInProgress(t *testing.T) {
 			t.Fatalf("failed to connect to daemon - %s", err)
 		}
 
-		segment := NewSegment("hello", NewTraceID(), NewID(), conn)
+		segment := xray.NewSegment("hello", xray.NewTraceID(), xray.NewID(), conn)
 
 		// Close(), then call SubmitInProgress(), only expect 1 segment written
-		messages := ReadUDP(t, udplisten, 1, func() {
+		messages := xraytest.ReadUDP(t, udplisten, 1, func() {
 			segment.Namespace = "1"
 			segment.Close()
 			segment.Namespace = "2"
@@ -98,7 +100,7 @@ func TestSegment_SubmitInProgress(t *testing.T) {
 		})
 
 		// verify the In-Progress segment
-		s := ExtractSegment(t, messages[0])
+		s := xraytest.ExtractSegment(t, messages[0])
 		if s.InProgress {
 			t.Errorf("expected segment to be closed, but it is still InProgress")
 		}
diff --git a/middleware/xray/testing.go b/middleware/xray/xraytest/testing.go
similarity index 83%
rename from middleware/xray/testing.go
rename to middleware/xray/xraytest/testing.go
index 2ef23ac6..46f28755 100644
--- a/middleware/xray/testing.go
+++ b/middleware/xray/xraytest/testing.go
@@ -1,4 +1,6 @@
-package xray
+// Package xraytest contains test helpers for package xray that are used by
+// transport-specific X-Ray middleware tests.
+package xraytest
 
 import (
 	"encoding/json"
@@ -6,6 +8,8 @@ import (
 	"strings"
 	"testing"
 	"time"
+
+	"goa.design/goa/v3/middleware/xray"
 )
 
 // ReadUDP verifies that exactly the expected number of messages are received.
@@ -56,15 +60,15 @@ func ReadUDP(t *testing.T, udplisten string, expectedMessages int, sender func()
 }
 
 // ExtractSegment returns the unmarshalled segment JSON from a ReadUDP response.
-func ExtractSegment(t *testing.T, js string) *Segment {
+func ExtractSegment(t *testing.T, js string) *xray.Segment {
 	t.Helper()
 
-	var s *Segment
+	var s *xray.Segment
 	elems := strings.Split(js, "\n")
 	if len(elems) != 2 {
 		t.Fatalf("invalid number of lines, expected 2 got %d: %v", len(elems), elems)
 	}
-	if elems[0] != UDPHeader[:len(UDPHeader)-1] {
+	if elems[0] != xray.UDPHeader[:len(xray.UDPHeader)-1] {
 		t.Errorf("invalid header, got %s", elems[0])
 	}
 	err := json.Unmarshal([]byte(elems[1]), &s)