Skip to content

Commit

Permalink
Finish cmdline row calc test
Browse files Browse the repository at this point in the history
Add proper syncing mechanism to wait for event handling and vim message
handling by using Objective C method swapping. Inspiration from iTerm2.
  • Loading branch information
ychin committed Oct 25, 2023
1 parent fb1f551 commit 919cbff
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 91 deletions.
4 changes: 4 additions & 0 deletions src/MacVim/MMBackend.m
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions src/MacVim/MacVim.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
261 changes: 170 additions & 91 deletions src/MacVim/MacVimTests/MacVimTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,25 @@

#import <XCTest/XCTest.h>

#import <objc/runtime.h>

#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;
Expand All @@ -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<NSString *,id> *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", @""));
Expand Down Expand Up @@ -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<NSString *,id> *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.
Expand Down Expand Up @@ -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;

Expand All @@ -186,65 +268,62 @@ - (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];
[self waitForVimController:-1];
}

@end

// TODO test closing last window close the app
// TODO input tests

// TODO performance tests
// - Latency
// - draw text
// - battery life

0 comments on commit 919cbff

Please sign in to comment.