File: Git\GitRepositoryTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Concurrent;
using System.Diagnostics;
using Aspire.Cli.Git;
using Aspire.Cli.Telemetry;
using Aspire.Cli.Tests.Utils;
using Aspire.Tests;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Cli.Tests.Git;
 
public class GitRepositoryTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task GetIncludedFilesAsync_OutsideRepo_ReturnsNull()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var executionContext = workspace.CreateExecutionContext();
        using var profilingTelemetry = CreateProfilingTelemetry();
        var repo = new GitRepository(executionContext, new TestEnvironment(), NullLogger<GitRepository>.Instance, profilingTelemetry);
 
        var result = await repo.GetIncludedFilesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
 
        Assert.Null(result);
    }
 
    [Fact]
    public async Task GetIncludedFilesAsync_InGitRepo_ReturnsTrackedAndUntracked_ExcludingIgnored()
    {
        await GitTestHelper.EnsureGitAvailableAsync();
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await workspace.InitializeGitAsync().DefaultTimeout();
        await GitTestHelper.ConfigureGitIdentityAsync(workspace.WorkspaceRoot.FullName);
 
        // Tracked file under App/.
        var appDir = workspace.WorkspaceRoot.CreateSubdirectory("App");
        var trackedFile = Path.Combine(appDir.FullName, "AppHost.csproj");
        await File.WriteAllTextAsync(trackedFile, "Not a real project file.");
 
        // Untracked file under another subdirectory.
        var samplesDir = workspace.WorkspaceRoot.CreateSubdirectory("samples");
        var untrackedFile = Path.Combine(samplesDir.FullName, "Sample.csproj");
        await File.WriteAllTextAsync(untrackedFile, "Not a real project file.");
 
        // Gitignore rule that excludes bin/.
        var gitignorePath = Path.Combine(workspace.WorkspaceRoot.FullName, ".gitignore");
        await File.WriteAllTextAsync(gitignorePath, "bin/\n");
 
        // Ignored file under bin/.
        var binDir = workspace.WorkspaceRoot.CreateSubdirectory("bin");
        var ignoredFile = Path.Combine(binDir.FullName, "Stale.csproj");
        await File.WriteAllTextAsync(ignoredFile, "Not a real project file.");
 
        // Stage and commit the tracked file so it shows up under --cached.
        await GitTestHelper.RunGitAsync(workspace.WorkspaceRoot.FullName, "add", "App/AppHost.csproj", ".gitignore");
        await GitTestHelper.RunGitAsync(workspace.WorkspaceRoot.FullName, "commit", "-m", "init");
 
        var executionContext = workspace.CreateExecutionContext();
        using var profilingTelemetry = CreateProfilingTelemetry();
        var repo = new GitRepository(executionContext, new TestEnvironment(), NullLogger<GitRepository>.Instance, profilingTelemetry);
 
        var result = await repo.GetIncludedFilesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
 
        Assert.NotNull(result);
        Assert.Contains(Path.GetFullPath(trackedFile), result!);
        Assert.Contains(Path.GetFullPath(untrackedFile), result);
        Assert.DoesNotContain(Path.GetFullPath(ignoredFile), result);
    }
 
    [Fact]
    public async Task GetIncludedFilesAsync_DeletedTrackedFile_StillReturned()
    {
        await GitTestHelper.EnsureGitAvailableAsync();
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await workspace.InitializeGitAsync().DefaultTimeout();
        await GitTestHelper.ConfigureGitIdentityAsync(workspace.WorkspaceRoot.FullName);
 
        var trackedFile = Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj");
        await File.WriteAllTextAsync(trackedFile, "Not a real project file.");
        await GitTestHelper.RunGitAsync(workspace.WorkspaceRoot.FullName, "add", "AppHost.csproj");
        await GitTestHelper.RunGitAsync(workspace.WorkspaceRoot.FullName, "commit", "-m", "init");
 
        // Remove the file from the working tree without telling git, so it is still
        // listed by `git ls-files --cached`.
        File.Delete(trackedFile);
 
        var executionContext = workspace.CreateExecutionContext();
        using var profilingTelemetry = CreateProfilingTelemetry();
        var repo = new GitRepository(executionContext, new TestEnvironment(), NullLogger<GitRepository>.Instance, profilingTelemetry);
 
        var result = await repo.GetIncludedFilesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
 
        Assert.NotNull(result);
        Assert.Contains(Path.GetFullPath(trackedFile), result!);
    }
 
    [Fact]
    public async Task GetIncludedFilesAsync_EmitsProfilingActivityForGitProcess()
    {
        await GitTestHelper.EnsureGitAvailableAsync();
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        await workspace.InitializeGitAsync().DefaultTimeout();
        await GitTestHelper.ConfigureGitIdentityAsync(workspace.WorkspaceRoot.FullName);
 
        var trackedFile = Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj");
        await File.WriteAllTextAsync(trackedFile, "Not a real project file.");
        await GitTestHelper.RunGitAsync(workspace.WorkspaceRoot.FullName, "add", "AppHost.csproj");
        await GitTestHelper.RunGitAsync(workspace.WorkspaceRoot.FullName, "commit", "-m", "init");
 
        // ActivitySource listeners are process-wide, so this test can observe profiling spans
        // from other tests running in parallel. Use a unique session id and filter by it instead
        // of assuming every observed activity belongs to this git invocation.
        var sessionId = $"git-{Guid.NewGuid():N}";
        var startedActivities = new ConcurrentBag<Activity>();
        using var profilingTelemetry = CreateProfilingTelemetry(
            (ProfilingTelemetry.EnvironmentVariables.Enabled, "true"),
            (ProfilingTelemetry.EnvironmentVariables.SessionId, sessionId));
        using var listener = ActivityListenerHelper.Create(profilingTelemetry.ActivitySource, onActivityStarted: startedActivities.Add);
        var executionContext = workspace.CreateExecutionContext();
        var repo = new GitRepository(executionContext, new TestEnvironment(), NullLogger<GitRepository>.Instance, profilingTelemetry);
 
        var result = await repo.GetIncludedFilesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
 
        Assert.NotNull(result);
        var startedActivity = Assert.Single(startedActivities, activity =>
            IsActivityFromSession(activity, ProfilingTelemetry.Activities.Process, sessionId) &&
            activity.GetTagItem(ProfilingTelemetry.Tags.GitCommand) as string == "ls-files");
        Assert.Equal(ProfilingTelemetry.Activities.Process, startedActivity.OperationName);
        Assert.Equal("process git", startedActivity.DisplayName);
        Assert.Equal("ls-files", startedActivity.GetTagItem(ProfilingTelemetry.Tags.GitCommand));
        Assert.Equal(workspace.WorkspaceRoot.FullName, startedActivity.GetTagItem(ProfilingTelemetry.Tags.GitWorkingDirectory));
        Assert.Equal("git", startedActivity.GetTagItem(TelemetryConstants.Tags.ProcessExecutableName));
        Assert.Equal("git", startedActivity.GetTagItem(TelemetryConstants.Tags.ProcessExecutablePath));
        Assert.Equal(new[] { "ls-files", "--cached", "--others", "--exclude-standard", "-z" }, Assert.IsType<string[]>(startedActivity.GetTagItem(ProfilingTelemetry.Tags.ProcessCommandArgs)));
        Assert.Equal(5, startedActivity.GetTagItem(ProfilingTelemetry.Tags.ProcessCommandArgsCount));
        Assert.Equal(0, startedActivity.GetTagItem(TelemetryConstants.Tags.ProcessExitCode));
        Assert.True((int)startedActivity.GetTagItem(TelemetryConstants.Tags.ProcessPid)! > 0);
        Assert.True((int)startedActivity.GetTagItem(ProfilingTelemetry.Tags.GitStdoutLength)! > 0);
        Assert.Equal(sessionId, startedActivity.GetTagItem(ProfilingTelemetry.Tags.ProfilingSessionId));
    }
 
    private static bool IsActivityFromSession(Activity activity, string operationName, string sessionId)
    {
        return activity.OperationName == operationName &&
            Equals(sessionId, activity.GetTagItem(ProfilingTelemetry.Tags.ProfilingSessionId));
    }
 
    private static ProfilingTelemetry CreateProfilingTelemetry(params (string Key, string? Value)[] values)
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(values.Select(value => new KeyValuePair<string, string?>(value.Key, value.Value)))
            .Build();
        return new ProfilingTelemetry(configuration);
    }
 
}