<commit_before>package main

import (
	"bytes"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"sync"

	"github.com/gin-gonic/gin"
)

const VERSION = "0.1.1"

type Response struct {
	Success bool   `json:"success"`
	Message string `json:"message"`
}

type StatusResponse struct {
	Running  bool   `json:"running"`            // True if player is running
	File     string `json:"file"`               // Path to current media file
	Name     string `json:"name"`               // Titleized filename
	Position string `json:"position,omitempty"` // Current position in the movie
	Duration string `json:"duration,omitempty"` // Movie duration
}

type FileEntry struct {
	Filename string `json:"filename"`
	IsDir    bool   `json:"directory"`
}

var (
	// Regular expression to match all supported video files
	RegexFormats = regexp.MustCompile(`.(avi|mpg|mov|flv|wmv|asf|mpeg|m4v|divx|mp4|mkv)$`)

	// Regular expression to convert filenames to titles
	RegexBrackets = regexp.MustCompile(`[\(\[\]\)]`)
	RegexYear     = regexp.MustCompile(`((19|20)[\d]{2})`)
	RegexEpisode  = regexp.MustCompile(`(?i)S[\d]+E[\d]+`)
	RegexJunk     = regexp.MustCompile(`(?i)(1080p|720p|3d|brrip|bluray|webrip|x264|aac)`)
	RegexSpace    = regexp.MustCompile(`\s{2,}`)

	// OMXPlayer control commands, these are piped via STDIN to omxplayer process
	Commands = map[string]string{
		"pause":             "p",            // Pause/continue playback
		"stop":              "q",            // Stop playback and exit
		"volume_up":         "+",            // Change volume by +3dB
		"volume_down":       "-",            // Change volume by -3dB
		"subtitles":         "s",            // Enable/disable subtitles
		"seek_back":         "\x1b\x5b\x44", // Seek -30 seconds
		"seek_back_fast":    "\x1b\x5b\x42", // Seek -600 second
		"seek_forward":      "\x1b\x5b\x43", // Seek +30 second
		"seek_forward_fast": "\x1b\x5b\x41", // Seek +600 seconds
	}

	MediaPath   string         // Path where all media files are stored
	OmxPath     string         // Path to omxplayer executable
	Omx         *exec.Cmd      // Child process for spawning omxplayer
	OmxIn       io.WriteCloser // Child process STDIN pipe to send commands
	Command     chan string    // Channel to pass along commands to the player routine
	CurrentFile string         // Currently playing media file name
	Zeroconf    bool           // Enable Zeroconf discovery
	Frontend    bool           // Serve frontend app
	stream      *Stream        // Current stream
)

func httpBrowse(c *gin.Context) {
	path := c.Request.FormValue("path")

	if path != "" {
		path = fmt.Sprintf("%s/%s", MediaPath, path)
	} else {
		path = MediaPath
	}

	c.JSON(200, scanPath(path))
}

func httpCommand(c *gin.Context) {
	val := c.Params.ByName("command")

	if _, ok := Commands[val]; !ok {
		c.JSON(400, Response{false, "Invalid command"})
		return
	}

	fmt.Println("Received command:", val)

	// Handle requested commmand
	Command <- val

	c.JSON(200, Response{true, "OK"})
}

func httpServe(c *gin.Context) {
	file := c.Request.URL.Query().Get("file")
	if file == "" {
		return
	}

	file = fmt.Sprintf("%s/%s", MediaPath, file)
	if !fileExists(file) {
		c.String(404, "Not found")
		return
	}

	if !omxCanPlay(file) {
		c.String(400, "Invalid format")
		return
	}

	http.ServeFile(c.Writer, c.Request, file)
}

func httpPlay(c *gin.Context) {
	if omxIsActive() {
		c.JSON(400, Response{false, "Player is already running"})
		return
	}

	file := c.Request.FormValue("file")
	if file == "" {
		c.JSON(400, Response{false, "File is required"})
		return
	}

	file = fmt.Sprintf("%s/%s", MediaPath, file)

	if !fileExists(file) {
		c.JSON(400, Response{false, "File does not exist"})
		return
	}

	if !omxCanPlay(file) {
		c.JSON(400, Response{false, "File cannot be played"})
		return
	}

	go omxPlay(file)

	c.JSON(200, Response{true, "OK"})
}

func httpRemoveFile(c *gin.Context) {
	file := c.Request.FormValue("file")
	if file == "" {
		c.JSON(400, Response{false, "File is required"})
		return
	}

	fullPath := fmt.Sprintf("%s/%s", MediaPath, file)
	if !fileExists(fullPath) {
		c.JSON(400, Response{false, "File does not exist"})
		return
	}

	if err := os.RemoveAll(fullPath); err != nil {
		c.JSON(400, Response{false, err.Error()})
		return
	}

	c.JSON(200, Response{true, "OK"})
}

func httpStatus(c *gin.Context) {
	resp := StatusResponse{
		Running: omxIsActive(),
		File:    CurrentFile,
		Name:    fileToTitle(filepath.Base(CurrentFile)),
	}

	if stream != nil {
		resp.Duration = durationFromSeconds(stream.duration)
		resp.Position = stream.pos.String()
	}

	c.JSON(200, resp)
}

func httpIndex(c *gin.Context) {
	data, err := Asset("static/index.html")

	if err != nil {
		c.String(400, err.Error())
		return
	}

	c.Data(200, "text/html; charset=utf-8", data)
}

// Retrieve information about the host: uptime, storage, etc
func httpHost(c *gin.Context) {
	output := map[string]string{}
	commands := map[string]string{
		"os":      "uname -a",
		"uptime":  "uptime",
		"storage": "df -m",
		"memory":  "free -m",
	}

	wg := &sync.WaitGroup{}
	wg.Add(len(commands))

	for k, v := range commands {
		go func(key, name string) {
			defer wg.Done()

			args := strings.Split(name, " ")

			out := bytes.NewBuffer(nil)
			cmd := exec.Command(args[0], args[1:]...)
			cmd.Stdout = out
			cmd.Stderr = out

			if err := cmd.Run(); err != nil {
				log.Println("Failed to execute command", k, err)
				output[key] = "N/A"
				return
			}

			output[key] = strings.TrimSpace(out.String())
		}(k, v)
	}

	wg.Wait()

	c.JSON(200, map[string]interface{}{
		"uptime":  output["uptime"],
		"os":      output["os"],
		"storage": parseStorageInfo(output["storage"]),
		"memory":  parseMemoryInfo(output["memory"]),
	})
}

func terminate(message string, code int) {
	fmt.Println(message)
	os.Exit(code)
}

func usage() {
	terminate("Usage: omxremote path/to/media/dir", 0)
}

func init() {
	var printVersion bool

	flag.StringVar(&MediaPath, "media", "./", "Path to media files")
	flag.BoolVar(&Frontend, "frontend", true, "Enable frontend applicaiton")
	flag.BoolVar(&Zeroconf, "zeroconf", false, "Enable service advertisement with Zeroconf")
	flag.BoolVar(&printVersion, "v", false, "Print version")
	flag.Parse()

	if printVersion {
		fmt.Printf("omxremote v%v\n", VERSION)
		os.Exit(0)
	}
}

func main() {
	// Expand media path if needed
	MediaPath = strings.Replace(MediaPath, "~", os.Getenv("HOME"), 1)

	// Get path from arguments and remove trailing slash
	MediaPath = strings.TrimRight(MediaPath, "/")

	if !fileExists(MediaPath) {
		terminate(fmt.Sprintf("Directory does not exist: %s", MediaPath), 1)
	}

	// Check if player is installed
	if omxDetect() != nil {
		terminate("omxplayer is not installed", 1)
	}

	// Make sure nothing is running
	omxCleanup()

	// Start a remote command listener
	go omxListen()

	// Start zeroconf service advertisement
	if Zeroconf {
		stopZeroconf := make(chan bool)
		go startZeroConfAdvertisement(stopZeroconf)
	}

	// Disable debugging mode
	gin.SetMode("release")

	// Setup HTTP server
	router := gin.Default()

	// Handle CORS
	router.Use(func(c *gin.Context) {
		c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
		c.Header("Access-Control-Allow-Origin", "*")
		c.Header("Access-Control-Expose-Headers", "*")
	})

	// Server frontend application only if its enabled
	if Frontend == true {
		router.GET("/", httpIndex)
	}

	router.GET("/status", httpStatus)
	router.GET("/browse", httpBrowse)
	router.GET("/play", httpPlay)
	router.GET("/serve", httpServe)
	router.POST("/remove", httpRemoveFile)
	router.GET("/command/:command", httpCommand)
	router.GET("/host", httpHost)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	host := os.Getenv("HOST")
	if host == "" {
		host = "0.0.0.0"
	}

	fmt.Println("Starting server on " + host + ":" + port)
	router.Run(host + ":" + port)
}
<commit_msg>Trim space on path to be removed<commit_after>package main

import (
	"bytes"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"sync"

	"github.com/gin-gonic/gin"
)

const VERSION = "0.1.1"

type Response struct {
	Success bool   `json:"success"`
	Message string `json:"message"`
}

type StatusResponse struct {
	Running  bool   `json:"running"`            // True if player is running
	File     string `json:"file"`               // Path to current media file
	Name     string `json:"name"`               // Titleized filename
	Position string `json:"position,omitempty"` // Current position in the movie
	Duration string `json:"duration,omitempty"` // Movie duration
}

type FileEntry struct {
	Filename string `json:"filename"`
	IsDir    bool   `json:"directory"`
}

var (
	// Regular expression to match all supported video files
	RegexFormats = regexp.MustCompile(`.(avi|mpg|mov|flv|wmv|asf|mpeg|m4v|divx|mp4|mkv)$`)

	// Regular expression to convert filenames to titles
	RegexBrackets = regexp.MustCompile(`[\(\[\]\)]`)
	RegexYear     = regexp.MustCompile(`((19|20)[\d]{2})`)
	RegexEpisode  = regexp.MustCompile(`(?i)S[\d]+E[\d]+`)
	RegexJunk     = regexp.MustCompile(`(?i)(1080p|720p|3d|brrip|bluray|webrip|x264|aac)`)
	RegexSpace    = regexp.MustCompile(`\s{2,}`)

	// OMXPlayer control commands, these are piped via STDIN to omxplayer process
	Commands = map[string]string{
		"pause":             "p",            // Pause/continue playback
		"stop":              "q",            // Stop playback and exit
		"volume_up":         "+",            // Change volume by +3dB
		"volume_down":       "-",            // Change volume by -3dB
		"subtitles":         "s",            // Enable/disable subtitles
		"seek_back":         "\x1b\x5b\x44", // Seek -30 seconds
		"seek_back_fast":    "\x1b\x5b\x42", // Seek -600 second
		"seek_forward":      "\x1b\x5b\x43", // Seek +30 second
		"seek_forward_fast": "\x1b\x5b\x41", // Seek +600 seconds
	}

	MediaPath   string         // Path where all media files are stored
	OmxPath     string         // Path to omxplayer executable
	Omx         *exec.Cmd      // Child process for spawning omxplayer
	OmxIn       io.WriteCloser // Child process STDIN pipe to send commands
	Command     chan string    // Channel to pass along commands to the player routine
	CurrentFile string         // Currently playing media file name
	Zeroconf    bool           // Enable Zeroconf discovery
	Frontend    bool           // Serve frontend app
	stream      *Stream        // Current stream
)

func httpBrowse(c *gin.Context) {
	path := c.Request.FormValue("path")

	if path != "" {
		path = fmt.Sprintf("%s/%s", MediaPath, path)
	} else {
		path = MediaPath
	}

	c.JSON(200, scanPath(path))
}

func httpCommand(c *gin.Context) {
	val := c.Params.ByName("command")

	if _, ok := Commands[val]; !ok {
		c.JSON(400, Response{false, "Invalid command"})
		return
	}

	fmt.Println("Received command:", val)

	// Handle requested commmand
	Command <- val

	c.JSON(200, Response{true, "OK"})
}

