From aafdb9a2387b5a50d880a2ee563fcf2786217fc8 Mon Sep 17 00:00:00 2001 From: Naomi Seyfer Date: Tue, 29 Oct 2019 03:08:11 -0700 Subject: [PATCH] Refactor describe: printing levels and shared kn code (#430) * Refactor describe: printing levels and shared kn code * Test this refactor more * Comments --- pkg/kn/commands/describe.go | 203 ++++++++++++++++++++++++++++ pkg/kn/commands/describe_test.go | 110 +++++++++++++++ pkg/kn/commands/service/describe.go | 168 ++++------------------- pkg/printers/prefixwriter.go | 67 ++++++--- pkg/printers/prefixwriter_test.go | 53 ++++++++ 5 files changed, 437 insertions(+), 164 deletions(-) create mode 100644 pkg/kn/commands/describe.go create mode 100644 pkg/kn/commands/describe_test.go create mode 100644 pkg/printers/prefixwriter_test.go diff --git a/pkg/kn/commands/describe.go b/pkg/kn/commands/describe.go new file mode 100644 index 000000000..ca4b03d27 --- /dev/null +++ b/pkg/kn/commands/describe.go @@ -0,0 +1,203 @@ +// Copyright © 2019 The Knative Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "fmt" + "sort" + "strconv" + "strings" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/duration" + "knative.dev/client/pkg/printers" + "knative.dev/pkg/apis" +) + +// Max length When to truncate long strings (when not "all" mode switched on) +const TruncateAt = 100 + +func WriteMetadata(dw printers.PrefixWriter, m *metav1.ObjectMeta, printDetails bool) { + dw.WriteAttribute("Name", m.Name) + dw.WriteAttribute("Namespace", m.Namespace) + WriteMapDesc(dw, m.Labels, l("Labels"), "", printDetails) + WriteMapDesc(dw, m.Annotations, l("Annotations"), "", printDetails) + dw.WriteAttribute("Age", Age(m.CreationTimestamp.Time)) +} + +var boringDomains = map[string]bool{ + "serving.knative.dev": true, + "client.knative.dev": true, + "kubectl.kubernetes.io": true, +} + +func keyIsBoring(k string) bool { + parts := strings.Split(k, "/") + return len(parts) > 1 && boringDomains[parts[0]] +} + +// Write a map either compact in a single line (possibly truncated) or, if printDetails is set, +// over multiple line, one line per key-value pair. The output is sorted by keys. +func WriteMapDesc(dw printers.PrefixWriter, m map[string]string, label string, labelPrefix string, details bool) { + if len(m) == 0 { + return + } + + var keys []string + for k := range m { + if details || !keyIsBoring(k) { + keys = append(keys, k) + } + } + if len(keys) == 0 { + return + } + sort.Strings(keys) + + if details { + l := labelPrefix + label + + for _, key := range keys { + dw.WriteColsLn(l, key+"="+m[key]) + l = labelPrefix + } + return + } + + dw.WriteColsLn(label, joinAndTruncate(keys, m, TruncateAt-len(label)-2)) +} + +func Age(t time.Time) string { + if t.IsZero() { + return "" + } + return duration.ShortHumanDuration(time.Now().Sub(t)) +} + +func formatConditionType(condition apis.Condition) string { + return string(condition.Type) +} + +// Status in ASCII format +func formatStatus(c apis.Condition) string { + switch c.Status { + case corev1.ConditionTrue: + return "++" + case corev1.ConditionFalse: + switch c.Severity { + case apis.ConditionSeverityError: + return "!!" + case apis.ConditionSeverityWarning: + return " W" + case apis.ConditionSeverityInfo: + return " I" + default: + return " !" + } + default: + return "??" + } +} + +// Used for conditions table to do own formatting for the table, +// as the tabbed writer doesn't work nicely with colors +func getMaxTypeLen(conditions []apis.Condition) int { + max := 0 + for _, condition := range conditions { + if len(condition.Type) > max { + max = len(condition.Type) + } + } + return max +} + +// Sort conditions: Ready first, followed by error, then Warning, then Info +func sortConditions(conditions []apis.Condition) []apis.Condition { + // Don't change the orig slice + ret := make([]apis.Condition, len(conditions)) + for i, c := range conditions { + ret[i] = c + } + sort.SliceStable(ret, func(i, j int) bool { + ic := &ret[i] + jc := &ret[j] + // Ready first + if ic.Type == apis.ConditionReady { + return jc.Type != apis.ConditionReady + } + if jc.Type == apis.ConditionReady { + return false + } + // Among conditions of the same Severity, sort by Type + if ic.Severity == jc.Severity { + return ic.Type < jc.Type + } + // Error < Warning < Info + switch ic.Severity { + case apis.ConditionSeverityError: + return true + case apis.ConditionSeverityWarning: + return jc.Severity == apis.ConditionSeverityInfo + case apis.ConditionSeverityInfo: + return false + default: + return false + } + return false + }) + return ret +} + +// Print out a table with conditions. +func WriteConditions(dw printers.PrefixWriter, conditions []apis.Condition, printMessage bool) { + section := dw.WriteAttribute("Conditions", "") + conditions = sortConditions(conditions) + maxLen := getMaxTypeLen(conditions) + formatHeader := "%-2s %-" + strconv.Itoa(maxLen) + "s %6s %-s\n" + formatRow := "%-2s %-" + strconv.Itoa(maxLen) + "s %6s %-s\n" + section.Writef(formatHeader, "OK", "TYPE", "AGE", "REASON") + for _, condition := range conditions { + ok := formatStatus(condition) + reason := condition.Reason + if printMessage && reason != "" { + reason = fmt.Sprintf("%s (%s)", reason, condition.Message) + } + section.Writef(formatRow, ok, formatConditionType(condition), Age(condition.LastTransitionTime.Inner.Time), reason) + } +} + +// Format label (extracted so that color could be added more easily to all labels) +func l(label string) string { + return label + ":" +} + +// Join to key=value pair, comma separated, and truncate if longer than a limit +func joinAndTruncate(sortedKeys []string, m map[string]string, width int) string { + ret := "" + for _, key := range sortedKeys { + ret += fmt.Sprintf("%s=%s, ", key, m[key]) + if len(ret) > width { + break + } + } + // cut of two latest chars + ret = strings.TrimRight(ret, ", ") + if len(ret) <= width { + return ret + } + return string(ret[:width-4]) + " ..." +} diff --git a/pkg/kn/commands/describe_test.go b/pkg/kn/commands/describe_test.go new file mode 100644 index 000000000..7b5d79d91 --- /dev/null +++ b/pkg/kn/commands/describe_test.go @@ -0,0 +1,110 @@ +// Copyright © 2019 The Knative Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "bytes" + "regexp" + "strconv" + "strings" + "testing" + + "gotest.tools/assert" + "knative.dev/client/pkg/printers" + "knative.dev/pkg/apis" +) + +var testMap = map[string]string{ + "a": "b", + "c": "d", + "foo": "bar", + "serving.knative.dev/funky": "chicken", +} + +func TestWriteMapDesc(t *testing.T) { + buf := &bytes.Buffer{} + dw := printers.NewBarePrefixWriter(buf) + WriteMapDesc(dw, testMap, "eggs", "", false) + assert.Equal(t, buf.String(), "eggs\ta=b, c=d, foo=bar\n") +} + +func TestWriteMapDescDetailed(t *testing.T) { + buf := &bytes.Buffer{} + dw := printers.NewBarePrefixWriter(buf) + WriteMapDesc(dw, testMap, "eggs", "", true) + assert.Equal(t, buf.String(), "eggs\ta=b\n\tc=d\n\tfoo=bar\n\tserving.knative.dev/funky=chicken\n") +} + +func TestWriteMapTruncated(t *testing.T) { + buf := &bytes.Buffer{} + dw := printers.NewBarePrefixWriter(buf) + + items := map[string]string{} + for i := 0; i < 1000; i++ { + items[strconv.Itoa(i)] = strconv.Itoa(i + 1) + } + WriteMapDesc(dw, items, "eggs", "", false) + assert.Assert(t, len(strings.TrimSpace(buf.String())) <= TruncateAt) +} + +var someConditions = []apis.Condition{ + {Type: apis.ConditionReady, Status: "True"}, + {Type: "Aaa", Status: "True"}, + {Type: "Zzz", Status: "False"}, + {Type: "Bbb", Status: "False", Severity: apis.ConditionSeverityWarning, Reason: "Bad"}, + {Type: "Ccc", Status: "False", Severity: apis.ConditionSeverityInfo, Reason: "Eh."}, +} +var permutations = [][]int{ + {0, 1, 2, 3, 4}, + {4, 3, 2, 1, 0}, + {2, 1, 4, 3, 0}, + {2, 1, 0, 3, 4}, +} + +func TestSortConditions(t *testing.T) { + for _, p := range permutations { + permuted := make([]apis.Condition, len(someConditions)) + for i, j := range p { + permuted[i] = someConditions[j] + } + sorted := sortConditions(permuted) + assert.DeepEqual(t, sorted, someConditions) + } +} + +var spaces = regexp.MustCompile("\\s*") + +func normalizeSpace(s string) string { + return spaces.ReplaceAllLiteralString(s, " ") +} + +func TestWriteConditions(t *testing.T) { + for _, p := range permutations { + permuted := make([]apis.Condition, len(someConditions)) + for i, j := range p { + permuted[i] = someConditions[j] + } + buf := &bytes.Buffer{} + dw := printers.NewBarePrefixWriter(buf) + WriteConditions(dw, permuted, false) + assert.Equal(t, normalizeSpace(buf.String()), normalizeSpace(`Conditions: +OK TYPE AGE REASON +++ Ready +++ Aaa +!! Zzz + W Bbb Bad + I Ccc Eh.`)) + } +} diff --git a/pkg/kn/commands/service/describe.go b/pkg/kn/commands/service/describe.go index 2e21e718f..b89318700 100644 --- a/pkg/kn/commands/service/describe.go +++ b/pkg/kn/commands/service/describe.go @@ -33,15 +33,11 @@ import ( client_serving "knative.dev/client/pkg/serving" serving_kn_v1alpha1 "knative.dev/client/pkg/serving/v1alpha1" - "knative.dev/pkg/apis" - duckv1 "knative.dev/pkg/apis/duck/v1" "knative.dev/serving/pkg/apis/serving/v1alpha1" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/util/duration" - "knative.dev/client/pkg/kn/commands" ) @@ -52,9 +48,6 @@ import ( // Whether to print extended information var printDetails bool -// Max length When to truncate long strings (when not "all" mode switched on) -const truncateAt = 100 - // Matching image digest var imageDigestRegexp = regexp.MustCompile(`(?i)sha256:([0-9a-f]{64})`) @@ -190,7 +183,7 @@ func describe(w io.Writer, service *v1alpha1.Service, revisions []*revisionDesc, } // Condition info - writeConditions(dw, service) + commands.WriteConditions(dw, service.Status.Conditions, printDetails) if err := dw.Flush(); err != nil { return err } @@ -200,76 +193,56 @@ func describe(w io.Writer, service *v1alpha1.Service, revisions []*revisionDesc, // Write out main service information. Use colors for major items. func writeService(dw printers.PrefixWriter, service *v1alpha1.Service) { - dw.WriteColsLn(printers.Level0, l("Name"), service.Name) - dw.WriteColsLn(printers.Level0, l("Namespace"), service.Namespace) - dw.WriteColsLn(printers.Level0, l("URL"), extractURL(service)) + commands.WriteMetadata(dw, &service.ObjectMeta, printDetails) + dw.WriteAttribute("URL", extractURL(service)) if service.Status.Address != nil { url := service.Status.Address.GetURL() - dw.WriteColsLn(printers.Level0, l("Address"), url.String()) + dw.WriteAttribute("Address", url.String()) } - writeMapDesc(dw, printers.Level0, service.Labels, l("Labels"), "") - writeMapDesc(dw, printers.Level0, service.Annotations, l("Annotations"), "") - dw.WriteColsLn(printers.Level0, l("Age"), age(service.CreationTimestamp.Time)) } // Write out revisions associated with this service. By default only active // target revisions are printed, but with --all also inactive revisions // created by this services are shown func writeRevisions(dw printers.PrefixWriter, revisions []*revisionDesc, printDetails bool) { - dw.WriteColsLn(printers.Level0, l("Revisions")) + revSection := dw.WriteAttribute("Revisions", "") + dw.Flush() for _, revisionDesc := range revisions { - dw.WriteColsLn(printers.Level1, formatBullet(revisionDesc.percent, revisionDesc.ready), revisionHeader(revisionDesc)) + section := revSection.WriteColsLn(formatBullet(revisionDesc.percent, revisionDesc.ready), revisionHeader(revisionDesc)) if revisionDesc.ready == v1.ConditionFalse { - dw.WriteColsLn(printers.Level1, "", l("Error"), revisionDesc.reason) + section.WriteAttribute("Error", revisionDesc.reason) } - dw.WriteColsLn(printers.Level1, "", l("Image"), getImageDesc(revisionDesc)) + section.WriteAttribute("Image", getImageDesc(revisionDesc)) if printDetails { if revisionDesc.port != nil { - dw.WriteColsLn(printers.Level1, "", l("Port"), strconv.FormatInt(int64(*revisionDesc.port), 10)) + section.WriteAttribute("Port", strconv.FormatInt(int64(*revisionDesc.port), 10)) } - writeSliceDesc(dw, printers.Level1, revisionDesc.env, l("Env"), "\t") + writeSliceDesc(section, revisionDesc.env, l("Env"), "") // Scale spec if given if revisionDesc.maxScale != nil || revisionDesc.minScale != nil { - dw.WriteColsLn(printers.Level1, "", l("Scale"), formatScale(revisionDesc.minScale, revisionDesc.maxScale)) + section.WriteAttribute("Scale", formatScale(revisionDesc.minScale, revisionDesc.maxScale)) } // Concurrency specs if given if revisionDesc.concurrencyLimit != nil || revisionDesc.concurrencyTarget != nil { - writeConcurrencyOptions(dw, revisionDesc) + writeConcurrencyOptions(section, revisionDesc) } // Resources if given - writeResources(dw, "Memory", revisionDesc.requestsMemory, revisionDesc.limitsMemory) - writeResources(dw, "CPU", revisionDesc.requestsCPU, revisionDesc.limitsCPU) + writeResources(section, "Memory", revisionDesc.requestsMemory, revisionDesc.limitsMemory) + writeResources(section, "CPU", revisionDesc.requestsCPU, revisionDesc.limitsCPU) } } } -// Print out a table with conditions. Use green for 'ok', and red for 'nok' if color is enabled -func writeConditions(dw printers.PrefixWriter, service *v1alpha1.Service) { - dw.WriteColsLn(printers.Level0, l("Conditions")) - maxLen := getMaxTypeLen(service.Status.Conditions) - formatHeader := "%-2s %-" + strconv.Itoa(maxLen) + "s %6s %-s\n" - formatRow := "%-2s %-" + strconv.Itoa(maxLen) + "s %6s %-s\n" - dw.Write(printers.Level1, formatHeader, "OK", "TYPE", "AGE", "REASON") - for _, condition := range service.Status.Conditions { - ok := formatStatus(condition.Status) - reason := condition.Reason - if printDetails && reason != "" { - reason = fmt.Sprintf("%s (%s)", reason, condition.Message) - } - dw.Write(printers.Level1, formatRow, ok, formatConditionType(condition), age(condition.LastTransitionTime.Inner.Time), reason) - } -} - func writeConcurrencyOptions(dw printers.PrefixWriter, desc *revisionDesc) { - dw.WriteColsLn(printers.Level2, "", l("Concurrency")) + section := dw.WriteAttribute("Concurrency", "") if desc.concurrencyLimit != nil { - dw.WriteColsLn(printers.Level3, "", "", l("Limit"), strconv.FormatInt(*desc.concurrencyLimit, 10)) + section.WriteAttribute("Limit", strconv.FormatInt(*desc.concurrencyLimit, 10)) } if desc.concurrencyTarget != nil { - dw.WriteColsLn(printers.Level3, "", "", l("Target"), strconv.Itoa(*desc.concurrencyTarget)) + section.WriteAttribute("Target", strconv.Itoa(*desc.concurrencyTarget)) } } @@ -314,36 +287,7 @@ func revisionHeader(desc *revisionDesc) string { return header + " " + "[" + strconv.Itoa(desc.configurationGeneration) + "]" + " " + - "(" + age(desc.creationTimestamp) + ")" -} - -// Used for conditions table to do own formatting for the table, -// as the tabbed writer doesn't work nicely with colors -func getMaxTypeLen(conditions duckv1.Conditions) int { - max := 0 - for _, condition := range conditions { - if len(condition.Type) > max { - max = len(condition.Type) - } - } - return max -} - -// Color the type of the conditions -func formatConditionType(condition apis.Condition) string { - return string(condition.Type) -} - -// Status in ASCII format -func formatStatus(status corev1.ConditionStatus) string { - switch status { - case v1.ConditionTrue: - return "++" - case v1.ConditionFalse: - return "!!" - default: - return "??" - } + "(" + commands.Age(desc.creationTimestamp) + ")" } // Return either image name with tag or together with its resolved digest @@ -376,47 +320,9 @@ func shortenDigest(digest string) string { return digest } -var boringDomains = map[string]bool{ - "serving.knative.dev": true, - "client.knative.dev": true, - "kubectl.kubernetes.io": true, -} - -// Write a map either compact in a single line (possibly truncated) or, if printDetails is set, -// over multiple line, one line per key-value pair. The output is sorted by keys. -func writeMapDesc(dw printers.PrefixWriter, indent int, m map[string]string, label string, labelPrefix string) { - if len(m) == 0 { - return - } - - var keys []string - for k := range m { - parts := strings.Split(k, "/") - if printDetails || len(parts) <= 1 || !boringDomains[parts[0]] { - keys = append(keys, k) - } - } - if len(keys) == 0 { - return - } - sort.Strings(keys) - - if printDetails { - l := labelPrefix + label - - for _, key := range keys { - dw.WriteColsLn(indent, l, key+"="+m[key]) - l = labelPrefix - } - return - } - - dw.WriteColsLn(indent, label, joinAndTruncate(keys, m)) -} - // Writer a slice compact (printDetails == false) in one line, or over multiple line // with key-value line-by-line (printDetails == true) -func writeSliceDesc(dw printers.PrefixWriter, indent int, s []string, label string, labelPrefix string) { +func writeSliceDesc(dw printers.PrefixWriter, s []string, label string, labelPrefix string) { if len(s) == 0 { return @@ -425,17 +331,17 @@ func writeSliceDesc(dw printers.PrefixWriter, indent int, s []string, label stri if printDetails { l := labelPrefix + label for _, value := range s { - dw.WriteColsLn(indent, l, value) + dw.WriteColsLn(l, value) l = labelPrefix } return } joined := strings.Join(s, ", ") - if len(joined) > truncateAt { - joined = joined[:truncateAt-4] + " ..." + if len(joined) > commands.TruncateAt { + joined = joined[:commands.TruncateAt-4] + " ..." } - dw.WriteColsLn(indent, labelPrefix+label, joined) + dw.WriteAttribute(labelPrefix+label, joined) } // Write request ... limits or only one of them @@ -453,24 +359,7 @@ func writeResources(dw printers.PrefixWriter, label string, request string, limi return } - dw.WriteColsLn(printers.Level2, "", l(label), value) -} - -// Join to key=value pair, comma separated, and truncate if longer than a limit -func joinAndTruncate(sortedKeys []string, m map[string]string) string { - ret := "" - for _, key := range sortedKeys { - ret += fmt.Sprintf("%s=%s, ", key, m[key]) - if len(ret) > truncateAt { - break - } - } - // cut of two latest chars - ret = strings.TrimRight(ret, ", ") - if len(ret) <= truncateAt { - return ret - } - return string(ret[:truncateAt-4]) + " ..." + dw.WriteAttribute(label, value) } // Format target percentage that it fits in the revision table @@ -492,13 +381,6 @@ func formatBullet(percentage int64, status corev1.ConditionStatus) string { return fmt.Sprintf("%3d%s", percentage, symbol) } -func age(t time.Time) string { - if t.IsZero() { - return "" - } - return duration.ShortHumanDuration(time.Now().Sub(t)) -} - // Call the backend to query revisions for the given service and build up // the view objects used for output func getRevisionDescriptions(client serving_kn_v1alpha1.KnServingClient, service *v1alpha1.Service, withDetails bool) ([]*revisionDesc, error) { diff --git a/pkg/printers/prefixwriter.go b/pkg/printers/prefixwriter.go index fa0c2093c..440ab5212 100644 --- a/pkg/printers/prefixwriter.go +++ b/pkg/printers/prefixwriter.go @@ -25,51 +25,60 @@ type flusher interface { Flush() error } +func NewBarePrefixWriter(out io.Writer) PrefixWriter { + return &prefixWriter{out: out, nested: nil, colIndent: 0, spaceIndent: 0} +} + // NewPrefixWriter creates a new PrefixWriter. func NewPrefixWriter(out io.Writer) PrefixWriter { tabWriter := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0) - return &prefixWriter{out: tabWriter} + return &prefixWriter{out: tabWriter, nested: nil, colIndent: 0, spaceIndent: 0} } // PrefixWriter can write text at various indentation levels. type PrefixWriter interface { // Write writes text with the specified indentation level. - Write(level int, format string, a ...interface{}) + Writef(format string, a ...interface{}) // WriteLine writes an entire line with no indentation level. WriteLine(a ...interface{}) // Write columns with an initial indentation - WriteCols(level int, cols ...string) + WriteCols(cols ...string) PrefixWriter // Write columns with an initial indentation and a newline at the end - WriteColsLn(level int, cols ...string) + WriteColsLn(cols ...string) PrefixWriter // Flush forces indentation to be reset. Flush() error + + WriteAttribute(attr, value string) PrefixWriter } // prefixWriter implements PrefixWriter type prefixWriter struct { - out io.Writer + out io.Writer + nested PrefixWriter + colIndent int + spaceIndent int } var _ PrefixWriter = &prefixWriter{} -// Each level has 2 spaces for PrefixWriter -const ( - Level0 = iota - Level1 - Level2 - Level3 -) - -func (pw *prefixWriter) Write(level int, format string, a ...interface{}) { - levelSpace := " " +func (pw *prefixWriter) Writef(format string, a ...interface{}) { prefix := "" - for i := 0; i < level; i++ { + levelSpace := " " + for i := 0; i < pw.spaceIndent; i++ { prefix += levelSpace } - fmt.Fprintf(pw.out, prefix+format, a...) + levelTab := "\t" + for i := 0; i < pw.colIndent; i++ { + prefix += levelTab + } + if pw.nested != nil { + pw.nested.Writef(prefix+format, a...) + } else { + fmt.Fprintf(pw.out, prefix+format, a...) + } } -func (pw *prefixWriter) WriteCols(level int, cols ...string) { +func (pw *prefixWriter) WriteCols(cols ...string) PrefixWriter { ss := make([]string, len(cols)) for i := range cols { ss[i] = "%s" @@ -80,21 +89,37 @@ func (pw *prefixWriter) WriteCols(level int, cols ...string) { s[i] = v } - pw.Write(level, format, s...) + pw.Writef(format, s...) + return &prefixWriter{pw.out, pw, 1, 0} } -func (pw *prefixWriter) WriteColsLn(level int, cols ...string) { - pw.WriteCols(level, cols...) +// WriteCols writes the columns to the writer and returns a PrefixWriter for +// writing any further parts of the "record" in the last column. +func (pw *prefixWriter) WriteColsLn(cols ...string) PrefixWriter { + ret := pw.WriteCols(cols...) pw.WriteLine() + return ret } func (pw *prefixWriter) WriteLine(a ...interface{}) { fmt.Fprintln(pw.out, a...) } +// WriteAttribute writes the attr (as a label) with the given value and returns +// a PrefixWriter for writing any subattributes. +func (pw *prefixWriter) WriteAttribute(attr, value string) PrefixWriter { + pw.WriteColsLn(l(attr), value) + return &prefixWriter{pw.out, pw, 0, 1} +} + func (pw *prefixWriter) Flush() error { if f, ok := pw.out.(flusher); ok { return f.Flush() } return fmt.Errorf("output stream %v doesn't support Flush", pw.out) } + +// Format label (extracted so that color could be added more easily to all labels) +func l(label string) string { + return label + ":" +} diff --git a/pkg/printers/prefixwriter_test.go b/pkg/printers/prefixwriter_test.go new file mode 100644 index 000000000..c63b9aacc --- /dev/null +++ b/pkg/printers/prefixwriter_test.go @@ -0,0 +1,53 @@ +// Copyright © 2019 The Knative Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package printers + +import ( + "bytes" + "testing" + + "gotest.tools/assert" +) + +func TestWriteColsLn(t *testing.T) { + buf := &bytes.Buffer{} + w := NewBarePrefixWriter(buf) + sub := w.WriteColsLn("a", "bbbb", "c", "ddd") + sub.WriteColsLn("B", "C", "D", "E") + expected := "a\tbbbb\tc\tddd\n\tB\tC\tD\tE\n" + actual := buf.String() + assert.Equal(t, actual, expected) +} + +func TestWriteAttribute(t *testing.T) { + buf := &bytes.Buffer{} + w := NewBarePrefixWriter(buf) + sub := w.WriteAttribute("Thing", "Stuff") + sub.WriteColsLn("B", "C", "D", "E") + expected := "Thing:\tStuff\n B\tC\tD\tE\n" + actual := buf.String() + assert.Equal(t, actual, expected) +} + +func TestWriteNested(t *testing.T) { + buf := &bytes.Buffer{} + w := NewBarePrefixWriter(buf) + sub := w.WriteColsLn("*", "Header") + subsub := sub.WriteAttribute("Thing", "stuff") + subsub.WriteAttribute("Subthing", "substuff") + expected := "*\tHeader\n\tThing:\tstuff\n\t Subthing:\tsubstuff\n" + actual := buf.String() + assert.Equal(t, actual, expected) +}