mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
c750695d83
This changes makes it so that we output the empty string for Format when there is no data. It turns out there are many places in the code where we have such handling, but in a way that would break the JSON formatter (since we'd output nothing on stdout or text rather than `[]`/`null`).
509 lines
13 KiB
Go
509 lines
13 KiB
Go
package cliui_test
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/cli/cliui"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
type stringWrapper struct {
|
|
str string
|
|
}
|
|
|
|
var _ fmt.Stringer = stringWrapper{}
|
|
|
|
func (s stringWrapper) String() string {
|
|
return s.str
|
|
}
|
|
|
|
type myString string
|
|
|
|
type tableTest1 struct {
|
|
Name string `table:"name,default_sort"`
|
|
AltName *stringWrapper `table:"alt_name"`
|
|
NotIncluded string // no table tag
|
|
Age int `table:"age"`
|
|
Roles []string `table:"roles"`
|
|
Sub1 tableTest2 `table:"sub_1,recursive"`
|
|
Sub2 *tableTest2 `table:"sub_2,recursive"`
|
|
Sub3 tableTest3 `table:"sub 3,recursive"`
|
|
Sub4 tableTest2 `table:"sub 4"` // not recursive
|
|
|
|
// Types with special formatting.
|
|
Time time.Time `table:"time"`
|
|
TimePtr *time.Time `table:"time_ptr"`
|
|
NullTime codersdk.NullTime `table:"null_time"`
|
|
MyString *myString `table:"my_string"`
|
|
}
|
|
|
|
type tableTest2 struct {
|
|
Name stringWrapper `table:"name,default_sort"`
|
|
Age int `table:"age"`
|
|
NotIncluded string `table:"-"`
|
|
}
|
|
|
|
type tableTest3 struct {
|
|
NotIncluded string // no table tag
|
|
Sub tableTest2 `table:"inner,recursive"`
|
|
}
|
|
|
|
type tableTest4 struct {
|
|
Inline tableTest2 `table:"ignored,recursive_inline"`
|
|
SortField string `table:"sort_field"`
|
|
}
|
|
|
|
func Test_DisplayTable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
someTime := time.Date(2022, 8, 2, 15, 49, 10, 0, time.UTC)
|
|
myStr := myString("my string")
|
|
|
|
// Not sorted by name or age to test sorting.
|
|
in := []tableTest1{
|
|
{
|
|
Name: "bar",
|
|
AltName: &stringWrapper{str: "bar alt"},
|
|
Age: 20,
|
|
Roles: []string{"a"},
|
|
Sub1: tableTest2{
|
|
Name: stringWrapper{str: "bar1"},
|
|
Age: 21,
|
|
},
|
|
Sub2: nil,
|
|
Sub3: tableTest3{
|
|
Sub: tableTest2{
|
|
Name: stringWrapper{str: "bar3"},
|
|
Age: 23,
|
|
},
|
|
},
|
|
Sub4: tableTest2{
|
|
Name: stringWrapper{str: "bar4"},
|
|
Age: 24,
|
|
},
|
|
Time: someTime,
|
|
TimePtr: nil,
|
|
NullTime: codersdk.NullTime{
|
|
NullTime: sql.NullTime{
|
|
Time: someTime,
|
|
Valid: true,
|
|
},
|
|
},
|
|
MyString: &myStr,
|
|
},
|
|
{
|
|
Name: "foo",
|
|
Age: 10,
|
|
Roles: []string{"a", "b", "c"},
|
|
Sub1: tableTest2{
|
|
Name: stringWrapper{str: "foo1"},
|
|
Age: 11,
|
|
},
|
|
Sub2: &tableTest2{
|
|
Name: stringWrapper{str: "foo2"},
|
|
Age: 12,
|
|
},
|
|
Sub3: tableTest3{
|
|
Sub: tableTest2{
|
|
Name: stringWrapper{str: "foo3"},
|
|
Age: 13,
|
|
},
|
|
},
|
|
Sub4: tableTest2{
|
|
Name: stringWrapper{str: "foo4"},
|
|
Age: 14,
|
|
},
|
|
Time: someTime,
|
|
TimePtr: &someTime,
|
|
},
|
|
{
|
|
Name: "baz",
|
|
Age: 30,
|
|
Roles: nil,
|
|
Sub1: tableTest2{
|
|
Name: stringWrapper{str: "baz1"},
|
|
Age: 31,
|
|
},
|
|
Sub2: nil,
|
|
Sub3: tableTest3{
|
|
Sub: tableTest2{
|
|
Name: stringWrapper{str: "baz3"},
|
|
Age: 33,
|
|
},
|
|
},
|
|
Sub4: tableTest2{
|
|
Name: stringWrapper{str: "baz4"},
|
|
Age: 34,
|
|
},
|
|
Time: someTime,
|
|
TimePtr: nil,
|
|
},
|
|
}
|
|
|
|
// This test tests skipping fields without table tags, recursion, pointer
|
|
// dereferencing, and nil pointer skipping.
|
|
t.Run("OK", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
expected := `
|
|
NAME ALT NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR NULL TIME MY STRING
|
|
bar bar alt 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil> 2022-08-02T15:49:10Z my string
|
|
baz <nil> 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil> <nil> <nil>
|
|
foo <nil> 10 [a, b, c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z <nil> <nil>
|
|
`
|
|
|
|
// Test with non-pointer values.
|
|
out, err := cliui.DisplayTable(in, "", nil)
|
|
log.Println("rendered table:\n" + out)
|
|
require.NoError(t, err)
|
|
compareTables(t, expected, out)
|
|
|
|
// Test with pointer values.
|
|
inPtr := make([]*tableTest1, len(in))
|
|
for i, v := range in {
|
|
inPtr[i] = &v
|
|
}
|
|
out, err = cliui.DisplayTable(inPtr, "", nil)
|
|
require.NoError(t, err)
|
|
compareTables(t, expected, out)
|
|
})
|
|
|
|
t.Run("CustomSort", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
expected := `
|
|
NAME ALT NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR NULL TIME MY STRING
|
|
foo <nil> 10 [a, b, c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z <nil> <nil>
|
|
bar bar alt 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil> 2022-08-02T15:49:10Z my string
|
|
baz <nil> 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil> <nil> <nil>
|
|
`
|
|
|
|
out, err := cliui.DisplayTable(in, "age", nil)
|
|
log.Println("rendered table:\n" + out)
|
|
require.NoError(t, err)
|
|
compareTables(t, expected, out)
|
|
})
|
|
|
|
t.Run("Filter", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
expected := `
|
|
NAME SUB 1 NAME SUB 3 INNER NAME TIME
|
|
bar bar1 bar3 2022-08-02T15:49:10Z
|
|
baz baz1 baz3 2022-08-02T15:49:10Z
|
|
foo foo1 foo3 2022-08-02T15:49:10Z
|
|
`
|
|
|
|
out, err := cliui.DisplayTable(in, "", []string{"name", "sub_1_name", "sub_3 inner name", "time"})
|
|
log.Println("rendered table:\n" + out)
|
|
require.NoError(t, err)
|
|
compareTables(t, expected, out)
|
|
})
|
|
|
|
t.Run("Inline", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
expected := `
|
|
NAME AGE
|
|
Alice 25
|
|
`
|
|
|
|
inlineIn := []tableTest4{
|
|
{
|
|
Inline: tableTest2{
|
|
Name: stringWrapper{
|
|
str: "Alice",
|
|
},
|
|
Age: 25,
|
|
NotIncluded: "IgnoreMe",
|
|
},
|
|
},
|
|
}
|
|
out, err := cliui.DisplayTable(inlineIn, "", []string{"name", "age"})
|
|
log.Println("rendered table:\n" + out)
|
|
require.NoError(t, err)
|
|
compareTables(t, expected, out)
|
|
})
|
|
|
|
// This test ensures we can display dynamically typed slices
|
|
t.Run("Interfaces", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
in := []any{tableTest1{}}
|
|
out, err := cliui.DisplayTable(in, "", nil)
|
|
t.Log("rendered table:\n" + out)
|
|
require.NoError(t, err)
|
|
other := []tableTest1{{}}
|
|
expected, err := cliui.DisplayTable(other, "", nil)
|
|
require.NoError(t, err)
|
|
compareTables(t, expected, out)
|
|
})
|
|
|
|
t.Run("WithSeparator", func(t *testing.T) {
|
|
t.Parallel()
|
|
expected := `
|
|
NAME ALT NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR NULL TIME MY STRING
|
|
bar bar alt 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil> 2022-08-02T15:49:10Z my string
|
|
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
baz <nil> 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil> <nil> <nil>
|
|
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
foo <nil> 10 [a, b, c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z <nil> <nil>
|
|
`
|
|
|
|
var inlineIn []any
|
|
for _, v := range in {
|
|
inlineIn = append(inlineIn, v)
|
|
inlineIn = append(inlineIn, cliui.TableSeparator{})
|
|
}
|
|
out, err := cliui.DisplayTable(inlineIn, "", nil)
|
|
t.Log("rendered table:\n" + out)
|
|
require.NoError(t, err)
|
|
compareTables(t, expected, out)
|
|
})
|
|
|
|
// This test ensures that safeties against invalid use of `table` tags
|
|
// causes errors (even without data).
|
|
t.Run("Errors", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("NotSlice", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var in string
|
|
_, err := cliui.DisplayTable(in, "", nil)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("BadSortColumn", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := cliui.DisplayTable(in, "bad_column_does_not_exist", nil)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("BadFilterColumns", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := cliui.DisplayTable(in, "", []string{"name", "bad_column_does_not_exist"})
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("Interfaces", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("WithoutData", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var in []any
|
|
_, err := cliui.DisplayTable(in, "", nil)
|
|
require.Error(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("NotStruct", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("WithoutData", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var in []string
|
|
_, err := cliui.DisplayTable(in, "", nil)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("WithData", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
in := []string{"foo", "bar", "baz"}
|
|
_, err := cliui.DisplayTable(in, "", nil)
|
|
require.Error(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("NoTableTags", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type noTableTagsTest struct {
|
|
Field string `json:"field"`
|
|
}
|
|
|
|
t.Run("WithoutData", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var in []noTableTagsTest
|
|
_, err := cliui.DisplayTable(in, "", nil)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("WithData", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
in := []noTableTagsTest{{Field: "hi"}}
|
|
_, err := cliui.DisplayTable(in, "", nil)
|
|
require.Error(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("InvalidTag/NoName", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type noNameTest struct {
|
|
Field string `table:""`
|
|
}
|
|
|
|
t.Run("WithoutData", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var in []noNameTest
|
|
_, err := cliui.DisplayTable(in, "", nil)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("WithData", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
in := []noNameTest{{Field: "test"}}
|
|
_, err := cliui.DisplayTable(in, "", nil)
|
|
require.Error(t, err)
|
|
})
|
|
})
|
|
|
|
t.Run("InvalidTag/BadSyntax", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type invalidSyntaxTest struct {
|
|
Field string `table:"asda,asdjada"`
|
|
}
|
|
|
|
t.Run("WithoutData", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var in []invalidSyntaxTest
|
|
_, err := cliui.DisplayTable(in, "", nil)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("WithData", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
in := []invalidSyntaxTest{{Field: "test"}}
|
|
_, err := cliui.DisplayTable(in, "", nil)
|
|
require.Error(t, err)
|
|
})
|
|
})
|
|
})
|
|
|
|
t.Run("EmptyNil", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type emptyNilTest struct {
|
|
Name string `table:"name,default_sort"`
|
|
EmptyOnNil *string `table:"empty_on_nil,empty_nil"`
|
|
NormalBehavior *string `table:"normal_behavior"`
|
|
}
|
|
|
|
value := "value"
|
|
in := []emptyNilTest{
|
|
{
|
|
Name: "has_value",
|
|
EmptyOnNil: &value,
|
|
NormalBehavior: &value,
|
|
},
|
|
{
|
|
Name: "has_nil",
|
|
EmptyOnNil: nil,
|
|
NormalBehavior: nil,
|
|
},
|
|
}
|
|
|
|
expected := `
|
|
NAME EMPTY ON NIL NORMAL BEHAVIOR
|
|
has_nil <nil>
|
|
has_value value value
|
|
`
|
|
|
|
out, err := cliui.DisplayTable(in, "", nil)
|
|
log.Println("rendered table:\n" + out)
|
|
require.NoError(t, err)
|
|
compareTables(t, expected, out)
|
|
})
|
|
|
|
t.Run("EmptyNilWithRecursiveInline", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type nestedData struct {
|
|
Name string `table:"name"`
|
|
}
|
|
|
|
type inlineTest struct {
|
|
Nested *nestedData `table:"ignored,recursive_inline,empty_nil"`
|
|
Count int `table:"count,default_sort"`
|
|
}
|
|
|
|
in := []inlineTest{
|
|
{
|
|
Nested: &nestedData{
|
|
Name: "alice",
|
|
},
|
|
Count: 1,
|
|
},
|
|
{
|
|
Nested: nil,
|
|
Count: 2,
|
|
},
|
|
}
|
|
|
|
expected := `
|
|
NAME COUNT
|
|
alice 1
|
|
2
|
|
`
|
|
|
|
out, err := cliui.DisplayTable(in, "", nil)
|
|
log.Println("rendered table:\n" + out)
|
|
require.NoError(t, err)
|
|
compareTables(t, expected, out)
|
|
})
|
|
|
|
t.Run("Empty", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var in []tableTest4
|
|
out, err := cliui.DisplayTable(in, "", nil)
|
|
require.NoError(t, err)
|
|
require.Empty(t, out)
|
|
})
|
|
}
|
|
|
|
// compareTables normalizes the incoming table lines
|
|
func compareTables(t *testing.T, expected, out string) {
|
|
t.Helper()
|
|
|
|
expectedLines := strings.Split(strings.TrimSpace(expected), "\n")
|
|
gotLines := strings.Split(strings.TrimSpace(out), "\n")
|
|
assert.Equal(t, len(expectedLines), len(gotLines), "expected line count does not match generated line count")
|
|
|
|
// Map the expected and got lines to normalize them.
|
|
expectedNormalized := make([]string, len(expectedLines))
|
|
gotNormalized := make([]string, len(gotLines))
|
|
normalizeLine := func(s string) string {
|
|
return strings.Join(strings.Fields(strings.TrimSpace(s)), " ")
|
|
}
|
|
for i, s := range expectedLines {
|
|
expectedNormalized[i] = normalizeLine(s)
|
|
}
|
|
for i, s := range gotLines {
|
|
gotNormalized[i] = normalizeLine(s)
|
|
}
|
|
|
|
require.Equal(t, expectedNormalized, gotNormalized, "expected lines to match generated lines")
|
|
}
|