github.com/opentofu/opentofu@v1.7.1/internal/lang/funcs/filesystem_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package funcs
     7  
     8  import (
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"testing"
    13  
    14  	homedir "github.com/mitchellh/go-homedir"
    15  	"github.com/opentofu/opentofu/internal/lang/marks"
    16  	"github.com/zclconf/go-cty/cty"
    17  	"github.com/zclconf/go-cty/cty/function"
    18  	"github.com/zclconf/go-cty/cty/function/stdlib"
    19  )
    20  
    21  func TestFile(t *testing.T) {
    22  	tests := []struct {
    23  		Path cty.Value
    24  		Want cty.Value
    25  		Err  string
    26  	}{
    27  		{
    28  			cty.StringVal("testdata/hello.txt"),
    29  			cty.StringVal("Hello World"),
    30  			``,
    31  		},
    32  		{
    33  			cty.StringVal("testdata/icon.png"),
    34  			cty.NilVal,
    35  			`contents of "testdata/icon.png" are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead`,
    36  		},
    37  		{
    38  			cty.StringVal("testdata/icon.png").Mark(marks.Sensitive),
    39  			cty.NilVal,
    40  			`contents of (sensitive value) are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead`,
    41  		},
    42  		{
    43  			cty.StringVal("testdata/missing"),
    44  			cty.NilVal,
    45  			`no file exists at "testdata/missing"; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource`,
    46  		},
    47  		{
    48  			cty.StringVal("testdata/missing").Mark(marks.Sensitive),
    49  			cty.NilVal,
    50  			`no file exists at (sensitive value); this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource`,
    51  		},
    52  	}
    53  
    54  	for _, test := range tests {
    55  		t.Run(fmt.Sprintf("File(\".\", %#v)", test.Path), func(t *testing.T) {
    56  			got, err := File(".", test.Path)
    57  
    58  			if test.Err != "" {
    59  				if err == nil {
    60  					t.Fatal("succeeded; want error")
    61  				}
    62  				if got, want := err.Error(), test.Err; got != want {
    63  					t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
    64  				}
    65  				return
    66  			} else if err != nil {
    67  				t.Fatalf("unexpected error: %s", err)
    68  			}
    69  
    70  			if !got.RawEquals(test.Want) {
    71  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
    72  			}
    73  		})
    74  	}
    75  }
    76  
    77  func TestTemplateFile(t *testing.T) {
    78  	tests := []struct {
    79  		Path cty.Value
    80  		Vars cty.Value
    81  		Want cty.Value
    82  		Err  string
    83  	}{
    84  		{
    85  			cty.StringVal("testdata/hello.txt"),
    86  			cty.EmptyObjectVal,
    87  			cty.StringVal("Hello World"),
    88  			``,
    89  		},
    90  		{
    91  			cty.StringVal("testdata/icon.png"),
    92  			cty.EmptyObjectVal,
    93  			cty.NilVal,
    94  			`contents of "testdata/icon.png" are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead`,
    95  		},
    96  		{
    97  			cty.StringVal("testdata/missing"),
    98  			cty.EmptyObjectVal,
    99  			cty.NilVal,
   100  			`no file exists at "testdata/missing"; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource`,
   101  		},
   102  		{
   103  			cty.StringVal("testdata/secrets.txt").Mark(marks.Sensitive),
   104  			cty.EmptyObjectVal,
   105  			cty.NilVal,
   106  			`no file exists at (sensitive value); this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource`,
   107  		},
   108  		{
   109  			cty.StringVal("testdata/hello.tmpl"),
   110  			cty.MapVal(map[string]cty.Value{
   111  				"name": cty.StringVal("Jodie"),
   112  			}),
   113  			cty.StringVal("Hello, Jodie!"),
   114  			``,
   115  		},
   116  		{
   117  			cty.StringVal("testdata/hello.tmpl"),
   118  			cty.MapVal(map[string]cty.Value{
   119  				"name!": cty.StringVal("Jodie"),
   120  			}),
   121  			cty.NilVal,
   122  			`invalid template variable name "name!": must start with a letter, followed by zero or more letters, digits, and underscores`,
   123  		},
   124  		{
   125  			cty.StringVal("testdata/hello.tmpl"),
   126  			cty.ObjectVal(map[string]cty.Value{
   127  				"name": cty.StringVal("Jimbo"),
   128  			}),
   129  			cty.StringVal("Hello, Jimbo!"),
   130  			``,
   131  		},
   132  		{
   133  			cty.StringVal("testdata/hello.tmpl"),
   134  			cty.EmptyObjectVal,
   135  			cty.NilVal,
   136  			`vars map does not contain key "name", referenced at testdata/hello.tmpl:1,10-14`,
   137  		},
   138  		{
   139  			cty.StringVal("testdata/func.tmpl"),
   140  			cty.ObjectVal(map[string]cty.Value{
   141  				"list": cty.ListVal([]cty.Value{
   142  					cty.StringVal("a"),
   143  					cty.StringVal("b"),
   144  					cty.StringVal("c"),
   145  				}),
   146  			}),
   147  			cty.StringVal("The items are a, b, c"),
   148  			``,
   149  		},
   150  		{
   151  			cty.StringVal("testdata/recursive.tmpl"),
   152  			cty.MapValEmpty(cty.String),
   153  			cty.NilVal,
   154  			`maximum recursion depth 1024 reached in testdata/recursive.tmpl:1,3-16, testdata/recursive.tmpl:1,3-16 ... `,
   155  		},
   156  		{
   157  			cty.StringVal("testdata/list.tmpl"),
   158  			cty.ObjectVal(map[string]cty.Value{
   159  				"list": cty.ListVal([]cty.Value{
   160  					cty.StringVal("a"),
   161  					cty.StringVal("b"),
   162  					cty.StringVal("c"),
   163  				}),
   164  			}),
   165  			cty.StringVal("- a\n- b\n- c\n"),
   166  			``,
   167  		},
   168  		{
   169  			cty.StringVal("testdata/list.tmpl"),
   170  			cty.ObjectVal(map[string]cty.Value{
   171  				"list": cty.True,
   172  			}),
   173  			cty.NilVal,
   174  			`testdata/list.tmpl:1,13-17: Iteration over non-iterable value; A value of type bool cannot be used as the collection in a 'for' expression.`,
   175  		},
   176  		{
   177  			cty.StringVal("testdata/bare.tmpl"),
   178  			cty.ObjectVal(map[string]cty.Value{
   179  				"val": cty.True,
   180  			}),
   181  			cty.True, // since this template contains only an interpolation, its true value shines through
   182  			``,
   183  		},
   184  	}
   185  
   186  	templateFileFn := MakeTemplateFileFunc(".", func() map[string]function.Function {
   187  		return map[string]function.Function{
   188  			"join":         stdlib.JoinFunc,
   189  			"templatefile": MakeFileFunc(".", false), // just a placeholder, since templatefile itself overrides this
   190  		}
   191  	})
   192  
   193  	for _, test := range tests {
   194  		t.Run(fmt.Sprintf("TemplateFile(%#v, %#v)", test.Path, test.Vars), func(t *testing.T) {
   195  			got, err := templateFileFn.Call([]cty.Value{test.Path, test.Vars})
   196  
   197  			if argErr, ok := err.(function.ArgError); ok {
   198  				if argErr.Index < 0 || argErr.Index > 1 {
   199  					t.Errorf("ArgError index %d is out of range for templatefile (must be 0 or 1)", argErr.Index)
   200  				}
   201  			}
   202  
   203  			if test.Err != "" {
   204  				if err == nil {
   205  					t.Fatal("succeeded; want error")
   206  				}
   207  				if got, want := err.Error(), test.Err; got != want {
   208  					t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
   209  				}
   210  				return
   211  			} else if err != nil {
   212  				t.Fatalf("unexpected error: %s", err)
   213  			}
   214  
   215  			if !got.RawEquals(test.Want) {
   216  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   217  			}
   218  		})
   219  	}
   220  }
   221  
   222  func Test_templateMaxRecursionDepth(t *testing.T) {
   223  	tests := []struct {
   224  		Input string
   225  		Want  int
   226  		Err   string
   227  	}{
   228  		{
   229  			"",
   230  			1024,
   231  			``,
   232  		}, {
   233  			"4096",
   234  			4096,
   235  			``,
   236  		}, {
   237  			"apple",
   238  			-1,
   239  			`invalid value for TF_TEMPLATE_RECURSION_DEPTH: strconv.Atoi: parsing "apple": invalid syntax`,
   240  		},
   241  	}
   242  
   243  	for _, test := range tests {
   244  		t.Run(fmt.Sprintf("templateMaxRecursion(%s)", test.Input), func(t *testing.T) {
   245  			os.Setenv("TF_TEMPLATE_RECURSION_DEPTH", test.Input)
   246  			got, err := templateMaxRecursionDepth()
   247  			if test.Err != "" {
   248  				if err == nil {
   249  					t.Fatal("succeeded; want error")
   250  				}
   251  				if got, want := err.Error(), test.Err; got != want {
   252  					t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
   253  				}
   254  				return
   255  			} else if err != nil {
   256  				t.Fatalf("unexpected error: %s", err)
   257  			}
   258  
   259  			if got != test.Want {
   260  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   261  			}
   262  		})
   263  	}
   264  }
   265  
   266  func TestFileExists(t *testing.T) {
   267  	tests := []struct {
   268  		Path cty.Value
   269  		Want cty.Value
   270  		Err  string
   271  	}{
   272  		{
   273  			cty.StringVal("testdata/hello.txt"),
   274  			cty.BoolVal(true),
   275  			``,
   276  		},
   277  		{
   278  			cty.StringVal(""),
   279  			cty.BoolVal(false),
   280  			`"." is a directory, not a file`,
   281  		},
   282  		{
   283  			cty.StringVal("testdata").Mark(marks.Sensitive),
   284  			cty.BoolVal(false),
   285  			`(sensitive value) is a directory, not a file`,
   286  		},
   287  		{
   288  			cty.StringVal("testdata/missing"),
   289  			cty.BoolVal(false),
   290  			``,
   291  		},
   292  		{
   293  			cty.StringVal("testdata/unreadable/foobar"),
   294  			cty.BoolVal(false),
   295  			`failed to stat "testdata/unreadable/foobar"`,
   296  		},
   297  		{
   298  			cty.StringVal("testdata/unreadable/foobar").Mark(marks.Sensitive),
   299  			cty.BoolVal(false),
   300  			`failed to stat (sensitive value)`,
   301  		},
   302  	}
   303  
   304  	// Ensure "unreadable" directory cannot be listed during the test run
   305  	fi, err := os.Lstat("testdata/unreadable")
   306  	if err != nil {
   307  		t.Fatal(err)
   308  	}
   309  	os.Chmod("testdata/unreadable", 0000)
   310  	defer func(mode os.FileMode) {
   311  		os.Chmod("testdata/unreadable", mode)
   312  	}(fi.Mode())
   313  
   314  	for _, test := range tests {
   315  		t.Run(fmt.Sprintf("FileExists(\".\", %#v)", test.Path), func(t *testing.T) {
   316  			got, err := FileExists(".", test.Path)
   317  
   318  			if test.Err != "" {
   319  				if err == nil {
   320  					t.Fatal("succeeded; want error")
   321  				}
   322  				if got, want := err.Error(), test.Err; got != want {
   323  					t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
   324  				}
   325  				return
   326  			} else if err != nil {
   327  				t.Fatalf("unexpected error: %s", err)
   328  			}
   329  
   330  			if !got.RawEquals(test.Want) {
   331  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   332  			}
   333  		})
   334  	}
   335  }
   336  
   337  func TestFileSet(t *testing.T) {
   338  	tests := []struct {
   339  		Path    cty.Value
   340  		Pattern cty.Value
   341  		Want    cty.Value
   342  		Err     string
   343  	}{
   344  		{
   345  			cty.StringVal("."),
   346  			cty.StringVal("testdata*"),
   347  			cty.SetValEmpty(cty.String),
   348  			``,
   349  		},
   350  		{
   351  			cty.StringVal("."),
   352  			cty.StringVal("testdata"),
   353  			cty.SetValEmpty(cty.String),
   354  			``,
   355  		},
   356  		{
   357  			cty.StringVal("."),
   358  			cty.StringVal("{testdata,missing}"),
   359  			cty.SetValEmpty(cty.String),
   360  			``,
   361  		},
   362  		{
   363  			cty.StringVal("."),
   364  			cty.StringVal("testdata/missing"),
   365  			cty.SetValEmpty(cty.String),
   366  			``,
   367  		},
   368  		{
   369  			cty.StringVal("."),
   370  			cty.StringVal("testdata/missing*"),
   371  			cty.SetValEmpty(cty.String),
   372  			``,
   373  		},
   374  		{
   375  			cty.StringVal("."),
   376  			cty.StringVal("*/missing"),
   377  			cty.SetValEmpty(cty.String),
   378  			``,
   379  		},
   380  		{
   381  			cty.StringVal("."),
   382  			cty.StringVal("**/missing"),
   383  			cty.SetValEmpty(cty.String),
   384  			``,
   385  		},
   386  		{
   387  			cty.StringVal("."),
   388  			cty.StringVal("testdata/*.txt"),
   389  			cty.SetVal([]cty.Value{
   390  				cty.StringVal("testdata/hello.txt"),
   391  			}),
   392  			``,
   393  		},
   394  		{
   395  			cty.StringVal("."),
   396  			cty.StringVal("testdata/hello.txt"),
   397  			cty.SetVal([]cty.Value{
   398  				cty.StringVal("testdata/hello.txt"),
   399  			}),
   400  			``,
   401  		},
   402  		{
   403  			cty.StringVal("."),
   404  			cty.StringVal("testdata/hello.???"),
   405  			cty.SetVal([]cty.Value{
   406  				cty.StringVal("testdata/hello.txt"),
   407  			}),
   408  			``,
   409  		},
   410  		{
   411  			cty.StringVal("."),
   412  			cty.StringVal("testdata/hello*"),
   413  			cty.SetVal([]cty.Value{
   414  				cty.StringVal("testdata/hello.tmpl"),
   415  				cty.StringVal("testdata/hello.txt"),
   416  			}),
   417  			``,
   418  		},
   419  		{
   420  			cty.StringVal("."),
   421  			cty.StringVal("testdata/hello.{tmpl,txt}"),
   422  			cty.SetVal([]cty.Value{
   423  				cty.StringVal("testdata/hello.tmpl"),
   424  				cty.StringVal("testdata/hello.txt"),
   425  			}),
   426  			``,
   427  		},
   428  		{
   429  			cty.StringVal("."),
   430  			cty.StringVal("*/hello.txt"),
   431  			cty.SetVal([]cty.Value{
   432  				cty.StringVal("testdata/hello.txt"),
   433  			}),
   434  			``,
   435  		},
   436  		{
   437  			cty.StringVal("."),
   438  			cty.StringVal("*/*.txt"),
   439  			cty.SetVal([]cty.Value{
   440  				cty.StringVal("testdata/hello.txt"),
   441  			}),
   442  			``,
   443  		},
   444  		{
   445  			cty.StringVal("."),
   446  			cty.StringVal("*/hello*"),
   447  			cty.SetVal([]cty.Value{
   448  				cty.StringVal("testdata/hello.tmpl"),
   449  				cty.StringVal("testdata/hello.txt"),
   450  			}),
   451  			``,
   452  		},
   453  		{
   454  			cty.StringVal("."),
   455  			cty.StringVal("**/hello*"),
   456  			cty.SetVal([]cty.Value{
   457  				cty.StringVal("testdata/hello.tmpl"),
   458  				cty.StringVal("testdata/hello.txt"),
   459  			}),
   460  			``,
   461  		},
   462  		{
   463  			cty.StringVal("."),
   464  			cty.StringVal("**/hello.{tmpl,txt}"),
   465  			cty.SetVal([]cty.Value{
   466  				cty.StringVal("testdata/hello.tmpl"),
   467  				cty.StringVal("testdata/hello.txt"),
   468  			}),
   469  			``,
   470  		},
   471  		{
   472  			cty.StringVal("."),
   473  			cty.StringVal("["),
   474  			cty.SetValEmpty(cty.String),
   475  			`failed to glob pattern "[": syntax error in pattern`,
   476  		},
   477  		{
   478  			cty.StringVal("."),
   479  			cty.StringVal("[").Mark(marks.Sensitive),
   480  			cty.SetValEmpty(cty.String),
   481  			`failed to glob pattern (sensitive value): syntax error in pattern`,
   482  		},
   483  		{
   484  			cty.StringVal("."),
   485  			cty.StringVal("\\"),
   486  			cty.SetValEmpty(cty.String),
   487  			`failed to glob pattern "\\": syntax error in pattern`,
   488  		},
   489  		{
   490  			cty.StringVal("testdata"),
   491  			cty.StringVal("missing"),
   492  			cty.SetValEmpty(cty.String),
   493  			``,
   494  		},
   495  		{
   496  			cty.StringVal("testdata"),
   497  			cty.StringVal("missing*"),
   498  			cty.SetValEmpty(cty.String),
   499  			``,
   500  		},
   501  		{
   502  			cty.StringVal("testdata"),
   503  			cty.StringVal("*.txt"),
   504  			cty.SetVal([]cty.Value{
   505  				cty.StringVal("hello.txt"),
   506  			}),
   507  			``,
   508  		},
   509  		{
   510  			cty.StringVal("testdata"),
   511  			cty.StringVal("hello.txt"),
   512  			cty.SetVal([]cty.Value{
   513  				cty.StringVal("hello.txt"),
   514  			}),
   515  			``,
   516  		},
   517  		{
   518  			cty.StringVal("testdata"),
   519  			cty.StringVal("hello.???"),
   520  			cty.SetVal([]cty.Value{
   521  				cty.StringVal("hello.txt"),
   522  			}),
   523  			``,
   524  		},
   525  		{
   526  			cty.StringVal("testdata"),
   527  			cty.StringVal("hello*"),
   528  			cty.SetVal([]cty.Value{
   529  				cty.StringVal("hello.tmpl"),
   530  				cty.StringVal("hello.txt"),
   531  			}),
   532  			``,
   533  		},
   534  	}
   535  
   536  	for _, test := range tests {
   537  		t.Run(fmt.Sprintf("FileSet(\".\", %#v, %#v)", test.Path, test.Pattern), func(t *testing.T) {
   538  			got, err := FileSet(".", test.Path, test.Pattern)
   539  
   540  			if test.Err != "" {
   541  				if err == nil {
   542  					t.Fatal("succeeded; want error")
   543  				}
   544  				if got, want := err.Error(), test.Err; got != want {
   545  					t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
   546  				}
   547  				return
   548  			} else if err != nil {
   549  				t.Fatalf("unexpected error: %s", err)
   550  			}
   551  
   552  			if !got.RawEquals(test.Want) {
   553  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   554  			}
   555  		})
   556  	}
   557  }
   558  
   559  func TestFileBase64(t *testing.T) {
   560  	tests := []struct {
   561  		Path cty.Value
   562  		Want cty.Value
   563  		Err  bool
   564  	}{
   565  		{
   566  			cty.StringVal("testdata/hello.txt"),
   567  			cty.StringVal("SGVsbG8gV29ybGQ="),
   568  			false,
   569  		},
   570  		{
   571  			cty.StringVal("testdata/icon.png"),
   572  			cty.StringVal("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAINSURBVHgBjZNdSBRRFMf/M7tttl9zsdBld7EbZdi6xRZR0UPUU0UvPaeLC71JHxCFWg+2RAkhNRVaSLELGRRIQYX41gci9Bz2UMSOkWgY7szO6vqxznVmxGFdd1f/cGDmnnN+5z/DuUAFERohetBKNbZyjQ5ndduxkPpay2vtC7ZabNseGJuTJ2VsJG8wfDV0sD7dc4ewia8ww3jef6g+5Qk0xorrOWtqMHzS48onoqenacvZ//C6tHXwJwM1+DAsSNI/R1wdH01an+D1h2/7dywkBrt/kRORLLY6WEl3R0MzOLxvlgyOkPNcVQ03r0595ldSjEbPLeKKuAtvvxCUk5G72deI5gstYOB2Gmf8arLpzDz6buWg5Kpx6vJejHx3Wz/s2w8XLokHoDjvYujjJ7RejFpQe+GEOp+GjtisDuPRlfSR98M5jE9twZ5wE14k2iB4PWadoqilAUqWh+DWTNDT9ixeDVXhz+INdFxrRTnxhS+9A3rDJG+CLFdBPyppDcCwL7iZCSqEbALASV1Jpz7dZgJWQFrJBiWjovf5S32B2NiahLFlkSMNqQdxmmY/fcyI/seU9b95xwzJSobd6+5hdaHjKbe+dKt9XPEEA7Q7sNR5vTlHzXTtQ/P8vvhM/i39jc9MjIqF9Vwpm8TXQPO8Pabb7CREkNNy5pHdkRVlSdr4MhWDCKWkUs0yuFTBNunfXEAAAAAASUVORK5CYII="),
   573  			false,
   574  		},
   575  		{
   576  			cty.StringVal("testdata/missing"),
   577  			cty.NilVal,
   578  			true, // no file exists
   579  		},
   580  	}
   581  
   582  	for _, test := range tests {
   583  		t.Run(fmt.Sprintf("FileBase64(\".\", %#v)", test.Path), func(t *testing.T) {
   584  			got, err := FileBase64(".", test.Path)
   585  
   586  			if test.Err {
   587  				if err == nil {
   588  					t.Fatal("succeeded; want error")
   589  				}
   590  				return
   591  			} else if err != nil {
   592  				t.Fatalf("unexpected error: %s", err)
   593  			}
   594  
   595  			if !got.RawEquals(test.Want) {
   596  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   597  			}
   598  		})
   599  	}
   600  }
   601  
   602  func TestBasename(t *testing.T) {
   603  	tests := []struct {
   604  		Path cty.Value
   605  		Want cty.Value
   606  		Err  bool
   607  	}{
   608  		{
   609  			cty.StringVal("testdata/hello.txt"),
   610  			cty.StringVal("hello.txt"),
   611  			false,
   612  		},
   613  		{
   614  			cty.StringVal("hello.txt"),
   615  			cty.StringVal("hello.txt"),
   616  			false,
   617  		},
   618  		{
   619  			cty.StringVal(""),
   620  			cty.StringVal("."),
   621  			false,
   622  		},
   623  	}
   624  
   625  	for _, test := range tests {
   626  		t.Run(fmt.Sprintf("Basename(%#v)", test.Path), func(t *testing.T) {
   627  			got, err := Basename(test.Path)
   628  
   629  			if test.Err {
   630  				if err == nil {
   631  					t.Fatal("succeeded; want error")
   632  				}
   633  				return
   634  			} else if err != nil {
   635  				t.Fatalf("unexpected error: %s", err)
   636  			}
   637  
   638  			if !got.RawEquals(test.Want) {
   639  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   640  			}
   641  		})
   642  	}
   643  }
   644  
   645  func TestDirname(t *testing.T) {
   646  	tests := []struct {
   647  		Path cty.Value
   648  		Want cty.Value
   649  		Err  bool
   650  	}{
   651  		{
   652  			cty.StringVal("testdata/hello.txt"),
   653  			cty.StringVal("testdata"),
   654  			false,
   655  		},
   656  		{
   657  			cty.StringVal("testdata/foo/hello.txt"),
   658  			cty.StringVal("testdata/foo"),
   659  			false,
   660  		},
   661  		{
   662  			cty.StringVal("hello.txt"),
   663  			cty.StringVal("."),
   664  			false,
   665  		},
   666  		{
   667  			cty.StringVal(""),
   668  			cty.StringVal("."),
   669  			false,
   670  		},
   671  	}
   672  
   673  	for _, test := range tests {
   674  		t.Run(fmt.Sprintf("Dirname(%#v)", test.Path), func(t *testing.T) {
   675  			got, err := Dirname(test.Path)
   676  
   677  			if test.Err {
   678  				if err == nil {
   679  					t.Fatal("succeeded; want error")
   680  				}
   681  				return
   682  			} else if err != nil {
   683  				t.Fatalf("unexpected error: %s", err)
   684  			}
   685  
   686  			if !got.RawEquals(test.Want) {
   687  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   688  			}
   689  		})
   690  	}
   691  }
   692  
   693  func TestPathExpand(t *testing.T) {
   694  	homePath, err := homedir.Dir()
   695  	if err != nil {
   696  		t.Fatalf("Error getting home directory: %v", err)
   697  	}
   698  
   699  	tests := []struct {
   700  		Path cty.Value
   701  		Want cty.Value
   702  		Err  bool
   703  	}{
   704  		{
   705  			cty.StringVal("~/test-file"),
   706  			cty.StringVal(filepath.Join(homePath, "test-file")),
   707  			false,
   708  		},
   709  		{
   710  			cty.StringVal("~/another/test/file"),
   711  			cty.StringVal(filepath.Join(homePath, "another/test/file")),
   712  			false,
   713  		},
   714  		{
   715  			cty.StringVal("/root/file"),
   716  			cty.StringVal("/root/file"),
   717  			false,
   718  		},
   719  		{
   720  			cty.StringVal("/"),
   721  			cty.StringVal("/"),
   722  			false,
   723  		},
   724  	}
   725  
   726  	for _, test := range tests {
   727  		t.Run(fmt.Sprintf("Dirname(%#v)", test.Path), func(t *testing.T) {
   728  			got, err := Pathexpand(test.Path)
   729  
   730  			if test.Err {
   731  				if err == nil {
   732  					t.Fatal("succeeded; want error")
   733  				}
   734  				return
   735  			} else if err != nil {
   736  				t.Fatalf("unexpected error: %s", err)
   737  			}
   738  
   739  			if !got.RawEquals(test.Want) {
   740  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   741  			}
   742  		})
   743  	}
   744  }