<commit_before>// Copyright 2016 Martin Hebnes Pedersen (LA5NTA). All rights reserved.
// Use of this source code is governed by the MIT-license that can be
// found in the LICENSE file.

package cfg

import "github.com/la5nta/wl2k-go/transport/ardop"

const (
	PlaceholderMycall = "{mycall}"
)

type Config struct {
	// This station's callsign.
	MyCall string `json:"mycall"`

	// Secure login password used when a secure login challenge is received.
	//
	// The user is prompted if this is undefined.
	SecureLoginPassword string `json:"secure_login_password"`

	// Auxiliary callsigns to fetch email on behalf of.
	AuxAddrs []string `json:"auxiliary_addresses"`

	// Maidenhead grid square (e.g. JP20qe).
	Locator string `json:"locator"`

	// List of service codes for rmslist (defaults to PUBLIC)
	ServiceCodes []string `json:"service_codes"`

	// Default HTTP listen address (for web UI).
	//
	// Use ":8080" to listen on any device, port 8080.
	HTTPAddr string `json:"http_addr"`

	// Handshake comment lines sent to remote node on incoming connections.
	//
	// Example: ["QTH: Hagavik, Norway. Operator: Martin", "Rig: FT-897 with Signalink USB"]
	MOTD []string `json:"motd"`

	// Connect aliases
	//
	// Example: {"LA1B-10": "ax25:///LD5GU/LA1B-10", "LA1B": "winmor://LA3F?freq=5350"}
	// Any occurrence of the substring "{mycall}" will be replaced with user's callsign.
	ConnectAliases map[string]string `json:"connect_aliases"`

	// Methods to listen for incoming P2P connections by default.
	//
	// Example: ["ax25", "winmor", "telnet", "ardop"]
	Listen []string `json:"listen"`

	// Hamlib rigs available (with reference name) for ptt and frequency control.
	HamlibRigs map[string]HamlibConfig `json:"hamlib_rigs"`

	AX25      AX25Config      `json:"ax25"`       // See AX25Config.
	SerialTNC SerialTNCConfig `json:"serial-tnc"` // See SerialTNCConfig.
	Winmor    WinmorConfig    `json:"winmor"`     // See WinmorConfig.
	Ardop     ArdopConfig     `json:"ardop"`      // See ArdopConfig.
	Pactor    PactorConfig    `json:"pactor"`     // See PactorConfig.
	Telnet    TelnetConfig    `json:"telnet"`     // See TelnetConfig.

	// WARNING: If you enable GPSd http endpoint (enable_http) you might
	// expose your current position to anyone who has access to Pat!!!
	// See GPSdConfig.
	GPSd GPSdConfig `json:gpsd`

	// Legacy support for old config files
	GPSdAddrLegacy string `json:"gpsd_addr"`

	// Command schedule (cron-like syntax).
	//
	// Examples:
	//   # Connect to telnet once every hour
	//   "@hourly": "connect telnet"
	//
	//   # Change winmor listen frequency based on hour of day
	//   "00 10 * * *": "freq winmor:7350.000", # 40m from 10:00
	//   "00 18 * * *": "freq winmor:5347.000", # 60m from 18:00
	//   "00 22 * * *": "freq winmor:3602.000"  # 80m from 22:00
	Schedule map[string]string `json:"schedule"`

	// By default, Pat posts your callsign and running version to the Winlink CMS Web Services
	//
	// Set to true if you don't want your information sent.
	VersionReportingDisabled bool `json:"version_reporting_disabled"`
}

type HamlibConfig struct {
	// The network type ("serial" or "tcp"). Use 'tcp' for rigctld.
	//
	// (For serial support: build with "-tags libhamlib".)
	Network string `json:"network,omitempty"`

	// The rig address.
	//
	// For tcp (rigctld): "address:port" (e.g. localhost:4532).
	// For serial: "/path/to/tty?model=&baudrate=" (e.g. /dev/ttyS0?model=123&baudrate=4800).
	Address string `json:"address,omitempty"`

	// The rig's VFO to control ("A" or "B"). If empty, the current active VFO is used.
	VFO string `json:"VFO"`
}

type WinmorConfig struct {
	// Network address of the Winmor TNC (e.g. localhost:8500).
	Addr string `json:"addr"`

	// Bandwidth to use when getting an inbound connection (500/1600).
	InboundBandwidth int `json:"inbound_bandwidth"`

	// TX audio drive level
	//
	// Set to 0 to use WINMOR defaults
	DriveLevel int `json:"drive_level"`

	// (optional) Reference name to the Hamlib rig to control frequency and ptt.
	Rig string `json:"rig"`

	// Set to true if hamlib should control PTT (SignaLink=false, most rigexpert=true).
	PTTControl bool `json:"ptt_ctrl"`
}

type ArdopConfig struct {
	// Network address of the Ardop TNC (e.g. localhost:8515).
	Addr string `json:"addr"`

	// ARQ bandwidth (200/500/1000/2000 MAX/FORCED).
	ARQBandwidth ardop.Bandwidth `json:"arq_bandwidth"`

	// (optional) Reference name to the Hamlib rig to control frequency and ptt.
	Rig string `json:"rig"`

	// Set to true if hamlib should control PTT (SignaLink=false, most rigexpert=true).
	PTTControl bool `json:"ptt_ctrl"`

	// (optional) Send ID frame at a regular interval when the listener is active (unit is seconds)
	BeaconInterval int `json:"beacon_interval"`

	// Send FSK CW ID after an ID frame.
	CWID bool `json:"cwid_enabled"`
}

type PactorConfig struct {
	// Path/port to TNC device (e.g. /dev/ttyUSB0 or COM1).
	Path string `json:"path"`

	// Baudrate for the serial port (e.g. 57600).
	Baudrate int `json:"baudrate"`

	// (optional) Reference name to the Hamlib rig for frequency control.
	Rig string `json:"rig"`

	// (optional) Path to custom TNC initialization script.
	InitScript string `json:"custom_init_script"`
}

type TelnetConfig struct {
	// Network address (and port) to listen for telnet-p2p connections (e.g. :8774).
	ListenAddr string `json:"listen_addr"`

	// Telnet-p2p password.
	Password string `json:"password"`
}

type SerialTNCConfig struct {
	// Serial port (e.g. /dev/ttyUSB0 or COM1).
	Path string `json:"path"`

	// Baudrate for the serial port (e.g. 57600).
	Baudrate int `json:"baudrate"`

	// Type of TNC (currently only 'kenwood').
	Type string `json:"type"`
}

type AX25Config struct {
	// axport to use (as defined in /etc/ax25/axports).
	Port string `json:"port"`

	// Optional beacon when listening for incoming packet-p2p connections.
	Beacon BeaconConfig `json:"beacon"`

	// (optional) Reference name to the Hamlib rig for frequency control.
	Rig string `json:"rig"`
}

type BeaconConfig struct {
	// Beacon interval in seconds (e.g. 3600 for once every 1 hour)
	Every int `json:"every"` // (seconds)

	// Beacon data/message
	Message string `json:"message"`

	// Beacon destination (e.g. IDENT)
	Destination string `json:"destination"`
}

type GPSdConfig struct {
	// enable GPSd support in web interface
	EnableHttp bool    `json:"enable_http"`

	// Use server time instead of timestamp provided by GPSd (e.g for older GPS
	// device with week roll-over issue)
	UseServerTime bool `json:"use_server_time"`

	// Address and port of GPSd server (e.g. localhost:2947)
	Addr string        `json:"addr"`
}

var DefaultConfig Config = Config{
	MOTD:         []string{"Open source Winlink client - getpat.io"},
	AuxAddrs:     []string{},
	ServiceCodes: []string{"PUBLIC"},
	ConnectAliases: map[string]string{
		"telnet": "telnet://{mycall}:CMSTelnet@cms.winlink.org:8772/wl2k",
	},
	Listen:   []string{},
	HTTPAddr: "localhost:8080",
	AX25: AX25Config{
		Port: "wl2k",
		Beacon: BeaconConfig{
			Every:       3600,
			Message:     "Winlink P2P",
			Destination: "IDENT",
		},
	},
	SerialTNC: SerialTNCConfig{
		Path:     "/dev/ttyUSB0",
		Baudrate: 9600,
		Type:     "Kenwood",
	},
	Winmor: WinmorConfig{
		Addr:             "localhost:8500",
		InboundBandwidth: 1600,
	},
	Ardop: ArdopConfig{
		Addr:         "localhost:8515",
		ARQBandwidth: ardop.Bandwidth500Max,
		CWID:         true,
	},
	Pactor: PactorConfig{
		Path:     "/dev/ttyUSB0",
		Baudrate: 57600,
	},
	Telnet: TelnetConfig{
		ListenAddr: ":8774",
		Password:   "",
	},
	GPSd: GPSdConfig{
		EnableHttp:    false,
		UseServerTime: false,
		Addr:          "localhost:2947", // Default listen address for GPSd
	},
	GPSdAddrLegacy: "",
	Schedule:          map[string]string{},
	HamlibRigs:        map[string]HamlibConfig{},
}
<commit_msg>Make sure, deprecated gpsd_addr is not written in new config files. Add deprecated hint. See https://github.com/la5nta/pat/pull/163#discussion_r329368898<commit_after>// Copyright 2016 Martin Hebnes Pedersen (LA5NTA). All rights reserved.
// Use of this source code is governed by the MIT-license that can be
// found in the LICENSE file.

package cfg

import "github.com/la5nta/wl2k-go/transport/ardop"

const (
	PlaceholderMycall = "{mycall}"
)

type Config struct {
	// This station's callsign.
	MyCall string `json:"mycall"`

	// Secure login password used when a secure login challenge is received.
	//
	// The user is prompted if this is undefined.
	SecureLoginPassword string `json:"secure_login_password"`

	// Auxiliary callsigns to fetch email on behalf of.
	AuxAddrs []string `json:"auxiliary_addresses"`

	// Maidenhead grid square (e.g. JP20qe).
	Locator string `json:"locator"`

	// List of service codes for rmslist (defaults to PUBLIC)
	ServiceCodes []string `json:"service_codes"`

	// Default HTTP listen address (for web UI).
	//
	// Use ":8080" to listen on any device, port 8080.
	HTTPAddr string `json:"http_addr"`

	// Handshake comment lines sent to remote node on incoming connections.
	//
	// Example: ["QTH: Hagavik, Norway. Operator: Martin", "Rig: FT-897 with Signalink USB"]
	MOTD []string `json:"motd"`

	// Connect aliases
	//
	// Example: {"LA1B-10": "ax25:///LD5GU/LA1B-10", "LA1B": "winmor://LA3F?freq=5350"}
	// Any occurrence of the substring "{mycall}" will be replaced with user's callsign.
	ConnectAliases map[string]string `json:"connect_aliases"`

	// Methods to listen for incoming P2P connections by default.
	//
	// Example: ["ax25", "winmor", "telnet", "ardop"]
	Listen []string `json:"listen"`

	// Hamlib rigs available (with reference name) for ptt and frequency control.
	HamlibRigs map[string]HamlibConfig `json:"hamlib_rigs"`

	AX25      AX25Config      `json:"ax25"`       // See AX25Config.
	SerialTNC SerialTNCConfig `json:"serial-tnc"` // See SerialTNCConfig.
	Winmor    WinmorConfig    `json:"winmor"`     // See WinmorConfig.
	Ardop     ArdopConfig     `json:"ardop"`      // See ArdopConfig.
	Pactor    PactorConfig    `json:"pactor"`     // See PactorConfig.
	Telnet    TelnetConfig    `json:"telnet"`     // See TelnetConfig.

	// See GPSdConfig.
	GPSd GPSdConfig `json:gpsd`

	// Legacy support for old config files only. This field is deprecated!
	// Please use "Addr" field in GPSd config struct (GPSd.Addr)
	GPSdAddrLegacy string `json:"gpsd_addr,omitempty"`

	// Command schedule (cron-like syntax).
	//
	// Examples:
	//   # Connect to telnet once every hour
	//   "@hourly": "connect telnet"
	//
	//   # Change winmor listen frequency based on hour of day
	//   "00 10 * * *": "freq winmor:7350.000", # 40m from 10:00
	//   "00 18 * * *": "freq winmor:5347.000", # 60m from 18:00
	//   "00 22 * * *": "freq winmor:3602.000"  # 80m from 22:00
	Schedule map[string]string `json:"schedule"`

	// By default, Pat posts your callsign and running version to the Winlink CMS Web Services
	//
	// Set to true if you don't want your information sent.
	VersionReportingDisabled bool `json:"version_reporting_disabled"`
}

type HamlibConfig struct {
	// The network type ("serial" or "tcp"). Use 'tcp' for rigctld.
	//
	// (For serial support: build with "-tags libhamlib".)
	Network string `json:"network,omitempty"`

	// The rig address.
	//
	// For tcp (rigctld): "address:port" (e.g. localhost:4532).
	// For serial: "/path/to/tty?model=&baudrate=" (e.g. /dev/ttyS0?model=123&baudrate=4800).
	Address string `json:"address,omitempty"`

	// The rig's VFO to control ("A" or "B"). If empty, the current active VFO is used.
	VFO string `json:"VFO"`
}

