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.
This commit is contained in:
134
internal/pack/version.go
Normal file
134
internal/pack/version.go
Normal file
@@ -0,0 +1,134 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user