github.com/golang/review@v0.0.0-20190122205339-266ee1edf5c3/git-codereview/util_test.go (about)

     1  // Copyright 2014 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  
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"net"
    13  	"net/http"
    14  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"reflect"
    18  	"strings"
    19  	"sync"
    20  	"testing"
    21  )
    22  
    23  var gitversion = "unknown git version" // git version for error logs
    24  
    25  type gitTest struct {
    26  	pwd         string // current directory before test
    27  	tmpdir      string // temporary directory holding repos
    28  	server      string // server repo root
    29  	client      string // client repo root
    30  	nwork       int    // number of calls to work method
    31  	nworkServer int    // number of calls to serverWork method
    32  	nworkOther  int    // number of calls to serverWorkUnrelated method
    33  }
    34  
    35  // resetReadOnlyFlagAll resets windows read-only flag
    36  // set on path and any children it contains.
    37  // The flag is set by git and has to be removed.
    38  // os.Remove refuses to remove files with read-only flag set.
    39  func resetReadOnlyFlagAll(path string) error {
    40  	fi, err := os.Stat(path)
    41  	if err != nil {
    42  		return err
    43  	}
    44  
    45  	if !fi.IsDir() {
    46  		return os.Chmod(path, 0666)
    47  	}
    48  
    49  	fd, err := os.Open(path)
    50  	if err != nil {
    51  		return err
    52  	}
    53  	defer fd.Close()
    54  
    55  	names, _ := fd.Readdirnames(-1)
    56  	for _, name := range names {
    57  		resetReadOnlyFlagAll(path + string(filepath.Separator) + name)
    58  	}
    59  	return nil
    60  }
    61  
    62  func (gt *gitTest) done() {
    63  	os.Chdir(gt.pwd) // change out of gt.tmpdir first, otherwise following os.RemoveAll fails on windows
    64  	resetReadOnlyFlagAll(gt.tmpdir)
    65  	os.RemoveAll(gt.tmpdir)
    66  	cachedConfig = nil
    67  }
    68  
    69  // doWork simulates commit 'n' touching 'file' in 'dir'
    70  func doWork(t *testing.T, n int, dir, file, changeid string) {
    71  	write(t, dir+"/"+file, fmt.Sprintf("new content %d", n))
    72  	trun(t, dir, "git", "add", file)
    73  	suffix := ""
    74  	if n > 1 {
    75  		suffix = fmt.Sprintf(" #%d", n)
    76  	}
    77  	msg := fmt.Sprintf("msg%s\n\nChange-Id: I%d%s\n", suffix, n, changeid)
    78  	trun(t, dir, "git", "commit", "-m", msg)
    79  }
    80  
    81  func (gt *gitTest) work(t *testing.T) {
    82  	if gt.nwork == 0 {
    83  		trun(t, gt.client, "git", "checkout", "-b", "work")
    84  		trun(t, gt.client, "git", "branch", "--set-upstream-to", "origin/master")
    85  		trun(t, gt.client, "git", "tag", "work") // make sure commands do the right thing when there is a tag of the same name
    86  	}
    87  
    88  	// make local change on client
    89  	gt.nwork++
    90  	doWork(t, gt.nwork, gt.client, "file", "23456789")
    91  }
    92  
    93  func (gt *gitTest) workFile(t *testing.T, file string) {
    94  	// make local change on client in the specific file
    95  	gt.nwork++
    96  	doWork(t, gt.nwork, gt.client, file, "23456789")
    97  }
    98  
    99  func (gt *gitTest) serverWork(t *testing.T) {
   100  	// make change on server
   101  	// duplicating the sequence of changes in gt.work to simulate them
   102  	// having gone through Gerrit and submitted with possibly
   103  	// different commit hashes but the same content.
   104  	gt.nworkServer++
   105  	doWork(t, gt.nworkServer, gt.server, "file", "23456789")
   106  }
   107  
   108  func (gt *gitTest) serverWorkUnrelated(t *testing.T) {
   109  	// make unrelated change on server
   110  	// this makes history different on client and server
   111  	gt.nworkOther++
   112  	doWork(t, gt.nworkOther, gt.server, "otherfile", "9999")
   113  }
   114  
   115  func newGitTest(t *testing.T) (gt *gitTest) {
   116  	// The Linux builders seem not to have git in their paths.
   117  	// That makes this whole repo a bit useless on such systems,
   118  	// but make sure the tests don't fail.
   119  	_, err := exec.LookPath("git")
   120  	if err != nil {
   121  		t.Skipf("cannot find git in path: %v", err)
   122  	}
   123  
   124  	tmpdir, err := ioutil.TempDir("", "git-codereview-test")
   125  	if err != nil {
   126  		t.Fatal(err)
   127  	}
   128  	defer func() {
   129  		if gt == nil {
   130  			os.RemoveAll(tmpdir)
   131  		}
   132  	}()
   133  
   134  	gitversion = trun(t, tmpdir, "git", "--version")
   135  
   136  	server := tmpdir + "/git-origin"
   137  
   138  	mkdir(t, server)
   139  	write(t, server+"/file", "this is master")
   140  	write(t, server+"/.gitattributes", "* -text\n")
   141  	trun(t, server, "git", "init", ".")
   142  	trun(t, server, "git", "config", "user.name", "gopher")
   143  	trun(t, server, "git", "config", "user.email", "gopher@example.com")
   144  	trun(t, server, "git", "add", "file", ".gitattributes")
   145  	trun(t, server, "git", "commit", "-m", "on master")
   146  
   147  	for _, name := range []string{"dev.branch", "release.branch"} {
   148  		trun(t, server, "git", "checkout", "master")
   149  		trun(t, server, "git", "checkout", "-b", name)
   150  		write(t, server+"/file."+name, "this is "+name)
   151  		trun(t, server, "git", "add", "file."+name)
   152  		trun(t, server, "git", "commit", "-m", "on "+name)
   153  	}
   154  	trun(t, server, "git", "checkout", "master")
   155  
   156  	client := tmpdir + "/git-client"
   157  	mkdir(t, client)
   158  	trun(t, client, "git", "clone", server, ".")
   159  	trun(t, client, "git", "config", "user.name", "gopher")
   160  	trun(t, client, "git", "config", "user.email", "gopher@example.com")
   161  
   162  	// write stub hooks to keep installHook from installing its own.
   163  	// If it installs its own, git will look for git-codereview on the current path
   164  	// and may find an old git-codereview that does just about anything.
   165  	// In any event, we wouldn't be testing what we want to test.
   166  	// Tests that want to exercise hooks need to arrange for a git-codereview
   167  	// in the path and replace these with the real ones.
   168  	if _, err := os.Stat(client + "/.git/hooks"); os.IsNotExist(err) {
   169  		mkdir(t, client+"/.git/hooks")
   170  	}
   171  	for _, h := range hookFiles {
   172  		write(t, client+"/.git/hooks/"+h, "#!/bin/bash\nexit 0\n")
   173  	}
   174  
   175  	trun(t, client, "git", "config", "core.editor", "false")
   176  	pwd, err := os.Getwd()
   177  	if err != nil {
   178  		t.Fatal(err)
   179  	}
   180  
   181  	if err := os.Chdir(client); err != nil {
   182  		t.Fatal(err)
   183  	}
   184  
   185  	return &gitTest{
   186  		pwd:    pwd,
   187  		tmpdir: tmpdir,
   188  		server: server,
   189  		client: client,
   190  	}
   191  }
   192  
   193  func (gt *gitTest) enableGerrit(t *testing.T) {
   194  	write(t, gt.server+"/codereview.cfg", "gerrit: myserver\n")
   195  	trun(t, gt.server, "git", "add", "codereview.cfg")
   196  	trun(t, gt.server, "git", "commit", "-m", "add gerrit")
   197  	trun(t, gt.client, "git", "pull", "-r")
   198  }
   199  
   200  func (gt *gitTest) removeStubHooks() {
   201  	os.RemoveAll(gt.client + "/.git/hooks/")
   202  }
   203  
   204  func mkdir(t *testing.T, dir string) {
   205  	if err := os.Mkdir(dir, 0777); err != nil {
   206  		t.Fatal(err)
   207  	}
   208  }
   209  
   210  func chdir(t *testing.T, dir string) {
   211  	if err := os.Chdir(dir); err != nil {
   212  		t.Fatal(err)
   213  	}
   214  }
   215  
   216  func write(t *testing.T, file, data string) {
   217  	if err := ioutil.WriteFile(file, []byte(data), 0666); err != nil {
   218  		t.Fatal(err)
   219  	}
   220  }
   221  
   222  func read(t *testing.T, file string) []byte {
   223  	b, err := ioutil.ReadFile(file)
   224  	if err != nil {
   225  		t.Fatal(err)
   226  	}
   227  	return b
   228  }
   229  
   230  func remove(t *testing.T, file string) {
   231  	if err := os.RemoveAll(file); err != nil {
   232  		t.Fatal(err)
   233  	}
   234  }
   235  
   236  func trun(t *testing.T, dir string, cmdline ...string) string {
   237  	cmd := exec.Command(cmdline[0], cmdline[1:]...)
   238  	cmd.Dir = dir
   239  	out, err := cmd.CombinedOutput()
   240  	if err != nil {
   241  		if cmdline[0] == "git" {
   242  			t.Fatalf("in %s/, ran %s with %s:\n%v\n%s", filepath.Base(dir), cmdline, gitversion, err, out)
   243  		}
   244  		t.Fatalf("in %s/, ran %s: %v\n%s", filepath.Base(dir), cmdline, err, out)
   245  	}
   246  	return string(out)
   247  }
   248  
   249  // fromSlash is like filepath.FromSlash, but it ignores ! at the start of the path
   250  // and " (staged)" at the end.
   251  func fromSlash(path string) string {
   252  	if len(path) > 0 && path[0] == '!' {
   253  		return "!" + fromSlash(path[1:])
   254  	}
   255  	if strings.HasSuffix(path, " (staged)") {
   256  		return fromSlash(path[:len(path)-len(" (staged)")]) + " (staged)"
   257  	}
   258  	return filepath.FromSlash(path)
   259  }
   260  
   261  var (
   262  	runLog     []string
   263  	testStderr *bytes.Buffer
   264  	testStdout *bytes.Buffer
   265  	died       bool
   266  )
   267  
   268  var mainCanDie bool
   269  
   270  func testMainDied(t *testing.T, args ...string) {
   271  	mainCanDie = true
   272  	testMain(t, args...)
   273  	if !died {
   274  		t.Fatalf("expected to die, did not\nstdout:\n%sstderr:\n%s", testStdout, testStderr)
   275  	}
   276  }
   277  
   278  func testMain(t *testing.T, args ...string) {
   279  	*noRun = false
   280  	*verbose = 0
   281  	cachedConfig = nil
   282  
   283  	t.Logf("git-codereview %s", strings.Join(args, " "))
   284  
   285  	canDie := mainCanDie
   286  	mainCanDie = false // reset for next invocation
   287  
   288  	defer func() {
   289  		runLog = runLogTrap
   290  		testStdout = stdoutTrap
   291  		testStderr = stderrTrap
   292  
   293  		dieTrap = nil
   294  		runLogTrap = nil
   295  		stdoutTrap = nil
   296  		stderrTrap = nil
   297  		if err := recover(); err != nil {
   298  			if died && canDie {
   299  				return
   300  			}
   301  			var msg string
   302  			if died {
   303  				msg = "died"
   304  			} else {
   305  				msg = fmt.Sprintf("panic: %v", err)
   306  			}
   307  			t.Fatalf("%s\nstdout:\n%sstderr:\n%s", msg, testStdout, testStderr)
   308  		}
   309  	}()
   310  
   311  	dieTrap = func() {
   312  		died = true
   313  		panic("died")
   314  	}
   315  	died = false
   316  	runLogTrap = []string{} // non-nil, to trigger saving of commands
   317  	stdoutTrap = new(bytes.Buffer)
   318  	stderrTrap = new(bytes.Buffer)
   319  
   320  	os.Args = append([]string{"git-codereview"}, args...)
   321  	main()
   322  }
   323  
   324  func testRan(t *testing.T, cmds ...string) {
   325  	if cmds == nil {
   326  		cmds = []string{}
   327  	}
   328  	if !reflect.DeepEqual(runLog, cmds) {
   329  		t.Errorf("ran:\n%s", strings.Join(runLog, "\n"))
   330  		t.Errorf("wanted:\n%s", strings.Join(cmds, "\n"))
   331  	}
   332  }
   333  
   334  func testPrinted(t *testing.T, buf *bytes.Buffer, name string, messages ...string) {
   335  	all := buf.String()
   336  	var errors bytes.Buffer
   337  	for _, msg := range messages {
   338  		if strings.HasPrefix(msg, "!") {
   339  			if strings.Contains(all, msg[1:]) {
   340  				fmt.Fprintf(&errors, "%s does (but should not) contain %q\n", name, msg[1:])
   341  			}
   342  			continue
   343  		}
   344  		if !strings.Contains(all, msg) {
   345  			fmt.Fprintf(&errors, "%s does not contain %q\n", name, msg)
   346  		}
   347  	}
   348  	if errors.Len() > 0 {
   349  		t.Fatalf("wrong output\n%s%s:\n%s", &errors, name, all)
   350  	}
   351  }
   352  
   353  func testPrintedStdout(t *testing.T, messages ...string) {
   354  	testPrinted(t, testStdout, "stdout", messages...)
   355  }
   356  
   357  func testPrintedStderr(t *testing.T, messages ...string) {
   358  	testPrinted(t, testStderr, "stderr", messages...)
   359  }
   360  
   361  func testNoStdout(t *testing.T) {
   362  	if testStdout.Len() != 0 {
   363  		t.Fatalf("unexpected stdout:\n%s", testStdout)
   364  	}
   365  }
   366  
   367  func testNoStderr(t *testing.T) {
   368  	if testStderr.Len() != 0 {
   369  		t.Fatalf("unexpected stderr:\n%s", testStderr)
   370  	}
   371  }
   372  
   373  type gerritServer struct {
   374  	l     net.Listener
   375  	mu    sync.Mutex
   376  	reply map[string]gerritReply
   377  }
   378  
   379  func newGerritServer(t *testing.T) *gerritServer {
   380  	l, err := net.Listen("tcp", "127.0.0.1:0")
   381  	if err != nil {
   382  		t.Fatalf("starting fake gerrit: %v", err)
   383  	}
   384  
   385  	auth.host = l.Addr().String()
   386  	auth.url = "http://" + auth.host
   387  	auth.project = "proj"
   388  	auth.user = "gopher"
   389  	auth.password = "PASSWORD"
   390  
   391  	s := &gerritServer{l: l, reply: make(map[string]gerritReply)}
   392  	go http.Serve(l, s)
   393  	return s
   394  }
   395  
   396  func (s *gerritServer) done() {
   397  	s.l.Close()
   398  	auth.host = ""
   399  	auth.url = ""
   400  	auth.project = ""
   401  	auth.user = ""
   402  	auth.password = ""
   403  }
   404  
   405  type gerritReply struct {
   406  	status int
   407  	body   string
   408  	json   interface{}
   409  	f      func() gerritReply
   410  }
   411  
   412  func (s *gerritServer) setReply(path string, reply gerritReply) {
   413  	s.mu.Lock()
   414  	defer s.mu.Unlock()
   415  	s.reply[path] = reply
   416  }
   417  
   418  func (s *gerritServer) setJSON(id, json string) {
   419  	s.setReply("/a/changes/proj~master~"+id, gerritReply{body: ")]}'\n" + json})
   420  }
   421  
   422  func (s *gerritServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
   423  	if req.URL.Path == "/a/changes/" {
   424  		s.serveChangesQuery(w, req)
   425  		return
   426  	}
   427  	s.mu.Lock()
   428  	defer s.mu.Unlock()
   429  	reply, ok := s.reply[req.URL.Path]
   430  	if !ok {
   431  		http.NotFound(w, req)
   432  		return
   433  	}
   434  	if reply.f != nil {
   435  		reply = reply.f()
   436  	}
   437  	if reply.status != 0 {
   438  		w.WriteHeader(reply.status)
   439  	}
   440  	if reply.json != nil {
   441  		body, err := json.Marshal(reply.json)
   442  		if err != nil {
   443  			dief("%v", err)
   444  		}
   445  		reply.body = ")]}'\n" + string(body)
   446  	}
   447  	if len(reply.body) > 0 {
   448  		w.Write([]byte(reply.body))
   449  	}
   450  }
   451  
   452  func (s *gerritServer) serveChangesQuery(w http.ResponseWriter, req *http.Request) {
   453  	s.mu.Lock()
   454  	defer s.mu.Unlock()
   455  	qs := req.URL.Query()["q"]
   456  	if len(qs) > 10 {
   457  		http.Error(w, "too many queries", 500)
   458  	}
   459  	var buf bytes.Buffer
   460  	fmt.Fprintf(&buf, ")]}'\n")
   461  	end := ""
   462  	if len(qs) > 1 {
   463  		fmt.Fprintf(&buf, "[")
   464  		end = "]"
   465  	}
   466  	sep := ""
   467  	for _, q := range qs {
   468  		fmt.Fprintf(&buf, "%s[", sep)
   469  		if strings.HasPrefix(q, "change:") {
   470  			reply, ok := s.reply[req.URL.Path+strings.TrimPrefix(q, "change:")]
   471  			if ok {
   472  				if reply.json != nil {
   473  					body, err := json.Marshal(reply.json)
   474  					if err != nil {
   475  						dief("%v", err)
   476  					}
   477  					reply.body = ")]}'\n" + string(body)
   478  				}
   479  				body := reply.body
   480  				i := strings.Index(body, "\n")
   481  				if i > 0 {
   482  					body = body[i+1:]
   483  				}
   484  				fmt.Fprintf(&buf, "%s", body)
   485  			}
   486  		}
   487  		fmt.Fprintf(&buf, "]")
   488  		sep = ","
   489  	}
   490  	fmt.Fprintf(&buf, "%s", end)
   491  	w.Write(buf.Bytes())
   492  }