package main import ( "bytes" "context" "errors" "strings" "testing" "github.com/novox/mesh-controller/internal/catalogue" ) // novox/hq issue 237: every line of an act's answer comes from one resolution of the node taken after // its write, so the answer agrees with `plan` for the node right after. func aProviderAndItsConsumer(t *testing.T, open *stores) { t.Helper() register(t, open, catalogue.Manifest{Module: "db", Version: "1", Provides: []catalogue.Offer{{Name: "rows", Scope: catalogue.ScopeNode}}}) register(t, open, catalogue.Manifest{Module: "app", Version: "1", Requires: []string{"rows"}}) } // twoRivalsAndANote is a node that does not resolve — two modules claim one node seat there — and a // module beside them that changes nothing about it. func twoRivalsAndANote(t *testing.T, open *stores) { t.Helper() one, two := rivals() register(t, open, one) register(t, open, two) register(t, open, catalogue.Manifest{Module: "notes", Version: "1"}) } func TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer(t *testing.T) { open := aMesh(t) ctx := t.Context() twoRivalsAndANote(t, open) // Straight into the store, as a node that came to this before the act. for _, m := range []string{"rival-one", "rival-two", "notes"} { if _, err := open.inventory.Assign(ctx, "laptop", m); err != nil { t.Fatal(err) } } said := answered(t, func() error { return assignCommand(ctx, "unassign", []string{"laptop", "notes"}) }) if _, _, err := planFor(ctx, open, "laptop"); err == nil || !unresolvable(err) { t.Fatalf("laptop resolves with two claimants of one seat: %v", err) } if n := strings.Count(said, "laptop does not resolve now"); n != 1 { t.Errorf("the answer says laptop does not resolve %d times, want once:\n%s", n, said) } if strings.Contains(said, "is left out of the rest of the mesh") { t.Errorf("the answer carries a line from a read that is not its resolution:\n%s", said) } } func TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt(t *testing.T) { open := aMesh(t) ctx := t.Context() aProviderAndItsConsumer(t, open) if _, err := assign(ctx, open, "laptop", "db", "app"); err != nil { t.Fatal(err) } said, err := unassign(ctx, open, "laptop", "app") if err != nil { t.Fatal(err) } if _, _, err := planFor(ctx, open, "laptop"); err != nil { t.Fatal(err) } if strings.Contains(said, "does not resolve") || strings.Contains(said, "could not be read") { t.Errorf("a node that resolves is said not to:\n%s", said) } } func TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal(t *testing.T) { open := aMesh(t) ctx := t.Context() twoRivalsAndANote(t, open) if _, err := assign(ctx, open, "laptop", "rival-one"); err != nil { t.Fatal(err) } // Kept and not resolvable: a second claimant of the seat, which assignment keeps. said, err := assign(ctx, open, "laptop", "rival-two") if err == nil { t.Fatalf("two claimants of one node seat resolved:\n%s", said) } _, _, planErr := planFor(ctx, open, "laptop") if planErr == nil || planErr.Error() != err.Error() { t.Errorf("the act's refusal is not the plan's right after:\nact: %v\nplan: %v", err, planErr) } if strings.Contains(said, "now has") { t.Errorf("the answer says a seat is now held on a node that does not resolve:\n%s", said) } } // The other readers of the whole mesh still say a machine they leave out, on standard error (novox/hq // issue 188); an act sends it elsewhere. func TestAMachineLeftOutIsSaidWhereTheContextSays(t *testing.T) { var buf bytes.Buffer sayLeftOut(leftOutSaidBy(context.Background(), &buf), "laptop", errors.New("why")) if buf.String() != "laptop is left out of the rest of the mesh: it does not resolve: why\n" { t.Errorf("said %q", buf.String()) } got := answered(t, func() error { sayLeftOut(context.Background(), "laptop", errors.New("why")) return nil }) if !strings.Contains(got, "laptop is left out") { t.Errorf("without an act's writer the line did not reach standard error: %q", got) } }