package main import ( "errors" "strings" "testing" "time" ) const searchOut = `extra/zsh 5.9.2-1 [installed] A very advanced and programmable command interpreter (shell) for UNIX extra/ripgrep 15.2.0-1 [installed: 15.1.0-1] A search tool core/base-devel 1-2 (base-devel) Basic tools to build Arch Linux packages ` func TestSearchReadsHeaderAndDescriptionAndWhatIsInstalled(t *testing.T) { f := ParseSearch(searchOut) if len(f) != 3 { t.Fatalf("%+v", f) } if f[0].Repository != "extra" || f[0].Name != "zsh" || !f[0].Installed || f[0].InstalledAs != "5.9.2-1" || !strings.HasPrefix(f[0].Description, "A very advanced") { t.Fatalf("%+v", f[0]) } if f[1].InstalledAs != "15.1.0-1" || f[1].Version != "15.2.0-1" { t.Fatalf("%+v", f[1]) } if f[2].Installed || len(f[2].Groups) != 1 || f[2].Groups[0] != "base-devel" { t.Fatalf("%+v", f[2]) } } func TestASearchThatFindsNothingIsEmptyAndAFailureIsAnError(t *testing.T) { m := machine(fake(func(c call) Ran { return Ran{Status: 1} }, nil), 1000) r, err := m.Search("nothing", 50) if err != nil || r["count"] != 0 { t.Fatalf("%v %v", r, err) } m = machine(fake(func(c call) Ran { return Ran{Status: 1, Stderr: "error: failed to initialize alpm library\n"} }, nil), 1000) if _, err := m.Search("x", 50); err == nil || !strings.Contains(err.Error(), "failed to initialize") { t.Fatalf("%v", err) } } const infoOut = `Name : zsh Version : 5.9.2-1 Depends On : pcre2 libcap gdbm Optional Deps : grml-zsh-config: grml's zsh setup zsh-doc: documentation [installed] Required By : None Install Reason : Explicitly installed ` func TestInfoReadsListsAsListsAndFallsBackToTheRepositories(t *testing.T) { p := ParseInfo(infoOut) if len(p) != 1 { t.Fatalf("%v", p) } if deps := p[0]["Depends On"].([]string); len(deps) != 3 || deps[2] != "gdbm" { t.Fatalf("%v", p[0]["Depends On"]) } if opt := p[0]["Optional Deps"].([]string); len(opt) != 2 || !strings.HasPrefix(opt[1], "zsh-doc") { t.Fatalf("%v", p[0]["Optional Deps"]) } if req := p[0]["Required By"].([]string); len(req) != 0 { t.Fatalf("None is empty: %v", req) } var calls []call m := machine(byLine(map[string]Ran{ "pacman -Qi -- zsh": {Status: 1, Stderr: "error: package 'zsh' was not found\n"}, "pacman -Si -- zsh": {Stdout: "Repository : extra\n" + infoOut}, }, &calls), 1000) r, err := m.Info("zsh") if err != nil || r["installed"] != false || r["package"].(map[string]any)["Repository"] != "extra" { t.Fatalf("%v %v", r, err) } } func TestInstalledSaysWhyAndWhatIsForeign(t *testing.T) { m := machine(byLine(map[string]Ran{ "pacman -Q": {Stdout: "glibc 2.42-1\nyay 12.0-1\nzsh 5.9-1\n"}, "pacman -Qeq": {Stdout: "yay\nzsh\n"}, "pacman -Qmq": {Stdout: "yay\n"}, }, nil), 1000) r, err := m.Installed("", "", false, 10) if err != nil { t.Fatal(err) } pk := r["packages"].([]Package) if pk[0].Reason != "dependency" || pk[1].Reason != "explicit" || !pk[1].Foreign || pk[2].Foreign { t.Fatalf("%+v", pk) } if tot := r["totals"].(map[string]int); tot["explicit"] != 2 || tot["dependency"] != 1 || tot["foreign"] != 1 { t.Fatalf("%v", tot) } r, _ = m.Installed("", "", true, 10) if r["count"] != 1 { t.Fatalf("foreign only: %v", r) } r, _ = m.Installed("", "explicit", false, 1) if r["count"] != 2 || r["truncated"] != true { t.Fatalf("bounded: %v", r) } } func TestOwnsAnswersNoOwnerAsAnAnswer(t *testing.T) { m := machine(byLine(map[string]Ran{ "pacman -Qo -- /usr/bin/zsh": {Stdout: "/usr/bin/zsh is owned by zsh 5.9.2-1\n"}, "pacman -Qo -- /etc/hostname": {Status: 1, Stderr: "error: No package owns /etc/hostname\n"}, "pacman -Qo -- /nope": {Status: 1, Stderr: "error: failed to read file '/nope': No such file or directory\n"}, }, nil), 1000) if r, err := m.Owns("/usr/bin/zsh"); err != nil || r["package"] != "zsh" || r["owned"] != true { t.Fatalf("%v %v", r, err) } if r, err := m.Owns("/etc/hostname"); err != nil || r["owned"] != false { t.Fatalf("%v %v", r, err) } if _, err := m.Owns("/nope"); err == nil { t.Fatal("a path that is not there is an error") } if _, err := m.Owns("relative"); err == nil { t.Fatal("a relative path was taken") } } func TestUpdatesReadsCheckupdatesAndItsNothingToDo(t *testing.T) { m := machine(byLine(map[string]Ran{"checkupdates": {Stdout: "linux 6.1-1 -> 6.2-1\nzsh 5.9-1 -> 5.9-2\n"}}, nil), 1000) r, err := m.Updates() if err != nil || r["count"] != 2 || r["updates"].([]Update)[0] != (Update{"linux", "6.1-1", "6.2-1"}) { t.Fatalf("%v %v", r, err) } m = machine(byLine(map[string]Ran{"checkupdates": {Status: 2}}, nil), 1000) if r, err := m.Updates(); err != nil || r["count"] != 0 { t.Fatalf("%v %v", r, err) } m = machine(byLine(map[string]Ran{"checkupdates": {Status: 1, Stderr: "==> ERROR: Cannot fetch updates\n"}}, nil), 1000) if _, err := m.Updates(); err == nil || !strings.Contains(err.Error(), "Cannot fetch updates") { t.Fatalf("%v", err) } } func lockless(m *Machine) *Machine { m.ReadFile = func(p string) ([]byte, error) { return nil, errNoFile } return m } func TestAnUpgradeRunsAsAUnitOfItsOwnThroughSudoAndBringsTheNews(t *testing.T) { fetch = func(string) ([]byte, error) { return nil, errors.New("no network") } var calls []call m := lockless(machine(fake(func(c call) Ran { return Ran{} }, &calls), 1000)) r, err := m.Upgrade("", 60) if err != nil { t.Fatal(err) } unit := r["started"].(string) if unit != "mesh-pacman-upgrade-1791115200" { t.Fatalf("unit: %s", unit) } last := calls[len(calls)-1] want := "sudo -n systemd-run --unit=" + unit if !strings.HasPrefix(last.String(), want) || !strings.HasSuffix(last.String(), "--quiet pacman -Syu --noconfirm") || strings.Contains(last.String(), "--wait") { t.Fatalf("started as: %s", last) } news := r["news"].(map[string]any) if news["reachable"] != false || !strings.Contains(news["error"].(string), "no network") { t.Fatalf("no network is an answer: %v", news) } } func TestAnUpgradeIsRefusedWhileTheDatabaseIsLocked(t *testing.T) { fetch = func(string) ([]byte, error) { return nil, errors.New("offline") } var calls []call m := machine(fake(func(c call) Ran { return Ran{} }, &calls), 1000) m.ReadFile = func(p string) ([]byte, error) { if p == DBLock { return []byte{}, nil } return nil, errNoFile } if _, err := m.Upgrade("", 60); err == nil || !strings.Contains(err.Error(), "another pacman holds") { t.Fatalf("%v", err) } for _, c := range calls { if c.name == "sudo" { t.Fatal("started while locked") } } } func TestAnUpgradesUnitIsReadBack(t *testing.T) { unit := "mesh-pacman-upgrade-1791115200" m := machine(byLine(map[string]Ran{ "systemctl show " + unit + " --no-pager --property=LoadState --property=ActiveState --property=SubState --property=Result --property=ExecMainStatus": {Stdout: "LoadState=loaded\nActiveState=failed\nSubState=failed\nResult=exit-code\nExecMainStatus=1\n"}, "sudo -n journalctl --no-pager -o cat -n 60 -u " + unit: {Stdout: "error: failed to commit transaction (conflicting files)\n"}, }, nil), 1000) r, err := m.Upgrade(unit, 60) if err != nil || r["finished"] != true || r["succeeded"] != false || r["exit_status"] != "1" || len(r["log"].([]string)) != 1 { t.Fatalf("%v %v", r, err) } if _, err := m.Upgrade("sshd.service", 60); err == nil { t.Fatal("a unit the tools did not start was read") } } func orphanMachine(calls *[]call) *Machine { return lockless(machine(fake(func(c call) Ran { switch { case c.String() == "pacman -Qdt": return Ran{Stdout: "argon2 20190702-6\nclang21 21.1.8-1\n"} case c.name == "sudo" && c.args[1] == "systemd-run": return Ran{} case c.name == "sudo" && c.args[1] == "journalctl": return Ran{Stdout: "removing argon2...\n"} } return Ran{Status: 99} }, calls), 1000)) } func TestRemovingOrphansTakesOnlyOrphansNamedOrAll(t *testing.T) { var calls []call m := orphanMachine(&calls) if _, err := m.RemoveOrphans(nil, false); err == nil || !strings.Contains(err.Error(), "name the orphans") { t.Fatalf("nothing named: %v", err) } if _, err := m.RemoveOrphans([]string{"glibc"}, false); err == nil || !strings.Contains(err.Error(), "glibc is not an orphan") { t.Fatalf("not an orphan: %v", err) } r, err := m.RemoveOrphans([]string{"argon2"}, false) if err != nil || strings.Join(r["removed"].([]string), ",") != "argon2" { t.Fatalf("%v %v", r, err) } var run string for _, c := range calls { if c.name == "sudo" && c.args[1] == "systemd-run" { run = c.String() } } if !strings.Contains(run, "--wait pacman -Rs --noconfirm -- argon2") { t.Fatalf("ran: %s", run) } r, _ = m.RemoveOrphans(nil, true) if len(r["removed"].([]string)) != 2 { t.Fatalf("all: %v", r) } } func TestCacheSaysWhatCleaningWouldFreeAndCleansThroughSudo(t *testing.T) { var calls []call m := machine(fake(func(c call) Ran { switch c.String() { case "find /var/cache/pacman/pkg -mindepth 1 -maxdepth 1 -printf %y %s %f\n": return Ran{Status: 1, Stdout: "f 1000 zsh-5.9-1-x86_64.pkg.tar.zst\nf 10 zsh-5.9-1-x86_64.pkg.tar.zst.sig\nd 4096 download-abc\n", Stderr: "find: permission denied\n"} case "paccache -d -k 3": return Ran{Stdout: "\n==> finished dry run: 12 candidates (disk space saved: 1.5 GiB)\n"} case "sudo -n paccache -r -k 3": return Ran{Stdout: "==> finished: 12 packages removed (disk space saved: 1.5 GiB)\n"} } if c.name == "systemctl" { return Ran{Stdout: "ActiveState=active\nUnitFileState=enabled\n"} } return Ran{Status: 99} }, &calls), 1000) c, err := m.Cache(3, false, false) if err != nil || c.Files != 1 || c.Bytes != 1010 || c.LeftDownloads != 1 || c.Candidates != 12 || c.Frees != "1.5 GiB" || c.Cleaned { t.Fatalf("%+v %v", c, err) } c, err = m.Cache(3, false, true) if err != nil || !c.Cleaned || c.Candidates != 12 || c.Timer["UnitFileState"] != "enabled" { t.Fatalf("%+v %v", c, err) } } const pacmanLog = `[2026-09-24T17:47:36+0200] [PACMAN] starting full system upgrade [2026-09-24T17:48:00+0200] [ALPM] upgraded linux (6.1-1 -> 6.2-1) [2026-09-24T17:48:01+0200] [ALPM] installed zsh (5.9-1) [2026-10-02T09:00:00+0200] [ALPM] removed ntp (4.2.8-1) [2026-10-02T09:00:00+0200] [ALPM-SCRIPTLET] some words [2022-01-01 10:00] [ALPM] installed old (1-1) ` func TestHistoryReadsTheLogSinceADayAndTheLastFullUpgrade(t *testing.T) { m := machine(nil, 1000) m.ReadFile = func(p string) ([]byte, error) { return []byte(pacmanLog), nil } r, err := m.History("2026-09-01", "", "", 10) if err != nil { t.Fatal(err) } ev := r["events"].([]Event) if len(ev) != 3 || ev[0].From != "6.1-1" || ev[0].Version != "6.2-1" || ev[2].Action != "removed" { t.Fatalf("%+v", ev) } if r["last_full_upgrade"] != "2026-09-24T17:47:36+02:00" { t.Fatalf("%v", r["last_full_upgrade"]) } r, _ = m.History("2026-09-01", "removed", "", 10) if r["count"] != 1 { t.Fatalf("%v", r) } r, _ = m.History("2026-09-01", "", "", 1) if r["truncated"] != true || r["events"].([]Event)[0].Package != "ntp" { t.Fatalf("the newest are kept: %v", r) } if _, err := m.History("yesterday", "", "", 1); err == nil { t.Fatal("not a day") } if _, err := m.History("", "exploded", "", 1); err == nil { t.Fatal("not an action") } } const feed = ` Arch Linux: Recent news updates Manual intervention neededhttps://example.org/news/a/<p>Do this <b>first</b>.</p>Tue, 22 Sep 2026 09:09:27 +0000 Old newshttps://example.org/news/b/oldMon, 01 Jun 2026 09:00:00 +0000 ` func TestNewsSinceTheLastUpgrade(t *testing.T) { items, err := ParseNews([]byte(feed), time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)) if err != nil || len(items) != 1 || items[0].Title != "Manual intervention needed" || items[0].Summary != "Do this first ." { t.Fatalf("%+v %v", items, err) } fetch = func(string) ([]byte, error) { return []byte(feed), nil } m := machine(nil, 1000) m.ReadFile = func(p string) ([]byte, error) { return []byte(pacmanLog), nil } n := m.News("") if n["reachable"] != true || n["since_last_full_upgrade"] != true || len(n["items"].([]NewsItem)) != 0 { t.Fatalf("after the last upgrade on the 24th, the post of the 22nd is old: %v", n) } } const mirrorlistReflector = `################################################################################ ################# Arch Linux mirrorlist generated by Reflector ################# ################################################################################ # With: reflector @/etc/xdg/reflector/reflector.conf # When: 2024-06-12 21:26:34 UTC Server = https://mirror.example.org/archlinux/$repo/os/$arch Server = https://mirror2.example.org/$repo/os/$arch #Server = https://old.example.org/$repo/os/$arch ` func TestMirrorsReadTheListAndRefreshWithoutWaiting(t *testing.T) { var calls []call m := machine(fake(func(c call) Ran { if c.name == "systemctl" && c.args[0] == "show" { return Ran{Stdout: "LoadState=loaded\nActiveState=active\nUnitFileState=enabled\n"} } return Ran{} }, &calls), 1000) m.ReadFile = func(p string) ([]byte, error) { switch p { case Mirrorlist: return []byte(mirrorlistReflector), nil case ReflectorConf: return []byte("# comment\n--save /etc/pacman.d/mirrorlist\n--sort rate\n"), nil } return nil, errNoFile } r, err := m.MirrorList(true) if err != nil || len(r.Servers) != 2 || r.Commented != 1 || r.GeneratedBy != "reflector" || r.When != "2024-06-12 21:26:34 UTC" || len(r.Reflector) != 2 || !r.Refreshing { t.Fatalf("%+v %v", r, err) } if calls[len(calls)-1].String() != "sudo -n systemctl start --no-block reflector.service" { t.Fatalf("%v", calls[len(calls)-1]) } } func TestConfigIsReadAsPacmanConfResolvesIt(t *testing.T) { c := ParseConf("[options]\nHoldPkg = pacman\nHoldPkg = glibc\nCheckSpace\nParallelDownloads = 5\n[core]\nUsage = All\nServer = https://a/core\nServer = https://b/core\n[extra]\nServer = https://a/extra\n") if len(c.Options["HoldPkg"]) != 2 || c.Options["ParallelDownloads"][0] != "5" || len(c.Repositories) != 2 || c.Repositories[0].Servers != 2 || c.Repositories[0].FirstServer != "https://a/core" { t.Fatalf("%+v", c) } }