Refactor describe: printing levels and shared kn code (#430)

* Refactor describe: printing levels and shared kn code

* Test this refactor more

* Comments
This commit is contained in:
Naomi Seyfer 2019-10-29 03:08:11 -07:00 committed by Knative Prow Robot
parent b871112e1f
commit aafdb9a238
5 changed files with 437 additions and 164 deletions

203
pkg/kn/commands/describe.go Normal file
View File

@ -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]) + " ..."
}

View File

@ -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.`))
}
}

View File

@ -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) {

View File

@ -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 + ":"
}

View File

@ -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)
}