mcfg: implement SourceEnv, and move a bunch of code it shares with SourceCLI to source(_test).go
This commit is contained in:
parent
526e35cf3f
commit
2e9790451f
118
mcfg/cli_test.go
118
mcfg/cli_test.go
@ -2,14 +2,11 @@ package mcfg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
. "testing"
|
||||
"time"
|
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/mchk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -46,105 +43,35 @@ func TestSourceCLIHelp(t *T) {
|
||||
|
||||
func TestSourceCLI(t *T) {
|
||||
type state struct {
|
||||
cfg *Cfg
|
||||
availCfgs []*Cfg
|
||||
|
||||
srcCommonState
|
||||
SourceCLI
|
||||
expPVs []ParamValue
|
||||
}
|
||||
|
||||
type params struct {
|
||||
name string
|
||||
availCfgI int // not technically needed, but makes subsequent steps easier
|
||||
path []string
|
||||
isBool bool
|
||||
nonBoolType string // "int", "str", "duration", "json"
|
||||
unset bool
|
||||
nonBoolWEq bool // use equal sign when setting value
|
||||
nonBoolVal string
|
||||
srcCommonParams
|
||||
nonBoolWEq bool // use equal sign when setting value
|
||||
}
|
||||
|
||||
chk := mchk.Checker{
|
||||
Init: func() mchk.State {
|
||||
var s state
|
||||
s.cfg = New()
|
||||
{
|
||||
a := s.cfg.Child("a")
|
||||
b := s.cfg.Child("b")
|
||||
c := s.cfg.Child("c")
|
||||
ab := a.Child("b")
|
||||
bc := b.Child("c")
|
||||
abc := ab.Child("c")
|
||||
s.availCfgs = []*Cfg{s.cfg, a, b, c, ab, bc, abc}
|
||||
}
|
||||
s.srcCommonState = newSrcCommonState()
|
||||
s.SourceCLI.Args = make([]string, 0, 16)
|
||||
return s
|
||||
},
|
||||
Next: func(ss mchk.State) mchk.Action {
|
||||
s := ss.(state)
|
||||
var p params
|
||||
if i := mrand.Intn(8); i == 0 {
|
||||
p.name = mrand.Hex(1) + "-" + mrand.Hex(8)
|
||||
} else if i == 1 {
|
||||
p.name = mrand.Hex(1) + "=" + mrand.Hex(8)
|
||||
} else {
|
||||
p.name = mrand.Hex(8)
|
||||
}
|
||||
|
||||
p.availCfgI = mrand.Intn(len(s.availCfgs))
|
||||
thisCfg := s.availCfgs[p.availCfgI]
|
||||
p.path = thisCfg.Path
|
||||
|
||||
p.isBool = mrand.Intn(2) == 0
|
||||
if !p.isBool {
|
||||
p.nonBoolType = mrand.Element([]string{
|
||||
"int",
|
||||
"str",
|
||||
"duration",
|
||||
"json",
|
||||
}, nil).(string)
|
||||
}
|
||||
p.unset = mrand.Intn(10) == 0
|
||||
|
||||
if p.isBool || p.unset {
|
||||
return mchk.Action{Params: p}
|
||||
}
|
||||
|
||||
p.srcCommonParams = s.srcCommonState.next()
|
||||
// if the param is a bool or unset this won't get used, but w/e
|
||||
p.nonBoolWEq = mrand.Intn(2) == 0
|
||||
switch p.nonBoolType {
|
||||
case "int":
|
||||
p.nonBoolVal = fmt.Sprint(mrand.Int())
|
||||
case "str":
|
||||
p.nonBoolVal = mrand.Hex(16)
|
||||
case "duration":
|
||||
dur := time.Duration(mrand.Intn(86400)) * time.Second
|
||||
p.nonBoolVal = dur.String()
|
||||
case "json":
|
||||
b, _ := json.Marshal(map[string]int{
|
||||
mrand.Hex(4): mrand.Int(),
|
||||
mrand.Hex(4): mrand.Int(),
|
||||
mrand.Hex(4): mrand.Int(),
|
||||
})
|
||||
p.nonBoolVal = string(b)
|
||||
}
|
||||
return mchk.Action{Params: p}
|
||||
},
|
||||
Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) {
|
||||
s := ss.(state)
|
||||
p := a.Params.(params)
|
||||
|
||||
// the param needs to get added to its cfg as a Param
|
||||
thisCfg := s.availCfgs[p.availCfgI]
|
||||
cfgP := Param{
|
||||
Name: p.name,
|
||||
IsString: p.nonBoolType == "str" || p.nonBoolType == "duration",
|
||||
IsBool: p.isBool,
|
||||
// the cli parser doesn't actually care about the other fields of Param,
|
||||
// those are only used by Cfg once it has all ParamValues together
|
||||
}
|
||||
thisCfg.ParamAdd(cfgP)
|
||||
|
||||
// if the arg is set then add it to the cli args and the expected output pvs
|
||||
s.srcCommonState = s.srcCommonState.applyCfgAndPV(p.srcCommonParams)
|
||||
if !p.unset {
|
||||
arg := cliKeyPrefix
|
||||
if len(p.path) > 0 {
|
||||
@ -161,39 +88,14 @@ func TestSourceCLI(t *T) {
|
||||
arg += p.nonBoolVal
|
||||
}
|
||||
s.SourceCLI.Args = append(s.SourceCLI.Args, arg)
|
||||
|
||||
pv := ParamValue{
|
||||
Param: cfgP,
|
||||
Path: p.path,
|
||||
}
|
||||
if p.isBool {
|
||||
pv.Value = json.RawMessage("true")
|
||||
} else {
|
||||
switch p.nonBoolType {
|
||||
case "str", "duration":
|
||||
pv.Value = json.RawMessage(fmt.Sprintf("%q", p.nonBoolVal))
|
||||
case "int", "json":
|
||||
pv.Value = json.RawMessage(p.nonBoolVal)
|
||||
default:
|
||||
panic("shouldn't get here")
|
||||
}
|
||||
}
|
||||
s.expPVs = append(s.expPVs, pv)
|
||||
}
|
||||
|
||||
// and finally the state needs to be checked
|
||||
gotPVs, err := s.SourceCLI.Parse(s.cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, massert.All(
|
||||
massert.Len(gotPVs, len(s.expPVs)),
|
||||
massert.Subset(s.expPVs, gotPVs),
|
||||
).Assert()
|
||||
err := s.srcCommonState.assert(s.SourceCLI)
|
||||
return s, err
|
||||
},
|
||||
}
|
||||
|
||||
if err := chk.RunFor(5 * time.Second); err != nil {
|
||||
if err := chk.RunFor(2 * time.Second); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
78
mcfg/env.go
Normal file
78
mcfg/env.go
Normal file
@ -0,0 +1,78 @@
|
||||
package mcfg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SourceEnv is a Source which will parse configuration from the process
|
||||
// environment.
|
||||
//
|
||||
// Possible Env options are generated by joining a Param's Path and Name with
|
||||
// underscores and making all characters uppercase, as well as changing all
|
||||
// dashes to underscores.
|
||||
//
|
||||
// cfg := mcfg.New().Child("foo").Child("bar")
|
||||
// addr := cfg.ParamString("srv-addr", "", "Some address")
|
||||
// // the Env option to fill addr will be "FOO_BAR_SRV_ADDR"
|
||||
//
|
||||
type SourceEnv struct {
|
||||
Env []string // in the format key=value
|
||||
|
||||
// If set then all expected Env options must be prefixed with this string,
|
||||
// which will be uppercased and have dashes replaced with underscores like
|
||||
// all the other parts of the option names.
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func (env SourceEnv) expectedName(path []string, name string) string {
|
||||
out := strings.Join(append(path, name), "_")
|
||||
if env.Prefix != "" {
|
||||
out = env.Prefix + "_" + out
|
||||
}
|
||||
out = strings.Replace(out, "-", "_", -1)
|
||||
out = strings.ToUpper(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Parse implements the method for the Source interface
|
||||
func (env SourceEnv) Parse(cfg *Cfg) ([]ParamValue, error) {
|
||||
kvs := env.Env
|
||||
if kvs == nil {
|
||||
kvs = os.Environ()
|
||||
}
|
||||
|
||||
pvM := map[string]ParamValue{}
|
||||
for _, pv := range cfg.allParamValues() {
|
||||
name := env.expectedName(pv.Path, pv.Name)
|
||||
pvM[name] = pv
|
||||
}
|
||||
|
||||
pvs := make([]ParamValue, 0, len(kvs))
|
||||
for _, kv := range kvs {
|
||||
split := strings.SplitN(kv, "=", 2)
|
||||
if len(split) != 2 {
|
||||
return nil, fmt.Errorf("malformed environment kv %q", kv)
|
||||
}
|
||||
k, v := split[0], split[1]
|
||||
if pv, ok := pvM[k]; ok {
|
||||
if pv.IsBool {
|
||||
if v == "" {
|
||||
pv.Value = json.RawMessage("false")
|
||||
} else {
|
||||
pv.Value = json.RawMessage("true")
|
||||
}
|
||||
} else if pv.IsString && (v == "" || v[0] != '"') {
|
||||
pv.Value = json.RawMessage(`"` + v + `"`)
|
||||
|
||||
} else {
|
||||
pv.Value = json.RawMessage(v)
|
||||
}
|
||||
pvs = append(pvs, pv)
|
||||
}
|
||||
}
|
||||
|
||||
return pvs, nil
|
||||
}
|
58
mcfg/env_test.go
Normal file
58
mcfg/env_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
package mcfg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
. "testing"
|
||||
"time"
|
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/mchk"
|
||||
)
|
||||
|
||||
func TestSourceEnv(t *T) {
|
||||
type state struct {
|
||||
srcCommonState
|
||||
SourceEnv
|
||||
}
|
||||
|
||||
type params struct {
|
||||
srcCommonParams
|
||||
}
|
||||
|
||||
chk := mchk.Checker{
|
||||
Init: func() mchk.State {
|
||||
var s state
|
||||
s.srcCommonState = newSrcCommonState()
|
||||
s.SourceEnv.Env = make([]string, 0, 16)
|
||||
return s
|
||||
},
|
||||
Next: func(ss mchk.State) mchk.Action {
|
||||
s := ss.(state)
|
||||
var p params
|
||||
p.srcCommonParams = s.srcCommonState.next()
|
||||
return mchk.Action{Params: p}
|
||||
},
|
||||
Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) {
|
||||
s := ss.(state)
|
||||
p := a.Params.(params)
|
||||
s.srcCommonState = s.srcCommonState.applyCfgAndPV(p.srcCommonParams)
|
||||
if !p.unset {
|
||||
kv := strings.Join(append(p.path, p.name), "_")
|
||||
kv = strings.Replace(kv, "-", "_", -1)
|
||||
kv = strings.ToUpper(kv)
|
||||
kv += "="
|
||||
if p.isBool {
|
||||
kv += "1"
|
||||
} else {
|
||||
kv += p.nonBoolVal
|
||||
}
|
||||
s.SourceEnv.Env = append(s.SourceEnv.Env, kv)
|
||||
}
|
||||
err := s.srcCommonState.assert(s.SourceEnv)
|
||||
return s, err
|
||||
},
|
||||
}
|
||||
|
||||
if err := chk.RunFor(2 * time.Second); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
142
mcfg/source_test.go
Normal file
142
mcfg/source_test.go
Normal file
@ -0,0 +1,142 @@
|
||||
package mcfg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
|
||||
)
|
||||
|
||||
// The tests for the different Sources use mchk as their primary method of
|
||||
// checking. They end up sharing a lot of the same functionality, so in here is
|
||||
// all the code they share
|
||||
|
||||
type srcCommonState struct {
|
||||
cfg *Cfg
|
||||
availCfgs []*Cfg
|
||||
expPVs []ParamValue
|
||||
|
||||
// each specific test should wrap this to add the Source itself
|
||||
}
|
||||
|
||||
func newSrcCommonState() srcCommonState {
|
||||
var scs srcCommonState
|
||||
scs.cfg = New()
|
||||
{
|
||||
a := scs.cfg.Child("a")
|
||||
b := scs.cfg.Child("b")
|
||||
c := scs.cfg.Child("c")
|
||||
ab := a.Child("b")
|
||||
bc := b.Child("c")
|
||||
abc := ab.Child("c")
|
||||
scs.availCfgs = []*Cfg{scs.cfg, a, b, c, ab, bc, abc}
|
||||
}
|
||||
return scs
|
||||
}
|
||||
|
||||
type srcCommonParams struct {
|
||||
name string
|
||||
availCfgI int // not technically needed, but finding the cfg easier
|
||||
path []string
|
||||
isBool bool
|
||||
nonBoolType string // "int", "str", "duration", "json"
|
||||
unset bool
|
||||
nonBoolVal string
|
||||
}
|
||||
|
||||
func (scs srcCommonState) next() srcCommonParams {
|
||||
var p srcCommonParams
|
||||
if i := mrand.Intn(8); i == 0 {
|
||||
p.name = mrand.Hex(1) + "-" + mrand.Hex(8)
|
||||
} else {
|
||||
p.name = mrand.Hex(8)
|
||||
}
|
||||
|
||||
p.availCfgI = mrand.Intn(len(scs.availCfgs))
|
||||
thisCfg := scs.availCfgs[p.availCfgI]
|
||||
p.path = thisCfg.Path
|
||||
|
||||
p.isBool = mrand.Intn(2) == 0
|
||||
if !p.isBool {
|
||||
p.nonBoolType = mrand.Element([]string{
|
||||
"int",
|
||||
"str",
|
||||
"duration",
|
||||
"json",
|
||||
}, nil).(string)
|
||||
}
|
||||
p.unset = mrand.Intn(10) == 0
|
||||
|
||||
if p.isBool || p.unset {
|
||||
return p
|
||||
}
|
||||
|
||||
switch p.nonBoolType {
|
||||
case "int":
|
||||
p.nonBoolVal = fmt.Sprint(mrand.Int())
|
||||
case "str":
|
||||
p.nonBoolVal = mrand.Hex(16)
|
||||
case "duration":
|
||||
dur := time.Duration(mrand.Intn(86400)) * time.Second
|
||||
p.nonBoolVal = dur.String()
|
||||
case "json":
|
||||
b, _ := json.Marshal(map[string]int{
|
||||
mrand.Hex(4): mrand.Int(),
|
||||
mrand.Hex(4): mrand.Int(),
|
||||
mrand.Hex(4): mrand.Int(),
|
||||
})
|
||||
p.nonBoolVal = string(b)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// adds the new cfg param to the cfg, and if the param is expected to be set in
|
||||
// the Source adds it to the expected ParamValues as well
|
||||
func (scs srcCommonState) applyCfgAndPV(p srcCommonParams) srcCommonState {
|
||||
thisCfg := scs.availCfgs[p.availCfgI]
|
||||
cfgP := Param{
|
||||
Name: p.name,
|
||||
IsString: p.nonBoolType == "str" || p.nonBoolType == "duration",
|
||||
IsBool: p.isBool,
|
||||
// the Sources don't actually care about the other fields of Param,
|
||||
// those are only used by Cfg once it has all ParamValues together
|
||||
}
|
||||
thisCfg.ParamAdd(cfgP)
|
||||
|
||||
if !p.unset {
|
||||
pv := ParamValue{
|
||||
Param: cfgP,
|
||||
Path: p.path,
|
||||
}
|
||||
if p.isBool {
|
||||
pv.Value = json.RawMessage("true")
|
||||
} else {
|
||||
switch p.nonBoolType {
|
||||
case "str", "duration":
|
||||
pv.Value = json.RawMessage(fmt.Sprintf("%q", p.nonBoolVal))
|
||||
case "int", "json":
|
||||
pv.Value = json.RawMessage(p.nonBoolVal)
|
||||
default:
|
||||
panic("shouldn't get here")
|
||||
}
|
||||
}
|
||||
scs.expPVs = append(scs.expPVs, pv)
|
||||
}
|
||||
|
||||
return scs
|
||||
}
|
||||
|
||||
// given a Source asserts that it's Parse method returns the expected
|
||||
// ParamValues
|
||||
func (scs srcCommonState) assert(s Source) error {
|
||||
gotPVs, err := s.Parse(scs.cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return massert.All(
|
||||
massert.Len(gotPVs, len(scs.expPVs)),
|
||||
massert.Subset(scs.expPVs, gotPVs),
|
||||
).Assert()
|
||||
}
|
Loading…
Reference in New Issue
Block a user