func httpServe(c *gin.Context) {
	file := c.Request.URL.Query().Get("file")
	if file == "" {
		return
	}

	file = fmt.Sprintf("%s/%s", MediaPath, file)
	if !fileExists(file) {
		c.String(404, "Not found")
		return
	}

	if !omxCanPlay(file) {
		c.String(400, "Invalid format")
		return
	}

	http.ServeFile(c.Writer, c.Request, file)
}

func httpPlay(c *gin.Context) {
	if omxIsActive() {
		c.JSON(400, Response{false, "Player is already running"})
		return
	}

	file := c.Request.FormValue("file")
	if file == "" {
		c.JSON(400, Response{false, "File is required"})
		return
	}

	file = fmt.Sprintf("%s/%s", MediaPath, file)

	if !fileExists(file) {
		c.JSON(400, Response{false, "File does not exist"})
		return
	}

	if !omxCanPlay(file) {
		c.JSON(400, Response{false, "File cannot be played"})
		return
	}

	go omxPlay(file)

	c.JSON(200, Response{true, "OK"})
}

func httpRemoveFile(c *gin.Context) {
	file := strings.TrimSpace(c.Request.FormValue("file"))
	if file == "" {
		c.JSON(400, Response{false, "File is required"})
		return
	}

	fullPath := fmt.Sprintf("%s/%s", MediaPath, file)
	if !fileExists(fullPath) {
		c.JSON(400, Response{false, "File does not exist"})
		return
	}

	if err := os.RemoveAll(fullPath); err != nil {
		c.JSON(400, Response{false, err.Error()})
		return
	}

	c.JSON(200, Response{true, "OK"})
}

func httpStatus(c *gin.Context) {
	resp := StatusResponse{
		Running: omxIsActive(),
		File:    CurrentFile,
		Name:    fileToTitle(filepath.Base(CurrentFile)),
	}

	if stream != nil {
		resp.Duration = durationFromSeconds(stream.duration)
		resp.Position = stream.pos.String()
	}

	c.JSON(200, resp)
}

func httpIndex(c *gin.Context) {
	data, err := Asset("static/index.html")

	if err != nil {
		c.String(400, err.Error())
		return
	}

	c.Data(200, "text/html; charset=utf-8", data)
}

// Retrieve information about the host: uptime, storage, etc
func httpHost(c *gin.Context) {
	output := map[string]string{}
	commands := map[string]string{
		"os":      "uname -a",
		"uptime":  "uptime",
		"storage": "df -m",
		"memory":  "free -m",
	}

	wg := &sync.WaitGroup{}
	wg.Add(len(commands))

	for k, v := range commands {
		go func(key, name string) {
			defer wg.Done()

			args := strings.Split(name, " ")

			out := bytes.NewBuffer(nil)
			cmd := exec.Command(args[0], args[1:]...)
			cmd.Stdout = out
			cmd.Stderr = out

			if err := cmd.Run(); err != nil {
				log.Println("Failed to execute command", k, err)
				output[key] = "N/A"
				return
			}

			output[key] = strings.TrimSpace(out.String())
		}(k, v)
	}

	wg.Wait()

	c.JSON(200, map[string]interface{}{
		"uptime":  output["uptime"],
		"os":      output["os"],
		"storage": parseStorageInfo(output["storage"]),
		"memory":  parseMemoryInfo(output["memory"]),
	})
}

func terminate(message string, code int) {
	fmt.Println(message)
	os.Exit(code)
}

func usage() {
	terminate("Usage: omxremote path/to/media/dir", 0)
}

func init() {
	var printVersion bool

	flag.StringVar(&MediaPath, "media", "./", "Path to media files")
	flag.BoolVar(&Frontend, "frontend", true, "Enable frontend applicaiton")
	flag.BoolVar(&Zeroconf, "zeroconf", false, "Enable service advertisement with Zeroconf")
	flag.BoolVar(&printVersion, "v", false, "Print version")
	flag.Parse()

	if printVersion {
		fmt.Printf("omxremote v%v\n", VERSION)
		os.Exit(0)
	}
}

func main() {
	// Expand media path if needed
	MediaPath = strings.Replace(MediaPath, "~", os.Getenv("HOME"), 1)

	// Get path from arguments and remove trailing slash
	MediaPath = strings.TrimRight(MediaPath, "/")

	if !fileExists(MediaPath) {
		terminate(fmt.Sprintf("Directory does not exist: %s", MediaPath), 1)
	}

	// Check if player is installed
	if omxDetect() != nil {
		terminate("omxplayer is not installed", 1)
	}

	// Make sure nothing is running
	omxCleanup()

	// Start a remote command listener
	go omxListen()

	// Start zeroconf service advertisement
	if Zeroconf {
		stopZeroconf := make(chan bool)
		go startZeroConfAdvertisement(stopZeroconf)
	}

	// Disable debugging mode
	gin.SetMode("release")

	// Setup HTTP server
	router := gin.Default()

	// Handle CORS
	router.Use(func(c *gin.Context) {
		c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
		c.Header("Access-Control-Allow-Origin", "*")
		c.Header("Access-Control-Expose-Headers", "*")
	})

	// Server frontend application only if its enabled
	if Frontend == true {
		router.GET("/", httpIndex)
	}

	router.GET("/status", httpStatus)
	router.GET("/browse", httpBrowse)
	router.GET("/play", httpPlay)
	router.GET("/serve", httpServe)
	router.POST("/remove", httpRemoveFile)
	router.GET("/command/:command", httpCommand)
	router.GET("/host", httpHost)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	host := os.Getenv("HOST")
	if host == "" {
		host = "0.0.0.0"
	}

	fmt.Println("Starting server on " + host + ":" + port)
	router.Run(host + ":" + port)
}
<|endoftext|>


<commit_before>package magicsql

import (
	"database/sql"
	"errors"
	"fmt"
	"log"
)

// Querier defines an interface for top-level sql types that can run SQL and
// prepare statements
type Querier interface {
	Query(string, ...interface{}) (*sql.Rows, error)
	Exec(string, ...interface{}) (sql.Result, error)
	Prepare(string) (*sql.Stmt, error)
}

// Operation represents a short-lived single-purpose combination of database
// calls.  On the first failure, its internal error is set, which all
// "children" (statements, transactions, etc) will see.  All children will
// refuse to perform any functions once an error has occurred, making it safe
// to perform a chain of related database calls and only check for an error
// when it makes sense.
//
// When a transaction is started, the operation will route all database calls
// through the transaction instead of the global database handler.  At this
// time, only one transaction at a time is supported (i.e., no nesting
// transactions).
type Operation struct {
	parent *DB
	err    error
	tx     *sql.Tx
	q      Querier
	Dbg    bool
}

// NewOperation creates an operation in its default state: its parent is the
// passed-in DB instance, and it defaults to using direct database calls until
// a transaction is started.
func NewOperation(db *DB) *Operation {
	var o = &Operation{parent: db}
	o.q = o.parent.db

	return o
}

// Err returns the *first* error which occurred on any database call owned by the Operation
func (op *Operation) Err() error {
	return op.err
}

// SetErr tells the Operation to stop handling any more queries.  It shouldn't
// usually be called directly, but it can be if you need to tell the object
// "here's a thing that may be an error; don't do any more work if it is".
func (op *Operation) SetErr(err error) {
	if op.Err() != nil {
		return
	}

	op.err = err
}

// Query wraps sql's Query, returning a wrapped Rows object
func (op *Operation) Query(query string, args ...interface{}) *Rows {
	if op.Err() != nil {
		return &Rows{nil, op}
	}

	if op.Dbg {
		log.Printf("DEBUG - Querying: %s, %#v", query, args)
	}

	var r, err = op.q.Query(query, args...)
	op.SetErr(err)
	return &Rows{r, op}
}

// Exec wraps sql's DB.Exec, returning a wrapped Result
func (op *Operation) Exec(query string, args ...interface{}) *Result {
	if op.Err() != nil {
		return &Result{nil, op}
	}

	if op.Dbg {
		log.Printf("DEBUG - Executing: %s, %#v", query, args)
	}

	var r, err = op.q.Exec(query, args...)
	op.SetErr(err)
	return &Result{r, op}
}

// Prepare wrap's sql's DB.Prepare, returning a wrapped Stmt.  The statement
// must be closed by the caller or eventually MySQL will run out of prepared
// statements.
func (op *Operation) Prepare(query string) *Stmt {
	if op.Err() != nil {
		return &Stmt{nil, op}
	}

	if op.Dbg {
		log.Printf("DEBUG - Preparing: %s", query)
	}

	var st, err = op.q.Prepare(query)
	op.SetErr(err)
	return &Stmt{st, op}
}

// Reset clears the error if any is present
func (op *Operation) Reset() {
	op.err = nil
}

// BeginTransaction wraps sql's Begin and uses a wrapped sql.Tx to dispatch
// Query, Exec, and Prepare calls.  When the transaction is complete, instead
// of manually rolling back or committing, simply call op.EndTransaction() and
// it will rollback / commit based on the error state.  If you need to force a
// rollback, set an error manually with Operation.SetErr().
//
// If a transaction is started while one is already in progress, the operation
// gets into an error state (i.e., nested transactions are not supported).
func (op *Operation) BeginTransaction() {
	if op.Err() != nil {
		return
	}

	if op.tx != nil {
		op.SetErr(errors.New("cannot nest transactions"))
		return
	}

	var tx, err = op.parent.db.Begin()
	op.tx = tx
	op.SetErr(err)
	op.q = tx
}

// EndTransaction commits the transaction if no errors occurred, or rolls back
// if there was an error
func (op *Operation) EndTransaction() {
	// If there was never a transaction due to errors, this could happen and we
	// don't want a panic
	if op.tx == nil {
		return
	}

	if op.Err() != nil {
		op.tx.Rollback()
	} else {
		op.SetErr(op.tx.Commit())
	}

	op.tx = nil
	op.q = op.parent.db
}

// Table creates an OperationTable tied to the given table name and reflecting
// on obj's type to auto-build certain SQL statements
func (op *Operation) Table(tableName string, obj interface{}) *OperationTable {
	var mt = &MagicTable{Object: obj, Name: tableName}
	mt.Configure(nil)
	return &OperationTable{op: op, t: mt}
}

// OperationTable allows tying a stored MagicTable to this specific operation,
// rather than passing table name and an empty interface to Select() and Save()
func (op *Operation) OperationTable(mt *MagicTable) *OperationTable {
	return &OperationTable{op: op, t: mt}
}

// Save wraps Operation.Table() and Table.Save().  It creates an INSERT or
// UPDATE statement for the given object based on whether its primary key is
// zero.  Stores any errors the database returns, and fails if obj isn't tagged
// with a primary key field.
func (op *Operation) Save(tableName string, obj interface{}) *Result {
	var emptyResult = &Result{nil, op}

	if op.Err() != nil {
		return emptyResult
	}

	var ot = op.Table(tableName, obj)
	if ot.t.primaryKey == nil {
		op.SetErr(fmt.Errorf("no primary key tagged for structure %s", ot.t.RType.Name()))
		return emptyResult
	}

	return ot.Save(obj)
}

// Select wraps Operation.Table() and Table.Select().  It creates a Select
// object for further refining.
func (op *Operation) Select(tableName string, obj interface{}) Select {
	return op.Table(tableName, obj).Select()
}
<commit_msg>Add an explicit Rollback method<commit_after>package magicsql

import (
	"database/sql"
	"errors"
	"fmt"
	"log"
)

// Querier defines an interface for top-level sql types that can run SQL and
// prepare statements
type Querier interface {
	Query(string, ...interface{}) (*sql.Rows, error)
	Exec(string, ...interface{}) (sql.Result, error)
	Prepare(string) (*sql.Stmt, error)
}

// Operation represents a short-lived single-purpose combination of database
// calls.  On the first failure, its internal error is set, which all
// "children" (statements, transactions, etc) will see.  All children will
// refuse to perform any functions once an error has occurred, making it safe
// to perform a chain of related database calls and only check for an error
// when it makes sense.
//
// When a transaction is started, the operation will route all database calls
// through the transaction instead of the global database handler.  At this
// time, only one transaction at a time is supported (i.e., no nesting
// transactions).
type Operation struct {
	parent *DB
	err    error
	tx     *sql.Tx
	q      Querier
	Dbg    bool
}

