296 lines
11 KiB
Go
296 lines
11 KiB
Go
package delivery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The checker's keys (novox/hq ADR 0447 clauses 1 to 3, ADR 0444, ADR 0446 clause 5, ADR 0453 clause 4): the verdicts of
|
|
// its checks and the passing verdicts it keeps per package and level. The checker writes them from R4c, dark; the
|
|
// forge bridge writes the status `mesh/check` from a verdict (unrequired until R4f), the gate reads it from R4f, and
|
|
// the view and the watchman read them (a running check carries a pulse).
|
|
//
|
|
// **The verdict's key is the group and the heads, not the fingerprints** (the lead's limit for R4a). Research 063
|
|
// section 01 drew `verdicts.<g>.check.<fpset>`; ADR 0447 clause 1 judges a pull request's head merged onto its
|
|
// trunk's head, and a repository without modules has no fingerprint set, so the key is `<group>.<heads hash>` as the
|
|
// heads hash of HeadsHash gives it. The fpset (FingerprintSet) is not part of it.
|
|
const (
|
|
// CheckerModule is the module of the checker part.
|
|
CheckerModule = "mesh-delivery-checker"
|
|
// CheckerVerdictsBucket is the checker's bucket of verdicts, `<owner module>_<name>`.
|
|
CheckerVerdictsBucket = CheckerModule + "_verdicts"
|
|
// CheckerVerdictsHistory is the revisions the verdicts bucket keeps per key.
|
|
CheckerVerdictsHistory = 5
|
|
// CheckerKeptBucket is the checker's bucket of kept verdicts. A key is written once, by a create that fails when
|
|
// the key exists, by the checker's credential alone, from the result the checker reads from the test run and
|
|
// never from anything the code under test writes (ADR 0444 clause 5); nothing changes it afterwards.
|
|
CheckerKeptBucket = CheckerModule + "_kept"
|
|
// CheckerKeptHistory is the revisions the kept bucket keeps per key.
|
|
CheckerKeptHistory = 1
|
|
)
|
|
|
|
// The test levels (ADR 0445 clause 2) and the states of a verdict.
|
|
const (
|
|
LevelUnit = "unit"
|
|
LevelIntegration = "integration"
|
|
|
|
VerdictRunning = "running"
|
|
VerdictPass = "pass"
|
|
VerdictFail = "fail"
|
|
)
|
|
|
|
func checkLevel(level string) error {
|
|
if level != LevelUnit && level != LevelIntegration {
|
|
return fmt.Errorf("delivery: the level %q is not %s or %s", level, LevelUnit, LevelIntegration)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isHex12(s string) bool { return len(s) == 12 && strings.Trim(s, "0123456789abcdef") == "" }
|
|
|
|
// CheckerVerdictKey is the key, in CheckerVerdictsBucket, of a group's check at its heads: `<escaped group>.<heads
|
|
// hash>`, the group escaped as CandidatesKey does and the heads hash as HeadsHash gives it (12 lowercase hex digits).
|
|
func CheckerVerdictKey(group, headsHash string) (string, error) {
|
|
g, err := groupKey(group)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !isHex12(headsHash) {
|
|
return "", fmt.Errorf("delivery: %q is not a heads hash, 12 lowercase hex digits", headsHash)
|
|
}
|
|
return g + "." + headsHash, nil
|
|
}
|
|
|
|
// ParseCheckerVerdictKey is the inverse of CheckerVerdictKey.
|
|
func ParseCheckerVerdictKey(key string) (group, headsHash string, err error) {
|
|
i := strings.LastIndex(key, ".")
|
|
if i < 0 {
|
|
return "", "", fmt.Errorf("delivery: %q is not a verdict key, <group>.<heads hash>", key)
|
|
}
|
|
part, headsHash := key[:i], key[i+1:]
|
|
if !isHex12(headsHash) {
|
|
return "", "", fmt.Errorf("delivery: %q is not a verdict key: %q is not a heads hash, 12 lowercase hex digits", key, headsHash)
|
|
}
|
|
if group, err = parseGroupKey(part); err != nil {
|
|
return "", "", fmt.Errorf("delivery: %q is not a verdict key: %w", key, err)
|
|
}
|
|
if strings.Contains(part, ".") {
|
|
return "", "", fmt.Errorf("delivery: %q is not a verdict key: the group part holds a dot", key)
|
|
}
|
|
return group, headsHash, nil
|
|
}
|
|
|
|
// LevelResult is a level's tests: how many ran in this check and how many were taken from kept verdicts (ADR 0444
|
|
// clause 2).
|
|
type LevelResult struct {
|
|
Run int `json:"run"`
|
|
Kept int `json:"kept"`
|
|
}
|
|
|
|
// Took is the time of each part of a check in integer milliseconds (ADR 0447 clause 3): waiting in the queue, cloning,
|
|
// the unit level, the integration level's setup and its running, and the composition.
|
|
type Took struct {
|
|
Queue int64 `json:"queue"`
|
|
Clone int64 `json:"clone"`
|
|
Unit int64 `json:"unit"`
|
|
IntegrationSetup int64 `json:"integration-setup"`
|
|
IntegrationRun int64 `json:"integration-run"`
|
|
Composition int64 `json:"composition"`
|
|
}
|
|
|
|
// FailedFirst names what failed first: a test (its package and name) or a machine (and the words of the composition's
|
|
// refusal); one of them, with Words optional.
|
|
type FailedFirst struct {
|
|
Test string `json:"test,omitempty"`
|
|
Machine string `json:"machine,omitempty"`
|
|
Words string `json:"words,omitempty"`
|
|
}
|
|
|
|
// Verdict is the value of a verdict key: the group and its heads hash, the trunk head of each repository it was judged
|
|
// against (repository to full sha), the State (running, pass or fail), what failed first, the tests run and kept per
|
|
// level, each part's time, the machines composed and the mesh-sdk version the composition ran with (ADR 0446 clause 5).
|
|
type Verdict struct {
|
|
V int `json:"v"`
|
|
Group string `json:"group"`
|
|
Heads string `json:"heads"`
|
|
Trunk map[string]string `json:"trunk,omitempty"`
|
|
State string `json:"state"`
|
|
Failed FailedFirst `json:"failed,omitzero"`
|
|
Levels map[string]LevelResult `json:"levels,omitempty"`
|
|
Took Took `json:"took,omitzero"`
|
|
Machines []string `json:"machines,omitempty"`
|
|
SDK string `json:"sdk,omitempty"`
|
|
}
|
|
|
|
func (v Verdict) check() error {
|
|
if v.Group == "" {
|
|
return fmt.Errorf("delivery: a verdict names no group")
|
|
}
|
|
if !isHex12(v.Heads) {
|
|
return fmt.Errorf("delivery: the verdict's heads hash %q is not 12 lowercase hex digits", v.Heads)
|
|
}
|
|
for repository, head := range v.Trunk {
|
|
if head == "" {
|
|
return fmt.Errorf("delivery: the verdict's trunk head of %s is empty", repository)
|
|
}
|
|
}
|
|
for level, r := range v.Levels {
|
|
if err := checkLevel(level); err != nil {
|
|
return err
|
|
}
|
|
if r.Run < 0 || r.Kept < 0 {
|
|
return fmt.Errorf("delivery: the verdict's %s level counts a negative number of tests", level)
|
|
}
|
|
}
|
|
for part, ms := range map[string]int64{"queue": v.Took.Queue, "clone": v.Took.Clone, "unit": v.Took.Unit,
|
|
"integration-setup": v.Took.IntegrationSetup, "integration-run": v.Took.IntegrationRun, "composition": v.Took.Composition} {
|
|
if ms < 0 {
|
|
return fmt.Errorf("delivery: the verdict's time of the part %s is negative", part)
|
|
}
|
|
}
|
|
named := v.Failed.Test != "" || v.Failed.Machine != ""
|
|
switch v.State {
|
|
case VerdictRunning:
|
|
if v.Failed != (FailedFirst{}) {
|
|
return fmt.Errorf("delivery: a running verdict names no failure")
|
|
}
|
|
case VerdictPass:
|
|
if v.Failed != (FailedFirst{}) {
|
|
return fmt.Errorf("delivery: a pass names no failure")
|
|
}
|
|
for _, level := range []string{LevelUnit, LevelIntegration} {
|
|
if _, ok := v.Levels[level]; !ok {
|
|
return fmt.Errorf("delivery: a pass says nothing of the %s level", level)
|
|
}
|
|
}
|
|
case VerdictFail:
|
|
if !named {
|
|
return fmt.Errorf("delivery: a fail names neither a test nor a machine")
|
|
}
|
|
default:
|
|
return fmt.Errorf("delivery: the verdict state %q is not running, pass or fail", v.State)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeVerdict is v as JSON at Version; it refuses a fail that names neither a test nor a machine, a pass without both
|
|
// levels, an unknown state or level, and a negative count or time.
|
|
func EncodeVerdict(v Verdict) ([]byte, error) {
|
|
if err := v.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
v.V = Version
|
|
return json.Marshal(v)
|
|
}
|
|
|
|
// DecodeVerdict reads a verdict; it refuses an unknown version (ErrUnknownVersion) and what EncodeVerdict refuses.
|
|
func DecodeVerdict(data []byte) (Verdict, error) {
|
|
var v Verdict
|
|
if err := decode(data, &v); err != nil {
|
|
return Verdict{}, err
|
|
}
|
|
if err := v.check(); err != nil {
|
|
return Verdict{}, err
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// SameVerdict says whether two verdicts are the same value, so the checker writes only when they differ.
|
|
func SameVerdict(a, b Verdict) bool { return reflect.DeepEqual(a, b) }
|
|
|
|
// KeptKey is the key, in CheckerKeptBucket, of a kept verdict: `<level>.<digest>`, the level unit or integration (ADR
|
|
// 0445 clause 2) and the digest, lowercase hex, of everything that package's tests read at that level as the
|
|
// language's build module names it (ADR 0444 clause 1, ADR 0445 clause 5).
|
|
func KeptKey(level, digest string) (string, error) {
|
|
if err := checkLevel(level); err != nil {
|
|
return "", err
|
|
}
|
|
if digest == "" || strings.Trim(digest, "0123456789abcdef") != "" {
|
|
return "", fmt.Errorf("delivery: %q is not a digest, lowercase hex", digest)
|
|
}
|
|
return level + "." + digest, nil
|
|
}
|
|
|
|
// ParseKeptKey is the inverse of KeptKey.
|
|
func ParseKeptKey(key string) (level, digest string, err error) {
|
|
level, digest, ok := strings.Cut(key, ".")
|
|
if !ok {
|
|
return "", "", fmt.Errorf("delivery: %q is not a kept key, <level>.<digest>", key)
|
|
}
|
|
if _, err = KeptKey(level, digest); err != nil {
|
|
return "", "", fmt.Errorf("delivery: %q is not a kept key: %w", key, err)
|
|
}
|
|
return level, digest, nil
|
|
}
|
|
|
|
// Kept is the value of a kept key: a passing verdict of one package at one level (State is pass and nothing else: a
|
|
// failure is never kept, ADR 0444 clause 1), with the repository, the package, the number of tests, the time they took
|
|
// in milliseconds, the toolchain's version and when it was kept.
|
|
type Kept struct {
|
|
V int `json:"v"`
|
|
State string `json:"state"`
|
|
Repository string `json:"repository"`
|
|
Package string `json:"package"`
|
|
Level string `json:"level"`
|
|
Tests int `json:"tests"`
|
|
Took int64 `json:"took"`
|
|
Toolchain string `json:"toolchain"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
func (k Kept) check() error {
|
|
if k.State != VerdictPass {
|
|
return fmt.Errorf("delivery: a kept verdict is a pass, never %q: a failure is never kept", k.State)
|
|
}
|
|
if err := checkRepository(k.Repository); err != nil {
|
|
return err
|
|
}
|
|
if k.Package == "" {
|
|
return fmt.Errorf("delivery: a kept verdict names no package")
|
|
}
|
|
if err := checkLevel(k.Level); err != nil {
|
|
return err
|
|
}
|
|
if k.Tests < 0 || k.Took < 0 {
|
|
return fmt.Errorf("delivery: a kept verdict counts a negative number of tests or a negative time")
|
|
}
|
|
if k.Toolchain == "" {
|
|
return fmt.Errorf("delivery: a kept verdict names no toolchain version")
|
|
}
|
|
if k.At.IsZero() {
|
|
return fmt.Errorf("delivery: a kept verdict says no time")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeKept is k as JSON at Version; it refuses a kept value that is not a pass.
|
|
func EncodeKept(k Kept) ([]byte, error) {
|
|
if err := k.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
k.V = Version
|
|
return json.Marshal(k)
|
|
}
|
|
|
|
// DecodeKept reads a kept value; it refuses an unknown version (ErrUnknownVersion) and what EncodeKept refuses.
|
|
func DecodeKept(data []byte) (Kept, error) {
|
|
var k Kept
|
|
if err := decode(data, &k); err != nil {
|
|
return Kept{}, err
|
|
}
|
|
if err := k.check(); err != nil {
|
|
return Kept{}, err
|
|
}
|
|
return k, nil
|
|
}
|
|
|
|
// SameKept says whether two kept values are the same value.
|
|
func SameKept(a, b Kept) bool {
|
|
at := a.At.Equal(b.At)
|
|
a.At, b.At = time.Time{}, time.Time{}
|
|
return at && a == b
|
|
}
|