02.03.01 — The Result[T] type

02.03.01 — The Result[T] type#

Source file: src/ocarina/railway/result.py — zero dependencies.

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 design decisions, dissected#

1. _BaseResult as a common parent#

class _BaseResult:
    error: Exception | None

One job: let mypy read result.error without narrowing first. Without the base, you’d have to write:

02.04.01 — The validate → assert_that → execute → raise_if_invalid flow

02.04.01 — The validate → assert_that → execute → raise_if_invalid flow#

Source file: src/ocarina/dsl/invariants/validate.py (entry-point) + src/ocarina/dsl/invariants/internals/validation_chain.py

The state machine#

Diagram#

validate(value: T)
       │
       ▼
ValidationStartBlock[T]
       │
       └─ assert_that(predicate, *, msg=None)
              │
              ▼
       ValidationAssertBlock[T]   ← state accepting several continuations
              │
              ├─► assert_that(P)     [loop, AND-chainable, see dedicated diagram]
              ├─► otherwise(P)       [OR, see 03-otherwise-any-of.md]
              ├─► then(U, name=...)  [value swap, see 04-then-chain-of-validations.md]
              └─► execute()          → _ValidationResult
                                          │
                                          ├─ is_valid: bool
                                          ├─ errors: Sequence[InvariantViolationError]
                                          ├─ validated_values: Sequence[Any]
                                          └─ raise_if_invalid() : None | raise AggregateInvariantViolationError

assert_that loop#

assert_that declares one assertion on the current value. Multiple assert_that calls chain — each adds another condition (logical AND, order preserved).

02.05.01 — Test[Driver]

02.05.01 — Test[Driver]#

Source file: src/ocarina/dsl/testing/oc_test.py

Signature#

@final
class Test[Driver]:
    def __init__(
        self,
        *,
        name: TestName,
        test_id: str | None = None,
        test_scenario: TestScenario[Driver],
        pre_test_scenarios_fragments: Sequence[TestScenarioFragment[Driver]] | None = None,
        post_test_scenarios_fragments: Sequence[TestScenarioFragment[Driver]] | None = None,
        skipped: bool = False,
    ) -> None:
        if test_id is None:
            test_id = name
        self.name = name
        self.test_id = test_id
        self._test_scenario = test_scenario
        self._pre_test_scenarios_fragments = pre_test_scenarios_fragments or []
        self._post_test_scenarios_fragments = post_test_scenarios_fragments or []
        self._skipped = skipped

Six parameters:

ParameterTypeRole
nameTestName (str)Human label; appears in the report, and becomes the log file name (hence subject to is_valid_filename).
test_idstr | NoneStable identifier for --only / --exclude. If absent: takes name.
test_scenarioTestScenario[Driver] (alias = Callable[[Driver, ILogger], Scenario[Driver]])Factory that builds the Scenario at execution time.
pre_test_scenarios_fragmentsSequence[TestScenarioFragment[Driver]] | None(driver, logger) -> TestChain functions executed before the main scenario.
post_test_scenarios_fragmentsSequence[TestScenarioFragment[Driver]] | None(driver, logger) -> TestChain functions executed after the main scenario.
skippedboolIf True, the test is registered but not executed.

1. @final#

No user inheritance. Want a “special” test? Compose via fragments or a scenario — don’t subclass.

02.10.01 — WebDriversPool[Driver]

02.10.01 — WebDriversPool[Driver]#

Source file: src/ocarina/infra/drivers_pool.py

Thread-safe driver pool, max concurrency enforced by semaphore, monitored async warmup, clean shutdown. No reuse — every acquire() disposes its driver on the way out (clean state).

Constructor#

@final
class WebDriversPool[Driver]:
    def __init__(
        self,
        create_driver: Thunk[BuiltWebDriver[Driver]],
        max_size: int,
        warmup_timeout: float | None = None,
    ) -> None:
        self._create_driver = create_driver
        self._pool: Queue[BuiltWebDriver[Driver]] = Queue(max_size)
        self._semaphore = Semaphore(max_size)
        self._warmup_timeout = (
            warmup_timeout if warmup_timeout is not None and warmup_timeout > 0.1
            else 60.0 * 5
        )
PrimitiveRole
Queue(max_size)Queue of pre-built available drivers.
Semaphore(max_size)Guarantees that at most max_size drivers live simultaneously (whether queued or acquired).
warmup_timeoutMax no-progress delay during warmup before raising WarmupTimeoutError. Default 300s.

acquire()#

