<commit_before>package server

import (
	"errors"
	mdb "github.com/msackman/gomdb"
	"log"
	"reflect"
	"runtime"
	"sync"
	"time"
)

type ReadonlyTransaction func(rtxn *RTxn) (interface{}, error)
type ReadWriteTransaction func(rwtxn *RWTxn) (interface{}, error)

type DBISettings struct {
	Flags uint
	dbi   mdb.DBI
}

type MDBServer struct {
	writerChan  chan mdbQuery
	readerChan  chan mdbQuery
	terminated  chan struct{}
	readers     []*mdbReader
	env         *mdb.Env
	rwtxn       *RWTxn
	batchedTxn  []*readWriteTransactionFuture
	txn         *mdb.Txn
	ticker      *time.Ticker
	txnDuration time.Duration
	zeroDBI     mdb.DBI
}

type mdbReader struct {
	queryChan chan mdbQuery
	server    *MDBServer
	rtxn      *RTxn
}

type mdbQuery interface {
	mdbQueryWitness()
}

type queryShutdown struct{}
type queryInternalShutdown sync.WaitGroup

func (qs *queryShutdown) mdbQueryWitness()                {}
func (qis *queryInternalShutdown) mdbQueryWitness()       {}
func (rotf *readonlyTransactionFuture) mdbQueryWitness()  {}
func (rwtf *readWriteTransactionFuture) mdbQueryWitness() {}

const (
	defaultChanSize = 64
)

var (
	NotAStructPointer = errors.New("Not a pointer to a struct")
	UnexpectedMessage = errors.New("Unexpected message")
	shutdownQuery     = &queryShutdown{}
)

func NewMDBServer(path string, openFlags, filemode uint, mapSize uint64, numReaders int, dbiStruct interface{}) (*MDBServer, error) {
	writerChan := make(chan mdbQuery, defaultChanSize)
	readerChan := make(chan mdbQuery, defaultChanSize)
	if numReaders < 1 {
		numReaders = runtime.GOMAXPROCS(0) / 2 // with 0, just returns current value
		if numReaders < 1 {
			numReaders = 1
		}
	}
	server := &MDBServer{
		writerChan: writerChan,
		readerChan: readerChan,
		terminated: make(chan struct{}),
		readers:    make([]*mdbReader, numReaders),
		rwtxn:      &RWTxn{},
		batchedTxn: make([]*readWriteTransactionFuture, 0, defaultChanSize),
	}
	resultChan := make(chan error, 0)
	go server.actor(path, openFlags, filemode, mapSize, dbiStruct, resultChan)
	result := <-resultChan
	if result == nil {
		return server, nil
	} else {
		return nil, result
	}
}

func (mdb *MDBServer) ReadonlyTransaction(txn ReadonlyTransaction) TransactionFuture {
	txnFuture := newReadonlyTransactionFuture(txn, mdb)
	select {
	case mdb.readerChan <- txnFuture:
	case <-mdb.terminated:
	}
	return txnFuture
}

func (mdb *MDBServer) ReadWriteTransaction(forceCommit bool, txn ReadWriteTransaction) TransactionFuture {
	txnFuture := newReadWriteTransactionFuture(txn, forceCommit, mdb)
	select {
	case mdb.writerChan <- txnFuture:
	case <-mdb.terminated:
	}
	return txnFuture
}

func (mdb *MDBServer) Shutdown() {
	select {
	case mdb.writerChan <- shutdownQuery:
		<-mdb.terminated
	case <-mdb.terminated:
	}
}

func (server *MDBServer) actor(path string, flags, mode uint, mapSize uint64, dbiStruct interface{}, initResult chan<- error) {
	runtime.LockOSThread()
	defer func() {
		if server.env != nil {
			server.env.Close()
		}
	}()
	if err := server.init(path, flags, mode, mapSize, dbiStruct); err != nil {
		initResult <- err
		return
	}
	close(initResult)
	server.actorLoop()
}

func (server *MDBServer) actorLoop() {
	var err error
	terminate := false
	for !terminate {
		if server.txn == nil {
			query := <-server.writerChan
			terminate, err = server.handleQuery(query)
		} else {
			select {
			case query := <-server.writerChan:
				terminate, err = server.handleQuery(query)
			case <-server.ticker.C:
				err = server.commitTxns()
			default:
				err = server.commitTxns()
			}
		}
		terminate = terminate || err != nil
	}
	if err != nil {
		log.Println(err)
	}
	if err = server.commitTxns(); err != nil {
		log.Println(err)
	}
	server.handleShutdown()
	close(server.terminated)
}

func (server *MDBServer) handleQuery(query mdbQuery) (terminate bool, err error) {
	switch msg := query.(type) {
	case *queryShutdown:
		terminate = true
	case *readWriteTransactionFuture:
		err = server.handleRunTxn(msg)
	default:
		err = UnexpectedMessage
	}
	return
}

func (server *MDBServer) handleShutdown() {
	wg := new(sync.WaitGroup)
	wg.Add(len(server.readers))
	is := (*queryInternalShutdown)(wg)
	for _, reader := range server.readers {
		reader.queryChan <- is
	}
	wg.Wait()
}

func (server *MDBServer) init(path string, flags, mode uint, mapSize uint64, dbiStruct interface{}) error {
	env, err := mdb.NewEnv()
	if err != nil {
		return err
	}

	if err = env.SetMapSize(mapSize); err != nil {
		return err
	}
	if err = env.SetMaxReaders(uint(1 + len(server.readers))); err != nil {
		return err
	}

	dbiMap, err := analyzeDbiStruct(dbiStruct)
	if err != nil {
		return err
	}

	if l := len(dbiMap); l != 0 {
		if err = env.SetMaxDBs(1 + mdb.DBI(l)); err != nil {
			return err
		}
	}
	if err = env.Open(path, flags, mode); err != nil {
		return err
	}
	server.env = env

	txn, err := env.BeginTxn(nil, 0)
	if err != nil {
		return err
	}
	zeroDBI, err := txn.DBIOpen(nil, 0)
	if err != nil {
		txn.Abort()
		return err
	}
	server.zeroDBI = zeroDBI
	for name, value := range dbiMap {
		dbi, err := txn.DBIOpen(&name, value.Flags)
		if err != nil {
			txn.Abort()
			return err
		}
		value.dbi = dbi
	}
	if err = txn.Commit(); err != nil {
		return err
	}

	if err = server.calibrate(); err != nil {
		return err
	}

	for idx := range server.readers {
		reader := &mdbReader{
			queryChan: make(chan mdbQuery, defaultChanSize),
			server:    server,
			rtxn:      &RTxn{},
		}
		server.readers[idx] = reader
		go reader.actorLoop()
	}
	return nil
}

func (server *MDBServer) calibrate() error {
	key := []byte("calibration")
	value := []byte("  testing testing 1 2 3")
	start := time.Now()
	end := start
	count := 0
	hundredMillis := 100 * time.Millisecond
	for count < 50 {
		value[0] = byte(count)
		txn, err := server.env.BeginTxn(nil, 0)
		if txn.Put(server.zeroDBI, key, value, 0); err != nil {
			txn.Abort()
			return err
		}
		if err = txn.Commit(); err != nil {
			return err
		}
		count++
		end = time.Now()
		if end.Sub(start) > hundredMillis {
			break
		}
	}
	txn, err := server.env.BeginTxn(nil, 0)
	if err != nil {
		return err
	}
	if err = txn.Del(server.zeroDBI, key, nil); err != nil {
		txn.Abort()
		return err
	}
	if err = txn.Commit(); err != nil {
		return err
	}
	server.txnDuration = time.Duration(float64(5*end.Sub(start).Nanoseconds()) / float64(count))
	return nil
}

func (server *MDBServer) handleRunTxn(txnFuture *readWriteTransactionFuture) error {
	/*
		If creating a txn, or commiting a txn errors, then that kills both the txns and us.
			If the txn func itself errors, that kills the txns, but it doesn't kill us.
	*/
	var err error
	server.batchedTxn = append(server.batchedTxn, txnFuture)
	txn := server.txn
	if txn == nil {
		txn, err = server.env.BeginTxn(nil, 0)
		if err != nil {
			server.txnsComplete(err)
			return err
		}
		server.txn = txn
	}
	rwtxn := server.rwtxn
	rwtxn.txn = txn
	var txnErr error
	txnFuture.result, txnErr = txnFuture.txn(rwtxn)
	if txnErr == nil {
		if txnFuture.forceCommit {
			server.commitTxns()
		} else {
			server.ensureTicker()
		}
	} else {
		txn.Abort()
		server.txnsComplete(txnErr)
	}
	return err
}

func (server *MDBServer) txnsComplete(err error) {
	server.txn = nil
	server.cancelTicker()
	for _, txnFuture := range server.batchedTxn {
		txnFuture.error = err
		close(txnFuture.signal)
	}
	server.batchedTxn = server.batchedTxn[:0]
}

func (server *MDBServer) ensureTicker() {
	if server.ticker == nil {
		ticker := time.NewTicker(server.txnDuration)
		server.ticker = ticker
	}
}

func (server *MDBServer) cancelTicker() {
	if server.ticker != nil {
		server.ticker.Stop()
		server.ticker = nil
	}
}

func (server *MDBServer) commitTxns() error {
	if server.txn == nil {
		return nil
	} else {
		err := server.txn.Commit()
		server.txnsComplete(err)
		return err
	}
}

func analyzeDbiStruct(dbiStruct interface{}) (map[string]*DBISettings, error) {
	m := make(map[string]*DBISettings)
	if dbiStruct == nil {
		return m, nil
	}

	t := reflect.TypeOf(dbiStruct)
	if t.Kind() != reflect.Ptr {
		return nil, NotAStructPointer
	}

	t = t.Elem()
	if t.Kind() != reflect.Struct {
		return nil, NotAStructPointer
	}

	dbiSettings := &DBISettings{}
	dbiSettingsType := reflect.TypeOf(dbiSettings)

	v := reflect.ValueOf(dbiStruct)
	v = v.Elem()
	for idx := 0; idx < t.NumField(); idx++ {
		field := t.Field(idx)
		fieldValue := v.Field(idx)
		if dbiSettingsType.AssignableTo(field.Type) && fieldValue.CanSet() &&
			fieldValue.CanInterface() && !fieldValue.IsNil() {
			m[field.Name] = fieldValue.Interface().(*DBISettings)
		}
	}
	return m, nil
}

func (reader *mdbReader) actorLoop() {
	runtime.LockOSThread()
	txnChan := reader.server.readerChan
	var err error
	terminate := false
	for !terminate {
		select {
		case direct := <-reader.queryChan:
			switch query := direct.(type) {
			case *queryInternalShutdown:
				((*sync.WaitGroup)(query)).Done()
				terminate = true
			default:
				err = UnexpectedMessage
			}
		case query := <-txnChan:
			switch msg := query.(type) {
			case *readonlyTransactionFuture:
				err = reader.handleRunTxn(msg)
			default:
				err = UnexpectedMessage
			}
		}
		terminate = terminate || err != nil
	}
	if err != nil {
		log.Println(err)
	}
}

func (reader *mdbReader) handleRunTxn(txnFuture *readonlyTransactionFuture) error {
	defer close(txnFuture.signal)
	txn, err := reader.server.env.BeginTxn(nil, mdb.RDONLY)
	if err != nil {
		txnFuture.error = err
		return err
	}
	rtxn := reader.rtxn
	rtxn.txn = txn
	txnFuture.result, txnFuture.error = txnFuture.txn(rtxn)
	txn.Abort()
	return nil
}

type RTxn struct {
	txn *mdb.Txn
}

func (rtxn *RTxn) Reset()                                           { rtxn.txn.Reset() }
func (rtxn *RTxn) Renew() error                                     { return rtxn.txn.Renew() }
func (rtxn *RTxn) Get(dbi *DBISettings, key []byte) ([]byte, error) { return rtxn.txn.Get(dbi.dbi, key) }
func (rtxn *RTxn) GetVal(dbi *DBISettings, key []byte) (mdb.Val, error) {
	return rtxn.txn.GetVal(dbi.dbi, key)
}

type RWTxn struct {
	RTxn
}

func (rwtxn *RWTxn) Drop(dbi *DBISettings, del int) error { return rwtxn.txn.Drop(dbi.dbi, del) }
func (rwtxn *RWTxn) Put(dbi *DBISettings, key, val []byte, flags uint) error {
	return rwtxn.txn.Put(dbi.dbi, key, val, flags)
}
func (rwtxn *RWTxn) Del(dbi *DBISettings, key, val []byte) error {
	return rwtxn.txn.Del(dbi.dbi, key, val)
}

type TransactionFuture interface {
	Force() TransactionFuture
	ResultError() (interface{}, error)
}

type plainTransactionFuture struct {
	result     interface{}
	error      error
	signal     chan struct{}
	terminated chan struct{}
}

func (tf *plainTransactionFuture) Force() TransactionFuture {
	select {
	case <-tf.signal:
	case <-tf.terminated:
	}
	return tf
}

func (tf *plainTransactionFuture) ResultError() (interface{}, error) {
	tf.Force()
	return tf.result, tf.error
}

type readonlyTransactionFuture struct {
	plainTransactionFuture
	txn ReadonlyTransaction
}

type readWriteTransactionFuture struct {
	plainTransactionFuture
	txn         ReadWriteTransaction
	forceCommit bool
}

func newPlainTransactionFuture(mdb *MDBServer) plainTransactionFuture {
	return plainTransactionFuture{
		signal:     make(chan struct{}),
		terminated: mdb.terminated,
	}
}

func newReadonlyTransactionFuture(txn ReadonlyTransaction, mdb *MDBServer) *readonlyTransactionFuture {
	return &readonlyTransactionFuture{
		plainTransactionFuture: newPlainTransactionFuture(mdb),
		txn: txn,
	}
}

func newReadWriteTransactionFuture(txn ReadWriteTransaction, forceCommit bool, mdb *MDBServer) *readWriteTransactionFuture {
	return &readWriteTransactionFuture{
		plainTransactionFuture: newPlainTransactionFuture(mdb),
		txn:         txn,
		forceCommit: forceCommit,
	}
}
<commit_msg>Support cursor ops in the server.<commit_after>package server

