package main // overview_test.go — the inventory view: what mgsh knows that plain git cannot. import ( "errors" "io" "os" "path/filepath" "strings" "testing" "time" ) // TestReadStatusParsesPorcelainV2 covers the single `git status` call that // replaced three separate ones: branch, upstream, ahead/behind and dirty all // come out of its header. func TestReadStatusParsesPorcelainV2(t *testing.T) { dir := t.TempDir() bare := filepath.Join(t.TempDir(), "o.git") mustGit(t, "", "init", "--bare", "-q", bare) mustGit(t, dir, "init", "-q") mustGit(t, dir, "config", "user.name", "t") mustGit(t, dir, "config", "user.email", "t@e") mustGit(t, dir, "commit", "-q", "--allow-empty", "-m", "[mike@laptop] work") mustGit(t, dir, "remote", "add", "origin", bare) mustGit(t, dir, "push", "-q", "-u", "origin", "HEAD") var s projStatus readStatus(&s, dir) if !s.hasUpstream || s.ahead != 0 || s.behind != 0 || s.dirty { t.Errorf("clean synced repo = %+v", s) } if s.branch == "" || s.branch == "-" { t.Errorf("branch not read: %q", s.branch) } // one unstaged file and one unpushed commit if err := os.WriteFile(filepath.Join(dir, "x"), []byte("x"), 0644); err != nil { t.Fatal(err) } mustGit(t, dir, "commit", "-q", "--allow-empty", "-m", "[mike@desktop] more") s = projStatus{} readStatus(&s, dir) if !s.dirty { t.Error("untracked file did not register as dirty") } if s.ahead != 1 { t.Errorf("ahead = %d, want 1", s.ahead) } // and the host stamp `push` writes is picked up var l projStatus readLastCommit(&l, dir) if l.lastHost != "desktop" { t.Errorf("lastHost = %q, want desktop", l.lastHost) } if l.lastWhen.IsZero() { t.Error("lastWhen not read") } } // TestCommitHostRe: only mgsh's own "[user@host]" stamp counts. func TestCommitHostRe(t *testing.T) { cases := map[string]string{ "[mike@laptop] fixed a thing": "laptop", "[mike@build-01] ": "build-01", "[@host] no user": "host", "fixed a thing": "", "[not a stamp] text": "", "see [a@b] mid-line": "", } for subj, want := range cases { got := "" if m := commitHostRe.FindStringSubmatch(subj); m != nil { got = m[1] } if got != want { t.Errorf("host of %q = %q, want %q", subj, got, want) } } } // TestReportInventoryUnreachableServer: when the server cannot be listed, the // overview must say so rather than claim every project is missing there. func TestReportInventoryUnreachableServer(t *testing.T) { out := captureStdout(t, func() { reportInventory([]string{"a", "b"}, nil, errors.New("network is unreachable")) }) if strings.Contains(out, "not on the server") { t.Errorf("an unreachable server was reported as missing repositories: %q", out) } if !strings.Contains(out, "not reachable") { t.Errorf("no hint that the server was unreachable: %q", out) } } // TestReportInventorySplitsSides is the join that plain git cannot do. func TestReportInventorySplitsSides(t *testing.T) { out := captureStdout(t, func() { reportInventory([]string{"both", "onlyhere"}, []string{"both", "onlythere"}, nil) }) if !strings.Contains(out, "not on the server (init)") || !strings.Contains(out, "onlyhere") { t.Errorf("local-only project not reported: %q", out) } if !strings.Contains(out, "not cloned here (clone)") || !strings.Contains(out, "onlythere") { t.Errorf("server-only project not reported: %q", out) } if strings.Contains(out, "both") { t.Errorf("a project present on both sides should not be listed: %q", out) } } func TestShortAge(t *testing.T) { cases := []struct { d time.Duration want string }{ {30 * time.Second, "now"}, {90 * time.Second, "1m"}, {2 * time.Hour, "2h"}, {36 * time.Hour, "1d"}, {20 * 24 * time.Hour, "20d"}, } for _, c := range cases { if got := shortAge(c.d); got != c.want { t.Errorf("shortAge(%v) = %q, want %q", c.d, got, c.want) } } } // captureStdout collects everything a function prints. func captureStdout(t *testing.T, fn func()) string { t.Helper() old := os.Stdout r, w, err := os.Pipe() if err != nil { t.Fatal(err) } os.Stdout = w done := make(chan string) go func() { var b strings.Builder io.Copy(&b, r) done <- b.String() }() fn() w.Close() os.Stdout = old return <-done }