type WinmorConfig struct {
	// Network address of the Winmor TNC (e.g. localhost:8500).
	Addr string `json:"addr"`

	// Bandwidth to use when getting an inbound connection (500/1600).
	InboundBandwidth int `json:"inbound_bandwidth"`

	// TX audio drive level
	//
	// Set to 0 to use WINMOR defaults
	DriveLevel int `json:"drive_level"`

	// (optional) Reference name to the Hamlib rig to control frequency and ptt.
	Rig string `json:"rig"`

	// Set to true if hamlib should control PTT (SignaLink=false, most rigexpert=true).
	PTTControl bool `json:"ptt_ctrl"`
}

type ArdopConfig struct {
	// Network address of the Ardop TNC (e.g. localhost:8515).
	Addr string `json:"addr"`

	// ARQ bandwidth (200/500/1000/2000 MAX/FORCED).
	ARQBandwidth ardop.Bandwidth `json:"arq_bandwidth"`

	// (optional) Reference name to the Hamlib rig to control frequency and ptt.
	Rig string `json:"rig"`

	// Set to true if hamlib should control PTT (SignaLink=false, most rigexpert=true).
	PTTControl bool `json:"ptt_ctrl"`

	// (optional) Send ID frame at a regular interval when the listener is active (unit is seconds)
	BeaconInterval int `json:"beacon_interval"`

	// Send FSK CW ID after an ID frame.
	CWID bool `json:"cwid_enabled"`
}

type PactorConfig struct {
	// Path/port to TNC device (e.g. /dev/ttyUSB0 or COM1).
	Path string `json:"path"`

	// Baudrate for the serial port (e.g. 57600).
	Baudrate int `json:"baudrate"`

	// (optional) Reference name to the Hamlib rig for frequency control.
	Rig string `json:"rig"`

	// (optional) Path to custom TNC initialization script.
	InitScript string `json:"custom_init_script"`
}

type TelnetConfig struct {
	// Network address (and port) to listen for telnet-p2p connections (e.g. :8774).
	ListenAddr string `json:"listen_addr"`

	// Telnet-p2p password.
	Password string `json:"password"`
}

type SerialTNCConfig struct {
	// Serial port (e.g. /dev/ttyUSB0 or COM1).
	Path string `json:"path"`

	// Baudrate for the serial port (e.g. 57600).
	Baudrate int `json:"baudrate"`

	// Type of TNC (currently only 'kenwood').
	Type string `json:"type"`
}

type AX25Config struct {
	// axport to use (as defined in /etc/ax25/axports).
	Port string `json:"port"`

	// Optional beacon when listening for incoming packet-p2p connections.
	Beacon BeaconConfig `json:"beacon"`

	// (optional) Reference name to the Hamlib rig for frequency control.
	Rig string `json:"rig"`
}

type BeaconConfig struct {
	// Beacon interval in seconds (e.g. 3600 for once every 1 hour)
	Every int `json:"every"` // (seconds)

	// Beacon data/message
	Message string `json:"message"`

	// Beacon destination (e.g. IDENT)
	Destination string `json:"destination"`
}

type GPSdConfig struct {
	// enable GPSd support in web interface
	EnableHttp bool    `json:"enable_http"`

	// Use server time instead of timestamp provided by GPSd (e.g for older GPS
	// device with week roll-over issue)
	UseServerTime bool `json:"use_server_time"`

	// Address and port of GPSd server (e.g. localhost:2947)
	Addr string        `json:"addr"`
}

var DefaultConfig Config = Config{
	MOTD:         []string{"Open source Winlink client - getpat.io"},
	AuxAddrs:     []string{},
	ServiceCodes: []string{"PUBLIC"},
	ConnectAliases: map[string]string{
		"telnet": "telnet://{mycall}:CMSTelnet@cms.winlink.org:8772/wl2k",
	},
	Listen:   []string{},
	HTTPAddr: "localhost:8080",
	AX25: AX25Config{
		Port: "wl2k",
		Beacon: BeaconConfig{
			Every:       3600,
			Message:     "Winlink P2P",
			Destination: "IDENT",
		},
	},
	SerialTNC: SerialTNCConfig{
		Path:     "/dev/ttyUSB0",
		Baudrate: 9600,
		Type:     "Kenwood",
	},
	Winmor: WinmorConfig{
		Addr:             "localhost:8500",
		InboundBandwidth: 1600,
	},
	Ardop: ArdopConfig{
		Addr:         "localhost:8515",
		ARQBandwidth: ardop.Bandwidth500Max,
		CWID:         true,
	},
	Pactor: PactorConfig{
		Path:     "/dev/ttyUSB0",
		Baudrate: 57600,
	},
	Telnet: TelnetConfig{
		ListenAddr: ":8774",
		Password:   "",
	},
	GPSd: GPSdConfig{
		EnableHttp:    false,
		UseServerTime: false,
		Addr:          "localhost:2947", // Default listen address for GPSd
	},
	GPSdAddrLegacy: "",
	Schedule:          map[string]string{},
	HamlibRigs:        map[string]HamlibConfig{},
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
	"github.com/naoina/go-stringutil"
	"go/ast"
	"go/parser"
	"go/token"
	"io"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"strings"
)

type column struct {
	Type string
	Name string
	Opt  map[string]string
}

type table struct {
	Name    string
	Columns []column
	Pk      string
	Index   []string
}

// Read schema info. from struct
func parseStruct(gd *ast.GenDecl, st *ast.StructType) table {
	tbl := table{"", make([]column, 0), "", make([]string, 0)}
	for _, s := range gd.Specs {
		if typ, ok := s.(*ast.TypeSpec); ok {
			tbl.Name = typ.Name.Name
			break
		}
	}
EachField:
	for _, fld := range st.Fields.List {
		col := column{"", "", make(map[string]string)}
		tp := fld.Type
		var tn string
		if id, ok := tp.(*ast.Ident); ok {
			tn = id.Name
		}

		if id, ok := tp.(*ast.SelectorExpr); ok {
			tn = id.Sel.Name
		}

		if ar, ok := tp.(*ast.ArrayType); ok {
			tn = ar.Elt.(*ast.Ident).Name + "[]"
		}

		// Convert Go's type to SQL type
		switch tn {
		case "bool":
			col.Type = "boolean"
			col.Opt["null"] = "false"
		case "int", "int16", "int32", "int64", "uint", "uint16", "uint32", "uint64":
			col.Type = "integer"
			col.Opt["null"] = "false"
		case "string":
			col.Type = "string"
			col.Opt["null"] = "false"
		case "byte[]":
			col.Type = "binary"
		case "float", "float64":
			col.Type = "float"
			col.Opt["null"] = "false"
		case "Time":
			col.Type = "timestamp" // TODO Sometime, it's suite to use datetime.
			col.Opt["null"] = "false"
		case "NullBool":
			col.Type = "boolean"
		case "NullInt64":
			col.Type = "integer"
		case "NullFloat64":
			col.Type = "float"
		case "NullString":
			col.Type = "string"
		default:
			log.Print(reflect.TypeOf(tp).String(), tp)
		}

		col.Name = fld.Names[0].String() // is it preferd to use stringutil.join() ?

		// タグの解釈
		if fld.Tag != nil {
			tgs := strings.Split(strings.Trim(fld.Tag.Value, "`"), " ")
			for _, t := range tgs {
				if len(t) == 0 {
					continue
				}
				kv := strings.Split(t, ":")
				kv[1] = strings.Trim(kv[1], "\"")

				switch kv[0] {
				case "db":
					switch kv[1] {
					case "-": // no target.
						continue EachField
					case "pk": // primary key
						tbl.Pk = col.Name
					case "unique": // unique constraint
						tbl.Index = append(tbl.Index, col.Name)
					}
				case "size": // size constraint
					col.Opt["limit"] = kv[1]
				case "default": // default value
					col.Opt["default"] = kv[1]
				case "column": // column name
					col.Name = kv[1]
				case "refer": // reference to another table
					col.Type = "references"
					delete(col.Opt, "null")
					if len(kv[1]) > 0 {
						col.Name = kv[1]
					} else {
						col.Name = strings.TrimSuffix(col.Name, "Id")
					}
				}
			}
		}

		tbl.Columns = append(tbl.Columns, col)
	}

	return tbl
}

// Search target struct from file
func parseFile(fset *token.FileSet, file *ast.File, tables []table) []table {
	if tables == nil {
		tables = make([]table, 0)
	}
	cm := ast.NewCommentMap(fset, file, file.Comments)

	for node, cgs := range cm {
		//      To process module comment, here is a place for.
		//		if _, ok := node.(*ast.File); ok {
		//			for _, cg := range cgs {
		//				for _, c := range cg.List {
		//					log.Println(c.Text)
		//				}
		//			}
		//		}
		if gd, ok := node.(*ast.GenDecl); ok {
			for _, spec := range gd.Specs {
				if ts, ok := spec.(*ast.TypeSpec); !ok {
					continue
				} else if st, ok2 := ts.Type.(*ast.StructType); !ok2 {
					continue
				} else {
				Outer:
					for _, cg := range cgs {
						for _, c := range cg.List {
							if strings.Contains(c.Text, `db:"entity"`) {
								tables = append(tables, parseStruct(gd, st))
								break Outer
							}
						}
					}
				}
			}
		}
	}

	return tables
}

// Output in Rails's migration file format.
func putMigrate(tables []table, w io.Writer) {
	for _, tbl := range tables {
		ixFlg := true
		if len(tbl.Index) == 1 {
			ixFlg = false
		}

		fmt.Fprintf(w, "create_table '%s'", stringutil.ToSnakeCase(tbl.Name))
		if len(tbl.Pk) == 0 {
			fmt.Fprint(w, ", id:false")
		} else if tbl.Pk != "Id" {
			fmt.Fprint(w, ", primary_key:'", stringutil.ToSnakeCase(tbl.Pk),"'")
		}
		fmt.Fprint(w, " do |t|\n")
		for _, col := range tbl.Columns {
			if col.Name == tbl.Pk {
				continue
			}
			cs := fmt.Sprintf("t.%s :%s", col.Type, stringutil.ToSnakeCase(col.Name))
			if !ixFlg && col.Name == tbl.Index[0] {
				cs = cs + ", unique:true"
			}
			if len(col.Opt["null"]) > 0 {
				cs = cs + ", null:" + col.Opt["null"]
			}
			fmt.Fprintln(w, " ", cs)
		}
		fmt.Fprintln(w, "end")
		if len(tbl.Index) > 1 {
			is := fmt.Sprintf("add_index :%s, [", stringutil.ToSnakeCase(tbl.Name))
			it := make([]string, 0)
			for _, i := range tbl.Index {
				it = append(it, ":"+stringutil.ToSnakeCase(i))
			}
			is += strings.Join(it, ", ")
			fmt.Fprint(w, is)
			fmt.Fprintln(w, "], unique:true")
		}
		fmt.Fprintln(w)
	}
}

func init() {
}

func main() {
	flag.Parse()

	var tables []table

	fn, _ := filepath.Abs(flag.Arg(0))
	fi, _ := os.Stat(fn)
	fset := token.NewFileSet()
	if fi.IsDir() {
		pkgs, _ := parser.ParseDir(fset, fn, nil, parser.ParseComments)
		for _, pkg := range pkgs {
			for _, file := range pkg.Files {
				tables = parseFile(fset, file, tables)
			}
		}
	} else {
		file, _ := parser.ParseFile(fset, fn, nil, parser.ParseComments)
		tables = parseFile(fset, file, tables)
	}

	putMigrate(tables, os.Stdout)
}
<commit_msg>Add command line option to specify output file name.<commit_after>package main

import (
	"flag"
	"fmt"
	"github.com/naoina/go-stringutil"
	"go/ast"
	"go/parser"
	"go/token"
	"io"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"strings"
)

type column struct {
	Type string
	Name string
	Opt  map[string]string
}

type table struct {
	Name    string
	Columns []column
	Pk      string
	Index   []string
}

