diff --git a/src/Microsoft.AspNet.Diagnostics/ErrorPageMiddleware.cs b/src/Microsoft.AspNet.Diagnostics/ErrorPageMiddleware.cs index 893036a593..7aeb42a55e 100644 --- a/src/Microsoft.AspNet.Diagnostics/ErrorPageMiddleware.cs +++ b/src/Microsoft.AspNet.Diagnostics/ErrorPageMiddleware.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Diagnostics.Views; +using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.Http; using Microsoft.Framework.Internal; using Microsoft.Framework.Logging; @@ -28,6 +29,7 @@ namespace Microsoft.AspNet.Diagnostics private readonly ErrorPageOptions _options; private static readonly bool IsMono = Type.GetType("Mono.Runtime") != null; private readonly ILogger _logger; + private readonly IFileProvider _fileProvider; /// /// Initializes a new instance of the class @@ -35,11 +37,15 @@ namespace Microsoft.AspNet.Diagnostics /// /// public ErrorPageMiddleware( - [NotNull] RequestDelegate next, [NotNull] ErrorPageOptions options, ILoggerFactory loggerFactory) + [NotNull] RequestDelegate next, + [NotNull] ErrorPageOptions options, + ILoggerFactory loggerFactory, + IApplicationEnvironment appEnvironment) { _next = next; _options = options; _logger = loggerFactory.CreateLogger(); + _fileProvider = options.FileProvider ?? new PhysicalFileProvider(appEnvironment.ApplicationBasePath); } /// @@ -183,31 +189,102 @@ namespace Microsoft.AspNet.Diagnostics int lineNumber = line.ToInt32(); - return string.IsNullOrEmpty(file) - ? LoadFrame(string.IsNullOrEmpty(function) ? line.ToString() : function, string.Empty, 0) - : LoadFrame(function, file, lineNumber); + if (string.IsNullOrEmpty(file)) + { + return GetStackFrame( + // Handle stack trace lines like + // "--- End of stack trace from previous location where exception from thrown ---" + string.IsNullOrEmpty(function) ? line.ToString() : function, + file: string.Empty, + lineNumber: 0); + } + else + { + return GetStackFrame(function, file, lineNumber); + } } - private StackFrame LoadFrame(string function, string file, int lineNumber) + // make it internal to enable unit testing + internal StackFrame GetStackFrame(string function, string file, int lineNumber) { var frame = new StackFrame { Function = function, File = file, Line = lineNumber }; + + if (string.IsNullOrEmpty(file)) + { + return frame; + } + + IEnumerable lines = null; if (File.Exists(file)) { - var code = File.ReadLines(file); - ReadFrameContent(frame, code, lineNumber, lineNumber); + lines = File.ReadLines(file); } + else + { + // Handle relative paths and embedded files + var fileInfo = _fileProvider.GetFileInfo(file); + if (fileInfo.Exists) + { + // ReadLines doesn't accept a stream. Use ReadLines as its more efficient + // relative to reading lines via stream reader + if (!string.IsNullOrEmpty(fileInfo.PhysicalPath)) + { + lines = File.ReadLines(fileInfo.PhysicalPath); + } + else + { + lines = ReadLines(fileInfo); + } + } + } + + if (lines != null) + { + ReadFrameContent(frame, lines, lineNumber, lineNumber); + } + return frame; } - private void ReadFrameContent(StackFrame frame, - IEnumerable code, - int startLineNumber, - int endLineNumber) + // make it internal to enable unit testing + internal void ReadFrameContent( + StackFrame frame, + IEnumerable allLines, + int errorStartLineNumberInFile, + int errorEndLineNumberInFile) { - frame.PreContextLine = Math.Max(startLineNumber - _options.SourceCodeLineCount, 1); - frame.PreContextCode = code.Skip(frame.PreContextLine - 1).Take(startLineNumber - frame.PreContextLine).ToArray(); - frame.ContextCode = code.Skip(startLineNumber - 1).Take(1 + Math.Max(0, endLineNumber - startLineNumber)); - frame.PostContextCode = code.Skip(startLineNumber).Take(_options.SourceCodeLineCount).ToArray(); + // Get the line boundaries in the file to be read and read all these lines at once into an array. + var preErrorLineNumberInFile = Math.Max(errorStartLineNumberInFile - _options.SourceCodeLineCount, 1); + var postErrorLineNumberInFile = errorEndLineNumberInFile + _options.SourceCodeLineCount; + var codeBlock = allLines + .Skip(preErrorLineNumberInFile - 1) + .Take(postErrorLineNumberInFile - preErrorLineNumberInFile + 1) + .ToArray(); + + var numOfErrorLines = (errorEndLineNumberInFile - errorStartLineNumberInFile) + 1; + var errorStartLineNumberInArray = errorStartLineNumberInFile - preErrorLineNumberInFile; + + frame.PreContextLine = preErrorLineNumberInFile; + frame.PreContextCode = codeBlock.Take(errorStartLineNumberInArray).ToArray(); + frame.ContextCode = codeBlock + .Skip(errorStartLineNumberInArray) + .Take(numOfErrorLines) + .ToArray(); + frame.PostContextCode = codeBlock + .Skip(errorStartLineNumberInArray + numOfErrorLines) + .ToArray(); + } + + private static IEnumerable ReadLines(IFileInfo fileInfo) + { + using (var reader = new StreamReader(fileInfo.CreateReadStream())) + { + string line; + while ((line = reader.ReadLine()) != null) + { + yield return line; + } + } } internal class Chunk diff --git a/src/Microsoft.AspNet.Diagnostics/ErrorPageOptions.cs b/src/Microsoft.AspNet.Diagnostics/ErrorPageOptions.cs index 6e1a05432a..5db1e0c46f 100644 --- a/src/Microsoft.AspNet.Diagnostics/ErrorPageOptions.cs +++ b/src/Microsoft.AspNet.Diagnostics/ErrorPageOptions.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNet.FileProviders; + namespace Microsoft.AspNet.Diagnostics { /// @@ -23,5 +25,13 @@ namespace Microsoft.AspNet.Diagnostics /// source code referenced by the exception stack trace is present on the server. /// public int SourceCodeLineCount { get; set; } + + /// + /// Provides files containing source code used to display contextual information of an exception. + /// + /// + /// If null will use a . + /// + public IFileProvider FileProvider { get; set; } } } diff --git a/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cs b/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cs index 2bab1297cc..b64b1f4654 100644 --- a/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cs +++ b/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cs @@ -143,8 +143,8 @@ using Views #line hidden WriteLiteral("
  • (tabIndex, 1242), false)); + WriteAttribute("tabindex", Tuple.Create(" tabindex=\"", 1246), Tuple.Create("\"", 1266), + Tuple.Create(Tuple.Create("", 1257), Tuple.Create(tabIndex, 1257), false)); WriteLiteral(">\r\n"); #line 42 "CompilationErrorPage.cshtml" @@ -193,7 +193,7 @@ using Views #line hidden #line 48 "CompilationErrorPage.cshtml" - if (frame.Line != 0 && frame.ContextCode !=null && frame.ContextCode.Any()) + if (frame.Line != 0 && frame.ContextCode.Any()) { #line default @@ -207,15 +207,15 @@ using Views #line hidden #line 51 "CompilationErrorPage.cshtml" - if (frame.PreContextCode != null) + if (frame.PreContextCode.Any()) { #line default #line hidden WriteLiteral(" (frame.PreContextLine, 1785), false)); + WriteAttribute("start", Tuple.Create(" start=\"", 1790), Tuple.Create("\"", 1819), + Tuple.Create(Tuple.Create("", 1798), Tuple.Create(frame.PreContextLine, 1798), false)); WriteLiteral(" class=\"collapsible\">\r\n"); #line 54 "CompilationErrorPage.cshtml" @@ -251,8 +251,8 @@ using Views #line hidden WriteLiteral(" (frame.Line, 2195), false)); + WriteAttribute("start", Tuple.Create(" start=\"", 2200), Tuple.Create("\"", 2219), + Tuple.Create(Tuple.Create("", 2208), Tuple.Create(frame.Line, 2208), false)); WriteLiteral(" class=\"highlight\">\r\n"); #line 61 "CompilationErrorPage.cshtml" @@ -288,15 +288,15 @@ using Views #line hidden #line 66 "CompilationErrorPage.cshtml" - if (frame.PostContextCode != null) + if (frame.PostContextCode.Any()) { #line default #line hidden WriteLiteral(" (frame.Line + 1, 2643), false)); + WriteAttribute("start", Tuple.Create(" start=\'", 2646), Tuple.Create("\'", 2671), + Tuple.Create(Tuple.Create("", 2654), Tuple.Create(frame.Line + 1, 2654), false)); WriteLiteral(" class=\"collapsible\">\r\n"); #line 69 "CompilationErrorPage.cshtml" diff --git a/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cshtml b/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cshtml index 22d2a539a3..5e7c80164a 100644 --- a/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cshtml +++ b/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cshtml @@ -45,10 +45,10 @@

    @frame.ErrorDetails

    } - @if (frame.Line != 0 && frame.ContextCode !=null && frame.ContextCode.Any()) + @if (frame.Line != 0 && frame.ContextCode.Any()) {
    - @if (frame.PreContextCode != null) + @if (frame.PreContextCode.Any()) {
      @foreach (var line in frame.PreContextCode) @@ -63,7 +63,7 @@
    1. @line
    2. }
    - @if (frame.PostContextCode != null) + @if (frame.PostContextCode.Any()) {
      @foreach (var line in frame.PostContextCode) diff --git a/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cs b/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cs index 080d3a431d..f08d7afe2c 100644 --- a/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cs +++ b/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cs @@ -65,8 +65,8 @@ using Views #line hidden WriteLiteral("\r\n\r\n(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, 525), false)); + WriteAttribute("lang", Tuple.Create(" lang=\"", 533), Tuple.Create("\"", 594), + Tuple.Create(Tuple.Create("", 540), Tuple.Create(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, 540), false)); WriteLiteral(" xmlns=\"http://www.w3.org/1999/xhtml\">\r\n \r\n \r\n "); #line 26 "ErrorPage.cshtml" @@ -147,8 +147,8 @@ using Views #line default #line hidden WriteLiteral(" in <code"); - WriteAttribute("title", Tuple.Create(" title=\"", 1895), Tuple.Create("\"", 1919), - Tuple.Create(Tuple.Create("", 1903), Tuple.Create<System.Object, System.Int32>(firstFrame.File, 1903), false)); + WriteAttribute("title", Tuple.Create(" title=\"", 1910), Tuple.Create("\"", 1934), + Tuple.Create(Tuple.Create("", 1918), Tuple.Create<System.Object, System.Int32>(firstFrame.File, 1918), false)); WriteLiteral(">"); #line 50 "ErrorPage.cshtml" Write(System.IO.Path.GetFileName(firstFrame.File)); @@ -281,8 +281,8 @@ using Views #line hidden WriteLiteral(" <li class=\"frame\""); - WriteAttribute("tabindex", Tuple.Create(" tabindex=\"", 3327), Tuple.Create("\"", 3347), - Tuple.Create(Tuple.Create("", 3338), Tuple.Create<System.Object, System.Int32>(tabIndex, 3338), false)); + WriteAttribute("tabindex", Tuple.Create(" tabindex=\"", 3342), Tuple.Create("\"", 3362), + Tuple.Create(Tuple.Create("", 3353), Tuple.Create<System.Object, System.Int32>(tabIndex, 3353), false)); WriteLiteral(">\r\n"); #line 87 "ErrorPage.cshtml" @@ -332,8 +332,8 @@ using Views #line default #line hidden WriteLiteral(" in <code"); - WriteAttribute("title", Tuple.Create(" title=\"", 3742), Tuple.Create("\"", 3761), - Tuple.Create(Tuple.Create("", 3750), Tuple.Create<System.Object, System.Int32>(frame.File, 3750), false)); + WriteAttribute("title", Tuple.Create(" title=\"", 3757), Tuple.Create("\"", 3776), + Tuple.Create(Tuple.Create("", 3765), Tuple.Create<System.Object, System.Int32>(frame.File, 3765), false)); WriteLiteral(">"); #line 94 "ErrorPage.cshtml" Write(System.IO.Path.GetFileName(frame.File)); @@ -355,7 +355,7 @@ using Views #line hidden #line 97 "ErrorPage.cshtml" - if (frame.Line != 0 && frame.ContextCode !=null && frame.ContextCode.Any()) + if (frame.Line != 0 && frame.ContextCode.Any()) { #line default @@ -369,15 +369,15 @@ using Views #line hidden #line 100 "ErrorPage.cshtml" - if (frame.PreContextCode != null) + if (frame.PreContextCode.Any()) { #line default #line hidden WriteLiteral(" <ol"); - WriteAttribute("start", Tuple.Create(" start=\"", 4194), Tuple.Create("\"", 4223), - Tuple.Create(Tuple.Create("", 4202), Tuple.Create<System.Object, System.Int32>(frame.PreContextLine, 4202), false)); + WriteAttribute("start", Tuple.Create(" start=\"", 4207), Tuple.Create("\"", 4236), + Tuple.Create(Tuple.Create("", 4215), Tuple.Create<System.Object, System.Int32>(frame.PreContextLine, 4215), false)); WriteLiteral(" class=\"collapsible\">\r\n"); #line 103 "ErrorPage.cshtml" @@ -413,8 +413,8 @@ using Views #line hidden WriteLiteral("\r\n <ol"); - WriteAttribute("start", Tuple.Create(" start=\"", 4663), Tuple.Create("\"", 4682), - Tuple.Create(Tuple.Create("", 4671), Tuple.Create<System.Object, System.Int32>(frame.Line, 4671), false)); + WriteAttribute("start", Tuple.Create(" start=\"", 4676), Tuple.Create("\"", 4695), + Tuple.Create(Tuple.Create("", 4684), Tuple.Create<System.Object, System.Int32>(frame.Line, 4684), false)); WriteLiteral(" class=\"highlight\">\r\n"); #line 111 "ErrorPage.cshtml" @@ -450,15 +450,15 @@ using Views #line hidden #line 117 "ErrorPage.cshtml" - if (frame.PostContextCode != null) + if (frame.PostContextCode.Any()) { #line default #line hidden WriteLiteral(" <ol"); - WriteAttribute("start", Tuple.Create(" start=\'", 5177), Tuple.Create("\'", 5202), - Tuple.Create(Tuple.Create("", 5185), Tuple.Create<System.Object, System.Int32>(frame.Line + 1, 5185), false)); + WriteAttribute("start", Tuple.Create(" start=\'", 5188), Tuple.Create("\'", 5213), + Tuple.Create(Tuple.Create("", 5196), Tuple.Create<System.Object, System.Int32>(frame.Line + 1, 5196), false)); WriteLiteral(" class=\"collapsible\">\r\n"); #line 120 "ErrorPage.cshtml" diff --git a/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cshtml b/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cshtml index b6905bf89d..c332e26830 100644 --- a/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cshtml +++ b/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cshtml @@ -94,10 +94,10 @@ <h3>@frame.Function in <code title="@frame.File">@System.IO.Path.GetFileName(frame.File)</code></h3> } - @if (frame.Line != 0 && frame.ContextCode !=null && frame.ContextCode.Any()) + @if (frame.Line != 0 && frame.ContextCode.Any()) { <div class="source"> - @if (frame.PreContextCode != null) + @if (frame.PreContextCode.Any()) { <ol start="@frame.PreContextLine" class="collapsible"> @foreach (var line in frame.PreContextCode) @@ -114,7 +114,7 @@ } </ol> - @if (frame.PostContextCode != null) + @if (frame.PostContextCode.Any()) { <ol start='@(frame.Line + 1)' class="collapsible"> @foreach (var line in frame.PostContextCode) diff --git a/src/Microsoft.AspNet.Diagnostics/Views/StackFrame.cs b/src/Microsoft.AspNet.Diagnostics/Views/StackFrame.cs index abb13eefd5..55a3d56b61 100644 --- a/src/Microsoft.AspNet.Diagnostics/Views/StackFrame.cs +++ b/src/Microsoft.AspNet.Diagnostics/Views/StackFrame.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; +using System.Linq; namespace Microsoft.AspNet.Diagnostics.Views { @@ -32,19 +33,19 @@ namespace Microsoft.AspNet.Diagnostics.Views public int PreContextLine { get; set; } /// <summary> - /// + /// Lines of code before the actual error line(s). /// </summary> - public IEnumerable<string> PreContextCode { get; set; } + public IEnumerable<string> PreContextCode { get; set; } = Enumerable.Empty<string>(); /// <summary> - /// + /// Line(s) of code responsible for the error. /// </summary> - public IEnumerable<string> ContextCode { get; set; } + public IEnumerable<string> ContextCode { get; set; } = Enumerable.Empty<string>(); /// <summary> - /// + /// Lines of code after the actual error line(s). /// </summary> - public IEnumerable<string> PostContextCode { get; set; } + public IEnumerable<string> PostContextCode { get; set; } = Enumerable.Empty<string>(); /// <summary> /// Specific error details for this stack frame. diff --git a/src/Microsoft.AspNet.Diagnostics/project.json b/src/Microsoft.AspNet.Diagnostics/project.json index 9675c120f2..298d5fad22 100644 --- a/src/Microsoft.AspNet.Diagnostics/project.json +++ b/src/Microsoft.AspNet.Diagnostics/project.json @@ -7,13 +7,17 @@ }, "dependencies": { "Microsoft.AspNet.Diagnostics.Abstractions": "1.0.0-*", + "Microsoft.AspNet.FileProviders.Physical": "1.0.0-*", "Microsoft.AspNet.Http.Extensions": "1.0.0-*", "Microsoft.AspNet.WebUtilities": "1.0.0-*", + "Microsoft.Framework.Logging.Abstractions": "1.0.0-*", + "Microsoft.Framework.NotNullAttribute.Sources": { + "type": "build", + "version": "1.0.0-*" + }, "Microsoft.Framework.OptionsModel": "1.0.0-*", - "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" }, - "Microsoft.Framework.WebEncoders.Core": "1.0.0-*", "Microsoft.Framework.Runtime.Compilation.Abstractions": "1.0.0-*", - "Microsoft.Framework.Logging.Abstractions": "1.0.0-*" + "Microsoft.Framework.WebEncoders.Core": "1.0.0-*" }, "frameworks": { "dnx451": {}, diff --git a/test/Microsoft.AspNet.Diagnostics.Tests/ErrorPageMiddlewareTest.cs b/test/Microsoft.AspNet.Diagnostics.Tests/ErrorPageMiddlewareTest.cs new file mode 100644 index 0000000000..62d9af1ef2 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Tests/ErrorPageMiddlewareTest.cs @@ -0,0 +1,451 @@ +// 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.Linq; +using System.Reflection; +using System.Runtime.Versioning; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Diagnostics.Views; +using Microsoft.AspNet.FileProviders; +using Microsoft.Framework.Caching; +using Microsoft.Framework.Logging; +using Microsoft.Framework.Runtime; +using Xunit; + +namespace Microsoft.AspNet.Diagnostics +{ + public class ErrorPageMiddlewareTest + { + [Theory] + [InlineData("TestFiles/SourceFile.txt")] + [InlineData(@"TestFiles\SourceFile.txt")] + public void UsesDefaultFileProvider_IfNotProvidedOnOptions(string relativePath) + { + // Arrange & Act + var middleware = GetErrorPageMiddleware(fileProvider: null); + var stackFrame = middleware.GetStackFrame("func1", relativePath, lineNumber: 10); + + // Assert + // Lines 4-16 (inclusive) is the code block + Assert.Equal(4, stackFrame.PreContextLine); + Assert.Equal(GetCodeLines(4, 9), stackFrame.PreContextCode); + Assert.Equal(GetCodeLines(10, 10), stackFrame.ContextCode); + Assert.Equal(GetCodeLines(11, 16), stackFrame.PostContextCode); + } + + public static TheoryData<string> AbsolutePathsData + { + get + { + var rootPath = Directory.GetCurrentDirectory(); + return new TheoryData<string>() + { + Path.Combine(rootPath, "TestFiles/SourceFile.txt"), + Path.Combine(rootPath, @"TestFiles\SourceFile.txt") + }; + } + } + + [Theory] + [MemberData(nameof(AbsolutePathsData))] + public void DisplaysSourceCodeLines_ForAbsolutePaths(string absoluteFilePath) + { + // Arrange + var rootPath = Directory.GetCurrentDirectory(); + // PhysicalFileProvider handles only relative paths but we fall back to work with absolute paths too + var provider = new PhysicalFileProvider(rootPath); + + // Act + var middleware = GetErrorPageMiddleware(provider); + var stackFrame = middleware.GetStackFrame("func1", absoluteFilePath, lineNumber: 10); + + // Assert + // Lines 4-16 (inclusive) is the code block + Assert.Equal(4, stackFrame.PreContextLine); + Assert.Equal(GetCodeLines(4, 9), stackFrame.PreContextCode); + Assert.Equal(GetCodeLines(10, 10), stackFrame.ContextCode); + Assert.Equal(GetCodeLines(11, 16), stackFrame.PostContextCode); + } + + [Theory] + [InlineData("TestFiles/SourceFile.txt")] + [InlineData(@"TestFiles\SourceFile.txt")] + public void DisplaysSourceCodeLines_ForRelativePaths(string relativePath) + { + // Arrange + var rootPath = Directory.GetCurrentDirectory(); + var provider = new PhysicalFileProvider(rootPath); + + // Act + var middleware = GetErrorPageMiddleware(provider); + var stackFrame = middleware.GetStackFrame("func1", relativePath, lineNumber: 10); + + // Assert + // Lines 4-16 (inclusive) is the code block + Assert.Equal(4, stackFrame.PreContextLine); + Assert.Equal(GetCodeLines(4, 9), stackFrame.PreContextCode); + Assert.Equal(GetCodeLines(10, 10), stackFrame.ContextCode); + Assert.Equal(GetCodeLines(11, 16), stackFrame.PostContextCode); + } + + [Theory] + [InlineData("TestFiles/EmbeddedSourceFile.txt")] + //[InlineData(@"TestFiles\EmbeddedSourceFile.txt")] + public void DisplaysSourceCodeLines_ForRelativeEmbeddedPaths(string relativePath) + { + // Arrange + var provider = new EmbeddedFileProvider( + GetType().GetTypeInfo().Assembly, + baseNamespace: $"{typeof(ErrorPageMiddlewareTest).GetTypeInfo().Assembly.GetName().Name}.Resources"); + + // Act + var middleware = GetErrorPageMiddleware(provider); + var stackFrame = middleware.GetStackFrame("func1", relativePath, lineNumber: 10); + + // Assert + // Lines 4-16 (inclusive) is the code block + Assert.Equal(4, stackFrame.PreContextLine); + Assert.Equal(GetCodeLines(4, 9), stackFrame.PreContextCode); + Assert.Equal(GetCodeLines(10, 10), stackFrame.ContextCode); + Assert.Equal(GetCodeLines(11, 16), stackFrame.PostContextCode); + } + + public static TheoryData<ErrorData> DisplaysSourceCodeLines_PreAndPostErrorLineData + { + get + { + return new TheoryData<ErrorData>() + { + new ErrorData() + { + AllLines = GetCodeLines(1, 30), + ErrorStartLine = 10, + ErrorEndLine = 10, + ExpectedPreContextLine = 4, + ExpectedPreErrorCode = GetCodeLines(4, 9), + ExpectedErrorCode = GetCodeLines(10, 10), + ExpectedPostErrorCode = GetCodeLines(11, 16) + }, + new ErrorData() + { + AllLines = GetCodeLines(1, 30), + ErrorStartLine = 10, + ErrorEndLine = 13, // multi-line error + ExpectedPreContextLine = 4, + ExpectedPreErrorCode = GetCodeLines(4, 9), + ExpectedErrorCode = GetCodeLines(10, 13), + ExpectedPostErrorCode = GetCodeLines(14, 19) + }, + + // PreErrorCode less than source code line count + new ErrorData() + { + AllLines = GetCodeLines(1, 10), + ErrorStartLine = 1, + ErrorEndLine = 1, + ExpectedPreContextLine = 1, + ExpectedPreErrorCode = Enumerable.Empty<string>(), + ExpectedErrorCode = GetCodeLines(1, 1), + ExpectedPostErrorCode = GetCodeLines(2, 7) + }, + new ErrorData() + { + AllLines = GetCodeLines(1, 10), + ErrorStartLine = 3, + ErrorEndLine = 5, + ExpectedPreContextLine = 1, + ExpectedPreErrorCode = GetCodeLines(1, 2), + ExpectedErrorCode = GetCodeLines(3, 5), + ExpectedPostErrorCode = GetCodeLines(6, 10) + }, + + // PostErrorCode less than source code line count + new ErrorData() + { + AllLines = GetCodeLines(1, 10), + ErrorStartLine = 10, + ErrorEndLine = 10, + ExpectedPreContextLine = 4, + ExpectedPreErrorCode = GetCodeLines(4, 9), + ExpectedErrorCode = GetCodeLines(10, 10), + ExpectedPostErrorCode = Enumerable.Empty<string>() + }, + new ErrorData() + { + AllLines = GetCodeLines(1, 10), + ErrorStartLine = 7, + ErrorEndLine = 10, + ExpectedPreContextLine = 1, + ExpectedPreErrorCode = GetCodeLines(1, 6), + ExpectedErrorCode = GetCodeLines(7, 10), + ExpectedPostErrorCode = Enumerable.Empty<string>() + }, + new ErrorData() + { + AllLines = GetCodeLines(1, 10), + ErrorStartLine = 5, + ErrorEndLine = 8, + ExpectedPreContextLine = 1, + ExpectedPreErrorCode = GetCodeLines(1, 4), + ExpectedErrorCode = GetCodeLines(5, 8), + ExpectedPostErrorCode = GetCodeLines(9, 10) + }, + + // Pre and Post error code less than source code line count + new ErrorData() + { + AllLines = GetCodeLines(1, 4), + ErrorStartLine = 2, + ErrorEndLine = 3, + ExpectedPreContextLine = 1, + ExpectedPreErrorCode = GetCodeLines(1, 1), + ExpectedErrorCode = GetCodeLines(2, 3), + ExpectedPostErrorCode = GetCodeLines(4, 4) + }, + new ErrorData() + { + AllLines = GetCodeLines(1, 4), + ErrorStartLine = 1, + ErrorEndLine = 4, + ExpectedPreContextLine = 1, + ExpectedPreErrorCode = Enumerable.Empty<string>(), + ExpectedErrorCode = GetCodeLines(1, 4), + ExpectedPostErrorCode = Enumerable.Empty<string>() + }, + + // change source code line count + new ErrorData() + { + SourceCodeLineCount = 1, + AllLines = GetCodeLines(1, 1), + ErrorStartLine = 1, + ErrorEndLine = 1, + ExpectedPreContextLine = 1, + ExpectedPreErrorCode = Enumerable.Empty<string>(), + ExpectedErrorCode = GetCodeLines(1, 1), + ExpectedPostErrorCode = Enumerable.Empty<string>() + }, + }; + } + } + + [Theory] + [MemberData(nameof(DisplaysSourceCodeLines_PreAndPostErrorLineData))] + public void DisplaysSourceCodeLines_PreAndPostErrorLine(ErrorData errorData) + { + // Arrange + var middleware = GetErrorPageMiddleware(); + var stackFrame = new StackFrame(); + + // Act + middleware.ReadFrameContent( + stackFrame, errorData.AllLines, errorData.ErrorStartLine, errorData.ErrorEndLine); + + // Assert + Assert.Equal(errorData.ExpectedPreContextLine, stackFrame.PreContextLine); + Assert.Equal(errorData.ExpectedPreErrorCode, stackFrame.PreContextCode); + Assert.Equal(errorData.ExpectedErrorCode, stackFrame.ContextCode); + Assert.Equal(errorData.ExpectedPostErrorCode, stackFrame.PostContextCode); + } + + private static IEnumerable<string> GetCodeLines(int fromLine, int toLine) + { + var start = fromLine; + var count = toLine - fromLine + 1; + return Enumerable.Range(start, count).Select(i => string.Format("Line{0}", i)); + } + + private ErrorPageMiddleware GetErrorPageMiddleware( + IFileProvider fileProvider = null, int sourceCodeLineCount = 6) + { + var errorPageOptions = new ErrorPageOptions(); + errorPageOptions.SourceCodeLineCount = sourceCodeLineCount; + + if (fileProvider != null) + { + errorPageOptions.FileProvider = fileProvider; + } + + var middleware = new ErrorPageMiddleware( + (httpContext) => { return Task.FromResult(0); }, + errorPageOptions, + new LoggerFactory(), + new TestApplicationEnvironment()); + + return middleware; + } + + private class TestApplicationEnvironment : IApplicationEnvironment + { + public string ApplicationBasePath + { + get + { + return Directory.GetCurrentDirectory(); + } + } + + public string ApplicationName + { + get + { + throw new NotImplementedException(); + } + } + + public string ApplicationVersion + { + get + { + throw new NotImplementedException(); + } + } + + public string Configuration + { + get + { + throw new NotImplementedException(); + } + } + + public FrameworkName RuntimeFramework + { + get + { + throw new NotImplementedException(); + } + } + + public string Version + { + get + { + throw new NotImplementedException(); + } + } + + public object GetData(string name) + { + throw new NotImplementedException(); + } + + public void SetData(string name, object value) + { + throw new NotImplementedException(); + } + } + + private class TestFileProvider : IFileProvider + { + private readonly IEnumerable<string> _sourceCodeLines; + + public TestFileProvider(IEnumerable<string> sourceCodeLines) + { + _sourceCodeLines = sourceCodeLines; + } + + public IDirectoryContents GetDirectoryContents(string subpath) + { + throw new NotImplementedException(); + } + + public IFileInfo GetFileInfo(string subpath) + { + return new TestFileInfo(_sourceCodeLines); + } + + public IExpirationTrigger Watch(string filter) + { + throw new NotImplementedException(); + } + } + + private class TestFileInfo : IFileInfo + { + private readonly MemoryStream _stream; + + public TestFileInfo(IEnumerable<string> sourceCodeLines) + { + _stream = new MemoryStream(); + using (var writer = new StreamWriter(_stream, Encoding.UTF8, 1024, leaveOpen: true)) + { + foreach (var line in sourceCodeLines) + { + writer.WriteLine(line); + } + } + _stream.Seek(0, SeekOrigin.Begin); + } + + public bool Exists + { + get + { + return true; + } + } + + public bool IsDirectory + { + get + { + throw new NotImplementedException(); + } + } + + public DateTimeOffset LastModified + { + get + { + throw new NotImplementedException(); + } + } + + public long Length + { + get + { + throw new NotImplementedException(); + } + } + + public string Name + { + get + { + throw new NotImplementedException(); + } + } + + public string PhysicalPath + { + get + { + return null; + } + } + + public Stream CreateReadStream() + { + return _stream; + } + } + + public class ErrorData + { + public int SourceCodeLineCount { get; set; } = 6; + public IEnumerable<string> AllLines { get; set; } + public int ErrorStartLine { get; set; } + public int ErrorEndLine { get; set; } + public int ExpectedPreContextLine { get; set; } + public IEnumerable<string> ExpectedPreErrorCode { get; set; } + public IEnumerable<string> ExpectedErrorCode { get; set; } + public IEnumerable<string> ExpectedPostErrorCode { get; set; } + } + } +} diff --git a/test/Microsoft.AspNet.Diagnostics.Tests/Resources/TestFiles/EmbeddedSourceFile.txt b/test/Microsoft.AspNet.Diagnostics.Tests/Resources/TestFiles/EmbeddedSourceFile.txt new file mode 100644 index 0000000000..93c63d5245 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Tests/Resources/TestFiles/EmbeddedSourceFile.txt @@ -0,0 +1,30 @@ +Line1 +Line2 +Line3 +Line4 +Line5 +Line6 +Line7 +Line8 +Line9 +Line10 +Line11 +Line12 +Line13 +Line14 +Line15 +Line16 +Line17 +Line18 +Line19 +Line20 +Line21 +Line22 +Line23 +Line24 +Line25 +Line26 +Line27 +Line28 +Line29 +Line30 \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Tests/TestFiles/SourceFile.txt b/test/Microsoft.AspNet.Diagnostics.Tests/TestFiles/SourceFile.txt new file mode 100644 index 0000000000..93c63d5245 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Tests/TestFiles/SourceFile.txt @@ -0,0 +1,30 @@ +Line1 +Line2 +Line3 +Line4 +Line5 +Line6 +Line7 +Line8 +Line9 +Line10 +Line11 +Line12 +Line13 +Line14 +Line15 +Line16 +Line17 +Line18 +Line19 +Line20 +Line21 +Line22 +Line23 +Line24 +Line25 +Line26 +Line27 +Line28 +Line29 +Line30 \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Tests/project.json b/test/Microsoft.AspNet.Diagnostics.Tests/project.json index 3e2f021b66..8c5fe2b045 100644 --- a/test/Microsoft.AspNet.Diagnostics.Tests/project.json +++ b/test/Microsoft.AspNet.Diagnostics.Tests/project.json @@ -4,6 +4,7 @@ }, "dependencies": { "Microsoft.AspNet.Diagnostics.Elm": "1.0.0-*", + "Microsoft.AspNet.FileProviders.Embedded": "1.0.0-*", "Microsoft.AspNet.TestHost": "1.0.0-*", "Microsoft.Framework.DependencyInjection": "1.0.0-*", "xunit.runner.aspnet": "2.0.0-aspnet-*" @@ -18,7 +19,10 @@ "dnxcore50": { } }, + "commands": { "test": "xunit.runner.aspnet" - } + }, + + "resource": [ "Resources/**" ] }