<commit_before>package main

import (
	"flag"
	"fmt"
	"log"
	"sort"
	"sync"

	"time"

	"math"

	"encoding/json"

	"github.com/jmoiron/sqlx"
	"github.com/vincentserpoul/playwithsql/dbhandler"
	"github.com/vincentserpoul/playwithsql/status"
)

// Results to be returned
type Results struct {
	DBType       string
	MaxConns     int
	Date         time.Time
	BenchResults []BenchResult
}

// BenchResult data
type BenchResult struct {
	Action     string
	Loops      int
	PauseTime  time.Duration
	Errors     int
	Min        time.Duration
	Max        time.Duration
	Median     time.Duration
	StandDev   time.Duration
	Throughput int
}

func main() {

	// Flags
	dbName := "playwithsql"
	dbType := flag.String("db", "mysql", "type of db to bench: mysql, cockroachdb, postgres")
	dbHost := flag.String("host", "127.0.0.1", "host IP")
	loops := flag.Int("loops", 100, "number of loops")
	maxConns := flag.Int("maxconns", 10, "number of max connections")
	flag.Parse()

	db, err := dbhandler.Get(*dbType, *dbHost, dbName)
	if err != nil {
		log.Fatalf("%s - %s - %s, \n%v", *dbType, *dbHost, dbName, err)
	}

	// Connection
	islatestSQLLink := status.GetSQLIntImpl(*dbType)
	err = islatestSQLLink.MigrateDown(db)
	if err != nil {
		log.Fatalf("%v", err)
	}
	err = islatestSQLLink.MigrateUp(db)
	if err != nil {
		log.Fatalf("%v", err)
	}

	// Number of max connections
	// TODO set the param in the db config
	db.SetMaxOpenConns(*maxConns)
	db.SetMaxIdleConns(*maxConns)

	var results = Results{
		DBType:   *dbType,
		MaxConns: *maxConns,
		Date:     time.Now(),
	}

	// Create
	createResults, testEntityoneIDs, err := BenchmarkCreate(*loops, db, islatestSQLLink)
	if err != nil {
		log.Fatalf("%v", err)
	}
	results.BenchResults = append(results.BenchResults, createResults)

	// Update
	updateResults, err := BenchmarkUpdateStatus(*loops, db, islatestSQLLink, testEntityoneIDs)
	if err != nil {
		log.Fatalf("%v", err)
	}
	results.BenchResults = append(results.BenchResults, updateResults)

	// Select by status
	selectByStatusResults, err := BenchmarkSelectEntityoneByStatus(*loops, db, islatestSQLLink)
	if err != nil {
		log.Fatalf("%v", err)
	}
	results.BenchResults = append(results.BenchResults, selectByStatusResults)

	// Select by PK
	selectByPKResults, err := BenchmarkSelectEntityoneOneByPK(*loops, db, islatestSQLLink, testEntityoneIDs)
	if err != nil {
		log.Fatalf("%v", err)
	}
	results.BenchResults = append(results.BenchResults, selectByPKResults)

	jsonResults, err := json.Marshal(results)
	if err != nil {
		log.Fatalf("%v", err)
	}

	fmt.Printf("%s\n", jsonResults)
}

// BenchmarkCreate will loop a loops number of time and give the resulting time taken
func BenchmarkCreate(
	loops int, dbConn *sqlx.DB, benchSQLLink *status.SQLIntImpl,
) (
	results BenchResult,
	testEntityoneIDs []int64,
	err error,
) {
	entityIDsC := make(chan int64)

	var latencies []time.Duration
	var errCount int
	latenciesC, errorC := handleResults(&latencies, &errCount)

	before := time.Now()
	var wg sync.WaitGroup

	// Pause time
	dynPauseTime := time.Duration(1 * time.Millisecond)
	dynPauseTimeC := dynPauseTimeInit(&dynPauseTime)
	defer close(dynPauseTimeC)

	for i := 0; i < loops; i++ {
		time.Sleep(dynPauseTime)
		wg.Add(1)
		go func(wg *sync.WaitGroup) {
			defer wg.Done()
			var e status.Entityone
			beforeLocal := time.Now()
			ok := false
			var errCr error
			retryCount := 0
			for retryCount < 3 && !ok {
				// For each error, we add some pause time
				errCr = e.Create(dbConn, benchSQLLink)
				if errCr != nil {
					retryCount++
					time.Sleep(dynPauseTime)
					dynPauseTimeC <- time.Duration(1 * time.Millisecond)
				} else {
					ok = true
				}
			}
			if errCr != nil {
				errorC <- errCr
			} else {
				latenciesC <- time.Since(beforeLocal)
				entityIDsC <- e.ID
				// If no error, we increment down a little bit
				dynPauseTimeC <- time.Duration(-1 * time.Millisecond)
			}
		}(&wg)
	}

	// Receive the entityIDs
	go func() {
		for entityID := range entityIDsC {
			testEntityoneIDs = append(testEntityoneIDs, entityID)
		}
	}()

	wg.Wait()
	timeTaken := time.Since(before)

	return BenchResult{
			Action:     "create",
			Loops:      loops,
			PauseTime:  dynPauseTime,
			Errors:     errCount,
			Min:        getMin(latencies),
			Max:        getMax(latencies),
			Median:     getMedian(latencies),
			StandDev:   getStandardDeviation(latencies),
			Throughput: int(float64(loops) / timeTaken.Seconds()),
		},
		testEntityoneIDs,
		nil
}

// BenchmarkUpdateStatus benchmark for status updates (include deletes)
func BenchmarkUpdateStatus(
	loops int, dbConn *sqlx.DB, benchSQLLink *status.SQLIntImpl, testEntityoneIDs []int64,
) (
	results BenchResult,
	err error,
) {
	if len(testEntityoneIDs) == 0 {
		return results, fmt.Errorf("BenchmarkUpdateStatus: no entity created, nothing to update")
	}

	var latencies []time.Duration
	var errCount int
	latenciesC, errorC := handleResults(&latencies, &errCount)

	before := time.Now()
	var wg sync.WaitGroup

	// Pause time
	dynPauseTime := time.Duration(1 * time.Millisecond)
	dynPauseTimeC := dynPauseTimeInit(&dynPauseTime)
	defer close(dynPauseTimeC)

	for i := 0; i < loops; i++ {
		time.Sleep(dynPauseTime)
		wg.Add(1)
		go func(wg *sync.WaitGroup) {
			defer wg.Done()
			var e status.Entityone
			e.ID = testEntityoneIDs[i%len(testEntityoneIDs)]
			beforeLocal := time.Now()
			ok := false
			var errU error
			retryCount := 0
			for retryCount < 3 && !ok {
				errU = e.UpdateStatus(dbConn, benchSQLLink, status.ActionCancel, status.StatusCancelled)
				if errU != nil {
					retryCount++
					time.Sleep(dynPauseTime)
					dynPauseTimeC <- time.Duration(1 * time.Millisecond)
				} else {
					ok = true
				}
			}
			if errU != nil {
				errorC <- errU
			} else {
				// If no error, we increment down a little bit
				dynPauseTimeC <- time.Duration(-1 * time.Millisecond)
				latenciesC <- time.Since(beforeLocal)
			}
		}(&wg)
	}

	wg.Wait()
	timeTaken := time.Since(before)

	return BenchResult{
			Action:     "updateStatus",
			Loops:      loops,
			PauseTime:  dynPauseTime,
			Errors:     errCount,
			Min:        getMin(latencies),
			Max:        getMax(latencies),
			Median:     getMedian(latencies),
			StandDev:   getStandardDeviation(latencies),
			Throughput: int(float64(loops) / timeTaken.Seconds()),
		},
		nil

}

// BenchmarkSelectEntityoneByStatus benchmark with select by status
func BenchmarkSelectEntityoneByStatus(
	loops int, dbConn *sqlx.DB, benchSQLLink *status.SQLIntImpl,
) (
	results BenchResult,
	err error,
) {
	var latencies []time.Duration
	var errCount int
	latenciesC, errorC := handleResults(&latencies, &errCount)

	var wg sync.WaitGroup
	before := time.Now()

	for i := 0; i < loops; i++ {
		wg.Add(1)
		go func(wg *sync.WaitGroup) {
			defer wg.Done()
			beforeLocal := time.Now()
			_, errSel := status.SelectEntityoneByStatus(dbConn, benchSQLLink, status.StatusCancelled)
			if errSel != nil {
				errorC <- errSel
			} else {
				latenciesC <- time.Since(beforeLocal)
			}
		}(&wg)
	}

	wg.Wait()
	timeTaken := time.Since(before)

	return BenchResult{
			Action:     "selectEntityoneByStatus",
			Loops:      loops,
			PauseTime:  0,
			Errors:     errCount,
			Min:        getMin(latencies),
			Max:        getMax(latencies),
			Median:     getMedian(latencies),
			StandDev:   getStandardDeviation(latencies),
			Throughput: int(float64(loops) / timeTaken.Seconds()),
		},
		nil
}

// BenchmarkSelectEntityoneOneByPK benchmark with select by primary key
func BenchmarkSelectEntityoneOneByPK(
	loops int, dbConn *sqlx.DB, benchSQLLink *status.SQLIntImpl, testEntityoneIDs []int64,
) (
	results BenchResult,
	err error,
) {
	var latencies []time.Duration
	var errCount int
	latenciesC, errorC := handleResults(&latencies, &errCount)

	before := time.Now()
	var wg sync.WaitGroup

	for i := 0; i < loops; i++ {
		wg.Add(1)
		go func(wg *sync.WaitGroup) {
			defer wg.Done()
			beforeLocal := time.Now()
			_, errSel := status.SelectEntityoneOneByPK(dbConn, benchSQLLink, testEntityoneIDs[i%len(testEntityoneIDs)])
			if errSel != nil {
				errorC <- errSel
			} else {
				latenciesC <- time.Since(beforeLocal)
			}
		}(&wg)
	}

	wg.Wait()
	timeTaken := time.Since(before)

	return BenchResult{
			Action:     "selectEntityoneOneByPK",
			Loops:      loops,
			PauseTime:  0,
			Errors:     errCount,
			Min:        getMin(latencies),
			Max:        getMax(latencies),
			Median:     getMedian(latencies),
			StandDev:   getStandardDeviation(latencies),
			Throughput: int(float64(loops) / timeTaken.Seconds()),
		},
		nil
}

// handleResults will generate two channels that will receive latencies and errors
func handleResults(latencies *[]time.Duration, errCount *int) (chan time.Duration, chan error) {
	latenciesC := make(chan time.Duration)
	errorC := make(chan error)

	go func() {
		for {
			select {
			case latency := <-latenciesC:
				*latencies = append(*latencies, latency)
			case <-errorC:
				*errCount++
			}
		}
	}()

	return latenciesC, errorC
}

// dynPauseTimeInit generates a channel that will be used to dynamically update the pause time between transactions
func dynPauseTimeInit(dynPauseTime *time.Duration) chan time.Duration {
	dynPauseTimeC := make(chan time.Duration)
	go func() {
		for additionalPauseTime := range dynPauseTimeC {
			if (*dynPauseTime+additionalPauseTime) > 1*time.Millisecond && (*dynPauseTime+additionalPauseTime) < 200*time.Millisecond {
				*dynPauseTime += additionalPauseTime
			}
		}
	}()
	return dynPauseTimeC
}

// getMin retrieves the min latency
func getMin(latencies []time.Duration) time.Duration {
	if len(latencies) == 0 {
		return 0
	}

	sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
	return latencies[0]
}

// getMax retrieves the max latency
func getMax(latencies []time.Duration) time.Duration {
	if len(latencies) == 0 {
		return 0
	}

	sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
	return latencies[len(latencies)-1]
}

// getMedian returns the median duration of a list
func getMedian(latencies []time.Duration) time.Duration {
	sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })

	if len(latencies) == 0 {
		return 0
	}
	if len(latencies) == 1 {
		return latencies[0]
	}
	if len(latencies)%2 == 0 {
		return latencies[(len(latencies)/2-1)] + latencies[(len(latencies)/2+1)]
	}
	return latencies[len(latencies)/2]
}

