package main import ( "context" "errors" "strings" "testing" "github.com/novox/mesh-controller/internal/conditions" ) // **A filling log or bucket is said at three quarters of its cap and cleared below seven tenths** // (novox/hq ADR 0297 §6): raised at 75% naming the module, the bucket or log and its fill; not at 74.9%; // kept between 70% and 75% while it is open, and not raised there when it is not; and cleared — said no // more — once it reads below 70%, open or not. func TestAFillingBucketOrLogIsRaisedAtThreeQuartersAndClearedBelowSevenTenths(t *testing.T) { const mib = 1024 * 1024 log := bucketOrLog{Module: "mesh-issues", Kind: "log", Name: "changes", Stream: "LOG_mesh-issues_changes"} bucket := bucketOrLog{Module: "mesh-issues", Kind: "bucket", Name: "issues", Stream: "KV_mesh-issues_issues"} for _, c := range []struct { name string of bucketOrLog bytes uint64 max uint64 open bool said bool }{ {"a log at exactly 75%", log, 768 * mib, 1024 * mib, false, true}, {"a bucket at exactly 75%", bucket, 48 * mib, 64 * mib, false, true}, {"a bucket full", bucket, 64 * mib, 64 * mib, false, true}, {"a log just under 75%", log, 768*mib - 1, 1024 * mib, false, false}, {"a bucket at 72%, not open", bucket, 64 * mib * 72 / 100, 64 * mib, false, false}, {"a bucket at 72%, open: kept", bucket, 64 * mib * 72 / 100, 64 * mib, true, true}, {"a log at exactly 70%, open: kept", log, 700 * mib, 1000 * mib, true, true}, {"a log just under 70%, open: cleared", log, 700*mib - 1, 1000 * mib, true, false}, {"a bucket at 10%, open: cleared", bucket, 64 * mib / 10, 64 * mib, true, false}, {"a bucket with no cap", bucket, 100, 0, true, false}, } { o, said := fillObservation(c.of, filled{Bytes: c.bytes, Max: c.max}, c.open) if said != c.said { t.Errorf("%s: said %v, want %v", c.name, said, c.said) continue } if !said { continue } if o.Key() != fillKey(c.of) || o.Kind != kindBucketOrLogFilling || o.Severity != conditions.Warning { t.Errorf("%s: raised as %s (%s, %s)", c.name, o.Key(), o.Kind, o.Severity) } for _, part := range []string{c.of.Module, c.of.Kind + " " + c.of.Name, "%", " of "} { if !strings.Contains(o.Summary, part) { t.Errorf("%s: does not name %q: %q", c.name, part, o.Summary) } } if !strings.Contains(o.Said, "bytes") { t.Errorf("%s: the evidence does not say the fill in bytes: %q", c.name, o.Said) } } o, _ := fillObservation(log, filled{Bytes: 800 * mib, Max: 1024 * mib}, false) if !strings.Contains(o.Summary, "800.0 MiB of 1024.0 MiB, 78%") { t.Errorf("the fill is said as %q", o.Summary) } } // **One bucket or log that cannot be read does not stop the others being judged** (review of #231): it is // said as unread with its reason, a fill condition open for it is kept, and every other bucket and log // is judged by its own fill. func TestAnUnreadableBucketOrLogIsSaidAndTheRestAreStillJudged(t *testing.T) { const mib = 1024 * 1024 broken := bucketOrLog{Module: "a", Kind: "bucket", Name: "broken", Stream: "KV_a_broken"} brokenOpen := bucketOrLog{Module: "a", Kind: "log", Name: "was-filling", Stream: "LOG_a_was-filling"} full := bucketOrLog{Module: "b", Kind: "log", Name: "changes", Stream: "LOG_b_changes"} empty := bucketOrLog{Module: "c", Kind: "bucket", Name: "quiet", Stream: "KV_c_quiet"} absent := bucketOrLog{Module: "d", Kind: "bucket", Name: "not-yet", Stream: "KV_d_not-yet"} refusal := errors.New("the bus did not answer") read := func(_ context.Context, s bucketOrLog) (filled, bool, error) { switch s { case broken, brokenOpen: return filled{}, false, refusal case full: return filled{Bytes: 900 * mib, Max: 1000 * mib}, true, nil case empty: return filled{Bytes: 1, Max: 64 * mib}, true, nil } return filled{}, false, nil } open := func(_ context.Context, key string) (conditions.Condition, bool, error) { if key == fillKey(brokenOpen) { return conditions.Condition{Key: key, Severity: conditions.Warning, Summary: "a's log was-filling holds 800.0 MiB of 1024.0 MiB, 78%"}, true, nil } return conditions.Condition{}, false, nil } got := map[string]conditions.Observation{} for _, o := range judgeFills(context.Background(), []bucketOrLog{broken, brokenOpen, full, empty, absent}, read, open) { got[o.Key()] = o } for _, s := range []bucketOrLog{broken, brokenOpen} { o, said := got[conditions.Key(conditions.ScopeBus, s.Stream, "unread")] if !said || o.Kind != kindBucketOrLogUnread || !o.Confirm || !strings.Contains(o.Said, refusal.Error()) { t.Errorf("%s could not be read and was said as %+v", s.Stream, o) } } if o, kept := got[fillKey(brokenOpen)]; !kept || !strings.Contains(o.Said, "kept open") { t.Errorf("an open fill condition of a log not read this time was not kept: %+v", o) } if _, said := got[fillKey(broken)]; said { t.Error("a bucket not read and not filling was said filling") } if o, said := got[fillKey(full)]; !said || o.Kind != kindBucketOrLogFilling { t.Errorf("a log at 90%% beside an unreadable one was not judged: %+v", got) } if len(got) != 4 { t.Errorf("said %d findings, want 4 (two unread, one kept, one filling): %v", len(got), got) } } // Both kinds have plain words of their own, which hold to the plain rule (novox/hq ADR 0253). func TestAFillingOrUnreadBucketOrLogIsSaidInPlainWords(t *testing.T) { for _, kind := range []string{kindBucketOrLogFilling, kindBucketOrLogUnread} { wording, has := plainWordings[kind] if !has { t.Errorf("%s has no plain words", kind) continue } if why, ok := conditions.PlainWords(wording(conditions.Observation{Kind: kind})); !ok { t.Errorf("%s: %s", kind, why) } } } // **When neither the bucket or log nor its condition can be read, nothing is said of its fill** (second // review of #231): the unread finding covers it, and no fill is invented for it. For one that is read and // measured between seven tenths and three quarters, a condition that cannot be read is taken as open, so // an unknown never clears it. func TestNothingIsSaidOfAFillWhenNeitherItNorItsConditionCanBeRead(t *testing.T) { const mib = 1024 * 1024 broken := bucketOrLog{Module: "a", Kind: "log", Name: "changes", Stream: "LOG_a_changes"} between := bucketOrLog{Module: "b", Kind: "bucket", Name: "issues", Stream: "KV_b_issues"} read := func(_ context.Context, s bucketOrLog) (filled, bool, error) { if s == broken { return filled{}, false, errors.New("the bus did not answer") } return filled{Bytes: 72 * mib, Max: 100 * mib}, true, nil } open := func(context.Context, string) (conditions.Condition, bool, error) { return conditions.Condition{}, false, errors.New("the conditions bucket did not answer") } got := map[string]conditions.Observation{} for _, o := range judgeFills(context.Background(), []bucketOrLog{broken, between}, read, open) { got[o.Key()] = o } if o, said := got[fillKey(broken)]; said { t.Fatalf("a fill was said for a log whose fill and condition could not be read: %+v", o) } if _, said := got[conditions.Key(conditions.ScopeBus, broken.Stream, "unread")]; !said { t.Error("the log that could not be read was not said as unread") } if _, kept := got[fillKey(between)]; !kept { t.Error("a bucket at 72% whose condition could not be read was cleared") } if len(got) != 2 { t.Errorf("said %d findings, want 2: %v", len(got), got) } }