<commit_before>package main

import (
	"errors"
	"io"
	"testing"
	"time"

	"github.com/yosssi/gmq/mqtt/client"
	"github.com/yosssi/gmq/mqtt/packet"
)

const testAddress = "iot.eclipse.org:1883"

var errTest = errors.New("test")

type packetErr struct{}

func (p packetErr) WriteTo(w io.Writer) (int64, error) {
	return 0, errTest
}

func (p packetErr) Type() (byte, error) {
	return 0x00, errTest
}

func Test_commandConn_run_err(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	if err := cmd.run(); err == nil {
		t.Error("err => nil, want => not nil")
	}
}

func Test_commandConn_run(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.network = "tcp"
	cmd.address = testAddress

	if err := cmd.run(); err != nil {
		t.Error("err => %q, want => nil", err)
	}

	if err := disconnect(cmd.ctx); err != nil {
		t.Error("err => %q, want => nil", err)
	}
}

func Test_commandConn_waitCONNACK_connack(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.wg.Add(1)
	go cmd.waitCONNACK()

	cmd.ctx.connack <- struct{}{}

	cmd.ctx.wg.Wait()
}

func Test_commandConn_waitCONNACK_timeout(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.connackTimeout = 1

	cmd.ctx.wg.Add(1)
	go cmd.waitCONNACK()

	cmd.ctx.wg.Wait()
}

func Test_commandConn_waitCONNACK_timeout_disconnDefault(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.connackTimeout = 1

	cmd.ctx.disconn <- struct{}{}

	cmd.ctx.wg.Add(1)
	go cmd.waitCONNACK()

	cmd.ctx.wg.Wait()
}

func Test_commandConn_waitCONNACK_connackEnd(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.wg.Add(1)
	go cmd.waitCONNACK()

	cmd.ctx.connackEnd <- struct{}{}

	cmd.ctx.wg.Wait()
}

func Test_commandConn_receive_ReceiveErr_disconnecting(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.disconnecting = true

	cmd.ctx.wg.Add(1)
	go cmd.receive()

	cmd.ctx.wg.Wait()
}

func Test_commandConn_receive_ReceiveErr(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.wg.Add(1)
	go cmd.receive()

	cmd.ctx.wg.Wait()
}

func Test_commandConn_receive_ReceiveErr_default(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.disconn <- struct{}{}

	cmd.ctx.wg.Add(1)
	go cmd.receive()

	cmd.ctx.wg.Wait()
}

func Test_commandConn_receive(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	ctx.cli = client.New(nil)

	if err := ctx.cli.Connect("tcp", testAddress, nil); err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.wg.Add(1)
	go cmd.receive()

	time.Sleep(1 * time.Second)

	if err := disconnect(cmd.ctx); err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.wg.Wait()
}

func Test_commandConn_handle_err(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.handle(packetErr{})
}

func Test_commandConn_handle_default(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	p, err := packet.NewCONNACKFromBytes([]byte{0x20, 0x02}, []byte{0x00, 0x00})
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.connack <- struct{}{}

	cmd.handle(p)
}

func Test_commandConn_handle(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	p, err := packet.NewCONNACKFromBytes([]byte{0x20, 0x02}, []byte{0x00, 0x00})
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.handle(p)
}

func Test_commandConn_send_send(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	ctx.cli = client.New(nil)

	if err := ctx.cli.Connect("tcp", testAddress, nil); err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.wg.Add(1)
	go cmd.send()

	cmd.ctx.send <- packet.NewPINGREQ()

	time.Sleep(1 * time.Second)

	cmd.ctx.sendEnd <- struct{}{}

	cmd.ctx.wg.Wait()

	if err := disconnect(cmd.ctx); err != nil {
		t.Error("err => %q, want => nil", err)
	}
}

func Test_commandConn_send_keepAlive(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	ctx.cli = client.New(nil)

	if err := ctx.cli.Connect("tcp", testAddress, nil); err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	var keepAlive uint = 1
	cmd.connectOpts.KeepAlive = &keepAlive

	cmd.ctx.wg.Add(1)
	go cmd.send()

	time.Sleep(2 * time.Second)

	cmd.ctx.sendEnd <- struct{}{}

	cmd.ctx.wg.Wait()

	if err := disconnect(cmd.ctx); err != nil {
		t.Error("err => %q, want => nil", err)
	}
}

func Test_commandConn_sendPacket_disconnecting(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	ctx.cli = client.New(nil)

	cmd.ctx.disconnecting = true

	cmd.sendPacket(nil)
}

func Test_commandConn_sendPacket_disconn(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	ctx.cli = client.New(nil)

	cmd.sendPacket(nil)
}

func Test_commandConn_sendPacket_default(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	ctx.cli = client.New(nil)

	cmd.ctx.disconn <- struct{}{}

	cmd.sendPacket(nil)
}
<commit_msg>Update cmd/gmq-cli/command_conn_test.go<commit_after>package main

import (
	"errors"
	"io"
	"net"
	"testing"
	"time"

	"github.com/yosssi/gmq/mqtt/client"
	"github.com/yosssi/gmq/mqtt/packet"
)

const testAddress = "iot.eclipse.org:1883"

var errTest = errors.New("test")

type packetErr struct{}

func (p packetErr) WriteTo(w io.Writer) (int64, error) {
	return 0, errTest
}

func (p packetErr) Type() (byte, error) {
	return 0x00, errTest
}

func Test_commandConn_run_err(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	if err := cmd.run(); err == nil {
		t.Error("err => nil, want => not nil")
	}
}

func Test_commandConn_run(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.network = "tcp"
	cmd.address = testAddress

	if err := cmd.run(); err != nil {
		t.Error("err => %q, want => nil", err)
	}

	if err := disconnect(cmd.ctx); err != nil {
		t.Error("err => %q, want => nil", err)
	}
}

func Test_commandConn_waitCONNACK_connack(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.wg.Add(1)
	go cmd.waitCONNACK()

	cmd.ctx.connack <- struct{}{}

	cmd.ctx.wg.Wait()
}

func Test_commandConn_waitCONNACK_timeout(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.connackTimeout = 1

	cmd.ctx.wg.Add(1)
	go cmd.waitCONNACK()

	cmd.ctx.wg.Wait()
}

func Test_commandConn_waitCONNACK_timeout_disconnDefault(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.connackTimeout = 1

	cmd.ctx.disconn <- struct{}{}

	cmd.ctx.wg.Add(1)
	go cmd.waitCONNACK()

	cmd.ctx.wg.Wait()
}

func Test_commandConn_waitCONNACK_connackEnd(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.wg.Add(1)
	go cmd.waitCONNACK()

	cmd.ctx.connackEnd <- struct{}{}

	cmd.ctx.wg.Wait()
}

func Test_commandConn_receive_ReceiveErr_disconnecting(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.disconnecting = true

	cmd.ctx.wg.Add(1)
	go cmd.receive()

	cmd.ctx.wg.Wait()
}

func Test_commandConn_receive_ReceiveErr(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.wg.Add(1)
	go cmd.receive()

	cmd.ctx.wg.Wait()
}

func Test_commandConn_receive_ReceiveErr_default(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.disconn <- struct{}{}

	cmd.ctx.wg.Add(1)
	go cmd.receive()

	cmd.ctx.wg.Wait()
}

func Test_commandConn_receive(t *testing.T) {
	ln, err := net.Listen("tcp", "localhost:1883")
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
		return
	}

	defer ln.Close()

	go func() {
		for {
			conn, err := ln.Accept()
			if err != nil {
				return
			}

			conn.Write([]byte{0x20, 0x02, 0x00, 0x00})
		}
	}()

	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	ctx.cli = client.New(nil)

	if err := ctx.cli.Connect("tcp", "localhost:1883", nil); err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.wg.Add(1)
	go cmd.receive()

	time.Sleep(1 * time.Second)

	if err := disconnect(cmd.ctx); err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.wg.Wait()
}

func Test_commandConn_handle_err(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.handle(packetErr{})
}

func Test_commandConn_handle_default(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	p, err := packet.NewCONNACKFromBytes([]byte{0x20, 0x02}, []byte{0x00, 0x00})
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.connack <- struct{}{}

	cmd.handle(p)
}

func Test_commandConn_handle(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	p, err := packet.NewCONNACKFromBytes([]byte{0x20, 0x02}, []byte{0x00, 0x00})
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.handle(p)
}

func Test_commandConn_send_send(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	ctx.cli = client.New(nil)

	if err := ctx.cli.Connect("tcp", testAddress, nil); err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	cmd.ctx.wg.Add(1)
	go cmd.send()

	cmd.ctx.send <- packet.NewPINGREQ()

	time.Sleep(1 * time.Second)

	cmd.ctx.sendEnd <- struct{}{}

	cmd.ctx.wg.Wait()

	if err := disconnect(cmd.ctx); err != nil {
		t.Error("err => %q, want => nil", err)
	}
}

func Test_commandConn_send_keepAlive(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	ctx.cli = client.New(nil)

	if err := ctx.cli.Connect("tcp", testAddress, nil); err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	var keepAlive uint = 1
	cmd.connectOpts.KeepAlive = &keepAlive

	cmd.ctx.wg.Add(1)
	go cmd.send()

	time.Sleep(2 * time.Second)

	cmd.ctx.sendEnd <- struct{}{}

	cmd.ctx.wg.Wait()

	if err := disconnect(cmd.ctx); err != nil {
		t.Error("err => %q, want => nil", err)
	}
}

func Test_commandConn_sendPacket_disconnecting(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	ctx.cli = client.New(nil)

	cmd.ctx.disconnecting = true

	cmd.sendPacket(nil)
}

func Test_commandConn_sendPacket_disconn(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	ctx.cli = client.New(nil)

	cmd.sendPacket(nil)
}

func Test_commandConn_sendPacket_default(t *testing.T) {
	ctx := newContext()

	cmd, err := newCommandConn(nil, ctx)
	if err != nil {
		t.Errorf("err => %q, want => nil", err)
	}

	ctx.cli = client.New(nil)

	cmd.ctx.disconn <- struct{}{}

	cmd.sendPacket(nil)
}
<|endoftext|>


<commit_before>package phraseapp

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"

	"gopkg.in/yaml.v2"
)

// Config contains all information from a .phraseapp.yml config file
type Config struct {
	Credentials
	Debug bool `cli:"opt --verbose -v desc='Verbose output'"`

	Page    *int
	PerPage *int

	DefaultProjectID  string
	DefaultFileFormat string

	Defaults map[string]map[string]interface{}

	Targets []byte
	Sources []byte
}

const configName = ".phraseapp.yml"

// ReadConfig reads a .phraseapp.yml config file
func ReadConfig() (*Config, error) {
	cfg := &Config{}
	rawCfg := struct{ PhraseApp *Config }{PhraseApp: cfg}

	content, err := configContent()
	switch {
	case err != nil:
		return nil, err
	case content == nil:
		return cfg, nil
	default:
		return cfg, yaml.Unmarshal(content, rawCfg)
	}
}

func configContent() ([]byte, error) {
	path, err := configPath()
	switch {
	case err != nil:
		return nil, err
	case path == "":
		return nil, nil
	default:
		return ioutil.ReadFile(path)
	}
}

func configPath() (string, error) {
	if possiblePath := os.Getenv("PHRASEAPP_CONFIG"); possiblePath != "" {
		_, err := os.Stat(possiblePath)
		if err == nil {
			return possiblePath, nil
		}

		if os.IsNotExist(err) {
			err = fmt.Errorf("file %q (from PHRASEAPP_CONFIG environment variable) doesn't exist", possiblePath)
		}

		return "", err
	}

	workingDir, err := os.Getwd()
	if err != nil {
		return "", nil
	}

	possiblePath := filepath.Join(workingDir, configName)
	if _, err := os.Stat(possiblePath); err == nil {
		return possiblePath, nil
	}

	possiblePath = defaultConfigDir()
	if _, err := os.Stat(possiblePath); err != nil {
		return "", nil
	}

	return possiblePath, nil
}

func (cfg *Config) UnmarshalYAML(unmarshal func(i interface{}) error) error {
	m := map[string]interface{}{}
	err := ParseYAMLToMap(unmarshal, map[string]interface{}{
		"access_token": &cfg.Credentials.Token,
		"host":         &cfg.Credentials.Host,
		"debug":        &cfg.Debug,
		"page":         &cfg.Page,
		"perpage":      &cfg.PerPage,
		"project_id":   &cfg.DefaultProjectID,
		"file_format":  &cfg.DefaultFileFormat,
		"push":         &cfg.Sources,
		"pull":         &cfg.Targets,
		"defaults":     &m,
	})
	if err != nil {
		return err
	}

	cfg.Defaults = map[string]map[string]interface{}{}
	for path, rawConfig := range m {
		cfg.Defaults[path], err = ValidateIsRawMap("defaults."+path, rawConfig)
		if err != nil {
			return err
		}
	}

	return nil
}

