blob: 374fbe24e8868ef4460eebbce0fe3952dcdc1a99 [file] [log] [blame] [edit]
//
// RACTestScheduler.m
// ReactiveCocoa
//
// Created by Justin Spahr-Summers on 2013-07-06.
// Copyright (c) 2013 GitHub, Inc. All rights reserved.
//
#import "RACTestScheduler.h"
#import "RACEXTScope.h"
#import "RACCompoundDisposable.h"
#import "RACDisposable.h"
#import "RACScheduler+Private.h"
@interface RACTestSchedulerAction : NSObject
// The date at which the action should be executed.
//
// This absolute time will not actually be honored. This date is only used for
// comparison, to determine which block should be run _next_.
@property (nonatomic, copy, readonly) NSDate *date;
// The scheduled block.
@property (nonatomic, copy, readonly) void (^block)(void);
// A disposable for this action.
//
// When disposed, the action should not start executing if it hasn't already.
@property (nonatomic, strong, readonly) RACDisposable *disposable;
// Initializes a new scheduler action.
- (id)initWithDate:(NSDate *)date block:(void (^)(void))block;
@end
static CFComparisonResult RACCompareScheduledActions(const void *ptr1, const void *ptr2, void *info) {
RACTestSchedulerAction *action1 = (__bridge id)ptr1;
RACTestSchedulerAction *action2 = (__bridge id)ptr2;
return CFDateCompare((__bridge CFDateRef)action1.date, (__bridge CFDateRef)action2.date, NULL);
}
static const void *RACRetainScheduledAction(CFAllocatorRef allocator, const void *ptr) {
return CFRetain(ptr);
}
static void RACReleaseScheduledAction(CFAllocatorRef allocator, const void *ptr) {
CFRelease(ptr);
}
@interface RACTestScheduler ()
// All of the RACTestSchedulerActions that have been enqueued and not yet
// executed.
//
// The minimum value in the heap represents the action to execute next.
//
// This property should only be used while synchronized on self.
@property (nonatomic, assign, readonly) CFBinaryHeapRef scheduledActions;
// The number of blocks that have been directly enqueued with -schedule: so
// far.
//
// This is used to ensure unique dates when two blocks are enqueued
// simultaneously.
//
// This property should only be used while synchronized on self.
@property (nonatomic, assign) NSUInteger numberOfDirectlyScheduledBlocks;
@end
@implementation RACTestScheduler
#pragma mark Lifecycle
- (instancetype)init {
self = [super initWithName:@"com.github.ReactiveCocoa.RACTestScheduler"];
if (self == nil) return nil;
CFBinaryHeapCallBacks callbacks = (CFBinaryHeapCallBacks){
.version = 0,
.retain = &RACRetainScheduledAction,
.release = &RACReleaseScheduledAction,
.copyDescription = &CFCopyDescription,
.compare = &RACCompareScheduledActions
};
_scheduledActions = CFBinaryHeapCreate(NULL, 0, &callbacks, NULL);
return self;
}
- (void)dealloc {
[self stepAll];
if (_scheduledActions != NULL) {
CFRelease(_scheduledActions);
_scheduledActions = NULL;
}
}
#pragma mark Execution
- (void)step {
[self step:1];
}
- (void)step:(NSUInteger)ticks {
@synchronized (self) {
for (NSUInteger i = 0; i < ticks; i++) {
const void *actionPtr = NULL;
if (!CFBinaryHeapGetMinimumIfPresent(self.scheduledActions, &actionPtr)) break;
RACTestSchedulerAction *action = (__bridge id)actionPtr;
CFBinaryHeapRemoveMinimumValue(self.scheduledActions);
if (action.disposable.disposed) continue;
RACScheduler *previousScheduler = RACScheduler.currentScheduler;
NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = self;
action.block();
if (previousScheduler != nil) {
NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = previousScheduler;
} else {
[NSThread.currentThread.threadDictionary removeObjectForKey:RACSchedulerCurrentSchedulerKey];
}
}
}
}
- (void)stepAll {
[self step:NSUIntegerMax];
}
#pragma mark RACScheduler
- (RACDisposable *)schedule:(void (^)(void))block {
NSCParameterAssert(block != nil);
@synchronized (self) {
NSDate *uniqueDate = [NSDate dateWithTimeIntervalSinceReferenceDate:self.numberOfDirectlyScheduledBlocks];
self.numberOfDirectlyScheduledBlocks++;
RACTestSchedulerAction *action = [[RACTestSchedulerAction alloc] initWithDate:uniqueDate block:block];
CFBinaryHeapAddValue(self.scheduledActions, (__bridge void *)action);
return action.disposable;
}
}
- (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block {
NSCParameterAssert(date != nil);
NSCParameterAssert(block != nil);
@synchronized (self) {
RACTestSchedulerAction *action = [[RACTestSchedulerAction alloc] initWithDate:date block:block];
CFBinaryHeapAddValue(self.scheduledActions, (__bridge void *)action);
return action.disposable;
}
}
- (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block {
NSCParameterAssert(date != nil);
NSCParameterAssert(block != nil);
NSCParameterAssert(interval >= 0);
NSCParameterAssert(leeway >= 0);
RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable];
@weakify(self);
@synchronized (self) {
__block RACDisposable *thisDisposable = nil;
void (^reschedulingBlock)(void) = ^{
@strongify(self);
[compoundDisposable removeDisposable:thisDisposable];
// Schedule the next interval.
RACDisposable *schedulingDisposable = [self after:[date dateByAddingTimeInterval:interval] repeatingEvery:interval withLeeway:leeway schedule:block];
[compoundDisposable addDisposable:schedulingDisposable];
block();
};
RACTestSchedulerAction *action = [[RACTestSchedulerAction alloc] initWithDate:date block:reschedulingBlock];
CFBinaryHeapAddValue(self.scheduledActions, (__bridge void *)action);
thisDisposable = action.disposable;
[compoundDisposable addDisposable:thisDisposable];
}
return compoundDisposable;
}
@end
@implementation RACTestSchedulerAction
#pragma mark Lifecycle
- (id)initWithDate:(NSDate *)date block:(void (^)(void))block {
NSCParameterAssert(date != nil);
NSCParameterAssert(block != nil);
self = [super init];
if (self == nil) return nil;
_date = [date copy];
_block = [block copy];
_disposable = [[RACDisposable alloc] init];
return self;
}
#pragma mark NSObject
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p>{ date: %@ }", self.class, self, self.date];
}
@end