blob: dba9935b5820b51a18224367456aa485fc76ff38 [file] [log] [blame]
// Copyright 2018 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 codehost
import (
"archive/zip"
"bytes"
"cmd/go/internal/cfg"
"cmd/go/internal/vcweb/vcstest"
"context"
"flag"
"internal/testenv"
"io"
"io/fs"
"log"
"os"
"path"
"path/filepath"
"reflect"
"runtime"
"strings"
"sync"
"testing"
"time"
)
func TestMain(m *testing.M) {
flag.Parse()
if err := testMain(m); err != nil {
log.Fatal(err)
}
}
var gitrepo1, hgrepo1, vgotest1 string
var altRepos = func() []string {
return []string{
"localGitRepo",
hgrepo1,
}
}
// TODO: Convert gitrepo1 to svn, bzr, fossil and add tests.
// For now, at least the hgrepo1 tests check the general vcs.go logic.
// localGitRepo is like gitrepo1 but allows archive access
// (although that doesn't really matter after CL 120041),
// and has a file:// URL instead of http:// or https://
// (which might still matter).
var localGitRepo string
// localGitURL initializes the repo in localGitRepo and returns its URL.
func localGitURL(t testing.TB) string {
testenv.MustHaveExecPath(t, "git")
if runtime.GOOS == "android" && strings.HasSuffix(testenv.Builder(), "-corellium") {
testenv.SkipFlaky(t, 59940)
}
localGitURLOnce.Do(func() {
// Clone gitrepo1 into a local directory.
// If we use a file:// URL to access the local directory,
// then git starts up all the usual protocol machinery,
// which will let us test remote git archive invocations.
_, localGitURLErr = Run(context.Background(), "", "git", "clone", "--mirror", gitrepo1, localGitRepo)
if localGitURLErr != nil {
return
}
_, localGitURLErr = Run(context.Background(), localGitRepo, "git", "config", "daemon.uploadarch", "true")
})
if localGitURLErr != nil {
t.Fatal(localGitURLErr)
}
// Convert absolute path to file URL. LocalGitRepo will not accept
// Windows absolute paths because they look like a host:path remote.
// TODO(golang.org/issue/32456): use url.FromFilePath when implemented.
if strings.HasPrefix(localGitRepo, "/") {
return "file://" + localGitRepo
} else {
return "file:///" + filepath.ToSlash(localGitRepo)
}
}
var (
localGitURLOnce sync.Once
localGitURLErr error
)
func testMain(m *testing.M) (err error) {
cfg.BuildX = testing.Verbose()
srv, err := vcstest.NewServer()
if err != nil {
return err
}
defer func() {
if closeErr := srv.Close(); err == nil {
err = closeErr
}
}()
gitrepo1 = srv.HTTP.URL + "/git/gitrepo1"
hgrepo1 = srv.HTTP.URL + "/hg/hgrepo1"
vgotest1 = srv.HTTP.URL + "/git/vgotest1"
dir, err := os.MkdirTemp("", "gitrepo-test-")
if err != nil {
return err
}
defer func() {
if rmErr := os.RemoveAll(dir); err == nil {
err = rmErr
}
}()
localGitRepo = filepath.Join(dir, "gitrepo2")
// Redirect the module cache to a fresh directory to avoid crosstalk, and make
// it read/write so that the test can still clean it up easily when done.
cfg.GOMODCACHE = filepath.Join(dir, "modcache")
cfg.ModCacheRW = true
m.Run()
return nil
}
func testContext(t testing.TB) context.Context {
w := newTestWriter(t)
return cfg.WithBuildXWriter(context.Background(), w)
}
// A testWriter is an io.Writer that writes to a test's log.
//
// The writer batches written data until the last byte of a write is a newline
// character, then flushes the batched data as a single call to Logf.
// Any remaining unflushed data is logged during Cleanup.
type testWriter struct {
t testing.TB
mu sync.Mutex
buf bytes.Buffer
}
func newTestWriter(t testing.TB) *testWriter {
w := &testWriter{t: t}
t.Cleanup(func() {
w.mu.Lock()
defer w.mu.Unlock()
if b := w.buf.Bytes(); len(b) > 0 {
w.t.Logf("%s", b)
w.buf.Reset()
}
})
return w
}
func (w *testWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
n, err := w.buf.Write(p)
if b := w.buf.Bytes(); len(b) > 0 && b[len(b)-1] == '\n' {
w.t.Logf("%s", b)
w.buf.Reset()
}
return n, err
}
func testRepo(ctx context.Context, t *testing.T, remote string) (Repo, error) {
if remote == "localGitRepo" {
return LocalGitRepo(ctx, localGitURL(t))
}
vcsName := "git"
for _, k := range []string{"hg"} {
if strings.Contains(remote, "/"+k+"/") {
vcsName = k
}
}
if testing.Short() && vcsName == "hg" {
t.Skipf("skipping hg test in short mode: hg is slow")
}
testenv.MustHaveExecPath(t, vcsName)
if runtime.GOOS == "android" && strings.HasSuffix(testenv.Builder(), "-corellium") {
testenv.SkipFlaky(t, 59940)
}
return NewRepo(ctx, vcsName, remote)
}
func TestTags(t *testing.T) {
t.Parallel()
type tagsTest struct {
repo string
prefix string
tags []Tag
}
runTest := func(tt tagsTest) func(*testing.T) {
return func(t *testing.T) {
t.Parallel()
ctx := testContext(t)
r, err := testRepo(ctx, t, tt.repo)
if err != nil {
t.Fatal(err)
}
tags, err := r.Tags(ctx, tt.prefix)
if err != nil {
t.Fatal(err)
}
if tags == nil || !reflect.DeepEqual(tags.List, tt.tags) {
t.Errorf("Tags(%q): incorrect tags\nhave %v\nwant %v", tt.prefix, tags, tt.tags)
}
}
}
for _, tt := range []tagsTest{
{gitrepo1, "xxx", []Tag{}},
{gitrepo1, "", []Tag{
{"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"},
{"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"},
{"v2.0.1", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"},
{"v2.0.2", "9d02800338b8a55be062c838d1f02e0c5780b9eb"},
{"v2.3", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"},
}},
{gitrepo1, "v", []Tag{
{"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"},
{"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"},
{"v2.0.1", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"},
{"v2.0.2", "9d02800338b8a55be062c838d1f02e0c5780b9eb"},
{"v2.3", "76a00fb249b7f93091bc2c89a789dab1fc1bc26f"},
}},
{gitrepo1, "v1", []Tag{
{"v1.2.3", "ede458df7cd0fdca520df19a33158086a8a68e81"},
{"v1.2.4-annotated", "ede458df7cd0fdca520df19a33158086a8a68e81"},
}},
{gitrepo1, "2", []Tag{}},
} {
t.Run(path.Base(tt.repo)+"/"+tt.prefix, runTest(tt))
if tt.repo == gitrepo1 {
// Clear hashes.
clearTags := []Tag{}
for _, tag := range tt.tags {
clearTags = append(clearTags, Tag{tag.Name, ""})
}
tags := tt.tags
for _, tt.repo = range altRepos() {
if strings.Contains(tt.repo, "Git") {
tt.tags = tags
} else {
tt.tags = clearTags
}
t.Run(path.Base(tt.repo)+"/"+tt.prefix, runTest(tt))
}
}
}
}
func TestLatest(t *testing.T) {
t.Parallel()
type latestTest struct {
repo string
info *RevInfo
}
runTest := func(tt latestTest) func(*testing.T) {
return func(t *testing.T) {
t.Parallel()
ctx := testContext(t)
r, err := testRepo(ctx, t, tt.repo)
if err != nil {
t.Fatal(err)
}
info, err := r.Latest(ctx)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(info, tt.info) {
t.Errorf("Latest: incorrect info\nhave %+v (origin %+v)\nwant %+v (origin %+v)", info, info.Origin, tt.info, tt.info.Origin)
}
}
}
for _, tt := range []latestTest{
{
gitrepo1,
&RevInfo{
Origin: &Origin{
VCS: "git",
URL: gitrepo1,
Ref: "HEAD",
Hash: "ede458df7cd0fdca520df19a33158086a8a68e81",
},
Name: "ede458df7cd0fdca520df19a33158086a8a68e81",
Short: "ede458df7cd0",
Version: "ede458df7cd0fdca520df19a33158086a8a68e81",
Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
Tags: []string{"v1.2.3", "v1.2.4-annotated"},
},
},
{
hgrepo1,
&RevInfo{
Origin: &Origin{
VCS: "hg",
URL: hgrepo1,
Hash: "18518c07eb8ed5c80221e997e518cccaa8c0c287",
},
Name: "18518c07eb8ed5c80221e997e518cccaa8c0c287",
Short: "18518c07eb8e",
Version: "18518c07eb8ed5c80221e997e518cccaa8c0c287",
Time: time.Date(2018, 6, 27, 16, 16, 30, 0, time.UTC),
},
},
} {
t.Run(path.Base(tt.repo), runTest(tt))
if tt.repo == gitrepo1 {
tt.repo = "localGitRepo"
info := *tt.info
tt.info = &info
o := *info.Origin
info.Origin = &o
o.URL = localGitURL(t)
t.Run(path.Base(tt.repo), runTest(tt))
}
}
}
func TestReadFile(t *testing.T) {
t.Parallel()
type readFileTest struct {
repo string
rev string
file string
err string
data string
}
runTest := func(tt readFileTest) func(*testing.T) {
return func(t *testing.T) {
t.Parallel()
ctx := testContext(t)
r, err := testRepo(ctx, t, tt.repo)
if err != nil {
t.Fatal(err)
}
data, err := r.ReadFile(ctx, tt.rev, tt.file, 100)
if err != nil {
if tt.err == "" {
t.Fatalf("ReadFile: unexpected error %v", err)
}
if !strings.Contains(err.Error(), tt.err) {
t.Fatalf("ReadFile: wrong error %q, want %q", err, tt.err)
}
if len(data) != 0 {
t.Errorf("ReadFile: non-empty data %q with error %v", data, err)
}
return
}
if tt.err != "" {
t.Fatalf("ReadFile: no error, wanted %v", tt.err)
}
if string(data) != tt.data {
t.Errorf("ReadFile: incorrect data\nhave %q\nwant %q", data, tt.data)
}
}
}
for _, tt := range []readFileTest{
{
repo: gitrepo1,
rev: "latest",
file: "README",
data: "",
},
{
repo: gitrepo1,
rev: "v2",
file: "another.txt",
data: "another\n",
},
{
repo: gitrepo1,
rev: "v2.3.4",
file: "another.txt",
err: fs.ErrNotExist.Error(),
},
} {
t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.file, runTest(tt))
if tt.repo == gitrepo1 {
for _, tt.repo = range altRepos() {
t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.file, runTest(tt))
}
}
}
}
type zipFile struct {
name string
size int64
}
func TestReadZip(t *testing.T) {
t.Parallel()
type readZipTest struct {
repo string
rev string
subdir string
err string
files map[string]uint64
}
runTest := func(tt readZipTest) func(*testing.T) {
return func(t *testing.T) {
t.Parallel()
ctx := testContext(t)
r, err := testRepo(ctx, t, tt.repo)
if err != nil {
t.Fatal(err)
}
rc, err := r.ReadZip(ctx, tt.rev, tt.subdir, 100000)
if err != nil {
if tt.err == "" {
t.Fatalf("ReadZip: unexpected error %v", err)
}
if !strings.Contains(err.Error(), tt.err) {
t.Fatalf("ReadZip: wrong error %q, want %q", err, tt.err)
}
if rc != nil {
t.Errorf("ReadZip: non-nil io.ReadCloser with error %v", err)
}
return
}
defer rc.Close()
if tt.err != "" {
t.Fatalf("ReadZip: no error, wanted %v", tt.err)
}
zipdata, err := io.ReadAll(rc)
if err != nil {
t.Fatal(err)
}
z, err := zip.NewReader(bytes.NewReader(zipdata), int64(len(zipdata)))
if err != nil {
t.Fatalf("ReadZip: cannot read zip file: %v", err)
}
have := make(map[string]bool)
for _, f := range z.File {
size, ok := tt.files[f.Name]
if !ok {
t.Errorf("ReadZip: unexpected file %s", f.Name)
continue
}
have[f.Name] = true
if size != ^uint64(0) && f.UncompressedSize64 != size {
t.Errorf("ReadZip: file %s has unexpected size %d != %d", f.Name, f.UncompressedSize64, size)
}
}
for name := range tt.files {
if !have[name] {
t.Errorf("ReadZip: missing file %s", name)
}
}
}
}
for _, tt := range []readZipTest{
{
repo: gitrepo1,
rev: "v2.3.4",
subdir: "",
files: map[string]uint64{
"prefix/": 0,
"prefix/README": 0,
"prefix/v2": 3,
},
},
{
repo: hgrepo1,
rev: "v2.3.4",
subdir: "",
files: map[string]uint64{
"prefix/.hg_archival.txt": ^uint64(0),
"prefix/README": 0,
"prefix/v2": 3,
},
},
{
repo: gitrepo1,
rev: "v2",
subdir: "",
files: map[string]uint64{
"prefix/": 0,
"prefix/README": 0,
"prefix/v2": 3,
"prefix/another.txt": 8,
"prefix/foo.txt": 13,
},
},
{
repo: hgrepo1,
rev: "v2",
subdir: "",
files: map[string]uint64{
"prefix/.hg_archival.txt": ^uint64(0),
"prefix/README": 0,
"prefix/v2": 3,
"prefix/another.txt": 8,
"prefix/foo.txt": 13,
},
},
{
repo: gitrepo1,
rev: "v3",
subdir: "",
files: map[string]uint64{
"prefix/": 0,
"prefix/v3/": 0,
"prefix/v3/sub/": 0,
"prefix/v3/sub/dir/": 0,
"prefix/v3/sub/dir/file.txt": 16,
"prefix/README": 0,
},
},
{
repo: hgrepo1,
rev: "v3",
subdir: "",
files: map[string]uint64{
"prefix/.hg_archival.txt": ^uint64(0),
"prefix/.hgtags": 405,
"prefix/v3/sub/dir/file.txt": 16,
"prefix/README": 0,
},
},
{
repo: gitrepo1,
rev: "v3",
subdir: "v3/sub/dir",
files: map[string]uint64{
"prefix/": 0,
"prefix/v3/": 0,
"prefix/v3/sub/": 0,
"prefix/v3/sub/dir/": 0,
"prefix/v3/sub/dir/file.txt": 16,
},
},
{
repo: hgrepo1,
rev: "v3",
subdir: "v3/sub/dir",
files: map[string]uint64{
"prefix/v3/sub/dir/file.txt": 16,
},
},
{
repo: gitrepo1,
rev: "v3",
subdir: "v3/sub",
files: map[string]uint64{
"prefix/": 0,
"prefix/v3/": 0,
"prefix/v3/sub/": 0,
"prefix/v3/sub/dir/": 0,
"prefix/v3/sub/dir/file.txt": 16,
},
},
{
repo: hgrepo1,
rev: "v3",
subdir: "v3/sub",
files: map[string]uint64{
"prefix/v3/sub/dir/file.txt": 16,
},
},
{
repo: gitrepo1,
rev: "aaaaaaaaab",
subdir: "",
err: "unknown revision",
},
{
repo: hgrepo1,
rev: "aaaaaaaaab",
subdir: "",
err: "unknown revision",
},
{
repo: vgotest1,
rev: "submod/v1.0.4",
subdir: "submod",
files: map[string]uint64{
"prefix/": 0,
"prefix/submod/": 0,
"prefix/submod/go.mod": 53,
"prefix/submod/pkg/": 0,
"prefix/submod/pkg/p.go": 31,
},
},
} {
t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.subdir, runTest(tt))
if tt.repo == gitrepo1 {
tt.repo = "localGitRepo"
t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.subdir, runTest(tt))
}
}
}
var hgmap = map[string]string{
"HEAD": "41964ddce1180313bdc01d0a39a2813344d6261d", // not tip due to bad hgrepo1 conversion
"9d02800338b8a55be062c838d1f02e0c5780b9eb": "8f49ee7a6ddcdec6f0112d9dca48d4a2e4c3c09e",
"76a00fb249b7f93091bc2c89a789dab1fc1bc26f": "88fde824ec8b41a76baa16b7e84212cee9f3edd0",
"ede458df7cd0fdca520df19a33158086a8a68e81": "41964ddce1180313bdc01d0a39a2813344d6261d",
"97f6aa59c81c623494825b43d39e445566e429a4": "c0cbbfb24c7c3c50c35c7b88e7db777da4ff625d",
}
func TestStat(t *testing.T) {
t.Parallel()
type statTest struct {
repo string
rev string
err string
info *RevInfo
}
runTest := func(tt statTest) func(*testing.T) {
return func(t *testing.T) {
t.Parallel()
ctx := testContext(t)
r, err := testRepo(ctx, t, tt.repo)
if err != nil {
t.Fatal(err)
}
info, err := r.Stat(ctx, tt.rev)
if err != nil {
if tt.err == "" {
t.Fatalf("Stat: unexpected error %v", err)
}
if !strings.Contains(err.Error(), tt.err) {
t.Fatalf("Stat: wrong error %q, want %q", err, tt.err)
}
if info != nil && info.Origin == nil {
t.Errorf("Stat: non-nil info with nil Origin with error %q", err)
}
return
}
info.Origin = nil // TestLatest and ../../../testdata/script/reuse_git.txt test Origin well enough
if !reflect.DeepEqual(info, tt.info) {
t.Errorf("Stat: incorrect info\nhave %+v\nwant %+v", *info, *tt.info)
}
}
}
for _, tt := range []statTest{
{
repo: gitrepo1,
rev: "HEAD",
info: &RevInfo{
Name: "ede458df7cd0fdca520df19a33158086a8a68e81",
Short: "ede458df7cd0",
Version: "ede458df7cd0fdca520df19a33158086a8a68e81",
Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
Tags: []string{"v1.2.3", "v1.2.4-annotated"},
},
},
{
repo: gitrepo1,
rev: "v2", // branch
info: &RevInfo{
Name: "9d02800338b8a55be062c838d1f02e0c5780b9eb",
Short: "9d02800338b8",
Version: "9d02800338b8a55be062c838d1f02e0c5780b9eb",
Time: time.Date(2018, 4, 17, 20, 00, 32, 0, time.UTC),
Tags: []string{"v2.0.2"},
},
},
{
repo: gitrepo1,
rev: "v2.3.4", // badly-named branch (semver should be a tag)
info: &RevInfo{
Name: "76a00fb249b7f93091bc2c89a789dab1fc1bc26f",
Short: "76a00fb249b7",
Version: "76a00fb249b7f93091bc2c89a789dab1fc1bc26f",
Time: time.Date(2018, 4, 17, 19, 45, 48, 0, time.UTC),
Tags: []string{"v2.0.1", "v2.3"},
},
},
{
repo: gitrepo1,
rev: "v2.3", // badly-named tag (we only respect full semver v2.3.0)
info: &RevInfo{
Name: "76a00fb249b7f93091bc2c89a789dab1fc1bc26f",
Short: "76a00fb249b7",
Version: "v2.3",
Time: time.Date(2018, 4, 17, 19, 45, 48, 0, time.UTC),
Tags: []string{"v2.0.1", "v2.3"},
},
},
{
repo: gitrepo1,
rev: "v1.2.3", // tag
info: &RevInfo{
Name: "ede458df7cd0fdca520df19a33158086a8a68e81",
Short: "ede458df7cd0",
Version: "v1.2.3",
Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
Tags: []string{"v1.2.3", "v1.2.4-annotated"},
},
},
{
repo: gitrepo1,
rev: "ede458df", // hash prefix in refs
info: &RevInfo{
Name: "ede458df7cd0fdca520df19a33158086a8a68e81",
Short: "ede458df7cd0",
Version: "ede458df7cd0fdca520df19a33158086a8a68e81",
Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
Tags: []string{"v1.2.3", "v1.2.4-annotated"},
},
},
{
repo: gitrepo1,
rev: "97f6aa59", // hash prefix not in refs
info: &RevInfo{
Name: "97f6aa59c81c623494825b43d39e445566e429a4",
Short: "97f6aa59c81c",
Version: "97f6aa59c81c623494825b43d39e445566e429a4",
Time: time.Date(2018, 4, 17, 20, 0, 19, 0, time.UTC),
},
},
{
repo: gitrepo1,
rev: "v1.2.4-annotated", // annotated tag uses unwrapped commit hash
info: &RevInfo{
Name: "ede458df7cd0fdca520df19a33158086a8a68e81",
Short: "ede458df7cd0",
Version: "v1.2.4-annotated",
Time: time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
Tags: []string{"v1.2.3", "v1.2.4-annotated"},
},
},
{
repo: gitrepo1,
rev: "aaaaaaaaab",
err: "unknown revision",
},
} {
t.Run(path.Base(tt.repo)+"/"+tt.rev, runTest(tt))
if tt.repo == gitrepo1 {
for _, tt.repo = range altRepos() {
old := tt
var m map[string]string
if tt.repo == hgrepo1 {
m = hgmap
}
if tt.info != nil {
info := *tt.info
tt.info = &info
tt.info.Name = remap(tt.info.Name, m)
tt.info.Version = remap(tt.info.Version, m)
tt.info.Short = remap(tt.info.Short, m)
}
tt.rev = remap(tt.rev, m)
t.Run(path.Base(tt.repo)+"/"+tt.rev, runTest(tt))
tt = old
}
}
}
}
func remap(name string, m map[string]string) string {
if m[name] != "" {
return m[name]
}
if AllHex(name) {
for k, v := range m {
if strings.HasPrefix(k, name) {
return v[:len(name)]
}
}
}
return name
}