From 14fc427068183c5d44fc2488d648157ac66d1335 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 19 Dec 2017 14:09:37 -0800 Subject: [PATCH] Make RazorCodeGen track file renames \ deletes Fixes #1810 --- .../Microsoft.AspNetCore.Razor.Design.targets | 30 +++- .../IntegrationTests/Assert.cs | 4 +- .../IntegrationTests/FIleThumbPrint.cs | 44 ++++++ .../MSBuildIntegrationTestBase.cs | 34 +++++ .../IntegrationTests/ProjectDirectory.cs | 21 ++- .../RazorGenerateIntegrationTest.cs | 128 +++++++++++++++++- ...rosoft.AspNetCore.Razor.Design.Test.csproj | 2 +- ...osoft.AspNetCore.Razor.Test.MvcShim.csproj | 2 +- 8 files changed, 255 insertions(+), 10 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/FIleThumbPrint.cs diff --git a/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.targets b/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.targets index 319ab4f87c..2696535783 100644 --- a/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.targets +++ b/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.targets @@ -91,10 +91,34 @@ + + + <_RazorSourcesHashFile>$(IntermediateOutputPath)$(MSBuildProjectName).RazorSourceHash.cache + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/Assert.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/Assert.cs index b8a00f7d3b..9373f3aef3 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/Assert.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/Assert.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests if (result == null) { throw new ArgumentNullException(nameof(result)); - }; + } if (result.ExitCode != 0) { @@ -259,4 +259,4 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests protected override string Heading => $"File: '{FilePath}' was found, but should not exist."; } } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/FIleThumbPrint.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/FIleThumbPrint.cs new file mode 100644 index 0000000000..767f0242b6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/FIleThumbPrint.cs @@ -0,0 +1,44 @@ +// 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.IO; +using System.Security.Cryptography; + +namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests +{ + public class FileThumbPrint : IEquatable + { + private FileThumbPrint(DateTime lastWriteTimeUtc, string hash) + { + LastWriteTimeUtc = lastWriteTimeUtc; + Hash = hash; + } + + public DateTime LastWriteTimeUtc { get; } + + public string Hash { get; } + + public static FileThumbPrint Create(string path) + { + byte[] hashBytes; + using (var sha1 = SHA1.Create()) + using (var fileStream = File.OpenRead(path)) + { + hashBytes = sha1.ComputeHash(fileStream); + } + + var hash = Convert.ToBase64String(hashBytes); + var lastWriteTimeUtc = File.GetLastWriteTimeUtc(path); + return new FileThumbPrint(lastWriteTimeUtc, hash); + } + + public bool Equals(FileThumbPrint other) + { + return LastWriteTimeUtc == other.LastWriteTimeUtc && + string.Equals(Hash, other.Hash, StringComparison.Ordinal); + } + + public override int GetHashCode() => LastWriteTimeUtc.GetHashCode(); + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/MSBuildIntegrationTestBase.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/MSBuildIntegrationTestBase.cs index 6da2ffd0e8..dd051029b3 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/MSBuildIntegrationTestBase.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/MSBuildIntegrationTestBase.cs @@ -2,10 +2,12 @@ // 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; using System.Threading; using System.Threading.Tasks; +using Moq; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { @@ -68,5 +70,37 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests File.WriteAllText(filePath, content, Encoding.UTF8); } + + /// + /// Locks all files, discovered at the time of method invocation, under the + /// specified from reads or writes. + /// + public IDisposable LockDirectory(string directory) + { + directory = Path.Combine(Project.DirectoryPath, directory); + var disposables = new List(); + foreach (var file in Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)) + { + disposables.Add(LockFile(file)); + } + + var disposable = new Mock(); + disposable.Setup(d => d.Dispose()) + .Callback(() => disposables.ForEach(d => d.Dispose())); + + return disposable.Object; + } + + public IDisposable LockFile(string path) + { + path = Path.Combine(Project.DirectoryPath, path); + return File.Open(path, FileMode.Open, FileAccess.Read, FileShare.None); + } + + public FileThumbPrint GetThumbPrint(string path) + { + path = Path.Combine(Project.DirectoryPath, path); + return FileThumbPrint.Create(path); + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/ProjectDirectory.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/ProjectDirectory.cs index 2c31984594..2f462313cf 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/ProjectDirectory.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/ProjectDirectory.cs @@ -2,18 +2,27 @@ // 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.Threading; using Microsoft.AspNetCore.Testing; +using Moq; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { internal class ProjectDirectory : IDisposable { +#if PRESERVE_WORKING_DIRECTORY + public bool PreserveWorkingDirectory { get; set; } = true; +#else + public bool PreserveWorkingDirectory { get; set; } +#endif + public static ProjectDirectory Create(string projectName) { - var destinationPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var destinationPath = Path.Combine(Path.GetTempPath(), "Razor", Path.GetRandomFileName()); Directory.CreateDirectory(destinationPath); try @@ -80,6 +89,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests #else #error Unknown Configuration #endif + var text = $@" @@ -113,7 +123,14 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests public void Dispose() { - CleanupDirectory(DirectoryPath); + if (PreserveWorkingDirectory) + { + Console.WriteLine($"Skipping deletion of working directory {DirectoryPath}"); + } + else + { + CleanupDirectory(DirectoryPath); + } } private static void CleanupDirectory(string filePath) diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorGenerateIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorGenerateIntegrationTest.cs index 993e82f018..f64b290644 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorGenerateIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/RazorGenerateIntegrationTest.cs @@ -1,6 +1,9 @@ // 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.IO; +using System.Linq; using System.Threading.Tasks; using Xunit; @@ -8,11 +11,13 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { public class RazorGenerateIntegrationTest : MSBuildIntegrationTestBase { + private const string RazorGenerateTarget = "RazorGenerate"; + [Fact] [InitializeTestProject("SimpleMvc")] public async Task RazorGenerate_Success_GeneratesFilesOnDisk() { - var result = await DotnetMSBuild("RazorGenerate"); + var result = await DotnetMSBuild(RazorGenerateTarget); Assert.BuildPassed(result); @@ -52,5 +57,126 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests // The file should still be generated even if we had a Razor syntax error. Assert.FileExists(result, RazorIntermediateOutputPath, "Views", "Home", "Index.cs"); } + + [Fact] + [InitializeTestProject("SimpleMvc")] + public async Task RazorGenerate_BuildsIncrementally() + { + // Act - 1 + var result = await DotnetMSBuild(RazorGenerateTarget); + var generatedFile = Path.Combine(Project.DirectoryPath, RazorIntermediateOutputPath, "Views", "Home", "About.cs"); + + // Assert - 1 + Assert.BuildPassed(result); + Assert.FileExists(result, generatedFile); + var thumbPrint = GetThumbPrint(generatedFile); + + // Act - 2 + using (var razorGenDirectoryLock = LockDirectory(RazorIntermediateOutputPath)) + { + result = await DotnetMSBuild(RazorGenerateTarget); + } + + // Assert - 2 + Assert.BuildPassed(result); + Assert.FileExists(result, generatedFile); + var currentThumbPrint = GetThumbPrint(generatedFile); + Assert.Equal(thumbPrint, currentThumbPrint); + } + + [Fact] + [InitializeTestProject("SimpleMvc")] + public async Task RazorGenerate_Rebuilds_IfSourcesAreUpdated() + { + // Act - 1 + var result = await DotnetMSBuild(RazorGenerateTarget); + var file = Path.Combine(Project.DirectoryPath, "Views", "Home", "Contact.cshtml"); + var generatedFile = Path.Combine(RazorIntermediateOutputPath, "Views", "Home", "Contact.cs"); + + // Assert - 1 + Assert.BuildPassed(result); + var fileThumbPrint = GetThumbPrint(generatedFile); + + // Act - 2 + // Update the source content and build. We should expect the outputs to be regenerated. + // Timestamps on xplat are precise only to a second. Add a delay so we can ensure that MSBuild recognizes the + // file change. See https://github.com/dotnet/corefx/issues/26024 + await Task.Delay(TimeSpan.FromSeconds(1)); + ReplaceContent("Uodated content", file); + result = await DotnetMSBuild(RazorGenerateTarget); + + // Assert - 2 + Assert.BuildPassed(result); + var newThumbPrint = GetThumbPrint(generatedFile); + Assert.NotEqual(fileThumbPrint, newThumbPrint); + } + + [Fact] + [InitializeTestProject("SimpleMvc")] + public async Task RazorGenerate_Rebuilds_IfOutputFilesAreMissing() + { + // Act - 1 + var result = await DotnetMSBuild(RazorGenerateTarget); + var file = Path.Combine(Project.DirectoryPath, RazorIntermediateOutputPath, "Views", "Home", "About.cs"); + + // Assert - 1 + Assert.BuildPassed(result); + Assert.FileExists(result, file); + + // Act - 2 + File.Delete(file); + result = await DotnetMSBuild(RazorGenerateTarget); + + // Assert - 2 + Assert.BuildPassed(result); + Assert.FileExists(result, file); + } + + [Fact] + [InitializeTestProject("SimpleMvc")] + public async Task RazorGenerate_Rebuilds_IfInputFilesAreRenamed() + { + // Act - 1 + var result = await DotnetMSBuild(RazorGenerateTarget); + var file = Path.Combine(Project.DirectoryPath, "Views", "Home", "Index.cshtml"); + var renamed = Path.Combine(Project.DirectoryPath, "Views", "Home", "NewIndex.cshtml"); + var generated = Path.Combine(RazorIntermediateOutputPath, "Views", "Home", "Index.cs"); + + // Assert - 1 + Assert.BuildPassed(result); + Assert.FileExists(result, file); + Assert.FileExists(result, generated); + + // Act - 2 + File.Move(file, renamed); + result = await DotnetMSBuild(RazorGenerateTarget); + + // Assert - 2 + Assert.BuildPassed(result); + Assert.FileExists(result, RazorIntermediateOutputPath, "Views", "Home", "NewIndex.cs"); + Assert.FileDoesNotExist(result, generated); + } + + [Fact] + [InitializeTestProject("SimpleMvc")] + public async Task RazorGenerate_Rebuilds_IfInputFilesAreDeleted() + { + // Act - 1 + var result = await DotnetMSBuild(RazorGenerateTarget); + var file = Path.Combine(Project.DirectoryPath, "Views", "Home", "Index.cshtml"); + var generatedFile = Path.Combine(RazorIntermediateOutputPath, "Views", "Home", "Index.cs"); + + // Assert - 1 + Assert.BuildPassed(result); + Assert.FileExists(result, generatedFile); + + // Act - 2 + File.Delete(file); + result = await DotnetMSBuild(RazorGenerateTarget); + + // Assert - 2 + Assert.BuildPassed(result); + Assert.FileDoesNotExist(result, generatedFile); + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/Microsoft.AspNetCore.Razor.Design.Test.csproj b/test/Microsoft.AspNetCore.Razor.Design.Test/Microsoft.AspNetCore.Razor.Design.Test.csproj index 5493ad8adc..4377052fc5 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/Microsoft.AspNetCore.Razor.Design.Test.csproj +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/Microsoft.AspNetCore.Razor.Design.Test.csproj @@ -9,7 +9,7 @@ --> netcoreapp2.0 true - + $(DefineConstants);PRESERVE_WORKING_DIRECTORY true diff --git a/test/Microsoft.AspNetCore.Razor.Test.MvcShim/Microsoft.AspNetCore.Razor.Test.MvcShim.csproj b/test/Microsoft.AspNetCore.Razor.Test.MvcShim/Microsoft.AspNetCore.Razor.Test.MvcShim.csproj index 9583421ebb..0fe0b6a3fc 100644 --- a/test/Microsoft.AspNetCore.Razor.Test.MvcShim/Microsoft.AspNetCore.Razor.Test.MvcShim.csproj +++ b/test/Microsoft.AspNetCore.Razor.Test.MvcShim/Microsoft.AspNetCore.Razor.Test.MvcShim.csproj @@ -1,7 +1,7 @@  - $(StandardTestTfms);netstandard2.0 + $(StandardTestTfms) true