<commit_before>package godless

import "fmt"

//go:generate mockgen -destination mock/mock_remote_store.go -imports lib=github.com/johnny-morrice/godless -self_package lib github.com/johnny-morrice/godless RemoteStore

type RemoteStore interface {
	Connect() error
	AddNamespace(RemoteNamespaceRecord) (RemoteStoreAddress, error)
	AddIndex(RemoteNamespaceIndex) (RemoteStoreAddress, error)
	CatNamespace(RemoteStoreAddress) (RemoteNamespaceRecord, error)
	CatIndex(RemoteStoreAddress) (RemoteNamespaceIndex, error)
	Disconnect() error
}

type RemoteStoreAddress interface {
	Path() string
}

type RemoteNamespaceRecord struct {
	Namespace Namespace
}

// TODO tiny type for table name.
type RemoteNamespaceIndex struct {
	Index map[TableName][]RemoteStoreAddress
}

func EmptyRemoteNamespaceIndex() RemoteNamespaceIndex {
	return MakeRemoteNamespaceIndex(map[TableName]RemoteStoreAddress{})
}

func MakeRemoteNamespaceIndex(indices map[TableName]RemoteStoreAddress) RemoteNamespaceIndex {
	out := RemoteNamespaceIndex{
		Index: map[TableName][]RemoteStoreAddress{},
	}

	for table, addr := range indices {
		out.Index[table] = []RemoteStoreAddress{addr}
	}

	return out
}

func (rni RemoteNamespaceIndex) APIIndex() APIRemoteIndex {
	apiIndex := APIRemoteIndex{
		Index: map[string][]string{},
	}

	for table, addrs := range rni.Index {
		apiAddrs := make([]string, len(addrs))
		for i, a := range addrs {
			apiAddrs[i] = a.Path()
		}
		apiIndex.Index[string(table)] = apiAddrs
	}

	return apiIndex
}

func (rni RemoteNamespaceIndex) AllTables() []TableName {
	tables := make([]TableName, len(rni.Index))

	i := 0
	for name := range rni.Index {
		tables[i] = name
		i++
	}

	return tables
}

func (rni RemoteNamespaceIndex) GetTableAddrs(tableName TableName) ([]RemoteStoreAddress, error) {
	indices, ok := rni.Index[tableName]

	if !ok {
		return nil, fmt.Errorf("No table in index: '%v'", tableName)
	}

	return indices, nil
}

func (rni RemoteNamespaceIndex) JoinNamespace(addr RemoteStoreAddress, namespace Namespace) RemoteNamespaceIndex {
	tables := namespace.GetTableNames()

	joined := rni.Copy()
	for _, t := range tables {
		joined = joined.addTable(t, addr)
	}

	return joined
}

func (rni RemoteNamespaceIndex) addTable(table TableName, addr RemoteStoreAddress) RemoteNamespaceIndex {
	if addrs, ok := rni.Index[table]; ok {
		rni.Index[table] = append(addrs, addr)
	} else {
		rni.Index[table] = []RemoteStoreAddress{addr}
	}

	return rni
}

func (rni RemoteNamespaceIndex) Copy() RemoteNamespaceIndex {
	cpy := EmptyRemoteNamespaceIndex()

	for table, addrs := range rni.Index {
		addrCopy := make([]RemoteStoreAddress, len(addrs))
		for i, a := range addrs {
			addrCopy[i] = a
		}
		cpy.Index[table] = addrCopy
	}

	return cpy
}

var EMPTY_RECORD RemoteNamespaceRecord
var EMPTY_INDEX RemoteNamespaceIndex
<commit_msg>Fix defect in RemoteStoreIndex<commit_after>package godless

import (
	"fmt"
	"sort"
)

//go:generate mockgen -destination mock/mock_remote_store.go -imports lib=github.com/johnny-morrice/godless -self_package lib github.com/johnny-morrice/godless RemoteStore

type RemoteStore interface {
	Connect() error
	AddNamespace(RemoteNamespaceRecord) (RemoteStoreAddress, error)
	AddIndex(RemoteNamespaceIndex) (RemoteStoreAddress, error)
	CatNamespace(RemoteStoreAddress) (RemoteNamespaceRecord, error)
	CatIndex(RemoteStoreAddress) (RemoteNamespaceIndex, error)
	Disconnect() error
}

type RemoteStoreAddress interface {
	Path() string
}

type byPath []RemoteStoreAddress

func (addrs byPath) Len() int {
	return len(addrs)
}

func (addrs byPath) Swap(i, j int) {
	addrs[i], addrs[j] = addrs[j], addrs[i]
}

func (addrs byPath) Less(i, j int) bool {
	return addrs[i].Path() < addrs[j].Path()
}

func normalStoreAddress(addrs []RemoteStoreAddress) []RemoteStoreAddress {
	uniq := uniqStoreAddress(addrs)
	sort.Sort(byPath(uniq))
	return uniq
}

func uniqStoreAddress(addrs []RemoteStoreAddress) []RemoteStoreAddress {
	dedupe := map[string]RemoteStoreAddress{}

	for _, a := range addrs {
		path := a.Path()
		if _, present := dedupe[path]; !present {
			dedupe[path] = a
		}
	}

	uniq := make([]RemoteStoreAddress, len(dedupe))

	i := 0
	for _, a := range dedupe {
		uniq[i] = a
		i++
	}

	return uniq
}

type RemoteNamespaceRecord struct {
	Namespace Namespace
}

// TODO tiny type for table name.
type RemoteNamespaceIndex struct {
	Index map[TableName][]RemoteStoreAddress
}

func EmptyRemoteNamespaceIndex() RemoteNamespaceIndex {
	return MakeRemoteNamespaceIndex(map[TableName]RemoteStoreAddress{})
}

func MakeRemoteNamespaceIndex(indices map[TableName]RemoteStoreAddress) RemoteNamespaceIndex {
	out := RemoteNamespaceIndex{
		Index: map[TableName][]RemoteStoreAddress{},
	}

	for table, addr := range indices {
		out.Index[table] = []RemoteStoreAddress{addr}
	}

	return out
}

func (rni RemoteNamespaceIndex) APIIndex() APIRemoteIndex {
	apiIndex := APIRemoteIndex{
		Index: map[string][]string{},
	}

	for table, addrs := range rni.Index {
		apiAddrs := make([]string, len(addrs))
		for i, a := range addrs {
			apiAddrs[i] = a.Path()
		}
		apiIndex.Index[string(table)] = apiAddrs
	}

	return apiIndex
}

func (rni RemoteNamespaceIndex) AllTables() []TableName {
	tables := make([]TableName, len(rni.Index))

	i := 0
	for name := range rni.Index {
		tables[i] = name
		i++
	}

	return tables
}

func (rni RemoteNamespaceIndex) GetTableAddrs(tableName TableName) ([]RemoteStoreAddress, error) {
	indices, ok := rni.Index[tableName]

	if !ok {
		return nil, fmt.Errorf("No table in index: '%v'", tableName)
	}

	return indices, nil
}

func (rni RemoteNamespaceIndex) JoinNamespace(addr RemoteStoreAddress, namespace Namespace) RemoteNamespaceIndex {
	tables := namespace.GetTableNames()

	joined := rni.Copy()
	for _, t := range tables {
		joined = joined.addTable(t, addr)
	}

	return joined
}

func (rni RemoteNamespaceIndex) addTable(table TableName, addr RemoteStoreAddress) RemoteNamespaceIndex {
	if addrs, ok := rni.Index[table]; ok {
		normal := normalStoreAddress(append(addrs, addr))
		rni.Index[table] = normal
	} else {
		rni.Index[table] = []RemoteStoreAddress{addr}
	}

	return rni
}

func (rni RemoteNamespaceIndex) Copy() RemoteNamespaceIndex {
	cpy := EmptyRemoteNamespaceIndex()

	for table, addrs := range rni.Index {
		addrCopy := make([]RemoteStoreAddress, len(addrs))
		for i, a := range addrs {
			addrCopy[i] = a
		}
		cpy.Index[table] = addrCopy
	}

	return cpy
}

var EMPTY_RECORD RemoteNamespaceRecord
var EMPTY_INDEX RemoteNamespaceIndex
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"
)

var (
	Namespace   = "dashboard"
	ConsulAddr  = "127.0.0.1:8500"
	Version     string
	ExtAssetDir string
	Nodes       []Node
	mutex       sync.RWMutex
)

type KVPair struct {
	Key         string
	CreateIndex int64
	ModifyIndex int64
	LockIndex   int64
	Flags       int64
	Value       []byte
}

type Status int64

const (
	Success Status = iota
	Warning
	Danger
	Info
)

func (s Status) MarshalText() ([]byte, error) {
	if s <= Danger {
		return []byte(strings.ToLower(s.String())), nil
	} else {
		return []byte(strconv.FormatInt(int64(s), 10)), nil
	}
}

type Item struct {
	Category  string `json:"category"`
	Node      string `json:"node"`
	Address   string `json:"address"`
	Timestamp string `json:"timestamp"`
	Status    Status `json:"status"`
	Key       string `json:"key"`
	Data      string `json:"data"`
}

func (kv *KVPair) NewItem() Item {
	item := Item{
		Data:      string(kv.Value),
		Timestamp: time.Unix(kv.Flags/1000, 0).Format("2006-01-02 15:04:05 -0700"),
	}
	item.Status = Status(kv.Flags % 1000)

	// kv.Key : {namespace}/{category}/{node}/{key}
	path := strings.Split(kv.Key, "/")
	item.Category = path[1]
	if len(path) >= 3 {
		item.Node = path[2]
	}
	if len(path) >= 4 {
		item.Key = path[3]
	}
	return item
}

type Node struct {
	Node    string
	Address string
}

func main() {
	var (
		port        int
		showVersion bool
	)
	flag.StringVar(&Namespace, "namespace", Namespace, "Consul kv top level key name. (/v1/kv/{namespace}/...)")
	flag.IntVar(&port, "port", 3000, "http listen port")
	flag.StringVar(&ExtAssetDir, "asset", "", "Serve files located in /assets from local directory. If not specified, use built-in asset.")
	flag.BoolVar(&showVersion, "v", false, "show vesion")
	flag.BoolVar(&showVersion, "version", false, "show vesion")
	flag.Parse()

	if showVersion {
		fmt.Println("consul-kv-dashboard: version:", Version)
		return
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/", indexPage)
	mux.HandleFunc("/api/", kvApiProxy)

	if ExtAssetDir != "" {
		mux.Handle("/assets/",
			http.StripPrefix("/assets/", http.FileServer(http.Dir(ExtAssetDir))))
	} else {
		mux.Handle("/assets/",
			http.FileServer(NewAssetFileSystem("/assets/")))
	}
	http.Handle("/", mux)

	go updateNodeList()
	go watchForTrigger()
	log.Println("listen port:", port)
	log.Println("asset directory:", ExtAssetDir)
	log.Println("namespace:", Namespace)
	log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), nil))
}

func indexPage(w http.ResponseWriter, r *http.Request) {
	var (
		data []byte
		err  error
	)
	if ExtAssetDir == "" {
		data, err = Asset("index.html")
	} else {
		var f *os.File
		f, err = os.Open(ExtAssetDir + "/index.html")
		data, err = ioutil.ReadAll(f)
	}
	if err != nil {
		log.Println(err)
	}
	fmt.Fprint(w, string(data))
}

