From 53b2c6474773579ce87ee3ed284d7c29ade8afa9 Mon Sep 17 00:00:00 2001 From: kenjones Date: Fri, 22 Sep 2017 15:58:12 -0400 Subject: [PATCH] Task: Increase test coverage, includes refactor Adds test cases to increase test coverage. Refactors code to enable adding tests by reducing the number of locations where `os.Exit()` is called from. --- commands_test.go | 305 +++++++++++++++++++++++++++++++++++++++++ data_navigator.go | 50 ++++--- data_navigator_test.go | 107 +++++++++++---- json_converter.go | 7 +- json_converter_test.go | 12 +- utils_test.go | 40 ++++++ yaml.go | 212 +++++++++++++++------------- yaml_test.go | 44 ++++-- 8 files changed, 620 insertions(+), 157 deletions(-) create mode 100644 commands_test.go diff --git a/commands_test.go b/commands_test.go new file mode 100644 index 00000000..6834a4ce --- /dev/null +++ b/commands_test.go @@ -0,0 +1,305 @@ +package main + +import ( + "fmt" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func getRootCommand() *cobra.Command { + return newCommandCLI() +} + +func TestRootCmd(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "") + if result.Error != nil { + t.Error(result.Error) + } + + if !strings.Contains(result.Output, "Usage:") { + t.Error("Expected usage message to be printed out, but the usage message was not found.") + } + +} + +func TestRootCmd_VerboseLong(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "--verbose") + if result.Error != nil { + t.Error(result.Error) + } + + if !verbose { + t.Error("Expected verbose to be true") + } +} + +func TestRootCmd_VerboseShort(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "-v") + if result.Error != nil { + t.Error(result.Error) + } + + if !verbose { + t.Error("Expected verbose to be true") + } +} + +func TestRootCmd_TrimLong(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "--trim") + if result.Error != nil { + t.Error(result.Error) + } + + if !trimOutput { + t.Error("Expected trimOutput to be true") + } +} + +func TestRootCmd_TrimShort(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "-t") + if result.Error != nil { + t.Error(result.Error) + } + + if !trimOutput { + t.Error("Expected trimOutput to be true") + } +} + +func TestRootCmd_ToJsonLong(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "--tojson") + if result.Error != nil { + t.Error(result.Error) + } + + if !outputToJSON { + t.Error("Expected outputToJSON to be true") + } +} + +func TestRootCmd_ToJsonShort(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "-j") + if result.Error != nil { + t.Error(result.Error) + } + + if !outputToJSON { + t.Error("Expected outputToJSON to be true") + } +} + +func TestReadCmd(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "read examples/sample.yaml b.c") + if result.Error != nil { + t.Error(result.Error) + } + assertResult(t, "2\n", result.Output) +} + +func TestReadCmd_Error(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "read") + if result.Error == nil { + t.Error("Expected command to fail due to missing arg") + } + expectedOutput := `Must provide filename` + assertResult(t, expectedOutput, result.Error.Error()) +} + +func TestReadCmd_ErrorEmptyFilename(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "read ") + if result.Error == nil { + t.Error("Expected command to fail due to missing arg") + } + expectedOutput := `Must provide filename` + assertResult(t, expectedOutput, result.Error.Error()) +} + +func TestReadCmd_ErrorUnreadableFile(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "read fake-unknown") + if result.Error == nil { + t.Error("Expected command to fail due to unknown file") + } + expectedOutput := `open fake-unknown: no such file or directory` + assertResult(t, expectedOutput, result.Error.Error()) +} + +func TestReadCmd_ErrorBadPath(t *testing.T) { + content := `b: + d: + e: + - 3 + - 4 + f: + - 1 + - 2 +` + filename := writeTempYamlFile(content) + defer removeTempYamlFile(filename) + + cmd := getRootCommand() + result := runCmd(cmd, fmt.Sprintf("read %s b.d.*.[x]", filename)) + if result.Error == nil { + t.Fatal("Expected command to fail due to invalid path") + } + expectedOutput := `Error accessing array: strconv.ParseInt: parsing "x": invalid syntax` + assertResult(t, expectedOutput, result.Error.Error()) +} + +func TestReadCmd_Verbose(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "-v read examples/sample.yaml b.c") + if result.Error != nil { + t.Error(result.Error) + } + assertResult(t, "2\n", result.Output) +} + +func TestReadCmd_NoTrim(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "--trim=false read examples/sample.yaml b.c") + if result.Error != nil { + t.Error(result.Error) + } + assertResult(t, "2\n\n", result.Output) +} + +func TestReadCmd_ToJson(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "-j read examples/sample.yaml b.c") + if result.Error != nil { + t.Error(result.Error) + } + assertResult(t, "2\n", result.Output) +} + +func TestNewCmd(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "new b.c 3") + if result.Error != nil { + t.Error(result.Error) + } + expectedOutput := `b: + c: 3 +` + assertResult(t, expectedOutput, result.Output) +} + +func TestNewCmd_Error(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "new b.c") + if result.Error == nil { + t.Error("Expected command to fail due to missing arg") + } + expectedOutput := `Must provide ` + assertResult(t, expectedOutput, result.Error.Error()) +} + +func TestNewCmd_Verbose(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "-v new b.c 3") + if result.Error != nil { + t.Error(result.Error) + } + expectedOutput := `b: + c: 3 +` + assertResult(t, expectedOutput, result.Output) +} + +func TestNewCmd_ToJson(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "-j new b.c 3") + if result.Error != nil { + t.Error(result.Error) + } + expectedOutput := `{"b":{"c":3}} +` + assertResult(t, expectedOutput, result.Output) +} + +func TestWriteCmd(t *testing.T) { + content := `b: + c: 3 +` + filename := writeTempYamlFile(content) + defer removeTempYamlFile(filename) + + cmd := getRootCommand() + result := runCmd(cmd, fmt.Sprintf("write %s b.c 7", filename)) + if result.Error != nil { + t.Error(result.Error) + } + expectedOutput := `b: + c: 7 +` + assertResult(t, expectedOutput, result.Output) +} + +func TestWriteCmd_Error(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "write") + if result.Error == nil { + t.Error("Expected command to fail due to missing arg") + } + expectedOutput := `Must provide ` + assertResult(t, expectedOutput, result.Error.Error()) +} + +func TestWriteCmd_ErrorUnreadableFile(t *testing.T) { + cmd := getRootCommand() + result := runCmd(cmd, "write fake-unknown a.b 3") + if result.Error == nil { + t.Error("Expected command to fail due to unknown file") + } + expectedOutput := `open fake-unknown: no such file or directory` + assertResult(t, expectedOutput, result.Error.Error()) +} + +func TestWriteCmd_Verbose(t *testing.T) { + content := `b: + c: 3 +` + filename := writeTempYamlFile(content) + defer removeTempYamlFile(filename) + + cmd := getRootCommand() + result := runCmd(cmd, fmt.Sprintf("-v write %s b.c 7", filename)) + if result.Error != nil { + t.Error(result.Error) + } + expectedOutput := `b: + c: 7 +` + assertResult(t, expectedOutput, result.Output) +} + +func TestWriteCmd_Inplace(t *testing.T) { + content := `b: + c: 3 +` + filename := writeTempYamlFile(content) + defer removeTempYamlFile(filename) + + cmd := getRootCommand() + result := runCmd(cmd, fmt.Sprintf("write -i %s b.c 7", filename)) + if result.Error != nil { + t.Error(result.Error) + } + gotOutput := readTempYamlFile(filename) + expectedOutput := `b: + c: 7` + assertResult(t, expectedOutput, gotOutput) +} diff --git a/data_navigator.go b/data_navigator.go index d6f28954..0fc8aa68 100644 --- a/data_navigator.go +++ b/data_navigator.go @@ -1,9 +1,10 @@ package main import ( + "fmt" "strconv" - yaml "gopkg.in/yaml.v2" + "gopkg.in/yaml.v2" ) func entryInSlice(context yaml.MapSlice, key interface{}) *yaml.MapItem { @@ -79,10 +80,12 @@ func writeArray(context interface{}, paths []string, value interface{}) []interf log.Debugf("\tarray %v\n", array) rawIndex := paths[0] - index, err := strconv.ParseInt(rawIndex, 10, 64) - if err != nil { - die("Error accessing array: %v", err) - } + index, _ := strconv.ParseInt(rawIndex, 10, 64) + // writeArray is only called by updatedChildValue which handles parsing the + // index, as such this renders this dead code. + // if err != nil { + // return array, fmt.Errorf("Error accessing array: %v", err) + // } for index >= int64(len(array)) { array = append(array, nil) } @@ -96,7 +99,7 @@ func writeArray(context interface{}, paths []string, value interface{}) []interf return array } -func readMap(context yaml.MapSlice, head string, tail []string) interface{} { +func readMap(context yaml.MapSlice, head string, tail []string) (interface{}, error) { if head == "*" { return readMapSplat(context, tail) } @@ -109,21 +112,25 @@ func readMap(context yaml.MapSlice, head string, tail []string) interface{} { return calculateValue(value, tail) } -func readMapSplat(context yaml.MapSlice, tail []string) interface{} { +func readMapSplat(context yaml.MapSlice, tail []string) (interface{}, error) { var newArray = make([]interface{}, len(context)) var i = 0 for _, entry := range context { if len(tail) > 0 { - newArray[i] = recurse(entry.Value, tail[0], tail[1:]) + val, err := recurse(entry.Value, tail[0], tail[1:]) + if err != nil { + return nil, err + } + newArray[i] = val } else { newArray[i] = entry.Value } i++ } - return newArray + return newArray, nil } -func recurse(value interface{}, head string, tail []string) interface{} { +func recurse(value interface{}, head string, tail []string) (interface{}, error) { switch value.(type) { case []interface{}: if head == "*" { @@ -131,37 +138,40 @@ func recurse(value interface{}, head string, tail []string) interface{} { } index, err := strconv.ParseInt(head, 10, 64) if err != nil { - die("Error accessing array: %v", err) + return nil, fmt.Errorf("Error accessing array: %v", err) } return readArray(value.([]interface{}), index, tail) case yaml.MapSlice: return readMap(value.(yaml.MapSlice), head, tail) default: - return nil + return nil, nil } } -func readArray(array []interface{}, head int64, tail []string) interface{} { +func readArray(array []interface{}, head int64, tail []string) (interface{}, error) { if head >= int64(len(array)) { - return nil + return nil, nil } value := array[head] - return calculateValue(value, tail) } -func readArraySplat(array []interface{}, tail []string) interface{} { +func readArraySplat(array []interface{}, tail []string) (interface{}, error) { var newArray = make([]interface{}, len(array)) for index, value := range array { - newArray[index] = calculateValue(value, tail) + val, err := calculateValue(value, tail) + if err != nil { + return nil, err + } + newArray[index] = val } - return newArray + return newArray, nil } -func calculateValue(value interface{}, tail []string) interface{} { +func calculateValue(value interface{}, tail []string) (interface{}, error) { if len(tail) > 0 { return recurse(value, tail[0], tail[1:]) } - return value + return value, nil } diff --git a/data_navigator_test.go b/data_navigator_test.go index 287d0d64..540c737f 100644 --- a/data_navigator_test.go +++ b/data_navigator_test.go @@ -2,27 +2,20 @@ package main import ( "fmt" - "os" "sort" "testing" - logging "github.com/op/go-logging" yaml "gopkg.in/yaml.v2" ) -func TestMain(m *testing.M) { - backend.SetLevel(logging.ERROR, "") - logging.SetBackend(backend) - os.Exit(m.Run()) -} - func TestReadMap_simple(t *testing.T) { var data = parseData(` --- b: c: 2 `) - assertResult(t, 2, readMap(data, "b", []string{"c"})) + got, _ := readMap(data, "b", []string{"c"}) + assertResult(t, 2, got) } func TestReadMap_splat(t *testing.T) { @@ -32,7 +25,8 @@ mapSplat: item1: things item2: whatever `) - var result = readMap(data, "mapSplat", []string{"*"}).([]interface{}) + res, _ := readMap(data, "mapSplat", []string{"*"}) + result := res.([]interface{}) var actual = []string{result[0].(string), result[1].(string)} sort.Strings(actual) assertResult(t, "[things whatever]", fmt.Sprintf("%v", actual)) @@ -48,7 +42,8 @@ mapSplatDeep: cats: apples `) - var result = readMap(data, "mapSplatDeep", []string{"*", "cats"}).([]interface{}) + res, _ := readMap(data, "mapSplatDeep", []string{"*", "cats"}) + result := res.([]interface{}) var actual = []string{result[0].(string), result[1].(string)} sort.Strings(actual) assertResult(t, "[apples bananas]", fmt.Sprintf("%v", actual)) @@ -60,7 +55,8 @@ func TestReadMap_key_doesnt_exist(t *testing.T) { b: c: 2 `) - assertResult(t, nil, readMap(data, "b.x.f", []string{"c"})) + got, _ := readMap(data, "b.x.f", []string{"c"}) + assertResult(t, nil, got) } func TestReadMap_recurse_against_string(t *testing.T) { @@ -68,7 +64,8 @@ func TestReadMap_recurse_against_string(t *testing.T) { --- a: cat `) - assertResult(t, nil, readMap(data, "a", []string{"b"})) + got, _ := readMap(data, "a", []string{"b"}) + assertResult(t, nil, got) } func TestReadMap_with_array(t *testing.T) { @@ -79,7 +76,64 @@ b: - 3 - 4 `) - assertResult(t, 4, readMap(data, "b", []string{"d", "1"})) + got, _ := readMap(data, "b", []string{"d", "1"}) + assertResult(t, 4, got) +} + +func TestReadMap_with_array_and_bad_index(t *testing.T) { + var data = parseData(` +--- +b: + d: + - 3 + - 4 +`) + _, err := readMap(data, "b", []string{"d", "x"}) + if err == nil { + t.Fatal("Expected error due to invalid path") + } + expectedOutput := `Error accessing array: strconv.ParseInt: parsing "x": invalid syntax` + assertResult(t, expectedOutput, err.Error()) +} + +func TestReadMap_with_mapsplat_array_and_bad_index(t *testing.T) { + var data = parseData(` +--- +b: + d: + e: + - 3 + - 4 + f: + - 1 + - 2 +`) + _, err := readMap(data, "b", []string{"d", "*", "x"}) + if err == nil { + t.Fatal("Expected error due to invalid path") + } + expectedOutput := `Error accessing array: strconv.ParseInt: parsing "x": invalid syntax` + assertResult(t, expectedOutput, err.Error()) +} + +func TestReadMap_with_arraysplat_map_array_and_bad_index(t *testing.T) { + var data = parseData(` +--- +b: + d: + - names: + - fred + - smith + - names: + - sam + - bo +`) + _, err := readMap(data, "b", []string{"d", "*", "names", "x"}) + if err == nil { + t.Fatal("Expected error due to invalid path") + } + expectedOutput := `Error accessing array: strconv.ParseInt: parsing "x": invalid syntax` + assertResult(t, expectedOutput, err.Error()) } func TestReadMap_with_array_out_of_bounds(t *testing.T) { @@ -90,7 +144,8 @@ b: - 3 - 4 `) - assertResult(t, nil, readMap(data, "b", []string{"d", "3"})) + got, _ := readMap(data, "b", []string{"d", "3"}) + assertResult(t, nil, got) } func TestReadMap_with_array_out_of_bounds_by_1(t *testing.T) { @@ -101,7 +156,8 @@ b: - 3 - 4 `) - assertResult(t, nil, readMap(data, "b", []string{"d", "2"})) + got, _ := readMap(data, "b", []string{"d", "2"}) + assertResult(t, nil, got) } func TestReadMap_with_array_splat(t *testing.T) { @@ -114,7 +170,8 @@ e: name: Sam thing: dog `) - assertResult(t, "[Fred Sam]", fmt.Sprintf("%v", readMap(data, "e", []string{"*", "name"}))) + got, _ := readMap(data, "e", []string{"*", "name"}) + assertResult(t, "[Fred Sam]", fmt.Sprintf("%v", got)) } func TestWrite_really_simple(t *testing.T) { @@ -158,7 +215,8 @@ b: `) updated := writeMap(data, []string{"b", "d", "f"}, "4") - assertResult(t, "4", readMap(updated, "b", []string{"d", "f"})) + got, _ := readMap(updated, "b", []string{"d", "f"}) + assertResult(t, "4", got) } func TestWrite_array(t *testing.T) { @@ -180,7 +238,8 @@ b: `) updated := writeMap(data, []string{"b", "0"}, "4") - assertResult(t, "4", readMap(updated, "b", []string{"0"})) + got, _ := readMap(updated, "b", []string{"0"}) + assertResult(t, "4", got) } func TestWrite_new_array_deep(t *testing.T) { @@ -193,7 +252,8 @@ b: - c: "4"` updated := writeMap(data, []string{"b", "0", "c"}, "4") - assertResult(t, expected, yamlToString(updated)) + got, _ := yamlToString(updated) + assertResult(t, expected, got) } func TestWrite_new_map_array_deep(t *testing.T) { @@ -203,7 +263,8 @@ b: `) updated := writeMap(data, []string{"b", "d", "0"}, "4") - assertResult(t, "4", readMap(updated, "b", []string{"d", "0"})) + got, _ := readMap(updated, "b", []string{"d", "0"}) + assertResult(t, "4", got) } func TestWrite_add_to_array(t *testing.T) { @@ -217,8 +278,8 @@ b: - bb` updated := writeMap(data, []string{"b", "1"}, "bb") - - assertResult(t, expected, yamlToString(updated)) + got, _ := yamlToString(updated) + assertResult(t, expected, got) } func TestWrite_with_no_tail(t *testing.T) { diff --git a/json_converter.go b/json_converter.go index 756b5f0b..566bdc9b 100644 --- a/json_converter.go +++ b/json_converter.go @@ -2,17 +2,18 @@ package main import ( "encoding/json" + "fmt" "strconv" yaml "gopkg.in/yaml.v2" ) -func jsonToString(context interface{}) string { +func jsonToString(context interface{}) (string, error) { out, err := json.Marshal(toJSON(context)) if err != nil { - die("error printing yaml as json: ", err) + return "", fmt.Errorf("error printing yaml as json: %v", err) } - return string(out) + return string(out), nil } func toJSON(context interface{}) interface{} { diff --git a/json_converter_test.go b/json_converter_test.go index 102066be..026c522d 100644 --- a/json_converter_test.go +++ b/json_converter_test.go @@ -10,7 +10,8 @@ func TestJsonToString(t *testing.T) { b: c: 2 `) - assertResult(t, "{\"b\":{\"c\":2}}", jsonToString(data)) + got, _ := jsonToString(data) + assertResult(t, "{\"b\":{\"c\":2}}", got) } func TestJsonToString_withIntKey(t *testing.T) { @@ -19,7 +20,8 @@ func TestJsonToString_withIntKey(t *testing.T) { b: 2: c `) - assertResult(t, `{"b":{"2":"c"}}`, jsonToString(data)) + got, _ := jsonToString(data) + assertResult(t, `{"b":{"2":"c"}}`, got) } func TestJsonToString_withBoolKey(t *testing.T) { @@ -28,7 +30,8 @@ func TestJsonToString_withBoolKey(t *testing.T) { b: false: c `) - assertResult(t, `{"b":{"false":"c"}}`, jsonToString(data)) + got, _ := jsonToString(data) + assertResult(t, `{"b":{"false":"c"}}`, got) } func TestJsonToString_withArray(t *testing.T) { @@ -38,5 +41,6 @@ b: - item: one - item: two `) - assertResult(t, "{\"b\":[{\"item\":\"one\"},{\"item\":\"two\"}]}", jsonToString(data)) + got, _ := jsonToString(data) + assertResult(t, "{\"b\":[{\"item\":\"one\"},{\"item\":\"two\"}]}", got) } diff --git a/utils_test.go b/utils_test.go index 5d872dfb..88789194 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,14 +1,35 @@ package main import ( + "bytes" "fmt" + "io/ioutil" "os" "reflect" + "strings" "testing" + "github.com/spf13/cobra" yaml "gopkg.in/yaml.v2" ) +type resulter struct { + Error error + Output string + Command *cobra.Command +} + +func runCmd(c *cobra.Command, input string) resulter { + buf := new(bytes.Buffer) + c.SetOutput(buf) + c.SetArgs(strings.Split(input, " ")) + + err := c.Execute() + output := buf.String() + + return resulter{err, output, c} +} + func parseData(rawData string) yaml.MapSlice { var parsedData yaml.MapSlice err := yaml.Unmarshal([]byte(rawData), &parsedData) @@ -38,3 +59,22 @@ func assertResultWithContext(t *testing.T, expectedValue interface{}, actualValu t.Error(": expected <", expectedValue, "> but got <", actualValue, ">") } } + +func writeTempYamlFile(content string) string { + tmpfile, _ := ioutil.TempFile("", "testyaml") + defer func() { + _ = tmpfile.Close() + }() + + _, _ = tmpfile.Write([]byte(content)) + return tmpfile.Name() +} + +func readTempYamlFile(name string) string { + content, _ := ioutil.ReadFile(name) + return string(content) +} + +func removeTempYamlFile(name string) { + _ = os.Remove(name) +} diff --git a/yaml.go b/yaml.go index 4299040a..a425e4ac 100644 --- a/yaml.go +++ b/yaml.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "io/ioutil" "os" @@ -15,30 +16,45 @@ import ( var trimOutput = true var writeInplace = false var writeScript = "" -var inputJSON = false var outputToJSON = false var verbose = false var log = logging.MustGetLogger("yaml") -var format = logging.MustStringFormatter( - `%{color}%{time:15:04:05} %{shortfunc} [%{level:.4s}]%{color:reset} %{message}`, -) -var backend = logging.AddModuleLevel( - logging.NewBackendFormatter(logging.NewLogBackend(os.Stderr, "", 0), format)) func main() { - backend.SetLevel(logging.ERROR, "") - logging.SetBackend(backend) + cmd := newCommandCLI() + if err := cmd.Execute(); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } +} - var cmdRead = createReadCmd() - var cmdWrite = createWriteCmd() - var cmdNew = createNewCmd() +func newCommandCLI() *cobra.Command { + var rootCmd = &cobra.Command{ + Use: "yaml", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + var format = logging.MustStringFormatter( + `%{color}%{time:15:04:05} %{shortfunc} [%{level:.4s}]%{color:reset} %{message}`, + ) + var backend = logging.AddModuleLevel( + logging.NewBackendFormatter(logging.NewLogBackend(os.Stderr, "", 0), format)) + + if verbose { + backend.SetLevel(logging.DEBUG, "") + } else { + backend.SetLevel(logging.ERROR, "") + } + + logging.SetBackend(backend) + }, + } - var rootCmd = &cobra.Command{Use: "yaml"} rootCmd.PersistentFlags().BoolVarP(&trimOutput, "trim", "t", true, "trim yaml output") rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "output as json") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode") - rootCmd.AddCommand(cmdRead, cmdWrite, cmdNew) - _ = rootCmd.Execute() + + rootCmd.AddCommand(createReadCmd(), createWriteCmd(), createNewCmd()) + + return rootCmd } func createReadCmd() *cobra.Command { @@ -54,7 +70,7 @@ yaml r things.yaml a.array[0].blah yaml r things.yaml a.array[*].blah `, Long: "Outputs the value of the given path in the yaml file to STDOUT", - Run: readProperty, + RunE: readProperty, } } @@ -81,7 +97,7 @@ a.b.c: true, a.b.e: - name: bob `, - Run: writeProperty, + RunE: writeProperty, } cmdWrite.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace") cmdWrite.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for updating yaml") @@ -104,37 +120,47 @@ Outputs to STDOUT Create Scripts: Note that you can give a create script to perform more sophisticated yaml. This follows the same format as the update script. `, - Run: newProperty, + RunE: newProperty, } cmdNew.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for updating yaml") return cmdNew } -func readProperty(cmd *cobra.Command, args []string) { - if verbose { - backend.SetLevel(logging.DEBUG, "") +func readProperty(cmd *cobra.Command, args []string) error { + data, err := read(args) + if err != nil { + return err } - print(read(args)) + dataStr, err := toString(data) + if err != nil { + return err + } + cmd.Println(dataStr) + return nil } -func read(args []string) interface{} { - +func read(args []string) (interface{}, error) { var parsedData yaml.MapSlice var path = "" - if len(args) > 1 { + + if len(args) < 1 { + return nil, errors.New("Must provide filename") + } else if len(args) > 1 { path = args[1] } - err := readData(args[0], &parsedData, inputJSON) - if err != nil { + + if err := readData(args[0], &parsedData); err != nil { var generalData interface{} - readDataOrDie(args[0], &generalData, inputJSON) + if err = readData(args[0], &generalData); err != nil { + return nil, err + } item := yaml.MapItem{Key: "thing", Value: generalData} parsedData = yaml.MapSlice{item} path = "thing." + path } if path == "" { - return parsedData + return parsedData, nil } var paths = parsePath(path) @@ -142,20 +168,27 @@ func read(args []string) interface{} { return readMap(parsedData, paths[0], paths[1:]) } -func newProperty(cmd *cobra.Command, args []string) { - if verbose { - backend.SetLevel(logging.DEBUG, "") +func newProperty(cmd *cobra.Command, args []string) error { + updatedData, err := newYaml(args) + if err != nil { + return err } - updatedData := newYaml(args) - print(updatedData) + dataStr, err := toString(updatedData) + if err != nil { + return err + } + cmd.Println(dataStr) + return nil } -func newYaml(args []string) interface{} { +func newYaml(args []string) (interface{}, error) { var writeCommands yaml.MapSlice if writeScript != "" { - readDataOrDie(writeScript, &writeCommands, false) + if err := readData(writeScript, &writeCommands); err != nil { + return nil, err + } } else if len(args) < 2 { - die("Must provide ") + return nil, errors.New("Must provide ") } else { writeCommands = make(yaml.MapSlice, 1) writeCommands[0] = yaml.MapItem{Key: args[0], Value: parseValue(args[1])} @@ -175,19 +208,31 @@ func newYaml(args []string) interface{} { return updateParsedData(parsedData, writeCommands, prependCommand) } -func writeProperty(cmd *cobra.Command, args []string) { - if verbose { - backend.SetLevel(logging.DEBUG, "") - } - updatedData := updateYaml(args) - if writeInplace { - _ = ioutil.WriteFile(args[0], []byte(yamlToString(updatedData)), 0644) - } else { - print(updatedData) +func writeProperty(cmd *cobra.Command, args []string) error { + updatedData, err := updateYaml(args) + if err != nil { + return err } + return write(cmd, args[0], updatedData) } -func updateParsedData(parsedData yaml.MapSlice, writeCommands yaml.MapSlice, prependCommand string) interface{} { +func write(cmd *cobra.Command, filename string, updatedData interface{}) error { + if writeInplace { + dataStr, err := yamlToString(updatedData) + if err != nil { + return err + } + return ioutil.WriteFile(filename, []byte(dataStr), 0644) + } + dataStr, err := toString(updatedData) + if err != nil { + return err + } + cmd.Println(dataStr) + return nil +} + +func updateParsedData(parsedData yaml.MapSlice, writeCommands yaml.MapSlice, prependCommand string) (interface{}, error) { var prefix = "" if prependCommand != "" { prefix = prependCommand + "." @@ -201,26 +246,29 @@ func updateParsedData(parsedData yaml.MapSlice, writeCommands yaml.MapSlice, pre if prependCommand != "" { return readMap(parsedData, prependCommand, make([]string, 0)) } - return parsedData + return parsedData, nil } -func updateYaml(args []string) interface{} { +func updateYaml(args []string) (interface{}, error) { var writeCommands yaml.MapSlice var prependCommand = "" if writeScript != "" { - readDataOrDie(writeScript, &writeCommands, false) + if err := readData(writeScript, &writeCommands); err != nil { + return nil, err + } } else if len(args) < 3 { - die("Must provide ") + return nil, errors.New("Must provide ") } else { writeCommands = make(yaml.MapSlice, 1) writeCommands[0] = yaml.MapItem{Key: args[1], Value: parseValue(args[2])} } var parsedData yaml.MapSlice - err := readData(args[0], &parsedData, inputJSON) - if err != nil { + if err := readData(args[0], &parsedData); err != nil { var generalData interface{} - readDataOrDie(args[0], &generalData, inputJSON) + if err = readData(args[0], &generalData); err != nil { + return nil, err + } item := yaml.MapItem{Key: "thing", Value: generalData} parsedData = yaml.MapSlice{item} prependCommand = "thing" @@ -246,69 +294,43 @@ func parseValue(argument string) interface{} { return argument[1 : len(argument)-1] } -func print(context interface{}) { - var out string +func toString(context interface{}) (string, error) { if outputToJSON { - out = jsonToString(context) - } else { - out = yamlToString(context) + return jsonToString(context) } - fmt.Println(out) + return yamlToString(context) } -func yamlToString(context interface{}) string { +func yamlToString(context interface{}) (string, error) { out, err := yaml.Marshal(context) if err != nil { - die("error printing yaml: %v", err) + return "", fmt.Errorf("error printing yaml: %v", err) } outStr := string(out) // trim the trailing new line as it's easier for a script to add // it in if required than to remove it if trimOutput { - return strings.Trim(outStr, "\n ") + return strings.Trim(outStr, "\n "), nil } - return outStr + return outStr, nil } -func readDataOrDie(filename string, parsedData interface{}, readAsJSON bool) { - err := readData(filename, parsedData, readAsJSON) - if err != nil { - die("error parsing data: ", err) - } -} - -func readData(filename string, parsedData interface{}, readAsJSON bool) error { +func readData(filename string, parsedData interface{}) error { if filename == "" { - die("Must provide filename") + return errors.New("Must provide filename") } var rawData []byte + var err error if filename == "-" { - rawData = readStdin() + rawData, err = ioutil.ReadAll(os.Stdin) } else { - rawData = readFile(filename) + rawData, err = ioutil.ReadFile(filename) + } + + if err != nil { + return err } return yaml.Unmarshal(rawData, parsedData) } - -func readStdin() []byte { - bytes, err := ioutil.ReadAll(os.Stdin) - if err != nil { - die("error reading stdin", err) - } - return bytes -} - -func readFile(filename string) []byte { - var rawData, readError = ioutil.ReadFile(filename) - if readError != nil { - die("error: %v", readError) - } - return rawData -} - -func die(message ...interface{}) { - fmt.Println(message) - os.Exit(1) -} diff --git a/yaml_test.go b/yaml_test.go index 0164e997..fa0dd3eb 100644 --- a/yaml_test.go +++ b/yaml_test.go @@ -24,23 +24,23 @@ func TestParseValue(t *testing.T) { } func TestRead(t *testing.T) { - result := read([]string{"examples/sample.yaml", "b.c"}) + result, _ := read([]string{"examples/sample.yaml", "b.c"}) assertResult(t, 2, result) } func TestReadArray(t *testing.T) { - result := read([]string{"examples/sample_array.yaml", "[1]"}) + result, _ := read([]string{"examples/sample_array.yaml", "[1]"}) assertResult(t, 2, result) } func TestReadString(t *testing.T) { - result := read([]string{"examples/sample_text.yaml"}) + result, _ := read([]string{"examples/sample_text.yaml"}) assertResult(t, "hi", result) } func TestOrder(t *testing.T) { - result := read([]string{"examples/order.yaml"}) - formattedResult := yamlToString(result) + result, _ := read([]string{"examples/order.yaml"}) + formattedResult, _ := yamlToString(result) assertResult(t, `version: 3 application: MyApp`, @@ -48,7 +48,7 @@ application: MyApp`, } func TestNewYaml(t *testing.T) { - result := newYaml([]string{"b.c", "3"}) + result, _ := newYaml([]string{"b.c", "3"}) formattedResult := fmt.Sprintf("%v", result) assertResult(t, "[{b [{c 3}]}]", @@ -56,7 +56,7 @@ func TestNewYaml(t *testing.T) { } func TestNewYamlArray(t *testing.T) { - result := newYaml([]string{"[0].cat", "meow"}) + result, _ := newYaml([]string{"[0].cat", "meow"}) formattedResult := fmt.Sprintf("%v", result) assertResult(t, "[[{cat meow}]]", @@ -64,7 +64,7 @@ func TestNewYamlArray(t *testing.T) { } func TestUpdateYaml(t *testing.T) { - result := updateYaml([]string{"examples/sample.yaml", "b.c", "3"}) + result, _ := updateYaml([]string{"examples/sample.yaml", "b.c", "3"}) formattedResult := fmt.Sprintf("%v", result) assertResult(t, "[{a Easy! as one two three} {b [{c 3} {d [3 4]} {e [[{name fred} {value 3}] [{name sam} {value 4}]]}]}]", @@ -72,7 +72,7 @@ func TestUpdateYaml(t *testing.T) { } func TestUpdateYamlArray(t *testing.T) { - result := updateYaml([]string{"examples/sample_array.yaml", "[0]", "3"}) + result, _ := updateYaml([]string{"examples/sample_array.yaml", "[0]", "3"}) formattedResult := fmt.Sprintf("%v", result) assertResult(t, "[3 2 3]", @@ -81,7 +81,17 @@ func TestUpdateYamlArray(t *testing.T) { func TestUpdateYaml_WithScript(t *testing.T) { writeScript = "examples/instruction_sample.yaml" - updateYaml([]string{"examples/sample.yaml"}) + _, _ = updateYaml([]string{"examples/sample.yaml"}) +} + +func TestUpdateYaml_WithUnknownScript(t *testing.T) { + writeScript = "fake-unknown" + _, err := updateYaml([]string{"examples/sample.yaml"}) + if err == nil { + t.Error("Expected error due to unknown file") + } + expectedOutput := `open fake-unknown: no such file or directory` + assertResult(t, expectedOutput, err.Error()) } func TestNewYaml_WithScript(t *testing.T) { @@ -90,7 +100,17 @@ func TestNewYaml_WithScript(t *testing.T) { c: cat e: - name: Mike Farah` - result := newYaml([]string{""}) - actualResult := yamlToString(result) + result, _ := newYaml([]string{""}) + actualResult, _ := yamlToString(result) assertResult(t, expectedResult, actualResult) } + +func TestNewYaml_WithUnknownScript(t *testing.T) { + writeScript = "fake-unknown" + _, err := newYaml([]string{""}) + if err == nil { + t.Error("Expected error due to unknown file") + } + expectedOutput := `open fake-unknown: no such file or directory` + assertResult(t, expectedOutput, err.Error()) +}