| // Copyright 2014 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 main |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "io/ioutil" |
| "net" |
| "net/http" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "reflect" |
| "strings" |
| "sync" |
| "testing" |
| ) |
| |
| var gitversion = "unknown git version" // git version for error logs |
| |
| type gitTest struct { |
| pwd string // current directory before test |
| tmpdir string // temporary directory holding repos |
| server string // server repo root |
| client string // client repo root |
| nwork int // number of calls to work method |
| nworkServer int // number of calls to serverWork method |
| nworkOther int // number of calls to serverWorkUnrelated method |
| } |
| |
| // resetReadOnlyFlagAll resets windows read-only flag |
| // set on path and any children it contains. |
| // The flag is set by git and has to be removed. |
| // os.Remove refuses to remove files with read-only flag set. |
| func resetReadOnlyFlagAll(path string) error { |
| fi, err := os.Stat(path) |
| if err != nil { |
| return err |
| } |
| |
| if !fi.IsDir() { |
| return os.Chmod(path, 0666) |
| } |
| |
| fd, err := os.Open(path) |
| if err != nil { |
| return err |
| } |
| defer fd.Close() |
| |
| names, _ := fd.Readdirnames(-1) |
| for _, name := range names { |
| resetReadOnlyFlagAll(path + string(filepath.Separator) + name) |
| } |
| return nil |
| } |
| |
| func (gt *gitTest) done() { |
| os.Chdir(gt.pwd) // change out of gt.tmpdir first, otherwise following os.RemoveAll fails on windows |
| resetReadOnlyFlagAll(gt.tmpdir) |
| os.RemoveAll(gt.tmpdir) |
| cachedConfig = nil |
| } |
| |
| // doWork simulates commit 'n' touching 'file' in 'dir' |
| func doWork(t *testing.T, n int, dir, file, changeid string) { |
| write(t, dir+"/"+file, fmt.Sprintf("new content %d", n)) |
| trun(t, dir, "git", "add", file) |
| suffix := "" |
| if n > 1 { |
| suffix = fmt.Sprintf(" #%d", n) |
| } |
| msg := fmt.Sprintf("msg%s\n\nChange-Id: I%d%s\n", suffix, n, changeid) |
| trun(t, dir, "git", "commit", "-m", msg) |
| } |
| |
| func (gt *gitTest) work(t *testing.T) { |
| if gt.nwork == 0 { |
| trun(t, gt.client, "git", "checkout", "-b", "work") |
| trun(t, gt.client, "git", "branch", "--set-upstream-to", "origin/master") |
| trun(t, gt.client, "git", "tag", "work") // make sure commands do the right thing when there is a tag of the same name |
| } |
| |
| // make local change on client |
| gt.nwork++ |
| doWork(t, gt.nwork, gt.client, "file", "23456789") |
| } |
| |
| func (gt *gitTest) workFile(t *testing.T, file string) { |
| // make local change on client in the specific file |
| gt.nwork++ |
| doWork(t, gt.nwork, gt.client, file, "23456789") |
| } |
| |
| func (gt *gitTest) serverWork(t *testing.T) { |
| // make change on server |
| // duplicating the sequence of changes in gt.work to simulate them |
| // having gone through Gerrit and submitted with possibly |
| // different commit hashes but the same content. |
| gt.nworkServer++ |
| doWork(t, gt.nworkServer, gt.server, "file", "23456789") |
| } |
| |
| func (gt *gitTest) serverWorkUnrelated(t *testing.T) { |
| // make unrelated change on server |
| // this makes history different on client and server |
| gt.nworkOther++ |
| doWork(t, gt.nworkOther, gt.server, "otherfile", "9999") |
| } |
| |
| func newGitTest(t *testing.T) (gt *gitTest) { |
| // The Linux builders seem not to have git in their paths. |
| // That makes this whole repo a bit useless on such systems, |
| // but make sure the tests don't fail. |
| _, err := exec.LookPath("git") |
| if err != nil { |
| t.Skipf("cannot find git in path: %v", err) |
| } |
| |
| tmpdir, err := ioutil.TempDir("", "git-codereview-test") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer func() { |
| if gt == nil { |
| os.RemoveAll(tmpdir) |
| } |
| }() |
| |
| gitversion = trun(t, tmpdir, "git", "--version") |
| |
| server := tmpdir + "/git-origin" |
| |
| mkdir(t, server) |
| write(t, server+"/file", "this is master") |
| write(t, server+"/.gitattributes", "* -text\n") |
| trun(t, server, "git", "init", ".") |
| trun(t, server, "git", "config", "user.name", "gopher") |
| trun(t, server, "git", "config", "user.email", "gopher@example.com") |
| trun(t, server, "git", "add", "file", ".gitattributes") |
| trun(t, server, "git", "commit", "-m", "on master") |
| |
| for _, name := range []string{"dev.branch", "release.branch"} { |
| trun(t, server, "git", "checkout", "master") |
| trun(t, server, "git", "checkout", "-b", name) |
| write(t, server+"/file."+name, "this is "+name) |
| trun(t, server, "git", "add", "file."+name) |
| trun(t, server, "git", "commit", "-m", "on "+name) |
| } |
| trun(t, server, "git", "checkout", "master") |
| |
| client := tmpdir + "/git-client" |
| mkdir(t, client) |
| trun(t, client, "git", "clone", server, ".") |
| trun(t, client, "git", "config", "user.name", "gopher") |
| trun(t, client, "git", "config", "user.email", "gopher@example.com") |
| |
| // write stub hooks to keep installHook from installing its own. |
| // If it installs its own, git will look for git-codereview on the current path |
| // and may find an old git-codereview that does just about anything. |
| // In any event, we wouldn't be testing what we want to test. |
| // Tests that want to exercise hooks need to arrange for a git-codereview |
| // in the path and replace these with the real ones. |
| if _, err := os.Stat(client + "/.git/hooks"); os.IsNotExist(err) { |
| mkdir(t, client+"/.git/hooks") |
| } |
| for _, h := range hookFiles { |
| write(t, client+"/.git/hooks/"+h, "#!/bin/bash\nexit 0\n") |
| } |
| |
| trun(t, client, "git", "config", "core.editor", "false") |
| pwd, err := os.Getwd() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if err := os.Chdir(client); err != nil { |
| t.Fatal(err) |
| } |
| |
| return &gitTest{ |
| pwd: pwd, |
| tmpdir: tmpdir, |
| server: server, |
| client: client, |
| } |
| } |
| |
| func (gt *gitTest) enableGerrit(t *testing.T) { |
| write(t, gt.server+"/codereview.cfg", "gerrit: myserver\n") |
| trun(t, gt.server, "git", "add", "codereview.cfg") |
| trun(t, gt.server, "git", "commit", "-m", "add gerrit") |
| trun(t, gt.client, "git", "pull", "-r") |
| } |
| |
| func (gt *gitTest) removeStubHooks() { |
| os.RemoveAll(gt.client + "/.git/hooks/") |
| } |
| |
| func mkdir(t *testing.T, dir string) { |
| if err := os.Mkdir(dir, 0777); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| func chdir(t *testing.T, dir string) { |
| if err := os.Chdir(dir); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| func write(t *testing.T, file, data string) { |
| if err := ioutil.WriteFile(file, []byte(data), 0666); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| func read(t *testing.T, file string) []byte { |
| b, err := ioutil.ReadFile(file) |
| if err != nil { |
| t.Fatal(err) |
| } |
| return b |
| } |
| |
| func remove(t *testing.T, file string) { |
| if err := os.RemoveAll(file); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| func trun(t *testing.T, dir string, cmdline ...string) string { |
| cmd := exec.Command(cmdline[0], cmdline[1:]...) |
| cmd.Dir = dir |
| out, err := cmd.CombinedOutput() |
| if err != nil { |
| if cmdline[0] == "git" { |
| t.Fatalf("in %s/, ran %s with %s:\n%v\n%s", filepath.Base(dir), cmdline, gitversion, err, out) |
| } |
| t.Fatalf("in %s/, ran %s: %v\n%s", filepath.Base(dir), cmdline, err, out) |
| } |
| return string(out) |
| } |
| |
| // fromSlash is like filepath.FromSlash, but it ignores ! at the start of the path |
| // and " (staged)" at the end. |
| func fromSlash(path string) string { |
| if len(path) > 0 && path[0] == '!' { |
| return "!" + fromSlash(path[1:]) |
| } |
| if strings.HasSuffix(path, " (staged)") { |
| return fromSlash(path[:len(path)-len(" (staged)")]) + " (staged)" |
| } |
| return filepath.FromSlash(path) |
| } |
| |
| var ( |
| runLog []string |
| testStderr *bytes.Buffer |
| testStdout *bytes.Buffer |
| died bool |
| ) |
| |
| var mainCanDie bool |
| |
| func testMainDied(t *testing.T, args ...string) { |
| mainCanDie = true |
| testMain(t, args...) |
| if !died { |
| t.Fatalf("expected to die, did not\nstdout:\n%sstderr:\n%s", testStdout, testStderr) |
| } |
| } |
| |
| func testMain(t *testing.T, args ...string) { |
| *noRun = false |
| *verbose = 0 |
| cachedConfig = nil |
| |
| t.Logf("git-codereview %s", strings.Join(args, " ")) |
| |
| canDie := mainCanDie |
| mainCanDie = false // reset for next invocation |
| |
| defer func() { |
| runLog = runLogTrap |
| testStdout = stdoutTrap |
| testStderr = stderrTrap |
| |
| dieTrap = nil |
| runLogTrap = nil |
| stdoutTrap = nil |
| stderrTrap = nil |
| if err := recover(); err != nil { |
| if died && canDie { |
| return |
| } |
| var msg string |
| if died { |
| msg = "died" |
| } else { |
| msg = fmt.Sprintf("panic: %v", err) |
| } |
| t.Fatalf("%s\nstdout:\n%sstderr:\n%s", msg, testStdout, testStderr) |
| } |
| }() |
| |
| dieTrap = func() { |
| died = true |
| panic("died") |
| } |
| died = false |
| runLogTrap = []string{} // non-nil, to trigger saving of commands |
| stdoutTrap = new(bytes.Buffer) |
| stderrTrap = new(bytes.Buffer) |
| |
| os.Args = append([]string{"git-codereview"}, args...) |
| main() |
| } |
| |
| func testRan(t *testing.T, cmds ...string) { |
| if cmds == nil { |
| cmds = []string{} |
| } |
| if !reflect.DeepEqual(runLog, cmds) { |
| t.Errorf("ran:\n%s", strings.Join(runLog, "\n")) |
| t.Errorf("wanted:\n%s", strings.Join(cmds, "\n")) |
| } |
| } |
| |
| func testPrinted(t *testing.T, buf *bytes.Buffer, name string, messages ...string) { |
| all := buf.String() |
| var errors bytes.Buffer |
| for _, msg := range messages { |
| if strings.HasPrefix(msg, "!") { |
| if strings.Contains(all, msg[1:]) { |
| fmt.Fprintf(&errors, "%s does (but should not) contain %q\n", name, msg[1:]) |
| } |
| continue |
| } |
| if !strings.Contains(all, msg) { |
| fmt.Fprintf(&errors, "%s does not contain %q\n", name, msg) |
| } |
| } |
| if errors.Len() > 0 { |
| t.Fatalf("wrong output\n%s%s:\n%s", &errors, name, all) |
| } |
| } |
| |
| func testPrintedStdout(t *testing.T, messages ...string) { |
| testPrinted(t, testStdout, "stdout", messages...) |
| } |
| |
| func testPrintedStderr(t *testing.T, messages ...string) { |
| testPrinted(t, testStderr, "stderr", messages...) |
| } |
| |
| func testNoStdout(t *testing.T) { |
| if testStdout.Len() != 0 { |
| t.Fatalf("unexpected stdout:\n%s", testStdout) |
| } |
| } |
| |
| func testNoStderr(t *testing.T) { |
| if testStderr.Len() != 0 { |
| t.Fatalf("unexpected stderr:\n%s", testStderr) |
| } |
| } |
| |
| type gerritServer struct { |
| l net.Listener |
| mu sync.Mutex |
| reply map[string]gerritReply |
| } |
| |
| func newGerritServer(t *testing.T) *gerritServer { |
| l, err := net.Listen("tcp", "127.0.0.1:0") |
| if err != nil { |
| t.Fatalf("starting fake gerrit: %v", err) |
| } |
| |
| auth.host = l.Addr().String() |
| auth.url = "http://" + auth.host |
| auth.project = "proj" |
| auth.user = "gopher" |
| auth.password = "PASSWORD" |
| |
| s := &gerritServer{l: l, reply: make(map[string]gerritReply)} |
| go http.Serve(l, s) |
| return s |
| } |
| |
| func (s *gerritServer) done() { |
| s.l.Close() |
| auth.host = "" |
| auth.url = "" |
| auth.project = "" |
| auth.user = "" |
| auth.password = "" |
| } |
| |
| type gerritReply struct { |
| status int |
| body string |
| json interface{} |
| f func() gerritReply |
| } |
| |
| func (s *gerritServer) setReply(path string, reply gerritReply) { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| s.reply[path] = reply |
| } |
| |
| func (s *gerritServer) setJSON(id, json string) { |
| s.setReply("/a/changes/proj~master~"+id, gerritReply{body: ")]}'\n" + json}) |
| } |
| |
| func (s *gerritServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { |
| if req.URL.Path == "/a/changes/" { |
| s.serveChangesQuery(w, req) |
| return |
| } |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| reply, ok := s.reply[req.URL.Path] |
| if !ok { |
| http.NotFound(w, req) |
| return |
| } |
| if reply.f != nil { |
| reply = reply.f() |
| } |
| if reply.status != 0 { |
| w.WriteHeader(reply.status) |
| } |
| if reply.json != nil { |
| body, err := json.Marshal(reply.json) |
| if err != nil { |
| dief("%v", err) |
| } |
| reply.body = ")]}'\n" + string(body) |
| } |
| if len(reply.body) > 0 { |
| w.Write([]byte(reply.body)) |
| } |
| } |
| |
| func (s *gerritServer) serveChangesQuery(w http.ResponseWriter, req *http.Request) { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| qs := req.URL.Query()["q"] |
| if len(qs) > 10 { |
| http.Error(w, "too many queries", 500) |
| } |
| var buf bytes.Buffer |
| fmt.Fprintf(&buf, ")]}'\n") |
| end := "" |
| if len(qs) > 1 { |
| fmt.Fprintf(&buf, "[") |
| end = "]" |
| } |
| sep := "" |
| for _, q := range qs { |
| fmt.Fprintf(&buf, "%s[", sep) |
| if strings.HasPrefix(q, "change:") { |
| reply, ok := s.reply[req.URL.Path+strings.TrimPrefix(q, "change:")] |
| if ok { |
| if reply.json != nil { |
| body, err := json.Marshal(reply.json) |
| if err != nil { |
| dief("%v", err) |
| } |
| reply.body = ")]}'\n" + string(body) |
| } |
| body := reply.body |
| i := strings.Index(body, "\n") |
| if i > 0 { |
| body = body[i+1:] |
| } |
| fmt.Fprintf(&buf, "%s", body) |
| } |
| } |
| fmt.Fprintf(&buf, "]") |
| sep = "," |
| } |
| fmt.Fprintf(&buf, "%s", end) |
| w.Write(buf.Bytes()) |
| } |