// getStandardDeviation returns the standard deviation of the list
func getStandardDeviation(latencies []time.Duration) time.Duration {

	if len(latencies) == 0 {
		return 0
	}

	// Sum the square of the mean subtracted from each number
	mean := getMean(latencies)

	var variance float64

	for _, latency := range latencies {
		variance += math.Pow(float64(latency.Nanoseconds()-mean.Nanoseconds()), 2)
	}

	return time.Duration(math.Sqrt(variance / float64(len(latencies))))
}

// getMean returns the mean of the list
func getMean(latencies []time.Duration) time.Duration {
	if len(latencies) == 0 {
		return 0
	}

	var total time.Duration
	for _, latency := range latencies {
		total += latency
	}

	return time.Duration(total.Nanoseconds() / int64(len(latencies)))
}
<commit_msg>add constant for retry limit<commit_after>package main

import (
	"flag"
	"fmt"
	"log"
	"sort"
	"sync"

	"time"

	"math"

	"encoding/json"

	"github.com/jmoiron/sqlx"
	"github.com/vincentserpoul/playwithsql/dbhandler"
	"github.com/vincentserpoul/playwithsql/status"
)

// Number of retries after query returns an error
const maxRetryCount = 5

// Results to be returned
type Results struct {
	DBType       string
	MaxConns     int
	Date         time.Time
	BenchResults []BenchResult
}

// BenchResult data
type BenchResult struct {
	Action     string
	Loops      int
	PauseTime  time.Duration
	Errors     int
	Min        time.Duration
	Max        time.Duration
	Median     time.Duration
	StandDev   time.Duration
	Throughput int
}

func main() {

	// Flags
	dbName := "playwithsql"
	dbType := flag.String("db", "mysql", "type of db to bench: mysql, cockroachdb, postgres")
	dbHost := flag.String("host", "127.0.0.1", "host IP")
	loops := flag.Int("loops", 100, "number of loops")
	maxConns := flag.Int("maxconns", 10, "number of max connections")
	flag.Parse()

	db, err := dbhandler.Get(*dbType, *dbHost, dbName)
	if err != nil {
		log.Fatalf("%s - %s - %s, \n%v", *dbType, *dbHost, dbName, err)
	}

	// Connection
	islatestSQLLink := status.GetSQLIntImpl(*dbType)
	err = islatestSQLLink.MigrateDown(db)
	if err != nil {
		log.Fatalf("%v", err)
	}
	err = islatestSQLLink.MigrateUp(db)
	if err != nil {
		log.Fatalf("%v", err)
	}

	// Number of max connections
	// TODO set the param in the db config
	db.SetMaxOpenConns(*maxConns)
	db.SetMaxIdleConns(*maxConns)

	var results = Results{
		DBType:   *dbType,
		MaxConns: *maxConns,
		Date:     time.Now(),
	}

	// Create
	createResults, testEntityoneIDs, err := BenchmarkCreate(*loops, db, islatestSQLLink)
	if err != nil {
		log.Fatalf("%v", err)
	}
	results.BenchResults = append(results.BenchResults, createResults)

	// Update
	updateResults, err := BenchmarkUpdateStatus(*loops, db, islatestSQLLink, testEntityoneIDs)
	if err != nil {
		log.Fatalf("%v", err)
	}
	results.BenchResults = append(results.BenchResults, updateResults)

	// Select by status
	selectByStatusResults, err := BenchmarkSelectEntityoneByStatus(*loops, db, islatestSQLLink)
	if err != nil {
		log.Fatalf("%v", err)
	}
	results.BenchResults = append(results.BenchResults, selectByStatusResults)

	// Select by PK
	selectByPKResults, err := BenchmarkSelectEntityoneOneByPK(*loops, db, islatestSQLLink, testEntityoneIDs)
	if err != nil {
		log.Fatalf("%v", err)
	}
	results.BenchResults = append(results.BenchResults, selectByPKResults)

	jsonResults, err := json.Marshal(results)
	if err != nil {
		log.Fatalf("%v", err)
	}

	fmt.Printf("%s\n", jsonResults)
}

// BenchmarkCreate will loop a loops number of time and give the resulting time taken
func BenchmarkCreate(
	loops int, dbConn *sqlx.DB, benchSQLLink *status.SQLIntImpl,
) (
	results BenchResult,
	testEntityoneIDs []int64,
	err error,
) {
	entityIDsC := make(chan int64)

	var latencies []time.Duration
	var errCount int
	latenciesC, errorC := handleResults(&latencies, &errCount)

	before := time.Now()
	var wg sync.WaitGroup

	// Pause time
	dynPauseTime := time.Duration(1 * time.Millisecond)
	dynPauseTimeC := dynPauseTimeInit(&dynPauseTime)
	defer close(dynPauseTimeC)

	for i := 0; i < loops; i++ {
		time.Sleep(dynPauseTime)
		wg.Add(1)
		go func(wg *sync.WaitGroup) {
			defer wg.Done()
			var e status.Entityone
			beforeLocal := time.Now()
			ok := false
			var errCr error
			retryCount := 0
			for retryCount < maxRetryCount && !ok {
				// For each error, we add some pause time
				errCr = e.Create(dbConn, benchSQLLink)
				if errCr != nil {
					retryCount++
					time.Sleep(dynPauseTime)
					dynPauseTimeC <- time.Duration(1 * time.Millisecond)
				} else {
					ok = true
				}
			}
			if errCr != nil {
				errorC <- errCr
			} else {
				latenciesC <- time.Since(beforeLocal)
				entityIDsC <- e.ID
				// If no error, we increment down a little bit
				dynPauseTimeC <- time.Duration(-1 * time.Millisecond)
			}
		}(&wg)
	}

	// Receive the entityIDs
	go func() {
		for entityID := range entityIDsC {
			testEntityoneIDs = append(testEntityoneIDs, entityID)
		}
	}()

	wg.Wait()
	timeTaken := time.Since(before)

	return BenchResult{
			Action:     "create",
			Loops:      loops,
			PauseTime:  dynPauseTime,
			Errors:     errCount,
			Min:        getMin(latencies),
			Max:        getMax(latencies),
			Median:     getMedian(latencies),
			StandDev:   getStandardDeviation(latencies),
			Throughput: int(float64(loops) / timeTaken.Seconds()),
		},
		testEntityoneIDs,
		nil
}

// BenchmarkUpdateStatus benchmark for status updates (include deletes)
func BenchmarkUpdateStatus(
	loops int, dbConn *sqlx.DB, benchSQLLink *status.SQLIntImpl, testEntityoneIDs []int64,
) (
	results BenchResult,
	err error,
) {
	if len(testEntityoneIDs) == 0 {
		return results, fmt.Errorf("BenchmarkUpdateStatus: no entity created, nothing to update")
	}

	var latencies []time.Duration
	var errCount int
	latenciesC, errorC := handleResults(&latencies, &errCount)

	before := time.Now()
	var wg sync.WaitGroup

	// Pause time
	dynPauseTime := time.Duration(1 * time.Millisecond)
	dynPauseTimeC := dynPauseTimeInit(&dynPauseTime)
	defer close(dynPauseTimeC)

	for i := 0; i < loops; i++ {
		time.Sleep(dynPauseTime)
		wg.Add(1)
		go func(wg *sync.WaitGroup) {
			defer wg.Done()
			var e status.Entityone
			e.ID = testEntityoneIDs[i%len(testEntityoneIDs)]
			beforeLocal := time.Now()
			ok := false
			var errU error
			retryCount := 0
			for retryCount < maxRetryCount && !ok {
				errU = e.UpdateStatus(dbConn, benchSQLLink, status.ActionCancel, status.StatusCancelled)
				if errU != nil {
					retryCount++
					time.Sleep(dynPauseTime)
					dynPauseTimeC <- time.Duration(1 * time.Millisecond)
				} else {
					ok = true
				}
			}
			if errU != nil {
				errorC <- errU
			} else {
				// If no error, we increment down a little bit
				dynPauseTimeC <- time.Duration(-1 * time.Millisecond)
				latenciesC <- time.Since(beforeLocal)
			}
		}(&wg)
	}

	wg.Wait()
	timeTaken := time.Since(before)

	return BenchResult{
			Action:     "updateStatus",
			Loops:      loops,
			PauseTime:  dynPauseTime,
			Errors:     errCount,
			Min:        getMin(latencies),
			Max:        getMax(latencies),
			Median:     getMedian(latencies),
			StandDev:   getStandardDeviation(latencies),
			Throughput: int(float64(loops) / timeTaken.Seconds()),
		},
		nil

}

// BenchmarkSelectEntityoneByStatus benchmark with select by status
func BenchmarkSelectEntityoneByStatus(
	loops int, dbConn *sqlx.DB, benchSQLLink *status.SQLIntImpl,
) (
	results BenchResult,
	err error,
) {
	var latencies []time.Duration
	var errCount int
	latenciesC, errorC := handleResults(&latencies, &errCount)

	var wg sync.WaitGroup
	before := time.Now()

	for i := 0; i < loops; i++ {
		wg.Add(1)
		go func(wg *sync.WaitGroup) {
			defer wg.Done()
			beforeLocal := time.Now()
			_, errSel := status.SelectEntityoneByStatus(dbConn, benchSQLLink, status.StatusCancelled)
			if errSel != nil {
				errorC <- errSel
			} else {
				latenciesC <- time.Since(beforeLocal)
			}
		}(&wg)
	}

	wg.Wait()
	timeTaken := time.Since(before)

	return BenchResult{
			Action:     "selectEntityoneByStatus",
			Loops:      loops,
			PauseTime:  0,
			Errors:     errCount,
			Min:        getMin(latencies),
			Max:        getMax(latencies),
			Median:     getMedian(latencies),
			StandDev:   getStandardDeviation(latencies),
			Throughput: int(float64(loops) / timeTaken.Seconds()),
		},
		nil
}

// BenchmarkSelectEntityoneOneByPK benchmark with select by primary key
func BenchmarkSelectEntityoneOneByPK(
	loops int, dbConn *sqlx.DB, benchSQLLink *status.SQLIntImpl, testEntityoneIDs []int64,
) (
	results BenchResult,
	err error,
) {
	var latencies []time.Duration
	var errCount int
	latenciesC, errorC := handleResults(&latencies, &errCount)

	before := time.Now()
	var wg sync.WaitGroup

	for i := 0; i < loops; i++ {
		wg.Add(1)
		go func(wg *sync.WaitGroup) {
			defer wg.Done()
			beforeLocal := time.Now()
			_, errSel := status.SelectEntityoneOneByPK(dbConn, benchSQLLink, testEntityoneIDs[i%len(testEntityoneIDs)])
			if errSel != nil {
				errorC <- errSel
			} else {
				latenciesC <- time.Since(beforeLocal)
			}
		}(&wg)
	}

	wg.Wait()
	timeTaken := time.Since(before)

	return BenchResult{
			Action:     "selectEntityoneOneByPK",
			Loops:      loops,
			PauseTime:  0,
			Errors:     errCount,
			Min:        getMin(latencies),
			Max:        getMax(latencies),
			Median:     getMedian(latencies),
			StandDev:   getStandardDeviation(latencies),
			Throughput: int(float64(loops) / timeTaken.Seconds()),
		},
		nil
}

// handleResults will generate two channels that will receive latencies and errors
func handleResults(latencies *[]time.Duration, errCount *int) (chan time.Duration, chan error) {
	latenciesC := make(chan time.Duration)
	errorC := make(chan error)

	go func() {
		for {
			select {
			case latency := <-latenciesC:
				*latencies = append(*latencies, latency)
			case <-errorC:
				*errCount++
			}
		}
	}()

	return latenciesC, errorC
}

// dynPauseTimeInit generates a channel that will be used to dynamically update the pause time between transactions
func dynPauseTimeInit(dynPauseTime *time.Duration) chan time.Duration {
	dynPauseTimeC := make(chan time.Duration)
	go func() {
		for additionalPauseTime := range dynPauseTimeC {
			if (*dynPauseTime+additionalPauseTime) > 1*time.Millisecond && (*dynPauseTime+additionalPauseTime) < 200*time.Millisecond {
				*dynPauseTime += additionalPauseTime
			}
		}
	}()
	return dynPauseTimeC
}