// Read schema info. from struct
func parseStruct(gd *ast.GenDecl, st *ast.StructType) table {
	tbl := table{"", make([]column, 0), "", make([]string, 0)}
	for _, s := range gd.Specs {
		if typ, ok := s.(*ast.TypeSpec); ok {
			tbl.Name = typ.Name.Name
			break
		}
	}
EachField:
	for _, fld := range st.Fields.List {
		col := column{"", "", make(map[string]string)}
		tp := fld.Type
		var tn string
		if id, ok := tp.(*ast.Ident); ok {
			tn = id.Name
		}

		if id, ok := tp.(*ast.SelectorExpr); ok {
			tn = id.Sel.Name
		}

		if ar, ok := tp.(*ast.ArrayType); ok {
			tn = ar.Elt.(*ast.Ident).Name + "[]"
		}

		// Convert Go's type to SQL type
		switch tn {
		case "bool":
			col.Type = "boolean"
			col.Opt["null"] = "false"
		case "int", "int16", "int32", "int64", "uint", "uint16", "uint32", "uint64":
			col.Type = "integer"
			col.Opt["null"] = "false"
		case "string":
			col.Type = "string"
			col.Opt["null"] = "false"
		case "byte[]":
			col.Type = "binary"
		case "float", "float64":
			col.Type = "float"
			col.Opt["null"] = "false"
		case "Time":
			col.Type = "timestamp" // TODO Sometime, it's suite to use datetime.
			col.Opt["null"] = "false"
		case "NullBool":
			col.Type = "boolean"
		case "NullInt64":
			col.Type = "integer"
		case "NullFloat64":
			col.Type = "float"
		case "NullString":
			col.Type = "string"
		default:
			log.Print(reflect.TypeOf(tp).String(), tp)
		}

		col.Name = fld.Names[0].String() // is it preferd to use stringutil.join() ?

		// タグの解釈
		if fld.Tag != nil {
			tgs := strings.Split(strings.Trim(fld.Tag.Value, "`"), " ")
			for _, t := range tgs {
				if len(t) == 0 {
					continue
				}
				kv := strings.Split(t, ":")
				kv[1] = strings.Trim(kv[1], "\"")

				switch kv[0] {
				case "db":
					switch kv[1] {
					case "-": // no target.
						continue EachField
					case "pk": // primary key
						tbl.Pk = col.Name
					case "unique": // unique constraint
						tbl.Index = append(tbl.Index, col.Name)
					}
				case "size": // size constraint
					col.Opt["limit"] = kv[1]
				case "default": // default value
					col.Opt["default"] = kv[1]
				case "column": // column name
					col.Name = kv[1]
				case "refer": // reference to another table
					col.Type = "references"
					delete(col.Opt, "null")
					if len(kv[1]) > 0 {
						col.Name = kv[1]
					} else {
						col.Name = strings.TrimSuffix(col.Name, "Id")
					}
				}
			}
		}

		tbl.Columns = append(tbl.Columns, col)
	}

	return tbl
}

// Search target struct from file
func parseFile(fset *token.FileSet, file *ast.File, tables []table) []table {
	if tables == nil {
		tables = make([]table, 0)
	}
	cm := ast.NewCommentMap(fset, file, file.Comments)

	for node, cgs := range cm {
		//      To process module comment, here is a place for.
		//		if _, ok := node.(*ast.File); ok {
		//			for _, cg := range cgs {
		//				for _, c := range cg.List {
		//					log.Println(c.Text)
		//				}
		//			}
		//		}
		if gd, ok := node.(*ast.GenDecl); ok {
			for _, spec := range gd.Specs {
				if ts, ok := spec.(*ast.TypeSpec); !ok {
					continue
				} else if st, ok2 := ts.Type.(*ast.StructType); !ok2 {
					continue
				} else {
				Outer:
					for _, cg := range cgs {
						for _, c := range cg.List {
							if strings.Contains(c.Text, `db:"entity"`) {
								tables = append(tables, parseStruct(gd, st))
								break Outer
							}
						}
					}
				}
			}
		}
	}

	return tables
}

// Output in Rails's migration file format.
func putMigrate(tables []table, w io.Writer) {
	for _, tbl := range tables {
		ixFlg := true
		if len(tbl.Index) == 1 {
			ixFlg = false
		}

		fmt.Fprintf(w, "create_table '%s'", stringutil.ToSnakeCase(tbl.Name))
		if len(tbl.Pk) == 0 {
			fmt.Fprint(w, ", id:false")
		} else if tbl.Pk != "Id" {
			fmt.Fprint(w, ", primary_key:'", stringutil.ToSnakeCase(tbl.Pk),"'")
		}
		fmt.Fprint(w, " do |t|\n")
		for _, col := range tbl.Columns {
			if col.Name == tbl.Pk {
				continue
			}
			cs := fmt.Sprintf("t.%s :%s", col.Type, stringutil.ToSnakeCase(col.Name))
			if !ixFlg && col.Name == tbl.Index[0] {
				cs = cs + ", unique:true"
			}
			if len(col.Opt["null"]) > 0 {
				cs = cs + ", null:" + col.Opt["null"]
			}
			fmt.Fprintln(w, " ", cs)
		}
		fmt.Fprintln(w, "end")
		if len(tbl.Index) > 1 {
			is := fmt.Sprintf("add_index :%s, [", stringutil.ToSnakeCase(tbl.Name))
			it := make([]string, 0)
			for _, i := range tbl.Index {
				it = append(it, ":"+stringutil.ToSnakeCase(i))
			}
			is += strings.Join(it, ", ")
			fmt.Fprint(w, is)
			fmt.Fprintln(w, "], unique:true")
		}
		fmt.Fprintln(w)
	}
}

var (
	out settableWriter = settableWriter{os.Stdout}
)

type settableWriter struct {
	out io.Writer
}

func (*settableWriter) String() string {
	return ""
}

func (s *settableWriter) Set(fn string) error {
	f, err := os.Create(fn)
	if f == nil || err != nil {
		log.Print(err)
		return err
	}
	s.out = f
	return nil
}

func (s settableWriter) Write(p []byte) (int, error) {
	if s.out == nil {
		log.Print("Not initialized.")
		return 0, nil
	}
	return s.out.Write(p)
}

func init() {
	flag.Var(&out, "o", "Output file path.")
}

func main() {
	flag.Parse()

	var tables []table

	fn, _ := filepath.Abs(flag.Arg(0))
	fi, err := os.Stat(fn)
	if err != nil {
		log.Panic(err)
	}
	fset := token.NewFileSet()
	if fi.IsDir() {
		pkgs, _ := parser.ParseDir(fset, fn, nil, parser.ParseComments)
		for _, pkg := range pkgs {
			for _, file := range pkg.Files {
				tables = parseFile(fset, file, tables)
			}
		}
	} else {
		file, _ := parser.ParseFile(fset, fn, nil, parser.ParseComments)
		tables = parseFile(fset, file, tables)
	}

	putMigrate(tables, out)
}
<|endoftext|>


<commit_before>// Copyright ©2021 The Gonum 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 testlapack

import (
	"fmt"
	"math"
	"testing"

	"golang.org/x/exp/rand"

	"gonum.org/v1/gonum/blas"
	"gonum.org/v1/gonum/blas/blas64"
	"gonum.org/v1/gonum/floats"
	"gonum.org/v1/gonum/lapack"
)

type Dgesc2er interface {
	Dgesc2(n int, a []float64, lda int, rhs []float64, ipiv, jpiv []int) (scale float64)

	Dgetc2er
}

func Dgesc2Test(t *testing.T, impl Dgesc2er) {
	rnd := rand.New(rand.NewSource(1))
	for _, n := range []int{0, 1, 2, 3, 4, 5, 10, 20, 50} {
		for _, lda := range []int{n, n + 3} {
			testDgesc2(t, impl, rnd, n, lda, false)
			testDgesc2(t, impl, rnd, n, lda, true)
		}
	}
}

func testDgesc2(t *testing.T, impl Dgesc2er, rnd *rand.Rand, n, lda int, big bool) {
	const tol = 1e-14

	name := fmt.Sprintf("n=%v,lda=%v,big=%v", n, lda, big)

	// Generate random general matrix.
	a := randomGeneral(n, n, max(1, lda), rnd)

	// Generate a random right hand side vector.
	b := randomGeneral(n, 1, 1, rnd)
	if big {
		for i := 0; i < n; i++ {
			b.Data[i] *= bignum
		}
	}

	// Compute the LU factorization of A with full pivoting.
	lu := cloneGeneral(a)
	ipiv := make([]int, n)
	jpiv := make([]int, n)
	impl.Dgetc2(n, lu.Data, lu.Stride, ipiv, jpiv)

	// Make copies of const input to Dgesc2.
	luCopy := cloneGeneral(lu)
	ipivCopy := make([]int, len(ipiv))
	copy(ipivCopy, ipiv)
	jpivCopy := make([]int, len(jpiv))
	copy(jpivCopy, jpiv)

	// Call Dgesc2 to solve A*x = scale*b.
	x := cloneGeneral(b)
	scale := impl.Dgesc2(n, lu.Data, lu.Stride, x.Data, ipiv, jpiv)

	if n == 0 {
		return
	}

	// Check that const input to Dgesc2 hasn't been modified.
	if !floats.Same(lu.Data, luCopy.Data) {
		t.Errorf("%v: unexpected modification in lu", name)
	}
	if !intsEqual(ipiv, ipivCopy) {
		t.Errorf("%v: unexpected modification in ipiv", name)
	}
	if !intsEqual(jpiv, jpivCopy) {
		t.Errorf("%v: unexpected modification in ipiv", name)
	}

	if scale <= 0 || 1 < scale {
		t.Errorf("%v: scale %v out of bounds (0,1]", name, scale)
	}
	if !big && scale != 1 {
		t.Errorf("%v: unexpected scaling, scale=%v", name, scale)
	}

	// Compute the difference rhs := A*x - scale*b.
	diff := b
	for i := 0; i < n; i++ {
		diff.Data[i] *= scale
	}
	blas64.Gemm(blas.NoTrans, blas.NoTrans, 1, a, x, -1, diff)

	// Compute the residual |A*x - scale*b| / |x|.
	xnorm := dlange(lapack.MaxColumnSum, n, 1, x.Data, 1)
	resid := dlange(lapack.MaxColumnSum, n, 1, diff.Data, 1) / xnorm
	if resid > tol || math.IsNaN(resid) {
		t.Errorf("%v: unexpected result; resid=%v, want<=%v", name, resid, tol)
	}
}
<commit_msg>lapack/testlapack: fix typo in test for Dgesc2<commit_after>// Copyright ©2021 The Gonum 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 testlapack

import (
	"fmt"
	"math"
	"testing"

	"golang.org/x/exp/rand"

	"gonum.org/v1/gonum/blas"
	"gonum.org/v1/gonum/blas/blas64"
	"gonum.org/v1/gonum/floats"
	"gonum.org/v1/gonum/lapack"
)

type Dgesc2er interface {
	Dgesc2(n int, a []float64, lda int, rhs []float64, ipiv, jpiv []int) (scale float64)

	Dgetc2er
}

func Dgesc2Test(t *testing.T, impl Dgesc2er) {
	rnd := rand.New(rand.NewSource(1))
	for _, n := range []int{0, 1, 2, 3, 4, 5, 10, 20, 50} {
		for _, lda := range []int{n, n + 3} {
			testDgesc2(t, impl, rnd, n, lda, false)
			testDgesc2(t, impl, rnd, n, lda, true)
		}
	}
}

func testDgesc2(t *testing.T, impl Dgesc2er, rnd *rand.Rand, n, lda int, big bool) {
	const tol = 1e-14

	name := fmt.Sprintf("n=%v,lda=%v,big=%v", n, lda, big)

	// Generate random general matrix.
	a := randomGeneral(n, n, max(1, lda), rnd)

	// Generate a random right hand side vector.
	b := randomGeneral(n, 1, 1, rnd)
	if big {
		for i := 0; i < n; i++ {
			b.Data[i] *= bignum
		}
	}

	// Compute the LU factorization of A with full pivoting.
	lu := cloneGeneral(a)
	ipiv := make([]int, n)
	jpiv := make([]int, n)
	impl.Dgetc2(n, lu.Data, lu.Stride, ipiv, jpiv)

	// Make copies of const input to Dgesc2.
	luCopy := cloneGeneral(lu)
	ipivCopy := make([]int, len(ipiv))
	copy(ipivCopy, ipiv)
	jpivCopy := make([]int, len(jpiv))
	copy(jpivCopy, jpiv)

	// Call Dgesc2 to solve A*x = scale*b.
	x := cloneGeneral(b)
	scale := impl.Dgesc2(n, lu.Data, lu.Stride, x.Data, ipiv, jpiv)

	if n == 0 {
		return
	}

	// Check that const input to Dgesc2 hasn't been modified.
	if !floats.Same(lu.Data, luCopy.Data) {
		t.Errorf("%v: unexpected modification in lu", name)
	}
	if !intsEqual(ipiv, ipivCopy) {
		t.Errorf("%v: unexpected modification in ipiv", name)
	}
	if !intsEqual(jpiv, jpivCopy) {
		t.Errorf("%v: unexpected modification in jpiv", name)
	}

	if scale <= 0 || 1 < scale {
		t.Errorf("%v: scale %v out of bounds (0,1]", name, scale)
	}
	if !big && scale != 1 {
		t.Errorf("%v: unexpected scaling, scale=%v", name, scale)
	}

	// Compute the difference rhs := A*x - scale*b.
	diff := b
	for i := 0; i < n; i++ {
		diff.Data[i] *= scale
	}
	blas64.Gemm(blas.NoTrans, blas.NoTrans, 1, a, x, -1, diff)

	// Compute the residual |A*x - scale*b| / |x|.
	xnorm := dlange(lapack.MaxColumnSum, n, 1, x.Data, 1)
	resid := dlange(lapack.MaxColumnSum, n, 1, diff.Data, 1) / xnorm
	if resid > tol || math.IsNaN(resid) {
		t.Errorf("%v: unexpected result; resid=%v, want<=%v", name, resid, tol)
	}
}
<|endoftext|>


<commit_before>package sudoku

import (
	"testing"
)

//TODO: test a few more puzzles to make sure I'm exercising it correctly.