import (
	"errors"
	mdb "github.com/msackman/gomdb"
	"log"
	"reflect"
	"runtime"
	"sync"
	"time"
)

type ReadonlyTransaction func(rtxn *RTxn) (interface{}, error)
type ReadWriteTransaction func(rwtxn *RWTxn) (interface{}, error)

type DBISettings struct {
	Flags uint
	dbi   mdb.DBI
}

type MDBServer struct {
	writerChan  chan mdbQuery
	readerChan  chan mdbQuery
	terminated  chan struct{}
	readers     []*mdbReader
	env         *mdb.Env
	rwtxn       *RWTxn
	batchedTxn  []*readWriteTransactionFuture
	txn         *mdb.Txn
	ticker      *time.Ticker
	txnDuration time.Duration
	zeroDBI     mdb.DBI
}

type mdbReader struct {
	queryChan chan mdbQuery
	server    *MDBServer
	rtxn      *RTxn
}

type mdbQuery interface {
	mdbQueryWitness()
}

type queryShutdown struct{}
type queryInternalShutdown sync.WaitGroup

func (qs *queryShutdown) mdbQueryWitness()                {}
func (qis *queryInternalShutdown) mdbQueryWitness()       {}
func (rotf *readonlyTransactionFuture) mdbQueryWitness()  {}
func (rwtf *readWriteTransactionFuture) mdbQueryWitness() {}

const (
	defaultChanSize = 64
)

var (
	NotAStructPointer = errors.New("Not a pointer to a struct")
	UnexpectedMessage = errors.New("Unexpected message")
	shutdownQuery     = &queryShutdown{}
)

func NewMDBServer(path string, openFlags, filemode uint, mapSize uint64, numReaders int, dbiStruct interface{}) (*MDBServer, error) {
	writerChan := make(chan mdbQuery, defaultChanSize)
	readerChan := make(chan mdbQuery, defaultChanSize)
	if numReaders < 1 {
		numReaders = runtime.GOMAXPROCS(0) / 2 // with 0, just returns current value
		if numReaders < 1 {
			numReaders = 1
		}
	}
	server := &MDBServer{
		writerChan: writerChan,
		readerChan: readerChan,
		terminated: make(chan struct{}),
		readers:    make([]*mdbReader, numReaders),
		rwtxn:      &RWTxn{},
		batchedTxn: make([]*readWriteTransactionFuture, 0, defaultChanSize),
	}
	resultChan := make(chan error, 0)
	go server.actor(path, openFlags, filemode, mapSize, dbiStruct, resultChan)
	result := <-resultChan
	if result == nil {
		return server, nil
	} else {
		return nil, result
	}
}

func (mdb *MDBServer) ReadonlyTransaction(txn ReadonlyTransaction) TransactionFuture {
	txnFuture := newReadonlyTransactionFuture(txn, mdb)
	select {
	case mdb.readerChan <- txnFuture:
	case <-mdb.terminated:
	}
	return txnFuture
}

func (mdb *MDBServer) ReadWriteTransaction(forceCommit bool, txn ReadWriteTransaction) TransactionFuture {
	txnFuture := newReadWriteTransactionFuture(txn, forceCommit, mdb)
	select {
	case mdb.writerChan <- txnFuture:
	case <-mdb.terminated:
	}
	return txnFuture
}

func (mdb *MDBServer) Shutdown() {
	select {
	case mdb.writerChan <- shutdownQuery:
		<-mdb.terminated
	case <-mdb.terminated:
	}
}

func (server *MDBServer) actor(path string, flags, mode uint, mapSize uint64, dbiStruct interface{}, initResult chan<- error) {
	runtime.LockOSThread()
	defer func() {
		if server.env != nil {
			server.env.Close()
		}
	}()
	if err := server.init(path, flags, mode, mapSize, dbiStruct); err != nil {
		initResult <- err
		return
	}
	close(initResult)
	server.actorLoop()
}

func (server *MDBServer) actorLoop() {
	var err error
	terminate := false
	for !terminate {
		if server.txn == nil {
			query := <-server.writerChan
			terminate, err = server.handleQuery(query)
		} else {
			select {
			case query := <-server.writerChan:
				terminate, err = server.handleQuery(query)
			case <-server.ticker.C:
				err = server.commitTxns()
			default:
				err = server.commitTxns()
			}
		}
		terminate = terminate || err != nil
	}
	if err != nil {
		log.Println(err)
	}
	if err = server.commitTxns(); err != nil {
		log.Println(err)
	}
	server.handleShutdown()
	close(server.terminated)
}

func (server *MDBServer) handleQuery(query mdbQuery) (terminate bool, err error) {
	switch msg := query.(type) {
	case *queryShutdown:
		terminate = true
	case *readWriteTransactionFuture:
		err = server.handleRunTxn(msg)
	default:
		err = UnexpectedMessage
	}
	return
}

func (server *MDBServer) handleShutdown() {
	wg := new(sync.WaitGroup)
	wg.Add(len(server.readers))
	is := (*queryInternalShutdown)(wg)
	for _, reader := range server.readers {
		reader.queryChan <- is
	}
	wg.Wait()
}

func (server *MDBServer) init(path string, flags, mode uint, mapSize uint64, dbiStruct interface{}) error {
	env, err := mdb.NewEnv()
	if err != nil {
		return err
	}

	if err = env.SetMapSize(mapSize); err != nil {
		return err
	}
	if err = env.SetMaxReaders(uint(1 + len(server.readers))); err != nil {
		return err
	}

	dbiMap, err := analyzeDbiStruct(dbiStruct)
	if err != nil {
		return err
	}

	if l := len(dbiMap); l != 0 {
		if err = env.SetMaxDBs(1 + mdb.DBI(l)); err != nil {
			return err
		}
	}
	if err = env.Open(path, flags, mode); err != nil {
		return err
	}
	server.env = env

	txn, err := env.BeginTxn(nil, 0)
	if err != nil {
		return err
	}
	zeroDBI, err := txn.DBIOpen(nil, 0)
	if err != nil {
		txn.Abort()
		return err
	}
	server.zeroDBI = zeroDBI
	for name, value := range dbiMap {
		dbi, err := txn.DBIOpen(&name, value.Flags)
		if err != nil {
			txn.Abort()
			return err
		}
		value.dbi = dbi
	}
	if err = txn.Commit(); err != nil {
		return err
	}

	if err = server.calibrate(); err != nil {
		return err
	}

	for idx := range server.readers {
		reader := &mdbReader{
			queryChan: make(chan mdbQuery, defaultChanSize),
			server:    server,
			rtxn:      &RTxn{},
		}
		server.readers[idx] = reader
		go reader.actorLoop()
	}
	return nil
}

func (server *MDBServer) calibrate() error {
	key := []byte("calibration")
	value := []byte("  testing testing 1 2 3")
	start := time.Now()
	end := start
	count := 0
	hundredMillis := 100 * time.Millisecond
	for count < 50 {
		value[0] = byte(count)
		txn, err := server.env.BeginTxn(nil, 0)
		if txn.Put(server.zeroDBI, key, value, 0); err != nil {
			txn.Abort()
			return err
		}
		if err = txn.Commit(); err != nil {
			return err
		}
		count++
		end = time.Now()
		if end.Sub(start) > hundredMillis {
			break
		}
	}
	txn, err := server.env.BeginTxn(nil, 0)
	if err != nil {
		return err
	}
	if err = txn.Del(server.zeroDBI, key, nil); err != nil {
		txn.Abort()
		return err
	}
	if err = txn.Commit(); err != nil {
		return err
	}
	server.txnDuration = time.Duration(float64(5*end.Sub(start).Nanoseconds()) / float64(count))
	return nil
}

func (server *MDBServer) handleRunTxn(txnFuture *readWriteTransactionFuture) error {
	/*
		If creating a txn, or commiting a txn errors, then that kills both the txns and us.
			If the txn func itself errors, that kills the txns, but it doesn't kill us.
	*/
	var err error
	server.batchedTxn = append(server.batchedTxn, txnFuture)
	txn := server.txn
	if txn == nil {
		txn, err = server.env.BeginTxn(nil, 0)
		if err != nil {
			server.txnsComplete(err)
			return err
		}
		server.txn = txn
	}
	rwtxn := server.rwtxn
	rwtxn.txn = txn
	var txnErr error
	txnFuture.result, txnErr = txnFuture.txn(rwtxn)
	if txnErr == nil {
		if txnFuture.forceCommit {
			server.commitTxns()
		} else {
			server.ensureTicker()
		}
	} else {
		txn.Abort()
		server.txnsComplete(txnErr)
	}
	return err
}

func (server *MDBServer) txnsComplete(err error) {
	server.txn = nil
	server.cancelTicker()
	for _, txnFuture := range server.batchedTxn {
		txnFuture.error = err
		close(txnFuture.signal)
	}
	server.batchedTxn = server.batchedTxn[:0]
}

func (server *MDBServer) ensureTicker() {
	if server.ticker == nil {
		ticker := time.NewTicker(server.txnDuration)
		server.ticker = ticker
	}
}

func (server *MDBServer) cancelTicker() {
	if server.ticker != nil {
		server.ticker.Stop()
		server.ticker = nil
	}
}

func (server *MDBServer) commitTxns() error {
	if server.txn == nil {
		return nil
	} else {
		err := server.txn.Commit()
		server.txnsComplete(err)
		return err
	}
}

func analyzeDbiStruct(dbiStruct interface{}) (map[string]*DBISettings, error) {
	m := make(map[string]*DBISettings)
	if dbiStruct == nil {
		return m, nil
	}

	t := reflect.TypeOf(dbiStruct)
	if t.Kind() != reflect.Ptr {
		return nil, NotAStructPointer
	}

	t = t.Elem()
	if t.Kind() != reflect.Struct {
		return nil, NotAStructPointer
	}

	dbiSettings := &DBISettings{}
	dbiSettingsType := reflect.TypeOf(dbiSettings)

	v := reflect.ValueOf(dbiStruct)
	v = v.Elem()
	for idx := 0; idx < t.NumField(); idx++ {
		field := t.Field(idx)
		fieldValue := v.Field(idx)
		if dbiSettingsType.AssignableTo(field.Type) && fieldValue.CanSet() &&
			fieldValue.CanInterface() && !fieldValue.IsNil() {
			m[field.Name] = fieldValue.Interface().(*DBISettings)
		}
	}
	return m, nil
}

func (reader *mdbReader) actorLoop() {
	runtime.LockOSThread()
	txnChan := reader.server.readerChan
	var err error
	terminate := false
	for !terminate {
		select {
		case direct := <-reader.queryChan:
			switch query := direct.(type) {
			case *queryInternalShutdown:
				((*sync.WaitGroup)(query)).Done()
				terminate = true
			default:
				err = UnexpectedMessage
			}
		case query := <-txnChan:
			switch msg := query.(type) {
			case *readonlyTransactionFuture:
				err = reader.handleRunTxn(msg)
			default:
				err = UnexpectedMessage
			}
		}
		terminate = terminate || err != nil
	}
	if err != nil {
		log.Println(err)
	}
}

func (reader *mdbReader) handleRunTxn(txnFuture *readonlyTransactionFuture) error {
	defer close(txnFuture.signal)
	txn, err := reader.server.env.BeginTxn(nil, mdb.RDONLY)
	if err != nil {
		txnFuture.error = err
		return err
	}
	rtxn := reader.rtxn
	rtxn.txn = txn
	txnFuture.result, txnFuture.error = txnFuture.txn(rtxn)
	txn.Abort()
	return nil
}

type RTxn struct {
	txn *mdb.Txn
}

func (rtxn *RTxn) Reset()                                           { rtxn.txn.Reset() }
func (rtxn *RTxn) Renew() error                                     { return rtxn.txn.Renew() }
func (rtxn *RTxn) Get(dbi *DBISettings, key []byte) ([]byte, error) { return rtxn.txn.Get(dbi.dbi, key) }
func (rtxn *RTxn) GetVal(dbi *DBISettings, key []byte) (mdb.Val, error) {
	return rtxn.txn.GetVal(dbi.dbi, key)
}
func (rtxn *RTxn) WithCursor(dbi *DBISettings, fun func(cursor *mdb.Cursor) (interface{}, error)) (interface{}, error) {
	cursor, err := rtxn.txn.CursorOpen(dbi.dbi)
	if err != nil {
		return nil, err
	}
	defer cursor.Close()
	return fun(cursor)
}

type RWTxn struct {
	RTxn
}

func (rwtxn *RWTxn) Drop(dbi *DBISettings, del int) error { return rwtxn.txn.Drop(dbi.dbi, del) }
func (rwtxn *RWTxn) Put(dbi *DBISettings, key, val []byte, flags uint) error {
	return rwtxn.txn.Put(dbi.dbi, key, val, flags)
}
func (rwtxn *RWTxn) Del(dbi *DBISettings, key, val []byte) error {
	return rwtxn.txn.Del(dbi.dbi, key, val)
}

type TransactionFuture interface {
	Force() TransactionFuture
	ResultError() (interface{}, error)
}

type plainTransactionFuture struct {
	result     interface{}
	error      error
	signal     chan struct{}
	terminated chan struct{}
}

func (tf *plainTransactionFuture) Force() TransactionFuture {
	select {
	case <-tf.signal:
	case <-tf.terminated:
	}
	return tf
}

func (tf *plainTransactionFuture) ResultError() (interface{}, error) {
	tf.Force()
	return tf.result, tf.error
}

type readonlyTransactionFuture struct {
	plainTransactionFuture
	txn ReadonlyTransaction
}

type readWriteTransactionFuture struct {
	plainTransactionFuture
	txn         ReadWriteTransaction
	forceCommit bool
}

func newPlainTransactionFuture(mdb *MDBServer) plainTransactionFuture {
	return plainTransactionFuture{
		signal:     make(chan struct{}),
		terminated: mdb.terminated,
	}
}

