From 919cbff0b184fc471ff61055d8e9c32ba20d42a2 Mon Sep 17 00:00:00 2001 From: Yee Cheng Chin Date: Wed, 25 Oct 2023 00:55:23 -0700 Subject: [PATCH] Finish cmdline row calc test Add proper syncing mechanism to wait for event handling and vim message handling by using Objective C method swapping. Inspiration from iTerm2. --- src/MacVim/MMBackend.m | 4 + src/MacVim/MacVim.h | 1 + src/MacVim/MacVimTests/MacVimTests.m | 261 +++++++++++++++++---------- 3 files changed, 175 insertions(+), 91 deletions(-) diff --git a/src/MacVim/MMBackend.m b/src/MacVim/MMBackend.m index 5e3bc0ff45..cd127be61e 100644 --- a/src/MacVim/MMBackend.m +++ b/src/MacVim/MMBackend.m @@ -2319,6 +2319,10 @@ - (void)handleInputEvent:(int)msgid data:(NSData *)data [self setImState:NO]; } else if (BackingPropertiesChangedMsgID == msgid) { [self redrawScreen]; + } else if (LoopBackMsgID == msgid) { + // This is a debug message used for confirming a message has been + // received and echoed back to caller for synchronization purpose. + [self queueMessage:msgid data:nil]; } else { ASLogWarn(@"Unknown message received (msgid=%d)", msgid); } diff --git a/src/MacVim/MacVim.h b/src/MacVim/MacVim.h index 946e729c45..97d662636a 100644 --- a/src/MacVim/MacVim.h +++ b/src/MacVim/MacVim.h @@ -344,6 +344,7 @@ extern const char * const MMVimMsgIDStrings[]; MSG(EnableThinStrokesMsgID) \ MSG(DisableThinStrokesMsgID) \ MSG(ShowDefinitionMsgID) \ + MSG(LoopBackMsgID) /* Simple message that Vim will reflect back to MacVim */ \ MSG(LastMsgID) \ enum { diff --git a/src/MacVim/MacVimTests/MacVimTests.m b/src/MacVim/MacVimTests/MacVimTests.m index 575850539f..db30d6fc61 100644 --- a/src/MacVim/MacVimTests/MacVimTests.m +++ b/src/MacVim/MacVimTests/MacVimTests.m @@ -8,17 +8,25 @@ #import +#import + #import "Miscellaneous.h" #import "MMAppController.h" +#import "MMApplication.h" #import "MMTextView.h" #import "MMWindowController.h" #import "MMVimController.h" #import "MMVimView.h" +// Expose private methods for testing purposes @interface MMAppController (Private) + (NSDictionary*)parseOpenURL:(NSURL*)url; @end +@interface MMVimController (Private) +- (void)handleMessage:(int)msgid data:(NSData *)data; +@end + // Test harness @interface MMAppController (Tests) - (NSMutableArray*)vimControllers; @@ -36,6 +44,130 @@ @interface MacVimTests : XCTestCase @implementation MacVimTests +/// Wait for a Vim controller to be added/removed. By the time this is fulfilled +/// the Vim window should be ready and visible. +- (void)waitForVimController:(int)delta { + NSArray *vimControllers = [MMAppController.sharedInstance vimControllers]; + const int desiredCount = (int)vimControllers.count + delta; + [self waitForExpectations:@[[[XCTNSPredicateExpectation alloc] + initWithPredicate:[NSPredicate predicateWithBlock:^(id vimControllers, NSDictionary *bindings) { + return (BOOL)((int)[(NSArray*)vimControllers count] == desiredCount); + }] + object:vimControllers]] + timeout:5]; +} + +/// Wait for event handling to be finished at the main loop. +- (void)waitForEventHandling { + // Inject a custom event. By the time we handle this event all queued events + // will have been consumed. + const NSInteger appEventType = 1687648131; // magic number to prevent collisions + XCTestExpectation *expectation = [self expectationWithDescription:@"EventHandling"]; + + Method method = class_getInstanceMethod([MMApplication class], @selector(sendEvent:)); + IMP origIMP = method_getImplementation(method); + IMP newIMP = imp_implementationWithBlock(^(id self, NSEvent *event) { + typedef void (*fn)(id,SEL,NSEvent*); + if (event.type == NSEventTypeApplicationDefined && event.data1 == appEventType) { + [expectation fulfill]; + } else { + ((fn)origIMP)(self, @selector(sendEvent:), event); + } + }); + + NSApplication* app = [NSApplication sharedApplication]; + NSEvent* customEvent = [NSEvent otherEventWithType:NSEventTypeApplicationDefined + location:NSMakePoint(50, 50) + modifierFlags:0 + timestamp:100 + windowNumber:[[NSApp mainWindow] windowNumber] + context:0 + subtype:0 + data1:appEventType + data2:0]; + + method_setImplementation(method, newIMP); + + [app postEvent:customEvent atStart:NO]; + [self waitForExpectations:@[expectation] timeout:10]; + + method_setImplementation(method, origIMP); +} + +/// Wait for Vim to process all pending messages in its queue. +- (void)waitForVimProcess { + // Implement this by sending a loopback message (Vim will send the message + // back to us) as a synchronization mechanism as Vim handles its messages + // sequentially. + XCTestExpectation *expectation = [self expectationWithDescription:@"EventHandling"]; + + Method method = class_getInstanceMethod([MMVimController class], @selector(handleMessage:data:)); + IMP origIMP = method_getImplementation(method); + IMP newIMP = imp_implementationWithBlock(^(id self, int msgid, NSData *data) { + typedef void (*fn)(id,SEL,int,NSData*); + if (msgid == LoopBackMsgID) { + [expectation fulfill]; + } else { + ((fn)origIMP)(self, @selector(handleMessage:data:), msgid, data); + } + }); + + method_setImplementation(method, newIMP); + + [[MMAppController.sharedInstance keyVimController] sendMessage:LoopBackMsgID data:nil]; + [self waitForExpectations:@[expectation] timeout:10]; + + method_setImplementation(method, origIMP); +} + +/// Wait for both event handling to be finished at the main loop and for Vim to +/// process all pending messages in its queue. +- (void)waitForEventHandlingAndVimProcess { + [self waitForEventHandling]; + [self waitForVimProcess]; +} + +/// Wait for a fixed timeout before fulfilling expectation. +/// +/// @note Should only be used for quick iteration / debugging unless we cannot +/// find an alternative way to specify an expectation, as timeouts tend to be +/// fragile and take more time to complete. +- (void)waitTimeout:(double)delaySecs { + XCTestExpectation *expectation = [self expectationWithDescription:@"Timeout"]; + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delaySecs * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ + [expectation fulfill]; + }); + [self waitForExpectations:@[expectation] timeout:delaySecs + 10]; +} + +/// Send a single key to MacVim via event handling system. +- (void)sendKeyToVim:(NSString*)chars withMods:(int)mods { + NSApplication* app = [NSApplication sharedApplication]; + NSEvent* keyEvent = [NSEvent keyEventWithType:NSEventTypeKeyDown + location:NSMakePoint(50, 50) + modifierFlags:mods + timestamp:100 + windowNumber:[[NSApp mainWindow] windowNumber] + context:0 + characters:chars + charactersIgnoringModifiers:chars + isARepeat:NO + keyCode:0]; + + [app postEvent:keyEvent atStart:NO]; +} + +/// Send a string to MacVim via event handling system. Each character will be +/// sent separately as if the user typed it. +- (void)sendStringToVim:(NSString*)chars withMods:(int)mods { + for (NSUInteger i = 0; i < chars.length; i++) { + unichar ch = [chars characterAtIndex:i]; + NSString *str = [NSString stringWithCharacters:&ch length:1]; + [self sendKeyToVim:str withMods:mods]; + } +} + - (void)testCompareSemanticVersions { // bogus values evaluate to 0 XCTAssertEqual(0, compareSemanticVersions(@"bogus", @"")); @@ -87,33 +219,6 @@ - (void)testParseOpenURL { XCTAssertEqualObjects([[NSURL URLWithString:@"file:///foo%25bar"] path], @"/foo%bar"); } -/// Wait for a Vim controller to be added/removed. By the time this is fulfilled -/// the Vim window should be ready and visible. -- (void)waitForVimController:(int)delta { - NSArray *vimControllers = [MMAppController.sharedInstance vimControllers]; - const __block int desiredCount = (int)vimControllers.count + delta; - [self waitForExpectations:@[[[XCTNSPredicateExpectation alloc] - initWithPredicate:[NSPredicate predicateWithBlock:^(id vimControllers, NSDictionary *bindings) { - return (BOOL)((int)[(NSArray*)vimControllers count] == desiredCount); - }] - object:vimControllers]] - timeout:5]; -} - -/// Wait for a fixed timeout before fulfilling expectation. -/// -/// @note Should only be used for quick iteration / debugging unless we cannot -/// find an alternative way to specify an expectation, as timeouts tend to be -/// fragile and take more time to complete. -- (void)waitTimeout:(double)delaySecs { - __block XCTestExpectation *expectation = [self expectationWithDescription:@"Timeout"]; - dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delaySecs * NSEC_PER_SEC)); - dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ - [expectation fulfill]; - }); - [self waitForExpectations:@[expectation] timeout:delaySecs + 10]; -} - /// Test that the "Vim Tutor" menu item works and can be used to launch the /// bundled vimtutor. Previously this was silently broken by Vim v8.2.3502 /// and fixed in https://github.com/macvim-dev/macvim/pull/1265. @@ -151,33 +256,10 @@ - (void)testVimTutor { [self waitForVimController:-1]; } -- (void)sendKeyToVim:(NSString*)chars withMods:(int)mods { - NSApplication* app = [NSApplication sharedApplication]; - NSEvent* keyEvent = [NSEvent keyEventWithType:NSEventTypeKeyDown - location:NSMakePoint(50, 50) - modifierFlags:mods - timestamp:100 - windowNumber:[[NSApp mainWindow] windowNumber] - context:0 - characters:chars - charactersIgnoringModifiers:chars - isARepeat:NO - keyCode:0]; - - [app postEvent:keyEvent atStart:NO]; -} - -- (void)sendStringToVim:(NSString*)chars withMods:(int)mods { - for (NSUInteger i = 0; i < chars.length; i++) { - unichar ch = [chars characterAtIndex:i]; - NSString *str = [NSString stringWithCharacters:&ch length:1]; - [self sendKeyToVim:str withMods:mods]; - } -} - /// Test that cmdline row calculation (used by MMCmdLineAlignBottom) is correct. -/// This is an integration test as the calculation is done in Vim, and has to -/// account for "Press Enter" and "More" prompts when showing messages. +/// This is an integration test as the calculation is done in Vim, which has +/// special logic to account for "Press Enter" and "--more--" prompts when showing +/// messages. - (void) testCmdlineRowCalculation { MMAppController *app = MMAppController.sharedInstance; @@ -186,52 +268,58 @@ - (void) testCmdlineRowCalculation { MMTextView *textView = [[[[app keyVimController] windowController] vimView] textView]; const int numLines = [textView maxRows]; + const int numCols = [textView maxColumns]; + + // Define convenience macro (don't use functions to preserve line numbers in callstack) +#define ASSERT_NUM_CMDLINES(expected) \ +{ \ + const int cmdlineRow = [[[app keyVimController] objectForVimStateKey:@"cmdline_row"] intValue]; \ + const int numBottomLines = numLines - cmdlineRow; \ + XCTAssertEqual(expected, numBottomLines); \ +} // Default value - int cmdlineRow = [[[app keyVimController] objectForVimStateKey:@"cmdline_row"] intValue]; - int numBottomLines = numLines - cmdlineRow; - XCTAssertEqual(1, numBottomLines); + ASSERT_NUM_CMDLINES(1); // Print more lines than we have room for to trigger "Press Enter" [self sendStringToVim:@":echo join(repeat(['test line'], 3), \"\\n\")\n" withMods:0]; - // Test only - [self waitTimeout:0.5]; - - cmdlineRow = [[[app keyVimController] objectForVimStateKey:@"cmdline_row"] intValue]; - numBottomLines = numLines - cmdlineRow; - XCTAssertEqual(1, numBottomLines); + [self waitForEventHandlingAndVimProcess]; + ASSERT_NUM_CMDLINES(1); // Test non-1 cmdheight works [self sendStringToVim:@":set cmdheight=3\n" withMods:0]; - [self waitTimeout:0.5]; + [self waitForEventHandlingAndVimProcess]; + ASSERT_NUM_CMDLINES(3); - cmdlineRow = [[[app keyVimController] objectForVimStateKey:@"cmdline_row"] intValue]; - numBottomLines = numLines - cmdlineRow; - XCTAssertEqual(3, numBottomLines); + // Test typing enough characters to cause cmdheight to grow + [self sendStringToVim:[@":\"" stringByPaddingToLength:numCols * 3 - 1 withString:@"a" startingAtIndex:0] withMods:0]; + [self waitForEventHandlingAndVimProcess]; + ASSERT_NUM_CMDLINES(3); + + [self sendStringToVim:@"bbbb" withMods:0]; + [self waitForEventHandlingAndVimProcess]; + ASSERT_NUM_CMDLINES(4); + + [self sendStringToVim:@"\n" withMods:0]; + [self waitForEventHandlingAndVimProcess]; + ASSERT_NUM_CMDLINES(3); // Printing just enough lines within cmdheight should not affect anything [self sendStringToVim:@":echo join(repeat(['test line'], 3), \"\\n\")\n" withMods:0]; - [self waitTimeout:0.5]; + [self waitForEventHandlingAndVimProcess]; + ASSERT_NUM_CMDLINES(3); - cmdlineRow = [[[app keyVimController] objectForVimStateKey:@"cmdline_row"] intValue]; - numBottomLines = numLines - cmdlineRow; - XCTAssertEqual(3, numBottomLines); - - // Printing more lines than cmdheight will once again trigger "Pree Enter" + // Printing more lines than cmdheight will once again trigger "Press Enter" [self sendStringToVim:@":echo join(repeat(['test line'], 4), \"\\n\")\n" withMods:0]; - [self waitTimeout:0.5]; - - cmdlineRow = [[[app keyVimController] objectForVimStateKey:@"cmdline_row"] intValue]; - numBottomLines = numLines - cmdlineRow; - XCTAssertEqual(1, numBottomLines); + [self waitForEventHandlingAndVimProcess]; + ASSERT_NUM_CMDLINES(1); - // Printing more lines than the screen will trigger "More" prompt + // Printing more lines than the screen will trigger "--more--" prompt [self sendStringToVim:@":echo join(repeat(['test line'], 2000), \"\\n\")\n" withMods:0]; - [self waitTimeout:0.5]; + [self waitForEventHandlingAndVimProcess]; + ASSERT_NUM_CMDLINES(1); - cmdlineRow = [[[app keyVimController] objectForVimStateKey:@"cmdline_row"] intValue]; - numBottomLines = numLines - cmdlineRow; - XCTAssertEqual(1, numBottomLines); +#undef ASSERT_NUM_CMDLINES // Clean up [[app keyVimController] sendMessage:VimShouldCloseMsgID data:nil]; @@ -239,12 +327,3 @@ - (void) testCmdlineRowCalculation { } @end - -// TODO test closing last window close the app -// TODO input tests - -// TODO performance tests -// - Latency -// - draw text -// - battery life -