func TestXYWing(t *testing.T) {
	options := solveTechniqueTestHelperOptions{
		targetCells: []cellRef{{3, 8}, {5, 3}},
		//TODO: figure out how to test that {3,6} (the pivot cell) comes first
		pointerCells: []cellRef{{3, 6}, {3, 3}, {5, 7}},
		targetNums:   IntSlice([]int{7}),
		//TODO: test for pointernums
		//TODO: test description
	}
	humanSolveTechniqueTestHelper(t, "xywing_example.sdk", "XYWing", options)
	techniqueVariantsTestHelper(t, "XYWing")

}
<commit_msg>TESTS FAIL, but itermittently (they did at last commit, too). Added pointerNums to XYWing test.<commit_after>package sudoku

import (
	"testing"
)

//TODO: test a few more puzzles to make sure I'm exercising it correctly.

func TestXYWing(t *testing.T) {
	options := solveTechniqueTestHelperOptions{
		targetCells: []cellRef{{3, 8}, {5, 3}},
		//TODO: figure out how to test that {3,6} (the pivot cell) comes first
		pointerCells: []cellRef{{3, 6}, {3, 3}, {5, 7}},
		targetNums:   IntSlice([]int{7}),
		pointerNums:  IntSlice{2, 5, 7},
		//TODO: test description
	}
	humanSolveTechniqueTestHelper(t, "xywing_example.sdk", "XYWing", options)
	techniqueVariantsTestHelper(t, "XYWing")

}
<|endoftext|>


<commit_before>package ogo

import (
	"fmt"
	"mime"
	"net/http"
	"regexp"
	"runtime/debug"
	"strings"
	"time"

	"github.com/Odinman/ogo/utils"
	"github.com/dustin/randbo"
	"github.com/zenazn/goji/web"
)

// Key to use when setting the request ID.
const (
	//固定参数名称
	_PARAM_FIELDS  = "fields"
	_PARAM_PAGE    = "page"
	_PARAM_PERPAGE = "per_page"
	_PARAM_DATE    = "date"
	_PARAM_START   = "start"
	_PARAM_END     = "end"
	_PARAM_ORDERBY = "orderby"

	//特殊前缀
	_PPREFIX_NOT  = '!'
	_PPREFIX_LIKE = '~'
	_PPREFIX_GT   = '>'
	_PPREFIX_LT   = '<'

	OriginalRemoteAddrKey = "originalRemoteAddr"

	// 查询类型
	CTYPE_IS = iota
	CTYPE_NOT
	CTYPE_OR
	CTYPE_LIKE
	CTYPE_GT
	CTYPE_LT
	CTYPE_JOIN
	CTYPE_RANGE
	CTYPE_ORDER
	CTYPE_PAGE
	CTYPE_RAW
)

var (
	xForwardedFor      = http.CanonicalHeaderKey("X-Forwarded-For")
	xRealIP            = http.CanonicalHeaderKey("X-Real-IP")
	contentType        = http.CanonicalHeaderKey("Content-Type")
	accHeader          = http.CanonicalHeaderKey("Accept")
	otpHeader          = http.CanonicalHeaderKey("X-Qh-Otp")
	contentDisposition = http.CanonicalHeaderKey("Content-Disposition")
	contentMD5         = http.CanonicalHeaderKey("Content-MD5")
	rcHolder           func(c web.C, w http.ResponseWriter, r *http.Request) *RESTContext
)

/* {{{ func getCTypeByPrefix(p string) int
 *
 */
func getCTypeByPrefix(p byte) int {
	switch p {
	case _PPREFIX_NOT:
		return CTYPE_NOT
	case _PPREFIX_LIKE:
		return CTYPE_LIKE
	case _PPREFIX_GT:
		return CTYPE_GT
	case _PPREFIX_LT:
		return CTYPE_LT
	default:
		return CTYPE_IS
	}
}

/* }}} */

/* {{{ func EnvInit(c *web.C, h http.Handler) http.Handler
 * 初始化环境
 */
func EnvInit(c *web.C, h http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		ac := new(Access) //access日志信息
		ac.Time = time.Now()
		ac.Http = new(HTTPLog)
		ac.Http.Method = r.Method
		ac.Http.URI = r.RequestURI
		ac.Http.Proto = r.Proto
		ac.Http.Host = r.Host
		ac.Http.InHeader = &r.Header
		// env
		if c.Env == nil {
			//c.Env = make(map[string]interface{})
			c.Env = make(map[interface{}]interface{})
		}

		// make rand string(for debug, session...)
		buf := make([]byte, 16)
		randbo.New().Read(buf) //号称最快的随机字符串
		ac.Session = fmt.Sprintf("%x", buf)

		c.Env[RequestIDKey] = ac.Session

		c.Env[LogPrefixKey] = "[" + ac.Session[:10] + "]" //只显示前十位

		Trace("[%s] [%s %s] started", ac.Session[:10], r.Method, r.RequestURI)

		lw := utils.WrapWriter(w)

		pathPieces := strings.Split(r.URL.Path, "/")
		for off, piece := range pathPieces {
			if piece != "" {
				if off == 1 {
					c.Env[EndpointKey] = piece
				}
				if off == 2 && piece[0] != '@' { //@开头是selector
					c.Env[RowkeyKey] = piece
				}
				if off > 1 && piece[0] == '@' {
					c.Env[SelectorKey] = piece
				}
			}
		}
		// real ip(处理在代理服务器之后的情况)
		if rip := realIP(r); rip != "" {
			c.Env[OriginalRemoteAddrKey] = r.RemoteAddr
			r.RemoteAddr = rip
		}
		ac.Http.IP = r.RemoteAddr

		//init RESTContext
		var rcErr error
		var rc *RESTContext
		rc, rcHolder, rcErr = RCHolder(*c, w, r)
		rc.Access = ac
		rc.Access.Http.ReqLength = len(rc.RequestBody)
		if rcErr != nil {
			rc.RESTBadRequest(rcErr)
			return
		}
		//app logging,默认为AppLog
		rc.NewAppLogging(new(AppLog))

		h.ServeHTTP(lw, r)

	}

	return http.HandlerFunc(fn)
}

/* }}} */

/* {{{ func Defer(c *web.C, h http.Handler) http.Handler
 * recovers from panics
 */
func Defer(c *web.C, h http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {

		rc := rcHolder(*c, w, r)
		defer func() {
			if err := recover(); err != nil {
				rc.Critical("[%s %s] %v", r.Method, r.URL.Path, err)
				//debug.PrintStack()
				rc.Critical("%s", debug.Stack())
				//http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
				rc.HTTPError(http.StatusInternalServerError)
			}
			// release locks
			rc.ReleaseLocks()
			// launch tasks
			rc.LaunchTasks()

			// save access log here
			ac := rc.Access
			ac.Duration = time.Now().Sub(ac.Time).String()
			ac.Http.Status = rc.Status
			ac.Http.OutHeader = w.Header()
			ac.Http.RepLength = rc.ContentLength
			if sb := rc.GetEnv(SaveBodyKey); sb != nil && sb.(bool) == true {
				//可以由应用程序决定是否记录body
				ac.Http.ReqBody = string(rc.RequestBody)
			}
			//ac.App = string(rc.RequestBody)

			rc.Debug("[%s %s] end:%d in %s", ac.Http.Method, ac.Http.URI, ac.Http.Status, ac.Duration)
			// save access
			rc.SaveAccess()
		}()

		h.ServeHTTP(w, r)
	}

	return http.HandlerFunc(fn)
}

/* }}} */

/* {{{ func ParseHeaders(c *web.C, h http.Handler) http.Handler
 * headers相关处理
 */
func ParseHeaders(c *web.C, h http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {

		rc := rcHolder(*c, w, r)

		if cs := r.Header.Get(contentMD5); cs != "" {
			rc.SetEnv(ContentMD5Key, cs)
		}

		// Accept
		if acc := r.Header.Get(accHeader); acc != "" {
			if acs := regexp.MustCompile("^application/vnd.[\\w]+.v([\\d.]+)\\+(\\w+)$").FindStringSubmatch(acc); len(acs) == 3 {
				rc.Version = acs[1]
				if strings.ToLower(acs[2]) == "html" {
					rc.Accept = ContentTypeHTML
				} else if strings.ToLower(acs[2]) == "xml" {
					rc.Accept = ContentTypeXML
				}
			}
		}
		// OTP Header
		if v, t, s, err := utils.ParseOTP(r.Header, otpHeader); err == nil {
			rc.OTP = &OTPSpec{Value: v, Type: t, Sn: s}
		}

		// Content-Type
		if ct := r.Header.Get(contentType); ct != "" {
			rc.SetEnv(MimeTypeKey, ct)
		}
		if cd := r.Header.Get(contentDisposition); cd != "" {
			//以传入的Disposition为主
			if t, m, e := mime.ParseMediaType(cd); e == nil {
				rc.Debug("disposition: %s, mediatype: %s", cd, t)
				rc.SetEnv(DispositionMTKey, t)
				for k, v := range m {
					dk := DispositionPrefix + k + "_"
					rc.Debug("disposition key: %s, value: %v", dk, v)
					rc.SetEnv(dk, v)
				}
			}

		}

		h.ServeHTTP(w, r)
	}

	return http.HandlerFunc(fn)
}

/* }}} */

/* {{{ func ParseParams(c *web.C, h http.Handler) http.Handler {
 *
 */
func ParseParams(c *web.C, h http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {

		rc := rcHolder(*c, w, r)
		// 解析参数
		r.ParseForm()
		// 根据ogo规则解析参数
		var ct int
		var p, pp string
		rc.setTimeRangeFromStartEnd()
		for k, v := range r.Form {
			switch k { //处理参数
			case _PARAM_DATE:
				rc.setTimeRangeFromDate(v)
			case _PARAM_ORDERBY:
				rc.setOrderBy(v)
			case _PARAM_FIELDS:
				//过滤字段
				if len(v) > 1 { //传了多个
					rc.SetEnv(FieldsKey, v)
				} else {
					if strings.Contains(v[0], ",") {
						rc.SetEnv(FieldsKey, strings.Split(v[0], ","))
					} else {
						rc.SetEnv(FieldsKey, v)
					}
				}
			case _PARAM_PERPAGE:
				if len(v) > 0 {
					pp = v[0]
				}
			case _PARAM_PAGE: //分页信息
				if len(v) > 0 {
					p = v[0]
				}
			default:
				//除了以上的特别字段,其他都是条件查询
				var cv interface{}
				//var con *Condition
				//var err error

				if len(v) > 1 {
					cv = v
				} else {
					//cv = v[0]
					//处理逗号情况
					if strings.Contains(v[0], ",") {
						cv = strings.Split(v[0], ",")
					} else {
						cv = v[0]
					}
				}

				//根据参数名第一个字符来判断条件类型
				prefix := k[0] //param prefix
				if ct = getCTypeByPrefix(prefix); ct != CTYPE_IS {
					k = k[1:]
					//Debug("[key: %s][ctype: %d]", k, ct)
				}

				if strings.Contains(k, "|") { //包含"|",OR条件
					os := strings.Split(k, "|")
					for _, of := range os {
						if of != "" {
							//rc.Debug("[or condition][or field: %s]", of)
							rc.setCondition(NewCondition(CTYPE_OR, of, NewCondition(ct, k, cv))) // k代表同类的or条件
						}
					}
				} else {
					//如果参数中包含".",代表有关联查询
					if strings.Contains(k, ".") {
						js := strings.SplitN(k, ".", 2)
						if js[0] != "" && js[1] != "" {
							k = js[0]
							cv = NewCondition(ct, js[1], cv)
							//查询类型变为join
							rc.Trace("join: %s, %s; con: %v", k, cv.(*Condition).Field, cv)
							ct = CTYPE_JOIN
						}
					}
					rc.setCondition(NewCondition(ct, k, cv))
				}

			}
		}
		//记录分页信息
		rc.SetEnv(PaginationKey, NewPagination(p, pp))

		h.ServeHTTP(w, r)
	}

	return http.HandlerFunc(fn)
}

/* }}} */

/* {{{ func realIP(r *http.Request) string
 * 获取真实IP
 */
func realIP(r *http.Request) string {
	var ip string

	if xff := r.Header.Get(xForwardedFor); xff != "" {
		i := strings.Index(xff, ", ")
		if i == -1 {
			i = len(xff)
		}
		ip = xff[:i]
	} else if xrip := r.Header.Get(xRealIP); xrip != "" {
		ip = xrip
	}

	return ip
}

/* }}} */
<commit_msg>update<commit_after>package ogo

import (
	"fmt"
	"mime"
	"net/http"
	"regexp"
	"runtime/debug"
	"strings"
	"time"

	"github.com/Odinman/ogo/utils"
	"github.com/dustin/randbo"
	"github.com/zenazn/goji/web"
)

// Key to use when setting the request ID.
const (
	//固定参数名称
	_PARAM_FIELDS  = "fields"
	_PARAM_PAGE    = "page"
	_PARAM_PERPAGE = "per_page"
	_PARAM_DATE    = "date"
	_PARAM_START   = "start"
	_PARAM_END     = "end"
	_PARAM_ORDERBY = "orderby"

	//特殊前缀
	_PPREFIX_NOT  = '!'
	_PPREFIX_LIKE = '~'
	_PPREFIX_GT   = '>'
	_PPREFIX_LT   = '<'

	OriginalRemoteAddrKey = "originalRemoteAddr"

	// 查询类型
	CTYPE_IS = iota
	CTYPE_NOT
	CTYPE_OR
	CTYPE_LIKE
	CTYPE_GT
	CTYPE_LT
	CTYPE_JOIN
	CTYPE_RANGE
	CTYPE_ORDER
	CTYPE_PAGE
	CTYPE_RAW
)