// NewOperation creates an operation in its default state: its parent is the
// passed-in DB instance, and it defaults to using direct database calls until
// a transaction is started.
func NewOperation(db *DB) *Operation {
	var o = &Operation{parent: db}
	o.q = o.parent.db

	return o
}

// Err returns the *first* error which occurred on any database call owned by the Operation
func (op *Operation) Err() error {
	return op.err
}

// SetErr tells the Operation to stop handling any more queries.  It shouldn't
// usually be called directly, but it can be if you need to tell the object
// "here's a thing that may be an error; don't do any more work if it is".
func (op *Operation) SetErr(err error) {
	if op.Err() != nil {
		return
	}

	op.err = err
}

// Query wraps sql's Query, returning a wrapped Rows object
func (op *Operation) Query(query string, args ...interface{}) *Rows {
	if op.Err() != nil {
		return &Rows{nil, op}
	}

	if op.Dbg {
		log.Printf("DEBUG - Querying: %s, %#v", query, args)
	}

	var r, err = op.q.Query(query, args...)
	op.SetErr(err)
	return &Rows{r, op}
}

// Exec wraps sql's DB.Exec, returning a wrapped Result
func (op *Operation) Exec(query string, args ...interface{}) *Result {
	if op.Err() != nil {
		return &Result{nil, op}
	}

	if op.Dbg {
		log.Printf("DEBUG - Executing: %s, %#v", query, args)
	}

	var r, err = op.q.Exec(query, args...)
	op.SetErr(err)
	return &Result{r, op}
}

// Prepare wrap's sql's DB.Prepare, returning a wrapped Stmt.  The statement
// must be closed by the caller or eventually MySQL will run out of prepared
// statements.
func (op *Operation) Prepare(query string) *Stmt {
	if op.Err() != nil {
		return &Stmt{nil, op}
	}

	if op.Dbg {
		log.Printf("DEBUG - Preparing: %s", query)
	}

	var st, err = op.q.Prepare(query)
	op.SetErr(err)
	return &Stmt{st, op}
}

// Reset clears the error if any is present
func (op *Operation) Reset() {
	op.err = nil
}

// BeginTransaction wraps sql's Begin and uses a wrapped sql.Tx to dispatch
// Query, Exec, and Prepare calls.  When the transaction is complete, instead
// of manually rolling back or committing, simply call op.EndTransaction() and
// it will rollback / commit based on the error state.  If you need to force a
// rollback, set an error manually with Operation.SetErr().
//
// If a transaction is started while one is already in progress, the operation
// gets into an error state (i.e., nested transactions are not supported).
func (op *Operation) BeginTransaction() {
	if op.Err() != nil {
		return
	}

	if op.tx != nil {
		op.SetErr(errors.New("cannot nest transactions"))
		return
	}

	var tx, err = op.parent.db.Begin()
	op.tx = tx
	op.SetErr(err)
	op.q = tx
}

// Rollback tries to roll back the transaction even if there is no error
func (op *Operation) Rollback() {
	// If there was never a transaction due to errors, this could happen and we
	// don't want a panic
	if op.tx == nil {
		return
	}

	op.tx.Rollback()
	op.tx = nil
	op.q = op.parent.db
}

// EndTransaction commits the transaction if no errors occurred, or rolls back
// if there was an error
func (op *Operation) EndTransaction() {
	// If there was never a transaction due to errors, this could happen and we
	// don't want a panic
	if op.tx == nil {
		return
	}

	if op.Err() != nil {
		op.tx.Rollback()
	} else {
		op.SetErr(op.tx.Commit())
	}

	op.tx = nil
	op.q = op.parent.db
}

// Table creates an OperationTable tied to the given table name and reflecting
// on obj's type to auto-build certain SQL statements
func (op *Operation) Table(tableName string, obj interface{}) *OperationTable {
	var mt = &MagicTable{Object: obj, Name: tableName}
	mt.Configure(nil)
	return &OperationTable{op: op, t: mt}
}

// OperationTable allows tying a stored MagicTable to this specific operation,
// rather than passing table name and an empty interface to Select() and Save()
func (op *Operation) OperationTable(mt *MagicTable) *OperationTable {
	return &OperationTable{op: op, t: mt}
}

// Save wraps Operation.Table() and Table.Save().  It creates an INSERT or
// UPDATE statement for the given object based on whether its primary key is
// zero.  Stores any errors the database returns, and fails if obj isn't tagged
// with a primary key field.
func (op *Operation) Save(tableName string, obj interface{}) *Result {
	var emptyResult = &Result{nil, op}

	if op.Err() != nil {
		return emptyResult
	}

	var ot = op.Table(tableName, obj)
	if ot.t.primaryKey == nil {
		op.SetErr(fmt.Errorf("no primary key tagged for structure %s", ot.t.RType.Name()))
		return emptyResult
	}

	return ot.Save(obj)
}

// Select wraps Operation.Table() and Table.Select().  It creates a Select
// object for further refining.
func (op *Operation) Select(tableName string, obj interface{}) Select {
	return op.Table(tableName, obj).Select()
}
<|endoftext|>


<commit_before>package php

import (
	"stephensearles.com/php/ast"
	"stephensearles.com/php/token"
)

type operationType int

const (
	nilOperation operationType = 1 << iota
	unaryOperation
	binaryOperation
	ternaryOperation
	assignmentOperation
	subexpressionBeginOperation
	subexpressionEndOperation
)

func operationTypeForToken(t token.Token) operationType {
	switch t {
	case token.UnaryOperator, token.BitwiseNotOperator:
		return unaryOperation
	case token.AdditionOperator,
		token.SubtractionOperator,
		token.ConcatenationOperator,
		token.ComparisonOperator,
		token.MultOperator,
		token.AndOperator,
		token.OrOperator,
		token.AmpersandOperator,
		token.BitwiseXorOperator,
		token.BitwiseOrOperator,
		token.BitwiseShiftOperator,
		token.WrittenAndOperator,
		token.WrittenXorOperator,
		token.WrittenOrOperator,
		token.InstanceofOperator:
		return binaryOperation
	case token.TernaryOperator1:
		return ternaryOperation
	case token.AssignmentOperator:
		return assignmentOperation
	case token.OpenParen:
		return subexpressionBeginOperation
	case token.CloseParen:
		return subexpressionEndOperation
	}
	return nilOperation
}

func newUnaryOperation(operator Item, expr ast.Expression) ast.OperatorExpression {
	t := ast.Numeric
	if operator.val == "!" {
		t = ast.Boolean
	}
	return ast.OperatorExpression{
		Type:     t,
		Operand1: expr,
		Operator: operator.val,
	}
}

func newBinaryOperation(operator Item, expr1, expr2 ast.Expression) ast.OperatorExpression {
	t := ast.Numeric
	switch operator.typ {
	case token.ComparisonOperator, token.AndOperator, token.OrOperator, token.WrittenAndOperator, token.WrittenOrOperator, token.WrittenXorOperator:
		t = ast.Boolean
	case token.ConcatenationOperator:
		t = ast.String
	case token.AmpersandOperator, token.BitwiseXorOperator, token.BitwiseOrOperator, token.BitwiseShiftOperator:
		t = ast.AnyType
	}
	return ast.OperatorExpression{
		Type:     t,
		Operand1: expr1,
		Operand2: expr2,
		Operator: operator.val,
	}
}

func (p *parser) parseBinaryOperation(lhs ast.Expression, operator Item, originalParenLevel int) ast.Expression {
	p.next()
	rhs := p.parseOperand()
	for {
		nextOperator := p.peek()
		nextOperatorPrecedence, ok := operatorPrecedence[nextOperator.typ]
		if ok && nextOperatorPrecedence > operatorPrecedence[operator.typ] {
			rhs = p.parseOperation(originalParenLevel, rhs)
		} else {
			break
		}
	}
	return newBinaryOperation(operator, lhs, rhs)
}

func (p *parser) parseTernaryOperation(lhs ast.Expression) ast.Expression {
	truthy := p.parseNextExpression()
	p.expect(token.TernaryOperator2)
	falsy := p.parseNextExpression()
	return &ast.OperatorExpression{
		Operand1: lhs,
		Operand2: truthy,
		Operand3: falsy,
		Type:     truthy.EvaluatesTo() | falsy.EvaluatesTo(),
		Operator: "?:",
	}
}

func (p *parser) parseUnaryExpressionRight(operand ast.Expression, operator Item) ast.Expression {
	return newUnaryOperation(operator, operand)
}

func (p *parser) parseUnaryExpressionLeft(operand ast.Expression, operator Item) ast.Expression {
	return newUnaryOperation(operator, operand)
}
<commit_msg>adding support for the middle ternary operand being left out<commit_after>package php

import (
	"stephensearles.com/php/ast"
	"stephensearles.com/php/token"
)

type operationType int

const (
	nilOperation operationType = 1 << iota
	unaryOperation
	binaryOperation
	ternaryOperation
	assignmentOperation
	subexpressionBeginOperation
	subexpressionEndOperation
)

func operationTypeForToken(t token.Token) operationType {
	switch t {
	case token.UnaryOperator, token.BitwiseNotOperator:
		return unaryOperation
	case token.AdditionOperator,
		token.SubtractionOperator,
		token.ConcatenationOperator,
		token.ComparisonOperator,
		token.MultOperator,
		token.AndOperator,
		token.OrOperator,
		token.AmpersandOperator,
		token.BitwiseXorOperator,
		token.BitwiseOrOperator,
		token.BitwiseShiftOperator,
		token.WrittenAndOperator,
		token.WrittenXorOperator,
		token.WrittenOrOperator,
		token.InstanceofOperator:
		return binaryOperation
	case token.TernaryOperator1:
		return ternaryOperation
	case token.AssignmentOperator:
		return assignmentOperation
	case token.OpenParen:
		return subexpressionBeginOperation
	case token.CloseParen:
		return subexpressionEndOperation
	}
	return nilOperation
}

func newUnaryOperation(operator Item, expr ast.Expression) ast.OperatorExpression {
	t := ast.Numeric
	if operator.val == "!" {
		t = ast.Boolean
	}
	return ast.OperatorExpression{
		Type:     t,
		Operand1: expr,
		Operator: operator.val,
	}
}

func newBinaryOperation(operator Item, expr1, expr2 ast.Expression) ast.OperatorExpression {
	t := ast.Numeric
	switch operator.typ {
	case token.ComparisonOperator, token.AndOperator, token.OrOperator, token.WrittenAndOperator, token.WrittenOrOperator, token.WrittenXorOperator:
		t = ast.Boolean
	case token.ConcatenationOperator:
		t = ast.String
	case token.AmpersandOperator, token.BitwiseXorOperator, token.BitwiseOrOperator, token.BitwiseShiftOperator:
		t = ast.AnyType
	}
	return ast.OperatorExpression{
		Type:     t,
		Operand1: expr1,
		Operand2: expr2,
		Operator: operator.val,
	}
}

func (p *parser) parseBinaryOperation(lhs ast.Expression, operator Item, originalParenLevel int) ast.Expression {
	p.next()
	rhs := p.parseOperand()
	for {
		nextOperator := p.peek()
		nextOperatorPrecedence, ok := operatorPrecedence[nextOperator.typ]
		if ok && nextOperatorPrecedence > operatorPrecedence[operator.typ] {
			rhs = p.parseOperation(originalParenLevel, rhs)
		} else {
			break
		}
	}
	return newBinaryOperation(operator, lhs, rhs)
}

func (p *parser) parseTernaryOperation(lhs ast.Expression) ast.Expression {
	var truthy ast.Expression
	if p.peek().typ == token.TernaryOperator2 {
		truthy = lhs
	} else {
		truthy = p.parseNextExpression()
	}
	p.expect(token.TernaryOperator2)
	falsy := p.parseNextExpression()
	return &ast.OperatorExpression{
		Operand1: lhs,
		Operand2: truthy,
		Operand3: falsy,
		Type:     truthy.EvaluatesTo() | falsy.EvaluatesTo(),
		Operator: "?:",
	}
}

func (p *parser) parseUnaryExpressionRight(operand ast.Expression, operator Item) ast.Expression {
	return newUnaryOperation(operator, operand)
}

