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, _ := m.resource("defaults")["content"].(string) if !strings.HasPrefix(content, " 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(""), 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) } }