var (
	xForwardedFor      = http.CanonicalHeaderKey("X-Forwarded-For")
	xRealIP            = http.CanonicalHeaderKey("X-Real-IP")
	contentType        = http.CanonicalHeaderKey("Content-Type")
	accHeader          = http.CanonicalHeaderKey("Accept")
	otpHeader          = http.CanonicalHeaderKey("X-Qh-Otp")
	contentDisposition = http.CanonicalHeaderKey("Content-Disposition")
	contentMD5         = http.CanonicalHeaderKey("Content-MD5")
	rcHolder           func(c web.C, w http.ResponseWriter, r *http.Request) *RESTContext
)

/* {{{ func getCTypeByPrefix(p string) int
 *
 */
func getCTypeByPrefix(p byte) int {
	switch p {
	case _PPREFIX_NOT:
		return CTYPE_NOT
	case _PPREFIX_LIKE:
		return CTYPE_LIKE
	case _PPREFIX_GT:
		return CTYPE_GT
	case _PPREFIX_LT:
		return CTYPE_LT
	default:
		return CTYPE_IS
	}
}

/* }}} */

/* {{{ func EnvInit(c *web.C, h http.Handler) http.Handler
 * 初始化环境
 */
func EnvInit(c *web.C, h http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		ac := new(Access) //access日志信息
		ac.Time = time.Now()
		ac.Http = new(HTTPLog)
		ac.Http.Method = r.Method
		ac.Http.URI = r.RequestURI
		ac.Http.Proto = r.Proto
		ac.Http.Host = r.Host
		ac.Http.InHeader = &r.Header
		// env
		if c.Env == nil {
			//c.Env = make(map[string]interface{})
			c.Env = make(map[interface{}]interface{})
		}

		// make rand string(for debug, session...)
		buf := make([]byte, 16)
		randbo.New().Read(buf) //号称最快的随机字符串
		ac.Session = fmt.Sprintf("%x", buf)

		c.Env[RequestIDKey] = ac.Session

		c.Env[LogPrefixKey] = "[" + ac.Session[:10] + "]" //只显示前十位

		Trace("[%s] [%s %s] started", ac.Session[:10], r.Method, r.RequestURI)

		lw := utils.WrapWriter(w)

		pathPieces := strings.Split(r.URL.Path, "/")
		for off, piece := range pathPieces {
			if piece != "" {
				if off == 1 {
					c.Env[EndpointKey] = piece
				}
				if off == 2 && piece[0] != '@' { //@开头是selector
					c.Env[RowkeyKey] = piece
				}
				if off > 1 && piece[0] == '@' {
					c.Env[SelectorKey] = piece
				}
			}
		}
		// real ip(处理在代理服务器之后的情况)
		if rip := realIP(r); rip != "" {
			c.Env[OriginalRemoteAddrKey] = r.RemoteAddr
			r.RemoteAddr = rip
		}
		ac.Http.IP = r.RemoteAddr

		//init RESTContext
		var rcErr error
		var rc *RESTContext
		rc, rcHolder, rcErr = RCHolder(*c, w, r)
		rc.Access = ac
		rc.Access.Http.ReqLength = len(rc.RequestBody)
		if rcErr != nil {
			rc.RESTBadRequest(rcErr)
			return
		}
		//app logging,默认为AppLog
		rc.NewAppLogging(new(AppLog))

		h.ServeHTTP(lw, r)

	}

	return http.HandlerFunc(fn)
}

/* }}} */

/* {{{ func Defer(c *web.C, h http.Handler) http.Handler
 * recovers from panics
 */
func Defer(c *web.C, h http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {

		rc := rcHolder(*c, w, r)
		defer func() {
			if err := recover(); err != nil {
				rc.Critical("[%s %s] %v", r.Method, r.URL.Path, err)
				//debug.PrintStack()
				rc.Critical("%s", debug.Stack())
				//http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
				rc.HTTPError(http.StatusInternalServerError)
			}
			// release locks
			rc.ReleaseLocks()
			// launch tasks
			rc.LaunchTasks()

			// save access log here
			ac := rc.Access
			ac.Duration = time.Now().Sub(ac.Time).String()
			ac.Http.Status = rc.Status
			ac.Http.OutHeader = w.Header()
			ac.Http.RepLength = rc.ContentLength
			if sb := rc.GetEnv(SaveBodyKey); sb != nil && sb.(bool) == true {
				//可以由应用程序决定是否记录body
				ac.Http.ReqBody = string(rc.RequestBody)
			}
			//ac.App = string(rc.RequestBody)

			rc.Debug("[%s %s] end:%d in %s", ac.Http.Method, ac.Http.URI, ac.Http.Status, ac.Duration)
			// save access
			rc.SaveAccess()
		}()

		h.ServeHTTP(w, r)
	}

	return http.HandlerFunc(fn)
}

/* }}} */

/* {{{ func ParseHeaders(c *web.C, h http.Handler) http.Handler
 * headers相关处理
 */
func ParseHeaders(c *web.C, h http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {

		rc := rcHolder(*c, w, r)

		if cs := r.Header.Get(contentMD5); cs != "" {
			rc.SetEnv(ContentMD5Key, cs)
		}

		// Accept
		//rc.Debug("html selector: %s", c.Env[SelectorKey])
		if c.Env[SelectorKey] == "@_html_" {
			rc.Accept = ContentTypeHTML
		} else if acc := r.Header.Get(accHeader); acc != "" {
			if acs := regexp.MustCompile("^application/vnd.[\\w]+.v([\\d.]+)\\+(\\w+)$").FindStringSubmatch(acc); len(acs) == 3 {
				rc.Version = acs[1]
				if strings.ToLower(acs[2]) == "html" {
					rc.Accept = ContentTypeHTML
				} else if strings.ToLower(acs[2]) == "xml" {
					rc.Accept = ContentTypeXML
				}
			}
		}
		// OTP Header
		if v, t, s, err := utils.ParseOTP(r.Header, otpHeader); err == nil {
			rc.OTP = &OTPSpec{Value: v, Type: t, Sn: s}
		}

		// Content-Type
		if ct := r.Header.Get(contentType); ct != "" {
			rc.SetEnv(MimeTypeKey, ct)
		}
		if cd := r.Header.Get(contentDisposition); cd != "" {
			//以传入的Disposition为主
			if t, m, e := mime.ParseMediaType(cd); e == nil {
				rc.Debug("disposition: %s, mediatype: %s", cd, t)
				rc.SetEnv(DispositionMTKey, t)
				for k, v := range m {
					dk := DispositionPrefix + k + "_"
					rc.Debug("disposition key: %s, value: %v", dk, v)
					rc.SetEnv(dk, v)
				}
			}

		}

		h.ServeHTTP(w, r)
	}

	return http.HandlerFunc(fn)
}

/* }}} */

/* {{{ func ParseParams(c *web.C, h http.Handler) http.Handler {
 *
 */
func ParseParams(c *web.C, h http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {

		rc := rcHolder(*c, w, r)
		// 解析参数
		r.ParseForm()
		// 根据ogo规则解析参数
		var ct int
		var p, pp string
		rc.setTimeRangeFromStartEnd()
		for k, v := range r.Form {
			switch k { //处理参数
			case _PARAM_DATE:
				rc.setTimeRangeFromDate(v)
			case _PARAM_ORDERBY:
				rc.setOrderBy(v)
			case _PARAM_FIELDS:
				//过滤字段
				if len(v) > 1 { //传了多个
					rc.SetEnv(FieldsKey, v)
				} else {
					if strings.Contains(v[0], ",") {
						rc.SetEnv(FieldsKey, strings.Split(v[0], ","))
					} else {
						rc.SetEnv(FieldsKey, v)
					}
				}
			case _PARAM_PERPAGE:
				if len(v) > 0 {
					pp = v[0]
				}
			case _PARAM_PAGE: //分页信息
				if len(v) > 0 {
					p = v[0]
				}
			default:
				//除了以上的特别字段,其他都是条件查询
				var cv interface{}
				//var con *Condition
				//var err error

				if len(v) > 1 {
					cv = v
				} else {
					//cv = v[0]
					//处理逗号情况
					if strings.Contains(v[0], ",") {
						cv = strings.Split(v[0], ",")
					} else {
						cv = v[0]
					}
				}

				//根据参数名第一个字符来判断条件类型
				prefix := k[0] //param prefix
				if ct = getCTypeByPrefix(prefix); ct != CTYPE_IS {
					k = k[1:]
					//Debug("[key: %s][ctype: %d]", k, ct)
				}

				if strings.Contains(k, "|") { //包含"|",OR条件
					os := strings.Split(k, "|")
					for _, of := range os {
						if of != "" {
							//rc.Debug("[or condition][or field: %s]", of)
							rc.setCondition(NewCondition(CTYPE_OR, of, NewCondition(ct, k, cv))) // k代表同类的or条件
						}
					}
				} else {
					//如果参数中包含".",代表有关联查询
					if strings.Contains(k, ".") {
						js := strings.SplitN(k, ".", 2)
						if js[0] != "" && js[1] != "" {
							k = js[0]
							cv = NewCondition(ct, js[1], cv)
							//查询类型变为join
							rc.Trace("join: %s, %s; con: %v", k, cv.(*Condition).Field, cv)
							ct = CTYPE_JOIN
						}
					}
					rc.setCondition(NewCondition(ct, k, cv))
				}

			}
		}
		//记录分页信息
		rc.SetEnv(PaginationKey, NewPagination(p, pp))

		h.ServeHTTP(w, r)
	}

	return http.HandlerFunc(fn)
}

/* }}} */

/* {{{ func realIP(r *http.Request) string
 * 获取真实IP
 */
func realIP(r *http.Request) string {
	var ip string

	if xff := r.Header.Get(xForwardedFor); xff != "" {
		i := strings.Index(xff, ", ")
		if i == -1 {
			i = len(xff)
		}
		ip = xff[:i]
	} else if xrip := r.Header.Get(xRealIP); xrip != "" {
		ip = xrip
	}

	return ip
}

/* }}} */
<|endoftext|>


<commit_before>package terraform

import (
	"fmt"
	"log"

	"github.com/hashicorp/terraform/addrs"
	"github.com/hashicorp/terraform/providers"
	"github.com/hashicorp/terraform/states"
)

// EvalRefresh is an EvalNode implementation that does a refresh for
// a resource.
type EvalRefresh struct {
	Addr     addrs.ResourceInstance
	Provider *providers.Interface
	State    **states.ResourceInstanceObject
	Output   **states.ResourceInstanceObject
}

// TODO: test
func (n *EvalRefresh) Eval(ctx EvalContext) (interface{}, error) {
	state := *n.State
	absAddr := n.Addr.Absolute(ctx.Path())

	// If we have no state, we don't do any refreshing
	if state == nil {
		log.Printf("[DEBUG] refresh: %s: no state, so not refreshing", n.Addr.Absolute(ctx.Path()))
		return nil, nil
	}

	// Call pre-refresh hook
	err := ctx.Hook(func(h Hook) (HookAction, error) {
		return h.PreRefresh(absAddr, states.CurrentGen, state.Value)
	})
	if err != nil {
		return nil, err
	}

	// Refresh!
	priorVal := state.Value
	// TODO: Shim our new state type into the old one
	//provider := *n.Provider
	//state, err = provider.Refresh(legacyInfo, state)
	return nil, fmt.Errorf("EvalRefresh is not yet updated for new state type")
	if err != nil {
		return nil, fmt.Errorf("%s: %s", n.Addr.Absolute(ctx.Path()), err.Error())
	}
	if state == nil {
		log.Printf("[TRACE] EvalRefresh: after refresh, %s has nil state", n.Addr)
	}

	// Call post-refresh hook
	err = ctx.Hook(func(h Hook) (HookAction, error) {
		return h.PostRefresh(absAddr, states.CurrentGen, priorVal, state.Value)
	})
	if err != nil {
		return nil, err
	}

	if n.Output != nil {
		*n.Output = state
	}

	return nil, nil
}
<commit_msg>update Refresh<commit_after>package terraform

import (
	"fmt"
	"log"

	"github.com/hashicorp/terraform/addrs"
	"github.com/hashicorp/terraform/providers"
	"github.com/hashicorp/terraform/states"
)

// EvalRefresh is an EvalNode implementation that does a refresh for
// a resource.
type EvalRefresh struct {
	Addr     addrs.ResourceInstance
	Provider *providers.Interface
	State    **states.ResourceInstanceObject
	Output   **states.ResourceInstanceObject
}

