File: ExecutableFileTests.cs
Web Access
Project: src\tests\Infrastructure.Tests\Infrastructure.Tests.csproj (Infrastructure.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.ComponentModel;
using System.Diagnostics;
using Xunit;
 
namespace Infrastructure.Tests;
 
public sealed class ExecutableFileTests(ITestOutputHelper output) : IDisposable
{
    private readonly TemporaryWorkspace _workspace = TemporaryWorkspace.Create(output);
 
    [Fact]
    [SkipOnPlatform(TestPlatforms.Windows, "ETXTBSY is a Unix exec failure.")]
    public async Task WrittenExecutableCanBeExecutedImmediatelyWhileOtherThreadsStartProcesses()
    {
        const int Iterations = 1000;
        const int Spawners = 4;
 
        // Other threads keep forking, like parallel test classes in this host do. Any fork that happens while
        // this process holds a stub open for writing makes exec of that stub fail with ETXTBSY.
        using var stop = new CancellationTokenSource();
        var spawners = Enumerable.Range(0, Spawners).Select(_ => Task.Run(async () =>
        {
            while (!stop.IsCancellationRequested)
            {
                using var process = Process.Start(new ProcessStartInfo("/bin/sh", ["-c", "exit 0"]) { UseShellExecute = false })!;
                await process.WaitForExitAsync();
            }
        })).ToArray();
 
        var failures = new List<string>();
        try
        {
            for (var i = 0; i < Iterations; i++)
            {
                var path = Path.Combine(_workspace.Path, $"stub-{i}");
                await ExecutableFile.WriteAsync(path, "#!/bin/sh\nexit 7\n");
 
                try
                {
                    using var process = Process.Start(new ProcessStartInfo(path) { UseShellExecute = false })!;
                    await process.WaitForExitAsync();
                    if (process.ExitCode != 7)
                    {
                        failures.Add($"stub-{i}: exit code {process.ExitCode}");
                    }
                }
                catch (Win32Exception ex)
                {
                    failures.Add($"stub-{i}: {ex.Message}");
                }
            }
        }
        finally
        {
            stop.Cancel();
            await Task.WhenAll(spawners);
        }
 
        Assert.True(failures.Count == 0, $"{failures.Count}/{Iterations} launches failed:{Environment.NewLine}{string.Join(Environment.NewLine, failures.Take(5))}");
    }
 
    [Fact]
    public async Task WritesContentsAndExecutableMode()
    {
        var path = Path.Combine(_workspace.Path, "tool");
        var contents = "#!/usr/bin/env bash\necho 'héllo'\n";
 
        await ExecutableFile.WriteAsync(path, contents, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupExecute);
 
        Assert.Equal(contents, await File.ReadAllTextAsync(path));
        if (!OperatingSystem.IsWindows())
        {
            Assert.Equal(
                UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupExecute,
                File.GetUnixFileMode(path));
        }
    }
 
    public void Dispose() => _workspace.Dispose();
}