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
|
||||
- run: go build ./...
|
||||
- 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:
|
||||
name: Debian (apt)
|
||||
@@ -32,7 +36,12 @@ jobs:
|
||||
apt-get update
|
||||
apt-get install -y sudo tree curl
|
||||
- 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:
|
||||
name: Ubuntu (apt + snap)
|
||||
@@ -48,9 +57,14 @@ jobs:
|
||||
sudo apt-get install -y tree
|
||||
sudo snap install hello-world 2>/dev/null; sudo snap remove hello-world 2>/dev/null
|
||||
- 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)
|
||||
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:
|
||||
name: Fedora 39 (dnf4)
|
||||
@@ -65,7 +79,12 @@ jobs:
|
||||
run: |
|
||||
dnf install -y tree sudo
|
||||
- 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:
|
||||
name: Fedora latest (dnf5)
|
||||
@@ -80,7 +99,12 @@ jobs:
|
||||
run: |
|
||||
dnf install -y tree sudo
|
||||
- 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:
|
||||
name: Alpine (apk)
|
||||
@@ -95,7 +119,12 @@ jobs:
|
||||
run: |
|
||||
apk add --no-cache sudo tree bash
|
||||
- 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:
|
||||
name: Arch Linux (pacman)
|
||||
@@ -111,7 +140,12 @@ jobs:
|
||||
pacman -Syu --noconfirm
|
||||
pacman -S --noconfirm sudo tree
|
||||
- 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:
|
||||
name: Ubuntu + Flatpak
|
||||
@@ -130,4 +164,29 @@ jobs:
|
||||
sudo apt-get install -y flatpak
|
||||
sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
- 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()
|
||||
|
||||
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) {
|
||||
err := mgr.Update(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mgr.Update(ctx))
|
||||
})
|
||||
|
||||
t.Run("Search", func(t *testing.T) {
|
||||
pkgs, err := mgr.Search(ctx, "curl")
|
||||
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) {
|
||||
// apk info only works on installed packages, use "tree" after install
|
||||
// or test with a pre-installed package like "busybox"
|
||||
t.Run("Search_NoResults", func(t *testing.T) {
|
||||
pkgs, err := mgr.Search(ctx, "xyznonexistentpackage999")
|
||||
// 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")
|
||||
if err != nil {
|
||||
t.Skip("busybox not installed, skipping Info test")
|
||||
t.Skip("busybox not installed")
|
||||
}
|
||||
require.NotNil(t, pkg)
|
||||
assert.Equal(t, "busybox", pkg.Name)
|
||||
assert.NotEmpty(t, pkg.Version)
|
||||
})
|
||||
|
||||
t.Run("Install", func(t *testing.T) {
|
||||
err := mgr.Install(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
||||
t.Run("Info_NotFound", func(t *testing.T) {
|
||||
_, 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)
|
||||
})
|
||||
|
||||
t.Run("IsInstalled", func(t *testing.T) {
|
||||
t.Run("IsInstalled_True", func(t *testing.T) {
|
||||
installed, err := mgr.IsInstalled(ctx, "tree")
|
||||
require.NoError(t, err)
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, pkgs)
|
||||
|
||||
found := false
|
||||
for _, p := range pkgs {
|
||||
if p.Name == "tree" {
|
||||
found = true
|
||||
assert.NotEmpty(t, p.Version)
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "tree should be in installed list")
|
||||
})
|
||||
|
||||
t.Run("Remove", func(t *testing.T) {
|
||||
err := mgr.Remove(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
||||
t.Run("Info_AfterInstall", func(t *testing.T) {
|
||||
pkg, err := mgr.Info(ctx, "tree")
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
assert.False(t, installed)
|
||||
})
|
||||
|
||||
t.Run("Capabilities", func(t *testing.T) {
|
||||
if vq, ok := mgr.(snack.VersionQuerier); ok {
|
||||
ver, err := vq.LatestVersion(ctx, "busybox")
|
||||
// --- VersionQuerier ---
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
_ = upgrades
|
||||
}
|
||||
t.Logf("upgradable packages: %d", len(pkgs))
|
||||
})
|
||||
|
||||
if cl, ok := mgr.(snack.Cleaner); ok {
|
||||
t.Run("UpgradeAvailable", func(t *testing.T) {
|
||||
avail, err := vq.UpgradeAvailable(ctx, "busybox")
|
||||
require.NoError(t, err)
|
||||
_ = avail
|
||||
})
|
||||
|
||||
t.Run("VersionCmp", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
v1, v2 string
|
||||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if fo, ok := mgr.(snack.FileOwner); ok {
|
||||
owner, err := fo.Owner(ctx, "/usr/bin/apk")
|
||||
if err == nil {
|
||||
assert.NotEmpty(t, owner)
|
||||
}
|
||||
}
|
||||
// --- 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()
|
||||
|
||||
// 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) {
|
||||
err := mgr.Update(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mgr.Update(ctx))
|
||||
})
|
||||
|
||||
t.Run("Search", func(t *testing.T) {
|
||||
pkgs, err := mgr.Search(ctx, "curl")
|
||||
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) {
|
||||
pkg, err := mgr.Info(ctx, "curl")
|
||||
pkg, err := mgr.Info(ctx, "bash")
|
||||
require.NoError(t, err)
|
||||
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) {
|
||||
err := mgr.Install(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
||||
t.Run("Info_NotFound", func(t *testing.T) {
|
||||
_, 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)
|
||||
})
|
||||
|
||||
t.Run("IsInstalled", func(t *testing.T) {
|
||||
t.Run("IsInstalled_True", func(t *testing.T) {
|
||||
installed, err := mgr.IsInstalled(ctx, "tree")
|
||||
require.NoError(t, err)
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, pkgs)
|
||||
|
||||
found := false
|
||||
for _, p := range pkgs {
|
||||
if p.Name == "tree" {
|
||||
found = true
|
||||
assert.NotEmpty(t, p.Version)
|
||||
assert.True(t, p.Installed)
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "tree should be in installed list")
|
||||
})
|
||||
|
||||
t.Run("Remove", func(t *testing.T) {
|
||||
err := mgr.Remove(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
||||
t.Run("Install_WithVersion", func(t *testing.T) {
|
||||
// Get current version and reinstall with explicit version
|
||||
ver, err := mgr.Version(ctx, "tree")
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
assert.False(t, installed)
|
||||
})
|
||||
|
||||
t.Run("Capabilities", func(t *testing.T) {
|
||||
if vq, ok := mgr.(snack.VersionQuerier); ok {
|
||||
t.Run("Install_Multiple", func(t *testing.T) {
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
_ = upgrades
|
||||
}
|
||||
// May be empty in a freshly updated container, that's fine
|
||||
t.Logf("upgradable packages: %d", len(pkgs))
|
||||
for _, p := range pkgs {
|
||||
assert.NotEmpty(t, p.Name)
|
||||
assert.NotEmpty(t, p.Version)
|
||||
}
|
||||
})
|
||||
|
||||
if cl, ok := mgr.(snack.Cleaner); ok {
|
||||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if fo, ok := mgr.(snack.FileOwner); ok {
|
||||
owner, err := fo.Owner(ctx, "/usr/bin/apt")
|
||||
if err == nil {
|
||||
assert.NotEmpty(t, owner)
|
||||
// --- 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) {
|
||||
mgr, err := detect.Default()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, mgr)
|
||||
t.Logf("Detected: %s", mgr.Name())
|
||||
t.Run("Default", func(t *testing.T) {
|
||||
mgr, err := detect.Default()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, mgr)
|
||||
assert.NotEmpty(t, mgr.Name())
|
||||
assert.True(t, mgr.Available())
|
||||
t.Logf("Detected default: %s", mgr.Name())
|
||||
})
|
||||
|
||||
all := detect.All()
|
||||
require.NotEmpty(t, all)
|
||||
for _, m := range all {
|
||||
t.Logf("Available: %s", m.Name())
|
||||
}
|
||||
t.Run("All", func(t *testing.T) {
|
||||
all := detect.All()
|
||||
require.NotEmpty(t, all)
|
||||
for _, m := range all {
|
||||
assert.NotEmpty(t, m.Name())
|
||||
assert.True(t, m.Available())
|
||||
t.Logf("Available: %s", m.Name())
|
||||
}
|
||||
})
|
||||
|
||||
caps := snack.GetCapabilities(mgr)
|
||||
t.Logf("Capabilities: %+v", caps)
|
||||
assert.NotEmpty(t, mgr.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)
|
||||
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())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func (d *DNF) Unhold(ctx context.Context, pkgs []string) error {
|
||||
|
||||
// ListHeld returns all currently held packages.
|
||||
func (d *DNF) ListHeld(ctx context.Context) ([]snack.Package, error) {
|
||||
return listHeld(ctx)
|
||||
return listHeld(ctx, d.v5)
|
||||
}
|
||||
|
||||
// Autoremove removes orphaned packages.
|
||||
|
||||
@@ -102,11 +102,14 @@ func unhold(ctx context.Context, pkgs []string) error {
|
||||
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{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dnf listHeld: %w", err)
|
||||
}
|
||||
if v5 {
|
||||
return parseVersionLockDNF5(out), nil
|
||||
}
|
||||
return parseVersionLock(out), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ func unhold(_ context.Context, _ []string) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func listHeld(_ context.Context) ([]snack.Package, error) {
|
||||
func listHeld(_ context.Context, _ bool) ([]snack.Package, error) {
|
||||
return nil, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
|
||||
@@ -27,104 +27,376 @@ func TestIntegration_DNF(t *testing.T) {
|
||||
}
|
||||
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) {
|
||||
err := mgr.Update(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mgr.Update(ctx))
|
||||
})
|
||||
|
||||
t.Run("Search", func(t *testing.T) {
|
||||
pkgs, err := mgr.Search(ctx, "curl")
|
||||
require.NoError(t, err)
|
||||
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) {
|
||||
pkg, err := mgr.Info(ctx, "curl")
|
||||
pkg, err := mgr.Info(ctx, "bash")
|
||||
require.NoError(t, err)
|
||||
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) {
|
||||
err := mgr.Install(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
||||
t.Run("Info_NotFound", func(t *testing.T) {
|
||||
_, 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)
|
||||
})
|
||||
|
||||
t.Run("IsInstalled", func(t *testing.T) {
|
||||
t.Run("IsInstalled_True", func(t *testing.T) {
|
||||
installed, err := mgr.IsInstalled(ctx, "tree")
|
||||
require.NoError(t, err)
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, pkgs)
|
||||
|
||||
found := false
|
||||
for _, p := range pkgs {
|
||||
if p.Name == "tree" {
|
||||
found = true
|
||||
assert.NotEmpty(t, p.Version)
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "tree should be in installed list")
|
||||
})
|
||||
|
||||
t.Run("Remove", func(t *testing.T) {
|
||||
err := mgr.Remove(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
||||
t.Run("Install_Multiple", func(t *testing.T) {
|
||||
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)
|
||||
})
|
||||
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
assert.False(t, installed)
|
||||
})
|
||||
|
||||
t.Run("Capabilities", func(t *testing.T) {
|
||||
if vq, ok := mgr.(snack.VersionQuerier); ok {
|
||||
// --- VersionQuerier ---
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
_ = upgrades
|
||||
}
|
||||
t.Logf("upgradable packages: %d", len(pkgs))
|
||||
for _, p := range pkgs {
|
||||
assert.NotEmpty(t, p.Name)
|
||||
assert.NotEmpty(t, p.Version)
|
||||
}
|
||||
})
|
||||
|
||||
if cl, ok := mgr.(snack.Cleaner); ok {
|
||||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if fo, ok := mgr.(snack.FileOwner); ok {
|
||||
owner, err := fo.Owner(ctx, "/usr/bin/dnf")
|
||||
if err == nil {
|
||||
assert.NotEmpty(t, owner)
|
||||
}
|
||||
}
|
||||
// --- FileOwner ---
|
||||
t.Run("FileOwner", func(t *testing.T) {
|
||||
fo, ok := mgr.(snack.FileOwner)
|
||||
require.True(t, ok)
|
||||
|
||||
if g, ok := mgr.(snack.Grouper); ok {
|
||||
groups, err := g.GroupList(ctx)
|
||||
_ = 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)
|
||||
_ = groups
|
||||
}
|
||||
require.NotEmpty(t, files)
|
||||
|
||||
if rm, ok := mgr.(snack.RepoManager); ok {
|
||||
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, "should have at least one repo")
|
||||
require.NotEmpty(t, repos)
|
||||
for _, r := range repos {
|
||||
assert.NotEmpty(t, r.ID)
|
||||
}
|
||||
t.Logf("repos: %d", len(repos))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if nn, ok := mgr.(snack.NameNormalizer); ok {
|
||||
got := nn.NormalizeName("curl.x86_64")
|
||||
assert.Equal(t, "curl", got)
|
||||
// --- 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
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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) {
|
||||
input := `repo id repo name status
|
||||
fedora Fedora 43 - x86_64 enabled
|
||||
|
||||
@@ -19,23 +19,57 @@ func TestIntegration_Dpkg(t *testing.T) {
|
||||
}
|
||||
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) {
|
||||
pkgs, err := mgr.List(ctx)
|
||||
require.NoError(t, err)
|
||||
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) {
|
||||
// bash should always be installed
|
||||
t.Run("IsInstalled_True", func(t *testing.T) {
|
||||
installed, err := mgr.IsInstalled(ctx, "bash")
|
||||
require.NoError(t, err)
|
||||
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) {
|
||||
ver, err := mgr.Version(ctx, "bash")
|
||||
require.NoError(t, err)
|
||||
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) {
|
||||
@@ -43,6 +77,12 @@ func TestIntegration_Dpkg(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pkg)
|
||||
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) {
|
||||
@@ -51,15 +91,86 @@ func TestIntegration_Dpkg(t *testing.T) {
|
||||
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) {
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, owner)
|
||||
assert.Contains(t, owner, "dpkg")
|
||||
})
|
||||
|
||||
files, err := fo.FileList(ctx, "bash")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, files)
|
||||
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: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,30 +15,43 @@ import (
|
||||
func TestIntegration_Flatpak(t *testing.T) {
|
||||
var mgr snack.Manager = flatpak.New()
|
||||
if !mgr.Available() {
|
||||
t.Skip("flatpak not available — install it first")
|
||||
t.Skip("flatpak not available")
|
||||
}
|
||||
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) {
|
||||
err := mgr.Update(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mgr.Update(ctx))
|
||||
})
|
||||
|
||||
// --- RepoManager ---
|
||||
t.Run("RepoManager", func(t *testing.T) {
|
||||
rm, ok := mgr.(snack.RepoManager)
|
||||
if !ok {
|
||||
t.Skip("RepoManager not implemented")
|
||||
}
|
||||
repos, err := rm.ListRepos(ctx)
|
||||
require.NoError(t, err)
|
||||
found := false
|
||||
for _, r := range repos {
|
||||
if r.Name == "flathub" || r.ID == "flathub" {
|
||||
found = true
|
||||
break
|
||||
require.True(t, ok)
|
||||
|
||||
t.Run("ListRepos", func(t *testing.T) {
|
||||
repos, err := rm.ListRepos(ctx)
|
||||
require.NoError(t, err)
|
||||
found := false
|
||||
for _, r := range repos {
|
||||
if r.Name == "flathub" || r.ID == "flathub" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
@@ -47,18 +60,54 @@ func TestIntegration_Flatpak(t *testing.T) {
|
||||
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) {
|
||||
err := mgr.Install(ctx, snack.Targets("com.github.tchx84.Flatseal"), snack.WithSudo(), snack.WithAssumeYes())
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
found := false
|
||||
@@ -68,7 +117,7 @@ func TestIntegration_Flatpak(t *testing.T) {
|
||||
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) {
|
||||
@@ -81,4 +130,25 @@ func TestIntegration_Flatpak(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
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
|
||||
|
||||
// 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.
|
||||
package snack_test
|
||||
@@ -22,11 +22,20 @@ import (
|
||||
|
||||
const goVersion = "1.26.0"
|
||||
|
||||
// distroTest describes a container environment and the test packages available in it.
|
||||
type distroTest struct {
|
||||
name string
|
||||
image string
|
||||
setup string // shell commands to install deps (Go installed separately)
|
||||
packages string // space-separated test directories
|
||||
setup string // shell commands to install deps
|
||||
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.
|
||||
@@ -39,34 +48,62 @@ func installGo(prereqs string) string {
|
||||
|
||||
var distros = []distroTest{
|
||||
{
|
||||
name: "debian-apt",
|
||||
image: "debian:bookworm",
|
||||
setup: installGo("apt-get update && apt-get install -y sudo tree curl wget"),
|
||||
packages: "./apt/ ./dpkg/ ./detect/",
|
||||
name: "debian-apt",
|
||||
image: "debian:bookworm",
|
||||
setup: installGo("apt-get update && apt-get install -y sudo tree curl wget dpkg"),
|
||||
packages: "./apt/ ./dpkg/ ./detect/",
|
||||
testPkg: "tree",
|
||||
searchQuery: "curl",
|
||||
infoPkg: "bash",
|
||||
knownFile: "/usr/bin/tree",
|
||||
knownFileOwner: "tree",
|
||||
},
|
||||
{
|
||||
name: "alpine-apk",
|
||||
image: "alpine:latest",
|
||||
setup: installGo("apk add --no-cache sudo tree bash wget libc6-compat"),
|
||||
packages: "./apk/ ./detect/",
|
||||
name: "alpine-apk",
|
||||
image: "alpine:latest",
|
||||
setup: installGo("apk add --no-cache sudo tree bash wget libc6-compat curl"),
|
||||
packages: "./apk/ ./detect/",
|
||||
testPkg: "tree",
|
||||
searchQuery: "curl",
|
||||
infoPkg: "bash",
|
||||
knownFile: "/usr/bin/tree",
|
||||
knownFileOwner: "tree",
|
||||
},
|
||||
{
|
||||
name: "archlinux-pacman",
|
||||
image: "archlinux:latest",
|
||||
setup: installGo("pacman -Syu --noconfirm && pacman -S --noconfirm sudo tree wget"),
|
||||
packages: "./pacman/ ./detect/",
|
||||
name: "archlinux-pacman",
|
||||
image: "archlinux:latest",
|
||||
setup: installGo("pacman -Syu --noconfirm && pacman -S --noconfirm sudo tree wget"),
|
||||
packages: "./pacman/ ./detect/",
|
||||
testPkg: "tree",
|
||||
searchQuery: "curl",
|
||||
infoPkg: "bash",
|
||||
knownFile: "/usr/bin/tree",
|
||||
knownFileOwner: "tree",
|
||||
groupName: "base-devel",
|
||||
},
|
||||
{
|
||||
name: "fedora-dnf4",
|
||||
image: "fedora:39",
|
||||
setup: installGo("dnf install -y tree sudo wget"),
|
||||
packages: "./dnf/ ./rpm/ ./detect/",
|
||||
name: "fedora-dnf4",
|
||||
image: "fedora:39",
|
||||
setup: installGo("dnf install -y tree sudo wget"),
|
||||
packages: "./dnf/ ./rpm/ ./detect/",
|
||||
testPkg: "tree",
|
||||
searchQuery: "curl",
|
||||
infoPkg: "bash",
|
||||
knownFile: "/usr/bin/tree",
|
||||
knownFileOwner: "tree",
|
||||
groupName: "Development Tools",
|
||||
},
|
||||
{
|
||||
name: "fedora-dnf5",
|
||||
image: "fedora:latest",
|
||||
setup: installGo("dnf install -y tree sudo wget"),
|
||||
packages: "./dnf/ ./rpm/ ./detect/",
|
||||
name: "fedora-dnf5",
|
||||
image: "fedora:latest",
|
||||
setup: installGo("dnf install -y tree sudo wget"),
|
||||
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
|
||||
t.Run(d.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
req := testcontainers.ContainerRequest{
|
||||
@@ -100,7 +137,6 @@ func TestContainers(t *testing.T) {
|
||||
_ = container.Terminate(ctx)
|
||||
}()
|
||||
|
||||
// Copy source into container
|
||||
containerID := container.GetContainerID()
|
||||
|
||||
// Execute setup
|
||||
@@ -116,9 +152,10 @@ func TestContainers(t *testing.T) {
|
||||
t.Fatalf("docker cp failed: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// Run integration tests
|
||||
// Run integration tests with coverage
|
||||
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,
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
||||
// Read output
|
||||
buf := make([]byte, 64*1024)
|
||||
buf := make([]byte, 128*1024)
|
||||
var output2 strings.Builder
|
||||
for {
|
||||
n, readErr := reader.Read(buf)
|
||||
@@ -145,6 +181,13 @@ func TestContainers(t *testing.T) {
|
||||
if exitCode != 0 {
|
||||
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()
|
||||
|
||||
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) {
|
||||
err := mgr.Update(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mgr.Update(ctx))
|
||||
})
|
||||
|
||||
t.Run("Search", func(t *testing.T) {
|
||||
pkgs, err := mgr.Search(ctx, "curl")
|
||||
require.NoError(t, err)
|
||||
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) {
|
||||
pkg, err := mgr.Info(ctx, "curl")
|
||||
pkg, err := mgr.Info(ctx, "bash")
|
||||
require.NoError(t, err)
|
||||
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) {
|
||||
err := mgr.Install(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
||||
t.Run("Info_NotFound", func(t *testing.T) {
|
||||
_, 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)
|
||||
})
|
||||
|
||||
t.Run("IsInstalled", func(t *testing.T) {
|
||||
t.Run("IsInstalled_True", func(t *testing.T) {
|
||||
installed, err := mgr.IsInstalled(ctx, "tree")
|
||||
require.NoError(t, err)
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, pkgs)
|
||||
|
||||
found := false
|
||||
for _, p := range pkgs {
|
||||
if p.Name == "tree" {
|
||||
found = true
|
||||
assert.NotEmpty(t, p.Version)
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "tree should be in installed list")
|
||||
})
|
||||
|
||||
t.Run("Remove", func(t *testing.T) {
|
||||
err := mgr.Remove(ctx, snack.Targets("tree"), snack.WithSudo(), snack.WithAssumeYes())
|
||||
t.Run("Install_Multiple", func(t *testing.T) {
|
||||
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("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")
|
||||
require.NoError(t, err)
|
||||
assert.False(t, installed)
|
||||
})
|
||||
|
||||
t.Run("Capabilities", func(t *testing.T) {
|
||||
if vq, ok := mgr.(snack.VersionQuerier); ok {
|
||||
// --- VersionQuerier ---
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
_ = upgrades
|
||||
}
|
||||
t.Logf("upgradable packages: %d", len(pkgs))
|
||||
})
|
||||
|
||||
if cl, ok := mgr.(snack.Cleaner); ok {
|
||||
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) {
|
||||
tests := []struct {
|
||||
v1, v2 string
|
||||
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)
|
||||
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) {
|
||||
err := cl.Autoremove(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Clean", func(t *testing.T) {
|
||||
err := cl.Clean(ctx)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if fo, ok := mgr.(snack.FileOwner); ok {
|
||||
owner, err := fo.Owner(ctx, "/usr/bin/pacman")
|
||||
if err == nil {
|
||||
assert.NotEmpty(t, owner)
|
||||
// --- 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")
|
||||
})
|
||||
|
||||
if g, ok := mgr.(snack.Grouper); ok {
|
||||
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)
|
||||
assert.NotEmpty(t, groups)
|
||||
}
|
||||
// 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) {
|
||||
mgr := pkg.New()
|
||||
var mgr snack.Manager = pkg.New()
|
||||
if !mgr.Available() {
|
||||
t.Skip("pkg not available")
|
||||
}
|
||||
|
||||
@@ -19,22 +19,56 @@ func TestIntegration_RPM(t *testing.T) {
|
||||
}
|
||||
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) {
|
||||
pkgs, err := mgr.List(ctx)
|
||||
require.NoError(t, err)
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
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) {
|
||||
ver, err := mgr.Version(ctx, "bash")
|
||||
require.NoError(t, err)
|
||||
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) {
|
||||
@@ -42,17 +76,92 @@ func TestIntegration_RPM(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pkg)
|
||||
assert.Equal(t, "bash", pkg.Name)
|
||||
assert.NotEmpty(t, pkg.Version)
|
||||
assert.NotEmpty(t, pkg.Description)
|
||||
})
|
||||
|
||||
t.Run("FileOwner", func(t *testing.T) {
|
||||
if fo, ok := mgr.(snack.FileOwner); ok {
|
||||
owner, err := fo.Owner(ctx, "/bin/bash")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, owner)
|
||||
t.Run("Info_NotFound", func(t *testing.T) {
|
||||
_, err := mgr.Info(ctx, "xyznonexistentpackage999")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
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")
|
||||
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()
|
||||
|
||||
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) {
|
||||
err := mgr.Update(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mgr.Update(ctx))
|
||||
})
|
||||
|
||||
t.Run("Search", func(t *testing.T) {
|
||||
@@ -30,51 +41,133 @@ func TestIntegration_Snap(t *testing.T) {
|
||||
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) {
|
||||
pkg, err := mgr.Info(ctx, "hello-world")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pkg)
|
||||
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) {
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("IsInstalled", func(t *testing.T) {
|
||||
t.Run("IsInstalled_True", func(t *testing.T) {
|
||||
installed, err := mgr.IsInstalled(ctx, "hello-world")
|
||||
require.NoError(t, err)
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
found := false
|
||||
for _, p := range pkgs {
|
||||
if p.Name == "hello-world" {
|
||||
found = true
|
||||
assert.NotEmpty(t, p.Version)
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "hello-world should be in installed list")
|
||||
})
|
||||
|
||||
t.Run("Remove", func(t *testing.T) {
|
||||
err := mgr.Remove(ctx, snack.Targets("hello-world"), snack.WithSudo(), snack.WithAssumeYes())
|
||||
require.NoError(t, err)
|
||||
// --- VersionQuerier ---
|
||||
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)
|
||||
assert.NotEmpty(t, ver)
|
||||
t.Logf("hello-world latest: %s", ver)
|
||||
})
|
||||
|
||||
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("IsInstalled_After_Remove", func(t *testing.T) {
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
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