const cfgValueErrStr = "configuration key %q has invalid value: %T\nsee https://phraseapp.com/docs/developers/cli/configuration/"
const cfgKeyErrStr = "configuration key %q has invalid type: %T\nsee https://phraseapp.com/docs/developers/cli/configuration/"
const cfgInvalidKeyErrStr = "configuration key %q unknown\nsee https://phraseapp.com/docs/developers/cli/configuration/"

func ValidateIsString(k string, v interface{}) (string, error) {
	s, ok := v.(string)
	if !ok {
		return "", fmt.Errorf(cfgValueErrStr, k, v)
	}
	return s, nil
}

func ValidateIsBool(k string, v interface{}) (bool, error) {
	b, ok := v.(bool)
	if !ok {
		return false, fmt.Errorf(cfgValueErrStr, k, v)
	}
	return b, nil
}

func ValidateIsInt(k string, v interface{}) (int, error) {
	i, ok := v.(int)
	if !ok {
		return 0, fmt.Errorf(cfgValueErrStr, k, v)
	}
	return i, nil
}

func ValidateIsRawMap(k string, v interface{}) (map[string]interface{}, error) {
	raw, ok := v.(map[interface{}]interface{})
	if !ok {
		return nil, fmt.Errorf(cfgValueErrStr, k, v)
	}

	ps := map[string]interface{}{}
	for mk, mv := range raw {
		s, ok := mk.(string)
		if !ok {
			return nil, fmt.Errorf(cfgKeyErrStr, fmt.Sprintf("%s.%v", k, mk), mk)
		}
		ps[s] = mv
	}
	return ps, nil
}

func ConvertToStringMap(raw map[string]interface{}) (map[string]string, error) {
	ps := map[string]string{}
	for mk, mv := range raw {
		switch v := mv.(type) {
		case string:
			ps[mk] = v
		case bool:
			ps[mk] = fmt.Sprintf("%t", v)
		case int:
			ps[mk] = fmt.Sprintf("%d", v)
		default:
			return nil, fmt.Errorf("invalid type of key %q: %T", mk, mv)
		}
	}
	return ps, nil
}

// Calls the YAML parser function (see yaml.v2/Unmarshaler interface) with a map
// of string to interface. This map is then iterated to match against the given
// map of keys to fields, validates the type and sets the fields accordingly.
func ParseYAMLToMap(unmarshal func(interface{}) error, keysToField map[string]interface{}) error {
	m := map[string]interface{}{}
	if err := unmarshal(m); err != nil {
		return err
	}

	var err error
	for k, v := range m {
		value, found := keysToField[k]
		if !found {
			return fmt.Errorf(cfgInvalidKeyErrStr, k)
		}

		switch val := value.(type) {
		case *string:
			*val, err = ValidateIsString(k, v)
		case *int:
			*val, err = ValidateIsInt(k, v)
		case **int:
			*val = new(int)
			**val, err = ValidateIsInt(k, v)
		case *bool:
			*val, err = ValidateIsBool(k, v)
		case *map[string]interface{}:
			*val, err = ValidateIsRawMap(k, v)
		case *[]byte:
			*val, err = yaml.Marshal(v)
		default:
			err = fmt.Errorf(cfgValueErrStr, k, value)
		}
		if err != nil {
			return err
		}
	}

	return nil
}
<commit_msg>Use per_page instead perpage param for pagination<commit_after>package phraseapp

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"

	"gopkg.in/yaml.v2"
)

// Config contains all information from a .phraseapp.yml config file
type Config struct {
	Credentials
	Debug bool `cli:"opt --verbose -v desc='Verbose output'"`

	Page    *int
	PerPage *int

	DefaultProjectID  string
	DefaultFileFormat string

	Defaults map[string]map[string]interface{}

	Targets []byte
	Sources []byte
}

const configName = ".phraseapp.yml"

// ReadConfig reads a .phraseapp.yml config file
func ReadConfig() (*Config, error) {
	cfg := &Config{}
	rawCfg := struct{ PhraseApp *Config }{PhraseApp: cfg}

	content, err := configContent()
	switch {
	case err != nil:
		return nil, err
	case content == nil:
		return cfg, nil
	default:
		return cfg, yaml.Unmarshal(content, rawCfg)
	}
}

func configContent() ([]byte, error) {
	path, err := configPath()
	switch {
	case err != nil:
		return nil, err
	case path == "":
		return nil, nil
	default:
		return ioutil.ReadFile(path)
	}
}

func configPath() (string, error) {
	if possiblePath := os.Getenv("PHRASEAPP_CONFIG"); possiblePath != "" {
		_, err := os.Stat(possiblePath)
		if err == nil {
			return possiblePath, nil
		}

		if os.IsNotExist(err) {
			err = fmt.Errorf("file %q (from PHRASEAPP_CONFIG environment variable) doesn't exist", possiblePath)
		}

		return "", err
	}

	workingDir, err := os.Getwd()
	if err != nil {
		return "", nil
	}

	possiblePath := filepath.Join(workingDir, configName)
	if _, err := os.Stat(possiblePath); err == nil {
		return possiblePath, nil
	}

	possiblePath = defaultConfigDir()
	if _, err := os.Stat(possiblePath); err != nil {
		return "", nil
	}

	return possiblePath, nil
}

func (cfg *Config) UnmarshalYAML(unmarshal func(i interface{}) error) error {
	m := map[string]interface{}{}
	err := ParseYAMLToMap(unmarshal, map[string]interface{}{
		"access_token": &cfg.Credentials.Token,
		"host":         &cfg.Credentials.Host,
		"debug":        &cfg.Debug,
		"page":         &cfg.Page,
		"per_page":     &cfg.PerPage,
		"project_id":   &cfg.DefaultProjectID,
		"file_format":  &cfg.DefaultFileFormat,
		"push":         &cfg.Sources,
		"pull":         &cfg.Targets,
		"defaults":     &m,
	})
	if err != nil {
		return err
	}

	cfg.Defaults = map[string]map[string]interface{}{}
	for path, rawConfig := range m {
		cfg.Defaults[path], err = ValidateIsRawMap("defaults."+path, rawConfig)
		if err != nil {
			return err
		}
	}

	return nil
}

const cfgValueErrStr = "configuration key %q has invalid value: %T\nsee https://phraseapp.com/docs/developers/cli/configuration/"
const cfgKeyErrStr = "configuration key %q has invalid type: %T\nsee https://phraseapp.com/docs/developers/cli/configuration/"
const cfgInvalidKeyErrStr = "configuration key %q unknown\nsee https://phraseapp.com/docs/developers/cli/configuration/"

func ValidateIsString(k string, v interface{}) (string, error) {
	s, ok := v.(string)
	if !ok {
		return "", fmt.Errorf(cfgValueErrStr, k, v)
	}
	return s, nil
}

func ValidateIsBool(k string, v interface{}) (bool, error) {
	b, ok := v.(bool)
	if !ok {
		return false, fmt.Errorf(cfgValueErrStr, k, v)
	}
	return b, nil
}

func ValidateIsInt(k string, v interface{}) (int, error) {
	i, ok := v.(int)
	if !ok {
		return 0, fmt.Errorf(cfgValueErrStr, k, v)
	}
	return i, nil
}

func ValidateIsRawMap(k string, v interface{}) (map[string]interface{}, error) {
	raw, ok := v.(map[interface{}]interface{})
	if !ok {
		return nil, fmt.Errorf(cfgValueErrStr, k, v)
	}

	ps := map[string]interface{}{}
	for mk, mv := range raw {
		s, ok := mk.(string)
		if !ok {
			return nil, fmt.Errorf(cfgKeyErrStr, fmt.Sprintf("%s.%v", k, mk), mk)
		}
		ps[s] = mv
	}
	return ps, nil
}

func ConvertToStringMap(raw map[string]interface{}) (map[string]string, error) {
	ps := map[string]string{}
	for mk, mv := range raw {
		switch v := mv.(type) {
		case string:
			ps[mk] = v
		case bool:
			ps[mk] = fmt.Sprintf("%t", v)
		case int:
			ps[mk] = fmt.Sprintf("%d", v)
		default:
			return nil, fmt.Errorf("invalid type of key %q: %T", mk, mv)
		}
	}
	return ps, nil
}

// Calls the YAML parser function (see yaml.v2/Unmarshaler interface) with a map
// of string to interface. This map is then iterated to match against the given
// map of keys to fields, validates the type and sets the fields accordingly.
func ParseYAMLToMap(unmarshal func(interface{}) error, keysToField map[string]interface{}) error {
	m := map[string]interface{}{}
	if err := unmarshal(m); err != nil {
		return err
	}

	var err error
	for k, v := range m {
		value, found := keysToField[k]
		if !found {
			return fmt.Errorf(cfgInvalidKeyErrStr, k)
		}

		switch val := value.(type) {
		case *string:
			*val, err = ValidateIsString(k, v)
		case *int:
			*val, err = ValidateIsInt(k, v)
		case **int:
			*val = new(int)
			**val, err = ValidateIsInt(k, v)
		case *bool:
			*val, err = ValidateIsBool(k, v)
		case *map[string]interface{}:
			*val, err = ValidateIsRawMap(k, v)
		case *[]byte:
			*val, err = yaml.Marshal(v)
		default:
			err = fmt.Errorf(cfgValueErrStr, k, value)
		}
		if err != nil {
			return err
		}
	}

	return nil
}
<|endoftext|>


<commit_before>package core

import (
	"flag"
	"os"
	"reflect"
	"strings"
	"testing"

	"github.com/joyent/containerpilot/commands"
)

// ------------------------------------------

var testJSON = `{
	"consul": "consul:8500",
	"preStart": "/bin/to/preStart.sh arg1 arg2",
	"preStop": ["/bin/to/preStop.sh","arg1","arg2"],
	"postStop": ["/bin/to/postStop.sh"],
	"stopTimeout": 5,
	"services": [
			{
					"name": "serviceA",
					"port": 8080,
					"interfaces": "eth0",
					"health": "/bin/to/healthcheck/for/service/A.sh",
					"poll": 30,
					"ttl": "19",
					"tags": ["tag1","tag2"]
			},
			{
					"name": "serviceB",
					"port": 5000,
					"interfaces": ["ethwe","eth0"],
					"health": "/bin/to/healthcheck/for/service/B.sh",
					"poll": 30,
					"ttl": 103
			}
	],
	"backends": [
			{
					"name": "upstreamA",
					"poll": 11,
					"onChange": "/bin/to/onChangeEvent/for/upstream/A.sh {{.TEST}}",
					"tag": "dev"
			},
			{
					"name": "upstreamB",
					"poll": 79,
					"onChange": "/bin/to/onChangeEvent/for/upstream/B.sh {{.ENV_NOT_FOUND}}"
			}
	]
}
`

func TestValidConfigParse(t *testing.T) {
	defer argTestCleanup(argTestSetup())

	os.Setenv("TEST", "HELLO")
	os.Args = []string{"this", "-config", testJSON, "/testdata/test.sh", "valid1", "--debug"}
	app, err := LoadApp()
	if err != nil {
		t.Fatalf("Unexpected error in LoadApp: %v", err)
	}

	if len(app.Backends) != 2 || len(app.Services) != 2 {
		t.Fatalf("Expected 2 backends and 2 services but got: len(backends)=%d, len(services)=%d", len(app.Backends), len(app.Services))
	}
	args := flag.Args()
	if len(args) != 3 || args[0] != "/testdata/test.sh" {
		t.Errorf("Expected 3 args but got unexpected results: %v", args)
	}

	expectedTags := []string{"tag1", "tag2"}
	if !reflect.DeepEqual(app.Services[0].Tags, expectedTags) {
		t.Errorf("Expected tags %s for serviceA, but got: %s", expectedTags, app.Services[0].Tags)
	}

	if app.Services[1].Tags != nil {
		t.Errorf("Expected no tags for serviceB, but got: %s", app.Services[1].Tags)
	}

	if app.Services[0].TTL != 19 {
		t.Errorf("Expected ttl=19 for serviceA, but got: %d", app.Services[1].TTL)
	}

	if app.Services[1].TTL != 103 {
		t.Errorf("Expected ttl=103 for serviceB, but got: %d", app.Services[1].TTL)
	}

	if app.Backends[0].Tag != "dev" {
		t.Errorf("Expected tag %s for upstreamA, but got: %s", "dev", app.Backends[0].Tag)
	}

	if app.Backends[1].Tag != "" {
		t.Errorf("Expected no tag for upstreamB, but got: %s", app.Backends[1].Tag)
	}

	validateCommandParsed(t, "preStart", app.PreStartCmd,
		"/bin/to/preStart.sh", []string{"arg1", "arg2"})
	validateCommandParsed(t, "preStop", app.PreStopCmd,
		"/bin/to/preStop.sh", []string{"arg1", "arg2"})
	validateCommandParsed(t, "postStop", app.PostStopCmd,
		"/bin/to/postStop.sh", nil) //[]string{})
}

