Files
snack/pkg/pkg_test.go
Tai Groot 1fa7de6d66 feat: add PackageUpgrade to Capabilities, exhaustive detect + CLI tests
- Add PackageUpgrade field to Capabilities struct and GetCapabilities
- Add PackageUpgrade to all 11 provider capability tests
- Add pkg-upgrade to CLI detect command output
- Expand detect tests: ByName for all managers, concurrent Reset,
  HasBinary, candidates/allManagers coverage
- Add cmd/snack unit tests: targets, opts, getManager, version
- 838 tests passing, 0 failures
2026-03-06 01:07:35 +00:00

672 lines
16 KiB
Go

package pkg
import (
"testing"
"github.com/gogrlx/snack"
)
func TestParseQuery(t *testing.T) {
input := "nginx\t1.24.0\tRobust and small WWW server\ncurl\t8.5.0\tCommand line tool for transferring data\n"
pkgs := parseQuery(input)
if len(pkgs) != 2 {
t.Fatalf("expected 2 packages, got %d", len(pkgs))
}
if pkgs[0].Name != "nginx" || pkgs[0].Version != "1.24.0" {
t.Errorf("unexpected first package: %+v", pkgs[0])
}
if pkgs[0].Description != "Robust and small WWW server" {
t.Errorf("unexpected description: %q", pkgs[0].Description)
}
if !pkgs[0].Installed {
t.Error("expected Installed=true")
}
}
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
`
pkgs := parseSearch(input)
if len(pkgs) != 2 {
t.Fatalf("expected 2 packages, got %d", len(pkgs))
}
if pkgs[0].Name != "nginx" || pkgs[0].Version != "1.24.0" {
t.Errorf("unexpected first package: %+v", pkgs[0])
}
if pkgs[1].Name != "curl" || pkgs[1].Version != "8.5.0" {
t.Errorf("unexpected second package: %+v", pkgs[1])
}
}
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
Comment : Robust and small WWW server
Arch : FreeBSD:14:amd64
`
pkg := parseInfo(input)
if pkg == nil {
t.Fatal("expected non-nil package")
}
if pkg.Name != "nginx" {
t.Errorf("expected name 'nginx', got %q", pkg.Name)
}
if pkg.Version != "1.24.0" {
t.Errorf("unexpected version: %q", pkg.Version)
}
if pkg.Description != "Robust and small WWW server" {
t.Errorf("unexpected description: %q", pkg.Description)
}
if pkg.Arch != "FreeBSD:14:amd64" {
t.Errorf("unexpected arch: %q", pkg.Arch)
}
}
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:
Upgrading nginx: 1.24.0 -> 1.26.0
Upgrading curl: 8.5.0 -> 8.6.0
Number of packages to be upgraded: 2
`
pkgs := parseUpgrades(input)
if len(pkgs) != 2 {
t.Fatalf("expected 2 packages, got %d", len(pkgs))
}
if pkgs[0].Name != "nginx" || pkgs[0].Version != "1.26.0" {
t.Errorf("unexpected first package: %+v", pkgs[0])
}
if pkgs[1].Name != "curl" || pkgs[1].Version != "8.6.0" {
t.Errorf("unexpected second package: %+v", pkgs[1])
}
}
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
/usr/local/etc/nginx/nginx.conf
/usr/local/share/doc/nginx/README
`
files := parseFileList(input)
if len(files) != 3 {
t.Fatalf("expected 3 files, got %d", len(files))
}
if files[0] != "/usr/local/sbin/nginx" {
t.Errorf("unexpected file: %q", files[0])
}
}
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)
if name != "nginx" {
t.Errorf("expected 'nginx', got %q", name)
}
}
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
wantName string
wantVersion string
}{
{"nginx-1.24.0", "nginx", "1.24.0"},
{"py39-pip-23.1", "py39-pip", "23.1"},
{"bash", "bash", ""},
}
for _, tt := range tests {
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 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)
var _ snack.Cleaner = (*Pkg)(nil)
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")
}
if !caps.PackageUpgrade {
t.Error("expected PackageUpgrade=true")
}
}