combine-schedulers icon indicating copy to clipboard operation
combine-schedulers copied to clipboard

Simulate the inaccuracy of schedulers

Open runloop opened this issue 2 years ago • 3 comments

This is a fantastic library and aids testing of time based asynchrony really well. But in a way, it only simulates the happiest path due to the precision of the scheduler. As we all know, when a timer is set it will complete at a point of timer after the scheduled time, if only a fraction of a second after. In your videos on Clocks, you demonstrate how this drift can add relatively quickly.

Would it be possible to add a feature where we can test against these small inaccuracies? So after advancing the scheduler, rather than the schedulers' .now property being set exactly to the next scheduled actions date, it could be set with a defined drift, maybe even the .minimumTolerance. This way, if we need to, we can test against these small inaccuracies of timing that can add up after many events.

runloop avatar Jan 11 '23 11:01 runloop

This may be a naive approach, but it appears to work and all the tests still pass. It allows for the creation of a new test scheduler where the now date is always set to the next action date plus the minimum tolerance.

public final class TestScheduler<SchedulerTimeType, SchedulerOptions> ... {
  // ...
  public let minimumTolerance: SchedulerTimeType.Stride

  public init(now: SchedulerTimeType, minimumTolerance: SchedulerTimeType.Stride = .zero) {
    self.now = now
    self.minimumTolerance = minimumTolerance
  }


  // in both advance methods update now to next.date but include minimumTolerance.
  public func advance(to instant: SchedulerTimeType) {
    while self.lock.sync(operation: { self.now }) <= instant {
      self.lock.lock()
      self.scheduled.sort { ($0.date, $0.sequence) < ($1.date, $1.sequence) }
      
      guard
        let next = self.scheduled.first,
        instant >= next.date
      else {
        self.now = instant
        self.lock.unlock()
        return
      }
      
      self.now = next.date.advanced(by: minimumTolerance) // <-- include minimum tolerance to have predictable inaccuracy
      self.scheduled.removeFirst()
      self.lock.unlock()
      next.action()
    }
  }

// create a new test scheduler that includes defined inaccuracy.

extension RunLoop {
  /// ...
  public static func inaccurate(
    by minimumTolerance: SchedulerTimeType.Stride
  ) -> TestSchedulerOf<RunLoop> {
    .init(now: .init(.init(timeIntervalSince1970: 0)), minimumTolerance: minimumTolerance)
  }
}

runloop avatar Jan 11 '23 11:01 runloop

Hey @runloop, this is a really great idea! Wanna PR it?

mbrandonw avatar Jan 11 '23 16:01 mbrandonw

I gave it a shot.

https://github.com/pointfreeco/combine-schedulers/pull/72

runloop avatar Jan 11 '23 17:01 runloop