Prevent LOH allocations when constructing large Razor Source Documents. (#1049)
* Prevent LOH allocations when constructing large Razor Source Documents. * Feedback
This commit is contained in:
parent
3159266169
commit
dee8d8694f
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
Encoding = encoding;
|
Encoding = encoding;
|
||||||
Filename = filename;
|
Filename = filename;
|
||||||
|
|
||||||
_lines = new LineCollection(this, LineCollection.GetLineStarts(content));
|
_lines = new DefaultRazorSourceLineCollection(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override char this[int position] => _content[position];
|
public override char this[int position] => _content[position];
|
||||||
|
|
@ -72,118 +72,5 @@ namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
|
|
||||||
_content.CopyTo(sourceIndex, destination, destinationIndex, count);
|
_content.CopyTo(sourceIndex, destination, destinationIndex, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LineCollection : RazorSourceLineCollection
|
|
||||||
{
|
|
||||||
private readonly DefaultRazorSourceDocument _document;
|
|
||||||
private readonly int[] _lineStarts;
|
|
||||||
|
|
||||||
public LineCollection(DefaultRazorSourceDocument document, int[] lineStarts)
|
|
||||||
{
|
|
||||||
_document = document;
|
|
||||||
_lineStarts = lineStarts;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int Count => _lineStarts.Length;
|
|
||||||
|
|
||||||
public override int GetLineLength(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= _lineStarts.Length)
|
|
||||||
{
|
|
||||||
throw new IndexOutOfRangeException(nameof(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index == _lineStarts.Length - 1)
|
|
||||||
{
|
|
||||||
// Last line is special.
|
|
||||||
return _document.Length - _lineStarts[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
return _lineStarts[index + 1] - _lineStarts[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override SourceLocation GetLocation(int position)
|
|
||||||
{
|
|
||||||
if (position < 0 || position >= _document.Length)
|
|
||||||
{
|
|
||||||
throw new IndexOutOfRangeException(nameof(position));
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = Array.BinarySearch<int>(_lineStarts, position);
|
|
||||||
if (index >= 0)
|
|
||||||
{
|
|
||||||
// We have an exact match for the start of a line.
|
|
||||||
Debug.Assert(_lineStarts[index] == position);
|
|
||||||
|
|
||||||
return new SourceLocation(_document.Filename, position, index, characterIndex: 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Index is the complement of the line *after* the one we want, because BinarySearch tells
|
|
||||||
// us where we'd put position *if* it were the start of a line.
|
|
||||||
index = (~index) - 1;
|
|
||||||
if (index == -1)
|
|
||||||
{
|
|
||||||
// There's no preceding line, so it's based on the start of the string
|
|
||||||
return new SourceLocation(_document.Filename, position, 0, position);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var characterIndex = position - _lineStarts[index];
|
|
||||||
return new SourceLocation(_document.Filename, position, index, characterIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int[] GetLineStarts(string text)
|
|
||||||
{
|
|
||||||
var starts = new List<int>();
|
|
||||||
|
|
||||||
// We always consider a document to have at least a 0th line, even if it's empty.
|
|
||||||
starts.Add(0);
|
|
||||||
|
|
||||||
var unprocessedCR = false;
|
|
||||||
|
|
||||||
// Length - 1 because we don't care if there was a linebreak as the last character.
|
|
||||||
for (var i = 0; i < text.Length - 1; i++)
|
|
||||||
{
|
|
||||||
var c = text[i];
|
|
||||||
var isLineBreak = false;
|
|
||||||
|
|
||||||
switch (c)
|
|
||||||
{
|
|
||||||
case '\r':
|
|
||||||
unprocessedCR = true;
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case '\n':
|
|
||||||
unprocessedCR = false;
|
|
||||||
isLineBreak = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '\u0085':
|
|
||||||
case '\u2028':
|
|
||||||
case '\u2029':
|
|
||||||
isLineBreak = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unprocessedCR)
|
|
||||||
{
|
|
||||||
// If we get here it means that we had a CR followed by something other than an LF.
|
|
||||||
// Add the CR as a line break.
|
|
||||||
starts.Add(i);
|
|
||||||
unprocessedCR = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLineBreak)
|
|
||||||
{
|
|
||||||
starts.Add(i + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return starts.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
// 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 System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
|
{
|
||||||
|
internal class DefaultRazorSourceLineCollection : RazorSourceLineCollection
|
||||||
|
{
|
||||||
|
private readonly RazorSourceDocument _document;
|
||||||
|
private readonly int[] _lineStarts;
|
||||||
|
|
||||||
|
public DefaultRazorSourceLineCollection(RazorSourceDocument document)
|
||||||
|
{
|
||||||
|
_document = document;
|
||||||
|
_lineStarts = GetLineStarts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Count => _lineStarts.Length;
|
||||||
|
|
||||||
|
public override int GetLineLength(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= _lineStarts.Length)
|
||||||
|
{
|
||||||
|
throw new IndexOutOfRangeException(nameof(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index == _lineStarts.Length - 1)
|
||||||
|
{
|
||||||
|
// Last line is special.
|
||||||
|
return _document.Length - _lineStarts[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return _lineStarts[index + 1] - _lineStarts[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override SourceLocation GetLocation(int position)
|
||||||
|
{
|
||||||
|
if (position < 0 || position >= _document.Length)
|
||||||
|
{
|
||||||
|
throw new IndexOutOfRangeException(nameof(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = Array.BinarySearch<int>(_lineStarts, position);
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
// We have an exact match for the start of a line.
|
||||||
|
Debug.Assert(_lineStarts[index] == position);
|
||||||
|
|
||||||
|
return new SourceLocation(_document.Filename, position, index, characterIndex: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Index is the complement of the line *after* the one we want, because BinarySearch tells
|
||||||
|
// us where we'd put position *if* it were the start of a line.
|
||||||
|
index = (~index) - 1;
|
||||||
|
if (index == -1)
|
||||||
|
{
|
||||||
|
// There's no preceding line, so it's based on the start of the string
|
||||||
|
return new SourceLocation(_document.Filename, position, 0, position);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var characterIndex = position - _lineStarts[index];
|
||||||
|
return new SourceLocation(_document.Filename, position, index, characterIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[] GetLineStarts()
|
||||||
|
{
|
||||||
|
var starts = new List<int>();
|
||||||
|
|
||||||
|
// We always consider a document to have at least a 0th line, even if it's empty.
|
||||||
|
starts.Add(0);
|
||||||
|
|
||||||
|
var unprocessedCR = false;
|
||||||
|
|
||||||
|
// Length - 1 because we don't care if there was a linebreak as the last character.
|
||||||
|
var length = _document.Length;
|
||||||
|
for (var i = 0; i < length - 1; i++)
|
||||||
|
{
|
||||||
|
var c = _document[i];
|
||||||
|
var isLineBreak = false;
|
||||||
|
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case '\r':
|
||||||
|
unprocessedCR = true;
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case '\n':
|
||||||
|
unprocessedCR = false;
|
||||||
|
isLineBreak = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '\u0085':
|
||||||
|
case '\u2028':
|
||||||
|
case '\u2029':
|
||||||
|
isLineBreak = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unprocessedCR)
|
||||||
|
{
|
||||||
|
// If we get here it means that we had a CR followed by something other than an LF.
|
||||||
|
// Add the CR as a line break.
|
||||||
|
starts.Add(i);
|
||||||
|
unprocessedCR = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLineBreak)
|
||||||
|
{
|
||||||
|
starts.Add(i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return starts.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
// 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 System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
|
{
|
||||||
|
internal class LargeTextRazorSourceDocument : RazorSourceDocument
|
||||||
|
{
|
||||||
|
private readonly List<char[]> _chunks;
|
||||||
|
|
||||||
|
private readonly int _chunkMaxLength;
|
||||||
|
|
||||||
|
private readonly RazorSourceLineCollection _lines;
|
||||||
|
|
||||||
|
private readonly int _length;
|
||||||
|
|
||||||
|
public LargeTextRazorSourceDocument(StreamReader reader, int chunkMaxLength, Encoding encoding, string filename)
|
||||||
|
{
|
||||||
|
if (reader == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(reader));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(encoding));
|
||||||
|
}
|
||||||
|
|
||||||
|
_chunkMaxLength = chunkMaxLength;
|
||||||
|
Encoding = encoding;
|
||||||
|
Filename = filename;
|
||||||
|
|
||||||
|
ReadChunks(reader, _chunkMaxLength, out _length, out _chunks);
|
||||||
|
_lines = new DefaultRazorSourceLineCollection(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override char this[int position]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var chunkIndex = position / _chunkMaxLength;
|
||||||
|
var insideChunkPosition = position % _chunkMaxLength;
|
||||||
|
|
||||||
|
return _chunks[chunkIndex][insideChunkPosition];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Encoding Encoding { get; }
|
||||||
|
|
||||||
|
public override string Filename { get; }
|
||||||
|
|
||||||
|
public override int Length => _length;
|
||||||
|
|
||||||
|
public override RazorSourceLineCollection Lines => _lines;
|
||||||
|
|
||||||
|
public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)
|
||||||
|
{
|
||||||
|
if (destination == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(destination));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceIndex < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(sourceIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destinationIndex < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(destinationIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count < 0 || count > Length - sourceIndex || count > destination.Length - destinationIndex)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunkIndex = sourceIndex / _chunkMaxLength;
|
||||||
|
var insideChunkPosition = sourceIndex % _chunkMaxLength;
|
||||||
|
var remaining = count;
|
||||||
|
var currentDestIndex = destinationIndex;
|
||||||
|
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var toCopy = Math.Min(remaining, _chunkMaxLength - insideChunkPosition);
|
||||||
|
Array.Copy(_chunks[chunkIndex], insideChunkPosition, destination, currentDestIndex, toCopy);
|
||||||
|
|
||||||
|
remaining -= toCopy;
|
||||||
|
currentDestIndex += toCopy;
|
||||||
|
chunkIndex++;
|
||||||
|
insideChunkPosition = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReadChunks(StreamReader reader, int chunkMaxLength, out int length, out List<char[]> chunks)
|
||||||
|
{
|
||||||
|
length = 0;
|
||||||
|
chunks = new List<char[]>();
|
||||||
|
|
||||||
|
int read;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var chunk = new char[chunkMaxLength];
|
||||||
|
read = reader.ReadBlock(chunk, 0, chunkMaxLength);
|
||||||
|
|
||||||
|
length += read;
|
||||||
|
|
||||||
|
if (read > 0)
|
||||||
|
{
|
||||||
|
chunks.Add(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (read == chunkMaxLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
{
|
{
|
||||||
public abstract class RazorSourceDocument
|
public abstract class RazorSourceDocument
|
||||||
{
|
{
|
||||||
|
private const int LargeObjectHeapLimitInChars = 40 * 1024; // 40K Unicode chars is 80KB which is less than the large object heap limit.
|
||||||
|
|
||||||
internal static readonly RazorSourceDocument[] EmptyArray = new RazorSourceDocument[0];
|
internal static readonly RazorSourceDocument[] EmptyArray = new RazorSourceDocument[0];
|
||||||
|
|
||||||
public abstract Encoding Encoding { get; }
|
public abstract Encoding Encoding { get; }
|
||||||
|
|
@ -75,16 +77,18 @@ namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
|
|
||||||
if (streamLength > 0)
|
if (streamLength > 0)
|
||||||
{
|
{
|
||||||
|
var bufferSize = Math.Min(streamLength, LargeObjectHeapLimitInChars);
|
||||||
|
|
||||||
var reader = new StreamReader(
|
var reader = new StreamReader(
|
||||||
stream,
|
stream,
|
||||||
contentEncoding,
|
contentEncoding,
|
||||||
detectEncodingFromByteOrderMarks: true,
|
detectEncodingFromByteOrderMarks: true,
|
||||||
bufferSize: streamLength,
|
bufferSize: bufferSize,
|
||||||
leaveOpen: true);
|
leaveOpen: true);
|
||||||
|
|
||||||
using (reader)
|
using (reader)
|
||||||
{
|
{
|
||||||
content = reader.ReadToEnd();
|
reader.Peek(); // Just to populate the encoding
|
||||||
|
|
||||||
if (encoding == null)
|
if (encoding == null)
|
||||||
{
|
{
|
||||||
|
|
@ -97,6 +101,18 @@ namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
encoding.EncodingName,
|
encoding.EncodingName,
|
||||||
reader.CurrentEncoding.EncodingName));
|
reader.CurrentEncoding.EncodingName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (streamLength > LargeObjectHeapLimitInChars)
|
||||||
|
{
|
||||||
|
// If the resulting string would end up on the large object heap, then use LargeTextRazorSourceDocument.
|
||||||
|
return new LargeTextRazorSourceDocument(
|
||||||
|
reader,
|
||||||
|
LargeObjectHeapLimitInChars,
|
||||||
|
contentEncoding,
|
||||||
|
filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
content = reader.ReadToEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Evolution.Test
|
||||||
|
{
|
||||||
|
public class LargeTextRazorSourceDocumentTest
|
||||||
|
{
|
||||||
|
private const int ChunkTestLength = 10;
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(ChunkTestLength - 1)]
|
||||||
|
[InlineData(ChunkTestLength)]
|
||||||
|
[InlineData(ChunkTestLength + 1)]
|
||||||
|
[InlineData(ChunkTestLength * 2 - 1)]
|
||||||
|
[InlineData(ChunkTestLength * 2)]
|
||||||
|
[InlineData(ChunkTestLength * 2 + 1)]
|
||||||
|
public void Indexer_ProvidesCharacterAccessToContent(int contentLength)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var content = new char[contentLength];
|
||||||
|
|
||||||
|
for (var i = 0; i < contentLength - 1; i++)
|
||||||
|
{
|
||||||
|
content[i] = 'a';
|
||||||
|
}
|
||||||
|
content[contentLength - 1] = 'b';
|
||||||
|
var contentString = new string(content);
|
||||||
|
|
||||||
|
var stream = TestRazorSourceDocument.CreateStreamContent(new string(content));
|
||||||
|
var reader = new StreamReader(stream, true);
|
||||||
|
var document = new LargeTextRazorSourceDocument(reader, ChunkTestLength, Encoding.UTF8, "file.cshtml");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var output = new char[contentLength];
|
||||||
|
for (var i = 0; i < document.Length; i++)
|
||||||
|
{
|
||||||
|
output[i] = document[i];
|
||||||
|
}
|
||||||
|
var outputString = new string(output);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(contentLength, document.Length);
|
||||||
|
Assert.Equal(contentString, outputString);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("test.cshtml")]
|
||||||
|
[InlineData(null)]
|
||||||
|
public void Filename(string fileName)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var stream = TestRazorSourceDocument.CreateStreamContent("abc");
|
||||||
|
var reader = new StreamReader(stream, true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var document = new LargeTextRazorSourceDocument(reader, ChunkTestLength, Encoding.UTF8, fileName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(fileName, document.Filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Lines()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var stream = TestRazorSourceDocument.CreateStreamContent("abc\ndef\nghi");
|
||||||
|
var reader = new StreamReader(stream, true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var document = new LargeTextRazorSourceDocument(reader, ChunkTestLength, Encoding.UTF8, "file.cshtml");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(3, document.Lines.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("", 0, 0, 0)] // Nothing to copy
|
||||||
|
[InlineData("a", 0, 100, 1)] // Destination index different from start
|
||||||
|
[InlineData("j", ChunkTestLength - 1, 0, 1)] // One char just before the chunk limit
|
||||||
|
[InlineData("k", ChunkTestLength, 0, 1)] // One char one the chunk limit
|
||||||
|
[InlineData("l", ChunkTestLength + 1, 0, 1)] // One char just after the chunk limit
|
||||||
|
[InlineData("jk", ChunkTestLength - 1, 0, 2)] // Two char that are on both chunk sides
|
||||||
|
[InlineData("abcdefghijklmnopqrstuvwxy", 0, 100, 25)] // Everything except the last
|
||||||
|
[InlineData("abcdefghijklmnopqrstuvwxyz", 0, 0, 26)] // Copy all
|
||||||
|
[InlineData("xyz", 23, 0, 3)] // The last chars
|
||||||
|
public void CopyTo(string expected, int sourceIndex, int destinationIndex, int count)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var stream = TestRazorSourceDocument.CreateStreamContent("abcdefghijklmnopqrstuvwxyz");
|
||||||
|
|
||||||
|
var reader = new StreamReader(stream, true);
|
||||||
|
var document = new LargeTextRazorSourceDocument(reader, ChunkTestLength, Encoding.UTF8, "file.cshtml");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var destination = new char[1000];
|
||||||
|
document.CopyTo(sourceIndex, destination, destinationIndex, count);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var copy = new string(destination, destinationIndex, count);
|
||||||
|
Assert.Equal(expected, copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -94,5 +94,20 @@ namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
() => RazorSourceDocument.ReadFrom(content, "file.cshtml", Encoding.UTF8));
|
() => RazorSourceDocument.ReadFrom(content, "file.cshtml", Encoding.UTF8));
|
||||||
Assert.Equal(expectedMessage, exception.Message);
|
Assert.Equal(expectedMessage, exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadFrom_LargeContent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var content = TestRazorSourceDocument.CreateStreamContent(new string('a', 100000));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var document = RazorSourceDocument.ReadFrom(content, "file.cshtml");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<LargeTextRazorSourceDocument>(document);
|
||||||
|
Assert.Equal("file.cshtml", document.Filename);
|
||||||
|
Assert.Same(Encoding.UTF8, document.Encoding);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue