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 )