Browse Source

Rewrite leak tests without dotMemory Unit (#20095)

* Remove dotMemory Unit from LeakTests

* Nuke: run leak tests normally
pull/20211/head
Julien Lebosquain 10 months ago
committed by GitHub
parent
commit
0f8f70437c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      Avalonia.sln
  2. 5
      build/JetBrains.dotMemoryUnit.props
  3. 28
      nukebuild/Build.cs
  4. 1
      nukebuild/_build.csproj
  5. 16
      src/Avalonia.Base/Animation/Animatable.cs
  6. 5
      src/Avalonia.Base/Animation/TransitionObservableBase.cs
  7. 2
      src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
  8. 2
      tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj
  9. 9
      tests/Avalonia.LeakTests/AvaloniaObjectTests.cs
  10. 79
      tests/Avalonia.LeakTests/BindingExpressionTests.cs
  11. 611
      tests/Avalonia.LeakTests/ControlTests.cs
  12. 56
      tests/Avalonia.LeakTests/DataContextTests.cs
  13. 16
      tests/Avalonia.LeakTests/ReleaseFact.cs
  14. 88
      tests/Avalonia.LeakTests/TransitionTests.cs

1
Avalonia.sln

@ -90,7 +90,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\EmbedXaml.props = build\EmbedXaml.props build\EmbedXaml.props = build\EmbedXaml.props
build\HarfBuzzSharp.props = build\HarfBuzzSharp.props build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
build\ImageSharp.props = build\ImageSharp.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.CSharp.props = build\Microsoft.CSharp.props
build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props
build\Moq.props = build\Moq.props build\Moq.props = build\Moq.props

5
build/JetBrains.dotMemoryUnit.props

@ -1,5 +0,0 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="JetBrains.DotMemoryUnit" Version="3.2.20220510" />
</ItemGroup>
</Project>

28
nukebuild/Build.cs

@ -15,7 +15,6 @@ using Nuke.Common.Tools.Npm;
using Nuke.Common.Utilities; using Nuke.Common.Utilities;
using static Nuke.Common.EnvironmentInfo; using static Nuke.Common.EnvironmentInfo;
using static Nuke.Common.IO.PathConstruction; using static Nuke.Common.IO.PathConstruction;
using static Nuke.Common.Tools.DotMemoryUnit.DotMemoryUnitTasks;
using static Nuke.Common.Tools.DotNet.DotNetTasks; using static Nuke.Common.Tools.DotNet.DotNetTasks;
using static Serilog.Log; using static Serilog.Log;
using MicroCom.CodeGenerator; 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<string> GetArguments(ToolOptions options);
}
void RunCoreTest(string projectName, Action<string, string> runTest) void RunCoreTest(string projectName, Action<string, string> runTest)
{ {
Information($"Running tests from {projectName}"); Information($"Running tests from {projectName}");
@ -325,11 +307,7 @@ partial class Build : NukeBuild
.DependsOn(Compile) .DependsOn(Compile)
.Executes(() => .Executes(() =>
{ {
void DoMemoryTest() RunCoreTest("Avalonia.LeakTests");
{
RunCoreDotMemoryUnit("Avalonia.LeakTests");
}
ControlFlow.ExecuteWithRetry(DoMemoryTest, delay: TimeSpan.FromMilliseconds(3));
}); });
Target ZipFiles => _ => _ Target ZipFiles => _ => _
@ -418,8 +396,8 @@ partial class Build : NukeBuild
.DependsOn(RunCoreLibsTests) .DependsOn(RunCoreLibsTests)
.DependsOn(RunRenderTests) .DependsOn(RunRenderTests)
.DependsOn(RunToolsTests) .DependsOn(RunToolsTests)
.DependsOn(RunHtmlPreviewerTests); .DependsOn(RunHtmlPreviewerTests)
//.DependsOn(RunLeakTests); // dotMemory Unit doesn't support modern .NET versions, see https://youtrack.jetbrains.com/issue/DMU-300/ .DependsOn(RunLeakTests);
Target Package => _ => _ Target Package => _ => _
.DependsOn(RunTests) .DependsOn(RunTests)

1
nukebuild/_build.csproj

@ -11,7 +11,6 @@
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization> <EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
</PropertyGroup> </PropertyGroup>
<Import Project="..\build\JetBrains.dotMemoryUnit.props" />
<ItemGroup> <ItemGroup>
<PackageReference Include="Nuke.Common" Version="9.0.4" /> <PackageReference Include="Nuke.Common" Version="9.0.4" />
<PackageReference Include="MicroCom.CodeGenerator" Version="0.11.0" /> <PackageReference Include="MicroCom.CodeGenerator" Version="0.11.0" />

16
src/Avalonia.Base/Animation/Animatable.cs

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.PropertyStore;
#nullable enable #nullable enable
@ -266,6 +267,21 @@ namespace Avalonia.Animation
return value; return value;
} }
/// <summary>
/// For unit tests only!
/// </summary>
internal TransitionInstance? TryGetTransitionInstance<TValue>(Transition<TValue> transition)
{
if (_transitionState is not null && _transitionState.TryGetValue(transition, out var transitionState))
{
var valueEntry = transitionState.Instance as BindingEntryBase<TValue, TValue>;
var transitionObservable = valueEntry?.Source as TransitionObservableBase<TValue>;
return transitionObservable?.Progress as TransitionInstance;
}
return null;
}
private class TransitionState private class TransitionState
{ {
public IDisposable? Instance { get; set; } public IDisposable? Instance { get; set; }

5
src/Avalonia.Base/Animation/TransitionObservableBase.cs

@ -22,6 +22,11 @@ namespace Avalonia.Animation
_easing = easing; _easing = easing;
} }
/// <summary>
/// For unit tests only!
/// </summary>
internal IObservable<double> Progress => _progress;
/// <summary> /// <summary>
/// Produces value at given progress time point. /// Produces value at given progress time point.
/// </summary> /// </summary>

