]> git.saurik.com Git - apple/security.git/blob - supd/Tests/SFAnalyticsTests.m
Security-59306.11.20.tar.gz
[apple/security.git] / supd / Tests / SFAnalyticsTests.m
1 /*
2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #import <XCTest/XCTest.h>
25 #import <Security/SFAnalytics.h>
26 #import "SFAnalyticsDefines.h"
27 #import "SFAnalyticsSQLiteStore.h"
28 #import "NSDate+SFAnalytics.h"
29 #import "SFSQLite.h"
30 #import <Prequelite/Prequelite.h>
31 #import <CoreFoundation/CFPriv.h>
32 #import <notify.h>
33 #import "keychain/analytics/TestResourceUsage.h"
34
35 @interface UnitTestAnalytics : SFAnalytics
36 + (NSString*)databasePath;
37 + (void)setDatabasePath:(NSString*)path;
38 @end
39
40 // MARK: SFAnalytics subclass for custom DB
41
42 @implementation UnitTestAnalytics
43 static NSString* _utapath;
44
45 + (NSString*)databasePath
46 {
47 return _utapath;
48 }
49
50 + (void)setDatabasePath:(NSString*)path
51 {
52 _utapath = path;
53 }
54
55 @end
56
57 @interface SFAnalyticsTests : XCTestCase
58 @end
59
60 @implementation SFAnalyticsTests
61 {
62 UnitTestAnalytics* _analytics;
63 NSString* _dbpath;
64 PQLConnection* _db;
65 }
66
67 static NSString* _path;
68 static NSInteger _testnum;
69 static NSString* build = NULL;
70 static NSString* product = NULL;
71
72 // MARK: Test helper methods
73
74 - (void)assertNoSuccessEvents
75 {
76 XCTAssertFalse([[_db fetch:@"select * from success_count"] next]);
77 }
78
79 - (void)assertNoHardFailures
80 {
81 XCTAssertFalse([[_db fetch:@"select * from hard_failures"] next]);
82 }
83
84 - (void)assertNoSoftFailures
85 {
86 XCTAssertFalse([[_db fetch:@"select * from soft_failures"] next]);
87 }
88
89 - (void)assertNoNotes
90 {
91 XCTAssertFalse([[_db fetch:@"select * from notes"] next]);
92 }
93
94 - (void)assertNoSamples
95 {
96 XCTAssertFalse([[_db fetch:@"select * from samples"] next]);
97 }
98
99 - (void)assertNoEventsAnywhere
100 {
101 [self assertNoNotes];
102 [self assertNoSuccessEvents];
103 [self assertNoHardFailures];
104 [self assertNoSoftFailures];
105 [self assertNoSamples];
106 }
107
108 - (void)recentTimeStamp:(NSTimeInterval)timestamp timestampBucket:(SFAnalyticsTimestampBucket)bucket
109 {
110 NSTimeInterval roundedTimestamp = [[NSDate date] timeIntervalSince1970WithBucket:bucket];
111 XCTAssertEqualWithAccuracy(roundedTimestamp, timestamp, 5, @"Timestamp is pretty recent (%f ~? %f)", timestamp, roundedTimestamp);
112 }
113
114 - (void)properEventLogged:(PQLResultSet*)result eventType:(NSString*)eventType class:(SFAnalyticsEventClass)class attributes:(NSDictionary*)attrs
115 {
116 [self _properEventLogged:result eventType:eventType class:class];
117
118 NSDictionary* rowdata = [NSPropertyListSerialization propertyListWithData:[result dataAtIndex:2] options:NSPropertyListImmutable format:nil error:nil];
119 for (NSString* key in [attrs allKeys]) {
120 XCTAssert([attrs[key] isEqualToString:rowdata[key]], @"Attribute \"%@\" value \"%@\" matches expected \"%@\"", key, rowdata[key], attrs[key]);
121 }
122 XCTAssertFalse([result next], @"only one row returned");
123 }
124
125 - (void)properEventLogged:(PQLResultSet*)result eventType:(NSString*)eventType class:(SFAnalyticsEventClass)class
126 {
127 [self _properEventLogged:result eventType:eventType class:class];
128 XCTAssertFalse([result next], @"only one row returned");
129 }
130
131 - (void)properEventLogged:(PQLResultSet*)result eventType:(NSString*)eventType class:(SFAnalyticsEventClass)class timestampBucket:(SFAnalyticsTimestampBucket)bucket
132 {
133 [self _properEventLogged:result eventType:eventType class:class timestampBucket:bucket];
134 XCTAssertFalse([result next], @"only one row returned");
135 }
136
137 - (void)_properEventLogged:(PQLResultSet*)result eventType:(NSString*)eventType class:(SFAnalyticsEventClass)class
138 {
139 [self _properEventLogged:result eventType:eventType class:class timestampBucket:SFAnalyticsTimestampBucketSecond];
140 }
141
142 - (void)_properEventLogged:(PQLResultSet*)result eventType:(NSString*)eventType class:(SFAnalyticsEventClass)class timestampBucket:(SFAnalyticsTimestampBucket)bucket
143 {
144 XCTAssert([result next], @"result found after adding an event");
145 NSError* error = nil;
146 [result doubleAtIndex:1];
147 NSDictionary* rowdata = [NSPropertyListSerialization propertyListWithData:[result dataAtIndex:2] options:NSPropertyListImmutable format:nil error:&error];
148 XCTAssertNotNil(rowdata, @"able to deserialize db data, %@", error);
149 NSNumber *timestamp = rowdata[SFAnalyticsEventTime];
150 XCTAssert([timestamp isKindOfClass:[NSNumber class]], @"Timestamp is an NSNumber");
151 [self recentTimeStamp:([timestamp doubleValue] / 1000) timestampBucket:bucket]; // We need to convert to seconds, as its stored in milliseconds
152 XCTAssertTrue([rowdata[SFAnalyticsEventType] isKindOfClass:[NSString class]] && [rowdata[SFAnalyticsEventType] isEqualToString:eventType], @"found eventType \"%@\" in db", eventType);
153 XCTAssertTrue([rowdata[SFAnalyticsEventClassKey] isKindOfClass:[NSNumber class]] && [rowdata[SFAnalyticsEventClassKey] intValue] == class, @"eventClass is %ld", (long)class);
154 XCTAssertTrue([rowdata[@"build"] isEqualToString:build], @"event row includes build");
155 XCTAssertTrue([rowdata[@"product"] isEqualToString:product], @"event row includes product");
156 XCTAssertTrue(rowdata[@"internal"], @"event row includes internal");
157 }
158
159 - (void)checkSuccessCountsForEvent:(NSString*)eventType success:(int)success hard:(int)hard soft:(int)soft
160 {
161 PQLResultSet* result = [_db fetch:@"select * from success_count where event_type = %@", eventType];
162 XCTAssert([result next]);
163 XCTAssertTrue([[result stringAtIndex:0] isEqualToString:eventType], @"event name \"%@\", expected \"%@\"", [result stringAtIndex:0], eventType);
164 XCTAssertEqual([result intAtIndex:1], success, @"correct count of successes: %d / %d", [result intAtIndex:1], success);
165 XCTAssertEqual([result intAtIndex:2], hard, @"correct count of successes: %d / %d", [result intAtIndex:2], hard);
166 XCTAssertEqual([result intAtIndex:3], soft, @"correct count of successes: %d / %d", [result intAtIndex:3], soft);
167 XCTAssertFalse([result next], @"no more than one row returned");
168 }
169
170 - (void)checkSamples:(NSArray*)samples name:(NSString*)samplerName totalSamples:(NSUInteger)total accuracy:(double)accuracy
171 {
172 NSUInteger samplescount = 0, targetcount = 0;
173 NSMutableArray* samplesfound = [NSMutableArray array];
174 PQLResultSet* result = [_db fetch:@"select * from samples"];
175 while ([result next]) {
176 ++samplescount;
177 [self recentTimeStamp:[[result numberAtIndex:1] doubleValue] timestampBucket:SFAnalyticsTimestampBucketSecond];
178 if ([[result stringAtIndex:2] isEqual:samplerName]) {
179 ++targetcount;
180 [samplesfound addObject:[result numberAtIndex:3]];
181 }
182 }
183
184 XCTAssertEqual([samples count], targetcount);
185 XCTAssertEqual(samplescount, total);
186
187 [samplesfound sortUsingSelector:@selector(compare:)];
188 NSArray* sortedInput = [samples sortedArrayUsingSelector:@selector(compare:)];
189 for (NSUInteger idx = 0; idx < [samples count]; ++idx) {
190 XCTAssertEqualWithAccuracy([samplesfound[idx] doubleValue], [sortedInput[idx] doubleValue], accuracy);
191 }
192 }
193
194 - (void)waitForSamplerWork:(double)interval
195 {
196 dispatch_semaphore_t sema = dispatch_semaphore_create(0);
197 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * interval), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
198 dispatch_semaphore_signal(sema);
199 });
200 dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
201 }
202
203 // MARK: Test administration
204
205 + (void)setUp
206 {
207 NSError* error;
208 _path = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%@/", [[NSUUID UUID] UUIDString]]];
209 [[NSFileManager defaultManager] createDirectoryAtPath:_path
210 withIntermediateDirectories:YES
211 attributes:nil
212 error:&error];
213 // No XCTAssert in class method
214 if (error) {
215 NSLog(@"Could not make directory at %@", _path);
216 }
217 NSDictionary *version = CFBridgingRelease(_CFCopySystemVersionDictionary());
218 if (version) {
219 build = version[(__bridge NSString *)_kCFSystemVersionBuildVersionKey];
220 product = version[(__bridge NSString *)_kCFSystemVersionProductNameKey];
221 } else {
222 NSLog(@"could not get build version/product, tests should fail");
223 }
224
225 [TestResourceUsage monitorTestResourceUsage];
226 }
227
228 - (void)setUp
229 {
230 [super setUp];
231 self.continueAfterFailure = NO;
232 NSError* error = nil;
233 _dbpath = [_path stringByAppendingFormat:@"/test_%ld.db", (long)++_testnum];
234 NSLog(@"sqlite3 %@", _dbpath);
235 [UnitTestAnalytics setDatabasePath:_dbpath];
236 _analytics = [UnitTestAnalytics logger];
237 _db = [PQLConnection new];
238
239 XCTAssertTrue([_db openAtURL:[NSURL URLWithString:_dbpath] sharedCache:NO error:&error]);
240 XCTAssertNil(error, @"could open db");
241 XCTAssertNotNil(_db);
242 }
243
244 - (void)tearDown
245 {
246 NSError *error = nil;
247 XCTAssertTrue([_db close:&error], @"could close db");
248 XCTAssertNil(error, @"No error from closing db");
249 [_analytics removeState];
250 [super tearDown];
251 }
252
253 + (void)tearDown
254 {
255 [[NSFileManager defaultManager] removeItemAtPath:_path error:nil];
256 }
257
258 // MARK: SFAnalytics Tests
259
260 - (void)testDbIsEmptyAtStartup
261 {
262 [self assertNoEventsAnywhere];
263 }
264
265 - (void)testDontCrashWithEmptyDBPath
266 {
267 NSString* schema = @"CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY AUTOINCREMENT,data BLOB);";
268 NSString* path = [NSString stringWithFormat:@"%@/empty", _path];
269
270 XCTAssertNil([SFAnalyticsSQLiteStore storeWithPath:nil schema:schema]);
271 XCTAssertNil([SFAnalyticsSQLiteStore storeWithPath:@"" schema:schema]);
272 XCTAssertNil([SFAnalyticsSQLiteStore storeWithPath:path schema:nil]);
273 XCTAssertNil([SFAnalyticsSQLiteStore storeWithPath:path schema:@""]);
274
275 XCTAssertNil([[SFSQLite alloc] initWithPath:nil schema:schema]);
276 XCTAssertNil([[SFSQLite alloc] initWithPath:@"" schema:schema]);
277 XCTAssertNil([[SFSQLite alloc] initWithPath:path schema:nil]);
278 XCTAssertNil([[SFSQLite alloc] initWithPath:path schema:@""]);
279 }
280
281 - (void)testAddingEventsWithNilName
282 {
283 #pragma clang diagnostic push
284 #pragma clang diagnostic ignored "-Wnonnull"
285 [_analytics logSuccessForEventNamed:nil];
286 [self assertNoEventsAnywhere];
287
288 [_analytics logHardFailureForEventNamed:nil withAttributes:nil];
289 [self assertNoEventsAnywhere];
290
291 [_analytics logSoftFailureForEventNamed:nil withAttributes:nil];
292 [self assertNoEventsAnywhere];
293
294 [_analytics noteEventNamed:nil];
295 [self assertNoEventsAnywhere];
296 #pragma clang diagnostic pop
297 }
298
299 - (void)testLogSuccess
300 {
301 [_analytics logSuccessForEventNamed:@"unittestevent"];
302 [self assertNoHardFailures];
303 [self assertNoSoftFailures];
304
305 PQLResultSet* result = [_db fetch:@"select success_count from success_count"];
306 XCTAssert([result next], @"a row was found after adding an event");
307 XCTAssertEqual([result intAtIndex:0], 1, @"success count is 1 after adding an event");
308 XCTAssertFalse([result next], @"only one row found in success_count after inserting a single event");
309 [self assertNoNotes];
310 [self assertNoHardFailures];
311 [self assertNoSoftFailures];
312 }
313
314 - (void)testLogWithRoundedTimestamp
315 {
316 SFAnalyticsTimestampBucket bucket = SFAnalyticsTimestampBucketMinute;
317
318 [_analytics logSoftFailureForEventNamed:@"unittestevent" withAttributes:nil timestampBucket:bucket];
319 [self assertNoHardFailures];
320
321 // First check success_count has logged a soft failure
322 [self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:0 soft:1];
323
324 // then check soft_failures itself
325 PQLResultSet* result = [_db fetch:@"select * from soft_failures"];
326 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSoftFailure timestampBucket:bucket];
327 [self assertNoNotes];
328 [self assertNoHardFailures];
329 }
330
331 - (void)testLogRecoverableFailure
332 {
333 [_analytics logSoftFailureForEventNamed:@"unittestevent" withAttributes:nil];
334 [self assertNoHardFailures];
335
336 // First check success_count has logged a soft failure
337 [self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:0 soft:1];
338
339 // then check soft_failures itself
340 PQLResultSet* result = [_db fetch:@"select * from soft_failures"];
341 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSoftFailure];
342 [self assertNoNotes];
343 [self assertNoHardFailures];
344 }
345
346 - (void)testLogRecoverablyFailureWithAttributes
347 {
348 NSDictionary* attrs = @{@"attr1" : @"value1", @"attr2" : @"value2"};
349 [_analytics logSoftFailureForEventNamed:@"unittestevent" withAttributes:attrs];
350 [self assertNoHardFailures];
351
352 [self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:0 soft:1];
353
354 // then check soft_failures itself
355 PQLResultSet* result = [_db fetch:@"select * from soft_failures"];
356 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSoftFailure attributes:attrs];
357 [self assertNoNotes];
358 [self assertNoHardFailures];
359 }
360
361 - (void)testLogUnrecoverableFailure
362 {
363 [_analytics logHardFailureForEventNamed:@"unittestevent" withAttributes:nil];
364 [self assertNoSoftFailures];
365
366 // First check success_count has logged a hard failure
367 [self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:1 soft:0];
368
369 // then check hard_failures itself
370 PQLResultSet* result = [_db fetch:@"select * from hard_failures"];
371 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassHardFailure];
372 [self assertNoNotes];
373 [self assertNoSoftFailures];
374 }
375
376 - (void)testLogUnrecoverableFailureWithAttributes
377 {
378 NSDictionary* attrs = @{@"attr1" : @"value1", @"attr2" : @"value2"};
379 [_analytics logHardFailureForEventNamed:@"unittestevent" withAttributes:attrs];
380 [self assertNoSoftFailures];
381
382 // First check success_count has logged a hard failure
383 [self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:1 soft:0];
384
385 // then check hard_failures itself
386 PQLResultSet* result = [_db fetch:@"select * from hard_failures"];
387 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassHardFailure attributes:attrs];
388 [self assertNoNotes];
389 [self assertNoSoftFailures];
390 }
391
392 - (void)testLogSeveralEvents
393 {
394 NSDictionary* attrs = @{@"attr1" : @"value1", @"attr2" : @"value2"};
395 int iterations = 100;
396 for (int idx = 0; idx < iterations; ++idx) {
397 [_analytics logHardFailureForEventNamed:@"unittesthardfailure" withAttributes:attrs];
398 [_analytics logSoftFailureForEventNamed:@"unittestsoftfailure" withAttributes:attrs];
399 [_analytics logSuccessForEventNamed:@"unittestsuccess"];
400 [_analytics logHardFailureForEventNamed:@"unittestcombined" withAttributes:attrs];
401 [_analytics logSoftFailureForEventNamed:@"unittestcombined" withAttributes:attrs];
402 [_analytics logSuccessForEventNamed:@"unittestcombined"];
403 }
404
405 [self checkSuccessCountsForEvent:@"unittesthardfailure" success:0 hard:iterations soft:0];
406 [self checkSuccessCountsForEvent:@"unittestsoftfailure" success:0 hard:0 soft:iterations];
407 [self checkSuccessCountsForEvent:@"unittestsuccess" success:iterations hard:0 soft:0];
408 [self checkSuccessCountsForEvent:@"unittestcombined" success:iterations hard:iterations soft:iterations];
409 }
410
411 - (void)testNoteEvent
412 {
413 [_analytics noteEventNamed:@"unittestevent"];
414 [self assertNoSoftFailures];
415 [self assertNoHardFailures];
416
417 // First check success_count has logged a success
418 [self checkSuccessCountsForEvent:@"unittestevent" success:1 hard:0 soft:0];
419
420 PQLResultSet* result = [_db fetch:@"select * from notes"];
421 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassNote];
422 }
423
424 // MARK: SFAnalyticsSampler Tests
425
426 - (void)testSamplerSimple
427 {
428 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerSimple_%li", (long)_testnum];
429
430 // This block should be set immediately and fire in 1000ms. Give it a little slack in checking though
431 XCTestExpectation* exp = [self expectationWithDescription:@"waiting for sampler to fire"];
432 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
433 [exp fulfill];
434 return @15.3;
435 }];
436 [self waitForExpectations:@[exp] timeout:1.2f];
437 [_analytics removeMetricSamplerForName:samplerName];
438
439 // The expectation is fulfilled before returning and after returning some more work needs to happen. Let it settle down.
440 [self waitForSamplerWork:0.2f];
441
442 [self checkSamples:@[@15.3] name:samplerName totalSamples:1 accuracy:0.01f];
443 }
444
445 // Test state removal mostly
446 - (void)testSamplerSimpleLoop
447 {
448 [self tearDown];
449 for (int idx = 0; idx < 3; ++idx) {
450 [self setUp];
451 @autoreleasepool {
452 [self testSamplerSimple];
453 }
454 [self tearDown];
455 }
456 }
457
458
459 - (void)testSamplerDoesNotFirePrematurely
460 {
461 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerDoesNotFirePrematurely_%li", (long)_testnum];
462 __block BOOL run = NO;
463 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
464 run = YES;
465 return @0.9;
466 }];
467
468 [self waitForSamplerWork:0.5f];
469 XCTAssertFalse(run, @"sample did not fire prematurely");
470 [_analytics removeMetricSamplerForName:samplerName];
471 }
472
473 - (void)testSamplerRemove
474 {
475 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerRemove_%li", (long)_testnum];
476 __block BOOL run = NO;
477 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
478 run = YES;
479 return @23.8;
480 }];
481 XCTAssertNotNil([_analytics existingMetricSamplerForName:samplerName], @"SFAnalytics held onto the sampler we setup");
482 [_analytics removeMetricSamplerForName:samplerName];
483 XCTAssertNil([_analytics existingMetricSamplerForName:samplerName], @"SFAnalytics got rid of our sampler");
484
485 [self waitForSamplerWork:2.0f];
486 XCTAssertFalse(run, @"sampler did not run after removal");
487 }
488
489 - (void)testSamplerRepeatedSampling
490 {
491 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerRepeatedSampling_%li", (long)_testnum];
492 __block int run = 0;
493 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
494 run += 1;
495 return @1.5;
496 }];
497
498 [self waitForSamplerWork:3.5f];
499 [_analytics removeMetricSamplerForName:samplerName];
500 XCTAssertEqual(run, 3, @"sampler ran correct number of times");
501 [self checkSamples:@[@1.5, @1.5, @1.5] name:samplerName totalSamples:3 accuracy:0.01f];
502 }
503
504 - (void)testSamplerDisable
505 {
506 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerDisable_%li", (long)_testnum];
507 __block int run = 0;
508 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
509 run += 1;
510 return @44.9;
511 }];
512
513 [[_analytics existingMetricSamplerForName:samplerName] pauseSampling];
514 [self waitForSamplerWork:2.0f];
515 XCTAssertEqual(run, 0, @"sampler did not run while disabled");
516
517 [[_analytics existingMetricSamplerForName:samplerName] resumeSampling];
518 [self waitForSamplerWork:1.3f];
519 XCTAssertEqual(run, 1, @"sampler ran after resuming");
520
521 [self checkSamples:@[@44.9] name:samplerName totalSamples:1 accuracy:0.01f];
522 }
523
524 - (void)testSamplerWithBadData
525 {
526 #pragma clang diagnostic push
527 #pragma clang diagnostic ignored "-Wnonnull"
528 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerWithBadData_%li", (long)_testnum];
529
530 // bad name
531 XCTAssertNil([_analytics addMetricSamplerForName:nil withTimeInterval:3.0f block:^NSNumber *{
532 return @0.0;
533 }]);
534
535 // bad interval
536 XCTAssertNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:0.0f block:^NSNumber *{
537 return @0.0;
538 }]);
539
540 XCTAssertNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:2.0f block:nil]);
541 #pragma clang diagnostic pop
542 }
543
544 - (void)testSamplerOncePerReport
545 {
546 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerOncePerReport_%li", (long)_testnum];
547 __block int run = 0;
548 [_analytics addMetricSamplerForName:samplerName withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSNumber *{
549 run += 1;
550 return @74.1;
551 }];
552
553 // There's no point in waiting, it could have been set to some arbitrarily long timer instead
554
555 notify_post(SFAnalyticsFireSamplersNotification);
556 [self waitForSamplerWork:0.5f];
557 XCTAssertEqual(run, 1, @"once-per-report sampler fired once in response to notification");
558 [self checkSamples:@[@74.1] name:samplerName totalSamples:1 accuracy:0.01f];
559 }
560
561 - (void)testSamplerOncePerReportEnsuresSingleSampleInDatabase
562 {
563 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerSetTimeInterval_%li", (long)_testnum];
564 [_analytics addMetricSamplerForName:samplerName withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSNumber *{
565 return @57.6;
566 }];
567 notify_post(SFAnalyticsFireSamplersNotification);
568 [self waitForSamplerWork:0.5f];
569 notify_post(SFAnalyticsFireSamplersNotification);
570 [self waitForSamplerWork:0.5f];
571 [self checkSamples:@[@57.6] name:samplerName totalSamples:1 accuracy:0.01f];
572 }
573
574 - (void)testSamplerAddSamplerTwice
575 {
576 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerDisable_%li", (long)_testnum];
577 XCTAssertNotNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:3.0f block:^NSNumber *{
578 return @7.7;
579 }], @"adding first sampler works okay");
580
581 XCTAssertNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:3.0f block:^NSNumber *{
582 return @7.8;
583 }], @"adding duplicate sampler did not work");
584 }
585
586 - (void)testSamplerLogBadSample
587 {
588 #pragma clang diagnostic push
589 #pragma clang diagnostic ignored "-Wnonnull"
590 [_analytics logMetric:nil withName:@"testsampler"];
591 [self checkSamples:@[] name:@"testsampler" totalSamples:0 accuracy:0.01f];
592 #pragma clang diagnostic pop
593
594 id badobj = [NSString stringWithUTF8String:"yolo!"];
595 [_analytics logMetric:badobj withName:@"testSampler"];
596 [self checkSamples:@[] name:@"testsampler" totalSamples:0 accuracy:0.01f];
597 }
598
599 - (void)testSamplerSetTimeInterval
600 {
601 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerSetTimeInterval_%li", (long)_testnum];
602 __block NSUInteger run = 0;
603
604 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
605 ++run;
606 return @23.8;
607 }];
608 [self waitForSamplerWork:1.2f];
609 [_analytics existingMetricSamplerForName:samplerName].samplingInterval = 1.5f;
610 [self waitForSamplerWork:2.5f];
611 XCTAssertEqual(run, 2ul);
612 [self checkSamples:@[@23.8, @23.8] name:samplerName totalSamples:2 accuracy:0.01f];
613 }
614
615 // MARK: SFAnalyticsMultiSampler Tests
616
617 - (void)testMultiSamplerSimple
618 {
619 NSString* samplerName = [NSString stringWithFormat:@"UnitTestMultiSamplerSimple_%li", (long)_testnum];
620
621 XCTestExpectation* exp = [self expectationWithDescription:@"waiting for sampler to fire"];
622 [_analytics AddMultiSamplerForName:samplerName withTimeInterval:1.0f block:^NSDictionary<NSString *,NSNumber *> *{
623 [exp fulfill];
624 return @{@"val1" : @89.4f, @"val2" : @11.2f};
625 }];
626 [self waitForExpectations:@[exp] timeout:1.3f];
627 [_analytics removeMultiSamplerForName:samplerName];
628
629 // The expectation is fulfilled before returning and after returning some more work needs to happen. Let it settle down.
630 [self waitForSamplerWork:0.2f];
631
632 [self checkSamples:@[@89.4f] name:@"val1" totalSamples:2 accuracy:0.01f];
633 [self checkSamples:@[@11.2f] name:@"val2" totalSamples:2 accuracy:0.01f];
634 }
635
636 - (void)testMultiSamplerOncePerReport
637 {
638 NSString* samplerName = [NSString stringWithFormat:@"UnitTestMultiSamplerOncePerReport_%li", (long)_testnum];
639 __block int run = 0;
640 [_analytics AddMultiSamplerForName:samplerName withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{
641 run += 1;
642 return @{@"val1" : @33.8f, @"val2" : @54.6f};
643 }];
644
645 // There's no point in waiting, it could have been set to some arbitrarily long timer instead
646
647 notify_post(SFAnalyticsFireSamplersNotification);
648 [self waitForSamplerWork:1.0f];
649 XCTAssertEqual(run, 1, @"once-per-report sampler fired once in response to notification");
650 [self checkSamples:@[@33.8f] name:@"val1" totalSamples:2 accuracy:0.01f];
651 [self checkSamples:@[@54.6f] name:@"val2" totalSamples:2 accuracy:0.01f];
652 }
653
654 - (void)testMultiSamplerSetTimeInterval
655 {
656 NSString* samplerName = [NSString stringWithFormat:@"UnitTestMultiSamplerSetTimeInterval_%li", (long)_testnum];
657 __block NSUInteger run = 0;
658 [_analytics AddMultiSamplerForName:samplerName withTimeInterval:1.0f block:^NSDictionary<NSString *,NSNumber *> *{
659 ++run;
660 return @{@"val1" : @29.3f, @"val2" : @19.3f};
661 }];
662 [self waitForSamplerWork:1.2f];
663 [_analytics existingMultiSamplerForName:samplerName].samplingInterval = 1.5f;
664 [self waitForSamplerWork:2.5f];
665 XCTAssertEqual(run, 2ul);
666 [self checkSamples:@[@29.3f, @29.3f] name:@"val1" totalSamples:4 accuracy:0.01f];
667 [self checkSamples:@[@19.3f, @19.3f] name:@"val2" totalSamples:4 accuracy:0.01f];
668 }
669
670
671 // MARK: SFAnalyticsActivityTracker Tests
672
673 - (void)testTrackerSimple
674 {
675 NSString* trackerName = @"UnitTestTrackerSimple";
676 @autoreleasepool {
677 [_analytics logSystemMetricsForActivityNamed:trackerName withAction:^{
678 [NSThread sleepForTimeInterval:0.3f];
679 }];
680 }
681
682
683 [self checkSamples:@[@(0.3f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.05f * NSEC_PER_SEC)];
684 }
685
686 - (void)testTrackerMultipleBlocks
687 {
688 NSString* trackerName = @"UnitTestTrackerMultipleBlocks";
689 @autoreleasepool {
690 SFAnalyticsActivityTracker* tracker = [_analytics logSystemMetricsForActivityNamed:trackerName withAction:^{
691 [NSThread sleepForTimeInterval:0.3f];
692 }];
693
694 [tracker performAction:^{
695 [NSThread sleepForTimeInterval:0.2f];
696 }];
697 }
698
699 [self checkSamples:@[@(0.5f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.1f * NSEC_PER_SEC)];
700 }
701
702 - (void)testTrackerAction
703 {
704 NSString* trackerName = @"UnitTestTrackerOneBlock";
705 @autoreleasepool {
706 SFAnalyticsActivityTracker* tracker = [_analytics logSystemMetricsForActivityNamed:trackerName withAction:NULL];
707 [tracker performAction:^{
708 [NSThread sleepForTimeInterval:0.2f];
709 }];
710 }
711
712 [self checkSamples:@[@(0.2f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.1f * NSEC_PER_SEC)];
713 }
714
715 - (void)testTrackerStartStop {
716
717 NSString* trackerName = @"UnitTestTrackerStartStop";
718 @autoreleasepool {
719 SFAnalyticsActivityTracker* tracker = [_analytics logSystemMetricsForActivityNamed:trackerName withAction:NULL];
720 [tracker start];
721 [NSThread sleepForTimeInterval:0.2f];
722 [tracker stop];
723 }
724
725 [self checkSamples:@[@(0.2f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.1f * NSEC_PER_SEC)];
726 }
727
728 - (void)testTrackerCancel
729 {
730 NSString* trackerName = @"UnitTestTrackerCancel";
731 @autoreleasepool {
732 [[_analytics logSystemMetricsForActivityNamed:trackerName withAction:^{
733 [NSThread sleepForTimeInterval:0.3f];
734 }] cancel];
735 }
736
737 [self assertNoEventsAnywhere];
738 }
739
740
741
742 - (void)testTrackerBadData
743 {
744 #pragma clang diagnostic push
745 #pragma clang diagnostic ignored "-Wnonnull"
746 // Inspect database to find out it's empty
747 [_analytics logMetric:nil withName:@"fake"];
748 [_analytics logMetric:@3.0 withName:nil];
749
750 // get object back so inspect that, too
751 XCTAssertNil([_analytics logSystemMetricsForActivityNamed:nil withAction:^{return;}]);
752
753 [self assertNoEventsAnywhere];
754 #pragma clang diagnostic pop
755 }
756
757 // MARK: Miscellaneous
758
759 - (void)testInstantiateBaseClass
760 {
761 XCTAssertNil([SFAnalytics logger]);
762 }
763
764 - (void)testFuzzyDaysSinceDate
765 {
766 NSInteger secondsPerDay = 60 * 60 * 24;
767 XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate date]], 0);
768 XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate dateWithTimeIntervalSinceNow:secondsPerDay * -3]], 1);
769 XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate dateWithTimeIntervalSinceNow:secondsPerDay * -18]], 7);
770 XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate dateWithTimeIntervalSinceNow:secondsPerDay * -77]], 30);
771 XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate dateWithTimeIntervalSinceNow:secondsPerDay * -370]], 365);
772 XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:[NSDate distantPast]], 1000);
773 #pragma clang diagnostic push
774 #pragma clang diagnostic ignored "-Wnonnull"
775 XCTAssertEqual([SFAnalytics fuzzyDaysSinceDate:nil], -1);
776 #pragma clang diagnostic pop
777 }
778
779 - (void)testRingBuffer {
780 [self assertNoEventsAnywhere];
781 for (unsigned idx = 0; idx < (SFAnalyticsMaxEventsToReport + 50); ++idx) {
782 [_analytics logHardFailureForEventNamed:@"ringbufferevent" withAttributes:nil];
783 }
784
785 PQLResultSet* result = [_db fetch:@"select count(*) from hard_failures"];
786 XCTAssertTrue([result next], @"Got a count from hard_failures");
787 XCTAssertLessThanOrEqual([result unsignedIntAtIndex:0], SFAnalyticsMaxEventsToReport, @"Ring buffer contains a sane number of events");
788 }
789
790 - (void)testRaceToCreateLoggers
791 {
792 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
793 for (NSInteger idx = 0; idx < 500; ++idx) {
794 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
795 UnitTestAnalytics* logger = [UnitTestAnalytics logger];
796 [logger logSuccessForEventNamed:@"testevent"];
797 dispatch_semaphore_signal(semaphore);
798 });
799 }
800
801 for (NSInteger idx = 0; idx < 500; ++idx) {
802 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
803 }
804 }
805
806 - (void)testDateProperty
807 {
808 NSString* propertyKey = @"testDataPropertyKey";
809 XCTAssertNil([_analytics datePropertyForKey:propertyKey]);
810 NSDate* test = [NSDate date];
811 [_analytics setDateProperty:test forKey:propertyKey];
812 NSDate* retrieved = [_analytics datePropertyForKey:propertyKey];
813 XCTAssertNotNil(retrieved);
814 // Storing in SQLite as string loses subsecond resolution, so we need some slack
815 XCTAssertEqualWithAccuracy([test timeIntervalSinceDate:retrieved], 0, 1);
816
817 [_analytics setDateProperty:nil forKey:propertyKey];
818 XCTAssertNil([_analytics datePropertyForKey:propertyKey]);
819 }
820
821 @end