// TODO: test
func (n *EvalRefresh) Eval(ctx EvalContext) (interface{}, error) {
	state := *n.State
	absAddr := n.Addr.Absolute(ctx.Path())

	// If we have no state, we don't do any refreshing
	if state == nil {
		log.Printf("[DEBUG] refresh: %s: no state, so not refreshing", n.Addr.Absolute(ctx.Path()))
		return nil, nil
	}

	// Call pre-refresh hook
	err := ctx.Hook(func(h Hook) (HookAction, error) {
		return h.PreRefresh(absAddr, states.CurrentGen, state.Value)
	})
	if err != nil {
		return nil, err
	}

	// Refresh!
	priorVal := state.Value
	req := providers.ReadResourceRequest{
		TypeName:   n.Addr.Resource.Type,
		PriorState: priorVal,
	}

	provider := *n.Provider
	resp := provider.ReadResource(req)
	if resp.Diagnostics.HasErrors() {
		return nil, fmt.Errorf("%s: %s", n.Addr.Absolute(ctx.Path()), resp.Diagnostics.Err())
	}

	state.Value = resp.NewState

	// Call post-refresh hook
	err = ctx.Hook(func(h Hook) (HookAction, error) {
		return h.PostRefresh(absAddr, states.CurrentGen, priorVal, state.Value)
	})
	if err != nil {
		return nil, err
	}

	if n.Output != nil {
		*n.Output = state
	}

	return nil, nil
}
<|endoftext|>


<commit_before>package httpapi

import (
	"encoding/json"
	"github.com/boivie/lovebeat-go/service"
	"github.com/gorilla/mux"
	"github.com/op/go-logging"
	"io"
	"net/http"
	"strconv"
	"time"
)

var (
	svcs   *service.Services
	client service.ServiceIf
)

func now() int64 { return time.Now().Unix() }

var log = logging.MustGetLogger("lovebeat")

func ServiceHandler(c http.ResponseWriter, r *http.Request) {
	params := mux.Vars(r)
	name := params["name"]

	var err = r.ParseForm()
	if err != nil {
		log.Error("error parsing form ", err)
		return
	}

	var errtmo, warntmo = r.FormValue("err-tmo"), r.FormValue("warn-tmo")

	client.Beat(name)

	if val, err := strconv.Atoi(errtmo); err == nil {
		client.SetErrorTimeout(name, val)
	}

	if val, err := strconv.Atoi(warntmo); err == nil {
		client.SetWarningTimeout(name, val)
	}

	c.Header().Add("Content-Type", "text/plain")
	c.Header().Add("Content-Length", "3")
	io.WriteString(c, "{}\n")
}

func DeleteServiceHandler(c http.ResponseWriter, r *http.Request) {
	params := mux.Vars(r)
	name := params["name"]

	client.DeleteService(name)

	c.Header().Add("Content-Type", "text/plain")
	c.Header().Add("Content-Length", "3")
	io.WriteString(c, "{}\n")
}

func CreateViewHandler(c http.ResponseWriter, r *http.Request) {
	params := mux.Vars(r)
	view_name := params["name"]
	var expr = r.FormValue("regexp")
	if expr == "" {
		log.Error("No regexp provided")
		return
	}

	client.CreateOrUpdateView(view_name, expr)

	c.Header().Add("Content-Type", "text/plain")
	c.Header().Add("Content-Length", "3")
	io.WriteString(c, "{}\n")
}

func DeleteViewHandler(c http.ResponseWriter, r *http.Request) {
	params := mux.Vars(r)
	name := params["name"]

	client.DeleteView(name)

	c.Header().Add("Content-Type", "text/plain")
	c.Header().Add("Content-Length", "3")
	io.WriteString(c, "{}\n")
}

type JsonView struct {
	Name  string `json:"name"`
	State string `json:"state"`
}

func GetViewsHandler(c http.ResponseWriter, r *http.Request) {
	var ret = make([]JsonView, 0)
	for _, v := range client.GetViews() {
		js := JsonView{
			Name:  v.Name,
			State: v.State,
		}
		ret = append(ret, js)
	}
	var encoded, _ = json.MarshalIndent(ret, "", "  ")

	c.Header().Add("Content-Type", "text/plain")
	c.Header().Add("Content-Length", strconv.Itoa(len(encoded)+1))
	c.Write(encoded)
	io.WriteString(c, "\n")
}

type JsonService struct {
	Name           string `json:"name"`
	LastBeat       int64  `json:"last_beat"`
	WarningTimeout int64  `json:"warning_timeout"`
	ErrorTimeout   int64  `json:"error_timeout"`
	State          string `json:"state"`
}

func GetServicesHandler(c http.ResponseWriter, r *http.Request) {
	viewName := "all"

	if val, ok := r.URL.Query()["view"]; ok {
		viewName = val[0]
	}

	var ret = make([]JsonService, 0)
	for _, s := range client.GetServices(viewName) {
		js := JsonService{
			Name:           s.Name,
			LastBeat:       s.LastBeat,
			WarningTimeout: s.WarningTimeout,
			ErrorTimeout:   s.ErrorTimeout,
			State:          s.State,
		}
		ret = append(ret, js)
	}
	var encoded, _ = json.MarshalIndent(ret, "", "  ")

	c.Header().Add("Content-Type", "text/plain")
	c.Header().Add("Content-Length", strconv.Itoa(len(encoded)+1))
	c.Write(encoded)
	io.WriteString(c, "\n")
}

func Register(rtr *mux.Router, client_ service.ServiceIf) {
	client = client_
	rtr.HandleFunc("/api/services/", GetServicesHandler).Methods("GET")
	rtr.HandleFunc("/api/services/{name:[a-z0-9.]+}", ServiceHandler).Methods("POST")
	rtr.HandleFunc("/api/services/{name:[a-z0-9.]+}", DeleteServiceHandler).Methods("DELETE")
	rtr.HandleFunc("/api/views/", GetViewsHandler).Methods("GET")
	rtr.HandleFunc("/api/views/{name:[a-z0-9.]+}", CreateViewHandler).Methods("POST")
	rtr.HandleFunc("/api/views/{name:[a-z0-9.]+}", DeleteViewHandler).Methods("DELETE")
}
<commit_msg>httpapi: Added "last beat delta"<commit_after>package httpapi

import (
	"encoding/json"
	"github.com/boivie/lovebeat-go/service"
	"github.com/gorilla/mux"
	"github.com/op/go-logging"
	"io"
	"net/http"
	"strconv"
	"time"
)

var (
	svcs   *service.Services
	client service.ServiceIf
)

func now() int64 { return time.Now().Unix() }

var log = logging.MustGetLogger("lovebeat")

func ServiceHandler(c http.ResponseWriter, r *http.Request) {
	params := mux.Vars(r)
	name := params["name"]

	var err = r.ParseForm()
	if err != nil {
		log.Error("error parsing form ", err)
		return
	}

	var errtmo, warntmo = r.FormValue("err-tmo"), r.FormValue("warn-tmo")

	client.Beat(name)

	if val, err := strconv.Atoi(errtmo); err == nil {
		client.SetErrorTimeout(name, val)
	}

	if val, err := strconv.Atoi(warntmo); err == nil {
		client.SetWarningTimeout(name, val)
	}

	c.Header().Add("Content-Type", "text/plain")
	c.Header().Add("Content-Length", "3")
	io.WriteString(c, "{}\n")
}

func DeleteServiceHandler(c http.ResponseWriter, r *http.Request) {
	params := mux.Vars(r)
	name := params["name"]

	client.DeleteService(name)

	c.Header().Add("Content-Type", "text/plain")
	c.Header().Add("Content-Length", "3")
	io.WriteString(c, "{}\n")
}

func CreateViewHandler(c http.ResponseWriter, r *http.Request) {
	params := mux.Vars(r)
	view_name := params["name"]
	var expr = r.FormValue("regexp")
	if expr == "" {
		log.Error("No regexp provided")
		return
	}

	client.CreateOrUpdateView(view_name, expr)

	c.Header().Add("Content-Type", "text/plain")
	c.Header().Add("Content-Length", "3")
	io.WriteString(c, "{}\n")
}

func DeleteViewHandler(c http.ResponseWriter, r *http.Request) {
	params := mux.Vars(r)
	name := params["name"]

	client.DeleteView(name)

	c.Header().Add("Content-Type", "text/plain")
	c.Header().Add("Content-Length", "3")
	io.WriteString(c, "{}\n")
}

type JsonView struct {
	Name  string `json:"name"`
	State string `json:"state"`
}

func GetViewsHandler(c http.ResponseWriter, r *http.Request) {
	var ret = make([]JsonView, 0)
	for _, v := range client.GetViews() {
		js := JsonView{
			Name:  v.Name,
			State: v.State,
		}
		ret = append(ret, js)
	}
	var encoded, _ = json.MarshalIndent(ret, "", "  ")

	c.Header().Add("Content-Type", "text/plain")
	c.Header().Add("Content-Length", strconv.Itoa(len(encoded)+1))
	c.Write(encoded)
	io.WriteString(c, "\n")
}

type JsonService struct {
	Name           string `json:"name"`
	LastBeat       int64  `json:"last_beat"`
	LastBeatDelta  int64  `json:"last_beat_delta"`
	WarningTimeout int64  `json:"warning_timeout"`
	ErrorTimeout   int64  `json:"error_timeout"`
	State          string `json:"state"`
}

func GetServicesHandler(c http.ResponseWriter, r *http.Request) {
	viewName := "all"

	if val, ok := r.URL.Query()["view"]; ok {
		viewName = val[0]
	}
	var now = now()
	var ret = make([]JsonService, 0)
	for _, s := range client.GetServices(viewName) {
		js := JsonService{
			Name:           s.Name,
			LastBeat:       s.LastBeat,
			LastBeatDelta:  now - s.LastBeat,
			WarningTimeout: s.WarningTimeout,
			ErrorTimeout:   s.ErrorTimeout,
			State:          s.State,
		}
		ret = append(ret, js)
	}
	var encoded, _ = json.MarshalIndent(ret, "", "  ")

	c.Header().Add("Content-Type", "text/plain")
	c.Header().Add("Content-Length", strconv.Itoa(len(encoded)+1))
	c.Write(encoded)
	io.WriteString(c, "\n")
}

func Register(rtr *mux.Router, client_ service.ServiceIf) {
	client = client_
	rtr.HandleFunc("/api/services/", GetServicesHandler).Methods("GET")
	rtr.HandleFunc("/api/services/{name:[a-z0-9.]+}", ServiceHandler).Methods("POST")
	rtr.HandleFunc("/api/services/{name:[a-z0-9.]+}", DeleteServiceHandler).Methods("DELETE")
	rtr.HandleFunc("/api/views/", GetViewsHandler).Methods("GET")
	rtr.HandleFunc("/api/views/{name:[a-z0-9.]+}", CreateViewHandler).Methods("POST")
	rtr.HandleFunc("/api/views/{name:[a-z0-9.]+}", DeleteViewHandler).Methods("DELETE")
}
<|endoftext|>


<commit_before>// Copyright 2012 Aaron Jacobs. All Rights Reserved.
// Author: aaronjjacobs@gmail.com (Aaron Jacobs)
//
// 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 blob_test

import (
	"errors"
	"github.com/jacobsa/comeback/blob"
	"github.com/jacobsa/comeback/blob/mock"
	"github.com/jacobsa/comeback/crypto/mock"
	. "github.com/jacobsa/oglematchers"
	"github.com/jacobsa/oglemock"
	. "github.com/jacobsa/ogletest"
	"testing"
)

func TestEncrypting(t *testing.T) { RunTests(t) }

////////////////////////////////////////////////////////////////////////
// Helpers
////////////////////////////////////////////////////////////////////////

type encryptingStoreTest struct{
	crypter mock_crypto.MockCrypter
	wrapped mock_blob.MockStore
	store blob.Store
}

func (t *encryptingStoreTest) SetUp(i *TestInfo) {
	t.crypter = mock_crypto.NewMockCrypter(i.MockController, "crypter")
	t.wrapped = mock_blob.NewMockStore(i.MockController, "wrapped")
	t.store = blob.NewEncryptingStore(t.crypter, t.wrapped)
}

////////////////////////////////////////////////////////////////////////
// Store
////////////////////////////////////////////////////////////////////////

type StoreTest struct {
	encryptingStoreTest
}

func init() { RegisterTestSuite(&StoreTest{}) }

func (t *StoreTest) CallsCrypter() {
	blob := []byte{0xde, 0xad}

	// Crypter
	ExpectCall(t.crypter, "Encrypt")(DeepEquals(blob)).
		WillOnce(oglemock.Return(nil, errors.New("")))

	// Call
	t.store.Store(blob)
}

func (t *StoreTest) CrypterReturnsError() {
	ExpectEq("TODO", "")
}

func (t *StoreTest) CallsWrapped() {
	ExpectEq("TODO", "")
}

func (t *StoreTest) WrappedReturnsError() {
	ExpectEq("TODO", "")
}

func (t *StoreTest) WrappedSucceeds() {
	ExpectEq("TODO", "")
}

////////////////////////////////////////////////////////////////////////
// Load
////////////////////////////////////////////////////////////////////////

type LoadTest struct {
	encryptingStoreTest
}

func init() { RegisterTestSuite(&LoadTest{}) }

func (t *LoadTest) CallsWrapped() {
	ExpectEq("TODO", "")
}

func (t *LoadTest) WrappedReturnsError() {
	ExpectEq("TODO", "")
}

func (t *LoadTest) CallsCrypter() {
	ExpectEq("TODO", "")
}

func (t *LoadTest) CrypterReturnsError() {
	ExpectEq("TODO", "")
}

func (t *LoadTest) CrypterSucceeds() {
	ExpectEq("TODO", "")
}
<commit_msg>StoreTest.CrypterReturnsError<commit_after>// Copyright 2012 Aaron Jacobs. All Rights Reserved.
// Author: aaronjjacobs@gmail.com (Aaron Jacobs)
//
// 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 blob_test

