forked from Gitlink/gitlink-cli
369 lines
12 KiB
Go
369 lines
12 KiB
Go
package output
|
||
|
||
import (
|
||
"bytes"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func TestPrintTable_NestedSliceRendersAsTable(t *testing.T) {
|
||
data := map[string]interface{}{
|
||
"tags": []interface{}{
|
||
map[string]interface{}{"name": "v1", "id": "a"},
|
||
map[string]interface{}{"name": "v2", "id": "b"},
|
||
},
|
||
"total_count": 2,
|
||
}
|
||
env := SuccessEnvelope(data, nil)
|
||
var buf bytes.Buffer
|
||
if err := PrintTo(&buf, env, "table"); err != nil {
|
||
t.Fatalf("PrintTo returned error: %v", err)
|
||
}
|
||
out := buf.String()
|
||
for _, want := range []string{"name", "v1", "v2", "id"} {
|
||
if !strings.Contains(out, want) {
|
||
t.Errorf("expected output to contain %q, got:\n%s", want, out)
|
||
}
|
||
}
|
||
// Must NOT start with JSON envelope.
|
||
if strings.HasPrefix(strings.TrimSpace(out), "{") {
|
||
t.Errorf("expected table output, not JSON; got:\n%s", out)
|
||
}
|
||
}
|
||
|
||
// TestPrintTable_NestedMapRendersAsDetailTable 验证含嵌套 map 的单对象详情
|
||
// 现在渲染为易读的 KEY/VALUE 表格(嵌套字段用缩进展开),而非回退 JSON。
|
||
func TestPrintTable_NestedMapRendersAsDetailTable(t *testing.T) {
|
||
data := map[string]interface{}{
|
||
"name": "v1.0.0",
|
||
"commit": map[string]interface{}{"sha": "abc123", "message": "release"},
|
||
"tagger": map[string]interface{}{"login": "alice", "name": "Alice"},
|
||
}
|
||
env := SuccessEnvelope(data, nil)
|
||
var buf bytes.Buffer
|
||
if err := PrintTo(&buf, env, "table"); err != nil {
|
||
t.Fatalf("PrintTo returned error: %v", err)
|
||
}
|
||
out := buf.String()
|
||
// Should be a key/value table, NOT JSON.
|
||
if strings.HasPrefix(strings.TrimSpace(out), "{") {
|
||
t.Errorf("expected table output for nested map, not JSON; got:\n%s", out)
|
||
}
|
||
// Top-level key present.
|
||
for _, want := range []string{"name", "v1.0.0", "KEY", "VALUE"} {
|
||
if !strings.Contains(out, want) {
|
||
t.Errorf("expected output to contain %q; got:\n%s", want, out)
|
||
}
|
||
}
|
||
// Nested fields should be expanded inline (not collapsed to <object>).
|
||
for _, want := range []string{"sha", "abc123", "login", "alice"} {
|
||
if !strings.Contains(out, want) {
|
||
t.Errorf("expected nested field %q to be expanded; got:\n%s", want, out)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestPrintTable_SimpleMapRendersAsMapTable(t *testing.T) {
|
||
data := map[string]interface{}{
|
||
"name": "x",
|
||
"count": 5,
|
||
}
|
||
env := SuccessEnvelope(data, nil)
|
||
var buf bytes.Buffer
|
||
if err := PrintTo(&buf, env, "table"); err != nil {
|
||
t.Fatalf("PrintTo returned error: %v", err)
|
||
}
|
||
out := buf.String()
|
||
if !strings.Contains(out, "KEY") || !strings.Contains(out, "VALUE") {
|
||
t.Errorf("expected KEY/VALUE map table; got:\n%s", out)
|
||
}
|
||
}
|
||
|
||
// TestPrintTable_NativeMapStringSlice 验证本地构造的 []map[string]string
|
||
// (如 wiki +list 的 pages)也能渲染为表格,而非折叠成 "[N items]"。
|
||
// 回归测试:之前 findLargestSlice 只认 []interface{},导致这类列表被当
|
||
// 单对象详情的 slice 字段,显示为 "[2 items]"。
|
||
func TestPrintTable_NativeMapStringSlice(t *testing.T) {
|
||
data := map[string]interface{}{
|
||
"total_count": 2,
|
||
"pages": []map[string]string{
|
||
{"name": "Home"},
|
||
{"name": "About"},
|
||
},
|
||
}
|
||
env := SuccessEnvelope(data, nil)
|
||
var buf bytes.Buffer
|
||
if err := PrintTo(&buf, env, "table"); err != nil {
|
||
t.Fatalf("PrintTo returned error: %v", err)
|
||
}
|
||
out := buf.String()
|
||
if strings.Contains(out, "[2 items]") {
|
||
t.Errorf("native map slice should render as table, not [N items]; got:\n%s", out)
|
||
}
|
||
for _, want := range []string{"name", "Home", "About"} {
|
||
if !strings.Contains(out, want) {
|
||
t.Errorf("expected output to contain %q; got:\n%s", want, out)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestPrintTable_ErrorEnvelope(t *testing.T) {
|
||
env := ErrorEnvelope("E_FAIL", "boom", "")
|
||
var buf bytes.Buffer
|
||
if err := PrintTo(&buf, env, "table"); err != nil {
|
||
t.Fatalf("PrintTo returned error: %v", err)
|
||
}
|
||
out := buf.String()
|
||
if !strings.Contains(out, "Error:") {
|
||
t.Errorf("expected output to contain \"Error:\"; got:\n%s", out)
|
||
}
|
||
if !strings.Contains(out, "boom") {
|
||
t.Errorf("expected output to contain error message \"boom\"; got:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestPrintTable_PicksLargestSlice(t *testing.T) {
|
||
data := map[string]interface{}{
|
||
"small": []interface{}{
|
||
map[string]interface{}{"a": 1},
|
||
},
|
||
"big": []interface{}{
|
||
map[string]interface{}{"x": 1},
|
||
map[string]interface{}{"x": 2},
|
||
map[string]interface{}{"x": 3},
|
||
},
|
||
}
|
||
env := SuccessEnvelope(data, nil)
|
||
var buf bytes.Buffer
|
||
if err := PrintTo(&buf, env, "table"); err != nil {
|
||
t.Fatalf("PrintTo returned error: %v", err)
|
||
}
|
||
out := buf.String()
|
||
// Must not be a JSON envelope fallback.
|
||
if strings.HasPrefix(strings.TrimSpace(out), "{") {
|
||
t.Errorf("expected table output, not JSON; got:\n%s", out)
|
||
}
|
||
if !strings.Contains(out, "x") {
|
||
t.Errorf("expected output to render \"big\" list with x column; got:\n%s", out)
|
||
}
|
||
for _, want := range []string{"1", "2", "3"} {
|
||
if !strings.Contains(out, want) {
|
||
t.Errorf("expected output to contain row value %q; got:\n%s", want, out)
|
||
}
|
||
}
|
||
lines := strings.Split(strings.TrimSpace(out), "\n")
|
||
// header + dash line + 3 rows = 5 lines minimum
|
||
if len(lines) < 5 {
|
||
t.Errorf("expected at least 5 lines (header/dash/3 rows); got %d:\n%s", len(lines), out)
|
||
}
|
||
// The "a" column from the smaller slice must not appear as a header.
|
||
for _, line := range lines {
|
||
// header line is tab-separated; ensure "a" is not one of the columns.
|
||
cols := strings.Split(line, "\t")
|
||
for _, c := range cols {
|
||
if strings.TrimSpace(c) == "a" {
|
||
t.Errorf("did not expect \"a\" column from smaller slice; got line: %q", line)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Readability improvements (truncation, column limits, nested-map summaries) ---
|
||
|
||
func TestFormatValue_TruncatesLongString(t *testing.T) {
|
||
long := strings.Repeat("a", 100)
|
||
got := formatValue(long)
|
||
if len(got) > 40 {
|
||
t.Errorf("expected output <= 40 chars, got %d: %q", len(got), got)
|
||
}
|
||
if !strings.HasSuffix(got, "...") {
|
||
t.Errorf("expected output to end with \"...\", got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestFormatValue_ShortStringUntouched(t *testing.T) {
|
||
got := formatValue("hello")
|
||
if got != "hello" {
|
||
t.Errorf("expected short string unchanged, got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestFormatValue_NestedMapShowsSummary(t *testing.T) {
|
||
m := map[string]interface{}{
|
||
"login": "alice",
|
||
"id": 123,
|
||
"image_url": "http://example.com/avatar.png",
|
||
}
|
||
got := formatValue(m)
|
||
if !strings.Contains(got, "alice") {
|
||
t.Errorf("expected summary to contain \"alice\", got %q", got)
|
||
}
|
||
if strings.Contains(got, "image_url") {
|
||
t.Errorf("expected summary to omit noisy field \"image_url\", got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestFormatValue_NestedMapWithoutKnownFields(t *testing.T) {
|
||
m := map[string]interface{}{
|
||
"foo": "bar",
|
||
"baz": 42,
|
||
}
|
||
got := formatValue(m)
|
||
// No recognized key -> should be a short placeholder, not a JSON blob.
|
||
if strings.HasPrefix(got, "{") {
|
||
t.Errorf("expected placeholder for object without known fields, got JSON: %q", got)
|
||
}
|
||
}
|
||
|
||
func TestFormatValue_FloatNoScientificNotation(t *testing.T) {
|
||
got := formatValue(1.77920097e+09)
|
||
if strings.Contains(got, "e+") || strings.Contains(got, "E+") {
|
||
t.Errorf("expected float without scientific notation, got %q", got)
|
||
}
|
||
if !strings.Contains(got, "1779200970") {
|
||
t.Errorf("expected full decimal expansion, got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestPrintSliceTable_LimitsColumns(t *testing.T) {
|
||
// Build a single row with 12 columns.
|
||
row := map[string]interface{}{}
|
||
cols := []string{"id", "name", "login", "title", "subject", "number",
|
||
"status", "state", "extra1", "extra2", "extra3", "extra4"}
|
||
for _, c := range cols {
|
||
row[c] = "v_" + c
|
||
}
|
||
var buf bytes.Buffer
|
||
if err := printSliceTable(&buf, []interface{}{row}); err != nil {
|
||
t.Fatalf("printSliceTable returned error: %v", err)
|
||
}
|
||
out := buf.String()
|
||
|
||
// The header line is the first non-empty line.
|
||
lines := strings.Split(out, "\n")
|
||
var header string
|
||
for _, l := range lines {
|
||
if strings.TrimSpace(l) != "" {
|
||
header = l
|
||
break
|
||
}
|
||
}
|
||
colCount := len(strings.Split(header, "\t"))
|
||
if colCount > 8 {
|
||
t.Errorf("expected <= 8 columns in header, got %d:\n%s", colCount, header)
|
||
}
|
||
// And the output should note that columns were omitted.
|
||
if !strings.Contains(out, "省略") && !strings.Contains(strings.ToLower(out), "omitted") {
|
||
t.Errorf("expected output to mention omitted columns (省略/omitted), got:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestPrintSliceTable_PreservesPriorityHeaders(t *testing.T) {
|
||
// Even with >8 columns, priority fields like id/name should survive.
|
||
row := map[string]interface{}{}
|
||
cols := []string{"a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "id", "name"}
|
||
for _, c := range cols {
|
||
row[c] = "v"
|
||
}
|
||
var buf bytes.Buffer
|
||
if err := printSliceTable(&buf, []interface{}{row}); err != nil {
|
||
t.Fatalf("printSliceTable returned error: %v", err)
|
||
}
|
||
out := buf.String()
|
||
if !strings.Contains(out, "id") || !strings.Contains(out, "name") {
|
||
t.Errorf("expected priority headers id/name to survive; got:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestTruncateString(t *testing.T) {
|
||
if got := truncateString("abc", 10); got != "abc" {
|
||
t.Errorf("expected short string unchanged, got %q", got)
|
||
}
|
||
got := truncateString("abcdefghij", 5)
|
||
if len(got) > 5 {
|
||
t.Errorf("expected len <= 5, got %d: %q", len(got), got)
|
||
}
|
||
if !strings.HasSuffix(got, "...") {
|
||
t.Errorf("expected ... suffix, got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestFormatNestedMap_LoginAndSha(t *testing.T) {
|
||
m := map[string]interface{}{
|
||
"login": "bob",
|
||
"sha": "0123456789abcdef0123456789abcdef",
|
||
}
|
||
got := formatNestedMap(m)
|
||
if !strings.Contains(got, "bob") {
|
||
t.Errorf("expected login in summary, got %q", got)
|
||
}
|
||
if !strings.Contains(got, "0123456") {
|
||
t.Errorf("expected truncated sha prefix in summary, got %q", got)
|
||
}
|
||
// Should not contain the full sha.
|
||
if strings.Contains(got, "0123456789abcdef0123456789abcdef") {
|
||
t.Errorf("expected sha to be truncated, got %q", got)
|
||
}
|
||
}
|
||
|
||
// TestPrintSliceTable_HeaderUnionAcrossRows 验证当各行字段集合不一致时
|
||
// (GitLink 分支列表的典型情况:并非每个分支都返回 protected 字段),
|
||
// 表头应取所有行的字段并集,而不是仅取第一行。否则 protected 列会因落在
|
||
// 第一位的样本恰好缺失而「时有时无」。
|
||
func TestPrintSliceTable_HeaderUnionAcrossRows(t *testing.T) {
|
||
items := []interface{}{
|
||
map[string]interface{}{"name": "develop"}, // 第一行缺 protected
|
||
map[string]interface{}{"name": "main", "protected": false}, // 第二行带 protected
|
||
}
|
||
var buf bytes.Buffer
|
||
if err := printSliceTable(&buf, items); err != nil {
|
||
t.Fatalf("printSliceTable returned error: %v", err)
|
||
}
|
||
out := buf.String()
|
||
if !strings.Contains(out, "protected") {
|
||
t.Errorf("expected header to include \"protected\" from row union (not just first row); got:\n%s", out)
|
||
}
|
||
if !strings.Contains(out, "develop") || !strings.Contains(out, "main") {
|
||
t.Errorf("expected both rows present in output; got:\n%s", out)
|
||
}
|
||
}
|
||
|
||
// TestPrintSliceTable_ProtectedSurvivesColumnCap 验证当列数超过上限时,
|
||
// protected(是否保护分支)作为优先字段会被保留、不被截断丢弃——否则在
|
||
// 字段较多的分支列表里这一列仍可能消失。
|
||
func TestPrintSliceTable_ProtectedSurvivesColumnCap(t *testing.T) {
|
||
row := map[string]interface{}{}
|
||
cols := []string{"a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "id", "name", "protected"}
|
||
for _, c := range cols {
|
||
row[c] = "v"
|
||
}
|
||
var buf bytes.Buffer
|
||
if err := printSliceTable(&buf, []interface{}{row}); err != nil {
|
||
t.Fatalf("printSliceTable returned error: %v", err)
|
||
}
|
||
out := buf.String()
|
||
if !strings.Contains(out, "protected") {
|
||
t.Errorf("expected priority field \"protected\" to survive the column cap; got:\n%s", out)
|
||
}
|
||
}
|
||
|
||
// TestOrderKeySet_BranchColumnOrderIsStable 守护分支核心列的固定顺序
|
||
// name / protected / last_commit / commit_time。当前若 last_commit /
|
||
// commit_time 不在 preferredHeaders,orderKeySet 会把这两个非优先键按 map
|
||
// 遍历序追加,导致列顺序随机——重复调用必暴露不一致。
|
||
func TestOrderKeySet_BranchColumnOrderIsStable(t *testing.T) {
|
||
want := []string{"name", "protected", "last_commit", "commit_time"}
|
||
keys := map[string]bool{"name": true, "protected": true, "last_commit": true, "commit_time": true}
|
||
for i := 0; i < 100; i++ {
|
||
got := orderKeySet(keys)
|
||
if len(got) != len(want) {
|
||
t.Fatalf("iteration %d: got %d keys %v, want %v", i, len(got), got, want)
|
||
}
|
||
for j, w := range want {
|
||
if got[j] != w {
|
||
t.Fatalf("iteration %d: got[%d]=%q, want %q (full %v)", i, j, got[j], w, got)
|
||
}
|
||
}
|
||
}
|
||
}
|