Chapitre 02.04 — Invariants#

Sous-DSL d’Ocarina dédié à l’expression d’invariants typés et composables. Utilisé partout : par la CLI, par les POMs, par les custom_invariants/testing/ qui valident la cohérence des suites avant exécution.

Plan#

#FichierSujet
0101-validate-flow.mdLe flot complet validate(value) → assert_that → execute → raise_if_invalid + la classe _ValidationChain.
0202-assertions.mdCatalogue des assertions builtin (is_str, is_email, is_positive, is_in, has_unique_elements, each, is_valid_filename, …).
0303-otherwise-any-of.md.otherwise(...) et la combinaison OR via _any_of.
0404-then-chain-of-validations.md.then(new_value) et chain_validations(...) pour la composition.
0505-business-vs-framework-validator.mdBusinessInvariantValidator vs FrameworkInvariantValidator
0606-invariant-errors.mdInvariantViolationError, DuplicatesError, AggregateInvariantViolationError.

Le pourquoi du DSL d’invariants#

  1. Garde-fou avant exécution. TestSuite.__init__ valide avant de lancer quoi que ce soit que les noms / IDs des tests sont uniques, que max_workers >= 1, etc.
  2. Validation CLI. Chaque flag a son validate=lambda chain: chain.assert_that(...) dans le CliStore. Les erreurs s’agrègent et sortent en un seul message d’erreur.
  3. Validation côté POM. Les actions de POM peuvent valider leurs paramètres (« retries doit être positif ») via validate(retries, name="retries").assert_that(is_positive).execute().raise_if_invalid().

Le Holy Book le résume :

Utilisable dans les POMs, validate permet d’exprimer des invariants sous forme de chaînes. L’exécution est différée : il faut appeler .execute() explicitement.

Le résultat expose is_valid, errors et validated_values. Il est inerte par défaut. .raise_if_invalid() remonte l’exception si besoin.

Principes de conception#

PrincipeRéalisation
Exécution différée.execute() est obligatoire. Avant ça, la chaîne est composable mais inerte.
Erreurs agrégées_ValidationChain exécute tous les steps et collecte les erreurs ; pas de fail-fast.
Composition typée.then(U) change de type ; le T est conservé dans la chaîne.
OR logique.otherwise(pred) combine avec le précédent via _any_of.
Type safetyvalidate("lol").assert_that(is_positive) provoque une erreur mypy (prédicat incompatible).
RéutilisabilitéBusinessInvariantValidator.create(...) ou FrameworkInvariantValidator.create(...) pour factoriser un invariant.

Lien fort avec ROP#

Le sous-DSL invariants suit la même grammaire que ROP :

ROPInvariants
ActionStart[T] → ActionFailure[T] → ActionSuccess[T] → ActionChain[T]ValidationStartBlock[T] → ValidationAssertBlock[T] → _ValidationResult
Composition flat via chain_actions(*)Composition flat via chain_validations(*)
Évaluation paresseuse (Thunk[ActionChain[T]])Évaluation paresseuse (.execute())
Result[T]_ValidationResult (avec is_valid, errors, validated_values)
Erreur agrégée par FailErreur agrégée par AggregateInvariantViolationError

C’est le même pattern appliqué à un autre domaine. Voir ../03-railway/ pour le pattern d’origine.