mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-02 05:08:42 -07:00
test: exhaustive unit tests for all provider-specific interfaces
Add 740 total tests (up from ~200) covering: - Compile-time interface compliance for all providers - GetCapabilities assertions for every provider - Parse function edge cases: empty, malformed, single-entry, multi-entry - apt: extract inline parse logic into testable functions (parsePolicyCandidate, parseUpgradeSimulation, parseHoldList, parseOwner, parseSourcesLine) - dnf/rpm: edge cases for both dnf4 and dnf5 parsers, normalize/parseArch - pacman/aur: parseUpgrades, parseGroupPkgSet, capabilities - apk: parseUpgradeSimulation, parseListLine, SupportsDryRun - flatpak/snap: semverCmp, stripNonNumeric edge cases - pkg/ports: all parse functions with thorough edge cases Every provider now has: - Interface compliance checks (what it implements AND what it doesn't) - Capabilities test via snack.GetCapabilities() - Parse function unit tests with table-driven edge cases
This commit is contained in:
384
apk/apk_test.go
384
apk/apk_test.go
@@ -17,13 +17,23 @@ func TestSplitNameVersion(t *testing.T) {
|
||||
{"libcurl-doc-8.5.0-r0", "libcurl-doc", "8.5.0-r0"},
|
||||
{"go-1.21.5-r0", "go", "1.21.5-r0"},
|
||||
{"noversion", "noversion", ""},
|
||||
// Edge cases
|
||||
{"", "", ""},
|
||||
{"a-1", "a", "1"},
|
||||
{"my-pkg-name-0.1-r0", "my-pkg-name", "0.1-r0"},
|
||||
{"a-b-c-3.0", "a-b-c", "3.0"},
|
||||
{"single", "single", ""},
|
||||
{"-1.0", "-1.0", ""}, // no digit follows last hyphen at position > 0
|
||||
{"pkg-0", "pkg", "0"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
name, ver := splitNameVersion(tt.input)
|
||||
if name != tt.name || ver != tt.version {
|
||||
t.Errorf("splitNameVersion(%q) = (%q, %q), want (%q, %q)",
|
||||
tt.input, name, ver, tt.name, tt.version)
|
||||
}
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
name, ver := splitNameVersion(tt.input)
|
||||
if name != tt.name || ver != tt.version {
|
||||
t.Errorf("splitNameVersion(%q) = (%q, %q), want (%q, %q)",
|
||||
tt.input, name, ver, tt.name, tt.version)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +56,111 @@ musl-1.2.4-r2 x86_64 {musl} (MIT) [installed]
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListInstalledEdgeCases(t *testing.T) {
|
||||
t.Run("empty input", func(t *testing.T) {
|
||||
pkgs := parseListInstalled("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("whitespace only", func(t *testing.T) {
|
||||
pkgs := parseListInstalled(" \n \n ")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single package", func(t *testing.T) {
|
||||
pkgs := parseListInstalled("busybox-1.36.1-r5 x86_64 {busybox} (GPL-2.0-only) [installed]\n")
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "busybox" || pkgs[0].Version != "1.36.1-r5" {
|
||||
t.Errorf("unexpected package: %+v", pkgs[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not installed", func(t *testing.T) {
|
||||
pkgs := parseListInstalled("curl-8.5.0-r0 x86_64 {curl} (MIT)\n")
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Installed {
|
||||
t.Error("expected Installed=false")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseListLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
wantName string
|
||||
wantVer string
|
||||
wantArch string
|
||||
installed bool
|
||||
}{
|
||||
{
|
||||
name: "full line",
|
||||
line: "curl-8.5.0-r0 x86_64 {curl} (MIT) [installed]",
|
||||
wantName: "curl",
|
||||
wantVer: "8.5.0-r0",
|
||||
wantArch: "x86_64",
|
||||
installed: true,
|
||||
},
|
||||
{
|
||||
name: "no installed marker",
|
||||
line: "vim-9.0-r0 x86_64 {vim} (Vim)",
|
||||
wantName: "vim",
|
||||
wantVer: "9.0-r0",
|
||||
wantArch: "x86_64",
|
||||
installed: false,
|
||||
},
|
||||
{
|
||||
name: "name only",
|
||||
line: "curl-8.5.0-r0",
|
||||
wantName: "curl",
|
||||
wantVer: "8.5.0-r0",
|
||||
wantArch: "",
|
||||
installed: false,
|
||||
},
|
||||
{
|
||||
name: "empty line",
|
||||
line: "",
|
||||
wantName: "",
|
||||
wantVer: "",
|
||||
wantArch: "",
|
||||
installed: false,
|
||||
},
|
||||
{
|
||||
name: "aarch64 arch",
|
||||
line: "openssl-3.1.4-r0 aarch64 {openssl} (Apache-2.0) [installed]",
|
||||
wantName: "openssl",
|
||||
wantVer: "3.1.4-r0",
|
||||
wantArch: "aarch64",
|
||||
installed: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkg := parseListLine(tt.line)
|
||||
if pkg.Name != tt.wantName {
|
||||
t.Errorf("Name = %q, want %q", pkg.Name, tt.wantName)
|
||||
}
|
||||
if pkg.Version != tt.wantVer {
|
||||
t.Errorf("Version = %q, want %q", pkg.Version, tt.wantVer)
|
||||
}
|
||||
if pkg.Arch != tt.wantArch {
|
||||
t.Errorf("Arch = %q, want %q", pkg.Arch, tt.wantArch)
|
||||
}
|
||||
if pkg.Installed != tt.installed {
|
||||
t.Errorf("Installed = %v, want %v", pkg.Installed, tt.installed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSearch(t *testing.T) {
|
||||
// verbose output
|
||||
output := `curl-8.5.0-r0 - URL retrieval utility and library
|
||||
@@ -76,6 +191,51 @@ curl-doc-8.5.0-r0
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSearchEdgeCases(t *testing.T) {
|
||||
t.Run("empty input", func(t *testing.T) {
|
||||
pkgs := parseSearch("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single result verbose", func(t *testing.T) {
|
||||
pkgs := parseSearch("nginx-1.24.0-r0 - HTTP and reverse proxy server\n")
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "nginx" {
|
||||
t.Errorf("expected nginx, got %q", pkgs[0].Name)
|
||||
}
|
||||
if pkgs[0].Description != "HTTP and reverse proxy server" {
|
||||
t.Errorf("unexpected description: %q", pkgs[0].Description)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single result plain", func(t *testing.T) {
|
||||
pkgs := parseSearch("nginx-1.24.0-r0\n")
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "nginx" || pkgs[0].Version != "1.24.0-r0" {
|
||||
t.Errorf("unexpected package: %+v", pkgs[0])
|
||||
}
|
||||
if pkgs[0].Description != "" {
|
||||
t.Errorf("expected empty description, got %q", pkgs[0].Description)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("description with hyphens", func(t *testing.T) {
|
||||
pkgs := parseSearch("git-2.43.0-r0 - Distributed version control system - fast\n")
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Description != "Distributed version control system - fast" {
|
||||
t.Errorf("unexpected description: %q", pkgs[0].Description)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseInfo(t *testing.T) {
|
||||
output := `curl-8.5.0-r0 installed size:
|
||||
description: URL retrieval utility and library
|
||||
@@ -94,11 +254,72 @@ webpage: https://curl.se/
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoEdgeCases(t *testing.T) {
|
||||
t.Run("empty input", func(t *testing.T) {
|
||||
pkg := parseInfo("")
|
||||
if pkg == nil {
|
||||
t.Fatal("expected non-nil (parseInfo always returns a pkg)")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no description", func(t *testing.T) {
|
||||
pkg := parseInfo("arch: aarch64\n")
|
||||
if pkg == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if pkg.Arch != "aarch64" {
|
||||
t.Errorf("expected aarch64, got %q", pkg.Arch)
|
||||
}
|
||||
if pkg.Description != "" {
|
||||
t.Errorf("expected empty description, got %q", pkg.Description)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple colons in value", func(t *testing.T) {
|
||||
pkg := parseInfo("description: A tool: does things: really well\n")
|
||||
if pkg == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
// Note: strings.Cut splits on first colon only
|
||||
if pkg.Description != "A tool: does things: really well" {
|
||||
t.Errorf("unexpected description: %q", pkg.Description)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseInfoNameVersion(t *testing.T) {
|
||||
output := "curl-8.5.0-r0 description:\nsome stuff"
|
||||
name, ver := parseInfoNameVersion(output)
|
||||
if name != "curl" || ver != "8.5.0-r0" {
|
||||
t.Errorf("got (%q, %q), want (curl, 8.5.0-r0)", name, ver)
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantN string
|
||||
wantV string
|
||||
}{
|
||||
{
|
||||
name: "standard",
|
||||
input: "curl-8.5.0-r0 description:\nsome stuff",
|
||||
wantN: "curl",
|
||||
wantV: "8.5.0-r0",
|
||||
},
|
||||
{
|
||||
name: "single line no version",
|
||||
input: "noversion",
|
||||
wantN: "noversion",
|
||||
wantV: "",
|
||||
},
|
||||
{
|
||||
name: "multi-hyphen name",
|
||||
input: "lib-ssl-dev-3.0.0-r0 some text",
|
||||
wantN: "lib-ssl-dev",
|
||||
wantV: "3.0.0-r0",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
name, ver := parseInfoNameVersion(tt.input)
|
||||
if name != tt.wantN || ver != tt.wantV {
|
||||
t.Errorf("got (%q, %q), want (%q, %q)", name, ver, tt.wantN, tt.wantV)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,3 +333,148 @@ func TestName(t *testing.T) {
|
||||
t.Errorf("expected apk, got %q", a.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// Compile-time interface compliance checks
|
||||
var (
|
||||
_ snack.VersionQuerier = (*Apk)(nil)
|
||||
_ snack.Cleaner = (*Apk)(nil)
|
||||
_ snack.FileOwner = (*Apk)(nil)
|
||||
_ snack.DryRunner = (*Apk)(nil)
|
||||
_ snack.PackageUpgrader = (*Apk)(nil)
|
||||
)
|
||||
|
||||
func TestInterfaceCompliance(t *testing.T) {
|
||||
// Verify at test time as well
|
||||
var m snack.Manager = New()
|
||||
if _, ok := m.(snack.VersionQuerier); !ok {
|
||||
t.Error("Apk should implement VersionQuerier")
|
||||
}
|
||||
if _, ok := m.(snack.Cleaner); !ok {
|
||||
t.Error("Apk should implement Cleaner")
|
||||
}
|
||||
if _, ok := m.(snack.FileOwner); !ok {
|
||||
t.Error("Apk should implement FileOwner")
|
||||
}
|
||||
if _, ok := m.(snack.DryRunner); !ok {
|
||||
t.Error("Apk should implement DryRunner")
|
||||
}
|
||||
if _, ok := m.(snack.PackageUpgrader); !ok {
|
||||
t.Error("Apk should implement PackageUpgrader")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilities(t *testing.T) {
|
||||
caps := snack.GetCapabilities(New())
|
||||
if !caps.VersionQuery {
|
||||
t.Error("expected VersionQuery=true")
|
||||
}
|
||||
if !caps.Clean {
|
||||
t.Error("expected Clean=true")
|
||||
}
|
||||
if !caps.FileOwnership {
|
||||
t.Error("expected FileOwnership=true")
|
||||
}
|
||||
if !caps.DryRun {
|
||||
t.Error("expected DryRun=true")
|
||||
}
|
||||
// Should be false
|
||||
if caps.Hold {
|
||||
t.Error("expected Hold=false")
|
||||
}
|
||||
if caps.RepoManagement {
|
||||
t.Error("expected RepoManagement=false")
|
||||
}
|
||||
if caps.KeyManagement {
|
||||
t.Error("expected KeyManagement=false")
|
||||
}
|
||||
if caps.Groups {
|
||||
t.Error("expected Groups=false")
|
||||
}
|
||||
if caps.NameNormalize {
|
||||
t.Error("expected NameNormalize=false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportsDryRun(t *testing.T) {
|
||||
a := New()
|
||||
if !a.SupportsDryRun() {
|
||||
t.Error("SupportsDryRun() should return true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUpgradeSimulation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantLen int
|
||||
wantPkgs []snack.Package
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
input: "",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "OK only",
|
||||
input: "OK: 123 MiB in 45 packages\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "single upgrade",
|
||||
input: "(1/1) Upgrading curl (8.5.0-r0 -> 8.6.0-r0)\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "curl", Version: "8.6.0-r0", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple upgrades",
|
||||
input: `(1/3) Upgrading musl (1.2.4-r2 -> 1.2.5-r0)
|
||||
(2/3) Upgrading openssl (3.1.4-r0 -> 3.2.0-r0)
|
||||
(3/3) Upgrading curl (8.5.0-r0 -> 8.6.0-r0)
|
||||
OK: 123 MiB in 45 packages
|
||||
`,
|
||||
wantLen: 3,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "musl", Version: "1.2.5-r0", Installed: true},
|
||||
{Name: "openssl", Version: "3.2.0-r0", Installed: true},
|
||||
{Name: "curl", Version: "8.6.0-r0", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-upgrade lines only",
|
||||
input: "Purging old package\nInstalling new-pkg\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "upgrade without version parens",
|
||||
input: "(1/1) Upgrading busybox\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "busybox", Version: "", Installed: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgs := parseUpgradeSimulation(tt.input)
|
||||
if len(pkgs) != tt.wantLen {
|
||||
t.Fatalf("expected %d packages, got %d", tt.wantLen, len(pkgs))
|
||||
}
|
||||
for i, want := range tt.wantPkgs {
|
||||
if i >= len(pkgs) {
|
||||
break
|
||||
}
|
||||
if pkgs[i].Name != want.Name {
|
||||
t.Errorf("pkg[%d].Name = %q, want %q", i, pkgs[i].Name, want.Name)
|
||||
}
|
||||
if pkgs[i].Version != want.Version {
|
||||
t.Errorf("pkg[%d].Version = %q, want %q", i, pkgs[i].Version, want.Version)
|
||||
}
|
||||
if pkgs[i].Installed != want.Installed {
|
||||
t.Errorf("pkg[%d].Installed = %v, want %v", i, pkgs[i].Installed, want.Installed)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,5 +67,49 @@ func TestNew(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Apt implements snack.Manager at compile time.
|
||||
var _ snack.Manager = (*Apt)(nil)
|
||||
func TestSupportsDryRun(t *testing.T) {
|
||||
a := New()
|
||||
if !a.SupportsDryRun() {
|
||||
t.Error("expected SupportsDryRun() = true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilities(t *testing.T) {
|
||||
caps := snack.GetCapabilities(New())
|
||||
checks := []struct {
|
||||
name string
|
||||
got bool
|
||||
want bool
|
||||
}{
|
||||
{"VersionQuery", caps.VersionQuery, true},
|
||||
{"Hold", caps.Hold, true},
|
||||
{"Clean", caps.Clean, true},
|
||||
{"FileOwnership", caps.FileOwnership, true},
|
||||
{"RepoManagement", caps.RepoManagement, true},
|
||||
{"KeyManagement", caps.KeyManagement, true},
|
||||
{"Groups", caps.Groups, false},
|
||||
{"NameNormalize", caps.NameNormalize, true},
|
||||
{"DryRun", caps.DryRun, true},
|
||||
}
|
||||
for _, c := range checks {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if c.got != c.want {
|
||||
t.Errorf("Capabilities.%s = %v, want %v", c.name, c.got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Compile-time interface checks.
|
||||
var (
|
||||
_ snack.Manager = (*Apt)(nil)
|
||||
_ snack.VersionQuerier = (*Apt)(nil)
|
||||
_ snack.Holder = (*Apt)(nil)
|
||||
_ snack.Cleaner = (*Apt)(nil)
|
||||
_ snack.FileOwner = (*Apt)(nil)
|
||||
_ snack.RepoManager = (*Apt)(nil)
|
||||
_ snack.KeyManager = (*Apt)(nil)
|
||||
_ snack.NameNormalizer = (*Apt)(nil)
|
||||
_ snack.DryRunner = (*Apt)(nil)
|
||||
_ snack.PackageUpgrader = (*Apt)(nil)
|
||||
)
|
||||
|
||||
@@ -23,17 +23,11 @@ func latestVersion(ctx context.Context, pkg string) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
||||
}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Candidate:") {
|
||||
candidate := strings.TrimSpace(strings.TrimPrefix(line, "Candidate:"))
|
||||
if candidate == "(none)" {
|
||||
return "", fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
||||
}
|
||||
return candidate, nil
|
||||
}
|
||||
candidate := parsePolicyCandidate(string(out))
|
||||
if candidate == "" {
|
||||
return "", fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
||||
}
|
||||
return "", fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
||||
return candidate, nil
|
||||
}
|
||||
|
||||
func listUpgrades(ctx context.Context) ([]snack.Package, error) {
|
||||
@@ -45,38 +39,7 @@ func listUpgrades(ctx context.Context) ([]snack.Package, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("apt-get --just-print upgrade: %w", err)
|
||||
}
|
||||
var pkgs []snack.Package
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
// Lines starting with "Inst " indicate upgradable packages.
|
||||
// Format: "Inst pkg [old-ver] (new-ver repo [arch])"
|
||||
if !strings.HasPrefix(line, "Inst ") {
|
||||
continue
|
||||
}
|
||||
line = strings.TrimPrefix(line, "Inst ")
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
name := fields[0]
|
||||
// Find the new version in parentheses
|
||||
parenStart := strings.Index(line, "(")
|
||||
parenEnd := strings.Index(line, ")")
|
||||
if parenStart < 0 || parenEnd < 0 {
|
||||
continue
|
||||
}
|
||||
verFields := strings.Fields(line[parenStart+1 : parenEnd])
|
||||
if len(verFields) < 1 {
|
||||
continue
|
||||
}
|
||||
p := snack.Package{
|
||||
Name: name,
|
||||
Version: verFields[0],
|
||||
Installed: true,
|
||||
}
|
||||
pkgs = append(pkgs, p)
|
||||
}
|
||||
return pkgs, nil
|
||||
return parseUpgradeSimulation(string(out)), nil
|
||||
}
|
||||
|
||||
func upgradeAvailable(ctx context.Context, pkg string) (bool, error) {
|
||||
@@ -85,19 +48,12 @@ func upgradeAvailable(ctx context.Context, pkg string) (bool, error) {
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
||||
}
|
||||
var installed, candidate string
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Installed:") {
|
||||
installed = strings.TrimSpace(strings.TrimPrefix(line, "Installed:"))
|
||||
} else if strings.HasPrefix(line, "Candidate:") {
|
||||
candidate = strings.TrimSpace(strings.TrimPrefix(line, "Candidate:"))
|
||||
}
|
||||
}
|
||||
if installed == "(none)" || installed == "" {
|
||||
installed := parsePolicyInstalled(string(out))
|
||||
candidate := parsePolicyCandidate(string(out))
|
||||
if installed == "" {
|
||||
return false, fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotInstalled)
|
||||
}
|
||||
if candidate == "(none)" || candidate == "" || candidate == installed {
|
||||
if candidate == "" || candidate == installed {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
@@ -148,15 +104,7 @@ func listHeld(ctx context.Context) ([]snack.Package, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("apt-mark showhold: %w", err)
|
||||
}
|
||||
var pkgs []snack.Package
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
pkgs = append(pkgs, snack.Package{Name: line, Installed: true})
|
||||
}
|
||||
return pkgs, nil
|
||||
return parseHoldList(string(out)), nil
|
||||
}
|
||||
|
||||
func isHeld(ctx context.Context, pkg string) (bool, error) {
|
||||
@@ -198,14 +146,7 @@ func fileList(ctx context.Context, pkg string) ([]string, error) {
|
||||
}
|
||||
return nil, fmt.Errorf("dpkg-query -L %s: %w: %s", pkg, err, errMsg)
|
||||
}
|
||||
var files []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
files = append(files, line)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
return parseFileList(string(out)), nil
|
||||
}
|
||||
|
||||
func owner(ctx context.Context, path string) (string, error) {
|
||||
@@ -216,18 +157,11 @@ func owner(ctx context.Context, path string) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("dpkg -S %s: %w", path, snack.ErrNotFound)
|
||||
}
|
||||
// Output format: "package: /path/to/file" or "package1, package2: /path"
|
||||
line := strings.TrimSpace(strings.Split(string(out), "\n")[0])
|
||||
colonIdx := strings.Index(line, ":")
|
||||
if colonIdx < 0 {
|
||||
pkg := parseOwner(string(out))
|
||||
if pkg == "" {
|
||||
return "", fmt.Errorf("dpkg -S %s: unexpected output", path)
|
||||
}
|
||||
// Return first package if multiple
|
||||
pkgPart := line[:colonIdx]
|
||||
if commaIdx := strings.Index(pkgPart, ","); commaIdx >= 0 {
|
||||
pkgPart = strings.TrimSpace(pkgPart[:commaIdx])
|
||||
}
|
||||
return strings.TrimSpace(pkgPart), nil
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
// --- RepoManager ---
|
||||
@@ -249,51 +183,14 @@ func listRepos(_ context.Context) ([]snack.Repository, error) {
|
||||
}
|
||||
scanner := bufio.NewScanner(bytes.NewReader(data))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
enabled := true
|
||||
// deb822 format (.sources files) not fully parsed; treat as single entry
|
||||
if strings.HasPrefix(line, "deb ") || strings.HasPrefix(line, "deb-src ") {
|
||||
repos = append(repos, snack.Repository{
|
||||
ID: line,
|
||||
URL: extractURL(line),
|
||||
Enabled: enabled,
|
||||
Type: strings.Fields(line)[0],
|
||||
})
|
||||
if r := parseSourcesLine(scanner.Text()); r != nil {
|
||||
repos = append(repos, *r)
|
||||
}
|
||||
}
|
||||
}
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
// extractURL pulls the URL from a deb/deb-src line.
|
||||
func extractURL(line string) string {
|
||||
fields := strings.Fields(line)
|
||||
inOptions := false
|
||||
for i, f := range fields {
|
||||
if i == 0 {
|
||||
continue // skip deb/deb-src
|
||||
}
|
||||
if inOptions {
|
||||
if strings.HasSuffix(f, "]") {
|
||||
inOptions = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(f, "[") {
|
||||
if strings.HasSuffix(f, "]") {
|
||||
// Single-token options like [arch=amd64]
|
||||
continue
|
||||
}
|
||||
inOptions = true
|
||||
continue
|
||||
}
|
||||
return f
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func addRepo(ctx context.Context, repo snack.Repository) error {
|
||||
repoLine := repo.URL
|
||||
|
||||
@@ -150,50 +150,3 @@ func TestBuildArgs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "basic_deb",
|
||||
input: "deb http://archive.ubuntu.com/ubuntu/ jammy main",
|
||||
want: "http://archive.ubuntu.com/ubuntu/",
|
||||
},
|
||||
{
|
||||
name: "deb_src",
|
||||
input: "deb-src http://archive.ubuntu.com/ubuntu/ jammy main",
|
||||
want: "http://archive.ubuntu.com/ubuntu/",
|
||||
},
|
||||
{
|
||||
name: "with_options",
|
||||
input: "deb [arch=amd64] https://apt.example.com/repo stable main",
|
||||
want: "https://apt.example.com/repo",
|
||||
},
|
||||
{
|
||||
name: "with_signed_by",
|
||||
input: "deb [arch=amd64 signed-by=/etc/apt/keyrings/key.gpg] https://repo.example.com/deb stable main",
|
||||
want: "https://repo.example.com/deb",
|
||||
},
|
||||
{
|
||||
name: "just_type",
|
||||
input: "deb",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "empty_options_bracket",
|
||||
input: "deb [] http://example.com/repo stable",
|
||||
want: "http://example.com/repo",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := extractURL(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("extractURL(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
152
apt/parse.go
152
apt/parse.go
@@ -51,6 +51,158 @@ func parseSearch(output string) []snack.Package {
|
||||
return pkgs
|
||||
}
|
||||
|
||||
// parsePolicyCandidate extracts the Candidate version from apt-cache policy output.
|
||||
// Returns empty string if no candidate is found or candidate is "(none)".
|
||||
func parsePolicyCandidate(output string) string {
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Candidate:") {
|
||||
candidate := strings.TrimSpace(strings.TrimPrefix(line, "Candidate:"))
|
||||
if candidate == "(none)" {
|
||||
return ""
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// parsePolicyInstalled extracts the Installed version from apt-cache policy output.
|
||||
// Returns empty string if not installed or "(none)".
|
||||
func parsePolicyInstalled(output string) string {
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Installed:") {
|
||||
installed := strings.TrimSpace(strings.TrimPrefix(line, "Installed:"))
|
||||
if installed == "(none)" {
|
||||
return ""
|
||||
}
|
||||
return installed
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// parseUpgradeSimulation parses apt-get --just-print upgrade output.
|
||||
// Lines starting with "Inst " indicate upgradable packages.
|
||||
// Format: "Inst pkg [old-ver] (new-ver repo [arch])"
|
||||
func parseUpgradeSimulation(output string) []snack.Package {
|
||||
var pkgs []snack.Package
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, "Inst ") {
|
||||
continue
|
||||
}
|
||||
line = strings.TrimPrefix(line, "Inst ")
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
name := fields[0]
|
||||
parenStart := strings.Index(line, "(")
|
||||
parenEnd := strings.Index(line, ")")
|
||||
if parenStart < 0 || parenEnd < 0 {
|
||||
continue
|
||||
}
|
||||
verFields := strings.Fields(line[parenStart+1 : parenEnd])
|
||||
if len(verFields) < 1 {
|
||||
continue
|
||||
}
|
||||
pkgs = append(pkgs, snack.Package{
|
||||
Name: name,
|
||||
Version: verFields[0],
|
||||
Installed: true,
|
||||
})
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
// parseHoldList parses apt-mark showhold output (one package name per line).
|
||||
func parseHoldList(output string) []snack.Package {
|
||||
var pkgs []snack.Package
|
||||
for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
pkgs = append(pkgs, snack.Package{Name: line, Installed: true})
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
// parseFileList parses dpkg-query -L output (one file path per line).
|
||||
func parseFileList(output string) []string {
|
||||
var files []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
files = append(files, line)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// parseOwner parses dpkg -S output to extract the owning package name.
|
||||
// Output format: "package: /path/to/file" or "pkg1, pkg2: /path".
|
||||
// Returns the first package name.
|
||||
func parseOwner(output string) string {
|
||||
line := strings.TrimSpace(strings.Split(output, "\n")[0])
|
||||
colonIdx := strings.Index(line, ":")
|
||||
if colonIdx < 0 {
|
||||
return ""
|
||||
}
|
||||
pkgPart := line[:colonIdx]
|
||||
if commaIdx := strings.Index(pkgPart, ","); commaIdx >= 0 {
|
||||
pkgPart = strings.TrimSpace(pkgPart[:commaIdx])
|
||||
}
|
||||
return strings.TrimSpace(pkgPart)
|
||||
}
|
||||
|
||||
// parseSourcesLine parses a single deb/deb-src line from sources.list.
|
||||
// Returns a Repository if the line is valid, or nil if it's a comment/blank.
|
||||
func parseSourcesLine(line string) *snack.Repository {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasPrefix(line, "deb ") && !strings.HasPrefix(line, "deb-src ") {
|
||||
return nil
|
||||
}
|
||||
return &snack.Repository{
|
||||
ID: line,
|
||||
URL: extractURL(line),
|
||||
Enabled: true,
|
||||
Type: strings.Fields(line)[0],
|
||||
}
|
||||
}
|
||||
|
||||
// extractURL pulls the URL from a deb/deb-src line.
|
||||
func extractURL(line string) string {
|
||||
fields := strings.Fields(line)
|
||||
inOptions := false
|
||||
for i, f := range fields {
|
||||
if i == 0 {
|
||||
continue // skip deb/deb-src
|
||||
}
|
||||
if inOptions {
|
||||
if strings.HasSuffix(f, "]") {
|
||||
inOptions = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(f, "[") {
|
||||
if strings.HasSuffix(f, "]") {
|
||||
// Single-token options like [arch=amd64]
|
||||
continue
|
||||
}
|
||||
inOptions = true
|
||||
continue
|
||||
}
|
||||
return f
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// parseInfo parses apt-cache show output into a Package.
|
||||
func parseInfo(output string) (*snack.Package, error) {
|
||||
p := &snack.Package{}
|
||||
|
||||
@@ -194,3 +194,445 @@ func TestParseInfo_EdgeCases(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- New parse function tests ---
|
||||
|
||||
func TestParsePolicyCandidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "normal_policy_output",
|
||||
input: `bash:
|
||||
Installed: 5.2-1
|
||||
Candidate: 5.2-2
|
||||
Version table:
|
||||
5.2-2 500
|
||||
500 http://archive.ubuntu.com/ubuntu jammy/main amd64 Packages
|
||||
*** 5.2-1 100
|
||||
100 /var/lib/dpkg/status`,
|
||||
want: "5.2-2",
|
||||
},
|
||||
{
|
||||
name: "candidate_none",
|
||||
input: `virtual-pkg:
|
||||
Installed: (none)
|
||||
Candidate: (none)
|
||||
Version table:`,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "empty_input",
|
||||
input: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "installed_equals_candidate",
|
||||
input: `curl:
|
||||
Installed: 7.88.1-10+deb12u4
|
||||
Candidate: 7.88.1-10+deb12u4
|
||||
Version table:
|
||||
*** 7.88.1-10+deb12u4 500`,
|
||||
want: "7.88.1-10+deb12u4",
|
||||
},
|
||||
{
|
||||
name: "epoch_version",
|
||||
input: `systemd:
|
||||
Installed: 1:252-2
|
||||
Candidate: 1:252-3
|
||||
Version table:`,
|
||||
want: "1:252-3",
|
||||
},
|
||||
{
|
||||
name: "no_candidate_line",
|
||||
input: "bash:\n Installed: 5.2-1\n",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parsePolicyCandidate(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("parsePolicyCandidate() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePolicyInstalled(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "normal",
|
||||
input: `bash:
|
||||
Installed: 5.2-1
|
||||
Candidate: 5.2-2`,
|
||||
want: "5.2-1",
|
||||
},
|
||||
{
|
||||
name: "not_installed",
|
||||
input: `foo:
|
||||
Installed: (none)
|
||||
Candidate: 1.0`,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
input: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "epoch_version",
|
||||
input: `systemd:
|
||||
Installed: 1:252-2
|
||||
Candidate: 1:252-3`,
|
||||
want: "1:252-2",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parsePolicyInstalled(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("parsePolicyInstalled() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUpgradeSimulation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []snack.Package
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
input: "",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "single_upgrade",
|
||||
input: `Reading package lists...
|
||||
Building dependency tree...
|
||||
Reading state information...
|
||||
The following packages will be upgraded:
|
||||
bash
|
||||
1 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
|
||||
Inst bash [5.2-1] (5.2-2 Ubuntu:22.04/jammy [amd64])
|
||||
Conf bash (5.2-2 Ubuntu:22.04/jammy [amd64])`,
|
||||
want: []snack.Package{
|
||||
{Name: "bash", Version: "5.2-2", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple_upgrades",
|
||||
input: `Inst bash [5.2-1] (5.2-2 Ubuntu:22.04/jammy [amd64])
|
||||
Inst curl [7.88.0] (7.88.1 Ubuntu:22.04/jammy [amd64])
|
||||
Inst systemd [1:252-1] (1:252-2 Ubuntu:22.04/jammy [amd64])
|
||||
Conf bash (5.2-2 Ubuntu:22.04/jammy [amd64])
|
||||
Conf curl (7.88.1 Ubuntu:22.04/jammy [amd64])
|
||||
Conf systemd (1:252-2 Ubuntu:22.04/jammy [amd64])`,
|
||||
want: []snack.Package{
|
||||
{Name: "bash", Version: "5.2-2", Installed: true},
|
||||
{Name: "curl", Version: "7.88.1", Installed: true},
|
||||
{Name: "systemd", Version: "1:252-2", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no_inst_lines",
|
||||
input: "Reading package lists...\nBuilding dependency tree...\n0 upgraded, 0 newly installed.\n",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "inst_without_parens",
|
||||
input: "Inst bash no-parens-here\n",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "inst_with_empty_parens",
|
||||
input: "Inst bash [5.2-1] ()\n",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "conf_lines_ignored",
|
||||
input: "Conf bash (5.2-2 Ubuntu:22.04/jammy [amd64])\n",
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseUpgradeSimulation(tt.input)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("parseUpgradeSimulation() returned %d packages, want %d", len(got), len(tt.want))
|
||||
}
|
||||
for i, g := range got {
|
||||
w := tt.want[i]
|
||||
if g.Name != w.Name || g.Version != w.Version || g.Installed != w.Installed {
|
||||
t.Errorf("package[%d] = %+v, want %+v", i, g, w)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHoldList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
}{
|
||||
{"empty", "", nil},
|
||||
{"whitespace_only", " \n \n", nil},
|
||||
{"single_package", "bash\n", []string{"bash"}},
|
||||
{"multiple_packages", "bash\ncurl\nnginx\n", []string{"bash", "curl", "nginx"}},
|
||||
{"blank_lines_mixed", "\nbash\n\ncurl\n\n", []string{"bash", "curl"}},
|
||||
{"trailing_whitespace", " bash \n curl \n", []string{"bash", "curl"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgs := parseHoldList(tt.input)
|
||||
var names []string
|
||||
for _, p := range pkgs {
|
||||
names = append(names, p.Name)
|
||||
if !p.Installed {
|
||||
t.Errorf("expected Installed=true for %q", p.Name)
|
||||
}
|
||||
}
|
||||
if len(names) != len(tt.want) {
|
||||
t.Fatalf("got %d packages, want %d", len(names), len(tt.want))
|
||||
}
|
||||
for i, n := range names {
|
||||
if n != tt.want[i] {
|
||||
t.Errorf("package[%d] = %q, want %q", i, n, tt.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFileList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
}{
|
||||
{"empty", "", nil},
|
||||
{"whitespace_only", " \n\n ", nil},
|
||||
{
|
||||
name: "single_file",
|
||||
input: "/usr/bin/bash\n",
|
||||
want: []string{"/usr/bin/bash"},
|
||||
},
|
||||
{
|
||||
name: "multiple_files",
|
||||
input: "/.\n/usr\n/usr/bin\n/usr/bin/bash\n/usr/share/man/man1/bash.1.gz\n",
|
||||
want: []string{"/.", "/usr", "/usr/bin", "/usr/bin/bash", "/usr/share/man/man1/bash.1.gz"},
|
||||
},
|
||||
{
|
||||
name: "blank_lines_mixed",
|
||||
input: "\n/usr/bin/curl\n\n/usr/share/doc/curl\n\n",
|
||||
want: []string{"/usr/bin/curl", "/usr/share/doc/curl"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseFileList(tt.input)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("parseFileList() returned %d files, want %d", len(got), len(tt.want))
|
||||
}
|
||||
for i, f := range got {
|
||||
if f != tt.want[i] {
|
||||
t.Errorf("file[%d] = %q, want %q", i, f, tt.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOwner(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "single_package",
|
||||
input: "bash: /usr/bin/bash\n",
|
||||
want: "bash",
|
||||
},
|
||||
{
|
||||
name: "multiple_packages",
|
||||
input: "bash, dash: /usr/bin/sh\n",
|
||||
want: "bash",
|
||||
},
|
||||
{
|
||||
name: "package_with_arch",
|
||||
input: "libc6:amd64: /lib/x86_64-linux-gnu/libc.so.6\n",
|
||||
want: "libc6", // parseOwner splits on first colon, arch suffix is stripped
|
||||
},
|
||||
{
|
||||
name: "multiple_lines",
|
||||
input: "coreutils: /usr/bin/ls\ncoreutils: /usr/bin/cat\n",
|
||||
want: "coreutils",
|
||||
},
|
||||
{
|
||||
name: "no_colon",
|
||||
input: "unexpected output without colon",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
input: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "whitespace_around_package",
|
||||
input: " nginx : /usr/sbin/nginx\n",
|
||||
want: "nginx",
|
||||
},
|
||||
{
|
||||
name: "three_packages_comma_separated",
|
||||
input: "pkg1, pkg2, pkg3: /some/path\n",
|
||||
want: "pkg1",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseOwner(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("parseOwner() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSourcesLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantNil bool
|
||||
wantURL string
|
||||
wantTyp string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
input: "",
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "comment",
|
||||
input: "# This is a comment",
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "non_deb_line",
|
||||
input: "some random text",
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "basic_deb",
|
||||
input: "deb http://archive.ubuntu.com/ubuntu/ jammy main",
|
||||
wantURL: "http://archive.ubuntu.com/ubuntu/",
|
||||
wantTyp: "deb",
|
||||
},
|
||||
{
|
||||
name: "deb_src",
|
||||
input: "deb-src http://archive.ubuntu.com/ubuntu/ jammy main",
|
||||
wantURL: "http://archive.ubuntu.com/ubuntu/",
|
||||
wantTyp: "deb-src",
|
||||
},
|
||||
{
|
||||
name: "with_options",
|
||||
input: "deb [arch=amd64] https://repo.example.com/deb stable main",
|
||||
wantURL: "https://repo.example.com/deb",
|
||||
wantTyp: "deb",
|
||||
},
|
||||
{
|
||||
name: "with_signed_by",
|
||||
input: "deb [arch=amd64 signed-by=/etc/apt/keyrings/key.gpg] https://repo.example.com stable main",
|
||||
wantURL: "https://repo.example.com",
|
||||
wantTyp: "deb",
|
||||
},
|
||||
{
|
||||
name: "leading_whitespace",
|
||||
input: " deb http://example.com/repo stable main",
|
||||
wantURL: "http://example.com/repo",
|
||||
wantTyp: "deb",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := parseSourcesLine(tt.input)
|
||||
if tt.wantNil {
|
||||
if r != nil {
|
||||
t.Errorf("parseSourcesLine() = %+v, want nil", r)
|
||||
}
|
||||
return
|
||||
}
|
||||
if r == nil {
|
||||
t.Fatal("parseSourcesLine() = nil, want non-nil")
|
||||
}
|
||||
if r.URL != tt.wantURL {
|
||||
t.Errorf("URL = %q, want %q", r.URL, tt.wantURL)
|
||||
}
|
||||
if r.Type != tt.wantTyp {
|
||||
t.Errorf("Type = %q, want %q", r.Type, tt.wantTyp)
|
||||
}
|
||||
if !r.Enabled {
|
||||
t.Error("expected Enabled=true")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "basic_deb",
|
||||
input: "deb http://archive.ubuntu.com/ubuntu/ jammy main",
|
||||
want: "http://archive.ubuntu.com/ubuntu/",
|
||||
},
|
||||
{
|
||||
name: "deb_src",
|
||||
input: "deb-src http://archive.ubuntu.com/ubuntu/ jammy main",
|
||||
want: "http://archive.ubuntu.com/ubuntu/",
|
||||
},
|
||||
{
|
||||
name: "with_options",
|
||||
input: "deb [arch=amd64] https://apt.example.com/repo stable main",
|
||||
want: "https://apt.example.com/repo",
|
||||
},
|
||||
{
|
||||
name: "with_signed_by",
|
||||
input: "deb [arch=amd64 signed-by=/etc/apt/keyrings/key.gpg] https://repo.example.com/deb stable main",
|
||||
want: "https://repo.example.com/deb",
|
||||
},
|
||||
{
|
||||
name: "just_type",
|
||||
input: "deb",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "empty_options_bracket",
|
||||
input: "deb [] http://example.com/repo stable",
|
||||
want: "http://example.com/repo",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := extractURL(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("extractURL(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
140
aur/aur_test.go
140
aur/aur_test.go
@@ -3,7 +3,9 @@ package aur
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogrlx/snack"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParsePackageList(t *testing.T) {
|
||||
@@ -63,3 +65,141 @@ func TestNewWithOptions(t *testing.T) {
|
||||
assert.Equal(t, "/tmp/aur-builds", a.BuildDir)
|
||||
assert.Equal(t, []string{"--skippgpcheck", "--nocheck"}, a.MakepkgFlags)
|
||||
}
|
||||
|
||||
func TestInterfaceCompliance(t *testing.T) {
|
||||
var _ snack.Manager = (*AUR)(nil)
|
||||
var _ snack.VersionQuerier = (*AUR)(nil)
|
||||
var _ snack.Cleaner = (*AUR)(nil)
|
||||
var _ snack.PackageUpgrader = (*AUR)(nil)
|
||||
}
|
||||
|
||||
func TestInterfaceNonCompliance(t *testing.T) {
|
||||
a := New()
|
||||
var m snack.Manager = a
|
||||
|
||||
if _, ok := m.(snack.FileOwner); ok {
|
||||
t.Error("AUR should not implement FileOwner")
|
||||
}
|
||||
if _, ok := m.(snack.Holder); ok {
|
||||
t.Error("AUR should not implement Holder")
|
||||
}
|
||||
if _, ok := m.(snack.RepoManager); ok {
|
||||
t.Error("AUR should not implement RepoManager")
|
||||
}
|
||||
if _, ok := m.(snack.KeyManager); ok {
|
||||
t.Error("AUR should not implement KeyManager")
|
||||
}
|
||||
if _, ok := m.(snack.Grouper); ok {
|
||||
t.Error("AUR should not implement Grouper")
|
||||
}
|
||||
if _, ok := m.(snack.NameNormalizer); ok {
|
||||
t.Error("AUR should not implement NameNormalizer")
|
||||
}
|
||||
if _, ok := m.(snack.DryRunner); ok {
|
||||
t.Error("AUR should not implement DryRunner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilities(t *testing.T) {
|
||||
caps := snack.GetCapabilities(New())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
got bool
|
||||
want bool
|
||||
}{
|
||||
{"VersionQuery", caps.VersionQuery, true},
|
||||
{"Clean", caps.Clean, true},
|
||||
{"FileOwnership", caps.FileOwnership, false},
|
||||
{"Hold", caps.Hold, false},
|
||||
{"RepoManagement", caps.RepoManagement, false},
|
||||
{"KeyManagement", caps.KeyManagement, false},
|
||||
{"Groups", caps.Groups, false},
|
||||
{"NameNormalize", caps.NameNormalize, false},
|
||||
{"DryRun", caps.DryRun, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.got != tt.want {
|
||||
t.Errorf("%s = %v, want %v", tt.name, tt.got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
a := New()
|
||||
assert.Equal(t, "aur", a.Name())
|
||||
}
|
||||
|
||||
func TestParsePackageList_EdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantLen int
|
||||
wantNames []string
|
||||
wantVers []string
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
input: " \n\t\n \n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "single package",
|
||||
input: "yay 12.5.7-1\n",
|
||||
wantLen: 1,
|
||||
wantNames: []string{"yay"},
|
||||
wantVers: []string{"12.5.7-1"},
|
||||
},
|
||||
{
|
||||
name: "malformed single field",
|
||||
input: "orphan\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "malformed mixed with valid",
|
||||
input: "orphan\nyay 12.5.7-1\nbadline\nparu 2.0-1\n",
|
||||
wantLen: 2,
|
||||
wantNames: []string{"yay", "paru"},
|
||||
wantVers: []string{"12.5.7-1", "2.0-1"},
|
||||
},
|
||||
{
|
||||
name: "extra fields ignored",
|
||||
input: "yay 12.5.7-1 extra stuff\n",
|
||||
wantLen: 1,
|
||||
wantNames: []string{"yay"},
|
||||
wantVers: []string{"12.5.7-1"},
|
||||
},
|
||||
{
|
||||
name: "trailing and leading whitespace on lines",
|
||||
input: " yay 12.5.7-1 \n paru 2.0.4-1\n\n",
|
||||
wantLen: 2,
|
||||
wantNames: []string{"yay", "paru"},
|
||||
wantVers: []string{"12.5.7-1", "2.0.4-1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgs := parsePackageList(tt.input)
|
||||
require.Len(t, pkgs, tt.wantLen)
|
||||
for i, p := range pkgs {
|
||||
assert.Equal(t, "aur", p.Repository, "all packages should have Repository=aur")
|
||||
assert.True(t, p.Installed, "all packages should have Installed=true")
|
||||
if i < len(tt.wantNames) {
|
||||
assert.Equal(t, tt.wantNames[i], p.Name)
|
||||
}
|
||||
if i < len(tt.wantVers) {
|
||||
assert.Equal(t, tt.wantVers[i], p.Version)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
86
dnf/dnf_test.go
Normal file
86
dnf/dnf_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package dnf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogrlx/snack"
|
||||
)
|
||||
|
||||
// Compile-time interface assertions — DNF implements all optional interfaces.
|
||||
var (
|
||||
_ snack.Manager = (*DNF)(nil)
|
||||
_ snack.VersionQuerier = (*DNF)(nil)
|
||||
_ snack.Holder = (*DNF)(nil)
|
||||
_ snack.Cleaner = (*DNF)(nil)
|
||||
_ snack.FileOwner = (*DNF)(nil)
|
||||
_ snack.RepoManager = (*DNF)(nil)
|
||||
_ snack.KeyManager = (*DNF)(nil)
|
||||
_ snack.Grouper = (*DNF)(nil)
|
||||
_ snack.NameNormalizer = (*DNF)(nil)
|
||||
_ snack.DryRunner = (*DNF)(nil)
|
||||
_ snack.PackageUpgrader = (*DNF)(nil)
|
||||
)
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
d := New()
|
||||
if got := d.Name(); got != "dnf" {
|
||||
t.Errorf("Name() = %q, want %q", got, "dnf")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportsDryRun(t *testing.T) {
|
||||
d := New()
|
||||
if !d.SupportsDryRun() {
|
||||
t.Error("SupportsDryRun() = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCapabilities(t *testing.T) {
|
||||
d := New()
|
||||
caps := snack.GetCapabilities(d)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
got bool
|
||||
}{
|
||||
{"VersionQuery", caps.VersionQuery},
|
||||
{"Hold", caps.Hold},
|
||||
{"Clean", caps.Clean},
|
||||
{"FileOwnership", caps.FileOwnership},
|
||||
{"RepoManagement", caps.RepoManagement},
|
||||
{"KeyManagement", caps.KeyManagement},
|
||||
{"Groups", caps.Groups},
|
||||
{"NameNormalize", caps.NameNormalize},
|
||||
{"DryRun", caps.DryRun},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if !tt.got {
|
||||
t.Errorf("Capabilities.%s = false, want true", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeNameMethod(t *testing.T) {
|
||||
d := New()
|
||||
tests := []struct {
|
||||
input, want string
|
||||
}{
|
||||
{"nginx.x86_64", "nginx"},
|
||||
{"curl", "curl"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := d.NormalizeName(tt.input); got != tt.want {
|
||||
t.Errorf("NormalizeName(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArchMethod(t *testing.T) {
|
||||
d := New()
|
||||
name, arch := d.ParseArch("nginx.x86_64")
|
||||
if name != "nginx" || arch != "x86_64" {
|
||||
t.Errorf("ParseArch(\"nginx.x86_64\") = (%q, %q), want (\"nginx\", \"x86_64\")", name, arch)
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ package dnf
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gogrlx/snack"
|
||||
)
|
||||
|
||||
func TestParseList(t *testing.T) {
|
||||
@@ -458,15 +456,399 @@ updates-testing Fedora 43 - x86_64 - Test Updates
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure interface checks from capabilities.go are satisfied.
|
||||
var (
|
||||
_ snack.Manager = (*DNF)(nil)
|
||||
_ snack.VersionQuerier = (*DNF)(nil)
|
||||
_ snack.Holder = (*DNF)(nil)
|
||||
_ snack.Cleaner = (*DNF)(nil)
|
||||
_ snack.FileOwner = (*DNF)(nil)
|
||||
_ snack.RepoManager = (*DNF)(nil)
|
||||
_ snack.KeyManager = (*DNF)(nil)
|
||||
_ snack.Grouper = (*DNF)(nil)
|
||||
_ snack.NameNormalizer = (*DNF)(nil)
|
||||
)
|
||||
// --- Edge case tests ---
|
||||
|
||||
func TestParseListEmpty(t *testing.T) {
|
||||
pkgs := parseList("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages from empty input, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListSinglePackage(t *testing.T) {
|
||||
input := `Installed Packages
|
||||
curl.x86_64 7.76.1-23.el9 @baseos
|
||||
`
|
||||
pkgs := parseList(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "curl" {
|
||||
t.Errorf("Name = %q, want curl", pkgs[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListMalformedLines(t *testing.T) {
|
||||
input := `Installed Packages
|
||||
curl.x86_64 7.76.1-23.el9 @baseos
|
||||
thislinehasnospaces
|
||||
only-one-field
|
||||
bash.x86_64 5.1.8-6.el9 @anaconda
|
||||
`
|
||||
pkgs := parseList(input)
|
||||
if len(pkgs) != 2 {
|
||||
t.Fatalf("expected 2 packages (skip malformed), got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListNoHeader(t *testing.T) {
|
||||
// Lines that look like packages without the "Installed Packages" header
|
||||
input := `curl.x86_64 7.76.1-23.el9 @baseos
|
||||
bash.x86_64 5.1.8-6.el9 @anaconda
|
||||
`
|
||||
pkgs := parseList(input)
|
||||
if len(pkgs) != 2 {
|
||||
t.Fatalf("expected 2 packages, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListTwoColumns(t *testing.T) {
|
||||
// Only name.arch and version, no repo column
|
||||
input := `Installed Packages
|
||||
curl.x86_64 7.76.1-23.el9
|
||||
`
|
||||
pkgs := parseList(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Repository != "" {
|
||||
t.Errorf("Repository = %q, want empty", pkgs[0].Repository)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSearchEmpty(t *testing.T) {
|
||||
pkgs := parseSearch("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages from empty input, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSearchSingleResult(t *testing.T) {
|
||||
input := `=== Name Exactly Matched: curl ===
|
||||
curl.x86_64 : A utility for getting files from remote servers
|
||||
`
|
||||
pkgs := parseSearch(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "curl" {
|
||||
t.Errorf("Name = %q, want curl", pkgs[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSearchMalformedLines(t *testing.T) {
|
||||
input := `=== Name Matched ===
|
||||
curl.x86_64 : A utility
|
||||
no-separator-here
|
||||
another.line.without : proper : colons
|
||||
bash.noarch : Shell
|
||||
`
|
||||
pkgs := parseSearch(input)
|
||||
// "curl.x86_64 : A utility" and "another.line.without : proper : colons" and "bash.noarch : Shell"
|
||||
if len(pkgs) != 3 {
|
||||
t.Fatalf("expected 3 packages, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoEmpty(t *testing.T) {
|
||||
p := parseInfo("")
|
||||
if p != nil {
|
||||
t.Errorf("expected nil from empty input, got %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoNoName(t *testing.T) {
|
||||
input := `Version : 1.0
|
||||
Architecture : x86_64
|
||||
`
|
||||
p := parseInfo(input)
|
||||
if p != nil {
|
||||
t.Errorf("expected nil when no Name field, got %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoReleaseBeforeVersion(t *testing.T) {
|
||||
// Release without prior Version should not panic
|
||||
input := `Name : test
|
||||
Release : 1.el9
|
||||
Version : 2.0
|
||||
`
|
||||
p := parseInfo(input)
|
||||
if p == nil {
|
||||
t.Fatal("expected non-nil package")
|
||||
}
|
||||
// Release came before Version was set, so it won't append properly,
|
||||
// but Version should at least be set
|
||||
if p.Name != "test" {
|
||||
t.Errorf("Name = %q, want test", p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoFromRepo(t *testing.T) {
|
||||
input := `Name : bash
|
||||
Version : 5.1.8
|
||||
Release : 6.el9
|
||||
From repo : baseos
|
||||
Summary : The GNU Bourne Again shell
|
||||
`
|
||||
p := parseInfo(input)
|
||||
if p == nil {
|
||||
t.Fatal("expected non-nil package")
|
||||
}
|
||||
if p.Repository != "baseos" {
|
||||
t.Errorf("Repository = %q, want baseos", p.Repository)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVersionLockEmpty(t *testing.T) {
|
||||
pkgs := parseVersionLock("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages from empty input, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVersionLockSingleEntry(t *testing.T) {
|
||||
input := `nginx-0:1.20.1-14.el9_2.1.*
|
||||
`
|
||||
pkgs := parseVersionLock(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "nginx" {
|
||||
t.Errorf("Name = %q, want nginx", pkgs[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRepoListEmpty(t *testing.T) {
|
||||
repos := parseRepoList("")
|
||||
if len(repos) != 0 {
|
||||
t.Errorf("expected 0 repos from empty input, got %d", len(repos))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRepoListSingleRepo(t *testing.T) {
|
||||
input := `repo id repo name status
|
||||
baseos CentOS Stream 9 - BaseOS enabled
|
||||
`
|
||||
repos := parseRepoList(input)
|
||||
if len(repos) != 1 {
|
||||
t.Fatalf("expected 1 repo, got %d", len(repos))
|
||||
}
|
||||
if repos[0].ID != "baseos" || !repos[0].Enabled {
|
||||
t.Errorf("unexpected repo: %+v", repos[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGroupListEmpty(t *testing.T) {
|
||||
groups := parseGroupList("")
|
||||
if len(groups) != 0 {
|
||||
t.Errorf("expected 0 groups from empty input, got %d", len(groups))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGroupInfoEmpty(t *testing.T) {
|
||||
pkgs := parseGroupInfo("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages from empty input, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGroupInfoWithMarks(t *testing.T) {
|
||||
input := `Group: Web Server
|
||||
Mandatory Packages:
|
||||
= httpd
|
||||
+ mod_ssl
|
||||
- php
|
||||
`
|
||||
pkgs := parseGroupInfo(input)
|
||||
if len(pkgs) != 3 {
|
||||
t.Fatalf("expected 3 packages, got %d", len(pkgs))
|
||||
}
|
||||
names := map[string]bool{}
|
||||
for _, p := range pkgs {
|
||||
names[p.Name] = true
|
||||
}
|
||||
for _, want := range []string{"httpd", "mod_ssl", "php"} {
|
||||
if !names[want] {
|
||||
t.Errorf("missing package %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGroupIsInstalledEmpty(t *testing.T) {
|
||||
if parseGroupIsInstalled("", "anything") {
|
||||
t.Error("expected false for empty input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeNameEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
input, want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"pkg.unknown.ext", "pkg.unknown.ext"},
|
||||
{"name.with.dots.x86_64", "name.with.dots"},
|
||||
{"python3.11", "python3.11"},
|
||||
{"glibc.s390x", "glibc"},
|
||||
{"kernel.src", "kernel"},
|
||||
{".x86_64", ""},
|
||||
{"pkg.ppc64le", "pkg"},
|
||||
{"pkg.armv7hl", "pkg"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got := normalizeName(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("normalizeName(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArchEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
input, wantName, wantArch string
|
||||
}{
|
||||
{"", "", ""},
|
||||
{"pkg.i386", "pkg", "i386"},
|
||||
{"pkg.ppc64le", "pkg", "ppc64le"},
|
||||
{"pkg.s390x", "pkg", "s390x"},
|
||||
{"pkg.armv7hl", "pkg", "armv7hl"},
|
||||
{"pkg.src", "pkg", "src"},
|
||||
{"pkg.unknown", "pkg.unknown", ""},
|
||||
{"name.with.many.dots.noarch", "name.with.many.dots", "noarch"},
|
||||
{".noarch", "", "noarch"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
name, arch := parseArch(tt.input)
|
||||
if name != tt.wantName || arch != tt.wantArch {
|
||||
t.Errorf("parseArch(%q) = (%q, %q), want (%q, %q)",
|
||||
tt.input, name, arch, tt.wantName, tt.wantArch)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- dnf5 edge case tests ---
|
||||
|
||||
func TestStripPreambleEmpty(t *testing.T) {
|
||||
got := stripPreamble("")
|
||||
if got != "" {
|
||||
t.Errorf("expected empty, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripPreambleNoPreamble(t *testing.T) {
|
||||
input := "Installed packages\nbash.x86_64 5.3.0-2.fc43 abc\n"
|
||||
got := stripPreamble(input)
|
||||
if got != input {
|
||||
t.Errorf("expected unchanged output when no preamble present")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListDNF5Empty(t *testing.T) {
|
||||
pkgs := parseListDNF5("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages from empty input, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListDNF5SinglePackage(t *testing.T) {
|
||||
input := `Installed packages
|
||||
curl.aarch64 7.76.1-23.el9 abc123
|
||||
`
|
||||
pkgs := parseListDNF5(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "curl" || pkgs[0].Arch != "aarch64" {
|
||||
t.Errorf("unexpected: %+v", pkgs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSearchDNF5Empty(t *testing.T) {
|
||||
pkgs := parseSearchDNF5("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages from empty input, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoDNF5Empty(t *testing.T) {
|
||||
p := parseInfoDNF5("")
|
||||
if p != nil {
|
||||
t.Errorf("expected nil from empty input, got %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoDNF5NoName(t *testing.T) {
|
||||
input := `Version : 1.0
|
||||
Architecture : x86_64
|
||||
`
|
||||
p := parseInfoDNF5(input)
|
||||
if p != nil {
|
||||
t.Errorf("expected nil when no Name field, got %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGroupListDNF5Empty(t *testing.T) {
|
||||
groups := parseGroupListDNF5("")
|
||||
if len(groups) != 0 {
|
||||
t.Errorf("expected 0 groups from empty input, got %d", len(groups))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGroupIsInstalledDNF5Empty(t *testing.T) {
|
||||
if parseGroupIsInstalledDNF5("", "anything") {
|
||||
t.Error("expected false for empty input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVersionLockDNF5Empty(t *testing.T) {
|
||||
pkgs := parseVersionLockDNF5("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages from empty input, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVersionLockDNF5SingleEntry(t *testing.T) {
|
||||
input := `# Added by 'versionlock add' command on 2026-02-26 03:14:29
|
||||
Package name: nginx
|
||||
evr = 1.20.1-14.el9
|
||||
`
|
||||
pkgs := parseVersionLockDNF5(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "nginx" {
|
||||
t.Errorf("Name = %q, want nginx", pkgs[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRepoListDNF5Empty(t *testing.T) {
|
||||
repos := parseRepoListDNF5("")
|
||||
if len(repos) != 0 {
|
||||
t.Errorf("expected 0 repos from empty input, got %d", len(repos))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGroupInfoDNF5Empty(t *testing.T) {
|
||||
pkgs := parseGroupInfoDNF5("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages from empty input, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGroupInfoDNF5SinglePackage(t *testing.T) {
|
||||
input := `Id : test-group
|
||||
Name : Test
|
||||
Mandatory packages : single-pkg
|
||||
`
|
||||
pkgs := parseGroupInfoDNF5(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "single-pkg" {
|
||||
t.Errorf("Name = %q, want single-pkg", pkgs[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,71 @@ func TestParseListEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListEdgeCases(t *testing.T) {
|
||||
t.Run("single entry", func(t *testing.T) {
|
||||
pkgs := parseList("Firefox\torg.mozilla.Firefox\t131.0\tflathub\n")
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "Firefox" {
|
||||
t.Errorf("expected Firefox, got %q", pkgs[0].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("two fields only", func(t *testing.T) {
|
||||
pkgs := parseList("Firefox\torg.mozilla.Firefox\n")
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "Firefox" {
|
||||
t.Errorf("expected Firefox, got %q", pkgs[0].Name)
|
||||
}
|
||||
if pkgs[0].Version != "" {
|
||||
t.Errorf("expected empty version, got %q", pkgs[0].Version)
|
||||
}
|
||||
if pkgs[0].Repository != "" {
|
||||
t.Errorf("expected empty repository, got %q", pkgs[0].Repository)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single field skipped", func(t *testing.T) {
|
||||
pkgs := parseList("Firefox\n")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages (needs >=2 fields), got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("extra fields", func(t *testing.T) {
|
||||
pkgs := parseList("Firefox\torg.mozilla.Firefox\t131.0\tflathub\textra\n")
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Repository != "flathub" {
|
||||
t.Errorf("expected flathub, got %q", pkgs[0].Repository)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("whitespace only lines", func(t *testing.T) {
|
||||
pkgs := parseList(" \n\t\n \n")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("three fields no repo", func(t *testing.T) {
|
||||
pkgs := parseList("GIMP\torg.gimp.GIMP\t2.10.38\n")
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Version != "2.10.38" {
|
||||
t.Errorf("expected version 2.10.38, got %q", pkgs[0].Version)
|
||||
}
|
||||
if pkgs[0].Repository != "" {
|
||||
t.Errorf("expected empty repository, got %q", pkgs[0].Repository)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseSearch(t *testing.T) {
|
||||
input := "Firefox\torg.mozilla.Firefox\t131.0\tflathub\n" +
|
||||
"Firefox ESR\torg.mozilla.FirefoxESR\t128.3\tflathub\n"
|
||||
@@ -60,6 +125,32 @@ func TestParseSearchNoMatches(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSearchEdgeCases(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
pkgs := parseSearch("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single field line skipped", func(t *testing.T) {
|
||||
pkgs := parseSearch("JustAName\n")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages (needs >=2 fields), got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not installed result", func(t *testing.T) {
|
||||
pkgs := parseSearch("VLC\torg.videolan.VLC\t3.0.20\tflathub\n")
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Installed {
|
||||
t.Error("search results should not be marked installed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseInfo(t *testing.T) {
|
||||
input := `Name: Firefox
|
||||
Description: Fast, private web browser
|
||||
@@ -92,6 +183,46 @@ func TestParseInfoEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoEdgeCases(t *testing.T) {
|
||||
t.Run("name only", func(t *testing.T) {
|
||||
pkg := parseInfo("Name: VLC\n")
|
||||
if pkg == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if pkg.Name != "VLC" {
|
||||
t.Errorf("expected VLC, got %q", pkg.Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no name returns nil", func(t *testing.T) {
|
||||
pkg := parseInfo("Version: 1.0\nArch: x86_64\n")
|
||||
if pkg != nil {
|
||||
t.Error("expected nil when no Name field")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no colon lines ignored", func(t *testing.T) {
|
||||
pkg := parseInfo("Name: Test\nsome random line without colon\nVersion: 2.0\n")
|
||||
if pkg == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if pkg.Version != "2.0" {
|
||||
t.Errorf("expected version 2.0, got %q", pkg.Version)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("value with colons", func(t *testing.T) {
|
||||
pkg := parseInfo("Name: MyApp\nDescription: A tool: does things: well\n")
|
||||
if pkg == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
// parseInfo uses strings.Index for first colon
|
||||
if pkg.Description != "A tool: does things: well" {
|
||||
t.Errorf("unexpected description: %q", pkg.Description)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseRemotes(t *testing.T) {
|
||||
input := "flathub\thttps://dl.flathub.org/repo/\t\n" +
|
||||
"gnome-nightly\thttps://nightly.gnome.org/repo/\tdisabled\n"
|
||||
@@ -113,10 +244,162 @@ func TestParseRemotes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRemotesEdgeCases(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
repos := parseRemotes("")
|
||||
if len(repos) != 0 {
|
||||
t.Errorf("expected 0 repos, got %d", len(repos))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single enabled remote", func(t *testing.T) {
|
||||
repos := parseRemotes("flathub\thttps://dl.flathub.org/repo/\t\n")
|
||||
if len(repos) != 1 {
|
||||
t.Fatalf("expected 1 repo, got %d", len(repos))
|
||||
}
|
||||
if !repos[0].Enabled {
|
||||
t.Error("expected enabled")
|
||||
}
|
||||
if repos[0].Name != "flathub" {
|
||||
t.Errorf("expected Name=flathub, got %q", repos[0].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single disabled remote", func(t *testing.T) {
|
||||
repos := parseRemotes("test-remote\thttps://example.com/\tdisabled\n")
|
||||
if len(repos) != 1 {
|
||||
t.Fatalf("expected 1 repo, got %d", len(repos))
|
||||
}
|
||||
if repos[0].Enabled {
|
||||
t.Error("expected disabled")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no URL field", func(t *testing.T) {
|
||||
repos := parseRemotes("myremote\n")
|
||||
if len(repos) != 1 {
|
||||
t.Fatalf("expected 1 repo, got %d", len(repos))
|
||||
}
|
||||
if repos[0].ID != "myremote" {
|
||||
t.Errorf("expected myremote, got %q", repos[0].ID)
|
||||
}
|
||||
if repos[0].URL != "" {
|
||||
t.Errorf("expected empty URL, got %q", repos[0].URL)
|
||||
}
|
||||
if !repos[0].Enabled {
|
||||
t.Error("expected enabled by default")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("whitespace lines ignored", func(t *testing.T) {
|
||||
repos := parseRemotes(" \n\n \n")
|
||||
if len(repos) != 0 {
|
||||
t.Errorf("expected 0 repos, got %d", len(repos))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSemverCmp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b string
|
||||
want int
|
||||
}{
|
||||
{"equal", "1.0.0", "1.0.0", 0},
|
||||
{"less major", "1.0.0", "2.0.0", -1},
|
||||
{"greater major", "2.0.0", "1.0.0", 1},
|
||||
{"less minor", "1.2.3", "1.3.0", -1},
|
||||
{"less patch", "1.2.3", "1.2.4", -1},
|
||||
{"multi-digit", "1.10.0", "1.9.0", 1},
|
||||
{"short vs long equal", "1.0", "1.0.0", 0},
|
||||
{"short vs long less", "1.0", "1.0.1", -1},
|
||||
{"short vs long greater", "1.1", "1.0.9", 1},
|
||||
{"single component", "5", "3", 1},
|
||||
{"single equal", "3", "3", 0},
|
||||
{"empty vs empty", "", "", 0},
|
||||
{"empty vs version", "", "1.0", -1},
|
||||
{"version vs empty", "1.0", "", 1},
|
||||
{"non-numeric suffix", "1.0.0-beta", "1.0.0-rc1", 0},
|
||||
{"pre-release stripped", "1.0.0beta2", "1.0.0rc1", 0},
|
||||
{"four components", "1.2.3.4", "1.2.3.5", -1},
|
||||
{"different lengths", "1.0.0.0", "1.0.0", 0},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := semverCmp(tt.a, tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("semverCmp(%q, %q) = %d, want %d", tt.a, tt.b, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripNonNumeric(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"123", "123"},
|
||||
{"123abc", "123"},
|
||||
{"abc", ""},
|
||||
{"0beta", "0"},
|
||||
{"", ""},
|
||||
{"42-rc1", "42"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got := stripNonNumeric(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("stripNonNumeric(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceCompliance(t *testing.T) {
|
||||
var _ snack.Manager = (*Flatpak)(nil)
|
||||
var _ snack.Cleaner = (*Flatpak)(nil)
|
||||
var _ snack.RepoManager = (*Flatpak)(nil)
|
||||
var _ snack.VersionQuerier = (*Flatpak)(nil)
|
||||
var _ snack.PackageUpgrader = (*Flatpak)(nil)
|
||||
}
|
||||
|
||||
// Compile-time interface checks in test file
|
||||
var (
|
||||
_ snack.VersionQuerier = (*Flatpak)(nil)
|
||||
_ snack.PackageUpgrader = (*Flatpak)(nil)
|
||||
)
|
||||
|
||||
func TestCapabilities(t *testing.T) {
|
||||
caps := snack.GetCapabilities(New())
|
||||
if !caps.Clean {
|
||||
t.Error("expected Clean=true")
|
||||
}
|
||||
if !caps.RepoManagement {
|
||||
t.Error("expected RepoManagement=true")
|
||||
}
|
||||
if !caps.VersionQuery {
|
||||
t.Error("expected VersionQuery=true")
|
||||
}
|
||||
// Should be false
|
||||
if caps.FileOwnership {
|
||||
t.Error("expected FileOwnership=false")
|
||||
}
|
||||
if caps.DryRun {
|
||||
t.Error("expected DryRun=false")
|
||||
}
|
||||
if caps.Hold {
|
||||
t.Error("expected Hold=false")
|
||||
}
|
||||
if caps.KeyManagement {
|
||||
t.Error("expected KeyManagement=false")
|
||||
}
|
||||
if caps.Groups {
|
||||
t.Error("expected Groups=false")
|
||||
}
|
||||
if caps.NameNormalize {
|
||||
t.Error("expected NameNormalize=false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
|
||||
@@ -79,36 +79,88 @@ func TestBuildArgs_RootBeforeBaseArgs(t *testing.T) {
|
||||
assert.Greater(t, sIdx, rIdx, "root flag should come before base args")
|
||||
}
|
||||
|
||||
func TestParseUpgrades_Empty(t *testing.T) {
|
||||
assert.Empty(t, parseUpgrades(""))
|
||||
}
|
||||
func TestParseUpgrades(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantLen int
|
||||
wantNames []string
|
||||
wantVers []string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
input: "",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
input: " \n \n\n\t\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "standard arrow format",
|
||||
input: "linux 6.7.3.arch1-1 -> 6.7.4.arch1-1\nvim 9.0.2-1 -> 9.1.0-1\n",
|
||||
wantLen: 2,
|
||||
wantNames: []string{"linux", "vim"},
|
||||
wantVers: []string{"6.7.4.arch1-1", "9.1.0-1"},
|
||||
},
|
||||
{
|
||||
name: "single package arrow format",
|
||||
input: "curl 8.6.0-1 -> 8.7.1-1\n",
|
||||
wantLen: 1,
|
||||
wantNames: []string{"curl"},
|
||||
wantVers: []string{"8.7.1-1"},
|
||||
},
|
||||
{
|
||||
name: "fallback two-field format",
|
||||
input: "pkg 2.0\n",
|
||||
wantLen: 1,
|
||||
wantNames: []string{"pkg"},
|
||||
wantVers: []string{"2.0"},
|
||||
},
|
||||
{
|
||||
name: "mixed arrow and fallback",
|
||||
input: "linux 6.7.3 -> 6.7.4\npkg 2.0\n",
|
||||
wantLen: 2,
|
||||
wantNames: []string{"linux", "pkg"},
|
||||
wantVers: []string{"6.7.4", "2.0"},
|
||||
},
|
||||
{
|
||||
name: "whitespace around entries",
|
||||
input: "\n \nlinux 6.7.3 -> 6.7.4\n\n",
|
||||
wantLen: 1,
|
||||
wantNames: []string{"linux"},
|
||||
wantVers: []string{"6.7.4"},
|
||||
},
|
||||
{
|
||||
name: "single field line skipped",
|
||||
input: "orphan\nvalid 1.0 -> 2.0\n",
|
||||
wantLen: 1,
|
||||
},
|
||||
{
|
||||
name: "epoch in version",
|
||||
input: "java-runtime 1:21.0.2-1 -> 1:21.0.3-1\n",
|
||||
wantLen: 1,
|
||||
wantNames: []string{"java-runtime"},
|
||||
wantVers: []string{"1:21.0.3-1"},
|
||||
},
|
||||
}
|
||||
|
||||
func TestParseUpgrades_Standard(t *testing.T) {
|
||||
input := `linux 6.7.3.arch1-1 -> 6.7.4.arch1-1
|
||||
vim 9.0.2-1 -> 9.1.0-1
|
||||
`
|
||||
pkgs := parseUpgrades(input)
|
||||
require.Len(t, pkgs, 2)
|
||||
assert.Equal(t, "linux", pkgs[0].Name)
|
||||
assert.Equal(t, "6.7.4.arch1-1", pkgs[0].Version)
|
||||
assert.True(t, pkgs[0].Installed)
|
||||
assert.Equal(t, "vim", pkgs[1].Name)
|
||||
assert.Equal(t, "9.1.0-1", pkgs[1].Version)
|
||||
}
|
||||
|
||||
func TestParseUpgrades_FallbackFormat(t *testing.T) {
|
||||
// Some versions of pacman might output "pkg newver" without the arrow
|
||||
input := "pkg 2.0\n"
|
||||
pkgs := parseUpgrades(input)
|
||||
require.Len(t, pkgs, 1)
|
||||
assert.Equal(t, "pkg", pkgs[0].Name)
|
||||
assert.Equal(t, "2.0", pkgs[0].Version)
|
||||
}
|
||||
|
||||
func TestParseUpgrades_WhitespaceLines(t *testing.T) {
|
||||
input := "\n \nlinux 6.7.3 -> 6.7.4\n\n"
|
||||
pkgs := parseUpgrades(input)
|
||||
require.Len(t, pkgs, 1)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgs := parseUpgrades(tt.input)
|
||||
require.Len(t, pkgs, tt.wantLen)
|
||||
for i, p := range pkgs {
|
||||
assert.True(t, p.Installed, "all upgrade entries should have Installed=true")
|
||||
if i < len(tt.wantNames) {
|
||||
assert.Equal(t, tt.wantNames[i], p.Name)
|
||||
}
|
||||
if i < len(tt.wantVers) {
|
||||
assert.Equal(t, tt.wantVers[i], p.Version)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGroupPkgSet_Empty(t *testing.T) {
|
||||
@@ -149,6 +201,31 @@ group pkg2
|
||||
assert.Len(t, set, 2)
|
||||
}
|
||||
|
||||
func TestParseGroupPkgSet_WhitespaceOnly(t *testing.T) {
|
||||
set := parseGroupPkgSet(" \n \n\t\n")
|
||||
assert.Empty(t, set)
|
||||
}
|
||||
|
||||
func TestParseGroupPkgSet_MultipleGroups(t *testing.T) {
|
||||
// Different group names, same package names — set uses pkg name (second field)
|
||||
input := `base-devel gcc
|
||||
xorg xorg-server
|
||||
base-devel gcc
|
||||
`
|
||||
set := parseGroupPkgSet(input)
|
||||
assert.Len(t, set, 2)
|
||||
assert.Contains(t, set, "gcc")
|
||||
assert.Contains(t, set, "xorg-server")
|
||||
}
|
||||
|
||||
func TestParseGroupPkgSet_ExtraFields(t *testing.T) {
|
||||
// Lines with more than 2 fields — should still use second field
|
||||
input := "group pkg extra stuff\n"
|
||||
set := parseGroupPkgSet(input)
|
||||
assert.Len(t, set, 1)
|
||||
assert.Contains(t, set, "pkg")
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
p := New()
|
||||
assert.NotNil(t, p)
|
||||
|
||||
@@ -130,6 +130,58 @@ func TestBuildArgs(t *testing.T) {
|
||||
|
||||
func TestInterfaceCompliance(t *testing.T) {
|
||||
var _ snack.Manager = (*Pacman)(nil)
|
||||
var _ snack.VersionQuerier = (*Pacman)(nil)
|
||||
var _ snack.Cleaner = (*Pacman)(nil)
|
||||
var _ snack.FileOwner = (*Pacman)(nil)
|
||||
var _ snack.Grouper = (*Pacman)(nil)
|
||||
var _ snack.DryRunner = (*Pacman)(nil)
|
||||
var _ snack.PackageUpgrader = (*Pacman)(nil)
|
||||
}
|
||||
|
||||
func TestInterfaceNonCompliance(t *testing.T) {
|
||||
p := New()
|
||||
var m snack.Manager = p
|
||||
|
||||
if _, ok := m.(snack.Holder); ok {
|
||||
t.Error("Pacman should not implement Holder")
|
||||
}
|
||||
if _, ok := m.(snack.RepoManager); ok {
|
||||
t.Error("Pacman should not implement RepoManager")
|
||||
}
|
||||
if _, ok := m.(snack.KeyManager); ok {
|
||||
t.Error("Pacman should not implement KeyManager")
|
||||
}
|
||||
if _, ok := m.(snack.NameNormalizer); ok {
|
||||
t.Error("Pacman should not implement NameNormalizer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilities(t *testing.T) {
|
||||
caps := snack.GetCapabilities(New())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
got bool
|
||||
want bool
|
||||
}{
|
||||
{"VersionQuery", caps.VersionQuery, true},
|
||||
{"Clean", caps.Clean, true},
|
||||
{"FileOwnership", caps.FileOwnership, true},
|
||||
{"Groups", caps.Groups, true},
|
||||
{"DryRun", caps.DryRun, true},
|
||||
{"Hold", caps.Hold, false},
|
||||
{"RepoManagement", caps.RepoManagement, false},
|
||||
{"KeyManagement", caps.KeyManagement, false},
|
||||
{"NameNormalize", caps.NameNormalize, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.got != tt.want {
|
||||
t.Errorf("%s = %v, want %v", tt.name, tt.got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
|
||||
527
pkg/pkg_test.go
527
pkg/pkg_test.go
@@ -23,6 +23,94 @@ func TestParseQuery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseQueryEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantLen int
|
||||
wantPkgs []snack.Package
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
input: " \n \n\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "single entry",
|
||||
input: "vim\t9.0\tVi IMproved\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "vim", Version: "9.0", Description: "Vi IMproved", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing description (two fields only)",
|
||||
input: "bash\t5.2\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "bash", Version: "5.2", Description: "", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single field only (no tabs, skipped)",
|
||||
input: "justname\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "description with tabs",
|
||||
input: "pkg\t1.0\tA\ttabbed\tdescription\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "pkg", Version: "1.0", Description: "A\ttabbed\tdescription", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "trailing and leading whitespace on lines",
|
||||
input: " nginx\t1.24.0\tWeb server \n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "nginx", Version: "1.24.0", Description: "Web server", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple entries with blank lines between",
|
||||
input: "a\t1.0\tAlpha\n\nb\t2.0\tBeta\n",
|
||||
wantLen: 2,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgs := parseQuery(tt.input)
|
||||
if len(pkgs) != tt.wantLen {
|
||||
t.Fatalf("expected %d packages, got %d", tt.wantLen, len(pkgs))
|
||||
}
|
||||
for i, want := range tt.wantPkgs {
|
||||
if i >= len(pkgs) {
|
||||
break
|
||||
}
|
||||
got := pkgs[i]
|
||||
if got.Name != want.Name {
|
||||
t.Errorf("[%d] Name = %q, want %q", i, got.Name, want.Name)
|
||||
}
|
||||
if got.Version != want.Version {
|
||||
t.Errorf("[%d] Version = %q, want %q", i, got.Version, want.Version)
|
||||
}
|
||||
if got.Description != want.Description {
|
||||
t.Errorf("[%d] Description = %q, want %q", i, got.Description, want.Description)
|
||||
}
|
||||
if got.Installed != want.Installed {
|
||||
t.Errorf("[%d] Installed = %v, want %v", i, got.Installed, want.Installed)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSearch(t *testing.T) {
|
||||
input := `nginx-1.24.0 Robust and small WWW server
|
||||
curl-8.5.0 Command line tool for transferring data
|
||||
@@ -39,6 +127,81 @@ curl-8.5.0 Command line tool for transferring data
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSearchEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantLen int
|
||||
wantPkgs []snack.Package
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "name with many hyphens",
|
||||
input: "py39-django-rest-framework-3.14.0 RESTful Web APIs for Django\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "py39-django-rest-framework", Version: "3.14.0", Description: "RESTful Web APIs for Django"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no comment (name-version only)",
|
||||
input: "zsh-5.9\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "zsh", Version: "5.9", Description: ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "very long output many packages",
|
||||
input: "a-1.0 desc1\nb-2.0 desc2\nc-3.0 desc3\nd-4.0 desc4\ne-5.0 desc5\n",
|
||||
wantLen: 5,
|
||||
},
|
||||
{
|
||||
name: "single character name",
|
||||
input: "R-4.3.2 Statistical Computing\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "R", Version: "4.3.2", Description: "Statistical Computing"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "version with complex suffix",
|
||||
input: "libressl-3.8.2_1 TLS library\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "libressl", Version: "3.8.2_1", Description: "TLS library"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgs := parseSearch(tt.input)
|
||||
if len(pkgs) != tt.wantLen {
|
||||
t.Fatalf("expected %d packages, got %d", tt.wantLen, len(pkgs))
|
||||
}
|
||||
for i, want := range tt.wantPkgs {
|
||||
if i >= len(pkgs) {
|
||||
break
|
||||
}
|
||||
got := pkgs[i]
|
||||
if got.Name != want.Name {
|
||||
t.Errorf("[%d] Name = %q, want %q", i, got.Name, want.Name)
|
||||
}
|
||||
if got.Version != want.Version {
|
||||
t.Errorf("[%d] Version = %q, want %q", i, got.Version, want.Version)
|
||||
}
|
||||
if got.Description != want.Description {
|
||||
t.Errorf("[%d] Description = %q, want %q", i, got.Description, want.Description)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfo(t *testing.T) {
|
||||
input := `Name : nginx
|
||||
Version : 1.24.0
|
||||
@@ -63,6 +226,77 @@ Arch : FreeBSD:14:amd64
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantNil bool
|
||||
want *snack.Package
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "no name field returns nil",
|
||||
input: "Version : 1.0\nComment : test\n",
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "name only (missing other fields)",
|
||||
input: "Name : bash\n",
|
||||
want: &snack.Package{Name: "bash", Installed: true},
|
||||
},
|
||||
{
|
||||
name: "extra unknown fields are ignored",
|
||||
input: "Name : vim\nVersion : 9.0\nMaintainer : someone@example.com\nWWW : https://vim.org\nComment : Vi IMproved\n",
|
||||
want: &snack.Package{Name: "vim", Version: "9.0", Description: "Vi IMproved", Installed: true},
|
||||
},
|
||||
{
|
||||
name: "colon in value",
|
||||
input: "Name : nginx\nComment : HTTP server: fast and reliable\n",
|
||||
want: &snack.Package{Name: "nginx", Description: "HTTP server: fast and reliable", Installed: true},
|
||||
},
|
||||
{
|
||||
name: "lines without colons are skipped",
|
||||
input: "This is random text\nNo colons here\n",
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "whitespace around values",
|
||||
input: "Name : curl \nVersion : 8.5.0 \n",
|
||||
want: &snack.Package{Name: "curl", Version: "8.5.0", Installed: true},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseInfo(tt.input)
|
||||
if tt.wantNil {
|
||||
if got != nil {
|
||||
t.Errorf("expected nil, got %+v", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatal("expected non-nil package")
|
||||
}
|
||||
if got.Name != tt.want.Name {
|
||||
t.Errorf("Name = %q, want %q", got.Name, tt.want.Name)
|
||||
}
|
||||
if got.Version != tt.want.Version {
|
||||
t.Errorf("Version = %q, want %q", got.Version, tt.want.Version)
|
||||
}
|
||||
if got.Description != tt.want.Description {
|
||||
t.Errorf("Description = %q, want %q", got.Description, tt.want.Description)
|
||||
}
|
||||
if got.Installed != tt.want.Installed {
|
||||
t.Errorf("Installed = %v, want %v", got.Installed, tt.want.Installed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUpgrades(t *testing.T) {
|
||||
input := `Updating FreeBSD repository catalogue...
|
||||
The following 2 package(s) will be affected:
|
||||
@@ -84,6 +318,90 @@ Number of packages to be upgraded: 2
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUpgradesEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantLen int
|
||||
wantPkgs []snack.Package
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "no upgrade lines",
|
||||
input: "Updating FreeBSD repository catalogue...\nAll packages are up to date.\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "mix of Upgrading Installing Reinstalling",
|
||||
input: `Upgrading nginx: 1.24.0 -> 1.26.0
|
||||
Installing newpkg: 0 -> 1.0.0
|
||||
Reinstalling bash: 5.2 -> 5.2
|
||||
`,
|
||||
wantLen: 3,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "nginx", Version: "1.26.0", Installed: true},
|
||||
{Name: "newpkg", Version: "1.0.0", Installed: true},
|
||||
{Name: "bash", Version: "5.2", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "line with -> but no recognized prefix is skipped",
|
||||
input: "Something: 1.0 -> 2.0\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "upgrading line without colon is skipped",
|
||||
input: "Upgrading nginx 1.24.0 -> 1.26.0\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "upgrading line with -> but not enough parts after colon",
|
||||
input: "Upgrading nginx: -> \n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "upgrading line with wrong arrow",
|
||||
input: "Upgrading nginx: 1.24.0 => 1.26.0\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "single upgrade line",
|
||||
input: "Upgrading zsh: 5.8 -> 5.9\n",
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "zsh", Version: "5.9", Installed: true},
|
||||
},
|
||||
wantLen: 1,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgs := parseUpgrades(tt.input)
|
||||
if len(pkgs) != tt.wantLen {
|
||||
t.Fatalf("expected %d packages, got %d", tt.wantLen, len(pkgs))
|
||||
}
|
||||
for i, want := range tt.wantPkgs {
|
||||
if i >= len(pkgs) {
|
||||
break
|
||||
}
|
||||
got := pkgs[i]
|
||||
if got.Name != want.Name {
|
||||
t.Errorf("[%d] Name = %q, want %q", i, got.Name, want.Name)
|
||||
}
|
||||
if got.Version != want.Version {
|
||||
t.Errorf("[%d] Version = %q, want %q", i, got.Version, want.Version)
|
||||
}
|
||||
if got.Installed != want.Installed {
|
||||
t.Errorf("[%d] Installed = %v, want %v", i, got.Installed, want.Installed)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFileList(t *testing.T) {
|
||||
input := `nginx-1.24.0:
|
||||
/usr/local/sbin/nginx
|
||||
@@ -99,6 +417,67 @@ func TestParseFileList(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFileListEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantLen int
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "header only no files",
|
||||
input: "nginx-1.24.0:\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "paths with spaces",
|
||||
input: "pkg-1.0:\n\t/usr/local/share/my package/file name.txt\n\t/usr/local/share/another dir/test\n",
|
||||
wantLen: 2,
|
||||
want: []string{
|
||||
"/usr/local/share/my package/file name.txt",
|
||||
"/usr/local/share/another dir/test",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single file",
|
||||
input: "bash-5.2:\n\t/usr/local/bin/bash\n",
|
||||
wantLen: 1,
|
||||
want: []string{"/usr/local/bin/bash"},
|
||||
},
|
||||
{
|
||||
name: "no header just file paths",
|
||||
input: "/usr/local/bin/curl\n/usr/local/lib/libcurl.so\n",
|
||||
wantLen: 2,
|
||||
},
|
||||
{
|
||||
name: "blank lines between files",
|
||||
input: "pkg-1.0:\n\t/usr/local/bin/a\n\n\t/usr/local/bin/b\n",
|
||||
wantLen: 2,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
files := parseFileList(tt.input)
|
||||
if len(files) != tt.wantLen {
|
||||
t.Fatalf("expected %d files, got %d", tt.wantLen, len(files))
|
||||
}
|
||||
for i, w := range tt.want {
|
||||
if i >= len(files) {
|
||||
break
|
||||
}
|
||||
if files[i] != w {
|
||||
t.Errorf("[%d] got %q, want %q", i, files[i], w)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOwner(t *testing.T) {
|
||||
input := "/usr/local/sbin/nginx was installed by package nginx-1.24.0\n"
|
||||
name := parseOwner(input)
|
||||
@@ -107,6 +486,48 @@ func TestParseOwner(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOwnerEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "standard format",
|
||||
input: "/usr/local/bin/curl was installed by package curl-8.5.0\n",
|
||||
want: "curl",
|
||||
},
|
||||
{
|
||||
name: "package name with hyphens",
|
||||
input: "/usr/local/lib/libpython3.so was installed by package py39-python-3.9.18\n",
|
||||
want: "py39-python",
|
||||
},
|
||||
{
|
||||
name: "no match returns trimmed input",
|
||||
input: "some random output\n",
|
||||
want: "some random output",
|
||||
},
|
||||
{
|
||||
name: "whitespace around",
|
||||
input: " /usr/local/bin/bash was installed by package bash-5.2.21 \n",
|
||||
want: "bash",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseOwner(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("parseOwner(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitNameVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
@@ -126,6 +547,73 @@ func TestSplitNameVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitNameVersionEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantName string
|
||||
wantVersion string
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
wantName: "",
|
||||
wantVersion: "",
|
||||
},
|
||||
{
|
||||
name: "no hyphen",
|
||||
input: "singleword",
|
||||
wantName: "singleword",
|
||||
wantVersion: "",
|
||||
},
|
||||
{
|
||||
name: "multiple hyphens",
|
||||
input: "py39-django-rest-3.14.0",
|
||||
wantName: "py39-django-rest",
|
||||
wantVersion: "3.14.0",
|
||||
},
|
||||
{
|
||||
name: "leading hyphen",
|
||||
input: "-1.0",
|
||||
wantName: "-1.0",
|
||||
wantVersion: "",
|
||||
},
|
||||
{
|
||||
name: "trailing hyphen",
|
||||
input: "nginx-",
|
||||
wantName: "nginx",
|
||||
wantVersion: "",
|
||||
},
|
||||
{
|
||||
name: "only hyphen",
|
||||
input: "-",
|
||||
wantName: "-",
|
||||
wantVersion: "",
|
||||
},
|
||||
{
|
||||
name: "hyphen at index 1",
|
||||
input: "a-1.0",
|
||||
wantName: "a",
|
||||
wantVersion: "1.0",
|
||||
},
|
||||
{
|
||||
name: "version with underscore suffix",
|
||||
input: "libressl-3.8.2_1",
|
||||
wantName: "libressl",
|
||||
wantVersion: "3.8.2_1",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
name, ver := splitNameVersion(tt.input)
|
||||
if name != tt.wantName || ver != tt.wantVersion {
|
||||
t.Errorf("splitNameVersion(%q) = (%q, %q), want (%q, %q)",
|
||||
tt.input, name, ver, tt.wantName, tt.wantVersion)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceCompliance(t *testing.T) {
|
||||
var _ snack.Manager = (*Pkg)(nil)
|
||||
var _ snack.VersionQuerier = (*Pkg)(nil)
|
||||
@@ -133,9 +621,48 @@ func TestInterfaceCompliance(t *testing.T) {
|
||||
var _ snack.FileOwner = (*Pkg)(nil)
|
||||
}
|
||||
|
||||
func TestPackageUpgraderInterface(t *testing.T) {
|
||||
var _ snack.PackageUpgrader = (*Pkg)(nil)
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
p := New()
|
||||
if p.Name() != "pkg" {
|
||||
t.Errorf("Name() = %q, want %q", p.Name(), "pkg")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilities(t *testing.T) {
|
||||
caps := snack.GetCapabilities(New())
|
||||
|
||||
// Should be true
|
||||
if !caps.VersionQuery {
|
||||
t.Error("expected VersionQuery=true")
|
||||
}
|
||||
if !caps.Clean {
|
||||
t.Error("expected Clean=true")
|
||||
}
|
||||
if !caps.FileOwnership {
|
||||
t.Error("expected FileOwnership=true")
|
||||
}
|
||||
|
||||
// Should be false
|
||||
if caps.Hold {
|
||||
t.Error("expected Hold=false")
|
||||
}
|
||||
if caps.RepoManagement {
|
||||
t.Error("expected RepoManagement=false")
|
||||
}
|
||||
if caps.KeyManagement {
|
||||
t.Error("expected KeyManagement=false")
|
||||
}
|
||||
if caps.Groups {
|
||||
t.Error("expected Groups=false")
|
||||
}
|
||||
if caps.NameNormalize {
|
||||
t.Error("expected NameNormalize=false")
|
||||
}
|
||||
if caps.DryRun {
|
||||
t.Error("expected DryRun=false")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,93 @@ python-3.11.7p0 interpreted object-oriented programming language
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantLen int
|
||||
wantPkgs []snack.Package
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
input: " \n \n\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "single entry",
|
||||
input: "vim-9.0.2100 Vi IMproved\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "vim", Version: "9.0.2100", Description: "Vi IMproved", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no description",
|
||||
input: "vim-9.0.2100\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "vim", Version: "9.0.2100", Description: "", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty lines between entries",
|
||||
input: "bash-5.2 Shell\n\n\ncurl-8.5.0 Transfer tool\n",
|
||||
wantLen: 2,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "bash", Version: "5.2", Description: "Shell", Installed: true},
|
||||
{Name: "curl", Version: "8.5.0", Description: "Transfer tool", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "package with p-suffix version (OpenBSD style)",
|
||||
input: "python-3.11.7p0 interpreted language\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "python", Version: "3.11.7p0", Description: "interpreted language", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "package name with multiple hyphens",
|
||||
input: "py3-django-rest-3.14.0 REST framework\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "py3-django-rest", Version: "3.14.0", Description: "REST framework", Installed: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgs := parseList(tt.input)
|
||||
if len(pkgs) != tt.wantLen {
|
||||
t.Fatalf("expected %d packages, got %d", tt.wantLen, len(pkgs))
|
||||
}
|
||||
for i, want := range tt.wantPkgs {
|
||||
if i >= len(pkgs) {
|
||||
break
|
||||
}
|
||||
got := pkgs[i]
|
||||
if got.Name != want.Name {
|
||||
t.Errorf("[%d] Name = %q, want %q", i, got.Name, want.Name)
|
||||
}
|
||||
if got.Version != want.Version {
|
||||
t.Errorf("[%d] Version = %q, want %q", i, got.Version, want.Version)
|
||||
}
|
||||
if got.Description != want.Description {
|
||||
t.Errorf("[%d] Description = %q, want %q", i, got.Description, want.Description)
|
||||
}
|
||||
if got.Installed != want.Installed {
|
||||
t.Errorf("[%d] Installed = %v, want %v", i, got.Installed, want.Installed)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSearchResults(t *testing.T) {
|
||||
input := `nginx-1.24.0
|
||||
nginx-1.25.3
|
||||
@@ -42,6 +129,83 @@ nginx-1.25.3
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSearchResultsEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantLen int
|
||||
wantPkgs []snack.Package
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
input: " \n\n \n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "single result",
|
||||
input: "curl-8.5.0\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "curl", Version: "8.5.0"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "name with many hyphens",
|
||||
input: "py3-django-rest-framework-3.14.0\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "py3-django-rest-framework", Version: "3.14.0"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no version (no hyphen)",
|
||||
input: "quirks\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "quirks", Version: ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "p-suffix version",
|
||||
input: "python-3.11.7p0\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "python", Version: "3.11.7p0"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple results with blank lines",
|
||||
input: "a-1.0\n\nb-2.0\n\nc-3.0\n",
|
||||
wantLen: 3,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgs := parseSearchResults(tt.input)
|
||||
if len(pkgs) != tt.wantLen {
|
||||
t.Fatalf("expected %d packages, got %d", tt.wantLen, len(pkgs))
|
||||
}
|
||||
for i, want := range tt.wantPkgs {
|
||||
if i >= len(pkgs) {
|
||||
break
|
||||
}
|
||||
got := pkgs[i]
|
||||
if got.Name != want.Name {
|
||||
t.Errorf("[%d] Name = %q, want %q", i, got.Name, want.Name)
|
||||
}
|
||||
if got.Version != want.Version {
|
||||
t.Errorf("[%d] Version = %q, want %q", i, got.Version, want.Version)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoOutput(t *testing.T) {
|
||||
input := `Information for nginx-1.24.0:
|
||||
|
||||
@@ -82,6 +246,131 @@ curl is a tool to transfer data from or to a server.
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoOutputEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
pkgArg string
|
||||
wantNil bool
|
||||
want *snack.Package
|
||||
}{
|
||||
{
|
||||
name: "empty input and empty pkg arg",
|
||||
input: "",
|
||||
pkgArg: "",
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "empty input with pkg arg fallback",
|
||||
input: "",
|
||||
pkgArg: "curl-8.5.0",
|
||||
want: &snack.Package{Name: "curl", Version: "8.5.0", Installed: true},
|
||||
},
|
||||
{
|
||||
name: "no header, falls back to pkg arg",
|
||||
input: "Some random output\nwithout the expected header",
|
||||
pkgArg: "vim-9.0",
|
||||
want: &snack.Package{Name: "vim", Version: "9.0", Installed: true},
|
||||
},
|
||||
{
|
||||
name: "comment on same line as Comment: label",
|
||||
input: `Information for zsh-5.9:
|
||||
|
||||
Comment: Zsh shell
|
||||
`,
|
||||
pkgArg: "zsh-5.9",
|
||||
want: &snack.Package{Name: "zsh", Version: "5.9", Description: "Zsh shell", Installed: true},
|
||||
},
|
||||
{
|
||||
name: "comment on next line after Comment: label (not captured as description)",
|
||||
input: `Information for bash-5.2:
|
||||
|
||||
Comment:
|
||||
GNU Bourne Again Shell
|
||||
`,
|
||||
pkgArg: "bash-5.2",
|
||||
// Comment: with nothing after the colon sets Description="",
|
||||
// and the next line isn't in a Description: block so it's ignored.
|
||||
want: &snack.Package{Name: "bash", Version: "5.2", Description: "", Installed: true},
|
||||
},
|
||||
{
|
||||
name: "description spans multiple lines",
|
||||
input: `Information for git-2.43.0:
|
||||
|
||||
Comment: distributed version control system
|
||||
Description:
|
||||
Git is a fast, scalable, distributed revision control system
|
||||
with an unusually rich command set.
|
||||
`,
|
||||
pkgArg: "git-2.43.0",
|
||||
want: &snack.Package{Name: "git", Version: "2.43.0", Description: "distributed version control system", Installed: true},
|
||||
},
|
||||
{
|
||||
name: "extra fields are ignored",
|
||||
input: `Information for tmux-3.3a:
|
||||
|
||||
Comment: terminal multiplexer
|
||||
Maintainer: someone@openbsd.org
|
||||
WWW: https://tmux.github.io
|
||||
Description:
|
||||
tmux is a terminal multiplexer.
|
||||
`,
|
||||
pkgArg: "tmux-3.3a",
|
||||
want: &snack.Package{Name: "tmux", Version: "3.3a", Description: "terminal multiplexer", Installed: true},
|
||||
},
|
||||
{
|
||||
name: "pkg arg with no version (no hyphen)",
|
||||
input: "",
|
||||
pkgArg: "quirks",
|
||||
want: &snack.Package{Name: "quirks", Version: "", Installed: true},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseInfoOutput(tt.input, tt.pkgArg)
|
||||
if tt.wantNil {
|
||||
if got != nil {
|
||||
t.Errorf("expected nil, got %+v", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatal("expected non-nil package")
|
||||
}
|
||||
if got.Name != tt.want.Name {
|
||||
t.Errorf("Name = %q, want %q", got.Name, tt.want.Name)
|
||||
}
|
||||
if got.Version != tt.want.Version {
|
||||
t.Errorf("Version = %q, want %q", got.Version, tt.want.Version)
|
||||
}
|
||||
if tt.want.Description != "" && got.Description != tt.want.Description {
|
||||
t.Errorf("Description = %q, want %q", got.Description, tt.want.Description)
|
||||
}
|
||||
if got.Installed != tt.want.Installed {
|
||||
t.Errorf("Installed = %v, want %v", got.Installed, tt.want.Installed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoOutputEmpty(t *testing.T) {
|
||||
pkg := parseInfoOutput("", "")
|
||||
if pkg != nil {
|
||||
t.Error("expected nil for empty input and empty pkg name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoOutputFallbackToPkgArg(t *testing.T) {
|
||||
input := "Some random output\nwithout the expected header"
|
||||
pkg := parseInfoOutput(input, "curl-8.5.0")
|
||||
if pkg == nil {
|
||||
t.Fatal("expected non-nil package from fallback")
|
||||
}
|
||||
if pkg.Name != "curl" || pkg.Version != "8.5.0" {
|
||||
t.Errorf("unexpected fallback parse: %+v", pkg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitNameVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
@@ -101,57 +390,76 @@ func TestSplitNameVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListEmpty(t *testing.T) {
|
||||
pkgs := parseList("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Fatalf("expected 0 packages, got %d", len(pkgs))
|
||||
func TestSplitNameVersionEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantName string
|
||||
wantVersion string
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
wantName: "",
|
||||
wantVersion: "",
|
||||
},
|
||||
{
|
||||
name: "no hyphen",
|
||||
input: "singleword",
|
||||
wantName: "singleword",
|
||||
wantVersion: "",
|
||||
},
|
||||
{
|
||||
name: "multiple hyphens",
|
||||
input: "py3-django-rest-3.14.0",
|
||||
wantName: "py3-django-rest",
|
||||
wantVersion: "3.14.0",
|
||||
},
|
||||
{
|
||||
name: "leading hyphen (idx=0, returns whole string)",
|
||||
input: "-1.0",
|
||||
wantName: "-1.0",
|
||||
wantVersion: "",
|
||||
},
|
||||
{
|
||||
name: "trailing hyphen",
|
||||
input: "nginx-",
|
||||
wantName: "nginx",
|
||||
wantVersion: "",
|
||||
},
|
||||
{
|
||||
name: "only hyphen",
|
||||
input: "-",
|
||||
wantName: "-",
|
||||
wantVersion: "",
|
||||
},
|
||||
{
|
||||
name: "hyphen at index 1",
|
||||
input: "a-1.0",
|
||||
wantName: "a",
|
||||
wantVersion: "1.0",
|
||||
},
|
||||
{
|
||||
name: "p-suffix version",
|
||||
input: "python-3.11.7p0",
|
||||
wantName: "python",
|
||||
wantVersion: "3.11.7p0",
|
||||
},
|
||||
{
|
||||
name: "version with v prefix",
|
||||
input: "go-v1.21.5",
|
||||
wantName: "go",
|
||||
wantVersion: "v1.21.5",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListWhitespaceOnly(t *testing.T) {
|
||||
pkgs := parseList(" \n \n\n")
|
||||
if len(pkgs) != 0 {
|
||||
t.Fatalf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListNoDescription(t *testing.T) {
|
||||
input := "vim-9.0.2100\n"
|
||||
pkgs := parseList(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "vim" || pkgs[0].Version != "9.0.2100" {
|
||||
t.Errorf("unexpected package: %+v", pkgs[0])
|
||||
}
|
||||
if pkgs[0].Description != "" {
|
||||
t.Errorf("expected empty description, got %q", pkgs[0].Description)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSearchResultsEmpty(t *testing.T) {
|
||||
pkgs := parseSearchResults("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Fatalf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoOutputEmpty(t *testing.T) {
|
||||
pkg := parseInfoOutput("", "")
|
||||
if pkg != nil {
|
||||
t.Error("expected nil for empty input and empty pkg name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoOutputFallbackToPkgArg(t *testing.T) {
|
||||
// No "Information for" header — should fall back to parsing the pkg argument.
|
||||
input := "Some random output\nwithout the expected header"
|
||||
pkg := parseInfoOutput(input, "curl-8.5.0")
|
||||
if pkg == nil {
|
||||
t.Fatal("expected non-nil package from fallback")
|
||||
}
|
||||
if pkg.Name != "curl" || pkg.Version != "8.5.0" {
|
||||
t.Errorf("unexpected fallback parse: %+v", pkg)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
name, ver := splitNameVersion(tt.input)
|
||||
if name != tt.wantName || ver != tt.wantVersion {
|
||||
t.Errorf("splitNameVersion(%q) = (%q, %q), want (%q, %q)",
|
||||
tt.input, name, ver, tt.wantName, tt.wantVersion)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,14 +471,7 @@ func TestSplitNameVersionNoHyphen(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSplitNameVersionLeadingHyphen(t *testing.T) {
|
||||
// A hyphen at position 0 should return the whole string as name.
|
||||
name, ver := splitNameVersion("-1.0")
|
||||
if name != "" || ver != "1.0" {
|
||||
// LastIndex("-1.0", "-") is 0, and idx <= 0 returns (s, "")
|
||||
// Actually idx=0 means the condition idx <= 0 is true
|
||||
}
|
||||
// Re-check: idx=0, condition is idx <= 0, so returns (s, "")
|
||||
name, ver = splitNameVersion("-1.0")
|
||||
if name != "-1.0" || ver != "" {
|
||||
t.Errorf("splitNameVersion(\"-1.0\") = (%q, %q), want (\"-1.0\", \"\")", name, ver)
|
||||
}
|
||||
@@ -196,6 +497,91 @@ python-3.11.7p0 -> python-3.11.8p0
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUpgradeOutputEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantLen int
|
||||
wantPkgs []snack.Package
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
input: " \n\n \n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "single upgrade",
|
||||
input: "bash-5.2 -> bash-5.3\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "bash", Version: "5.3", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "line without -> is skipped",
|
||||
input: "Some info line\nbash-5.2 -> bash-5.3\nAnother line\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "bash", Version: "5.3", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "malformed line (-> but not enough fields)",
|
||||
input: "-> bash-5.3\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "wrong arrow (=> instead of ->)",
|
||||
input: "bash-5.2 => bash-5.3\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "package name with multiple hyphens",
|
||||
input: "py3-django-rest-3.14.0 -> py3-django-rest-3.15.0\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "py3-django-rest", Version: "3.15.0", Installed: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "p-suffix versions",
|
||||
input: "python-3.11.7p0 -> python-3.11.8p1\n",
|
||||
wantLen: 1,
|
||||
wantPkgs: []snack.Package{
|
||||
{Name: "python", Version: "3.11.8p1", Installed: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgs := parseUpgradeOutput(tt.input)
|
||||
if len(pkgs) != tt.wantLen {
|
||||
t.Fatalf("expected %d packages, got %d", tt.wantLen, len(pkgs))
|
||||
}
|
||||
for i, want := range tt.wantPkgs {
|
||||
if i >= len(pkgs) {
|
||||
break
|
||||
}
|
||||
got := pkgs[i]
|
||||
if got.Name != want.Name {
|
||||
t.Errorf("[%d] Name = %q, want %q", i, got.Name, want.Name)
|
||||
}
|
||||
if got.Version != want.Version {
|
||||
t.Errorf("[%d] Version = %q, want %q", i, got.Version, want.Version)
|
||||
}
|
||||
if got.Installed != want.Installed {
|
||||
t.Errorf("[%d] Installed = %v, want %v", i, got.Installed, want.Installed)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUpgradeOutputEmpty(t *testing.T) {
|
||||
pkgs := parseUpgradeOutput("")
|
||||
if len(pkgs) != 0 {
|
||||
@@ -221,6 +607,73 @@ Files:
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFileListOutputEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantLen int
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "header only no files",
|
||||
input: "Information for pkg-1.0:\n\nFiles:\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "paths with spaces",
|
||||
input: "Information for pkg-1.0:\n\nFiles:\n/usr/local/share/my dir/file name.txt\n/usr/local/share/another path/test\n",
|
||||
wantLen: 2,
|
||||
want: []string{
|
||||
"/usr/local/share/my dir/file name.txt",
|
||||
"/usr/local/share/another path/test",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single file",
|
||||
input: "Files:\n/usr/local/bin/bash\n",
|
||||
wantLen: 1,
|
||||
want: []string{"/usr/local/bin/bash"},
|
||||
},
|
||||
{
|
||||
name: "no header just paths",
|
||||
input: "/usr/local/bin/a\n/usr/local/bin/b\n",
|
||||
wantLen: 2,
|
||||
},
|
||||
{
|
||||
name: "blank lines between files",
|
||||
input: "Files:\n/usr/local/bin/a\n\n/usr/local/bin/b\n",
|
||||
wantLen: 2,
|
||||
},
|
||||
{
|
||||
name: "non-path lines are skipped",
|
||||
input: "Information for pkg-1.0:\n\nFiles:\nNot a path\n/usr/local/bin/real\n",
|
||||
wantLen: 1,
|
||||
want: []string{"/usr/local/bin/real"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
files := parseFileListOutput(tt.input)
|
||||
if len(files) != tt.wantLen {
|
||||
t.Fatalf("expected %d files, got %d", tt.wantLen, len(files))
|
||||
}
|
||||
for i, w := range tt.want {
|
||||
if i >= len(files) {
|
||||
break
|
||||
}
|
||||
if files[i] != w {
|
||||
t.Errorf("[%d] got %q, want %q", i, files[i], w)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFileListOutputEmpty(t *testing.T) {
|
||||
files := parseFileListOutput("")
|
||||
if len(files) != 0 {
|
||||
@@ -245,6 +698,58 @@ func TestParseOwnerOutput(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOwnerOutputEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
input: " \n ",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "name with many hyphens",
|
||||
input: "py3-django-rest-framework-3.14.0",
|
||||
want: "py3-django-rest-framework",
|
||||
},
|
||||
{
|
||||
name: "no version (no hyphen)",
|
||||
input: "quirks",
|
||||
want: "quirks",
|
||||
},
|
||||
{
|
||||
name: "leading/trailing whitespace",
|
||||
input: " curl-8.5.0 ",
|
||||
want: "curl",
|
||||
},
|
||||
{
|
||||
name: "p-suffix version",
|
||||
input: "python-3.11.7p0",
|
||||
want: "python",
|
||||
},
|
||||
{
|
||||
name: "trailing newline",
|
||||
input: "bash-5.2\n",
|
||||
want: "bash",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseOwnerOutput(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("parseOwnerOutput(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceCompliance(t *testing.T) {
|
||||
var _ snack.Manager = (*Ports)(nil)
|
||||
var _ snack.VersionQuerier = (*Ports)(nil)
|
||||
@@ -253,9 +758,48 @@ func TestInterfaceCompliance(t *testing.T) {
|
||||
var _ snack.PackageUpgrader = (*Ports)(nil)
|
||||
}
|
||||
|
||||
func TestPackageUpgraderInterface(t *testing.T) {
|
||||
var _ snack.PackageUpgrader = (*Ports)(nil)
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
p := New()
|
||||
if p.Name() != "ports" {
|
||||
t.Errorf("Name() = %q, want %q", p.Name(), "ports")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilities(t *testing.T) {
|
||||
caps := snack.GetCapabilities(New())
|
||||
|
||||
// Should be true
|
||||
if !caps.VersionQuery {
|
||||
t.Error("expected VersionQuery=true")
|
||||
}
|
||||
if !caps.Clean {
|
||||
t.Error("expected Clean=true")
|
||||
}
|
||||
if !caps.FileOwnership {
|
||||
t.Error("expected FileOwnership=true")
|
||||
}
|
||||
|
||||
// Should be false
|
||||
if caps.Hold {
|
||||
t.Error("expected Hold=false")
|
||||
}
|
||||
if caps.RepoManagement {
|
||||
t.Error("expected RepoManagement=false")
|
||||
}
|
||||
if caps.KeyManagement {
|
||||
t.Error("expected KeyManagement=false")
|
||||
}
|
||||
if caps.Groups {
|
||||
t.Error("expected Groups=false")
|
||||
}
|
||||
if caps.NameNormalize {
|
||||
t.Error("expected NameNormalize=false")
|
||||
}
|
||||
if caps.DryRun {
|
||||
t.Error("expected DryRun=false")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package rpm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogrlx/snack"
|
||||
)
|
||||
|
||||
func TestParseList(t *testing.T) {
|
||||
@@ -95,9 +93,128 @@ func TestParseArchSuffix(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Compile-time interface checks.
|
||||
var (
|
||||
_ snack.Manager = (*RPM)(nil)
|
||||
_ snack.FileOwner = (*RPM)(nil)
|
||||
_ snack.NameNormalizer = (*RPM)(nil)
|
||||
)
|
||||
// --- Edge case tests ---
|
||||
|
||||
func TestParseListEmpty(t *testing.T) {
|
||||
pkgs := parseList("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages from empty input, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListSinglePackage(t *testing.T) {
|
||||
input := "curl\t7.76.1-23.el9\tA utility\n"
|
||||
pkgs := parseList(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "curl" {
|
||||
t.Errorf("Name = %q, want curl", pkgs[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListNoDescription(t *testing.T) {
|
||||
input := "bash\t5.1.8-6.el9\n"
|
||||
pkgs := parseList(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Description != "" {
|
||||
t.Errorf("Description = %q, want empty", pkgs[0].Description)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListMalformedLines(t *testing.T) {
|
||||
input := "bash\t5.1.8-6.el9\tShell\nno-tab-here\ncurl\t7.76.1\tHTTP tool\n"
|
||||
pkgs := parseList(input)
|
||||
if len(pkgs) != 2 {
|
||||
t.Fatalf("expected 2 packages (skip malformed), got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoEmpty(t *testing.T) {
|
||||
p := parseInfo("")
|
||||
if p != nil {
|
||||
t.Errorf("expected nil from empty input, got %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoNoName(t *testing.T) {
|
||||
input := `Version : 1.0
|
||||
Architecture: x86_64
|
||||
`
|
||||
p := parseInfo(input)
|
||||
if p != nil {
|
||||
t.Errorf("expected nil when no Name field, got %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInfoArchField(t *testing.T) {
|
||||
// Test both "Architecture" and "Arch" key forms
|
||||
input := `Name : test
|
||||
Version : 1.0
|
||||
Release : 1.el9
|
||||
Arch : aarch64
|
||||
Summary : Test package
|
||||
`
|
||||
p := parseInfo(input)
|
||||
if p == nil {
|
||||
t.Fatal("expected non-nil package")
|
||||
}
|
||||
if p.Arch != "aarch64" {
|
||||
t.Errorf("Arch = %q, want aarch64", p.Arch)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeNameEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
input, want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"pkg.unknown.ext", "pkg.unknown.ext"},
|
||||
{"name.with.dots.x86_64", "name.with.dots"},
|
||||
{"python3.11", "python3.11"},
|
||||
{"glibc.s390x", "glibc"},
|
||||
{"kernel.src", "kernel"},
|
||||
{".x86_64", ""},
|
||||
{"pkg.ppc64le", "pkg"},
|
||||
{"pkg.armv7hl", "pkg"},
|
||||
{"pkg.i386", "pkg"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got := normalizeName(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("normalizeName(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArchSuffixEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
input, wantName, wantArch string
|
||||
}{
|
||||
{"", "", ""},
|
||||
{"pkg.i386", "pkg", "i386"},
|
||||
{"pkg.ppc64le", "pkg", "ppc64le"},
|
||||
{"pkg.s390x", "pkg", "s390x"},
|
||||
{"pkg.armv7hl", "pkg", "armv7hl"},
|
||||
{"pkg.src", "pkg", "src"},
|
||||
{"pkg.aarch64", "pkg", "aarch64"},
|
||||
{"pkg.noarch", "pkg", "noarch"},
|
||||
{"pkg.unknown", "pkg.unknown", ""},
|
||||
{"name.with.many.dots.noarch", "name.with.many.dots", "noarch"},
|
||||
{".noarch", "", "noarch"},
|
||||
{"pkg.x86_64.extra", "pkg.x86_64.extra", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
name, arch := parseArchSuffix(tt.input)
|
||||
if name != tt.wantName || arch != tt.wantArch {
|
||||
t.Errorf("parseArchSuffix(%q) = (%q, %q), want (%q, %q)",
|
||||
tt.input, name, arch, tt.wantName, tt.wantArch)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
87
rpm/rpm_test.go
Normal file
87
rpm/rpm_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package rpm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogrlx/snack"
|
||||
)
|
||||
|
||||
// Compile-time interface assertions.
|
||||
var (
|
||||
_ snack.Manager = (*RPM)(nil)
|
||||
_ snack.FileOwner = (*RPM)(nil)
|
||||
_ snack.NameNormalizer = (*RPM)(nil)
|
||||
)
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
r := New()
|
||||
if got := r.Name(); got != "rpm" {
|
||||
t.Errorf("Name() = %q, want %q", got, "rpm")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCapabilities(t *testing.T) {
|
||||
r := New()
|
||||
caps := snack.GetCapabilities(r)
|
||||
|
||||
wantTrue := map[string]bool{
|
||||
"FileOwnership": caps.FileOwnership,
|
||||
"NameNormalize": caps.NameNormalize,
|
||||
}
|
||||
for name, got := range wantTrue {
|
||||
t.Run(name+"_true", func(t *testing.T) {
|
||||
if !got {
|
||||
t.Errorf("Capabilities.%s = false, want true", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wantFalse := map[string]bool{
|
||||
"VersionQuery": caps.VersionQuery,
|
||||
"Hold": caps.Hold,
|
||||
"Clean": caps.Clean,
|
||||
"RepoManagement": caps.RepoManagement,
|
||||
"KeyManagement": caps.KeyManagement,
|
||||
"Groups": caps.Groups,
|
||||
"DryRun": caps.DryRun,
|
||||
}
|
||||
for name, got := range wantFalse {
|
||||
t.Run(name+"_false", func(t *testing.T) {
|
||||
if got {
|
||||
t.Errorf("Capabilities.%s = true, want false", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeNameMethod(t *testing.T) {
|
||||
r := New()
|
||||
tests := []struct {
|
||||
input, want string
|
||||
}{
|
||||
{"nginx.x86_64", "nginx"},
|
||||
{"curl", "curl"},
|
||||
{"bash.noarch", "bash"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := r.NormalizeName(tt.input); got != tt.want {
|
||||
t.Errorf("NormalizeName(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArchMethod(t *testing.T) {
|
||||
r := New()
|
||||
name, arch := r.ParseArch("nginx.x86_64")
|
||||
if name != "nginx" || arch != "x86_64" {
|
||||
t.Errorf("ParseArch(\"nginx.x86_64\") = (%q, %q), want (\"nginx\", \"x86_64\")", name, arch)
|
||||
}
|
||||
}
|
||||
|
||||
// RPM should NOT implement DryRunner.
|
||||
func TestNotDryRunner(t *testing.T) {
|
||||
r := New()
|
||||
if _, ok := interface{}(r).(snack.DryRunner); ok {
|
||||
t.Error("RPM should not implement DryRunner")
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,56 @@ func TestParseSnapListEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSnapListEdgeCases(t *testing.T) {
|
||||
t.Run("single entry", func(t *testing.T) {
|
||||
input := `Name Version Rev Tracking Publisher Notes
|
||||
core22 20240111 1122 latest/stable canonical✓ base
|
||||
`
|
||||
pkgs := parseSnapList(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "core22" {
|
||||
t.Errorf("expected core22, got %q", pkgs[0].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("header only no trailing newline", func(t *testing.T) {
|
||||
input := "Name Version Rev Tracking Publisher Notes"
|
||||
pkgs := parseSnapList(input)
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single field line skipped", func(t *testing.T) {
|
||||
input := "Name Version Rev Tracking Publisher Notes\nsinglefield\n"
|
||||
pkgs := parseSnapList(input)
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages (need >=2 fields), got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("extra whitespace lines", func(t *testing.T) {
|
||||
input := "Name Version Rev Tracking Publisher Notes\n \n\n"
|
||||
pkgs := parseSnapList(input)
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("many columns", func(t *testing.T) {
|
||||
input := "Name Version Rev Tracking Publisher Notes\nsnap1 2.0 100 latest/stable pub note extra more\n"
|
||||
pkgs := parseSnapList(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "snap1" || pkgs[0].Version != "2.0" {
|
||||
t.Errorf("unexpected package: %+v", pkgs[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseSnapFind(t *testing.T) {
|
||||
input := `Name Version Publisher Notes Summary
|
||||
firefox 131.0 mozilla✓ - Mozilla Firefox web browser
|
||||
@@ -52,6 +102,67 @@ chromium 129.0 nickvdp - Chromium web browser
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSnapFindEdgeCases(t *testing.T) {
|
||||
t.Run("single result", func(t *testing.T) {
|
||||
input := "Name Version Publisher Notes Summary\nfirefox 131.0 mozilla - Web browser\n"
|
||||
pkgs := parseSnapFind(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "firefox" {
|
||||
t.Errorf("expected firefox, got %q", pkgs[0].Name)
|
||||
}
|
||||
if pkgs[0].Description != "Web browser" {
|
||||
t.Errorf("expected 'Web browser', got %q", pkgs[0].Description)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("header only", func(t *testing.T) {
|
||||
input := "Name Version Publisher Notes Summary\n"
|
||||
pkgs := parseSnapFind(input)
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("too few fields skipped", func(t *testing.T) {
|
||||
input := "Name Version Publisher Notes Summary\nfoo 1.0 pub\n"
|
||||
pkgs := parseSnapFind(input)
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages (need >=4 fields), got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("exactly four fields no summary", func(t *testing.T) {
|
||||
input := "Name Version Publisher Notes Summary\nfoo 1.0 pub note\n"
|
||||
pkgs := parseSnapFind(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Description != "" {
|
||||
t.Errorf("expected empty description, got %q", pkgs[0].Description)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multi-word summary", func(t *testing.T) {
|
||||
input := "Name Version Publisher Notes Summary\nmysnap 2.0 me - A very long description with many words\n"
|
||||
pkgs := parseSnapFind(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Description != "A very long description with many words" {
|
||||
t.Errorf("unexpected description: %q", pkgs[0].Description)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty input", func(t *testing.T) {
|
||||
pkgs := parseSnapFind("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseSnapInfo(t *testing.T) {
|
||||
input := `name: firefox
|
||||
summary: Mozilla Firefox web browser
|
||||
@@ -84,6 +195,57 @@ func TestParseSnapInfoEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSnapInfoEdgeCases(t *testing.T) {
|
||||
t.Run("not installed snap", func(t *testing.T) {
|
||||
input := `name: hello-world
|
||||
summary: A simple hello world snap
|
||||
publisher: Canonical✓ (canonical✓)
|
||||
snap-id: buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ
|
||||
`
|
||||
pkg := parseSnapInfo(input)
|
||||
if pkg == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if pkg.Name != "hello-world" {
|
||||
t.Errorf("expected hello-world, got %q", pkg.Name)
|
||||
}
|
||||
if pkg.Installed {
|
||||
t.Error("expected Installed=false for snap without installed field")
|
||||
}
|
||||
if pkg.Version != "" {
|
||||
t.Errorf("expected empty version, got %q", pkg.Version)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("name only", func(t *testing.T) {
|
||||
pkg := parseSnapInfo("name: test\n")
|
||||
if pkg == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if pkg.Name != "test" {
|
||||
t.Errorf("expected test, got %q", pkg.Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no name returns nil", func(t *testing.T) {
|
||||
pkg := parseSnapInfo("summary: something\ninstalled: 1.0 (1) 10MB\n")
|
||||
if pkg != nil {
|
||||
t.Error("expected nil when no name field")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no colon lines ignored", func(t *testing.T) {
|
||||
input := "name: mysnap\nrandom text without colon\nsummary: A snap\n"
|
||||
pkg := parseSnapInfo(input)
|
||||
if pkg == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
if pkg.Description != "A snap" {
|
||||
t.Errorf("unexpected description: %q", pkg.Description)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseSnapInfoVersion(t *testing.T) {
|
||||
input := `name: firefox
|
||||
channels:
|
||||
@@ -105,6 +267,39 @@ func TestParseSnapInfoVersionMissing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSnapInfoVersionEdgeCases(t *testing.T) {
|
||||
t.Run("empty input", func(t *testing.T) {
|
||||
ver := parseSnapInfoVersion("")
|
||||
if ver != "" {
|
||||
t.Errorf("expected empty, got %q", ver)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("stable channel with dashes", func(t *testing.T) {
|
||||
input := " latest/stable: 2024.01.15 2024-01-15 (100) 50MB -\n"
|
||||
ver := parseSnapInfoVersion(input)
|
||||
if ver != "2024.01.15" {
|
||||
t.Errorf("expected 2024.01.15, got %q", ver)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("closed channel marked --", func(t *testing.T) {
|
||||
input := " latest/stable: --\n"
|
||||
ver := parseSnapInfoVersion(input)
|
||||
if ver != "" {
|
||||
t.Errorf("expected empty for closed channel, got %q", ver)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("closed channel marked ^", func(t *testing.T) {
|
||||
input := " latest/stable: ^ \n"
|
||||
ver := parseSnapInfoVersion(input)
|
||||
if ver != "" {
|
||||
t.Errorf("expected empty for ^ channel, got %q", ver)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseSnapRefreshList(t *testing.T) {
|
||||
input := `Name Version Rev Publisher Notes
|
||||
firefox 132.0 4650 mozilla✓ -
|
||||
@@ -128,30 +323,127 @@ All snaps up to date.
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSnapRefreshListEdgeCases(t *testing.T) {
|
||||
t.Run("multiple upgrades", func(t *testing.T) {
|
||||
input := "Name Version Rev Publisher Notes\nfoo 2.0 10 pub -\nbar 3.0 20 pub -\n"
|
||||
pkgs := parseSnapRefreshList(input)
|
||||
if len(pkgs) != 2 {
|
||||
t.Fatalf("expected 2 packages, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "foo" || pkgs[1].Name != "bar" {
|
||||
t.Errorf("unexpected packages: %+v, %+v", pkgs[0], pkgs[1])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("header only", func(t *testing.T) {
|
||||
input := "Name Version Rev Publisher Notes\n"
|
||||
pkgs := parseSnapRefreshList(input)
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSemverCmp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b string
|
||||
want int
|
||||
}{
|
||||
{"1.0.0", "1.0.0", 0},
|
||||
{"1.0.0", "2.0.0", -1},
|
||||
{"2.0.0", "1.0.0", 1},
|
||||
{"1.2.3", "1.2.4", -1},
|
||||
{"1.10.0", "1.9.0", 1},
|
||||
{"1.0", "1.0.0", 0},
|
||||
{"131.0", "132.0", -1},
|
||||
{"equal", "1.0.0", "1.0.0", 0},
|
||||
{"less major", "1.0.0", "2.0.0", -1},
|
||||
{"greater major", "2.0.0", "1.0.0", 1},
|
||||
{"less patch", "1.2.3", "1.2.4", -1},
|
||||
{"multi-digit minor", "1.10.0", "1.9.0", 1},
|
||||
{"short vs long equal", "1.0", "1.0.0", 0},
|
||||
{"real versions", "131.0", "132.0", -1},
|
||||
// Edge cases
|
||||
{"single component", "5", "3", 1},
|
||||
{"single equal", "3", "3", 0},
|
||||
{"empty vs empty", "", "", 0},
|
||||
{"empty vs version", "", "1.0", -1},
|
||||
{"version vs empty", "1.0", "", 1},
|
||||
{"non-numeric suffix", "1.0.0-beta", "1.0.0-rc1", 0},
|
||||
{"four components", "1.2.3.4", "1.2.3.5", -1},
|
||||
{"different lengths padded", "1.0.0.0", "1.0.0", 0},
|
||||
{"short less", "1.0", "1.0.1", -1},
|
||||
{"short greater", "1.1", "1.0.9", 1},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := semverCmp(tt.a, tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("semverCmp(%q, %q) = %d, want %d", tt.a, tt.b, got, tt.want)
|
||||
}
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := semverCmp(tt.a, tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("semverCmp(%q, %q) = %d, want %d", tt.a, tt.b, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripNonNumeric(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"123", "123"},
|
||||
{"123abc", "123"},
|
||||
{"abc", ""},
|
||||
{"0beta", "0"},
|
||||
{"", ""},
|
||||
{"42-rc1", "42"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got := stripNonNumeric(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("stripNonNumeric(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceCompliance(t *testing.T) {
|
||||
var _ snack.Manager = (*Snap)(nil)
|
||||
var _ snack.VersionQuerier = (*Snap)(nil)
|
||||
var _ snack.Cleaner = (*Snap)(nil)
|
||||
var _ snack.PackageUpgrader = (*Snap)(nil)
|
||||
}
|
||||
|
||||
// Compile-time interface checks
|
||||
var (
|
||||
_ snack.Cleaner = (*Snap)(nil)
|
||||
_ snack.PackageUpgrader = (*Snap)(nil)
|
||||
)
|
||||
|
||||
func TestCapabilities(t *testing.T) {
|
||||
caps := snack.GetCapabilities(New())
|
||||
if !caps.VersionQuery {
|
||||
t.Error("expected VersionQuery=true")
|
||||
}
|
||||
if !caps.Clean {
|
||||
t.Error("expected Clean=true")
|
||||
}
|
||||
// Should be false
|
||||
if caps.FileOwnership {
|
||||
t.Error("expected FileOwnership=false")
|
||||
}
|
||||
if caps.DryRun {
|
||||
t.Error("expected DryRun=false")
|
||||
}
|
||||
if caps.Hold {
|
||||
t.Error("expected Hold=false")
|
||||
}
|
||||
if caps.RepoManagement {
|
||||
t.Error("expected RepoManagement=false")
|
||||
}
|
||||
if caps.KeyManagement {
|
||||
t.Error("expected KeyManagement=false")
|
||||
}
|
||||
if caps.Groups {
|
||||
t.Error("expected Groups=false")
|
||||
}
|
||||
if caps.NameNormalize {
|
||||
t.Error("expected NameNormalize=false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user