Skip to content

Commit 0a85688

Browse files
committed
add integration tests
This test verifies that the output from this system is usable by the `go get` tool. It operates by proxying `go get` HTTP requests to a locally running proxy, then checking the URL the VCS cloned from. This isn't a hermetic test: it requires internet access to github.com and bitbucket.org, which I'm not sure how to reasonably fake and provide the same level of confidence. It checks PATH for locally installed copies of go, git, and hg, but those could be bundled in a test environment. I bumped up the version of Go required/tested against to Go 1.8 in order to use subtests. Go 1.8 is now GA on App Engine, so this should not be a problem.
1 parent 5ad859d commit 0a85688

File tree

2 files changed

+313
-1
lines changed

2 files changed

+313
-1
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
sudo: false
22
language: go
3+
script: go test -short -v ./...
34
go:
4-
- 1.6
5+
- 1.8.x
56
- 1.x

integration_test.go

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
// Copyright 2018 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"bytes"
19+
"errors"
20+
"io"
21+
"io/ioutil"
22+
"net"
23+
"net/http"
24+
"net/http/httptest"
25+
"os"
26+
"os/exec"
27+
"path/filepath"
28+
"strings"
29+
"sync"
30+
"testing"
31+
"time"
32+
)
33+
34+
func TestIntegration(t *testing.T) {
35+
if testing.Short() {
36+
t.Skip("Skipping due to -test.short")
37+
}
38+
goExe, err := exec.LookPath("go")
39+
if err != nil {
40+
t.Skipf("Could not find go tool: %v", err)
41+
}
42+
43+
var gitURLArgv []string
44+
var hgURLArgv []string
45+
if gitExe, err := exec.LookPath("git"); err != nil {
46+
t.Logf("Could not find git: %v", err)
47+
} else {
48+
gitURLArgv = []string{gitExe, "remote", "get-url", "origin"}
49+
}
50+
if hgExe, err := exec.LookPath("hg"); err != nil {
51+
t.Logf("Could not find hg: %v", err)
52+
} else {
53+
hgURLArgv = []string{hgExe, "paths", "default"}
54+
}
55+
if gitURLArgv == nil && hgURLArgv == nil {
56+
t.Skip("Could not find any VCS; skipping")
57+
}
58+
tests := []struct {
59+
name string
60+
config string
61+
importPath string
62+
getURLArgv []string
63+
wantURL string
64+
}{
65+
{
66+
name: "GitHub",
67+
config: "host: example.com\n" +
68+
"paths:\n" +
69+
" /portmidi:\n" +
70+
" repo: https://github.com/rakyll/portmidi\n",
71+
importPath: "example.com/portmidi",
72+
getURLArgv: gitURLArgv,
73+
wantURL: "https://github.com/rakyll/portmidi",
74+
},
75+
{
76+
name: "Bitbucket Mercurial",
77+
config: "host: example.com\n" +
78+
"paths:\n" +
79+
" /gopdf:\n" +
80+
" repo: https://bitbucket.org/zombiezen/gopdf\n" +
81+
" vcs: hg\n",
82+
importPath: "example.com/gopdf/pdf",
83+
getURLArgv: hgURLArgv,
84+
wantURL: "https://bitbucket.org/zombiezen/gopdf",
85+
},
86+
{
87+
name: "Bitbucket Git",
88+
config: "host: example.com\n" +
89+
"paths:\n" +
90+
" /cardcpx:\n" +
91+
" repo: https://bitbucket.org/zombiezen/cardcpx.git\n" +
92+
" vcs: git\n",
93+
importPath: "example.com/cardcpx/natsort",
94+
getURLArgv: gitURLArgv,
95+
wantURL: "https://bitbucket.org/zombiezen/cardcpx.git",
96+
},
97+
}
98+
99+
for _, test := range tests {
100+
t.Run(test.name, func(t *testing.T) {
101+
if test.getURLArgv == nil {
102+
t.Skip("VCS tool not installed; skipping")
103+
}
104+
h, err := newHandler([]byte(test.config))
105+
if err != nil {
106+
t.Fatal(err)
107+
}
108+
tempRoot, err := ioutil.TempDir("", "govanityurls_integration")
109+
if err != nil {
110+
t.Fatal(err)
111+
}
112+
defer func() {
113+
if err := os.RemoveAll(tempRoot); err != nil {
114+
t.Error(err)
115+
}
116+
}()
117+
tempDir := filepath.Join(tempRoot, "tmp")
118+
if err := os.Mkdir(tempDir, 0777); err != nil {
119+
t.Fatal(err)
120+
}
121+
cacheDir := filepath.Join(tempRoot, "cache")
122+
if err := os.Mkdir(cacheDir, 0777); err != nil {
123+
t.Fatal(err)
124+
}
125+
gopathDir := filepath.Join(tempRoot, "gopath")
126+
if err := os.Mkdir(gopathDir, 0777); err != nil {
127+
t.Fatal(err)
128+
}
129+
srv := httptest.NewServer(&proxy{
130+
rt: http.DefaultTransport,
131+
host: "example.com",
132+
handler: h,
133+
})
134+
defer srv.Close()
135+
goCmd := exec.Command(goExe, "get", "-insecure", "-d", test.importPath)
136+
goCmd.Env = appendEnv(os.Environ(),
137+
"GOPATH="+gopathDir,
138+
"HTTP_PROXY="+srv.URL,
139+
"TMPDIR="+tempDir,
140+
// Go 1.10+ environment variables:
141+
"GOCACHE="+cacheDir,
142+
"GOTMPDIR="+tempDir)
143+
getOutput, err := goCmd.CombinedOutput()
144+
if err != nil {
145+
t.Fatalf("go get failed. log:\n%s", getOutput)
146+
}
147+
148+
vcsStderr := new(bytes.Buffer)
149+
vcsStderrLimiter := &limitedWriter{w: vcsStderr, n: 2048}
150+
vcsCmd := exec.Cmd{
151+
Path: test.getURLArgv[0],
152+
Args: test.getURLArgv,
153+
Env: appendEnv(os.Environ(),
154+
"HOME="+tempRoot,
155+
"TMPDIR="+tempDir,
156+
"XDG_CONFIG_HOME="+filepath.Join(tempRoot, ".config")), // intentionally does not exist
157+
Dir: filepath.Join(gopathDir, "src", filepath.FromSlash(test.importPath)),
158+
Stderr: vcsStderrLimiter,
159+
}
160+
got, err := vcsCmd.Output()
161+
if err != nil {
162+
format := "%s failed. log:\n%s"
163+
if vcsStderrLimiter.truncated {
164+
format += "<truncated>"
165+
}
166+
t.Fatalf(format, strings.Join(test.getURLArgv, " "), vcsStderr.Bytes())
167+
}
168+
if want := []byte(test.wantURL + "\n"); !bytes.Equal(got, want) {
169+
t.Errorf("%s = %q; want %q", strings.Join(test.getURLArgv, " "), got, want)
170+
}
171+
})
172+
}
173+
}
174+
175+
// proxy is an HTTP proxy that forwards requests to a certain host to
176+
// another handler.
177+
type proxy struct {
178+
rt http.RoundTripper
179+
180+
host string
181+
handler http.Handler
182+
}
183+
184+
func (p *proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
185+
host, _, err := net.SplitHostPort(r.Host)
186+
if err != nil {
187+
// Just "host", without port.
188+
host = r.URL.Host
189+
}
190+
switch r.Method {
191+
case http.MethodGet, http.MethodHead:
192+
if host == p.host {
193+
p.handler.ServeHTTP(w, r)
194+
return
195+
}
196+
res, err := p.rt.RoundTrip(r)
197+
if err != nil {
198+
http.Error(w, err.Error(), http.StatusBadGateway)
199+
return
200+
}
201+
defer res.Body.Close()
202+
for k, v := range res.Header {
203+
w.Header()[k] = append([]string(nil), v...)
204+
}
205+
w.WriteHeader(res.StatusCode)
206+
io.Copy(w, res.Body)
207+
case http.MethodConnect:
208+
if host == p.host {
209+
w.Header().Set("Allow", "GET, HEAD")
210+
http.Error(w, "test proxy only allows GET and HEAD to "+r.Host, http.StatusMethodNotAllowed)
211+
return
212+
}
213+
p.connect(w, r)
214+
default:
215+
w.Header().Set("Allow", "GET, HEAD, CONNECT")
216+
http.Error(w, "test proxy only allows GET, HEAD, and CONNECT", http.StatusMethodNotAllowed)
217+
}
218+
}
219+
220+
func (p *proxy) connect(w http.ResponseWriter, r *http.Request) {
221+
c1, err := new(net.Dialer).DialContext(r.Context(), "tcp", r.Host)
222+
if err != nil {
223+
http.Error(w, err.Error(), http.StatusServiceUnavailable)
224+
return
225+
}
226+
w.WriteHeader(http.StatusOK)
227+
hi, ok := w.(http.Hijacker)
228+
if !ok {
229+
http.Error(w, "could convert HTTP connection to TCP", http.StatusInternalServerError)
230+
return
231+
}
232+
c2, brw, err := hi.Hijack()
233+
if err != nil {
234+
http.Error(w, err.Error(), http.StatusInternalServerError)
235+
return
236+
}
237+
c2.SetDeadline(time.Time{})
238+
var wg sync.WaitGroup
239+
wg.Add(2)
240+
go func() {
241+
defer wg.Done()
242+
buf, _ := brw.Reader.Peek(brw.Reader.Buffered())
243+
c1.Write(buf)
244+
io.Copy(c1, c2)
245+
}()
246+
go func() {
247+
defer wg.Done()
248+
io.Copy(c2, c1)
249+
}()
250+
wg.Wait()
251+
c1.Close()
252+
c2.Close()
253+
}
254+
255+
// appendEnv returns a new environment variable list that overrides
256+
// environment variables from base.
257+
//
258+
// Before Go 1.9, os/exec.Cmd.Env would not be deduplicated.
259+
func appendEnv(base []string, add ...string) []string {
260+
out := make([]string, 0, len(base)+len(add))
261+
saw := make(map[string]int)
262+
insert := func(kv string) {
263+
eq := strings.IndexByte(kv, '=')
264+
if eq < 0 {
265+
out = append(out, kv)
266+
return
267+
}
268+
k := kv[:eq]
269+
if dupIdx, isDup := saw[k]; isDup {
270+
out[dupIdx] = kv
271+
return
272+
}
273+
saw[k] = len(out)
274+
out = append(out, kv)
275+
}
276+
for _, kv := range base {
277+
insert(kv)
278+
}
279+
for _, kv := range add {
280+
insert(kv)
281+
}
282+
return out
283+
}
284+
285+
type limitedWriter struct {
286+
w io.Writer
287+
n int64
288+
truncated bool
289+
}
290+
291+
func (l *limitedWriter) Write(p []byte) (n int, err error) {
292+
if len(p) == 0 {
293+
return 0, nil
294+
}
295+
if l.n <= 0 {
296+
l.truncated = true
297+
return 0, errors.New("reached write limit")
298+
}
299+
if int64(len(p)) > l.n {
300+
l.truncated = true
301+
n, err = l.w.Write(p[:l.n])
302+
l.n -= int64(n)
303+
if err != nil {
304+
return n, err
305+
}
306+
return n, errors.New("reached write limit")
307+
}
308+
n, err = l.w.Write(p)
309+
l.n -= int64(n)
310+
return n, err
311+
}

0 commit comments

Comments
 (0)