From ed02e7008d5fcdfe4df0f66acdf94268f9d774b1 Mon Sep 17 00:00:00 2001 From: Michael Haney Date: Mon, 1 Apr 2019 18:49:59 -0700 Subject: [PATCH 1/4] Implement the CCT prioritizer --- .../Classes/GDTCCTUploader.m | 104 ++++++++++++++- .../Classes/Private/GDTCCTUploader.h | 21 ++- .../Tests/Unit/GDTCCTUploaderTest.m | 55 ++++++++ .../Tests/Unit/TestServer/GDTCCTTestServer.h | 52 ++++++++ .../Tests/Unit/TestServer/GDTCCTTestServer.m | 124 ++++++++++++++++++ 5 files changed, 353 insertions(+), 3 deletions(-) create mode 100644 GoogleDataTransportCCTSupport/Tests/Unit/TestServer/GDTCCTTestServer.h create mode 100644 GoogleDataTransportCCTSupport/Tests/Unit/TestServer/GDTCCTTestServer.m diff --git a/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTUploader.m b/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTUploader.m index ee5846a1a0a..175befe572b 100644 --- a/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTUploader.m +++ b/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTUploader.m @@ -17,6 +17,20 @@ #import "GDTCCTUploader.h" #import +#import +#import +#import + +#import "GDTCCTNanopbHelpers.h" +#import "GDTCCTPrioritizer.h" +#import "cct.nanopb.h" + +@interface GDTCCTUploader () + +// Redeclared as readwrite. +@property(nullable, nonatomic, readwrite) NSURLSessionUploadTask *currentTask; + +@end @implementation GDTCCTUploader @@ -34,8 +48,94 @@ + (instancetype)sharedInstance { return sharedInstance; } -- (void)uploadPackage:(nonnull GDTUploadPackage *)package - onComplete:(nonnull GDTUploaderCompletionBlock)onComplete { +- (instancetype)init { + self = [super init]; + if (self) { + _uploaderQueue = dispatch_queue_create("com.google.GDTCCTUploader", DISPATCH_QUEUE_SERIAL); + NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; + _uploaderSession = [NSURLSession sessionWithConfiguration:config]; + } + return self; +} + +- (NSURL *)defaultServerURL { + static NSURL *defaultServerURL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // These strings should be interleaved to construct the real URL. This is just to (hopefully) + // fool github URL scanning bots. + const char *p1 = "hts/frbslgiggolai.o/0clgbth"; + const char *p2 = "tp:/ieaeogn.ogepscmvc/o/ac"; + const char defaultURL[54] = { + p1[0], p2[0], p1[1], p2[1], p1[2], p2[2], p1[3], p2[3], p1[4], p2[4], p1[5], + p2[5], p1[6], p2[6], p1[7], p2[7], p1[8], p2[8], p1[9], p2[9], p1[10], p2[10], + p1[11], p2[11], p1[12], p2[12], p1[13], p2[13], p1[14], p2[14], p1[15], p2[15], p1[16], + p2[16], p1[17], p2[17], p1[18], p2[18], p1[19], p2[19], p1[20], p2[20], p1[21], p2[21], + p1[22], p2[22], p1[23], p2[23], p1[24], p2[24], p1[25], p2[25], p1[26], '\0'}; + defaultServerURL = [NSURL URLWithString:[NSString stringWithUTF8String:defaultURL]]; + }); + return defaultServerURL; +} + +- (void)uploadPackage:(GDTUploadPackage *)package + onComplete:(GDTUploaderCompletionBlock)onComplete { + dispatch_async(_uploaderQueue, ^{ + NSAssert(!self->_currentTask, @"An upload shouldn't be initiated with another in progress."); + NSURL *serverURL = self.serverURL ? self.serverURL : [self defaultServerURL]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:serverURL]; + request.HTTPMethod = @"POST"; + + id completionHandler = ^(NSData *_Nullable data, NSURLResponse *_Nullable response, + NSError *_Nullable error) { + NSAssert(!error, @"There should be no errors uploading events: %@", error); + if (onComplete) { + GDTClock *nextUploadTime; + NSError *decodingError; + gdt_cct_LogResponse response = GDTCCTDecodeLogResponse(data, &decodingError); + if (!decodingError && response.has_next_request_wait_millis) { + nextUploadTime = [GDTClock clockSnapshotInTheFuture:response.next_request_wait_millis]; + } else { + // 15 minutes from now. + nextUploadTime = [GDTClock clockSnapshotInTheFuture:15 * 60 * 1000]; + } + pb_release(gdt_cct_LogResponse_fields, &response); + onComplete(kGDTTargetCCT, nextUploadTime, error); + } + self.currentTask = nil; + }; + NSData *requestProtoData = [self constructRequestProtoFromPackage:(GDTUploadPackage *)package]; + self.currentTask = [self.uploaderSession uploadTaskWithRequest:request + fromData:requestProtoData + completionHandler:completionHandler]; + [self.currentTask resume]; + }); +} + +#pragma mark - Private helper methods + +/** Constructs data given an upload package. + * + * @param package The upload package used to construct the request proto bytes. + * @return Proto bytes representing a gdt_cct_LogRequest object. + */ +- (nonnull NSData *)constructRequestProtoFromPackage:(GDTUploadPackage *)package { + // Segment the log events by log type. + NSMutableDictionary *> *logMappingIDToLogSet = + [[NSMutableDictionary alloc] init]; + [package.events + enumerateObjectsUsingBlock:^(GDTStoredEvent *_Nonnull event, BOOL *_Nonnull stop) { + NSMutableSet *logSet = logMappingIDToLogSet[event.mappingID]; + logSet = logSet ? logSet : [[NSMutableSet alloc] init]; + [logSet addObject:event]; + logMappingIDToLogSet[event.mappingID] = logSet; + }]; + + gdt_cct_BatchedLogRequest batchedLogRequest = + GDTCCTConstructBatchedLogRequest(logMappingIDToLogSet); + + NSData *data = GDTCCTEncodeBatchedLogRequest(&batchedLogRequest); + pb_release(gdt_cct_BatchedLogRequest_fields, &batchedLogRequest); + return data ? data : [[NSData alloc] init]; } @end diff --git a/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/Private/GDTCCTUploader.h b/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/Private/GDTCCTUploader.h index eb8ce160567..fa58da72369 100644 --- a/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/Private/GDTCCTUploader.h +++ b/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/Private/GDTCCTUploader.h @@ -18,10 +18,29 @@ #import +NS_ASSUME_NONNULL_BEGIN + /** Class capable of uploading events to the CCT backend. */ @interface GDTCCTUploader : NSObject -/** Creates/returns the single instance. */ +/** The queue on which all CCT uploading will occur. */ +@property(nonatomic, readonly) dispatch_queue_t uploaderQueue; + +/** The server URL to upload to. Look at .m for the default value. */ +@property(nonatomic) NSURL *serverURL; + +/** The URL session that will attempt upload. */ +@property(nonatomic, readonly) NSURLSession *uploaderSession; + +/** The current upload task. */ +@property(nullable, nonatomic, readonly) NSURLSessionUploadTask *currentTask; + +/** Creates and/or returns the singleton instance of this class. + * + * @return The singleton instance of this class. + */ + (instancetype)sharedInstance; @end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransportCCTSupport/Tests/Unit/GDTCCTUploaderTest.m b/GoogleDataTransportCCTSupport/Tests/Unit/GDTCCTUploaderTest.m index 2f25ef2e416..c4486ba465c 100644 --- a/GoogleDataTransportCCTSupport/Tests/Unit/GDTCCTUploaderTest.m +++ b/GoogleDataTransportCCTSupport/Tests/Unit/GDTCCTUploaderTest.m @@ -16,12 +16,67 @@ #import +#import "GDTCCTEventGenerator.h" +#import "GDTCCTNanopbHelpers.h" +#import "GDTCCTTestServer.h" #import "GDTCCTUploader.h" @interface GDTCCTUploaderTest : XCTestCase +/** An event generator for testing. */ +@property(nonatomic) GDTCCTEventGenerator *generator; + +/** The local HTTP server to use for testing. */ +@property(nonatomic) GDTCCTTestServer *testServer; + @end @implementation GDTCCTUploaderTest +- (void)setUp { + self.generator = [[GDTCCTEventGenerator alloc] init]; + self.testServer = [[GDTCCTTestServer alloc] init]; + [self.testServer registerLogBatchPath]; + [self.testServer start]; + XCTAssertTrue(self.testServer.isRunning); +} + +- (void)tearDown { + [super tearDown]; + [self.generator deleteGeneratedFilesFromDisk]; + [self.testServer stop]; +} + +- (void)testUploadGivenConditions { + NSArray *storedEventsA = [self.generator generateTheFiveConsistentStoredEvents]; + NSSet *storedEvents = [NSSet setWithArray:storedEventsA]; + + GDTUploadPackage *package = [[GDTUploadPackage alloc] init]; + package.events = storedEvents; + GDTCCTUploader *uploader = [[GDTCCTUploader alloc] init]; + uploader.serverURL = [self.testServer.serverURL URLByAppendingPathComponent:@"logBatch"]; + __weak id weakSelf = self; + XCTestExpectation *responseSentExpectation = [self expectationWithDescription:@"response sent"]; + self.testServer.responseCompletedBlock = + ^(GCDWebServerRequest *_Nonnull request, GCDWebServerResponse *_Nonnull response) { + // Redefining the self var addresses strong self capturing in the XCTAssert macros. + id self = weakSelf; + XCTAssertNotNil(self); + [responseSentExpectation fulfill]; + XCTAssertEqual(response.statusCode, 200); + XCTAssertTrue(response.hasBody); + }; + XCTestExpectation *uploadExpectation = [self expectationWithDescription:@"upload completes"]; + [uploader uploadPackage:package + onComplete:^(GDTTarget target, GDTClock *_Nonnull nextUploadAttemptUTC, + NSError *_Nullable uploadError) { + [uploadExpectation fulfill]; + XCTAssertTrue(nextUploadAttemptUTC.timeMillis > [GDTClock snapshot].timeMillis); + }]; + [self waitForExpectations:@[ responseSentExpectation, uploadExpectation ] timeout:30.0]; + dispatch_sync(uploader.uploaderQueue, ^{ + XCTAssertNil(uploader.currentTask); + }); +} + @end diff --git a/GoogleDataTransportCCTSupport/Tests/Unit/TestServer/GDTCCTTestServer.h b/GoogleDataTransportCCTSupport/Tests/Unit/TestServer/GDTCCTTestServer.h new file mode 100644 index 00000000000..1ed98294fcb --- /dev/null +++ b/GoogleDataTransportCCTSupport/Tests/Unit/TestServer/GDTCCTTestServer.h @@ -0,0 +1,52 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import "GCDWebServer.h" +#import "GCDWebServerDataResponse.h" +#import "GCDWebServerFileResponse.h" + +NS_ASSUME_NONNULL_BEGIN + +@class GCDWebServerRequest; +@class GCDWebServerResponse; + +/** This class provides a hermetic test service that runs on the test device/simulator. */ +@interface GDTCCTTestServer : NSObject + +/** The URL of the server. */ +@property(nonatomic, readonly) NSURL *serverURL; + +/** Just before responding, this block will be scheduled to run on a global queue. */ +@property(nonatomic, copy, nullable) void (^responseCompletedBlock) + (GCDWebServerRequest *request, GCDWebServerResponse *response); + +/** YES if the server is running, NO otherwise. */ +@property(nonatomic, readonly) BOOL isRunning; + +/** Registers the /log/batch path, which responds with some JSON. */ +- (void)registerLogBatchPath; + +/** Starts the server. Can be called after calling `-stop`. */ +- (void)start; + +/** Stops the server. */ +- (void)stop; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransportCCTSupport/Tests/Unit/TestServer/GDTCCTTestServer.m b/GoogleDataTransportCCTSupport/Tests/Unit/TestServer/GDTCCTTestServer.m new file mode 100644 index 00000000000..340f9ed2447 --- /dev/null +++ b/GoogleDataTransportCCTSupport/Tests/Unit/TestServer/GDTCCTTestServer.m @@ -0,0 +1,124 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GDTCCTTestServer.h" + +#import +#import + +#import "GDTCCTNanopbHelpers.h" +#import "cct.nanopb.h" + +@interface GDTCCTTestServer () + +/** The server object. */ +@property(nonatomic) GCDWebServer *server; + +// Redeclare as readwrite and mutable. +@property(nonatomic, readwrite) NSMutableDictionary *registeredTestPaths; + +@end + +@implementation GDTCCTTestServer + +- (instancetype)init { + self = [super init]; + if (self) { + [GCDWebServer setLogLevel:3]; + _server = [[GCDWebServer alloc] init]; + _registeredTestPaths = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)dealloc { + [_server stop]; +} + +- (void)start { + NSAssert(self.server.isRunning == NO, @"The server should not be already running."); + NSError *error; + [self.server + startWithOptions:@{GCDWebServerOption_Port : @0, GCDWebServerOption_BindToLocalhost : @YES} + error:&error]; + NSAssert(error == nil, @"Error when starting server: %@", error); +} + +- (void)stop { + NSAssert(self.server.isRunning, @"The server should be running before stopping."); + [self.server stop]; +} + +- (BOOL)isRunning { + return [self.server isRunning]; +} + +- (NSURL *)serverURL { + return _server.serverURL; +} + +#pragma mark - Private helper methods + +/** Constructs a nanopb LogResponse object, serializes it to NSData, and returns it. + * + * @return NSData respresenting a LogResponse with a next_request_wait_millis of 42424 milliseconds. + */ +- (NSData *)responseData { + gdt_cct_LogResponse logResponse = gdt_cct_LogResponse_init_default; + logResponse.next_request_wait_millis = 42424; + logResponse.has_next_request_wait_millis = 1; + + pb_ostream_t sizestream = PB_OSTREAM_SIZING; + // Encode 1 time to determine the size. + if (!pb_encode(&sizestream, gdt_cct_LogResponse_fields, &logResponse)) { + NSCAssert(NO, @"Error in nanopb encoding for size: %s", PB_GET_ERROR(&sizestream)); + } + + // Encode a 2nd time to actually get the bytes from it. + size_t bufferSize = sizestream.bytes_written; + CFMutableDataRef dataRef = CFDataCreateMutable(CFAllocatorGetDefault(), bufferSize); + pb_ostream_t ostream = pb_ostream_from_buffer((void *)CFDataGetBytePtr(dataRef), bufferSize); + if (!pb_encode(&sizestream, gdt_cct_LogResponse_fields, &logResponse)) { + NSCAssert(NO, @"Error in nanopb encoding for bytes: %s", PB_GET_ERROR(&ostream)); + } + CFDataSetLength(dataRef, ostream.bytes_written); + pb_release(gdt_cct_LogResponse_fields, &logResponse); + return CFBridgingRelease(dataRef); +} + +#pragma mark - HTTP Path handling methods + +- (void)registerLogBatchPath { + id processBlock = ^GCDWebServerResponse *(__kindof GCDWebServerRequest *request) { + GCDWebServerDataResponse *response = + [[GCDWebServerDataResponse alloc] initWithData:[self responseData] + contentType:@"application/text"]; + response.gzipContentEncodingEnabled = YES; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (self.responseCompletedBlock) { + self.responseCompletedBlock(request, response); + } + }); + return response; + }; + [self.server addHandlerForMethod:@"POST" + path:@"/logBatch" + requestClass:[GCDWebServerRequest class] + processBlock:processBlock]; +} + +@end From 6cf55fc17f1e368817197fcbe28c6e2457e75110 Mon Sep 17 00:00:00 2001 From: Michael Haney Date: Mon, 1 Apr 2019 18:50:25 -0700 Subject: [PATCH 2/4] Add support for high priority uploads --- .../Classes/GDTCCTPrioritizer.m | 10 ++++++---- .../Classes/Private/GDTCCTPrioritizer.h | 9 ++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTPrioritizer.m b/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTPrioritizer.m index 6b5aaff20a4..7125173ba4d 100644 --- a/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTPrioritizer.m +++ b/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTPrioritizer.m @@ -25,10 +25,6 @@ + (void)load { [[GDTRegistrar sharedInstance] registerPrioritizer:prioritizer target:kGDTTargetCCT]; } -/** Creates and returns the singleton instance of this class. - * - * @return The singleton instance of this class. - */ + (instancetype)sharedInstance { static GDTCCTPrioritizer *sharedInstance; static dispatch_once_t onceToken; @@ -67,6 +63,12 @@ - (GDTUploadPackage *)uploadPackageWithConditions:(GDTUploadConditions)condition GDTUploadPackage *package = [[GDTUploadPackage alloc] init]; dispatch_sync(_queue, ^{ NSSet *logEventsThatWillBeSent; + // A high priority event effectively flushes all events to be sent. + if ((conditions & GDTUploadConditionHighPriority) == GDTUploadConditionHighPriority) { + package.events = self.events; + return; + } + if ((conditions & GDTUploadConditionWifiData) == GDTUploadConditionWifiData) { logEventsThatWillBeSent = [self logEventsOkToSendOnWifi]; } else { diff --git a/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/Private/GDTCCTPrioritizer.h b/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/Private/GDTCCTPrioritizer.h index 92857a69939..4311cb8c5cc 100644 --- a/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/Private/GDTCCTPrioritizer.h +++ b/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/Private/GDTCCTPrioritizer.h @@ -18,6 +18,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + /** Manages the prioritization of events from GoogleDataTransport. */ @interface GDTCCTPrioritizer : NSObject @@ -30,7 +32,12 @@ /** The most recent attempted upload of daily uploaded logs. */ @property(nonatomic) GDTClock *timeOfLastDailyUpload; -/** Creates and/or returns the singleton instance of the prioritizer. */ +/** Creates and/or returns the singleton instance of this class. + * + * @return The singleton instance of this class. + */ + (instancetype)sharedInstance; +NS_ASSUME_NONNULL_END + @end From c999db0bde41dcf618276bbc488f89f68ef0ce1c Mon Sep 17 00:00:00 2001 From: Michael Haney Date: Mon, 1 Apr 2019 18:51:22 -0700 Subject: [PATCH 3/4] Add an integration test, demonstrating usage of CCT --- .../Tests/Integration/GDTCCTIntegrationTest.m | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 GoogleDataTransportCCTSupport/Tests/Integration/GDTCCTIntegrationTest.m diff --git a/GoogleDataTransportCCTSupport/Tests/Integration/GDTCCTIntegrationTest.m b/GoogleDataTransportCCTSupport/Tests/Integration/GDTCCTIntegrationTest.m new file mode 100644 index 00000000000..dba5f0d0dae --- /dev/null +++ b/GoogleDataTransportCCTSupport/Tests/Integration/GDTCCTIntegrationTest.m @@ -0,0 +1,171 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import +#import + +#import "GDTCCTPrioritizer.h" +#import "GDTCCTUploader.h" + +typedef void (^GDTCCTIntegrationTestBlock)(NSURLSessionUploadTask *_Nullable); + +@interface GDTCCTTestDataObject : NSObject + +@end + +@implementation GDTCCTTestDataObject + +- (NSData *)transportBytes { + // Return some random event data corresponding to mapping ID 1018. + NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; + NSArray *dataFiles = @[ + @"message-32347456.dat", @"message-35458880.dat", @"message-39882816.dat", + @"message-40043840.dat", @"message-40657984.dat" + ]; + NSURL *fileURL = [testBundle URLForResource:dataFiles[arc4random_uniform(5)] withExtension:nil]; + return [NSData dataWithContentsOfURL:fileURL]; +} + +@end + +@interface GDTCCTIntegrationTest : XCTestCase + +/** If YES, the network conditions were good enough to allow running integration tests. */ +@property(nonatomic) BOOL okToRunTest; + +/** If YES, allow the recursive generating of events. */ +@property(nonatomic) BOOL generateEvents; + +/** The transporter used by the test. */ +@property(nonatomic) GDTTransport *transport; + +@end + +@implementation GDTCCTIntegrationTest + +- (void)setUp { + self.generateEvents = YES; + SCNetworkReachabilityRef reachabilityRef = + SCNetworkReachabilityCreateWithName(CFAllocatorGetDefault(), "https://google.com"); + SCNetworkReachabilityFlags flags; + Boolean success = SCNetworkReachabilityGetFlags(reachabilityRef, &flags); + if (success) { + self.okToRunTest = + (flags & kSCNetworkReachabilityFlagsReachable) == kSCNetworkReachabilityFlagsReachable; + self.transport = [[GDTTransport alloc] initWithMappingID:@"1018" + transformers:nil + target:kGDTTargetCCT]; + } +} + +/** Generates an event and sends it through the transport infrastructure. */ +- (void)generateEvent { + GDTEvent *event = [self.transport eventForTransport]; + event.dataObject = [[GDTCCTTestDataObject alloc] init]; + [self.transport sendDataEvent:event]; +} + +/** Generates events recursively at random intervals between 0 and 5 seconds. */ +- (void)recursivelyGenerateEvent { + if (self.generateEvents) { + GDTEvent *event = [self.transport eventForTransport]; + event.dataObject = [[GDTCCTTestDataObject alloc] init]; + [self.transport sendDataEvent:event]; + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(arc4random_uniform(6) * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + [self recursivelyGenerateEvent]; + }); + } +} + +/** Tests sending data to CCT with a high priority event if network conditions are good. */ +- (void)testSendingDataToCCT { + if (!self.okToRunTest) { + NSLog(@"Skipping the integration test, as the network conditions weren't good enough."); + return; + } + + NSUInteger lengthOfTestToRunInSeconds = 10; + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC); + dispatch_source_set_event_handler(timer, ^{ + static int numberOfTimesCalled = 0; + numberOfTimesCalled++; + if (numberOfTimesCalled < lengthOfTestToRunInSeconds) { + [self generateEvent]; + } else { + dispatch_source_cancel(timer); + } + }); + dispatch_resume(timer); + + // Run for a bit, several seconds longer than the previous bit. + [[NSRunLoop currentRunLoop] + runUntilDate:[NSDate dateWithTimeIntervalSinceNow:lengthOfTestToRunInSeconds + 5]]; + + XCTestExpectation *taskCreatedExpectation = [self expectationWithDescription:@"task created"]; + XCTestExpectation *taskDoneExpectation = [self expectationWithDescription:@"task done"]; + + [[GDTCCTUploader sharedInstance] + addObserver:self + forKeyPath:@"currentTask" + options:NSKeyValueObservingOptionNew + context:(__bridge void *_Nullable)(^(NSURLSessionUploadTask *_Nullable task) { + if (task) { + [taskCreatedExpectation fulfill]; + } else { + [taskDoneExpectation fulfill]; + } + })]; + + // Send a high priority event to flush events. + GDTEvent *event = [self.transport eventForTransport]; + event.dataObject = [[GDTCCTTestDataObject alloc] init]; + event.qosTier = GDTEventQoSFast; + [self.transport sendDataEvent:event]; + + [self waitForExpectations:@[ taskCreatedExpectation, taskDoneExpectation ] timeout:25.0]; + + // Just run for a minute whilst generating events. + NSInteger secondsToRun = 65; + [self generateEvents]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(secondsToRun * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + self.generateEvents = NO; + }); + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:secondsToRun]]; +} + +// KVO is utilized here to know whether or not the task has completed. +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([keyPath isEqualToString:@"currentTask"]) { + NSURLSessionUploadTask *task = change[NSKeyValueChangeNewKey]; + typedef void (^GDTCCTIntegrationTestBlock)(NSURLSessionUploadTask *_Nullable); + if (context) { + GDTCCTIntegrationTestBlock block = (__bridge GDTCCTIntegrationTestBlock)context; + block([task isKindOfClass:[NSNull class]] ? nil : task); + } + } +} + +@end From 005d4f442c6e302b6a2a2365a1eb000a1b5ab7a1 Mon Sep 17 00:00:00 2001 From: Michael Haney Date: Mon, 1 Apr 2019 18:51:37 -0700 Subject: [PATCH 4/4] Update the podspec --- GoogleDataTransportCCTSupport.podspec | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/GoogleDataTransportCCTSupport.podspec b/GoogleDataTransportCCTSupport.podspec index 924e7c84221..d7be698b8a4 100644 --- a/GoogleDataTransportCCTSupport.podspec +++ b/GoogleDataTransportCCTSupport.podspec @@ -46,6 +46,13 @@ Support library to provide event prioritization and uploading for the GoogleData test_spec.requires_app_host = false test_spec.source_files = 'GoogleDataTransportCCTSupport/Tests/Unit/**/*.{h,m}' test_spec.resources = ['GoogleDataTransportCCTSupport/Tests/Data/**/*'] + test_spec.dependency 'GCDWebServer' + end + + s.test_spec 'Tests-Integration' do |test_spec| + test_spec.requires_app_host = false + test_spec.source_files = 'GoogleDataTransportCCTSupport/Tests/Integration/**/*.{h,m}' + test_spec.resources = ['GoogleDataTransportCCTSupport/Tests/Data/**/*'] end end