summaryrefslogtreecommitdiff
path: root/tests/remembering.go
diff options
context:
space:
mode:
Diffstat (limited to 'tests/remembering.go')
-rw-r--r--tests/remembering.go523
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()
+}