github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/internal/builder/writer/json_test.go (about) 1 package writer_test 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "testing" 9 10 "github.com/Masterminds/semver" 11 "github.com/buildpacks/lifecycle/api" 12 "github.com/heroku/color" 13 "github.com/sclevine/spec" 14 "github.com/sclevine/spec/report" 15 16 pubbldr "github.com/buildpacks/pack/builder" 17 "github.com/buildpacks/pack/internal/builder" 18 "github.com/buildpacks/pack/internal/builder/writer" 19 "github.com/buildpacks/pack/internal/config" 20 "github.com/buildpacks/pack/pkg/client" 21 "github.com/buildpacks/pack/pkg/dist" 22 "github.com/buildpacks/pack/pkg/logging" 23 h "github.com/buildpacks/pack/testhelpers" 24 ) 25 26 func TestJSON(t *testing.T) { 27 color.Disable(true) 28 defer color.Disable(false) 29 spec.Run(t, "Builder Writer", testJSON, spec.Parallel(), spec.Report(report.Terminal{})) 30 } 31 32 func testJSON(t *testing.T, when spec.G, it spec.S) { 33 const ( 34 expectedRemoteRunImages = `"run_images": [ 35 { 36 "name": "first/local", 37 "user_configured": true 38 }, 39 { 40 "name": "second/local", 41 "user_configured": true 42 }, 43 { 44 "name": "some/run-image" 45 }, 46 { 47 "name": "first/default" 48 }, 49 { 50 "name": "second/default" 51 } 52 ]` 53 expectedLocalRunImages = `"run_images": [ 54 { 55 "name": "first/local", 56 "user_configured": true 57 }, 58 { 59 "name": "second/local", 60 "user_configured": true 61 }, 62 { 63 "name": "some/run-image" 64 }, 65 { 66 "name": "first/local-default" 67 }, 68 { 69 "name": "second/local-default" 70 } 71 ]` 72 73 expectedBuildpacks = `"buildpacks": [ 74 { 75 "id": "test.top.nested", 76 "version": "test.top.nested.version" 77 }, 78 { 79 "id": "test.nested", 80 "homepage": "http://geocities.com/top-bp" 81 }, 82 { 83 "id": "test.bp.one", 84 "version": "test.bp.one.version", 85 "homepage": "http://geocities.com/cool-bp" 86 }, 87 { 88 "id": "test.bp.two", 89 "version": "test.bp.two.version" 90 }, 91 { 92 "id": "test.bp.three", 93 "version": "test.bp.three.version" 94 } 95 ]` 96 97 expectedExtensions = `"extensions": [ 98 { 99 "homepage": "http://geocities.com/cool-bp", 100 "id": "test.bp.one", 101 "version": "test.bp.one.version" 102 }, 103 { 104 "id": "test.bp.two", 105 "version": "test.bp.two.version" 106 }, 107 { 108 "id": "test.bp.three", 109 "version": "test.bp.three.version" 110 } 111 ]` 112 expectedDetectionOrder = `"detection_order": [ 113 { 114 "buildpacks": [ 115 { 116 "id": "test.top.nested", 117 "version": "test.top.nested.version", 118 "buildpacks": [ 119 { 120 "id": "test.nested", 121 "homepage": "http://geocities.com/top-bp", 122 "buildpacks": [ 123 { 124 "id": "test.bp.one", 125 "version": "test.bp.one.version", 126 "homepage": "http://geocities.com/cool-bp", 127 "optional": true 128 } 129 ] 130 }, 131 { 132 "id": "test.bp.three", 133 "version": "test.bp.three.version", 134 "optional": true 135 }, 136 { 137 "id": "test.nested.two", 138 "version": "test.nested.two.version", 139 "buildpacks": [ 140 { 141 "id": "test.bp.one", 142 "version": "test.bp.one.version", 143 "homepage": "http://geocities.com/cool-bp", 144 "optional": true, 145 "cyclic": true 146 } 147 ] 148 } 149 ] 150 }, 151 { 152 "id": "test.bp.two", 153 "version": "test.bp.two.version", 154 "optional": true 155 } 156 ] 157 }, 158 { 159 "id": "test.bp.three", 160 "version": "test.bp.three.version" 161 } 162 ]` 163 expectedOrderExtensions = `"order_extensions": [ 164 { 165 "id": "test.top.nested", 166 "version": "test.top.nested.version" 167 }, 168 { 169 "homepage": "http://geocities.com/cool-bp", 170 "id": "test.bp.one", 171 "version": "test.bp.one.version", 172 "optional": true 173 }, 174 { 175 "id": "test.bp.two", 176 "version": "test.bp.two.version", 177 "optional": true 178 }, 179 { 180 "id": "test.bp.three", 181 "version": "test.bp.three.version" 182 } 183 ]` 184 expectedStackWithMixins = `"stack": { 185 "id": "test.stack.id", 186 "mixins": [ 187 "mixin1", 188 "mixin2", 189 "build:mixin3", 190 "build:mixin4" 191 ] 192 }` 193 ) 194 195 var ( 196 assert = h.NewAssertionManager(t) 197 outBuf bytes.Buffer 198 199 remoteInfo *client.BuilderInfo 200 localInfo *client.BuilderInfo 201 202 expectedRemoteInfo = fmt.Sprintf(`"remote_info": { 203 "description": "Some remote description", 204 "created_by": { 205 "name": "Pack CLI", 206 "version": "1.2.3" 207 }, 208 "stack": { 209 "id": "test.stack.id" 210 }, 211 "lifecycle": { 212 "version": "6.7.8", 213 "buildpack_apis": { 214 "deprecated": null, 215 "supported": [ 216 "1.2", 217 "2.3" 218 ] 219 }, 220 "platform_apis": { 221 "deprecated": [ 222 "0.1", 223 "1.2" 224 ], 225 "supported": [ 226 "4.5" 227 ] 228 } 229 }, 230 %s, 231 %s, 232 %s, 233 %s, 234 %s 235 }`, expectedRemoteRunImages, expectedBuildpacks, expectedDetectionOrder, expectedExtensions, expectedOrderExtensions) 236 237 expectedLocalInfo = fmt.Sprintf(`"local_info": { 238 "description": "Some local description", 239 "created_by": { 240 "name": "Pack CLI", 241 "version": "4.5.6" 242 }, 243 "stack": { 244 "id": "test.stack.id" 245 }, 246 "lifecycle": { 247 "version": "4.5.6", 248 "buildpack_apis": { 249 "deprecated": [ 250 "4.5", 251 "6.7" 252 ], 253 "supported": [ 254 "8.9", 255 "10.11" 256 ] 257 }, 258 "platform_apis": { 259 "deprecated": null, 260 "supported": [ 261 "7.8" 262 ] 263 } 264 }, 265 %s, 266 %s, 267 %s, 268 %s, 269 %s 270 }`, expectedLocalRunImages, expectedBuildpacks, expectedDetectionOrder, expectedExtensions, expectedOrderExtensions) 271 272 expectedPrettifiedJSON = fmt.Sprintf(`{ 273 "builder_name": "test-builder", 274 "trusted": false, 275 "default": false, 276 %s, 277 %s 278 } 279 `, expectedRemoteInfo, expectedLocalInfo) 280 ) 281 282 when("Print", func() { 283 it.Before(func() { 284 remoteInfo = &client.BuilderInfo{ 285 Description: "Some remote description", 286 Stack: "test.stack.id", 287 Mixins: []string{"mixin1", "mixin2", "build:mixin3", "build:mixin4"}, 288 RunImages: []pubbldr.RunImageConfig{{Image: "some/run-image", Mirrors: []string{"first/default", "second/default"}}}, 289 Buildpacks: buildpacks, 290 Order: order, 291 Extensions: extensions, 292 OrderExtensions: orderExtensions, 293 BuildpackLayers: dist.ModuleLayers{}, 294 Lifecycle: builder.LifecycleDescriptor{ 295 Info: builder.LifecycleInfo{ 296 Version: &builder.Version{ 297 Version: *semver.MustParse("6.7.8"), 298 }, 299 }, 300 APIs: builder.LifecycleAPIs{ 301 Buildpack: builder.APIVersions{ 302 Deprecated: nil, 303 Supported: builder.APISet{api.MustParse("1.2"), api.MustParse("2.3")}, 304 }, 305 Platform: builder.APIVersions{ 306 Deprecated: builder.APISet{api.MustParse("0.1"), api.MustParse("1.2")}, 307 Supported: builder.APISet{api.MustParse("4.5")}, 308 }, 309 }, 310 }, 311 CreatedBy: builder.CreatorMetadata{ 312 Name: "Pack CLI", 313 Version: "1.2.3", 314 }, 315 } 316 317 localInfo = &client.BuilderInfo{ 318 Description: "Some local description", 319 Stack: "test.stack.id", 320 Mixins: []string{"mixin1", "mixin2", "build:mixin3", "build:mixin4"}, 321 RunImages: []pubbldr.RunImageConfig{{Image: "some/run-image", Mirrors: []string{"first/local-default", "second/local-default"}}}, 322 Buildpacks: buildpacks, 323 Order: order, 324 Extensions: extensions, 325 OrderExtensions: orderExtensions, 326 BuildpackLayers: dist.ModuleLayers{}, 327 Lifecycle: builder.LifecycleDescriptor{ 328 Info: builder.LifecycleInfo{ 329 Version: &builder.Version{ 330 Version: *semver.MustParse("4.5.6"), 331 }, 332 }, 333 APIs: builder.LifecycleAPIs{ 334 Buildpack: builder.APIVersions{ 335 Deprecated: builder.APISet{api.MustParse("4.5"), api.MustParse("6.7")}, 336 Supported: builder.APISet{api.MustParse("8.9"), api.MustParse("10.11")}, 337 }, 338 Platform: builder.APIVersions{ 339 Deprecated: nil, 340 Supported: builder.APISet{api.MustParse("7.8")}, 341 }, 342 }, 343 }, 344 CreatedBy: builder.CreatorMetadata{ 345 Name: "Pack CLI", 346 Version: "4.5.6", 347 }, 348 } 349 }) 350 351 it("prints both local remote builders as valid JSON", func() { 352 jsonWriter := writer.NewJSON() 353 354 logger := logging.NewLogWithWriters(&outBuf, &outBuf) 355 err := jsonWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo) 356 assert.Nil(err) 357 358 prettyJSON, err := validPrettifiedJSONOutput(outBuf) 359 assert.Nil(err) 360 361 assert.ContainsJSON(prettyJSON, expectedPrettifiedJSON) 362 }) 363 364 when("builder doesn't exist locally or remotely", func() { 365 it("returns an error", func() { 366 jsonWriter := writer.NewJSON() 367 368 logger := logging.NewLogWithWriters(&outBuf, &outBuf) 369 err := jsonWriter.Print(logger, localRunImages, nil, nil, nil, nil, sharedBuilderInfo) 370 assert.ErrorWithMessage(err, "unable to find builder 'test-builder' locally or remotely") 371 }) 372 }) 373 374 when("builder doesn't exist locally", func() { 375 it("shows null for local builder, and normal output for remote", func() { 376 jsonWriter := writer.NewJSON() 377 378 logger := logging.NewLogWithWriters(&outBuf, &outBuf) 379 err := jsonWriter.Print(logger, localRunImages, nil, remoteInfo, nil, nil, sharedBuilderInfo) 380 assert.Nil(err) 381 382 prettyJSON, err := validPrettifiedJSONOutput(outBuf) 383 assert.Nil(err) 384 385 assert.ContainsJSON(prettyJSON, `{"local_info": null}`) 386 assert.ContainsJSON(prettyJSON, fmt.Sprintf("{%s}", expectedRemoteInfo)) 387 }) 388 }) 389 390 when("builder doesn't exist remotely", func() { 391 it("shows null for remote builder, and normal output for local", func() { 392 jsonWriter := writer.NewJSON() 393 394 logger := logging.NewLogWithWriters(&outBuf, &outBuf) 395 err := jsonWriter.Print(logger, localRunImages, localInfo, nil, nil, nil, sharedBuilderInfo) 396 assert.Nil(err) 397 398 prettyJSON, err := validPrettifiedJSONOutput(outBuf) 399 assert.Nil(err) 400 401 assert.ContainsJSON(prettyJSON, `{"remote_info": null}`) 402 assert.ContainsJSON(prettyJSON, fmt.Sprintf("{%s}", expectedLocalInfo)) 403 }) 404 }) 405 406 when("localErr is an error", func() { 407 it("returns the error, and doesn't write any json output", func() { 408 expectedErr := errors.New("failed to retrieve local info") 409 410 jsonWriter := writer.NewJSON() 411 412 logger := logging.NewLogWithWriters(&outBuf, &outBuf) 413 err := jsonWriter.Print(logger, localRunImages, localInfo, remoteInfo, expectedErr, nil, sharedBuilderInfo) 414 assert.ErrorWithMessage(err, "preparing output for 'test-builder': failed to retrieve local info") 415 416 assert.Equal(outBuf.String(), "") 417 }) 418 }) 419 420 when("remoteErr is an error", func() { 421 it("returns the error, and doesn't write any json output", func() { 422 expectedErr := errors.New("failed to retrieve remote info") 423 424 jsonWriter := writer.NewJSON() 425 426 logger := logging.NewLogWithWriters(&outBuf, &outBuf) 427 err := jsonWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, expectedErr, sharedBuilderInfo) 428 assert.ErrorWithMessage(err, "preparing output for 'test-builder': failed to retrieve remote info") 429 430 assert.Equal(outBuf.String(), "") 431 }) 432 }) 433 434 when("logger is verbose", func() { 435 it("displays mixins associated with the stack", func() { 436 jsonWriter := writer.NewJSON() 437 438 logger := logging.NewLogWithWriters(&outBuf, &outBuf, logging.WithVerbose()) 439 err := jsonWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo) 440 assert.Nil(err) 441 442 prettifiedJSON, err := validPrettifiedJSONOutput(outBuf) 443 assert.Nil(err) 444 445 assert.ContainsJSON(prettifiedJSON, fmt.Sprintf("{%s}", expectedStackWithMixins)) 446 }) 447 }) 448 449 when("no run images are specified", func() { 450 it("displays run images as empty list", func() { 451 localInfo.RunImages = []pubbldr.RunImageConfig{} 452 remoteInfo.RunImages = []pubbldr.RunImageConfig{} 453 emptyLocalRunImages := []config.RunImage{} 454 455 jsonWriter := writer.NewJSON() 456 457 logger := logging.NewLogWithWriters(&outBuf, &outBuf, logging.WithVerbose()) 458 err := jsonWriter.Print(logger, emptyLocalRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo) 459 assert.Nil(err) 460 461 prettifiedJSON, err := validPrettifiedJSONOutput(outBuf) 462 assert.Nil(err) 463 464 assert.ContainsJSON(prettifiedJSON, `{"run_images": []}`) 465 }) 466 }) 467 468 when("no buildpacks are specified", func() { 469 it("displays buildpacks as empty list", func() { 470 localInfo.Buildpacks = []dist.ModuleInfo{} 471 remoteInfo.Buildpacks = []dist.ModuleInfo{} 472 473 jsonWriter := writer.NewJSON() 474 475 logger := logging.NewLogWithWriters(&outBuf, &outBuf, logging.WithVerbose()) 476 err := jsonWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo) 477 assert.Nil(err) 478 479 prettifiedJSON, err := validPrettifiedJSONOutput(outBuf) 480 assert.Nil(err) 481 482 assert.ContainsJSON(prettifiedJSON, `{"buildpacks": []}`) 483 }) 484 }) 485 486 when("no detection order is specified", func() { 487 it("displays detection order as empty list", func() { 488 localInfo.Order = pubbldr.DetectionOrder{} 489 remoteInfo.Order = pubbldr.DetectionOrder{} 490 491 jsonWriter := writer.NewJSON() 492 493 logger := logging.NewLogWithWriters(&outBuf, &outBuf, logging.WithVerbose()) 494 err := jsonWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo) 495 assert.Nil(err) 496 497 prettifiedJSON, err := validPrettifiedJSONOutput(outBuf) 498 assert.Nil(err) 499 500 assert.ContainsJSON(prettifiedJSON, `{"detection_order": []}`) 501 }) 502 }) 503 }) 504 } 505 506 func validPrettifiedJSONOutput(source bytes.Buffer) (string, error) { 507 err := json.Unmarshal(source.Bytes(), &struct{}{}) 508 if err != nil { 509 return "", fmt.Errorf("failed to unmarshal to json: %w", err) 510 } 511 512 var prettifiedOutput bytes.Buffer 513 err = json.Indent(&prettifiedOutput, source.Bytes(), "", " ") 514 if err != nil { 515 return "", fmt.Errorf("failed to prettify source json: %w", err) 516 } 517 518 return prettifiedOutput.String(), nil 519 }