Chapitre 02.04 — Invariants

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 :

Chapitre 02.05 — Orchestration

Chapitre 02.05 — Orchestration#

Chaîne Test → TestSuite → TestCampaign → TestCycle : comment chaque niveau s’articule, qui gère la parallélisation, qui gère les rejeux, qui décide du skip, et où vivent les invariants pré-exécution.

Plan#

#FichierSujet
0101-test.mdLa classe Test[Driver] — spawn, fragments pre/post, skip.
0202-test-executor.mdTestExecutor — exécution d’une tentative, ordre : setup → watchers.start → chain → watchers.stop → teardown.
0303-test-flow-retries.mdTestFlow — boucle de rejeu (1+max_retries), backoff linéaire, gestion des setup-failures.
0404-test-suite.mdTestSuite — parallélisation avec ThreadPoolExecutor, filtrage IDs, garde-fous.
0505-saturation.mdSaturation des workers : clonage aléatoire [COPY N]. Pourquoi et comment.
0606-test-campaign.mdTestCampaign — séquence de suites, campaign_has_failed.
0707-test-cycle-modes.mdTestCycle — smoke + main, modes fail-fast vs wait-for-all, has_test_cycle_failed.
0808-filter-tests-by-ids.mdfilter_tests_by_ids — --only/--exclude, mutex, ignore les unknown IDs.

Schéma#

            ┌────────────────────────────────────────────────┐
            │                  TestCycle                     │
            │                                                │
            │  ┌──────────────────────────────────────────┐  │
            │  │ smoke_tests_campaigns                    │  │ ◄── 1er, gate
            │  │  └─ TestCampaign                         │  │     mode : fail-fast |
            │  │      └─ TestSuite                        │  │            wait-for-all
            │  │          └─ Test                         │  │
            │  └──────────────────────────────────────────┘  │
            │                                                │
            │  ┌──────────────────────────────────────────┐  │
            │  │ campaigns (main)                         │  │ ◄── skippées si
            │  │  └─ TestCampaign                         │  │     un smoke fail
            │  │      └─ TestSuite                        │  │
            │  │          └─ Test                         │  │
            │  └──────────────────────────────────────────┘  │
            └────────────────────────────────────────────────┘

Qui fait quoi ?#

NiveauResponsabilité uniqueConcurrenceHors périmètre
TestMétadonnées + spawn(driver, logger)aucuneexécution
TestExecutorUne tentativeaucunerejeu, acquisition de driver
TestFlowBoucle de rejeuaucuneparallélisation, agrégation
TestSuiteParallélisation + saturation + filtrage IDsN threadsséquence inter-suites
TestCampaignSéquence de suitessuite-levelsmoke vs main
TestCycleSmoke + main + modecampaign-levelbootstrap / plugins post-exec

Cette séparation stricte est délibérée : TestExecutor ne sait rien des retries, TestFlow ne sait rien de la concurrence, TestSuite ne sait rien de l’agrégation campaign-level. Chaque classe a un seul axe de responsabilité.

04.06 — Property-based testing (hypothesis)

04.06 — Property-based testing (hypothesis)#

Un seul fichier (test_invariants_properties.py), mais le pattern est intéressant : on génère des inputs et on vérifie des propriétés universelles des prédicats d’invariants.

Le concept#

hypothesis génère automatiquement des cas de test qui satisfont des contraintes données, puis vérifie qu’une propriété est vraie pour tous les cas générés. En cas d’échec, hypothesis fait du shrinking pour trouver le plus petit contre-exemple.

from hypothesis import given, strategies as st

@given(st.integers())
def test_is_positive_passes_on_positive(value):
    if value >= 0:
        is_positive(value)             # ne doit pas lever
    else:
        with pytest.raises(InvariantViolationError):
            is_positive(value)         # doit lever

hypothesis génère des entiers (typiquement 100 par défaut), vérifie la propriété pour chacun. Si une exception inattendue surgit, c’est un fail. Le shrinker trouve le plus petit cas qui fail (0 ? -1 ? MIN_INT ?).