Fix corruption in templates that use title function (#3278)

This commit fixes data corruption in templates that use the title
function as it used a shared cases.Title when casers should not
be shared between goroutines. When templates that used the title
function were executed at the same time, data corruption would occur
in the text that was being cased. This is fixed using a separate
caser for each function call.

Add tests to assertDefaultFuncs are thread-safe

Signed-off-by: George Robinson <george.robinson@grafana.com>
This commit is contained in:
George Robinson 2023-03-07 10:37:39 +00:00 committed by GitHub
parent 6bb49b2f1c
commit 012dbc9d81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 65 additions and 4 deletions

View File

@ -170,9 +170,12 @@ type FuncMap map[string]interface{}
var DefaultFuncs = FuncMap{ var DefaultFuncs = FuncMap{
"toUpper": strings.ToUpper, "toUpper": strings.ToUpper,
"toLower": strings.ToLower, "toLower": strings.ToLower,
"title": func(text string) string {
// casers should not be shared between goroutines, instead
// create a new caser each time this function is called
return cases.Title(language.AmericanEnglish).String(text)
},
"trimSpace": strings.TrimSpace, "trimSpace": strings.TrimSpace,
"title": cases.Title(language.AmericanEnglish).String,
// join is equal to strings.Join but inverts the argument order // join is equal to strings.Join but inverts the argument order
// for easier pipelining in templates. // for easier pipelining in templates.
"join": func(sep string, s []string) string { "join": func(sep string, s []string) string {

View File

@ -16,6 +16,7 @@ package template
import ( import (
tmplhtml "html/template" tmplhtml "html/template"
"net/url" "net/url"
"sync"
"testing" "testing"
tmpltext "text/template" tmpltext "text/template"
"time" "time"
@ -465,3 +466,60 @@ func TestTemplateExpansionWithOptions(t *testing.T) {
}) })
} }
} }
// This test asserts that template functions are thread-safe.
func TestTemplateFuncs(t *testing.T) {
tmpl, err := FromGlobs([]string{})
require.NoError(t, err)
for _, tc := range []struct {
title string
in string
data interface{}
exp string
}{{
title: "Template using toUpper",
in: `{{ "abc" | toUpper }}`,
exp: "ABC",
}, {
title: "Template using toLower",
in: `{{ "ABC" | toLower }}`,
exp: "abc",
}, {
title: "Template using title",
in: `{{ "abc" | title }}`,
exp: "Abc",
}, {
title: "Template using trimSpace",
in: `{{ " abc " | trimSpace }}`,
exp: "abc",
}, {
title: "Template using join",
in: `{{ . | join "," }}`,
data: []string{"abc", "def"},
exp: "abc,def",
}, {
title: "Template using match",
in: `{{ match "[a-z]+" "abc" }}`,
exp: "true",
}, {
title: "Template using reReplaceAll",
in: `{{ reReplaceAll "ab" "AB" "abc" }}`,
exp: "ABc",
}} {
tc := tc
t.Run(tc.title, func(t *testing.T) {
wg := sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
got, err := tmpl.ExecuteTextString(tc.in, tc.data)
require.NoError(t, err)
require.Equal(t, tc.exp, got)
}()
}
wg.Wait()
})
}
}