TestEveryMachineComposesAlikeBothWays reads the facts snapshot MESH_FACTS names (the live one in every check and try), else testdata/facts-equal.json, else the snapshot of a mesh of its own; raises it into throwaway stores as the merge gate does; and compares, machine by machine: (a) Foreseeing on the fresh store (error or none, the validator's problems, LeftOut and its reasons, Withheld, Unbound, Unplaced, HandedOnly, the own secrets named foreseen, and the ports mesh-sdk names foreseen against the ones the controller's send allocates); (b) the send, on two stores, the controller's Allocating composition and the adapter's compose, perform and compose again (ports, pair credentials and own secrets made, by key); (c) after the send, Reading both ways on one store, byte for byte. A difference names the machine, the comparison and the first resource and field. The comparison itself is tested without a store, and failed first when the body was not read. The test is deleted with the controller's composition (issue 1389).
247 lines
8.3 KiB
Go
247 lines
8.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The comparison of the controller's composition and mesh-sdk's (novox/hq ADR 0453 clause 3, design 47 R4b, issue 1384),
|
|
// the part of it that needs no store. The test that raises a mesh and composes every machine both ways is
|
|
// compose_equal_integration_test.go; both are deleted with the controller's own composition (issue 1389).
|
|
|
|
// composedBoth is what one composition of a machine said, in the terms the two paths are compared in.
|
|
type composedBoth struct {
|
|
Err string
|
|
Problems []string
|
|
LeftOut map[string]string
|
|
Withheld []string
|
|
Unbound []string
|
|
Unplaced []string
|
|
Handed []string
|
|
// Foreseen are the own secrets the composition names foreseen, as `module/name`.
|
|
Foreseen []string
|
|
Body []byte
|
|
}
|
|
|
|
// capturedFrom is a sendable, its validator problems and the error that came with it, as compared.
|
|
func capturedFrom(d sendable, problems []string, err error) composedBoth {
|
|
if err != nil {
|
|
return composedBoth{Err: err.Error()}
|
|
}
|
|
c := composedBoth{Problems: append([]string(nil), problems...), LeftOut: d.leftOutWhy,
|
|
Unplaced: append([]string(nil), d.unplaced...), Handed: append([]string(nil), d.handedOnly...),
|
|
Foreseen: append([]string(nil), d.foreseen...)}
|
|
for _, o := range d.withheld {
|
|
c.Withheld = append(c.Withheld, o.String())
|
|
}
|
|
for _, u := range d.unbound {
|
|
c.Unbound = append(c.Unbound, u.String())
|
|
}
|
|
sort.Strings(c.Problems)
|
|
sort.Strings(c.Withheld)
|
|
sort.Strings(c.Unbound)
|
|
sort.Strings(c.Unplaced)
|
|
sort.Strings(c.Handed)
|
|
sort.Strings(c.Foreseen)
|
|
c.Body, _ = d.Body()
|
|
return c
|
|
}
|
|
|
|
// bothWaysDiffer says how two compositions of one machine differ, one line for each field that does, naming the machine and
|
|
// the comparison. A machine both refuse alike is no difference. withBody compares the declaration byte for byte and names
|
|
// the first resource that differs.
|
|
func bothWaysDiffer(machine, comparison string, old, sdk composedBoth, withBody bool) []string {
|
|
var out []string
|
|
say := func(field string, a, b any) {
|
|
out = append(out, fmt.Sprintf("%s, %s: %s differs: the controller's %v, mesh-sdk's %v", machine, comparison, field, a, b))
|
|
}
|
|
if old.Err != sdk.Err {
|
|
say("the refusal", quoted(old.Err), quoted(sdk.Err))
|
|
return out
|
|
}
|
|
if old.Err != "" {
|
|
return nil
|
|
}
|
|
for _, f := range []struct {
|
|
name string
|
|
old, sdk []string
|
|
}{
|
|
{"the validator's problems", old.Problems, sdk.Problems},
|
|
{"Withheld", old.Withheld, sdk.Withheld},
|
|
{"Unbound", old.Unbound, sdk.Unbound},
|
|
{"Unplaced", old.Unplaced, sdk.Unplaced},
|
|
{"HandedOnly", old.Handed, sdk.Handed},
|
|
{"the own secrets named foreseen", old.Foreseen, sdk.Foreseen},
|
|
} {
|
|
if !sameStrings(f.old, f.sdk) {
|
|
say(f.name, f.old, f.sdk)
|
|
}
|
|
}
|
|
if !sameLeftOut(old.LeftOut, sdk.LeftOut) {
|
|
say("LeftOut and its reasons", old.LeftOut, sdk.LeftOut)
|
|
}
|
|
if withBody && !bytes.Equal(old.Body, sdk.Body) {
|
|
out = append(out, fmt.Sprintf("%s, %s: the declaration differs byte for byte; %s", machine, comparison,
|
|
firstDifferingResource(old.Body, sdk.Body)))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func quoted(s string) string {
|
|
if s == "" {
|
|
return "none"
|
|
}
|
|
return fmt.Sprintf("%q", s)
|
|
}
|
|
|
|
func sameStrings(a, b []string) bool {
|
|
if len(a) == 0 && len(b) == 0 {
|
|
return true
|
|
}
|
|
return reflect.DeepEqual(a, b)
|
|
}
|
|
|
|
func sameLeftOut(a, b map[string]string) bool {
|
|
if len(a) == 0 && len(b) == 0 {
|
|
return true
|
|
}
|
|
return reflect.DeepEqual(a, b)
|
|
}
|
|
|
|
// firstDifferingResource names the first resource of two declaration bodies that is not the same, and the first field of it.
|
|
func firstDifferingResource(a, b []byte) string {
|
|
var x, y struct {
|
|
Resources []map[string]any `json:"resources"`
|
|
}
|
|
if json.Unmarshal(a, &x) != nil || json.Unmarshal(b, &y) != nil {
|
|
return "a body is not a declaration"
|
|
}
|
|
byID := func(rs []map[string]any) map[string]map[string]any {
|
|
out := map[string]map[string]any{}
|
|
for _, r := range rs {
|
|
out[fmt.Sprint(r["id"])] = r
|
|
}
|
|
return out
|
|
}
|
|
ids := map[string]bool{}
|
|
left, right := byID(x.Resources), byID(y.Resources)
|
|
for id := range left {
|
|
ids[id] = true
|
|
}
|
|
for id := range right {
|
|
ids[id] = true
|
|
}
|
|
var order []string
|
|
for id := range ids {
|
|
order = append(order, id)
|
|
}
|
|
sort.Strings(order)
|
|
for _, id := range order {
|
|
l, inLeft := left[id]
|
|
r, inRight := right[id]
|
|
switch {
|
|
case !inRight:
|
|
return fmt.Sprintf("resource %s is only in the controller's", id)
|
|
case !inLeft:
|
|
return fmt.Sprintf("resource %s is only in mesh-sdk's", id)
|
|
}
|
|
var fields []string
|
|
for k := range l {
|
|
fields = append(fields, k)
|
|
}
|
|
for k := range r {
|
|
if _, have := l[k]; !have {
|
|
fields = append(fields, k)
|
|
}
|
|
}
|
|
sort.Strings(fields)
|
|
for _, k := range fields {
|
|
if !reflect.DeepEqual(l[k], r[k]) {
|
|
return fmt.Sprintf("resource %s, field %s: the controller's %v, mesh-sdk's %v", id, k, l[k], r[k])
|
|
}
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(left, right) {
|
|
return "the envelopes differ"
|
|
}
|
|
return "the resources are the same; the envelope differs"
|
|
}
|
|
|
|
// made is what a send made in a store, by key and never by value: the machine ports given, the pair credentials and the
|
|
// own secrets.
|
|
type made struct {
|
|
Ports []string
|
|
Pairs []string
|
|
OwnSecrets []string
|
|
}
|
|
|
|
// madeDiffer says how what two sends made differs.
|
|
func madeDiffer(comparison string, old, sdk made) []string {
|
|
var out []string
|
|
for _, f := range []struct {
|
|
name string
|
|
old, sdk []string
|
|
}{
|
|
{"the ports allocated", old.Ports, sdk.Ports},
|
|
{"the pair credentials made", old.Pairs, sdk.Pairs},
|
|
{"the own secrets made", old.OwnSecrets, sdk.OwnSecrets},
|
|
} {
|
|
if !sameStrings(f.old, f.sdk) {
|
|
out = append(out, fmt.Sprintf("%s: %s differ: the controller's %s, mesh-sdk's %s", comparison, f.name,
|
|
strings.Join(f.old, ", "), strings.Join(f.sdk, ", ")))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 1384/AC3: the comparison itself. A composition that differs by one setting's worth is named, with its machine and the
|
|
// resource and field that first differ; two that agree, and two that refuse alike, are not differences. It failed first,
|
|
// before bothWaysDiffer read the body: a changed setting went unseen.
|
|
func TestTheComparisonNamesTheMachineWhoseCompositionDiffers(t *testing.T) {
|
|
body := func(port string) []byte {
|
|
raw, err := json.Marshal(map[string]any{"declaration": 1, "resources": []map[string]any{
|
|
{"id": "files.config", "type": "file", "content": "port=" + port},
|
|
{"id": "files.dir", "type": "directory"}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
same := composedBoth{Body: body("8080"), Foreseen: []string{"files/admin"}}
|
|
if got := bothWaysDiffer("laptop", "Reading", same, same, true); len(got) != 0 {
|
|
t.Fatalf("a composition differs from itself: %v", got)
|
|
}
|
|
changed := composedBoth{Body: body("8081"), Foreseen: []string{"files/admin"}}
|
|
got := bothWaysDiffer("laptop", "Reading", same, changed, true)
|
|
if len(got) != 1 || !strings.Contains(got[0], "laptop, Reading") ||
|
|
!strings.Contains(got[0], "resource files.config, field content") ||
|
|
!strings.Contains(got[0], "port=8080") || !strings.Contains(got[0], "port=8081") {
|
|
t.Fatalf("the changed setting is not named with its machine, resource and field: %v", got)
|
|
}
|
|
// A field of the lists.
|
|
other := composedBoth{Body: body("8080"), Foreseen: []string{"files/admin", "files/token"}}
|
|
if got := bothWaysDiffer("anchor", "Foreseeing", same, other, false); len(got) != 1 ||
|
|
!strings.Contains(got[0], "anchor, Foreseeing: the own secrets named foreseen differs") {
|
|
t.Fatalf("a secret named foreseen on one path alone is not named: %v", got)
|
|
}
|
|
// Refused alike is said, not failed; refused differently is a difference.
|
|
a, b := composedBoth{Err: "files on laptop needs a credential"}, composedBoth{Err: "files on laptop needs a credential"}
|
|
if got := bothWaysDiffer("laptop", "Foreseeing", a, b, true); len(got) != 0 {
|
|
t.Fatalf("two compositions that refuse a machine alike differ: %v", got)
|
|
}
|
|
if got := bothWaysDiffer("laptop", "Foreseeing", a, composedBoth{Body: body("1")}, true); len(got) != 1 ||
|
|
!strings.Contains(got[0], "the refusal differs") {
|
|
t.Fatalf("a machine one path refuses and the other composes is not named: %v", got)
|
|
}
|
|
// And what a send made.
|
|
if got := madeDiffer("the send", made{Ports: []string{"anchor files:80->8080"}}, made{}); len(got) != 1 ||
|
|
!strings.Contains(got[0], "the ports allocated differ") {
|
|
t.Fatalf("a port allocated on one path alone is not named: %v", got)
|
|
}
|
|
}
|