diff --git a/kitty_tests/check_build.py b/kitty_tests/check_build.py index 6b3a798e2..63f21b302 100644 --- a/kitty_tests/check_build.py +++ b/kitty_tests/check_build.py @@ -138,24 +138,6 @@ class TestBuild(BaseTest): self.assertEqual(cp.returncode, 0, cp.stdout) self.assertIn('dictation forwarding probe passed', cp.stdout) - def test_macos_ime_commit(self) -> None: - from kitty.constants import glfw_path, is_macos - - if not is_macos or not shutil.which('clang'): - self.skipTest('Cocoa IME regression test is macOS only and requires clang') - cocoa_module = glfw_path('cocoa') - src = os.path.join(os.path.dirname(__file__), 'cocoa_ime_commit.m') - symbols = subprocess.check_output(['nm', '-g', cocoa_module], text=True) - asan_flags = ['-fsanitize=address,undefined', '-fno-omit-frame-pointer'] if '__asan_init' in symbols else [] - with tempfile.TemporaryDirectory() as tdir: - exe = os.path.join(tdir, 'cocoa_ime_commit') - cmd = ['clang', '-D_GLFW_COCOA', '-fno-objc-arc', '-framework', 'Cocoa', '-framework', 'Carbon'] - cp = subprocess.run(cmd + asan_flags + [src, '-o', exe], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=60) - self.assertEqual(cp.returncode, 0, cp.stdout) - cp = subprocess.run([exe, cocoa_module], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=30) - self.assertEqual(cp.returncode, 0, cp.stdout) - self.assertTrue(json.loads(cp.stdout)['all_passed'], cp.stdout) - def test_glfw_modules(self) -> None: from kitty.constants import glfw_path, is_macos diff --git a/kitty_tests/cocoa_ime_commit.m b/kitty_tests/cocoa_ime_commit.m deleted file mode 100644 index 93e91a0d0..000000000 --- a/kitty_tests/cocoa_ime_commit.m +++ /dev/null @@ -1,202 +0,0 @@ -// License: GPL v3 -// Headless regression test for the actual Cocoa module's IME receiver. -// Build/run instructions and fixture limits: cocoa_ime_commit.md. -#import -#import -#include -#include -#include -#undef MAX -#undef MIN -#include "../glfw/internal.h" - -static id view; -static Ivar handlerIvar; -static NSMutableData *delivered; -static NSMutableArray *results; -static NSUInteger commitCount, clearCount, previewBytes; -static BOOL allPassed = YES; - -static void -capture(GLFWwindow *window, GLFWkeyevent *event) { - (void)window; - if (event->ime_state == GLFW_IME_COMMIT_TEXT) { - ++commitCount; - if (event->text) [delivered appendBytes:event->text length:strlen(event->text)]; - } else if (event->ime_state == GLFW_IME_PREEDIT_CHANGED) { - if (!event->text || !event->text[0]) ++clearCount; - else previewBytes = strlen(event->text); - } -} - -static void -set_handler(int mode) { - memcpy((char *)view + ivar_getOffset(handlerIvar), &mode, sizeof(mode)); -} - -static NSString * -repeat(NSString *unit, NSUInteger n) { - NSMutableString *s = [NSMutableString string]; - for (NSUInteger i = 0; i < n; ++i) [s appendString:unit]; - return s; -} - -static void -reset_observed(void) { - [delivered setLength:0]; - commitCount = clearCount = 0; -} - -static void -prepare(NSString *marked) { - set_handler(0); - // Reset both the native text buffer and preedit through the real method. - [view setMarkedText:@"reset" selectedRange:NSMakeRange(5, 0) replacementRange:NSMakeRange(NSNotFound, 0)]; - [view setMarkedText:marked selectedRange:NSMakeRange(marked.length, 0) replacementRange:NSMakeRange(NSNotFound, 0)]; - reset_observed(); -} - -static void -insert(id text) { - [view insertText:text replacementRange:NSMakeRange(NSNotFound, 0)]; -} - -static void -check(NSString *name, NSString *expected, NSUInteger commits, NSUInteger clears, BOOL marked) { - NSData *bytes = [expected dataUsingEncoding:NSUTF8StringEncoding]; - BOOL same = [delivered isEqualToData:bytes]; - BOOL passed = same && commitCount == commits && clearCount == clears && [view hasMarkedText] == marked; - allPassed &= passed; - [results addObject:@{ - @"case" : name, - @"expected_utf8_bytes" : @(bytes.length), - @"committed_utf8_bytes" : @(delivered.length), - @"commit_events" : @(commitCount), - @"preedit_clear_events" : @(clearCount), - @"has_marked_text_after" : @([view hasMarkedText]), - @"content_matches" : @(same), - @"passed" : @(passed) - }]; -} - -int -main(int argc, const char **argv) { - @autoreleasepool { - if (argc != 2) { - fprintf(stderr, "usage: cocoa_ime_commit /path/to/glfw-cocoa.so\n"); - return 2; - } - void *module = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); - if (!module) { - fprintf(stderr, "dlopen: %s\n", dlerror()); - return 2; - } - Class klass = objc_getClass("GLFWContentView"); - Ivar windowIvar = class_getInstanceVariable(klass, "window"); - Ivar markedIvar = class_getInstanceVariable(klass, "markedText"); - handlerIvar = class_getInstanceVariable(klass, "in_key_handler"); - if (!klass || !windowIvar || !markedIvar || !handlerIvar) { - fprintf(stderr, "native receiver layout missing\n"); - return 2; - } - view = class_createInstance(klass, 0); - _GLFWwindow *fixtureWindow = calloc(1, sizeof(*fixtureWindow)); - if (!view || !fixtureWindow) return 2; - fixtureWindow->callbacks.keyboard = capture; - memcpy((char *)view + ivar_getOffset(windowIvar), &fixtureWindow, sizeof(fixtureWindow)); - object_setIvar(view, markedIvar, [[NSMutableAttributedString alloc] init]); - delivered = [[NSMutableData alloc] init]; - results = [[NSMutableArray alloc] init]; - - NSArray *cases = @[ - @[ @"ascii-510", repeat(@"a", 510) ], - @[ @"ascii-511", repeat(@"a", 511) ], - @[ @"ascii-512", repeat(@"a", 512) ], - @[ @"ascii-513", repeat(@"a", 513) ], - @[ @"han-170-bytes-510", repeat(@"文", 170) ], - @[ @"han-171-bytes-513", repeat(@"文", 171) ], - @[ @"mixed-bytes-511", [repeat(@"文", 170) stringByAppendingString:@"a"] ], - @[ @"mixed-bytes-512", [repeat(@"文", 170) stringByAppendingString:@"ab"] ], - @[ @"han-383-bytes-1149", repeat(@"文", 383) ], - @[ @"emoji-bytes-512", repeat(@"😀", 128) ], - @[ @"han-bytes-90000", repeat(@"文", 30000) ], - @[ @"short-after-long", @"done" ] - ]; - for (NSArray *c in cases) { - NSString *input = c[1]; - prepare(input); - BOOL previewMatches = previewBytes == [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - insert(input); - check(c[0], input, 1, 1, NO); - if (!previewMatches) { - allPassed = NO; - fprintf(stderr, "Full preedit was not delivered for %s\n", [c[0] UTF8String]); - } - } - - prepare(@""); - NSString *unmarked = repeat(@"x", 2048); - insert(unmarked); - check(@"unmarked-long", unmarked, 1, 0, NO); - - prepare(@"候选"); - NSString *attributedText = repeat(@"文😀e\u0301\n", 128); - insert([[[NSAttributedString alloc] initWithString:attributedText] autorelease]); - check(@"attributed-long", attributedText, 1, 1, NO); - - prepare(@""); - insert(@"甲"); - insert(@"乙"); - check(@"separate-event-loop-commits", @"甲乙", 2, 0, NO); - - prepare(@"待确认"); - insert(nil); - check(@"nil-keeps-preedit", @"", 0, 0, YES); - insert(@""); - check(@"empty-keeps-preedit", @"", 0, 0, YES); - insert(@"\n"); - check(@"control-keeps-preedit", @"", 0, 0, YES); - insert(@"\x7f"); - check(@"delete-keeps-preedit", @"", 0, 0, YES); - - // Simulate insertText callbacks while keyDown is collecting text. They - // must not bypass the surrounding key's dispatch by emitting commits. - prepare(@""); - set_handler(1); - insert(@"甲"); - insert(@"乙"); - check(@"key-handler-defers-two-callbacks", @"", 0, 0, NO); - // Use the existing modifier branch to observe the pending accumulator; - // this is a fixture probe, not a simulated physical modifier gesture. - set_handler(2); - insert(@"丙"); - check(@"existing-buffer-still-accumulates", @"甲乙丙", 1, 0, NO); - - prepare(@"候选"); - set_handler(1); - insert(@"已确认"); - check(@"key-handler-unmarks-without-early-commit", @"", 0, 0, NO); - - // The narrowly scoped fix intentionally leaves modifier buffering alone. - prepare(@""); - set_handler(2); - insert(repeat(@"m", 511)); - check(@"modifier-short-behavior", repeat(@"m", 511), 1, 0, NO); - prepare(@""); - set_handler(2); - insert(repeat(@"m", 512)); - check(@"modifier-overflow-unchanged", @"", 0, 0, NO); - - NSDictionary *report = @{ - @"native_module" : @(argv[1]), - @"all_passed" : @(allPassed), - @"cases" : results, - @"capture_boundary" : @"native GLFW keyboard callback; no input method, GUI or terminal child", - @"key_handler_tests" : @"receiver callback state fixtures; not full physical key dispatch" - }; - NSData *json = [NSJSONSerialization dataWithJSONObject:report options:NSJSONWritingPrettyPrinted error:nil]; - fwrite(json.bytes, 1, json.length, stdout); - fputc('\n', stdout); - return allPassed ? 0 : 1; - } -} diff --git a/kitty_tests/cocoa_ime_commit.md b/kitty_tests/cocoa_ime_commit.md deleted file mode 100644 index e8d377650..000000000 --- a/kitty_tests/cocoa_ime_commit.md +++ /dev/null @@ -1,40 +0,0 @@ -# Cocoa IME commit regression - -This standalone macOS test calls the actual `GLFWContentView` methods from a -specified `glfw-cocoa.so`. It does not start an input method or open a window. -After building from source, run it through the normal test entry point: - -```sh -./test.py macos_ime_commit -``` - -The runner skips this test outside macOS or if `clang` is unavailable. It also -links the probe with the sanitizer runtime when the Cocoa module uses ASan. -For a standalone run against an unsanitized module from a matching checkout: - -```sh -mkdir -p build/ime-test -clang -D_GLFW_COCOA -fno-objc-arc -framework Cocoa -framework Carbon \ - kitty_tests/cocoa_ime_commit.m -o build/ime-test/cocoa_ime_commit -build/ime-test/cocoa_ime_commit "$PWD/kitty/glfw-cocoa.so" -``` - -The test covers UTF-8 length boundaries, long Chinese and emoji commits, -attributed strings with combining characters and embedded newlines, -preedit clearing, separate callbacks, and existing control/empty input handling. -It also checks that key-handler callbacks continue staging text and that the -modifier branch retains its previous behavior, including its existing limit. -The change under test only removes the limit on event-loop commits. - -The fixture allocates the native receiver without initializing NSView/window -lifecycle, and supplies only the ivars and keyboard callback used by the tested -methods. These small fixture objects deliberately live until process exit; -NSView destruction is not invoked on the uninitialized view. No NSApplication, -NSWindow, input context, Accessibility action or synthetic GUI event is created. - -Callback mode fixtures test staging within `insertText`, not a complete physical -keyDown/flagsChanged sequence. Delivery is observed at the native GLFW callback, -not at a terminal child or editor. Run each native module in a separate process -to avoid Objective-C class-name collisions. Against the pre-fix module this test -must fail the oversized event-loop cases; against the repaired module it must -report `all_passed: true` and exit zero.