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:
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user