1// Copyright 2018 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
5package codehost
6
7import (
8	"archive/zip"
9	"bytes"
10	"flag"
11	"fmt"
12	"internal/testenv"
13	"io/ioutil"
14	"log"
15	"os"
16	"os/exec"
17	"path"
18	"path/filepath"
19	"reflect"
20	"strings"
21	"testing"
22	"time"
23)
24
25func TestMain(m *testing.M) {
26	// needed for initializing the test environment variables as testing.Short
27	// and HasExternalNetwork
28	flag.Parse()
29	os.Exit(testMain(m))
30}
31
32const (
33	gitrepo1 = "https://vcs-test.golang.org/git/gitrepo1"
34	hgrepo1  = "https://vcs-test.golang.org/hg/hgrepo1"
35)
36
37var altRepos = []string{
38	"localGitRepo",
39	hgrepo1,
40}
41
42// TODO: Convert gitrepo1 to svn, bzr, fossil and add tests.
43// For now, at least the hgrepo1 tests check the general vcs.go logic.
44
45// localGitRepo is like gitrepo1 but allows archive access.
46var localGitRepo string
47
48func testMain(m *testing.M) int {
49	if _, err := exec.LookPath("git"); err != nil {
50		fmt.Fprintln(os.Stderr, "skipping because git binary not found")
51		fmt.Println("PASS")
52		return 0
53	}
54
55	dir, err := ioutil.TempDir("", "gitrepo-test-")
56	if err != nil {
57		log.Fatal(err)
58	}
59	defer os.RemoveAll(dir)
60	WorkRoot = dir
61
62	if testenv.HasExternalNetwork() && testenv.HasExec() {
63		// Clone gitrepo1 into a local directory.
64		// If we use a file:// URL to access the local directory,
65		// then git starts up all the usual protocol machinery,
66		// which will let us test remote git archive invocations.
67		localGitRepo = filepath.Join(dir, "gitrepo2")
68		if _, err := Run("", "git", "clone", "--mirror", gitrepo1, localGitRepo); err != nil {
69			log.Fatal(err)
70		}
71		if _, err := Run(localGitRepo, "git", "config", "daemon.uploadarch", "true"); err != nil {
72			log.Fatal(err)
73		}
74	}
75
76	return m.Run()
77}
78
79func testRepo(remote string) (Repo, error) {
80	if remote == "localGitRepo" {
81		return LocalGitRepo(filepath.ToSlash(localGitRepo))
82	}
83	kind := "git"
84	for _, k := range []string{"hg"} {
85		if strings.Contains(remote, "/"+k+"/") {
86			kind = k
87		}
88	}
89	return NewRepo(kind, remote)
90}
91
92var tagsTests = []struct {
93	repo   string
94	prefix string
95	tags   []string
96}{
97	{gitrepo1, "xxx", []string{}},
98	{gitrepo1, "", []string{"v1.2.3", "v1.2.4-annotated", "v2.0.1", "v2.0.2", "v2.3"}},
99	{gitrepo1, "v", []string{"v1.2.3", "v1.2.4-annotated", "v2.0.1", "v2.0.2", "v2.3"}},
100	{gitrepo1, "v1", []string{"v1.2.3", "v1.2.4-annotated"}},
101	{gitrepo1, "2", []string{}},
102}
103
104func TestTags(t *testing.T) {
105	testenv.MustHaveExternalNetwork(t)
106	testenv.MustHaveExec(t)
107
108	for _, tt := range tagsTests {
109		f := func(t *testing.T) {
110			r, err := testRepo(tt.repo)
111			if err != nil {
112				t.Fatal(err)
113			}
114			tags, err := r.Tags(tt.prefix)
115			if err != nil {
116				t.Fatal(err)
117			}
118			if !reflect.DeepEqual(tags, tt.tags) {
119				t.Errorf("Tags: incorrect tags\nhave %v\nwant %v", tags, tt.tags)
120			}
121		}
122		t.Run(path.Base(tt.repo)+"/"+tt.prefix, f)
123		if tt.repo == gitrepo1 {
124			for _, tt.repo = range altRepos {
125				t.Run(path.Base(tt.repo)+"/"+tt.prefix, f)
126			}
127		}
128	}
129}
130
131var latestTests = []struct {
132	repo string
133	info *RevInfo
134}{
135	{
136		gitrepo1,
137		&RevInfo{
138			Name:    "ede458df7cd0fdca520df19a33158086a8a68e81",
139			Short:   "ede458df7cd0",
140			Version: "ede458df7cd0fdca520df19a33158086a8a68e81",
141			Time:    time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
142			Tags:    []string{"v1.2.3", "v1.2.4-annotated"},
143		},
144	},
145	{
146		hgrepo1,
147		&RevInfo{
148			Name:    "18518c07eb8ed5c80221e997e518cccaa8c0c287",
149			Short:   "18518c07eb8e",
150			Version: "18518c07eb8ed5c80221e997e518cccaa8c0c287",
151			Time:    time.Date(2018, 6, 27, 16, 16, 30, 0, time.UTC),
152		},
153	},
154}
155
156func TestLatest(t *testing.T) {
157	testenv.MustHaveExternalNetwork(t)
158	testenv.MustHaveExec(t)
159
160	for _, tt := range latestTests {
161		f := func(t *testing.T) {
162			r, err := testRepo(tt.repo)
163			if err != nil {
164				t.Fatal(err)
165			}
166			info, err := r.Latest()
167			if err != nil {
168				t.Fatal(err)
169			}
170			if !reflect.DeepEqual(info, tt.info) {
171				t.Errorf("Latest: incorrect info\nhave %+v\nwant %+v", *info, *tt.info)
172			}
173		}
174		t.Run(path.Base(tt.repo), f)
175		if tt.repo == gitrepo1 {
176			tt.repo = "localGitRepo"
177			t.Run(path.Base(tt.repo), f)
178		}
179	}
180}
181
182var readFileTests = []struct {
183	repo string
184	rev  string
185	file string
186	err  string
187	data string
188}{
189	{
190		repo: gitrepo1,
191		rev:  "latest",
192		file: "README",
193		data: "",
194	},
195	{
196		repo: gitrepo1,
197		rev:  "v2",
198		file: "another.txt",
199		data: "another\n",
200	},
201	{
202		repo: gitrepo1,
203		rev:  "v2.3.4",
204		file: "another.txt",
205		err:  os.ErrNotExist.Error(),
206	},
207}
208
209func TestReadFile(t *testing.T) {
210	testenv.MustHaveExternalNetwork(t)
211	testenv.MustHaveExec(t)
212
213	for _, tt := range readFileTests {
214		f := func(t *testing.T) {
215			r, err := testRepo(tt.repo)
216			if err != nil {
217				t.Fatal(err)
218			}
219			data, err := r.ReadFile(tt.rev, tt.file, 100)
220			if err != nil {
221				if tt.err == "" {
222					t.Fatalf("ReadFile: unexpected error %v", err)
223				}
224				if !strings.Contains(err.Error(), tt.err) {
225					t.Fatalf("ReadFile: wrong error %q, want %q", err, tt.err)
226				}
227				if len(data) != 0 {
228					t.Errorf("ReadFile: non-empty data %q with error %v", data, err)
229				}
230				return
231			}
232			if tt.err != "" {
233				t.Fatalf("ReadFile: no error, wanted %v", tt.err)
234			}
235			if string(data) != tt.data {
236				t.Errorf("ReadFile: incorrect data\nhave %q\nwant %q", data, tt.data)
237			}
238		}
239		t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.file, f)
240		if tt.repo == gitrepo1 {
241			for _, tt.repo = range altRepos {
242				t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.file, f)
243			}
244		}
245	}
246}
247
248var readZipTests = []struct {
249	repo         string
250	rev          string
251	subdir       string
252	actualSubdir string
253	err          string
254	files        map[string]uint64
255}{
256	{
257		repo:   gitrepo1,
258		rev:    "v2.3.4",
259		subdir: "",
260		files: map[string]uint64{
261			"prefix/":       0,
262			"prefix/README": 0,
263			"prefix/v2":     3,
264		},
265	},
266	{
267		repo:   hgrepo1,
268		rev:    "v2.3.4",
269		subdir: "",
270		files: map[string]uint64{
271			"prefix/.hg_archival.txt": ^uint64(0),
272			"prefix/README":           0,
273			"prefix/v2":               3,
274		},
275	},
276
277	{
278		repo:   gitrepo1,
279		rev:    "v2",
280		subdir: "",
281		files: map[string]uint64{
282			"prefix/":            0,
283			"prefix/README":      0,
284			"prefix/v2":          3,
285			"prefix/another.txt": 8,
286			"prefix/foo.txt":     13,
287		},
288	},
289	{
290		repo:   hgrepo1,
291		rev:    "v2",
292		subdir: "",
293		files: map[string]uint64{
294			"prefix/.hg_archival.txt": ^uint64(0),
295			"prefix/README":           0,
296			"prefix/v2":               3,
297			"prefix/another.txt":      8,
298			"prefix/foo.txt":          13,
299		},
300	},
301
302	{
303		repo:   gitrepo1,
304		rev:    "v3",
305		subdir: "",
306		files: map[string]uint64{
307			"prefix/":                    0,
308			"prefix/v3/":                 0,
309			"prefix/v3/sub/":             0,
310			"prefix/v3/sub/dir/":         0,
311			"prefix/v3/sub/dir/file.txt": 16,
312			"prefix/README":              0,
313		},
314	},
315	{
316		repo:   hgrepo1,
317		rev:    "v3",
318		subdir: "",
319		files: map[string]uint64{
320			"prefix/.hg_archival.txt":    ^uint64(0),
321			"prefix/.hgtags":             405,
322			"prefix/v3/sub/dir/file.txt": 16,
323			"prefix/README":              0,
324		},
325	},
326
327	{
328		repo:   gitrepo1,
329		rev:    "v3",
330		subdir: "v3/sub/dir",
331		files: map[string]uint64{
332			"prefix/":                    0,
333			"prefix/v3/":                 0,
334			"prefix/v3/sub/":             0,
335			"prefix/v3/sub/dir/":         0,
336			"prefix/v3/sub/dir/file.txt": 16,
337		},
338	},
339	{
340		repo:   hgrepo1,
341		rev:    "v3",
342		subdir: "v3/sub/dir",
343		files: map[string]uint64{
344			"prefix/v3/sub/dir/file.txt": 16,
345		},
346	},
347
348	{
349		repo:   gitrepo1,
350		rev:    "v3",
351		subdir: "v3/sub",
352		files: map[string]uint64{
353			"prefix/":                    0,
354			"prefix/v3/":                 0,
355			"prefix/v3/sub/":             0,
356			"prefix/v3/sub/dir/":         0,
357			"prefix/v3/sub/dir/file.txt": 16,
358		},
359	},
360	{
361		repo:   hgrepo1,
362		rev:    "v3",
363		subdir: "v3/sub",
364		files: map[string]uint64{
365			"prefix/v3/sub/dir/file.txt": 16,
366		},
367	},
368
369	{
370		repo:   gitrepo1,
371		rev:    "aaaaaaaaab",
372		subdir: "",
373		err:    "unknown revision",
374	},
375	{
376		repo:   hgrepo1,
377		rev:    "aaaaaaaaab",
378		subdir: "",
379		err:    "unknown revision",
380	},
381
382	{
383		repo:   "https://github.com/rsc/vgotest1",
384		rev:    "submod/v1.0.4",
385		subdir: "submod",
386		files: map[string]uint64{
387			"prefix/":                0,
388			"prefix/submod/":         0,
389			"prefix/submod/go.mod":   53,
390			"prefix/submod/pkg/":     0,
391			"prefix/submod/pkg/p.go": 31,
392		},
393	},
394}
395
396type zipFile struct {
397	name string
398	size int64
399}
400
401func TestReadZip(t *testing.T) {
402	testenv.MustHaveExternalNetwork(t)
403	testenv.MustHaveExec(t)
404
405	for _, tt := range readZipTests {
406		f := func(t *testing.T) {
407			r, err := testRepo(tt.repo)
408			if err != nil {
409				t.Fatal(err)
410			}
411			rc, actualSubdir, err := r.ReadZip(tt.rev, tt.subdir, 100000)
412			if err != nil {
413				if tt.err == "" {
414					t.Fatalf("ReadZip: unexpected error %v", err)
415				}
416				if !strings.Contains(err.Error(), tt.err) {
417					t.Fatalf("ReadZip: wrong error %q, want %q", err, tt.err)
418				}
419				if rc != nil {
420					t.Errorf("ReadZip: non-nil io.ReadCloser with error %v", err)
421				}
422				return
423			}
424			defer rc.Close()
425			if tt.err != "" {
426				t.Fatalf("ReadZip: no error, wanted %v", tt.err)
427			}
428			if actualSubdir != tt.actualSubdir {
429				t.Fatalf("ReadZip: actualSubdir = %q, want %q", actualSubdir, tt.actualSubdir)
430			}
431			zipdata, err := ioutil.ReadAll(rc)
432			if err != nil {
433				t.Fatal(err)
434			}
435			z, err := zip.NewReader(bytes.NewReader(zipdata), int64(len(zipdata)))
436			if err != nil {
437				t.Fatalf("ReadZip: cannot read zip file: %v", err)
438			}
439			have := make(map[string]bool)
440			for _, f := range z.File {
441				size, ok := tt.files[f.Name]
442				if !ok {
443					t.Errorf("ReadZip: unexpected file %s", f.Name)
444					continue
445				}
446				have[f.Name] = true
447				if size != ^uint64(0) && f.UncompressedSize64 != size {
448					t.Errorf("ReadZip: file %s has unexpected size %d != %d", f.Name, f.UncompressedSize64, size)
449				}
450			}
451			for name := range tt.files {
452				if !have[name] {
453					t.Errorf("ReadZip: missing file %s", name)
454				}
455			}
456		}
457		t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.subdir, f)
458		if tt.repo == gitrepo1 {
459			tt.repo = "localGitRepo"
460			t.Run(path.Base(tt.repo)+"/"+tt.rev+"/"+tt.subdir, f)
461		}
462	}
463}
464
465var hgmap = map[string]string{
466	"HEAD": "41964ddce1180313bdc01d0a39a2813344d6261d", // not tip due to bad hgrepo1 conversion
467	"9d02800338b8a55be062c838d1f02e0c5780b9eb": "8f49ee7a6ddcdec6f0112d9dca48d4a2e4c3c09e",
468	"76a00fb249b7f93091bc2c89a789dab1fc1bc26f": "88fde824ec8b41a76baa16b7e84212cee9f3edd0",
469	"ede458df7cd0fdca520df19a33158086a8a68e81": "41964ddce1180313bdc01d0a39a2813344d6261d",
470	"97f6aa59c81c623494825b43d39e445566e429a4": "c0cbbfb24c7c3c50c35c7b88e7db777da4ff625d",
471}
472
473var statTests = []struct {
474	repo string
475	rev  string
476	err  string
477	info *RevInfo
478}{
479	{
480		repo: gitrepo1,
481		rev:  "HEAD",
482		info: &RevInfo{
483			Name:    "ede458df7cd0fdca520df19a33158086a8a68e81",
484			Short:   "ede458df7cd0",
485			Version: "ede458df7cd0fdca520df19a33158086a8a68e81",
486			Time:    time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
487			Tags:    []string{"v1.2.3", "v1.2.4-annotated"},
488		},
489	},
490	{
491		repo: gitrepo1,
492		rev:  "v2", // branch
493		info: &RevInfo{
494			Name:    "9d02800338b8a55be062c838d1f02e0c5780b9eb",
495			Short:   "9d02800338b8",
496			Version: "9d02800338b8a55be062c838d1f02e0c5780b9eb",
497			Time:    time.Date(2018, 4, 17, 20, 00, 32, 0, time.UTC),
498			Tags:    []string{"v2.0.2"},
499		},
500	},
501	{
502		repo: gitrepo1,
503		rev:  "v2.3.4", // badly-named branch (semver should be a tag)
504		info: &RevInfo{
505			Name:    "76a00fb249b7f93091bc2c89a789dab1fc1bc26f",
506			Short:   "76a00fb249b7",
507			Version: "76a00fb249b7f93091bc2c89a789dab1fc1bc26f",
508			Time:    time.Date(2018, 4, 17, 19, 45, 48, 0, time.UTC),
509			Tags:    []string{"v2.0.1", "v2.3"},
510		},
511	},
512	{
513		repo: gitrepo1,
514		rev:  "v2.3", // badly-named tag (we only respect full semver v2.3.0)
515		info: &RevInfo{
516			Name:    "76a00fb249b7f93091bc2c89a789dab1fc1bc26f",
517			Short:   "76a00fb249b7",
518			Version: "v2.3",
519			Time:    time.Date(2018, 4, 17, 19, 45, 48, 0, time.UTC),
520			Tags:    []string{"v2.0.1", "v2.3"},
521		},
522	},
523	{
524		repo: gitrepo1,
525		rev:  "v1.2.3", // tag
526		info: &RevInfo{
527			Name:    "ede458df7cd0fdca520df19a33158086a8a68e81",
528			Short:   "ede458df7cd0",
529			Version: "v1.2.3",
530			Time:    time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
531			Tags:    []string{"v1.2.3", "v1.2.4-annotated"},
532		},
533	},
534	{
535		repo: gitrepo1,
536		rev:  "ede458df", // hash prefix in refs
537		info: &RevInfo{
538			Name:    "ede458df7cd0fdca520df19a33158086a8a68e81",
539			Short:   "ede458df7cd0",
540			Version: "ede458df7cd0fdca520df19a33158086a8a68e81",
541			Time:    time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
542			Tags:    []string{"v1.2.3", "v1.2.4-annotated"},
543		},
544	},
545	{
546		repo: gitrepo1,
547		rev:  "97f6aa59", // hash prefix not in refs
548		info: &RevInfo{
549			Name:    "97f6aa59c81c623494825b43d39e445566e429a4",
550			Short:   "97f6aa59c81c",
551			Version: "97f6aa59c81c623494825b43d39e445566e429a4",
552			Time:    time.Date(2018, 4, 17, 20, 0, 19, 0, time.UTC),
553		},
554	},
555	{
556		repo: gitrepo1,
557		rev:  "v1.2.4-annotated", // annotated tag uses unwrapped commit hash
558		info: &RevInfo{
559			Name:    "ede458df7cd0fdca520df19a33158086a8a68e81",
560			Short:   "ede458df7cd0",
561			Version: "v1.2.4-annotated",
562			Time:    time.Date(2018, 4, 17, 19, 43, 22, 0, time.UTC),
563			Tags:    []string{"v1.2.3", "v1.2.4-annotated"},
564		},
565	},
566	{
567		repo: gitrepo1,
568		rev:  "aaaaaaaaab",
569		err:  "unknown revision",
570	},
571}
572
573func TestStat(t *testing.T) {
574	testenv.MustHaveExternalNetwork(t)
575	testenv.MustHaveExec(t)
576
577	for _, tt := range statTests {
578		f := func(t *testing.T) {
579			r, err := testRepo(tt.repo)
580			if err != nil {
581				t.Fatal(err)
582			}
583			info, err := r.Stat(tt.rev)
584			if err != nil {
585				if tt.err == "" {
586					t.Fatalf("Stat: unexpected error %v", err)
587				}
588				if !strings.Contains(err.Error(), tt.err) {
589					t.Fatalf("Stat: wrong error %q, want %q", err, tt.err)
590				}
591				if info != nil {
592					t.Errorf("Stat: non-nil info with error %q", err)
593				}
594				return
595			}
596			if !reflect.DeepEqual(info, tt.info) {
597				t.Errorf("Stat: incorrect info\nhave %+v\nwant %+v", *info, *tt.info)
598			}
599		}
600		t.Run(path.Base(tt.repo)+"/"+tt.rev, f)
601		if tt.repo == gitrepo1 {
602			for _, tt.repo = range altRepos {
603				old := tt
604				var m map[string]string
605				if tt.repo == hgrepo1 {
606					m = hgmap
607				}
608				if tt.info != nil {
609					info := *tt.info
610					tt.info = &info
611					tt.info.Name = remap(tt.info.Name, m)
612					tt.info.Version = remap(tt.info.Version, m)
613					tt.info.Short = remap(tt.info.Short, m)
614				}
615				tt.rev = remap(tt.rev, m)
616				t.Run(path.Base(tt.repo)+"/"+tt.rev, f)
617				tt = old
618			}
619		}
620	}
621}
622
623func remap(name string, m map[string]string) string {
624	if m[name] != "" {
625		return m[name]
626	}
627	if AllHex(name) {
628		for k, v := range m {
629			if strings.HasPrefix(k, name) {
630				return v[:len(name)]
631			}
632		}
633	}
634	return name
635}
636