@contextmanager
def acquire(self) -> Iterator[Driver]:
    try:
        driver, dispose = self._pool.get_nowait()
    except Empty:
        self._semaphore.acquire()
        try:
            driver, dispose = self._create_driver()
        except Exception:
            self._semaphore.release()
            raise

    try:
        yield driver
    finally:
        with suppress(Exception):
            dispose()
        self._semaphore.release()
              acquire()
                  │
                  ▼
   ┌──────────────────────────────────┐
   │ pool.get_nowait()                │── OK ─► driver, dispose         ← warmup case
   └──────────────┬───────────────────┘
                  │ Empty
                  ▼
   ┌──────────────────────────────────┐
   │ sem.acquire()                    │  ← blocks if N drivers alive
   └──────────────┬───────────────────┘
                  ▼
   ┌──────────────────────────────────┐
   │ create_driver()                  │── raises ─► sem.release(); raise
   └──────────────┬───────────────────┘
                  ▼
   ┌──────────────────────────────────┐
   │ yield driver                     │  ← caller uses it
   └──────────────┬───────────────────┘
                  ▼
                finally :
                  with suppress : dispose()       ← driver disposed
                  sem.release()                   ← release a slot

1. No reuse#

Queue.get_nowait() consumes the entry. Once pulled, the driver never goes back. At the end (finally), it’s disposed.

02.11.01 — CliBuilder + CliArg + _SilentArgumentParser

02.11.01 — CliBuilder + CliArg + _SilentArgumentParser#

Source file: src/ocarina/opinionated/cli/builder.py

Declarative overlay on top of argparse. Aggregates validation errors, rewrites the help output on error, and registers post-parse effects.

_SilentArgumentParser#

class _SilentArgumentParser(ArgumentParser):
    def error(self, message: str) -> Never:
        """Raise an error."""
        raise ValueError(message)

The standard ArgumentParser calls sys.exit(2) directly on error. With that turd of a default, you can’t intercept and you can’t aggregate multiple errors.

_SilentArgumentParser reroutes errors as ValueError. So CliBuilder.parse can catch and aggregate them.

02.03.02 — The builder state machine

02.03.02 — The builder state machine#

Source file: src/ocarina/dsl/testing_with_railway/internals/action_chain.py

Here’s where the entire ROP mechanic lives: four successive typed states that literally (as in: the type-checker literally) refuse any syntactic improvisation. The DSL doubles as its own immune system.

Overview#

                      ┌─────────────────┐
   start(action)  →   │  ActionStart[T] │     an Action[T] = Thunk[Result[T]]
                      └────────┬────────┘
                               │ .failure(failure_handler)
                               ▼
                      ┌─────────────────┐
                      │ ActionFailure[T]│
                      └────────┬────────┘
                               │ .success(success_handler)
                               ▼
                      ┌─────────────────┐
                      │ ActionSuccess[T]│
                      └────────┬────────┘
                               │ .execute()   ── runs action(), fires the handler
                               ▼
                      ┌─────────────────┐
                      │ ActionChain[T]  │     holds (has_failed, result)
                      └──────┬──────────┘
                             │ .then(next_action_or_start)
              ┌──────────────┴──────────────┐
              │                             │
   has_failed=True                   has_failed=False
              │                             │
              ▼                             ▼
   ┌──────────────────┐         ┌───────────────────┐
   │ NeutralAction-   │         │ ActionStart[T]    │  → cycle starts over
   │ Start[T]         │         │  (the next action │
   │ (failure rail:   │         │   will execute)   │
   │  the whole chain │         └───────────────────┘
   │  becomes no-op,  │
   │  while keeping   │
   │  the fluent API) │
   └──────────────────┘
              │
              ▼ (.failure → .success → .execute, all no-op)
   ┌──────────────────┐
   │  ActionChain[T]  │   has_failed=True, result=<accumulated Fail>
   └──────────────────┘

The types Action, FailureHandler, SuccHandler#

type Action[T]        = Thunk[Result[T]]      # () -> Result[T]
type FailureHandler   = Callable[[Exception], None]
type SuccHandler      = Effect                # () -> None
  • Action[T] is a Thunk. Calling ActionStart(action) does not run anything. The action just gets captured.
  • FailureHandler gets the exception, not the Fail or the Result. Deliberate: 99% of handlers want to log the exception, take a screenshot, and move on.
  • SuccHandler is an Effect (no argument). Same idea — a success handler logs a static message, snaps a screenshot, doesn’t need the result.

Typestate Pattern: why you can’t get it wrong#

The chain is strictly linear. Each method returns a different type that only exposes the next legal step:

02.04.02 — Catalog of builtin assertions

02.04.02 — Catalog of builtin assertions#

Source file: src/ocarina/dsl/invariants/assertions.py — ~25 predicates.

