// ============================================================ // 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(); 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 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;