diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 982a056..6a7dbc8 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -7,9 +7,9 @@ on:
branches: [ "main" ]
env:
- DotNetVersion: '6.0.401'
- UnoCheck_Version: '1.5.4'
- UnoCheck_Manifest: 'https://raw.githubusercontent.com/unoplatform/uno.check/34b1a60f5c1c51604b47362781969dde46979fd5/manifests/uno.ui.manifest.json'
+ DotNetVersion: '7.0.401'
+ UnoCheck_Version: '1.16.0'
+ UnoCheck_Manifest: 'https://raw.githubusercontent.com/unoplatform/uno.check/519b147a80d92e35cac4b8f97855d9302c91b340/manifests/uno.ui.manifest.json'
jobs:
build:
@@ -45,7 +45,7 @@ jobs:
- run: |
& dotnet tool update --global uno.check --version $env:UnoCheck_Version --add-source https://api.nuget.org/v3/index.json
- & uno-check -v --ci --non-interactive --fix --skip xcode --skip gtk3 --skip vswin --skip vsmac --manifest $env:UnoCheck_Manifest
+ & uno-check -v --ci --non-interactive --fix --skip xcode --skip gtk3 --skip vswin --skip vswinworkloads --skip vsmac --manifest $env:UnoCheck_Manifest
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1.1
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 4abf556..83e847e 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -15,6 +15,7 @@
enable
true
+ $(NoWarn);CS1998
diff --git a/src/TestApp/Directory.Build.props b/src/TestApp/Directory.Build.props
new file mode 100644
index 0000000..54367bb
--- /dev/null
+++ b/src/TestApp/Directory.Build.props
@@ -0,0 +1,20 @@
+
+
+
+
+ $(DefineConstants);IS_UNO_RUNTIMETEST_PROJECT
+
+
+
+
+ <_Parameter1>$(MSBuildProjectFullPath)
+
+
+
+ <_Parameter1>$([System.IO.Path]::GetFileName(`$(PkgUno_UI_DevServer)`))
+
+
+ <_Parameter1>$([System.IO.Path]::GetFileName(`$(PkgUno_WinUI_DevServer)`))
+
+
+
\ No newline at end of file
diff --git a/src/TestApp/Directory.Build.targets b/src/TestApp/Directory.Build.targets
new file mode 100644
index 0000000..92f1890
--- /dev/null
+++ b/src/TestApp/Directory.Build.targets
@@ -0,0 +1,7 @@
+
+
+
+
+ $(DefineConstants);HAS_UNO_DEVSERVER
+
+
\ No newline at end of file
diff --git a/src/TestApp/shared/App.xaml.cs b/src/TestApp/shared/App.xaml.cs
index 49ab0dd..0801c20 100644
--- a/src/TestApp/shared/App.xaml.cs
+++ b/src/TestApp/shared/App.xaml.cs
@@ -165,6 +165,14 @@ private static void InitializeLogging()
builder.AddFilter("Windows", LogLevel.Warning);
builder.AddFilter("Microsoft", LogLevel.Warning);
+ builder.AddFilter("Uno.UI.RuntimeTests.HotReload", LogLevel.Debug);
+
+ // Hot reload testing
+ builder.AddFilter("Uno.UI.RemoteControl", LogLevel.Debug);
+ builder.AddFilter("Uno.UI.RuntimeTests.Internal.Helpers", LogLevel.Debug); // DevServer and SecondaryApp
+ builder.AddFilter("Uno.UI.RuntimeTests.HotReloadHelper", LogLevel.Trace); // Helper used by tests in secondary app instances (non debuggable)
+ builder.AddFilter("Uno.UI.RuntimeTests.UnitTestsControl", LogLevel.Information); // Dumps the runner status
+
// Generic Xaml events
// builder.AddFilter("Windows.UI.Xaml", LogLevel.Debug );
// builder.AddFilter("Windows.UI.Xaml.VisualStateGroup", LogLevel.Debug );
diff --git a/src/TestApp/shared/FilterTests.cs b/src/TestApp/shared/FilterTests.cs
new file mode 100644
index 0000000..caee6b2
--- /dev/null
+++ b/src/TestApp/shared/FilterTests.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Uno.UI.RuntimeTests.Engine;
+
+[TestClass]
+public class FilterTests
+{
+ [TestMethod]
+ [DataRow("abc & ghi", "abc", false)]
+ [DataRow("abc & ghi", "ghi", false)]
+ [DataRow("abc & ghi", "abc.ghi", true)]
+ [DataRow("abc & (ghi)", "abc", false)]
+ [DataRow("abc & (ghi)", "ghi", false)]
+ [DataRow("abc & (ghi)", "abc.ghi", true)]
+ [DataRow("a.b.c & ghi", "a.b", false)]
+ [DataRow("a.b.c & ghi", "a.b.c.def.ghi", true)]
+ [DataRow("a.b.c & (ghi)", "a.b", false)]
+ [DataRow("a.b.c & (ghi)", "a.b.c.def.ghi", true)]
+ [DataRow("abc & g.h.i", "g.h", false)]
+ [DataRow("abc & g.h.i", "abc.def.g.h.i", true)]
+
+ [DataRow("abc | ghi", "abc", true)]
+ [DataRow("abc | ghi", "ghi", true)]
+ [DataRow("abc | ghi", "abc.ghi", true)]
+ [DataRow("abc | (ghi)", "abc", true)]
+ [DataRow("abc | (ghi)", "ghi", true)]
+ [DataRow("abc | (ghi)", "abc.ghi", true)]
+ [DataRow("a.b.c | ghi", "a.b", false)]
+ [DataRow("a.b.c | ghi", "a.b.c", true)]
+ [DataRow("a.b.c | ghi", "a.b.c.def.ghi", true)]
+ [DataRow("a.b.c | (ghi)", "a.b", false)]
+ [DataRow("a.b.c | (ghi)", "a.b.c", true)]
+ [DataRow("a.b.c | (ghi)", "a.b.c.def.ghi", true)]
+ [DataRow("abc | g.h.i", "g.h", false)]
+ [DataRow("abc | g.h.i", "g.h.i", true)]
+ [DataRow("abc | g.h.i", "abc.def.g.h.i", true)]
+
+ [DataRow("abc ; ghi", "abc", true)]
+ [DataRow("abc ; ghi", "ghi", true)]
+ [DataRow("abc ; ghi", "abc.ghi", true)]
+ [DataRow("abc ; (ghi)", "abc", true)]
+ [DataRow("abc ; (ghi)", "ghi", true)]
+ [DataRow("abc ; (ghi)", "abc.ghi", true)]
+ [DataRow("a.b.c ; ghi", "a.b", false)]
+ [DataRow("a.b.c ; ghi", "a.b.c", true)]
+ [DataRow("a.b.c ; ghi", "a.b.c.def.ghi", true)]
+ [DataRow("a.b.c ; (ghi)", "a.b", false)]
+ [DataRow("a.b.c ; (ghi)", "a.b.c", true)]
+ [DataRow("a.b.c ; (ghi)", "a.b.c.def.ghi", true)]
+ [DataRow("abc ; g.h.i", "g.h", false)]
+ [DataRow("abc ; g.h.i", "g.h.i", true)]
+ [DataRow("abc ; g.h.i", "abc.def.g.h.i", true)]
+ public void When_ParseAndMatch(string filter, string method, bool expectedResult)
+ {
+ UnitTestFilter sut = filter;
+ var result = sut.IsMatch(method);
+
+ Assert.AreEqual(expectedResult, result);
+ }
+}
\ No newline at end of file
diff --git a/src/TestApp/shared/HotReloadTest_SimpleSubject.cs b/src/TestApp/shared/HotReloadTest_SimpleSubject.cs
new file mode 100644
index 0000000..58fd1e0
--- /dev/null
+++ b/src/TestApp/shared/HotReloadTest_SimpleSubject.cs
@@ -0,0 +1,12 @@
+#pragma warning disable
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Uno.UI.RuntimeTests.Engine;
+
+public class HotReloadTest_SimpleSubject
+{
+ public string Value => "42";
+}
\ No newline at end of file
diff --git a/src/TestApp/shared/HotReloadTests.cs b/src/TestApp/shared/HotReloadTests.cs
new file mode 100644
index 0000000..1518df2
--- /dev/null
+++ b/src/TestApp/shared/HotReloadTests.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Uno.Extensions;
+
+#if HAS_UNO_WINUI || WINDOWS_WINUI
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+#else
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+#endif
+
+namespace Uno.UI.RuntimeTests.Engine;
+
+[TestClass]
+public class HotReloadSanity
+{
+ [TestMethod]
+ public void Is_HotReload_Supported()
+ {
+#if __SKIA__ // DevServer should be referenced also in release
+ Assert.IsTrue(HotReloadHelper.IsSupported);
+#else
+ Assert.IsFalse(HotReloadHelper.IsSupported);
+#endif
+ }
+}
+
+[TestClass]
+[RunsInSecondaryApp]
+public class HotReloadTests
+{
+ [TestMethod]
+#if !__SKIA__
+ [Ignore("This tests assume directs access to the file system which not possible on this platform.")]
+#endif
+ public async Task Is_SourcesEditable(CancellationToken ct)
+ {
+ var sutPath = "../../shared/HotReloadTests_Subject.xaml";
+ var dir = Path.GetDirectoryName(typeof(HotReloadHelper).Assembly.GetCustomAttribute()!.ProjectFullPath)!;
+ var file = Path.Combine(dir, sutPath);
+
+ Assert.IsTrue(File.Exists(file));
+ Assert.IsTrue(File.ReadAllText(file).Contains("Original text"));
+
+ await using var _ = await HotReloadHelper.UpdateSourceFile(sutPath, "Original text", "Updated text from Can_Edit_File", waitForMetadataUpdate: false, ct);
+
+ await TestHelper.WaitFor(() => File.ReadAllText(file).Contains("Updated text from Can_Edit_File"), ct);
+
+ Assert.IsTrue(File.ReadAllText(file).Contains("Updated text from Can_Edit_File"));
+ }
+
+ [TestMethod]
+ public async Task Is_CodeHotReload_Enabled(CancellationToken ct)
+ {
+ if (!HotReloadHelper.IsSupported)
+ {
+ Assert.Inconclusive("Hot reload testing is not supported on this platform.");
+ }
+
+ var sut = new HotReloadTest_SimpleSubject();
+
+ Debug.Assert(sut.Value == "42");
+
+ await using var _ = await HotReloadHelper.UpdateSourceFile("../../shared/HotReloadTest_SimpleSubject.cs", "42", "43", ct);
+
+ Debug.Assert(sut.Value == "43");
+ }
+
+ [TestMethod]
+ public async Task Is_UIHotReload_Enabled(CancellationToken ct)
+ {
+ if (!HotReloadHelper.IsSupported)
+ {
+ Assert.Inconclusive("Hot reload testing is not supported on this platform.");
+ }
+
+ await UIHelper.Load(new HotReloadTests_Subject(), ct);
+
+ Assert.AreEqual("Original text", UIHelper.GetChild().Text);
+
+ await using var _ = await HotReloadHelper.UpdateSourceFile("Original text", "Updated text", ct);
+
+ await AsyncAssert.AreEqual("Updated text", () => UIHelper.GetChild().Text, ct);
+ }
+}
\ No newline at end of file
diff --git a/src/TestApp/shared/HotReloadTests_Subject.xaml b/src/TestApp/shared/HotReloadTests_Subject.xaml
new file mode 100644
index 0000000..f613814
--- /dev/null
+++ b/src/TestApp/shared/HotReloadTests_Subject.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/src/TestApp/shared/HotReloadTests_Subject.xaml.cs b/src/TestApp/shared/HotReloadTests_Subject.xaml.cs
new file mode 100644
index 0000000..977c900
--- /dev/null
+++ b/src/TestApp/shared/HotReloadTests_Subject.xaml.cs
@@ -0,0 +1,20 @@
+
+#if HAS_UNO_WINUI || WINDOWS_WINUI
+using Microsoft.UI.Xaml.Controls;
+#else
+using Windows.UI.Xaml.Controls;
+#endif
+
+namespace Uno.UI.RuntimeTests.Engine
+{
+ ///
+ /// An empty page that can be used on its own or navigated to within a Frame.
+ ///
+ public sealed partial class HotReloadTests_Subject : Page
+ {
+ public HotReloadTests_Subject()
+ {
+ this.InitializeComponent();
+ }
+ }
+}
diff --git a/src/TestApp/shared/SecondaryAppTests.cs b/src/TestApp/shared/SecondaryAppTests.cs
new file mode 100644
index 0000000..78eb3d9
--- /dev/null
+++ b/src/TestApp/shared/SecondaryAppTests.cs
@@ -0,0 +1,75 @@
+#pragma warning disable
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Uno.UI.RuntimeTests.Internal.Helpers;
+
+namespace Uno.UI.RuntimeTests.Engine;
+
+[TestClass]
+public class SecondaryAppSanity
+{
+ [TestMethod]
+ public void Is_SecondaryApp_Supported()
+ {
+#if __SKIA__
+ Assert.IsTrue(SecondaryApp.IsSupported);
+#else
+ Assert.IsFalse(SecondaryApp.IsSupported);
+#endif
+ }
+}
+
+[TestClass]
+[RunsInSecondaryApp(ignoreIfNotSupported: true)]
+public class SecondaryAppTests
+{
+ [TestMethod]
+ public void Is_From_A_Secondary_App()
+ {
+ Assert.IsTrue(Environment.GetEnvironmentVariable("UNO_RUNTIME_TESTS_IS_SECONDARY_APP") is "true");
+ }
+
+ [TestMethod]
+ public async Task Is_DevServer_Connected()
+ {
+#if HAS_UNO_DEVSERVER
+ Assert.IsNotNull(Uno.UI.RemoteControl.RemoteControlClient.Instance);
+
+ var connected = Uno.UI.RemoteControl.RemoteControlClient.Instance.WaitForConnection(CancellationToken.None);
+ var timeout = Task.Delay(500);
+
+ Assert.AreEqual(connected, await Task.WhenAny(connected, timeout));
+#else
+ Assert.Inconclusive("Dev server in not supported on this platform.");
+#endif
+ }
+
+#if DEBUG
+ [TestMethod]
+ public async Task No_Longer_Sane() // expected to fail
+ {
+ await Task.Delay(2000);
+
+ throw new Exception("Great works require a touch of insanity.");
+ }
+
+ [TestMethod, Ignore]
+ public void Is_An_Illusion() // expected to be ignored
+ {
+ }
+#endif
+}
+
+[TestClass]
+public class NotSecondaryAppTests
+{
+ [TestMethod]
+ public void Is_From_A_Secondary_App()
+ {
+ Assert.IsFalse(Environment.GetEnvironmentVariable("UNO_RUNTIME_TESTS_IS_SECONDARY_APP") is "true");
+ }
+}
\ No newline at end of file
diff --git a/src/TestApp/shared/Uno.UI.RuntimeTests.Engine.Shared.shproj b/src/TestApp/shared/Uno.UI.RuntimeTests.Engine.Shared.shproj
index bab8c8a..28ad0ee 100644
--- a/src/TestApp/shared/Uno.UI.RuntimeTests.Engine.Shared.shproj
+++ b/src/TestApp/shared/Uno.UI.RuntimeTests.Engine.Shared.shproj
@@ -11,6 +11,16 @@
+
+
+
+ <_Globbed_Compile Remove="FilterTests.cs" />
+ <_Globbed_Compile Remove="HotReloadTests.cs" />
+ <_Globbed_Compile Remove="HotReloadTests_Subject.xaml.cs" />
+ <_Globbed_Compile Remove="HotReloadTest_SimpleSubject.cs" />
<_Globbed_Compile Remove="MetaAttributes.cs" />
+
+ <_Globbled_Page Remove="HotReloadTests_Subject.xaml" />
+
\ No newline at end of file
diff --git a/src/TestApp/shared/uno.ui.runtimetests.engine.Shared.projitems b/src/TestApp/shared/uno.ui.runtimetests.engine.Shared.projitems
index 0083c1d..ecf55ea 100644
--- a/src/TestApp/shared/uno.ui.runtimetests.engine.Shared.projitems
+++ b/src/TestApp/shared/uno.ui.runtimetests.engine.Shared.projitems
@@ -18,12 +18,21 @@
App.xaml
+
+
+
+
MainPage.xaml
+
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
diff --git a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Mobile/Uno.UI.RuntimeTests.Engine.Mobile.csproj b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Mobile/Uno.UI.RuntimeTests.Engine.Mobile.csproj
index 06a60fc..b951a15 100644
--- a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Mobile/Uno.UI.RuntimeTests.Engine.Mobile.csproj
+++ b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Mobile/Uno.UI.RuntimeTests.Engine.Mobile.csproj
@@ -1,37 +1,36 @@
-
+
- net6.0-android;net6.0-ios;net6.0-maccatalyst
+ net7.0-android;net7.0-ios;net7.0-maccatalyst
-
+
true
Exe
-
+
- True
+ True
true
- 14.2
- 14.0
- 21.0
- 10.14
+ 14.2
+ 14.0
+ 21.0
+ 10.14
- iossimulator-x64
- maccatalyst-x64
- osx-x64
+ iossimulator-x64
+ maccatalyst-x64
+ osx-x64
-
-
-
+
+
+
-
-
+
@@ -40,8 +39,8 @@
-
-
+
+
$(MtouchExtraArgs) --setenv=MONO_GC_PARAMS=soft-heap-limit=512m,nursery-size=64m,evacuation-threshold=66,major=marksweep,concurrent-sweep
$(MtouchExtraArgs) --registrar:static
@@ -52,7 +51,7 @@
-
+
$(MtouchExtraArgs) --setenv=MONO_GC_PARAMS=soft-heap-limit=512m,nursery-size=64m,evacuation-threshold=66,major=marksweep,concurrent-sweep
@@ -67,7 +66,7 @@
-
+
link
diff --git a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Program.cs b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Program.cs
index 48726c7..95a7f7a 100644
--- a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Program.cs
+++ b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Program.cs
@@ -1,6 +1,7 @@
using System;
using GLib;
using Uno.UI.Runtime.Skia;
+using Uno.UI.Runtime.Skia.Gtk;
namespace Uno.UI.RuntimeTests.Engine.Skia.Gtk
{
diff --git a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Uno.UI.RuntimeTests.Engine.Skia.Gtk.csproj b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Uno.UI.RuntimeTests.Engine.Skia.Gtk.csproj
index 21801e9..b7a6d9d 100644
--- a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Uno.UI.RuntimeTests.Engine.Skia.Gtk.csproj
+++ b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Uno.UI.RuntimeTests.Engine.Skia.Gtk.csproj
@@ -1,8 +1,10 @@
-
+
WinExe
Exe
- net6.0
+ net7.0
+ $(WarningsNotAsErrors);Uno0001
+ $(DefineConstants);__SKIA__
@@ -14,12 +16,11 @@
-
-
-
+
+
+
-
diff --git a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Program.cs b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Program.cs
index c3b7b2e..8923ca7 100644
--- a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Program.cs
+++ b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Program.cs
@@ -1,5 +1,6 @@
using System;
using Uno.UI.Runtime.Skia;
+using Uno.UI.Runtime.Skia.Linux.FrameBuffer;
namespace Uno.UI.RuntimeTests.Engine
{
diff --git a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer.csproj b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer.csproj
index dd53b5f..710b5ae 100644
--- a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer.csproj
+++ b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer.csproj
@@ -1,9 +1,10 @@
-
+
WinExe
Exe
- net6.0
- $(NoWarn);Uno0001
+ net7.0
+ $(WarningsNotAsErrors);Uno0001
+ $(DefineConstants);__SKIA__
@@ -15,12 +16,11 @@
-
-
-
+
+
+
-
diff --git a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/App.xaml b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/App.xaml
index 0ef813e..0606217 100644
--- a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/App.xaml
+++ b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/App.xaml
@@ -1,9 +1,7 @@
-
-
-
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:local="clr-namespace:Uno.UI.RuntimeTests.Engine.WPF">
+
+
diff --git a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/App.xaml.cs b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/App.xaml.cs
index 596521a..cdfad1e 100644
--- a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/App.xaml.cs
+++ b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/App.xaml.cs
@@ -1,17 +1,14 @@
using System;
-using System.Collections.Generic;
-using System.Configuration;
-using System.Data;
using System.Linq;
-using System.Threading.Tasks;
using System.Windows;
+using Uno.UI.Runtime.Skia.Wpf;
-namespace Uno.UI.RuntimeTests.Engine.WPF
+namespace Uno.UI.RuntimeTests.Engine.WPF;
+
+public partial class App : Application
{
- ///
- /// Interaction logic for App.xaml
- ///
- public partial class App : Application
+ public App()
{
+ new WpfHost(Dispatcher, () => new Uno.UI.RuntimeTests.Engine.App()).Run();
}
-}
+}
\ No newline at end of file
diff --git a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/MainWindow.xaml b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/MainWindow.xaml
deleted file mode 100644
index 14553fb..0000000
--- a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/MainWindow.xaml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/MainWindow.xaml.cs b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/MainWindow.xaml.cs
deleted file mode 100644
index 6a250d1..0000000
--- a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/MainWindow.xaml.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Data;
-using System.Windows.Documents;
-using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using System.Windows.Navigation;
-using System.Windows.Shapes;
-
-namespace Uno.UI.RuntimeTests.Engine.WPF
-{
- ///
- /// Interaction logic for MainWindow.xaml
- ///
- public partial class MainWindow : Window
- {
- public MainWindow()
- {
- InitializeComponent();
-
- root.Content = new global::Uno.UI.Skia.Platform.WpfHost(Dispatcher, () => new Uno.UI.RuntimeTests.Engine.App());
- }
- }
-}
diff --git a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/Uno.UI.RuntimeTests.Engine.Skia.WPF.csproj b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/Uno.UI.RuntimeTests.Engine.Skia.WPF.csproj
index 64d7674..7b4baf0 100644
--- a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/Uno.UI.RuntimeTests.Engine.Skia.WPF.csproj
+++ b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Skia.WPF/Uno.UI.RuntimeTests.Engine.Skia.WPF.csproj
@@ -1,9 +1,11 @@
-
+
WinExe
Exe
- net6.0-windows
+ net7.0-windows
true
+ $(WarningsNotAsErrors);Uno0001
+ $(DefineConstants);__SKIA__
@@ -18,12 +20,11 @@
-
-
-
+
+
+
-
diff --git a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.UWP/Uno.UI.RuntimeTests.Engine.Uwp.csproj b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.UWP/Uno.UI.RuntimeTests.Engine.Uwp.csproj
index 7b3c5b0..4548515 100644
--- a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.UWP/Uno.UI.RuntimeTests.Engine.Uwp.csproj
+++ b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.UWP/Uno.UI.RuntimeTests.Engine.Uwp.csproj
@@ -11,7 +11,6 @@
6.2.11
-
diff --git a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Wasm/Uno.UI.RuntimeTests.Engine.Wasm.csproj b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Wasm/Uno.UI.RuntimeTests.Engine.Wasm.csproj
index c375aeb..13d3d2e 100644
--- a/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Wasm/Uno.UI.RuntimeTests.Engine.Wasm.csproj
+++ b/src/TestApp/uwp/Uno.UI.RuntimeTests.Engine.Wasm/Uno.UI.RuntimeTests.Engine.Wasm.csproj
@@ -1,8 +1,8 @@
-
+
Exe
- net6.0
- NU1701;Uno0001
+ net7.0
+ $(NoWarn);NU1701;Uno0001
true
@@ -44,14 +44,13 @@
-
-
-
+
+
+
-
diff --git a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Mobile/Uno.UI.RuntimeTests.Engine.Mobile.csproj b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Mobile/Uno.UI.RuntimeTests.Engine.Mobile.csproj
index 35b871e..e4f2dcd 100644
--- a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Mobile/Uno.UI.RuntimeTests.Engine.Mobile.csproj
+++ b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Mobile/Uno.UI.RuntimeTests.Engine.Mobile.csproj
@@ -1,33 +1,32 @@
-
+
- net6.0-android;net6.0-ios;net6.0-maccatalyst;net6.0-macos
+ net7.0-android;net7.0-ios;net7.0-maccatalyst;net7.0-macos
true
Exe
- iossimulator-x64
- maccatalyst-x64
- osx-x64
+ iossimulator-x64
+ maccatalyst-x64
+ osx-x64
-
+
-
+
true
- 14.2
- 14.0
- 21.0
- 10.14
+ 14.2
+ 14.0
+ 21.0
+ 10.14
-
-
-
+
+
+
-
-
+
@@ -36,8 +35,8 @@
-
-
+
+
$(MtouchExtraArgs) --setenv=MONO_GC_PARAMS=soft-heap-limit=512m,nursery-size=64m,evacuation-threshold=66,major=marksweep,concurrent-sweep
$(MtouchExtraArgs) --registrar:static
@@ -48,7 +47,7 @@
-
+
$(MtouchExtraArgs) --setenv=MONO_GC_PARAMS=soft-heap-limit=512m,nursery-size=64m,evacuation-threshold=66,major=marksweep,concurrent-sweep
@@ -63,7 +62,7 @@
-
+
diff --git a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Program.cs b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Program.cs
index 5cd2ed5..ef5f735 100644
--- a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Program.cs
+++ b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Program.cs
@@ -1,22 +1,21 @@
using GLib;
using System;
-using Uno.UI.Runtime.Skia;
+using Uno.UI.Runtime.Skia.Gtk;
-namespace Uno.UI.RuntimeTests.Engine.Skia.Gtk
+namespace Uno.UI.RuntimeTests.Engine.Skia.Gtk;
+
+internal class Program
{
- internal class Program
- {
- static void Main(string[] args)
- {
- ExceptionManager.UnhandledException += delegate (UnhandledExceptionArgs expArgs)
- {
- Console.WriteLine("GLIB UNHANDLED EXCEPTION" + expArgs.ExceptionObject.ToString());
- expArgs.ExitApplication = true;
- };
+ static void Main(string[] args)
+ {
+ ExceptionManager.UnhandledException += delegate (UnhandledExceptionArgs expArgs)
+ {
+ Console.WriteLine("GLIB UNHANDLED EXCEPTION" + expArgs.ExceptionObject.ToString());
+ expArgs.ExitApplication = true;
+ };
- var host = new GtkHost(() => new App());
+ var host = new GtkHost(() => new App());
- host.Run();
- }
- }
-}
+ host.Run();
+ }
+}
\ No newline at end of file
diff --git a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Uno.UI.RuntimeTests.Engine.Skia.Gtk.csproj b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Uno.UI.RuntimeTests.Engine.Skia.Gtk.csproj
index 69273d1..2cf194e 100644
--- a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Uno.UI.RuntimeTests.Engine.Skia.Gtk.csproj
+++ b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Gtk/Uno.UI.RuntimeTests.Engine.Skia.Gtk.csproj
@@ -1,8 +1,10 @@
-
+
WinExe
Exe
- net6.0
+ net7.0
+ $(WarningsNotAsErrors);Uno0001
+ $(DefineConstants);__SKIA__
@@ -14,12 +16,11 @@
-
-
-
+
+
+
-
diff --git a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Gtk/test/PullDevServer.csproj b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Gtk/test/PullDevServer.csproj
new file mode 100644
index 0000000..f7a2947
--- /dev/null
+++ b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Gtk/test/PullDevServer.csproj
@@ -0,0 +1,9 @@
+
+
+ Exe
+ net7.0
+
+
+
+
+
\ No newline at end of file
diff --git a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Program.cs b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Program.cs
index 63a0f48..b7fd767 100644
--- a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Program.cs
+++ b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Program.cs
@@ -2,6 +2,7 @@
using System;
using Uno.UI.Runtime.Skia;
using Windows.UI.Core;
+using Uno.UI.Runtime.Skia.Linux.FrameBuffer;
namespace Uno.UI.RuntimeTests.Engine
{
diff --git a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer.csproj b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer.csproj
index e2a993d..834561a 100644
--- a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer.csproj
+++ b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer/Uno.UI.RuntimeTests.Engine.Skia.Linux.FrameBuffer.csproj
@@ -1,9 +1,10 @@
-
+
WinExe
Exe
- net6.0
- $(NoWarn);Uno0001
+ net7.0
+ $(WarningsNotAsErrors);Uno0001
+ $(DefineConstants);__SKIA__
@@ -15,12 +16,11 @@
-
-
-
+
+
+
-
diff --git a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/App.xaml b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/App.xaml
index 6aeb858..f554ee9 100644
--- a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/App.xaml
+++ b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/App.xaml
@@ -1,8 +1,7 @@
+ xmlns:local="clr-namespace:Uno.UI.RuntimeTests.Engine.WPF">
diff --git a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/App.xaml.cs b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/App.xaml.cs
index eb1519c..8acc453 100644
--- a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/App.xaml.cs
+++ b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/App.xaml.cs
@@ -1,17 +1,19 @@
using System;
-using System.Collections.Generic;
-using System.Configuration;
-using System.Data;
using System.Linq;
-using System.Threading.Tasks;
using System.Windows;
+using Uno.UI.RemoteControl;
+using Uno.UI.Runtime.Skia.Wpf;
-namespace Uno.UI.RuntimeTests.Engine.WPF
+namespace Uno.UI.RuntimeTests.Engine.WPF;
+
+///
+/// Interaction logic for App.xaml
+///
+public partial class App : Application
{
- ///
- /// Interaction logic for App.xaml
- ///
- public partial class App : Application
- {
- }
-}
+ public App()
+ {
+ var host = new WpfHost(Dispatcher, () => new Uno.UI.RuntimeTests.Engine.App());
+ host.Run();
+ }
+}
\ No newline at end of file
diff --git a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/MainWindow.xaml b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/MainWindow.xaml
deleted file mode 100644
index ffbd31e..0000000
--- a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/MainWindow.xaml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/MainWindow.xaml.cs b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/MainWindow.xaml.cs
deleted file mode 100644
index cd07619..0000000
--- a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/MainWindow.xaml.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Data;
-using System.Windows.Documents;
-using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using System.Windows.Navigation;
-using System.Windows.Shapes;
-
-namespace Uno.UI.RuntimeTests.Engine.WPF
-{
- ///
- /// Interaction logic for MainWindow.xaml
- ///
- public partial class MainWindow : Window
- {
- public MainWindow()
- {
- InitializeComponent();
-
- root.Content = new global::Uno.UI.Skia.Platform.WpfHost(Dispatcher, () => new Uno.UI.RuntimeTests.Engine.App());
- }
- }
-}
diff --git a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/Uno.UI.RuntimeTests.Engine.Skia.Wpf.csproj b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/Uno.UI.RuntimeTests.Engine.Skia.Wpf.csproj
index 2d3b373..c34066b 100644
--- a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/Uno.UI.RuntimeTests.Engine.Skia.Wpf.csproj
+++ b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Skia.Wpf/Uno.UI.RuntimeTests.Engine.Skia.Wpf.csproj
@@ -1,9 +1,11 @@
-
+
WinExe
Exe
- net6.0-windows
+ net7.0-windows
true
+ $(WarningsNotAsErrors);Uno0001
+ $(DefineConstants);__SKIA__
@@ -18,12 +20,11 @@
-
-
-
+
+
+
-
diff --git a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Wasm/Uno.UI.RuntimeTests.Engine.Wasm.csproj b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Wasm/Uno.UI.RuntimeTests.Engine.Wasm.csproj
index 2bf367d..3b87420 100644
--- a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Wasm/Uno.UI.RuntimeTests.Engine.Wasm.csproj
+++ b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Wasm/Uno.UI.RuntimeTests.Engine.Wasm.csproj
@@ -1,8 +1,8 @@
-
+
Exe
- net6.0
- NU1701;Uno0001
+ net7.0
+ $(NoWarn);NU1701;Uno0001
true
@@ -44,14 +44,13 @@
-
-
-
+
+
+
-
diff --git a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Windows/Uno.UI.RuntimeTests.Engine.Windows.csproj b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Windows/Uno.UI.RuntimeTests.Engine.Windows.csproj
index da7eff9..f190fab 100644
--- a/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Windows/Uno.UI.RuntimeTests.Engine.Windows.csproj
+++ b/src/TestApp/winui/Uno.UI.RuntimeTests.Engine.Windows/Uno.UI.RuntimeTests.Engine.Windows.csproj
@@ -10,8 +10,7 @@
win10-$(Platform).pubxml
true
true
-
- WINDOWS_WINUI
+ $(DefineConstants);WINDOWS_WINUI
@@ -28,7 +27,6 @@
-
diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/RuntimeTestDevServerAttribute.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/RuntimeTestDevServerAttribute.cs
new file mode 100644
index 0000000..b1c5474
--- /dev/null
+++ b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/RuntimeTestDevServerAttribute.cs
@@ -0,0 +1,30 @@
+#if !UNO_RUNTIMETESTS_DISABLE_EMBEDDEDRUNNER
+#nullable enable
+
+#if !IS_UNO_RUNTIMETEST_PROJECT
+#pragma warning disable
+#endif
+
+using System;
+using System.Linq;
+
+namespace Uno.UI.RuntimeTests.Engine;
+
+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
+public sealed class RuntimeTestDevServerAttribute : Attribute
+{
+ // Note: We prefer to capture it at compilation time instead of using reflection,
+ // so if dev-server assembly has been overriden with invalid version (e.g. for debug purposes),
+ // we are still able to get the right one.
+
+ ///
+ /// The version of the DevServer package used to compile the test engine.
+ ///
+ public string Version { get; }
+
+ public RuntimeTestDevServerAttribute(string version)
+ {
+ Version = version;
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/RuntimeTestEmbeddedRunner.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/RuntimeTestEmbeddedRunner.cs
new file mode 100644
index 0000000..af91bff
--- /dev/null
+++ b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/RuntimeTestEmbeddedRunner.cs
@@ -0,0 +1,185 @@
+#if WINDOWS_UWP
+#define UNO_RUNTIMETESTS_DISABLE_EMBEDDEDRUNNER
+#endif
+
+#if !UNO_RUNTIMETESTS_DISABLE_EMBEDDEDRUNNER
+#nullable enable
+
+#if !IS_UNO_RUNTIMETEST_PROJECT
+#pragma warning disable
+#endif
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using System.Threading;
+using Windows.UI.Core;
+using Windows.UI.Xaml;
+using Microsoft.UI.Xaml;
+
+namespace Uno.UI.RuntimeTests.Engine;
+
+///
+/// A runtime-test runner that is embedded in applications that are referencing the runtime-test engine package.
+///
+///
+/// This class is intended to be used only by the the test engine itself and should not be used by applications.
+/// API contract is not guaranteed and might change in future releases.
+///
+internal static partial class RuntimeTestEmbeddedRunner
+{
+ private enum TestResultKind
+ {
+ NUnit = 0,
+ UnoRuntimeTests,
+ }
+
+#pragma warning disable CA2255 // The 'ModuleInitializer' attribute is only intended to be used in application code or advanced source generator scenarios
+ [ModuleInitializer]
+#pragma warning restore CA2255
+ public static void AutoStartTests()
+ {
+ if (Environment.GetEnvironmentVariable("UNO_RUNTIME_TESTS_RUN_TESTS") is { } testsConfig
+ && Environment.GetEnvironmentVariable("UNO_RUNTIME_TESTS_OUTPUT_PATH") is { } outputPath)
+ {
+ var outputKind = Enum.TryParse(Environment.GetEnvironmentVariable("UNO_RUNTIME_TESTS_OUTPUT_KIND"), ignoreCase: true, out var kind)
+ ? kind
+ : TestResultKind.NUnit;
+ var isSecondaryApp = Environment.GetEnvironmentVariable("UNO_RUNTIME_TESTS_IS_SECONDARY_APP")?.ToLowerInvariant() switch
+ {
+ null => false,
+ "false" => false,
+ "0" => false,
+ _ => true
+ };
+
+ _ = RunTestsAndExit(testsConfig, outputPath, outputKind, isSecondaryApp);
+ }
+ }
+
+ private static async Task RunTestsAndExit(string testsConfigRaw, string outputPath, TestResultKind outputKind, bool isSecondaryApp)
+ {
+ var ct = new CancellationTokenSource();
+
+ try
+ {
+ Log("Waiting for app to init before running runtime-tests");
+
+#pragma warning disable CA1416 // Validate platform compatibility
+ Console.CancelKeyPress += (_, _) => ct.Cancel(true);
+#pragma warning restore CA1416 // Validate platform compatibility
+
+ // Wait for the app to init it-self
+ await Task.Delay(500, ct.Token).ConfigureAwait(false);
+ for (var i = 0; Window.Current is null && i < 100; i++)
+ {
+ await Task.Delay(50, ct.Token).ConfigureAwait(false);
+ }
+
+ var window = Window.Current;
+ if (window is null or { Dispatcher: null })
+ {
+ throw new InvalidOperationException("Window.Current is null or does not have any valid dispatcher");
+ }
+
+ // While the app init, parse the tests config
+ var config = default(UnitTestEngineConfig?);
+ if (testsConfigRaw.StartsWith('{'))
+ {
+ try
+ {
+ config = JsonSerializer.Deserialize(testsConfigRaw);
+ }
+ catch { }
+ }
+ config ??= new UnitTestEngineConfig { Filter = testsConfigRaw };
+
+ // Let continue on the dispatcher thread
+ var tcs = new TaskCompletionSource();
+ await window
+ .Dispatcher
+ .RunAsync(
+ CoreDispatcherPriority.Normal,
+ async () =>
+ {
+ try
+ {
+ await RunTests(window, config, outputPath, outputKind, isSecondaryApp, ct.Token);
+ tcs.TrySetResult();
+ }
+ catch (OperationCanceledException) when (ct.IsCancellationRequested)
+ {
+ tcs.TrySetCanceled();
+ }
+ catch (Exception error)
+ {
+ tcs.TrySetException(error);
+ }
+ })
+ .AsTask(ct.Token)
+ .ConfigureAwait(false);
+
+ await tcs.Task.ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) when (ct.IsCancellationRequested)
+ {
+ LogError("Runtime-tests has been cancelled.");
+ Environment.Exit(-1);
+ }
+ catch (Exception error)
+ {
+ LogError("Failed to run runtime-test.");
+ LogError(error.ToString());
+ Environment.Exit(1);
+ }
+ finally
+ {
+ Log("Runtime-test completed, exiting app.");
+ Environment.Exit(0);
+ }
+ }
+
+ private static async Task RunTests(Window window, UnitTestEngineConfig config, string outputPath, TestResultKind outputKind, bool isSecondaryApp, CancellationToken ct)
+ {
+ // Wait for the app to init it-self
+ for (var i = 0; window.Content is null or { ActualSize.X: 0 } or { ActualSize.Y: 0 } && i < 20; i++)
+ {
+ await Task.Delay(20, ct);
+ }
+
+ // Then override the app content by the test control
+ Log("Initializing runtime-tests engine.");
+ var engine = new UnitTestsControl { IsSecondaryApp = isSecondaryApp };
+ Window.Current.Content = engine;
+ await UIHelper.WaitForLoaded(engine, ct);
+
+ // Run the test !
+ Log($"Running runtime-tests ({config})");
+ await engine.RunTests(ct, config).ConfigureAwait(false);
+
+ // Finally save the test results
+ Log($"Saving runtime-tests results to {outputPath}.");
+ switch (outputKind)
+ {
+ case TestResultKind.UnoRuntimeTests:
+ await File.WriteAllTextAsync(outputPath, JsonSerializer.Serialize(engine.Results), Encoding.UTF8, ct);
+ break;
+
+ default:
+ case TestResultKind.NUnit:
+ await File.WriteAllTextAsync(outputPath, engine.NUnitTestResultsDocument, Encoding.Unicode, ct);
+ break;
+ }
+ }
+
+ private static void Log(string text)
+ => Console.WriteLine(text);
+
+ private static void LogError(string text)
+ => Console.Error.WriteLine(text);
+}
+#endif
\ No newline at end of file
diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/RuntimeTestsSourceProjectAttribute.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/RuntimeTestsSourceProjectAttribute.cs
new file mode 100644
index 0000000..ba7113e
--- /dev/null
+++ b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/RuntimeTestsSourceProjectAttribute.cs
@@ -0,0 +1,29 @@
+#if !UNO_RUNTIMETESTS_DISABLE_EMBEDDEDRUNNER
+#nullable enable
+
+#if !IS_UNO_RUNTIMETEST_PROJECT
+#pragma warning disable
+#endif
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Uno.UI.RuntimeTests.Engine;
+
+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
+public sealed class RuntimeTestsSourceProjectAttribute : Attribute
+{
+ // Note: This is somehow a duplicate of the Uno.UI.RemoteControl.ProjectConfigurationAttribute but it allows us to have the attribute
+ // to be defined on the assembly that is compiling the runtime test engine (instead on the app head only)
+ // which allows us to use it in HotReloadTestHelper without having any dependency on the app type.
+
+ public string ProjectFullPath { get; }
+
+ public RuntimeTestsSourceProjectAttribute(string projectFullPath)
+ {
+ ProjectFullPath = projectFullPath;
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/_Private/DevServer.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/_Private/DevServer.cs
new file mode 100644
index 0000000..fed5d2b
--- /dev/null
+++ b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/_Private/DevServer.cs
@@ -0,0 +1,229 @@
+#if !UNO_RUNTIMETESTS_DISABLE_UI && (__SKIA__ || IS_SECONDARY_APP_SUPPORTED)
+#nullable enable
+
+#if !IS_UNO_RUNTIMETEST_PROJECT
+#pragma warning disable
+#endif
+#pragma warning disable CA1848 // Log perf
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Uno.Extensions;
+using Uno.UI.RuntimeTests.Engine;
+
+namespace Uno.UI.RuntimeTests.Internal.Helpers;
+
+///
+/// Helper class to start a dev server instance.
+///
+///
+/// This class is intended to be used only by the the test engine itself and should not be used by applications.
+/// API contract is not guaranteed and might change in future releases.
+///
+internal sealed partial class DevServer : IAsyncDisposable
+{
+ private static readonly ILogger _log = typeof(DevServer).Log();
+ private static int _instance;
+ private static string? _devServerPath;
+
+ ///
+ /// Starts a new dev server instance
+ ///
+ /// Cancellation token to abort the initialization of the server.
+ /// The new dev server instance.
+ public static async Task Start(CancellationToken ct)
+ {
+#if !HAS_UNO_DEVSERVER
+ throw new NotSupportedException("Dev server is not supported on this platform.");
+#else
+ var path = await GetDevServer(ct);
+ var port = GetTcpPort();
+
+ return StartCore(path, port);
+#endif
+ }
+
+ private readonly Process _process;
+
+ private DevServer(Process process, int port)
+ {
+ Port = port;
+ _process = process;
+ }
+
+ ///
+ /// The port on which the dev server is listening
+ ///
+ public int Port { get; }
+
+ private static async Task GetDevServer(CancellationToken ct)
+ => _devServerPath ??= await PullDevServer(ct);
+
+ ///
+ /// Pulls the latest version of dev server from NuGet and returns the path to the executable
+ ///
+ private static async Task PullDevServer(CancellationToken ct)
+ {
+ var dir = Path.Combine(Path.GetTempPath(), $"DevServer_{Guid.NewGuid():N}");
+ Directory.CreateDirectory(dir);
+
+ try
+ {
+ using (var log = _log.Scope("GET_DOTNET_VERSION"))
+ {
+ var rawVersion = await ProcessHelper.ExecuteAsync(
+ ct,
+ "dotnet",
+ new() { "--version" },
+ Environment.CurrentDirectory, // Needed to get the version used by the current app (i.e. including global.json)
+ log);
+ var dotnetVersion = GetDotnetVersion(rawVersion);
+
+ var csProj = @$"
+
+ Exe
+ net{dotnetVersion.Major}.{dotnetVersion.Minor}
+
+";
+ await File.WriteAllTextAsync(Path.Combine(dir, "PullDevServer.csproj"), csProj, ct);
+ }
+
+ using (var log = _log.Scope("PULL_DEV_SERVER"))
+ {
+ var args = new List { "add", "package" };
+#if HAS_UNO_WINUI || WINDOWS_WINUI
+ args.Add("Uno.WinUI.DevServer");
+#else
+ args.Add("Uno.UI.DevServer");
+#endif
+ // If the assembly is not a debug version it should have a valid version
+ // Note: This is the version of the RemoteControl assembly, not the RemoteControl.Host, but they should be in sync (both are part of the DevServer package)
+ if (Type.GetType("Uno.UI.RemoteControl.RemoteControlClient, Uno.UI.RemoteControl", throwOnError: false)
+ ?.Assembly
+ .GetCustomAttribute()
+ ?.InformationalVersion is { Length: > 0 } runtimeVersion
+ && Regex.Match(runtimeVersion, @"^(?\d+\.\d+\.\d+(-\w+\.\d+))+") is {Success: true} match)
+ {
+ args.Add("--version");
+ args.Add(match.Groups["version"].Value);
+ }
+ // Otherwise we use the version used to compile the test engine
+ else if (typeof(DevServer).Assembly.GetCustomAttribute()?.Version is { Length: > 0 } version)
+ {
+ args.Add("--version");
+ args.Add(version);
+ }
+ // As a last chance, we just use the latest version
+ else
+ {
+ args.Add("--prerelease"); // latest version
+ }
+
+ await ProcessHelper.ExecuteAsync(ct, "dotnet", args, dir, log);
+ }
+
+ using (var log = _log.Scope("GET_DEV_SERVER_PATH"))
+ {
+ var data = await ProcessHelper.ExecuteAsync(
+ ct,
+ "dotnet",
+ new() { "build", "/t:GetRemoteControlHostPath" },
+ dir,
+ log);
+
+ return GetConfigurationValue(data, "RemoteControlHostPath") is { Length: > 0 } path
+ ? path
+ : throw new InvalidOperationException("Failed to get remote control host path");
+ }
+ }
+ finally
+ {
+ try
+ {
+ Directory.Delete(dir, recursive: true);
+ }
+ catch { /* Nothing to do */ }
+ }
+ }
+
+ ///
+ /// Starts the dev server on the given port
+ ///
+ private static DevServer StartCore(string hostBinPath, int port)
+ {
+ if (!File.Exists(hostBinPath))
+ {
+ _log.LogError($"DevServer {hostBinPath} does not exist");
+ throw new InvalidOperationException($"Unable to find {hostBinPath}");
+ }
+
+ var arguments = $"\"{hostBinPath}\" --httpPort {port} --ppid {Environment.ProcessId} --metadata-updates true";
+ var pi = new ProcessStartInfo("dotnet", arguments)
+ {
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ WorkingDirectory = Path.GetDirectoryName(hostBinPath),
+ };
+
+ var process = new System.Diagnostics.Process { StartInfo = pi };
+
+ process.StartAndLog(_log.Scope($"DEV_SERVER_{Interlocked.Increment(ref _instance):D2}"));
+
+ return new DevServer(process, port);
+ }
+
+ #region Misc helpers
+ private static string? GetConfigurationValue(string msbuildResult, string nodeName)
+ => Regex.Match(msbuildResult, $"<{nodeName}>(?.*?){nodeName}>") is { Success: true } match
+ ? match.Groups["value"].Value
+ : null;
+
+ private static Version GetDotnetVersion(string dotnetRawVersion)
+ => Version.TryParse(dotnetRawVersion?.Split('-').FirstOrDefault(), out var version)
+ ? version
+ : throw new InvalidOperationException("Failed to read dotnet version");
+
+ private static int GetTcpPort()
+ {
+ var l = new TcpListener(IPAddress.Loopback, 0);
+ l.Start();
+ var port = ((IPEndPoint)l.LocalEndpoint).Port;
+ l.Stop();
+ return port;
+ }
+ #endregion
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ if (_process is null or { HasExited: true })
+ {
+ return;
+ }
+
+ try
+ {
+ _process.Kill(true); // Best effort, the app should kill itself anyway
+ }
+ catch (Exception e)
+ {
+ _log.LogError("Failed to kill dev server", e);
+ }
+
+ await _process.WaitForExitAsync(CancellationToken.None);
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/_Private/LogScope.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/_Private/LogScope.cs
new file mode 100644
index 0000000..54fd811
--- /dev/null
+++ b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/_Private/LogScope.cs
@@ -0,0 +1,52 @@
+#if !UNO_RUNTIMETESTS_DISABLE_UI
+using System;
+using System.Linq;
+using Microsoft.Extensions.Logging;
+
+namespace Uno.UI.RuntimeTests.Internal.Helpers;
+
+internal static class LoggerExtensions
+{
+ public static LogScope CreateScopedLog(this Type type, string scopeName)
+#if false
+ => new(type.Log(), type.Log().BeginScope(scopeName));
+#else
+ => new(Uno.Extensions.LogExtensionPoint.AmbientLoggerFactory.CreateLogger(type.FullName + "#" + scopeName));
+#endif
+
+ public static LogScope Scope(this ILogger log, string scopeName)
+#if false
+ => new(log, log.BeginScope(scopeName));
+#else
+ => new (Uno.Extensions.LogExtensionPoint.AmbientLoggerFactory.CreateLogger(typeof(T).FullName + "#" + scopeName));
+#endif
+}
+
+internal readonly struct LogScope : IDisposable, ILogger
+{
+ private readonly ILogger _logger;
+ private readonly IDisposable? _scope;
+
+ public LogScope(ILogger logger, IDisposable? scope = null)
+ {
+ _logger = logger;
+ _scope = scope;
+ }
+
+ ///
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ => _logger.Log(logLevel, eventId, state, exception, formatter);
+
+ ///
+ public bool IsEnabled(LogLevel logLevel)
+ => _logger.IsEnabled(logLevel);
+
+ ///
+ public IDisposable BeginScope(TState state)
+ => _logger.BeginScope(state);
+
+ ///
+ public void Dispose()
+ => _scope?.Dispose();
+}
+#endif
\ No newline at end of file
diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/_Private/ProcessHelper.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/_Private/ProcessHelper.cs
new file mode 100644
index 0000000..8bc46d7
--- /dev/null
+++ b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/_Private/ProcessHelper.cs
@@ -0,0 +1,177 @@
+#if !UNO_RUNTIMETESTS_DISABLE_UI && (__SKIA__ || IS_SECONDARY_APP_SUPPORTED)
+#nullable enable
+
+#if !IS_UNO_RUNTIMETEST_PROJECT
+#pragma warning disable
+#endif
+#pragma warning disable CA1848 // Log perf
+
+using System;
+using System.Diagnostics;
+using System.Text;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Uno.Extensions;
+
+namespace Uno.UI.RuntimeTests.Internal.Helpers;
+
+///
+/// An helper class used by test engine tu run tests in a separate process.
+///
+///
+/// This class is intended to be used only by the the test engine itself and should not be used by applications.
+/// API contract is not guaranteed and might change in future releases.
+///
+internal static partial class ProcessHelper
+{
+ public static async Task ExecuteAsync(
+ CancellationToken ct,
+ string executable,
+ List parameters,
+ string workingDirectory,
+ ILogger log,
+ Dictionary? environmentVariables = null)
+ {
+ var process = SetupProcess(executable, parameters, workingDirectory, environmentVariables);
+ var output = new StringBuilder();
+ var error = new StringBuilder();
+
+ log.LogTrace("Waiting for process exit");
+
+ // hookup the event handlers to capture the data that is received
+ process.OutputDataReceived += (sender, args) => output.Append(args.Data);
+ process.ErrorDataReceived += (sender, args) => error.Append(args.Data);
+
+ if (ct.IsCancellationRequested)
+ {
+ return "";
+ }
+
+ var pi = process.StartInfo;
+ log.LogDebug($"Started process (wd:{pi.WorkingDirectory}): {pi.FileName} {string.Join(" ", pi.ArgumentList)})");
+
+ process.Start();
+
+ // start our event pumps
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+
+ await process.WaitForExitWithCancellationAsync(ct);
+
+ process.EnsureSuccess(log, error);
+
+ return output.ToString();
+ }
+
+ public static async Task ExecuteAndLogAsync(
+ this Process process,
+ ILogger log,
+ CancellationToken ct)
+ {
+ process.StartAndLog(log);
+
+ log.LogTrace("Waiting for process exit");
+
+ await process.WaitForExitWithCancellationAsync(ct);
+
+ process.EnsureSuccess(log);
+ }
+
+ public static Process StartAndLog(
+ string executable,
+ List parameters,
+ string workingDirectory,
+ ILogger log,
+ Dictionary? environmentVariables = null)
+ => SetupProcess(executable, parameters, workingDirectory, environmentVariables).StartAndLog(log);
+
+ public static Process StartAndLog(
+ this Process process,
+ ILogger log)
+ {
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardError = true;
+
+ // hookup the event handlers to capture the data that is received
+ process.OutputDataReceived += (sender, args) => log.LogDebug(args.Data ?? "");
+ process.ErrorDataReceived += (sender, args) => log.LogError(args.Data ?? "");
+
+ var pi = process.StartInfo;
+ log.LogDebug($"Started process (wd:{pi.WorkingDirectory}): {pi.FileName} {string.Join(" ", pi.ArgumentList)})");
+
+ process.Start();
+
+ // start our event pumps
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+
+ return process;
+ }
+
+ private static Process SetupProcess(
+ string executable,
+ List parameters,
+ string workingDirectory,
+ Dictionary? environmentVariables = null)
+ {
+ var pi = new ProcessStartInfo(executable)
+ {
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ WorkingDirectory = workingDirectory,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ };
+
+ foreach (var param in parameters)
+ {
+ pi.ArgumentList.Add(param);
+ }
+
+ if (environmentVariables is not null)
+ {
+ foreach (var env in environmentVariables)
+ {
+ pi.EnvironmentVariables[env.Key] = env.Value;
+ }
+ }
+
+ var process = new System.Diagnostics.Process
+ {
+ StartInfo = pi
+ };
+
+ return process;
+ }
+
+ public static async Task WaitForExitWithCancellationAsync(this Process process, CancellationToken ct)
+ {
+ await using var cancel = ct.Register(process.Close);
+ await process.WaitForExitAsync(CancellationToken.None); // If the ct has been cancelled, we want to wait for exit!
+ }
+
+ public static void EnsureSuccess(this Process process, ILogger log, StringBuilder error)
+ {
+ if (process.ExitCode != 0)
+ {
+ var processError = new InvalidOperationException(error.ToString());
+ log.LogError($"Process '{process.StartInfo.FileName}' failed with code {process.ExitCode}", processError);
+
+ throw new InvalidOperationException($"Process '{process.StartInfo.FileName}' failed with code {process.ExitCode}", processError);
+ }
+ }
+
+ public static void EnsureSuccess(this Process process, ILogger log)
+ {
+ if (process.ExitCode != 0)
+ {
+ log.LogError($"Process '{process.StartInfo.FileName}' failed with code {process.ExitCode}");
+
+ throw new InvalidOperationException($"Process '{process.StartInfo.FileName}' failed with code {process.ExitCode}");
+ }
+ }
+}
+#endif
diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/_Private/SecondaryApp.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/_Private/SecondaryApp.cs
new file mode 100644
index 0000000..a4cd4a5
--- /dev/null
+++ b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/ExternalRunner/_Private/SecondaryApp.cs
@@ -0,0 +1,113 @@
+#if !UNO_RUNTIMETESTS_DISABLE_UI
+#nullable enable
+
+#if !IS_UNO_RUNTIMETEST_PROJECT
+#pragma warning disable
+#endif
+
+#if __SKIA__
+#define IS_SECONDARY_APP_SUPPORTED
+#endif
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+using System.Threading;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Uno.Extensions;
+
+namespace Uno.UI.RuntimeTests.Internal.Helpers;
+
+///
+/// Helper class to run tests in a secondary app.
+///
+///
+/// This class is intended to be used only by the the test engine itself and should not be used by applications.
+/// API contract is not guaranteed and might change in future releases.
+///
+internal static partial class SecondaryApp
+{
+ ///
+ /// Gets a boolean indicating if the current platform supports running tests in a secondary app.
+ ///
+ public static bool IsSupported => // Note: not as const to avoid "CS0162 unreachable code" warning
+#if IS_SECONDARY_APP_SUPPORTED
+ true;
+#else
+ false;
+#endif
+
+ ///
+ /// Run the tests defined by the given configuration in another instance of the current application.
+ ///
+ /// Test engine configuration.
+ /// Token to cancel the test run.
+ /// Indicates if the application should be ran head-less or not.
+ /// The test results.
+ internal static async Task RunTest(UnitTestEngineConfig config, CancellationToken ct, bool isAppVisible = false)
+ {
+#if !IS_SECONDARY_APP_SUPPORTED
+ throw new NotSupportedException("Secondary app is not supported on this platform.");
+#else
+ // First we fetch and start the dev-server (needed to HR tests for instance)
+ await using var devServer = await DevServer.Start(ct);
+
+ // Second we start the app (requesting it to connect to the test-dev-server and to run the tests)
+ var resultFile = await RunLocalApp("127.0.0.1", devServer.Port, config, isAppVisible, ct);
+
+ // Finally, read the test results
+ try
+ {
+ var results = await JsonSerializer.DeserializeAsync(File.OpenRead(resultFile), cancellationToken: ct);
+
+ return results ?? Array.Empty();
+ }
+ catch (JsonException error)
+ {
+ throw new InvalidOperationException(
+ $"Failed to deserialize the test results from '{resultFile}', this usually indicates that the secondary app has been closed (or crashed) before the end of the test suit.",
+ error);
+ }
+ }
+
+ private static int _instance;
+
+ private static async Task RunLocalApp(string devServerHost, int devServerPort, UnitTestEngineConfig config, bool isAppVisible, CancellationToken ct)
+ {
+ var testOutput = Path.GetTempFileName();
+
+ var childStartInfo = new ProcessStartInfo(
+ Environment.ProcessPath ?? throw new InvalidOperationException("Cannot determine the current app executable path"),
+ string.Join(" ", Environment.GetCommandLineArgs().Select(arg => '"' + arg + '"')))
+ {
+ UseShellExecute = false,
+ CreateNoWindow = !isAppVisible,
+ WindowStyle = isAppVisible ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden,
+ WorkingDirectory = Environment.CurrentDirectory,
+ };
+
+ // Configure the runtime to allow hot-reload
+ childStartInfo.EnvironmentVariables["DOTNET_MODIFIABLE_ASSEMBLIES"] = "debug";
+
+ // Requests to the uno app to attempt to connect to the given dev-server instance
+ childStartInfo.EnvironmentVariables.Add("UNO_DEV_SERVER_HOST", devServerHost);
+ childStartInfo.EnvironmentVariables.Add("UNO_DEV_SERVER_PORT", devServerPort.ToString());
+
+ // Request to the runtime tests engine to auto-start at startup
+ childStartInfo.EnvironmentVariables.Add("UNO_RUNTIME_TESTS_RUN_TESTS", JsonSerializer.Serialize(config, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault }));
+ childStartInfo.EnvironmentVariables.Add("UNO_RUNTIME_TESTS_OUTPUT_PATH", testOutput);
+ childStartInfo.EnvironmentVariables.Add("UNO_RUNTIME_TESTS_OUTPUT_KIND", "UnoRuntimeTests"); // "NUnit"
+ childStartInfo.EnvironmentVariables.Add("UNO_RUNTIME_TESTS_IS_SECONDARY_APP", "true"); // "NUnit"
+
+ var childProcess = new Process { StartInfo = childStartInfo };
+
+ await childProcess.ExecuteAndLogAsync(typeof(SecondaryApp).CreateScopedLog($"CHILD_TEST_APP_{Interlocked.Increment(ref _instance):D2}"), ct);
+
+ return testOutput;
+#endif
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/UI/TestCase.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/TestCase.cs
similarity index 100%
rename from src/Uno.UI.RuntimeTests.Engine.Library/UI/TestCase.cs
rename to src/Uno.UI.RuntimeTests.Engine.Library/Engine/TestCase.cs
diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/UI/TestCaseResult.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/TestCaseResult.cs
similarity index 52%
rename from src/Uno.UI.RuntimeTests.Engine.Library/UI/TestCaseResult.cs
rename to src/Uno.UI.RuntimeTests.Engine.Library/Engine/TestCaseResult.cs
index e0bb2d6..8952cf1 100644
--- a/src/Uno.UI.RuntimeTests.Engine.Library/UI/TestCaseResult.cs
+++ b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/TestCaseResult.cs
@@ -13,6 +13,14 @@ internal record TestCaseResult
public string? TestName { get; init; }
public TimeSpan Duration { get; init; }
public string? Message { get; init; }
+ public TestCaseError? Error { get; init; }
+ public string? ConsoleOutput { get; init; }
+}
+
+internal record TestCaseError(string Type, string Message)
+{
+ public static implicit operator TestCaseError?(Exception? ex)
+ => ex is null ? default : new TestCaseError(ex.GetType().Name, ex.Message);
}
diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/UI/TestResult.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/TestResult.cs
similarity index 100%
rename from src/Uno.UI.RuntimeTests.Engine.Library/UI/TestResult.cs
rename to src/Uno.UI.RuntimeTests.Engine.Library/Engine/TestResult.cs
diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.CustomConsoleOutput.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/UI/UnitTestsControl.CustomConsoleOutput.cs
similarity index 100%
rename from src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.CustomConsoleOutput.cs
rename to src/Uno.UI.RuntimeTests.Engine.Library/Engine/UI/UnitTestsControl.CustomConsoleOutput.cs
diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/UI/TestRun.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/UI/UnitTestsControl.TestRun.cs
similarity index 100%
rename from src/Uno.UI.RuntimeTests.Engine.Library/UI/TestRun.cs
rename to src/Uno.UI.RuntimeTests.Engine.Library/Engine/UI/UnitTestsControl.TestRun.cs
diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/UI/UnitTestsControl.cs
similarity index 80%
rename from src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.cs
rename to src/Uno.UI.RuntimeTests.Engine.Library/Engine/UI/UnitTestsControl.cs
index 4a8c27b..a34456d 100644
--- a/src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.cs
+++ b/src/Uno.UI.RuntimeTests.Engine.Library/Engine/UI/UnitTestsControl.cs
@@ -1,6 +1,7 @@
#if !UNO_RUNTIMETESTS_DISABLE_UI
#nullable enable
+#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
using System;
using System.Collections.Generic;
@@ -12,6 +13,7 @@
using System.Reflection;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
@@ -19,7 +21,7 @@
using Microsoft.Extensions.Logging;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
-using Newtonsoft.Json;
+using Uno.UI.RuntimeTests.Engine;
using Uno.UI.RuntimeTests.Internal.Helpers;
#if HAS_UNO
@@ -65,7 +67,6 @@ public sealed partial class UnitTestsControl : UserControl
#endif
#pragma warning restore CS0109
- private const StringComparison StrComp = StringComparison.InvariantCultureIgnoreCase;
private Task? _runner;
private CancellationTokenSource? _cts = new CancellationTokenSource();
#if DEBUG
@@ -80,7 +81,7 @@ public sealed partial class UnitTestsControl : UserControl
private ApplicationView? _applicationView;
#pragma warning restore CS0649 // Unused field
- private List _testCases = new List();
+ private readonly List _testCases = new();
private TestRun? _currentRun;
// On WinUI/UWP dependency properties cannot be accessed outside of
@@ -113,6 +114,8 @@ public UnitTestsControl()
#endif
}
+ internal IEnumerable Results => _testCases;
+
private static void OverrideDebugProviderAsserts()
{
#if NETSTANDARD2_0 || NET5_0_OR_GREATER
@@ -139,6 +142,15 @@ public bool IsRunningOnCI
public static readonly DependencyProperty IsRunningOnCIProperty =
DependencyProperty.Register("IsRunningOnCI", typeof(bool), typeof(UnitTestsControl), new PropertyMetadata(false));
+ public bool IsSecondaryApp
+ {
+ get { return (bool)GetValue(IsSecondaryAppProperty); }
+ set { SetValue(IsSecondaryAppProperty, value); }
+ }
+
+ public static readonly DependencyProperty IsSecondaryAppProperty =
+ DependencyProperty.Register(nameof(IsSecondaryApp), typeof(bool), typeof(UnitTestsControl), new PropertyMetadata(false));
+
///
/// Defines the test group for splitting runtime tests on CI
///
@@ -319,17 +331,35 @@ private void ReportTestClass(TypeInfo testClass)
}
private void ReportTestResult(string testName, TimeSpan duration, TestResult testResult, Exception? error = null, string? message = null, string? console = null)
- {
- _testCases.Add(
+ => ReportTestResult(
new TestCaseResult
{
TestName = testName,
Duration = duration,
TestResult = testResult,
- Message = error?.ToString() ?? message
+ Message = error?.ToString() ?? message,
+ Error = error,
+ ConsoleOutput = console
});
- void Update()
+ private void ReportTestResult(params TestCaseResult[] results)
+ {
+ _testCases.AddRange(results);
+ _dispatcher.Invoke(() =>
+ {
+ foreach (var result in results)
+ {
+ UpdateUI(result);
+ }
+ });
+#if HAS_UNO
+ foreach (var result in results)
+ {
+ _log?.Info($"Test completed '{result.TestName}'='{result.TestResult}'");
+ }
+#endif
+
+ void UpdateUI(TestCaseResult result)
{
if (_currentRun is null)
{
@@ -354,27 +384,27 @@ void Update()
testResultBlock.Inlines.Add(new Run
{
- Text = GetTestResultIcon(testResult) + ' ' + testName + retriesText,
+ Text = GetTestResultIcon(result.TestResult) + ' ' + result.TestName + retriesText,
FontSize = 13.5d,
- Foreground = new SolidColorBrush(UnitTestsControl.GetTestResultColor(testResult)),
+ Foreground = new SolidColorBrush(UnitTestsControl.GetTestResultColor(result.TestResult)),
FontWeight = FontWeights.ExtraBold
});
- if (message is { })
+ if (result.Message is { })
{
- testResultBlock.Inlines.Add(new Run { Text = "\n ..." + message, FontStyle = FontStyle.Italic });
+ testResultBlock.Inlines.Add(new Run { Text = "\n ..." + result.Message, FontStyle = FontStyle.Italic });
}
- if (error is { })
+ if (result.Error is { })
{
- var isFailed = testResult == TestResult.Failed || testResult == TestResult.Error;
+ var isFailed = result.TestResult == TestResult.Failed || result.TestResult == TestResult.Error;
var foreground = isFailed ? new SolidColorBrush(Colors.Red) : new SolidColorBrush(Colors.Yellow);
- testResultBlock.Inlines.Add(new Run { Text = "\nEXCEPTION>" + error.Message, Foreground = foreground });
+ testResultBlock.Inlines.Add(new Run { Text = "\nEXCEPTION>" + result.Error.Message, Foreground = foreground });
if (isFailed)
{
- failedTestDetails.Text += $"{testResult}: {testName} [{error.GetType()}] \n {error}\n\n";
+ failedTestDetails.Text += $"{result.TestResult}: {result.TestName} [{result.Error.Type}] \n {result.Error.Message}\n\n";
if (failedTestDetailsRow.Height.Value == 0)
{
failedTestDetailsRow.Height = new GridLength(100);
@@ -382,9 +412,9 @@ void Update()
}
}
- if (console is { })
+ if (result.ConsoleOutput is { })
{
- testResultBlock.Inlines.Add(new Run { Text = "\nOUT>" + console, Foreground = new SolidColorBrush(Colors.Gray) });
+ testResultBlock.Inlines.Add(new Run { Text = "\nOUT>" + result.ConsoleOutput, Foreground = new SolidColorBrush(Colors.Gray) });
}
if (!IsRunningOnCI)
@@ -393,13 +423,11 @@ void Update()
testResultBlock.StartBringIntoView();
}
- if (testResult == TestResult.Error || testResult == TestResult.Failed)
+ if (result.TestResult == TestResult.Error || result.TestResult == TestResult.Failed)
{
- failedTests.Text += "§" + testName;
+ failedTests.Text += "§" + result.TestName;
}
}
-
- _dispatcher.Invoke(Update);
}
private static string GenerateNUnitTestResults(List testCases, TestRun testRun)
@@ -492,14 +520,14 @@ private void EnableConfigPersistence()
{
try
{
- var config = JsonConvert.DeserializeObject(configStr);
+ var config = JsonSerializer.Deserialize(configStr);
if (config is not null)
{
consoleOutput.IsChecked = config.IsConsoleOutputEnabled;
runIgnored.IsChecked = config.IsRunningIgnored;
retry.IsChecked = config.Attempts > 1;
- testFilter.Text = string.Join(";", config.Filters ?? Array.Empty());
+ testFilter.Text = config.Filter;
}
}
catch (Exception)
@@ -525,7 +553,7 @@ private void ListenConfigChanged()
void StoreConfig()
{
var config = BuildConfig();
- ApplicationData.Current.LocalSettings.Values["unitestcontrols_config"] = JsonConvert.SerializeObject(config);
+ ApplicationData.Current.LocalSettings.Values["unitestcontrols_config"] = JsonSerializer.Serialize(config);
}
}
@@ -542,7 +570,7 @@ private UnitTestEngineConfig BuildConfig()
return new UnitTestEngineConfig
{
- Filters = filter?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(),
+ Filter = filter,
IsConsoleOutputEnabled = isConsoleOutput,
IsRunningIgnored = isRunningIgnored,
Attempts = attempts,
@@ -675,17 +703,6 @@ await _dispatcher.RunAsync(() =>
await GenerateTestResults();
}
- private static IEnumerable FilterTests(UnitTestClassInfo testClassInfo, string[]? filters)
- {
- var testClassNameContainsFilters = filters?.Any(f => testClassInfo.Type?.FullName?.Contains(f, StrComp) ?? false) ?? false;
- return testClassInfo.Tests?.
- Where(t => ((!filters?.Any()) ?? true)
- || testClassNameContainsFilters
- || (filters?.Any(f => t.DeclaringType?.FullName?.Contains(f, StrComp) ?? false) ?? false)
- || (filters?.Any(f => t.Name.Contains(f, StrComp)) ?? false))
- ?? Array.Empty();
- }
-
private async Task ExecuteTestsForInstance(
CancellationToken ct,
object instance,
@@ -696,7 +713,9 @@ private async Task ExecuteTestsForInstance(
? ConsoleOutputRecorder.Start()
: default;
- var tests = UnitTestsControl.FilterTests(testClassInfo, config.Filters)
+ var tests = (config.Filter is null
+ ? testClassInfo.Tests
+ : testClassInfo.Tests.Where(test => config.Filter.IsMatch(test)))
.Select(method => new UnitTestMethodInfo(instance, method))
.ToArray();
@@ -708,6 +727,52 @@ private async Task ExecuteTestsForInstance(
ReportTestClass(testClassInfo.Type.GetTypeInfo());
_ = ReportMessage($"Running {tests.Length} test methods");
+ if (testClassInfo.RunsInSecondaryApp is { } secondaryApp && !IsSecondaryApp)
+ {
+ try
+ {
+ var testCases = tests.SelectMany(t => t.GetCases(ct)).ToList();
+ if (!SecondaryApp.IsSupported && secondaryApp.IgnoreIfNotSupported && !config.IsRunningIgnored)
+ {
+ foreach (var testCase in testCases)
+ {
+ ReportTestResult(
+ testCase.ToString(),
+ TimeSpan.Zero,
+ TestResult.Skipped,
+ null,
+ $"Test of class {instance.GetType().Name} are expected to be run in a secondary app, but secondary app is not supported on this platform.");
+ }
+
+ return;
+ }
+
+ config = config with { Filter = $"{testClassInfo.Type.FullName} & ({config.Filter})" };
+
+ var results = await SecondaryApp.RunTest(config, ct, isAppVisible: Debugger.IsAttached);
+ foreach (var result in results)
+ {
+ ReportTestResult(result);
+ }
+
+ if (results.Length != testCases.Count)
+ {
+ ReportTestResult(
+ instance.GetType().Name,
+ TimeSpan.Zero,
+ TestResult.Failed,
+ new InvalidOperationException($"Unexpected tests results, got {results.Length} test results from the secondary app for class {instance.GetType().Name} while we where expecting {testCases.Count}.\""),
+ $"Got {results.Length} test results from the secondary app for class {instance.GetType().Name} while we where expecting {testCases.Count}.");
+ }
+ }
+ catch (Exception error)
+ {
+ ReportTestResult(instance.GetType().Name, TimeSpan.Zero, TestResult.Failed, error, $"Failed to run tests in secondary app for test class {instance.GetType().Name}");
+ }
+
+ return;
+ }
+
foreach (var test in tests)
{
var testName = test.Name;
@@ -737,7 +802,7 @@ private async Task ExecuteTestsForInstance(
}
}
- foreach (var testCase in test.GetCases())
+ foreach (var testCase in test.GetCases(ct))
{
if (ct.IsCancellationRequested)
{
@@ -766,7 +831,7 @@ async Task InvokeTestMethod(TestCase testCase)
await ReportMessage($"Running test {fullTestName}");
ReportTestsResults();
- var cleanupActions = new List>();
+ var cleanupActions = new List>();
var sw = new Stopwatch();
var canRetry = true;
@@ -778,7 +843,7 @@ async Task InvokeTestMethod(TestCase testCase)
{
if (test.RequiresFullWindow)
{
- await _dispatcher.RunAsync(() =>
+ await ExecuteOnDispatcher(() =>
{
#if __ANDROID__
// Hide the systray!
@@ -787,10 +852,10 @@ await _dispatcher.RunAsync(() =>
UnitTestsUIContentHelper.UseActualWindowRoot = true;
UnitTestsUIContentHelper.SaveOriginalContent();
- });
+ }, ct);
cleanupActions.Add(async _ =>
{
- await _dispatcher.RunAsync(() =>
+ await ExecuteOnDispatcher(() =>
{
#if __ANDROID__
// Restore the systray!
@@ -798,73 +863,46 @@ await _dispatcher.RunAsync(() =>
#endif
UnitTestsUIContentHelper.RestoreOriginalContent();
UnitTestsUIContentHelper.UseActualWindowRoot = false;
- });
+ }, CancellationToken.None);
});
}
- object? returnValue = null;
- if (test.RunsOnUIThread)
+ // Configure pointers injection
+ await ExecuteOnDispatcher(() =>
{
- await _dispatcher.RunAsync(() =>
+ if (InputInjectorHelper.TryGetCurrent() is not null)
{
- if (InputInjectorHelper.TryGetCurrent() is not null)
- {
- InputInjectorHelper.Current.CleanupPointers();
- }
-
- if (testCase.Pointer is { } pt)
- {
- var ptSubscription = InputInjectorHelper.Current.SetPointerType(pt);
-#pragma warning disable CS1998
- cleanupActions.Add(async _ => ptSubscription.Dispose());
-#pragma warning restore CS1998
- }
-
- if (instance.GetType().GetProperty("Pointers", BindingFlags.Instance | BindingFlags.Public) is { SetMethod: not null } pointerProp
- && pointerProp.PropertyType == typeof(InputInjectorHelper))
- {
- pointerProp.SetMethod.Invoke(instance, new[] { InputInjectorHelper.Current });
- }
-
+ InputInjectorHelper.Current.CleanupPointers();
+ }
- sw.Start();
- testClassInfo.Initialize?.Invoke(instance, Array.Empty