// getMin retrieves the min latency
func getMin(latencies []time.Duration) time.Duration {
	if len(latencies) == 0 {
		return 0
	}

	sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
	return latencies[0]
}

// getMax retrieves the max latency
func getMax(latencies []time.Duration) time.Duration {
	if len(latencies) == 0 {
		return 0
	}

	sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
	return latencies[len(latencies)-1]
}

// getMedian returns the median duration of a list
func getMedian(latencies []time.Duration) time.Duration {
	sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })

	if len(latencies) == 0 {
		return 0
	}
	if len(latencies) == 1 {
		return latencies[0]
	}
	if len(latencies)%2 == 0 {
		return latencies[(len(latencies)/2-1)] + latencies[(len(latencies)/2+1)]
	}
	return latencies[len(latencies)/2]
}

// getStandardDeviation returns the standard deviation of the list
func getStandardDeviation(latencies []time.Duration) time.Duration {

	if len(latencies) == 0 {
		return 0
	}

	// Sum the square of the mean subtracted from each number
	mean := getMean(latencies)

	var variance float64

	for _, latency := range latencies {
		variance += math.Pow(float64(latency.Nanoseconds()-mean.Nanoseconds()), 2)
	}

	return time.Duration(math.Sqrt(variance / float64(len(latencies))))
}

// getMean returns the mean of the list
func getMean(latencies []time.Duration) time.Duration {
	if len(latencies) == 0 {
		return 0
	}

	var total time.Duration
	for _, latency := range latencies {
		total += latency
	}

	return time.Duration(total.Nanoseconds() / int64(len(latencies)))
}
<|endoftext|>


<commit_before>/*
Copyright 2016 The Kubernetes 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.
*/

package cluster

import (
	"k8s.io/kubernetes/federation/apis/federation"
	"k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/api/rest"
	"k8s.io/kubernetes/pkg/watch"
)

// Registry is an interface implemented by things that know how to store Cluster objects.
type Registry interface {
	ListClusters(ctx api.Context, options *api.ListOptions) (*federation.ClusterList, error)
	WatchCluster(ctx api.Context, options *api.ListOptions) (watch.Interface, error)
	GetCluster(ctx api.Context, name string) (*federation.Cluster, error)
	CreateCluster(ctx api.Context, cluster *federation.Cluster) error
	UpdateCluster(ctx api.Context, cluster *federation.Cluster) error
	DeleteCluster(ctx api.Context, name string) error
}

// storage puts strong typing around storage calls

type storage struct {
	rest.StandardStorage
}

// NewRegistry returns a new Registry interface for the given Storage. Any mismatched
// types will panic.
func NewRegistry(s rest.StandardStorage) Registry {
	return &storage{s}
}

func (s *storage) ListClusters(ctx api.Context, options *api.ListOptions) (*federation.ClusterList, error) {
	obj, err := s.List(ctx, options)
	if err != nil {
		return nil, err
	}
	return obj.(*federation.ClusterList), nil
}

func (s *storage) WatchCluster(ctx api.Context, options *api.ListOptions) (watch.Interface, error) {
	return s.Watch(ctx, options)
}

func (s *storage) GetCluster(ctx api.Context, name string) (*federation.Cluster, error) {
	obj, err := s.Get(ctx, name)
	if err != nil {
		return nil, err
	}
	return obj.(*federation.Cluster), nil
}

func (s *storage) CreateCluster(ctx api.Context, cluster *federation.Cluster) error {
	_, err := s.Create(ctx, cluster)
	return err
}

func (s *storage) UpdateCluster(ctx api.Context, cluster *federation.Cluster) error {
	_, _, err := s.Update(ctx, cluster.Name, rest.DefaultUpdatedObjectInfo(cluster, api.Scheme))
	return err
}

func (s *storage) DeleteCluster(ctx api.Context, name string) error {
	_, err := s.Delete(ctx, name, nil)
	return err
}
<commit_msg>Remove unnecessary empty line.<commit_after>/*
Copyright 2016 The Kubernetes 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.
*/

package cluster

import (
	"k8s.io/kubernetes/federation/apis/federation"
	"k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/api/rest"
	"k8s.io/kubernetes/pkg/watch"
)

// Registry is an interface implemented by things that know how to store Cluster objects.
type Registry interface {
	ListClusters(ctx api.Context, options *api.ListOptions) (*federation.ClusterList, error)
	WatchCluster(ctx api.Context, options *api.ListOptions) (watch.Interface, error)
	GetCluster(ctx api.Context, name string) (*federation.Cluster, error)
	CreateCluster(ctx api.Context, cluster *federation.Cluster) error
	UpdateCluster(ctx api.Context, cluster *federation.Cluster) error
	DeleteCluster(ctx api.Context, name string) error
}

// storage puts strong typing around storage calls
type storage struct {
	rest.StandardStorage
}

// NewRegistry returns a new Registry interface for the given Storage. Any mismatched
// types will panic.
func NewRegistry(s rest.StandardStorage) Registry {
	return &storage{s}
}

func (s *storage) ListClusters(ctx api.Context, options *api.ListOptions) (*federation.ClusterList, error) {
	obj, err := s.List(ctx, options)
	if err != nil {
		return nil, err
	}
	return obj.(*federation.ClusterList), nil
}

func (s *storage) WatchCluster(ctx api.Context, options *api.ListOptions) (watch.Interface, error) {
	return s.Watch(ctx, options)
}

func (s *storage) GetCluster(ctx api.Context, name string) (*federation.Cluster, error) {
	obj, err := s.Get(ctx, name)
	if err != nil {
		return nil, err
	}
	return obj.(*federation.Cluster), nil
}

func (s *storage) CreateCluster(ctx api.Context, cluster *federation.Cluster) error {
	_, err := s.Create(ctx, cluster)
	return err
}

func (s *storage) UpdateCluster(ctx api.Context, cluster *federation.Cluster) error {
	_, _, err := s.Update(ctx, cluster.Name, rest.DefaultUpdatedObjectInfo(cluster, api.Scheme))
	return err
}

func (s *storage) DeleteCluster(ctx api.Context, name string) error {
	_, err := s.Delete(ctx, name, nil)
	return err
}
<|endoftext|>


<commit_before>package controller

import (
	"fmt"
	"strings"

	"github.com/Sirupsen/logrus"
	"github.com/rancher/longhorn/replica/client"
	"github.com/rancher/longhorn/types"
)

func (c *Controller) Revert(name string) error {
	for _, rep := range c.replicas {
		if rep.Mode != types.RW {
			return fmt.Errorf("Replica %s is in mode %s", rep.Address, rep.Mode)
		}
	}

	clients, name, err := c.clientsAndSnapshot(name)
	if err != nil {
		return err
	}

	if err := c.shutdownFrontend(); err != nil {
		return err
	}

	c.Lock()
	defer c.Unlock()

	for address, client := range clients {
		logrus.Infof("Reverting to snapshot %s on %s", name, address)
		if err := client.Revert(name); err != nil {
			c.setReplicaModeNoLock(address, types.ERR)
			return err
		}
	}

	return c.startFrontend()
}

func (c *Controller) clientsAndSnapshot(name string) (map[string]*client.ReplicaClient, string, error) {
	clients := map[string]*client.ReplicaClient{}

	for _, replica := range c.replicas {
		if !strings.HasPrefix(replica.Address, "tcp://") {
			return nil, "", fmt.Errorf("Backend %s does not support revert", replica.Address)
		}

		repClient, err := client.NewReplicaClient(replica.Address)
		if err != nil {
			return nil, "", err
		}

		rep, err := repClient.GetReplica()
		if err != nil {
			return nil, "", err
		}

		found := ""
		for _, snapshot := range rep.Chain {
			if snapshot == name {
				found = name
				break
			}
			fullName := "volume-snap-" + name + ".img"
			if snapshot == fullName {
				found = fullName
				break
			}
		}

		if found == "" {
			return nil, "", fmt.Errorf("Failed to find snapshot %s on %s", name, replica)
		}

		name = found
		clients[replica.Address] = repClient
	}

	return clients, name, nil
}
<commit_msg>Workaround live chain detection for revert to implement restore<commit_after>package controller

import (
	"fmt"
	"strings"

	"github.com/Sirupsen/logrus"
	"github.com/rancher/longhorn/replica/client"
	"github.com/rancher/longhorn/types"
)

func (c *Controller) Revert(name string) error {
	for _, rep := range c.replicas {
		if rep.Mode != types.RW {
			return fmt.Errorf("Replica %s is in mode %s", rep.Address, rep.Mode)
		}
	}

	clients, name, err := c.clientsAndSnapshot(name)
	if err != nil {
		return err
	}

	if err := c.shutdownFrontend(); err != nil {
		return err
	}

	c.Lock()
	defer c.Unlock()

	for address, client := range clients {
		logrus.Infof("Reverting to snapshot %s on %s", name, address)
		if err := client.Revert(name); err != nil {
			c.setReplicaModeNoLock(address, types.ERR)
			return err
		}
	}

	return c.startFrontend()
}

func (c *Controller) clientsAndSnapshot(name string) (map[string]*client.ReplicaClient, string, error) {
	clients := map[string]*client.ReplicaClient{}

	for _, replica := range c.replicas {
		if !strings.HasPrefix(replica.Address, "tcp://") {
			return nil, "", fmt.Errorf("Backend %s does not support revert", replica.Address)
		}

		repClient, err := client.NewReplicaClient(replica.Address)
		if err != nil {
			return nil, "", err
		}

		_, err = repClient.GetReplica()
		if err != nil {
			return nil, "", err
		}

		/*
			found := ""
			for _, snapshot := range rep.Chain {
				if snapshot == name {
					found = name
					break
				}
				fullName := "volume-snap-" + name + ".img"
				if snapshot == fullName {
					found = fullName
					break
				}
			}

			if found == "" {
				return nil, "", fmt.Errorf("Failed to find snapshot %s on %s", name, replica)
			}

			name = found
		*/
		clients[replica.Address] = repClient
	}
	name = "volume-snap-" + name + ".img"

	return clients, name, nil
}
<|endoftext|>


<commit_before>package gox12

import (
	"testing"
)

// AAA
func TestSegmentParseSegmentId(t *testing.T) {
	str2 := "TST&AA!1!1&BB!5"
	seg := NewSegment(str2, '&', '!', '^')
	expectedSegId := "TST"
	if seg.SegmentId != expectedSegId {
		t.Errorf("Didn't get expected result [%s], instead got [%s]", expectedSegId, seg.SegmentId)
	}
}

func TestSegmentSetValueSubelement(t *testing.T) {
	var segtests = []struct {
		refdes   string
		expected string
	}{
		{"SVC01-1", "HC"},
		{"SVC01-2", "H0004"},
		{"SVC01-3", "HF"},
		{"SVC01-4", "H8"},
	}
	segmentStr := "SVC*HC:H0005:HF:H9*56.70*56.52**6"
	seg := NewSegment(segmentStr, '*', ':', '~')
	err := seg.SetValue("SVC01-4", "H8")
	err = seg.SetValue("SVC01-2", "H0004")
	if err != nil {
		t.Errorf("Failed to SetValue INS05 [%s]", err)
	}

	for _, tt := range segtests {
		actual, found, err := seg.GetValue(tt.refdes)
		if err != nil {
			t.Errorf("Didn't get a value for [%s]", tt.refdes)
		}
		if !found {
			t.Errorf("Didn't get a value for [%s]", tt.refdes)
		}
		if actual != tt.expected {
			t.Errorf("Didn't get expected result [%s] for path [%s], instead got [%s]", tt.expected, tt.refdes, actual)
		}
	}
}