func TestServiceConfigRequiredFields(t *testing.T) {
	// Missing `name`
	var testJSON = `{"consul": "consul:8500", "services": [
                           {"name": "", "port": 8080, "poll": 30, "ttl": 19 }]}`
	validateParseError(t, testJSON, []string{"`name`"})

	// Missing `poll`
	testJSON = `{"consul": "consul:8500", "services": [
                       {"name": "name", "port": 8080, "ttl": 19}]}`
	validateParseError(t, testJSON, []string{"`poll`"})

	// Missing `ttl`
	testJSON = `{"consul": "consul:8500", "services": [
                       {"name": "name", "port": 8080, "poll": 19}]}`
	validateParseError(t, testJSON, []string{"`ttl`"})

	testJSON = `{"consul": "consul:8500", "services": [
                       {"name": "name", "poll": 19, "ttl": 19}]}`
	validateParseError(t, testJSON, []string{"`port`"})
}

func TestBackendConfigRequiredFields(t *testing.T) {
	// Missing `name`
	var testJSON = `{"consul": "consul:8500", "backends": [
                           {"name": "", "poll": 30, "onChange": "true"}]}`
	validateParseError(t, testJSON, []string{"`name`"})

	// Missing `poll`
	testJSON = `{"consul": "consul:8500", "backends": [
                       {"name": "name", "onChange": "true"}]}`
	validateParseError(t, testJSON, []string{"`poll`"})

	// Missing `onChange`
	testJSON = `{"consul": "consul:8500", "backends": [
                       {"name": "name", "poll": 19 }]}`
	validateParseError(t, testJSON, []string{"`onChange`"})
}

func TestInvalidConfigNoConfigFlag(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	os.Args = []string{"this", "/testdata/test.sh", "invalid1", "--debug"}
	if _, err := LoadApp(); err != nil && err.Error() != "-config flag is required" {
		t.Errorf("Expected error but got %s", err)
	}
}

func TestInvalidConfigParseNoDiscovery(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	testParseExpectError(t, "{}", "No discovery backend defined")
}

func TestInvalidConfigParseFile(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	testParseExpectError(t, "file:///xxxx",
		"Could not read config file: open /xxxx: no such file or directory")
}

func TestInvalidConfigParseNotJson(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	testParseExpectError(t, "<>",
		"Parse error at line:col [1:1]")
}

func TestJSONTemplateParseError(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	testParseExpectError(t,
		`{
    "test": {{ .NO_SUCH_KEY }},
    "test2": "hello"
}`,
		"Parse error at line:col [2:13]")
}

func TestJSONTemplateParseError2(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	testParseExpectError(t,
		`{
    "test1": "1",
    "test2": 2,
    "test3": false,
    test2: "hello"
}`,
		"Parse error at line:col [5:5]")
}

func TestParseTrailingComma(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	testParseExpectError(t,
		`{
			"consul": "consul:8500",
			"tasks": [{
				"command": ["echo","hi"]
			},
		]
	}`, "Do you have an extra comma somewhere?")
}

func TestRenderArgs(t *testing.T) {
	flags := []string{"-name", "{{ .HOSTNAME }}"}
	expected, _ := os.Hostname()
	if got := getArgs(flags)[1]; got != expected {
		t.Errorf("Expected %v but got %v for rendered hostname", expected, got)
	}

	// invalid template should just be returned unchanged
	flags = []string{"-name", "{{ .HOSTNAME }"}
	expected = "{{ .HOSTNAME }"
	if got := getArgs(flags)[1]; got != expected {
		t.Errorf("Expected %v but got %v for unrendered hostname", expected, got)
	}
}

func TestMetricServiceCreation(t *testing.T) {

	jsonFragment := `{
    "consul": "consul:8500",
    "telemetry": {
      "port": 9090
    }
  }`
	if app, err := NewApp(jsonFragment); err != nil {
		t.Fatalf("Got error while initializing config: %v", err)
	} else {
		if len(app.Services) != 1 {
			t.Errorf("Expected telemetry service but got %v", app.Services)
		} else {
			service := app.Services[0]
			if service.Name != "containerpilot" {
				t.Errorf("Got incorrect service back: %v", service)
			}
			for _, envVar := range os.Environ() {
				if strings.HasPrefix(envVar, "CONTAINERPILOT_CONTAINERPILOT_IP") {
					return
				}
			}
			t.Errorf("Did not find CONTAINERPILOT_CONTAINERPILOT_IP env var")
		}
	}
}

func TestPidEnvVar(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	os.Args = []string{"this", "-config", "{}", "/testdata/test.sh"}
	if _, err := LoadApp(); err == nil {
		t.Fatalf("Expected error in LoadApp but got none")
	}
	if pid := os.Getenv("CONTAINERPILOT_PID"); pid == "" {
		t.Errorf("Expected CONTAINERPILOT_PID to be set even on error")
	}
}

// ----------------------------------------------------
// test helpers

func argTestSetup() []string {
	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
	flag.Usage = nil
	return os.Args
}

func argTestCleanup(oldArgs []string) {
	os.Args = oldArgs
}

func testParseExpectError(t *testing.T, testJSON string, expected string) {
	os.Args = []string{"this", "-config", testJSON, "/testdata/test.sh", "test", "--debug"}
	if _, err := LoadApp(); err != nil && !strings.Contains(err.Error(), expected) {
		t.Errorf("Expected %s but got %s", expected, err)
	}
}

func validateParseError(t *testing.T, testJSON string, matchStrings []string) {
	if _, err := NewApp(testJSON); err == nil {
		t.Errorf("Expected error parsing config")
	} else {
		for _, match := range matchStrings {
			if !strings.Contains(err.Error(), match) {
				t.Errorf("Expected message does not contain %s: %s", match, err)
			}
		}
	}
}

func validateCommandParsed(t *testing.T, name string, parsed *commands.Command,
	expectedExec string, expectedArgs []string) {
	if parsed == nil {
		t.Errorf("%s not configured", name)
	}
	if !reflect.DeepEqual(parsed.Exec, expectedExec) {
		t.Errorf("%s executable not configured: %s != %s", name, parsed.Exec, expectedExec)
	}
	if !reflect.DeepEqual(parsed.Args, expectedArgs) {
		t.Errorf("%s arguments not configured: %s != %s", name, parsed.Args, expectedArgs)
	}
}
<commit_msg>LoadApp returns a Runnable interface.<commit_after>package core

import (
	"flag"
	"os"
	"reflect"
	"strings"
	"testing"

	"github.com/joyent/containerpilot/commands"
)

// ------------------------------------------

var testJSON = `{
	"consul": "consul:8500",
	"preStart": "/bin/to/preStart.sh arg1 arg2",
	"preStop": ["/bin/to/preStop.sh","arg1","arg2"],
	"postStop": ["/bin/to/postStop.sh"],
	"stopTimeout": 5,
	"services": [
			{
					"name": "serviceA",
					"port": 8080,
					"interfaces": "eth0",
					"health": "/bin/to/healthcheck/for/service/A.sh",
					"poll": 30,
					"ttl": "19",
					"tags": ["tag1","tag2"]
			},
			{
					"name": "serviceB",
					"port": 5000,
					"interfaces": ["ethwe","eth0"],
					"health": "/bin/to/healthcheck/for/service/B.sh",
					"poll": 30,
					"ttl": 103
			}
	],
	"backends": [
			{
					"name": "upstreamA",
					"poll": 11,
					"onChange": "/bin/to/onChangeEvent/for/upstream/A.sh {{.TEST}}",
					"tag": "dev"
			},
			{
					"name": "upstreamB",
					"poll": 79,
					"onChange": "/bin/to/onChangeEvent/for/upstream/B.sh {{.ENV_NOT_FOUND}}"
			}
	]
}
`

func TestValidConfigParse(t *testing.T) {
	defer argTestCleanup(argTestSetup())

	os.Setenv("TEST", "HELLO")
	os.Args = []string{"this", "-config", testJSON, "/testdata/test.sh", "valid1", "--debug"}
	runnable, err := LoadApp()
	if err != nil {
		t.Fatalf("Unexpected error in LoadApp: %v", err)
	}
	var app = runnable.(*App)

	if len(app.Backends) != 2 || len(app.Services) != 2 {
		t.Fatalf("Expected 2 backends and 2 services but got: len(backends)=%d, len(services)=%d", len(app.Backends), len(app.Services))
	}
	args := flag.Args()
	if len(args) != 3 || args[0] != "/testdata/test.sh" {
		t.Errorf("Expected 3 args but got unexpected results: %v", args)
	}

	expectedTags := []string{"tag1", "tag2"}
	if !reflect.DeepEqual(app.Services[0].Tags, expectedTags) {
		t.Errorf("Expected tags %s for serviceA, but got: %s", expectedTags, app.Services[0].Tags)
	}

	if app.Services[1].Tags != nil {
		t.Errorf("Expected no tags for serviceB, but got: %s", app.Services[1].Tags)
	}

	if app.Services[0].TTL != 19 {
		t.Errorf("Expected ttl=19 for serviceA, but got: %d", app.Services[1].TTL)
	}

	if app.Services[1].TTL != 103 {
		t.Errorf("Expected ttl=103 for serviceB, but got: %d", app.Services[1].TTL)
	}

	if app.Backends[0].Tag != "dev" {
		t.Errorf("Expected tag %s for upstreamA, but got: %s", "dev", app.Backends[0].Tag)
	}

	if app.Backends[1].Tag != "" {
		t.Errorf("Expected no tag for upstreamB, but got: %s", app.Backends[1].Tag)
	}

	validateCommandParsed(t, "preStart", app.PreStartCmd,
		"/bin/to/preStart.sh", []string{"arg1", "arg2"})
	validateCommandParsed(t, "preStop", app.PreStopCmd,
		"/bin/to/preStop.sh", []string{"arg1", "arg2"})
	validateCommandParsed(t, "postStop", app.PostStopCmd,
		"/bin/to/postStop.sh", nil) //[]string{})
}

func TestServiceConfigRequiredFields(t *testing.T) {
	// Missing `name`
	var testJSON = `{"consul": "consul:8500", "services": [
                           {"name": "", "port": 8080, "poll": 30, "ttl": 19 }]}`
	validateParseError(t, testJSON, []string{"`name`"})

	// Missing `poll`
	testJSON = `{"consul": "consul:8500", "services": [
                       {"name": "name", "port": 8080, "ttl": 19}]}`
	validateParseError(t, testJSON, []string{"`poll`"})

	// Missing `ttl`
	testJSON = `{"consul": "consul:8500", "services": [
                       {"name": "name", "port": 8080, "poll": 19}]}`
	validateParseError(t, testJSON, []string{"`ttl`"})

	testJSON = `{"consul": "consul:8500", "services": [
                       {"name": "name", "poll": 19, "ttl": 19}]}`
	validateParseError(t, testJSON, []string{"`port`"})
}

func TestBackendConfigRequiredFields(t *testing.T) {
	// Missing `name`
	var testJSON = `{"consul": "consul:8500", "backends": [
                           {"name": "", "poll": 30, "onChange": "true"}]}`
	validateParseError(t, testJSON, []string{"`name`"})

	// Missing `poll`
	testJSON = `{"consul": "consul:8500", "backends": [
                       {"name": "name", "onChange": "true"}]}`
	validateParseError(t, testJSON, []string{"`poll`"})

	// Missing `onChange`
	testJSON = `{"consul": "consul:8500", "backends": [
                       {"name": "name", "poll": 19 }]}`
	validateParseError(t, testJSON, []string{"`onChange`"})
}