func newReadonlyTransactionFuture(txn ReadonlyTransaction, mdb *MDBServer) *readonlyTransactionFuture {
	return &readonlyTransactionFuture{
		plainTransactionFuture: newPlainTransactionFuture(mdb),
		txn: txn,
	}
}

func newReadWriteTransactionFuture(txn ReadWriteTransaction, forceCommit bool, mdb *MDBServer) *readWriteTransactionFuture {
	return &readWriteTransactionFuture{
		plainTransactionFuture: newPlainTransactionFuture(mdb),
		txn:         txn,
		forceCommit: forceCommit,
	}
}
<|endoftext|>


<commit_before>// Copyright 2017 Pilosa Corp.
//
// 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 server contains the `pilosa server` subcommand which runs Pilosa
// itself. The purpose of this package is to define an easily tested Command
// object which handles interpreting configuration and setting up all the
// objects that Pilosa needs.
package server

import (
	"errors"
	"fmt"
	"io"
	"math/rand"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/pilosa/pilosa"
	"github.com/pilosa/pilosa/gossip"
	"github.com/pilosa/pilosa/httpbroadcast"
	"github.com/pilosa/pilosa/statsd"
)

func init() {
	rand.Seed(time.Now().UTC().UnixNano())
}

const (
	// DefaultDataDir is the default data directory.
	DefaultDataDir = "~/.pilosa"
)

// Command represents the state of the pilosa server command.
type Command struct {
	Server *pilosa.Server

	// Configuration.
	Config *pilosa.Config

	// Profiling options.
	CPUProfile string
	CPUTime    time.Duration

	// Standard input/output
	*pilosa.CmdIO

	// running will be closed once Command.Run is finished.
	Started chan struct{}
	// Done will be closed when Command.Close() is called
	Done chan struct{}
}

// NewCommand returns a new instance of Main.
func NewCommand(stdin io.Reader, stdout, stderr io.Writer) *Command {
	return &Command{
		Server: pilosa.NewServer(),
		Config: pilosa.NewConfig(),

		CmdIO: pilosa.NewCmdIO(stdin, stdout, stderr),

		Started: make(chan struct{}),
		Done:    make(chan struct{}),
	}
}

// Run executes the pilosa server.
func (m *Command) Run(args ...string) (err error) {
	defer close(m.Started)
	prefix := "~" + string(filepath.Separator)
	if strings.HasPrefix(m.Config.DataDir, prefix) {
		HomeDir := os.Getenv("HOME")
		if HomeDir == "" {
			return errors.New("data directory not specified and no home dir available")
		}
		m.Config.DataDir = filepath.Join(HomeDir, strings.TrimPrefix(m.Config.DataDir, prefix))
	}

	// SetupServer
	err = m.SetupServer()
	if err != nil {
		return err
	}

	// Initialize server.
	if err = m.Server.Open(); err != nil {
		return fmt.Errorf("server.Open: %v", err)
	}

	m.Server.Logger().Printf("Listening as http://%s\n", m.Server.Host)
	return nil
}

// SetupServer use the cluster configuration to setup this server
func (m *Command) SetupServer() error {
	var err error
	cluster := pilosa.NewCluster()
	cluster.ReplicaN = m.Config.Cluster.ReplicaN

	for _, hostport := range m.Config.Cluster.Hosts {
		cluster.Nodes = append(cluster.Nodes, &pilosa.Node{Host: hostport})
	}
	// TODO: if InternalHosts is not provided then pilosa.Node.InternalHost is empty.
	// This will throw an error when trying to Broadcast messages over HTTP.
	// One option may be to fall back to using host from hostport + config.InternalPort.
	for i, internalhostport := range m.Config.Cluster.InternalHosts {
		cluster.Nodes[i].InternalHost = internalhostport
	}
	m.Server.Cluster = cluster

	// Setup logging output.
	if m.Config.LogPath == "" {
		m.Server.LogOutput = m.Stderr
	} else {
		logFile, err := os.OpenFile(m.Config.LogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)
		if err != nil {
			return err
		}
		m.Server.LogOutput = logFile
	}

	// Configure holder.
	m.Server.Logger().Printf("Using data from: %s\n", m.Config.DataDir)
	m.Server.Holder.Path = m.Config.DataDir
	m.Server.MetricInterval = time.Duration(m.Config.Metric.PollingInterval)
	m.Server.Holder.Stats, err = NewStatsClient(m.Config.Metric.Service, m.Config.Metric.Host)
	if err != nil {
		return err
	}

	m.Server.Holder.Stats.SetLogger(m.Server.LogOutput)

	// Copy configuration flags.
	m.Server.MaxWritesPerRequest = m.Config.MaxWritesPerRequest

	m.Server.Host, err = normalizeHost(m.Config.Host)
	if err != nil {
		return err
	}

	// Set internal port (string).
	internalPortStr := pilosa.DefaultInternalPort
	if m.Config.Cluster.InternalPort != "" {
		internalPortStr = m.Config.Cluster.InternalPort
	}

	switch m.Config.Cluster.Type {
	case "http":
		m.Server.Broadcaster = httpbroadcast.NewHTTPBroadcaster(m.Server, internalPortStr)
		m.Server.BroadcastReceiver = httpbroadcast.NewHTTPBroadcastReceiver(internalPortStr, m.Server.LogOutput)
		m.Server.Cluster.NodeSet = httpbroadcast.NewHTTPNodeSet()
		err := m.Server.Cluster.NodeSet.(*httpbroadcast.HTTPNodeSet).Join(m.Server.Cluster.Nodes)
		if err != nil {
			return err
		}
	case "gossip":
		gossipPort, err := strconv.Atoi(internalPortStr)
		if err != nil {
			return err
		}
		gossipSeed := pilosa.DefaultHost
		if m.Config.Cluster.GossipSeed != "" {
			gossipSeed = m.Config.Cluster.GossipSeed
		}
		// get the host portion of addr to use for binding
		gossipHost, _, err := net.SplitHostPort(m.Config.Host)
		if err != nil {
			gossipHost = m.Config.Host
		}
		gossipNodeSet := gossip.NewGossipNodeSet(m.Config.Host, gossipHost, gossipPort, gossipSeed, m.Server, m.Server.LogOutput)
		m.Server.Cluster.NodeSet = gossipNodeSet
		m.Server.Broadcaster = gossipNodeSet
		m.Server.BroadcastReceiver = gossipNodeSet
	case "static", "":
		m.Server.Broadcaster = pilosa.NopBroadcaster
		m.Server.Cluster.NodeSet = pilosa.NewStaticNodeSet()
		m.Server.BroadcastReceiver = pilosa.NopBroadcastReceiver
		err := m.Server.Cluster.NodeSet.(*pilosa.StaticNodeSet).Join(m.Server.Cluster.Nodes)
		if err != nil {
			return err
		}
	default:
		return fmt.Errorf("'%v' is not a supported value for broadcaster type", m.Config.Cluster.Type)
	}

	// Set configuration options.
	m.Server.AntiEntropyInterval = time.Duration(m.Config.AntiEntropy.Interval)
	m.Server.Cluster.LongQueryTime = time.Duration(m.Config.Cluster.LongQueryTime)
	return nil
}

func normalizeHost(host string) (string, error) {
	if !strings.Contains(host, ":") {
		host = host + ":"
	} else if strings.Contains(host, "://") {
		if strings.HasPrefix(host, "http://") {
			host = host[7:]
		} else {
			return "", fmt.Errorf("invalid scheme or host: '%s'. use the format [http://]<host>:<port>", host)
		}
	}
	return host, nil
}

// Close shuts down the server.
func (m *Command) Close() error {
	var logErr error
	serveErr := m.Server.Close()
	logOutput := m.Server.LogOutput
	if closer, ok := logOutput.(io.Closer); ok {
		logErr = closer.Close()
	}
	close(m.Done)
	if serveErr != nil && logErr != nil {
		return fmt.Errorf("closing server: '%v', closing logs: '%v'", serveErr, logErr)
	} else if logErr != nil {
		return logErr
	}
	return serveErr
}

// NewStatsClient creates a stats client from the config
func NewStatsClient(name string, host string) (pilosa.StatsClient, error) {
	switch name {
	case "expvar":
		return pilosa.NewExpvarStatsClient(), nil
	case "statsd":
		return statsd.NewStatsClient(host)
	default:
		return pilosa.NopStatsClient, nil
	}
}
<commit_msg>Deduplicate logfile opening code<commit_after>// Copyright 2017 Pilosa Corp.
//
// 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 server contains the `pilosa server` subcommand which runs Pilosa
// itself. The purpose of this package is to define an easily tested Command
// object which handles interpreting configuration and setting up all the
// objects that Pilosa needs.
package server

import (
	"errors"
	"fmt"
	"io"
	"math/rand"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/pilosa/pilosa"
	"github.com/pilosa/pilosa/gossip"
	"github.com/pilosa/pilosa/httpbroadcast"
	"github.com/pilosa/pilosa/statsd"
)

func init() {
	rand.Seed(time.Now().UTC().UnixNano())
}

const (
	// DefaultDataDir is the default data directory.
	DefaultDataDir = "~/.pilosa"
)

// Command represents the state of the pilosa server command.
type Command struct {
	Server *pilosa.Server

	// Configuration.
	Config *pilosa.Config

	// Profiling options.
	CPUProfile string
	CPUTime    time.Duration

	// Standard input/output
	*pilosa.CmdIO

	// running will be closed once Command.Run is finished.
	Started chan struct{}
	// Done will be closed when Command.Close() is called
	Done chan struct{}
}

// NewCommand returns a new instance of Main.
func NewCommand(stdin io.Reader, stdout, stderr io.Writer) *Command {
	return &Command{
		Server: pilosa.NewServer(),
		Config: pilosa.NewConfig(),

		CmdIO: pilosa.NewCmdIO(stdin, stdout, stderr),

		Started: make(chan struct{}),
		Done:    make(chan struct{}),
	}
}

// Run executes the pilosa server.
func (m *Command) Run(args ...string) (err error) {
	defer close(m.Started)
	prefix := "~" + string(filepath.Separator)
	if strings.HasPrefix(m.Config.DataDir, prefix) {
		HomeDir := os.Getenv("HOME")
		if HomeDir == "" {
			return errors.New("data directory not specified and no home dir available")
		}
		m.Config.DataDir = filepath.Join(HomeDir, strings.TrimPrefix(m.Config.DataDir, prefix))
	}

	// SetupServer
	err = m.SetupServer()
	if err != nil {
		return err
	}

	// Initialize server.
	if err = m.Server.Open(); err != nil {
		return fmt.Errorf("server.Open: %v", err)
	}

	m.Server.Logger().Printf("Listening as http://%s\n", m.Server.Host)
	return nil
}

// SetupServer use the cluster configuration to setup this server
func (m *Command) SetupServer() error {
	var err error
	cluster := pilosa.NewCluster()
	cluster.ReplicaN = m.Config.Cluster.ReplicaN

	for _, hostport := range m.Config.Cluster.Hosts {
		cluster.Nodes = append(cluster.Nodes, &pilosa.Node{Host: hostport})
	}
	// TODO: if InternalHosts is not provided then pilosa.Node.InternalHost is empty.
	// This will throw an error when trying to Broadcast messages over HTTP.
	// One option may be to fall back to using host from hostport + config.InternalPort.
	for i, internalhostport := range m.Config.Cluster.InternalHosts {
		cluster.Nodes[i].InternalHost = internalhostport
	}
	m.Server.Cluster = cluster

	// Setup logging output.
	m.Server.LogOutput, err = GetLogWriter(m.Config.LogPath, m.Stderr)
	if err != nil {
		return err
	}

	// Configure holder.
	m.Server.Logger().Printf("Using data from: %s\n", m.Config.DataDir)
	m.Server.Holder.Path = m.Config.DataDir
	m.Server.MetricInterval = time.Duration(m.Config.Metric.PollingInterval)
	m.Server.Holder.Stats, err = NewStatsClient(m.Config.Metric.Service, m.Config.Metric.Host)
	if err != nil {
		return err
	}

	m.Server.Holder.Stats.SetLogger(m.Server.LogOutput)

	// Copy configuration flags.
	m.Server.MaxWritesPerRequest = m.Config.MaxWritesPerRequest

	m.Server.Host, err = normalizeHost(m.Config.Host)
	if err != nil {
		return err
	}

	// Set internal port (string).
	internalPortStr := pilosa.DefaultInternalPort
	if m.Config.Cluster.InternalPort != "" {
		internalPortStr = m.Config.Cluster.InternalPort
	}

	switch m.Config.Cluster.Type {
	case "http":
		m.Server.Broadcaster = httpbroadcast.NewHTTPBroadcaster(m.Server, internalPortStr)
		m.Server.BroadcastReceiver = httpbroadcast.NewHTTPBroadcastReceiver(internalPortStr, m.Server.LogOutput)
		m.Server.Cluster.NodeSet = httpbroadcast.NewHTTPNodeSet()
		err := m.Server.Cluster.NodeSet.(*httpbroadcast.HTTPNodeSet).Join(m.Server.Cluster.Nodes)
		if err != nil {
			return err
		}
	case "gossip":
		gossipPort, err := strconv.Atoi(internalPortStr)
		if err != nil {
			return err
		}
		gossipSeed := pilosa.DefaultHost
		if m.Config.Cluster.GossipSeed != "" {
			gossipSeed = m.Config.Cluster.GossipSeed
		}
		// get the host portion of addr to use for binding
		gossipHost, _, err := net.SplitHostPort(m.Config.Host)
		if err != nil {
			gossipHost = m.Config.Host
		}
		gossipNodeSet := gossip.NewGossipNodeSet(m.Config.Host, gossipHost, gossipPort, gossipSeed, m.Server, m.Server.LogOutput)
		m.Server.Cluster.NodeSet = gossipNodeSet
		m.Server.Broadcaster = gossipNodeSet
		m.Server.BroadcastReceiver = gossipNodeSet
	case "static", "":
		m.Server.Broadcaster = pilosa.NopBroadcaster
		m.Server.Cluster.NodeSet = pilosa.NewStaticNodeSet()
		m.Server.BroadcastReceiver = pilosa.NopBroadcastReceiver
		err := m.Server.Cluster.NodeSet.(*pilosa.StaticNodeSet).Join(m.Server.Cluster.Nodes)
		if err != nil {
			return err
		}
	default:
		return fmt.Errorf("'%v' is not a supported value for broadcaster type", m.Config.Cluster.Type)
	}

	// Set configuration options.
	m.Server.AntiEntropyInterval = time.Duration(m.Config.AntiEntropy.Interval)
	m.Server.Cluster.LongQueryTime = time.Duration(m.Config.Cluster.LongQueryTime)
	return nil
}

