From e997852281cd5d58c4c41848412ec4b5f3ddbe9f Mon Sep 17 00:00:00 2001 From: Nicklas Laine Overgaard <866244+nover@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:55:25 +0200 Subject: [PATCH 1/2] refactor: ivcs interface to allow passing working directory --- src/Vcs/Git/GitVcs.cs | 69 +++++++++++++++------ src/Vcs/IVcs.cs | 12 ++-- test/VersionCliTest.cs | 132 ++++++++++++++++++++--------------------- 3 files changed, 126 insertions(+), 87 deletions(-) diff --git a/src/Vcs/Git/GitVcs.cs b/src/Vcs/Git/GitVcs.cs index 3a3133d..116d6ed 100644 --- a/src/Vcs/Git/GitVcs.cs +++ b/src/Vcs/Git/GitVcs.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.IO; namespace Skarp.Version.Cli.Vcs.Git { @@ -10,14 +11,15 @@ public class GitVcs : IVcs /// /// Path to the cs project file that was version updated /// The message to include in the commit - public void Commit(string csProjFilePath, string message) + /// + public void Commit(string csProjFilePath, string message, string cwd = null) { - if(!LaunchGitWithArgs($"add \"{csProjFilePath}\"")) + if(!LaunchGitWithArgs($"add \"{csProjFilePath}\"", cwd: cwd)) { throw new OperationCanceledException($"Unable to add cs proj file {csProjFilePath} to git index"); } - if(!LaunchGitWithArgs($"commit -m \"{message}\"")) + if(!LaunchGitWithArgs($"commit -m \"{message}\"", cwd: cwd)) { throw new OperationCanceledException("Unable to commit"); } @@ -27,42 +29,54 @@ public void Commit(string csProjFilePath, string message) /// Determines whether the current repository is clean. /// /// - public bool IsRepositoryClean() + public bool IsRepositoryClean(string cwd = null) { - return LaunchGitWithArgs("diff-index --quiet HEAD --"); + return LaunchGitWithArgs("diff-index HEAD --", cwd: cwd); } /// /// Determines whether git is present in PATH on the current computer /// /// - public bool IsVcsToolPresent() + public bool IsVcsToolPresent(string cwd = null) { // launching `git --help` returns exit code 0 where as `git` returns 1 as git wants a cmd line argument - return LaunchGitWithArgs("--help"); + return LaunchGitWithArgs("--help", cwd: cwd); } /// /// Creates a new tag /// /// Name of the tag - public void Tag(string tagName) + /// + public void Tag(string tagName, string cwd = null) { - if(!LaunchGitWithArgs($"tag {tagName}")) + if(!LaunchGitWithArgs($"tag {tagName}", cwd: cwd)) { throw new OperationCanceledException("Unable to create tag"); } } - private static bool LaunchGitWithArgs(string args, int waitForExitTimeMs = 1000, int exitCode = 0) + /// + /// Helper method for launching git with different arguments while returning just a boolean of whether the + /// "command" was successful + /// + /// The args to pass onto git, e.g `diff` to launch `git diff` + /// How long to wait for the git operation to complete + /// The expected exit code + /// The working directory to change into, if any. Leave null for "current directory" + /// + internal static bool LaunchGitWithArgs( + string args, + int waitForExitTimeMs = 1000, + int exitCode = 0, + string cwd = null + ) { try { - var startInfo = CreateGitShellStartInfo(args); - var proc = Process.Start(startInfo); - proc.WaitForExit(waitForExitTimeMs); - - return proc.ExitCode == exitCode; + var (procExitCode, stdOut, stdErr) = LaunchGitWithArgsInner(args, waitForExitTimeMs, cwd); + return procExitCode == exitCode; } catch (Exception ex) { @@ -71,15 +85,36 @@ private static bool LaunchGitWithArgs(string args, int waitForExitTimeMs = 1000, } } - private static ProcessStartInfo CreateGitShellStartInfo(string args) + internal static (int ExitCode, string stdOut, string stdErr) LaunchGitWithArgsInner( + string args, + int waitForExitTimeMs, + string cwd = null + ) { - return new ProcessStartInfo("git") + var startInfo = CreateGitShellStartInfo(args, cwd); + var proc = Process.Start(startInfo); + proc.WaitForExit(waitForExitTimeMs); + + var stdOut = proc.StandardOutput.ReadToEnd(); + var stdErr = proc.StandardError.ReadToEnd(); + return (proc.ExitCode, stdOut,stdErr); + } + + internal static ProcessStartInfo CreateGitShellStartInfo(string args, string cwd = null) + { + var procInfo = new ProcessStartInfo("git") { Arguments = args, RedirectStandardError = true, RedirectStandardInput = true, RedirectStandardOutput = true, }; + + if (!string.IsNullOrWhiteSpace(cwd)) + { + procInfo.WorkingDirectory = cwd; + } + return procInfo; } public string ToolName() diff --git a/src/Vcs/IVcs.cs b/src/Vcs/IVcs.cs index 40a28e3..7197220 100644 --- a/src/Vcs/IVcs.cs +++ b/src/Vcs/IVcs.cs @@ -16,15 +16,17 @@ public interface IVcs /// are available in the current CLI contenxt - i.e check that `git` command can be found /// and executed /// + /// Change working directory - leave null for current directory /// true if the tool exists, false otherwise - bool IsVcsToolPresent(); + bool IsVcsToolPresent(string cwd = null); /// /// When implemented by a concrete class it returns true if the /// current HEAD of the local repository is clean - i.e no pending changes /// + /// Change working directory - leave null for current directory /// - bool IsRepositoryClean(); + bool IsRepositoryClean(string cwd = null); /// /// When implemented by a concrete class it allows to create a commit with the @@ -32,13 +34,15 @@ public interface IVcs /// /// Path to the cs project file /// The message to create the commit message with - void Commit(string csProjFilePath, string message); + /// Change working directory - leave null for current directory + void Commit(string csProjFilePath, string message, string cwd = null); /// /// When implemented by a concrete class it will tag the latest commit with the /// given tag name /// /// The name of the tag to create - i.e v1.0.2 - void Tag(string tagName); + /// Change working directory - leave null for current directory + void Tag(string tagName, string cwd = null); } } \ No newline at end of file diff --git a/test/VersionCliTest.cs b/test/VersionCliTest.cs index 32c4555..7831fb5 100644 --- a/test/VersionCliTest.cs +++ b/test/VersionCliTest.cs @@ -60,7 +60,7 @@ public void VersionCli_Bump_VersionPrefix() [Fact] public void VersionCli_throws_when_vcs_tool_is_not_present_and_doVcs_is_true() { - A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(false); + A.CallTo(() => _vcsTool.IsVcsToolPresent(null)).Returns(false); var ex = Assert.Throws(() => _cli.Execute(new VersionCliArgs{VersionBump = VersionBump.Major, DoVcs = true})); Assert.Equal("Unable to find the vcs tool _FAKE_ in your path", ex.Message); @@ -69,7 +69,7 @@ public void VersionCli_throws_when_vcs_tool_is_not_present_and_doVcs_is_true() [Fact] public void VersionCli_doesNotThrow_when_vcs_tool_is_not_present_if_doVcs_is_false() { - A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(false); + A.CallTo(() => _vcsTool.IsVcsToolPresent(null)).Returns(false); A.CallTo(() => _fileParser.Version).Returns("1.2.1"); A.CallTo(() => _fileParser.PackageVersion).Returns("1.2.1"); @@ -79,8 +79,8 @@ public void VersionCli_doesNotThrow_when_vcs_tool_is_not_present_if_doVcs_is_fal [Fact] public void VersionCli_throws_when_repo_is_not_clean_and_doVcs_is_true() { - A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); - A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(false); + A.CallTo(() => _vcsTool.IsVcsToolPresent(null)).Returns(true); + A.CallTo(() => _vcsTool.IsRepositoryClean(null)).Returns(false); var ex = Assert.Throws(() => _cli.Execute(new VersionCliArgs{VersionBump = VersionBump.Major, DoVcs = true})); Assert.Equal("You currently have uncomitted changes in your repository, please commit these and try again", @@ -90,8 +90,8 @@ public void VersionCli_throws_when_repo_is_not_clean_and_doVcs_is_true() [Fact] public void VersionCli_doesNotThrow_when_repo_is_not_clean_if_doVcs_is_false() { - A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); - A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(false); + A.CallTo(() => _vcsTool.IsVcsToolPresent(null)).Returns(true); + A.CallTo(() => _vcsTool.IsRepositoryClean(null)).Returns(false); A.CallTo(() => _fileParser.Version).Returns("1.2.1"); A.CallTo(() => _fileParser.PackageVersion).Returns("1.2.1"); @@ -102,10 +102,10 @@ public void VersionCli_doesNotThrow_when_repo_is_not_clean_if_doVcs_is_false() public void VersionCli_can_bump_versions() { // Configure - A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); - A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); - A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); - A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); + A.CallTo(() => _vcsTool.IsRepositoryClean(null)).Returns(true); + A.CallTo(() => _vcsTool.IsVcsToolPresent(null)).Returns(true); + A.CallTo(() => _vcsTool.Commit(A._, A._, null)).DoesNothing(); + A.CallTo(() => _vcsTool.Tag(A._, null)).DoesNothing(); A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); const string csProjFilePath = "/unit-test/test.csproj"; @@ -130,10 +130,10 @@ public void VersionCli_can_bump_versions() .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Commit( A.That.Matches(path => path == csProjFilePath), - A.That.Matches(msg => msg == "v2.0.0"))) + A.That.Matches(msg => msg == "v2.0.0"), null)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Tag( - A.That.Matches(tag => tag == "v2.0.0"))) + A.That.Matches(tag => tag == "v2.0.0"), null)) .MustHaveHappened(Repeated.Exactly.Once); } @@ -141,10 +141,10 @@ public void VersionCli_can_bump_versions() public void VersionCli_can_bump_pre_release_versions() { // Configure - A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); - A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); - A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); - A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); + A.CallTo(() => _vcsTool.IsRepositoryClean(null)).Returns(true); + A.CallTo(() => _vcsTool.IsVcsToolPresent(null)).Returns(true); + A.CallTo(() => _vcsTool.Commit(A._, A._ ,null)).DoesNothing(); + A.CallTo(() => _vcsTool.Tag(A._, null)).DoesNothing(); A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); const string csProjFilePath = "/unit-test/test.csproj"; @@ -169,10 +169,10 @@ public void VersionCli_can_bump_pre_release_versions() .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Commit( A.That.Matches(path => path == csProjFilePath), - A.That.Matches(msg => msg == "v2.0.0-next.0"))) + A.That.Matches(msg => msg == "v2.0.0-next.0"), null)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Tag( - A.That.Matches(tag => tag == "v2.0.0-next.0"))) + A.That.Matches(tag => tag == "v2.0.0-next.0"), null)) .MustHaveHappened(Repeated.Exactly.Once); } @@ -180,10 +180,10 @@ public void VersionCli_can_bump_pre_release_versions() public void VersionCli_can_bump_pre_release_with_custom_prefix() { // Configure - A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); - A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); - A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); - A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); + A.CallTo(() => _vcsTool.IsRepositoryClean(null)).Returns(true); + A.CallTo(() => _vcsTool.IsVcsToolPresent(null)).Returns(true); + A.CallTo(() => _vcsTool.Commit(A._, A._, null)).DoesNothing(); + A.CallTo(() => _vcsTool.Tag(A._, null)).DoesNothing(); A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); const string csProjFilePath = "/unit-test/test.csproj"; @@ -208,10 +208,10 @@ public void VersionCli_can_bump_pre_release_with_custom_prefix() .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Commit( csProjFilePath, - "v2.0.0-beta.0")) + "v2.0.0-beta.0", null)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Tag( - "v2.0.0-beta.0")) + "v2.0.0-beta.0", null)) .MustHaveHappened(Repeated.Exactly.Once); } @@ -219,10 +219,10 @@ public void VersionCli_can_bump_pre_release_with_custom_prefix() public void VersionCli_can_bump_pre_release_with_build_meta_versions() { // Configure - A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); - A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); - A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); - A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); + A.CallTo(() => _vcsTool.IsRepositoryClean(null)).Returns(true); + A.CallTo(() => _vcsTool.IsVcsToolPresent(null)).Returns(true); + A.CallTo(() => _vcsTool.Commit(A._, A._ ,null)).DoesNothing(); + A.CallTo(() => _vcsTool.Tag(A._, null)).DoesNothing(); A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); const string csProjFilePath = "/unit-test/test.csproj"; @@ -247,10 +247,10 @@ public void VersionCli_can_bump_pre_release_with_build_meta_versions() .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Commit( A.That.Matches(path => path == csProjFilePath), - A.That.Matches(msg => msg == "v2.0.0-next.0+master"))) + A.That.Matches(msg => msg == "v2.0.0-next.0+master"), null)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Tag( - A.That.Matches(tag => tag == "v2.0.0-next.0+master"))) + A.That.Matches(tag => tag == "v2.0.0-next.0+master"), null)) .MustHaveHappened(Repeated.Exactly.Once); } @@ -258,10 +258,10 @@ public void VersionCli_can_bump_pre_release_with_build_meta_versions() public void VersionCli_can_bump_versions_can_skip_vcs() { // Configure - A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); - A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); - A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); - A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); + A.CallTo(() => _vcsTool.IsRepositoryClean(null)).Returns(true); + A.CallTo(() => _vcsTool.IsVcsToolPresent(null)).Returns(true); + A.CallTo(() => _vcsTool.Commit(A._, A._, null)).DoesNothing(); + A.CallTo(() => _vcsTool.Tag(A._, null)).DoesNothing(); A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); const string csProjFilePath = "/unit-test/test.csproj"; @@ -283,18 +283,18 @@ public void VersionCli_can_bump_versions_can_skip_vcs() A.CallTo(() => _filePatcher.Flush( A.That.Matches(path => path == csProjFilePath))) .MustHaveHappened(Repeated.Exactly.Once); - A.CallTo(() => _vcsTool.Commit(A._, A._)).MustNotHaveHappened(); - A.CallTo(() => _vcsTool.Tag(A._)).MustNotHaveHappened(); + A.CallTo(() => _vcsTool.Commit(A._, A._, null)).MustNotHaveHappened(); + A.CallTo(() => _vcsTool.Tag(A._, null)).MustNotHaveHappened(); } [Fact] public void VersionCli_can_bump_versions_can_dry_run() { // Configure - A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); - A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); - A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); - A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); + A.CallTo(() => _vcsTool.IsRepositoryClean(null)).Returns(true); + A.CallTo(() => _vcsTool.IsVcsToolPresent(null)).Returns(true); + A.CallTo(() => _vcsTool.Commit(A._, A._, null)).DoesNothing(); + A.CallTo(() => _vcsTool.Tag(A._, null)).DoesNothing(); A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); const string csProjFilePath = "/unit-test/test.csproj"; @@ -320,18 +320,18 @@ public void VersionCli_can_bump_versions_can_dry_run() A.CallTo(() => _filePatcher.Flush( A.That.Matches(path => path == csProjFilePath))) .MustNotHaveHappened(); - A.CallTo(() => _vcsTool.Commit(A._, A._)).MustNotHaveHappened(); - A.CallTo(() => _vcsTool.Tag(A._)).MustNotHaveHappened(); + A.CallTo(() => _vcsTool.Commit(A._, A._, null)).MustNotHaveHappened(); + A.CallTo(() => _vcsTool.Tag(A._, null)).MustNotHaveHappened(); } [Fact] public void VersionCli_can_set_vcs_commit_message() { // Configure - A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); - A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); - A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); - A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); + A.CallTo(() => _vcsTool.IsRepositoryClean(null)).Returns(true); + A.CallTo(() => _vcsTool.IsVcsToolPresent(null)).Returns(true); + A.CallTo(() => _vcsTool.Commit(A._, A._, null)).DoesNothing(); + A.CallTo(() => _vcsTool.Tag(A._, null)).DoesNothing(); A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); const string csProjFilePath = "/unit-test/test.csproj"; @@ -356,10 +356,10 @@ public void VersionCli_can_set_vcs_commit_message() .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Commit( A.That.Matches(path => path == csProjFilePath), - A.That.Matches(msg => msg == "commit message"))) + A.That.Matches(msg => msg == "commit message"), null)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Tag( - A.That.Matches(tag => tag == "v2.0.0"))) + A.That.Matches(tag => tag == "v2.0.0"), null)) .MustHaveHappened(Repeated.Exactly.Once); } @@ -367,10 +367,10 @@ public void VersionCli_can_set_vcs_commit_message() public void VersionCli_can_set_vcs_commit_message_with_variables() { // Configure - A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); - A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); - A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); - A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); + A.CallTo(() => _vcsTool.IsRepositoryClean(null)).Returns(true); + A.CallTo(() => _vcsTool.IsVcsToolPresent(null)).Returns(true); + A.CallTo(() => _vcsTool.Commit(A._, A._, null)).DoesNothing(); + A.CallTo(() => _vcsTool.Tag(A._, null)).DoesNothing(); A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); const string csProjFilePath = "/unit-test/test.csproj"; @@ -396,10 +396,10 @@ public void VersionCli_can_set_vcs_commit_message_with_variables() .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Commit( A.That.Matches(path => path == csProjFilePath), - A.That.Matches(msg => msg == "bump from v1.2.1 to v2.0.0 at unit-test"))) + A.That.Matches(msg => msg == "bump from v1.2.1 to v2.0.0 at unit-test"), null)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Tag( - A.That.Matches(tag => tag == "v2.0.0"))) + A.That.Matches(tag => tag == "v2.0.0"), null)) .MustHaveHappened(Repeated.Exactly.Once); } @@ -407,10 +407,10 @@ public void VersionCli_can_set_vcs_commit_message_with_variables() public void VersionCli_can_set_vcs_tag() { // Configure - A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); - A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); - A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); - A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); + A.CallTo(() => _vcsTool.IsRepositoryClean(null)).Returns(true); + A.CallTo(() => _vcsTool.IsVcsToolPresent(null)).Returns(true); + A.CallTo(() => _vcsTool.Commit(A._, A._, null)).DoesNothing(); + A.CallTo(() => _vcsTool.Tag(A._, null)).DoesNothing(); A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); const string csProjFilePath = "/unit-test/test.csproj"; @@ -435,10 +435,10 @@ public void VersionCli_can_set_vcs_tag() .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Commit( A.That.Matches(path => path == csProjFilePath), - A.That.Matches(msg => msg == "v2.0.0"))) + A.That.Matches(msg => msg == "v2.0.0"), null)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Tag( - A.That.Matches(tag => tag == "vcs tag"))) + A.That.Matches(tag => tag == "vcs tag"), null)) .MustHaveHappened(Repeated.Exactly.Once); } @@ -446,10 +446,10 @@ public void VersionCli_can_set_vcs_tag() public void VersionCli_can_set_vcs_tag_with_variables() { // Configure - A.CallTo(() => _vcsTool.IsRepositoryClean()).Returns(true); - A.CallTo(() => _vcsTool.IsVcsToolPresent()).Returns(true); - A.CallTo(() => _vcsTool.Commit(A._, A._)).DoesNothing(); - A.CallTo(() => _vcsTool.Tag(A._)).DoesNothing(); + A.CallTo(() => _vcsTool.IsRepositoryClean(null)).Returns(true); + A.CallTo(() => _vcsTool.IsVcsToolPresent(null)).Returns(true); + A.CallTo(() => _vcsTool.Commit(A._, A._, null)).DoesNothing(); + A.CallTo(() => _vcsTool.Tag(A._, null)).DoesNothing(); A.CallTo(() => _fileDetector.FindAndLoadCsProj(A._)).Returns(""); const string csProjFilePath = "/unit-test/test.csproj"; @@ -475,10 +475,10 @@ public void VersionCli_can_set_vcs_tag_with_variables() .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Commit( A.That.Matches(path => path == csProjFilePath), - A.That.Matches(msg => msg == "v2.0.0"))) + A.That.Matches(msg => msg == "v2.0.0"), null)) .MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _vcsTool.Tag( - A.That.Matches(tag => tag == "bump from v1.2.1 to v2.0.0 at unit-test"))) + A.That.Matches(tag => tag == "bump from v1.2.1 to v2.0.0 at unit-test"), null)) .MustHaveHappened(Repeated.Exactly.Once); } } From 6264b57e6cb7315e8ea1a514ddee804aefe5f468 Mon Sep 17 00:00:00 2001 From: Nicklas Laine Overgaard <866244+nover@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:56:00 +0200 Subject: [PATCH 2/2] feat: add git vcs tests --- test/GitVcsTest.cs | 31 ------------ test/Vcs/Git/GitVcsTest.cs | 78 ++++++++++++++++++++++++++++++ test/Vcs/Git/GitVcsTestFixture.cs | 59 ++++++++++++++++++++++ test/target-git.zip | Bin 0 -> 19200 bytes 4 files changed, 137 insertions(+), 31 deletions(-) delete mode 100644 test/GitVcsTest.cs create mode 100644 test/Vcs/Git/GitVcsTest.cs create mode 100644 test/Vcs/Git/GitVcsTestFixture.cs create mode 100644 test/target-git.zip diff --git a/test/GitVcsTest.cs b/test/GitVcsTest.cs deleted file mode 100644 index 3fa2f71..0000000 --- a/test/GitVcsTest.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Skarp.Version.Cli.Vcs.Git; -using Xunit; - -namespace Skarp.Version.Cli.Test -{ - public class GitVcsTest - { - private readonly GitVcs _vcs; - - public GitVcsTest() - { - _vcs = new GitVcs(); - } - - [Fact( - Skip = "Dont run on build servers" - )] - public void DetectingGitOnMachineWorks() - { - Assert.True(_vcs.IsVcsToolPresent()); - } - - [Fact( - Skip = "Dont run on build servers" - )] - public void IsRepositoryCleanWorks() - { - Assert.True(_vcs.IsRepositoryClean()); - } - } -} \ No newline at end of file diff --git a/test/Vcs/Git/GitVcsTest.cs b/test/Vcs/Git/GitVcsTest.cs new file mode 100644 index 0000000..f50d240 --- /dev/null +++ b/test/Vcs/Git/GitVcsTest.cs @@ -0,0 +1,78 @@ +using System; +using System.IO; +using Skarp.Version.Cli.Vcs.Git; +using Xunit; + +namespace Skarp.Version.Cli.Test.Vcs.Git +{ + public class GitVcsTest : IClassFixture + { + private readonly GitVcsFixture _fixture; + + + public GitVcsTest(GitVcsFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public void ReturnsProperToolname() + { + Assert.Equal("git", _fixture.Vcs.ToolName()); + } + + [Fact] + public void DetectingGitOnMachineWorks() + { + Assert.True(_fixture.Vcs.IsVcsToolPresent(_fixture.AbsolutePathToGitTestDir)); + } + + [Fact] + public void IsRepositoryCleanWorks() + { + Assert.True(_fixture.Vcs.IsRepositoryClean(_fixture.AbsolutePathToGitTestDir)); + } + + [Fact] + public void CanCommit() + { + // arrange + var commitMessage = Guid.NewGuid().ToString("N"); + var fileToCommit = "dotnet-version.dll"; + File.Copy(fileToCommit, Path.Combine(_fixture.GitTestDir, fileToCommit)); + + // act + _fixture.Vcs.Commit(fileToCommit, commitMessage, _fixture.AbsolutePathToGitTestDir); + + // assert + + // grep the git-log for messages containing our guid message + var ( + exitCode, + stdOut, + _ + ) = GitVcs.LaunchGitWithArgsInner($"log --grep={commitMessage}", 1000, + _fixture.AbsolutePathToGitTestDir); + Assert.Equal(0, exitCode); + Assert.Contains(commitMessage, stdOut); + } + + [Fact] + public void CanCreateTags() + { + var tagToMake = Guid.NewGuid().ToString("N"); + + _fixture.Vcs.Tag(tagToMake, _fixture.AbsolutePathToGitTestDir); + + var (exitCode, stdOut, _) = + GitVcs.LaunchGitWithArgsInner( + "tag -l" + , 1000, + _fixture.AbsolutePathToGitTestDir + ); + + Assert.Equal(0, exitCode); + Assert.Contains(tagToMake, stdOut); + } + } +} \ No newline at end of file diff --git a/test/Vcs/Git/GitVcsTestFixture.cs b/test/Vcs/Git/GitVcsTestFixture.cs new file mode 100644 index 0000000..e6d49ba --- /dev/null +++ b/test/Vcs/Git/GitVcsTestFixture.cs @@ -0,0 +1,59 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Threading; +using Skarp.Version.Cli.Vcs.Git; + +namespace Skarp.Version.Cli.Test.Vcs.Git +{ + public class GitVcsFixture : IDisposable + { + public readonly string GitTestDir; + public readonly GitVcs Vcs; + public readonly string AbsolutePathToGitTestDir; + + public GitVcsFixture() + { + GitTestDir = "./target-git-dir"; + AbsolutePathToGitTestDir = Path.Combine( + Directory.GetCurrentDirectory(), + GitTestDir + ); + DirectoryDelete(GitTestDir, recursive: true); + + ZipFile.ExtractToDirectory("./target-git.zip", "./"); + Vcs = new GitVcs(); + + var (_, stdOut, _) = + GitVcs.LaunchGitWithArgsInner( + "config user.email", + 1000, + AbsolutePathToGitTestDir + ); + if (string.IsNullOrWhiteSpace(stdOut)) + { + GitVcs.LaunchGitWithArgs("config user.email nicklas@skarp.dk"); + GitVcs.LaunchGitWithArgs("config user.name Nicklas Laine Overgaard"); + } + } + + + private void DirectoryDelete(string dir, bool recursive) + { + try + { + Directory.Delete(dir, recursive); + } + // we don't want to fail at all if deleting the dir fails + catch (Exception ex) + { + } + } + + public void Dispose() + { + DirectoryDelete(GitTestDir, recursive: true); + } + } +} \ No newline at end of file diff --git a/test/target-git.zip b/test/target-git.zip new file mode 100644 index 0000000000000000000000000000000000000000..adcacb10fcaf580c8d4aeffa15aca61ac3edcdb5 GIT binary patch literal 19200 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 literal 0 HcmV?d00001