02.01 — Identité technique d'Ocarina

02.01 — Identité technique d’Ocarina#

pyproject.toml#

[project]
name = "ocarina"
version = "1.1.10"
description = "Websites test framework for Igor"
requires-python = ">=3.14"
authors = [{ name="Igor Casanova", email="[REDACTED]" }]
license = "MIT"
license-files = ["LICEN[CS]E*"]
readme = "README.md"

dependencies = ["python-docx>=1.2.0"]
  1. version = "1.1.10" : le projet est en stable 1.x, pas en pré-version.
  2. requires-python = ">=3.14" : le typage générique PEP 695 est utilisé partout.
  3. dependencies = ["python-docx>=1.2.0"] : une seule dépendance d’exécution. Tout le reste est dans dev.
  4. license = "MIT".
  5. description = "Websites test framework for Igor" : pas « for everyone », pas « for humans », explicitement « for Igor ». Cohérent avec ../01-philosophy/05-political-stance.md : « C’est ma voiture. »

Dépendances dev#

[dependency-groups]
dev = [
    "ruff>=0.15.0",
    "mypy>=1.17.0",
    "mypy-extensions>=1.1.0",
    "typing-extensions>=4.14.0",
    "pytest>=9.0.0",
    "pytest-cov>=7.0.0",
    "hypothesis>=6.151.0",
    "pytest-mypy-plugins>=3.1.0",
    "allure-pytest>=2.15.3",
    "allure-python-commons>=2.15.3",
    "pre-commit>=4.5.1",
    "syrupy>=5.1.0",
    "selenium>=4.40.0",
    "playwright>=1.60.0",
    "twine>=6.2.0",
    "build>=1.4.2",
    "prysk>=0.20.0",
]
OutilRôle dans Ocarina
ruffLinter + formatter (remplace flake8 + isort + black). select = ["ALL"].
mypy (+ extensions)Type-checker. strict = true (cf. mypy.ini).
pytestRunner des tests unitaires (le framework est lui-même testé).
pytest-covCouverture de tests (du framework lui-même). Configurée dans pyproject.toml#tool.coverage.
hypothesisProperty-based testing (PBT) → test_invariants_properties.py.
pytest-mypy-pluginsTests statiques de typage (vérifie les erreurs mypy attendues et tests d’inférence de types avec reveal_type).
allure-pytestallure-python-commonsRapport Allure (déployé sur GH Pages).
pre-commitHooks Git locaux (ruff-format).
syrupySnapshot testing (sortie de pretty_print_results, results_to_json).
seleniumPrésent en dev parce qu’Ocarina ne dépend pas de Selenium pour fonctionner, livre juste un adapter Selenium pour être immédiatement opérationnel.
playwrightMême logique : un second adapter livré (depuis la 1.1.3). Ocarina pilote désormais Selenium et Playwright out of the box, derrière les mêmes ports.
twinebuildPublication PyPI.
pryskTests CLI au format cram (.t). Successeur de cram.

mypy.ini#

[mypy]
python_version = 3.14
strict = true

# * ... Allow missing annotations (type inference is cool)
disallow_incomplete_defs = false

# * ... Allow missing annotations (type inference is cool)
disallow_untyped_defs = false
  • strict = true : active --warn-redundant-casts, --warn-unused-ignores, --no-implicit-optional, --check-untyped-defs, etc.
  • Deux exceptions : disallow_incomplete_defs et disallow_untyped_defs désactivés, car l’auteur estime que l’inférence de type de mypy est suffisante quand on n’a pas besoin d’expliciter. Toutes les annotations explicites sont là où elles comptent.

pyproject.toml#tool.ruff#

[tool.ruff.lint]
select = ["ALL"]
ignore = [
    "ANN002", "ANN003", "ANN201",
    "TRY003",
    "C901",
    "D203",    # conflit avec D211
    "D213",    # conflit avec D212
    "COM812",  # conflit avec le formatter ruff (auto-fix)
]