import (
	"errors"
	"github.com/jacobsa/comeback/blob"
	"github.com/jacobsa/comeback/blob/mock"
	"github.com/jacobsa/comeback/crypto/mock"
	. "github.com/jacobsa/oglematchers"
	"github.com/jacobsa/oglemock"
	. "github.com/jacobsa/ogletest"
	"testing"
)

func TestEncrypting(t *testing.T) { RunTests(t) }

////////////////////////////////////////////////////////////////////////
// Helpers
////////////////////////////////////////////////////////////////////////

type encryptingStoreTest struct{
	crypter mock_crypto.MockCrypter
	wrapped mock_blob.MockStore
	store blob.Store
}

func (t *encryptingStoreTest) SetUp(i *TestInfo) {
	t.crypter = mock_crypto.NewMockCrypter(i.MockController, "crypter")
	t.wrapped = mock_blob.NewMockStore(i.MockController, "wrapped")
	t.store = blob.NewEncryptingStore(t.crypter, t.wrapped)
}

////////////////////////////////////////////////////////////////////////
// Store
////////////////////////////////////////////////////////////////////////

type StoreTest struct {
	encryptingStoreTest
}

func init() { RegisterTestSuite(&StoreTest{}) }

func (t *StoreTest) CallsCrypter() {
	blob := []byte{0xde, 0xad}

	// Crypter
	ExpectCall(t.crypter, "Encrypt")(DeepEquals(blob)).
		WillOnce(oglemock.Return(nil, errors.New("")))

	// Call
	t.store.Store(blob)
}

func (t *StoreTest) CrypterReturnsError() {
	// Crypter
	ExpectCall(t.crypter, "Encrypt")(Any()).
		WillOnce(oglemock.Return(nil, errors.New("taco")))

	// Call
	_, err := t.store.Store([]byte{})

	ExpectThat(err, Error(HasSubstr("Encrypt")))
	ExpectThat(err, Error(HasSubstr("taco")))
}

func (t *StoreTest) CallsWrapped() {
	ExpectEq("TODO", "")
}

func (t *StoreTest) WrappedReturnsError() {
	ExpectEq("TODO", "")
}

func (t *StoreTest) WrappedSucceeds() {
	ExpectEq("TODO", "")
}

////////////////////////////////////////////////////////////////////////
// Load
////////////////////////////////////////////////////////////////////////

type LoadTest struct {
	encryptingStoreTest
}

func init() { RegisterTestSuite(&LoadTest{}) }

func (t *LoadTest) CallsWrapped() {
	ExpectEq("TODO", "")
}

func (t *LoadTest) WrappedReturnsError() {
	ExpectEq("TODO", "")
}

func (t *LoadTest) CallsCrypter() {
	ExpectEq("TODO", "")
}

func (t *LoadTest) CrypterReturnsError() {
	ExpectEq("TODO", "")
}

func (t *LoadTest) CrypterSucceeds() {
	ExpectEq("TODO", "")
}
<|endoftext|>


<commit_before>package command

import (
	"fmt"
)

type MysqlConvert struct {
	Options MysqlCommonOptions `group:"common"`
	Positional struct {
		Database string `description:"Database" required:"true"`
	} `positional-args:"true"`
	Charset   string           `long:"charset"      description:"MySQL charset"               default:"utf8"`
	Collation string           `long:"collation"    description:"MySQL collation"             default:"utf8_general_ci"`
}

func (conf *MysqlConvert) Execute(args []string) error {
	Logger.Main("Converting MySQL database \"%s\" to charset \"%s\" and collation \"%s\"", conf.Positional.Database, conf.Charset, conf.Collation)
	if err := conf.Options.Init(); err != nil {
		return err
	}

	defer NewSigIntHandler(func() {})()

	// Convert database
	Logger.Step("converting atabase \"%s\"", conf.Positional.Database)
	statement := fmt.Sprintf(
		"SET FOREIGN_KEY_CHECKS=0; ALTER DATABASE %s CHARACTER SET %s COLLATE %s",
		mysqlIdentifier(conf.Positional.Database),
		conf.Charset,
		conf.Collation,
	)
	conf.Options.ExecStatement("mysql", statement)

	// Convert tables
	tableList := conf.Options.GetTableList(conf.Positional.Database)
	for _, table := range tableList {
		Logger.Step("converting table \"%s\"", table)
		statement := fmt.Sprintf(
			"SET FOREIGN_KEY_CHECKS=0; ALTER TABLE %s.%s CONVERT TO CHARACTER SET %s COLLATE %s",
			mysqlIdentifier(conf.Positional.Database),
			mysqlIdentifier(table),
			conf.Charset,
			conf.Collation,
		)
		conf.Options.ExecStatement("mysql", statement)
	}

	return nil
}
<commit_msg>Switch to utf8mb4 for mysql default convert<commit_after>package command

import (
	"fmt"
)

type MysqlConvert struct {
	Options MysqlCommonOptions `group:"common"`
	Positional struct {
		Database string `description:"Database" required:"true"`
	} `positional-args:"true"`
	Charset   string           `long:"charset"      description:"MySQL charset"               default:"utf8mb4"`
	Collation string           `long:"collation"    description:"MySQL collation"             default:"utf8mb4_unicode_ci"`
}

func (conf *MysqlConvert) Execute(args []string) error {
	Logger.Main("Converting MySQL database \"%s\" to charset \"%s\" and collation \"%s\"", conf.Positional.Database, conf.Charset, conf.Collation)
	if err := conf.Options.Init(); err != nil {
		return err
	}

	defer NewSigIntHandler(func() {})()

	// Convert database
	Logger.Step("converting atabase \"%s\"", conf.Positional.Database)
	statement := fmt.Sprintf(
		"SET FOREIGN_KEY_CHECKS=0; ALTER DATABASE %s CHARACTER SET %s COLLATE %s",
		mysqlIdentifier(conf.Positional.Database),
		conf.Charset,
		conf.Collation,
	)
	conf.Options.ExecStatement("mysql", statement)

	// Convert tables
	tableList := conf.Options.GetTableList(conf.Positional.Database)
	for _, table := range tableList {
		Logger.Step("converting table \"%s\"", table)
		statement := fmt.Sprintf(
			"SET FOREIGN_KEY_CHECKS=0; ALTER TABLE %s.%s CONVERT TO CHARACTER SET %s COLLATE %s",
			mysqlIdentifier(conf.Positional.Database),
			mysqlIdentifier(table),
			conf.Charset,
			conf.Collation,
		)
		conf.Options.ExecStatement("mysql", statement)
	}

	return nil
}
<|endoftext|>


<commit_before>package command

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"sort"
	"sync"
	"testing"
	"time"

	"github.com/mackerelio/mackerel-agent/agent"
	"github.com/mackerelio/mackerel-agent/config"
	"github.com/mackerelio/mackerel-agent/logging"
	"github.com/mackerelio/mackerel-agent/mackerel"
	"github.com/mackerelio/mackerel-agent/metrics"
)

func TestDelayByHost(t *testing.T) {
	delay1 := time.Duration(delayByHost(&mackerel.Host{
		ID:     "246PUVUngPo",
		Name:   "hogehoge2.host.h",
		Type:   "unknown",
		Status: "working",
	})) * time.Second

	delay2 := time.Duration(delayByHost(&mackerel.Host{
		ID:     "21GZjCE5Etb",
		Name:   "hogehoge2.host.h",
		Type:   "unknown",
		Status: "working",
	})) * time.Second

	if !(0 <= delay1.Seconds() && delay1.Seconds() < 60) {
		t.Errorf("delay shoud be between 0 and 60 but %v", delay1)
	}

	if delay1 == delay2 {
		t.Error("delays shoud be different")
	}
}

type jsonObject map[string]interface{}

// newMockAPIServer makes a dummy root directry, a mock API server, a conf.Config to using them
// and returns the Config, mock handlers map and the server.
// The mock handlers map is "<method> <path>"-to-jsonObject-generator map.
func newMockAPIServer(t *testing.T) (config.Config, map[string]func(*http.Request) (int, jsonObject), *httptest.Server) {
	mockHandlers := map[string]func(*http.Request) (int, jsonObject){}

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		key := req.Method + " " + req.URL.Path
		handler, ok := mockHandlers[key]
		if !ok {
			t.Fatal("Unexpected request: " + key)
		}

		statusCode, data := handler(req)

		respJSON, err := json.Marshal(data)
		if err != nil {
			t.Fatal("marshalling JSON failed: ", err)
		}

		if statusCode != 0 {
			w.WriteHeader(statusCode)
		}

		w.Header().Set("Content-Type", "application/json")
		fmt.Fprint(w, string(respJSON))
	}))

	root, err := ioutil.TempDir("", "mackerel-agent-test")
	if err != nil {
		t.Fatal(err)
	}

	conf := config.Config{
		Apibase:    ts.URL,
		Root:       root,
		Connection: config.DefaultConfig.Connection,
	}

	return conf, mockHandlers, ts
}

func TestPrepareWithCreate(t *testing.T) {
	conf, mockHandlers, ts := newMockAPIServer(t)
	defer ts.Close()

	mockHandlers["POST /api/v0/hosts"] = func(req *http.Request) (int, jsonObject) {
		return 200, jsonObject{
			"id": "xxx1234567890",
		}
	}

	mockHandlers["GET /api/v0/hosts/xxx1234567890"] = func(req *http.Request) (int, jsonObject) {
		return 200, jsonObject{
			"host": mackerel.Host{
				ID:     "xxx1234567890",
				Name:   "host.example.com",
				Type:   "unknown",
				Status: "standby",
			},
		}
	}

	c, _ := Prepare(&conf)
	api := c.API
	host := c.Host

	if api.BaseURL.String() != ts.URL {
		t.Errorf("Apibase mismatch: %s != %s", api.BaseURL, ts.URL)
	}

	if host.ID != "xxx1234567890" {
		t.Error("Host ID mismatch", host)
	}

	if host.Name != "host.example.com" {
		t.Error("Host name mismatch", host)
	}
}

func TestPrepareWithCreateWithFail(t *testing.T) {
	conf, mockHandlers, ts := newMockAPIServer(t)
	defer ts.Close()

	mockHandlers["POST /api/v0/hosts"] = func(req *http.Request) (int, jsonObject) {
		return 403, jsonObject{
			"result": "error",
		}
	}

	origRetryNum := retryNum
	retryNum = 1
	defer func() {
		retryNum = origRetryNum
	}()

	_, err := Prepare(&conf)

	if err == nil {
		t.Errorf("error should be occurred")
	}
}

func TestPrepareWithUpdate(t *testing.T) {
	conf, mockHandlers, ts := newMockAPIServer(t)
	defer ts.Close()
	tempDir, _ := ioutil.TempDir("", "")
	conf.Root = tempDir
	conf.SaveHostID("xxx12345678901")

	mockHandlers["PUT /api/v0/hosts/xxx12345678901"] = func(req *http.Request) (int, jsonObject) {
		return 200, jsonObject{
			"result": "OK",
		}
	}

	mockHandlers["GET /api/v0/hosts/xxx12345678901"] = func(req *http.Request) (int, jsonObject) {
		return 200, jsonObject{
			"host": mackerel.Host{
				ID:     "xxx12345678901",
				Name:   "host.example.com",
				Type:   "unknown",
				Status: "standby",
			},
		}
	}

	c, _ := Prepare(&conf)
	api := c.API
	host := c.Host

	if api.BaseURL.String() != ts.URL {
		t.Errorf("Apibase mismatch: %s != %s", api.BaseURL, ts.URL)
	}

	if host.ID != "xxx12345678901" {
		t.Error("Host ID mismatch", host)
	}

	if host.Name != "host.example.com" {
		t.Error("Host name mismatch", host)
	}
}

func TestCollectHostSpecs(t *testing.T) {
	hostname, meta, _ /*interfaces*/, _ /*customIdentifier*/, err := collectHostSpecs()

	if err != nil {
		t.Errorf("collectHostSpecs should not fail: %s", err)
	}

	if hostname == "" {
		t.Error("hostname should not be empty")
	}

	if _, ok := meta["cpu"]; !ok {
		t.Error("meta.cpu should exist")
	}
}

type counterGenerator struct {
	counter int
	sync.Mutex
}

func (g *counterGenerator) Generate() (metrics.Values, error) {
	g.Lock()
	defer g.Unlock()
	g.counter = g.counter + 1
	return map[string]float64{"dummy.a": float64(g.counter)}, nil
}

type byTime []mackerel.CreatingMetricsValue

func (b byTime) Len() int           { return len(b) }
func (b byTime) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
func (b byTime) Less(i, j int) bool { return b[i].Time < b[j].Time }