// GetLogWriter opens a file for logging, or a default io.Writer (such as stderr) for an empty path.
func GetLogWriter(path string, defaultWriter io.Writer) (io.Writer, error) {
	// This is split out so it can be used in NewServeCmd as well as SetupServer
	if path == "" {
		return defaultWriter, nil
	} else {
		logFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)
		if err != nil {
			return nil, err
		}
		return logFile, nil
	}
}

func normalizeHost(host string) (string, error) {
	if !strings.Contains(host, ":") {
		host = host + ":"
	} else if strings.Contains(host, "://") {
		if strings.HasPrefix(host, "http://") {
			host = host[7:]
		} else {
			return "", fmt.Errorf("invalid scheme or host: '%s'. use the format [http://]<host>:<port>", host)
		}
	}
	return host, nil
}

// Close shuts down the server.
func (m *Command) Close() error {
	var logErr error
	serveErr := m.Server.Close()
	logOutput := m.Server.LogOutput
	if closer, ok := logOutput.(io.Closer); ok {
		logErr = closer.Close()
	}
	close(m.Done)
	if serveErr != nil && logErr != nil {
		return fmt.Errorf("closing server: '%v', closing logs: '%v'", serveErr, logErr)
	} else if logErr != nil {
		return logErr
	}
	return serveErr
}

// NewStatsClient creates a stats client from the config
func NewStatsClient(name string, host string) (pilosa.StatsClient, error) {
	switch name {
	case "expvar":
		return pilosa.NewExpvarStatsClient(), nil
	case "statsd":
		return statsd.NewStatsClient(host)
	default:
		return pilosa.NopStatsClient, nil
	}
}
<|endoftext|>


<commit_before>package geom

import (
	"reflect"
	"testing"
)

var (
	_ = []T{
		&LineString{},
		&LinearRing{},
		&MultiLineString{},
		&MultiPoint{},
		&MultiPolygon{},
		&Point{},
		&Polygon{},
	}
	_ = []interface {
		Area() float64
		Empty() bool
		Length() float64
	}{
		&LineString{},
		&LinearRing{},
		&MultiLineString{},
		&MultiPoint{},
		&MultiPolygon{},
		&Point{},
		&Polygon{},
	}
)

func aliases(x, y []float64) bool {
	// http://golang.org/src/pkg/math/big/nat.go#L340
	return cap(x) > 0 && cap(y) > 0 && &x[0:cap(x)][cap(x)-1] == &y[0:cap(y)][cap(y)-1]
}

func TestSet(t *testing.T) {
	for _, tc := range []struct {
		c, other, want Coord
	}{
		{Coord{1.0, 2.0}, Coord{2.0, 3.0}, Coord{2.0, 3.0}},
		{Coord{1.0, 2.0, 3.0}, Coord{2.0, 3.0, 4.0}, Coord{2.0, 3.0, 4.0}},
		{Coord{1.0, 2.0}, Coord{2.0, 3.0, 4.0}, Coord{2.0, 3.0}},
		{Coord{1.0, 2.0, 3.0}, Coord{2.0, 3.0}, Coord{2.0, 3.0, 3.0}},
	} {
		if tc.c.Set(tc.other); !reflect.DeepEqual(tc.c, tc.want) {
			t.Errorf("%v.Set(%v); got %v, want %v", tc.c, tc.other, tc.c, tc.want)
		}
	}
}

func TestLayoutString(t *testing.T) {
	for _, tc := range []struct {
		l    Layout
		want string
	}{
		{NoLayout, "NoLayout"},
		{XY, "XY"},
		{XYZ, "XYZ"},
		{XYM, "XYM"},
		{XYZM, "XYZM"},
		{Layout(5), "Layout(5)"},
	} {
		if got := tc.l.String(); got != tc.want {
			t.Errorf("%#v.String() == %v, want %v", tc.l, got, tc.want)
		}
	}
}

func TestVerify(t *testing.T) {
	for _, tc := range []struct {
		v interface {
			verify() error
		}
		want error
	}{
		{
			&geom0{},
			nil,
		},
		{
			&geom0{NoLayout, 0, Coord{0, 0}, 0},
			errNonEmptyFlatCoords,
		},
		{
			&geom0{XY, 1, Coord{0, 0}, 0},
			errStrideLayoutMismatch,
		},
		{
			&geom0{XY, 2, Coord{0}, 0},
			errLengthStrideMismatch,
		},
		{
			&geom1{},
			nil,
		},
		{
			&geom1{geom0{NoLayout, 0, Coord{0}, 0}},
			errNonEmptyFlatCoords,
		},
		{
			&geom1{geom0{XY, 1, Coord{0, 0}, 0}},
			errStrideLayoutMismatch,
		},
		{
			&geom1{geom0{XY, 2, Coord{0}, 0}},
			errLengthStrideMismatch,
		},
		{
			&geom2{},
			nil,
		},
		{
			&geom2{geom1{geom0{NoLayout, 0, Coord{0}, 0}}, []int{}},
			errNonEmptyFlatCoords,
		},
		{
			&geom2{geom1{geom0{NoLayout, 0, Coord{}, 0}}, []int{4}},
			errNonEmptyEnds,
		},
		{
			&geom2{geom1{geom0{XY, 2, Coord{0}, 0}}, []int{4}},
			errLengthStrideMismatch,
		},
		{
			&geom2{geom1{geom0{XY, 1, Coord{0, 0, 0, 0}, 0}}, []int{-1}},
			errStrideLayoutMismatch,
		},
		{
			&geom2{geom1{geom0{XY, 2, Coord{0, 0, 0, 0}, 0}}, []int{-1}},
			errMisalignedEnd,
		},
		{
			&geom2{geom1{geom0{XY, 2, Coord{0, 0, 0, 0}, 0}}, []int{3}},
			errMisalignedEnd,
		},
		{
			&geom2{geom1{geom0{XY, 2, Coord{0, 0, 0, 0, 0, 0, 0, 0}, 0}}, []int{8, 4}},
			errOutOfOrderEnd,
		},
		{
			&geom2{geom1{geom0{XY, 2, Coord{0, 0, 0, 0, 0, 0, 0, 0}, 0}}, []int{4, 4}},
			errIncorrectEnd,
		},
		{
			&geom2{geom1{geom0{XY, 2, Coord{0, 0, 0, 0, 0, 0, 0, 0}, 0}}, []int{4, 12}},
			errIncorrectEnd,
		},
		{
			&geom3{},
			nil,
		},
		// FIXME add more geom3 test cases
	} {
		if got := tc.v.verify(); got != tc.want {
			t.Errorf("%#v.verify() == %v, want %v", tc.v, got, tc.want)
		}
	}
}

func TestEqualCoords(t *testing.T) {
	for _, tc := range []struct {
		c1, c2 Coord
		layout Layout
		equal  bool
	}{
		{
			c1:     Coord{},
			c2:     Coord{0, 0},
			layout: XY,
			equal:  false,
		},
		{
			c1:     Coord{},
			c2:     Coord{},
			layout: XY,
			equal:  true,
		},
		{
			c1:     Coord{1, 0},
			c2:     Coord{},
			layout: XY,
			equal:  false,
		},
		{
			c1:     Coord{1, 0},
			c2:     Coord{1},
			layout: XY,
			equal:  false,
		},
		{
			c1:     Coord{1},
			c2:     Coord{},
			layout: XY,
			equal:  false,
		},
		{
			c1:     Coord{1},
			c2:     Coord{1},
			layout: XY,
			equal:  true,
		},
		{
			c1:     Coord{1},
			c2:     Coord{0},
			layout: XY,
			equal:  false,
		},
		{
			c1:     Coord{0, 0},
			c2:     Coord{0, 0},
			layout: XY,
			equal:  true,
		},
		{
			c1:     Coord{0, 0},
			c2:     Coord{1, 0},
			layout: XY,
			equal:  false,
		},
		{
			c1:     Coord{0, 1},
			c2:     Coord{0, 0},
			layout: XY,
			equal:  false,
		},
		{
			c1:     Coord{0, 0, 3},
			c2:     Coord{0, 0},
			layout: XY,
			equal:  true,
		},
		{
			c1:     Coord{0, 0, 3},
			c2:     Coord{0, 0, 3},
			layout: XYZ,
			equal:  true,
		},
		{
			c1:     Coord{0, 0, 3},
			c2:     Coord{0, 0, 4},
			layout: XYZ,
			equal:  false,
		},
		{
			c1:     Coord{0, 0, 3, 4, 5, 6, 7, 8, 9, 10},
			c2:     Coord{0, 0, 3, 4, 5, 6, 7, 8, 9, 10},
			layout: Layout(10),
			equal:  true,
		},
		{
			c1:     Coord{0, 0, 3, 4, 5, 6, 7, 8, 9, 10},
			c2:     Coord{0, 0, 3, 4, 5, 6, 8, 8, 9, 10},
			layout: Layout(10),
			equal:  false,
		},
	} {
		if tc.c1.Equal(tc.layout, tc.c2) != tc.equal {
			t.Errorf("%v.Equals(%s, %v) is not '%v'", tc.c1, tc.layout, tc.c2, tc.equal)
		}
	}
}
func TestSetCoord(t *testing.T) {
	for _, tc := range []struct {
		src, dest Coord
		expected  Coord
		layout    Layout
	}{
		{
			src:      Coord{0, 0},
			dest:     Coord{1, 1},
			expected: Coord{0, 0},
			layout:   XY,
		},
		{
			src:      Coord{1, 0},
			dest:     Coord{},
			expected: Coord{},
			layout:   Layout(0),
		},
		{
			src:      Coord{},
			dest:     Coord{1, 2},
			expected: Coord{1, 2},
			layout:   XY,
		},
		{
			src:      Coord{3},
			dest:     Coord{1, 2},
			expected: Coord{3, 2},
			layout:   XY,
		},
	} {

		tc.dest.Set(tc.src)
		if !tc.dest.Equal(tc.layout, tc.expected) {
			t.Errorf("Setting %v with %v did not result in %v", tc.dest, tc.src, tc.dest)
		}
	}
}
<commit_msg>Add more geom3 test cases<commit_after>package geom

import (
	"reflect"
	"testing"
)

var (
	_ = []T{
		&LineString{},
		&LinearRing{},
		&MultiLineString{},
		&MultiPoint{},
		&MultiPolygon{},
		&Point{},
		&Polygon{},
	}
	_ = []interface {
		Area() float64
		Empty() bool
		Length() float64
	}{
		&LineString{},
		&LinearRing{},
		&MultiLineString{},
		&MultiPoint{},
		&MultiPolygon{},
		&Point{},
		&Polygon{},
	}
)

func aliases(x, y []float64) bool {
	// http://golang.org/src/pkg/math/big/nat.go#L340
	return cap(x) > 0 && cap(y) > 0 && &x[0:cap(x)][cap(x)-1] == &y[0:cap(y)][cap(y)-1]
}

func TestSet(t *testing.T) {
	for _, tc := range []struct {
		c, other, want Coord
	}{
		{Coord{1.0, 2.0}, Coord{2.0, 3.0}, Coord{2.0, 3.0}},
		{Coord{1.0, 2.0, 3.0}, Coord{2.0, 3.0, 4.0}, Coord{2.0, 3.0, 4.0}},
		{Coord{1.0, 2.0}, Coord{2.0, 3.0, 4.0}, Coord{2.0, 3.0}},
		{Coord{1.0, 2.0, 3.0}, Coord{2.0, 3.0}, Coord{2.0, 3.0, 3.0}},
	} {
		if tc.c.Set(tc.other); !reflect.DeepEqual(tc.c, tc.want) {
			t.Errorf("%v.Set(%v); got %v, want %v", tc.c, tc.other, tc.c, tc.want)
		}
	}
}

func TestLayoutString(t *testing.T) {
	for _, tc := range []struct {
		l    Layout
		want string
	}{
		{NoLayout, "NoLayout"},
		{XY, "XY"},
		{XYZ, "XYZ"},
		{XYM, "XYM"},
		{XYZM, "XYZM"},
		{Layout(5), "Layout(5)"},
	} {
		if got := tc.l.String(); got != tc.want {
			t.Errorf("%#v.String() == %v, want %v", tc.l, got, tc.want)
		}
	}
}

