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