committed by
GitHub
90 changed files with 1385 additions and 1581 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