[iOS] Fix ArgumentOutOfRangeException in ObservableGroupedSource when removing a group from grouped CollectionView#34692
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34692Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34692" |
|
@dotnet-policy-service agree company="audius GmbH" |
bf07aeb to
aacbd45
Compare
…hen removing a group on iOS Skip the pre-processing UpdateSection call for Remove actions. Standard INCC semantics require the item to already be removed from the backing collection before CollectionChanged fires. This means _groupSource has N-1 items when MAUI receives CollectionChanged(Remove), but UIKit still has N sections. Calling UpdateSection at this point iterates all N of UIKit's sections and calls GetGroupCount(N-1) -> _groupSource[N-1] -> ArgumentOutOfRangeException. The post-processing UpdateSection call (after DeleteSections) already handles reconciliation. Also add a defensive bounds check in GetGroupCount to guard against other re-entrancy scenarios where UIKit calls back synchronously with a stale section index during DeleteSections. Fixes: dotnet#34691
… on iOS Adds three device tests to CollectionViewTests.iOS.cs verifying that removing sections from a grouped CollectionView does not throw ArgumentOutOfRangeException (issue dotnet#34691): - ItemsSourceGroupedRemoveLastSectionDoesNotCrash: removes the last group, which was the classic crash case where _groupSource[N-1] was accessed with only N-1 items. - ItemsSourceGroupedRemoveFirstSectionDoesNotCrash: removes from the beginning/middle. - ItemsSourceGroupedRemoveAllSectionsOneByOneDoesNotCrash: removes all groups one by one, exercising the full range of section indices.
aacbd45 to
de54a54
Compare
🚦 Gate - Test Before and After Fix📊 Expand Full Gate —
|
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
📱 CollectionViewTests (ItemsSourceGroupedRemoveLastSectionDoesNotCrash, ItemsSourceGroupedRemoveFirstSectionDoesNotCrash, ItemsSourceGroupedRemoveAllSectionsOneByOneDoesNotCrash) Category=CollectionView |
❌ PASS — 486s | ✅ PASS — 204s |
🔴 Without fix — 📱 CollectionViewTests (ItemsSourceGroupedRemoveLastSectionDoesNotCrash, ItemsSourceGroupedRemoveFirstSectionDoesNotCrash, ItemsSourceGroupedRemoveAllSectionsOneByOneDoesNotCrash): PASS ❌ · 486s
(truncated to last 15,000 chars)
Run + 1208
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x0000000182d009e8 CoreFoundation`CFRunLoopRunSpecific + 572
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x00000001842d0c78 Foundation`-[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 212
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x00000001843443a4 Foundation`-[NSRunLoop(NSRunLoop) runUntilDate:] + 100
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x00000001029d6f28 mlaunch`xamarin_dyn_objc_msgSend + 160
�[40m�[37mdbug�[39m�[22m�[49m: frame #10: 0x000000010820e68c
�[40m�[37mdbug�[39m�[22m�[49m: frame #11: 0x0000000108574008
�[40m�[37mdbug�[39m�[22m�[49m: frame #12: 0x0000000108207f6c
�[40m�[37mdbug�[39m�[22m�[49m: frame #13: 0x00000001081a10b4
�[40m�[37mdbug�[39m�[22m�[49m: frame #14: 0x00000001079bcd34
�[40m�[37mdbug�[39m�[22m�[49m: frame #15: 0x0000000104894c04 libcoreclr.dylib`CallDescrWorkerInternal + 132
�[40m�[37mdbug�[39m�[22m�[49m: frame #16: 0x0000000104712d30 libcoreclr.dylib`MethodDescCallSite::CallTargetWorker(unsigned long long const*, unsigned long long*, int) + 836
�[40m�[37mdbug�[39m�[22m�[49m: frame #17: 0x0000000104619350 libcoreclr.dylib`RunMain(MethodDesc*, short, int*, PtrArray**) + 648
�[40m�[37mdbug�[39m�[22m�[49m: frame #18: 0x0000000104619688 libcoreclr.dylib`Assembly::ExecuteMainMethod(PtrArray**, int) + 264
�[40m�[37mdbug�[39m�[22m�[49m: frame #19: 0x000000010464129c libcoreclr.dylib`CorHost2::ExecuteAssembly(unsigned int, char16_t const*, int, char16_t const**, unsigned int*) + 640
�[40m�[37mdbug�[39m�[22m�[49m: frame #20: 0x0000000104607650 libcoreclr.dylib`coreclr_execute_assembly + 232
�[40m�[37mdbug�[39m�[22m�[49m: frame #21: 0x00000001029d2140 mlaunch`mono_jit_exec + 204
�[40m�[37mdbug�[39m�[22m�[49m: frame #22: 0x00000001029d5ecc mlaunch`xamarin_main + 884
�[40m�[37mdbug�[39m�[22m�[49m: frame #23: 0x00000001029d71f4 mlaunch`main + 64
�[40m�[37mdbug�[39m�[22m�[49m: frame #24: 0x0000000182876b98 dyld`start + 6076
�[40m�[37mdbug�[39m�[22m�[49m: thread #2
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd5c34 libsystem_kernel.dylib`mach_msg2_trap + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000182be83a0 libsystem_kernel.dylib`mach_msg2_internal + 76
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x0000000182bde764 libsystem_kernel.dylib`mach_msg_overwrite + 484
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x0000000182bd5fa8 libsystem_kernel.dylib`mach_msg + 24
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x00000001046052f4 libcoreclr.dylib`MachMessage::Receive(unsigned int) + 80
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x000000010460461c libcoreclr.dylib`SEHExceptionThread(void*) + 164
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #3, name = '.NET SynchManager'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bdbd04 libsystem_kernel.dylib`kevent + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x00000001045f9304 libcoreclr.dylib`CorUnix::CPalSynchronizationManager::ReadBytesFromProcessPipe(int, unsigned char*, int) + 484
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x00000001045f89f0 libcoreclr.dylib`CorUnix::CPalSynchronizationManager::WorkerThread(void*) + 164
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x00000001046020fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #4
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd78b0 libsystem_kernel.dylib`__workq_kernreturn + 8
�[40m�[37mdbug�[39m�[22m�[49m: thread #5, name = '.NET EventPipe'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bde498 libsystem_kernel.dylib`poll + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x00000001048f4e90 libcoreclr.dylib`ds_ipc_poll(_DiagnosticsIpcPollHandle*, unsigned long, unsigned int, void (*)(char const*, unsigned int)) + 172
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x00000001049a2bb0 libcoreclr.dylib`ds_ipc_stream_factory_get_next_available_stream(void (*)(char const*, unsigned int)) + 756
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x00000001049a0a68 libcoreclr.dylib`server_thread(void*) + 372
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x00000001046020fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #6, name = '.NET DebugPipe'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd6678 libsystem_kernel.dylib`__open + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000182be16a4 libsystem_kernel.dylib`open + 64
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x00000001048f5a84 libcoreclr.dylib`TwoWayPipe::WaitForConnection() + 40
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x00000001048f0578 libcoreclr.dylib`DbgTransportSession::TransportWorker() + 232
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x00000001048ef5c8 libcoreclr.dylib`DbgTransportSession::TransportWorkerStatic(void*) + 40
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x00000001046020fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #7, name = '.NET Debugger'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd93cc libsystem_kernel.dylib`__psynch_cvwait + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000182c1809c libsystem_pthread.dylib`_pthread_cond_wait + 984
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x00000001045f6f6c libcoreclr.dylib`CorUnix::CPalSynchronizationManager::ThreadNativeWait(CorUnix::_ThreadNativeWaitData*, unsigned int, CorUnix::ThreadWakeupReason*, unsigned int*) + 320
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x00000001045f6bec libcoreclr.dylib`CorUnix::CPalSynchronizationManager::BlockThread(CorUnix::CPalThread*, unsigned int, bool, bool, CorUnix::ThreadWakeupReason*, unsigned int*) + 380
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x00000001045fb0cc libcoreclr.dylib`CorUnix::InternalWaitForMultipleObjectsEx(CorUnix::CPalThread*, unsigned int, void* const*, int, unsigned int, int, int) + 1600
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x00000001048edda8 libcoreclr.dylib`DebuggerRCThread::MainLoop() + 228
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x00000001048edc70 libcoreclr.dylib`DebuggerRCThread::ThreadProc() + 256
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x00000001048eda24 libcoreclr.dylib`DebuggerRCThread::ThreadProcStatic(void*) + 56
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x00000001046020fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #8
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd93cc libsystem_kernel.dylib`__psynch_cvwait + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000182c1809c libsystem_pthread.dylib`_pthread_cond_wait + 984
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x00000001045f6f6c libcoreclr.dylib`CorUnix::CPalSynchronizationManager::ThreadNativeWait(CorUnix::_ThreadNativeWaitData*, unsigned int, CorUnix::ThreadWakeupReason*, unsigned int*) + 320
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x00000001045f6bec libcoreclr.dylib`CorUnix::CPalSynchronizationManager::BlockThread(CorUnix::CPalThread*, unsigned int, bool, bool, CorUnix::ThreadWakeupReason*, unsigned int*) + 380
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x00000001045fb0cc libcoreclr.dylib`CorUnix::InternalWaitForMultipleObjectsEx(CorUnix::CPalThread*, unsigned int, void* const*, int, unsigned int, int, int) + 1600
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x0000000104748078 libcoreclr.dylib`FinalizerThread::WaitForFinalizerEvent(CLREvent*) + 240
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x00000001047481d8 libcoreclr.dylib`FinalizerThread::FinalizerThreadWorker(void*) + 264
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x00000001046e5fa8 libcoreclr.dylib`ManagedThreadBase_DispatchOuter(ManagedThreadCallState*) + 248
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x00000001046e648c libcoreclr.dylib`ManagedThreadBase::FinalizerBase(void (*)(void*)) + 36
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x0000000104748350 libcoreclr.dylib`FinalizerThread::FinalizerThreadStart(void*) + 88
�[40m�[37mdbug�[39m�[22m�[49m: frame #10: 0x00000001046020fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #11: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #9, name = '.NET SigHandler'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd67dc libsystem_kernel.dylib`read + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000102a90e98 libSystem.Native.dylib`SignalHandlerLoop + 96
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #10
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bdbd04 libsystem_kernel.dylib`kevent + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000102a8f4a4 libSystem.Native.dylib`SystemNative_WaitForSocketEvents + 80
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x000000010831cc1c
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x000000010831c804
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x000000010831c5bc
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x00000001082014c0
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x0000000108201218
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x0000000108201108
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x0000000104894c04 libcoreclr.dylib`CallDescrWorkerInternal + 132
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x0000000104712988 libcoreclr.dylib`DispatchCallSimple(unsigned long*, unsigned int, unsigned long long, unsigned int) + 268
�[40m�[37mdbug�[39m�[22m�[49m: frame #10: 0x0000000104724c6c libcoreclr.dylib`ThreadNative::KickOffThread_Worker(void*) + 148
�[40m�[37mdbug�[39m�[22m�[49m: frame #11: 0x00000001046e5fa8 libcoreclr.dylib`ManagedThreadBase_DispatchOuter(ManagedThreadCallState*) + 248
�[40m�[37mdbug�[39m�[22m�[49m: frame #12: 0x00000001046e645c libcoreclr.dylib`ManagedThreadBase::KickOff(void (*)(void*), void*) + 32
�[40m�[37mdbug�[39m�[22m�[49m: frame #13: 0x0000000104724d44 libcoreclr.dylib`ThreadNative::KickOffThread(void*) + 172
�[40m�[37mdbug�[39m�[22m�[49m: frame #14: 0x00000001046020fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #15: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #11
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd5c34 libsystem_kernel.dylib`mach_msg2_trap + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000182be83a0 libsystem_kernel.dylib`mach_msg2_internal + 76
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x0000000182bde764 libsystem_kernel.dylib`mach_msg_overwrite + 484
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x0000000182bd5fa8 libsystem_kernel.dylib`mach_msg + 24
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x0000000182d02c0c CoreFoundation`__CFRunLoopServiceMachPort + 160
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x0000000182d01528 CoreFoundation`__CFRunLoopRun + 1208
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x0000000182d009e8 CoreFoundation`CFRunLoopRunSpecific + 572
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x00000001842d0c78 Foundation`-[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 212
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x00000001029d6f28 mlaunch`xamarin_dyn_objc_msgSend + 160
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x000000010856a47c
�[40m�[37mdbug�[39m�[22m�[49m: frame #10: 0x000000010856a340
�[40m�[37mdbug�[39m�[22m�[49m: frame #11: 0x000000010856a174
�[40m�[37mdbug�[39m�[22m�[49m: frame #12: 0x00000001085670f8
�[40m�[37mdbug�[39m�[22m�[49m: frame #13: 0x0000000108201468
�[40m�[37mdbug�[39m�[22m�[49m: frame #14: 0x0000000108201218
�[40m�[37mdbug�[39m�[22m�[49m: frame #15: 0x0000000108201108
�[40m�[37mdbug�[39m�[22m�[49m: frame #16: 0x0000000104894c04 libcoreclr.dylib`CallDescrWorkerInternal + 132
�[40m�[37mdbug�[39m�[22m�[49m: frame #17: 0x0000000104712988 libcoreclr.dylib`DispatchCallSimple(unsigned long*, unsigned int, unsigned long long, unsigned int) + 268
�[40m�[37mdbug�[39m�[22m�[49m: frame #18: 0x0000000104724c6c libcoreclr.dylib`ThreadNative::KickOffThread_Worker(void*) + 148
�[40m�[37mdbug�[39m�[22m�[49m: frame #19: 0x00000001046e5fa8 libcoreclr.dylib`ManagedThreadBase_DispatchOuter(ManagedThreadCallState*) + 248
�[40m�[37mdbug�[39m�[22m�[49m: frame #20: 0x00000001046e645c libcoreclr.dylib`ManagedThreadBase::KickOff(void (*)(void*), void*) + 32
�[40m�[37mdbug�[39m�[22m�[49m: frame #21: 0x0000000104724d44 libcoreclr.dylib`ThreadNative::KickOffThread(void*) + 172
�[40m�[37mdbug�[39m�[22m�[49m: frame #22: 0x00000001046020fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #23: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #12, name = 'com.apple.CFSocket.private'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182be0c2c libsystem_kernel.dylib`__select + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000182d28a80 CoreFoundation`__CFSocketManager + 704
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #13
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd78b0 libsystem_kernel.dylib`__workq_kernreturn + 8
�[40m�[37mdbug�[39m�[22m�[49m: (lldb) detach
�[40m�[37mdbug�[39m�[22m�[49m: Process 9458 detached
�[40m�[37mdbug�[39m�[22m�[49m: (lldb) quit
�[40m�[37mdbug�[39m�[22m�[49m: 9458 Execution timed out after 60 seconds and the process was killed.
�[40m�[37mdbug�[39m�[22m�[49m: Process mlaunch exited with 137
�[40m�[37mdbug�[39m�[22m�[49m: Failed to list crash reports from device.
�[40m�[37mdbug�[39m�[22m�[49m: Test run started but crashed and no test results were reported
�[40m�[37mdbug�[39m�[22m�[49m: No crash reports, waiting 30 seconds for the crash report service...
�[41m�[30mfail�[39m�[22m�[49m: Application test run crashed
Failed to launch the application, please try again. If the problem persists, try rebooting MacOS
�[40m�[32minfo�[39m�[22m�[49m: Uninstalling the application 'com.microsoft.maui.controls.devicetests' from 'iPhone 11 Pro'
�[40m�[37mdbug�[39m�[22m�[49m:
�[40m�[37mdbug�[39m�[22m�[49m: Running /Applications/Xcode_26.1.1.app/Contents/Developer/usr/bin/simctl
�[40m�[37mdbug�[39m�[22m�[49m: An error was encountered processing the command (domain=com.apple.CoreSimulator.SimError, code=405):
�[40m�[37mdbug�[39m�[22m�[49m: Unable to lookup in current state: Shutdown
�[40m�[37mdbug�[39m�[22m�[49m: Process simctl exited with 149
�[41m�[30mfail�[39m�[22m�[49m: Failed to uninstall the app bundle! Check logs for more details!
XHarness exit code: 83 (APP_LAUNCH_FAILURE)
Passed: 0
Failed: 0
Tests completed with exit code: 83
🟢 With fix — 📱 CollectionViewTests (ItemsSourceGroupedRemoveLastSectionDoesNotCrash, ItemsSourceGroupedRemoveFirstSectionDoesNotCrash, ItemsSourceGroupedRemoveAllSectionsOneByOneDoesNotCrash): PASS ✅ · 204s
Determining projects to restore...
All projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13750190
Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Release/net10.0-ios26.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13750190
Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Release/net10.0-ios26.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13750190
Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Release/net10.0-ios26.0/Microsoft.Maui.dll
Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Release/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13750190
Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Release/net10.0-ios26.0/Microsoft.Maui.Controls.dll
TestUtils.DeviceTests -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/TestUtils.DeviceTests/Release/net10.0-ios/Microsoft.Maui.TestUtils.DeviceTests.dll
##vso[build.updatebuildnumber]10.0.60-ci+azdo.13750190
Controls.Xaml -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Xaml/Release/net10.0-ios26.0/Microsoft.Maui.Controls.Xaml.dll
TestUtils.DeviceTests.Runners -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/TestUtils.DeviceTests.Runners/Release/net10.0-ios/Microsoft.Maui.TestUtils.DeviceTests.Runners.dll
Core.DeviceTests.Shared -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core.DeviceTests.Shared/Release/net10.0-ios/Microsoft.Maui.DeviceTests.Shared.dll
TestUtils.DeviceTests.Runners.SourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/TestUtils.DeviceTests.Runners.SourceGen/Release/netstandard2.0/Microsoft.Maui.TestUtils.DeviceTests.Runners.SourceGen.dll
Detected signing identity:
Code Signing Key: "" (-)
Provisioning Profile: "" () - no entitlements
Bundle Id: com.microsoft.maui.controls.devicetests
App Id: com.microsoft.maui.controls.devicetests
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Elements/CollectionView/CollectionViewTests.iOS.cs(121,5): warning xUnit2013: Do not use Assert.Equal() to check for collection size. Use Assert.Single instead. (https://xunit.net/xunit.analyzers/rules/xUnit2013) [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj::TargetFramework=net10.0-ios]
Controls.DeviceTests -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.dll
Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
Optimizing assemblies for size. This process might take a while.
IL stripping assemblies
Build succeeded.
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Elements/CollectionView/CollectionViewTests.iOS.cs(121,5): warning xUnit2013: Do not use Assert.Equal() to check for collection size. Use Assert.Single instead. (https://xunit.net/xunit.analyzers/rules/xUnit2013) [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj::TargetFramework=net10.0-ios]
1 Warning(s)
0 Error(s)
Time Elapsed 00:02:28.24
[11.0.0-prerelease.26107.1+bfbac237157e59cdbd19334325b2af80bd6e9828] XHarness command issued: apple test --app artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.app --target ios-simulator-64_18.6 --device 61D65259-33D1-401B-AACB-B08BD2DD9597 -o artifacts/log --timeout 01:00:00 -v --set-env=TestFilter=Category=CollectionView
�[40m�[32minfo�[39m�[22m�[49m: Preparing run for ios-simulator-64_18.6 targeting 61D65259-33D1-401B-AACB-B08BD2DD9597
�[40m�[32minfo�[39m�[22m�[49m: Looking for available ios-simulator-64_18.6 simulators..
�[40m�[37mdbug�[39m�[22m�[49m: Looking for available ios-simulator-64_18.6 simulators. Storing logs into list-ios-simulator-64_18.6-20260405_111627.log
�[40m�[32minfo�[39m�[22m�[49m: Found simulator device 'iPhone 11 Pro'
�[40m�[32minfo�[39m�[22m�[49m: Getting app bundle information from '/Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.app'..
�[40m�[37mdbug�[39m�[22m�[49m:
�[40m�[37mdbug�[39m�[22m�[49m: Running /usr/libexec/PlistBuddy
�[40m�[37mdbug�[39m�[22m�[49m: Process PlistBuddy exited with 0
�[40m�[37mdbug�[39m�[22m�[49m:
�[40m�[37mdbug�[39m�[22m�[49m: Running /usr/libexec/PlistBuddy
�[40m�[37mdbug�[39m�[22m�[49m: Process PlistBuddy exited with 0
�[40m�[37mdbug�[39m�[22m�[49m:
�[40m�[37mdbug�[39m�[22m�[49m: Running /usr/libexec/PlistBuddy
�[40m�[37mdbug�[39m�[22m�[49m: Process PlistBuddy exited with 0
�[40m�[37mdbug�[39m�[22m�[49m:
�[40m�[37mdbug�[39m�[22m�[49m: Running /usr/libexec/PlistBuddy
�[40m�[37mdbug�[39m�[22m�[49m: Process PlistBuddy exited with 0
�[40m�[32minfo�[39m�[22m�[49m: Uninstalling any previous instance of 'com.microsoft.maui.controls.devicetests' from 'iPhone 11 Pro'
�[40m�[37mdbug�[39m�[22m�[49m:
�[40m�[37mdbug�[39m�[22m�[49m: Running /Applications/Xcode_26.1.1.app/Contents/Developer/usr/bin/simctl
�[40m�[37mdbug�[39m�[22m�[49m: Process simctl exited with 0
�[40m�[32minfo�[39m�[22m�[49m: Application 'com.microsoft.maui.controls.devicetests' was uninstalled successfully
�[40m�[32minfo�[39m�[22m�[49m: Installing application 'Microsoft.Maui.Controls.DeviceTests' on 'iPhone 11 Pro'
�[40m�[37mdbug�[39m�[22m�[49m: Installing '/Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.app' to 'iPhone 11 Pro' (142.15 MB)
�[40m�[37mdbug�[39m�[22m�[49m:
�[40m�[37mdbug�[39m�[22m�[49m: Running /Users/cloudtest/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26107.1/tools/net10.0/any/../../../runtimes/any/native/mlaunch/bin/mlaunch
�[40m�[37mdbug�[39m�[22m�[49m: Using Xcode 26.1.1 found in /Applications/Xcode_26.1.1.app
�[40m�[37mdbug�[39m�[22m�[49m: xcrun simctl list --json --json-output /tmp/tmpzwNzID.tmp
�[40m�[37mdbug�[39m�[22m�[49m: Xamarin.Hosting: No need to boot (already booted): iPhone 11 Pro
�[40m�[37mdbug�[39m�[22m�[49m: Xamarin.Hosting: Installing on iPhone 11 Pro (61D65259-33D1-401B-AACB-B08BD2DD9597) by executing 'xcrun simctl install 61D65259-33D1-401B-AACB-B08BD2DD9597 /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.app'
�[40m�[37mdbug�[39m�[22m�[49m: Xamarin.Hosting: The bundle id com.microsoft.maui.controls.devicetests was successfully installed.
�[40m�[37mdbug�[39m�[22m�[49m: Process mlaunch exited with 0
�[40m�[32minfo�[39m�[22m�[49m: Application 'Microsoft.Maui.Controls.DeviceTests' was installed successfully on 'iPhone 11 Pro'
�[40m�[32minfo�[39m�[22m�[49m: Starting test run for com.microsoft.maui.controls.devicetests..
�[40m�[37mdbug�[39m�[22m�[49m: Test log server listening on: 0.0.0.0:56972
�[40m�[37mdbug�[39m�[22m�[49m: *** Executing 'Microsoft.Maui.Controls.DeviceTests' on ios-simulator-64_18.6 'iPhone 11 Pro' ***
�[40m�[37mdbug�[39m�[22m�[49m: System log for the 'iPhone 11 Pro' simulator is: /Users/cloudtest/Library/Logs/CoreSimulator/61D65259-33D1-401B-AACB-B08BD2DD9597/system.log
�[40m�[37mdbug�[39m�[22m�[49m: Simulator 'iPhone 11 Pro' is already booted
�[40m�[37mdbug�[39m�[22m�[49m: Scanning log stream for Microsoft.Maui.Controls.DeviceTests into '/Users/cloudtest/vss/_work/1/s/artifacts/log/Microsoft.Maui.Controls.DeviceTests.log'..
�[40m�[37mdbug�[39m�[22m�[49m:
�[40m�[37mdbug�[39m�[22m�[49m: Running /Applications/Xcode_26.1.1.app/Contents/Developer/usr/bin/simctl
�[40m�[37mdbug�[39m�[22m�[49m: Launching the app
�[40m�[37mdbug�[39m�[22m�[49m:
�[40m�[37mdbug�[39m�[22m�[49m: Running /Users/cloudtest/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26107.1/tools/net10.0/any/../../../runtimes/any/native/mlaunch/bin/mlaunch
�[40m�[37mdbug�[39m�[22m�[49m: Connection from 127.0.0.1:56992 saving logs to /Users/cloudtest/vss/_work/1/s/artifacts/log/test-ios-simulator-64_18.6-20260405_111630.log
�[40m�[37mdbug�[39m�[22m�[49m: Tests have finished executing
�[40m�[37mdbug�[39m�[22m�[49m: Process mlaunch exited with 0
�[40m�[37mdbug�[39m�[22m�[49m: Test run completed
�[40m�[37mdbug�[39m�[22m�[49m:
�[40m�[37mdbug�[39m�[22m�[49m: Running /bin/bash
�[40m�[37mdbug�[39m�[22m�[49m: Process simctl exited with 137
�[40m�[37mdbug�[39m�[22m�[49m: cp: /Users/cloudtest/Library/Developer/CoreSimulator/Devices/61D65259-33D1-401B-AACB-B08BD2DD9597/data/Containers/Data/Application/E2D337C1-9763-4A92-A68A-681F12AD0934/Documents/test-results.xml: No such file or directory
�[40m�[37mdbug�[39m�[22m�[49m: Process bash exited with 1
�[40m�[37mdbug�[39m�[22m�[49m: Test run succeeded
�[40m�[37mdbug�[39m�[22m�[49m: No crash reports, waiting 0 seconds for the crash report service...
�[40m�[32minfo�[39m�[22m�[49m: Application finished the test run successfully
�[40m�[32minfo�[39m�[22m�[49m: Tests run: 46 Passed: 42 Inconclusive: 0 Failed: 0 Ignored: 4
�[40m�[32minfo�[39m�[22m�[49m: Uninstalling the application 'com.microsoft.maui.controls.devicetests' from 'iPhone 11 Pro'
�[40m�[37mdbug�[39m�[22m�[49m:
�[40m�[37mdbug�[39m�[22m�[49m: Running /Applications/Xcode_26.1.1.app/Contents/Developer/usr/bin/simctl
�[40m�[37mdbug�[39m�[22m�[49m: Process simctl exited with 0
�[40m�[32minfo�[39m�[22m�[49m: Application 'com.microsoft.maui.controls.devicetests' was uninstalled successfully
XHarness exit code: 0
Passed: 84
Failed: 0
Tests completed successfully
⚠️ Issues found
- ❌ CollectionViewTests (ItemsSourceGroupedRemoveLastSectionDoesNotCrash, ItemsSourceGroupedRemoveFirstSectionDoesNotCrash, ItemsSourceGroupedRemoveAllSectionsOneByOneDoesNotCrash) PASSED without fix (should fail) — tests don't catch the bug
📁 Fix files reverted (2 files)
eng/pipelines/ci-copilot.ymlsrc/Controls/src/Core/Handlers/Items/iOS/ObservableGroupedSource.cs
🤖 AI Summary📊 Expand Full Review —
|
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #34692 | Skip UpdateSection before Remove + defensive bounds check in GetGroupCount |
⏳ PENDING (Gate ❌ FAILED — tests pass without fix) | ObservableGroupedSource.cs |
Two-pronged: prevents crash + adds safety net |
🔧 Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-opus-4.6) | Clamp UpdateSection loop to Math.Min(numberOfSections, _groupSource.Count) |
✅ PASS (ios) | 1 file (+5/-2) | Single change; intrinsically safe for all actions |
| 2 | try-fix (claude-sonnet-4.6) | Remove pre-processing UpdateSection entirely (all actions) |
1 file (-3) | Logic sound; build env issue | |
| 3 | try-fix (gpt-5.3-codex) | Re-entrancy guard _isUpdatingSection bool; GetGroupCount returns 0 only during UpdateSection for OOB |
✅ PASS (ios) | 1 file (+17/-2) | Preserves both call sites; adds state |
| 4 | try-fix (gpt-5.4) | Temporary pre-remove section snapshot — feed UIKit old count during reconciliation | ❌ FAIL (exit 143 — test killed) | 1 file | Complex; unverified |
| PR | PR #34692 | Skip UpdateSection before Remove + defensive bounds check in GetGroupCount |
❌ Gate FAILED (tests pass without fix) | 1 fix + 1 test | Two-part; well-motivated; tests don't prove regression |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 2 | Yes | PerformBatchUpdates wrapping all UIKit mutations — UIKit-native atomicity, eliminates re-entrant callbacks. Not pursued: complex refactor; doesn't address UpdateSection probe itself |
| claude-sonnet-4.6 | 2 | Yes | Same PerformBatchUpdates idea — structural change replacing UpdateSection probing with UIKit batch protocol. Not pursued: same reasoning |
| gpt-5.3-codex | 2 | Yes | Section-consistency check before incremental update — if NumberOfSections != _groupSource.Count, fallback to ReloadData. Not pursued: loses section animations; broader than needed |
Exhausted: Yes — solution space covered (skip, clamp, guard, delete, snapshot all tried; PerformBatchUpdates is a refactor beyond targeted fix scope)
Selected Fix: Attempt 1 (clamp UpdateSection loop to Math.Min(numberOfSections, _groupSource.Count)) — simplest single-location change, intrinsically safe for all actions without conditional logic or new state. Alternatively, the PR's two-part fix is more targeted (Remove-only skip + safety net).
📋 Report — Final Recommendation
⚠️ Final Recommendation: REQUEST CHANGES
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | iOS only; Items/ handler; 1 fix file + 1 test file |
| Gate | ❌ FAILED | Tests PASS even without fix — regression not proven |
| Try-Fix | ✅ COMPLETE | 4 attempts; 2 passing alternatives found (Attempts 1 & 3) |
| Report | ✅ COMPLETE |
Summary
The PR fixes a real and well-diagnosed crash (ArgumentOutOfRangeException in ObservableGroupedSource.GetGroupCount on iOS when removing a group from a grouped CollectionView). The root cause analysis is correct and the code fix is sound. However, the gate failed: the three regression tests added by the PR pass even without the fix, meaning they do not prove the fix addresses the crash. This is the primary blocker.
Try-Fix found two independently passing alternatives, confirming the general approach space is valid. The simplest alternative (Attempt 1: clamp UpdateSection loop bound) is a single-location change that's arguably cleaner than the PR's two-part approach.
Root Cause
ObservableGroupedSource.CollectionChanged calls UpdateSection(collectionView) before the Remove action is processed. Per INotifyCollectionChanged semantics, _groupSource already has N-1 items when the event fires, but UIKit still has N sections. UpdateSection iterates N sections and calls NumberOfItemsInSection(N-1), which re-enters the data source via ItemCountInGroup(N-1) → GetGroupCount(N-1) → _groupSource[N-1] which is out of range.
Fix Quality
The PR's two-part fix is well-motivated and targeted:
- Skip
UpdateSectionbeforeRemove— directly addresses the root cause for the Remove case. Conservative (only changes the Remove path; Add/Replace/Move/Reset are unaffected). - Defensive bounds check in
GetGroupCount— good defense-in-depth for other re-entrancy scenarios (UIKit can call back synchronously duringDeleteSections).
However, the tests are inadequate:
- Tests pass even WITHOUT the fix — the crash is timing-sensitive and the test setup does not consistently trigger the synchronous UIKit re-entrancy sequence.
- Tests use
Task.Delay(1000)/Task.Delay(500)/Task.Delay(300)— arbitrary delays are unreliable for device test synchronization. - Tests only verify post-removal count (
Assert.Equal(2, groupData.Count)) — they don't verify the crash path was exercised.
Try-Fix found a simpler alternative (Attempt 1):
Clamping UpdateSection's loop bound to Math.Min(numberOfSections, _groupSource.Count) (+5/-2 lines in one method) is intrinsically safe for all actions without conditional logic. It preserves the pre-processing call's intent while making it harmless when called with a stale UIKit count. This is arguably a better fix than the PR's two-part approach because it eliminates the hazard at the source rather than working around it action-by-action.
Requested Changes
-
Fix the tests so they actually detect the regression. The tests must FAIL on the broken baseline (without the fix). Options:
- Wrap
RemoveAtin a try/catch and assert thatArgumentOutOfRangeExceptionis NOT thrown (or useAssert.DoesNotThrow). - Use
await collectionView.WaitForLayout()orEnsureHandlerCreated()patterns (consistent with other CollectionView device tests in the file) instead of arbitraryTask.Delay. - Consider whether the crash only manifests on physical device vs. simulator; if simulator-only tests can't reproduce it reliably, document this and mark the tests appropriately.
- Wrap
-
Consider the simpler Attempt 1 fix: Clamping
UpdateSectionloop toMath.Min(numberOfSections, _groupSource.Count)achieves the same safety with a single change and no conditional per-action logic. The PR author may prefer their own two-part approach — that is fine — but should be aware of this alternative. -
Minor: The three test methods have nearly identical boilerplate setup. Consider extracting shared setup into a helper method.
The code fix itself (skipping UpdateSection for Remove + bounds check in GetGroupCount) is correct and should be kept once the tests are strengthened.
|
If I understand correctly, a change is optional, and conservative code is preferable. So no change is necessary? By the way, the issue and pull request are generated automatically by Copilot (CLI) from a frequently occurring exception in Sentry. I think it's great that it works so well. Use Sentry MCP, check if it's our own code or Maui, and fix it directly in Maui. |
Summary
Fixes #34691 —
ArgumentOutOfRangeExceptioninObservableGroupedSource.GetGroupCountwhen removing a group from a groupedCollectionViewon iOS.Root Cause
CollectionChanged(NotifyCollectionChangedEventArgs)calledUpdateSection()before processing theRemoveaction:Standard
INotifyCollectionChangedsemantics require the item to be already removed from the backing collection beforeCollectionChangedfires. So when MAUI receivesCollectionChanged(Remove),_groupSourcehas N-1 items — but UIKit still has N sections.UpdateSectioniterates UIKit's N sections and callsNumberOfItemsInSection(N-1), which re-enters the data source:The
Compatibilitylayer (Microsoft.Maui.Controls.Compatibility.Platform.iOS.ObservableGroupedSource) does not callUpdateSectionbeforeRemoveand is unaffected.Fix
Skip
UpdateSectionbeforeRemove— the post-processing call (afterDeleteSectionscompletes) already handles UIKit reconciliation.Defensive bounds check in
GetGroupCount— guards against other re-entrancy scenarios where UIKit calls back synchronously with a stale section index duringDeleteSections.Testing
CollectionViewwith dynamic section removal on iOSUpdateSectionis still called forAdd,Replace,Move, andReset— no regression expected for those actionsUpdateSectionstill runs forRemoveafterDeleteSectionscompletes