github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/formatter/container_test.go (about)

     1  // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
     2  //go:build go1.19
     3  
     4  package formatter
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/docker/cli/internal/test"
    15  	"github.com/docker/docker/api/types"
    16  	"github.com/docker/docker/pkg/stringid"
    17  	"gotest.tools/v3/assert"
    18  	is "gotest.tools/v3/assert/cmp"
    19  	"gotest.tools/v3/golden"
    20  )
    21  
    22  func TestContainerPsContext(t *testing.T) {
    23  	containerID := stringid.GenerateRandomID()
    24  	unix := time.Now().Add(-65 * time.Second).Unix()
    25  
    26  	var ctx ContainerContext
    27  	cases := []struct {
    28  		container types.Container
    29  		trunc     bool
    30  		expValue  string
    31  		call      func() string
    32  	}{
    33  		{types.Container{ID: containerID}, true, stringid.TruncateID(containerID), ctx.ID},
    34  		{types.Container{ID: containerID}, false, containerID, ctx.ID},
    35  		{types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", ctx.Names},
    36  		{types.Container{Image: "ubuntu"}, true, "ubuntu", ctx.Image},
    37  		{types.Container{Image: "verylongimagename"}, true, "verylongimagename", ctx.Image},
    38  		{types.Container{Image: "verylongimagename"}, false, "verylongimagename", ctx.Image},
    39  		{
    40  			types.Container{
    41  				Image:   "a5a665ff33eced1e0803148700880edab4",
    42  				ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
    43  			},
    44  			true,
    45  			"a5a665ff33ec",
    46  			ctx.Image,
    47  		},
    48  		{
    49  			types.Container{
    50  				Image:   "a5a665ff33eced1e0803148700880edab4",
    51  				ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
    52  			},
    53  			false,
    54  			"a5a665ff33eced1e0803148700880edab4",
    55  			ctx.Image,
    56  		},
    57  		{types.Container{Image: ""}, true, "<no image>", ctx.Image},
    58  		{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, ctx.Command},
    59  		{types.Container{Created: unix}, true, time.Unix(unix, 0).String(), ctx.CreatedAt},
    60  		{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", ctx.Ports},
    61  		{types.Container{Status: "RUNNING"}, true, "RUNNING", ctx.Status},
    62  		{types.Container{SizeRw: 10}, true, "10B", ctx.Size},
    63  		{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", ctx.Size},
    64  		{types.Container{}, true, "", ctx.Labels},
    65  		{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", ctx.Labels},
    66  		{types.Container{Created: unix}, true, "About a minute ago", ctx.RunningFor},
    67  		{types.Container{
    68  			Mounts: []types.MountPoint{
    69  				{
    70  					Name:   "this-is-a-long-volume-name-and-will-be-truncated-if-trunc-is-set",
    71  					Driver: "local",
    72  					Source: "/a/path",
    73  				},
    74  			},
    75  		}, true, "this-is-a-long…", ctx.Mounts},
    76  		{types.Container{
    77  			Mounts: []types.MountPoint{
    78  				{
    79  					Driver: "local",
    80  					Source: "/a/path",
    81  				},
    82  			},
    83  		}, false, "/a/path", ctx.Mounts},
    84  		{types.Container{
    85  			Mounts: []types.MountPoint{
    86  				{
    87  					Name:   "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203",
    88  					Driver: "local",
    89  					Source: "/a/path",
    90  				},
    91  			},
    92  		}, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", ctx.Mounts},
    93  	}
    94  
    95  	for _, c := range cases {
    96  		ctx = ContainerContext{c: c.container, trunc: c.trunc}
    97  		v := c.call()
    98  		if strings.Contains(v, ",") {
    99  			test.CompareMultipleValues(t, v, c.expValue)
   100  		} else if v != c.expValue {
   101  			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
   102  		}
   103  	}
   104  
   105  	c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
   106  	ctx = ContainerContext{c: c1, trunc: true}
   107  
   108  	sid := ctx.Label("com.docker.swarm.swarm-id")
   109  	node := ctx.Label("com.docker.swarm.node_name")
   110  	if sid != "33" {
   111  		t.Fatalf("Expected 33, was %s\n", sid)
   112  	}
   113  
   114  	if node != "ubuntu" {
   115  		t.Fatalf("Expected ubuntu, was %s\n", node)
   116  	}
   117  
   118  	c2 := types.Container{}
   119  	ctx = ContainerContext{c: c2, trunc: true}
   120  
   121  	label := ctx.Label("anything.really")
   122  	if label != "" {
   123  		t.Fatalf("Expected an empty string, was %s", label)
   124  	}
   125  }
   126  
   127  func TestContainerContextWrite(t *testing.T) {
   128  	unixTime := time.Now().AddDate(0, 0, -1).Unix()
   129  	expectedTime := time.Unix(unixTime, 0).String()
   130  
   131  	cases := []struct {
   132  		context  Context
   133  		expected string
   134  	}{
   135  		// Errors
   136  		{
   137  			Context{Format: "{{InvalidFunction}}"},
   138  			`template parsing error: template: :1: function "InvalidFunction" not defined`,
   139  		},
   140  		{
   141  			Context{Format: "{{nil}}"},
   142  			`template parsing error: template: :1:2: executing "" at <nil>: nil is not a command`,
   143  		},
   144  		// Table Format
   145  		{
   146  			Context{Format: NewContainerFormat("table", false, true)},
   147  			`CONTAINER ID   IMAGE     COMMAND   CREATED        STATUS    PORTS     NAMES        SIZE
   148  containerID1   ubuntu    ""        24 hours ago                       foobar_baz   0B
   149  containerID2   ubuntu    ""        24 hours ago                       foobar_bar   0B
   150  `,
   151  		},
   152  		{
   153  			Context{Format: NewContainerFormat("table", false, false)},
   154  			`CONTAINER ID   IMAGE     COMMAND   CREATED        STATUS    PORTS     NAMES
   155  containerID1   ubuntu    ""        24 hours ago                       foobar_baz
   156  containerID2   ubuntu    ""        24 hours ago                       foobar_bar
   157  `,
   158  		},
   159  		{
   160  			Context{Format: NewContainerFormat("table {{.Image}}", false, false)},
   161  			"IMAGE\nubuntu\nubuntu\n",
   162  		},
   163  		{
   164  			Context{Format: NewContainerFormat("table {{.Image}}", false, true)},
   165  			"IMAGE\nubuntu\nubuntu\n",
   166  		},
   167  		{
   168  			Context{Format: NewContainerFormat("table {{.Image}}", true, false)},
   169  			"containerID1\ncontainerID2\n",
   170  		},
   171  		{
   172  			Context{Format: NewContainerFormat("table", true, false)},
   173  			"containerID1\ncontainerID2\n",
   174  		},
   175  		{
   176  			Context{Format: NewContainerFormat("table {{.State}}", false, true)},
   177  			"STATE\nrunning\nrunning\n",
   178  		},
   179  		// Raw Format
   180  		{
   181  			Context{Format: NewContainerFormat("raw", false, false)},
   182  			fmt.Sprintf(`container_id: containerID1
   183  image: ubuntu
   184  command: ""
   185  created_at: %s
   186  state: running
   187  status:
   188  names: foobar_baz
   189  labels:
   190  ports:
   191  
   192  container_id: containerID2
   193  image: ubuntu
   194  command: ""
   195  created_at: %s
   196  state: running
   197  status:
   198  names: foobar_bar
   199  labels:
   200  ports:
   201  
   202  `, expectedTime, expectedTime),
   203  		},
   204  		{
   205  			Context{Format: NewContainerFormat("raw", false, true)},
   206  			fmt.Sprintf(`container_id: containerID1
   207  image: ubuntu
   208  command: ""
   209  created_at: %s
   210  state: running
   211  status:
   212  names: foobar_baz
   213  labels:
   214  ports:
   215  size: 0B
   216  
   217  container_id: containerID2
   218  image: ubuntu
   219  command: ""
   220  created_at: %s
   221  state: running
   222  status:
   223  names: foobar_bar
   224  labels:
   225  ports:
   226  size: 0B
   227  
   228  `, expectedTime, expectedTime),
   229  		},
   230  		{
   231  			Context{Format: NewContainerFormat("raw", true, false)},
   232  			"container_id: containerID1\ncontainer_id: containerID2\n",
   233  		},
   234  		// Custom Format
   235  		{
   236  			Context{Format: "{{.Image}}"},
   237  			"ubuntu\nubuntu\n",
   238  		},
   239  		{
   240  			Context{Format: NewContainerFormat("{{.Image}}", false, true)},
   241  			"ubuntu\nubuntu\n",
   242  		},
   243  		// Special headers for customized table format
   244  		{
   245  			Context{Format: NewContainerFormat(`table {{truncate .ID 5}}\t{{json .Image}} {{.RunningFor}}/{{title .Status}}/{{pad .Ports 2 2}}.{{upper .Names}} {{lower .Status}}`, false, true)},
   246  			string(golden.Get(t, "container-context-write-special-headers.golden")),
   247  		},
   248  		{
   249  			Context{Format: NewContainerFormat(`table {{split .Image ":"}}`, false, false)},
   250  			"IMAGE\n[ubuntu]\n[ubuntu]\n",
   251  		},
   252  	}
   253  
   254  	containers := []types.Container{
   255  		{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime, State: "running"},
   256  		{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime, State: "running"},
   257  	}
   258  
   259  	for _, tc := range cases {
   260  		tc := tc
   261  		t.Run(string(tc.context.Format), func(t *testing.T) {
   262  			var out bytes.Buffer
   263  			tc.context.Output = &out
   264  			err := ContainerWrite(tc.context, containers)
   265  			if err != nil {
   266  				assert.Error(t, err, tc.expected)
   267  			} else {
   268  				assert.Equal(t, out.String(), tc.expected)
   269  			}
   270  		})
   271  	}
   272  }
   273  
   274  func TestContainerContextWriteWithNoContainers(t *testing.T) {
   275  	out := bytes.NewBufferString("")
   276  	containers := []types.Container{}
   277  
   278  	cases := []struct {
   279  		context  Context
   280  		expected string
   281  	}{
   282  		{
   283  			Context{
   284  				Format: "{{.Image}}",
   285  				Output: out,
   286  			},
   287  			"",
   288  		},
   289  		{
   290  			Context{
   291  				Format: "table {{.Image}}",
   292  				Output: out,
   293  			},
   294  			"IMAGE\n",
   295  		},
   296  		{
   297  			Context{
   298  				Format: NewContainerFormat("{{.Image}}", false, true),
   299  				Output: out,
   300  			},
   301  			"",
   302  		},
   303  		{
   304  			Context{
   305  				Format: NewContainerFormat("table {{.Image}}", false, true),
   306  				Output: out,
   307  			},
   308  			"IMAGE\n",
   309  		},
   310  		{
   311  			Context{
   312  				Format: "table {{.Image}}\t{{.Size}}",
   313  				Output: out,
   314  			},
   315  			"IMAGE     SIZE\n",
   316  		},
   317  		{
   318  			Context{
   319  				Format: NewContainerFormat("table {{.Image}}\t{{.Size}}", false, true),
   320  				Output: out,
   321  			},
   322  			"IMAGE     SIZE\n",
   323  		},
   324  	}
   325  
   326  	for _, tc := range cases {
   327  		tc := tc
   328  		t.Run(string(tc.context.Format), func(t *testing.T) {
   329  			err := ContainerWrite(tc.context, containers)
   330  			assert.NilError(t, err)
   331  			assert.Equal(t, out.String(), tc.expected)
   332  			// Clean buffer
   333  			out.Reset()
   334  		})
   335  	}
   336  }
   337  
   338  func TestContainerContextWriteJSON(t *testing.T) {
   339  	unix := time.Now().Add(-65 * time.Second).Unix()
   340  	containers := []types.Container{
   341  		{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unix, State: "running"},
   342  		{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unix, State: "running"},
   343  	}
   344  	expectedCreated := time.Unix(unix, 0).String()
   345  	expectedJSONs := []map[string]any{
   346  		{
   347  			"Command":      "\"\"",
   348  			"CreatedAt":    expectedCreated,
   349  			"ID":           "containerID1",
   350  			"Image":        "ubuntu",
   351  			"Labels":       "",
   352  			"LocalVolumes": "0",
   353  			"Mounts":       "",
   354  			"Names":        "foobar_baz",
   355  			"Networks":     "",
   356  			"Ports":        "",
   357  			"RunningFor":   "About a minute ago",
   358  			"Size":         "0B",
   359  			"State":        "running",
   360  			"Status":       "",
   361  		},
   362  		{
   363  			"Command":      "\"\"",
   364  			"CreatedAt":    expectedCreated,
   365  			"ID":           "containerID2",
   366  			"Image":        "ubuntu",
   367  			"Labels":       "",
   368  			"LocalVolumes": "0",
   369  			"Mounts":       "",
   370  			"Names":        "foobar_bar",
   371  			"Networks":     "",
   372  			"Ports":        "",
   373  			"RunningFor":   "About a minute ago",
   374  			"Size":         "0B",
   375  			"State":        "running",
   376  			"Status":       "",
   377  		},
   378  	}
   379  	out := bytes.NewBufferString("")
   380  	err := ContainerWrite(Context{Format: "{{json .}}", Output: out}, containers)
   381  	if err != nil {
   382  		t.Fatal(err)
   383  	}
   384  	for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
   385  		msg := fmt.Sprintf("Output: line %d: %s", i, line)
   386  		var m map[string]any
   387  		err := json.Unmarshal([]byte(line), &m)
   388  		assert.NilError(t, err, msg)
   389  		assert.Check(t, is.DeepEqual(expectedJSONs[i], m), msg)
   390  	}
   391  }
   392  
   393  func TestContainerContextWriteJSONField(t *testing.T) {
   394  	containers := []types.Container{
   395  		{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu"},
   396  		{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu"},
   397  	}
   398  	out := bytes.NewBufferString("")
   399  	err := ContainerWrite(Context{Format: "{{json .ID}}", Output: out}, containers)
   400  	if err != nil {
   401  		t.Fatal(err)
   402  	}
   403  	for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
   404  		msg := fmt.Sprintf("Output: line %d: %s", i, line)
   405  		var s string
   406  		err := json.Unmarshal([]byte(line), &s)
   407  		assert.NilError(t, err, msg)
   408  		assert.Check(t, is.Equal(containers[i].ID, s), msg)
   409  	}
   410  }
   411  
   412  func TestContainerBackCompat(t *testing.T) {
   413  	containers := []types.Container{{ID: "brewhaha"}}
   414  	cases := []string{
   415  		"ID",
   416  		"Names",
   417  		"Image",
   418  		"Command",
   419  		"CreatedAt",
   420  		"RunningFor",
   421  		"Ports",
   422  		"Status",
   423  		"Size",
   424  		"Labels",
   425  		"Mounts",
   426  	}
   427  	buf := bytes.NewBuffer(nil)
   428  	for _, c := range cases {
   429  		ctx := Context{Format: Format(fmt.Sprintf("{{ .%s }}", c)), Output: buf}
   430  		if err := ContainerWrite(ctx, containers); err != nil {
   431  			t.Logf("could not render template for field '%s': %v", c, err)
   432  			t.Fail()
   433  		}
   434  		buf.Reset()
   435  	}
   436  }
   437  
   438  type ports struct {
   439  	ports    []types.Port
   440  	expected string
   441  }
   442  
   443  //nolint:lll
   444  func TestDisplayablePorts(t *testing.T) {
   445  	cases := []ports{
   446  		{
   447  			[]types.Port{
   448  				{
   449  					PrivatePort: 9988,
   450  					Type:        "tcp",
   451  				},
   452  			},
   453  			"9988/tcp",
   454  		},
   455  		{
   456  			[]types.Port{
   457  				{
   458  					PrivatePort: 9988,
   459  					Type:        "udp",
   460  				},
   461  			},
   462  			"9988/udp",
   463  		},
   464  		{
   465  			[]types.Port{
   466  				{
   467  					IP:          "0.0.0.0",
   468  					PrivatePort: 9988,
   469  					Type:        "tcp",
   470  				},
   471  			},
   472  			"0.0.0.0:0->9988/tcp",
   473  		},
   474  		{
   475  			[]types.Port{
   476  				{
   477  					PrivatePort: 9988,
   478  					PublicPort:  8899,
   479  					Type:        "tcp",
   480  				},
   481  			},
   482  			"9988/tcp",
   483  		},
   484  		{
   485  			[]types.Port{
   486  				{
   487  					IP:          "4.3.2.1",
   488  					PrivatePort: 9988,
   489  					PublicPort:  8899,
   490  					Type:        "tcp",
   491  				},
   492  			},
   493  			"4.3.2.1:8899->9988/tcp",
   494  		},
   495  		{
   496  			[]types.Port{
   497  				{
   498  					IP:          "4.3.2.1",
   499  					PrivatePort: 9988,
   500  					PublicPort:  9988,
   501  					Type:        "tcp",
   502  				},
   503  			},
   504  			"4.3.2.1:9988->9988/tcp",
   505  		},
   506  		{
   507  			[]types.Port{
   508  				{
   509  					PrivatePort: 9988,
   510  					Type:        "udp",
   511  				}, {
   512  					PrivatePort: 9988,
   513  					Type:        "udp",
   514  				},
   515  			},
   516  			"9988/udp, 9988/udp",
   517  		},
   518  		{
   519  			[]types.Port{
   520  				{
   521  					IP:          "1.2.3.4",
   522  					PublicPort:  9998,
   523  					PrivatePort: 9998,
   524  					Type:        "udp",
   525  				}, {
   526  					IP:          "1.2.3.4",
   527  					PublicPort:  9999,
   528  					PrivatePort: 9999,
   529  					Type:        "udp",
   530  				},
   531  			},
   532  			"1.2.3.4:9998-9999->9998-9999/udp",
   533  		},
   534  		{
   535  			[]types.Port{
   536  				{
   537  					IP:          "1.2.3.4",
   538  					PublicPort:  8887,
   539  					PrivatePort: 9998,
   540  					Type:        "udp",
   541  				}, {
   542  					IP:          "1.2.3.4",
   543  					PublicPort:  8888,
   544  					PrivatePort: 9999,
   545  					Type:        "udp",
   546  				},
   547  			},
   548  			"1.2.3.4:8887->9998/udp, 1.2.3.4:8888->9999/udp",
   549  		},
   550  		{
   551  			[]types.Port{
   552  				{
   553  					PrivatePort: 9998,
   554  					Type:        "udp",
   555  				}, {
   556  					PrivatePort: 9999,
   557  					Type:        "udp",
   558  				},
   559  			},
   560  			"9998-9999/udp",
   561  		},
   562  		{
   563  			[]types.Port{
   564  				{
   565  					IP:          "1.2.3.4",
   566  					PrivatePort: 6677,
   567  					PublicPort:  7766,
   568  					Type:        "tcp",
   569  				}, {
   570  					PrivatePort: 9988,
   571  					PublicPort:  8899,
   572  					Type:        "udp",
   573  				},
   574  			},
   575  			"9988/udp, 1.2.3.4:7766->6677/tcp",
   576  		},
   577  		{
   578  			[]types.Port{
   579  				{
   580  					IP:          "1.2.3.4",
   581  					PrivatePort: 9988,
   582  					PublicPort:  8899,
   583  					Type:        "udp",
   584  				}, {
   585  					IP:          "1.2.3.4",
   586  					PrivatePort: 9988,
   587  					PublicPort:  8899,
   588  					Type:        "tcp",
   589  				}, {
   590  					IP:          "4.3.2.1",
   591  					PrivatePort: 2233,
   592  					PublicPort:  3322,
   593  					Type:        "tcp",
   594  				},
   595  			},
   596  			"4.3.2.1:3322->2233/tcp, 1.2.3.4:8899->9988/tcp, 1.2.3.4:8899->9988/udp",
   597  		},
   598  		{
   599  			[]types.Port{
   600  				{
   601  					PrivatePort: 9988,
   602  					PublicPort:  8899,
   603  					Type:        "udp",
   604  				}, {
   605  					IP:          "1.2.3.4",
   606  					PrivatePort: 6677,
   607  					PublicPort:  7766,
   608  					Type:        "tcp",
   609  				}, {
   610  					IP:          "4.3.2.1",
   611  					PrivatePort: 2233,
   612  					PublicPort:  3322,
   613  					Type:        "tcp",
   614  				},
   615  			},
   616  			"9988/udp, 4.3.2.1:3322->2233/tcp, 1.2.3.4:7766->6677/tcp",
   617  		},
   618  		{
   619  			[]types.Port{
   620  				{
   621  					PrivatePort: 80,
   622  					Type:        "tcp",
   623  				}, {
   624  					PrivatePort: 1024,
   625  					Type:        "tcp",
   626  				}, {
   627  					PrivatePort: 80,
   628  					Type:        "udp",
   629  				}, {
   630  					PrivatePort: 1024,
   631  					Type:        "udp",
   632  				}, {
   633  					IP:          "1.1.1.1",
   634  					PublicPort:  80,
   635  					PrivatePort: 1024,
   636  					Type:        "tcp",
   637  				}, {
   638  					IP:          "1.1.1.1",
   639  					PublicPort:  80,
   640  					PrivatePort: 1024,
   641  					Type:        "udp",
   642  				}, {
   643  					IP:          "1.1.1.1",
   644  					PublicPort:  1024,
   645  					PrivatePort: 80,
   646  					Type:        "tcp",
   647  				}, {
   648  					IP:          "1.1.1.1",
   649  					PublicPort:  1024,
   650  					PrivatePort: 80,
   651  					Type:        "udp",
   652  				}, {
   653  					IP:          "2.1.1.1",
   654  					PublicPort:  80,
   655  					PrivatePort: 1024,
   656  					Type:        "tcp",
   657  				}, {
   658  					IP:          "2.1.1.1",
   659  					PublicPort:  80,
   660  					PrivatePort: 1024,
   661  					Type:        "udp",
   662  				}, {
   663  					IP:          "2.1.1.1",
   664  					PublicPort:  1024,
   665  					PrivatePort: 80,
   666  					Type:        "tcp",
   667  				}, {
   668  					IP:          "2.1.1.1",
   669  					PublicPort:  1024,
   670  					PrivatePort: 80,
   671  					Type:        "udp",
   672  				}, {
   673  					PrivatePort: 12345,
   674  					Type:        "sctp",
   675  				},
   676  			},
   677  			"80/tcp, 80/udp, 1024/tcp, 1024/udp, 12345/sctp, 1.1.1.1:1024->80/tcp, 1.1.1.1:1024->80/udp, 2.1.1.1:1024->80/tcp, 2.1.1.1:1024->80/udp, 1.1.1.1:80->1024/tcp, 1.1.1.1:80->1024/udp, 2.1.1.1:80->1024/tcp, 2.1.1.1:80->1024/udp",
   678  		},
   679  	}
   680  
   681  	for _, port := range cases {
   682  		actual := DisplayablePorts(port.ports)
   683  		assert.Check(t, is.Equal(port.expected, actual))
   684  	}
   685  }