pacman writes its errors to stderr, which the runner folds into the error rather than the output; the stale-index classifier read the output alone and never saw a single 'failed retrieving file', so a ten-week-old database on the control node reported as a wall of 404s from the mirrors. The classifier now reads everything pacman said, knows a failed signature as the same staleness, tells a mirror outage with a fresh database apart from it, and names the database's date and age beside the remedy: a full upgrade by the operator, never a one-package sync, which on this distribution is a partial upgrade. Whose job keeping the database current is stays issue 205's question.
116 lines
5.2 KiB
Go
116 lines
5.2 KiB
Go
package system
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// pacman's own words from the control node on 2026-10-02 (novox/hq 04-ISSUES/205): every mirror
|
|
// 404s the versioned file a ten-week-old database names, and the one copy that arrives fails its
|
|
// signature. Errors are pacman's stderr, which the runner folds into the error, not the output.
|
|
const staleStderr = `error: failed retrieving file 'nodejs-26.5.0-1-x86_64.pkg.tar.zst' from mirror.hetzner.com : The requested URL returned error: 404
|
|
error: failed retrieving file 'nodejs-26.5.0-1-x86_64.pkg.tar.zst' from mirror.rackspace.com : The requested URL returned error: 404
|
|
error: failed retrieving file 'nodejs-26.5.0-1-x86_64.pkg.tar.zst' from arch.lucassymons.net : Could not resolve host: arch.lucassymons.net
|
|
warning: fatal error from arch.lucassymons.net, skipping for the remainder of this transaction
|
|
error: failed retrieving file 'nodejs-26.5.0-1-x86_64.pkg.tar.zst' from mirrors.cqu.edu.cn : Connection timed out after 10002 milliseconds
|
|
error: nodejs: signature from "Bert Peters (packager key) <bertptrs@archlinux.org>" is invalid
|
|
error: failed to commit transaction (invalid or corrupted package (PGP signature))`
|
|
|
|
const staleStdout = `resolving dependencies...
|
|
looking for conflicting packages...
|
|
|
|
Packages (4) ada-3.4.4-1 c-ares-1.34.8-1 simdjson-1:4.6.4-1 nodejs-26.5.0-1
|
|
|
|
:: Retrieving packages...
|
|
nodejs-26.5.0-1-x86_64 downloading...
|
|
checking keyring...
|
|
checking package integrity...
|
|
:: File /var/cache/pacman/pkg/nodejs-26.5.0-1-x86_64.pkg.tar.zst is corrupted (invalid or corrupted package (PGP signature)).
|
|
Errors occurred, no packages were upgraded.`
|
|
|
|
// A runner that behaves as ExecRunner does on failure: stdout as the output, stderr in the error.
|
|
func pacmanFailing(stdout, stderr string) Runner {
|
|
return func(_ context.Context, name string, args ...string) (string, error) {
|
|
return stdout, errors.New(name + " exited 1: " + stderr)
|
|
}
|
|
}
|
|
|
|
func TestAStaleDatabaseIsSaidAsOneWithItsAgeAndTheRemedy(t *testing.T) {
|
|
was := syncDatabaseAge
|
|
defer func() { syncDatabaseAge = was }()
|
|
syncDatabaseAge = func() string {
|
|
return describeAge(time.Date(2026, 7, 24, 16, 56, 0, 0, time.UTC), time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC))
|
|
}
|
|
|
|
err := arch{}.InstallPackage(context.Background(), pacmanFailing(staleStdout, staleStderr), "nodejs")
|
|
if err == nil {
|
|
t.Fatal("a failed install must fail")
|
|
}
|
|
for _, want := range []string{
|
|
"the package database on this machine is from 2026-07-24, 10 weeks old",
|
|
"stale package index",
|
|
"upgrading the machine — a full upgrade (`pacman -Syu`) by its operator — before the mesh can install nodejs",
|
|
"partial upgrade",
|
|
"returned error: 404", // pacman's own words follow
|
|
} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("the error does not say %q:\n%s", want, err)
|
|
}
|
|
}
|
|
if strings.Contains(err.Error(), "mirrors or the network") {
|
|
t.Errorf("a stale database must not be read as a mirror outage:\n%s", err)
|
|
}
|
|
}
|
|
|
|
// The same 404s read from stderr alone — the half this classifier used to be blind to.
|
|
func TestTheClassifierReadsWhatPacmanWroteToStderr(t *testing.T) {
|
|
if classifyInstallFailure(staleStderr) != installStale {
|
|
t.Fatal("every mirror 404ing the named file is a stale database")
|
|
}
|
|
if classifyInstallFailure(staleStdout) != installStale {
|
|
t.Fatal("a corrupted-signature line alone is a stale keyring")
|
|
}
|
|
if classifyInstallFailure("") != installOther {
|
|
t.Fatal("nothing said is nothing classified")
|
|
}
|
|
}
|
|
|
|
func TestAnUnreachableMirrorWithAFreshDatabaseIsAMirrorProblem(t *testing.T) {
|
|
was := syncDatabaseAge
|
|
defer func() { syncDatabaseAge = was }()
|
|
syncDatabaseAge = func() string { return "from 2026-10-02, less than a day old" }
|
|
outage := `error: failed retrieving file 'core.db' from mirror.hetzner.com : Could not resolve host: mirror.hetzner.com
|
|
error: failed retrieving file 'core.db' from mirror.rackspace.com : Connection timed out after 10001 milliseconds
|
|
error: failed to synchronize all databases (failed to retrieve some files)`
|
|
if classifyInstallFailure(outage) != installMirrors {
|
|
t.Fatal("no mirror answering, no 404, no signature fault: the mirrors, not the machine")
|
|
}
|
|
err := arch{}.InstallPackage(context.Background(), pacmanFailing("", outage), "nodejs")
|
|
if err == nil || !strings.Contains(err.Error(), "mirrors or the network") || !strings.Contains(err.Error(), "less than a day old") {
|
|
t.Errorf("a mirror outage is said as one, with the database's age beside it:\n%v", err)
|
|
}
|
|
}
|
|
|
|
func TestAFailureThatIsNeitherKeepsPacmansWords(t *testing.T) {
|
|
err := arch{}.InstallPackage(context.Background(), pacmanFailing("", "error: target not found: nodejsx"), "nodejsx")
|
|
if err == nil || !strings.Contains(err.Error(), "target not found") || strings.Contains(err.Error(), "package database on this machine") {
|
|
t.Errorf("an unknown package is pacman's own error, not a stale database:\n%v", err)
|
|
}
|
|
}
|
|
|
|
func TestDescribeAge(t *testing.T) {
|
|
now := time.Date(2026, 10, 3, 0, 0, 0, 0, time.UTC)
|
|
for when, want := range map[time.Time]string{
|
|
now.Add(-2 * time.Hour): "less than a day old",
|
|
now.Add(-5 * 24 * time.Hour): "5 days old",
|
|
now.Add(-71 * 24 * time.Hour): "10 weeks old",
|
|
} {
|
|
if got := describeAge(when, now); !strings.HasSuffix(got, want) {
|
|
t.Errorf("%s: got %q, want suffix %q", when, got, want)
|
|
}
|
|
}
|
|
}
|