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