w4c-workflows-api/w4c-workflows-api.Tests/GitRunnerTests.cs
Vitali sharp8n 42ffcb9adc workflows
2026-09-13 19:28:47 +03:00

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 */ }
}
}