| // |
| // GTMFileSystemKQueueTest.m |
| // |
| // Copyright 2008 Google Inc. |
| // |
| // 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 "GTMSenTestCase.h" |
| #import "GTMFileSystemKQueue.h" |
| |
| |
| #pragma clang diagnostic push |
| // Ignore all of the deprecation warnings for GTMFileSystemKQueue |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| |
| // Private methods of GTMFileSystemKQueue we use for some tests |
| @interface GTMFileSystemKQueue (PrivateMethods) |
| - (void)unregisterWithKQueue; |
| @end |
| |
| |
| @interface GTMFileSystemKQueueTest : GTMTestCase { |
| @private |
| NSString *testPath_; |
| NSString *testPath2_; |
| } |
| @end |
| |
| |
| // Helper class to serve as callback target of the kqueue test |
| @interface GTMFSKQTestHelper : NSObject { |
| @private |
| int writes_, renames_, deletes_; |
| GTM_WEAK GTMFileSystemKQueue *queue_; |
| } |
| @end |
| |
| @implementation GTMFSKQTestHelper |
| |
| - (void)callbackForQueue:(GTMFileSystemKQueue *)queue |
| events:(GTMFileSystemKQueueEvents)event { |
| if (queue != queue_) { |
| // We should never get here. |
| [NSException raise:NSInternalInconsistencyException format:@"Bad Queue!"]; |
| } |
| |
| if (event & kGTMFileSystemKQueueWriteEvent) { |
| ++writes_; |
| } |
| if (event & kGTMFileSystemKQueueDeleteEvent) { |
| ++deletes_; |
| } |
| if (event & kGTMFileSystemKQueueRenameEvent) { |
| ++renames_; |
| } |
| } |
| - (int)totals { |
| return writes_ + renames_ + deletes_; |
| } |
| - (int)writes { |
| return writes_; |
| } |
| - (int)renames { |
| return renames_; |
| } |
| - (int)deletes { |
| return deletes_; |
| } |
| |
| - (void)setKQueue:(GTMFileSystemKQueue *)queue { |
| queue_ = queue; |
| } |
| |
| @end |
| |
| |
| @implementation GTMFileSystemKQueueTest |
| |
| - (void)setUp { |
| NSString *temp = NSTemporaryDirectory(); |
| testPath_ |
| = [[temp stringByAppendingPathComponent: |
| @"GTMFileSystemKQueueTest.testfile"] retain]; |
| testPath2_ = [[testPath_ stringByAppendingPathExtension:@"2"] retain]; |
| |
| // make sure the files aren't in the way of the test |
| NSFileManager *fm = [NSFileManager defaultManager]; |
| #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| NSError *error = nil; |
| [fm removeItemAtPath:testPath_ error:&error]; |
| [fm removeItemAtPath:testPath2_ error:&error]; |
| #else |
| [fm removeFileAtPath:testPath_ handler:nil]; |
| [fm removeFileAtPath:testPath2_ handler:nil]; |
| #endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| } |
| |
| - (void)tearDown { |
| // make sure we clean up the files from a failed test |
| NSFileManager *fm = [NSFileManager defaultManager]; |
| #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| NSError *error = nil; |
| [fm removeItemAtPath:testPath_ error:&error]; |
| [fm removeItemAtPath:testPath2_ error:&error]; |
| #else |
| [fm removeFileAtPath:testPath_ handler:nil]; |
| [fm removeFileAtPath:testPath2_ handler:nil]; |
| #endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| |
| [testPath_ release]; |
| testPath_ = nil; |
| [testPath2_ release]; |
| testPath2_ = nil; |
| } |
| |
| - (void)testInit { |
| GTMFileSystemKQueue *testKQ; |
| GTMFSKQTestHelper *helper = [[[GTMFSKQTestHelper alloc] init] autorelease]; |
| XCTAssertNotNil(helper); |
| |
| // init should fail |
| testKQ = [[[GTMFileSystemKQueue alloc] init] autorelease]; |
| XCTAssertNil(testKQ); |
| |
| // no path |
| testKQ |
| = [[[GTMFileSystemKQueue alloc] initWithPath:nil |
| forEvents:kGTMFileSystemKQueueAllEvents |
| acrossReplace:YES |
| target:helper |
| action:@selector(callbackForQueue:events:)] autorelease]; |
| XCTAssertNil(testKQ); |
| |
| // not events |
| testKQ |
| = [[[GTMFileSystemKQueue alloc] initWithPath:@"/var/log/system.log" |
| forEvents:0 |
| acrossReplace:YES |
| target:helper |
| action:@selector(callbackForQueue:events:)] autorelease]; |
| XCTAssertNil(testKQ); |
| |
| // no target |
| testKQ |
| = [[[GTMFileSystemKQueue alloc] initWithPath:@"/var/log/system.log" |
| forEvents:kGTMFileSystemKQueueAllEvents |
| acrossReplace:YES |
| target:nil |
| action:@selector(callbackForQueue:events:)] autorelease]; |
| XCTAssertNil(testKQ); |
| |
| // no handler |
| testKQ |
| = [[[GTMFileSystemKQueue alloc] initWithPath:@"/var/log/system.log" |
| forEvents:0 |
| acrossReplace:YES |
| target:helper |
| action:nil] autorelease]; |
| XCTAssertNil(testKQ); |
| |
| |
| // path that doesn't exist |
| testKQ |
| = [[[GTMFileSystemKQueue alloc] initWithPath:@"/path/that/does/not/exist" |
| forEvents:kGTMFileSystemKQueueAllEvents |
| acrossReplace:YES |
| target:helper |
| action:@selector(callbackForQueue:events:)] autorelease]; |
| XCTAssertNil(testKQ); |
| } |
| |
| - (void)spinForEvents:(GTMFSKQTestHelper *)helper { |
| // Spin the runloop for a second so that the helper callbacks fire |
| unsigned int attempts = 0; |
| int initialTotals = [helper totals]; |
| while (([helper totals] == initialTotals) && (attempts < 10)) { // Try for up to 2s |
| [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.2]]; |
| attempts++; |
| } |
| } |
| |
| - (void)testWriteAndDelete { |
| |
| NSFileManager *fm = [NSFileManager defaultManager]; |
| GTMFSKQTestHelper *helper = [[[GTMFSKQTestHelper alloc] init] autorelease]; |
| XCTAssertNotNil(helper); |
| |
| XCTAssertTrue([fm createFileAtPath:testPath_ contents:nil attributes:nil]); |
| NSFileHandle *testFH = [NSFileHandle fileHandleForWritingAtPath:testPath_]; |
| XCTAssertNotNil(testFH); |
| |
| // Start monitoring the file |
| GTMFileSystemKQueue *testKQ |
| = [[GTMFileSystemKQueue alloc] initWithPath:testPath_ |
| forEvents:kGTMFileSystemKQueueAllEvents |
| acrossReplace:YES |
| target:helper |
| action:@selector(callbackForQueue:events:)]; |
| XCTAssertNotNil(testKQ); |
| XCTAssertEqualObjects([testKQ path], testPath_); |
| [helper setKQueue:testKQ]; |
| |
| // Write to the file |
| [testFH writeData:[@"doh!" dataUsingEncoding:NSUnicodeStringEncoding]]; |
| |
| // Spin the runloop for a second so that the helper callbacks fire |
| [self spinForEvents:helper]; |
| XCTAssertEqual([helper totals], 1); |
| |
| // Close and delete |
| [testFH closeFile]; |
| #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| NSError *error = nil; |
| XCTAssertTrue([fm removeItemAtPath:testPath_ error:&error], @"Err: %@", error); |
| #else |
| XCTAssertTrue([fm removeFileAtPath:testPath_ handler:nil]); |
| #endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| |
| [self spinForEvents:helper]; |
| XCTAssertEqual([helper totals], 2); |
| |
| // Clean up the kqueue |
| [testKQ release]; |
| testKQ = nil; |
| |
| XCTAssertEqual([helper writes], 1); |
| XCTAssertEqual([helper deletes], 1); |
| XCTAssertEqual([helper renames], 0); |
| } |
| |
| - (void)testWriteAndDeleteAndWrite { |
| |
| // One will pass YES to |acrossReplace|, the other will pass NO. |
| |
| NSFileManager *fm = [NSFileManager defaultManager]; |
| GTMFSKQTestHelper *helper = [[[GTMFSKQTestHelper alloc] init] autorelease]; |
| XCTAssertNotNil(helper); |
| GTMFSKQTestHelper *helper2 = [[[GTMFSKQTestHelper alloc] init] autorelease]; |
| XCTAssertNotNil(helper); |
| |
| // Create a temp file path |
| XCTAssertTrue([fm createFileAtPath:testPath_ contents:nil attributes:nil]); |
| NSFileHandle *testFH = [NSFileHandle fileHandleForWritingAtPath:testPath_]; |
| XCTAssertNotNil(testFH); |
| |
| // Start monitoring the file |
| GTMFileSystemKQueue *testKQ |
| = [[GTMFileSystemKQueue alloc] initWithPath:testPath_ |
| forEvents:kGTMFileSystemKQueueAllEvents |
| acrossReplace:YES |
| target:helper |
| action:@selector(callbackForQueue:events:)]; |
| XCTAssertNotNil(testKQ); |
| XCTAssertEqualObjects([testKQ path], testPath_); |
| [helper setKQueue:testKQ]; |
| |
| GTMFileSystemKQueue *testKQ2 |
| = [[GTMFileSystemKQueue alloc] initWithPath:testPath_ |
| forEvents:kGTMFileSystemKQueueAllEvents |
| acrossReplace:NO |
| target:helper2 |
| action:@selector(callbackForQueue:events:)]; |
| XCTAssertNotNil(testKQ2); |
| XCTAssertEqualObjects([testKQ2 path], testPath_); |
| [helper2 setKQueue:testKQ2]; |
| |
| // Write to the file |
| [testFH writeData:[@"doh!" dataUsingEncoding:NSUnicodeStringEncoding]]; |
| |
| // Spin the runloop for a second so that the helper callbacks fire |
| [self spinForEvents:helper]; |
| XCTAssertEqual([helper totals], 1); |
| XCTAssertEqual([helper2 totals], 1); |
| |
| // Close and delete |
| [testFH closeFile]; |
| #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| NSError *error = nil; |
| XCTAssertTrue([fm removeItemAtPath:testPath_ error:&error], @"Err: %@", error); |
| #else |
| XCTAssertTrue([fm removeFileAtPath:testPath_ handler:nil]); |
| #endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| |
| // Recreate |
| XCTAssertTrue([fm createFileAtPath:testPath_ contents:nil attributes:nil]); |
| testFH = [NSFileHandle fileHandleForWritingAtPath:testPath_]; |
| XCTAssertNotNil(testFH); |
| [testFH writeData:[@"ha!" dataUsingEncoding:NSUnicodeStringEncoding]]; |
| |
| // Spin the runloop for a second so that the helper callbacks fire |
| [self spinForEvents:helper]; |
| XCTAssertEqual([helper totals], 2); |
| XCTAssertEqual([helper2 totals], 2); |
| |
| // Write to it again |
| [testFH writeData:[@"continued..." dataUsingEncoding:NSUnicodeStringEncoding]]; |
| |
| // Spin the runloop for a second so that the helper callbacks fire |
| [self spinForEvents:helper]; |
| XCTAssertEqual([helper totals], 3); |
| XCTAssertEqual([helper2 totals], 2); |
| |
| // Close and delete |
| [testFH closeFile]; |
| #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| XCTAssertTrue([fm removeItemAtPath:testPath_ error:&error], @"Err: %@", error); |
| #else |
| XCTAssertTrue([fm removeFileAtPath:testPath_ handler:nil]); |
| #endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| |
| // Spin the runloop for a second so that the helper callbacks fire |
| [self spinForEvents:helper]; |
| XCTAssertEqual([helper totals], 4); |
| XCTAssertEqual([helper2 totals], 2); |
| |
| // Clean up the kqueue |
| [testKQ release]; |
| testKQ = nil; |
| [testKQ2 release]; |
| testKQ2 = nil; |
| |
| XCTAssertEqual([helper writes], 2); |
| XCTAssertEqual([helper deletes], 2); |
| XCTAssertEqual([helper renames], 0); |
| XCTAssertEqual([helper2 writes], 1); |
| XCTAssertEqual([helper2 deletes], 1); |
| XCTAssertEqual([helper2 renames], 0); |
| } |
| |
| - (void)testWriteAndRenameAndWrite { |
| |
| // One will pass YES to |acrossReplace|, the other will pass NO. |
| |
| NSFileManager *fm = [NSFileManager defaultManager]; |
| GTMFSKQTestHelper *helper = [[[GTMFSKQTestHelper alloc] init] autorelease]; |
| XCTAssertNotNil(helper); |
| GTMFSKQTestHelper *helper2 = [[[GTMFSKQTestHelper alloc] init] autorelease]; |
| XCTAssertNotNil(helper2); |
| |
| // Create a temp file path |
| XCTAssertTrue([fm createFileAtPath:testPath_ contents:nil attributes:nil]); |
| NSFileHandle *testFH = [NSFileHandle fileHandleForWritingAtPath:testPath_]; |
| XCTAssertNotNil(testFH); |
| |
| // Start monitoring the file |
| GTMFileSystemKQueue *testKQ |
| = [[GTMFileSystemKQueue alloc] initWithPath:testPath_ |
| forEvents:kGTMFileSystemKQueueAllEvents |
| acrossReplace:YES |
| target:helper |
| action:@selector(callbackForQueue:events:)]; |
| XCTAssertNotNil(testKQ); |
| XCTAssertEqualObjects([testKQ path], testPath_); |
| [helper setKQueue:testKQ]; |
| |
| GTMFileSystemKQueue *testKQ2 |
| = [[GTMFileSystemKQueue alloc] initWithPath:testPath_ |
| forEvents:kGTMFileSystemKQueueAllEvents |
| acrossReplace:NO |
| target:helper2 |
| action:@selector(callbackForQueue:events:)]; |
| XCTAssertNotNil(testKQ2); |
| XCTAssertEqualObjects([testKQ2 path], testPath_); |
| [helper2 setKQueue:testKQ2]; |
| |
| // Write to the file |
| [testFH writeData:[@"doh!" dataUsingEncoding:NSUnicodeStringEncoding]]; |
| |
| // Spin the runloop for a second so that the helper callbacks fire |
| [self spinForEvents:helper]; |
| XCTAssertEqual([helper totals], 1); |
| XCTAssertEqual([helper2 totals], 1); |
| |
| // Move it and create the file again |
| #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| NSError *error = nil; |
| XCTAssertTrue([fm moveItemAtPath:testPath_ toPath:testPath2_ error:&error], |
| @"Error: %@", error); |
| #else |
| XCTAssertTrue([fm movePath:testPath_ toPath:testPath2_ handler:nil]); |
| #endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| |
| XCTAssertTrue([fm createFileAtPath:testPath_ contents:nil attributes:nil]); |
| NSFileHandle *testFHPrime |
| = [NSFileHandle fileHandleForWritingAtPath:testPath_]; |
| XCTAssertNotNil(testFHPrime); |
| [testFHPrime writeData:[@"eh?" dataUsingEncoding:NSUnicodeStringEncoding]]; |
| |
| // Spin the runloop for a second so that the helper callbacks fire |
| [self spinForEvents:helper]; |
| XCTAssertEqual([helper totals], 2); |
| XCTAssertEqual([helper2 totals], 2); |
| |
| // Write to the new file |
| [testFHPrime writeData:[@"continue..." dataUsingEncoding:NSUnicodeStringEncoding]]; |
| |
| // Spin the runloop for a second so that the helper callbacks fire |
| [self spinForEvents:helper]; |
| XCTAssertEqual([helper totals], 3); |
| XCTAssertEqual([helper2 totals], 2); |
| |
| // Write to the old file |
| [testFH writeData:[@"continue old..." dataUsingEncoding:NSUnicodeStringEncoding]]; |
| |
| // Spin the runloop for a second so that the helper callbacks fire |
| [self spinForEvents:helper]; |
| XCTAssertEqual([helper totals], 3); |
| XCTAssertEqual([helper2 totals], 3); |
| |
| // and now close old |
| [testFH closeFile]; |
| #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| XCTAssertTrue([fm removeItemAtPath:testPath2_ error:&error], @"Err: %@", error); |
| #else |
| XCTAssertTrue([fm removeFileAtPath:testPath2_ handler:nil]); |
| #endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| |
| // Spin the runloop for a second so that the helper callbacks fire |
| [self spinForEvents:helper]; |
| XCTAssertEqual([helper totals], 3); |
| XCTAssertEqual([helper2 totals], 4); |
| |
| // and now close new |
| [testFHPrime closeFile]; |
| #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| XCTAssertTrue([fm removeItemAtPath:testPath_ error:&error], @"Err: %@", error); |
| #else |
| XCTAssertTrue([fm removeFileAtPath:testPath_ handler:nil]); |
| #endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| |
| // Spin the runloop for a second so that the helper callbacks fire |
| [self spinForEvents:helper]; |
| XCTAssertEqual([helper totals], 4); |
| XCTAssertEqual([helper2 totals], 4); |
| |
| // Clean up the kqueue |
| [testKQ release]; |
| testKQ = nil; |
| [testKQ2 release]; |
| testKQ2 = nil; |
| |
| XCTAssertEqual([helper writes], 2); |
| XCTAssertEqual([helper deletes], 1); |
| XCTAssertEqual([helper renames], 1); |
| XCTAssertEqual([helper2 writes], 2); |
| XCTAssertEqual([helper2 deletes], 1); |
| XCTAssertEqual([helper2 renames], 1); |
| } |
| |
| - (void)testNoSpinHang { |
| // This case tests a specific historically problematic interaction of |
| // GTMFileSystemKQueue and the runloop. GTMFileSystemKQueue uses the CFSocket |
| // notifications (and thus the runloop) for monitoring, however, you can |
| // dealloc the instance (and thus unregister the underlying kevent descriptor) |
| // prior to any runloop spin. The unregister removes the pending notifications |
| // from the monitored main kqueue file descriptor that CFSocket has previously |
| // noticed but not yet called back. At that point a kevent() call in the |
| // socket callback without a timeout would hang the runloop. |
| |
| // Warn this may hang |
| NSLog(@"%s on failure this will hang.", __PRETTY_FUNCTION__); |
| |
| NSFileManager *fm = [NSFileManager defaultManager]; |
| GTMFSKQTestHelper *helper = [[[GTMFSKQTestHelper alloc] init] autorelease]; |
| XCTAssertNotNil(helper); |
| XCTAssertTrue([fm createFileAtPath:testPath_ contents:nil attributes:nil]); |
| NSFileHandle *testFH = [NSFileHandle fileHandleForWritingAtPath:testPath_]; |
| XCTAssertNotNil(testFH); |
| |
| // Start monitoring the file |
| GTMFileSystemKQueue *testKQ |
| = [[GTMFileSystemKQueue alloc] initWithPath:testPath_ |
| forEvents:kGTMFileSystemKQueueAllEvents |
| acrossReplace:YES |
| target:helper |
| action:@selector(callbackForQueue:events:)]; |
| XCTAssertNotNil(testKQ); |
| XCTAssertEqualObjects([testKQ path], testPath_); |
| [helper setKQueue:testKQ]; |
| |
| // Write to the file |
| [testFH writeData:[@"doh!" dataUsingEncoding:NSUnicodeStringEncoding]]; |
| // Close and delete |
| [testFH closeFile]; |
| #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| NSError *error = nil; |
| XCTAssertTrue([fm removeItemAtPath:testPath_ error:&error], @"Err: %@", error); |
| #else |
| XCTAssertTrue([fm removeFileAtPath:testPath_ handler:nil]); |
| #endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| |
| // Now destroy the queue, with events outstanding from the CFSocket, but |
| // unconsumed. |
| XCTAssertEqual([testKQ retainCount], (NSUInteger)1); |
| [testKQ release]; |
| testKQ = nil; |
| |
| // Spin the runloop, no events were delivered (and we should not hang) |
| [self spinForEvents:helper]; |
| XCTAssertEqual([helper totals], 0); |
| } |
| |
| @end |
| |
| #pragma clang diagnostic pop |