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#
| Endroit | Forme | Déclencheur |
|---|---|---|
ChainRunner[T] | Thunk[ActionChain[T]] | runner.run() |
validate(...) | ValidationStartBlock / ValidationAssertBlock | .execute() |
match_page(...) | retourne un ChainRunner[Any] | .run() (via la chaîne englobante) |
Watcher.callback | Callable[[Watcher], None] | _loop quand start() est appelé |
logger.set_prefix(thunk) | Thunk[str] | recalculé à chaque appel de log |
Scenario.setup / teardown | Effect | appelé par TestExecutor |
bootstrap(post_exec=...) | Callable[[TestCycleResults], None] | appelé après run_plugins |
CliBuilder(effects_factory=lambda ns: (...)) | Effects | appelés après le parse argparse |
test_scenario: TestScenario[Driver] | Callable[[Driver, ILogger], Scenario[Driver]] | appelé par Test.spawn |
dispatch[mode]() dans TestCycle.run_all | dict 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 paresse | Avec paresse |
|---|---|---|
| Stocker un scénario dans une variable | impossible (déjà exécuté) | trivial |
Multiplier [runner] * 5 | exécute 1 fois, on a 5 références au résultat | exécute 5 fois |
Passer un runner à un autre runner (composition) | impossible | trivial |
Réordonner les act dans un test refactor | difficile | trivial |
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égationC’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 sConsé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_factoryest appelé après le parse argparse, avecnscomplet.- Il retourne un tuple d’
Effect. - Le
CliBuilderles 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 :
drivern’existe pas au moment de la déclaration du test (il est acquis parTestFlow.run).loggern’existe pas non plus (il est créé parTestSuite._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/elif | dispatch table |
|---|---|
| Code impératif | Code déclaratif |
| Une seule branche évaluée — c’est OK | Aucune branche évaluée tant qu’on ne fait pas () |
| Difficile à étendre | Ajouter un mode = ajouter une clé |
Vérification d’exhaustivité du Literal plus difficile | Le 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.