243 lines
6.7 KiB
C#
243 lines
6.7 KiB
C#
// ============================================================
|
|
// EXÉCUTEUR DE TESTS MINIMAL (contournement du testhost bloqué par le bac à sable)
|
|
//
|
|
// Charge l'assembly de tests par réflexion et exécute les [Fact] / [Theory]
|
|
// en respectant Skip (comme le runner xunit). AUCUNE référence xunit à la
|
|
// compilation : les attributs sont reconnus par leur nom complet.
|
|
//
|
|
// USAGE : runner [--filter sous-chaine] [--list]
|
|
// ============================================================
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Runtime.Loader;
|
|
using System.Threading.Tasks;
|
|
|
|
string assemblyPath = Path.Combine(AppContext.BaseDirectory, "Omika.Tests.dll");
|
|
string? filtre = null;
|
|
bool listOnly = false;
|
|
|
|
for (int i = 0; i < args.Length; i++)
|
|
{
|
|
if (args[i] == "--filter" && i + 1 < args.Length)
|
|
{
|
|
filtre = args[++i];
|
|
}
|
|
else if (args[i] == "--list")
|
|
{
|
|
listOnly = true;
|
|
}
|
|
}
|
|
|
|
if (!File.Exists(assemblyPath))
|
|
{
|
|
Console.Error.WriteLine($"Assembly de tests introuvable : {assemblyPath}");
|
|
return 2;
|
|
}
|
|
|
|
Assembly tests = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
|
|
|
|
int reussis = 0;
|
|
int echoues = 0;
|
|
int ignores = 0;
|
|
var echecs = new List<string>();
|
|
|
|
foreach (Type type in tests.GetTypes().OrderBy(t => t.FullName, StringComparer.Ordinal))
|
|
{
|
|
if (!type.IsClass || type.IsAbstract)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (filtre is not null
|
|
&& !(type.FullName?.Contains(filtre, StringComparison.OrdinalIgnoreCase) ?? false))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
MethodInfo[] methodes = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
|
.OrderBy(m => m.Name, StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
if (!methodes.Any(m => EstTest(m)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (MethodInfo methode in methodes)
|
|
{
|
|
if (!EstTest(methode))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
(bool estTest, string? raisonSkip) = DetailsTest(methode);
|
|
if (!estTest)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string nom = $"{type.FullName}.{methode.Name}";
|
|
|
|
if (listOnly)
|
|
{
|
|
Console.WriteLine(nom);
|
|
continue;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(raisonSkip))
|
|
{
|
|
ignores++;
|
|
continue;
|
|
}
|
|
|
|
foreach (object[]? arguments in ArgumentsTheorie(methode))
|
|
{
|
|
string etiquette = arguments is { Length: > 0 }
|
|
? $"{nom}({string.Join(", ", arguments.Select(a => a?.ToString() ?? "null"))})"
|
|
: nom;
|
|
|
|
try
|
|
{
|
|
object? instance = Activator.CreateInstance(type);
|
|
try
|
|
{
|
|
object? resultat = methode.Invoke(instance, arguments);
|
|
if (resultat is Task tache)
|
|
{
|
|
tache.GetAwaiter().GetResult();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
(instance as IDisposable)?.Dispose();
|
|
}
|
|
|
|
reussis++;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
echoues++;
|
|
Exception reelle = ex is TargetInvocationException { InnerException: not null } invocation
|
|
? invocation.InnerException!
|
|
: ex;
|
|
string message = $"{etiquette} :: {reelle.GetType().Name} : {reelle.Message}";
|
|
if (reelle.Message.Contains("SKIP", StringComparison.Ordinal))
|
|
{
|
|
ignores++;
|
|
echoues--;
|
|
continue;
|
|
}
|
|
|
|
echecs.Add(message);
|
|
Console.WriteLine($"ECHEC {message}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!listOnly)
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine($"Résultat : {reussis} réussis, {echoues} échoués, {ignores} ignorés.");
|
|
if (echecs.Count > 0)
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine("--- ÉCHECS ---");
|
|
foreach (string echec in echecs)
|
|
{
|
|
Console.WriteLine(echec);
|
|
}
|
|
}
|
|
}
|
|
|
|
return echoues == 0 ? 0 : 1;
|
|
|
|
static bool EstTest(MethodInfo methode) => DetailsTest(methode).EstTest;
|
|
|
|
static (bool EstTest, string? RaisonSkip) DetailsTest(MethodInfo methode)
|
|
{
|
|
foreach (object attribut in methode.GetCustomAttributes(inherit: true))
|
|
{
|
|
string? nom = attribut.GetType().FullName;
|
|
if (nom is "Xunit.FactAttribute" or "Xunit.TheoryAttribute")
|
|
{
|
|
string? skip = attribut.GetType().GetProperty("Skip")?.GetValue(attribut) as string;
|
|
return (true, skip);
|
|
}
|
|
}
|
|
|
|
return (false, null);
|
|
}
|
|
|
|
static IEnumerable<object[]?> ArgumentsTheorie(MethodInfo methode)
|
|
{
|
|
bool theorie = methode.GetCustomAttributes(inherit: true)
|
|
.Any(a => a.GetType().FullName == "Xunit.TheoryAttribute");
|
|
if (!theorie)
|
|
{
|
|
yield return null;
|
|
yield break;
|
|
}
|
|
|
|
bool auMoinsUn = false;
|
|
foreach (object attribut in methode.GetCustomAttributes(inherit: true))
|
|
{
|
|
// InlineData, MemberData et ClassData partagent la même entrée publique :
|
|
// DataAttribute.GetData(MethodInfo) énumère les lignes du cas de test.
|
|
MethodInfo? getData = attribut.GetType().GetMethod("GetData", [typeof(MethodInfo)]);
|
|
if (getData is null || getData.ReturnType == typeof(void))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
object? lignesBrutes;
|
|
try
|
|
{
|
|
lignesBrutes = getData.Invoke(attribut, [methode]);
|
|
}
|
|
catch
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (lignesBrutes is not IEnumerable lignes)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (object? ligne in lignes)
|
|
{
|
|
auMoinsUn = true;
|
|
if (ligne is object?[] tableau)
|
|
{
|
|
yield return tableau;
|
|
}
|
|
else if (methode.GetParameters().Length == 1)
|
|
{
|
|
// `[InlineData(null)]` : Roslyn passe la tableau params entier à
|
|
// null, xunit le relit comme UN argument nul (un seul paramètre).
|
|
yield return [ligne];
|
|
}
|
|
else
|
|
{
|
|
yield return methode.GetParameters().Select(p => ValeurParDefaut(p.ParameterType)).ToArray();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!auMoinsUn)
|
|
{
|
|
// Théorie sans source de données exploitable : exécutée avec ses valeurs
|
|
// par défaut, uniquement pour ne pas la perdre du total.
|
|
yield return methode.GetParameters().Select(p => ValeurParDefaut(p.ParameterType)).ToArray();
|
|
}
|
|
}
|
|
|
|
static object? ValeurParDefaut(Type type)
|
|
=> type.IsValueType ? Activator.CreateInstance(type) : null;
|