[tool.ruff]
exclude = ["**/.venv/**", "**/bin/**", "**/__init__.py", "**/__bypass_linter__"]
  • select = ["ALL"] : toutes les règles de ruff sont activées (~800).
  • Ignore list courte : six règles seulement, toutes justifiées.
  • B008 NE doit JAMAIS être ignoré (« # "B008", # * ... NEVER ignore this rule without knowing very well what you are doing: https://docs.astral.sh/ruff/rules/function-call-in-default-argument/ »). C’est une note pour ne pas répéter une erreur passée.
  • ****/**bypass_linter**** : convention de répertoire pour héberger du code explicitement non-linté (utile pour des modules expérimentaux internes).

pyproject.toml#tool.pytest.ini_options#

testpaths = ["tests"]
python_files = ["test_*.py"]
norecursedirs = [".*", "__pycache__"]
log_cli = true
log_cli_level = "DEBUG"
log_cli_format = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
addopts = """
--cov=src
--cov-branch
--cov-report=term-missing
--cov-report=html
--cov-report=xml:coverage.xml
"""
RéglageEffet
log_cli = trueTous les logs des tests apparaissent en CLI (utile pour les scénarios qui passent par ILogger).
--cov=src + --cov-branchCouverture par branche, pas seulement par ligne.
--cov-report=html + xmlTrois sorties : terminal, HTML local, XML pour ingestion CI.

pyproject.toml#tool.coverage#

Voir le détail ici : ../04-internal-tests/07-coverage-policy.md. En bref : tout ce qui est shape (custom_types, ports, errors), ce qui requiert un navigateur réel (adapters Selenium), et ce qui est traversé par les cram tests (cli/store, cli/builder), est explicitement omis. Pour ne pas fausser les métriques.

02.02 — Arborescence du module ocarina

02.02 — Arborescence du module ocarina#

Le module Python source contient 134 fichiers .py répartis en quatre couches conceptuelles.

src/ocarina/
├── __init__.py                              # vide — pas de "facade" globale, on importe précisément
├── py.typed                                 # PEP 561 : déclare le package comme "typed"
│
├── railway/                                 # Couche 1 — primitives FP
│   ├── __init__.py
│   └── result.py                            # Ok[T] / Fail / Result[T] / is_ok / is_fail
│
├── custom_types/                            # Couche 2 — types et alias
│   ├── __init__.py
│   ├── effect.py                            # type Effect = Callable[[], None]
│   ├── thunk.py                             # type Thunk[T] = Callable[[], T]
│   ├── tpom.py                              # TypeVar TPOM bound POMBase
│   ├── supports_write.py                    # Protocol SupportsWrite[T], reflète type non exposé par lib standard Python
│   ├── built_web_driver.py                  # type BuiltWebDriver[Driver] = tuple[Driver, Effect]
│   ├── scenario.py                          # Scenario[Driver] (frozen dataclass)
│   ├── test_components.py                   # TestChain / TestSetup / TestTeardown / TestWatchers
│   ├── test_runner.py                       # TestRunner[Driver]
│   ├── oc_test.py                           # TestName / TestScenario / TestScenarioFragment
│   ├── oc_test_layers.py                    # TestResult / TestSuiteResult / TestCampaignResults / TestCycleResults
│   ├── selenium/
│   │   ├── built_web_driver.py              # BuiltSeleniumWebDriver
│   │   ├── oc_test_scenario.py
│   │   ├── supported_browsers.py            # type SupportedSeleniumBrowser = Literal["chrome", "firefox", "edge", "safari"]
│   │   └── web_drivers_pool.py              # type SeleniumWebDriversPool = WebDriversPool[WebDriver]
│   └── playwright/
│       ├── built_web_driver.py              # type BuiltPlaywrightDriver = BuiltWebDriver[PlaywrightDriver]
│       ├── oc_test_scenario.py              # PlaywrightTestScenario / PlaywrightTestScenarioFragment
│       ├── supported_browsers.py            # type SupportedPlaywrightBrowser = Literal["chromium", "firefox", "webkit"]
│       └── web_drivers_pool.py              # type PlaywrightDriversPool = WebDriversPool[PlaywrightDriver]
│
├── custom_errors/                           # Couche 2 : erreurs
│   ├── __init__.py
│   └── test_framework/
│       ├── __init__.py
│       ├── no_matching_branch.py            # NoMatchingBranchError
│       ├── pages.py                         # PageVerificationError
│       ├── driver_died.py                   # DriverDiedError
│       └── campaigns.py
│
├── custom_invariants/                       # Couche 2 : invariants prêts-à-l'emploi
│   ├── __init__.py
│   └── testing/
│       ├── __init__.py
│       ├── workers.py                       # validate_workers_amount
│       ├── oc_test_runners_ids.py           # validate_test_runners_ids
│       ├── oc_test_runners_names.py         # validate_test_runners_names (unique + valid filename)
│       ├── oc_test_suites_names.py
│       ├── oc_test_campaigns_names.py
│       └── oc_test_cycles_names.py
│
├── ports/                                   # Couche 3 : ports (interfaces)
│   ├── __init__.py
│   ├── ilogger.py                           # ILogger (ABC)
│   └── itake_screenshot.py                  # ITakeScreenshot[Driver] (Protocol)
│
├── pom/                                     # Couche 3 : Page Object Model
│   ├── __init__.py
│   ├── base.py                              # POMBase (ABC : verify + get_current_title)
│   ├── selenium/
│   │   ├── __init__.py
│   │   └── muted.py                         # MutedPOM utilitaire
│   └── playwright/
│       ├── __init__.py
│       └── muted.py                         # MutedPlaywrightPOM utilitaire
│
├── aggregates/                              # Couche 3 : agrégats de résultats
│   ├── __init__.py
│   └── tests_layers.py                      # is_test_result_ok / _fail / _skipped (TypeGuard)
│
├── dsl/                                     # Couche 4 : DSL Ocarina
│   ├── __init__.py
│   ├── invariants/
│   │   ├── __init__.py
│   │   ├── validate.py                      # entry-point validate(), BusinessInvariantValidator, FrameworkInvariantValidator
│   │   ├── assertions.py                    # is_str / is_email / is_positive / is_in / has_unique_elements / each / ...
│   │   ├── errors.py                        # InvariantViolationError / DuplicatesError / AggregateInvariantViolationError
│   │   └── internals/
│   │       ├── __init__.py
│   │       └── validation_chain.py          # ValidationStartBlock / ValidationAssertBlock / _ValidationChain / _any_of / chain_validations
│   ├── testing/
│   │   ├── __init__.py
│   │   ├── oc_test.py                       # class Test[Driver]
│   │   ├── oc_test_suite.py                 # class TestSuite[Driver]
│   │   ├── oc_test_campaign.py              # class TestCampaign[Driver] + campaign_has_failed
│   │   ├── oc_test_cycle.py                 # class TestCycle[Driver] + type Mode + has_test_cycle_failed
│   │   ├── filter_tests_by_ids.py
│   │   ├── watcher.py                       # class Watcher[Driver]
│   │   ├── internals/
│   │   │   ├── __init__.py
│   │   │   ├── test_executor.py             # class TestExecutor[Driver] + ExecutionOutcome (frozen, slots)
│   │   │   └── test_flow.py                 # class TestFlow[Driver]
│   │   ├── selenium/
│   │   │   ├── __init__.py
│   │   │   ├── create_test.py               # create_selenium_test(...)
│   │   │   └── create_watcher.py            # create_selenium_watcher(...) + type SeleniumWatcher
│   │   └── playwright/
│   │       ├── __init__.py
│   │       ├── create_test.py               # create_playwright_test(...)
│   │       └── create_watcher.py            # create_playwright_watcher(...) + type PlaywrightWatcher
│   └── testing_with_railway/
│       ├── __init__.py
│       ├── chain_actions.py                 # class ChainRunner[T] + chain_actions
│       ├── match_page.py                    # When + _match_page_builder + create_match_page
│       ├── constructors/
│       │   ├── __init__.py
│       │   └── create_act.py                # primitive de bas niveau (à wrapper côté projet)
│       └── internals/
│           ├── __init__.py
│           └── action_chain.py              # ActionStart → ActionFailure → ActionSuccess → ActionChain
│                                            # + Neutral{Start,Failure,Success}  (rail d'échec)
│
├── infra/                                   # Couche 5 : infrastructure
│   ├── __init__.py
│   ├── drivers_pool.py                      # class WebDriversPool[Driver] + WarmupTimeoutError
│   ├── driver_builder.py                    # class DriverBuilder[Driver]
│   ├── screenshotter.py                     # class Screenshotter[TDriver] + ScreenshotterConfig
│   ├── act_counter.py                       # class ActCounter (interface)
│   ├── selenium/
│   │   ├── __init__.py
│   │   ├── create_driver.py                 # _build_firefox / _build_chrome / _build_edge / _build_safari
│   │   ├── create_drivers_pool.py           # create_selenium_drivers_pool
│   │   ├── create_screenshotter.py          # create_selenium_screenshotter
│   │   ├── driver_healthcheck.py            # driver_healthcheck (ping driver.title)
│   │   └── mixins.py                        # SeleniumTitleMixin (détrompeur de typage)
│   └── playwright/
│       ├── __init__.py
│       ├── create_driver.py                 # create_playwright_driver (chromium / firefox / webkit)
│       ├── create_drivers_pool.py           # create_playwright_drivers_pool
│       ├── create_screenshotter.py          # create_playwright_screenshotter + _playwright_save_full_page
│       ├── driver.py                        # class PlaywrightDriver (wrapper sync)
│       ├── driver_healthcheck.py            # playwright_driver_healthcheck (ping driver.title)
│       └── mixins.py                        # PlaywrightTitleMixin (détrompeur de typage)
│
└── opinionated/                             # Couche 6 : opt-in, tout ce qui est "joli mais remplaçable"
    ├── __init__.py
    ├── consts/loggers_choices.py            # LOGGERS_CHOICES = ("terminal", "file", "terminal+file", "muted")
    ├── infra/
    │   ├── __init__.py
    │   └── act_counter.py                   # ThreadsBasedActCounter (compteur thread-local)
    ├── cli/
    │   ├── __init__.py
    │   ├── builder.py                       # CliBuilder + CliArg + _SilentArgumentParser
    │   ├── store.py                         # CliStore[TKeys] + _CliField[T] + field(...)
    │   ├── phantoms.py                      # phantom_validate (no-op predicate)
    │   ├── selenium/
    │   │   ├── __init__.py
    │   │   ├── cli_store_singleton.py       # SeleniumCliStoreSingleton (push / get)
    │   │   └── create_cli_store.py          # create_selenium_{auto,win,macos,linux}_cli_store
    │   └── playwright/
    │       ├── __init__.py
    │       ├── cli_store_singleton.py       # PlaywrightCliStoreSingleton (push / get)
    │       └── create_cli_store.py          # create_playwright_{,auto_}cli_store
    ├── dsl/
    │   ├── __init__.py
    │   └── drive_page.py                    # drive_page = chain_actions, alias sémantique
    ├── launcher/
    │   ├── __init__.py
    │   └── bootstrap.py                     # bootstrap(...) + run_plugins(*plugins, exceptions_logger)
    ├── loggers/
    │   ├── __init__.py
    │   ├── create_matching_logger.py        # create_matching_logger("terminal"|"file"|...) + get_default_log_dir
    │   ├── print_logger.py                  # PrintLogger (ANSI)
    │   ├── file_logger.py                   # FileLogger (taxonomie -> arbre de fichiers)
    │   ├── print_and_file_logger.py
    │   ├── muted_logger.py
    │   ├── custom_types/supported_loggers.py
    │   └── utils/
    │       ├── __init__.py
    │       └── format_metadata_str.py       # format_utc_date_metadata_str / format_current_thread_metadata_str / concat_metadata
    └── plugins/
        ├── __init__.py
        └── reports/
            ├── __init__.py
            ├── pretty_print_results.py      # rapport ANSI hiérarchique
            ├── results_to_json.py           # générateur JSON
            ├── docx_tests_proofs.py         # générateur DOCX (consomme l'arbre de logs)
            └── timing.py                    # context manager `with timing(prefix="Duration:")`

Schéma en couches#

┌──────────────────────────────────────────────────────────────────────────┐
│ USER PROJECT (e.g. ocarina-example, ocarina-with-ai-example)             │
│  ┌────────────────────┐  ┌────────────────────┐  ┌─────────────────────┐ │
│  │ pages/ (POMBase)   │  │ scenarios/ (Test)  │  │ adapters/ (act, ...)│ │
│  └────────────────────┘  └────────────────────┘  └─────────────────────┘ │
└─────────────────────────────────┬────────────────────────────────────────┘
                                  │ imports
┌─────────────────────────────────▼────────────────────────────────────────┐
│ COUCHE 6 — opinionated/  (CLI, loggers, plugins, bootstrap)              │
│   opt-in. Peut être remplacée intégralement.                             │
└─────────────────────────────────┬────────────────────────────────────────┘
                                  │
┌─────────────────────────────────▼────────────────────────────────────────┐
│ COUCHE 5 — infra/  (pool, builder, screenshotter, adapters Selenium)     │
│   I/O. Pas de logique DSL.                                               │
└─────────────────────────────────┬────────────────────────────────────────┘
                                  │
┌─────────────────────────────────▼────────────────────────────────────────┐
│ COUCHE 4 — dsl/  (testing, invariants, testing_with_railway)             │
│   DSL pur, sans I/O. Toute la grammaire.                                 │
└─────────────────────────────────┬────────────────────────────────────────┘
                                  │
┌─────────────────────────────────▼────────────────────────────────────────┐
│ COUCHE 3 — ports/  +  pom/  +  aggregates/                               │
│   Interfaces et abstractions.                                            │
└─────────────────────────────────┬────────────────────────────────────────┘
                                  │
┌─────────────────────────────────▼────────────────────────────────────────┐
│ COUCHE 2 — custom_types/  +  custom_errors/  +  custom_invariants/       │
│   Shapes, alias, types, exceptions, invariants prêts-à-l'emploi.       │
└─────────────────────────────────┬────────────────────────────────────────┘
                                  │
┌─────────────────────────────────▼────────────────────────────────────────┐
│ COUCHE 1 — railway/  (Result, Ok, Fail, is_ok, is_fail)                  │
│   La primitive fonctionnelle. Aucune dépendance.                         │
└──────────────────────────────────────────────────────────────────────────┘

Cette stratification stricte garantit l’absence de cycles d’import et permet de remplacer chaque couche supérieure sans toucher aux couches inférieures :

02.06 — Scenario[Driver]

02.06 — Scenario[Driver]#

Fichier source : src/ocarina/custom_types/scenario.py

Dataclass#

@final
@dataclass(frozen=True)
class Scenario[Driver]:
    test_chain: TestChain
    setup: TestSetup = field(default=None)
    teardown: TestTeardown = field(default=None)
    watchers: TestWatchers[Driver] | None = field(default=None)
# src/ocarina/custom_types/test_components.py
type TestChain = Sequence[ChainRunner[Any]]
type TestSetup = Effect | None
type TestTeardown = Effect | None
type TestWatchers[Driver] = Sequence[Watcher[Driver]] | None

Cycle de vie (per attempt)#

1. setup()           — optionnel, Effect libre (DB, API, …)
       → lève     :  skip de test_chain, jump à teardown,
                     return Outcome(setup_failed=True, should_retry=True)
       → ok       :  continue à test_chain

2. test_chain        — la chaîne réelle (Sequence[ChainRunner])
       (les watchers tournent pendant ce temps)

3. teardown()        — optionnel, toujours exécuté
       → lève     :  log warning + ignore (n'affecte pas le verdict)

Si TOUTES les tentatives lèvent au setup :
    → test marqué SKIPPED (pas FAILED)
    → log warning « setup keeps failing »

setup et teardown#

setup et teardown sont driver-free et injectionless by design.

02.07 — Watcher[Driver]

02.07 — Watcher[Driver]#

Fichier source : src/ocarina/dsl/testing/watcher.py

Observateur parallélisé qui tourne en daemon thread aux côtés de la test_chain. Conçu pour détecter les frictions imprévisibles et sans impact direct sur le scénario qui se produisent pendant que le scénario tourne : toasts d’erreur aléatoires, validations parasites, popups inattendus.

Le problème#

Citation du Holy Book (handling-flakiness) :

Plus surprenant encore : des applications affichant des toasts d’erreur sans raison apparente, ou des formulaires signalant des erreurs de validation sur des saisies pourtant correctes, sans pour autant bloquer le parcours.

02.08 — POMBase

02.08 — POMBase#

Fichier source : src/ocarina/pom/base.py

Base abstraite framework-agnostic du Page Object Model. Deux méthodes obligatoires. Aucune mention de Selenium.

Code#

class POMBase(ABC):
    @abstractmethod
    def verify(self, *, timeout: float | None = None) -> Self:
        ...

    @abstractmethod
    def get_current_title(self) -> str:
        ...

Six octets de contrat (les deux signatures). C’est tout.

Pourquoi deux méthodes#

verify(timeout=None) -> Self#

But : prouver qu’on est sur la bonne page. Sinon, lève.

  • L’argument timeout permet d’attendre jusqu’à un certain temps que la page se charge (utile pour les Selenium WebDriverWait).
  • Le default None laisse l’implémentation choisir (typiquement, lire depuis get_timeout() qui lit la CLI).
  • Le retour Self permet le method chaining fluide (MyPage(...).open().verify().click()).

C’est l’unique méthode dont on a vraiment besoin Test-side pour garantir que « je suis bien sur la page que je veux ». Toutes les actions (click_xxx, enter_xxx) sont laissées à la subclass.

02.09 — Ports : ILogger, ITakeScreenshot

02.09 — Ports : ILogger, ITakeScreenshot#

Dossier source : src/ocarina/ports/

Deux ports seulement. C’est tout. Le reste (WebDriversPool, Screenshotter, etc.) vit dans infra/, pas dans ports/. La distinction est claire : un port est une abstraction au-dessus de laquelle vit le DSL ; une infra est l’implémentation des adapters.

ILogger#

class ILogger(ABC):
    @abstractmethod
    def set_prefix(self, prefix_thunk: Thunk[str]) -> Self: ...

    @abstractmethod
    def set_domain_taxonomy(self, taxonomy: tuple[str, ...]) -> Self: ...

    @abstractmethod
    def raw(self, *args: object, stream: SupportsWrite[str] | None = None, **kwargs: object) -> None: ...

    @abstractmethod
    def critical(self, msg: str, *args, exc: Exception | None = None, **kwargs) -> None: ...

    @abstractmethod
    def error(self, msg: str, *args, exc: Exception | None = None, **kwargs) -> None: ...

    @abstractmethod
    def warning(self, msg: str, *args, exc: Exception | None = None, **kwargs) -> None: ...

    @abstractmethod
    def info(self, msg: str, *args, exc: Exception | None = None, **kwargs) -> None: ...

    @abstractmethod
    def debug(self, msg: str, *args, exc: Exception | None = None, **kwargs) -> None: ...

    @abstractmethod
    def test_name(self, msg: str, *args, exc: Exception | None = None, **kwargs) -> None: ...

    @abstractmethod
    def success(self, msg: str, *args, exc: Exception | None = None, **kwargs) -> None: ...

    @abstractmethod
    def exception(self, msg: str, *args, exc: Exception | None = None, **kwargs) -> None: ...

    @abstractmethod
    def cleanup(self) -> None: ...
MéthodeSémantique
set_prefix(thunk)Préfixe paresseux appliqué à chaque log (pour timestamps, threads…). Retourne Self pour chaining.
set_domain_taxonomy(taxonomy)Définit la hiérarchie de domaine ((cycle, campaign, suite, test)). Crucial pour le FileLogger qui crée une arborescence de fichiers.
raw(*args, stream=None)Écriture brute, sans format (utilisé par les plugins de rapport).
critical / error / warning / info / debugNiveaux standards. Chaque méthode accepte exc= pour passer une exception et la formatter.
test_name(msg)Niveau custom : annonce le test en cours (logger.test_name(test.name)).
success(msg)Niveau custom : assertion réussie (utilisé par les .success(...) handlers).
exception(msg, exc=)Logue une exception avec traceback complet.
cleanup()Fin de vie : flush + close (pour le FileLogger). Recycle le fichier de log entre les retries.

Trois choses qui ne sont pas dans ILogger#

  • Pas de set_level() : le niveau est géré par l’implémentation (ex. MutedLogger filtre tout).
  • Pas de add_handler() : pas d’API à la logging.Logger du stdlib. La composition se fait par instanciation de loggers différents (PrintAndFileLogger wrappe les deux).
  • Pas de child(name) : la taxonomy est passée en bloc avec set_domain_taxonomy.

set_prefix(thunk)#

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

Si on passait une str, ce serait calculé au moment du set_prefix, donc figé. Avec un Thunk, le préfixe est calculé à chaque appel de log. Idéal pour les timestamps.

02.12 — Custom types & custom errors

02.12 — Custom types & custom errors#

Couche shape d’Ocarina. Aucune logique : juste des types, des alias, des exceptions, des protocols. C’est ce qui rend le DSL typé sans logique runtime additionnelle.

custom_types/#

FichierContenu
effect.pytype Effect = Callable[[], None] + type Effects = tuple[Effect, ...]
thunk.pytype Thunk[T] = Callable[[], T]
tpom.pyTypeVar TPOM bound POMBase
supports_write.pyProtocol SupportsWrite[T] (write(s: T) -> Any)
built_web_driver.pytype BuiltWebDriver[Driver] = tuple[Driver, Effect]
scenario.pyScenario[Driver] (frozen dataclass)
test_components.pyTestChain, TestSetup, TestTeardown, TestWatchers[Driver]
test_runner.pyTestRunner[Driver] (frozen dataclass)
oc_test.pyTestName, TestScenario[Driver], TestScenarioFragment[Driver]
oc_test_layers.pyTestId, TestResult, TestSuiteResult, TestSuiteResults, TestCampaignResults, TestCycleResults
selenium/built_web_driver.pyBuiltSeleniumWebDriver = BuiltWebDriver[WebDriver]
selenium/oc_test_scenario.pySeleniumTestScenario = TestScenario[WebDriver]
selenium/supported_browsers.pytype SupportedSeleniumBrowser = Literal["chrome", "firefox", "edge", "safari"]
selenium/web_drivers_pool.pytype SeleniumWebDriversPool = WebDriversPool[WebDriver]

Tous ces fichiers sont excluded de la couverture (pyproject.toml#tool.coverage) :