func TestInvalidConfigNoConfigFlag(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	os.Args = []string{"this", "/testdata/test.sh", "invalid1", "--debug"}
	if _, err := LoadApp(); err != nil && err.Error() != "-config flag is required" {
		t.Errorf("Expected error but got %s", err)
	}
}

func TestInvalidConfigParseNoDiscovery(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	testParseExpectError(t, "{}", "No discovery backend defined")
}

func TestInvalidConfigParseFile(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	testParseExpectError(t, "file:///xxxx",
		"Could not read config file: open /xxxx: no such file or directory")
}

func TestInvalidConfigParseNotJson(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	testParseExpectError(t, "<>",
		"Parse error at line:col [1:1]")
}

func TestJSONTemplateParseError(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	testParseExpectError(t,
		`{
    "test": {{ .NO_SUCH_KEY }},
    "test2": "hello"
}`,
		"Parse error at line:col [2:13]")
}

func TestJSONTemplateParseError2(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	testParseExpectError(t,
		`{
    "test1": "1",
    "test2": 2,
    "test3": false,
    test2: "hello"
}`,
		"Parse error at line:col [5:5]")
}

func TestParseTrailingComma(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	testParseExpectError(t,
		`{
			"consul": "consul:8500",
			"tasks": [{
				"command": ["echo","hi"]
			},
		]
	}`, "Do you have an extra comma somewhere?")
}

func TestRenderArgs(t *testing.T) {
	flags := []string{"-name", "{{ .HOSTNAME }}"}
	expected, _ := os.Hostname()
	if got := getArgs(flags)[1]; got != expected {
		t.Errorf("Expected %v but got %v for rendered hostname", expected, got)
	}

	// invalid template should just be returned unchanged
	flags = []string{"-name", "{{ .HOSTNAME }"}
	expected = "{{ .HOSTNAME }"
	if got := getArgs(flags)[1]; got != expected {
		t.Errorf("Expected %v but got %v for unrendered hostname", expected, got)
	}
}

func TestMetricServiceCreation(t *testing.T) {

	jsonFragment := `{
    "consul": "consul:8500",
    "telemetry": {
      "port": 9090
    }
  }`
	if app, err := NewApp(jsonFragment); err != nil {
		t.Fatalf("Got error while initializing config: %v", err)
	} else {
		if len(app.Services) != 1 {
			t.Errorf("Expected telemetry service but got %v", app.Services)
		} else {
			service := app.Services[0]
			if service.Name != "containerpilot" {
				t.Errorf("Got incorrect service back: %v", service)
			}
			for _, envVar := range os.Environ() {
				if strings.HasPrefix(envVar, "CONTAINERPILOT_CONTAINERPILOT_IP") {
					return
				}
			}
			t.Errorf("Did not find CONTAINERPILOT_CONTAINERPILOT_IP env var")
		}
	}
}

func TestPidEnvVar(t *testing.T) {
	defer argTestCleanup(argTestSetup())
	os.Args = []string{"this", "-config", "{}", "/testdata/test.sh"}
	if _, err := LoadApp(); err == nil {
		t.Fatalf("Expected error in LoadApp but got none")
	}
	if pid := os.Getenv("CONTAINERPILOT_PID"); pid == "" {
		t.Errorf("Expected CONTAINERPILOT_PID to be set even on error")
	}
}

// ----------------------------------------------------
// test helpers

func argTestSetup() []string {
	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
	flag.Usage = nil
	return os.Args
}

func argTestCleanup(oldArgs []string) {
	os.Args = oldArgs
}

func testParseExpectError(t *testing.T, testJSON string, expected string) {
	os.Args = []string{"this", "-config", testJSON, "/testdata/test.sh", "test", "--debug"}
	if _, err := LoadApp(); err != nil && !strings.Contains(err.Error(), expected) {
		t.Errorf("Expected %s but got %s", expected, err)
	}
}

func validateParseError(t *testing.T, testJSON string, matchStrings []string) {
	if _, err := NewApp(testJSON); err == nil {
		t.Errorf("Expected error parsing config")
	} else {
		for _, match := range matchStrings {
			if !strings.Contains(err.Error(), match) {
				t.Errorf("Expected message does not contain %s: %s", match, err)
			}
		}
	}
}

func validateCommandParsed(t *testing.T, name string, parsed *commands.Command,
	expectedExec string, expectedArgs []string) {
	if parsed == nil {
		t.Errorf("%s not configured", name)
	}
	if !reflect.DeepEqual(parsed.Exec, expectedExec) {
		t.Errorf("%s executable not configured: %s != %s", name, parsed.Exec, expectedExec)
	}
	if !reflect.DeepEqual(parsed.Args, expectedArgs) {
		t.Errorf("%s arguments not configured: %s != %s", name, parsed.Args, expectedArgs)
	}
}
<|endoftext|>


<commit_before>package aws

import (
	"fmt"
	"log"
	"strconv"
	"strings"
	"testing"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/batch"
	"github.com/hashicorp/terraform/helper/acctest"
	"github.com/hashicorp/terraform/helper/resource"
	"github.com/hashicorp/terraform/terraform"
)

func init() {
	resource.AddTestSweepers("aws_batch_job_queue", &resource.Sweeper{
		Name: "aws_batch_job_queue",
		F:    testSweepBatchJobQueues,
	})
}

func testSweepBatchJobQueues(region string) error {
	client, err := sharedClientForRegion(region)
	if err != nil {
		return fmt.Errorf("error getting client: %s", err)
	}
	conn := client.(*AWSClient).batchconn

	prefixes := []string{
		"tf_acc",
	}

	out, err := conn.DescribeJobQueues(&batch.DescribeJobQueuesInput{})
	if err != nil {
		if testSweepSkipSweepError(err) {
			log.Printf("[WARN] Skipping Batch Job Queue sweep for %s: %s", region, err)
			return nil
		}
		return fmt.Errorf("Error retrieving Batch Job Queues: %s", err)
	}
	for _, jobQueue := range out.JobQueues {
		name := jobQueue.JobQueueName
		skip := true
		for _, prefix := range prefixes {
			if strings.HasPrefix(*name, prefix) {
				skip = false
				break
			}
		}
		if skip {
			log.Printf("[INFO] Skipping Batch Job Queue: %s", *name)
			continue
		}

		log.Printf("[INFO] Disabling Batch Job Queue: %s", *name)
		err := disableBatchJobQueue(*name, conn)
		if err != nil {
			log.Printf("[ERROR] Failed to disable Batch Job Queue %s: %s", *name, err)
			continue
		}

		log.Printf("[INFO] Deleting Batch Job Queue: %s", *name)
		err = deleteBatchJobQueue(*name, conn)
		if err != nil {
			log.Printf("[ERROR] Failed to delete Batch Job Queue %s: %s", *name, err)
		}
	}

	return nil
}

func TestAccAWSBatchJobQueue_basic(t *testing.T) {
	var jq batch.JobQueueDetail
	ri := acctest.RandInt()
	config := fmt.Sprintf(testAccBatchJobQueueBasic, ri)
	resource.ParallelTest(t, resource.TestCase{
		PreCheck:     func() { testAccPreCheck(t) },
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckBatchJobQueueDestroy,
		Steps: []resource.TestStep{
			{
				Config: config,
				Check: resource.ComposeTestCheckFunc(
					testAccCheckBatchJobQueueExists("aws_batch_job_queue.test_queue", &jq),
					testAccCheckBatchJobQueueAttributes(&jq),
				),
			},
		},
	})
}

func TestAccAWSBatchJobQueue_disappears(t *testing.T) {
	var jobQueue1 batch.JobQueueDetail
	resourceName := "aws_batch_job_queue.test_queue"
	rInt := acctest.RandInt()

	resource.ParallelTest(t, resource.TestCase{
		PreCheck:     func() { testAccPreCheck(t) },
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckAWSLaunchTemplateDestroy,
		Steps: []resource.TestStep{
			{
				Config: fmt.Sprintf(testAccBatchJobQueueBasic, rInt),
				Check: resource.ComposeTestCheckFunc(
					testAccCheckBatchJobQueueExists(resourceName, &jobQueue1),
					testAccCheckBatchJobQueueDisappears(&jobQueue1),
				),
				ExpectNonEmptyPlan: true,
			},
		},
	})
}

func TestAccAWSBatchJobQueue_update(t *testing.T) {
	var jq batch.JobQueueDetail
	ri := acctest.RandInt()
	config := fmt.Sprintf(testAccBatchJobQueueBasic, ri)
	updateConfig := fmt.Sprintf(testAccBatchJobQueueUpdate, ri)
	resource.ParallelTest(t, resource.TestCase{
		PreCheck:     func() { testAccPreCheck(t) },
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckBatchJobQueueDestroy,
		Steps: []resource.TestStep{
			{
				Config: config,
				Check: resource.ComposeTestCheckFunc(
					testAccCheckBatchJobQueueExists("aws_batch_job_queue.test_queue", &jq),
					testAccCheckBatchJobQueueAttributes(&jq),
				),
			},
			{
				Config: updateConfig,
				Check: resource.ComposeTestCheckFunc(
					testAccCheckBatchJobQueueExists("aws_batch_job_queue.test_queue", &jq),
					testAccCheckBatchJobQueueAttributes(&jq),
				),
			},
		},
	})
}

func testAccCheckBatchJobQueueExists(n string, jq *batch.JobQueueDetail) resource.TestCheckFunc {
	return func(s *terraform.State) error {
		rs, ok := s.RootModule().Resources[n]
		log.Printf("State: %#v", s.RootModule().Resources)
		if !ok {
			return fmt.Errorf("Not found: %s", n)
		}

		if rs.Primary.ID == "" {
			return fmt.Errorf("No Batch Job Queue ID is set")
		}

		conn := testAccProvider.Meta().(*AWSClient).batchconn
		name := rs.Primary.Attributes["name"]
		queue, err := getJobQueue(conn, name)
		if err != nil {
			return err
		}
		if queue == nil {
			return fmt.Errorf("Not found: %s", n)
		}
		*jq = *queue

		return nil
	}
}

func testAccCheckBatchJobQueueAttributes(jq *batch.JobQueueDetail) resource.TestCheckFunc {
	return func(s *terraform.State) error {
		if !strings.HasPrefix(*jq.JobQueueName, "tf_acctest_batch_job_queue") {
			return fmt.Errorf("Bad Job Queue name: %s", *jq.JobQueueName)
		}
		for _, rs := range s.RootModule().Resources {
			if rs.Type != "aws_batch_job_queue" {
				continue
			}
			if *jq.JobQueueArn != rs.Primary.Attributes["arn"] {
				return fmt.Errorf("Bad Job Queue ARN\n\t expected: %s\n\tgot: %s\n", rs.Primary.Attributes["arn"], *jq.JobQueueArn)
			}
			if *jq.State != rs.Primary.Attributes["state"] {
				return fmt.Errorf("Bad Job Queue State\n\t expected: %s\n\tgot: %s\n", rs.Primary.Attributes["state"], *jq.State)
			}
			priority, err := strconv.ParseInt(rs.Primary.Attributes["priority"], 10, 64)
			if err != nil {
				return err
			}
			if *jq.Priority != priority {
				return fmt.Errorf("Bad Job Queue Priority\n\t expected: %s\n\tgot: %d\n", rs.Primary.Attributes["priority"], *jq.Priority)
			}
		}
		return nil
	}
}

func testAccCheckBatchJobQueueDestroy(s *terraform.State) error {
	for _, rs := range s.RootModule().Resources {
		if rs.Type != "aws_batch_job_queue" {
			continue
		}
		conn := testAccProvider.Meta().(*AWSClient).batchconn
		jq, err := getJobQueue(conn, rs.Primary.Attributes["name"])
		if err == nil {
			if jq != nil {
				return fmt.Errorf("Error: Job Queue still exists")
			}
		}
		return nil
	}
	return nil
}

func testAccCheckBatchJobQueueDisappears(jobQueue *batch.JobQueueDetail) resource.TestCheckFunc {
	return func(s *terraform.State) error {
		conn := testAccProvider.Meta().(*AWSClient).batchconn
		name := aws.StringValue(jobQueue.JobQueueName)

		err := disableBatchJobQueue(name, conn)
		if err != nil {
			return fmt.Errorf("error disabling Batch Job Queue (%s): %s", name, err)
		}

		return deleteBatchJobQueue(name, conn)
	}
}

