diff --git a/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs b/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs index 15eab9b1ea94..6cc3cceade86 100644 --- a/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs +++ b/src/Core/src/Handlers/DatePicker/DatePickerHandler.Android.cs @@ -1,7 +1,6 @@ using System; using Android.App; -using Android.Content.Res; -using Android.Graphics.Drawables; +using Android.Views; using Microsoft.Maui.Devices; namespace Microsoft.Maui.Handlers @@ -31,7 +30,21 @@ protected override MauiDatePicker CreatePlatformView() protected override void ConnectHandler(MauiDatePicker platformView) { base.ConnectHandler(platformView); + platformView.ViewAttachedToWindow += OnViewAttachedToWindow; + platformView.ViewDetachedFromWindow += OnViewDetachedFromWindow; + if (platformView.IsAttachedToWindow) + OnViewAttachedToWindow(); + } + + void OnViewDetachedFromWindow(object? sender = null, View.ViewDetachedFromWindowEventArgs? e = null) + { + // I tested and this is called when an activity is destroyed + DeviceDisplay.MainDisplayInfoChanged -= OnMainDisplayInfoChanged; + } + + void OnViewAttachedToWindow(object? sender = null, View.ViewAttachedToWindowEventArgs? e = null) + { DeviceDisplay.MainDisplayInfoChanged += OnMainDisplayInfoChanged; } @@ -44,7 +57,9 @@ protected override void DisconnectHandler(MauiDatePicker platformView) _dialog = null; } - DeviceDisplay.MainDisplayInfoChanged -= OnMainDisplayInfoChanged; + platformView.ViewAttachedToWindow -= OnViewAttachedToWindow; + platformView.ViewDetachedFromWindow -= OnViewDetachedFromWindow; + OnViewDetachedFromWindow(); base.DisconnectHandler(platformView); } @@ -152,4 +167,4 @@ void OnMainDisplayInfoChanged(object? sender, DisplayInfoChangedEventArgs e) } } } -} \ No newline at end of file +} diff --git a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.cs b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.cs index 0582e8dc5c6b..3df6c99299f0 100644 --- a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.cs +++ b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.cs @@ -88,6 +88,15 @@ protected TCustomHandler CreateHandler(IElement view, return handler; } + + protected IPlatformViewHandler CreateHandler(IElement view, Type handlerType) + { + var handler = (IPlatformViewHandler)Activator.CreateInstance(handlerType); + InitializeViewHandler(view, handler, MauiContext); + return handler; + + } + public void Dispose() { ((IDisposable)_mauiApp).Dispose(); diff --git a/src/Core/tests/DeviceTests/Handlers/HandlerTestBaseOfT.Tests.cs b/src/Core/tests/DeviceTests/Handlers/HandlerTestBaseOfT.Tests.cs index 292885050168..164d1e32550c 100644 --- a/src/Core/tests/DeviceTests/Handlers/HandlerTestBaseOfT.Tests.cs +++ b/src/Core/tests/DeviceTests/Handlers/HandlerTestBaseOfT.Tests.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Maui.DeviceTests.Stubs; @@ -12,58 +11,6 @@ namespace Microsoft.Maui.DeviceTests { public abstract partial class HandlerTestBase { - - // This way of testing leaks currently doesn't seem to work on WinAppSDK - // If you set a break point and the app breaks then the test passes?!? - // Not sure if you can test this type of thing with WinAppSDK or not -#if !WINDOWS - [Fact(DisplayName = "Handlers Deallocate When No Longer Referenced")] - public async Task HandlersDeallocateWhenNoLongerReferenced() - { - // Once this includes all handlers we can delete this - Type[] testedTypes = new[] - { - typeof(EditorHandler), -#if IOS - typeof(DatePickerHandler) -#endif - }; - - if (!testedTypes.Any(t => t.IsAssignableTo(typeof(THandler)))) - return; - - var handler = await CreateHandlerAsync(new TStub()) as IPlatformViewHandler; - - WeakReference weakHandler = new WeakReference((THandler)handler); - WeakReference weakView = new WeakReference((TStub)handler.VirtualView); - - handler = null; - - await AssertionExtensions.Wait(() => - { - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - GC.WaitForPendingFinalizers(); - - if (weakHandler.TryGetTarget(out THandler _) || - weakView.TryGetTarget(out TStub _)) - { - return false; - } - - return true; - - }, 1500); - - if (weakHandler.TryGetTarget(out THandler _)) - Assert.True(false, $"{typeof(THandler)} failed to collect"); - - if (weakView.TryGetTarget(out TStub _)) - Assert.True(false, $"{typeof(TStub)} failed to collect"); - } -#endif - [Fact] public async Task DisconnectHandlerDoesntCrash() { @@ -73,7 +20,7 @@ await InvokeOnMainThreadAsync(() => handler.DisconnectHandler(); }); } - + [Fact(DisplayName = "Automation Id is set correctly")] public async Task SetAutomationId() { diff --git a/src/Core/tests/DeviceTests/Memory/MemoryTestFixture.cs b/src/Core/tests/DeviceTests/Memory/MemoryTestFixture.cs new file mode 100644 index 000000000000..4e6f021b068c --- /dev/null +++ b/src/Core/tests/DeviceTests/Memory/MemoryTestFixture.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + + +namespace Microsoft.Maui.Handlers.Memory +{ + public class MemoryTestFixture : IDisposable + { + Dictionary _handlers + = new Dictionary(); + + public MemoryTestFixture() + { + } + + public void AddReferences(Type handlerType, (WeakReference handler, WeakReference view) value) => + _handlers.Add(handlerType, value); + + public bool HasType(Type handlerType) => _handlers.ContainsKey(handlerType); + + public bool DoReferencesStillExist(Type handlerType) + { + WeakReference weakHandler; + WeakReference weakView; + (weakHandler, weakView) = _handlers[handlerType]; + + + if (weakHandler.Target != null || + weakHandler.IsAlive || + weakView.Target != null || + weakView.IsAlive) + { + return true; + } + + return false; + } + + public void Dispose() + { + _handlers.Clear(); + } + } +} diff --git a/src/Core/tests/DeviceTests/Memory/MemoryTestOrdering.cs b/src/Core/tests/DeviceTests/Memory/MemoryTestOrdering.cs new file mode 100644 index 000000000000..dea2d194ba34 --- /dev/null +++ b/src/Core/tests/DeviceTests/Memory/MemoryTestOrdering.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit.Abstractions; +using Xunit.Sdk; + + +namespace Microsoft.Maui.Handlers.Memory +{ + public class MemoryTestOrdering : ITestCaseOrderer + { + public IEnumerable OrderTestCases(IEnumerable testCases) + where TTestCase : ITestCase + { + var result = testCases.ToList(); + + + if (result.Count > 2) + throw new InvalidOperationException("Add new test to sort if you want it to run"); + + return new List() + { + result.First(x=> x.TestMethod.Method.Name == nameof(MemoryTests.Allocate)), + result.First(x=> x.TestMethod.Method.Name == nameof(MemoryTests.CheckAllocation)), + }; + } + } +} \ No newline at end of file diff --git a/src/Core/tests/DeviceTests/Memory/MemoryTestTypes.cs b/src/Core/tests/DeviceTests/Memory/MemoryTestTypes.cs new file mode 100644 index 000000000000..75e74b638034 --- /dev/null +++ b/src/Core/tests/DeviceTests/Memory/MemoryTestTypes.cs @@ -0,0 +1,18 @@ +using System.Collections; +using System.Collections.Generic; +using Microsoft.Maui.DeviceTests.Stubs; + + +namespace Microsoft.Maui.Handlers.Memory +{ + public class MemoryTestTypes : IEnumerable + { + public IEnumerator GetEnumerator() + { + yield return new object[] { (typeof(DatePickerStub), typeof(DatePickerHandler)) }; + yield return new object[] { (typeof(EditorStub), typeof(EditorHandler)) }; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/Core/tests/DeviceTests/Memory/MemoryTests.cs b/src/Core/tests/DeviceTests/Memory/MemoryTests.cs new file mode 100644 index 000000000000..fc348bdc364e --- /dev/null +++ b/src/Core/tests/DeviceTests/Memory/MemoryTests.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Maui.DeviceTests; +using Microsoft.Maui.DeviceTests.Stubs; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Media; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + + +namespace Microsoft.Maui.Handlers.Memory +{ + /// + /// Trying to allocate and then rely on the GC to collect within the same test was a bit unreliable when running + /// tests from the xHarness CLI. + /// For example, if you call Thread.Sleep it will block the entire test run from moving forward whereas locally + /// it's able to still run in parallel. So, I broke the allocate and check allocate into two steps + /// which seems to make Android and WinUI happier. Low APIs on Android still have issues running via xHarness + /// which is why we only currently run these on API 30+ + /// + [TestCaseOrderer("Microsoft.Maui.Handlers.Memory.MemoryTestOrdering", "Microsoft.Maui.Core.DeviceTests")] + public class MemoryTests : HandlerTestBase, IClassFixture + { + MemoryTestFixture _fixture; + public MemoryTests(MemoryTestFixture fixture) + { + _fixture = fixture; + } + + [Theory] + [ClassData(typeof(MemoryTestTypes))] + public async Task Allocate((Type ViewType, Type HandlerType) data) + { + if (!OperatingSystem.IsAndroidVersionAtLeast(30)) + return; + + var handler = await InvokeOnMainThreadAsync(() => CreateHandler((IElement)Activator.CreateInstance(data.ViewType), data.HandlerType)); + WeakReference weakHandler = new WeakReference(handler); + _fixture.AddReferences(data.HandlerType, (weakHandler, new WeakReference(handler.VirtualView))); + handler = null; + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + + [Theory] + [ClassData(typeof(MemoryTestTypes))] + public async Task CheckAllocation((Type ViewType, Type HandlerType) data) + { + if (!OperatingSystem.IsAndroidVersionAtLeast(30)) + return; + + // This is mainly relevant when running inside the visual runner as a single test + if (!_fixture.HasType(data.HandlerType)) + await Allocate(data); + + await AssertionExtensions.Wait(() => + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + if (_fixture.DoReferencesStillExist(data.HandlerType)) + { + return false; + } + + return true; + + }, 5000); + + if (_fixture.DoReferencesStillExist(data.HandlerType)) + { + Assert.True(false, $"{data.HandlerType} failed to collect."); + } + } + } +} \ No newline at end of file