perkeep/misc/buildbot/builder/builder.go

1086 lines
28 KiB
Go

/*
Copyright 2012 The Camlistore Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// The buildbot is Camlistore's continuous builder.
// This builder program is started by the master. It then rebuilds
// Go 1, GoTip, Camlistore, and runs a battery of tests for Camlistore.
// It then sends a report to the master and terminates.
// It can also respond to progress requests from the master.
package main
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"syscall"
"time"
"camlistore.org/pkg/osutil"
)
const (
interval = 60 * time.Second // polling frequency
warmup = 30 * time.Second // duration before we test if devcam server has started properly
)
var (
// TODO(mpl): use that one, same as in master.
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.")
arch = flag.String("arch", "", "The arch we report the master(s). Defaults to runtime.GOARCH.")
fakeTests = flag.Bool("faketests", false, "Run fast fake tests instead of the real ones, for faster debugging.")
help = flag.Bool("h", false, "show this help")
host = flag.String("host", "0.0.0.0:8081", "listening hostname and port")
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.")
ourOS = flag.String("os", "", "The OS we report the master(s). Defaults to runtime.GOOS.")
skipGo1Build = flag.Bool("skipgo1build", false, "skip initial go1 build, for debugging and quickly going to the next steps.")
verbose = flag.Bool("verbose", false, "print what's going on")
skipTLSCheck = flag.Bool("skiptlscheck", false, "accept any certificate presented by server when uploading results.")
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.")
)
var (
testFile = []string{"AUTHORS", "CONTRIBUTORS"}
cacheDir string
camliHeadHash string
camliRoot string
camputCacheDir string
client = http.DefaultClient
dbg *debugger
defaultPATH string
go1Dir string
goTipDir string
goTipHash string
biSuitelk sync.Mutex
currentTestSuite *testSuite
currentBiSuite *biTestSuite
// Process of the camlistore server, so we can kill it when
// we get killed ourselves.
camliProc *os.Process
// For "If-Modified-Since" requests asking for progress.
// Updated every time a new test task/run is added to the test suite.
lastModified time.Time
)
var devcamBin = filepath.Join("bin", "devcam")
var (
hgCloneGo1Cmd = newTask("hg", "clone", "-u", "release", "https://code.google.com/p/go")
hgCloneGoTipCmd = newTask("hg", "clone", "-u", "tip", "https://code.google.com/p/go")
hgPullCmd = newTask("hg", "pull")
hgUpdateCmd = newTask("hg", "update", "-C", "default")
hgLogCmd = newTask("hg", "log", "-r", "tip", "--template", "{node}")
hgConfigCmd = newTask("hg", "--config", "extensions.purge=", "purge", "--all")
gitCloneCmd = newTask("git", "clone", "https://camlistore.googlesource.com/camlistore")
gitResetCmd = newTask("git", "reset", "--hard")
gitCleanCmd = newTask("git", "clean", "-Xdf")
gitPullCmd = newTask("git", "pull")
gitRevCmd = newTask("git", "rev-parse", "HEAD")
buildGoCmd = newTask("./make.bash")
buildCamliCmd = newTask("go", "run", "make.go", "-v")
runTestsCmd = newTask(devcamBin, "test")
runCamliCmd = newTask(devcamBin, "server", "--wipe", "--mysql")
camgetCmd = newTask(devcamBin, "get")
camputCmd = newTask(devcamBin, "put", "file", "--permanode", testFile[0])
camputVivifyCmd = newTask(devcamBin, "put", "file", "--vivify", testFile[1])
camputFilenodesCmd = newTask(devcamBin, "put", "file", "--filenodes", "pkg")
)
func usage() {
fmt.Fprintf(os.Stderr, "\t builderBot \n")
flag.PrintDefaults()
os.Exit(2)
}
type debugger struct {
lg *log.Logger
}
func (dbg *debugger) Printf(format string, v ...interface{}) {
if dbg != nil && *verbose {
dbg.lg.Printf(format, v...)
}
}
func (dbg *debugger) Println(v ...interface{}) {
if v == nil {
return
}
if dbg != nil && *verbose {
dbg.lg.Println(v...)
}
}
type task struct {
Program string
Args []string
Start time.Time
Duration time.Duration
Err string
hidden bool
}
func newTask(program string, args ...string) *task {
return &task{Program: program, Args: args}
}
// because sometimes we do not want to modify the tsk template
// so we make a copy of it
func newTaskFrom(tsk *task) *task {
return newTask(tsk.Program, tsk.Args...)
}
func (t *task) String() string {
return fmt.Sprintf("%v %v", t.Program, t.Args)
}
func (t *task) Error() string {
return t.Err
}
func (t *task) run() (string, error) {
var err error
defer func() {
t.Duration = time.Now().Sub(t.Start)
if !t.hidden {
biSuitelk.Lock()
currentTestSuite.addRun(t)
biSuitelk.Unlock()
}
}()
dbg.Println(t.String())
cmd := exec.Command(t.Program, t.Args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
t.Start = time.Now()
setTaskErr := func() {
var sout, serr string
if sout = stdout.String(); sout == "" {
sout = "(empty)"
}
if serr = stderr.String(); serr == "" {
serr = "(empty)"
}
t.Err = fmt.Sprintf("Stdout:\n%s\n\nStderr:\n%s", sout, serr)
if err != nil {
t.Err = fmt.Sprintf("%v\n\n%v", err, t.Err)
}
}
// TODO(mpl, wathiede): make it learn about task durations.
errc := make(chan error)
go func() {
errc <- cmd.Run()
}()
if *taskLifespan > 0 {
select {
case <-time.After(time.Duration(*taskLifespan) * time.Second):
setTaskErr()
t.Err = fmt.Sprintf("%v\n\nTask %q took too long. Giving up after %v seconds.\n",
t.Err, t.String(), *taskLifespan)
if cmd.Process != nil {
if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
dbg.Printf("Could not terminate process for task %q: %v", t.String(), err)
}
}
return "", t
case err = <-errc:
break
}
} else {
err = <-errc
}
if err != nil {
setTaskErr()
return "", t
}
return stdout.String(), nil
}
type testSuite struct {
Run []*task
CamliHash string
GoHash string
Err string
Start time.Time
IsTip bool
}
func (ts *testSuite) addRun(tsk *task) {
if ts == nil {
panic("Tried adding a run to a nil testSuite")
}
if ts.Start.IsZero() && len(ts.Run) == 0 {
ts.Start = tsk.Start
}
if tsk.Err != "" && ts.Err == "" {
ts.Err = tsk.Err
}
ts.Run = append(ts.Run, tsk)
lastModified = time.Now()
}
type biTestSuite struct {
Local bool
Go1 testSuite
GoTip testSuite
}
func main() {
flag.Usage = usage
flag.Parse()
if *help {
usage()
}
if *skipTLSCheck {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{Transport: tr}
}
go handleSignals()
http.HandleFunc("/progress", progressHandler)
go func() {
log.Printf("Now starting to listen on %v", *host)
if err := http.ListenAndServe(*host, nil); err != nil {
log.Fatalf("Could not start listening on %v: %v", *host, err)
}
}()
setup()
biSuitelk.Lock()
currentBiSuite = &biTestSuite{}
biSuitelk.Unlock()
for _, isTip := range [2]bool{false, true} {
currentTestSuite = &testSuite{
Run: make([]*task, 0, 1),
IsTip: isTip,
Start: time.Now(),
}
// We prepare the Go tip tree as soon as in the Go 1 run, so
// we can set GoTipHash in the test suite.
if !isTip {
if err := prepGoTipTree(); err != nil {
endOfSuite(err)
// If we failed with that in the Go 1 run, we just restart
// from scratch instead of trying to cope with it in the Gotip run.
// Same for buildGoTip and prepCamliTree.
break
}
}
biSuitelk.Lock()
currentTestSuite.GoHash = goTipHash
biSuitelk.Unlock()
if isTip && !*fakeTests {
if err := buildGoTip(); err != nil {
endOfSuite(err)
break
}
}
if err := prepCamliTree(isTip); err != nil {
endOfSuite(err)
break
}
biSuitelk.Lock()
currentTestSuite.CamliHash = camliHeadHash
biSuitelk.Unlock()
restorePATH()
goDir := go1Dir
if isTip {
goDir = goTipDir
}
switchGo(goDir)
if *fakeTests {
if err := fakeRun(); err != nil {
endOfSuite(err)
continue
}
endOfSuite(nil)
if isTip {
break
}
continue
}
if err := buildCamli(); err != nil {
endOfSuite(err)
continue
}
if err := runTests(); err != nil {
endOfSuite(err)
continue
}
if err := runCamli(); err != nil {
endOfSuite(err)
continue
}
if err := hitCamliUi(); err != nil {
endOfSuite(err)
continue
}
doVivify := false
if err := camputOne(doVivify); err != nil {
endOfSuite(err)
continue
}
doVivify = true
if err := camputOne(doVivify); err != nil {
endOfSuite(err)
continue
}
if err := camputMany(); err != nil {
endOfSuite(err)
continue
}
endOfSuite(nil)
}
sanitizeRevs()
sendReport()
}
func sanitizeRevs() {
if currentBiSuite == nil {
return
}
if currentBiSuite.GoTip.Start.IsZero() {
return
}
if currentBiSuite.GoTip.CamliHash == "" && currentBiSuite.Go1.CamliHash == "" {
dbg.Printf("CamliHash not set in both Go1 and GoTip test suites")
return
}
if currentBiSuite.GoTip.CamliHash == "" && currentBiSuite.Go1.CamliHash == "" {
dbg.Printf("GoHash not set in both Go1 and GoTip test suites")
return
}
if currentBiSuite.GoTip.CamliHash != "" && currentBiSuite.Go1.CamliHash != "" &&
currentBiSuite.GoTip.CamliHash != currentBiSuite.Go1.CamliHash {
panic("CamliHash in GoTip suite and in Go1 suite differ; should not happen.")
}
if currentBiSuite.GoTip.GoHash != "" && currentBiSuite.Go1.GoHash != "" &&
currentBiSuite.GoTip.GoHash != currentBiSuite.Go1.GoHash {
panic("GoHash in GoTip suite and in Go1 suite differ; should not happen.")
}
if currentBiSuite.GoTip.GoHash == "" {
currentBiSuite.GoTip.GoHash = currentBiSuite.Go1.GoHash
}
if currentBiSuite.Go1.GoHash == "" {
currentBiSuite.Go1.GoHash = currentBiSuite.GoTip.GoHash
}
if currentBiSuite.GoTip.CamliHash == "" {
currentBiSuite.GoTip.CamliHash = currentBiSuite.Go1.CamliHash
}
if currentBiSuite.Go1.CamliHash == "" {
currentBiSuite.Go1.CamliHash = currentBiSuite.GoTip.CamliHash
}
}
func endOfSuite(err error) {
biSuitelk.Lock()
defer biSuitelk.Unlock()
if currentTestSuite.IsTip {
currentBiSuite.GoTip = *currentTestSuite
} else {
currentBiSuite.Go1 = *currentTestSuite
}
killCamli()
if err != nil {
log.Printf("%v", err)
} else {
dbg.Println("All good.")
}
}
func masterHostsReader(r io.Reader) ([]string, error) {
hosts := []string{}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
l := scanner.Text()
u, err := url.Parse(l)
if err != nil {
return nil, err
}
if u.Host == "" {
return nil, fmt.Errorf("URL missing Host: %q", l)
}
hosts = append(hosts, u.String())
}
if err := scanner.Err(); err != nil {
return nil, err
}
return hosts, nil
}
var masterHostsFile = filepath.Join(osutil.CamliConfigDir(), "builderbot-config")
func loadMasterHosts() error {
r, err := os.Open(masterHostsFile)
if err != nil {
return err
}
defer r.Close()
hosts, err := masterHostsReader(r)
if err != nil {
return err
}
if *masterHosts != "" {
*masterHosts += ","
}
log.Println("Additional host(s) to send our build reports:", hosts)
*masterHosts += strings.Join(hosts, ",")
return nil
}
func setup() {
var err error
defaultPATH = os.Getenv("PATH")
if defaultPATH == "" {
log.Fatal("PATH not set")
}
log.SetPrefix("BUILDER: ")
dbg = &debugger{log.New(os.Stderr, "BUILDER: ", log.LstdFlags)}
err = loadMasterHosts()
if err != nil {
if os.IsNotExist(err) {
log.Printf("%q missing. No additional remote master(s) will receive build report.", masterHostsFile)
} else {
log.Println("Error parsing master hosts file %q: %v",
masterHostsFile, err)
}
}
// the OS we run on
if *ourOS == "" {
*ourOS = runtime.GOOS
if *ourOS == "" {
// Can this happen? I don't think so, but just in case...
panic("runtime.GOOS was not set")
}
}
// the arch we run on
if *arch == "" {
*arch = runtime.GOARCH
if *arch == "" {
panic("runtime.GOARCH was not set")
}
}
// cacheDir
cacheDir = filepath.Join(os.TempDir(), "camlibot-cache")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
log.Fatalf("Could not create cache dir %v: %v", cacheDir, err)
}
// get go1 and gotip source
if err := os.Chdir(cacheDir); err != nil {
log.Fatalf("Could not cd to %v: %v", cacheDir, err)
}
go1Dir, err = filepath.Abs("go1")
if err != nil {
log.Fatalf("Problem with Go 1 dir: %v", err)
}
goTipDir, err = filepath.Abs("gotip")
if err != nil {
log.Fatalf("Problem with Go tip dir: %v", err)
}
for _, goDir := range []string{go1Dir, goTipDir} {
// if go dirs exist, just reuse them
if _, err := os.Stat(goDir); err != nil {
if !os.IsNotExist(err) {
log.Fatalf("Could not stat %v: %v", goDir, err)
}
// go1/gotip dir not here, let's clone it.
hgCloneCmd := hgCloneGo1Cmd
if goDir == goTipDir {
hgCloneCmd = hgCloneGoTipCmd
}
tsk := newTask(hgCloneCmd.Program, hgCloneCmd.Args...)
tsk.hidden = true
if _, err := tsk.run(); err != nil {
log.Fatalf("Could not hg clone %v: %v", goDir, err)
}
if err := os.Rename("go", goDir); err != nil {
log.Fatalf("Could not rename go dir into %v: %v", goDir, err)
}
}
}
if !*skipGo1Build {
// build Go1
if err := buildGo1(); err != nil {
log.Fatal(err)
}
}
// get camlistore source
if err := os.Chdir(cacheDir); err != nil {
log.Fatal("Could not cd to %v: %v", cacheDir, err)
}
camliRoot, err = filepath.Abs("camlistore.org")
if err != nil {
log.Fatal(err)
}
// if camlistore dir already exists, reuse it
if _, err := os.Stat(camliRoot); err != nil {
if !os.IsNotExist(err) {
log.Fatalf("Could not stat %v: %v", camliRoot, err)
}
cloneCmd := newTask(gitCloneCmd.Program, append(gitCloneCmd.Args, camliRoot)...)
cloneCmd.hidden = true
if _, err := cloneCmd.run(); err != nil {
log.Fatalf("Could not git clone into %v: %v", camliRoot, err)
}
}
// recording camput cache dir, so we can clean it up fast everytime
homeDir := os.Getenv("HOME")
if homeDir == "" {
log.Fatal("HOME not set")
}
camputCacheDir = filepath.Join(homeDir, ".cache", "camlistore")
}
func buildGo1() error {
if err := os.Chdir(filepath.Join(go1Dir, "src")); err != nil {
log.Fatalf("Could not cd to %v: %v", go1Dir, err)
}
tsk := newTask(buildGoCmd.Program, buildGoCmd.Args...)
tsk.hidden = true
if _, err := tsk.run(); err != nil {
return err
}
return nil
}
func handleSignals() {
c := make(chan os.Signal)
sigs := []os.Signal{syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT}
signal.Notify(c, sigs...)
for {
sig := <-c
sysSig, ok := sig.(syscall.Signal)
if !ok {
log.Fatal("Not a unix signal")
}
switch sysSig {
case syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
log.Printf("Received %v signal, terminating.", sig)
killCamli()
os.Exit(0)
default:
panic("should not get other signals here")
}
}
}
var plausibleHashRx = regexp.MustCompile(`^[a-f0-9]{40}$`)
func prepGoTipTree() error {
if err := os.Chdir(goTipDir); err != nil {
return fmt.Errorf("Could not cd to %v: %v", goTipDir, err)
}
tasks := []*task{
newTaskFrom(hgPullCmd),
newTaskFrom(hgUpdateCmd),
newTaskFrom(hgLogCmd),
newTaskFrom(hgConfigCmd),
}
hash := ""
for _, t := range tasks {
out, err := t.run()
if err != nil {
return fmt.Errorf("Could not prepare the Go tip tree with %v: %v", t.String(), err)
}
if t.String() == hgLogCmd.String() {
hash = strings.TrimRight(out, "\n")
}
}
if !plausibleHashRx.MatchString(hash) {
return fmt.Errorf("Go rev %q does not look like an hg hash.", hash)
}
goTipHash = hash
dbg.Println("current head in go tree: " + goTipHash)
return nil
}
func buildGoTip() error {
srcDir := filepath.Join(goTipDir, "src")
if err := os.Chdir(srcDir); err != nil {
return fmt.Errorf("Could not cd to %v: %v", srcDir, err)
}
if _, err := newTaskFrom(buildGoCmd).run(); err != nil {
return err
}
return nil
}
func prepCamliTree(isTip bool) error {
if err := os.Chdir(camliRoot); err != nil {
return fmt.Errorf("Could not cd to %v: %v", camliRoot, err)
}
rev := "HEAD"
if isTip {
if !plausibleHashRx.MatchString(camliHeadHash) {
// the run with Go 1 should have taken care of setting camliHeadHash
return errors.New("camliHeadHash hasn't been set properly in the Go 1 run")
}
// we reset to the rev that was noted at the previous run with Go 1
// because we want to do both runs at the same rev
rev = camliHeadHash
}
resetCmd := newTask(gitResetCmd.Program, append(gitResetCmd.Args, rev)...)
tasks := []*task{
resetCmd,
newTaskFrom(gitCleanCmd),
}
for _, t := range tasks {
_, err := t.run()
if err != nil {
return fmt.Errorf("Could not prepare the Camli tree with %v: %v\n", t.String(), err)
}
}
if isTip {
// We only need to pull and get the camli head hash when in the Go 1 run
return nil
}
tasks = []*task{
newTaskFrom(gitPullCmd),
newTaskFrom(gitRevCmd),
}
hash := ""
for _, t := range tasks {
out, err := t.run()
if err != nil {
return fmt.Errorf("Could not prepare the Camli tree with %v: %v\n", t.String(), err)
}
hash = strings.TrimRight(out, "\n")
}
if !plausibleHashRx.MatchString(hash) {
return fmt.Errorf("Camlistore rev %q does not look like a git hash.", hash)
}
camliHeadHash = hash
return nil
}
func restorePATH() {
err := os.Setenv("PATH", defaultPATH)
if err != nil {
log.Fatalf("Could not set PATH to %v: %v", defaultPATH, err)
}
}
func switchGo(goDir string) {
if runtime.GOOS == "plan9" {
panic("plan 9 not unsupported")
}
gobin := filepath.Join(goDir, "bin", "go")
if _, err := os.Stat(gobin); err != nil {
log.Fatalf("Could not stat 'go' bin at %q: %v", gobin, err)
}
p := filepath.Join(goDir, "bin") + string(filepath.ListSeparator) + defaultPATH
if err := os.Setenv("PATH", p); err != nil {
log.Fatalf("Could not set PATH to %v: %v", p, err)
}
if err := os.Setenv("GOROOT", goDir); err != nil {
log.Fatalf("Could not set GOROOT to %v: %v", goDir, err)
}
}
func cleanBuildGopaths() {
tmpDir := filepath.Join(camliRoot, "tmp")
if _, err := os.Stat(tmpDir); err != nil {
if !os.IsNotExist(err) {
log.Fatalf("Could not stat %v: %v", tmpDir, err)
}
// Does not exist, we only have to recreate it
// TODO(mpl): hmm maybe it should be an error that
// it does not exist, since it also contains the
// closure stuff?
if err := os.MkdirAll(tmpDir, 0755); err != nil {
log.Fatalf("Could not mkdir %v: %v", tmpDir, err)
}
return
}
f, err := os.Open(tmpDir)
if err != nil {
log.Fatalf("Could not open %v: %v", tmpDir, err)
}
defer f.Close()
names, err := f.Readdirnames(-1)
if err != nil {
log.Fatal("Could not read %v: %v", tmpDir, err)
}
for _, v := range names {
if strings.HasPrefix(v, "build-gopath") {
if err := os.RemoveAll(filepath.Join(tmpDir, v)); err != nil {
log.Fatalf("Could not remove %v: %v", v, err)
}
}
}
}
func fakeRun() error {
if _, err := newTask("sleep", "1").run(); err != nil {
return err
}
return nil
}
func buildCamli() error {
if err := os.Chdir(camliRoot); err != nil {
log.Fatalf("Could not cd to %v: %v", camliRoot, err)
}
// Clean up Camlistore's hermetic gopaths
cleanBuildGopaths()
if *verbose {
tsk := newTask("go", "version")
out, err := tsk.run()
tsk.hidden = true
if err != nil {
return fmt.Errorf("failed to run 'go version': %v", err)
}
out = strings.TrimRight(out, "\n")
dbg.Printf("Building Camlistore with: %v\n", out)
}
if _, err := newTaskFrom(buildCamliCmd).run(); err != nil {
return err
}
return nil
}
func runCamli() error {
if err := os.Chdir(camliRoot); err != nil {
log.Fatal(err)
}
t := newTaskFrom(runCamliCmd)
dbg.Println(t.String())
cmd := exec.Command(t.Program, t.Args...)
var output []byte
errc := make(chan error, 1)
t.Start = time.Now()
go func() {
var err error
output, err = cmd.CombinedOutput()
if err != nil {
err = fmt.Errorf("%v: %v", err, string(output))
}
errc <- err
}()
select {
case err := <-errc:
t.Err = fmt.Sprintf("%v terminated early:\n%v\n", t.String(), err)
biSuitelk.Lock()
currentTestSuite.addRun(t)
biSuitelk.Unlock()
log.Println(t.Err)
return t
case <-time.After(warmup):
biSuitelk.Lock()
currentTestSuite.addRun(t)
camliProc = cmd.Process
biSuitelk.Unlock()
dbg.Printf("%v running OK so far\n", t.String())
}
return nil
}
func killCamli() {
if camliProc == nil {
return
}
dbg.Println("killing Camlistore server")
if err := camliProc.Kill(); err != nil {
log.Fatalf("Could not kill server with pid %v: %v", camliProc.Pid, err)
}
camliProc = nil
dbg.Println("")
}
func hitCamliUi() error {
if err := hitURL("http://localhost:3179/ui/"); err != nil {
return fmt.Errorf("could not reach camlistored UI page (dead server?): %v", err)
}
return nil
}
func hitURL(url string) (err error) {
tsk := newTask("http.Get", url)
defer func() {
if err != nil {
tsk.Err = fmt.Sprintf("%v", err)
}
biSuitelk.Lock()
currentTestSuite.addRun(tsk)
biSuitelk.Unlock()
}()
dbg.Println(tsk.String())
tsk.Start = time.Now()
var resp *http.Response
resp, err = http.Get(url)
if err != nil {
return fmt.Errorf("%v: %v\n", tsk.String(), err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("%v, got StatusCode: %d\n", tsk.String(), resp.StatusCode)
}
return nil
}
func camputOne(vivify bool) error {
if err := os.Chdir(camliRoot); err != nil {
log.Fatalf("Could not cd to %v: %v", camliRoot, err)
}
// clean up camput caches
if err := os.RemoveAll(camputCacheDir); err != nil {
log.Fatalf("Problem cleaning up camputCacheDir %v: %v", camputCacheDir, err)
}
// push the file to camli
tsk := newTaskFrom(camputCmd)
if vivify {
tsk = newTaskFrom(camputVivifyCmd)
}
out, err := tsk.run()
if err != nil {
return err
}
// TODO(mpl): parsing camput output is kinda weak.
firstSHA1 := regexp.MustCompile(`.*(sha1-[a-zA-Z0-9]+)\nsha1-[a-zA-Z0-9]+\nsha1-[a-zA-Z0-9]+\n.*`)
if vivify {
firstSHA1 = regexp.MustCompile(`.*(sha1-[a-zA-Z0-9]+)\n.*`)
}
m := firstSHA1.FindStringSubmatch(out)
if m == nil {
return fmt.Errorf("%v: unexpected camput output\n", tsk.String())
}
blobref := m[1]
// get the file's json to find out the file's blobref
tsk = newTask(camgetCmd.Program, append(camgetCmd.Args, blobref)...)
out, err = tsk.run()
if err != nil {
return err
}
blobrefPattern := regexp.MustCompile(`"blobRef": "(sha1-[a-zA-Z0-9]+)",\n.*`)
m = blobrefPattern.FindStringSubmatch(out)
if m == nil {
return fmt.Errorf("%v: unexpected camget output\n", tsk.String())
}
blobref = m[1]
// finally, get the file back
tsk = newTask(camgetCmd.Program, append(camgetCmd.Args, blobref)...)
out, err = tsk.run()
if err != nil {
return err
}
// and compare it with the original
wantFile := testFile[0]
if vivify {
wantFile = testFile[1]
}
fileContents, err := ioutil.ReadFile(wantFile)
if err != nil {
log.Fatalf("Could not read %v: %v", wantFile, err)
}
if string(fileContents) != out {
return fmt.Errorf("%v: contents fetched with camget differ from %v contents", tsk.String(), wantFile)
}
return nil
}
func camputMany() error {
err := os.Chdir(camliRoot)
if err != nil {
log.Fatalf("Could not cd to %v: %v", camliRoot, err)
}
// upload the full camli pkg tree
if _, err := newTaskFrom(camputFilenodesCmd).run(); err != nil {
return err
}
return nil
}
func runTests() error {
if err := os.Chdir(camliRoot); err != nil {
log.Fatal(err)
}
if _, err := newTaskFrom(runTestsCmd).run(); err != nil {
return err
}
return nil
}
const reportPrefix = "/report"
func postToURL(u string, r io.Reader) (*http.Response, error) {
// Default to plain HTTP.
if !(strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://")) {
u = "http://" + u
}
uri, err := url.Parse(u)
if err != nil {
return nil, err
}
// If the URL explicitly specifies "/" or something else, we'll POST to
// that, otherwise default to build-time default.
if uri.Path == "" {
uri.Path = reportPrefix
}
// Save user/pass if specified in the URL.
user := uri.User
// But don't send user/pass in URL to server.
uri.User = nil
req, err := http.NewRequest("POST", uri.String(), r)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "text/javascript")
// If user/pass set on original URL, set the auth header for the request.
if user != nil {
pass, ok := user.Password()
if !ok {
log.Println("Password not set for", user.Username(), "in", u)
}
req.SetBasicAuth(user.Username(), pass)
}
return client.Do(req)
}
func sendReport() {
biSuitelk.Lock()
// we make a copy so we can release the lock quickly enough
currentBiSuiteCpy := &biTestSuite{
Go1: currentBiSuite.Go1,
GoTip: currentBiSuite.GoTip,
}
biSuitelk.Unlock()
masters := strings.Split(*masterHosts, ",")
OSArch := *ourOS + "_" + *arch
toReport := struct {
OSArch string
Ts *biTestSuite
}{
OSArch: OSArch,
Ts: currentBiSuiteCpy,
}
for _, v := range masters {
// TODO(mpl): ipv6 too I suppose. just make a IsLocalhost func or whatever.
// probably can borrow something from camli code for that.
if strings.HasPrefix(v, "localhost") || strings.HasPrefix(v, "127.0.0.1") {
toReport.Ts.Local = true
} else {
toReport.Ts.Local = false
}
report, err := json.MarshalIndent(toReport, "", " ")
if err != nil {
log.Printf("JSON serialization error: %v", err)
return
}
r := bytes.NewReader(report)
resp, err := postToURL(v, r)
if err != nil {
log.Printf("Could not send report: %v", err)
continue
}
resp.Body.Close()
}
}
func progressHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
log.Printf("Invalid method in progress handler: %v, want GET", r.Method)
http.Error(w, "Invalid method", http.StatusMethodNotAllowed)
return
}
if checkLastModified(w, r, lastModified) {
return
}
biSuitelk.Lock()
if currentBiSuite != nil {
if currentTestSuite.IsTip {
currentBiSuite.GoTip = *currentTestSuite
} else {
currentBiSuite.Go1 = *currentTestSuite
}
}
sanitizeRevs()
report, err := json.MarshalIndent(currentBiSuite, "", " ")
if err != nil {
biSuitelk.Unlock()
log.Printf("JSON serialization error: %v", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
biSuitelk.Unlock()
_, err = io.Copy(w, bytes.NewReader(report))
if err != nil {
log.Printf("Could not send progress report: %v", err)
}
}
// modtime is the modification time of the resource to be served, or IsZero().
// return value is whether this request is now complete.
func checkLastModified(w http.ResponseWriter, r *http.Request, modtime time.Time) bool {
if modtime.IsZero() {
return false
}
// The Date-Modified header truncates sub-second precision, so
// use mtime < t+1s instead of mtime <= t to check for unmodified.
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) {
h := w.Header()
delete(h, "Content-Type")
delete(h, "Content-Length")
w.WriteHeader(http.StatusNotModified)
return true
}
w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
return false
}