XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.XUnit

【DLL】提供方便快捷的测试,无需编写Main方法,可直接添加特性在类或方法上进行测试

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Diagnostics;
using System.Reflection;
using XFEExtension.NetCore.XUnit.Reporting;
using XFEExtension.NetCore.XUnit.Runtime;

namespace XFEExtension.NetCore.XUnit.Execution;

internal static class WorkerProcess
{
    public static async Task<TestCaseResult> RunTestAsync(TestDescriptor descriptor, TestRunSettings settings, CancellationToken cancellationToken)
    {
        var resultPath = GetTemporaryPath("test");
        var settingsPath = GetTemporaryPath("settings");
        await File.WriteAllTextAsync(settingsPath, BuiltInReporters.Serialize(settings), cancellationToken).ConfigureAwait(false);
        var start = Stopwatch.GetTimestamp();
        Process? process = null;
        try
        {
            process = Start("--xfe-worker-test", descriptor.Id, resultPath, settingsPath);
            var stdoutTask = process.StandardOutput.ReadToEndAsync();
            var stderrTask = process.StandardError.ReadToEndAsync();
            var timeout = descriptor.TimeoutMilliseconds > 0 ? descriptor.TimeoutMilliseconds : settings.DefaultTimeoutMilliseconds;
            var completed = await WaitAsync(process, timeout, cancellationToken).ConfigureAwait(false);
            var elapsed = Stopwatch.GetElapsedTime(start);
            if (!completed)
            {
                await EnsureStoppedAsync(process).ConfigureAwait(false);
                var timedOutOutput = await stdoutTask.ConfigureAwait(false) + await stderrTask.ConfigureAwait(false);
                return new TestCaseResult(descriptor.Id, descriptor.DisplayName, TestOutcome.TimedOut, TimeSpan.Zero, elapsed, 1,
                    $"Test exceeded the {timeout} ms timeout and its worker process was terminated.", Output: timedOutOutput)
                {
                    IsLegacySingleRun = descriptor.IsLegacySingleRun,
                    TypeName = descriptor.TypeName,
                    MethodName = descriptor.MethodName
                };
            }

            var stdout = await stdoutTask.ConfigureAwait(false);
            var stderr = await stderrTask.ConfigureAwait(false);
            if (File.Exists(resultPath))
            {
                var result = BuiltInReporters.Deserialize<TestCaseResult>(await File.ReadAllTextAsync(resultPath, cancellationToken).ConfigureAwait(false));
                if (result is not null)
                    return result with { Output = string.Concat(result.Output, stdout, stderr) };
            }
            return new TestCaseResult(descriptor.Id, descriptor.DisplayName, TestOutcome.Crashed, TimeSpan.Zero, elapsed, 1,
                $"Worker exited with code {process.ExitCode} without producing a result.", Output: stdout + stderr)
            {
                IsLegacySingleRun = descriptor.IsLegacySingleRun,
                TypeName = descriptor.TypeName,
                MethodName = descriptor.MethodName
            };
        }
        finally
        {
            if (process is not null)
            {
                await EnsureStoppedAsync(process).ConfigureAwait(false);
                process.Dispose();
            }
            TryDelete(resultPath);
            TryDelete(settingsPath);
        }
    }

    public static async Task<BenchmarkSummary> RunBenchmarkAsync(BenchmarkDescriptor descriptor, BenchmarkJob job, CancellationToken cancellationToken)
    {
        var resultPath = GetTemporaryPath("benchmark");
        var settingsPath = GetTemporaryPath("settings");
        await File.WriteAllTextAsync(settingsPath, BuiltInReporters.Serialize(job), cancellationToken).ConfigureAwait(false);
        Process? process = null;
        try
        {
            process = Start("--xfe-worker-benchmark", descriptor.Id, resultPath, settingsPath);
            var stdoutTask = process.StandardOutput.ReadToEndAsync();
            var stderrTask = process.StandardError.ReadToEndAsync();
            await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
            var stdout = await stdoutTask.ConfigureAwait(false);
            var stderr = await stderrTask.ConfigureAwait(false);
            if (process.ExitCode == 0 && File.Exists(resultPath))
            {
                var result = BuiltInReporters.Deserialize<BenchmarkSummary>(await File.ReadAllTextAsync(resultPath, cancellationToken).ConfigureAwait(false));
                if (result is not null)
                    return result;
            }
            throw new InvalidOperationException($"Benchmark worker failed with exit code {process.ExitCode}.{Environment.NewLine}{stdout}{stderr}");
        }
        finally
        {
            if (process is not null)
            {
                await EnsureStoppedAsync(process).ConfigureAwait(false);
                process.Dispose();
            }
            TryDelete(resultPath);
            TryDelete(settingsPath);
        }
    }

    private static Process Start(string mode, string id, string resultPath, string settingsPath)
    {
        var processPath = Environment.ProcessPath ?? throw new InvalidOperationException("Cannot determine the current executable path.");
        var info = new ProcessStartInfo(processPath)
        {
            UseShellExecute = false,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            WorkingDirectory = Environment.CurrentDirectory
        };
        if (string.Equals(Path.GetFileNameWithoutExtension(processPath), "dotnet", StringComparison.OrdinalIgnoreCase))
            info.ArgumentList.Add(Assembly.GetEntryAssembly()?.Location ?? throw new InvalidOperationException("Cannot determine the entry assembly."));
        info.ArgumentList.Add(mode);
        info.ArgumentList.Add(id);
        info.ArgumentList.Add("--xfe-result");
        info.ArgumentList.Add(resultPath);
        info.ArgumentList.Add("--xfe-settings");
        info.ArgumentList.Add(settingsPath);
        return Process.Start(info) ?? throw new InvalidOperationException("Failed to start the worker process.");
    }

    private static async Task<bool> WaitAsync(Process process, int timeoutMilliseconds, CancellationToken cancellationToken)
    {
        if (timeoutMilliseconds <= 0)
        {
            await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
            return true;
        }
        var waitTask = process.WaitForExitAsync(cancellationToken);
        var timeoutTask = Task.Delay(timeoutMilliseconds, cancellationToken);
        return await Task.WhenAny(waitTask, timeoutTask).ConfigureAwait(false) == waitTask;
    }

    private static void Kill(Process process)
    {
        try
        {
            if (!process.HasExited)
                process.Kill(true);
        }
        catch
        {
            // The process may have exited between the checks.
        }
    }

    private static async Task EnsureStoppedAsync(Process process)
    {
        try
        {
            Kill(process);
            await process.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false);
        }
        catch
        {
            // Process teardown is best effort; the caller will report the original failure.
        }
    }

    private static string GetTemporaryPath(string kind) => Path.Combine(Path.GetTempPath(), $"xfe-{kind}-{Guid.NewGuid():N}.json");

    private static void TryDelete(string path)
    {
        try
        {
            File.Delete(path);
        }
        catch
        {
            // Temporary files are best-effort cleanup only.
        }
    }
}