diff options
Diffstat (limited to 'tests/remembering.go')
| -rw-r--r-- | tests/remembering.go | 523 |
1 files changed, 523 insertions, 0 deletions
diff --git a/tests/remembering.go b/tests/remembering.go new file mode 100644 index 0000000..3f8e446 --- /dev/null +++ b/tests/remembering.go @@ -0,0 +1,523 @@ +package remembering + +import ( + "fmt" + "os" + "path/filepath" + "reflect" + "strings" +) + + + +func showColour() bool { + return os.Getenv("NO_COLOUR") == "" +} + +func testStart(name string) { + fmt.Fprintf(os.Stderr, "%s:\n", name) +} + +func testing(message string, body func()) { + if showColour() { + fmt.Fprintf( + os.Stderr, + "\033[0;33mtesting\033[0m: %s... ", + message, + ) + body() + fmt.Fprintf(os.Stderr, "\033[0;32mOK\033[0m.\n") + } else { + fmt.Fprintf(os.Stderr, "testing: %s... ", message) + body() + fmt.Fprintf(os.Stderr, "OK.\n") + } +} + +func assertEq(given any, expected any) { + if !reflect.DeepEqual(given, expected) { + if showColour() { + fmt.Fprintf(os.Stderr, "\033[0;31mERR\033[0m.\n") + } else { + fmt.Fprintf(os.Stderr, "ERR.\n") + } + fmt.Fprintf(os.Stderr, "given != expected\n") + fmt.Fprintf(os.Stderr, "given: %#v\n", given) + fmt.Fprintf(os.Stderr, "expected: %#v\n", expected) + os.Exit(1) + } +} + + + +func mktmp() string { + dir, err := os.MkdirTemp("", "remembering-tests-") + if err != nil { + panic(err) + } + return dir +} + +func withEnv(key string, value string, body func()) { + saved, had := os.LookupEnv(key) + os.Setenv(key, value) + defer func() { + if had { + os.Setenv(key, saved) + } else { + os.Unsetenv(key) + } + }() + body() +} + +func runWith(stdin string, args ...string) (int, string, string) { + out := strings.Builder{} + errW := strings.Builder{} + rc := run(envT{ + allArgs: append([]string{"remembering"}, args...), + in: strings.NewReader(stdin), + out: &out, + err: &errW, + }) + return rc, out.String(), errW.String() +} + +func seedProfile(name string, content string) { + path, err := profilePath(name) + if err != nil { + panic(err) + } + err = os.MkdirAll(filepath.Dir(path), 0755) + if err != nil { + panic(err) + } + err = os.WriteFile(path, []byte(content), 0644) + if err != nil { + panic(err) + } +} + +func profileContent(name string) string { + path, err := profilePath(name) + if err != nil { + panic(err) + } + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + return string(data) +} + + + +func test_splitLines() { + testStart("splitLines()") + + testing("empty input has no lines", func() { + assertEq(splitLines(""), []string{}) + }) + + testing("the final newline opens no empty line", func() { + assertEq(splitLines("a\nb\n"), []string{"a", "b"}) + }) + + testing("a missing final newline still ends a line", func() { + assertEq(splitLines("a\nb"), []string{"a", "b"}) + }) + + testing("blank lines in the middle are kept", func() { + assertEq( + splitLines("a\n\nb\n"), + []string{"a", "", "b"}, + ) + }) + + testing("a lone newline is one empty line", func() { + assertEq(splitLines("\n"), []string{""}) + }) +} + +func test_parseProfile() { + testStart("parseProfile()") + + testing("canonical lines", func() { + assertEq( + parseProfile("0 profile a\n12 profile b c\n"), + []entryT{{0, "a"}, {12, "b c"}}, + ) + }) + + testing("text keeps its leading blanks", func() { + assertEq( + parseProfile("1 profile spaced\n"), + []entryT{{1, " spaced"}}, + ) + }) + + testing("untagged and blank lines are dropped", func() { + given := parseProfile( + "1 stdin a\njunk\n\n2 profile b\n", + ) + assertEq(given, []entryT{{2, "b"}}) + }) + + testing("a non-numeric count reads as zero", func() { + assertEq( + parseProfile("x profile a\n"), + []entryT{{0, "a"}}, + ) + }) + + testing("duplicate texts are kept apart", func() { + assertEq( + parseProfile("3 profile a\n2 profile a\n"), + []entryT{{3, "a"}, {2, "a"}}, + ) + }) +} + +func test_serializeProfile() { + testStart("serializeProfile()") + + testing("one line per entry, count first", func() { + given := serializeProfile([]entryT{ + {1, "a b"}, + {0, "c"}, + }) + assertEq(given, "1 profile a b\n0 profile c\n") + }) + + testing("no entries make an empty file", func() { + assertEq(serializeProfile([]entryT{}), "") + }) + + testing("parse undoes serialize", func() { + entries := []entryT{{4, " x"}, {0, "y z"}} + assertEq( + parseProfile(serializeProfile(entries)), + entries, + ) + }) +} + +func test_menuFor() { + testStart("menuFor()") + + testing("most picked first, ties in byte order", func() { + profile := []entryT{ + {4, "l"}, {3, "f"}, {2, "a"}, {2, "g"}, + {2, "r"}, {1, "b"}, {1, "d"}, {1, "m"}, + {1, "s"}, + } + lines := []string{ + "a", "b", "c", "d", "f", + "g", "l", "m", "r", "s", + } + assertEq(menuFor(profile, lines), []entryT{ + {4, "l"}, {3, "f"}, {2, "a"}, {2, "g"}, + {2, "r"}, {1, "b"}, {1, "d"}, {1, "m"}, + {1, "s"}, {0, "c"}, + }) + }) + + testing("duplicate profile counts are summed", func() { + profile := []entryT{{2, "x"}, {3, "x"}} + given := menuFor(profile, []string{"x", "y"}) + assertEq(given, []entryT{{5, "x"}, {0, "y"}}) + }) + + testing("duplicate stdin lines all appear", func() { + given := menuFor([]entryT{}, []string{"x", "x"}) + assertEq(given, []entryT{{0, "x"}, {0, "x"}}) + }) + + testing("byte order, not collation order", func() { + given := menuFor([]entryT{}, []string{"é", "e"}) + assertEq(given, []entryT{{0, "e"}, {0, "é"}}) + }) + + testing("stdin defines the menu", func() { + profile := []entryT{{9, "ghost"}} + given := menuFor(profile, []string{"a"}) + assertEq(given, []entryT{{0, "a"}}) + }) +} + +func test_menuText() { + testStart("menuText()") + + testing("texts, one per line", func() { + menu := []entryT{{1, "a"}, {0, "b c"}} + assertEq(menuText(menu), "a\nb c\n") + }) + + testing("an empty menu is empty input", func() { + assertEq(menuText([]entryT{}), "") + }) +} + +func test_nextProfile() { + testStart("nextProfile()") + + zeroes := []entryT{ + {0, "a"}, {0, "b"}, {0, "c"}, {0, "d"}, {0, "e"}, + } + lines := []string{"a", "b", "c", "d", "e"} + + testing("the pick's count is bumped", func() { + assertEq(nextProfile(zeroes, lines, "a"), []entryT{ + {1, "a"}, {0, "b"}, {0, "c"}, + {0, "d"}, {0, "e"}, + }) + }) + + testing("an unknown pick is appended at the end", func() { + assertEq(nextProfile(zeroes, lines, "f"), []entryT{ + {0, "a"}, {0, "b"}, {0, "c"}, + {0, "d"}, {0, "e"}, {1, "f"}, + }) + }) + + testing("offered but never picked enters at zero", func() { + given := nextProfile( + []entryT{{0, "a"}}, lines, "a", + ) + assertEq(given, []entryT{ + {1, "a"}, {0, "b"}, {0, "c"}, + {0, "d"}, {0, "e"}, + }) + }) + + testing("entries absent from stdin are retained", func() { + profile := []entryT{{0, "a"}, {7, "z"}} + given := nextProfile( + profile, []string{"a", "b"}, "a", + ) + assertEq(given, []entryT{ + {1, "a"}, {0, "b"}, {7, "z"}, + }) + }) + + testing("duplicate texts collapse to the highest", func() { + profile := []entryT{{2, "x"}, {5, "x"}} + given := nextProfile(profile, []string{}, "x") + assertEq(given, []entryT{{6, "x"}}) + }) + + testing("picking twice accumulates", func() { + once := nextProfile([]entryT{}, lines, "c") + twice := nextProfile(once, lines, "c") + assertEq(twice, []entryT{ + {0, "a"}, {0, "b"}, {2, "c"}, + {0, "d"}, {0, "e"}, + }) + }) +} + +func test_profilePath() { + testStart("profilePath()") + + testing("$XDG_DATA_HOME wins", func() { + withEnv("XDG_DATA_HOME", "/x", func() { + withEnv("HOME", "/h", func() { + given, err := profilePath("name") + assertEq(err, nil) + assertEq( + given, + "/x/remembering/name", + ) + }) + }) + }) + + testing("an empty $XDG_DATA_HOME falls back", func() { + withEnv("XDG_DATA_HOME", "", func() { + withEnv("HOME", "/h", func() { + given, err := profilePath("name") + assertEq(err, nil) + assertEq( + given, + "/h/.local/share/"+ + "remembering/name", + ) + }) + }) + }) + + testing("the default name is the cwd, bashed", func() { + withEnv("XDG_DATA_HOME", "/x", func() { + cwd, err := os.Getwd() + assertEq(err, nil) + expected := filepath.Join( + "/x", Name, + strings.ReplaceAll(cwd, "/", "!"), + ) + given, err := profilePath("") + assertEq(err, nil) + assertEq(given, expected) + }) + }) +} + +func test_run_usage() { + testStart("run() usage") + + testing("a missing COMMAND is a usage error", func() { + rc, out, errW := runWith("") + assertEq(rc, 2) + assertEq(out, "") + assertEq( + strings.Contains( + errW, "Missing \"-- COMMAND\"", + ), + true, + ) + assertEq(strings.Contains(errW, "Usage:"), true) + }) + + testing("an unknown flag is a usage error", func() { + rc, _, errW := runWith("", "-x", "--", "head") + assertEq(rc, 2) + assertEq(strings.Contains(errW, "Usage:"), true) + }) + + testing("long options are rejected", func() { + rc, _, errW := runWith("", "--help") + assertEq(rc, 2) + assertEq(strings.Contains(errW, "Usage:"), true) + }) + + testing("-p alone still misses COMMAND", func() { + rc, _, errW := runWith("", "-p", "x") + assertEq(rc, 2) + assertEq(strings.Contains(errW, "Usage:"), true) + }) +} + +func test_run_flow() { + testStart("run() flow") + + tmp := mktmp() + defer os.RemoveAll(tmp) + + withEnv("XDG_DATA_HOME", tmp, func() { + testing("a pick is learnt, printed", func() { + rc, out, errW := runWith( + "b\na\n", + "-p", "t1", "--", "head", "-n1", + ) + assertEq(rc, 0) + assertEq(out, "a\n") + assertEq(errW, "") + assertEq( + profileContent("t1"), + "1 profile a\n0 profile b\n", + ) + }) + + testing("a second pick ranks above", func() { + rc, out, _ := runWith( + "b\na\n", + "-p", "t1", "--", "head", "-n1", + ) + assertEq(rc, 0) + assertEq(out, "a\n") + assertEq( + profileContent("t1"), + "2 profile a\n0 profile b\n", + ) + }) + + testing("no pick leaves the profile alone", func() { + rc, out, errW := runWith( + "", + "-p", "t2", "--", "head", "-n1", + ) + assertEq(rc, 0) + assertEq(out, "") + assertEq(errW, "") + assertEq(profileContent("t2"), "") + }) + + testing("an unknown pick is appended", func() { + seedProfile( + "t3", + "0 profile a\n0 profile b\n", + ) + rc, out, _ := runWith( + "a\nb\n", + "-p", "t3", "--", + "sh", "-c", "echo q", + ) + assertEq(rc, 0) + assertEq(out, "q\n") + assertEq( + profileContent("t3"), + "0 profile a\n0 profile b\n"+ + "1 profile q\n", + ) + }) + + testing("the command status is forwarded", func() { + rc, out, _ := runWith( + "", + "-p", "t4", "--", + "sh", "-c", "exit 7", + ) + assertEq(rc, 7) + assertEq(out, "") + }) + + testing("a signal death is 128 plus it", func() { + rc, _, _ := runWith( + "", + "-p", "t5", "--", + "sh", "-c", "kill -9 $$", + ) + assertEq(rc, 137) + }) + + testing("a missing command is 127", func() { + rc, _, errW := runWith( + "", + "-p", "t6", "--", + "/nonexistent-remembering-x", + ) + assertEq(rc, 127) + assertEq( + strings.Contains( + errW, "remembering:", + ), + true, + ) + }) + + testing("the command stderr passes through", func() { + rc, out, errW := runWith( + "x\n", + "-p", "t7", "--", + "sh", "-c", + "echo warn >&2; head -n1", + ) + assertEq(rc, 0) + assertEq(out, "x\n") + assertEq(errW, "warn\n") + }) + }) +} + + + +func MainTest() { + test_splitLines() + test_parseProfile() + test_serializeProfile() + test_menuFor() + test_menuText() + test_nextProfile() + test_profilePath() + test_run_usage() + test_run_flow() +} |