func TestVerify(t *testing.T) {
	for _, tc := range []struct {
		v interface {
			verify() error
		}
		want error
	}{
		{
			&geom0{},
			nil,
		},
		{
			&geom0{NoLayout, 0, Coord{0, 0}, 0},
			errNonEmptyFlatCoords,
		},
		{
			&geom0{XY, 1, Coord{0, 0}, 0},
			errStrideLayoutMismatch,
		},
		{
			&geom0{XY, 2, Coord{0}, 0},
			errLengthStrideMismatch,
		},
		{
			&geom1{},
			nil,
		},
		{
			&geom1{geom0{NoLayout, 0, Coord{0}, 0}},
			errNonEmptyFlatCoords,
		},
		{
			&geom1{geom0{XY, 1, Coord{0, 0}, 0}},
			errStrideLayoutMismatch,
		},
		{
			&geom1{geom0{XY, 2, Coord{0}, 0}},
			errLengthStrideMismatch,
		},
		{
			&geom2{},
			nil,
		},
		{
			&geom2{geom1{geom0{NoLayout, 0, Coord{0}, 0}}, []int{}},
			errNonEmptyFlatCoords,
		},
		{
			&geom2{geom1{geom0{NoLayout, 0, Coord{}, 0}}, []int{4}},
			errNonEmptyEnds,
		},
		{
			&geom2{geom1{geom0{XY, 2, Coord{0}, 0}}, []int{4}},
			errLengthStrideMismatch,
		},
		{
			&geom2{geom1{geom0{XY, 1, Coord{0, 0, 0, 0}, 0}}, []int{-1}},
			errStrideLayoutMismatch,
		},
		{
			&geom2{geom1{geom0{XY, 2, Coord{0, 0, 0, 0}, 0}}, []int{-1}},
			errMisalignedEnd,
		},
		{
			&geom2{geom1{geom0{XY, 2, Coord{0, 0, 0, 0}, 0}}, []int{3}},
			errMisalignedEnd,
		},
		{
			&geom2{geom1{geom0{XY, 2, Coord{0, 0, 0, 0, 0, 0, 0, 0}, 0}}, []int{8, 4}},
			errOutOfOrderEnd,
		},
		{
			&geom2{geom1{geom0{XY, 2, Coord{0, 0, 0, 0, 0, 0, 0, 0}, 0}}, []int{4, 4}},
			errIncorrectEnd,
		},
		{
			&geom2{geom1{geom0{XY, 2, Coord{0, 0, 0, 0, 0, 0, 0, 0}, 0}}, []int{4, 12}},
			errIncorrectEnd,
		},
		{
			&geom3{},
			nil,
		},
		{
			&geom3{geom1{geom0{XY, 3, Coord{}, 0}}, [][]int{}},
			errStrideLayoutMismatch,
		},
		{
			&geom3{geom1{geom0{NoLayout, 0, Coord{0}, 0}}, [][]int{}},
			errNonEmptyFlatCoords,
		},
		{
			&geom3{geom1{geom0{NoLayout, 0, Coord{}, 0}}, [][]int{{0}}},
			errNonEmptyEndss,
		},
		{
			&geom3{geom1{geom0{XY, 2, Coord{0}, 0}}, [][]int{}},
			errLengthStrideMismatch,
		},
		{
			&geom3{geom1{geom0{XY, 2, Coord{0, 0}, 0}}, [][]int{{1}}},
			errMisalignedEnd,
		},
		{
			&geom3{geom1{geom0{XY, 2, Coord{0, 0, 0, 0}, 0}}, [][]int{{4, 2}}},
			errOutOfOrderEnd,
		},
		{
			&geom3{geom1{geom0{XY, 2, Coord{0, 0, 0, 0}, 0}}, [][]int{{2}}},
			errIncorrectEnd,
		},
	} {
		if got := tc.v.verify(); got != tc.want {
			t.Errorf("%#v.verify() == %v, want %v", tc.v, got, tc.want)
		}
	}
}

func TestEqualCoords(t *testing.T) {
	for _, tc := range []struct {
		c1, c2 Coord
		layout Layout
		equal  bool
	}{
		{
			c1:     Coord{},
			c2:     Coord{0, 0},
			layout: XY,
			equal:  false,
		},
		{
			c1:     Coord{},
			c2:     Coord{},
			layout: XY,
			equal:  true,
		},
		{
			c1:     Coord{1, 0},
			c2:     Coord{},
			layout: XY,
			equal:  false,
		},
		{
			c1:     Coord{1, 0},
			c2:     Coord{1},
			layout: XY,
			equal:  false,
		},
		{
			c1:     Coord{1},
			c2:     Coord{},
			layout: XY,
			equal:  false,
		},
		{
			c1:     Coord{1},
			c2:     Coord{1},
			layout: XY,
			equal:  true,
		},
		{
			c1:     Coord{1},
			c2:     Coord{0},
			layout: XY,
			equal:  false,
		},
		{
			c1:     Coord{0, 0},
			c2:     Coord{0, 0},
			layout: XY,
			equal:  true,
		},
		{
			c1:     Coord{0, 0},
			c2:     Coord{1, 0},
			layout: XY,
			equal:  false,
		},
		{
			c1:     Coord{0, 1},
			c2:     Coord{0, 0},
			layout: XY,
			equal:  false,
		},
		{
			c1:     Coord{0, 0, 3},
			c2:     Coord{0, 0},
			layout: XY,
			equal:  true,
		},
		{
			c1:     Coord{0, 0, 3},
			c2:     Coord{0, 0, 3},
			layout: XYZ,
			equal:  true,
		},
		{
			c1:     Coord{0, 0, 3},
			c2:     Coord{0, 0, 4},
			layout: XYZ,
			equal:  false,
		},
		{
			c1:     Coord{0, 0, 3, 4, 5, 6, 7, 8, 9, 10},
			c2:     Coord{0, 0, 3, 4, 5, 6, 7, 8, 9, 10},
			layout: Layout(10),
			equal:  true,
		},
		{
			c1:     Coord{0, 0, 3, 4, 5, 6, 7, 8, 9, 10},
			c2:     Coord{0, 0, 3, 4, 5, 6, 8, 8, 9, 10},
			layout: Layout(10),
			equal:  false,
		},
	} {
		if tc.c1.Equal(tc.layout, tc.c2) != tc.equal {
			t.Errorf("%v.Equals(%s, %v) is not '%v'", tc.c1, tc.layout, tc.c2, tc.equal)
		}
	}
}
func TestSetCoord(t *testing.T) {
	for _, tc := range []struct {
		src, dest Coord
		expected  Coord
		layout    Layout
	}{
		{
			src:      Coord{0, 0},
			dest:     Coord{1, 1},
			expected: Coord{0, 0},
			layout:   XY,
		},
		{
			src:      Coord{1, 0},
			dest:     Coord{},
			expected: Coord{},
			layout:   Layout(0),
		},
		{
			src:      Coord{},
			dest:     Coord{1, 2},
			expected: Coord{1, 2},
			layout:   XY,
		},
		{
			src:      Coord{3},
			dest:     Coord{1, 2},
			expected: Coord{3, 2},
			layout:   XY,
		},
	} {

		tc.dest.Set(tc.src)
		if !tc.dest.Equal(tc.layout, tc.expected) {
			t.Errorf("Setting %v with %v did not result in %v", tc.dest, tc.src, tc.dest)
		}
	}
}
<|endoftext|>


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

package commands

import (
	"path/filepath"
	"reflect"
	"testing"

	"nvim-go/context"
	"nvim-go/nvimutil"

	"github.com/neovim/go-client/nvim"
)

var testVetRoot = filepath.Join(testGoPath, "src", "vet")

