02.01 — Identité technique d'Ocarina

02.01 — Identité technique d’Ocarina#

pyproject.toml#

[project]
name = "ocarina"
version = "1.1.14"
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.14" : 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-pytest / allure-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.
twine / buildPublication 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) :