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.

04.01 — From the outside like a user strategy

04.01 — "From the outside like a user" strategy#

The posture lives in the scenario tests’ conftest.py. No test cheats by peeking at internals.

tests/scenarios/conftest.py#

ComponentRole
FakeDriver (dataclass)Minimal driver: title, disposed. No Selenium method.
make_built_driver()Factory (FakeDriver, dispose), i.e. the signature WebDriversPool expects.
make_pool(max_size=2)WebDriversPool[FakeDriver] ready to go.
RecordingPOM(POMBase)POM that records its calls and can be configured to raise.
acting(pom, step)Full ActionSuccess[RecordingPOM] (failure+success as no-ops).
scenario_of("ok", "ok2")TestScenario[FakeDriver] with N steps.
failing_scenario(step="boom", exc=...)1-step scenario that raises.
make_test(name, scenario=..., test_id=..., skipped=False)Test[FakeDriver]
make_suite(name, tests, pool=..., transient_errors=..., max_retries_per_test=...)TestSuite[FakeDriver]
make_campaign(name, suites, max_workers=1)TestCampaign[FakeDriver]
make_cycle(name="cycle", campaigns=..., smoke=..., mode=...)TestCycle[FakeDriver]
run_chain(runner)Helper that executes a ChainRunner and returns the ActionChain.

FakeDriver#

@dataclass
class FakeDriver:
    title: str = "fake"
    disposed: bool = False

That’s all. Ocarina’s core calls no Selenium API.

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:

00.03 — End-to-end execution flow of an e2e campaign

00.03 — End-to-end execution flow of an e2e campaign#

Run python -u src/main.py … on either suite (ocarina-example or ocarina-with-ai-example) and this chain kicks off:

USER  ────────────────────────────────────────────────────────────────────
       python -u src/main.py --browser firefox --workers 3 [...]
              │
              ▼
       (1) parse CLI
           CliStoreSingleton.push(create_selenium_auto_cli_store())
              │
              ▼
       (2) build pool
           create_selenium_drivers_pool(max_size=N)
              │
              ▼
       (3) warm up external dependencies
           - Redis (ocarina-example)
           - Heroku dyno (ai-example, via curl --retry 6)
              │
              ▼
       (4) bootstrap(
              test_cycle  = create_e2e_test_cycle(drivers_pool),
              run_plugins = lambda results: run_plugins(
                                generate_docx_proof, generate_json_results,
                                exceptions_logger=...),
              post_exec   = pretty_print_results + sys.exit(1) on fail
           )
              │
OCARINA  ─────┼──────────────────────────────────────────────────────────
              │
              ▼
       (5) TestCycle.run_all (saturate_workers=True)
              ├─ smoke_tests_campaigns      [mode: fail-fast | wait-for-all]
              │     └─ TestCampaign.run_all
              │           └─ TestSuite.run (max_workers, saturate_workers)
              │                 └─ ThreadPoolExecutor → TestFlow.run
              │                       └─ pool.acquire() → TestExecutor.execute
              │                             ├─ setup()                  (optional)
              │                             ├─ watchers.start()         (daemon threads)
              │                             ├─ chain_runners            (Railway DSL)
              │                             ├─ watchers.stop()
              │                             └─ teardown()               (always)
              ├─ campaigns (main)
              │  (skipped if a smoke failed)
              │