func TestCommands_Vet(t *testing.T) {
	type fields struct {
		Nvim     *nvim.Nvim
		Pipeline *nvim.Pipeline
		Batch    *nvim.Batch
		ctxt     *context.Context
	}
	type args struct {
		args []string
		eval *CmdVetEval
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    []*nvim.QuickfixError
		wantErr bool
	}{
		{
			name: "method.go(2 suggest)",
			fields: fields{
				Nvim: nvimutil.TestNvim(t, filepath.Join(testVetRoot, "method.go")),
			},
			args: args{
				args: []string{"method.go"},
				eval: &CmdVetEval{
					Cwd:  testVetRoot,
					File: filepath.Join(testVetRoot, "method.go"),
				},
			},
			want: []*nvim.QuickfixError{&nvim.QuickfixError{
				FileName: "method.go",
				LNum:     17,
				Col:      0,
				Text:     "method Scan(x fmt.ScanState, c byte) should have signature Scan(fmt.ScanState, rune) error",
			}, &nvim.QuickfixError{
				FileName: "method.go",
				LNum:     21,
				Col:      0,
				Text:     "method ReadByte() byte should have signature ReadByte() (byte, error)",
			}},
			wantErr: false,
		},
		{
			name: "method.go + unused.go(8 suggest)",
			fields: fields{
				Nvim: nvimutil.TestNvim(t, filepath.Join(testVetRoot, "unused.go")),
			},
			args: args{
				args: []string{"."},
				eval: &CmdVetEval{
					Cwd:  testVetRoot,
					File: filepath.Join(testVetRoot, "unused.go"),
				},
			},
			want: []*nvim.QuickfixError{&nvim.QuickfixError{
				FileName: "method.go",
				LNum:     17,
				Col:      0,
				Text:     "method Scan(x fmt.ScanState, c byte) should have signature Scan(fmt.ScanState, rune) error",
			}, &nvim.QuickfixError{
				FileName: "method.go",
				LNum:     21,
				Col:      0,
				Text:     "method ReadByte() byte should have signature ReadByte() (byte, error)",
			}, &nvim.QuickfixError{
				FileName: "unused.go",
				LNum:     16,
				Col:      0,
				Text:     "result of fmt.Errorf call not used",
			}, &nvim.QuickfixError{
				FileName: "unused.go",
				LNum:     19,
				Col:      0,
				Text:     "result of errors.New call not used",
			}, &nvim.QuickfixError{
				FileName: "unused.go",
				LNum:     22,
				Col:      0,
				Text:     "result of (error).Error call not used",
			}, &nvim.QuickfixError{
				FileName: "unused.go",
				LNum:     25,
				Col:      0,
				Text:     "result of (bytes.Buffer).String call not used",
			}, &nvim.QuickfixError{
				FileName: "unused.go",
				LNum:     27,
				Col:      0,
				Text:     "result of fmt.Sprint call not used",
			}, &nvim.QuickfixError{
				FileName: "unused.go",
				LNum:     28,
				Col:      0,
				Text:     "result of fmt.Sprintf call not used",
			}},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		tt.fields.ctxt = context.NewContext()
		c := NewCommands(tt.fields.Nvim, tt.fields.ctxt)
		got, err := c.Vet(tt.args.args, tt.args.eval)
		if (err != nil) != tt.wantErr {
			t.Errorf("%q. Commands.Vet(%v, %v) error = %v, wantErr %v", tt.name, tt.args.args, tt.args.eval, err, tt.wantErr)
			continue
		}
		if !reflect.DeepEqual(got, tt.want) {
			t.Errorf("%q. Commands.Vet(%v, %v) = %v, want %v", tt.name, tt.args.args, tt.args.eval, got, tt.want)
		}
	}
}
<commit_msg>test/commands: Fix not set Build.Tool for vet test & Use subtest<commit_after>// Copyright 2016 The nvim-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.

package commands

import (
	"path/filepath"
	"reflect"
	"testing"

	"nvim-go/context"
	"nvim-go/nvimutil"

	"github.com/neovim/go-client/nvim"
)

var testVetRoot = filepath.Join(testGoPath, "src", "vet")

func TestCommands_Vet(t *testing.T) {
	type fields struct {
		Nvim     *nvim.Nvim
		Pipeline *nvim.Pipeline
		Batch    *nvim.Batch
		ctxt     *context.Context
	}
	type args struct {
		args []string
		eval *CmdVetEval
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    []*nvim.QuickfixError
		wantErr bool
		tool    string
	}{
		// method.go:17: method Scan(x fmt.ScanState, c byte) should have signature Scan(fmt.ScanState, rune) error
		// method.go:21: method ReadByte() byte should have signature ReadByte() (byte, error)
		{
			name: "method.go (2 suggest)",
			fields: fields{
				Nvim: nvimutil.TestNvim(t, filepath.Join(testVetRoot, "method.go")),
				ctxt: context.NewContext(),
			},
			args: args{
				args: []string{"method.go"},
				eval: &CmdVetEval{
					Cwd:  testVetRoot,
					File: filepath.Join(testVetRoot, "method.go"),
				},
			},
			want: []*nvim.QuickfixError{&nvim.QuickfixError{
				FileName: "method.go",
				LNum:     17,
				Col:      0,
				Text:     "method Scan(x fmt.ScanState, c byte) should have signature Scan(fmt.ScanState, rune) error",
			}, &nvim.QuickfixError{
				FileName: "method.go",
				LNum:     21,
				Col:      0,
				Text:     "method ReadByte() byte should have signature ReadByte() (byte, error)",
			}},
			wantErr: false,
			tool:    "go",
		},

		// method.go:17: method Scan(x fmt.ScanState, c byte) should have signature Scan(fmt.ScanState, rune) error
		// method.go:21: method ReadByte() byte should have signature ReadByte() (byte, error)
		// unused.go:16: result of fmt.Errorf call not used
		// unused.go:19: result of errors.New call not used
		// unused.go:22: result of (error).Error call not used
		// unused.go:25: result of (bytes.Buffer).String call not used
		// unused.go:27: result of fmt.Sprint call not used
		// unused.go:28: result of fmt.Sprintf call not used
		{
			name: "method.go and unused.go(8 suggest)",
			fields: fields{
				Nvim: nvimutil.TestNvim(t, filepath.Join(testVetRoot, "unused.go")),
				ctxt: context.NewContext(),
			},
			args: args{
				args: []string{"."},
				eval: &CmdVetEval{
					Cwd:  testVetRoot,
					File: filepath.Join(testVetRoot, "unused.go"),
				},
			},
			want: []*nvim.QuickfixError{&nvim.QuickfixError{
				FileName: "method.go",
				LNum:     17,
				Col:      0,
				Text:     "method Scan(x fmt.ScanState, c byte) should have signature Scan(fmt.ScanState, rune) error",
			}, &nvim.QuickfixError{
				FileName: "method.go",
				LNum:     21,
				Col:      0,
				Text:     "method ReadByte() byte should have signature ReadByte() (byte, error)",
			}, &nvim.QuickfixError{
				FileName: "unused.go",
				LNum:     16,
				Col:      0,
				Text:     "result of fmt.Errorf call not used",
			}, &nvim.QuickfixError{
				FileName: "unused.go",
				LNum:     19,
				Col:      0,
				Text:     "result of errors.New call not used",
			}, &nvim.QuickfixError{
				FileName: "unused.go",
				LNum:     22,
				Col:      0,
				Text:     "result of (error).Error call not used",
			}, &nvim.QuickfixError{
				FileName: "unused.go",
				LNum:     25,
				Col:      0,
				Text:     "result of (bytes.Buffer).String call not used",
			}, &nvim.QuickfixError{
				FileName: "unused.go",
				LNum:     27,
				Col:      0,
				Text:     "result of fmt.Sprint call not used",
			}, &nvim.QuickfixError{
				FileName: "unused.go",
				LNum:     28,
				Col:      0,
				Text:     "result of fmt.Sprintf call not used",
			}},
			wantErr: false,
			tool:    "go",
		},
	}
	for _, tt := range tests {
		tt.fields.ctxt.Build.Tool = tt.tool
		c := NewCommands(tt.fields.Nvim, tt.fields.ctxt)

		t.Run(tt.name, func(t *testing.T) {
			got, err := c.Vet(tt.args.args, tt.args.eval)
			if (err != nil) != tt.wantErr {
				t.Errorf("%q. Commands.Vet(%v, %v) error = %v, wantErr %v", tt.name, tt.args.args, tt.args.eval, err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("%q. Commands.Vet(%v, %v) =", tt.name, tt.args.args, tt.args.eval)
				for _, g := range got {
					t.Errorf("%+v", g)
				}
				t.Error("want =")
				for _, w := range tt.want {
					t.Errorf("%+v", w)
				}
			}
		})
	}
}
<|endoftext|>


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

package nvimutil

import (
	"fmt"
	"strings"

	"nvim-go/config"
	"nvim-go/pathutil"

	"github.com/neovim/go-client/nvim"
)

var pkgTerminal = "GoTerminal"

var bufName = "__GO_TERMINAL__"

// Terminal represents a Neovim terminal.
type Terminal struct {
	v *nvim.Nvim
	p *nvim.Pipeline

	cmd  []string
	mode string
	// Name terminal buffer name.
	Name string
	// Dir specifies the working directory of the command on terminal.
	Dir string
	// Size open the terminal window size.
	Size int

	cw nvim.Window

	*Buf
}

// NewTerminal return the Neovim terminal buffer.
func NewTerminal(vim *nvim.Nvim, name string, command []string, mode string) *Terminal {
	return &Terminal{
		v:    vim,
		p:    vim.NewPipeline(),
		Name: name,
		cmd:  command,
		mode: mode,
	}
}

// Create creats the new Neovim terminal buffer.
func (t *Terminal) Create() (err error) {
	t.cw, err = t.v.CurrentWindow()
	if err != nil {
		return err
	}

	switch {
	case t.mode == "split":
		t.Size = int(config.TerminalHeight)
	case t.mode == "vsplit":
		t.Size = int(config.TerminalWidth)
	default:
		// nothing to do
	}

	option := t.setTerminalOption()
	name := fmt.Sprintf("| terminal %s", strings.Join(t.cmd, " "))
	mode := fmt.Sprintf("%s %d%s", config.TerminalPosition, t.Size, t.mode)
	t.Buf = NewBuffer(t.v)
	t.Buf.Create(name, FiletypeTerminal, mode, option)
	t.Buf.Name = t.Name
	t.Buf.UpdateSyntax(FiletypeTerminal)

	// Get terminal buffer and windows information.
	t.p.CurrentBuffer(&t.Buffer)
	t.p.CurrentWindow(&t.Window)
	if err := t.p.Wait(); err != nil {
		return err
	}
	defer t.switchFocus()()

	// Cleanup cursor highlighting
	// TODO(zchee): Can use p.ClearBufferHighlight?
	t.p.Command("highlight TermCursor gui=NONE guifg=NONE guibg=NONE")
	t.p.Command("highlight TermCursorNC gui=NONE guifg=NONE guibg=NONE")

	// Set autoclose buffer if the current buffer is only terminal
	// TODO(zchee): convert to rpc way
	t.p.Command("autocmd WinEnter <buffer> if winnr('$') == 1 | quit | endif")

	return t.p.Wait()
}

// Run runs the command in the terminal buffer.
func (t *Terminal) Run(cmd []string) error {
	if t.Dir != "" {
		defer pathutil.Chdir(t.v, t.Dir)()
	}

	if t.Buf != nil && IsBufferValid(t.v, t.Buffer) {
		defer t.switchFocus()()

		t.v.SetBufferOption(t.Buffer, BufOptionModified, false)
		t.v.Call("termopen", nil, cmd)
		t.v.SetBufferName(t.Buffer, t.Buf.Name)
	} else {
		t.Create()
	}
	// Workaround for "autocmd BufEnter term://* startinsert"
	if config.TerminalStopInsert {
		t.v.Command("stopinsert")
	}

	return nil
}

// TODO(zchee): flashing when switch the window.
func (t *Terminal) switchFocus() func() {
	t.v.SetCurrentWindow(t.Window)

	return func() {
		t.v.SetCurrentWindow(t.cw)
	}
}

func (t *Terminal) setTerminalOption() map[NvimOption]map[string]interface{} {
	option := make(map[NvimOption]map[string]interface{})
	bufoption := make(map[string]interface{})
	bufvar := make(map[string]interface{})
	windowoption := make(map[string]interface{})

	bufoption[BufOptionBufhidden] = BufhiddenDelete
	bufoption[BufOptionBuflisted] = false
	bufoption[BufOptionBuftype] = BuftypeNofile
	bufoption[BufOptionFiletype] = FiletypeTerminal
	bufoption[BufOptionModifiable] = false
	bufoption[BufOptionSwapfile] = false

	bufvar[BufVarColorcolumn] = ""

	windowoption[WinOptionList] = false
	windowoption[WinOptionNumber] = false
	windowoption[WinOptionRelativenumber] = false
	windowoption[WinOptionWinfixheight] = true

	option[BufferOption] = bufoption
	option[BufferVar] = bufvar
	option[WindowOption] = windowoption

	return option
}
<commit_msg>nvimutil/terminal: add getWindowSize function<commit_after>// Copyright 2016 The nvim-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.

package nvimutil

import (
	"fmt"
	"strings"

	"nvim-go/config"
	"nvim-go/pathutil"

	"github.com/neovim/go-client/nvim"
)

var pkgTerminal = "GoTerminal"

var bufName = "__GO_TERMINAL__"

// Terminal represents a Neovim terminal.
type Terminal struct {
	v *nvim.Nvim
	p *nvim.Pipeline

	cmd  []string
	mode string
	// Name terminal buffer name.
	Name string
	// Dir specifies the working directory of the command on terminal.
	Dir string
	// Size open the terminal window size.
	Size int

	cw nvim.Window

	*Buf
}

// NewTerminal return the Neovim terminal buffer.
func NewTerminal(vim *nvim.Nvim, name string, command []string, mode string) *Terminal {
	return &Terminal{
		v:    vim,
		p:    vim.NewPipeline(),
		Name: name,
		cmd:  command,
		mode: mode,
	}
}

// Create creats the new Neovim terminal buffer.
func (t *Terminal) Create() (err error) {
	t.cw, err = t.v.CurrentWindow()
	if err != nil {
		return err
	}

	switch {
	case t.mode == "split":
		t.Size = t.getWindowSize(config.TerminalHeight, t.v.WindowHeight)
	case t.mode == "vsplit":
		t.Size = t.getWindowSize(config.TerminalWidth, t.v.WindowWidth)
	default:
		// nothing to do
	}

	option := t.setTerminalOption()
	name := fmt.Sprintf("| terminal %s", strings.Join(t.cmd, " "))
	mode := fmt.Sprintf("%s %d%s", config.TerminalPosition, t.Size, t.mode)
	t.Buf = NewBuffer(t.v)
	t.Buf.Create(name, FiletypeTerminal, mode, option)
	t.Buf.Name = t.Name
	t.Buf.UpdateSyntax(FiletypeTerminal)

	// Get terminal buffer and windows information.
	t.p.CurrentBuffer(&t.Buffer)
	t.p.CurrentWindow(&t.Window)
	if err := t.p.Wait(); err != nil {
		return err
	}
	defer t.switchFocus()()

	// Cleanup cursor highlighting
	// TODO(zchee): Can use p.ClearBufferHighlight?
	t.p.Command("highlight TermCursor gui=NONE guifg=NONE guibg=NONE")
	t.p.Command("highlight TermCursorNC gui=NONE guifg=NONE guibg=NONE")

	// Set autoclose buffer if the current buffer is only terminal
	// TODO(zchee): convert to rpc way
	t.p.Command("autocmd WinEnter <buffer> if winnr('$') == 1 | quit | endif")

	return t.p.Wait()
}

// Run runs the command in the terminal buffer.
func (t *Terminal) Run(cmd []string) error {
	if t.Dir != "" {
		defer pathutil.Chdir(t.v, t.Dir)()
	}

	if t.Buf != nil && IsBufferValid(t.v, t.Buffer) {
		defer t.switchFocus()()

		t.v.SetBufferOption(t.Buffer, BufOptionModified, false)
		t.v.Call("termopen", nil, cmd)
		t.v.SetBufferName(t.Buffer, t.Buf.Name)
	} else {
		t.Create()
	}
	// Workaround for "autocmd BufEnter term://* startinsert"
	if config.TerminalStopInsert {
		t.v.Command("stopinsert")
	}

	return nil
}

// getWindowSize return the one third of window (height|width) size if cfg is 0
func (t *Terminal) getWindowSize(cfg int64, fn func(nvim.Window) (int, error)) int {
	if cfg == 0 {
		i, err := fn(t.cw)
		if err != nil {
			return 0
		}
		return i / 3
	}
	return int(cfg)
}

// TODO(zchee): flashing when switch the window.
func (t *Terminal) switchFocus() func() {
	t.v.SetCurrentWindow(t.Window)

	return func() {
		t.v.SetCurrentWindow(t.cw)
	}
}

func (t *Terminal) setTerminalOption() map[NvimOption]map[string]interface{} {
	option := make(map[NvimOption]map[string]interface{})
	bufoption := make(map[string]interface{})
	bufvar := make(map[string]interface{})
	windowoption := make(map[string]interface{})

	bufoption[BufOptionBufhidden] = BufhiddenDelete
	bufoption[BufOptionBuflisted] = false
	bufoption[BufOptionBuftype] = BuftypeNofile
	bufoption[BufOptionFiletype] = FiletypeTerminal
	bufoption[BufOptionModifiable] = false
	bufoption[BufOptionSwapfile] = false

	bufvar[BufVarColorcolumn] = ""

	windowoption[WinOptionList] = false
	windowoption[WinOptionNumber] = false
	windowoption[WinOptionRelativenumber] = false
	windowoption[WinOptionWinfixheight] = true

	option[BufferOption] = bufoption
	option[BufferVar] = bufvar
	option[WindowOption] = windowoption

	return option
}
<|endoftext|>


<commit_before>package coal

import "gopkg.in/mgo.v2/bson"

// C is a short-hand function to extract the collection of a model.
func C(m Model) string {
	return Init(m).Meta().Collection
}

// F is a short-hand function to extract the database BSON field name of a model
// field. F will panic if no field has been found.
func F(m Model, field string) string {
	// find field
	f := Init(m).Meta().Fields[field]
	if f == nil {
		panic(`coal: field "` + field + `" not found on "` + m.Meta().Name + `"`)
	}

	return f.BSONField
}

// A is a short-hand function to extract the attribute JSON ley of a model field.
// A will panic if no field has been found.
func A(m Model, field string) string {
	// find field
	f := Init(m).Meta().Fields[field]
	if f == nil {
		panic(`coal: field "` + field + `" not found on "` + m.Meta().Name + `"`)
	}

	return f.JSONKey
}

// R is a short-hand function to extract the relationship name of a model field.
// R will panic if no field has been found.
func R(m Model, field string) string {
	// find field
	f := Init(m).Meta().Fields[field]
	if f == nil {
		panic(`coal: field "` + field + `" not found on "` + m.Meta().Name + `"`)
	}

	return f.RelName
}

// P is a short-hand function to get a pointer of the passed id.
func P(id bson.ObjectId) *bson.ObjectId {
	return &id
}

// N is a short-hand function to get a typed nil for an id pointer.
func N() *bson.ObjectId {
	return nil
}

// Unique is a helper to get a unique list of object ids.
func Unique(ids []bson.ObjectId) []bson.ObjectId {
	// prepare map
	m := make(map[bson.ObjectId]bool)
	l := make([]bson.ObjectId, 0, len(ids))

	for _, id := range ids {
		if _, ok := m[id]; !ok {
			m[id] = true
			l = append(l, id)
		}
	}

	return l
}
<commit_msg>updated docs<commit_after>package coal

import "gopkg.in/mgo.v2/bson"

// C is a short-hand function to extract the collection of a model.
func C(m Model) string {
	return Init(m).Meta().Collection
}

// F is a short-hand function to extract the database BSON field name of a model
// field. F will panic if no field has been found.
func F(m Model, field string) string {
	// find field
	f := Init(m).Meta().Fields[field]
	if f == nil {
		panic(`coal: field "` + field + `" not found on "` + m.Meta().Name + `"`)
	}

	return f.BSONField
}

// A is a short-hand function to extract the attribute JSON ley of a model field.
// A will panic if no field has been found.
func A(m Model, field string) string {
	// find field
	f := Init(m).Meta().Fields[field]
	if f == nil {
		panic(`coal: field "` + field + `" not found on "` + m.Meta().Name + `"`)
	}

	return f.JSONKey
}

// R is a short-hand function to extract the relationship name of a model field.
// R will panic if no field has been found.
func R(m Model, field string) string {
	// find field
	f := Init(m).Meta().Fields[field]
	if f == nil {
		panic(`coal: field "` + field + `" not found on "` + m.Meta().Name + `"`)
	}

	return f.RelName
}

// P is a short-hand function to get a pointer of the passed object id.
func P(id bson.ObjectId) *bson.ObjectId {
	return &id
}

// N is a short-hand function to get a typed nil object id pointer.
func N() *bson.ObjectId {
	return nil
}

// Unique is a helper to get a unique list of object ids.
func Unique(ids []bson.ObjectId) []bson.ObjectId {
	// prepare map
	m := make(map[bson.ObjectId]bool)
	l := make([]bson.ObjectId, 0, len(ids))

	for _, id := range ids {
		if _, ok := m[id]; !ok {
			m[id] = true
			l = append(l, id)
		}
	}

	return l
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	trakt "github.com/42minutes/go-trakt"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type TestAuthMethod struct{}

func (t TestAuthMethod) String() string {
	return "god"
}

type mockFileInfo struct {
	name string
}

func (m mockFileInfo) Name() string       { return m.name }
func (m mockFileInfo) Size() int64        { return 1 }
func (m mockFileInfo) Mode() os.FileMode  { return os.ModeDir }
func (m mockFileInfo) ModTime() time.Time { return time.Time{} }
func (m mockFileInfo) IsDir() bool        { return true }
func (m mockFileInfo) Sys() interface{}   { return nil }

func readTestData(file string) string {
	data, err := ioutil.ReadFile(file)
	if err != nil {
		fmt.Printf("err %+v\n", err)
		os.Exit(1)
	}
	return string(data)
}

func mockClient(URL string) Trakt {
	client := Trakt{
		trakt.NewClientWith(
			URL,
			"",               // userAgent
			"",               // apiKey
			TestAuthMethod{}, // authMethod
			nil,              // http.Client
		),
	}

	return client
}

func TestDirsToShows(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintln(w, readTestData("testdata/searchResults.json"))
	}))
	defer ts.Close()

	client := mockClient(ts.URL)

	shows := client.turnDirsIntoShows([]os.FileInfo{mockFileInfo{}})
	assert.NotEmpty(t, shows)
}

