diff --git a/Avalonia.sln b/Avalonia.sln index b7625ed1dd..dda16bb5ad 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -90,7 +90,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\EmbedXaml.props = build\EmbedXaml.props build\HarfBuzzSharp.props = build\HarfBuzzSharp.props build\ImageSharp.props = build\ImageSharp.props - build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props build\Microsoft.CSharp.props = build\Microsoft.CSharp.props build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props build\Moq.props = build\Moq.props diff --git a/build/JetBrains.dotMemoryUnit.props b/build/JetBrains.dotMemoryUnit.props deleted file mode 100644 index 5d74d474cf..0000000000 --- a/build/JetBrains.dotMemoryUnit.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index d3c2cb1d01..c24d2f0c32 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -15,7 +15,6 @@ using Nuke.Common.Tools.Npm; using Nuke.Common.Utilities; using static Nuke.Common.EnvironmentInfo; using static Nuke.Common.IO.PathConstruction; -using static Nuke.Common.Tools.DotMemoryUnit.DotMemoryUnitTasks; using static Nuke.Common.Tools.DotNet.DotNetTasks; using static Serilog.Log; using MicroCom.CodeGenerator; @@ -194,23 +193,6 @@ partial class Build : NukeBuild }); } - void RunCoreDotMemoryUnit(string projectName) - { - RunCoreTest(projectName, (project, tfm) => - { - var testSettings = ApplySetting(new DotNetTestSettings(), project, tfm); - var testToolPath = GetToolPathInternal(new DotNetTasks(), testSettings); - var testArgs = GetArguments(testSettings).JoinSpace(); - DotMemoryUnit($"{testToolPath} --propagate-exit-code -- {testArgs:nq}"); - }); - - [UnsafeAccessor(UnsafeAccessorKind.Method, Name = nameof(GetToolPathInternal))] - extern static string GetToolPathInternal(ToolTasks tasks, ToolOptions options); - - [UnsafeAccessor(UnsafeAccessorKind.Method, Name = nameof(GetArguments))] - extern static IEnumerable GetArguments(ToolOptions options); - } - void RunCoreTest(string projectName, Action runTest) { Information($"Running tests from {projectName}"); @@ -325,11 +307,7 @@ partial class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - void DoMemoryTest() - { - RunCoreDotMemoryUnit("Avalonia.LeakTests"); - } - ControlFlow.ExecuteWithRetry(DoMemoryTest, delay: TimeSpan.FromMilliseconds(3)); + RunCoreTest("Avalonia.LeakTests"); }); Target ZipFiles => _ => _ @@ -418,8 +396,8 @@ partial class Build : NukeBuild .DependsOn(RunCoreLibsTests) .DependsOn(RunRenderTests) .DependsOn(RunToolsTests) - .DependsOn(RunHtmlPreviewerTests); - //.DependsOn(RunLeakTests); // dotMemory Unit doesn't support modern .NET versions, see https://youtrack.jetbrains.com/issue/DMU-300/ + .DependsOn(RunHtmlPreviewerTests) + .DependsOn(RunLeakTests); Target Package => _ => _ .DependsOn(RunTests) diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 7a0828dc94..b57414b32e 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -11,7 +11,6 @@ true - diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index 30c1f78c36..8bcb61ff99 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/src/Avalonia.Base/Animation/Animatable.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using Avalonia.Data; +using Avalonia.PropertyStore; #nullable enable @@ -266,6 +267,21 @@ namespace Avalonia.Animation return value; } + /// + /// For unit tests only! + /// + internal TransitionInstance? TryGetTransitionInstance(Transition transition) + { + if (_transitionState is not null && _transitionState.TryGetValue(transition, out var transitionState)) + { + var valueEntry = transitionState.Instance as BindingEntryBase; + var transitionObservable = valueEntry?.Source as TransitionObservableBase; + return transitionObservable?.Progress as TransitionInstance; + } + + return null; + } + private class TransitionState { public IDisposable? Instance { get; set; } diff --git a/src/Avalonia.Base/Animation/TransitionObservableBase.cs b/src/Avalonia.Base/Animation/TransitionObservableBase.cs index 92462dc537..819a8a9338 100644 --- a/src/Avalonia.Base/Animation/TransitionObservableBase.cs +++ b/src/Avalonia.Base/Animation/TransitionObservableBase.cs @@ -22,6 +22,11 @@ namespace Avalonia.Animation _easing = easing; } + /// + /// For unit tests only! + /// + internal IObservable Progress => _progress; + /// /// Produces value at given progress time point. /// diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs index bbcab307ac..19047b1673 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs @@ -51,7 +51,7 @@ namespace Avalonia.PropertyStore public AvaloniaProperty Property { get; } AvaloniaProperty IValueEntry.Property => Property; protected ValueFrame Frame { get; } - protected object Source { get; } + protected internal object Source { get; } public void Dispose() { diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj index 660e264a8b..f1c22e11e1 100644 --- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj +++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj @@ -2,9 +2,7 @@ $(AvsCurrentTargetFramework) - - diff --git a/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs b/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs index da33bc6c79..ecaca2db07 100644 --- a/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs +++ b/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs @@ -1,7 +1,6 @@ #nullable enable using System; -using System.Collections.Generic; using System.ComponentModel; using System.Reactive.Subjects; using System.Runtime.CompilerServices; @@ -12,20 +11,12 @@ using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; using Avalonia.Threading; using Avalonia.UnitTests; -using JetBrains.dotMemoryUnit; using Xunit; -using Xunit.Abstractions; namespace Avalonia.LeakTests { - [DotMemoryUnit(FailIfRunWithoutSupport = false)] public class AvaloniaObjectTests : ScopedTestBase { - public AvaloniaObjectTests(ITestOutputHelper atr) - { - DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); - } - [Fact] public void Binding_To_Direct_Property_Does_Not_Get_Collected() { diff --git a/tests/Avalonia.LeakTests/BindingExpressionTests.cs b/tests/Avalonia.LeakTests/BindingExpressionTests.cs index 4b38cfe384..5a6eddc869 100644 --- a/tests/Avalonia.LeakTests/BindingExpressionTests.cs +++ b/tests/Avalonia.LeakTests/BindingExpressionTests.cs @@ -2,91 +2,94 @@ using System.Collections.Generic; using Avalonia.Collections; using Avalonia.Data.Core; -using Avalonia.Markup.Data; using Avalonia.UnitTests; -using JetBrains.dotMemoryUnit; using Xunit; -using Xunit.Abstractions; namespace Avalonia.LeakTests { - [DotMemoryUnit(FailIfRunWithoutSupport = false)] - public class BindingExpressionTests + public class BindingExpressionTests : ScopedTestBase { - public BindingExpressionTests(ITestOutputHelper atr) - { - DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); - } - [Fact] public void Should_Not_Keep_Source_Alive_ObservableCollection() { - Func run = () => + static WeakReference CreateExpression() { - var source = new { Foo = new AvaloniaList { "foo", "bar" } }; + var list = new AvaloniaList { "foo", "bar" }; + var source = new { Foo = list }; var target = BindingExpression.Create(source, o => o.Foo); target.ToObservable().Subscribe(_ => { }); - return target; - }; + return new WeakReference(list); + } + + var weakSource = CreateExpression(); + Assert.True(weakSource.IsAlive); - var result = run(); + GC.Collect(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is>()).ObjectsCount)); + Assert.False(weakSource.IsAlive); } [Fact] public void Should_Not_Keep_Source_Alive_ObservableCollection_With_DataValidation() { - Func run = () => + static WeakReference CreateExpression() { - var source = new { Foo = new AvaloniaList { "foo", "bar" } }; + var list = new AvaloniaList { "foo", "bar" }; + var source = new { Foo = list }; var target = BindingExpression.Create(source, o => o.Foo, enableDataValidation: true); target.ToObservable().Subscribe(_ => { }); - return target; - }; + return new WeakReference(list); + } - var result = run(); + var weakSource = CreateExpression(); + Assert.True(weakSource.IsAlive); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is>()).ObjectsCount)); + GC.Collect(); + + Assert.False(weakSource.IsAlive); } [Fact] public void Should_Not_Keep_Source_Alive_NonIntegerIndexer() { - Func run = () => + static WeakReference CreateExpression() { - var source = new { Foo = new NonIntegerIndexer() }; + var indexer = new NonIntegerIndexer(); + var source = new { Foo = indexer }; var target = BindingExpression.Create(source, o => o.Foo); target.ToObservable().Subscribe(_ => { }); - return target; - }; + return new WeakReference(indexer); + } + + var weakSource = CreateExpression(); + Assert.True(weakSource.IsAlive); - var result = run(); + GC.Collect(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + Assert.False(weakSource.IsAlive); } [Fact] public void Should_Not_Keep_Source_Alive_MethodBinding() { - Func run = () => + static WeakReference CreateExpression() { - var source = new { Foo = new MethodBound() }; + var methodBound = new MethodBound(); + var source = new { Foo = methodBound }; var target = BindingExpression.Create(source, o => (Action)o.Foo.A); target.ToObservable().Subscribe(_ => { }); - return target; - }; + return new WeakReference(methodBound); + } + + var weakSource = CreateExpression(); + Assert.True(weakSource.IsAlive); - var result = run(); + GC.Collect(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + Assert.False(weakSource.IsAlive); } private class MethodBound diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 5da96511ce..47d8417bd6 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -1,95 +1,80 @@ +#nullable enable + using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Disposables; -using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Presenters; -using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Input; -using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; -using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.Styling; using Avalonia.Threading; using Avalonia.UnitTests; using Avalonia.VisualTree; -using JetBrains.dotMemoryUnit; using Moq; using Xunit; -using Xunit.Abstractions; namespace Avalonia.LeakTests { - [DotMemoryUnit(FailIfRunWithoutSupport = false)] - public class ControlTests + public class ControlTests : ScopedTestBase { - // Need to have the collection as field, so GC will not free it - private readonly ObservableCollection _observableCollection = new(); - - public ControlTests(ITestOutputHelper atr) - { - DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); - } - - [Fact] + [ReleaseFact] public void Canvas_Is_Freed() { using (Start()) { - Func run = () => + static WeakReference Run() { + var canvas = new Canvas(); var window = new Window { - Content = new Canvas() + Content = canvas }; window.Show(); // Do a layout and make sure that Canvas gets added to visual tree. window.LayoutManager.ExecuteInitialLayoutPass(); - Assert.IsType(window.Presenter.Child); + Assert.IsType(window.Presenter!.Child); // Clear the content and ensure the Canvas is removed. window.Content = null; window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); - return window; - }; + return new WeakReference(canvas); + } - var result = run(); + var weakCanvas = Run(); + Assert.True(weakCanvas.IsAlive); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + CollectGarbage(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + Assert.False(weakCanvas.IsAlive); } } - [Fact] + [ReleaseFact] public void Named_Canvas_Is_Freed() { using (Start()) { - Func run = () => + static WeakReference Run() { var scope = new NameScope(); + var canvas = new Canvas { Name = "foo" }; var window = new Window { - Content = new Canvas - { - Name = "foo" - }.RegisterInNameScope(scope) + Content = canvas.RegisterInNameScope(scope) }; NameScope.SetNameScope(window, scope); @@ -98,7 +83,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that Canvas gets added to visual tree. window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Find("foo")); - Assert.IsType(window.Presenter.Child); + Assert.IsType(window.Presenter!.Child); // Clear the content and ensure the Canvas is removed. window.Content = null; @@ -107,72 +92,70 @@ namespace Avalonia.LeakTests window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); - return window; - }; + return new WeakReference(canvas); + } - var result = run(); + var weakCanvas = Run(); + Assert.True(weakCanvas.IsAlive); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + CollectGarbage(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + Assert.False(weakCanvas.IsAlive); } } - [Fact] + [ReleaseFact] public void ScrollViewer_With_Content_Is_Freed() { using (Start()) { - Func run = () => + static WeakReference Run() { + var canvas = new Canvas(); var window = new Window { Content = new ScrollViewer { - Content = new Canvas() + Content = canvas } }; window.Show(); - // Do a layout and make sure that ScrollViewer gets added to visual tree and its + // Do a layout and make sure that ScrollViewer gets added to visual tree and its // template applied. window.LayoutManager.ExecuteInitialLayoutPass(); - Assert.IsType(window.Presenter.Child); - Assert.IsType(((ScrollViewer)window.Presenter.Child).Presenter.Child); + Assert.IsType(window.Presenter!.Child); + Assert.IsType(((ScrollViewer)window.Presenter!.Child).Presenter!.Child); // Clear the content and ensure the ScrollViewer is removed. window.Content = null; window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); - return window; - }; + return new WeakReference(canvas); + } - var result = run(); + var weakCanvas = Run(); + Assert.True(weakCanvas.IsAlive); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + CollectGarbage(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + Assert.False(weakCanvas.IsAlive); } } - [Fact] + [ReleaseFact] public void TextBox_Is_Freed() { using (Start()) { - Func run = () => + static WeakReference Run() { + var textBox = new TextBox(); var window = new Window { - Content = new TextBox() + Content = textBox }; window.Show(); @@ -180,7 +163,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TextBox gets added to visual tree and its // template applied. window.LayoutManager.ExecuteInitialLayoutPass(); - Assert.IsType(window.Presenter.Child); + Assert.IsType(window.Presenter!.Child); Assert.NotEmpty(window.Presenter.Child.GetVisualChildren()); // Clear the content and ensure the TextBox is removed. @@ -188,33 +171,33 @@ namespace Avalonia.LeakTests window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); - return window; - }; + return new WeakReference(textBox); + } - var result = run(); + var weakTextBox = Run(); + Assert.True(weakTextBox.IsAlive); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + CollectGarbage(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + Assert.False(weakTextBox.IsAlive); } } - [Fact] + [ReleaseFact] public void TextBox_With_Xaml_Binding_Is_Freed() { using (Start()) { - Func run = () => + static (WeakReference, WeakReference) Run() { + var node = new Node { Name = "foo" }; var window = new Window { - DataContext = new Node { Name = "foo" }, + DataContext = node, Content = new TextBox() }; - var binding = new Avalonia.Data.Binding + var binding = new Binding { Path = "Name" }; @@ -224,10 +207,10 @@ namespace Avalonia.LeakTests window.Show(); - // Do a layout and make sure that TextBox gets added to visual tree and its + // Do a layout and make sure that TextBox gets added to visual tree and its // Text property set. window.LayoutManager.ExecuteInitialLayoutPass(); - Assert.IsType(window.Presenter.Child); + Assert.IsType(window.Presenter!.Child); Assert.Equal("foo", ((TextBox)window.Presenter.Child).Text); // Clear the content and DataContext and ensure the TextBox is removed. @@ -236,18 +219,17 @@ namespace Avalonia.LeakTests window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); - return window; - }; + return (new WeakReference(node), new WeakReference(textBox)); + } - var result = run(); + var (weakNode, weakTextBox) = Run(); + Assert.True(weakNode.IsAlive); + Assert.True(weakTextBox.IsAlive); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + CollectGarbage(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + Assert.False(weakNode.IsAlive); + Assert.False(weakTextBox.IsAlive); } } @@ -268,7 +250,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TextBox gets added to visual tree and its // template applied. window.LayoutManager.ExecuteInitialLayoutPass(); - Assert.Same(textBox, window.Presenter.Child); + Assert.Same(textBox, window.Presenter!.Child); // Get the border from the TextBox template. var border = textBox.GetTemplateChildren().FirstOrDefault(x => x.Name == "border"); @@ -287,12 +269,12 @@ namespace Avalonia.LeakTests } } - [Fact] + [ReleaseFact] public void TreeView_Is_Freed() { using (Start()) { - Func run = () => + static WeakReference Run() { var nodes = new[] { @@ -327,69 +309,69 @@ namespace Avalonia.LeakTests // Clear the content and ensure the TreeView is removed. window.Content = null; window.LayoutManager.ExecuteLayoutPass(); - Assert.Null(window.Presenter.Child); + Assert.Null(window.Presenter!.Child); - return window; - }; + return new WeakReference(target); + } - var result = run(); + var weakTreeView = Run(); + Assert.True(weakTreeView.IsAlive); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + CollectGarbage(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + Assert.False(weakTreeView.IsAlive); } } - [Fact] + [ReleaseFact] public void Slider_Is_Freed() { using (Start()) { - Func run = () => + static WeakReference Run() { + var slider = new Slider(); var window = new Window { - Content = new Slider() + Content = slider }; window.Show(); // Do a layout and make sure that Slider gets added to visual tree. window.LayoutManager.ExecuteInitialLayoutPass(); - Assert.IsType(window.Presenter.Child); + Assert.IsType(window.Presenter!.Child); // Clear the content and ensure the Slider is removed. window.Content = null; window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); - return window; - }; + return new WeakReference(slider); + } - var result = run(); + var weakSlider = Run(); + Assert.True(weakSlider.IsAlive); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + CollectGarbage(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + Assert.False(weakSlider.IsAlive); } } - [Fact] + [ReleaseFact] public void TabItem_Is_Freed() { using (Start()) { - Func run = () => + static WeakReference Run() { + var tabItem = new TabItem(); var window = new Window { Content = new TabControl { - ItemsSource = new[] { new TabItem() } + ItemsSource = new[] { tabItem } } }; @@ -397,24 +379,23 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TabControl and TabItem gets added to visual tree. window.LayoutManager.ExecuteInitialLayoutPass(); - var tabControl = Assert.IsType(window.Presenter.Child); - Assert.IsType(tabControl.Presenter.Panel.Children[0]); + var tabControl = Assert.IsType(window.Presenter!.Child); + Assert.IsType(tabControl.Presenter!.Panel!.Children[0]); // Clear the items and ensure the TabItem is removed. tabControl.ItemsSource = null; window.LayoutManager.ExecuteLayoutPass(); Assert.Empty(tabControl.Presenter.Panel.Children); - return window; - }; + return new WeakReference(tabItem); + } - var result = run(); + var weakTabItem = Run(); + Assert.True(weakTabItem.IsAlive); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + CollectGarbage(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + Assert.False(weakTabItem.IsAlive); } } @@ -432,7 +413,7 @@ namespace Avalonia.LeakTests impl.SetupGet(x => x.RenderScaling).Returns(1); impl.SetupProperty(x => x.Closed); impl.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor()); - impl.Setup(x => x.Dispose()).Callback(() => impl.Object.Closed()); + impl.Setup(x => x.Dispose()).Callback(() => impl.Object.Closed!()); impl.Setup(x => x.TryGetFeature(It.Is(t => t == typeof(IScreenImpl)))).Returns(screens.Object); AvaloniaLocator.CurrentMutable.Bind() @@ -447,13 +428,13 @@ namespace Avalonia.LeakTests } } - [Fact] + [ReleaseFact] public void Control_With_Style_RenderTransform_Is_Freed() { // # Issue #3545 using (Start()) { - Func run = () => + static WeakReference Run() { var window = new Window { @@ -479,7 +460,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that Canvas gets added to visual tree with // its render transform. window.LayoutManager.ExecuteInitialLayoutPass(); - var canvas = Assert.IsType(window.Presenter.Child); + var canvas = Assert.IsType(window.Presenter!.Child); Assert.IsType(canvas.RenderTransform); // Clear the content and ensure the Canvas is removed. @@ -487,32 +468,33 @@ namespace Avalonia.LeakTests window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); - return window; - }; + return new WeakReference(canvas); + } - var result = run(); + var weakCanvas = Run(); + Assert.True(weakCanvas.IsAlive); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + CollectGarbage(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + Assert.False(weakCanvas.IsAlive); } } - [Fact] + [ReleaseFact] public void Attached_ContextMenu_Is_Freed() { using (Start()) { - void AttachShowAndDetachContextMenu(Control control) + (WeakReference, WeakReference, WeakReference) AttachShowAndDetachContextMenu(Control control) { + var menuItem1 = new MenuItem { Header = "Foo" }; + var menuItem2 = new MenuItem { Header = "Foo" }; var contextMenu = new ContextMenu { Items = { - new MenuItem { Header = "Foo" }, - new MenuItem { Header = "Foo" }, + menuItem1, + menuItem2 } }; @@ -520,208 +502,203 @@ namespace Avalonia.LeakTests contextMenu.Open(control); contextMenu.Close(); control.ContextMenu = null; + + return (new WeakReference(menuItem1), new WeakReference(menuItem2), new WeakReference(contextMenu)); } var window = new Window { Focusable = true }; window.Show(); - Assert.Same(window, window.FocusManager.GetFocusedElement()); - - // Context menu in resources means the baseline may not be 0. - var initialMenuCount = 0; - var initialMenuItemCount = 0; - dotMemory.Check(memory => - { - initialMenuCount = memory.GetObjects(where => where.Type.Is()).ObjectsCount; - initialMenuItemCount = memory.GetObjects(where => where.Type.Is()).ObjectsCount; - }); - - AttachShowAndDetachContextMenu(window); + Assert.Same(window, window.FocusManager!.GetFocusedElement()); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + var (weakMenuItem1, weakMenuItem2, weakContextMenu) = AttachShowAndDetachContextMenu(window); + Assert.True(weakMenuItem1.IsAlive); + Assert.True(weakMenuItem2.IsAlive); + Assert.True(weakContextMenu.IsAlive); Mock.Get(window.PlatformImpl).Invocations.Clear(); - dotMemory.Check(memory => - Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); - dotMemory.Check(memory => - Assert.Equal(initialMenuItemCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + CollectGarbage(); + + Assert.False(weakMenuItem1.IsAlive); + Assert.False(weakMenuItem2.IsAlive); + Assert.False(weakContextMenu.IsAlive); } } - [Fact] + [ReleaseFact] public void Attached_Control_From_ContextMenu_Is_Freed() { using (Start()) { var contextMenu = new ContextMenu(); - Func run = () => + + WeakReference Run() { + var textBlock = new TextBlock + { + ContextMenu = contextMenu + }; var window = new Window { - Content = new TextBlock - { - ContextMenu = contextMenu - } + Content = textBlock }; window.Show(); // Do a layout and make sure that TextBlock gets added to visual tree. window.LayoutManager.ExecuteInitialLayoutPass(); - Assert.IsType(window.Presenter.Child); + Assert.IsType(window.Presenter!.Child); // Clear the content and ensure the TextBlock is removed. window.Content = null; window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); - return window; - }; + return new WeakReference(textBlock); + } - var result = run(); + var weakTextBlock = Run(); + Assert.True(weakTextBlock.IsAlive); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + CollectGarbage(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + Assert.False(weakTextBlock.IsAlive); } } - [Fact] + [ReleaseFact] public void Standalone_ContextMenu_Is_Freed() { using (Start()) { - void BuildAndShowContextMenu(Control control) + (WeakReference, WeakReference, WeakReference) BuildAndShowContextMenu(Control control) { + var menuItem1 = new MenuItem { Header = "Foo" }; + var menuItem2 = new MenuItem { Header = "Foo" }; var contextMenu = new ContextMenu { Items = { - new MenuItem { Header = "Foo" }, - new MenuItem { Header = "Foo" }, + menuItem1, + menuItem2 } }; contextMenu.Open(control); contextMenu.Close(); + + return (new WeakReference(menuItem1), new WeakReference(menuItem2), new WeakReference(contextMenu)); } var window = new Window { Focusable = true }; window.Show(); - Assert.Same(window, window.FocusManager.GetFocusedElement()); - - // Context menu in resources means the baseline may not be 0. - var initialMenuCount = 0; - var initialMenuItemCount = 0; - dotMemory.Check(memory => - { - initialMenuCount = memory.GetObjects(where => where.Type.Is()).ObjectsCount; - initialMenuItemCount = memory.GetObjects(where => where.Type.Is()).ObjectsCount; - }); + Assert.Same(window, window.FocusManager!.GetFocusedElement()); - BuildAndShowContextMenu(window); - BuildAndShowContextMenu(window); + var (weakMenuItem1, weakMenuItem2, weakContextMenu1) = BuildAndShowContextMenu(window); + var (weakMenuItem3, weakMenuItem4, weakContextMenu2) = BuildAndShowContextMenu(window); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + Assert.True(weakMenuItem1.IsAlive); + Assert.True(weakMenuItem2.IsAlive); + Assert.True(weakContextMenu1.IsAlive); + Assert.True(weakMenuItem3.IsAlive); + Assert.True(weakMenuItem4.IsAlive); + Assert.True(weakContextMenu2.IsAlive); Mock.Get(window.PlatformImpl).Invocations.Clear(); - dotMemory.Check(memory => - Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); - dotMemory.Check(memory => - Assert.Equal(initialMenuItemCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + CollectGarbage(); + + Assert.False(weakMenuItem1.IsAlive); + Assert.False(weakMenuItem2.IsAlive); + Assert.False(weakContextMenu1.IsAlive); + Assert.False(weakMenuItem3.IsAlive); + Assert.False(weakMenuItem4.IsAlive); + Assert.False(weakContextMenu2.IsAlive); } } - [Fact] + [ReleaseFact] public void Path_Is_Freed() { using (Start()) { var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; - Func run = () => + WeakReference Run() { + var path = new Path + { + Data = geometry + }; var window = new Window { - Content = new Path - { - Data = geometry - } + Content = path }; window.Show(); window.LayoutManager.ExecuteInitialLayoutPass(); - Assert.IsType(window.Presenter.Child); + Assert.IsType(window.Presenter!.Child); window.Content = null; window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); - return window; - }; - - var result = run(); + return new WeakReference(path); + } - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + var weakPath = Run(); + Assert.True(weakPath.IsAlive); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + CollectGarbage(); - // We are keeping geometry alive to simulate a resource that outlives the control. - GC.KeepAlive(geometry); + Assert.False(weakPath.IsAlive); } } - [Fact] + [ReleaseFact] public void Polyline_WithObservableCollectionPointsBinding_Is_Freed() { using (Start()) { var observableCollection = new ObservableCollection(){new()}; - Func run = () => + WeakReference Run() { + var polyline = new Polyline + { + Points = observableCollection + }; var window = new Window { - Content = new Polyline() - { - Points = observableCollection - } + Content = polyline }; window.Show(); window.LayoutManager.ExecuteInitialLayoutPass(); - Assert.IsType(window.Presenter.Child); + Assert.IsType(window.Presenter!.Child); window.Content = null; window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); - return window; - }; + return new WeakReference(polyline); + } - var result = run(); + var weakPolyline = Run(); + Assert.True(weakPolyline.IsAlive); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + CollectGarbage(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + Assert.False(weakPolyline.IsAlive); // We are keeping collection alive to simulate a resource that outlives the control. GC.KeepAlive(observableCollection); } } - [Fact] + [ReleaseFact] public void ElementName_Binding_In_DataTemplate_Is_Freed() { using (Start()) @@ -734,6 +711,9 @@ namespace Avalonia.LeakTests NameScope ns; TextBox tb; ListBox lb; + + var weakCanvases = new List(); + var window = new Window { [NameScope.NameScopeProperty] = ns = new NameScope(), @@ -754,7 +734,8 @@ namespace Avalonia.LeakTests { ItemsSource = items, ItemTemplate = new FuncDataTemplate((_, _) => - new Canvas + { + var canvas = new Canvas { Width = 10, Height = 10, @@ -762,11 +743,14 @@ namespace Avalonia.LeakTests { ElementName = "tb", Path = "Text", - NameScope = new WeakReference(ns), + NameScope = new WeakReference(ns), } - }), + }; + weakCanvases.Add(new WeakReference(canvas)); + return canvas; + }), Padding = new Thickness(0), - }), + }) } } }; @@ -779,7 +763,7 @@ namespace Avalonia.LeakTests void AssertInitialItemState() { var item0 = (ListBoxItem)lb.GetRealizedContainers().First(); - var canvas0 = (Canvas)item0.Presenter.Child; + var canvas0 = (Canvas)item0.Presenter!.Child!; Assert.Equal("foo", canvas0.Tag); } @@ -790,21 +774,24 @@ namespace Avalonia.LeakTests window.LayoutManager.ExecuteLayoutPass(); Assert.Empty(lb.GetRealizedContainers()); + Assert.Equal(10, weakCanvases.Count); + + foreach (var weakReference in weakCanvases) + Assert.True(weakReference.IsAlive); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + CollectGarbage(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + foreach (var weakReference in weakCanvases) + Assert.False(weakReference.IsAlive); } } - [Fact] + [ReleaseFact] public void HotKeyManager_Should_Release_Reference_When_Control_Detached() { using (Start()) { - Func run = () => + static WeakReference Run() { var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control); var tl = new Window @@ -818,32 +805,31 @@ namespace Avalonia.LeakTests tl.Content = button; tl.Template = CreateWindowTemplate(); tl.ApplyTemplate(); - tl.Presenter.ApplyTemplate(); + tl.Presenter!.ApplyTemplate(); HotKeyManager.SetHotKey(button, gesture1); // Detach the button from the logical tree, so there is no reference to it tl.Content = null; tl.ApplyTemplate(); - return tl; - }; + return new WeakReference(button); + } - var result = run(); + var weakButton = Run(); + Assert.True(weakButton.IsAlive); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + CollectGarbage(); - dotMemory.Check(memory => - Assert.Equal(0, memory.GetObjects(where => where.Type.Is