func kvApiProxy(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	path := strings.TrimLeft(r.URL.Path, "/api")
	resp, _, err := callConsulAPI(
		"/v1/kv/" + Namespace + "/" + path + "?" + r.URL.RawQuery,
	)
	if err != nil {
		http.Error(w, fmt.Sprintf("%s", err), http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusNotFound {
		w.Header().Set("Content-Type", "application/json")
		http.Error(w, "[]", resp.StatusCode)
		return
	}
	if resp.StatusCode != http.StatusOK {
		http.Error(w, "", resp.StatusCode)
		io.Copy(w, resp.Body)
		return
	}
	// copy response header to client
	for name, value := range resp.Header {
		if strings.HasPrefix(name, "X-") || name == "Content-Type" {
			for _, v := range value {
				w.Header().Set(name, v)
			}
		}
	}

	// keys or values
	dec := json.NewDecoder(resp.Body)
	enc := json.NewEncoder(w)
	if _, t := r.Form["keys"]; t {
		var keys []string
		uniqKeyMap := make(map[string]bool)
		dec.Decode(&keys)
		for _, key := range keys {
			path := strings.Split(key, "/")
			if len(path) >= 2 {
				uniqKeyMap[path[1]] = true
			}
		}
		uniqKeys := make([]string, 0, len(uniqKeyMap))
		for key, _ := range uniqKeyMap {
			uniqKeys = append(uniqKeys, key)
		}
		sort.Strings(uniqKeys)
		enc.Encode(uniqKeys)
	} else {
		var kvps []*KVPair
		dec.Decode(&kvps)
		items := make([]Item, 0, len(kvps))
		for _, kv := range kvps {
			item := kv.NewItem()
			if itemInNodes(&item) {
				items = append(items, item)
			}
		}
		enc.Encode(items)
	}
}

func watchForTrigger() {
	var index int64
	lastStatus := make(map[string]Status)
	for {
		resp, newIndex, err := callConsulAPI(
			"/v1/kv/" + Namespace + "/?recurse&wait=55s&index=" + strconv.FormatInt(index, 10),
		)
		if err != nil {
			log.Println("[error]", err)
			time.Sleep(10 * time.Second)
			continue
		}
		index = newIndex
		defer resp.Body.Close()
		var kvps []*KVPair
		dec := json.NewDecoder(resp.Body)
		dec.Decode(&kvps)

		currentStatus := make(map[string]Status)
		for _, kv := range kvps {
			item := kv.NewItem()
			if !itemInNodes(&item) {
				continue
			}
			if _, exist := currentStatus[item.Category]; !exist {
				currentStatus[item.Category] = item.Status
			} else if currentStatus[item.Category] < item.Status {
				currentStatus[item.Category] = item.Status
			}
		}
		for category, status := range currentStatus {
			if _, exist := lastStatus[category]; !exist {
				// at first initialze
				lastStatus[category] = status
			} else if lastStatus[category] != status {
				// status changed. invoking trigger.
				log.Printf("%s status %s -> %s", category, lastStatus[category], status)
				lastStatus[category] = status
			}
		}
		time.Sleep(1 * time.Second)
	}
}

func updateNodeList() {
	var index int64
	for {
		resp, newIndex, err := callConsulAPI(
			"/v1/catalog/nodes?index=" + strconv.FormatInt(index, 10) + "&wait=55s",
		)
		if err != nil {
			log.Println("[error]", err)
			time.Sleep(10 * time.Second)
			continue
		}
		index = newIndex
		defer resp.Body.Close()
		dec := json.NewDecoder(resp.Body)
		mutex.Lock()
		dec.Decode(&Nodes)
		log.Println("[info]", Nodes)
		mutex.Unlock()
		time.Sleep(1 * time.Second)
	}
}

func itemInNodes(item *Item) bool {
	mutex.RLock()
	defer mutex.RUnlock()
	for _, node := range Nodes {
		if item.Node == node.Node {
			item.Address = node.Address
			return true
		}
	}
	return false
}

func callConsulAPI(path string) (*http.Response, int64, error) {
	var index int64
	_url := "http://" + ConsulAddr + path
	log.Println("[info] get", _url)
	resp, err := http.Get(_url)
	if err != nil {
		log.Println("[error]", err)
		return nil, index, err
	}
	_indexes := resp.Header["X-Consul-Index"]
	if len(_indexes) > 0 {
		index, _ = strconv.ParseInt(_indexes[0], 10, 64)
	}
	return resp, index, nil
}
<commit_msg>add trigger argument.<commit_after>package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/exec"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"
)

var (
	Namespace   = "dashboard"
	ConsulAddr  = "127.0.0.1:8500"
	Version     string
	ExtAssetDir string
	Nodes       []Node
	mutex       sync.RWMutex
)

type KVPair struct {
	Key         string
	CreateIndex int64
	ModifyIndex int64
	LockIndex   int64
	Flags       int64
	Value       []byte
}

type Status int64

const (
	Success Status = iota
	Warning
	Danger
	Info
)

func (s Status) MarshalText() ([]byte, error) {
	if s <= Danger {
		return []byte(strings.ToLower(s.String())), nil
	} else {
		return []byte(strconv.FormatInt(int64(s), 10)), nil
	}
}

type Item struct {
	Category  string `json:"category"`
	Node      string `json:"node"`
	Address   string `json:"address"`
	Timestamp string `json:"timestamp"`
	Status    Status `json:"status"`
	Key       string `json:"key"`
	Data      string `json:"data"`
}

func (kv *KVPair) NewItem() Item {
	item := Item{
		Data:      string(kv.Value),
		Timestamp: time.Unix(kv.Flags/1000, 0).Format("2006-01-02 15:04:05 -0700"),
	}
	item.Status = Status(kv.Flags % 1000)

	// kv.Key : {namespace}/{category}/{node}/{key}
	path := strings.Split(kv.Key, "/")
	item.Category = path[1]
	if len(path) >= 3 {
		item.Node = path[2]
	}
	if len(path) >= 4 {
		item.Key = path[3]
	}
	return item
}

type Node struct {
	Node    string
	Address string
}

func main() {
	var (
		port        int
		showVersion bool
		trigger     string
	)
	flag.StringVar(&Namespace, "namespace", Namespace, "Consul kv top level key name. (/v1/kv/{namespace}/...)")
	flag.IntVar(&port, "port", 3000, "http listen port")
	flag.StringVar(&ExtAssetDir, "asset", "", "Serve files located in /assets from local directory. If not specified, use built-in asset.")
	flag.BoolVar(&showVersion, "v", false, "show vesion")
	flag.BoolVar(&showVersion, "version", false, "show vesion")
	flag.StringVar(&trigger, "trigger", "", "trigger command")
	flag.Parse()

	if showVersion {
		fmt.Println("consul-kv-dashboard: version:", Version)
		return
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/", indexPage)
	mux.HandleFunc("/api/", kvApiProxy)

	if ExtAssetDir != "" {
		mux.Handle("/assets/",
			http.StripPrefix("/assets/", http.FileServer(http.Dir(ExtAssetDir))))
	} else {
		mux.Handle("/assets/",
			http.FileServer(NewAssetFileSystem("/assets/")))
	}
	http.Handle("/", mux)

	log.Println("listen port:", port)
	log.Println("asset directory:", ExtAssetDir)
	log.Println("namespace:", Namespace)
	if trigger != "" {
		log.Println("trigger:", trigger)
		go watchForTrigger(trigger)
	}
	go updateNodeList()

	log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), nil))
}

func indexPage(w http.ResponseWriter, r *http.Request) {
	var (
		data []byte
		err  error
	)
	if ExtAssetDir == "" {
		data, err = Asset("index.html")
	} else {
		var f *os.File
		f, err = os.Open(ExtAssetDir + "/index.html")
		data, err = ioutil.ReadAll(f)
	}
	if err != nil {
		log.Println(err)
	}
	fmt.Fprint(w, string(data))
}

