mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-14 10:41:23 -07:00
Compare commits
3 Commits
v0.2.0
...
cd/aur-imp
| Author | SHA1 | Date | |
|---|---|---|---|
| b6b50491e2 | |||
| a1d13e8a7d | |||
| c00133718e |
30
.github/workflows/release.yml
vendored
30
.github/workflows/release.yml
vendored
@@ -1,30 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
goreleaser:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
|
|
||||||
- uses: goreleaser/goreleaser-action@v6
|
|
||||||
with:
|
|
||||||
distribution: goreleaser-pro
|
|
||||||
version: "~> v2"
|
|
||||||
args: release --clean
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
|
||||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
|
||||||
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
before:
|
|
||||||
hooks:
|
|
||||||
- go mod tidy
|
|
||||||
|
|
||||||
snapshot:
|
|
||||||
name_template: "{{ incpatch .Version }}-next"
|
|
||||||
|
|
||||||
builds:
|
|
||||||
- main: ./cmd/snack/
|
|
||||||
id: snack
|
|
||||||
binary: snack
|
|
||||||
goos:
|
|
||||||
- linux
|
|
||||||
- darwin
|
|
||||||
- freebsd
|
|
||||||
- openbsd
|
|
||||||
goarch:
|
|
||||||
- amd64
|
|
||||||
- arm64
|
|
||||||
- arm
|
|
||||||
ldflags:
|
|
||||||
- -X main.version={{.Version}}
|
|
||||||
|
|
||||||
universal_binaries:
|
|
||||||
- id: snack-universal
|
|
||||||
ids:
|
|
||||||
- snack
|
|
||||||
name_template: "snack-{{.Version}}-darwin-universal"
|
|
||||||
replace: true
|
|
||||||
|
|
||||||
archives:
|
|
||||||
- formats: tar.gz
|
|
||||||
allow_different_binary_count: true
|
|
||||||
ids:
|
|
||||||
- snack
|
|
||||||
- snack-universal
|
|
||||||
name_template: "snack-{{.Version}}-{{.Os}}-{{.Arch}}"
|
|
||||||
|
|
||||||
nfpms:
|
|
||||||
- id: snack
|
|
||||||
package_name: snack
|
|
||||||
builds: [snack]
|
|
||||||
formats: [apk, deb, rpm]
|
|
||||||
bindir: /usr/bin
|
|
||||||
description: "A unified CLI for system package managers"
|
|
||||||
maintainer: Tai Groot <tai@taigrr.com>
|
|
||||||
license: 0BSD
|
|
||||||
homepage: https://github.com/gogrlx/snack
|
|
||||||
vendor: Adatomic, Inc.
|
|
||||||
|
|
||||||
release:
|
|
||||||
github:
|
|
||||||
owner: gogrlx
|
|
||||||
name: snack
|
|
||||||
ids:
|
|
||||||
- snack
|
|
||||||
- snack-universal
|
|
||||||
draft: true
|
|
||||||
prerelease: auto
|
|
||||||
|
|
||||||
changelog:
|
|
||||||
use: github
|
|
||||||
sort: asc
|
|
||||||
abbrev: -1
|
|
||||||
filters:
|
|
||||||
exclude:
|
|
||||||
- "^docs:"
|
|
||||||
- "^test:"
|
|
||||||
- "^ci:"
|
|
||||||
- "^chore:"
|
|
||||||
- "^style:"
|
|
||||||
groups:
|
|
||||||
- title: "Breaking Changes"
|
|
||||||
regexp: "^.*!:+.*$"
|
|
||||||
order: 0
|
|
||||||
- title: "Features"
|
|
||||||
regexp: "^.*feat[(\\w)]*:+.*$"
|
|
||||||
order: 1
|
|
||||||
- title: "Bug Fixes"
|
|
||||||
regexp: "^.*fix[(\\w)]*:+.*$"
|
|
||||||
order: 2
|
|
||||||
- title: "Performance"
|
|
||||||
regexp: "^.*perf[(\\w)]*:+.*$"
|
|
||||||
order: 3
|
|
||||||
- title: "Refactor"
|
|
||||||
regexp: "^.*refactor[(\\w)]*:+.*$"
|
|
||||||
order: 4
|
|
||||||
- title: "Build"
|
|
||||||
regexp: "^.*build[(\\w)]*:+.*$"
|
|
||||||
order: 5
|
|
||||||
- title: Others
|
|
||||||
order: 999
|
|
||||||
132
README.md
132
README.md
@@ -11,22 +11,20 @@ Part of the [grlx](https://github.com/gogrlx/grlx) ecosystem.
|
|||||||
|
|
||||||
## Supported Package Managers
|
## Supported Package Managers
|
||||||
|
|
||||||
| Package | Manager | Platform | Extras |
|
| Package | Manager | Platform | Status |
|
||||||
|---------|---------|----------|--------|
|
|---------|---------|----------|--------|
|
||||||
| `apt` | APT (apt-get/apt-cache) | Debian/Ubuntu | DryRun, FileOwner, Holder, KeyManager, NameNormalizer, RepoManager |
|
| `pacman` | pacman | Arch Linux | đźš§ |
|
||||||
| `dpkg` | dpkg | Debian/Ubuntu | DryRun, FileOwner, NameNormalizer |
|
| `aur` | AUR (makepkg) | Arch Linux | đźš§ |
|
||||||
| `dnf` | DNF 4/5 | Fedora/RHEL | DryRun, FileOwner, Grouper, Holder, KeyManager, NameNormalizer, RepoManager |
|
| `apk` | apk-tools | Alpine Linux | đźš§ |
|
||||||
| `rpm` | RPM | Fedora/RHEL | FileOwner, NameNormalizer |
|
| `apt` | APT (apt-get/apt-cache) | Debian/Ubuntu | đźš§ |
|
||||||
| `pacman` | pacman | Arch Linux | DryRun, FileOwner, Grouper |
|
| `dpkg` | dpkg | Debian/Ubuntu | đźš§ |
|
||||||
| `aur` | AUR (makepkg) | Arch Linux | — |
|
| `dnf` | DNF | Fedora/RHEL | đźš§ |
|
||||||
| `apk` | apk-tools | Alpine Linux | DryRun, FileOwner |
|
| `rpm` | RPM | Fedora/RHEL | đźš§ |
|
||||||
| `flatpak` | Flatpak | Cross-distro | RepoManager |
|
| `flatpak` | Flatpak | Cross-distro | đźš§ |
|
||||||
| `snap` | snapd | Cross-distro | — |
|
| `snap` | snapd | Cross-distro | đźš§ |
|
||||||
| `pkg` | pkg(8) | FreeBSD | FileOwner |
|
| `pkg` | pkg(8) | FreeBSD | đźš§ |
|
||||||
| `ports` | ports/packages | OpenBSD | FileOwner |
|
| `ports` | ports/packages | OpenBSD | đźš§ |
|
||||||
| `detect` | Auto-detection | All | — |
|
| `detect` | Auto-detection | All | đźš§ |
|
||||||
|
|
||||||
All providers implement `Manager`, `VersionQuerier`, `Cleaner`, and `PackageUpgrader`. The **Extras** column lists additional capabilities beyond that baseline.
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@@ -52,12 +50,11 @@ func main() {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
mgr := apt.New()
|
mgr := apt.New()
|
||||||
|
|
||||||
// Install packages
|
// Install a package
|
||||||
result, err := mgr.Install(ctx, snack.Targets("nginx", "curl"), snack.WithSudo(), snack.WithAssumeYes())
|
err := mgr.Install(ctx, []string{"nginx"}, snack.WithSudo(), snack.WithAssumeYes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Installed: %d, Unchanged: %d\n", len(result.Installed), len(result.Unchanged))
|
|
||||||
|
|
||||||
// Check if installed
|
// Check if installed
|
||||||
installed, err := mgr.IsInstalled(ctx, "nginx")
|
installed, err := mgr.IsInstalled(ctx, "nginx")
|
||||||
@@ -65,14 +62,6 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
fmt.Println("nginx installed:", installed)
|
fmt.Println("nginx installed:", installed)
|
||||||
|
|
||||||
// Upgrade specific packages
|
|
||||||
if up, ok := mgr.(snack.PackageUpgrader); ok {
|
|
||||||
_, err := up.UpgradePackages(ctx, snack.Targets("nginx"), snack.WithSudo(), snack.WithAssumeYes())
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -86,11 +75,6 @@ if err != nil {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
fmt.Println("Detected:", mgr.Name())
|
fmt.Println("Detected:", mgr.Name())
|
||||||
|
|
||||||
// All available managers
|
|
||||||
for _, m := range detect.All() {
|
|
||||||
fmt.Println(m.Name())
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Interfaces
|
## Interfaces
|
||||||
@@ -99,21 +83,17 @@ snack uses a layered interface design. Every provider implements `Manager` (the
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
// Base — every provider
|
// Base — every provider
|
||||||
snack.Manager // Install, Remove, Purge, Upgrade, Update, List, Search, Info, IsInstalled, Version
|
snack.Manager // Install, Remove, Purge, Upgrade, Update, List, Search, Info, IsInstalled, Version
|
||||||
|
|
||||||
// Core optional — implemented by all providers
|
// Optional capabilities — type-assert to check
|
||||||
snack.VersionQuerier // LatestVersion, ListUpgrades, UpgradeAvailable, VersionCmp
|
snack.VersionQuerier // LatestVersion, ListUpgrades, UpgradeAvailable, VersionCmp
|
||||||
snack.Cleaner // Autoremove, Clean (orphan/cache cleanup)
|
snack.Holder // Hold, Unhold, ListHeld (version pinning)
|
||||||
snack.PackageUpgrader // UpgradePackages (upgrade specific packages)
|
snack.Cleaner // Autoremove, Clean (orphan/cache cleanup)
|
||||||
|
snack.FileOwner // FileList, Owner (file-to-package queries)
|
||||||
// Provider-specific — type-assert to check
|
snack.RepoManager // ListRepos, AddRepo, RemoveRepo
|
||||||
snack.Holder // Hold, Unhold, ListHeld, IsHeld (version pinning)
|
snack.KeyManager // AddKey, RemoveKey, ListKeys (GPG keys)
|
||||||
snack.FileOwner // FileList, Owner (file-to-package queries)
|
snack.Grouper // GroupList, GroupInfo, GroupInstall
|
||||||
snack.RepoManager // ListRepos, AddRepo, RemoveRepo
|
snack.NameNormalizer // NormalizeName, ParseArch
|
||||||
snack.KeyManager // AddKey, RemoveKey, ListKeys (GPG keys)
|
|
||||||
snack.Grouper // GroupList, GroupInfo, GroupInstall, GroupIsInstalled
|
|
||||||
snack.NameNormalizer // NormalizeName, ParseArch
|
|
||||||
snack.DryRunner // SupportsDryRun (honors WithDryRun option)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Check capabilities at runtime:
|
Check capabilities at runtime:
|
||||||
@@ -123,49 +103,8 @@ caps := snack.GetCapabilities(mgr)
|
|||||||
if caps.Hold {
|
if caps.Hold {
|
||||||
mgr.(snack.Holder).Hold(ctx, []string{"nginx"})
|
mgr.(snack.Holder).Hold(ctx, []string{"nginx"})
|
||||||
}
|
}
|
||||||
if caps.FileOwnership {
|
|
||||||
owner, _ := mgr.(snack.FileOwner).Owner(ctx, "/usr/bin/curl")
|
|
||||||
fmt.Println("Owned by:", owner)
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
|
||||||
|
|
||||||
All mutating operations accept functional options:
|
|
||||||
|
|
||||||
```go
|
|
||||||
snack.WithSudo() // prepend sudo
|
|
||||||
snack.WithAssumeYes() // auto-confirm prompts
|
|
||||||
snack.WithDryRun() // simulate (if DryRunner)
|
|
||||||
snack.WithVerbose() // verbose output
|
|
||||||
snack.WithRefresh() // refresh index before operation
|
|
||||||
snack.WithReinstall() // reinstall even if current
|
|
||||||
snack.WithRoot("/mnt") // alternate root filesystem
|
|
||||||
snack.WithFromRepo("sid") // install from specific repository
|
|
||||||
```
|
|
||||||
|
|
||||||
## CLI
|
|
||||||
|
|
||||||
A companion CLI is included at `cmd/snack`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
snack install nginx curl # install packages
|
|
||||||
snack remove nginx # remove packages
|
|
||||||
snack upgrade # upgrade all packages
|
|
||||||
snack update # refresh package index
|
|
||||||
snack search redis # search for packages
|
|
||||||
snack info nginx # show package details
|
|
||||||
snack list # list installed packages
|
|
||||||
snack which /usr/bin/curl # find owning package
|
|
||||||
snack hold nginx # pin package version
|
|
||||||
snack unhold nginx # unpin package version
|
|
||||||
snack clean # autoremove + clean cache
|
|
||||||
snack detect # show detected managers + capabilities
|
|
||||||
snack version # show version
|
|
||||||
```
|
|
||||||
|
|
||||||
Global flags: `--manager <name>`, `--sudo`, `--yes`, `--dry-run`
|
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
- **Thin CLI wrappers** — each sub-package wraps a package manager's CLI tools. No FFI, no library bindings.
|
- **Thin CLI wrappers** — each sub-package wraps a package manager's CLI tools. No FFI, no library bindings.
|
||||||
@@ -176,6 +115,27 @@ Global flags: `--manager <name>`, `--sudo`, `--yes`, `--dry-run`
|
|||||||
- **Platform-safe** — build tags ensure packages compile everywhere but only run where appropriate.
|
- **Platform-safe** — build tags ensure packages compile everywhere but only run where appropriate.
|
||||||
- **No root assumption** — use `snack.WithSudo()` when elevated privileges are needed.
|
- **No root assumption** — use `snack.WithSudo()` when elevated privileges are needed.
|
||||||
|
|
||||||
|
## Implementation Priority
|
||||||
|
|
||||||
|
1. pacman + AUR (Arch Linux)
|
||||||
|
2. apk (Alpine Linux)
|
||||||
|
3. apt + dpkg (Debian/Ubuntu)
|
||||||
|
4. dnf + rpm (Fedora/RHEL)
|
||||||
|
5. flatpak + snap (cross-distro)
|
||||||
|
6. pkg + ports (BSD)
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
A companion CLI tool is planned for direct terminal usage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
snack install nginx
|
||||||
|
snack remove nginx
|
||||||
|
snack search redis
|
||||||
|
snack list
|
||||||
|
snack upgrade
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
0BSD — see [LICENSE](LICENSE).
|
0BSD — see [LICENSE](LICENSE).
|
||||||
|
|||||||
399
apk/apk_test.go
399
apk/apk_test.go
@@ -17,23 +17,13 @@ func TestSplitNameVersion(t *testing.T) {
|
|||||||
{"libcurl-doc-8.5.0-r0", "libcurl-doc", "8.5.0-r0"},
|
{"libcurl-doc-8.5.0-r0", "libcurl-doc", "8.5.0-r0"},
|
||||||
{"go-1.21.5-r0", "go", "1.21.5-r0"},
|
{"go-1.21.5-r0", "go", "1.21.5-r0"},
|
||||||
{"noversion", "noversion", ""},
|
{"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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.input, func(t *testing.T) {
|
name, ver := splitNameVersion(tt.input)
|
||||||
name, ver := splitNameVersion(tt.input)
|
if name != tt.name || ver != tt.version {
|
||||||
if name != tt.name || ver != tt.version {
|
t.Errorf("splitNameVersion(%q) = (%q, %q), want (%q, %q)",
|
||||||
t.Errorf("splitNameVersion(%q) = (%q, %q), want (%q, %q)",
|
tt.input, name, ver, tt.name, tt.version)
|
||||||
tt.input, name, ver, tt.name, tt.version)
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,111 +46,6 @@ 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) {
|
func TestParseSearch(t *testing.T) {
|
||||||
// verbose output
|
// verbose output
|
||||||
output := `curl-8.5.0-r0 - URL retrieval utility and library
|
output := `curl-8.5.0-r0 - URL retrieval utility and library
|
||||||
@@ -191,51 +76,6 @@ 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) {
|
func TestParseInfo(t *testing.T) {
|
||||||
output := `curl-8.5.0-r0 installed size:
|
output := `curl-8.5.0-r0 installed size:
|
||||||
description: URL retrieval utility and library
|
description: URL retrieval utility and library
|
||||||
@@ -254,84 +94,11 @@ 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) {
|
func TestParseInfoNameVersion(t *testing.T) {
|
||||||
tests := []struct {
|
output := "curl-8.5.0-r0 description:\nsome stuff"
|
||||||
name string
|
name, ver := parseInfoNameVersion(output)
|
||||||
input string
|
if name != "curl" || ver != "8.5.0-r0" {
|
||||||
wantN string
|
t.Errorf("got (%q, %q), want (curl, 8.5.0-r0)", name, ver)
|
||||||
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",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
input: "",
|
|
||||||
wantN: "",
|
|
||||||
wantV: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "whitespace_only",
|
|
||||||
input: " ",
|
|
||||||
wantN: "",
|
|
||||||
wantV: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,151 +112,3 @@ func TestName(t *testing.T) {
|
|||||||
t.Errorf("expected apk, got %q", a.Name())
|
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")
|
|
||||||
}
|
|
||||||
if !caps.PackageUpgrade {
|
|
||||||
t.Error("expected PackageUpgrade=true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
10
apk/parse.go
10
apk/parse.go
@@ -127,12 +127,10 @@ func parseInfo(output string) *snack.Package {
|
|||||||
// The first line is typically "pkgname-version description".
|
// The first line is typically "pkgname-version description".
|
||||||
func parseInfoNameVersion(output string) (string, string) {
|
func parseInfoNameVersion(output string) (string, string) {
|
||||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||||
if len(lines) == 0 || lines[0] == "" {
|
if len(lines) == 0 {
|
||||||
return "", ""
|
return "", ""
|
||||||
}
|
}
|
||||||
fields := strings.Fields(lines[0])
|
// first line: name-version
|
||||||
if len(fields) == 0 {
|
first := strings.Fields(lines[0])[0]
|
||||||
return "", ""
|
return splitNameVersion(first)
|
||||||
}
|
|
||||||
return splitNameVersion(fields[0])
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,50 +67,5 @@ func TestNew(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSupportsDryRun(t *testing.T) {
|
// Verify Apt implements snack.Manager at compile time.
|
||||||
a := New()
|
var _ snack.Manager = (*Apt)(nil)
|
||||||
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},
|
|
||||||
{"PackageUpgrade", caps.PackageUpgrade, 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,11 +23,17 @@ func latestVersion(ctx context.Context, pkg string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
return "", fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
||||||
}
|
}
|
||||||
candidate := parsePolicyCandidate(string(out))
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
if candidate == "" {
|
line = strings.TrimSpace(line)
|
||||||
return "", fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return candidate, nil
|
return "", fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func listUpgrades(ctx context.Context) ([]snack.Package, error) {
|
func listUpgrades(ctx context.Context) ([]snack.Package, error) {
|
||||||
@@ -39,7 +45,38 @@ func listUpgrades(ctx context.Context) ([]snack.Package, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("apt-get --just-print upgrade: %w", err)
|
return nil, fmt.Errorf("apt-get --just-print upgrade: %w", err)
|
||||||
}
|
}
|
||||||
return parseUpgradeSimulation(string(out)), nil
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func upgradeAvailable(ctx context.Context, pkg string) (bool, error) {
|
func upgradeAvailable(ctx context.Context, pkg string) (bool, error) {
|
||||||
@@ -48,12 +85,19 @@ func upgradeAvailable(ctx context.Context, pkg string) (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
return false, fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
||||||
}
|
}
|
||||||
installed := parsePolicyInstalled(string(out))
|
var installed, candidate string
|
||||||
candidate := parsePolicyCandidate(string(out))
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
if installed == "" {
|
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 == "" {
|
||||||
return false, fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotInstalled)
|
return false, fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotInstalled)
|
||||||
}
|
}
|
||||||
if candidate == "" || candidate == installed {
|
if candidate == "(none)" || candidate == "" || candidate == installed {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
@@ -104,7 +148,15 @@ func listHeld(ctx context.Context) ([]snack.Package, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("apt-mark showhold: %w", err)
|
return nil, fmt.Errorf("apt-mark showhold: %w", err)
|
||||||
}
|
}
|
||||||
return parseHoldList(string(out)), nil
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func isHeld(ctx context.Context, pkg string) (bool, error) {
|
func isHeld(ctx context.Context, pkg string) (bool, error) {
|
||||||
@@ -146,7 +198,14 @@ func fileList(ctx context.Context, pkg string) ([]string, error) {
|
|||||||
}
|
}
|
||||||
return nil, fmt.Errorf("dpkg-query -L %s: %w: %s", pkg, err, errMsg)
|
return nil, fmt.Errorf("dpkg-query -L %s: %w: %s", pkg, err, errMsg)
|
||||||
}
|
}
|
||||||
return parseFileList(string(out)), nil
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func owner(ctx context.Context, path string) (string, error) {
|
func owner(ctx context.Context, path string) (string, error) {
|
||||||
@@ -157,11 +216,18 @@ func owner(ctx context.Context, path string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("dpkg -S %s: %w", path, snack.ErrNotFound)
|
return "", fmt.Errorf("dpkg -S %s: %w", path, snack.ErrNotFound)
|
||||||
}
|
}
|
||||||
pkg := parseOwner(string(out))
|
// Output format: "package: /path/to/file" or "package1, package2: /path"
|
||||||
if pkg == "" {
|
line := strings.TrimSpace(strings.Split(string(out), "\n")[0])
|
||||||
|
colonIdx := strings.Index(line, ":")
|
||||||
|
if colonIdx < 0 {
|
||||||
return "", fmt.Errorf("dpkg -S %s: unexpected output", path)
|
return "", fmt.Errorf("dpkg -S %s: unexpected output", path)
|
||||||
}
|
}
|
||||||
return pkg, nil
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- RepoManager ---
|
// --- RepoManager ---
|
||||||
@@ -183,14 +249,51 @@ func listRepos(_ context.Context) ([]snack.Repository, error) {
|
|||||||
}
|
}
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(data))
|
scanner := bufio.NewScanner(bytes.NewReader(data))
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
if r := parseSourcesLine(scanner.Text()); r != nil {
|
line := strings.TrimSpace(scanner.Text())
|
||||||
repos = append(repos, *r)
|
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],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return repos, nil
|
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 {
|
func addRepo(ctx context.Context, repo snack.Repository) error {
|
||||||
repoLine := repo.URL
|
repoLine := repo.URL
|
||||||
|
|||||||
@@ -150,3 +150,50 @@ 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,158 +51,6 @@ func parseSearch(output string) []snack.Package {
|
|||||||
return pkgs
|
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.
|
// parseInfo parses apt-cache show output into a Package.
|
||||||
func parseInfo(output string) (*snack.Package, error) {
|
func parseInfo(output string) (*snack.Package, error) {
|
||||||
p := &snack.Package{}
|
p := &snack.Package{}
|
||||||
|
|||||||
@@ -194,445 +194,3 @@ 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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
141
aur/aur_test.go
141
aur/aur_test.go
@@ -3,9 +3,7 @@ package aur
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gogrlx/snack"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParsePackageList(t *testing.T) {
|
func TestParsePackageList(t *testing.T) {
|
||||||
@@ -65,142 +63,3 @@ func TestNewWithOptions(t *testing.T) {
|
|||||||
assert.Equal(t, "/tmp/aur-builds", a.BuildDir)
|
assert.Equal(t, "/tmp/aur-builds", a.BuildDir)
|
||||||
assert.Equal(t, []string{"--skippgpcheck", "--nocheck"}, a.MakepkgFlags)
|
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},
|
|
||||||
{"PackageUpgrade", caps.PackageUpgrade, true},
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,16 +4,15 @@ package snack
|
|||||||
// Useful for grlx to determine what operations are available before
|
// Useful for grlx to determine what operations are available before
|
||||||
// attempting them.
|
// attempting them.
|
||||||
type Capabilities struct {
|
type Capabilities struct {
|
||||||
VersionQuery bool
|
VersionQuery bool
|
||||||
Hold bool
|
Hold bool
|
||||||
Clean bool
|
Clean bool
|
||||||
FileOwnership bool
|
FileOwnership bool
|
||||||
RepoManagement bool
|
RepoManagement bool
|
||||||
KeyManagement bool
|
KeyManagement bool
|
||||||
Groups bool
|
Groups bool
|
||||||
NameNormalize bool
|
NameNormalize bool
|
||||||
DryRun bool
|
DryRun bool
|
||||||
PackageUpgrade bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCapabilities probes a Manager for all optional interface support.
|
// GetCapabilities probes a Manager for all optional interface support.
|
||||||
@@ -27,17 +26,15 @@ func GetCapabilities(m Manager) Capabilities {
|
|||||||
_, g := m.(Grouper)
|
_, g := m.(Grouper)
|
||||||
_, nn := m.(NameNormalizer)
|
_, nn := m.(NameNormalizer)
|
||||||
_, dr := m.(DryRunner)
|
_, dr := m.(DryRunner)
|
||||||
_, pu := m.(PackageUpgrader)
|
|
||||||
return Capabilities{
|
return Capabilities{
|
||||||
VersionQuery: vq,
|
VersionQuery: vq,
|
||||||
Hold: h,
|
Hold: h,
|
||||||
Clean: c,
|
Clean: c,
|
||||||
FileOwnership: fo,
|
FileOwnership: fo,
|
||||||
RepoManagement: rm,
|
RepoManagement: rm,
|
||||||
KeyManagement: km,
|
KeyManagement: km,
|
||||||
Groups: g,
|
Groups: g,
|
||||||
NameNormalize: nn,
|
NameNormalize: nn,
|
||||||
DryRun: dr,
|
DryRun: dr,
|
||||||
PackageUpgrade: pu,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ func TestGetCapabilities_BaseManager(t *testing.T) {
|
|||||||
assert.False(t, caps.Groups)
|
assert.False(t, caps.Groups)
|
||||||
assert.False(t, caps.NameNormalize)
|
assert.False(t, caps.NameNormalize)
|
||||||
assert.False(t, caps.DryRun)
|
assert.False(t, caps.DryRun)
|
||||||
assert.False(t, caps.PackageUpgrade)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetCapabilities_FullManager(t *testing.T) {
|
func TestGetCapabilities_FullManager(t *testing.T) {
|
||||||
@@ -91,5 +90,4 @@ func TestGetCapabilities_FullManager(t *testing.T) {
|
|||||||
assert.True(t, caps.Groups)
|
assert.True(t, caps.Groups)
|
||||||
assert.True(t, caps.NameNormalize)
|
assert.True(t, caps.NameNormalize)
|
||||||
assert.True(t, caps.DryRun)
|
assert.True(t, caps.DryRun)
|
||||||
assert.True(t, caps.PackageUpgrade)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -387,9 +387,6 @@ func detectCmd() *cobra.Command {
|
|||||||
if caps.NameNormalize {
|
if caps.NameNormalize {
|
||||||
capList = append(capList, "normalize")
|
capList = append(capList, "normalize")
|
||||||
}
|
}
|
||||||
if caps.PackageUpgrade {
|
|
||||||
capList = append(capList, "pkg-upgrade")
|
|
||||||
}
|
|
||||||
capStr := ""
|
capStr := ""
|
||||||
if len(capList) > 0 {
|
if len(capList) > 0 {
|
||||||
capStr = " [" + strings.Join(capList, ", ") + "]"
|
capStr = " [" + strings.Join(capList, ", ") + "]"
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/gogrlx/snack"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTargets(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
ver string
|
|
||||||
want []snack.Target
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no_args",
|
|
||||||
args: nil,
|
|
||||||
want: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single_no_version",
|
|
||||||
args: []string{"curl"},
|
|
||||||
want: []snack.Target{{Name: "curl"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple_no_version",
|
|
||||||
args: []string{"curl", "wget"},
|
|
||||||
want: []snack.Target{{Name: "curl"}, {Name: "wget"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single_with_version",
|
|
||||||
args: []string{"curl"},
|
|
||||||
ver: "7.88",
|
|
||||||
want: []snack.Target{{Name: "curl", Version: "7.88"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple_with_version",
|
|
||||||
args: []string{"curl", "wget"},
|
|
||||||
ver: "1.0",
|
|
||||||
want: []snack.Target{{Name: "curl", Version: "1.0"}, {Name: "wget", Version: "1.0"}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := targets(tt.args, tt.ver)
|
|
||||||
if len(got) != len(tt.want) {
|
|
||||||
t.Fatalf("targets() returned %d, want %d", len(got), len(tt.want))
|
|
||||||
}
|
|
||||||
for i, g := range got {
|
|
||||||
if g.Name != tt.want[i].Name {
|
|
||||||
t.Errorf("[%d] Name = %q, want %q", i, g.Name, tt.want[i].Name)
|
|
||||||
}
|
|
||||||
if g.Version != tt.want[i].Version {
|
|
||||||
t.Errorf("[%d] Version = %q, want %q", i, g.Version, tt.want[i].Version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOpts(t *testing.T) {
|
|
||||||
// Reset flags
|
|
||||||
flagSudo = false
|
|
||||||
flagYes = false
|
|
||||||
flagDry = false
|
|
||||||
|
|
||||||
o := opts()
|
|
||||||
if len(o) != 0 {
|
|
||||||
t.Errorf("expected 0 options with no flags, got %d", len(o))
|
|
||||||
}
|
|
||||||
|
|
||||||
flagSudo = true
|
|
||||||
o = opts()
|
|
||||||
if len(o) != 1 {
|
|
||||||
t.Errorf("expected 1 option with sudo, got %d", len(o))
|
|
||||||
}
|
|
||||||
|
|
||||||
flagYes = true
|
|
||||||
flagDry = true
|
|
||||||
o = opts()
|
|
||||||
if len(o) != 3 {
|
|
||||||
t.Errorf("expected 3 options with all flags, got %d", len(o))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
flagSudo = false
|
|
||||||
flagYes = false
|
|
||||||
flagDry = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOptsApply(t *testing.T) {
|
|
||||||
flagSudo = true
|
|
||||||
flagYes = true
|
|
||||||
flagDry = true
|
|
||||||
defer func() {
|
|
||||||
flagSudo = false
|
|
||||||
flagYes = false
|
|
||||||
flagDry = false
|
|
||||||
}()
|
|
||||||
|
|
||||||
applied := snack.ApplyOptions(opts()...)
|
|
||||||
if !applied.Sudo {
|
|
||||||
t.Error("expected Sudo=true")
|
|
||||||
}
|
|
||||||
if !applied.AssumeYes {
|
|
||||||
t.Error("expected AssumeYes=true")
|
|
||||||
}
|
|
||||||
if !applied.DryRun {
|
|
||||||
t.Error("expected DryRun=true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetManager(t *testing.T) {
|
|
||||||
// Default detection
|
|
||||||
flagMgr = ""
|
|
||||||
m, err := getManager()
|
|
||||||
if err != nil {
|
|
||||||
t.Skipf("no manager available: %v", err)
|
|
||||||
}
|
|
||||||
if m.Name() == "" {
|
|
||||||
t.Error("expected non-empty manager name")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicit override
|
|
||||||
flagMgr = "apt"
|
|
||||||
m, err = getManager()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("getManager() with --manager=apt failed: %v", err)
|
|
||||||
}
|
|
||||||
if m.Name() != "apt" {
|
|
||||||
t.Errorf("expected Name()=apt, got %q", m.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unknown manager
|
|
||||||
flagMgr = "nonexistent-manager-xyz"
|
|
||||||
_, err = getManager()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error for unknown manager")
|
|
||||||
}
|
|
||||||
|
|
||||||
flagMgr = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVersionString(t *testing.T) {
|
|
||||||
if version == "" {
|
|
||||||
t.Error("version should not be empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//go:build !linux && !freebsd && !openbsd
|
|
||||||
|
|
||||||
package detect
|
|
||||||
|
|
||||||
// candidates returns an empty list on unsupported platforms.
|
|
||||||
func candidates() []managerFactory {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// allManagers returns an empty list on unsupported platforms.
|
|
||||||
func allManagers() []managerFactory {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
package detect
|
package detect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gogrlx/snack"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestByNameUnknown(t *testing.T) {
|
func TestByNameUnknown(t *testing.T) {
|
||||||
@@ -12,62 +9,15 @@ func TestByNameUnknown(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for unknown manager")
|
t.Fatal("expected error for unknown manager")
|
||||||
}
|
}
|
||||||
if !errors.Is(err, snack.ErrManagerNotFound) {
|
|
||||||
t.Errorf("expected ErrManagerNotFound, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestByNameKnown(t *testing.T) {
|
|
||||||
// All known manager names should be resolvable by ByName, even if
|
|
||||||
// unavailable on this system.
|
|
||||||
knownNames := []string{"apt", "dnf", "pacman", "apk", "flatpak", "snap", "aur"}
|
|
||||||
for _, name := range knownNames {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
m, err := ByName(name)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ByName(%q) returned error: %v", name, err)
|
|
||||||
}
|
|
||||||
if m.Name() != name {
|
|
||||||
t.Errorf("ByName(%q).Name() = %q", name, m.Name())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestByNameReturnsCorrectType(t *testing.T) {
|
|
||||||
m, err := ByName("apt")
|
|
||||||
if err != nil {
|
|
||||||
t.Skip("apt not in allManagers on this platform")
|
|
||||||
}
|
|
||||||
if m.Name() != "apt" {
|
|
||||||
t.Errorf("expected Name()=apt, got %q", m.Name())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAllReturnsSlice(t *testing.T) {
|
func TestAllReturnsSlice(t *testing.T) {
|
||||||
managers := All()
|
// Just verify it doesn't panic; actual availability depends on system.
|
||||||
// On Linux with apt installed, we should get at least 1
|
_ = All()
|
||||||
// But don't fail if none — could be a weird CI environment
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
for _, m := range managers {
|
|
||||||
name := m.Name()
|
|
||||||
if seen[name] {
|
|
||||||
t.Errorf("duplicate manager in All(): %s", name)
|
|
||||||
}
|
|
||||||
seen[name] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAllManagersAreAvailable(t *testing.T) {
|
|
||||||
for _, m := range All() {
|
|
||||||
if !m.Available() {
|
|
||||||
t.Errorf("All() returned unavailable manager: %s", m.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultDoesNotPanic(t *testing.T) {
|
func TestDefaultDoesNotPanic(t *testing.T) {
|
||||||
Reset()
|
// May return error if no managers available; that's fine.
|
||||||
_, _ = Default()
|
_, _ = Default()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,65 +37,9 @@ func TestDefaultCachesResult(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultReturnsAvailableManager(t *testing.T) {
|
|
||||||
Reset()
|
|
||||||
m, err := Default()
|
|
||||||
if err != nil {
|
|
||||||
t.Skipf("no manager available on this system: %v", err)
|
|
||||||
}
|
|
||||||
if !m.Available() {
|
|
||||||
t.Error("Default() returned unavailable manager")
|
|
||||||
}
|
|
||||||
if m.Name() == "" {
|
|
||||||
t.Error("Default() returned manager with empty name")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResetAllowsRedetection(t *testing.T) {
|
func TestResetAllowsRedetection(t *testing.T) {
|
||||||
_, _ = Default()
|
_, _ = Default()
|
||||||
Reset()
|
Reset()
|
||||||
// After reset, calling Default() again should work.
|
// After reset, defaultOnce should be fresh; calling Default() again should work.
|
||||||
_, _ = Default()
|
_, _ = Default()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResetConcurrent(t *testing.T) {
|
|
||||||
done := make(chan struct{})
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
go func() {
|
|
||||||
defer func() { done <- struct{}{} }()
|
|
||||||
Reset()
|
|
||||||
_, _ = Default()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
<-done
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHasBinary(t *testing.T) {
|
|
||||||
// sh should exist on any Unix system
|
|
||||||
if !HasBinary("sh") {
|
|
||||||
t.Error("expected HasBinary(sh) = true")
|
|
||||||
}
|
|
||||||
if HasBinary("this-binary-does-not-exist-anywhere-12345") {
|
|
||||||
t.Error("expected HasBinary(nonexistent) = false")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCandidatesNotEmpty(t *testing.T) {
|
|
||||||
c := candidates()
|
|
||||||
if len(c) == 0 {
|
|
||||||
t.Error("candidates() returned empty slice")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAllManagersNotEmpty(t *testing.T) {
|
|
||||||
a := allManagers()
|
|
||||||
if len(a) == 0 {
|
|
||||||
t.Error("allManagers() returned empty slice")
|
|
||||||
}
|
|
||||||
// allManagers should be a superset of candidates
|
|
||||||
if len(a) < len(candidates()) {
|
|
||||||
t.Error("allManagers() should include at least all candidates")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
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},
|
|
||||||
{"PackageUpgrade", caps.PackageUpgrade},
|
|
||||||
}
|
|
||||||
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,6 +3,8 @@ package dnf
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gogrlx/snack"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseList(t *testing.T) {
|
func TestParseList(t *testing.T) {
|
||||||
@@ -456,399 +458,15 @@ updates-testing Fedora 43 - x86_64 - Test Updates
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Edge case tests ---
|
// Ensure interface checks from capabilities.go are satisfied.
|
||||||
|
var (
|
||||||
func TestParseListEmpty(t *testing.T) {
|
_ snack.Manager = (*DNF)(nil)
|
||||||
pkgs := parseList("")
|
_ snack.VersionQuerier = (*DNF)(nil)
|
||||||
if len(pkgs) != 0 {
|
_ snack.Holder = (*DNF)(nil)
|
||||||
t.Errorf("expected 0 packages from empty input, got %d", len(pkgs))
|
_ snack.Cleaner = (*DNF)(nil)
|
||||||
}
|
_ snack.FileOwner = (*DNF)(nil)
|
||||||
}
|
_ snack.RepoManager = (*DNF)(nil)
|
||||||
|
_ snack.KeyManager = (*DNF)(nil)
|
||||||
func TestParseListSinglePackage(t *testing.T) {
|
_ snack.Grouper = (*DNF)(nil)
|
||||||
input := `Installed Packages
|
_ snack.NameNormalizer = (*DNF)(nil)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -86,145 +86,5 @@ func TestUpdateUnsupported(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time interface assertions.
|
// Verify Dpkg implements snack.Manager at compile time.
|
||||||
var (
|
var _ snack.Manager = (*Dpkg)(nil)
|
||||||
_ snack.Manager = (*Dpkg)(nil)
|
|
||||||
_ snack.FileOwner = (*Dpkg)(nil)
|
|
||||||
_ snack.NameNormalizer = (*Dpkg)(nil)
|
|
||||||
_ snack.DryRunner = (*Dpkg)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSupportsDryRun(t *testing.T) {
|
|
||||||
d := New()
|
|
||||||
if !d.SupportsDryRun() {
|
|
||||||
t.Error("expected SupportsDryRun() = true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCapabilities(t *testing.T) {
|
|
||||||
caps := snack.GetCapabilities(New())
|
|
||||||
checks := []struct {
|
|
||||||
name string
|
|
||||||
got bool
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{"FileOwnership", caps.FileOwnership, true},
|
|
||||||
{"NameNormalize", caps.NameNormalize, true},
|
|
||||||
{"DryRun", caps.DryRun, true},
|
|
||||||
{"VersionQuery", caps.VersionQuery, false},
|
|
||||||
{"Hold", caps.Hold, false},
|
|
||||||
{"Clean", caps.Clean, false},
|
|
||||||
{"RepoManagement", caps.RepoManagement, false},
|
|
||||||
{"KeyManagement", caps.KeyManagement, false},
|
|
||||||
{"Groups", caps.Groups, false},
|
|
||||||
{"PackageUpgrade", caps.PackageUpgrade, false},
|
|
||||||
}
|
|
||||||
for _, c := range checks {
|
|
||||||
t.Run(c.name, func(t *testing.T) {
|
|
||||||
if c.got != c.want {
|
|
||||||
t.Errorf("%s = %v, want %v", c.name, c.got, c.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeNameMethod(t *testing.T) {
|
|
||||||
d := New()
|
|
||||||
if got := d.NormalizeName("curl:amd64"); got != "curl" {
|
|
||||||
t.Errorf("NormalizeName(curl:amd64) = %q, want %q", got, "curl")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseArchMethod(t *testing.T) {
|
|
||||||
d := New()
|
|
||||||
name, arch := d.ParseArch("bash:arm64")
|
|
||||||
if name != "bash" || arch != "arm64" {
|
|
||||||
t.Errorf("ParseArch(bash:arm64) = (%q, %q), want (bash, arm64)", name, arch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseListEdgeCases(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
want int
|
|
||||||
}{
|
|
||||||
{"empty", "", 0},
|
|
||||||
{"whitespace_only", " \n \n ", 0},
|
|
||||||
{"single_installed", "bash\t5.2-1\tinstall ok installed", 1},
|
|
||||||
{"no_status_field", "bash\t5.2-1", 1},
|
|
||||||
{"blank_lines_mixed", "\nbash\t5.2-1\tinstall ok installed\n\ncurl\t7.88\tinstall ok installed\n", 2},
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDpkgListEdgeCases(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
want int
|
|
||||||
}{
|
|
||||||
{"empty", "", 0},
|
|
||||||
{"header_only", "Desired=Unknown/Install/Remove/Purge/Hold\n||/ Name Version Architecture Description\n+++-====-====-====-====", 0},
|
|
||||||
{"single_package", "ii bash 5.2-1 amd64 GNU Bourne Again SHell", 1},
|
|
||||||
{"held_package", "hi nginx 1.24 amd64 web server", 1},
|
|
||||||
{"purge_pending", "pn oldpkg 1.0 amd64 old package", 1},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
pkgs := parseDpkgList(tt.input)
|
|
||||||
if len(pkgs) != tt.want {
|
|
||||||
t.Errorf("parseDpkgList() returned %d packages, want %d", len(pkgs), tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseInfoEdgeCases(t *testing.T) {
|
|
||||||
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("all_fields", func(t *testing.T) {
|
|
||||||
input := "Package: vim\nStatus: install ok installed\nVersion: 9.0\nArchitecture: arm64\nDescription: Vi IMproved\n"
|
|
||||||
p, err := parseInfo(input)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if p.Name != "vim" || p.Version != "9.0" || p.Arch != "arm64" || !p.Installed {
|
|
||||||
t.Errorf("unexpected: %+v", p)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("version_with_epoch", func(t *testing.T) {
|
|
||||||
input := "Package: systemd\nVersion: 1:252-2\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")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("not_installed_status", func(t *testing.T) {
|
|
||||||
input := "Package: curl\nStatus: deinstall ok config-files\nVersion: 7.88\n"
|
|
||||||
p, err := parseInfo(input)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if p.Installed {
|
|
||||||
t.Error("expected Installed=false for deinstall status")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
package dpkg
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestNormalizeName(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{"curl", "curl"},
|
|
||||||
{"curl:amd64", "curl"},
|
|
||||||
{"bash:arm64", "bash"},
|
|
||||||
{"python3:i386", "python3"},
|
|
||||||
{"libc6:armhf", "libc6"},
|
|
||||||
{"pkg:armel", "pkg"},
|
|
||||||
{"pkg:mips", "pkg"},
|
|
||||||
{"pkg:mipsel", "pkg"},
|
|
||||||
{"pkg:mips64el", "pkg"},
|
|
||||||
{"pkg:ppc64el", "pkg"},
|
|
||||||
{"pkg:s390x", "pkg"},
|
|
||||||
{"pkg:all", "pkg"},
|
|
||||||
{"pkg:any", "pkg"},
|
|
||||||
// Unknown arch suffix should be kept
|
|
||||||
{"pkg:unknown", "pkg:unknown"},
|
|
||||||
{"libstdc++6:amd64", "libstdc++6"},
|
|
||||||
{"", ""},
|
|
||||||
// Multiple colons — only last one checked
|
|
||||||
{"a:b:amd64", "a:b"},
|
|
||||||
}
|
|
||||||
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 TestParseArch(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
wantName string
|
|
||||||
wantArch string
|
|
||||||
}{
|
|
||||||
{"curl:amd64", "curl", "amd64"},
|
|
||||||
{"bash:arm64", "bash", "arm64"},
|
|
||||||
{"python3", "python3", ""},
|
|
||||||
{"curl:i386", "curl", "i386"},
|
|
||||||
{"pkg:armhf", "pkg", "armhf"},
|
|
||||||
{"pkg:armel", "pkg", "armel"},
|
|
||||||
{"pkg:mips", "pkg", "mips"},
|
|
||||||
{"pkg:mipsel", "pkg", "mipsel"},
|
|
||||||
{"pkg:mips64el", "pkg", "mips64el"},
|
|
||||||
{"pkg:ppc64el", "pkg", "ppc64el"},
|
|
||||||
{"pkg:s390x", "pkg", "s390x"},
|
|
||||||
{"pkg:all", "pkg", "all"},
|
|
||||||
{"pkg:any", "pkg", "any"},
|
|
||||||
// Unknown arch — not split
|
|
||||||
{"pkg:foobar", "pkg:foobar", ""},
|
|
||||||
{"", "", ""},
|
|
||||||
// Multiple colons
|
|
||||||
{"a:b:arm64", "a:b", "arm64"},
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,71 +37,6 @@ 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) {
|
func TestParseSearch(t *testing.T) {
|
||||||
input := "Firefox\torg.mozilla.Firefox\t131.0\tflathub\n" +
|
input := "Firefox\torg.mozilla.Firefox\t131.0\tflathub\n" +
|
||||||
"Firefox ESR\torg.mozilla.FirefoxESR\t128.3\tflathub\n"
|
"Firefox ESR\torg.mozilla.FirefoxESR\t128.3\tflathub\n"
|
||||||
@@ -125,32 +60,6 @@ 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) {
|
func TestParseInfo(t *testing.T) {
|
||||||
input := `Name: Firefox
|
input := `Name: Firefox
|
||||||
Description: Fast, private web browser
|
Description: Fast, private web browser
|
||||||
@@ -183,46 +92,6 @@ 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) {
|
func TestParseRemotes(t *testing.T) {
|
||||||
input := "flathub\thttps://dl.flathub.org/repo/\t\n" +
|
input := "flathub\thttps://dl.flathub.org/repo/\t\n" +
|
||||||
"gnome-nightly\thttps://nightly.gnome.org/repo/\tdisabled\n"
|
"gnome-nightly\thttps://nightly.gnome.org/repo/\tdisabled\n"
|
||||||
@@ -244,165 +113,10 @@ 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) {
|
func TestInterfaceCompliance(t *testing.T) {
|
||||||
var _ snack.Manager = (*Flatpak)(nil)
|
var _ snack.Manager = (*Flatpak)(nil)
|
||||||
var _ snack.Cleaner = (*Flatpak)(nil)
|
var _ snack.Cleaner = (*Flatpak)(nil)
|
||||||
var _ snack.RepoManager = (*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")
|
|
||||||
}
|
|
||||||
if !caps.PackageUpgrade {
|
|
||||||
t.Error("expected PackageUpgrade=true")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestName(t *testing.T) {
|
func TestName(t *testing.T) {
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -27,7 +27,7 @@ require (
|
|||||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -43,8 +43,8 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
|
|||||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
|||||||
@@ -79,88 +79,36 @@ func TestBuildArgs_RootBeforeBaseArgs(t *testing.T) {
|
|||||||
assert.Greater(t, sIdx, rIdx, "root flag should come before base args")
|
assert.Greater(t, sIdx, rIdx, "root flag should come before base args")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseUpgrades(t *testing.T) {
|
func TestParseUpgrades_Empty(t *testing.T) {
|
||||||
tests := []struct {
|
assert.Empty(t, parseUpgrades(""))
|
||||||
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"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
func TestParseUpgrades_Standard(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
input := `linux 6.7.3.arch1-1 -> 6.7.4.arch1-1
|
||||||
pkgs := parseUpgrades(tt.input)
|
vim 9.0.2-1 -> 9.1.0-1
|
||||||
require.Len(t, pkgs, tt.wantLen)
|
`
|
||||||
for i, p := range pkgs {
|
pkgs := parseUpgrades(input)
|
||||||
assert.True(t, p.Installed, "all upgrade entries should have Installed=true")
|
require.Len(t, pkgs, 2)
|
||||||
if i < len(tt.wantNames) {
|
assert.Equal(t, "linux", pkgs[0].Name)
|
||||||
assert.Equal(t, tt.wantNames[i], p.Name)
|
assert.Equal(t, "6.7.4.arch1-1", pkgs[0].Version)
|
||||||
}
|
assert.True(t, pkgs[0].Installed)
|
||||||
if i < len(tt.wantVers) {
|
assert.Equal(t, "vim", pkgs[1].Name)
|
||||||
assert.Equal(t, tt.wantVers[i], p.Version)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseGroupPkgSet_Empty(t *testing.T) {
|
func TestParseGroupPkgSet_Empty(t *testing.T) {
|
||||||
@@ -201,31 +149,6 @@ group pkg2
|
|||||||
assert.Len(t, set, 2)
|
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) {
|
func TestNew(t *testing.T) {
|
||||||
p := New()
|
p := New()
|
||||||
assert.NotNil(t, p)
|
assert.NotNil(t, p)
|
||||||
|
|||||||
@@ -130,59 +130,6 @@ func TestBuildArgs(t *testing.T) {
|
|||||||
|
|
||||||
func TestInterfaceCompliance(t *testing.T) {
|
func TestInterfaceCompliance(t *testing.T) {
|
||||||
var _ snack.Manager = (*Pacman)(nil)
|
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},
|
|
||||||
{"PackageUpgrade", caps.PackageUpgrade, true},
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
func TestName(t *testing.T) {
|
||||||
|
|||||||
530
pkg/pkg_test.go
530
pkg/pkg_test.go
@@ -23,94 +23,6 @@ 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) {
|
func TestParseSearch(t *testing.T) {
|
||||||
input := `nginx-1.24.0 Robust and small WWW server
|
input := `nginx-1.24.0 Robust and small WWW server
|
||||||
curl-8.5.0 Command line tool for transferring data
|
curl-8.5.0 Command line tool for transferring data
|
||||||
@@ -127,81 +39,6 @@ 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) {
|
func TestParseInfo(t *testing.T) {
|
||||||
input := `Name : nginx
|
input := `Name : nginx
|
||||||
Version : 1.24.0
|
Version : 1.24.0
|
||||||
@@ -226,77 +63,6 @@ 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) {
|
func TestParseUpgrades(t *testing.T) {
|
||||||
input := `Updating FreeBSD repository catalogue...
|
input := `Updating FreeBSD repository catalogue...
|
||||||
The following 2 package(s) will be affected:
|
The following 2 package(s) will be affected:
|
||||||
@@ -318,90 +84,6 @@ 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) {
|
func TestParseFileList(t *testing.T) {
|
||||||
input := `nginx-1.24.0:
|
input := `nginx-1.24.0:
|
||||||
/usr/local/sbin/nginx
|
/usr/local/sbin/nginx
|
||||||
@@ -417,67 +99,6 @@ 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) {
|
func TestParseOwner(t *testing.T) {
|
||||||
input := "/usr/local/sbin/nginx was installed by package nginx-1.24.0\n"
|
input := "/usr/local/sbin/nginx was installed by package nginx-1.24.0\n"
|
||||||
name := parseOwner(input)
|
name := parseOwner(input)
|
||||||
@@ -486,48 +107,6 @@ 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) {
|
func TestSplitNameVersion(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input string
|
input string
|
||||||
@@ -547,73 +126,6 @@ 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) {
|
func TestInterfaceCompliance(t *testing.T) {
|
||||||
var _ snack.Manager = (*Pkg)(nil)
|
var _ snack.Manager = (*Pkg)(nil)
|
||||||
var _ snack.VersionQuerier = (*Pkg)(nil)
|
var _ snack.VersionQuerier = (*Pkg)(nil)
|
||||||
@@ -621,51 +133,9 @@ func TestInterfaceCompliance(t *testing.T) {
|
|||||||
var _ snack.FileOwner = (*Pkg)(nil)
|
var _ snack.FileOwner = (*Pkg)(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPackageUpgraderInterface(t *testing.T) {
|
|
||||||
var _ snack.PackageUpgrader = (*Pkg)(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestName(t *testing.T) {
|
func TestName(t *testing.T) {
|
||||||
p := New()
|
p := New()
|
||||||
if p.Name() != "pkg" {
|
if p.Name() != "pkg" {
|
||||||
t.Errorf("Name() = %q, want %q", 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -29,93 +29,6 @@ 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) {
|
func TestParseSearchResults(t *testing.T) {
|
||||||
input := `nginx-1.24.0
|
input := `nginx-1.24.0
|
||||||
nginx-1.25.3
|
nginx-1.25.3
|
||||||
@@ -129,83 +42,6 @@ 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) {
|
func TestParseInfoOutput(t *testing.T) {
|
||||||
input := `Information for nginx-1.24.0:
|
input := `Information for nginx-1.24.0:
|
||||||
|
|
||||||
@@ -246,131 +82,6 @@ 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) {
|
func TestSplitNameVersion(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input string
|
input string
|
||||||
@@ -390,76 +101,57 @@ func TestSplitNameVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSplitNameVersionEdgeCases(t *testing.T) {
|
func TestParseListEmpty(t *testing.T) {
|
||||||
tests := []struct {
|
pkgs := parseList("")
|
||||||
name string
|
if len(pkgs) != 0 {
|
||||||
input string
|
t.Fatalf("expected 0 packages, got %d", len(pkgs))
|
||||||
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",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
}
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
name, ver := splitNameVersion(tt.input)
|
func TestParseListWhitespaceOnly(t *testing.T) {
|
||||||
if name != tt.wantName || ver != tt.wantVersion {
|
pkgs := parseList(" \n \n\n")
|
||||||
t.Errorf("splitNameVersion(%q) = (%q, %q), want (%q, %q)",
|
if len(pkgs) != 0 {
|
||||||
tt.input, name, ver, tt.wantName, tt.wantVersion)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,7 +163,14 @@ func TestSplitNameVersionNoHyphen(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSplitNameVersionLeadingHyphen(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")
|
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 != "" {
|
if name != "-1.0" || ver != "" {
|
||||||
t.Errorf("splitNameVersion(\"-1.0\") = (%q, %q), want (\"-1.0\", \"\")", name, ver)
|
t.Errorf("splitNameVersion(\"-1.0\") = (%q, %q), want (\"-1.0\", \"\")", name, ver)
|
||||||
}
|
}
|
||||||
@@ -497,91 +196,6 @@ 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) {
|
func TestParseUpgradeOutputEmpty(t *testing.T) {
|
||||||
pkgs := parseUpgradeOutput("")
|
pkgs := parseUpgradeOutput("")
|
||||||
if len(pkgs) != 0 {
|
if len(pkgs) != 0 {
|
||||||
@@ -607,73 +221,6 @@ 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) {
|
func TestParseFileListOutputEmpty(t *testing.T) {
|
||||||
files := parseFileListOutput("")
|
files := parseFileListOutput("")
|
||||||
if len(files) != 0 {
|
if len(files) != 0 {
|
||||||
@@ -698,58 +245,6 @@ 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) {
|
func TestInterfaceCompliance(t *testing.T) {
|
||||||
var _ snack.Manager = (*Ports)(nil)
|
var _ snack.Manager = (*Ports)(nil)
|
||||||
var _ snack.VersionQuerier = (*Ports)(nil)
|
var _ snack.VersionQuerier = (*Ports)(nil)
|
||||||
@@ -758,51 +253,9 @@ func TestInterfaceCompliance(t *testing.T) {
|
|||||||
var _ snack.PackageUpgrader = (*Ports)(nil)
|
var _ snack.PackageUpgrader = (*Ports)(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPackageUpgraderInterface(t *testing.T) {
|
|
||||||
var _ snack.PackageUpgrader = (*Ports)(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestName(t *testing.T) {
|
func TestName(t *testing.T) {
|
||||||
p := New()
|
p := New()
|
||||||
if p.Name() != "ports" {
|
if p.Name() != "ports" {
|
||||||
t.Errorf("Name() = %q, want %q", 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")
|
|
||||||
}
|
|
||||||
if !caps.PackageUpgrade {
|
|
||||||
t.Error("expected PackageUpgrade=true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package rpm
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gogrlx/snack"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseList(t *testing.T) {
|
func TestParseList(t *testing.T) {
|
||||||
@@ -93,128 +95,9 @@ func TestParseArchSuffix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Edge case tests ---
|
// Compile-time interface checks.
|
||||||
|
var (
|
||||||
func TestParseListEmpty(t *testing.T) {
|
_ snack.Manager = (*RPM)(nil)
|
||||||
pkgs := parseList("")
|
_ snack.FileOwner = (*RPM)(nil)
|
||||||
if len(pkgs) != 0 {
|
_ snack.NameNormalizer = (*RPM)(nil)
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
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,
|
|
||||||
"PackageUpgrade": caps.PackageUpgrade,
|
|
||||||
}
|
|
||||||
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,56 +35,6 @@ 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) {
|
func TestParseSnapFind(t *testing.T) {
|
||||||
input := `Name Version Publisher Notes Summary
|
input := `Name Version Publisher Notes Summary
|
||||||
firefox 131.0 mozillaâś“ - Mozilla Firefox web browser
|
firefox 131.0 mozillaâś“ - Mozilla Firefox web browser
|
||||||
@@ -102,67 +52,6 @@ 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) {
|
func TestParseSnapInfo(t *testing.T) {
|
||||||
input := `name: firefox
|
input := `name: firefox
|
||||||
summary: Mozilla Firefox web browser
|
summary: Mozilla Firefox web browser
|
||||||
@@ -195,57 +84,6 @@ 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) {
|
func TestParseSnapInfoVersion(t *testing.T) {
|
||||||
input := `name: firefox
|
input := `name: firefox
|
||||||
channels:
|
channels:
|
||||||
@@ -267,39 +105,6 @@ 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) {
|
func TestParseSnapRefreshList(t *testing.T) {
|
||||||
input := `Name Version Rev Publisher Notes
|
input := `Name Version Rev Publisher Notes
|
||||||
firefox 132.0 4650 mozillaâś“ -
|
firefox 132.0 4650 mozillaâś“ -
|
||||||
@@ -323,130 +128,30 @@ 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) {
|
func TestSemverCmp(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
|
||||||
a, b string
|
a, b string
|
||||||
want int
|
want int
|
||||||
}{
|
}{
|
||||||
{"equal", "1.0.0", "1.0.0", 0},
|
{"1.0.0", "1.0.0", 0},
|
||||||
{"less major", "1.0.0", "2.0.0", -1},
|
{"1.0.0", "2.0.0", -1},
|
||||||
{"greater major", "2.0.0", "1.0.0", 1},
|
{"2.0.0", "1.0.0", 1},
|
||||||
{"less patch", "1.2.3", "1.2.4", -1},
|
{"1.2.3", "1.2.4", -1},
|
||||||
{"multi-digit minor", "1.10.0", "1.9.0", 1},
|
{"1.10.0", "1.9.0", 1},
|
||||||
{"short vs long equal", "1.0", "1.0.0", 0},
|
{"1.0", "1.0.0", 0},
|
||||||
{"real versions", "131.0", "132.0", -1},
|
{"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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
got := semverCmp(tt.a, tt.b)
|
||||||
got := semverCmp(tt.a, tt.b)
|
if got != tt.want {
|
||||||
if got != tt.want {
|
t.Errorf("semverCmp(%q, %q) = %d, want %d", tt.a, tt.b, 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) {
|
func TestInterfaceCompliance(t *testing.T) {
|
||||||
var _ snack.Manager = (*Snap)(nil)
|
var _ snack.Manager = (*Snap)(nil)
|
||||||
var _ snack.VersionQuerier = (*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")
|
|
||||||
}
|
|
||||||
if !caps.PackageUpgrade {
|
|
||||||
t.Error("expected PackageUpgrade=true")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestName(t *testing.T) {
|
func TestName(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user