JustDummies

Juste des dummies, mais redoutablement efficaces.

Sections de la documentation

Inspecter un pool

Quand vous tirez d’une liste que vous avez fournie vous-même, les contraintes déclarées à côté d’elle rétrécissent cette liste : chaque valeur les satisfait ou non, et le domaine est l’ensemble de celles qui les satisfont. Une valeur qui échoue cesse simplement d’être tirée.

Avec quatre valeurs écrites à l’appel, c’est sans conséquence — vous les voyez. Dès l’instant où la liste est un catalogue que vous ne pouvez pas lire d’un coup d’œil, ça cesse de l’être :

// 2 417 prénoms, un par ligne, maintenus par quelqu'un qui n'a jamais vu ce test.
string[] firstNames = System.IO.File.ReadAllLines("first-names.txt");

string name = Any.String().OneOf(firstNames).Alpha().WithLengthBetween(2, 64).Generate();

Cette ligne d’arrange a l’air juste, et elle s’exécute. Mais Alpha() signifie lettres ASCII : chaque Anne-Marie, chaque N’Golo, chaque José du fichier a disparu en silence — quelques centaines de prénoms, peut-être, sur deux mille. Vos tests passent toujours. Ils ont simplement cessé de tirer du catalogue dont vous les croyiez issus, et rien nulle part ne le dit.

Que ce soit un défaut dépend d’une chose que la bibliothèque ne peut pas savoir : ou bien le catalogue est faux et ces prénoms n’y ont pas leur place, ou bien l’invariant est faux et Alpha() est plus strict que le code qu’il représente. Les deux réparations tiennent au même fait, et c’est celui qu’une inspection de pool rend.

Atteindre l’inspection

Les générateurs dont vous fournissez le pool implémentent IPoolInspection<T> explicitement : elle n’apparaît donc jamais parmi les contraintes pendant que vous les écrivez. Vous l’atteignez par un cast :

string[] firstNames = System.IO.File.ReadAllLines("first-names.txt");

IPoolInspection<string> pool = Any.String().OneOf(firstNames).Alpha().WithLengthBetween(2, 64);

IReadOnlyList<string>                drawable = pool.GetSurvivors();
IReadOnlyList<PoolRejection<string>> refused  = pool.GetRejections();

Rien ici ne tire. Le domaine est fixé au moment où vous déclarez les contraintes, donc les deux appels rendent la même réponse à chaque fois et sous n’importe quelle graine, et une inspection entre deux tirages laisse une exécution amorcée rejouer exactement comme elle l’aurait fait.

Lire le rapport

À cette échelle, vous ne voulez pas lire les rejets un par un — vous voulez la forme des dégâts. Chaque rejet porte la valeur et toutes les contraintes qui la refusent, et un DeclaredConstraint est une valeur comparable : le regroupement est donc le premier regard naturel :

string[] firstNames = System.IO.File.ReadAllLines("first-names.txt");

IPoolInspection<string> pool    = Any.String().OneOf(firstNames).Alpha().WithLengthBetween(2, 64);
IReadOnlyList<PoolRejection<string>> refused = pool.GetRejections();

// 214 of 2417 names never draw
Console.WriteLine($"{refused.Count} of {firstNames.Length} names never draw");

// Alpha(): 213
// WithLengthBetween(2, 64): 1
foreach (IGrouping<DeclaredConstraint, PoolRejection<string>> reason in refused.GroupBy(rejection => rejection.RejectedBy[0])) {
    Console.WriteLine($"{reason.Key}: {reason.Count()}");
}

Cette seconde ligne est toute la réponse en un chiffre : 213 prénoms perdus par Alpha(), c’est un invariant trop strict, tandis que le seul perdu par la borne de longueur est une ligne vide dans le fichier. Deux réparations différentes, distinguées sans lire un seul prénom.

Un DeclaredConstraint garde son Name et ses Arguments rendus séparés, ce qui vous permet de grouper et de filtrer par contrainte au lieu de parser du texte. Ses Arguments valent ... quand les valeurs sont de celles que la bibliothèque ne doit pas rendre — un pool de votre propre type, dont le ToString est le vôtre et pourrait être n’importe quoi. Et quand une valeur échoue pour plus d’une raison, RejectedBy les porte toutes plutôt que la première rencontrée, puisque relâcher l’une de deux raisons ne changerait rien.

Verrouiller un catalogue par un test

La raison d’être de l’inspection est que vous pouvez en faire une vérification qui s’exécute là où vit le catalogue, au lieu de constater un pool rétréci des mois plus tard :

string[] firstNames = System.IO.File.ReadAllLines("first-names.txt");

IPoolInspection<string> pool = Any.String().OneOf(firstNames).Alpha().WithLengthBetween(2, 64);

Assert.Empty(pool.GetRejections());

Ce test échoue le jour où quelqu’un ajoute un prénom que l’invariant refuse — et comme un rejet nomme la valeur comme la contrainte, l’échec dit quel prénom et quel invariant, pas seulement qu’un compte a changé. Un pool entièrement vidé ne va jamais jusque-là : un value set auquel les contraintes ne laissent rien lève une ConflictingAnyConstraintException dès la ligne d’arrange, en nommant les deux côtés.

Ce qu’elle ne fait pas

La bibliothèque rend compte ; elle ne juge pas. Elle n’avertit jamais qu’une partie de votre pool a été écartée, parce que rétrécir un catalogue partagé sur un appel précis est exactement ce à quoi sert la déclaration d’une contrainte à côté d’un value set — un générateur qui y verrait une erreur aurait tort plus souvent que raison. Tirer un prénom d’adulte d’un catalogue qui contient aussi des prénoms d’enfants, c’est le même mécanisme fonctionnant comme prévu.

L’interface est par ailleurs optionnelle. Elle est portée par tout générateur qui admet un value set que vous fournissez — Any.OneOf(...)/Any.ElementOf(...), Any.String().OneOf(...), et toutes les familles dotées d’un OneOf : les entiers, Any.Decimal(), les flottants, les dates et heures, Any.Char(), Any.Guid() et Any.Enum<T>(). Un générateur sans pool à vous ne la porte pas du tout, alors écrivez le cast comme un test quand vous ne savez pas ce que vous tenez :

IAny<string> generator = Any.String().OneOf("Camille", "Ada");

if (generator is IPoolInspection<string> inspectable && inspectable.IsPooled) {
    Console.WriteLine(inspectable.GetRejections().Count);
}

IsPooled est la seconde moitié de cette question : un générateur de chaîne qui construit sa valeur au lieu de la choisir parmi des valeurs fournies répond false, avec un rapport vide plutôt qu’une exception.

Et ce n’est pas « ce générateur a un domaine dénombrable ». Any.Int32().Between(1, 1_000_000) est parfaitement dénombrable et répond false : ce million de valeurs appartient au moteur, pas à vous. Il n’y a rien de vôtre à auditer, donc rien à rendre — l’inspection ne parle jamais que d’une liste que vous avez confiée.

Lire la source, ou la corriger là-bas ouvre un nouvel onglet· Repris depuis lib-v1.0.0-preview.6