2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
24 #import <XCTest/XCTest.h>
25 #import <Security/SFAnalytics.h>
26 #import "SFAnalyticsDefines.h"
27 #import "SFAnalyticsSQLiteStore.h"
29 #import <Prequelite/Prequelite.h>
30 #import <CoreFoundation/CFPriv.h>
33 @interface UnitTestAnalytics : SFAnalytics
34 + (NSString*)databasePath;
35 + (void)setDatabasePath:(NSString*)path;
38 // MARK: SFAnalytics subclass for custom DB
40 @implementation UnitTestAnalytics
41 static NSString* _utapath;
43 + (NSString*)databasePath
48 + (void)setDatabasePath:(NSString*)path
55 @interface SFAnalyticsTests : XCTestCase
58 @implementation SFAnalyticsTests
60 UnitTestAnalytics* _analytics;
65 static NSString* _path;
66 static NSInteger _testnum;
67 static NSString* build = NULL;
68 static NSString* product = NULL;
70 // MARK: Test helper methods
72 - (void)assertNoSuccessEvents
74 XCTAssertFalse([[_db fetch:@"select * from success_count"] next]);
77 - (void)assertNoHardFailures
79 XCTAssertFalse([[_db fetch:@"select * from hard_failures"] next]);
82 - (void)assertNoSoftFailures
84 XCTAssertFalse([[_db fetch:@"select * from soft_failures"] next]);
87 - (void)assertNoAllEvents
89 XCTAssertFalse([[_db fetch:@"select * from all_events"] next]);
92 - (void)assertNoSamples
94 XCTAssertFalse([[_db fetch:@"select * from samples"] next]);
97 - (void)assertNoEventsAnywhere
99 [self assertNoAllEvents];
100 [self assertNoSuccessEvents];
101 [self assertNoHardFailures];
102 [self assertNoSoftFailures];
103 [self assertNoSamples];
106 - (void)recentTimeStamp:(NSNumber*)timestamp
108 XCTAssert([timestamp isKindOfClass:[NSNumber class]], @"Timestamp is an NSNumber");
109 NSDate* eventTime = [NSDate dateWithTimeIntervalSince1970:[timestamp doubleValue]];
110 XCTAssertLessThanOrEqual([[NSDate date] timeIntervalSinceDate:eventTime], 5, @"Timestamp (%@) is pretty recent", timestamp);
113 - (void)properEventLogged:(PQLResultSet*)result eventType:(NSString*)eventType class:(SFAnalyticsEventClass)class attributes:(NSDictionary*)attrs
115 [self _properEventLogged:result eventType:eventType class:class];
117 NSDictionary* rowdata = [NSPropertyListSerialization propertyListWithData:[result dataAtIndex:2] options:NSPropertyListImmutable format:nil error:nil];
118 for (NSString* key in [attrs allKeys]) {
119 XCTAssert([attrs[key] isEqualToString:rowdata[key]], @"Attribute \"%@\" value \"%@\" matches expected \"%@\"", key, rowdata[key], attrs[key]);
121 XCTAssertFalse([result next], @"only one row returned");
124 - (void)properEventLogged:(PQLResultSet*)result eventType:(NSString*)eventType class:(SFAnalyticsEventClass)class
126 [self _properEventLogged:result eventType:eventType class:class];
127 XCTAssertFalse([result next], @"only one row returned");
130 - (void)_properEventLogged:(PQLResultSet*)result eventType:(NSString*)eventType class:(SFAnalyticsEventClass)class
132 XCTAssert([result next], @"result found after adding an event");
133 NSError* error = nil;
134 [result doubleAtIndex:1];
135 NSDictionary* rowdata = [NSPropertyListSerialization propertyListWithData:[result dataAtIndex:2] options:NSPropertyListImmutable format:nil error:&error];
136 XCTAssertNotNil(rowdata, @"able to deserialize db data, %@", error);
137 [self recentTimeStamp:rowdata[SFAnalyticsEventTime]];
138 XCTAssertTrue([rowdata[SFAnalyticsEventType] isKindOfClass:[NSString class]] && [rowdata[SFAnalyticsEventType] isEqualToString:eventType], @"found eventType \"%@\" in db", eventType);
139 XCTAssertTrue([rowdata[SFAnalyticsEventClassKey] isKindOfClass:[NSNumber class]] && [rowdata[SFAnalyticsEventClassKey] intValue] == class, @"eventClass is %ld", class);
140 XCTAssertTrue([rowdata[@"build"] isEqualToString:build], @"event row includes build");
141 XCTAssertTrue([rowdata[@"product"] isEqualToString:product], @"event row includes product");
144 - (void)checkSuccessCountsForEvent:(NSString*)eventType success:(int)success hard:(int)hard soft:(int)soft
146 PQLResultSet* result = [_db fetch:@"select * from success_count where event_type = %@", eventType];
147 XCTAssert([result next]);
148 XCTAssertTrue([[result stringAtIndex:0] isEqualToString:eventType], @"event name \"%@\", expected \"%@\"", [result stringAtIndex:0], eventType);
149 XCTAssertEqual([result intAtIndex:1], success, @"correct count of successes: %d / %d", [result intAtIndex:1], success);
150 XCTAssertEqual([result intAtIndex:2], hard, @"correct count of successes: %d / %d", [result intAtIndex:2], hard);
151 XCTAssertEqual([result intAtIndex:3], soft, @"correct count of successes: %d / %d", [result intAtIndex:3], soft);
152 XCTAssertFalse([result next], @"no more than one row returned");
155 - (void)checkSamples:(NSArray*)samples name:(NSString*)samplerName totalSamples:(NSUInteger)total accuracy:(double)accuracy
157 NSUInteger samplescount = 0, targetcount = 0;
158 NSMutableArray* samplesfound = [NSMutableArray array];
159 PQLResultSet* result = [_db fetch:@"select * from samples"];
160 while ([result next]) {
162 [self recentTimeStamp:[result numberAtIndex:1]];
163 if ([[result stringAtIndex:2] isEqual:samplerName]) {
165 [samplesfound addObject:[result numberAtIndex:3]];
169 XCTAssertEqual([samples count], targetcount);
170 XCTAssertEqual(samplescount, total);
172 [samplesfound sortUsingSelector:@selector(compare:)];
173 NSArray* sortedInput = [samples sortedArrayUsingSelector:@selector(compare:)];
174 for (NSUInteger idx = 0; idx < [samples count]; ++idx) {
175 XCTAssertEqualWithAccuracy([samplesfound[idx] doubleValue], [sortedInput[idx] doubleValue], accuracy);
179 - (void)waitForSamplerWork:(double)interval
181 dispatch_semaphore_t sema = dispatch_semaphore_create(0);
182 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * interval), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
183 dispatch_semaphore_signal(sema);
185 dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
188 // MARK: Test administration
193 _path = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%@/", [[NSUUID UUID] UUIDString]]];
194 [[NSFileManager defaultManager] createDirectoryAtPath:_path
195 withIntermediateDirectories:YES
198 // No XCTAssert in class method
200 NSLog(@"Could not make directory at %@", _path);
202 NSDictionary *version = CFBridgingRelease(_CFCopySystemVersionDictionary());
204 build = version[(__bridge NSString *)_kCFSystemVersionBuildVersionKey];
205 product = version[(__bridge NSString *)_kCFSystemVersionProductNameKey];
207 NSLog(@"could not get build version/product, tests should fail");
214 self.continueAfterFailure = NO;
215 NSError* error = nil;
216 _dbpath = [_path stringByAppendingFormat:@"/test_%ld.db", (long)++_testnum];
217 NSLog(@"sqlite3 %@", _dbpath);
218 [UnitTestAnalytics setDatabasePath:_dbpath];
219 _analytics = [UnitTestAnalytics logger];
220 _db = [PQLConnection new];
222 XCTAssertTrue([_db openAtURL:[NSURL URLWithString:_dbpath] sharedCache:NO error:&error]);
223 XCTAssertNil(error, @"could open db");
224 XCTAssertNotNil(_db);
229 NSError *error = nil;
230 XCTAssertTrue([_db close:&error], @"could close db");
231 XCTAssertNil(error, @"No error from closing db");
232 [_analytics removeState];
238 [[NSFileManager defaultManager] removeItemAtPath:_path error:nil];
241 // MARK: SFAnalytics Tests
243 - (void)testDbIsEmptyAtStartup
245 [self assertNoEventsAnywhere];
248 - (void)testDontCrashWithEmptyDBPath
250 NSString* schema = @"CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY AUTOINCREMENT,data BLOB);";
251 NSString* path = [NSString stringWithFormat:@"%@/empty", _path];
253 XCTAssertNil([SFAnalyticsSQLiteStore storeWithPath:nil schema:schema]);
254 XCTAssertNil([SFAnalyticsSQLiteStore storeWithPath:@"" schema:schema]);
255 XCTAssertNil([SFAnalyticsSQLiteStore storeWithPath:path schema:nil]);
256 XCTAssertNil([SFAnalyticsSQLiteStore storeWithPath:path schema:@""]);
258 XCTAssertNil([[SFSQLite alloc] initWithPath:nil schema:schema]);
259 XCTAssertNil([[SFSQLite alloc] initWithPath:@"" schema:schema]);
260 XCTAssertNil([[SFSQLite alloc] initWithPath:path schema:nil]);
261 XCTAssertNil([[SFSQLite alloc] initWithPath:path schema:@""]);
264 - (void)testAddingEventsWithNilName
266 [_analytics logSuccessForEventNamed:nil];
267 [self assertNoEventsAnywhere];
269 [_analytics logHardFailureForEventNamed:nil withAttributes:nil];
270 [self assertNoEventsAnywhere];
272 [_analytics logSoftFailureForEventNamed:nil withAttributes:nil];
273 [self assertNoEventsAnywhere];
275 [_analytics noteEventNamed:nil];
276 [self assertNoEventsAnywhere];
279 - (void)testLogSuccess
281 [_analytics logSuccessForEventNamed:@"unittestevent"];
282 [self assertNoHardFailures];
283 [self assertNoSoftFailures];
285 PQLResultSet* result = [_db fetch:@"select success_count from success_count"];
286 XCTAssert([result next], @"a row was found after adding an event");
287 XCTAssertEqual([result intAtIndex:0], 1, @"success count is 1 after adding an event");
288 XCTAssertFalse([result next], @"only one row found in success_count after inserting a single event");
289 result = [_db fetch:@"select * from all_events"];
290 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSuccess];
293 - (void)testLogRecoverableFailure
295 [_analytics logSoftFailureForEventNamed:@"unittestevent" withAttributes:nil];
296 [self assertNoHardFailures];
298 // First check success_count has logged a soft failure
299 [self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:0 soft:1];
301 // then check soft_failures itself
302 PQLResultSet* result = [_db fetch:@"select * from soft_failures"];
303 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSoftFailure];
305 // finally check all_events
306 result = [_db fetch:@"select * from all_events"];
307 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSoftFailure];
310 - (void)testLogRecoverablyFailureWithAttributes
312 NSDictionary* attrs = @{@"attr1" : @"value1", @"attr2" : @"value2"};
313 [_analytics logSoftFailureForEventNamed:@"unittestevent" withAttributes:attrs];
314 [self assertNoHardFailures];
316 [self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:0 soft:1];
318 // then check soft_failures itself
319 PQLResultSet* result = [_db fetch:@"select * from soft_failures"];
320 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSoftFailure attributes:attrs];
322 // finally check all_events
323 result = [_db fetch:@"select * from all_events"];
324 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSoftFailure attributes:attrs];
327 - (void)testLogUnrecoverableFailure
329 [_analytics logHardFailureForEventNamed:@"unittestevent" withAttributes:nil];
330 [self assertNoSoftFailures];
332 // First check success_count has logged a hard failure
333 [self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:1 soft:0];
335 // then check hard_failures itself
336 PQLResultSet* result = [_db fetch:@"select * from hard_failures"];
337 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassHardFailure];
339 // finally check all_events
340 result = [_db fetch:@"select * from all_events"];
341 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassHardFailure];
344 - (void)testLogUnrecoverableFailureWithAttributes
346 NSDictionary* attrs = @{@"attr1" : @"value1", @"attr2" : @"value2"};
347 [_analytics logHardFailureForEventNamed:@"unittestevent" withAttributes:attrs];
348 [self assertNoSoftFailures];
350 // First check success_count has logged a hard failure
351 [self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:1 soft:0];
353 // then check hard_failures itself
354 PQLResultSet* result = [_db fetch:@"select * from hard_failures"];
355 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassHardFailure attributes:attrs];
357 // finally check all_events
358 result = [_db fetch:@"select * from all_events"];
359 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassHardFailure attributes:attrs];
362 - (void)testLogSeveralEvents
364 NSDictionary* attrs = @{@"attr1" : @"value1", @"attr2" : @"value2"};
365 int iterations = 100;
366 for (int idx = 0; idx < iterations; ++idx) {
367 [_analytics logHardFailureForEventNamed:@"unittesthardfailure" withAttributes:attrs];
368 [_analytics logSoftFailureForEventNamed:@"unittestsoftfailure" withAttributes:attrs];
369 [_analytics logSuccessForEventNamed:@"unittestsuccess"];
370 [_analytics logHardFailureForEventNamed:@"unittestcombined" withAttributes:attrs];
371 [_analytics logSoftFailureForEventNamed:@"unittestcombined" withAttributes:attrs];
372 [_analytics logSuccessForEventNamed:@"unittestcombined"];
375 [self checkSuccessCountsForEvent:@"unittesthardfailure" success:0 hard:iterations soft:0];
376 [self checkSuccessCountsForEvent:@"unittestsoftfailure" success:0 hard:0 soft:iterations];
377 [self checkSuccessCountsForEvent:@"unittestsuccess" success:iterations hard:0 soft:0];
378 [self checkSuccessCountsForEvent:@"unittestcombined" success:iterations hard:iterations soft:iterations];
381 - (void)testNoteEvent
383 [_analytics noteEventNamed:@"unittestevent"];
384 [self assertNoSoftFailures];
385 [self assertNoHardFailures];
387 // First check success_count has logged a success
388 [self checkSuccessCountsForEvent:@"unittestevent" success:1 hard:0 soft:0];
390 PQLResultSet* result = [_db fetch:@"select * from all_events"];
391 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassNote];
394 // MARK: SFAnalyticsSampler Tests
396 - (void)testSamplerSimple
398 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerSimple_%li", (long)_testnum];
400 // This block should be set immediately and fire in 1000ms. Give it a little slack in checking though
401 XCTestExpectation* exp = [self expectationWithDescription:@"waiting for sampler to fire"];
402 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
406 [self waitForExpectations:@[exp] timeout:1.2f];
407 [_analytics removeMetricSamplerForName:samplerName];
409 // The expectation is fulfilled before returning and after returning some more work needs to happen. Let it settle down.
410 [self waitForSamplerWork:0.2f];
412 [self checkSamples:@[@15.3] name:samplerName totalSamples:1 accuracy:0.01f];
415 // Test state removal mostly
416 - (void)testSamplerSimpleLoop
419 for (int idx = 0; idx < 3; ++idx) {
422 [self testSamplerSimple];
429 - (void)testSamplerDoesNotFirePrematurely
431 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerDoesNotFirePrematurely_%li", (long)_testnum];
432 __block BOOL run = NO;
433 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
438 [self waitForSamplerWork:0.5f];
439 XCTAssertFalse(run, @"sample did not fire prematurely");
440 [_analytics removeMetricSamplerForName:samplerName];
443 - (void)testSamplerRemove
445 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerRemove_%li", (long)_testnum];
446 __block BOOL run = NO;
447 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
451 XCTAssertNotNil([_analytics existingMetricSamplerForName:samplerName], @"SFAnalytics held onto the sampler we setup");
452 [_analytics removeMetricSamplerForName:samplerName];
453 XCTAssertNil([_analytics existingMetricSamplerForName:samplerName], @"SFAnalytics got rid of our sampler");
455 [self waitForSamplerWork:2.0f];
456 XCTAssertFalse(run, @"sampler did not run after removal");
459 - (void)testSamplerRepeatedSampling
461 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerRepeatedSampling_%li", (long)_testnum];
463 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
468 [self waitForSamplerWork:3.5f];
469 [_analytics removeMetricSamplerForName:samplerName];
470 XCTAssertEqual(run, 3, @"sampler ran correct number of times");
471 [self checkSamples:@[@1.5, @1.5, @1.5] name:samplerName totalSamples:3 accuracy:0.01f];
474 - (void)testSamplerDisable
476 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerDisable_%li", (long)_testnum];
478 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
483 [[_analytics existingMetricSamplerForName:samplerName] pauseSampling];
484 [self waitForSamplerWork:2.0f];
485 XCTAssertEqual(run, 0, @"sampler did not run while disabled");
487 [[_analytics existingMetricSamplerForName:samplerName] resumeSampling];
488 [self waitForSamplerWork:1.3f];
489 XCTAssertEqual(run, 1, @"sampler ran after resuming");
491 [self checkSamples:@[@44.9] name:samplerName totalSamples:1 accuracy:0.01f];
494 - (void)testSamplerWithBadData
496 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerWithBadData_%li", (long)_testnum];
499 XCTAssertNil([_analytics addMetricSamplerForName:nil withTimeInterval:3.0f block:^NSNumber *{
504 XCTAssertNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:0.0f block:^NSNumber *{
508 XCTAssertNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:2.0f block:nil]);
511 - (void)testSamplerOncePerReport
513 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerOncePerReport_%li", (long)_testnum];
515 [_analytics addMetricSamplerForName:samplerName withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSNumber *{
520 // There's no point in waiting, it could have been set to some arbitrarily long timer instead
522 notify_post(SFAnalyticsFireSamplersNotification);
523 [self waitForSamplerWork:0.5f];
524 XCTAssertEqual(run, 1, @"once-per-report sampler fired once in response to notification");
525 [self checkSamples:@[@74.1] name:samplerName totalSamples:1 accuracy:0.01f];
528 - (void)testSamplerOncePerReportEnsuresSingleSampleInDatabase
530 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerSetTimeInterval_%li", (long)_testnum];
531 [_analytics addMetricSamplerForName:samplerName withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSNumber *{
534 notify_post(SFAnalyticsFireSamplersNotification);
535 [self waitForSamplerWork:0.5f];
536 notify_post(SFAnalyticsFireSamplersNotification);
537 [self waitForSamplerWork:0.5f];
538 [self checkSamples:@[@57.6] name:samplerName totalSamples:1 accuracy:0.01f];
541 - (void)testSamplerAddSamplerTwice
543 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerDisable_%li", (long)_testnum];
544 XCTAssertNotNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:3.0f block:^NSNumber *{
546 }], @"adding first sampler works okay");
548 XCTAssertNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:3.0f block:^NSNumber *{
550 }], @"adding duplicate sampler did not work");
553 - (void)testSamplerLogBadSample
555 [_analytics logMetric:nil withName:@"testsampler"];
556 [self checkSamples:@[] name:@"testsampler" totalSamples:0 accuracy:0.01f];
558 id badobj = [NSString stringWithUTF8String:"yolo!"];
559 [_analytics logMetric:badobj withName:@"testSampler"];
560 [self checkSamples:@[] name:@"testsampler" totalSamples:0 accuracy:0.01f];
563 - (void)testSamplerSetTimeInterval
565 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerSetTimeInterval_%li", (long)_testnum];
566 __block NSUInteger run = 0;
568 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
572 [self waitForSamplerWork:1.2f];
573 [_analytics existingMetricSamplerForName:samplerName].samplingInterval = 1.5f;
574 [self waitForSamplerWork:2.5f];
575 XCTAssertEqual(run, 2ul);
576 [self checkSamples:@[@23.8, @23.8] name:samplerName totalSamples:2 accuracy:0.01f];
579 // MARK: SFAnalyticsMultiSampler Tests
581 - (void)testMultiSamplerSimple
583 NSString* samplerName = [NSString stringWithFormat:@"UnitTestMultiSamplerSimple_%li", (long)_testnum];
585 XCTestExpectation* exp = [self expectationWithDescription:@"waiting for sampler to fire"];
586 [_analytics AddMultiSamplerForName:samplerName withTimeInterval:1.0f block:^NSDictionary<NSString *,NSNumber *> *{
588 return @{@"val1" : @89.4f, @"val2" : @11.2f};
590 [self waitForExpectations:@[exp] timeout:1.3f];
591 [_analytics removeMultiSamplerForName:samplerName];
593 // The expectation is fulfilled before returning and after returning some more work needs to happen. Let it settle down.
594 [self waitForSamplerWork:0.2f];
596 [self checkSamples:@[@89.4f] name:@"val1" totalSamples:2 accuracy:0.01f];
597 [self checkSamples:@[@11.2f] name:@"val2" totalSamples:2 accuracy:0.01f];
600 - (void)testMultiSamplerOncePerReport
602 NSString* samplerName = [NSString stringWithFormat:@"UnitTestMultiSamplerOncePerReport_%li", (long)_testnum];
604 [_analytics AddMultiSamplerForName:samplerName withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{
606 return @{@"val1" : @33.8f, @"val2" : @54.6f};
609 // There's no point in waiting, it could have been set to some arbitrarily long timer instead
611 notify_post(SFAnalyticsFireSamplersNotification);
612 [self waitForSamplerWork:1.0f];
613 XCTAssertEqual(run, 1, @"once-per-report sampler fired once in response to notification");
614 [self checkSamples:@[@33.8f] name:@"val1" totalSamples:2 accuracy:0.01f];
615 [self checkSamples:@[@54.6f] name:@"val2" totalSamples:2 accuracy:0.01f];
618 - (void)testMultiSamplerSetTimeInterval
620 NSString* samplerName = [NSString stringWithFormat:@"UnitTestMultiSamplerSetTimeInterval_%li", (long)_testnum];
621 __block NSUInteger run = 0;
622 [_analytics AddMultiSamplerForName:samplerName withTimeInterval:1.0f block:^NSDictionary<NSString *,NSNumber *> *{
624 return @{@"val1" : @29.3f, @"val2" : @19.3f};
626 [self waitForSamplerWork:1.2f];
627 [_analytics existingMultiSamplerForName:samplerName].samplingInterval = 1.5f;
628 [self waitForSamplerWork:2.5f];
629 XCTAssertEqual(run, 2ul);
630 [self checkSamples:@[@29.3f, @29.3f] name:@"val1" totalSamples:4 accuracy:0.01f];
631 [self checkSamples:@[@19.3f, @19.3f] name:@"val2" totalSamples:4 accuracy:0.01f];
635 // MARK: SFAnalyticsActivityTracker Tests
637 - (void)testTrackerSimple
639 NSString* trackerName = @"UnitTestTrackerSimple";
641 [_analytics logSystemMetricsForActivityNamed:trackerName withAction:^{
642 [NSThread sleepForTimeInterval:0.3f];
647 [self checkSamples:@[@(0.3f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.05f * NSEC_PER_SEC)];
650 - (void)testTrackerMultipleBlocks
652 NSString* trackerName = @"UnitTestTrackerMultipleBlocks";
654 SFAnalyticsActivityTracker* tracker = [_analytics logSystemMetricsForActivityNamed:trackerName withAction:^{
655 [NSThread sleepForTimeInterval:0.3f];
658 [tracker performAction:^{
659 [NSThread sleepForTimeInterval:0.2f];
663 [self checkSamples:@[@(0.5f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.1f * NSEC_PER_SEC)];
666 - (void)testTrackerAction
668 NSString* trackerName = @"UnitTestTrackerOneBlock";
670 SFAnalyticsActivityTracker* tracker = [_analytics logSystemMetricsForActivityNamed:trackerName withAction:NULL];
671 [tracker performAction:^{
672 [NSThread sleepForTimeInterval:0.2f];
676 [self checkSamples:@[@(0.2f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.1f * NSEC_PER_SEC)];
679 - (void)testTrackerStartStop {
681 NSString* trackerName = @"UnitTestTrackerStartStop";
683 SFAnalyticsActivityTracker* tracker = [_analytics logSystemMetricsForActivityNamed:trackerName withAction:NULL];
685 [NSThread sleepForTimeInterval:0.2f];
689 [self checkSamples:@[@(0.2f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.1f * NSEC_PER_SEC)];
692 - (void)testTrackerCancel
694 NSString* trackerName = @"UnitTestTrackerCancel";
696 [[_analytics logSystemMetricsForActivityNamed:trackerName withAction:^{
697 [NSThread sleepForTimeInterval:0.3f];
701 [self assertNoEventsAnywhere];
706 - (void)testTrackerBadData
708 // Inspect database to find out it's empty
709 [_analytics logMetric:nil withName:@"fake"];
710 [_analytics logMetric:@3.0 withName:nil];
712 // get object back so inspect that, too
713 XCTAssertNil([_analytics logSystemMetricsForActivityNamed:nil withAction:^{return;}]);
715 [self assertNoEventsAnywhere];
718 // MARK: Miscellaneous
720 - (void)testInstantiateBaseClass
722 XCTAssertNil([SFAnalytics logger]);
725 - (void)testFuzzyDaysSinceDate
727 NSInteger secondsPerDay = 60 * 60 * 24;
728 XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate date]], 0);
729 XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate dateWithTimeIntervalSinceNow:secondsPerDay * -3]], 1);
730 XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate dateWithTimeIntervalSinceNow:secondsPerDay * -18]], 7);
731 XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate dateWithTimeIntervalSinceNow:secondsPerDay * -77]], 30);
732 XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate dateWithTimeIntervalSinceNow:secondsPerDay * -370]], 365);
733 XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate distantPast]], 1000);
734 XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:nil], -1);
737 - (void)testRingBuffer {
738 [self assertNoEventsAnywhere];
739 for (unsigned idx = 0; idx < (SFAnalyticsMaxEventsToReport + 50); ++idx) {
740 [_analytics logHardFailureForEventNamed:@"ringbufferevent" withAttributes:nil];
743 PQLResultSet* result = [_db fetch:@"select count(*) from hard_failures"];
744 XCTAssertTrue([result next], @"Got a count from hard_failures");
745 XCTAssertLessThanOrEqual([result unsignedIntAtIndex:0], SFAnalyticsMaxEventsToReport, @"Ring buffer contains a sane number of events");
747 // all_events has a much larger buffer so it should handle the extra events okay
748 result = [_db fetch:@"select count(*) from all_events"];
749 XCTAssertTrue([result next], @"Got a count from all_events");
750 XCTAssertLessThanOrEqual([result unsignedIntAtIndex:0], SFAnalyticsMaxEventsToReport + 50);
753 - (void)testRaceToCreateLoggers
755 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
756 for (NSInteger idx = 0; idx < 500; ++idx) {
757 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
758 UnitTestAnalytics* logger = [UnitTestAnalytics logger];
759 [logger logSuccessForEventNamed:@"testevent"];
760 dispatch_semaphore_signal(semaphore);
764 for (NSInteger idx = 0; idx < 500; ++idx) {
765 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
769 - (void)testDateProperty
771 NSString* propertyKey = @"testDataPropertyKey";
772 XCTAssertNil([_analytics datePropertyForKey:propertyKey]);
773 NSDate* test = [NSDate date];
774 [_analytics setDateProperty:test forKey:propertyKey];
775 NSDate* retrieved = [_analytics datePropertyForKey:propertyKey];
776 XCTAssert(retrieved);
777 // Storing in SQLite as string loses subsecond resolution, so we need some slack
778 XCTAssertEqualWithAccuracy([test timeIntervalSinceDate:retrieved], 0, 1);
779 [_analytics setDateProperty:nil forKey:propertyKey];
780 XCTAssertNil([_analytics datePropertyForKey:propertyKey]);