github.com/windmeup/goreleaser@v1.21.95/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/stretchr/testify/require" 12 "github.com/windmeup/goreleaser/internal/artifact" 13 "github.com/windmeup/goreleaser/internal/testctx" 14 "github.com/windmeup/goreleaser/pkg/build" 15 "github.com/windmeup/goreleaser/pkg/config" 16 "github.com/windmeup/goreleaser/pkg/context" 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/windmeup/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/windmeup/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 "env bar: barrrrr": `env bar: {{ envOrDefault "BAR" "barrrrr" }}`, 96 "env foo: bar": `env foo: {{ envOrDefault "FOO" "barrrrr" }}`, 97 98 "remove this": "{{ filter .Env.MULTILINE \".*remove.*\" }}", 99 "something with\nmultiple lines\nto test things": "{{ reverseFilter .Env.MULTILINE \".*remove.*\" }}", 100 101 // maps 102 "123": `{{ $m := map "a" "1" "b" "2" }}{{ index $m "a" }}{{ indexOrDefault $m "b" "10" }}{{ indexOrDefault $m "c" "3" }}{{ index $m "z" }}`, 103 } { 104 tmpl := tmpl 105 expect := expect 106 t.Run(expect, func(t *testing.T) { 107 t.Parallel() 108 result, err := New(ctx).WithArtifact( 109 &artifact.Artifact{ 110 Name: "not-this-binary", 111 Path: "/tmp/foo.exe", 112 Goarch: "amd64", 113 Goos: "linux", 114 Goarm: "6", 115 Gomips: "softfloat", 116 Goamd64: "v3", 117 Extra: map[string]interface{}{ 118 artifact.ExtraBinary: "binary", 119 artifact.ExtraExt: ".exe", 120 }, 121 }, 122 ).Apply(tmpl) 123 require.NoError(t, err) 124 require.Equal(t, expect, result) 125 }) 126 } 127 128 t.Run("artifact without binary name", func(t *testing.T) { 129 t.Parallel() 130 result, err := New(ctx).WithArtifact( 131 &artifact.Artifact{ 132 Name: "another-binary", 133 Goarch: "amd64", 134 Goos: "linux", 135 Goarm: "6", 136 }, 137 ).Apply("{{ .Binary }}") 138 require.NoError(t, err) 139 require.Equal(t, ctx.Config.ProjectName, result) 140 }) 141 142 t.Run("template using artifact Fields with no artifact", func(t *testing.T) { 143 t.Parallel() 144 result, err := New(ctx).Apply("{{ .Os }}") 145 require.ErrorAs(t, err, &Error{}) 146 require.EqualError(t, err, `template: failed to apply "{{ .Os }}": map has no entry for key "Os"`) 147 require.Empty(t, result) 148 }) 149 } 150 151 func TestEnv(t *testing.T) { 152 testCases := []struct { 153 desc string 154 in string 155 out string 156 }{ 157 { 158 desc: "with env", 159 in: "{{ .Env.FOO }}", 160 out: "BAR", 161 }, 162 { 163 desc: "with env", 164 in: "{{ .Env.BAR }}", 165 out: "", 166 }, 167 } 168 ctx := testctx.New( 169 testctx.WithEnv(map[string]string{"FOO": "BAR"}), 170 testctx.WithCurrentTag("v1.2.3"), 171 ) 172 for _, tC := range testCases { 173 t.Run(tC.desc, func(t *testing.T) { 174 out, _ := New(ctx).Apply(tC.in) 175 require.Equal(t, tC.out, out) 176 }) 177 } 178 } 179 180 func TestWithEnv(t *testing.T) { 181 ctx := testctx.New( 182 testctx.WithEnv(map[string]string{"FOO": "BAR"}), 183 testctx.WithCurrentTag("v1.2.3"), 184 ) 185 tpl := New(ctx).WithEnvS([]string{ 186 "FOO=foo", 187 "BAR=bar", 188 "NOVAL=", 189 "=NOKEY", 190 "=", 191 "NOTHING", 192 }) 193 out, err := tpl.Apply("{{ .Env.FOO }}-{{ .Env.BAR }}") 194 require.NoError(t, err) 195 require.Equal(t, "foo-bar", out) 196 197 out, err = tpl.Apply(`{{ range $idx, $key := .Env }}{{ $idx }},{{ end }}`) 198 require.NoError(t, err) 199 require.Equal(t, "BAR,FOO,NOVAL,", out) 200 } 201 202 func TestFuncMap(t *testing.T) { 203 ctx := testctx.NewWithCfg(config.Project{ 204 ProjectName: "proj", 205 Env: []string{ 206 "FOO=bar", 207 }, 208 }) 209 wd, err := os.Getwd() 210 require.NoError(t, err) 211 212 ctx.Git.URL = "https://github.com/foo/bar.git" 213 ctx.ReleaseURL = "https://github.com/foo/bar/releases/tag/v1.0.0" 214 ctx.Git.CurrentTag = "v1.2.4" 215 for _, tc := range []struct { 216 Template string 217 Name string 218 Expected string 219 }{ 220 { 221 Template: `{{ replace "v1.24" "v" "" }}`, 222 Name: "replace", 223 Expected: "1.24", 224 }, 225 { 226 Template: `{{ if index .Env "SOME_ENV" }}{{ .Env.SOME_ENV }}{{ else }}default value{{ end }}`, 227 Name: "default value", 228 Expected: "default value", 229 }, 230 { 231 Template: `{{ if index .Env "FOO" }}{{ .Env.FOO }}{{ else }}default value{{ end }}`, 232 Name: "default value set", 233 Expected: "bar", 234 }, 235 { 236 Template: `{{ time "2006-01-02" }}`, 237 Name: "time YYYY-MM-DD", 238 }, 239 { 240 Template: `{{ time "01/02/2006" }}`, 241 Name: "time MM/DD/YYYY", 242 }, 243 { 244 Template: `{{ time "01/02/2006" }}`, 245 Name: "time MM/DD/YYYY", 246 }, 247 { 248 Template: `{{ tolower "TEST" }}`, 249 Name: "tolower", 250 Expected: "test", 251 }, 252 { 253 Template: `{{ trimprefix "v1.2.4" "v" }}`, 254 Name: "trimprefix", 255 Expected: "1.2.4", 256 }, 257 { 258 Template: `{{ trimsuffix .GitURL ".git" }}`, 259 Name: "trimsuffix", 260 Expected: "https://github.com/foo/bar", 261 }, 262 { 263 Template: `{{ title "file" }}`, 264 Name: "title", 265 Expected: "File", 266 }, 267 { 268 Template: `{{ .ReleaseURL }}`, 269 Name: "trimsuffix", 270 Expected: "https://github.com/foo/bar/releases/tag/v1.0.0", 271 }, 272 { 273 Template: `{{ toupper "test" }}`, 274 Name: "toupper", 275 Expected: "TEST", 276 }, 277 { 278 Template: `{{ trim " test " }}`, 279 Name: "trim", 280 Expected: "test", 281 }, 282 { 283 Template: `{{ abs "file" }}`, 284 Name: "abs", 285 Expected: filepath.Join(wd, "file"), 286 }, 287 } { 288 out, err := New(ctx).Apply(tc.Template) 289 require.NoError(t, err) 290 if tc.Expected != "" { 291 require.Equal(t, tc.Expected, out) 292 } else { 293 require.NotEmpty(t, out) 294 } 295 } 296 } 297 298 func TestApplySingleEnvOnly(t *testing.T) { 299 ctx := testctx.NewWithCfg(config.Project{ 300 Env: []string{ 301 "FOO=value", 302 "BAR=another", 303 }, 304 }) 305 306 testCases := []struct { 307 name string 308 tpl string 309 expectedErr error 310 }{ 311 { 312 "empty tpl", 313 "", 314 nil, 315 }, 316 { 317 "whitespaces", 318 " ", 319 nil, 320 }, 321 { 322 "plain-text only", 323 "raw-token", 324 ExpectedSingleEnvErr{}, 325 }, 326 { 327 "variable with spaces", 328 "{{ .Env.FOO }}", 329 nil, 330 }, 331 { 332 "variable without spaces", 333 "{{.Env.FOO}}", 334 nil, 335 }, 336 { 337 "variable with outer spaces", 338 " {{ .Env.FOO }} ", 339 nil, 340 }, 341 { 342 "unknown variable", 343 "{{ .Env.UNKNOWN }}", 344 template.ExecError{}, 345 }, 346 { 347 "other interpolation", 348 "{{ .ProjectName }}", 349 ExpectedSingleEnvErr{}, 350 }, 351 } 352 for _, tc := range testCases { 353 t.Run(tc.name, func(t *testing.T) { 354 _, err := New(ctx).ApplySingleEnvOnly(tc.tpl) 355 if tc.expectedErr != nil { 356 require.Error(t, err) 357 } else { 358 require.NoError(t, err) 359 } 360 }) 361 } 362 } 363 364 func TestInvalidTemplate(t *testing.T) { 365 ctx := testctx.New() 366 _, err := New(ctx).Apply("{{{.Foo}") 367 require.ErrorAs(t, err, &Error{}) 368 require.EqualError(t, err, `template: failed to apply "{{{.Foo}": unexpected "{" in command`) 369 } 370 371 func TestEnvNotFound(t *testing.T) { 372 ctx := testctx.New(testctx.WithCurrentTag("v1.2.4")) 373 result, err := New(ctx).Apply("{{.Env.FOO}}") 374 require.Empty(t, result) 375 require.ErrorAs(t, err, &Error{}) 376 require.EqualError(t, err, `template: failed to apply "{{.Env.FOO}}": map has no entry for key "FOO"`) 377 } 378 379 func TestWithExtraFields(t *testing.T) { 380 ctx := testctx.New() 381 out, _ := New(ctx).WithExtraFields(Fields{ 382 "MyCustomField": "foo", 383 }).Apply("{{ .MyCustomField }}") 384 require.Equal(t, "foo", out) 385 } 386 387 func TestBool(t *testing.T) { 388 t.Run("true", func(t *testing.T) { 389 for _, v := range []string{ 390 " TruE ", 391 "true", 392 "TRUE", 393 } { 394 t.Run(v, func(t *testing.T) { 395 ctx := testctx.NewWithCfg(config.Project{ 396 Env: []string{"FOO=" + v}, 397 }) 398 b, err := New(ctx).Bool("{{.Env.FOO}}") 399 require.NoError(t, err) 400 require.True(t, b) 401 }) 402 } 403 }) 404 t.Run("false", func(t *testing.T) { 405 for _, v := range []string{ 406 " ", 407 "", 408 "false", 409 "yada yada", 410 } { 411 t.Run(v, func(t *testing.T) { 412 ctx := testctx.NewWithCfg(config.Project{ 413 Env: []string{"FOO=" + v}, 414 }) 415 b, err := New(ctx).Bool("{{.Env.FOO}}") 416 require.NoError(t, err) 417 require.False(t, b) 418 }) 419 } 420 }) 421 } 422 423 func TestMdv2Escape(t *testing.T) { 424 require.Equal( 425 t, 426 "aaa\\_\\*\\[\\]\\(\\)\\~\\`\\>\\#\\+\\-\\=\\|\\{\\}\\.\\!", 427 mdv2Escape("aaa_*[]()~`>#+-=|{}.!")) 428 } 429 430 func TestInvalidMap(t *testing.T) { 431 _, err := New(testctx.New()).Apply(`{{ $m := map "a" }}`) 432 require.ErrorContains(t, err, "map expects even number of arguments, got 1") 433 } 434 435 func TestWithBuildOptions(t *testing.T) { 436 out, err := New(testctx.New()).WithBuildOptions(build.Options{ 437 Name: "name", 438 Path: "./path", 439 Ext: ".ext", 440 Target: "target", 441 Goos: "os", 442 Goarch: "arch", 443 Goamd64: "amd64", 444 Goarm: "arm", 445 Gomips: "mips", 446 }).Apply("{{.Name}}_{{.Path}}_{{.Ext}}_{{.Target}}_{{.Os}}_{{.Arch}}_{{.Amd64}}_{{.Arm}}_{{.Mips}}") 447 require.NoError(t, err) 448 require.Equal(t, "name_./path_.ext_target_os_arch_amd64_arm_mips", out) 449 }