func TestShowResultsToShows(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintln(w, readTestData("testdata/showDetails.json"))
	}))
	defer ts.Close()

	client := mockClient(ts.URL)

	r := trakt.ShowResult{}
	shows := client.turnShowResultsIntoShows(
		map[os.FileInfo]trakt.ShowResult{
			mockFileInfo{}: r,
		},
	)
	assert.NotEmpty(t, shows)
}

func TestAnnotateShows(t *testing.T) {
	stack := []string{
		"testdata/seasons.json",
		"testdata/episodes_1.json",
	}
	var f string
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		f, stack = stack[0], stack[1:len(stack)] // *POP*
		fmt.Fprintln(w, readTestData(f))
	}))
	defer ts.Close()

	client := mockClient(ts.URL)

	shows := map[os.FileInfo]FullShow{
		mockFileInfo{}: FullShow{},
	}
	client.addSeasonsAndEpisodesToShows(shows)
	show := shows[mockFileInfo{}]
	assert.Len(t, show.seasons, 1)
	assert.Len(t, show.seasons[0].episodes, 10)
}

func TestShowFiles(t *testing.T) {
	copyR("testdata/Videos_template", "testdata/Videos")
	defer os.RemoveAll("testdata/Videos")
	err := createJSONs()
	require.NoError(t, err)

	createdJSONs := []string{
		"testdata/Videos/shows.json",
		"testdata/Videos/Show One.json",
		"testdata/Videos/show1/seasons.json",
		"testdata/Videos/show1/1.json",
		"testdata/Videos/show1/2.json",
		"testdata/Videos/show1/3.json",
		"testdata/Videos/show1/1/episodes.json",
		"testdata/Videos/show1/1/Episode 1.json",
		"testdata/Videos/show1/1/Episode 2.json",
		"testdata/Videos/Show Two.json",
	}

	for _, j := range createdJSONs {
		_, err := os.Stat(j)
		assert.Nil(t, err, "%s should exist", j)
	}
}

func TestShowURLs(t *testing.T) {
	copyR("testdata/Videos_template", "testdata/Videos")
	defer os.RemoveAll("testdata/Videos")
	err := createJSONs()
	require.NoError(t, err)

	data, err := ioutil.ReadFile("testdata/Videos/shows.json")
	require.NoError(t, err)

	var shows []FullShow
	err = json.Unmarshal(data, &shows)
	require.NoError(t, err)

	assert.Equal(t, "testdata/Videos/Show One.json", shows[0].URL)
	assert.Equal(t, "testdata/Videos/Show Two.json", shows[1].URL)
}

func TestSeasonURLs(t *testing.T) {
	copyR("testdata/Videos_template", "testdata/Videos")
	defer os.RemoveAll("testdata/Videos")
	err := createJSONs()
	require.NoError(t, err)

	data, err := ioutil.ReadFile("testdata/Videos/show1/seasons.json")
	require.NoError(t, err)

	var seasons []season
	err = json.Unmarshal(data, &seasons)
	require.NoError(t, err)

	assert.Equal(t, "testdata/Videos/show1/1.json", seasons[0].URL)
	assert.Equal(t, "testdata/Videos/show1/2.json", seasons[1].URL)
}

func createJSONs() error {
	shows := map[os.FileInfo]FullShow{
		mockFileInfo{name: "testdata/Videos/show1"}: FullShow{
			show: trakt.Show{Title: "Show One"},
			seasons: []season{
				{
					// You need to drop the package name to address the embedded field.
					Season: trakt.Season{Number: 1},
					episodes: []trakt.Episode{
						{
							Title: "Episode 1",
						},
						{
							Title: "Episode 2",
						},
					},
				},
				{
					Season: trakt.Season{Number: 2},
				},
				{
					Season: trakt.Season{Number: 3},
				},
			},
		},
		mockFileInfo{name: "testdata/Videos/show2"}: FullShow{
			show: trakt.Show{Title: "Show Two"},
		},
	}

	c := newConjoiner("testdata/Videos")
	return c.createJSONs(shows)
}

func copyR(src, dest string) {
	filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
		target := strings.Replace(path, src, dest, -1)
		if info.IsDir() {
			os.MkdirAll(target, 0755)
		} else {
			copyFile(path, target)
		}
		return nil
	})
}

func copyFile(source string, dest string) (err error) {
	sourcefile, err := os.Open(source)
	if err != nil {
		return err
	}

	defer sourcefile.Close()

	destfile, err := os.Create(dest)
	if err != nil {
		return err
	}

	defer destfile.Close()

	_, err = io.Copy(destfile, sourcefile)
	if err == nil {
		sourceinfo, err := os.Stat(source)
		if err != nil {
			err = os.Chmod(dest, sourceinfo.Mode())
		}

	}

	return
}
<commit_msg>Make the test not rely on ordering.<commit_after>package main

import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	trakt "github.com/42minutes/go-trakt"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type TestAuthMethod struct{}

func (t TestAuthMethod) String() string {
	return "god"
}

type mockFileInfo struct {
	name string
}

func (m mockFileInfo) Name() string       { return m.name }
func (m mockFileInfo) Size() int64        { return 1 }
func (m mockFileInfo) Mode() os.FileMode  { return os.ModeDir }
func (m mockFileInfo) ModTime() time.Time { return time.Time{} }
func (m mockFileInfo) IsDir() bool        { return true }
func (m mockFileInfo) Sys() interface{}   { return nil }

func readTestData(file string) string {
	data, err := ioutil.ReadFile(file)
	if err != nil {
		fmt.Printf("err %+v\n", err)
		os.Exit(1)
	}
	return string(data)
}

func mockClient(URL string) Trakt {
	client := Trakt{
		trakt.NewClientWith(
			URL,
			"",               // userAgent
			"",               // apiKey
			TestAuthMethod{}, // authMethod
			nil,              // http.Client
		),
	}

	return client
}

func TestDirsToShows(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintln(w, readTestData("testdata/searchResults.json"))
	}))
	defer ts.Close()

	client := mockClient(ts.URL)

	shows := client.turnDirsIntoShows([]os.FileInfo{mockFileInfo{}})
	assert.NotEmpty(t, shows)
}

func TestShowResultsToShows(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintln(w, readTestData("testdata/showDetails.json"))
	}))
	defer ts.Close()

	client := mockClient(ts.URL)

	r := trakt.ShowResult{}
	shows := client.turnShowResultsIntoShows(
		map[os.FileInfo]trakt.ShowResult{
			mockFileInfo{}: r,
		},
	)
	assert.NotEmpty(t, shows)
}

func TestAnnotateShows(t *testing.T) {
	stack := []string{
		"testdata/seasons.json",
		"testdata/episodes_1.json",
	}
	var f string
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		f, stack = stack[0], stack[1:len(stack)] // *POP*
		fmt.Fprintln(w, readTestData(f))
	}))
	defer ts.Close()

	client := mockClient(ts.URL)

	shows := map[os.FileInfo]FullShow{
		mockFileInfo{}: FullShow{},
	}
	client.addSeasonsAndEpisodesToShows(shows)
	show := shows[mockFileInfo{}]
	assert.Len(t, show.seasons, 1)
	assert.Len(t, show.seasons[0].episodes, 10)
}

func TestShowFiles(t *testing.T) {
	copyR("testdata/Videos_template", "testdata/Videos")
	defer os.RemoveAll("testdata/Videos")
	err := createJSONs()
	require.NoError(t, err)

	createdJSONs := []string{
		"testdata/Videos/shows.json",
		"testdata/Videos/Show One.json",
		"testdata/Videos/show1/seasons.json",
		"testdata/Videos/show1/1.json",
		"testdata/Videos/show1/2.json",
		"testdata/Videos/show1/3.json",
		"testdata/Videos/show1/1/episodes.json",
		"testdata/Videos/show1/1/Episode 1.json",
		"testdata/Videos/show1/1/Episode 2.json",
		"testdata/Videos/Show Two.json",
	}

	for _, j := range createdJSONs {
		_, err := os.Stat(j)
		assert.Nil(t, err, "%s should exist", j)
	}
}

func TestShowURLs(t *testing.T) {
	copyR("testdata/Videos_template", "testdata/Videos")
	defer os.RemoveAll("testdata/Videos")
	err := createJSONs()
	require.NoError(t, err)

	data, err := ioutil.ReadFile("testdata/Videos/shows.json")
	require.NoError(t, err)

	var shows []FullShow
	err = json.Unmarshal(data, &shows)
	require.NoError(t, err)

	URLs := []string{shows[0].URL, shows[1].URL}
	assert.Contains(t, URLs, "testdata/Videos/Show One.json")
	assert.Contains(t, URLs, "testdata/Videos/Show Two.json")
}

func TestSeasonURLs(t *testing.T) {
	copyR("testdata/Videos_template", "testdata/Videos")
	defer os.RemoveAll("testdata/Videos")
	err := createJSONs()
	require.NoError(t, err)

	data, err := ioutil.ReadFile("testdata/Videos/show1/seasons.json")
	require.NoError(t, err)

	var seasons []season
	err = json.Unmarshal(data, &seasons)
	require.NoError(t, err)

	URLs := []string{seasons[0].URL, seasons[1].URL}
	assert.Contains(t, URLs, "testdata/Videos/show1/1.json")
	assert.Contains(t, URLs, "testdata/Videos/show1/2.json")
}

func createJSONs() error {
	shows := map[os.FileInfo]FullShow{
		mockFileInfo{name: "testdata/Videos/show1"}: FullShow{
			show: trakt.Show{Title: "Show One"},
			seasons: []season{
				{
					// You need to drop the package name to address the embedded field.
					Season: trakt.Season{Number: 1},
					episodes: []trakt.Episode{
						{
							Title: "Episode 1",
						},
						{
							Title: "Episode 2",
						},
					},
				},
				{
					Season: trakt.Season{Number: 2},
				},
				{
					Season: trakt.Season{Number: 3},
				},
			},
		},
		mockFileInfo{name: "testdata/Videos/show2"}: FullShow{
			show: trakt.Show{Title: "Show Two"},
		},
	}

	c := newConjoiner("testdata/Videos")
	return c.createJSONs(shows)
}

func copyR(src, dest string) {
	filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
		target := strings.Replace(path, src, dest, -1)
		if info.IsDir() {
			os.MkdirAll(target, 0755)
		} else {
			copyFile(path, target)
		}
		return nil
	})
}

func copyFile(source string, dest string) (err error) {
	sourcefile, err := os.Open(source)
	if err != nil {
		return err
	}

	defer sourcefile.Close()

	destfile, err := os.Create(dest)
	if err != nil {
		return err
	}

	defer destfile.Close()

	_, err = io.Copy(destfile, sourcefile)
	if err == nil {
		sourceinfo, err := os.Stat(source)
		if err != nil {
			err = os.Chmod(dest, sourceinfo.Mode())
		}

	}

	return
}
<|endoftext|>


<commit_before>package ebpf

import (
	"fmt"
	"os"
	"runtime"
	"sync"
	"unsafe"

	"github.com/cilium/ebpf"
	"github.com/cilium/ebpf/asm"
	"github.com/cilium/ebpf/link"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"golang.org/x/sys/unix"
)

func nilCloser() error {
	return nil
}

func findAttachedCgroupDeviceFilters(dirFd int) ([]*ebpf.Program, error) {
	type bpfAttrQuery struct {
		TargetFd    uint32
		AttachType  uint32
		QueryType   uint32
		AttachFlags uint32
		ProgIds     uint64 // __aligned_u64
		ProgCnt     uint32
	}

	// Currently you can only have 64 eBPF programs attached to a cgroup.
	size := 64
	retries := 0
	for retries < 10 {
		progIds := make([]uint32, size)
		query := bpfAttrQuery{
			TargetFd:   uint32(dirFd),
			AttachType: uint32(unix.BPF_CGROUP_DEVICE),
			ProgIds:    uint64(uintptr(unsafe.Pointer(&progIds[0]))),
			ProgCnt:    uint32(len(progIds)),
		}

		// Fetch the list of program ids.
		_, _, errno := unix.Syscall(unix.SYS_BPF,
			uintptr(unix.BPF_PROG_QUERY),
			uintptr(unsafe.Pointer(&query)),
			unsafe.Sizeof(query))
		size = int(query.ProgCnt)
		runtime.KeepAlive(query)
		if errno != 0 {
			// On ENOSPC we get the correct number of programs.
			if errno == unix.ENOSPC {
				retries++
				continue
			}
			return nil, fmt.Errorf("bpf_prog_query(BPF_CGROUP_DEVICE) failed: %w", errno)
		}

		// Convert the ids to program handles.
		progIds = progIds[:size]
		programs := make([]*ebpf.Program, len(progIds))
		for idx, progId := range progIds {
			program, err := ebpf.NewProgramFromID(ebpf.ProgramID(progId))
			if err != nil {
				return nil, fmt.Errorf("cannot fetch program from id: %w", err)
			}
			programs[idx] = program
		}
		runtime.KeepAlive(progIds)
		return programs, nil
	}

	return nil, errors.New("could not get complete list of CGROUP_DEVICE programs")
}

