using System; using System.Collections.Generic; using System.Threading.Tasks; using Avalonia.SourceGenerator; using Xunit; namespace Avalonia.Base.UnitTests.SourceGenerators; public class CrossThreadProxyGeneratorTests { public enum TestPriority { Low, Normal, High } [GenerateCrossThreadProxy(typeof(TestPriority), "Avalonia.Base.UnitTests.SourceGenerators.CrossThreadProxyGeneratorTests.TestPriority.Normal")] public interface IBaseProxied { void BaseFireAndForget(int x); } [GenerateCrossThreadProxy(typeof(TestPriority), "Avalonia.Base.UnitTests.SourceGenerators.CrossThreadProxyGeneratorTests.TestPriority.Normal")] public interface IDerivedProxied : IBaseProxied { void Increment(); int GetValue(); [GenerateCrossThreadProxyReturnTask] void AsyncFireAndForget(string s); } [GenerateCrossThreadProxy(typeof(TestPriority), "Avalonia.Base.UnitTests.SourceGenerators.CrossThreadProxyGeneratorTests.TestPriority.Normal")] public interface IProxiedDependency { void Execute(int value); } [GenerateCrossThreadProxy(typeof(TestPriority), "Avalonia.Base.UnitTests.SourceGenerators.CrossThreadProxyGeneratorTests.TestPriority.Normal")] public interface IAcceptsProxiedParam { void ProcessDependency(IProxiedDependency dep); void ProcessOptionalDependency(IProxiedDependency? dep); } private sealed class Target : IDerivedProxied { public List BaseCalls { get; } = new(); public int Counter; public List AsyncCalls { get; } = new(); public Func? GetValueImpl; public void BaseFireAndForget(int x) => BaseCalls.Add(x); public void Increment() => Counter++; public int GetValue() => GetValueImpl?.Invoke() ?? Counter; public void AsyncFireAndForget(string s) => AsyncCalls.Add(s); } private sealed class ProxiedDependencyTarget : IProxiedDependency { public int LastValue; public void Execute(int value) => LastValue = value; } private sealed class AcceptsProxiedParamTarget : IAcceptsProxiedParam { public IProxiedDependency? ReceivedDep; public IProxiedDependency? ReceivedOptionalDep; public bool OptionalWasCalled; public void ProcessDependency(IProxiedDependency dep) => ReceivedDep = dep; public void ProcessOptionalDependency(IProxiedDependency? dep) { ReceivedOptionalDep = dep; OptionalWasCalled = true; } } private sealed class QueueMarshaller { public readonly Queue<(Action action, TestPriority priority)> Queue = new(); public void Post(Action a, TestPriority p) => Queue.Enqueue((a, p)); public void DrainAll() { while (Queue.Count > 0) Queue.Dequeue().action(); } } [Fact] public void Fire_and_forget_void_routes_through_marshaller_with_default_priority() { var t = new Target(); var m = new QueueMarshaller(); var proxy = new DerivedProxiedProxy(t, m.Post); proxy.Increment(); Assert.Single(m.Queue); Assert.Equal(TestPriority.Normal, m.Queue.Peek().priority); Assert.Equal(0, t.Counter); m.DrainAll(); Assert.Equal(1, t.Counter); } [Fact] public void Explicit_priority_overload_is_used() { var t = new Target(); var m = new QueueMarshaller(); var proxy = new DerivedProxiedProxy(t, m.Post); proxy.Increment(TestPriority.High); Assert.Equal(TestPriority.High, m.Queue.Peek().priority); } [Fact] public async Task NonVoid_returns_Task_completed_after_marshaller_runs() { var t = new Target { Counter = 42 }; var m = new QueueMarshaller(); var proxy = new DerivedProxiedProxy(t, m.Post); var task = proxy.GetValue(); Assert.False(task.IsCompleted); m.DrainAll(); Assert.True(task.IsCompleted); Assert.Equal(42, await task); } [Fact] public void Exception_in_target_propagates_to_Task() { var t = new Target { GetValueImpl = () => throw new InvalidOperationException("boom") }; var m = new QueueMarshaller(); var proxy = new DerivedProxiedProxy(t, m.Post); var task = proxy.GetValue(); m.DrainAll(); Assert.True(task.IsFaulted); Assert.IsType(task.Exception!.InnerException); } [Fact] public void Void_method_with_ReturnTask_attribute_returns_Task() { var t = new Target(); var m = new QueueMarshaller(); var proxy = new DerivedProxiedProxy(t, m.Post); Task task = proxy.AsyncFireAndForget("hi"); Assert.False(task.IsCompleted); m.DrainAll(); Assert.True(task.IsCompleted); Assert.Equal(new[] { "hi" }, t.AsyncCalls); } [Fact] public void Inherited_method_routes_through_base_proxy() { var t = new Target(); var m = new QueueMarshaller(); var proxy = new DerivedProxiedProxy(t, m.Post); proxy.BaseFireAndForget(7); Assert.Single(m.Queue); m.DrainAll(); Assert.Equal(new[] { 7 }, t.BaseCalls); } [Fact] public void Derived_proxy_is_assignable_to_base_proxy() { var t = new Target(); var m = new QueueMarshaller(); BaseProxiedProxy proxy = new DerivedProxiedProxy(t, m.Post); proxy.BaseFireAndForget(3); m.DrainAll(); Assert.Equal(new[] { 3 }, t.BaseCalls); } [Fact] public void Marshaller_is_not_invoked_synchronously_during_proxy_call() { var t = new Target(); var m = new QueueMarshaller(); var proxy = new DerivedProxiedProxy(t, m.Post); proxy.Increment(); proxy.Increment(); proxy.Increment(); Assert.Equal(0, t.Counter); Assert.Equal(3, m.Queue.Count); } [Fact] public void Proxy_does_not_implement_proxied_interface() { Assert.False(typeof(IBaseProxied).IsAssignableFrom(typeof(BaseProxiedProxy))); Assert.False(typeof(IDerivedProxied).IsAssignableFrom(typeof(DerivedProxiedProxy))); } [Fact] public void Proxied_parameter_is_unwrapped_when_dispatching() { var depTarget = new ProxiedDependencyTarget(); var m = new QueueMarshaller(); var depProxy = new ProxiedDependencyProxy(depTarget, m.Post); var acceptsTarget = new AcceptsProxiedParamTarget(); var acceptsProxy = new AcceptsProxiedParamProxy(acceptsTarget, m.Post); acceptsProxy.ProcessDependency(depProxy); m.DrainAll(); Assert.Same(depTarget, acceptsTarget.ReceivedDep); } [Fact] public void Nullable_proxied_parameter_null_is_passed_through() { var acceptsTarget = new AcceptsProxiedParamTarget(); var m = new QueueMarshaller(); var acceptsProxy = new AcceptsProxiedParamProxy(acceptsTarget, m.Post); acceptsProxy.ProcessOptionalDependency(null); m.DrainAll(); Assert.True(acceptsTarget.OptionalWasCalled); Assert.Null(acceptsTarget.ReceivedOptionalDep); } [Fact] public void Nullable_proxied_parameter_proxy_is_unwrapped() { var depTarget = new ProxiedDependencyTarget(); var m = new QueueMarshaller(); var depProxy = new ProxiedDependencyProxy(depTarget, m.Post); var acceptsTarget = new AcceptsProxiedParamTarget(); var acceptsProxy = new AcceptsProxiedParamProxy(acceptsTarget, m.Post); acceptsProxy.ProcessOptionalDependency(depProxy); m.DrainAll(); Assert.Same(depTarget, acceptsTarget.ReceivedOptionalDep); } }