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 }