Skip to content

Commit 44b7e3a

Browse files
authored
Implement an integration/E2E test of the logging pipeline (#2356)
* Move the -removeLog API to be file-private, it's unused publicly. * Remove altering of in flight log set, that's done in the onComplete block * Copy the set of logs given to upload so it's not altered while the pipeline is operating on it. * Implement an integration/E2E test of the library's pipeline Includes a dependency on GCDWebServer in the test_spec to run an HTTP server endpoint that the uploader can upload to. * Rename -protoBytes to -transportBytes * Change the integration test timing * Spelling. * Fix the scheme names in build.sh
1 parent e3616ff commit 44b7e3a

18 files changed

+609
-48
lines changed

GoogleDataLogger.podspec

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ Pod::Spec.new do |s|
33
s.version = '0.1.0'
44
s.summary = 'Google Data Logging iOS SDK.'
55

6-
76
s.description = <<-DESC
87
Shared library for iOS SDK data logging needs.
98
DESC
@@ -38,8 +37,16 @@ Shared library for iOS SDK data logging needs.
3837
common_test_sources = ['GoogleDataLogger/Tests/Common/**/*.{h,m}']
3938

4039
# Unit test specs
41-
s.test_spec do |test_spec|
40+
s.test_spec 'Tests-Unit' do |test_spec|
4241
test_spec.requires_app_host = false
4342
test_spec.source_files = ['GoogleDataLogger/Tests/Unit/**/*.{h,m}'] + common_test_sources
4443
end
44+
45+
# Integration test specs
46+
s.test_spec 'Tests-Integration' do |test_spec|
47+
test_spec.requires_app_host = false
48+
test_spec.source_files = ['GoogleDataLogger/Tests/Integration/**/*.{h,m}'] + common_test_sources
49+
test_spec.compiler_flags = '-DGDL_LOG_TRACE_ENABLED=1'
50+
test_spec.dependency 'GCDWebServer'
51+
end
4552
end

GoogleDataLogger/GoogleDataLogger/Classes/GDLLogEvent.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ - (NSUInteger)hash {
5252
}
5353

5454
- (void)setExtension:(id<GDLLogProto>)extension {
55-
// If you're looking here because of a performance issue in -protoBytes slowing the assignment
55+
// If you're looking here because of a performance issue in -transportBytes slowing the assignment
5656
// of extension, one way to address this is to add a queue to this class,
5757
// dispatch_(barrier_ if concurrent)async here, and implement the getter with a dispatch_sync.
5858
if (extension != _extension) {
5959
_extension = extension;
60-
_extensionBytes = [extension protoBytes];
60+
_extensionBytes = [extension transportBytes];
6161
}
6262
}
6363

GoogleDataLogger/GoogleDataLogger/Classes/GDLLogStorage.h

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,6 @@ NS_ASSUME_NONNULL_BEGIN
3838
*/
3939
- (void)storeLog:(GDLLogEvent *)log;
4040

41-
/** Removes the corresponding log file from disk.
42-
*
43-
* @param logHash The hash value of the original log.
44-
* @param logTarget The logTarget of the original log.
45-
*/
46-
- (void)removeLog:(NSNumber *)logHash logTarget:(NSNumber *)logTarget;
47-
4841
/** Removes a set of log fields specified by their filenames.
4942
*
5043
* @param logHashes The set of log files to remove.

GoogleDataLogger/GoogleDataLogger/Classes/GDLLogStorage.m

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -105,30 +105,6 @@ - (void)storeLog:(GDLLogEvent *)log {
105105
});
106106
}
107107

108-
- (void)removeLog:(NSNumber *)logHash logTarget:(NSNumber *)logTarget {
109-
dispatch_async(_storageQueue, ^{
110-
NSURL *logFile = self.logHashToLogFile[logHash];
111-
112-
// Remove from disk, first and foremost.
113-
NSError *error;
114-
[[NSFileManager defaultManager] removeItemAtURL:logFile error:&error];
115-
GDLAssert(error == nil, @"There was an error removing a logFile: %@", error);
116-
117-
// Remove from the tracking collections.
118-
[self.logHashToLogFile removeObjectForKey:logHash];
119-
NSMutableSet<NSNumber *> *logHashes = self.logTargetToLogHashSet[logTarget];
120-
GDLAssert(logHashes, @"There wasn't a logSet for this logTarget.");
121-
[logHashes removeObject:logHash];
122-
// It's fine to not remove the set if it's empty.
123-
124-
// Check that a log prioritizer is available for this logTarget.
125-
id<GDLLogPrioritizer> logPrioritizer =
126-
[GDLRegistrar sharedInstance].logTargetToPrioritizer[logTarget];
127-
GDLAssert(logPrioritizer, @"There's no prioritizer registered for the given logTarget.");
128-
[logPrioritizer unprioritizeLog:logHash];
129-
});
130-
}
131-
132108
- (void)removeLogs:(NSSet<NSNumber *> *)logHashes logTarget:(NSNumber *)logTarget {
133109
dispatch_sync(_storageQueue, ^{
134110
for (NSNumber *logHash in logHashes) {
@@ -151,6 +127,33 @@ - (void)removeLogs:(NSSet<NSNumber *> *)logHashes logTarget:(NSNumber *)logTarge
151127

152128
#pragma mark - Private helper methods
153129

130+
/** Removes the corresponding log file from disk.
131+
*
132+
* @param logHash The hash value of the original log.
133+
* @param logTarget The logTarget of the original log.
134+
*/
135+
- (void)removeLog:(NSNumber *)logHash logTarget:(NSNumber *)logTarget {
136+
NSURL *logFile = self.logHashToLogFile[logHash];
137+
138+
// Remove from disk, first and foremost.
139+
NSError *error;
140+
[[NSFileManager defaultManager] removeItemAtURL:logFile error:&error];
141+
GDLAssert(error == nil, @"There was an error removing a logFile: %@", error);
142+
143+
// Remove from the tracking collections.
144+
[self.logHashToLogFile removeObjectForKey:logHash];
145+
NSMutableSet<NSNumber *> *logHashes = self.logTargetToLogHashSet[logTarget];
146+
GDLAssert(logHashes, @"There wasn't a logSet for this logTarget.");
147+
[logHashes removeObject:logHash];
148+
// It's fine to not remove the set if it's empty.
149+
150+
// Check that a log prioritizer is available for this logTarget.
151+
id<GDLLogPrioritizer> logPrioritizer =
152+
[GDLRegistrar sharedInstance].logTargetToPrioritizer[logTarget];
153+
GDLAssert(logPrioritizer, @"There's no prioritizer registered for the given logTarget.");
154+
[logPrioritizer unprioritizeLog:logHash];
155+
}
156+
154157
/** Creates the log directory if it does not exist. */
155158
- (void)createLogDirectoryIfNotExists {
156159
NSError *error;
@@ -168,16 +171,16 @@ - (void)createLogDirectoryIfNotExists {
168171
* @note This method should only be called from a method within a block on _storageQueue to maintain
169172
* thread safety.
170173
*
171-
* @param logProtoBytes The extensionBytes of the log, presumably proto bytes.
174+
* @param logTransportBytes The extensionBytes of the log, presumably proto bytes.
172175
* @param logHash The hash value of the log.
173176
* @return The filename
174177
*/
175-
- (NSURL *)saveLogProtoToDisk:(NSData *)logProtoBytes logHash:(NSUInteger)logHash {
178+
- (NSURL *)saveLogProtoToDisk:(NSData *)logTransportBytes logHash:(NSUInteger)logHash {
176179
NSString *storagePath = GDLStoragePath();
177180
NSString *logFile = [NSString stringWithFormat:@"log-%lu", (unsigned long)logHash];
178181
NSURL *logFilePath = [NSURL fileURLWithPath:[storagePath stringByAppendingPathComponent:logFile]];
179182

180-
BOOL writingSuccess = [logProtoBytes writeToURL:logFilePath atomically:YES];
183+
BOOL writingSuccess = [logTransportBytes writeToURL:logFilePath atomically:YES];
181184
if (!writingSuccess) {
182185
GDLLogError(GDLMCEFileWriteError, @"A log file could not be written: %@", logFilePath);
183186
}

GoogleDataLogger/GoogleDataLogger/Classes/GDLUploadCoordinator.m

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ - (GDLUploaderCompletionBlock)onCompleteBlock {
102102
strongSelf->_logTargetToNextUploadTimes[logTarget] = nextUploadAttemptUTC;
103103
NSSet<NSNumber *> *logHashSet =
104104
[strongSelf->_logTargetToInFlightLogSet objectForKey:logTarget];
105+
GDLAssert(logHashSet, @"There should be an in-flight log set to remove.");
105106
[strongSelf.logStorage removeLogs:logHashSet logTarget:logTarget];
106107
[strongSelf->_logTargetToInFlightLogSet removeObjectForKey:logTarget];
107108
if (strongSelf->_forcedUploadQueue.count) {
@@ -158,15 +159,16 @@ - (void)checkPrioritizersAndUploadLogs {
158159
GDLAssert(prioritizer && uploader, @"log target '%@' is missing an implementation",
159160
logTarget);
160161
GDLUploadConditions conds = [self uploadConditions];
161-
NSSet<NSNumber *> *logHashesToUpload = [prioritizer logsToUploadGivenConditions:conds];
162+
NSSet<NSNumber *> *logHashesToUpload =
163+
[[prioritizer logsToUploadGivenConditions:conds] copy];
162164
if (logHashesToUpload && logHashesToUpload.count > 0) {
163165
NSAssert(logHashesToUpload.count > 0, @"");
164166
NSSet<NSURL *> *logFilesToUpload =
165167
[strongSelf.logStorage logHashesToFiles:logHashesToUpload];
166168
NSAssert(logFilesToUpload.count == logHashesToUpload.count,
167169
@"There should be the same number of files to logs");
168-
[uploader uploadLogs:logFilesToUpload onComplete:self.onCompleteBlock];
169170
strongSelf->_logTargetToInFlightLogSet[logTarget] = logHashesToUpload;
171+
[uploader uploadLogs:logFilesToUpload onComplete:self.onCompleteBlock];
170172
}
171173
}
172174
}

GoogleDataLogger/GoogleDataLogger/Classes/Public/GDLLogProto.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN
2727
*
2828
* @return the serialized proto bytes of the implementing log proto.
2929
*/
30-
- (NSData *)protoBytes;
30+
- (NSData *)transportBytes;
3131

3232
@end
3333

GoogleDataLogger/GoogleDataLogger/DependencyWrappers/GDLConsoleLogger.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ static GULLoggerService kGDLConsoleLogger = @"[GoogleDataLogger]";
3030
*/
3131
typedef NS_ENUM(NSInteger, GDLMessageCode) {
3232

33-
/** For warning messages concerning protoBytes: not being implemented by a log extension. */
33+
/** For warning messages concerning transportBytes: not being implemented by a log extension. */
3434
GDLMCWExtensionMissingBytesImpl = 1,
3535

3636
/** For warning message concerning a failed log upload. */
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
* Copyright 2019 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import <XCTest/XCTest.h>
18+
19+
#import <GoogleDataLogger/GoogleDataLogger.h>
20+
21+
#import "GDLIntegrationTestPrioritizer.h"
22+
#import "GDLIntegrationTestUploader.h"
23+
#import "GDLTestServer.h"
24+
25+
#import "GDLLogStorage_Private.h"
26+
#import "GDLUploadCoordinator+Testing.h"
27+
28+
/** A test-only log object used in this integration test. */
29+
@interface GDLIntegrationTestLog : NSObject <GDLLogProto>
30+
31+
@end
32+
33+
@implementation GDLIntegrationTestLog
34+
35+
- (NSData *)transportBytes {
36+
// In real usage, protobuf's -data method or a custom implementation using nanopb are used.
37+
return [[NSString stringWithFormat:@"%@", [NSDate date]] dataUsingEncoding:NSUTF8StringEncoding];
38+
}
39+
40+
@end
41+
42+
/** A test-only log transformer. */
43+
@interface GDLIntegrationTestTransformer : NSObject <GDLLogTransformer>
44+
45+
@end
46+
47+
@implementation GDLIntegrationTestTransformer
48+
49+
- (GDLLogEvent *)transform:(GDLLogEvent *)logEvent {
50+
// drop half the logs during transforming.
51+
if (arc4random_uniform(2) == 1) {
52+
logEvent = nil;
53+
}
54+
return logEvent;
55+
}
56+
57+
@end
58+
59+
@interface GDLIntegrationTest : XCTestCase
60+
61+
/** A test prioritizer. */
62+
@property(nonatomic) GDLIntegrationTestPrioritizer *prioritizer;
63+
64+
/** A test uploader. */
65+
@property(nonatomic) GDLIntegrationTestUploader *uploader;
66+
67+
/** The first test logger. */
68+
@property(nonatomic) GDLLogger *logger1;
69+
70+
/** The second test logger. */
71+
@property(nonatomic) GDLLogger *logger2;
72+
73+
@end
74+
75+
@implementation GDLIntegrationTest
76+
77+
- (void)tearDown {
78+
dispatch_sync([GDLLogStorage sharedInstance].storageQueue, ^{
79+
XCTAssertEqual([GDLLogStorage sharedInstance].logHashToLogFile.count, 0);
80+
});
81+
}
82+
83+
- (void)testEndToEndLog {
84+
XCTestExpectation *expectation = [self expectationWithDescription:@"server got the request"];
85+
expectation.assertForOverFulfill = NO;
86+
87+
// Create the server.
88+
GDLTestServer *testServer = [[GDLTestServer alloc] init];
89+
[testServer setResponseCompletedBlock:^(GCDWebServerRequest *_Nonnull request,
90+
GCDWebServerResponse *_Nonnull response) {
91+
[expectation fulfill];
92+
}];
93+
[testServer registerTestPaths];
94+
[testServer start];
95+
96+
// Create loggers.
97+
self.logger1 = [[GDLLogger alloc] initWithLogMapID:@"logMap1"
98+
logTransformers:nil
99+
logTarget:kGDLIntegrationTestTarget];
100+
101+
self.logger2 = [[GDLLogger alloc] initWithLogMapID:@"logMap2"
102+
logTransformers:nil
103+
logTarget:kGDLIntegrationTestTarget];
104+
105+
// Create a prioritizer and uploader.
106+
self.prioritizer = [[GDLIntegrationTestPrioritizer alloc] init];
107+
self.uploader = [[GDLIntegrationTestUploader alloc] initWithServerURL:testServer.serverURL];
108+
109+
// Set the interval to be much shorter than the standard timer.
110+
[GDLUploadCoordinator sharedInstance].timerInterval = NSEC_PER_SEC * 0.1;
111+
[GDLUploadCoordinator sharedInstance].timerLeeway = NSEC_PER_SEC * 0.01;
112+
113+
// Confirm no logs are in disk.
114+
XCTAssertEqual([GDLLogStorage sharedInstance].logHashToLogFile.count, 0);
115+
XCTAssertEqual([GDLLogStorage sharedInstance].logTargetToLogHashSet.count, 0);
116+
117+
// Generate some logs data.
118+
[self generateLogs];
119+
120+
// Confirm logs are on disk.
121+
dispatch_sync([GDLLogStorage sharedInstance].storageQueue, ^{
122+
XCTAssertGreaterThan([GDLLogStorage sharedInstance].logHashToLogFile.count, 0);
123+
XCTAssertGreaterThan([GDLLogStorage sharedInstance].logTargetToLogHashSet.count, 0);
124+
});
125+
126+
// Confirm logs were sent and received.
127+
[self waitForExpectations:@[ expectation ] timeout:10.0];
128+
129+
// Generate logs for a bit.
130+
NSUInteger lengthOfTestToRunInSeconds = 30;
131+
[GDLUploadCoordinator sharedInstance].timerInterval = NSEC_PER_SEC * 5;
132+
[GDLUploadCoordinator sharedInstance].timerLeeway = NSEC_PER_SEC * 1;
133+
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
134+
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
135+
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
136+
dispatch_source_set_event_handler(timer, ^{
137+
static int numberOfTimesCalled = 0;
138+
numberOfTimesCalled++;
139+
if (numberOfTimesCalled < lengthOfTestToRunInSeconds) {
140+
[self generateLogs];
141+
} else {
142+
dispatch_source_cancel(timer);
143+
}
144+
});
145+
dispatch_resume(timer);
146+
147+
// Run for a bit, a couple seconds longer than the previous bit.
148+
[[NSRunLoop currentRunLoop]
149+
runUntilDate:[NSDate dateWithTimeIntervalSinceNow:lengthOfTestToRunInSeconds + 2]];
150+
151+
[testServer stop];
152+
}
153+
154+
/** Generates and logs a bunch of random logs. */
155+
- (void)generateLogs {
156+
for (int i = 0; i < 50; i++) {
157+
// Choose a random logger, and randomly choose if it's a telemetry log.
158+
GDLLogger *logger = arc4random_uniform(2) ? self.logger1 : self.logger2;
159+
BOOL isTelemetryLog = arc4random_uniform(2);
160+
161+
// Create a log
162+
GDLLogEvent *logEvent = [logger newEvent];
163+
logEvent.extension = [[GDLIntegrationTestLog alloc] init];
164+
165+
if (isTelemetryLog) {
166+
[logger logTelemetryEvent:logEvent];
167+
} else {
168+
[logger logDataEvent:logEvent];
169+
}
170+
}
171+
}
172+
173+
@end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2019 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import <Foundation/Foundation.h>
18+
19+
#import <GoogleDataLogger/GoogleDataLogger.h>
20+
21+
/** The integration test log target. Normally, you should use a value in GDLLogTargets.h. */
22+
static GDLLogTarget kGDLIntegrationTestTarget = 100;
23+
24+
/** An integration test prioritization class. */
25+
@interface GDLIntegrationTestPrioritizer : NSObject <GDLLogPrioritizer>
26+
27+
@end

0 commit comments

Comments
 (0)