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 }