From 634f0d4b844a48421d34b9e1daafe7fd4477cfa0 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 18 Oct 2023 20:53:32 -0400 Subject: [PATCH] feat: Add ability to use local file system to update file --- src/TestApp/shared/HotReloadTests.cs | 6 +- .../Helpers/HotReloadHelper.DevServer.cs | 101 ++++++ .../Library/Helpers/HotReloadHelper.Local.cs | 48 +++ .../HotReloadHelper.MetadataUpdateHandler.cs | 28 ++ .../Library/Helpers/HotReloadHelper.Mocks.cs | 31 -- .../Helpers/HotReloadHelper.NotSupported.cs | 27 ++ .../Library/Helpers/HotReloadHelper.cs | 287 ++++++++---------- .../CallerArgumentExpressionAttribute.cs | 25 ++ ...o.UI.RuntimeTests.Engine.Library.projitems | 6 +- 9 files changed, 368 insertions(+), 191 deletions(-) create mode 100644 src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.DevServer.cs create mode 100644 src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.Local.cs create mode 100644 src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.MetadataUpdateHandler.cs delete mode 100644 src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.Mocks.cs create mode 100644 src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.NotSupported.cs create mode 100644 src/Uno.UI.RuntimeTests.Engine.Library/Library/_Compat/CallerArgumentExpressionAttribute.cs diff --git a/src/TestApp/shared/HotReloadTests.cs b/src/TestApp/shared/HotReloadTests.cs index 3480fa5..1518df2 100644 --- a/src/TestApp/shared/HotReloadTests.cs +++ b/src/TestApp/shared/HotReloadTests.cs @@ -49,7 +49,7 @@ public async Task Is_SourcesEditable(CancellationToken ct) Assert.IsTrue(File.Exists(file)); Assert.IsTrue(File.ReadAllText(file).Contains("Original text")); - await using var _ = await HotReloadHelper.UpdateServerFile(sutPath, "Original text", "Updated text from Can_Edit_File", waitForMetadataUpdate: false, ct); + 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); @@ -68,7 +68,7 @@ public async Task Is_CodeHotReload_Enabled(CancellationToken ct) Debug.Assert(sut.Value == "42"); - await using var _ = await HotReloadHelper.UpdateServerFile("../../shared/HotReloadTest_SimpleSubject.cs", "42", "43", ct); + await using var _ = await HotReloadHelper.UpdateSourceFile("../../shared/HotReloadTest_SimpleSubject.cs", "42", "43", ct); Debug.Assert(sut.Value == "43"); } @@ -85,7 +85,7 @@ public async Task Is_UIHotReload_Enabled(CancellationToken ct) Assert.AreEqual("Original text", UIHelper.GetChild().Text); - await using var _ = await HotReloadHelper.UpdateServerFile("Original text", "Updated text", ct); + await using var _ = await HotReloadHelper.UpdateSourceFile("Original text", "Updated text", ct); await AsyncAssert.AreEqual("Updated text", () => UIHelper.GetChild().Text, ct); } diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.DevServer.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.DevServer.cs new file mode 100644 index 0000000..5877f7a --- /dev/null +++ b/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.DevServer.cs @@ -0,0 +1,101 @@ +#if !UNO_RUNTIMETESTS_DISABLE_LIBRARY && HAS_UNO_DEVSERVER // Set as soon as the DevServer package is referenced, cf. Uno.UI.RuntimeTests.Engine.targets +#nullable enable + +#if !IS_UNO_RUNTIMETEST_PROJECT +#pragma warning disable +#endif +#pragma warning disable CA1848 // Log perf + +using System; +using System.Collections; +using System.IO; +using System.Net.WebSockets; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Uno.UI.RuntimeTests.Internal.Helpers; +using Uno.UI.RemoteControl; // DevServer +using Uno.UI.RemoteControl.HotReload; // DevServer +using Uno.UI.RemoteControl.HotReload.Messages; // DevServer +using Uno.UI.RemoteControl.HotReload.MetadataUpdater; // DevServer + +namespace Uno.UI.RuntimeTests; + +partial class HotReloadHelper +{ + static partial void TryUseDevServerFileUpdater() + => Use(new DevServerUpdater()); + + partial record FileEdit + { + public UpdateFile ToMessage() + => new UpdateFile + { + FilePath = FilePath, + OldText = OldText, + NewText = NewText + }; + } + + private class DevServerUpdater : IFileUpdater + { + /// + public async ValueTask EnsureReady(CancellationToken ct) + { + _log.LogTrace("Getting dev-server ready ..."); + + if (RemoteControlClient.Instance is null) + { + throw new InvalidOperationException("Dev server is not available."); + } + + var timeout = Task.Delay(ConnectionTimeout, ct); + if (await Task.WhenAny(timeout, RemoteControlClient.Instance.WaitForConnection(ct)) == timeout) + { + throw new TimeoutException( + "Timeout while waiting for the app to connect to the dev-server. " + + "This usually indicates that the dev-server is not running on the expected combination of hots and port" + + "(For runtime-tests run in secondary app instance, the server should be listening for connection on " + + $"{Environment.GetEnvironmentVariable("UNO_DEV_SERVER_HOST")}:{Environment.GetEnvironmentVariable("UNO_DEV_SERVER_PORT")})."); + } + + _log.LogTrace("Client connected, waiting for dev-server to load the workspace (i.e. initializing roslyn with the solution) ..."); + + var processors = typeof(RemoteControlClient).GetField("_processors", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(RemoteControlClient.Instance) as IDictionary ?? throw new InvalidOperationException("Processors is null"); + var processor = processors["hotreload"] as ClientHotReloadProcessor ?? throw new InvalidOperationException("HotReloadProcessor is null"); + var hotReloadReady = typeof(ClientHotReloadProcessor).GetProperty("HotReloadWorkspaceLoaded", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(processor) as Task ?? throw new IOException("HotReloadWorkspaceLoaded is null"); + + timeout = Task.Delay(WorkspaceTimeout, ct); + if (await Task.WhenAny(timeout, hotReloadReady) == timeout) + { + throw new TimeoutException( + "Timeout while waiting for hot reload workspace to be loaded. " + + "This usually indicates that the dev-server failed to load the solution, you will find more info in dev-server logs " + + "(output in main app logs with [DEV_SERVER] prefix for runtime-tests run in secondary app instance)."); + } + + _log.LogTrace("Workspace is ready on dev-server, sending file update request ..."); + } + + /// + public async ValueTask Apply(FileEdit edition, CancellationToken ct) + { + try + { + await RemoteControlClient.Instance!.SendMessage(edition.ToMessage()); + + return null; + } + catch (WebSocketException) + { + return ct => + { + _log.LogWarning($"WAITING {RemoteControlClient.Instance!.ConnectionRetryInterval:g} to let client reconnect before retry**."); + return Task.Delay(RemoteControlClient.Instance.ConnectionRetryInterval + TimeSpan.FromMilliseconds(100), ct); + }; + } + } + } +} +#endif diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.Local.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.Local.cs new file mode 100644 index 0000000..98a8b09 --- /dev/null +++ b/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.Local.cs @@ -0,0 +1,48 @@ +#if !UNO_RUNTIMETESTS_DISABLE_LIBRARY + +#if !IS_UNO_RUNTIMETEST_PROJECT +#pragma warning disable +#endif + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Uno.UI.RuntimeTests; + +partial class HotReloadHelper +{ + static partial void TryUseLocalFileUpdater() + => Use(new LocalFileUpdater()); + + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static void UseLocalFileUpdater() + => Use(new LocalFileUpdater()); + + private class LocalFileUpdater : IFileUpdater + { + /// + public async ValueTask EnsureReady(CancellationToken ct) { } + + /// + public async ValueTask Apply(FileEdit edition, CancellationToken ct) + { + if (!File.Exists(edition.FilePath)) + { + throw new InvalidOperationException($"Source file {edition.FilePath} does not exist!"); + } + + var originalContent = await File.ReadAllTextAsync(edition.FilePath, ct); + var updatedContent = originalContent.Replace(edition.OldText, edition.NewText); + + await File.WriteAllTextAsync(edition.FilePath, updatedContent, ct); + + return null; + } + } +} +#endif \ No newline at end of file diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.MetadataUpdateHandler.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.MetadataUpdateHandler.cs new file mode 100644 index 0000000..bc7002e --- /dev/null +++ b/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.MetadataUpdateHandler.cs @@ -0,0 +1,28 @@ +#if !UNO_RUNTIMETESTS_DISABLE_LIBRARY + +#if !IS_UNO_RUNTIMETEST_PROJECT +#pragma warning disable +#endif + +using System; +using System.Collections.Generic; +using System.Text; +using Uno.UI.RuntimeTests; + +[assembly:System.Reflection.Metadata.MetadataUpdateHandlerAttribute(typeof(HotReloadHelper.MetadataUpdateHandler))] + +namespace Uno.UI.RuntimeTests; + +partial class HotReloadHelper +{ + internal static class MetadataUpdateHandler + { + public static event EventHandler? MetadataUpdated; + + internal static void UpdateApplication(Type[]? types) + { + MetadataUpdated?.Invoke(null, EventArgs.Empty); + } + } +} +#endif diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.Mocks.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.Mocks.cs deleted file mode 100644 index cfb6fcb..0000000 --- a/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.Mocks.cs +++ /dev/null @@ -1,31 +0,0 @@ -#if !UNO_RUNTIMETESTS_DISABLE_LIBRARY -#nullable enable - -#if !IS_UNO_RUNTIMETEST_PROJECT -#pragma warning disable -#endif - -using System; -using System.Collections.Generic; -using System.Text; - -#if !HAS_UNO_DEVSERVER -namespace Uno.UI.RemoteControl -{ - public partial class RemoteControlClient - { - public static RemoteControlClient? Instance => null; - } -} - -namespace Uno.UI.RemoteControl.HotReload.Messages -{ - public partial class UpdateFile - { - public string FilePath { get; set; } = string.Empty; - public string OldText { get; set; } = string.Empty; - public string NewText { get; set; } = string.Empty; - } -} -#endif -#endif \ No newline at end of file diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.NotSupported.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.NotSupported.cs new file mode 100644 index 0000000..4b8ac88 --- /dev/null +++ b/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.NotSupported.cs @@ -0,0 +1,27 @@ +#if !UNO_RUNTIMETESTS_DISABLE_LIBRARY + +#if !IS_UNO_RUNTIMETEST_PROJECT +#pragma warning disable +#endif + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Uno.UI.RuntimeTests; + +public static partial class HotReloadHelper +{ + private class NotSupported : IFileUpdater + { + /// + public ValueTask EnsureReady(CancellationToken ct) + => throw new NotSupportedException("Source code file edition is not supported on this platform."); + + /// + public ValueTask Apply(FileEdit edition, CancellationToken ct) + => throw new NotSupportedException("Source code file edition is not supported on this platform."); + } +} +#endif \ No newline at end of file diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.cs index 710ffc5..5fe4b08 100644 --- a/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.cs +++ b/src/Uno.UI.RuntimeTests.Engine.Library/Library/Helpers/HotReloadHelper.cs @@ -4,10 +4,13 @@ #if !IS_UNO_RUNTIMETEST_PROJECT #pragma warning disable #endif + #pragma warning disable CA1848 // Log perf +#pragma warning disable CA1823 // Field not used using System; using System.Collections; +using System.ComponentModel; using System.Globalization; using System.Linq; using System.Reflection; @@ -27,28 +30,49 @@ using Windows.UI.Xaml; #endif -#if HAS_UNO_DEVSERVER // Set as soon as the DevServer package is referenced, cf. Uno.UI.RuntimeTests.Engine.targets -using Uno.UI.RemoteControl; // DevServer -using Uno.UI.RemoteControl.HotReload; // DevServer -using Uno.UI.RemoteControl.HotReload.Messages; // DevServer -using Uno.UI.RemoteControl.HotReload.MetadataUpdater; // DevServer -#else -using Uno.UI.RemoteControl; // Mock, cf. HotReloadHelper.Mocks.cs -using Uno.UI.RemoteControl.HotReload.Messages; // Mock, cf. HotReloadHelper.Mocks.cs - -#pragma warning disable CA1823 // Field not used -#endif - namespace Uno.UI.RuntimeTests; public static partial class HotReloadHelper { - public static bool IsSupported => // Note: not as const to avoid "CS0162 unreachable code" warning -#if HAS_UNO_DEVSERVER - true; -#else - false; -#endif + public partial record FileEdit(string FilePath, string OldText, string NewText) + { + public FileEdit Revert() => this with { OldText = NewText, NewText = OldText }; + } + + private record RevertFileEdit(FileEdit Edition, bool WaitForMetadataUpdate) : IAsyncDisposable + { + /// + public async ValueTask DisposeAsync() + { + var revert = Edition.Revert(); + + _log.LogInformation($"Reverting changes made on {revert.FilePath} (from: \"{StartEnd(revert.OldText)}\" to \"{StartEnd(revert.NewText)}\")."); + + // Note: We also wait for metadata update here to ensure that the file is reverted before the test continues / we run another test. + if (await SendMessageCore(revert, WaitForMetadataUpdate, CancellationToken.None) is ReConnect reconnect) + { + _log.LogWarning($"Failed to revert file edition, let {_impl} reconnect."); + + await reconnect(CancellationToken.None); + if (await SendMessageCore(revert, WaitForMetadataUpdate, CancellationToken.None) is not null) + { + _log.LogError($"Failed to revert file edition on file {revert.FilePath}."); + } + } + } + } + + public delegate Task ReConnect(CancellationToken ct); + + public interface IFileUpdater + { + ValueTask EnsureReady(CancellationToken ct); + + ValueTask Apply(FileEdit edition, CancellationToken ct); + } + + private static readonly ILogger _log = typeof(HotReloadHelper).Log(); + private static IFileUpdater _impl = new NotSupported(); // The delay for the client to connect to the dev-server private static TimeSpan ConnectionTimeout = TimeSpan.FromSeconds(3); @@ -59,21 +83,49 @@ public static partial class HotReloadHelper // The delay for the server to send metadata update once a file has been modified private static TimeSpan MetadataUpdateTimeout = TimeSpan.FromSeconds(5); - private static readonly ILogger _log = typeof(HotReloadHelper).Log(); + static HotReloadHelper() + { + if (!IsSupported) + { + TryUseDevServerFileUpdater(); + } + + if (!IsSupported) + { + TryUseLocalFileUpdater(); + } + } + + static partial void TryUseDevServerFileUpdater(); + static partial void TryUseLocalFileUpdater(); /// - /// Request on the server to replace all occurrences of by the in the given file. + /// Configures the to use. + /// + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static void Use(IFileUpdater updater) + => _impl = updater; + + /// + /// Gets a boolean which indicates if the HotReloadHelper is supported on the current platform. + /// + /// If this returns false, all methods on this class are expected to throw exceptions. + public static bool IsSupported => _impl is not NotSupported; + + /// + /// Request to replace all occurrences of by the in the given source code file. /// /// Path of file in which the replacement should take place, relative to the project which reference the runtime-test engine (usually the application head). /// The original text to replace. /// The replacement text. /// An cancellation token to abort the asynchronous operation. /// An IAsyncDisposable object that will revert the change when disposed. - public static async ValueTask UpdateServerFile(string filPathRelativeToProject, string originalText, string replacementText, CancellationToken ct = default) - => await UpdateServerFile(filPathRelativeToProject, originalText, replacementText, true, ct); + public static async ValueTask UpdateSourceFile(string filPathRelativeToProject, string originalText, string replacementText, CancellationToken ct = default) + => await UpdateSourceFile(filPathRelativeToProject, originalText, replacementText, true, ct); /// - /// Request on the server to replace all occurrences of by the in the given file. + /// Request to replace all occurrences of by the in the given source code file. /// /// Path of file in which the replacement should take place, relative to the project which reference the runtime-test engine (usually the application head). /// The original text to replace. @@ -81,7 +133,7 @@ public static async ValueTask UpdateServerFile(string filPathR /// Request to wait for a metadata update to consider the file update to be applied. /// An cancellation token to abort the asynchronous operation. /// An IAsyncDisposable object that will revert the change when disposed. - public static async ValueTask UpdateServerFile(string filPathRelativeToProject, string originalText, string replacementText, bool waitForMetadataUpdate, CancellationToken ct = default) + public static async ValueTask UpdateSourceFile(string filPathRelativeToProject, string originalText, string replacementText, bool waitForMetadataUpdate, CancellationToken ct = default) { var projectFile = typeof(HotReloadHelper).Assembly.GetCustomAttribute()?.ProjectFullPath; if (projectFile is null or { Length: 0 }) @@ -102,112 +154,91 @@ public static async ValueTask UpdateServerFile(string filPathR } #endif - var message = new UpdateFile - { - FilePath = Path.Combine(projectDir, filPathRelativeToProject), - OldText = originalText, - NewText = replacementText - }; - - return await UpdateServerFile(message, waitForMetadataUpdate, ct); + return await UpdateSourceFile(new FileEdit(Path.Combine(projectDir, filPathRelativeToProject), originalText, replacementText), waitForMetadataUpdate, ct); } /// - /// Request on the server to replace all occurrences of by the in the source file of the given . + /// Request to replace all occurrences of by the in the source file of the given . /// /// Type of the for which the source code should be edited. /// The original text to replace. /// The replacement text. /// An cancellation token to abort the asynchronous operation. /// An IAsyncDisposable object that will revert the change when disposed. - public static async ValueTask UpdateServerFile(string originalText, string replacementText, CancellationToken ct = default) + public static async ValueTask UpdateSourceFile(string originalText, string replacementText, CancellationToken ct = default) where T : FrameworkElement, new() { - if (RemoteControlClient.Instance is null) - { - throw new InvalidOperationException("Dev server is not available."); - } - - var message = new T().CreateUpdateFileMessage( + var edition = new T().CreateFileEdit( originalText: originalText, replacementText: replacementText); - return await UpdateServerFile(message, true, ct); + return await UpdateSourceFile(edition, true, ct); } /// - /// Request on the server to apply teh given file update request. + /// Request to apply the given edition on the source code file. /// /// The file update request. /// Request to wait for a metadata update to consider the file update to be applied. /// An cancellation token to abort the asynchronous operation. /// An IAsyncDisposable object that will revert the change when disposed. -#if !HAS_UNO_DEVSERVER - public static async Task UpdateServerFile(UpdateFile message, bool waitForMetadataUpdate, CancellationToken ct = default) - { - _log.LogError("Dev server is not available."); - throw new InvalidOperationException("Dev server is not available."); - } -#else - public static async Task UpdateServerFile(UpdateFile message, bool waitForMetadataUpdate, CancellationToken ct = default) + public static async Task UpdateSourceFile(FileEdit message, bool waitForMetadataUpdate, CancellationToken ct = default) { _log.LogTrace($"Waiting for connection in order to update file {message.FilePath} (from: \"{StartEnd(message.OldText)}\" to \"{StartEnd(message.NewText)}\") ..."); + await _impl.EnsureReady(ct); - if (RemoteControlClient.Instance is null) + var revertMessage = new RevertFileEdit(message, waitForMetadataUpdate); + try { - throw new InvalidOperationException("Dev server is not available."); + await SendMessageCore(message, waitForMetadataUpdate, ct); } - - var timeout = Task.Delay(ConnectionTimeout, ct); - if (await Task.WhenAny(timeout, RemoteControlClient.Instance.WaitForConnection(ct)) == timeout) - { - throw new TimeoutException( - "Timeout while waiting for the app to connect to the dev-server. " - + "This usually indicates that the dev-server is not running on the expected combination of hots and port" - + "(For runtime-tests run in secondary app instance, the server should be listening for connection on " - + $"{Environment.GetEnvironmentVariable("UNO_DEV_SERVER_HOST")}:{Environment.GetEnvironmentVariable("UNO_DEV_SERVER_PORT")})."); - } - - _log.LogTrace("Client connected, waiting for dev-server to load the workspace (i.e. initializing roslyn with the solution) ..."); - - var processors = typeof(RemoteControlClient).GetField("_processors", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(RemoteControlClient.Instance) as IDictionary ?? throw new InvalidOperationException("Processors is null"); - var processor = processors["hotreload"] as ClientHotReloadProcessor ?? throw new InvalidOperationException("HotReloadProcessor is null"); - var hotReloadReady = typeof(ClientHotReloadProcessor).GetProperty("HotReloadWorkspaceLoaded", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(processor) as Task ?? throw new IOException("HotReloadWorkspaceLoaded is null"); - - timeout = Task.Delay(WorkspaceTimeout, ct); - if (await Task.WhenAny(timeout, hotReloadReady) == timeout) + catch { - throw new TimeoutException( - "Timeout while waiting for hot reload workspace to be loaded. " - + "This usually indicates that the dev-server failed to load the solution, you will find more info in dev-server logs " - + "(output in main app logs with [DEV_SERVER] prefix for runtime-tests run in secondary app instance)."); + await revertMessage.DisposeAsync(); + + throw; } - _log.LogTrace("Workspace is ready on dev-server, sending file update request ..."); - - var revertMessage = new RevertFileUpdate(RemoteControlClient.Instance, message, waitForMetadataUpdate); - await SendMessageCore(message, revertMessage, waitForMetadataUpdate, ct); - return revertMessage; } - private static async Task SendMessageCore(UpdateFile message, RevertFileUpdate? revertMessage, bool waitForMetadataUpdate, CancellationToken ct) + /// + /// Creates a file update request to replace text in the source file of the given . + /// + /// An instance of the for which the source code should be edited. + /// The original text to replace. + /// The replacement text. + /// A file update request that can be sent to the dev-server. + public static FileEdit CreateFileEdit( + this FrameworkElement element, + string originalText, + string replacementText) + => new(element.GetDebugParseContext().FileName, originalText, replacementText); + + private static async Task SendMessageCore(FileEdit message, bool waitForMetadataUpdate, CancellationToken ct) { if (waitForMetadataUpdate) { - var cts = new TaskCompletionSource(); - await using var _ = ct.Register(() => cts.TrySetCanceled()); - void UpdateReceived(object? sender, object? args) => cts.TrySetResult(); - - await RemoteControlClient.Instance!.SendMessage(message); - - _log.LogTrace("File update message sent to the dev-server, waiting for metadata update (i.e. the code changes to be applied in the current app) ..."); + var cts = new TaskCompletionSource(); + using var ctReg = ct.Register(() => cts.TrySetCanceled()); + void UpdateReceived(object? sender, object? args) => cts.TrySetResult(default); try { - MetadataUpdaterHelper.MetadataUpdated += UpdateReceived; + MetadataUpdateHandler.MetadataUpdated += UpdateReceived; + //MetadataUpdaterHelper.MetadataUpdated += UpdateReceived; + + var reconnect = await _impl.Apply(message, ct); + if (reconnect is not null) + { + _log.LogTrace("Failed to request file edition, ignore metadata update (i.e. the code changes to be applied in the current app) waiting ..."); - var timeout = Task.Delay(MetadataUpdateTimeout, ct); //TestHelper.DefaultTimeout * 45, ct); + return reconnect; + } + + _log.LogTrace("File edition requested, waiting for metadata update (i.e. the code changes to be applied in the current app) ..."); + + var timeout = Task.Delay(MetadataUpdateTimeout, ct); if (await Task.WhenAny(timeout, cts.Task) == timeout) { throw new TimeoutException( @@ -216,85 +247,28 @@ private static async Task SendMessageCore(UpdateFile message, RevertFileUpdate? + "(output in main app logs with [DEV_SERVER] prefix for runtime-tests run in secondary app instance)."); } } - catch when (revertMessage is not null) - { - await revertMessage.DisposeAsync(); - - throw; - } finally { - MetadataUpdaterHelper.MetadataUpdated -= UpdateReceived; + //MetadataUpdaterHelper.MetadataUpdated -= UpdateReceived; + MetadataUpdateHandler.MetadataUpdated -= UpdateReceived; } + await Task.Delay(100, ct); // Let the metadata to be updated by all handlers. + _log.LogTrace("Received **a** metadata update, continuing test."); + return null; } else { - await RemoteControlClient.Instance!.SendMessage(message); + var result = await _impl.Apply(message, ct); - _log.LogTrace("File update message sent to the dev-server, continuing test without waiting for metadata update."); - } - } + _log.LogTrace("File edition requested, continuing test without waiting for metadata update."); - private record RevertFileUpdate(RemoteControlClient DevServer, UpdateFile Message, bool WaitForMetadataUpdate) : IAsyncDisposable - { - /// - public async ValueTask DisposeAsync() - { - var revertMessage = new UpdateFile - { - FilePath = Message.FilePath, - NewText = Message.OldText, - OldText = Message.NewText - }; - - try - { - _log.LogInformation($"Reverting changes made on {Message.FilePath} (from: \"{StartEnd(revertMessage.OldText)}\" to \"{StartEnd(revertMessage.NewText)}\")."); - - // Note: We also wait for metadata update here to ensure that the file is reverted before the test continues / we run another test. - await SendMessageCore(revertMessage, null, WaitForMetadataUpdate, CancellationToken.None); - } - catch (WebSocketException) - { - _log.LogWarning($"Failed to revert file update, connection to the dev-server might have been closed *** WAITING {DevServer.ConnectionRetryInterval:g} to let client reconnect before retry**."); - - // Wait for the client to attempt re-connection - await Task.Delay(DevServer.ConnectionRetryInterval + TimeSpan.FromMilliseconds(100)); - - try - { - // Note: We also wait for metadata update here to ensure that the file is reverted before the test continues / we run another test. - await SendMessageCore(revertMessage, null, WaitForMetadataUpdate, CancellationToken.None); - } - catch (WebSocketException) - { - _log.LogError($"Failed to revert file update, connection to the dev-server might have been closed. Cannot revert changes made on file {Message.FilePath}."); - } - } + return result; } } -#endif - - /// - /// Creates a file update request to replace text in the source file of the given . - /// - /// An instance of the for which the source code should be edited. - /// The original text to replace. - /// The replacement text. - /// A file update request that can be sent to the dev-server. - public static UpdateFile CreateUpdateFileMessage( - this FrameworkElement element, - string originalText, - string replacementText) - => new() - { - FilePath = element.GetDebugParseContext().FileName, - OldText = originalText, - NewText = replacementText - }; + #region Helpers private static (string FileName, int FileLine, int LinePosition) GetDebugParseContext(this FrameworkElement element) { var dpcProp = typeof(FrameworkElement).GetProperty("DebugParseContext", BindingFlags.Instance | BindingFlags.NonPublic); @@ -342,5 +316,6 @@ private static (string FileName, int FileLine, int LinePosition) GetDebugParseCo private static string StartEnd(string str, uint chars = 10) => str.Length <= chars ? str : $"{str[..(int)chars]}...{str[^(int)chars..]}"; + #endregion } #endif \ No newline at end of file diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Library/_Compat/CallerArgumentExpressionAttribute.cs b/src/Uno.UI.RuntimeTests.Engine.Library/Library/_Compat/CallerArgumentExpressionAttribute.cs new file mode 100644 index 0000000..6f97d2b --- /dev/null +++ b/src/Uno.UI.RuntimeTests.Engine.Library/Library/_Compat/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,25 @@ +#if !UNO_RUNTIMETESTS_DISABLE_LIBRARY && WINDOWS_UWP +#if !IS_UNO_RUNTIMETEST_PROJECT +#pragma warning disable +#endif + +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Runtime.CompilerServices +{ + /// Allows capturing of the expressions passed to a method. + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + public sealed class CallerArgumentExpressionAttribute : Attribute + { + /// Initializes a new instance of the class. + /// The name of the targeted parameter. + public CallerArgumentExpressionAttribute(string parameterName) => this.ParameterName = parameterName; + + /// Gets the target parameter name of the CallerArgumentExpression. + /// The name of the targeted parameter of the CallerArgumentExpression. + public string ParameterName { get; } + } +} +#endif diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Uno.UI.RuntimeTests.Engine.Library.projitems b/src/Uno.UI.RuntimeTests.Engine.Library/Uno.UI.RuntimeTests.Engine.Library.projitems index 89a0e8d..c445508 100644 --- a/src/Uno.UI.RuntimeTests.Engine.Library/Uno.UI.RuntimeTests.Engine.Library.projitems +++ b/src/Uno.UI.RuntimeTests.Engine.Library/Uno.UI.RuntimeTests.Engine.Library.projitems @@ -29,7 +29,11 @@ - + + + + +