github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/internal/inspectimage/writer/human_readable_test.go (about)

     1  package writer_test
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"testing"
     8  
     9  	"github.com/buildpacks/lifecycle/buildpack"
    10  	"github.com/buildpacks/lifecycle/launch"
    11  	"github.com/buildpacks/lifecycle/platform/files"
    12  	"github.com/heroku/color"
    13  	"github.com/sclevine/spec"
    14  	"github.com/sclevine/spec/report"
    15  
    16  	"github.com/buildpacks/pack/internal/config"
    17  	"github.com/buildpacks/pack/internal/inspectimage"
    18  	"github.com/buildpacks/pack/internal/inspectimage/writer"
    19  	"github.com/buildpacks/pack/pkg/client"
    20  	"github.com/buildpacks/pack/pkg/logging"
    21  	h "github.com/buildpacks/pack/testhelpers"
    22  )
    23  
    24  func TestHumanReadable(t *testing.T) {
    25  	color.Disable(true)
    26  	defer color.Disable(false)
    27  	spec.Run(t, "Human Readable Writer", testHumanReadable, spec.Parallel(), spec.Report(report.Terminal{}))
    28  }
    29  
    30  func testHumanReadable(t *testing.T, when spec.G, it spec.S) {
    31  	var (
    32  		assert = h.NewAssertionManager(t)
    33  		outBuf bytes.Buffer
    34  
    35  		remoteInfo              *client.ImageInfo
    36  		remoteWithExtensionInfo *client.ImageInfo
    37  		remoteInfoNoRebasable   *client.ImageInfo
    38  
    39  		localInfo              *client.ImageInfo
    40  		localWithExtensionInfo *client.ImageInfo
    41  		localInfoNoRebasable   *client.ImageInfo
    42  
    43  		expectedRemoteOutput = `REMOTE:
    44  
    45  Stack: test.stack.id.remote
    46  
    47  Base Image:
    48    Reference: some-remote-run-image-reference
    49    Top Layer: some-remote-top-layer
    50  
    51  Run Images:
    52    user-configured-mirror-for-remote        (user-configured)
    53    some-remote-run-image
    54    some-remote-mirror
    55    other-remote-mirror
    56  
    57  Rebasable: true
    58  
    59  Buildpacks:
    60    ID                          VERSION        HOMEPAGE
    61    test.bp.one.remote          1.0.0          https://some-homepage-one
    62    test.bp.two.remote          2.0.0          https://some-homepage-two
    63    test.bp.three.remote        3.0.0          -
    64  
    65  Processes:
    66    TYPE                              SHELL        COMMAND                      ARGS                     WORK DIR
    67    some-remote-type (default)        bash         /some/remote command         some remote args         /some-test-work-dir
    68    other-remote-type                              /other/remote/command        other remote args        /other-test-work-dir`
    69  		expectedRemoteNoRebasableOutput = `REMOTE:
    70  
    71  Stack: test.stack.id.remote
    72  
    73  Base Image:
    74    Reference: some-remote-run-image-reference
    75    Top Layer: some-remote-top-layer
    76  
    77  Run Images:
    78    user-configured-mirror-for-remote        (user-configured)
    79    some-remote-run-image
    80    some-remote-mirror
    81    other-remote-mirror
    82  
    83  Rebasable: false
    84  
    85  Buildpacks:
    86    ID                          VERSION        HOMEPAGE
    87    test.bp.one.remote          1.0.0          https://some-homepage-one
    88    test.bp.two.remote          2.0.0          https://some-homepage-two
    89    test.bp.three.remote        3.0.0          -
    90  
    91  Processes:
    92    TYPE                              SHELL        COMMAND                      ARGS                     WORK DIR
    93    some-remote-type (default)        bash         /some/remote command         some remote args         /some-test-work-dir
    94    other-remote-type                              /other/remote/command        other remote args        /other-test-work-dir`
    95  
    96  		expectedRemoteWithExtensionOutput = `REMOTE:
    97  
    98  Stack: test.stack.id.remote
    99  
   100  Base Image:
   101    Reference: some-remote-run-image-reference
   102    Top Layer: some-remote-top-layer
   103  
   104  Run Images:
   105    user-configured-mirror-for-remote        (user-configured)
   106    some-remote-run-image
   107    some-remote-mirror
   108    other-remote-mirror
   109  
   110  Rebasable: true
   111  
   112  Buildpacks:
   113    ID                          VERSION        HOMEPAGE
   114    test.bp.one.remote          1.0.0          https://some-homepage-one
   115    test.bp.two.remote          2.0.0          https://some-homepage-two
   116    test.bp.three.remote        3.0.0          -
   117  
   118  Extensions:
   119    ID                          VERSION        HOMEPAGE
   120    test.bp.one.remote          1.0.0          https://some-homepage-one
   121    test.bp.two.remote          2.0.0          https://some-homepage-two
   122    test.bp.three.remote        3.0.0          -
   123  
   124  Processes:
   125    TYPE                              SHELL        COMMAND                      ARGS                     WORK DIR
   126    some-remote-type (default)        bash         /some/remote command         some remote args         /some-test-work-dir
   127    other-remote-type                              /other/remote/command        other remote args        /other-test-work-dir`
   128  
   129  		expectedLocalOutput = `LOCAL:
   130  
   131  Stack: test.stack.id.local
   132  
   133  Base Image:
   134    Reference: some-local-run-image-reference
   135    Top Layer: some-local-top-layer
   136  
   137  Run Images:
   138    user-configured-mirror-for-local        (user-configured)
   139    some-local-run-image
   140    some-local-mirror
   141    other-local-mirror
   142  
   143  Rebasable: true
   144  
   145  Buildpacks:
   146    ID                         VERSION        HOMEPAGE
   147    test.bp.one.local          1.0.0          https://some-homepage-one
   148    test.bp.two.local          2.0.0          https://some-homepage-two
   149    test.bp.three.local        3.0.0          -
   150  
   151  Processes:
   152    TYPE                             SHELL        COMMAND                     ARGS                    WORK DIR
   153    some-local-type (default)        bash         /some/local command         some local args         /some-test-work-dir
   154    other-local-type                              /other/local/command        other local args        /other-test-work-dir`
   155  		expectedLocalNoRebasableOutput = `LOCAL:
   156  
   157  Stack: test.stack.id.local
   158  
   159  Base Image:
   160    Reference: some-local-run-image-reference
   161    Top Layer: some-local-top-layer
   162  
   163  Run Images:
   164    user-configured-mirror-for-local        (user-configured)
   165    some-local-run-image
   166    some-local-mirror
   167    other-local-mirror
   168  
   169  Rebasable: false
   170  
   171  Buildpacks:
   172    ID                         VERSION        HOMEPAGE
   173    test.bp.one.local          1.0.0          https://some-homepage-one
   174    test.bp.two.local          2.0.0          https://some-homepage-two
   175    test.bp.three.local        3.0.0          -
   176  
   177  Processes:
   178    TYPE                             SHELL        COMMAND                     ARGS                    WORK DIR
   179    some-local-type (default)        bash         /some/local command         some local args         /some-test-work-dir
   180    other-local-type                              /other/local/command        other local args        /other-test-work-dir`
   181  
   182  		expectedLocalWithExtensionOutput = `LOCAL:
   183  
   184  Stack: test.stack.id.local
   185  
   186  Base Image:
   187    Reference: some-local-run-image-reference
   188    Top Layer: some-local-top-layer
   189  
   190  Run Images:
   191    user-configured-mirror-for-local        (user-configured)
   192    some-local-run-image
   193    some-local-mirror
   194    other-local-mirror
   195  
   196  Rebasable: true
   197  
   198  Buildpacks:
   199    ID                         VERSION        HOMEPAGE
   200    test.bp.one.local          1.0.0          https://some-homepage-one
   201    test.bp.two.local          2.0.0          https://some-homepage-two
   202    test.bp.three.local        3.0.0          -
   203  
   204  Extensions:
   205    ID                         VERSION        HOMEPAGE
   206    test.bp.one.local          1.0.0          https://some-homepage-one
   207    test.bp.two.local          2.0.0          https://some-homepage-two
   208    test.bp.three.local        3.0.0          -
   209  
   210  Processes:
   211    TYPE                             SHELL        COMMAND                     ARGS                    WORK DIR
   212    some-local-type (default)        bash         /some/local command         some local args         /some-test-work-dir
   213    other-local-type                              /other/local/command        other local args        /other-test-work-dir`
   214  	)
   215  
   216  	when("Print", func() {
   217  		it.Before(func() {
   218  			remoteInfo = getRemoteBasicImageInfo(t)
   219  			remoteWithExtensionInfo = getRemoteImageInfoWithExtension(t)
   220  			remoteInfoNoRebasable = getRemoteImageInfoNoRebasable(t)
   221  
   222  			localInfo = getBasicLocalImageInfo(t)
   223  			localWithExtensionInfo = getLocalImageInfoWithExtension(t)
   224  			localInfoNoRebasable = getLocalImageInfoNoRebasable(t)
   225  
   226  			outBuf = bytes.Buffer{}
   227  		})
   228  
   229  		when("local and remote image exits", func() {
   230  			it("prints both local and remote image info in a human readable format", func() {
   231  				runImageMirrors := []config.RunImage{
   232  					{
   233  						Image:   "un-used-run-image",
   234  						Mirrors: []string{"un-used"},
   235  					},
   236  					{
   237  						Image:   "some-local-run-image",
   238  						Mirrors: []string{"user-configured-mirror-for-local"},
   239  					},
   240  					{
   241  						Image:   "some-remote-run-image",
   242  						Mirrors: []string{"user-configured-mirror-for-remote"},
   243  					},
   244  				}
   245  				sharedImageInfo := inspectimage.GeneralInfo{
   246  					Name:            "test-image",
   247  					RunImageMirrors: runImageMirrors,
   248  				}
   249  				humanReadableWriter := writer.NewHumanReadable()
   250  
   251  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   252  				err := humanReadableWriter.Print(logger, sharedImageInfo, localInfo, remoteInfo, nil, nil)
   253  				assert.Nil(err)
   254  
   255  				assert.Contains(outBuf.String(), expectedLocalOutput)
   256  				assert.Contains(outBuf.String(), expectedRemoteOutput)
   257  			})
   258  		})
   259  
   260  		when("localWithExtension and remoteWithExtension image exits", func() {
   261  			it("prints both localWithExtension and remoteWithExtension image info in a human readable format", func() {
   262  				runImageMirrors := []config.RunImage{
   263  					{
   264  						Image:   "un-used-run-image",
   265  						Mirrors: []string{"un-used"},
   266  					},
   267  					{
   268  						Image:   "some-local-run-image",
   269  						Mirrors: []string{"user-configured-mirror-for-local"},
   270  					},
   271  					{
   272  						Image:   "some-remote-run-image",
   273  						Mirrors: []string{"user-configured-mirror-for-remote"},
   274  					},
   275  				}
   276  				sharedImageInfo := inspectimage.GeneralInfo{
   277  					Name:            "test-image",
   278  					RunImageMirrors: runImageMirrors,
   279  				}
   280  				humanReadableWriter := writer.NewHumanReadable()
   281  
   282  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   283  				err := humanReadableWriter.Print(logger, sharedImageInfo, localWithExtensionInfo, remoteWithExtensionInfo, nil, nil)
   284  				assert.Nil(err)
   285  
   286  				assert.Contains(outBuf.String(), expectedLocalWithExtensionOutput)
   287  				assert.Contains(outBuf.String(), expectedRemoteWithExtensionOutput)
   288  			})
   289  		})
   290  
   291  		when("only local image exists", func() {
   292  			it("prints local image info in a human readable format", func() {
   293  				runImageMirrors := []config.RunImage{
   294  					{
   295  						Image:   "un-used-run-image",
   296  						Mirrors: []string{"un-used"},
   297  					},
   298  					{
   299  						Image:   "some-local-run-image",
   300  						Mirrors: []string{"user-configured-mirror-for-local"},
   301  					},
   302  					{
   303  						Image:   "some-remote-run-image",
   304  						Mirrors: []string{"user-configured-mirror-for-remote"},
   305  					},
   306  				}
   307  				sharedImageInfo := inspectimage.GeneralInfo{
   308  					Name:            "test-image",
   309  					RunImageMirrors: runImageMirrors,
   310  				}
   311  				humanReadableWriter := writer.NewHumanReadable()
   312  
   313  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   314  				err := humanReadableWriter.Print(logger, sharedImageInfo, localInfo, nil, nil, nil)
   315  				assert.Nil(err)
   316  
   317  				assert.Contains(outBuf.String(), expectedLocalOutput)
   318  				assert.NotContains(outBuf.String(), expectedRemoteOutput)
   319  			})
   320  			it("prints local no rebasable image info in a human readable format", func() {
   321  				runImageMirrors := []config.RunImage{
   322  					{
   323  						Image:   "un-used-run-image",
   324  						Mirrors: []string{"un-used"},
   325  					},
   326  					{
   327  						Image:   "some-local-run-image",
   328  						Mirrors: []string{"user-configured-mirror-for-local"},
   329  					},
   330  					{
   331  						Image:   "some-remote-run-image",
   332  						Mirrors: []string{"user-configured-mirror-for-remote"},
   333  					},
   334  				}
   335  				sharedImageInfo := inspectimage.GeneralInfo{
   336  					Name:            "test-image",
   337  					RunImageMirrors: runImageMirrors,
   338  				}
   339  				humanReadableWriter := writer.NewHumanReadable()
   340  
   341  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   342  				err := humanReadableWriter.Print(logger, sharedImageInfo, localInfoNoRebasable, nil, nil, nil)
   343  				assert.Nil(err)
   344  
   345  				assert.Contains(outBuf.String(), expectedLocalNoRebasableOutput)
   346  				assert.NotContains(outBuf.String(), expectedRemoteOutput)
   347  			})
   348  		})
   349  
   350  		when("only localWithExtension image exists", func() {
   351  			it("prints localWithExtension image info in a human readable format", func() {
   352  				runImageMirrors := []config.RunImage{
   353  					{
   354  						Image:   "un-used-run-image",
   355  						Mirrors: []string{"un-used"},
   356  					},
   357  					{
   358  						Image:   "some-local-run-image",
   359  						Mirrors: []string{"user-configured-mirror-for-local"},
   360  					},
   361  					{
   362  						Image:   "some-remote-run-image",
   363  						Mirrors: []string{"user-configured-mirror-for-remote"},
   364  					},
   365  				}
   366  				sharedImageInfo := inspectimage.GeneralInfo{
   367  					Name:            "test-image",
   368  					RunImageMirrors: runImageMirrors,
   369  				}
   370  				humanReadableWriter := writer.NewHumanReadable()
   371  
   372  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   373  				err := humanReadableWriter.Print(logger, sharedImageInfo, localWithExtensionInfo, nil, nil, nil)
   374  				assert.Nil(err)
   375  
   376  				assert.Contains(outBuf.String(), expectedLocalWithExtensionOutput)
   377  				assert.NotContains(outBuf.String(), expectedRemoteWithExtensionOutput)
   378  			})
   379  		})
   380  
   381  		when("only remote image exists", func() {
   382  			it("prints remote image info in a human readable format", func() {
   383  				runImageMirrors := []config.RunImage{
   384  					{
   385  						Image:   "un-used-run-image",
   386  						Mirrors: []string{"un-used"},
   387  					},
   388  					{
   389  						Image:   "some-local-run-image",
   390  						Mirrors: []string{"user-configured-mirror-for-local"},
   391  					},
   392  					{
   393  						Image:   "some-remote-run-image",
   394  						Mirrors: []string{"user-configured-mirror-for-remote"},
   395  					},
   396  				}
   397  				sharedImageInfo := inspectimage.GeneralInfo{
   398  					Name:            "test-image",
   399  					RunImageMirrors: runImageMirrors,
   400  				}
   401  				humanReadableWriter := writer.NewHumanReadable()
   402  
   403  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   404  				err := humanReadableWriter.Print(logger, sharedImageInfo, nil, remoteInfo, nil, nil)
   405  				assert.Nil(err)
   406  
   407  				assert.NotContains(outBuf.String(), expectedLocalOutput)
   408  				assert.Contains(outBuf.String(), expectedRemoteOutput)
   409  			})
   410  			it("prints remote no rebasable image info in a human readable format", func() {
   411  				runImageMirrors := []config.RunImage{
   412  					{
   413  						Image:   "un-used-run-image",
   414  						Mirrors: []string{"un-used"},
   415  					},
   416  					{
   417  						Image:   "some-local-run-image",
   418  						Mirrors: []string{"user-configured-mirror-for-local"},
   419  					},
   420  					{
   421  						Image:   "some-remote-run-image",
   422  						Mirrors: []string{"user-configured-mirror-for-remote"},
   423  					},
   424  				}
   425  				sharedImageInfo := inspectimage.GeneralInfo{
   426  					Name:            "test-image",
   427  					RunImageMirrors: runImageMirrors,
   428  				}
   429  				humanReadableWriter := writer.NewHumanReadable()
   430  
   431  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   432  				err := humanReadableWriter.Print(logger, sharedImageInfo, nil, remoteInfoNoRebasable, nil, nil)
   433  				assert.Nil(err)
   434  
   435  				assert.NotContains(outBuf.String(), expectedLocalOutput)
   436  				assert.Contains(outBuf.String(), expectedRemoteNoRebasableOutput)
   437  			})
   438  
   439  			when("buildpack metadata is missing", func() {
   440  				it.Before(func() {
   441  					remoteInfo.Buildpacks = []buildpack.GroupElement{}
   442  				})
   443  				it("displays a message indicating missing metadata", func() {
   444  					sharedImageInfo := inspectimage.GeneralInfo{
   445  						Name:            "test-image",
   446  						RunImageMirrors: []config.RunImage{},
   447  					}
   448  
   449  					humanReadableWriter := writer.NewHumanReadable()
   450  
   451  					logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   452  					err := humanReadableWriter.Print(logger, sharedImageInfo, nil, remoteInfo, nil, nil)
   453  					assert.Nil(err)
   454  
   455  					assert.Contains(outBuf.String(), "(buildpack metadata not present)")
   456  				})
   457  			})
   458  
   459  			when("there are no run images", func() {
   460  				it.Before(func() {
   461  					remoteInfo.Stack = files.Stack{}
   462  				})
   463  				it("displays a message indicating missing run images", func() {
   464  					sharedImageInfo := inspectimage.GeneralInfo{
   465  						Name:            "test-image",
   466  						RunImageMirrors: []config.RunImage{},
   467  					}
   468  
   469  					humanReadableWriter := writer.NewHumanReadable()
   470  
   471  					logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   472  					err := humanReadableWriter.Print(logger, sharedImageInfo, nil, remoteInfo, nil, nil)
   473  					assert.Nil(err)
   474  
   475  					assert.Contains(outBuf.String(), "Run Images:\n  (none)")
   476  				})
   477  			})
   478  		})
   479  
   480  		when("only remoteWithExtension image exists", func() {
   481  			it("prints remoteWithExtension image info in a human readable format", func() {
   482  				runImageMirrors := []config.RunImage{
   483  					{
   484  						Image:   "un-used-run-image",
   485  						Mirrors: []string{"un-used"},
   486  					},
   487  					{
   488  						Image:   "some-local-run-image",
   489  						Mirrors: []string{"user-configured-mirror-for-local"},
   490  					},
   491  					{
   492  						Image:   "some-remote-run-image",
   493  						Mirrors: []string{"user-configured-mirror-for-remote"},
   494  					},
   495  				}
   496  				sharedImageInfo := inspectimage.GeneralInfo{
   497  					Name:            "test-image",
   498  					RunImageMirrors: runImageMirrors,
   499  				}
   500  				humanReadableWriter := writer.NewHumanReadable()
   501  
   502  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   503  				err := humanReadableWriter.Print(logger, sharedImageInfo, nil, remoteWithExtensionInfo, nil, nil)
   504  				assert.Nil(err)
   505  
   506  				assert.NotContains(outBuf.String(), expectedLocalWithExtensionOutput)
   507  				assert.Contains(outBuf.String(), expectedRemoteWithExtensionOutput)
   508  			})
   509  
   510  			when("buildpack metadata is missing", func() {
   511  				it.Before(func() {
   512  					remoteWithExtensionInfo.Buildpacks = []buildpack.GroupElement{}
   513  				})
   514  				it("displays a message indicating missing metadata", func() {
   515  					sharedImageInfo := inspectimage.GeneralInfo{
   516  						Name:            "test-image",
   517  						RunImageMirrors: []config.RunImage{},
   518  					}
   519  
   520  					humanReadableWriter := writer.NewHumanReadable()
   521  
   522  					logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   523  					err := humanReadableWriter.Print(logger, sharedImageInfo, nil, remoteWithExtensionInfo, nil, nil)
   524  					assert.Nil(err)
   525  
   526  					assert.Contains(outBuf.String(), "(buildpack metadata not present)")
   527  				})
   528  			})
   529  
   530  			when("there are no run images", func() {
   531  				it.Before(func() {
   532  					remoteWithExtensionInfo.Stack = files.Stack{}
   533  				})
   534  				it("displays a message indicating missing run images", func() {
   535  					sharedImageInfo := inspectimage.GeneralInfo{
   536  						Name:            "test-image",
   537  						RunImageMirrors: []config.RunImage{},
   538  					}
   539  
   540  					humanReadableWriter := writer.NewHumanReadable()
   541  
   542  					logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   543  					err := humanReadableWriter.Print(logger, sharedImageInfo, nil, remoteWithExtensionInfo, nil, nil)
   544  					assert.Nil(err)
   545  
   546  					assert.Contains(outBuf.String(), "Run Images:\n  (none)")
   547  				})
   548  			})
   549  		})
   550  
   551  		when("error handled cases", func() {
   552  			when("there is a remoteErr", func() {
   553  				var remoteErr error
   554  				it.Before(func() {
   555  					remoteErr = errors.New("some remote error")
   556  				})
   557  				it("displays the remote error and local info", func() {
   558  					runImageMirrors := []config.RunImage{
   559  						{
   560  							Image:   "un-used-run-image",
   561  							Mirrors: []string{"un-used"},
   562  						},
   563  						{
   564  							Image:   "some-local-run-image",
   565  							Mirrors: []string{"user-configured-mirror-for-local"},
   566  						},
   567  						{
   568  							Image:   "some-remote-run-image",
   569  							Mirrors: []string{"user-configured-mirror-for-remote"},
   570  						},
   571  					}
   572  					sharedImageInfo := inspectimage.GeneralInfo{
   573  						Name:            "test-image",
   574  						RunImageMirrors: runImageMirrors,
   575  					}
   576  					humanReadableWriter := writer.NewHumanReadable()
   577  
   578  					logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   579  					err := humanReadableWriter.Print(logger, sharedImageInfo, localInfo, remoteInfo, nil, remoteErr)
   580  					assert.Nil(err)
   581  
   582  					assert.Contains(outBuf.String(), expectedLocalOutput)
   583  					assert.Contains(outBuf.String(), "some remote error")
   584  				})
   585  			})
   586  
   587  			when("there is a localErr", func() {
   588  				var localErr error
   589  				it.Before(func() {
   590  					localErr = errors.New("some local error")
   591  				})
   592  				it("displays the remote info and local error", func() {
   593  					runImageMirrors := []config.RunImage{
   594  						{
   595  							Image:   "un-used-run-image",
   596  							Mirrors: []string{"un-used"},
   597  						},
   598  						{
   599  							Image:   "some-local-run-image",
   600  							Mirrors: []string{"user-configured-mirror-for-local"},
   601  						},
   602  						{
   603  							Image:   "some-remote-run-image",
   604  							Mirrors: []string{"user-configured-mirror-for-remote"},
   605  						},
   606  					}
   607  					sharedImageInfo := inspectimage.GeneralInfo{
   608  						Name:            "test-image",
   609  						RunImageMirrors: runImageMirrors,
   610  					}
   611  					humanReadableWriter := writer.NewHumanReadable()
   612  
   613  					logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   614  					err := humanReadableWriter.Print(logger, sharedImageInfo, localInfo, remoteInfo, localErr, nil)
   615  					assert.Nil(err)
   616  
   617  					assert.Contains(outBuf.String(), expectedRemoteOutput)
   618  					assert.Contains(outBuf.String(), "some local error")
   619  				})
   620  			})
   621  
   622  			when("error handled cases", func() {
   623  				when("there is a remoteErr", func() {
   624  					var remoteErr error
   625  					it.Before(func() {
   626  						remoteErr = errors.New("some remote error")
   627  					})
   628  					it("displays the remote error and local info", func() {
   629  						runImageMirrors := []config.RunImage{
   630  							{
   631  								Image:   "un-used-run-image",
   632  								Mirrors: []string{"un-used"},
   633  							},
   634  							{
   635  								Image:   "some-local-run-image",
   636  								Mirrors: []string{"user-configured-mirror-for-local"},
   637  							},
   638  							{
   639  								Image:   "some-remote-run-image",
   640  								Mirrors: []string{"user-configured-mirror-for-remote"},
   641  							},
   642  						}
   643  						sharedImageInfo := inspectimage.GeneralInfo{
   644  							Name:            "test-image",
   645  							RunImageMirrors: runImageMirrors,
   646  						}
   647  						humanReadableWriter := writer.NewHumanReadable()
   648  
   649  						logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   650  						err := humanReadableWriter.Print(logger, sharedImageInfo, localWithExtensionInfo, remoteWithExtensionInfo, nil, remoteErr)
   651  						assert.Nil(err)
   652  
   653  						assert.Contains(outBuf.String(), expectedLocalWithExtensionOutput)
   654  						assert.Contains(outBuf.String(), "some remote error")
   655  					})
   656  				})
   657  
   658  				when("there is a localErr", func() {
   659  					var localErr error
   660  					it.Before(func() {
   661  						localErr = errors.New("some local error")
   662  					})
   663  					it("displays the remote info and local error", func() {
   664  						runImageMirrors := []config.RunImage{
   665  							{
   666  								Image:   "un-used-run-image",
   667  								Mirrors: []string{"un-used"},
   668  							},
   669  							{
   670  								Image:   "some-local-run-image",
   671  								Mirrors: []string{"user-configured-mirror-for-local"},
   672  							},
   673  							{
   674  								Image:   "some-remote-run-image",
   675  								Mirrors: []string{"user-configured-mirror-for-remote"},
   676  							},
   677  						}
   678  						sharedImageInfo := inspectimage.GeneralInfo{
   679  							Name:            "test-image",
   680  							RunImageMirrors: runImageMirrors,
   681  						}
   682  						humanReadableWriter := writer.NewHumanReadable()
   683  
   684  						logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   685  						err := humanReadableWriter.Print(logger, sharedImageInfo, localWithExtensionInfo, remoteWithExtensionInfo, localErr, nil)
   686  						assert.Nil(err)
   687  
   688  						assert.Contains(outBuf.String(), expectedRemoteWithExtensionOutput)
   689  						assert.Contains(outBuf.String(), "some local error")
   690  					})
   691  				})
   692  			})
   693  
   694  			when("error cases", func() {
   695  				when("both localInfo and remoteInfo are nil", func() {
   696  					it("displays a 'missing image' error message", func() {
   697  						humanReadableWriter := writer.NewHumanReadable()
   698  
   699  						logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   700  						err := humanReadableWriter.Print(logger, inspectimage.GeneralInfo{Name: "missing-image"}, nil, nil, nil, nil)
   701  						assert.ErrorWithMessage(err, fmt.Sprintf("unable to find image '%s' locally or remotely", "missing-image"))
   702  					})
   703  				})
   704  			})
   705  		})
   706  	})
   707  }
   708  
   709  func getRemoteBasicImageInfo(t testing.TB) *client.ImageInfo {
   710  	t.Helper()
   711  	return getRemoteImageInfo(t, false, true)
   712  }
   713  func getRemoteImageInfoWithExtension(t testing.TB) *client.ImageInfo {
   714  	t.Helper()
   715  	return getRemoteImageInfo(t, true, true)
   716  }
   717  
   718  func getRemoteImageInfoNoRebasable(t testing.TB) *client.ImageInfo {
   719  	t.Helper()
   720  	return getRemoteImageInfo(t, false, false)
   721  }
   722  
   723  func getRemoteImageInfo(t testing.TB, extension bool, rebasable bool) *client.ImageInfo {
   724  	t.Helper()
   725  
   726  	mockedStackID := "test.stack.id.remote"
   727  
   728  	mockedBuildpacks := []buildpack.GroupElement{
   729  		{ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"},
   730  		{ID: "test.bp.two.remote", Version: "2.0.0", Homepage: "https://some-homepage-two"},
   731  		{ID: "test.bp.three.remote", Version: "3.0.0"},
   732  	}
   733  
   734  	mockedBase := files.RunImageForRebase{
   735  		TopLayer:  "some-remote-top-layer",
   736  		Reference: "some-remote-run-image-reference",
   737  	}
   738  
   739  	mockedStack := files.Stack{
   740  		RunImage: files.RunImageForExport{
   741  			Image:   "some-remote-run-image",
   742  			Mirrors: []string{"some-remote-mirror", "other-remote-mirror"},
   743  		},
   744  	}
   745  
   746  	type someData struct {
   747  		String string
   748  		Bool   bool
   749  		Int    int
   750  		Nested struct {
   751  			String string
   752  		}
   753  	}
   754  	mockedMetadata := map[string]interface{}{
   755  		"RemoteData": someData{
   756  			String: "aString",
   757  			Bool:   true,
   758  			Int:    123,
   759  			Nested: struct {
   760  				String string
   761  			}{
   762  				String: "anotherString",
   763  			},
   764  		},
   765  	}
   766  
   767  	mockedBOM := []buildpack.BOMEntry{{
   768  		Require: buildpack.Require{
   769  			Name:     "name-1",
   770  			Metadata: mockedMetadata,
   771  		},
   772  		Buildpack: buildpack.GroupElement{ID: "test.bp.one.remote", Version: "1.0.0"},
   773  	}}
   774  
   775  	mockedProcesses := client.ProcessDetails{
   776  		DefaultProcess: &launch.Process{
   777  			Type:             "some-remote-type",
   778  			Command:          launch.RawCommand{Entries: []string{"/some/remote command"}},
   779  			Args:             []string{"some", "remote", "args"},
   780  			Direct:           false,
   781  			WorkingDirectory: "/some-test-work-dir",
   782  		},
   783  		OtherProcesses: []launch.Process{
   784  			{
   785  				Type:             "other-remote-type",
   786  				Command:          launch.RawCommand{Entries: []string{"/other/remote/command"}},
   787  				Args:             []string{"other", "remote", "args"},
   788  				Direct:           true,
   789  				WorkingDirectory: "/other-test-work-dir",
   790  			},
   791  		},
   792  	}
   793  
   794  	mockedExtension := []buildpack.GroupElement{
   795  		{ID: "test.bp.one.remote", Version: "1.0.0", Homepage: "https://some-homepage-one"},
   796  		{ID: "test.bp.two.remote", Version: "2.0.0", Homepage: "https://some-homepage-two"},
   797  		{ID: "test.bp.three.remote", Version: "3.0.0"},
   798  	}
   799  
   800  	imageInfo := &client.ImageInfo{
   801  		StackID:    mockedStackID,
   802  		Buildpacks: mockedBuildpacks,
   803  		Base:       mockedBase,
   804  		Stack:      mockedStack,
   805  		BOM:        mockedBOM,
   806  		Processes:  mockedProcesses,
   807  		Rebasable:  rebasable,
   808  	}
   809  
   810  	if extension {
   811  		imageInfo.Extensions = mockedExtension
   812  	}
   813  
   814  	return imageInfo
   815  }
   816  
   817  func getBasicLocalImageInfo(t testing.TB) *client.ImageInfo {
   818  	t.Helper()
   819  	return getLocalImageInfo(t, false, true)
   820  }
   821  
   822  func getLocalImageInfoWithExtension(t testing.TB) *client.ImageInfo {
   823  	t.Helper()
   824  	return getLocalImageInfo(t, true, true)
   825  }
   826  
   827  func getLocalImageInfoNoRebasable(t testing.TB) *client.ImageInfo {
   828  	t.Helper()
   829  	return getLocalImageInfo(t, false, false)
   830  }
   831  
   832  func getLocalImageInfo(t testing.TB, extension bool, rebasable bool) *client.ImageInfo {
   833  	t.Helper()
   834  
   835  	mockedStackID := "test.stack.id.local"
   836  
   837  	mockedBuildpacks := []buildpack.GroupElement{
   838  		{ID: "test.bp.one.local", Version: "1.0.0", Homepage: "https://some-homepage-one"},
   839  		{ID: "test.bp.two.local", Version: "2.0.0", Homepage: "https://some-homepage-two"},
   840  		{ID: "test.bp.three.local", Version: "3.0.0"},
   841  	}
   842  
   843  	mockedBase := files.RunImageForRebase{
   844  		TopLayer:  "some-local-top-layer",
   845  		Reference: "some-local-run-image-reference",
   846  	}
   847  
   848  	mockedPlatform := files.Stack{
   849  		RunImage: files.RunImageForExport{
   850  			Image:   "some-local-run-image",
   851  			Mirrors: []string{"some-local-mirror", "other-local-mirror"},
   852  		},
   853  	}
   854  
   855  	type someData struct {
   856  		String string
   857  		Bool   bool
   858  		Int    int
   859  		Nested struct {
   860  			String string
   861  		}
   862  	}
   863  	mockedMetadata := map[string]interface{}{
   864  		"LocalData": someData{
   865  			Bool: false,
   866  			Int:  456,
   867  		},
   868  	}
   869  
   870  	mockedBOM := []buildpack.BOMEntry{{
   871  		Require: buildpack.Require{
   872  			Name:     "name-1",
   873  			Version:  "version-1",
   874  			Metadata: mockedMetadata,
   875  		},
   876  		Buildpack: buildpack.GroupElement{ID: "test.bp.one.remote", Version: "1.0.0"},
   877  	}}
   878  
   879  	mockedProcesses := client.ProcessDetails{
   880  		DefaultProcess: &launch.Process{
   881  			Type:             "some-local-type",
   882  			Command:          launch.RawCommand{Entries: []string{"/some/local command"}},
   883  			Args:             []string{"some", "local", "args"},
   884  			Direct:           false,
   885  			WorkingDirectory: "/some-test-work-dir",
   886  		},
   887  		OtherProcesses: []launch.Process{
   888  			{
   889  				Type:             "other-local-type",
   890  				Command:          launch.RawCommand{Entries: []string{"/other/local/command"}},
   891  				Args:             []string{"other", "local", "args"},
   892  				Direct:           true,
   893  				WorkingDirectory: "/other-test-work-dir",
   894  			},
   895  		},
   896  	}
   897  
   898  	mockedExtension := []buildpack.GroupElement{
   899  		{ID: "test.bp.one.local", Version: "1.0.0", Homepage: "https://some-homepage-one"},
   900  		{ID: "test.bp.two.local", Version: "2.0.0", Homepage: "https://some-homepage-two"},
   901  		{ID: "test.bp.three.local", Version: "3.0.0"},
   902  	}
   903  
   904  	imageInfo := &client.ImageInfo{
   905  		StackID:    mockedStackID,
   906  		Buildpacks: mockedBuildpacks,
   907  		Base:       mockedBase,
   908  		Stack:      mockedPlatform,
   909  		BOM:        mockedBOM,
   910  		Processes:  mockedProcesses,
   911  		Rebasable:  rebasable,
   912  	}
   913  
   914  	if extension {
   915  		imageInfo.Extensions = mockedExtension
   916  	}
   917  
   918  	return imageInfo
   919  }