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