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
116
mcfg/cli_test.go
116
mcfg/cli_test.go
@ -2,14 +2,11 @@ package mcfg
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
. "testing"
|
. "testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/mrand"
|
"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/mediocregopher/mediocre-go-lib/mtest/mchk"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -46,105 +43,35 @@ func TestSourceCLIHelp(t *T) {
|
|||||||
|
|
||||||
func TestSourceCLI(t *T) {
|
func TestSourceCLI(t *T) {
|
||||||
type state struct {
|
type state struct {
|
||||||
cfg *Cfg
|
srcCommonState
|
||||||
availCfgs []*Cfg
|
|
||||||
|
|
||||||
SourceCLI
|
SourceCLI
|
||||||
expPVs []ParamValue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type params struct {
|
type params struct {
|
||||||
name string
|
srcCommonParams
|
||||||
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
|
nonBoolWEq bool // use equal sign when setting value
|
||||||
nonBoolVal string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
chk := mchk.Checker{
|
chk := mchk.Checker{
|
||||||
Init: func() mchk.State {
|
Init: func() mchk.State {
|
||||||
var s state
|
var s state
|
||||||
s.cfg = New()
|
s.srcCommonState = newSrcCommonState()
|
||||||
{
|
|
||||||
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.SourceCLI.Args = make([]string, 0, 16)
|
s.SourceCLI.Args = make([]string, 0, 16)
|
||||||
return s
|
return s
|
||||||
},
|
},
|
||||||
Next: func(ss mchk.State) mchk.Action {
|
Next: func(ss mchk.State) mchk.Action {
|
||||||
s := ss.(state)
|
s := ss.(state)
|
||||||
var p params
|
var p params
|
||||||
if i := mrand.Intn(8); i == 0 {
|
p.srcCommonParams = s.srcCommonState.next()
|
||||||
p.name = mrand.Hex(1) + "-" + mrand.Hex(8)
|
// if the param is a bool or unset this won't get used, but w/e
|
||||||
} 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.nonBoolWEq = mrand.Intn(2) == 0
|
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}
|
return mchk.Action{Params: p}
|
||||||
},
|
},
|
||||||
Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) {
|
Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) {
|
||||||
s := ss.(state)
|
s := ss.(state)
|
||||||
p := a.Params.(params)
|
p := a.Params.(params)
|
||||||
|
|
||||||
// the param needs to get added to its cfg as a Param
|
s.srcCommonState = s.srcCommonState.applyCfgAndPV(p.srcCommonParams)
|
||||||
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
|
|
||||||
if !p.unset {
|
if !p.unset {
|
||||||
arg := cliKeyPrefix
|
arg := cliKeyPrefix
|
||||||
if len(p.path) > 0 {
|
if len(p.path) > 0 {
|
||||||
@ -161,39 +88,14 @@ func TestSourceCLI(t *T) {
|
|||||||
arg += p.nonBoolVal
|
arg += p.nonBoolVal
|
||||||
}
|
}
|
||||||
s.SourceCLI.Args = append(s.SourceCLI.Args, arg)
|
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
|
err := s.srcCommonState.assert(s.SourceCLI)
|
||||||
gotPVs, err := s.SourceCLI.Parse(s.cfg)
|
return s, err
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return s, massert.All(
|
|
||||||
massert.Len(gotPVs, len(s.expPVs)),
|
|
||||||
massert.Subset(s.expPVs, gotPVs),
|
|
||||||
).Assert()
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := chk.RunFor(5 * time.Second); err != nil {
|
if err := chk.RunFor(2 * time.Second); err != nil {
|
||||||
t.Fatal(err)
|
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