02.11.06 — bootstrap + run_plugins#
Fichier source :
src/ocarina/opinionated/launcher/bootstrap.pyPoint d’entrée d’un projet Ocarina.
Trois étapes :cycle.run_all → run_plugins → post_exec.
Code#
def bootstrap[T](
*,
test_cycle: TestCycle[T],
run_plugins: Callable[[TestCycleResults], None],
post_exec: Callable[[TestCycleResults], None] | None = None,
saturate_workers: bool = True,
) -> None:
results = test_cycle.run_all(saturate_workers=saturate_workers)
run_plugins(results)
if post_exec:
post_exec(results)Ordre#
1. cycle.run_all() → results
2. run_plugins(results)
3. post_exec(results) (optionnel)| Étape | Effet | Pourquoi cet ordre |
|---|---|---|
| 1 | Tous les tests s’exécutent | On a besoin des results pour la suite |
| 2 | Les plugins se lancent (DOCX, JSON, etc.) | Doit s’exécuter avant post_exec car peut avoir besoin du temps de génération |
| 3 | Post-exec (pretty_print + sys.exit) | Dernière chose ; après ça, le process est mort |
Typage#
run_plugins: Callable[[TestCycleResults], None]
post_exec: Callable[[TestCycleResults], None] | None = NoneLes deux reçoivent les
resultsen argument :bootstrap( post_exec=_post_exec, # signature: (results) -> None test_cycle=create_e2e_test_cycle(drivers_pool), run_plugins=lambda results: run_plugins( # signature: (results) -> None lambda: generate_docx_proof(...), # plugin (closure) lambda: generate_json_results(results=results, ...), # plugin (closure capturant results) exceptions_logger=..., ), )post_execest optionnel. S’il n’est pas fourni, on quitte juste aprèsrun_plugins.
run_plugins#
def run_plugins(*plugins: Effect, exceptions_logger: ILogger) -> None:
if not plugins:
return
def _run_plugin(plugin: Effect, exceptions_logger: ILogger) -> None:
try:
plugin()
except Exception as exc:
exceptions_logger.exception("The plugin failed.", exc=exc)
if len(plugins) == 1:
_run_plugin(plugins[0], exceptions_logger)
return
with ThreadPoolExecutor(max_workers=len(plugins)) as executor:
futures = [
executor.submit(_run_plugin, plugin, exceptions_logger)
for plugin in plugins
]
for f in futures:
f.result()| Propriété | Détail |
|---|---|
| Vide → no-op | run_plugins() ne fait rien. |
| 1 plugin → séquentiel | Pas de thread inutile. |
| N plugins → parallèle | ThreadPoolExecutor(max_workers=N). |
| Pas de propagation d’exception | Chaque plugin est isolé ; un fail log + les autres continuent. |
main.py#
ocarina-example/src/main.py :
if __name__ == "__main__":
CliStoreSingleton().push(create_selenium_auto_cli_store())
drivers_pool = create_selenium_drivers_pool(
browser=get_browser(),
driver_path=get_driver_path(),
headless=get_headless(),
wait_timeout=get_timeout(),
max_size=get_max_workers(),
profile_path=get_profile_path(),
)
logger = create_matching_logger(CliStoreSingleton().get("logger"))
logger.info("Warming up the Redis client...")
warmup_redis_client()
logger.success("Redis client initialized!")
def _post_exec(results: TestCycleResults) -> None:
print()
pretty_print_results(results, with_colors=True)
if has_test_cycle_failed(results):
sys.exit(1)
with timing(prefix="Tests duration:"):
bootstrap(
post_exec=_post_exec,
test_cycle=create_e2e_test_cycle(drivers_pool),
run_plugins=lambda results: run_plugins(
lambda: generate_docx_proof(
logs_root=get_default_log_dir() / E2E_CYCLE_NAME,
logger=create_matching_logger("terminal").set_domain_taxonomy(
("Generate DOCX proofs plugin",)
),
output_root=Path.cwd() / ".reports" / "tests_docx_output",
),
lambda: generate_json_results(
results=results,
output_dir=Path.cwd() / ".reports" / "tests_json_output",
logger=create_matching_logger("terminal").set_domain_taxonomy(
("Generate JSON report file plugin",)
),
),
exceptions_logger=PrintLogger()
.set_prefix(
lambda: concat_metadata(
format_utc_date_metadata_str,
format_current_thread_metadata_str,
)
)
.set_domain_taxonomy(("Post-execution plugins",)),
),
)- Push le
CliStore(parse + validation CLI). - Construit la
drivers_poolà partir des valeurs CLI. - Crée un logger.
- Warm-up Redis (
warmup_redis_client()). - Définit
_post_exec(pretty_print +sys.exit(1)si échec). with timing(...): mesure la durée totale.bootstrap(...): compose tout.test_cycle=create_e2e_test_cycle(drivers_pool): cycle e2e construit.run_plugins=lambda results: run_plugins(...): DOCX + JSON en parallèle, exceptions loguées.post_exec=_post_exec: print + exit.
bootstrap est typé generic [T]#
def bootstrap[T](
*,
test_cycle: TestCycle[T],
...
) -> None:Le [T] est le type du driver. Permet à mypy de vérifier que toutes les composantes ont le même type de driver. Mais : bootstrap lui-même ne fait rien avec T, il le capture juste via TestCycle[T].
try/except : fuck you#
| Si une exception remonte de… | Conséquence souhaitée |
|---|---|
test_cycle.run_all() | Crash du process (mauvaise config) |
run_plugins(results) | Selon ce que fait l’utilisateur. Le run_plugins natif suppress les erreurs ; un autre pourrait propager. |
post_exec(results) | Idem |