167 lines
5.6 KiB
C#
167 lines
5.6 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using w4c_workflows.Services;
|
|
using Xunit;
|
|
|
|
namespace w4c_workflows.Tests;
|
|
|
|
/// <summary>
|
|
/// §4/S4: one async git entry point with a bounded wait, both pipes drained and
|
|
/// a short-TTL state cache. These tests cover the porcelain-v2 parser (pure),
|
|
/// the failure path, and — when git is available — a real working tree.
|
|
/// </summary>
|
|
public class GitRunnerTests
|
|
{
|
|
private static GitRunner Runner(int stateCacheSeconds = 0)
|
|
=> new(
|
|
new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["WorkflowSource:GitStateCacheSeconds"] = stateCacheSeconds.ToString(),
|
|
})
|
|
.Build(),
|
|
NullLogger<GitRunner>.Instance);
|
|
|
|
[Fact]
|
|
public void ParseState_clean_repo_reports_head_and_not_dirty()
|
|
{
|
|
var stdout = string.Join('\n',
|
|
"# branch.oid 0123456789abcdef",
|
|
"# branch.head main",
|
|
"# branch.upstream origin/main",
|
|
"# branch.ab +0 -0") + "\n";
|
|
|
|
var state = GitRunner.ParseState(new GitResult(0, stdout, "", TimedOut: false));
|
|
|
|
Assert.Equal("0123456789abcdef", state.HeadSha);
|
|
Assert.False(state.Dirty);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("1 .M N... 100644 100644 100644 aaa bbb file.txt")]
|
|
[InlineData("? new-file.txt")]
|
|
public void ParseState_any_content_line_marks_dirty(string line)
|
|
{
|
|
var stdout = "# branch.oid abc123\n# branch.head main\n" + line + "\n";
|
|
|
|
var state = GitRunner.ParseState(new GitResult(0, stdout, "", TimedOut: false));
|
|
|
|
Assert.Equal("abc123", state.HeadSha);
|
|
Assert.True(state.Dirty);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseState_unborn_branch_has_no_head()
|
|
{
|
|
var state = GitRunner.ParseState(
|
|
new GitResult(0, "# branch.oid (initial)\n# branch.head main\n", "", TimedOut: false));
|
|
|
|
Assert.Null(state.HeadSha);
|
|
Assert.False(state.Dirty);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(128)]
|
|
public void ParseState_failed_call_is_empty(int exitCode)
|
|
{
|
|
var state = GitRunner.ParseState(new GitResult(exitCode, "", "fatal: nope", TimedOut: false));
|
|
Assert.Null(state.HeadSha);
|
|
Assert.False(state.Dirty);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RunAsync_missing_working_directory_reports_failure_instead_of_throwing()
|
|
{
|
|
var missing = Path.Combine(Path.GetTempPath(), "w4c-gitrunner-" + Guid.NewGuid().ToString("N"));
|
|
|
|
var result = await Runner().RunAsync(missing, new[] { "rev-parse", "HEAD" });
|
|
|
|
Assert.False(result.Succeeded);
|
|
Assert.Equal(-1, result.ExitCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetStateAsync_reads_head_and_caches_until_invalidated()
|
|
{
|
|
if (!GitAvailable())
|
|
return; // no git on PATH — parser coverage above still runs
|
|
|
|
var dir = Directory.CreateTempSubdirectory("w4c-gitrunner-").FullName;
|
|
try
|
|
{
|
|
await GitRaw(dir, "init", "-q");
|
|
await File.WriteAllTextAsync(Path.Combine(dir, "workflow.yaml"), "name: test\n");
|
|
await GitRaw(dir, "add", "workflow.yaml");
|
|
await GitRaw(dir, "commit", "-q", "-m", "initial");
|
|
|
|
// Caching enabled: the clean state is cached…
|
|
var runner = Runner(stateCacheSeconds: 60);
|
|
var first = await runner.GetStateAsync(dir);
|
|
Assert.NotNull(first.HeadSha);
|
|
Assert.False(first.Dirty);
|
|
|
|
// …so an immediate re-read after a change still returns the cached state…
|
|
await File.WriteAllTextAsync(Path.Combine(dir, "workflow.yaml"), "name: changed\n");
|
|
var cached = await runner.GetStateAsync(dir);
|
|
Assert.False(cached.Dirty);
|
|
Assert.Equal(first.HeadSha, cached.HeadSha);
|
|
|
|
// …until the cache is dropped, when the change is observed.
|
|
runner.InvalidateState(dir);
|
|
var fresh = await runner.GetStateAsync(dir);
|
|
Assert.True(fresh.Dirty);
|
|
}
|
|
finally
|
|
{
|
|
TryDelete(dir);
|
|
}
|
|
}
|
|
|
|
private static bool GitAvailable()
|
|
{
|
|
try
|
|
{
|
|
using var p = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("git", "--version")
|
|
{
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
});
|
|
if (p == null) return false;
|
|
p.WaitForExit(5000);
|
|
return p.ExitCode == 0;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static async Task GitRaw(string workDir, params string[] args)
|
|
{
|
|
var psi = new System.Diagnostics.ProcessStartInfo("git")
|
|
{
|
|
WorkingDirectory = workDir,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
};
|
|
foreach (var a in args) psi.ArgumentList.Add(a);
|
|
psi.Environment["GIT_AUTHOR_NAME"] = "w4c test";
|
|
psi.Environment["GIT_AUTHOR_EMAIL"] = "test@w4c.local";
|
|
psi.Environment["GIT_COMMITTER_NAME"] = "w4c test";
|
|
psi.Environment["GIT_COMMITTER_EMAIL"] = "test@w4c.local";
|
|
|
|
using var p = System.Diagnostics.Process.Start(psi)!;
|
|
await p.WaitForExitAsync();
|
|
Assert.Equal(0, p.ExitCode);
|
|
}
|
|
|
|
private static void TryDelete(string dir)
|
|
{
|
|
try { Directory.Delete(dir, recursive: true); }
|
|
catch { /* best effort */ }
|
|
}
|
|
}
|