Merge pull request 'Say what relates two changes, before anything acts on it (hq issue 525)' (#242) from feat/525-what-relates-two-changes into main
This commit was merged in pull request #242.
This commit is contained in:
@@ -0,0 +1,111 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
|
)
|
||||||
|
|
||||||
|
// What relates two changes (novox/hq ADR 0304 decision 1, as ADR 0306 decision 3 reads it): two changes are
|
||||||
|
// related when they share a repository, a module, a delivery group's head, or an edge of the module graph
|
||||||
|
// between their modules. A module is every one the walk builds or sends: the modules its merges moved and the
|
||||||
|
// modules that depend on them, not only the ones the merges touched. Unrelated changes may walk beside each
|
||||||
|
// other (ADR 0304 decision 2, ADR 0282 decision 3); nothing walks so yet, and nothing but tests reads this.
|
||||||
|
|
||||||
|
// footprint is what one change reaches: its repositories (lower case), its modules, and its delivery group
|
||||||
|
// heads (the head branch names of its merges). unknown says a merge of it was kept without the modules it
|
||||||
|
// moves, so what it reaches is not known: it is related to every change, never wrongly to none.
|
||||||
|
type footprint struct {
|
||||||
|
repos, modules, heads map[string]bool
|
||||||
|
unknown string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFootprint() footprint {
|
||||||
|
return footprint{repos: map[string]bool{}, modules: map[string]bool{}, heads: map[string]bool{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// footprintOfWalk is a walk's footprint: the repositories it carries and the modules it plans. A walk keeps no
|
||||||
|
// head of its merges; kept are the merges the record holds for it, whose announcements say their heads, and
|
||||||
|
// may be nil.
|
||||||
|
func footprintOfWalk(p inventory.Plan, kept []inventory.BatchedMerge) footprint {
|
||||||
|
f := newFootprint()
|
||||||
|
for _, c := range p.Carried() {
|
||||||
|
f.repos[strings.ToLower(c.Repository)] = true
|
||||||
|
}
|
||||||
|
for name := range p.Modules {
|
||||||
|
f.modules[name] = true
|
||||||
|
}
|
||||||
|
for _, m := range kept {
|
||||||
|
f.repos[strings.ToLower(m.Repository)] = true
|
||||||
|
if h := announcedOf(m).Head; h != "" {
|
||||||
|
f.heads[h] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// footprintOfBatch is an assembling batch's footprint: each merge's repository and head, and the modules its
|
||||||
|
// merges moved when heard, widened to their dependents (planOfMoves). A merge kept with no moves (one owed a
|
||||||
|
// later look, or kept for its packaging) is judged again at the cut and may move anything then: its batch is
|
||||||
|
// unknown, related to everything.
|
||||||
|
func footprintOfBatch(merges []inventory.BatchedMerge, edges []inventory.Edge) footprint {
|
||||||
|
f := newFootprint()
|
||||||
|
var moved []string
|
||||||
|
for _, m := range merges {
|
||||||
|
k := announcedOf(m)
|
||||||
|
f.repos[strings.ToLower(m.Repository)] = true
|
||||||
|
if k.Head != "" {
|
||||||
|
f.heads[k.Head] = true
|
||||||
|
}
|
||||||
|
if len(k.Moves) == 0 && f.unknown == "" {
|
||||||
|
f.unknown = "the modules a merge of " + m.Repository + " moves are not known until its batch is cut"
|
||||||
|
}
|
||||||
|
moved = append(moved, k.Moves...)
|
||||||
|
}
|
||||||
|
for _, name := range reachableFrom(moved, edges) {
|
||||||
|
f.modules[name] = true
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// related says whether two changes are related, and the first reason in plain words; false and empty when
|
||||||
|
// they are not.
|
||||||
|
func related(a, b footprint, edges []inventory.Edge) (bool, string) {
|
||||||
|
if a.unknown != "" {
|
||||||
|
return true, a.unknown
|
||||||
|
}
|
||||||
|
if b.unknown != "" {
|
||||||
|
return true, b.unknown
|
||||||
|
}
|
||||||
|
if r := firstShared(a.repos, b.repos); r != "" {
|
||||||
|
return true, "both change " + r
|
||||||
|
}
|
||||||
|
if m := firstShared(a.modules, b.modules); m != "" {
|
||||||
|
return true, "both move " + m
|
||||||
|
}
|
||||||
|
if h := firstShared(a.heads, b.heads); h != "" {
|
||||||
|
return true, "both are members of group " + h
|
||||||
|
}
|
||||||
|
for _, e := range edges {
|
||||||
|
if a.modules[e.From] && b.modules[e.To] || b.modules[e.From] && a.modules[e.To] {
|
||||||
|
return true, e.From + " depends on " + e.To + " (" + e.Kind + ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstShared is the first key, in order, both sets hold; empty when none.
|
||||||
|
func firstShared(a, b map[string]bool) string {
|
||||||
|
var both []string
|
||||||
|
for k := range a {
|
||||||
|
if b[k] {
|
||||||
|
both = append(both, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(both) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
sort.Strings(both)
|
||||||
|
return both[0]
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
|
)
|
||||||
|
|
||||||
|
// heardMerge is a kept merge of a repository from a head branch that moved modules.
|
||||||
|
func heardMerge(repository, head string, moves ...string) inventory.BatchedMerge {
|
||||||
|
m := inventory.BatchedMerge{Repository: repository, Branch: "main", Commit: "c-" + repository}
|
||||||
|
m.Event, _ = json.Marshal(keptMerge{SourceMoved: link.SourceMoved{Head: head}, Moves: moves})
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// walkOf is a walk carrying one repository's commit and planning modules.
|
||||||
|
func walkOf(repository string, modules ...string) inventory.Plan {
|
||||||
|
p := inventory.Plan{Repository: repository, Commit: "w-" + repository, Modules: map[string]*inventory.PlanModule{}}
|
||||||
|
for _, name := range modules {
|
||||||
|
p.Modules[name] = &inventory.PlanModule{}
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRelatedChanges: two changes are related by a repository, a module, a group head or a graph edge between
|
||||||
|
// their modules, and by a module reached only through dependents (novox/hq ADR 0304 decision 1, ADR 0306
|
||||||
|
// decision 3); otherwise they are not.
|
||||||
|
func TestRelatedChanges(t *testing.T) {
|
||||||
|
edges := []inventory.Edge{
|
||||||
|
{From: "nzbget", To: "base-image", Kind: inventory.EdgeStandsOn},
|
||||||
|
{From: "app", To: "build-machine", Kind: inventory.EdgeBuiltBy},
|
||||||
|
}
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
a, b footprint
|
||||||
|
why string
|
||||||
|
}{
|
||||||
|
{"repository, case aside",
|
||||||
|
footprintOfWalk(walkOf("novox/Mesh-Catalog", "notes"), nil),
|
||||||
|
footprintOfBatch([]inventory.BatchedMerge{heardMerge("NOVOX/mesh-catalog", "fix/a", "app")}, nil),
|
||||||
|
"both change novox/mesh-catalog"},
|
||||||
|
{"module",
|
||||||
|
footprintOfWalk(walkOf("novox/one", "nzbget"), nil),
|
||||||
|
footprintOfBatch([]inventory.BatchedMerge{heardMerge("novox/two", "fix/b", "nzbget")}, nil),
|
||||||
|
"both move nzbget"},
|
||||||
|
{"edge, walk depends on batch",
|
||||||
|
footprintOfWalk(walkOf("novox/one", "nzbget"), nil),
|
||||||
|
footprintOfBatch([]inventory.BatchedMerge{heardMerge("novox/two", "fix/b", "base-image")}, nil),
|
||||||
|
"nzbget depends on base-image (stands-on)"},
|
||||||
|
{"edge, batch depends on walk",
|
||||||
|
footprintOfWalk(walkOf("novox/one", "build-machine"), nil),
|
||||||
|
footprintOfBatch([]inventory.BatchedMerge{heardMerge("novox/two", "fix/b", "app")}, nil),
|
||||||
|
"app depends on build-machine (built-by)"},
|
||||||
|
{"group head",
|
||||||
|
footprintOfWalk(walkOf("novox/one", "notes"), []inventory.BatchedMerge{heardMerge("novox/one", "feat/x")}),
|
||||||
|
footprintOfBatch([]inventory.BatchedMerge{heardMerge("novox/two", "feat/x", "app")}, nil),
|
||||||
|
"both are members of group feat/x"},
|
||||||
|
{"module reached through dependents",
|
||||||
|
footprintOfWalk(walkOf("novox/one", "nzbget"), nil),
|
||||||
|
footprintOfBatch([]inventory.BatchedMerge{heardMerge("novox/two", "fix/b", "base-image")}, edges),
|
||||||
|
"both move nzbget"},
|
||||||
|
{"unrelated",
|
||||||
|
footprintOfWalk(walkOf("novox/one", "notes"), []inventory.BatchedMerge{heardMerge("novox/one", "feat/x")}),
|
||||||
|
footprintOfBatch([]inventory.BatchedMerge{heardMerge("novox/two", "feat/y", "app")}, edges),
|
||||||
|
""},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
for _, pair := range [][2]footprint{{c.a, c.b}, {c.b, c.a}} {
|
||||||
|
is, why := related(pair[0], pair[1], edges)
|
||||||
|
if is != (c.why != "") || why != c.why {
|
||||||
|
t.Errorf("related = %v, %q; want %q", is, why, c.why)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFootprintOfBatchWidensToDependents: a batch's modules are what its merges moved and every module that
|
||||||
|
// depends on them, along the edges that widen a walk.
|
||||||
|
func TestFootprintOfBatchWidensToDependents(t *testing.T) {
|
||||||
|
edges := []inventory.Edge{
|
||||||
|
{From: "nzbget", To: "base-image", Kind: inventory.EdgeStandsOn},
|
||||||
|
{From: "base-image", To: "build-machine", Kind: inventory.EdgeBuiltBy},
|
||||||
|
}
|
||||||
|
f := footprintOfBatch([]inventory.BatchedMerge{heardMerge("novox/two", "", "base-image")}, edges)
|
||||||
|
if !f.modules["base-image"] || !f.modules["nzbget"] || f.modules["build-machine"] || len(f.heads) != 0 {
|
||||||
|
t.Errorf("footprint = %+v; want base-image and nzbget, no head", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A batch holding a merge kept with no moves (one owed a later look, judged again at the cut) is related to
|
||||||
|
// every change, in both orders: what it will move is not known, and an unknown change is never unrelated
|
||||||
|
// (issue 525 review).
|
||||||
|
func TestABatchWhoseMovesAreNotKnownIsRelatedToEverything(t *testing.T) {
|
||||||
|
unknown := footprintOfBatch([]inventory.BatchedMerge{heardMerge("novox/one", "fix/a", "app"), heardMerge("novox/two", "fix/b")}, nil)
|
||||||
|
other := footprintOfWalk(walkOf("novox/three", "nzbget"), nil)
|
||||||
|
for _, pair := range [][2]footprint{{unknown, other}, {other, unknown}} {
|
||||||
|
if ok, why := related(pair[0], pair[1], nil); !ok || !strings.Contains(why, "not known") {
|
||||||
|
t.Fatalf("an unknown batch read as %v (%q)", ok, why)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
known := footprintOfBatch([]inventory.BatchedMerge{heardMerge("novox/one", "fix/a", "app")}, nil)
|
||||||
|
if ok, why := related(known, other, nil); ok {
|
||||||
|
t.Fatalf("a known, unrelated batch read as related: %q", why)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user