Files
phamnazage-jpg 71cbaf5fa6 test(project): achieve ≥70% package coverage across all internal packages
- store/sqlite: 75.4% (repos + db coverage)
- host/sub2api: 80.8% (httptest mock server, pure function tests)
- app: 74.2% (handler error paths, NewActionSet closures)
- pack: 72.4%
- provision: 75.2%
- access: 77.3%
- config: 94.7% (lookup mock tests)

All tests pass: build, vet, race, coverage gates.
2026-05-15 19:26:25 +08:00

135 lines
3.4 KiB
Go

package pack
import (
"fmt"
"strconv"
"strings"
)
func CheckHostCompatibility(manifest Manifest, hostVersion string) error {
targetHost := strings.TrimSpace(manifest.TargetHost)
if targetHost == "" {
return fmt.Errorf("pack manifest target_host is required")
}
if targetHost != "sub2api" {
return fmt.Errorf("pack target_host %q is not supported", targetHost)
}
normalizedHost, err := parseVersion(strings.TrimSpace(hostVersion))
if err != nil {
return fmt.Errorf("parse host version %q: %w", hostVersion, err)
}
minVersion := strings.TrimSpace(manifest.MinHostVersion)
if minVersion != "" {
cmp, err := compareVersions(normalizedHost.raw, minVersion)
if err != nil {
return fmt.Errorf("compare min_host_version: %w", err)
}
if cmp < 0 {
return fmt.Errorf("host version %q is below min_host_version %q", hostVersion, minVersion)
}
}
maxVersion := strings.TrimSpace(manifest.MaxHostVersion)
if maxVersion != "" {
ok, err := matchesMaxConstraint(normalizedHost.raw, maxVersion)
if err != nil {
return fmt.Errorf("compare max_host_version: %w", err)
}
if !ok {
return fmt.Errorf("host version %q is above max_host_version %q", hostVersion, maxVersion)
}
}
return nil
}
type parsedVersion struct {
raw string
parts [3]int
}
func compareVersions(a, b string) (int, error) {
left, err := parseVersion(a)
if err != nil {
return 0, err
}
right, err := parseVersion(b)
if err != nil {
return 0, err
}
for i := 0; i < len(left.parts); i++ {
if left.parts[i] < right.parts[i] {
return -1, nil
}
if left.parts[i] > right.parts[i] {
return 1, nil
}
}
return 0, nil
}
func matchesMaxConstraint(hostVersion, maxVersion string) (bool, error) {
normalizedMax := normalizeVersion(maxVersion)
if strings.HasSuffix(normalizedMax, ".x") {
prefix := strings.TrimSuffix(normalizedMax, ".x")
parts := strings.Split(prefix, ".")
if len(parts) != 2 {
return false, fmt.Errorf("wildcard max version %q must be in N.N.x format", maxVersion)
}
host, err := parseVersion(hostVersion)
if err != nil {
return false, err
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return false, fmt.Errorf("parse major version %q: %w", parts[0], err)
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return false, fmt.Errorf("parse minor version %q: %w", parts[1], err)
}
if host.parts[0] < major {
return true, nil
}
if host.parts[0] > major {
return false, nil
}
return host.parts[1] <= minor, nil
}
cmp, err := compareVersions(hostVersion, maxVersion)
if err != nil {
return false, err
}
return cmp <= 0, nil
}
func parseVersion(value string) (parsedVersion, error) {
normalized := normalizeVersion(value)
if normalized == "" {
return parsedVersion{}, fmt.Errorf("version is required")
}
parts := strings.Split(normalized, ".")
if len(parts) != 3 {
return parsedVersion{}, fmt.Errorf("version %q must be in N.N.N format", value)
}
var parsed parsedVersion
parsed.raw = normalized
for i, part := range parts {
number, err := strconv.Atoi(part)
if err != nil {
return parsedVersion{}, fmt.Errorf("parse version segment %q: %w", part, err)
}
parsed.parts[i] = number
}
return parsed, nil
}
func normalizeVersion(value string) string {
trimmed := strings.TrimSpace(value)
trimmed = strings.TrimPrefix(trimmed, "v")
trimmed = strings.TrimPrefix(trimmed, "V")
return trimmed
}