committed by
GitHub
71 changed files with 1273 additions and 1511 deletions
@ -0,0 +1,19 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks> |
||||
|
<IsTestProject>false</IsTestProject> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<!-- Use lower minor version, as it is supposed to be compatible --> |
||||
|
<PackageReference Include="NUnit" Version="3.13.0" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<Import Project="..\..\..\build\ApiDiff.props" /> |
||||
|
<Import Project="..\..\..\build\DevAnalyzers.props" /> |
||||
|
<Import Project="..\..\..\build\NullableEnable.props" /> |
||||
|
</Project> |
||||
@ -0,0 +1,25 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Reflection; |
||||
|
using System.Threading; |
||||
|
using NUnit.Framework; |
||||
|
using NUnit.Framework.Interfaces; |
||||
|
using NUnit.Framework.Internal.Commands; |
||||
|
|
||||
|
namespace Avalonia.Headless.NUnit; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Identifies a nunit test that starts on Avalonia Dispatcher
|
||||
|
/// such that awaited expressions resume on the test's "main thread".
|
||||
|
/// </summary>
|
||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] |
||||
|
public sealed class AvaloniaTestAttribute : TestCaseAttribute, IWrapSetUpTearDown |
||||
|
{ |
||||
|
public TestCommand Wrap(TestCommand command) |
||||
|
{ |
||||
|
var session = |
||||
|
HeadlessUnitTestSession.GetOrStartForAssembly(command.Test.Method?.MethodInfo.DeclaringType?.Assembly); |
||||
|
|
||||
|
return AvaloniaTestMethodCommand.ProcessCommand(session, command); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Reflection; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Threading; |
||||
|
using NUnit.Framework.Interfaces; |
||||
|
using NUnit.Framework.Internal; |
||||
|
using NUnit.Framework.Internal.Commands; |
||||
|
|
||||
|
namespace Avalonia.Headless.NUnit; |
||||
|
|
||||
|
internal class AvaloniaTestMethodCommand : TestCommand |
||||
|
{ |
||||
|
private readonly HeadlessUnitTestSession _session; |
||||
|
private readonly TestCommand _innerCommand; |
||||
|
private readonly List<Action> _beforeTest; |
||||
|
private readonly List<Action> _afterTest; |
||||
|
|
||||
|
// There are multiple problems with NUnit integration at the moment when we wrote this integration.
|
||||
|
// NUnit doesn't have extensibility API for running on custom dispatcher/sync-context.
|
||||
|
// See https://github.com/nunit/nunit/issues/2917 https://github.com/nunit/nunit/issues/2774
|
||||
|
// To workaround that we had to replace inner TestMethodCommand with our own implementation while keeping original hierarchy of commands.
|
||||
|
// Which will respect proper async/await awaiting code that works with our session and can be block-awaited to fit in NUnit.
|
||||
|
// Also, we need to push BeforeTest/AfterTest callbacks to the very same session call.
|
||||
|
// I hope there will be a better solution without reflection, but for now that's it.
|
||||
|
private static FieldInfo s_innerCommand = typeof(DelegatingTestCommand) |
||||
|
.GetField("innerCommand", BindingFlags.Instance | BindingFlags.NonPublic)!; |
||||
|
private static FieldInfo s_beforeTest = typeof(BeforeAndAfterTestCommand) |
||||
|
.GetField("BeforeTest", BindingFlags.Instance | BindingFlags.NonPublic)!; |
||||
|
private static FieldInfo s_afterTest = typeof(BeforeAndAfterTestCommand) |
||||
|
.GetField("AfterTest", BindingFlags.Instance | BindingFlags.NonPublic)!; |
||||
|
|
||||
|
private AvaloniaTestMethodCommand( |
||||
|
HeadlessUnitTestSession session, |
||||
|
TestCommand innerCommand, |
||||
|
List<Action> beforeTest, |
||||
|
List<Action> afterTest) |
||||
|
: base(innerCommand.Test) |
||||
|
{ |
||||
|
_session = session; |
||||
|
_innerCommand = innerCommand; |
||||
|
_beforeTest = beforeTest; |
||||
|
_afterTest = afterTest; |
||||
|
} |
||||
|
|
||||
|
public static TestCommand ProcessCommand(HeadlessUnitTestSession session, TestCommand command) |
||||
|
{ |
||||
|
return ProcessCommand(session, command, new List<Action>(), new List<Action>()); |
||||
|
} |
||||
|
|
||||
|
private static TestCommand ProcessCommand(HeadlessUnitTestSession session, TestCommand command, List<Action> before, List<Action> after) |
||||
|
{ |
||||
|
if (command is BeforeAndAfterTestCommand beforeAndAfterTestCommand) |
||||
|
{ |
||||
|
if (s_beforeTest.GetValue(beforeAndAfterTestCommand) is Action<TestExecutionContext> beforeTest) |
||||
|
{ |
||||
|
Action<TestExecutionContext> beforeAction = c => before.Add(() => beforeTest(c)); |
||||
|
s_beforeTest.SetValue(beforeAndAfterTestCommand, beforeAction); |
||||
|
} |
||||
|
if (s_afterTest.GetValue(beforeAndAfterTestCommand) is Action<TestExecutionContext> afterTest) |
||||
|
{ |
||||
|
Action<TestExecutionContext> afterAction = c => after.Add(() => afterTest(c)); |
||||
|
s_afterTest.SetValue(beforeAndAfterTestCommand, afterAction); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (command is DelegatingTestCommand delegatingTestCommand |
||||
|
&& s_innerCommand.GetValue(delegatingTestCommand) is TestCommand inner) |
||||
|
{ |
||||
|
s_innerCommand.SetValue(delegatingTestCommand, ProcessCommand(session, inner, before, after)); |
||||
|
} |
||||
|
else if (command is TestMethodCommand methodCommand) |
||||
|
{ |
||||
|
return new AvaloniaTestMethodCommand(session, methodCommand, before, after); |
||||
|
} |
||||
|
|
||||
|
return command; |
||||
|
} |
||||
|
|
||||
|
public override TestResult Execute(TestExecutionContext context) |
||||
|
{ |
||||
|
return _session.Dispatch(() => ExecuteTestMethod(context), default).GetAwaiter().GetResult(); |
||||
|
} |
||||
|
|
||||
|
// Unfortunately, NUnit has issues with custom synchronization contexts, which means we need to add some hacks to make it work.
|
||||
|
private async Task<TestResult> ExecuteTestMethod(TestExecutionContext context) |
||||
|
{ |
||||
|
_beforeTest.ForEach(a => a()); |
||||
|
|
||||
|
var testMethod = _innerCommand.Test.Method; |
||||
|
var methodInfo = testMethod!.MethodInfo; |
||||
|
|
||||
|
var result = methodInfo.Invoke(context.TestObject, _innerCommand.Test.Arguments); |
||||
|
// Only Task, non generic ValueTask are supported in async context. No ValueTask<> nor F# tasks.
|
||||
|
if (result is Task task) |
||||
|
{ |
||||
|
await task; |
||||
|
} |
||||
|
else if (result is ValueTask valueTask) |
||||
|
{ |
||||
|
await valueTask; |
||||
|
} |
||||
|
|
||||
|
context.CurrentResult.SetResult(ResultState.Success); |
||||
|
|
||||
|
if (context.CurrentResult.AssertionResults.Count > 0) |
||||
|
context.CurrentResult.RecordTestCompletion(); |
||||
|
|
||||
|
if (context.ExecutionStatus != TestExecutionStatus.AbortRequested) |
||||
|
{ |
||||
|
_afterTest.ForEach(a => a()); |
||||
|
} |
||||
|
|
||||
|
return context.CurrentResult; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Reflection; |
||||
|
using System.Threading; |
||||
|
using NUnit.Framework; |
||||
|
using NUnit.Framework.Interfaces; |
||||
|
using NUnit.Framework.Internal.Commands; |
||||
|
|
||||
|
namespace Avalonia.Headless.NUnit; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Identifies a nunit theory that starts on Avalonia Dispatcher
|
||||
|
/// such that awaited expressions resume on the test's "main thread".
|
||||
|
/// </summary>
|
||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] |
||||
|
public sealed class AvaloniaTheoryAttribute : TheoryAttribute, IWrapSetUpTearDown |
||||
|
{ |
||||
|
public TestCommand Wrap(TestCommand command) |
||||
|
{ |
||||
|
var session = HeadlessUnitTestSession.GetOrStartForAssembly(command.Test.Method?.MethodInfo.DeclaringType?.Assembly); |
||||
|
|
||||
|
return AvaloniaTestMethodCommand.ProcessCommand(session, command); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using System.Threading; |
||||
|
using Xunit; |
||||
|
using Xunit.Abstractions; |
||||
|
using Xunit.Sdk; |
||||
|
|
||||
|
namespace Avalonia.Headless.XUnit; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Identifies an xunit test that starts on Avalonia Dispatcher
|
||||
|
/// such that awaited expressions resume on the test's "main thread".
|
||||
|
/// </summary>
|
||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] |
||||
|
[XunitTestCaseDiscoverer("Avalonia.Headless.XUnit.AvaloniaUIFactDiscoverer", "Avalonia.Headless.XUnit")] |
||||
|
public sealed class AvaloniaFactAttribute : FactAttribute |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
public class AvaloniaUIFactDiscoverer : FactDiscoverer |
||||
|
{ |
||||
|
private readonly IMessageSink diagnosticMessageSink; |
||||
|
public AvaloniaUIFactDiscoverer(IMessageSink diagnosticMessageSink) |
||||
|
: base(diagnosticMessageSink) |
||||
|
{ |
||||
|
this.diagnosticMessageSink = diagnosticMessageSink; |
||||
|
} |
||||
|
|
||||
|
protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) |
||||
|
{ |
||||
|
return new AvaloniaTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Reflection; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Threading; |
||||
|
using Xunit.Abstractions; |
||||
|
using Xunit.Sdk; |
||||
|
|
||||
|
namespace Avalonia.Headless.XUnit; |
||||
|
|
||||
|
internal class AvaloniaTestAssemblyRunner : XunitTestAssemblyRunner |
||||
|
{ |
||||
|
private HeadlessUnitTestSession? _session; |
||||
|
|
||||
|
public AvaloniaTestAssemblyRunner(ITestAssembly testAssembly, IEnumerable<IXunitTestCase> testCases, |
||||
|
IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, |
||||
|
ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink, |
||||
|
executionMessageSink, executionOptions) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override void SetupSyncContext(int maxParallelThreads) |
||||
|
{ |
||||
|
_session = HeadlessUnitTestSession.GetOrStartForAssembly( |
||||
|
Assembly.Load(new AssemblyName(TestAssembly.Assembly.Name))); |
||||
|
base.SetupSyncContext(1); |
||||
|
} |
||||
|
|
||||
|
public override void Dispose() |
||||
|
{ |
||||
|
_session?.Dispose(); |
||||
|
base.Dispose(); |
||||
|
} |
||||
|
|
||||
|
protected override Task<RunSummary> RunTestCollectionAsync( |
||||
|
IMessageBus messageBus, |
||||
|
ITestCollection testCollection, |
||||
|
IEnumerable<IXunitTestCase> testCases, |
||||
|
CancellationTokenSource cancellationTokenSource) |
||||
|
{ |
||||
|
return new AvaloniaTestCollectionRunner(_session!, testCollection, testCases, DiagnosticMessageSink, messageBus, |
||||
|
TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync(); |
||||
|
} |
||||
|
|
||||
|
private class AvaloniaTestCollectionRunner : XunitTestCollectionRunner |
||||
|
{ |
||||
|
private readonly HeadlessUnitTestSession _session; |
||||
|
|
||||
|
public AvaloniaTestCollectionRunner(HeadlessUnitTestSession session, |
||||
|
ITestCollection testCollection, IEnumerable<IXunitTestCase> testCases, |
||||
|
IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer, |
||||
|
ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(testCollection, |
||||
|
testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource) |
||||
|
{ |
||||
|
_session = session; |
||||
|
} |
||||
|
|
||||
|
protected override Task<RunSummary> RunTestClassAsync( |
||||
|
ITestClass testClass, |
||||
|
IReflectionTypeInfo @class, |
||||
|
IEnumerable<IXunitTestCase> testCases) |
||||
|
{ |
||||
|
return new AvaloniaTestClassRunner(_session, testClass, @class, testCases, DiagnosticMessageSink, MessageBus, |
||||
|
TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource, |
||||
|
CollectionFixtureMappings).RunAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private class AvaloniaTestClassRunner : XunitTestClassRunner |
||||
|
{ |
||||
|
private readonly HeadlessUnitTestSession _session; |
||||
|
|
||||
|
public AvaloniaTestClassRunner(HeadlessUnitTestSession session, ITestClass testClass, |
||||
|
IReflectionTypeInfo @class, |
||||
|
IEnumerable<IXunitTestCase> testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus, |
||||
|
ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator, |
||||
|
CancellationTokenSource cancellationTokenSource, IDictionary<Type, object> collectionFixtureMappings) : |
||||
|
base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, |
||||
|
cancellationTokenSource, collectionFixtureMappings) |
||||
|
{ |
||||
|
_session = session; |
||||
|
} |
||||
|
|
||||
|
protected override Task<RunSummary> RunTestMethodAsync( |
||||
|
ITestMethod testMethod, |
||||
|
IReflectionMethodInfo method, |
||||
|
IEnumerable<IXunitTestCase> testCases, |
||||
|
object[] constructorArguments) |
||||
|
{ |
||||
|
return new AvaloniaTestMethodRunner(_session, testMethod, Class, method, testCases, DiagnosticMessageSink, |
||||
|
MessageBus, new ExceptionAggregator(Aggregator), CancellationTokenSource, |
||||
|
constructorArguments).RunAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private class AvaloniaTestMethodRunner : XunitTestMethodRunner |
||||
|
{ |
||||
|
private readonly HeadlessUnitTestSession _session; |
||||
|
private readonly IMessageBus _messageBus; |
||||
|
private readonly ExceptionAggregator _aggregator; |
||||
|
private readonly CancellationTokenSource _cancellationTokenSource; |
||||
|
private readonly object[] _constructorArguments; |
||||
|
|
||||
|
public AvaloniaTestMethodRunner(HeadlessUnitTestSession session, ITestMethod testMethod, |
||||
|
IReflectionTypeInfo @class, |
||||
|
IReflectionMethodInfo method, IEnumerable<IXunitTestCase> testCases, IMessageSink diagnosticMessageSink, |
||||
|
IMessageBus messageBus, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource, |
||||
|
object[] constructorArguments) : base(testMethod, @class, method, testCases, diagnosticMessageSink, |
||||
|
messageBus, aggregator, cancellationTokenSource, constructorArguments) |
||||
|
{ |
||||
|
_session = session; |
||||
|
_messageBus = messageBus; |
||||
|
_aggregator = aggregator; |
||||
|
_cancellationTokenSource = cancellationTokenSource; |
||||
|
_constructorArguments = constructorArguments; |
||||
|
} |
||||
|
|
||||
|
protected override Task<RunSummary> RunTestCaseAsync(IXunitTestCase testCase) |
||||
|
{ |
||||
|
return AvaloniaTestCaseRunner.RunTest(_session, testCase, testCase.DisplayName, testCase.SkipReason, |
||||
|
_constructorArguments, testCase.TestMethodArguments, _messageBus, _aggregator, |
||||
|
_cancellationTokenSource); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using System.Runtime.ExceptionServices; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Threading; |
||||
|
using Xunit.Abstractions; |
||||
|
using Xunit.Sdk; |
||||
|
|
||||
|
namespace Avalonia.Headless.XUnit; |
||||
|
|
||||
|
internal class AvaloniaTestCase : XunitTestCase |
||||
|
{ |
||||
|
public AvaloniaTestCase( |
||||
|
IMessageSink diagnosticMessageSink, |
||||
|
TestMethodDisplay defaultMethodDisplay, |
||||
|
ITestMethod testMethod, |
||||
|
object?[]? testMethodArguments = null) |
||||
|
: base(diagnosticMessageSink, defaultMethodDisplay, TestMethodDisplayOptions.None, testMethod, testMethodArguments) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
[Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] |
||||
|
public AvaloniaTestCase() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public override Task<RunSummary> RunAsync( |
||||
|
IMessageSink diagnosticMessageSink, |
||||
|
IMessageBus messageBus, |
||||
|
object[] constructorArguments, |
||||
|
ExceptionAggregator aggregator, |
||||
|
CancellationTokenSource cancellationTokenSource) |
||||
|
{ |
||||
|
var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly); |
||||
|
|
||||
|
// We need to block the XUnit thread to ensure its concurrency throttle is effective.
|
||||
|
// See https://github.com/AArnott/Xunit.StaFact/pull/55#issuecomment-826187354 for details.
|
||||
|
var runSummary = AvaloniaTestCaseRunner |
||||
|
.RunTest(session, this, DisplayName, SkipReason, constructorArguments, |
||||
|
TestMethodArguments, messageBus, aggregator, cancellationTokenSource) |
||||
|
.GetAwaiter().GetResult(); |
||||
|
|
||||
|
return Task.FromResult(runSummary); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,98 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Reflection; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Threading; |
||||
|
using Xunit.Abstractions; |
||||
|
using Xunit.Sdk; |
||||
|
|
||||
|
namespace Avalonia.Headless.XUnit; |
||||
|
|
||||
|
internal class AvaloniaTestCaseRunner : XunitTestCaseRunner |
||||
|
{ |
||||
|
private readonly Action? _onAfterTestInvoked; |
||||
|
|
||||
|
public AvaloniaTestCaseRunner( |
||||
|
Action? onAfterTestInvoked, |
||||
|
IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments, |
||||
|
object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator, |
||||
|
CancellationTokenSource cancellationTokenSource) : base(testCase, displayName, skipReason, constructorArguments, |
||||
|
testMethodArguments, messageBus, aggregator, cancellationTokenSource) |
||||
|
{ |
||||
|
_onAfterTestInvoked = onAfterTestInvoked; |
||||
|
} |
||||
|
|
||||
|
public static Task<RunSummary> RunTest(HeadlessUnitTestSession session, |
||||
|
IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments, |
||||
|
object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator, |
||||
|
CancellationTokenSource cancellationTokenSource) |
||||
|
{ |
||||
|
var afterTest = () => Dispatcher.UIThread.RunJobs(); |
||||
|
return session.Dispatch(async () => |
||||
|
{ |
||||
|
var runner = new AvaloniaTestCaseRunner(afterTest, testCase, displayName, |
||||
|
skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource); |
||||
|
return await runner.RunAsync(); |
||||
|
}, cancellationTokenSource.Token); |
||||
|
} |
||||
|
|
||||
|
protected override XunitTestRunner CreateTestRunner(ITest test, IMessageBus messageBus, Type testClass, |
||||
|
object[] constructorArguments, |
||||
|
MethodInfo testMethod, object[] testMethodArguments, string skipReason, |
||||
|
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes, |
||||
|
ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) |
||||
|
{ |
||||
|
return new AvaloniaTestRunner(_onAfterTestInvoked, test, messageBus, testClass, constructorArguments, |
||||
|
testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource); |
||||
|
} |
||||
|
|
||||
|
private class AvaloniaTestRunner : XunitTestRunner |
||||
|
{ |
||||
|
private readonly Action? _onAfterTestInvoked; |
||||
|
|
||||
|
public AvaloniaTestRunner( |
||||
|
Action? onAfterTestInvoked, |
||||
|
ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod, |
||||
|
object[] testMethodArguments, string skipReason, |
||||
|
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes, ExceptionAggregator aggregator, |
||||
|
CancellationTokenSource cancellationTokenSource) : base(test, messageBus, testClass, constructorArguments, |
||||
|
testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource) |
||||
|
{ |
||||
|
_onAfterTestInvoked = onAfterTestInvoked; |
||||
|
} |
||||
|
|
||||
|
protected override Task<decimal> InvokeTestMethodAsync(ExceptionAggregator aggregator) |
||||
|
{ |
||||
|
return new AvaloniaTestInvoker(_onAfterTestInvoked, Test, MessageBus, TestClass, ConstructorArguments, |
||||
|
TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource).RunAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private class AvaloniaTestInvoker : XunitTestInvoker |
||||
|
{ |
||||
|
private readonly Action? _onAfterTestInvoked; |
||||
|
|
||||
|
public AvaloniaTestInvoker( |
||||
|
Action? onAfterTestInvoked, |
||||
|
ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod, |
||||
|
object[] testMethodArguments, IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes, |
||||
|
ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(test, messageBus, |
||||
|
testClass, constructorArguments, testMethod, testMethodArguments, beforeAfterAttributes, aggregator, |
||||
|
cancellationTokenSource) |
||||
|
{ |
||||
|
_onAfterTestInvoked = onAfterTestInvoked; |
||||
|
} |
||||
|
|
||||
|
protected override async Task AfterTestMethodInvokedAsync() |
||||
|
{ |
||||
|
await base.AfterTestMethodInvokedAsync(); |
||||
|
|
||||
|
// Only here we can execute random code after the test, where exception will be properly handled by the XUnit.
|
||||
|
if (_onAfterTestInvoked is not null) |
||||
|
{ |
||||
|
Aggregator.Run(_onAfterTestInvoked); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,61 +0,0 @@ |
|||||
using Avalonia.Threading; |
|
||||
using Xunit.Abstractions; |
|
||||
using Xunit.Sdk; |
|
||||
|
|
||||
namespace Avalonia.Headless.XUnit; |
|
||||
|
|
||||
internal class AvaloniaTestRunner<TAppBuilderEntry> : XunitTestAssemblyRunner |
|
||||
{ |
|
||||
private CancellationTokenSource? _cancellationTokenSource; |
|
||||
|
|
||||
public AvaloniaTestRunner(ITestAssembly testAssembly, IEnumerable<IXunitTestCase> testCases, |
|
||||
IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, |
|
||||
ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink, |
|
||||
executionMessageSink, executionOptions) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
protected override void SetupSyncContext(int maxParallelThreads) |
|
||||
{ |
|
||||
_cancellationTokenSource?.Dispose(); |
|
||||
_cancellationTokenSource = new CancellationTokenSource(); |
|
||||
SynchronizationContext.SetSynchronizationContext(InitNewApplicationContext(_cancellationTokenSource.Token).Result); |
|
||||
} |
|
||||
|
|
||||
public override void Dispose() |
|
||||
{ |
|
||||
_cancellationTokenSource?.Cancel(); |
|
||||
base.Dispose(); |
|
||||
} |
|
||||
|
|
||||
internal static Task<SynchronizationContext> InitNewApplicationContext(CancellationToken cancellationToken) |
|
||||
{ |
|
||||
var tcs = new TaskCompletionSource<SynchronizationContext>(); |
|
||||
|
|
||||
new Thread(() => |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
var appBuilder = AppBuilder.Configure(typeof(TAppBuilderEntry)); |
|
||||
|
|
||||
// If windowing subsystem wasn't initialized by user, force headless with default parameters.
|
|
||||
if (appBuilder.WindowingSubsystemName != "Headless") |
|
||||
{ |
|
||||
appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions()); |
|
||||
} |
|
||||
|
|
||||
appBuilder.SetupWithoutStarting(); |
|
||||
|
|
||||
tcs.SetResult(SynchronizationContext.Current!); |
|
||||
} |
|
||||
catch (Exception e) |
|
||||
{ |
|
||||
tcs.SetException(e); |
|
||||
} |
|
||||
|
|
||||
Dispatcher.UIThread.MainLoop(cancellationToken); |
|
||||
}) { IsBackground = true }.Start(); |
|
||||
|
|
||||
return tcs.Task; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,37 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.ComponentModel; |
||||
|
using Xunit; |
||||
|
using Xunit.Abstractions; |
||||
|
using Xunit.Sdk; |
||||
|
|
||||
|
namespace Avalonia.Headless.XUnit; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Identifies an xunit theory that starts on Avalonia Dispatcher
|
||||
|
/// such that awaited expressions resume on the test's "main thread".
|
||||
|
/// </summary>
|
||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] |
||||
|
[XunitTestCaseDiscoverer("Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer", "Avalonia.Headless.XUnit")] |
||||
|
public sealed class AvaloniaTheoryAttribute : TheoryAttribute |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
public class AvaloniaTheoryDiscoverer : TheoryDiscoverer |
||||
|
{ |
||||
|
public AvaloniaTheoryDiscoverer(IMessageSink diagnosticMessageSink) |
||||
|
: base(diagnosticMessageSink) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override IEnumerable<IXunitTestCase> CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow) |
||||
|
{ |
||||
|
yield return new AvaloniaTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, dataRow); |
||||
|
} |
||||
|
|
||||
|
protected override IEnumerable<IXunitTestCase> CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) |
||||
|
{ |
||||
|
yield return new AvaloniaTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), TestMethodDisplayOptions.None, testMethod); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Xunit.Abstractions; |
||||
|
using Xunit.Sdk; |
||||
|
|
||||
|
namespace Avalonia.Headless.XUnit; |
||||
|
|
||||
|
internal class AvaloniaTheoryTestCase : XunitTheoryTestCase |
||||
|
{ |
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
[Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] |
||||
|
public AvaloniaTheoryTestCase() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public AvaloniaTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod) |
||||
|
: base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public override Task<RunSummary> RunAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus, object[] constructorArguments, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) |
||||
|
{ |
||||
|
var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly); |
||||
|
|
||||
|
return AvaloniaTestCaseRunner |
||||
|
.RunTest(session, this, DisplayName, SkipReason, constructorArguments, |
||||
|
TestMethodArguments, messageBus, aggregator, cancellationTokenSource); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using System; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
|
||||
|
namespace Avalonia.Headless; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets up global avalonia test framework using avalonia application builder passed as a parameter.
|
||||
|
/// </summary>
|
||||
|
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] |
||||
|
public sealed class AvaloniaTestApplicationAttribute : Attribute |
||||
|
{ |
||||
|
public Type AppBuilderEntryPointType { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates instance of <see cref="AvaloniaTestApplicationAttribute"/>.
|
||||
|
/// </summary>
|
||||
|
/// <param name="appBuilderEntryPointType">
|
||||
|
/// Parameter from which <see cref="AppBuilder"/> should be created.
|
||||
|
/// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
|
||||
|
/// </param>
|
||||
|
public AvaloniaTestApplicationAttribute( |
||||
|
[DynamicallyAccessedMembers(HeadlessUnitTestSession.DynamicallyAccessed)] |
||||
|
Type appBuilderEntryPointType) |
||||
|
{ |
||||
|
AppBuilderEntryPointType = appBuilderEntryPointType; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,214 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Concurrent; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Reflection; |
||||
|
using System.Runtime.ExceptionServices; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls.Platform; |
||||
|
using Avalonia.Metadata; |
||||
|
using Avalonia.Reactive; |
||||
|
using Avalonia.Rendering; |
||||
|
using Avalonia.Threading; |
||||
|
|
||||
|
namespace Avalonia.Headless; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Headless unit test session that needs to be used by the actual testing framework.
|
||||
|
/// All UI tests are supposed to be executed from one of the <see cref="Dispatch"/> methods to keep execution flow on the UI thread.
|
||||
|
/// Disposing unit test session stops internal dispatcher loop.
|
||||
|
/// </summary>
|
||||
|
[Unstable("This API is experimental and might be unstable. Use on your risk. API might or might not be changed in a minor update.")] |
||||
|
public sealed class HeadlessUnitTestSession : IDisposable |
||||
|
{ |
||||
|
private static readonly ConcurrentDictionary<Assembly, HeadlessUnitTestSession> s_session = new(); |
||||
|
|
||||
|
private readonly AppBuilder _appBuilder; |
||||
|
private readonly CancellationTokenSource _cancellationTokenSource; |
||||
|
private readonly BlockingCollection<Action> _queue; |
||||
|
private readonly Task _dispatchTask; |
||||
|
|
||||
|
internal const DynamicallyAccessedMemberTypes DynamicallyAccessed = |
||||
|
DynamicallyAccessedMemberTypes.PublicMethods | |
||||
|
DynamicallyAccessedMemberTypes.NonPublicMethods | |
||||
|
DynamicallyAccessedMemberTypes.PublicParameterlessConstructor; |
||||
|
|
||||
|
private HeadlessUnitTestSession(AppBuilder appBuilder, CancellationTokenSource cancellationTokenSource, |
||||
|
BlockingCollection<Action> queue, Task dispatchTask) |
||||
|
{ |
||||
|
_appBuilder = appBuilder; |
||||
|
_cancellationTokenSource = cancellationTokenSource; |
||||
|
_queue = queue; |
||||
|
_dispatchTask = dispatchTask; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc cref="Dispatch{TResult}(Func{Task{TResult}}, CancellationToken)"/>
|
||||
|
public Task Dispatch(Action action, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
return Dispatch(() => |
||||
|
{ |
||||
|
action(); |
||||
|
return Task.FromResult(0); |
||||
|
}, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc cref="Dispatch{TResult}(Func{Task{TResult}}, CancellationToken)"/>
|
||||
|
public Task<TResult> Dispatch<TResult>(Func<TResult> action, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
return Dispatch(() => Task.FromResult(action()), cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Dispatch method queues an async operation on the dispatcher thread, creates a new application instance,
|
||||
|
/// setting app avalonia services, and runs <see cref="action"/> parameter.
|
||||
|
/// </summary>
|
||||
|
/// <param name="action">Action to execute on the dispatcher thread with avalonia services.</param>
|
||||
|
/// <param name="cancellationToken">Cancellation token to cancel execution.</param>
|
||||
|
/// <exception cref="ObjectDisposedException">
|
||||
|
/// If global session was already cancelled and thread killed, it's not possible to dispatch any actions again
|
||||
|
/// </exception>
|
||||
|
public Task<TResult> Dispatch<TResult>(Func<Task<TResult>> action, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
if (_cancellationTokenSource.IsCancellationRequested) |
||||
|
{ |
||||
|
throw new ObjectDisposedException("Session was already disposed."); |
||||
|
} |
||||
|
|
||||
|
var token = _cancellationTokenSource.Token; |
||||
|
|
||||
|
var tcs = new TaskCompletionSource<TResult>(); |
||||
|
_queue.Add(() => |
||||
|
{ |
||||
|
using var application = EnsureApplication(); |
||||
|
|
||||
|
var cts = new CancellationTokenSource(); |
||||
|
using var globalCts = token.Register(s => ((CancellationTokenSource)s!).Cancel(), cts, true); |
||||
|
using var localCts = cancellationToken.Register(s => ((CancellationTokenSource)s!).Cancel(), cts, true); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var task = action(); |
||||
|
task.ContinueWith((_, s) => ((CancellationTokenSource)s!).Cancel(), cts, |
||||
|
TaskScheduler.FromCurrentSynchronizationContext()); |
||||
|
|
||||
|
if (cts.IsCancellationRequested) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var frame = new DispatcherFrame(); |
||||
|
using var innerCts = cts.Token.Register(() => frame.Continue = false, true); |
||||
|
Dispatcher.UIThread.PushFrame(frame); |
||||
|
|
||||
|
var result = task.GetAwaiter().GetResult(); |
||||
|
tcs.TrySetResult(result); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
tcs.TrySetException(ex); |
||||
|
} |
||||
|
}); |
||||
|
return tcs.Task; |
||||
|
} |
||||
|
|
||||
|
private IDisposable EnsureApplication() |
||||
|
{ |
||||
|
var scope = AvaloniaLocator.EnterScope(); |
||||
|
try |
||||
|
{ |
||||
|
Dispatcher.ResetForUnitTests(); |
||||
|
_appBuilder.SetupUnsafe(); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
scope.Dispose(); |
||||
|
throw; |
||||
|
} |
||||
|
|
||||
|
return Disposable.Create(() => |
||||
|
{ |
||||
|
scope.Dispose(); |
||||
|
Dispatcher.ResetForUnitTests(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_cancellationTokenSource.Cancel(); |
||||
|
_queue.CompleteAdding(); |
||||
|
_dispatchTask.Wait(); |
||||
|
_cancellationTokenSource.Dispose(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates instance of <see cref="HeadlessUnitTestSession"/>.
|
||||
|
/// </summary>
|
||||
|
/// <param name="entryPointType">
|
||||
|
/// Parameter from which <see cref="AppBuilder"/> should be created.
|
||||
|
/// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
|
||||
|
/// </param>
|
||||
|
public static HeadlessUnitTestSession StartNew( |
||||
|
[DynamicallyAccessedMembers(DynamicallyAccessed)] |
||||
|
Type entryPointType) |
||||
|
{ |
||||
|
var tcs = new TaskCompletionSource<HeadlessUnitTestSession>(); |
||||
|
var cancellationTokenSource = new CancellationTokenSource(); |
||||
|
var queue = new BlockingCollection<Action>(); |
||||
|
|
||||
|
Task? task = null; |
||||
|
task = Task.Run(() => |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var appBuilder = AppBuilder.Configure(entryPointType); |
||||
|
|
||||
|
// If windowing subsystem wasn't initialized by user, force headless with default parameters.
|
||||
|
if (appBuilder.WindowingSubsystemName != "Headless") |
||||
|
{ |
||||
|
appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions()); |
||||
|
} |
||||
|
|
||||
|
// ReSharper disable once AccessToModifiedClosure
|
||||
|
tcs.SetResult(new HeadlessUnitTestSession(appBuilder, cancellationTokenSource, queue, task!)); |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
tcs.SetException(e); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
while (!cancellationTokenSource.IsCancellationRequested) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var action = queue.Take(cancellationTokenSource.Token); |
||||
|
action(); |
||||
|
} |
||||
|
catch (OperationCanceledException) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return tcs.Task.GetAwaiter().GetResult(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a session from AvaloniaTestApplicationAttribute attribute or reuses any existing.
|
||||
|
/// If AvaloniaTestApplicationAttribute doesn't exist, empty application is used.
|
||||
|
/// </summary>
|
||||
|
[UnconditionalSuppressMessage("Trimming", "IL2072", |
||||
|
Justification = "AvaloniaTestApplicationAttribute attribute should preserve type information.")] |
||||
|
public static HeadlessUnitTestSession GetOrStartForAssembly(Assembly? assembly) |
||||
|
{ |
||||
|
return s_session.GetOrAdd(assembly ?? typeof(HeadlessUnitTestSession).Assembly, a => |
||||
|
{ |
||||
|
var appBuilderEntryPointType = a.GetCustomAttribute<AvaloniaTestApplicationAttribute>() |
||||
|
?.AppBuilderEntryPointType; |
||||
|
return appBuilderEntryPointType is not null ? |
||||
|
StartNew(appBuilderEntryPointType) : |
||||
|
StartNew(typeof(Application)); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -1,285 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.IO; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.UnitTests; |
|
||||
using Avalonia.Media.Imaging; |
|
||||
using Avalonia.Media.TextFormatting; |
|
||||
|
|
||||
namespace Avalonia.Base.UnitTests.VisualTree |
|
||||
{ |
|
||||
class MockRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext |
|
||||
{ |
|
||||
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public bool IsLost => false; |
|
||||
|
|
||||
public object TryGetFeature(Type featureType) => null; |
|
||||
|
|
||||
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IStreamGeometryImpl CreateStreamGeometry() |
|
||||
{ |
|
||||
return new MockStreamGeometry(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<IGeometryImpl> children) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmap(Stream stream) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width, |
|
||||
BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height, |
|
||||
BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl LoadWriteableBitmap(string fileName) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmap(string fileName) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, |
|
||||
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) |
|
||||
{ |
|
||||
return this; |
|
||||
} |
|
||||
|
|
||||
public bool SupportsIndividualRoundRects { get; set; } |
|
||||
public AlphaFormat DefaultAlphaFormat { get; } |
|
||||
public PixelFormat DefaultPixelFormat { get; } |
|
||||
public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true; |
|
||||
|
|
||||
public IFontManagerImpl CreateFontManager() |
|
||||
{ |
|
||||
return new MockFontManagerImpl(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat fmt, AlphaFormat alphaFormat) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl CreateEllipseGeometry(Rect rect) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl CreateLineGeometry(Point p1, Point p2) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl CreateRectangleGeometry(Rect rect) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
class MockStreamGeometry : IStreamGeometryImpl |
|
||||
{ |
|
||||
private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); |
|
||||
public Rect Bounds |
|
||||
{ |
|
||||
get |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public double ContourLength { get; } |
|
||||
|
|
||||
public IStreamGeometryImpl Clone() |
|
||||
{ |
|
||||
return this; |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public bool FillContains(Point point) |
|
||||
{ |
|
||||
return _impl.FillContains(point); |
|
||||
} |
|
||||
|
|
||||
public Rect GetRenderBounds(IPen pen) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl Intersect(IGeometryImpl geometry) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IStreamGeometryContextImpl Open() |
|
||||
{ |
|
||||
return _impl; |
|
||||
} |
|
||||
|
|
||||
public bool StrokeContains(IPen pen, Point point) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public ITransformedGeometryImpl WithTransform(Matrix transform) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public bool TryGetPointAtDistance(double distance, out Point point) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
class MockStreamGeometryContext : IStreamGeometryContextImpl |
|
||||
{ |
|
||||
private List<Point> points = new List<Point>(); |
|
||||
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public void BeginFigure(Point startPoint, bool isFilled) |
|
||||
{ |
|
||||
points.Add(startPoint); |
|
||||
} |
|
||||
|
|
||||
public void CubicBezierTo(Point point1, Point point2, Point point3) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void EndFigure(bool isClosed) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void LineTo(Point point) |
|
||||
{ |
|
||||
points.Add(point); |
|
||||
} |
|
||||
|
|
||||
public void QuadraticBezierTo(Point control, Point endPoint) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public void SetFillRule(FillRule fillRule) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public bool FillContains(Point point) |
|
||||
{ |
|
||||
// Use the algorithm from https://www.blackpawn.com/texts/pointinpoly/default.html
|
|
||||
// to determine if the point is in the geometry (since it will always be convex in this situation)
|
|
||||
for (int i = 0; i < points.Count; i++) |
|
||||
{ |
|
||||
var a = points[i]; |
|
||||
var b = points[(i + 1) % points.Count]; |
|
||||
var c = points[(i + 2) % points.Count]; |
|
||||
|
|
||||
Vector v0 = c - a; |
|
||||
Vector v1 = b - a; |
|
||||
Vector v2 = point - a; |
|
||||
|
|
||||
var dot00 = v0 * v0; |
|
||||
var dot01 = v0 * v1; |
|
||||
var dot02 = v0 * v2; |
|
||||
var dot11 = v1 * v1; |
|
||||
var dot12 = v1 * v2; |
|
||||
|
|
||||
|
|
||||
var invDenom = 1 / (dot00 * dot11 - dot01 * dot01); |
|
||||
var u = (dot11 * dot02 - dot01 * dot12) * invDenom; |
|
||||
var v = (dot00 * dot12 - dot01 * dot02) * invDenom; |
|
||||
if ((u >= 0) && (v >= 0) && (u + v < 1)) return true; |
|
||||
} |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -1,17 +0,0 @@ |
|||||
using System; |
|
||||
using Avalonia.Input; |
|
||||
using Avalonia.Platform; |
|
||||
|
|
||||
namespace Avalonia.Benchmarks |
|
||||
{ |
|
||||
internal class NullCursorFactory : ICursorFactory |
|
||||
{ |
|
||||
public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new NullCursorImpl(); |
|
||||
ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) => new NullCursorImpl(); |
|
||||
|
|
||||
private class NullCursorImpl : ICursorImpl |
|
||||
{ |
|
||||
public void Dispose() { } |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,107 +0,0 @@ |
|||||
using System; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Rendering.SceneGraph; |
|
||||
using Avalonia.Utilities; |
|
||||
using Avalonia.Media.Imaging; |
|
||||
|
|
||||
namespace Avalonia.Benchmarks |
|
||||
{ |
|
||||
internal class NullDrawingContextImpl : IDrawingContextImpl |
|
||||
{ |
|
||||
public void Dispose() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public Matrix Transform { get; set; } |
|
||||
|
|
||||
public RenderOptions RenderOptions { get; set; } |
|
||||
|
|
||||
public void Clear(Color color) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void DrawLine(IPen pen, Point p1, Point p2) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void DrawEllipse(IBrush brush, IPen pen, Rect rect) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void DrawGlyphRun(IBrush foreground, IRef<IGlyphRunImpl> glyphRun) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public IDrawingContextLayerImpl CreateLayer(Size size) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
public void PushClip(Rect clip) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void PushClip(RoundedRect clip) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void PopClip() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void PushOpacity(double opacity, Rect bounds) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void PopOpacity() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void PushOpacityMask(IBrush mask, Rect bounds) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void PopOpacityMask() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void PushGeometryClip(IGeometryImpl clip) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void PopGeometryClip() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void PopBitmapBlendMode() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void Custom(ICustomDrawOperation custom) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public object GetFeature(Type t) => null; |
|
||||
} |
|
||||
} |
|
||||
@ -1,149 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.IO; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.UnitTests; |
|
||||
using Avalonia.Media.Imaging; |
|
||||
using Avalonia.Media.TextFormatting; |
|
||||
using Microsoft.Diagnostics.Runtime; |
|
||||
|
|
||||
namespace Avalonia.Benchmarks |
|
||||
{ |
|
||||
internal class NullRenderingPlatform : IPlatformRenderInterface, IPlatformRenderInterfaceContext |
|
||||
{ |
|
||||
public IGeometryImpl CreateEllipseGeometry(Rect rect) |
|
||||
{ |
|
||||
return new MockStreamGeometryImpl(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl CreateLineGeometry(Point p1, Point p2) |
|
||||
{ |
|
||||
return new MockStreamGeometryImpl(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl CreateRectangleGeometry(Rect rect) |
|
||||
{ |
|
||||
return new MockStreamGeometryImpl(); |
|
||||
} |
|
||||
|
|
||||
public IStreamGeometryImpl CreateStreamGeometry() |
|
||||
{ |
|
||||
return new MockStreamGeometryImpl(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<IGeometryImpl> children) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public bool IsLost => false; |
|
||||
|
|
||||
public object TryGetFeature(Type featureType) => null; |
|
||||
|
|
||||
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat format, AlphaFormat alphaFormat) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmap(string fileName) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmap(Stream stream) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width, |
|
||||
BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height, |
|
||||
BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl LoadWriteableBitmap(string fileName) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IFontManagerImpl CreateFontManager() |
|
||||
{ |
|
||||
return new MockFontManagerImpl(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) |
|
||||
{ |
|
||||
return new MockStreamGeometryImpl(); |
|
||||
} |
|
||||
|
|
||||
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, |
|
||||
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds) |
|
||||
{ |
|
||||
return new MockGlyphRun(glyphTypeface, fontRenderingEmSize, baselineOrigin, bounds); |
|
||||
} |
|
||||
|
|
||||
public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) |
|
||||
{ |
|
||||
return this; |
|
||||
} |
|
||||
|
|
||||
public bool SupportsIndividualRoundRects => true; |
|
||||
|
|
||||
public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; |
|
||||
|
|
||||
public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888; |
|
||||
public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true; |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
using System; |
|
||||
using System.Reactive.Disposables; |
|
||||
using System.Threading; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Threading; |
|
||||
|
|
||||
namespace Avalonia.Benchmarks |
|
||||
{ |
|
||||
internal class NullThreadingPlatform : IDispatcherImpl |
|
||||
{ |
|
||||
public void Signal() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void UpdateTimer(long? dueTimeInMs) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public bool CurrentThreadIsLoopThread => true; |
|
||||
|
|
||||
#pragma warning disable CS0067
|
|
||||
public event Action Signaled; |
|
||||
public event Action Timer; |
|
||||
public long Now => 0; |
|
||||
#pragma warning restore CS0067
|
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,7 @@ |
|||||
|
global using NUnit.Framework; |
||||
|
global using Avalonia.Headless.NUnit; |
||||
|
|
||||
|
using Avalonia.Headless; |
||||
|
using Avalonia.Headless.UnitTests; |
||||
|
|
||||
|
[assembly: AvaloniaTestApplication(typeof(TestApplication))] |
||||
@ -0,0 +1,31 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net6.0</TargetFramework> |
||||
|
<IsTestProject>true</IsTestProject> |
||||
|
<DefineConstants>$(DefineConstants);NUNIT</DefineConstants> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<Import Project="..\..\build\UnitTests.NetCore.targets" /> |
||||
|
<Import Project="..\..\build\UnitTests.NetFX.props" /> |
||||
|
<Import Project="..\..\build\Moq.props" /> |
||||
|
<Import Project="..\..\build\Rx.props" /> |
||||
|
<Import Project="..\..\build\SharedVersion.props" /> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="NUnit" Version="3.13.3" /> |
||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" /> |
||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<Compile Include="..\Avalonia.Headless.UnitTests\**\*.cs" /> |
||||
|
<Compile Remove="..\Avalonia.Headless.UnitTests\bin\**\*.cs" /> |
||||
|
<Compile Remove="..\Avalonia.Headless.UnitTests\obj\**\*.cs" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\Headless\Avalonia.Headless.NUnit\Avalonia.Headless.NUnit.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> |
||||
|
</ItemGroup> |
||||
|
</Project> |
||||
@ -0,0 +1,7 @@ |
|||||
|
global using Xunit; |
||||
|
global using Avalonia.Headless.XUnit; |
||||
|
using Avalonia.Headless; |
||||
|
using Avalonia.Headless.UnitTests; |
||||
|
using Avalonia.Headless.XUnit; |
||||
|
|
||||
|
[assembly: AvaloniaTestApplication(typeof(TestApplication))] |
||||
@ -1,62 +0,0 @@ |
|||||
using System; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using Avalonia.Threading; |
|
||||
|
|
||||
namespace Avalonia.UnitTests |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Immediately invokes dispatched jobs on the current thread.
|
|
||||
/// </summary>
|
|
||||
public class ImmediateDispatcher : IDispatcher |
|
||||
{ |
|
||||
/// <inheritdoc/>
|
|
||||
public bool CheckAccess() |
|
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void Post(Action action, DispatcherPriority priority) |
|
||||
{ |
|
||||
action(); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void Post(SendOrPostCallback action, object arg, DispatcherPriority priority) |
|
||||
{ |
|
||||
action(arg); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public Task InvokeAsync(Action action, DispatcherPriority priority) |
|
||||
{ |
|
||||
action(); |
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public Task<TResult> InvokeAsync<TResult>(Func<TResult> function, DispatcherPriority priority) |
|
||||
{ |
|
||||
var result = function(); |
|
||||
return Task.FromResult(result); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public Task InvokeAsync(Func<Task> function, DispatcherPriority priority) |
|
||||
{ |
|
||||
return function(); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> function, DispatcherPriority priority) |
|
||||
{ |
|
||||
return function(); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void VerifyAccess() |
|
||||
{ |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,63 +0,0 @@ |
|||||
using System.Diagnostics.CodeAnalysis; |
|
||||
using System.Globalization; |
|
||||
using System.IO; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
|
|
||||
namespace Avalonia.UnitTests |
|
||||
{ |
|
||||
public class MockFontManagerImpl : IFontManagerImpl |
|
||||
{ |
|
||||
private readonly string _defaultFamilyName; |
|
||||
|
|
||||
public MockFontManagerImpl(string defaultFamilyName = "Default") |
|
||||
{ |
|
||||
_defaultFamilyName = defaultFamilyName; |
|
||||
} |
|
||||
|
|
||||
public int TryCreateGlyphTypefaceCount { get; private set; } |
|
||||
|
|
||||
public string GetDefaultFontFamilyName() |
|
||||
{ |
|
||||
return _defaultFamilyName; |
|
||||
} |
|
||||
|
|
||||
string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) |
|
||||
{ |
|
||||
return new[] { _defaultFamilyName }; |
|
||||
} |
|
||||
|
|
||||
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, |
|
||||
FontStretch fontStretch, |
|
||||
CultureInfo culture, out Typeface fontKey) |
|
||||
{ |
|
||||
fontKey = new Typeface(_defaultFamilyName); |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, |
|
||||
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) |
|
||||
{ |
|
||||
glyphTypeface = null; |
|
||||
|
|
||||
TryCreateGlyphTypefaceCount++; |
|
||||
|
|
||||
if (familyName == "Unknown") |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
glyphTypeface = new MockGlyphTypeface(); |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
public virtual bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) |
|
||||
{ |
|
||||
glyphTypeface = new MockGlyphTypeface(); |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,34 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
|
|
||||
namespace Avalonia.UnitTests |
|
||||
{ |
|
||||
public class MockGlyphRun : IGlyphRunImpl |
|
||||
{ |
|
||||
public MockGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, Point baselineOrigin, Rect bounds) |
|
||||
{ |
|
||||
GlyphTypeface = glyphTypeface; |
|
||||
FontRenderingEmSize = fontRenderingEmSize; |
|
||||
BaselineOrigin = baselineOrigin; |
|
||||
Bounds =bounds; |
|
||||
} |
|
||||
|
|
||||
public IGlyphTypeface GlyphTypeface { get; } |
|
||||
|
|
||||
public double FontRenderingEmSize { get; } |
|
||||
|
|
||||
public Point BaselineOrigin { get; } |
|
||||
|
|
||||
public Rect Bounds { get; } |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
|
|
||||
} |
|
||||
|
|
||||
public IReadOnlyList<float> GetIntersections(float lowerLimit, float upperLimit) |
|
||||
=> Array.Empty<float>(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,81 +0,0 @@ |
|||||
using System; |
|
||||
using Avalonia.Media; |
|
||||
|
|
||||
namespace Avalonia.UnitTests |
|
||||
{ |
|
||||
public class MockGlyphTypeface : IGlyphTypeface |
|
||||
{ |
|
||||
public FontMetrics Metrics => new FontMetrics |
|
||||
{ |
|
||||
DesignEmHeight = 10, |
|
||||
Ascent = 2, |
|
||||
Descent = 10, |
|
||||
IsFixedPitch = true |
|
||||
}; |
|
||||
|
|
||||
public int GlyphCount => 1337; |
|
||||
|
|
||||
public FontSimulations FontSimulations => throw new NotImplementedException(); |
|
||||
|
|
||||
public string FamilyName => "$Default"; |
|
||||
|
|
||||
public FontWeight Weight { get; } |
|
||||
|
|
||||
public FontStyle Style { get; } |
|
||||
|
|
||||
public FontStretch Stretch { get; } |
|
||||
|
|
||||
public ushort GetGlyph(uint codepoint) |
|
||||
{ |
|
||||
return (ushort)codepoint; |
|
||||
} |
|
||||
|
|
||||
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints) |
|
||||
{ |
|
||||
return new ushort[codepoints.Length]; |
|
||||
} |
|
||||
|
|
||||
public int GetGlyphAdvance(ushort glyph) |
|
||||
{ |
|
||||
return 8; |
|
||||
} |
|
||||
|
|
||||
public bool TryGetGlyph(uint codepoint, out ushort glyph) |
|
||||
{ |
|
||||
glyph = 8; |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs) |
|
||||
{ |
|
||||
var advances = new int[glyphs.Length]; |
|
||||
|
|
||||
for (var i = 0; i < advances.Length; i++) |
|
||||
{ |
|
||||
advances[i] = 8; |
|
||||
} |
|
||||
|
|
||||
return advances; |
|
||||
} |
|
||||
|
|
||||
public void Dispose() { } |
|
||||
|
|
||||
public bool TryGetTable(uint tag, out byte[] table) |
|
||||
{ |
|
||||
table = null; |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) |
|
||||
{ |
|
||||
metrics = new GlyphMetrics |
|
||||
{ |
|
||||
Width = 10, |
|
||||
Height = 10 |
|
||||
}; |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,174 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.IO; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Media.Imaging; |
|
||||
using Avalonia.Media.TextFormatting; |
|
||||
using Moq; |
|
||||
|
|
||||
namespace Avalonia.UnitTests |
|
||||
{ |
|
||||
public class MockPlatformRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext |
|
||||
{ |
|
||||
public IGeometryImpl CreateEllipseGeometry(Rect rect) |
|
||||
{ |
|
||||
return Mock.Of<IGeometryImpl>(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl CreateLineGeometry(Point p1, Point p2) |
|
||||
{ |
|
||||
return Mock.Of<IGeometryImpl>(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl CreateRectangleGeometry(Rect rect) |
|
||||
{ |
|
||||
return Mock.Of<IGeometryImpl>(x => x.Bounds == rect); |
|
||||
} |
|
||||
|
|
||||
class MockRenderTarget : IRenderTarget |
|
||||
{ |
|
||||
public void Dispose() |
|
||||
{ |
|
||||
|
|
||||
} |
|
||||
|
|
||||
public IDrawingContextImpl CreateDrawingContext() |
|
||||
{ |
|
||||
var m = new Mock<IDrawingContextImpl>(); |
|
||||
m.Setup(c => c.CreateLayer(It.IsAny<Size>())) |
|
||||
.Returns(() => |
|
||||
{ |
|
||||
var r = new Mock<IDrawingContextLayerImpl>(); |
|
||||
r.Setup(r => r.CreateDrawingContext()) |
|
||||
.Returns(CreateDrawingContext()); |
|
||||
return r.Object; |
|
||||
} |
|
||||
); |
|
||||
return m.Object; |
|
||||
|
|
||||
} |
|
||||
|
|
||||
public bool IsCorrupted => false; |
|
||||
} |
|
||||
|
|
||||
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) |
|
||||
{ |
|
||||
return new MockRenderTarget(); |
|
||||
} |
|
||||
|
|
||||
public bool IsLost => false; |
|
||||
|
|
||||
public object TryGetFeature(Type featureType) => null; |
|
||||
|
|
||||
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) |
|
||||
{ |
|
||||
return Mock.Of<IRenderTargetBitmapImpl>(); |
|
||||
} |
|
||||
|
|
||||
public IStreamGeometryImpl CreateStreamGeometry() |
|
||||
{ |
|
||||
return new MockStreamGeometryImpl(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<IGeometryImpl> children) |
|
||||
{ |
|
||||
return Mock.Of<IGeometryImpl>(); |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) |
|
||||
{ |
|
||||
return Mock.Of<IGeometryImpl>(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl CreateWriteableBitmap( |
|
||||
PixelSize size, |
|
||||
Vector dpi, |
|
||||
PixelFormat format, |
|
||||
AlphaFormat alphaFormat) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmap(Stream stream) |
|
||||
{ |
|
||||
return Mock.Of<IBitmapImpl>(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width, |
|
||||
BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height, |
|
||||
BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl LoadWriteableBitmap(string fileName) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmap(string fileName) |
|
||||
{ |
|
||||
return Mock.Of<IBitmapImpl>(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
return Mock.Of<IBitmapImpl>(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
return Mock.Of<IBitmapImpl>(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) |
|
||||
{ |
|
||||
return Mock.Of<IBitmapImpl>(); |
|
||||
} |
|
||||
|
|
||||
public IBitmapImpl LoadBitmap( |
|
||||
PixelFormat format, |
|
||||
AlphaFormat alphaFormat, |
|
||||
IntPtr data, |
|
||||
PixelSize size, |
|
||||
Vector dpi, |
|
||||
int stride) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds) |
|
||||
{ |
|
||||
return new MockGlyphRun(glyphTypeface, fontRenderingEmSize, baselineOrigin, bounds); |
|
||||
} |
|
||||
|
|
||||
public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this; |
|
||||
|
|
||||
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) |
|
||||
{ |
|
||||
return Mock.Of<IGeometryImpl>(); |
|
||||
} |
|
||||
|
|
||||
public bool SupportsIndividualRoundRects { get; set; } |
|
||||
|
|
||||
public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; |
|
||||
|
|
||||
public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888; |
|
||||
public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true; |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,179 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
|
|
||||
namespace Avalonia.UnitTests |
|
||||
{ |
|
||||
public class MockStreamGeometryImpl : IStreamGeometryImpl, ITransformedGeometryImpl |
|
||||
{ |
|
||||
private MockStreamGeometryContext _context; |
|
||||
|
|
||||
public MockStreamGeometryImpl() |
|
||||
{ |
|
||||
Transform = Matrix.Identity; |
|
||||
_context = new MockStreamGeometryContext(); |
|
||||
} |
|
||||
|
|
||||
public MockStreamGeometryImpl(Matrix transform) |
|
||||
{ |
|
||||
Transform = transform; |
|
||||
_context = new MockStreamGeometryContext(); |
|
||||
} |
|
||||
|
|
||||
private MockStreamGeometryImpl(Matrix transform, MockStreamGeometryContext context) |
|
||||
{ |
|
||||
Transform = transform; |
|
||||
_context = context; |
|
||||
} |
|
||||
|
|
||||
public IGeometryImpl SourceGeometry { get; } |
|
||||
|
|
||||
public Rect Bounds => _context.CalculateBounds(); |
|
||||
|
|
||||
public double ContourLength { get; } |
|
||||
|
|
||||
public Matrix Transform { get; } |
|
||||
|
|
||||
public IStreamGeometryImpl Clone() |
|
||||
{ |
|
||||
return this; |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public bool FillContains(Point point) |
|
||||
{ |
|
||||
return _context.FillContains(point); |
|
||||
} |
|
||||
|
|
||||
public bool StrokeContains(IPen pen, Point point) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
public Rect GetRenderBounds(IPen pen) => Bounds; |
|
||||
|
|
||||
public IGeometryImpl Intersect(IGeometryImpl geometry) |
|
||||
{ |
|
||||
return new MockStreamGeometryImpl(Transform); |
|
||||
} |
|
||||
|
|
||||
public IStreamGeometryContextImpl Open() |
|
||||
{ |
|
||||
return _context; |
|
||||
} |
|
||||
|
|
||||
public ITransformedGeometryImpl WithTransform(Matrix transform) |
|
||||
{ |
|
||||
return new MockStreamGeometryImpl(transform, _context); |
|
||||
} |
|
||||
|
|
||||
public bool TryGetPointAtDistance(double distance, out Point point) |
|
||||
{ |
|
||||
point = new Point(); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent) |
|
||||
{ |
|
||||
point = new Point(); |
|
||||
tangent = new Point(); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry) |
|
||||
{ |
|
||||
segmentGeometry = null; |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
class MockStreamGeometryContext : IStreamGeometryContextImpl |
|
||||
{ |
|
||||
private List<Point> points = new List<Point>(); |
|
||||
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void BeginFigure(Point startPoint, bool isFilled) |
|
||||
{ |
|
||||
points.Add(startPoint); |
|
||||
} |
|
||||
|
|
||||
public Rect CalculateBounds() |
|
||||
{ |
|
||||
var left = double.MaxValue; |
|
||||
var right = double.MinValue; |
|
||||
var top = double.MaxValue; |
|
||||
var bottom = double.MinValue; |
|
||||
|
|
||||
foreach (var p in points) |
|
||||
{ |
|
||||
left = Math.Min(p.X, left); |
|
||||
right = Math.Max(p.X, right); |
|
||||
top = Math.Min(p.Y, top); |
|
||||
bottom = Math.Max(p.Y, bottom); |
|
||||
} |
|
||||
|
|
||||
return new Rect(new Point(left, top), new Point(right, bottom)); |
|
||||
} |
|
||||
|
|
||||
public void CubicBezierTo(Point point1, Point point2, Point point3) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void EndFigure(bool isClosed) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public void LineTo(Point point) |
|
||||
{ |
|
||||
points.Add(point); |
|
||||
} |
|
||||
|
|
||||
public void QuadraticBezierTo(Point control, Point endPoint) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public void SetFillRule(FillRule fillRule) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public bool FillContains(Point point) |
|
||||
{ |
|
||||
// Use the algorithm from https://www.blackpawn.com/texts/pointinpoly/default.html
|
|
||||
// to determine if the point is in the geometry (since it will always be convex in this situation)
|
|
||||
for (int i = 0; i < points.Count; i++) |
|
||||
{ |
|
||||
var a = points[i]; |
|
||||
var b = points[(i + 1) % points.Count]; |
|
||||
var c = points[(i + 2) % points.Count]; |
|
||||
|
|
||||
Vector v0 = c - a; |
|
||||
Vector v1 = b - a; |
|
||||
Vector v2 = point - a; |
|
||||
|
|
||||
var dot00 = v0 * v0; |
|
||||
var dot01 = v0 * v1; |
|
||||
var dot02 = v0 * v2; |
|
||||
var dot11 = v1 * v1; |
|
||||
var dot12 = v1 * v2; |
|
||||
|
|
||||
|
|
||||
var invDenom = 1 / (dot00 * dot11 - dot01 * dot01); |
|
||||
var u = (dot11 * dot02 - dot01 * dot12) * invDenom; |
|
||||
var v = (dot00 * dot12 - dot01 * dot02) * invDenom; |
|
||||
if ((u >= 0) && (v >= 0) && (u + v < 1)) return true; |
|
||||
} |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,38 +0,0 @@ |
|||||
using System; |
|
||||
using Avalonia.Media.TextFormatting; |
|
||||
using Avalonia.Media.TextFormatting.Unicode; |
|
||||
using Avalonia.Platform; |
|
||||
|
|
||||
namespace Avalonia.UnitTests |
|
||||
{ |
|
||||
public class MockTextShaperImpl : ITextShaperImpl |
|
||||
{ |
|
||||
public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options) |
|
||||
{ |
|
||||
var typeface = options.Typeface; |
|
||||
var fontRenderingEmSize = options.FontRenderingEmSize; |
|
||||
var bidiLevel = options.BidiLevel; |
|
||||
var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); |
|
||||
var textSpan = text.Span; |
|
||||
var textStartIndex = TextTestHelper.GetStartCharIndex(text); |
|
||||
|
|
||||
for (var i = 0; i < shapedBuffer.Length;) |
|
||||
{ |
|
||||
var glyphCluster = i + textStartIndex; |
|
||||
|
|
||||
var codepoint = Codepoint.ReadAt(textSpan, i, out var count); |
|
||||
|
|
||||
var glyphIndex = typeface.GetGlyph(codepoint); |
|
||||
|
|
||||
for (var j = 0; j < count; ++j) |
|
||||
{ |
|
||||
shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); |
|
||||
} |
|
||||
|
|
||||
i += count; |
|
||||
} |
|
||||
|
|
||||
return shapedBuffer; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,15 +0,0 @@ |
|||||
using System; |
|
||||
using System.Runtime.InteropServices; |
|
||||
|
|
||||
namespace Avalonia.UnitTests |
|
||||
{ |
|
||||
public static class TextTestHelper |
|
||||
{ |
|
||||
public static int GetStartCharIndex(ReadOnlyMemory<char> text) |
|
||||
{ |
|
||||
if (!MemoryMarshal.TryGetString(text, out _, out var start, out _)) |
|
||||
throw new InvalidOperationException("text memory should have been a string"); |
|
||||
return start; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue