From e2d6d0fdc30b7626f4a660dd7f21b2db583fae57 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 21 Feb 2021 18:48:23 +0100 Subject: [PATCH] Use yaml.v3 instead of modified yaml.v2 for handling YAML files (#791) * Add another test (that currently fails). * First shot at using yaml.v3 for reading YAML files with comments. * Allow parsing multi-document YAML files. * Use Decoder to parse multi-part documents. * Use yaml.v3 for config and audit. * First step of serializing YAML using yaml.v3. * Always serialize with yaml.v3. * Remove debug prints. * Remove traces of github.com/mozilla-services/yaml. * Improve serialization of documents consisting only of comments. * Improve handling of some empty documents. * Adjust to latest changes in go-yaml/yaml#684. * Bump yaml.v3 version, temporarily disable failing tests. * Run go mod tidy. * Fix CI. --- audit/audit.go | 2 +- config/config.go | 2 +- go.mod | 2 +- go.sum | 4 +- stores/yaml/store.go | 386 ++++++++++++++++++++++++++++---------- stores/yaml/store_test.go | 74 +++++++- 6 files changed, 363 insertions(+), 107 deletions(-) diff --git a/audit/audit.go b/audit/audit.go index 1a29f5ea2..b52215077 100644 --- a/audit/audit.go +++ b/audit/audit.go @@ -12,7 +12,7 @@ import ( // empty import as per https://godoc.org/github.com/lib/pq _ "github.com/lib/pq" - "github.com/mozilla-services/yaml" + "gopkg.in/yaml.v3" "github.com/sirupsen/logrus" "go.mozilla.org/sops/v3/logging" ) diff --git a/config/config.go b/config/config.go index 91f31df3c..18a71c3df 100644 --- a/config/config.go +++ b/config/config.go @@ -10,7 +10,7 @@ import ( "path" "regexp" - "github.com/mozilla-services/yaml" + "gopkg.in/yaml.v3" "github.com/sirupsen/logrus" "go.mozilla.org/sops/v3" "go.mozilla.org/sops/v3/age" diff --git a/go.mod b/go.mod index 934d0e6a4..4defa0447 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,6 @@ require ( github.com/lib/pq v1.2.0 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-wordwrap v1.0.0 - github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625 github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/runc v0.1.1 // indirect @@ -49,5 +48,6 @@ require ( google.golang.org/protobuf v1.25.0 gopkg.in/ini.v1 v1.44.0 gopkg.in/urfave/cli.v1 v1.20.0 + gopkg.in/yaml.v3 v3.0.0-20210107172259-749611fa9fcc gotest.tools v2.2.0+incompatible // indirect ) diff --git a/go.sum b/go.sum index bf2cfb87a..dcc5988c2 100644 --- a/go.sum +++ b/go.sum @@ -201,8 +201,6 @@ github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUb github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625 h1:5IeGQzguDQ+EsTR5HE7tMYkZe09mqQ9cDypdKQEB5Kg= -github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625/go.mod h1:Is/Ucts/yU/mWyGR8yELRoO46mejouKsJfQLAIfTR18= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -410,6 +408,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20210107172259-749611fa9fcc h1:XANm4xAMEQhRdWKqaL0qmhGDv7RuobwCO97TIlktaQE= +gopkg.in/yaml.v3 v3.0.0-20210107172259-749611fa9fcc/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/stores/yaml/store.go b/stores/yaml/store.go index 989a4f056..a3ae2fb46 100644 --- a/stores/yaml/store.go +++ b/stores/yaml/store.go @@ -1,9 +1,12 @@ package yaml //import "go.mozilla.org/sops/v3/stores/yaml" import ( + "bytes" "fmt" + "io" + "strings" - "github.com/mozilla-services/yaml" + "gopkg.in/yaml.v3" "go.mozilla.org/sops/v3" "go.mozilla.org/sops/v3/stores" ) @@ -12,102 +15,232 @@ import ( type Store struct { } -func (store Store) mapSliceToTreeBranch(in yaml.MapSlice) sops.TreeBranch { - branch := make(sops.TreeBranch, 0) - for _, item := range in { - if comment, ok := item.Key.(yaml.Comment); ok { - // Convert the yaml comment to a generic sops comment - branch = append(branch, sops.TreeItem{ - Key: sops.Comment{ - Value: comment.Value, - }, - Value: nil, - }) - } else { - branch = append(branch, sops.TreeItem{ - Key: item.Key, - Value: store.yamlValueToTreeValue(item.Value), - }) +func (store Store) appendCommentToList(comment string, list []interface{}) []interface{} { + if comment != "" { + for _, commentLine := range strings.Split(comment, "\n") { + if commentLine != "" { + list = append(list, sops.Comment{ + Value: commentLine[1:], + }) + } + } + } + return list +} + +func (store Store) appendCommentToMap(comment string, branch sops.TreeBranch) sops.TreeBranch { + if comment != "" { + for _, commentLine := range strings.Split(comment, "\n") { + if commentLine != "" { + branch = append(branch, sops.TreeItem{ + Key: sops.Comment{ + Value: commentLine[1:], + }, + Value: nil, + }) + } } } return branch } -func (store Store) yamlValueToTreeValue(in interface{}) interface{} { - switch in := in.(type) { - case map[interface{}]interface{}: - return store.yamlMapToTreeBranch(in) - case yaml.MapSlice: - return store.mapSliceToTreeBranch(in) - case []interface{}: - return store.yamlSliceToTreeValue(in) - case yaml.Comment: - return sops.Comment{Value: in.Value} - default: - return in +func (store Store) nodeToTreeValue(node *yaml.Node, commentsWereHandled bool) (interface{}, error) { + switch node.Kind { + case yaml.DocumentNode: + panic("documents should never be passed here") + case yaml.SequenceNode: + var result []interface{} + if !commentsWereHandled { + result = store.appendCommentToList(node.HeadComment, result) + result = store.appendCommentToList(node.LineComment, result) + } + for _, item := range node.Content { + result = store.appendCommentToList(item.HeadComment, result) + result = store.appendCommentToList(item.LineComment, result) + val, err := store.nodeToTreeValue(item, true) + if err != nil { + return nil, err + } + result = append(result, val) + result = store.appendCommentToList(item.FootComment, result) + } + if !commentsWereHandled { + result = store.appendCommentToList(node.FootComment, result) + } + return result, nil + case yaml.MappingNode: + branch := make(sops.TreeBranch, 0) + return store.appendYamlNodeToTreeBranch(node, branch, false) + case yaml.ScalarNode: + var result interface{} + node.Decode(&result) + return result, nil + case yaml.AliasNode: + return store.nodeToTreeValue(node.Alias, false); } + return nil, nil } -func (store *Store) yamlSliceToTreeValue(in []interface{}) []interface{} { - for i, v := range in { - in[i] = store.yamlValueToTreeValue(v) +func (store Store) appendYamlNodeToTreeBranch(node *yaml.Node, branch sops.TreeBranch, commentsWereHandled bool) (sops.TreeBranch, error) { + var err error + if !commentsWereHandled { + branch = store.appendCommentToMap(node.HeadComment, branch) + branch = store.appendCommentToMap(node.LineComment, branch) } - return in + switch node.Kind { + case yaml.DocumentNode: + for _, item := range node.Content { + branch, err = store.appendYamlNodeToTreeBranch(item, branch, false) + if err != nil { + return nil, err + } + } + case yaml.SequenceNode: + return nil, fmt.Errorf("YAML documents that are sequences are not supported") + case yaml.MappingNode: + for i := 0; i < len(node.Content); i += 2 { + key := node.Content[i] + value := node.Content[i + 1] + branch = store.appendCommentToMap(key.HeadComment, branch) + branch = store.appendCommentToMap(key.LineComment, branch) + handleValueComments := value.Kind == yaml.ScalarNode || value.Kind == yaml.AliasNode + if handleValueComments { + branch = store.appendCommentToMap(value.HeadComment, branch) + branch = store.appendCommentToMap(value.LineComment, branch) + } + var keyValue interface{} + key.Decode(&keyValue) + valueTV, err := store.nodeToTreeValue(value, handleValueComments) + if err != nil { + return nil, err + } + branch = append(branch, sops.TreeItem{ + Key: keyValue, + Value: valueTV, + }) + if handleValueComments { + branch = store.appendCommentToMap(value.FootComment, branch) + } + branch = store.appendCommentToMap(key.FootComment, branch) + } + case yaml.ScalarNode: + // A empty document with a document start marker without comments results in null + if node.ShortTag() == "!!null" { + return branch, nil + } + return nil, fmt.Errorf("YAML documents that are values are not supported") + case yaml.AliasNode: + branch, err = store.appendYamlNodeToTreeBranch(node.Alias, branch, false) + } + if !commentsWereHandled { + branch = store.appendCommentToMap(node.FootComment, branch) + } + return branch, nil } -func (store *Store) yamlMapToTreeBranch(in map[interface{}]interface{}) sops.TreeBranch { +func (store Store) yamlDocumentNodeToTreeBranch(in yaml.Node) (sops.TreeBranch, error) { branch := make(sops.TreeBranch, 0) - for k, v := range in { - branch = append(branch, sops.TreeItem{ - Key: k.(string), - Value: store.yamlValueToTreeValue(v), - }) - } - return branch + return store.appendYamlNodeToTreeBranch(&in, branch, false) } -func (store Store) treeValueToYamlValue(in interface{}) interface{} { +func (store *Store) addCommentsHead(node *yaml.Node, comments []string) []string { + if len(comments) > 0 { + comment := "#" + strings.Join(comments, "\n#") + if len(node.HeadComment) > 0 { + node.HeadComment = comment + "\n" + node.HeadComment + } else { + node.HeadComment = comment + } + } + return nil +} + +func (store *Store) addCommentsFoot(node *yaml.Node, comments []string) []string { + if len(comments) > 0 { + comment := "#" + strings.Join(comments, "\n#") + if len(node.FootComment) > 0 { + node.FootComment += "\n" + comment + } else { + node.FootComment = comment + } + } + return nil +} + +func (store *Store) treeValueToNode(in interface{}) *yaml.Node { switch in := in.(type) { case sops.TreeBranch: - return store.treeBranchToYamlMap(in) - case sops.Comment: - return yaml.Comment{in.Value} + var mapping = &yaml.Node{} + mapping.Kind = yaml.MappingNode + store.appendTreeBranch(in, mapping) + return mapping case []interface{}: - var out []interface{} - for _, v := range in { - out = append(out, store.treeValueToYamlValue(v)) - } - return out + var sequence = &yaml.Node{} + sequence.Kind = yaml.SequenceNode + store.appendSequence(in, sequence) + return sequence default: - return in + var valueNode = &yaml.Node{} + valueNode.Encode(in) + return valueNode } } -func (store Store) treeBranchToYamlMap(in sops.TreeBranch) yaml.MapSlice { - branch := make(yaml.MapSlice, 0) +func (store *Store) appendSequence(in []interface{}, sequence *yaml.Node) { + var comments []string + var beginning bool = true for _, item := range in { - if comment, ok := item.Key.(sops.Comment); ok { - branch = append(branch, yaml.MapItem{ - Key: store.treeValueToYamlValue(comment), - Value: nil, - }) + if comment, ok := item.(sops.Comment); ok { + comments = append(comments, comment.Value) } else { - branch = append(branch, yaml.MapItem{ - Key: item.Key, - Value: store.treeValueToYamlValue(item.Value), - }) + if beginning { + comments = store.addCommentsHead(sequence, comments) + beginning = false + } + itemNode := store.treeValueToNode(item) + comments = store.addCommentsHead(itemNode, comments) + sequence.Content = append(sequence.Content, itemNode) + } + } + if len(comments) > 0 { + if beginning { + comments = store.addCommentsHead(sequence, comments) + } else { + comments = store.addCommentsFoot(sequence.Content[len(sequence.Content) - 1], comments) + } + } +} + +func (store *Store) appendTreeBranch(branch sops.TreeBranch, mapping *yaml.Node) { + var comments []string + var beginning bool = true + for _, item := range branch { + if comment, ok := item.Key.(sops.Comment); ok { + comments = append(comments, comment.Value) + } else { + if beginning { + comments = store.addCommentsHead(mapping, comments) + beginning = false + } + var keyNode = &yaml.Node{} + keyNode.Encode(item.Key) + comments = store.addCommentsHead(keyNode, comments) + valueNode := store.treeValueToNode(item.Value) + mapping.Content = append(mapping.Content, keyNode, valueNode) + } + } + if len(comments) > 0 { + if beginning { + comments = store.addCommentsHead(mapping, comments) + } else { + comments = store.addCommentsFoot(mapping.Content[len(mapping.Content) - 1], comments) } } - return branch } // LoadEncryptedFile loads the contents of an encrypted yaml file onto a // sops.Tree runtime object func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) { - var data []yaml.MapSlice - if err := (yaml.CommentUnmarshaler{}).UnmarshalDocuments(in, &data); err != nil { - return sops.Tree{}, fmt.Errorf("Error unmarshaling input YAML: %s", err) - } // Because we don't know what fields the input file will have, we have to // load the file in two steps. // First, we load the file's metadata, the structure of which is known. @@ -123,14 +256,33 @@ func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) { if err != nil { return sops.Tree{}, err } + var data yaml.Node + if err := yaml.Unmarshal(in, &data); err != nil { + return sops.Tree{}, fmt.Errorf("Error unmarshaling input YAML: %s", err) + } var branches sops.TreeBranches - for _, doc := range data { - for i, item := range doc { - if item.Key == "sops" { // Erase - doc = append(doc[:i], doc[i+1:]...) + d := yaml.NewDecoder(bytes.NewReader(in)) + for true { + var data yaml.Node + err := d.Decode(&data) + if err == io.EOF { + break + } + if err != nil { + return sops.Tree{}, fmt.Errorf("Error unmarshaling input YAML: %s", err) + } + + branch, err := store.yamlDocumentNodeToTreeBranch(data) + if err != nil { + return sops.Tree{}, fmt.Errorf("Error unmarshaling input YAML: %s", err) + } + + for i, elt := range branch { + if elt.Key == "sops" { // Erase + branch = append(branch[:i], branch[i+1:]...) } } - branches = append(branches, store.mapSliceToTreeBranch(doc)) + branches = append(branches, branch) } return sops.Tree{ Branches: branches, @@ -139,16 +291,25 @@ func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) { } // LoadPlainFile loads the contents of a plaintext yaml file onto a -// sops.Tree runtime obejct +// sops.Tree runtime object func (store *Store) LoadPlainFile(in []byte) (sops.TreeBranches, error) { - var data []yaml.MapSlice - if err := (yaml.CommentUnmarshaler{}).UnmarshalDocuments(in, &data); err != nil { - return nil, fmt.Errorf("Error unmarshaling input YAML: %s", err) - } - var branches sops.TreeBranches - for _, doc := range data { - branches = append(branches, store.mapSliceToTreeBranch(doc)) + d := yaml.NewDecoder(bytes.NewReader(in)) + for true { + var data yaml.Node + err := d.Decode(&data) + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("Error unmarshaling input YAML: %s", err) + } + + branch, err := store.yamlDocumentNodeToTreeBranch(data) + if err != nil { + return nil, fmt.Errorf("Error unmarshaling input YAML: %s", err) + } + branches = append(branches, branch) } return branches, nil } @@ -156,45 +317,70 @@ func (store *Store) LoadPlainFile(in []byte) (sops.TreeBranches, error) { // EmitEncryptedFile returns the encrypted bytes of the yaml file corresponding to a // sops.Tree runtime object func (store *Store) EmitEncryptedFile(in sops.Tree) ([]byte, error) { - out := []byte{} - for i, branch := range in.Branches { - if i > 0 { - out = append(out, "---\n"...) - } - yamlMap := store.treeBranchToYamlMap(branch) - yamlMap = append(yamlMap, yaml.MapItem{Key: "sops", Value: stores.MetadataFromInternal(in.Metadata)}) - tout, err := (&yaml.YAMLMarshaler{Indent: 4}).Marshal(yamlMap) + var b bytes.Buffer + e := yaml.NewEncoder(io.Writer(&b)) + e.SetIndent(4) + for _, branch := range in.Branches { + // Document root + var doc = yaml.Node{} + doc.Kind = yaml.DocumentNode + // Add global mapping + var mapping = yaml.Node{} + mapping.Kind = yaml.MappingNode + doc.Content = append(doc.Content, &mapping) + // Create copy of branch with metadata appended + branch = append(sops.TreeBranch(nil), branch...) + branch = append(branch, sops.TreeItem{ + Key: "sops", + Value: stores.MetadataFromInternal(in.Metadata), + }) + // Marshal branch to global mapping node + store.appendTreeBranch(branch, &mapping) + // Encode YAML + err := e.Encode(&doc) if err != nil { return nil, fmt.Errorf("Error marshaling to yaml: %s", err) } - out = append(out, tout...) } - return out, nil + e.Close() + return b.Bytes(), nil } // EmitPlainFile returns the plaintext bytes of the yaml file corresponding to a // sops.TreeBranches runtime object func (store *Store) EmitPlainFile(branches sops.TreeBranches) ([]byte, error) { - var out []byte - for i, branch := range branches { - if i > 0 { - out = append(out, "---\n"...) + var b bytes.Buffer + e := yaml.NewEncoder(io.Writer(&b)) + e.SetIndent(4) + for _, branch := range branches { + // Document root + var doc = yaml.Node{} + doc.Kind = yaml.DocumentNode + // Add global mapping + var mapping = yaml.Node{} + mapping.Kind = yaml.MappingNode + // Marshal branch to global mapping node + store.appendTreeBranch(branch, &mapping) + if len(mapping.Content) == 0 { + doc.HeadComment = mapping.HeadComment + } else { + doc.Content = append(doc.Content, &mapping) } - yamlMap := store.treeBranchToYamlMap(branch) - tmpout, err := (&yaml.YAMLMarshaler{Indent: 4}).Marshal(yamlMap) + // Encode YAML + err := e.Encode(&doc) if err != nil { return nil, fmt.Errorf("Error marshaling to yaml: %s", err) } - out = append(out[:], tmpout[:]...) } - return out, nil + e.Close() + return b.Bytes(), nil } // EmitValue returns bytes corresponding to a single encoded value // in a generic interface{} object func (store *Store) EmitValue(v interface{}) ([]byte, error) { - v = store.treeValueToYamlValue(v) - return (&yaml.YAMLMarshaler{Indent: 4}).Marshal(v) + n := store.treeValueToNode(v) + return yaml.Marshal(n) } // EmitExample returns the bytes corresponding to an example complex tree diff --git a/stores/yaml/store_test.go b/stores/yaml/store_test.go index d8ccc4c46..be6b90eb2 100644 --- a/stores/yaml/store_test.go +++ b/stores/yaml/store_test.go @@ -15,6 +15,12 @@ key1_a: value --- key2: value2`) +var PLAIN_0 = []byte(`# comment 0 +key1: value +key1_a: value +# ^ comment 1 +`) + var BRANCHES = sops.TreeBranches{ sops.TreeBranch{ sops.TreeItem{ @@ -73,8 +79,16 @@ prometheus-node-exporter: ## jobLabel: node-exporter extraArgs: - - --collector.filesystem.ignored-mount-points=^/(dev|proc|sys|var/lib/docker/.+)($|/) - - --collector.filesystem.ignored-fs-types=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|sysfs|tracefs)$ + - --collector.filesystem.ignored-mount-points=^/(dev|proc|sys|var/lib/docker/.+)($|/) + - --collector.filesystem.ignored-fs-types=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|sysfs|tracefs)$ +`) + +var COMMENT_4 = []byte(`# foo +`) + +var COMMENT_5 = []byte(`# foo +--- +key: value `) func TestUnmarshalMetadataFromNonSOPSFile(t *testing.T) { @@ -95,6 +109,7 @@ func TestComment1(t *testing.T) { assert.Nil(t, err) bytes, err := (&Store{}).EmitPlainFile(branches) assert.Nil(t, err) + assert.Equal(t, string(COMMENT_1), string(bytes)) assert.Equal(t, COMMENT_1, bytes) } @@ -104,6 +119,7 @@ func TestComment2(t *testing.T) { assert.Nil(t, err) bytes, err := (&Store{}).EmitPlainFile(branches) assert.Nil(t, err) + assert.Equal(t, string(COMMENT_2), string(bytes)) assert.Equal(t, COMMENT_2, bytes) } @@ -113,5 +129,59 @@ func TestComment3(t *testing.T) { assert.Nil(t, err) bytes, err := (&Store{}).EmitPlainFile(branches) assert.Nil(t, err) + assert.Equal(t, string(COMMENT_3_OUT), string(bytes)) assert.Equal(t, COMMENT_3_OUT, bytes) } + +/* TODO: re-enable once https://github.com/go-yaml/yaml/pull/690 is merged +func TestComment4(t *testing.T) { + // First iteration: load and store + branches, err := (&Store{}).LoadPlainFile(COMMENT_4) + assert.Nil(t, err) + bytes, err := (&Store{}).EmitPlainFile(branches) + assert.Nil(t, err) + assert.Equal(t, string(COMMENT_4), string(bytes)) + assert.Equal(t, COMMENT_4, bytes) +} + +func TestComment5(t *testing.T) { + // First iteration: load and store + branches, err := (&Store{}).LoadPlainFile(COMMENT_5) + assert.Nil(t, err) + bytes, err := (&Store{}).EmitPlainFile(branches) + assert.Nil(t, err) + assert.Equal(t, string(COMMENT_5), string(bytes)) + assert.Equal(t, COMMENT_5, bytes) +} +*/ + +func TestEmpty(t *testing.T) { + // First iteration: load and store + branches, err := (&Store{}).LoadPlainFile([]byte(``)) + assert.Nil(t, err) + assert.Equal(t, len(branches), 0) + bytes, err := (&Store{}).EmitPlainFile(branches) + assert.Nil(t, err) + assert.Equal(t, ``, string(bytes)) +} + +/* TODO: re-enable once https://github.com/go-yaml/yaml/pull/690 is merged +func TestEmpty2(t *testing.T) { + // First iteration: load and store + branches, err := (&Store{}).LoadPlainFile([]byte(`---`)) + assert.Nil(t, err) + assert.Equal(t, len(branches), 1) + assert.Equal(t, len(branches[0]), 0) + bytes, err := (&Store{}).EmitPlainFile(branches) + assert.Nil(t, err) + assert.Equal(t, ``, string(bytes)) +} +*/ + +func TestEmitValue(t *testing.T) { + // First iteration: load and store + bytes, err := (&Store{}).EmitValue(BRANCHES[0]) + assert.Nil(t, err) + assert.Equal(t, string(PLAIN_0), string(bytes)) + assert.Equal(t, PLAIN_0, bytes) +}