628 lines
20 KiB
Go
628 lines
20 KiB
Go
package pkgmgr
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// All managers must satisfy the Manager interface.
|
|
var (
|
|
_ Manager = (*Apt)(nil)
|
|
_ Manager = (*AptGet)(nil)
|
|
_ Manager = (*Dnf)(nil)
|
|
_ Manager = (*Yum)(nil)
|
|
_ Manager = (*Zypper)(nil)
|
|
_ Manager = (*Pacman)(nil)
|
|
_ Manager = (*Apk)(nil)
|
|
_ Manager = (*Brew)(nil)
|
|
_ Manager = (*Yay)(nil)
|
|
_ Manager = (*Paru)(nil)
|
|
)
|
|
|
|
func TestCommandWrapper(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
wrapper []string
|
|
want []string
|
|
}{
|
|
{"none", nil, []string{"apt", "install", "vim"}},
|
|
{"single", []string{"sudo"}, []string{"sudo", "apt", "install", "vim"}},
|
|
{"multi", []string{"sudo", "-n"}, []string{"sudo", "-n", "apt", "install", "vim"}},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var b baseManager
|
|
b.SetCmdWrapper(tc.wrapper)
|
|
cmd := b.Command(context.Background(), "apt", "install", "vim")
|
|
if !slices.Equal(cmd.Args, tc.want) {
|
|
t.Errorf("Args = %v, want %v", cmd.Args, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommandIO(t *testing.T) {
|
|
// With no overrides the os streams are used.
|
|
var def baseManager
|
|
cmd := def.Command(context.Background(), "echo")
|
|
if cmd.Stdout != os.Stdout || cmd.Stderr != os.Stderr || cmd.Stdin != os.Stdin {
|
|
t.Error("default Command should use the os streams")
|
|
}
|
|
|
|
// SetIO overrides each stream.
|
|
var b baseManager
|
|
var out, errBuf bytes.Buffer
|
|
in := strings.NewReader("input")
|
|
b.SetIO(in, &out, &errBuf)
|
|
cmd = b.Command(context.Background(), "echo")
|
|
if cmd.Stdout != &out || cmd.Stderr != &errBuf || cmd.Stdin != in {
|
|
t.Error("SetIO streams were not applied to the command")
|
|
}
|
|
}
|
|
|
|
func TestJoinArgsDoesNotMutate(t *testing.T) {
|
|
base := make([]string, 1, 8) // spare capacity would let append clobber base
|
|
base[0] = "-y"
|
|
|
|
got := joinArgs(base, "install")
|
|
got = append(got, "vim")
|
|
|
|
if len(base) != 1 || base[0] != "-y" {
|
|
t.Errorf("joinArgs mutated its input: %v", base)
|
|
}
|
|
if want := []string{"-y", "install", "vim"}; !slices.Equal(got, want) {
|
|
t.Errorf("joinArgs = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseVersionList(t *testing.T) {
|
|
in := "git := 1:2.39.0\nfoo := 0:1.0\nmalformed line\n\n bar := 2.0 \n"
|
|
got, err := parseVersionList(strings.NewReader(in), " := ", true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
want := map[string]string{
|
|
"git": "1:2.39.0", // a non-zero epoch is preserved
|
|
"foo": "1.0", // a 0: epoch is trimmed
|
|
"bar": "2.0", // surrounding whitespace is trimmed
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %v, want %v", got, want)
|
|
}
|
|
for k, v := range want {
|
|
if got[k] != v {
|
|
t.Errorf("%q = %q, want %q", k, got[k], v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseApkList(t *testing.T) {
|
|
in := "busybox-1.36.1-r5\npy3-foo-1.2.3-r0\nlibc6-compat-1.2-r0\ngarbage\n"
|
|
got, err := parseApkList(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
want := map[string]string{
|
|
"busybox": "1.36.1-r5",
|
|
"py3-foo": "1.2.3-r0",
|
|
"libc6-compat": "1.2-r0",
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %v, want %v", got, want)
|
|
}
|
|
for k, v := range want {
|
|
if got[k] != v {
|
|
t.Errorf("%q = %q, want %q", k, got[k], v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAptKeyDest(t *testing.T) {
|
|
armored := []byte("-----BEGIN PGP PUBLIC KEY BLOCK-----\nabc\n")
|
|
if ext := filepath.Ext(aptKeyDest(armored)); ext != ".asc" {
|
|
t.Errorf("armored key ext = %q, want .asc", ext)
|
|
}
|
|
|
|
binary := []byte{0x99, 0x01, 0x02}
|
|
if ext := filepath.Ext(aptKeyDest(binary)); ext != ".gpg" {
|
|
t.Errorf("binary key ext = %q, want .gpg", ext)
|
|
}
|
|
|
|
// The destination is stable for the same key and lives in the keyrings dir.
|
|
if a, b := aptKeyDest(armored), aptKeyDest(armored); a != b {
|
|
t.Errorf("aptKeyDest not deterministic: %q vs %q", a, b)
|
|
}
|
|
if dir := filepath.Dir(aptKeyDest(armored)); dir != APT_KEYRINGS_DIR {
|
|
t.Errorf("key dir = %q, want %q", dir, APT_KEYRINGS_DIR)
|
|
}
|
|
}
|
|
|
|
func TestIniSectionRoundTrip(t *testing.T) {
|
|
file := filepath.Join(t.TempDir(), "pacman.conf")
|
|
if err := os.WriteFile(file, []byte("[options]\nHoldPkg = pacman\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := setIniSection(file, "myrepo", "Server = https://example.test/repo"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := getIniSection(file, "myrepo"); got != "Server = https://example.test/repo" {
|
|
t.Errorf("getIniSection = %q", got)
|
|
}
|
|
// Existing sections are preserved.
|
|
if got := getIniSection(file, "options"); got != "HoldPkg = pacman" {
|
|
t.Errorf("options section damaged: %q", got)
|
|
}
|
|
|
|
// Re-adding replaces rather than duplicates.
|
|
if err := setIniSection(file, "myrepo", "Server = https://example.test/other"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := getIniSection(file, "myrepo"); got != "Server = https://example.test/other" {
|
|
t.Errorf("after replace = %q", got)
|
|
}
|
|
|
|
if err := removeIniSection(file, "myrepo"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := getIniSection(file, "myrepo"); got != "" {
|
|
t.Errorf("removed section still present: %q", got)
|
|
}
|
|
if got := getIniSection(file, "options"); got != "HoldPkg = pacman" {
|
|
t.Errorf("options section lost after remove: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestMarkerSectionRoundTrip(t *testing.T) {
|
|
file := filepath.Join(t.TempDir(), "repositories")
|
|
if err := os.WriteFile(file, []byte("https://dl-cdn.alpinelinux.org/alpine/v3.20/main\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := setMarkerSection(file, "extra", "https://example.test/alpine"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := getMarkerSection(file, "extra"); got != "https://example.test/alpine" {
|
|
t.Errorf("getMarkerSection = %q", got)
|
|
}
|
|
|
|
// The pre-existing line is still present.
|
|
data, err := os.ReadFile(file)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(data), "dl-cdn.alpinelinux.org") {
|
|
t.Error("original repository line was lost")
|
|
}
|
|
|
|
if err := removeMarkerSection(file, "extra"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := getMarkerSection(file, "extra"); got != "" {
|
|
t.Errorf("removed marker section still present: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestListRepoFiles(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "docker.list"), []byte("deb https://download.docker.com/linux/ubuntu stable\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "hashicorp.list"), []byte("deb https://apt.releases.hashicorp.com stable main\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A non-matching suffix, a subdirectory whose name ends in the suffix, and a
|
|
// missing directory must all be ignored rather than listed or erroring.
|
|
if err := os.WriteFile(filepath.Join(dir, "ignore.repo"), []byte("noise\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(dir, "sub.list"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := listRepoFiles(dir, ".list")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantMap(t, got, map[string]string{
|
|
"docker": "deb https://download.docker.com/linux/ubuntu stable\n",
|
|
"hashicorp": "deb https://apt.releases.hashicorp.com stable main\n",
|
|
})
|
|
|
|
// The listed value matches readRepoFile for each stem, which is the same
|
|
// value GetRepo returns for the corresponding manager.
|
|
if got["docker"] != readRepoFile(filepath.Join(dir, "docker.list")) {
|
|
t.Error("listRepoFiles value does not match readRepoFile")
|
|
}
|
|
|
|
// A missing directory is an empty result, not an error.
|
|
got, err = listRepoFiles(filepath.Join(dir, "missing"), ".list")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantMap(t, got, map[string]string{})
|
|
}
|
|
|
|
func TestListIniSections(t *testing.T) {
|
|
file := filepath.Join(t.TempDir(), "pacman.conf")
|
|
content := "[options]\nHoldPkg = pacman\n\n" +
|
|
"[core]\nSigLevel = Required\nServer = https://example.test/core\n\n" +
|
|
"[extra]\nServer = https://example.test/extra\n"
|
|
if err := os.WriteFile(file, []byte(content), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := listIniSections(file, "options")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantMap(t, got, map[string]string{
|
|
"core": "SigLevel = Required\nServer = https://example.test/core",
|
|
"extra": "Server = https://example.test/extra",
|
|
})
|
|
|
|
// The listing matches getIniSection per name, which is what Pacman.GetRepo
|
|
// returns, and the excluded section is omitted.
|
|
if got["core"] != getIniSection(file, "core") {
|
|
t.Error("listIniSections value does not match getIniSection")
|
|
}
|
|
if _, ok := got["options"]; ok {
|
|
t.Error("the options section should be excluded from the listing")
|
|
}
|
|
|
|
// A missing file is an empty result, not an error.
|
|
got, err = listIniSections(filepath.Join(t.TempDir(), "missing"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantMap(t, got, map[string]string{})
|
|
}
|
|
|
|
func TestListMarkerSections(t *testing.T) {
|
|
file := filepath.Join(t.TempDir(), "repositories")
|
|
content := "https://dl-cdn.alpinelinux.org/alpine/v3.20/main\n" +
|
|
"# >>> extra\n" +
|
|
"https://example.test/alpine\n" +
|
|
"# <<< extra\n" +
|
|
"# >>> testing\n" +
|
|
"https://example.test/testing\n" +
|
|
"# <<< testing\n"
|
|
if err := os.WriteFile(file, []byte(content), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := listMarkerSections(file)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantMap(t, got, map[string]string{
|
|
"extra": "https://example.test/alpine",
|
|
"testing": "https://example.test/testing",
|
|
})
|
|
|
|
// The listing matches getMarkerSection per name, which is what Apk.GetRepo
|
|
// returns, and unmarked repository lines are not included.
|
|
if got["extra"] != getMarkerSection(file, "extra") {
|
|
t.Error("listMarkerSections value does not match getMarkerSection")
|
|
}
|
|
|
|
// A missing file is an empty result, not an error.
|
|
got, err = listMarkerSections(filepath.Join(t.TempDir(), "missing"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantMap(t, got, map[string]string{})
|
|
}
|
|
|
|
// TestAptKeyPrefersAptKeyBinary verifies that, for backward compatibility with
|
|
// older releases (e.g. Ubuntu 14.04) where apt-key is the only supported method
|
|
// of adding keys, AddRepoKey uses apt-key when it is present on PATH rather than
|
|
// falling back to the modern keyring directory.
|
|
func TestAptKeyPrefersAptKeyBinary(t *testing.T) {
|
|
dir := t.TempDir()
|
|
marker := filepath.Join(dir, "stdin")
|
|
|
|
// A fake apt-key that records the key it receives on stdin. It uses only
|
|
// shell builtins so it does not depend on PATH, which the test overrides.
|
|
script := "#!/bin/sh\nread key\necho \"$key\" > " + marker + "\n"
|
|
if err := os.WriteFile(filepath.Join(dir, "apt-key"), []byte(script), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("PATH", dir)
|
|
|
|
var a Apt
|
|
if err := a.AddRepoKey(context.Background(), "MYKEY"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
data, err := os.ReadFile(marker)
|
|
if err != nil {
|
|
t.Fatalf("apt-key was not invoked: %v", err)
|
|
}
|
|
if got := strings.TrimSpace(string(data)); got != "MYKEY" {
|
|
t.Errorf("apt-key received %q, want MYKEY", got)
|
|
}
|
|
}
|
|
|
|
func TestNonRootInvocation(t *testing.T) {
|
|
args := []string{"-S", "some-aur-pkg"}
|
|
|
|
// As a normal user the command runs directly, unwrapped.
|
|
cmd, got, err := nonRootInvocation("/usr/bin/yay", "/usr/bin/sudo", "alice", false, args)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cmd != "/usr/bin/yay" {
|
|
t.Errorf("command = %q, want /usr/bin/yay", cmd)
|
|
}
|
|
if !slices.Equal(got, args) {
|
|
t.Errorf("args = %v, want %v", got, args)
|
|
}
|
|
|
|
// As root the command drops to the target user via sudo -u.
|
|
cmd, got, err = nonRootInvocation("/usr/bin/yay", "/usr/bin/sudo", "alice", true, args)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cmd != "/usr/bin/sudo" {
|
|
t.Errorf("command = %q, want /usr/bin/sudo", cmd)
|
|
}
|
|
want := []string{"-u", "alice", "-H", "/usr/bin/yay", "-S", "some-aur-pkg"}
|
|
if !slices.Equal(got, want) {
|
|
t.Errorf("args = %v, want %v", got, want)
|
|
}
|
|
|
|
// As root with no user to drop to, it fails rather than running as root.
|
|
if _, _, err := nonRootInvocation("/usr/bin/yay", "/usr/bin/sudo", "", true, args); err == nil {
|
|
t.Error("expected an error when running as root with no drop user")
|
|
}
|
|
|
|
// As root the drop user must not itself be root.
|
|
if _, _, err := nonRootInvocation("/usr/bin/yay", "/usr/bin/sudo", "root", true, args); err == nil {
|
|
t.Error("expected an error when the drop user is root")
|
|
}
|
|
|
|
// As root with no sudo available, it cannot drop privileges.
|
|
if _, _, err := nonRootInvocation("/usr/bin/yay", "", "alice", true, args); err == nil {
|
|
t.Error("expected an error when sudo is unavailable")
|
|
}
|
|
}
|
|
|
|
func TestNonRootInvocationDoesNotMutateArgs(t *testing.T) {
|
|
args := []string{"-S", "pkg"}
|
|
if _, _, err := nonRootInvocation("/usr/bin/yay", "/usr/bin/sudo", "alice", true, args); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if want := []string{"-S", "pkg"}; !slices.Equal(args, want) {
|
|
t.Errorf("nonRootInvocation mutated its input: %v", args)
|
|
}
|
|
}
|
|
|
|
func TestParseVersionListMissingSeparator(t *testing.T) {
|
|
// Lines without the separator are skipped rather than erroring.
|
|
got, err := parseVersionList(strings.NewReader("nosephere\na := b\n"), " := ", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 1 || got["a"] != "b" {
|
|
t.Errorf("got %v, want map[a:b]", got)
|
|
}
|
|
}
|
|
|
|
// wantMap fails the test unless got equals want exactly.
|
|
func wantMap(t *testing.T, got, want map[string]string) {
|
|
t.Helper()
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %v, want %v", got, want)
|
|
}
|
|
for k, v := range want {
|
|
if got[k] != v {
|
|
t.Errorf("%q = %q, want %q", k, got[k], v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseAptUpgradable(t *testing.T) {
|
|
// A simulated `apt-get -s upgrade`: an Inst line with a prior version, one
|
|
// without (a newly pulled dependency), a Conf line, and noise.
|
|
in := "Reading package lists...\n" +
|
|
"Inst vim [2:8.1-1] (2:8.2-1 Ubuntu:22.04 [amd64])\n" +
|
|
"Inst libfoo (1.1 Ubuntu:22.04 [amd64])\n" +
|
|
"Conf vim (2:8.2-1 Ubuntu:22.04 [amd64])\n"
|
|
got, err := parseAptUpgradable(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantMap(t, got, map[string]string{"vim": "2:8.2-1", "libfoo": "1.1"})
|
|
}
|
|
|
|
func TestParseRpmUpgradable(t *testing.T) {
|
|
// `dnf -q list --upgrades` output with a header and a dotted package name.
|
|
in := "Available Upgrades\n" +
|
|
"vim-enhanced.x86_64 2:9.0.1-1.fc39 updates\n" +
|
|
"python3.11.x86_64 3.11.7-1.fc39 updates\n" +
|
|
"garbage line here that is not three columns wide\n"
|
|
got, err := parseRpmUpgradable(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The ".arch" qualifier is stripped, including for names that contain dots.
|
|
wantMap(t, got, map[string]string{"vim-enhanced": "2:9.0.1-1.fc39", "python3.11": "3.11.7-1.fc39"})
|
|
}
|
|
|
|
func TestParseZypperUpgradable(t *testing.T) {
|
|
in := "S | Repository | Name | Current Version | Available Version | Arch\n" +
|
|
"--+------------+------+-----------------+-------------------+------\n" +
|
|
"v | repo-oss | vim | 9.0.1-1.1 | 9.0.2-1.1 | x86_64\n"
|
|
got, err := parseZypperUpgradable(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantMap(t, got, map[string]string{"vim": "9.0.2-1.1"})
|
|
}
|
|
|
|
func TestParsePacmanUpgradable(t *testing.T) {
|
|
// Includes an "[ignored]" suffix and a malformed line.
|
|
in := "vim 9.0.1-1 -> 9.0.2-1\nlinux 6.6.1-1 -> 6.6.2-1 [ignored]\nnot an upgrade line\n"
|
|
got, err := parsePacmanUpgradable(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantMap(t, got, map[string]string{"vim": "9.0.2-1", "linux": "6.6.2-1"})
|
|
}
|
|
|
|
func TestParseApkUpgradable(t *testing.T) {
|
|
in := "Installed: Available:\nbusybox-1.36.1-r5 < 1.36.1-r6\nmusl-1.2.4-r2 < 1.2.5-r0\n"
|
|
got, err := parseApkUpgradable(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantMap(t, got, map[string]string{"busybox": "1.36.1-r6", "musl": "1.2.5-r0"})
|
|
}
|
|
|
|
func TestParseBrewUpgradable(t *testing.T) {
|
|
in := "git (2.39.0) < 2.43.0\nwget (1.21.3) < 1.21.4\nnot outdated output\n"
|
|
got, err := parseBrewUpgradable(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantMap(t, got, map[string]string{"git": "2.43.0", "wget": "1.21.4"})
|
|
}
|
|
|
|
// wantResults fails the test unless got equals want exactly, in order.
|
|
func wantResults(t *testing.T, got, want []SearchResult) {
|
|
t.Helper()
|
|
if !slices.Equal(got, want) {
|
|
t.Errorf("got %+v, want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseAptSearch(t *testing.T) {
|
|
in := "Sorting...\nFull Text Search...\n" +
|
|
"vim/jammy,now 2:8.2.3995-1ubuntu2 amd64 [installed]\n" +
|
|
" Vi IMproved - enhanced vi editor\n\n" +
|
|
"xxd/jammy 2:8.2.3995-1ubuntu2 amd64\n" +
|
|
" tool to make (or reverse) a hex dump\n"
|
|
got, err := parseAptSearch(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantResults(t, got, []SearchResult{
|
|
{Name: "vim", Version: "2:8.2.3995-1ubuntu2", Summary: "Vi IMproved - enhanced vi editor"},
|
|
{Name: "xxd", Version: "2:8.2.3995-1ubuntu2", Summary: "tool to make (or reverse) a hex dump"},
|
|
})
|
|
}
|
|
|
|
func TestParseAptCacheSearch(t *testing.T) {
|
|
in := "vim - Vi IMproved - enhanced vi editor\nxxd - make a hexdump\nnodashline\n"
|
|
got, err := parseAptCacheSearch(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Only the first " - " splits name from summary, so summaries may contain " - ".
|
|
wantResults(t, got, []SearchResult{
|
|
{Name: "vim", Summary: "Vi IMproved - enhanced vi editor"},
|
|
{Name: "xxd", Summary: "make a hexdump"},
|
|
})
|
|
}
|
|
|
|
func TestParseRpmSearch(t *testing.T) {
|
|
in := "====== Name Exactly Matched: vim ======\n" +
|
|
"vim-enhanced.x86_64 : A version of the VIM editor\n" +
|
|
"====== Name & Summary Matched: vim ======\n" +
|
|
"python3.11-foo.noarch : A library\n"
|
|
got, err := parseRpmSearch(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantResults(t, got, []SearchResult{
|
|
{Name: "vim-enhanced", Summary: "A version of the VIM editor"},
|
|
{Name: "python3.11-foo", Summary: "A library"},
|
|
})
|
|
}
|
|
|
|
func TestParseZypperSearch(t *testing.T) {
|
|
in := "S | Name | Summary | Type\n" +
|
|
"--+------+---------+--------\n" +
|
|
" | vim | Vi IMproved | package\n" +
|
|
"i | vim-data | VIM runtime files | package\n"
|
|
got, err := parseZypperSearch(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantResults(t, got, []SearchResult{
|
|
{Name: "vim", Summary: "Vi IMproved"},
|
|
{Name: "vim-data", Summary: "VIM runtime files"},
|
|
})
|
|
}
|
|
|
|
func TestParsePacmanSearch(t *testing.T) {
|
|
in := "extra/vim 9.0.2-1 [installed]\n" +
|
|
" Vi Improved, a programmer's text editor\n" +
|
|
"extra/neovim 0.9.4-1\n" +
|
|
" Fork of Vim aiming to improve user experience\n"
|
|
got, err := parsePacmanSearch(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantResults(t, got, []SearchResult{
|
|
{Name: "vim", Version: "9.0.2-1", Summary: "Vi Improved, a programmer's text editor"},
|
|
{Name: "neovim", Version: "0.9.4-1", Summary: "Fork of Vim aiming to improve user experience"},
|
|
})
|
|
}
|
|
|
|
func TestParseApkSearch(t *testing.T) {
|
|
in := "vim-9.0.2127-r0 - The VIM editor\nbusybox-1.36.1-r5 - Size optimized toolbox\ngarbage\n"
|
|
got, err := parseApkSearch(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantResults(t, got, []SearchResult{
|
|
{Name: "vim", Version: "9.0.2127-r0", Summary: "The VIM editor"},
|
|
{Name: "busybox", Version: "1.36.1-r5", Summary: "Size optimized toolbox"},
|
|
})
|
|
}
|
|
|
|
func TestParseBrewSearch(t *testing.T) {
|
|
in := "==> Formulae\nvim\nneovim\n==> Casks\nmacvim\n"
|
|
got, err := parseBrewSearch(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantResults(t, got, []SearchResult{
|
|
{Name: "vim"},
|
|
{Name: "neovim"},
|
|
{Name: "macvim"},
|
|
})
|
|
}
|
|
|
|
func TestExitCodeAllowed(t *testing.T) {
|
|
// A real exit-1 error, as pacman -Qu produces when nothing is upgradable.
|
|
exitErr := exec.Command("sh", "-c", "exit 1").Run()
|
|
if exitErr == nil {
|
|
t.Fatal("expected a non-nil exit error")
|
|
}
|
|
if !exitCodeAllowed(exitErr, []int{1}) {
|
|
t.Error("exit code 1 should be allowed when listed")
|
|
}
|
|
if exitCodeAllowed(exitErr, []int{2}) {
|
|
t.Error("exit code 1 should not be allowed when only 2 is listed")
|
|
}
|
|
if exitCodeAllowed(exitErr, nil) {
|
|
t.Error("no allowed codes means nothing is tolerated")
|
|
}
|
|
// A non-exit error is never an allowed exit code.
|
|
if exitCodeAllowed(errors.New("boom"), []int{1}) {
|
|
t.Error("a non-exit error should never be allowed")
|
|
}
|
|
}
|