mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-02 05:08:42 -07:00
- Root package unit tests: Targets, TargetNames, ApplyOptions, error sentinels - Every provider integration test now covers: - All Manager interface methods (positive + negative cases) - GetCapabilities verification (assert expected interfaces) - VersionQuerier: LatestVersion, ListUpgrades, UpgradeAvailable, VersionCmp - Holder: Hold, ListHeld, Unhold (apt, dnf) - Cleaner: Autoremove, Clean - FileOwner: FileList, Owner (+ not-found cases) - RepoManager: ListRepos (apt, dnf, flatpak) - KeyManager: ListKeys (apt, dnf) - Grouper: GroupList, GroupInfo (pacman, dnf) - NameNormalizer: NormalizeName, ParseArch table tests (apt, dpkg, dnf, rpm) - Containertest matrix: 5 distros (debian, alpine, arch, fedora39, fedora-latest) - CI: coverage profiles uploaded per-job, merged in codecov job - Added .gitignore for coverage files
194 lines
5.7 KiB
Go
194 lines
5.7 KiB
Go
//go:build containertest
|
|
|
|
// Package snack_test provides testcontainers-based integration tests that
|
|
// exercise every Manager method and capability interface across distros.
|
|
//
|
|
// go test -tags containertest -v -count=1 -timeout 15m .
|
|
//
|
|
// Requires Docker.
|
|
package snack_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/testcontainers/testcontainers-go"
|
|
"github.com/testcontainers/testcontainers-go/wait"
|
|
)
|
|
|
|
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
|
|
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.
|
|
func installGo(prereqs string) string {
|
|
return fmt.Sprintf(
|
|
"%s && wget -qO- https://go.dev/dl/go%s.linux-amd64.tar.gz | tar -C /usr/local -xzf -",
|
|
prereqs, goVersion,
|
|
)
|
|
}
|
|
|
|
var distros = []distroTest{
|
|
{
|
|
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 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/",
|
|
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/",
|
|
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/",
|
|
testPkg: "tree",
|
|
searchQuery: "curl",
|
|
infoPkg: "bash",
|
|
knownFile: "/usr/bin/tree",
|
|
knownFileOwner: "tree",
|
|
groupName: "Development Tools",
|
|
},
|
|
}
|
|
|
|
func TestContainers(t *testing.T) {
|
|
if _, err := exec.LookPath("docker"); err != nil {
|
|
t.Skip("docker not available")
|
|
}
|
|
|
|
for _, d := range distros {
|
|
d := d
|
|
t.Run(d.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Minute)
|
|
defer cancel()
|
|
|
|
req := testcontainers.ContainerRequest{
|
|
Image: d.image,
|
|
Cmd: []string{"sleep", "infinity"},
|
|
WaitingFor: wait.ForExec([]string{"echo", "ready"}).
|
|
WithStartupTimeout(60 * time.Second),
|
|
}
|
|
|
|
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
|
ContainerRequest: req,
|
|
Started: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to start %s container: %v", d.name, err)
|
|
}
|
|
defer func() {
|
|
_ = container.Terminate(ctx)
|
|
}()
|
|
|
|
containerID := container.GetContainerID()
|
|
|
|
// Execute setup
|
|
t.Logf("Setting up %s...", d.name)
|
|
exitCode, output, err := container.Exec(ctx, []string{"sh", "-c", d.setup})
|
|
if err != nil || exitCode != 0 {
|
|
t.Fatalf("setup failed (exit %d): %v\n%s", exitCode, err, output)
|
|
}
|
|
|
|
// Copy the module into the container
|
|
copyCmd := exec.CommandContext(ctx, "docker", "cp", ".", containerID+":/src")
|
|
if out, err := copyCmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("docker cp failed: %v\n%s", err, out)
|
|
}
|
|
|
|
// 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 -coverprofile=/tmp/coverage.out -coverpkg=./... %s 2>&1",
|
|
d.packages,
|
|
)
|
|
t.Logf("Running tests in %s: %s", d.name, testCmd)
|
|
|
|
exitCode, reader, err := container.Exec(ctx, []string{"sh", "-c", testCmd})
|
|
if err != nil {
|
|
t.Fatalf("test exec failed: %v", err)
|
|
}
|
|
|
|
buf := make([]byte, 128*1024)
|
|
var output2 strings.Builder
|
|
for {
|
|
n, readErr := reader.Read(buf)
|
|
if n > 0 {
|
|
output2.Write(buf[:n])
|
|
}
|
|
if readErr != nil {
|
|
break
|
|
}
|
|
}
|
|
t.Log(output2.String())
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|