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") }) testing("attached -pNAME is accepted", func() { rc, out, _ := runWith( "b\na\n", "-pt8", "--", "head", "-n1", ) assertEq(rc, 0) assertEq(out, "a\n") assertEq( profileContent("t8"), "1 profile a\n0 profile b\n", ) }) testing("-- shields the command's argv", func() { rc, out, _ := runWith( "", "-p", "t9", "--", "sh", "-c", "echo \"$0\"", "-pz", ) assertEq(rc, 0) assertEq(out, "-pz\n") }) testing("operands stop option parsing", func() { rc, out, _ := runWith( "", "-p", "t10", "sh", "-c", "echo \"$0\"", "-pz", ) assertEq(rc, 0) assertEq(out, "-pz\n") }) }) } func MainTest() { test_splitLines() test_parseProfile() test_serializeProfile() test_menuFor() test_menuText() test_nextProfile() test_profilePath() test_run_usage() test_run_flow() }