github.com/goreleaser/goreleaser@v1.25.1/internal/tmpl/tmpl_test.go (about) 1 package tmpl 2 3 import ( 4 "os" 5 "path/filepath" 6 "runtime" 7 "testing" 8 "text/template" 9 "time" 10 11 "github.com/goreleaser/goreleaser/internal/artifact" 12 "github.com/goreleaser/goreleaser/internal/testctx" 13 "github.com/goreleaser/goreleaser/pkg/build" 14 "github.com/goreleaser/goreleaser/pkg/config" 15 "github.com/goreleaser/goreleaser/pkg/context" 16 "github.com/stretchr/testify/require" 17 ) 18 19 func TestWithArtifact(t *testing.T) { 20 t.Parallel() 21 ctx := testctx.NewWithCfg( 22 config.Project{ 23 ProjectName: "proj", 24 Release: config.Release{ 25 Draft: true, 26 }, 27 }, 28 testctx.WithVersion("1.2.3"), 29 testctx.WithGitInfo(context.GitInfo{ 30 PreviousTag: "v1.2.2", 31 CurrentTag: "v1.2.3", 32 Branch: "test-branch", 33 Commit: "commit", 34 FullCommit: "fullcommit", 35 ShortCommit: "shortcommit", 36 TagSubject: "awesome release", 37 TagContents: "awesome release\n\nanother line", 38 TagBody: "another line", 39 Dirty: true, 40 }), 41 testctx.WithEnv(map[string]string{ 42 "FOO": "bar", 43 "MULTILINE": "something with\nmultiple lines\nremove this\nto test things", 44 }), 45 testctx.WithSemver(1, 2, 3, ""), 46 testctx.Snapshot, 47 func(ctx *context.Context) { 48 ctx.ModulePath = "github.com/goreleaser/goreleaser" 49 ctx.ReleaseNotes = "test release notes" 50 ctx.Date = time.Unix(1678327562, 0) 51 }, 52 ) 53 for expect, tmpl := range map[string]string{ 54 "bar": "{{.Env.FOO}}", 55 "linux": "{{.Os}}", 56 "amd64": "{{.Arch}}", 57 "6": "{{.Arm}}", 58 "softfloat": "{{.Mips}}", 59 "v3": "{{.Amd64}}", 60 "1.2.3": "{{.Version}}", 61 "v1.2.3": "{{.Tag}}", 62 "1-2-3": "{{.Major}}-{{.Minor}}-{{.Patch}}", 63 "test-branch": "{{.Branch}}", 64 "commit": "{{.Commit}}", 65 "fullcommit": "{{.FullCommit}}", 66 "shortcommit": "{{.ShortCommit}}", 67 "binary": "{{.Binary}}", 68 "proj": "{{.ProjectName}}", 69 "github.com/goreleaser/goreleaser": "{{ .ModulePath }}", 70 "v2.0.0": "{{.Tag | incmajor }}", 71 "2.0.0": "{{.Version | incmajor }}", 72 "v1.3.0": "{{.Tag | incminor }}", 73 "1.3.0": "{{.Version | incminor }}", 74 "v1.2.4": "{{.Tag | incpatch }}", 75 "1.2.4": "{{.Version | incpatch }}", 76 "test release notes": "{{ .ReleaseNotes }}", 77 "v1.2.2": "{{ .PreviousTag }}", 78 "awesome release": "{{ .TagSubject }}", 79 "awesome release\n\nanother line": "{{ .TagContents }}", 80 "another line": "{{ .TagBody }}", 81 "runtime: " + runtime.GOOS: "runtime: {{ .Runtime.Goos }}", 82 "runtime: " + runtime.GOARCH: "runtime: {{ .Runtime.Goarch }}", 83 "artifact name: not-this-binary": "artifact name: {{ .ArtifactName }}", 84 "artifact ext: .exe": "artifact ext: {{ .ArtifactExt }}", 85 "artifact path: /tmp/foo.exe": "artifact path: {{ .ArtifactPath }}", 86 "artifact basename: foo.exe": "artifact basename: {{ base .ArtifactPath }}", 87 "artifact dir: /tmp": "artifact dir: {{ dir .ArtifactPath }}", 88 "2023": `{{ .Now.Format "2006" }}`, 89 "2023-03-09T02:06:02Z": `{{ .Date }}`, 90 "1678327562": `{{ .Timestamp }}`, 91 "snapshot true": `snapshot {{.IsSnapshot}}`, 92 "nightly false": `nightly {{.IsNightly}}`, 93 "draft true": `draft {{.IsDraft}}`, 94 "dirty true": `dirty {{.IsGitDirty}}`, 95 "clean false": `clean {{.IsGitClean}}`, 96 "state dirty": `state {{.GitTreeState}}`, 97 "env bar: barrrrr": `env bar: {{ envOrDefault "BAR" "barrrrr" }}`, 98 "env foo: bar": `env foo: {{ envOrDefault "FOO" "barrrrr" }}`, 99 100 "remove this": "{{ filter .Env.MULTILINE \".*remove.*\" }}", 101 "something with\nmultiple lines\nto test things": "{{ reverseFilter .Env.MULTILINE \".*remove.*\" }}", 102 103 // maps 104 "123": `{{ $m := map "a" "1" "b" "2" }}{{ index $m "a" }}{{ indexOrDefault $m "b" "10" }}{{ indexOrDefault $m "c" "3" }}{{ index $m "z" }}`, 105 } { 106 tmpl := tmpl 107 expect := expect 108 t.Run(expect, func(t *testing.T) { 109 t.Parallel() 110 result, err := New(ctx).WithArtifact( 111 &artifact.Artifact{ 112 Name: "not-this-binary", 113 Path: "/tmp/foo.exe", 114 Goarch: "amd64", 115 Goos: "linux", 116 Goarm: "6", 117 Gomips: "softfloat", 118 Goamd64: "v3", 119 Extra: map[string]interface{}{ 120 artifact.ExtraBinary: "binary", 121 artifact.ExtraExt: ".exe", 122 }, 123 }, 124 ).Apply(tmpl) 125 require.NoError(t, err) 126 require.Equal(t, expect, result) 127 }) 128 } 129 130 t.Run("artifact without binary name", func(t *testing.T) { 131 t.Parallel() 132 result, err := New(ctx).WithArtifact( 133 &artifact.Artifact{ 134 Name: "another-binary", 135 Goarch: "amd64", 136 Goos: "linux", 137 Goarm: "6", 138 }, 139 ).Apply("{{ .Binary }}") 140 require.NoError(t, err) 141 require.Equal(t, ctx.Config.ProjectName, result) 142 }) 143 144 t.Run("template using artifact Fields with no artifact", func(t *testing.T) { 145 t.Parallel() 146 result, err := New(ctx).Apply("{{ .Os }}") 147 require.ErrorAs(t, err, &Error{}) 148 require.EqualError(t, err, `template: failed to apply "{{ .Os }}": map has no entry for key "Os"`) 149 require.Empty(t, result) 150 }) 151 } 152 153 func TestEnv(t *testing.T) { 154 testCases := []struct { 155 desc string 156 in string 157 out string 158 }{ 159 { 160 desc: "with env", 161 in: "{{ .Env.FOO }}", 162 out: "BAR", 163 }, 164 { 165 desc: "with env", 166 in: "{{ .Env.BAR }}", 167 out: "", 168 }, 169 } 170 ctx := testctx.New( 171 testctx.WithEnv(map[string]string{"FOO": "BAR"}), 172 testctx.WithCurrentTag("v1.2.3"), 173 ) 174 for _, tC := range testCases { 175 t.Run(tC.desc, func(t *testing.T) { 176 out, _ := New(ctx).Apply(tC.in) 177 require.Equal(t, tC.out, out) 178 }) 179 } 180 } 181 182 func TestWithEnv(t *testing.T) { 183 ctx := testctx.New( 184 testctx.WithEnv(map[string]string{"FOO": "BAR"}), 185 testctx.WithCurrentTag("v1.2.3"), 186 ) 187 tpl := New(ctx).WithEnvS([]string{ 188 "FOO=foo", 189 "BAR=bar", 190 "NOVAL=", 191 "=NOKEY", 192 "=", 193 "NOTHING", 194 }) 195 out, err := tpl.Apply("{{ .Env.FOO }}-{{ .Env.BAR }}") 196 require.NoError(t, err) 197 require.Equal(t, "foo-bar", out) 198 199 out, err = tpl.Apply(`{{ range $idx, $key := .Env }}{{ $idx }},{{ end }}`) 200 require.NoError(t, err) 201 require.Equal(t, "BAR,FOO,NOVAL,", out) 202 } 203 204 func TestFuncMap(t *testing.T) { 205 ctx := testctx.NewWithCfg(config.Project{ 206 ProjectName: "proj", 207 Env: []string{ 208 "FOO=bar", 209 }, 210 }) 211 wd, err := os.Getwd() 212 require.NoError(t, err) 213 214 ctx.Git.URL = "https://github.com/foo/bar.git" 215 ctx.ReleaseURL = "https://github.com/foo/bar/releases/tag/v1.0.0" 216 ctx.Git.CurrentTag = "v1.2.4" 217 for _, tc := range []struct { 218 Template string 219 Name string 220 Expected string 221 }{ 222 { 223 Template: `{{ replace "v1.24" "v" "" }}`, 224 Name: "replace", 225 Expected: "1.24", 226 }, 227 { 228 Template: `{{ if index .Env "SOME_ENV" }}{{ .Env.SOME_ENV }}{{ else }}default value{{ end }}`, 229 Name: "default value", 230 Expected: "default value", 231 }, 232 { 233 Template: `{{ if index .Env "FOO" }}{{ .Env.FOO }}{{ else }}default value{{ end }}`, 234 Name: "default value set", 235 Expected: "bar", 236 }, 237 { 238 Template: `{{ time "2006-01-02" }}`, 239 Name: "time YYYY-MM-DD", 240 }, 241 { 242 Template: `{{ time "01/02/2006" }}`, 243 Name: "time MM/DD/YYYY", 244 }, 245 { 246 Template: `{{ time "01/02/2006" }}`, 247 Name: "time MM/DD/YYYY", 248 }, 249 { 250 Template: `{{ tolower "TEST" }}`, 251 Name: "tolower", 252 Expected: "test", 253 }, 254 { 255 Template: `{{ if contains "TEST_TEST_TEST" "TEST" }}it does{{else}}nope{{end}}`, 256 Name: "contains", 257 Expected: "it does", 258 }, 259 { 260 Template: `{{ trimprefix "v1.2.4" "v" }}`, 261 Name: "trimprefix", 262 Expected: "1.2.4", 263 }, 264 { 265 Template: `{{ trimsuffix .GitURL ".git" }}`, 266 Name: "trimsuffix", 267 Expected: "https://github.com/foo/bar", 268 }, 269 { 270 Template: `{{ title "file" }}`, 271 Name: "title", 272 Expected: "File", 273 }, 274 { 275 Template: `{{ .ReleaseURL }}`, 276 Name: "trimsuffix", 277 Expected: "https://github.com/foo/bar/releases/tag/v1.0.0", 278 }, 279 { 280 Template: `{{ toupper "test" }}`, 281 Name: "toupper", 282 Expected: "TEST", 283 }, 284 { 285 Template: `{{ trim " test " }}`, 286 Name: "trim", 287 Expected: "test", 288 }, 289 { 290 Template: `{{ abs "file" }}`, 291 Name: "abs", 292 Expected: filepath.Join(wd, "file"), 293 }, 294 } { 295 out, err := New(ctx).Apply(tc.Template) 296 require.NoError(t, err) 297 if tc.Expected != "" { 298 require.Equal(t, tc.Expected, out) 299 } else { 300 require.NotEmpty(t, out) 301 } 302 } 303 } 304 305 func TestApplySingleEnvOnly(t *testing.T) { 306 ctx := testctx.NewWithCfg(config.Project{ 307 Env: []string{ 308 "FOO=value", 309 "BAR=another", 310 }, 311 }) 312 313 testCases := []struct { 314 name string 315 tpl string 316 expectedErr error 317 }{ 318 { 319 "empty tpl", 320 "", 321 nil, 322 }, 323 { 324 "whitespaces", 325 " ", 326 nil, 327 }, 328 { 329 "plain-text only", 330 "raw-token", 331 ExpectedSingleEnvErr{}, 332 }, 333 { 334 "variable with spaces", 335 "{{ .Env.FOO }}", 336 nil, 337 }, 338 { 339 "variable without spaces", 340 "{{.Env.FOO}}", 341 nil, 342 }, 343 { 344 "variable with outer spaces", 345 " {{ .Env.FOO }} ", 346 nil, 347 }, 348 { 349 "unknown variable", 350 "{{ .Env.UNKNOWN }}", 351 template.ExecError{}, 352 }, 353 { 354 "other interpolation", 355 "{{ .ProjectName }}", 356 ExpectedSingleEnvErr{}, 357 }, 358 } 359 for _, tc := range testCases { 360 t.Run(tc.name, func(t *testing.T) { 361 _, err := New(ctx).ApplySingleEnvOnly(tc.tpl) 362 if tc.expectedErr != nil { 363 require.Error(t, err) 364 } else { 365 require.NoError(t, err) 366 } 367 }) 368 } 369 } 370 371 func TestInvalidTemplate(t *testing.T) { 372 ctx := testctx.New() 373 _, err := New(ctx).Apply("{{{.Foo}") 374 require.ErrorAs(t, err, &Error{}) 375 require.EqualError(t, err, `template: failed to apply "{{{.Foo}": unexpected "{" in command`) 376 } 377 378 func TestEnvNotFound(t *testing.T) { 379 ctx := testctx.New(testctx.WithCurrentTag("v1.2.4")) 380 result, err := New(ctx).Apply("{{.Env.FOO}}") 381 require.Empty(t, result) 382 require.ErrorAs(t, err, &Error{}) 383 require.EqualError(t, err, `template: failed to apply "{{.Env.FOO}}": map has no entry for key "FOO"`) 384 } 385 386 func TestWithExtraFields(t *testing.T) { 387 ctx := testctx.New() 388 out, _ := New(ctx).WithExtraFields(Fields{ 389 "MyCustomField": "foo", 390 }).Apply("{{ .MyCustomField }}") 391 require.Equal(t, "foo", out) 392 } 393 394 func TestBool(t *testing.T) { 395 t.Run("true", func(t *testing.T) { 396 for _, v := range []string{ 397 " TruE ", 398 "true", 399 "TRUE", 400 } { 401 t.Run(v, func(t *testing.T) { 402 ctx := testctx.NewWithCfg(config.Project{ 403 Env: []string{"FOO=" + v}, 404 }) 405 b, err := New(ctx).Bool("{{.Env.FOO}}") 406 require.NoError(t, err) 407 require.True(t, b) 408 }) 409 } 410 }) 411 t.Run("false", func(t *testing.T) { 412 for _, v := range []string{ 413 " ", 414 "", 415 "false", 416 "yada yada", 417 } { 418 t.Run(v, func(t *testing.T) { 419 ctx := testctx.NewWithCfg(config.Project{ 420 Env: []string{"FOO=" + v}, 421 }) 422 b, err := New(ctx).Bool("{{.Env.FOO}}") 423 require.NoError(t, err) 424 require.False(t, b) 425 }) 426 } 427 }) 428 } 429 430 func TestMdv2Escape(t *testing.T) { 431 require.Equal( 432 t, 433 "aaa\\_\\*\\[\\]\\(\\)\\~\\`\\>\\#\\+\\-\\=\\|\\{\\}\\.\\!", 434 mdv2Escape("aaa_*[]()~`>#+-=|{}.!")) 435 } 436 437 func TestInvalidMap(t *testing.T) { 438 _, err := New(testctx.New()).Apply(`{{ $m := map "a" }}`) 439 require.ErrorContains(t, err, "map expects even number of arguments, got 1") 440 } 441 442 func TestWithBuildOptions(t *testing.T) { 443 out, err := New(testctx.New()).WithBuildOptions(build.Options{ 444 Name: "name", 445 Path: "./path", 446 Ext: ".ext", 447 Target: "target", 448 Goos: "os", 449 Goarch: "arch", 450 Goamd64: "amd64", 451 Goarm: "arm", 452 Gomips: "mips", 453 }).Apply("{{.Name}}_{{.Path}}_{{.Ext}}_{{.Target}}_{{.Os}}_{{.Arch}}_{{.Amd64}}_{{.Arm}}_{{.Mips}}") 454 require.NoError(t, err) 455 require.Equal(t, "name_./path_.ext_target_os_arch_amd64_arm_mips", out) 456 }