const testAccBatchJobQueueBaseConfig = `
########## ecs_instance_role ##########

resource "aws_iam_role" "ecs_instance_role" {
  name = "ecs_instance_role_%[1]d"
  assume_role_policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
	{
	    "Action": "sts:AssumeRole",
	    "Effect": "Allow",
	    "Principal": {
		"Service": "ec2.amazonaws.com"
	    }
	}
    ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "ecs_instance_role" {
  role       = "${aws_iam_role.ecs_instance_role.name}"
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
}

resource "aws_iam_instance_profile" "ecs_instance_role" {
  name  = "ecs_instance_role_%[1]d"
  role = "${aws_iam_role.ecs_instance_role.name}"
}

########## aws_batch_service_role ##########

resource "aws_iam_role" "aws_batch_service_role" {
  name = "aws_batch_service_role_%[1]d"
  assume_role_policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
	{
	    "Action": "sts:AssumeRole",
	    "Effect": "Allow",
	    "Principal": {
		"Service": "batch.amazonaws.com"
	    }
	}
    ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "aws_batch_service_role" {
  role       = "${aws_iam_role.aws_batch_service_role.name}"
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole"
}

########## security group ##########

resource "aws_security_group" "test_acc" {
  name = "aws_batch_compute_environment_security_group_%[1]d"
}

########## subnets ##########

resource "aws_vpc" "test_acc" {
  cidr_block = "10.1.0.0/16"
  tags = {
    Name = "terraform-testacc-batch-job-queue"
  }
}

resource "aws_subnet" "test_acc" {
  vpc_id = "${aws_vpc.test_acc.id}"
  cidr_block = "10.1.1.0/24"
  tags = {
    Name = "tf-acc-batch-job-queue"
  }
}

resource "aws_batch_compute_environment" "test_environment" {
  compute_environment_name = "tf_acctest_batch_compute_environment_%[1]d"
  compute_resources = {
    instance_role = "${aws_iam_role.aws_batch_service_role.arn}"
    instance_type = ["m3.medium"]
    max_vcpus = 1
    min_vcpus = 0
    security_group_ids = ["${aws_security_group.test_acc.id}"]
    subnets = ["${aws_subnet.test_acc.id}"]
    type = "EC2"
  }
  service_role = "${aws_iam_role.aws_batch_service_role.arn}"
  type = "MANAGED"
  depends_on = ["aws_iam_role_policy_attachment.aws_batch_service_role"]
}`

var testAccBatchJobQueueBasic = testAccBatchJobQueueBaseConfig + `
resource "aws_batch_job_queue" "test_queue" {
  name = "tf_acctest_batch_job_queue_%[1]d"
  state = "ENABLED"
  priority = 1
  compute_environments = ["${aws_batch_compute_environment.test_environment.arn}"]
}`

var testAccBatchJobQueueUpdate = testAccBatchJobQueueBaseConfig + `
resource "aws_batch_job_queue" "test_queue" {
  name = "tf_acctest_batch_job_queue_%[1]d"
  state = "DISABLED"
  priority = 2
  compute_environments = ["${aws_batch_compute_environment.test_environment.arn}"]
}`
<commit_msg>tests/resource/aws_batch_job_queue: Omit equals for compute_resources configuration<commit_after>package aws

import (
	"fmt"
	"log"
	"strconv"
	"strings"
	"testing"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/batch"
	"github.com/hashicorp/terraform/helper/acctest"
	"github.com/hashicorp/terraform/helper/resource"
	"github.com/hashicorp/terraform/terraform"
)

func init() {
	resource.AddTestSweepers("aws_batch_job_queue", &resource.Sweeper{
		Name: "aws_batch_job_queue",
		F:    testSweepBatchJobQueues,
	})
}

func testSweepBatchJobQueues(region string) error {
	client, err := sharedClientForRegion(region)
	if err != nil {
		return fmt.Errorf("error getting client: %s", err)
	}
	conn := client.(*AWSClient).batchconn

	prefixes := []string{
		"tf_acc",
	}

	out, err := conn.DescribeJobQueues(&batch.DescribeJobQueuesInput{})
	if err != nil {
		if testSweepSkipSweepError(err) {
			log.Printf("[WARN] Skipping Batch Job Queue sweep for %s: %s", region, err)
			return nil
		}
		return fmt.Errorf("Error retrieving Batch Job Queues: %s", err)
	}
	for _, jobQueue := range out.JobQueues {
		name := jobQueue.JobQueueName
		skip := true
		for _, prefix := range prefixes {
			if strings.HasPrefix(*name, prefix) {
				skip = false
				break
			}
		}
		if skip {
			log.Printf("[INFO] Skipping Batch Job Queue: %s", *name)
			continue
		}

		log.Printf("[INFO] Disabling Batch Job Queue: %s", *name)
		err := disableBatchJobQueue(*name, conn)
		if err != nil {
			log.Printf("[ERROR] Failed to disable Batch Job Queue %s: %s", *name, err)
			continue
		}

		log.Printf("[INFO] Deleting Batch Job Queue: %s", *name)
		err = deleteBatchJobQueue(*name, conn)
		if err != nil {
			log.Printf("[ERROR] Failed to delete Batch Job Queue %s: %s", *name, err)
		}
	}

	return nil
}

func TestAccAWSBatchJobQueue_basic(t *testing.T) {
	var jq batch.JobQueueDetail
	ri := acctest.RandInt()
	config := fmt.Sprintf(testAccBatchJobQueueBasic, ri)
	resource.ParallelTest(t, resource.TestCase{
		PreCheck:     func() { testAccPreCheck(t) },
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckBatchJobQueueDestroy,
		Steps: []resource.TestStep{
			{
				Config: config,
				Check: resource.ComposeTestCheckFunc(
					testAccCheckBatchJobQueueExists("aws_batch_job_queue.test_queue", &jq),
					testAccCheckBatchJobQueueAttributes(&jq),
				),
			},
		},
	})
}

func TestAccAWSBatchJobQueue_disappears(t *testing.T) {
	var jobQueue1 batch.JobQueueDetail
	resourceName := "aws_batch_job_queue.test_queue"
	rInt := acctest.RandInt()

	resource.ParallelTest(t, resource.TestCase{
		PreCheck:     func() { testAccPreCheck(t) },
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckAWSLaunchTemplateDestroy,
		Steps: []resource.TestStep{
			{
				Config: fmt.Sprintf(testAccBatchJobQueueBasic, rInt),
				Check: resource.ComposeTestCheckFunc(
					testAccCheckBatchJobQueueExists(resourceName, &jobQueue1),
					testAccCheckBatchJobQueueDisappears(&jobQueue1),
				),
				ExpectNonEmptyPlan: true,
			},
		},
	})
}

func TestAccAWSBatchJobQueue_update(t *testing.T) {
	var jq batch.JobQueueDetail
	ri := acctest.RandInt()
	config := fmt.Sprintf(testAccBatchJobQueueBasic, ri)
	updateConfig := fmt.Sprintf(testAccBatchJobQueueUpdate, ri)
	resource.ParallelTest(t, resource.TestCase{
		PreCheck:     func() { testAccPreCheck(t) },
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckBatchJobQueueDestroy,
		Steps: []resource.TestStep{
			{
				Config: config,
				Check: resource.ComposeTestCheckFunc(
					testAccCheckBatchJobQueueExists("aws_batch_job_queue.test_queue", &jq),
					testAccCheckBatchJobQueueAttributes(&jq),
				),
			},
			{
				Config: updateConfig,
				Check: resource.ComposeTestCheckFunc(
					testAccCheckBatchJobQueueExists("aws_batch_job_queue.test_queue", &jq),
					testAccCheckBatchJobQueueAttributes(&jq),
				),
			},
		},
	})
}

func testAccCheckBatchJobQueueExists(n string, jq *batch.JobQueueDetail) resource.TestCheckFunc {
	return func(s *terraform.State) error {
		rs, ok := s.RootModule().Resources[n]
		log.Printf("State: %#v", s.RootModule().Resources)
		if !ok {
			return fmt.Errorf("Not found: %s", n)
		}

		if rs.Primary.ID == "" {
			return fmt.Errorf("No Batch Job Queue ID is set")
		}

		conn := testAccProvider.Meta().(*AWSClient).batchconn
		name := rs.Primary.Attributes["name"]
		queue, err := getJobQueue(conn, name)
		if err != nil {
			return err
		}
		if queue == nil {
			return fmt.Errorf("Not found: %s", n)
		}
		*jq = *queue

		return nil
	}
}

func testAccCheckBatchJobQueueAttributes(jq *batch.JobQueueDetail) resource.TestCheckFunc {
	return func(s *terraform.State) error {
		if !strings.HasPrefix(*jq.JobQueueName, "tf_acctest_batch_job_queue") {
			return fmt.Errorf("Bad Job Queue name: %s", *jq.JobQueueName)
		}
		for _, rs := range s.RootModule().Resources {
			if rs.Type != "aws_batch_job_queue" {
				continue
			}
			if *jq.JobQueueArn != rs.Primary.Attributes["arn"] {
				return fmt.Errorf("Bad Job Queue ARN\n\t expected: %s\n\tgot: %s\n", rs.Primary.Attributes["arn"], *jq.JobQueueArn)
			}
			if *jq.State != rs.Primary.Attributes["state"] {
				return fmt.Errorf("Bad Job Queue State\n\t expected: %s\n\tgot: %s\n", rs.Primary.Attributes["state"], *jq.State)
			}
			priority, err := strconv.ParseInt(rs.Primary.Attributes["priority"], 10, 64)
			if err != nil {
				return err
			}
			if *jq.Priority != priority {
				return fmt.Errorf("Bad Job Queue Priority\n\t expected: %s\n\tgot: %d\n", rs.Primary.Attributes["priority"], *jq.Priority)
			}
		}
		return nil
	}
}

func testAccCheckBatchJobQueueDestroy(s *terraform.State) error {
	for _, rs := range s.RootModule().Resources {
		if rs.Type != "aws_batch_job_queue" {
			continue
		}
		conn := testAccProvider.Meta().(*AWSClient).batchconn
		jq, err := getJobQueue(conn, rs.Primary.Attributes["name"])
		if err == nil {
			if jq != nil {
				return fmt.Errorf("Error: Job Queue still exists")
			}
		}
		return nil
	}
	return nil
}

func testAccCheckBatchJobQueueDisappears(jobQueue *batch.JobQueueDetail) resource.TestCheckFunc {
	return func(s *terraform.State) error {
		conn := testAccProvider.Meta().(*AWSClient).batchconn
		name := aws.StringValue(jobQueue.JobQueueName)

		err := disableBatchJobQueue(name, conn)
		if err != nil {
			return fmt.Errorf("error disabling Batch Job Queue (%s): %s", name, err)
		}

		return deleteBatchJobQueue(name, conn)
	}
}

const testAccBatchJobQueueBaseConfig = `
########## ecs_instance_role ##########

resource "aws_iam_role" "ecs_instance_role" {
  name = "ecs_instance_role_%[1]d"
  assume_role_policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
	{
	    "Action": "sts:AssumeRole",
	    "Effect": "Allow",
	    "Principal": {
		"Service": "ec2.amazonaws.com"
	    }
	}
    ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "ecs_instance_role" {
  role       = "${aws_iam_role.ecs_instance_role.name}"
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
}

resource "aws_iam_instance_profile" "ecs_instance_role" {
  name  = "ecs_instance_role_%[1]d"
  role = "${aws_iam_role.ecs_instance_role.name}"
}

########## aws_batch_service_role ##########

resource "aws_iam_role" "aws_batch_service_role" {
  name = "aws_batch_service_role_%[1]d"
  assume_role_policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
	{
	    "Action": "sts:AssumeRole",
	    "Effect": "Allow",
	    "Principal": {
		"Service": "batch.amazonaws.com"
	    }
	}
    ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "aws_batch_service_role" {
  role       = "${aws_iam_role.aws_batch_service_role.name}"
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole"
}

########## security group ##########

resource "aws_security_group" "test_acc" {
  name = "aws_batch_compute_environment_security_group_%[1]d"
}

########## subnets ##########

resource "aws_vpc" "test_acc" {
  cidr_block = "10.1.0.0/16"
  tags = {
    Name = "terraform-testacc-batch-job-queue"
  }
}

resource "aws_subnet" "test_acc" {
  vpc_id = "${aws_vpc.test_acc.id}"
  cidr_block = "10.1.1.0/24"
  tags = {
    Name = "tf-acc-batch-job-queue"
  }
}

resource "aws_batch_compute_environment" "test_environment" {
  compute_environment_name = "tf_acctest_batch_compute_environment_%[1]d"
  compute_resources {
    instance_role = "${aws_iam_role.aws_batch_service_role.arn}"
    instance_type = ["m3.medium"]
    max_vcpus = 1
    min_vcpus = 0
    security_group_ids = ["${aws_security_group.test_acc.id}"]
    subnets = ["${aws_subnet.test_acc.id}"]
    type = "EC2"
  }
  service_role = "${aws_iam_role.aws_batch_service_role.arn}"
  type = "MANAGED"
  depends_on = ["aws_iam_role_policy_attachment.aws_batch_service_role"]
}`

var testAccBatchJobQueueBasic = testAccBatchJobQueueBaseConfig + `
resource "aws_batch_job_queue" "test_queue" {
  name = "tf_acctest_batch_job_queue_%[1]d"
  state = "ENABLED"
  priority = 1
  compute_environments = ["${aws_batch_compute_environment.test_environment.arn}"]
}`

var testAccBatchJobQueueUpdate = testAccBatchJobQueueBaseConfig + `
resource "aws_batch_job_queue" "test_queue" {
  name = "tf_acctest_batch_job_queue_%[1]d"
  state = "DISABLED"
  priority = 2
  compute_environments = ["${aws_batch_compute_environment.test_environment.arn}"]
}`
<|endoftext|>


<commit_before>/*
	Here we create a randomly generated maze/labyrinth using Prim's algorithm.
	Description of said algorithm can be found on https://en.wikipedia.org/wiki/Prim%27s_algorithm
	We also define structure Point which is used in the maze generation as well tracking the position
	of various objects/structures(npc, traps, the playeble character ect)
	Not only do we generate the labyrith but we also run chechs,
	for where to place truesures(in dead-ends of the maze) and traps(on crossroads)
*/

package Labyrinth

import "math/rand"

var wall string = "8"
var pass string = " "
var treasure string = "$"
var trap string = "*"
pointMap := make(map[Point]bool)

//The structure has 3 fields:
//	integers for X and Y coordinates
//	field for parent Point
type Point struct{
	x,y int
	parent *Point
}

//This function returns the opposite of a given Point given its Parent Point
//The returned Point is used for our maze generation sequence
//refer to Prim's algorithm for more details
func (point Point) Opposite() Point {
	return Point{2 * point.x - point.parent.x, 2 * point.y - point.parent.y, &point}
}

//The stucture has two fields:
//integers width and heigth of the labyrinth/2D array
//2D array of strings(of characters) which will represent the generated maze
type Labyrinth struct{
	width, heigt int
	//rng int
	labyrinth [width][heigt]string
}

//The main algorithm used to generate the maze
//"0" for wall cells
//" " for empty cells(a.k.a path)
func (lab *Labyrinth) Prim(seed int64) {
	frontier := make([]Point, 0, 40)
	rand.Seed(seed)
	start := Point{rand.Intn(lab.width - 1) + 1, rand.Intn(lab.width - 1) + 1, nil}
	lab.labyrinth[start.x][start.y] = wall
	lab.Neighbours(&start, &frontier)
	for {
		randomPoint := rand.Intn(len(frontier))
		current := frontier[randomPoint]
		frontier = append(frontier[:randomPoint], frontier[randomPoint +1])

		opposite := current.Opposite()
		last := opposite

		if lab.labyrinth[opposite.x][opposite.y] == wall {
			lab.labyrinth[current.x][current.y] == pass

			if opposite.x != 0 && opposite.x != lab.width - 1 && opposite.y != 0 && opposite.y != lab.heigth - 1 {
				lab.labyrinth[opposite.x][opposite.y] == pass
			}

			lab.Neighbours(&opposite, &frontier)
		}
		if len(frontier) == 0 {
			lab.labyrinth[last.x][last.y] = "E"
			break
		}
	}
}

//all neighbours(left, right, top, bottom) of a given Point will be passed to AddNeighbour
func(lab *Labyrinth) Neighbours(point *Point, frontier *[]Point) {
	lab.AddNeighbour(*point.x + 1, *point.y    , *point.parent, &frontier)
	lab.AddNeighbour(*point.x - 1, *point.y    , *point.parent, &frontier)
	lab.AddNeighbour(*point.x    , *point.y + 1, *point.parent, &frontier)
	lab.AddNeighbour(*point.x    , *point.y - 1, *point.parent, &frontier)
}

//adds the Neighbours af a give point to the frontier list which is used in the maze generation algorithm
func(lab *Labyrinth) AddNeighbour(x int, y int, parent Point, frontier *[]Point) {
	if !pointMap[Point{x, y, parent}] {
		if (x > 0 && x < lab.width - 1) && (y > 0 && y < lab.heigth - 1) {
			pointToBeAdd := Point{x, y, &parent}
			frontier = append(frontier, pointToBeAdd)
			pointMap[pointToBeAdd] = true
		}
	}
}

//This function will make all outher cells of the maze "wall" cells
//unless the algorithm is improved enough so that this step becomes obsolete
func(lab *Labyrinth) AddBorder() {

}

// IsTreasure 25% to place a treasure at a dead-end in the maze
func(lab *Labyrinth) IsTreasure(x int, y int) bool {
	return true
}

//Determines if a given point has 3 neighnours that are "wall" cells
func(lab *Labyrinth) IsDeadEnd(situation [4]string) bool {
	return true
}

//places a "T" for treasure in the 2d array at x and y coordinates
func(lab *Labyrinth) CreateTreasure(x int, y int) {

}

//Determines if a given point is a crossroad, a point that has 1 or 0 neighbours that are "wall" cells
func(lab *Labyrinth) IsCrossRoad(situation [4]string) bool {
	return true
}

//at a given crossroad randoms whethere the tile will be a trap
func(lab *Labyrinth) IsTrap(x int, y int,) bool {
	return true
}

//creates a trap at give coordinates
func(lab *Labyrinth) CreateTrap(x int, y int) {

}<commit_msg>minor fix<commit_after>/*
	Here we create a randomly generated maze/labyrinth using Prim's algorithm.
	Description of said algorithm can be found on https://en.wikipedia.org/wiki/Prim%27s_algorithm
	We also define structure Point which is used in the maze generation as well tracking the position
	of various objects/structures(npc, traps, the playeble character ect)
	Not only do we generate the labyrith but we also run chechs,
	for where to place truesures(in dead-ends of the maze) and traps(on crossroads)
*/

package Labyrinth

import "math/rand"

var wall string = "0"
var pass string = " "
var treasure string = "$"
var trap string = "*"
var pointMap = map[Point]bool{}

//The structure has 3 fields:
//	integers for X and Y coordinates
//	field for parent Point
type Point struct{
	x,y int
	parent *Point
}

//This function returns the opposite of a given Point given its Parent Point
//The returned Point is used for our maze generation sequence
//refer to Prim's algorithm for more details
func (point Point) Opposite() Point {
	return Point{2 * point.x - point.parent.x, 2 * point.y - point.parent.y, &point}
}

//The stucture has two fields:
//integers width and height of the labyrinth/2D array
//2D array of strings(of characters) which will represent the generated maze
type Labyrinth struct{
	width, height int
	//rng int
	labyrinth [40][40]string
}

//The main algorithm used to generate the maze
//"0" for wall cells
//" " for empty cells(a.k.a path)
func (lab *Labyrinth) Prim(seed int64) {
	frontier := make([]Point, 0, 40)
	rand.Seed(seed)
	start := Point{rand.Intn(lab.width - 1) + 1, rand.Intn(lab.width - 1) + 1, nil}
	lab.labyrinth[start.x][start.y] = trap
	lab.Neighbours(&start, &frontier)
	for {
		randomPoint := rand.Intn(len(frontier))
		current := frontier[randomPoint]
		frontier = append(frontier[:randomPoint], frontier[randomPoint +1:]...)

		opposite := current.Opposite()
		last := opposite

		if lab.labyrinth[opposite.x][opposite.y] == wall {
			lab.labyrinth[current.x][current.y] = pass

			if opposite.x != 0 && opposite.x != lab.width - 1 && opposite.y != 0 && opposite.y != lab.height - 1 {
				lab.labyrinth[opposite.x][opposite.y] = pass
			}

			lab.Neighbours(&opposite, &frontier)
		}
		if len(frontier) == 0 {
			lab.labyrinth[last.x][last.y] = "E"
			break
		}
	}
}

//all neighbours(left, right, top, bottom) of a given Point will be passed to AddNeighbour
func(lab *Labyrinth) Neighbours(point *Point, frontier *[]Point) {
	lab.AddNeighbour(point.x + 1, point.y  	 , point, frontier)
	lab.AddNeighbour(point.x - 1, point.y 	 , point, frontier)
	lab.AddNeighbour(point.x    , point.y + 1,point, frontier)
	lab.AddNeighbour(point.x    , point.y - 1,point, frontier)
}

//adds the Neighbours af a give point to the frontier list which is used in the maze generation algorithm
func(lab *Labyrinth) AddNeighbour(x int, y int, parent *Point, frontier *[]Point) {
	if !pointMap[Point{x, y, parent}] {
		if (x > 0 && x < lab.width - 1) && (y > 0 && y < lab.height - 1) {
			pointToBeAdd := Point{x, y, parent}
			*frontier = append(*frontier, pointToBeAdd)
			pointMap[pointToBeAdd] = true
		}
	}
}

//This function will make all outher cells of the maze "wall" cells
//unless the algorithm is improved enough so that this step becomes obsolete
func(lab *Labyrinth) AddBorder() {

}

//Determines if a given point has 3 neighnours that are "wall" cells
func(lab *Labyrinth) IsDeadEnd(situation [4]string) bool {
	return true
}

// IsTreasure 25% to place a treasure at a dead-end in the maze
func(lab *Labyrinth) IsTreasure(x int, y int) bool {
	return true
}

//places a "T" for treasure in the 2d array at x and y coordinates
func(lab *Labyrinth) CreateTreasure(x int, y int) {

}

//Determines if a given point is a crossroad, a point that has 1 or 0 neighbours that are "wall" cells
func(lab *Labyrinth) IsCrossRoad(situation [4]string) bool {
	return true
}

//at a given crossroad randoms whethere the tile will be a trap
func(lab *Labyrinth) IsTrap(x int, y int,) bool {
	return true
}

//creates a trap at give coordinates
func(lab *Labyrinth) CreateTrap(x int, y int) {

}<|endoftext|>


<commit_before>package taggolib

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"strconv"
	"strings"
	"time"

	"github.com/eaburns/bit"
)

const (
	// Tags specific to ID3v2 MP3
	mp3TagEncoder = "ENCODER"
	mp3TagLength  = "LENGTH"
)

var (
	// mp3MagicNumber is the magic number used to identify a MP3 audio stream
	mp3MagicNumber = []byte("ID3")
	// mp3APICFrame is the name of the APIC, or attached picture ID3 frame
	mp3APICFrame = []byte("APIC")
)

// mp3Parser represents a MP3 audio metadata tag parser
type mp3Parser struct {
	id3Header *mp3ID3v2Header
	mp3Header *mp3Header
	reader    io.ReadSeeker
	tags      map[string]string
}

// Album returns the Album tag for this stream
func (m mp3Parser) Album() string {
	return m.tags[tagAlbum]
}

// AlbumArtist returns the AlbumArtist tag for this stream
func (m mp3Parser) AlbumArtist() string {
	return m.tags[tagAlbumArtist]
}

// Artist returns the Artist tag for this stream
func (m mp3Parser) Artist() string {
	return m.tags[tagArtist]
}

// BitDepth returns the bits-per-sample of this stream
func (m mp3Parser) BitDepth() int {
	return 16
}

// Bitrate calculates the audio bitrate for this stream
func (m mp3Parser) Bitrate() int {
	return mp3BitrateMap[m.mp3Header.Bitrate]
}

// Channels returns the number of channels for this stream
func (m mp3Parser) Channels() int {
	return mp3ChannelModeMap[m.mp3Header.ChannelMode]
}

// Comment returns the Comment tag for this stream
func (m mp3Parser) Comment() string {
	return m.tags[tagComment]
}

// Date returns the Date tag for this stream
func (m mp3Parser) Date() string {
	return m.tags[tagDate]
}

// DiscNumber returns the DiscNumber tag for this stream
func (m mp3Parser) DiscNumber() int {
	disc, err := strconv.Atoi(m.tags[tagDiscNumber])
	if err != nil {
		return 0
	}

	return disc
}