func TestLoop(t *testing.T) {
	if testing.Verbose() {
		logging.SetLogLevel(logging.DEBUG)
	}

	conf, mockHandlers, ts := newMockAPIServer(t)
	defer ts.Close()

	if testing.Short() {
		// Shrink time scale
		originalPostMetricsInterval := config.PostMetricsInterval

		config.PostMetricsInterval = 10 * time.Second
		ratio := config.PostMetricsInterval.Seconds() / originalPostMetricsInterval.Seconds()

		conf.Connection.PostMetricsDequeueDelaySeconds =
			int(float64(config.DefaultConfig.Connection.PostMetricsRetryDelaySeconds) * ratio)
		conf.Connection.PostMetricsRetryDelaySeconds =
			int(float64(config.DefaultConfig.Connection.PostMetricsRetryDelaySeconds) * ratio)

		defer func() {
			config.PostMetricsInterval = originalPostMetricsInterval
		}()
	}

	/// Simulate the situation that mackerel.io is down for 3 min
	// Strategy:
	// counterGenerator generates values 1,2,3,4,...
	// when we got value 3, the server will start responding 503 for three times (inclusive)
	// so the agent should queue the generated values and retry sending.
	//
	//  status: o . o . x . x . x . o o o o o
	//    send: 1 . 2 . 3 . 3 . 3 . 3 4 5 6 7
	// collect: 1 . 2 . 3 . 4 . 5 . 6 . 7 . 8
	//           ^
	//           30s
	const (
		totalFailures = 3
		totalPosts    = 7
	)
	failureCount := 0
	receivedDataPoints := []mackerel.CreatingMetricsValue{}
	done := make(chan struct{})

	mockHandlers["POST /api/v0/tsdb"] = func(req *http.Request) (int, jsonObject) {
		payload := []mackerel.CreatingMetricsValue{}
		json.NewDecoder(req.Body).Decode(&payload)

		for _, p := range payload {
			value := p.Value.(float64)
			if value == 3 {
				failureCount++
				if failureCount <= totalFailures {
					return 503, jsonObject{
						"failure": failureCount, // just for DEBUG logging
					}
				}
			}

			if value == totalPosts {
				defer func() { done <- struct{}{} }()
			}
		}

		receivedDataPoints = append(receivedDataPoints, payload...)

		return 200, jsonObject{
			"success": true,
		}
	}
	mockHandlers["PUT /api/v0/hosts/xyzabc12345"] = func(req *http.Request) (int, jsonObject) {
		return 200, jsonObject{
			"result": "OK",
		}
	}

	// Prepare required objects...
	ag := &agent.Agent{
		MetricsGenerators: []metrics.Generator{
			&counterGenerator{},
		},
	}

	api, err := mackerel.NewAPI(conf.Apibase, conf.Apikey, true)
	if err != nil {
		t.Fatal(err)
	}

	host := &mackerel.Host{ID: "xyzabc12345"}

	termCh := make(chan struct{})
	exitCh := make(chan error)
	c := &Context{
		Agent:  ag,
		Config: &conf,
		API:    api,
		Host:   host,
	}
	// Start looping!
	go func() {
		exitCh <- loop(c, termCh)
	}()

	<-done

	// Verify results
	if len(receivedDataPoints) != totalPosts {
		t.Errorf("the agent should have sent %d datapoints, got: %+v", totalPosts, receivedDataPoints)
	}

	sort.Sort(byTime(receivedDataPoints))

	for i := 0; i < totalPosts; i++ {
		value := receivedDataPoints[i].Value.(float64)
		if value != float64(i+1) {
			t.Errorf("the %dth datapoint should have value %d, got: %+v", i, i+1, receivedDataPoints)
		}
	}

	termCh <- struct{}{}
	exitErr := <-exitCh
	if exitErr != nil {
		t.Errorf("exitErr should be nil, got: %s", exitErr)
	}
}
<commit_msg>fix test<commit_after>package command

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"sort"
	"sync"
	"testing"
	"time"

	"github.com/mackerelio/mackerel-agent/agent"
	"github.com/mackerelio/mackerel-agent/config"
	"github.com/mackerelio/mackerel-agent/logging"
	"github.com/mackerelio/mackerel-agent/mackerel"
	"github.com/mackerelio/mackerel-agent/metrics"
)

func TestDelayByHost(t *testing.T) {
	delay1 := time.Duration(delayByHost(&mackerel.Host{
		ID:     "246PUVUngPo",
		Name:   "hogehoge2.host.h",
		Type:   "unknown",
		Status: "working",
	})) * time.Second

	delay2 := time.Duration(delayByHost(&mackerel.Host{
		ID:     "21GZjCE5Etb",
		Name:   "hogehoge2.host.h",
		Type:   "unknown",
		Status: "working",
	})) * time.Second

	if !(0 <= delay1.Seconds() && delay1.Seconds() < 60) {
		t.Errorf("delay shoud be between 0 and 60 but %v", delay1)
	}

	if delay1 == delay2 {
		t.Error("delays shoud be different")
	}
}

type jsonObject map[string]interface{}

// newMockAPIServer makes a dummy root directry, a mock API server, a conf.Config to using them
// and returns the Config, mock handlers map and the server.
// The mock handlers map is "<method> <path>"-to-jsonObject-generator map.
func newMockAPIServer(t *testing.T) (config.Config, map[string]func(*http.Request) (int, jsonObject), *httptest.Server) {
	mockHandlers := map[string]func(*http.Request) (int, jsonObject){}

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		key := req.Method + " " + req.URL.Path
		handler, ok := mockHandlers[key]
		if !ok {
			t.Fatal("Unexpected request: " + key)
		}

		statusCode, data := handler(req)

		respJSON, err := json.Marshal(data)
		if err != nil {
			t.Fatal("marshalling JSON failed: ", err)
		}

		if statusCode != 0 {
			w.WriteHeader(statusCode)
		}

		w.Header().Set("Content-Type", "application/json")
		fmt.Fprint(w, string(respJSON))
	}))

	root, err := ioutil.TempDir("", "mackerel-agent-test")
	if err != nil {
		t.Fatal(err)
	}

	conf := config.Config{
		Apibase:    ts.URL,
		Root:       root,
		Connection: config.DefaultConfig.Connection,
	}

	return conf, mockHandlers, ts
}

func TestPrepareWithCreate(t *testing.T) {
	conf, mockHandlers, ts := newMockAPIServer(t)
	defer ts.Close()

	mockHandlers["POST /api/v0/hosts"] = func(req *http.Request) (int, jsonObject) {
		return 200, jsonObject{
			"id": "xxx1234567890",
		}
	}

	mockHandlers["GET /api/v0/hosts/xxx1234567890"] = func(req *http.Request) (int, jsonObject) {
		return 200, jsonObject{
			"host": mackerel.Host{
				ID:     "xxx1234567890",
				Name:   "host.example.com",
				Type:   "unknown",
				Status: "standby",
			},
		}
	}

	c, _ := Prepare(&conf)
	api := c.API
	host := c.Host

	if api.BaseURL.String() != ts.URL {
		t.Errorf("Apibase mismatch: %s != %s", api.BaseURL, ts.URL)
	}

	if host.ID != "xxx1234567890" {
		t.Error("Host ID mismatch", host)
	}

	if host.Name != "host.example.com" {
		t.Error("Host name mismatch", host)
	}
}

func TestPrepareWithCreateWithFail(t *testing.T) {
	conf, mockHandlers, ts := newMockAPIServer(t)
	defer ts.Close()

	mockHandlers["POST /api/v0/hosts"] = func(req *http.Request) (int, jsonObject) {
		return 403, jsonObject{
			"result": "error",
		}
	}

	origRetryNum := retryNum
	retryNum = 1
	defer func() {
		retryNum = origRetryNum
	}()

	_, err := Prepare(&conf)

	if err == nil {
		t.Errorf("error should be occurred")
	}
}

func TestPrepareWithUpdate(t *testing.T) {
	conf, mockHandlers, ts := newMockAPIServer(t)
	defer ts.Close()
	tempDir, _ := ioutil.TempDir("", "")
	conf.Root = tempDir
	conf.SaveHostID("xxx12345678901")

	mockHandlers["PUT /api/v0/hosts/xxx12345678901"] = func(req *http.Request) (int, jsonObject) {
		return 200, jsonObject{
			"result": "OK",
		}
	}

	mockHandlers["GET /api/v0/hosts/xxx12345678901"] = func(req *http.Request) (int, jsonObject) {
		return 200, jsonObject{
			"host": mackerel.Host{
				ID:     "xxx12345678901",
				Name:   "host.example.com",
				Type:   "unknown",
				Status: "standby",
			},
		}
	}

	c, _ := Prepare(&conf)
	api := c.API
	host := c.Host

	if api.BaseURL.String() != ts.URL {
		t.Errorf("Apibase mismatch: %s != %s", api.BaseURL, ts.URL)
	}

	if host.ID != "xxx12345678901" {
		t.Error("Host ID mismatch", host)
	}

	if host.Name != "host.example.com" {
		t.Error("Host name mismatch", host)
	}
}

func TestCollectHostSpecs(t *testing.T) {
	hostname, meta, _ /*interfaces*/, _ /*customIdentifier*/, err := collectHostSpecs()

	if err != nil {
		t.Errorf("collectHostSpecs should not fail: %s", err)
	}

	if hostname == "" {
		t.Error("hostname should not be empty")
	}

	if _, ok := meta["cpu"]; !ok {
		t.Error("meta.cpu should exist")
	}
}

type counterGenerator struct {
	counter int
	sync.Mutex
}

func (g *counterGenerator) Generate() (metrics.Values, error) {
	g.Lock()
	defer g.Unlock()
	g.counter = g.counter + 1
	return map[string]float64{"dummy.a": float64(g.counter)}, nil
}

type byTime []mackerel.CreatingMetricsValue

func (b byTime) Len() int           { return len(b) }
func (b byTime) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
func (b byTime) Less(i, j int) bool { return b[i].Time < b[j].Time }

func TestLoop(t *testing.T) {
	if testing.Verbose() {
		logging.SetLogLevel(logging.DEBUG)
	}

	conf, mockHandlers, ts := newMockAPIServer(t)
	defer ts.Close()

	if testing.Short() {
		// Shrink time scale
		originalPostMetricsInterval := config.PostMetricsInterval

		config.PostMetricsInterval = 10 * time.Second
		ratio := config.PostMetricsInterval.Seconds() / originalPostMetricsInterval.Seconds()

		conf.Connection.PostMetricsDequeueDelaySeconds =
			int(float64(config.DefaultConfig.Connection.PostMetricsRetryDelaySeconds) * ratio)
		conf.Connection.PostMetricsRetryDelaySeconds =
			int(float64(config.DefaultConfig.Connection.PostMetricsRetryDelaySeconds) * ratio)

		defer func() {
			config.PostMetricsInterval = originalPostMetricsInterval
		}()
	}

	/// Simulate the situation that mackerel.io is down for 3 min
	// Strategy:
	// counterGenerator generates values 1,2,3,4,...
	// when we got value 3, the server will start responding 503 for three times (inclusive)
	// so the agent should queue the generated values and retry sending.
	//
	//  status: o . o . x . x . x . o o o o o
	//    send: 1 . 2 . 3 . 3 . 3 . 3 4 5 6 7
	// collect: 1 . 2 . 3 . 4 . 5 . 6 . 7 . 8
	//           ^
	//           30s
	const (
		totalFailures = 3
		totalPosts    = 7
	)
	failureCount := 0
	receivedDataPoints := []mackerel.CreatingMetricsValue{}
	done := make(chan struct{})

	mockHandlers["POST /api/v0/tsdb"] = func(req *http.Request) (int, jsonObject) {
		payload := []mackerel.CreatingMetricsValue{}
		json.NewDecoder(req.Body).Decode(&payload)

		for _, p := range payload {
			value := p.Value.(float64)
			if value == 3 {
				failureCount++
				if failureCount <= totalFailures {
					return 503, jsonObject{
						"failure": failureCount, // just for DEBUG logging
					}
				}
			}

			if value == totalPosts {
				defer func() { done <- struct{}{} }()
			}
		}

		receivedDataPoints = append(receivedDataPoints, payload...)

		return 200, jsonObject{
			"success": true,
		}
	}
	mockHandlers["PUT /api/v0/hosts/xyzabc12345"] = func(req *http.Request) (int, jsonObject) {
		return 200, jsonObject{
			"result": "OK",
		}
	}

	// Prepare required objects...
	ag := &agent.Agent{
		MetricsGenerators: []metrics.Generator{
			&counterGenerator{},
		},
	}

	api, err := mackerel.NewAPI(conf.Apibase, conf.Apikey, true)
	if err != nil {
		t.Fatal(err)
	}

	host := &mackerel.Host{ID: "xyzabc12345"}

	termCh := make(chan struct{})
	exitCh := make(chan error)
	c := &App{
		Agent:  ag,
		Config: &conf,
		API:    api,
		Host:   host,
	}
	// Start looping!
	go func() {
		exitCh <- loop(c, termCh)
	}()

	<-done

	// Verify results
	if len(receivedDataPoints) != totalPosts {
		t.Errorf("the agent should have sent %d datapoints, got: %+v", totalPosts, receivedDataPoints)
	}

	sort.Sort(byTime(receivedDataPoints))

	for i := 0; i < totalPosts; i++ {
		value := receivedDataPoints[i].Value.(float64)
		if value != float64(i+1) {
			t.Errorf("the %dth datapoint should have value %d, got: %+v", i, i+1, receivedDataPoints)
		}
	}

	termCh <- struct{}{}
	exitErr := <-exitCh
	if exitErr != nil {
		t.Errorf("exitErr should be nil, got: %s", exitErr)
	}
}
<|endoftext|>