summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/benchmarks/ranking-throughput/main.go7
-rw-r--r--tests/benchmarks/ranking-throughput/remembering.go76
-rwxr-xr-xtests/cli-opts.sh106
-rw-r--r--tests/functional/pick-roundtrip/main.go7
-rw-r--r--tests/functional/pick-roundtrip/remembering.go129
-rw-r--r--tests/fuzz/profile/main.go7
-rw-r--r--tests/fuzz/profile/remembering.go76
-rw-r--r--tests/main.go7
-rwxr-xr-xtests/ranking.sh12
-rw-r--r--tests/remembering.go523
-rwxr-xr-xtests/signals.sh10
11 files changed, 902 insertions, 58 deletions
diff --git a/tests/benchmarks/ranking-throughput/main.go b/tests/benchmarks/ranking-throughput/main.go
new file mode 100644
index 0000000..0b02dc7
--- /dev/null
+++ b/tests/benchmarks/ranking-throughput/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "remembering"
+
+func main() {
+ remembering.MainTest()
+}
diff --git a/tests/benchmarks/ranking-throughput/remembering.go b/tests/benchmarks/ranking-throughput/remembering.go
new file mode 100644
index 0000000..041eaeb
--- /dev/null
+++ b/tests/benchmarks/ranking-throughput/remembering.go
@@ -0,0 +1,76 @@
+package remembering
+
+import (
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+
+
+var linesFlag = flag.Int(
+ "lines",
+ 1000000,
+ "The number of menu lines to rank",
+)
+
+func MainTest() {
+ flag.Parse()
+ n := *linesFlag
+
+ tmp, err := os.MkdirTemp("", "remembering-bench-")
+ if err != nil {
+ panic(err)
+ }
+ defer os.RemoveAll(tmp)
+ err = os.Setenv("XDG_DATA_HOME", tmp)
+ if err != nil {
+ panic(err)
+ }
+
+ menu := strings.Builder{}
+ for i := 0; i < n; i++ {
+ fmt.Fprintf(&menu, "item-%07d\n", i)
+ }
+
+ // half the items are already known, with spread counts
+ profile := strings.Builder{}
+ for i := 0; i < n; i += 2 {
+ fmt.Fprintf(
+ &profile, "%d profile item-%07d\n",
+ i%97, i,
+ )
+ }
+ path, err := profilePath("bench")
+ if err != nil {
+ panic(err)
+ }
+ err = os.MkdirAll(filepath.Dir(path), 0755)
+ if err != nil {
+ panic(err)
+ }
+ err = os.WriteFile(
+ path, []byte(profile.String()), 0644,
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ rc := run(envT{
+ allArgs: []string{
+ "remembering", "-p", "bench",
+ "--", "tail", "-n1",
+ },
+ in: strings.NewReader(menu.String()),
+ out: io.Discard,
+ err: os.Stderr,
+ })
+ if rc != 0 {
+ panic("ranking failed")
+ }
+
+ fmt.Printf("ranked %d lines\n", n)
+}
diff --git a/tests/cli-opts.sh b/tests/cli-opts.sh
index 83a5bfc..0ce4eef 100755
--- a/tests/cli-opts.sh
+++ b/tests/cli-opts.sh
@@ -3,6 +3,8 @@ set -u
. tests/lib.sh
+REMEMBERING="$PWD/remembering.bin"
+
export XDG_DATA_HOME="$PWD/tests/test-profiles"
test_unsupported_long_flags() {
@@ -10,32 +12,40 @@ test_unsupported_long_flags() {
N="$LINENO"
OUT="$(mkstemp)"
ERR="$(mkstemp)"
- ./src/remembering --unknown-long-flag 1>"$OUT" 2>"$ERR"
+ "$REMEMBERING" --unknown-long-flag 1>"$OUT" 2>"$ERR"
STATUS=$?
assert_status 2
assert_usage "$ERR"
test_ok
}
-test_missing_required_flags() {
- testing 'missing required flags'
+test_unknown_flags() {
+ testing 'unknown flags'
N="$LINENO"
OUT="$(mkstemp)"
ERR="$(mkstemp)"
- ./src/remembering -a something -b else 1>"$OUT" 2>"$ERR"
+ "$REMEMBERING" -a something -b else 1>"$OUT" 2>"$ERR"
STATUS=$?
assert_status 2
assert_usage "$ERR"
test_ok
}
-test_single_required_flag() {
- testing 'single required flag'
+test_missing_command() {
+ testing 'missing command'
+
+ N="$LINENO"
+ OUT="$(mkstemp)"
+ ERR="$(mkstemp)"
+ "$REMEMBERING" 1>"$OUT" 2>"$ERR"
+ STATUS=$?
+ assert_status 2
+ assert_usage "$ERR"
N="$LINENO"
OUT="$(mkstemp)"
ERR="$(mkstemp)"
- ./src/remembering -p valid-test-profile 1>"$OUT" 2>"$ERR"
+ "$REMEMBERING" -p valid-test-profile 1>"$OUT" 2>"$ERR"
STATUS=$?
assert_status 2
assert_usage "$ERR"
@@ -43,7 +53,7 @@ test_single_required_flag() {
N="$LINENO"
OUT="$(mkstemp)"
ERR="$(mkstemp)"
- ./src/remembering -c 'head -n' 1>"$OUT" 2>"$ERR"
+ "$REMEMBERING" -c 'head -n' 1>"$OUT" 2>"$ERR"
STATUS=$?
assert_status 2
assert_usage "$ERR"
@@ -57,7 +67,7 @@ test_flags_without_required_argument() {
N="$LINENO"
OUT="$(mkstemp)"
ERR="$(mkstemp)"
- ./src/remembering -pc 1>"$OUT" 2>"$ERR"
+ "$REMEMBERING" -pc 1>"$OUT" 2>"$ERR"
STATUS=$?
assert_status 2
assert_usage "$ERR"
@@ -65,7 +75,7 @@ test_flags_without_required_argument() {
N="$LINENO"
OUT="$(mkstemp)"
ERR="$(mkstemp)"
- ./src/remembering -cp 1>"$OUT" 2>"$ERR"
+ "$REMEMBERING" -cp 1>"$OUT" 2>"$ERR"
STATUS=$?
assert_status 2
assert_usage "$ERR"
@@ -73,7 +83,7 @@ test_flags_without_required_argument() {
N="$LINENO"
OUT="$(mkstemp)"
ERR="$(mkstemp)"
- ./src/remembering -p -c 1>"$OUT" 2>"$ERR"
+ "$REMEMBERING" -p -c 1>"$OUT" 2>"$ERR"
STATUS=$?
assert_status 2
assert_usage "$ERR"
@@ -87,7 +97,7 @@ test_valid_options() {
OUT="$(mkstemp)"
ERR="$(mkstemp)"
printf 'a\nb\nc\n' | \
- ./src/remembering \
+ "$REMEMBERING" \
-p "always-unique-$(uuid)" \
-- head -n1 \
1>"$OUT" 2>"$ERR"
@@ -98,69 +108,68 @@ test_valid_options() {
test_ok
}
-test_help_flags() {
- testing 'help flags'
+test_rejected_help_flags() {
+ testing 'help flags are rejected'
N="$LINENO"
OUT="$(mkstemp)"
ERR="$(mkstemp)"
- ./src/remembering -h 1>"$OUT" 2>"$ERR"
+ "$REMEMBERING" -h 1>"$OUT" 2>"$ERR"
STATUS=$?
- assert_status 0
- assert_empty_stderr
- assert_usage "$OUT"
+ assert_status 2
+ assert_empty_stdout
+ assert_usage "$ERR"
N="$LINENO"
OUT="$(mkstemp)"
ERR="$(mkstemp)"
- ./src/remembering --help 1>"$OUT" 2>"$ERR"
+ "$REMEMBERING" --help 1>"$OUT" 2>"$ERR"
STATUS=$?
- assert_status 0
- assert_empty_stderr
- assert_usage "$OUT"
+ assert_status 2
+ assert_empty_stdout
+ assert_usage "$ERR"
N="$LINENO"
OUT="$(mkstemp)"
ERR="$(mkstemp)"
- ./src/remembering -p profile -c command --help 1>"$OUT" 2>"$ERR"
+ "$REMEMBERING" -p profile --help -- head -n1 1>"$OUT" 2>"$ERR"
STATUS=$?
- assert_status 0
- assert_empty_stderr
- assert_usage "$OUT"
+ assert_status 2
+ assert_empty_stdout
+ assert_usage "$ERR"
N="$LINENO"
OUT="$(mkstemp)"
ERR="$(mkstemp)"
- ./src/remembering --help -p profile -c command 1>"$OUT" 2>"$ERR"
+ "$REMEMBERING" --help -p profile -- head -n1 1>"$OUT" 2>"$ERR"
STATUS=$?
- assert_status 0
- assert_empty_stderr
- assert_usage "$OUT"
+ assert_status 2
+ assert_empty_stdout
+ assert_usage "$ERR"
test_ok
}
-test_version_flags() {
- testing 'version flags'
- REGEX='^remembering [0-9\.]+ [0-9-]+$'
+test_rejected_version_flags() {
+ testing 'version flags are rejected'
N="$LINENO"
OUT="$(mkstemp)"
ERR="$(mkstemp)"
- ./src/remembering -V 1>"$OUT" 2>"$ERR"
+ "$REMEMBERING" -V 1>"$OUT" 2>"$ERR"
STATUS=$?
- assert_status 0
- assert_empty_stderr
- assert_grep_stdout "$REGEX"
+ assert_status 2
+ assert_empty_stdout
+ assert_usage "$ERR"
N="$LINENO"
OUT="$(mkstemp)"
ERR="$(mkstemp)"
- ./src/remembering --version 1>"$OUT" 2>"$ERR"
+ "$REMEMBERING" --version 1>"$OUT" 2>"$ERR"
STATUS=$?
- assert_status 0
- assert_empty_stderr
- assert_grep_stdout "$REGEX"
+ assert_status 2
+ assert_empty_stdout
+ assert_usage "$ERR"
test_ok
}
@@ -174,7 +183,7 @@ test_environment_variables_and_precedence() {
PROFILE='environment-variables-xdg'
XDG="$PWD/tests/test-profiles/xdg-test-$(uuid)"
printf 'a\n' | \
- XDG_DATA_HOME="$XDG" ./src/remembering \
+ XDG_DATA_HOME="$XDG" "$REMEMBERING" \
-p "$PROFILE" \
-- head -n1 \
1>"$OUT" 2>"$ERR"
@@ -190,7 +199,7 @@ test_environment_variables_and_precedence() {
PROFILE='environment-variables-home'
HHOME="$PWD/tests/test-profiles/home-test-$(uuid)"
printf 'b\n' | \
- HOME="$HHOME" XDG_DATA_HOME='' ./src/remembering \
+ HOME="$HHOME" XDG_DATA_HOME='' "$REMEMBERING" \
-p "$PROFILE" \
-- head -n1 \
1>"$OUT" 2>"$ERR"
@@ -207,7 +216,7 @@ test_environment_variables_and_precedence() {
HHOME="$PWD/tests/test-profiles/home-wins-over-xdg-test-$(uuid)"
XDG="$PWD/tests/test-profiles/xdg-wins-over-home-test-$(uuid)"
printf 'c\n' | \
- HOME="$HHOME" XDG_DATA_HOME="$XDG" ./src/remembering \
+ HOME="$HHOME" XDG_DATA_HOME="$XDG" "$REMEMBERING" \
-p "$PROFILE" \
-- head -n1 \
1>"$OUT" 2>"$ERR"
@@ -227,11 +236,10 @@ test_environment_variables_and_precedence() {
}
test_unsupported_long_flags
-test_missing_required_flags
-test_missing_required_flags
-test_single_required_flag
+test_unknown_flags
+test_missing_command
test_flags_without_required_argument
test_valid_options
-test_help_flags
-test_version_flags
+test_rejected_help_flags
+test_rejected_version_flags
test_environment_variables_and_precedence
diff --git a/tests/functional/pick-roundtrip/main.go b/tests/functional/pick-roundtrip/main.go
new file mode 100644
index 0000000..0b02dc7
--- /dev/null
+++ b/tests/functional/pick-roundtrip/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "remembering"
+
+func main() {
+ remembering.MainTest()
+}
diff --git a/tests/functional/pick-roundtrip/remembering.go b/tests/functional/pick-roundtrip/remembering.go
new file mode 100644
index 0000000..e2d54da
--- /dev/null
+++ b/tests/functional/pick-roundtrip/remembering.go
@@ -0,0 +1,129 @@
+package remembering
+
+import (
+ "fmt"
+ "os"
+ "reflect"
+ "strings"
+)
+
+
+
+func showColour() bool {
+ return os.Getenv("NO_COLOUR") == ""
+}
+
+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 pick(menu string, command ...string) (int, string, string) {
+ out := strings.Builder{}
+ errW := strings.Builder{}
+ rc := run(envT{
+ allArgs: append(
+ []string{"remembering", "-p", "func", "--"},
+ command...,
+ ),
+ in: strings.NewReader(menu),
+ out: &out,
+ err: &errW,
+ })
+ return rc, out.String(), errW.String()
+}
+
+func profileBytes() string {
+ path, err := profilePath("func")
+ if err != nil {
+ panic(err)
+ }
+ data, err := os.ReadFile(path)
+ if err != nil {
+ panic(err)
+ }
+ return string(data)
+}
+
+
+
+func MainTest() {
+ testing("a session of picks shapes the ranking", func() {
+ tmp, err := os.MkdirTemp(
+ "", "remembering-functional-",
+ )
+ assertEq(err, nil)
+ defer os.RemoveAll(tmp)
+ saved := os.Getenv("XDG_DATA_HOME")
+ os.Setenv("XDG_DATA_HOME", tmp)
+ defer os.Setenv("XDG_DATA_HOME", saved)
+
+ menu := "a\nb\nc\nd\ne\n"
+
+ rc, out, errW := pick(menu, "grep", "-F", "c")
+ assertEq(rc, 0)
+ assertEq(out, "c\n")
+ assertEq(errW, "")
+ assertEq(
+ profileBytes(),
+ "0 profile a\n"+
+ "0 profile b\n"+
+ "1 profile c\n"+
+ "0 profile d\n"+
+ "0 profile e\n",
+ )
+
+ // the learnt pick now ranks first
+ rc, out, errW = pick(menu, "head", "-n1")
+ assertEq(rc, 0)
+ assertEq(out, "c\n")
+ assertEq(errW, "")
+ assertEq(
+ profileBytes(),
+ "0 profile a\n"+
+ "0 profile b\n"+
+ "2 profile c\n"+
+ "0 profile d\n"+
+ "0 profile e\n",
+ )
+
+ // a cancelled menu forwards the status and
+ // learns nothing
+ rc, out, _ = pick(menu, "sh", "-c", "exit 3")
+ assertEq(rc, 3)
+ assertEq(out, "")
+ assertEq(
+ strings.Contains(
+ profileBytes(), "2 profile c",
+ ),
+ true,
+ )
+ })
+}
diff --git a/tests/fuzz/profile/main.go b/tests/fuzz/profile/main.go
new file mode 100644
index 0000000..0b02dc7
--- /dev/null
+++ b/tests/fuzz/profile/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "remembering"
+
+func main() {
+ remembering.MainTest()
+}
diff --git a/tests/fuzz/profile/remembering.go b/tests/fuzz/profile/remembering.go
new file mode 100644
index 0000000..aab4ac4
--- /dev/null
+++ b/tests/fuzz/profile/remembering.go
@@ -0,0 +1,76 @@
+package remembering
+
+import (
+ "os"
+ "reflect"
+ "strings"
+ "testing"
+ "testing/internal/testdeps"
+)
+
+
+
+func fn(f *testing.F) {
+ f.Add("0 profile a\n1 profile b\n", "c\nd", "a")
+ f.Add("", "x", "x")
+ f.Fuzz(func(
+ t *testing.T,
+ content string,
+ input string,
+ choice string,
+ ) {
+ profile := parseProfile(content)
+ again := parseProfile(serializeProfile(profile))
+ if !reflect.DeepEqual(again, profile) {
+ t.Fatalf(
+ "roundtrip: %#v != %#v",
+ again, profile,
+ )
+ }
+
+ if choice == "" ||
+ strings.Contains(choice, "\n") {
+ return
+ }
+ lines := splitLines(input)
+ next := nextProfile(profile, lines, choice)
+
+ found := false
+ for _, entry := range next {
+ if entry.text == choice {
+ found = true
+ }
+ }
+ if !found {
+ t.Fatalf(
+ "pick %q not learnt in %#v",
+ choice, next,
+ )
+ }
+
+ reNext := parseProfile(serializeProfile(next))
+ if !reflect.DeepEqual(reNext, next) {
+ t.Fatalf(
+ "next roundtrip: %#v != %#v",
+ reNext, next,
+ )
+ }
+ })
+}
+
+
+
+func MainTest() {
+ fuzzTargets := []testing.InternalFuzzTarget{
+ {"fn", fn},
+ }
+
+ deps := testdeps.TestDeps{}
+ tests := []testing.InternalTest {}
+ benchmarks := []testing.InternalBenchmark{}
+ examples := []testing.InternalExample {}
+ m := testing.MainStart(
+ deps, tests, benchmarks, fuzzTargets, examples,
+ )
+ os.Exit(m.Run())
+}
diff --git a/tests/main.go b/tests/main.go
new file mode 100644
index 0000000..0b02dc7
--- /dev/null
+++ b/tests/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "remembering"
+
+func main() {
+ remembering.MainTest()
+}
diff --git a/tests/ranking.sh b/tests/ranking.sh
index af26c94..45cb0ec 100755
--- a/tests/ranking.sh
+++ b/tests/ranking.sh
@@ -3,6 +3,8 @@ set -eu
. tests/lib.sh
+REMEMBERING="$PWD/remembering.bin"
+
export XDG_DATA_HOME="$PWD/tests/test-profiles"
export LANG=C.UTF-8
@@ -38,7 +40,7 @@ pick_x() {
PICK="$1"
echo "${2:-$INPUT}" | \
- ./src/remembering \
+ "$REMEMBERING" \
-p "$PROFILE" \
-- sh -c "tee -a /dev/stderr | grep -F \"$PICK\"" \
1>"$OUT" 2>"$ERR"
@@ -83,7 +85,7 @@ test_picking_first_makes_it_be_always_first() {
PROFILE="always-picks-first-$(uuid)"
for _ in $(seq 10); do
printf 'always-picked\nnever-picked\n' | \
- ./src/remembering \
+ "$REMEMBERING" \
-p "$PROFILE" \
-- head -n1 \
1>"$OUT" 2>"$ERR"
@@ -203,7 +205,7 @@ d
e'
echo "$INPUT" | \
- ./src/remembering \
+ "$REMEMBERING" \
-p "$PROFILE" \
-- echo f \
1>"$OUT" 2>"$ERR"
@@ -277,7 +279,7 @@ test_stdin_is_empty() {
ERR="$(mkstemp)"
printf '' | \
- ./src/remembering \
+ "$REMEMBERING" \
-p "$PROFILE" \
-- sh -c 'tee -a /dev/stderr | head -n1' \
1>"$OUT" 2>"$ERR"
@@ -340,7 +342,7 @@ test_really_long_list() {
PROFILE="really-long-list-$(uuid)"
N=999999
seq "$N" | \
- ./src/remembering \
+ "$REMEMBERING" \
-p "$PROFILE" \
-- tail -n1 \
1>"$OUT" 2>"$ERR"
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()
+}
diff --git a/tests/signals.sh b/tests/signals.sh
index 36491cf..e1cb063 100755
--- a/tests/signals.sh
+++ b/tests/signals.sh
@@ -3,6 +3,8 @@ set -u
. tests/lib.sh
+REMEMBERING="$PWD/remembering.bin"
+
XDG_DATA_HOME="$PWD/tests/test-profiles/signals-$(uuid)"
export XDG_DATA_HOME
@@ -10,15 +12,15 @@ test_status_is_zero_when_command_is_successful() {
testing 'status is 0 when command is successful'
N="$LINENO"
- printf 'a\n' | ./src/remembering -pp1 -- head -n1 1>/dev/null 2>/dev/null
+ printf 'a\n' | "$REMEMBERING" -p p1 -- head -n1 1>/dev/null 2>/dev/null
STATUS=$?
assert_status 0
- printf '' | ./src/remembering -pp2 -- true 1>/dev/null 2>/dev/null
+ printf '' | "$REMEMBERING" -p p2 -- true 1>/dev/null 2>/dev/null
STATUS=$?
assert_status 0
- seq 9 | ./src/remembering -pp3 -- grep 7 1>/dev/null 2>/dev/null
+ seq 9 | "$REMEMBERING" -p p3 -- grep 7 1>/dev/null 2>/dev/null
STATUS=$?
assert_status 0
@@ -29,7 +31,7 @@ test_status_is_forwarded_from_command() {
testing 'status is forwarded from command'
N="$LINENO"
for status in $(seq 1 125); do
- printf '' | ./src/remembering -pp4 -- sh -c "exit $status" 1>/dev/null 2>/dev/null
+ printf '' | "$REMEMBERING" -p p4 -- sh -c "exit $status" 1>/dev/null 2>/dev/null
STATUS=$?
assert_status "$status"
done