03.03 — Évaluation paresseuse#

Dans Ocarina, rien n’est exécuté tant qu’on ne l’a pas explicitement déclenché. C’est ce qui rend les scénarios composables comme des valeurs.

Zzz#

EndroitFormeDéclencheur
ChainRunner[T]Thunk[ActionChain[T]]runner.run()
validate(...)ValidationStartBlockValidationAssertBlock.execute()
match_page(...)retourne un ChainRunner[Any].run() (via la chaîne englobante)
Watcher.callbackCallable[[Watcher], None]_loop quand start() est appelé
logger.set_prefix(thunk)Thunk[str]recalculé à chaque appel de log
Scenario.setupteardownEffectappelé par TestExecutor
bootstrap(post_exec=...)Callable[[TestCycleResults], None]appelé après run_plugins
CliBuilder(effects_factory=lambda ns: (...))Effectsappelés après le parse argparse
test_scenario: TestScenario[Driver]Callable[[Driver, ILogger], Scenario[Driver]]appelé par Test.spawn
dispatch[mode]() dans TestCycle.run_alldict de Thunk[bool]appelé en lookup

ChainRunner#

runner = drive_page(act1, act2, act3)        # ⚠️  rien exécuté
# … plus tard …
chain = runner.run()                         # ▶︎  exécution
CapacitéSans paresseAvec paresse
Stocker un scénario dans une variableimpossible (déjà exécuté)trivial
Multiplier [runner] * 5exécute 1 fois, on a 5 références au résultatexécute 5 fois
Passer un runner à un autre runner (composition)impossibletrivial
Réordonner les act dans un test refactordifficiletrivial

validate(...).execute()#

v = validate(value, name="x").assert_that(is_positive).assert_that(is_not_zero)
# … on peut composer …
combined = chain_validations(v, other_validation)
# … rien d'exécuté jusqu'ici …
combined.execute().raise_if_invalid()        # ▶︎  exécution + agrégation

C’est ce qui permet à _ValidationChain de collecter toutes les erreurs avant d’en lever une seule (AggregateInvariantViolationError).

Watcher.callback#

watcher = Watcher(callback=my_callback, name="x", poll_interval=0.5)
# rien d'exécuté

watcher.start(driver, logger, take_screenshot)
# ▶︎  spawn d'un daemon thread qui appelle my_callback(self) toutes les 0.5 s

Conséquence : la création d’un watcher est gratuite. On peut en mettre 10 dans scenario.watchers sans coût ; ils ne tournent que pendant test_chain.

set_prefix(thunk)#

logger.set_prefix(lambda: f"[{datetime.now().isoformat()}]")

Pourquoi un Thunk ? Parce qu’on veut un préfixe différent à chaque log (timestamps changent). Si on passait une str, ce serait figé.

effects_factory(ns) -> Effects#

return CliBuilder(
    args=[...],
    effects_factory=lambda ns: (
        lambda: store.set("workers", ns.workers),
        lambda: store.set("browser", ns.browser),
        ...
        _create_validate_only_exclude_mutex_effect(ns),
        ...
    ),
)
  • effects_factory est appelé après le parse argparse, avec ns complet.
  • Il retourne un tuple d’Effect.
  • Le CliBuilder les exécute un par un.

On peut séparer la déclaration des effets ((lambda: ..., lambda: ...)) de leur exécution.

TestScenario[Driver]#

type TestScenario[Driver] = Callable[[Driver, ILogger], Scenario[Driver]]

Pas un Scenario directement :

  • driver n’existe pas au moment de la déclaration du test (il est acquis par TestFlow.run).
  • logger n’existe pas non plus (il est créé par TestSuite._create_logger).

→ On déclare le scénario en différé : « voici comment construire le Scenario quand tu auras un driver et un logger ». Le framework appelle cette factory au runtime.

dispatch[mode]()#

dispatch: dict[Mode, Thunk[bool]] = {
    "fail-fast-on-first-smoke-campaigns-sequence-fail":
        lambda: _run_smoke(fail_fast=True),
    "wait-for-all-smoke-tests":
        lambda: _run_smoke(fail_fast=False),
}

skip_all = dispatch[self._mode]()
if/elifdispatch table
Code impératifCode déclaratif
Une seule branche évaluée — c’est OKAucune branche évaluée tant qu’on ne fait pas ()
Difficile à étendreAjouter un mode = ajouter une clé
Vérification d’exhaustivité du Literal plus difficileLe checker peut vérifier que la table couvre Mode

Paresse et composition#

La paresse n’est pas une optimisation, c’est ce qui rend la composition propre.

Si validate(...).assert_that(...) s’exécutait tout de suite, on ne pourrait pas combiner avec une autre chaîne via chain_validations.

Si un ChainRunner s’exécutait à sa création, on ne pourrait pas le mettre dans une branche match_page.