// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Rendering; using Microsoft.AspNetCore.Blazor.RenderTree; using Microsoft.AspNetCore.Blazor.Server.Circuits; using System; using System.IO; using System.Text; using Xunit; namespace Microsoft.AspNetCore.Blazor.Server { public class RenderBatchWriterTest { static object NullStringMarker = new object(); [Fact] public void CanSerializeEmptyRenderBatch() { // Arrange/Act var bytes = Serialize(new RenderBatch()); // Assert AssertBinaryContents(bytes, /* startIndex */ 0, 0, // Length of UpdatedComponents 0, // Length of ReferenceFrames 0, // Length of DisposedComponentIds 0, // Length of DisposedEventHandlerIds 0, // Index of UpdatedComponents 4, // Index of ReferenceFrames 8, // Index of DisposedComponentIds 12, // Index of DisposedEventHandlerIds 16 // Index of Strings ); Assert.Equal(36, bytes.Length); // No other data } [Fact] public void CanIncludeDisposedComponentIds() { // Arrange/Act var bytes = Serialize(new RenderBatch( default, default, new ArrayRange(new[] { 123, int.MaxValue, int.MinValue, 456 }, 3), // Only use first 3 to show that param is respected default)); // Assert AssertBinaryContents(bytes, /* startIndex */ 0, 0, // Length of UpdatedComponents 0, // Length of ReferenceFrames 3, 123, int.MaxValue, int.MinValue, // DisposedComponentIds as length-prefixed array 0, // Length of DisposedEventHandlerIds 0, // Index of UpdatedComponents 4, // Index of ReferenceFrames 8, // Index of DisposedComponentIds 24, // Index of DisposedEventHandlerIds 28 // Index of strings ); Assert.Equal(48, bytes.Length); // No other data } [Fact] public void CanIncludeDisposedEventHandlerIds() { // Arrange/Act var bytes = Serialize(new RenderBatch( new ArrayRange(), new ArrayRange(), new ArrayRange(), new ArrayRange(new[] { 123, int.MaxValue, int.MinValue, 456 }, 3) // Only use first 3 to show that param is respected )); // Assert AssertBinaryContents(bytes, /* startIndex */ 0, 0, // Length of UpdatedComponents 0, // Length of ReferenceFrames 0, // Length of DisposedComponentIds 3, 123, int.MaxValue, int.MinValue, // DisposedEventHandlerIds as length-prefixed array 0, // Index of UpdatedComponents 4, // Index of ReferenceFrames 8, // Index of DisposedComponentIds 12, // Index of DisposedEventHandlerIds 28 // Index of strings ); Assert.Equal(48, bytes.Length); // No other data } [Fact] public void CanIncludeUpdatedComponentsWithEmptyEdits() { // Arrange/Act var bytes = Serialize(new RenderBatch( new ArrayRange(new[] { new RenderTreeDiff(123, default), new RenderTreeDiff(int.MaxValue, default), }, 2), default, default, default)); // Assert AssertBinaryContents(bytes, /* startIndex */ 0, // UpdatedComponents[0] 123, // ComponentId 0, // Edits length // UpdatedComponents[1] int.MaxValue, // ComponentId 0, // Edits length 2, // Length of UpdatedComponents 0, // Index of UpdatedComponents[0] 8, // Index of UpdatedComponents[1] 0, // Length of ReferenceFrames 0, // Length of DisposedComponentIds 0, // Length of DisposedEventHandlerIds 16, // Index of UpdatedComponents 28, // Index of ReferenceFrames 32, // Index of DisposedComponentIds 36, // Index of DisposedEventHandlerIds 40 // Index of strings ); Assert.Equal(60, bytes.Length); // No other data } [Fact] public void CanIncludeEdits() { // Arrange/Act var edits = new[] { default, // Skipped (because offset=1 below) RenderTreeEdit.PrependFrame(456, 789), RenderTreeEdit.RemoveFrame(101), RenderTreeEdit.SetAttribute(102, 103), RenderTreeEdit.RemoveAttribute(104, "Some removed attribute"), RenderTreeEdit.UpdateText(105, 106), RenderTreeEdit.StepIn(107), RenderTreeEdit.StepOut(), }; var bytes = Serialize(new RenderBatch( new ArrayRange(new[] { new RenderTreeDiff(123, new ArraySegment( edits, 1, edits.Length - 1)) // Skip first to show offset is respected }, 1), default, default, default)); // Assert var diffsStartIndex = ReadInt(bytes, bytes.Length - 20); AssertBinaryContents(bytes, diffsStartIndex, 1, // Number of diffs 0); // Index of diffs[0] AssertBinaryContents(bytes, 0, 123, // Component ID for diff 0 7, // diff[0].Edits.Count RenderTreeEditType.PrependFrame, 456, 789, NullStringMarker, RenderTreeEditType.RemoveFrame, 101, 0, NullStringMarker, RenderTreeEditType.SetAttribute, 102, 103, NullStringMarker, RenderTreeEditType.RemoveAttribute, 104, 0, "Some removed attribute", RenderTreeEditType.UpdateText, 105, 106, NullStringMarker, RenderTreeEditType.StepIn, 107, 0, NullStringMarker, RenderTreeEditType.StepOut, 0, 0, NullStringMarker ); } [Fact] public void CanIncludeReferenceFrames() { // Arrange/Act var bytes = Serialize(new RenderBatch( default, new ArrayRange(new[] { RenderTreeFrame.Attribute(123, "Attribute with string value", "String value"), RenderTreeFrame.Attribute(124, "Attribute with nonstring value", 1), RenderTreeFrame.Attribute(125, "Attribute with delegate value", new Action(() => { })) .WithAttributeEventHandlerId(789), RenderTreeFrame.ChildComponent(126, typeof(object)) .WithComponentSubtreeLength(5678) .WithComponentInstance(2000, new FakeComponent()), RenderTreeFrame.ComponentReferenceCapture(127, value => { }, 1001), RenderTreeFrame.Element(128, "Some element") .WithElementSubtreeLength(1234), RenderTreeFrame.ElementReferenceCapture(129, value => { }) .WithElementReferenceCaptureId("my unique ID"), RenderTreeFrame.Region(130) .WithRegionSubtreeLength(1234), RenderTreeFrame.Text(131, "Some text"), }, 9), default, default)); // Assert var referenceFramesStartIndex = ReadInt(bytes, bytes.Length - 16); AssertBinaryContents(bytes, referenceFramesStartIndex, 9, // Number of frames RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0, RenderTreeFrameType.Attribute, "Attribute with nonstring value", NullStringMarker, 0, RenderTreeFrameType.Attribute, "Attribute with delegate value", NullStringMarker, 789, RenderTreeFrameType.Component, 5678, 2000, 0, RenderTreeFrameType.ComponentReferenceCapture, 0, 0, 0, RenderTreeFrameType.Element, 1234, "Some element", 0, RenderTreeFrameType.ElementReferenceCapture, "my unique ID", 0, 0, RenderTreeFrameType.Region, 1234, 0, 0, RenderTreeFrameType.Text, "Some text", 0, 0 ); } private Span Serialize(RenderBatch renderBatch) { using (var ms = new MemoryStream()) using (var writer = new RenderBatchWriter(ms, leaveOpen: false)) { writer.Write(renderBatch); return new Span(ms.ToArray(), 0, (int)ms.Length); } } static void AssertBinaryContents(Span data, int startIndex, params object[] entries) { var bytes = data.ToArray(); // The string table position is given by the final int var stringTableStartPosition = BitConverter.ToInt32(bytes, bytes.Length - 4); using (var ms = new MemoryStream(bytes)) using (var reader = new BinaryReader(ms)) { ms.Seek(startIndex, SeekOrigin.Begin); foreach (var expectedEntryIterationVar in entries) { // Assume enums are represented as ints var expectedEntry = expectedEntryIterationVar.GetType().IsEnum ? (int)expectedEntryIterationVar : expectedEntryIterationVar; if (expectedEntry is int expectedInt) { Assert.Equal(expectedInt, reader.ReadInt32()); } else if (expectedEntry is string || expectedEntry == NullStringMarker) { // For strings, we have to look up the value in the table of strings // that appears at the end of the serialized data var indexIntoStringTable = reader.ReadInt32(); var expectedString = expectedEntry as string; if (expectedString == null) { Assert.Equal(-1, indexIntoStringTable); } else { // The string table entries are all length-prefixed UTF8 blobs var tableEntryPos = BitConverter.ToInt32(bytes, stringTableStartPosition + 4 * indexIntoStringTable); var length = (int)ReadUnsignedLEB128(bytes, tableEntryPos, out var numLEB128Bytes); var value = Encoding.UTF8.GetString(bytes, tableEntryPos + numLEB128Bytes, length); Assert.Equal(expectedString, value); } } else { throw new InvalidOperationException($"Unsupported type: {expectedEntry.GetType().FullName}"); } } } } static int ReadInt(Span bytes, int startOffset) => BitConverter.ToInt32(bytes.Slice(startOffset, 4).ToArray(), 0); public static uint ReadUnsignedLEB128(byte[] bytes, int startOffset, out int numBytesRead) { var result = (uint)0; var shift = 0; var currentByte = (byte)128; numBytesRead = 0; for (var count = 0; count < 4 && currentByte >= 128; count++) { currentByte = bytes[startOffset + count]; result += (uint)(currentByte & 0x7f) << shift; shift += 7; numBytesRead++; } return result; } class FakeComponent : IComponent { public void Init(RenderHandle renderHandle) => throw new NotImplementedException(); public void SetParameters(ParameterCollection parameters) => throw new NotImplementedException(); } } }