github.com/mponton/terratest@v0.44.0/modules/terraform/format_test.go (about)

     1  package terraform
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  
     7  	"github.com/stretchr/testify/assert"
     8  )
     9  
    10  func TestFormatTerraformPlanFileAsArgs(t *testing.T) {
    11  	t.Parallel()
    12  
    13  	testCases := []struct {
    14  		command  string
    15  		out      string
    16  		expected []string
    17  	}{
    18  		{"plan", "/some/plan/output", []string{"-out=/some/plan/output"}},
    19  		{"plan", "", nil},
    20  		{"apply", "/some/plan/output", []string{"/some/plan/output"}},
    21  		{"apply", "", nil},
    22  		{"show", "/some/plan/output", []string{"/some/plan/output"}},
    23  		{"show", "", nil},
    24  	}
    25  
    26  	for _, testCase := range testCases {
    27  		checkResultWithRetry(t, 100, testCase.expected, fmt.Sprintf("FormatTerraformPlanFileAsArgs(%v)", testCase.out), func() interface{} {
    28  			return FormatTerraformPlanFileAsArg(testCase.command, testCase.out)
    29  		})
    30  	}
    31  }
    32  
    33  func TestFormatTerraformPluginDirAsArgs(t *testing.T) {
    34  	t.Parallel()
    35  
    36  	testCases := []struct {
    37  		dir      string
    38  		expected []string
    39  	}{
    40  		{"/some/plugin/dir", []string{"-plugin-dir=/some/plugin/dir"}},
    41  		{"", nil},
    42  	}
    43  
    44  	for _, testCase := range testCases {
    45  		checkResultWithRetry(t, 100, testCase.expected, fmt.Sprintf("FormatTerraformPluginDirAsArgs(%v)", testCase.dir), func() interface{} {
    46  			return FormatTerraformPluginDirAsArgs(testCase.dir)
    47  		})
    48  	}
    49  }
    50  
    51  func TestFormatTerraformVarsAsArgs(t *testing.T) {
    52  	t.Parallel()
    53  
    54  	testCases := []struct {
    55  		vars     map[string]interface{}
    56  		expected []string
    57  	}{
    58  		{map[string]interface{}{}, nil},
    59  		{map[string]interface{}{"foo": "bar"}, []string{"-var", "foo=bar"}},
    60  		{map[string]interface{}{"foo": 123}, []string{"-var", "foo=123"}},
    61  		{map[string]interface{}{"foo": true}, []string{"-var", "foo=true"}},
    62  		{map[string]interface{}{"foo": nil}, []string{"-var", "foo=null"}},
    63  		{map[string]interface{}{"foo": []int{1, 2, 3}}, []string{"-var", "foo=[1, 2, 3]"}},
    64  		{map[string]interface{}{"foo": map[string]string{"baz": "blah"}}, []string{"-var", "foo={\"baz\" = \"blah\"}"}},
    65  		{
    66  			map[string]interface{}{"str": "bar", "int": -1, "bool": false, "list": []string{"foo", "bar", "baz"}, "map": map[string]int{"foo": 0}},
    67  			[]string{"-var", "str=bar", "-var", "int=-1", "-var", "bool=false", "-var", "list=[\"foo\", \"bar\", \"baz\"]", "-var", "map={\"foo\" = 0}"},
    68  		},
    69  	}
    70  
    71  	for _, testCase := range testCases {
    72  		checkResultWithRetry(t, 100, testCase.expected, fmt.Sprintf("FormatTerraformVarsAsArgs(%v)", testCase.vars), func() interface{} {
    73  			return FormatTerraformVarsAsArgs(testCase.vars)
    74  		})
    75  	}
    76  }
    77  
    78  func TestPrimitiveToHclString(t *testing.T) {
    79  	t.Parallel()
    80  
    81  	testCases := []struct {
    82  		value    interface{}
    83  		expected string
    84  	}{
    85  		{"", ""},
    86  		{"foo", "foo"},
    87  		{"true", "true"},
    88  		{true, "true"},
    89  		{3, "3"},
    90  		{nil, "null"},
    91  		{[]int{1, 2, 3}, "[1 2 3]"}, // Anything that isn't a primitive is forced into a string
    92  	}
    93  
    94  	for _, testCase := range testCases {
    95  		actual := primitiveToHclString(testCase.value, false)
    96  		assert.Equal(t, testCase.expected, actual, "Value: %v", testCase.value)
    97  	}
    98  }
    99  
   100  func TestMapToHclString(t *testing.T) {
   101  	t.Parallel()
   102  
   103  	testCases := []struct {
   104  		value    map[string]interface{}
   105  		expected string
   106  	}{
   107  		{map[string]interface{}{}, "{}"},
   108  		{map[string]interface{}{"key1": "value1"}, "{\"key1\" = \"value1\"}"},
   109  		{map[string]interface{}{"key1": 123}, "{\"key1\" = 123}"},
   110  		{map[string]interface{}{"key1": true}, "{\"key1\" = true}"},
   111  		{map[string]interface{}{"key1": []int{1, 2, 3}}, "{\"key1\" = [1, 2, 3]}"}, // Any value that isn't a primitive is forced into a string
   112  		{map[string]interface{}{"key1": "value1", "key2": 0, "key3": false}, "{\"key1\" = \"value1\", \"key2\" = 0, \"key3\" = false}"},
   113  		{map[string]interface{}{"key1.a.b.c": "value1"}, "{\"key1.a.b.c\" = \"value1\"}"},
   114  	}
   115  
   116  	for _, testCase := range testCases {
   117  		checkResultWithRetry(t, 100, testCase.expected, fmt.Sprintf("mapToHclString(%v)", testCase.value), func() interface{} {
   118  			return mapToHclString(testCase.value)
   119  		})
   120  	}
   121  }
   122  
   123  // Some of our tests execute code that loops over a map to produce output. The problem is that the order of map
   124  // iteration is generally unpredictable and, to make it even more unpredictable, Go intentionally randomizes the
   125  // iteration order (https://blog.golang.org/go-maps-in-action#TOC_7). Therefore, the order of items in the output
   126  // is unpredictable, and doing a simple assert.Equals call will intermittently fail.
   127  //
   128  // We have a few unsatisfactory ways to solve this problem:
   129  //
   130  //  1. Enforce iteration order. This is easy to do in other languages, where you have built-in sorted maps. In Go, no
   131  //     such map exists, and if you create a custom one, you can't use the range keyword on it
   132  //     (http://stackoverflow.com/a/35810932/483528). As a result, we'd have to modify our implementation code to take
   133  //     iteration order into account which is a totally unnecessary feature that increases complexity.
   134  //  2. We could parse the output string and do an order-independent comparison. However, that adds a bunch of parsing
   135  //     logic into the test code which is a totally unnecessary feature that increases complexity.
   136  //  3. We accept that Go is a shitty language and, if the test fails, we re-run it a bunch of times in the hope that, if
   137  //     the bug is caused by key ordering, we will randomly get the proper order in a future run. The code being tested
   138  //     here is tiny & fast, so doing a hundred retries is still sub millisecond, so while ugly, this provides a very
   139  //     simple solution.
   140  //
   141  // Isn't it great that Go's designers built features into the language to prevent bugs that now force every Go
   142  // developer to write thousands of lines of extra code like this, which is of course likely to contain bugs itself?
   143  func checkResultWithRetry(t *testing.T, maxRetries int, expectedValue interface{}, description string, generateValue func() interface{}) {
   144  	for i := 0; i < maxRetries; i++ {
   145  		actualValue := generateValue()
   146  		if assert.ObjectsAreEqual(expectedValue, actualValue) {
   147  			return
   148  		}
   149  		t.Logf("Retry %d of %s failed: expected %v, got %v", i, description, expectedValue, actualValue)
   150  	}
   151  
   152  	assert.Fail(t, "checkResultWithRetry failed", "After %d retries, %s still not succeeding (see retries above)", description)
   153  }
   154  
   155  func TestSliceToHclString(t *testing.T) {
   156  	t.Parallel()
   157  
   158  	testCases := []struct {
   159  		value    []interface{}
   160  		expected string
   161  	}{
   162  		{[]interface{}{}, "[]"},
   163  		{[]interface{}{"foo"}, "[\"foo\"]"},
   164  		{[]interface{}{123}, "[123]"},
   165  		{[]interface{}{true}, "[true]"},
   166  		{[]interface{}{[]int{1, 2, 3}}, "[[1, 2, 3]]"}, // Any value that isn't a primitive is forced into a string
   167  		{[]interface{}{"foo", 0, false}, "[\"foo\", 0, false]"},
   168  		{[]interface{}{map[string]interface{}{"foo": "bar"}}, "[{\"foo\" = \"bar\"}]"},
   169  		{[]interface{}{map[string]interface{}{"foo": "bar"}, map[string]interface{}{"foo": "bar"}}, "[{\"foo\" = \"bar\"}, {\"foo\" = \"bar\"}]"},
   170  	}
   171  
   172  	for _, testCase := range testCases {
   173  		actual := sliceToHclString(testCase.value)
   174  		assert.Equal(t, testCase.expected, actual, "Value: %v", testCase.value)
   175  	}
   176  }
   177  
   178  func TestToHclString(t *testing.T) {
   179  	t.Parallel()
   180  
   181  	testCases := []struct {
   182  		value    interface{}
   183  		expected string
   184  	}{
   185  		{"", ""},
   186  		{"foo", "foo"},
   187  		{123, "123"},
   188  		{true, "true"},
   189  		{[]int{1, 2, 3}, "[1, 2, 3]"},
   190  		{[]string{"foo", "bar", "baz"}, "[\"foo\", \"bar\", \"baz\"]"},
   191  		{map[string]string{"key1": "value1"}, "{\"key1\" = \"value1\"}"},
   192  		{map[string]int{"key1": 123}, "{\"key1\" = 123}"},
   193  	}
   194  
   195  	for _, testCase := range testCases {
   196  		actual := toHclString(testCase.value, false)
   197  		assert.Equal(t, testCase.expected, actual, "Value: %v", testCase.value)
   198  	}
   199  }
   200  
   201  func TestTryToConvertToGenericSlice(t *testing.T) {
   202  	t.Parallel()
   203  
   204  	testCases := []struct {
   205  		value           interface{}
   206  		expectedSlice   []interface{}
   207  		expectedIsSlice bool
   208  	}{
   209  		{"", []interface{}{}, false},
   210  		{"foo", []interface{}{}, false},
   211  		{true, []interface{}{}, false},
   212  		{531, []interface{}{}, false},
   213  		{map[string]string{"foo": "bar"}, []interface{}{}, false},
   214  		{[]string{}, []interface{}{}, true},
   215  		{[]int{}, []interface{}{}, true},
   216  		{[]bool{}, []interface{}{}, true},
   217  		{[]interface{}{}, []interface{}{}, true},
   218  		{[]string{"foo", "bar", "baz"}, []interface{}{"foo", "bar", "baz"}, true},
   219  		{[]int{1, 2, 3}, []interface{}{1, 2, 3}, true},
   220  		{[]bool{true, true, false}, []interface{}{true, true, false}, true},
   221  		{[]interface{}{"foo", "bar", "baz"}, []interface{}{"foo", "bar", "baz"}, true},
   222  	}
   223  
   224  	for _, testCase := range testCases {
   225  		actualSlice, actualIsSlice := tryToConvertToGenericSlice(testCase.value)
   226  		assert.Equal(t, testCase.expectedSlice, actualSlice, "Value: %v", testCase.value)
   227  		assert.Equal(t, testCase.expectedIsSlice, actualIsSlice, "Value: %v", testCase.value)
   228  	}
   229  }
   230  
   231  func TestTryToConvertToGenericMap(t *testing.T) {
   232  	t.Parallel()
   233  
   234  	testCases := []struct {
   235  		value         interface{}
   236  		expectedMap   map[string]interface{}
   237  		expectedIsMap bool
   238  	}{
   239  		{"", map[string]interface{}{}, false},
   240  		{"foo", map[string]interface{}{}, false},
   241  		{true, map[string]interface{}{}, false},
   242  		{531, map[string]interface{}{}, false},
   243  		{[]string{"foo", "bar"}, map[string]interface{}{}, false},
   244  		{map[int]int{}, map[string]interface{}{}, false},
   245  		{map[bool]string{}, map[string]interface{}{}, false},
   246  		{map[string]string{}, map[string]interface{}{}, true},
   247  		{map[string]int{}, map[string]interface{}{}, true},
   248  		{map[string]bool{}, map[string]interface{}{}, true},
   249  		{map[string]interface{}{}, map[string]interface{}{}, true},
   250  		{map[string]string{"key1": "value1", "key2": "value2"}, map[string]interface{}{"key1": "value1", "key2": "value2"}, true},
   251  		{map[string]int{"key1": 1, "key2": 2, "key3": 3}, map[string]interface{}{"key1": 1, "key2": 2, "key3": 3}, true},
   252  		{map[string]bool{"key1": true}, map[string]interface{}{"key1": true}, true},
   253  		{map[string]interface{}{"key1": "value1"}, map[string]interface{}{"key1": "value1"}, true},
   254  	}
   255  
   256  	for _, testCase := range testCases {
   257  		actualMap, actualIsMap := tryToConvertToGenericMap(testCase.value)
   258  		assert.Equal(t, testCase.expectedMap, actualMap, "Value: %v", testCase.value)
   259  		assert.Equal(t, testCase.expectedIsMap, actualIsMap, "Value: %v", testCase.value)
   260  	}
   261  }
   262  
   263  func TestFormatArgsAppliesLockCorrectly(t *testing.T) {
   264  	t.Parallel()
   265  
   266  	testCases := []struct {
   267  		command  []string
   268  		expected []string
   269  	}{
   270  		{[]string{"plan"}, []string{"plan", "-lock=false"}},
   271  		{[]string{"validate"}, []string{"validate"}},
   272  		{[]string{"plan-all"}, []string{"plan-all", "-lock=false"}},
   273  		{[]string{"run-all", "validate"}, []string{"run-all", "validate"}},
   274  		{[]string{"run-all", "plan"}, []string{"run-all", "plan", "-lock=false"}},
   275  	}
   276  
   277  	for _, testCase := range testCases {
   278  		assert.Equal(t, testCase.expected, FormatArgs(&Options{}, testCase.command...))
   279  	}
   280  }
   281  
   282  func TestFormatSetVarsAfterVarFilesFormatsCorrectly(t *testing.T) {
   283  	t.Parallel()
   284  
   285  	testCases := []struct {
   286  		command              []string
   287  		vars                 map[string]interface{}
   288  		varFiles             []string
   289  		setVarsAfterVarFiles bool
   290  		expected             []string
   291  	}{
   292  		{[]string{"plan"}, map[string]interface{}{"foo": "bar"}, []string{"test.tfvars"}, true, []string{"plan", "-var-file", "test.tfvars", "-var", "foo=bar", "-lock=false"}},
   293  		{[]string{"plan"}, map[string]interface{}{"foo": "bar", "hello": "world"}, []string{"test.tfvars"}, true, []string{"plan", "-var-file", "test.tfvars", "-var", "foo=bar", "-var", "hello=world", "-lock=false"}},
   294  		{[]string{"plan"}, map[string]interface{}{"foo": "bar", "hello": "world"}, []string{"test.tfvars"}, false, []string{"plan", "-var", "foo=bar", "-var", "hello=world", "-var-file", "test.tfvars", "-lock=false"}},
   295  		{[]string{"plan"}, map[string]interface{}{"foo": "bar"}, []string{"test.tfvars"}, false, []string{"plan", "-var", "foo=bar", "-var-file", "test.tfvars", "-lock=false"}},
   296  	}
   297  
   298  	for _, testCase := range testCases {
   299  		result := FormatArgs(&Options{SetVarsAfterVarFiles: testCase.setVarsAfterVarFiles, Vars: testCase.vars, VarFiles: testCase.varFiles}, testCase.command...)
   300  
   301  		// Make sure that -var and -var-file options are in the expected order relative to each other
   302  		// Note that the order of the different -var and -var-file options may change
   303  		// See this comment for more info: https://github.com/mponton/terratest/blob/6fb86056797e3e62ebdd9011ba26605e0976a6f8/modules/terraform/format_test.go#L123-L142
   304  		for idx, arg := range result {
   305  			if arg == "-var-file" || arg == "-var" {
   306  				assert.Equal(t, testCase.expected[idx], arg)
   307  			}
   308  		}
   309  
   310  		// Make sure that the order of other arguments hasn't been incorrectly modified
   311  		assert.Equal(t, testCase.expected[0], result[0])
   312  		assert.Equal(t, testCase.expected[len(testCase.expected)-1], result[len(result)-1])
   313  	}
   314  }