gitlink-cli/internal/output/formatter_test.go

369 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 不在 preferredHeadersorderKeySet 会把这两个非优先键按 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)
}
}
}
}