Skip to content

Commit b55a1ae

Browse files
authored
v23/verror: fix how default verror package paths are calculated (#164)
It was a mistake to use go.mod files at runtime since they are likely not present when apps are actually run!
1 parent 8d15935 commit b55a1ae

File tree

5 files changed

+162
-53
lines changed

5 files changed

+162
-53
lines changed

v23/verror/pkgpath.go

Lines changed: 81 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,19 @@
55
package verror
66

77
import (
8-
"fmt"
9-
"io/ioutil"
10-
"os"
118
"path"
129
"path/filepath"
10+
"reflect"
1311
"runtime"
1412
"strings"
1513
"sync"
16-
17-
"golang.org/x/mod/modfile"
1814
)
1915

2016
type pathCache struct {
2117
sync.Mutex
2218
paths map[string]string
2319
}
2420

25-
func enclosingGoMod(dir string) (string, error) {
26-
for {
27-
gomodfile := filepath.Join(dir, "go.mod")
28-
if fi, err := os.Stat(gomodfile); err == nil && !fi.IsDir() {
29-
return dir, nil
30-
}
31-
d := filepath.Dir(dir)
32-
if d == dir {
33-
return "", fmt.Errorf("failed to find enclosing go.mod for dir %v", dir)
34-
}
35-
dir = d
36-
}
37-
}
38-
3921
var pkgPathCache = pathCache{
4022
paths: make(map[string]string),
4123
}
@@ -53,40 +35,94 @@ func (pc *pathCache) set(dir, pkg string) {
5335
pc.paths[dir] = pkg
5436
}
5537

56-
func (pc *pathCache) pkgPath(file string) (string, error) {
57-
dir := filepath.Clean(filepath.Dir(file))
58-
if p, ok := pc.has(dir); ok {
59-
return p, nil
60-
}
61-
root, err := enclosingGoMod(dir)
62-
if err != nil {
63-
return "", err
64-
}
65-
gomodfile := filepath.Join(root, "go.mod")
66-
gomod, err := ioutil.ReadFile(gomodfile)
67-
if err != nil {
68-
return "", err
69-
}
70-
module := modfile.ModulePath(gomod)
71-
if len(module) == 0 {
72-
return "", fmt.Errorf("failed to read module path from %v", gomodfile)
38+
// IDPath returns a string of the form <package-path>.<name>
39+
// where <package-path> is derived from the type of the supplied
40+
// value. Typical usage would be except that dummy can be replaced
41+
// by an existing type defined in the package.
42+
//
43+
// type dummy int
44+
// verror.ID(verror.IDPath(dummy(0), "MyError"))
45+
//
46+
func IDPath(val interface{}, id string) ID {
47+
return ID(reflect.TypeOf(val).PkgPath() + "." + id)
48+
}
49+
50+
// longestCommonSuffix for a package path and filename.
51+
func longestCommonSuffix(pkgPath, filename string) (string, string) {
52+
longestPkg, longestFilePath := "", ""
53+
for {
54+
fl := filepath.Base(filename)
55+
pl := path.Base(pkgPath)
56+
if fl == pl {
57+
longestPkg = path.Join(fl, longestPkg)
58+
longestFilePath = filepath.Join(fl, longestFilePath)
59+
filename = filepath.Dir(filename)
60+
pkgPath = path.Dir(pkgPath)
61+
if fl == "/" {
62+
break
63+
}
64+
continue
65+
}
66+
break
7367
}
68+
return longestPkg, longestFilePath
69+
}
70+
71+
type pathState struct {
72+
pkg string // pkg path for the value passed to init
73+
dir string // the directory component for the file passed to init
74+
// The portion of the local file path that is outside of the go module,
75+
// e.g. for /a/b/c/core/v23/verror it would be /a/b/c/core.
76+
filePrefix string
77+
// the portion of the package path that does not appear in the file name,
78+
// e.g. for /a/b/c/core/v23/verror and v.io/v23/verror it would be v.io.
79+
pkgPrefix string
80+
}
81+
82+
func (ps *pathState) init(pkgPath string, file string) {
83+
ps.pkg = pkgPath
84+
ps.dir = filepath.Dir(file)
85+
pkgLCS, fileLCS := longestCommonSuffix(ps.pkg, ps.dir)
86+
ps.filePrefix = filepath.Clean(strings.TrimSuffix(ps.dir, fileLCS))
87+
ps.pkgPrefix = path.Clean(strings.TrimSuffix(ps.pkg, pkgLCS))
88+
}
89+
90+
var (
91+
ps = &pathState{}
92+
initOnce sync.Once
93+
)
7494

75-
pkgPath := strings.TrimPrefix(dir, root)
76-
if !strings.HasPrefix(pkgPath, module) {
77-
pkgPath = path.Join(module, pkgPath)
95+
func convertFileToPkgName(filename string) string {
96+
return path.Clean(strings.ReplaceAll(filename, string(filepath.Separator), "/"))
97+
}
98+
99+
func (pc *pathCache) pkgPath(file string) string {
100+
initOnce.Do(func() {
101+
type dummy int
102+
_, file, _, _ := runtime.Caller(0)
103+
ps.init(reflect.TypeOf(dummy(0)).PkgPath(), file)
104+
})
105+
pdir := filepath.Dir(file)
106+
rel := strings.TrimPrefix(pdir, ps.filePrefix)
107+
if rel == pdir {
108+
return ""
78109
}
79-
pc.set(dir, pkgPath)
80-
return pkgPath, nil
110+
relPkg := convertFileToPkgName(rel)
111+
pkgPath := path.Join(ps.pkgPrefix, relPkg)
112+
pc.set(filepath.Dir(file), pkgPath)
113+
return pkgPath
81114
}
82115

83116
func ensurePackagePath(id ID) ID {
117+
sid := string(id)
118+
if strings.Contains(sid, ".") && sid[0] != '.' {
119+
return id
120+
}
84121
_, file, _, _ := runtime.Caller(2)
85-
pkg, err := pkgPathCache.pkgPath(file)
86-
if err != nil {
87-
panic(fmt.Sprintf("failed to determine package name for %v: %v", file, err))
122+
pkg := pkgPathCache.pkgPath(file)
123+
if len(pkg) == 0 {
124+
return id
88125
}
89-
sid := string(id)
90126
if strings.HasPrefix(sid, pkg) {
91127
return id
92128
}

v23/verror/pkgpath_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package verror
2+
3+
import (
4+
"path/filepath"
5+
"reflect"
6+
"runtime"
7+
"strings"
8+
"testing"
9+
)
10+
11+
func TestLCS(t *testing.T) {
12+
for i, tc := range []struct {
13+
pkg, path string
14+
lcsPkg, lcsPath string
15+
}{
16+
{"a", "b", "", ""},
17+
{"/a/b/c", "/a/b/c", "/a/b/c", "/a/b/c"},
18+
{"/a/b/c/d", "/x/y/c/d", "c/d", "c/d"},
19+
} {
20+
lcsPkg, lcsPath := longestCommonSuffix(tc.pkg, tc.path)
21+
if got, want := lcsPkg, tc.lcsPkg; got != want {
22+
t.Errorf("%v: got %v, want %v", i, got, want)
23+
}
24+
if got, want := lcsPath, tc.lcsPath; got != want {
25+
t.Errorf("%v: got %v, want %v", i, got, want)
26+
}
27+
}
28+
}
29+
30+
func TestPkgPath(t *testing.T) {
31+
type dummy int
32+
ps := &pathState{}
33+
_, file, _, _ := runtime.Caller(0)
34+
ps.init(reflect.TypeOf(dummy(0)).PkgPath(), file)
35+
if got, want := ps.pkg, "v.io/v23/verror"; got != want {
36+
t.Errorf("got %v, want %v", got, want)
37+
}
38+
if got, want := ps.dir, filepath.Dir(file); got != want {
39+
t.Errorf("got %v, want %v", got, want)
40+
}
41+
if got, want := strings.HasPrefix(file, ps.filePrefix), true; got != want {
42+
t.Errorf("got %v, want %v", got, want)
43+
}
44+
if got, want := ps.pkgPrefix, "v.io"; got != want {
45+
t.Errorf("got %v, want %v", got, want)
46+
}
47+
ps.init("github.com/grailbio/base", "/a/b/src/base/file")
48+
if got, want := ps.pkgPrefix, "github.com/grailbio"; got != want {
49+
t.Errorf("got %v, want %v", got, want)
50+
}
51+
}

v23/verror/verror.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,8 @@ type IDAction struct {
178178

179179
// Register returns a IDAction with the given ID and Action fields, and
180180
// inserts a message into the default i18n Catalogue in US English.
181-
// Other languages can be added by adding to the Catalogue. Register
182-
// will internally prepend the caller's package path to id if it's not
183-
// already present.
181+
// Other languages can be added by adding to the Catalogue.
182+
// IDPath can be used to generate an appropriate ID.
184183
func Register(id ID, action ActionCode, englishText string) IDAction {
185184
id = ensurePackagePath(id)
186185
i18n.Cat().SetWithBase(defaultLangID(i18n.NoLangID), i18n.MsgID(id), englishText)
@@ -189,8 +188,7 @@ func Register(id ID, action ActionCode, englishText string) IDAction {
189188

190189
// NewIDAction creates a new instance of IDAction with the given ID and Action
191190
// field. It should be used when localization support is not required instead
192-
// of Register. NewIDAction will internally prepend the caller's package path
193-
// to id if it's not already present.
191+
// of Register. IDPath can be used to
194192
func NewIDAction(id ID, action ActionCode) IDAction {
195193
return IDAction{ensurePackagePath(id), action}
196194
}

v23/verror/verror_id_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package verror_test
2+
3+
import (
4+
"testing"
5+
6+
"v.io/v23/flow"
7+
"v.io/v23/verror"
8+
"v.io/x/ref/lib/security"
9+
)
10+
11+
func TestPackageErrorIDs(t *testing.T) {
12+
for i, tc := range []struct {
13+
err error
14+
id string
15+
}{
16+
{verror.ErrInternal.Errorf(nil, "x"), "v.io/v23/verror.Internal"},
17+
{flow.ErrAuth.Errorf(nil, "x"), "v.io/v23/flow.Auth"},
18+
{security.ErrBadPassphrase.Errorf(nil, "x"), "v.io/x/ref/lib/security.errBadPassphrase"},
19+
} {
20+
if got, want := verror.ErrorID(tc.err), verror.ID(tc.id); got != want {
21+
t.Errorf("%v: got %v, want %v", i, got, want)
22+
}
23+
}
24+
}

x/ref/runtime/internal/flow/util_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestMaybeWrapError(t *testing.T) {
2424
{verror.ErrUnknown.Errorf(ctx, "some random thing"), false},
2525
{flow.ErrAuth.Errorf(ctx, ""), false},
2626
}
27-
for _, test := range tests {
27+
for i, test := range tests {
2828
werr := MaybeWrapError(flow.ErrAuth, ctx, test.err)
2929
// If the returned error is not equal to the original error it was wrapped.
3030
msg := ""
@@ -33,9 +33,9 @@ func TestMaybeWrapError(t *testing.T) {
3333
}
3434
if wasWrapped := werr.Error() != msg; wasWrapped != test.wrap {
3535
if test.wrap {
36-
t.Errorf("wanted %v to be wrapped", test.err)
36+
t.Errorf("%v: wanted %v to be wrapped", i, test.err)
3737
} else {
38-
t.Errorf("did not want %v to be wrapped", test.err)
38+
t.Errorf("%v: did not want %v to be wrapped", i, test.err)
3939
}
4040
}
4141
}

0 commit comments

Comments
 (0)