diff --git a/goutil/goutil.go b/goutil/goutil.go index 020380d..e97d4ce 100644 --- a/goutil/goutil.go +++ b/goutil/goutil.go @@ -63,10 +63,10 @@ func ResolvePackage(path string, mode build.ImportMode) (pkg *build.Package, err return nil, errors.New("cannot resolve empty string") } - switch path[0] { - case '/': + switch { + case isAbsPath(path): pkg, err = build.ImportDir(path, mode) - case '.': + case path[0] == '.': path, err = filepath.Abs(path) if err != nil { return nil, err @@ -88,6 +88,29 @@ func ResolvePackage(path string, mode build.ImportMode) (pkg *build.Package, err return pkg, err } +// isAbsPath reports whether p is an absolute path under either Unix or Windows +// conventions. filepath.IsAbs only knows the host's convention, so on Unix it +// rejects `C:\dir` and on Windows it rejects `/dir`. +func isAbsPath(p string) bool { + if filepath.IsAbs(p) || strings.HasPrefix(p, "/") { + return true + } + if strings.HasPrefix(p, `\\`) { // UNC path + return true + } + // Drive letter, e.g. C:\dir or C:/dir + return len(p) >= 3 && p[1] == ':' && (p[2] == '\\' || p[2] == '/') && + (('A' <= p[0] && p[0] <= 'Z') || ('a' <= p[0] && p[0] <= 'z')) +} + +// dirName returns the name of the directory containing file, accepting both +// separators so a Windows path resolves on any host: path.Dir is slash-only and +// returns "." for `C:\src\pkg\file.go`. A literal backslash in a Unix file +// name is treated as a separator, which Go source paths never rely on. +func dirName(file string) string { + return path.Base(path.Dir(strings.ReplaceAll(file, `\`, "/"))) +} + // ResolveWildcard finds all subpackages in the "example/..." format. The // "/vendor/" directory will be ignored. func ResolveWildcard(path string, mode build.ImportMode) ([]*build.Package, error) { @@ -198,7 +221,7 @@ func ResolveImport(file, pkgName string) (string, error) { r, ok := imports[pkgName] if !ok { - currentPkg := path.Base(path.Dir(file)) + currentPkg := dirName(file) if pkgName == currentPkg { r = "." } diff --git a/goutil/goutil_test.go b/goutil/goutil_test.go index 55a6a13..d150abd 100644 --- a/goutil/goutil_test.go +++ b/goutil/goutil_test.go @@ -319,3 +319,51 @@ func TestTagName(t *testing.T) { }) } + +func TestIsAbsPath(t *testing.T) { + cases := []struct { + in string + want bool + }{ + {"/home/dev/proj", true}, + {"/", true}, + {`C:\Users\dev\proj`, true}, + {"C:/Users/dev/proj", true}, + {`c:\users\dev`, true}, + {`\\server\share\proj`, true}, + {"github.com/teamwork/utils", false}, + {"./proj", false}, + {"proj", false}, + {"", false}, + {"C:", false}, + {"1:/nope", false}, + } + + for _, tc := range cases { + t.Run(tc.in, func(t *testing.T) { + if out := isAbsPath(tc.in); out != tc.want { + t.Errorf("\nout: %v\nwant: %v\n", out, tc.want) + } + }) + } +} + +func TestDirName(t *testing.T) { + cases := []struct { + in, want string + }{ + {"/home/dev/go/src/example/pkg/file.go", "pkg"}, + {`C:\Users\dev\go\src\example\pkg\file.go`, "pkg"}, + {"C:/Users/dev/go/src/example/pkg/file.go", "pkg"}, + {"pkg/file.go", "pkg"}, + {"file.go", "."}, + } + + for _, tc := range cases { + t.Run(tc.in, func(t *testing.T) { + if out := dirName(tc.in); out != tc.want { + t.Errorf("\nout: %#v\nwant: %#v\n", out, tc.want) + } + }) + } +} diff --git a/httputilx/httputilx_test.go b/httputilx/httputilx_test.go index 3ae29b4..4d799f9 100644 --- a/httputilx/httputilx_test.go +++ b/httputilx/httputilx_test.go @@ -201,15 +201,36 @@ func chunk(s string) string { // TODO: better to not depend on interwebz... func TestFetch(t *testing.T) { + // Served locally rather than from httpbin.org: the external service returns + // a 503 often enough to fail the build for reasons unrelated to any change. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/html": + _, _ = w.Write([]byte("hello")) + case "/status/400": + w.WriteHeader(http.StatusBadRequest) + case "/status/500": + w.WriteHeader(http.StatusInternalServerError) + case "/status/418": + w.WriteHeader(http.StatusTeapot) + _, _ = w.Write([]byte("teapot")) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer srv.Close() + cases := []struct { in, want, wantErr string }{ - {"http://example.com", "