internal/frontend: move urlinfo to its own package
These are also being moved so that the code in fetch.go can be moved to
a new package without depending on internal/frontend.
A couple of functions that were only used by details.go are moved to
that file.
For #61399
Change-Id: Ic299069ea0b3aaeb80dbbf7f1ed4fedf7d1787df
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/518816
Reviewed-by: Robert Findley <rfindley@google.com>
kokoro-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Michael Matloob <matloob@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/frontend/404.go b/internal/frontend/404.go
index ba773d0..9824dd2 100644
--- a/internal/frontend/404.go
+++ b/internal/frontend/404.go
@@ -23,6 +23,7 @@
"golang.org/x/pkgsite/internal/experiment"
"golang.org/x/pkgsite/internal/frontend/page"
"golang.org/x/pkgsite/internal/frontend/serrors"
+ "golang.org/x/pkgsite/internal/frontend/urlinfo"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/stdlib"
"golang.org/x/pkgsite/internal/version"
@@ -187,7 +188,7 @@
// pathNotFoundError returns a page with an option on how to
// add a package or module to the site.
func pathNotFoundError(ctx context.Context, fullPath, requestedVersion string) error {
- if !isSupportedVersion(fullPath, requestedVersion) {
+ if !urlinfo.IsSupportedVersion(fullPath, requestedVersion) {
return invalidVersionError(fullPath, requestedVersion)
}
if stdlib.Contains(fullPath) {
diff --git a/internal/frontend/details.go b/internal/frontend/details.go
index cc4d776..1aac1ee 100644
--- a/internal/frontend/details.go
+++ b/internal/frontend/details.go
@@ -12,6 +12,7 @@
"golang.org/x/pkgsite/internal/frontend/page"
"golang.org/x/pkgsite/internal/frontend/serrors"
+ "golang.org/x/pkgsite/internal/frontend/urlinfo"
mstats "golang.org/x/pkgsite/internal/middleware/stats"
"github.com/google/safehtml/template"
@@ -51,11 +52,11 @@
ctx = setExperimentsFromQueryParam(ctx, r)
}
- urlInfo, err := extractURLPathInfo(r.URL.Path)
+ urlInfo, err := urlinfo.ExtractURLPathInfo(r.URL.Path)
if err != nil {
var epage *page.ErrorPage
- if uerr := new(userError); errors.As(err, &uerr) {
- epage = &page.ErrorPage{MessageData: uerr.userMessage}
+ if uerr := new(urlinfo.UserError); errors.As(err, &uerr) {
+ epage = &page.ErrorPage{MessageData: uerr.UserMessage}
}
return &serrors.ServerError{
Status: http.StatusBadRequest,
@@ -63,14 +64,14 @@
Epage: epage,
}
}
- if !isSupportedVersion(urlInfo.fullPath, urlInfo.requestedVersion) {
- return invalidVersionError(urlInfo.fullPath, urlInfo.requestedVersion)
+ if !urlinfo.IsSupportedVersion(urlInfo.FullPath, urlInfo.RequestedVersion) {
+ return invalidVersionError(urlInfo.FullPath, urlInfo.RequestedVersion)
}
- if urlPath := stdlibRedirectURL(urlInfo.fullPath); urlPath != "" {
+ if urlPath := stdlibRedirectURL(urlInfo.FullPath); urlPath != "" {
http.Redirect(w, r, urlPath, http.StatusMovedPermanently)
return
}
- if err := checkExcluded(ctx, ds, urlInfo.fullPath); err != nil {
+ if err := checkExcluded(ctx, ds, urlInfo.FullPath); err != nil {
return err
}
return s.serveUnitPage(ctx, w, r, ds, urlInfo)
@@ -140,3 +141,19 @@
tag.Upsert(keyVersionType, v),
}, versionTypeResults.M(1))
}
+
+func checkExcluded(ctx context.Context, ds internal.DataSource, fullPath string) error {
+ db, ok := ds.(internal.PostgresDB)
+ if !ok {
+ return nil
+ }
+ excluded, err := db.IsExcluded(ctx, fullPath)
+ if err != nil {
+ return err
+ }
+ if excluded {
+ // Return NotFound; don't let the user know that the package was excluded.
+ return &serrors.ServerError{Status: http.StatusNotFound}
+ }
+ return nil
+}
diff --git a/internal/frontend/experiments.go b/internal/frontend/experiments.go
new file mode 100644
index 0000000..0974792
--- /dev/null
+++ b/internal/frontend/experiments.go
@@ -0,0 +1,54 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package frontend
+
+import (
+ "context"
+ "net/http"
+ "strings"
+
+ "golang.org/x/pkgsite/internal/experiment"
+ "golang.org/x/pkgsite/internal/log"
+)
+
+func setExperimentsFromQueryParam(ctx context.Context, r *http.Request) context.Context {
+ if err := r.ParseForm(); err != nil {
+ log.Errorf(ctx, "ParseForm: %v", err)
+ return ctx
+ }
+ return newContextFromExps(ctx, r.Form["exp"])
+}
+
+// newContextFromExps adds and removes experiments from the context's experiment
+// set, creates a new set with the changes, and returns a context with the new
+// set. Each string in expMods can be either an experiment name, which means
+// that the experiment should be added, or "!" followed by an experiment name,
+// meaning that it should be removed.
+func newContextFromExps(ctx context.Context, expMods []string) context.Context {
+ var (
+ exps []string
+ remove = map[string]bool{}
+ )
+ set := experiment.FromContext(ctx)
+ for _, exp := range expMods {
+ if strings.HasPrefix(exp, "!") {
+ exp = exp[1:]
+ if set.IsActive(exp) {
+ remove[exp] = true
+ }
+ } else if !set.IsActive(exp) {
+ exps = append(exps, exp)
+ }
+ }
+ if len(exps) == 0 && len(remove) == 0 {
+ return ctx
+ }
+ for _, a := range set.Active() {
+ if !remove[a] {
+ exps = append(exps, a)
+ }
+ }
+ return experiment.NewContext(ctx, exps...)
+}
diff --git a/internal/frontend/experments_test.go b/internal/frontend/experments_test.go
new file mode 100644
index 0000000..cb28013
--- /dev/null
+++ b/internal/frontend/experments_test.go
@@ -0,0 +1,42 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package frontend
+
+import (
+ "context"
+ "sort"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/pkgsite/internal/experiment"
+)
+
+func TestNewContextFromExps(t *testing.T) {
+ for _, test := range []struct {
+ mods []string
+ want []string
+ }{
+ {
+ mods: []string{"c", "a", "b"},
+ want: []string{"a", "b", "c"},
+ },
+ {
+ mods: []string{"d", "a"},
+ want: []string{"a", "b", "c", "d"},
+ },
+ {
+ mods: []string{"d", "!b", "!a", "c"},
+ want: []string{"c", "d"},
+ },
+ } {
+ ctx := experiment.NewContext(context.Background(), "a", "b", "c")
+ ctx = newContextFromExps(ctx, test.mods)
+ got := experiment.FromContext(ctx).Active()
+ sort.Strings(got)
+ if !cmp.Equal(got, test.want) {
+ t.Errorf("mods=%v:\ngot %v\nwant %v", test.mods, got, test.want)
+ }
+ }
+}
diff --git a/internal/frontend/fetch.go b/internal/frontend/fetch.go
index 9f21b61..29fdff0 100644
--- a/internal/frontend/fetch.go
+++ b/internal/frontend/fetch.go
@@ -25,6 +25,7 @@
"golang.org/x/pkgsite/internal/experiment"
"golang.org/x/pkgsite/internal/fetch"
"golang.org/x/pkgsite/internal/frontend/serrors"
+ "golang.org/x/pkgsite/internal/frontend/urlinfo"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/proxy"
"golang.org/x/pkgsite/internal/queue"
@@ -39,7 +40,7 @@
// this module version in version_map.
errModuleDoesNotExist = errors.New("module does not exist")
// errPathDoesNotExistInModule indicates that a module for the path prefix
- // exists, but within that module version, this fullPath could not be found.
+ // exists, but within that module version, this FullPath could not be found.
errPathDoesNotExistInModule = errors.New("path does not exist in module")
fetchTimeout = 30 * time.Second
pollEvery = 1 * time.Second
@@ -103,11 +104,11 @@
return &serrors.ServerError{Status: http.StatusNotFound}
}
- urlInfo, err := extractURLPathInfo(strings.TrimPrefix(r.URL.Path, "/fetch"))
+ urlInfo, err := urlinfo.ExtractURLPathInfo(strings.TrimPrefix(r.URL.Path, "/fetch"))
if err != nil {
return &serrors.ServerError{Status: http.StatusBadRequest}
}
- status, responseText := s.fetchAndPoll(r.Context(), ds, urlInfo.modulePath, urlInfo.fullPath, urlInfo.requestedVersion)
+ status, responseText := s.fetchAndPoll(r.Context(), ds, urlInfo.ModulePath, urlInfo.FullPath, urlInfo.RequestedVersion)
if status != http.StatusOK {
return &serrors.ServerError{Status: status, ResponseText: responseText}
}
@@ -134,14 +135,14 @@
recordFrontendFetchMetric(ctx, status, time.Since(start))
}()
- if !isSupportedVersion(fullPath, requestedVersion) {
+ if !urlinfo.IsSupportedVersion(fullPath, requestedVersion) {
return http.StatusBadRequest, http.StatusText(http.StatusBadRequest)
}
if !experiment.IsActive(ctx, internal.ExperimentEnableStdFrontendFetch) && stdlib.Contains(fullPath) {
return http.StatusBadRequest, http.StatusText(http.StatusBadRequest)
}
- // Generate all possible module paths for the fullPath.
+ // Generate all possible module paths for the FullPath.
db := ds.(internal.PostgresDB)
modulePaths, err := modulePathsToFetch(ctx, db, fullPath, modulePath)
if err != nil {
@@ -165,7 +166,7 @@
}
// checkPossibleModulePaths checks all modulePaths at the requestedVersion, to see
-// if the fullPath exists. For each module path, it first checks version_map to
+// if the FullPath exists. For each module path, it first checks version_map to
// see if we already attempted to fetch the module. If not, and shouldQueue is
// true, it will enqueue the module to the frontend task queue to be fetched.
// checkPossibleModulePaths will then poll the database for each module path,
@@ -391,7 +392,7 @@
vm, err := db.GetVersionMap(ctx, modulePath, requestedVersion)
if err != nil {
// If an error is returned, there are two possibilities:
- // (1) A row for this modulePath and version does not exist.
+ // (1) A row for this ModulePath and version does not exist.
// This means that the fetch request is not done yet, so return
// statusNotFoundInVersionMap so the fetchHandler will call checkForPath
// again in a few seconds.
@@ -520,10 +521,10 @@
if fullPath == stdlib.ModulePath {
return []string{stdlib.ModulePath}, nil
}
- if !isValidPath(fullPath) {
+ if !urlinfo.IsValidPath(fullPath) {
return nil, &serrors.ServerError{
Status: http.StatusBadRequest,
- Err: fmt.Errorf("isValidPath(%q): false", fullPath),
+ Err: fmt.Errorf("urlinfo.IsValidPath(%q): false", fullPath),
}
}
paths := internal.CandidateModulePaths(fullPath)
diff --git a/internal/frontend/server.go b/internal/frontend/server.go
index 44f4d9b..5556cf2 100644
--- a/internal/frontend/server.go
+++ b/internal/frontend/server.go
@@ -28,6 +28,7 @@
"golang.org/x/pkgsite/internal/experiment"
pagepkg "golang.org/x/pkgsite/internal/frontend/page"
"golang.org/x/pkgsite/internal/frontend/serrors"
+ "golang.org/x/pkgsite/internal/frontend/urlinfo"
"golang.org/x/pkgsite/internal/godoc/dochtml"
"golang.org/x/pkgsite/internal/licenses"
"golang.org/x/pkgsite/internal/log"
@@ -329,12 +330,12 @@
if urlPath == "/" {
return defaultTTL
}
- info, err := parseDetailsURLPath(urlPath)
+ info, err := urlinfo.ParseDetailsURLPath(urlPath)
if err != nil {
log.Errorf(ctx, "falling back to default TTL: %v", err)
return defaultTTL
}
- if info.requestedVersion == version.Latest {
+ if info.RequestedVersion == version.Latest {
return shortTTL
}
if tab == "importedby" || tab == "versions" {
diff --git a/internal/frontend/unit.go b/internal/frontend/unit.go
index 6eaac93..c9b3882 100644
--- a/internal/frontend/unit.go
+++ b/internal/frontend/unit.go
@@ -19,6 +19,7 @@
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/frontend/page"
"golang.org/x/pkgsite/internal/frontend/serrors"
+ "golang.org/x/pkgsite/internal/frontend/urlinfo"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/middleware/stats"
"golang.org/x/pkgsite/internal/stdlib"
@@ -106,7 +107,7 @@
// serveUnitPage serves a unit page for a path.
func (s *Server) serveUnitPage(ctx context.Context, w http.ResponseWriter, r *http.Request,
- ds internal.DataSource, info *urlPathInfo) (err error) {
+ ds internal.DataSource, info *urlinfo.URLPathInfo) (err error) {
defer derrors.Wrap(&err, "serveUnitPage(ctx, w, r, ds, %v)", info)
defer stats.Elapsed(ctx, "serveUnitPage")()
@@ -121,12 +122,12 @@
return nil
}
- um, err := ds.GetUnitMeta(ctx, info.fullPath, info.modulePath, info.requestedVersion)
+ um, err := ds.GetUnitMeta(ctx, info.FullPath, info.ModulePath, info.RequestedVersion)
if err != nil {
if !errors.Is(err, derrors.NotFound) {
return err
}
- return s.servePathNotFoundPage(w, r, ds, info.fullPath, info.modulePath, info.requestedVersion)
+ return s.servePathNotFoundPage(w, r, ds, info.FullPath, info.ModulePath, info.RequestedVersion)
}
makeDepsDevURL := depsDevURLGenerator(ctx, um)
@@ -137,7 +138,7 @@
// It's also okay to provide just one (e.g. GOOS=windows), which will select
// the first doc with that value, ignoring the other one.
bc := internal.BuildContext{GOOS: r.FormValue("GOOS"), GOARCH: r.FormValue("GOARCH")}
- d, err := fetchDetailsForUnit(ctx, r, tab, ds, um, info.requestedVersion, bc, s.vulnClient)
+ d, err := fetchDetailsForUnit(ctx, r, tab, ds, um, info.RequestedVersion, bc, s.vulnClient)
if err != nil {
return err
}
@@ -145,8 +146,8 @@
return s.serveJSONPage(w, r, d)
}
- recordVersionTypeMetric(ctx, info.requestedVersion)
- if _, ok := internal.DefaultBranches[info.requestedVersion]; ok {
+ recordVersionTypeMetric(ctx, info.RequestedVersion)
+ if _, ok := internal.DefaultBranches[info.RequestedVersion]; ok {
// Since path@master is a moving target, we don't want it to be stale.
// As a result, we enqueue every request of path@master to the frontend
// task queue, which will initiate a fetch request depending on the
@@ -160,17 +161,17 @@
Err: err,
Epage: &page.ErrorPage{
MessageData: fmt.Sprintf(`Default branches like "@%s" are not supported. Omit to get the current version.`,
- info.requestedVersion),
+ info.RequestedVersion),
},
}
}
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
- log.Infof(ctx, "serveUnitPage: Scheduling %q@%q to be fetched", um.ModulePath, info.requestedVersion)
- if _, err := s.queue.ScheduleFetch(ctx, um.ModulePath, info.requestedVersion, nil); err != nil {
+ log.Infof(ctx, "serveUnitPage: Scheduling %q@%q to be fetched", um.ModulePath, info.RequestedVersion)
+ if _, err := s.queue.ScheduleFetch(ctx, um.ModulePath, info.RequestedVersion, nil); err != nil {
log.Errorf(ctx, "serveUnitPage(%q): scheduling fetch for %q@%q: %v",
- r.URL.Path, um.ModulePath, info.requestedVersion, err)
+ r.URL.Path, um.ModulePath, info.RequestedVersion, err)
}
}()
}
@@ -185,7 +186,7 @@
// If we've already called GetUnitMeta for an unknown module path and the latest version, pass
// it to GetLatestInfo to avoid a redundant call.
var latestUnitMeta *internal.UnitMeta
- if info.modulePath == internal.UnknownModulePath && info.requestedVersion == version.Latest {
+ if info.ModulePath == internal.UnknownModulePath && info.RequestedVersion == version.Latest {
latestUnitMeta = um
}
latestInfo := s.GetLatestInfo(ctx, um.Path, um.ModulePath, latestUnitMeta)
@@ -202,16 +203,16 @@
if tabSettings.Name == "" {
basePage.UseResponsiveLayout = true
}
- lv := linkVersion(um.ModulePath, info.requestedVersion, um.Version)
+ lv := linkVersion(um.ModulePath, info.RequestedVersion, um.Version)
page := UnitPage{
BasePage: basePage,
Unit: um,
- Breadcrumb: displayBreadcrumb(um, info.requestedVersion),
+ Breadcrumb: displayBreadcrumb(um, info.RequestedVersion),
Title: title,
SelectedTab: tabSettings,
- URLPath: constructUnitURL(um.Path, um.ModulePath, info.requestedVersion),
- CanonicalURLPath: canonicalURLPath(um.Path, um.ModulePath, info.requestedVersion, um.Version),
- DisplayVersion: displayVersion(um.ModulePath, info.requestedVersion, um.Version),
+ URLPath: constructUnitURL(um.Path, um.ModulePath, info.RequestedVersion),
+ CanonicalURLPath: canonicalURLPath(um.Path, um.ModulePath, info.RequestedVersion, um.Version),
+ DisplayVersion: displayVersion(um.ModulePath, info.RequestedVersion, um.Version),
LinkVersion: lv,
LatestURL: constructUnitURL(um.Path, um.ModulePath, version.Latest),
LatestMinorClass: latestMinorClass(lv, latestInfo),
diff --git a/internal/frontend/urlinfo.go b/internal/frontend/urlinfo.go
deleted file mode 100644
index 0c2d8c4..0000000
--- a/internal/frontend/urlinfo.go
+++ /dev/null
@@ -1,263 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package frontend
-
-import (
- "context"
- "fmt"
- "net/http"
- "strings"
-
- "golang.org/x/mod/module"
- "golang.org/x/mod/semver"
- "golang.org/x/pkgsite/internal"
- "golang.org/x/pkgsite/internal/derrors"
- "golang.org/x/pkgsite/internal/experiment"
- "golang.org/x/pkgsite/internal/fetch"
- "golang.org/x/pkgsite/internal/frontend/serrors"
- "golang.org/x/pkgsite/internal/log"
- "golang.org/x/pkgsite/internal/stdlib"
- "golang.org/x/pkgsite/internal/version"
-)
-
-type urlPathInfo struct {
- // fullPath is the full import path corresponding to the requested
- // package/module/directory page.
- fullPath string
- // modulePath is the path of the module corresponding to the fullPath and
- // resolvedVersion. If unknown, it is set to internal.UnknownModulePath.
- modulePath string
- // requestedVersion is the version requested by the user, which will be one
- // of the following: "latest", "master", a Go version tag, or a semantic
- // version.
- requestedVersion string
-}
-
-type userError struct {
- userMessage string
- err error
-}
-
-func (e *userError) Error() string {
- return e.err.Error()
-}
-
-func (e *userError) Unwrap() error {
- return e.err
-}
-
-// extractURLPathInfo extracts information from a request to pkg.go.dev.
-// If an error is returned, the user will be served an http.StatusBadRequest.
-func extractURLPathInfo(urlPath string) (_ *urlPathInfo, err error) {
- defer derrors.Wrap(&err, "extractURLPathInfo(%q)", urlPath)
-
- if m, _, _ := strings.Cut(strings.TrimPrefix(urlPath, "/"), "@"); stdlib.Contains(m) {
- return parseStdLibURLPath(urlPath)
- }
- return parseDetailsURLPath(urlPath)
-}
-
-// parseDetailsURLPath parses a URL path that refers (or may refer) to something
-// in the Go ecosystem.
-//
-// After trimming leading and trailing slashes, the path is expected to have one
-// of three forms, and we divide it into three parts: a full path, a module
-// path, and a version.
-//
-// 1. The path has no '@', like github.com/hashicorp/vault/api.
-// This is the full path. The module path is unknown. So is the version, so we
-// treat it as the latest version for whatever the path denotes.
-//
-// 2. The path has "@version" at the end, like github.com/hashicorp/vault/api@v1.2.3.
-// We split this at the '@' into a full path (github.com/hashicorp/vault/api)
-// and version (v1.2.3); the module path is still unknown.
-//
-// 3. The path has "@version" in the middle, like github.com/hashicorp/vault@v1.2.3/api.
-// (We call this the "canonical" form of a path.)
-// We remove the version to get the full path, which is again
-// github.com/hashicorp/vault/api. The version is v1.2.3, and the module path is
-// the part before the '@', github.com/hashicorp/vault.
-//
-// In one case, we do a little more than parse the urlPath into parts: if the full path
-// could be a part of the standard library (because it has no '.'), we assume it
-// is and set the modulePath to indicate the standard library.
-func parseDetailsURLPath(urlPath string) (_ *urlPathInfo, err error) {
- defer derrors.Wrap(&err, "parseDetailsURLPath(%q)", urlPath)
-
- // This splits urlPath into either:
- // /<module-path>[/<suffix>]
- // or
- // /<module-path>, @<version>/<suffix>
- // or
- // /<module-path>/<suffix>, @<version>
- modulePath, rest, found := strings.Cut(urlPath, "@")
- info := &urlPathInfo{
- fullPath: strings.TrimSuffix(strings.TrimPrefix(modulePath, "/"), "/"),
- modulePath: internal.UnknownModulePath,
- requestedVersion: version.Latest,
- }
- if found {
- // The urlPath contains a "@". Parse the version and suffix from
- // parts[1], the string after the '@'.
- endParts := strings.Split(rest, "/")
-
- // Parse the requestedVersion from the urlPath.
- // The first path component after the '@' is the version.
- // You cannot explicitly write "latest" for the version.
- if endParts[0] == version.Latest {
- return nil, &userError{
- err: fmt.Errorf("invalid version: %q", info.requestedVersion),
- userMessage: fmt.Sprintf("%q is not a valid version", endParts[0]),
- }
- }
- info.requestedVersion = endParts[0]
-
- // Parse the suffix following the "@version" from the urlPath.
- suffix := strings.Join(endParts[1:], "/")
- if suffix != "" {
- // If "@version" occurred in the middle of the path, the part before it
- // is the module path.
- info.modulePath = info.fullPath
- info.fullPath = info.fullPath + "/" + suffix
- }
- }
- if !isValidPath(info.fullPath) {
- return nil, &userError{
- err: fmt.Errorf("isValidPath(%q) is false", info.fullPath),
- userMessage: fmt.Sprintf("%q is not a valid import path", info.fullPath),
- }
- }
- return info, nil
-}
-
-func parseStdLibURLPath(urlPath string) (_ *urlPathInfo, err error) {
- defer derrors.Wrap(&err, "parseStdLibURLPath(%q)", urlPath)
-
- // This splits urlPath into either:
- // /<path>@<tag> or /<path>
- fullPath, tag, found := strings.Cut(urlPath, "@")
- fullPath = strings.TrimSuffix(strings.TrimPrefix(fullPath, "/"), "/")
- if !isValidPath(fullPath) {
- return nil, &userError{
- err: fmt.Errorf("isValidPath(%q) is false", fullPath),
- userMessage: fmt.Sprintf("%q is not a valid import path", fullPath),
- }
- }
-
- info := &urlPathInfo{
- fullPath: fullPath,
- modulePath: stdlib.ModulePath,
- }
- if !found {
- info.requestedVersion = version.Latest
- return info, nil
- }
- tag = strings.TrimSuffix(tag, "/")
- info.requestedVersion = stdlib.VersionForTag(tag)
- if info.requestedVersion == "" {
- if tag == fetch.LocalVersion {
- // Special case: 0.0.0 is the version for a local stdlib
- info.requestedVersion = fetch.LocalVersion
- return info, nil
- }
- return nil, &userError{
- err: fmt.Errorf("invalid Go tag for url: %q", urlPath),
- userMessage: fmt.Sprintf("%q is not a valid tag for the standard library", tag),
- }
- }
- return info, nil
-}
-
-// isValidPath reports whether a requested path could be a valid unit.
-func isValidPath(fullPath string) bool {
- if err := module.CheckImportPath(fullPath); err != nil {
- return false
- }
- parts := strings.Split(fullPath, "/")
- if parts[0] == "golang.org" {
- if len(parts) < 2 {
- return false
- }
- switch parts[1] {
- case "dl":
- return true
- case "x":
- return len(parts) >= 3
- default:
- return false
- }
- }
- if internal.VCSHostWithThreeElementRepoName(parts[0]) && len(parts) < 3 {
- return false
- }
- return true
-}
-
-func checkExcluded(ctx context.Context, ds internal.DataSource, fullPath string) error {
- db, ok := ds.(internal.PostgresDB)
- if !ok {
- return nil
- }
- excluded, err := db.IsExcluded(ctx, fullPath)
- if err != nil {
- return err
- }
- if excluded {
- // Return NotFound; don't let the user know that the package was excluded.
- return &serrors.ServerError{Status: http.StatusNotFound}
- }
- return nil
-}
-
-// isSupportedVersion reports whether the version is supported by the frontend.
-func isSupportedVersion(fullPath, requestedVersion string) bool {
- if stdlib.Contains(fullPath) && stdlib.SupportedBranches[requestedVersion] {
- return true
- }
- if _, ok := internal.DefaultBranches[requestedVersion]; ok {
- return !stdlib.Contains(fullPath) || requestedVersion == "master"
- }
- return requestedVersion == version.Latest || semver.IsValid(requestedVersion)
-}
-
-func setExperimentsFromQueryParam(ctx context.Context, r *http.Request) context.Context {
- if err := r.ParseForm(); err != nil {
- log.Errorf(ctx, "ParseForm: %v", err)
- return ctx
- }
- return newContextFromExps(ctx, r.Form["exp"])
-}
-
-// newContextFromExps adds and removes experiments from the context's experiment
-// set, creates a new set with the changes, and returns a context with the new
-// set. Each string in expMods can be either an experiment name, which means
-// that the experiment should be added, or "!" followed by an experiment name,
-// meaning that it should be removed.
-func newContextFromExps(ctx context.Context, expMods []string) context.Context {
- var (
- exps []string
- remove = map[string]bool{}
- )
- set := experiment.FromContext(ctx)
- for _, exp := range expMods {
- if strings.HasPrefix(exp, "!") {
- exp = exp[1:]
- if set.IsActive(exp) {
- remove[exp] = true
- }
- } else if !set.IsActive(exp) {
- exps = append(exps, exp)
- }
- }
- if len(exps) == 0 && len(remove) == 0 {
- return ctx
- }
- for _, a := range set.Active() {
- if !remove[a] {
- exps = append(exps, a)
- }
- }
- return experiment.NewContext(ctx, exps...)
-}
diff --git a/internal/frontend/stdlib_test.go b/internal/frontend/urlinfo/stdlib_test.go
similarity index 75%
rename from internal/frontend/stdlib_test.go
rename to internal/frontend/urlinfo/stdlib_test.go
index 8cf62bf..bc5b9ae 100644
--- a/internal/frontend/stdlib_test.go
+++ b/internal/frontend/urlinfo/stdlib_test.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package frontend
+package urlinfo
import (
"testing"
@@ -42,13 +42,13 @@
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
- got, err := parseStdLibURLPath(test.url)
+ got, err := parseStdlibURLPath(test.url)
if err != nil {
- t.Fatalf("parseStdLibURLPath(%q): %v)", test.url, err)
+ t.Fatalf("parseStdlibURLPath(%q): %v)", test.url, err)
}
- if test.wantVersion != got.requestedVersion || test.wantPath != got.fullPath {
- t.Fatalf("parseStdLibURLPath(%q): %q, %q, %v; want = %q, %q",
- test.url, got.fullPath, got.requestedVersion, err, test.wantPath, test.wantVersion)
+ if test.wantVersion != got.RequestedVersion || test.wantPath != got.FullPath {
+ t.Fatalf("parseStdlibURLPath(%q): %q, %q, %v; want = %q, %q",
+ test.url, got.FullPath, got.RequestedVersion, err, test.wantPath, test.wantVersion)
}
})
}
diff --git a/internal/frontend/urlinfo/urlinfo.go b/internal/frontend/urlinfo/urlinfo.go
new file mode 100644
index 0000000..b089873
--- /dev/null
+++ b/internal/frontend/urlinfo/urlinfo.go
@@ -0,0 +1,205 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package urlinfo provides functions for extracting information out
+// of url paths.
+package urlinfo
+
+import (
+ "fmt"
+ "strings"
+
+ "golang.org/x/mod/module"
+ "golang.org/x/mod/semver"
+ "golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/derrors"
+ "golang.org/x/pkgsite/internal/fetch"
+ "golang.org/x/pkgsite/internal/stdlib"
+ "golang.org/x/pkgsite/internal/version"
+)
+
+// URLPathInfo contains the information about what unit is requested in a URL path.
+type URLPathInfo struct {
+ // FullPath is the full import path corresponding to the requested
+ // package/module/directory page.
+ FullPath string
+ // ModulePath is the path of the module corresponding to the FullPath and
+ // resolvedVersion. If unknown, it is set to internal.UnknownModulePath.
+ ModulePath string
+ // requestedVersion is the version requested by the user, which will be one
+ // of the following: "latest", "master", a Go version tag, or a semantic
+ // version.
+ RequestedVersion string
+}
+
+type UserError struct {
+ UserMessage string
+ Err error
+}
+
+func (e *UserError) Error() string {
+ return e.Err.Error()
+}
+
+func (e *UserError) Unwrap() error {
+ return e.Err
+}
+
+// ExtractURLPathInfo extracts information from a request to pkg.go.dev.
+// If an error is returned, the user will be served an http.StatusBadRequest.
+func ExtractURLPathInfo(urlPath string) (_ *URLPathInfo, err error) {
+ defer derrors.Wrap(&err, "ExtractURLPathInfo(%q)", urlPath)
+
+ if m, _, _ := strings.Cut(strings.TrimPrefix(urlPath, "/"), "@"); stdlib.Contains(m) {
+ return parseStdlibURLPath(urlPath)
+ }
+ return ParseDetailsURLPath(urlPath)
+}
+
+// ParseDetailsURLPath parses a URL path that refers (or may refer) to something
+// in the Go ecosystem.
+//
+// After trimming leading and trailing slashes, the path is expected to have one
+// of three forms, and we divide it into three parts: a full path, a module
+// path, and a version.
+//
+// 1. The path has no '@', like github.com/hashicorp/vault/api.
+// This is the full path. The module path is unknown. So is the version, so we
+// treat it as the latest version for whatever the path denotes.
+//
+// 2. The path has "@version" at the end, like github.com/hashicorp/vault/api@v1.2.3.
+// We split this at the '@' into a full path (github.com/hashicorp/vault/api)
+// and version (v1.2.3); the module path is still unknown.
+//
+// 3. The path has "@version" in the middle, like github.com/hashicorp/vault@v1.2.3/api.
+// (We call this the "canonical" form of a path.)
+// We remove the version to get the full path, which is again
+// github.com/hashicorp/vault/api. The version is v1.2.3, and the module path is
+// the part before the '@', github.com/hashicorp/vault.
+//
+// In one case, we do a little more than parse the urlPath into parts: if the full path
+// could be a part of the standard library (because it has no '.'), we assume it
+// is and set the ModulePath to indicate the standard library.
+func ParseDetailsURLPath(urlPath string) (_ *URLPathInfo, err error) {
+ defer derrors.Wrap(&err, "ParseDetailsURLPath(%q)", urlPath)
+
+ // This splits urlPath into either:
+ // /<module-path>[/<suffix>]
+ // or
+ // /<module-path>, @<version>/<suffix>
+ // or
+ // /<module-path>/<suffix>, @<version>
+ modulePath, rest, found := strings.Cut(urlPath, "@")
+ info := &URLPathInfo{
+ FullPath: strings.TrimSuffix(strings.TrimPrefix(modulePath, "/"), "/"),
+ ModulePath: internal.UnknownModulePath,
+ RequestedVersion: version.Latest,
+ }
+ if found {
+ // The urlPath contains a "@". Parse the version and suffix from
+ // parts[1], the string after the '@'.
+ endParts := strings.Split(rest, "/")
+
+ // Parse the requestedVersion from the urlPath.
+ // The first path component after the '@' is the version.
+ // You cannot explicitly write "latest" for the version.
+ if endParts[0] == version.Latest {
+ return nil, &UserError{
+ Err: fmt.Errorf("invalid version: %q", info.RequestedVersion),
+ UserMessage: fmt.Sprintf("%q is not a valid version", endParts[0]),
+ }
+ }
+ info.RequestedVersion = endParts[0]
+
+ // Parse the suffix following the "@version" from the urlPath.
+ suffix := strings.Join(endParts[1:], "/")
+ if suffix != "" {
+ // If "@version" occurred in the middle of the path, the part before it
+ // is the module path.
+ info.ModulePath = info.FullPath
+ info.FullPath = info.FullPath + "/" + suffix
+ }
+ }
+ if !IsValidPath(info.FullPath) {
+ return nil, &UserError{
+ Err: fmt.Errorf("IsValidPath(%q) is false", info.FullPath),
+ UserMessage: fmt.Sprintf("%q is not a valid import path", info.FullPath),
+ }
+ }
+ return info, nil
+}
+
+func parseStdlibURLPath(urlPath string) (_ *URLPathInfo, err error) {
+ defer derrors.Wrap(&err, "parseStdlibURLPath(%q)", urlPath)
+
+ // This splits urlPath into either:
+ // /<path>@<tag> or /<path>
+ fullPath, tag, found := strings.Cut(urlPath, "@")
+ fullPath = strings.TrimSuffix(strings.TrimPrefix(fullPath, "/"), "/")
+ if !IsValidPath(fullPath) {
+ return nil, &UserError{
+ Err: fmt.Errorf("IsValidPath(%q) is false", fullPath),
+ UserMessage: fmt.Sprintf("%q is not a valid import path", fullPath),
+ }
+ }
+
+ info := &URLPathInfo{
+ FullPath: fullPath,
+ ModulePath: stdlib.ModulePath,
+ }
+ if !found {
+ info.RequestedVersion = version.Latest
+ return info, nil
+ }
+ tag = strings.TrimSuffix(tag, "/")
+ info.RequestedVersion = stdlib.VersionForTag(tag)
+ if info.RequestedVersion == "" {
+ if tag == fetch.LocalVersion {
+ // Special case: 0.0.0 is the version for a local stdlib
+ info.RequestedVersion = fetch.LocalVersion
+ return info, nil
+ }
+ return nil, &UserError{
+ Err: fmt.Errorf("invalid Go tag for url: %q", urlPath),
+ UserMessage: fmt.Sprintf("%q is not a valid tag for the standard library", tag),
+ }
+ }
+ return info, nil
+}
+
+// IsValidPath reports whether a requested path could be a valid unit.
+func IsValidPath(fullPath string) bool {
+ if err := module.CheckImportPath(fullPath); err != nil {
+ return false
+ }
+ parts := strings.Split(fullPath, "/")
+ if parts[0] == "golang.org" {
+ if len(parts) < 2 {
+ return false
+ }
+ switch parts[1] {
+ case "dl":
+ return true
+ case "x":
+ return len(parts) >= 3
+ default:
+ return false
+ }
+ }
+ if internal.VCSHostWithThreeElementRepoName(parts[0]) && len(parts) < 3 {
+ return false
+ }
+ return true
+}
+
+// IsSupportedVersion reports whether the version is supported by the frontend.
+func IsSupportedVersion(fullPath, requestedVersion string) bool {
+ if stdlib.Contains(fullPath) && stdlib.SupportedBranches[requestedVersion] {
+ return true
+ }
+ if _, ok := internal.DefaultBranches[requestedVersion]; ok {
+ return !stdlib.Contains(fullPath) || requestedVersion == "master"
+ }
+ return requestedVersion == version.Latest || semver.IsValid(requestedVersion)
+}
diff --git a/internal/frontend/urlinfo/urlinfo_test.go b/internal/frontend/urlinfo/urlinfo_test.go
new file mode 100644
index 0000000..db970ff
--- /dev/null
+++ b/internal/frontend/urlinfo/urlinfo_test.go
@@ -0,0 +1,201 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package urlinfo
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/stdlib"
+ "golang.org/x/pkgsite/internal/testing/sample"
+ "golang.org/x/pkgsite/internal/version"
+)
+
+func TestExtractURLPathInfo(t *testing.T) {
+ for _, test := range []struct {
+ name, url string
+ want *URLPathInfo // nil => want non-nil error
+ }{
+ {
+ name: "path at latest",
+ url: "/github.com/hashicorp/vault/api",
+ want: &URLPathInfo{
+ ModulePath: internal.UnknownModulePath,
+ FullPath: "github.com/hashicorp/vault/api",
+ RequestedVersion: version.Latest,
+ },
+ },
+ {
+ name: "path at version in nested module",
+ url: "/github.com/hashicorp/vault/api@v1.0.3",
+ want: &URLPathInfo{
+ ModulePath: internal.UnknownModulePath,
+ FullPath: "github.com/hashicorp/vault/api",
+ RequestedVersion: "v1.0.3",
+ },
+ },
+ {
+ name: "package at version in parent module",
+ url: "/github.com/hashicorp/vault@v1.0.3/api",
+ want: &URLPathInfo{
+ ModulePath: "github.com/hashicorp/vault",
+ FullPath: "github.com/hashicorp/vault/api",
+ RequestedVersion: "v1.0.3",
+ },
+ },
+ {
+ name: "package at version trailing slash",
+ url: "/github.com/hashicorp/vault/api@v1.0.3/",
+ want: &URLPathInfo{
+ ModulePath: internal.UnknownModulePath,
+ FullPath: "github.com/hashicorp/vault/api",
+ RequestedVersion: "v1.0.3",
+ },
+ },
+ {
+ name: "stdlib module",
+ url: "/std",
+ want: &URLPathInfo{
+ ModulePath: stdlib.ModulePath,
+ FullPath: "std",
+ RequestedVersion: version.Latest,
+ },
+ },
+ {
+ name: "stdlib module at version",
+ url: "/std@go1.14",
+ want: &URLPathInfo{
+ ModulePath: stdlib.ModulePath,
+ FullPath: "std",
+ RequestedVersion: "v1.14.0",
+ },
+ },
+ {
+ name: "stdlib",
+ url: "/net/http",
+ want: &URLPathInfo{
+ ModulePath: stdlib.ModulePath,
+ FullPath: "net/http",
+ RequestedVersion: version.Latest,
+ },
+ },
+ {
+ name: "stdlib at version",
+ url: "/net/http@go1.14",
+ want: &URLPathInfo{
+ ModulePath: stdlib.ModulePath,
+ FullPath: "net/http",
+ RequestedVersion: "v1.14.0",
+ },
+ },
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ got, err := ExtractURLPathInfo(test.url)
+ if err != nil {
+ t.Fatalf("ExtractURLPathInfo(%q): %v", test.url, err)
+ }
+ if diff := cmp.Diff(test.want, got, cmp.AllowUnexported(URLPathInfo{})); diff != "" {
+ t.Errorf("%q: mismatch (-want, +got):\n%s", test.url, diff)
+ }
+ })
+ }
+}
+
+func TestExtractURLPathInfo_Errors(t *testing.T) {
+ testCases := []struct {
+ name, url, wantModulePath, wantFullPath, wantVersion string
+ wantErr bool
+ }{
+ {
+ name: "invalid url",
+ url: "/",
+ wantErr: true,
+ },
+ {
+ name: "invalid url for github.com",
+ url: "/github.com/foo",
+ wantErr: true,
+ },
+ {
+ name: "invalid url missing module",
+ url: "@v1.0.0",
+ wantErr: true,
+ },
+ {
+ name: "explicit latest",
+ url: "/github.com/hashicorp/vault/api@latest",
+ wantErr: true,
+ },
+ {
+ name: "split stdlib",
+ url: "/net@go1.14/http",
+ wantErr: true,
+ },
+ }
+ for _, test := range testCases {
+ t.Run(test.name, func(t *testing.T) {
+ got, err := ExtractURLPathInfo(test.url)
+ if (err != nil) != test.wantErr {
+ t.Fatalf("ExtractURLPathInfo(%q) error = (%v); want error %t)", test.url, err, test.wantErr)
+ }
+ if !test.wantErr && (test.wantModulePath != got.ModulePath || test.wantVersion != got.RequestedVersion || test.wantFullPath != got.FullPath) {
+ t.Fatalf("ExtractURLPathInfo(%q): %q, %q, %q, %v; want = %q, %q, %q, want err %t",
+ test.url, got.FullPath, got.ModulePath, got.RequestedVersion, err, test.wantFullPath, test.wantModulePath, test.wantVersion, test.wantErr)
+ }
+ })
+ }
+}
+
+func TestIsValidPath(t *testing.T) {
+ tests := []struct {
+ path string
+ want bool
+ }{
+ {"net/http", true},
+ {"github.com/foo", false},
+ {"github.com/foo", false},
+ {"/github.com/foo/bar", false},
+ {"github.com/foo/bar/", false},
+ {"github.com/foo/bar", true},
+ {"github.com/foo/bar/baz", true},
+ {"golang.org/dl", true},
+ {"golang.org/dl/go1.2.3", true},
+ {"golang.org/x", false},
+ {"golang.org/x/tools", true},
+ {"golang.org/x/tools/go/packages", true},
+ {"gopkg.in/yaml.v2", true},
+ }
+ for _, test := range tests {
+ got := IsValidPath(test.path)
+ if got != test.want {
+ t.Errorf("IsValidPath(ctx, ds, %q) = %t, want %t", test.path, got, test.want)
+ }
+ }
+}
+
+func TestIsSupportedVersion(t *testing.T) {
+ tests := []struct {
+ path, version string
+ want bool
+ }{
+ {sample.ModulePath, "v1.2.3", true},
+ {sample.ModulePath, "v1.2.bad", false},
+ {sample.ModulePath, "latest", true},
+ {sample.ModulePath, "master", true},
+ {sample.ModulePath, "main", true},
+ {"net/http", "v1.2.3", true}, // IsSupportedVersion expects the goTag is already converted to semver
+ {"net/http", "v1.2.3.bad", false},
+ {"net/http", "latest", true},
+ {"net/http", "master", true},
+ {"net/http", "main", false},
+ }
+ for _, test := range tests {
+ got := IsSupportedVersion(test.path, test.version)
+ if got != test.want {
+ t.Errorf("IsSupportedVersion(ctx, ds, %q, %q) = %t, want %t", test.path, test.version, got, test.want)
+ }
+ }
+}
diff --git a/internal/frontend/urlinfo_test.go b/internal/frontend/urlinfo_test.go
deleted file mode 100644
index 7f66286..0000000
--- a/internal/frontend/urlinfo_test.go
+++ /dev/null
@@ -1,232 +0,0 @@
-// Copyright 2019 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package frontend
-
-import (
- "context"
- "sort"
- "testing"
-
- "github.com/google/go-cmp/cmp"
- "golang.org/x/pkgsite/internal"
- "golang.org/x/pkgsite/internal/experiment"
- "golang.org/x/pkgsite/internal/stdlib"
- "golang.org/x/pkgsite/internal/testing/sample"
- "golang.org/x/pkgsite/internal/version"
-)
-
-func TestExtractURLPathInfo(t *testing.T) {
- for _, test := range []struct {
- name, url string
- want *urlPathInfo // nil => want non-nil error
- }{
- {
- name: "path at latest",
- url: "/github.com/hashicorp/vault/api",
- want: &urlPathInfo{
- modulePath: internal.UnknownModulePath,
- fullPath: "github.com/hashicorp/vault/api",
- requestedVersion: version.Latest,
- },
- },
- {
- name: "path at version in nested module",
- url: "/github.com/hashicorp/vault/api@v1.0.3",
- want: &urlPathInfo{
- modulePath: internal.UnknownModulePath,
- fullPath: "github.com/hashicorp/vault/api",
- requestedVersion: "v1.0.3",
- },
- },
- {
- name: "package at version in parent module",
- url: "/github.com/hashicorp/vault@v1.0.3/api",
- want: &urlPathInfo{
- modulePath: "github.com/hashicorp/vault",
- fullPath: "github.com/hashicorp/vault/api",
- requestedVersion: "v1.0.3",
- },
- },
- {
- name: "package at version trailing slash",
- url: "/github.com/hashicorp/vault/api@v1.0.3/",
- want: &urlPathInfo{
- modulePath: internal.UnknownModulePath,
- fullPath: "github.com/hashicorp/vault/api",
- requestedVersion: "v1.0.3",
- },
- },
- {
- name: "stdlib module",
- url: "/std",
- want: &urlPathInfo{
- modulePath: stdlib.ModulePath,
- fullPath: "std",
- requestedVersion: version.Latest,
- },
- },
- {
- name: "stdlib module at version",
- url: "/std@go1.14",
- want: &urlPathInfo{
- modulePath: stdlib.ModulePath,
- fullPath: "std",
- requestedVersion: "v1.14.0",
- },
- },
- {
- name: "stdlib",
- url: "/net/http",
- want: &urlPathInfo{
- modulePath: stdlib.ModulePath,
- fullPath: "net/http",
- requestedVersion: version.Latest,
- },
- },
- {
- name: "stdlib at version",
- url: "/net/http@go1.14",
- want: &urlPathInfo{
- modulePath: stdlib.ModulePath,
- fullPath: "net/http",
- requestedVersion: "v1.14.0",
- },
- },
- } {
- t.Run(test.name, func(t *testing.T) {
- got, err := extractURLPathInfo(test.url)
- if err != nil {
- t.Fatalf("extractURLPathInfo(%q): %v", test.url, err)
- }
- if diff := cmp.Diff(test.want, got, cmp.AllowUnexported(urlPathInfo{})); diff != "" {
- t.Errorf("%q: mismatch (-want, +got):\n%s", test.url, diff)
- }
- })
- }
-}
-
-func TestExtractURLPathInfo_Errors(t *testing.T) {
- testCases := []struct {
- name, url, wantModulePath, wantFullPath, wantVersion string
- wantErr bool
- }{
- {
- name: "invalid url",
- url: "/",
- wantErr: true,
- },
- {
- name: "invalid url for github.com",
- url: "/github.com/foo",
- wantErr: true,
- },
- {
- name: "invalid url missing module",
- url: "@v1.0.0",
- wantErr: true,
- },
- {
- name: "explicit latest",
- url: "/github.com/hashicorp/vault/api@latest",
- wantErr: true,
- },
- {
- name: "split stdlib",
- url: "/net@go1.14/http",
- wantErr: true,
- },
- }
- for _, test := range testCases {
- t.Run(test.name, func(t *testing.T) {
- got, err := extractURLPathInfo(test.url)
- if (err != nil) != test.wantErr {
- t.Fatalf("extractURLPathInfo(%q) error = (%v); want error %t)", test.url, err, test.wantErr)
- }
- if !test.wantErr && (test.wantModulePath != got.modulePath || test.wantVersion != got.requestedVersion || test.wantFullPath != got.fullPath) {
- t.Fatalf("extractURLPathInfo(%q): %q, %q, %q, %v; want = %q, %q, %q, want err %t",
- test.url, got.fullPath, got.modulePath, got.requestedVersion, err, test.wantFullPath, test.wantModulePath, test.wantVersion, test.wantErr)
- }
- })
- }
-}
-
-func TestNewContextFromExps(t *testing.T) {
- for _, test := range []struct {
- mods []string
- want []string
- }{
- {
- mods: []string{"c", "a", "b"},
- want: []string{"a", "b", "c"},
- },
- {
- mods: []string{"d", "a"},
- want: []string{"a", "b", "c", "d"},
- },
- {
- mods: []string{"d", "!b", "!a", "c"},
- want: []string{"c", "d"},
- },
- } {
- ctx := experiment.NewContext(context.Background(), "a", "b", "c")
- ctx = newContextFromExps(ctx, test.mods)
- got := experiment.FromContext(ctx).Active()
- sort.Strings(got)
- if !cmp.Equal(got, test.want) {
- t.Errorf("mods=%v:\ngot %v\nwant %v", test.mods, got, test.want)
- }
- }
-}
-
-func TestIsValidPath(t *testing.T) {
- tests := []struct {
- path string
- want bool
- }{
- {"net/http", true},
- {"github.com/foo", false},
- {"github.com/foo", false},
- {"/github.com/foo/bar", false},
- {"github.com/foo/bar/", false},
- {"github.com/foo/bar", true},
- {"github.com/foo/bar/baz", true},
- {"golang.org/dl", true},
- {"golang.org/dl/go1.2.3", true},
- {"golang.org/x", false},
- {"golang.org/x/tools", true},
- {"golang.org/x/tools/go/packages", true},
- {"gopkg.in/yaml.v2", true},
- }
- for _, test := range tests {
- got := isValidPath(test.path)
- if got != test.want {
- t.Errorf("isValidPath(ctx, ds, %q) = %t, want %t", test.path, got, test.want)
- }
- }
-}
-
-func TestIsSupportedVersion(t *testing.T) {
- tests := []struct {
- path, version string
- want bool
- }{
- {sample.ModulePath, "v1.2.3", true},
- {sample.ModulePath, "v1.2.bad", false},
- {sample.ModulePath, "latest", true},
- {sample.ModulePath, "master", true},
- {sample.ModulePath, "main", true},
- {"net/http", "v1.2.3", true}, // isSupportedVersion expects the goTag is already converted to semver
- {"net/http", "v1.2.3.bad", false},
- {"net/http", "latest", true},
- {"net/http", "master", true},
- {"net/http", "main", false},
- }
- for _, test := range tests {
- got := isSupportedVersion(test.path, test.version)
- if got != test.want {
- t.Errorf("isSupportedVersion(ctx, ds, %q, %q) = %t, want %t", test.path, test.version, got, test.want)
- }
- }
-}