Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Make runtime tests engine more global using friendly #119

Merged
merged 1 commit into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,6 @@
#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;

/// <summary>
Expand All @@ -31,9 +14,9 @@ namespace Uno.UI.RuntimeTests.Internal.Helpers;
/// 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.
/// </remarks>
internal sealed partial class DevServer : IAsyncDisposable
internal sealed partial class DevServer : global::System.IAsyncDisposable
{
private static readonly ILogger _log = typeof(DevServer).Log();
private static readonly global::Microsoft.Extensions.Logging.ILogger _log = global::Uno.Extensions.LogExtensionPoint.Log(typeof(DevServer));
private static int _instance;
private static string? _devServerPath;

Expand All @@ -42,7 +25,7 @@ internal sealed partial class DevServer : IAsyncDisposable
/// </summary>
/// <param name="ct">Cancellation token to abort the initialization of the server.</param>
/// <returns>The new dev server instance.</returns>
public static async Task<DevServer> Start(CancellationToken ct)
public static async global::System.Threading.Tasks.Task<DevServer> Start(global::System.Threading.CancellationToken ct)
{
#if !HAS_UNO_DEVSERVER
throw new NotSupportedException("Dev server has not been referenced.");
Expand All @@ -54,9 +37,9 @@ public static async Task<DevServer> Start(CancellationToken ct)
#endif
}

private readonly Process _process;
private readonly global::System.Diagnostics.Process _process;

private DevServer(Process process, int port)
private DevServer(global::System.Diagnostics.Process process, int port)
{
Port = port;
_process = process;
Expand All @@ -67,16 +50,16 @@ private DevServer(Process process, int port)
/// </summary>
public int Port { get; }

private static async Task<string> GetDevServer(CancellationToken ct)
private static async global::System.Threading.Tasks.Task<string> GetDevServer(global::System.Threading.CancellationToken ct)
=> _devServerPath ??= await PullDevServer(ct);

/// <summary>
/// Pulls the latest version of dev server from NuGet and returns the path to the executable
/// </summary>
private static async Task<string> PullDevServer(CancellationToken ct)
private static async global::System.Threading.Tasks.Task<string> PullDevServer(global::System.Threading.CancellationToken ct)
{
var dir = Path.Combine(Path.GetTempPath(), $"DevServer_{Guid.NewGuid():N}");
Directory.CreateDirectory(dir);
var dir = global::System.IO.Path.Combine(global::System.IO.Path.GetTempPath(), $"DevServer_{(global::System.Guid.NewGuid()):N}");
global::System.IO.Directory.CreateDirectory(dir);

try
{
Expand All @@ -86,7 +69,7 @@ private static async Task<string> PullDevServer(CancellationToken ct)
ct,
"dotnet",
new() { "--version" },
Environment.CurrentDirectory, // Needed to get the version used by the current app (i.e. including global.json)
global::System.Environment.CurrentDirectory, // Needed to get the version used by the current app (i.e. including global.json)
log);
var dotnetVersion = GetDotnetVersion(rawVersion);

Expand All @@ -96,30 +79,28 @@ private static async Task<string> PullDevServer(CancellationToken ct)
<TargetFramework>net{dotnetVersion.Major}.{dotnetVersion.Minor}</TargetFramework>
</PropertyGroup>
</Project>";
await File.WriteAllTextAsync(Path.Combine(dir, "PullDevServer.csproj"), csProj, ct);
await global::System.IO.File.WriteAllTextAsync(global::System.IO.Path.Combine(dir, "PullDevServer.csproj"), csProj, ct);
}

using (var log = _log.Scope<DevServer>("PULL_DEV_SERVER"))
{
var args = new List<string> { "add", "package" };
var args = new global::System.Collections.Generic.List<string> { "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<AssemblyInformationalVersionAttribute>()
?.InformationalVersion is { Length: > 0 } runtimeVersion
&& Regex.Match(runtimeVersion, @"^(?<version>\d+\.\d+\.\d+(-\w+\.\d+))+") is {Success: true} match)
if (global::System.Type.GetType("Uno.UI.RemoteControl.RemoteControlClient, Uno.UI.RemoteControl", throwOnError: false)?.Assembly is { } devServerAssembly
&& global::System.Reflection.CustomAttributeExtensions.GetCustomAttribute<global::System.Reflection.AssemblyInformationalVersionAttribute>(devServerAssembly)?.InformationalVersion is { Length: > 0 } runtimeVersion
&& global::System.Text.RegularExpressions.Regex.Match(runtimeVersion, @"^(?<version>\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<RuntimeTestDevServerAttribute>()?.Version is { Length: > 0 } version)
else if (global::System.Reflection.CustomAttributeExtensions.GetCustomAttribute<global::Uno.UI.RuntimeTests.Engine.RuntimeTestDevServerAttribute>(typeof(DevServer).Assembly)?.Version is { Length: > 0 } version)
{
args.Add("--version");
args.Add(version);
Expand All @@ -144,14 +125,14 @@ private static async Task<string> PullDevServer(CancellationToken ct)

return GetConfigurationValue(data, "RemoteControlHostPath") is { Length: > 0 } path
? path
: throw new InvalidOperationException("Failed to get remote control host path");
: throw new global::System.InvalidOperationException("Failed to get remote control host path");
}
}
finally
{
try
{
Directory.Delete(dir, recursive: true);
global::System.IO.Directory.Delete(dir, recursive: true);
}
catch { /* Nothing to do */ }
}
Expand All @@ -162,51 +143,52 @@ private static async Task<string> PullDevServer(CancellationToken ct)
/// </summary>
private static DevServer StartCore(string hostBinPath, int port)
{
if (!File.Exists(hostBinPath))
if (!global::System.IO.File.Exists(hostBinPath))
{
_log.LogError($"DevServer {hostBinPath} does not exist");
throw new InvalidOperationException($"Unable to find {hostBinPath}");
global::Microsoft.Extensions.Logging.LoggerExtensions.LogError(_log, $"DevServer {hostBinPath} does not exist");
throw new global::System.InvalidOperationException($"Unable to find {hostBinPath}");
}

var arguments = $"\"{hostBinPath}\" --httpPort {port} --ppid {Environment.ProcessId} --metadata-updates true";
var pi = new ProcessStartInfo("dotnet", arguments)
var arguments = $"\"{hostBinPath}\" --httpPort {port} --ppid {(global::System.Environment.ProcessId)} --metadata-updates true";
var pi = new global::System.Diagnostics.ProcessStartInfo("dotnet", arguments)
{
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
WorkingDirectory = Path.GetDirectoryName(hostBinPath),
WindowStyle = global::System.Diagnostics.ProcessWindowStyle.Hidden,
WorkingDirectory = global::System.IO.Path.GetDirectoryName(hostBinPath),
};

var process = new System.Diagnostics.Process { StartInfo = pi };
var process = new global::System.Diagnostics.Process { StartInfo = pi };

process.StartAndLog(_log.Scope<DevServer>($"DEV_SERVER_{Interlocked.Increment(ref _instance):D2}"));
process.StartAndLog(_log.Scope<DevServer>($"DEV_SERVER_{(global::System.Threading.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}>(?<value>.*?)</{nodeName}>") is { Success: true } match
=> global::System.Text.RegularExpressions.Regex.Match(msbuildResult, $"<{nodeName}>(?<value>.*?)</{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 global::System.Version GetDotnetVersion(string dotnetRawVersion)
=> dotnetRawVersion?.Split('-') is { } versionParts
&& global::System.Version.TryParse(global::System.Linq.Enumerable.FirstOrDefault(versionParts), out var version)
? version
: throw new global::System.InvalidOperationException("Failed to read dotnet version");

private static int GetTcpPort()
{
var l = new TcpListener(IPAddress.Loopback, 0);
var l = new global::System.Net.Sockets.TcpListener(global::System.Net.IPAddress.Loopback, 0);
l.Start();
var port = ((IPEndPoint)l.LocalEndpoint).Port;
var port = ((global::System.Net.IPEndPoint)l.LocalEndpoint).Port;
l.Stop();
return port;
}
#endregion

/// <inheritdoc />
public async ValueTask DisposeAsync()
public async global::System.Threading.Tasks.ValueTask DisposeAsync()
{
if (_process is null or { HasExited: true })
{
Expand All @@ -217,12 +199,12 @@ public async ValueTask DisposeAsync()
{
_process.Kill(true); // Best effort, the app should kill itself anyway
}
catch (Exception e)
catch (global::System.Exception e)
{
_log.LogError("Failed to kill dev server", e);
global::Microsoft.Extensions.Logging.LoggerExtensions.LogError(_log, "Failed to kill dev server", e);
}

await _process.WaitForExitAsync(CancellationToken.None);
await _process.WaitForExitAsync(global::System.Threading.CancellationToken.None);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,6 @@
#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;

/// <summary>
Expand Down Expand Up @@ -47,10 +37,10 @@ internal static partial class SecondaryApp
/// <param name="ct">Token to cancel the test run.</param>
/// <param name="isAppVisible">Indicates if the application should be ran head-less or not.</param>
/// <returns>The test results.</returns>
internal static async Task<TestCaseResult[]> RunTest(UnitTestEngineConfig config, CancellationToken ct, bool isAppVisible = false)
internal static async global::System.Threading.Tasks.Task<TestCaseResult[]> RunTest(UnitTestEngineConfig config, global::System.Threading.CancellationToken ct, bool isAppVisible = false)
{
#if !IS_SECONDARY_APP_SUPPORTED
throw new NotSupportedException("Secondary app is not supported on this platform.");
throw new global::System.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);
Expand All @@ -61,32 +51,33 @@ internal static async Task<TestCaseResult[]> RunTest(UnitTestEngineConfig config
// Finally, read the test results
try
{
var results = await JsonSerializer.DeserializeAsync<TestCaseResult[]>(File.OpenRead(resultFile), cancellationToken: ct);
var results = await global::System.Text.Json.JsonSerializer.DeserializeAsync<TestCaseResult[]>(global::System.IO.File.OpenRead(resultFile), cancellationToken: ct);

return results ?? Array.Empty<TestCaseResult>();
return results ?? global::System.Array.Empty<TestCaseResult>();
}
catch (JsonException error)
catch (global::System.Text.Json.JsonException error)
{
throw new InvalidOperationException(
throw new global::System.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<string> RunLocalApp(string devServerHost, int devServerPort, UnitTestEngineConfig config, bool isAppVisible, CancellationToken ct)
private static async global::System.Threading.Tasks.Task<string> RunLocalApp(string devServerHost, int devServerPort, UnitTestEngineConfig config, bool isAppVisible, global::System.Threading.CancellationToken ct)
{
var testOutput = Path.GetTempFileName();
var testOutput = global::System.IO.Path.GetTempFileName();
var configJson = global::System.Text.Json.JsonSerializer.Serialize(config, new global::System.Text.Json.JsonSerializerOptions { DefaultIgnoreCondition = global::System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault });

var childStartInfo = new ProcessStartInfo(
Environment.ProcessPath ?? throw new InvalidOperationException("Cannot determine the current app executable path"),
string.Join(" ", Environment.GetCommandLineArgs().Select(arg => '"' + arg + '"')))
var childStartInfo = new global::System.Diagnostics.ProcessStartInfo(
global::System.Environment.ProcessPath ?? throw new global::System.InvalidOperationException("Cannot determine the current app executable path"),
string.Join(" ", global::System.Linq.Enumerable.Select(global::System.Environment.GetCommandLineArgs(), arg => '"' + arg + '"')))
{
UseShellExecute = false,
CreateNoWindow = !isAppVisible,
WindowStyle = isAppVisible ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden,
WorkingDirectory = Environment.CurrentDirectory,
WindowStyle = isAppVisible ? global::System.Diagnostics.ProcessWindowStyle.Normal : global::System.Diagnostics.ProcessWindowStyle.Hidden,
WorkingDirectory = global::System.Environment.CurrentDirectory,
};

// Configure the runtime to allow hot-reload
Expand All @@ -97,14 +88,14 @@ private static async Task<string> RunLocalApp(string devServerHost, int devServe
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_RUN_TESTS", configJson);
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 };
var childProcess = new global::System.Diagnostics.Process { StartInfo = childStartInfo };

await childProcess.ExecuteAndLogAsync(typeof(SecondaryApp).CreateScopedLog($"CHILD_TEST_APP_{Interlocked.Increment(ref _instance):D2}"), ct);
await childProcess.ExecuteAndLogAsync(typeof(SecondaryApp).CreateScopedLog($"CHILD_TEST_APP_{(global::System.Threading.Interlocked.Increment(ref _instance)):D2}"), ct);

return testOutput;
#endif
Expand Down
Loading
Loading