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.
This commit is contained in:
kenjones 2017-09-22 15:58:12 -04:00
parent 359ca5a117
commit 53b2c64747
8 changed files with 620 additions and 157 deletions

305
commands_test.go Normal file
View File

@ -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 <path_to_update> <value>`
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 <filename> <path_to_update> <value>`
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)
}

View File

@ -1,9 +1,10 @@
package main package main
import ( import (
"fmt"
"strconv" "strconv"
yaml "gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
func entryInSlice(context yaml.MapSlice, key interface{}) *yaml.MapItem { 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) log.Debugf("\tarray %v\n", array)
rawIndex := paths[0] rawIndex := paths[0]
index, err := strconv.ParseInt(rawIndex, 10, 64) index, _ := strconv.ParseInt(rawIndex, 10, 64)
if err != nil { // writeArray is only called by updatedChildValue which handles parsing the
die("Error accessing array: %v", err) // 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)) { for index >= int64(len(array)) {
array = append(array, nil) array = append(array, nil)
} }
@ -96,7 +99,7 @@ func writeArray(context interface{}, paths []string, value interface{}) []interf
return array 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 == "*" { if head == "*" {
return readMapSplat(context, tail) return readMapSplat(context, tail)
} }
@ -109,21 +112,25 @@ func readMap(context yaml.MapSlice, head string, tail []string) interface{} {
return calculateValue(value, tail) 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 newArray = make([]interface{}, len(context))
var i = 0 var i = 0
for _, entry := range context { for _, entry := range context {
if len(tail) > 0 { 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 { } else {
newArray[i] = entry.Value newArray[i] = entry.Value
} }
i++ 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) { switch value.(type) {
case []interface{}: case []interface{}:
if head == "*" { if head == "*" {
@ -131,37 +138,40 @@ func recurse(value interface{}, head string, tail []string) interface{} {
} }
index, err := strconv.ParseInt(head, 10, 64) index, err := strconv.ParseInt(head, 10, 64)
if err != nil { if err != nil {
die("Error accessing array: %v", err) return nil, fmt.Errorf("Error accessing array: %v", err)
} }
return readArray(value.([]interface{}), index, tail) return readArray(value.([]interface{}), index, tail)
case yaml.MapSlice: case yaml.MapSlice:
return readMap(value.(yaml.MapSlice), head, tail) return readMap(value.(yaml.MapSlice), head, tail)
default: 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)) { if head >= int64(len(array)) {
return nil return nil, nil
} }
value := array[head] value := array[head]
return calculateValue(value, tail) 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)) var newArray = make([]interface{}, len(array))
for index, value := range array { for index, value := range array {
newArray[index] = calculateValue(value, tail) val, err := calculateValue(value, tail)
if err != nil {
return nil, err
} }
return newArray newArray[index] = val
}
return newArray, nil
} }
func calculateValue(value interface{}, tail []string) interface{} { func calculateValue(value interface{}, tail []string) (interface{}, error) {
if len(tail) > 0 { if len(tail) > 0 {
return recurse(value, tail[0], tail[1:]) return recurse(value, tail[0], tail[1:])
} }
return value return value, nil
} }

View File

