initial commit [141.14.129.234,mike]
This commit is contained in:
+423
@@ -0,0 +1,423 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSanitizeComment(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{"", ""},
|
||||
{"simple comment", "simple_comment"},
|
||||
{"it's a test", "it_s_a_test"},
|
||||
{"a/b:c;d", "a_b_c_d"},
|
||||
{"added -v flag", "added_v_flag"},
|
||||
{" padded spaces ", "padded_spaces"},
|
||||
{"weird<>|?chars", "weird_chars"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := sanitizeComment(c.in); got != c.want {
|
||||
t.Errorf("sanitizeComment(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveStampFormat(t *testing.T) {
|
||||
s := archiveStamp()
|
||||
// expect DDMMYY.HHMM => 6 digits, dot, 4 digits
|
||||
if len(s) != 11 || s[6] != '.' {
|
||||
t.Fatalf("archiveStamp() = %q, want DDMMYY.HHMM shape", s)
|
||||
}
|
||||
for i, r := range s {
|
||||
if i == 6 {
|
||||
continue
|
||||
}
|
||||
if r < '0' || r > '9' {
|
||||
t.Fatalf("archiveStamp() = %q has non-digit at %d", s, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatLogNoColor(t *testing.T) {
|
||||
useColor = false
|
||||
// fixed reference time: 2020-06-15 12:00:00 local
|
||||
now := time.Date(2020, 6, 15, 12, 0, 0, 0, time.Local)
|
||||
old := time.Date(2019, 10, 17, 16, 9, 0, 0, time.Local).Unix()
|
||||
line := "f9e76ff " + strconv.FormatInt(old, 10) + " initial commit"
|
||||
out := formatLog([]string{line}, now)
|
||||
if !strings.Contains(out, "f9e76ff") || !strings.Contains(out, "initial commit") {
|
||||
t.Fatalf("formatLog missing hash/subject: %q", out)
|
||||
}
|
||||
// older than a week -> full date form contains year and weekday
|
||||
if !strings.Contains(out, "2019") || !strings.Contains(out, "Thu") {
|
||||
t.Fatalf("formatLog full date form expected, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatLogRecentCompact(t *testing.T) {
|
||||
useColor = false
|
||||
now := time.Date(2020, 6, 15, 12, 0, 0, 0, time.Local)
|
||||
recent := now.Add(-2 * time.Hour).Unix()
|
||||
out := formatLog([]string{"abc123 " + strconv.FormatInt(recent, 10) + " recent work"}, now)
|
||||
// within a day -> tiny form: only HH:MM, no year, no weekday
|
||||
if strings.Contains(out, "2020") {
|
||||
t.Fatalf("recent entry should use compact time form, got %q", out)
|
||||
}
|
||||
if !strings.Contains(out, "recent work") {
|
||||
t.Fatalf("missing subject, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorRepoLine(t *testing.T) {
|
||||
useColor = false
|
||||
in := "Sep 28 2016 Betaflight3.0.0"
|
||||
if got := colorRepoLine(in); got != in {
|
||||
t.Errorf("colorRepoLine with color off changed input: %q", got)
|
||||
}
|
||||
|
||||
useColor = true
|
||||
got := colorRepoLine(in)
|
||||
if !strings.Contains(got, "Betaflight3.0.0") || !strings.Contains(got, cGreen) || !strings.Contains(got, cYellow) {
|
||||
t.Errorf("colorRepoLine did not color parts: %q", got)
|
||||
}
|
||||
useColor = false
|
||||
}
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
rc := `
|
||||
# comment line
|
||||
githost = 10.0.0.1
|
||||
GitPort: 22
|
||||
gituser = "deploy"
|
||||
editor = 'code'
|
||||
ignored line without separator
|
||||
base=/tmp/src
|
||||
`
|
||||
m := parseConfig(rc)
|
||||
checks := map[string]string{
|
||||
"githost": "10.0.0.1",
|
||||
"gitport": "22",
|
||||
"gituser": "deploy",
|
||||
"editor": "code",
|
||||
"base": "/tmp/src",
|
||||
}
|
||||
for k, want := range checks {
|
||||
if m[k] != want {
|
||||
t.Errorf("parseConfig[%q] = %q, want %q", k, m[k], want)
|
||||
}
|
||||
}
|
||||
if _, ok := m["ignored line without separator"]; ok {
|
||||
t.Errorf("line without separator should be ignored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyConfig(t *testing.T) {
|
||||
c := Config{GitName: "Original Name"}
|
||||
applyConfig(&c, map[string]string{
|
||||
"githost": "example.com",
|
||||
"gitport": "2200",
|
||||
"gitname": "", // empty must not override an existing value
|
||||
})
|
||||
if c.GitHost != "example.com" {
|
||||
t.Errorf("GitHost = %q, want example.com", c.GitHost)
|
||||
}
|
||||
if c.GitPort != "2200" {
|
||||
t.Errorf("GitPort = %q, want 2200", c.GitPort)
|
||||
}
|
||||
if c.GitName != "Original Name" {
|
||||
t.Errorf("empty value must not override GitName, got %q", c.GitName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingRequired(t *testing.T) {
|
||||
full := Config{Base: "/b", GitHost: "h", GitPort: "22", GitUser: "u", GitPath: "/p"}
|
||||
if m := full.missingRequired(); len(m) != 0 {
|
||||
t.Errorf("complete config reported missing: %v", m)
|
||||
}
|
||||
partial := Config{Base: "/b", GitPort: "22"}
|
||||
got := strings.Join(partial.missingRequired(), ",")
|
||||
if got != "githost,gituser,gitpath" {
|
||||
t.Errorf("missingRequired = %q, want githost,gituser,gitpath", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitLines(t *testing.T) {
|
||||
if got := splitLines(""); got != nil {
|
||||
t.Errorf("splitLines(\"\") = %v, want nil", got)
|
||||
}
|
||||
got := splitLines("a\nb\nc\n")
|
||||
if len(got) != 3 || got[0] != "a" || got[2] != "c" {
|
||||
t.Errorf("splitLines = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWord(t *testing.T) {
|
||||
ws := []string{"a", "b"}
|
||||
if word(ws, 0) != "a" || word(ws, 1) != "b" || word(ws, 2) != "" {
|
||||
t.Errorf("word indexing wrong")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnquote(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{"'echo $1'", "echo $1"},
|
||||
{`"echo $1"`, "echo $1"},
|
||||
{"echo $1", "echo $1"},
|
||||
{"'unbalanced", "'unbalanced"},
|
||||
{"''", ""},
|
||||
{"'", "'"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := unquote(c.in); got != c.want {
|
||||
t.Errorf("unquote(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigTemplateGenerated(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
for _, e := range []string{"MGSH_BASE", "MGSH_GITHOST", "MGSH_GITPORT", "MGSH_GITUSER", "MGSH_GITPATH"} {
|
||||
t.Setenv(e, "") // ensure env can't satisfy the requirements
|
||||
}
|
||||
rc := filepath.Join(home, ".mgshrc")
|
||||
|
||||
// no ~/.mgshrc yet -> loadConfig writes a blank template (no real values)
|
||||
c := loadConfig()
|
||||
if !fileExists(rc) {
|
||||
t.Fatalf("loadConfig did not generate %s", rc)
|
||||
}
|
||||
if m := c.missingRequired(); len(m) != len(requiredKeys) {
|
||||
t.Fatalf("blank template should leave all required unset, missing=%v", m)
|
||||
}
|
||||
if s := readFile(t, rc); !strings.Contains(s, "base") || !strings.Contains(s, "githost") {
|
||||
t.Fatalf("template missing key hints:\n%s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigLoadAndAliasRoundTrip(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
rc := filepath.Join(home, ".mgshrc")
|
||||
os.WriteFile(rc, []byte(
|
||||
"base = "+home+"\ngithost = h.example\ngitport = 22\ngituser = git\ngitpath = /home/git\n"), 0644)
|
||||
|
||||
c := loadConfig()
|
||||
if m := c.missingRequired(); len(m) != 0 {
|
||||
t.Fatalf("configured file still reports missing: %v", m)
|
||||
}
|
||||
if c.GitHost != "h.example" {
|
||||
t.Fatalf("GitHost = %q, want h.example", c.GitHost)
|
||||
}
|
||||
|
||||
// defining an alias persists it into ~/.mgshrc without losing config lines
|
||||
aliases = map[string]string{"co": "checkout $1"}
|
||||
saveAliases()
|
||||
s := readFile(t, rc)
|
||||
if !strings.Contains(s, "alias co 'checkout $1'") {
|
||||
t.Fatalf("alias not written to rc:\n%s", s)
|
||||
}
|
||||
if !strings.Contains(s, "githost = h.example") {
|
||||
t.Fatalf("saveAliases clobbered config lines:\n%s", s)
|
||||
}
|
||||
|
||||
// and it reloads from the same file
|
||||
aliases = map[string]string{}
|
||||
loadAliases()
|
||||
if aliases["co"] != "checkout $1" {
|
||||
t.Fatalf("alias did not round-trip, got %q", aliases["co"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyAliasMigration(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
legacy := filepath.Join(home, ".mgsh_aliases")
|
||||
if err := os.WriteFile(legacy, []byte("# header\nls\t!lsd -la\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
loadConfig() // generates ~/.mgshrc and migrates the legacy alias
|
||||
rc := filepath.Join(home, ".mgshrc")
|
||||
if s := readFile(t, rc); !strings.Contains(s, "alias ls '!lsd -la'") {
|
||||
t.Fatalf("legacy alias not migrated into rc:\n%s", s)
|
||||
}
|
||||
if fileExists(legacy) {
|
||||
t.Fatalf("legacy alias file should be removed after migration")
|
||||
}
|
||||
|
||||
aliases = map[string]string{}
|
||||
loadAliases()
|
||||
if aliases["ls"] != "!lsd -la" {
|
||||
t.Fatalf("migrated alias not loaded, got %q", aliases["ls"])
|
||||
}
|
||||
}
|
||||
|
||||
func readFile(t *testing.T, p string) string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func TestTruthy(t *testing.T) {
|
||||
on := []string{"1", "true", "TRUE", "yes", "On", " true "}
|
||||
off := []string{"", "0", "false", "no", "off", "nope"}
|
||||
for _, s := range on {
|
||||
if !truthy(s) {
|
||||
t.Errorf("truthy(%q) = false, want true", s)
|
||||
}
|
||||
}
|
||||
for _, s := range off {
|
||||
if truthy(s) {
|
||||
t.Errorf("truthy(%q) = true, want false", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatProjStatus(t *testing.T) {
|
||||
useColor = false
|
||||
defer func() { useColor = false }()
|
||||
cases := []struct {
|
||||
s projStatus
|
||||
contains []string
|
||||
absent []string
|
||||
}{
|
||||
{projStatus{name: "a", branch: "master", dirty: true, hasUpstream: true, ahead: 2},
|
||||
[]string{"a", "*", "↑2"}, []string{"↓", "(master)"}},
|
||||
{projStatus{name: "b", branch: "main", hasUpstream: true, behind: 3},
|
||||
[]string{"b", "↓3"}, []string{"*", "✓", "(main)"}},
|
||||
{projStatus{name: "c", branch: "master", hasUpstream: true},
|
||||
[]string{"c", "✓"}, []string{"*", "↑", "↓"}},
|
||||
{projStatus{name: "d", branch: "feature", dirty: true},
|
||||
[]string{"d", "*", "(feature)"}, nil},
|
||||
{projStatus{name: "e", branch: "master"}, // clean, no upstream
|
||||
[]string{"e", "no upstream"}, []string{"*"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := formatProjStatus(c.s, 8)
|
||||
for _, sub := range c.contains {
|
||||
if !strings.Contains(got, sub) {
|
||||
t.Errorf("formatProjStatus(%+v) = %q, missing %q", c.s, got, sub)
|
||||
}
|
||||
}
|
||||
for _, sub := range c.absent {
|
||||
if strings.Contains(got, sub) {
|
||||
t.Errorf("formatProjStatus(%+v) = %q, should not contain %q", c.s, got, sub)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectRemoteKind(t *testing.T) {
|
||||
cases := []struct {
|
||||
url, override string
|
||||
want remoteKind
|
||||
}{
|
||||
{"https://git.fhi.mpg.de", "", kindGitea},
|
||||
{"https://github.com", "", kindGitHub},
|
||||
{"https://api.github.com", "", kindGitHub},
|
||||
{"https://gitlab.com", "", kindGitLab},
|
||||
{"https://gitlab.example.org", "", kindGitLab},
|
||||
{"https://git.fhi.mpg.de", "github", kindGitHub}, // override wins
|
||||
{"https://github.com", "gitlab", kindGitLab}, // override wins
|
||||
{"https://anything", "", kindGitea}, // default
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := detectRemoteKind(c.url, c.override); got != c.want {
|
||||
t.Errorf("detectRemoteKind(%q,%q) = %d, want %d", c.url, c.override, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteAPIEndpoints(t *testing.T) {
|
||||
cases := []struct {
|
||||
url, typ string
|
||||
root, web, hdrKey string
|
||||
}{
|
||||
{"https://git.fhi.mpg.de/", "", "https://git.fhi.mpg.de/api/v1", "https://git.fhi.mpg.de/mike/mgsh.git", "Authorization"},
|
||||
{"https://github.com", "", "https://api.github.com", "https://github.com/mike/mgsh.git", "Authorization"},
|
||||
{"https://gitlab.com", "", "https://gitlab.com/api/v4", "https://gitlab.com/mike/mgsh.git", "PRIVATE-TOKEN"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
api := newRemoteAPI(c.url, "tok", c.typ)
|
||||
if got := api.apiRoot(); got != c.root {
|
||||
t.Errorf("apiRoot(%q) = %q, want %q", c.url, got, c.root)
|
||||
}
|
||||
if got := api.repoWebURL("mike", "mgsh"); got != c.web {
|
||||
t.Errorf("repoWebURL(%q) = %q, want %q", c.url, got, c.web)
|
||||
}
|
||||
if k, _ := api.authHeader(); k != c.hdrKey {
|
||||
t.Errorf("authHeader(%q) key = %q, want %q", c.url, k, c.hdrKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstLine(t *testing.T) {
|
||||
if got := firstLine([]byte(" hello\nworld ")); got != "hello" {
|
||||
t.Errorf("firstLine multiline = %q, want hello", got)
|
||||
}
|
||||
long := strings.Repeat("x", 300)
|
||||
if got := firstLine([]byte(long)); len(got) != 200 {
|
||||
t.Errorf("firstLine did not truncate, len=%d", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountSourceLines(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
write := func(rel string, data []byte) {
|
||||
p := filepath.Join(dir, rel)
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, data, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// counted: any text file, regardless of extension, in any visible subdir
|
||||
write("main.go", []byte("a\nb\nc\n")) // 3
|
||||
write("Makefile", []byte("all:\n\techo hi\n")) // 2 (no extension)
|
||||
write("sub/util.py", []byte("x\ny\n")) // 2
|
||||
write("bin/script", []byte("#!/bin/sh\nls\n")) // 2
|
||||
// skipped: hidden file, file in hidden dir, and a binary file
|
||||
write(".gitignore", []byte("node_modules\n"))
|
||||
write(".git/config", []byte("[core]\n\trepo\n"))
|
||||
write("logo.png", []byte{0x89, 'P', 'N', 'G', 0x00, 0x0a, 0x00})
|
||||
|
||||
lines, files := countSourceLines(dir)
|
||||
if files != 4 {
|
||||
t.Errorf("countSourceLines files = %d, want 4", files)
|
||||
}
|
||||
if lines != 9 {
|
||||
t.Errorf("countSourceLines lines = %d, want 9", lines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandAlias(t *testing.T) {
|
||||
cases := []struct {
|
||||
body string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{"checkout $1", []string{"main"}, "checkout main"},
|
||||
{"!echo $1", []string{"hello"}, "!echo hello"},
|
||||
{"push $*", []string{"fixed", "bug"}, "push fixed bug"},
|
||||
{"push $@", []string{"a", "b"}, "push a b"},
|
||||
{"echo $1 $2", []string{"a"}, "echo a"}, // missing $2 -> empty
|
||||
{"status", []string{"x"}, "status x"}, // no placeholder -> append args
|
||||
{"status", nil, "status"}, // no placeholder, no args
|
||||
{"log", []string{}, "log"}, // empty args slice
|
||||
{"diff $1", nil, "diff"}, // placeholder with no arg -> empty
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := expandAlias(c.body, c.args); got != c.want {
|
||||
t.Errorf("expandAlias(%q, %v) = %q, want %q", c.body, c.args, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user