Make RazorCodeGen track file renames \ deletes

Fixes #1810
This commit is contained in:
Pranav K 2017-12-19 14:09:37 -08:00
parent 3f948ad3c5
commit 14fc427068
8 changed files with 255 additions and 10 deletions

View File

@ -91,10 +91,34 @@
</ItemGroup>
</Target>
<Target Name="_RazorCreateSourceHashFiles" DependsOnTargets="_RazorResolveSourceFiles">
<PropertyGroup>
<_RazorSourcesHashFile>$(IntermediateOutputPath)$(MSBuildProjectName).RazorSourceHash.cache</_RazorSourcesHashFile>
</PropertyGroup>
<Hash ItemsToHash="@(RazorCompile)">
<Output TaskParameter="HashResult" PropertyName="_RazorSourcesHash" />
</Hash>
<MakeDir
Directories="$(IntermediateOutputPath)"
Condition="!Exists('$(IntermediateOutputPath)')" />
<WriteLinesToFile
Lines="$(_RazorSourcesHash)"
File="$(_RazorSourcesHashFile)"
Overwrite="True"
WriteOnlyWhenDifferent="True" />
<ItemGroup>
<FileWrites Include="$(_RazorSourcesHashFile)" />
</ItemGroup>
</Target>
<Target
Name="RazorCoreGenerate"
DependsOnTargets="_RazorResolveSourceFiles;_RazorResolveTagHelpers"
Inputs="$(MSBuildAllProjects);@(RazorCompile);$(_RazorTagHelperOutputCache)"
DependsOnTargets="_RazorResolveSourceFiles;_RazorCreateSourceHashFiles;_RazorResolveTagHelpers"
Inputs="$(MSBuildAllProjects);$(_RazorSourcesHashFile);@(RazorCompile);$(_RazorTagHelperOutputCache)"
Outputs="@(_RazorGenerated)">
<RemoveDir
@ -235,4 +259,4 @@
DestinationFolder="$(OutDir)"
SkipUnchangedFiles="true"/>
</Target>
</Project>
</Project>

View File

@ -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.";
}
}
}
}

View File

@ -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<FileThumbPrint>
{
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();
}
}

View File

@ -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);
}
/// <summary>
/// Locks all files, discovered at the time of method invocation, under the
/// specified <paramref name="directory" /> from reads or writes.
/// </summary>
public IDisposable LockDirectory(string directory)
{
directory = Path.Combine(Project.DirectoryPath, directory);
var disposables = new List<IDisposable>();
foreach (var file in Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories))
{
disposables.Add(LockFile(file));
}
var disposable = new Mock<IDisposable>();
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);
}
}
}

View File

@ -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 = $@"
<Project>
<PropertyGroup>
@ -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)

View File

@ -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);
}
}
}

View File

@ -9,7 +9,7 @@
-->
<TargetFramework>netcoreapp2.0</TargetFramework>
<PreserveCompilationContext>true</PreserveCompilationContext>
<DefineConstants Condition="'$(PreserveWorkingDirectory)'=='true'">$(DefineConstants);PRESERVE_WORKING_DIRECTORY</DefineConstants>
<!-- Copy references locally so that we can use them in the test. -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms);netstandard2.0</TargetFrameworks>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
<PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>