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) } } } // TestMarkUnpublishedUnreachableServer: a listing that failed must leave every // row unmarked. Not knowing is not the same as knowing they are missing — // marking all of them would tell the user to re-init their whole base. func TestMarkUnpublishedUnreachableServer(t *testing.T) { rows := []projStatus{{name: "a"}, {name: "b"}} markUnpublished(rows, nil, errors.New("network is unreachable")) for _, r := range rows { if r.notOnServer { t.Errorf("%s marked as missing although the server could not be listed", r.name) } } } // TestMarkUnpublished flags only what the server really does not have. func TestMarkUnpublished(t *testing.T) { rows := []projStatus{{name: "both"}, {name: "onlyhere"}} markUnpublished(rows, []string{"both", "onlythere"}, nil) if rows[0].notOnServer { t.Error("a project present on both sides was marked") } if !rows[1].notOnServer { t.Error("a local-only project was not marked") } } // TestUnpublishedRowsCarryTheHint: the entries live in the table now, with the // action in their own column, rather than in a list underneath it. func TestUnpublishedRowsCarryTheHint(t *testing.T) { useColor = false rows := []projStatus{ {name: "published", isRepo: true, hasUpstream: true}, {name: "fresh", isRepo: true, notOnServer: true}, {name: "notarepo", notOnServer: true}, } w := measureOverview(rows) if !w.hint { t.Fatal("hint column not reserved although rows need it") } got := []string{} for _, r := range rows { got = append(got, formatProjStatus(r, w)) } if strings.Contains(got[0], "init") { t.Errorf("a published project was hinted: %q", got[0]) } for _, i := range []int{1, 2} { if !strings.Contains(got[i], "init") { t.Errorf("row %d missing the init hint: %q", i, got[i]) } } // a directory that is not a repository has no sync state to report if strings.ContainsAny(got[2], "✓–↑↓") { t.Errorf("non-repository row claims a git state: %q", got[2]) } // with nothing to hint the column disappears entirely if measureOverview(rows[:1]).hint { t.Error("hint column reserved although no row needs it") } } 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 }