02.03.01 — Le type Result[T]

02.03.01 — Le type Result[T]#

Fichier source : src/ocarina/railway/result.py — zéro dépendance.

Code#

from dataclasses import dataclass, field
from typing import TypeGuard, final


class _BaseResult:
    """Base class ensuring both Ok and Fail have error attribute for type narrowing."""
    error: Exception | None


@final
@dataclass(frozen=True)
class Ok[T](_BaseResult):
    value: T
    error: None = None


@final
@dataclass(frozen=True)
class Fail(_BaseResult):
    error: Exception = field(default_factory=lambda: Exception("Unknown error"))


type Result[T] = Ok[T] | Fail


def is_ok[T](result: Result[T]) -> TypeGuard[Ok[T]]:
    return isinstance(result, Ok)


def is_fail[T](result: Result[T]) -> TypeGuard[Fail]:
    return isinstance(result, Fail)

Six décisions de design à décortiquer#

1. _BaseResult comme parent commun#

class _BaseResult:
    error: Exception | None

But unique : permettre à mypy d’accéder à result.error sans narrowing préalable. Sans cette base, on devrait écrire :

03.01 — Effect, Thunk[T], Result[T]

03.01 — Effect, Thunk[T], Result[T]#

Trois lignes de code dans custom_types/ + une dans railway/. Tout le DSL repose dessus.

Triplet#

# src/ocarina/custom_types/effect.py
type Effect = Callable[[], None]
type Effects = tuple[Effect, ...]

# src/ocarina/custom_types/thunk.py
type Thunk[T] = Callable[[], T]

# src/ocarina/railway/result.py
type Result[T] = Ok[T] | Fail
TypeSémantique FPDéfinition
EffectEffet de bord (déféré)() -> None
Thunk[T]Computation déférée avec valeur() -> T (paresse + valeur)
Result[T]Computation qui peut échouerUnion discriminée Ok[T] | Fail

Distinction Effect vs Thunk#

log_msg: Effect = lambda: print("hello")        # () -> None
get_42: Thunk[int] = lambda: 42                 # () -> int
fetch:  Thunk[Result[int]] = lambda: Ok(42)     # () -> Result[int]

Si la fonction retourne quelque chose qu’on va consommer ailleurs → Thunk[T].
Sinon → Effect.

02.03.02 — La machine à états du builder

02.03.02 — La machine à états du builder#

Fichier source : src/ocarina/dsl/testing_with_railway/internals/action_chain.py

C’est ici qu’est implémentée toute la mécanique ROP : le passage par quatre états successifs typés, qui interdit littéralement (au sens du type-checker) toute fantaisie syntaxique. Le DSL est aussi son propre système immunitaire.

