{This file written by ChatGPT.} procedure TestGetTimeUntil(); procedure Fail(TestName, Detail: String); begin Writeln('GetTimeUntil test failed: ', TestName); Writeln(' ', Detail); Halt(1); end; procedure Check(TestName: String; Condition: Boolean; Detail: String); begin if (not Condition) then Fail(TestName, Detail); end; procedure InitialiseSubscription(var Subscription: TMaterialSubscriberSubscription; Rate: TQuantityRate); begin Subscription := Default(TMaterialSubscriberSubscription); Subscription.FData.FRate := Rate; end; function MakeTarget(Units: Int64): TQuantity64; begin Result := TQuantity64.FromUnits(Units); end; function MakeMilliseconds(Milliseconds: Int64): TMillisecondsDuration; begin Result := TMillisecondsDuration.FromMilliseconds(Milliseconds); end; function CountTriggeredAt(Time: TMillisecondsDuration; Subscriptions: TMaterialSubscriberSubscriptionArray): TQuantity64; var Subscription: PMaterialSubscriberSubscription; begin Result := TQuantity64.Zero; for Subscription in Subscriptions do begin if (Assigned(Subscription)) then Result := Result + Subscription^.Rate.ComputeFullPeriods(Time); end; end; procedure CheckResult(TestName: String; Target: TQuantity64; Subscriptions: TMaterialSubscriberSubscriptionArray; Expected: TMillisecondsDuration); var Actual: TMillisecondsDuration; Previous: TMillisecondsDuration; ActualCount: TQuantity64; PreviousCount: TQuantity64; begin Check(TestName, Target.IsPositive, 'Target assumption violated.'); Actual := GetTimeUntil(Target, Subscriptions); Check(TestName, Actual = Expected, 'Returned time did not equal expected time.'); ActualCount := CountTriggeredAt(Actual, Subscriptions); Check(TestName, ActualCount >= Target, 'Returned time does not produce enough events.'); if (Actual.IsPositive) then begin Previous := Actual - MakeMilliseconds(1); PreviousCount := CountTriggeredAt(Previous, Subscriptions); Check(TestName, PreviousCount < Target, 'Returned time was not minimal.'); end; end; var SubscriptionA: TMaterialSubscriberSubscription; SubscriptionB: TMaterialSubscriberSubscription; SubscriptionC: TMaterialSubscriberSubscription; Subscriptions: TMaterialSubscriberSubscriptionArray; begin SubscriptionA := Default(TMaterialSubscriberSubscription); SubscriptionB := Default(TMaterialSubscriberSubscription); SubscriptionC := Default(TMaterialSubscriberSubscription); { One event every 1000 ms. The 5th event occurs at 5000 ms. } InitialiseSubscription(SubscriptionA, TQuantityRate.FromPerSecond(1.0)); Subscriptions := [@SubscriptionA]; CheckResult( 'single rate, exact period', MakeTarget(5), Subscriptions, MakeMilliseconds(5000) ); { Two rates: 1/s and 2/s. At 2000 ms, counts are 2 + 4 = 6. } InitialiseSubscription(SubscriptionA, TQuantityRate.FromPerSecond(1.0)); InitialiseSubscription(SubscriptionB, TQuantityRate.FromPerSecond(2.0)); Subscriptions := [@SubscriptionA, @SubscriptionB]; CheckResult( 'two rates, combined total', MakeTarget(6), Subscriptions, MakeMilliseconds(2000) ); { Simultaneous triggers count separately: 2/s and 4/s both trigger at 500 ms. } InitialiseSubscription(SubscriptionA, TQuantityRate.FromPerSecond(2.0)); InitialiseSubscription(SubscriptionB, TQuantityRate.FromPerSecond(4.0)); Subscriptions := [@SubscriptionA, @SubscriptionB]; CheckResult( 'simultaneous triggers count separately', MakeTarget(3), Subscriptions, MakeMilliseconds(500) ); { Nil entries must be skipped. This is equivalent to a single 1/s subscription. } InitialiseSubscription(SubscriptionA, TQuantityRate.FromPerSecond(1.0)); Subscriptions := [nil, @SubscriptionA, nil]; CheckResult( 'nil subscriptions skipped', MakeTarget(3), Subscriptions, MakeMilliseconds(3000) ); { Non-integer millisecond result must round up to the first millisecond at which enough full periods have occurred. 3/s has events at 333.333..., 666.666..., 1000 ms, so the first event is visible at 334 ms under integer millisecond precision and flooring. } InitialiseSubscription(SubscriptionA, TQuantityRate.FromPerSecond(3.0)); Subscriptions := [@SubscriptionA]; CheckResult( 'ceil fractional millisecond boundary', MakeTarget(1), Subscriptions, MakeMilliseconds(334) ); { Heterogeneous periods: 100 ms, 250 ms, 1000 ms. At 500 ms: 5 + 2 + 0 = 7. At 499 ms: 4 + 1 + 0 = 5. } InitialiseSubscription(SubscriptionA, TQuantityRate.FromPerMillisecond(0.01)); InitialiseSubscription(SubscriptionB, TQuantityRate.FromPerMillisecond(0.004)); InitialiseSubscription(SubscriptionC, TQuantityRate.FromPerSecond(1.0)); Subscriptions := [@SubscriptionA, @SubscriptionB, @SubscriptionC]; CheckResult( 'mixed periods minimal boundary', MakeTarget(7), Subscriptions, MakeMilliseconds(500) ); { Larger target to exercise binary search / accumulated-rate behavior. } InitialiseSubscription(SubscriptionA, TQuantityRate.FromPerSecond(10.0)); InitialiseSubscription(SubscriptionB, TQuantityRate.FromPerSecond(25.0)); Subscriptions := [@SubscriptionA, @SubscriptionB]; CheckResult( 'larger target', MakeTarget(350), Subscriptions, MakeMilliseconds(10000) ); end;