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 (%@) is pretty recent", 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 \"%@\" value \"%@\" matches expected \"%@\"", key, rowdata[key], attrs[key]);
    }
    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, %@", error);
    [self recentTimeStamp:rowdata[SFAnalyticsEventTime]];
    XCTAssertTrue([rowdata[SFAnalyticsEventType] isKindOfClass:[NSString class]] && [rowdata[SFAnalyticsEventType] isEqualToString:eventType], @"found eventType \"%@\" in db", eventType);
    XCTAssertTrue([rowdata[SFAnalyticsEventClassKey] isKindOfClass:[NSNumber class]] && [rowdata[SFAnalyticsEventClassKey] intValue] == class, @"eventClass is %ld", class);
    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 = %@", eventType];
    XCTAssert([result next]);
    XCTAssertTrue([[result stringAtIndex:0] isEqualToString:eventType], @"event name \"%@\", expected \"%@\"", [result stringAtIndex:0], eventType);
    XCTAssertEqual([result intAtIndex:1], success, @"correct count of successes: %d / %d", [result intAtIndex:1], success);
    XCTAssertEqual([result intAtIndex:2], hard, @"correct count of successes: %d / %d", [result intAtIndex:2], hard);
    XCTAssertEqual([result intAtIndex:3], soft, @"correct count of successes: %d / %d", [result intAtIndex:3], soft);
    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:@"%@/", [[NSUUID UUID] UUIDString]]];
    [[NSFileManager defaultManager] createDirectoryAtPath:_path
                              withIntermediateDirectories:YES
                                               attributes:nil
                                                    error:&error];
    // No XCTAssert in class method
    if (error) {
        NSLog(@"Could not make directory at %@", _path);
    }
    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_%ld.db", (long)++_testnum];
    NSLog(@"sqlite3 %@", _dbpath);
    [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:@"%@/empty", _path];

    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_%li", (long)_testnum];
    
    // 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_%li", (long)_testnum];
    __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_%li", (long)_testnum];
    __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_%li", (long)_testnum];
    __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_%li", (long)_testnum];
    __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_%li", (long)_testnum];

    // 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_%li", (long)_testnum];
    __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_%li", (long)_testnum];
    [_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_%li", (long)_testnum];
    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_%li", (long)_testnum];
    __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_%li", (long)_testnum];

    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_%li", (long)_testnum];
    __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_%li", (long)_testnum];
    __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