Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/platform/mac/menus_tests.rs
StratoSDK / crates / strato-ui-renderer / src / platform / mac / menus_tests.rs
1//! Memory-behavior repros for APP-4154 batch 1.C (strato_ui-platform-nsstring).
2//!
3//! Covers two kinds of fix in `menus.rs`:
4//!
5//! 1. Retain → autorelease conversion (line 298, `make_menu_item` standard
6//! action): the key-equivalent NSString used to be `NSString::alloc(nil)
7//! .init_str(...)`, which returns a +1 retained reference. The PR uses
8//! `make_nsstring`, which autoreleases. Covered by
9//! [`make_menu_item_standard_action_memory_behavior`].
10//!
11//! 2. Local `NSAutoreleasePool` wrapper around `apply_changes` body. The
12//! NSString temporaries produced by `make_nsstring(name)` and inside
13//! `resolve_key_equivalent` used to go into whatever ambient pool AppKit
14//! had set up (or leak if called from a Rust thread with no active pool).
15//! The PR drains them per call. Covered by
16//! [`apply_changes_local_pool_memory_behavior`], which deliberately runs
17//! WITHOUT any outer `NSAutoreleasePool` so the local-pool drain is the
18//! only thing that can release the temporaries.
19use cocoa::appkit::NSMenuItem;
20use cocoa::base::nil;
21use cocoa::foundation::NSAutoreleasePool;
22use objc::runtime::Object;
23use objc::{msg_send, sel, sel_impl};
24use strato_ui_core::actions::StandardAction;
25use strato_ui_core::keymap::Keystroke;
26use strato_ui_core::platform::menu::{MenuItem, MenuItemPropertyChanges};
27 
28use super::{apply_changes, make_menu_item};
29 
30/// How many outer pool cycles for the retain → autorelease test.
31const MENU_ITEM_OUTER: usize = 40;
32/// Inner iterations per outer cycle. Each one allocates one NSMenuItem plus
33/// (on master) one retained NSString for the key equivalent.
34const MENU_ITEM_INNER: usize = 10_000;
35 
36/// Driver for the local-pool wrapper test. `apply_changes` creates a handful
37/// of NSString temporaries per call; without an outer pool, master accumulates
38/// them all, while the branch drains them per iteration.
39const APPLY_CHANGES_ITERS: usize = 200_000;
40 
41/// Reproduces the per-call NSString leak fixed by switching the key-equivalent
42/// argument to `make_nsstring` on line 298. Each outer cycle gets its own
43/// autorelease pool; the branch reclaims everything on drain, master keeps the
44/// retained key-equivalent strings alive.
45#[test]
46fn make_menu_item_standard_action_memory_behavior() {
47 unsafe {
48 for _ in 0..MENU_ITEM_OUTER {
49 let pool = NSAutoreleasePool::new(nil);
50 for _ in 0..MENU_ITEM_INNER {
51 // `Quit` has a non-empty key equivalent ("q"); `Close Window`
52 // has an empty one. Mix the two so we cover both branches.
53 let _ = make_menu_item(MenuItem::Standard(StandardAction::Quit));
54 let _ = make_menu_item(MenuItem::Standard(StandardAction::Close));
55 }
56 pool.drain();
57 }
58 }
59}
60 
61/// Reproduces the accumulation that the `apply_changes` local pool prevents.
62/// Note the deliberate absence of an outer `NSAutoreleasePool` — this is what
63/// makes the local-pool wrapper observable.
64#[test]
65fn apply_changes_local_pool_memory_behavior() {
66 unsafe {
67 // Hold a single menu item for the entire loop so that the only growth
68 // we measure is the NSString temporaries inside `apply_changes`, not
69 // the menu item objects themselves.
70 let outer_pool = NSAutoreleasePool::new(nil);
71 let item: *mut Object = msg_send![NSMenuItem::alloc(nil), init];
72 // Retain so we can freely drain the outer pool after constructing it.
73 let _: *mut Object = msg_send![item, retain];
74 outer_pool.drain();
75 
76 for _ in 0..APPLY_CHANGES_ITERS {
77 let changes = MenuItemPropertyChanges {
78 name: Some("Warp Menu Item".to_string()),
79 keystroke: Some(Some(Keystroke {
80 cmd: true,
81 key: "k".to_string(),
82 ..Default::default()
83 })),
84 disabled: Some(false),
85 checked: Some(false),
86 submenu: None,
87 };
88 apply_changes(changes, item);
89 }
90 
91 // Balance the manual retain above.
92 let _: () = msg_send![item, release];
93 }
94}
95