Merge pull request 'The second reader release: the validator judges G's new forms (design 47 G §621, §697, §508)' (#205) from fix/47-G-reader-release-2 into main
This commit was merged in pull request #205.
This commit is contained in:
@@ -870,11 +870,7 @@ func (n *askNotes) note(ctx context.Context, language, id, step string) error {
|
|||||||
v.Asks = map[string]string{}
|
v.Asks = map[string]string{}
|
||||||
}
|
}
|
||||||
v.Asks[language] = id
|
v.Asks[language] = id
|
||||||
if err := n.b.Claimer.Finish(ctx, n.cl, v); err != nil {
|
return n.b.Claimer.Finish(ctx, n.cl, v)
|
||||||
return err
|
|
||||||
}
|
|
||||||
n.b.handAsk(n.cl.Key, language, id)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// runLane puts the lane's part of the build as one ask on its build seat's stream (the delivery stream for a trunk key, the
|
// runLane puts the lane's part of the build as one ask on its build seat's stream (the delivery stream for a trunk key, the
|
||||||
@@ -959,6 +955,9 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m mes
|
|||||||
l.err = err
|
l.err = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// A request to build the key again by hand is answered the ask as asked once it is put, not once it is noted: a
|
||||||
|
// put the stream refuses after the note answers the key's failure (BuildAgainResult.Asks).
|
||||||
|
b.handAsk(asked.cl.Key, language, ask.ID)
|
||||||
}
|
}
|
||||||
step, ask, err := b.answer(ctx, l, ask, asked, module, m.Fp2)
|
step, ask, err := b.answer(ctx, l, ask, asked, module, m.Fp2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -983,6 +983,8 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
|
|||||||
during bool
|
during bool
|
||||||
// standing is a key taken as it stands (left building): its claim is the key's own revision, not a new one.
|
// standing is a key taken as it stands (left building): its claim is the key's own revision, not a new one.
|
||||||
standing bool
|
standing bool
|
||||||
|
// putRefused is a stream that refuses the ask after its note was written to the key.
|
||||||
|
putRefused bool
|
||||||
}{
|
}{
|
||||||
{name: "build again: a look the builder queued during the by-hand build still runs afterwards", again: true, during: true,
|
{name: "build again: a look the builder queued during the by-hand build still runs afterwards", again: true, during: true,
|
||||||
key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}},
|
key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}},
|
||||||
@@ -1000,6 +1002,10 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
|
|||||||
{name: "build again: a build that fails before its asks says the key failed, not that it was asked", again: true, failed: "cannot be read",
|
{name: "build again: a build that fails before its asks says the key failed, not that it was asked", again: true, failed: "cannot be read",
|
||||||
edit: func(m *meshcatalog.ModulesByModuleValue) { m.Manifest = mdigest('b') },
|
edit: func(m *meshcatalog.ModulesByModuleValue) { m.Manifest = mdigest('b') },
|
||||||
key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}},
|
key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}},
|
||||||
|
// An ask is answered as asked only once it is put, not once it is noted in the key (BuildAgainResult.Asks): a put
|
||||||
|
// the stream refuses after the note answers the key's failure and no ask id.
|
||||||
|
{name: "build again: an ask its stream refuses after its note answers the key failed, not asked", again: true, putRefused: true,
|
||||||
|
failed: "the stream refused the ask", key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}},
|
||||||
{name: "build again: a failed key is written building as a new revision and asks the seat", again: true,
|
{name: "build again: a failed key is written building as a new revision and asks the seat", again: true,
|
||||||
key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: the step failed: no tun"}},
|
key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: the step failed: no tun"}},
|
||||||
{name: "build again: a done key is refused, nothing to do", again: true, refuse: "is done: nothing to do",
|
{name: "build again: a done key is refused, nothing to do", again: true, refuse: "is done: nothing to do",
|
||||||
@@ -1057,6 +1063,9 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
|
|||||||
r.b.hands[r.key] = &handWatch{}
|
r.b.hands[r.key] = &handWatch{}
|
||||||
}
|
}
|
||||||
r.b.known["m"] = m // the builder has read the modules value, and looks at nothing until asked
|
r.b.known["m"] = m // the builder has read the modules value, and looks at nothing until asked
|
||||||
|
if c.putRefused {
|
||||||
|
r.b.Asks = buildrun.Asker{Publisher: refusingPublisher{}, Waiter: r.asks, Bound: 2 * time.Second}
|
||||||
|
}
|
||||||
if c.standing {
|
if c.standing {
|
||||||
r.b.tried[r.key] = true // its look of the key ended before its write: a later look takes over nothing by itself
|
r.b.tried[r.key] = true // its look of the key ended before its write: a later look takes over nothing by itself
|
||||||
}
|
}
|
||||||
@@ -1330,6 +1339,13 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// refusingPublisher is an asks stream that refuses every ask.
|
||||||
|
type refusingPublisher struct{}
|
||||||
|
|
||||||
|
func (refusingPublisher) PublishAsk(context.Context, string, string, []byte) error {
|
||||||
|
return errors.New("the stream refused the ask")
|
||||||
|
}
|
||||||
|
|
||||||
// heldAsks says an ask is held when its id is among held.
|
// heldAsks says an ask is held when its id is among held.
|
||||||
type heldAsks map[string]bool
|
type heldAsks map[string]bool
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ func Between(shelf catalogue.Shelf, longestMachine int, known KnownPackages) []s
|
|||||||
between = append(between, PackagesCatalogueProblems(shelf, known)...)
|
between = append(between, PackagesCatalogueProblems(shelf, known)...)
|
||||||
between = append(between, IdentityProblems(shelf, longestMachine)...)
|
between = append(between, IdentityProblems(shelf, longestMachine)...)
|
||||||
between = append(between, DataProblems(shelf)...)
|
between = append(between, DataProblems(shelf)...)
|
||||||
|
between = append(between, nodeEngineClaimProblems(shelf)...)
|
||||||
sort.Strings(between)
|
sort.Strings(between)
|
||||||
return slices.Compact(between)
|
return slices.Compact(between)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1706,6 +1706,19 @@ func EventProblems(m catalogue.Manifest) []string {
|
|||||||
m.Module, c, catalogue.TheRest))
|
m.Module, c, catalogue.TheRest))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if seat, state, isSeat, err := catalogue.SeatViewConsume(c); isSeat {
|
||||||
|
// A consumed view of a seat (design 47 G §616, §699): the seat offers the state as a view; the holder is resolved
|
||||||
|
// where the view is followed.
|
||||||
|
switch s, known := catalogue.SeatNamed(seat); {
|
||||||
|
case err != nil:
|
||||||
|
problems = append(problems, fmt.Sprintf("%s consumes %v", m.Module, err))
|
||||||
|
case !known:
|
||||||
|
problems = append(problems, fmt.Sprintf("%s consumes %q, a view of the seat %q, "+catalogue.UndeclaredSeat, m.Module, c, seat))
|
||||||
|
case !slices.Contains(s.Views, state):
|
||||||
|
problems = append(problems, fmt.Sprintf("%s consumes %q, and the seat %s does not offer the state %q as a view (design 47 G §699)", m.Module, c, seat, state))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err := consumePattern(c); err != nil {
|
if err := consumePattern(c); err != nil {
|
||||||
problems = append(problems, fmt.Sprintf("%s consumes %q: %v", m.Module, c, err))
|
problems = append(problems, fmt.Sprintf("%s consumes %q: %v", m.Module, c, err))
|
||||||
}
|
}
|
||||||
@@ -4638,13 +4651,6 @@ func claimProblems(m catalogue.Manifest) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range m.Claims {
|
for _, c := range m.Claims {
|
||||||
if s, known := catalogue.SeatNamed(c.Name); known && s.HeldByTheEngine {
|
|
||||||
// The node's engine holds it on every node, and no module may (novox/hq ADR 0563 §1): a module answering
|
|
||||||
// root-free or a hand-over would be an agent answering it.
|
|
||||||
problems = append(problems, fmt.Sprintf("%s claims %s, which every node's engine holds itself and no "+
|
|
||||||
"module may (novox/hq ADR 0563 §1)", m.Module, c.Name))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, known := catalogue.SeatNamed(c.Name); known {
|
if _, known := catalogue.SeatNamed(c.Name); known {
|
||||||
// **A seat's scope and what it delivers are not judged here** (novox/hq ADR 0122).
|
// **A seat's scope and what it delivers are not judged here** (novox/hq ADR 0122).
|
||||||
// This function runs wherever a manifest is parsed, and one of those places is the
|
// This function runs wherever a manifest is parsed, and one of those places is the
|
||||||
@@ -5565,12 +5571,15 @@ var streamsKeptUntilMoved = map[string][]string{
|
|||||||
"mesh-delivery-forge-bridge": {"forge"},
|
"mesh-delivery-forge-bridge": {"forge"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// PredefinedFormProblems refuses each stream a module declares that streamsKeptUntilMoved does not name for it.
|
// PredefinedFormProblems refuses each stream a module declares that streamsKeptUntilMoved does not name for it, but a
|
||||||
|
// foundation part's.
|
||||||
func PredefinedFormProblems(m catalogue.Manifest) []string {
|
func PredefinedFormProblems(m catalogue.Manifest) []string {
|
||||||
var out []string
|
var out []string
|
||||||
for _, s := range m.Streams {
|
for _, s := range m.Streams {
|
||||||
// A stream that keeps its history is one of the bus's predefined forms (novox/hq ADR 0592 §6).
|
// A stream that keeps its history is one of the bus's predefined forms (novox/hq ADR 0592 §6).
|
||||||
if !s.KeepsHistory() && !slices.Contains(streamsKeptUntilMoved[m.Module], s.Name) {
|
// A foundation part keeps its old sections for good, its streams among them: its findings stream
|
||||||
|
// `<module>-findings` (design 47 G §529, A-30, A-146).
|
||||||
|
if !s.KeepsHistory() && !slices.Contains(Foundation, m.Module) && !slices.Contains(streamsKeptUntilMoved[m.Module], s.Name) {
|
||||||
out = append(out, fmt.Sprintf("%s keeps the stream %q, outside the bus's predefined forms: keep it as a state, "+
|
out = append(out, fmt.Sprintf("%s keeps the stream %q, outside the bus's predefined forms: keep it as a state, "+
|
||||||
"a log or an object store, which the runtime reads for the module's tools and others read by `reads`", m.Module, s.Name))
|
"a log or an object store, which the runtime reads for the module's tools and others read by `reads`", m.Module, s.Name))
|
||||||
}
|
}
|
||||||
@@ -7905,3 +7914,22 @@ func ObjectsCatalogueProblems(shelf catalogue.Shelf) []string {
|
|||||||
}
|
}
|
||||||
return problems
|
return problems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NodeEngineSeat is the seat the node-engine's module claims as one module (design 47 G §508).
|
||||||
|
const NodeEngineSeat = "node-engine"
|
||||||
|
|
||||||
|
// nodeEngineClaimProblems refuses a revision in which more than one module claims the seat node-engine, naming them: one
|
||||||
|
// module claims it, the node-engine's own (design 47 G §508), in place of refusing every claim.
|
||||||
|
func nodeEngineClaimProblems(shelf catalogue.Shelf) []string {
|
||||||
|
var holders []string
|
||||||
|
for module, m := range shelf {
|
||||||
|
if slices.ContainsFunc(m.Claims, func(c catalogue.Claim) bool { return c.Name == NodeEngineSeat }) {
|
||||||
|
holders = append(holders, module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(holders) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
slices.Sort(holders)
|
||||||
|
return []string{fmt.Sprintf("%s claim the seat %s, which one module claims (design 47 G §508)", strings.Join(holders, " and "), NodeEngineSeat)}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2929,7 +2929,7 @@ func caseAStateNameIsLocalAndOneToken(t *testing.T) {
|
|||||||
{`{"module":"a.b","version":"1","state":["s"]}`, `no dot`},
|
{`{"module":"a.b","version":"1","state":["s"]}`, `no dot`},
|
||||||
{`{"module":"a","version":"1","reads":["bindings"]}`, `a read is <module>.<name>`},
|
{`{"module":"a","version":"1","reads":["bindings"]}`, `a read is <module>.<name>`},
|
||||||
{`{"module":"a","version":"1","reads":["a.s"]}`, `which is its own state`},
|
{`{"module":"a","version":"1","reads":["a.s"]}`, `which is its own state`},
|
||||||
{`{"module":"a","version":"1","state":[{"name":"s","shared":true}]}`, `{name, history, ttl-seconds, per-machine, key-ttl, read-only, form, catch-up}`},
|
{`{"module":"a","version":"1","state":[{"name":"s","shared":true}]}`, `{name, history, ttl-seconds, per-machine, key-ttl, read-only, max-mib, form, catch-up}`},
|
||||||
} {
|
} {
|
||||||
_, err := ParseManifest([]byte(c.manifest))
|
_, err := ParseManifest([]byte(c.manifest))
|
||||||
if c.says == "" {
|
if c.says == "" {
|
||||||
@@ -5111,6 +5111,11 @@ func caseTwoModulesCannotDeclareOneStream(t *testing.T) {
|
|||||||
if got := PredefinedFormProblems(catalogue.Manifest{Module: "mesh-mail", Streams: a.Streams}); len(got) != 0 {
|
if got := PredefinedFormProblems(catalogue.Manifest{Module: "mesh-mail", Streams: a.Streams}); len(got) != 0 {
|
||||||
t.Fatalf("a stream kept until it moves was refused: %v", got)
|
t.Fatalf("a stream kept until it moves was refused: %v", got)
|
||||||
}
|
}
|
||||||
|
// A foundation part keeps its old sections for good: its findings stream is accepted (design 47 G §529, A-30, A-146).
|
||||||
|
findings := []catalogue.StreamDeclaration{{Name: "mesh-delivery-indexer-findings", Events: []string{"finding-written.*"}}}
|
||||||
|
if got := PredefinedFormProblems(catalogue.Manifest{Module: "mesh-delivery-indexer", Streams: findings}); len(got) != 0 {
|
||||||
|
t.Fatalf("a foundation part's findings stream was refused: %v", got)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A seat name meaning two protocols is the failure nobody could diagnose afterwards.
|
// A seat name meaning two protocols is the failure nobody could diagnose afterwards.
|
||||||
@@ -6379,4 +6384,58 @@ func TestRulesOther(t *testing.T) {
|
|||||||
t.Run("AskingForTheRuleSetWithNowhereToPutItIsRefused", caseAskingForTheRuleSetWithNowhereToPutItIsRefused)
|
t.Run("AskingForTheRuleSetWithNowhereToPutItIsRefused", caseAskingForTheRuleSetWithNowhereToPutItIsRefused)
|
||||||
t.Run("TakesSettingsIsRefusedWhereItCannotMeanAnything", caseTakesSettingsIsRefusedWhereItCannotMeanAnything)
|
t.Run("TakesSettingsIsRefusedWhereItCannotMeanAnything", caseTakesSettingsIsRefusedWhereItCannotMeanAnything)
|
||||||
t.Run("TheInstallationCheckReadsADefault", caseTheInstallationCheckReadsADefault)
|
t.Run("TheInstallationCheckReadsADefault", caseTheInstallationCheckReadsADefault)
|
||||||
|
t.Run("ASeatNamedViewIsJudgedAgainstTheSeatsViews", caseASeatNamedViewIsJudgedAgainstTheSeatsViews)
|
||||||
|
t.Run("OneModuleClaimsTheNodeEnginesSeat", caseOneModuleClaimsTheNodeEnginesSeat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A consumed view, `seat:<seat>.views.<state>`, is accepted where the seat offers the state as a view and refused, by name,
|
||||||
|
// where it does not or the seat is unknown (design 47 G §616, §699, §621).
|
||||||
|
func caseASeatNamedViewIsJudgedAgainstTheSeatsViews(t *testing.T) {
|
||||||
|
was := catalogue.Seats()
|
||||||
|
t.Cleanup(func() { catalogue.UseSeats(was) })
|
||||||
|
catalogue.UseSeats([]catalogue.Seat{{Name: "mesh-module-assigner", Scope: catalogue.ScopeMesh, Decision: "test",
|
||||||
|
Views: []string{"assignments"}}})
|
||||||
|
for _, row := range []struct{ consume, says string }{
|
||||||
|
{"seat:mesh-module-assigner.views.assignments", ""},
|
||||||
|
{"seat:mesh-module-assigner.views.placement", "does not offer the state"},
|
||||||
|
{"seat:mesh-unknown.views.assignments", "mesh-unknown"},
|
||||||
|
{"seat:mesh-module-assigner.assignments", "does not name a seat and its view"},
|
||||||
|
} {
|
||||||
|
problems := strings.Join(EventProblems(catalogue.Manifest{Module: "reader", Consumes: []string{row.consume}}), "\n")
|
||||||
|
if row.says == "" && problems != "" || row.says != "" && !strings.Contains(problems, row.says) {
|
||||||
|
t.Errorf("%s: problems %q, want %q", row.consume, problems, row.says)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One module of a revision claims the seat node-engine and is accepted; a second is refused naming both (design 47 G §508).
|
||||||
|
func caseOneModuleClaimsTheNodeEnginesSeat(t *testing.T) {
|
||||||
|
was := catalogue.Seats()
|
||||||
|
t.Cleanup(func() { catalogue.UseSeats(was) })
|
||||||
|
catalogue.UseSeats([]catalogue.Seat{{Name: NodeEngineSeat, Scope: catalogue.ScopeNode, Decision: "test", HeldByTheEngine: true}})
|
||||||
|
engine := func(module string) []byte {
|
||||||
|
return []byte(`{"module":"` + module + `","version":"1","claims":[{"name":"node-engine","scope":"node"}]}`)
|
||||||
|
}
|
||||||
|
for _, row := range []struct {
|
||||||
|
name string
|
||||||
|
raws map[string][]byte
|
||||||
|
wants string
|
||||||
|
}{
|
||||||
|
{"one claim", map[string][]byte{"mesh-host": engine("mesh-host")}, ""},
|
||||||
|
{"two claims", map[string][]byte{"mesh-host": engine("mesh-host"), "other": engine("other")},
|
||||||
|
"mesh-host and other claim the seat node-engine, which one module claims"},
|
||||||
|
} {
|
||||||
|
shelf := catalogue.Shelf{}
|
||||||
|
for name, raw := range row.raws {
|
||||||
|
m, problems := Manifest(raw, nil)
|
||||||
|
if len(problems) > 0 {
|
||||||
|
t.Fatalf("%s: %s refused alone: %v", row.name, name, problems)
|
||||||
|
}
|
||||||
|
shelf[name] = m
|
||||||
|
}
|
||||||
|
got := strings.Join(Between(shelf, 0, nil), "\n")
|
||||||
|
if row.wants == "" && strings.Contains(got, "node-engine") || row.wants != "" && !strings.Contains(got, row.wants) {
|
||||||
|
t.Errorf("%s: %s", row.name, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user