Every assertion follows the same contract:

  • A direct predicate (value: T) -> None that raises InvariantViolationError on contract violation.
  • Or a closure when you need a config argument (is_equal_to(cmp), etc.).
  • Or, occasionally, a higher-order function (HOF) like each. Lives here pragmatically — a dedicated file for a single case would be overkill.

Full table#

AssertionDirect / closure / HOFDomainDescription
is_str(value)directAnyRaises if value is not a str
is_none(value)directAnyRaises if value is not None
is_not_none(value)directAnyRaises if value is None
is_equal_to(cmp)closureAnyReturns (value) -> None that raises if value != cmp
is_not_equal_to(cmp)closureAnyReturns (value) -> None that raises if value == cmp
is_less_than(cmp)closurefloatvalue < cmp
is_less_than_or_equal_to(cmp)closurefloatvalue <= cmp
is_greater_than(cmp)closurefloatvalue > cmp
is_greater_than_or_equal_to(cmp)closurefloatvalue >= cmp
is_positive(value)directfloatvalue >= 0
is_not_zero(value)directfloatvalue != 0
is_in(elements)closureAnyvalue in tuple(elements)
is_file(value)directstr | PathPath(value).is_file()
is_dir(value)directstr | PathPath(value).is_dir()
is_iso_date_string(value)directstrdatetime.fromisoformat(value)
is_iso_utc_date_string(value)directstrChecks ISO + tzinfo == UTC
is_email(value)directstrNo whitespace, exactly one @, non-empty parts, domain contains .
has_unique_elements(*, key=None)closureIterable[Any]Detects duplicates (raises DuplicatesError). Type-strict (1 ≠ True), supports unhashables.
is_empty(value)directSizedlen(value) == 0
is_truthy(value)directAnybool(value) is True
is_valid_filename(value)directstrCross-platform: forbidden chars, Windows reserved words, no leading/trailing space or dot, length ≤ 255
each(predicate)HOFIterable[Any]Applies predicate to each element

A few implementations in detail#

is_email#

def is_email(value: str) -> None:
    """Assert that the string is a valid email address (fast check)."""
    if " " in value:
        raise InvariantViolationError(f"'{value}' must not contain whitespace.")
    if value.count("@") != 1:
        raise InvariantViolationError(f"'{value}' must contain exactly one '@' character.")
    local_part, domain_part = value.split("@")
    if not local_part or not domain_part:
        raise InvariantViolationError(f"'{value}' must have non-empty local and domain parts.")
    if "." not in domain_part:
        raise InvariantViolationError(f"Domain part of '{value}' must contain at least one '.'.")

Four bare-minimum checks. No RFC 5322 regex. Deliberate: real email validation happens by sending an email, not by regex.

02.05.02 — TestExecutor[Driver]

02.05.02 — TestExecutor[Driver]#

Source file: src/ocarina/dsl/testing/internals/test_executor.py

One job: run one attempt of a test with one driver. Knows nothing about retries, the driver pool, or suite-level aggregation.

ExecutionOutcome#

@final
@dataclass(frozen=True, slots=True)
class ExecutionOutcome:
    result: TestResult
    skipped: bool
    setup_failed: bool
    should_retry: bool
    steps_count: int
FieldTypeMeaning
resultTestResult = Result[Any] | NoneThe chain’s result (Ok, Fail), or None if skip / setup_failed.
skippedboolTrue if test_runner.skipped (i.e. Test(skipped=True)).
setup_failedboolTrue if the scenario’s setup() raised.
should_retryboolTrue if the retry rule applies (transient_error detected).
steps_countintNumber of act calls; -1 if skip or setup_failed.
  • slots=True: blocks dynamic attribute addition, saves memory. This object rides the hot path — slots earns its keep.
  • frozen=True: immutable, safe to share across threads.
  • @final: no inheritance.

Execution order for one attempt#

