From 6d1ff3af8c0e891707312806a001c3f91df1af5e Mon Sep 17 00:00:00 2001 From: Mihai Codoban Date: Tue, 26 Jan 2016 14:53:56 -0800 Subject: [PATCH] Allow false positive rebuilds when timestamp collision occurs Fixes #965 --- .../commands/dotnet-build/CompileContext.cs | 2 +- .../Assertions/CommandResultAssertions.cs | 14 +++++ .../Assertions/CommandResultExtensions.cs | 5 -- .../BuildProjectToProjectTests.cs | 7 ++- .../dotnet-build.Tests/IncrementalTestBase.cs | 42 +++++--------- test/dotnet-build.Tests/IncrementalTests.cs | 57 ++++++++++++++----- 6 files changed, 76 insertions(+), 51 deletions(-) diff --git a/src/dotnet/commands/dotnet-build/CompileContext.cs b/src/dotnet/commands/dotnet-build/CompileContext.cs index 8086ac62d..cc6efee93 100644 --- a/src/dotnet/commands/dotnet-build/CompileContext.cs +++ b/src/dotnet/commands/dotnet-build/CompileContext.cs @@ -130,7 +130,7 @@ namespace Microsoft.DotNet.Tools.Build } // find inputs that are older than the earliest output - var newInputs = compilerIO.Inputs.FindAll(p => File.GetLastWriteTimeUtc(p) > minDateUtc); + var newInputs = compilerIO.Inputs.FindAll(p => File.GetLastWriteTimeUtc(p) >= minDateUtc); if (!newInputs.Any()) { diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultAssertions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultAssertions.cs index cd3cdf4a8..70b452596 100644 --- a/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultAssertions.cs +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultAssertions.cs @@ -89,6 +89,20 @@ namespace Microsoft.DotNet.Tools.Test.Utilities $"Exit Code: {_commandResult.ExitCode}{Environment.NewLine}" + $"StdOut:{Environment.NewLine}{_commandResult.StdOut}{Environment.NewLine}" + $"StdErr:{Environment.NewLine}{_commandResult.StdErr}{Environment.NewLine}"; ; + } + + public AndConstraint HaveSkippedProjectCompilation(string skippedProject) + { + _commandResult.StdOut.Should().Contain($"Project {skippedProject} (DNXCore,Version=v5.0) was previously compiled. Skipping compilation."); + + return new AndConstraint(this); + } + + public AndConstraint HaveCompiledProject(string compiledProject) + { + _commandResult.StdOut.Should().Contain($"Project {compiledProject} (DNXCore,Version=v5.0) will be compiled"); + + return new AndConstraint(this); } } } diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultExtensions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultExtensions.cs index d29ab52b2..ad3bf23c3 100644 --- a/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultExtensions.cs +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultExtensions.cs @@ -2,11 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.DotNet.Cli.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FluentAssertions; namespace Microsoft.DotNet.Tools.Test.Utilities { diff --git a/test/dotnet-build.Tests/BuildProjectToProjectTests.cs b/test/dotnet-build.Tests/BuildProjectToProjectTests.cs index bf78f3828..7c4f967e4 100644 --- a/test/dotnet-build.Tests/BuildProjectToProjectTests.cs +++ b/test/dotnet-build.Tests/BuildProjectToProjectTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Tools.Test.Utilities; using Xunit; namespace Microsoft.DotNet.Tools.Builder.Tests @@ -59,18 +60,18 @@ namespace Microsoft.DotNet.Tools.Builder.Tests { foreach (var rebuiltProject in expectedRebuilt) { - AssertProjectCompiled(rebuiltProject, buildResult); + buildResult.Should().HaveCompiledProject(rebuiltProject); } foreach (var skippedProject in SetDifference(_projects, expectedRebuilt)) { - AssertProjectSkipped(skippedProject, buildResult); + buildResult.Should().HaveSkippedProjectCompilation(skippedProject); } } protected override string GetProjectDirectory(string projectName) { - return Path.Combine(_tempProjectRoot.Path, "src", projectName); + return Path.Combine(TempProjectRoot.Path, "src", projectName); } } } \ No newline at end of file diff --git a/test/dotnet-build.Tests/IncrementalTestBase.cs b/test/dotnet-build.Tests/IncrementalTestBase.cs index 0fdb0b01d..932858ff6 100644 --- a/test/dotnet-build.Tests/IncrementalTestBase.cs +++ b/test/dotnet-build.Tests/IncrementalTestBase.cs @@ -13,26 +13,24 @@ namespace Microsoft.DotNet.Tools.Builder.Tests { public class IncrementalTestBase : TestBase { - protected readonly TempDirectory _tempProjectRoot; + protected readonly TempDirectory TempProjectRoot; - private readonly string _testProjectsRoot; - protected readonly string _mainProject; - protected readonly string _expectedOutput; + protected readonly string MainProject; + protected readonly string ExpectedOutput; public IncrementalTestBase(string testProjectsRoot, string mainProject, string expectedOutput) { - _testProjectsRoot = testProjectsRoot; - _mainProject = mainProject; - _expectedOutput = expectedOutput; + MainProject = mainProject; + ExpectedOutput = expectedOutput; var root = Temp.CreateDirectory(); - _tempProjectRoot = root.CopyDirectory(testProjectsRoot); + TempProjectRoot = root.CopyDirectory(testProjectsRoot); } protected void TouchSourcesOfProject() { - TouchSourcesOfProject(_mainProject); + TouchSourcesOfProject(MainProject); } protected void TouchSourcesOfProject(string projectToTouch) @@ -50,9 +48,9 @@ namespace Microsoft.DotNet.Tools.Builder.Tests protected CommandResult BuildProject(bool forceIncrementalUnsafe = false, bool expectBuildFailure = false) { - var outputDir = GetBinDirectory(); - var intermediateOutputDir = Path.Combine(Directory.GetParent(outputDir).FullName, "obj", _mainProject); - var mainProjectFile = GetProjectFile(_mainProject); + var outputDir = GetBinRoot(); + var intermediateOutputDir = Path.Combine(Directory.GetParent(outputDir).FullName, "obj", MainProject); + var mainProjectFile = GetProjectFile(MainProject); var buildCommand = new BuildCommand(mainProjectFile, output: outputDir, tempOutput: intermediateOutputDir ,forceIncrementalUnsafe : forceIncrementalUnsafe); var result = buildCommand.ExecuteWithCapturedOutput(); @@ -60,7 +58,7 @@ namespace Microsoft.DotNet.Tools.Builder.Tests if (!expectBuildFailure) { result.Should().Pass(); - TestOutputExecutable(outputDir, buildCommand.GetOutputExecutableName(), _expectedOutput); + TestOutputExecutable(outputDir, buildCommand.GetOutputExecutableName(), ExpectedOutput); } else { @@ -70,24 +68,14 @@ namespace Microsoft.DotNet.Tools.Builder.Tests return result; } - protected static void AssertProjectSkipped(string skippedProject, CommandResult buildResult) + protected string GetBinRoot() { - Assert.Contains($"Project {skippedProject} (DNXCore,Version=v5.0) was previously compiled. Skipping compilation.", buildResult.StdOut, StringComparison.OrdinalIgnoreCase); - } - - protected static void AssertProjectCompiled(string rebuiltProject, CommandResult buildResult) - { - Assert.Contains($"Project {rebuiltProject} (DNXCore,Version=v5.0) will be compiled", buildResult.StdOut, StringComparison.OrdinalIgnoreCase); - } - - protected string GetBinDirectory() - { - return Path.Combine(_tempProjectRoot.Path, "bin"); + return Path.Combine(TempProjectRoot.Path, "bin"); } protected virtual string GetProjectDirectory(string projectName) { - return Path.Combine(_tempProjectRoot.Path); + return Path.Combine(TempProjectRoot.Path); } protected string GetProjectFile(string projectName) @@ -108,7 +96,7 @@ namespace Microsoft.DotNet.Tools.Builder.Tests protected string GetCompilationOutputPath() { - var executablePath = Path.Combine(GetBinDirectory(), "Debug", "dnxcore50"); + var executablePath = Path.Combine(GetBinRoot(), "Debug", "dnxcore50"); return executablePath; } diff --git a/test/dotnet-build.Tests/IncrementalTests.cs b/test/dotnet-build.Tests/IncrementalTests.cs index 9defc1cbd..6178011b3 100644 --- a/test/dotnet-build.Tests/IncrementalTests.cs +++ b/test/dotnet-build.Tests/IncrementalTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.DotNet.Cli.Utils; @@ -25,7 +24,7 @@ namespace Microsoft.DotNet.Tools.Builder.Tests public void TestForceIncrementalUnsafe() { var buildResult = BuildProject(); - AssertProjectCompiled(_mainProject, buildResult); + buildResult.Should().HaveCompiledProject(MainProject); buildResult = BuildProject(forceIncrementalUnsafe: true); Assert.Contains("[Forced Unsafe]", buildResult.StdOut); @@ -54,9 +53,9 @@ namespace Microsoft.DotNet.Tools.Builder.Tests { var buildResult = BuildProject(); - AssertProjectCompiled(_mainProject, buildResult); + buildResult.Should().HaveCompiledProject(MainProject); - var lockFile = Path.Combine(_tempProjectRoot.Path, "project.lock.json"); + var lockFile = Path.Combine(TempProjectRoot.Path, "project.lock.json"); Assert.True(File.Exists(lockFile)); File.Delete(lockFile); @@ -66,38 +65,66 @@ namespace Microsoft.DotNet.Tools.Builder.Tests Assert.Contains("does not have a lock file", buildResult.StdOut); } - [Fact(Skip="https://github.com/dotnet/cli/issues/980")] + [Fact] public void TestRebuildChangedLockFile() { var buildResult = BuildProject(); - AssertProjectCompiled(_mainProject, buildResult); + buildResult.Should().HaveCompiledProject(MainProject); - var lockFile = Path.Combine(_tempProjectRoot.Path, "project.lock.json"); + var lockFile = Path.Combine(TempProjectRoot.Path, "project.lock.json"); TouchFile(lockFile); buildResult = BuildProject(); - AssertProjectCompiled(_mainProject, buildResult); + buildResult.Should().HaveCompiledProject(MainProject); } - [Fact(Skip="https://github.com/dotnet/cli/issues/980")] + [Fact] public void TestRebuildChangedProjectFile() { var buildResult = BuildProject(); - AssertProjectCompiled(_mainProject, buildResult); + buildResult.Should().HaveCompiledProject(MainProject); - TouchFile(GetProjectFile(_mainProject)); + TouchFile(GetProjectFile(MainProject)); buildResult = BuildProject(); - AssertProjectCompiled(_mainProject, buildResult); + buildResult.Should().HaveCompiledProject(MainProject); + } + + // regression for https://github.com/dotnet/cli/issues/965 + [Fact] + public void TestInputWithSameTimeAsOutputCausesProjectToCompile() + { + var buildResult = BuildProject(); + buildResult.Should().HaveCompiledProject(MainProject); + + var outputTimestamp = SetAllOutputItemsToSameTime(); + + // set an input to have the same last write time as an output item + // this should trigger recompilation to account for file systems with second timestamp granularity + // (an input file that changed within the same second as the previous outputs should trigger a rebuild) + File.SetLastWriteTime(GetProjectFile(MainProject), outputTimestamp); + + buildResult = BuildProject(); + buildResult.Should().HaveCompiledProject(MainProject); + } + + private DateTime SetAllOutputItemsToSameTime() + { + var now = DateTime.Now; + foreach (var f in Directory.EnumerateFiles(GetCompilationOutputPath())) + { + File.SetLastWriteTime(f, now); + } + return now; } private void TestDeleteOutputWithExtension(string extension) { var buildResult = BuildProject(); - AssertProjectCompiled(_mainProject, buildResult); + buildResult.Should().HaveCompiledProject(MainProject); Reporter.Verbose.WriteLine($"Files in {GetCompilationOutputPath()}"); foreach (var file in Directory.EnumerateFiles(GetCompilationOutputPath())) @@ -109,7 +136,7 @@ namespace Microsoft.DotNet.Tools.Builder.Tests foreach (var outputFile in Directory.EnumerateFiles(GetCompilationOutputPath()).Where(f => { var fileName = Path.GetFileName(f); - return fileName.StartsWith(_mainProject, StringComparison.OrdinalIgnoreCase) && + return fileName.StartsWith(MainProject, StringComparison.OrdinalIgnoreCase) && fileName.EndsWith(extension, StringComparison.OrdinalIgnoreCase); })) { @@ -121,7 +148,7 @@ namespace Microsoft.DotNet.Tools.Builder.Tests // second build; should get rebuilt since we deleted an output item buildResult = BuildProject(); - AssertProjectCompiled(_mainProject, buildResult); + buildResult.Should().HaveCompiledProject(MainProject); } } } \ No newline at end of file