Files
cli/templates/templates_test.go
Sebastiaan van Stijn e34a3422cc templates: make "join" work with non-string slices and map values
Add a custom join function that allows for non-string slices to be
joined, following the same rules as "fmt.Sprint", it will use the
fmt.Stringer interface if implemented, or "error" if the type has
an "Error()".

For maps, it joins the map-values, for example:

    docker image inspect --format '{{join .Config.Labels ", "}}' ubuntu
    24.04, ubuntu

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-26 18:35:58 +01:00

232 lines
5.0 KiB
Go

package templates
import (
"bytes"
"testing"
"text/template"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
// GitHub #32120
func TestParseJSONFunctions(t *testing.T) {
tm, err := Parse(`{{json .Ports}}`)
assert.NilError(t, err)
var b bytes.Buffer
assert.NilError(t, tm.Execute(&b, map[string]string{"Ports": "0.0.0.0:2->8/udp"}))
want := "\"0.0.0.0:2->8/udp\""
assert.Check(t, is.Equal(want, b.String()))
}
func TestParseStringFunctions(t *testing.T) {
tm, err := Parse(`{{join (split . ":") "/"}}`)
assert.NilError(t, err)
var b bytes.Buffer
assert.NilError(t, tm.Execute(&b, "text:with:colon"))
want := "text/with/colon"
assert.Check(t, is.Equal(want, b.String()))
}
func TestNewParse(t *testing.T) {
tm, err := New("foo").Parse("this is a {{ . }}")
assert.NilError(t, err)
var b bytes.Buffer
assert.NilError(t, tm.Execute(&b, "string"))
want := "this is a string"
assert.Check(t, is.Equal(want, b.String()))
}
func TestParseTruncateFunction(t *testing.T) {
source := "tupx5xzf6hvsrhnruz5cr8gwp"
testCases := []struct {
template string
expected string
}{
{
template: `{{truncate . 5}}`,
expected: "tupx5",
},
{
template: `{{truncate . 25}}`,
expected: "tupx5xzf6hvsrhnruz5cr8gwp",
},
{
template: `{{truncate . 30}}`,
expected: "tupx5xzf6hvsrhnruz5cr8gwp",
},
{
template: `{{pad . 3 3}}`,
expected: " tupx5xzf6hvsrhnruz5cr8gwp ",
},
}
for _, tc := range testCases {
tm, err := Parse(tc.template)
assert.NilError(t, err)
t.Run("Non Empty Source Test with template: "+tc.template, func(t *testing.T) {
var b bytes.Buffer
assert.NilError(t, tm.Execute(&b, source))
assert.Check(t, is.Equal(tc.expected, b.String()))
})
t.Run("Empty Source Test with template: "+tc.template, func(t *testing.T) {
var c bytes.Buffer
assert.NilError(t, tm.Execute(&c, ""))
assert.Check(t, is.Equal("", c.String()))
})
t.Run("Nil Source Test with template: "+tc.template, func(t *testing.T) {
var c bytes.Buffer
assert.Check(t, tm.Execute(&c, nil) != nil)
assert.Check(t, is.Equal("", c.String()))
})
}
}
func TestHeaderFunctions(t *testing.T) {
const source = "hello world"
tests := []struct {
doc string
template string
}{
{
doc: "json",
template: `{{ json .}}`,
},
{
doc: "split",
template: `{{ split . ","}}`,
},
{
doc: "join",
template: `{{ join . ","}}`,
},
{
doc: "title",
template: `{{ title .}}`,
},
{
doc: "lower",
template: `{{ lower .}}`,
},
{
doc: "upper",
template: `{{ upper .}}`,
},
{
doc: "truncate",
template: `{{ truncate . 2}}`,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
tmpl, err := New("").Funcs(HeaderFunctions).Parse(tc.template)
assert.NilError(t, err)
var b bytes.Buffer
assert.NilError(t, tmpl.Execute(&b, source))
// All header-functions are currently stubs, and don't modify the input.
expected := source
assert.Equal(t, expected, b.String())
})
}
}
type stringerString string
func (s stringerString) String() string {
return "stringer" + string(s)
}
type stringerAndError string
func (s stringerAndError) String() string {
return "stringer" + string(s)
}
func (s stringerAndError) Error() string {
return "error" + string(s)
}
func TestJoinElements(t *testing.T) {
tests := []struct {
doc string
data any
expOut string
expErr string
}{
{
doc: "nil",
data: nil,
expOut: `output: ""`,
},
{
doc: "non-slice",
data: "hello",
expOut: `output: "`,
expErr: `error calling join: expected slice, got string`,
},
{
doc: "structs",
data: []struct{ A, B string }{{"1", "2"}, {"3", "4"}},
expOut: `output: "{1 2}, {3 4}"`,
},
{
doc: "map with strings",
data: map[string]string{"A": "1", "B": "2", "C": "3"},
expOut: `output: "1, 2, 3"`,
},
{
doc: "map with stringers",
data: map[string]stringerString{"A": "1", "B": "2", "C": "3"},
expOut: `output: "stringer1, stringer2, stringer3"`,
},
{
doc: "map with errors",
data: []stringerAndError{"1", "2", "3"},
expOut: `output: "error1, error2, error3"`,
},
{
doc: "stringers",
data: []stringerString{"1", "2", "3"},
expOut: `output: "stringer1, stringer2, stringer3"`,
},
{
doc: "stringer with errors",
data: []stringerAndError{"1", "2", "3"},
expOut: `output: "error1, error2, error3"`,
},
{
doc: "slice of bools",
data: []bool{true, false, true},
expOut: `output: "true, false, true"`,
},
}
const formatStr = `output: "{{- join . ", " -}}"`
tmpl, err := New("my-template").Funcs(template.FuncMap{"join": joinElements}).Parse(formatStr)
assert.NilError(t, err)
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
var b bytes.Buffer
err := tmpl.Execute(&b, tc.data)
if tc.expErr != "" {
assert.ErrorContains(t, err, tc.expErr)
} else {
assert.NilError(t, err)
}
assert.Equal(t, b.String(), tc.expOut)
})
}
}