From 6d3bd33c64f09b5014cffc071c6f083a2dfccc17 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 3 Mar 2016 19:02:48 -0800 Subject: [PATCH] Revive DefaultRoslynCompilationServiceTest Partially fixes #4140 --- .../DefaultRoslynCompilationService.cs | 64 +++- .../DefaultRoslynCompilationServiceTest.cs | 313 ++++++++++++++++++ .../project.json | 1 + 3 files changed, 364 insertions(+), 14 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs index 2f1736486f..a3e1879797 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs @@ -35,8 +35,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal private readonly Lazy _supportsPdbGeneration = new Lazy(SymbolsUtility.SupportsSymbolsGeneration); private readonly ConcurrentDictionary _metadataFileCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - - private readonly IApplicationEnvironment _environment; private readonly IFileProvider _fileProvider; private readonly Lazy> _applicationReferences; private readonly Action _compilationCallback; @@ -61,21 +59,33 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal IOptions optionsAccessor, IRazorViewEngineFileProviderAccessor fileProviderAccessor, ILoggerFactory loggerFactory) + : this( + GetDependencyContext(environment), + optionsAccessor.Value, + fileProviderAccessor, + loggerFactory) { - _environment = environment; + } + + // Internal for unit testing + internal DefaultRoslynCompilationService( + DependencyContext dependencyContext, + RazorViewEngineOptions viewEngineOptions, + IRazorViewEngineFileProviderAccessor fileProviderAccessor, + ILoggerFactory loggerFactory) + { + _dependencyContext = dependencyContext; _applicationReferences = new Lazy>(GetApplicationReferences); _fileProvider = fileProviderAccessor.FileProvider; - _compilationCallback = optionsAccessor.Value.CompilationCallback; - _parseOptions = optionsAccessor.Value.ParseOptions; - _compilationOptions = optionsAccessor.Value.CompilationOptions; + _compilationCallback = viewEngineOptions.CompilationCallback; + _parseOptions = viewEngineOptions.ParseOptions; + _compilationOptions = viewEngineOptions.CompilationOptions; _logger = loggerFactory.CreateLogger(); #if NETSTANDARD1_5 _razorLoadContext = new RazorLoadContext(); #endif - var applicationAssembly = Assembly.Load(new AssemblyName(environment.ApplicationName)); - _dependencyContext = DependencyContext.Load(applicationAssembly); } /// @@ -186,7 +196,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal return compilation.RemoveAllSyntaxTrees().AddSyntaxTrees(rewrittenTrees); } - private CompilationResult GetCompilationFailedResult( + // Internal for unit testing + internal CompilationResult GetCompilationFailedResult( string relativePath, string compilationContent, string assemblyName, @@ -236,16 +247,30 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal private List GetApplicationReferences() { + var metadataReferences = new List(); if (_dependencyContext == null) { // Avoid null ref if the entry point does not have DependencyContext specified. - return new List(); + return metadataReferences; } - return _dependencyContext.CompileLibraries - .SelectMany(library => library.ResolveReferencePaths()) - .Select(CreateMetadataFileReference) - .ToList(); + for (var i = 0; i < _dependencyContext.CompileLibraries.Count; i++) + { + var library = _dependencyContext.CompileLibraries[i]; + IEnumerable referencePaths; + try + { + referencePaths = library.ResolveReferencePaths(); + } + catch (InvalidOperationException) + { + continue; + } + + metadataReferences.AddRange(referencePaths.Select(CreateMetadataFileReference)); + } + + return metadataReferences; } private MetadataReference CreateMetadataFileReference(string path) @@ -301,6 +326,17 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal mappedLineSpan.EndLinePosition.Character + 1); } + private static DependencyContext GetDependencyContext(IApplicationEnvironment environment) + { + if (environment.ApplicationName != null) + { + var applicationAssembly = Assembly.Load(new AssemblyName(environment.ApplicationName)); + return DependencyContext.Load(applicationAssembly); + } + + return null; + } + #if NETSTANDARD1_5 private class RazorLoadContext : AssemblyLoadContext { diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs new file mode 100644 index 0000000000..758bb1845b --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs @@ -0,0 +1,313 @@ +// 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.Reflection; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.DependencyModel; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public class DefaultRoslynCompilationServiceTest + { + [Fact] + public void Compile_ReturnsCompilationResult() + { + // Arrange + var content = @" +public class MyTestType {}"; + + var compilationService = new DefaultRoslynCompilationService( + GetDependencyContext(), + GetOptions(), + GetFileProviderAccessor(), + NullLoggerFactory.Instance); + var relativeFileInfo = new RelativeFileInfo( + new TestFileInfo { PhysicalPath = "SomePath" }, + "some-relative-path"); + + // Act + var result = compilationService.Compile(relativeFileInfo, content); + + // Assert + Assert.Equal("MyTestType", result.CompiledType.Name); + } + + [Fact] + public void Compile_ReturnsCompilationFailureWithPathsFromLinePragmas() + { + // Arrange + var viewPath = "some-relative-path"; + var fileContent = "test file content"; + var content = $@" +#line 1 ""{viewPath}"" +this should fail"; + var fileProvider = new TestFileProvider(); + var fileInfo = fileProvider.AddFile(viewPath, fileContent); + + var compilationService = new DefaultRoslynCompilationService( + GetDependencyContext(), + GetOptions(), + GetFileProviderAccessor(fileProvider), + NullLoggerFactory.Instance); + var relativeFileInfo = new RelativeFileInfo(fileInfo, "some-relative-path"); + + // Act + var result = compilationService.Compile(relativeFileInfo, content); + + // Assert + Assert.IsType(result); + Assert.Null(result.CompiledType); + var compilationFailure = Assert.Single(result.CompilationFailures); + Assert.Equal(relativeFileInfo.RelativePath, compilationFailure.SourceFilePath); + Assert.Equal(fileContent, compilationFailure.SourceFileContent); + } + + [Fact] + public void Compile_ReturnsGeneratedCodePath_IfLinePragmaIsNotAvailable() + { + // Arrange + var fileContent = "file content"; + var content = @"this should fail"; + + var compilationService = new DefaultRoslynCompilationService( + GetDependencyContext(), + GetOptions(), + GetFileProviderAccessor(), + NullLoggerFactory.Instance); + var relativeFileInfo = new RelativeFileInfo( + new TestFileInfo { Content = fileContent }, + "some-relative-path"); + + // Act + var result = compilationService.Compile(relativeFileInfo, content); + + // Assert + Assert.IsType(result); + Assert.Null(result.CompiledType); + + var compilationFailure = Assert.Single(result.CompilationFailures); + Assert.Equal("Generated Code", compilationFailure.SourceFilePath); + Assert.Equal(content, compilationFailure.SourceFileContent); + } + + [Fact] + public void Compile_DoesNotThrow_IfFileCannotBeRead() + { + // Arrange + var path = "some-relative-path"; + var content = $@" +#line 1 ""{path}"" +this should fail"; + + var mockFileInfo = new Mock(); + mockFileInfo.Setup(f => f.CreateReadStream()) + .Throws(new Exception()); + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, mockFileInfo.Object); + + var compilationService = new DefaultRoslynCompilationService( + GetDependencyContext(), + GetOptions(), + GetFileProviderAccessor(), + NullLoggerFactory.Instance); + var relativeFileInfo = new RelativeFileInfo(mockFileInfo.Object, path); + + // Act + var result = compilationService.Compile(relativeFileInfo, content); + + // Assert + Assert.IsType(result); + Assert.Null(result.CompiledType); + var compilationFailure = Assert.Single(result.CompilationFailures); + Assert.Equal(path, compilationFailure.SourceFilePath); + Assert.Null(compilationFailure.SourceFileContent); + } + + [Fact] + public void Compile_UsesApplicationsCompilationSettings_ForParsingAndCompilation() + { + // Arrange + var content = @" +#if MY_CUSTOM_DEFINE +public class MyCustomDefinedClass {} +#else +public class MyNonCustomDefinedClass {} +#endif +"; + + var options = GetOptions(); + options.ParseOptions = options.ParseOptions.WithPreprocessorSymbols("MY_CUSTOM_DEFINE"); + + var compilationService = new DefaultRoslynCompilationService( + GetDependencyContext(), + options, + GetFileProviderAccessor(), + NullLoggerFactory.Instance); + var relativeFileInfo = new RelativeFileInfo( + new TestFileInfo { PhysicalPath = "SomePath" }, + "some-relative-path"); + + // Act + var result = compilationService.Compile(relativeFileInfo, content); + + // Assert + Assert.NotNull(result.CompiledType); + Assert.Equal("MyCustomDefinedClass", result.CompiledType.Name); + } + + [Fact] + public void GetCompilationFailedResult_ReturnsCompilationResult_WithGroupedMessages() + { + // Arrange + var viewPath = "Views/Home/Index"; + var generatedCodeFileName = "Generated Code"; + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(viewPath, "view-content"); + var options = new RazorViewEngineOptions(); + options.FileProviders.Add(fileProvider); + + var compilationService = new DefaultRoslynCompilationService( + GetDependencyContext(), + options, + GetFileProviderAccessor(fileProvider), + NullLoggerFactory.Instance); + + var assemblyName = "random-assembly-name"; + + var diagnostics = new[] + { + Diagnostic.Create( + GetDiagnosticDescriptor("message-1"), + Location.Create( + viewPath, + new TextSpan(10, 5), + new LinePositionSpan(new LinePosition(10, 1), new LinePosition(10, 2)))), + Diagnostic.Create( + GetDiagnosticDescriptor("message-2"), + Location.Create( + assemblyName, + new TextSpan(1, 6), + new LinePositionSpan(new LinePosition(1, 2), new LinePosition(3, 4)))), + Diagnostic.Create( + GetDiagnosticDescriptor("message-3"), + Location.Create( + viewPath, + new TextSpan(40, 50), + new LinePositionSpan(new LinePosition(30, 5), new LinePosition(40, 12)))), + }; + + // Act + var compilationResult = compilationService.GetCompilationFailedResult( + viewPath, + "compilation-content", + assemblyName, + diagnostics); + + // Assert + Assert.Collection(compilationResult.CompilationFailures, + failure => + { + Assert.Equal(viewPath, failure.SourceFilePath); + Assert.Equal("view-content", failure.SourceFileContent); + Assert.Collection(failure.Messages, + message => + { + Assert.Equal("message-1", message.Message); + Assert.Equal(viewPath, message.SourceFilePath); + Assert.Equal(11, message.StartLine); + Assert.Equal(2, message.StartColumn); + Assert.Equal(11, message.EndLine); + Assert.Equal(3, message.EndColumn); + }, + message => + { + Assert.Equal("message-3", message.Message); + Assert.Equal(viewPath, message.SourceFilePath); + Assert.Equal(31, message.StartLine); + Assert.Equal(6, message.StartColumn); + Assert.Equal(41, message.EndLine); + Assert.Equal(13, message.EndColumn); + }); + }, + failure => + { + Assert.Equal(generatedCodeFileName, failure.SourceFilePath); + Assert.Equal("compilation-content", failure.SourceFileContent); + Assert.Collection(failure.Messages, + message => + { + Assert.Equal("message-2", message.Message); + Assert.Equal(assemblyName, message.SourceFilePath); + Assert.Equal(2, message.StartLine); + Assert.Equal(3, message.StartColumn); + Assert.Equal(4, message.EndLine); + Assert.Equal(5, message.EndColumn); + }); + }); + } + + [Fact] + public void Compile_RunsCallback() + { + var content = "public class MyTestType {}"; + RoslynCompilationContext usedCompilation = null; + + var compilationService = new DefaultRoslynCompilationService( + GetDependencyContext(), + GetOptions(callback: c => usedCompilation = c), + GetFileProviderAccessor(), + NullLoggerFactory.Instance); + + var relativeFileInfo = new RelativeFileInfo( + new TestFileInfo { PhysicalPath = "SomePath" }, + "some-relative-path"); + + // Act + var result = compilationService.Compile(relativeFileInfo, content); + + Assert.NotNull(usedCompilation); + Assert.Single(usedCompilation.Compilation.SyntaxTrees); + } + + private static DiagnosticDescriptor GetDiagnosticDescriptor(string messageFormat) + { + return new DiagnosticDescriptor( + id: "someid", + title: "sometitle", + messageFormat: messageFormat, + category: "some-category", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + } + + private static RazorViewEngineOptions GetOptions(Action callback = null) + { + return new RazorViewEngineOptions + { + CompilationCallback = callback ?? (c => { }), + }; + } + + private IRazorViewEngineFileProviderAccessor GetFileProviderAccessor(IFileProvider fileProvider = null) + { + var options = new Mock(); + options.SetupGet(o => o.FileProvider) + .Returns(fileProvider ?? new TestFileProvider()); + + return options.Object; + } + + private DependencyContext GetDependencyContext() + { + var assembly = typeof(DefaultRoslynCompilationServiceTest).GetTypeInfo().Assembly; + return DependencyContext.Load(assembly); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/project.json b/test/Microsoft.AspNetCore.Mvc.Razor.Test/project.json index d02ef9eb7a..3fefb5da9d 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/project.json +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/project.json @@ -1,5 +1,6 @@ { "compilationOptions": { + "preserveCompilationContext": true, "warningsAsErrors": true, "keyFile": "../../tools/Key.snk" },