]> git.saurik.com Git - apple/security.git/blob - supd/Tests/SFAnalyticsTests.m
Security-58286.200.222.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 "SFSQLite.h"
29 #import <Prequelite/Prequelite.h>
30 #import <CoreFoundation/CFPriv.h>
31 #import <notify.h>
32
33 @interface UnitTestAnalytics : SFAnalytics
34 + (NSString*)databasePath;
35 + (void)setDatabasePath:(NSString*)path;
36 @end
37
38 // MARK: SFAnalytics subclass for custom DB
39
40 @implementation UnitTestAnalytics
41 static NSString* _utapath;
42
43 + (NSString*)databasePath
44 {
45 return _utapath;
46 }
47
48 + (void)setDatabasePath:(NSString*)path
49 {
50 _utapath = path;
51 }
52
53 @end
54
55 @interface SFAnalyticsTests : XCTestCase
56 @end
57
58 @implementation SFAnalyticsTests
59 {
60 UnitTestAnalytics* _analytics;
61 NSString* _dbpath;
62 PQLConnection* _db;
63 }
64
65 static NSString* _path;
66 static NSInteger _testnum;
67 static NSString* build = NULL;
68 static NSString* product = NULL;
69
70 // MARK: Test helper methods
71
72 - (void)assertNoSuccessEvents
73 {
74 XCTAssertFalse([[_db fetch:@"select * from success_count"] next]);
75 }
76
77 - (void)assertNoHardFailures
78 {
79 XCTAssertFalse([[_db fetch:@"select * from hard_failures"] next]);
80 }
81
82 - (void)assertNoSoftFailures
83 {
84 XCTAssertFalse([[_db fetch:@"select * from soft_failures"] next]);
85 }
86
87 - (void)assertNoAllEvents
88 {
89 XCTAssertFalse([[_db fetch:@"select * from all_events"] next]);
90 }
91
92 - (void)assertNoSamples
93 {
94 XCTAssertFalse([[_db fetch:@"select * from samples"] next]);
95 }
96
97 - (void)assertNoEventsAnywhere
98 {
99 [self assertNoAllEvents];
100 [self assertNoSuccessEvents];
101 [self assertNoHardFailures];
102 [self assertNoSoftFailures];
103 [self assertNoSamples];
104 }
105
106 - (void)recentTimeStamp:(NSNumber*)timestamp
107 {
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);
111 }
112
113 - (void)properEventLogged:(PQLResultSet*)result eventType:(NSString*)eventType class:(SFAnalyticsEventClass)class attributes:(NSDictionary*)attrs
114 {
115 [self _properEventLogged:result eventType:eventType class:class];
116
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]);
120 }
121 XCTAssertFalse([result next], @"only one row returned");
122 }
123
124 - (void)properEventLogged:(PQLResultSet*)result eventType:(NSString*)eventType class:(SFAnalyticsEventClass)class
125 {
126 [self _properEventLogged:result eventType:eventType class:class];
127 XCTAssertFalse([result next], @"only one row returned");
128 }
129
130 - (void)_properEventLogged:(PQLResultSet*)result eventType:(NSString*)eventType class:(SFAnalyticsEventClass)class
131 {
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");
142 }
143
144 - (void)checkSuccessCountsForEvent:(NSString*)eventType success:(int)success hard:(int)hard soft:(int)soft
145 {
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");
153 }
154
155 - (void)checkSamples:(NSArray*)samples name:(NSString*)samplerName totalSamples:(NSUInteger)total accuracy:(double)accuracy
156 {
157 NSUInteger samplescount = 0, targetcount = 0;
158 NSMutableArray* samplesfound = [NSMutableArray array];
159 PQLResultSet* result = [_db fetch:@"select * from samples"];
160 while ([result next]) {
161 ++samplescount;
162 [self recentTimeStamp:[result numberAtIndex:1]];
163 if ([[result stringAtIndex:2] isEqual:samplerName]) {
164 ++targetcount;
165 [samplesfound addObject:[result numberAtIndex:3]];
166 }
167 }
168
169 XCTAssertEqual([samples count], targetcount);
170 XCTAssertEqual(samplescount, total);
171
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);
176 }
177 }
178
179 - (void)waitForSamplerWork:(double)interval
180 {
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);
184 });
185 dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
186 }
187
188 // MARK: Test administration
189
190 + (void)setUp
191 {
192 NSError* error;
193 _path = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%@/", [[NSUUID UUID] UUIDString]]];
194 [[NSFileManager defaultManager] createDirectoryAtPath:_path
195 withIntermediateDirectories:YES
196 attributes:nil
197 error:&error];
198 // No XCTAssert in class method
199 if (error) {
200 NSLog(@"Could not make directory at %@", _path);
201 }
202 NSDictionary *version = CFBridgingRelease(_CFCopySystemVersionDictionary());
203 if (version) {
204 build = version[(__bridge NSString *)_kCFSystemVersionBuildVersionKey];
205 product = version[(__bridge NSString *)_kCFSystemVersionProductNameKey];
206 } else {
207 NSLog(@"could not get build version/product, tests should fail");
208 }
209 }
210
211 - (void)setUp
212 {
213 [super setUp];
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];
221
222 XCTAssertTrue([_db openAtURL:[NSURL URLWithString:_dbpath] sharedCache:NO error:&error]);
223 XCTAssertNil(error, @"could open db");
224 XCTAssertNotNil(_db);
225 }
226
227 - (void)tearDown
228 {
229 NSError *error = nil;
230 XCTAssertTrue([_db close:&error], @"could close db");
231 XCTAssertNil(error, @"No error from closing db");
232 [_analytics removeState];
233 [super tearDown];
234 }
235
236 + (void)tearDown
237 {
238 [[NSFileManager defaultManager] removeItemAtPath:_path error:nil];
239 }
240
241 // MARK: SFAnalytics Tests
242
243 - (void)testDbIsEmptyAtStartup
244 {
245 [self assertNoEventsAnywhere];
246 }
247
248 - (void)testDontCrashWithEmptyDBPath
249 {
250 NSString* schema = @"CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY AUTOINCREMENT,data BLOB);";
251 NSString* path = [NSString stringWithFormat:@"%@/empty", _path];
252
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:@""]);
257
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:@""]);
262 }
263
264 - (void)testAddingEventsWithNilName
265 {
266 [_analytics logSuccessForEventNamed:nil];
267 [self assertNoEventsAnywhere];
268
269 [_analytics logHardFailureForEventNamed:nil withAttributes:nil];
270 [self assertNoEventsAnywhere];
271
272 [_analytics logSoftFailureForEventNamed:nil withAttributes:nil];
273 [self assertNoEventsAnywhere];
274
275 [_analytics noteEventNamed:nil];
276 [self assertNoEventsAnywhere];
277 }
278
279 - (void)testLogSuccess
280 {
281 [_analytics logSuccessForEventNamed:@"unittestevent"];
282 [self assertNoHardFailures];
283 [self assertNoSoftFailures];
284
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];
291 }
292
293 - (void)testLogRecoverableFailure
294 {
295 [_analytics logSoftFailureForEventNamed:@"unittestevent" withAttributes:nil];
296 [self assertNoHardFailures];
297
298 // First check success_count has logged a soft failure
299 [self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:0 soft:1];
300
301 // then check soft_failures itself
302 PQLResultSet* result = [_db fetch:@"select * from soft_failures"];
303 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSoftFailure];
304
305 // finally check all_events
306 result = [_db fetch:@"select * from all_events"];
307 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSoftFailure];
308 }
309
310 - (void)testLogRecoverablyFailureWithAttributes
311 {
312 NSDictionary* attrs = @{@"attr1" : @"value1", @"attr2" : @"value2"};
313 [_analytics logSoftFailureForEventNamed:@"unittestevent" withAttributes:attrs];
314 [self assertNoHardFailures];
315
316 [self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:0 soft:1];
317
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];
321
322 // finally check all_events
323 result = [_db fetch:@"select * from all_events"];
324 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassSoftFailure attributes:attrs];
325 }
326
327 - (void)testLogUnrecoverableFailure
328 {
329 [_analytics logHardFailureForEventNamed:@"unittestevent" withAttributes:nil];
330 [self assertNoSoftFailures];
331
332 // First check success_count has logged a hard failure
333 [self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:1 soft:0];
334
335 // then check hard_failures itself
336 PQLResultSet* result = [_db fetch:@"select * from hard_failures"];
337 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassHardFailure];
338
339 // finally check all_events
340 result = [_db fetch:@"select * from all_events"];
341 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassHardFailure];
342 }
343
344 - (void)testLogUnrecoverableFailureWithAttributes
345 {
346 NSDictionary* attrs = @{@"attr1" : @"value1", @"attr2" : @"value2"};
347 [_analytics logHardFailureForEventNamed:@"unittestevent" withAttributes:attrs];
348 [self assertNoSoftFailures];
349
350 // First check success_count has logged a hard failure
351 [self checkSuccessCountsForEvent:@"unittestevent" success:0 hard:1 soft:0];
352
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];
356
357 // finally check all_events
358 result = [_db fetch:@"select * from all_events"];
359 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassHardFailure attributes:attrs];
360 }
361
362 - (void)testLogSeveralEvents
363 {
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"];
373 }
374
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];
379 }
380
381 - (void)testNoteEvent
382 {
383 [_analytics noteEventNamed:@"unittestevent"];
384 [self assertNoSoftFailures];
385 [self assertNoHardFailures];
386
387 // First check success_count has logged a success
388 [self checkSuccessCountsForEvent:@"unittestevent" success:1 hard:0 soft:0];
389
390 PQLResultSet* result = [_db fetch:@"select * from all_events"];
391 [self properEventLogged:result eventType:@"unittestevent" class:SFAnalyticsEventClassNote];
392 }
393
394 // MARK: SFAnalyticsSampler Tests
395
396 - (void)testSamplerSimple
397 {
398 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerSimple_%li", (long)_testnum];
399
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 *{
403 [exp fulfill];
404 return @15.3;
405 }];
406 [self waitForExpectations:@[exp] timeout:1.2f];
407 [_analytics removeMetricSamplerForName:samplerName];
408
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];
411
412 [self checkSamples:@[@15.3] name:samplerName totalSamples:1 accuracy:0.01f];
413 }
414
415 // Test state removal mostly
416 - (void)testSamplerSimpleLoop
417 {
418 [self tearDown];
419 for (int idx = 0; idx < 3; ++idx) {
420 [self setUp];
421 @autoreleasepool {
422 [self testSamplerSimple];
423 }
424 [self tearDown];
425 }
426 }
427
428
429 - (void)testSamplerDoesNotFirePrematurely
430 {
431 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerDoesNotFirePrematurely_%li", (long)_testnum];
432 __block BOOL run = NO;
433 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
434 run = YES;
435 return @0.9;
436 }];
437
438 [self waitForSamplerWork:0.5f];
439 XCTAssertFalse(run, @"sample did not fire prematurely");
440 [_analytics removeMetricSamplerForName:samplerName];
441 }
442
443 - (void)testSamplerRemove
444 {
445 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerRemove_%li", (long)_testnum];
446 __block BOOL run = NO;
447 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
448 run = YES;
449 return @23.8;
450 }];
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");
454
455 [self waitForSamplerWork:2.0f];
456 XCTAssertFalse(run, @"sampler did not run after removal");
457 }
458
459 - (void)testSamplerRepeatedSampling
460 {
461 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerRepeatedSampling_%li", (long)_testnum];
462 __block int run = 0;
463 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
464 run += 1;
465 return @1.5;
466 }];
467
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];
472 }
473
474 - (void)testSamplerDisable
475 {
476 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerDisable_%li", (long)_testnum];
477 __block int run = 0;
478 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
479 run += 1;
480 return @44.9;
481 }];
482
483 [[_analytics existingMetricSamplerForName:samplerName] pauseSampling];
484 [self waitForSamplerWork:2.0f];
485 XCTAssertEqual(run, 0, @"sampler did not run while disabled");
486
487 [[_analytics existingMetricSamplerForName:samplerName] resumeSampling];
488 [self waitForSamplerWork:1.3f];
489 XCTAssertEqual(run, 1, @"sampler ran after resuming");
490
491 [self checkSamples:@[@44.9] name:samplerName totalSamples:1 accuracy:0.01f];
492 }
493
494 - (void)testSamplerWithBadData
495 {
496 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerWithBadData_%li", (long)_testnum];
497
498 // bad name
499 XCTAssertNil([_analytics addMetricSamplerForName:nil withTimeInterval:3.0f block:^NSNumber *{
500 return @0.0;
501 }]);
502
503 // bad interval
504 XCTAssertNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:0.0f block:^NSNumber *{
505 return @0.0;
506 }]);
507
508 XCTAssertNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:2.0f block:nil]);
509 }
510
511 - (void)testSamplerOncePerReport
512 {
513 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerOncePerReport_%li", (long)_testnum];
514 __block int run = 0;
515 [_analytics addMetricSamplerForName:samplerName withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSNumber *{
516 run += 1;
517 return @74.1;
518 }];
519
520 // There's no point in waiting, it could have been set to some arbitrarily long timer instead
521
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];
526 }
527
528 - (void)testSamplerOncePerReportEnsuresSingleSampleInDatabase
529 {
530 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerSetTimeInterval_%li", (long)_testnum];
531 [_analytics addMetricSamplerForName:samplerName withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSNumber *{
532 return @57.6;
533 }];
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];
539 }
540
541 - (void)testSamplerAddSamplerTwice
542 {
543 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerDisable_%li", (long)_testnum];
544 XCTAssertNotNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:3.0f block:^NSNumber *{
545 return @7.7;
546 }], @"adding first sampler works okay");
547
548 XCTAssertNil([_analytics addMetricSamplerForName:samplerName withTimeInterval:3.0f block:^NSNumber *{
549 return @7.8;
550 }], @"adding duplicate sampler did not work");
551 }
552
553 - (void)testSamplerLogBadSample
554 {
555 [_analytics logMetric:nil withName:@"testsampler"];
556 [self checkSamples:@[] name:@"testsampler" totalSamples:0 accuracy:0.01f];
557
558 id badobj = [NSString stringWithUTF8String:"yolo!"];
559 [_analytics logMetric:badobj withName:@"testSampler"];
560 [self checkSamples:@[] name:@"testsampler" totalSamples:0 accuracy:0.01f];
561 }
562
563 - (void)testSamplerSetTimeInterval
564 {
565 NSString* samplerName = [NSString stringWithFormat:@"UnitTestSamplerSetTimeInterval_%li", (long)_testnum];
566 __block NSUInteger run = 0;
567
568 [_analytics addMetricSamplerForName:samplerName withTimeInterval:1.0f block:^NSNumber *{
569 ++run;
570 return @23.8;
571 }];
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];
577 }
578
579 // MARK: SFAnalyticsMultiSampler Tests
580
581 - (void)testMultiSamplerSimple
582 {
583 NSString* samplerName = [NSString stringWithFormat:@"UnitTestMultiSamplerSimple_%li", (long)_testnum];
584
585 XCTestExpectation* exp = [self expectationWithDescription:@"waiting for sampler to fire"];
586 [_analytics AddMultiSamplerForName:samplerName withTimeInterval:1.0f block:^NSDictionary<NSString *,NSNumber *> *{
587 [exp fulfill];
588 return @{@"val1" : @89.4f, @"val2" : @11.2f};
589 }];
590 [self waitForExpectations:@[exp] timeout:1.3f];
591 [_analytics removeMultiSamplerForName:samplerName];
592
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];
595
596 [self checkSamples:@[@89.4f] name:@"val1" totalSamples:2 accuracy:0.01f];
597 [self checkSamples:@[@11.2f] name:@"val2" totalSamples:2 accuracy:0.01f];
598 }
599
600 - (void)testMultiSamplerOncePerReport
601 {
602 NSString* samplerName = [NSString stringWithFormat:@"UnitTestMultiSamplerOncePerReport_%li", (long)_testnum];
603 __block int run = 0;
604 [_analytics AddMultiSamplerForName:samplerName withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{
605 run += 1;
606 return @{@"val1" : @33.8f, @"val2" : @54.6f};
607 }];
608
609 // There's no point in waiting, it could have been set to some arbitrarily long timer instead
610
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];
616 }
617
618 - (void)testMultiSamplerSetTimeInterval
619 {
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 *> *{
623 ++run;
624 return @{@"val1" : @29.3f, @"val2" : @19.3f};
625 }];
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];
632 }
633
634
635 // MARK: SFAnalyticsActivityTracker Tests
636
637 - (void)testTrackerSimple
638 {
639 NSString* trackerName = @"UnitTestTrackerSimple";
640 @autoreleasepool {
641 [_analytics logSystemMetricsForActivityNamed:trackerName withAction:^{
642 [NSThread sleepForTimeInterval:0.3f];
643 }];
644 }
645
646
647 [self checkSamples:@[@(0.3f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.05f * NSEC_PER_SEC)];
648 }
649
650 - (void)testTrackerMultipleBlocks
651 {
652 NSString* trackerName = @"UnitTestTrackerMultipleBlocks";
653 @autoreleasepool {
654 SFAnalyticsActivityTracker* tracker = [_analytics logSystemMetricsForActivityNamed:trackerName withAction:^{
655 [NSThread sleepForTimeInterval:0.3f];
656 }];
657
658 [tracker performAction:^{
659 [NSThread sleepForTimeInterval:0.2f];
660 }];
661 }
662
663 [self checkSamples:@[@(0.5f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.1f * NSEC_PER_SEC)];
664 }
665
666 - (void)testTrackerAction
667 {
668 NSString* trackerName = @"UnitTestTrackerOneBlock";
669 @autoreleasepool {
670 SFAnalyticsActivityTracker* tracker = [_analytics logSystemMetricsForActivityNamed:trackerName withAction:NULL];
671 [tracker performAction:^{
672 [NSThread sleepForTimeInterval:0.2f];
673 }];
674 }
675
676 [self checkSamples:@[@(0.2f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.1f * NSEC_PER_SEC)];
677 }
678
679 - (void)testTrackerStartStop {
680
681 NSString* trackerName = @"UnitTestTrackerStartStop";
682 @autoreleasepool {
683 SFAnalyticsActivityTracker* tracker = [_analytics logSystemMetricsForActivityNamed:trackerName withAction:NULL];
684 [tracker start];
685 [NSThread sleepForTimeInterval:0.2f];
686 [tracker stop];
687 }
688
689 [self checkSamples:@[@(0.2f * NSEC_PER_SEC)] name:trackerName totalSamples:1 accuracy:(0.1f * NSEC_PER_SEC)];
690 }
691
692 - (void)testTrackerCancel
693 {
694 NSString* trackerName = @"UnitTestTrackerCancel";
695 @autoreleasepool {
696 [[_analytics logSystemMetricsForActivityNamed:trackerName withAction:^{
697 [NSThread sleepForTimeInterval:0.3f];
698 }] cancel];
699 }
700
701 [self assertNoEventsAnywhere];
702 }
703
704
705
706 - (void)testTrackerBadData
707 {
708 // Inspect database to find out it's empty
709 [_analytics logMetric:nil withName:@"fake"];
710 [_analytics logMetric:@3.0 withName:nil];
711
712 // get object back so inspect that, too
713 XCTAssertNil([_analytics logSystemMetricsForActivityNamed:nil withAction:^{return;}]);
714
715 [self assertNoEventsAnywhere];
716 }
717
718 // MARK: Miscellaneous
719
720 - (void)testInstantiateBaseClass
721 {
722 XCTAssertNil([SFAnalytics logger]);
723 }
724
725 - (void)testFuzzyDaysSinceDate
726 {
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);
735 }
736
737 - (void)testRingBuffer {
738 [self assertNoEventsAnywhere];
739 for (unsigned idx = 0; idx < (SFAnalyticsMaxEventsToReport + 50); ++idx) {
740 [_analytics logHardFailureForEventNamed:@"ringbufferevent" withAttributes:nil];
741 }
742
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");
746
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);
751 }
752
753 - (void)testRaceToCreateLoggers
754 {
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);
761 });
762 }
763
764 for (NSInteger idx = 0; idx < 500; ++idx) {
765 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
766 }
767 }
768
769 - (void)testDateProperty
770 {
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]);
781 }
782
783 @end