package obs
import (
"reflect"
"testing"
"time"
)
var t0 = time.Date(2026, 9, 16, 12, 0, 0, 0, time.UTC)
// The line answers the same question as the number above it: how many
// were running, not how many started.
func TestConcurrencyCountsWhatWasRunning(t *testing.T) {
// A window of 4 bins over 4 seconds: bins start at +0s, +1s, +2s, +3s.
now := t0.Add(4 * time.Second)
subs := []Subprocess{
// Runs the whole window: counted in every bin.
{At: t0.Add(-time.Second), Duration: 10 * time.Second},
// Starts and ends inside the second bin: counted once.
{At: t0.Add(1100 * time.Millisecond), Duration: 300 * time.Millisecond},
// Ends exactly at a bin start: not running at that instant.
{At: t0.Add(2 * time.Second), Duration: time.Second},
}
got := ConcurrencyBins(subs, now, 4*time.Second, 4)
if want := []int{1, 1, 2, 1}; !reflect.DeepEqual(got, want) {
t.Fatalf("concurrency = %v, want %v", got, want)
}
// Counting starts instead would read 1,1,1,0 — a different shape
// under the same number, which is the reason this counts overlap.
}
// Events outside the window are dropped, not piled into an end bin.
func TestCountBinsKeepsTheWindowHonest(t *testing.T) {
now := t0.Add(4 * time.Second)
ats := []time.Time{
t0.Add(-time.Hour), // long before: dropped
t0.Add(100 * time.Millisecond), // bin 0
t0.Add(200 * time.Millisecond), // bin 0
t0.Add(3500 * time.Millisecond), // bin 3
now, // the instant now: last bin
}
got := CountBins(ats, now, 4*time.Second, 4)
if want := []int{2, 0, 0, 2}; !reflect.DeepEqual(got, want) {
t.Fatalf("counts = %v, want %v", got, want)
}
}
// A degenerate ask draws nothing rather than dividing by zero.
func TestSeriesRefuseDegenerateWindows(t *testing.T) {
if got := ConcurrencyBins(nil, t0, 0, 4); got != nil {
t.Errorf("zero window = %v, want nil", got)
}
if got := CountBins(nil, t0, time.Minute, 0); got != nil {
t.Errorf("zero bins = %v, want nil", got)
}
}