Replaces the AUR greenclip (declared absent) with the official clipmenu, started once from the session's xinitrc slot, its menu the launcher's dmenu command bound as an i3 drop-in, its history in the runtime directory. Go tools read and change clipmenu's own store under its lock.
164 lines
5.4 KiB
Go
164 lines
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const nobody = 4194400
|
|
|
|
func TestEntryIdsAreWhatClipmenuNamesItsFiles(t *testing.T) {
|
|
for _, line := range []string{"hello", "", "two words (3 lines)", "ünïcode ✓", strings.Repeat("x", 300)} {
|
|
out, err := exec.Command("bash", "-c", `cksum <<< "$1"`, "_", line).Output()
|
|
if err != nil {
|
|
t.Skip("bash or cksum is missing here")
|
|
}
|
|
if got, want := entryID(line), strings.TrimSpace(string(out)); got != want {
|
|
t.Errorf("%q: %s, cksum says %s", line, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// store makes clipmenu's store as clipmenud leaves it, for the account the tools run as.
|
|
func store(t *testing.T, clips map[string]string, order ...string) string {
|
|
t.Helper()
|
|
fakeMachine(t)
|
|
runtime := filepath.Join(runUserDir, strconv.Itoa(os.Getuid()))
|
|
t.Setenv("USER", "op")
|
|
t.Setenv("CM_DIR", "")
|
|
dir := filepath.Join(runtime, "clipmenu.6.op")
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(runtime, "bus"), nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var cache strings.Builder
|
|
for i, line := range order {
|
|
cache.WriteString(strconv.FormatInt(1_700_000_000_000_000_000+int64(i)*1_000_000_000, 10) + " " + line + "\n")
|
|
if err := os.WriteFile(filepath.Join(dir, entryID(line)), []byte(clips[line]), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "line_cache"), []byte(cache.String()), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return dir
|
|
}
|
|
|
|
func TestTheHistoryIsNewestFirstOnceEachWithItsText(t *testing.T) {
|
|
store(t, map[string]string{"first": "first", "second (2 lines)": "second\nline two"}, "first", "second (2 lines)", "first")
|
|
fakeProcess(t, nobody, "clipmenud")
|
|
h, err := History(10, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !h.Collecting || h.Total != 2 || h.Entries[0].Line != "first" || h.Entries[1].Text != "second\nline two" || h.Entries[1].Bytes != 15 {
|
|
t.Fatalf("%+v", h)
|
|
}
|
|
if h, _ := History(1, 3); len(h.Entries) != 1 || h.Entries[0].Text != "fir" || !h.Entries[0].Truncated {
|
|
t.Fatalf("limited and cut: %+v", h)
|
|
}
|
|
if h, _ := History(10, 0); h.Entries[0].Text != "" || h.Entries[0].Bytes != 5 {
|
|
t.Fatalf("without text: %+v", h)
|
|
}
|
|
}
|
|
|
|
func TestAnEntryIsDeletedByIdOrLineWithItsText(t *testing.T) {
|
|
dir := store(t, map[string]string{"a": "a", "b": "b", "c": "c"}, "a", "b", "c", "a")
|
|
r, err := Delete(entryID("a"), "")
|
|
if err != nil || r.Removed != 2 || r.Remaining != 2 {
|
|
t.Fatalf("by id, both copies: %+v, %v", r, err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, entryID("a"))); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatal("the text stayed")
|
|
}
|
|
if r, err := Delete("", "b"); err != nil || r.Removed != 1 || r.Remaining != 1 {
|
|
t.Fatalf("by line: %+v, %v", r, err)
|
|
}
|
|
if _, err := Delete("", "zzz"); err == nil {
|
|
t.Fatal("a missing entry was reported deleted")
|
|
}
|
|
if _, err := Delete("x", "y"); err == nil {
|
|
t.Fatal("both an id and a line were accepted")
|
|
}
|
|
cache, _ := os.ReadFile(filepath.Join(dir, "line_cache"))
|
|
if !strings.HasSuffix(string(cache), " c\n") || strings.Count(string(cache), "\n") != 1 {
|
|
t.Fatalf("line cache: %q", cache)
|
|
}
|
|
}
|
|
|
|
func TestClearForgetsEverythingButKeepsTheDaemonsLocks(t *testing.T) {
|
|
dir := store(t, map[string]string{"a": "a", "b": "b"}, "a", "b")
|
|
if err := os.WriteFile(filepath.Join(dir, "session_lock"), nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r, err := Clear()
|
|
if err != nil || r.Removed != 2 {
|
|
t.Fatalf("%+v, %v", r, err)
|
|
}
|
|
left, _ := os.ReadDir(dir)
|
|
var names []string
|
|
for _, f := range left {
|
|
names = append(names, f.Name())
|
|
}
|
|
if strings.Join(names, ",") != "line_cache,lock,session_lock" {
|
|
t.Fatalf("left: %v", names)
|
|
}
|
|
}
|
|
|
|
func TestCopyHandsTheTextToAnOwnerUnderTheAccountsServiceManager(t *testing.T) {
|
|
store(t, nil)
|
|
fakeProcess(t, nobody, "i3", "DISPLAY=:1")
|
|
bin := fakeBinaries(t, map[string]string{"systemctl": "true", "systemd-run": `echo "$*" > "$LOG"; for last; do :; done; cat "$last" > "$LOG.text"`})
|
|
t.Setenv("LOG", filepath.Join(bin, "log"))
|
|
r, err := Copy("secret-free text")
|
|
if err != nil || r.Bytes != 16 || !strings.HasPrefix(r.Unit, "clipmenu-copy-") {
|
|
t.Fatalf("%+v, %v", r, err)
|
|
}
|
|
asked, _ := os.ReadFile(filepath.Join(bin, "log"))
|
|
if !strings.Contains(string(asked), "--setenv=DISPLAY=:1 -- /bin/sh -c xsel --nodetach --input --clipboard") {
|
|
t.Fatalf("asked: %s", asked)
|
|
}
|
|
handed, _ := os.ReadFile(filepath.Join(bin, "log.text"))
|
|
if string(handed) != "secret-free text" {
|
|
t.Fatalf("handed over: %q", handed)
|
|
}
|
|
if _, err := Copy(""); err == nil {
|
|
t.Fatal("empty text was accepted")
|
|
}
|
|
}
|
|
|
|
func TestPasteReadsTheClipboardOrSaysThereIsNoSession(t *testing.T) {
|
|
fakeMachine(t)
|
|
if _, err := Paste(); !errors.Is(err, ErrNoSession) {
|
|
t.Fatal(err)
|
|
}
|
|
fakeProcess(t, nobody, "i3", "DISPLAY=:1")
|
|
fakeBinaries(t, map[string]string{"xsel": `[ "$*" = "--output --clipboard" ] && printf 'on the clipboard'`})
|
|
p, err := Paste()
|
|
if err != nil || p.Text != "on the clipboard" || p.Bytes != 16 {
|
|
t.Fatalf("%+v, %v", p, err)
|
|
}
|
|
}
|
|
|
|
func TestWithoutAStoreTheChangesSayWhy(t *testing.T) {
|
|
fakeMachine(t)
|
|
runtime := filepath.Join(runUserDir, strconv.Itoa(os.Getuid()))
|
|
if err := os.MkdirAll(runtime, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("USER", "op")
|
|
if _, err := Clear(); err == nil || !strings.Contains(err.Error(), "clipmenud has not run") {
|
|
t.Fatal(err)
|
|
}
|
|
if h, err := History(5, 0); err != nil || h.Total != 0 || h.Collecting {
|
|
t.Fatalf("an empty history: %+v, %v", h, err)
|
|
}
|
|
}
|