Skip to content
quarry Artifact [fe650e0b40]
Login

Artifact [fe650e0b40]

Artifact fe650e0b402dfffef5358b52defc8931b83d481d4a2d8fbf7680871abe690b3e:


package ingest

import (
	"errors"
	"testing"
	"time"
)

// The whole reason the fleet admin surface exists is that a wedged store
// is invisible: its counts are exactly the counts it had when it broke,
// so a page showing only counts cannot tell it from a healthy one. That
// makes the failure bookkeeping — not the success bookkeeping — the part
// worth pinning down.
func TestStatsDistinguishWedgedFromFlapping(t *testing.T) {
	p := &Pipeline{}

	if got := p.Stats(); got.Failures != 0 || !got.LastOK.IsZero() {
		t.Fatalf("fresh pipeline already has history: %+v", got)
	}

	// A store that has never succeeded must not read as healthy just
	// because nothing has run yet: LastOK stays zero.
	p.record(0, time.Millisecond, errors.New("boom"))
	p.record(0, time.Millisecond, errors.New("boom"))
	p.record(0, time.Millisecond, errors.New("boom"))
	s := p.Stats()
	if s.Failures != 3 {
		t.Errorf("Failures = %d, want 3 consecutive", s.Failures)
	}
	if !s.LastOK.IsZero() {
		t.Errorf("LastOK set despite never succeeding: %v", s.LastOK)
	}
	if s.LastErr != "boom" {
		t.Errorf("LastErr = %q, want the failure preserved", s.LastErr)
	}
	if s.LastErrAt.IsZero() {
		t.Error("LastErrAt not stamped")
	}

	// One success must clear the streak — otherwise a store that
	// recovered still reads as broken forever, and an operator learns to
	// ignore the column.
	p.record(7, 2*time.Millisecond, nil)
	s = p.Stats()
	if s.Failures != 0 {
		t.Errorf("Failures = %d after recovery, want 0", s.Failures)
	}
	if s.LastErr != "" {
		t.Errorf("LastErr = %q after recovery, want cleared", s.LastErr)
	}
	if s.LastOK.IsZero() {
		t.Error("LastOK not stamped on success")
	}
	if s.Total != 7 || s.LastCount != 7 {
		t.Errorf("Total/LastCount = %d/%d, want 7/7", s.Total, s.LastCount)
	}

	// A later failure must NOT erase the fact that the store was once
	// healthy, or "failing since" becomes unanswerable.
	p.record(0, time.Millisecond, errors.New("again"))
	s = p.Stats()
	if s.LastOK.IsZero() {
		t.Error("LastOK cleared by a subsequent failure")
	}
	if s.Failures != 1 {
		t.Errorf("Failures = %d, want the streak restarted at 1", s.Failures)
	}
	// Totals are cumulative across the run, so a failed round must not
	// discard what earlier rounds published.
	if s.Total != 7 {
		t.Errorf("Total = %d after a failed round, want 7 retained", s.Total)
	}
}

// Fallbacks accumulate independently of round outcome: an artifact that
// would not parse is not a round failure, and conflating the two would
// make every fossil-born corpus look broken.
func TestFallbacksAreNotFailures(t *testing.T) {
	p := &Pipeline{}
	p.fallback()
	p.fallback()
	p.record(2, time.Millisecond, nil)

	s := p.Stats()
	if s.Fallbacks != 2 {
		t.Errorf("Fallbacks = %d, want 2", s.Fallbacks)
	}
	if s.Failures != 0 || s.LastErr != "" {
		t.Errorf("parse fallbacks reported as a round failure: %+v", s)
	}
}