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