mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-02 13:18:43 -07:00
- Update go.mod from Go 1.26.0 to 1.26.1 - Update dependencies: golang.org/x/sync, golang.org/x/sys, charmbracelet/x/exp/charmtone, mattn/go-runewidth - Fix goimports formatting in 10 files - Add apk/normalize_test.go: tests for normalizeName and parseArchNormalize with all known arch suffixes - Add rpm/parse_test.go: tests for parseList, parseInfo, parseArchSuffix, and normalizeName (all at 100% coverage) - All tests pass with -race, staticcheck and go vet clean
639 lines
15 KiB
Go
639 lines
15 KiB
Go
package apt
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/gogrlx/snack"
|
|
)
|
|
|
|
func TestParseList_EdgeCases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want int
|
|
}{
|
|
{"empty", "", 0},
|
|
{"whitespace_only", " \n \n ", 0},
|
|
{"single_tab_field", "bash", 0}, // needs at least 2 tab-separated fields
|
|
{"no_description", "bash\t5.2-1", 1}, // 2 fields is OK
|
|
{"with_description", "bash\t5.2-1\tGNU Bourne Again SHell", 1},
|
|
{"blank_lines_mixed", "\nbash\t5.2-1\n\ncurl\t7.88\n\n", 2},
|
|
{"trailing_newline", "bash\t5.2-1\n", 1},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
pkgs := parseList(tt.input)
|
|
if len(pkgs) != tt.want {
|
|
t.Errorf("parseList() returned %d packages, want %d", len(pkgs), tt.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Verify fields are populated correctly
|
|
t.Run("field_values", func(t *testing.T) {
|
|
input := "bash\t5.2-1\tGNU Bourne Again SHell"
|
|
pkgs := parseList(input)
|
|
if len(pkgs) != 1 {
|
|
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
|
}
|
|
p := pkgs[0]
|
|
if p.Name != "bash" {
|
|
t.Errorf("Name = %q, want %q", p.Name, "bash")
|
|
}
|
|
if p.Version != "5.2-1" {
|
|
t.Errorf("Version = %q, want %q", p.Version, "5.2-1")
|
|
}
|
|
if p.Description != "GNU Bourne Again SHell" {
|
|
t.Errorf("Description = %q, want %q", p.Description, "GNU Bourne Again SHell")
|
|
}
|
|
if !p.Installed {
|
|
t.Error("expected Installed=true")
|
|
}
|
|
})
|
|
|
|
t.Run("no_description_fields", func(t *testing.T) {
|
|
pkgs := parseList("vim\t9.0")
|
|
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)
|
|
}
|
|
})
|
|
|
|
t.Run("description_with_tabs", func(t *testing.T) {
|
|
// Third field captures everything after the second tab
|
|
pkgs := parseList("pkg\t1.0\tdesc\twith\ttabs")
|
|
if len(pkgs) != 1 {
|
|
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
|
}
|
|
if pkgs[0].Description != "desc\twith\ttabs" {
|
|
t.Errorf("Description = %q, want %q", pkgs[0].Description, "desc\twith\ttabs")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestParseSearch_EdgeCases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want int
|
|
}{
|
|
{"empty", "", 0},
|
|
{"whitespace_only", " \n ", 0},
|
|
{"no_dash_separator", "vim", 1}, // still parses, just no description
|
|
{"normal", "vim - Vi IMproved", 1},
|
|
{"blank_lines", "\nvim - Vi IMproved\n\nnano - small editor\n", 2},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
pkgs := parseSearch(tt.input)
|
|
if len(pkgs) != tt.want {
|
|
t.Errorf("parseSearch() returned %d packages, want %d", len(pkgs), tt.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("no_description", func(t *testing.T) {
|
|
pkgs := parseSearch("vim")
|
|
if len(pkgs) != 1 {
|
|
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
|
}
|
|
if pkgs[0].Name != "vim" {
|
|
t.Errorf("Name = %q, want %q", pkgs[0].Name, "vim")
|
|
}
|
|
if pkgs[0].Description != "" {
|
|
t.Errorf("Description = %q, want empty", pkgs[0].Description)
|
|
}
|
|
})
|
|
|
|
t.Run("description_with_dashes", func(t *testing.T) {
|
|
// Only splits on first " - "
|
|
pkgs := parseSearch("gcc - GNU C Compiler - version 12")
|
|
if len(pkgs) != 1 {
|
|
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
|
}
|
|
if pkgs[0].Name != "gcc" {
|
|
t.Errorf("Name = %q, want %q", pkgs[0].Name, "gcc")
|
|
}
|
|
if pkgs[0].Description != "GNU C Compiler - version 12" {
|
|
t.Errorf("Description = %q, want %q", pkgs[0].Description, "GNU C Compiler - version 12")
|
|
}
|
|
})
|
|
|
|
t.Run("whitespace_trimming", func(t *testing.T) {
|
|
pkgs := parseSearch(" vim - Vi IMproved ")
|
|
if len(pkgs) != 1 {
|
|
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
|
}
|
|
if pkgs[0].Name != "vim" {
|
|
t.Errorf("Name = %q, want %q", pkgs[0].Name, "vim")
|
|
}
|
|
if pkgs[0].Description != "Vi IMproved" {
|
|
t.Errorf("Description = %q, want %q", pkgs[0].Description, "Vi IMproved")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestParseInfo_EdgeCases(t *testing.T) {
|
|
t.Run("all_fields", func(t *testing.T) {
|
|
input := "Package: bash\nVersion: 5.2-1\nArchitecture: amd64\nDescription: GNU Bourne Again SHell\n"
|
|
p, err := parseInfo(input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.Name != "bash" || p.Version != "5.2-1" || p.Arch != "amd64" || p.Description != "GNU Bourne Again SHell" {
|
|
t.Errorf("unexpected package: %+v", p)
|
|
}
|
|
})
|
|
|
|
t.Run("empty_returns_not_found", func(t *testing.T) {
|
|
_, err := parseInfo("")
|
|
if err != snack.ErrNotFound {
|
|
t.Errorf("expected ErrNotFound, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("no_package_field", func(t *testing.T) {
|
|
_, err := parseInfo("Version: 1.0\nArchitecture: amd64\n")
|
|
if err != snack.ErrNotFound {
|
|
t.Errorf("expected ErrNotFound, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("extra_fields_ignored", func(t *testing.T) {
|
|
input := "Package: curl\nMaintainer: someone\nVersion: 7.88\nPriority: optional\n"
|
|
p, err := parseInfo(input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.Name != "curl" || p.Version != "7.88" {
|
|
t.Errorf("unexpected: %+v", p)
|
|
}
|
|
})
|
|
|
|
t.Run("lines_without_colon", func(t *testing.T) {
|
|
input := "Package: vim\nsome continuation line\nVersion: 9.0\n"
|
|
p, err := parseInfo(input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.Name != "vim" || p.Version != "9.0" {
|
|
t.Errorf("unexpected: %+v", p)
|
|
}
|
|
})
|
|
|
|
t.Run("version_with_epoch", func(t *testing.T) {
|
|
input := "Package: systemd\nVersion: 1:252-2\nArchitecture: amd64\n"
|
|
p, err := parseInfo(input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.Version != "1:252-2" {
|
|
t.Errorf("Version = %q, want %q", p.Version, "1:252-2")
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- 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)
|
|
}
|
|
})
|
|
}
|
|
}
|