var (
	haveBpfProgReplaceBool bool
	haveBpfProgReplaceOnce sync.Once
)

// Loosely based on the BPF_F_REPLACE support check in
//   <https://github.com/cilium/ebpf/blob/v0.6.0/link/syscalls.go>.
func haveBpfProgReplace() bool {
	haveBpfProgReplaceOnce.Do(func() {
		prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
			Type:    ebpf.CGroupDevice,
			License: "MIT",
			Instructions: asm.Instructions{
				asm.Mov.Imm(asm.R0, 0),
				asm.Return(),
			},
		})
		if err != nil {
			logrus.Debugf("checking for BPF_F_REPLACE support: ebpf.NewProgram failed: %v", err)
			return
		}
		defer prog.Close()

		devnull, err := os.Open("/dev/null")
		if err != nil {
			logrus.Debugf("checking for BPF_F_REPLACE support: open dummy target fd: %v", err)
			return
		}
		defer devnull.Close()

		// We know that we have BPF_PROG_ATTACH since we can load
		// BPF_CGROUP_DEVICE programs. If passing BPF_F_REPLACE gives us EINVAL
		// we know that the feature isn't present.
		err = link.RawAttachProgram(link.RawAttachProgramOptions{
			// We rely on this fd being checked after attachFlags.
			Target: int(devnull.Fd()),
			// Attempt to "replace" bad fds with this program.
			Program: prog,
			Attach:  ebpf.AttachCGroupDevice,
			Flags:   unix.BPF_F_ALLOW_MULTI | unix.BPF_F_REPLACE,
		})
		if errors.Is(err, unix.EINVAL) {
			// not supported
			return
		}
		// attach_flags test succeded.
		if !errors.Is(err, unix.EBADF) {
			logrus.Debugf("checking for BPF_F_REPLACE: got unexpected (not EBADF or EINVAL) error: %v", err)
		}
		haveBpfProgReplaceBool = true
	})
	return haveBpfProgReplaceBool
}

// LoadAttachCgroupDeviceFilter installs eBPF device filter program to /sys/fs/cgroup/<foo> directory.
//
// Requires the system to be running in cgroup2 unified-mode with kernel >= 4.15 .
//
// https://github.com/torvalds/linux/commit/ebc614f687369f9df99828572b1d85a7c2de3d92
func LoadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFd int) (func() error, error) {
	// Increase `ulimit -l` limit to avoid BPF_PROG_LOAD error (#2167).
	// This limit is not inherited into the container.
	memlockLimit := &unix.Rlimit{
		Cur: unix.RLIM_INFINITY,
		Max: unix.RLIM_INFINITY,
	}
	_ = unix.Setrlimit(unix.RLIMIT_MEMLOCK, memlockLimit)

	// Get the list of existing programs.
	oldProgs, err := findAttachedCgroupDeviceFilters(dirFd)
	if err != nil {
		return nilCloser, err
	}
	useReplaceProg := haveBpfProgReplace() && len(oldProgs) == 1

	// Generate new program.
	spec := &ebpf.ProgramSpec{
		Type:         ebpf.CGroupDevice,
		Instructions: insts,
		License:      license,
	}
	prog, err := ebpf.NewProgram(spec)
	if err != nil {
		return nilCloser, err
	}

	// If there is only one old program, we can just replace it directly.
	var (
		replaceProg *ebpf.Program
		attachFlags uint32 = unix.BPF_F_ALLOW_MULTI
	)
	if useReplaceProg {
		replaceProg = oldProgs[0]
		attachFlags |= unix.BPF_F_REPLACE
	}
	err = link.RawAttachProgram(link.RawAttachProgramOptions{
		Target:  dirFd,
		Program: prog,
		Replace: replaceProg,
		Attach:  ebpf.AttachCGroupDevice,
		Flags:   attachFlags,
	})
	if err != nil {
		return nilCloser, fmt.Errorf("failed to call BPF_PROG_ATTACH (BPF_CGROUP_DEVICE, BPF_F_ALLOW_MULTI): %w", err)
	}
	closer := func() error {
		err = link.RawDetachProgram(link.RawDetachProgramOptions{
			Target:  dirFd,
			Program: prog,
			Attach:  ebpf.AttachCGroupDevice,
		})
		if err != nil {
			return fmt.Errorf("failed to call BPF_PROG_DETACH (BPF_CGROUP_DEVICE): %w", err)
		}
		// TODO: Should we attach the old filters back in this case? Otherwise
		//       we fail-open on a security feature, which is a bit scary.
		return nil
	}
	if !useReplaceProg {
		// If there was more than one old program, give a warning (since this
		// really shouldn't happen with runc-managed cgroups) and then detach
		// all the old programs.
		if len(oldProgs) > 1 {
			logrus.Warnf("found more than one filter (%d) attached to a cgroup -- removing extra filters!", len(oldProgs))
		}
		for _, oldProg := range oldProgs {
			err = link.RawDetachProgram(link.RawDetachProgramOptions{
				Target:  dirFd,
				Program: oldProg,
				Attach:  ebpf.AttachCGroupDevice,
			})
			if err != nil {
				return closer, fmt.Errorf("failed to call BPF_PROG_DETACH (BPF_CGROUP_DEVICE) on old filter program: %w", err)
			}
		}
	}
	return closer, nil
}
<commit_msg>cgroupv2: ebpf: debug info when detaching programs in fallback mode<commit_after>package ebpf

import (
	"fmt"
	"os"
	"runtime"
	"sync"
	"unsafe"

	"github.com/cilium/ebpf"
	"github.com/cilium/ebpf/asm"
	"github.com/cilium/ebpf/link"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"golang.org/x/sys/unix"
)

func nilCloser() error {
	return nil
}

func findAttachedCgroupDeviceFilters(dirFd int) ([]*ebpf.Program, error) {
	type bpfAttrQuery struct {
		TargetFd    uint32
		AttachType  uint32
		QueryType   uint32
		AttachFlags uint32
		ProgIds     uint64 // __aligned_u64
		ProgCnt     uint32
	}

	// Currently you can only have 64 eBPF programs attached to a cgroup.
	size := 64
	retries := 0
	for retries < 10 {
		progIds := make([]uint32, size)
		query := bpfAttrQuery{
			TargetFd:   uint32(dirFd),
			AttachType: uint32(unix.BPF_CGROUP_DEVICE),
			ProgIds:    uint64(uintptr(unsafe.Pointer(&progIds[0]))),
			ProgCnt:    uint32(len(progIds)),
		}

		// Fetch the list of program ids.
		_, _, errno := unix.Syscall(unix.SYS_BPF,
			uintptr(unix.BPF_PROG_QUERY),
			uintptr(unsafe.Pointer(&query)),
			unsafe.Sizeof(query))
		size = int(query.ProgCnt)
		runtime.KeepAlive(query)
		if errno != 0 {
			// On ENOSPC we get the correct number of programs.
			if errno == unix.ENOSPC {
				retries++
				continue
			}
			return nil, fmt.Errorf("bpf_prog_query(BPF_CGROUP_DEVICE) failed: %w", errno)
		}

		// Convert the ids to program handles.
		progIds = progIds[:size]
		programs := make([]*ebpf.Program, len(progIds))
		for idx, progId := range progIds {
			program, err := ebpf.NewProgramFromID(ebpf.ProgramID(progId))
			if err != nil {
				return nil, fmt.Errorf("cannot fetch program from id: %w", err)
			}
			programs[idx] = program
		}
		runtime.KeepAlive(progIds)
		return programs, nil
	}

	return nil, errors.New("could not get complete list of CGROUP_DEVICE programs")
}

var (
	haveBpfProgReplaceBool bool
	haveBpfProgReplaceOnce sync.Once
)

// Loosely based on the BPF_F_REPLACE support check in
//   <https://github.com/cilium/ebpf/blob/v0.6.0/link/syscalls.go>.
func haveBpfProgReplace() bool {
	haveBpfProgReplaceOnce.Do(func() {
		prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
			Type:    ebpf.CGroupDevice,
			License: "MIT",
			Instructions: asm.Instructions{
				asm.Mov.Imm(asm.R0, 0),
				asm.Return(),
			},
		})
		if err != nil {
			logrus.Debugf("checking for BPF_F_REPLACE support: ebpf.NewProgram failed: %v", err)
			return
		}
		defer prog.Close()

		devnull, err := os.Open("/dev/null")
		if err != nil {
			logrus.Debugf("checking for BPF_F_REPLACE support: open dummy target fd: %v", err)
			return
		}
		defer devnull.Close()

		// We know that we have BPF_PROG_ATTACH since we can load
		// BPF_CGROUP_DEVICE programs. If passing BPF_F_REPLACE gives us EINVAL
		// we know that the feature isn't present.
		err = link.RawAttachProgram(link.RawAttachProgramOptions{
			// We rely on this fd being checked after attachFlags.
			Target: int(devnull.Fd()),
			// Attempt to "replace" bad fds with this program.
			Program: prog,
			Attach:  ebpf.AttachCGroupDevice,
			Flags:   unix.BPF_F_ALLOW_MULTI | unix.BPF_F_REPLACE,
		})
		if errors.Is(err, unix.EINVAL) {
			// not supported
			return
		}
		// attach_flags test succeded.
		if !errors.Is(err, unix.EBADF) {
			logrus.Debugf("checking for BPF_F_REPLACE: got unexpected (not EBADF or EINVAL) error: %v", err)
		}
		haveBpfProgReplaceBool = true
	})
	return haveBpfProgReplaceBool
}

// LoadAttachCgroupDeviceFilter installs eBPF device filter program to /sys/fs/cgroup/<foo> directory.
//
// Requires the system to be running in cgroup2 unified-mode with kernel >= 4.15 .
//
// https://github.com/torvalds/linux/commit/ebc614f687369f9df99828572b1d85a7c2de3d92
func LoadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFd int) (func() error, error) {
	// Increase `ulimit -l` limit to avoid BPF_PROG_LOAD error (#2167).
	// This limit is not inherited into the container.
	memlockLimit := &unix.Rlimit{
		Cur: unix.RLIM_INFINITY,
		Max: unix.RLIM_INFINITY,
	}
	_ = unix.Setrlimit(unix.RLIMIT_MEMLOCK, memlockLimit)

	// Get the list of existing programs.
	oldProgs, err := findAttachedCgroupDeviceFilters(dirFd)
	if err != nil {
		return nilCloser, err
	}
	useReplaceProg := haveBpfProgReplace() && len(oldProgs) == 1

	// Generate new program.
	spec := &ebpf.ProgramSpec{
		Type:         ebpf.CGroupDevice,
		Instructions: insts,
		License:      license,
	}
	prog, err := ebpf.NewProgram(spec)
	if err != nil {
		return nilCloser, err
	}

	// If there is only one old program, we can just replace it directly.
	var (
		replaceProg *ebpf.Program
		attachFlags uint32 = unix.BPF_F_ALLOW_MULTI
	)
	if useReplaceProg {
		replaceProg = oldProgs[0]
		attachFlags |= unix.BPF_F_REPLACE
	}
	err = link.RawAttachProgram(link.RawAttachProgramOptions{
		Target:  dirFd,
		Program: prog,
		Replace: replaceProg,
		Attach:  ebpf.AttachCGroupDevice,
		Flags:   attachFlags,
	})
	if err != nil {
		return nilCloser, fmt.Errorf("failed to call BPF_PROG_ATTACH (BPF_CGROUP_DEVICE, BPF_F_ALLOW_MULTI): %w", err)
	}
	closer := func() error {
		err = link.RawDetachProgram(link.RawDetachProgramOptions{
			Target:  dirFd,
			Program: prog,
			Attach:  ebpf.AttachCGroupDevice,
		})
		if err != nil {
			return fmt.Errorf("failed to call BPF_PROG_DETACH (BPF_CGROUP_DEVICE): %w", err)
		}
		// TODO: Should we attach the old filters back in this case? Otherwise
		//       we fail-open on a security feature, which is a bit scary.
		return nil
	}
	if !useReplaceProg {
		logLevel := logrus.DebugLevel
		// If there was more than one old program, give a warning (since this
		// really shouldn't happen with runc-managed cgroups) and then detach
		// all the old programs.
		if len(oldProgs) > 1 {
			// NOTE: Ideally this should be a warning but it turns out that
			//       systemd-managed cgroups trigger this warning (apparently
			//       systemd doesn't delete old non-systemd programs when
			//       setting properties).
			logrus.Infof("found more than one filter (%d) attached to a cgroup -- removing extra filters!", len(oldProgs))
			logLevel = logrus.InfoLevel
		}
		for idx, oldProg := range oldProgs {
			// Output some extra debug info.
			if info, err := oldProg.Info(); err == nil {
				fields := logrus.Fields{
					"type": info.Type.String(),
					"tag":  info.Tag,
					"name": info.Name,
				}
				if id, ok := info.ID(); ok {
					fields["id"] = id
				}
				if runCount, ok := info.RunCount(); ok {
					fields["run_count"] = runCount
				}
				if runtime, ok := info.Runtime(); ok {
					fields["runtime"] = runtime.String()
				}
				logrus.WithFields(fields).Logf(logLevel, "removing old filter %d from cgroup", idx)
			}
			err = link.RawDetachProgram(link.RawDetachProgramOptions{
				Target:  dirFd,
				Program: oldProg,
				Attach:  ebpf.AttachCGroupDevice,
			})
			if err != nil {
				return closer, fmt.Errorf("failed to call BPF_PROG_DETACH (BPF_CGROUP_DEVICE) on old filter program: %w", err)
			}
		}
	}
	return closer, nil
}
<|endoftext|>