2
src/Avalonia.Base/PropertyStore/BindingEntryBase.cs

@ -51,7 +51,7 @@ namespace Avalonia.PropertyStore
public AvaloniaProperty Property { get; } public AvaloniaProperty Property { get; }
AvaloniaProperty IValueEntry.Property => Property; AvaloniaProperty IValueEntry.Property => Property;
protected ValueFrame Frame { get; } protected ValueFrame Frame { get; }
protected object Source { get; } protected internal object Source { get; }
public void Dispose() public void Dispose()
{ {

2
tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj

@ -2,9 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework> <TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
</PropertyGroup> </PropertyGroup>
<Import Project="..\..\build\JetBrains.dotMemoryUnit.props" />
<Import Project="..\..\build\Moq.props" /> <Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\XUnit.props" /> <Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\NetFX.props" /> <Import Project="..\..\build\NetFX.props" />
<Import Project="..\..\build\SharedVersion.props" /> <Import Project="..\..\build\SharedVersion.props" />

9
tests/Avalonia.LeakTests/AvaloniaObjectTests.cs

@ -1,7 +1,6 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -12,20 +11,12 @@ using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using JetBrains.dotMemoryUnit;
using Xunit; using Xunit;
using Xunit.Abstractions;
namespace Avalonia.LeakTests namespace Avalonia.LeakTests
{ {
[DotMemoryUnit(FailIfRunWithoutSupport = false)]
public class AvaloniaObjectTests : ScopedTestBase public class AvaloniaObjectTests : ScopedTestBase
{ {
public AvaloniaObjectTests(ITestOutputHelper atr)
{
DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine);
}
[Fact] [Fact]
public void Binding_To_Direct_Property_Does_Not_Get_Collected() public void Binding_To_Direct_Property_Does_Not_Get_Collected()
{ {

79
tests/Avalonia.LeakTests/BindingExpressionTests.cs

@ -2,91 +2,94 @@
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Markup.Data;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using JetBrains.dotMemoryUnit;
using Xunit; using Xunit;
using Xunit.Abstractions;
namespace Avalonia.LeakTests namespace Avalonia.LeakTests
{ {
[DotMemoryUnit(FailIfRunWithoutSupport = false)] public class BindingExpressionTests : ScopedTestBase
public class BindingExpressionTests
{ {
public BindingExpressionTests(ITestOutputHelper atr)
{
DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine);
}
[Fact] [Fact]
public void Should_Not_Keep_Source_Alive_ObservableCollection() public void Should_Not_Keep_Source_Alive_ObservableCollection()
{ {
Func<BindingExpression> run = () => static WeakReference CreateExpression()
{ {
var source = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var list = new AvaloniaList<string> { "foo", "bar" };
var source = new { Foo = list };
var target = BindingExpression.Create(source, o => o.Foo); var target = BindingExpression.Create(source, o => o.Foo);
target.ToObservable().Subscribe(_ => { }); 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.False(weakSource.IsAlive);
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<AvaloniaList<string>>()).ObjectsCount));
} }
[Fact] [Fact]
public void Should_Not_Keep_Source_Alive_ObservableCollection_With_DataValidation() public void Should_Not_Keep_Source_Alive_ObservableCollection_With_DataValidation()
{ {
Func<BindingExpression> run = () => static WeakReference CreateExpression()
{ {
var source = new { Foo = new AvaloniaList<string> { "foo", "bar" } }; var list = new AvaloniaList<string> { "foo", "bar" };
var source = new { Foo = list };
var target = BindingExpression.Create(source, o => o.Foo, enableDataValidation: true); var target = BindingExpression.Create(source, o => o.Foo, enableDataValidation: true);
target.ToObservable().Subscribe(_ => { }); target.ToObservable().Subscribe(_ => { });
return target; return new WeakReference(list);
}; }
var result = run(); var weakSource = CreateExpression();
Assert.True(weakSource.IsAlive);
dotMemory.Check(memory => GC.Collect();
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<AvaloniaList<string>>()).ObjectsCount));
Assert.False(weakSource.IsAlive);
} }
[Fact] [Fact]
public void Should_Not_Keep_Source_Alive_NonIntegerIndexer() public void Should_Not_Keep_Source_Alive_NonIntegerIndexer()
{ {
Func<BindingExpression> 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); var target = BindingExpression.Create(source, o => o.Foo);
target.ToObservable().Subscribe(_ => { }); 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.False(weakSource.IsAlive);
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<NonIntegerIndexer>()).ObjectsCount));
} }
[Fact] [Fact]
public void Should_Not_Keep_Source_Alive_MethodBinding() public void Should_Not_Keep_Source_Alive_MethodBinding()
{ {
Func<BindingExpression> 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); var target = BindingExpression.Create(source, o => (Action)o.Foo.A);
target.ToObservable().Subscribe(_ => { }); 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.False(weakSource.IsAlive);
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<MethodBound>()).ObjectsCount));
} }
private class MethodBound private class MethodBound

