Skip to content

Commit 0077d00

Browse files
sudo-sturbiajulieqiu
authored andcommitted
internal/fetch: create FetchLocalModule
Create FetchLocalModule to fetch modules from local directories without needing a proxy instance. Add tests for FetchLocalModule and update helper functions. Updates golang/go#40159 Change-Id: Ie1296e6a2008bf8d7c3811864d5d948e042fcb38 Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/260777 Reviewed-by: Jonathan Amsterdam <jba@google.com> Reviewed-by: Julie Qiu <julie@golang.org> Trust: Jonathan Amsterdam <jba@google.com> Run-TryBot: Jonathan Amsterdam <jba@google.com>
1 parent 77c9937 commit 0077d00

File tree

4 files changed

+376
-72
lines changed

4 files changed

+376
-72
lines changed

internal/fetch/fetch_test.go

+81-70
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package fetch
77
import (
88
"context"
99
"errors"
10+
"fmt"
1011
"io"
1112
"net/http"
1213
"net/http/httptest"
@@ -18,7 +19,7 @@ import (
1819
"golang.org/x/pkgsite/internal"
1920
"golang.org/x/pkgsite/internal/derrors"
2021
"golang.org/x/pkgsite/internal/godoc"
21-
"golang.org/x/pkgsite/internal/proxy"
22+
"golang.org/x/pkgsite/internal/licenses"
2223
"golang.org/x/pkgsite/internal/source"
2324
"golang.org/x/pkgsite/internal/stdlib"
2425
"golang.org/x/pkgsite/internal/testing/sample"
@@ -48,6 +49,7 @@ func TestFetchModule(t *testing.T) {
4849
name string
4950
mod *testModule
5051
fetchVersion string
52+
proxyOnly bool
5153
}{
5254
{name: "basic", mod: moduleNoGoMod},
5355
{name: "wasm", mod: moduleWasm},
@@ -63,54 +65,67 @@ func TestFetchModule(t *testing.T) {
6365
{name: "module with type example", mod: moduleTypeExample},
6466
{name: "module with method example", mod: moduleMethodExample},
6567
{name: "module with nonredistributable packages", mod: moduleNonRedist},
66-
{name: "stdlib module", mod: moduleStd},
67-
{name: "master version of module", mod: moduleMaster, fetchVersion: "master"},
68-
{name: "latest version of module", mod: moduleLatest, fetchVersion: "latest"},
68+
// Proxy only as stdlib is not accounted for in local mode
69+
{name: "stdlib module", mod: moduleStd, proxyOnly: true},
70+
// Proxy only as version is pre specified in local mode
71+
{name: "master version of module", mod: moduleMaster, fetchVersion: "master", proxyOnly: true},
72+
// Proxy only as version is pre specified in local mode
73+
{name: "latest version of module", mod: moduleLatest, fetchVersion: "latest", proxyOnly: true},
6974
} {
70-
t.Run(test.name, func(t *testing.T) {
71-
ctx := context.Background()
72-
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
73-
defer cancel()
74-
75-
modulePath := test.mod.mod.ModulePath
76-
version := test.mod.mod.Version
77-
fetchVersion := test.fetchVersion
78-
if version == "" {
79-
version = "v1.0.0"
80-
}
81-
if fetchVersion == "" {
82-
fetchVersion = version
83-
}
84-
sourceClient := source.NewClient(sourceTimeout)
85-
proxyClient, teardownProxy := proxy.SetupTestClient(t, []*proxy.Module{{
86-
ModulePath: modulePath,
87-
Version: version,
88-
Files: test.mod.mod.Files,
89-
}})
90-
defer teardownProxy()
91-
got := FetchModule(ctx, modulePath, fetchVersion, proxyClient, sourceClient)
92-
defer got.Defer()
93-
if got.Error != nil {
94-
t.Fatal(got.Error)
95-
}
96-
d := licenseDetector(ctx, t, modulePath, got.ResolvedVersion, proxyClient)
97-
fr := cleanFetchResult(test.mod.fr, d)
98-
sortFetchResult(fr)
99-
sortFetchResult(got)
100-
opts := []cmp.Option{
101-
cmpopts.IgnoreFields(internal.LegacyPackage{}, "DocumentationHTML"),
102-
cmpopts.IgnoreFields(internal.Documentation{}, "HTML"),
103-
cmpopts.IgnoreFields(internal.PackageVersionState{}, "Error"),
104-
cmpopts.IgnoreFields(FetchResult{}, "Defer"),
105-
cmp.AllowUnexported(source.Info{}),
106-
cmpopts.EquateEmpty(),
107-
}
108-
opts = append(opts, sample.LicenseCmpOpts...)
109-
if diff := cmp.Diff(fr, got, opts...); diff != "" {
110-
t.Fatalf("mismatch (-want +got):\n%s", diff)
75+
for _, fetcher := range []struct {
76+
name string
77+
fetch func(t *testing.T, withLicenseDetector bool, ctx context.Context, mod *testModule, fetchVersion string) (*FetchResult, *licenses.Detector)
78+
}{
79+
{name: "proxy", fetch: proxyFetcher},
80+
{name: "local", fetch: localFetcher},
81+
} {
82+
if test.proxyOnly && fetcher.name == "local" {
83+
continue
11184
}
112-
validateDocumentationHTML(t, got.Module, fr.Module)
113-
})
85+
t.Run(fmt.Sprintf("%s:%s", fetcher.name, test.name), func(t *testing.T) {
86+
ctx := context.Background()
87+
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
88+
defer cancel()
89+
90+
got, d := fetcher.fetch(t, true, ctx, test.mod, test.fetchVersion)
91+
defer got.Defer()
92+
if got.Error != nil {
93+
t.Fatal("fetching failed: %w", got.Error)
94+
}
95+
96+
if fetcher.name == "proxy" {
97+
test.mod.fr = cleanFetchResult(t, test.mod.fr, d)
98+
}
99+
fr := updateFetchResultVersions(t, test.mod.fr, fetcher.name == "local")
100+
sortFetchResult(fr)
101+
sortFetchResult(got)
102+
opts := []cmp.Option{
103+
cmpopts.IgnoreFields(internal.LegacyPackage{}, "DocumentationHTML"),
104+
cmpopts.IgnoreFields(internal.Documentation{}, "HTML"),
105+
cmpopts.IgnoreFields(internal.PackageVersionState{}, "Error"),
106+
cmpopts.IgnoreFields(FetchResult{}, "Defer"),
107+
cmp.AllowUnexported(source.Info{}),
108+
cmpopts.EquateEmpty(),
109+
}
110+
if fetcher.name == "local" {
111+
opts = append(opts,
112+
[]cmp.Option{
113+
// Pre specified for all modules
114+
cmpopts.IgnoreFields(internal.Module{}, "SourceInfo"),
115+
cmpopts.IgnoreFields(internal.Module{}, "Version"),
116+
cmpopts.IgnoreFields(FetchResult{}, "RequestedVersion"),
117+
cmpopts.IgnoreFields(FetchResult{}, "ResolvedVersion"),
118+
cmpopts.IgnoreFields(internal.Module{}, "CommitTime"),
119+
}...)
120+
}
121+
122+
opts = append(opts, sample.LicenseCmpOpts...)
123+
if diff := cmp.Diff(fr, got, opts...); diff != "" {
124+
t.Fatalf("mismatch (-want +got):\n%s", diff)
125+
}
126+
validateDocumentationHTML(t, got.Module, fr.Module)
127+
})
128+
}
114129
}
115130
}
116131
func TestFetchModule_Errors(t *testing.T) {
@@ -125,29 +140,25 @@ func TestFetchModule_Errors(t *testing.T) {
125140
{name: "alternative", mod: moduleAlternative, wantErr: derrors.AlternativeModule, wantGoModPath: "canonical"},
126141
{name: "empty module", mod: moduleEmpty, wantErr: derrors.BadModule},
127142
} {
128-
t.Run(test.name, func(t *testing.T) {
129-
modulePath := test.mod.mod.ModulePath
130-
version := test.mod.mod.Version
131-
if version == "" {
132-
version = "v1.0.0"
133-
}
134-
proxyClient, teardownProxy := proxy.SetupTestClient(t, []*proxy.Module{{
135-
ModulePath: modulePath,
136-
Files: test.mod.mod.Files,
137-
}})
138-
defer teardownProxy()
139-
140-
sourceClient := source.NewClient(sourceTimeout)
141-
got := FetchModule(ctx, modulePath, "v1.0.0", proxyClient, sourceClient)
142-
defer got.Defer()
143-
if !errors.Is(got.Error, test.wantErr) {
144-
t.Fatalf("FetchModule(ctx, %q, v1.0.0, proxyClient, sourceClient): %v; wantErr = %v)", modulePath, got.Error, test.wantErr)
145-
}
146-
if test.wantGoModPath != "" {
147-
if got == nil || got.GoModPath != test.wantGoModPath {
148-
t.Errorf("got %+v, wanted GoModPath %q", got, test.wantGoModPath)
143+
for _, fetcher := range []struct {
144+
name string
145+
fetch func(t *testing.T, withLicenseDetector bool, ctx context.Context, mod *testModule, fetchVersion string) (*FetchResult, *licenses.Detector)
146+
}{
147+
{name: "proxy", fetch: proxyFetcher},
148+
{name: "local", fetch: localFetcher},
149+
} {
150+
t.Run(fmt.Sprintf("%s:%s", fetcher.name, test.name), func(t *testing.T) {
151+
got, _ := fetcher.fetch(t, false, ctx, test.mod, "")
152+
defer got.Defer()
153+
if !errors.Is(got.Error, test.wantErr) {
154+
t.Fatalf("got error = %v; wantErr = %v)", got.Error, test.wantErr)
149155
}
150-
}
151-
})
156+
if test.wantGoModPath != "" {
157+
if got == nil || got.GoModPath != test.wantGoModPath {
158+
t.Errorf("got %+v, wanted GoModPath %q", got, test.wantGoModPath)
159+
}
160+
}
161+
})
162+
}
152163
}
153164
}

internal/fetch/fetchlocal.go

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package fetch
6+
7+
import (
8+
"archive/zip"
9+
"bytes"
10+
"context"
11+
"fmt"
12+
"io"
13+
"io/ioutil"
14+
"net/http"
15+
"os"
16+
"path/filepath"
17+
"strings"
18+
"time"
19+
20+
"golang.org/x/mod/modfile"
21+
"golang.org/x/pkgsite/internal/derrors"
22+
"golang.org/x/pkgsite/internal/log"
23+
"golang.org/x/pkgsite/internal/source"
24+
)
25+
26+
// Version and commit time are pre specified when fetching a local module, as these
27+
// fields are normally obtained from a proxy.
28+
var (
29+
LocalVersion = "latest"
30+
LocalCommitTime = time.Time{}
31+
)
32+
33+
// FetchLocalModule fetches a module from a local directory and process its contents
34+
// to return an internal.Module and other related information. modulePath is not necessary
35+
// if the module has a go.mod file, but if both exist, then they must match.
36+
// FetchResult.Error should be checked to verify that the fetch succeeded. Even if the
37+
// error is non-nil the result may contain useful data.
38+
func FetchLocalModule(ctx context.Context, modulePath, localPath string, sourceClient *source.Client) *FetchResult {
39+
fr := &FetchResult{
40+
ModulePath: modulePath,
41+
RequestedVersion: LocalVersion,
42+
ResolvedVersion: LocalVersion,
43+
Defer: func() {},
44+
}
45+
46+
var fi *FetchInfo
47+
defer func() {
48+
if fr.Error != nil {
49+
derrors.Wrap(&fr.Error, "FetchLocalModule(%q, %q)", modulePath, localPath)
50+
fr.Status = derrors.ToStatus(fr.Error)
51+
}
52+
if fr.Status == 0 {
53+
fr.Status = http.StatusOK
54+
}
55+
if fi != nil {
56+
finishFetchInfo(fi, fr.Status, fr.Error)
57+
}
58+
log.Debugf(ctx, "memory after fetch of %s: %dM", fr.ModulePath, allocMeg())
59+
60+
}()
61+
62+
info, err := os.Stat(localPath)
63+
if err != nil {
64+
fr.Error = fmt.Errorf("%s: %w", err.Error(), derrors.NotFound)
65+
return fr
66+
}
67+
68+
if !info.IsDir() {
69+
fr.Error = fmt.Errorf("%s not a directory: %w", localPath, derrors.NotFound)
70+
return fr
71+
}
72+
73+
fi = &FetchInfo{
74+
ModulePath: fr.ModulePath,
75+
Version: fr.ResolvedVersion,
76+
Start: time.Now(),
77+
}
78+
startFetchInfo(fi)
79+
80+
// Options for module path are either the modulePath parameter or go.mod file.
81+
// Accepted cases:
82+
// - Both are given and are the same.
83+
// - Only one is given. Note that: if modulePath is given and there's no go.mod
84+
// file, then the package is assumed to be using GOPATH.
85+
// Errors:
86+
// - Both are given and are different.
87+
// - Neither is given.
88+
if goModBytes, err := ioutil.ReadFile(filepath.Join(localPath, "go.mod")); err != nil {
89+
fr.GoModPath = modulePath
90+
} else {
91+
fr.GoModPath = modfile.ModulePath(goModBytes)
92+
if fr.GoModPath != modulePath && modulePath != "" {
93+
fr.Error = fmt.Errorf("module path=%s, go.mod path=%s: %w", modulePath, fr.GoModPath, derrors.AlternativeModule)
94+
return fr
95+
}
96+
}
97+
98+
if fr.GoModPath == "" {
99+
fr.Error = fmt.Errorf("no module path: %w", derrors.BadModule)
100+
return fr
101+
}
102+
fr.ModulePath = fr.GoModPath
103+
104+
zipReader, err := createZipReader(localPath, fr.GoModPath, LocalVersion)
105+
if err != nil {
106+
fr.Error = fmt.Errorf("couldn't create a zip: %s, %w", err.Error(), derrors.BadModule)
107+
return fr
108+
}
109+
110+
mod, pvs, err := processZipFile(ctx, fr.GoModPath, LocalVersion, LocalCommitTime, zipReader, sourceClient)
111+
if err != nil {
112+
fr.Error = err
113+
return fr
114+
}
115+
116+
fr.Module = mod
117+
fr.PackageVersionStates = pvs
118+
fr.Module.SourceInfo = nil // version is not known, so even if info is found it most likely is wrong.
119+
for _, state := range fr.PackageVersionStates {
120+
if state.Status != http.StatusOK {
121+
fr.Status = derrors.ToStatus(derrors.HasIncompletePackages)
122+
}
123+
}
124+
return fr
125+
}
126+
127+
// createZipReader creates a zip file from a directory given a local path and
128+
// returns a zip.Reader to be passed to processZipFile. The purpose of the
129+
// function is to transform a local go module into a zip file to be processed by
130+
// existing functions.
131+
func createZipReader(localPath, modulePath, version string) (*zip.Reader, error) {
132+
buf := new(bytes.Buffer)
133+
w := zip.NewWriter(buf)
134+
err := filepath.Walk(localPath, func(path string, info os.FileInfo, err error) error {
135+
if err != nil {
136+
return err
137+
}
138+
if info.IsDir() {
139+
return nil
140+
}
141+
142+
readFrom, err := os.Open(path)
143+
if err != nil {
144+
return err
145+
}
146+
defer readFrom.Close()
147+
148+
writeTo, err := w.Create(filepath.Join(moduleVersionDir(modulePath, version), strings.TrimPrefix(path, localPath)))
149+
if err != nil {
150+
return err
151+
}
152+
153+
_, err = io.Copy(writeTo, readFrom)
154+
return err
155+
})
156+
if err != nil {
157+
return nil, err
158+
}
159+
if err := w.Close(); err != nil {
160+
return nil, err
161+
}
162+
163+
reader := bytes.NewReader(buf.Bytes())
164+
return zip.NewReader(reader, reader.Size())
165+
}

0 commit comments

Comments
 (0)