github.com/pulumi/terraform@v1.4.0/pkg/lang/functions_test.go (about)

     1  package lang
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"testing"
     8  
     9  	"github.com/hashicorp/hcl/v2"
    10  	"github.com/hashicorp/hcl/v2/hclsyntax"
    11  	"github.com/pulumi/terraform/pkg/experiments"
    12  	"github.com/pulumi/terraform/pkg/lang/marks"
    13  	homedir "github.com/mitchellh/go-homedir"
    14  	"github.com/zclconf/go-cty/cty"
    15  )
    16  
    17  // TestFunctions tests that functions are callable through the functionality
    18  // in the langs package, via HCL.
    19  //
    20  // These tests are primarily here to assert that the functions are properly
    21  // registered in the functions table, rather than to test all of the details
    22  // of the functions. Each function should only have one or two tests here,
    23  // since the main set of unit tests for a function should live alongside that
    24  // function either in the "funcs" subdirectory here or over in the cty
    25  // function/stdlib package.
    26  //
    27  // One exception to that is we can use this test mechanism to assert common
    28  // patterns that are used in real-world configurations which rely on behaviors
    29  // implemented either in this lang package or in HCL itself, such as automatic
    30  // type conversions. The function unit tests don't cover those things because
    31  // they call directly into the functions.
    32  //
    33  // With that said then, this test function should contain at least one simple
    34  // test case per function registered in the functions table (just to prove
    35  // it really is registered correctly) and possibly a small set of additional
    36  // functions showing real-world use-cases that rely on type conversion
    37  // behaviors.
    38  func TestFunctions(t *testing.T) {
    39  	// used in `pathexpand()` test
    40  	homePath, err := homedir.Dir()
    41  	if err != nil {
    42  		t.Fatalf("Error getting home directory: %v", err)
    43  	}
    44  
    45  	tests := map[string][]struct {
    46  		src  string
    47  		want cty.Value
    48  	}{
    49  		// Please maintain this list in alphabetical order by function, with
    50  		// a blank line between the group of tests for each function.
    51  
    52  		"abs": {
    53  			{
    54  				`abs(-1)`,
    55  				cty.NumberIntVal(1),
    56  			},
    57  		},
    58  
    59  		"abspath": {
    60  			{
    61  				`abspath(".")`,
    62  				cty.StringVal((func() string {
    63  					cwd, err := os.Getwd()
    64  					if err != nil {
    65  						panic(err)
    66  					}
    67  					return filepath.ToSlash(cwd)
    68  				})()),
    69  			},
    70  		},
    71  
    72  		"alltrue": {
    73  			{
    74  				`alltrue(["true", true])`,
    75  				cty.True,
    76  			},
    77  		},
    78  
    79  		"anytrue": {
    80  			{
    81  				`anytrue([])`,
    82  				cty.False,
    83  			},
    84  		},
    85  
    86  		"base64decode": {
    87  			{
    88  				`base64decode("YWJjMTIzIT8kKiYoKSctPUB+")`,
    89  				cty.StringVal("abc123!?$*&()'-=@~"),
    90  			},
    91  		},
    92  
    93  		"base64encode": {
    94  			{
    95  				`base64encode("abc123!?$*&()'-=@~")`,
    96  				cty.StringVal("YWJjMTIzIT8kKiYoKSctPUB+"),
    97  			},
    98  		},
    99  
   100  		"base64gzip": {
   101  			{
   102  				`base64gzip("test")`,
   103  				cty.StringVal("H4sIAAAAAAAA/ypJLS4BAAAA//8BAAD//wx+f9gEAAAA"),
   104  			},
   105  		},
   106  
   107  		"base64sha256": {
   108  			{
   109  				`base64sha256("test")`,
   110  				cty.StringVal("n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="),
   111  			},
   112  		},
   113  
   114  		"base64sha512": {
   115  			{
   116  				`base64sha512("test")`,
   117  				cty.StringVal("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="),
   118  			},
   119  		},
   120  
   121  		"basename": {
   122  			{
   123  				`basename("testdata/hello.txt")`,
   124  				cty.StringVal("hello.txt"),
   125  			},
   126  		},
   127  
   128  		"can": {
   129  			{
   130  				`can(true)`,
   131  				cty.True,
   132  			},
   133  			{
   134  				// Note: "can" only works with expressions that pass static
   135  				// validation, because it only gets an opportunity to run in
   136  				// that case. The following "works" (captures the error) because
   137  				// Terraform understands it as a reference to an attribute
   138  				// that does not exist during dynamic evaluation.
   139  				//
   140  				// "can" doesn't work with references that could never possibly
   141  				// be valid and are thus caught during static validation, such
   142  				// as an expression like "foo" alone which would be understood
   143  				// as an invalid resource reference.
   144  				`can({}.baz)`,
   145  				cty.False,
   146  			},
   147  		},
   148  
   149  		"ceil": {
   150  			{
   151  				`ceil(1.2)`,
   152  				cty.NumberIntVal(2),
   153  			},
   154  		},
   155  
   156  		"chomp": {
   157  			{
   158  				`chomp("goodbye\ncruel\nworld\n")`,
   159  				cty.StringVal("goodbye\ncruel\nworld"),
   160  			},
   161  		},
   162  
   163  		"chunklist": {
   164  			{
   165  				`chunklist(["a", "b", "c"], 1)`,
   166  				cty.ListVal([]cty.Value{
   167  					cty.ListVal([]cty.Value{
   168  						cty.StringVal("a"),
   169  					}),
   170  					cty.ListVal([]cty.Value{
   171  						cty.StringVal("b"),
   172  					}),
   173  					cty.ListVal([]cty.Value{
   174  						cty.StringVal("c"),
   175  					}),
   176  				}),
   177  			},
   178  		},
   179  
   180  		"cidrhost": {
   181  			{
   182  				`cidrhost("192.168.1.0/24", 5)`,
   183  				cty.StringVal("192.168.1.5"),
   184  			},
   185  		},
   186  
   187  		"cidrnetmask": {
   188  			{
   189  				`cidrnetmask("192.168.1.0/24")`,
   190  				cty.StringVal("255.255.255.0"),
   191  			},
   192  		},
   193  
   194  		"cidrsubnet": {
   195  			{
   196  				`cidrsubnet("192.168.2.0/20", 4, 6)`,
   197  				cty.StringVal("192.168.6.0/24"),
   198  			},
   199  		},
   200  
   201  		"cidrsubnets": {
   202  			{
   203  				`cidrsubnets("10.0.0.0/8", 8, 8, 16, 8)`,
   204  				cty.ListVal([]cty.Value{
   205  					cty.StringVal("10.0.0.0/16"),
   206  					cty.StringVal("10.1.0.0/16"),
   207  					cty.StringVal("10.2.0.0/24"),
   208  					cty.StringVal("10.3.0.0/16"),
   209  				}),
   210  			},
   211  		},
   212  
   213  		"coalesce": {
   214  			{
   215  				`coalesce("first", "second", "third")`,
   216  				cty.StringVal("first"),
   217  			},
   218  
   219  			{
   220  				`coalescelist(["first", "second"], ["third", "fourth"])`,
   221  				cty.TupleVal([]cty.Value{
   222  					cty.StringVal("first"), cty.StringVal("second"),
   223  				}),
   224  			},
   225  		},
   226  
   227  		"coalescelist": {
   228  			{
   229  				`coalescelist(tolist(["a", "b"]), tolist(["c", "d"]))`,
   230  				cty.ListVal([]cty.Value{
   231  					cty.StringVal("a"),
   232  					cty.StringVal("b"),
   233  				}),
   234  			},
   235  			{
   236  				`coalescelist(["a", "b"], ["c", "d"])`,
   237  				cty.TupleVal([]cty.Value{
   238  					cty.StringVal("a"),
   239  					cty.StringVal("b"),
   240  				}),
   241  			},
   242  		},
   243  
   244  		"compact": {
   245  			{
   246  				`compact(["test", "", "test"])`,
   247  				cty.ListVal([]cty.Value{
   248  					cty.StringVal("test"), cty.StringVal("test"),
   249  				}),
   250  			},
   251  		},
   252  
   253  		"concat": {
   254  			{
   255  				`concat(["a", ""], ["b", "c"])`,
   256  				cty.TupleVal([]cty.Value{
   257  					cty.StringVal("a"),
   258  					cty.StringVal(""),
   259  					cty.StringVal("b"),
   260  					cty.StringVal("c"),
   261  				}),
   262  			},
   263  		},
   264  
   265  		"contains": {
   266  			{
   267  				`contains(["a", "b"], "a")`,
   268  				cty.True,
   269  			},
   270  			{ // Should also work with sets, due to automatic conversion
   271  				`contains(toset(["a", "b"]), "a")`,
   272  				cty.True,
   273  			},
   274  		},
   275  
   276  		"csvdecode": {
   277  			{
   278  				`csvdecode("a,b,c\n1,2,3\n4,5,6")`,
   279  				cty.ListVal([]cty.Value{
   280  					cty.ObjectVal(map[string]cty.Value{
   281  						"a": cty.StringVal("1"),
   282  						"b": cty.StringVal("2"),
   283  						"c": cty.StringVal("3"),
   284  					}),
   285  					cty.ObjectVal(map[string]cty.Value{
   286  						"a": cty.StringVal("4"),
   287  						"b": cty.StringVal("5"),
   288  						"c": cty.StringVal("6"),
   289  					}),
   290  				}),
   291  			},
   292  		},
   293  
   294  		"dirname": {
   295  			{
   296  				`dirname("testdata/hello.txt")`,
   297  				cty.StringVal("testdata"),
   298  			},
   299  		},
   300  
   301  		"distinct": {
   302  			{
   303  				`distinct(["a", "b", "a", "b"])`,
   304  				cty.ListVal([]cty.Value{
   305  					cty.StringVal("a"), cty.StringVal("b"),
   306  				}),
   307  			},
   308  		},
   309  
   310  		"element": {
   311  			{
   312  				`element(["hello"], 0)`,
   313  				cty.StringVal("hello"),
   314  			},
   315  		},
   316  
   317  		"endswith": {
   318  			{
   319  				`endswith("hello world", "world")`,
   320  				cty.True,
   321  			},
   322  			{
   323  				`endswith("hello world", "hello")`,
   324  				cty.False,
   325  			},
   326  			{
   327  				`endswith("hello world", "")`,
   328  				cty.True,
   329  				// Completely empty suffix value  ( "" )
   330  				// will always evaluate to true for all strings.
   331  			},
   332  			{
   333  				`endswith("hello world", " ")`,
   334  				cty.False,
   335  			},
   336  			{
   337  				`endswith("", "")`,
   338  				cty.True,
   339  			},
   340  			{
   341  				`endswith("", " ")`,
   342  				cty.False,
   343  			},
   344  			{
   345  				`endswith(" ", "")`,
   346  				cty.True,
   347  			},
   348  			{
   349  				`endswith("", "hello")`,
   350  				cty.False,
   351  			},
   352  			{
   353  				`endswith(" ", "hello")`,
   354  				cty.False,
   355  			},
   356  		},
   357  
   358  		"file": {
   359  			{
   360  				`file("hello.txt")`,
   361  				cty.StringVal("hello!"),
   362  			},
   363  		},
   364  
   365  		"fileexists": {
   366  			{
   367  				`fileexists("hello.txt")`,
   368  				cty.BoolVal(true),
   369  			},
   370  		},
   371  
   372  		"fileset": {
   373  			{
   374  				`fileset(".", "*/hello.*")`,
   375  				cty.SetVal([]cty.Value{
   376  					cty.StringVal("subdirectory/hello.tmpl"),
   377  					cty.StringVal("subdirectory/hello.txt"),
   378  				}),
   379  			},
   380  			{
   381  				`fileset(".", "subdirectory/hello.*")`,
   382  				cty.SetVal([]cty.Value{
   383  					cty.StringVal("subdirectory/hello.tmpl"),
   384  					cty.StringVal("subdirectory/hello.txt"),
   385  				}),
   386  			},
   387  			{
   388  				`fileset(".", "hello.*")`,
   389  				cty.SetVal([]cty.Value{
   390  					cty.StringVal("hello.tmpl"),
   391  					cty.StringVal("hello.txt"),
   392  				}),
   393  			},
   394  			{
   395  				`fileset("subdirectory", "hello.*")`,
   396  				cty.SetVal([]cty.Value{
   397  					cty.StringVal("hello.tmpl"),
   398  					cty.StringVal("hello.txt"),
   399  				}),
   400  			},
   401  		},
   402  
   403  		"filebase64": {
   404  			{
   405  				`filebase64("hello.txt")`,
   406  				cty.StringVal("aGVsbG8h"),
   407  			},
   408  		},
   409  
   410  		"filebase64sha256": {
   411  			{
   412  				`filebase64sha256("hello.txt")`,
   413  				cty.StringVal("zgYJL7lI2f+sfRo3bkBLJrdXW8wR7gWkYV/vT+w6MIs="),
   414  			},
   415  		},
   416  
   417  		"filebase64sha512": {
   418  			{
   419  				`filebase64sha512("hello.txt")`,
   420  				cty.StringVal("xvgdsOn4IGyXHJ5YJuO6gj/7saOpAPgEdlKov3jqmP38dFhVo4U6Y1Z1RY620arxIJ6I6tLRkjgrXEy91oUOAg=="),
   421  			},
   422  		},
   423  
   424  		"filemd5": {
   425  			{
   426  				`filemd5("hello.txt")`,
   427  				cty.StringVal("5a8dd3ad0756a93ded72b823b19dd877"),
   428  			},
   429  		},
   430  
   431  		"filesha1": {
   432  			{
   433  				`filesha1("hello.txt")`,
   434  				cty.StringVal("8f7d88e901a5ad3a05d8cc0de93313fd76028f8c"),
   435  			},
   436  		},
   437  
   438  		"filesha256": {
   439  			{
   440  				`filesha256("hello.txt")`,
   441  				cty.StringVal("ce06092fb948d9ffac7d1a376e404b26b7575bcc11ee05a4615fef4fec3a308b"),
   442  			},
   443  		},
   444  
   445  		"filesha512": {
   446  			{
   447  				`filesha512("hello.txt")`,
   448  				cty.StringVal("c6f81db0e9f8206c971c9e5826e3ba823ffbb1a3a900f8047652a8bf78ea98fdfc745855a3853a635675458eb6d1aaf1209e88ead2d192382b5c4cbdd6850e02"),
   449  			},
   450  		},
   451  
   452  		"flatten": {
   453  			{
   454  				`flatten([["a", "b"], ["c", "d"]])`,
   455  				cty.TupleVal([]cty.Value{
   456  					cty.StringVal("a"),
   457  					cty.StringVal("b"),
   458  					cty.StringVal("c"),
   459  					cty.StringVal("d"),
   460  				}),
   461  			},
   462  		},
   463  
   464  		"floor": {
   465  			{
   466  				`floor(-1.8)`,
   467  				cty.NumberFloatVal(-2),
   468  			},
   469  		},
   470  
   471  		"format": {
   472  			{
   473  				`format("Hello, %s!", "Ander")`,
   474  				cty.StringVal("Hello, Ander!"),
   475  			},
   476  		},
   477  
   478  		"formatlist": {
   479  			{
   480  				`formatlist("Hello, %s!", ["Valentina", "Ander", "Olivia", "Sam"])`,
   481  				cty.ListVal([]cty.Value{
   482  					cty.StringVal("Hello, Valentina!"),
   483  					cty.StringVal("Hello, Ander!"),
   484  					cty.StringVal("Hello, Olivia!"),
   485  					cty.StringVal("Hello, Sam!"),
   486  				}),
   487  			},
   488  		},
   489  
   490  		"formatdate": {
   491  			{
   492  				`formatdate("DD MMM YYYY hh:mm ZZZ", "2018-01-04T23:12:01Z")`,
   493  				cty.StringVal("04 Jan 2018 23:12 UTC"),
   494  			},
   495  		},
   496  
   497  		"indent": {
   498  			{
   499  				fmt.Sprintf("indent(4, %#v)", Poem),
   500  				cty.StringVal("Fleas:\n    Adam\n    Had'em\n    \n    E.E. Cummings"),
   501  			},
   502  		},
   503  
   504  		"index": {
   505  			{
   506  				`index(["a", "b", "c"], "a")`,
   507  				cty.NumberIntVal(0),
   508  			},
   509  		},
   510  
   511  		"join": {
   512  			{
   513  				`join(" ", ["Hello", "World"])`,
   514  				cty.StringVal("Hello World"),
   515  			},
   516  		},
   517  
   518  		"jsondecode": {
   519  			{
   520  				`jsondecode("{\"hello\": \"world\"}")`,
   521  				cty.ObjectVal(map[string]cty.Value{
   522  					"hello": cty.StringVal("world"),
   523  				}),
   524  			},
   525  		},
   526  
   527  		"jsonencode": {
   528  			{
   529  				`jsonencode({"hello"="world"})`,
   530  				cty.StringVal("{\"hello\":\"world\"}"),
   531  			},
   532  			// We are intentionally choosing to escape <, >, and & characters
   533  			// to preserve backwards compatibility with Terraform 0.11
   534  			{
   535  				`jsonencode({"hello"="<cats & kittens>"})`,
   536  				cty.StringVal("{\"hello\":\"\\u003ccats \\u0026 kittens\\u003e\"}"),
   537  			},
   538  		},
   539  
   540  		"keys": {
   541  			{
   542  				`keys({"hello"=1, "goodbye"=42})`,
   543  				cty.TupleVal([]cty.Value{
   544  					cty.StringVal("goodbye"),
   545  					cty.StringVal("hello"),
   546  				}),
   547  			},
   548  		},
   549  
   550  		"length": {
   551  			{
   552  				`length(["the", "quick", "brown", "bear"])`,
   553  				cty.NumberIntVal(4),
   554  			},
   555  		},
   556  
   557  		"list": {
   558  			// There are intentionally no test cases for "list" because
   559  			// it is a stub that always returns an error.
   560  		},
   561  
   562  		"log": {
   563  			{
   564  				`log(1, 10)`,
   565  				cty.NumberFloatVal(0),
   566  			},
   567  		},
   568  
   569  		"lookup": {
   570  			{
   571  				`lookup({hello=1, goodbye=42}, "goodbye")`,
   572  				cty.NumberIntVal(42),
   573  			},
   574  		},
   575  
   576  		"lower": {
   577  			{
   578  				`lower("HELLO")`,
   579  				cty.StringVal("hello"),
   580  			},
   581  		},
   582  
   583  		"map": {
   584  			// There are intentionally no test cases for "map" because
   585  			// it is a stub that always returns an error.
   586  		},
   587  
   588  		"matchkeys": {
   589  			{
   590  				`matchkeys(["a", "b", "c"], ["ref1", "ref2", "ref3"], ["ref1"])`,
   591  				cty.ListVal([]cty.Value{
   592  					cty.StringVal("a"),
   593  				}),
   594  			},
   595  			{ // mixing types in searchset
   596  				`matchkeys(["a", "b", "c"], [1, 2, 3], [1, "3"])`,
   597  				cty.ListVal([]cty.Value{
   598  					cty.StringVal("a"),
   599  					cty.StringVal("c"),
   600  				}),
   601  			},
   602  		},
   603  
   604  		"max": {
   605  			{
   606  				`max(12, 54, 3)`,
   607  				cty.NumberIntVal(54),
   608  			},
   609  		},
   610  
   611  		"md5": {
   612  			{
   613  				`md5("tada")`,
   614  				cty.StringVal("ce47d07243bb6eaf5e1322c81baf9bbf"),
   615  			},
   616  		},
   617  
   618  		"merge": {
   619  			{
   620  				`merge({"a"="b"}, {"c"="d"})`,
   621  				cty.ObjectVal(map[string]cty.Value{
   622  					"a": cty.StringVal("b"),
   623  					"c": cty.StringVal("d"),
   624  				}),
   625  			},
   626  		},
   627  
   628  		"min": {
   629  			{
   630  				`min(12, 54, 3)`,
   631  				cty.NumberIntVal(3),
   632  			},
   633  		},
   634  
   635  		"nonsensitive": {
   636  			{
   637  				// Due to how this test is set up we have no way to get
   638  				// a sensitive value other than to generate one with
   639  				// another function, so this is a bit odd but does still
   640  				// meet the goal of verifying that the "nonsensitive"
   641  				// function is correctly registered.
   642  				`nonsensitive(sensitive(1))`,
   643  				cty.NumberIntVal(1),
   644  			},
   645  		},
   646  
   647  		"one": {
   648  			{
   649  				`one([])`,
   650  				cty.NullVal(cty.DynamicPseudoType),
   651  			},
   652  			{
   653  				`one([true])`,
   654  				cty.True,
   655  			},
   656  		},
   657  
   658  		"parseint": {
   659  			{
   660  				`parseint("100", 10)`,
   661  				cty.NumberIntVal(100),
   662  			},
   663  		},
   664  
   665  		"pathexpand": {
   666  			{
   667  				`pathexpand("~/test-file")`,
   668  				cty.StringVal(filepath.Join(homePath, "test-file")),
   669  			},
   670  		},
   671  
   672  		"pow": {
   673  			{
   674  				`pow(1,0)`,
   675  				cty.NumberFloatVal(1),
   676  			},
   677  		},
   678  
   679  		"range": {
   680  			{
   681  				`range(3)`,
   682  				cty.ListVal([]cty.Value{
   683  					cty.NumberIntVal(0),
   684  					cty.NumberIntVal(1),
   685  					cty.NumberIntVal(2),
   686  				}),
   687  			},
   688  			{
   689  				`range(1, 4)`,
   690  				cty.ListVal([]cty.Value{
   691  					cty.NumberIntVal(1),
   692  					cty.NumberIntVal(2),
   693  					cty.NumberIntVal(3),
   694  				}),
   695  			},
   696  			{
   697  				`range(1, 8, 2)`,
   698  				cty.ListVal([]cty.Value{
   699  					cty.NumberIntVal(1),
   700  					cty.NumberIntVal(3),
   701  					cty.NumberIntVal(5),
   702  					cty.NumberIntVal(7),
   703  				}),
   704  			},
   705  		},
   706  
   707  		"regex": {
   708  			{
   709  				`regex("(\\d+)([a-z]+)", "aaa111bbb222")`,
   710  				cty.TupleVal([]cty.Value{cty.StringVal("111"), cty.StringVal("bbb")}),
   711  			},
   712  		},
   713  
   714  		"regexall": {
   715  			{
   716  				`regexall("(\\d+)([a-z]+)", "...111aaa222bbb...")`,
   717  				cty.ListVal([]cty.Value{
   718  					cty.TupleVal([]cty.Value{cty.StringVal("111"), cty.StringVal("aaa")}),
   719  					cty.TupleVal([]cty.Value{cty.StringVal("222"), cty.StringVal("bbb")}),
   720  				}),
   721  			},
   722  		},
   723  
   724  		"replace": {
   725  			{
   726  				`replace("hello", "hel", "bel")`,
   727  				cty.StringVal("bello"),
   728  			},
   729  		},
   730  
   731  		"reverse": {
   732  			{
   733  				`reverse(["a", true, 0])`,
   734  				cty.TupleVal([]cty.Value{cty.Zero, cty.True, cty.StringVal("a")}),
   735  			},
   736  		},
   737  
   738  		"rsadecrypt": {
   739  			{
   740  				fmt.Sprintf("rsadecrypt(%#v, %#v)", CipherBase64, PrivateKey),
   741  				cty.StringVal("message"),
   742  			},
   743  		},
   744  
   745  		"sensitive": {
   746  			{
   747  				`sensitive(1)`,
   748  				cty.NumberIntVal(1).Mark(marks.Sensitive),
   749  			},
   750  		},
   751  
   752  		"setintersection": {
   753  			{
   754  				`setintersection(["a", "b"], ["b", "c"], ["b", "d"])`,
   755  				cty.SetVal([]cty.Value{
   756  					cty.StringVal("b"),
   757  				}),
   758  			},
   759  		},
   760  
   761  		"setproduct": {
   762  			{
   763  				`setproduct(["development", "staging", "production"], ["app1", "app2"])`,
   764  				cty.ListVal([]cty.Value{
   765  					cty.TupleVal([]cty.Value{cty.StringVal("development"), cty.StringVal("app1")}),
   766  					cty.TupleVal([]cty.Value{cty.StringVal("development"), cty.StringVal("app2")}),
   767  					cty.TupleVal([]cty.Value{cty.StringVal("staging"), cty.StringVal("app1")}),
   768  					cty.TupleVal([]cty.Value{cty.StringVal("staging"), cty.StringVal("app2")}),
   769  					cty.TupleVal([]cty.Value{cty.StringVal("production"), cty.StringVal("app1")}),
   770  					cty.TupleVal([]cty.Value{cty.StringVal("production"), cty.StringVal("app2")}),
   771  				}),
   772  			},
   773  		},
   774  
   775  		"setsubtract": {
   776  			{
   777  				`setsubtract(["a", "b", "c"], ["a", "c"])`,
   778  				cty.SetVal([]cty.Value{
   779  					cty.StringVal("b"),
   780  				}),
   781  			},
   782  		},
   783  
   784  		"setunion": {
   785  			{
   786  				`setunion(["a", "b"], ["b", "c"], ["d"])`,
   787  				cty.SetVal([]cty.Value{
   788  					cty.StringVal("d"),
   789  					cty.StringVal("b"),
   790  					cty.StringVal("a"),
   791  					cty.StringVal("c"),
   792  				}),
   793  			},
   794  		},
   795  
   796  		"sha1": {
   797  			{
   798  				`sha1("test")`,
   799  				cty.StringVal("a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"),
   800  			},
   801  		},
   802  
   803  		"sha256": {
   804  			{
   805  				`sha256("test")`,
   806  				cty.StringVal("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"),
   807  			},
   808  		},
   809  
   810  		"sha512": {
   811  			{
   812  				`sha512("test")`,
   813  				cty.StringVal("ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff"),
   814  			},
   815  		},
   816  
   817  		"signum": {
   818  			{
   819  				`signum(12)`,
   820  				cty.NumberFloatVal(1),
   821  			},
   822  		},
   823  
   824  		"slice": {
   825  			{
   826  				// force a list type here for testing
   827  				`slice(tolist(["a", "b", "c", "d"]), 1, 3)`,
   828  				cty.ListVal([]cty.Value{
   829  					cty.StringVal("b"), cty.StringVal("c"),
   830  				}),
   831  			},
   832  			{
   833  				`slice(["a", "b", 3, 4], 1, 3)`,
   834  				cty.TupleVal([]cty.Value{
   835  					cty.StringVal("b"), cty.NumberIntVal(3),
   836  				}),
   837  			},
   838  		},
   839  
   840  		"sort": {
   841  			{
   842  				`sort(["banana", "apple"])`,
   843  				cty.ListVal([]cty.Value{
   844  					cty.StringVal("apple"),
   845  					cty.StringVal("banana"),
   846  				}),
   847  			},
   848  		},
   849  
   850  		"split": {
   851  			{
   852  				`split(" ", "Hello World")`,
   853  				cty.ListVal([]cty.Value{
   854  					cty.StringVal("Hello"),
   855  					cty.StringVal("World"),
   856  				}),
   857  			},
   858  		},
   859  
   860  		"startswith": {
   861  			{
   862  				`startswith("hello world", "hello")`,
   863  				cty.True,
   864  			},
   865  			{
   866  				`startswith("hello world", "world")`,
   867  				cty.False,
   868  			},
   869  			{
   870  				`startswith("hello world", "")`,
   871  				cty.True,
   872  				// Completely empty prefix value  ( "" )
   873  				// will always evaluate to true for all strings.
   874  			},
   875  			{
   876  				`startswith("hello world", " ")`,
   877  				cty.False,
   878  			},
   879  			{
   880  				`startswith("", "")`,
   881  				cty.True,
   882  			},
   883  			{
   884  				`startswith("", " ")`,
   885  				cty.False,
   886  			},
   887  			{
   888  				`startswith(" ", "")`,
   889  				cty.True,
   890  			},
   891  			{
   892  				`startswith("", "hello")`,
   893  				cty.False,
   894  			},
   895  			{
   896  				`startswith(" ", "hello")`,
   897  				cty.False,
   898  			},
   899  		},
   900  
   901  		"strrev": {
   902  			{
   903  				`strrev("hello world")`,
   904  				cty.StringVal("dlrow olleh"),
   905  			},
   906  		},
   907  
   908  		"substr": {
   909  			{
   910  				`substr("hello world", 1, 4)`,
   911  				cty.StringVal("ello"),
   912  			},
   913  		},
   914  
   915  		"sum": {
   916  			{
   917  				`sum([2340.5,10,3])`,
   918  				cty.NumberFloatVal(2353.5),
   919  			},
   920  		},
   921  
   922  		"textdecodebase64": {
   923  			{
   924  				`textdecodebase64("dABlAHMAdAA=", "UTF-16LE")`,
   925  				cty.StringVal("test"),
   926  			},
   927  		},
   928  
   929  		"textencodebase64": {
   930  			{
   931  				`textencodebase64("test", "UTF-16LE")`,
   932  				cty.StringVal("dABlAHMAdAA="),
   933  			},
   934  		},
   935  
   936  		"templatefile": {
   937  			{
   938  				`templatefile("hello.tmpl", {name = "Jodie"})`,
   939  				cty.StringVal("Hello, Jodie!"),
   940  			},
   941  		},
   942  
   943  		"timeadd": {
   944  			{
   945  				`timeadd("2017-11-22T00:00:00Z", "1s")`,
   946  				cty.StringVal("2017-11-22T00:00:01Z"),
   947  			},
   948  		},
   949  
   950  		"timecmp": {
   951  			{
   952  				`timecmp("2017-11-22T00:00:00Z", "2017-11-22T00:00:00Z")`,
   953  				cty.Zero,
   954  			},
   955  		},
   956  
   957  		"title": {
   958  			{
   959  				`title("hello")`,
   960  				cty.StringVal("Hello"),
   961  			},
   962  		},
   963  
   964  		"tobool": {
   965  			{
   966  				`tobool("false")`,
   967  				cty.False,
   968  			},
   969  		},
   970  
   971  		"tolist": {
   972  			{
   973  				`tolist(["a", "b", "c"])`,
   974  				cty.ListVal([]cty.Value{
   975  					cty.StringVal("a"), cty.StringVal("b"), cty.StringVal("c"),
   976  				}),
   977  			},
   978  		},
   979  
   980  		"tomap": {
   981  			{
   982  				`tomap({"a" = 1, "b" = 2})`,
   983  				cty.MapVal(map[string]cty.Value{
   984  					"a": cty.NumberIntVal(1),
   985  					"b": cty.NumberIntVal(2),
   986  				}),
   987  			},
   988  		},
   989  
   990  		"tonumber": {
   991  			{
   992  				`tonumber("42")`,
   993  				cty.NumberIntVal(42),
   994  			},
   995  		},
   996  
   997  		"toset": {
   998  			{
   999  				`toset(["a", "b", "c"])`,
  1000  				cty.SetVal([]cty.Value{
  1001  					cty.StringVal("a"), cty.StringVal("b"), cty.StringVal("c"),
  1002  				}),
  1003  			},
  1004  		},
  1005  
  1006  		"tostring": {
  1007  			{
  1008  				`tostring("a")`,
  1009  				cty.StringVal("a"),
  1010  			},
  1011  		},
  1012  
  1013  		"transpose": {
  1014  			{
  1015  				`transpose({"a" = ["1", "2"], "b" = ["2", "3"]})`,
  1016  				cty.MapVal(map[string]cty.Value{
  1017  					"1": cty.ListVal([]cty.Value{cty.StringVal("a")}),
  1018  					"2": cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}),
  1019  					"3": cty.ListVal([]cty.Value{cty.StringVal("b")}),
  1020  				}),
  1021  			},
  1022  		},
  1023  
  1024  		"trim": {
  1025  			{
  1026  				`trim("?!hello?!", "!?")`,
  1027  				cty.StringVal("hello"),
  1028  			},
  1029  		},
  1030  
  1031  		"trimprefix": {
  1032  			{
  1033  				`trimprefix("helloworld", "hello")`,
  1034  				cty.StringVal("world"),
  1035  			},
  1036  		},
  1037  
  1038  		"trimspace": {
  1039  			{
  1040  				`trimspace(" hello ")`,
  1041  				cty.StringVal("hello"),
  1042  			},
  1043  		},
  1044  
  1045  		"trimsuffix": {
  1046  			{
  1047  				`trimsuffix("helloworld", "world")`,
  1048  				cty.StringVal("hello"),
  1049  			},
  1050  		},
  1051  
  1052  		"try": {
  1053  			{
  1054  				// Note: "try" only works with expressions that pass static
  1055  				// validation, because it only gets an opportunity to run in
  1056  				// that case. The following "works" (captures the error) because
  1057  				// Terraform understands it as a reference to an attribute
  1058  				// that does not exist during dynamic evaluation.
  1059  				//
  1060  				// "try" doesn't work with references that could never possibly
  1061  				// be valid and are thus caught during static validation, such
  1062  				// as an expression like "foo" alone which would be understood
  1063  				// as an invalid resource reference. That's okay because this
  1064  				// function exists primarily to ease access to dynamically-typed
  1065  				// structures that Terraform can't statically validate by
  1066  				// definition.
  1067  				`try({}.baz, "fallback")`,
  1068  				cty.StringVal("fallback"),
  1069  			},
  1070  			{
  1071  				`try("fallback")`,
  1072  				cty.StringVal("fallback"),
  1073  			},
  1074  		},
  1075  
  1076  		"upper": {
  1077  			{
  1078  				`upper("hello")`,
  1079  				cty.StringVal("HELLO"),
  1080  			},
  1081  		},
  1082  
  1083  		"urlencode": {
  1084  			{
  1085  				`urlencode("foo:bar@localhost?foo=bar&bar=baz")`,
  1086  				cty.StringVal("foo%3Abar%40localhost%3Ffoo%3Dbar%26bar%3Dbaz"),
  1087  			},
  1088  		},
  1089  
  1090  		"uuidv5": {
  1091  			{
  1092  				`uuidv5("dns", "tada")`,
  1093  				cty.StringVal("faa898db-9b9d-5b75-86a9-149e7bb8e3b8"),
  1094  			},
  1095  			{
  1096  				`uuidv5("url", "tada")`,
  1097  				cty.StringVal("2c1ff6b4-211f-577e-94de-d978b0caa16e"),
  1098  			},
  1099  			{
  1100  				`uuidv5("oid", "tada")`,
  1101  				cty.StringVal("61eeea26-5176-5288-87fc-232d6ed30d2f"),
  1102  			},
  1103  			{
  1104  				`uuidv5("x500", "tada")`,
  1105  				cty.StringVal("7e12415e-f7c9-57c3-9e43-52dc9950d264"),
  1106  			},
  1107  			{
  1108  				`uuidv5("6ba7b810-9dad-11d1-80b4-00c04fd430c8", "tada")`,
  1109  				cty.StringVal("faa898db-9b9d-5b75-86a9-149e7bb8e3b8"),
  1110  			},
  1111  		},
  1112  
  1113  		"values": {
  1114  			{
  1115  				`values({"hello"="world", "what's"="up"})`,
  1116  				cty.TupleVal([]cty.Value{
  1117  					cty.StringVal("world"),
  1118  					cty.StringVal("up"),
  1119  				}),
  1120  			},
  1121  		},
  1122  
  1123  		"yamldecode": {
  1124  			{
  1125  				`yamldecode("true")`,
  1126  				cty.True,
  1127  			},
  1128  			{
  1129  				`yamldecode("key: 0ba")`,
  1130  				cty.ObjectVal(map[string]cty.Value{
  1131  					"key": cty.StringVal("0ba"),
  1132  				}),
  1133  			},
  1134  			{
  1135  				`yamldecode("~")`,
  1136  				cty.NullVal(cty.DynamicPseudoType),
  1137  			},
  1138  		},
  1139  
  1140  		"yamlencode": {
  1141  			{
  1142  				`yamlencode(["foo", "bar", true])`,
  1143  				cty.StringVal("- \"foo\"\n- \"bar\"\n- true\n"),
  1144  			},
  1145  			{
  1146  				`yamlencode({a = "b", c = "d"})`,
  1147  				cty.StringVal("\"a\": \"b\"\n\"c\": \"d\"\n"),
  1148  			},
  1149  			{
  1150  				`yamlencode(true)`,
  1151  				// the ... here is an "end of document" marker, produced for implied primitive types only
  1152  				cty.StringVal("true\n...\n"),
  1153  			},
  1154  		},
  1155  
  1156  		"zipmap": {
  1157  			{
  1158  				`zipmap(["hello", "bar"], ["world", "baz"])`,
  1159  				cty.ObjectVal(map[string]cty.Value{
  1160  					"hello": cty.StringVal("world"),
  1161  					"bar":   cty.StringVal("baz"),
  1162  				}),
  1163  			},
  1164  		},
  1165  	}
  1166  
  1167  	experimentalFuncs := map[string]experiments.Experiment{}
  1168  	experimentalFuncs["defaults"] = experiments.ModuleVariableOptionalAttrs
  1169  
  1170  	t.Run("all functions are tested", func(t *testing.T) {
  1171  		data := &dataForTests{} // no variables available; we only need literals here
  1172  		scope := &Scope{
  1173  			Data:    data,
  1174  			BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem
  1175  		}
  1176  
  1177  		// Check that there is at least one test case for each function, omitting
  1178  		// those functions that do not return consistent values
  1179  		allFunctions := scope.Functions()
  1180  
  1181  		// TODO: we can test the impure functions partially by configuring the scope
  1182  		// with PureOnly: true and then verify that they return unknown values of a
  1183  		// suitable type.
  1184  		for _, impureFunc := range impureFunctions {
  1185  			delete(allFunctions, impureFunc)
  1186  		}
  1187  		for f := range scope.Functions() {
  1188  			if _, ok := tests[f]; !ok {
  1189  				t.Errorf("Missing test for function %s\n", f)
  1190  			}
  1191  		}
  1192  	})
  1193  
  1194  	for funcName, funcTests := range tests {
  1195  		t.Run(funcName, func(t *testing.T) {
  1196  
  1197  			// prepareScope starts as a no-op, but if a function is marked as
  1198  			// experimental in our experimentalFuncs table above then we'll
  1199  			// reassign this to be a function that activates the appropriate
  1200  			// experiment.
  1201  			prepareScope := func(t *testing.T, scope *Scope) {}
  1202  
  1203  			if experiment, isExperimental := experimentalFuncs[funcName]; isExperimental {
  1204  				// First, we'll run all of the tests without the experiment
  1205  				// enabled to see that they do actually fail in that case.
  1206  				for _, test := range funcTests {
  1207  					testName := fmt.Sprintf("experimental(%s)", test.src)
  1208  					t.Run(testName, func(t *testing.T) {
  1209  						data := &dataForTests{} // no variables available; we only need literals here
  1210  						scope := &Scope{
  1211  							Data:    data,
  1212  							BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem
  1213  						}
  1214  
  1215  						expr, parseDiags := hclsyntax.ParseExpression([]byte(test.src), "test.hcl", hcl.Pos{Line: 1, Column: 1})
  1216  						if parseDiags.HasErrors() {
  1217  							for _, diag := range parseDiags {
  1218  								t.Error(diag.Error())
  1219  							}
  1220  							return
  1221  						}
  1222  
  1223  						_, diags := scope.EvalExpr(expr, cty.DynamicPseudoType)
  1224  						if !diags.HasErrors() {
  1225  							t.Errorf("experimental function %q succeeded without its experiment %s enabled\nexpr: %s", funcName, experiment.Keyword(), test.src)
  1226  						}
  1227  					})
  1228  				}
  1229  
  1230  				// Now make the experiment active in the scope so that the
  1231  				// function will actually work when we test it below.
  1232  				prepareScope = func(t *testing.T, scope *Scope) {
  1233  					t.Helper()
  1234  					t.Logf("activating experiment %s to test %q", experiment.Keyword(), funcName)
  1235  					experimentsSet := experiments.NewSet()
  1236  					experimentsSet.Add(experiment)
  1237  					scope.SetActiveExperiments(experimentsSet)
  1238  				}
  1239  			}
  1240  
  1241  			for _, test := range funcTests {
  1242  				t.Run(test.src, func(t *testing.T) {
  1243  					data := &dataForTests{} // no variables available; we only need literals here
  1244  					scope := &Scope{
  1245  						Data:    data,
  1246  						BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem
  1247  					}
  1248  					prepareScope(t, scope)
  1249  
  1250  					expr, parseDiags := hclsyntax.ParseExpression([]byte(test.src), "test.hcl", hcl.Pos{Line: 1, Column: 1})
  1251  					if parseDiags.HasErrors() {
  1252  						for _, diag := range parseDiags {
  1253  							t.Error(diag.Error())
  1254  						}
  1255  						return
  1256  					}
  1257  
  1258  					got, diags := scope.EvalExpr(expr, cty.DynamicPseudoType)
  1259  					if diags.HasErrors() {
  1260  						for _, diag := range diags {
  1261  							t.Errorf("%s: %s", diag.Description().Summary, diag.Description().Detail)
  1262  						}
  1263  						return
  1264  					}
  1265  
  1266  					if !test.want.RawEquals(got) {
  1267  						t.Errorf("wrong result\nexpr: %s\ngot:  %#v\nwant: %#v", test.src, got, test.want)
  1268  					}
  1269  				})
  1270  			}
  1271  		})
  1272  	}
  1273  }
  1274  
  1275  const (
  1276  	CipherBase64 = "eczGaDhXDbOFRZGhjx2etVzWbRqWDlmq0bvNt284JHVbwCgObiuyX9uV0LSAMY707IEgMkExJqXmsB4OWKxvB7epRB9G/3+F+pcrQpODlDuL9oDUAsa65zEpYF0Wbn7Oh7nrMQncyUPpyr9WUlALl0gRWytOA23S+y5joa4M34KFpawFgoqTu/2EEH4Xl1zo+0fy73fEto+nfkUY+meuyGZ1nUx/+DljP7ZqxHBFSlLODmtuTMdswUbHbXbWneW51D7Jm7xB8nSdiA2JQNK5+Sg5x8aNfgvFTt/m2w2+qpsyFa5Wjeu6fZmXSl840CA07aXbk9vN4I81WmJyblD/ZA=="
  1277  	PrivateKey   = `
  1278  -----BEGIN RSA PRIVATE KEY-----
  1279  MIIEowIBAAKCAQEAgUElV5mwqkloIrM8ZNZ72gSCcnSJt7+/Usa5G+D15YQUAdf9
  1280  c1zEekTfHgDP+04nw/uFNFaE5v1RbHaPxhZYVg5ZErNCa/hzn+x10xzcepeS3KPV
  1281  Xcxae4MR0BEegvqZqJzN9loXsNL/c3H/B+2Gle3hTxjlWFb3F5qLgR+4Mf4ruhER
  1282  1v6eHQa/nchi03MBpT4UeJ7MrL92hTJYLdpSyCqmr8yjxkKJDVC2uRrr+sTSxfh7
  1283  r6v24u/vp/QTmBIAlNPgadVAZw17iNNb7vjV7Gwl/5gHXonCUKURaV++dBNLrHIZ
  1284  pqcAM8wHRph8mD1EfL9hsz77pHewxolBATV+7QIDAQABAoIBAC1rK+kFW3vrAYm3
  1285  +8/fQnQQw5nec4o6+crng6JVQXLeH32qXShNf8kLLG/Jj0vaYcTPPDZw9JCKkTMQ
  1286  0mKj9XR/5DLbBMsV6eNXXuvJJ3x4iKW5eD9WkLD4FKlNarBRyO7j8sfPTqXW7uat
  1287  NxWdFH7YsSRvNh/9pyQHLWA5OituidMrYbc3EUx8B1GPNyJ9W8Q8znNYLfwYOjU4
  1288  Wv1SLE6qGQQH9Q0WzA2WUf8jklCYyMYTIywAjGb8kbAJlKhmj2t2Igjmqtwt1PYc
  1289  pGlqbtQBDUiWXt5S4YX/1maIQ/49yeNUajjpbJiH3DbhJbHwFTzP3pZ9P9GHOzlG
  1290  kYR+wSECgYEAw/Xida8kSv8n86V3qSY/I+fYQ5V+jDtXIE+JhRnS8xzbOzz3v0WS
  1291  Oo5H+o4nJx5eL3Ghb3Gcm0Jn46dHrxinHbm+3RjXv/X6tlbxIYjRSQfHOTSMCTvd
  1292  qcliF5vC6RCLXuc7R+IWR1Ky6eDEZGtrvt3DyeYABsp9fRUFR/6NluUCgYEAqNsw
  1293  1aSl7WJa27F0DoJdlU9LWerpXcazlJcIdOz/S9QDmSK3RDQTdqfTxRmrxiYI9LEs
  1294  mkOkvzlnnOBMpnZ3ZOU5qIRfprecRIi37KDAOHWGnlC0EWGgl46YLb7/jXiWf0AG
  1295  Y+DfJJNd9i6TbIDWu8254/erAS6bKMhW/3q7f2kCgYAZ7Id/BiKJAWRpqTRBXlvw
  1296  BhXoKvjI2HjYP21z/EyZ+PFPzur/lNaZhIUlMnUfibbwE9pFggQzzf8scM7c7Sf+
  1297  mLoVSdoQ/Rujz7CqvQzi2nKSsM7t0curUIb3lJWee5/UeEaxZcmIufoNUrzohAWH
  1298  BJOIPDM4ssUTLRq7wYM9uQKBgHCBau5OP8gE6mjKuXsZXWUoahpFLKwwwmJUp2vQ
  1299  pOFPJ/6WZOlqkTVT6QPAcPUbTohKrF80hsZqZyDdSfT3peFx4ZLocBrS56m6NmHR
  1300  UYHMvJ8rQm76T1fryHVidz85g3zRmfBeWg8yqT5oFg4LYgfLsPm1gRjOhs8LfPvI
  1301  OLlRAoGBAIZ5Uv4Z3s8O7WKXXUe/lq6j7vfiVkR1NW/Z/WLKXZpnmvJ7FgxN4e56
  1302  RXT7GwNQHIY8eDjDnsHxzrxd+raOxOZeKcMHj3XyjCX3NHfTscnsBPAGYpY/Wxzh
  1303  T8UYnFu6RzkixElTf2rseEav7rkdKkI3LAeIZy7B0HulKKsmqVQ7
  1304  -----END RSA PRIVATE KEY-----
  1305  `
  1306  	Poem = `Fleas:
  1307  Adam
  1308  Had'em
  1309  
  1310  E.E. Cummings`
  1311  )