func kvApiProxy(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	path := strings.TrimLeft(r.URL.Path, "/api")
	resp, _, err := callConsulAPI(
		"/v1/kv/" + Namespace + "/" + path + "?" + r.URL.RawQuery,
	)
	if err != nil {
		http.Error(w, fmt.Sprintf("%s", err), http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusNotFound {
		w.Header().Set("Content-Type", "application/json")
		http.Error(w, "[]", resp.StatusCode)
		return
	}
	if resp.StatusCode != http.StatusOK {
		http.Error(w, "", resp.StatusCode)
		io.Copy(w, resp.Body)
		return
	}
	// copy response header to client
	for name, value := range resp.Header {
		if strings.HasPrefix(name, "X-") || name == "Content-Type" {
			for _, v := range value {
				w.Header().Set(name, v)
			}
		}
	}

	// keys or values
	dec := json.NewDecoder(resp.Body)
	enc := json.NewEncoder(w)
	if _, t := r.Form["keys"]; t {
		var keys []string
		uniqKeyMap := make(map[string]bool)
		dec.Decode(&keys)
		for _, key := range keys {
			path := strings.Split(key, "/")
			if len(path) >= 2 {
				uniqKeyMap[path[1]] = true
			}
		}
		uniqKeys := make([]string, 0, len(uniqKeyMap))
		for key, _ := range uniqKeyMap {
			uniqKeys = append(uniqKeys, key)
		}
		sort.Strings(uniqKeys)
		enc.Encode(uniqKeys)
	} else {
		var kvps []*KVPair
		dec.Decode(&kvps)
		items := make([]Item, 0, len(kvps))
		for _, kv := range kvps {
			item := kv.NewItem()
			if itemInNodes(&item) {
				items = append(items, item)
			}
		}
		enc.Encode(items)
	}
}

func watchForTrigger(command string) {
	var index int64
	lastStatus := make(map[string]Status)
	for {
		resp, newIndex, err := callConsulAPI(
			"/v1/kv/" + Namespace + "/?recurse&wait=55s&index=" + strconv.FormatInt(index, 10),
		)
		if err != nil {
			log.Println("[error]", err)
			time.Sleep(10 * time.Second)
			continue
		}
		index = newIndex
		defer resp.Body.Close()
		var kvps []*KVPair
		dec := json.NewDecoder(resp.Body)
		dec.Decode(&kvps)

		currentItem := make(map[string]Item)
		for _, kv := range kvps {
			item := kv.NewItem()
			if !itemInNodes(&item) {
				continue
			}
			if _, exist := currentItem[item.Category]; !exist {
				currentItem[item.Category] = item
			} else if currentItem[item.Category].Status < item.Status {
				currentItem[item.Category] = item
			}
		}
		for category, item := range currentItem {
			if _, exist := lastStatus[category]; !exist {
				// at first initialze
				lastStatus[category] = item.Status
				log.Printf("[info] %s: status %s", category, item.Status)
			} else if lastStatus[category] != item.Status {
				// status changed. invoking trigger.
				log.Printf("[info] %s: status %s -> %s", category, lastStatus[category], item.Status)
				lastStatus[category] = item.Status
				b, _ := json.Marshal(item)
				err := invokePipe(command, bytes.NewReader(b))
				if err != nil {
					log.Println("[error]", err)
				}
			}
		}
		time.Sleep(1 * time.Second)
	}
}

func invokePipe(command string, src io.Reader) error {
	log.Println("[info] Invoking command:", command)
	cmd := exec.Command("sh", "-c", command)
	stdin, err := cmd.StdinPipe()
	if err != nil {
		return err
	}
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return err
	}
	stderr, err := cmd.StderrPipe()
	if err != nil {
		return err
	}

	err = cmd.Start()
	if err != nil {
		return err
	}
	cmdCh := make(chan error)
	// src => stdin
	go func() {
		_, err := io.Copy(stdin, src)
		if err != nil {
			cmdCh <- err
		}
		stdin.Close()
	}()
	// wait for command exit
	go func() {
		cmdCh <- cmd.Wait()
	}()
	go io.Copy(os.Stdout, stdout)
	go io.Copy(os.Stderr, stderr)

	cmdErr := <-cmdCh
	return cmdErr
}

func updateNodeList() {
	var index int64
	for {
		resp, newIndex, err := callConsulAPI(
			"/v1/catalog/nodes?index=" + strconv.FormatInt(index, 10) + "&wait=55s",
		)
		if err != nil {
			log.Println("[error]", err)
			time.Sleep(10 * time.Second)
			continue
		}
		index = newIndex
		defer resp.Body.Close()
		dec := json.NewDecoder(resp.Body)
		mutex.Lock()
		dec.Decode(&Nodes)
		log.Println("[info]", Nodes)
		mutex.Unlock()
		time.Sleep(1 * time.Second)
	}
}

func itemInNodes(item *Item) bool {
	mutex.RLock()
	defer mutex.RUnlock()
	for _, node := range Nodes {
		if item.Node == node.Node {
			item.Address = node.Address
			return true
		}
	}
	return false
}

func callConsulAPI(path string) (*http.Response, int64, error) {
	var index int64
	_url := "http://" + ConsulAddr + path
	log.Println("[info] get", _url)
	resp, err := http.Get(_url)
	if err != nil {
		log.Println("[error]", err)
		return nil, index, err
	}
	_indexes := resp.Header["X-Consul-Index"]
	if len(_indexes) > 0 {
		index, _ = strconv.ParseInt(_indexes[0], 10, 64)
	}
	return resp, index, nil
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"os"
	"os/signal"
	"syscall"

	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/robfig/cron"
)

var version = "undefined"

func main() {
	var config = &Config{}
	flag.StringVar(&config.Environment, "Environment", "dev", "Environment dev|int|stg|test|prep|prod")
	flag.StringVar(&config.LogLevel, "LogLevel", "debug", "logging threshold level: debug|info|warn|error|fatal|panic")
	flag.IntVar(&config.Port, "Port", 9898, "HTTP port to listen on")
	flag.StringVar(&config.Hostname, "Hostname", "", "Hostname")
	flag.StringVar(&config.ConsulURI, "ConsulURI", "localhost:8500", "Consul address")
	flag.StringVar(&config.ConsulTTL, "ConsulTTL", "10s", "Consul session TTL")
	flag.StringVar(&config.ConsulKV, "ConsulKV", "pgha", "Consul KV prefix")
	flag.IntVar(&config.ConsulRetry, "ConsulRetry", 10, "Number of Consul connection reties")
	flag.StringVar(&config.PostgresURI, "PostgresURI", "postgres://user:password@localhost/db?sslmode=disable", "Postgres URI")
	flag.IntVar(&config.PostgresCheck, "PostgresCheck", 5, "Postgres checks interval in seconds")
	flag.StringVar(&config.NatsURI, "NatsURI", "nats://localhost:4222", "Nats URI")
	flag.StringVar(&config.User, "User", "postgres", "User to run under")
	flag.Parse()
	setLogLevel(config.LogLevel)
	log.Infof("Starting with config: %+v", config)

	if config.Hostname == "" {
		config.Hostname, _ = os.Hostname()
	}

	if config.Environment != "debug" {
		//check if running under postgres
		id := execId(10)
		if id != config.User {
			log.Fatalf("Running under %s expected postgres", id)
		}

		//check if repmgr is installed
		execRepmgrVersion(2)
	}

	scheduler := cron.New()
	scheduler.Start()

	status := NewStatus()

	pgmon, err := NewPGMonitor(config.PostgresURI, status)
	if err != nil {
		log.Fatalf("Postgres connection failed %s", err.Error())
	}

	isMaster, err := pgmon.IsMaster()
	if err != nil {
		log.Fatalf("Can't determine Postgres cluster state %s", err.Error())
	}
	status.SetPostgresStatus(isMaster)
	go pgmon.Start()

	pgstats, err := NewPGStats(config, scheduler)
	if err != nil {
		log.Fatalf("PGStats init failed %s", err.Error())
	}

	stats, err := pgstats.GetReplicationStats()
	if err != nil {
		log.Fatalf("PGStats GetReplicationStats failed %s", err.Error())
	}
	log.Infof("Postgres replication stats: %+v", stats)

	err = pgstats.SaveReplicationStats(stats)
	if err != nil {
		log.Fatalf("PGStats SaveReplicationStats failed %s", err.Error())
	}
	pgstats.Start()

	election, err := NewElection(config, status)
	if err != nil {
		log.Fatal(err.Error())
	}

INIT:
	leader, err := election.GetLeaderWithRetry(10, 5)
	if err != nil {
		//stop pg service if is master
		if isMaster {
			execPgStop(20)
			log.Fatal("Stopping postgres service: this pg node is master but Consul is unreachable")
		}
		log.Fatalf("Consul connection failed %s", err.Error())
	}

	if len(leader) > 0 {
		if isMaster {
			//stop pg service, this should never happen
			execPgStop(20)
			log.Fatalf("Stopping postgres service: leader is %v but this pg node %v is master", leader, config.Hostname)
		} else {
			log.Infof("Should join cluster as follower since leader is %v", leader)
		}
	} else {
		if isMaster {
			log.Infof("Should join cluster as leader since no leader found and this pg node %v is master", config.Hostname)
		} else {
			//retry till there is a master running (do not enter election mode)
			log.Warnf("Conflict detected: no leader found but this pg node %v is slave, will wait 5sec and retry", config.Hostname)
			time.Sleep(5 * time.Second)
			//TODO: better use recursion
			goto INIT
		}
	}

	go election.Start()

	server, err := NewHttpServer(config, status, election)
	if err != nil {
		log.Fatal(err.Error())
	}
	log.Infof("Starting HTTP server on port %v", config.Port)
	go server.Start()

	//wait for exit signal
	sigChan := make(chan os.Signal)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
	sig := <-sigChan
	log.Infof("Shutting down %v signal received", sig)
	election.Stop()
	pgmon.Stop()
	scheduler.Start()
}

func setLogLevel(levelName string) {
	level, err := log.ParseLevel(levelName)
	if err != nil {
		log.Fatal(err)
	}
	log.SetLevel(level)
}
<commit_msg>xlog stats info at startup<commit_after>package main

import (
	"flag"
	"os"
	"os/signal"
	"syscall"

	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/robfig/cron"
)

var version = "undefined"

func main() {
	var config = &Config{}
	flag.StringVar(&config.Environment, "Environment", "dev", "Environment dev|int|stg|test|prep|prod")
	flag.StringVar(&config.LogLevel, "LogLevel", "debug", "logging threshold level: debug|info|warn|error|fatal|panic")
	flag.IntVar(&config.Port, "Port", 9898, "HTTP port to listen on")
	flag.StringVar(&config.Hostname, "Hostname", "", "Hostname")
	flag.StringVar(&config.ConsulURI, "ConsulURI", "localhost:8500", "Consul address")
	flag.StringVar(&config.ConsulTTL, "ConsulTTL", "10s", "Consul session TTL")
	flag.StringVar(&config.ConsulKV, "ConsulKV", "pgha", "Consul KV prefix")
	flag.IntVar(&config.ConsulRetry, "ConsulRetry", 10, "Number of Consul connection reties")
	flag.StringVar(&config.PostgresURI, "PostgresURI", "postgres://user:password@localhost/db?sslmode=disable", "Postgres URI")
	flag.IntVar(&config.PostgresCheck, "PostgresCheck", 5, "Postgres checks interval in seconds")
	flag.StringVar(&config.NatsURI, "NatsURI", "nats://localhost:4222", "Nats URI")
	flag.StringVar(&config.User, "User", "postgres", "User to run under")
	flag.Parse()
	setLogLevel(config.LogLevel)
	log.Infof("Starting with config: %+v", config)

	if config.Hostname == "" {
		config.Hostname, _ = os.Hostname()
	}

	if config.Environment != "debug" {
		//check if running under postgres
		id := execId(10)
		if id != config.User {
			log.Fatalf("Running under %s expected postgres", id)
		}

		//check if repmgr is installed
		execRepmgrVersion(2)
	}

	scheduler := cron.New()
	scheduler.Start()

	status := NewStatus()

	pgmon, err := NewPGMonitor(config.PostgresURI, status)
	if err != nil {
		log.Fatalf("Postgres connection failed %s", err.Error())
	}

	isMaster, err := pgmon.IsMaster()
	if err != nil {
		log.Fatalf("Can't determine Postgres cluster state %s", err.Error())
	}
	status.SetPostgresStatus(isMaster)
	go pgmon.Start()

	pgstats, err := NewPGStats(config, scheduler)
	if err != nil {
		log.Fatalf("PGStats init failed %s", err.Error())
	}

	stats, err := pgstats.GetReplicationStats()
	if err != nil {
		log.Fatalf("PGStats GetReplicationStats failed %s", err.Error())
	}
	log.Infof("Postgres replication stats: %+v", stats)
	pgstats.Start()

	election, err := NewElection(config, status)
	if err != nil {
		log.Fatal(err.Error())
	}

INIT:
	leader, err := election.GetLeaderWithRetry(10, 5)
	if err != nil {
		//stop pg service if is master
		if isMaster {
			execPgStop(20)
			log.Fatal("Stopping postgres service: this pg node is master but Consul is unreachable")
		}
		log.Fatalf("Consul connection failed %s", err.Error())
	}

	if len(leader) > 0 {
		if isMaster {
			//stop pg service, this should never happen
			execPgStop(20)
			log.Fatalf("Stopping postgres service: leader is %v but this pg node %v is master", leader, config.Hostname)
		} else {
			log.Infof("Should join cluster as follower since leader is %v", leader)
		}
	} else {
		if isMaster {
			log.Infof("Should join cluster as leader since no leader found and this pg node %v is master", config.Hostname)
		} else {
			//retry till there is a master running (do not enter election mode)
			log.Warnf("Conflict detected: no leader found but this pg node %v is slave, will wait 5sec and retry", config.Hostname)
			time.Sleep(5 * time.Second)
			//TODO: better use recursion
			goto INIT
		}
	}

	go election.Start()

	server, err := NewHttpServer(config, status, election)
	if err != nil {
		log.Fatal(err.Error())
	}
	log.Infof("Starting HTTP server on port %v", config.Port)
	go server.Start()

	//wait for exit signal
	sigChan := make(chan os.Signal)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
	sig := <-sigChan
	log.Infof("Shutting down %v signal received", sig)
	election.Stop()
	pgmon.Stop()
	scheduler.Start()
}

func setLogLevel(levelName string) {
	level, err := log.ParseLevel(levelName)
	if err != nil {
		log.Fatal(err)
	}
	log.SetLevel(level)
}
<|endoftext|>


<commit_before>package app

import (
	"github.com/revel/revel"
	"github.com/revel/revel/modules/static/app/controllers"
	"cruncher/app/models/riotapi"
	"cruncher/app/models/cron"
	"cruncher/app/models/database"
)

func init() {
	// Filters is the default set of global filters.
	revel.Filters = []revel.Filter{
		revel.PanicFilter,             // Recover from panics and display an error page instead.
		revel.RouterFilter,            // Use the routing table to select the right Action
		revel.FilterConfiguringFilter, // A hook for adding or removing per-Action filters.
		revel.ParamsFilter,            // Parse parameters into Controller.Params.
		revel.SessionFilter,           // Restore and write the session cookie.
		revel.FlashFilter,             // Restore and write the flash cookie.
		revel.ValidationFilter,        // Restore kept validation errors and save new ones from cookie.
		revel.I18nFilter,              // Resolve the requested language
		HeaderFilter,                  // Add some security based headers
		revel.InterceptorFilter,       // Run interceptors around the action.
		revel.CompressFilter,          // Compress the result.
		revel.ActionInvoker,           // Invoke the action.
	}

	// register startup functions with OnAppStart
	// ( order dependent )
	revel.OnAppStart(riotapi.LoadAPIKey)
	revel.OnAppStart(database.Connect)
	revel.OnAppStart(cron.Start)

	revel.FilterController(controllers.Static{}).Add(staticCache)
}

// TODO turn this into revel.HeaderFilter
// should probably also have a filter for CSRF
// not sure if it can go in the same filter or not
var HeaderFilter = func(c *revel.Controller, fc []revel.Filter) {
	// Add some common security headers
	c.Response.Out.Header().Add("X-Frame-Options", "SAMEORIGIN")
	c.Response.Out.Header().Add("X-XSS-Protection", "1; mode=block")
	c.Response.Out.Header().Add("X-Content-Type-Options", "nosniff")

	fc[0](c, fc[1:]) // Execute the next filter stage.
}

func staticCache(c *revel.Controller, fc []revel.Filter) {
	fc[0](c, fc[1:])
	c.Response.Out.Header().Set("Cache-Control", "max-age=86400")
}
<commit_msg>Added pause before starting database operations<commit_after>package app

import (
	"github.com/revel/revel"
	"github.com/revel/revel/modules/static/app/controllers"
	"time"
	"cruncher/app/models/riotapi"
	"cruncher/app/models/cron"
	"cruncher/app/models/database"
)

func init() {
	// Filters is the default set of global filters.
	revel.Filters = []revel.Filter{
		revel.PanicFilter,             // Recover from panics and display an error page instead.
		revel.RouterFilter,            // Use the routing table to select the right Action
		revel.FilterConfiguringFilter, // A hook for adding or removing per-Action filters.
		revel.ParamsFilter,            // Parse parameters into Controller.Params.
		revel.SessionFilter,           // Restore and write the session cookie.
		revel.FlashFilter,             // Restore and write the flash cookie.
		revel.ValidationFilter,        // Restore kept validation errors and save new ones from cookie.
		revel.I18nFilter,              // Resolve the requested language
		HeaderFilter,                  // Add some security based headers
		revel.InterceptorFilter,       // Run interceptors around the action.
		revel.CompressFilter,          // Compress the result.
		revel.ActionInvoker,           // Invoke the action.
	}

	// register startup functions with OnAppStart
	// ( order dependent )
	revel.OnAppStart(riotapi.LoadAPIKey)

	time.Sleep(time.Duration(20) * time.Second)
	revel.OnAppStart(database.Connect)
	revel.OnAppStart(cron.Start)

	revel.FilterController(controllers.Static{}).Add(staticCache)
}

// TODO turn this into revel.HeaderFilter
// should probably also have a filter for CSRF
// not sure if it can go in the same filter or not
var HeaderFilter = func(c *revel.Controller, fc []revel.Filter) {
	// Add some common security headers
	c.Response.Out.Header().Add("X-Frame-Options", "SAMEORIGIN")
	c.Response.Out.Header().Add("X-XSS-Protection", "1; mode=block")
	c.Response.Out.Header().Add("X-Content-Type-Options", "nosniff")

	fc[0](c, fc[1:]) // Execute the next filter stage.
}

func staticCache(c *revel.Controller, fc []revel.Filter) {
	fc[0](c, fc[1:])
	c.Response.Out.Header().Set("Cache-Control", "max-age=86400")
}
<|endoftext|>


<commit_before>// Copyright 2015 Matthew Collins
//
// 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 render

import (
	"math"

	"github.com/thinkofdeath/steven/native"
	"github.com/thinkofdeath/steven/render/gl"
	"github.com/thinkofdeath/steven/type/direction"
)

var (
	buffers           = make(map[position]*ChunkBuffer)
	elementBuffer     gl.Buffer
	elementBufferSize int
	elementBufferType gl.Type = gl.UnsignedShort
)

// ChunkBuffer is a renderable chunk section
type ChunkBuffer struct {
	position
	invalid bool

	array       gl.VertexArray
	buffer      gl.Buffer
	bufferSize  int
	count       int
	arrayT      gl.VertexArray
	bufferT     gl.Buffer
	bufferTSize int
	countT      int
	cullBits    uint64

	renderedOn uint

	neighborChunks [6]*ChunkBuffer
}

// IsVisible returns whether the 'to' face is visible through
// 'from' face.
func (cb *ChunkBuffer) IsVisible(from, to direction.Type) bool {
	return (cb.cullBits & (1 << (from*6 + to))) != 0
}

// AllocateColumn ensures the column's buffers are allocated.
func AllocateColumn(x, z int) {
	for i := 0; i < 16; i++ {
		if _, ok := buffers[position{x, i, z}]; !ok {
			buffers[position{x, i, z}] = &ChunkBuffer{
				position: position{X: x, Y: i, Z: z},
				cullBits: math.MaxUint64,
				invalid:  true,
			}
		}
	}
	// Update neighbors
	for i := 0; i < 16; i++ {
		c := buffers[position{x, i, z}]
		for _, d := range direction.Values {
			ox, oy, oz := d.Offset()
			o := buffers[position{x + ox, i + oy, z + oz}]
			if o != nil {
				c.neighborChunks[d] = o
				o.neighborChunks[d.Opposite()] = c
			}
		}
	}
}

// FreeColumn deallocates the column's buffers.
func FreeColumn(x, z int) {
	for i := 0; i < 16; i++ {
		// Update neighbors
		c := buffers[position{x, i, z}]
		for _, d := range direction.Values {
			ox, oy, oz := d.Offset()
			o := buffers[position{x + ox, i + oy, z + oz}]
			if o != nil {
				c.neighborChunks[d] = nil
				o.neighborChunks[d.Opposite()] = nil
			}
		}
		delete(buffers, position{x, i, z})
	}
}

// AllocateChunkBuffer allocates a chunk buffer and adds it to the
// render list.
func AllocateChunkBuffer(x, y, z int) *ChunkBuffer {
	c := buffers[position{x, y, z}]
	c.invalid = false
	return c
}

func ensureElementBuffer(size int) {
	if elementBufferSize < size {
		data, ty := genElementBuffer(size)
		elementBufferType = ty
		elementBuffer.Bind(gl.ElementArrayBuffer)
		elementBuffer.Data(data, gl.DynamicDraw)
		elementBufferSize = size
	}
}

func genElementBuffer(size int) ([]byte, gl.Type) {
	data := make([]byte, size*4)
	offset := 0
	ty := gl.UnsignedShort
	if uint32(size/6)*4+3 >= math.MaxUint16 {
		ty = gl.UnsignedInt
	}
	for i := 0; i < size/6; i++ {
		for _, val := range []uint32{0, 1, 2, 2, 1, 3} {
			if ty == gl.UnsignedInt {
				native.Order.PutUint32(data[offset:], uint32(i)*4+val)
				offset += 4
			} else {
				native.Order.PutUint16(data[offset:], uint16(uint32(i)*4+val))
				offset += 2
			}
		}
	}
	return data, ty
}

// Upload uploads the passed vertex data to the buffer.
func (cb *ChunkBuffer) Upload(data []byte, indices int, cullBits uint64) {
	if cb.invalid {
		return
	}
	cb.cullBits = cullBits
	var n bool

	if indices == 0 {
		if cb.array.IsValid() {
			cb.array.Delete()
			cb.buffer.Delete()
		}
		return
	}

	if !cb.array.IsValid() {
		cb.array = gl.CreateVertexArray()
		cb.buffer = gl.CreateBuffer()
		n = true
	}

	cb.array.Bind()
	shaderChunk.Position.Enable()
	shaderChunk.TextureInfo.Enable()
	shaderChunk.TextureOffset.Enable()
	shaderChunk.Color.Enable()
	shaderChunk.Lighting.Enable()

	ensureElementBuffer(indices)
	elementBuffer.Bind(gl.ElementArrayBuffer)

	cb.buffer.Bind(gl.ArrayBuffer)
	if n || len(data) > cb.bufferSize {
		cb.bufferSize = len(data)
		cb.buffer.Data(data, gl.DynamicDraw)
	} else {
		target := cb.buffer.Map(gl.WriteOnly, len(data))
		copy(target, data)
		cb.buffer.Unmap()
	}
	shaderChunk.Position.Pointer(3, gl.Float, false, 40, 0)
	shaderChunk.TextureInfo.Pointer(4, gl.UnsignedShort, false, 40, 12)
	shaderChunk.TextureOffset.Pointer(3, gl.Short, false, 40, 20)
	shaderChunk.Color.Pointer(3, gl.UnsignedByte, true, 40, 28)
	shaderChunk.Lighting.Pointer(2, gl.UnsignedShort, false, 40, 32)

	cb.count = indices
}

// UploadTrans uploads the passed vertex data to the translucent buffer.
func (cb *ChunkBuffer) UploadTrans(data []byte, indices int) {
	if cb.invalid {
		return
	}
	var n bool
	if indices == 0 {
		if cb.arrayT.IsValid() {
			cb.arrayT.Delete()
			cb.bufferT.Delete()
		}
		return
	}

	if !cb.arrayT.IsValid() {
		cb.arrayT = gl.CreateVertexArray()
		cb.bufferT = gl.CreateBuffer()
		n = true
	}

	cb.arrayT.Bind()
	shaderChunkT.Position.Enable()
	shaderChunkT.TextureInfo.Enable()
	shaderChunkT.TextureOffset.Enable()
	shaderChunkT.Color.Enable()
	shaderChunkT.Lighting.Enable()

	ensureElementBuffer(indices)
	elementBuffer.Bind(gl.ElementArrayBuffer)

	cb.bufferT.Bind(gl.ArrayBuffer)
	if n || len(data) > cb.bufferTSize {
		cb.bufferTSize = len(data)
		cb.bufferT.Data(data, gl.DynamicDraw)
	} else {
		target := cb.bufferT.Map(gl.WriteOnly, len(data))
		copy(target, data)
		cb.bufferT.Unmap()
	}
	shaderChunkT.Position.Pointer(3, gl.Float, false, 40, 0)
	shaderChunkT.TextureInfo.Pointer(4, gl.UnsignedShort, false, 40, 12)
	shaderChunkT.TextureOffset.Pointer(3, gl.Short, false, 40, 20)
	shaderChunkT.Color.Pointer(3, gl.UnsignedByte, true, 40, 28)
	shaderChunkT.Lighting.Pointer(2, gl.UnsignedShort, false, 40, 32)

	cb.countT = indices
}

// Free removes the buffer and frees related resources.
func (cb *ChunkBuffer) Free() {
	if cb.invalid {
		return
	}
	// Clear state
	cb.invalid = true
	cb.count = 0
	cb.countT = 0
	cb.cullBits = math.MaxUint64

	if cb.buffer.IsValid() {
		cb.buffer.Delete()
	}
	if cb.array.IsValid() {
		cb.array.Delete()
	}
	if cb.bufferT.IsValid() {
		cb.bufferT.Delete()
	}
	if cb.arrayT.IsValid() {
		cb.arrayT.Delete()
	}
}
<commit_msg>render: cut down on resizes for the element buffer<commit_after>// Copyright 2015 Matthew Collins
//
// 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 render

import (
	"math"

	"github.com/thinkofdeath/steven/native"
	"github.com/thinkofdeath/steven/render/gl"
	"github.com/thinkofdeath/steven/type/direction"
)

var (
	buffers           = make(map[position]*ChunkBuffer)
	elementBuffer     gl.Buffer
	elementBufferSize int
	elementBufferType gl.Type = gl.UnsignedShort
)

// ChunkBuffer is a renderable chunk section
type ChunkBuffer struct {
	position
	invalid bool

	array       gl.VertexArray
	buffer      gl.Buffer
	bufferSize  int
	count       int
	arrayT      gl.VertexArray
	bufferT     gl.Buffer
	bufferTSize int
	countT      int
	cullBits    uint64

	renderedOn uint

	neighborChunks [6]*ChunkBuffer
}

// IsVisible returns whether the 'to' face is visible through
// 'from' face.
func (cb *ChunkBuffer) IsVisible(from, to direction.Type) bool {
	return (cb.cullBits & (1 << (from*6 + to))) != 0
}

// AllocateColumn ensures the column's buffers are allocated.
func AllocateColumn(x, z int) {
	for i := 0; i < 16; i++ {
		if _, ok := buffers[position{x, i, z}]; !ok {
			buffers[position{x, i, z}] = &ChunkBuffer{
				position: position{X: x, Y: i, Z: z},
				cullBits: math.MaxUint64,
				invalid:  true,
			}
		}
	}
	// Update neighbors
	for i := 0; i < 16; i++ {
		c := buffers[position{x, i, z}]
		for _, d := range direction.Values {
			ox, oy, oz := d.Offset()
			o := buffers[position{x + ox, i + oy, z + oz}]
			if o != nil {
				c.neighborChunks[d] = o
				o.neighborChunks[d.Opposite()] = c
			}
		}
	}
}

// FreeColumn deallocates the column's buffers.
func FreeColumn(x, z int) {
	for i := 0; i < 16; i++ {
		// Update neighbors
		c := buffers[position{x, i, z}]
		for _, d := range direction.Values {
			ox, oy, oz := d.Offset()
			o := buffers[position{x + ox, i + oy, z + oz}]
			if o != nil {
				c.neighborChunks[d] = nil
				o.neighborChunks[d.Opposite()] = nil
			}
		}
		delete(buffers, position{x, i, z})
	}
}

// AllocateChunkBuffer allocates a chunk buffer and adds it to the
// render list.
func AllocateChunkBuffer(x, y, z int) *ChunkBuffer {
	c := buffers[position{x, y, z}]
	c.invalid = false
	return c
}

func ensureElementBuffer(size int) {
	if elementBufferSize < size {
		size = nearestPow2(size)
		data, ty := genElementBuffer(size)
		elementBufferType = ty
		elementBuffer.Bind(gl.ElementArrayBuffer)
		elementBuffer.Data(data, gl.DynamicDraw)
		elementBufferSize = size
	}
}

func nearestPow2(v int) int {
	v--
	v |= v >> 1
	v |= v >> 2
	v |= v >> 4
	v |= v >> 8
	v |= v >> 16
	v++
	return v
}

func genElementBuffer(size int) ([]byte, gl.Type) {
	data := make([]byte, size*4)
	offset := 0
	ty := gl.UnsignedShort
	if uint32(size/6)*4+3 >= math.MaxUint16 {
		ty = gl.UnsignedInt
	}
	for i := 0; i < size/6; i++ {
		for _, val := range []uint32{0, 1, 2, 2, 1, 3} {
			if ty == gl.UnsignedInt {
				native.Order.PutUint32(data[offset:], uint32(i)*4+val)
				offset += 4
			} else {
				native.Order.PutUint16(data[offset:], uint16(uint32(i)*4+val))
				offset += 2
			}
		}
	}
	return data, ty
}

// Upload uploads the passed vertex data to the buffer.
func (cb *ChunkBuffer) Upload(data []byte, indices int, cullBits uint64) {
	if cb.invalid {
		return
	}
	cb.cullBits = cullBits
	var n bool

	if indices == 0 {
		if cb.array.IsValid() {
			cb.array.Delete()
			cb.buffer.Delete()
		}
		return
	}

	if !cb.array.IsValid() {
		cb.array = gl.CreateVertexArray()
		cb.buffer = gl.CreateBuffer()
		n = true
	}

	cb.array.Bind()
	shaderChunk.Position.Enable()
	shaderChunk.TextureInfo.Enable()
	shaderChunk.TextureOffset.Enable()
	shaderChunk.Color.Enable()
	shaderChunk.Lighting.Enable()

	ensureElementBuffer(indices)
	elementBuffer.Bind(gl.ElementArrayBuffer)

	cb.buffer.Bind(gl.ArrayBuffer)
	if n || len(data) > cb.bufferSize {
		cb.bufferSize = len(data)
		cb.buffer.Data(data, gl.DynamicDraw)
	} else {
		target := cb.buffer.Map(gl.WriteOnly, len(data))
		copy(target, data)
		cb.buffer.Unmap()
	}
	shaderChunk.Position.Pointer(3, gl.Float, false, 40, 0)
	shaderChunk.TextureInfo.Pointer(4, gl.UnsignedShort, false, 40, 12)
	shaderChunk.TextureOffset.Pointer(3, gl.Short, false, 40, 20)
	shaderChunk.Color.Pointer(3, gl.UnsignedByte, true, 40, 28)
	shaderChunk.Lighting.Pointer(2, gl.UnsignedShort, false, 40, 32)

	cb.count = indices
}

// UploadTrans uploads the passed vertex data to the translucent buffer.
func (cb *ChunkBuffer) UploadTrans(data []byte, indices int) {
	if cb.invalid {
		return
	}
	var n bool
	if indices == 0 {
		if cb.arrayT.IsValid() {
			cb.arrayT.Delete()
			cb.bufferT.Delete()
		}
		return
	}

	if !cb.arrayT.IsValid() {
		cb.arrayT = gl.CreateVertexArray()
		cb.bufferT = gl.CreateBuffer()
		n = true
	}

	cb.arrayT.Bind()
	shaderChunkT.Position.Enable()
	shaderChunkT.TextureInfo.Enable()
	shaderChunkT.TextureOffset.Enable()
	shaderChunkT.Color.Enable()
	shaderChunkT.Lighting.Enable()

	ensureElementBuffer(indices)
	elementBuffer.Bind(gl.ElementArrayBuffer)

	cb.bufferT.Bind(gl.ArrayBuffer)
	if n || len(data) > cb.bufferTSize {
		cb.bufferTSize = len(data)
		cb.bufferT.Data(data, gl.DynamicDraw)
	} else {
		target := cb.bufferT.Map(gl.WriteOnly, len(data))
		copy(target, data)
		cb.bufferT.Unmap()
	}
	shaderChunkT.Position.Pointer(3, gl.Float, false, 40, 0)
	shaderChunkT.TextureInfo.Pointer(4, gl.UnsignedShort, false, 40, 12)
	shaderChunkT.TextureOffset.Pointer(3, gl.Short, false, 40, 20)
	shaderChunkT.Color.Pointer(3, gl.UnsignedByte, true, 40, 28)
	shaderChunkT.Lighting.Pointer(2, gl.UnsignedShort, false, 40, 32)

	cb.countT = indices
}

// Free removes the buffer and frees related resources.
func (cb *ChunkBuffer) Free() {
	if cb.invalid {
		return
	}
	// Clear state
	cb.invalid = true
	cb.count = 0
	cb.countT = 0
	cb.cullBits = math.MaxUint64

	if cb.buffer.IsValid() {
		cb.buffer.Delete()
	}
	if cb.array.IsValid() {
		cb.array.Delete()
	}
	if cb.bufferT.IsValid() {
		cb.bufferT.Delete()
	}
	if cb.arrayT.IsValid() {
		cb.arrayT.Delete()
	}
}
<|endoftext|>


<commit_before>package monitrondashboard

// Basic dashboard functionality

import (
	"errors"
	"fmt"
	"github.com/nsf/termbox-go"
)

const OrangeColour int = 167

const textPadding int = 1

const buildingMessage string = "Building"

// rect is a simple struct giving a bounding rectangle for a widget
type rect struct {
	point
	size
}

func (r rect) String() string {
	return fmt.Sprintf("<Rect x:%d,y:%d | w:%d,h:%d>", r.x, r.y,
		r.w, r.h)
}

func NewRect(x, y, w, h int) rect {
	return rect{
		point{x, y},
		size{w, h},
	}
}

// point defines a single point on the screen
type point struct {
	x int
	y int
}

// size defines a width and height an object takes up
type size struct {
	w int
	h int
}

// buildState is an int type defining the states a build can be in.
type buildState int

const (
	BuildStateFailed buildState = iota
	BuildStateAcknowledged
	BuildStatePassed
	BuildStateUnknown
)

// BgColour returns the termbox Attribute for the background colour
// of a build in this state.
func (bs buildState) BgColour() termbox.Attribute {
	switch bs {
	case BuildStateFailed:
		return termbox.ColorRed
	case BuildStateAcknowledged:
		return termbox.Attribute(OrangeColour)
	case BuildStatePassed:
		return termbox.ColorGreen
	case BuildStateUnknown:
		return termbox.ColorMagenta
	}
	return termbox.ColorCyan
}

// BgColour returns the termbox Attribute for the text colour
// of a build in this state.
func (bs buildState) FgColour() termbox.Attribute {
	switch bs {
	case BuildStateFailed:
		return termbox.ColorWhite
	case BuildStateAcknowledged:
		return termbox.ColorWhite
	case BuildStatePassed:
		return termbox.ColorWhite
	case BuildStateUnknown:
		return termbox.ColorWhite
	}
	return termbox.ColorWhite
}

// build is a struct covering a build and its current state.
type build struct {
	name         string
	buildState   buildState
	building     bool
	acknowledger string
}

// drawBuildState draws a status box for an individual build within bounds
func drawBuildState(build build, bounds rect) {
	bgColour := build.buildState.BgColour()
	fgColour := build.buildState.FgColour()

	availableWidth := bounds.size.w - 2*textPadding
	buildNameWithLengthRestriction, _ := elipsize(build.name, availableWidth)

	nameWriter := createTextWriter(buildNameWithLengthRestriction, point{1, 0})

	var buildingWriter func(char rune, point point) rune
	var usernameWriter func(char rune, point point) rune
	if build.building {
		buildingWriter = createTextWriter(buildingMessage, point{1, 1})
		usernameWriter = createTextWriter(build.acknowledger, point{10, 1})
	} else {
		buildingWriter = func(char rune, point point) rune { return char }
		usernameWriter = createTextWriter(build.acknowledger, point{1, 1})
	}

	for x := 0; x < bounds.w; x++ {
		for y := 0; y < bounds.h; y++ {
			char := ' '
			currentPoint := point{x, y}
			char = nameWriter(char, currentPoint)
			char = buildingWriter(char, currentPoint)
			char = usernameWriter(char, currentPoint)

			termbox.SetCell(x+bounds.x, y+bounds.y, char, fgColour, bgColour)
		}
	}
}

// createTextPrinter takes a string and a starting point and returns a function,
// the returned function takes the current char to be displayed and a point and
// will return the the character this printer thinks should be displayed at this point.
// Note these functions take the existing char to enable multiple functions to be
// chained together
func createTextWriter(text string, startingPoint point) func(char rune, point point) rune {
	runeText := []rune(text)
	expectedRow := startingPoint.y
	columnOffset := startingPoint.x
	return func(char rune, point point) rune {
		if point.y == expectedRow && point.x >= columnOffset {
			charIndex := point.x - columnOffset
			if charIndex < len(runeText) {
				return runeText[charIndex]
			}
		}
		return char
	}
}

// ellipsize returns a string restricted to maxLength using an ellipsis
func elipsize(s string, maxLength int) (string, error) {
	if maxLength < 3 {
		return "", errors.New("Max length too short to ellipsize.")
	}
	runes := []rune(s)
	if len(runes) > maxLength {
		return fmt.Sprintf("%s...", string(runes[0:maxLength-3])), nil
	}
	return s, nil
}

type Layout struct {
	boxes []rect
}

func layoutGridForScreen(minimumBoxSize size, numberOfBoxes int, padding int, screenSize size) (Layout, error) {
	maximumNumberOfVerticalBoxes := (screenSize.h - padding) / (minimumBoxSize.h + padding)
	// integer division that always rounds up
	requiredNumberOfColumns := (numberOfBoxes + maximumNumberOfVerticalBoxes - 1) / maximumNumberOfVerticalBoxes
	columnWidth := (screenSize.w - padding) / requiredNumberOfColumns
	boxWidth := columnWidth - padding
	if boxWidth < minimumBoxSize.w {
		return Layout{}, errors.New("Screen is too small to fit the grid")
	}

	boxes := []rect{}
	i := 0
gridloop:
	for x := 0; x < requiredNumberOfColumns; x++ {
		for y := 0; y < maximumNumberOfVerticalBoxes; y++ {
			boxes = append(boxes, rect{
				point{
					padding + x*columnWidth,
					padding + y*(minimumBoxSize.h+padding),
				},
				size{boxWidth, minimumBoxSize.h},
			})
			i = i + 1
			if i == numberOfBoxes {
				break gridloop
			}
		}
	}

	return Layout{
		boxes: boxes,
	}, nil
}

// redraw redraws the screen.
func redraw() error {
	screenWidth, screenHeight := termbox.Size()
	layout, err := layoutGridForScreen(size{30, 3}, 10, 2, size{screenWidth, screenHeight})
	if err != nil {
		return err
	}

	for i := 0; i < 10; i++ {
		box := layout.boxes[i]
		drawBuildState(build{"test", BuildStateFailed, false, ""}, box)
	}

	termbox.Flush()
	return nil
}

func Run() {
	err := termbox.Init()
	if err != nil {
		panic(err)
	}
	defer termbox.Close()
	termbox.SetInputMode(termbox.InputEsc)
	termbox.SetOutputMode(termbox.Output256)
	if err := redraw(); err != nil {
		fmt.Println("Error: %s", err)
		return
	}

mainloop:
	for {
		switch ev := termbox.PollEvent(); ev.Type {
		case termbox.EventKey:
			switch ev.Key {
			case termbox.KeyEsc:
				break mainloop
			default:
				if ev.Ch == 'q' {
					break mainloop
				}
			}
		case termbox.EventError:
			fmt.Printf("Error: %s\n", ev.Err)
			break mainloop
		}
		if err := redraw(); err != nil {
			fmt.Println("Error: %s", err)
			return
		}
	}
}
<commit_msg>Handle terminal window resize events.<commit_after>package monitrondashboard

// Basic dashboard functionality

import (
	"errors"
	"fmt"
	"github.com/nsf/termbox-go"
)

const OrangeColour int = 167

const textPadding int = 1

const buildingMessage string = "Building"

// rect is a simple struct giving a bounding rectangle for a widget
type rect struct {
	point
	size
}

func (r rect) String() string {
	return fmt.Sprintf("<Rect x:%d,y:%d | w:%d,h:%d>", r.x, r.y,
		r.w, r.h)
}

func NewRect(x, y, w, h int) rect {
	return rect{
		point{x, y},
		size{w, h},
	}
}

// point defines a single point on the screen
type point struct {
	x int
	y int
}

// size defines a width and height an object takes up
type size struct {
	w int
	h int
}

// buildState is an int type defining the states a build can be in.
type buildState int

const (
	BuildStateFailed buildState = iota
	BuildStateAcknowledged
	BuildStatePassed
	BuildStateUnknown
)

// BgColour returns the termbox Attribute for the background colour
// of a build in this state.
func (bs buildState) BgColour() termbox.Attribute {
	switch bs {
	case BuildStateFailed:
		return termbox.ColorRed
	case BuildStateAcknowledged:
		return termbox.Attribute(OrangeColour)
	case BuildStatePassed:
		return termbox.ColorGreen
	case BuildStateUnknown:
		return termbox.ColorMagenta
	}
	return termbox.ColorCyan
}

// BgColour returns the termbox Attribute for the text colour
// of a build in this state.
func (bs buildState) FgColour() termbox.Attribute {
	switch bs {
	case BuildStateFailed:
		return termbox.ColorWhite
	case BuildStateAcknowledged:
		return termbox.ColorWhite
	case BuildStatePassed:
		return termbox.ColorWhite
	case BuildStateUnknown:
		return termbox.ColorWhite
	}
	return termbox.ColorWhite
}

// build is a struct covering a build and its current state.
type build struct {
	name         string
	buildState   buildState
	building     bool
	acknowledger string
}

// drawBuildState draws a status box for an individual build within bounds
func drawBuildState(build build, bounds rect) {
	bgColour := build.buildState.BgColour()
	fgColour := build.buildState.FgColour()

	availableWidth := bounds.size.w - 2*textPadding
	buildNameWithLengthRestriction, _ := elipsize(build.name, availableWidth)

	nameWriter := createTextWriter(buildNameWithLengthRestriction, point{1, 0})

	var buildingWriter func(char rune, point point) rune
	var usernameWriter func(char rune, point point) rune
	if build.building {
		buildingWriter = createTextWriter(buildingMessage, point{1, 1})
		usernameWriter = createTextWriter(build.acknowledger, point{10, 1})
	} else {
		buildingWriter = func(char rune, point point) rune { return char }
		usernameWriter = createTextWriter(build.acknowledger, point{1, 1})
	}

	for x := 0; x < bounds.w; x++ {
		for y := 0; y < bounds.h; y++ {
			char := ' '
			currentPoint := point{x, y}
			char = nameWriter(char, currentPoint)
			char = buildingWriter(char, currentPoint)
			char = usernameWriter(char, currentPoint)

			termbox.SetCell(x+bounds.x, y+bounds.y, char, fgColour, bgColour)
		}
	}
}

// createTextPrinter takes a string and a starting point and returns a function,
// the returned function takes the current char to be displayed and a point and
// will return the the character this printer thinks should be displayed at this point.
// Note these functions take the existing char to enable multiple functions to be
// chained together
func createTextWriter(text string, startingPoint point) func(char rune, point point) rune {
	runeText := []rune(text)
	expectedRow := startingPoint.y
	columnOffset := startingPoint.x
	return func(char rune, point point) rune {
		if point.y == expectedRow && point.x >= columnOffset {
			charIndex := point.x - columnOffset
			if charIndex < len(runeText) {
				return runeText[charIndex]
			}
		}
		return char
	}
}

// ellipsize returns a string restricted to maxLength using an ellipsis
func elipsize(s string, maxLength int) (string, error) {
	if maxLength < 3 {
		return "", errors.New("Max length too short to ellipsize.")
	}
	runes := []rune(s)
	if len(runes) > maxLength {
		return fmt.Sprintf("%s...", string(runes[0:maxLength-3])), nil
	}
	return s, nil
}

type Layout struct {
	boxes []rect
}

func layoutGridForScreen(minimumBoxSize size, numberOfBoxes int, padding int, screenSize size) (Layout, error) {
	maximumNumberOfVerticalBoxes := (screenSize.h - padding) / (minimumBoxSize.h + padding)
	// integer division that always rounds up
	requiredNumberOfColumns := (numberOfBoxes + maximumNumberOfVerticalBoxes - 1) / maximumNumberOfVerticalBoxes
	columnWidth := (screenSize.w - padding) / requiredNumberOfColumns
	boxWidth := columnWidth - padding
	if boxWidth < minimumBoxSize.w {
		return Layout{}, errors.New("Screen is too small to fit the grid")
	}

	boxes := []rect{}
	i := 0
gridloop:
	for x := 0; x < requiredNumberOfColumns; x++ {
		for y := 0; y < maximumNumberOfVerticalBoxes; y++ {
			boxes = append(boxes, rect{
				point{
					padding + x*columnWidth,
					padding + y*(minimumBoxSize.h+padding),
				},
				size{boxWidth, minimumBoxSize.h},
			})
			i = i + 1
			if i == numberOfBoxes {
				break gridloop
			}
		}
	}

	return Layout{
		boxes: boxes,
	}, nil
}

// redraw redraws the screen.
func redraw() error {
	screenWidth, screenHeight := termbox.Size()
	layout, err := layoutGridForScreen(size{30, 3}, 15, 1, size{screenWidth, screenHeight})
	if err != nil {
		return err
	}

	for i := 0; i < 15; i++ {
		box := layout.boxes[i]
		drawBuildState(build{"test", BuildStateFailed, false, ""}, box)
	}

	termbox.Flush()
	return nil
}

func Run() {
	err := termbox.Init()
	if err != nil {
		panic(err)
	}
	defer termbox.Close()
	termbox.SetInputMode(termbox.InputEsc)
	termbox.SetOutputMode(termbox.Output256)
	if err := redraw(); err != nil {
		fmt.Println("Error: %s", err)
		return
	}

mainloop:
	for {
		switch ev := termbox.PollEvent(); ev.Type {
		case termbox.EventKey:
			switch ev.Key {
			case termbox.KeyEsc:
				break mainloop
			default:
				if ev.Ch == 'q' {
					break mainloop
				}
			}
		case termbox.EventError:
			fmt.Printf("Error: %s\n", ev.Err)
			break mainloop
		case termbox.EventResize:
			termbox.Clear(0, 0)
			if err := redraw(); err != nil {
				fmt.Println("Error: %s", err)
				return
			}
		}
		if err := redraw(); err != nil {
			fmt.Println("Error: %s", err)
			return
		}
	}
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
	"os"
	"time"

	"github.com/adam000/Go-SDL2/image"
	"github.com/adam000/Go-SDL2/sdl"
)

var fullscreen = flag.Bool("fullscreen", false, "fullscreen window")

func main() {
	flag.Parse()
	if flag.NArg() == 0 {
		flag.Usage()
		sdl.Quit()
		os.Exit(2)
	}
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run() error {
	defer sdl.Quit()

	surfaces, err := loadImages(flag.Args())
	defer func() {
		for _, s := range surfaces {
			s.Free()
		}
	}()
	if err != nil {
		return err
	}

	_, renderer, err := openWindow(flag.Arg(0), maxSize(surfaces))
	if err != nil {
		return err
	}

	textures, err := convertToTextures(renderer, surfaces)
	defer func() {
		for _, t := range textures {
			t.Destroy()
		}
	}()
	if err != nil {
		return err
	}

	mainLoop(renderer, textures)
	return nil
}

func mainLoop(renderer sdl.Renderer, textures []sdl.Texture) {
	currTex := 0
	for {
		// Poll for events
		for {
			ev := sdl.PollEvent()
			if ev == nil {
				break
			}
			switch ev.Type() {
			case sdl.QuitEventType:
				fmt.Println("QUIT")
				return
			case sdl.KeyDownEventType:
				fmt.Println("KEY")
				// TODO(light): advance texture
			}
		}

		// Display image
		renderer.Clear()
		renderer.CopyTexture(textures[currTex], nil, nil)
		renderer.Present()

		// Wait a bit
		time.Sleep(100 * time.Millisecond)
	}
}

func openWindow(title string, size sdl.Point) (sdl.Window, sdl.Renderer, error) {
	var windowFlags sdl.WindowFlag
	if *fullscreen {
		windowFlags |= sdl.WindowFullscreen
	}
	window, err := sdl.NewWindow(title, 0, 0, size.X, size.Y, windowFlags)
	if err != nil {
		return window, sdl.Renderer{}, err
	}
	renderer, err := sdl.NewRenderer(window, -1, 0)
	return window, renderer, err
}

func loadImages(names []string) ([]sdl.Surface, error) {
	surfaces := make([]sdl.Surface, 0, len(names))
	for _, name := range names {
		s, err := image.Load(name)
		if err != nil {
			return surfaces, &os.PathError{Op: "open", Path: name, Err: err}
		}
		surfaces = append(surfaces, s)
	}
	return surfaces, nil
}

func convertToTextures(renderer sdl.Renderer, surfaces []sdl.Surface) ([]sdl.Texture, error) {
	textures := make([]sdl.Texture, 0, len(surfaces))
	for _, s := range surfaces {
		t, err := s.ToTexture(renderer)
		if err != nil {
			return textures, err
		}
		textures = append(textures, t)
	}
	return textures, nil
}

func maxSize(s []sdl.Surface) sdl.Point {
	var size sdl.Point
	for _, ss := range s {
		z := ss.Size()
		if z.X > size.X {
			size.X = z.X
		}
		if z.Y > size.Y {
			size.Y = z.Y
		}
	}
	return size
}
<commit_msg>Update showimage demo to use new events API<commit_after>package main

import (
	"flag"
	"fmt"
	"os"
	"time"

	"github.com/adam000/Go-SDL2/image"
	"github.com/adam000/Go-SDL2/sdl"
	"github.com/adam000/Go-SDL2/sdl/keys"
)

var fullscreen = flag.Bool("fullscreen", false, "fullscreen window")

func main() {
	flag.Parse()
	if flag.NArg() == 0 {
		flag.Usage()
		sdl.Quit()
		os.Exit(2)
	}
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run() error {
	defer sdl.Quit()

	surfaces, err := loadImages(flag.Args())
	defer func() {
		for _, s := range surfaces {
			s.Free()
		}
	}()
	if err != nil {
		return err
	}

	_, renderer, err := openWindow(flag.Arg(0), maxSize(surfaces))
	if err != nil {
		return err
	}

	textures, err := convertToTextures(renderer, surfaces)
	defer func() {
		for _, t := range textures {
			t.Destroy()
		}
	}()
	if err != nil {
		return err
	}

	mainLoop(renderer, textures)
	return nil
}

func mainLoop(renderer sdl.Renderer, textures []sdl.Texture) {
	currTex := 0
	for {
		// Poll for events
		for sdl.HasEvent() {
			switch ev := sdl.PollEvent(); ev.Type() {
			case sdl.QuitEventType:
				fmt.Println("QUIT")
				return
			case sdl.KeyDownEventType:
				ev := ev.(*sdl.KeyboardEvent)
				fmt.Println("KEY", ev.Code)
				switch ev.Code {
				case keys.Escape:
					return
				case keys.Left:
					if currTex > 0 {
						currTex--
					} else {
						currTex = len(textures) - 1
					}
				case keys.Right:
					if currTex < len(textures)-1 {
						currTex++
					} else {
						currTex = 0
					}
				}
			}
		}

		// Display image
		renderer.Clear()
		renderer.CopyTexture(textures[currTex], nil, nil)
		renderer.Present()

		// Wait a bit
		time.Sleep(100 * time.Millisecond)
	}
}

func openWindow(title string, size sdl.Point) (sdl.Window, sdl.Renderer, error) {
	var windowFlags sdl.WindowFlag
	if *fullscreen {
		windowFlags |= sdl.WindowFullscreen
	}
	window, err := sdl.NewWindow(title, 0, 0, size.X, size.Y, windowFlags)
	if err != nil {
		return window, sdl.Renderer{}, err
	}
	renderer, err := sdl.NewRenderer(window, -1, 0)
	return window, renderer, err
}

func loadImages(names []string) ([]sdl.Surface, error) {
	surfaces := make([]sdl.Surface, 0, len(names))
	for _, name := range names {
		s, err := image.Load(name)
		if err != nil {
			return surfaces, &os.PathError{Op: "open", Path: name, Err: err}
		}
		surfaces = append(surfaces, s)
	}
	return surfaces, nil
}

func convertToTextures(renderer sdl.Renderer, surfaces []sdl.Surface) ([]sdl.Texture, error) {
	textures := make([]sdl.Texture, 0, len(surfaces))
	for _, s := range surfaces {
		t, err := s.ToTexture(renderer)
		if err != nil {
			return textures, err
		}
		textures = append(textures, t)
	}
	return textures, nil
}

func maxSize(s []sdl.Surface) sdl.Point {
	var size sdl.Point
	for _, ss := range s {
		z := ss.Size()
		if z.X > size.X {
			size.X = z.X
		}
		if z.Y > size.Y {
			size.Y = z.Y
		}
	}
	return size
}
<|endoftext|>


<commit_before>//
// Copyright 2017, Sander van Harmelen
//
// 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 gitlab

import (
	"bytes"
	"fmt"
	"net/url"
)

// RepositoriesService handles communication with the repositories related
// methods of the GitLab API.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html
type RepositoriesService struct {
	client *Client
}

// TreeNode represents a GitLab repository file or directory.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html
type TreeNode struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	Type string `json:"type"`
	Path string `json:"path"`
	Mode string `json:"mode"`
}

func (t TreeNode) String() string {
	return Stringify(t)
}

// ListTreeOptions represents the available ListTree() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#list-repository-tree
type ListTreeOptions struct {
	ListOptions
	Path      *string `url:"path,omitempty" json:"path,omitempty"`
	Ref       *string `url:"ref,omitempty" json:"ref,omitempty"`
	Recursive *bool   `url:"recursive,omitempty" json:"recursive,omitempty"`
}

// ListTree gets a list of repository files and directories in a project.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#list-repository-tree
func (s *RepositoriesService) ListTree(pid interface{}, opt *ListTreeOptions, options ...OptionFunc) ([]*TreeNode, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/repository/tree", url.QueryEscape(project))

	req, err := s.client.NewRequest("GET", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	var t []*TreeNode
	resp, err := s.client.Do(req, &t)
	if err != nil {
		return nil, resp, err
	}

	return t, resp, err
}

// Blob gets information about blob in repository like size and content. Note
// that blob content is Base64 encoded.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#get-a-blob-from-repository
func (s *RepositoriesService) Blob(pid interface{}, sha string, options ...OptionFunc) ([]byte, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/repository/blobs/%s", url.QueryEscape(project), sha)

	req, err := s.client.NewRequest("GET", u, nil, options)
	if err != nil {
		return nil, nil, err
	}

	var b bytes.Buffer
	resp, err := s.client.Do(req, &b)
	if err != nil {
		return nil, resp, err
	}

	return b.Bytes(), resp, err
}

// RawBlobContent gets the raw file contents for a blob by blob SHA.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#raw-blob-content
func (s *RepositoriesService) RawBlobContent(pid interface{}, sha string, options ...OptionFunc) ([]byte, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/repository/blobs/%s/raw", url.QueryEscape(project), sha)

	req, err := s.client.NewRequest("GET", u, nil, options)
	if err != nil {
		return nil, nil, err
	}

	var b bytes.Buffer
	resp, err := s.client.Do(req, &b)
	if err != nil {
		return nil, resp, err
	}

	return b.Bytes(), resp, err
}

// ArchiveOptions represents the available Archive() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#get-file-archive
type ArchiveOptions struct {
	SHA *string `url:"sha,omitempty" json:"sha,omitempty"`
}

// Archive gets an archive of the repository.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#get-file-archive
func (s *RepositoriesService) Archive(pid interface{}, opt *ArchiveOptions, options ...OptionFunc) ([]byte, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/repository/archive", url.QueryEscape(project))

	req, err := s.client.NewRequest("GET", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	var b bytes.Buffer
	resp, err := s.client.Do(req, &b)
	if err != nil {
		return nil, resp, err
	}

	return b.Bytes(), resp, err
}

// Compare represents the result of a comparison of branches, tags or commits.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits
type Compare struct {
	Commit         *Commit   `json:"commit"`
	Commits        []*Commit `json:"commits"`
	Diffs          []*Diff   `json:"diffs"`
	CompareTimeout bool      `json:"compare_timeout"`
	CompareSameRef bool      `json:"compare_same_ref"`
}

func (c Compare) String() string {
	return Stringify(c)
}

// CompareOptions represents the available Compare() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits
type CompareOptions struct {
	From     *string `url:"from,omitempty" json:"from,omitempty"`
	To       *string `url:"to,omitempty" json:"to,omitempty"`
	Straight *bool   `url:"straight,omitempty" json:"straight,omitempty"`
}

// Compare compares branches, tags or commits.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits
func (s *RepositoriesService) Compare(pid interface{}, opt *CompareOptions, options ...OptionFunc) (*Compare, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/repository/compare", url.QueryEscape(project))

	req, err := s.client.NewRequest("GET", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	c := new(Compare)
	resp, err := s.client.Do(req, c)
	if err != nil {
		return nil, resp, err
	}

	return c, resp, err
}

// Contributor represents a GitLap contributor.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html#contributors
type Contributor struct {
	Name      string `json:"name,omitempty"`
	Email     string `json:"email,omitempty"`
	Commits   int    `json:"commits,omitempty"`
	Additions int    `json:"additions,omitempty"`
	Deletions int    `json:"deletions,omitempty"`
}

func (c Contributor) String() string {
	return Stringify(c)
}

// ListContributorsOptions represents the available ListContributors() options.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html#contributors
type ListContributorsOptions ListOptions

// Contributors gets the repository contributors list.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html#contributors
func (s *RepositoriesService) Contributors(pid interface{}, opt *ListContributorsOptions, options ...OptionFunc) ([]*Contributor, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/repository/contributors", url.QueryEscape(project))

	req, err := s.client.NewRequest("GET", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	var c []*Contributor
	resp, err := s.client.Do(req, &c)
	if err != nil {
		return nil, resp, err
	}

	return c, resp, err
}

// MergeBaseOptions represents the available MergeBase() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#merge-base
type MergeBaseOptions struct {
	Ref []string `url:"refs[],omitempty" json:"refs,omitempty"`
}

// MergeBase gets the common ancestor for 2 refs (commit SHAs, branch
// names or tags).
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#merge-base
func (s *RepositoriesService) MergeBase(pid interface{}, opt *MergeBaseOptions, options ...OptionFunc) (*Commit, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/repository/merge_base", url.QueryEscape(project))

	req, err := s.client.NewRequest("GET", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	c := new(Commit)
	resp, err := s.client.Do(req, c)
	if err != nil {
		return nil, resp, err
	}

	return c, resp, err
}
<commit_msg>Add support for different archive formats<commit_after>//
// Copyright 2017, Sander van Harmelen
//
// 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 gitlab

import (
	"bytes"
	"fmt"
	"net/url"
)

// RepositoriesService handles communication with the repositories related
// methods of the GitLab API.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html
type RepositoriesService struct {
	client *Client
}

// TreeNode represents a GitLab repository file or directory.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html
type TreeNode struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	Type string `json:"type"`
	Path string `json:"path"`
	Mode string `json:"mode"`
}

func (t TreeNode) String() string {
	return Stringify(t)
}

// ListTreeOptions represents the available ListTree() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#list-repository-tree
type ListTreeOptions struct {
	ListOptions
	Path      *string `url:"path,omitempty" json:"path,omitempty"`
	Ref       *string `url:"ref,omitempty" json:"ref,omitempty"`
	Recursive *bool   `url:"recursive,omitempty" json:"recursive,omitempty"`
}

// ListTree gets a list of repository files and directories in a project.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#list-repository-tree
func (s *RepositoriesService) ListTree(pid interface{}, opt *ListTreeOptions, options ...OptionFunc) ([]*TreeNode, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/repository/tree", url.QueryEscape(project))

	req, err := s.client.NewRequest("GET", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	var t []*TreeNode
	resp, err := s.client.Do(req, &t)
	if err != nil {
		return nil, resp, err
	}

	return t, resp, err
}

// Blob gets information about blob in repository like size and content. Note
// that blob content is Base64 encoded.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#get-a-blob-from-repository
func (s *RepositoriesService) Blob(pid interface{}, sha string, options ...OptionFunc) ([]byte, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/repository/blobs/%s", url.QueryEscape(project), sha)

	req, err := s.client.NewRequest("GET", u, nil, options)
	if err != nil {
		return nil, nil, err
	}

	var b bytes.Buffer
	resp, err := s.client.Do(req, &b)
	if err != nil {
		return nil, resp, err
	}

	return b.Bytes(), resp, err
}

// RawBlobContent gets the raw file contents for a blob by blob SHA.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#raw-blob-content
func (s *RepositoriesService) RawBlobContent(pid interface{}, sha string, options ...OptionFunc) ([]byte, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/repository/blobs/%s/raw", url.QueryEscape(project), sha)

	req, err := s.client.NewRequest("GET", u, nil, options)
	if err != nil {
		return nil, nil, err
	}

	var b bytes.Buffer
	resp, err := s.client.Do(req, &b)
	if err != nil {
		return nil, resp, err
	}

	return b.Bytes(), resp, err
}

// ArchiveOptions represents the available Archive() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#get-file-archive
type ArchiveOptions struct {
	Format *string `url:"-" json:"-"`
	SHA    *string `url:"sha,omitempty" json:"sha,omitempty"`
}

// Archive gets an archive of the repository.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#get-file-archive
func (s *RepositoriesService) Archive(pid interface{}, opt *ArchiveOptions, options ...OptionFunc) ([]byte, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/repository/archive", url.QueryEscape(project))

	// Set an optional format for the archive.
	if opt != nil && opt.Format != nil {
		u = fmt.Sprintf("%s.%s", u, *opt.Format)
	}

	req, err := s.client.NewRequest("GET", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	var b bytes.Buffer
	resp, err := s.client.Do(req, &b)
	if err != nil {
		return nil, resp, err
	}

	return b.Bytes(), resp, err
}

// Compare represents the result of a comparison of branches, tags or commits.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits
type Compare struct {
	Commit         *Commit   `json:"commit"`
	Commits        []*Commit `json:"commits"`
	Diffs          []*Diff   `json:"diffs"`
	CompareTimeout bool      `json:"compare_timeout"`
	CompareSameRef bool      `json:"compare_same_ref"`
}

func (c Compare) String() string {
	return Stringify(c)
}

// CompareOptions represents the available Compare() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits
type CompareOptions struct {
	From     *string `url:"from,omitempty" json:"from,omitempty"`
	To       *string `url:"to,omitempty" json:"to,omitempty"`
	Straight *bool   `url:"straight,omitempty" json:"straight,omitempty"`
}

// Compare compares branches, tags or commits.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits
func (s *RepositoriesService) Compare(pid interface{}, opt *CompareOptions, options ...OptionFunc) (*Compare, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/repository/compare", url.QueryEscape(project))

	req, err := s.client.NewRequest("GET", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	c := new(Compare)
	resp, err := s.client.Do(req, c)
	if err != nil {
		return nil, resp, err
	}

	return c, resp, err
}

// Contributor represents a GitLap contributor.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html#contributors
type Contributor struct {
	Name      string `json:"name,omitempty"`
	Email     string `json:"email,omitempty"`
	Commits   int    `json:"commits,omitempty"`
	Additions int    `json:"additions,omitempty"`
	Deletions int    `json:"deletions,omitempty"`
}

func (c Contributor) String() string {
	return Stringify(c)
}

// ListContributorsOptions represents the available ListContributors() options.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html#contributors
type ListContributorsOptions ListOptions

// Contributors gets the repository contributors list.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html#contributors
func (s *RepositoriesService) Contributors(pid interface{}, opt *ListContributorsOptions, options ...OptionFunc) ([]*Contributor, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/repository/contributors", url.QueryEscape(project))

	req, err := s.client.NewRequest("GET", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	var c []*Contributor
	resp, err := s.client.Do(req, &c)
	if err != nil {
		return nil, resp, err
	}

	return c, resp, err
}

// MergeBaseOptions represents the available MergeBase() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#merge-base
type MergeBaseOptions struct {
	Ref []string `url:"refs[],omitempty" json:"refs,omitempty"`
}

// MergeBase gets the common ancestor for 2 refs (commit SHAs, branch
// names or tags).
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/repositories.html#merge-base
func (s *RepositoriesService) MergeBase(pid interface{}, opt *MergeBaseOptions, options ...OptionFunc) (*Commit, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/repository/merge_base", url.QueryEscape(project))

	req, err := s.client.NewRequest("GET", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	c := new(Commit)
	resp, err := s.client.Do(req, c)
	if err != nil {
		return nil, resp, err
	}

	return c, resp, err
}
<|endoftext|>


<commit_before>package intercept

import (
	"bytes"
	"errors"
	"github.com/nbio/st"
	"io/ioutil"
	"net/http"
	"strings"
	"testing"
)

var errRead = errors.New("read error")

type errorReader struct{}

func (r *errorReader) Read(p []byte) (int, error) {
	return 0, errRead
}

type user struct {
	Name string
}

func TestNewRequestModifier(t *testing.T) {
	h := http.Header{}
	h.Set("foo", "bar")
	req := &http.Request{Header: h}
	modifier := NewRequestModifier(req)
	st.Expect(t, modifier.Request, req)
	st.Expect(t, modifier.Header, h)
}

func TestReadString(t *testing.T) {
	bodyStr := `{"name":"Rick"}`
	strReader := strings.NewReader(bodyStr)
	body := ioutil.NopCloser(strReader)
	req := &http.Request{Header: http.Header{}, Body: body}
	modifier := NewRequestModifier(req)
	str, err := modifier.ReadString()
	st.Expect(t, err, nil)
	st.Expect(t, str, bodyStr)
}

func TestReadStringError(t *testing.T) {
	body := ioutil.NopCloser(&errorReader{})
	req := &http.Request{Header: http.Header{}, Body: body}
	modifier := NewRequestModifier(req)
	str, err := modifier.ReadString()
	st.Expect(t, err, errRead)
	st.Expect(t, str, "")
}

func TestReadBytes(t *testing.T) {
	bodyBytes := []byte(`{"name":"Rick"}`)
	strReader := bytes.NewBuffer(bodyBytes)
	body := ioutil.NopCloser(strReader)
	req := &http.Request{Header: http.Header{}, Body: body}
	modifier := NewRequestModifier(req)
	str, err := modifier.ReadBytes()
	st.Expect(t, err, nil)
	st.Expect(t, str, bodyBytes)
}

func TestReadBytesError(t *testing.T) {
	body := ioutil.NopCloser(&errorReader{})
	req := &http.Request{Header: http.Header{}, Body: body}
	modifier := NewRequestModifier(req)
	buf, err := modifier.ReadBytes()
	st.Expect(t, err, errRead)
	st.Expect(t, len(buf), 0)
}

func TestDecodeJSON(t *testing.T) {
	bodyBytes := []byte(`{"name":"Rick"}`)
	strReader := bytes.NewBuffer(bodyBytes)
	body := ioutil.NopCloser(strReader)
	req := &http.Request{Header: http.Header{}, Body: body}
	modifier := NewRequestModifier(req)
	u := user{}
	err := modifier.DecodeJSON(&u)
	st.Expect(t, err, nil)
	st.Expect(t, u.Name, "Rick")
}
<commit_msg>feat(test): Add test for DecodeJSON error<commit_after>package intercept

import (
	"bytes"
	"errors"
	"github.com/nbio/st"
	"io/ioutil"
	"net/http"
	"strings"
	"testing"
)

var errRead = errors.New("read error")

type errorReader struct{}

func (r *errorReader) Read(p []byte) (int, error) {
	return 0, errRead
}

type user struct {
	Name string
}

func TestNewRequestModifier(t *testing.T) {
	h := http.Header{}
	h.Set("foo", "bar")
	req := &http.Request{Header: h}
	modifier := NewRequestModifier(req)
	st.Expect(t, modifier.Request, req)
	st.Expect(t, modifier.Header, h)
}

func TestReadString(t *testing.T) {
	bodyStr := `{"name":"Rick"}`
	strReader := strings.NewReader(bodyStr)
	body := ioutil.NopCloser(strReader)
	req := &http.Request{Header: http.Header{}, Body: body}
	modifier := NewRequestModifier(req)
	str, err := modifier.ReadString()
	st.Expect(t, err, nil)
	st.Expect(t, str, bodyStr)
}

func TestReadStringError(t *testing.T) {
	body := ioutil.NopCloser(&errorReader{})
	req := &http.Request{Header: http.Header{}, Body: body}
	modifier := NewRequestModifier(req)
	str, err := modifier.ReadString()
	st.Expect(t, err, errRead)
	st.Expect(t, str, "")
}

func TestReadBytes(t *testing.T) {
	bodyBytes := []byte(`{"name":"Rick"}`)
	strReader := bytes.NewBuffer(bodyBytes)
	body := ioutil.NopCloser(strReader)
	req := &http.Request{Header: http.Header{}, Body: body}
	modifier := NewRequestModifier(req)
	str, err := modifier.ReadBytes()
	st.Expect(t, err, nil)
	st.Expect(t, str, bodyBytes)
}

func TestReadBytesError(t *testing.T) {
	body := ioutil.NopCloser(&errorReader{})
	req := &http.Request{Header: http.Header{}, Body: body}
	modifier := NewRequestModifier(req)
	buf, err := modifier.ReadBytes()
	st.Expect(t, err, errRead)
	st.Expect(t, len(buf), 0)
}

func TestDecodeJSON(t *testing.T) {
	bodyBytes := []byte(`{"name":"Rick"}`)
	strReader := bytes.NewBuffer(bodyBytes)
	body := ioutil.NopCloser(strReader)
	req := &http.Request{Header: http.Header{}, Body: body}
	modifier := NewRequestModifier(req)
	u := user{}
	err := modifier.DecodeJSON(&u)
	st.Expect(t, err, nil)
	st.Expect(t, u.Name, "Rick")
}

func TestDecodeJSONErrorFromReadBytes(t *testing.T) {
	body := ioutil.NopCloser(&errorReader{})
	req := &http.Request{Header: http.Header{}, Body: body}
	modifier := NewRequestModifier(req)
	u := user{}
	err := modifier.DecodeJSON(&u)
	st.Expect(t, err, errRead)
	st.Expect(t, u.Name, "")
}
<|endoftext|>