func (p *parser) parseUnaryExpressionLeft(operand ast.Expression, operator Item) ast.Expression {
	return newUnaryOperation(operator, operand)
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"goseapi"
	"os"
)

func main() {
	questionid := os.Args[1]
	var answers []goseapi.Answer
	_, err := goseapi.Do("/questions/"+questionid+"/answers", &answers, &goseapi.Params{
		Site:  goseapi.StackOverflow,
		Sort:  goseapi.SortScore,
		Order: "desc",
	})
	if err != nil {
		fmt.Printf("Unable to fetch answer for %s: '%v'", questionid, err)
		os.Exit(1)
	}
	for _, answer := range answers {
		fmt.Println(answer.ID)
		var comments []goseapi.Comment
		_, err := goseapi.Do(fmt.Sprintf("/answers/%d/comments", answer.ID), &comments, &goseapi.Params{
			Site:   goseapi.StackOverflow,
			Sort:   goseapi.SortCreationDate,
			Order:  "asc",
			Filter: "!40nvjILjlVtsS1Mrk",
		})
		if err != nil {
			fmt.Printf("Unable to fetch comments for answer %s: '%v'", answer.ID, err)
			os.Exit(1)
		}
		for _, comment := range comments {
			fmt.Printf("As mentioned by [%s](%s) in [the comments](%s)  \n", comment.Owner.DisplayName, comment.Owner.Link, comment.Link)
			fmt.Printf("> %s\n\n", comment.Body)
		}
	}
}
<commit_msg>Put comment.Body into markdown<commit_after>package main

import (
	"fmt"
	"goseapi"
	"os"
	"regexp"
	"strings"
)

var re = regexp.MustCompile(`<a href="([^"]+)".*?>(.*?)</a>`)

