diff --git a/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs b/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs index 60437f5a7741..34e11d46f42c 100644 --- a/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs +++ b/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs @@ -59,7 +59,7 @@ public static void Initialize() agent.Reporter.Report("Writing capabilities: " + agent.Capabilities, AgentMessageSeverity.Verbose); var initPayload = new ClientInitializationPayload(agent.Capabilities); - initPayload.Write(pipeClient); + await initPayload.WriteAsync(pipeClient, CancellationToken.None); while (pipeClient.IsConnected) { @@ -71,8 +71,8 @@ public static void Initialize() var logEntries = agent.GetAndClearLogEntries(update.ResponseLoggingLevel); // response: - pipeClient.WriteByte(UpdatePayload.ApplySuccessValue); - UpdatePayload.WriteLog(pipeClient, logEntries); + await pipeClient.WriteAsync((byte)UpdatePayload.ApplySuccessValue, CancellationToken.None); + await UpdatePayload.WriteLogAsync(pipeClient, logEntries, CancellationToken.None); } } catch (Exception ex) diff --git a/src/BuiltInTools/HotReloadAgent/AgentMessageSeverity.cs b/src/BuiltInTools/HotReloadAgent/AgentMessageSeverity.cs index d6c23929b7b4..3ab84cecde97 100644 --- a/src/BuiltInTools/HotReloadAgent/AgentMessageSeverity.cs +++ b/src/BuiltInTools/HotReloadAgent/AgentMessageSeverity.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +namespace Microsoft.DotNet.HotReload; internal enum AgentMessageSeverity : byte { diff --git a/src/BuiltInTools/HotReloadAgent/Microsoft.DotNet.HotReload.Agent.Package.csproj b/src/BuiltInTools/HotReloadAgent/Microsoft.DotNet.HotReload.Agent.Package.csproj index 75b7a922b2e4..9daff92e3b72 100644 --- a/src/BuiltInTools/HotReloadAgent/Microsoft.DotNet.HotReload.Agent.Package.csproj +++ b/src/BuiltInTools/HotReloadAgent/Microsoft.DotNet.HotReload.Agent.Package.csproj @@ -1,7 +1,10 @@  - - $(VisualStudioServiceTargetFramework) + + netstandard2.1 false none false @@ -19,9 +22,6 @@ $(NoWarn);NU5128 - - - diff --git a/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs b/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs index ef22b5f8ffc5..35d0828ad56a 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs @@ -119,7 +119,10 @@ await browserRefreshServer.SendAndReceiveAsync( anyFailure = true; } - ReportLog(reporter, data.Log.Select(entry => (entry.Message, (AgentMessageSeverity)entry.Severity))); + foreach (var entry in data.Log) + { + ReportLogEntry(reporter, entry.Message, (AgentMessageSeverity)entry.Severity); + } }, cancellationToken); diff --git a/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs b/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs index 2fade347530f..a3228802a53e 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs @@ -36,9 +36,9 @@ async Task> ConnectAsync() // When the client connects, the first payload it sends is the initialization payload which includes the apply capabilities. - var capabilities = ClientInitializationPayload.Read(_pipe).Capabilities; + var capabilities = (await ClientInitializationPayload.ReadAsync(_pipe, cancellationToken)).Capabilities; Reporter.Verbose($"Capabilities: '{capabilities}'"); - return capabilities.Split(' ').ToImmutableArray(); + return [.. capabilities.Split(' ')]; } catch (EndOfStreamException) { @@ -149,7 +149,11 @@ private async Task ReceiveApplyUpdateResult(CancellationToken cancellation return false; } - ReportLog(Reporter, UpdatePayload.ReadLog(_pipe)); + await foreach (var (message, severity) in UpdatePayload.ReadLogAsync(_pipe, cancellationToken)) + { + ReportLogEntry(Reporter, message, severity); + } + return true; } finally diff --git a/src/BuiltInTools/dotnet-watch/HotReload/DeltaApplier.cs b/src/BuiltInTools/dotnet-watch/HotReload/DeltaApplier.cs index 897dc710d841..78f6a02d01a6 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/DeltaApplier.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/DeltaApplier.cs @@ -28,24 +28,21 @@ internal abstract class DeltaApplier(IReporter reporter) : IDisposable public abstract void Dispose(); - public static void ReportLog(IReporter reporter, IEnumerable<(string message, AgentMessageSeverity severity)> log) + public static void ReportLogEntry(IReporter reporter, string message, AgentMessageSeverity severity) { - foreach (var (message, severity) in log) + switch (severity) { - switch (severity) - { - case AgentMessageSeverity.Error: - reporter.Error(message); - break; - - case AgentMessageSeverity.Warning: - reporter.Warn(message, emoji: "⚠"); - break; - - default: - reporter.Verbose(message, emoji: "🕵️"); - break; - } + case AgentMessageSeverity.Error: + reporter.Error(message); + break; + + case AgentMessageSeverity.Warning: + reporter.Warn(message, emoji: "⚠"); + break; + + default: + reporter.Verbose(message, emoji: "🕵️"); + break; } } } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs b/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs index d241241cc7de..d95c3e8e5128 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs @@ -1,15 +1,25 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; using Microsoft.DotNet.HotReload; namespace Microsoft.DotNet.Watch { + internal enum PayloadType + { + ManagedCodeUpdate = 1, + StaticAssetUpdate = 2, + InitialUpdatesCompleted = 3, + } + internal readonly struct UpdatePayload(IReadOnlyList deltas, ResponseLoggingLevel responseLoggingLevel) { public const byte ApplySuccessValue = 0; - private const byte Version = 2; + private const byte Version = 4; public IReadOnlyList Deltas { get; } = deltas; public ResponseLoggingLevel ResponseLoggingLevel { get; } = responseLoggingLevel; @@ -19,58 +29,32 @@ internal readonly struct UpdatePayload(IReadOnlyList deltas, Respon /// public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken) { - await using var binaryWriter = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true); - binaryWriter.Write(Version); - binaryWriter.Write(Deltas.Count); + await stream.WriteAsync(Version, cancellationToken); + await stream.WriteAsync(Deltas.Count, cancellationToken); - for (var i = 0; i < Deltas.Count; i++) + foreach (var delta in Deltas) { - var delta = Deltas[i]; - binaryWriter.Write(delta.ModuleId.ToString()); - await WriteBytesAsync(binaryWriter, delta.MetadataDelta, cancellationToken); - await WriteBytesAsync(binaryWriter, delta.ILDelta, cancellationToken); - await WriteBytesAsync(binaryWriter, delta.PdbDelta, cancellationToken); - WriteIntArray(binaryWriter, delta.UpdatedTypes); + await stream.WriteAsync(delta.ModuleId, cancellationToken); + await stream.WriteByteArrayAsync(delta.MetadataDelta, cancellationToken); + await stream.WriteByteArrayAsync(delta.ILDelta, cancellationToken); + await stream.WriteByteArrayAsync(delta.PdbDelta, cancellationToken); + await stream.WriteAsync(delta.UpdatedTypes, cancellationToken); } - binaryWriter.Write((byte)ResponseLoggingLevel); - - static ValueTask WriteBytesAsync(BinaryWriter binaryWriter, byte[] bytes, CancellationToken cancellationToken) - { - binaryWriter.Write(bytes.Length); - binaryWriter.Flush(); - return binaryWriter.BaseStream.WriteAsync(bytes, cancellationToken); - } - - static void WriteIntArray(BinaryWriter binaryWriter, int[] values) - { - if (values is null) - { - binaryWriter.Write(0); - return; - } - - binaryWriter.Write(values.Length); - foreach (var value in values) - { - binaryWriter.Write(value); - } - } + await stream.WriteAsync((byte)ResponseLoggingLevel, cancellationToken); } /// /// Called by the dotnet-watch. /// - public static void WriteLog(Stream stream, IReadOnlyCollection<(string message, AgentMessageSeverity severity)> log) + public static async ValueTask WriteLogAsync(Stream stream, IReadOnlyCollection<(string message, AgentMessageSeverity severity)> log, CancellationToken cancellationToken) { - using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true); - - writer.Write(log.Count); + await stream.WriteAsync(log.Count, cancellationToken); foreach (var (message, severity) in log) { - writer.Write(message); - writer.Write((byte)severity); + await stream.WriteAsync(message, cancellationToken); + await stream.WriteAsync((byte)severity, cancellationToken); } } @@ -79,117 +63,369 @@ public static void WriteLog(Stream stream, IReadOnlyCollection<(string message, /// public static async ValueTask ReadAsync(Stream stream, CancellationToken cancellationToken) { - using var binaryReader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); - var version = binaryReader.ReadByte(); + var version = await stream.ReadByteAsync(cancellationToken); if (version != Version) { throw new NotSupportedException($"Unsupported version {version}."); } - var count = binaryReader.ReadInt32(); + var count = await stream.ReadInt32Async(cancellationToken); var deltas = new UpdateDelta[count]; for (var i = 0; i < count; i++) { - var moduleId = Guid.Parse(binaryReader.ReadString()); - var metadataDelta = await ReadBytesAsync(binaryReader, cancellationToken); - var ilDelta = await ReadBytesAsync(binaryReader, cancellationToken); - var pdbDelta = await ReadBytesAsync(binaryReader, cancellationToken); - var updatedTypes = ReadIntArray(binaryReader); + var moduleId = await stream.ReadGuidAsync(cancellationToken); + var metadataDelta = await stream.ReadByteArrayAsync(cancellationToken); + var ilDelta = await stream.ReadByteArrayAsync(cancellationToken); + var pdbDelta = await stream.ReadByteArrayAsync(cancellationToken); + var updatedTypes = await stream.ReadIntArrayAsync(cancellationToken); deltas[i] = new UpdateDelta(moduleId, metadataDelta: metadataDelta, ilDelta: ilDelta, pdbDelta: pdbDelta, updatedTypes); } - var responseLoggingLevel = (ResponseLoggingLevel)binaryReader.ReadByte(); - + var responseLoggingLevel = (ResponseLoggingLevel)await stream.ReadByteAsync(cancellationToken); return new UpdatePayload(deltas, responseLoggingLevel: responseLoggingLevel); + } + + /// + /// Called by delta applier. + /// + public static async IAsyncEnumerable<(string message, AgentMessageSeverity severity)> ReadLogAsync(Stream stream, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var entryCount = await stream.ReadInt32Async(cancellationToken); - static async ValueTask ReadBytesAsync(BinaryReader binaryReader, CancellationToken cancellationToken) + for (var i = 0; i < entryCount; i++) { - var numBytes = binaryReader.ReadInt32(); + var message = await stream.ReadStringAsync(cancellationToken); + var severity = (AgentMessageSeverity)await stream.ReadByteAsync(cancellationToken); + yield return (message, severity); + } + } + } - var bytes = new byte[numBytes]; + internal readonly struct ClientInitializationPayload(string capabilities) + { + private const byte Version = 0; - var read = 0; - while (read < numBytes) - { - read += await binaryReader.BaseStream.ReadAsync(bytes.AsMemory(read), cancellationToken); - } + public string Capabilities { get; } = capabilities; + + /// + /// Called by delta applier. + /// + public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken) + { + await stream.WriteAsync(Version, cancellationToken); + await stream.WriteAsync(Capabilities, cancellationToken); + } + + /// + /// Called by dotnet-watch. + /// + public static async ValueTask ReadAsync(Stream stream, CancellationToken cancellationToken) + { + var version = await stream.ReadByteAsync(cancellationToken); + if (version != Version) + { + throw new NotSupportedException($"Unsupported version {version}."); + } + + var capabilities = await stream.ReadStringAsync(cancellationToken); + return new ClientInitializationPayload(capabilities); + } + } + + internal readonly struct StaticAssetPayload( + string assemblyName, + string relativePath, + byte[] contents, + bool isApplicationProject) + { + private const byte Version = 1; + + public string AssemblyName { get; } = assemblyName; + public bool IsApplicationProject { get; } = isApplicationProject; + public string RelativePath { get; } = relativePath; + public byte[] Contents { get; } = contents; + + public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationToken) + { + await stream.WriteAsync(Version, cancellationToken); + await stream.WriteAsync(AssemblyName, cancellationToken); + await stream.WriteAsync(IsApplicationProject, cancellationToken); + await stream.WriteAsync(RelativePath, cancellationToken); + await stream.WriteByteArrayAsync(Contents, cancellationToken); + } - return bytes; + public static async ValueTask ReadAsync(Stream stream, CancellationToken cancellationToken) + { + var version = await stream.ReadByteAsync(cancellationToken); + if (version != Version) + { + throw new NotSupportedException($"Unsupported version {version}."); } - static int[] ReadIntArray(BinaryReader binaryReader) + var assemblyName = await stream.ReadStringAsync(cancellationToken); + var isAppProject = await stream.ReadBooleanAsync(cancellationToken); + var relativePath = await stream.ReadStringAsync(cancellationToken); + var contents = await stream.ReadByteArrayAsync(cancellationToken); + + return new StaticAssetPayload( + assemblyName: assemblyName, + relativePath: relativePath, + contents: contents, + isApplicationProject: isAppProject); + } + } + + /// + /// Implements async read/write helpers that provide functionality of and . + /// See https://github.com/dotnet/runtime/issues/17229 + /// + internal static class StreamExtesions + { + public static ValueTask WriteAsync(this Stream stream, bool value, CancellationToken cancellationToken) + => WriteAsync(stream, (byte)(value ? 1 : 0), cancellationToken); + + public static async ValueTask WriteAsync(this Stream stream, byte value, CancellationToken cancellationToken) + { + var size = sizeof(byte); + var buffer = ArrayPool.Shared.Rent(minimumLength: size); + try { - var numValues = binaryReader.ReadInt32(); - if (numValues == 0) - { - return Array.Empty(); - } + buffer[0] = value; + await stream.WriteAsync(buffer, offset: 0, count: size, cancellationToken); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } - var values = new int[numValues]; + public static async ValueTask WriteAsync(this Stream stream, int value, CancellationToken cancellationToken) + { + var size = sizeof(int); + var buffer = ArrayPool.Shared.Rent(minimumLength: size); + try + { + BinaryPrimitives.WriteInt32LittleEndian(buffer, value); + await stream.WriteAsync(buffer, offset: 0, count: size, cancellationToken); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public static ValueTask WriteAsync(this Stream stream, Guid value, CancellationToken cancellationToken) + => stream.WriteAsync(value.ToByteArray(), cancellationToken); - for (var i = 0; i < numValues; i++) + public static async ValueTask WriteByteArrayAsync(this Stream stream, byte[] value, CancellationToken cancellationToken) + { + await stream.WriteAsync(value.Length, cancellationToken); + await stream.WriteAsync(value, cancellationToken); + } + + public static async ValueTask WriteAsync(this Stream stream, int[] value, CancellationToken cancellationToken) + { + var size = sizeof(int) * (value.Length + 1); + var buffer = ArrayPool.Shared.Rent(minimumLength: size); + try + { + BinaryPrimitives.WriteInt32LittleEndian(buffer, value.Length); + for (int i = 0; i < value.Length; i++) { - values[i] = binaryReader.ReadInt32(); + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan((i + 1) * sizeof(int), sizeof(int)), value[i]); } - return values; + await stream.WriteAsync(buffer, offset: 0, count: size, cancellationToken); + } + finally + { + ArrayPool.Shared.Return(buffer); } } - /// - /// Called by delta applier. - /// - public static IEnumerable<(string message, AgentMessageSeverity severity)> ReadLog(Stream stream) + public static async ValueTask WriteAsync(this Stream stream, string value, CancellationToken cancellationToken) { - using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); + var bytes = Encoding.UTF8.GetBytes(value); + await stream.Write7BitEncodedIntAsync(bytes.Length, cancellationToken); + await stream.WriteAsync(bytes, cancellationToken); + } - var entryCount = reader.ReadInt32(); + public static async ValueTask Write7BitEncodedIntAsync(this Stream stream, int value, CancellationToken cancellationToken) + { + uint uValue = (uint)value; - for (var i = 0; i < entryCount; i++) + while (uValue > 0x7Fu) { - yield return (reader.ReadString(), (AgentMessageSeverity)reader.ReadByte()); + await stream.WriteAsync((byte)(uValue | ~0x7Fu), cancellationToken); + uValue >>= 7; } + + await stream.WriteAsync((byte)uValue, cancellationToken); } - } - internal readonly struct ClientInitializationPayload - { - private const byte Version = 0; + public static async ValueTask ReadBooleanAsync(this Stream stream, CancellationToken cancellationToken) + => await stream.ReadByteAsync(cancellationToken) != 0; - public string Capabilities { get; } + public static async ValueTask ReadByteAsync(this Stream stream, CancellationToken cancellationToken) + { + int size = sizeof(byte); + var buffer = ArrayPool.Shared.Rent(minimumLength: size); + try + { + await ReadExactlyAsync(stream, buffer.AsMemory(0, size), cancellationToken); + return buffer[0]; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } - public ClientInitializationPayload(string capabilities) + public static async ValueTask ReadInt32Async(this Stream stream, CancellationToken cancellationToken) { - Capabilities = capabilities; + int size = sizeof(int); + var buffer = ArrayPool.Shared.Rent(minimumLength: size); + try + { + await ReadExactlyAsync(stream, buffer.AsMemory(0, size), cancellationToken); + return BinaryPrimitives.ReadInt32LittleEndian(buffer); + } + finally + { + ArrayPool.Shared.Return(buffer); + } } - /// - /// Called by delta applier. - /// - public void Write(Stream stream) + public static async ValueTask ReadGuidAsync(this Stream stream, CancellationToken cancellationToken) { - using var binaryWriter = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true); - binaryWriter.Write(Version); - binaryWriter.Write(Capabilities); - binaryWriter.Flush(); + const int size = 16; + var buffer = ArrayPool.Shared.Rent(minimumLength: size); + try + { + await ReadExactlyAsync(stream, buffer.AsMemory(0, size), cancellationToken); + return new Guid(buffer.AsSpan(0, size)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } } - /// - /// Called by dotnet-watch. - /// - public static ClientInitializationPayload Read(Stream stream) + public static async ValueTask ReadByteArrayAsync(this Stream stream, CancellationToken cancellationToken) { - using var binaryReader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); - var version = binaryReader.ReadByte(); - if (version != Version) + var count = await stream.ReadInt32Async(cancellationToken); + if (count == 0) { - throw new NotSupportedException($"Unsupported version {version}."); + return []; } - var capabilities = binaryReader.ReadString(); - return new ClientInitializationPayload(capabilities); + var bytes = new byte[count]; + await ReadExactlyAsync(stream, bytes, cancellationToken); + return bytes; + } + + public static async ValueTask ReadIntArrayAsync(this Stream stream, CancellationToken cancellationToken) + { + var count = await stream.ReadInt32Async(cancellationToken); + if (count == 0) + { + return []; + } + + var result = new int[count]; + int size = count * sizeof(int); + var buffer = ArrayPool.Shared.Rent(minimumLength: size); + try + { + await ReadExactlyAsync(stream, buffer.AsMemory(0, size), cancellationToken); + + for (var i = 0; i < count; i++) + { + result[i] = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(i * sizeof(int))); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + return result; + } + + public static async ValueTask ReadStringAsync(this Stream stream, CancellationToken cancellationToken) + { + int size = await stream.Read7BitEncodedIntAsync(cancellationToken); + if (size < 0) + { + throw new InvalidDataException(); + } + + if (size == 0) + { + return string.Empty; + } + + var buffer = ArrayPool.Shared.Rent(minimumLength: size); + try + { + await ReadExactlyAsync(stream, buffer.AsMemory(0, size), cancellationToken); + return Encoding.UTF8.GetString(buffer.AsSpan(0, size)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public static async ValueTask Read7BitEncodedIntAsync(this Stream stream, CancellationToken cancellationToken) + { + const int MaxBytesWithoutOverflow = 4; + + uint result = 0; + byte b; + + for (int shift = 0; shift < MaxBytesWithoutOverflow * 7; shift += 7) + { + b = await stream.ReadByteAsync(cancellationToken); + result |= (b & 0x7Fu) << shift; + + if (b <= 0x7Fu) + { + return (int)result; + } + } + + // Read the 5th byte. Since we already read 28 bits, + // the value of this byte must fit within 4 bits (32 - 28), + // and it must not have the high bit set. + + b = await stream.ReadByteAsync(cancellationToken); + if (b > 0b_1111u) + { + throw new InvalidDataException(); + } + + result |= (uint)b << (MaxBytesWithoutOverflow * 7); + return (int)result; + } + + private static async ValueTask ReadExactlyAsync(this Stream stream, Memory buffer, CancellationToken cancellationToken) + { + int totalRead = 0; + while (totalRead < buffer.Length) + { + int read = await stream.ReadAsync(buffer.Slice(totalRead), cancellationToken).ConfigureAwait(false); + if (read == 0) + { + throw new EndOfStreamException(); + } + + totalRead += read; + } + + return totalRead; } } } diff --git a/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/StaticAssetPayloadTests.cs b/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/StaticAssetPayloadTests.cs new file mode 100644 index 000000000000..1240d15b7fb0 --- /dev/null +++ b/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/StaticAssetPayloadTests.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.HotReload; + +namespace Microsoft.DotNet.Watch.UnitTests; + +public class StaticAssetPayloadTests +{ + [Fact] + public async Task Roundtrip() + { + var initial = new StaticAssetPayload( + assemblyName: "assembly name", + relativePath: "some path", + [1, 2, 3], + isApplicationProject: true); + + using var stream = new MemoryStream(); + await initial.WriteAsync(stream, CancellationToken.None); + + stream.Position = 0; + var read = await StaticAssetPayload.ReadAsync(stream, CancellationToken.None); + + AssertEqual(initial, read); + } + + private static void AssertEqual(StaticAssetPayload initial, StaticAssetPayload read) + { + Assert.Equal(initial.AssemblyName, read.AssemblyName); + Assert.Equal(initial.RelativePath, read.RelativePath); + Assert.Equal(initial.IsApplicationProject, read.IsApplicationProject); + Assert.Equal(initial.Contents, read.Contents); + } +} diff --git a/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/StreamExtensionsTests.cs b/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/StreamExtensionsTests.cs new file mode 100644 index 000000000000..2da1c3b4252d --- /dev/null +++ b/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/StreamExtensionsTests.cs @@ -0,0 +1,177 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Watch.UnitTests; + +public class StreamExtensionsTests +{ + private static async Task TestAsync( + T expected, + Action syncWrite, + Func syncRead, + Func asyncWrite, + Func> asyncRead, + bool useBinaryReader, + bool useBinaryWriter) + { + var stream = new MemoryStream(); + + if (useBinaryWriter) + { + using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true); + syncWrite(writer, expected); + } + else + { + await asyncWrite(stream, expected, CancellationToken.None); + } + + var bytesWritten = stream.Position; + stream.Position = 0; + + T actual; + if (useBinaryReader) + { + using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); + actual = syncRead(reader); + } + else + { + actual = await asyncRead(stream, CancellationToken.None); + } + + Assert.Equal(expected, actual); + Assert.Equal(bytesWritten, stream.Position); + } + + [Theory] + [CombinatorialData] + public async Task ReadWrite_String( + [CombinatorialValues("", "\u1234", "hello")] string expected, + bool useBinaryWriter, + bool useBinaryReader) + { + await TestAsync( + expected, + (w, v) => w.Write(v), + r => r.ReadString(), + StreamExtesions.WriteAsync, + StreamExtesions.ReadStringAsync, + useBinaryWriter, + useBinaryReader); + } + + [Theory] + [CombinatorialData] + public async Task ReadWrite_7BitEncodedInt( + [CombinatorialValues(-1, -127, -128, -255, -256, int.MinValue, 0, 1, 10, 127, 128, 255, 256, int.MaxValue)] int expected, + bool useBinaryWriter, + bool useBinaryReader) + { + await TestAsync( + expected, + (w, v) => w.Write7BitEncodedInt(v), + r => r.Read7BitEncodedInt(), + StreamExtesions.Write7BitEncodedIntAsync, + StreamExtesions.Read7BitEncodedIntAsync, + useBinaryWriter, + useBinaryReader); + } + + [Theory] + [CombinatorialData] + public async Task ReadWrite_Byte( + [CombinatorialValues(0, 255)] byte expected, + bool useBinaryWriter, + bool useBinaryReader) + { + await TestAsync( + expected, + (w, v) => w.Write(v), + r => r.ReadByte(), + StreamExtesions.WriteAsync, + StreamExtesions.ReadByteAsync, + useBinaryWriter, + useBinaryReader); + } + + [Theory] + [CombinatorialData] + public async Task ReadWrite_Int32( + [CombinatorialValues(int.MinValue, 0, int.MaxValue)] int expected, + bool useBinaryWriter, + bool useBinaryReader) + { + await TestAsync( + expected, + (w, v) => w.Write(v), + r => r.ReadInt32(), + StreamExtesions.WriteAsync, + StreamExtesions.ReadInt32Async, + useBinaryWriter, + useBinaryReader); + } + + [Theory] + [CombinatorialData] + public async Task ReadWrite_Bool( + bool expected, + bool useBinaryWriter, + bool useBinaryReader) + { + await TestAsync( + expected, + (w, v) => w.Write(v), + r => r.ReadBoolean(), + StreamExtesions.WriteAsync, + StreamExtesions.ReadBooleanAsync, + useBinaryWriter, + useBinaryReader); + } + + [Theory] + [CombinatorialData] + public async Task ReadWrite_Int32Array( + [CombinatorialValues(0, 1, 1234)] int length) + { + var expected = Enumerable.Range(0, length).ToArray(); + + var stream = new MemoryStream(); + + await stream.WriteAsync(expected, CancellationToken.None); + stream.Position = 0; + + var actual = await stream.ReadIntArrayAsync(CancellationToken.None); + Assert.Equal(expected, actual); + } + + [Theory] + [CombinatorialData] + public async Task ReadWrite_ByteArray( + [CombinatorialValues(0, 1, 1234)] int length) + { + var expected = Enumerable.Range(0, length).Select(i => (byte)i).ToArray(); + + var stream = new MemoryStream(); + + await stream.WriteByteArrayAsync(expected, CancellationToken.None); + stream.Position = 0; + + var actual = await stream.ReadByteArrayAsync(CancellationToken.None); + Assert.Equal(expected, actual); + } + + [Fact] + public async Task ReadWrite_Guid() + { + var expected = Guid.NewGuid(); + + var stream = new MemoryStream(); + + await stream.WriteAsync(expected, CancellationToken.None); + stream.Position = 0; + + var actual = await stream.ReadGuidAsync(CancellationToken.None); + Assert.Equal(expected, actual); + } +} diff --git a/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/UpdatePayloadTests.cs b/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/UpdatePayloadTests.cs new file mode 100644 index 000000000000..79b2240033c9 --- /dev/null +++ b/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/UpdatePayloadTests.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.HotReload; + +namespace Microsoft.DotNet.Watch.UnitTests; + +public class UpdatePayloadTests +{ + [Fact] + public async Task Roundtrip() + { + var initial = new UpdatePayload( + [ + new UpdateDelta( + moduleId: Guid.NewGuid(), + ilDelta: [0, 0, 1], + metadataDelta: [0, 1, 1], + pdbDelta: [], + updatedTypes: [60, 74, 22323]), + new UpdateDelta( + moduleId: Guid.NewGuid(), + ilDelta: [1, 0, 0], + metadataDelta: [1, 0, 1], + pdbDelta: [], + updatedTypes: [-18]) + ], + responseLoggingLevel: ResponseLoggingLevel.WarningsAndErrors); + + using var stream = new MemoryStream(); + await initial.WriteAsync(stream, CancellationToken.None); + + stream.Position = 0; + var read = await UpdatePayload.ReadAsync(stream, CancellationToken.None); + + AssertEqual(initial, read); + } + + [Fact] + public async Task WithLargeDeltas() + { + var initial = new UpdatePayload( + [ + new UpdateDelta( + moduleId: Guid.NewGuid(), + ilDelta: Enumerable.Range(0, 68200).Select(c => (byte)(c % 2)).ToArray(), + metadataDelta: [0, 1, 1], + pdbDelta: [], + updatedTypes: Array.Empty()) + ], + responseLoggingLevel: ResponseLoggingLevel.Verbose); + + using var stream = new MemoryStream(); + await initial.WriteAsync(stream, CancellationToken.None); + + stream.Position = 0; + var read = await UpdatePayload.ReadAsync(stream, CancellationToken.None); + + AssertEqual(initial, read); + } + + private static void AssertEqual(UpdatePayload initial, UpdatePayload read) + { + Assert.Equal(initial.Deltas.Count, read.Deltas.Count); + + for (var i = 0; i < initial.Deltas.Count; i++) + { + var e = initial.Deltas[i]; + var a = read.Deltas[i]; + + Assert.Equal(e.ModuleId, a.ModuleId); + Assert.Equal(e.ILDelta, a.ILDelta); + Assert.Equal(e.MetadataDelta, a.MetadataDelta); + Assert.Equal(e.UpdatedTypes, a.UpdatedTypes); + } + + Assert.Equal(initial.ResponseLoggingLevel, read.ResponseLoggingLevel); + } +} diff --git a/test/dotnet-watch.Tests/HotReload/UpdatePayloadTests.cs b/test/dotnet-watch.Tests/HotReload/UpdatePayloadTests.cs deleted file mode 100644 index 78c5fc317867..000000000000 --- a/test/dotnet-watch.Tests/HotReload/UpdatePayloadTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.DotNet.HotReload; - -namespace Microsoft.DotNet.Watch.UnitTests -{ - public class UpdatePayloadTests - { - [Fact] - public async Task UpdatePayload_CanRoundTrip() - { - var initial = new UpdatePayload( - [ - new UpdateDelta( - moduleId: Guid.NewGuid(), - ilDelta: [0, 0, 1], - metadataDelta: [0, 1, 1], - pdbDelta: [2, 3], - updatedTypes: [5, 4]), - new UpdateDelta( - moduleId: Guid.NewGuid(), - ilDelta: [1, 0, 0], - metadataDelta: [1, 0, 1], - pdbDelta: [7, 8], - updatedTypes: [9]) - ], - responseLoggingLevel: ResponseLoggingLevel.Verbose); - - using var stream = new MemoryStream(); - await initial.WriteAsync(stream, default); - - stream.Position = 0; - var read = await UpdatePayload.ReadAsync(stream, default); - - AssertEqual(initial, read); - } - - [Fact] - public async Task UpdatePayload_CanRoundTripUpdatedTypes() - { - var initial = new UpdatePayload( - [ - new UpdateDelta( - moduleId: Guid.NewGuid(), - ilDelta: [0, 0, 1], - metadataDelta: [0, 1, 1], - pdbDelta: [], - updatedTypes: [60, 74, 22323]), - new UpdateDelta( - moduleId: Guid.NewGuid(), - ilDelta: [1, 0, 0], - metadataDelta: [1, 0, 1], - pdbDelta: [], - updatedTypes: [-18]) - ], - responseLoggingLevel: ResponseLoggingLevel.WarningsAndErrors); - - using var stream = new MemoryStream(); - await initial.WriteAsync(stream, default); - - stream.Position = 0; - var read = await UpdatePayload.ReadAsync(stream, default); - - AssertEqual(initial, read); - } - - [Fact] - public async Task UpdatePayload_WithLargeDeltas_CanRoundtrip() - { - var initial = new UpdatePayload( - [ - new UpdateDelta( - moduleId: Guid.NewGuid(), - ilDelta: Enumerable.Range(0, 68200).Select(c => (byte)(c % 2)).ToArray(), - metadataDelta: [0, 1, 1], - pdbDelta: [], - updatedTypes: Array.Empty()) - ], - responseLoggingLevel: ResponseLoggingLevel.Verbose); - - using var stream = new MemoryStream(); - await initial.WriteAsync(stream, default); - - stream.Position = 0; - var read = await UpdatePayload.ReadAsync(stream, default); - - AssertEqual(initial, read); - } - - private static void AssertEqual(UpdatePayload initial, UpdatePayload read) - { - Assert.Equal(initial.Deltas.Count, read.Deltas.Count); - - for (var i = 0; i < initial.Deltas.Count; i++) - { - var e = initial.Deltas[i]; - var a = read.Deltas[i]; - - Assert.Equal(e.ModuleId, a.ModuleId); - Assert.Equal(e.ILDelta, a.ILDelta); - Assert.Equal(e.MetadataDelta, a.MetadataDelta); - if (e.UpdatedTypes is null) - { - Assert.Empty(a.UpdatedTypes); - } - else - { - Assert.Equal(e.UpdatedTypes, a.UpdatedTypes); - } - } - - Assert.Equal(initial.ResponseLoggingLevel, read.ResponseLoggingLevel); - } - } -}