From afd2cb334cc091589c125c87c7a772a2bb6c1203 Mon Sep 17 00:00:00 2001 From: Shane O'Donovan Date: Thu, 13 Aug 2026 10:07:53 +0100 Subject: [PATCH 1/2] goutil: handle Windows paths in ResolvePackage and ResolveImport Two path helpers assumed Unix conventions, so both misbehave on a Windows checkout. ResolvePackage switched on path[0] and only treated a leading '/' as a directory. A Windows absolute path starts with a drive letter, so it fell through to build.Import and was resolved as though it were an import path, which always fails. ResolveImport's same-package fallback derived the current package name with path.Base(path.Dir(file)). The path package is slash-only, so for C:\src\pkg\file.go it evaluates to "." and the fallback never fires, leaving same-package type references unresolvable. Both are fixed with helpers that accept either convention, so they are unit-testable on any host rather than only on Windows. No behaviour change for Unix paths. Co-Authored-By: Claude Opus 5 (1M context) --- goutil/goutil.go | 31 ++++++++++++++++++++++++---- goutil/goutil_test.go | 48 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) 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) + } + }) + } +} From f6b1ece4c038905926ab05c3c84c787d8852280e Mon Sep 17 00:00:00 2001 From: Shane O'Donovan Date: Thu, 13 Aug 2026 10:27:05 +0100 Subject: [PATCH 2/2] httputilx: serve TestFetch's fixtures locally instead of httpbin.org MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestFetch drove its status-code cases off httpbin.org, which returns 503 often enough to fail unrelated builds — it failed on this branch while passing on a re-run of the same commit. An httptest server now serves the 400/500/418 responses, and the connection-failure case dials a closed local port rather than relying on a domain never resolving, which a wildcard DNS resolver can defeat. Co-Authored-By: Claude Opus 5 (1M context) --- httputilx/httputilx_test.go | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) 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", "