Pre-release polish: be explicit about which firecracker versions
banger has been validated against, and give users a one-line install
suggestion when the binary is missing rather than the previous
generic "install firecracker or set firecracker_bin".
internal/firecracker/version.go (new):
* MinSupportedVersion = "1.5.0" — the floor banger refuses to
launch below. Bumping this is a deliberate decision, paired
with whatever helper feature started requiring the newer
firecracker.
* KnownTestedVersion = "1.14.1" — what banger's smoke suite
actually runs against today.
* SemVer + Compare + ParseVersionOutput, table-tested. The parser
tolerates the trailing "exiting successfully" log line that
firecracker tacks onto --version; only the canonical
"Firecracker vX.Y.Z" line matters.
* QueryVersion shells `<bin> --version` through a CommandRunner-
shaped interface; doesn't import internal/system to keep the
firecracker package leaf-clean.
internal/daemon/doctor.go:
* New addFirecrackerVersionCheck replaces the previous bare
RequireExecutable preflight for firecracker. Three outcomes:
PASS within [Min, Tested], WARN above Tested (newer firecracker
usually works but is outside the tested window), FAIL below Min
or when the binary is missing.
* On missing binary, surfaces a distro-aware install command via
parseOSReleaseIDs(/etc/os-release) → guessFirecrackerInstall
Command. Pinned suggestions for debian (apt), arch/manjaro
(paru), and nixos (nix-env). Other distros get only the upstream
Releases URL — guessing wrong sends users on a wild goose chase.
* runtimeChecks no longer includes the firecracker preflight; the
new check subsumes it.
README.md:
* Requirements line now spells out the tested-against version
(v1.14.1) and the supported floor (≥ v1.5.0), and points at
`banger doctor` for the version check + install hint.
Tests: ParseVersionOutput across canonical/prerelease/garbage inputs,
SemVer.Compare across major/minor/patch boundaries, MustParseSemVer
panics on malformed inputs. Doctor-side: PASS on tested version,
FAIL below Min, WARN above Tested, FAIL with upstream URL when
missing, install-hint dispatch table covering debian/ubuntu (via
ID_LIKE)/arch/manjaro/nixos/fedora-fallback/missing-os-release.
The renamed TestDoctorReport_MissingFirecrackerFails... now asserts
against the new check name. Live `banger doctor` reports
"v1.14.1 at /usr/bin/firecracker (within tested range; min v1.5.0,
tested v1.14.1)" against the smoke host.
Smoke bare_run still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
2.9 KiB
Go
96 lines
2.9 KiB
Go
package firecracker
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseVersionOutput(t *testing.T) {
|
|
t.Parallel()
|
|
for _, tc := range []struct {
|
|
name string
|
|
input string
|
|
want SemVer
|
|
wantErr bool
|
|
}{
|
|
{name: "canonical", input: "Firecracker v1.14.1\n", want: SemVer{Major: 1, Minor: 14, Patch: 1}},
|
|
{name: "with_trailing_log", input: "Firecracker v1.14.1\n\n2026-04-28T17:38:12.392171332 [anonymous-instance:main] exit_code=0\n", want: SemVer{Major: 1, Minor: 14, Patch: 1}},
|
|
{name: "prerelease", input: "Firecracker v1.10.0-rc1\n", want: SemVer{Major: 1, Minor: 10, Patch: 0, PreRelease: "rc1"}},
|
|
{name: "two_digit_minor", input: "Firecracker v2.0.42\n", want: SemVer{Major: 2, Minor: 0, Patch: 42}},
|
|
{name: "garbage", input: "not a firecracker", wantErr: true},
|
|
{name: "empty", input: "", wantErr: true},
|
|
{name: "missing_v", input: "Firecracker 1.14.1", wantErr: true},
|
|
} {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got, err := ParseVersionOutput(tc.input)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("ParseVersionOutput(%q) succeeded, want error", tc.input)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("ParseVersionOutput(%q) = %v", tc.input, err)
|
|
}
|
|
if got != tc.want {
|
|
t.Fatalf("ParseVersionOutput(%q) = %+v, want %+v", tc.input, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSemVerCompare(t *testing.T) {
|
|
t.Parallel()
|
|
for _, tc := range []struct {
|
|
name string
|
|
a, b string
|
|
want int
|
|
}{
|
|
{name: "equal", a: "1.14.1", b: "1.14.1", want: 0},
|
|
{name: "patch_lower", a: "1.14.0", b: "1.14.1", want: -1},
|
|
{name: "patch_higher", a: "1.14.2", b: "1.14.1", want: 1},
|
|
{name: "minor_dominates_patch", a: "1.10.999", b: "1.11.0", want: -1},
|
|
{name: "major_dominates", a: "2.0.0", b: "1.99.99", want: 1},
|
|
{name: "min_vs_tested_today", a: MinSupportedVersion, b: KnownTestedVersion, want: -1},
|
|
} {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
a := MustParseSemVer(tc.a)
|
|
b := MustParseSemVer(tc.b)
|
|
if got := a.Compare(b); got != tc.want {
|
|
t.Fatalf("(%s).Compare(%s) = %d, want %d", tc.a, tc.b, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSemVerString(t *testing.T) {
|
|
t.Parallel()
|
|
if got := MustParseSemVer("1.14.1").String(); got != "v1.14.1" {
|
|
t.Fatalf("v1.14.1.String() = %q", got)
|
|
}
|
|
pre := SemVer{Major: 1, Minor: 10, Patch: 0, PreRelease: "rc1"}
|
|
if got := pre.String(); got != "v1.10.0-rc1" {
|
|
t.Fatalf("rc String() = %q", got)
|
|
}
|
|
}
|
|
|
|
// MustParseSemVer panics on malformed input; pin a few inputs so a
|
|
// future refactor doesn't accidentally widen what counts as valid.
|
|
func TestMustParseSemVerRejectsMalformed(t *testing.T) {
|
|
t.Parallel()
|
|
for _, bad := range []string{"", "1", "1.2", "1.2.3.4", "v1.2.x", "vfoo"} {
|
|
bad := bad
|
|
t.Run(strings.ReplaceAll(bad, ".", "_"), func(t *testing.T) {
|
|
defer func() {
|
|
if r := recover(); r == nil {
|
|
t.Errorf("MustParseSemVer(%q) did not panic", bad)
|
|
}
|
|
}()
|
|
_ = MustParseSemVer(bad)
|
|
})
|
|
}
|
|
}
|