package monthlyusagehistory import ( "reflect" "testing" "time" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/queue/zset" ) func TestLocalSupplyObservationFirstEventsAndStages(t *testing.T) { started := time.Now() o := &supplyObservation{started: started, channels: map[string]*supplyChannel{"a": {stage: "initializing", changed: started, stageDuration: map[string]time.Duration{}}}} o.event("a", "sql_query", 0, false, false, started.Add(10*time.Millisecond)) o.event("a", "page_ready", 2, false, false, started.Add(20*time.Millisecond)) o.event("a", "redis_write", 1, true, false, started.Add(30*time.Millisecond)) o.event("a", "channel_capacity", 1, false, false, started.Add(40*time.Millisecond)) o.event("a", "sql_query", 0, false, false, started.Add(50*time.Millisecond)) o.event("a", "redis_write", 0, true, false, started.Add(60*time.Millisecond)) o.event("a", "upper_bound", 0, false, true, started.Add(70*time.Millisecond)) o.event("unknown", "ignored", 0, false, false, started) s := o.channels["a"] if s.firstQuery == nil || *s.firstQuery != 10 || s.firstEnqueue == nil || *s.firstEnqueue != 30 || !s.ended || s.pending != 0 || s.version != 7 || s.stageDuration["channel_capacity"] != 10*time.Millisecond || s.stageDuration["sql_query"] != 20*time.Millisecond { t.Fatalf("observation=%+v", s) } } func TestSupplyObservationAccumulatesSubMillisecondStages(t *testing.T) { started := time.Unix(0, 0) o := &supplyObservation{started: started, channels: map[string]*supplyChannel{"a": {stage: "redis_read", changed: started, stageDuration: map[string]time.Duration{}}}} for i := 1; i <= 100000; i++ { stage := "redis_read" if i%2 == 1 { stage = "redis_write" } o.event("a", stage, 0, false, i == 100000, started.Add(time.Duration(i)*500*time.Microsecond)) } if got := o.channels["a"].stageMilliseconds(started.Add(time.Hour)); got["redis_read"] != 25000 || got["redis_write"] != 25000 { t.Fatalf("stageMs=%v, want 25000ms per stage", got) } } func TestSupplyObservationFinalFlushPreservesFraction(t *testing.T) { started := time.Unix(0, 0) o := &supplyObservation{started: started, channels: map[string]*supplyChannel{"a": {stage: "redis_write", changed: started, stageDuration: map[string]time.Duration{}}}} o.event("a", "redis_read", 0, false, false, started.Add(600*time.Microsecond)) o.event("a", "redis_write", 0, false, false, started.Add(1200*time.Microsecond)) s := o.channels["a"] if got := s.stageMilliseconds(started.Add(1800 * time.Microsecond)); got["redis_write"] != 1 || got["redis_read"] != 0 { t.Fatalf("final stageMs=%v", got) } if s.stageDuration["redis_write"] != 1200*time.Microsecond || s.stageDuration["redis_read"] != 600*time.Microsecond { t.Fatalf("fractional duration lost: %v", s.stageDuration) } } func TestSupplySamplesDistinguishReadyLeaseFutureAndClosedRound(t *testing.T) { store := testStore(t) ctx := contextForTest(t) p := testProducer(t, scannerFor(nil, 0), store, Config{}) if err := store.CreateRun(ctx, zset.RunMeta{RunID: "supply"}); err != nil { t.Fatal(err) } active, _ := store.RoundActiveKey("supply", 1, "a") lease, _ := store.RoundLeaseKey("supply", 1, "a") _, _ = store.RedisClient().Do(ctx, "ZADD", active, time.Now().Add(-time.Minute).UnixMilli(), "ready", time.Now().Add(time.Hour).UnixMilli(), "future") _, _ = store.RedisClient().Do(ctx, "HSET", lease, "leased", "token") values, err := p.sampleSupply(ctx, "supply", "a") if err != nil || !reflect.DeepEqual(values, []int64{1, 1, 1, 1, 1}) { t.Fatalf("sample=%v err=%v", values, err) } meta, _ := store.RunMetaKey("supply") for _, args := range [][]any{{"blocked:a", 1}, {"blocked:a", 0, "producerDone", 1}, {"producerDone", 0, "currentRound", 2}, {"currentRound", 1, "syncType", "replay"}} { _, _ = store.RedisClient().Do(ctx, "HSET", append([]any{meta}, args...)...) values, err = p.sampleSupply(ctx, "supply", "a") if err != nil || values[0] != 1 || values[1] != 0 { t.Fatalf("ineligible sample=%v err=%v", values, err) } } values, err = p.sampleSupply(ctx, "missing", "a") if err != nil || values[0] != 0 || values[1] != 0 { t.Fatalf("missing sample=%v err=%v", values, err) } } func TestSupplySamplerTracksWaitingChannelsAndStops(t *testing.T) { store := testStore(t) ctx := contextForTest(t) p := testProducer(t, scannerFor(nil, 0), store, Config{}) if err := store.CreateRun(ctx, zset.RunMeta{RunID: "sampling"}); err != nil { t.Fatal(err) } finish := p.startObservation(ctx, "sampling", []string{"a", "b"}, time.Now()) defer finish() o := p.observation p.observe("a", "channel_capacity", 1, false, false) p.observe("b", "sql_slot", 0, false, false) waitUntil(t, ctx, func() bool { o.mu.Lock() defer o.mu.Unlock() return o.channels["a"].validSamples > 0 && o.channels["b"].validSamples > 0 }) p.observe("a", "cancelled", 0, false, true) p.observe("b", "cancelled", 0, false, true) o.mu.Lock() defer o.mu.Unlock() if o.channels["a"].stageDuration["channel_capacity"] < 900*time.Millisecond || o.channels["b"].stageDuration["sql_slot"] < 900*time.Millisecond { t.Fatalf("missing wait attribution: a=%+v b=%+v", o.channels["a"], o.channels["b"]) } }