Merge pull request #20 from gogrlx/cd/exhaustive-containertests

test: exhaustive integration tests + codecov
This commit is contained in:
2026-02-25 22:18:55 -05:00
committed by GitHub
19 changed files with 1773 additions and 211 deletions

View File

@@ -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
View File

@@ -0,0 +1,2 @@
coverage.out
coverage.html

View File

@@ -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) {
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) err := cl.Clean(ctx)
require.NoError(t, err) require.NoError(t, err)
} })
})
if fo, ok := mgr.(snack.FileOwner); ok { // --- FileOwner ---
owner, err := fo.Owner(ctx, "/usr/bin/apk") t.Run("FileOwner", func(t *testing.T) {
if err == nil { fo, ok := mgr.(snack.FileOwner)
assert.NotEmpty(t, owner) 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)
}) })
} }

View File

@@ -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 {
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) err := cl.Clean(ctx)
require.NoError(t, err) require.NoError(t, err)
} })
})
if fo, ok := mgr.(snack.FileOwner); ok { // --- FileOwner ---
owner, err := fo.Owner(ctx, "/usr/bin/apt") t.Run("FileOwner", func(t *testing.T) {
if err == nil { fo, ok := mgr.(snack.FileOwner)
assert.NotEmpty(t, owner) 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())
} }

View File

@@ -12,18 +12,53 @@ import (
) )
func TestIntegration_Detect(t *testing.T) { func TestIntegration_Detect(t *testing.T) {
mgr, err := detect.Default() t.Run("Default", func(t *testing.T) {
require.NoError(t, err) mgr, err := detect.Default()
require.NotNil(t, mgr) require.NoError(t, err)
t.Logf("Detected: %s", mgr.Name()) require.NotNil(t, mgr)
assert.NotEmpty(t, mgr.Name())
assert.True(t, mgr.Available())
t.Logf("Detected default: %s", mgr.Name())
})
all := detect.All() t.Run("All", func(t *testing.T) {
require.NotEmpty(t, all) all := detect.All()
for _, m := range all { require.NotEmpty(t, all)
t.Logf("Available: %s", m.Name()) 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.Run("ByName_Valid", func(t *testing.T) {
t.Logf("Capabilities: %+v", caps) all := detect.All()
assert.NotEmpty(t, mgr.Name()) 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())
})
} }

View File

@@ -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.

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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)
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) err := cl.Clean(ctx)
require.NoError(t, err) require.NoError(t, err)
} })
})
if fo, ok := mgr.(snack.FileOwner); ok { // --- FileOwner ---
owner, err := fo.Owner(ctx, "/usr/bin/dnf") t.Run("FileOwner", func(t *testing.T) {
if err == nil { fo, ok := mgr.(snack.FileOwner)
assert.NotEmpty(t, owner) require.True(t, ok)
}
}
if g, ok := mgr.(snack.Grouper); ok { _ = mgr.Install(ctx, snack.Targets("tree"), snack.WithAssumeYes())
groups, err := g.GroupList(ctx)
t.Run("FileList", func(t *testing.T) {
files, err := fo.FileList(ctx, "tree")
require.NoError(t, err) 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) repos, err := rm.ListRepos(ctx)
require.NoError(t, err) 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)) t.Logf("repos: %d", len(repos))
} })
})
if nn, ok := mgr.(snack.NameNormalizer); ok { // --- KeyManager ---
got := nn.NormalizeName("curl.x86_64") t.Run("KeyManager", func(t *testing.T) {
assert.Equal(t, "curl", got) 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)
})
} }

View File

@@ -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:
// //

View File

@@ -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

View File

@@ -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
})
} }

View File

@@ -15,30 +15,43 @@ 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
for _, r := range repos { for _, r := range repos {
if r.Name == "flathub" || r.ID == "flathub" { if r.Name == "flathub" || r.ID == "flathub" {
found = true found = true
break 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) { t.Run("Search", func(t *testing.T) {
@@ -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)
})
} }

View File

@@ -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.
@@ -39,34 +48,62 @@ func installGo(prereqs string) string {
var distros = []distroTest{ 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)
}
}) })
} }
} }

View File

@@ -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) {
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) err := cl.Clean(ctx)
require.NoError(t, err) require.NoError(t, err)
} })
})
if fo, ok := mgr.(snack.FileOwner); ok { // --- FileOwner ---
owner, err := fo.Owner(ctx, "/usr/bin/pacman") t.Run("FileOwner", func(t *testing.T) {
if err == nil { fo, ok := mgr.(snack.FileOwner)
assert.NotEmpty(t, owner) 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) groups, err := g.GroupList(ctx)
require.NoError(t, err) 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)
}) })
} }

View File

@@ -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")
} }

View File

@@ -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
View 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)
}
}
}
}

View File

@@ -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) {
require.NoError(t, err) 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") 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)
})
} }