// Duration returns the time duration for this stream
func (m mp3Parser) Duration() time.Duration {
	// Parse length as integer
	length, err := strconv.Atoi(m.tags[mp3TagLength])
	if err != nil {
		return time.Duration(0 * time.Second)
	}

	return time.Duration(length/1000) * time.Second
}

// Encoder returns the encoder for this stream
func (m mp3Parser) Encoder() string {
	return m.tags[mp3TagEncoder]
}

// Format returns the name of the MP3 format
func (m mp3Parser) Format() string {
	return "MP3"
}

// Genre returns the Genre tag for this stream
func (m mp3Parser) Genre() string {
	return m.tags[tagGenre]
}

// SampleRate returns the sample rate in Hertz for this stream
func (m mp3Parser) SampleRate() int {
	return mp3SampleRateMap[m.mp3Header.SampleRate]
}

// Tag attempts to return the raw, unprocessed tag with the specified name for this stream
func (m mp3Parser) Tag(name string) string {
	return m.tags[name]
}

// Title returns the Title tag for this stream
func (m mp3Parser) Title() string {
	return m.tags[tagTitle]
}

// TrackNumber returns the TrackNumber tag for this stream
func (m mp3Parser) TrackNumber() int {
	// Check for a /, such as 2/8
	track, err := strconv.Atoi(strings.Split(m.tags[tagTrackNumber], "/")[0])
	if err != nil {
		return 0
	}

	return track
}

// newMP3Parser creates a parser for MP3 audio streams
func newMP3Parser(reader io.ReadSeeker) (*mp3Parser, error) {
	// Create MP3 parser
	parser := &mp3Parser{
		reader: reader,
	}

	// Parse ID3v2 header
	if err := parser.parseID3v2Header(); err != nil {
		return nil, err
	}

	// Parse ID3v2 frames
	if err := parser.parseID3v2Frames(); err != nil {
		return nil, err
	}

	// Parse MP3 header
	if err := parser.parseMP3Header(); err != nil {
		return nil, err
	}

	// Return parser
	return parser, nil
}

// parseID3v2Header parses the ID3v2 header at the start of an MP3 stream
func (m *mp3Parser) parseID3v2Header() error {
	// Create and use a bit reader to parse the following fields
	//   8 - ID3v2 major version
	//   8 - ID3v2 minor version
	//   1 - Unsynchronization (boolean) (ID3v2.3+)
	//   1 - Extended (boolean) (ID3v2.3+)
	//   1 - Experimental (boolean) (ID3v2.3+)
	//   1 - Footer (boolean) (ID3v2.4+)
	//   4 - (empty)
	//  32 - Size
	fields, err := bit.NewReader(m.reader).ReadFields(8, 8, 1, 1, 1, 1, 4, 32)
	if err != nil {
		return err
	}

	// Generate ID3v2 header
	m.id3Header = &mp3ID3v2Header{
		MajorVersion:      uint8(fields[0]),
		MinorVersion:      uint8(fields[1]),
		Unsynchronization: fields[2] == 1,
		Extended:          fields[3] == 1,
		Experimental:      fields[4] == 1,
		Footer:            fields[5] == 1,
		Size:              uint32(fields[7]),
	}

	// Ensure ID3v2 version is supported
	if m.id3Header.MajorVersion != 3 && m.id3Header.MajorVersion != 4 {
		return TagError{
			Err:     errUnsupportedVersion,
			Format:  m.Format(),
			Details: fmt.Sprintf("unsupported ID3 version: ID3v2.%d.%d", m.id3Header.MajorVersion, m.id3Header.MinorVersion),
		}
	}

	// Unsychronization is currently not supported
	if m.id3Header.Unsynchronization {
		return TagError{
			Err:     errUnsupportedVersion,
			Format:  m.Format(),
			Details: "ID3 unsynchronization is not supported",
		}
	}

	// Ensure Footer boolean is not defined prior to ID3v2.4
	if m.id3Header.MajorVersion < 4 && m.id3Header.Footer {
		return TagError{
			Err:     errInvalidStream,
			Format:  m.Format(),
			Details: "ID3 footer bit set prior to version ID3v2.4",
		}
	}

	// Check for extended header
	if m.id3Header.Extended {
		// Read size of extended header
		var headerSize uint32
		if err := binary.Read(m.reader, binary.BigEndian, &headerSize); err != nil {
			return err
		}

		// Seek past extended header (minus size of uint32 read), since the information
		// is irrelevant for tag parsing
		if _, err := m.reader.Seek(int64(headerSize)-4, 1); err != nil {
			return err
		}
	}

	return nil
}

// parseID3v2Frames parses ID3v2 frames from an MP3 stream
func (m *mp3Parser) parseID3v2Frames() error {
	// Store discovered tags in map
	tagMap := map[string]string{}

	// Create buffers for frame information
	frameBuf := make([]byte, 4)
	var frameLength uint32
	tagBuf := make([]byte, 2048)

	// Byte slices which should be trimmed and discarded from prefix or suffix
	trimPrefix := []byte{255, 254}
	trimSuffix := []byte{0}

	// Continuously loop and parse frames
	for {
		// Parse a frame title
		if _, err := m.reader.Read(frameBuf); err != nil {
			return err
		}

		// Stop parsing frames when frame title is nil, because we have reached padding
		if frameBuf[0] == byte(0) {
			break
		}

		// If byte 255 discovered, we have reached the start of the MP3 header
		if frameBuf[0] == byte(255) {
			// Pre-seed the current data as a bytes reader, to parse MP3 header
			m.reader = bytes.NewReader(frameBuf)
			break
		}

		// Parse the length of the frame data
		if err := binary.Read(m.reader, binary.BigEndian, &frameLength); err != nil {
			return err
		}

		// Skip over frame flags
		if _, err := m.reader.Seek(2, 1); err != nil {
			return err
		}

		// If frame is APIC, or "attached picture", seek past the picture
		if bytes.Equal(frameBuf, mp3APICFrame) {
			// Seek past picture data and continue loop
			if _, err := m.reader.Seek(int64(frameLength), 1); err != nil {
				return err
			}

			continue
		}

		// Parse the frame data tag
		n, err := m.reader.Read(tagBuf[:frameLength])
		if err != nil {
			return err
		}

		// Trim leading bytes such as UTF-8 BOM, garbage bytes, trim trailing nil
		// TODO: handle encodings that aren't UTF-8, stored in tagBuf[0]
		tag := string(bytes.TrimPrefix(bytes.TrimSuffix(tagBuf[1:n], trimSuffix), trimPrefix))

		// Map frame title to tag title, store frame data
		tagMap[mp3ID3v2FrameToTag[string(frameBuf)]] = tag
	}

	// Store tags in parser
	m.tags = tagMap
	return nil
}

// mp3ID3v2FrameToTag maps a MP3 ID3v2 frame title to its actual tag name
var mp3ID3v2FrameToTag = map[string]string{
	"COMM": tagComment,
	"TALB": tagAlbum,
	"TCON": tagGenre,
	"TDRC": tagDate,
	"TIT2": tagTitle,
	"TLEN": mp3TagLength,
	"TPE1": tagArtist,
	"TPE2": tagAlbumArtist,
	"TPOS": tagDiscNumber,
	"TRCK": tagTrackNumber,
	"TSSE": mp3TagEncoder,
	"TYER": tagDate,
}

// mp3ID3v2Header represents the MP3 ID3v2 header section
type mp3ID3v2Header struct {
	MajorVersion      uint8
	MinorVersion      uint8
	Unsynchronization bool
	Extended          bool
	Experimental      bool
	Footer            bool
	Size              uint32
}

// mp3ID3v2ExtendedHeader reperesents the optional MP3 ID3v2 extended header section
type mp3ID3v2ExtendedHeader struct {
	HeaderSize   uint32
	CRC32Present bool
	PaddingSize  uint32
}

// parseMP3Header parses the MP3 header after the ID3 headers in a MP3 stream
func (m *mp3Parser) parseMP3Header() error {
	// Read buffers continuously until we reach end of padding section, and find the
	// MP3 header, which starts with byte 255
	headerBuf := make([]byte, 128)
	for {
		if _, err := m.reader.Read(headerBuf); err != nil {
			return err
		}

		// If first byte is 255, value was pre-seeded by tag parser
		if headerBuf[0] == byte(255) {
			break
		}

		// Search for byte 255
		index := bytes.Index(headerBuf, []byte{255})
		if index != -1 {
			// We have encountered the header, re-slice forward to its index
			headerBuf = headerBuf[index:]
			break
		}
	}

	// Create and use a bit reader to parse the following fields
	//  11 - MP3 frame sync (all bits set)
	//   2 - MPEG audio version ID
	//   2 - Layer description
	//   1 - Protection bit (boolean)
	//   4 - Bitrate index
	fields, err := bit.NewReader(bytes.NewReader(headerBuf)).ReadFields(11, 2, 2, 1, 4, 2, 1, 1, 2)
	if err != nil {
		return err
	}

	// Create output MP3 header
	m.mp3Header = &mp3Header{
		MPEGVersionID: uint8(fields[1]),
		MPEGLayerID:   uint8(fields[2]),
		Protected:     fields[3] == 0,
		Bitrate:       uint16(fields[4]),
		SampleRate:    uint16(fields[5]),
		Padding:       fields[6] == 1,
		Private:       fields[7] == 1,
		ChannelMode:   uint8(fields[8]),
	}

	// Check to make sure we are parsing MPEG Version 1, Layer 3
	// Note: this check is correct, as these values actually map to:
	//   - Version ID 3 -> MPEG Version 1
	//   - Layer ID 1 -> MPEG Layer 3
	if m.mp3Header.MPEGVersionID != 3 {
		return TagError{
			Err:     errUnsupportedVersion,
			Format:  m.Format(),
			Details: fmt.Sprintf("unsupported MPEG version ID: %d", m.mp3Header.MPEGVersionID),
		}
	}

	if m.mp3Header.MPEGLayerID != 1 {
		return TagError{
			Err:     errUnsupportedVersion,
			Format:  m.Format(),
			Details: fmt.Sprintf("unsupported MPEG layer ID: %d", m.mp3Header.MPEGLayerID),
		}
	}

	return nil
}

// mp3Header represents a MP3 audio stream header, and contains information about the stream
type mp3Header struct {
	MPEGVersionID uint8
	MPEGLayerID   uint8
	Protected     bool
	Bitrate       uint16
	SampleRate    uint16
	Padding       bool
	Private       bool
	ChannelMode   uint8
}

// mp3BitrateMap maps MPEG Layer 3 Version 1 bitrate to its actual rate
var mp3BitrateMap = map[uint16]int{
	0:  0,
	1:  32,
	2:  40,
	3:  48,
	4:  56,
	5:  64,
	6:  80,
	7:  96,
	8:  112,
	9:  128,
	10: 160,
	11: 192,
	12: 224,
	13: 256,
	14: 320,
}

// mp3SampleRateMap maps MPEG Layer 3 Version 1 sample rate to its actual rate
var mp3SampleRateMap = map[uint16]int{
	0: 44100,
	1: 48000,
	2: 32000,
}

// mp3ChannelModeMap maps MPEG Layer 3 Version 1 channels to the number of channels
var mp3ChannelModeMap = map[uint8]int{
	0: 2,
	1: 2,
	3: 2,
	4: 1,
}
<commit_msg>Note bug in MP3 duration parsing<commit_after>package taggolib

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"strconv"
	"strings"
	"time"

	"github.com/eaburns/bit"
)

const (
	// Tags specific to ID3v2 MP3
	mp3TagEncoder = "ENCODER"
	mp3TagLength  = "LENGTH"
)

var (
	// mp3MagicNumber is the magic number used to identify a MP3 audio stream
	mp3MagicNumber = []byte("ID3")
	// mp3APICFrame is the name of the APIC, or attached picture ID3 frame
	mp3APICFrame = []byte("APIC")
)

// mp3Parser represents a MP3 audio metadata tag parser
type mp3Parser struct {
	id3Header *mp3ID3v2Header
	mp3Header *mp3Header
	reader    io.ReadSeeker
	tags      map[string]string
}

// Album returns the Album tag for this stream
func (m mp3Parser) Album() string {
	return m.tags[tagAlbum]
}

// AlbumArtist returns the AlbumArtist tag for this stream
func (m mp3Parser) AlbumArtist() string {
	return m.tags[tagAlbumArtist]
}

