Browse Source

Rewrite leak tests without dotMemory Unit (#20095)

* Remove dotMemory Unit from LeakTests

* Nuke: run leak tests normally
pull/20211/head
Julien Lebosquain 2 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\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

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 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<string> GetArguments(ToolOptions options);
}
void RunCoreTest(string projectName, Action<string, string> 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)

1
nukebuild/_build.csproj

@ -11,7 +11,6 @@
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
</PropertyGroup>
<Import Project="..\build\JetBrains.dotMemoryUnit.props" />
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="9.0.4" />
<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.Linq;
using Avalonia.Data;
using Avalonia.PropertyStore;
#nullable enable
@ -266,6 +267,21 @@ namespace Avalonia.Animation
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
{
public IDisposable? Instance { get; set; }

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

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

2
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()
{

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

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

9
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()
{

79
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<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);
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<AvaloniaList<string>>()).ObjectsCount));
Assert.False(weakSource.IsAlive);
}
[Fact]
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);
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<AvaloniaList<string>>()).ObjectsCount));
GC.Collect();
Assert.False(weakSource.IsAlive);
}
[Fact]
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);
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<NonIntegerIndexer>()).ObjectsCount));
Assert.False(weakSource.IsAlive);
}
[Fact]
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);
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<MethodBound>()).ObjectsCount));
Assert.False(weakSource.IsAlive);
}
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.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Reactive;
using Avalonia.Threading;
using Avalonia.UnitTests;
using JetBrains.dotMemoryUnit;
using Xunit;
using Xunit.Abstractions;
namespace Avalonia.LeakTests;
internal class ViewModelForDisposingTest
{
~ViewModelForDisposingTest() { ; }
[SuppressMessage("ReSharper", "EmptyDestructor", Justification = "Needed for test")]
~ViewModelForDisposingTest() { }
}
[DotMemoryUnit(FailIfRunWithoutSupport = false)]
public class DataContextTests
public class DataContextTests : ScopedTestBase
{
public DataContextTests(ITestOutputHelper atr)
{
DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine);
}
[Fact]
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 lifetime = new ClassicDesktopStyleApplicationLifetime();
lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown;
var window = new Window { DataContext = new ViewModelForDisposingTest() };
var viewModel = new ViewModelForDisposingTest();
var window = new Window { DataContext = viewModel };
window.Show();
window.Close();
return Disposable.Create(lifetime, lt => lt.Shutdown())
var disposable = Disposable.Create(lifetime, lt => lt.Shutdown())
.DisposeWith(new CompositeDisposable(lifetime, unitTestApp));
weakDataContext = new WeakReference(viewModel);
return disposable;
}
using var _ = Run();
// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
GC.Collect();
using var _ = Run(out var weakDataContext);
Assert.True(weakDataContext.IsAlive);
CollectGarbage();
dotMemory.Check(m => Assert.Equal(0,
m.GetObjects(o => o.Type.Is<ViewModelForDisposingTest>()).ObjectsCount));
Assert.False(weakDataContext.IsAlive);
}
[Fact]
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);
var window = new Window { DataContext = new ViewModelForDisposingTest() };
var viewModel = new ViewModelForDisposingTest();
var window = new Window { DataContext = viewModel };
window.Show();
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)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
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 Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.UnitTests;
using JetBrains.dotMemoryUnit;
using Xunit;
using Xunit.Abstractions;
namespace Avalonia.LeakTests
{
[DotMemoryUnit(FailIfRunWithoutSupport = false)]
public class TransitionTests
public class TransitionTests : ScopedTestBase
{
public TransitionTests(ITestOutputHelper atr)
{
DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine);
}
[Fact]
public void Transition_On_StyledProperty_Is_Freed()
{
@ -26,19 +17,11 @@ namespace Avalonia.LeakTests
using (UnitTestApplication.Start(TestServices.StyledWindow.With(globalClock: clock)))
{
Func<Border> run = () =>
WeakReference Run()
{
var border = new Border
{
Transitions = new Transitions()
{
new DoubleTransition
{
Duration = TimeSpan.FromSeconds(1),
Property = Border.OpacityProperty,
}
}
};
var opacityTransition = new DoubleTransition { Duration = TimeSpan.FromSeconds(1), Property = Border.OpacityProperty, };
var border = new Border { Transitions = new Transitions() { opacityTransition } };
var window = new Window();
window.Content = border;
window.Show();
@ -50,22 +33,27 @@ namespace Avalonia.LeakTests
Assert.Equal(0.5, border.Opacity);
var transitionInstance = border.TryGetTransitionInstance(opacityTransition);
Assert.NotNull(transitionInstance);
clock.Pulse(TimeSpan.FromSeconds(1));
Assert.Equal(0, border.Opacity);
window.Close();
return border;
};
return new WeakReference(transitionInstance);
}
var weakTransitionInstance = Run();
Assert.True(weakTransitionInstance.IsAlive);
var result = run();
CollectGarbage();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TransitionInstance>()).ObjectsCount));
Assert.False(weakTransitionInstance.IsAlive);
}
}
[Fact]
public void Shared_Transition_Collection_Is_Not_Leaking()
{
@ -85,8 +73,8 @@ namespace Avalonia.LeakTests
BasedOn = Application.Current?.Resources[typeof(Button)] as ControlTheme,
Setters = { new Setter(Animatable.TransitionsProperty, sharedTransitions) }
};
Func<Window> run = () =>
WeakReference Run()
{
var button = new Button() { Theme = controlTheme };
var window = new Window();
@ -95,16 +83,15 @@ namespace Avalonia.LeakTests
window.Content = null;
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)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
CollectGarbage();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Button>()).ObjectsCount));
Assert.False(weakButton.IsAlive);
}
}
@ -126,31 +113,38 @@ namespace Avalonia.LeakTests
BasedOn = Application.Current?.Resources[typeof(Button)] as ControlTheme,
Setters = { new Setter(Animatable.TransitionsProperty, sharedTransitions) }
};
Func<Window> run = () =>
WeakReference Run()
{
var window = new Window();
window.Show();
var button = new Button() { Theme = controlTheme };
window.Content = new UserControl
{
Content = new Button() { Theme = controlTheme },
Content = button,
// When invisible, Button won't be attached to the visual tree
IsVisible = false
};
window.Content = null;
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)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
CollectGarbage();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Button>()).ObjectsCount));
Assert.False(weakButton.IsAlive);
}
}
private static void CollectGarbage()
{
// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
GC.Collect();
}
}
}

Loading…
Cancel
Save