Vue d’ensemble#

                      ┌─────────────────┐
   start(action)  →   │  ActionStart[T] │     un Action[T] = Thunk[Result[T]]
                      └────────┬────────┘
                               │ .failure(failure_handler)
                               ▼
                      ┌─────────────────┐
                      │ ActionFailure[T]│
                      └────────┬────────┘
                               │ .success(success_handler)
                               ▼
                      ┌─────────────────┐
                      │ ActionSuccess[T]│
                      └────────┬────────┘
                               │ .execute()   ── exécute action(), fire le handler
                               ▼
                      ┌─────────────────┐
                      │ ActionChain[T]  │     contient (has_failed, result)
                      └──────┬──────────┘
                             │ .then(next_action_or_start)
              ┌──────────────┴──────────────┐
              │                             │
   has_failed=True                   has_failed=False
              │                             │
              ▼                             ▼
   ┌──────────────────┐         ┌──────────────────┐
   │ NeutralAction-   │         │ ActionStart[T]   │  → cycle recommence
   │ Start[T]         │         │  (l'action suiv. │
   │ (rail d'échec :  │         │   sera exécutée) │
   │  toute la chaîne │         └──────────────────┘
   │  devient no-op,  │
   │ tout en gardant  │
   │ l'API fluide)    │
   └──────────────────┘
              │
              ▼ (.failure → .success → .execute, tout en no-op)
   ┌──────────────────┐
   │  ActionChain[T]  │   has_failed=True, result=<le Fail accumulé>
   └──────────────────┘

Les types Action, FailureHandler, SuccHandler#

type Action[T]        = Thunk[Result[T]]      # () -> Result[T]
type FailureHandler   = Callable[[Exception], None]
type SuccHandler      = Effect                # () -> None
  • Action[T] est un Thunk. Cela veut dire qu’au moment où ActionStart(action) est appelé, rien n’est exécuté. L’action est juste capturée.
  • FailureHandler reçoit l’exception, mais ne reçoit pas le Fail ni le Result. C’est volontaire : 99% des handlers veulent tracer l’exception, prendre un screenshot, et c’est tout.
  • SuccHandler est un Effect (sans argument). Idem : un handler de succès log un message statique, fait un screenshot, pas besoin du résultat.

Typestate Pattern : pourquoi on ne peut pas se tromper#

L’enchaînement est strictement linéaire. Chaque méthode renvoie un type différent, qui n’expose que la suite logique de l’enchaînement :

01.03 — KISS et complexité ostentatoire

01.03 — KISS et complexité ostentatoire#

La critique reçue#

Le premier retour public reçu par Ocarina, systématique d’après l’auteur :

La première « critique » a souvent été la même : « Tu te vantes de quelque chose qui est très simple. »

Le Holy Book y répond longuement. Trois piliers : le caractère opérant de la simplicité, la distinction simple ≠ peu élaboré, et le refus de toutes les contre-propositions « créatives » reçues.

Chapitre 02 — Ocarina, le framework

Chapitre 02 — Ocarina, le framework#

Ce chapitre dépile l’intégralité du framework ocarina (Python 3.14+, version 1.1.10) ainsi que du Railway Oriented Programming jusqu’aux plugins de reporting. Il est structuré comme un parcours en couches : du plus profond (le type Result[T]) vers le plus visible (le bootstrap qui démarre tout).

Plan du chapitre#

#SujetFichier ou dossier
01Identité technique, dépendances, toolchain01-identity.md
02Arborescence complète du module + schéma en couches02-module-tree.md
03Railway Oriented Programming03-railway/
04Invariants (validate, assertions, chain_validations)04-invariants/
05Orchestration (Test → TestSuite → TestCampaign → TestCycle)05-orchestration/
06Cycle de vie d’un Scenario06-scenario.md
07Watcher07-watcher.md
08POMBase08-pom-base.md
09Ports & adapters (ILogger, ITakeScreenshot)09-ports.md
10Infrastructure (pool, builder, screenshotter, act counter, adapters Selenium)10-infra/
11Couche opinionated (CLI, loggers, plugins, bootstrap)11-opinionated/
12Custom types & custom errors12-custom-types-errors.md

Lectures connexes#

Chapitre 02.03 — Railway Oriented Programming

Chapitre 02.03 — Railway Oriented Programming#

Le cœur d’Ocarina. Tout le DSL repose sur cette mécanique : représenter le succès et l’échec comme deux rails parallèles, faire qu’un échec bascule le train sur le rail d’échec, et l’y fait rester (court-circuit), préserver la composition syntaxique pour que le scénario reste lisible.

Plan#

#FichierSujet
0101-result.mdResult[T] = Ok[T] | Fail, is_ok/is_fail
0202-action-chain-states.mdMachine à états ActionStart → ActionFailure → ActionSuccess → ActionChain.
0303-neutral-rail.mdRail d’échec : NeutralActionStart / NeutralActionFailure / NeutralActionSuccess.
0404-chain-actions-fold.mdChainRunner[T] (thunk) + chain_actions (fold).
0505-create-act-hooks.mdcreate_act et ses hooks (on_failure, on_run_effect, act_counter_effect).
0606-drive-page.mddrive_page, alias sémantique.
0707-match-page-when.mdmatch_pagewhen + politique d’exceptions.

Filiation#

L’inspiration directe est le Railway Oriented Programming popularisé par Scott Wlaschin (F#).

01.04 — Citations et influences revendiquées

01.04 — Citations et influences revendiquées#

Les références citées dans le Holy Book ne sont pas neutres : elles fixent le cadre théorique d’Ocarina. On les recoupe ici par grands axes, puis on en propose une lecture d’ensemble.

Axe technique — Programmation fonctionnelle, types, langages#

InfluenceOrigineRapport avec Ocarina
Railway Oriented Programming (ROP)Scott Wlaschin (F#), pattern bien connu en programmation fonctionnelleLe cœur du framework. Result[T] = Ok[T] | Fail, short-circuit, builder fluide. Voir ../02-ocarina/03-railway/.
λ-calcul (1930)Alonzo ChurchCité comme l’invention qui rend possible la « formalisation propre » qu’Ocarina poursuit : « Ce que l’on pensait depuis les années 1930, depuis l’invention du λ-calcul, est enfin scalable à l’échelle que l’on aurait toujours voulu lui donner. »
McCulloch & Pitts (1943)Première formalisation du neurone artificielCité comme rappel que les vrais progrès en IA sont anciens et continus, contre la hype récente.
Système de types Python (PEP 695)PEP 695 (Python 3.12+)Pré-requis pour le typage générique paramétrique qui structure Ocarina (TestSuite[Driver], Result[T], etc.). Voir ../03-functional/06-pep-695-generics.md.

Axe culturel — Anti-narcissisme, simplicité, KISS bien compris#

InfluenceCitationPourquoi
Terry Davis (« le programmeur le plus intelligent qui ait jamais existé »)« Un idiot admire la complexité, un génie admire la simplicité, un physicien essaie de simplifier… »Argumentaire anti-complexité ostentatoire ; pierre angulaire du chapitre « Premiers retours ».
Lao-Tseu« Pour avoir de la connaissance, ajouter des choses chaque jour. Pour avoir de la sagesse, enlever des choses chaque jour. »Démarche de retrait : ce qui reste après élimination.
Antoine de Saint-Exupéry« La perfection est atteinte non pas lorsqu’il n’y a plus rien à ajouter, mais lorsqu’il n’y a plus rien à retirer. »Reformulation occidentale du précédent. Clos le chapitre « Premiers scénarios ».
Alan Watts« La meilleure façon de résoudre un problème est souvent d’en sortir. »Cité en fin du chapitre « Premiers jutsus » : légitime un design qui refuse certaines fonctionnalités (réactivité, async). Légitime la sortie de l’écosystème des trucs de geeks.
Rainer Maria Rilke« Pour l’instant, vivez les questions… »Clos le chapitre « Premiers pas ». Posture envers la pratique : la maîtrise vient par l’usage.
Marcel Proust« Le style (…) est une question non de technique, mais de vision. »Clos le chapitre « Extensibilité ». Justifie le refus d’imposer un DSL : la vision (POMs + actions) prime sur la technique (un format).

Axe politique — Code, souveraineté, anti-startup-nation#

InfluenceOrigineRapport avec Ocarina
Code is LawLawrence Lessig (1999), repris par Ethereum (2015) comme idéal positifCité comme la posture face à la « gouvernance démocratique du code ». Le code est la donnée brute, auditable. Le projet revendique : « De nouveau, avec l’IA, ce maillon qu’il manquait cruellement, crions-le, aussi fort que l’on criait “HACK THE PLANET” en 99 : CODE IS LAW. »
DHH — I won’t let you pay me for my open sourceDavid Heinemeier Hansson, blog persoCité pour légitimer le refus de contributions non alignées (« Fuck You » fait référence à la célèbre slide de DHH.)
Paul Graham — HatersEssai PG (paulgraham.com/fh.html)Justifie la décision de « ne JAMAIS perdre [son] temps à argumenter lorsque ce n’est pas la peine ».
Yegor Bugayenko — PromptCitation « Retry flaky blocks. »Fond la politique transient_errors + retries linéaires (TestFlow). Lire aussi : Angry Tests.
The DAO hack (2016)Hack de The DAO sur EthereumCité pour pointer que ce sont les bugs (« des satanés bugs, à cause de satanés dévs ») qui ont fait reculer l’idée de Code is Law, d’où l’importance du typage et de la rigueur.
Lee Robinson (ex-Vercel)Décembre 2025 : remplacement de Sanity par des fichiers Markdown + IA chez CursorSoutient le « retour à la donnée brute ».
Cluely (Roy Lee)Cité pour illustrer le bullshit entrepreneurial à éviterContraste explicite avec la philosophie Ocarina.

Axe identitaire / underground#

InfluenceOrigine
Lulzsec — « In the Lulzboat, salute, bitch, and show some respect. »Antisec (YTCracker, 2011)
« We Can Do All What You Can’t Do »YogyaCarderLink
YTCracker — Robots Will Definitely Take Your JobLien SoundCloud, musique de fond du chapitre « Premiers retours »
« HACK THE PLANET » (1999)Slogan emblématique de la scène hackers des années 1990 + Antisec (YTCracker, 2011)
Zone-HSite historique de defaces web
r/unixporn, r/AntiTaffSubreddits

Axe IA / outillage moderne#

InfluenceOrigineRapport avec Ocarina
Claude CodeAnthropicOutillage de référence du projet IA (ocarina-with-ai-example). Cité plusieurs fois comme « pont » remplaçant les DSLs.
IntelliCode (2018)MicrosoftCité comme repère temporel (« on y avait déjà goûté, avant même IntelliCode »).
R., savant fou du siloAnecdote personnelle (un collègue de 2013, plugin Emacs IA privé)Rappel que l’IA générative (« 15 000 lignes à supprimer pour éviter d’en écrire 1000 ») existait avant la hype publique.
Prisma, Vercel, CursorÉcosystème SaaSCités comme cas pratiques de retour à la donnée brute assisté par IA.

Cohérence d’ensemble#

Toutes ces influences pointent vers trois axes convergents :

02.03.04 — chain_actions et le ChainRunner

02.03.04 — chain_actions et le ChainRunner#

Fichier source : src/ocarina/dsl/testing_with_railway/chain_actions.py

C’est la primitive qui rend le DSL « plat ». Sans elle, un scénario à N pas serait un escalier de .then(...).failure(...).success(...).execute(). Avec elle, c’est une liste.

« Parenthesis hell »#

Solution :

# Flat, readable syntax
runner = chain_actions(
    action1.failure(h1).success(h2),
    action2.failure(h3).success(h4),
    action3.failure(h5).success(h6)
)
chain = runner.run()  # Execute when ready

Benefits :

  • Lazy evaluation : Build chain without executing
  • Flat syntax : No deep nesting
  • Automatic short-circuiting : Stops on first failure
  • Composability : ChainRunner is a value

(Extrait de docstring.)

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 :

02.03.06 — drive_page

02.03.06 — drive_page#

Fichier source : src/ocarina/opinionated/dsl/drive_page.py

Code#

def drive_page(
    first: ActionSuccess[TPOM], *rest: ActionSuccess[TPOM]
) -> ChainRunner[TPOM]:
    return chain_actions(first, *rest)

C’est exactement chain_actions.

Pourquoi cet alias ?#

1. Sémantique : « je prends le contrôle d’une page »#

Citation du Holy Book (Premiers scénarios) :

drive_page exprime que l’on prend le contrôle d’une page. Toute transition devient explicite par l’ouverture d’un nouveau drive_page.

return [
    drive_page(
        act(on_homepage, open_homepage)...,
        act(on_homepage, verify_homepage)...,
        act(on_homepage, click_cta)...,
    ),                                          # ⬅ fermeture du contrôle de la homepage
    drive_page(                                 # ⬅ ouverture du contrôle de la page suivante
        act(on_target_page, verify_target_page)...,
    ),
]

2. Discipline#

Mélanger des actes sur deux POMs différents dans un même drive_page devient une erreur mypy (cf. 02-action-chain-states.md, section « narrowing par act() »). Cela force concrètement à respecter la sémantique.