func TestSegmentSetValueSegment(t *testing.T) {
	var segtests = []struct {
		refdes   string
		expected string
	}{
		{"INS01", "Y"},
		{"INS02", "18"},
		{"INS05", "C"},
	}
	segmentStr := "INS*Y*18*030*20*A"
	seg := NewSegment(segmentStr, '*', ':', '~')
	err := seg.SetValue("INS05", "C")
	if err != nil {
		t.Errorf("Failed to SetValue INS05 [%s]", err)
	}

	for _, tt := range segtests {
		actual, found, err := seg.GetValue(tt.refdes)
		if err != nil {
			t.Errorf("Didn't get a value for [%s]", tt.refdes)
		}
		if !found {
			t.Errorf("Didn't get a value for [%s]", tt.refdes)
		}
		if actual != tt.expected {
			t.Errorf("Didn't get expected result [%s] for path [%s], instead got [%s]", tt.expected, tt.refdes, actual)
		}
	}
}

func TestSegmentParseComposites(t *testing.T) {
	var segtests = []struct {
		refdes   string
		expected string
	}{
		{"TST01-1", "AA"},
		{"TST01-2", "1"},
		{"TST01-3", "5"},
		{"TST02-1", "BB"},
	}
	segmentStr := "TST&AA!1!5&BB!5"
	seg := NewSegment(segmentStr, '&', '!', '^')
	for _, tt := range segtests {
		actual, found, err := seg.GetValue(tt.refdes)
		if err != nil {
			t.Errorf("Didn't get a value for [%s]", tt.refdes)
		}
		if !found {
			t.Errorf("Didn't get a value for [%s]", tt.refdes)
		}
		if actual != tt.expected {
			t.Errorf("Didn't get expected result [%s] for path [%s], instead got [%s]", tt.expected, tt.refdes, actual)
		}
	}
}

func TestSegmentIndexNotFound(t *testing.T) {
	var segtests = []struct {
		refdes   string
		expected string
	}{
		{"TST01-5", ""},
		{"TST06", ""},
		{"TST07", ""},
		{"TST05-2", ""},
	}
	segmentStr := "TST&AA!1!5&BB!5&&X"
	seg := NewSegment(segmentStr, '&', '!', '^')
	for _, tt := range segtests {
		actual, found, err := seg.GetValue(tt.refdes)
		if err != nil {
			t.Errorf("Didn't get a value for [%s]", tt.refdes)
		}
		if found {
			t.Errorf("Found should be false for [%s]", tt.refdes)
		}
		if actual != tt.expected {
			t.Errorf("Didn't get expected result [%s] for path [%s], instead got [%s]", tt.expected, tt.refdes, actual)
		}
	}
}

func TestSegmentIdentity(t *testing.T) {
	var segtests = []struct {
		rawseg string
	}{
		{"TST*AA:1:1*BB:5*ZZ"},
		{"ISA*00*          *00*          *ZZ*ZZ000          *ZZ*ZZ001          *030828*1128*U*00401*000010121*0*T*:\n"},
	}
	for _, tt := range segtests {
		seg := NewSegment(tt.rawseg, '*', ':', '^')
		actual := seg.String()
		if actual != tt.rawseg {
			t.Errorf("Didn't get expected result [%s], instead got [%s]", tt.rawseg, actual)
		}
	}
}

func TestSegmentString(t *testing.T) {
	var segtests = []struct {
		rawseg   string
		expected string
	}{
		{"TST*AA:1:1*BB:5*Zed", "TST*AA:1:1*BB:5*Zed"},
		{"N1*55:123*PIRATE**Da", "N1*55:123*PIRATE**Da"},
	}
	for _, tt := range segtests {
		seg := NewSegment(tt.rawseg, '*', ':', '^')
		actual := seg.String()
		if actual != tt.expected {
			t.Errorf("Didn't get expected result [%s], instead got [%s]", tt.expected, actual)
		}
	}
}

func TestSegmentFormat(t *testing.T) {
	var segtests = []struct {
		rawseg   string
		expected string
	}{
		{"TST*AA:1:1*BB:5*Zed", "TST#AA%1%1#BB%5#Zed"},
		{"N1*55:123*PIRATE**Dada", "N1#55%123#PIRATE##Dada"},
	}
	for _, tt := range segtests {
		seg := NewSegment(tt.rawseg, '*', ':', '^')
		actual := seg.Format('#', '%', '^')
		if actual != tt.expected {
			t.Errorf("Didn't get expected result [%s], instead got [%s]", tt.expected, actual)
		}
	}
}

func BenchmarkSegmentParse(b *testing.B) {
	str2 := "TST&AA!1!1&BB!5"
	for i := 0; i < b.N; i++ {
		_ = NewSegment(str2, '&', '!', '^')
	}
}

func BenchmarkSegmentString(b *testing.B) {
	rawseg := "TST&AA!1!1&BBbbbbbbbbb!5&&B!FjhhealkjF&&J&HJY&IU"
	s := NewSegment(rawseg, '&', '!', '^')
	for i := 0; i < b.N; i++ {
		_ = s.String()
	}
}
func BenchmarkSegmentFormat(b *testing.B) {
	rawseg := "TST&AA!1!1&BBbbbbbbbbb!5&&B!FjhhealkjF&&J&HJY&IU"
	s := NewSegment(rawseg, '&', '!', '^')
	for i := 0; i < b.N; i++ {
		_ = s.Format('*', ':', '^')
	}
}
<commit_msg>generalize segment setValue tests<commit_after>package gox12

import (
	"testing"
)

// AAA
func TestSegmentParseSegmentId(t *testing.T) {
	str2 := "TST&AA!1!1&BB!5"
	seg := NewSegment(str2, '&', '!', '^')
	expectedSegId := "TST"
	if seg.SegmentId != expectedSegId {
		t.Errorf("Didn't get expected result [%s], instead got [%s]", expectedSegId, seg.SegmentId)
	}
}

func TestSegmentSetValueSubelement(t *testing.T) {
	var segtests = []struct {
		refdes   string
		expected string
	}{
		{"SVC01-1", "HC"},
		{"SVC01-2", "H0004"},
		{"SVC01-3", "HF"},
		{"SVC01-4", "H8"},
	}
	segmentStr := "SVC*AA:H0005:FF:H9*56.70*56.52**6"

	for _, tt := range segtests {
		seg := NewSegment(segmentStr, '*', ':', '~')
		// first, ensure value is not already set
		actual, found, err := seg.GetValue(tt.refdes)
		if err != nil || !found || actual == tt.expected {
			t.Errorf("Pre SetValue, expected result [%s] already set for path [%s]", tt.expected, tt.refdes)
		}
		// Act
		err = seg.SetValue(tt.refdes, tt.expected)
		if err != nil {
			t.Errorf("Failed to SetValue [%s] [%s]", tt.refdes, err)
		}
		actual, found, err = seg.GetValue(tt.refdes)
		if err != nil {
			t.Errorf("Didn't get a value for [%s]", tt.refdes)
		}
		if !found {
			t.Errorf("Didn't get a value for [%s]", tt.refdes)
		}
		if actual != tt.expected {
			t.Errorf("Didn't get expected result [%s] for path [%s], instead got [%s]", tt.expected, tt.refdes, actual)
		}
	}
}

func TestSegmentSetValueSegment(t *testing.T) {
	var segtests = []struct {
		refdes   string
		expected string
	}{
		{"INS01", "BB"},
		{"INS02", "99"},
		{"INS05", "Z"},
	}
	segmentStr := "INS*Y*18*030*20*A"
	for _, tt := range segtests {
		seg := NewSegment(segmentStr, '*', ':', '~')
		// first, ensure value is not already set
		actual, found, err := seg.GetValue(tt.refdes)
		if err != nil || !found || actual == tt.expected {
			t.Errorf("Pre SetValue, expected result [%s] already set for path [%s]", tt.expected, tt.refdes)
		}
		// Act
		err = seg.SetValue(tt.refdes, tt.expected)
		if err != nil {
			t.Errorf("Failed to SetValue [%s] [%s]", tt.refdes, err)
		}
		actual, found, err = seg.GetValue(tt.refdes)
		if err != nil {
			t.Errorf("Didn't get a value for [%s]", tt.refdes)
		}
		if !found {
			t.Errorf("Didn't get a value for [%s]", tt.refdes)
		}
		if actual != tt.expected {
			t.Errorf("Didn't get expected result [%s] for path [%s], instead got [%s]", tt.expected, tt.refdes, actual)
		}
	}
}

func TestSegmentParseComposites(t *testing.T) {
	var segtests = []struct {
		refdes   string
		expected string
	}{
		{"TST01-1", "AA"},
		{"TST01-2", "1"},
		{"TST01-3", "5"},
		{"TST02-1", "BB"},
	}
	segmentStr := "TST&AA!1!5&BB!5"
	seg := NewSegment(segmentStr, '&', '!', '^')
	for _, tt := range segtests {
		actual, found, err := seg.GetValue(tt.refdes)
		if err != nil {
			t.Errorf("Didn't get a value for [%s]", tt.refdes)
		}
		if !found {
			t.Errorf("Didn't get a value for [%s]", tt.refdes)
		}
		if actual != tt.expected {
			t.Errorf("Didn't get expected result [%s] for path [%s], instead got [%s]", tt.expected, tt.refdes, actual)
		}
	}
}

func TestSegmentIndexNotFound(t *testing.T) {
	var segtests = []struct {
		refdes   string
		expected string
	}{
		{"TST01-5", ""},
		{"TST06", ""},
		{"TST07", ""},
		{"TST05-2", ""},
	}
	segmentStr := "TST&AA!1!5&BB!5&&X"
	seg := NewSegment(segmentStr, '&', '!', '^')
	for _, tt := range segtests {
		actual, found, err := seg.GetValue(tt.refdes)
		if err != nil {
			t.Errorf("Didn't get a value for [%s]", tt.refdes)
		}
		if found {
			t.Errorf("Found should be false for [%s]", tt.refdes)
		}
		if actual != tt.expected {
			t.Errorf("Didn't get expected result [%s] for path [%s], instead got [%s]", tt.expected, tt.refdes, actual)
		}
	}
}

func TestSegmentIdentity(t *testing.T) {
	var segtests = []struct {
		rawseg string
	}{
		{"TST*AA:1:1*BB:5*ZZ"},
		{"ISA*00*          *00*          *ZZ*ZZ000          *ZZ*ZZ001          *030828*1128*U*00401*000010121*0*T*:\n"},
	}
	for _, tt := range segtests {
		seg := NewSegment(tt.rawseg, '*', ':', '^')
		actual := seg.String()
		if actual != tt.rawseg {
			t.Errorf("Didn't get expected result [%s], instead got [%s]", tt.rawseg, actual)
		}
	}
}

func TestSegmentString(t *testing.T) {
	var segtests = []struct {
		rawseg   string
		expected string
	}{
		{"TST*AA:1:1*BB:5*Zed", "TST*AA:1:1*BB:5*Zed"},
		{"N1*55:123*PIRATE**Da", "N1*55:123*PIRATE**Da"},
	}
	for _, tt := range segtests {
		seg := NewSegment(tt.rawseg, '*', ':', '^')
		actual := seg.String()
		if actual != tt.expected {
			t.Errorf("Didn't get expected result [%s], instead got [%s]", tt.expected, actual)
		}
	}
}

func TestSegmentFormat(t *testing.T) {
	var segtests = []struct {
		rawseg   string
		expected string
	}{
		{"TST*AA:1:1*BB:5*Zed", "TST#AA%1%1#BB%5#Zed"},
		{"N1*55:123*PIRATE**Dada", "N1#55%123#PIRATE##Dada"},
	}
	for _, tt := range segtests {
		seg := NewSegment(tt.rawseg, '*', ':', '^')
		actual := seg.Format('#', '%', '^')
		if actual != tt.expected {
			t.Errorf("Didn't get expected result [%s], instead got [%s]", tt.expected, actual)
		}
	}
}

func BenchmarkSegmentParse(b *testing.B) {
	str2 := "TST&AA!1!1&BB!5"
	for i := 0; i < b.N; i++ {
		_ = NewSegment(str2, '&', '!', '^')
	}
}

