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