// Artist returns the Artist tag for this stream
func (m mp3Parser) Artist() string {
	return m.tags[tagArtist]
}

// BitDepth returns the bits-per-sample of this stream
func (m mp3Parser) BitDepth() int {
	return 16
}

// Bitrate calculates the audio bitrate for this stream
func (m mp3Parser) Bitrate() int {
	return mp3BitrateMap[m.mp3Header.Bitrate]
}

// Channels returns the number of channels for this stream
func (m mp3Parser) Channels() int {
	return mp3ChannelModeMap[m.mp3Header.ChannelMode]
}

// Comment returns the Comment tag for this stream
func (m mp3Parser) Comment() string {
	return m.tags[tagComment]
}

// Date returns the Date tag for this stream
func (m mp3Parser) Date() string {
	return m.tags[tagDate]
}

// DiscNumber returns the DiscNumber tag for this stream
func (m mp3Parser) DiscNumber() int {
	disc, err := strconv.Atoi(m.tags[tagDiscNumber])
	if err != nil {
		return 0
	}

	return disc
}

// Duration returns the time duration for this stream
// BUG(mdlayher): if the LENGTH tag is not present, this will always return duration 0
func (m mp3Parser) Duration() time.Duration {
	// Parse length as integer
	length, err := strconv.Atoi(m.tags[mp3TagLength])
	if err != nil {
		return time.Duration(0 * time.Second)
	}

	return time.Duration(length/1000) * time.Second
}

// Encoder returns the encoder for this stream
func (m mp3Parser) Encoder() string {
	return m.tags[mp3TagEncoder]
}

// Format returns the name of the MP3 format
func (m mp3Parser) Format() string {
	return "MP3"
}

// Genre returns the Genre tag for this stream
func (m mp3Parser) Genre() string {
	return m.tags[tagGenre]
}

// SampleRate returns the sample rate in Hertz for this stream
func (m mp3Parser) SampleRate() int {
	return mp3SampleRateMap[m.mp3Header.SampleRate]
}

// Tag attempts to return the raw, unprocessed tag with the specified name for this stream
func (m mp3Parser) Tag(name string) string {
	return m.tags[name]
}

// Title returns the Title tag for this stream
func (m mp3Parser) Title() string {
	return m.tags[tagTitle]
}

// TrackNumber returns the TrackNumber tag for this stream
func (m mp3Parser) TrackNumber() int {
	// Check for a /, such as 2/8
	track, err := strconv.Atoi(strings.Split(m.tags[tagTrackNumber], "/")[0])
	if err != nil {
		return 0
	}

	return track
}

// newMP3Parser creates a parser for MP3 audio streams
func newMP3Parser(reader io.ReadSeeker) (*mp3Parser, error) {
	// Create MP3 parser
	parser := &mp3Parser{
		reader: reader,
	}

	// Parse ID3v2 header
	if err := parser.parseID3v2Header(); err != nil {
		return nil, err
	}

	// Parse ID3v2 frames
	if err := parser.parseID3v2Frames(); err != nil {
		return nil, err
	}

	// Parse MP3 header
	if err := parser.parseMP3Header(); err != nil {
		return nil, err
	}

	// Return parser
	return parser, nil
}

// parseID3v2Header parses the ID3v2 header at the start of an MP3 stream
func (m *mp3Parser) parseID3v2Header() error {
	// Create and use a bit reader to parse the following fields
	//   8 - ID3v2 major version
	//   8 - ID3v2 minor version
	//   1 - Unsynchronization (boolean) (ID3v2.3+)
	//   1 - Extended (boolean) (ID3v2.3+)
	//   1 - Experimental (boolean) (ID3v2.3+)
	//   1 - Footer (boolean) (ID3v2.4+)
	//   4 - (empty)
	//  32 - Size
	fields, err := bit.NewReader(m.reader).ReadFields(8, 8, 1, 1, 1, 1, 4, 32)
	if err != nil {
		return err
	}

	// Generate ID3v2 header
	m.id3Header = &mp3ID3v2Header{
		MajorVersion:      uint8(fields[0]),
		MinorVersion:      uint8(fields[1]),
		Unsynchronization: fields[2] == 1,
		Extended:          fields[3] == 1,
		Experimental:      fields[4] == 1,
		Footer:            fields[5] == 1,
		Size:              uint32(fields[7]),
	}

	// Ensure ID3v2 version is supported
	if m.id3Header.MajorVersion != 3 && m.id3Header.MajorVersion != 4 {
		return TagError{
			Err:     errUnsupportedVersion,
			Format:  m.Format(),
			Details: fmt.Sprintf("unsupported ID3 version: ID3v2.%d.%d", m.id3Header.MajorVersion, m.id3Header.MinorVersion),
		}
	}

	// Unsychronization is currently not supported
	if m.id3Header.Unsynchronization {
		return TagError{
			Err:     errUnsupportedVersion,
			Format:  m.Format(),
			Details: "ID3 unsynchronization is not supported",
		}
	}

	// Ensure Footer boolean is not defined prior to ID3v2.4
	if m.id3Header.MajorVersion < 4 && m.id3Header.Footer {
		return TagError{
			Err:     errInvalidStream,
			Format:  m.Format(),
			Details: "ID3 footer bit set prior to version ID3v2.4",
		}
	}

	// Check for extended header
	if m.id3Header.Extended {
		// Read size of extended header
		var headerSize uint32
		if err := binary.Read(m.reader, binary.BigEndian, &headerSize); err != nil {
			return err
		}

		// Seek past extended header (minus size of uint32 read), since the information
		// is irrelevant for tag parsing
		if _, err := m.reader.Seek(int64(headerSize)-4, 1); err != nil {
			return err
		}
	}

	return nil
}

// parseID3v2Frames parses ID3v2 frames from an MP3 stream
func (m *mp3Parser) parseID3v2Frames() error {
	// Store discovered tags in map
	tagMap := map[string]string{}

	// Create buffers for frame information
	frameBuf := make([]byte, 4)
	var frameLength uint32
	tagBuf := make([]byte, 2048)

	// Byte slices which should be trimmed and discarded from prefix or suffix
	trimPrefix := []byte{255, 254}
	trimSuffix := []byte{0}

	// Continuously loop and parse frames
	for {
		// Parse a frame title
		if _, err := m.reader.Read(frameBuf); err != nil {
			return err
		}

		// Stop parsing frames when frame title is nil, because we have reached padding
		if frameBuf[0] == byte(0) {
			break
		}

		// If byte 255 discovered, we have reached the start of the MP3 header
		if frameBuf[0] == byte(255) {
			// Pre-seed the current data as a bytes reader, to parse MP3 header
			m.reader = bytes.NewReader(frameBuf)
			break
		}

		// Parse the length of the frame data
		if err := binary.Read(m.reader, binary.BigEndian, &frameLength); err != nil {
			return err
		}

		// Skip over frame flags
		if _, err := m.reader.Seek(2, 1); err != nil {
			return err
		}

		// If frame is APIC, or "attached picture", seek past the picture
		if bytes.Equal(frameBuf, mp3APICFrame) {
			// Seek past picture data and continue loop
			if _, err := m.reader.Seek(int64(frameLength), 1); err != nil {
				return err
			}

			continue
		}

		// Parse the frame data tag
		n, err := m.reader.Read(tagBuf[:frameLength])
		if err != nil {
			return err
		}

		// Trim leading bytes such as UTF-8 BOM, garbage bytes, trim trailing nil
		// TODO: handle encodings that aren't UTF-8, stored in tagBuf[0]
		tag := string(bytes.TrimPrefix(bytes.TrimSuffix(tagBuf[1:n], trimSuffix), trimPrefix))

		// Map frame title to tag title, store frame data
		tagMap[mp3ID3v2FrameToTag[string(frameBuf)]] = tag
	}

	// Store tags in parser
	m.tags = tagMap
	return nil
}

// mp3ID3v2FrameToTag maps a MP3 ID3v2 frame title to its actual tag name
var mp3ID3v2FrameToTag = map[string]string{
	"COMM": tagComment,
	"TALB": tagAlbum,
	"TCON": tagGenre,
	"TDRC": tagDate,
	"TIT2": tagTitle,
	"TLEN": mp3TagLength,
	"TPE1": tagArtist,
	"TPE2": tagAlbumArtist,
	"TPOS": tagDiscNumber,
	"TRCK": tagTrackNumber,
	"TSSE": mp3TagEncoder,
	"TYER": tagDate,
}

// mp3ID3v2Header represents the MP3 ID3v2 header section
type mp3ID3v2Header struct {
	MajorVersion      uint8
	MinorVersion      uint8
	Unsynchronization bool
	Extended          bool
	Experimental      bool
	Footer            bool
	Size              uint32
}

// mp3ID3v2ExtendedHeader reperesents the optional MP3 ID3v2 extended header section
type mp3ID3v2ExtendedHeader struct {
	HeaderSize   uint32
	CRC32Present bool
	PaddingSize  uint32
}

// parseMP3Header parses the MP3 header after the ID3 headers in a MP3 stream
func (m *mp3Parser) parseMP3Header() error {
	// Read buffers continuously until we reach end of padding section, and find the
	// MP3 header, which starts with byte 255
	headerBuf := make([]byte, 128)
	for {
		if _, err := m.reader.Read(headerBuf); err != nil {
			return err
		}

		// If first byte is 255, value was pre-seeded by tag parser
		if headerBuf[0] == byte(255) {
			break
		}

		// Search for byte 255
		index := bytes.Index(headerBuf, []byte{255})
		if index != -1 {
			// We have encountered the header, re-slice forward to its index
			headerBuf = headerBuf[index:]
			break
		}
	}

	// Create and use a bit reader to parse the following fields
	//  11 - MP3 frame sync (all bits set)
	//   2 - MPEG audio version ID
	//   2 - Layer description
	//   1 - Protection bit (boolean)
	//   4 - Bitrate index
	fields, err := bit.NewReader(bytes.NewReader(headerBuf)).ReadFields(11, 2, 2, 1, 4, 2, 1, 1, 2)
	if err != nil {
		return err
	}

	// Create output MP3 header
	m.mp3Header = &mp3Header{
		MPEGVersionID: uint8(fields[1]),
		MPEGLayerID:   uint8(fields[2]),
		Protected:     fields[3] == 0,
		Bitrate:       uint16(fields[4]),
		SampleRate:    uint16(fields[5]),
		Padding:       fields[6] == 1,
		Private:       fields[7] == 1,
		ChannelMode:   uint8(fields[8]),
	}

	// Check to make sure we are parsing MPEG Version 1, Layer 3
	// Note: this check is correct, as these values actually map to:
	//   - Version ID 3 -> MPEG Version 1
	//   - Layer ID 1 -> MPEG Layer 3
	if m.mp3Header.MPEGVersionID != 3 {
		return TagError{
			Err:     errUnsupportedVersion,
			Format:  m.Format(),
			Details: fmt.Sprintf("unsupported MPEG version ID: %d", m.mp3Header.MPEGVersionID),
		}
	}

	if m.mp3Header.MPEGLayerID != 1 {
		return TagError{
			Err:     errUnsupportedVersion,
			Format:  m.Format(),
			Details: fmt.Sprintf("unsupported MPEG layer ID: %d", m.mp3Header.MPEGLayerID),
		}
	}

	return nil
}

// mp3Header represents a MP3 audio stream header, and contains information about the stream
type mp3Header struct {
	MPEGVersionID uint8
	MPEGLayerID   uint8
	Protected     bool
	Bitrate       uint16
	SampleRate    uint16
	Padding       bool
	Private       bool
	ChannelMode   uint8
}

// mp3BitrateMap maps MPEG Layer 3 Version 1 bitrate to its actual rate
var mp3BitrateMap = map[uint16]int{
	0:  0,
	1:  32,
	2:  40,
	3:  48,
	4:  56,
	5:  64,
	6:  80,
	7:  96,
	8:  112,
	9:  128,
	10: 160,
	11: 192,
	12: 224,
	13: 256,
	14: 320,
}

// mp3SampleRateMap maps MPEG Layer 3 Version 1 sample rate to its actual rate
var mp3SampleRateMap = map[uint16]int{
	0: 44100,
	1: 48000,
	2: 32000,
}

// mp3ChannelModeMap maps MPEG Layer 3 Version 1 channels to the number of channels
var mp3ChannelModeMap = map[uint8]int{
	0: 2,
	1: 2,
	3: 2,
	4: 1,
}
<|endoftext|>