611
tests/Avalonia.LeakTests/ControlTests.cs

File diff suppressed because it is too large

56
tests/Avalonia.LeakTests/DataContextTests.cs

@ -1,70 +1,76 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using JetBrains.dotMemoryUnit;
using Xunit; using Xunit;
using Xunit.Abstractions;
namespace Avalonia.LeakTests; namespace Avalonia.LeakTests;
internal class ViewModelForDisposingTest internal class ViewModelForDisposingTest
{ {
~ViewModelForDisposingTest() { ; } [SuppressMessage("ReSharper", "EmptyDestructor", Justification = "Needed for test")]
~ViewModelForDisposingTest() { }
} }
[DotMemoryUnit(FailIfRunWithoutSupport = false)] public class DataContextTests : ScopedTestBase
public class DataContextTests
{ {
public DataContextTests(ITestOutputHelper atr)
{
DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine);
}
[Fact] [Fact]
public void Window_DataContext_Disposed_After_Window_Close_With_Lifetime() public void Window_DataContext_Disposed_After_Window_Close_With_Lifetime()
{ {
static IDisposable Run() static IDisposable Run(out WeakReference weakDataContext)
{ {
var unitTestApp = UnitTestApplication.Start(TestServices.StyledWindow); var unitTestApp = UnitTestApplication.Start(TestServices.StyledWindow);
var lifetime = new ClassicDesktopStyleApplicationLifetime(); var lifetime = new ClassicDesktopStyleApplicationLifetime();
lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown;
var window = new Window { DataContext = new ViewModelForDisposingTest() }; var viewModel = new ViewModelForDisposingTest();
var window = new Window { DataContext = viewModel };
window.Show(); window.Show();
window.Close(); window.Close();
return Disposable.Create(lifetime, lt => lt.Shutdown()) var disposable = Disposable.Create(lifetime, lt => lt.Shutdown())
.DisposeWith(new CompositeDisposable(lifetime, unitTestApp)); .DisposeWith(new CompositeDisposable(lifetime, unitTestApp));
weakDataContext = new WeakReference(viewModel);
return disposable;
} }
using var _ = Run(); using var _ = Run(out var weakDataContext);
// Process all Loaded events to free control reference(s) Assert.True(weakDataContext.IsAlive);
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
GC.Collect(); CollectGarbage();
dotMemory.Check(m => Assert.Equal(0, Assert.False(weakDataContext.IsAlive);
m.GetObjects(o => o.Type.Is<ViewModelForDisposingTest>()).ObjectsCount));
} }
[Fact] [Fact]
public void Window_DataContext_Disposed_After_Window_Close_Without_Lifetime() public void Window_DataContext_Disposed_After_Window_Close_Without_Lifetime()
{ {
static void Run() static void Run(out WeakReference weakDataContext)
{ {
using var _ = UnitTestApplication.Start(TestServices.StyledWindow); using var _ = UnitTestApplication.Start(TestServices.StyledWindow);
var window = new Window { DataContext = new ViewModelForDisposingTest() }; var viewModel = new ViewModelForDisposingTest();
var window = new Window { DataContext = viewModel };
window.Show(); window.Show();
window.Close(); window.Close();
weakDataContext = new WeakReference(viewModel);
} }
Run(); Run(out var weakDataContext);
Assert.True(weakDataContext.IsAlive);
CollectGarbage();
Assert.False(weakDataContext.IsAlive);
}
private static void CollectGarbage()
{
// Process all Loaded events to free control reference(s) // Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
GC.Collect(); GC.Collect();
dotMemory.Check(m => Assert.Equal(0,
m.GetObjects(o => o.Type.Is<ViewModelForDisposingTest>()).ObjectsCount));
} }
} }

