parent
9b5f786df8
commit
fd8b0fbf8a
37 changed files with 652 additions and 566 deletions
|
@ -4,7 +4,7 @@ parameters:
|
|||
defaults: &defaults
|
||||
resource_class: large
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser:1.22301.20401
|
||||
- image: bepsays/ci-hugoreleaser:1.22400.20000
|
||||
environment: &buildenv
|
||||
GOMODCACHE: /root/project/gomodcache
|
||||
version: 2
|
||||
|
@ -58,7 +58,7 @@ jobs:
|
|||
environment:
|
||||
<<: [*buildenv]
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22301.20401
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22400.20000
|
||||
steps:
|
||||
- *restore-cache
|
||||
- &attach-workspace
|
||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
|||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.x, 1.23.x]
|
||||
go-version: [1.23.x, 1.24.x]
|
||||
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
|
2
go.mod
2
go.mod
|
@ -170,4 +170,4 @@ require (
|
|||
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
|
||||
)
|
||||
|
||||
go 1.22.6
|
||||
go 1.23
|
||||
|
|
|
@ -16,7 +16,7 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
// The current is built with 6885bad7dd86880be6929c02085e5c7a67ff2887 go1.23.0
|
||||
// The current is built with 3901409b5d [release-branch.go1.24] go1.24.0
|
||||
// TODO(bep) preserve the staticcheck.conf file.
|
||||
fmt.Println("Forking ...")
|
||||
defer fmt.Println("Done ...")
|
||||
|
@ -216,6 +216,7 @@ func rewrite(filename, rule string) {
|
|||
}
|
||||
|
||||
func goimports(dir string) {
|
||||
// Needs go install golang.org/x/tools/cmd/goimports@latest
|
||||
cmf, _ := hexec.SafeCommand("goimports", "-w", dir)
|
||||
out, err := cmf.CombinedOutput()
|
||||
if err != nil {
|
||||
|
|
|
@ -37,12 +37,14 @@ const KnownEnv = `
|
|||
GOARCH
|
||||
GOARM
|
||||
GOARM64
|
||||
GOAUTH
|
||||
GOBIN
|
||||
GOCACHE
|
||||
GOCACHEPROG
|
||||
GOENV
|
||||
GOEXE
|
||||
GOEXPERIMENT
|
||||
GOFIPS140
|
||||
GOFLAGS
|
||||
GOGCCFLAGS
|
||||
GOHOSTARCH
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
//go:build go1.13 && !windows
|
||||
// +build go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
//go:build go1.13 && !windows
|
||||
// +build go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
|
@ -428,7 +428,7 @@ func TestStringer(t *testing.T) {
|
|||
if err := tmpl.Execute(b, s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expect := "string=3"
|
||||
var expect = "string=3"
|
||||
if b.String() != expect {
|
||||
t.Errorf("expected %q got %q", expect, b.String())
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
//go:build go1.13 && !windows
|
||||
// +build go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"maps"
|
||||
"regexp"
|
||||
|
||||
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||
|
@ -145,7 +146,7 @@ func (e *escaper) escape(c context, n parse.Node) context {
|
|||
return c
|
||||
case *parse.ContinueNode:
|
||||
c.n = n
|
||||
e.rangeContext.continues = append(e.rangeContext.breaks, c)
|
||||
e.rangeContext.continues = append(e.rangeContext.continues, c)
|
||||
return context{state: stateDead}
|
||||
case *parse.IfNode:
|
||||
return e.escapeBranch(c, &n.BranchNode, "if")
|
||||
|
@ -588,22 +589,14 @@ func (e *escaper) escapeListConditionally(c context, n *parse.ListNode, filter f
|
|||
e1 := makeEscaper(e.ns)
|
||||
e1.rangeContext = e.rangeContext
|
||||
// Make type inferences available to f.
|
||||
for k, v := range e.output {
|
||||
e1.output[k] = v
|
||||
}
|
||||
maps.Copy(e1.output, e.output)
|
||||
c = e1.escapeList(c, n)
|
||||
ok := filter != nil && filter(&e1, c)
|
||||
if ok {
|
||||
// Copy inferences and edits from e1 back into e.
|
||||
for k, v := range e1.output {
|
||||
e.output[k] = v
|
||||
}
|
||||
for k, v := range e1.derived {
|
||||
e.derived[k] = v
|
||||
}
|
||||
for k, v := range e1.called {
|
||||
e.called[k] = v
|
||||
}
|
||||
maps.Copy(e.output, e1.output)
|
||||
maps.Copy(e.derived, e1.derived)
|
||||
maps.Copy(e.called, e1.called)
|
||||
for k, v := range e1.actionNodeEdits {
|
||||
e.editActionNode(k, v)
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
//go:build go1.13 && !windows
|
||||
// +build go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
|
@ -944,6 +944,7 @@ func TestEscapeSet(t *testing.T) {
|
|||
t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
|
@ -1064,6 +1065,10 @@ func TestErrors(t *testing.T) {
|
|||
"{{range .Items}}<a{{if .X}}{{continue}}{{end}}>{{end}}",
|
||||
"z:1:29: at range loop continue: {{range}} branches end in different contexts",
|
||||
},
|
||||
{
|
||||
"{{range .Items}}{{if .X}}{{break}}{{end}}<a{{if .Y}}{{continue}}{{end}}>{{if .Z}}{{continue}}{{end}}{{end}}",
|
||||
"z:1:54: at range loop continue: {{range}} branches end in different contexts",
|
||||
},
|
||||
{
|
||||
"<a b=1 c={{.H}}",
|
||||
"z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd",
|
||||
|
@ -1193,6 +1198,7 @@ func TestErrors(t *testing.T) {
|
|||
// Check that we get the same error if we call Execute again.
|
||||
if err := tmpl.Execute(buf, nil); err == nil || err.Error() != got {
|
||||
t.Errorf("input=%q: unexpected error on second call %q", test.input, err)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.13
|
||||
// +build go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
|
@ -80,6 +83,7 @@ func Example() {
|
|||
// <div><strong>no rows</strong></div>
|
||||
// </body>
|
||||
// </html>
|
||||
|
||||
}
|
||||
|
||||
func Example_autoescaping() {
|
||||
|
@ -120,6 +124,7 @@ func Example_escape() {
|
|||
// \"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
|
||||
// \"Fran \u0026 Freddie\'s Diner\"32\u003Ctasty@example.com\u003E
|
||||
// %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
|
||||
|
||||
}
|
||||
|
||||
func ExampleTemplate_Delims() {
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.13
|
||||
// +build go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
|
|
|
@ -325,16 +325,12 @@ var execTests = []execTest{
|
|||
{"$.U.V", "{{$.U.V}}", "v", tVal, true},
|
||||
{"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true},
|
||||
{"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true},
|
||||
{
|
||||
"nested assignment",
|
||||
{"nested assignment",
|
||||
"{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}",
|
||||
"3", tVal, true,
|
||||
},
|
||||
{
|
||||
"nested assignment changes the last declaration",
|
||||
"3", tVal, true},
|
||||
{"nested assignment changes the last declaration",
|
||||
"{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}",
|
||||
"1", tVal, true,
|
||||
},
|
||||
"1", tVal, true},
|
||||
|
||||
// Type with String method.
|
||||
{"V{6666}.String()", "-{{.V0}}-", "-{6666}-", tVal, true}, // NOTE: -<6666>- in text/template
|
||||
|
@ -381,21 +377,15 @@ var execTests = []execTest{
|
|||
{".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: <nil>-", tVal, true},
|
||||
{".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: <nil>-", tVal, true},
|
||||
{"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true},
|
||||
{
|
||||
"method on chained var",
|
||||
{"method on chained var",
|
||||
"{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
|
||||
"true", tVal, true,
|
||||
},
|
||||
{
|
||||
"chained method",
|
||||
"true", tVal, true},
|
||||
{"chained method",
|
||||
"{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
|
||||
"true", tVal, true,
|
||||
},
|
||||
{
|
||||
"chained method on variable",
|
||||
"true", tVal, true},
|
||||
{"chained method on variable",
|
||||
"{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}",
|
||||
"true", tVal, true,
|
||||
},
|
||||
"true", tVal, true},
|
||||
{".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true},
|
||||
{".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true},
|
||||
{"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true},
|
||||
|
@ -481,14 +471,10 @@ var execTests = []execTest{
|
|||
{"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true},
|
||||
|
||||
// HTML.
|
||||
{
|
||||
"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
|
||||
"<script>alert("XSS");</script>", nil, true,
|
||||
},
|
||||
{
|
||||
"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
|
||||
"<script>alert("XSS");</script>", nil, true,
|
||||
},
|
||||
{"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
|
||||
"<script>alert("XSS");</script>", nil, true},
|
||||
{"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
|
||||
"<script>alert("XSS");</script>", nil, true},
|
||||
{"html", `{{html .PS}}`, "a string", tVal, true},
|
||||
{"html typed nil", `{{html .NIL}}`, "<nil>", tVal, true},
|
||||
{"html untyped nil", `{{html .Empty0}}`, "<nil>", tVal, true}, // NOTE: "<no value>" in text/template
|
||||
|
@ -854,7 +840,7 @@ var delimPairs = []string{
|
|||
|
||||
func TestDelims(t *testing.T) {
|
||||
const hello = "Hello, world"
|
||||
value := struct{ Str string }{hello}
|
||||
var value = struct{ Str string }{hello}
|
||||
for i := 0; i < len(delimPairs); i += 2 {
|
||||
text := ".Str"
|
||||
left := delimPairs[i+0]
|
||||
|
@ -877,7 +863,7 @@ func TestDelims(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("delim %q text %q parse err %s", left, text, err)
|
||||
}
|
||||
b := new(strings.Builder)
|
||||
var b = new(strings.Builder)
|
||||
err = tmpl.Execute(b, value)
|
||||
if err != nil {
|
||||
t.Fatalf("delim %q exec err %s", left, err)
|
||||
|
@ -978,7 +964,7 @@ const treeTemplate = `
|
|||
`
|
||||
|
||||
func TestTree(t *testing.T) {
|
||||
tree := &Tree{
|
||||
var tree = &Tree{
|
||||
1,
|
||||
&Tree{
|
||||
2, &Tree{
|
||||
|
@ -1229,7 +1215,7 @@ var cmpTests = []cmpTest{
|
|||
|
||||
func TestComparison(t *testing.T) {
|
||||
b := new(strings.Builder)
|
||||
cmpStruct := struct {
|
||||
var cmpStruct = struct {
|
||||
Uthree, Ufour uint
|
||||
NegOne, Three int
|
||||
Ptr, NilPtr *int
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
//go:build go1.13 && !windows
|
||||
// +build go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"fmt"
|
||||
htmltemplate "html/template"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
@ -145,6 +146,8 @@ func indirectToJSONMarshaler(a any) any {
|
|||
return v.Interface()
|
||||
}
|
||||
|
||||
var scriptTagRe = regexp.MustCompile("(?i)<(/?)script")
|
||||
|
||||
// jsValEscaper escapes its inputs to a JS Expression (section 11.14) that has
|
||||
// neither side-effects nor free variables outside (NaN, Infinity).
|
||||
func jsValEscaper(args ...any) string {
|
||||
|
@ -182,9 +185,9 @@ func jsValEscaper(args ...any) string {
|
|||
// In particular we:
|
||||
// * replace "*/" comment end tokens with "* /", which does not
|
||||
// terminate the comment
|
||||
// * replace "</script" with "\x3C/script", and "<!--" with
|
||||
// "\x3C!--", which prevents confusing script block termination
|
||||
// semantics
|
||||
// * replace "<script" and "</script" with "\x3Cscript" and "\x3C/script"
|
||||
// (case insensitively), and "<!--" with "\x3C!--", which prevents
|
||||
// confusing script block termination semantics
|
||||
//
|
||||
// We also put a space before the comment so that if it is flush against
|
||||
// a division operator it is not turned into a line comment:
|
||||
|
@ -193,8 +196,8 @@ func jsValEscaper(args ...any) string {
|
|||
// x//* error marshaling y:
|
||||
// second line of error message */null
|
||||
errStr := err.Error()
|
||||
errStr = string(scriptTagRe.ReplaceAll([]byte(errStr), []byte(`\x3C${1}script`)))
|
||||
errStr = strings.ReplaceAll(errStr, "*/", "* /")
|
||||
errStr = strings.ReplaceAll(errStr, "</script", `\x3C/script`)
|
||||
errStr = strings.ReplaceAll(errStr, "<!--", `\x3C!--`)
|
||||
return fmt.Sprintf(" /* %s */null ", errStr)
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
//go:build go1.13 && !windows
|
||||
// +build go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
|
@ -110,7 +110,7 @@ func TestNextJsCtx(t *testing.T) {
|
|||
type jsonErrType struct{}
|
||||
|
||||
func (e *jsonErrType) MarshalJSON() ([]byte, error) {
|
||||
return nil, errors.New("beep */ boop </script blip <!--")
|
||||
return nil, errors.New("a */ b <script c </script d <!-- e <sCrIpT f </sCrIpT")
|
||||
}
|
||||
|
||||
func TestJSValEscaper(t *testing.T) {
|
||||
|
@ -163,7 +163,7 @@ func TestJSValEscaper(t *testing.T) {
|
|||
{"</script", `"\u003c/script"`, false},
|
||||
{"\U0001D11E", "\"\U0001D11E\"", false}, // or "\uD834\uDD1E"
|
||||
{nil, " null ", false},
|
||||
{&jsonErrType{}, " /* json: error calling MarshalJSON for type *template.jsonErrType: beep * / boop \\x3C/script blip \\x3C!-- */null ", true},
|
||||
{&jsonErrType{}, " /* json: error calling MarshalJSON for type *template.jsonErrType: a * / b \\x3Cscript c \\x3C/script d \\x3C!-- e \\x3Cscript f \\x3C/script */null ", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
@ -221,8 +221,7 @@ func TestJSStrEscaper(t *testing.T) {
|
|||
{"<!--", `\u003c!--`},
|
||||
{"-->", `--\u003e`},
|
||||
// From https://code.google.com/p/doctype/wiki/ArticleUtf7
|
||||
{
|
||||
"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
|
||||
{"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
|
||||
`\u002bADw-script\u002bAD4-alert(1)\u002bADw-\/script\u002bAD4-`,
|
||||
},
|
||||
// Invalid UTF-8 sequence
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
// Tests for multiple-template execution, copied from text/template.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
//go:build go1.13 && !windows
|
||||
// +build go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
|
@ -268,7 +268,7 @@ func TestIssue19294(t *testing.T) {
|
|||
// by the contents of "stylesheet", but if the internal map associating
|
||||
// names with templates is built in the wrong order, the empty block
|
||||
// looks non-empty and this doesn't happen.
|
||||
inlined := map[string]string{
|
||||
var inlined = map[string]string{
|
||||
"stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
|
||||
"xhtml": `{{block "stylesheet" .}}{{end}}`,
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.13
|
||||
// +build go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
|
@ -15,6 +18,7 @@ import (
|
|||
)
|
||||
|
||||
func TestTemplateClone(t *testing.T) {
|
||||
|
||||
orig := New("name")
|
||||
clone, err := orig.Clone()
|
||||
if err != nil {
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
//go:build go1.13 && !windows
|
||||
// +build go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
|
@ -43,6 +43,7 @@ func TestFindEndTag(t *testing.T) {
|
|||
}
|
||||
|
||||
func BenchmarkTemplateSpecialTags(b *testing.B) {
|
||||
|
||||
r := struct {
|
||||
Name, Gift string
|
||||
}{"Aunt Mildred", "bone china tea set"}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
//go:build go1.13 && !windows
|
||||
// +build go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
|
|
|
@ -31,20 +31,17 @@ import (
|
|||
// If exec is not supported, testenv.SyscallIsNotSupported will return true
|
||||
// for the resulting error.
|
||||
func MustHaveExec(t testing.TB) {
|
||||
tryExecOnce.Do(func() {
|
||||
tryExecErr = tryExec()
|
||||
})
|
||||
if tryExecErr != nil {
|
||||
t.Skipf("skipping test: cannot exec subprocess on %s/%s: %v", runtime.GOOS, runtime.GOARCH, tryExecErr)
|
||||
if err := tryExec(); err != nil {
|
||||
msg := fmt.Sprintf("cannot exec subprocess on %s/%s: %v", runtime.GOOS, runtime.GOARCH, err)
|
||||
if t == nil {
|
||||
panic(msg)
|
||||
}
|
||||
t.Helper()
|
||||
t.Skip("skipping test:", msg)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
tryExecOnce sync.Once
|
||||
tryExecErr error
|
||||
)
|
||||
|
||||
func tryExec() error {
|
||||
var tryExec = sync.OnceValue(func() error {
|
||||
switch runtime.GOOS {
|
||||
case "wasip1", "js", "ios":
|
||||
default:
|
||||
|
@ -70,15 +67,37 @@ func tryExec() error {
|
|||
|
||||
// We know that this is a test executable. We should be able to run it with a
|
||||
// no-op flag to check for overall exec support.
|
||||
exe, err := os.Executable()
|
||||
exe, err := exePath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't probe for exec support: %w", err)
|
||||
}
|
||||
cmd := exec.Command(exe, "-test.list=^$")
|
||||
cmd.Env = origEnv
|
||||
return cmd.Run()
|
||||
})
|
||||
|
||||
// Executable is a wrapper around [MustHaveExec] and [os.Executable].
|
||||
// It returns the path name for the executable that started the current process,
|
||||
// or skips the test if the current system can't start new processes,
|
||||
// or fails the test if the path can not be obtained.
|
||||
func Executable(t testing.TB) string {
|
||||
MustHaveExec(t)
|
||||
|
||||
exe, err := exePath()
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("os.Executable error: %v", err)
|
||||
if t == nil {
|
||||
panic(msg)
|
||||
}
|
||||
t.Fatal(msg)
|
||||
}
|
||||
return exe
|
||||
}
|
||||
|
||||
var exePath = sync.OnceValues(func() (string, error) {
|
||||
return os.Executable()
|
||||
})
|
||||
|
||||
var execPaths sync.Map // path -> error
|
||||
|
||||
// MustHaveExecPath checks that the current system can start the named executable
|
||||
|
@ -93,6 +112,7 @@ func MustHaveExecPath(t testing.TB, path string) {
|
|||
err, _ = execPaths.LoadOrStore(path, err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Helper()
|
||||
t.Skipf("skipping test: %s: %s", path, err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ package testenv
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
@ -43,15 +42,22 @@ func Builder() string {
|
|||
|
||||
// HasGoBuild reports whether the current system can build programs with “go build”
|
||||
// and then run them with os.StartProcess or exec.Command.
|
||||
// Modified by Hugo (not needed)
|
||||
func HasGoBuild() bool {
|
||||
return false
|
||||
if os.Getenv("GO_GCFLAGS") != "" {
|
||||
// It's too much work to require every caller of the go command
|
||||
// to pass along "-gcflags="+os.Getenv("GO_GCFLAGS").
|
||||
// For now, if $GO_GCFLAGS is set, report that we simply can't
|
||||
// run go build.
|
||||
return false
|
||||
}
|
||||
|
||||
return tryGoBuild() == nil
|
||||
}
|
||||
|
||||
var (
|
||||
goBuildOnce sync.Once
|
||||
goBuildErr error
|
||||
)
|
||||
var tryGoBuild = sync.OnceValue(func() error {
|
||||
// Removed by Hugo, not used.
|
||||
return nil
|
||||
})
|
||||
|
||||
// MustHaveGoBuild checks that the current system can build programs with “go build”
|
||||
// and then run them with os.StartProcess or exec.Command.
|
||||
|
@ -63,7 +69,7 @@ func MustHaveGoBuild(t testing.TB) {
|
|||
}
|
||||
if !HasGoBuild() {
|
||||
t.Helper()
|
||||
t.Skipf("skipping test: 'go build' unavailable: %v", goBuildErr)
|
||||
t.Skipf("skipping test: 'go build' unavailable: %v", tryGoBuild())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,6 +83,7 @@ func HasGoRun() bool {
|
|||
// If not, MustHaveGoRun calls t.Skip with an explanation.
|
||||
func MustHaveGoRun(t testing.TB) {
|
||||
if !HasGoRun() {
|
||||
t.Helper()
|
||||
t.Skipf("skipping test: 'go run' not available on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
|
@ -96,6 +103,7 @@ func HasParallelism() bool {
|
|||
// threads in parallel. If not, MustHaveParallelism calls t.Skip with an explanation.
|
||||
func MustHaveParallelism(t testing.TB) {
|
||||
if !HasParallelism() {
|
||||
t.Helper()
|
||||
t.Skipf("skipping test: no parallelism available on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
|
@ -119,82 +127,67 @@ func GoToolPath(t testing.TB) string {
|
|||
return path
|
||||
}
|
||||
|
||||
var (
|
||||
gorootOnce sync.Once
|
||||
gorootPath string
|
||||
gorootErr error
|
||||
)
|
||||
var findGOROOT = sync.OnceValues(func() (path string, err error) {
|
||||
if path := runtime.GOROOT(); path != "" {
|
||||
// If runtime.GOROOT() is non-empty, assume that it is valid.
|
||||
//
|
||||
// (It might not be: for example, the user may have explicitly set GOROOT
|
||||
// to the wrong directory. But this case is
|
||||
// rare, and if that happens the user can fix what they broke.)
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func findGOROOT() (string, error) {
|
||||
gorootOnce.Do(func() {
|
||||
gorootPath = runtime.GOROOT()
|
||||
if gorootPath != "" {
|
||||
// If runtime.GOROOT() is non-empty, assume that it is valid.
|
||||
//
|
||||
// (It might not be: for example, the user may have explicitly set GOROOT
|
||||
// to the wrong directory. But this case is
|
||||
// rare, and if that happens the user can fix what they broke.)
|
||||
return
|
||||
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
|
||||
// binary was built with -trimpath).
|
||||
//
|
||||
// Since this is internal/testenv, we can cheat and assume that the caller
|
||||
// is a test of some package in a subdirectory of GOROOT/src. ('go test'
|
||||
// runs the test in the directory containing the packaged under test.) That
|
||||
// means that if we start walking up the tree, we should eventually find
|
||||
// GOROOT/src/go.mod, and we can report the parent directory of that.
|
||||
//
|
||||
// Notably, this works even if we can't run 'go env GOROOT' as a
|
||||
// subprocess.
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("finding GOROOT: %w", err)
|
||||
}
|
||||
|
||||
dir := cwd
|
||||
for {
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
// dir is either "." or only a volume name.
|
||||
return "", fmt.Errorf("failed to locate GOROOT/src in any parent directory")
|
||||
}
|
||||
|
||||
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
|
||||
// binary was built with -trimpath).
|
||||
//
|
||||
// Since this is internal/testenv, we can cheat and assume that the caller
|
||||
// is a test of some package in a subdirectory of GOROOT/src. ('go test'
|
||||
// runs the test in the directory containing the packaged under test.) That
|
||||
// means that if we start walking up the tree, we should eventually find
|
||||
// GOROOT/src/go.mod, and we can report the parent directory of that.
|
||||
//
|
||||
// Notably, this works even if we can't run 'go env GOROOT' as a
|
||||
// subprocess.
|
||||
if base := filepath.Base(dir); base != "src" {
|
||||
dir = parent
|
||||
continue // dir cannot be GOROOT/src if it doesn't end in "src".
|
||||
}
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
b, err := os.ReadFile(filepath.Join(dir, "go.mod"))
|
||||
if err != nil {
|
||||
gorootErr = fmt.Errorf("finding GOROOT: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
dir := cwd
|
||||
for {
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
// dir is either "." or only a volume name.
|
||||
gorootErr = fmt.Errorf("failed to locate GOROOT/src in any parent directory")
|
||||
return
|
||||
}
|
||||
|
||||
if base := filepath.Base(dir); base != "src" {
|
||||
if os.IsNotExist(err) {
|
||||
dir = parent
|
||||
continue // dir cannot be GOROOT/src if it doesn't end in "src".
|
||||
continue
|
||||
}
|
||||
return "", fmt.Errorf("finding GOROOT: %w", err)
|
||||
}
|
||||
goMod := string(b)
|
||||
|
||||
b, err := os.ReadFile(filepath.Join(dir, "go.mod"))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
dir = parent
|
||||
continue
|
||||
}
|
||||
gorootErr = fmt.Errorf("finding GOROOT: %w", err)
|
||||
return
|
||||
}
|
||||
goMod := string(b)
|
||||
|
||||
for goMod != "" {
|
||||
var line string
|
||||
line, goMod, _ = strings.Cut(goMod, "\n")
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 && fields[0] == "module" && fields[1] == "std" {
|
||||
// Found "module std", which is the module declaration in GOROOT/src!
|
||||
gorootPath = parent
|
||||
return
|
||||
}
|
||||
for goMod != "" {
|
||||
var line string
|
||||
line, goMod, _ = strings.Cut(goMod, "\n")
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 && fields[0] == "module" && fields[1] == "std" {
|
||||
// Found "module std", which is the module declaration in GOROOT/src!
|
||||
return parent, nil
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return gorootPath, gorootErr
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// GOROOT reports the path to the directory containing the root of the Go
|
||||
// project source tree. This is normally equivalent to runtime.GOROOT, but
|
||||
|
@ -217,28 +210,22 @@ func GOROOT(t testing.TB) string {
|
|||
|
||||
// GoTool reports the path to the Go tool.
|
||||
func GoTool() (string, error) {
|
||||
if !HasGoBuild() {
|
||||
return "", errors.New("platform cannot run go tool")
|
||||
}
|
||||
goToolOnce.Do(func() {
|
||||
goToolPath, goToolErr = exec.LookPath("go")
|
||||
})
|
||||
return goToolPath, goToolErr
|
||||
// Removed by Hugo, not used.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var (
|
||||
goToolOnce sync.Once
|
||||
goToolPath string
|
||||
goToolErr error
|
||||
)
|
||||
var goTool = sync.OnceValues(func() (string, error) {
|
||||
return exec.LookPath("go")
|
||||
})
|
||||
|
||||
// HasSrc reports whether the entire source tree is available under GOROOT.
|
||||
func HasSrc() bool {
|
||||
// MustHaveSource checks that the entire source tree is available under GOROOT.
|
||||
// If not, it calls t.Skip with an explanation.
|
||||
func MustHaveSource(t testing.TB) {
|
||||
switch runtime.GOOS {
|
||||
case "ios":
|
||||
return false
|
||||
t.Helper()
|
||||
t.Skip("skipping test: no source tree on " + runtime.GOOS)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// HasExternalNetwork reports whether the current system can use
|
||||
|
@ -263,41 +250,39 @@ func MustHaveExternalNetwork(t testing.TB) {
|
|||
|
||||
// HasCGO reports whether the current system can use cgo.
|
||||
func HasCGO() bool {
|
||||
hasCgoOnce.Do(func() {
|
||||
goTool, err := GoTool()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(goTool, "env", "CGO_ENABLED")
|
||||
cmd.Env = origEnv
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("%v: %v", cmd, out))
|
||||
}
|
||||
hasCgo, err = strconv.ParseBool(string(bytes.TrimSpace(out)))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("%v: non-boolean output %q", cmd, out))
|
||||
}
|
||||
})
|
||||
return hasCgo
|
||||
return hasCgo()
|
||||
}
|
||||
|
||||
var (
|
||||
hasCgoOnce sync.Once
|
||||
hasCgo bool
|
||||
)
|
||||
var hasCgo = sync.OnceValue(func() bool {
|
||||
goTool, err := goTool()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
cmd := exec.Command(goTool, "env", "CGO_ENABLED")
|
||||
cmd.Env = origEnv
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("%v: %v", cmd, out))
|
||||
}
|
||||
ok, err := strconv.ParseBool(string(bytes.TrimSpace(out)))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("%v: non-boolean output %q", cmd, out))
|
||||
}
|
||||
return ok
|
||||
})
|
||||
|
||||
// MustHaveCGO calls t.Skip if cgo is not available.
|
||||
func MustHaveCGO(t testing.TB) {
|
||||
if !HasCGO() {
|
||||
t.Helper()
|
||||
t.Skipf("skipping test: no cgo")
|
||||
}
|
||||
}
|
||||
|
||||
// CanInternalLink reports whether the current system can link programs with
|
||||
// internal linking.
|
||||
// Modified by Hugo (not needed)
|
||||
func CanInternalLink(withCgo bool) bool {
|
||||
// Removed by Hugo, not used.
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -306,6 +291,7 @@ func CanInternalLink(withCgo bool) bool {
|
|||
// If not, MustInternalLink calls t.Skip with an explanation.
|
||||
func MustInternalLink(t testing.TB, withCgo bool) {
|
||||
if !CanInternalLink(withCgo) {
|
||||
t.Helper()
|
||||
if withCgo && CanInternalLink(false) {
|
||||
t.Skipf("skipping test: internal linking on %s/%s is not supported with cgo", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
@ -316,15 +302,15 @@ func MustInternalLink(t testing.TB, withCgo bool) {
|
|||
// MustInternalLinkPIE checks whether the current system can link PIE binary using
|
||||
// internal linking.
|
||||
// If not, MustInternalLinkPIE calls t.Skip with an explanation.
|
||||
// Modified by Hugo (not needed)
|
||||
func MustInternalLinkPIE(t testing.TB) {
|
||||
// Removed by Hugo, not used.
|
||||
}
|
||||
|
||||
// MustHaveBuildMode reports whether the current system can build programs in
|
||||
// the given build mode.
|
||||
// If not, MustHaveBuildMode calls t.Skip with an explanation.
|
||||
// Modified by Hugo (not needed)
|
||||
func MustHaveBuildMode(t testing.TB, buildmode string) {
|
||||
// Removed by Hugo, not used.
|
||||
}
|
||||
|
||||
// HasSymlink reports whether the current system can use os.Symlink.
|
||||
|
@ -338,6 +324,7 @@ func HasSymlink() bool {
|
|||
func MustHaveSymlink(t testing.TB) {
|
||||
ok, reason := hasSymlink()
|
||||
if !ok {
|
||||
t.Helper()
|
||||
t.Skipf("skipping test: cannot make symlinks on %s/%s: %s", runtime.GOOS, runtime.GOARCH, reason)
|
||||
}
|
||||
}
|
||||
|
@ -354,6 +341,7 @@ func HasLink() bool {
|
|||
// If not, MustHaveLink calls t.Skip with an explanation.
|
||||
func MustHaveLink(t testing.TB) {
|
||||
if !HasLink() {
|
||||
t.Helper()
|
||||
t.Skipf("skipping test: hardlinks are not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
|
@ -361,15 +349,15 @@ func MustHaveLink(t testing.TB) {
|
|||
var flaky = flag.Bool("flaky", false, "run known-flaky tests too")
|
||||
|
||||
func SkipFlaky(t testing.TB, issue int) {
|
||||
t.Helper()
|
||||
if !*flaky {
|
||||
t.Helper()
|
||||
t.Skipf("skipping known flaky test without the -flaky flag; see golang.org/issue/%d", issue)
|
||||
}
|
||||
}
|
||||
|
||||
func SkipFlakyNet(t testing.TB) {
|
||||
t.Helper()
|
||||
if v, _ := strconv.ParseBool(os.Getenv("GO_BUILDER_FLAKY_NET")); v {
|
||||
t.Helper()
|
||||
t.Skip("skipping test on builder known to have frequent network failures")
|
||||
}
|
||||
}
|
||||
|
@ -455,6 +443,6 @@ func SyscallIsNotSupported(err error) bool {
|
|||
// ParallelOn64Bit calls t.Parallel() unless there is a case that cannot be parallel.
|
||||
// This function should be used when it is necessary to avoid t.Parallel on
|
||||
// 32-bit machines, typically because the test uses lots of memory.
|
||||
// Disabled by Hugo.
|
||||
func ParallelOn64Bit(t *testing.T) {
|
||||
// Removed by Hugo, not used.
|
||||
}
|
||||
|
|
|
@ -11,9 +11,10 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func hasSymlink() (ok bool, reason string) {
|
||||
var hasSymlink = sync.OnceValues(func() (ok bool, reason string) {
|
||||
switch runtime.GOOS {
|
||||
case "plan9":
|
||||
return false, ""
|
||||
|
@ -43,4 +44,4 @@ func hasSymlink() (ok bool, reason string) {
|
|||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
})
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
)
|
||||
|
||||
func TestGoToolLocation(t *testing.T) {
|
||||
t.Skip("This test is not relevant for Hugo")
|
||||
testenv.MustHaveGoBuild(t)
|
||||
|
||||
var exeSuffix string
|
||||
|
@ -54,8 +55,83 @@ func TestGoToolLocation(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Modified by Hugo (not needed)
|
||||
func TestHasGoBuild(t *testing.T) {
|
||||
if !testenv.HasGoBuild() {
|
||||
switch runtime.GOOS {
|
||||
case "js", "wasip1":
|
||||
// No exec syscall, so these shouldn't be able to 'go build'.
|
||||
t.Logf("HasGoBuild is false on %s", runtime.GOOS)
|
||||
return
|
||||
}
|
||||
|
||||
b := testenv.Builder()
|
||||
if b == "" {
|
||||
// We shouldn't make assumptions about what kind of sandbox or build
|
||||
// environment external Go users may be running in.
|
||||
t.Skipf("skipping: 'go build' unavailable")
|
||||
}
|
||||
|
||||
// Since we control the Go builders, we know which ones ought
|
||||
// to be able to run 'go build'. Check that they can.
|
||||
//
|
||||
// (Note that we don't verify that any builders *can't* run 'go build'.
|
||||
// If a builder starts running 'go build' tests when it shouldn't,
|
||||
// we will presumably find out about it when those tests fail.)
|
||||
switch runtime.GOOS {
|
||||
case "ios":
|
||||
if isCorelliumBuilder(b) {
|
||||
// The corellium environment is self-hosting, so it should be able
|
||||
// to build even though real "ios" devices can't exec.
|
||||
} else {
|
||||
// The usual iOS sandbox does not allow the app to start another
|
||||
// process. If we add builders on stock iOS devices, they presumably
|
||||
// will not be able to exec, so we may as well allow that now.
|
||||
t.Logf("HasGoBuild is false on %s", b)
|
||||
return
|
||||
}
|
||||
case "android":
|
||||
panic("Removed by Hugo, should not be used")
|
||||
}
|
||||
|
||||
if strings.Contains(b, "-noopt") {
|
||||
// The -noopt builder sets GO_GCFLAGS, which causes tests of 'go build' to
|
||||
// be skipped.
|
||||
t.Logf("HasGoBuild is false on %s", b)
|
||||
return
|
||||
}
|
||||
|
||||
t.Fatalf("HasGoBuild unexpectedly false on %s", b)
|
||||
}
|
||||
|
||||
t.Logf("HasGoBuild is true; checking consistency with other functions")
|
||||
|
||||
hasExec := false
|
||||
hasExecGo := false
|
||||
t.Run("MustHaveExec", func(t *testing.T) {
|
||||
testenv.MustHaveExec(t)
|
||||
hasExec = true
|
||||
})
|
||||
t.Run("MustHaveExecPath", func(t *testing.T) {
|
||||
testenv.MustHaveExecPath(t, "go")
|
||||
hasExecGo = true
|
||||
})
|
||||
if !hasExec {
|
||||
t.Errorf(`MustHaveExec(t) skipped unexpectedly`)
|
||||
}
|
||||
if !hasExecGo {
|
||||
t.Errorf(`MustHaveExecPath(t, "go") skipped unexpectedly`)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
mainGo := filepath.Join(dir, "main.go")
|
||||
if err := os.WriteFile(mainGo, []byte("package main\nfunc main() {}\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := testenv.Command(t, "go", "build", "-o", os.DevNull, mainGo)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("%v: %v\n%s", cmd, err, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustHaveExec(t *testing.T) {
|
||||
|
|
|
@ -5,16 +5,14 @@
|
|||
package testenv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var symlinkOnce sync.Once
|
||||
var winSymlinkErr error
|
||||
|
||||
func initWinHasSymlink() {
|
||||
var hasSymlink = sync.OnceValues(func() (bool, string) {
|
||||
tmpdir, err := os.MkdirTemp("", "symtest")
|
||||
if err != nil {
|
||||
panic("failed to create temp directory: " + err.Error())
|
||||
|
@ -22,26 +20,13 @@ func initWinHasSymlink() {
|
|||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
err = os.Symlink("target", filepath.Join(tmpdir, "symlink"))
|
||||
if err != nil {
|
||||
err = err.(*os.LinkError).Err
|
||||
switch err {
|
||||
case syscall.EWINDOWS, syscall.ERROR_PRIVILEGE_NOT_HELD:
|
||||
winSymlinkErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasSymlink() (ok bool, reason string) {
|
||||
symlinkOnce.Do(initWinHasSymlink)
|
||||
|
||||
switch winSymlinkErr {
|
||||
case nil:
|
||||
switch {
|
||||
case err == nil:
|
||||
return true, ""
|
||||
case syscall.EWINDOWS:
|
||||
case errors.Is(err, syscall.EWINDOWS):
|
||||
return false, ": symlinks are not supported on your version of Windows"
|
||||
case syscall.ERROR_PRIVILEGE_NOT_HELD:
|
||||
case errors.Is(err, syscall.ERROR_PRIVILEGE_NOT_HELD):
|
||||
return false, ": you don't have enough privileges to create symlinks"
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
})
|
||||
|
|
|
@ -98,7 +98,8 @@ data, defined in detail in the corresponding sections that follow.
|
|||
{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
|
||||
|
||||
{{range pipeline}} T1 {{end}}
|
||||
The value of the pipeline must be an array, slice, map, or channel.
|
||||
The value of the pipeline must be an array, slice, map, iter.Seq,
|
||||
iter.Seq2, integer or channel.
|
||||
If the value of the pipeline has length zero, nothing is output;
|
||||
otherwise, dot is set to the successive elements of the array,
|
||||
slice, or map and T1 is executed. If the value is a map and the
|
||||
|
@ -106,7 +107,8 @@ data, defined in detail in the corresponding sections that follow.
|
|||
visited in sorted key order.
|
||||
|
||||
{{range pipeline}} T1 {{else}} T0 {{end}}
|
||||
The value of the pipeline must be an array, slice, map, or channel.
|
||||
The value of the pipeline must be an array, slice, map, iter.Seq,
|
||||
iter.Seq2, integer or channel.
|
||||
If the value of the pipeline has length zero, dot is unaffected and
|
||||
T0 is executed; otherwise, dot is set to the successive elements
|
||||
of the array, slice, or map and T1 is executed.
|
||||
|
@ -162,37 +164,55 @@ An argument is a simple value, denoted by one of the following.
|
|||
the host machine's ints are 32 or 64 bits.
|
||||
- The keyword nil, representing an untyped Go nil.
|
||||
- The character '.' (period):
|
||||
|
||||
.
|
||||
|
||||
The result is the value of dot.
|
||||
- A variable name, which is a (possibly empty) alphanumeric string
|
||||
preceded by a dollar sign, such as
|
||||
|
||||
$piOver2
|
||||
|
||||
or
|
||||
|
||||
$
|
||||
|
||||
The result is the value of the variable.
|
||||
Variables are described below.
|
||||
- The name of a field of the data, which must be a struct, preceded
|
||||
by a period, such as
|
||||
|
||||
.Field
|
||||
|
||||
The result is the value of the field. Field invocations may be
|
||||
chained:
|
||||
|
||||
.Field1.Field2
|
||||
|
||||
Fields can also be evaluated on variables, including chaining:
|
||||
|
||||
$x.Field1.Field2
|
||||
- The name of a key of the data, which must be a map, preceded
|
||||
by a period, such as
|
||||
|
||||
.Key
|
||||
|
||||
The result is the map element value indexed by the key.
|
||||
Key invocations may be chained and combined with fields to any
|
||||
depth:
|
||||
|
||||
.Field1.Key1.Field2.Key2
|
||||
|
||||
Although the key must be an alphanumeric identifier, unlike with
|
||||
field names they do not need to start with an upper case letter.
|
||||
Keys can also be evaluated on variables, including chaining:
|
||||
|
||||
$x.key1.key2
|
||||
- The name of a niladic method of the data, preceded by a period,
|
||||
such as
|
||||
|
||||
.Method
|
||||
|
||||
The result is the value of invoking the method with dot as the
|
||||
receiver, dot.Method(). Such a method must have one return value (of
|
||||
any type) or two return values, the second of which is an error.
|
||||
|
@ -200,16 +220,22 @@ An argument is a simple value, denoted by one of the following.
|
|||
and an error is returned to the caller as the value of Execute.
|
||||
Method invocations may be chained and combined with fields and keys
|
||||
to any depth:
|
||||
|
||||
.Field1.Key1.Method1.Field2.Key2.Method2
|
||||
|
||||
Methods can also be evaluated on variables, including chaining:
|
||||
|
||||
$x.Method1.Field
|
||||
- The name of a niladic function, such as
|
||||
|
||||
fun
|
||||
|
||||
The result is the value of invoking the function, fun(). The return
|
||||
types and values behave as in methods. Functions and function
|
||||
names are described below.
|
||||
- A parenthesized instance of one the above, for grouping. The result
|
||||
may be accessed by a field or map key invocation.
|
||||
|
||||
print (.F1 arg1) (.F2 arg2)
|
||||
(.StructValuedMethod "arg").Field
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ Josie
|
|||
Name, Gift string
|
||||
Attended bool
|
||||
}
|
||||
recipients := []Recipient{
|
||||
var recipients = []Recipient{
|
||||
{"Aunt Mildred", "bone china tea set", true},
|
||||
{"Uncle John", "moleskin pants", false},
|
||||
{"Cousin Rodney", "", false},
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.13
|
||||
// +build go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
|
|
|
@ -395,6 +395,22 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
|
|||
s.walk(elem, r.List)
|
||||
}
|
||||
switch val.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
if len(r.Pipe.Decl) > 1 {
|
||||
s.errorf("can't use %v to iterate over more than one variable", val)
|
||||
break
|
||||
}
|
||||
run := false
|
||||
for v := range val.Seq() {
|
||||
run = true
|
||||
// Pass element as second value, as we do for channels.
|
||||
oneIteration(reflect.Value{}, v)
|
||||
}
|
||||
if !run {
|
||||
break
|
||||
}
|
||||
return
|
||||
case reflect.Array, reflect.Slice:
|
||||
if val.Len() == 0 {
|
||||
break
|
||||
|
@ -434,6 +450,43 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
|
|||
return
|
||||
case reflect.Invalid:
|
||||
break // An invalid value is likely a nil map, etc. and acts like an empty map.
|
||||
case reflect.Func:
|
||||
if val.Type().CanSeq() {
|
||||
if len(r.Pipe.Decl) > 1 {
|
||||
s.errorf("can't use %v iterate over more than one variable", val)
|
||||
break
|
||||
}
|
||||
run := false
|
||||
for v := range val.Seq() {
|
||||
run = true
|
||||
// Pass element as second value,
|
||||
// as we do for channels.
|
||||
oneIteration(reflect.Value{}, v)
|
||||
}
|
||||
if !run {
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
if val.Type().CanSeq2() {
|
||||
run := false
|
||||
for i, v := range val.Seq2() {
|
||||
run = true
|
||||
if len(r.Pipe.Decl) > 1 {
|
||||
oneIteration(i, v)
|
||||
} else {
|
||||
// If there is only one range variable,
|
||||
// oneIteration will use the
|
||||
// second value.
|
||||
oneIteration(reflect.Value{}, i)
|
||||
}
|
||||
}
|
||||
if !run {
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
s.errorf("range can't iterate over %v", val)
|
||||
}
|
||||
|
@ -757,7 +810,7 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
|
|||
return v
|
||||
}
|
||||
}
|
||||
if final != missingVal {
|
||||
if !final.Equal(missingVal) {
|
||||
// The last argument to and/or is coming from
|
||||
// the pipeline. We didn't short circuit on an earlier
|
||||
// argument, so we are going to return this one.
|
||||
|
@ -803,7 +856,13 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
|
|||
// Special case for the "call" builtin.
|
||||
// Insert the name of the callee function as the first argument.
|
||||
if isBuiltin && name == "call" {
|
||||
calleeName := args[0].String()
|
||||
var calleeName string
|
||||
if len(args) == 0 {
|
||||
// final must be present or we would have errored out above.
|
||||
calleeName = final.String()
|
||||
} else {
|
||||
calleeName = args[0].String()
|
||||
}
|
||||
argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
|
||||
fun = reflect.ValueOf(call)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -412,6 +413,9 @@ var execTests = []execTest{
|
|||
{"Interface Call", `{{stringer .S}}`, "foozle", map[string]any{"S": bytes.NewBufferString("foozle")}, true},
|
||||
{".ErrFunc", "{{call .ErrFunc}}", "bla", tVal, true},
|
||||
{"call nil", "{{call nil}}", "", tVal, false},
|
||||
{"empty call", "{{call}}", "", tVal, false},
|
||||
{"empty call after pipe valid", "{{.ErrFunc | call}}", "bla", tVal, true},
|
||||
{"empty call after pipe invalid", "{{1 | call}}", "", tVal, false},
|
||||
|
||||
// Erroneous function calls (check args).
|
||||
{".BinaryFuncTooFew", "{{call .BinaryFunc `1`}}", "", tVal, false},
|
||||
|
@ -618,6 +622,30 @@ var execTests = []execTest{
|
|||
{"declare in range", "{{range $x := .PSI}}<{{$foo:=$x}}{{$x}}>{{end}}", "<21><22><23>", tVal, true},
|
||||
{"range count", `{{range $i, $x := count 5}}[{{$i}}]{{$x}}{{end}}`, "[0]a[1]b[2]c[3]d[4]e", tVal, true},
|
||||
{"range nil count", `{{range $i, $x := count 0}}{{else}}empty{{end}}`, "empty", tVal, true},
|
||||
{"range iter.Seq[int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal1(2), true},
|
||||
{"i = range iter.Seq[int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true},
|
||||
{"range iter.Seq[int] over two var", `{{range $i, $c := .}}{{$c}}{{end}}`, "", fVal1(2), false},
|
||||
{"i, c := range iter.Seq2[int,int]", `{{range $i, $c := .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
|
||||
{"i, c = range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
|
||||
{"i = range iter.Seq2[int,int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal2(2), true},
|
||||
{"i := range iter.Seq2[int,int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal2(2), true},
|
||||
{"i,c,x range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{$x := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
|
||||
{"i,x range iter.Seq[int]", `{{$i := 0}}{{$x := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true},
|
||||
{"range iter.Seq[int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal1(0), true},
|
||||
{"range iter.Seq2[int,int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal2(0), true},
|
||||
{"range int8", rangeTestInt, rangeTestData[int8](), int8(5), true},
|
||||
{"range int16", rangeTestInt, rangeTestData[int16](), int16(5), true},
|
||||
{"range int32", rangeTestInt, rangeTestData[int32](), int32(5), true},
|
||||
{"range int64", rangeTestInt, rangeTestData[int64](), int64(5), true},
|
||||
{"range int", rangeTestInt, rangeTestData[int](), int(5), true},
|
||||
{"range uint8", rangeTestInt, rangeTestData[uint8](), uint8(5), true},
|
||||
{"range uint16", rangeTestInt, rangeTestData[uint16](), uint16(5), true},
|
||||
{"range uint32", rangeTestInt, rangeTestData[uint32](), uint32(5), true},
|
||||
{"range uint64", rangeTestInt, rangeTestData[uint64](), uint64(5), true},
|
||||
{"range uint", rangeTestInt, rangeTestData[uint](), uint(5), true},
|
||||
{"range uintptr", rangeTestInt, rangeTestData[uintptr](), uintptr(5), true},
|
||||
{"range uintptr(0)", `{{range $v := .}}{{print $v}}{{else}}empty{{end}}`, "empty", uintptr(0), true},
|
||||
{"range 5", `{{range $v := 5}}{{printf "%T%d" $v $v}}{{end}}`, rangeTestData[int](), nil, true},
|
||||
|
||||
// Cute examples.
|
||||
{"or as if true", `{{or .SI "slice is empty"}}`, "[3 4 5]", tVal, true},
|
||||
|
@ -722,6 +750,37 @@ var execTests = []execTest{
|
|||
{"issue60801", "{{$k := 0}}{{$v := 0}}{{range $k, $v = .AI}}{{$k}}={{$v}} {{end}}", "0=3 1=4 2=5 ", tVal, true},
|
||||
}
|
||||
|
||||
func fVal1(i int) iter.Seq[int] {
|
||||
return func(yield func(int) bool) {
|
||||
for v := range i {
|
||||
if !yield(v) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fVal2(i int) iter.Seq2[int, int] {
|
||||
return func(yield func(int, int) bool) {
|
||||
for v := range i {
|
||||
if !yield(v, v+1) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rangeTestInt = `{{range $v := .}}{{printf "%T%d" $v $v}}{{end}}`
|
||||
|
||||
func rangeTestData[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr]() string {
|
||||
I := T(5)
|
||||
var buf strings.Builder
|
||||
for i := T(0); i < I; i++ {
|
||||
fmt.Fprintf(&buf, "%T%d", i, i)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func zeroArgs() string {
|
||||
return "zeroArgs"
|
||||
}
|
||||
|
|
|
@ -304,14 +304,14 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
|
|||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if args != nil {
|
||||
args = args[1:] // Zeroth arg is function name/node; not passed to function.
|
||||
}
|
||||
|
||||
typ := fun.Type()
|
||||
numFirst := len(first)
|
||||
numFirst := len(first) // Added for Hugo
|
||||
numIn := len(args) + numFirst // Added for Hugo
|
||||
if final != missingVal {
|
||||
if !isMissing(final) {
|
||||
numIn++
|
||||
}
|
||||
numFixed := len(args) + len(first) // Adjusted for Hugo
|
||||
|
@ -346,7 +346,7 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
|
|||
return v
|
||||
}
|
||||
}
|
||||
if final != missingVal {
|
||||
if !final.Equal(missingVal) {
|
||||
// The last argument to and/or is coming from
|
||||
// the pipeline. We didn't short circuit on an earlier
|
||||
// argument, so we are going to return this one.
|
||||
|
@ -373,7 +373,7 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
|
|||
}
|
||||
}
|
||||
// Add final value if necessary.
|
||||
if final != missingVal {
|
||||
if !isMissing(final) {
|
||||
t := typ.In(typ.NumIn() - 1)
|
||||
if typ.IsVariadic() {
|
||||
if numIn-1 < numFixed {
|
||||
|
@ -392,7 +392,13 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
|
|||
// Special case for the "call" builtin.
|
||||
// Insert the name of the callee function as the first argument.
|
||||
if isBuiltin && name == "call" {
|
||||
calleeName := args[0].String()
|
||||
var calleeName string
|
||||
if len(args) == 0 {
|
||||
// final must be present or we would have errored out above.
|
||||
calleeName = final.String()
|
||||
} else {
|
||||
calleeName = args[0].String()
|
||||
}
|
||||
argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
|
||||
fun = reflect.ValueOf(call)
|
||||
}
|
||||
|
|
|
@ -2,16 +2,18 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.13
|
||||
// +build go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
|
||||
)
|
||||
|
||||
// Issue 36021: verify that text/template doesn't prevent the linker from removing
|
||||
|
@ -42,7 +44,7 @@ func main() {
|
|||
`
|
||||
td := t.TempDir()
|
||||
|
||||
if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0o644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "x.exe", "x.go")
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
//go:build go1.13 && !windows
|
||||
// +build go1.13,!windows
|
||||
|
||||
package template
|
||||
|
||||
|
@ -11,11 +11,10 @@ package template
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -32,32 +31,22 @@ type multiParseTest struct {
|
|||
}
|
||||
|
||||
var multiParseTests = []multiParseTest{
|
||||
{
|
||||
"empty", "", noError,
|
||||
{"empty", "", noError,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"one", `{{define "foo"}} FOO {{end}}`, noError,
|
||||
nil},
|
||||
{"one", `{{define "foo"}} FOO {{end}}`, noError,
|
||||
[]string{"foo"},
|
||||
[]string{" FOO "},
|
||||
},
|
||||
{
|
||||
"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
|
||||
[]string{" FOO "}},
|
||||
{"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
|
||||
[]string{"foo", "bar"},
|
||||
[]string{" FOO ", " BAR "},
|
||||
},
|
||||
[]string{" FOO ", " BAR "}},
|
||||
// errors
|
||||
{
|
||||
"missing end", `{{define "foo"}} FOO `, hasError,
|
||||
{"missing end", `{{define "foo"}} FOO `, hasError,
|
||||
nil,
|
||||
nil},
|
||||
{"malformed name", `{{define "foo}} FOO `, hasError,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"malformed name", `{{define "foo}} FOO `, hasError,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
nil},
|
||||
}
|
||||
|
||||
func TestMultiParse(t *testing.T) {
|
||||
|
@ -443,7 +432,7 @@ func TestIssue19294(t *testing.T) {
|
|||
// by the contents of "stylesheet", but if the internal map associating
|
||||
// names with templates is built in the wrong order, the empty block
|
||||
// looks non-empty and this doesn't happen.
|
||||
inlined := map[string]string{
|
||||
var inlined = map[string]string{
|
||||
"stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
|
||||
"xhtml": `{{block "stylesheet" .}}{{end}}`,
|
||||
}
|
||||
|
|
|
@ -352,6 +352,7 @@ func lexComment(l *lexer) stateFn {
|
|||
if !delim {
|
||||
return l.errorf("comment ends before closing delimiter")
|
||||
}
|
||||
l.line += strings.Count(l.input[l.start:l.pos], "\n")
|
||||
i := l.thisItem(itemComment)
|
||||
if trimSpace {
|
||||
l.pos += trimMarkerLen
|
||||
|
|
|
@ -548,6 +548,16 @@ var lexPosTests = []lexTest{
|
|||
{itemRightDelim, 11, "}}", 2},
|
||||
{itemEOF, 13, "", 2},
|
||||
}},
|
||||
{"longcomment", "{{/*\n*/}}\n{{undefinedFunction \"test\"}}", []item{
|
||||
{itemComment, 2, "/*\n*/", 1},
|
||||
{itemText, 9, "\n", 2},
|
||||
{itemLeftDelim, 10, "{{", 3},
|
||||
{itemIdentifier, 12, "undefinedFunction", 3},
|
||||
{itemSpace, 29, " ", 3},
|
||||
{itemString, 30, "\"test\"", 3},
|
||||
{itemRightDelim, 36, "}}", 3},
|
||||
{itemEOF, 38, "", 3},
|
||||
}},
|
||||
}
|
||||
|
||||
// The other tests don't check position, to make the test cases easier to construct.
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.13
|
||||
// +build go1.13
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
|
@ -33,9 +36,9 @@ var numberTests = []numberTest{
|
|||
{"7_3", true, true, true, false, 73, 73, 73, 0},
|
||||
{"0b10_010_01", true, true, true, false, 73, 73, 73, 0},
|
||||
{"0B10_010_01", true, true, true, false, 73, 73, 73, 0},
|
||||
{"073", true, true, true, false, 0o73, 0o73, 0o73, 0},
|
||||
{"0o73", true, true, true, false, 0o73, 0o73, 0o73, 0},
|
||||
{"0O73", true, true, true, false, 0o73, 0o73, 0o73, 0},
|
||||
{"073", true, true, true, false, 073, 073, 073, 0},
|
||||
{"0o73", true, true, true, false, 073, 073, 073, 0},
|
||||
{"0O73", true, true, true, false, 073, 073, 073, 0},
|
||||
{"0x73", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||
{"0X73", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||
{"0x7_3", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||
|
@ -61,7 +64,7 @@ var numberTests = []numberTest{
|
|||
{"-12+0i", true, false, true, true, -12, 0, -12, -12},
|
||||
{"13+0i", true, true, true, true, 13, 13, 13, 13},
|
||||
// funny bases
|
||||
{"0123", true, true, true, false, 0o123, 0o123, 0o123, 0},
|
||||
{"0123", true, true, true, false, 0123, 0123, 0123, 0},
|
||||
{"-0x0", true, true, true, false, 0, 0, 0, 0},
|
||||
{"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
|
||||
// character constants
|
||||
|
@ -176,150 +179,78 @@ const (
|
|||
)
|
||||
|
||||
var parseTests = []parseTest{
|
||||
{
|
||||
"empty", "", noError,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"comment", "{{/*\n\n\n*/}}", noError,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"spaces", " \t\n", noError,
|
||||
`" \t\n"`,
|
||||
},
|
||||
{
|
||||
"text", "some text", noError,
|
||||
`"some text"`,
|
||||
},
|
||||
{
|
||||
"emptyAction", "{{}}", hasError,
|
||||
`{{}}`,
|
||||
},
|
||||
{
|
||||
"field", "{{.X}}", noError,
|
||||
`{{.X}}`,
|
||||
},
|
||||
{
|
||||
"simple command", "{{printf}}", noError,
|
||||
`{{printf}}`,
|
||||
},
|
||||
{
|
||||
"$ invocation", "{{$}}", noError,
|
||||
"{{$}}",
|
||||
},
|
||||
{
|
||||
"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
|
||||
"{{with $x := 3}}{{$x 23}}{{end}}",
|
||||
},
|
||||
{
|
||||
"variable with fields", "{{$.I}}", noError,
|
||||
"{{$.I}}",
|
||||
},
|
||||
{
|
||||
"multi-word command", "{{printf `%d` 23}}", noError,
|
||||
"{{printf `%d` 23}}",
|
||||
},
|
||||
{
|
||||
"pipeline", "{{.X|.Y}}", noError,
|
||||
`{{.X | .Y}}`,
|
||||
},
|
||||
{
|
||||
"pipeline with decl", "{{$x := .X|.Y}}", noError,
|
||||
`{{$x := .X | .Y}}`,
|
||||
},
|
||||
{
|
||||
"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
|
||||
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`,
|
||||
},
|
||||
{
|
||||
"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
|
||||
`{{(.Y .Z).Field}}`,
|
||||
},
|
||||
{
|
||||
"simple if", "{{if .X}}hello{{end}}", noError,
|
||||
`{{if .X}}"hello"{{end}}`,
|
||||
},
|
||||
{
|
||||
"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
|
||||
`{{if .X}}"true"{{else}}"false"{{end}}`,
|
||||
},
|
||||
{
|
||||
"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
|
||||
`{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+", noError,
|
||||
`"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`,
|
||||
},
|
||||
{
|
||||
"simple range", "{{range .X}}hello{{end}}", noError,
|
||||
`{{range .X}}"hello"{{end}}`,
|
||||
},
|
||||
{
|
||||
"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
|
||||
`{{range .X.Y.Z}}"hello"{{end}}`,
|
||||
},
|
||||
{
|
||||
"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
|
||||
`{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
|
||||
`{{range .X}}"true"{{else}}"false"{{end}}`,
|
||||
},
|
||||
{
|
||||
"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
|
||||
`{{range .X | .M}}"true"{{else}}"false"{{end}}`,
|
||||
},
|
||||
{
|
||||
"range []int", "{{range .SI}}{{.}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
|
||||
`{{range $x := .SI}}{{.}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
|
||||
`{{range $x, $y := .SI}}{{.}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{break}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{continue}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
|
||||
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"template", "{{template `x`}}", noError,
|
||||
`{{template "x"}}`,
|
||||
},
|
||||
{
|
||||
"template with arg", "{{template `x` .Y}}", noError,
|
||||
`{{template "x" .Y}}`,
|
||||
},
|
||||
{
|
||||
"with", "{{with .X}}hello{{end}}", noError,
|
||||
`{{with .X}}"hello"{{end}}`,
|
||||
},
|
||||
{
|
||||
"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
|
||||
`{{with .X}}"hello"{{else}}"goodbye"{{end}}`,
|
||||
},
|
||||
{
|
||||
"with with else with", "{{with .X}}hello{{else with .Y}}goodbye{{end}}", noError,
|
||||
`{{with .X}}"hello"{{else}}{{with .Y}}"goodbye"{{end}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"with else chain", "{{with .X}}X{{else with .Y}}Y{{else with .Z}}Z{{end}}", noError,
|
||||
`{{with .X}}"X"{{else}}{{with .Y}}"Y"{{else}}{{with .Z}}"Z"{{end}}{{end}}{{end}}`,
|
||||
},
|
||||
{"empty", "", noError,
|
||||
``},
|
||||
{"comment", "{{/*\n\n\n*/}}", noError,
|
||||
``},
|
||||
{"spaces", " \t\n", noError,
|
||||
`" \t\n"`},
|
||||
{"text", "some text", noError,
|
||||
`"some text"`},
|
||||
{"emptyAction", "{{}}", hasError,
|
||||
`{{}}`},
|
||||
{"field", "{{.X}}", noError,
|
||||
`{{.X}}`},
|
||||
{"simple command", "{{printf}}", noError,
|
||||
`{{printf}}`},
|
||||
{"$ invocation", "{{$}}", noError,
|
||||
"{{$}}"},
|
||||
{"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
|
||||
"{{with $x := 3}}{{$x 23}}{{end}}"},
|
||||
{"variable with fields", "{{$.I}}", noError,
|
||||
"{{$.I}}"},
|
||||
{"multi-word command", "{{printf `%d` 23}}", noError,
|
||||
"{{printf `%d` 23}}"},
|
||||
{"pipeline", "{{.X|.Y}}", noError,
|
||||
`{{.X | .Y}}`},
|
||||
{"pipeline with decl", "{{$x := .X|.Y}}", noError,
|
||||
`{{$x := .X | .Y}}`},
|
||||
{"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
|
||||
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`},
|
||||
{"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
|
||||
`{{(.Y .Z).Field}}`},
|
||||
{"simple if", "{{if .X}}hello{{end}}", noError,
|
||||
`{{if .X}}"hello"{{end}}`},
|
||||
{"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
|
||||
`{{if .X}}"true"{{else}}"false"{{end}}`},
|
||||
{"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
|
||||
`{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`},
|
||||
{"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+", noError,
|
||||
`"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`},
|
||||
{"simple range", "{{range .X}}hello{{end}}", noError,
|
||||
`{{range .X}}"hello"{{end}}`},
|
||||
{"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
|
||||
`{{range .X.Y.Z}}"hello"{{end}}`},
|
||||
{"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
|
||||
`{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`},
|
||||
{"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
|
||||
`{{range .X}}"true"{{else}}"false"{{end}}`},
|
||||
{"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
|
||||
`{{range .X | .M}}"true"{{else}}"false"{{end}}`},
|
||||
{"range []int", "{{range .SI}}{{.}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{end}}`},
|
||||
{"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
|
||||
`{{range $x := .SI}}{{.}}{{end}}`},
|
||||
{"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
|
||||
`{{range $x, $y := .SI}}{{.}}{{end}}`},
|
||||
{"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{break}}{{end}}`},
|
||||
{"range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{continue}}{{end}}`},
|
||||
{"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
|
||||
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`},
|
||||
{"template", "{{template `x`}}", noError,
|
||||
`{{template "x"}}`},
|
||||
{"template with arg", "{{template `x` .Y}}", noError,
|
||||
`{{template "x" .Y}}`},
|
||||
{"with", "{{with .X}}hello{{end}}", noError,
|
||||
`{{with .X}}"hello"{{end}}`},
|
||||
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
|
||||
`{{with .X}}"hello"{{else}}"goodbye"{{end}}`},
|
||||
{"with with else with", "{{with .X}}hello{{else with .Y}}goodbye{{end}}", noError,
|
||||
`{{with .X}}"hello"{{else}}{{with .Y}}"goodbye"{{end}}{{end}}`},
|
||||
{"with else chain", "{{with .X}}X{{else with .Y}}Y{{else with .Z}}Z{{end}}", noError,
|
||||
`{{with .X}}"X"{{else}}{{with .Y}}"Y"{{else}}{{with .Z}}"Z"{{end}}{{end}}{{end}}`},
|
||||
// Trimming spaces.
|
||||
{"trim left", "x \r\n\t{{- 3}}", noError, `"x"{{3}}`},
|
||||
{"trim right", "{{3 -}}\n\n\ty", noError, `{{3}}"y"`},
|
||||
|
@ -328,24 +259,18 @@ var parseTests = []parseTest{
|
|||
{"comment trim left", "x \r\n\t{{- /* hi */}}", noError, `"x"`},
|
||||
{"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `"y"`},
|
||||
{"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x""y"`},
|
||||
{
|
||||
"block definition", `{{block "foo" .}}hello{{end}}`, noError,
|
||||
`{{template "foo" .}}`,
|
||||
},
|
||||
{"block definition", `{{block "foo" .}}hello{{end}}`, noError,
|
||||
`{{template "foo" .}}`},
|
||||
|
||||
{"newline in assignment", "{{ $x \n := \n 1 \n }}", noError, "{{$x := 1}}"},
|
||||
{"newline in empty action", "{{\n}}", hasError, "{{\n}}"},
|
||||
{"newline in pipeline", "{{\n\"x\"\n|\nprintf\n}}", noError, `{{"x" | printf}}`},
|
||||
{"newline in comment", "{{/*\nhello\n*/}}", noError, ""},
|
||||
{"newline in comment", "{{-\n/*\nhello\n*/\n-}}", noError, ""},
|
||||
{
|
||||
"spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{continue}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{break}}{{end}}`,
|
||||
},
|
||||
{"spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{continue}}{{end}}`},
|
||||
{"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{break}}{{end}}`},
|
||||
|
||||
// Errors.
|
||||
{"unclosed action", "hello{{range", hasError, ""},
|
||||
|
@ -487,7 +412,7 @@ func TestKeywordsAndFuncs(t *testing.T) {
|
|||
{
|
||||
// 'break' is a defined function, don't treat it as a keyword: it should
|
||||
// accept an argument successfully.
|
||||
funcsWithKeywordFunc := map[string]any{
|
||||
var funcsWithKeywordFunc = map[string]any{
|
||||
"break": func(in any) any { return in },
|
||||
}
|
||||
tmpl, err := New("").Parse(inp, "", "", make(map[string]*Tree), funcsWithKeywordFunc)
|
||||
|
@ -574,168 +499,104 @@ func TestErrorContextWithTreeCopy(t *testing.T) {
|
|||
// All failures, and the result is a string that must appear in the error message.
|
||||
var errorTests = []parseTest{
|
||||
// Check line numbers are accurate.
|
||||
{
|
||||
"unclosed1",
|
||||
{"unclosed1",
|
||||
"line1\n{{",
|
||||
hasError, `unclosed1:2: unclosed action`,
|
||||
},
|
||||
{
|
||||
"unclosed2",
|
||||
hasError, `unclosed1:2: unclosed action`},
|
||||
{"unclosed2",
|
||||
"line1\n{{define `x`}}line2\n{{",
|
||||
hasError, `unclosed2:3: unclosed action`,
|
||||
},
|
||||
{
|
||||
"unclosed3",
|
||||
hasError, `unclosed2:3: unclosed action`},
|
||||
{"unclosed3",
|
||||
"line1\n{{\"x\"\n\"y\"\n",
|
||||
hasError, `unclosed3:4: unclosed action started at unclosed3:2`,
|
||||
},
|
||||
{
|
||||
"unclosed4",
|
||||
hasError, `unclosed3:4: unclosed action started at unclosed3:2`},
|
||||
{"unclosed4",
|
||||
"{{\n\n\n\n\n",
|
||||
hasError, `unclosed4:6: unclosed action started at unclosed4:1`,
|
||||
},
|
||||
{
|
||||
"var1",
|
||||
hasError, `unclosed4:6: unclosed action started at unclosed4:1`},
|
||||
{"var1",
|
||||
"line1\n{{\nx\n}}",
|
||||
hasError, `var1:3: function "x" not defined`,
|
||||
},
|
||||
hasError, `var1:3: function "x" not defined`},
|
||||
// Specific errors.
|
||||
{
|
||||
"function",
|
||||
{"function",
|
||||
"{{foo}}",
|
||||
hasError, `function "foo" not defined`,
|
||||
},
|
||||
{
|
||||
"comment1",
|
||||
hasError, `function "foo" not defined`},
|
||||
{"comment1",
|
||||
"{{/*}}",
|
||||
hasError, `comment1:1: unclosed comment`,
|
||||
},
|
||||
{
|
||||
"comment2",
|
||||
hasError, `comment1:1: unclosed comment`},
|
||||
{"comment2",
|
||||
"{{/*\nhello\n}}",
|
||||
hasError, `comment2:1: unclosed comment`,
|
||||
},
|
||||
{
|
||||
"lparen",
|
||||
hasError, `comment2:1: unclosed comment`},
|
||||
{"lparen",
|
||||
"{{.X (1 2 3}}",
|
||||
hasError, `unclosed left paren`,
|
||||
},
|
||||
{
|
||||
"rparen",
|
||||
hasError, `unclosed left paren`},
|
||||
{"rparen",
|
||||
"{{.X 1 2 3 ) }}",
|
||||
hasError, "unexpected right paren",
|
||||
},
|
||||
{
|
||||
"rparen2",
|
||||
hasError, "unexpected right paren"},
|
||||
{"rparen2",
|
||||
"{{(.X 1 2 3",
|
||||
hasError, `unclosed action`,
|
||||
},
|
||||
{
|
||||
"space",
|
||||
hasError, `unclosed action`},
|
||||
{"space",
|
||||
"{{`x`3}}",
|
||||
hasError, `in operand`,
|
||||
},
|
||||
{
|
||||
"idchar",
|
||||
hasError, `in operand`},
|
||||
{"idchar",
|
||||
"{{a#}}",
|
||||
hasError, `'#'`,
|
||||
},
|
||||
{
|
||||
"charconst",
|
||||
hasError, `'#'`},
|
||||
{"charconst",
|
||||
"{{'a}}",
|
||||
hasError, `unterminated character constant`,
|
||||
},
|
||||
{
|
||||
"stringconst",
|
||||
hasError, `unterminated character constant`},
|
||||
{"stringconst",
|
||||
`{{"a}}`,
|
||||
hasError, `unterminated quoted string`,
|
||||
},
|
||||
{
|
||||
"rawstringconst",
|
||||
hasError, `unterminated quoted string`},
|
||||
{"rawstringconst",
|
||||
"{{`a}}",
|
||||
hasError, `unterminated raw quoted string`,
|
||||
},
|
||||
{
|
||||
"number",
|
||||
hasError, `unterminated raw quoted string`},
|
||||
{"number",
|
||||
"{{0xi}}",
|
||||
hasError, `number syntax`,
|
||||
},
|
||||
{
|
||||
"multidefine",
|
||||
hasError, `number syntax`},
|
||||
{"multidefine",
|
||||
"{{define `a`}}a{{end}}{{define `a`}}b{{end}}",
|
||||
hasError, `multiple definition of template`,
|
||||
},
|
||||
{
|
||||
"eof",
|
||||
hasError, `multiple definition of template`},
|
||||
{"eof",
|
||||
"{{range .X}}",
|
||||
hasError, `unexpected EOF`,
|
||||
},
|
||||
{
|
||||
"variable",
|
||||
hasError, `unexpected EOF`},
|
||||
{"variable",
|
||||
// Declare $x so it's defined, to avoid that error, and then check we don't parse a declaration.
|
||||
"{{$x := 23}}{{with $x.y := 3}}{{$x 23}}{{end}}",
|
||||
hasError, `unexpected ":="`,
|
||||
},
|
||||
{
|
||||
"multidecl",
|
||||
hasError, `unexpected ":="`},
|
||||
{"multidecl",
|
||||
"{{$a,$b,$c := 23}}",
|
||||
hasError, `too many declarations`,
|
||||
},
|
||||
{
|
||||
"undefvar",
|
||||
hasError, `too many declarations`},
|
||||
{"undefvar",
|
||||
"{{$a}}",
|
||||
hasError, `undefined variable`,
|
||||
},
|
||||
{
|
||||
"wrongdot",
|
||||
hasError, `undefined variable`},
|
||||
{"wrongdot",
|
||||
"{{true.any}}",
|
||||
hasError, `unexpected . after term`,
|
||||
},
|
||||
{
|
||||
"wrongpipeline",
|
||||
hasError, `unexpected . after term`},
|
||||
{"wrongpipeline",
|
||||
"{{12|false}}",
|
||||
hasError, `non executable command in pipeline`,
|
||||
},
|
||||
{
|
||||
"emptypipeline",
|
||||
hasError, `non executable command in pipeline`},
|
||||
{"emptypipeline",
|
||||
`{{ ( ) }}`,
|
||||
hasError, `missing value for parenthesized pipeline`,
|
||||
},
|
||||
{
|
||||
"multilinerawstring",
|
||||
hasError, `missing value for parenthesized pipeline`},
|
||||
{"multilinerawstring",
|
||||
"{{ $v := `\n` }} {{",
|
||||
hasError, `multilinerawstring:2: unclosed action`,
|
||||
},
|
||||
{
|
||||
"rangeundefvar",
|
||||
hasError, `multilinerawstring:2: unclosed action`},
|
||||
{"rangeundefvar",
|
||||
"{{range $k}}{{end}}",
|
||||
hasError, `undefined variable`,
|
||||
},
|
||||
{
|
||||
"rangeundefvars",
|
||||
hasError, `undefined variable`},
|
||||
{"rangeundefvars",
|
||||
"{{range $k, $v}}{{end}}",
|
||||
hasError, `undefined variable`,
|
||||
},
|
||||
{
|
||||
"rangemissingvalue1",
|
||||
hasError, `undefined variable`},
|
||||
{"rangemissingvalue1",
|
||||
"{{range $k,}}{{end}}",
|
||||
hasError, `missing value for range`,
|
||||
},
|
||||
{
|
||||
"rangemissingvalue2",
|
||||
hasError, `missing value for range`},
|
||||
{"rangemissingvalue2",
|
||||
"{{range $k, $v := }}{{end}}",
|
||||
hasError, `missing value for range`,
|
||||
},
|
||||
{
|
||||
"rangenotvariable1",
|
||||
hasError, `missing value for range`},
|
||||
{"rangenotvariable1",
|
||||
"{{range $k, .}}{{end}}",
|
||||
hasError, `range can only initialize variables`,
|
||||
},
|
||||
{
|
||||
"rangenotvariable2",
|
||||
hasError, `range can only initialize variables`},
|
||||
{"rangenotvariable2",
|
||||
"{{range $k, 123 := .}}{{end}}",
|
||||
hasError, `range can only initialize variables`,
|
||||
},
|
||||
hasError, `range can only initialize variables`},
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
|
|
|
@ -6,6 +6,7 @@ package template
|
|||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
"maps"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
@ -102,12 +103,8 @@ func (t *Template) Clone() (*Template, error) {
|
|||
}
|
||||
t.muFuncs.RLock()
|
||||
defer t.muFuncs.RUnlock()
|
||||
for k, v := range t.parseFuncs {
|
||||
nt.parseFuncs[k] = v
|
||||
}
|
||||
for k, v := range t.execFuncs {
|
||||
nt.execFuncs[k] = v
|
||||
}
|
||||
maps.Copy(nt.parseFuncs, t.parseFuncs)
|
||||
maps.Copy(nt.execFuncs, t.execFuncs)
|
||||
return nt, nil
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue