303 lines
11 KiB
Go
303 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestTheManifestInstallsTheFiveDecidedFacesAndOwnsOneAccountFile(t *testing.T) {
|
|
m := readManifest(t)
|
|
holdsTheBundle(t, m, "fonts")
|
|
want := "inter-font,noto-fonts,noto-fonts-emoji,ttf-jetbrains-mono-nerd,ttf-nerd-fonts-symbols"
|
|
if got := strings.Join(m.packages(), ","); got != want {
|
|
t.Errorf("packages %s, want %s", got, want)
|
|
}
|
|
files := 0
|
|
for _, r := range m.Resources {
|
|
if r["type"] == "file" {
|
|
files++
|
|
}
|
|
if r["type"] == "service" || r["type"] == "directory" {
|
|
t.Errorf("a font needs no %s: %v", r["type"], r["id"])
|
|
}
|
|
}
|
|
f := m.resource("defaults")
|
|
if files != 1 || f == nil {
|
|
t.Fatalf("one owned file, got %d", files)
|
|
}
|
|
if f["path"] != "${machine:account-home}/"+ConfigFile || f["owner"] != "${machine:account}" || f["mode"] != "0644" {
|
|
t.Errorf("the file is the account's, at %s: %v", ConfigFile, f)
|
|
}
|
|
if _, into := f["into"]; into {
|
|
t.Error("the file is the module's whole, not written into")
|
|
}
|
|
}
|
|
|
|
// fontconfig's document, as far as the module's file uses it.
|
|
type fcDoc struct {
|
|
Aliases []struct {
|
|
Binding string `xml:"binding,attr"`
|
|
Family string `xml:"family"`
|
|
Prefer []string `xml:"prefer>family"`
|
|
Accept []string `xml:"accept>family"`
|
|
} `xml:"alias"`
|
|
Matches []struct {
|
|
Target string `xml:"target,attr"`
|
|
Edits []struct {
|
|
Name string `xml:"name,attr"`
|
|
Mode string `xml:"mode,attr"`
|
|
String string `xml:"string"`
|
|
} `xml:"edit"`
|
|
} `xml:"match"`
|
|
}
|
|
|
|
func TestTheFontconfigFileMapsEachGenericToTheDecidedFaceStronglyAndFallsBackLast(t *testing.T) {
|
|
m := readManifest(t)
|
|
content := templateText(t, m.resource("defaults"))
|
|
if !strings.HasPrefix(content, "<?xml") || !strings.Contains(content, `<!DOCTYPE fontconfig`) {
|
|
t.Fatal("not a fontconfig document")
|
|
}
|
|
if strings.Contains(content, "$") {
|
|
t.Error("the file holds no placeholder: it is the same on every machine")
|
|
}
|
|
var doc fcDoc
|
|
if err := xml.Unmarshal([]byte(content), &doc); err != nil {
|
|
t.Fatalf("not well-formed: %v", err)
|
|
}
|
|
firstPrefer := map[string]string{}
|
|
order := []string{}
|
|
for _, a := range doc.Aliases {
|
|
order = append(order, a.Family)
|
|
if len(a.Prefer) > 0 {
|
|
firstPrefer[a.Family] = a.Prefer[0]
|
|
if a.Binding != "same" {
|
|
t.Errorf("%s is bound %q: a weakly added face loses to the distribution's choice", a.Family, a.Binding)
|
|
}
|
|
}
|
|
}
|
|
for generic, face := range Decided {
|
|
if firstPrefer[generic] != face {
|
|
t.Errorf("%s prefers %q, decided %q", generic, firstPrefer[generic], face)
|
|
}
|
|
}
|
|
// The retired families lead to monospace, and are placed before the monospace rule.
|
|
mono := strings.Index(strings.Join(order, "|"), "|monospace|")
|
|
for _, r := range []string{"Hack Nerd Font", "MesloLGS NF", "Iosevka Nerd Font"} {
|
|
at := strings.Index(strings.Join(order, "|"), r)
|
|
if at < 0 || at > mono {
|
|
t.Errorf("%s is not mapped to monospace before the monospace rule", r)
|
|
}
|
|
}
|
|
if len(doc.Matches) != 1 || doc.Matches[0].Target != "pattern" {
|
|
t.Fatalf("one pattern match for the fallbacks: %+v", doc.Matches)
|
|
}
|
|
got := []string{}
|
|
for _, e := range doc.Matches[0].Edits {
|
|
if e.Name != "family" || e.Mode != "append_last" {
|
|
t.Errorf("a fallback is appended last, never prepended: %+v", e)
|
|
}
|
|
got = append(got, e.String)
|
|
}
|
|
if strings.Join(got, ",") != "Symbols Nerd Font,Noto Color Emoji" {
|
|
t.Errorf("fallbacks %v", got)
|
|
}
|
|
}
|
|
|
|
func TestFamiliesGroupsFilesAndTellsAHandCopiedFaceFromAPackagedOne(t *testing.T) {
|
|
t.Setenv("MESH_OPERATOR_HOME", "/home/op")
|
|
using(t, func(line string, c Cmd) Result {
|
|
return ok("Inter\tRegular\t\t/usr/share/fonts/inter/Inter.ttc\n" +
|
|
"Inter\tBold\t\t/usr/share/fonts/inter/Inter-Bold.ttc\n" +
|
|
"Hack Nerd Font\tRegular\t100\t/home/op/.local/share/fonts/Hack.ttf\n" +
|
|
"Noto Sans Mono\tRegular\t100\t/usr/share/fonts/noto/NotoSansMono.ttf\n")
|
|
})
|
|
got, err := Families("", 10)
|
|
if err != nil || got.Count != 3 {
|
|
t.Fatalf("%+v %v", got, err)
|
|
}
|
|
if f := got.Families[0]; f.Family != "Hack Nerd Font" || f.Source != "account" || !f.Monospace {
|
|
t.Errorf("%+v", f)
|
|
}
|
|
if f := got.Families[1]; f.Family != "Inter" || f.Files != 2 || f.Monospace || f.Source != "package" || len(f.Styles) != 2 {
|
|
t.Errorf("%+v", f)
|
|
}
|
|
got, _ = Families("noto", 10)
|
|
if got.Count != 1 {
|
|
t.Errorf("filtered: %+v", got)
|
|
}
|
|
got, _ = Families("", 1)
|
|
if !got.Truncated || len(got.Families) != 1 || got.Count != 3 {
|
|
t.Errorf("bounded: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestMatchSaysWhetherEachGenericIsTheDecidedFace(t *testing.T) {
|
|
f := using(t, func(line string, c Cmd) Result {
|
|
switch c.Args[len(c.Args)-1] {
|
|
case "monospace":
|
|
return ok("JetBrainsMono Nerd Font\tRegular\t/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Regular.ttf")
|
|
case "sans-serif", "system-ui":
|
|
return ok("Noto Sans\tRegular\t/usr/share/fonts/noto/NotoSans-Regular.ttf")
|
|
case "serif":
|
|
return ok("Noto Serif\tRegular\t/usr/share/fonts/noto/NotoSerif-Regular.ttf")
|
|
}
|
|
return ok("Noto Color Emoji\tRegular\t/usr/share/fonts/noto/NotoColorEmoji.ttf")
|
|
})
|
|
got, err := Match(nil)
|
|
if err != nil || len(got.Resolved) != 5 || got.AllAsDecided == nil || *got.AllAsDecided {
|
|
t.Fatalf("%+v %v", got, err)
|
|
}
|
|
if r := got.Resolved[0]; !*r.AsDecided || r.Source != "package" {
|
|
t.Errorf("monospace: %+v", r)
|
|
}
|
|
if r := got.Resolved[1]; *r.AsDecided || r.Decided != "Inter" {
|
|
t.Errorf("sans-serif is not Inter here: %+v", r)
|
|
}
|
|
if len(f.asked) != 5 {
|
|
t.Errorf("one fc-match per generic: %v", f.lines())
|
|
}
|
|
got, _ = Match([]string{"Inter:bold"})
|
|
if got.AllAsDecided != nil || got.Resolved[0].AsDecided != nil {
|
|
t.Errorf("a pattern of the caller's own has no decided face: %+v", got)
|
|
}
|
|
if _, err := Match([]string{"--help"}); err == nil {
|
|
t.Error("an option as a pattern")
|
|
}
|
|
}
|
|
|
|
func TestMatchAnEmptyAnswerIsAnError(t *testing.T) {
|
|
using(t, func(string, Cmd) Result { return ok("") })
|
|
if _, err := Match(nil); err == nil {
|
|
t.Fatal("an empty fc-match answer was read as a face")
|
|
}
|
|
}
|
|
|
|
func TestGlyphReadsACharacterOrACodePointAndAsksForIt(t *testing.T) {
|
|
for in, want := range map[string]rune{"\uf120": 0xf120, "U+F120": 0xf120, "0x1f600": 0x1f600, "a": 'a', "é": 'é'} {
|
|
got, err := codePoint(in)
|
|
if err != nil || got != want {
|
|
t.Errorf("%q: %x %v", in, got, err)
|
|
}
|
|
}
|
|
for _, bad := range []string{"ab", "U+ZZ", "U+0", "120"} {
|
|
if _, err := codePoint(bad); err == nil {
|
|
t.Errorf("%q was read as a code point", bad)
|
|
}
|
|
}
|
|
f := using(t, func(line string, c Cmd) Result {
|
|
if c.Name == "fc-list" {
|
|
return ok("Symbols Nerd Font\tRegular\t100\t/usr/share/fonts/TTF/SymbolsNerdFont-Regular.ttf\n" +
|
|
"JetBrainsMono Nerd Font\tBold\t100\t/usr/share/fonts/TTF/a.ttf\nJetBrainsMono Nerd Font\tRegular\t100\t/usr/share/fonts/TTF/b.ttf\n")
|
|
}
|
|
return ok("JetBrainsMono Nerd Font\tRegular\t/usr/share/fonts/TTF/b.ttf")
|
|
})
|
|
got, err := Glyph("U+F120")
|
|
if err != nil || got.Count != 2 || got.CodePoint != "U+F120" || got.DrawnBy["monospace"] != "JetBrainsMono Nerd Font" {
|
|
t.Fatalf("%+v %v", got, err)
|
|
}
|
|
if !strings.Contains(f.lines()[0], ":charset=f120") || !strings.Contains(f.lines()[1], "monospace:charset=f120") {
|
|
t.Errorf("asked %v", f.lines())
|
|
}
|
|
}
|
|
|
|
func TestSourcesMarksDuplicatesEncodedNamesPackagedAndRetiredFacesForRemoval(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("MESH_OPERATOR_HOME", home)
|
|
dir := filepath.Join(home, ".local/share/fonts")
|
|
_ = os.MkdirAll(dir, 0o755)
|
|
write := func(name, body string) string {
|
|
p := filepath.Join(dir, name)
|
|
_ = os.WriteFile(p, []byte(body), 0o644)
|
|
return p
|
|
}
|
|
meslo := write("MesloLGS NF Regular.ttf", "meslo")
|
|
encoded := write("MesloLGS%20NF%20Regular.ttf", "meslo")
|
|
grape := write("GrapeNuts-Regular.ttf", "grape")
|
|
noto := write("NotoSans-Copy.ttf", "noto")
|
|
_ = os.WriteFile(filepath.Join(dir, ".uuid"), []byte("x"), 0o644)
|
|
f := using(t, func(line string, c Cmd) Result {
|
|
switch c.Name {
|
|
case "fc-scan":
|
|
return ok(meslo + "\tMesloLGS NF\tRegular\n" + encoded + "\tMesloLGS NF\tRegular\n" + grape + "\tGrape Nuts\tRegular\n" + noto + "\tNoto Sans\tRegular\n")
|
|
case "fc-list":
|
|
return ok("Noto Sans\t/usr/share/fonts/noto/NotoSans-Regular.ttf\nNoto Sans\t/usr/share/fonts/noto/NotoSans-Bold.ttf\nInter\t/usr/share/fonts/inter/Inter.ttc\nMesloLGS NF\t" + meslo + "\n")
|
|
case "pacman":
|
|
return Result{Status: 1, Stdout: "/usr/share/fonts/noto/NotoSans-Regular.ttf is owned by noto-fonts 1:2026.08.01-1\n", Stderr: "error: No package owns /usr/share/fonts/inter/Inter.ttc\n"}
|
|
}
|
|
return Result{Status: 9}
|
|
})
|
|
got, err := Sources()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Hand) != 4 {
|
|
t.Fatalf("four font files, the .uuid is not one: %+v", got.Hand)
|
|
}
|
|
by := map[string]HandFile{}
|
|
for _, h := range got.Hand {
|
|
by[filepath.Base(h.File)] = h
|
|
}
|
|
if h := by["GrapeNuts-Regular.ttf"]; h.Remove || h.Family != "Grape Nuts" {
|
|
t.Errorf("a face nothing replaces is kept: %+v", h)
|
|
}
|
|
if h := by["MesloLGS%20NF%20Regular.ttf"]; !h.Remove || len(h.Why) < 3 {
|
|
t.Errorf("an encoded duplicate of a retired face: %+v", h)
|
|
}
|
|
if h := by["NotoSans-Copy.ttf"]; !h.Remove || !strings.Contains(strings.Join(h.Why, ";"), "noto-fonts") {
|
|
t.Errorf("a face a package installs: %+v", h)
|
|
}
|
|
if got.RemoveCount != 3 || got.HandBytes != int64(len("meslo")*2+len("grape")+len("noto")) {
|
|
t.Errorf("%d removable, %d bytes", got.RemoveCount, got.HandBytes)
|
|
}
|
|
pk := map[string]string{}
|
|
for _, p := range got.Packaged {
|
|
pk[p.Family] = p.Package
|
|
}
|
|
if pk["Noto Sans"] != "noto-fonts" || pk["Inter"] != "(no package)" || pk["MesloLGS NF"] != "" {
|
|
t.Errorf("packaged %v", pk)
|
|
}
|
|
for _, l := range f.lines() {
|
|
if strings.Contains(l, "rm ") || strings.HasPrefix(l, "sudo") {
|
|
t.Errorf("sources only reads: %s", l)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConfigSaysWhetherTheFileIsInPlaceAndLoaded(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("MESH_OPERATOR_HOME", home)
|
|
path := filepath.Join(home, ConfigFile)
|
|
using(t, func(string, Cmd) Result { return ok("+ " + path + ": The mesh\n- /etc/fonts/conf.d/x.conf\n") })
|
|
got, err := Config()
|
|
if err != nil || got.Present || !strings.Contains(got.Note, "not in place") {
|
|
t.Fatalf("absent: %+v %v", got, err)
|
|
}
|
|
_ = os.MkdirAll(filepath.Dir(path), 0o755)
|
|
_ = os.WriteFile(path, []byte("<fontconfig/>"), 0o644)
|
|
_ = os.WriteFile(filepath.Join(filepath.Dir(path), "99-mine.conf"), []byte("x"), 0o644)
|
|
got, err = Config()
|
|
if err != nil || !got.Present || !got.Loaded || len(got.Others) != 1 {
|
|
t.Fatalf("present: %+v %v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestCacheRebuildIsTheAccountsUnlessTheSystemIsAskedWhichNeedsRoot(t *testing.T) {
|
|
f := using(t, func(string, Cmd) Result { return ok("") })
|
|
if got, err := CacheRebuild(false); err != nil || got.Scope != "account" {
|
|
t.Fatal(got, err)
|
|
}
|
|
if got, err := CacheRebuild(true); err != nil || got.Scope != "system" {
|
|
t.Fatal(got, err)
|
|
}
|
|
if l := f.lines(); l[0] != "fc-cache -f" || l[1] != "sudo -n fc-cache -s -f" {
|
|
t.Errorf("%v", l)
|
|
}
|
|
using(t, func(string, Cmd) Result { return Result{Status: 1, Stderr: "sudo: a password is required"} })
|
|
if _, err := CacheRebuild(true); err == nil || !strings.Contains(err.Error(), "sudo -n refused") {
|
|
t.Errorf("a refused escalation: %v", err)
|
|
}
|
|
}
|