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  }