func BenchmarkSegmentString(b *testing.B) {
	rawseg := "TST&AA!1!1&BBbbbbbbbbb!5&&B!FjhhealkjF&&J&HJY&IU"
	s := NewSegment(rawseg, '&', '!', '^')
	for i := 0; i < b.N; i++ {
		_ = s.String()
	}
}
func BenchmarkSegmentFormat(b *testing.B) {
	rawseg := "TST&AA!1!1&BBbbbbbbbbb!5&&B!FjhhealkjF&&J&HJY&IU"
	s := NewSegment(rawseg, '&', '!', '^')
	for i := 0; i < b.N; i++ {
		_ = s.Format('*', ':', '^')
	}
}
<|endoftext|>


<commit_before>package controller

import (
	"time"

	"github.com/ninjasphere/app-scheduler/model"
)

type window struct {
	model *model.Window
	from  event
	until event
}

func (w *window) init(m *model.Window) error {
	w.model = m
	err := w.from.init(m.From)
	if err != nil {
		err = w.until.init(m.Until)
	}
	return err
}

// Answer true if the window is open with respect to the specified time.
func (w *window) isOpen(ref time.Time) bool {
	openTimestamp := w.openTimestamp(ref)
	closeTimestamp := w.closeTimestamp(openTimestamp)

	return openTimestamp.Sub(ref) < 0 &&
		ref.Sub(closeTimestamp) < 0 &&
		openTimestamp.Sub(closeTimestamp) > 0
}

// Answer timestamp of the current (or next) open event, given the current timestamp
func (w *window) openTimestamp(ref time.Time) time.Time {
	return w.from.asTimestamp(ref)
}

// Answer the timestamp of the next close event, given an open event with the specified timestamp.
func (w *window) closeTimestamp(ref time.Time) time.Time {
	openTimestamp := w.openTimestamp(ref)
	closeTimestamp := w.until.asTimestamp(openTimestamp)
	if closeTimestamp.Sub(openTimestamp) < 0 {
		if w.from.clockTime && w.until.clockTime {
			// when open and close times are specified with clock times, then
			// we must cope with open times that start before midnight and end after midnight
			closeTimestamp = closeTimestamp.AddDate(0, 1, 1)
		} else {
			log.Fatalf("confusing window specification - what should I do here? (from,until) == (%s, %s)", w.model.From, w.model.Until)
		}
	}
	return closeTimestamp
}

// Answer a channel that will signal when the specified deadline time has been reached.
func getWaiter(deadline time.Time) chan time.Time {
	now := time.Now()
	delay := deadline.Sub(now)
	waiter := make(chan time.Time, 1)
	if delay > 0 {
		time.AfterFunc(delay, func() {
			waiter <- time.Now()
		})
	} else {
		waiter <- now
	}
	return waiter
}

// Answer a channel that will receive an event when the next open event occurs.
func (w *window) whenOpen(ref time.Time) chan time.Time {
	return getWaiter(w.openTimestamp(ref))
}

// Answer a channel that will receive an event when the next close event after the specified open event occurs.
func (w *window) whenClosed(opened time.Time) chan time.Time {
	return getWaiter(w.closeTimestamp(opened))
}
<commit_msg>fix: add one day, not one month and one day!<commit_after>package controller

import (
	"time"

	"github.com/ninjasphere/app-scheduler/model"
)

type window struct {
	model *model.Window
	from  event
	until event
}

func (w *window) init(m *model.Window) error {
	w.model = m
	err := w.from.init(m.From)
	if err != nil {
		err = w.until.init(m.Until)
	}
	return err
}

// Answer true if the window is open with respect to the specified time.
func (w *window) isOpen(ref time.Time) bool {
	openTimestamp := w.openTimestamp(ref)
	closeTimestamp := w.closeTimestamp(openTimestamp)

	return openTimestamp.Sub(ref) < 0 &&
		ref.Sub(closeTimestamp) < 0 &&
		openTimestamp.Sub(closeTimestamp) > 0
}

// Answer timestamp of the current (or next) open event, given the current timestamp
func (w *window) openTimestamp(ref time.Time) time.Time {
	return w.from.asTimestamp(ref)
}

// Answer the timestamp of the next close event, given an open event with the specified timestamp.
func (w *window) closeTimestamp(ref time.Time) time.Time {
	openTimestamp := w.openTimestamp(ref)
	closeTimestamp := w.until.asTimestamp(openTimestamp)
	if closeTimestamp.Sub(openTimestamp) < 0 {
		if w.from.clockTime && w.until.clockTime {
			// when open and close times are specified with clock times, then
			// we must cope with open times that start before midnight and end after midnight
			closeTimestamp = closeTimestamp.AddDate(0, 0, 1)
		} else {
			log.Fatalf("confusing window specification - what should I do here? (from,until) == (%s, %s)", w.model.From, w.model.Until)
		}
	}
	return closeTimestamp
}

// Answer a channel that will signal when the specified deadline time has been reached.
func getWaiter(deadline time.Time) chan time.Time {
	now := time.Now()
	delay := deadline.Sub(now)
	waiter := make(chan time.Time, 1)
	if delay > 0 {
		time.AfterFunc(delay, func() {
			waiter <- time.Now()
		})
	} else {
		waiter <- now
	}
	return waiter
}

// Answer a channel that will receive an event when the next open event occurs.
func (w *window) whenOpen(ref time.Time) chan time.Time {
	return getWaiter(w.openTimestamp(ref))
}

// Answer a channel that will receive an event when the next close event after the specified open event occurs.
func (w *window) whenClosed(opened time.Time) chan time.Time {
	return getWaiter(w.closeTimestamp(opened))
}
<|endoftext|>


<commit_before>package htmltest

import (
	"github.com/wjdp/htmltest/htmldoc"
	"github.com/wjdp/htmltest/issues"
	"github.com/wjdp/htmltest/output"
	"golang.org/x/net/html"
	"net/http"
	"net/url"
	"os"
	"path"
	"strings"
	"fmt"
)

func (hT *HTMLTest) checkLink(document *htmldoc.Document, node *html.Node) {
	attrs := htmldoc.ExtractAttrs(node.Attr,
		[]string{"href", "rel", hT.opts.IgnoreTagAttribute})

	// Ignore if data-proofer-ignore set
	if htmldoc.AttrPresent(node.Attr, hT.opts.IgnoreTagAttribute) {
		return
	}

	// Check if favicon
	if htmldoc.AttrPresent(node.Attr, "rel") &&
		(attrs["rel"] == "icon" || attrs["rel"] == "shortcut icon") &&
		node.Parent.Data == "head" {
		document.State.FaviconPresent = true
	}

	// Ignore if rel=dns-prefetch, see #40. If we have more cases like this a hashable type should be created and
	// checked against.
	if attrs["rel"] == "dns-prefetch" {
		return
	}

	// Create reference
	ref := htmldoc.NewReference(document, node, attrs["href"])

	// Check for missing href, fail for link nodes
	if !htmldoc.AttrPresent(node.Attr, "href") {
		switch node.Data {
		case "a":
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelDebug,
				Message:   "anchor without href",
				Reference: ref,
			})
			return
		case "link":
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelError,
				Message:   "link tag missing href",
				Reference: ref,
			})
			return
		}
	}

	// Blank href
	if attrs["href"] == "" {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelError,
			Message:   "href blank",
			Reference: ref,
		})
		return
	}

	// href="#"
	if attrs["href"] == "#" {
		if hT.opts.CheckInternalHash && !hT.opts.IgnoreInternalEmptyHash {
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelError,
				Message:   "empty hash",
				Reference: ref,
			})
		}
		return
	}

	// Route reference check
	switch ref.Scheme() {
	case "http":
		hT.enforceHTTPS(ref)
		hT.checkExternal(ref)
	case "https":
		hT.checkExternal(ref)
	case "file":
		hT.checkInternal(ref)
	case "self":
		hT.checkInternalHash(ref)
	case "mailto":
		hT.checkMailto(ref)
	case "tel":
		hT.checkTel(ref)
	}

	// TODO: Other schemes
	// What to do about unknown schemes, could be perfectly valid or a typo.
	// Perhaps show a warning, which can be suppressed per-scheme in options.
	// Preload with a couple of common ones, ftp &c.

}

func (hT *HTMLTest) checkExternal(ref *htmldoc.Reference) {
	if !hT.opts.CheckExternal {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelDebug,
			Message:   "skipping external check",
			Reference: ref,
		})
		return
	}

	urlStr := ref.URLString()

	// Does this url match an url ignore rule?
	if hT.opts.isURLIgnored(urlStr) {
		return
	}

	if hT.opts.StripQueryString && !InList(hT.opts.StripQueryExcludes, urlStr) {
		urlStr = htmldoc.URLStripQueryString(urlStr)
	}
	var statusCode int

	cR, isCached := hT.refCache.Get(urlStr)

	if isCached && statusCodeValid(cR.StatusCode) {
		// If we have a valid result in cache, use that
		statusCode = cR.StatusCode
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelDebug,
			Message:   "from cache",
			Reference: ref,
		})
	} else {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelDebug,
			Message:   "fresh",
			Reference: ref,
		})
		urlURL, err := url.Parse(urlStr)
		req := &http.Request{
			Method: "GET",
			URL:    urlURL,
			Header: map[string][]string{
				"Range": {"bytes=0-0"}, // If server supports prevents body being sent
			},
		}

		hT.httpChannel <- true // Add to http concurrency limiter

		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelInfo,
			Message:   "hitting",
			Reference: ref,
		})

		resp, err := hT.httpClient.Do(req)

		<-hT.httpChannel // Bump off http concurrency limiter

		if err != nil {
			if strings.Contains(err.Error(), "dial tcp") {
				// Remove long prefix
				prefix := "Get " + urlStr + ": dial tcp: lookup "
				cleanedMessage := strings.TrimPrefix(err.Error(), prefix)
				// Add error
				hT.issueStore.AddIssue(issues.Issue{
					Level:     issues.LevelError,
					Message:   cleanedMessage,
					Reference: ref,
				})
				return
			}
			if strings.Contains(err.Error(), "Client.Timeout") {
				hT.issueStore.AddIssue(issues.Issue{
					Level:     issues.LevelError,
					Message:   "request exceeded our ExternalTimeout",
					Reference: ref,
				})
				return
			}

			// Unhandled client error, return generic error
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelError,
				Message:   err.Error(),
				Reference: ref,
			})

			return
		}
		// Save cached result
		hT.refCache.Save(urlStr, resp.StatusCode)
		statusCode = resp.StatusCode
	}

	switch statusCode {
	case http.StatusOK:
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelDebug,
			Message:   http.StatusText(statusCode),
			Reference: ref,
		})
	case http.StatusPartialContent:
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelDebug,
			Message:   http.StatusText(statusCode),
			Reference: ref,
		})
	default:
		attrs := htmldoc.ExtractAttrs(ref.Node.Attr, []string{"rel"})
		if attrs["rel"] == "canonical" && hT.opts.IgnoreCanonicalBrokenLinks {
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelWarning,
				Message:   http.StatusText(statusCode) + " [rel=\"canonical\"]",
				Reference: ref,
			})
		} else {
			// Failed VCRed requests end up here with a status code of zero
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelError,
				Message:   fmt.Sprintf("%s %d", "Non-OK status:", statusCode),
				Reference: ref,
			})
		}
	}

	// TODO check a hash id exists in external page if present in reference (URL.Fragment)
}

func (hT *HTMLTest) checkInternal(ref *htmldoc.Reference) {
	if !hT.opts.CheckInternal {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelDebug,
			Message:   "skipping internal check",
			Reference: ref,
		})
		return
	}

	// First lookup in document store,
	refDoc, refExists := hT.documentStore.ResolveRef(ref)

	if refExists {
		// If path doesn't end in slash and the resolved ref is an index.html, complain
		if (ref.URL.Path[len(ref.URL.Path)-1] != '/') && (path.Base(refDoc.SitePath) == hT.opts.DirectoryIndex) && (!hT.opts.IgnoreDirectoryMissingTrailingSlash) {
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelError,
				Message:   "target is a directory, href lacks trailing slash",
				Reference: ref,
			})
		}
	} else {
		// If that fails attempt to lookup with filesystem, resolve a path and check
		refOsPath := path.Join(hT.opts.DirectoryPath, ref.RefSitePath())
		hT.checkFile(ref, refOsPath)
	}

	if len(ref.URL.Fragment) > 0 {
		// Is also a hash link
		hT.checkInternalHash(ref)
	}
}

