Give the source revision, candidates, modules and forge messages one layout the parts read (issue 1200)
This commit is contained in:
@@ -18,6 +18,11 @@
|
||||
// question 9 item 7): a writer writes a new version only once every reader's have shows a version that reads it. A
|
||||
// writer writes only when the new value differs from the one before (the Same functions).
|
||||
//
|
||||
// **Source, candidates, modules and the forge** (issue 1200; ADR 0437 §5, release 3): the gate's bucket `source` with
|
||||
// its one key `main` (every repository's merged head and the group that moved it), the indexer's `candidates.<group>`
|
||||
// and `modules.<module>` (a module's source fingerprint, or none and why), and the forge bridge's messages named
|
||||
// `forge.<owner>.<name>.<kind>` (source.go).
|
||||
//
|
||||
// Standard library only.
|
||||
package delivery
|
||||
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The modules of the parts release 3 adds (novox/hq ADR 0437): their buckets are their own state, `<module>_<name>`.
|
||||
const (
|
||||
GateModule = "mesh-delivery-gate"
|
||||
IndexerModule = "mesh-delivery-indexer"
|
||||
ForgeBridgeModule = "mesh-delivery-forge-bridge"
|
||||
TellerModule = "mesh-delivery-teller"
|
||||
)
|
||||
|
||||
// The gate's source bucket and the indexer's candidates and modules buckets (ADR 0437 §5). The source bucket has
|
||||
// one key, SourceMainKey: every repository's merged head and the group that moved it, written by the gate alone by
|
||||
// compare-and-set; its revision is the mesh's source revision. The indexer writes the other two.
|
||||
const (
|
||||
SourceBucket = GateModule + "_source"
|
||||
SourceMainKey = "main"
|
||||
SourceHistory = 64
|
||||
CandidatesBucket = IndexerModule + "_candidates"
|
||||
// CandidatesHistory is the revisions the candidates bucket keeps per key.
|
||||
CandidatesHistory = 10
|
||||
ModulesBucket = IndexerModule + "_modules"
|
||||
// ModulesHistory is the revisions the modules bucket keeps per key.
|
||||
ModulesHistory = 20
|
||||
)
|
||||
|
||||
// ForgeStream is the forge bridge's stream as its manifest declares it; the broker derives the real names.
|
||||
const ForgeStream = "forge"
|
||||
|
||||
// The kinds of forge message, the last part of a forge event's name.
|
||||
const (
|
||||
ForgePull = "pull"
|
||||
ForgeMerged = "merged"
|
||||
ForgeRefused = "refused"
|
||||
)
|
||||
|
||||
// The actions of a pull message.
|
||||
const (
|
||||
PullOpened = "opened"
|
||||
PullUpdated = "updated"
|
||||
PullClosed = "closed"
|
||||
)
|
||||
|
||||
// SourcePrefix opens every source fingerprint.
|
||||
const SourcePrefix = "src1:"
|
||||
|
||||
// CandidatesKey is the key, in CandidatesBucket, of a group: `<group>`, the branch name the group's pull requests
|
||||
// share (a slash is allowed); a lone pull request is a group of one under its branch name.
|
||||
func CandidatesKey(group string) (string, error) {
|
||||
if err := checkName("group", group); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return group, nil
|
||||
}
|
||||
|
||||
// ParseCandidatesKey is the inverse of CandidatesKey.
|
||||
func ParseCandidatesKey(key string) (group string, err error) {
|
||||
if group, err = CandidatesKey(key); err != nil {
|
||||
return "", fmt.Errorf("delivery: %q is not a candidates key: %w", key, err)
|
||||
}
|
||||
return group, nil
|
||||
}
|
||||
|
||||
// ModulesKey is the key, in ModulesBucket, of a module: `<module>`.
|
||||
func ModulesKey(module string) (string, error) {
|
||||
if err := checkName("module", module); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return module, nil
|
||||
}
|
||||
|
||||
// ParseModulesKey is the inverse of ModulesKey.
|
||||
func ParseModulesKey(key string) (module string, err error) {
|
||||
if module, err = ModulesKey(key); err != nil {
|
||||
return "", fmt.Errorf("delivery: %q is not a modules key: %w", key, err)
|
||||
}
|
||||
return module, nil
|
||||
}
|
||||
|
||||
// checkRepository refuses a repository that is not `<owner>/<name>`: exactly one slash, each part a valid name.
|
||||
func checkRepository(repository string) error {
|
||||
owner, name, ok := strings.Cut(repository, "/")
|
||||
if !ok || strings.Contains(name, "/") {
|
||||
return fmt.Errorf("delivery: the repository %q is not <owner>/<name>, with exactly one slash", repository)
|
||||
}
|
||||
if err := checkName("repository owner", owner); err != nil {
|
||||
return fmt.Errorf("delivery: the repository %q: %w", repository, err)
|
||||
}
|
||||
if err := checkName("repository name", name); err != nil {
|
||||
return fmt.Errorf("delivery: the repository %q: %w", repository, err)
|
||||
}
|
||||
if strings.Contains(owner, "/") {
|
||||
return fmt.Errorf("delivery: the repository %q is not <owner>/<name>, with exactly one slash", repository)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkForgeKind(kind string) error {
|
||||
switch kind {
|
||||
case ForgePull, ForgeMerged, ForgeRefused:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("delivery: the forge kind %q is not pull, merged or refused", kind)
|
||||
}
|
||||
|
||||
// ForgeEvent is the name of a forge message of the given kind: `forge.<owner>.<name>.<kind>`.
|
||||
func ForgeEvent(repository, kind string) (string, error) {
|
||||
if err := checkRepository(repository); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := checkForgeKind(kind); err != nil {
|
||||
return "", err
|
||||
}
|
||||
owner, name, _ := strings.Cut(repository, "/")
|
||||
return "forge." + owner + "." + name + "." + kind, nil
|
||||
}
|
||||
|
||||
// ParseForgeEvent is the inverse of ForgeEvent.
|
||||
func ParseForgeEvent(event string) (repository, kind string, err error) {
|
||||
parts := strings.Split(event, ".")
|
||||
if len(parts) != 4 || parts[0] != "forge" {
|
||||
return "", "", fmt.Errorf("delivery: %q is not a forge event, forge.<owner>.<name>.<kind>", event)
|
||||
}
|
||||
repository = parts[1] + "/" + parts[2]
|
||||
if _, err := ForgeEvent(repository, parts[3]); err != nil {
|
||||
return "", "", fmt.Errorf("delivery: %q is not a forge event: %w", event, err)
|
||||
}
|
||||
return repository, parts[3], nil
|
||||
}
|
||||
|
||||
// Head is one pull request at one commit: the repository as `<owner>/<name>` and the full sha.
|
||||
type Head struct {
|
||||
Repository string `json:"repository"`
|
||||
Number int `json:"number"`
|
||||
Head string `json:"head"`
|
||||
}
|
||||
|
||||
// HeadsHash is the first 12 hex digits of the sha256 over the lines `<repository>#<number>=<head>`, sorted and
|
||||
// joined by a newline: the same for every order of heads, different when any head changes. An empty list is refused.
|
||||
func HeadsHash(heads []Head) (string, error) {
|
||||
if len(heads) == 0 {
|
||||
return "", fmt.Errorf("delivery: a heads hash needs at least one pull request")
|
||||
}
|
||||
lines := make([]string, len(heads))
|
||||
for i, h := range heads {
|
||||
lines[i] = fmt.Sprintf("%s#%d=%s", h.Repository, h.Number, h.Head)
|
||||
}
|
||||
sort.Strings(lines)
|
||||
sum := sha256.Sum256([]byte(strings.Join(lines, "\n")))
|
||||
return hex.EncodeToString(sum[:])[:12], nil
|
||||
}
|
||||
|
||||
// Source is the value of SourceMainKey: every repository's merged head (repository to full sha, not only the
|
||||
// group's) and the group that moved it.
|
||||
type Source struct {
|
||||
V int `json:"v"`
|
||||
Group string `json:"group"`
|
||||
Heads map[string]string `json:"heads"`
|
||||
}
|
||||
|
||||
func (s Source) check() error {
|
||||
if len(s.Heads) == 0 {
|
||||
return fmt.Errorf("delivery: a source names no repository")
|
||||
}
|
||||
for repository, head := range s.Heads {
|
||||
if repository == "" {
|
||||
return fmt.Errorf("delivery: a source names a repository with no name")
|
||||
}
|
||||
if head == "" {
|
||||
return fmt.Errorf("delivery: the source's head of %s is empty", repository)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Candidate is the value of a candidates key: the group's pull requests and heads, the heads hash, the base (the
|
||||
// source revision it was previewed onto) and the modules each pull request touches. It carries no fingerprint
|
||||
// until release 4 (ADR 0437 §5).
|
||||
type Candidate struct {
|
||||
V int `json:"v"`
|
||||
Group string `json:"group"`
|
||||
Base uint64 `json:"base"`
|
||||
HeadsHash string `json:"heads"`
|
||||
Pulls []CandidatePull `json:"pulls"`
|
||||
}
|
||||
|
||||
// CandidatePull is one pull request of a candidate and the modules it touches.
|
||||
type CandidatePull struct {
|
||||
Head
|
||||
Modules []string `json:"modules"`
|
||||
}
|
||||
|
||||
func (c Candidate) check() error {
|
||||
if len(c.Pulls) == 0 {
|
||||
return fmt.Errorf("delivery: a candidate names no pull request")
|
||||
}
|
||||
heads := make([]Head, len(c.Pulls))
|
||||
for i, p := range c.Pulls {
|
||||
heads[i] = p.Head
|
||||
}
|
||||
want, _ := HeadsHash(heads)
|
||||
if c.HeadsHash != want {
|
||||
return fmt.Errorf("delivery: the candidate's heads hash %q is not the hash %q of its pull requests' heads", c.HeadsHash, want)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Module is the value of a modules key: the module's source fingerprint, or none and why (a build that resolves
|
||||
// packages when built gets none, said here), and where it was computed: Repository and Commit are the trunk merge,
|
||||
// Src the source revision then (0 before the gate writes source.main). Exactly one of FP and None is set.
|
||||
type Module struct {
|
||||
V int `json:"v"`
|
||||
FP string `json:"fp,omitempty"`
|
||||
None string `json:"none,omitempty"`
|
||||
Src uint64 `json:"src,omitempty"`
|
||||
Repository string `json:"repository"`
|
||||
Commit string `json:"commit"`
|
||||
}
|
||||
|
||||
func (m Module) check() error {
|
||||
switch {
|
||||
case m.FP != "" && m.None != "":
|
||||
return fmt.Errorf("delivery: a module has both a fingerprint and a reason for none")
|
||||
case m.FP == "" && m.None == "":
|
||||
return fmt.Errorf("delivery: a module has neither a fingerprint nor a reason for none")
|
||||
case m.FP != "" && !strings.HasPrefix(m.FP, SourcePrefix):
|
||||
return fmt.Errorf("delivery: the module's fingerprint %q does not start with %s", m.FP, SourcePrefix)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Forge is a message of the forge bridge's stream, trimmed to repository, branch, pull request and head. A pull
|
||||
// message says Action; a merged message carries the merge commit, the tree of every directory holding a
|
||||
// module.json and the repository's root tree; a refused message names the owed act it answers and the forge's own
|
||||
// words.
|
||||
type Forge struct {
|
||||
V int `json:"v"`
|
||||
Kind string `json:"kind"`
|
||||
Repository string `json:"repository"`
|
||||
Branch string `json:"branch,omitempty"`
|
||||
Number int `json:"number,omitempty"`
|
||||
Head string `json:"head,omitempty"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Merge string `json:"merge,omitempty"`
|
||||
Trees map[string]string `json:"trees,omitempty"`
|
||||
Root string `json:"root,omitempty"`
|
||||
Act string `json:"act,omitempty"`
|
||||
Words string `json:"words,omitempty"`
|
||||
}
|
||||
|
||||
func (f Forge) check() error {
|
||||
if err := checkRepository(f.Repository); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkForgeKind(f.Kind); err != nil {
|
||||
return err
|
||||
}
|
||||
switch f.Kind {
|
||||
case ForgePull:
|
||||
if f.Number <= 0 {
|
||||
return fmt.Errorf("delivery: a pull message needs its pull request number")
|
||||
}
|
||||
if f.Head == "" {
|
||||
return fmt.Errorf("delivery: a pull message needs its head")
|
||||
}
|
||||
switch f.Action {
|
||||
case PullOpened, PullUpdated, PullClosed:
|
||||
default:
|
||||
return fmt.Errorf("delivery: the pull action %q is not opened, updated or closed", f.Action)
|
||||
}
|
||||
case ForgeMerged:
|
||||
if f.Number <= 0 {
|
||||
return fmt.Errorf("delivery: a merged message needs its pull request number")
|
||||
}
|
||||
if f.Head == "" {
|
||||
return fmt.Errorf("delivery: a merged message needs its head")
|
||||
}
|
||||
if f.Merge == "" {
|
||||
return fmt.Errorf("delivery: a merged message needs its merge commit")
|
||||
}
|
||||
if f.Root == "" {
|
||||
return fmt.Errorf("delivery: a merged message needs the repository's root tree")
|
||||
}
|
||||
if len(f.Trees) == 0 {
|
||||
return fmt.Errorf("delivery: a merged message needs the tree of at least one directory holding a module.json")
|
||||
}
|
||||
for dir, tree := range f.Trees {
|
||||
if tree == "" {
|
||||
return fmt.Errorf("delivery: a merged message's tree of the directory %q is empty", dir)
|
||||
}
|
||||
}
|
||||
case ForgeRefused:
|
||||
if f.Act == "" {
|
||||
return fmt.Errorf("delivery: a refused message needs the act it answers")
|
||||
}
|
||||
if f.Words == "" {
|
||||
return fmt.Errorf("delivery: a refused message needs the forge's words")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeSource is s as JSON at Version; it refuses a source with no repository or an empty head.
|
||||
func EncodeSource(s Source) ([]byte, error) {
|
||||
if err := s.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.V = Version
|
||||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
// DecodeSource reads a source; it refuses an unknown version (ErrUnknownVersion) and what EncodeSource refuses.
|
||||
func DecodeSource(data []byte) (Source, error) {
|
||||
var s Source
|
||||
if err := decode(data, &s); err != nil {
|
||||
return Source{}, err
|
||||
}
|
||||
if err := s.check(); err != nil {
|
||||
return Source{}, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// EncodeCandidate is c as JSON at Version; it refuses a heads hash that is not the hash of its pull requests' heads.
|
||||
func EncodeCandidate(c Candidate) ([]byte, error) {
|
||||
if err := c.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.V = Version
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
// DecodeCandidate reads a candidate; it refuses an unknown version (ErrUnknownVersion) and what EncodeCandidate refuses.
|
||||
func DecodeCandidate(data []byte) (Candidate, error) {
|
||||
var c Candidate
|
||||
if err := decode(data, &c); err != nil {
|
||||
return Candidate{}, err
|
||||
}
|
||||
if err := c.check(); err != nil {
|
||||
return Candidate{}, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// EncodeModule is m as JSON at Version; it refuses both or neither of a fingerprint and a reason, and a
|
||||
// fingerprint not of the form src1:.
|
||||
func EncodeModule(m Module) ([]byte, error) {
|
||||
if err := m.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.V = Version
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// DecodeModule reads a module; it refuses an unknown version (ErrUnknownVersion) and what EncodeModule refuses.
|
||||
func DecodeModule(data []byte) (Module, error) {
|
||||
var m Module
|
||||
if err := decode(data, &m); err != nil {
|
||||
return Module{}, err
|
||||
}
|
||||
if err := m.check(); err != nil {
|
||||
return Module{}, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// EncodeForge is f as JSON at Version; it refuses a message missing what its kind needs.
|
||||
func EncodeForge(f Forge) ([]byte, error) {
|
||||
if err := f.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.V = Version
|
||||
return json.Marshal(f)
|
||||
}
|
||||
|
||||
// DecodeForge reads a forge message; it refuses an unknown version (ErrUnknownVersion) and what EncodeForge refuses.
|
||||
func DecodeForge(data []byte) (Forge, error) {
|
||||
var f Forge
|
||||
if err := decode(data, &f); err != nil {
|
||||
return Forge{}, err
|
||||
}
|
||||
if err := f.check(); err != nil {
|
||||
return Forge{}, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// SameSource says whether two sources are the same value, so a writer writes only when they differ.
|
||||
func SameSource(a, b Source) bool { return reflect.DeepEqual(a, b) }
|
||||
|
||||
// SameCandidate says whether two candidates are the same value.
|
||||
func SameCandidate(a, b Candidate) bool { return reflect.DeepEqual(a, b) }
|
||||
|
||||
// SameModule says whether two modules are the same value.
|
||||
func SameModule(a, b Module) bool { return a == b }
|
||||
|
||||
// SameForge says whether two forge messages are the same value.
|
||||
func SameForge(a, b Forge) bool { return reflect.DeepEqual(a, b) }
|
||||
@@ -0,0 +1,288 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSourceKeys(t *testing.T) { // 1200/AC1
|
||||
if k, err := CandidatesKey("feat/delivery-source-keys"); err != nil || k != "feat/delivery-source-keys" {
|
||||
t.Fatalf("candidates key = %q, %v", k, err)
|
||||
}
|
||||
if g, err := ParseCandidatesKey("feat/x"); err != nil || g != "feat/x" {
|
||||
t.Fatalf("parse candidates = %q, %v", g, err)
|
||||
}
|
||||
if k, err := ModulesKey("mesh-mail"); err != nil || k != "mesh-mail" {
|
||||
t.Fatalf("modules key = %q, %v", k, err)
|
||||
}
|
||||
if m, err := ParseModulesKey("mesh-mail"); err != nil || m != "mesh-mail" {
|
||||
t.Fatalf("parse modules = %q, %v", m, err)
|
||||
}
|
||||
for _, bad := range []string{"", "a.b", "a*b", "a>b", "a b", "a\tb"} {
|
||||
if _, err := CandidatesKey(bad); err == nil {
|
||||
t.Errorf("candidates key %q accepted", bad)
|
||||
}
|
||||
if _, err := ModulesKey(bad); err == nil {
|
||||
t.Errorf("modules key %q accepted", bad)
|
||||
}
|
||||
if _, err := ParseCandidatesKey(bad); err == nil {
|
||||
t.Errorf("parse candidates %q accepted", bad)
|
||||
}
|
||||
if _, err := ParseModulesKey(bad); err == nil {
|
||||
t.Errorf("parse modules %q accepted", bad)
|
||||
}
|
||||
}
|
||||
ev, err := ForgeEvent("novox/mesh-sdk", ForgeMerged)
|
||||
if err != nil || ev != "forge.novox.mesh-sdk.merged" {
|
||||
t.Fatalf("forge event = %q, %v", ev, err)
|
||||
}
|
||||
repo, kind, err := ParseForgeEvent(ev)
|
||||
if err != nil || repo != "novox/mesh-sdk" || kind != ForgeMerged {
|
||||
t.Fatalf("parse forge event = %q %q %v", repo, kind, err)
|
||||
}
|
||||
for _, k := range []string{ForgePull, ForgeMerged, ForgeRefused} {
|
||||
if _, err := ForgeEvent("a/b", k); err != nil {
|
||||
t.Errorf("kind %s refused: %v", k, err)
|
||||
}
|
||||
}
|
||||
for _, bad := range []struct{ repo, kind string }{
|
||||
{"", "pull"}, {"novox", "pull"}, {"a/b/c", "pull"}, {"/b", "pull"}, {"a/", "pull"},
|
||||
{"a.b/c", "pull"}, {"a/b c", "pull"}, {"a/b", "closed"}, {"a/b", ""},
|
||||
} {
|
||||
if _, err := ForgeEvent(bad.repo, bad.kind); err == nil {
|
||||
t.Errorf("forge event %q %q accepted", bad.repo, bad.kind)
|
||||
} else if bad.kind == "closed" && !strings.Contains(err.Error(), "closed") {
|
||||
t.Errorf("error does not name the kind: %v", err)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"", "forge.a.b", "forge.a.b.closed", "x.a.b.pull", "forge.a.b.pull.x", "forge..b.pull"} {
|
||||
if _, _, err := ParseForgeEvent(bad); err == nil {
|
||||
t.Errorf("parse forge event %q accepted", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadsHash(t *testing.T) { // 1200/AC2
|
||||
a := Head{Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa"}
|
||||
b := Head{Repository: "novox/mesh-control", Number: 7, Head: "bbbb"}
|
||||
h1, err := HeadsHash([]Head{a, b})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h2, _ := HeadsHash([]Head{b, a})
|
||||
if h1 != h2 {
|
||||
t.Errorf("order changes the hash: %s %s", h1, h2)
|
||||
}
|
||||
if len(h1) != 12 || strings.Trim(h1, "0123456789abcdef") != "" {
|
||||
t.Errorf("hash %q is not 12 hex digits", h1)
|
||||
}
|
||||
for _, changed := range []Head{
|
||||
{Repository: "novox/mesh-sdk", Number: 36, Head: "aaab"},
|
||||
{Repository: "novox/mesh-sdk", Number: 37, Head: "aaaa"},
|
||||
{Repository: "novox/other", Number: 36, Head: "aaaa"},
|
||||
} {
|
||||
h3, _ := HeadsHash([]Head{changed, b})
|
||||
if h3 == h1 {
|
||||
t.Errorf("changing %v keeps the hash", changed)
|
||||
}
|
||||
}
|
||||
if _, err := HeadsHash(nil); err == nil {
|
||||
t.Error("an empty list is accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func sampleSource() Source {
|
||||
return Source{Group: "feat/x", Heads: map[string]string{"novox/mesh-sdk": "abc123", "novox/mesh-control": "def456"}}
|
||||
}
|
||||
|
||||
func sampleCandidate() Candidate {
|
||||
pulls := []CandidatePull{
|
||||
{Head: Head{Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa"}, Modules: []string{"mesh-mail"}},
|
||||
{Head: Head{Repository: "novox/mesh-control", Number: 7, Head: "bbbb"}, Modules: []string{"mesh-controller"}},
|
||||
}
|
||||
hh, _ := HeadsHash([]Head{pulls[0].Head, pulls[1].Head})
|
||||
return Candidate{Group: "feat/x", Base: 12, HeadsHash: hh, Pulls: pulls}
|
||||
}
|
||||
|
||||
func sampleForges() []Forge {
|
||||
return []Forge{
|
||||
{Kind: ForgePull, Repository: "novox/mesh-sdk", Branch: "feat/x", Number: 36, Head: "aaaa", Action: "opened"},
|
||||
{Kind: ForgeMerged, Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa", Merge: "cccc",
|
||||
Trees: map[string]string{"modules/a": "t1"}, Root: "t0"},
|
||||
{Kind: ForgeRefused, Repository: "novox/mesh-sdk", Act: "novox/mesh-sdk.36.aaaa.view", Words: "403 forbidden"},
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceValuesRoundTrip(t *testing.T) { // 1200/AC3
|
||||
check := func(name string, data []byte, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
var probe map[string]any
|
||||
if err := json.Unmarshal(data, &probe); err != nil || probe["v"] != float64(1) {
|
||||
t.Fatalf("%s: json %s does not carry v:1", name, data)
|
||||
}
|
||||
}
|
||||
s := sampleSource()
|
||||
data, err := EncodeSource(s)
|
||||
check("source", data, err)
|
||||
s.V = Version
|
||||
if got, err := DecodeSource(data); err != nil || !SameSource(got, s) {
|
||||
t.Errorf("source round trip: %+v %v", got, err)
|
||||
}
|
||||
c := sampleCandidate()
|
||||
data, err = EncodeCandidate(c)
|
||||
check("candidate", data, err)
|
||||
c.V = Version
|
||||
if got, err := DecodeCandidate(data); err != nil || !SameCandidate(got, c) {
|
||||
t.Errorf("candidate round trip: %+v %v", got, err)
|
||||
}
|
||||
for _, m := range []Module{
|
||||
{FP: "src1:abc", Src: 5, Repository: "novox/mesh-sdk", Commit: "cccc"},
|
||||
{None: "a build that resolves packages when built", Repository: "novox/mesh-sdk", Commit: "cccc"},
|
||||
} {
|
||||
data, err = EncodeModule(m)
|
||||
check("module", data, err)
|
||||
m.V = Version
|
||||
if got, err := DecodeModule(data); err != nil || !SameModule(got, m) {
|
||||
t.Errorf("module round trip: %+v %v", got, err)
|
||||
}
|
||||
}
|
||||
for _, f := range sampleForges() {
|
||||
data, err = EncodeForge(f)
|
||||
check("forge "+f.Kind, data, err)
|
||||
f.V = Version
|
||||
if got, err := DecodeForge(data); err != nil || !SameForge(got, f) {
|
||||
t.Errorf("forge %s round trip: %+v %v", f.Kind, got, err)
|
||||
}
|
||||
}
|
||||
for name, dec := range map[string]func([]byte) error{
|
||||
"source": func(b []byte) error { _, err := DecodeSource(b); return err },
|
||||
"candidate": func(b []byte) error { _, err := DecodeCandidate(b); return err },
|
||||
"module": func(b []byte) error { _, err := DecodeModule(b); return err },
|
||||
"forge": func(b []byte) error { _, err := DecodeForge(b); return err },
|
||||
} {
|
||||
for _, in := range []string{`{"group":"g"}`, `{"v":2,"group":"g"}`} {
|
||||
if err := dec([]byte(in)); !errors.Is(err, ErrUnknownVersion) {
|
||||
t.Errorf("%s decode of %s = %v, want ErrUnknownVersion", name, in, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceValuesRefused(t *testing.T) { // 1200/AC4
|
||||
wantErr := func(name string, err error, words ...string) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Errorf("%s accepted", name)
|
||||
return
|
||||
}
|
||||
for _, w := range words {
|
||||
if !strings.Contains(err.Error(), w) {
|
||||
t.Errorf("%s: error %q does not name %q", name, err, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
_, err := EncodeSource(Source{Group: "g"})
|
||||
wantErr("source without repositories", err, "repositor")
|
||||
_, err = EncodeSource(Source{Group: "g", Heads: map[string]string{"a/b": ""}})
|
||||
wantErr("source with an empty head", err, "a/b")
|
||||
_, err = EncodeSource(Source{Group: "g", Heads: map[string]string{"": "x"}})
|
||||
wantErr("source with an empty repository", err, "repositor")
|
||||
|
||||
c := sampleCandidate()
|
||||
c.HeadsHash = "000000000000"
|
||||
_, err = EncodeCandidate(c)
|
||||
wantErr("candidate with a stale hash", err, "heads hash")
|
||||
c = sampleCandidate()
|
||||
c.Pulls = nil
|
||||
_, err = EncodeCandidate(c)
|
||||
wantErr("candidate without pulls", err, "pull")
|
||||
|
||||
_, err = EncodeModule(Module{FP: "src1:x", None: "why", Repository: "a/b", Commit: "c"})
|
||||
wantErr("module with both", err, "both")
|
||||
_, err = EncodeModule(Module{Repository: "a/b", Commit: "c"})
|
||||
wantErr("module with neither", err, "fingerprint", "none")
|
||||
_, err = EncodeModule(Module{FP: "abc", Repository: "a/b", Commit: "c"})
|
||||
wantErr("module with a bad fingerprint", err, "src1:")
|
||||
|
||||
f := sampleForges()[1]
|
||||
f.Merge = ""
|
||||
_, err = EncodeForge(f)
|
||||
wantErr("merged without merge commit", err, "merge")
|
||||
f = sampleForges()[1]
|
||||
f.Root = ""
|
||||
_, err = EncodeForge(f)
|
||||
wantErr("merged without root", err, "root")
|
||||
f = sampleForges()[1]
|
||||
f.Trees = nil
|
||||
_, err = EncodeForge(f)
|
||||
wantErr("merged without trees", err, "tree")
|
||||
f = sampleForges()[1]
|
||||
f.Trees = map[string]string{"modules/a": ""}
|
||||
_, err = EncodeForge(f)
|
||||
wantErr("merged with an empty tree", err, "modules/a")
|
||||
f = sampleForges()[0]
|
||||
f.Action = "reopened"
|
||||
_, err = EncodeForge(f)
|
||||
wantErr("pull with an unknown action", err, "reopened")
|
||||
f = sampleForges()[0]
|
||||
f.Head = ""
|
||||
_, err = EncodeForge(f)
|
||||
wantErr("pull without head", err, "head")
|
||||
f = sampleForges()[2]
|
||||
f.Words = ""
|
||||
_, err = EncodeForge(f)
|
||||
wantErr("refused without words", err, "words")
|
||||
f = sampleForges()[2]
|
||||
f.Act = ""
|
||||
_, err = EncodeForge(f)
|
||||
wantErr("refused without act", err, "act")
|
||||
_, err = EncodeForge(Forge{Kind: "closed", Repository: "a/b"})
|
||||
wantErr("unknown kind", err, "closed")
|
||||
_, err = EncodeForge(Forge{Kind: ForgePull, Repository: "nope", Number: 1, Head: "h", Action: "opened"})
|
||||
wantErr("bad repository", err, "nope")
|
||||
}
|
||||
|
||||
func TestSourceSame(t *testing.T) { // 1200/AC5
|
||||
if !SameSource(sampleSource(), sampleSource()) {
|
||||
t.Error("equal sources differ")
|
||||
}
|
||||
s := sampleSource()
|
||||
s.Heads["novox/mesh-sdk"] = "zzz"
|
||||
if SameSource(s, sampleSource()) {
|
||||
t.Error("different sources are the same")
|
||||
}
|
||||
if !SameCandidate(sampleCandidate(), sampleCandidate()) {
|
||||
t.Error("equal candidates differ")
|
||||
}
|
||||
c := sampleCandidate()
|
||||
c.Pulls[0].Modules = []string{"other"}
|
||||
if SameCandidate(c, sampleCandidate()) {
|
||||
t.Error("different candidates are the same")
|
||||
}
|
||||
m := Module{FP: "src1:a", Repository: "a/b", Commit: "c"}
|
||||
if !SameModule(m, m) || SameModule(m, Module{FP: "src1:b", Repository: "a/b", Commit: "c"}) {
|
||||
t.Error("SameModule wrong")
|
||||
}
|
||||
f := sampleForges()[1]
|
||||
g := sampleForges()[1]
|
||||
if !SameForge(f, g) {
|
||||
t.Error("equal forge messages differ")
|
||||
}
|
||||
g.Trees["modules/a"] = "other"
|
||||
if SameForge(f, g) {
|
||||
t.Error("different forge messages are the same")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceBuckets(t *testing.T) {
|
||||
if SourceBucket != "mesh-delivery-gate_source" || CandidatesBucket != "mesh-delivery-indexer_candidates" ||
|
||||
ModulesBucket != "mesh-delivery-indexer_modules" || SourceMainKey != "main" ||
|
||||
SourceHistory != 64 || CandidatesHistory != 10 || ModulesHistory != 20 {
|
||||
t.Error("a bucket or history differs from the decided layout")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user