// 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. #include "Crashlytics/Crashlytics/Helpers/FIRCLSFile.h" #import @interface FIRCLSFileTests : XCTestCase @property(nonatomic, assign) FIRCLSFile unbufferedFile; @property(nonatomic, assign) FIRCLSFile bufferedFile; @property(nonatomic, strong) NSString *unbufferedPath; @property(nonatomic, strong) NSString *bufferedPath; @end @implementation FIRCLSFileTests - (void)setUp { [super setUp]; self.unbufferedPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"file_test"]; self.bufferedPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"buffered_file_test"]; [[NSFileManager defaultManager] removeItemAtPath:self.unbufferedPath error:nil]; FIRCLSFileInitWithPathMode(&_unbufferedFile, [self.unbufferedPath fileSystemRepresentation], false, false); FIRCLSFileInitWithPathMode(&_bufferedFile, [self.bufferedPath fileSystemRepresentation], false, true); } - (void)tearDown { FIRCLSFileClose(&_unbufferedFile); FIRCLSFileClose(&_bufferedFile); [super tearDown]; } - (NSString *)contentsOfFileAtPath:(NSString *)path { return [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; } #pragma mark - - (void)testEmptySection { [self emptySectionWithFile:&_unbufferedFile filePath:self.unbufferedPath buffered:NO]; [self emptySectionWithFile:&_bufferedFile filePath:self.bufferedPath buffered:YES]; } - (void)emptySectionWithFile:(FIRCLSFile *)file filePath:(NSString *)filePath buffered:(BOOL)buffered { FIRCLSFileWriteSectionStart(file, "empty"); FIRCLSFileWriteSectionEnd(file); NSString *contents = [self contentsOfFileAtPath:filePath]; if (buffered) { XCTAssertEqualObjects( contents, @"", @"Expected empty file contents for buffered case: buffer has not yet been flushed to file"); FIRCLSFileFlushWriteBuffer(file); contents = [self contentsOfFileAtPath:filePath]; } XCTAssertEqualObjects( contents, @"{\"empty\":}\n", @"Empty written to and retrieved from file does not match input in %@buffered case", buffered ? @"" : @"un"); } #pragma mark - - (void)testSingleArrayCollection { [self singleArrayCollectionWithFile:&_unbufferedFile filePath:self.unbufferedPath buffered:NO]; [self singleArrayCollectionWithFile:&_bufferedFile filePath:self.bufferedPath buffered:YES]; } - (void)singleArrayCollectionWithFile:(FIRCLSFile *)file filePath:(NSString *)filePath buffered:(BOOL)buffered { FIRCLSFileWriteSectionStart(file, "array"); FIRCLSFileWriteArrayStart(file); FIRCLSFileWriteArrayEntryUint64(file, 1); FIRCLSFileWriteArrayEnd(file); FIRCLSFileWriteSectionEnd(file); NSString *contents = [self contentsOfFileAtPath:filePath]; if (buffered) { XCTAssertEqualObjects( contents, @"", @"Expected empty file contents for buffered case: buffer has not yet been flushed to file"); FIRCLSFileFlushWriteBuffer(file); contents = [self contentsOfFileAtPath:filePath]; } XCTAssertEqualObjects( contents, @"{\"array\":[1]}\n", @"Single array written to and retrieved from file does not match input in %@buffered case", buffered ? @"" : @"un"); } #pragma mark - - (void)testEmptyCollectionFollowedByEntry { [self emptyCollectionFollowedByEntryWithFile:&_unbufferedFile filePath:self.unbufferedPath buffered:NO]; [self emptyCollectionFollowedByEntryWithFile:&_bufferedFile filePath:self.bufferedPath buffered:YES]; } - (void)emptyCollectionFollowedByEntryWithFile:(FIRCLSFile *)file filePath:(NSString *)filePath buffered:(BOOL)buffered { FIRCLSFileWriteSectionStart(file, "empty_array"); FIRCLSFileWriteArrayStart(file); FIRCLSFileWriteArrayEnd(file); // and now put in another entry into the hash FIRCLSFileWriteHashEntryUint64(file, "value", 1); FIRCLSFileWriteSectionEnd(file); NSString *contents = [self contentsOfFileAtPath:filePath]; if (buffered) { XCTAssertEqualObjects( contents, @"", @"Expected empty file contents for buffered case: buffer has not yet been flushed to file"); FIRCLSFileFlushWriteBuffer(file); contents = [self contentsOfFileAtPath:filePath]; } XCTAssertEqualObjects(contents, @"{\"empty_array\":[],\"value\":1}\n", @"Empty collection and entry written to and retrieved from file does not " @"match input in %@buffered case", buffered ? @"" : @"un"); } #pragma mark - - (void)testHexEncodingString { [self hexEncodingStringWithFile:&_unbufferedFile filePath:self.unbufferedPath buffered:NO]; [self hexEncodingStringWithFile:&_bufferedFile filePath:self.bufferedPath buffered:YES]; } - (void)hexEncodingStringWithFile:(FIRCLSFile *)file filePath:(NSString *)filePath buffered:(BOOL)buffered { FIRCLSFileWriteHashStart(file); FIRCLSFileWriteHashEntryHexEncodedString(file, "hex", "hex string"); FIRCLSFileWriteHashEnd(file); NSString *contents = [self contentsOfFileAtPath:filePath]; if (buffered) { XCTAssertEqualObjects( contents, @"", @"Expected empty file contents for buffered case: buffer has not yet been flushed to file"); FIRCLSFileFlushWriteBuffer(file); contents = [self contentsOfFileAtPath:filePath]; } XCTAssertEqualObjects(contents, @"{\"hex\":\"68657820737472696e67\"}", @"Hex encoded string written to and retrieved from file does not match " @"input in %@buffered case", buffered ? @"" : @"un"); } #pragma mark - - (void)testHexEncodingLongString { [self hexEncodingLongStringWithFile:&_unbufferedFile filePath:self.unbufferedPath length:CLS_FILE_HEX_BUFFER * 10 buffered:NO]; [self hexEncodingLongStringWithFile:&_bufferedFile filePath:self.bufferedPath length:FIRCLSWriteBufferLength * 10 buffered:YES]; } - (void)hexEncodingLongStringWithFile:(FIRCLSFile *)file filePath:(NSString *)filePath length:(size_t)length buffered:(BOOL)buffered { char *longString = malloc(length * sizeof(char)); memset(longString, 'a', length); // fill it with 'a' characters longString[length - 1] = 0; // null terminate FIRCLSFileWriteHashStart(file); FIRCLSFileWriteHashEntryHexEncodedString(file, "hex", longString); FIRCLSFileWriteHashEnd(file); NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:filePath] options:0 error:nil]; if (buffered) { XCTAssertNil(dict); FIRCLSFileFlushWriteBuffer(file); dict = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:filePath] options:0 error:nil]; } XCTAssertNotNil(dict, @"Expected a dictionary serialized from JSON file contents in %@buffered case", buffered ? @"" : @"un"); XCTAssertEqual([dict[@"hex"] length], (length - 1) * 2, @"Long hex encoded string written to and retrieved from file does not match input " @"in %@buffered case", buffered ? @"" : @"un"); free(longString); } #pragma mark - - (void)testSignedInteger { [self signedIntegerWithFile:&_unbufferedFile filePath:self.unbufferedPath buffered:NO]; [self signedIntegerWithFile:&_bufferedFile filePath:self.bufferedPath buffered:YES]; } - (void)signedIntegerWithFile:(FIRCLSFile *)file filePath:(NSString *)filePath buffered:(BOOL)buffered { FIRCLSFileWriteSectionStart(file, "signed"); FIRCLSFileWriteHashStart(file); FIRCLSFileWriteHashEntryInt64(file, "value", -1); FIRCLSFileWriteHashEnd(file); FIRCLSFileWriteSectionEnd(file); NSString *contents = [self contentsOfFileAtPath:filePath]; if (buffered) { XCTAssertEqualObjects( contents, @"", @"Expected empty file contents for buffered case: buffer has not yet been flushed to file"); FIRCLSFileFlushWriteBuffer(file); contents = [self contentsOfFileAtPath:filePath]; } XCTAssertEqualObjects( contents, @"{\"signed\":{\"value\":-1}}\n", @"Signed integer written to and retrieved from file does not match input in %@buffered case", buffered ? @"" : @"un"); } #pragma mark - - (void)testBigInteger { [self bigIntegerWithFile:&_unbufferedFile filePath:self.unbufferedPath buffered:NO]; [self bigIntegerWithFile:&_bufferedFile filePath:self.bufferedPath buffered:YES]; } - (void)bigIntegerWithFile:(FIRCLSFile *)file filePath:(NSString *)filePath buffered:(BOOL)buffered { FIRCLSFileWriteSectionStart(file, "big_int"); FIRCLSFileWriteHashStart(file); FIRCLSFileWriteHashEntryUint64(file, "value", 0x12345678900af8a1); FIRCLSFileWriteHashEnd(file); FIRCLSFileWriteSectionEnd(file); NSString *contents = [self contentsOfFileAtPath:filePath]; if (buffered) { XCTAssertEqualObjects( contents, @"", @"Expected empty file contents for buffered case: buffer has not yet been flushed to file"); FIRCLSFileFlushWriteBuffer(file); contents = [self contentsOfFileAtPath:filePath]; } XCTAssertEqualObjects( contents, @"{\"big_int\":{\"value\":1311768467284359329}}\n", @"Big integer written to and retrieved from file does not match input in %@buffered case", buffered ? @"" : @"un"); } #pragma mark - - (void)testMaxUInt64 { [self maxUInt64WithFile:&_unbufferedFile filePath:self.unbufferedPath buffered:NO]; [self maxUInt64WithFile:&_bufferedFile filePath:self.bufferedPath buffered:YES]; } - (void)maxUInt64WithFile:(FIRCLSFile *)file filePath:(NSString *)filePath buffered:(BOOL)buffered { FIRCLSFileWriteSectionStart(file, "big_int"); FIRCLSFileWriteHashStart(file); FIRCLSFileWriteHashEntryUint64(file, "value", 0xFFFFFFFFFFFFFFFF); FIRCLSFileWriteHashEnd(file); FIRCLSFileWriteSectionEnd(file); NSString *contents = [self contentsOfFileAtPath:filePath]; if (buffered) { XCTAssertEqualObjects( contents, @"", @"Expected empty file contents for buffered case: buffer has not yet been flushed to file"); FIRCLSFileFlushWriteBuffer(file); contents = [self contentsOfFileAtPath:filePath]; } XCTAssertEqualObjects(contents, @"{\"big_int\":{\"value\":18446744073709551615}}\n", @"Max unsigned integer written to and retrieved from file does not match " @"input in %@buffered case", buffered ? @"" : @"un"); } #pragma mark - - (void)testSimpleHashPerformanceWithUnbufferedFile { [self measureBlock:^{ // just one run isn't sufficient for (NSUInteger i = 0; i < 2000; ++i) { FIRCLSFileWriteSectionStart(&self->_unbufferedFile, "hash_test"); FIRCLSFileWriteHashEntryString(&self->_unbufferedFile, "key1", "value1"); FIRCLSFileWriteHashEntryUint64(&self->_unbufferedFile, "key2", 64); FIRCLSFileWriteHashEntryNSString(&self->_unbufferedFile, "key2", @"some string"); FIRCLSFileWriteSectionEnd(&self->_unbufferedFile); } }]; } - (void)testSimpleHashPerformanceWithBufferedFile { [self measureBlock:^{ // just one run isn't sufficient for (NSUInteger i = 0; i < 2000; ++i) { FIRCLSFileWriteSectionStart(&self->_bufferedFile, "hash_test"); FIRCLSFileWriteHashEntryString(&self->_bufferedFile, "key1", "value1"); FIRCLSFileWriteHashEntryUint64(&self->_bufferedFile, "key2", 64); FIRCLSFileWriteHashEntryNSString(&self->_bufferedFile, "key2", @"some string"); FIRCLSFileWriteSectionEnd(&self->_bufferedFile); } }]; } #pragma mark - - (void)testOpenAndClose { [self openAndCloseWithFile:&_unbufferedFile filePath:self.unbufferedPath buffered:NO]; [self openAndCloseWithFile:&_bufferedFile filePath:self.bufferedPath buffered:YES]; } - (void)openAndCloseWithFile:(FIRCLSFile *)file filePath:(NSString *)filePath buffered:(BOOL)buffered { XCTAssert(FIRCLSFileIsOpen(file), @"File should be opened by setup in %@buffered case", buffered ? @"" : @"un"); XCTAssert(FIRCLSFileClose(file), @"Closing the file should succeed in %@buffered case", buffered ? @"" : @"un"); XCTAssertFalse(FIRCLSFileIsOpen(file), @"File should now be marked as closed in %@buffered case", buffered ? @"" : @"un"); XCTAssert(FIRCLSFileInitWithPath(file, [filePath fileSystemRepresentation], buffered), @"Re-opening the same file structure should succeed in %@buffered case", buffered ? @"" : @"un"); XCTAssert(FIRCLSFileIsOpen(file), @"That file should be marked as open as well in %@buffered case", buffered ? @"" : @"un"); } #pragma mark - - (void)testCloseAndOpenAlternatingBufferedOption { [self closeAndOpenAlternatingBufferedOptionWithFile:&_unbufferedFile filePath:self.unbufferedPath buffered:NO]; [self closeAndOpenAlternatingBufferedOptionWithFile:&_bufferedFile filePath:self.bufferedPath buffered:YES]; } - (void)closeAndOpenAlternatingBufferedOptionWithFile:(FIRCLSFile *)file filePath:(NSString *)filePath buffered:(BOOL)buffered { XCTAssert(FIRCLSFileIsOpen(file), @"File should be opened by setup in %@buffered case", buffered ? @"" : @"un"); XCTAssert(FIRCLSFileClose(file), @"Closing the file should succeed in %@buffered case", buffered ? @"" : @"un"); XCTAssertFalse(FIRCLSFileIsOpen(file), @"File should now be marked as closed in %@buffered case", buffered ? @"" : @"un"); XCTAssert(FIRCLSFileInitWithPath(file, [filePath fileSystemRepresentation], !buffered), @"Re-opening the same file structure should succeed in %@buffered case", buffered ? @"" : @"un"); XCTAssert(FIRCLSFileIsOpen(file), @"That file should be marked as open as well in %@buffered case", buffered ? @"" : @"un"); XCTAssert(FIRCLSFileClose(file), @"Closing the file should succeed in %@buffered case", buffered ? @"" : @"un"); XCTAssertFalse(FIRCLSFileIsOpen(file), @"File should now be marked as closed in %@buffered case", buffered ? @"" : @"un"); XCTAssert(FIRCLSFileInitWithPath(file, [filePath fileSystemRepresentation], buffered), @"Re-opening the same file structure should succeed in %@buffered case", buffered ? @"" : @"un"); XCTAssert(FIRCLSFileIsOpen(file), @"That file should be marked as open as well in %@buffered case", buffered ? @"" : @"un"); } #pragma mark - - (void)testLoggingInputLongerThanBuffer { size_t inputLength = (FIRCLSWriteBufferLength + 2) * sizeof(char); char *input = malloc(inputLength); for (size_t i = 0; i < inputLength - 1; i++) { input[i] = i % 26 + 'a'; } input[inputLength - 1] = '\0'; NSString *inputString = [NSString stringWithUTF8String:input]; FIRCLSFileWriteHashStart(&_bufferedFile); FIRCLSFileWriteHashEntryString(&_bufferedFile, "value", input); FIRCLSFileWriteHashEnd(&_bufferedFile); FIRCLSFileFlushWriteBuffer(&_bufferedFile); NSError *error; NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:self.bufferedPath] options:0 error:&error]; XCTAssertNotNil(dict, @"No data was retrieved from log file while writing long input with buffering"); NSString *writtenValue = dict[@"value"]; XCTAssertEqualObjects(inputString, writtenValue, @"Data was lost while writing long input to file with write buffering"); free(input); } @end