SFAnalyticsTests.m [plain text]
/*
* Copyright (c) 2017 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/
#import <XCTest/XCTest.h>
#import <Security/SFAnalytics.h>
#import "SFAnalyticsDefines.h"
#import "SFAnalyticsSQLiteStore.h"
#import "SFSQLite.h"
#import <Prequelite/Prequelite.h>
#import <CoreFoundation/CFPriv.h>
#import <notify.h>
@interface UnitTestAnalytics : SFAnalytics
+ (NSString*)databasePath;
+ (void)setDatabasePath:(NSString*)path;
@end
// MARK: SFAnalytics subclass for custom DB
@implementation UnitTestAnalytics
static NSString* _utapath;
+ (NSString*)databasePath
{
return _utapath;
}
+ (void)setDatabasePath:(NSString*)path
{
_utapath = path;
}
@end
@interface SFAnalyticsTests : XCTestCase
@end
@implementation SFAnalyticsTests
{
UnitTestAnalytics* _analytics;
NSString* _dbpath;
PQLConnection* _db;
}
static NSString* _path;
static NSInteger _testnum;
static NSString* build = NULL;
static NSString* product = NULL;
// MARK: Test helper methods
- (void)assertNoSuccessEvents
{
XCTAssertFalse([[_db fetch:@"select * from success_count"] next]);
}
- (void)assertNoHardFailures
{
XCTAssertFalse([[_db fetch:@"select * from hard_failures"] next]);
}
- (void)assertNoSoftFailures
{
XCTAssertFalse([[_db fetch:@"select * from soft_failures"] next]);
}
- (void)assertNoAllEvents
{
XCTAssertFalse([[_db fetch:@"select * from all_events"] next]);
}
- (void)assertNoSamples
{
XCTAssertFalse([[_db fetch:@"select * from samples"] next]);
}
- (void)assertNoEventsAnywhere
{
[self assertNoAllEvents];
[self assertNoSuccessEvents];
[self assertNoHardFailures];
[self assertNoSoftFailures];
[self assertNoSamples];
}
- (void)recentTimeStamp:(NSNumber*)timestamp
{
XCTAssert([timestamp isKindOfClass:[NSNumber class]], @"Timestamp is an NSNumber");
NSDate* eventTime = [NSDate dateWithTimeIntervalSince1970:[timestamp doubleValue]];
XCTAssertLessThanOrEqual([[NSDate date] timeIntervalSinceDate:eventTime], 5, @"Timestamp (}
- (void)properEventLogged:(PQLResultSet*)result eventType:(NSString*)eventType class:(SFAnalyticsEventClass)class attributes:(NSDictionary*)attrs
{
[self _properEventLogged:result eventType:eventType class:class];
NSDictionary* rowdata = [NSPropertyListSerialization propertyListWithData:[result dataAtIndex:2] options:NSPropertyListImmutable format:nil error:nil];
for (NSString* key in [attrs allKeys]) {
XCTAssert([attrs[key] isEqualToString:rowdata[key]], @"Attribute \" }
XCTAssertFalse([result next], @"only one row returned");
}
- (void)properEventLogged:(PQLResultSet*)result eventType:(NSString*)eventType class:(SFAnalyticsEventClass)class
{
[self _properEventLogged:result eventType:eventType class:class];
XCTAssertFalse([result next], @"only one row returned");
}
- (void)_properEventLogged:(PQLResultSet*)result eventType:(NSString*)eventType class:(SFAnalyticsEventClass)class
{
XCTAssert([result next], @"result found after adding an event");
NSError* error = nil;
[result doubleAtIndex:1];
NSDictionary* rowdata = [NSPropertyListSerialization propertyListWithData:[result dataAtIndex:2] options:NSPropertyListImmutable format:nil error:&error];
XCTAssertNotNil(rowdata, @"able to deserialize db data, [self recentTimeStamp:rowdata[SFAnalyticsEventTime]];
XCTAssertTrue([rowdata[SFAnalyticsEventType] isKindOfClass:[NSString class]] && [rowdata[SFAnalyticsEventType] isEqualToString:eventType], @"found eventType \" XCTAssertTrue([rowdata[SFAnalyticsEventClassKey] isKindOfClass:[NSNumber class]] && [rowdata[SFAnalyticsEventClassKey] intValue] == class, @"eventClass is XCTAssertTrue([rowdata[@"build"] isEqualToString:build], @"event row includes build");
XCTAssertTrue([rowdata[@"product"] isEqualToString:product], @"event row includes product");
}
- (void)checkSuccessCountsForEvent:(NSString*)eventType success:(int)success hard:(int)hard soft:(int)soft
{
PQLResultSet* result = [_db fetch:@"select * from success_count where event_type = XCTAssert([result next]);
XCTAssertTrue([[result stringAtIndex:0] isEqualToString:eventType], @"event name \" XCTAssertEqual([result intAtIndex:1], success, @"correct count of successes: XCTAssertEqual([result intAtIndex:2], hard, @"correct count of successes: XCTAssertEqual([result intAtIndex:3], soft, @"correct count of successes: XCTAssertFalse([result next], @"no more than one row returned");
}
- (void)checkSamples:(NSArray*)samples name:(NSString*)samplerName totalSamples:(NSUInteger)total accuracy:(double)accuracy
{
NSUInteger samplescount = 0, targetcount = 0;
NSMutableArray* samplesfound = [NSMutableArray array];
PQLResultSet* result = [_db fetch:@"select * from samples"];
while ([result next]) {
++samplescount;
[self recentTimeStamp:[result numberAtIndex:1]];
if ([[result stringAtIndex:2] isEqual:samplerName]) {
++targetcount;
[samplesfound addObject:[result numberAtIndex:3]];
}
}
XCTAssertEqual([samples count], targetcount);
XCTAssertEqual(samplescount, total);
[samplesfound sortUsingSelector:@selector(compare:)];
NSArray* sortedInput = [samples sortedArrayUsingSelector:@selector(compare:)];
for (NSUInteger idx = 0; idx < [samples count]; ++idx) {
XCTAssertEqualWithAccuracy([samplesfound[idx] doubleValue], [sortedInput[idx] doubleValue], accuracy);
}
}
- (void)waitForSamplerWork:(double)interval
{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * interval), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
dispatch_semaphore_signal(sema);
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
// MARK: Test administration
+ (void)setUp
{
NSError* error;
_path = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@" [[NSFileManager defaultManager] createDirectoryAtPath:_path
withIntermediateDirectories:YES
attributes:nil
error:&error];
// No XCTAssert in class method
if (error) {
NSLog(@"Could not make directory at }
NSDictionary *version = CFBridgingRelease(_CFCopySystemVersionDictionary());
if (version) {
build = version[(__bridge NSString *)_kCFSystemVersionBuildVersionKey];
product = version[(__bridge NSString *)_kCFSystemVersionProductNameKey];
} else {
NSLog(@"could not get build version/product, tests should fail");
}
}
- (void)setUp
{
[super setUp];
self.continueAfterFailure = NO;
NSError* error = nil;
_dbpath = [_path stringByAppendingFormat:@"/test_ NSLog(@"sqlite3 [UnitTestAnalytics setDatabasePath:_dbpath];
_analytics = [UnitTestAnalytics logger];
_db = [PQLConnection new];
XCTAssertTrue([_db openAtURL:[NSURL URLWithString:_dbpath] sharedCache:NO error:&error]);
XCTAssertNil(error, @"could open db");
XCTAssertNotNil(_db);
}
- (void)tearDown
{
NSError *error = nil;
XCTAssertTrue([_db close:&error], @"could close db");
XCTAssertNil(error, @"No error from closing db");
[_analytics removeState];
[super tearDown];
}
+ (void)tearDown
{
[[NSFileManager defaultManager] removeItemAtPath:_path error:nil];
}
// MARK: SFAnalytics Tests
- (void)testDbIsEmptyAtStartup
{
[self assertNoEventsAnywhere];
}
- (void)testDontCrashWithEmptyDBPath
{
NSString* schema = @"CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY AUTOINCREMENT,data BLOB);";
NSString* path = [NSString stringWithFormat:@"
XCTAssertNil([SFAnalyticsSQLiteStore storeWithPath:nil schema:schema]);
XCTAssertNil([SFAnalyticsSQLiteStore storeWithPath:@"" schema:schema]);
XCTAssertNil([SFAnalyticsSQLiteStore storeWithPath:path schema:nil]);
XCTAssertNil([SFAnalyticsSQLiteStore storeWithPath:path schema:@""]);
XCTAssertNil([[SFSQLite alloc] initWithPath:nil schema:schema]);
XCTAssertNil([[SFSQLite alloc] initWithPath:@"" schema:schema]);
XCTAssertNil([[SFSQLite alloc] initWithPath:path schema:nil]);
XCTAssertNil([[SFSQLite alloc] initWithPath:path schema:@""]);
}
- (void)testAddingEventsWithNilName
{
[_analytics logSuccessForEventNamed:nil];
[self assertNoEventsAnywhere];
[_analytics logHardFailureForEventNamed:nil withAttributes:nil];
[self assertNoEventsAnywhere];
[_analytics logSoftFailureForEventNamed:nil withAttributes:nil];
[self assertNoEventsAnywhere];
[_analytics noteEventNamed:nil];
[self assertNoEventsAnywhere];
}
- (void)testLogSuccess
{
[_analytics logSuccessForEventNamed:@"unittestevent"];
[self assertNoHardFailures];
[self assertNoSoftFailures];
PQLResultSet* result = [_db fetch:@"select success_count from success_count"];
XCTAssert([result next], @"a row was found after adding an event");
XCTAssertEqual([result intAtIndex:0], 1, @"success count is 1 after adding an event");
XCTAssertFalse([result next], @"only one row found in success_count after inserting a single event");
result = [_db fetch:@"select * from all_events"];
[self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSuccess];
}
- (void)testLogRecoverableFailure
{
[_analytics logSoftFailureForEventNamed:@"unittestevent" withAttributes:nil];
[self assertNoHardFailures];
// First check success_count has logged a soft failure
[self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:0 soft:1];
// then check soft_failures itself
PQLResultSet* result = [_db fetch:@"select * from soft_failures"];
[self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSoftFailure];
// finally check all_events
result = [_db fetch:@"select * from all_events"];
[self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSoftFailure];
}
- (void)testLogRecoverablyFailureWithAttributes
{
NSDictionary* attrs = @{@"attr1" : @"value1", @"attr2" : @"value2"};
[_analytics logSoftFailureForEventNamed:@"unittestevent" withAttributes:attrs];
[self assertNoHardFailures];
[self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:0 soft:1];
// then check soft_failures itself
PQLResultSet* result = [_db fetch:@"select * from soft_failures"];
[self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSoftFailure attributes:attrs];
// finally check all_events
result = [_db fetch:@"select * from all_events"];
[self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSoftFailure attributes:attrs];
}
- (void)testLogUnrecoverableFailure
{
[_analytics logHardFailureForEventNamed:@"unittestevent" withAttributes:nil];
[self assertNoSoftFailures];
// First check success_count has logged a hard failure
[self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:1 soft:0];
// then check hard_failures itself
PQLResultSet* result = [_db fetch:@"select * from hard_failures"];
[self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassHardFailure];
// finally check all_events
result = [_db fetch:@"select * from all_events"];
[self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassHardFailure];
}
- (void)testLogUnrecoverableFailureWithAttributes
{
NSDictionary* attrs = @{@"attr1" : @"value1", @"attr2" : @"value2"};
[_analytics logHardFailureForEventNamed:@"unittestevent" withAttributes:attrs];
[self assertNoSoftFailures];
// First check success_count has logged a hard failure
[self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:1 soft:0];
// then check hard_failures itself
PQLResultSet* result = [_db fetch:@"select * from hard_failures"];
[self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassHardFailure attributes:attrs];
// finally check all_events
result = [_db fetch:@"select * from all_events"];
[self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassHardFailure attributes:attrs];
}
- (void)testLogSeveralEvents
{
NSDictionary* attrs = @{@"attr1" : @"value1", @"attr2" : @"value2"};
int iterations = 100;
for (int idx = 0; idx < iterations; ++idx) {
[_analytics logHardFailureForEventNamed:@"unittesthardfailure" withAttributes:attrs];
[_analytics logSoftFailureForEventNamed:@"unittestsoftfailure" withAttributes:attrs];
[_analytics logSuccessForEventNamed:@"unittestsuccess"];
[_analytics logHardFailureForEventNamed:@"unittestcombined" withAttributes:attrs];
[_analytics logSoftFailureForEventNamed:@"unittestcombined" withAttributes:attrs];
[_analytics logSuccessForEventNamed:@"unittestcombined"];
}
[self checkSuccessCountsForEvent:@"unittesthardfailure" success:0 hard:iterations soft:0];
[self checkSuccessCountsForEvent:@"unittestsoftfailure" success:0 hard:0 soft:iterations];
[self checkSuccessCountsForEvent:@"unittestsuccess" success:iterations hard:0 soft:0];
[self checkSuccessCountsForEvent:@"unittestcombined" success:iterations hard:iterations soft:iterations];
}
- (void)testNoteEvent
{
[_analytics noteEventNamed:@"unittestevent"];
[self assertNoSoftFailures];
[self assertNoHardFailures];
// First check success_count has logged a success
[self checkSuccessCountsForEvent:@"unittestevent" success:1 hard:0 soft:0];
PQLResultSet* result = [_db fetch:@"select * from all_events"];
[self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassNote];
}
// MARK: SFAnalyticsSampler Tests
- (void)testSamplerSimple
{
NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerSimple_
// This block should be set immediately and fire in 1000ms. Give it a little slack in checking though
XCTestExpectation* exp = [self expectationWithDescription:@"waiting for sampler to fire"];
[_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
[exp fulfill];
return @15.3;
}];
[self waitForExpectations:@[exp] timeout:1.2f];
[_analytics removeMetricSamplerForName:samplerName];
// The expectation is fulfilled before returning and after returning some more work needs to happen. Let it settle down.
[self waitForSamplerWork:0.2f];
[self checkSamples:@[@15.3] name:samplerName totalSamples:1 accuracy:0.01f];
}
// Test state removal mostly
- (void)testSamplerSimpleLoop
{
[self tearDown];
for (int idx = 0; idx < 3; ++idx) {
[self setUp];
@autoreleasepool {
[self testSamplerSimple];
}
[self tearDown];
}
}
- (void)testSamplerDoesNotFirePrematurely
{
NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerDoesNotFirePrematurely_ __block BOOL run = NO;
[_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
run = YES;
return @0.9;
}];
[self waitForSamplerWork:0.5f];
XCTAssertFalse(run, @"sample did not fire prematurely");
[_analytics removeMetricSamplerForName:samplerName];
}
- (void)testSamplerRemove
{
NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerRemove_ __block BOOL run = NO;
[_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
run = YES;
return @23.8;
}];
XCTAssertNotNil([_analytics existingMetricSamplerForName:samplerName], @"SFAnalytics held onto the sampler we setup");
[_analytics removeMetricSamplerForName:samplerName];
XCTAssertNil([_analytics existingMetricSamplerForName:samplerName], @"SFAnalytics got rid of our sampler");
[self waitForSamplerWork:2.0f];
XCTAssertFalse(run, @"sampler did not run after removal");
}
- (void)testSamplerRepeatedSampling
{
NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerRepeatedSampling_ __block int run = 0;
[_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
run += 1;
return @1.5;
}];
[self waitForSamplerWork:3.5f];
[_analytics removeMetricSamplerForName:samplerName];
XCTAssertEqual(run, 3, @"sampler ran correct number of times");
[self checkSamples:@[@1.5, @1.5, @1.5] name:samplerName totalSamples:3 accuracy:0.01f];
}
- (void)testSamplerDisable
{
NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerDisable_ __block int run = 0;
[_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
run += 1;
return @44.9;
}];
[[_analytics existingMetricSamplerForName:samplerName] pauseSampling];
[self waitForSamplerWork:2.0f];
XCTAssertEqual(run, 0, @"sampler did not run while disabled");
[[_analytics existingMetricSamplerForName:samplerName] resumeSampling];
[self waitForSamplerWork:1.3f];
XCTAssertEqual(run, 1, @"sampler ran after resuming");
[self checkSamples:@[@44.9] name:samplerName totalSamples:1 accuracy:0.01f];
}
- (void)testSamplerWithBadData
{
NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerWithBadData_
// bad name
XCTAssertNil([_analytics addMetricSamplerForName:nil withTimeInterval:3.0f block:^NSNumber *{
return @0.0;
}]);
// bad interval
XCTAssertNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:0.0f block:^NSNumber *{
return @0.0;
}]);
XCTAssertNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:2.0f block:nil]);
}
- (void)testSamplerOncePerReport
{
NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerOncePerReport_ __block int run = 0;
[_analytics addMetricSamplerForName:samplerName withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSNumber *{
run += 1;
return @74.1;
}];
// There's no point in waiting, it could have been set to some arbitrarily long timer instead
notify_post(SFAnalyticsFireSamplersNotification);
[self waitForSamplerWork:0.5f];
XCTAssertEqual(run, 1, @"once-per-report sampler fired once in response to notification");
[self checkSamples:@[@74.1] name:samplerName totalSamples:1 accuracy:0.01f];
}
- (void)testSamplerOncePerReportEnsuresSingleSampleInDatabase
{
NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerSetTimeInterval_ [_analytics addMetricSamplerForName:samplerName withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSNumber *{
return @57.6;
}];
notify_post(SFAnalyticsFireSamplersNotification);
[self waitForSamplerWork:0.5f];
notify_post(SFAnalyticsFireSamplersNotification);
[self waitForSamplerWork:0.5f];
[self checkSamples:@[@57.6] name:samplerName totalSamples:1 accuracy:0.01f];
}
- (void)testSamplerAddSamplerTwice
{
NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerDisable_ XCTAssertNotNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:3.0f block:^NSNumber *{
return @7.7;
}], @"adding first sampler works okay");
XCTAssertNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:3.0f block:^NSNumber *{
return @7.8;
}], @"adding duplicate sampler did not work");
}
- (void)testSamplerLogBadSample
{
[_analytics logMetric:nil withName:@"testsampler"];
[self checkSamples:@[] name:@"testsampler" totalSamples:0 accuracy:0.01f];
id badobj = [NSString stringWithUTF8String:"yolo!"];
[_analytics logMetric:badobj withName:@"testSampler"];
[self checkSamples:@[] name:@"testsampler" totalSamples:0 accuracy:0.01f];
}
- (void)testSamplerSetTimeInterval
{
NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerSetTimeInterval_ __block NSUInteger run = 0;
[_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
++run;
return @23.8;
}];
[self waitForSamplerWork:1.2f];
[_analytics existingMetricSamplerForName:samplerName].samplingInterval = 1.5f;
[self waitForSamplerWork:2.5f];
XCTAssertEqual(run, 2ul);
[self checkSamples:@[@23.8, @23.8] name:samplerName totalSamples:2 accuracy:0.01f];
}
// MARK: SFAnalyticsMultiSampler Tests
- (void)testMultiSamplerSimple
{
NSString* samplerName = [NSString stringWithFormat:@"UnitTestMultiSamplerSimple_
XCTestExpectation* exp = [self expectationWithDescription:@"waiting for sampler to fire"];
[_analytics AddMultiSamplerForName:samplerName withTimeInterval:1.0f block:^NSDictionary<NSString *,NSNumber *> *{
[exp fulfill];
return @{@"val1" : @89.4f, @"val2" : @11.2f};
}];
[self waitForExpectations:@[exp] timeout:1.3f];
[_analytics removeMultiSamplerForName:samplerName];
// The expectation is fulfilled before returning and after returning some more work needs to happen. Let it settle down.
[self waitForSamplerWork:0.2f];
[self checkSamples:@[@89.4f] name:@"val1" totalSamples:2 accuracy:0.01f];
[self checkSamples:@[@11.2f] name:@"val2" totalSamples:2 accuracy:0.01f];
}
- (void)testMultiSamplerOncePerReport
{
NSString* samplerName = [NSString stringWithFormat:@"UnitTestMultiSamplerOncePerReport_ __block int run = 0;
[_analytics AddMultiSamplerForName:samplerName withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{
run += 1;
return @{@"val1" : @33.8f, @"val2" : @54.6f};
}];
// There's no point in waiting, it could have been set to some arbitrarily long timer instead
notify_post(SFAnalyticsFireSamplersNotification);
[self waitForSamplerWork:1.0f];
XCTAssertEqual(run, 1, @"once-per-report sampler fired once in response to notification");
[self checkSamples:@[@33.8f] name:@"val1" totalSamples:2 accuracy:0.01f];
[self checkSamples:@[@54.6f] name:@"val2" totalSamples:2 accuracy:0.01f];
}
- (void)testMultiSamplerSetTimeInterval
{
NSString* samplerName = [NSString stringWithFormat:@"UnitTestMultiSamplerSetTimeInterval_ __block NSUInteger run = 0;
[_analytics AddMultiSamplerForName:samplerName withTimeInterval:1.0f block:^NSDictionary<NSString *,NSNumber *> *{
++run;
return @{@"val1" : @29.3f, @"val2" : @19.3f};
}];
[self waitForSamplerWork:1.2f];
[_analytics existingMultiSamplerForName:samplerName].samplingInterval = 1.5f;
[self waitForSamplerWork:2.5f];
XCTAssertEqual(run, 2ul);
[self checkSamples:@[@29.3f, @29.3f] name:@"val1" totalSamples:4 accuracy:0.01f];
[self checkSamples:@[@19.3f, @19.3f] name:@"val2" totalSamples:4 accuracy:0.01f];
}
// MARK: SFAnalyticsActivityTracker Tests
- (void)testTrackerSimple
{
NSString* trackerName = @"UnitTestTrackerSimple";
@autoreleasepool {
[_analytics logSystemMetricsForActivityNamed:trackerName withAction:^{
[NSThread sleepForTimeInterval:0.3f];
}];
}
[self checkSamples:@[@(0.3f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.05f * NSEC_PER_SEC)];
}
- (void)testTrackerMultipleBlocks
{
NSString* trackerName = @"UnitTestTrackerMultipleBlocks";
@autoreleasepool {
SFAnalyticsActivityTracker* tracker = [_analytics logSystemMetricsForActivityNamed:trackerName withAction:^{
[NSThread sleepForTimeInterval:0.3f];
}];
[tracker performAction:^{
[NSThread sleepForTimeInterval:0.2f];
}];
}
[self checkSamples:@[@(0.5f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.1f * NSEC_PER_SEC)];
}
- (void)testTrackerAction
{
NSString* trackerName = @"UnitTestTrackerOneBlock";
@autoreleasepool {
SFAnalyticsActivityTracker* tracker = [_analytics logSystemMetricsForActivityNamed:trackerName withAction:NULL];
[tracker performAction:^{
[NSThread sleepForTimeInterval:0.2f];
}];
}
[self checkSamples:@[@(0.2f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.1f * NSEC_PER_SEC)];
}
- (void)testTrackerStartStop {
NSString* trackerName = @"UnitTestTrackerStartStop";
@autoreleasepool {
SFAnalyticsActivityTracker* tracker = [_analytics logSystemMetricsForActivityNamed:trackerName withAction:NULL];
[tracker start];
[NSThread sleepForTimeInterval:0.2f];
[tracker stop];
}
[self checkSamples:@[@(0.2f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.1f * NSEC_PER_SEC)];
}
- (void)testTrackerCancel
{
NSString* trackerName = @"UnitTestTrackerCancel";
@autoreleasepool {
[[_analytics logSystemMetricsForActivityNamed:trackerName withAction:^{
[NSThread sleepForTimeInterval:0.3f];
}] cancel];
}
[self assertNoEventsAnywhere];
}
- (void)testTrackerBadData
{
// Inspect database to find out it's empty
[_analytics logMetric:nil withName:@"fake"];
[_analytics logMetric:@3.0 withName:nil];
// get object back so inspect that, too
XCTAssertNil([_analytics logSystemMetricsForActivityNamed:nil withAction:^{return;}]);
[self assertNoEventsAnywhere];
}
// MARK: Miscellaneous
- (void)testInstantiateBaseClass
{
XCTAssertNil([SFAnalytics logger]);
}
- (void)testFuzzyDaysSinceDate
{
NSInteger secondsPerDay = 60 * 60 * 24;
XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate date]], 0);
XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate dateWithTimeIntervalSinceNow:secondsPerDay * -3]], 1);
XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate dateWithTimeIntervalSinceNow:secondsPerDay * -18]], 7);
XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate dateWithTimeIntervalSinceNow:secondsPerDay * -77]], 30);
XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate dateWithTimeIntervalSinceNow:secondsPerDay * -370]], 365);
XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate distantPast]], 1000);
XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:nil], -1);
}
- (void)testRingBuffer {
[self assertNoEventsAnywhere];
for (unsigned idx = 0; idx < (SFAnalyticsMaxEventsToReport + 50); ++idx) {
[_analytics logHardFailureForEventNamed:@"ringbufferevent" withAttributes:nil];
}
PQLResultSet* result = [_db fetch:@"select count(*) from hard_failures"];
XCTAssertTrue([result next], @"Got a count from hard_failures");
XCTAssertLessThanOrEqual([result unsignedIntAtIndex:0], SFAnalyticsMaxEventsToReport, @"Ring buffer contains a sane number of events");
// all_events has a much larger buffer so it should handle the extra events okay
result = [_db fetch:@"select count(*) from all_events"];
XCTAssertTrue([result next], @"Got a count from all_events");
XCTAssertLessThanOrEqual([result unsignedIntAtIndex:0], SFAnalyticsMaxEventsToReport + 50);
}
- (void)testRaceToCreateLoggers
{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
for (NSInteger idx = 0; idx < 500; ++idx) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UnitTestAnalytics* logger = [UnitTestAnalytics logger];
[logger logSuccessForEventNamed:@"testevent"];
dispatch_semaphore_signal(semaphore);
});
}
for (NSInteger idx = 0; idx < 500; ++idx) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
}
- (void)testDateProperty
{
NSString* propertyKey = @"testDataPropertyKey";
XCTAssertNil([_analytics datePropertyForKey:propertyKey]);
NSDate* test = [NSDate date];
[_analytics setDateProperty:test forKey:propertyKey];
NSDate* retrieved = [_analytics datePropertyForKey:propertyKey];
XCTAssert(retrieved);
// Storing in SQLite as string loses subsecond resolution, so we need some slack
XCTAssertEqualWithAccuracy([test timeIntervalSinceDate:retrieved], 0, 1);
[_analytics setDateProperty:nil forKey:propertyKey];
XCTAssertNil([_analytics datePropertyForKey:propertyKey]);
}
@end