From 3f948ad3c5cd7c0745ba5f1580acd2b8b52f72fc Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 20 Dec 2017 16:08:35 -0800 Subject: [PATCH] Verify support for reporting errors using MSBuild (#1867) * Verify support for reporting errors using MSBuild * Verifies that CSC errors get mapped to the right place Resolves #1850 --- .../RunCommand.cs | 3 +- .../IntegrationTests/Assert.cs | 79 ++++++++++++++++++- .../IntegrationTests/BuildIntegrationTest.cs | 19 +++++ .../MSBuildIntegrationTestBase.cs | 22 ++++++ .../RazorGenerateIntegrationTest.cs | 24 +++++- 5 files changed, 140 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.AspNetCore.Razor.GenerateTool/RunCommand.cs b/src/Microsoft.AspNetCore.Razor.GenerateTool/RunCommand.cs index 9ce95b5f14..479d71e708 100644 --- a/src/Microsoft.AspNetCore.Razor.GenerateTool/RunCommand.cs +++ b/src/Microsoft.AspNetCore.Razor.GenerateTool/RunCommand.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; @@ -60,7 +59,7 @@ namespace Microsoft.AspNetCore.Razor.GenerateTool success = false; foreach (var error in result.CSharpDocument.Diagnostics) { - Console.Error.WriteLine(error.GetMessage()); + Console.Error.WriteLine(error.ToString()); } } diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/Assert.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/Assert.cs index 9439792abd..b8a00f7d3b 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/Assert.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/Assert.cs @@ -3,14 +3,17 @@ using System; using System.IO; -using System.Linq; -using System.Runtime.Serialization; using System.Text; +using System.Text.RegularExpressions; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { internal class Assert : Xunit.Assert { + // Matches `{filename}: error {code}: {message} [{project}] + // See https://stackoverflow.com/questions/3441452/msbuild-and-ignorestandarderrorwarningformat/5180353#5180353 + private static readonly Regex ErrorRegex = new Regex(@"^(?'location'.+): error (?'errorcode'[A-Z0-9]+): (?'message'.+) \[(?'project'.+)\]$"); + public static void BuildPassed(MSBuildResult result) { if (result == null) @@ -24,6 +27,52 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests } } + public static void BuildError(MSBuildResult result, string errorCode, string location = null) + { + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + // We don't really need to search line by line, I'm doing this so that it's possible/easy to debug. + var lines = result.Output.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + for (var i = 0; i < lines.Length; i++) + { + var line = lines[i]; + var match = ErrorRegex.Match(line); + if (match.Success) + { + if (match.Groups["errorcode"].Value != errorCode) + { + continue; + } + + if (location != null && match.Groups["location"].Value != location) + { + continue; + } + + // This is a match + return; + } + } + + throw new BuildErrorMissingException(result, errorCode); + } + + public static void BuildFailed(MSBuildResult result) + { + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + }; + + if (result.ExitCode == 0) + { + throw new BuildPassedException(result); + } + } + public static void FileExists(MSBuildResult result, params string[] paths) { if (result == null) @@ -106,6 +155,20 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests } } + private class BuildErrorMissingException : MSBuildXunitException + { + public BuildErrorMissingException(MSBuildResult result, string errorCode) + : base(result) + { + ErrorCode = errorCode; + } + + public string ErrorCode { get; } + + protected override string Heading => $"Error code '{ErrorCode}' was not found."; + } + + private class BuildFailedException : MSBuildXunitException { public BuildFailedException(MSBuildResult result) @@ -113,7 +176,17 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { } - protected override string Heading => "Build failed:"; + protected override string Heading => "Build failed."; + } + + private class BuildPassedException : MSBuildXunitException + { + public BuildPassedException(MSBuildResult result) + : base(result) + { + } + + protected override string Heading => "Build should have failed, but it passed."; } private class FileMissingException : MSBuildXunitException diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildIntegrationTest.cs index 33a615fc05..4f626ad30e 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildIntegrationTest.cs @@ -19,5 +19,24 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.FileExists(result, OutputPath, "SimpleMvc.dll"); Assert.FileExists(result, OutputPath, "SimpleMvc.PrecompiledViews.dll"); } + + [Fact] + [InitializeTestProject("SimpleMvc")] + public async Task Build_ErrorInGeneratedCode_ReportsMSBuildError() + { + // Introducing a C# semantic error + ReplaceContent("@{ var foo = \"\".Substring(\"bleh\"); }", "Views", "Home", "Index.cshtml"); + + var result = await DotnetMSBuild("Build", "/p:RazorCompileOnBuild=true"); + + Assert.BuildFailed(result); + + // Verifying that the error correctly gets mapped to the original source + Assert.BuildError(result, "CS1503", location: Path.Combine("Views", "Home", "Index.cshtml") + "(1,27)"); + + // Compilation failed without creating the views assembly + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.dll"); + Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/MSBuildIntegrationTestBase.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/MSBuildIntegrationTestBase.cs index 6ce4634d9d..6da2ffd0e8 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/MSBuildIntegrationTestBase.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/MSBuildIntegrationTestBase.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -46,5 +47,26 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests return MSBuildProcessManager.RunProcessAsync(Project, $"{restoreArgument} /t:{target} /p:Configuration={Configuration} {args}", timeout); } + + internal void ReplaceContent(string content, params string[] paths) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (paths == null) + { + throw new ArgumentNullException(nameof(paths)); + } + + var filePath = Path.Combine(Project.DirectoryPath, Path.Combine(paths)); + if (!File.Exists(filePath)) + { + throw new InvalidOperationException($"File {filePath} could not be found."); + } + + File.WriteAllText(filePath, content, Encoding.UTF8); + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorGenerateIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorGenerateIntegrationTest.cs index 0b40241929..993e82f018 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorGenerateIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorGenerateIntegrationTest.cs @@ -1,8 +1,6 @@ // 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.IO; -using System.Linq; using System.Threading.Tasks; using Xunit; @@ -32,5 +30,27 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.FileExists(result, RazorIntermediateOutputPath, "Views", "Shared", "Error.cs"); Assert.FileCountEquals(result, 8, RazorIntermediateOutputPath, "*.cs"); } + + [Fact] + [InitializeTestProject("SimpleMvc")] + public async Task RazorGenerate_ErrorInRazorFile_ReportsMSBuildError() + { + // Introducing a syntax error, an unclosed brace + ReplaceContent("@{", "Views", "Home", "Index.cshtml"); + + var result = await DotnetMSBuild("RazorGenerate"); + + Assert.BuildFailed(result); + + // Looks like C:\...\Views\Home\Index.cshtml(1,2): error RZ1006: The code block is missi... [C:\Users\rynowak\AppData\Local\Temp\rwnv03ll.wb0\SimpleMvc.csproj] + Assert.BuildError(result, "RZ1006"); + + // RazorGenerate should compile the assembly, but not the views. + Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.dll"); + Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.PrecompiledViews.dll"); + + // The file should still be generated even if we had a Razor syntax error. + Assert.FileExists(result, RazorIntermediateOutputPath, "Views", "Home", "Index.cs"); + } } }