16
tests/Avalonia.LeakTests/ReleaseFact.cs

@ -0,0 +1,16 @@
using Xunit;
namespace Avalonia.LeakTests;
/// <summary>
/// Use on leak tests where objects are somehow kept rooted in debug mode.
/// </summary>
internal sealed class ReleaseFactAttribute : FactAttribute
{
public ReleaseFactAttribute()
{
#if DEBUG
Skip = "Only runs in Release mode";
#endif
}
}

88
tests/Avalonia.LeakTests/TransitionTests.cs

@ -1,24 +1,15 @@
using System; using System;
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using JetBrains.dotMemoryUnit;
using Xunit; using Xunit;
using Xunit.Abstractions;
namespace Avalonia.LeakTests namespace Avalonia.LeakTests
{ {
[DotMemoryUnit(FailIfRunWithoutSupport = false)] public class TransitionTests : ScopedTestBase
public class TransitionTests
{ {
public TransitionTests(ITestOutputHelper atr)
{
DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine);
}
[Fact] [Fact]
public void Transition_On_StyledProperty_Is_Freed() public void Transition_On_StyledProperty_Is_Freed()
{ {
@ -26,19 +17,11 @@ namespace Avalonia.LeakTests
using (UnitTestApplication.Start(TestServices.StyledWindow.With(globalClock: clock))) using (UnitTestApplication.Start(TestServices.StyledWindow.With(globalClock: clock)))
{ {
Func<Border> run = () => WeakReference Run()
{ {
var border = new Border var opacityTransition = new DoubleTransition { Duration = TimeSpan.FromSeconds(1), Property = Border.OpacityProperty, };
{
Transitions = new Transitions() var border = new Border { Transitions = new Transitions() { opacityTransition } };
{
new DoubleTransition
{
Duration = TimeSpan.FromSeconds(1),
Property = Border.OpacityProperty,
}
}
};
var window = new Window(); var window = new Window();
window.Content = border; window.Content = border;
window.Show(); window.Show();
@ -50,22 +33,27 @@ namespace Avalonia.LeakTests
Assert.Equal(0.5, border.Opacity); Assert.Equal(0.5, border.Opacity);
var transitionInstance = border.TryGetTransitionInstance(opacityTransition);
Assert.NotNull(transitionInstance);
clock.Pulse(TimeSpan.FromSeconds(1)); clock.Pulse(TimeSpan.FromSeconds(1));
Assert.Equal(0, border.Opacity); Assert.Equal(0, border.Opacity);
window.Close(); window.Close();
return border; return new WeakReference(transitionInstance);
}; }
var weakTransitionInstance = Run();
Assert.True(weakTransitionInstance.IsAlive);
var result = run(); CollectGarbage();
dotMemory.Check(memory => Assert.False(weakTransitionInstance.IsAlive);
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TransitionInstance>()).ObjectsCount));
} }
} }
[Fact] [Fact]
public void Shared_Transition_Collection_Is_Not_Leaking() public void Shared_Transition_Collection_Is_Not_Leaking()
{ {
@ -85,8 +73,8 @@ namespace Avalonia.LeakTests
BasedOn = Application.Current?.Resources[typeof(Button)] as ControlTheme, BasedOn = Application.Current?.Resources[typeof(Button)] as ControlTheme,
Setters = { new Setter(Animatable.TransitionsProperty, sharedTransitions) } Setters = { new Setter(Animatable.TransitionsProperty, sharedTransitions) }
}; };
Func<Window> run = () => WeakReference Run()
{ {
var button = new Button() { Theme = controlTheme }; var button = new Button() { Theme = controlTheme };
var window = new Window(); var window = new Window();
@ -95,16 +83,15 @@ namespace Avalonia.LeakTests
window.Content = null; window.Content = null;
window.Close(); window.Close();
return window; return new WeakReference(button);
}; }
var result = run(); var weakButton = Run();
Assert.True(weakButton.IsAlive);
// Process all Loaded events to free control reference(s) CollectGarbage();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
dotMemory.Check(memory => Assert.False(weakButton.IsAlive);
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Button>()).ObjectsCount));
} }
} }
@ -126,31 +113,38 @@ namespace Avalonia.LeakTests
BasedOn = Application.Current?.Resources[typeof(Button)] as ControlTheme, BasedOn = Application.Current?.Resources[typeof(Button)] as ControlTheme,
Setters = { new Setter(Animatable.TransitionsProperty, sharedTransitions) } Setters = { new Setter(Animatable.TransitionsProperty, sharedTransitions) }
}; };
Func<Window> run = () => WeakReference Run()
{ {
var window = new Window(); var window = new Window();
window.Show(); window.Show();
var button = new Button() { Theme = controlTheme };
window.Content = new UserControl window.Content = new UserControl
{ {
Content = new Button() { Theme = controlTheme }, Content = button,
// When invisible, Button won't be attached to the visual tree // When invisible, Button won't be attached to the visual tree
IsVisible = false IsVisible = false
}; };
window.Content = null; window.Content = null;
window.Close(); window.Close();
return window; return new WeakReference(button);
}; }
var result = run(); var weakButton = Run();
Assert.True(weakButton.IsAlive);
// Process all Loaded events to free control reference(s) CollectGarbage();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
dotMemory.Check(memory => Assert.False(weakButton.IsAlive);
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Button>()).ObjectsCount));
} }
} }
private static void CollectGarbage()
{
// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
GC.Collect();
}
} }
} }

Loading…
Cancel
Save