github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/misc/buildbot/builder/builder.go (about)

     1  /*
     2  Copyright 2012 The Camlistore Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8       http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // The buildbot is Camlistore's continuous builder.
    18  // This builder program is started by the master. It then rebuilds
    19  // Go 1, GoTip, Camlistore, and runs a battery of tests for Camlistore.
    20  // It then sends a report to the master and terminates.
    21  // It can also respond to progress requests from the master.
    22  package main
    23  
    24  import (
    25  	"bufio"
    26  	"bytes"
    27  	"crypto/tls"
    28  	"encoding/json"
    29  	"errors"
    30  	"flag"
    31  	"fmt"
    32  	"io"
    33  	"io/ioutil"
    34  	"log"
    35  	"net/http"
    36  	"net/url"
    37  	"os"
    38  	"os/exec"
    39  	"os/signal"
    40  	"path/filepath"
    41  	"regexp"
    42  	"runtime"
    43  	"strings"
    44  	"sync"
    45  	"syscall"
    46  	"time"
    47  
    48  	"camlistore.org/pkg/osutil"
    49  )
    50  
    51  const (
    52  	interval = 60 * time.Second // polling frequency
    53  	warmup   = 30 * time.Second // duration before we test if devcam server has started properly
    54  )
    55  
    56  var (
    57  	// TODO(mpl): use that one, same as in master.
    58  	altCamliRevURL = flag.String("camlirevurl", "", "alternative URL to query about the latest camlistore revision hash (e.g camlistore.org/latesthash), to alleviate hitting too often the Camlistore git repo.")
    59  	arch           = flag.String("arch", "", "The arch we report the master(s). Defaults to runtime.GOARCH.")
    60  	fakeTests      = flag.Bool("faketests", false, "Run fast fake tests instead of the real ones, for faster debugging.")
    61  	help           = flag.Bool("h", false, "show this help")
    62  	host           = flag.String("host", "0.0.0.0:8081", "listening hostname and port")
    63  	masterHosts    = flag.String("masterhosts", "localhost:8080", "listening hostname and port of the master bots, i.e where to send the test suite reports. Comma separated list.")
    64  	ourOS          = flag.String("os", "", "The OS we report the master(s). Defaults to runtime.GOOS.")
    65  	skipGo1Build   = flag.Bool("skipgo1build", false, "skip initial go1 build, for debugging and quickly going to the next steps.")
    66  	verbose        = flag.Bool("verbose", false, "print what's going on")
    67  	skipTLSCheck   = flag.Bool("skiptlscheck", false, "accept any certificate presented by server when uploading results.")
    68  	taskLifespan   = flag.Int("timeout", 600, "Lifespan (in seconds) for each task run by this builder, after which the task automatically terminates. 0 or negative means infinite.")
    69  )
    70  
    71  var (
    72  	testFile       = []string{"AUTHORS", "CONTRIBUTORS"}
    73  	cacheDir       string
    74  	camliHeadHash  string
    75  	camliRoot      string
    76  	camputCacheDir string
    77  	client         = http.DefaultClient
    78  	dbg            *debugger
    79  	defaultPATH    string
    80  	go1Dir         string
    81  	goTipDir       string
    82  	goTipHash      string
    83  
    84  	biSuitelk        sync.Mutex
    85  	currentTestSuite *testSuite
    86  	currentBiSuite   *biTestSuite
    87  
    88  	// Process of the camlistore server, so we can kill it when
    89  	// we get killed ourselves.
    90  	camliProc *os.Process
    91  
    92  	// For "If-Modified-Since" requests asking for progress.
    93  	// Updated every time a new test task/run is added to the test suite.
    94  	lastModified time.Time
    95  )
    96  
    97  var devcamBin = filepath.Join("bin", "devcam")
    98  var (
    99  	hgCloneGo1Cmd      = newTask("hg", "clone", "-u", "release", "https://code.google.com/p/go")
   100  	hgCloneGoTipCmd    = newTask("hg", "clone", "-u", "tip", "https://code.google.com/p/go")
   101  	hgPullCmd          = newTask("hg", "pull")
   102  	hgUpdateCmd        = newTask("hg", "update", "-C", "default")
   103  	hgLogCmd           = newTask("hg", "log", "-r", "tip", "--template", "{node}")
   104  	hgConfigCmd        = newTask("hg", "--config", "extensions.purge=", "purge", "--all")
   105  	gitCloneCmd        = newTask("git", "clone", "https://camlistore.googlesource.com/camlistore")
   106  	gitResetCmd        = newTask("git", "reset", "--hard")
   107  	gitCleanCmd        = newTask("git", "clean", "-Xdf")
   108  	gitPullCmd         = newTask("git", "pull")
   109  	gitRevCmd          = newTask("git", "rev-parse", "HEAD")
   110  	buildGoCmd         = newTask("./make.bash")
   111  	buildCamliCmd      = newTask("go", "run", "make.go", "-v")
   112  	runTestsCmd        = newTask(devcamBin, "test")
   113  	runCamliCmd        = newTask(devcamBin, "server", "--wipe", "--mysql")
   114  	camgetCmd          = newTask(devcamBin, "get")
   115  	camputCmd          = newTask(devcamBin, "put", "file", "--permanode", testFile[0])
   116  	camputVivifyCmd    = newTask(devcamBin, "put", "file", "--vivify", testFile[1])
   117  	camputFilenodesCmd = newTask(devcamBin, "put", "file", "--filenodes", "pkg")
   118  )
   119  
   120  func usage() {
   121  	fmt.Fprintf(os.Stderr, "\t builderBot \n")
   122  	flag.PrintDefaults()
   123  	os.Exit(2)
   124  }
   125  
   126  type debugger struct {
   127  	lg *log.Logger
   128  }
   129  
   130  func (dbg *debugger) Printf(format string, v ...interface{}) {
   131  	if dbg != nil && *verbose {
   132  		dbg.lg.Printf(format, v...)
   133  	}
   134  }
   135  
   136  func (dbg *debugger) Println(v ...interface{}) {
   137  	if v == nil {
   138  		return
   139  	}
   140  	if dbg != nil && *verbose {
   141  		dbg.lg.Println(v...)
   142  	}
   143  }
   144  
   145  type task struct {
   146  	Program  string
   147  	Args     []string
   148  	Start    time.Time
   149  	Duration time.Duration
   150  	Err      string
   151  	hidden   bool
   152  }
   153  
   154  func newTask(program string, args ...string) *task {
   155  	return &task{Program: program, Args: args}
   156  }
   157  
   158  // because sometimes we do not want to modify the tsk template
   159  // so we make a copy of it
   160  func newTaskFrom(tsk *task) *task {
   161  	return newTask(tsk.Program, tsk.Args...)
   162  }
   163  
   164  func (t *task) String() string {
   165  	return fmt.Sprintf("%v %v", t.Program, t.Args)
   166  }
   167  
   168  func (t *task) Error() string {
   169  	return t.Err
   170  }
   171  
   172  func (t *task) run() (string, error) {
   173  	var err error
   174  	defer func() {
   175  		t.Duration = time.Now().Sub(t.Start)
   176  		if !t.hidden {
   177  			biSuitelk.Lock()
   178  			currentTestSuite.addRun(t)
   179  			biSuitelk.Unlock()
   180  		}
   181  	}()
   182  	dbg.Println(t.String())
   183  	cmd := exec.Command(t.Program, t.Args...)
   184  	var stdout, stderr bytes.Buffer
   185  	cmd.Stdout = &stdout
   186  	cmd.Stderr = &stderr
   187  	t.Start = time.Now()
   188  	setTaskErr := func() {
   189  		var sout, serr string
   190  		if sout = stdout.String(); sout == "" {
   191  			sout = "(empty)"
   192  		}
   193  		if serr = stderr.String(); serr == "" {
   194  			serr = "(empty)"
   195  		}
   196  		t.Err = fmt.Sprintf("Stdout:\n%s\n\nStderr:\n%s", sout, serr)
   197  		if err != nil {
   198  			t.Err = fmt.Sprintf("%v\n\n%v", err, t.Err)
   199  		}
   200  	}
   201  	// TODO(mpl, wathiede): make it learn about task durations.
   202  	errc := make(chan error)
   203  	go func() {
   204  		errc <- cmd.Run()
   205  	}()
   206  	if *taskLifespan > 0 {
   207  		select {
   208  		case <-time.After(time.Duration(*taskLifespan) * time.Second):
   209  			setTaskErr()
   210  			t.Err = fmt.Sprintf("%v\n\nTask %q took too long. Giving up after %v seconds.\n",
   211  				t.Err, t.String(), *taskLifespan)
   212  			if cmd.Process != nil {
   213  				if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
   214  					dbg.Printf("Could not terminate process for task %q: %v", t.String(), err)
   215  				}
   216  			}
   217  			return "", t
   218  		case err = <-errc:
   219  			break
   220  		}
   221  	} else {
   222  		err = <-errc
   223  	}
   224  	if err != nil {
   225  		setTaskErr()
   226  		return "", t
   227  	}
   228  	return stdout.String(), nil
   229  }
   230  
   231  type testSuite struct {
   232  	Run       []*task
   233  	CamliHash string
   234  	GoHash    string
   235  	Err       string
   236  	Start     time.Time
   237  	IsTip     bool
   238  }
   239  
   240  func (ts *testSuite) addRun(tsk *task) {
   241  	if ts == nil {
   242  		panic("Tried adding a run to a nil testSuite")
   243  	}
   244  	if ts.Start.IsZero() && len(ts.Run) == 0 {
   245  		ts.Start = tsk.Start
   246  	}
   247  	if tsk.Err != "" && ts.Err == "" {
   248  		ts.Err = tsk.Err
   249  	}
   250  	ts.Run = append(ts.Run, tsk)
   251  	lastModified = time.Now()
   252  }
   253  
   254  type biTestSuite struct {
   255  	Local bool
   256  	Go1   testSuite
   257  	GoTip testSuite
   258  }
   259  
   260  func main() {
   261  	flag.Usage = usage
   262  	flag.Parse()
   263  	if *help {
   264  		usage()
   265  	}
   266  
   267  	if *skipTLSCheck {
   268  		tr := &http.Transport{
   269  			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
   270  		}
   271  		client = &http.Client{Transport: tr}
   272  	}
   273  
   274  	go handleSignals()
   275  	http.HandleFunc("/progress", progressHandler)
   276  	go func() {
   277  		log.Printf("Now starting to listen on %v", *host)
   278  		if err := http.ListenAndServe(*host, nil); err != nil {
   279  			log.Fatalf("Could not start listening on %v: %v", *host, err)
   280  		}
   281  	}()
   282  	setup()
   283  
   284  	biSuitelk.Lock()
   285  	currentBiSuite = &biTestSuite{}
   286  	biSuitelk.Unlock()
   287  	for _, isTip := range [2]bool{false, true} {
   288  		currentTestSuite = &testSuite{
   289  			Run:   make([]*task, 0, 1),
   290  			IsTip: isTip,
   291  			Start: time.Now(),
   292  		}
   293  		// We prepare the Go tip tree as soon as in the Go 1 run, so
   294  		// we can set GoTipHash in the test suite.
   295  		if !isTip {
   296  			if err := prepGoTipTree(); err != nil {
   297  				endOfSuite(err)
   298  				// If we failed with that in the Go 1 run, we just restart
   299  				// from scratch instead of trying to cope with it in the Gotip run.
   300  				// Same for buildGoTip and prepCamliTree.
   301  				break
   302  			}
   303  		}
   304  
   305  		biSuitelk.Lock()
   306  		currentTestSuite.GoHash = goTipHash
   307  		biSuitelk.Unlock()
   308  		if isTip && !*fakeTests {
   309  			if err := buildGoTip(); err != nil {
   310  				endOfSuite(err)
   311  				break
   312  			}
   313  		}
   314  		if err := prepCamliTree(isTip); err != nil {
   315  			endOfSuite(err)
   316  			break
   317  		}
   318  
   319  		biSuitelk.Lock()
   320  		currentTestSuite.CamliHash = camliHeadHash
   321  		biSuitelk.Unlock()
   322  		restorePATH()
   323  		goDir := go1Dir
   324  		if isTip {
   325  			goDir = goTipDir
   326  		}
   327  		switchGo(goDir)
   328  		if *fakeTests {
   329  			if err := fakeRun(); err != nil {
   330  				endOfSuite(err)
   331  				continue
   332  			}
   333  			endOfSuite(nil)
   334  			if isTip {
   335  				break
   336  			}
   337  			continue
   338  		}
   339  		if err := buildCamli(); err != nil {
   340  			endOfSuite(err)
   341  			continue
   342  		}
   343  		if err := runTests(); err != nil {
   344  			endOfSuite(err)
   345  			continue
   346  		}
   347  		if err := runCamli(); err != nil {
   348  			endOfSuite(err)
   349  			continue
   350  		}
   351  		if err := hitCamliUi(); err != nil {
   352  			endOfSuite(err)
   353  			continue
   354  		}
   355  		doVivify := false
   356  		if err := camputOne(doVivify); err != nil {
   357  			endOfSuite(err)
   358  			continue
   359  		}
   360  		doVivify = true
   361  		if err := camputOne(doVivify); err != nil {
   362  			endOfSuite(err)
   363  			continue
   364  		}
   365  		if err := camputMany(); err != nil {
   366  			endOfSuite(err)
   367  			continue
   368  		}
   369  		endOfSuite(nil)
   370  	}
   371  	sanitizeRevs()
   372  	sendReport()
   373  }
   374  
   375  func sanitizeRevs() {
   376  	if currentBiSuite == nil {
   377  		return
   378  	}
   379  	if currentBiSuite.GoTip.Start.IsZero() {
   380  		return
   381  	}
   382  	if currentBiSuite.GoTip.CamliHash == "" && currentBiSuite.Go1.CamliHash == "" {
   383  		dbg.Printf("CamliHash not set in both Go1 and GoTip test suites")
   384  		return
   385  	}
   386  	if currentBiSuite.GoTip.CamliHash == "" && currentBiSuite.Go1.CamliHash == "" {
   387  		dbg.Printf("GoHash not set in both Go1 and GoTip test suites")
   388  		return
   389  	}
   390  	if currentBiSuite.GoTip.CamliHash != "" && currentBiSuite.Go1.CamliHash != "" &&
   391  		currentBiSuite.GoTip.CamliHash != currentBiSuite.Go1.CamliHash {
   392  		panic("CamliHash in GoTip suite and in Go1 suite differ; should not happen.")
   393  	}
   394  	if currentBiSuite.GoTip.GoHash != "" && currentBiSuite.Go1.GoHash != "" &&
   395  		currentBiSuite.GoTip.GoHash != currentBiSuite.Go1.GoHash {
   396  		panic("GoHash in GoTip suite and in Go1 suite differ; should not happen.")
   397  	}
   398  	if currentBiSuite.GoTip.GoHash == "" {
   399  		currentBiSuite.GoTip.GoHash = currentBiSuite.Go1.GoHash
   400  	}
   401  	if currentBiSuite.Go1.GoHash == "" {
   402  		currentBiSuite.Go1.GoHash = currentBiSuite.GoTip.GoHash
   403  	}
   404  	if currentBiSuite.GoTip.CamliHash == "" {
   405  		currentBiSuite.GoTip.CamliHash = currentBiSuite.Go1.CamliHash
   406  	}
   407  	if currentBiSuite.Go1.CamliHash == "" {
   408  		currentBiSuite.Go1.CamliHash = currentBiSuite.GoTip.CamliHash
   409  	}
   410  }
   411  
   412  func endOfSuite(err error) {
   413  	biSuitelk.Lock()
   414  	defer biSuitelk.Unlock()
   415  	if currentTestSuite.IsTip {
   416  		currentBiSuite.GoTip = *currentTestSuite
   417  	} else {
   418  		currentBiSuite.Go1 = *currentTestSuite
   419  	}
   420  	killCamli()
   421  	if err != nil {
   422  		log.Printf("%v", err)
   423  	} else {
   424  		dbg.Println("All good.")
   425  	}
   426  }
   427  
   428  func masterHostsReader(r io.Reader) ([]string, error) {
   429  	hosts := []string{}
   430  	scanner := bufio.NewScanner(r)
   431  	for scanner.Scan() {
   432  		l := scanner.Text()
   433  		u, err := url.Parse(l)
   434  		if err != nil {
   435  			return nil, err
   436  		}
   437  		if u.Host == "" {
   438  			return nil, fmt.Errorf("URL missing Host: %q", l)
   439  		}
   440  		hosts = append(hosts, u.String())
   441  	}
   442  	if err := scanner.Err(); err != nil {
   443  		return nil, err
   444  	}
   445  	return hosts, nil
   446  }
   447  
   448  var masterHostsFile = filepath.Join(osutil.CamliConfigDir(), "builderbot-config")
   449  
   450  func loadMasterHosts() error {
   451  	r, err := os.Open(masterHostsFile)
   452  	if err != nil {
   453  		return err
   454  	}
   455  	defer r.Close()
   456  
   457  	hosts, err := masterHostsReader(r)
   458  	if err != nil {
   459  		return err
   460  	}
   461  	if *masterHosts != "" {
   462  		*masterHosts += ","
   463  	}
   464  	log.Println("Additional host(s) to send our build reports:", hosts)
   465  	*masterHosts += strings.Join(hosts, ",")
   466  	return nil
   467  }
   468  
   469  func setup() {
   470  	var err error
   471  	defaultPATH = os.Getenv("PATH")
   472  	if defaultPATH == "" {
   473  		log.Fatal("PATH not set")
   474  	}
   475  	log.SetPrefix("BUILDER: ")
   476  	dbg = &debugger{log.New(os.Stderr, "BUILDER: ", log.LstdFlags)}
   477  
   478  	err = loadMasterHosts()
   479  	if err != nil {
   480  		if os.IsNotExist(err) {
   481  			log.Printf("%q missing. No additional remote master(s) will receive build report.", masterHostsFile)
   482  		} else {
   483  			log.Println("Error parsing master hosts file %q: %v",
   484  				masterHostsFile, err)
   485  		}
   486  	}
   487  
   488  	// the OS we run on
   489  	if *ourOS == "" {
   490  		*ourOS = runtime.GOOS
   491  		if *ourOS == "" {
   492  			// Can this happen? I don't think so, but just in case...
   493  			panic("runtime.GOOS was not set")
   494  		}
   495  	}
   496  	// the arch we run on
   497  	if *arch == "" {
   498  		*arch = runtime.GOARCH
   499  		if *arch == "" {
   500  			panic("runtime.GOARCH was not set")
   501  		}
   502  	}
   503  
   504  	// cacheDir
   505  	cacheDir = filepath.Join(os.TempDir(), "camlibot-cache")
   506  	if err := os.MkdirAll(cacheDir, 0755); err != nil {
   507  		log.Fatalf("Could not create cache dir %v: %v", cacheDir, err)
   508  	}
   509  
   510  	// get go1 and gotip source
   511  	if err := os.Chdir(cacheDir); err != nil {
   512  		log.Fatalf("Could not cd to %v: %v", cacheDir, err)
   513  	}
   514  	go1Dir, err = filepath.Abs("go1")
   515  	if err != nil {
   516  		log.Fatalf("Problem with Go 1 dir: %v", err)
   517  	}
   518  	goTipDir, err = filepath.Abs("gotip")
   519  	if err != nil {
   520  		log.Fatalf("Problem with Go tip dir: %v", err)
   521  	}
   522  	for _, goDir := range []string{go1Dir, goTipDir} {
   523  		// if go dirs exist, just reuse them
   524  		if _, err := os.Stat(goDir); err != nil {
   525  			if !os.IsNotExist(err) {
   526  				log.Fatalf("Could not stat %v: %v", goDir, err)
   527  			}
   528  			// go1/gotip dir not here, let's clone it.
   529  			hgCloneCmd := hgCloneGo1Cmd
   530  			if goDir == goTipDir {
   531  				hgCloneCmd = hgCloneGoTipCmd
   532  			}
   533  			tsk := newTask(hgCloneCmd.Program, hgCloneCmd.Args...)
   534  			tsk.hidden = true
   535  			if _, err := tsk.run(); err != nil {
   536  				log.Fatalf("Could not hg clone %v: %v", goDir, err)
   537  			}
   538  			if err := os.Rename("go", goDir); err != nil {
   539  				log.Fatalf("Could not rename go dir into %v: %v", goDir, err)
   540  			}
   541  		}
   542  	}
   543  
   544  	if !*skipGo1Build {
   545  		// build Go1
   546  		if err := buildGo1(); err != nil {
   547  			log.Fatal(err)
   548  		}
   549  	}
   550  
   551  	// get camlistore source
   552  	if err := os.Chdir(cacheDir); err != nil {
   553  		log.Fatal("Could not cd to %v: %v", cacheDir, err)
   554  	}
   555  	camliRoot, err = filepath.Abs("camlistore.org")
   556  	if err != nil {
   557  		log.Fatal(err)
   558  	}
   559  	// if camlistore dir already exists, reuse it
   560  	if _, err := os.Stat(camliRoot); err != nil {
   561  		if !os.IsNotExist(err) {
   562  			log.Fatalf("Could not stat %v: %v", camliRoot, err)
   563  		}
   564  		cloneCmd := newTask(gitCloneCmd.Program, append(gitCloneCmd.Args, camliRoot)...)
   565  		cloneCmd.hidden = true
   566  		if _, err := cloneCmd.run(); err != nil {
   567  			log.Fatalf("Could not git clone into %v: %v", camliRoot, err)
   568  		}
   569  	}
   570  
   571  	// recording camput cache dir, so we can clean it up fast everytime
   572  	homeDir := os.Getenv("HOME")
   573  	if homeDir == "" {
   574  		log.Fatal("HOME not set")
   575  	}
   576  	camputCacheDir = filepath.Join(homeDir, ".cache", "camlistore")
   577  }
   578  
   579  func buildGo1() error {
   580  	if err := os.Chdir(filepath.Join(go1Dir, "src")); err != nil {
   581  		log.Fatalf("Could not cd to %v: %v", go1Dir, err)
   582  	}
   583  	tsk := newTask(buildGoCmd.Program, buildGoCmd.Args...)
   584  	tsk.hidden = true
   585  	if _, err := tsk.run(); err != nil {
   586  		return err
   587  	}
   588  	return nil
   589  }
   590  
   591  func handleSignals() {
   592  	c := make(chan os.Signal)
   593  	sigs := []os.Signal{syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT}
   594  	signal.Notify(c, sigs...)
   595  	for {
   596  		sig := <-c
   597  		sysSig, ok := sig.(syscall.Signal)
   598  		if !ok {
   599  			log.Fatal("Not a unix signal")
   600  		}
   601  		switch sysSig {
   602  		case syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
   603  			log.Printf("Received %v signal, terminating.", sig)
   604  			killCamli()
   605  			os.Exit(0)
   606  		default:
   607  			panic("should not get other signals here")
   608  		}
   609  	}
   610  }
   611  
   612  var plausibleHashRx = regexp.MustCompile(`^[a-f0-9]{40}$`)
   613  
   614  func prepGoTipTree() error {
   615  	if err := os.Chdir(goTipDir); err != nil {
   616  		return fmt.Errorf("Could not cd to %v: %v", goTipDir, err)
   617  	}
   618  	tasks := []*task{
   619  		newTaskFrom(hgPullCmd),
   620  		newTaskFrom(hgUpdateCmd),
   621  		newTaskFrom(hgLogCmd),
   622  		newTaskFrom(hgConfigCmd),
   623  	}
   624  	hash := ""
   625  	for _, t := range tasks {
   626  		out, err := t.run()
   627  		if err != nil {
   628  			return fmt.Errorf("Could not prepare the Go tip tree with %v: %v", t.String(), err)
   629  		}
   630  		if t.String() == hgLogCmd.String() {
   631  			hash = strings.TrimRight(out, "\n")
   632  		}
   633  	}
   634  	if !plausibleHashRx.MatchString(hash) {
   635  		return fmt.Errorf("Go rev %q does not look like an hg hash.", hash)
   636  	}
   637  	goTipHash = hash
   638  	dbg.Println("current head in go tree: " + goTipHash)
   639  	return nil
   640  }
   641  
   642  func buildGoTip() error {
   643  	srcDir := filepath.Join(goTipDir, "src")
   644  	if err := os.Chdir(srcDir); err != nil {
   645  		return fmt.Errorf("Could not cd to %v: %v", srcDir, err)
   646  	}
   647  	if _, err := newTaskFrom(buildGoCmd).run(); err != nil {
   648  		return err
   649  	}
   650  	return nil
   651  }
   652  
   653  func prepCamliTree(isTip bool) error {
   654  	if err := os.Chdir(camliRoot); err != nil {
   655  		return fmt.Errorf("Could not cd to %v: %v", camliRoot, err)
   656  	}
   657  	rev := "HEAD"
   658  	if isTip {
   659  		if !plausibleHashRx.MatchString(camliHeadHash) {
   660  			// the run with Go 1 should have taken care of setting camliHeadHash
   661  			return errors.New("camliHeadHash hasn't been set properly in the Go 1 run")
   662  		}
   663  		// we reset to the rev that was noted at the previous run with Go 1
   664  		// because we want to do both runs at the same rev
   665  		rev = camliHeadHash
   666  	}
   667  	resetCmd := newTask(gitResetCmd.Program, append(gitResetCmd.Args, rev)...)
   668  	tasks := []*task{
   669  		resetCmd,
   670  		newTaskFrom(gitCleanCmd),
   671  	}
   672  	for _, t := range tasks {
   673  		_, err := t.run()
   674  		if err != nil {
   675  			return fmt.Errorf("Could not prepare the Camli tree with %v: %v\n", t.String(), err)
   676  		}
   677  	}
   678  	if isTip {
   679  		// We only need to pull and get the camli head hash when in the Go 1 run
   680  		return nil
   681  	}
   682  	tasks = []*task{
   683  		newTaskFrom(gitPullCmd),
   684  		newTaskFrom(gitRevCmd),
   685  	}
   686  	hash := ""
   687  	for _, t := range tasks {
   688  		out, err := t.run()
   689  		if err != nil {
   690  			return fmt.Errorf("Could not prepare the Camli tree with %v: %v\n", t.String(), err)
   691  		}
   692  		hash = strings.TrimRight(out, "\n")
   693  	}
   694  	if !plausibleHashRx.MatchString(hash) {
   695  		return fmt.Errorf("Camlistore rev %q does not look like a git hash.", hash)
   696  	}
   697  	camliHeadHash = hash
   698  	return nil
   699  }
   700  
   701  func restorePATH() {
   702  	err := os.Setenv("PATH", defaultPATH)
   703  	if err != nil {
   704  		log.Fatalf("Could not set PATH to %v: %v", defaultPATH, err)
   705  	}
   706  }
   707  
   708  func switchGo(goDir string) {
   709  	if runtime.GOOS == "plan9" {
   710  		panic("plan 9 not unsupported")
   711  	}
   712  	gobin := filepath.Join(goDir, "bin", "go")
   713  	if _, err := os.Stat(gobin); err != nil {
   714  		log.Fatalf("Could not stat 'go' bin at %q: %v", gobin, err)
   715  	}
   716  	p := filepath.Join(goDir, "bin") + string(filepath.ListSeparator) + defaultPATH
   717  	if err := os.Setenv("PATH", p); err != nil {
   718  		log.Fatalf("Could not set PATH to %v: %v", p, err)
   719  	}
   720  	if err := os.Setenv("GOROOT", goDir); err != nil {
   721  		log.Fatalf("Could not set GOROOT to %v: %v", goDir, err)
   722  	}
   723  }
   724  
   725  func cleanBuildGopaths() {
   726  	tmpDir := filepath.Join(camliRoot, "tmp")
   727  	if _, err := os.Stat(tmpDir); err != nil {
   728  		if !os.IsNotExist(err) {
   729  			log.Fatalf("Could not stat %v: %v", tmpDir, err)
   730  		}
   731  		// Does not exist, we only have to recreate it
   732  		// TODO(mpl): hmm maybe it should be an error that
   733  		// it does not exist, since it also contains the
   734  		// closure stuff?
   735  		if err := os.MkdirAll(tmpDir, 0755); err != nil {
   736  			log.Fatalf("Could not mkdir %v: %v", tmpDir, err)
   737  		}
   738  		return
   739  	}
   740  	f, err := os.Open(tmpDir)
   741  	if err != nil {
   742  		log.Fatalf("Could not open %v: %v", tmpDir, err)
   743  	}
   744  	defer f.Close()
   745  	names, err := f.Readdirnames(-1)
   746  	if err != nil {
   747  		log.Fatal("Could not read %v: %v", tmpDir, err)
   748  	}
   749  	for _, v := range names {
   750  		if strings.HasPrefix(v, "build-gopath") {
   751  			if err := os.RemoveAll(filepath.Join(tmpDir, v)); err != nil {
   752  				log.Fatalf("Could not remove %v: %v", v, err)
   753  			}
   754  		}
   755  	}
   756  }
   757  
   758  func fakeRun() error {
   759  	if _, err := newTask("sleep", "1").run(); err != nil {
   760  		return err
   761  	}
   762  	return nil
   763  }
   764  
   765  func buildCamli() error {
   766  	if err := os.Chdir(camliRoot); err != nil {
   767  		log.Fatalf("Could not cd to %v: %v", camliRoot, err)
   768  	}
   769  	// Clean up Camlistore's hermetic gopaths
   770  	cleanBuildGopaths()
   771  
   772  	if *verbose {
   773  		tsk := newTask("go", "version")
   774  		out, err := tsk.run()
   775  		tsk.hidden = true
   776  		if err != nil {
   777  			return fmt.Errorf("failed to run 'go version': %v", err)
   778  		}
   779  		out = strings.TrimRight(out, "\n")
   780  		dbg.Printf("Building Camlistore with: %v\n", out)
   781  	}
   782  	if _, err := newTaskFrom(buildCamliCmd).run(); err != nil {
   783  		return err
   784  	}
   785  	return nil
   786  }
   787  
   788  func runCamli() error {
   789  	if err := os.Chdir(camliRoot); err != nil {
   790  		log.Fatal(err)
   791  	}
   792  
   793  	t := newTaskFrom(runCamliCmd)
   794  	dbg.Println(t.String())
   795  	cmd := exec.Command(t.Program, t.Args...)
   796  	var output []byte
   797  	errc := make(chan error, 1)
   798  	t.Start = time.Now()
   799  	go func() {
   800  		var err error
   801  		output, err = cmd.CombinedOutput()
   802  		if err != nil {
   803  			err = fmt.Errorf("%v: %v", err, string(output))
   804  		}
   805  		errc <- err
   806  	}()
   807  	select {
   808  	case err := <-errc:
   809  		t.Err = fmt.Sprintf("%v terminated early:\n%v\n", t.String(), err)
   810  		biSuitelk.Lock()
   811  		currentTestSuite.addRun(t)
   812  		biSuitelk.Unlock()
   813  		log.Println(t.Err)
   814  		return t
   815  	case <-time.After(warmup):
   816  		biSuitelk.Lock()
   817  		currentTestSuite.addRun(t)
   818  		camliProc = cmd.Process
   819  		biSuitelk.Unlock()
   820  		dbg.Printf("%v running OK so far\n", t.String())
   821  	}
   822  	return nil
   823  }
   824  
   825  func killCamli() {
   826  	if camliProc == nil {
   827  		return
   828  	}
   829  	dbg.Println("killing Camlistore server")
   830  	if err := camliProc.Kill(); err != nil {
   831  		log.Fatalf("Could not kill server with pid %v: %v", camliProc.Pid, err)
   832  	}
   833  	camliProc = nil
   834  	dbg.Println("")
   835  }
   836  
   837  func hitCamliUi() error {
   838  	if err := hitURL("http://localhost:3179/ui/"); err != nil {
   839  		return fmt.Errorf("could not reach camlistored UI page (dead server?): %v", err)
   840  	}
   841  	return nil
   842  }
   843  
   844  func hitURL(url string) (err error) {
   845  	tsk := newTask("http.Get", url)
   846  	defer func() {
   847  		if err != nil {
   848  			tsk.Err = fmt.Sprintf("%v", err)
   849  		}
   850  		biSuitelk.Lock()
   851  		currentTestSuite.addRun(tsk)
   852  		biSuitelk.Unlock()
   853  	}()
   854  	dbg.Println(tsk.String())
   855  	tsk.Start = time.Now()
   856  	var resp *http.Response
   857  	resp, err = http.Get(url)
   858  	if err != nil {
   859  		return fmt.Errorf("%v: %v\n", tsk.String(), err)
   860  	}
   861  	defer resp.Body.Close()
   862  	if resp.StatusCode != 200 {
   863  		return fmt.Errorf("%v, got StatusCode: %d\n", tsk.String(), resp.StatusCode)
   864  	}
   865  	return nil
   866  }
   867  
   868  func camputOne(vivify bool) error {
   869  	if err := os.Chdir(camliRoot); err != nil {
   870  		log.Fatalf("Could not cd to %v: %v", camliRoot, err)
   871  	}
   872  
   873  	// clean up camput caches
   874  	if err := os.RemoveAll(camputCacheDir); err != nil {
   875  		log.Fatalf("Problem cleaning up camputCacheDir %v: %v", camputCacheDir, err)
   876  	}
   877  
   878  	// push the file to camli
   879  	tsk := newTaskFrom(camputCmd)
   880  	if vivify {
   881  		tsk = newTaskFrom(camputVivifyCmd)
   882  	}
   883  	out, err := tsk.run()
   884  	if err != nil {
   885  		return err
   886  	}
   887  	// TODO(mpl): parsing camput output is kinda weak.
   888  	firstSHA1 := regexp.MustCompile(`.*(sha1-[a-zA-Z0-9]+)\nsha1-[a-zA-Z0-9]+\nsha1-[a-zA-Z0-9]+\n.*`)
   889  	if vivify {
   890  		firstSHA1 = regexp.MustCompile(`.*(sha1-[a-zA-Z0-9]+)\n.*`)
   891  	}
   892  	m := firstSHA1.FindStringSubmatch(out)
   893  	if m == nil {
   894  		return fmt.Errorf("%v: unexpected camput output\n", tsk.String())
   895  	}
   896  	blobref := m[1]
   897  
   898  	// get the file's json to find out the file's blobref
   899  	tsk = newTask(camgetCmd.Program, append(camgetCmd.Args, blobref)...)
   900  	out, err = tsk.run()
   901  	if err != nil {
   902  		return err
   903  	}
   904  	blobrefPattern := regexp.MustCompile(`"blobRef": "(sha1-[a-zA-Z0-9]+)",\n.*`)
   905  	m = blobrefPattern.FindStringSubmatch(out)
   906  	if m == nil {
   907  		return fmt.Errorf("%v: unexpected camget output\n", tsk.String())
   908  	}
   909  	blobref = m[1]
   910  
   911  	// finally, get the file back
   912  	tsk = newTask(camgetCmd.Program, append(camgetCmd.Args, blobref)...)
   913  	out, err = tsk.run()
   914  	if err != nil {
   915  		return err
   916  	}
   917  
   918  	// and compare it with the original
   919  	wantFile := testFile[0]
   920  	if vivify {
   921  		wantFile = testFile[1]
   922  	}
   923  	fileContents, err := ioutil.ReadFile(wantFile)
   924  	if err != nil {
   925  		log.Fatalf("Could not read %v: %v", wantFile, err)
   926  	}
   927  	if string(fileContents) != out {
   928  		return fmt.Errorf("%v: contents fetched with camget differ from %v contents", tsk.String(), wantFile)
   929  	}
   930  	return nil
   931  }
   932  
   933  func camputMany() error {
   934  	err := os.Chdir(camliRoot)
   935  	if err != nil {
   936  		log.Fatalf("Could not cd to %v: %v", camliRoot, err)
   937  	}
   938  
   939  	// upload the full camli pkg tree
   940  	if _, err := newTaskFrom(camputFilenodesCmd).run(); err != nil {
   941  		return err
   942  	}
   943  	return nil
   944  }
   945  
   946  func runTests() error {
   947  	if err := os.Chdir(camliRoot); err != nil {
   948  		log.Fatal(err)
   949  	}
   950  	if _, err := newTaskFrom(runTestsCmd).run(); err != nil {
   951  		return err
   952  	}
   953  	return nil
   954  }
   955  
   956  const reportPrefix = "/report"
   957  
   958  func postToURL(u string, r io.Reader) (*http.Response, error) {
   959  	// Default to plain HTTP.
   960  	if !(strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://")) {
   961  		u = "http://" + u
   962  	}
   963  	uri, err := url.Parse(u)
   964  	if err != nil {
   965  		return nil, err
   966  	}
   967  
   968  	// If the URL explicitly specifies "/" or something else, we'll POST to
   969  	// that, otherwise default to build-time default.
   970  	if uri.Path == "" {
   971  		uri.Path = reportPrefix
   972  	}
   973  
   974  	// Save user/pass if specified in the URL.
   975  	user := uri.User
   976  	// But don't send user/pass in URL to server.
   977  	uri.User = nil
   978  
   979  	req, err := http.NewRequest("POST", uri.String(), r)
   980  	if err != nil {
   981  		return nil, err
   982  	}
   983  	req.Header.Set("Content-Type", "text/javascript")
   984  	// If user/pass set on original URL, set the auth header for the request.
   985  	if user != nil {
   986  		pass, ok := user.Password()
   987  		if !ok {
   988  			log.Println("Password not set for", user.Username(), "in", u)
   989  		}
   990  		req.SetBasicAuth(user.Username(), pass)
   991  	}
   992  	return client.Do(req)
   993  }
   994  
   995  func sendReport() {
   996  	biSuitelk.Lock()
   997  	// we make a copy so we can release the lock quickly enough
   998  	currentBiSuiteCpy := &biTestSuite{
   999  		Go1:   currentBiSuite.Go1,
  1000  		GoTip: currentBiSuite.GoTip,
  1001  	}
  1002  	biSuitelk.Unlock()
  1003  	masters := strings.Split(*masterHosts, ",")
  1004  	OSArch := *ourOS + "_" + *arch
  1005  	toReport := struct {
  1006  		OSArch string
  1007  		Ts     *biTestSuite
  1008  	}{
  1009  		OSArch: OSArch,
  1010  		Ts:     currentBiSuiteCpy,
  1011  	}
  1012  	for _, v := range masters {
  1013  		// TODO(mpl): ipv6 too I suppose. just make a IsLocalhost func or whatever.
  1014  		// probably can borrow something from camli code for that.
  1015  		if strings.HasPrefix(v, "localhost") || strings.HasPrefix(v, "127.0.0.1") {
  1016  			toReport.Ts.Local = true
  1017  		} else {
  1018  			toReport.Ts.Local = false
  1019  		}
  1020  		report, err := json.MarshalIndent(toReport, "", "  ")
  1021  		if err != nil {
  1022  			log.Printf("JSON serialization error: %v", err)
  1023  			return
  1024  		}
  1025  		r := bytes.NewReader(report)
  1026  		resp, err := postToURL(v, r)
  1027  		if err != nil {
  1028  			log.Printf("Could not send report: %v", err)
  1029  			continue
  1030  		}
  1031  		resp.Body.Close()
  1032  	}
  1033  }
  1034  
  1035  func progressHandler(w http.ResponseWriter, r *http.Request) {
  1036  	if r.Method != "GET" {
  1037  		log.Printf("Invalid method in progress handler: %v, want GET", r.Method)
  1038  		http.Error(w, "Invalid method", http.StatusMethodNotAllowed)
  1039  		return
  1040  	}
  1041  	if checkLastModified(w, r, lastModified) {
  1042  		return
  1043  	}
  1044  	biSuitelk.Lock()
  1045  	if currentBiSuite != nil {
  1046  		if currentTestSuite.IsTip {
  1047  			currentBiSuite.GoTip = *currentTestSuite
  1048  		} else {
  1049  			currentBiSuite.Go1 = *currentTestSuite
  1050  		}
  1051  	}
  1052  	sanitizeRevs()
  1053  	report, err := json.MarshalIndent(currentBiSuite, "", "  ")
  1054  	if err != nil {
  1055  		biSuitelk.Unlock()
  1056  		log.Printf("JSON serialization error: %v", err)
  1057  		http.Error(w, "internal error", http.StatusInternalServerError)
  1058  		return
  1059  	}
  1060  	biSuitelk.Unlock()
  1061  	_, err = io.Copy(w, bytes.NewReader(report))
  1062  	if err != nil {
  1063  		log.Printf("Could not send progress report: %v", err)
  1064  	}
  1065  }
  1066  
  1067  // modtime is the modification time of the resource to be served, or IsZero().
  1068  // return value is whether this request is now complete.
  1069  func checkLastModified(w http.ResponseWriter, r *http.Request, modtime time.Time) bool {
  1070  	if modtime.IsZero() {
  1071  		return false
  1072  	}
  1073  
  1074  	// The Date-Modified header truncates sub-second precision, so
  1075  	// use mtime < t+1s instead of mtime <= t to check for unmodified.
  1076  	if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) {
  1077  		h := w.Header()
  1078  		delete(h, "Content-Type")
  1079  		delete(h, "Content-Length")
  1080  		w.WriteHeader(http.StatusNotModified)
  1081  		return true
  1082  	}
  1083  	w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
  1084  	return false
  1085  }