github.com/moby/docker@v26.1.3+incompatible/pkg/jsonmessage/jsonmessage_test.go (about)

     1  package jsonmessage // import "github.com/docker/docker/pkg/jsonmessage"
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/moby/term"
    11  	"gotest.tools/v3/assert"
    12  	is "gotest.tools/v3/assert/cmp"
    13  )
    14  
    15  func TestError(t *testing.T) {
    16  	je := JSONError{404, "Not found"}
    17  	assert.Assert(t, is.Error(&je, "Not found"))
    18  }
    19  
    20  func TestProgressString(t *testing.T) {
    21  	type expected struct {
    22  		short string
    23  		long  string
    24  	}
    25  
    26  	shortAndLong := func(short, long string) expected {
    27  		return expected{short: short, long: long}
    28  	}
    29  
    30  	start := time.Date(2017, 12, 3, 15, 10, 1, 0, time.UTC)
    31  	timeAfter := func(delta time.Duration) func() time.Time {
    32  		return func() time.Time {
    33  			return start.Add(delta)
    34  		}
    35  	}
    36  
    37  	testcases := []struct {
    38  		name     string
    39  		progress JSONProgress
    40  		expected expected
    41  	}{
    42  		{
    43  			name: "no progress",
    44  		},
    45  		{
    46  			name:     "progress 1",
    47  			progress: JSONProgress{Current: 1},
    48  			expected: shortAndLong("      1B", "      1B"),
    49  		},
    50  		{
    51  			name: "some progress with a start time",
    52  			progress: JSONProgress{
    53  				Current: 20,
    54  				Total:   100,
    55  				Start:   start.Unix(),
    56  				nowFunc: timeAfter(time.Second),
    57  			},
    58  			expected: shortAndLong(
    59  				"     20B/100B 4s",
    60  				"[==========>                                        ]      20B/100B 4s",
    61  			),
    62  		},
    63  		{
    64  			name:     "some progress without a start time",
    65  			progress: JSONProgress{Current: 50, Total: 100},
    66  			expected: shortAndLong(
    67  				"     50B/100B",
    68  				"[=========================>                         ]      50B/100B",
    69  			),
    70  		},
    71  		{
    72  			name:     "current more than total is not negative gh#7136",
    73  			progress: JSONProgress{Current: 50, Total: 40},
    74  			expected: shortAndLong(
    75  				"     50B",
    76  				"[==================================================>]      50B",
    77  			),
    78  		},
    79  		{
    80  			name:     "with units",
    81  			progress: JSONProgress{Current: 50, Total: 100, Units: "units"},
    82  			expected: shortAndLong(
    83  				"50/100 units",
    84  				"[=========================>                         ] 50/100 units",
    85  			),
    86  		},
    87  		{
    88  			name:     "current more than total with units is not negative ",
    89  			progress: JSONProgress{Current: 50, Total: 40, Units: "units"},
    90  			expected: shortAndLong(
    91  				"50 units",
    92  				"[==================================================>] 50 units",
    93  			),
    94  		},
    95  		{
    96  			name:     "hide counts",
    97  			progress: JSONProgress{Current: 50, Total: 100, HideCounts: true},
    98  			expected: shortAndLong(
    99  				"",
   100  				"[=========================>                         ] ",
   101  			),
   102  		},
   103  	}
   104  
   105  	for _, testcase := range testcases {
   106  		t.Run(testcase.name, func(t *testing.T) {
   107  			testcase.progress.winSize = 100
   108  			assert.Equal(t, testcase.progress.String(), testcase.expected.short)
   109  
   110  			testcase.progress.winSize = 200
   111  			assert.Equal(t, testcase.progress.String(), testcase.expected.long)
   112  		})
   113  	}
   114  }
   115  
   116  func TestJSONMessageDisplay(t *testing.T) {
   117  	now := time.Now()
   118  	messages := map[JSONMessage][]string{
   119  		// Empty
   120  		{}: {"\n", "\n"},
   121  		// Status
   122  		{
   123  			Status: "status",
   124  		}: {
   125  			"status\n",
   126  			"status\n",
   127  		},
   128  		// General
   129  		{
   130  			Time:   now.Unix(),
   131  			ID:     "ID",
   132  			From:   "From",
   133  			Status: "status",
   134  		}: {
   135  			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(now.Unix(), 0).Format(RFC3339NanoFixed)),
   136  			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(now.Unix(), 0).Format(RFC3339NanoFixed)),
   137  		},
   138  		// General, with nano precision time
   139  		{
   140  			TimeNano: now.UnixNano(),
   141  			ID:       "ID",
   142  			From:     "From",
   143  			Status:   "status",
   144  		}: {
   145  			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(RFC3339NanoFixed)),
   146  			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(RFC3339NanoFixed)),
   147  		},
   148  		// General, with both times Nano is preferred
   149  		{
   150  			Time:     now.Unix(),
   151  			TimeNano: now.UnixNano(),
   152  			ID:       "ID",
   153  			From:     "From",
   154  			Status:   "status",
   155  		}: {
   156  			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(RFC3339NanoFixed)),
   157  			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(RFC3339NanoFixed)),
   158  		},
   159  		// Stream over status
   160  		{
   161  			Status: "status",
   162  			Stream: "stream",
   163  		}: {
   164  			"stream",
   165  			"stream",
   166  		},
   167  		// With progress message
   168  		{
   169  			Status:          "status",
   170  			ProgressMessage: "progressMessage",
   171  		}: {
   172  			"status progressMessage",
   173  			"status progressMessage",
   174  		},
   175  		// With progress, stream empty
   176  		{
   177  			Status:   "status",
   178  			Stream:   "",
   179  			Progress: &JSONProgress{Current: 1},
   180  		}: {
   181  			"",
   182  			fmt.Sprintf("%c[2K\rstatus       1B\r", 27),
   183  		},
   184  	}
   185  
   186  	// The tests :)
   187  	for jsonMessage, expectedMessages := range messages {
   188  		// Without terminal
   189  		data := bytes.NewBuffer([]byte{})
   190  		if err := jsonMessage.Display(data, false); err != nil {
   191  			t.Fatal(err)
   192  		}
   193  		if data.String() != expectedMessages[0] {
   194  			t.Fatalf("Expected %q,got %q", expectedMessages[0], data.String())
   195  		}
   196  		// With terminal
   197  		data = bytes.NewBuffer([]byte{})
   198  		if err := jsonMessage.Display(data, true); err != nil {
   199  			t.Fatal(err)
   200  		}
   201  		if data.String() != expectedMessages[1] {
   202  			t.Fatalf("\nExpected %q\n     got %q", expectedMessages[1], data.String())
   203  		}
   204  	}
   205  }
   206  
   207  // Test JSONMessage with an Error. It will return an error with the text as error, not the meaning of the HTTP code.
   208  func TestJSONMessageDisplayWithJSONError(t *testing.T) {
   209  	data := bytes.NewBuffer([]byte{})
   210  	jsonMessage := JSONMessage{Error: &JSONError{404, "Can't find it"}}
   211  
   212  	err := jsonMessage.Display(data, true)
   213  	if err == nil || err.Error() != "Can't find it" {
   214  		t.Fatalf("Expected a JSONError 404, got %q", err)
   215  	}
   216  
   217  	jsonMessage = JSONMessage{Error: &JSONError{401, "Anything"}}
   218  	err = jsonMessage.Display(data, true)
   219  	assert.Check(t, is.Error(err, "Anything"))
   220  }
   221  
   222  func TestDisplayJSONMessagesStreamInvalidJSON(t *testing.T) {
   223  	var inFd uintptr
   224  	data := bytes.NewBuffer([]byte{})
   225  	reader := strings.NewReader("This is not a 'valid' JSON []")
   226  	inFd, _ = term.GetFdInfo(reader)
   227  
   228  	exp := "invalid character "
   229  	if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err == nil || !strings.HasPrefix(err.Error(), exp) {
   230  		t.Fatalf("Expected error (%s...), got %q", exp, err)
   231  	}
   232  }
   233  
   234  func TestDisplayJSONMessagesStream(t *testing.T) {
   235  	var inFd uintptr
   236  
   237  	messages := map[string][]string{
   238  		// empty string
   239  		"": {
   240  			"",
   241  			"",
   242  		},
   243  		// Without progress & ID
   244  		`{ "status": "status" }`: {
   245  			"status\n",
   246  			"status\n",
   247  		},
   248  		// Without progress, with ID
   249  		`{ "id": "ID","status": "status" }`: {
   250  			"ID: status\n",
   251  			"ID: status\n",
   252  		},
   253  		// With progress
   254  		`{ "id": "ID", "status": "status", "progress": "ProgressMessage" }`: {
   255  			"ID: status ProgressMessage",
   256  			fmt.Sprintf("\n%c[%dAID: status ProgressMessage%c[%dB", 27, 1, 27, 1),
   257  		},
   258  		// With progressDetail
   259  		`{ "id": "ID", "status": "status", "progressDetail": { "Current": 1} }`: {
   260  			"", // progressbar is disabled in non-terminal
   261  			fmt.Sprintf("\n%c[%dA%c[2K\rID: status       1B\r%c[%dB", 27, 1, 27, 27, 1),
   262  		},
   263  	}
   264  
   265  	// Use $TERM which is unlikely to exist, forcing DisplayJSONMessageStream to
   266  	// (hopefully) use &noTermInfo.
   267  	t.Setenv("TERM", "xyzzy-non-existent-terminfo")
   268  
   269  	for jsonMessage, expectedMessages := range messages {
   270  		data := bytes.NewBuffer([]byte{})
   271  		reader := strings.NewReader(jsonMessage)
   272  		inFd, _ = term.GetFdInfo(reader)
   273  
   274  		// Without terminal
   275  		if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err != nil {
   276  			t.Fatal(err)
   277  		}
   278  		if data.String() != expectedMessages[0] {
   279  			t.Fatalf("Expected an %q, got %q", expectedMessages[0], data.String())
   280  		}
   281  
   282  		// With terminal
   283  		data = bytes.NewBuffer([]byte{})
   284  		reader = strings.NewReader(jsonMessage)
   285  		if err := DisplayJSONMessagesStream(reader, data, inFd, true, nil); err != nil {
   286  			t.Fatal(err)
   287  		}
   288  		if data.String() != expectedMessages[1] {
   289  			t.Fatalf("\nExpected %q\n     got %q", expectedMessages[1], data.String())
   290  		}
   291  	}
   292  }