func (hT *HTMLTest) checkInternalHash(ref *htmldoc.Reference) {
	if !hT.opts.CheckInternalHash {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelDebug,
			Message:   "skipping hash check",
			Reference: ref,
		})
		return
	}

	// var refDoc *htmldoc.Document
	if len(ref.URL.Fragment) == 0 {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelError,
			Message:   "missing hash",
			Reference: ref,
		})
	}

	if len(ref.URL.Path) > 0 {
		// internal
		refDoc, _ := hT.documentStore.ResolveRef(ref)
		if !refDoc.IsHashValid(ref.URL.Fragment) {
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelError,
				Message:   "hash does not exist",
				Reference: ref,
			})
		}
	} else {
		// self
		if !ref.Document.IsHashValid(ref.URL.Fragment) {
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelError,
				Message:   "hash does not exist",
				Reference: ref,
			})
		}
	}
}

func (hT *HTMLTest) checkFile(ref *htmldoc.Reference, absPath string) {
	f, err := os.Stat(absPath)
	if os.IsNotExist(err) {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelError,
			Message:   "target does not exist",
			Reference: ref,
		})
		return
	}
	output.CheckErrorPanic(err)

	if f.IsDir() {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelError,
			Message:   "target is a directory, no index",
			Reference: ref,
		})
	}
}

func (hT *HTMLTest) checkMailto(ref *htmldoc.Reference) {
	if !hT.opts.CheckMailto {
		return
	}
	if len(ref.URL.Opaque) == 0 {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelError,
			Message:   "mailto is empty",
			Reference: ref,
		})
		return
	}
	if !strings.Contains(ref.URL.Opaque, "@") {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelError,
			Message:   "contains an invalid email address",
			Reference: ref,
		})
		return
	}
}

func (hT *HTMLTest) checkTel(ref *htmldoc.Reference) {
	if !hT.opts.CheckTel {
		return
	}
	if len(ref.URL.Opaque) == 0 {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelError,
			Message:   "tel is empty",
			Reference: ref,
		})
		return
	}
}
<commit_msg>fix panic when path doesn't exist and have a segment<commit_after>package htmltest

import (
	"github.com/wjdp/htmltest/htmldoc"
	"github.com/wjdp/htmltest/issues"
	"github.com/wjdp/htmltest/output"
	"golang.org/x/net/html"
	"net/http"
	"net/url"
	"os"
	"path"
	"strings"
	"fmt"
)

func (hT *HTMLTest) checkLink(document *htmldoc.Document, node *html.Node) {
	attrs := htmldoc.ExtractAttrs(node.Attr,
		[]string{"href", "rel", hT.opts.IgnoreTagAttribute})

	// Ignore if data-proofer-ignore set
	if htmldoc.AttrPresent(node.Attr, hT.opts.IgnoreTagAttribute) {
		return
	}

	// Check if favicon
	if htmldoc.AttrPresent(node.Attr, "rel") &&
		(attrs["rel"] == "icon" || attrs["rel"] == "shortcut icon") &&
		node.Parent.Data == "head" {
		document.State.FaviconPresent = true
	}

	// Ignore if rel=dns-prefetch, see #40. If we have more cases like this a hashable type should be created and
	// checked against.
	if attrs["rel"] == "dns-prefetch" {
		return
	}

	// Create reference
	ref := htmldoc.NewReference(document, node, attrs["href"])

	// Check for missing href, fail for link nodes
	if !htmldoc.AttrPresent(node.Attr, "href") {
		switch node.Data {
		case "a":
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelDebug,
				Message:   "anchor without href",
				Reference: ref,
			})
			return
		case "link":
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelError,
				Message:   "link tag missing href",
				Reference: ref,
			})
			return
		}
	}

	// Blank href
	if attrs["href"] == "" {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelError,
			Message:   "href blank",
			Reference: ref,
		})
		return
	}

	// href="#"
	if attrs["href"] == "#" {
		if hT.opts.CheckInternalHash && !hT.opts.IgnoreInternalEmptyHash {
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelError,
				Message:   "empty hash",
				Reference: ref,
			})
		}
		return
	}

	// Route reference check
	switch ref.Scheme() {
	case "http":
		hT.enforceHTTPS(ref)
		hT.checkExternal(ref)
	case "https":
		hT.checkExternal(ref)
	case "file":
		hT.checkInternal(ref)
	case "self":
		hT.checkInternalHash(ref)
	case "mailto":
		hT.checkMailto(ref)
	case "tel":
		hT.checkTel(ref)
	}

	// TODO: Other schemes
	// What to do about unknown schemes, could be perfectly valid or a typo.
	// Perhaps show a warning, which can be suppressed per-scheme in options.
	// Preload with a couple of common ones, ftp &c.

}

func (hT *HTMLTest) checkExternal(ref *htmldoc.Reference) {
	if !hT.opts.CheckExternal {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelDebug,
			Message:   "skipping external check",
			Reference: ref,
		})
		return
	}

	urlStr := ref.URLString()

	// Does this url match an url ignore rule?
	if hT.opts.isURLIgnored(urlStr) {
		return
	}

	if hT.opts.StripQueryString && !InList(hT.opts.StripQueryExcludes, urlStr) {
		urlStr = htmldoc.URLStripQueryString(urlStr)
	}
	var statusCode int

	cR, isCached := hT.refCache.Get(urlStr)

	if isCached && statusCodeValid(cR.StatusCode) {
		// If we have a valid result in cache, use that
		statusCode = cR.StatusCode
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelDebug,
			Message:   "from cache",
			Reference: ref,
		})
	} else {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelDebug,
			Message:   "fresh",
			Reference: ref,
		})
		urlURL, err := url.Parse(urlStr)
		req := &http.Request{
			Method: "GET",
			URL:    urlURL,
			Header: map[string][]string{
				"Range": {"bytes=0-0"}, // If server supports prevents body being sent
			},
		}

		hT.httpChannel <- true // Add to http concurrency limiter

		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelInfo,
			Message:   "hitting",
			Reference: ref,
		})

		resp, err := hT.httpClient.Do(req)

		<-hT.httpChannel // Bump off http concurrency limiter

		if err != nil {
			if strings.Contains(err.Error(), "dial tcp") {
				// Remove long prefix
				prefix := "Get " + urlStr + ": dial tcp: lookup "
				cleanedMessage := strings.TrimPrefix(err.Error(), prefix)
				// Add error
				hT.issueStore.AddIssue(issues.Issue{
					Level:     issues.LevelError,
					Message:   cleanedMessage,
					Reference: ref,
				})
				return
			}
			if strings.Contains(err.Error(), "Client.Timeout") {
				hT.issueStore.AddIssue(issues.Issue{
					Level:     issues.LevelError,
					Message:   "request exceeded our ExternalTimeout",
					Reference: ref,
				})
				return
			}

			// Unhandled client error, return generic error
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelError,
				Message:   err.Error(),
				Reference: ref,
			})

			return
		}
		// Save cached result
		hT.refCache.Save(urlStr, resp.StatusCode)
		statusCode = resp.StatusCode
	}

	switch statusCode {
	case http.StatusOK:
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelDebug,
			Message:   http.StatusText(statusCode),
			Reference: ref,
		})
	case http.StatusPartialContent:
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelDebug,
			Message:   http.StatusText(statusCode),
			Reference: ref,
		})
	default:
		attrs := htmldoc.ExtractAttrs(ref.Node.Attr, []string{"rel"})
		if attrs["rel"] == "canonical" && hT.opts.IgnoreCanonicalBrokenLinks {
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelWarning,
				Message:   http.StatusText(statusCode) + " [rel=\"canonical\"]",
				Reference: ref,
			})
		} else {
			// Failed VCRed requests end up here with a status code of zero
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelError,
				Message:   fmt.Sprintf("%s %d", "Non-OK status:", statusCode),
				Reference: ref,
			})
		}
	}

	// TODO check a hash id exists in external page if present in reference (URL.Fragment)
}

func (hT *HTMLTest) checkInternal(ref *htmldoc.Reference) {
	if !hT.opts.CheckInternal {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelDebug,
			Message:   "skipping internal check",
			Reference: ref,
		})
		return
	}

	// First lookup in document store,
	refDoc, refExists := hT.documentStore.ResolveRef(ref)

	if refExists {
		// If path doesn't end in slash and the resolved ref is an index.html, complain
		if (ref.URL.Path[len(ref.URL.Path)-1] != '/') && (path.Base(refDoc.SitePath) == hT.opts.DirectoryIndex) && (!hT.opts.IgnoreDirectoryMissingTrailingSlash) {
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelError,
				Message:   "target is a directory, href lacks trailing slash",
				Reference: ref,
			})
			refExists = false
		}
	} else {
		// If that fails attempt to lookup with filesystem, resolve a path and check
		refOsPath := path.Join(hT.opts.DirectoryPath, ref.RefSitePath())
		refExists = hT.checkFile(ref, refOsPath)
	}

	if refExists && len(ref.URL.Fragment) > 0 {
		// Is also a hash link
		hT.checkInternalHash(ref)
	}
}

func (hT *HTMLTest) checkInternalHash(ref *htmldoc.Reference) {
	if !hT.opts.CheckInternalHash {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelDebug,
			Message:   "skipping hash check",
			Reference: ref,
		})
		return
	}

	// var refDoc *htmldoc.Document
	if len(ref.URL.Fragment) == 0 {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelError,
			Message:   "missing hash",
			Reference: ref,
		})
	}

	if len(ref.URL.Path) > 0 {
		// internal
		refDoc, _ := hT.documentStore.ResolveRef(ref)
		if !refDoc.IsHashValid(ref.URL.Fragment) {
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelError,
				Message:   "hash does not exist",
				Reference: ref,
			})
		}
	} else {
		// self
		if !ref.Document.IsHashValid(ref.URL.Fragment) {
			hT.issueStore.AddIssue(issues.Issue{
				Level:     issues.LevelError,
				Message:   "hash does not exist",
				Reference: ref,
			})
		}
	}
}

func (hT *HTMLTest) checkFile(ref *htmldoc.Reference, absPath string) bool {
	f, err := os.Stat(absPath)
	if os.IsNotExist(err) {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelError,
			Message:   "target does not exist",
			Reference: ref,
		})
		return false
	}
	output.CheckErrorPanic(err)

	if f.IsDir() {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelError,
			Message:   "target is a directory, no index",
			Reference: ref,
		})
		return false
	}
	return true
}

func (hT *HTMLTest) checkMailto(ref *htmldoc.Reference) {
	if !hT.opts.CheckMailto {
		return
	}
	if len(ref.URL.Opaque) == 0 {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelError,
			Message:   "mailto is empty",
			Reference: ref,
		})
		return
	}
	if !strings.Contains(ref.URL.Opaque, "@") {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelError,
			Message:   "contains an invalid email address",
			Reference: ref,
		})
		return
	}
}

func (hT *HTMLTest) checkTel(ref *htmldoc.Reference) {
	if !hT.opts.CheckTel {
		return
	}
	if len(ref.URL.Opaque) == 0 {
		hT.issueStore.AddIssue(issues.Issue{
			Level:     issues.LevelError,
			Message:   "tel is empty",
			Reference: ref,
		})
		return
	}
}
<|endoftext|>


<commit_before>/*
Copyright 2019 The Kubernetes 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.
*/

package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	"sigs.k8s.io/yaml"
)

const (
	gcsSourceDir = "/source"
	gcsLogsDir   = "/logs"
)

type Step struct {
	Name string `yaml:"name"`
	Args []string
}

type CloudBuildYAMLFile struct {
	Steps         []Step `yaml:"steps"`
	Substitutions map[string]string
	Images        []string
}

