Initial commit

This commit is contained in:
Ria Bhatia
2017-12-04 13:32:57 -06:00
committed by Erik St. Martin
commit 0075e5b0f3
9056 changed files with 2523100 additions and 0 deletions

View File

@@ -0,0 +1,221 @@
package jsonmessage
import (
"encoding/json"
"fmt"
"io"
"strings"
"time"
"github.com/hyperhq/hypercli/pkg/jsonlog"
"github.com/hyperhq/hypercli/pkg/term"
"github.com/docker/go-units"
)
// JSONError wraps a concrete Code and Message, `Code` is
// is a integer error code, `Message` is the error message.
type JSONError struct {
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
func (e *JSONError) Error() string {
return e.Message
}
// JSONProgress describes a Progress. terminalFd is the fd of the current terminal,
// Start is the initial value for the operation. Current is the current status and
// value of the progress made towards Total. Total is the end value describing when
// we made 100% progress for an operation.
type JSONProgress struct {
terminalFd uintptr
Current int64 `json:"current,omitempty"`
Total int64 `json:"total,omitempty"`
Start int64 `json:"start,omitempty"`
}
func (p *JSONProgress) String() string {
var (
width = 200
pbBox string
numbersBox string
timeLeftBox string
)
ws, err := term.GetWinsize(p.terminalFd)
if err == nil {
width = int(ws.Width)
}
if p.Current <= 0 && p.Total <= 0 {
return ""
}
current := units.HumanSize(float64(p.Current))
if p.Total <= 0 {
return fmt.Sprintf("%8v", current)
}
total := units.HumanSize(float64(p.Total))
percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
if percentage > 50 {
percentage = 50
}
if width > 110 {
// this number can't be negative gh#7136
numSpaces := 0
if 50-percentage > 0 {
numSpaces = 50 - percentage
}
pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
}
numbersBox = fmt.Sprintf("%8v/%v", current, total)
if p.Current > p.Total {
// remove total display if the reported current is wonky.
numbersBox = fmt.Sprintf("%8v", current)
}
if p.Current > 0 && p.Start > 0 && percentage < 50 {
fromStart := time.Now().UTC().Sub(time.Unix(p.Start, 0))
perEntry := fromStart / time.Duration(p.Current)
left := time.Duration(p.Total-p.Current) * perEntry
left = (left / time.Second) * time.Second
if width > 50 {
timeLeftBox = " " + left.String()
}
}
return pbBox + numbersBox + timeLeftBox
}
// JSONMessage defines a message struct. It describes
// the created time, where it from, status, ID of the
// message. It's used for docker events.
type JSONMessage struct {
Stream string `json:"stream,omitempty"`
Status string `json:"status,omitempty"`
Progress *JSONProgress `json:"progressDetail,omitempty"`
ProgressMessage string `json:"progress,omitempty"` //deprecated
ID string `json:"id,omitempty"`
From string `json:"from,omitempty"`
Time int64 `json:"time,omitempty"`
TimeNano int64 `json:"timeNano,omitempty"`
Error *JSONError `json:"errorDetail,omitempty"`
ErrorMessage string `json:"error,omitempty"` //deprecated
// Aux contains out-of-band data, such as digests for push signing.
Aux *json.RawMessage `json:"aux,omitempty"`
}
// Display displays the JSONMessage to `out`. `isTerminal` describes if `out`
// is a terminal. If this is the case, it will erase the entire current line
// when displaying the progressbar.
func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error {
if jm.Error != nil {
if jm.Error.Code == 401 {
return fmt.Errorf("Authentication is required.")
}
return jm.Error
}
var endl string
if isTerminal && jm.Stream == "" && jm.Progress != nil {
// <ESC>[2K = erase entire current line
fmt.Fprintf(out, "%c[2K\r", 27)
endl = "\r"
} else if jm.Progress != nil && jm.Progress.String() != "" { //disable progressbar in non-terminal
return nil
}
if jm.TimeNano != 0 {
fmt.Fprintf(out, "%s ", time.Unix(0, jm.TimeNano).Format(jsonlog.RFC3339NanoFixed))
} else if jm.Time != 0 {
fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(jsonlog.RFC3339NanoFixed))
}
if jm.ID != "" {
fmt.Fprintf(out, "%s: ", jm.ID)
}
if jm.From != "" {
fmt.Fprintf(out, "(from %s) ", jm.From)
}
if jm.Progress != nil && isTerminal {
fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl)
} else if jm.ProgressMessage != "" { //deprecated
fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl)
} else if jm.Stream != "" {
fmt.Fprintf(out, "%s%s", jm.Stream, endl)
} else {
fmt.Fprintf(out, "%s%s\n", jm.Status, endl)
}
return nil
}
// DisplayJSONMessagesStream displays a json message stream from `in` to `out`, `isTerminal`
// describes if `out` is a terminal. If this is the case, it will print `\n` at the end of
// each line and move the cursor while displaying.
func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool, auxCallback func(*json.RawMessage)) error {
var (
dec = json.NewDecoder(in)
ids = make(map[string]int)
)
for {
diff := 0
var jm JSONMessage
if err := dec.Decode(&jm); err != nil {
if err == io.EOF {
break
}
return err
}
if jm.Aux != nil {
if auxCallback != nil {
auxCallback(jm.Aux)
}
continue
}
if jm.Progress != nil {
jm.Progress.terminalFd = terminalFd
}
if jm.ID != "" && (jm.Progress != nil || jm.ProgressMessage != "") {
line, ok := ids[jm.ID]
if !ok {
// NOTE: This approach of using len(id) to
// figure out the number of lines of history
// only works as long as we clear the history
// when we output something that's not
// accounted for in the map, such as a line
// with no ID.
line = len(ids)
ids[jm.ID] = line
if isTerminal {
fmt.Fprintf(out, "\n")
}
} else {
diff = len(ids) - line
}
if isTerminal {
// NOTE: this appears to be necessary even if
// diff == 0.
// <ESC>[{diff}A = move cursor up diff rows
fmt.Fprintf(out, "%c[%dA", 27, diff)
}
} else {
// When outputting something that isn't progress
// output, clear the history of previous lines. We
// don't want progress entries from some previous
// operation to be updated (for example, pull -a
// with multiple tags).
ids = make(map[string]int)
}
err := jm.Display(out, isTerminal)
if jm.ID != "" && isTerminal {
// NOTE: this appears to be necessary even if
// diff == 0.
// <ESC>[{diff}B = move cursor down diff rows
fmt.Fprintf(out, "%c[%dB", 27, diff)
}
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,231 @@
package jsonmessage
import (
"bytes"
"fmt"
"strings"
"testing"
"time"
"github.com/hyperhq/hypercli/pkg/jsonlog"
"github.com/hyperhq/hypercli/pkg/term"
)
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) {
jp := JSONProgress{}
if jp.String() != "" {
t.Fatalf("Expected empty string, got '%s'", jp.String())
}
expected := " 1 B"
jp2 := JSONProgress{Current: 1}
if jp2.String() != expected {
t.Fatalf("Expected %q, got %q", expected, jp2.String())
}
expectedStart := "[==========> ] 20 B/100 B"
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 = "[=========================> ] 50 B/100 B"
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 = "[==================================================>] 50 B"
jp5 := JSONProgress{Current: 50, Total: 40}
if jp5.String() != expected {
t.Fatalf("Expected %q, got %q", expected, jp5.String())
}
}
func TestJSONMessageDisplay(t *testing.T) {
now := time.Now()
messages := map[JSONMessage][]string{
// Empty
JSONMessage{}: {"\n", "\n"},
// Status
JSONMessage{
Status: "status",
}: {
"status\n",
"status\n",
},
// General
JSONMessage{
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
JSONMessage{
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
JSONMessage{
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
JSONMessage{
Status: "status",
Stream: "stream",
}: {
"stream",
"stream",
},
// With progress message
JSONMessage{
Status: "status",
ProgressMessage: "progressMessage",
}: {
"status progressMessage",
"status progressMessage",
},
// With progress, stream empty
JSONMessage{
Status: "status",
Stream: "",
Progress: &JSONProgress{Current: 1},
}: {
"",
fmt.Sprintf("%c[2K\rstatus 1 B\r", 27),
},
}
// The tests :)
for jsonMessage, expectedMessages := range messages {
// Without terminal
data := bytes.NewBuffer([]byte{})
if err := jsonMessage.Display(data, false); err != nil {
t.Fatal(err)
}
if data.String() != expectedMessages[0] {
t.Fatalf("Expected [%v], got [%v]", expectedMessages[0], data.String())
}
// With terminal
data = bytes.NewBuffer([]byte{})
if err := jsonMessage.Display(data, true); err != nil {
t.Fatal(err)
}
if data.String() != expectedMessages[1] {
t.Fatalf("Expected [%v], got [%v]", 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, true)
if err == nil || err.Error() != "Can't find it" {
t.Fatalf("Expected a JSONError 404, got [%v]", err)
}
jsonMessage = JSONMessage{Error: &JSONError{401, "Anything"}}
err = jsonMessage.Display(data, true)
if err == nil || err.Error() != "Authentication is required." {
t.Fatalf("Expected an error [Authentication is required.], got [%v]", err)
}
}
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 [%v]", 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%c[%dB", 27, 0),
},
// With progress
"{ \"id\": \"ID\", \"status\": \"status\", \"progress\": \"ProgressMessage\" }": {
"ID: status ProgressMessage",
fmt.Sprintf("\n%c[%dAID: status ProgressMessage%c[%dB", 27, 0, 27, 0),
},
// With progressDetail
"{ \"id\": \"ID\", \"status\": \"status\", \"progressDetail\": { \"Current\": 1} }": {
"", // progressbar is disabled in non-terminal
fmt.Sprintf("\n%c[%dA%c[2K\rID: status 1 B\r%c[%dB", 27, 0, 27, 27, 0),
},
}
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 [%v], got [%v]", 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("Expected an [%v], got [%v]", expectedMessages[1], data.String())
}
}
}