github.com/pulumi/terraform@v1.4.0/pkg/command/views/json_view_test.go (about) 1 package views 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strings" 7 "testing" 8 "time" 9 10 "github.com/google/go-cmp/cmp" 11 "github.com/pulumi/terraform/pkg/addrs" 12 viewsjson "github.com/pulumi/terraform/pkg/command/views/json" 13 "github.com/pulumi/terraform/pkg/plans" 14 "github.com/pulumi/terraform/pkg/terminal" 15 "github.com/pulumi/terraform/pkg/tfdiags" 16 tfversion "github.com/pulumi/terraform/version" 17 ) 18 19 // Calling NewJSONView should also always output a version message, which is a 20 // convenient way to test that NewJSONView works. 21 func TestNewJSONView(t *testing.T) { 22 streams, done := terminal.StreamsForTesting(t) 23 NewJSONView(NewView(streams)) 24 25 version := tfversion.String() 26 want := []map[string]interface{}{ 27 { 28 "@level": "info", 29 "@message": fmt.Sprintf("Terraform %s", version), 30 "@module": "terraform.ui", 31 "type": "version", 32 "terraform": version, 33 "ui": JSON_UI_VERSION, 34 }, 35 } 36 37 testJSONViewOutputEqualsFull(t, done(t).Stdout(), want) 38 } 39 40 func TestJSONView_Log(t *testing.T) { 41 streams, done := terminal.StreamsForTesting(t) 42 jv := NewJSONView(NewView(streams)) 43 44 jv.Log("hello, world") 45 46 want := []map[string]interface{}{ 47 { 48 "@level": "info", 49 "@message": "hello, world", 50 "@module": "terraform.ui", 51 "type": "log", 52 }, 53 } 54 testJSONViewOutputEquals(t, done(t).Stdout(), want) 55 } 56 57 // This test covers only the basics of JSON diagnostic rendering, as more 58 // complex diagnostics are tested elsewhere. 59 func TestJSONView_Diagnostics(t *testing.T) { 60 streams, done := terminal.StreamsForTesting(t) 61 jv := NewJSONView(NewView(streams)) 62 63 var diags tfdiags.Diagnostics 64 diags = diags.Append(tfdiags.Sourceless( 65 tfdiags.Warning, 66 `Improper use of "less"`, 67 `You probably mean "10 buckets or fewer"`, 68 )) 69 diags = diags.Append(tfdiags.Sourceless( 70 tfdiags.Error, 71 "Unusually stripey cat detected", 72 "Are you sure this random_pet isn't a cheetah?", 73 )) 74 75 jv.Diagnostics(diags) 76 77 want := []map[string]interface{}{ 78 { 79 "@level": "warn", 80 "@message": `Warning: Improper use of "less"`, 81 "@module": "terraform.ui", 82 "type": "diagnostic", 83 "diagnostic": map[string]interface{}{ 84 "severity": "warning", 85 "summary": `Improper use of "less"`, 86 "detail": `You probably mean "10 buckets or fewer"`, 87 }, 88 }, 89 { 90 "@level": "error", 91 "@message": "Error: Unusually stripey cat detected", 92 "@module": "terraform.ui", 93 "type": "diagnostic", 94 "diagnostic": map[string]interface{}{ 95 "severity": "error", 96 "summary": "Unusually stripey cat detected", 97 "detail": "Are you sure this random_pet isn't a cheetah?", 98 }, 99 }, 100 } 101 testJSONViewOutputEquals(t, done(t).Stdout(), want) 102 } 103 104 func TestJSONView_PlannedChange(t *testing.T) { 105 streams, done := terminal.StreamsForTesting(t) 106 jv := NewJSONView(NewView(streams)) 107 108 foo, diags := addrs.ParseModuleInstanceStr("module.foo") 109 if len(diags) > 0 { 110 t.Fatal(diags.Err()) 111 } 112 managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"} 113 cs := &plans.ResourceInstanceChangeSrc{ 114 Addr: managed.Instance(addrs.StringKey("boop")).Absolute(foo), 115 PrevRunAddr: managed.Instance(addrs.StringKey("boop")).Absolute(foo), 116 ChangeSrc: plans.ChangeSrc{ 117 Action: plans.Create, 118 }, 119 } 120 jv.PlannedChange(viewsjson.NewResourceInstanceChange(cs)) 121 122 want := []map[string]interface{}{ 123 { 124 "@level": "info", 125 "@message": `module.foo.test_instance.bar["boop"]: Plan to create`, 126 "@module": "terraform.ui", 127 "type": "planned_change", 128 "change": map[string]interface{}{ 129 "action": "create", 130 "resource": map[string]interface{}{ 131 "addr": `module.foo.test_instance.bar["boop"]`, 132 "implied_provider": "test", 133 "module": "module.foo", 134 "resource": `test_instance.bar["boop"]`, 135 "resource_key": "boop", 136 "resource_name": "bar", 137 "resource_type": "test_instance", 138 }, 139 }, 140 }, 141 } 142 testJSONViewOutputEquals(t, done(t).Stdout(), want) 143 } 144 145 func TestJSONView_ResourceDrift(t *testing.T) { 146 streams, done := terminal.StreamsForTesting(t) 147 jv := NewJSONView(NewView(streams)) 148 149 foo, diags := addrs.ParseModuleInstanceStr("module.foo") 150 if len(diags) > 0 { 151 t.Fatal(diags.Err()) 152 } 153 managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"} 154 cs := &plans.ResourceInstanceChangeSrc{ 155 Addr: managed.Instance(addrs.StringKey("boop")).Absolute(foo), 156 PrevRunAddr: managed.Instance(addrs.StringKey("boop")).Absolute(foo), 157 ChangeSrc: plans.ChangeSrc{ 158 Action: plans.Update, 159 }, 160 } 161 jv.ResourceDrift(viewsjson.NewResourceInstanceChange(cs)) 162 163 want := []map[string]interface{}{ 164 { 165 "@level": "info", 166 "@message": `module.foo.test_instance.bar["boop"]: Drift detected (update)`, 167 "@module": "terraform.ui", 168 "type": "resource_drift", 169 "change": map[string]interface{}{ 170 "action": "update", 171 "resource": map[string]interface{}{ 172 "addr": `module.foo.test_instance.bar["boop"]`, 173 "implied_provider": "test", 174 "module": "module.foo", 175 "resource": `test_instance.bar["boop"]`, 176 "resource_key": "boop", 177 "resource_name": "bar", 178 "resource_type": "test_instance", 179 }, 180 }, 181 }, 182 } 183 testJSONViewOutputEquals(t, done(t).Stdout(), want) 184 } 185 186 func TestJSONView_ChangeSummary(t *testing.T) { 187 streams, done := terminal.StreamsForTesting(t) 188 jv := NewJSONView(NewView(streams)) 189 190 jv.ChangeSummary(&viewsjson.ChangeSummary{ 191 Add: 1, 192 Change: 2, 193 Remove: 3, 194 Operation: viewsjson.OperationApplied, 195 }) 196 197 want := []map[string]interface{}{ 198 { 199 "@level": "info", 200 "@message": "Apply complete! Resources: 1 added, 2 changed, 3 destroyed.", 201 "@module": "terraform.ui", 202 "type": "change_summary", 203 "changes": map[string]interface{}{ 204 "add": float64(1), 205 "change": float64(2), 206 "remove": float64(3), 207 "operation": "apply", 208 }, 209 }, 210 } 211 testJSONViewOutputEquals(t, done(t).Stdout(), want) 212 } 213 214 func TestJSONView_Hook(t *testing.T) { 215 streams, done := terminal.StreamsForTesting(t) 216 jv := NewJSONView(NewView(streams)) 217 218 foo, diags := addrs.ParseModuleInstanceStr("module.foo") 219 if len(diags) > 0 { 220 t.Fatal(diags.Err()) 221 } 222 managed := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "bar"} 223 addr := managed.Instance(addrs.StringKey("boop")).Absolute(foo) 224 hook := viewsjson.NewApplyComplete(addr, plans.Create, "id", "boop-beep", 34*time.Second) 225 226 jv.Hook(hook) 227 228 want := []map[string]interface{}{ 229 { 230 "@level": "info", 231 "@message": `module.foo.test_instance.bar["boop"]: Creation complete after 34s [id=boop-beep]`, 232 "@module": "terraform.ui", 233 "type": "apply_complete", 234 "hook": map[string]interface{}{ 235 "resource": map[string]interface{}{ 236 "addr": `module.foo.test_instance.bar["boop"]`, 237 "implied_provider": "test", 238 "module": "module.foo", 239 "resource": `test_instance.bar["boop"]`, 240 "resource_key": "boop", 241 "resource_name": "bar", 242 "resource_type": "test_instance", 243 }, 244 "action": "create", 245 "id_key": "id", 246 "id_value": "boop-beep", 247 "elapsed_seconds": float64(34), 248 }, 249 }, 250 } 251 testJSONViewOutputEquals(t, done(t).Stdout(), want) 252 } 253 254 func TestJSONView_Outputs(t *testing.T) { 255 streams, done := terminal.StreamsForTesting(t) 256 jv := NewJSONView(NewView(streams)) 257 258 jv.Outputs(viewsjson.Outputs{ 259 "boop_count": { 260 Sensitive: false, 261 Value: json.RawMessage(`92`), 262 Type: json.RawMessage(`"number"`), 263 }, 264 "password": { 265 Sensitive: true, 266 Value: json.RawMessage(`"horse-battery"`), 267 Type: json.RawMessage(`"string"`), 268 }, 269 }) 270 271 want := []map[string]interface{}{ 272 { 273 "@level": "info", 274 "@message": "Outputs: 2", 275 "@module": "terraform.ui", 276 "type": "outputs", 277 "outputs": map[string]interface{}{ 278 "boop_count": map[string]interface{}{ 279 "sensitive": false, 280 "value": float64(92), 281 "type": "number", 282 }, 283 "password": map[string]interface{}{ 284 "sensitive": true, 285 "value": "horse-battery", 286 "type": "string", 287 }, 288 }, 289 }, 290 } 291 testJSONViewOutputEquals(t, done(t).Stdout(), want) 292 } 293 294 // This helper function tests a possibly multi-line JSONView output string 295 // against a slice of structs representing the desired log messages. It 296 // verifies that the output of JSONView is in JSON log format, one message per 297 // line. 298 func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string]interface{}) { 299 t.Helper() 300 301 // Remove final trailing newline 302 output = strings.TrimSuffix(output, "\n") 303 304 // Split log into lines, each of which should be a JSON log message 305 gotLines := strings.Split(output, "\n") 306 307 if len(gotLines) != len(want) { 308 t.Errorf("unexpected number of messages. got %d, want %d", len(gotLines), len(want)) 309 } 310 311 // Unmarshal each line and compare to the expected value 312 for i := range gotLines { 313 var gotStruct map[string]interface{} 314 if i >= len(want) { 315 t.Error("reached end of want messages too soon") 316 break 317 } 318 wantStruct := want[i] 319 320 if err := json.Unmarshal([]byte(gotLines[i]), &gotStruct); err != nil { 321 t.Fatal(err) 322 } 323 324 if timestamp, ok := gotStruct["@timestamp"]; !ok { 325 t.Errorf("message has no timestamp: %#v", gotStruct) 326 } else { 327 // Remove the timestamp value from the struct to allow comparison 328 delete(gotStruct, "@timestamp") 329 330 // Verify the timestamp format 331 if _, err := time.Parse("2006-01-02T15:04:05.000000Z07:00", timestamp.(string)); err != nil { 332 t.Errorf("error parsing timestamp on line %d: %s", i, err) 333 } 334 } 335 336 if !cmp.Equal(wantStruct, gotStruct) { 337 t.Errorf("unexpected output on line %d:\n%s", i, cmp.Diff(wantStruct, gotStruct)) 338 } 339 } 340 } 341 342 // testJSONViewOutputEquals skips the first line of output, since it ought to 343 // be a version message that we don't care about for most of our tests. 344 func testJSONViewOutputEquals(t *testing.T, output string, want []map[string]interface{}) { 345 t.Helper() 346 347 // Remove up to the first newline 348 index := strings.Index(output, "\n") 349 if index >= 0 { 350 output = output[index+1:] 351 } 352 testJSONViewOutputEqualsFull(t, output, want) 353 }