package jsonmessage import ( "bytes" "fmt" "os" "strings" "testing" "time" "github.com/docker/docker/pkg/jsonlog" "github.com/docker/docker/pkg/term" "github.com/stretchr/testify/assert" ) func TestError(t *testing.T) { je := JSONError{404, "Not found"} if je.Error() != "Not found" { t.Fatalf("Expected 'Not found' got '%s'", je.Error()) } } func TestProgress(t *testing.T) { termsz, err := term.GetWinsize(0) if err != nil { // we can safely ignore the err here termsz = nil } jp := JSONProgress{} if jp.String() != "" { t.Fatalf("Expected empty string, got '%s'", jp.String()) } expected := " 1B" jp2 := JSONProgress{Current: 1} if jp2.String() != expected { t.Fatalf("Expected %q, got %q", expected, jp2.String()) } expectedStart := "[==========> ] 20B/100B" if termsz != nil && termsz.Width <= 110 { expectedStart = " 20B/100B" } jp3 := JSONProgress{Current: 20, Total: 100, Start: time.Now().Unix()} // Just look at the start of the string // (the remaining time is really hard to test -_-) if jp3.String()[:len(expectedStart)] != expectedStart { t.Fatalf("Expected to start with %q, got %q", expectedStart, jp3.String()) } expected = "[=========================> ] 50B/100B" if termsz != nil && termsz.Width <= 110 { expected = " 50B/100B" } jp4 := JSONProgress{Current: 50, Total: 100} if jp4.String() != expected { t.Fatalf("Expected %q, got %q", expected, jp4.String()) } // this number can't be negative gh#7136 expected = "[==================================================>] 50B" if termsz != nil && termsz.Width <= 110 { expected = " 50B" } jp5 := JSONProgress{Current: 50, Total: 40} if jp5.String() != expected { t.Fatalf("Expected %q, got %q", expected, jp5.String()) } expected = "[=========================> ] 50/100 units" if termsz != nil && termsz.Width <= 110 { expected = " 50/100 units" } jp6 := JSONProgress{Current: 50, Total: 100, Units: "units"} if jp6.String() != expected { t.Fatalf("Expected %q, got %q", expected, jp6.String()) } // this number can't be negative expected = "[==================================================>] 50 units" if termsz != nil && termsz.Width <= 110 { expected = " 50 units" } jp7 := JSONProgress{Current: 50, Total: 40, Units: "units"} if jp7.String() != expected { t.Fatalf("Expected %q, got %q", expected, jp7.String()) } expected = "[=========================> ] " if termsz != nil && termsz.Width <= 110 { expected = "" } jp8 := JSONProgress{Current: 50, Total: 100, HideCounts: true} if jp8.String() != expected { t.Fatalf("Expected %q, got %q", expected, jp8.String()) } } func TestJSONMessageDisplay(t *testing.T) { now := time.Now() messages := map[JSONMessage][]string{ // Empty {}: {"\n", "\n"}, // Status { Status: "status", }: { "status\n", "status\n", }, // General { Time: now.Unix(), ID: "ID", From: "From", Status: "status", }: { fmt.Sprintf("%v ID: (from From) status\n", time.Unix(now.Unix(), 0).Format(jsonlog.RFC3339NanoFixed)), fmt.Sprintf("%v ID: (from From) status\n", time.Unix(now.Unix(), 0).Format(jsonlog.RFC3339NanoFixed)), }, // General, with nano precision time { TimeNano: now.UnixNano(), ID: "ID", From: "From", Status: "status", }: { fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(jsonlog.RFC3339NanoFixed)), fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(jsonlog.RFC3339NanoFixed)), }, // General, with both times Nano is preferred { Time: now.Unix(), TimeNano: now.UnixNano(), ID: "ID", From: "From", Status: "status", }: { fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(jsonlog.RFC3339NanoFixed)), fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(jsonlog.RFC3339NanoFixed)), }, // Stream over status { Status: "status", Stream: "stream", }: { "stream", "stream", }, // With progress message { Status: "status", ProgressMessage: "progressMessage", }: { "status progressMessage", "status progressMessage", }, // With progress, stream empty { Status: "status", Stream: "", Progress: &JSONProgress{Current: 1}, }: { "", fmt.Sprintf("%c[1K%c[K\rstatus 1B\r", 27, 27), }, } // The tests :) for jsonMessage, expectedMessages := range messages { // Without terminal data := bytes.NewBuffer([]byte{}) if err := jsonMessage.Display(data, nil); err != nil { t.Fatal(err) } if data.String() != expectedMessages[0] { t.Fatalf("Expected %q,got %q", expectedMessages[0], data.String()) } // With terminal data = bytes.NewBuffer([]byte{}) if err := jsonMessage.Display(data, &noTermInfo{}); err != nil { t.Fatal(err) } if data.String() != expectedMessages[1] { t.Fatalf("\nExpected %q\n got %q", expectedMessages[1], data.String()) } } } // Test JSONMessage with an Error. It will return an error with the text as error, not the meaning of the HTTP code. func TestJSONMessageDisplayWithJSONError(t *testing.T) { data := bytes.NewBuffer([]byte{}) jsonMessage := JSONMessage{Error: &JSONError{404, "Can't find it"}} err := jsonMessage.Display(data, &noTermInfo{}) if err == nil || err.Error() != "Can't find it" { t.Fatalf("Expected a JSONError 404, got %q", err) } jsonMessage = JSONMessage{Error: &JSONError{401, "Anything"}} err = jsonMessage.Display(data, &noTermInfo{}) assert.EqualError(t, err, "authentication is required") } func TestDisplayJSONMessagesStreamInvalidJSON(t *testing.T) { var ( inFd uintptr ) data := bytes.NewBuffer([]byte{}) reader := strings.NewReader("This is not a 'valid' JSON []") inFd, _ = term.GetFdInfo(reader) if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err == nil && err.Error()[:17] != "invalid character" { t.Fatalf("Should have thrown an error (invalid character in ..), got %q", err) } } func TestDisplayJSONMessagesStream(t *testing.T) { var ( inFd uintptr ) messages := map[string][]string{ // empty string "": { "", ""}, // Without progress & ID "{ \"status\": \"status\" }": { "status\n", "status\n", }, // Without progress, with ID "{ \"id\": \"ID\",\"status\": \"status\" }": { "ID: status\n", fmt.Sprintf("ID: status\n"), }, // With progress "{ \"id\": \"ID\", \"status\": \"status\", \"progress\": \"ProgressMessage\" }": { "ID: status ProgressMessage", fmt.Sprintf("\n%c[%dAID: status ProgressMessage%c[%dB", 27, 1, 27, 1), }, // With progressDetail "{ \"id\": \"ID\", \"status\": \"status\", \"progressDetail\": { \"Current\": 1} }": { "", // progressbar is disabled in non-terminal fmt.Sprintf("\n%c[%dA%c[1K%c[K\rID: status 1B\r%c[%dB", 27, 1, 27, 27, 27, 1), }, } // Use $TERM which is unlikely to exist, forcing DisplayJSONMessageStream to // (hopefully) use &noTermInfo. origTerm := os.Getenv("TERM") os.Setenv("TERM", "xyzzy-non-existent-terminfo") for jsonMessage, expectedMessages := range messages { data := bytes.NewBuffer([]byte{}) reader := strings.NewReader(jsonMessage) inFd, _ = term.GetFdInfo(reader) // Without terminal if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err != nil { t.Fatal(err) } if data.String() != expectedMessages[0] { t.Fatalf("Expected an %q, got %q", expectedMessages[0], data.String()) } // With terminal data = bytes.NewBuffer([]byte{}) reader = strings.NewReader(jsonMessage) if err := DisplayJSONMessagesStream(reader, data, inFd, true, nil); err != nil { t.Fatal(err) } if data.String() != expectedMessages[1] { t.Fatalf("\nExpected %q\n got %q", expectedMessages[1], data.String()) } } os.Setenv("TERM", origTerm) }