internal/localdatasource: implement a local datasource
Create localdatasource package which implements an in-memory
internal.DataSource to display documentation locally.
Add tests for localdatasource.DataSource.
Updates golang/go#40159
Change-Id: Ie18dd68e6108cfa361c4db31030679fc55661d35
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/260778
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/localdatasource/datasource.go b/internal/localdatasource/datasource.go
new file mode 100644
index 0000000..c08261e
--- /dev/null
+++ b/internal/localdatasource/datasource.go
@@ -0,0 +1,186 @@
+// Copyright 2020 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 localdatasource implements an in-memory internal.DataSource used to load
+// and display documentation for local modules that are not available via a proxy.
+// Similar to proxydatasource, search and other tabs are not supported in this mode.
+package localdatasource
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/derrors"
+ "golang.org/x/pkgsite/internal/fetch"
+ "golang.org/x/pkgsite/internal/source"
+)
+
+// DataSource implements an in-memory internal.DataSource used to display documentation
+// locally. DataSource is not backed by a database or a proxy instance.
+type DataSource struct {
+ sourceClient *source.Client
+
+ mu sync.Mutex
+ loadedModules map[string]*internal.Module
+}
+
+// New creates and returns a new local datasource that bypasses license
+// checks by default.
+func New() *DataSource {
+ return &DataSource{
+ loadedModules: make(map[string]*internal.Module),
+ }
+}
+
+// Load loads a module from the given local path. Loading is required before
+// being able to display the module.
+func (ds *DataSource) Load(ctx context.Context, localPath string) (err error) {
+ defer derrors.Wrap(&err, "Load(%q)", localPath)
+ return ds.fetch(ctx, "", localPath)
+}
+
+// LoadFromGOPATH loads a module from GOPATH using the given import path. The full
+// path of the module should be GOPATH/src/importPath. If several GOPATHs exist, the
+// module is loaded from the first one that contains the import path. Loading is required
+// before being able to display the module.
+func (ds *DataSource) LoadFromGOPATH(ctx context.Context, importPath string) (err error) {
+ defer derrors.Wrap(&err, "LoadFromGOPATH(%q)", importPath)
+
+ path := getFullPath(importPath)
+ if path == "" {
+ return fmt.Errorf("path %s doesn't exist: %w", importPath, derrors.NotFound)
+ }
+
+ return ds.fetch(ctx, importPath, path)
+}
+
+// fetch fetches a module using FetchLocalModule and adds it to the datasource.
+// If the fetching fails, an error is returned.
+func (ds *DataSource) fetch(ctx context.Context, modulePath, localPath string) error {
+ fr := fetch.FetchLocalModule(ctx, modulePath, localPath, ds.sourceClient)
+ if fr.Error != nil {
+ return fr.Error
+ }
+
+ fr.Module.IsRedistributable = true
+ for _, unit := range fr.Module.Units {
+ unit.IsRedistributable = true
+ }
+ for _, pkg := range fr.Module.LegacyPackages {
+ pkg.IsRedistributable = true
+ }
+
+ ds.mu.Lock()
+ defer ds.mu.Unlock()
+ ds.loadedModules[fr.ModulePath] = fr.Module
+ return nil
+}
+
+// getFullPath takes an import path, tests it relative to each GOPATH, and returns
+// a full path to the module. If the given import path doesn't exist in any GOPATH,
+// an empty string is returned.
+func getFullPath(modulePath string) string {
+ gopaths := filepath.SplitList(os.Getenv("GOPATH"))
+ for _, gopath := range gopaths {
+ path := filepath.Join(gopath, "src", modulePath)
+ info, err := os.Stat(path)
+ if err == nil && info.IsDir() {
+ return path
+ }
+ }
+ return ""
+}
+
+// GetUnit returns information about a unit. Both the module path and package
+// path must both be known.
+func (ds *DataSource) GetUnit(ctx context.Context, pathInfo *internal.UnitMeta, fields internal.FieldSet) (_ *internal.Unit, err error) {
+ defer derrors.Wrap(&err, "GetUnit(%q, %q)", pathInfo.Path, pathInfo.ModulePath)
+
+ modulepath := pathInfo.ModulePath
+ path := pathInfo.Path
+
+ ds.mu.Lock()
+ defer ds.mu.Unlock()
+ if ds.loadedModules[modulepath] == nil {
+ return nil, fmt.Errorf("%s not loaded: %w", modulepath, derrors.NotFound)
+ }
+
+ module := ds.loadedModules[modulepath]
+ for _, unit := range module.Units {
+ if unit.Path == path {
+ return unit, nil
+ }
+ }
+
+ return nil, fmt.Errorf("%s not found: %w", path, derrors.NotFound)
+}
+
+// GetUnitMeta returns information about a path.
+func (ds *DataSource) GetUnitMeta(ctx context.Context, path, requestedModulePath, requestedVersion string) (_ *internal.UnitMeta, err error) {
+ defer derrors.Wrap(&err, "GetUnitMeta(%q, %q, %q)", path, requestedModulePath, requestedVersion)
+
+ if requestedModulePath == internal.UnknownModulePath {
+ requestedModulePath, err = ds.findModule(path)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ ds.mu.Lock()
+ module := ds.loadedModules[requestedModulePath]
+ ds.mu.Unlock()
+
+ um := &internal.UnitMeta{
+ Path: path,
+ ModulePath: requestedModulePath,
+ Version: fetch.LocalVersion,
+ CommitTime: fetch.LocalCommitTime,
+ }
+
+ for _, u := range module.Units {
+ if u.Path == path {
+ um.Name = u.Name
+ um.IsRedistributable = u.IsRedistributable
+ }
+ }
+
+ return um, nil
+}
+
+// findModule finds the longest module path in loadedModules containing the given
+// package path. It iteratively checks parent directories to find an import path.
+// Returns an error if no module is found.
+func (ds *DataSource) findModule(pkgPath string) (_ string, err error) {
+ defer derrors.Wrap(&err, "findModule(%q)", pkgPath)
+
+ pkgPath = strings.TrimLeft(pkgPath, "/")
+
+ ds.mu.Lock()
+ defer ds.mu.Unlock()
+ for modulePath := pkgPath; modulePath != "" && modulePath != "."; modulePath = path.Dir(modulePath) {
+ if ds.loadedModules[modulePath] != nil {
+ return modulePath, nil
+ }
+ }
+
+ return "", fmt.Errorf("%s not loaded: %w", pkgPath, derrors.NotFound)
+}
+
+// GetLatestMajorVersion returns the latest major version of a series path.
+// When fetching local modules, version is not accounted for, so an empty
+// string is returned.
+func (ds *DataSource) GetLatestMajorVersion(ctx context.Context, seriesPath string) (string, error) {
+ return "", nil
+}
+
+// GetNestedModules is not implemented.
+func (ds *DataSource) GetNestedModules(ctx context.Context, modulePath string) ([]*internal.ModuleInfo, error) {
+ return nil, nil
+}
diff --git a/internal/localdatasource/datasource_test.go b/internal/localdatasource/datasource_test.go
new file mode 100644
index 0000000..69a37a8
--- /dev/null
+++ b/internal/localdatasource/datasource_test.go
@@ -0,0 +1,237 @@
+// Copyright 2020 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 localdatasource
+
+import (
+ "context"
+ "errors"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/derrors"
+ "golang.org/x/pkgsite/internal/fetch"
+ "golang.org/x/pkgsite/internal/testing/testhelper"
+)
+
+var (
+ ctx context.Context
+ cancel func()
+ datasource *DataSource
+)
+
+func setup(t *testing.T) (context.Context, func(), *DataSource, error) {
+ t.Helper()
+
+ // Setup only once.
+ if datasource != nil {
+ return ctx, cancel, datasource, nil
+ }
+
+ modules := []map[string]string{
+ {
+ "go.mod": "module github.com/my/module\n\ngo 1.12",
+ "LICENSE": testhelper.BSD0License,
+ "README.md": "README FILE FOR TESTING.",
+ "bar/COPYING": testhelper.MITLicense,
+ "bar/README.md": "Another README FILE FOR TESTING.",
+ "bar/bar.go": `
+ // package bar
+ package bar
+
+ // Bar returns the string "bar".
+ func Bar() string {
+ return "bar"
+ }`,
+ "foo/LICENSE.md": testhelper.MITLicense,
+ "foo/foo.go": `
+ // package foo
+ package foo
+
+ import (
+ "fmt"
+
+ "github.com/my/module/bar"
+ )
+
+ // FooBar returns the string "foo bar".
+ func FooBar() string {
+ return fmt.Sprintf("foo %s", bar.Bar())
+ }`,
+ },
+ {
+ "go.mod": "module github.com/no/license\n\ngo 1.12",
+ "LICENSE": "unknown",
+ "bar/bar.go": `
+ // package bar
+ package bar
+
+ // Bar returns the string "bar".
+ func Bar() string {
+ return "bar"
+ }`,
+ },
+ }
+
+ datasource = New()
+ ctx, cancel = context.WithTimeout(context.Background(), 20*time.Second)
+ for _, module := range modules {
+ directory, err := testhelper.CreateTestDirectory(module)
+ if err != nil {
+ return ctx, func() { cancel() }, nil, err
+ }
+ defer os.RemoveAll(directory)
+
+ err = datasource.Load(ctx, directory)
+ if err != nil {
+ return ctx, func() { cancel() }, nil, err
+ }
+ }
+
+ return ctx, func() { cancel() }, datasource, nil
+}
+
+func TestGetUnitMeta(t *testing.T) {
+ ctx, cancel, ds, err := setup(t)
+ if err != nil {
+ t.Fatalf("setup failed: %s", err.Error())
+ }
+ defer cancel()
+
+ for _, test := range []struct {
+ path, modulePath string
+ want *internal.UnitMeta
+ wantErr error
+ }{
+ {
+ path: "github.com/my/module",
+ modulePath: "github.com/my/module",
+ want: &internal.UnitMeta{
+ Path: "github.com/my/module",
+ ModulePath: "github.com/my/module",
+ IsRedistributable: true,
+ Version: fetch.LocalVersion,
+ CommitTime: fetch.LocalCommitTime,
+ },
+ },
+ {
+ path: "github.com/my/module/bar",
+ modulePath: "github.com/my/module",
+ want: &internal.UnitMeta{
+ Path: "github.com/my/module/bar",
+ Name: "bar",
+ ModulePath: "github.com/my/module",
+ IsRedistributable: true,
+ Version: fetch.LocalVersion,
+ CommitTime: fetch.LocalCommitTime,
+ },
+ },
+ {
+ path: "github.com/my/module/foo",
+ modulePath: "github.com/my/module",
+ want: &internal.UnitMeta{
+ Path: "github.com/my/module/foo",
+ Name: "foo",
+ ModulePath: "github.com/my/module",
+ IsRedistributable: true,
+ Version: fetch.LocalVersion,
+ CommitTime: fetch.LocalCommitTime,
+ },
+ },
+ {
+ path: "github.com/my/module/bar",
+ modulePath: internal.UnknownModulePath,
+ want: &internal.UnitMeta{
+ Path: "github.com/my/module/bar",
+ Name: "bar",
+ ModulePath: "github.com/my/module",
+ IsRedistributable: true,
+ Version: fetch.LocalVersion,
+ CommitTime: fetch.LocalCommitTime,
+ },
+ },
+ {
+ path: "github.com/not/loaded",
+ modulePath: internal.UnknownModulePath,
+ wantErr: derrors.NotFound,
+ },
+ } {
+ t.Run(test.path, func(t *testing.T) {
+ got, err := ds.GetUnitMeta(ctx, test.path, test.modulePath, fetch.LocalVersion)
+ if test.wantErr != nil {
+ if !errors.Is(err, test.wantErr) {
+ t.Errorf("GetUnitMeta(%q, %q): %v; wantErr = %v)", test.path, test.modulePath, err, test.wantErr)
+ }
+ } else {
+ if err != nil {
+ t.Fatal(err)
+ }
+ if diff := cmp.Diff(test.want, got); diff != "" {
+ t.Errorf("mismatch (-want +got):\n%s", diff)
+
+ }
+ }
+ })
+ }
+}
+
+func TestGetUnit(t *testing.T) {
+ // This is a simple test to verify that data is fetched correctly. The
+ // return value of FetchResult is tested in internal/fetch so no need
+ // to repeat it.
+ ctx, cancel, ds, err := setup(t)
+ if err != nil {
+ t.Fatalf("setup failed: %s", err.Error())
+ }
+ defer cancel()
+
+ for _, test := range []struct {
+ path, modulePath string
+ wantLoaded bool
+ }{
+ {
+ path: "github.com/my/module",
+ modulePath: "github.com/my/module",
+ wantLoaded: true,
+ },
+ {
+ path: "github.com/my/module/foo",
+ modulePath: "github.com/my/module",
+ wantLoaded: true,
+ },
+ {
+ path: "github.com/no/license/bar",
+ modulePath: "github.com/no/license",
+ wantLoaded: true,
+ },
+ {
+ path: "github.com/not/loaded",
+ modulePath: internal.UnknownModulePath,
+ },
+ } {
+ t.Run(test.path, func(t *testing.T) {
+ um := &internal.UnitMeta{
+ Path: test.path,
+ ModulePath: test.modulePath,
+ }
+ got, err := ds.GetUnit(ctx, um, 0)
+ if !test.wantLoaded {
+ if err == nil {
+ t.Fatalf("returned not loaded module %q", test.path)
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("failed for %q: %q", test.path, err.Error())
+ }
+
+ if gotEmpty := (got.Documentation == nil && got.Readme == nil); gotEmpty {
+ t.Errorf("%q: gotEmpty = %t", test.path, gotEmpty)
+ }
+ })
+ }
+}