Files
vim-ale/internal/resolve/resolve_cache_test.go
AJ ONeal 47419b7eee fix(classifypkg): tag Rust static musl builds as libc='none'
Rust *-unknown-linux-musl builds are statically linked with zero
runtime libc dependency. Detect this pattern in classifyGitHub and
override libc from 'musl' to 'none'. Hard-musl packages (pwsh, bun,
node) use different filename patterns and keep libc='musl'.
2026-03-11 12:19:21 -06:00

401 lines
13 KiB
Go

package resolve_test
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/webinstall/webi-installers/internal/buildmeta"
"github.com/webinstall/webi-installers/internal/resolve"
)
// legacyAsset matches the _cache/ JSON format.
type legacyAsset struct {
Name string `json:"name"`
Version string `json:"version"`
LTS bool `json:"lts"`
Channel string `json:"channel"`
Date string `json:"date"`
OS string `json:"os"`
Arch string `json:"arch"`
Libc string `json:"libc"`
Ext string `json:"ext"`
Download string `json:"download"`
}
type legacyCache struct {
Releases []legacyAsset `json:"releases"`
}
func loadCacheDists(t *testing.T, pkg string) []resolve.Dist {
t.Helper()
cacheDir := filepath.Join("..", "..", "_cache", "2026-03")
path := filepath.Join(cacheDir, pkg+".json")
data, err := os.ReadFile(path)
if err != nil {
t.Skipf("no cache file for %s: %v", pkg, err)
}
var lc legacyCache
if err := json.Unmarshal(data, &lc); err != nil {
t.Fatalf("parse %s: %v", pkg, err)
}
dists := make([]resolve.Dist, len(lc.Releases))
for i, la := range lc.Releases {
dists[i] = resolve.Dist{
Filename: la.Name,
Version: la.Version,
LTS: la.LTS,
Channel: la.Channel,
Date: la.Date,
OS: la.OS,
Arch: la.Arch,
Libc: la.Libc,
Format: la.Ext,
Download: la.Download,
}
}
return dists
}
// platforms is the standard webi target matrix.
var platforms = []struct {
name string
os buildmeta.OS
arch buildmeta.Arch
formats []string
}{
{"darwin-arm64", buildmeta.OSDarwin, buildmeta.ArchARM64, []string{".tar.xz", ".tar.gz", ".zip"}},
{"darwin-amd64", buildmeta.OSDarwin, buildmeta.ArchAMD64, []string{".tar.xz", ".tar.gz", ".zip"}},
{"linux-amd64", buildmeta.OSLinux, buildmeta.ArchAMD64, []string{".tar.xz", ".exe.xz", ".tar.gz", ".xz", ".gz", ".zip"}},
{"linux-arm64", buildmeta.OSLinux, buildmeta.ArchARM64, []string{".tar.xz", ".exe.xz", ".tar.gz", ".xz", ".gz", ".zip"}},
{"linux-armv7", buildmeta.OSLinux, buildmeta.ArchARMv7, []string{".tar.xz", ".tar.gz", ".xz", ".gz", ".zip"}},
{"linux-armv6", buildmeta.OSLinux, buildmeta.ArchARMv6, []string{".tar.xz", ".tar.gz", ".xz", ".gz", ".zip"}},
{"windows-amd64", buildmeta.OSWindows, buildmeta.ArchAMD64, []string{".zip", ".tar.gz", ".exe", ".7z"}},
{"windows-arm64", buildmeta.OSWindows, buildmeta.ArchARM64, []string{".zip", ".tar.gz", ".exe", ".7z"}},
}
// TestResolveAllPackages loads every package from the cache and verifies
// the resolver finds a match for each platform the package supports.
func TestResolveAllPackages(t *testing.T) {
cacheDir := filepath.Join("..", "..", "_cache", "2026-03")
entries, err := os.ReadDir(cacheDir)
if err != nil {
t.Skipf("no cache dir: %v", err)
}
var pkgs []string
for _, e := range entries {
if strings.HasSuffix(e.Name(), ".json") {
pkgs = append(pkgs, strings.TrimSuffix(e.Name(), ".json"))
}
}
if len(pkgs) < 50 {
t.Fatalf("expected at least 50 packages, got %d", len(pkgs))
}
for _, pkg := range pkgs {
t.Run(pkg, func(t *testing.T) {
dists := loadCacheDists(t, pkg)
if len(dists) == 0 {
t.Skip("no releases")
}
// Determine which platforms this package supports.
cat := resolve.Survey(dists)
osSet := make(map[string]bool, len(cat.OSes))
for _, o := range cat.OSes {
osSet[o] = true
}
for _, plat := range platforms {
platOS := string(plat.os)
// Check if this package has any assets for this OS
// (including POSIX/ANYOS which are compatible).
supported := osSet[platOS] ||
osSet[string(buildmeta.OSAny)] ||
(platOS != "windows" && (osSet[string(buildmeta.OSPosix2017)] || osSet[string(buildmeta.OSPosix2024)]))
if !supported {
continue
}
t.Run(plat.name, func(t *testing.T) {
m := resolve.Best(dists, resolve.Query{
OS: plat.os,
Arch: plat.arch,
Formats: plat.formats,
})
if m == nil {
// This is a warning, not a failure — some packages
// legitimately don't have builds for all arches.
// But log it so we can spot unexpected gaps.
t.Logf("WARN: no match for %s on %s (has OSes: %v, Arches: %v)",
pkg, plat.name, cat.OSes, cat.Arches)
return
}
if m.Version == "" {
t.Error("matched but Version is empty")
}
if m.Download == "" {
t.Error("matched but Download is empty")
}
})
}
})
}
}
// Packages with known platform expectations. Each entry specifies
// platforms that MUST resolve and the expected latest version.
var knownPackages = []struct {
pkg string
version string // expected latest stable version (prefix match)
platforms []string // platform names from the platforms table
}{
{"bat", "0.26", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
{"caddy", "2.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv7", "linux-armv6", "windows-amd64"}},
{"delta", "0.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
{"fd", "10.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv7", "windows-amd64"}},
{"fzf", "0.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv7", "windows-amd64"}},
{"gh", "2.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv6", "windows-amd64"}},
{"rg", "", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
{"shellcheck", "0.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv6", "windows-amd64"}},
{"shfmt", "3.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv6", "windows-amd64"}},
{"xz", "", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
{"yq", "4.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv6", "windows-amd64"}},
{"zoxide", "0.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "linux-armv7", "windows-amd64"}},
{"aliasman", "", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64"}},
{"comrak", "0.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "windows-amd64"}},
{"hugo", "0.", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
{"node", "", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
{"terraform", "", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
{"zig", "", []string{"darwin-arm64", "darwin-amd64", "linux-amd64", "linux-arm64", "windows-amd64"}},
}
// TestKnownPackages verifies specific packages resolve correctly
// with expected versions and platform coverage.
func TestKnownPackages(t *testing.T) {
platMap := make(map[string]struct {
os buildmeta.OS
arch buildmeta.Arch
formats []string
})
for _, p := range platforms {
platMap[p.name] = struct {
os buildmeta.OS
arch buildmeta.Arch
formats []string
}{p.os, p.arch, p.formats}
}
for _, kp := range knownPackages {
t.Run(kp.pkg, func(t *testing.T) {
dists := loadCacheDists(t, kp.pkg)
for _, platName := range kp.platforms {
plat := platMap[platName]
t.Run(platName, func(t *testing.T) {
m := resolve.Best(dists, resolve.Query{
OS: plat.os,
Arch: plat.arch,
Formats: plat.formats,
})
if m == nil {
t.Skipf("no build available for %s on %s — upstream gap", kp.pkg, platName)
return
}
if kp.version != "" {
// Strip leading "v" for prefix comparison.
v := strings.TrimPrefix(m.Version, "v")
if !strings.HasPrefix(v, kp.version) {
t.Errorf("Version = %q, want prefix %q", m.Version, kp.version)
}
}
})
}
})
}
}
// TestResolveVersionConstraints tests version pinning across real packages.
func TestResolveVersionConstraints(t *testing.T) {
tests := []struct {
pkg string
version string // constraint
wantPfx string // expected version prefix in result
}{
{"bat", "0.25", "0.25"},
{"bat", "0.26", "0.26"},
{"gh", "2.40", "2.40"},
{"node", "20", "20."},
{"node", "22", "22."},
{"hugo", "0.121", "0.121"},
}
for _, tt := range tests {
name := fmt.Sprintf("%s@%s", tt.pkg, tt.version)
t.Run(name, func(t *testing.T) {
dists := loadCacheDists(t, tt.pkg)
m := resolve.Best(dists, resolve.Query{
OS: buildmeta.OSLinux,
Arch: buildmeta.ArchAMD64,
Formats: []string{".tar.xz", ".tar.gz", ".zip"},
Version: tt.version,
})
if m == nil {
t.Fatalf("no match for %s@%s", tt.pkg, tt.version)
}
v := strings.TrimPrefix(m.Version, "v")
if !strings.HasPrefix(v, tt.wantPfx) {
t.Errorf("Version = %q, want prefix %q", m.Version, tt.wantPfx)
}
})
}
}
// TestResolveArchFallbackReal tests arch fallback with real package data.
func TestResolveArchFallbackReal(t *testing.T) {
// awless only has amd64 builds — macOS ARM64 should fall back.
dists := loadCacheDists(t, "awless")
m := resolve.Best(dists, resolve.Query{
OS: buildmeta.OSDarwin,
Arch: buildmeta.ArchARM64,
Formats: []string{".tar.gz", ".zip"},
})
if m == nil {
t.Fatal("expected Rosetta 2 fallback for awless")
}
if m.Arch != "x86_64" {
t.Errorf("Arch = %q, want x86_64", m.Arch)
}
}
// TestResolvePosixPackages tests packages that use posix_2017/ANYOS.
func TestResolvePosixPackages(t *testing.T) {
posixPkgs := []string{"aliasman", "pathman", "serviceman"}
for _, pkg := range posixPkgs {
t.Run(pkg, func(t *testing.T) {
dists := loadCacheDists(t, pkg)
if len(dists) == 0 {
t.Skip("no releases")
}
// Should resolve on Linux.
m := resolve.Best(dists, resolve.Query{
OS: buildmeta.OSLinux,
Arch: buildmeta.ArchAMD64,
Formats: []string{".tar.xz", ".tar.gz", ".zip", ".xz", ".gz"},
})
if m == nil {
t.Error("expected match on Linux for POSIX package")
}
// Should resolve on macOS.
m = resolve.Best(dists, resolve.Query{
OS: buildmeta.OSDarwin,
Arch: buildmeta.ArchARM64,
Formats: []string{".tar.xz", ".tar.gz", ".zip"},
})
if m == nil {
t.Error("expected match on macOS for POSIX package")
}
// Should NOT resolve on Windows (POSIX packages aren't Windows-compatible).
m = resolve.Best(dists, resolve.Query{
OS: buildmeta.OSWindows,
Arch: buildmeta.ArchAMD64,
Formats: []string{".zip", ".tar.gz"},
})
// This may or may not resolve depending on whether the package
// also has Windows builds. Don't assert nil — just check it
// doesn't return a posix_2017 match for Windows.
if m != nil && (m.OS == "posix_2017" || m.OS == "posix_2024") {
t.Errorf("POSIX package should not match Windows, got OS=%q", m.OS)
}
})
}
}
// TestResolveLibcPreference tests libc selection.
// bat is a Rust project — its musl builds are static (libc='none').
// pwsh has hard musl dependencies (libc='musl').
func TestResolveLibcPreference(t *testing.T) {
batDists := loadCacheDists(t, "bat")
// Musl host requesting bat: gets the static musl build (tagged 'none').
m := resolve.Best(batDists, resolve.Query{
OS: buildmeta.OSLinux,
Arch: buildmeta.ArchAMD64,
Libc: buildmeta.LibcMusl,
Formats: []string{".tar.gz"},
})
if m == nil {
t.Fatal("expected match for musl host")
}
// Rust musl builds are static — tagged as 'none', not 'musl'.
if m.Libc != "none" {
t.Errorf("bat musl request: Libc = %q, want none (static musl)", m.Libc)
}
// Explicit gnu request.
m = resolve.Best(batDists, resolve.Query{
OS: buildmeta.OSLinux,
Arch: buildmeta.ArchAMD64,
Libc: buildmeta.LibcGNU,
Formats: []string{".tar.gz"},
})
if m == nil {
t.Fatal("expected gnu match")
}
if m.Libc != "gnu" {
t.Errorf("Libc = %q, want gnu", m.Libc)
}
// No preference — should still match (accepts any).
m = resolve.Best(batDists, resolve.Query{
OS: buildmeta.OSLinux,
Arch: buildmeta.ArchAMD64,
Formats: []string{".tar.gz"},
})
if m == nil {
t.Fatal("expected match with no libc preference")
}
// pwsh has hard musl builds (dynamically linked, requires musl runtime).
pwshDists := loadCacheDists(t, "pwsh")
m = resolve.Best(pwshDists, resolve.Query{
OS: buildmeta.OSLinux,
Arch: buildmeta.ArchAMD64,
Libc: buildmeta.LibcMusl,
Formats: []string{".tar.gz"},
})
if m == nil {
t.Fatal("expected pwsh musl match")
}
if m.Libc != "musl" {
t.Errorf("pwsh musl request: Libc = %q, want musl", m.Libc)
}
}
// TestResolveFormatFallback tests format preference cascading.
func TestResolveFormatFallback(t *testing.T) {
// Request .tar.xz first, fall back to .tar.gz.
dists := loadCacheDists(t, "bat")
m := resolve.Best(dists, resolve.Query{
OS: buildmeta.OSLinux,
Arch: buildmeta.ArchAMD64,
Formats: []string{".tar.xz", ".tar.gz", ".zip"},
})
if m == nil {
t.Fatal("expected match")
}
// bat only has .tar.gz — should fall back from .tar.xz.
if m.Format != ".tar.gz" {
t.Errorf("Format = %q, want .tar.gz (fallback from .tar.xz)", m.Format)
}
}