diff --git a/.gitignore b/.gitignore index c36f64e5de..640725fa26 100644 --- a/.gitignore +++ b/.gitignore @@ -162,7 +162,8 @@ $RECYCLE.BIN/ ################# ## Cake ################# -tools/ +tools/* +!tools/packages.config .nuget artifacts/ nuget diff --git a/build.cake b/build.cake index 6518431959..b3822271d4 100644 --- a/build.cake +++ b/build.cake @@ -11,7 +11,7 @@ // TOOLS /////////////////////////////////////////////////////////////////////////////// -#tool "nuget:?package=xunit.runner.console&version=2.1.0" +#tool "nuget:?package=xunit.runner.console&version=2.2.0" #tool "nuget:?package=OpenCover" /////////////////////////////////////////////////////////////////////////////// @@ -98,7 +98,6 @@ Task("Clean") CleanDirectory(parameters.TestsRoot); }); - Task("Restore-NuGet-Packages") .IsDependentOn("Clean") .WithCriteria(parameters.IsRunningOnWindows) @@ -171,23 +170,25 @@ void RunCoreTest(string dir, Parameters parameters, bool net461Only) continue; Information("Running for " + fw); DotNetCoreTest(System.IO.Path.Combine(dir, System.IO.Path.GetFileName(dir)+".csproj"), - new DotNetCoreTestSettings{Framework = fw}); + new DotNetCoreTestSettings { + Configuration = parameters.Configuration, + Framework = fw + }); } } - Task("Run-Net-Core-Unit-Tests") .IsDependentOn("Clean") .Does(() => { RunCoreTest("./tests/Avalonia.Base.UnitTests", parameters, false); - RunCoreTest("./tests/Avalonia.Controls.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Input.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Layout.UnitTests", parameters, true); - //RunCoreTest("./tests/Avalonia.Markup.UnitTests", parameters, true); - //RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Styling.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Visuals.UnitTests", parameters, true); + RunCoreTest("./tests/Avalonia.Controls.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Input.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Layout.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Markup.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Styling.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Visuals.UnitTests", parameters, false); }); Task("Run-Unit-Tests") diff --git a/build/Moq.props b/build/Moq.props index c8544b8309..55242d922e 100644 --- a/build/Moq.props +++ b/build/Moq.props @@ -1,5 +1,5 @@  - + diff --git a/build/XUnit.props b/build/XUnit.props index 58df7e8d3c..27e0afc987 100644 --- a/build/XUnit.props +++ b/build/XUnit.props @@ -7,7 +7,9 @@ - - + + + + diff --git a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj index 28e5e274d0..ac7d25a91e 100644 --- a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj +++ b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj @@ -181,5 +181,6 @@ + \ No newline at end of file diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index ed2b930114..f1f0749d41 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -14,8 +14,8 @@ namespace Avalonia.Layout /// public class LayoutManager : ILayoutManager { - private readonly HashSet _toMeasure = new HashSet(); - private readonly HashSet _toArrange = new HashSet(); + private readonly Queue _toMeasure = new Queue(); + private readonly Queue _toArrange = new Queue(); private bool _queued; private bool _running; @@ -25,8 +25,18 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - _toMeasure.Add(control); - _toArrange.Add(control); + if (!control.IsAttachedToVisualTree) + { +#if DEBUG + throw new AvaloniaInternalException( + "LayoutManager.InvalidateMeasure called on a control that is detached from the visual tree."); +#else + return; +#endif + } + + _toMeasure.Enqueue(control); + _toArrange.Enqueue(control); QueueLayoutPass(); } @@ -36,7 +46,17 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - _toArrange.Add(control); + if (!control.IsAttachedToVisualTree) + { +#if DEBUG + throw new AvaloniaInternalException( + "LayoutManager.InvalidateArrange called on a control that is detached from the visual tree."); +#else + return; +#endif + } + + _toArrange.Enqueue(control); QueueLayoutPass(); } @@ -103,8 +123,12 @@ namespace Avalonia.Layout { while (_toMeasure.Count > 0) { - var next = _toMeasure.First(); - Measure(next); + var control = _toMeasure.Dequeue(); + + if (!control.IsMeasureValid && control.IsAttachedToVisualTree) + { + Measure(control); + } } } @@ -112,53 +136,60 @@ namespace Avalonia.Layout { while (_toArrange.Count > 0 && _toMeasure.Count == 0) { - var next = _toArrange.First(); - Arrange(next); + var control = _toArrange.Dequeue(); + + if (!control.IsArrangeValid && control.IsAttachedToVisualTree) + { + Arrange(control); + } } } private void Measure(ILayoutable control) { - var root = control as ILayoutRoot; - var parent = control.VisualParent as ILayoutable; - - if (root != null) - { - root.Measure(root.MaxClientSize); - } - else if (parent != null) + // Controls closest to the visual root need to be arranged first. We don't try to store + // ordered invalidation lists, instead we traverse the tree upwards, measuring the + // controls closest to the root first. This has been shown by benchmarks to be the + // fastest and most memory-efficent algorithm. + if (control.VisualParent is ILayoutable parent) { Measure(parent); } - if (!control.IsMeasureValid) + // If the control being measured has IsMeasureValid == true here then its measure was + // handed by an ancestor and can be ignored. The measure may have also caused the + // control to be removed. + if (!control.IsMeasureValid && control.IsAttachedToVisualTree) { - control.Measure(control.PreviousMeasure ?? default(Size)); + if (control is ILayoutRoot root) + { + root.Measure(Size.Infinity); + } + else + { + control.Measure(control.PreviousMeasure.Value); + } } - - _toMeasure.Remove(control); } private void Arrange(ILayoutable control) { - var root = control as ILayoutRoot; - var parent = control.VisualParent as ILayoutable; - - if (root != null) - { - root.Arrange(new Rect(root.DesiredSize)); - } - else if (parent != null) + if (control.VisualParent is ILayoutable parent) { Arrange(parent); } - if (control.PreviousArrange.HasValue) + if (!control.IsArrangeValid && control.IsAttachedToVisualTree) { - control.Arrange(control.PreviousArrange.Value); + if (control is ILayoutRoot root) + { + root.Arrange(new Rect(control.DesiredSize)); + } + else + { + control.Arrange(control.PreviousArrange.Value); + } } - - _toArrange.Remove(control); } private void QueueLayoutPass() diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj index d55dc8d544..c656801d90 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs b/tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs index 8e5e3a305b..562de2dc06 100644 --- a/tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs +++ b/tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs @@ -7,4 +7,4 @@ using Xunit; [assembly: AssemblyTitle("Avalonia.UnitTests")] // Don't run tests in parallel. -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file +[assembly: CollectionBehavior(MaxParallelThreads = 1)] diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 1f5ebac203..21d7b186b4 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -49,6 +49,7 @@ + @@ -100,7 +101,7 @@ - + \ No newline at end of file diff --git a/tests/Avalonia.Benchmarks/Layout/Measure.cs b/tests/Avalonia.Benchmarks/Layout/Measure.cs new file mode 100644 index 0000000000..d1fdae9971 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Layout/Measure.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.UnitTests; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Layout +{ + [MemoryDiagnoser] + public class Measure : IDisposable + { + private IDisposable _app; + private TestRoot root; + private List controls = new List(); + + public Measure() + { + _app = UnitTestApplication.Start(TestServices.RealLayoutManager); + + var panel = new StackPanel(); + root = new TestRoot { Child = panel }; + controls.Add(panel); + CreateChildren(panel, 3, 5); + LayoutManager.Instance.ExecuteInitialLayoutPass(root); + } + + public void Dispose() + { + _app.Dispose(); + } + + [Benchmark] + public void Remeasure_Half() + { + var random = new Random(1); + + foreach (var control in controls) + { + if (random.Next(2) == 0) + { + control.InvalidateMeasure(); + } + } + + LayoutManager.Instance.ExecuteLayoutPass(); + } + + private void CreateChildren(IPanel parent, int childCount, int iterations) + { + for (var i = 0; i < childCount; ++i) + { + var control = new StackPanel(); + parent.Children.Add(control); + + if (iterations > 0) + { + CreateChildren(control, childCount, iterations - 1); + } + + controls.Add(control); + } + } + } +} diff --git a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs index 0af451efd2..33af55fdf9 100644 --- a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs +++ b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs @@ -11,6 +11,7 @@ using Avalonia.VisualTree; namespace Avalonia.Benchmarks.Styling { + [MemoryDiagnoser] public class ApplyStyling : IDisposable { private IDisposable _app; diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index f8235f7d68..f7b63cdb75 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj index d35542b51f..8dd8faf9db 100644 --- a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj +++ b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj index 8f9607fe67..86c9cf0617 100644 --- a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj +++ b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj b/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj index af33c80352..0950856dca 100644 --- a/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj +++ b/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index d76c9ea6bc..c4c0f9a441 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -2,22 +2,274 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Controls; +using Avalonia.UnitTests; +using System; using Xunit; +using System.Collections.Generic; namespace Avalonia.Layout.UnitTests { public class LayoutManagerTests { [Fact] - public void Invalidating_Child_Should_Remeasure_Parent() + public void Measures_And_Arranges_InvalidateMeasured_Control() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; + + control.InvalidateMeasure(); + target.ExecuteLayoutPass(); + + Assert.True(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Arranges_InvalidateArranged_Control() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; + + control.InvalidateArrange(); + target.ExecuteLayoutPass(); + + Assert.False(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Measures_Parent_Of_Newly_Added_Control() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot(); + + target.ExecuteInitialLayoutPass(root); + root.Child = control; + root.Measured = root.Arranged = false; + + target.ExecuteLayoutPass(); + + Assert.True(root.Measured); + Assert.True(root.Arranged); + Assert.True(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Measures_In_Correct_Order() + { + var target = new LayoutManager(); + + using (Start(target)) + { + LayoutTestControl control1; + LayoutTestControl control2; + var root = new LayoutTestRoot + { + Child = control1 = new LayoutTestControl + { + Child = control2 = new LayoutTestControl(), + } + }; + + + var order = new List(); + Size MeasureOverride(ILayoutable control, Size size) + { + order.Add(control); + return new Size(10, 10); + } + + root.DoMeasureOverride = MeasureOverride; + control1.DoMeasureOverride = MeasureOverride; + control2.DoMeasureOverride = MeasureOverride; + target.ExecuteInitialLayoutPass(root); + + control2.InvalidateMeasure(); + control1.InvalidateMeasure(); + root.InvalidateMeasure(); + + order.Clear(); + target.ExecuteLayoutPass(); + + Assert.Equal(new ILayoutable[] { root, control1, control2 }, order); + } + } + + [Fact] + public void Measures_Root_And_Grandparent_In_Correct_Order() + { + var target = new LayoutManager(); + + using (Start(target)) + { + LayoutTestControl control1; + LayoutTestControl control2; + var root = new LayoutTestRoot + { + Child = control1 = new LayoutTestControl + { + Child = control2 = new LayoutTestControl(), + } + }; + + + var order = new List(); + Size MeasureOverride(ILayoutable control, Size size) + { + order.Add(control); + return new Size(10, 10); + } + + root.DoMeasureOverride = MeasureOverride; + control1.DoMeasureOverride = MeasureOverride; + control2.DoMeasureOverride = MeasureOverride; + target.ExecuteInitialLayoutPass(root); + + control2.InvalidateMeasure(); + root.InvalidateMeasure(); + + order.Clear(); + target.ExecuteLayoutPass(); + + Assert.Equal(new ILayoutable[] { root, control2 }, order); + } + } + + [Fact] + public void Doesnt_Measure_Non_Invalidated_Root() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + root.Measured = root.Arranged = false; + control.Measured = control.Arranged = false; + + control.InvalidateMeasure(); + target.ExecuteLayoutPass(); + + Assert.False(root.Measured); + Assert.False(root.Arranged); + Assert.True(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Doesnt_Measure_Removed_Control() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; + + control.InvalidateMeasure(); + root.Child = null; + target.ExecuteLayoutPass(); + + Assert.False(control.Measured); + Assert.False(control.Arranged); + } + } + + [Fact] + public void Measures_Root_With_Infinity() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var root = new LayoutTestRoot(); + var availableSize = default(Size); + + // Should not measure with this size. + root.MaxClientSize = new Size(123, 456); + + root.DoMeasureOverride = (_, s) => + { + availableSize = s; + return new Size(100, 100); + }; + + target.ExecuteInitialLayoutPass(root); + + Assert.Equal(Size.Infinity, availableSize); + } + } + + [Fact] + public void Arranges_Root_With_DesiredSize() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var root = new LayoutTestRoot + { + Width = 100, + Height = 100, + }; + + var arrangeSize = default(Size); + + root.DoArrangeOverride = (_, s) => + { + arrangeSize = s; + return s; + }; + + target.ExecuteInitialLayoutPass(root); + Assert.Equal(new Size(100, 100), arrangeSize); + + root.Width = 120; + + target.ExecuteLayoutPass(); + Assert.Equal(new Size(120, 100), arrangeSize); + } + } + + [Fact] + public void Invalidating_Child_Remeasures_Parent() { using (AvaloniaLocator.EnterScope()) { - + AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); + Border border; StackPanel panel; - var root = new TestLayoutRoot + var root = new LayoutTestRoot { Child = panel = new StackPanel { @@ -38,5 +290,12 @@ namespace Avalonia.Layout.UnitTests Assert.Equal(new Size(100, 100), panel.DesiredSize); } } + + private IDisposable Start(LayoutManager layoutManager) + { + var result = AvaloniaLocator.EnterScope(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); + return result; + } } } diff --git a/tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs b/tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs new file mode 100644 index 0000000000..f7f072eb1e --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs @@ -0,0 +1,29 @@ +using System; +using Avalonia.Controls; + +namespace Avalonia.Layout.UnitTests +{ + internal class LayoutTestControl : Decorator + { + public bool Measured { get; set; } + public bool Arranged { get; set; } + public Func DoMeasureOverride { get; set; } + public Func DoArrangeOverride { get; set; } + + protected override Size MeasureOverride(Size availableSize) + { + Measured = true; + return DoMeasureOverride != null ? + DoMeasureOverride(this, availableSize) : + base.MeasureOverride(availableSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + Arranged = true; + return DoArrangeOverride != null ? + DoArrangeOverride(this, finalSize) : + base.ArrangeOverride(finalSize); + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs b/tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs new file mode 100644 index 0000000000..07476844e0 --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs @@ -0,0 +1,43 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.UnitTests; + +namespace Avalonia.Layout.UnitTests +{ + internal class LayoutTestRoot : TestRoot, ILayoutable + { + public bool Measured { get; set; } + public bool Arranged { get; set; } + public Func DoMeasureOverride { get; set; } + public Func DoArrangeOverride { get; set; } + + void ILayoutable.Measure(Size availableSize) + { + Measured = true; + Measure(availableSize); + } + + void ILayoutable.Arrange(Rect rect) + { + Arranged = true; + Arrange(rect); + } + + protected override Size MeasureOverride(Size availableSize) + { + return DoMeasureOverride != null ? + DoMeasureOverride(this, availableSize) : + base.MeasureOverride(availableSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + Arranged = true; + return DoArrangeOverride != null ? + DoArrangeOverride(this, finalSize) : + base.ArrangeOverride(finalSize); + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs new file mode 100644 index 0000000000..dcc65edc74 --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs @@ -0,0 +1,90 @@ +using System; +using Avalonia.Controls; +using Moq; +using Xunit; + +namespace Avalonia.Layout.UnitTests +{ + public class LayoutableTests + { + [Fact] + public void Only_Calls_LayoutManager_InvalidateMeasure_Once() + { + var target = new Mock(); + + using (Start(target.Object)) + { + var control = new Decorator(); + var root = new LayoutTestRoot { Child = control }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + target.ResetCalls(); + + control.InvalidateMeasure(); + control.InvalidateMeasure(); + + target.Verify(x => x.InvalidateMeasure(control), Times.Once()); + } + } + + [Fact] + public void Only_Calls_LayoutManager_InvalidateArrange_Once() + { + var target = new Mock(); + + using (Start(target.Object)) + { + var control = new Decorator(); + var root = new LayoutTestRoot { Child = control }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + target.ResetCalls(); + + control.InvalidateArrange(); + control.InvalidateArrange(); + + target.Verify(x => x.InvalidateArrange(control), Times.Once()); + } + } + + [Fact] + public void Attaching_Control_To_Tree_Invalidates_Parent_Measure() + { + var target = new Mock(); + + using (Start(target.Object)) + { + var control = new Decorator(); + var root = new LayoutTestRoot { Child = control }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + Assert.True(control.IsMeasureValid); + + root.Child = null; + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + Assert.False(control.IsMeasureValid); + Assert.True(root.IsMeasureValid); + + target.ResetCalls(); + + root.Child = control; + + Assert.False(root.IsMeasureValid); + Assert.False(control.IsMeasureValid); + target.Verify(x => x.InvalidateMeasure(root), Times.Once()); + } + } + + private IDisposable Start(ILayoutManager layoutManager) + { + var result = AvaloniaLocator.EnterScope(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); + return result; + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs b/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs deleted file mode 100644 index 8c4dfbb209..0000000000 --- a/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Avalonia.Controls; -using Avalonia.Platform; -using Avalonia.Rendering; - -namespace Avalonia.Layout.UnitTests -{ - internal class TestLayoutRoot : Decorator, ILayoutRoot, IRenderRoot - { - public TestLayoutRoot() - { - ClientSize = new Size(500, 500); - } - - public Size ClientSize - { - get; - set; - } - - public IRenderer Renderer => null; - - public IRenderTarget CreateRenderTarget() => null; - - public void Invalidate(Rect rect) - { - } - - public Point PointToClient(Point point) => point; - - public Point PointToScreen(Point point) => point; - - public Size MaxClientSize => Size.Infinity; - public double LayoutScaling => 1; - - public ILayoutManager LayoutManager { get; set; } = new LayoutManager(); - - } -} diff --git a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj index b7c4811495..3ccd3da044 100644 --- a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj +++ b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs index 062402d465..a08dfa39a6 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs @@ -183,7 +183,7 @@ namespace Avalonia.Markup.UnitTests.Data result); } - [Fact] + [Fact(Skip="Result is not always AggregateException.")] public async void Should_Return_BindingNotification_For_Invalid_FallbackValue() { #if NET461 @@ -203,13 +203,13 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal( new BindingNotification( new AggregateException( - new InvalidCastException("Could not convert 'foo' to 'System.Int32'"), + new InvalidCastException("'foo' is not a valid number."), new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")), BindingErrorType.Error), result); } - [Fact] + [Fact(Skip="Result is not always AggregateException.")] public async void Should_Return_BindingNotification_For_Invalid_FallbackValue_With_Data_Validation() { #if NET461 @@ -229,7 +229,7 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal( new BindingNotification( new AggregateException( - new InvalidCastException("Could not convert 'foo' to 'System.Int32'"), + new InvalidCastException("'foo' is not a valid number."), new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")), BindingErrorType.Error), result); @@ -286,6 +286,12 @@ namespace Avalonia.Markup.UnitTests.Data [Fact] public void Should_Pass_ConverterParameter_To_Convert() { +#if NET461 + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; +#else + CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; +#endif + var data = new Class1 { DoubleValue = 5.6 }; var converter = new Mock(); var target = new BindingExpression( @@ -296,12 +302,18 @@ namespace Avalonia.Markup.UnitTests.Data target.Subscribe(_ => { }); - converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentUICulture)); + converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.InvariantCulture)); } [Fact] public void Should_Pass_ConverterParameter_To_ConvertBack() { +#if NET461 + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; +#else + CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; +#endif + var data = new Class1 { DoubleValue = 5.6 }; var converter = new Mock(); var target = new BindingExpression( @@ -312,12 +324,18 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext("bar"); - converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentUICulture)); + converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.InvariantCulture)); } [Fact] public void Should_Handle_DataValidation() { +#if NET461 + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; +#else + CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; +#endif + var data = new Class1 { DoubleValue = 5.6 }; var converter = new Mock(); var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue", true), typeof(string)); diff --git a/tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs b/tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs index d1567d46be..4c3825ed44 100644 --- a/tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs +++ b/tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs @@ -37,4 +37,4 @@ using Xunit; [assembly: AssemblyFileVersion("1.0.0.0")] // Don't run tests in parallel. -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file +[assembly: CollectionBehavior(MaxParallelThreads = 1)] diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index 0cbdc142eb..f6f8f6bcb0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs index 034e9f74ce..24cc853318 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs @@ -7,4 +7,4 @@ using Xunit; [assembly: AssemblyTitle("Avalonia.Markup.Xaml.UnitTests")] // Don't run tests in parallel. -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file +[assembly: CollectionBehavior(MaxParallelThreads = 1)] diff --git a/tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj b/tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj index d35542b51f..8dd8faf9db 100644 --- a/tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj +++ b/tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index 628ccb2a1f..938fca8b4a 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -2,6 +2,7 @@ net461;netcoreapp1.1 false + Library true @@ -51,8 +52,5 @@ - - - \ No newline at end of file diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 7387602213..9be5fcf02e 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -43,7 +43,7 @@ namespace Avalonia.UnitTests public Size ClientSize => new Size(100, 100); - public Size MaxClientSize => Size.Infinity; + public Size MaxClientSize { get; set; } = Size.Infinity; public double LayoutScaling => 1; diff --git a/tools/packages.config b/tools/packages.config new file mode 100644 index 0000000000..5657d953fc --- /dev/null +++ b/tools/packages.config @@ -0,0 +1,4 @@ + + + +