File: TestDiscovery.cs
Web Access
Project: src\src\Tools\PrepareTests\PrepareTests.csproj (PrepareTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.IO.Pipes;
using System.Threading.Tasks;
using System.Threading;
using System.Text;
 
namespace PrepareTests;
internal class TestDiscovery
{
    private static readonly object s_lock = new();
 
    public static bool RunDiscovery(string repoRootDirectory, string dotnetPath, bool isUnix)
    {
        var binDirectory = Path.Combine(repoRootDirectory, "artifacts", "bin");
        var assemblies = GetAssemblies(binDirectory, isUnix);
        var testDiscoveryWorkerFolder = Path.Combine(binDirectory, "TestDiscoveryWorker");
        var (dotnetCoreWorker, dotnetFrameworkWorker) = GetWorkers(binDirectory);
 
        Console.WriteLine($"Found {assemblies.Count} test assemblies");
 
        var success = true;
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        Parallel.ForEach(assemblies, assembly =>
        {
            var workerPath = assembly.Contains("net472")
                ? dotnetFrameworkWorker
                : dotnetCoreWorker;
 
            var result = RunWorker(dotnetPath, workerPath, assembly);
            lock (s_lock)
            {
                success &= result;
            }
        });
        stopwatch.Stop();
 
        if (success)
        {
            Console.WriteLine($"Discovered tests in {stopwatch.Elapsed}");
        }
        else
        {
            Console.WriteLine($"Test discovery failed");
        }
 
        return success;
    }
 
    static (string tfm, string configuration) GetTfmAndConfiguration()
    {
        var dir = Path.GetDirectoryName(typeof(TestDiscovery).Assembly.Location);
        var tfm = Path.GetFileName(dir)!;
        var configuration = Path.GetFileName(Path.GetDirectoryName(dir))!;
        return (tfm, configuration);
    }
 
    static (string dotnetCoreWorker, string dotnetFrameworkWorker) GetWorkers(string binDirectory)
    {
        var (tfm, configuration) = GetTfmAndConfiguration();
        var testDiscoveryWorkerFolder = Path.Combine(binDirectory, "TestDiscoveryWorker");
        return (Path.Combine(testDiscoveryWorkerFolder, configuration, tfm, "TestDiscoveryWorker.dll"),
                Path.Combine(testDiscoveryWorkerFolder, configuration, "net472", "TestDiscoveryWorker.exe"));
    }
 
    static bool RunWorker(string dotnetPath, string pathToWorker, string pathToAssembly)
    {
        var success = true;
        var pipeClient = new Process();
        var arguments = new List<string>();
        if (pathToWorker.EndsWith("dll"))
        {
            arguments.Add(pathToWorker);
            pipeClient.StartInfo.FileName = dotnetPath;
        }
        else
        {
            pipeClient.StartInfo.FileName = pathToWorker;
        }
 
        var errorOutput = new StringBuilder();
 
        using (var pipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable))
        {
            // Pass the client process a handle to the server.
            arguments.Add(pipeServer.GetClientHandleAsString());
            pipeClient.StartInfo.Arguments = string.Join(" ", arguments);
            pipeClient.StartInfo.UseShellExecute = false;
 
            // Errors will be logged to stderr, redirect to us so we can capture it.
            pipeClient.StartInfo.RedirectStandardError = true;
            pipeClient.ErrorDataReceived += PipeClient_ErrorDataReceived;
            pipeClient.Start();
 
            pipeClient.BeginErrorReadLine();
 
            pipeServer.DisposeLocalCopyOfClientHandle();
 
            try
            {
                // Read user input and send that to the client process.
                using var sw = new StreamWriter(pipeServer);
                sw.AutoFlush = true;
                // Send a 'sync message' and wait for client to receive it.
                sw.WriteLine("ASSEMBLY");
                // Send the console input to the client process.
                sw.WriteLine(pathToAssembly);
            }
            // Catch the IOException that is raised if the pipe is broken
            // or disconnected.
            catch (Exception e)
            {
                Console.Error.WriteLine($"Error: {e.Message}");
                success = false;
            }
        }
 
        pipeClient.WaitForExit();
        success &= pipeClient.ExitCode == 0;
        pipeClient.Close();
 
        if (!success)
        {
            Console.WriteLine($"Failed to discover tests in {pathToAssembly}:{Environment.NewLine}{errorOutput}");
        }
 
        return success;
 
        void PipeClient_ErrorDataReceived(object sender, DataReceivedEventArgs e)
        {
            errorOutput.AppendLine(e.Data);
        }
    }
 
    private static List<string> GetAssemblies(string binDirectory, bool isUnix)
    {
        var unitTestAssemblies = Directory.GetFiles(binDirectory, "*UnitTests.dll", SearchOption.AllDirectories);
        var integrationTestAssemblies = Directory.GetFiles(binDirectory, "*IntegrationTests.dll", SearchOption.AllDirectories);
        var assemblies = unitTestAssemblies.Concat(integrationTestAssemblies).Where(ShouldInclude);
        return assemblies.ToList();
 
        bool ShouldInclude(string path)
        {
            if (isUnix)
            {
                // Our unix build will build net framework dlls for multi-targeted projects.
                // These are not valid testing on unix and discovery will throw if we try.
                return Path.GetFileName(Path.GetDirectoryName(path)) != "net472";
            }
 
            return true;
        }
    }
}