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 )