@ -2,27 +2,20 @@ package main
import ( import (
"fmt" "fmt"
"os"
"sort" "sort"
"testing" "testing"
logging "github.com/op/go-logging"
yaml "gopkg.in/yaml.v2" 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) { func TestReadMap_simple(t *testing.T) {
var data = parseData(` var data = parseData(`
--- ---
b: b:
c: 2 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) { func TestReadMap_splat(t *testing.T) {
@ -32,7 +25,8 @@ mapSplat:
item1: things item1: things
item2: whatever 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)} var actual = []string{result[0].(string), result[1].(string)}
sort.Strings(actual) sort.Strings(actual)
assertResult(t, "[things whatever]", fmt.Sprintf("%v", actual)) assertResult(t, "[things whatever]", fmt.Sprintf("%v", actual))
@ -48,7 +42,8 @@ mapSplatDeep:
cats: apples 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)} var actual = []string{result[0].(string), result[1].(string)}
sort.Strings(actual) sort.Strings(actual)
assertResult(t, "[apples bananas]", fmt.Sprintf("%v", actual)) assertResult(t, "[apples bananas]", fmt.Sprintf("%v", actual))
@ -60,7 +55,8 @@ func TestReadMap_key_doesnt_exist(t *testing.T) {
b: b:
c: 2 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) { func TestReadMap_recurse_against_string(t *testing.T) {
@ -68,7 +64,8 @@ func TestReadMap_recurse_against_string(t *testing.T) {
--- ---
a: cat 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) { func TestReadMap_with_array(t *testing.T) {
@ -79,7 +76,64 @@ b:
- 3 - 3
- 4 - 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) { func TestReadMap_with_array_out_of_bounds(t *testing.T) {
@ -90,7 +144,8 @@ b:
- 3 - 3
- 4 - 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) { func TestReadMap_with_array_out_of_bounds_by_1(t *testing.T) {
@ -101,7 +156,8 @@ b:
- 3 - 3
- 4 - 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) { func TestReadMap_with_array_splat(t *testing.T) {
@ -114,7 +170,8 @@ e:
name: Sam name: Sam
thing: dog 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) { func TestWrite_really_simple(t *testing.T) {
@ -158,7 +215,8 @@ b:
`) `)
updated := writeMap(data, []string{"b", "d", "f"}, "4") 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) { func TestWrite_array(t *testing.T) {
@ -180,7 +238,8 @@ b:
`) `)
updated := writeMap(data, []string{"b", "0"}, "4") 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) { func TestWrite_new_array_deep(t *testing.T) {
@ -193,7 +252,8 @@ b:
- c: "4"` - c: "4"`
updated := writeMap(data, []string{"b", "0", "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) { func TestWrite_new_map_array_deep(t *testing.T) {
@ -203,7 +263,8 @@ b:
`) `)
updated := writeMap(data, []string{"b", "d", "0"}, "4") 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) { func TestWrite_add_to_array(t *testing.T) {
@ -217,8 +278,8 @@ b:
- bb` - bb`
updated := writeMap(data, []string{"b", "1"}, "bb") updated := writeMap(data, []string{"b", "1"}, "bb")
got, _ := yamlToString(updated)
assertResult(t, expected, yamlToString(updated)) assertResult(t, expected, got)
} }
func TestWrite_with_no_tail(t *testing.T) { func TestWrite_with_no_tail(t *testing.T) {

View File

@ -2,17 +2,18 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt"
"strconv" "strconv"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
) )
func jsonToString(context interface{}) string { func jsonToString(context interface{}) (string, error) {
out, err := json.Marshal(toJSON(context)) out, err := json.Marshal(toJSON(context))
if err != nil { 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{} { func toJSON(context interface{}) interface{} {

View File

@ -10,7 +10,8 @@ func TestJsonToString(t *testing.T) {
b: b:
c: 2 c: 2
`) `)
assertResult(t, "{\"b\":{\"c\":2}}", jsonToString(data)) got, _ := jsonToString(data)
assertResult(t, "{\"b\":{\"c\":2}}", got)
} }
func TestJsonToString_withIntKey(t *testing.T) { func TestJsonToString_withIntKey(t *testing.T) {
@ -19,7 +20,8 @@ func TestJsonToString_withIntKey(t *testing.T) {
b: b:
2: c 2: c
`) `)
assertResult(t, `{"b":{"2":"c"}}`, jsonToString(data)) got, _ := jsonToString(data)
assertResult(t, `{"b":{"2":"c"}}`, got)
} }
func TestJsonToString_withBoolKey(t *testing.T) { func TestJsonToString_withBoolKey(t *testing.T) {
@ -28,7 +30,8 @@ func TestJsonToString_withBoolKey(t *testing.T) {
b: b:
false: c false: c
`) `)
assertResult(t, `{"b":{"false":"c"}}`, jsonToString(data)) got, _ := jsonToString(data)
assertResult(t, `{"b":{"false":"c"}}`, got)
} }
func TestJsonToString_withArray(t *testing.T) { func TestJsonToString_withArray(t *testing.T) {
@ -38,5 +41,6 @@ b:
- item: one - item: one
- item: two - item: two
`) `)
assertResult(t, "{\"b\":[{\"item\":\"one\"},{\"item\":\"two\"}]}", jsonToString(data)) got, _ := jsonToString(data)
assertResult(t, "{\"b\":[{\"item\":\"one\"},{\"item\":\"two\"}]}", got)
} }

View File

@ -1,14 +1,35 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"reflect" "reflect"
"strings"
"testing" "testing"
"github.com/spf13/cobra"
yaml "gopkg.in/yaml.v2" 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 { func parseData(rawData string) yaml.MapSlice {
var parsedData yaml.MapSlice var parsedData yaml.MapSlice
err := yaml.Unmarshal([]byte(rawData), &parsedData) 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, ">") 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)
}

210
yaml.go
View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -15,30 +16,45 @@ import (
var trimOutput = true var trimOutput = true
var writeInplace = false var writeInplace = false
var writeScript = "" var writeScript = ""
var inputJSON = false
var outputToJSON = false var outputToJSON = false
var verbose = false var verbose = false
var log = logging.MustGetLogger("yaml") 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() { func main() {
cmd := newCommandCLI()
if err := cmd.Execute(); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
}
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, "") backend.SetLevel(logging.ERROR, "")
}
logging.SetBackend(backend) logging.SetBackend(backend)
},
}
var cmdRead = createReadCmd()
var cmdWrite = createWriteCmd()
var cmdNew = createNewCmd()
var rootCmd = &cobra.Command{Use: "yaml"}
rootCmd.PersistentFlags().BoolVarP(&trimOutput, "trim", "t", true, "trim yaml output") rootCmd.PersistentFlags().BoolVarP(&trimOutput, "trim", "t", true, "trim yaml output")
rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "output as json") rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "output as json")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode") 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 { func createReadCmd() *cobra.Command {
@ -54,7 +70,7 @@ yaml r things.yaml a.array[0].blah
yaml r things.yaml a.array[*].blah yaml r things.yaml a.array[*].blah
`, `,
Long: "Outputs the value of the given path in the yaml file to STDOUT", 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: a.b.e:
- name: bob - name: bob
`, `,
Run: writeProperty, RunE: writeProperty,
} }
cmdWrite.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace") cmdWrite.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
cmdWrite.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for updating yaml") cmdWrite.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for updating yaml")
@ -104,37 +120,47 @@ Outputs to STDOUT
Create Scripts: Create Scripts:
Note that you can give a create script to perform more sophisticated yaml. This follows the same format as the update script. 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") cmdNew.PersistentFlags().StringVarP(&writeScript, "script", "s", "", "yaml script for updating yaml")
return cmdNew return cmdNew
} }
func readProperty(cmd *cobra.Command, args []string) { func readProperty(cmd *cobra.Command, args []string) error {
if verbose { data, err := read(args)
backend.SetLevel(logging.DEBUG, "") 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 parsedData yaml.MapSlice
var path = "" 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] path = args[1]
} }
err := readData(args[0], &parsedData, inputJSON)
if err != nil { if err := readData(args[0], &parsedData); err != nil {
var generalData interface{} 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} item := yaml.MapItem{Key: "thing", Value: generalData}
parsedData = yaml.MapSlice{item} parsedData = yaml.MapSlice{item}
path = "thing." + path path = "thing." + path
} }
if path == "" { if path == "" {
return parsedData return parsedData, nil
} }
var paths = parsePath(path) var paths = parsePath(path)
@ -142,20 +168,27 @@ func read(args []string) interface{} {
return readMap(parsedData, paths[0], paths[1:]) return readMap(parsedData, paths[0], paths[1:])
} }
func newProperty(cmd *cobra.Command, args []string) { func newProperty(cmd *cobra.Command, args []string) error {
if verbose { updatedData, err := newYaml(args)
backend.SetLevel(logging.DEBUG, "") if err != nil {
return err
} }
updatedData := newYaml(args) dataStr, err := toString(updatedData)
print(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 var writeCommands yaml.MapSlice
if writeScript != "" { if writeScript != "" {
readDataOrDie(writeScript, &writeCommands, false) if err := readData(writeScript, &writeCommands); err != nil {
return nil, err
}
} else if len(args) < 2 { } else if len(args) < 2 {
die("Must provide <path_to_update> <value>") return nil, errors.New("Must provide <path_to_update> <value>")
} else { } else {
writeCommands = make(yaml.MapSlice, 1) writeCommands = make(yaml.MapSlice, 1)
writeCommands[0] = yaml.MapItem{Key: args[0], Value: parseValue(args[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) return updateParsedData(parsedData, writeCommands, prependCommand)
} }
func writeProperty(cmd *cobra.Command, args []string) { func writeProperty(cmd *cobra.Command, args []string) error {
if verbose { updatedData, err := updateYaml(args)
backend.SetLevel(logging.DEBUG, "") if err != nil {
} return err
updatedData := updateYaml(args)
if writeInplace {
_ = ioutil.WriteFile(args[0], []byte(yamlToString(updatedData)), 0644)
} else {
print(updatedData)
} }
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 = "" var prefix = ""
if prependCommand != "" { if prependCommand != "" {
prefix = prependCommand + "." prefix = prependCommand + "."
@ -201,26 +246,29 @@ func updateParsedData(parsedData yaml.MapSlice, writeCommands yaml.MapSlice, pre
if prependCommand != "" { if prependCommand != "" {
return readMap(parsedData, prependCommand, make([]string, 0)) 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 writeCommands yaml.MapSlice
var prependCommand = "" var prependCommand = ""
if writeScript != "" { if writeScript != "" {
readDataOrDie(writeScript, &writeCommands, false) if err := readData(writeScript, &writeCommands); err != nil {
return nil, err
}
} else if len(args) < 3 { } else if len(args) < 3 {
die("Must provide <filename> <path_to_update> <value>") return nil, errors.New("Must provide <filename> <path_to_update> <value>")
} else { } else {
writeCommands = make(yaml.MapSlice, 1) writeCommands = make(yaml.MapSlice, 1)
writeCommands[0] = yaml.MapItem{Key: args[1], Value: parseValue(args[2])} writeCommands[0] = yaml.MapItem{Key: args[1], Value: parseValue(args[2])}
} }
var parsedData yaml.MapSlice var parsedData yaml.MapSlice
err := readData(args[0], &parsedData, inputJSON) if err := readData(args[0], &parsedData); err != nil {
if err != nil {
var generalData interface{} 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} item := yaml.MapItem{Key: "thing", Value: generalData}
parsedData = yaml.MapSlice{item} parsedData = yaml.MapSlice{item}
prependCommand = "thing" prependCommand = "thing"
@ -246,69 +294,43 @@ func parseValue(argument string) interface{} {
return argument[1 : len(argument)-1] return argument[1 : len(argument)-1]
} }
func print(context interface{}) { func toString(context interface{}) (string, error) {
var out string
if outputToJSON { if outputToJSON {
out = jsonToString(context) return jsonToString(context)
} else {
out = yamlToString(context)
} }
fmt.Println(out) return yamlToString(context)
} }
func yamlToString(context interface{}) string { func yamlToString(context interface{}) (string, error) {
out, err := yaml.Marshal(context) out, err := yaml.Marshal(context)
if err != nil { if err != nil {
die("error printing yaml: %v", err) return "", fmt.Errorf("error printing yaml: %v", err)
} }
outStr := string(out) outStr := string(out)
// trim the trailing new line as it's easier for a script to add // trim the trailing new line as it's easier for a script to add
// it in if required than to remove it // it in if required than to remove it
if trimOutput { 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) { func readData(filename string, parsedData interface{}) error {
err := readData(filename, parsedData, readAsJSON)
if err != nil {
die("error parsing data: ", err)
}
}
func readData(filename string, parsedData interface{}, readAsJSON bool) error {
if filename == "" { if filename == "" {
die("Must provide filename") return errors.New("Must provide filename")
} }
var rawData []byte var rawData []byte
var err error
if filename == "-" { if filename == "-" {
rawData = readStdin() rawData, err = ioutil.ReadAll(os.Stdin)
} else { } else {
rawData = readFile(filename) rawData, err = ioutil.ReadFile(filename)
}
if err != nil {
return err
} }
return yaml.Unmarshal(rawData, parsedData) 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)
}

View File

@ -24,23 +24,23 @@ func TestParseValue(t *testing.T) {
} }
func TestRead(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) assertResult(t, 2, result)
} }
func TestReadArray(t *testing.T) { 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) assertResult(t, 2, result)
} }
func TestReadString(t *testing.T) { func TestReadString(t *testing.T) {
result := read([]string{"examples/sample_text.yaml"}) result, _ := read([]string{"examples/sample_text.yaml"})
assertResult(t, "hi", result) assertResult(t, "hi", result)
} }
func TestOrder(t *testing.T) { func TestOrder(t *testing.T) {
result := read([]string{"examples/order.yaml"}) result, _ := read([]string{"examples/order.yaml"})
formattedResult := yamlToString(result) formattedResult, _ := yamlToString(result)
assertResult(t, assertResult(t,
`version: 3 `version: 3
application: MyApp`, application: MyApp`,
@ -48,7 +48,7 @@ application: MyApp`,
} }
func TestNewYaml(t *testing.T) { func TestNewYaml(t *testing.T) {
result := newYaml([]string{"b.c", "3"}) result, _ := newYaml([]string{"b.c", "3"})
formattedResult := fmt.Sprintf("%v", result) formattedResult := fmt.Sprintf("%v", result)
assertResult(t, assertResult(t,
"[{b [{c 3}]}]", "[{b [{c 3}]}]",
@ -56,7 +56,7 @@ func TestNewYaml(t *testing.T) {
} }
func TestNewYamlArray(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) formattedResult := fmt.Sprintf("%v", result)
assertResult(t, assertResult(t,
"[[{cat meow}]]", "[[{cat meow}]]",
@ -64,7 +64,7 @@ func TestNewYamlArray(t *testing.T) {
} }
func TestUpdateYaml(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) formattedResult := fmt.Sprintf("%v", result)
assertResult(t, assertResult(t,
"[{a Easy! as one two three} {b [{c 3} {d [3 4]} {e [[{name fred} {value 3}] [{name sam} {value 4}]]}]}]", "[{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) { 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) formattedResult := fmt.Sprintf("%v", result)
assertResult(t, assertResult(t,
"[3 2 3]", "[3 2 3]",
@ -81,7 +81,17 @@ func TestUpdateYamlArray(t *testing.T) {
func TestUpdateYaml_WithScript(t *testing.T) { func TestUpdateYaml_WithScript(t *testing.T) {
writeScript = "examples/instruction_sample.yaml" 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) { func TestNewYaml_WithScript(t *testing.T) {
@ -90,7 +100,17 @@ func TestNewYaml_WithScript(t *testing.T) {
c: cat c: cat
e: e:
- name: Mike Farah` - name: Mike Farah`
result := newYaml([]string{""}) result, _ := newYaml([]string{""})
actualResult := yamlToString(result) actualResult, _ := yamlToString(result)
assertResult(t, expectedResult, actualResult) 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())
}