func getProjectId() (string, error) {
	cmd := exec.Command("gcloud", "config", "get-value", "project")
	projectId, err := cmd.Output()
	if err != nil {
		return "", fmt.Errorf("failed to get project_id: %v", err)
	}
	return string(projectId), nil
}

func getImageName(o options, tag string, config string) (string, error) {
	var cloudbuildyamlFile CloudBuildYAMLFile
	buf, _ := ioutil.ReadFile(o.cloudbuildFile)
	if err := yaml.Unmarshal(buf, &cloudbuildyamlFile); err != nil {
		return "", fmt.Errorf("failed to get image name: %v", err)
	}
	var projectId, _ = getProjectId()
	var imageNames = cloudbuildyamlFile.Images
	r := strings.NewReplacer("$PROJECT_ID", strings.TrimSpace(projectId), "$_GIT_TAG", tag, "$_CONFIG", config)
	var result string
	for _, name := range imageNames {
		result = result + r.Replace(name) + " "
	}
	return result, nil
}

func runCmd(command string, args ...string) error {
	cmd := exec.Command(command, args...)
	cmd.Stderr = os.Stderr
	cmd.Stdout = os.Stdout
	return cmd.Run()
}

func getVersion() (string, error) {
	cmd := exec.Command("git", "describe", "--tags", "--always", "--dirty")
	output, err := cmd.Output()
	if err != nil {
		return "", err
	}
	t := time.Now().Format("20060102")
	return fmt.Sprintf("v%s-%s", t, strings.TrimSpace(string(output))), nil
}

func (o *options) validateConfigDir() error {
	configDir := o.configDir
	dirInfo, err := os.Stat(o.configDir)
	if os.IsNotExist(err) {
		log.Fatalf("Config directory (%s) does not exist", configDir)
	}

	if !dirInfo.IsDir() {
		log.Fatalf("Config directory (%s) is not actually a directory", configDir)
	}

	_, err = os.Stat(o.cloudbuildFile)
	if os.IsNotExist(err) {
		log.Fatalf("%s does not exist", o.cloudbuildFile)
	}

	return nil
}

func (o *options) uploadBuildDir(targetBucket string) (string, error) {
	f, err := ioutil.TempFile("", "")
	if err != nil {
		return "", fmt.Errorf("failed to create temp file: %v", err)
	}
	name := f.Name()
	_ = f.Close()
	defer os.Remove(name)

	log.Printf("Creating source tarball at %s...\n", name)
	var args []string
	if !o.withGitDirectory {
		args = append(args, "--exclude", ".git")
	}
	args = append(args, "-czf", name, ".")
	if err := runCmd("tar", args...); err != nil {
		return "", fmt.Errorf("failed to tar files: %s", err)
	}

	u := uuid.New()
	uploaded := fmt.Sprintf("%s/%s.tgz", targetBucket, u.String())
	log.Printf("Uploading %s to %s...\n", name, uploaded)
	if err := runCmd("gsutil", "cp", name, uploaded); err != nil {
		return "", fmt.Errorf("failed to upload files: %s", err)
	}

	return uploaded, nil
}

func getExtraSubs(o options) map[string]string {
	envs := strings.Split(o.envPassthrough, ",")
	subs := map[string]string{}
	for _, e := range envs {
		e = strings.TrimSpace(e)
		if e != "" {
			subs[e] = os.Getenv(e)
		}
	}
	return subs
}

func runSingleJob(o options, jobName, uploaded, version string, subs map[string]string) error {
	s := make([]string, 0, len(subs)+1)
	for k, v := range subs {
		s = append(s, fmt.Sprintf("_%s=%s", k, v))
	}

	s = append(s, "_GIT_TAG="+version)
	args := []string{
		"builds", "submit",
		"--verbosity", "info",
		"--config", o.cloudbuildFile,
		"--substitutions", strings.Join(s, ","),
	}

	if o.project != "" {
		args = append(args, "--project", o.project)
	}

	if o.scratchBucket != "" {
		args = append(args, "--gcs-log-dir", o.scratchBucket+gcsLogsDir)
		args = append(args, "--gcs-source-staging-dir", o.scratchBucket+gcsSourceDir)
	}

	if uploaded != "" {
		args = append(args, uploaded)
	} else {
		if o.noSource {
			args = append(args, "--no-source")
		} else {
			args = append(args, ".")
		}
	}

	cmd := exec.Command("gcloud", args...)

	var p string
	if o.logDir != "" {
		p = path.Join(o.logDir, strings.Replace(jobName, "/", "-", -1)+".log")
		f, err := os.Create(p)

		if err != nil {
			return fmt.Errorf("couldn't create %s: %v", p, err)
		}

		defer f.Sync()
		defer f.Close()

		cmd.Stdout = f
		cmd.Stderr = f
	} else {
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
	}

	if err := cmd.Run(); err != nil {
		if o.logDir != "" {
			buildLog, _ := ioutil.ReadFile(p)
			fmt.Println(string(buildLog))
		}
		return fmt.Errorf("error running %s: %v", cmd.Args, err)
	}

	return nil
}

type variants map[string]map[string]string

func getVariants(o options) (variants, error) {
	content, err := ioutil.ReadFile(path.Join(o.configDir, "variants.yaml"))
	if err != nil {
		if !os.IsNotExist(err) {
			return nil, fmt.Errorf("failed to load variants.yaml: %v", err)
		}
		if o.variant != "" {
			return nil, fmt.Errorf("no variants.yaml found, but a build variant (%q) was specified", o.variant)
		}
		return nil, nil
	}
	v := struct {
		Variants variants `json:"variants"`
	}{}
	if err := yaml.UnmarshalStrict(content, &v); err != nil {
		return nil, fmt.Errorf("failed to read variants.yaml: %v", err)
	}
	if o.variant != "" {
		va, ok := v.Variants[o.variant]
		if !ok {
			return nil, fmt.Errorf("requested variant %q, which is not present in variants.yaml", o.variant)
		}
		return variants{o.variant: va}, nil
	}
	return v.Variants, nil
}

func runBuildJobs(o options) []error {
	var uploaded string
	if o.scratchBucket != "" {
		if !o.noSource {
			var err error
			uploaded, err = o.uploadBuildDir(o.scratchBucket + gcsSourceDir)
			if err != nil {
				return []error{fmt.Errorf("failed to upload source: %v", err)}
			}
		}
	} else {
		log.Println("Skipping advance upload and relying on gcloud...")
	}

	log.Println("Running build jobs...")
	tag, err := getVersion()
	if err != nil {
		return []error{fmt.Errorf("failed to get current tag: %v", err)}
	}

	if !o.allowDirty && strings.HasSuffix(tag, "-dirty") {
		return []error{fmt.Errorf("the working copy is dirty")}
	}

	vs, err := getVariants(o)
	if err != nil {
		return []error{err}
	}

	if len(vs) == 0 {
		log.Println("No variants.yaml, starting single build job...")
		if err := runSingleJob(o, "build", uploaded, tag, getExtraSubs(o)); err != nil {
			return []error{err}
		}
		var imageName, _ = getImageName(o, tag, "")
		log.Printf("Successfully built image: %v \n", imageName)
		return nil
	}

	log.Printf("Found variants.yaml, starting %d build jobs...\n", len(vs))

	w := sync.WaitGroup{}
	w.Add(len(vs))
	var errors []error
	extraSubs := getExtraSubs(o)
	for k, v := range vs {
		go func(job string, vc map[string]string) {
			defer w.Done()
			log.Printf("Starting job %q...\n", job)
			if err := runSingleJob(o, job, uploaded, tag, mergeMaps(extraSubs, vc)); err != nil {
				errors = append(errors, fmt.Errorf("job %q failed: %v", job, err))
				log.Printf("Job %q failed: %v\n", job, err)
			} else {
				var imageName, _ = getImageName(o, tag, job)
				log.Printf("Successfully built image: %v \n", imageName)
				log.Printf("Job %q completed.\n", job)
			}
		}(k, v)
	}
	w.Wait()
	return errors
}

type options struct {
	buildDir       string
	configDir      string
	cloudbuildFile string
	logDir         string
	scratchBucket  string
	project        string
	allowDirty     bool
	noSource       bool
	variant        string
	envPassthrough string

	// withGitDirectory will include the .git directory when uploading the source to GCB
	withGitDirectory bool
}

func mergeMaps(maps ...map[string]string) map[string]string {
	out := map[string]string{}
	for _, m := range maps {
		for k, v := range m {
			out[k] = v
		}
	}
	return out
}

func parseFlags() options {
	o := options{}
	flag.StringVar(&o.buildDir, "build-dir", "", "If provided, this directory will be uploaded as the source for the Google Cloud Build run.")
	flag.StringVar(&o.cloudbuildFile, "gcb-config", "cloudbuild.yaml", "If provided, this will be used as the name of the Google Cloud Build config file.")
	flag.StringVar(&o.logDir, "log-dir", "", "If provided, build logs will be sent to files in this directory instead of to stdout/stderr.")
	flag.StringVar(&o.scratchBucket, "scratch-bucket", "", "The complete GCS path for Cloud Build to store scratch files (sources, logs).")
	flag.StringVar(&o.project, "project", "", "If specified, use a non-default GCP project.")
	flag.BoolVar(&o.allowDirty, "allow-dirty", false, "If true, allow pushing dirty builds.")
	flag.BoolVar(&o.noSource, "no-source", false, "If true, no source will be uploaded with this build.")
	flag.StringVar(&o.variant, "variant", "", "If specified, build only the given variant. An error if no variants are defined.")
	flag.StringVar(&o.envPassthrough, "env-passthrough", "", "Comma-separated list of specified environment variables to be passed to GCB as substitutions with an _ prefix. If the variable doesn't exist, the substitution will exist but be empty.")
	flag.BoolVar(&o.withGitDirectory, "with-git-dir", o.withGitDirectory, "If true, upload the .git directory to GCB, so we can e.g. get the git log and tag.")

	flag.Parse()

	if flag.NArg() < 1 {
		_, _ = fmt.Fprintln(os.Stderr, "expected a config directory to be provided")
		os.Exit(1)
	}

	o.configDir = strings.TrimSuffix(flag.Arg(0), "/")

	return o
}

func main() {
	o := parseFlags()

	if bazelWorkspace := os.Getenv("BUILD_WORKSPACE_DIRECTORY"); bazelWorkspace != "" {
		if err := os.Chdir(bazelWorkspace); err != nil {
			log.Fatalf("Failed to chdir to bazel workspace (%s): %v", bazelWorkspace, err)
		}
	}

	if o.buildDir == "" {
		o.buildDir = o.configDir
	}

	log.Printf("Build directory: %s\n", o.buildDir)

	// Canonicalize the config directory to be an absolute path.
	// As we're about to cd into the build directory, we need a consistent way to reference the config files
	// when the config directory is not the same as the build directory.
	absConfigDir, absErr := filepath.Abs(o.configDir)
	if absErr != nil {
		log.Fatalf("Could not resolve absolute path for config directory: %v", absErr)
	}

	o.configDir = absConfigDir
	o.cloudbuildFile = path.Join(o.configDir, o.cloudbuildFile)

	configDirErr := o.validateConfigDir()
	if configDirErr != nil {
		log.Fatalf("Could not validate config directory: %v", configDirErr)
	}

	log.Printf("Config directory: %s\n", o.configDir)

	log.Printf("cd-ing to build directory: %s\n", o.buildDir)
	if err := os.Chdir(o.buildDir); err != nil {
		log.Fatalf("Failed to chdir to build directory (%s): %v", o.buildDir, err)
	}

	errors := runBuildJobs(o)
	if len(errors) != 0 {
		log.Fatalf("Failed to run some build jobs: %v", errors)
	}
	log.Println("Finished.")
}
<commit_msg>Update naming for variables and methods<commit_after>/*
Copyright 2019 The Kubernetes 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.
*/

package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	"sigs.k8s.io/yaml"
)

const (
	gcsSourceDir = "/source"
	gcsLogsDir   = "/logs"
)

type Step struct {
	Name string `yaml:"name"`
	Args []string
}

