mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-02 05:08:42 -07:00
Merge pull request #20 from gogrlx/cd/exhaustive-containertests
test: exhaustive integration tests + codecov
This commit is contained in:
77
.github/workflows/integration.yml
vendored
77
.github/workflows/integration.yml
vendored
@@ -16,7 +16,11 @@ jobs:
|
|||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
- run: go build ./...
|
- run: go build ./...
|
||||||
- run: go vet ./...
|
- run: go vet ./...
|
||||||
- run: go test -race ./...
|
- run: go test -race -coverprofile=coverage-unit.out ./...
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: coverage-unit
|
||||||
|
path: coverage-unit.out
|
||||||
|
|
||||||
debian:
|
debian:
|
||||||
name: Debian (apt)
|
name: Debian (apt)
|
||||||
@@ -32,7 +36,12 @@ jobs:
|
|||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y sudo tree curl
|
apt-get install -y sudo tree curl
|
||||||
- name: Integration tests
|
- name: Integration tests
|
||||||
run: go test -v -tags integration -count=1 ./apt/ ./dpkg/ ./detect/
|
run: go test -v -tags integration -count=1 -coverprofile=coverage-debian.out ./apt/ ./dpkg/ ./detect/
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: coverage-debian
|
||||||
|
path: coverage-debian.out
|
||||||
|
|
||||||
ubuntu:
|
ubuntu:
|
||||||
name: Ubuntu (apt + snap)
|
name: Ubuntu (apt + snap)
|
||||||
@@ -48,9 +57,14 @@ jobs:
|
|||||||
sudo apt-get install -y tree
|
sudo apt-get install -y tree
|
||||||
sudo snap install hello-world 2>/dev/null; sudo snap remove hello-world 2>/dev/null
|
sudo snap install hello-world 2>/dev/null; sudo snap remove hello-world 2>/dev/null
|
||||||
- name: Integration tests (apt)
|
- name: Integration tests (apt)
|
||||||
run: sudo -E go test -v -tags integration -count=1 ./apt/ ./dpkg/ ./detect/
|
run: sudo -E go test -v -tags integration -count=1 -coverprofile=coverage-ubuntu-apt.out ./apt/ ./dpkg/ ./detect/
|
||||||
- name: Integration tests (snap)
|
- name: Integration tests (snap)
|
||||||
run: sudo -E go test -v -tags integration -count=1 ./snap/
|
run: sudo -E go test -v -tags integration -count=1 -coverprofile=coverage-ubuntu-snap.out ./snap/
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: coverage-ubuntu
|
||||||
|
path: coverage-ubuntu-*.out
|
||||||
|
|
||||||
fedora-dnf4:
|
fedora-dnf4:
|
||||||
name: Fedora 39 (dnf4)
|
name: Fedora 39 (dnf4)
|
||||||
@@ -65,7 +79,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
dnf install -y tree sudo
|
dnf install -y tree sudo
|
||||||
- name: Integration tests
|
- name: Integration tests
|
||||||
run: go test -v -tags integration -count=1 ./dnf/ ./rpm/ ./detect/
|
run: go test -v -tags integration -count=1 -coverprofile=coverage-fedora39.out ./dnf/ ./rpm/ ./detect/
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: coverage-fedora39
|
||||||
|
path: coverage-fedora39.out
|
||||||
|
|
||||||
fedora-dnf5:
|
fedora-dnf5:
|
||||||
name: Fedora latest (dnf5)
|
name: Fedora latest (dnf5)
|
||||||
@@ -80,7 +99,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
dnf install -y tree sudo
|
dnf install -y tree sudo
|
||||||
- name: Integration tests
|
- name: Integration tests
|
||||||
run: go test -v -tags integration -count=1 ./dnf/ ./rpm/ ./detect/
|
run: go test -v -tags integration -count=1 -coverprofile=coverage-fedora-latest.out ./dnf/ ./rpm/ ./detect/
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: coverage-fedora-latest
|
||||||
|
path: coverage-fedora-latest.out
|
||||||
|
|
||||||
alpine:
|
alpine:
|
||||||
name: Alpine (apk)
|
name: Alpine (apk)
|
||||||
@@ -95,7 +119,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
apk add --no-cache sudo tree bash
|
apk add --no-cache sudo tree bash
|
||||||
- name: Integration tests
|
- name: Integration tests
|
||||||
run: go test -v -tags integration -count=1 ./apk/ ./detect/
|
run: go test -v -tags integration -count=1 -coverprofile=coverage-alpine.out ./apk/ ./detect/
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: coverage-alpine
|
||||||
|
path: coverage-alpine.out
|
||||||
|
|
||||||
arch:
|
arch:
|
||||||
name: Arch Linux (pacman)
|
name: Arch Linux (pacman)
|
||||||
@@ -111,7 +140,12 @@ jobs:
|
|||||||
pacman -Syu --noconfirm
|
pacman -Syu --noconfirm
|
||||||
pacman -S --noconfirm sudo tree
|
pacman -S --noconfirm sudo tree
|
||||||
- name: Integration tests
|
- name: Integration tests
|
||||||
run: go test -v -tags integration -count=1 ./pacman/ ./detect/
|
run: go test -v -tags integration -count=1 -coverprofile=coverage-arch.out ./pacman/ ./detect/
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: coverage-arch
|
||||||
|
path: coverage-arch.out
|
||||||
|
|
||||||
flatpak:
|
flatpak:
|
||||||
name: Ubuntu + Flatpak
|
name: Ubuntu + Flatpak
|
||||||
@@ -130,4 +164,29 @@ jobs:
|
|||||||
sudo apt-get install -y flatpak
|
sudo apt-get install -y flatpak
|
||||||
sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||||
- name: Integration tests
|
- name: Integration tests
|
||||||
run: sudo -E go test -v -tags integration -count=1 ./flatpak/
|
run: sudo -E go test -v -tags integration -count=1 -coverprofile=coverage-flatpak.out ./flatpak/
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: coverage-flatpak
|
||||||
|
path: coverage-flatpak.out
|
||||||
|
|
||||||
|
codecov:
|
||||||
|
name: Upload Coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [unit-tests, debian, ubuntu, fedora-dnf4, fedora-dnf5, alpine, arch, flatpak]
|
||||||
|
if: always()
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: coverage-*
|
||||||
|
merge-multiple: true
|
||||||
|
- name: List coverage files
|
||||||
|
run: ls -la coverage-*.out 2>/dev/null || echo "No coverage files found"
|
||||||
|
- uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
files: coverage-unit.out,coverage-debian.out,coverage-ubuntu-apt.out,coverage-ubuntu-snap.out,coverage-fedora39.out,coverage-fedora-latest.out,coverage-alpine.out,coverage-arch.out,coverage-flatpak.out
|
||||||
|
fail_ci_if_error: false
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
coverage.out
|
||||||
|
coverage.html
|
||||||
@@ -19,89 +19,245 @@ func TestIntegration_Apk(t *testing.T) {
|
|||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
assert.Equal(t, "apk", mgr.Name())
|
||||||
|
|
||||||
|
caps := snack.GetCapabilities(mgr)
|
||||||
|
assert.True(t, caps.VersionQuery, "apk should support VersionQuery")
|
||||||
|
assert.True(t, caps.Clean, "apk should support Clean")
|
||||||
|
assert.True(t, caps.FileOwnership, "apk should support FileOwnership")
|
||||||
|
assert.False(t, caps.Hold, "apk should not support Hold")
|
||||||
|
assert.False(t, caps.RepoManagement, "apk should not support RepoManagement")
|
||||||
|
assert.False(t, caps.KeyManagement, "apk should not support KeyManagement")
|
||||||
|
assert.False(t, caps.Groups, "apk should not support Groups")
|
||||||
|
assert.False(t, caps.NameNormalize, "apk should not support NameNormalize")
|
||||||
|
|
||||||
t.Run("Update", func(t *testing.T) {
|
t.Run("Update", func(t *testing.T) {
|
||||||
err := mgr.Update(ctx)
|
require.NoError(t, mgr.Update(ctx))
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Search", func(t *testing.T) {
|
t.Run("Search", func(t *testing.T) {
|
||||||
pkgs, err := mgr.Search(ctx, "curl")
|
pkgs, err := mgr.Search(ctx, "curl")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, pkgs)
|
require.NotEmpty(t, pkgs, "search for curl should return results")
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, p := range pkgs {
|
||||||
|
if p.Name == "curl" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "curl should appear in search results")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Info", func(t *testing.T) {
|
t.Run("Search_NoResults", func(t *testing.T) {
|
||||||
// apk info only works on installed packages, use "tree" after install
|
pkgs, err := mgr.Search(ctx, "xyznonexistentpackage999")
|
||||||
// or test with a pre-installed package like "busybox"
|
// apk search may return error or empty results for no matches
|
||||||
|
if err == nil {
|
||||||
|
assert.Empty(t, pkgs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Info_PreInstalled", func(t *testing.T) {
|
||||||
pkg, err := mgr.Info(ctx, "busybox")
|
pkg, err := mgr.Info(ctx, "busybox")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skip("busybox not installed, skipping Info test")
|
t.Skip("busybox not installed")
|
||||||
}
|
}
|
||||||
require.NotNil(t, pkg)
|
require.NotNil(t, pkg)
|
||||||
|
assert.Equal(t, "busybox", pkg.Name)
|
||||||
|
assert.NotEmpty(t, pkg.Version)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Install", func(t *testing.T) {
|
t.Run("Info_NotFound", func(t *testing.T) {
|
||||||
err := mgr.Install(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
_, err := mgr.Info(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Install_Single", func(t *testing.T) {
|
||||||
|
_ = mgr.Remove(ctx, snack.Targets("tree"))
|
||||||
|
err := mgr.Install(ctx, snack.Targets("tree"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IsInstalled", func(t *testing.T) {
|
t.Run("IsInstalled_True", func(t *testing.T) {
|
||||||
installed, err := mgr.IsInstalled(ctx, "tree")
|
installed, err := mgr.IsInstalled(ctx, "tree")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, installed)
|
assert.True(t, installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Version", func(t *testing.T) {
|
t.Run("IsInstalled_False", func(t *testing.T) {
|
||||||
|
installed, err := mgr.IsInstalled(ctx, "xyznonexistentpackage999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, installed)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Version_Installed", func(t *testing.T) {
|
||||||
ver, err := mgr.Version(ctx, "tree")
|
ver, err := mgr.Version(ctx, "tree")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, ver)
|
assert.NotEmpty(t, ver)
|
||||||
|
t.Logf("tree version: %s", ver)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("List", func(t *testing.T) {
|
t.Run("Version_NotInstalled", func(t *testing.T) {
|
||||||
|
_, err := mgr.Version(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("List_ContainsInstalled", func(t *testing.T) {
|
||||||
pkgs, err := mgr.List(ctx)
|
pkgs, err := mgr.List(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, pkgs)
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
for _, p := range pkgs {
|
for _, p := range pkgs {
|
||||||
if p.Name == "tree" {
|
if p.Name == "tree" {
|
||||||
found = true
|
found = true
|
||||||
|
assert.NotEmpty(t, p.Version)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.True(t, found, "tree should be in installed list")
|
assert.True(t, found, "tree should be in installed list")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Remove", func(t *testing.T) {
|
t.Run("Info_AfterInstall", func(t *testing.T) {
|
||||||
err := mgr.Remove(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
pkg, err := mgr.Info(ctx, "tree")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, pkg)
|
||||||
|
assert.Equal(t, "tree", pkg.Name)
|
||||||
|
assert.NotEmpty(t, pkg.Version)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IsInstalled_After_Remove", func(t *testing.T) {
|
t.Run("Install_Multiple", func(t *testing.T) {
|
||||||
|
err := mgr.Install(ctx, snack.Targets("tree", "less"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, pkg := range []string{"tree", "less"} {
|
||||||
|
installed, err := mgr.IsInstalled(ctx, pkg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, installed, "%s should be installed", pkg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Purge", func(t *testing.T) {
|
||||||
|
// apk purge is same as remove
|
||||||
|
err := mgr.Purge(ctx, snack.Targets("less"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
installed, err := mgr.IsInstalled(ctx, "less")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, installed)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Remove", func(t *testing.T) {
|
||||||
|
err := mgr.Remove(ctx, snack.Targets("tree"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
installed, err := mgr.IsInstalled(ctx, "tree")
|
installed, err := mgr.IsInstalled(ctx, "tree")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, installed)
|
assert.False(t, installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Capabilities", func(t *testing.T) {
|
// --- VersionQuerier ---
|
||||||
if vq, ok := mgr.(snack.VersionQuerier); ok {
|
t.Run("VersionQuerier", func(t *testing.T) {
|
||||||
ver, err := vq.LatestVersion(ctx, "busybox")
|
vq, ok := mgr.(snack.VersionQuerier)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
t.Run("LatestVersion", func(t *testing.T) {
|
||||||
|
ver, err := vq.LatestVersion(ctx, "curl")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, ver)
|
assert.NotEmpty(t, ver)
|
||||||
|
t.Logf("curl latest: %s", ver)
|
||||||
|
})
|
||||||
|
|
||||||
upgrades, err := vq.ListUpgrades(ctx)
|
t.Run("LatestVersion_NotFound", func(t *testing.T) {
|
||||||
|
_, err := vq.LatestVersion(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ListUpgrades", func(t *testing.T) {
|
||||||
|
pkgs, err := vq.ListUpgrades(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_ = upgrades
|
t.Logf("upgradable packages: %d", len(pkgs))
|
||||||
}
|
})
|
||||||
|
|
||||||
if cl, ok := mgr.(snack.Cleaner); ok {
|
t.Run("UpgradeAvailable", func(t *testing.T) {
|
||||||
err := cl.Clean(ctx)
|
avail, err := vq.UpgradeAvailable(ctx, "busybox")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
_ = avail
|
||||||
|
})
|
||||||
|
|
||||||
if fo, ok := mgr.(snack.FileOwner); ok {
|
t.Run("VersionCmp", func(t *testing.T) {
|
||||||
owner, err := fo.Owner(ctx, "/usr/bin/apk")
|
tests := []struct {
|
||||||
if err == nil {
|
v1, v2 string
|
||||||
assert.NotEmpty(t, owner)
|
want int
|
||||||
|
}{
|
||||||
|
{"1.0.0-r0", "1.0.0-r1", -1},
|
||||||
|
{"2.0.0-r0", "1.0.0-r0", 1},
|
||||||
|
{"1.0.0-r0", "1.0.0-r0", 0},
|
||||||
}
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
cmp, err := vq.VersionCmp(ctx, tt.v1, tt.v2)
|
||||||
|
require.NoError(t, err, "VersionCmp(%s, %s)", tt.v1, tt.v2)
|
||||||
|
assert.Equal(t, tt.want, cmp, "VersionCmp(%s, %s)", tt.v1, tt.v2)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Cleaner ---
|
||||||
|
t.Run("Cleaner", func(t *testing.T) {
|
||||||
|
cl, ok := mgr.(snack.Cleaner)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
t.Run("Autoremove", func(t *testing.T) {
|
||||||
|
// apk doesn't have autoremove but we exercise the path
|
||||||
|
err := cl.Autoremove(ctx)
|
||||||
|
// May or may not be supported
|
||||||
|
_ = err
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Clean", func(t *testing.T) {
|
||||||
|
err := cl.Clean(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- FileOwner ---
|
||||||
|
t.Run("FileOwner", func(t *testing.T) {
|
||||||
|
fo, ok := mgr.(snack.FileOwner)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
// Install tree for file tests
|
||||||
|
_ = mgr.Install(ctx, snack.Targets("tree"))
|
||||||
|
|
||||||
|
t.Run("FileList", func(t *testing.T) {
|
||||||
|
files, err := fo.FileList(ctx, "tree")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, files)
|
||||||
|
t.Logf("tree files: %d", len(files))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FileList_NotInstalled", func(t *testing.T) {
|
||||||
|
_, err := fo.FileList(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Owner", func(t *testing.T) {
|
||||||
|
owner, err := fo.Owner(ctx, "/usr/bin/tree")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, owner, "tree")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Owner_NotFound", func(t *testing.T) {
|
||||||
|
_, err := fo.Owner(ctx, "/nonexistent/path/xyz")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
_ = mgr.Remove(ctx, snack.Targets("tree"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Upgrade ---
|
||||||
|
t.Run("Upgrade", func(t *testing.T) {
|
||||||
|
err := mgr.Upgrade(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,86 +19,355 @@ func TestIntegration_Apt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Verify Name
|
||||||
|
assert.Equal(t, "apt", mgr.Name())
|
||||||
|
|
||||||
|
// Verify capabilities reported correctly
|
||||||
|
caps := snack.GetCapabilities(mgr)
|
||||||
|
assert.True(t, caps.VersionQuery, "apt should support VersionQuery")
|
||||||
|
assert.True(t, caps.Hold, "apt should support Hold")
|
||||||
|
assert.True(t, caps.Clean, "apt should support Clean")
|
||||||
|
assert.True(t, caps.FileOwnership, "apt should support FileOwnership")
|
||||||
|
assert.True(t, caps.RepoManagement, "apt should support RepoManagement")
|
||||||
|
assert.True(t, caps.KeyManagement, "apt should support KeyManagement")
|
||||||
|
assert.True(t, caps.NameNormalize, "apt should support NameNormalize")
|
||||||
|
assert.False(t, caps.Groups, "apt should not support Groups")
|
||||||
|
|
||||||
t.Run("Update", func(t *testing.T) {
|
t.Run("Update", func(t *testing.T) {
|
||||||
err := mgr.Update(ctx)
|
require.NoError(t, mgr.Update(ctx))
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Search", func(t *testing.T) {
|
t.Run("Search", func(t *testing.T) {
|
||||||
pkgs, err := mgr.Search(ctx, "curl")
|
pkgs, err := mgr.Search(ctx, "curl")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, pkgs)
|
require.NotEmpty(t, pkgs, "search for curl should return results")
|
||||||
|
|
||||||
|
// Verify Package struct fields are populated
|
||||||
|
found := false
|
||||||
|
for _, p := range pkgs {
|
||||||
|
if p.Name == "curl" {
|
||||||
|
found = true
|
||||||
|
assert.NotEmpty(t, p.Description, "curl should have a description")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "curl should appear in search results")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Search_NoResults", func(t *testing.T) {
|
||||||
|
pkgs, err := mgr.Search(ctx, "xyznonexistentpackage999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, pkgs)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Info", func(t *testing.T) {
|
t.Run("Info", func(t *testing.T) {
|
||||||
pkg, err := mgr.Info(ctx, "curl")
|
pkg, err := mgr.Info(ctx, "bash")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, pkg)
|
require.NotNil(t, pkg)
|
||||||
assert.Equal(t, "curl", pkg.Name)
|
assert.Equal(t, "bash", pkg.Name)
|
||||||
|
assert.NotEmpty(t, pkg.Version)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Install", func(t *testing.T) {
|
t.Run("Info_NotFound", func(t *testing.T) {
|
||||||
err := mgr.Install(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
_, err := mgr.Info(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Install_Single", func(t *testing.T) {
|
||||||
|
// Remove first to ensure clean state
|
||||||
|
_ = mgr.Remove(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
|
||||||
|
err := mgr.Install(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IsInstalled", func(t *testing.T) {
|
t.Run("IsInstalled_True", func(t *testing.T) {
|
||||||
installed, err := mgr.IsInstalled(ctx, "tree")
|
installed, err := mgr.IsInstalled(ctx, "tree")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, installed)
|
assert.True(t, installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Version", func(t *testing.T) {
|
t.Run("IsInstalled_False", func(t *testing.T) {
|
||||||
|
installed, err := mgr.IsInstalled(ctx, "xyznonexistentpackage999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, installed)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Version_Installed", func(t *testing.T) {
|
||||||
ver, err := mgr.Version(ctx, "tree")
|
ver, err := mgr.Version(ctx, "tree")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, ver)
|
assert.NotEmpty(t, ver)
|
||||||
|
t.Logf("tree version: %s", ver)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("List", func(t *testing.T) {
|
t.Run("Version_NotInstalled", func(t *testing.T) {
|
||||||
|
_, err := mgr.Version(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("List_ContainsInstalled", func(t *testing.T) {
|
||||||
pkgs, err := mgr.List(ctx)
|
pkgs, err := mgr.List(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, pkgs)
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
for _, p := range pkgs {
|
for _, p := range pkgs {
|
||||||
if p.Name == "tree" {
|
if p.Name == "tree" {
|
||||||
found = true
|
found = true
|
||||||
|
assert.NotEmpty(t, p.Version)
|
||||||
|
assert.True(t, p.Installed)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.True(t, found, "tree should be in installed list")
|
assert.True(t, found, "tree should be in installed list")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Remove", func(t *testing.T) {
|
t.Run("Install_WithVersion", func(t *testing.T) {
|
||||||
err := mgr.Remove(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
// Get current version and reinstall with explicit version
|
||||||
|
ver, err := mgr.Version(ctx, "tree")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = mgr.Install(ctx, []snack.Target{{Name: "tree", Version: ver}}, snack.WithAssumeYes())
|
||||||
|
// Should succeed (already installed at that version) or be a no-op
|
||||||
|
// Some apt versions may return success, others may note it's already installed
|
||||||
|
_ = err
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IsInstalled_After_Remove", func(t *testing.T) {
|
t.Run("Purge", func(t *testing.T) {
|
||||||
|
// Install then purge (removes config files too)
|
||||||
|
_ = mgr.Install(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
err := mgr.Purge(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
installed, err := mgr.IsInstalled(ctx, "tree")
|
installed, err := mgr.IsInstalled(ctx, "tree")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, installed)
|
assert.False(t, installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Capabilities", func(t *testing.T) {
|
t.Run("Install_Multiple", func(t *testing.T) {
|
||||||
if vq, ok := mgr.(snack.VersionQuerier); ok {
|
err := mgr.Install(ctx, snack.Targets("tree", "less"), snack.WithAssumeYes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, pkg := range []string{"tree", "less"} {
|
||||||
|
installed, err := mgr.IsInstalled(ctx, pkg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, installed, "%s should be installed", pkg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Remove", func(t *testing.T) {
|
||||||
|
err := mgr.Remove(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
installed, err := mgr.IsInstalled(ctx, "tree")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, installed)
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- VersionQuerier ---
|
||||||
|
t.Run("VersionQuerier", func(t *testing.T) {
|
||||||
|
vq, ok := mgr.(snack.VersionQuerier)
|
||||||
|
require.True(t, ok, "apt must implement VersionQuerier")
|
||||||
|
|
||||||
|
t.Run("LatestVersion", func(t *testing.T) {
|
||||||
ver, err := vq.LatestVersion(ctx, "curl")
|
ver, err := vq.LatestVersion(ctx, "curl")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, ver)
|
assert.NotEmpty(t, ver)
|
||||||
|
t.Logf("curl latest: %s", ver)
|
||||||
|
})
|
||||||
|
|
||||||
upgrades, err := vq.ListUpgrades(ctx)
|
t.Run("LatestVersion_NotFound", func(t *testing.T) {
|
||||||
|
_, err := vq.LatestVersion(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ListUpgrades", func(t *testing.T) {
|
||||||
|
pkgs, err := vq.ListUpgrades(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_ = upgrades
|
// May be empty in a freshly updated container, that's fine
|
||||||
}
|
t.Logf("upgradable packages: %d", len(pkgs))
|
||||||
|
for _, p := range pkgs {
|
||||||
if cl, ok := mgr.(snack.Cleaner); ok {
|
assert.NotEmpty(t, p.Name)
|
||||||
err := cl.Clean(ctx)
|
assert.NotEmpty(t, p.Version)
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fo, ok := mgr.(snack.FileOwner); ok {
|
|
||||||
owner, err := fo.Owner(ctx, "/usr/bin/apt")
|
|
||||||
if err == nil {
|
|
||||||
assert.NotEmpty(t, owner)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("UpgradeAvailable", func(t *testing.T) {
|
||||||
|
// bash is typically at latest in a fresh container
|
||||||
|
avail, err := vq.UpgradeAvailable(ctx, "bash")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_ = avail // just exercising the code path
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("VersionCmp", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
v1, v2 string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"1.0-1", "1.0-2", -1},
|
||||||
|
{"2.0-1", "1.0-1", 1},
|
||||||
|
{"1.0-1", "1.0-1", 0},
|
||||||
|
{"1:1.0-1", "1.0-1", 1}, // epoch
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
cmp, err := vq.VersionCmp(ctx, tt.v1, tt.v2)
|
||||||
|
require.NoError(t, err, "VersionCmp(%s, %s)", tt.v1, tt.v2)
|
||||||
|
assert.Equal(t, tt.want, cmp, "VersionCmp(%s, %s)", tt.v1, tt.v2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Holder ---
|
||||||
|
t.Run("Holder", func(t *testing.T) {
|
||||||
|
h, ok := mgr.(snack.Holder)
|
||||||
|
require.True(t, ok, "apt must implement Holder")
|
||||||
|
|
||||||
|
// Ensure tree is installed for hold tests
|
||||||
|
_ = mgr.Install(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
|
||||||
|
t.Run("Hold", func(t *testing.T) {
|
||||||
|
err := h.Hold(ctx, []string{"tree"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ListHeld", func(t *testing.T) {
|
||||||
|
held, err := h.ListHeld(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
found := false
|
||||||
|
for _, p := range held {
|
||||||
|
if p.Name == "tree" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "tree should be in held list")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Unhold", func(t *testing.T) {
|
||||||
|
err := h.Unhold(ctx, []string{"tree"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
held, err := h.ListHeld(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
for _, p := range held {
|
||||||
|
assert.NotEqual(t, "tree", p.Name, "tree should not be held after unhold")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Cleaner ---
|
||||||
|
t.Run("Cleaner", func(t *testing.T) {
|
||||||
|
cl, ok := mgr.(snack.Cleaner)
|
||||||
|
require.True(t, ok, "apt must implement Cleaner")
|
||||||
|
|
||||||
|
t.Run("Autoremove", func(t *testing.T) {
|
||||||
|
err := cl.Autoremove(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Clean", func(t *testing.T) {
|
||||||
|
err := cl.Clean(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- FileOwner ---
|
||||||
|
t.Run("FileOwner", func(t *testing.T) {
|
||||||
|
fo, ok := mgr.(snack.FileOwner)
|
||||||
|
require.True(t, ok, "apt must implement FileOwner")
|
||||||
|
|
||||||
|
// Ensure tree is installed
|
||||||
|
_ = mgr.Install(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
|
||||||
|
t.Run("FileList", func(t *testing.T) {
|
||||||
|
files, err := fo.FileList(ctx, "tree")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, files)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, f := range files {
|
||||||
|
if f == "/usr/bin/tree" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "/usr/bin/tree should be in file list")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FileList_NotInstalled", func(t *testing.T) {
|
||||||
|
_, err := fo.FileList(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Owner", func(t *testing.T) {
|
||||||
|
owner, err := fo.Owner(ctx, "/usr/bin/tree")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, owner, "tree")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Owner_NotFound", func(t *testing.T) {
|
||||||
|
_, err := fo.Owner(ctx, "/nonexistent/path/xyz")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- RepoManager ---
|
||||||
|
t.Run("RepoManager", func(t *testing.T) {
|
||||||
|
rm, ok := mgr.(snack.RepoManager)
|
||||||
|
require.True(t, ok, "apt must implement RepoManager")
|
||||||
|
|
||||||
|
t.Run("ListRepos", func(t *testing.T) {
|
||||||
|
repos, err := rm.ListRepos(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Logf("repos: %d", len(repos))
|
||||||
|
// Container may use DEB822 format (.sources) not parsed by current implementation
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- KeyManager ---
|
||||||
|
t.Run("KeyManager", func(t *testing.T) {
|
||||||
|
km, ok := mgr.(snack.KeyManager)
|
||||||
|
require.True(t, ok, "apt must implement KeyManager")
|
||||||
|
|
||||||
|
t.Run("ListKeys", func(t *testing.T) {
|
||||||
|
keys, err := km.ListKeys(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// May have keys or not, just exercising the path
|
||||||
|
t.Logf("keys: %d", len(keys))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- NameNormalizer ---
|
||||||
|
t.Run("NameNormalizer", func(t *testing.T) {
|
||||||
|
nn, ok := mgr.(snack.NameNormalizer)
|
||||||
|
require.True(t, ok, "apt must implement NameNormalizer")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input, wantName, wantArch string
|
||||||
|
}{
|
||||||
|
{"curl:amd64", "curl", "amd64"},
|
||||||
|
{"bash:arm64", "bash", "arm64"},
|
||||||
|
{"python3", "python3", ""},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run("NormalizeName_"+tt.input, func(t *testing.T) {
|
||||||
|
got := nn.NormalizeName(tt.input)
|
||||||
|
assert.Equal(t, tt.wantName, got)
|
||||||
|
})
|
||||||
|
t.Run("ParseArch_"+tt.input, func(t *testing.T) {
|
||||||
|
name, arch := nn.ParseArch(tt.input)
|
||||||
|
assert.Equal(t, tt.wantName, name)
|
||||||
|
assert.Equal(t, tt.wantArch, arch)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Upgrade (run last as it may change state) ---
|
||||||
|
t.Run("Upgrade", func(t *testing.T) {
|
||||||
|
err := mgr.Upgrade(ctx, snack.WithAssumeYes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
_ = mgr.Remove(ctx, snack.Targets("tree", "less"), snack.WithAssumeYes())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,18 +12,53 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestIntegration_Detect(t *testing.T) {
|
func TestIntegration_Detect(t *testing.T) {
|
||||||
|
t.Run("Default", func(t *testing.T) {
|
||||||
mgr, err := detect.Default()
|
mgr, err := detect.Default()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, mgr)
|
require.NotNil(t, mgr)
|
||||||
t.Logf("Detected: %s", mgr.Name())
|
assert.NotEmpty(t, mgr.Name())
|
||||||
|
assert.True(t, mgr.Available())
|
||||||
|
t.Logf("Detected default: %s", mgr.Name())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("All", func(t *testing.T) {
|
||||||
all := detect.All()
|
all := detect.All()
|
||||||
require.NotEmpty(t, all)
|
require.NotEmpty(t, all)
|
||||||
for _, m := range all {
|
for _, m := range all {
|
||||||
|
assert.NotEmpty(t, m.Name())
|
||||||
|
assert.True(t, m.Available())
|
||||||
t.Logf("Available: %s", m.Name())
|
t.Logf("Available: %s", m.Name())
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ByName_Valid", func(t *testing.T) {
|
||||||
|
all := detect.All()
|
||||||
|
require.NotEmpty(t, all)
|
||||||
|
|
||||||
|
// Should be able to find each detected manager by name
|
||||||
|
for _, m := range all {
|
||||||
|
found, err := detect.ByName(m.Name())
|
||||||
|
require.NoError(t, err, "ByName(%s)", m.Name())
|
||||||
|
require.NotNil(t, found)
|
||||||
|
assert.Equal(t, m.Name(), found.Name())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ByName_Invalid", func(t *testing.T) {
|
||||||
|
_, err := detect.ByName("xyznonexistentmanager999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, snack.ErrManagerNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Capabilities", func(t *testing.T) {
|
||||||
|
mgr, err := detect.Default()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
caps := snack.GetCapabilities(mgr)
|
caps := snack.GetCapabilities(mgr)
|
||||||
t.Logf("Capabilities: %+v", caps)
|
t.Logf("Default manager %s capabilities: %+v", mgr.Name(), caps)
|
||||||
|
|
||||||
|
// Every manager should have at least the base Manager interface
|
||||||
|
// (which isn't in Capabilities, but let's verify some basics)
|
||||||
assert.NotEmpty(t, mgr.Name())
|
assert.NotEmpty(t, mgr.Name())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func (d *DNF) Unhold(ctx context.Context, pkgs []string) error {
|
|||||||
|
|
||||||
// ListHeld returns all currently held packages.
|
// ListHeld returns all currently held packages.
|
||||||
func (d *DNF) ListHeld(ctx context.Context) ([]snack.Package, error) {
|
func (d *DNF) ListHeld(ctx context.Context) ([]snack.Package, error) {
|
||||||
return listHeld(ctx)
|
return listHeld(ctx, d.v5)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Autoremove removes orphaned packages.
|
// Autoremove removes orphaned packages.
|
||||||
|
|||||||
@@ -102,11 +102,14 @@ func unhold(ctx context.Context, pkgs []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func listHeld(ctx context.Context) ([]snack.Package, error) {
|
func listHeld(ctx context.Context, v5 bool) ([]snack.Package, error) {
|
||||||
out, err := run(ctx, []string{"versionlock", "list"}, snack.Options{})
|
out, err := run(ctx, []string{"versionlock", "list"}, snack.Options{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("dnf listHeld: %w", err)
|
return nil, fmt.Errorf("dnf listHeld: %w", err)
|
||||||
}
|
}
|
||||||
|
if v5 {
|
||||||
|
return parseVersionLockDNF5(out), nil
|
||||||
|
}
|
||||||
return parseVersionLock(out), nil
|
return parseVersionLock(out), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func unhold(_ context.Context, _ []string) error {
|
|||||||
return snack.ErrUnsupportedPlatform
|
return snack.ErrUnsupportedPlatform
|
||||||
}
|
}
|
||||||
|
|
||||||
func listHeld(_ context.Context) ([]snack.Package, error) {
|
func listHeld(_ context.Context, _ bool) ([]snack.Package, error) {
|
||||||
return nil, snack.ErrUnsupportedPlatform
|
return nil, snack.ErrUnsupportedPlatform
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,104 +27,376 @@ func TestIntegration_DNF(t *testing.T) {
|
|||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
assert.Equal(t, "dnf", mgr.Name())
|
||||||
|
|
||||||
|
caps := snack.GetCapabilities(mgr)
|
||||||
|
assert.True(t, caps.VersionQuery, "dnf should support VersionQuery")
|
||||||
|
assert.True(t, caps.Hold, "dnf should support Hold")
|
||||||
|
assert.True(t, caps.Clean, "dnf should support Clean")
|
||||||
|
assert.True(t, caps.FileOwnership, "dnf should support FileOwnership")
|
||||||
|
assert.True(t, caps.RepoManagement, "dnf should support RepoManagement")
|
||||||
|
assert.True(t, caps.KeyManagement, "dnf should support KeyManagement")
|
||||||
|
assert.True(t, caps.Groups, "dnf should support Groups")
|
||||||
|
assert.True(t, caps.NameNormalize, "dnf should support NameNormalize")
|
||||||
|
|
||||||
t.Run("Update", func(t *testing.T) {
|
t.Run("Update", func(t *testing.T) {
|
||||||
err := mgr.Update(ctx)
|
require.NoError(t, mgr.Update(ctx))
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Search", func(t *testing.T) {
|
t.Run("Search", func(t *testing.T) {
|
||||||
pkgs, err := mgr.Search(ctx, "curl")
|
pkgs, err := mgr.Search(ctx, "curl")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, pkgs)
|
require.NotEmpty(t, pkgs)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, p := range pkgs {
|
||||||
|
if p.Name == "curl" {
|
||||||
|
found = true
|
||||||
|
assert.NotEmpty(t, p.Description)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "curl should appear in search results")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Search_NoResults", func(t *testing.T) {
|
||||||
|
pkgs, err := mgr.Search(ctx, "xyznonexistentpackage999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, pkgs)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Info", func(t *testing.T) {
|
t.Run("Info", func(t *testing.T) {
|
||||||
pkg, err := mgr.Info(ctx, "curl")
|
pkg, err := mgr.Info(ctx, "bash")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, pkg)
|
require.NotNil(t, pkg)
|
||||||
assert.Equal(t, "curl", pkg.Name)
|
assert.Equal(t, "bash", pkg.Name)
|
||||||
|
assert.NotEmpty(t, pkg.Version)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Install", func(t *testing.T) {
|
t.Run("Info_NotFound", func(t *testing.T) {
|
||||||
err := mgr.Install(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
_, err := mgr.Info(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, snack.ErrNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Install_Single", func(t *testing.T) {
|
||||||
|
_ = mgr.Remove(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
err := mgr.Install(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IsInstalled", func(t *testing.T) {
|
t.Run("IsInstalled_True", func(t *testing.T) {
|
||||||
installed, err := mgr.IsInstalled(ctx, "tree")
|
installed, err := mgr.IsInstalled(ctx, "tree")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, installed)
|
assert.True(t, installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Version", func(t *testing.T) {
|
t.Run("IsInstalled_False", func(t *testing.T) {
|
||||||
|
installed, err := mgr.IsInstalled(ctx, "xyznonexistentpackage999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, installed)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Version_Installed", func(t *testing.T) {
|
||||||
ver, err := mgr.Version(ctx, "tree")
|
ver, err := mgr.Version(ctx, "tree")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, ver)
|
assert.NotEmpty(t, ver)
|
||||||
|
t.Logf("tree version: %s", ver)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("List", func(t *testing.T) {
|
t.Run("Version_NotInstalled", func(t *testing.T) {
|
||||||
|
_, err := mgr.Version(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, snack.ErrNotInstalled)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("List_ContainsInstalled", func(t *testing.T) {
|
||||||
pkgs, err := mgr.List(ctx)
|
pkgs, err := mgr.List(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, pkgs)
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
for _, p := range pkgs {
|
for _, p := range pkgs {
|
||||||
if p.Name == "tree" {
|
if p.Name == "tree" {
|
||||||
found = true
|
found = true
|
||||||
|
assert.NotEmpty(t, p.Version)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.True(t, found, "tree should be in installed list")
|
assert.True(t, found, "tree should be in installed list")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Remove", func(t *testing.T) {
|
t.Run("Install_Multiple", func(t *testing.T) {
|
||||||
err := mgr.Remove(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
err := mgr.Install(ctx, snack.Targets("tree", "less"), snack.WithAssumeYes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, pkg := range []string{"tree", "less"} {
|
||||||
|
installed, err := mgr.IsInstalled(ctx, pkg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, installed, "%s should be installed", pkg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Install_WithRefresh", func(t *testing.T) {
|
||||||
|
err := mgr.Install(ctx, snack.Targets("tree"), snack.WithAssumeYes(), snack.WithRefresh())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IsInstalled_After_Remove", func(t *testing.T) {
|
t.Run("Purge", func(t *testing.T) {
|
||||||
|
err := mgr.Purge(ctx, snack.Targets("less"), snack.WithAssumeYes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
installed, err := mgr.IsInstalled(ctx, "less")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, installed)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Remove", func(t *testing.T) {
|
||||||
|
err := mgr.Remove(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
installed, err := mgr.IsInstalled(ctx, "tree")
|
installed, err := mgr.IsInstalled(ctx, "tree")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, installed)
|
assert.False(t, installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Capabilities", func(t *testing.T) {
|
// --- VersionQuerier ---
|
||||||
if vq, ok := mgr.(snack.VersionQuerier); ok {
|
t.Run("VersionQuerier", func(t *testing.T) {
|
||||||
|
vq, ok := mgr.(snack.VersionQuerier)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
t.Run("LatestVersion", func(t *testing.T) {
|
||||||
ver, err := vq.LatestVersion(ctx, "curl")
|
ver, err := vq.LatestVersion(ctx, "curl")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, ver)
|
assert.NotEmpty(t, ver)
|
||||||
|
t.Logf("curl latest: %s", ver)
|
||||||
|
})
|
||||||
|
|
||||||
upgrades, err := vq.ListUpgrades(ctx)
|
t.Run("LatestVersion_NotFound", func(t *testing.T) {
|
||||||
|
_, err := vq.LatestVersion(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ListUpgrades", func(t *testing.T) {
|
||||||
|
pkgs, err := vq.ListUpgrades(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_ = upgrades
|
t.Logf("upgradable packages: %d", len(pkgs))
|
||||||
}
|
for _, p := range pkgs {
|
||||||
|
assert.NotEmpty(t, p.Name)
|
||||||
if cl, ok := mgr.(snack.Cleaner); ok {
|
assert.NotEmpty(t, p.Version)
|
||||||
err := cl.Clean(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fo, ok := mgr.(snack.FileOwner); ok {
|
|
||||||
owner, err := fo.Owner(ctx, "/usr/bin/dnf")
|
|
||||||
if err == nil {
|
|
||||||
assert.NotEmpty(t, owner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if g, ok := mgr.(snack.Grouper); ok {
|
|
||||||
groups, err := g.GroupList(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
_ = groups
|
|
||||||
}
|
|
||||||
|
|
||||||
if rm, ok := mgr.(snack.RepoManager); ok {
|
|
||||||
repos, err := rm.ListRepos(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, repos, "should have at least one repo")
|
|
||||||
t.Logf("repos: %d", len(repos))
|
|
||||||
}
|
|
||||||
|
|
||||||
if nn, ok := mgr.(snack.NameNormalizer); ok {
|
|
||||||
got := nn.NormalizeName("curl.x86_64")
|
|
||||||
assert.Equal(t, "curl", got)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("UpgradeAvailable", func(t *testing.T) {
|
||||||
|
avail, err := vq.UpgradeAvailable(ctx, "bash")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_ = avail
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("VersionCmp", func(t *testing.T) {
|
||||||
|
// rpmdev-vercmp may not be installed; skip if not available
|
||||||
|
cmp, err := vq.VersionCmp(ctx, "1.0-1", "1.0-2")
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("rpmdev-vercmp not available")
|
||||||
|
}
|
||||||
|
assert.Equal(t, -1, cmp)
|
||||||
|
|
||||||
|
cmp, err = vq.VersionCmp(ctx, "2.0-1", "1.0-1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, cmp)
|
||||||
|
|
||||||
|
cmp, err = vq.VersionCmp(ctx, "1.0-1", "1.0-1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, cmp)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Holder ---
|
||||||
|
t.Run("Holder", func(t *testing.T) {
|
||||||
|
h, ok := mgr.(snack.Holder)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
// Install tree for hold tests; also ensure versionlock plugin
|
||||||
|
_ = mgr.Install(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
|
||||||
|
t.Run("Hold", func(t *testing.T) {
|
||||||
|
err := h.Hold(ctx, []string{"tree"})
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("versionlock plugin not available: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("ListHeld", func(t *testing.T) {
|
||||||
|
held, err := h.ListHeld(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
found := false
|
||||||
|
for _, p := range held {
|
||||||
|
if p.Name == "tree" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "tree should be in held list")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Unhold", func(t *testing.T) {
|
||||||
|
err := h.Unhold(ctx, []string{"tree"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
_ = mgr.Remove(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Cleaner ---
|
||||||
|
t.Run("Cleaner", func(t *testing.T) {
|
||||||
|
cl, ok := mgr.(snack.Cleaner)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
t.Run("Autoremove", func(t *testing.T) {
|
||||||
|
err := cl.Autoremove(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Clean", func(t *testing.T) {
|
||||||
|
err := cl.Clean(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- FileOwner ---
|
||||||
|
t.Run("FileOwner", func(t *testing.T) {
|
||||||
|
fo, ok := mgr.(snack.FileOwner)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
_ = mgr.Install(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
|
||||||
|
t.Run("FileList", func(t *testing.T) {
|
||||||
|
files, err := fo.FileList(ctx, "tree")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, files)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, f := range files {
|
||||||
|
if f == "/usr/bin/tree" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "/usr/bin/tree should be in file list")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FileList_NotInstalled", func(t *testing.T) {
|
||||||
|
_, err := fo.FileList(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Owner", func(t *testing.T) {
|
||||||
|
owner, err := fo.Owner(ctx, "/usr/bin/tree")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, owner, "tree")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Owner_NotFound", func(t *testing.T) {
|
||||||
|
_, err := fo.Owner(ctx, "/nonexistent/path/xyz")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
_ = mgr.Remove(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- RepoManager ---
|
||||||
|
t.Run("RepoManager", func(t *testing.T) {
|
||||||
|
rm, ok := mgr.(snack.RepoManager)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
t.Run("ListRepos", func(t *testing.T) {
|
||||||
|
repos, err := rm.ListRepos(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, repos)
|
||||||
|
for _, r := range repos {
|
||||||
|
assert.NotEmpty(t, r.ID)
|
||||||
|
}
|
||||||
|
t.Logf("repos: %d", len(repos))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- KeyManager ---
|
||||||
|
t.Run("KeyManager", func(t *testing.T) {
|
||||||
|
km, ok := mgr.(snack.KeyManager)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
t.Run("ListKeys", func(t *testing.T) {
|
||||||
|
keys, err := km.ListKeys(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, keys, "Fedora should have GPG keys")
|
||||||
|
t.Logf("keys: %d", len(keys))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Grouper ---
|
||||||
|
t.Run("Grouper", func(t *testing.T) {
|
||||||
|
g, ok := mgr.(snack.Grouper)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
t.Run("GroupList", func(t *testing.T) {
|
||||||
|
groups, err := g.GroupList(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, groups)
|
||||||
|
t.Logf("groups: %d", len(groups))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GroupInfo", func(t *testing.T) {
|
||||||
|
// Use a group that should exist
|
||||||
|
groups, err := g.GroupList(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, groups)
|
||||||
|
|
||||||
|
pkgs, err := g.GroupInfo(ctx, groups[0])
|
||||||
|
// Some groups may not have packages queryable by name
|
||||||
|
if err == nil {
|
||||||
|
t.Logf("group %q packages: %d", groups[0], len(pkgs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GroupInfo_NotFound", func(t *testing.T) {
|
||||||
|
// dnf5 may return empty instead of error for unknown groups
|
||||||
|
pkgs, err := g.GroupInfo(ctx, "xyznonexistentgroup999")
|
||||||
|
if err == nil {
|
||||||
|
assert.Empty(t, pkgs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- NameNormalizer ---
|
||||||
|
t.Run("NameNormalizer", func(t *testing.T) {
|
||||||
|
nn, ok := mgr.(snack.NameNormalizer)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input, wantName, wantArch string
|
||||||
|
}{
|
||||||
|
{"curl.x86_64", "curl", "x86_64"},
|
||||||
|
{"bash.aarch64", "bash", "aarch64"},
|
||||||
|
{"python3.noarch", "python3", "noarch"},
|
||||||
|
{"python3.11.x86_64", "python3.11", "x86_64"},
|
||||||
|
{"glibc", "glibc", ""},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run("NormalizeName_"+tt.input, func(t *testing.T) {
|
||||||
|
got := nn.NormalizeName(tt.input)
|
||||||
|
assert.Equal(t, tt.wantName, got)
|
||||||
|
})
|
||||||
|
t.Run("ParseArch_"+tt.input, func(t *testing.T) {
|
||||||
|
name, arch := nn.ParseArch(tt.input)
|
||||||
|
assert.Equal(t, tt.wantName, name)
|
||||||
|
assert.Equal(t, tt.wantArch, arch)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Upgrade ---
|
||||||
|
t.Run("Upgrade", func(t *testing.T) {
|
||||||
|
err := mgr.Upgrade(ctx, snack.WithAssumeYes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,6 +188,27 @@ func parseGroupListDNF5(output string) []string {
|
|||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseVersionLockDNF5 parses `dnf5 versionlock list` output.
|
||||||
|
// Format:
|
||||||
|
//
|
||||||
|
// # Added by 'versionlock add' command on 2026-02-26 03:14:29
|
||||||
|
// Package name: tree
|
||||||
|
// evr = 2.2.1-2.fc43
|
||||||
|
func parseVersionLockDNF5(output string) []snack.Package {
|
||||||
|
output = stripPreamble(output)
|
||||||
|
var pkgs []snack.Package
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "Package name:") {
|
||||||
|
name := strings.TrimSpace(strings.TrimPrefix(trimmed, "Package name:"))
|
||||||
|
if name != "" {
|
||||||
|
pkgs = append(pkgs, snack.Package{Name: name, Installed: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pkgs
|
||||||
|
}
|
||||||
|
|
||||||
// parseGroupInfoDNF5 parses `dnf5 group info` output.
|
// parseGroupInfoDNF5 parses `dnf5 group info` output.
|
||||||
// Format:
|
// Format:
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -362,6 +362,26 @@ Default packages : NetworkManager-config-connectivity-fedora
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseVersionLockDNF5(t *testing.T) {
|
||||||
|
input := `# Added by 'versionlock add' command on 2026-02-26 03:14:29
|
||||||
|
Package name: tree
|
||||||
|
evr = 2.2.1-2.fc43
|
||||||
|
# Added by 'versionlock add' command on 2026-02-26 03:14:45
|
||||||
|
Package name: curl
|
||||||
|
evr = 8.11.1-3.fc43
|
||||||
|
`
|
||||||
|
pkgs := parseVersionLockDNF5(input)
|
||||||
|
if len(pkgs) != 2 {
|
||||||
|
t.Fatalf("expected 2 packages, got %d", len(pkgs))
|
||||||
|
}
|
||||||
|
if pkgs[0].Name != "tree" {
|
||||||
|
t.Errorf("pkg[0].Name = %q, want tree", pkgs[0].Name)
|
||||||
|
}
|
||||||
|
if pkgs[1].Name != "curl" {
|
||||||
|
t.Errorf("pkg[1].Name = %q, want curl", pkgs[1].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseRepoListDNF5(t *testing.T) {
|
func TestParseRepoListDNF5(t *testing.T) {
|
||||||
input := `repo id repo name status
|
input := `repo id repo name status
|
||||||
fedora Fedora 43 - x86_64 enabled
|
fedora Fedora 43 - x86_64 enabled
|
||||||
|
|||||||
@@ -19,23 +19,57 @@ func TestIntegration_Dpkg(t *testing.T) {
|
|||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
assert.Equal(t, "dpkg", mgr.Name())
|
||||||
|
|
||||||
|
caps := snack.GetCapabilities(mgr)
|
||||||
|
assert.True(t, caps.FileOwnership, "dpkg should support FileOwnership")
|
||||||
|
assert.True(t, caps.NameNormalize, "dpkg should support NameNormalize")
|
||||||
|
assert.False(t, caps.VersionQuery)
|
||||||
|
assert.False(t, caps.Hold)
|
||||||
|
assert.False(t, caps.Clean)
|
||||||
|
assert.False(t, caps.RepoManagement)
|
||||||
|
assert.False(t, caps.KeyManagement)
|
||||||
|
assert.False(t, caps.Groups)
|
||||||
|
|
||||||
t.Run("List", func(t *testing.T) {
|
t.Run("List", func(t *testing.T) {
|
||||||
pkgs, err := mgr.List(ctx)
|
pkgs, err := mgr.List(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, pkgs)
|
require.NotEmpty(t, pkgs)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, p := range pkgs {
|
||||||
|
if p.Name == "bash" {
|
||||||
|
found = true
|
||||||
|
assert.NotEmpty(t, p.Version)
|
||||||
|
assert.True(t, p.Installed)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "bash should be in list")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IsInstalled", func(t *testing.T) {
|
t.Run("IsInstalled_True", func(t *testing.T) {
|
||||||
// bash should always be installed
|
|
||||||
installed, err := mgr.IsInstalled(ctx, "bash")
|
installed, err := mgr.IsInstalled(ctx, "bash")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, installed)
|
assert.True(t, installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("IsInstalled_False", func(t *testing.T) {
|
||||||
|
installed, err := mgr.IsInstalled(ctx, "xyznonexistentpackage999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, installed)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Version", func(t *testing.T) {
|
t.Run("Version", func(t *testing.T) {
|
||||||
ver, err := mgr.Version(ctx, "bash")
|
ver, err := mgr.Version(ctx, "bash")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, ver)
|
assert.NotEmpty(t, ver)
|
||||||
|
t.Logf("bash version: %s", ver)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Version_NotInstalled", func(t *testing.T) {
|
||||||
|
_, err := mgr.Version(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Info", func(t *testing.T) {
|
t.Run("Info", func(t *testing.T) {
|
||||||
@@ -43,6 +77,12 @@ func TestIntegration_Dpkg(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, pkg)
|
require.NotNil(t, pkg)
|
||||||
assert.Equal(t, "bash", pkg.Name)
|
assert.Equal(t, "bash", pkg.Name)
|
||||||
|
assert.NotEmpty(t, pkg.Version)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Info_NotFound", func(t *testing.T) {
|
||||||
|
_, err := mgr.Info(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Search", func(t *testing.T) {
|
t.Run("Search", func(t *testing.T) {
|
||||||
@@ -51,15 +91,86 @@ func TestIntegration_Dpkg(t *testing.T) {
|
|||||||
require.NotEmpty(t, pkgs)
|
require.NotEmpty(t, pkgs)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Search_NoResults", func(t *testing.T) {
|
||||||
|
pkgs, err := mgr.Search(ctx, "xyznonexistentpackage999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, pkgs)
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- FileOwner ---
|
||||||
t.Run("FileOwner", func(t *testing.T) {
|
t.Run("FileOwner", func(t *testing.T) {
|
||||||
if fo, ok := mgr.(snack.FileOwner); ok {
|
fo, ok := mgr.(snack.FileOwner)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
t.Run("FileList", func(t *testing.T) {
|
||||||
|
files, err := fo.FileList(ctx, "bash")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, files)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, f := range files {
|
||||||
|
if f == "/usr/bin/bash" || f == "/bin/bash" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "bash binary should be in file list")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FileList_NotInstalled", func(t *testing.T) {
|
||||||
|
_, err := fo.FileList(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Owner", func(t *testing.T) {
|
||||||
owner, err := fo.Owner(ctx, "/usr/bin/dpkg")
|
owner, err := fo.Owner(ctx, "/usr/bin/dpkg")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, owner)
|
assert.NotEmpty(t, owner)
|
||||||
|
assert.Contains(t, owner, "dpkg")
|
||||||
|
})
|
||||||
|
|
||||||
files, err := fo.FileList(ctx, "bash")
|
t.Run("Owner_NotFound", func(t *testing.T) {
|
||||||
require.NoError(t, err)
|
_, err := fo.Owner(ctx, "/nonexistent/path/xyz")
|
||||||
assert.NotEmpty(t, files)
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- NameNormalizer ---
|
||||||
|
t.Run("NameNormalizer", func(t *testing.T) {
|
||||||
|
nn, ok := mgr.(snack.NameNormalizer)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input, wantName, wantArch string
|
||||||
|
}{
|
||||||
|
{"curl:amd64", "curl", "amd64"},
|
||||||
|
{"bash:arm64", "bash", "arm64"},
|
||||||
|
{"python3", "python3", ""},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run("NormalizeName_"+tt.input, func(t *testing.T) {
|
||||||
|
got := nn.NormalizeName(tt.input)
|
||||||
|
assert.Equal(t, tt.wantName, got)
|
||||||
|
})
|
||||||
|
t.Run("ParseArch_"+tt.input, func(t *testing.T) {
|
||||||
|
name, arch := nn.ParseArch(tt.input)
|
||||||
|
assert.Equal(t, tt.wantName, name)
|
||||||
|
assert.Equal(t, tt.wantArch, arch)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// --- Operations that dpkg doesn't really support (exercise error paths) ---
|
||||||
|
t.Run("Install_Unsupported", func(t *testing.T) {
|
||||||
|
// dpkg install requires a .deb file path, not a package name
|
||||||
|
// This should fail gracefully
|
||||||
|
err := mgr.Install(ctx, snack.Targets("tree"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Update", func(t *testing.T) {
|
||||||
|
// dpkg doesn't have update, should be a no-op or error
|
||||||
|
err := mgr.Update(ctx)
|
||||||
|
_ = err // exercise path
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,20 +15,32 @@ import (
|
|||||||
func TestIntegration_Flatpak(t *testing.T) {
|
func TestIntegration_Flatpak(t *testing.T) {
|
||||||
var mgr snack.Manager = flatpak.New()
|
var mgr snack.Manager = flatpak.New()
|
||||||
if !mgr.Available() {
|
if !mgr.Available() {
|
||||||
t.Skip("flatpak not available — install it first")
|
t.Skip("flatpak not available")
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
assert.Equal(t, "flatpak", mgr.Name())
|
||||||
|
|
||||||
|
caps := snack.GetCapabilities(mgr)
|
||||||
|
assert.True(t, caps.Clean, "flatpak should support Clean")
|
||||||
|
assert.True(t, caps.RepoManagement, "flatpak should support RepoManagement")
|
||||||
|
assert.False(t, caps.VersionQuery)
|
||||||
|
assert.False(t, caps.Hold)
|
||||||
|
assert.False(t, caps.FileOwnership)
|
||||||
|
assert.False(t, caps.KeyManagement)
|
||||||
|
assert.False(t, caps.Groups)
|
||||||
|
assert.False(t, caps.NameNormalize)
|
||||||
|
|
||||||
t.Run("Update", func(t *testing.T) {
|
t.Run("Update", func(t *testing.T) {
|
||||||
err := mgr.Update(ctx)
|
require.NoError(t, mgr.Update(ctx))
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// --- RepoManager ---
|
||||||
t.Run("RepoManager", func(t *testing.T) {
|
t.Run("RepoManager", func(t *testing.T) {
|
||||||
rm, ok := mgr.(snack.RepoManager)
|
rm, ok := mgr.(snack.RepoManager)
|
||||||
if !ok {
|
require.True(t, ok)
|
||||||
t.Skip("RepoManager not implemented")
|
|
||||||
}
|
t.Run("ListRepos", func(t *testing.T) {
|
||||||
repos, err := rm.ListRepos(ctx)
|
repos, err := rm.ListRepos(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
found := false
|
found := false
|
||||||
@@ -40,6 +52,7 @@ func TestIntegration_Flatpak(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.True(t, found, "flathub repo should be configured")
|
assert.True(t, found, "flathub repo should be configured")
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Search", func(t *testing.T) {
|
t.Run("Search", func(t *testing.T) {
|
||||||
pkgs, err := mgr.Search(ctx, "Flatseal")
|
pkgs, err := mgr.Search(ctx, "Flatseal")
|
||||||
@@ -47,18 +60,54 @@ func TestIntegration_Flatpak(t *testing.T) {
|
|||||||
require.NotEmpty(t, pkgs)
|
require.NotEmpty(t, pkgs)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Search_NoResults", func(t *testing.T) {
|
||||||
|
pkgs, err := mgr.Search(ctx, "xyznonexistentpackage999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, pkgs)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Install", func(t *testing.T) {
|
t.Run("Install", func(t *testing.T) {
|
||||||
err := mgr.Install(ctx, snack.Targets("com.github.tchx84.Flatseal"), snack.WithSudo(), snack.WithAssumeYes())
|
err := mgr.Install(ctx, snack.Targets("com.github.tchx84.Flatseal"), snack.WithSudo(), snack.WithAssumeYes())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IsInstalled", func(t *testing.T) {
|
t.Run("IsInstalled_True", func(t *testing.T) {
|
||||||
installed, err := mgr.IsInstalled(ctx, "com.github.tchx84.Flatseal")
|
installed, err := mgr.IsInstalled(ctx, "com.github.tchx84.Flatseal")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, installed)
|
assert.True(t, installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("List", func(t *testing.T) {
|
t.Run("IsInstalled_False", func(t *testing.T) {
|
||||||
|
installed, err := mgr.IsInstalled(ctx, "xyznonexistentpackage999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, installed)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Info", func(t *testing.T) {
|
||||||
|
pkg, err := mgr.Info(ctx, "com.github.tchx84.Flatseal")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Info failed (flatpak may not support info on app IDs): %v", err)
|
||||||
|
} else {
|
||||||
|
require.NotNil(t, pkg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Info_NotFound", func(t *testing.T) {
|
||||||
|
_, err := mgr.Info(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Version", func(t *testing.T) {
|
||||||
|
ver, err := mgr.Version(ctx, "com.github.tchx84.Flatseal")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Version failed: %v", err)
|
||||||
|
} else {
|
||||||
|
assert.NotEmpty(t, ver)
|
||||||
|
t.Logf("Flatseal version: %s", ver)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("List_ContainsInstalled", func(t *testing.T) {
|
||||||
pkgs, err := mgr.List(ctx)
|
pkgs, err := mgr.List(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
found := false
|
found := false
|
||||||
@@ -68,7 +117,7 @@ func TestIntegration_Flatpak(t *testing.T) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.True(t, found, "Flatseal should be in installed list (by Name or Application ID)")
|
assert.True(t, found, "Flatseal should be in installed list")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Remove", func(t *testing.T) {
|
t.Run("Remove", func(t *testing.T) {
|
||||||
@@ -81,4 +130,25 @@ func TestIntegration_Flatpak(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, installed)
|
assert.False(t, installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// --- Cleaner ---
|
||||||
|
t.Run("Cleaner", func(t *testing.T) {
|
||||||
|
cl, ok := mgr.(snack.Cleaner)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
t.Run("Autoremove", func(t *testing.T) {
|
||||||
|
err := cl.Autoremove(ctx, snack.WithAssumeYes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Clean", func(t *testing.T) {
|
||||||
|
err := cl.Clean(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Upgrade", func(t *testing.T) {
|
||||||
|
err := mgr.Upgrade(ctx, snack.WithAssumeYes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
//go:build containertest
|
//go:build containertest
|
||||||
|
|
||||||
// Package snack_test provides testcontainers-based integration tests that
|
// Package snack_test provides testcontainers-based integration tests that
|
||||||
// run each package manager in its native container. Use:
|
// exercise every Manager method and capability interface across distros.
|
||||||
//
|
//
|
||||||
// go test -tags containertest -v -count=1 -timeout 10m .
|
// go test -tags containertest -v -count=1 -timeout 15m .
|
||||||
//
|
//
|
||||||
// Requires Docker.
|
// Requires Docker.
|
||||||
package snack_test
|
package snack_test
|
||||||
@@ -22,11 +22,20 @@ import (
|
|||||||
|
|
||||||
const goVersion = "1.26.0"
|
const goVersion = "1.26.0"
|
||||||
|
|
||||||
|
// distroTest describes a container environment and the test packages available in it.
|
||||||
type distroTest struct {
|
type distroTest struct {
|
||||||
name string
|
name string
|
||||||
image string
|
image string
|
||||||
setup string // shell commands to install deps (Go installed separately)
|
setup string // shell commands to install deps
|
||||||
packages string // space-separated test directories
|
packages string // space-separated Go test directories
|
||||||
|
|
||||||
|
// Test fixture data — packages known to exist in this distro's repos.
|
||||||
|
testPkg string // a small package to install/remove (e.g. "tree")
|
||||||
|
searchQuery string // a query that returns results (e.g. "curl")
|
||||||
|
infoPkg string // a package to get info on (always available)
|
||||||
|
knownFile string // a file path owned by a known package
|
||||||
|
knownFileOwner string // the package that owns knownFile (may include version)
|
||||||
|
groupName string // a package group name (empty if no Grouper)
|
||||||
}
|
}
|
||||||
|
|
||||||
// installGo returns a shell snippet that installs Go from the official tarball.
|
// installGo returns a shell snippet that installs Go from the official tarball.
|
||||||
@@ -41,32 +50,60 @@ var distros = []distroTest{
|
|||||||
{
|
{
|
||||||
name: "debian-apt",
|
name: "debian-apt",
|
||||||
image: "debian:bookworm",
|
image: "debian:bookworm",
|
||||||
setup: installGo("apt-get update && apt-get install -y sudo tree curl wget"),
|
setup: installGo("apt-get update && apt-get install -y sudo tree curl wget dpkg"),
|
||||||
packages: "./apt/ ./dpkg/ ./detect/",
|
packages: "./apt/ ./dpkg/ ./detect/",
|
||||||
|
testPkg: "tree",
|
||||||
|
searchQuery: "curl",
|
||||||
|
infoPkg: "bash",
|
||||||
|
knownFile: "/usr/bin/tree",
|
||||||
|
knownFileOwner: "tree",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "alpine-apk",
|
name: "alpine-apk",
|
||||||
image: "alpine:latest",
|
image: "alpine:latest",
|
||||||
setup: installGo("apk add --no-cache sudo tree bash wget libc6-compat"),
|
setup: installGo("apk add --no-cache sudo tree bash wget libc6-compat curl"),
|
||||||
packages: "./apk/ ./detect/",
|
packages: "./apk/ ./detect/",
|
||||||
|
testPkg: "tree",
|
||||||
|
searchQuery: "curl",
|
||||||
|
infoPkg: "bash",
|
||||||
|
knownFile: "/usr/bin/tree",
|
||||||
|
knownFileOwner: "tree",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "archlinux-pacman",
|
name: "archlinux-pacman",
|
||||||
image: "archlinux:latest",
|
image: "archlinux:latest",
|
||||||
setup: installGo("pacman -Syu --noconfirm && pacman -S --noconfirm sudo tree wget"),
|
setup: installGo("pacman -Syu --noconfirm && pacman -S --noconfirm sudo tree wget"),
|
||||||
packages: "./pacman/ ./detect/",
|
packages: "./pacman/ ./detect/",
|
||||||
|
testPkg: "tree",
|
||||||
|
searchQuery: "curl",
|
||||||
|
infoPkg: "bash",
|
||||||
|
knownFile: "/usr/bin/tree",
|
||||||
|
knownFileOwner: "tree",
|
||||||
|
groupName: "base-devel",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "fedora-dnf4",
|
name: "fedora-dnf4",
|
||||||
image: "fedora:39",
|
image: "fedora:39",
|
||||||
setup: installGo("dnf install -y tree sudo wget"),
|
setup: installGo("dnf install -y tree sudo wget"),
|
||||||
packages: "./dnf/ ./rpm/ ./detect/",
|
packages: "./dnf/ ./rpm/ ./detect/",
|
||||||
|
testPkg: "tree",
|
||||||
|
searchQuery: "curl",
|
||||||
|
infoPkg: "bash",
|
||||||
|
knownFile: "/usr/bin/tree",
|
||||||
|
knownFileOwner: "tree",
|
||||||
|
groupName: "Development Tools",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "fedora-dnf5",
|
name: "fedora-dnf5",
|
||||||
image: "fedora:latest",
|
image: "fedora:latest",
|
||||||
setup: installGo("dnf install -y tree sudo wget"),
|
setup: installGo("dnf install -y tree sudo wget"),
|
||||||
packages: "./dnf/ ./rpm/ ./detect/",
|
packages: "./dnf/ ./rpm/ ./detect/",
|
||||||
|
testPkg: "tree",
|
||||||
|
searchQuery: "curl",
|
||||||
|
infoPkg: "bash",
|
||||||
|
knownFile: "/usr/bin/tree",
|
||||||
|
knownFileOwner: "tree",
|
||||||
|
groupName: "Development Tools",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +116,7 @@ func TestContainers(t *testing.T) {
|
|||||||
d := d
|
d := d
|
||||||
t.Run(d.name, func(t *testing.T) {
|
t.Run(d.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
req := testcontainers.ContainerRequest{
|
req := testcontainers.ContainerRequest{
|
||||||
@@ -100,7 +137,6 @@ func TestContainers(t *testing.T) {
|
|||||||
_ = container.Terminate(ctx)
|
_ = container.Terminate(ctx)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Copy source into container
|
|
||||||
containerID := container.GetContainerID()
|
containerID := container.GetContainerID()
|
||||||
|
|
||||||
// Execute setup
|
// Execute setup
|
||||||
@@ -116,9 +152,10 @@ func TestContainers(t *testing.T) {
|
|||||||
t.Fatalf("docker cp failed: %v\n%s", err, out)
|
t.Fatalf("docker cp failed: %v\n%s", err, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run integration tests
|
// Run integration tests with coverage
|
||||||
testCmd := fmt.Sprintf(
|
testCmd := fmt.Sprintf(
|
||||||
"cd /src && export PATH=/usr/local/go/bin:$PATH && export GOPATH=/tmp/go && go test -v -tags integration -count=1 %s",
|
"cd /src && export PATH=/usr/local/go/bin:$PATH && export GOPATH=/tmp/go && "+
|
||||||
|
"go test -v -tags integration -count=1 -coverprofile=/tmp/coverage.out -coverpkg=./... %s 2>&1",
|
||||||
d.packages,
|
d.packages,
|
||||||
)
|
)
|
||||||
t.Logf("Running tests in %s: %s", d.name, testCmd)
|
t.Logf("Running tests in %s: %s", d.name, testCmd)
|
||||||
@@ -128,8 +165,7 @@ func TestContainers(t *testing.T) {
|
|||||||
t.Fatalf("test exec failed: %v", err)
|
t.Fatalf("test exec failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read output
|
buf := make([]byte, 128*1024)
|
||||||
buf := make([]byte, 64*1024)
|
|
||||||
var output2 strings.Builder
|
var output2 strings.Builder
|
||||||
for {
|
for {
|
||||||
n, readErr := reader.Read(buf)
|
n, readErr := reader.Read(buf)
|
||||||
@@ -145,6 +181,13 @@ func TestContainers(t *testing.T) {
|
|||||||
if exitCode != 0 {
|
if exitCode != 0 {
|
||||||
t.Fatalf("%s integration tests failed (exit %d)", d.name, exitCode)
|
t.Fatalf("%s integration tests failed (exit %d)", d.name, exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract coverage file
|
||||||
|
cpOut := exec.CommandContext(ctx, "docker", "cp",
|
||||||
|
containerID+":/tmp/coverage.out", fmt.Sprintf("coverage-%s.out", d.name))
|
||||||
|
if out, err := cpOut.CombinedOutput(); err != nil {
|
||||||
|
t.Logf("coverage extraction failed (non-fatal): %v\n%s", err, out)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,92 +19,274 @@ func TestIntegration_Pacman(t *testing.T) {
|
|||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
assert.Equal(t, "pacman", mgr.Name())
|
||||||
|
|
||||||
|
caps := snack.GetCapabilities(mgr)
|
||||||
|
assert.True(t, caps.VersionQuery, "pacman should support VersionQuery")
|
||||||
|
assert.True(t, caps.Clean, "pacman should support Clean")
|
||||||
|
assert.True(t, caps.FileOwnership, "pacman should support FileOwnership")
|
||||||
|
assert.True(t, caps.Groups, "pacman should support Groups")
|
||||||
|
assert.False(t, caps.Hold, "pacman should not support Hold")
|
||||||
|
assert.False(t, caps.RepoManagement, "pacman should not support RepoManagement")
|
||||||
|
assert.False(t, caps.KeyManagement, "pacman should not support KeyManagement")
|
||||||
|
assert.False(t, caps.NameNormalize, "pacman should not support NameNormalize")
|
||||||
|
|
||||||
t.Run("Update", func(t *testing.T) {
|
t.Run("Update", func(t *testing.T) {
|
||||||
err := mgr.Update(ctx)
|
require.NoError(t, mgr.Update(ctx))
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Search", func(t *testing.T) {
|
t.Run("Search", func(t *testing.T) {
|
||||||
pkgs, err := mgr.Search(ctx, "curl")
|
pkgs, err := mgr.Search(ctx, "curl")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, pkgs)
|
require.NotEmpty(t, pkgs)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, p := range pkgs {
|
||||||
|
if p.Name == "curl" {
|
||||||
|
found = true
|
||||||
|
assert.NotEmpty(t, p.Description)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "curl should appear in search results")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Search_NoResults", func(t *testing.T) {
|
||||||
|
pkgs, err := mgr.Search(ctx, "xyznonexistentpackage999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, pkgs)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Info", func(t *testing.T) {
|
t.Run("Info", func(t *testing.T) {
|
||||||
pkg, err := mgr.Info(ctx, "curl")
|
pkg, err := mgr.Info(ctx, "bash")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, pkg)
|
require.NotNil(t, pkg)
|
||||||
assert.Equal(t, "curl", pkg.Name)
|
assert.Equal(t, "bash", pkg.Name)
|
||||||
|
assert.NotEmpty(t, pkg.Version)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Install", func(t *testing.T) {
|
t.Run("Info_NotFound", func(t *testing.T) {
|
||||||
err := mgr.Install(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
_, err := mgr.Info(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Install_Single", func(t *testing.T) {
|
||||||
|
_ = mgr.Remove(ctx, snack.Targets("tree"))
|
||||||
|
err := mgr.Install(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IsInstalled", func(t *testing.T) {
|
t.Run("IsInstalled_True", func(t *testing.T) {
|
||||||
installed, err := mgr.IsInstalled(ctx, "tree")
|
installed, err := mgr.IsInstalled(ctx, "tree")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, installed)
|
assert.True(t, installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Version", func(t *testing.T) {
|
t.Run("IsInstalled_False", func(t *testing.T) {
|
||||||
|
installed, err := mgr.IsInstalled(ctx, "xyznonexistentpackage999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, installed)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Version_Installed", func(t *testing.T) {
|
||||||
ver, err := mgr.Version(ctx, "tree")
|
ver, err := mgr.Version(ctx, "tree")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, ver)
|
assert.NotEmpty(t, ver)
|
||||||
|
t.Logf("tree version: %s", ver)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("List", func(t *testing.T) {
|
t.Run("Version_NotInstalled", func(t *testing.T) {
|
||||||
|
_, err := mgr.Version(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("List_ContainsInstalled", func(t *testing.T) {
|
||||||
pkgs, err := mgr.List(ctx)
|
pkgs, err := mgr.List(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, pkgs)
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
for _, p := range pkgs {
|
for _, p := range pkgs {
|
||||||
if p.Name == "tree" {
|
if p.Name == "tree" {
|
||||||
found = true
|
found = true
|
||||||
|
assert.NotEmpty(t, p.Version)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.True(t, found, "tree should be in installed list")
|
assert.True(t, found, "tree should be in installed list")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Remove", func(t *testing.T) {
|
t.Run("Install_Multiple", func(t *testing.T) {
|
||||||
err := mgr.Remove(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
err := mgr.Install(ctx, snack.Targets("tree", "less"), snack.WithAssumeYes())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, pkg := range []string{"tree", "less"} {
|
||||||
|
installed, err := mgr.IsInstalled(ctx, pkg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, installed, "%s should be installed", pkg)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IsInstalled_After_Remove", func(t *testing.T) {
|
t.Run("Purge", func(t *testing.T) {
|
||||||
|
err := mgr.Purge(ctx, snack.Targets("less"), snack.WithAssumeYes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
installed, err := mgr.IsInstalled(ctx, "less")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, installed)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Remove", func(t *testing.T) {
|
||||||
|
err := mgr.Remove(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
installed, err := mgr.IsInstalled(ctx, "tree")
|
installed, err := mgr.IsInstalled(ctx, "tree")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, installed)
|
assert.False(t, installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Capabilities", func(t *testing.T) {
|
// --- VersionQuerier ---
|
||||||
if vq, ok := mgr.(snack.VersionQuerier); ok {
|
t.Run("VersionQuerier", func(t *testing.T) {
|
||||||
|
vq, ok := mgr.(snack.VersionQuerier)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
t.Run("LatestVersion", func(t *testing.T) {
|
||||||
ver, err := vq.LatestVersion(ctx, "curl")
|
ver, err := vq.LatestVersion(ctx, "curl")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, ver)
|
assert.NotEmpty(t, ver)
|
||||||
|
t.Logf("curl latest: %s", ver)
|
||||||
|
})
|
||||||
|
|
||||||
upgrades, err := vq.ListUpgrades(ctx)
|
t.Run("LatestVersion_NotFound", func(t *testing.T) {
|
||||||
|
_, err := vq.LatestVersion(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ListUpgrades", func(t *testing.T) {
|
||||||
|
pkgs, err := vq.ListUpgrades(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_ = upgrades
|
t.Logf("upgradable packages: %d", len(pkgs))
|
||||||
}
|
})
|
||||||
|
|
||||||
if cl, ok := mgr.(snack.Cleaner); ok {
|
t.Run("UpgradeAvailable", func(t *testing.T) {
|
||||||
err := cl.Clean(ctx)
|
avail, err := vq.UpgradeAvailable(ctx, "bash")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
_ = avail
|
||||||
|
})
|
||||||
|
|
||||||
if fo, ok := mgr.(snack.FileOwner); ok {
|
t.Run("VersionCmp", func(t *testing.T) {
|
||||||
owner, err := fo.Owner(ctx, "/usr/bin/pacman")
|
tests := []struct {
|
||||||
if err == nil {
|
v1, v2 string
|
||||||
assert.NotEmpty(t, owner)
|
want int
|
||||||
|
}{
|
||||||
|
{"1.0.0-1", "1.0.0-2", -1},
|
||||||
|
{"2.0.0-1", "1.0.0-1", 1},
|
||||||
|
{"1.0.0-1", "1.0.0-1", 0},
|
||||||
}
|
}
|
||||||
}
|
for _, tt := range tests {
|
||||||
|
cmp, err := vq.VersionCmp(ctx, tt.v1, tt.v2)
|
||||||
if g, ok := mgr.(snack.Grouper); ok {
|
require.NoError(t, err, "VersionCmp(%s, %s)", tt.v1, tt.v2)
|
||||||
groups, err := g.GroupList(ctx)
|
assert.Equal(t, tt.want, cmp, "VersionCmp(%s, %s)", tt.v1, tt.v2)
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, groups)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Cleaner ---
|
||||||
|
t.Run("Cleaner", func(t *testing.T) {
|
||||||
|
cl, ok := mgr.(snack.Cleaner)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
t.Run("Autoremove", func(t *testing.T) {
|
||||||
|
err := cl.Autoremove(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Clean", func(t *testing.T) {
|
||||||
|
err := cl.Clean(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- FileOwner ---
|
||||||
|
t.Run("FileOwner", func(t *testing.T) {
|
||||||
|
fo, ok := mgr.(snack.FileOwner)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
_ = mgr.Install(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
|
||||||
|
t.Run("FileList", func(t *testing.T) {
|
||||||
|
files, err := fo.FileList(ctx, "tree")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, files)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, f := range files {
|
||||||
|
if f == "/usr/bin/tree" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "/usr/bin/tree should be in file list")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FileList_NotInstalled", func(t *testing.T) {
|
||||||
|
_, err := fo.FileList(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Owner", func(t *testing.T) {
|
||||||
|
owner, err := fo.Owner(ctx, "/usr/bin/tree")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, owner, "tree")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Owner_NotFound", func(t *testing.T) {
|
||||||
|
_, err := fo.Owner(ctx, "/nonexistent/path/xyz")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
_ = mgr.Remove(ctx, snack.Targets("tree"), snack.WithAssumeYes())
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Grouper ---
|
||||||
|
t.Run("Grouper", func(t *testing.T) {
|
||||||
|
g, ok := mgr.(snack.Grouper)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
t.Run("GroupList", func(t *testing.T) {
|
||||||
|
groups, err := g.GroupList(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// Minimal containers may have no groups
|
||||||
|
t.Logf("groups: %d", len(groups))
|
||||||
|
if len(groups) == 0 {
|
||||||
|
t.Skip("no groups available in this container")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GroupInfo", func(t *testing.T) {
|
||||||
|
groups, err := g.GroupList(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if len(groups) == 0 {
|
||||||
|
t.Skip("no groups available")
|
||||||
|
}
|
||||||
|
pkgs, err := g.GroupInfo(ctx, groups[0])
|
||||||
|
if err == nil {
|
||||||
|
t.Logf("group %q packages: %d", groups[0], len(pkgs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GroupInfo_NotFound", func(t *testing.T) {
|
||||||
|
// pacman returns empty for unknown groups, not necessarily an error
|
||||||
|
pkgs, err := g.GroupInfo(ctx, "xyznonexistentgroup999")
|
||||||
|
if err == nil {
|
||||||
|
assert.Empty(t, pkgs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Upgrade ---
|
||||||
|
t.Run("Upgrade", func(t *testing.T) {
|
||||||
|
err := mgr.Upgrade(ctx, snack.WithAssumeYes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestIntegration_Pkg(t *testing.T) {
|
func TestIntegration_Pkg(t *testing.T) {
|
||||||
mgr := pkg.New()
|
var mgr snack.Manager = pkg.New()
|
||||||
if !mgr.Available() {
|
if !mgr.Available() {
|
||||||
t.Skip("pkg not available")
|
t.Skip("pkg not available")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,22 +19,56 @@ func TestIntegration_RPM(t *testing.T) {
|
|||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
assert.Equal(t, "rpm", mgr.Name())
|
||||||
|
|
||||||
|
caps := snack.GetCapabilities(mgr)
|
||||||
|
assert.True(t, caps.FileOwnership, "rpm should support FileOwnership")
|
||||||
|
assert.True(t, caps.NameNormalize, "rpm should support NameNormalize")
|
||||||
|
assert.False(t, caps.VersionQuery)
|
||||||
|
assert.False(t, caps.Hold)
|
||||||
|
assert.False(t, caps.Clean)
|
||||||
|
assert.False(t, caps.RepoManagement)
|
||||||
|
assert.False(t, caps.KeyManagement)
|
||||||
|
assert.False(t, caps.Groups)
|
||||||
|
|
||||||
t.Run("List", func(t *testing.T) {
|
t.Run("List", func(t *testing.T) {
|
||||||
pkgs, err := mgr.List(ctx)
|
pkgs, err := mgr.List(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, pkgs)
|
require.NotEmpty(t, pkgs)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, p := range pkgs {
|
||||||
|
if p.Name == "bash" {
|
||||||
|
found = true
|
||||||
|
assert.NotEmpty(t, p.Version)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "bash should be in list")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IsInstalled", func(t *testing.T) {
|
t.Run("IsInstalled_True", func(t *testing.T) {
|
||||||
installed, err := mgr.IsInstalled(ctx, "bash")
|
installed, err := mgr.IsInstalled(ctx, "bash")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, installed)
|
assert.True(t, installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("IsInstalled_False", func(t *testing.T) {
|
||||||
|
installed, err := mgr.IsInstalled(ctx, "xyznonexistentpackage999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, installed)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Version", func(t *testing.T) {
|
t.Run("Version", func(t *testing.T) {
|
||||||
ver, err := mgr.Version(ctx, "bash")
|
ver, err := mgr.Version(ctx, "bash")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, ver)
|
assert.NotEmpty(t, ver)
|
||||||
|
t.Logf("bash version: %s", ver)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Version_NotInstalled", func(t *testing.T) {
|
||||||
|
_, err := mgr.Version(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Info", func(t *testing.T) {
|
t.Run("Info", func(t *testing.T) {
|
||||||
@@ -42,17 +76,92 @@ func TestIntegration_RPM(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, pkg)
|
require.NotNil(t, pkg)
|
||||||
assert.Equal(t, "bash", pkg.Name)
|
assert.Equal(t, "bash", pkg.Name)
|
||||||
|
assert.NotEmpty(t, pkg.Version)
|
||||||
|
assert.NotEmpty(t, pkg.Description)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("FileOwner", func(t *testing.T) {
|
t.Run("Info_NotFound", func(t *testing.T) {
|
||||||
if fo, ok := mgr.(snack.FileOwner); ok {
|
_, err := mgr.Info(ctx, "xyznonexistentpackage999")
|
||||||
owner, err := fo.Owner(ctx, "/bin/bash")
|
assert.Error(t, err)
|
||||||
require.NoError(t, err)
|
})
|
||||||
assert.NotEmpty(t, owner)
|
|
||||||
|
|
||||||
|
t.Run("Search", func(t *testing.T) {
|
||||||
|
pkgs, err := mgr.Search(ctx, "bash")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, pkgs)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Search_NoResults", func(t *testing.T) {
|
||||||
|
pkgs, err := mgr.Search(ctx, "xyznonexistentpackage999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, pkgs)
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- FileOwner ---
|
||||||
|
t.Run("FileOwner", func(t *testing.T) {
|
||||||
|
fo, ok := mgr.(snack.FileOwner)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
t.Run("FileList", func(t *testing.T) {
|
||||||
files, err := fo.FileList(ctx, "bash")
|
files, err := fo.FileList(ctx, "bash")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, files)
|
require.NotEmpty(t, files)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FileList_NotInstalled", func(t *testing.T) {
|
||||||
|
_, err := fo.FileList(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Owner", func(t *testing.T) {
|
||||||
|
owner, err := fo.Owner(ctx, "/usr/bin/bash")
|
||||||
|
if err != nil {
|
||||||
|
// Try /bin/bash for older distros
|
||||||
|
owner, err = fo.Owner(ctx, "/bin/bash")
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, owner, "bash")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Owner_NotFound", func(t *testing.T) {
|
||||||
|
_, err := fo.Owner(ctx, "/nonexistent/path/xyz")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- NameNormalizer ---
|
||||||
|
t.Run("NameNormalizer", func(t *testing.T) {
|
||||||
|
nn, ok := mgr.(snack.NameNormalizer)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input, wantName, wantArch string
|
||||||
|
}{
|
||||||
|
{"curl.x86_64", "curl", "x86_64"},
|
||||||
|
{"bash.aarch64", "bash", "aarch64"},
|
||||||
|
{"python3", "python3", ""},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run("NormalizeName_"+tt.input, func(t *testing.T) {
|
||||||
|
got := nn.NormalizeName(tt.input)
|
||||||
|
assert.Equal(t, tt.wantName, got)
|
||||||
|
})
|
||||||
|
t.Run("ParseArch_"+tt.input, func(t *testing.T) {
|
||||||
|
name, arch := nn.ParseArch(tt.input)
|
||||||
|
assert.Equal(t, tt.wantName, name)
|
||||||
|
assert.Equal(t, tt.wantArch, arch)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// --- Operations rpm doesn't support ---
|
||||||
|
t.Run("Install_Unsupported", func(t *testing.T) {
|
||||||
|
err := mgr.Install(ctx, snack.Targets("tree"))
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Update", func(t *testing.T) {
|
||||||
|
err := mgr.Update(ctx)
|
||||||
|
_ = err
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
117
snack_test.go
Normal file
117
snack_test.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package snack_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gogrlx/snack"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTargets(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
names []string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{nil, 0},
|
||||||
|
{[]string{}, 0},
|
||||||
|
{[]string{"curl"}, 1},
|
||||||
|
{[]string{"curl", "wget", "tree"}, 3},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
targets := snack.Targets(tt.names...)
|
||||||
|
assert.Len(t, targets, tt.want)
|
||||||
|
for i, tgt := range targets {
|
||||||
|
assert.Equal(t, tgt.Name, tt.names[i])
|
||||||
|
assert.Empty(t, tgt.Version)
|
||||||
|
assert.Empty(t, tgt.FromRepo)
|
||||||
|
assert.Empty(t, tgt.Source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetNames(t *testing.T) {
|
||||||
|
targets := []snack.Target{
|
||||||
|
{Name: "curl", Version: "8.0"},
|
||||||
|
{Name: "wget"},
|
||||||
|
{Name: "tree", FromRepo: "main"},
|
||||||
|
}
|
||||||
|
names := snack.TargetNames(targets)
|
||||||
|
assert.Equal(t, []string{"curl", "wget", "tree"}, names)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetNames_Empty(t *testing.T) {
|
||||||
|
names := snack.TargetNames(nil)
|
||||||
|
assert.Empty(t, names)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyOptions(t *testing.T) {
|
||||||
|
t.Run("defaults", func(t *testing.T) {
|
||||||
|
o := snack.ApplyOptions()
|
||||||
|
assert.False(t, o.Sudo)
|
||||||
|
assert.False(t, o.AssumeYes)
|
||||||
|
assert.False(t, o.DryRun)
|
||||||
|
assert.False(t, o.Verbose)
|
||||||
|
assert.False(t, o.Refresh)
|
||||||
|
assert.False(t, o.Reinstall)
|
||||||
|
assert.Empty(t, o.Root)
|
||||||
|
assert.Empty(t, o.FromRepo)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("all options", func(t *testing.T) {
|
||||||
|
o := snack.ApplyOptions(
|
||||||
|
snack.WithSudo(),
|
||||||
|
snack.WithAssumeYes(),
|
||||||
|
snack.WithDryRun(),
|
||||||
|
snack.WithVerbose(),
|
||||||
|
snack.WithRefresh(),
|
||||||
|
snack.WithReinstall(),
|
||||||
|
snack.WithRoot("/mnt"),
|
||||||
|
snack.WithFromRepo("testing"),
|
||||||
|
)
|
||||||
|
assert.True(t, o.Sudo)
|
||||||
|
assert.True(t, o.AssumeYes)
|
||||||
|
assert.True(t, o.DryRun)
|
||||||
|
assert.True(t, o.Verbose)
|
||||||
|
assert.True(t, o.Refresh)
|
||||||
|
assert.True(t, o.Reinstall)
|
||||||
|
assert.Equal(t, "/mnt", o.Root)
|
||||||
|
assert.Equal(t, "testing", o.FromRepo)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCapabilities_NilSafe(t *testing.T) {
|
||||||
|
// GetCapabilities should work on any Manager implementation
|
||||||
|
// We can't easily test with nil, but we can verify the struct fields
|
||||||
|
caps := snack.Capabilities{}
|
||||||
|
assert.False(t, caps.VersionQuery)
|
||||||
|
assert.False(t, caps.Hold)
|
||||||
|
assert.False(t, caps.Clean)
|
||||||
|
assert.False(t, caps.FileOwnership)
|
||||||
|
assert.False(t, caps.RepoManagement)
|
||||||
|
assert.False(t, caps.KeyManagement)
|
||||||
|
assert.False(t, caps.Groups)
|
||||||
|
assert.False(t, caps.NameNormalize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrors(t *testing.T) {
|
||||||
|
// Verify error sentinels are distinct
|
||||||
|
errs := []error{
|
||||||
|
snack.ErrNotInstalled,
|
||||||
|
snack.ErrNotFound,
|
||||||
|
snack.ErrUnsupportedPlatform,
|
||||||
|
snack.ErrPermissionDenied,
|
||||||
|
snack.ErrAlreadyInstalled,
|
||||||
|
snack.ErrDependencyConflict,
|
||||||
|
snack.ErrManagerNotFound,
|
||||||
|
snack.ErrDaemonNotRunning,
|
||||||
|
}
|
||||||
|
for i, e1 := range errs {
|
||||||
|
assert.NotNil(t, e1)
|
||||||
|
assert.NotEmpty(t, e1.Error())
|
||||||
|
for j, e2 := range errs {
|
||||||
|
if i != j {
|
||||||
|
assert.NotEqual(t, e1, e2, "%v should not equal %v", e1, e2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,9 +19,20 @@ func TestIntegration_Snap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
assert.Equal(t, "snap", mgr.Name())
|
||||||
|
|
||||||
|
caps := snack.GetCapabilities(mgr)
|
||||||
|
assert.True(t, caps.VersionQuery, "snap should support VersionQuery")
|
||||||
|
assert.False(t, caps.Hold)
|
||||||
|
assert.False(t, caps.Clean)
|
||||||
|
assert.False(t, caps.FileOwnership)
|
||||||
|
assert.False(t, caps.RepoManagement)
|
||||||
|
assert.False(t, caps.KeyManagement)
|
||||||
|
assert.False(t, caps.Groups)
|
||||||
|
assert.False(t, caps.NameNormalize)
|
||||||
|
|
||||||
t.Run("Update", func(t *testing.T) {
|
t.Run("Update", func(t *testing.T) {
|
||||||
err := mgr.Update(ctx)
|
require.NoError(t, mgr.Update(ctx))
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Search", func(t *testing.T) {
|
t.Run("Search", func(t *testing.T) {
|
||||||
@@ -30,51 +41,133 @@ func TestIntegration_Snap(t *testing.T) {
|
|||||||
require.NotEmpty(t, pkgs)
|
require.NotEmpty(t, pkgs)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Search_NoResults", func(t *testing.T) {
|
||||||
|
pkgs, err := mgr.Search(ctx, "xyznonexistentpackage999zzz")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, pkgs)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Info", func(t *testing.T) {
|
t.Run("Info", func(t *testing.T) {
|
||||||
pkg, err := mgr.Info(ctx, "hello-world")
|
pkg, err := mgr.Info(ctx, "hello-world")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, pkg)
|
require.NotNil(t, pkg)
|
||||||
assert.Equal(t, "hello-world", pkg.Name)
|
assert.Equal(t, "hello-world", pkg.Name)
|
||||||
|
// Version may be empty for uninstalled snaps queried via snap info
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Info_NotFound", func(t *testing.T) {
|
||||||
|
_, err := mgr.Info(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Install", func(t *testing.T) {
|
t.Run("Install", func(t *testing.T) {
|
||||||
err := mgr.Install(ctx, snack.Targets("hello-world"), snack.WithSudo(), snack.WithAssumeYes())
|
_ = mgr.Remove(ctx, snack.Targets("hello-world"), snack.WithSudo())
|
||||||
|
err := mgr.Install(ctx, snack.Targets("hello-world"), snack.WithSudo())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IsInstalled", func(t *testing.T) {
|
t.Run("IsInstalled_True", func(t *testing.T) {
|
||||||
installed, err := mgr.IsInstalled(ctx, "hello-world")
|
installed, err := mgr.IsInstalled(ctx, "hello-world")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, installed)
|
assert.True(t, installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Version", func(t *testing.T) {
|
t.Run("IsInstalled_False", func(t *testing.T) {
|
||||||
|
installed, err := mgr.IsInstalled(ctx, "xyznonexistentpackage999")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, installed)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Version_Installed", func(t *testing.T) {
|
||||||
ver, err := mgr.Version(ctx, "hello-world")
|
ver, err := mgr.Version(ctx, "hello-world")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, ver)
|
assert.NotEmpty(t, ver)
|
||||||
|
t.Logf("hello-world version: %s", ver)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("List", func(t *testing.T) {
|
t.Run("Version_NotInstalled", func(t *testing.T) {
|
||||||
|
_, err := mgr.Version(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("List_ContainsInstalled", func(t *testing.T) {
|
||||||
pkgs, err := mgr.List(ctx)
|
pkgs, err := mgr.List(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
found := false
|
found := false
|
||||||
for _, p := range pkgs {
|
for _, p := range pkgs {
|
||||||
if p.Name == "hello-world" {
|
if p.Name == "hello-world" {
|
||||||
found = true
|
found = true
|
||||||
|
assert.NotEmpty(t, p.Version)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.True(t, found, "hello-world should be in installed list")
|
assert.True(t, found, "hello-world should be in installed list")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Remove", func(t *testing.T) {
|
// --- VersionQuerier ---
|
||||||
err := mgr.Remove(ctx, snack.Targets("hello-world"), snack.WithSudo(), snack.WithAssumeYes())
|
t.Run("VersionQuerier", func(t *testing.T) {
|
||||||
|
vq, ok := mgr.(snack.VersionQuerier)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
t.Run("LatestVersion", func(t *testing.T) {
|
||||||
|
ver, err := vq.LatestVersion(ctx, "hello-world")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, ver)
|
||||||
|
t.Logf("hello-world latest: %s", ver)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IsInstalled_After_Remove", func(t *testing.T) {
|
t.Run("LatestVersion_NotFound", func(t *testing.T) {
|
||||||
|
_, err := vq.LatestVersion(ctx, "xyznonexistentpackage999")
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ListUpgrades", func(t *testing.T) {
|
||||||
|
pkgs, err := vq.ListUpgrades(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Logf("upgradable snaps: %d", len(pkgs))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpgradeAvailable", func(t *testing.T) {
|
||||||
|
avail, err := vq.UpgradeAvailable(ctx, "hello-world")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_ = avail
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("VersionCmp", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
v1, v2 string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"1.0", "2.0", -1},
|
||||||
|
{"2.0", "1.0", 1},
|
||||||
|
{"1.0", "1.0", 0},
|
||||||
|
{"1.0.1", "1.0.0", 1},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
cmp, err := vq.VersionCmp(ctx, tt.v1, tt.v2)
|
||||||
|
require.NoError(t, err, "VersionCmp(%s, %s)", tt.v1, tt.v2)
|
||||||
|
assert.Equal(t, tt.want, cmp, "VersionCmp(%s, %s)", tt.v1, tt.v2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Remove", func(t *testing.T) {
|
||||||
|
err := mgr.Remove(ctx, snack.Targets("hello-world"), snack.WithSudo())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
installed, err := mgr.IsInstalled(ctx, "hello-world")
|
installed, err := mgr.IsInstalled(ctx, "hello-world")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, installed)
|
assert.False(t, installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Purge", func(t *testing.T) {
|
||||||
|
_ = mgr.Install(ctx, snack.Targets("hello-world"), snack.WithSudo())
|
||||||
|
err := mgr.Purge(ctx, snack.Targets("hello-world"), snack.WithSudo())
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Upgrade", func(t *testing.T) {
|
||||||
|
err := mgr.Upgrade(ctx, snack.WithSudo())
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user