mirror of
https://github.com/mikefarah/yq.git
synced 2026-03-10 15:54:26 +00:00
* feat: K8S KYAML output format support Reference: https://github.com/kubernetes/enhancements/blob/master/keps/sig-cli/5295-kyaml/README.md Co-authored-by: Codex <codex@openai.com> Generated-with: OpenAI Codex CLI (partial) Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com> * build: gomodcache/gocache should not be committed Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com> * chore: fix spelling of behaviour Signed-off-by: Robin H. Johnson <robbat2@gentoo.org> * build: pass GOFLAGS to docker to support buildvcs=false In trying to develop the KYAML support, various tests gave false positive results because they made assumptions about Git functionality Make it possible to avoid that by passing GOFLAGS='-buildvcs=false' to to Makefile. Signed-off-by: Robin H. Johnson <robbat2@gentoo.org> * doc: cover documentScenarios for tests Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com> * build: exclude go caches from gosec Without tuning, gosec scans all of the vendor/gocache/gomodcache, taking several minutes (3m35 here), whereas the core of the yq takes only 15 seconds to scan. If we intend to remediate upstream issues in future; add a seperate target to scan those. Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com> --------- Signed-off-by: Robin H. Johnson <rjohnson@coreweave.com> Signed-off-by: Robin H. Johnson <robbat2@gentoo.org> Co-authored-by: Codex <codex@openai.com>
543 lines
13 KiB
Go
543 lines
13 KiB
Go
//go:build !yq_nokyaml
|
|
|
|
package yqlib
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mikefarah/yq/v4/test"
|
|
)
|
|
|
|
var ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
|
|
|
func stripANSI(s string) string {
|
|
return ansiRe.ReplaceAllString(s, "")
|
|
}
|
|
|
|
var kyamlFormatScenarios = []formatScenario{
|
|
{
|
|
description: "Encode kyaml: plain string scalar",
|
|
subdescription: "Strings are always double-quoted in KYaml output.",
|
|
scenarioType: "encode",
|
|
indent: 2,
|
|
input: "cat\n",
|
|
expected: "\"cat\"\n",
|
|
},
|
|
{
|
|
description: "encode plain int scalar",
|
|
scenarioType: "encode",
|
|
indent: 2,
|
|
input: "12\n",
|
|
expected: "12\n",
|
|
skipDoc: true,
|
|
},
|
|
{
|
|
description: "encode plain bool scalar",
|
|
scenarioType: "encode",
|
|
indent: 2,
|
|
input: "true\n",
|
|
expected: "true\n",
|
|
skipDoc: true,
|
|
},
|
|
{
|
|
description: "encode plain null scalar",
|
|
scenarioType: "encode",
|
|
indent: 2,
|
|
input: "null\n",
|
|
expected: "null\n",
|
|
skipDoc: true,
|
|
},
|
|
{
|
|
description: "encode flow mapping and sequence",
|
|
scenarioType: "encode",
|
|
indent: 2,
|
|
input: "a: b\nc:\n - d\n",
|
|
expected: "{\n" +
|
|
" a: \"b\",\n" +
|
|
" c: [\n" +
|
|
" \"d\",\n" +
|
|
" ],\n" +
|
|
"}\n",
|
|
},
|
|
{
|
|
description: "encode non-string scalars",
|
|
scenarioType: "encode",
|
|
indent: 2,
|
|
input: "a: 12\n" +
|
|
"b: true\n" +
|
|
"c: null\n" +
|
|
"d: \"true\"\n",
|
|
expected: "{\n" +
|
|
" a: 12,\n" +
|
|
" b: true,\n" +
|
|
" c: null,\n" +
|
|
" d: \"true\",\n" +
|
|
"}\n",
|
|
},
|
|
{
|
|
description: "quote non-identifier keys",
|
|
scenarioType: "encode",
|
|
indent: 2,
|
|
input: "\"1a\": b\n\"has space\": c\n",
|
|
expected: "{\n" +
|
|
" \"1a\": \"b\",\n" +
|
|
" \"has space\": \"c\",\n" +
|
|
"}\n",
|
|
},
|
|
{
|
|
description: "escape quoted strings",
|
|
scenarioType: "encode",
|
|
indent: 2,
|
|
input: "a: \"line1\\nline2\\t\\\"q\\\"\"\n",
|
|
expected: "{\n" +
|
|
" a: \"line1\\nline2\\t\\\"q\\\"\",\n" +
|
|
"}\n",
|
|
},
|
|
{
|
|
description: "preserve comments when encoding",
|
|
scenarioType: "encode",
|
|
indent: 2,
|
|
input: "# leading\n" +
|
|
"a: 1 # a line\n" +
|
|
"# head b\n" +
|
|
"b: 2\n" +
|
|
"c:\n" +
|
|
" # head d\n" +
|
|
" - d # d line\n" +
|
|
" - e\n" +
|
|
"# trailing\n",
|
|
expected: "# leading\n" +
|
|
"{\n" +
|
|
" a: 1, # a line\n" +
|
|
" # head b\n" +
|
|
" b: 2,\n" +
|
|
" c: [\n" +
|
|
" # head d\n" +
|
|
" \"d\", # d line\n" +
|
|
" \"e\",\n" +
|
|
" ],\n" +
|
|
" # trailing\n" +
|
|
"}\n",
|
|
},
|
|
{
|
|
description: "Encode kyaml: anchors and aliases",
|
|
subdescription: "KYaml output does not support anchors/aliases; they are expanded to concrete values.",
|
|
scenarioType: "encode",
|
|
indent: 2,
|
|
input: "base: &base\n" +
|
|
" a: b\n" +
|
|
"copy: *base\n",
|
|
expected: "{\n" +
|
|
" base: {\n" +
|
|
" a: \"b\",\n" +
|
|
" },\n" +
|
|
" copy: {\n" +
|
|
" a: \"b\",\n" +
|
|
" },\n" +
|
|
"}\n",
|
|
},
|
|
{
|
|
description: "Encode kyaml: yaml to kyaml shows formatting differences",
|
|
subdescription: "KYaml uses flow-style collections (braces/brackets) and explicit commas.",
|
|
scenarioType: "encode",
|
|
indent: 2,
|
|
input: "person:\n" +
|
|
" name: John\n" +
|
|
" pets:\n" +
|
|
" - cat\n" +
|
|
" - dog\n",
|
|
expected: "{\n" +
|
|
" person: {\n" +
|
|
" name: \"John\",\n" +
|
|
" pets: [\n" +
|
|
" \"cat\",\n" +
|
|
" \"dog\",\n" +
|
|
" ],\n" +
|
|
" },\n" +
|
|
"}\n",
|
|
},
|
|
{
|
|
description: "Encode kyaml: nested lists of objects",
|
|
subdescription: "Lists and objects can be nested arbitrarily; KYaml always uses flow-style collections.",
|
|
scenarioType: "encode",
|
|
indent: 2,
|
|
input: "- name: a\n" +
|
|
" items:\n" +
|
|
" - id: 1\n" +
|
|
" tags:\n" +
|
|
" - k: x\n" +
|
|
" v: y\n" +
|
|
" - k: x2\n" +
|
|
" v: y2\n" +
|
|
" - id: 2\n" +
|
|
" tags:\n" +
|
|
" - k: z\n" +
|
|
" v: w\n",
|
|
expected: "[\n" +
|
|
" {\n" +
|
|
" name: \"a\",\n" +
|
|
" items: [\n" +
|
|
" {\n" +
|
|
" id: 1,\n" +
|
|
" tags: [\n" +
|
|
" {\n" +
|
|
" k: \"x\",\n" +
|
|
" v: \"y\",\n" +
|
|
" },\n" +
|
|
" {\n" +
|
|
" k: \"x2\",\n" +
|
|
" v: \"y2\",\n" +
|
|
" },\n" +
|
|
" ],\n" +
|
|
" },\n" +
|
|
" {\n" +
|
|
" id: 2,\n" +
|
|
" tags: [\n" +
|
|
" {\n" +
|
|
" k: \"z\",\n" +
|
|
" v: \"w\",\n" +
|
|
" },\n" +
|
|
" ],\n" +
|
|
" },\n" +
|
|
" ],\n" +
|
|
" },\n" +
|
|
"]\n",
|
|
},
|
|
}
|
|
|
|
func testKYamlScenario(t *testing.T, s formatScenario) {
|
|
prefs := ConfiguredKYamlPreferences.Copy()
|
|
prefs.Indent = s.indent
|
|
prefs.UnwrapScalar = false
|
|
|
|
switch s.scenarioType {
|
|
case "encode":
|
|
test.AssertResultWithContext(
|
|
t,
|
|
s.expected,
|
|
mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewKYamlEncoder(prefs)),
|
|
s.description,
|
|
)
|
|
default:
|
|
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
|
}
|
|
}
|
|
|
|
func documentKYamlScenario(_ *testing.T, w *bufio.Writer, i interface{}) {
|
|
s := i.(formatScenario)
|
|
if s.skipDoc {
|
|
return
|
|
}
|
|
|
|
switch s.scenarioType {
|
|
case "encode":
|
|
documentKYamlEncodeScenario(w, s)
|
|
default:
|
|
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
|
}
|
|
}
|
|
|
|
func documentKYamlEncodeScenario(w *bufio.Writer, s formatScenario) {
|
|
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
|
|
|
|
if s.subdescription != "" {
|
|
writeOrPanic(w, s.subdescription)
|
|
writeOrPanic(w, "\n\n")
|
|
}
|
|
|
|
writeOrPanic(w, "Given a sample.yml file of:\n")
|
|
writeOrPanic(w, fmt.Sprintf("```yaml\n%v\n```\n", s.input))
|
|
|
|
writeOrPanic(w, "then\n")
|
|
|
|
expression := s.expression
|
|
if expression == "" {
|
|
expression = "."
|
|
}
|
|
|
|
if s.indent == 2 {
|
|
writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=kyaml '%v' sample.yml\n```\n", expression))
|
|
} else {
|
|
writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=kyaml -I=%v '%v' sample.yml\n```\n", s.indent, expression))
|
|
}
|
|
|
|
writeOrPanic(w, "will output\n")
|
|
|
|
prefs := ConfiguredKYamlPreferences.Copy()
|
|
prefs.Indent = s.indent
|
|
prefs.UnwrapScalar = false
|
|
|
|
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewKYamlEncoder(prefs))))
|
|
}
|
|
|
|
func TestKYamlFormatScenarios(t *testing.T) {
|
|
for _, s := range kyamlFormatScenarios {
|
|
testKYamlScenario(t, s)
|
|
}
|
|
|
|
genericScenarios := make([]interface{}, len(kyamlFormatScenarios))
|
|
for i, s := range kyamlFormatScenarios {
|
|
genericScenarios[i] = s
|
|
}
|
|
documentScenarios(t, "usage", "kyaml", genericScenarios, documentKYamlScenario)
|
|
}
|
|
|
|
func TestKYamlEncoderPrintDocumentSeparator(t *testing.T) {
|
|
t.Run("enabled", func(t *testing.T) {
|
|
prefs := NewDefaultKYamlPreferences()
|
|
prefs.PrintDocSeparators = true
|
|
|
|
var buf bytes.Buffer
|
|
err := NewKYamlEncoder(prefs).PrintDocumentSeparator(&buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if buf.String() != "---\n" {
|
|
t.Fatalf("expected doc separator, got %q", buf.String())
|
|
}
|
|
})
|
|
|
|
t.Run("disabled", func(t *testing.T) {
|
|
prefs := NewDefaultKYamlPreferences()
|
|
prefs.PrintDocSeparators = false
|
|
|
|
var buf bytes.Buffer
|
|
err := NewKYamlEncoder(prefs).PrintDocumentSeparator(&buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if buf.String() != "" {
|
|
t.Fatalf("expected no output, got %q", buf.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestKYamlEncoderEncodeUnwrapScalar(t *testing.T) {
|
|
prefs := NewDefaultKYamlPreferences()
|
|
prefs.UnwrapScalar = true
|
|
|
|
var buf bytes.Buffer
|
|
err := NewKYamlEncoder(prefs).Encode(&buf, &CandidateNode{
|
|
Kind: ScalarNode,
|
|
Tag: "!!str",
|
|
Value: "cat",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if buf.String() != "cat\n" {
|
|
t.Fatalf("expected unwrapped scalar, got %q", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestKYamlEncoderEncodeColorsEnabled(t *testing.T) {
|
|
prefs := NewDefaultKYamlPreferences()
|
|
prefs.UnwrapScalar = false
|
|
prefs.ColorsEnabled = true
|
|
|
|
var buf bytes.Buffer
|
|
err := NewKYamlEncoder(prefs).Encode(&buf, &CandidateNode{
|
|
Kind: MappingNode,
|
|
Content: []*CandidateNode{
|
|
{Kind: ScalarNode, Tag: "!!str", Value: "a"},
|
|
{Kind: ScalarNode, Tag: "!!str", Value: "b"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
out := stripANSI(buf.String())
|
|
if !strings.Contains(out, "a:") || !strings.Contains(out, "\"b\"") {
|
|
t.Fatalf("expected colourised output to contain rendered tokens, got %q", out)
|
|
}
|
|
}
|
|
|
|
func TestKYamlEncoderWriteNodeAliasAndUnknown(t *testing.T) {
|
|
ke := NewKYamlEncoder(NewDefaultKYamlPreferences()).(*kyamlEncoder)
|
|
|
|
t.Run("alias_nil", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
err := ke.writeNode(&buf, &CandidateNode{Kind: AliasNode}, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if buf.String() != "null" {
|
|
t.Fatalf("expected null for nil alias, got %q", buf.String())
|
|
}
|
|
})
|
|
|
|
t.Run("alias_value", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
err := ke.writeNode(&buf, &CandidateNode{
|
|
Kind: AliasNode,
|
|
Alias: &CandidateNode{Kind: ScalarNode, Tag: "!!int", Value: "12"},
|
|
}, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if buf.String() != "12" {
|
|
t.Fatalf("expected dereferenced alias value, got %q", buf.String())
|
|
}
|
|
})
|
|
|
|
t.Run("unknown_kind", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
err := ke.writeNode(&buf, &CandidateNode{Kind: Kind(12345)}, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if buf.String() != "null" {
|
|
t.Fatalf("expected null for unknown kind, got %q", buf.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestKYamlEncoderEmptyCollections(t *testing.T) {
|
|
ke := NewKYamlEncoder(NewDefaultKYamlPreferences()).(*kyamlEncoder)
|
|
|
|
t.Run("empty_mapping", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
err := ke.writeNode(&buf, &CandidateNode{Kind: MappingNode}, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if buf.String() != "{}" {
|
|
t.Fatalf("expected empty mapping, got %q", buf.String())
|
|
}
|
|
})
|
|
|
|
t.Run("empty_sequence", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
err := ke.writeNode(&buf, &CandidateNode{Kind: SequenceNode}, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if buf.String() != "[]" {
|
|
t.Fatalf("expected empty sequence, got %q", buf.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestKYamlEncoderScalarFallbackAndEscaping(t *testing.T) {
|
|
ke := NewKYamlEncoder(NewDefaultKYamlPreferences()).(*kyamlEncoder)
|
|
|
|
t.Run("unknown_tag_falls_back_to_string", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
err := ke.writeNode(&buf, &CandidateNode{Kind: ScalarNode, Tag: "!!timestamp", Value: "2020-01-01T00:00:00Z"}, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if buf.String() != "\"2020-01-01T00:00:00Z\"" {
|
|
t.Fatalf("expected quoted fallback, got %q", buf.String())
|
|
}
|
|
})
|
|
|
|
t.Run("escape_double_quoted", func(t *testing.T) {
|
|
got := escapeDoubleQuotedString("a\\b\"c\n\r\t" + string(rune(0x01)))
|
|
want := "a\\\\b\\\"c\\n\\r\\t\\u0001"
|
|
if got != want {
|
|
t.Fatalf("expected %q, got %q", want, got)
|
|
}
|
|
})
|
|
|
|
t.Run("valid_bare_key", func(t *testing.T) {
|
|
if isValidKYamlBareKey("") {
|
|
t.Fatalf("expected empty string to be invalid")
|
|
}
|
|
if isValidKYamlBareKey("1a") {
|
|
t.Fatalf("expected leading digit to be invalid")
|
|
}
|
|
if !isValidKYamlBareKey("a_b-2") {
|
|
t.Fatalf("expected identifier-like key to be valid")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestKYamlEncoderCommentsInMapping(t *testing.T) {
|
|
prefs := NewDefaultKYamlPreferences()
|
|
prefs.UnwrapScalar = false
|
|
ke := NewKYamlEncoder(prefs).(*kyamlEncoder)
|
|
|
|
var buf bytes.Buffer
|
|
err := ke.writeNode(&buf, &CandidateNode{
|
|
Kind: MappingNode,
|
|
Content: []*CandidateNode{
|
|
{
|
|
Kind: ScalarNode,
|
|
Tag: "!!str",
|
|
Value: "a",
|
|
HeadComment: "key head",
|
|
LineComment: "key line",
|
|
FootComment: "key foot",
|
|
},
|
|
{
|
|
Kind: ScalarNode,
|
|
Tag: "!!str",
|
|
Value: "b",
|
|
HeadComment: "value head",
|
|
},
|
|
},
|
|
}, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
out := buf.String()
|
|
if !strings.Contains(out, "# key head\n") {
|
|
t.Fatalf("expected key head comment, got %q", out)
|
|
}
|
|
if !strings.Contains(out, "# value head\n") {
|
|
t.Fatalf("expected value head comment, got %q", out)
|
|
}
|
|
if !strings.Contains(out, ", # key line\n") {
|
|
t.Fatalf("expected inline key comment fallback, got %q", out)
|
|
}
|
|
if !strings.Contains(out, "# key foot\n") {
|
|
t.Fatalf("expected foot comment fallback, got %q", out)
|
|
}
|
|
}
|
|
|
|
func TestKYamlEncoderCommentBlockAndInlineComment(t *testing.T) {
|
|
ke := NewKYamlEncoder(NewDefaultKYamlPreferences()).(*kyamlEncoder)
|
|
|
|
t.Run("comment_block_prefixing_and_crlf", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
err := ke.writeCommentBlock(&buf, "line1\r\n\r\n# already\r\nline2", 2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := " # line1\n # already\n # line2\n"
|
|
if buf.String() != want {
|
|
t.Fatalf("expected %q, got %q", want, buf.String())
|
|
}
|
|
})
|
|
|
|
t.Run("inline_comment_prefix_and_first_line_only", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
err := ke.writeInlineComment(&buf, "hello\r\nsecond line")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if buf.String() != " # hello" {
|
|
t.Fatalf("expected %q, got %q", " # hello", buf.String())
|
|
}
|
|
})
|
|
|
|
t.Run("inline_comment_already_prefixed", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
err := ke.writeInlineComment(&buf, "# hello")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if buf.String() != " # hello" {
|
|
t.Fatalf("expected %q, got %q", " # hello", buf.String())
|
|
}
|
|
})
|
|
}
|