Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
// ============================================================
|
||||
// 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;
|
||||
Reference in New Issue
Block a user