func main() {
	questionid := os.Args[1]
	var answers []goseapi.Answer
	_, err := goseapi.Do("/questions/"+questionid+"/answers", &answers, &goseapi.Params{
		Site:  goseapi.StackOverflow,
		Sort:  goseapi.SortScore,
		Order: "desc",
	})
	if err != nil {
		fmt.Printf("Unable to fetch answer for %s: '%v'", questionid, err)
		os.Exit(1)
	}
	for _, answer := range answers {
		fmt.Println(answer.ID)
		var comments []goseapi.Comment
		_, err := goseapi.Do(fmt.Sprintf("/answers/%d/comments", answer.ID), &comments, &goseapi.Params{
			Site:   goseapi.StackOverflow,
			Sort:   goseapi.SortCreationDate,
			Order:  "asc",
			Filter: "!40nvjILjlVtsS1Mrk",
		})
		if err != nil {
			fmt.Printf("Unable to fetch comments for answer %s: '%v'", answer.ID, err)
			os.Exit(1)
		}
		for _, comment := range comments {
			fmt.Printf("As mentioned by [%s](%s) in [the comments](%s)  \n", comment.Owner.DisplayName, comment.Owner.Link, comment.Link)
			body := comment.Body
			body = strings.Replace(body, "", "*", -1)
			body = strings.Replace(body, "", "*", -1)
			body = strings.Replace(body, "", "`", -1)
			body = strings.Replace(body, "", "`", -1)
			body = strings.Replace(body, """, "\"", -1)
			body = strings.Replace(body, "/", "/", -1)
			body = strings.Replace(body, "'", "'", -1)
			body = re.ReplaceAllString(body, "[$2]($1)")
			fmt.Printf("> %s\n\n", body)
		}
	}
}
<|endoftext|>


<commit_before>// Copyright 2015 The draw2d Authors. All rights reserved.
// created: 26/06/2015 by Stani Michiels

package pdf2d

import (
	"fmt"
	"image"
	"image/color"
	"log"
	"os"

	"github.com/stanim/draw2d"
	"github.com/stanim/gofpdf"
)

const c255 = 254.0 / 65535.0

var (
	caps = map[draw2d.Cap]string{
		draw2d.RoundCap:  "round",
		draw2d.ButtCap:   "butt",
		draw2d.SquareCap: "square"}
)

func notImplemented(method string) {
	fmt.Printf("%s: not implemented\n", method)
}

func rgb(c color.Color) (int, int, int) {
	r, g, b, _ := c.RGBA()
	return int(float64(r) * c255), int(float64(g) * c255), int(float64(b) * c255)
}

// GraphicContext implements the draw2d.GraphicContext interface
// It provides draw2d with a pdf backend (based on gofpdf)
type GraphicContext struct {
	*draw2d.StackGraphicContext
	pdf *gofpdf.Fpdf
	DPI int
}

// NewGraphicContext creates a new pdf GraphicContext
func NewGraphicContext(pdf *gofpdf.Fpdf) *GraphicContext {
	dpi := 92
	return &GraphicContext{draw2d.NewStackGraphicContext(), pdf, dpi}
}

func (gc *GraphicContext) DrawImage(image image.Image) {
	notImplemented("DrawImage")
}
func (gc *GraphicContext) Clear() {
	notImplemented("Clear")
}

func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) {
	notImplemented("ClearRect")
}

func (gc *GraphicContext) SetDPI(dpi int) {
	gc.DPI = dpi
}

func (gc *GraphicContext) GetDPI() int {
	return gc.DPI
}

func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom float64) {
	notImplemented("GetStringBounds")
	return 0, 0, 0, 0
}

func (gc *GraphicContext) CreateStringPath(text string, x, y float64) (cursor float64) {
	notImplemented("CreateStringPath")
	return 0
}

func (gc *GraphicContext) FillString(text string) (cursor float64) {
	notImplemented("FillString")
	return 0
}

func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) {
	notImplemented("FillStringAt")
	return 0
}

func (gc *GraphicContext) StrokeString(text string) (cursor float64) {
	notImplemented("StrokeString")
	return 0
}

func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) {
	notImplemented("StrokeStringAt")
	return 0
}

func (gc *GraphicContext) Stroke(paths ...*draw2d.PathStorage) {
	gc.draw("D", paths...)
}

func (gc *GraphicContext) Fill(paths ...*draw2d.PathStorage) {
	gc.draw("F", paths...)
}

func (gc *GraphicContext) FillStroke(paths ...*draw2d.PathStorage) {
	gc.draw("FD", paths...)
}

var logger *log.Logger = log.New(os.Stdout, "", log.Lshortfile)

func (gc *GraphicContext) draw(style string, paths ...*draw2d.PathStorage) {
	paths = append(paths, gc.Current.Path)
	pathConverter := NewPathConverter(
		NewVertexMatrixTransform(gc.Current.Tr,
			NewPathLogger(logger, gc.pdf)))
	pathConverter.Convert(paths...)
	if gc.Current.FillRule.UseNonZeroWinding() {
		style += "*"
	}
	gc.pdf.DrawPath(style)
}

// overwrite StackGraphicContext methods

func (gc *GraphicContext) SetStrokeColor(c color.Color) {
	gc.StackGraphicContext.SetStrokeColor(c)
	gc.pdf.SetDrawColor(rgb(c))
}

func (gc *GraphicContext) SetFillColor(c color.Color) {
	gc.StackGraphicContext.SetFillColor(c)
	gc.pdf.SetFillColor(rgb(c))
}

func (gc *GraphicContext) SetLineWidth(LineWidth float64) {
	gc.StackGraphicContext.SetLineWidth(LineWidth)
	gc.pdf.SetLineWidth(LineWidth)
}

func (gc *GraphicContext) SetLineCap(Cap draw2d.Cap) {
	gc.StackGraphicContext.SetLineCap(Cap)
	gc.pdf.SetLineCapStyle(caps[Cap])
}
<commit_msg>fix c255 for rgb conversion<commit_after>// Copyright 2015 The draw2d Authors. All rights reserved.
// created: 26/06/2015 by Stani Michiels

package pdf2d

import (
	"fmt"
	"image"
	"image/color"
	"log"
	"os"

	"github.com/stanim/draw2d"
	"github.com/stanim/gofpdf"
)

var (
	caps = map[draw2d.Cap]string{
		draw2d.RoundCap:  "round",
		draw2d.ButtCap:   "butt",
		draw2d.SquareCap: "square"}
)

func notImplemented(method string) {
	fmt.Printf("%s: not implemented\n", method)
}

const c255 = 255.0 / 65535.0

func rgb(c color.Color) (int, int, int) {
	r, g, b, _ := c.RGBA()
	return int(float64(r) * c255), int(float64(g) * c255), int(float64(b) * c255)
}

// GraphicContext implements the draw2d.GraphicContext interface
// It provides draw2d with a pdf backend (based on gofpdf)
type GraphicContext struct {
	*draw2d.StackGraphicContext
	pdf *gofpdf.Fpdf
	DPI int
}

// NewGraphicContext creates a new pdf GraphicContext
func NewGraphicContext(pdf *gofpdf.Fpdf) *GraphicContext {
	dpi := 92
	return &GraphicContext{draw2d.NewStackGraphicContext(), pdf, dpi}
}

func (gc *GraphicContext) DrawImage(image image.Image) {
	notImplemented("DrawImage")
}
func (gc *GraphicContext) Clear() {
	notImplemented("Clear")
}

func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) {
	notImplemented("ClearRect")
}

func (gc *GraphicContext) SetDPI(dpi int) {
	gc.DPI = dpi
}

func (gc *GraphicContext) GetDPI() int {
	return gc.DPI
}

func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom float64) {
	notImplemented("GetStringBounds")
	return 0, 0, 0, 0
}

func (gc *GraphicContext) CreateStringPath(text string, x, y float64) (cursor float64) {
	notImplemented("CreateStringPath")
	return 0
}

func (gc *GraphicContext) FillString(text string) (cursor float64) {
	notImplemented("FillString")
	return 0
}

func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) {
	notImplemented("FillStringAt")
	return 0
}

func (gc *GraphicContext) StrokeString(text string) (cursor float64) {
	notImplemented("StrokeString")
	return 0
}

func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) {
	notImplemented("StrokeStringAt")
	return 0
}

func (gc *GraphicContext) Stroke(paths ...*draw2d.PathStorage) {
	gc.draw("D", paths...)
}

func (gc *GraphicContext) Fill(paths ...*draw2d.PathStorage) {
	gc.draw("F", paths...)
}

func (gc *GraphicContext) FillStroke(paths ...*draw2d.PathStorage) {
	gc.draw("FD", paths...)
}

var logger *log.Logger = log.New(os.Stdout, "", log.Lshortfile)

func (gc *GraphicContext) draw(style string, paths ...*draw2d.PathStorage) {
	paths = append(paths, gc.Current.Path)
	pathConverter := NewPathConverter(
		NewVertexMatrixTransform(gc.Current.Tr,
			NewPathLogger(logger, gc.pdf)))
	pathConverter.Convert(paths...)
	if gc.Current.FillRule.UseNonZeroWinding() {
		style += "*"
	}
	gc.pdf.DrawPath(style)
}

// overwrite StackGraphicContext methods

func (gc *GraphicContext) SetStrokeColor(c color.Color) {
	gc.StackGraphicContext.SetStrokeColor(c)
	gc.pdf.SetDrawColor(rgb(c))
}

func (gc *GraphicContext) SetFillColor(c color.Color) {
	gc.StackGraphicContext.SetFillColor(c)
	gc.pdf.SetFillColor(rgb(c))
}

func (gc *GraphicContext) SetLineWidth(LineWidth float64) {
	gc.StackGraphicContext.SetLineWidth(LineWidth)
	gc.pdf.SetLineWidth(LineWidth)
}

func (gc *GraphicContext) SetLineCap(Cap draw2d.Cap) {
	gc.StackGraphicContext.SetLineCap(Cap)
	gc.pdf.SetLineCapStyle(caps[Cap])
}
<|endoftext|>


<commit_before>/*
Copyright 2017 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 updateconfig

import (
	"fmt"
	"path"

	"github.com/mattn/go-zglob"
	"github.com/sirupsen/logrus"

	"k8s.io/test-infra/prow/github"
	"k8s.io/test-infra/prow/kube"
	"k8s.io/test-infra/prow/pluginhelp"
	"k8s.io/test-infra/prow/plugins"
)

const (
	pluginName = "config-updater"
)

func init() {
	plugins.RegisterPullRequestHandler(pluginName, handlePullRequest, helpProvider)
}

func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
	var configInfo map[string]string
	if len(enabledRepos) == 1 {
		msg := fmt.Sprintf(
			"The main configuration is kept in sync with '%s/%s'.\nThe plugin configuration is kept in sync with '%s/%s'.",
			enabledRepos[0],
			config.ConfigUpdater.ConfigFile,
			enabledRepos[0],
			config.ConfigUpdater.PluginFile,
		)
		configInfo = map[string]string{"": msg}
	}
	return &pluginhelp.PluginHelp{
			Description: "The config-updater plugin automatically redeploys configuration and plugin configuration files when they change. The plugin watches for pull request merges that modify either of the config files and updates the cluster's configmap resources in response.",
			Config:      configInfo,
		},
		nil
}

type githubClient interface {
	CreateComment(owner, repo string, number int, comment string) error
	GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error)
	GetFile(org, repo, filepath, commit string) ([]byte, error)
}

type kubeClient interface {
	GetConfigMap(name, namespace string) (kube.ConfigMap, error)
	ReplaceConfigMap(name string, config kube.ConfigMap) (kube.ConfigMap, error)
}

func handlePullRequest(pc plugins.PluginClient, pre github.PullRequestEvent) error {
	return handle(pc.GitHubClient, pc.KubeClient, pc.Logger, pre, maps(pc))
}

func maps(pc plugins.PluginClient) map[string]plugins.ConfigMapSpec {
	return pc.PluginConfig.ConfigUpdater.Maps
}

func update(gc githubClient, kc kubeClient, org, repo, commit, name, namespace string, updates map[string]string) error {
	currentContent, err := kc.GetConfigMap(name, namespace)
	if _, isNotFound := err.(kube.NotFoundError); err != nil && !isNotFound {
		return fmt.Errorf("failed to fetch current state of configmap: %v", err)
	}

	data := map[string]string{}
	if currentContent.Data != nil {
		data = currentContent.Data
	}

	for key, filename := range updates {
		if filename == "" {
			delete(data, key)
			continue
		}

		content, err := gc.GetFile(org, repo, filename, commit)
		if err != nil {
			return fmt.Errorf("get file err: %v", err)
		}
		data[key] = string(content)
	}

	cm := kube.ConfigMap{
		ObjectMeta: kube.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Data: data,
	}

	_, err = kc.ReplaceConfigMap(name, cm)
	if err != nil {
		return fmt.Errorf("replace config map err: %v", err)
	}
	return nil
}

func handle(gc githubClient, kc kubeClient, log *logrus.Entry, pre github.PullRequestEvent, configMaps map[string]plugins.ConfigMapSpec) error {
	// Only consider newly merged PRs
	if pre.Action != github.PullRequestActionClosed {
		return nil
	}

	if len(configMaps) == 0 { // Nothing to update
		return nil
	}

	pr := pre.PullRequest

	if !pr.Merged || pr.MergeSHA == nil || pr.Base.Repo.DefaultBranch != pr.Base.Ref {
		return nil
	}

	org := pr.Base.Repo.Owner.Login
	repo := pr.Base.Repo.Name

	// Which files changed in this PR?
	changes, err := gc.GetPullRequestChanges(org, repo, pr.Number)
	if err != nil {
		return err
	}

	message := func(name, namespace string, data map[string]string) string {
		identifier := fmt.Sprintf("`%s` configmap", name)
		if namespace != "" {
			identifier = fmt.Sprintf("%s in namespace `%s`", identifier, namespace)
		}
		msg := fmt.Sprintf("%s using the following files:", identifier)
		for key, file := range data {
			msg = fmt.Sprintf("%s\n    - key `%s` using file `%s`", msg, key, file)
		}
		return msg
	}

	// Are any of the changes files ones that define a configmap we want to update?
	var updated []string
	type configMapID struct {
		name, namespace string
	}
	toUpdate := map[configMapID]map[string]string{}
	for _, change := range changes {
		var cm plugins.ConfigMapSpec
		found := false

		for key, configMap := range configMaps {
			found, err = zglob.Match(key, change.Filename)
			if err != nil {
				// Should not happen, log err and continue
				log.WithError(err).Info("key matching error")
				continue
			}

			if found {
				cm = configMap
				break
			}
		}

		if !found {
			continue // This file does not define a configmap
		}

		// Yes, update the configmap with the contents of this file
		key := cm.Key
		if key == "" {
			key = path.Base(change.Filename)
		}
		id := configMapID{name: cm.Name, namespace: cm.Namespace}
		if _, ok := toUpdate[id]; !ok {
			toUpdate[id] = map[string]string{}
		}
		if change.Status == "removed" {
			toUpdate[id][key] = ""
		} else {
			toUpdate[id][key] = change.Filename
		}
	}

	for cm, data := range toUpdate {
		if err := update(gc, kc, org, repo, *pr.MergeSHA, cm.name, cm.namespace, data); err != nil {
			return err
		}
		updated = append(updated, message(cm.name, cm.namespace, data))
	}

	var msg string
	switch n := len(updated); n {
	case 0:
		return nil
	case 1:
		msg = fmt.Sprintf("Updated the %s", updated[0])
	default:
		msg = fmt.Sprintf("Updated the following %d configmaps:\n", n)
		for _, updateMsg := range updated {
			msg += fmt.Sprintf("  * %s\n", updateMsg)
		}
	}

	if err := gc.CreateComment(org, repo, pr.Number, plugins.FormatResponseRaw(pr.Body, pr.HTMLURL, pr.User.Login, msg)); err != nil {
		return fmt.Errorf("comment err: %v", err)
	}
	return nil
}
<commit_msg>fix indent again<commit_after>/*
Copyright 2017 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 updateconfig

import (
	"fmt"
	"path"

	"github.com/mattn/go-zglob"
	"github.com/sirupsen/logrus"

	"k8s.io/test-infra/prow/github"
	"k8s.io/test-infra/prow/kube"
	"k8s.io/test-infra/prow/pluginhelp"
	"k8s.io/test-infra/prow/plugins"
)

const (
	pluginName = "config-updater"
)

func init() {
	plugins.RegisterPullRequestHandler(pluginName, handlePullRequest, helpProvider)
}

func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
	var configInfo map[string]string
	if len(enabledRepos) == 1 {
		msg := fmt.Sprintf(
			"The main configuration is kept in sync with '%s/%s'.\nThe plugin configuration is kept in sync with '%s/%s'.",
			enabledRepos[0],
			config.ConfigUpdater.ConfigFile,
			enabledRepos[0],
			config.ConfigUpdater.PluginFile,
		)
		configInfo = map[string]string{"": msg}
	}
	return &pluginhelp.PluginHelp{
			Description: "The config-updater plugin automatically redeploys configuration and plugin configuration files when they change. The plugin watches for pull request merges that modify either of the config files and updates the cluster's configmap resources in response.",
			Config:      configInfo,
		},
		nil
}

type githubClient interface {
	CreateComment(owner, repo string, number int, comment string) error
	GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error)
	GetFile(org, repo, filepath, commit string) ([]byte, error)
}

type kubeClient interface {
	GetConfigMap(name, namespace string) (kube.ConfigMap, error)
	ReplaceConfigMap(name string, config kube.ConfigMap) (kube.ConfigMap, error)
}

func handlePullRequest(pc plugins.PluginClient, pre github.PullRequestEvent) error {
	return handle(pc.GitHubClient, pc.KubeClient, pc.Logger, pre, maps(pc))
}

func maps(pc plugins.PluginClient) map[string]plugins.ConfigMapSpec {
	return pc.PluginConfig.ConfigUpdater.Maps
}

func update(gc githubClient, kc kubeClient, org, repo, commit, name, namespace string, updates map[string]string) error {
	currentContent, err := kc.GetConfigMap(name, namespace)
	if _, isNotFound := err.(kube.NotFoundError); err != nil && !isNotFound {
		return fmt.Errorf("failed to fetch current state of configmap: %v", err)
	}

	data := map[string]string{}
	if currentContent.Data != nil {
		data = currentContent.Data
	}

	for key, filename := range updates {
		if filename == "" {
			delete(data, key)
			continue
		}

		content, err := gc.GetFile(org, repo, filename, commit)
		if err != nil {
			return fmt.Errorf("get file err: %v", err)
		}
		data[key] = string(content)
	}

	cm := kube.ConfigMap{
		ObjectMeta: kube.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Data: data,
	}

	_, err = kc.ReplaceConfigMap(name, cm)
	if err != nil {
		return fmt.Errorf("replace config map err: %v", err)
	}
	return nil
}

func handle(gc githubClient, kc kubeClient, log *logrus.Entry, pre github.PullRequestEvent, configMaps map[string]plugins.ConfigMapSpec) error {
	// Only consider newly merged PRs
	if pre.Action != github.PullRequestActionClosed {
		return nil
	}

	if len(configMaps) == 0 { // Nothing to update
		return nil
	}

	pr := pre.PullRequest

	if !pr.Merged || pr.MergeSHA == nil || pr.Base.Repo.DefaultBranch != pr.Base.Ref {
		return nil
	}

	org := pr.Base.Repo.Owner.Login
	repo := pr.Base.Repo.Name

	// Which files changed in this PR?
	changes, err := gc.GetPullRequestChanges(org, repo, pr.Number)
	if err != nil {
		return err
	}

	message := func(name, namespace string, data map[string]string, indent string) string {
		identifier := fmt.Sprintf("`%s` configmap", name)
		if namespace != "" {
			identifier = fmt.Sprintf("%s in namespace `%s`", identifier, namespace)
		}
		msg := fmt.Sprintf("%s using the following files:", identifier)
		for key, file := range data {
			msg = fmt.Sprintf("%s\n%s- key `%s` using file `%s`", msg, indent, key, file)
		}
		return msg
	}

	// Are any of the changes files ones that define a configmap we want to update?
	var updated []string
	type configMapID struct {
		name, namespace string
	}
	toUpdate := map[configMapID]map[string]string{}
	for _, change := range changes {
		var cm plugins.ConfigMapSpec
		found := false

		for key, configMap := range configMaps {
			found, err = zglob.Match(key, change.Filename)
			if err != nil {
				// Should not happen, log err and continue
				log.WithError(err).Info("key matching error")
				continue
			}

			if found {
				cm = configMap
				break
			}
		}

		if !found {
			continue // This file does not define a configmap
		}

		// Yes, update the configmap with the contents of this file
		key := cm.Key
		if key == "" {
			key = path.Base(change.Filename)
		}
		id := configMapID{name: cm.Name, namespace: cm.Namespace}
		if _, ok := toUpdate[id]; !ok {
			toUpdate[id] = map[string]string{}
		}
		if change.Status == "removed" {
			toUpdate[id][key] = ""
		} else {
			toUpdate[id][key] = change.Filename
		}
	}

	indent := " " // one space
	if len(toUpdate) > 1 {
		indent = "   " // three spaces for sub bullets
	}
	for cm, data := range toUpdate {
		if err := update(gc, kc, org, repo, *pr.MergeSHA, cm.name, cm.namespace, data); err != nil {
			return err
		}
		updated = append(updated, message(cm.name, cm.namespace, data, indent))
	}

	var msg string
	switch n := len(updated); n {
	case 0:
		return nil
	case 1:
		msg = fmt.Sprintf("Updated the %s", updated[0])
	default:
		msg = fmt.Sprintf("Updated the following %d configmaps:\n", n)
		for _, updateMsg := range updated {
			msg += fmt.Sprintf(" * %s\n", updateMsg) // one space indent
		}
	}

	if err := gc.CreateComment(org, repo, pr.Number, plugins.FormatResponseRaw(pr.Body, pr.HTMLURL, pr.User.Login, msg)); err != nil {
		return fmt.Errorf("comment err: %v", err)
	}
	return nil
}
<|endoftext|>


<commit_before>package simulation_test

import (
	"fmt"
	"sync"
	"time"

	"code.cloudfoundry.org/auction/simulation/util"
	"code.cloudfoundry.org/auction/simulation/visualization"
	"code.cloudfoundry.org/auctioneer"
	"code.cloudfoundry.org/bbs/models"
	"code.cloudfoundry.org/rep"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("Auction", func() {
	var initialDistributions map[int][]rep.LRP
	var linuxRootFSURL = models.PreloadedRootFS(linuxStack)

	newLRP := func(processGuid string, index int, memoryMB int) rep.LRP {
		lrpKey := models.NewActualLRPKey(processGuid, int32(index), "domain")
		return rep.NewLRP(lrpKey, rep.NewResource(int32(memoryMB), 1, 10), rep.NewPlacementConstraint(linuxRootFSURL, []string{}, []string{}))
	}

	generateUniqueLRPs := func(numInstances int, index int, memoryMB int) []rep.LRP {
		instances := []rep.LRP{}
		for i := 0; i < numInstances; i++ {
			instances = append(instances, newLRP(util.NewGrayscaleGuid("AAA"), index, memoryMB))
		}
		return instances
	}

	newLRPStartAuction := func(processGuid string, index int, memoryMB int32) auctioneer.LRPStartRequest {
		return auctioneer.NewLRPStartRequest(processGuid, "auction", []int{index}, rep.NewResource(memoryMB, 1, 10), rep.NewPlacementConstraint(linuxRootFSURL, []string{}, []string{}))
	}

	generateUniqueLRPStartAuctions := func(numInstances int, memoryMB int32) []auctioneer.LRPStartRequest {
		instances := []auctioneer.LRPStartRequest{}
		for i := 0; i < numInstances; i++ {
			instances = append(instances, newLRPStartAuction(util.NewGrayscaleGuid("BBB"), i, memoryMB))
		}
		return instances
	}

	generateLRPStartAuctionsWithRandomColor := func(numInstances int, memoryMB int32, colors []string) []auctioneer.LRPStartRequest {
		instances := []auctioneer.LRPStartRequest{}
		for i := 0; i < numInstances; i++ {
			color := colors[util.R.Intn(len(colors))]
			instances = append(instances, newLRPStartAuction(color, i, memoryMB))
		}
		return instances
	}

	generateLRPStartAuctionsForProcessGuid := func(numInstances int, processGuid string, memoryMB int32) []auctioneer.LRPStartRequest {
		instances := []auctioneer.LRPStartRequest{}
		for i := 0; i < numInstances; i++ {
			instances = append(instances, newLRPStartAuction(processGuid, i, memoryMB))
		}
		return instances
	}

	workForInstances := func(lrps []rep.LRP) rep.Work {
		return rep.Work{LRPs: lrps}
	}

	runStartAuction := func(lrpStartAuctions []auctioneer.LRPStartRequest, numCells int) {
		runnerDelegate.SetCellLimit(numCells)
		runner.ScheduleLRPsForAuctions(lrpStartAuctions)

		Eventually(runnerDelegate.ResultSize, time.Minute, 100*time.Millisecond).Should(Equal(len(lrpStartAuctions)))
	}

	runAndReportStartAuction := func(lrpStartAuctions []auctioneer.LRPStartRequest, numCells int, i int, j int) *visualization.Report {
		t := time.Now()
		runStartAuction(lrpStartAuctions, numCells)

		Eventually(runnerDelegate.ResultSize, time.Minute, 100*time.Millisecond).Should(Equal(len(lrpStartAuctions)))
		duration := time.Since(t)

		cells, _ := runnerDelegate.FetchCellReps()
		report := visualization.NewReport(len(lrpStartAuctions), cells, runnerDelegate.Results(), duration)

		visualization.PrintReport(report)
		svgReport.DrawReportCard(i, j, report)
		reports = append(reports, report)
		fmt.Println("Done...")
		return report
	}

	BeforeEach(func() {
		util.ResetGuids()
		initialDistributions = map[int][]rep.LRP{}
	})

	JustBeforeEach(func() {
		wg := &sync.WaitGroup{}
		wg.Add(len(initialDistributions))
		for index, instances := range initialDistributions {
			guid := cellGuid(index)
			instances := instances
			workPool.Submit(func() {
				cells[guid].Perform(logger, workForInstances(instances))
				wg.Done()
			})
		}
		wg.Wait()
	})

	Describe("Experiments", func() {
		Context("Small Cold LRPStarts", func() {
			napps := []int{8, 40, 200, 800}
			ncells := []int{4, 10, 20, 40}
			for i := range ncells {
				i := i
				It("should distribute evenly", func() {
					instances := generateUniqueLRPStartAuctions(napps[i], 1)

					runAndReportStartAuction(instances, ncells[i], i, 0)
				})
			}
		})

		Context("Large Cold LRPStarts", func() {
			ncells := []int{25, 4 * 25}
			n1apps := []int{1800, 4 * 1800}
			n2apps := []int{200, 4 * 200}
			n4apps := []int{50, 4 * 50}
			for i := range ncells {
				i := i
				Context("with single-instance and multi-instance apps", func() {
					It("should distribute evenly", func() {
						instances := []auctioneer.LRPStartRequest{}
						colors := []string{"purple", "red", "orange", "teal", "gray", "blue", "pink", "green", "lime", "cyan", "lightseagreen", "brown"}

						instances = append(instances, generateUniqueLRPStartAuctions(n1apps[i]/2, 1)...)
						instances = append(instances, generateLRPStartAuctionsWithRandomColor(n1apps[i]/2, 1, colors[:4])...)
						instances = append(instances, generateUniqueLRPStartAuctions(n2apps[i]/2, 2)...)
						instances = append(instances, generateLRPStartAuctionsWithRandomColor(n2apps[i]/2, 2, colors[4:8])...)
						instances = append(instances, generateUniqueLRPStartAuctions(n4apps[i]/2, 4)...)
						instances = append(instances, generateLRPStartAuctionsWithRandomColor(n4apps[i]/2, 4, colors[8:12])...)

						permutedInstances := make([]auctioneer.LRPStartRequest, len(instances))
						for i, index := range util.R.Perm(len(instances)) {
							permutedInstances[i] = instances[index]
						}

						runAndReportStartAuction(permutedInstances, ncells[i], i, 1)
					})
				})
			}
		})

		Context("Imbalanced scenario (e.g. a deploy)", func() {
			ncells := []int{100, 100}
			nempty := []int{5, 1}
			napps := []int{500, 100}

			for i := range ncells {
				i := i
				Context("scenario", func() {
					BeforeEach(func() {
						for j := 0; j < ncells[i]-nempty[i]; j++ {
							initialDistributions[j] = generateUniqueLRPs(50, 0, 1)
						}
					})

					It("should distribute evenly without overloading empty cells", func() {
						instances := generateUniqueLRPStartAuctions(napps[i], 1)

						runAndReportStartAuction(instances, ncells[i], i+2, 1)
					})
				})
			}
		})

		Context("AZ distribution", func() {
			ncells := 3
			napps := 40
			initialAppsOnZone0 := 50

			BeforeEach(func() {
				initialDistributions[1] = generateUniqueLRPs(initialAppsOnZone0, 0, 1)
			})

			It("should distribute across the zones", func() {
				instances := generateLRPStartAuctionsForProcessGuid(napps, "red", 1)

				report := runAndReportStartAuction(instances, ncells, 0, 2)

				By("populating the lone cell in Z1 even though it is heavily-loaded ")
				numOnZone0 := 0
				numOnZone0 += len(report.InstancesByRep[cellGuid(0)])
				numOnZone0 += len(report.InstancesByRep[cellGuid(2)])

				numOnZone1 := len(report.InstancesByRep[cellGuid(1)]) - initialAppsOnZone0

				Expect(numOnZone0).To(Equal(numOnZone1))
			})
		})

		Context("The Watters demo", func() {
			ncells := []int{10, 30, 100}
			napps := []int{80, 200, 400}

			for i := range ncells {
				i := i

				Context("scenario", func() {
					BeforeEach(func() {
						for j := 0; j < ncells[i]; j++ {
							initialDistributions[j] = generateUniqueLRPs(util.RandomIntIn(78, 80), 0, 1)
						}
					})

					It("should distribute evenly", func() {
						instances := generateLRPStartAuctionsForProcessGuid(napps[i], "red", 1)

						runAndReportStartAuction(instances, ncells[i], i+1, 2)
					})
				})
			}
		})

		Context("Packing optimally when memory is low", func() {
			nCells := 1

			It("should place boulders in before pebbles, but prevent boulders from saturating available capacity", func() {
				instances := []auctioneer.LRPStartRequest{}
				for i := 0; i < 80; i++ {
					instances = append(instances, generateUniqueLRPStartAuctions(1, 1)...)
				}
				instances = append(instances, generateLRPStartAuctionsForProcessGuid(2, "red", 50)...)

				runStartAuction(instances, nCells)
				results := runnerDelegate.Results()

				winners := []string{}
				losers := []string{}

				for _, result := range results.SuccessfulLRPs {
					winners = append(winners, fmt.Sprintf("%s-%d", result.ProcessGuid, result.Index))
				}
				for _, result := range results.FailedLRPs {
					losers = append(losers, fmt.Sprintf("%s-%d", result.ProcessGuid, result.Index))
				}

				Expect(winners).To(HaveLen(51))
				Expect(losers).To(HaveLen(31))

				Expect(winners).To(ContainElement("red-0"))
				Expect(losers).To(ContainElement("red-1"))
			})
		})
	})
})
<commit_msg>Add expectations on distributions of LRPs<commit_after>package simulation_test

import (
	"fmt"
	"math"
	"sync"
	"time"

	"code.cloudfoundry.org/auction/simulation/util"
	"code.cloudfoundry.org/auction/simulation/visualization"
	"code.cloudfoundry.org/auctioneer"
	"code.cloudfoundry.org/bbs/models"
	"code.cloudfoundry.org/rep"
	"github.com/GaryBoone/GoStats/stats"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("Auction", func() {
	var initialDistributions map[int][]rep.LRP
	var linuxRootFSURL = models.PreloadedRootFS(linuxStack)

	newLRP := func(processGuid string, index int, memoryMB int) rep.LRP {
		lrpKey := models.NewActualLRPKey(processGuid, int32(index), "domain")
		return rep.NewLRP(lrpKey, rep.NewResource(int32(memoryMB), 1, 10), rep.NewPlacementConstraint(linuxRootFSURL, []string{}, []string{}))
	}

	generateUniqueLRPs := func(numInstances int, index int, memoryMB int) []rep.LRP {
		instances := []rep.LRP{}
		for i := 0; i < numInstances; i++ {
			instances = append(instances, newLRP(util.NewGrayscaleGuid("AAA"), index, memoryMB))
		}
		return instances
	}

	newLRPStartAuction := func(processGuid string, index int, memoryMB int32) auctioneer.LRPStartRequest {
		return auctioneer.NewLRPStartRequest(processGuid, "auction", []int{index}, rep.NewResource(memoryMB, 1, 10), rep.NewPlacementConstraint(linuxRootFSURL, []string{}, []string{}))
	}

	generateUniqueLRPStartAuctions := func(numInstances int, memoryMB int32) []auctioneer.LRPStartRequest {
		instances := []auctioneer.LRPStartRequest{}
		for i := 0; i < numInstances; i++ {
			instances = append(instances, newLRPStartAuction(util.NewGrayscaleGuid("BBB"), i, memoryMB))
		}
		return instances
	}

	generateLRPStartAuctionsWithRandomColor := func(numInstances int, memoryMB int32, colors []string) []auctioneer.LRPStartRequest {
		instances := []auctioneer.LRPStartRequest{}
		for i := 0; i < numInstances; i++ {
			color := colors[util.R.Intn(len(colors))]
			instances = append(instances, newLRPStartAuction(color, i, memoryMB))
		}
		return instances
	}

	generateLRPStartAuctionsForProcessGuid := func(numInstances int, processGuid string, memoryMB int32) []auctioneer.LRPStartRequest {
		instances := []auctioneer.LRPStartRequest{}
		for i := 0; i < numInstances; i++ {
			instances = append(instances, newLRPStartAuction(processGuid, i, memoryMB))
		}
		return instances
	}

	workForInstances := func(lrps []rep.LRP) rep.Work {
		return rep.Work{LRPs: lrps}
	}

	runStartAuction := func(lrpStartAuctions []auctioneer.LRPStartRequest, numCells int) {
		runnerDelegate.SetCellLimit(numCells)
		runner.ScheduleLRPsForAuctions(lrpStartAuctions)

		Eventually(runnerDelegate.ResultSize, time.Minute, 100*time.Millisecond).Should(Equal(len(lrpStartAuctions)))
	}

	runAndReportStartAuction := func(lrpStartAuctions []auctioneer.LRPStartRequest, numCells int, i int, j int) *visualization.Report {
		t := time.Now()
		runStartAuction(lrpStartAuctions, numCells)

		Eventually(runnerDelegate.ResultSize, time.Minute, 100*time.Millisecond).Should(Equal(len(lrpStartAuctions)))
		duration := time.Since(t)

		cells, _ := runnerDelegate.FetchCellReps()
		report := visualization.NewReport(len(lrpStartAuctions), cells, runnerDelegate.Results(), duration)

		visualization.PrintReport(report)
		svgReport.DrawReportCard(i, j, report)
		reports = append(reports, report)
		fmt.Println("Done...")
		return report
	}

	getResultVector := func() []float64 {
		finalDistributions := make(map[string]float64)
		for _, lrpAuction := range runnerDelegate.Results().SuccessfulLRPs {
			finalDistributions[lrpAuction.Winner] += 1.0
		}

		distroVector := make([]float64, 0)
		for _, v := range finalDistributions {
			distroVector = append(distroVector, v)
		}

		return distroVector
	}

	// tolerance is tolerated the number of LRP deviance from the mean
	assertDistributionTolerances := func(tolerance int) {
		distroVector := getResultVector()
		mean := stats.StatsMean(distroVector)

		for j := range distroVector {
			deviance := math.Abs(distroVector[j] - mean)
			Expect(deviance).To(BeNumerically("<=", tolerance))
		}
	}

	BeforeEach(func() {
		util.ResetGuids()
		initialDistributions = map[int][]rep.LRP{}
	})

	JustBeforeEach(func() {
		wg := &sync.WaitGroup{}
		wg.Add(len(initialDistributions))
		for index, instances := range initialDistributions {
			guid := cellGuid(index)
			instances := instances
			workPool.Submit(func() {
				cells[guid].Perform(logger, workForInstances(instances))
				wg.Done()
			})
		}
		wg.Wait()
	})

	Describe("Experiments", func() {
		Context("Small Cold LRPStarts", func() {
			napps := []int{8, 40, 200, 800}
			ncells := []int{4, 10, 20, 40}
			for i := range ncells {
				i := i
				It("should distribute evenly", func() {
					instances := generateUniqueLRPStartAuctions(napps[i], 1)

					runAndReportStartAuction(instances, ncells[i], i, 0)

					assertDistributionTolerances(1)
				})
			}
		})

		Context("Large Cold LRPStarts", func() {
			ncells := []int{25, 4 * 25}
			n1apps := []int{1800, 4 * 1800}
			n2apps := []int{200, 4 * 200}
			n4apps := []int{50, 4 * 50}
			for i := range ncells {
				i := i
				Context("with single-instance and multi-instance apps", func() {
					It("should distribute evenly", func() {
						instances := []auctioneer.LRPStartRequest{}
						colors := []string{"purple", "red", "orange", "teal", "gray", "blue", "pink", "green", "lime", "cyan", "lightseagreen", "brown"}

						instances = append(instances, generateUniqueLRPStartAuctions(n1apps[i]/2, 1)...)
						instances = append(instances, generateLRPStartAuctionsWithRandomColor(n1apps[i]/2, 1, colors[:4])...)
						instances = append(instances, generateUniqueLRPStartAuctions(n2apps[i]/2, 2)...)
						instances = append(instances, generateLRPStartAuctionsWithRandomColor(n2apps[i]/2, 2, colors[4:8])...)
						instances = append(instances, generateUniqueLRPStartAuctions(n4apps[i]/2, 4)...)
						instances = append(instances, generateLRPStartAuctionsWithRandomColor(n4apps[i]/2, 4, colors[8:12])...)

						permutedInstances := make([]auctioneer.LRPStartRequest, len(instances))
						for i, index := range util.R.Perm(len(instances)) {
							permutedInstances[i] = instances[index]
						}

						runStartAuction(permutedInstances, ncells[i])

						assertDistributionTolerances(10)
					})
				})
			}
		})

		Context("Imbalanced scenario (e.g. a deploy)", func() {
			ncells := []int{100, 100}
			nempty := []int{5, 1}
			napps := []int{500, 100}

			for i := range ncells {
				i := i
				Context("scenario", func() {
					BeforeEach(func() {
						for j := 0; j < ncells[i]-nempty[i]; j++ {
							initialDistributions[j] = generateUniqueLRPs(50, 0, 1)
						}
					})

					It("should distribute evenly without overloading empty cells", func() {
						instances := generateUniqueLRPStartAuctions(napps[i], 1)

						runAndReportStartAuction(instances, ncells[i], i+2, 1)

						assertDistributionTolerances(1)
					})
				})
			}
		})

		Context("AZ distribution", func() {
			ncells := 3
			napps := 40
			initialAppsOnZone0 := 50

			BeforeEach(func() {
				initialDistributions[1] = generateUniqueLRPs(initialAppsOnZone0, 0, 1)
			})

			It("should distribute across the zones", func() {
				instances := generateLRPStartAuctionsForProcessGuid(napps, "red", 1)

				report := runAndReportStartAuction(instances, ncells, 0, 2)

				By("populating the lone cell in Z1 even though it is heavily-loaded ")
				numOnZone0 := 0
				numOnZone0 += len(report.InstancesByRep[cellGuid(0)])
				numOnZone0 += len(report.InstancesByRep[cellGuid(2)])

				numOnZone1 := len(report.InstancesByRep[cellGuid(1)]) - initialAppsOnZone0

				Expect(numOnZone0).To(Equal(numOnZone1))
			})
		})

		Context("The Watters demo", func() {
			ncells := []int{10, 30, 100}
			napps := []int{80, 200, 400}

			for i := range ncells {
				i := i

				Context("scenario", func() {
					BeforeEach(func() {
						for j := 0; j < ncells[i]; j++ {
							initialDistributions[j] = generateUniqueLRPs(util.RandomIntIn(78, 80), 0, 1)
						}
					})

					It("should distribute evenly", func() {
						instances := generateLRPStartAuctionsForProcessGuid(napps[i], "red", 1)

						runAndReportStartAuction(instances, ncells[i], i+1, 2)

						assertDistributionTolerances(1)
					})
				})
			}
		})

		Context("Packing optimally when memory is low", func() {
			nCells := 1

			It("should place boulders in before pebbles, but prevent boulders from saturating available capacity", func() {
				instances := []auctioneer.LRPStartRequest{}
				for i := 0; i < 80; i++ {
					instances = append(instances, generateUniqueLRPStartAuctions(1, 1)...)
				}
				instances = append(instances, generateLRPStartAuctionsForProcessGuid(2, "red", 50)...)

				runStartAuction(instances, nCells)
				results := runnerDelegate.Results()

				winners := []string{}
				losers := []string{}

				for _, result := range results.SuccessfulLRPs {
					winners = append(winners, fmt.Sprintf("%s-%d", result.ProcessGuid, result.Index))
				}
				for _, result := range results.FailedLRPs {
					losers = append(losers, fmt.Sprintf("%s-%d", result.ProcessGuid, result.Index))
				}

				Expect(winners).To(HaveLen(51))
				Expect(losers).To(HaveLen(31))

				Expect(winners).To(ContainElement("red-0"))
				Expect(losers).To(ContainElement("red-1"))
			})
		})
	})
})
<|endoftext|>


<commit_before>// Copyright 2017 The Prometheus 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 tsdb

import (
	"context"
	"io/ioutil"
	"math/rand"
	"os"
	"path/filepath"
	"testing"

	"github.com/go-kit/kit/log"
	"github.com/prometheus/tsdb/testutil"
	"github.com/prometheus/tsdb/tsdbutil"
)

// In Prometheus 2.1.0 we had a bug where the meta.json version was falsely bumped
// to 2. We had a migration in place resetting it to 1 but we should move immediately to
// version 3 next time to avoid confusion and issues.
func TestBlockMetaMustNeverBeVersion2(t *testing.T) {
	dir, err := ioutil.TempDir("", "metaversion")
	testutil.Ok(t, err)
	defer os.RemoveAll(dir)

	testutil.Ok(t, writeMetaFile(dir, &BlockMeta{}))

	meta, err := readMetaFile(dir)
	testutil.Ok(t, err)
	testutil.Assert(t, meta.Version != 2, "meta.json version must never be 2")
}

func TestSetCompactionFailed(t *testing.T) {
	tmpdir, err := ioutil.TempDir("", "test")
	testutil.Ok(t, err)
	defer os.RemoveAll(tmpdir)

	blockDir := createBlock(t, tmpdir, genSeries(1, 1, 0, 0))
	b, err := OpenBlock(nil, blockDir, nil)
	testutil.Ok(t, err)
	testutil.Equals(t, false, b.meta.Compaction.Failed)
	testutil.Ok(t, b.setCompactionFailed())
	testutil.Equals(t, true, b.meta.Compaction.Failed)
	testutil.Ok(t, b.Close())

	b, err = OpenBlock(nil, blockDir, nil)
	testutil.Ok(t, err)
	testutil.Equals(t, true, b.meta.Compaction.Failed)
	testutil.Ok(t, b.Close())
}

// createBlock creates a block with given set of series and returns its dir.
func createBlock(tb testing.TB, dir string, series []Series) string {
	head, err := NewHead(nil, nil, nil, 2*60*60*1000)
	testutil.Ok(tb, err)
	defer head.Close()

	app := head.Appender()
	for _, s := range series {
		ref := uint64(0)
		it := s.Iterator()
		for it.Next() {
			t, v := it.At()
			if ref != 0 {
				err := app.AddFast(ref, t, v)
				if err == nil {
					continue
				}
			}
			ref, err = app.Add(s.Labels(), t, v)
			testutil.Ok(tb, err)
			refs[i] = ref
		}
		testutil.Ok(tb, it.Err())
	}
	err = app.Commit()
	testutil.Ok(tb, err)

	compactor, err := NewLeveledCompactor(context.Background(), nil, log.NewNopLogger(), []int64{1000000}, nil)
	testutil.Ok(tb, err)

	testutil.Ok(tb, os.MkdirAll(dir, 0777))

	ulid, err := compactor.Write(dir, head, head.MinTime(), head.MaxTime(), nil)
	testutil.Ok(tb, err)
	return filepath.Join(dir, ulid.String())
}

// genSeries generates series with a given number of labels and values.
func genSeries(totalSeries, labelCount int, mint, maxt int64) []Series {
	if totalSeries == 0 || labelCount == 0 {
		return nil
	}
	series := make([]Series, totalSeries)

	for i := 0; i < totalSeries; i++ {
		lbls := make(map[string]string, labelCount)
		for len(lbls) < labelCount {
			lbls[randString()] = randString()
		}
		samples := make([]tsdbutil.Sample, 0, maxt-mint+1)
		for t := mint; t <= maxt; t++ {
			samples = append(samples, sample{t: t, v: rand.Float64()})
		}
		series[i] = newSeries(lbls, samples)
	}

	return series
}

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
	letterIdxBits = 6                    // 6 bits to represent a letter index
	letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
	letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
)

// randString generates random string.
func randString() string {
	maxLength := int32(50)
	length := rand.Int31n(maxLength)
	b := make([]byte, length+1)
	// A rand.Int63() generates 63 random bits, enough for letterIdxMax characters!
	for i, cache, remain := length, rand.Int63(), letterIdxMax; i >= 0; {
		if remain == 0 {
			cache, remain = rand.Int63(), letterIdxMax
		}
		if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
			b[i] = letterBytes[idx]
			i--
		}
		cache >>= letterIdxBits
		remain--
	}

	return string(b)
}
<commit_msg>nit<commit_after>// Copyright 2017 The Prometheus 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 tsdb

import (
	"context"
	"io/ioutil"
	"math/rand"
	"os"
	"path/filepath"
	"testing"

	"github.com/go-kit/kit/log"
	"github.com/prometheus/tsdb/testutil"
	"github.com/prometheus/tsdb/tsdbutil"
)

// In Prometheus 2.1.0 we had a bug where the meta.json version was falsely bumped
// to 2. We had a migration in place resetting it to 1 but we should move immediately to
// version 3 next time to avoid confusion and issues.
func TestBlockMetaMustNeverBeVersion2(t *testing.T) {
	dir, err := ioutil.TempDir("", "metaversion")
	testutil.Ok(t, err)
	defer os.RemoveAll(dir)

	testutil.Ok(t, writeMetaFile(dir, &BlockMeta{}))

	meta, err := readMetaFile(dir)
	testutil.Ok(t, err)
	testutil.Assert(t, meta.Version != 2, "meta.json version must never be 2")
}

func TestSetCompactionFailed(t *testing.T) {
	tmpdir, err := ioutil.TempDir("", "test")
	testutil.Ok(t, err)
	defer os.RemoveAll(tmpdir)

	blockDir := createBlock(t, tmpdir, genSeries(1, 1, 0, 0))
	b, err := OpenBlock(nil, blockDir, nil)
	testutil.Ok(t, err)
	testutil.Equals(t, false, b.meta.Compaction.Failed)
	testutil.Ok(t, b.setCompactionFailed())
	testutil.Equals(t, true, b.meta.Compaction.Failed)
	testutil.Ok(t, b.Close())

	b, err = OpenBlock(nil, blockDir, nil)
	testutil.Ok(t, err)
	testutil.Equals(t, true, b.meta.Compaction.Failed)
	testutil.Ok(t, b.Close())
}

// createBlock creates a block with given set of series and returns its dir.
func createBlock(tb testing.TB, dir string, series []Series) string {
	head, err := NewHead(nil, nil, nil, 2*60*60*1000)
	testutil.Ok(tb, err)
	defer head.Close()

	app := head.Appender()
	for _, s := range series {
		ref := uint64(0)
		it := s.Iterator()
		for it.Next() {
			t, v := it.At()
			if ref != 0 {
				err := app.AddFast(ref, t, v)
				if err == nil {
					continue
				}
			}
			ref, err = app.Add(s.Labels(), t, v)
			testutil.Ok(tb, err)
		}
		testutil.Ok(tb, it.Err())
	}
	err = app.Commit()
	testutil.Ok(tb, err)

	compactor, err := NewLeveledCompactor(context.Background(), nil, log.NewNopLogger(), []int64{1000000}, nil)
	testutil.Ok(tb, err)

	testutil.Ok(tb, os.MkdirAll(dir, 0777))

	ulid, err := compactor.Write(dir, head, head.MinTime(), head.MaxTime(), nil)
	testutil.Ok(tb, err)
	return filepath.Join(dir, ulid.String())
}

// genSeries generates series with a given number of labels and values.
func genSeries(totalSeries, labelCount int, mint, maxt int64) []Series {
	if totalSeries == 0 || labelCount == 0 {
		return nil
	}
	series := make([]Series, totalSeries)

	for i := 0; i < totalSeries; i++ {
		lbls := make(map[string]string, labelCount)
		for len(lbls) < labelCount {
			lbls[randString()] = randString()
		}
		samples := make([]tsdbutil.Sample, 0, maxt-mint+1)
		for t := mint; t <= maxt; t++ {
			samples = append(samples, sample{t: t, v: rand.Float64()})
		}
		series[i] = newSeries(lbls, samples)
	}

	return series
}

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
	letterIdxBits = 6                    // 6 bits to represent a letter index
	letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
	letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
)

// randString generates random string.
func randString() string {
	maxLength := int32(50)
	length := rand.Int31n(maxLength)
	b := make([]byte, length+1)
	// A rand.Int63() generates 63 random bits, enough for letterIdxMax characters!
	for i, cache, remain := length, rand.Int63(), letterIdxMax; i >= 0; {
		if remain == 0 {
			cache, remain = rand.Int63(), letterIdxMax
		}
		if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
			b[i] = letterBytes[idx]
			i--
		}
		cache >>= letterIdxBits
		remain--
	}

	return string(b)
}
<|endoftext|>


<commit_before>// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build ignore

// Addmod adds a module as a txtar archive to the testdata/mod directory.
//
// Usage:
//
//	go run addmod.go path@version...
//
// It should only be used for very small modules - we do not want to check
// very large files into testdata/mod.
//
// It is acceptable to edit the archive afterward to remove or shorten files.
// See mod/README for more information.
//
package main

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

	"../internal/txtar"
)

func usage() {
	fmt.Fprintf(os.Stderr, "usage: go run addmod.go path@version...\n")
	os.Exit(2)
}

var tmpdir string

func fatalf(format string, args ...interface{}) {
	os.RemoveAll(tmpdir)
	log.Fatalf(format, args...)
}

const goCmd = "vgo"

func main() {
	flag.Usage = usage
	flag.Parse()
	if flag.NArg() == 0 {
		usage()
	}

	log.SetPrefix("addmod: ")
	log.SetFlags(0)

	var err error
	tmpdir, err = ioutil.TempDir("", "addmod-")
	if err != nil {
		log.Fatal(err)
	}

	run := func(command string, args ...string) string {
		cmd := exec.Command(command, args...)
		cmd.Dir = tmpdir
		var stderr bytes.Buffer
		cmd.Stderr = &stderr
		out, err := cmd.Output()
		if err != nil {
			fatalf("%s %s: %v\n%s", command, strings.Join(args, " "), err, stderr.Bytes())
		}
		return string(out)
	}

	gopath := strings.TrimSpace(run("go", "env", "GOPATH"))
	if gopath == "" {
		fatalf("cannot find GOPATH")
	}

	exitCode := 0
	for _, arg := range flag.Args() {
		if err := ioutil.WriteFile(filepath.Join(tmpdir, "go.mod"), []byte("module m\n"), 0666); err != nil {
			fatalf("%v", err)
		}
		run(goCmd, "get", "-d", arg)
		path := arg
		if i := strings.Index(path, "@"); i >= 0 {
			path = path[:i]
		}
		out := run(goCmd, "list", "-m", "-f={{.Path}} {{.Version}} {{.Dir}}", path)
		f := strings.Fields(out)
		if len(f) != 3 {
			log.Printf("go list -m %s: unexpected output %q", arg, out)
			exitCode = 1
			continue
		}
		path, vers, dir := f[0], f[1], f[2]
		mod, err := ioutil.ReadFile(filepath.Join(gopath, "pkg/mod/cache/download", path, "@v", vers+".mod"))
		if err != nil {
			log.Printf("%s: %v", arg, err)
			exitCode = 1
			continue
		}
		info, err := ioutil.ReadFile(filepath.Join(gopath, "pkg/mod/cache/download", path, "@v", vers+".info"))
		if err != nil {
			log.Printf("%s: %v", arg, err)
			exitCode = 1
			continue
		}

		a := new(txtar.Archive)
		title := arg
		if !strings.Contains(arg, "@") {
			title += "@" + vers
		}
		a.Comment = []byte(fmt.Sprintf("module %s\n\n", title))
		a.Files = []txtar.File{
			{Name: ".mod", Data: mod},
			{Name: ".info", Data: info},
		}
		dir = filepath.Clean(dir)
		err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
			if !info.Mode().IsRegular() {
				return nil
			}
			name := info.Name()
			if name == "go.mod" || strings.HasSuffix(name, ".go") {
				data, err := ioutil.ReadFile(path)
				if err != nil {
					return err
				}
				a.Files = append(a.Files, txtar.File{Name: strings.TrimPrefix(path, dir+string(filepath.Separator)), Data: data})
			}
			return nil
		})
		if err != nil {
			log.Printf("%s: %v", arg, err)
			exitCode = 1
			continue
		}

		data := txtar.Format(a)
		target := filepath.Join("mod", strings.ReplaceAll(path, "/", "_")+"_"+vers+".txt")
		if err := ioutil.WriteFile(target, data, 0666); err != nil {
			log.Printf("%s: %v", arg, err)
			exitCode = 1
			continue
		}
	}
	os.RemoveAll(tmpdir)
	os.Exit(exitCode)
}
<commit_msg>cmd/go/testdata: update addmod.go to work with the mainline go command<commit_after>// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build ignore

// Addmod adds a module as a txtar archive to the testdata/mod directory.
//
// Usage:
//
//	go run addmod.go path@version...
//
// It should only be used for very small modules - we do not want to check
// very large files into testdata/mod.
//
// It is acceptable to edit the archive afterward to remove or shorten files.
// See mod/README for more information.
//
package main

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

	"cmd/go/internal/txtar"
)

func usage() {
	fmt.Fprintf(os.Stderr, "usage: go run addmod.go path@version...\n")
	os.Exit(2)
}

var tmpdir string

func fatalf(format string, args ...interface{}) {
	os.RemoveAll(tmpdir)
	log.Fatalf(format, args...)
}

const goCmd = "go"

func main() {
	flag.Usage = usage
	flag.Parse()
	if flag.NArg() == 0 {
		usage()
	}

	log.SetPrefix("addmod: ")
	log.SetFlags(0)

	var err error
	tmpdir, err = ioutil.TempDir("", "addmod-")
	if err != nil {
		log.Fatal(err)
	}

	run := func(command string, args ...string) string {
		cmd := exec.Command(command, args...)
		cmd.Dir = tmpdir
		var stderr bytes.Buffer
		cmd.Stderr = &stderr
		out, err := cmd.Output()
		if err != nil {
			fatalf("%s %s: %v\n%s", command, strings.Join(args, " "), err, stderr.Bytes())
		}
		return string(out)
	}

	gopath := strings.TrimSpace(run("go", "env", "GOPATH"))
	if gopath == "" {
		fatalf("cannot find GOPATH")
	}

	exitCode := 0
	for _, arg := range flag.Args() {
		if err := ioutil.WriteFile(filepath.Join(tmpdir, "go.mod"), []byte("module m\n"), 0666); err != nil {
			fatalf("%v", err)
		}
		run(goCmd, "get", "-d", arg)
		path := arg
		if i := strings.Index(path, "@"); i >= 0 {
			path = path[:i]
		}
		out := run(goCmd, "list", "-m", "-f={{.Path}} {{.Version}} {{.Dir}}", path)
		f := strings.Fields(out)
		if len(f) != 3 {
			log.Printf("go list -m %s: unexpected output %q", arg, out)
			exitCode = 1
			continue
		}
		path, vers, dir := f[0], f[1], f[2]
		mod, err := ioutil.ReadFile(filepath.Join(gopath, "pkg/mod/cache/download", path, "@v", vers+".mod"))
		if err != nil {
			log.Printf("%s: %v", arg, err)
			exitCode = 1
			continue
		}
		info, err := ioutil.ReadFile(filepath.Join(gopath, "pkg/mod/cache/download", path, "@v", vers+".info"))
		if err != nil {
			log.Printf("%s: %v", arg, err)
			exitCode = 1
			continue
		}

		a := new(txtar.Archive)
		title := arg
		if !strings.Contains(arg, "@") {
			title += "@" + vers
		}
		a.Comment = []byte(fmt.Sprintf("module %s\n\n", title))
		a.Files = []txtar.File{
			{Name: ".mod", Data: mod},
			{Name: ".info", Data: info},
		}
		dir = filepath.Clean(dir)
		err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
			if !info.Mode().IsRegular() {
				return nil
			}
			name := info.Name()
			if name == "go.mod" || strings.HasSuffix(name, ".go") {
				data, err := ioutil.ReadFile(path)
				if err != nil {
					return err
				}
				a.Files = append(a.Files, txtar.File{Name: strings.TrimPrefix(path, dir+string(filepath.Separator)), Data: data})
			}
			return nil
		})
		if err != nil {
			log.Printf("%s: %v", arg, err)
			exitCode = 1
			continue
		}

		data := txtar.Format(a)
		target := filepath.Join("mod", strings.ReplaceAll(path, "/", "_")+"_"+vers+".txt")
		if err := ioutil.WriteFile(target, data, 0666); err != nil {
			log.Printf("%s: %v", arg, err)
			exitCode = 1
			continue
		}
	}
	os.RemoveAll(tmpdir)
	os.Exit(exitCode)
}
<|endoftext|>