It requires x11-display, names itself in TERMINAL, and contributes its X resources to the xresources slot normal: today's palette and clipboard keys, JetBrainsMono Nerd Font, 10000 lines of scrollback, all scoped to XTerm* instead of every Xt program. It owns no file. The tools are the seat's open, which starts a terminal through the account's service manager so it outlives the runtime's restarts, and font (in force, what fontconfig resolves it to, set for new terminals) and colours.
211 lines
7.2 KiB
Go
211 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"xterm/internal/desktop"
|
|
)
|
|
|
|
type manifest struct {
|
|
Capabilities []string `json:"capabilities"`
|
|
Requires []string `json:"requires"`
|
|
Claims []struct {
|
|
Name string `json:"name"`
|
|
Serves []string `json:"serves"`
|
|
} `json:"claims"`
|
|
Tools []string `json:"tools"`
|
|
Environment struct {
|
|
Variables map[string]string `json:"variables"`
|
|
} `json:"environment"`
|
|
Shell []struct {
|
|
For, Slot, Code string
|
|
} `json:"shell"`
|
|
Resources []map[string]any `json:"resources"`
|
|
Build struct {
|
|
Artifacts []map[string]any `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
|
|
func readManifest(t *testing.T) manifest {
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m manifest
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func TestItHoldsTheTerminalSeatOnAnXDisplayAndNamesItselfTheTerminal(t *testing.T) {
|
|
m := readManifest(t)
|
|
if len(m.Claims) != 1 || m.Claims[0].Name != "node-terminal-emulator" || strings.Join(m.Claims[0].Serves, ",") != "open" {
|
|
t.Fatalf("%+v", m.Claims)
|
|
}
|
|
if strings.Join(m.Requires, ",") != "x11-display" || m.Environment.Variables["TERMINAL"] != "xterm" || len(m.Environment.Variables) != 1 {
|
|
t.Fatalf("%v %v", m.Requires, m.Environment)
|
|
}
|
|
served := map[string]bool{}
|
|
for _, tool := range tools(xterm{}) {
|
|
served[tool.Name] = true
|
|
}
|
|
if !served["node-terminal-emulator.open"] || len(served) != len(m.Tools)+1 {
|
|
t.Fatalf("%v %v", served, m.Tools)
|
|
}
|
|
for _, n := range m.Tools {
|
|
if !served[n] {
|
|
t.Errorf("%s", n)
|
|
}
|
|
}
|
|
if len(m.Resources) != 1 || m.Resources[0]["package"] != "xterm" {
|
|
t.Fatalf("%v", m.Resources)
|
|
}
|
|
if a := m.Build.Artifacts[0]; a["binary"] != "xterm-tools" || a["from"] != "cmd/xterm-tools" {
|
|
t.Fatalf("a binary not named xterm, so nothing looking for xterm by name finds the tools: %v", a)
|
|
}
|
|
}
|
|
|
|
func TestItsResourcesAreOneContributionInTheNormalSlotScopedToXTerm(t *testing.T) {
|
|
m := readManifest(t)
|
|
if len(m.Shell) != 1 || m.Shell[0].For != "xresources" || m.Shell[0].Slot != "normal" {
|
|
t.Fatalf("%+v", m.Shell)
|
|
}
|
|
if m.Shell[0].Code != Resources() {
|
|
t.Fatal("the manifest's resources are not the ones the bundle describes: regenerate the manifest from Resources()")
|
|
}
|
|
for _, l := range strings.Split(strings.TrimSpace(Resources()), "\n") {
|
|
switch {
|
|
case strings.HasPrefix(l, "!"), strings.HasPrefix(l, " "):
|
|
case strings.HasPrefix(l, "#"):
|
|
t.Errorf("merged with -nocpp, a # line is skipped: %q", l)
|
|
case !strings.HasPrefix(l, "XTerm*"):
|
|
t.Errorf("a resource of another program, or of every one: %q", l)
|
|
}
|
|
}
|
|
if !strings.Contains(Resources(), "XTerm*faceName: JetBrainsMono Nerd Font") || !strings.Contains(Resources(), "copy-selection(CLIPBOARD)") {
|
|
t.Fatal("the chosen face and the clipboard keys")
|
|
}
|
|
}
|
|
|
|
type fake struct {
|
|
ran []string
|
|
stdin []string
|
|
query string
|
|
}
|
|
|
|
func (f *fake) desk(session bool) desktop.Desk {
|
|
return desktop.Desk{
|
|
Find: func() (*desktop.Session, error) {
|
|
if !session {
|
|
return nil, &desktop.NoSession{Reason: "none"}
|
|
}
|
|
return &desktop.Session{Display: ":1", XAuthority: "/h/.Xauthority", Type: "x11", RuntimeDir: "/run/user/1000", Bus: "unix:path=/run/user/1000/bus"}, nil
|
|
},
|
|
Run: func(_ context.Context, _ []string, stdin []byte, name string, args ...string) desktop.Result {
|
|
f.ran = append(f.ran, strings.Join(append([]string{name}, args...), " "))
|
|
f.stdin = append(f.stdin, string(stdin))
|
|
switch name {
|
|
case "xrdb":
|
|
return desktop.Result{Stdout: f.query}
|
|
case "fc-list":
|
|
if strings.Contains(args[0], "Missing") {
|
|
return desktop.Result{}
|
|
}
|
|
return desktop.Result{Stdout: args[0] + "\n"}
|
|
case "fc-match":
|
|
return desktop.Result{Stdout: "JetBrainsMono Nerd Font|Regular|/usr/share/fonts/TTF/J.ttf"}
|
|
}
|
|
return desktop.Result{}
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestOpenHandsTheTerminalToTheAccountsServiceManagerWithTheSessionsDisplay(t *testing.T) {
|
|
f := &fake{}
|
|
dir := t.TempDir()
|
|
got, err := xterm{d: f.desk(true)}.open(context.Background(), desktop.Args{"directory": dir, "command": "htop", "hold": true, "title": "top"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(f.ran) != 1 || !strings.HasPrefix(f.ran[0], "systemd-run --user --collect --quiet --unit=mesh-xterm-") {
|
|
t.Fatalf("%q", f.ran)
|
|
}
|
|
for _, want := range []string{"--setenv=DISPLAY=:1", "--setenv=XAUTHORITY=/h/.Xauthority", "-- xterm -title top -hold -e /bin/sh -c", "sh " + dir + " htop"} {
|
|
if !strings.Contains(f.ran[0], want) {
|
|
t.Errorf("lacks %q: %s", want, f.ran[0])
|
|
}
|
|
}
|
|
if !strings.Contains(asJSON(got), `"unit":"mesh-xterm-`) {
|
|
t.Fatalf("%s", asJSON(got))
|
|
}
|
|
if _, err := (xterm{d: f.desk(true)}).open(context.Background(), desktop.Args{"directory": "/no/such/dir"}); err == nil {
|
|
t.Fatal("a directory that is not there")
|
|
}
|
|
if _, err := (xterm{d: (&fake{}).desk(false)}).open(context.Background(), desktop.Args{}); !desktop.IsNoSession(err) {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
}
|
|
|
|
func TestTheTerminalsScriptStartsInTheDirectoryAndRunsTheCommandThroughTheShell(t *testing.T) {
|
|
dir := t.TempDir()
|
|
args := OpenArgs(dir, "pwd > out", "", false)
|
|
sh := args[len(args)-6:] // /bin/sh -c script sh dir command
|
|
cmd := exec.Command(sh[0], sh[1:]...)
|
|
cmd.Env = []string{"SHELL=/bin/sh", "PATH=/usr/bin:/bin", "HOME=" + dir}
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("%v %s", err, out)
|
|
}
|
|
b, _ := os.ReadFile(filepath.Join(dir, "out"))
|
|
if strings.TrimSpace(string(b)) != dir {
|
|
t.Fatalf("%q", b)
|
|
}
|
|
}
|
|
|
|
func TestFontAndColoursAreReadAsAnXTermSeesThem(t *testing.T) {
|
|
q := "*faceName:\tHack Nerd Font\nXTerm*faceName:\tJetBrainsMono Nerd Font\nXTerm*faceSize:\t12\n" +
|
|
"XTerm*background:\t#000000\n*foreground:\trgb:a8/a8/a8\nXTerm*color1:\trgb:ff/00/00\n"
|
|
if Effective(q, "faceName") != "JetBrainsMono Nerd Font" || Effective(q, "foreground") != "rgb:a8/a8/a8" {
|
|
t.Fatal("XTerm's own value outranks the global one; the global one stands alone")
|
|
}
|
|
for in, want := range map[string]string{"rgb:9e/18/28": "#9e1828", "rgb:f/0/8": "#ff0088", "rgb:ffff/0000/8080": "#ff0080", "#ABCDEF": "#abcdef", "red": "red"} {
|
|
if got := Hex(in); got != want {
|
|
t.Errorf("%s: %s", in, got)
|
|
}
|
|
}
|
|
f := &fake{query: q}
|
|
got, err := xterm{d: f.desk(true)}.colours(context.Background(), desktop.Args{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
j := asJSON(got)
|
|
if !strings.Contains(j, `{"name":"color1","value":"#ff0000","default":"#9e1828","differs":true}`) || !strings.Contains(j, `{"name":"foreground","value":"#a8a8a8","default":"#a8a8a8"}`) {
|
|
t.Fatalf("%s", j)
|
|
}
|
|
if _, err := (xterm{d: f.desk(true)}).font(context.Background(), desktop.Args{"face": "Missing Font"}); err == nil {
|
|
t.Fatal("a face that is not installed")
|
|
}
|
|
if _, err := (xterm{d: f.desk(true)}).font(context.Background(), desktop.Args{"size": 99.0}); err == nil {
|
|
t.Fatal("size out of range")
|
|
}
|
|
f.ran, f.stdin = nil, nil
|
|
got, err = xterm{d: f.desk(true)}.font(context.Background(), desktop.Args{"face": "Inter", "size": 13.0})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if f.stdin[1] != "XTerm*faceName: Inter\nXTerm*faceSize: 13\n" || !strings.Contains(asJSON(got), `"lasts"`) {
|
|
t.Fatalf("%q %s", f.stdin, asJSON(got))
|
|
}
|
|
}
|
|
|
|
func asJSON(v any) string {
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
}
|