From 1964add0eda05be816f88c7f587a74ad032554d9 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Mon, 13 Aug 2018 14:17:53 -0400 Subject: [PATCH] mcfg: refactor cli test to use mtest.Checker --- mcfg/cli_test.go | 434 ++++++++++++++++++----------------------------- 1 file changed, 161 insertions(+), 273 deletions(-) diff --git a/mcfg/cli_test.go b/mcfg/cli_test.go index cf9f27d..f4b88e5 100644 --- a/mcfg/cli_test.go +++ b/mcfg/cli_test.go @@ -3,286 +3,19 @@ package mcfg import ( "bytes" "encoding/json" - "strconv" + "fmt" + "log" + "strings" . "testing" + "time" "github.com/mediocregopher/mediocre-go-lib/mrand" + "github.com/mediocregopher/mediocre-go-lib/mtest" + "github.com/mediocregopher/mediocre-go-lib/mtest/massert" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// * dimension -// - dimension value -// -// * Cfg path -// - New() -// - New.Child("a") -// - New.Child("a-b") -// - New.Child("a=b") -// * Param name -// - normal -// - w/ "-" -// - w/ "=" ? -// * Param type -// - bool -// - non-bool -// * non-bool type -// - int -// - string -// * Str value -// - empty -// - normal -// - w/ - -// - w/ = -// * Value format -// - w/ = -// - w/o = - -func combinate(slices ...[]string) [][]string { - out := [][]string{{}} - for _, slice := range slices { - if len(slice) == 0 { - continue - } - prev := out - out = make([][]string, 0, len(prev)*len(slice)) - for _, prevSet := range prev { - for _, sliceElem := range slice { - prevSetCp := make([]string, len(prevSet), len(prevSet)+1) - copy(prevSetCp, prevSet) - prevSetCp = append(prevSetCp, sliceElem) - out = append(out, prevSetCp) - } - } - } - return out -} - -func TestCombinate(t *T) { - type combTest struct { - args [][]string - exp [][]string - } - - tests := []combTest{ - { - args: [][]string{}, - exp: [][]string{{}}, - }, - { - args: [][]string{{"a"}}, - exp: [][]string{{"a"}}, - }, - { - args: [][]string{{"a"}, {"b"}}, - exp: [][]string{{"a", "b"}}, - }, - { - args: [][]string{{"a", "aa"}, {"b"}}, - exp: [][]string{ - {"a", "b"}, - {"aa", "b"}, - }, - }, - { - args: [][]string{{"a"}, {"b", "bb"}}, - exp: [][]string{ - {"a", "b"}, - {"a", "bb"}, - }, - }, - { - args: [][]string{{"a", "aa"}, {"b", "bb"}}, - exp: [][]string{ - {"a", "b"}, - {"aa", "b"}, - {"a", "bb"}, - {"aa", "bb"}, - }, - }, - } - - for i, test := range tests { - msgAndArgs := []interface{}{"test:%d args:%v", i, test.args} - got := combinate(test.args...) - assert.Len(t, got, len(test.exp), msgAndArgs...) - for _, expHas := range test.exp { - assert.Contains(t, got, expHas, msgAndArgs...) - } - } -} - -func TestSourceCLI(t *T) { - var ( - paths = []string{ - "root", - "child", - "childDash", - "childEq", - } - - paramNames = []string{ - "normal", - "wDash", - "wEq", - } - - isBool = []string{ - "isBool", - "isNotBool", - } - - nonBoolTypes = []string{ - "int", - "str", - } - - nonBoolFmts = []string{ - "wEq", - "woEq", - } - - nonBoolStrValues = []string{ - "empty", - "normal", - "wDash", - "wEq", - } - ) - - type cliTest struct { - path string - name string - isBool bool - nonBoolType string - nonBoolStrValue string - nonBoolFmt string - - // it's kinda hacky to make this a pointer, but it makes the code a lot - // easier to read later - exp *ParamValue - } - - var tests []cliTest - for _, comb := range combinate(paths, paramNames, isBool) { - var test cliTest - test.path = comb[0] - test.name = comb[1] - test.isBool = comb[2] == "isBool" - if test.isBool { - tests = append(tests, test) - continue - } - - for _, nonBoolComb := range combinate(nonBoolTypes, nonBoolFmts) { - test.nonBoolType = nonBoolComb[0] - test.nonBoolFmt = nonBoolComb[1] - if test.nonBoolType != "str" { - tests = append(tests, test) - continue - } - for _, nonBoolStrValue := range nonBoolStrValues { - test.nonBoolStrValue = nonBoolStrValue - tests = append(tests, test) - } - } - } - - childName := mrand.Hex(8) - childDashName := mrand.Hex(4) + "-" + mrand.Hex(4) - childEqName := mrand.Hex(4) + "=" + mrand.Hex(4) - - var args []string - rootCfg := New() - childCfg := rootCfg.Child(childName) - childDashCfg := rootCfg.Child(childDashName) - childEqCfg := rootCfg.Child(childEqName) - - for i := range tests { - var pv ParamValue - tests[i].exp = &pv - - switch tests[i].name { - case "normal": - pv.Name = mrand.Hex(8) - case "wDash": - pv.Name = mrand.Hex(4) + "-" + mrand.Hex(4) - case "wEq": - pv.Name = mrand.Hex(4) + "=" + mrand.Hex(4) - } - - pv.IsBool = tests[i].isBool - pv.IsString = !tests[i].isBool && tests[i].nonBoolType == "str" - - var arg string - switch tests[i].path { - case "root": - rootCfg.ParamAdd(pv.Param) - arg = "--" + pv.Name - case "child": - childCfg.ParamAdd(pv.Param) - pv.Path = append(pv.Path, childName) - arg = "--" + childName + "-" + pv.Name - case "childDash": - childDashCfg.ParamAdd(pv.Param) - pv.Path = append(pv.Path, childDashName) - arg = "--" + childDashName + "-" + pv.Name - case "childEq": - childEqCfg.ParamAdd(pv.Param) - pv.Path = append(pv.Path, childEqName) - arg = "--" + childEqName + "-" + pv.Name - } - - if pv.IsBool { - pv.Value = json.RawMessage("true") - args = append(args, arg) - continue - } - - var val string - switch tests[i].nonBoolType { - case "int": - val = strconv.Itoa(mrand.Int()) - pv.Value = json.RawMessage(val) - case "str": - switch tests[i].nonBoolStrValue { - case "empty": - // ez - case "normal": - val = mrand.Hex(8) - case "wDash": - val = mrand.Hex(4) + "-" + mrand.Hex(4) - case "wEq": - val = mrand.Hex(4) + "=" + mrand.Hex(4) - } - pv.Value = json.RawMessage(`"` + val + `"`) - } - - switch tests[i].nonBoolFmt { - case "wEq": - arg += "=" + val - args = append(args, arg) - case "woEq": - args = append(args, arg, val) - } - } - - src := SourceCLI{Args: args} - pvals, err := src.Parse(rootCfg) - require.NoError(t, err) - assert.Len(t, pvals, len(tests)) - - for _, test := range tests { - assert.Contains(t, pvals, *test.exp) - } - - // an extra bogus param or value should generate an error - src = SourceCLI{Args: append(args, "foo")} - _, err = src.Parse(rootCfg) - require.Error(t, err) - -} - func TestSourceCLIHelp(t *T) { cfg := New() cfg.ParamInt("foo", 5, "Test int param") @@ -311,3 +44,158 @@ func TestSourceCLIHelp(t *T) { ` assert.Equal(t, exp, buf.String()) } + +type testCLIState struct { + cfg *Cfg + availCfgs []*Cfg + + SourceCLI + expPVs []ParamValue +} + +type testCLIApplyer 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 +} + +func (tca testCLIApplyer) Apply(ss mtest.State) (mtest.State, error) { + s := ss.(testCLIState) + + // the tca needs to get added to its cfg as a Param + thisCfg := s.availCfgs[tca.availCfgI] + p := Param{ + Name: tca.name, + IsString: tca.nonBoolType == "str" || tca.nonBoolType == "duration", + IsBool: tca.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(p) + + // if the arg is set then add it to the cli args and the expected output pvs + if !tca.unset { + arg := cliKeyPrefix + if len(tca.path) > 0 { + arg += strings.Join(tca.path, cliKeyJoin) + cliKeyJoin + } + arg += tca.name + if !tca.isBool { + if tca.nonBoolWEq { + arg += "=" + } else { + s.SourceCLI.Args = append(s.SourceCLI.Args, arg) + arg = "" + } + arg += tca.nonBoolVal + } + s.SourceCLI.Args = append(s.SourceCLI.Args, arg) + log.Print(strings.Join(s.SourceCLI.Args, " ")) + + pv := ParamValue{ + Param: p, + Path: tca.path, + } + if tca.isBool { + pv.Value = json.RawMessage("true") + } else { + switch tca.nonBoolType { + case "str", "duration": + pv.Value = json.RawMessage(fmt.Sprintf("%q", tca.nonBoolVal)) + case "int", "json": + pv.Value = json.RawMessage(tca.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() +} + +func TestSourceCLI(t *T) { + chk := mtest.Checker{ + Init: func() mtest.State { + var s testCLIState + 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.SourceCLI.Args = make([]string, 0, 16) + return s + }, + Actions: func(ss mtest.State) []mtest.Action { + s := ss.(testCLIState) + var tca testCLIApplyer + if i := mrand.Intn(8); i == 0 { + tca.name = mrand.Hex(1) + "-" + mrand.Hex(8) + } else if i == 1 { + tca.name = mrand.Hex(1) + "=" + mrand.Hex(8) + } else { + tca.name = mrand.Hex(8) + } + + tca.availCfgI = mrand.Intn(len(s.availCfgs)) + thisCfg := s.availCfgs[tca.availCfgI] + tca.path = thisCfg.Path + + tca.isBool = mrand.Intn(2) == 0 + if !tca.isBool { + tca.nonBoolType = mrand.Element([]string{ + "int", + "str", + "duration", + "json", + }, nil).(string) + } + tca.unset = mrand.Intn(10) == 0 + + if tca.isBool || tca.unset { + return []mtest.Action{{Applyer: tca}} + } + + tca.nonBoolWEq = mrand.Intn(2) == 0 + switch tca.nonBoolType { + case "int": + tca.nonBoolVal = fmt.Sprint(mrand.Int()) + case "str": + tca.nonBoolVal = mrand.Hex(16) + case "duration": + dur := time.Duration(mrand.Intn(86400)) * time.Second + tca.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(), + }) + tca.nonBoolVal = string(b) + } + return []mtest.Action{{Applyer: tca}} + }, + } + + if err := chk.RunUntil(5 * time.Second); err != nil { + t.Fatal(err) + } +}