// struct for images/<image>/cloudbuild.yaml
// Example: images/alpine/cloudbuild.yaml
type CloudBuildYAMLFile struct {
	Steps         []Step `yaml:"steps"`
	Substitutions map[string]string
	Images        []string
}

func getProjectID() (string, error) {
	cmd := exec.Command("gcloud", "config", "get-value", "project")
	projectID, err := cmd.Output()
	if err != nil {
		return "", fmt.Errorf("failed to get project_id: %v", err)
	}
	return string(projectID), nil
}

func getImageName(o options, tag string, config string) (string, error) {
	var cloudbuildyamlFile CloudBuildYAMLFile
	buf, _ := ioutil.ReadFile(o.cloudbuildFile)
	if err := yaml.Unmarshal(buf, &cloudbuildyamlFile); err != nil {
		return "", fmt.Errorf("failed to get image name: %v", err)
	}
	var projectID, _ = getProjectID()
	var imageNames = cloudbuildyamlFile.Images
	r := strings.NewReplacer("$PROJECT_ID", strings.TrimSpace(projectID), "$_GIT_TAG", tag, "$_CONFIG", config)
	var result string
	for _, name := range imageNames {
		result = result + r.Replace(name) + " "
	}
	return result, nil
}

func runCmd(command string, args ...string) error {
	cmd := exec.Command(command, args...)
	cmd.Stderr = os.Stderr
	cmd.Stdout = os.Stdout
	return cmd.Run()
}

func getVersion() (string, error) {
	cmd := exec.Command("git", "describe", "--tags", "--always", "--dirty")
	output, err := cmd.Output()
	if err != nil {
		return "", err
	}
	t := time.Now().Format("20060102")
	return fmt.Sprintf("v%s-%s", t, strings.TrimSpace(string(output))), nil
}

func (o *options) validateConfigDir() error {
	configDir := o.configDir
	dirInfo, err := os.Stat(o.configDir)
	if os.IsNotExist(err) {
		log.Fatalf("Config directory (%s) does not exist", configDir)
	}

	if !dirInfo.IsDir() {
		log.Fatalf("Config directory (%s) is not actually a directory", configDir)
	}

	_, err = os.Stat(o.cloudbuildFile)
	if os.IsNotExist(err) {
		log.Fatalf("%s does not exist", o.cloudbuildFile)
	}

	return nil
}

func (o *options) uploadBuildDir(targetBucket string) (string, error) {
	f, err := ioutil.TempFile("", "")
	if err != nil {
		return "", fmt.Errorf("failed to create temp file: %v", err)
	}
	name := f.Name()
	_ = f.Close()
	defer os.Remove(name)

	log.Printf("Creating source tarball at %s...\n", name)
	var args []string
	if !o.withGitDirectory {
		args = append(args, "--exclude", ".git")
	}
	args = append(args, "-czf", name, ".")
	if err := runCmd("tar", args...); err != nil {
		return "", fmt.Errorf("failed to tar files: %s", err)
	}

	u := uuid.New()
	uploaded := fmt.Sprintf("%s/%s.tgz", targetBucket, u.String())
	log.Printf("Uploading %s to %s...\n", name, uploaded)
	if err := runCmd("gsutil", "cp", name, uploaded); err != nil {
		return "", fmt.Errorf("failed to upload files: %s", err)
	}

	return uploaded, nil
}

func getExtraSubs(o options) map[string]string {
	envs := strings.Split(o.envPassthrough, ",")
	subs := map[string]string{}
	for _, e := range envs {
		e = strings.TrimSpace(e)
		if e != "" {
			subs[e] = os.Getenv(e)
		}
	}
	return subs
}

func runSingleJob(o options, jobName, uploaded, version string, subs map[string]string) error {
	s := make([]string, 0, len(subs)+1)
	for k, v := range subs {
		s = append(s, fmt.Sprintf("_%s=%s", k, v))
	}

	s = append(s, "_GIT_TAG="+version)
	args := []string{
		"builds", "submit",
		"--verbosity", "info",
		"--config", o.cloudbuildFile,
		"--substitutions", strings.Join(s, ","),
	}

	if o.project != "" {
		args = append(args, "--project", o.project)
	}

	if o.scratchBucket != "" {
		args = append(args, "--gcs-log-dir", o.scratchBucket+gcsLogsDir)
		args = append(args, "--gcs-source-staging-dir", o.scratchBucket+gcsSourceDir)
	}

	if uploaded != "" {
		args = append(args, uploaded)
	} else {
		if o.noSource {
			args = append(args, "--no-source")
		} else {
			args = append(args, ".")
		}
	}

	cmd := exec.Command("gcloud", args...)

	var logFilePath string
	if o.logDir != "" {
		logFilePath = path.Join(o.logDir, strings.Replace(jobName, "/", "-", -1)+".log")
		f, err := os.Create(logFilePath)

		if err != nil {
			return fmt.Errorf("couldn't create %s: %v", logFilePath, err)
		}

		defer f.Sync()
		defer f.Close()

		cmd.Stdout = f
		cmd.Stderr = f
	} else {
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
	}

	if err := cmd.Run(); err != nil {
		if o.logDir != "" {
			buildLog, _ := ioutil.ReadFile(logFilePath)
			fmt.Println(string(buildLog))
		}
		return fmt.Errorf("error running %s: %v", cmd.Args, err)
	}

	return nil
}

type variants map[string]map[string]string

func getVariants(o options) (variants, error) {
	content, err := ioutil.ReadFile(path.Join(o.configDir, "variants.yaml"))
	if err != nil {
		if !os.IsNotExist(err) {
			return nil, fmt.Errorf("failed to load variants.yaml: %v", err)
		}
		if o.variant != "" {
			return nil, fmt.Errorf("no variants.yaml found, but a build variant (%q) was specified", o.variant)
		}
		return nil, nil
	}
	v := struct {
		Variants variants `json:"variants"`
	}{}
	if err := yaml.UnmarshalStrict(content, &v); err != nil {
		return nil, fmt.Errorf("failed to read variants.yaml: %v", err)
	}
	if o.variant != "" {
		va, ok := v.Variants[o.variant]
		if !ok {
			return nil, fmt.Errorf("requested variant %q, which is not present in variants.yaml", o.variant)
		}
		return variants{o.variant: va}, nil
	}
	return v.Variants, nil
}

func runBuildJobs(o options) []error {
	var uploaded string
	if o.scratchBucket != "" {
		if !o.noSource {
			var err error
			uploaded, err = o.uploadBuildDir(o.scratchBucket + gcsSourceDir)
			if err != nil {
				return []error{fmt.Errorf("failed to upload source: %v", err)}
			}
		}
	} else {
		log.Println("Skipping advance upload and relying on gcloud...")
	}

	log.Println("Running build jobs...")
	tag, err := getVersion()
	if err != nil {
		return []error{fmt.Errorf("failed to get current tag: %v", err)}
	}

	if !o.allowDirty && strings.HasSuffix(tag, "-dirty") {
		return []error{fmt.Errorf("the working copy is dirty")}
	}

	vs, err := getVariants(o)
	if err != nil {
		return []error{err}
	}

	if len(vs) == 0 {
		log.Println("No variants.yaml, starting single build job...")
		if err := runSingleJob(o, "build", uploaded, tag, getExtraSubs(o)); err != nil {
			return []error{err}
		}
		var imageName, _ = getImageName(o, tag, "")
		log.Printf("Successfully built image: %v \n", imageName)
		return nil
	}

	log.Printf("Found variants.yaml, starting %d build jobs...\n", len(vs))

	w := sync.WaitGroup{}
	w.Add(len(vs))
	var errors []error
	extraSubs := getExtraSubs(o)
	for k, v := range vs {
		go func(job string, vc map[string]string) {
			defer w.Done()
			log.Printf("Starting job %q...\n", job)
			if err := runSingleJob(o, job, uploaded, tag, mergeMaps(extraSubs, vc)); err != nil {
				errors = append(errors, fmt.Errorf("job %q failed: %v", job, err))
				log.Printf("Job %q failed: %v\n", job, err)
			} else {
				var imageName, _ = getImageName(o, tag, job)
				log.Printf("Successfully built image: %v \n", imageName)
				log.Printf("Job %q completed.\n", job)
			}
		}(k, v)
	}
	w.Wait()
	return errors
}

type options struct {
	buildDir       string
	configDir      string
	cloudbuildFile string
	logDir         string
	scratchBucket  string
	project        string
	allowDirty     bool
	noSource       bool
	variant        string
	envPassthrough string

	// withGitDirectory will include the .git directory when uploading the source to GCB
	withGitDirectory bool
}

func mergeMaps(maps ...map[string]string) map[string]string {
	out := map[string]string{}
	for _, m := range maps {
		for k, v := range m {
			out[k] = v
		}
	}
	return out
}

func parseFlags() options {
	o := options{}
	flag.StringVar(&o.buildDir, "build-dir", "", "If provided, this directory will be uploaded as the source for the Google Cloud Build run.")
	flag.StringVar(&o.cloudbuildFile, "gcb-config", "cloudbuild.yaml", "If provided, this will be used as the name of the Google Cloud Build config file.")
	flag.StringVar(&o.logDir, "log-dir", "", "If provided, build logs will be sent to files in this directory instead of to stdout/stderr.")
	flag.StringVar(&o.scratchBucket, "scratch-bucket", "", "The complete GCS path for Cloud Build to store scratch files (sources, logs).")
	flag.StringVar(&o.project, "project", "", "If specified, use a non-default GCP project.")
	flag.BoolVar(&o.allowDirty, "allow-dirty", false, "If true, allow pushing dirty builds.")
	flag.BoolVar(&o.noSource, "no-source", false, "If true, no source will be uploaded with this build.")
	flag.StringVar(&o.variant, "variant", "", "If specified, build only the given variant. An error if no variants are defined.")
	flag.StringVar(&o.envPassthrough, "env-passthrough", "", "Comma-separated list of specified environment variables to be passed to GCB as substitutions with an _ prefix. If the variable doesn't exist, the substitution will exist but be empty.")
	flag.BoolVar(&o.withGitDirectory, "with-git-dir", o.withGitDirectory, "If true, upload the .git directory to GCB, so we can e.g. get the git log and tag.")

	flag.Parse()

	if flag.NArg() < 1 {
		_, _ = fmt.Fprintln(os.Stderr, "expected a config directory to be provided")
		os.Exit(1)
	}

	o.configDir = strings.TrimSuffix(flag.Arg(0), "/")

	return o
}

func main() {
	o := parseFlags()

	if bazelWorkspace := os.Getenv("BUILD_WORKSPACE_DIRECTORY"); bazelWorkspace != "" {
		if err := os.Chdir(bazelWorkspace); err != nil {
			log.Fatalf("Failed to chdir to bazel workspace (%s): %v", bazelWorkspace, err)
		}
	}

	if o.buildDir == "" {
		o.buildDir = o.configDir
	}

	log.Printf("Build directory: %s\n", o.buildDir)

	// Canonicalize the config directory to be an absolute path.
	// As we're about to cd into the build directory, we need a consistent way to reference the config files
	// when the config directory is not the same as the build directory.
	absConfigDir, absErr := filepath.Abs(o.configDir)
	if absErr != nil {
		log.Fatalf("Could not resolve absolute path for config directory: %v", absErr)
	}

	o.configDir = absConfigDir
	o.cloudbuildFile = path.Join(o.configDir, o.cloudbuildFile)

	configDirErr := o.validateConfigDir()
	if configDirErr != nil {
		log.Fatalf("Could not validate config directory: %v", configDirErr)
	}

	log.Printf("Config directory: %s\n", o.configDir)

	log.Printf("cd-ing to build directory: %s\n", o.buildDir)
	if err := os.Chdir(o.buildDir); err != nil {
		log.Fatalf("Failed to chdir to build directory (%s): %v", o.buildDir, err)
	}

	errors := runBuildJobs(o)
	if len(errors) != 0 {
		log.Fatalf("Failed to run some build jobs: %v", errors)
	}
	log.Println("Finished.")
}
<|endoftext|>