┌──────────────────────────────────────────────────────────────────────┐
│  test_runner = test.spawn(driver, logger_with_taxonomy)              │
└──────────────────────────────┬───────────────────────────────────────┘
                               │
                               ▼
              ┌─────────────────────────────────┐
              │  test_runner.skipped ?          │── True ─► return Outcome(skipped=True, ...)
              └────────────────┬────────────────┘
                               │ False
                               ▼
              ┌─────────────────────────────────┐
              │  logger.test_name(test.name)    │   (annotation)
              └────────────────┬────────────────┘
                               │
                               ▼
              ┌─────────────────────────────────┐
              │  setup() (if not None)          │── raises ─► teardown() (if not None)
              └────────────────┬────────────────┘            ↓
                               │                  return Outcome(setup_failed=True, should_retry=True, ...)
                               ▼
              ┌─────────────────────────────────┐
              │  watchers.start(driver, logger, │
              │                 take_screenshot)│   (1 daemon thread per watcher)
              └────────────────┬────────────────┘
                               │
                               ▼
              ┌─────────────────────────────────┐
              │  _run_chain(chain_runners, ...) │── returns (result, should_retry)
              └────────────────┬────────────────┘
                               │
                               ▼
              ┌─────────────────────────────────┐
              │  watchers.stop()                │   (always)
              └────────────────┬────────────────┘
                               │
                               ▼
              ┌─────────────────────────────────┐
              │  teardown() (if not None)       │   (ALWAYS, even if chain failed)
              └────────────────┬────────────────┘     (exceptions are logged & swallowed)
                               │
                               ▼
              ┌─────────────────────────────────┐
              │  steps_count = act_counter.get()│
              │  return Outcome(...)            │
              └─────────────────────────────────┘

execute#

def execute(
    self, test: Test[Driver], *,
    driver: Driver,
    taxonomy: tuple[str, ...],
    logger_with_taxonomy: ILogger,
    logger_without_taxonomy: ILogger,
    attempt: int, max_attempts: int,
) -> ExecutionOutcome:
    test_runner = test.spawn(driver, logger_with_taxonomy)

    if test_runner.skipped:
        return ExecutionOutcome(result=None, skipped=True, setup_failed=False,
                                should_retry=False, steps_count=-1)

    logger_without_taxonomy.test_name(test.name)

    if test_runner.setup is not None:
        try:
            test_runner.setup()
        except Exception as exc:
            msg = f"{test.name} -- Setup failed (attempt {attempt}/{max_attempts}): {exc}"
            logger_with_taxonomy.warning(msg)
            if test_runner.teardown is not None:
                self._run_teardown(test_runner.teardown, test_name=test.name, logger=logger_with_taxonomy)
            return ExecutionOutcome(result=None, skipped=False, setup_failed=True,
                                    should_retry=True, steps_count=-1)

    watchers: Sequence[Watcher[Driver]] = test_runner.watchers or []
    self._start_watchers(watchers, driver=driver, test_name=test.name, taxonomy=taxonomy)

    result, should_retry = self._run_chain(
        test_runner.chain_runners, test_name=test.name, attempt=attempt,
        driver=driver, logger=logger_without_taxonomy,
        logger_with_taxonomy=logger_with_taxonomy, max_attempts=max_attempts,
    )

    self._stop_watchers(watchers)

    if test_runner.teardown is not None:
        self._run_teardown(test_runner.teardown, test_name=test.name, logger=logger_with_taxonomy)

    steps_count = self._act_counter.get()
    return ExecutionOutcome(result=result, skipped=False, setup_failed=False,
                            should_retry=should_retry, steps_count=steps_count)

Two loggers: why#

logger_with_taxonomy vs logger_without_taxonomy:

02.10.02 — DriverBuilder[Driver]

02.10.02 — DriverBuilder[Driver]#

Source file: src/ocarina/infra/driver_builder.py

Encapsulates profile handling (often a tmp copy of a user directory) and produces the (driver, dispose) pair the pool expects.

Why a builder?#

Building a Selenium driver with a profile requires:

  1. Copy the profile to a temp folder (otherwise Firefox/Chrome locks the original).
  2. Start the driver pointing at the temp folder.
  3. At the end: driver.quit(), then delete the temp folder.

DriverBuilder factors out those three steps:

02.11.02 — CliStore[TKeys] + _CliField[T] + phantom_validate

02.11.02 — CliStore[TKeys] + _CliField[T] + phantom_validate#

Source files: src/ocarina/opinionated/cli/store.py, src/ocarina/opinionated/cli/phantoms.py

Write-once store for parsed CLI values. Each field validates on write. TKeys: Literal[...] gives key autocomplete.

_CliField[T] — write-once field#

class _CliField[T]:
    def __init__(self, *, validate: ValidationChainBuilder[T]) -> None:
        self._value: T | _Unset = _UNSET
        self._validate = validate

    def set(self, value: T) -> None:
        if not isinstance(self._value, _Unset):
            raise RuntimeError("Value already set.")
        self._validate(_validate(value)).execute().raise_if_invalid()
        self._value = value

    def get(self) -> T:
        if isinstance(self._value, _Unset):
            raise RuntimeError("Value not set yet.")
        return self._value

1. _Unset sentinel#

class _Unset:
    pass

_UNSET = _Unset()

Why not None? Because None can be a legitimate value (--profile-path isn’t mandatory; its post-parse value can legitimately be None). A dedicated sentinel separates “not set yet” from “set to None”.