SUT   ────────┼────────────────────────────────────────────────────────────
              │
              ▼
       Selenium WebDriver ⇄ real browser
       sometimes: OTP HTTP GET + Redis (on the ocarina-example side)
              │
              ▼
       (6) run_plugins(results)
              ├─ generate_docx_proof  (reads the log tree, builds .docx files)
              ├─ generate_json_results (serializes results to .json)
              ├─ others if declared (parallelized via ThreadPoolExecutor)
              │
              ▼
       (6') post_exec(results)
              ├─ pretty_print_results (ANSI, hierarchical)
              └─ has_test_cycle_failed → sys.exit(1) if applicable

Step-by-step#

(1) Parse CLI#

create_selenium_auto_cli_store() detects the OS via platform.system():

02.10.03 — Screenshotter[TDriver]

02.10.03 — Screenshotter[TDriver]#

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

Generic, thread-safe screenshot utility. Driver-agnostic via a Protocol, configurable via a dataclass. Supports burst.

Protocol#

class ScreenshotDriver(Protocol):
    def save_screenshot(self, path: str) -> bool:
        """Save standard viewport screenshot."""

That’s it. Any object with save_screenshot(path: str) -> bool qualifies. Selenium WebDriver has it natively (driver.save_screenshot), Playwright can be wrapped, a fake driver in tests exposes it trivially.

ScreenshotterConfig[TDriver]#

@dataclass(frozen=True)
class ScreenshotterConfig[TDriver: ScreenshotDriver]:
    output_dir: Path
    file_ext: str = ".png"
    health_check: HealthCheck[TDriver] | None = None
    save_full_page: SaveFullPageScreenshot[TDriver] | None = None
    default_burst_delay: float = 0.5
    max_filename_retries: int = 500
    uuid_length: int = 8
FieldTypeRoleDefault
output_dirPathScreenshot directory — 
file_extstrExtension (.png, .jpg).png
health_checkHealthCheck[TDriver] | None(driver) -> None that raises if the driver is deadNone
save_full_pageSaveFullPageScreenshot[TDriver] | None(driver, path) -> bool for full-page screenshots (Firefox only)None
default_burst_delayfloatDelay between 2 shots in burst mode0.5s
max_filename_retriesintMax attempts to generate a unique name500
uuid_lengthintUUID suffix length in the name8

Immutable. Configure once, ship.

02.11.03 — Auto CLI store + flags + validation

02.11.03 — Auto CLI store + flags + validation#

Source file: src/ocarina/opinionated/cli/selenium/create_cli_store.py

Flags#

FlagDefaultTypeValidation
--driver-path""stris_file (except --browser safari)
--profile-pathNonestris_none OR is_dir
--browserNone (required)strargparse choices (per OS)
--not-headlessFalse (= headless by default)bool (store_true)phantom
--workers5intis_positive + is_not_zero
--logger"terminal+file"strchoices=LOGGERS_CHOICES
--wait-timeout10int≤ 60, > 0
--dont-force-delete-tmp-dirsFalsebool (store_true)phantom
--only[]list[str] (nargs="+")phantom + mutex
--exclude[]list[str] (nargs="+")phantom + mutex
_DEFAULT_WORKERS_AMOUNT = 5
_DEFAULT_LOGGER: SupportedLogger = "terminal+file"
_DEFAULT_BROWSER_AUTOMATION_TIMEOUT = 10
_MAX_BROWSER_AUTOMATION_TIMEOUT = 60

Literal[...]#

type SeleniumCliStoreKeys = Literal[
    "driver_path", "profile_path", "browser", "headless", "workers",
    "logger", "wait_timeout", "force_delete_tmp_dirs", "only", "exclude",
]

These keys are the store’s whole vocabulary. Call-sites use them: store.get("workers"), store.get("browser"), etc.

10.04 — ocarina-with-ai-example workflows

10.04 — ocarina-with-ai-example workflows#

See also ../08-ai-example/09-ci-matrix.md

Overview#

WorkflowTriggerOSEffectNote
ai_proof_ci.ymlpush main, PR, dispatchubuntu × py 3.14ruff format src/ --check + ruff check src/ + mypy src/PR gate, fast
ai_proof_e2e.ymldispatchubuntu × firefox+chrome matrixHeroku warm-up + run + sed filter ChromeDriver C++ stacks + uploadManual only

Differences vs ocarina-example#

Aspectocarina-exampleocarina-with-ai-example
Browser matrixFirefox only (e2e.yml)Firefox + Chrome (parallelized)
SUTIgoristan (GitHub Pages)CURA (Heroku eco-dyno)
Pre-runwarm-up Redis clientwarm-up Heroku dyno
Output filternonesed filters ChromeDriver C++ stack frames
WAIT_TIMEOUTdefault 1015 (compensates for Heroku latency)

Heroku warm-up#

curl -sf --retry 6 --retry-delay 5 --retry-all-errors \
  --max-time 30 https://katalon-demo-cura.herokuapp.com/ > /dev/null
echo "Dyno is warm"

CURA runs on a Heroku dyno that sleeps after inactivity. Tests hitting a cold dyno produce timeouts (false fails) and slow confirmations (false passes on gap tests). Wake it explicitly before any browser opens.

01.05 — The project's political stance

01.05 — The project’s political stance#

The Holy Book doesn’t separate philosophy from the politics of the repo itself. Five operating commitments come out of it.

1. Incorruptibility#

Sharing Ocarina means sharing a car I have maintained entirely myself, for myself, therefore with great care. Still, it is shared as-is, and will remain so. It’s my car.

I channeled all my anger into it, to pour all my love into it.

02.10.05 — Selenium adapters

02.10.05 — Selenium adapters#

Source folder: src/ocarina/infra/selenium/

Everything that materializes the abstractions (POMBase, WebDriversPool, Screenshotter) in Selenium flavor. Lives outside the pure DSL — and since 1.1.3 it has a twin: src/ocarina/infra/playwright/ ships the exact same contracts in Playwright flavor. Swapping backend is now picking a folder, not writing one.

Files in the folder#

FileRole
create_driver.py_build_firefox, _build_chrome, _build_edge, _build_safari + dispatch
create_drivers_pool.pycreate_selenium_drivers_pool (uses DriverBuilder + WebDriversPool)
create_screenshotter.pycreate_selenium_screenshotter + _selenium_save_full_page (Firefox-only)
driver_healthcheck.pydriver_healthcheck(driver) — pings driver.title
mixins.pySeleniumTitleMixin

create_driver.py#

def _build_firefox(*, profile_path, driver_path, headless, wait_timeout) -> WebDriver:
    service = FirefoxService(executable_path=driver_path)
    options = FirefoxOptions()
    if headless:
        options.add_argument("-headless")
    if profile_path:
        options.profile = FirefoxProfile(profile_path)
    driver = Firefox(service=service, options=options)
    driver.implicitly_wait(wait_timeout)
    return driver


def _build_chrome(*, profile_path, driver_path, headless, wait_timeout) -> WebDriver:
    service = ChromeService(executable_path=driver_path)
    options = ChromeOptions()
    if headless:
        options.add_argument("--headless=new")
    if profile_path:
        options.add_argument(f"--user-data-dir={profile_path}")
    driver = Chrome(service=service, options=options)
    driver.implicitly_wait(wait_timeout)
    return driver


def _build_edge(*, profile_path, driver_path, headless, wait_timeout) -> WebDriver:
    # … same as Chrome with EdgeService/EdgeOptions/Edge …


def _build_safari(*, profile_path, driver_path, headless, wait_timeout) -> WebDriver:
    # … Safari supports neither driver_path (uses macOS' native safaridriver),
    #   nor profile_path (no custom profile), nor headless …

1. implicitly_wait set only once#

Each builder calls driver.implicitly_wait(wait_timeout) once. Any find_element that doesn’t find the element immediately waits up to wait_timeout seconds before raising NoSuchElementException.

02.07 — Watcher[Driver]

02.07 — Watcher[Driver]#

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

Parallelized observer running as a daemon thread alongside the test_chain. Built to catch unpredictable frictions with no direct impact on the scenario that pop up while the scenario runs: random error toasts, parasitic validations, unexpected popups.

The problem#

Holy Book quote (First real-world hurdles):

Even more surprisingly: apps displaying error toasts for no apparent reason, or forms flagging validation errors on inputs that are actually correct, without blocking the journey.