From 9081c250674c246556be9b41face964d968d6655 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 23 Jul 2024 23:37:30 +0200 Subject: [PATCH] Revert "FunctionalTests: remove GitCommands tests" --- .github/workflows/continuous-integration.yml | 96 +- .../Tests/GitCommands/AddStageTests.cs | 43 + .../GitCommands/CherryPickConflictTests.cs | 104 ++ .../GitCommands/DeleteEmptyFolderTests.cs | 45 + .../Tests/GitCommands/EnumerationMergeTest.cs | 31 + .../Tests/GitCommands/GitCommandsTests.cs | 1117 +++++++++++++++++ .../Tests/GitCommands/GitRepoTests.cs | 656 ++++++++++ .../Tests/GitCommands/MergeConflictTests.cs | 120 ++ .../Tests/GitCommands/RebaseConflictTests.cs | 91 ++ .../Tests/GitCommands/RebaseTests.cs | 118 ++ .../Tests/GitCommands/ResetHardTests.cs | 79 ++ .../Tests/GitCommands/ResetMixedTests.cs | 103 ++ .../Tests/GitCommands/ResetSoftTests.cs | 59 + .../Tests/GitCommands/StatusTests.cs | 88 ++ .../Tests/GitCommands/UpdateIndexTests.cs | 75 ++ .../Tests/GitCommands/UpdateRefTests.cs | 48 + 16 files changed, 2872 insertions(+), 1 deletion(-) create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/AddStageTests.cs create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/CherryPickConflictTests.cs create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/DeleteEmptyFolderTests.cs create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/EnumerationMergeTest.cs create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/GitCommandsTests.cs create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/GitRepoTests.cs create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/MergeConflictTests.cs create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/RebaseConflictTests.cs create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/RebaseTests.cs create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/ResetHardTests.cs create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/ResetMixedTests.cs create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/ResetSoftTests.cs create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/StatusTests.cs create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/UpdateIndexTests.cs create mode 100644 Scalar.FunctionalTests/Tests/GitCommands/UpdateRefTests.cs diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 0063232f34..2d82d5d714 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-20.04, windows-2019, macos-13] + os: [ ubuntu-20.04, windows-2019, macos-10.15] features: [ignored] env: @@ -43,3 +43,97 @@ jobs: - name: Unit test run: dotnet test --no-restore + + - name: Setup platform (Linux) + if: runner.os == 'Linux' + run: | + echo "BUILD_PLATFORM=${{ runner.os }}" >>$GITHUB_ENV + echo "TRACE2_BASENAME=Trace2.${{ github.run_id }}__${{ github.run_number }}__${{ matrix.os }}__${{ matrix.features }}" >>$GITHUB_ENV + + - name: Setup platform (Mac) + if: runner.os == 'macOS' + run: | + echo 'BUILD_PLATFORM=Mac' >>$GITHUB_ENV + echo "TRACE2_BASENAME=Trace2.${{ github.run_id }}__${{ github.run_number }}__${{ matrix.os }}__${{ matrix.features }}" >>$GITHUB_ENV + + - name: Setup platform (Windows) + if: runner.os == 'Windows' + run: | + echo "BUILD_PLATFORM=${{ runner.os }}" >>$env:GITHUB_ENV + echo 'BUILD_FILE_EXT=.exe' >>$env:GITHUB_ENV + echo "TRACE2_BASENAME=Trace2.${{ github.run_id }}__${{ github.run_number }}__${{ matrix.os }}__${{ matrix.features }}" >>$env:GITHUB_ENV + + - name: Setup Git installer + shell: bash + run: | + GIT_VERSION=$(grep '' Directory.Build.props | grep -Eo '[0-9.]+(-\w+)*') + cd ../out + dotnet new classlib -n Scalar.GitInstaller + cd Scalar.GitInstaller + cp ../../scalar/nuget.config . + dotnet add Scalar.GitInstaller.csproj package "GitFor${BUILD_PLATFORM}.GVFS.Installer" --package-directory . --version "$GIT_VERSION" --source "https://pkgs.dev.azure.com/gvfs/ci/_packaging/Dependencies/nuget/v3/index.json" + + - name: Install Git (Linux) + if: runner.os == 'Linux' + run: | + cd ../out/Scalar.GitInstaller + sudo apt-get install -y $(find . -type f -name '*.deb') + + - name: Install Git (Mac) + if: runner.os == 'macOS' + run: | + cd ../out/Scalar.GitInstaller + sudo /usr/sbin/installer -pkg $(find . -type f -name '*.pkg') -target / + + - name: Install Git (Windows) + if: runner.os == 'Windows' + run: | + Set-Location -Path ..\out\Scalar.GitInstaller + Write-Host 'Uninstalling Git ...' + foreach ($file in Get-ChildItem 'C:\Program Files\Git' -Recurse -File -Include 'unins*.exe') { + & $file.Fullname /VERYSILENT /SUPPRESSMSGBOXES /NORESTART + Wait-Process -Name $file.Basename + } + Remove-Item 'C:\Program Files\Git' -Recurse -Force + Write-Host 'Installing GitForWindows ...' + $files = Get-ChildItem . -Recurse -File -Include 'Git-*.vfs.*.exe' + & $files[0].Fullname /DIR="C:\Program Files\Git" /NOICONS /COMPONENTS="ext,ext\shellhere,ext\guihere,assoc,assoc_sh,scalar" /GROUP="Git" /SP- /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /ALLOWDOWNGRADE=1 /LOG=install.log + Wait-Process $files[0].Basename + Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\GitForWindows' + + - name: Configure feature.scalar + run: git config --global feature.scalar ${{ matrix.features }} + + - id: functional_test + name: Functional test + shell: bash + run: | + export GIT_TRACE2_EVENT="$PWD/$TRACE2_BASENAME/Event" + export GIT_TRACE2_PERF="$PWD/$TRACE2_BASENAME/Perf" + export GIT_TRACE2_EVENT_BRIEF=true + export GIT_TRACE2_PERF_BRIEF=true + mkdir -p "$TRACE2_BASENAME" + mkdir -p "$TRACE2_BASENAME/Event" + mkdir -p "$TRACE2_BASENAME/Perf" + git version --build-options + cd ../out + Scalar.FunctionalTests/$BUILD_FRAGMENT/Scalar.FunctionalTests$BUILD_FILE_EXT --full-suite + + - id: trace2_zip_unix + if: runner.os != 'Windows' && ( success() || failure() ) && ( steps.functional_test.conclusion == 'success' || steps.functional_test.conclusion == 'failure' ) + name: Zip Trace2 Logs (Unix) + shell: bash + run: zip -q -r $TRACE2_BASENAME.zip $TRACE2_BASENAME/ + + - id: trace2_zip_windows + if: runner.os == 'Windows' && ( success() || failure() ) && ( steps.functional_test.conclusion == 'success' || steps.functional_test.conclusion == 'failure' ) + name: Zip Trace2 Logs (Windows) + run: Compress-Archive -DestinationPath ${{ env.TRACE2_BASENAME }}.zip -Path ${{ env.TRACE2_BASENAME }} + + - name: Archive Trace2 Logs + if: ( success() || failure() ) && ( steps.trace2_zip_unix.conclusion == 'success' || steps.trace2_zip_windows.conclusion == 'success' ) + uses: actions/upload-artifact@v2 + with: + name: ${{ env.TRACE2_BASENAME }}.zip + path: ${{ env.TRACE2_BASENAME }}.zip + retention-days: 3 diff --git a/Scalar.FunctionalTests/Tests/GitCommands/AddStageTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/AddStageTests.cs new file mode 100644 index 0000000000..e5b879a816 --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/AddStageTests.cs @@ -0,0 +1,43 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + public class AddStageTests : GitRepoTests + { + public AddStageTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: false, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase, Order(1)] + public void AddBasicTest() + { + this.EditFile("Some new content.", "Readme.md"); + this.ValidateGitCommand("add Readme.md"); + this.RunGitCommand("commit -m \"Changing the Readme.md\""); + } + + [TestCase, Order(2)] + public void StageBasicTest() + { + this.EditFile("Some new content.", "AuthoringTests.md"); + this.ValidateGitCommand("stage AuthoringTests.md"); + this.RunGitCommand("commit -m \"Changing the AuthoringTests.md\""); + } + + [TestCase, Order(3)] + public void AddAndStageHardLinksTest() + { + this.CreateHardLink("ReadmeLink.md", "Readme.md"); + this.ValidateGitCommand("add ReadmeLink.md"); + this.RunGitCommand("commit -m \"Created ReadmeLink.md\""); + + this.CreateHardLink("AuthoringTestsLink.md", "AuthoringTests.md"); + this.ValidateGitCommand("stage AuthoringTestsLink.md"); + this.RunGitCommand("commit -m \"Created AuthoringTestsLink.md\""); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/CherryPickConflictTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/CherryPickConflictTests.cs new file mode 100644 index 0000000000..9eb1e1c051 --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/CherryPickConflictTests.cs @@ -0,0 +1,104 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + public class CherryPickConflictTests : GitRepoTests + { + public CherryPickConflictTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void CherryPickConflict() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void CherryPickConflictWithFileReads() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ReadConflictTargetFiles(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void CherryPickConflictWithFileReads2() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ReadConflictTargetFiles(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + this.ValidateGitCommand("cherry-pick --abort"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); + } + + [TestCase] + public void CherryPickConflict_ThenAbort() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("cherry-pick --abort"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void CherryPickConflict_ThenSkip() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("cherry-pick --skip"); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void CherryPickConflict_UsingOurs() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("cherry-pick -Xours " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void CherryPickConflict_UsingTheirs() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("cherry-pick -Xtheirs " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void CherryPickNoCommit() + { + this.ValidateGitCommand("checkout 170b13ce1990c53944403a70e93c257061598ae0"); + this.ValidateGitCommand("cherry-pick --no-commit " + GitRepoTests.ConflictTargetBranch); + } + + [TestCase] + public void CherryPickNoCommitReset() + { + this.ValidateGitCommand("checkout 170b13ce1990c53944403a70e93c257061598ae0"); + this.ValidateGitCommand("cherry-pick --no-commit " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset"); + } + + protected override void CreateEnlistment() + { + base.CreateEnlistment(); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/DeleteEmptyFolderTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/DeleteEmptyFolderTests.cs new file mode 100644 index 0000000000..310af03c89 --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/DeleteEmptyFolderTests.cs @@ -0,0 +1,45 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Should; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + public class DeleteEmptyFolderTests : GitRepoTests + { + public DeleteEmptyFolderTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void VerifyResetHardDeletesEmptyFolders() + { + this.SetupFolderDeleteTest(); + + this.RunGitCommand("reset --hard HEAD"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + } + + [TestCase] + public void VerifyCleanDeletesEmptyFolders() + { + this.SetupFolderDeleteTest(); + + this.RunGitCommand("clean -fd"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + } + + private void SetupFolderDeleteTest() + { + this.ControlGitRepo.Fetch("FunctionalTests/20170202_RenameTestMergeTarget"); + this.ValidateGitCommand("checkout FunctionalTests/20170202_RenameTestMergeTarget"); + this.DeleteFile("Test_EPF_GitCommandsTestOnlyFileFolder", "file.txt"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m\"Delete only file.\""); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/EnumerationMergeTest.cs b/Scalar.FunctionalTests/Tests/GitCommands/EnumerationMergeTest.cs new file mode 100644 index 0000000000..076de7c1d8 --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/EnumerationMergeTest.cs @@ -0,0 +1,31 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + public class EnumerationMergeTest : GitRepoTests + { + // Commit that found GvFlt Bug 12258777: Entries are sometimes skipped during + // enumeration when they don't fit in a user's buffer + private const string EnumerationReproCommitish = "FunctionalTests/20170602"; + + public EnumerationMergeTest(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void ConfirmEnumerationMatches() + { + this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); + } + + protected override void CreateEnlistment() + { + this.CreateEnlistment(EnumerationReproCommitish); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/GitCommandsTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/GitCommandsTests.cs new file mode 100644 index 0000000000..137c5628ec --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/GitCommandsTests.cs @@ -0,0 +1,1117 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Should; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System; +using System.IO; +using System.Runtime.CompilerServices; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + public class GitCommandsTests : GitRepoTests + { + public const string TopLevelFolderToCreate = "level1"; + private const string EncodingFileFolder = "FilenameEncoding"; + private const string EncodingFilename = "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt"; + private const string ContentWhenEditingFile = "// Adding a comment to the file"; + private const string UnknownTestName = "Unknown"; + private const string SubFolderToCreate = "level2"; + + private static readonly string EditFilePath = Path.Combine("GVFS", "GVFS.Common", "GVFSContext.cs"); + private static readonly string DeleteFilePath = Path.Combine("GVFS", "GVFS", "Program.cs"); + private static readonly string RenameFilePathFrom = Path.Combine("GVFS", "GVFS.Common", "Physical", "FileSystem", "FileProperties.cs"); + private static readonly string RenameFilePathTo = Path.Combine("GVFS", "GVFS.Common", "Physical", "FileSystem", "FileProperties2.cs"); + private static readonly string RenameFolderPathFrom = Path.Combine("GVFS", "GVFS.Common", "PrefetchPacks"); + private static readonly string RenameFolderPathTo = Path.Combine("GVFS", "GVFS.Common", "PrefetchPacksRenamed"); + + public GitCommandsTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: false, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void VerifyTestFilesExist() + { + // Sanity checks to ensure that the test files we expect to be in our test repo are present + Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.EditFilePath).ShouldBeAFile(this.FileSystem); + Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.EditFilePath).ShouldBeAFile(this.FileSystem); + Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.DeleteFilePath).ShouldBeAFile(this.FileSystem); + Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFilePathFrom).ShouldBeAFile(this.FileSystem); + Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFolderPathFrom).ShouldBeADirectory(this.FileSystem); + } + + [TestCase] + public void StatusTest() + { + this.ValidateGitCommand("status"); + } + + [TestCase] + public void StatusShortTest() + { + this.ValidateGitCommand("status -s"); + } + + [TestCase] + public void BranchTest() + { + this.ValidateGitCommand("branch"); + } + + [TestCase] + public void NewBranchTest() + { + this.ValidateGitCommand("branch tests/functional/NewBranchTest"); + this.ValidateGitCommand("branch"); + } + + [TestCase] + public void DeleteBranchTest() + { + this.ValidateGitCommand("branch tests/functional/DeleteBranchTest"); + this.ValidateGitCommand("branch"); + this.ValidateGitCommand("branch -d tests/functional/DeleteBranchTest"); + this.ValidateGitCommand("branch"); + } + + [TestCase] + public void RenameCurrentBranchTest() + { + this.ValidateGitCommand("checkout -b tests/functional/RenameBranchTest"); + this.ValidateGitCommand("branch -m tests/functional/RenameBranchTest2"); + this.ValidateGitCommand("branch"); + } + + [TestCase] + public void UntrackedFileTest() + { + this.BasicCommit(this.CreateFile, addCommand: "add ."); + } + + [TestCase] + public void UntrackedEmptyFileTest() + { + this.BasicCommit(this.CreateEmptyFile, addCommand: "add ."); + } + + [TestCase] + public void UntrackedFileAddAllTest() + { + this.BasicCommit(this.CreateFile, addCommand: "add --all"); + } + + [TestCase] + public void UntrackedEmptyFileAddAllTest() + { + this.BasicCommit(this.CreateEmptyFile, addCommand: "add --all"); + } + + [TestCase] + public void StageUntrackedFileTest() + { + this.BasicCommit(this.CreateFile, addCommand: "stage ."); + } + + [TestCase] + public void StageUntrackedEmptyFileTest() + { + this.BasicCommit(this.CreateEmptyFile, addCommand: "stage ."); + } + + [TestCase] + public void StageUntrackedFileAddAllTest() + { + this.BasicCommit(this.CreateFile, addCommand: "stage --all"); + } + + [TestCase] + public void StageUntrackedEmptyFileAddAllTest() + { + this.BasicCommit(this.CreateEmptyFile, addCommand: "stage --all"); + } + + [TestCase] + public void CheckoutNewBranchTest() + { + this.ValidateGitCommand("checkout -b tests/functional/CheckoutNewBranchTest"); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void CheckoutOrphanBranchTest() + { + this.ValidateGitCommand("checkout --orphan tests/functional/CheckoutOrphanBranchTest"); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void CreateFileSwitchBranchTest() + { + this.SwitchBranch(fileSystemAction: this.CreateFile); + } + + [TestCase] + public void CreateFileStageChangesSwitchBranchTest() + { + this.StageChangesSwitchBranch(fileSystemAction: this.CreateFile); + } + + [TestCase] + public void CreateFileCommitChangesSwitchBranchTest() + { + this.CommitChangesSwitchBranch(fileSystemAction: this.CreateFile); + } + + [TestCase] + public void CreateFileCommitChangesSwitchBranchSwitchBranchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.CreateFile); + } + + [TestCase] + public void DeleteFileSwitchBranchTest() + { + this.SwitchBranch(fileSystemAction: this.DeleteFile); + } + + [TestCase] + public void DeleteFileStageChangesSwitchBranchTest() + { + this.StageChangesSwitchBranch(fileSystemAction: this.DeleteFile); + } + + [TestCase] + public void DeleteFileCommitChangesSwitchBranchTest() + { + this.CommitChangesSwitchBranch(fileSystemAction: this.DeleteFile); + } + + [TestCase] + public void DeleteFileCommitChangesSwitchBranchSwitchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.DeleteFile); + } + + [TestCase] + public void DeleteFileCommitChangesSwitchBranchSwitchBackDeleteFolderTest() + { + // 663045 - Confirm that folder can be deleted after deleting file then changing + // branches + string deleteFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeletePlaceholderNonEmptyFolder_DeleteOnClose", "NonEmptyFolder"); + string deleteFilePath = Path.Combine(deleteFolderPath, "bar.txt"); + + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: () => this.DeleteFile(deleteFilePath)); + this.DeleteFolder(deleteFolderPath); + } + + [TestCase] + public void DeleteFolderSwitchBranchTest() + { + this.SwitchBranch(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteLocalEmptyFolder_DeleteOnClose")); + } + + [TestCase] + public void DeleteFolderStageChangesSwitchBranchTest() + { + this.StageChangesSwitchBranch(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteLocalEmptyFolder_SetDisposition")); + } + + [TestCase] + public void DeleteFolderCommitChangesSwitchBranchTest() + { + this.CommitChangesSwitchBranch(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteNonRootVirtualFolder_DeleteOnClose")); + } + + [TestCase] + public void DeleteFolderCommitChangesSwitchBranchSwitchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteNonRootVirtualFolder_SetDisposition")); + } + + [TestCase] + public void DeleteFilesWithNameAheadOfDot() + { + string folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "1"); + this.FolderShouldExistAndHaveFile(folder, "#test"); + this.DeleteFile(folder, "#test"); + this.FolderShouldExistAndBeEmpty(folder); + + folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "2"); + this.FolderShouldExistAndHaveFile(folder, "$test"); + this.DeleteFile(folder, "$test"); + this.FolderShouldExistAndBeEmpty(folder); + + folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "3"); + this.FolderShouldExistAndHaveFile(folder, ")"); + this.DeleteFile(folder, ")"); + this.FolderShouldExistAndBeEmpty(folder); + + folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "4"); + this.FolderShouldExistAndHaveFile(folder, "+.test"); + this.DeleteFile(folder, "+.test"); + this.FolderShouldExistAndBeEmpty(folder); + + folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "5"); + this.FolderShouldExistAndHaveFile(folder, "-.test"); + this.DeleteFile(folder, "-.test"); + this.FolderShouldExistAndBeEmpty(folder); + + this.ValidateGitCommand("status"); + } + + [TestCase] + public void RenameFilesWithNameAheadOfDot() + { + this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "1", "#test"); + this.MoveFile( + Path.Combine("GitCommandsTests", "RenameFileTests", "1", "#test"), + Path.Combine("GitCommandsTests", "RenameFileTests", "1", "#testRenamed")); + + this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "2", "$test"); + this.MoveFile( + Path.Combine("GitCommandsTests", "RenameFileTests", "2", "$test"), + Path.Combine("GitCommandsTests", "RenameFileTests", "2", "$testRenamed")); + + this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "3", ")"); + this.MoveFile( + Path.Combine("GitCommandsTests", "RenameFileTests", "3", ")"), + Path.Combine("GitCommandsTests", "RenameFileTests", "3", ")Renamed")); + + this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "4", "+.test"); + this.MoveFile( + Path.Combine("GitCommandsTests", "RenameFileTests", "4", "+.test"), + Path.Combine("GitCommandsTests", "RenameFileTests", "4", "+.testRenamed")); + + this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "5", "-.test"); + this.MoveFile( + Path.Combine("GitCommandsTests", "RenameFileTests", "5", "-.test"), + Path.Combine("GitCommandsTests", "RenameFileTests", "5", "-.testRenamed")); + + this.ValidateGitCommand("status"); + } + + [TestCase] + public void DeleteFileWithNameAheadOfDotAndSwitchCommits() + { + string fileRelativePath = Path.Combine("DeleteFileWithNameAheadOfDotAndSwitchCommits", "(1).txt"); + this.DeleteFile(fileRelativePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("checkout -- DeleteFileWithNameAheadOfDotAndSwitchCommits/(1).txt"); + this.DeleteFile(fileRelativePath); + this.ValidateGitCommand("status"); + + // 14cf226119766146b1fa5c5aa4cd0896d05f6b63 is the commit prior to creating (1).txt, it has two different files with + // names that start with '(': + // (a).txt + // (z).txt + this.ValidateGitCommand("checkout 14cf226119766146b1fa5c5aa4cd0896d05f6b63"); + this.DeleteFile("DeleteFileWithNameAheadOfDotAndSwitchCommits", "(a).txt"); + this.ValidateGitCommand("checkout -- DeleteFileWithNameAheadOfDotAndSwitchCommits/(a).txt"); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack() + { + // 663045 - Confirm that folder can be deleted after adding a file then changing branches + string newFileParentFolderPath = Path.Combine("GVFS", "GVFS", "CommandLine"); + string newFilePath = Path.Combine(newFileParentFolderPath, "testfile.txt"); + string newFileContents = "test contents"; + + this.CommitChangesSwitchBranch( + fileSystemAction: () => this.CreateFile(newFileContents, newFilePath), + test: "AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.DeleteFolder(newFileParentFolderPath); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout tests/functional/AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + this.FolderShouldExist(newFileParentFolderPath); + this.FileShouldHaveContents(newFileContents, newFilePath); + } + + [TestCase] + public void OverwriteFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack() + { + string overwrittenFileParentFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition"); + + // GVFlt_DeleteFolderTest\GVFlt_DeletePlaceholderNonEmptyFolder_SetDispositiontestfile.txt already exists in the repo as TestFile.txt + string fileToOverwritePath = Path.Combine(overwrittenFileParentFolderPath, "testfile.txt"); + string newFileContents = "test contents"; + + this.CommitChangesSwitchBranch( + fileSystemAction: () => this.CreateFile(newFileContents, fileToOverwritePath), + test: "OverwriteFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.DeleteFolder(overwrittenFileParentFolderPath); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout tests/functional/OverwriteFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + string subFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition", "NonEmptyFolder"); + this.ShouldNotExistOnDisk(subFolderPath); + this.FolderShouldExist(overwrittenFileParentFolderPath); + this.FileShouldHaveContents(newFileContents, fileToOverwritePath); + } + + [TestCase] + public void AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack() + { + // 663045 - Confirm that grandparent folder can be deleted after adding a (granchild) file + // then changing branches + string newFileParentFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose", "NonEmptyFolder"); + string newFileGrandParentFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose"); + string newFilePath = Path.Combine(newFileParentFolderPath, "testfile.txt"); + string newFileContents = "test contents"; + + this.CommitChangesSwitchBranch( + fileSystemAction: () => this.CreateFile(newFileContents, newFilePath), + test: "AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.DeleteFolder(newFileGrandParentFolderPath); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout tests/functional/AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + this.FolderShouldExist(newFileParentFolderPath); + this.FolderShouldExist(newFileGrandParentFolderPath); + this.FileShouldHaveContents(newFileContents, newFilePath); + } + + [TestCase] + public void CommitWithNewlinesInMessage() + { + this.ValidateGitCommand("checkout -b tests/functional/commit_with_uncommon_arguments"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Message that contains \na\nnew\nline\""); + } + + [TestCase] + public void CaseOnlyRenameFileAndChangeBranches() + { + // 693190 - Confirm that file does not disappear after case-only rename and branch + // changes + string newBranchName = "tests/functional/CaseOnlyRenameFileAndChangeBranches"; + string oldFileName = "Readme.md"; + string newFileName = "README.md"; + + this.ValidateGitCommand("checkout -b " + newBranchName); + this.ValidateGitCommand("mv {0} {1}", oldFileName, newFileName); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for CaseOnlyRenameFileAndChangeBranches\""); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.FileShouldHaveCaseMatchingName(oldFileName); + + this.ValidateGitCommand("checkout " + newBranchName); + this.FileShouldHaveCaseMatchingName(newFileName); + } + + [TestCase] + public void MoveFileFromOutsideRepoToInsideRepoAndAdd() + { + string testFileContents = "0123456789"; + string filename = "MoveFileFromOutsideRepoToInsideRepo.cs"; + + // Create the test files in this.Enlistment.EnlistmentRoot as it's outside of src and the control + // repo and is cleaned up when the functional tests run + string oldFilePath = Path.Combine(this.Enlistment.EnlistmentRoot, filename); + string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, filename); + string scalarFilePath = Path.Combine(this.Enlistment.RepoRoot, filename); + + string newBranchName = "tests/functional/MoveFileFromOutsideRepoToInsideRepoAndAdd"; + this.ValidateGitCommand("checkout -b " + newBranchName); + + // Move file to control repo + this.FileSystem.WriteAllText(oldFilePath, testFileContents); + this.FileSystem.MoveFile(oldFilePath, controlFilePath); + oldFilePath.ShouldNotExistOnDisk(this.FileSystem); + controlFilePath.ShouldBeAFile(this.FileSystem).WithContents(testFileContents); + + // Move file to Scalar repo + this.FileSystem.WriteAllText(oldFilePath, testFileContents); + this.FileSystem.MoveFile(oldFilePath, scalarFilePath); + oldFilePath.ShouldNotExistOnDisk(this.FileSystem); + scalarFilePath.ShouldBeAFile(this.FileSystem).WithContents(testFileContents); + + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for MoveFileFromOutsideRepoToInsideRepoAndAdd\""); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + } + + [TestCase] + public void MoveFolderFromOutsideRepoToInsideRepoAndAdd() + { + string testFileContents = "0123456789"; + string filename = "MoveFolderFromOutsideRepoToInsideRepoAndAdd.cs"; + string folderName = "GitCommand_MoveFolderFromOutsideRepoToInsideRepoAndAdd"; + + // Create the test folders in this.Enlistment.EnlistmentRoot as it's outside of src and the control + // repo and is cleaned up when the functional tests run + string oldFolderPath = Path.Combine(this.Enlistment.EnlistmentRoot, folderName); + string oldFilePath = Path.Combine(this.Enlistment.EnlistmentRoot, folderName, filename); + string controlFolderPath = Path.Combine(this.ControlGitRepo.RootPath, folderName); + string scalarFolderPath = Path.Combine(this.Enlistment.RepoRoot, folderName); + + string newBranchName = "tests/functional/MoveFolderFromOutsideRepoToInsideRepoAndAdd"; + this.ValidateGitCommand("checkout -b " + newBranchName); + + // Move folder to control repo + this.FileSystem.CreateDirectory(oldFolderPath); + this.FileSystem.WriteAllText(oldFilePath, testFileContents); + this.FileSystem.MoveDirectory(oldFolderPath, controlFolderPath); + oldFolderPath.ShouldNotExistOnDisk(this.FileSystem); + Path.Combine(controlFolderPath, filename).ShouldBeAFile(this.FileSystem).WithContents(testFileContents); + + // Move folder to Scalar repo + this.FileSystem.CreateDirectory(oldFolderPath); + this.FileSystem.WriteAllText(oldFilePath, testFileContents); + this.FileSystem.MoveDirectory(oldFolderPath, scalarFolderPath); + oldFolderPath.ShouldNotExistOnDisk(this.FileSystem); + Path.Combine(scalarFolderPath, filename).ShouldBeAFile(this.FileSystem).WithContents(testFileContents); + + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add --sparse ."); + this.RunGitCommand("commit -m \"Change for MoveFolderFromOutsideRepoToInsideRepoAndAdd\""); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + } + + [TestCase] + public void MoveFileFromInsideRepoToOutsideRepoAndCommit() + { + string newBranchName = "tests/functional/MoveFileFromInsideRepoToOutsideRepoAndCommit"; + this.ValidateGitCommand("checkout -b " + newBranchName); + + string fileName = "Protocol.md"; + string controlTargetFolder = "MoveFileFromInsideRepoToOutsideRepoAndCommit_ControlTarget"; + string scalarTargetFolder = "MoveFileFromInsideRepoToOutsideRepoAndCommit_ScalarTarget"; + + // Create the target folders in this.Enlistment.EnlistmentRoot as it's outside of src and the control repo + // and is cleaned up when the functional tests run + string controlTargetFolderPath = Path.Combine(this.Enlistment.EnlistmentRoot, controlTargetFolder); + string scalarTargetFolderPath = Path.Combine(this.Enlistment.EnlistmentRoot, scalarTargetFolder); + string controlTargetFilePath = Path.Combine(controlTargetFolderPath, fileName); + string scalarTargetFilePath = Path.Combine(scalarTargetFolderPath, fileName); + + // Move control repo file + this.FileSystem.CreateDirectory(controlTargetFolderPath); + this.FileSystem.MoveFile(Path.Combine(this.ControlGitRepo.RootPath, fileName), controlTargetFilePath); + controlTargetFilePath.ShouldBeAFile(this.FileSystem); + + // Move Scalar repo file + this.FileSystem.CreateDirectory(scalarTargetFolderPath); + this.FileSystem.MoveFile(Path.Combine(this.Enlistment.RepoRoot, fileName), scalarTargetFilePath); + scalarTargetFilePath.ShouldBeAFile(this.FileSystem); + + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for MoveFileFromInsideRepoToOutsideRepoAndCommit\""); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + } + + [TestCase] + public void EditFileSwitchBranchTest() + { + this.SwitchBranch(fileSystemAction: this.EditFile); + } + + [TestCase] + public void EditFileStageChangesSwitchBranchTest() + { + this.StageChangesSwitchBranch(fileSystemAction: this.EditFile); + } + + [TestCase] + public void EditFileCommitChangesSwitchBranchTest() + { + this.CommitChangesSwitchBranch(fileSystemAction: this.EditFile); + } + + [TestCase] + public void EditFileCommitChangesSwitchBranchSwitchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.EditFile); + } + + [TestCase] + public void RenameFileCommitChangesSwitchBranchSwitchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.RenameFile); + } + + // Mac and Linux only because renames of partial folders are blocked on Windows + [TestCase] + [Category(Categories.POSIXOnly)] + public void MoveFolderCommitChangesSwitchBranchSwitchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.MoveFolder); + } + + // Mac and Linux only because Windows does not support file mode + [TestCase] + [Category(Categories.POSIXOnly)] + public void UpdateFileModeOnly() + { + const string TestFileName = "test-file-mode"; + this.CreateFile("#!/bin/bash\n", TestFileName); + this.ChangeMode(TestFileName, Convert.ToUInt16("755", 8)); + this.ValidateGitCommand($"add {TestFileName}"); + this.ValidateGitCommand($"ls-files --stage {TestFileName}"); + } + + [TestCase] + public void AddFileCommitThenDeleteAndCommit() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndCommit_before"); + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndCommit_after"); + string filePath = Path.Combine("GVFS", "testfile.txt"); + this.CreateFile("Some new content for the file", filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Delete file for AddFileCommitThenDeleteAndCommit\""); + this.ValidateGitCommand("checkout tests/functional/AddFileCommitThenDeleteAndCommit_before"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + this.ValidateGitCommand("checkout tests/functional/AddFileCommitThenDeleteAndCommit_after"); + } + + [TestCase] + public void AddFileCommitThenDeleteAndResetSoft() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string filePath = Path.Combine("GVFS", "testfile.txt"); + this.CreateFile("Some new content for the file", filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + } + + [TestCase] + public void AddFileCommitThenDeleteAndResetMixed() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string filePath = Path.Combine("GVFS", "testfile.txt"); + this.CreateFile("Some new content for the file", filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + } + + [TestCase] + public void AddFolderAndFileCommitThenDeleteAndResetSoft() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string folderPath = "test_folder"; + this.CreateFolder(folderPath); + string filePath = Path.Combine(folderPath, "testfile.txt"); + this.CreateFile("Some new content for the file", filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.DeleteFolder(folderPath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + } + + [TestCase] + public void AddFolderAndFileCommitThenDeleteAndResetMixed() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string folderPath = "test_folder"; + this.CreateFolder(folderPath); + string filePath = Path.Combine(folderPath, "testfile.txt"); + this.CreateFile("Some new content for the file", filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.DeleteFolder(folderPath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --mixed HEAD~1"); + } + + [TestCase] + public void AddFolderAndFileCommitThenResetSoftAndResetHard() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string folderPath = "test_folder"; + this.CreateFolder(folderPath); + string filePath = Path.Combine(folderPath, "testfile.txt"); + this.CreateFile("Some new content for the file", filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void AddFolderAndFileCommitThenResetSoftAndResetMixed() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string folderPath = "test_folder"; + this.CreateFolder(folderPath); + string filePath = Path.Combine(folderPath, "testfile.txt"); + this.CreateFile("Some new content for the file", filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.ValidateGitCommand("reset --mixed HEAD"); + } + + [TestCase] + public void AddFoldersAndFilesAndRenameFolder() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFoldersAndFilesAndRenameFolder"); + + string topMostNewFolder = "AddFoldersAndFilesAndRenameFolder_Test"; + this.CreateFolder(topMostNewFolder); + this.CreateFile("test contents", topMostNewFolder, "top_level_test_file.txt"); + + string testFolderLevel1 = Path.Combine(topMostNewFolder, "TestFolderLevel1"); + this.CreateFolder(testFolderLevel1); + this.CreateFile("test contents", testFolderLevel1, "level_1_test_file.txt"); + + string testFolderLevel2 = Path.Combine(testFolderLevel1, "TestFolderLevel2"); + this.CreateFolder(testFolderLevel2); + this.CreateFile("test contents", testFolderLevel2, "level_2_test_file.txt"); + + string testFolderLevel3 = Path.Combine(testFolderLevel2, "TestFolderLevel3"); + this.CreateFolder(testFolderLevel3); + this.CreateFile("test contents", testFolderLevel3, "level_3_test_file.txt"); + this.ValidateGitCommand("status"); + + this.MoveFolder(testFolderLevel3, Path.Combine(testFolderLevel2, "TestFolderLevel3Renamed")); + this.ValidateGitCommand("status"); + + this.MoveFolder(testFolderLevel2, Path.Combine(testFolderLevel1, "TestFolderLevel2Renamed")); + this.ValidateGitCommand("status"); + + this.MoveFolder(testFolderLevel1, Path.Combine(topMostNewFolder, "TestFolderLevel1Renamed")); + this.ValidateGitCommand("status"); + + this.MoveFolder(topMostNewFolder, "AddFoldersAndFilesAndRenameFolder_TestRenamed"); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void AddFileAfterFolderRename() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileAfterFolderRename"); + + string folder = "AddFileAfterFolderRename_Test"; + string renamedFolder = "AddFileAfterFolderRename_TestRenamed"; + this.CreateFolder(folder); + this.MoveFolder(folder, renamedFolder); + this.CreateFile("test contents", renamedFolder, "test_file.txt"); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void ResetSoft() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetSoft"); + this.ValidateGitCommand("reset --soft HEAD~1"); + } + + [TestCase] + public void ResetMixed() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetMixed"); + this.ValidateGitCommand("reset --mixed HEAD~1"); + } + + [TestCase] + public void ResetMixed2() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetMixed2"); + this.ValidateGitCommand("reset HEAD~1"); + } + + [TestCase] + public void ManuallyModifyHead() + { + this.ValidateGitCommand("status"); + this.ReplaceText("f1bce402a7a980a8320f3f235cf8c8fdade4b17a", TestConstants.DotGit.Head); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void ResetSoftTwice() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetSoftTwice"); + + // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and + // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2 + this.ValidateGitCommand("reset --soft 60d19c87328120d11618ad563c396044a50985b2"); + this.ValidateGitCommand("reset --soft 99fc72275f950b0052c8548bbcf83a851f2b4467"); + } + + [TestCase] + public void ResetMixedTwice() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetMixedTwice"); + + // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and + // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2 + this.ValidateGitCommand("reset --mixed 60d19c87328120d11618ad563c396044a50985b2"); + this.ValidateGitCommand("reset --mixed 99fc72275f950b0052c8548bbcf83a851f2b4467"); + } + + [TestCase] + public void ResetMixed2Twice() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetMixed2Twice"); + + // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and + // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2 + this.ValidateGitCommand("reset 60d19c87328120d11618ad563c396044a50985b2"); + this.ValidateGitCommand("reset 99fc72275f950b0052c8548bbcf83a851f2b4467"); + } + + [TestCase] + public void ResetHardAfterCreate() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterCreate"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterEdit() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterEdit"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterDelete() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterDelete"); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterCreateAndAdd() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterCreateAndAdd"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterEditAndAdd() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterEditAndAdd"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterDeleteAndAdd() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterDeleteAndAdd"); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ChangeTwoBranchesAndMerge() + { + this.ValidateGitCommand("checkout -b tests/functional/ChangeTwoBranchesAndMerge_1"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for ChangeTwoBranchesAndMerge first branch\""); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/ChangeTwoBranchesAndMerge_2"); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for ChangeTwoBranchesAndMerge second branch\""); + this.ValidateGitCommand("merge tests/functional/ChangeTwoBranchesAndMerge_1"); + } + + [TestCase] + public void ChangeBranchAndCherryPickIntoAnotherBranch() + { + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchesAndCherryPickIntoAnotherBranch_1"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Create for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\""); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Delete for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\""); + this.ValidateGitCommand("tag DeleteForCherryPick"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Edit for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\""); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchesAndCherryPickIntoAnotherBranch_2"); + this.RunGitCommand("cherry-pick -q DeleteForCherryPick"); + } + + [TestCase] + public void ChangeBranchAndMergeRebaseOnAnotherBranch() + { + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_1"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Create for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\""); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Delete for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\""); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_2"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Edit for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\""); + + this.RunGitCommand("rebase --merge tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_1", ignoreErrors: true); + this.ValidateGitCommand("rev-parse HEAD^{{tree}}"); + } + + [TestCase] + public void ChangeBranchAndRebaseOnAnotherBranch() + { + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndRebaseOnAnotherBranch_1"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Create for ChangeBranchAndRebaseOnAnotherBranch first branch\""); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Delete for ChangeBranchAndRebaseOnAnotherBranch first branch\""); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndRebaseOnAnotherBranch_2"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Edit for ChangeBranchAndRebaseOnAnotherBranch first branch\""); + + this.RunGitCommand("rebase tests/functional/ChangeBranchAndRebaseOnAnotherBranch_1", ignoreErrors: true); + this.ValidateGitCommand("rev-parse HEAD^{{tree}}"); + } + + [TestCase] + public void StashChanges() + { + this.ValidateGitCommand("checkout -b tests/functional/StashChanges"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.ValidateGitCommand("stash"); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/StashChanges_2"); + this.RunGitCommand("stash pop"); + } + + [TestCase] + public void OpenFileThenCheckout() + { + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.EditFilePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.EditFilePath); + + // Open files with ReadWrite sharing because depending on the state of the index (and the mtimes), git might need to read the file + // as part of status (while we have the handle open). + using (FileStream virtualFS = File.Open(virtualFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)) + using (StreamWriter virtualWriter = new StreamWriter(virtualFS)) + using (FileStream controlFS = File.Open(controlFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)) + using (StreamWriter controlWriter = new StreamWriter(controlFS)) + { + this.ValidateGitCommand("checkout -b tests/functional/OpenFileThenCheckout"); + virtualWriter.WriteLine("// Adding a line for testing purposes"); + controlWriter.WriteLine("// Adding a line for testing purposes"); + this.ValidateGitCommand("status"); + } + + // NOTE: Due to optimizations in checkout -b, the modified files will not be included as part of the + // success message. Validate that the succcess messages match, and the call to validate "status" below + // will ensure that Scalar is still reporting the edited file as modified. + + string controlRepoRoot = this.ControlGitRepo.RootPath; + string scalarRepoRoot = this.Enlistment.RepoRoot; + string command = "checkout -b tests/functional/OpenFileThenCheckout_2"; + ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command); + ProcessResult actualResult = GitHelpers.InvokeGitAgainstScalarRepo(scalarRepoRoot, command); + GitHelpers.LinesShouldMatch(command, expectedResult.Errors, actualResult.Errors); + actualResult.Errors.ShouldContain("Switched to a new branch"); + + this.ValidateGitCommand("status"); + } + + [TestCase] + public void EditFileNeedingUtf8Encoding() + { + this.ValidateGitCommand("checkout -b tests/functional/EditFileNeedingUtf8Encoding"); + this.ValidateGitCommand("status"); + + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, EncodingFileFolder, EncodingFilename); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, EncodingFileFolder, EncodingFilename); + + string contents = virtualFile.ShouldBeAFile(this.FileSystem).WithContents(); + string expectedContents = controlFile.ShouldBeAFile(this.FileSystem).WithContents(); + contents.ShouldEqual(expectedContents); + + this.ValidateGitCommand("status"); + + this.AppendAllText(ContentWhenEditingFile, virtualFile); + this.AppendAllText(ContentWhenEditingFile, controlFile); + + this.ValidateGitCommand("status"); + } + + [TestCase] + public void UseAlias() + { + this.ValidateGitCommand("config --local alias.potato status"); + this.ValidateGitCommand("potato"); + } + + [TestCase] + public void RenameOnlyFileInFolder() + { + this.ControlGitRepo.Fetch("FunctionalTests/20170202_RenameTestMergeTarget"); + this.ControlGitRepo.Fetch("FunctionalTests/20170202_RenameTestMergeSource"); + + this.ValidateGitCommand("checkout FunctionalTests/20170202_RenameTestMergeTarget"); + this.FileSystem.ReadAllText(this.Enlistment.GetSourcePath("Test_EPF_GitCommandsTestOnlyFileFolder", "file.txt")); + this.ValidateGitCommand("merge origin/FunctionalTests/20170202_RenameTestMergeSource"); + } + + [TestCase] + public void BlameTest() + { + this.ValidateGitCommand("blame Readme.md"); + } + + private void BasicCommit(Action fileSystemAction, string addCommand, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) + { + this.ValidateGitCommand($"checkout -b tests/functional/{test}"); + fileSystemAction(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand(addCommand); + this.RunGitCommand($"commit -m \"BasicCommit for {test}\""); + } + + private void SwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) + { + this.ValidateGitCommand("checkout -b tests/functional/{0}", test); + fileSystemAction(); + this.ValidateGitCommand("status"); + } + + private void StageChangesSwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) + { + this.ValidateGitCommand("checkout -b tests/functional/{0}", test); + fileSystemAction(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + } + + private void CommitChangesSwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) + { + this.ValidateGitCommand("checkout -b tests/functional/{0}", test); + fileSystemAction(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for {0}\"", test); + } + + private void CommitChangesSwitchBranchSwitchBack(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) + { + string branch = string.Format("tests/functional/{0}", test); + this.ValidateGitCommand("checkout -b {0}", branch); + fileSystemAction(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for {0}\"", branch); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + + this.ValidateGitCommand("checkout {0}", branch); + } + + private void CreateFile() + { + this.CreateFile("Some content here", Path.GetRandomFileName() + "tempFile.txt"); + this.CreateFolder(TopLevelFolderToCreate); + this.CreateFolder(Path.Combine(TopLevelFolderToCreate, SubFolderToCreate)); + this.CreateFile("File in new folder", Path.Combine(TopLevelFolderToCreate, SubFolderToCreate, Path.GetRandomFileName() + "folderFile.txt")); + } + + private void EditFile() + { + this.AppendAllText(ContentWhenEditingFile, GitCommandsTests.EditFilePath); + } + + private void DeleteFile() + { + this.DeleteFile(GitCommandsTests.DeleteFilePath); + } + + private void RenameFile() + { + string virtualFileFrom = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFilePathFrom); + string virtualFileTo = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFilePathTo); + string controlFileFrom = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.RenameFilePathFrom); + string controlFileTo = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.RenameFilePathTo); + this.FileSystem.MoveFile(virtualFileFrom, virtualFileTo); + this.FileSystem.MoveFile(controlFileFrom, controlFileTo); + virtualFileFrom.ShouldNotExistOnDisk(this.FileSystem); + controlFileFrom.ShouldNotExistOnDisk(this.FileSystem); + } + + private void MoveFolder() + { + this.MoveFolder(GitCommandsTests.RenameFolderPathFrom, GitCommandsTests.RenameFolderPathTo); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/GitRepoTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/GitRepoTests.cs new file mode 100644 index 0000000000..d2457b5077 --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/GitRepoTests.cs @@ -0,0 +1,656 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Should; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixture] + public abstract class GitRepoTests + { + protected const string ConflictSourceBranch = "FunctionalTests/20170206_Conflict_Source"; + protected const string ConflictTargetBranch = "FunctionalTests/20170206_Conflict_Target"; + protected const string NoConflictSourceBranch = "FunctionalTests/20170209_NoConflict_Source"; + protected const string DirectoryWithFileBeforeBranch = "FunctionalTests/20171025_DirectoryWithFileBefore"; + protected const string DirectoryWithFileAfterBranch = "FunctionalTests/20171025_DirectoryWithFileAfter"; + protected const string DirectoryWithDifferentFileAfterBranch = "FunctionalTests/20171025_DirectoryWithDifferentFile"; + protected const string DeepDirectoryWithOneFile = "FunctionalTests/20181010_DeepFolderOneFile"; + protected const string DeepDirectoryWithOneDifferentFile = "FunctionalTests/20181010_DeepFolderOneDifferentFile"; + + protected string[] pathPrefixes; + + // These are the folders for the sparse mode that are needed for the functional tests + // because they are the folders that the tests rely on to be there. + private static readonly string[] SparseModeFolders = new string[] + { + "a", + "AddFileAfterFolderRename_Test", + "AddFileAfterFolderRename_TestRenamed", + "AddFoldersAndFilesAndRenameFolder_Test", + "AddFoldersAndFilesAndRenameFolder_TestRenamed", + "c", + "CheckoutNewBranchFromStartingPointTest", + "CheckoutOrhpanBranchFromStartingPointTest", + "d", + "DeleteFileWithNameAheadOfDotAndSwitchCommits", + "EnumerateAndReadTestFiles", + "ErrorWhenPathTreatsFileAsFolderMatchesNTFS", + "file.txt", // Changes to a folder in one test + "foo.cpp", // Changes to a folder in one test + "FilenameEncoding", + "GitCommandsTests", + "GVFlt_BugRegressionTest", + "GVFlt_DeleteFileTest", + "GVFlt_DeleteFolderTest", + "GVFlt_EnumTest", + "GVFlt_FileAttributeTest", + "GVFlt_FileEATest", + "GVFlt_FileOperationTest", + "GVFlt_MoveFileTest", + "GVFlt_MoveFolderTest", + "GVFlt_MultiThreadTest", + "GVFlt_SetLinkTest", + Path.Combine("GVFS", "GVFS"), + Path.Combine("GVFS", "GVFS.Common"), + GitCommandsTests.TopLevelFolderToCreate, + "ResetTwice_OnlyDeletes_Test", + "ResetTwice_OnlyEdits_Test", + "Test_ConflictTests", + "Test_EPF_GitCommandsTestOnlyFileFolder", + "Test_EPF_MoveRenameFileTests", + "Test_EPF_MoveRenameFileTests_2", + "Test_EPF_MoveRenameFolderTests", + "Test_EPF_UpdatePlaceholderTests", + "Test_EPF_WorkingDirectoryTests", + "test_folder", + "TrailingSlashTests", + }; + + private bool enlistmentPerTest; + private Settings.ValidateWorkingTreeMode validateWorkingTree; + + public GitRepoTests(bool enlistmentPerTest, Settings.ValidateWorkingTreeMode validateWorkingTree) + { + this.enlistmentPerTest = enlistmentPerTest; + this.validateWorkingTree = validateWorkingTree; + this.FileSystem = new SystemIORunner(); + } + + public static object[] ValidateWorkingTree + { + get + { + return ScalarTestConfig.GitRepoTestsValidateWorkTree; + } + } + + public ControlGitRepo ControlGitRepo + { + get; private set; + } + + protected FileSystemRunner FileSystem + { + get; private set; + } + + protected ScalarFunctionalTestEnlistment Enlistment + { + get; private set; + } + + [OneTimeSetUp] + public virtual void SetupForFixture() + { + if (!this.enlistmentPerTest) + { + this.CreateEnlistment(); + } + } + + [OneTimeTearDown] + public virtual void TearDownForFixture() + { + if (!this.enlistmentPerTest) + { + this.DeleteEnlistment(); + } + } + + [SetUp] + public virtual void SetupForTest() + { + if (this.enlistmentPerTest) + { + this.CreateEnlistment(); + } + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + + this.CheckHeadCommitTree(); + + if (this.validateWorkingTree != Settings.ValidateWorkingTreeMode.None) + { + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + } + + this.ValidateGitCommand("status"); + } + + [TearDown] + public virtual void TearDownForTest() + { + this.TestValidationAndCleanup(); + } + + protected void TestValidationAndCleanup(bool ignoreCase = false) + { + try + { + this.CheckHeadCommitTree(); + + if (this.validateWorkingTree != Settings.ValidateWorkingTreeMode.None) + { + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, ignoreCase: ignoreCase, withinPrefixes: this.pathPrefixes); + } + + this.RunGitCommand("reset --hard -q HEAD"); + this.RunGitCommand("clean -d -f -x"); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + + this.CheckHeadCommitTree(); + + // If enlistmentPerTest is true we can always validate the working tree because + // this is the last place we'll use it + if ((this.validateWorkingTree != Settings.ValidateWorkingTreeMode.None) || this.enlistmentPerTest) + { + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, ignoreCase: ignoreCase, withinPrefixes: this.pathPrefixes); + } + } + finally + { + if (this.enlistmentPerTest) + { + this.DeleteEnlistment(); + } + } + } + + protected virtual void CreateEnlistment() + { + this.CreateEnlistment(null); + } + + protected void CreateEnlistment(string commitish = null) + { + this.Enlistment = ScalarFunctionalTestEnlistment.Clone( + ScalarTestConfig.PathToScalar, + commitish: commitish, + fullClone: this.validateWorkingTree != Settings.ValidateWorkingTreeMode.SparseMode); + GitProcess.Invoke(this.Enlistment.RepoRoot, "config core.editor true"); + GitProcess.Invoke(this.Enlistment.RepoRoot, "config advice.statusUoption false"); + GitProcess.Invoke(this.Enlistment.RepoRoot, "config advice.sparseIndexExpanded false"); + this.ControlGitRepo = ControlGitRepo.Create(commitish); + this.ControlGitRepo.Initialize(); + + if (this.validateWorkingTree == Settings.ValidateWorkingTreeMode.SparseMode) + { + StringBuilder sb = new StringBuilder(); + + foreach (string folder in SparseModeFolders) + { + sb.Append(folder.Replace(Path.DirectorySeparatorChar, TestConstants.GitPathSeparator) + .Trim(TestConstants.GitPathSeparator)); + sb.Append("\n"); + } + + GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "sparse-checkout init --cone", string.Empty); + GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "sparse-checkout set --stdin", sb.ToString()); + + // This line shouldn't be necessary! + GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "sparse-checkout init --cone", string.Empty); + GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "sparse-checkout set --stdin", sb.ToString()); + this.pathPrefixes = SparseModeFolders; + } + } + + protected virtual void DeleteEnlistment() + { + if (this.Enlistment != null) + { + this.Enlistment.DeleteAll(); + } + + if (this.ControlGitRepo != null) + { + RepositoryHelpers.DeleteTestDirectory(this.ControlGitRepo.RootPath); + } + } + + protected void CheckHeadCommitTree() + { + this.ValidateGitCommand("ls-tree HEAD"); + } + + protected void RunGitCommand(string command, params object[] args) + { + this.RunGitCommand(string.Format(command, args)); + } + + /* We are using the following method for these scenarios + * 1. Some commands compute a new commit sha, which is dependent on time and therefore + * won't match what is in the control repo. For those commands, we just ensure that + * the errors match what we expect, but we skip comparing the output + * 2. Using the sparse-checkout feature git will error out before checking the untracked files + * so the control repo will show the untracked files as being overwritten while the Scalar + * repo which is using the sparse-checkout will not. + * 3. Scalar is returning not found for files that are outside the sparse-checkout and there + * are cases when git will delete these files during a merge outputting that it removed them + * which the Scalar repo did not have to remove so the message is missing that output. + */ + protected void RunGitCommand(string command, bool ignoreErrors = false, bool checkStatus = true, string standardInput = null) + { + string controlRepoRoot = this.ControlGitRepo.RootPath; + string scalarRepoRoot = this.Enlistment.RepoRoot; + + ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command, standardInput); + ProcessResult actualResult = GitHelpers.InvokeGitAgainstScalarRepo(scalarRepoRoot, command, input: standardInput); + + if (!ignoreErrors) + { + GitHelpers.LinesShouldMatch(command, expectedResult.Errors, actualResult.Errors); + } + + if (command != "status" && checkStatus) + { + this.ValidateGitCommand("status"); + } + } + + protected void ValidateGitCommand(string command, params object[] args) + { + GitHelpers.ValidateGitCommand( + this.Enlistment, + this.ControlGitRepo, + command, + args); + } + + protected void ChangeMode(string filePath, ushort mode) + { + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.FileSystem.ChangeMode(virtualFile, mode); + this.FileSystem.ChangeMode(controlFile, mode); + } + + protected void CreateEmptyFile() + { + string filePath = Path.GetRandomFileName() + "emptyFile.txt"; + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.FileSystem.CreateEmptyFile(virtualFile); + this.FileSystem.CreateEmptyFile(controlFile); + } + + protected void CreateFile(string content, params string[] filePathPaths) + { + string filePath = Path.Combine(filePathPaths); + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.FileSystem.WriteAllText(virtualFile, content); + this.FileSystem.WriteAllText(controlFile, content); + } + + protected IDisposable ReadFileAndWriteWithoutClose(string path, string contents) + { + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, path); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, path); + this.FileSystem.ReadAllText(virtualFile); + this.FileSystem.ReadAllText(controlFile); + + DisposeList list = new DisposeList(); + list.Items.Add(this.FileSystem.OpenFileAndWrite(virtualFile, contents)); + list.Items.Add(this.FileSystem.OpenFileAndWrite(controlFile, contents)); + return list; + } + + protected void CreateFolder(string folderPath) + { + string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); + string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); + this.FileSystem.CreateDirectory(virtualFolder); + this.FileSystem.CreateDirectory(controlFolder); + } + + protected void EditFile(string content, params string[] filePathParts) + { + string filePath = Path.Combine(filePathParts); + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.FileSystem.AppendAllText(virtualFile, content); + this.FileSystem.AppendAllText(controlFile, content); + } + + protected void CreateHardLink(string newLinkFileName, string existingFileName) + { + string virtualExistingFile = Path.Combine(this.Enlistment.RepoRoot, existingFileName); + string controlExistingFile = Path.Combine(this.ControlGitRepo.RootPath, existingFileName); + string virtualNewLinkFile = Path.Combine(this.Enlistment.RepoRoot, newLinkFileName); + string controlNewLinkFile = Path.Combine(this.ControlGitRepo.RootPath, newLinkFileName); + + this.FileSystem.CreateHardLink(virtualNewLinkFile, virtualExistingFile); + this.FileSystem.CreateHardLink(controlNewLinkFile, controlExistingFile); + } + + protected void SetFileAsReadOnly(string filePath) + { + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + + File.SetAttributes(virtualFile, File.GetAttributes(virtualFile) | FileAttributes.ReadOnly); + File.SetAttributes(virtualFile, File.GetAttributes(controlFile) | FileAttributes.ReadOnly); + } + + protected void MoveFile(string pathFrom, string pathTo) + { + string virtualFileFrom = Path.Combine(this.Enlistment.RepoRoot, pathFrom); + string virtualFileTo = Path.Combine(this.Enlistment.RepoRoot, pathTo); + string controlFileFrom = Path.Combine(this.ControlGitRepo.RootPath, pathFrom); + string controlFileTo = Path.Combine(this.ControlGitRepo.RootPath, pathTo); + this.FileSystem.MoveFile(virtualFileFrom, virtualFileTo); + this.FileSystem.MoveFile(controlFileFrom, controlFileTo); + virtualFileFrom.ShouldNotExistOnDisk(this.FileSystem); + controlFileFrom.ShouldNotExistOnDisk(this.FileSystem); + virtualFileTo.ShouldBeAFile(this.FileSystem); + controlFileTo.ShouldBeAFile(this.FileSystem); + } + + protected void DeleteFile(params string[] filePathParts) + { + string filePath = Path.Combine(filePathParts); + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.FileSystem.DeleteFile(virtualFile); + this.FileSystem.DeleteFile(controlFile); + virtualFile.ShouldNotExistOnDisk(this.FileSystem); + controlFile.ShouldNotExistOnDisk(this.FileSystem); + } + + protected void DeleteFolder(params string[] folderPathParts) + { + string folderPath = Path.Combine(folderPathParts); + string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); + string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); + this.FileSystem.DeleteDirectory(virtualFolder); + this.FileSystem.DeleteDirectory(controlFolder); + virtualFolder.ShouldNotExistOnDisk(this.FileSystem); + controlFolder.ShouldNotExistOnDisk(this.FileSystem); + } + + protected void MoveFolder(string pathFrom, string pathTo) + { + string virtualFileFrom = Path.Combine(this.Enlistment.RepoRoot, pathFrom); + string virtualFileTo = Path.Combine(this.Enlistment.RepoRoot, pathTo); + string controlFileFrom = Path.Combine(this.ControlGitRepo.RootPath, pathFrom); + string controlFileTo = Path.Combine(this.ControlGitRepo.RootPath, pathTo); + this.FileSystem.MoveDirectory(virtualFileFrom, virtualFileTo); + this.FileSystem.MoveDirectory(controlFileFrom, controlFileTo); + virtualFileFrom.ShouldNotExistOnDisk(this.FileSystem); + controlFileFrom.ShouldNotExistOnDisk(this.FileSystem); + } + + protected void FolderShouldExist(params string[] folderPathParts) + { + string folderPath = Path.Combine(folderPathParts); + string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); + string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); + virtualFolder.ShouldBeADirectory(this.FileSystem); + controlFolder.ShouldBeADirectory(this.FileSystem); + } + + protected void FolderShouldExistAndHaveFile(params string[] filePathParts) + { + string filePath = Path.Combine(filePathParts); + string folderPath = Path.GetDirectoryName(filePath); + string fileName = Path.GetFileName(filePath); + + string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); + string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); + virtualFolder.ShouldBeADirectory(this.FileSystem).WithItems(fileName).Count().ShouldEqual(1); + controlFolder.ShouldBeADirectory(this.FileSystem).WithItems(fileName).Count().ShouldEqual(1); + } + + protected void FolderShouldExistAndBeEmpty(params string[] folderPathParts) + { + string folderPath = Path.Combine(folderPathParts); + string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); + string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); + virtualFolder.ShouldBeADirectory(this.FileSystem).WithNoItems(); + controlFolder.ShouldBeADirectory(this.FileSystem).WithNoItems(); + } + + protected void ShouldNotExistOnDisk(params string[] pathParts) + { + string path = Path.Combine(pathParts); + string virtualPath = Path.Combine(this.Enlistment.RepoRoot, path); + string controlPath = Path.Combine(this.ControlGitRepo.RootPath, path); + virtualPath.ShouldNotExistOnDisk(this.FileSystem); + controlPath.ShouldNotExistOnDisk(this.FileSystem); + } + + protected void FileShouldHaveContents(string contents, params string[] filePathParts) + { + string filePath = Path.Combine(filePathParts); + string virtualFilePath = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, filePath); + virtualFilePath.ShouldBeAFile(this.FileSystem).WithContents(contents); + controlFilePath.ShouldBeAFile(this.FileSystem).WithContents(contents); + } + + protected void FileContentsShouldMatch(params string[] filePathPaths) + { + string filePath = Path.Combine(filePathPaths); + string virtualFilePath = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, filePath); + bool virtualExists = File.Exists(virtualFilePath); + bool controlExists = File.Exists(controlFilePath); + + if (virtualExists) + { + if (controlExists) + { + virtualFilePath.ShouldBeAFile(this.FileSystem) + .WithContents(controlFilePath.ShouldBeAFile(this.FileSystem) + .WithContents()); + } + else + { + virtualExists.ShouldEqual(controlExists, $"{virtualExists} exists, but {controlExists} does not"); + } + } + else if (controlExists) + { + virtualExists.ShouldEqual(controlExists, $"{virtualExists} does not exist, but {controlExists} does"); + } + } + + protected void FileShouldHaveCaseMatchingName(string caseSensitiveFilePath) + { + string virtualFilePath = Path.Combine(this.Enlistment.RepoRoot, caseSensitiveFilePath); + string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, caseSensitiveFilePath); + string caseSensitiveName = Path.GetFileName(caseSensitiveFilePath); + virtualFilePath.ShouldBeAFile(this.FileSystem).WithCaseMatchingName(caseSensitiveName); + controlFilePath.ShouldBeAFile(this.FileSystem).WithCaseMatchingName(caseSensitiveName); + } + + protected void FolderShouldHaveCaseMatchingName(string caseSensitiveFolderPath) + { + string virtualFolderPath = Path.Combine(this.Enlistment.RepoRoot, caseSensitiveFolderPath); + string controlFolderPath = Path.Combine(this.ControlGitRepo.RootPath, caseSensitiveFolderPath); + string caseSensitiveName = Path.GetFileName(caseSensitiveFolderPath); + virtualFolderPath.ShouldBeADirectory(this.FileSystem).WithCaseMatchingName(caseSensitiveName); + controlFolderPath.ShouldBeADirectory(this.FileSystem).WithCaseMatchingName(caseSensitiveName); + } + + protected void AppendAllText(string content, params string[] filePathParts) + { + string filePath = Path.Combine(filePathParts); + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.FileSystem.AppendAllText(virtualFile, content); + this.FileSystem.AppendAllText(controlFile, content); + } + + protected void ReplaceText(string newContent, params string[] filePathParts) + { + string filePath = Path.Combine(filePathParts); + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.FileSystem.WriteAllText(virtualFile, newContent); + this.FileSystem.WriteAllText(controlFile, newContent); + } + + protected void SetupForFileDirectoryTest(string commandBranch = DirectoryWithFileAfterBranch) + { + this.ControlGitRepo.Fetch(DirectoryWithFileBeforeBranch); + this.ControlGitRepo.Fetch(commandBranch); + this.ValidateGitCommand($"checkout {DirectoryWithFileBeforeBranch}"); + } + + protected void ValidateFileDirectoryTest(string command, string commandBranch = DirectoryWithFileAfterBranch) + { + this.EditFile("Change file", "Readme.md"); + this.ValidateGitCommand("add --all"); + this.RunGitCommand("commit -m \"Some change\""); + this.ValidateGitCommand($"{command} {commandBranch}"); + } + + protected void RunFileDirectoryEnumerateTest(string command, string commandBranch = DirectoryWithFileAfterBranch) + { + this.SetupForFileDirectoryTest(commandBranch); + + // file.txt is a folder with a file named file.txt to test checking out branches + // that have folders with the same name as files + this.FileSystem.EnumerateDirectory(this.Enlistment.GetSourcePath("file.txt")); + this.ValidateFileDirectoryTest(command, commandBranch); + } + + protected void RunFileDirectoryReadTest(string command, string commandBranch = DirectoryWithFileAfterBranch) + { + this.SetupForFileDirectoryTest(commandBranch); + this.FileContentsShouldMatch("file.txt", "file.txt"); + this.ValidateFileDirectoryTest(command, commandBranch); + } + + protected void RunFileDirectoryWriteTest(string command, string commandBranch = DirectoryWithFileAfterBranch) + { + this.SetupForFileDirectoryTest(commandBranch); + this.EditFile("Change file", "file.txt", "file.txt"); + this.ValidateFileDirectoryTest(command, commandBranch); + } + + protected void ReadConflictTargetFiles() + { + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "DeletedFiles", "DeleteInSource.txt"); + } + + protected void FilesShouldMatchCheckoutOfTargetBranch() + { + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt"); + + this.FileContentsShouldMatch("Test_ConflictTests", "DeletedFiles", "DeleteInSource.txt"); + + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); + } + + protected void FilesShouldMatchCheckoutOfSourceBranch() + { + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedBySource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt"); + + this.FileContentsShouldMatch("Test_ConflictTests", "DeletedFiles", "DeleteInTarget.txt"); + + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSourceDeleteInTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); + } + + protected void FilesShouldMatchAfterNoConflict() + { + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt"); + + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); + } + + protected void FilesShouldMatchAfterConflict() + { + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedBySource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt"); + + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSourceDeleteInTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); + } + + private class DisposeList : IDisposable + { + public readonly List Items = new List(); + + public void Dispose() + { + foreach (IDisposable item in this.Items) + { + item.Dispose(); + } + } + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/MergeConflictTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/MergeConflictTests.cs new file mode 100644 index 0000000000..d245d9d75b --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/MergeConflictTests.cs @@ -0,0 +1,120 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Tools; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + public class MergeConflictTests : GitRepoTests + { + public MergeConflictTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void MergeConflict() + { + // No need to tear down this config since these tests are for enlistment per test. + this.SetupRenameDetectionAvoidanceInConfig(); + + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void MergeConflictWithFileReads() + { + // No need to tear down this config since these tests are for enlistment per test. + this.SetupRenameDetectionAvoidanceInConfig(); + + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ReadConflictTargetFiles(); + this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void MergeConflict_ThenAbort() + { + // No need to tear down this config since these tests are for enlistment per test. + this.SetupRenameDetectionAvoidanceInConfig(); + + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("merge --abort"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void MergeConflict_UsingOurs() + { + // No need to tear down this config since these tests are for enlistment per test. + this.SetupRenameDetectionAvoidanceInConfig(); + + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand($"merge -s ours {GitRepoTests.ConflictSourceBranch}"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void MergeConflict_UsingStrategyTheirs() + { + // No need to tear down this config since these tests are for enlistment per test. + this.SetupRenameDetectionAvoidanceInConfig(); + + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand($"merge -s recursive -Xtheirs {GitRepoTests.ConflictSourceBranch}"); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void MergeConflict_UsingStrategyOurs() + { + // No need to tear down this config since these tests are for enlistment per test. + this.SetupRenameDetectionAvoidanceInConfig(); + + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand($"merge -s recursive -Xours {GitRepoTests.ConflictSourceBranch}"); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void MergeConflictEnsureStatusFailsDueToConfig() + { + // This is compared against the message emitted by Scalar.Hooks\Program.cs + string expectedErrorMessagePart = "--no-renames"; + + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch, checkStatus: false); + + ProcessResult result1 = GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "status"); + result1.Errors.Contains(expectedErrorMessagePart); + + ProcessResult result2 = GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "status --no-renames"); + result2.Errors.Contains(expectedErrorMessagePart); + + // Complete setup to ensure teardown succeeds + GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "config --local test.renames false"); + } + + protected override void CreateEnlistment() + { + base.CreateEnlistment(); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + } + + private void SetupRenameDetectionAvoidanceInConfig() + { + // Tell the pre-command hook that it shouldn't check for "--no-renames" when runing "git status" + // as the control repo won't do that. When the pre-command hook has been updated to properly + // check for "status.renames" we can set that value here instead. + this.ValidateGitCommand("config --local test.renames false"); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/RebaseConflictTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/RebaseConflictTests.cs new file mode 100644 index 0000000000..d266f155d9 --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/RebaseConflictTests.cs @@ -0,0 +1,91 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + public class RebaseConflictTests : GitRepoTests + { + public RebaseConflictTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void RebaseConflict() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void RebaseConflictWithFileReads() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ReadConflictTargetFiles(); + this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void RebaseConflict_ThenAbort() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("rebase --abort"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void RebaseConflict_ThenSkip() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("rebase --skip"); + this.FilesShouldMatchCheckoutOfSourceBranch(); + } + + [TestCase] + public void RebaseConflict_RemoveDeletedTheirsFile() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("rm Test_ConflictTests/ModifiedFiles/ChangeInSourceDeleteInTarget.txt"); + } + + [TestCase] + public void RebaseConflict_AddThenContinue() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("add ."); + this.ValidateGitCommand("rebase --continue"); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void RebaseMultipleCommits() + { + string sourceCommit = "FunctionalTests/20170403_rebase_multiple_source"; + string targetCommit = "FunctionalTests/20170403_rebase_multiple_onto"; + + this.ControlGitRepo.Fetch(sourceCommit); + this.ControlGitRepo.Fetch(targetCommit); + + this.ValidateGitCommand("checkout " + sourceCommit); + this.RunGitCommand("rebase origin/" + targetCommit); + this.ValidateGitCommand("rebase --abort"); + } + + protected override void CreateEnlistment() + { + base.CreateEnlistment(); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/RebaseTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/RebaseTests.cs new file mode 100644 index 0000000000..3c064fc38b --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/RebaseTests.cs @@ -0,0 +1,118 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + public class RebaseTests : GitRepoTests + { + public RebaseTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + [Ignore("This is producing different output because git is not checking out files in the rebase. The virtual file system changes should address this issue.")] + public void RebaseSmallNoConflicts() + { + // 5d299512450f4029d7a1fe8d67e833b84247d393 is the tip of FunctionalTests/RebaseTestsSource_20170130 + string sourceCommit = "5d299512450f4029d7a1fe8d67e833b84247d393"; + + // Target commit 47fabb534c35af40156db6e8365165cb04f9dd75 is part of the history of + // FunctionalTests/20170130 + string targetCommit = "47fabb534c35af40156db6e8365165cb04f9dd75"; + + this.ControlGitRepo.Fetch(sourceCommit); + this.ControlGitRepo.Fetch(targetCommit); + + this.ValidateGitCommand("checkout {0}", sourceCommit); + this.ValidateGitCommand("rebase {0}", targetCommit); + } + + [TestCase] + public void RebaseSmallOneFileConflict() + { + // 5d299512450f4029d7a1fe8d67e833b84247d393 is the tip of FunctionalTests/RebaseTestsSource_20170130 + string sourceCommit = "5d299512450f4029d7a1fe8d67e833b84247d393"; + + // Target commit 99fc72275f950b0052c8548bbcf83a851f2b4467 is part of the history of + // FunctionalTests/20170130 + string targetCommit = "99fc72275f950b0052c8548bbcf83a851f2b4467"; + + this.ControlGitRepo.Fetch(sourceCommit); + this.ControlGitRepo.Fetch(targetCommit); + + this.ValidateGitCommand("checkout {0}", sourceCommit); + this.ValidateGitCommand("rebase {0}", targetCommit); + } + + [TestCase] + [Ignore("This is producing different output because git is not checking out files in the rebase. The virtual file system changes should address this issue.")] + public void RebaseEditThenDelete() + { + // 23a238b04497da2449fd730966c06f84b6326c3a is the tip of FunctionalTests/RebaseTestsSource_20170208 + string sourceCommit = "23a238b04497da2449fd730966c06f84b6326c3a"; + + // Target commit 47fabb534c35af40156db6e8365165cb04f9dd75 is part of the history of + // FunctionalTests/20170208 + string targetCommit = "47fabb534c35af40156db6e8365165cb04f9dd75"; + + this.ControlGitRepo.Fetch(sourceCommit); + this.ControlGitRepo.Fetch(targetCommit); + + this.ValidateGitCommand("checkout {0}", sourceCommit); + this.ValidateGitCommand("rebase {0}", targetCommit); + } + + [TestCase] + public void RebaseWithDirectoryNameSameAsFile() + { + this.SetupForFileDirectoryTest(); + this.ValidateFileDirectoryTest("rebase"); + } + + [TestCase] + public void RebaseWithDirectoryNameSameAsFileEnumerate() + { + this.RunFileDirectoryEnumerateTest("rebase"); + } + + [TestCase] + public void RebaseWithDirectoryNameSameAsFileWithRead() + { + this.RunFileDirectoryReadTest("rebase"); + } + + [TestCase] + public void RebaseWithDirectoryNameSameAsFileWithWrite() + { + this.RunFileDirectoryWriteTest("rebase"); + } + + [TestCase] + public void RebaseDirectoryWithOneFile() + { + this.SetupForFileDirectoryTest(commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + this.ValidateFileDirectoryTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + + [TestCase] + public void RebaseDirectoryWithOneFileEnumerate() + { + this.RunFileDirectoryEnumerateTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + + [TestCase] + public void RebaseDirectoryWithOneFileRead() + { + this.RunFileDirectoryReadTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + + [TestCase] + public void RebaseDirectoryWithOneFileWrite() + { + this.RunFileDirectoryWriteTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/ResetHardTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/ResetHardTests.cs new file mode 100644 index 0000000000..cbc31c640b --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/ResetHardTests.cs @@ -0,0 +1,79 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Should; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + public class ResetHardTests : GitRepoTests + { + private const string ResetHardCommand = "reset --hard"; + + public ResetHardTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void VerifyResetHardDeletesEmptyFolders() + { + this.ControlGitRepo.Fetch("FunctionalTests/20170202_RenameTestMergeTarget"); + this.ValidateGitCommand("checkout FunctionalTests/20170202_RenameTestMergeTarget"); + this.ValidateGitCommand("reset --hard HEAD~1"); + this.ShouldNotExistOnDisk("Test_EPF_GitCommandsTestOnlyFileFolder"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + } + + [TestCase] + public void ResetHardWithDirectoryNameSameAsFile() + { + this.SetupForFileDirectoryTest(); + this.ValidateFileDirectoryTest(ResetHardCommand); + } + + [TestCase] + public void ResetHardWithDirectoryNameSameAsFileEnumerate() + { + this.RunFileDirectoryEnumerateTest(ResetHardCommand); + } + + [TestCase] + public void ResetHardWithDirectoryNameSameAsFileWithRead() + { + this.RunFileDirectoryReadTest(ResetHardCommand); + } + + [TestCase] + public void ResetHardWithDirectoryNameSameAsFileWithWrite() + { + this.RunFileDirectoryWriteTest(ResetHardCommand); + } + + [TestCase] + public void ResetHardDirectoryWithOneFile() + { + this.SetupForFileDirectoryTest(commandBranch: GitRepoTests.DirectoryWithFileAfterBranch); + this.ValidateFileDirectoryTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + + [TestCase] + public void ResetHardDirectoryWithOneFileEnumerate() + { + this.RunFileDirectoryEnumerateTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + + [TestCase] + public void ResetHardDirectoryWithOneFileRead() + { + this.RunFileDirectoryReadTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + + [TestCase] + public void ResetHardDirectoryWithOneFileWrite() + { + this.RunFileDirectoryWriteTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/ResetMixedTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/ResetMixedTests.cs new file mode 100644 index 0000000000..c66fe90b8a --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/ResetMixedTests.cs @@ -0,0 +1,103 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Should; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + public class ResetMixedTests : GitRepoTests + { + public ResetMixedTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void ResetMixed() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --mixed HEAD~1"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetMixedAndCheckoutNewBranch() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --mixed HEAD~1"); + + // Use RunGitCommand rather than ValidateGitCommand as G4W optimizations for "checkout -b" mean that the + // command will not report modified and deleted files + this.RunGitCommand("checkout -b tests/functional/ResetMixedAndCheckoutNewBranch"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void ResetMixedAndCheckoutOrphanBranch() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --mixed HEAD~1"); + this.ValidateGitCommand("checkout --orphan tests/functional/ResetMixedAndCheckoutOrphanBranch"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetMixedThenCheckoutWithConflicts() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --mixed HEAD~1"); + + // Because git while using the sparse-checkout feature + // will check for index merge conflicts and error out before it checks + // for untracked files that will be overwritten we just run the command + this.RunGitCommand("checkout " + GitRepoTests.ConflictSourceBranch, ignoreErrors: true); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetMixedOnlyAddedThenCheckoutWithConflicts() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --mixed HEAD~1"); + + // This will reset all the files except the files that were added + // and are untracked to make sure we error out with those using sparse-checkout + this.ValidateGitCommand("checkout -f"); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetMixedAndCheckoutFile() + { + this.ControlGitRepo.Fetch("FunctionalTests/20170602"); + + // We start with a branch that deleted two files that were present in its parent commit + this.ValidateGitCommand("checkout FunctionalTests/20170602"); + + // Then reset --mixed to the parent commit, and validate that the deleted files did not come back into the projection + this.ValidateGitCommand("reset --mixed HEAD~1"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + + // And checkout a file (without changing branches) and ensure that that doesn't update the projection either + this.ValidateGitCommand("checkout HEAD~2 .gitattributes"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + + // And now if we checkout the original commit, the deleted files should stay deleted + this.ValidateGitCommand("checkout FunctionalTests/20170602"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + } + + protected override void CreateEnlistment() + { + base.CreateEnlistment(); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/ResetSoftTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/ResetSoftTests.cs new file mode 100644 index 0000000000..80770282b7 --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/ResetSoftTests.cs @@ -0,0 +1,59 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + public class ResetSoftTests : GitRepoTests + { + public ResetSoftTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void ResetSoft() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetSoftThenCheckoutWithConflicts() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetSoftThenCheckoutNoConflicts() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.ValidateGitCommand("checkout " + GitRepoTests.NoConflictSourceBranch); + this.FilesShouldMatchAfterNoConflict(); + } + + [TestCase] + public void ResetSoftThenResetHeadThenCheckoutNoConflicts() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.ValidateGitCommand("reset HEAD Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + this.ValidateGitCommand("checkout " + GitRepoTests.NoConflictSourceBranch); + this.FilesShouldMatchAfterNoConflict(); + } + + protected override void CreateEnlistment() + { + base.CreateEnlistment(); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); + this.ControlGitRepo.Fetch(GitRepoTests.NoConflictSourceBranch); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/StatusTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/StatusTests.cs new file mode 100644 index 0000000000..5a3238e42c --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/StatusTests.cs @@ -0,0 +1,88 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Should; +using System; +using System.IO; +using System.Threading; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + public class StatusTests : GitRepoTests + { + public StatusTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void MoveFileIntoDotGitDirectory() + { + string srcPath = @"Readme.md"; + string dstPath = Path.Combine(".git", "destination.txt"); + + this.MoveFile(srcPath, dstPath); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void DeleteThenCreateThenDeleteFile() + { + string srcPath = @"Readme.md"; + + this.DeleteFile(srcPath); + this.ValidateGitCommand("status"); + this.CreateFile("Testing", srcPath); + this.ValidateGitCommand("status"); + this.DeleteFile(srcPath); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void WriteWithoutClose() + { + string srcPath = @"Readme.md"; + using (IDisposable file = this.ReadFileAndWriteWithoutClose(srcPath, "More Stuff")) + { + this.ValidGitStatusWithRetry(srcPath); + } + } + + [TestCase] + public void AppendFileUsingBash() + { + // Bash will perform the append using '>>' which will cause KAUTH_VNODE_APPEND_DATA to be sent without hydration + // Other Runners may cause hydration before append + BashRunner bash = new BashRunner(); + string filePath = Path.Combine("Test_EPF_UpdatePlaceholderTests", "LockToPreventUpdate", "test.txt"); + string content = "Apended Data"; + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + bash.AppendAllText(virtualFile, content); + bash.AppendAllText(controlFile, content); + + this.ValidateGitCommand("status"); + + // We check the contents after status, to ensure this check didn't cause the hydration + string appendedContent = string.Concat("Commit2LockToPreventUpdate \r\n", content); + virtualFile.ShouldBeAFile(this.FileSystem).WithContents(appendedContent); + controlFile.ShouldBeAFile(this.FileSystem).WithContents(appendedContent); + } + + private void ValidGitStatusWithRetry(string srcPath) + { + try + { + this.ValidateGitCommand("status"); + } + catch (Exception ex) + { + Thread.Sleep(1000); + this.ValidateGitCommand("status"); + Assert.Fail("{0} was succesful on the second try, but failed on first: {1}", nameof(this.ValidateGitCommand), ex.Message); + } + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/UpdateIndexTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/UpdateIndexTests.cs new file mode 100644 index 0000000000..444edec354 --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/UpdateIndexTests.cs @@ -0,0 +1,75 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Tools; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + public class UpdateIndexTests : GitRepoTests + { + public UpdateIndexTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + [Ignore("TODO 940287: git update-index --remove does not check if the file is on disk if the skip-worktree bit is set")] + public void UpdateIndexRemoveFileOnDisk() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void UpdateIndexRemoveFileOnDiskDontCheckStatus() + { + // TODO 940287: Remove this test and re-enable UpdateIndexRemoveFileOnDisk + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + + // git-status will not match because update-index --remove does not check what is on disk if the skip-worktree bit is set, + // meaning it will always remove the file from the index + GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + + // Add the files back to the index so the git-status that is run during teardown matches + GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + } + + [TestCase] + public void UpdateIndexRemoveAddFileOpenForWrite() + { + // TODO 940287: Remove this test and re-enable UpdateIndexRemoveFileOnDisk + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + + // git-status will not match because update-index --remove does not check what is on disk if the skip-worktree bit is set, + // meaning it will always remove the file from the index + GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + + // Add the files back to the index so the git-status that is run during teardown matches + GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + } + + [TestCase] + public void UpdateIndexWithCacheInfo() + { + // Update Protocol.md with the contents from blob 583f1... + string command = $"update-index --cacheinfo 100644 \"583f1a56db7cc884d54534c5d9c56b93a1e00a2b\n\" Protocol.md"; + + this.ValidateGitCommand(command); + } + + protected override void CreateEnlistment() + { + base.CreateEnlistment(); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/UpdateRefTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/UpdateRefTests.cs new file mode 100644 index 0000000000..f31621ac3d --- /dev/null +++ b/Scalar.FunctionalTests/Tests/GitCommands/UpdateRefTests.cs @@ -0,0 +1,48 @@ +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Tools; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + public class UpdateRefTests : GitRepoTests + { + public UpdateRefTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void UpdateRefModifiesHead() + { + this.ValidateGitCommand("status"); + this.ValidateGitCommand("update-ref HEAD f1bce402a7a980a8320f3f235cf8c8fdade4b17a"); + } + + [TestCase] + public void UpdateRefModifiesHeadThenResets() + { + this.ValidateGitCommand("status"); + this.ValidateGitCommand("update-ref HEAD f1bce402a7a980a8320f3f235cf8c8fdade4b17a"); + this.ValidateGitCommand("reset HEAD"); + } + + public override void TearDownForTest() + { + if (FileSystemHelpers.CaseSensitiveFileSystem) + { + this.TestValidationAndCleanup(); + } + else + { + // On case-insensitive filesystems, we + // need to ignore case changes in this test because the update-ref will have + // folder names that only changed the case and when checking the folder structure + // it will create partial folders with that case and will not get updated to the + // previous case when the reset --hard running in the tear down step + this.TestValidationAndCleanup(ignoreCase: true); + } + } + } +}