<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Ocarina on Ocarina — manual</title>
    <link>https://mojo-molotov.github.io/from-ocarina-to-igor/tags/ocarina/</link>
    <description>Recent content in Ocarina on Ocarina — manual</description>
    <generator>Hugo</generator>
    <language>en-US</language>
    <lastBuildDate>Thu, 04 Jun 2026 21:25:43 +0200</lastBuildDate>
    <atom:link href="https://mojo-molotov.github.io/from-ocarina-to-igor/tags/ocarina/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>02.03.01 — The Result[T] type</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/01-result/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/01-result/</guid>
      <description>&lt;h1 id=&#34;020301the-resultt-type&#34;&gt;02.03.01 — The &lt;code&gt;Result[T]&lt;/code&gt; type&lt;a class=&#34;anchor&#34; href=&#34;#020301the-resultt-type&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/railway/result.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/railway/result.py&lt;/code&gt;&lt;/a&gt; — &lt;strong&gt;zero dependencies&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;code&#34;&gt;Code&lt;a class=&#34;anchor&#34; href=&#34;#code&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;from&lt;/span&gt; dataclasses &lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; dataclass, field&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;from&lt;/span&gt; typing &lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; TypeGuard, final&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_BaseResult&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Base class ensuring both Ok and Fail have error attribute for type narrowing.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    error: Exception &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@final&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@dataclass(frozen&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;Ok&lt;/span&gt;[T](_BaseResult):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    value: T&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    error: &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@final&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@dataclass(frozen&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;Fail&lt;/span&gt;(_BaseResult):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    error: Exception &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; field(default_factory&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;lambda&lt;/span&gt;: Exception(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Unknown error&amp;#34;&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; Result[T] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Ok[T] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; Fail&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;is_ok&lt;/span&gt;[T](result: Result[T]) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; TypeGuard[Ok[T]]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;isinstance&lt;/span&gt;(result, Ok)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;is_fail&lt;/span&gt;[T](result: Result[T]) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; TypeGuard[Fail]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;isinstance&lt;/span&gt;(result, Fail)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;six-design-decisions-dissected&#34;&gt;Six design decisions, dissected&lt;a class=&#34;anchor&#34; href=&#34;#six-design-decisions-dissected&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;1-_baseresult-as-a-common-parent&#34;&gt;1. &lt;code&gt;_BaseResult&lt;/code&gt; as a common parent&lt;a class=&#34;anchor&#34; href=&#34;#1-_baseresult-as-a-common-parent&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_BaseResult&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    error: Exception &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One job: let &lt;em&gt;mypy&lt;/em&gt; &lt;strong&gt;read &lt;code&gt;result.error&lt;/code&gt; without narrowing first&lt;/strong&gt;. Without the base, you&amp;rsquo;d have to write:&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.04.01 — The validate → assert_that → execute → raise_if_invalid flow</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/01-validate-flow/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/01-validate-flow/</guid>
      <description>&lt;h1 id=&#34;020401the-validate--assert_that--execute--raise_if_invalid-flow&#34;&gt;02.04.01 — The &lt;code&gt;validate → assert_that → execute → raise_if_invalid&lt;/code&gt; flow&lt;a class=&#34;anchor&#34; href=&#34;#020401the-validate--assert_that--execute--raise_if_invalid-flow&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/invariants/validate.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/invariants/validate.py&lt;/code&gt;&lt;/a&gt; (entry-point) + &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/invariants/internals/validation_chain.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/invariants/internals/validation_chain.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;the-state-machine&#34;&gt;The state machine&lt;a class=&#34;anchor&#34; href=&#34;#the-state-machine&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;diagram&#34;&gt;Diagram&lt;a class=&#34;anchor&#34; href=&#34;#diagram&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;validate(value: T)&#xA;       │&#xA;       ▼&#xA;ValidationStartBlock[T]&#xA;       │&#xA;       └─ assert_that(predicate, *, msg=None)&#xA;              │&#xA;              ▼&#xA;       ValidationAssertBlock[T]   ← state accepting several continuations&#xA;              │&#xA;              ├─► assert_that(P)     [loop, AND-chainable, see dedicated diagram]&#xA;              ├─► otherwise(P)       [OR, see 03-otherwise-any-of.md]&#xA;              ├─► then(U, name=...)  [value swap, see 04-then-chain-of-validations.md]&#xA;              └─► execute()          → _ValidationResult&#xA;                                          │&#xA;                                          ├─ is_valid: bool&#xA;                                          ├─ errors: Sequence[InvariantViolationError]&#xA;                                          ├─ validated_values: Sequence[Any]&#xA;                                          └─ raise_if_invalid() : None | raise AggregateInvariantViolationError&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;assert_that-loop&#34;&gt;&lt;code&gt;assert_that&lt;/code&gt; loop&lt;a class=&#34;anchor&#34; href=&#34;#assert_that-loop&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;&lt;code&gt;assert_that&lt;/code&gt; declares &lt;strong&gt;one&lt;/strong&gt; assertion on the current value. Multiple &lt;code&gt;assert_that&lt;/code&gt; calls chain — each adds another condition (logical AND, order preserved).&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.05.01 — Test[Driver]</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/01-test/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/01-test/</guid>
      <description>&lt;h1 id=&#34;020501testdriver&#34;&gt;02.05.01 — &lt;code&gt;Test[Driver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#020501testdriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing/oc_test.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing/oc_test.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;signature&#34;&gt;Signature&lt;a class=&#34;anchor&#34; href=&#34;#signature&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@final&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;Test&lt;/span&gt;[Driver]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;__init__&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        name: TestName,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        test_id: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        test_scenario: TestScenario[Driver],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        pre_test_scenarios_fragments: Sequence[TestScenarioFragment[Driver]] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        post_test_scenarios_fragments: Sequence[TestScenarioFragment[Driver]] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        skipped: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;False&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; test_id &lt;span style=&#34;color:#ff79c6&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            test_id &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; name&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; name&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;test_id &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; test_id&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_test_scenario &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; test_scenario&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_pre_test_scenarios_fragments &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; pre_test_scenarios_fragments &lt;span style=&#34;color:#ff79c6&#34;&gt;or&lt;/span&gt; []&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_post_test_scenarios_fragments &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; post_test_scenarios_fragments &lt;span style=&#34;color:#ff79c6&#34;&gt;or&lt;/span&gt; []&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_skipped &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; skipped&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Six parameters:&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Parameter&lt;/th&gt;&#xA;          &lt;th&gt;Type&lt;/th&gt;&#xA;          &lt;th&gt;Role&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestName&lt;/code&gt; (&lt;code&gt;str&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;Human label; appears in the report, and &lt;strong&gt;becomes the log file name&lt;/strong&gt; (hence subject to &lt;code&gt;is_valid_filename&lt;/code&gt;).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;test_id&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Stable identifier for &lt;code&gt;--only&lt;/code&gt; / &lt;code&gt;--exclude&lt;/code&gt;. If absent: takes &lt;code&gt;name&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;test_scenario&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestScenario[Driver]&lt;/code&gt; (alias = &lt;code&gt;Callable[[Driver, ILogger], Scenario[Driver]]&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;Factory&lt;/em&gt; that builds the &lt;code&gt;Scenario&lt;/code&gt; at execution time.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;pre_test_scenarios_fragments&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Sequence[TestScenarioFragment[Driver]] | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;(driver, logger) -&amp;gt; TestChain&lt;/code&gt; functions executed &lt;strong&gt;before&lt;/strong&gt; the main scenario.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;post_test_scenarios_fragments&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Sequence[TestScenarioFragment[Driver]] | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;(driver, logger) -&amp;gt; TestChain&lt;/code&gt; functions executed &lt;strong&gt;after&lt;/strong&gt; the main scenario.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;skipped&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;If &lt;code&gt;True&lt;/code&gt;, the test is registered but not executed.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h3 id=&#34;1-final&#34;&gt;1. &lt;code&gt;@final&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#1-final&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;No user inheritance. Want a &amp;ldquo;special&amp;rdquo; test? &lt;em&gt;Compose&lt;/em&gt; via fragments or a scenario — don&amp;rsquo;t subclass.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.10.01 — WebDriversPool[Driver]</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/01-drivers-pool/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/01-drivers-pool/</guid>
      <description>&lt;h1 id=&#34;021001webdriverspooldriver&#34;&gt;02.10.01 — &lt;code&gt;WebDriversPool[Driver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#021001webdriverspooldriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/infra/drivers_pool.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/infra/drivers_pool.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Thread-safe driver pool, max concurrency enforced by semaphore, monitored async &lt;em&gt;warmup&lt;/em&gt;, clean &lt;em&gt;shutdown&lt;/em&gt;. No reuse — every &lt;code&gt;acquire()&lt;/code&gt; disposes its driver on the way out (clean state).&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;constructor&#34;&gt;Constructor&lt;a class=&#34;anchor&#34; href=&#34;#constructor&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@final&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;WebDriversPool&lt;/span&gt;[Driver]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;__init__&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        create_driver: Thunk[BuiltWebDriver[Driver]],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        max_size: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        warmup_timeout: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;float&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_create_driver &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; create_driver&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_pool: Queue[BuiltWebDriver[Driver]] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Queue(max_size)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_semaphore &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Semaphore(max_size)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_warmup_timeout &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            warmup_timeout &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; warmup_timeout &lt;span style=&#34;color:#ff79c6&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;and&lt;/span&gt; warmup_timeout &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;0.1&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;60.0&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;5&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Primitive&lt;/th&gt;&#xA;          &lt;th&gt;Role&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;Queue(max_size)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Queue of &lt;em&gt;pre-built&lt;/em&gt; available drivers.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;Semaphore(max_size)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Guarantees that &lt;strong&gt;at most &lt;code&gt;max_size&lt;/code&gt; drivers&lt;/strong&gt; live simultaneously (whether queued or acquired).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;warmup_timeout&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Max no-progress delay during warmup before raising &lt;code&gt;WarmupTimeoutError&lt;/code&gt;. Default 300s.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;acquire&#34;&gt;&lt;code&gt;acquire()&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#acquire&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@contextmanager&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;acquire&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; Iterator[Driver]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;try&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        driver, dispose &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_pool&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;get_nowait()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;except&lt;/span&gt; Empty:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_semaphore&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;acquire()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;try&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            driver, dispose &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_create_driver()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;except&lt;/span&gt; Exception:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_semaphore&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;release()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;try&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;yield&lt;/span&gt; driver&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;finally&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;with&lt;/span&gt; suppress(Exception):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            dispose()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_semaphore&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;release()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;              acquire()&#xA;                  │&#xA;                  ▼&#xA;   ┌──────────────────────────────────┐&#xA;   │ pool.get_nowait()                │── OK ─► driver, dispose         ← warmup case&#xA;   └──────────────┬───────────────────┘&#xA;                  │ Empty&#xA;                  ▼&#xA;   ┌──────────────────────────────────┐&#xA;   │ sem.acquire()                    │  ← blocks if N drivers alive&#xA;   └──────────────┬───────────────────┘&#xA;                  ▼&#xA;   ┌──────────────────────────────────┐&#xA;   │ create_driver()                  │── raises ─► sem.release(); raise&#xA;   └──────────────┬───────────────────┘&#xA;                  ▼&#xA;   ┌──────────────────────────────────┐&#xA;   │ yield driver                     │  ← caller uses it&#xA;   └──────────────┬───────────────────┘&#xA;                  ▼&#xA;                finally :&#xA;                  with suppress : dispose()       ← driver disposed&#xA;                  sem.release()                   ← release a slot&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;1-no-reuse&#34;&gt;1. No reuse&lt;a class=&#34;anchor&#34; href=&#34;#1-no-reuse&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;&lt;code&gt;Queue.get_nowait()&lt;/code&gt; &lt;em&gt;consumes&lt;/em&gt; the entry. Once pulled, the driver never goes back. At the end (&lt;code&gt;finally&lt;/code&gt;), it&amp;rsquo;s &lt;strong&gt;disposed&lt;/strong&gt;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.11.01 — CliBuilder &#43; CliArg &#43; _SilentArgumentParser</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/01-cli-builder/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/01-cli-builder/</guid>
      <description>&lt;h1 id=&#34;021101clibuilder--cliarg--_silentargumentparser&#34;&gt;02.11.01 — &lt;code&gt;CliBuilder&lt;/code&gt; + &lt;code&gt;CliArg&lt;/code&gt; + &lt;code&gt;_SilentArgumentParser&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#021101clibuilder--cliarg--_silentargumentparser&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/opinionated/cli/builder.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/opinionated/cli/builder.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Declarative overlay on top of &lt;code&gt;argparse&lt;/code&gt;. &lt;em&gt;Aggregates&lt;/em&gt; validation errors, &lt;em&gt;rewrites&lt;/em&gt; the help output on error, and &lt;em&gt;registers&lt;/em&gt; post-parse effects.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;_silentargumentparser&#34;&gt;&lt;code&gt;_SilentArgumentParser&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#_silentargumentparser&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_SilentArgumentParser&lt;/span&gt;(ArgumentParser):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;error&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, message: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; Never:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Raise an error.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt; ValueError(message)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The standard &lt;code&gt;ArgumentParser&lt;/code&gt; calls &lt;code&gt;sys.exit(2)&lt;/code&gt; directly on error. With that turd of a default, you &lt;strong&gt;can&amp;rsquo;t&lt;/strong&gt; intercept and you &lt;strong&gt;can&amp;rsquo;t&lt;/strong&gt; aggregate multiple errors.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;_SilentArgumentParser&lt;/code&gt; &lt;em&gt;reroutes&lt;/em&gt; errors as &lt;code&gt;ValueError&lt;/code&gt;. So &lt;code&gt;CliBuilder.parse&lt;/code&gt; can catch and aggregate them.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.03.02 — The builder state machine</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/02-action-chain-states/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/02-action-chain-states/</guid>
      <description>&lt;h1 id=&#34;020302the-builder-state-machine&#34;&gt;02.03.02 — The builder state machine&lt;a class=&#34;anchor&#34; href=&#34;#020302the-builder-state-machine&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing_with_railway/internals/action_chain.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing_with_railway/internals/action_chain.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Here&amp;rsquo;s where the &lt;em&gt;entire&lt;/em&gt; ROP mechanic lives: four successive typed states that literally (as in: the &lt;em&gt;type-checker&lt;/em&gt; literally) refuse any syntactic improvisation. The DSL doubles as its own immune system.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;overview&#34;&gt;Overview&lt;a class=&#34;anchor&#34; href=&#34;#overview&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;                      ┌─────────────────┐&#xA;   start(action)  →   │  ActionStart[T] │     an Action[T] = Thunk[Result[T]]&#xA;                      └────────┬────────┘&#xA;                               │ .failure(failure_handler)&#xA;                               ▼&#xA;                      ┌─────────────────┐&#xA;                      │ ActionFailure[T]│&#xA;                      └────────┬────────┘&#xA;                               │ .success(success_handler)&#xA;                               ▼&#xA;                      ┌─────────────────┐&#xA;                      │ ActionSuccess[T]│&#xA;                      └────────┬────────┘&#xA;                               │ .execute()   ── runs action(), fires the handler&#xA;                               ▼&#xA;                      ┌─────────────────┐&#xA;                      │ ActionChain[T]  │     holds (has_failed, result)&#xA;                      └──────┬──────────┘&#xA;                             │ .then(next_action_or_start)&#xA;              ┌──────────────┴──────────────┐&#xA;              │                             │&#xA;   has_failed=True                   has_failed=False&#xA;              │                             │&#xA;              ▼                             ▼&#xA;   ┌──────────────────┐         ┌───────────────────┐&#xA;   │ NeutralAction-   │         │ ActionStart[T]    │  → cycle starts over&#xA;   │ Start[T]         │         │  (the next action │&#xA;   │ (failure rail:   │         │   will execute)   │&#xA;   │  the whole chain │         └───────────────────┘&#xA;   │  becomes no-op,  │&#xA;   │  while keeping   │&#xA;   │  the fluent API) │&#xA;   └──────────────────┘&#xA;              │&#xA;              ▼ (.failure → .success → .execute, all no-op)&#xA;   ┌──────────────────┐&#xA;   │  ActionChain[T]  │   has_failed=True, result=&amp;lt;accumulated Fail&amp;gt;&#xA;   └──────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;the-types-action-failurehandler-succhandler&#34;&gt;The types &lt;code&gt;Action&lt;/code&gt;, &lt;code&gt;FailureHandler&lt;/code&gt;, &lt;code&gt;SuccHandler&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#the-types-action-failurehandler-succhandler&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; Action[T]        &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Thunk[Result[T]]      &lt;span style=&#34;color:#6272a4&#34;&gt;# () -&amp;gt; Result[T]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; FailureHandler   &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Callable[[Exception], &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; SuccHandler      &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Effect                &lt;span style=&#34;color:#6272a4&#34;&gt;# () -&amp;gt; None&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;Action[T]&lt;/code&gt; is a &lt;code&gt;Thunk&lt;/code&gt;.&lt;/strong&gt; Calling &lt;code&gt;ActionStart(action)&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; run anything. The action just gets captured.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;FailureHandler&lt;/code&gt; gets the exception&lt;/strong&gt;, not the &lt;code&gt;Fail&lt;/code&gt; or the &lt;code&gt;Result&lt;/code&gt;. Deliberate: 99% of handlers want to log the exception, take a screenshot, and move on.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;SuccHandler&lt;/code&gt; is an &lt;code&gt;Effect&lt;/code&gt;&lt;/strong&gt; (no argument). Same idea — a success handler logs a static message, snaps a screenshot, doesn&amp;rsquo;t need the result.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;typestate-pattern-why-you-cant-get-it-wrong&#34;&gt;Typestate Pattern: why you can&amp;rsquo;t get it wrong&lt;a class=&#34;anchor&#34; href=&#34;#typestate-pattern-why-you-cant-get-it-wrong&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The chain is &lt;strong&gt;strictly linear&lt;/strong&gt;. Each method returns a different type that only exposes the &lt;em&gt;next legal step&lt;/em&gt;:&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.04.02 — Catalog of builtin assertions</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/02-assertions/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/02-assertions/</guid>
      <description>&lt;h1 id=&#34;020402catalog-of-builtin-assertions&#34;&gt;02.04.02 — Catalog of builtin assertions&lt;a class=&#34;anchor&#34; href=&#34;#020402catalog-of-builtin-assertions&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/invariants/assertions.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/invariants/assertions.py&lt;/code&gt;&lt;/a&gt; — ~25 predicates.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;p&gt;Every assertion follows the &lt;strong&gt;same contract&lt;/strong&gt;:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;A direct predicate &lt;code&gt;(value: T) -&amp;gt; None&lt;/code&gt; that raises &lt;code&gt;InvariantViolationError&lt;/code&gt; on contract violation.&lt;/li&gt;&#xA;&lt;li&gt;Or a &lt;strong&gt;closure&lt;/strong&gt; when you need a config argument (&lt;code&gt;is_equal_to(cmp)&lt;/code&gt;, etc.).&lt;/li&gt;&#xA;&lt;li&gt;Or, occasionally, a &lt;strong&gt;higher-order function (HOF)&lt;/strong&gt; like &lt;code&gt;each&lt;/code&gt;. Lives here pragmatically — a dedicated file for a single case would be overkill.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;full-table&#34;&gt;Full table&lt;a class=&#34;anchor&#34; href=&#34;#full-table&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Assertion&lt;/th&gt;&#xA;          &lt;th&gt;Direct / closure / HOF&lt;/th&gt;&#xA;          &lt;th&gt;Domain&lt;/th&gt;&#xA;          &lt;th&gt;Description&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_str(value)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;direct&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Any&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Raises if &lt;code&gt;value&lt;/code&gt; is not a &lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_none(value)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;direct&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Any&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Raises if &lt;code&gt;value is not None&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_not_none(value)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;direct&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Any&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Raises if &lt;code&gt;value is None&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_equal_to(cmp)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;closure&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Any&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Returns &lt;code&gt;(value) -&amp;gt; None&lt;/code&gt; that raises if &lt;code&gt;value != cmp&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_not_equal_to(cmp)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;closure&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Any&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Returns &lt;code&gt;(value) -&amp;gt; None&lt;/code&gt; that raises if &lt;code&gt;value == cmp&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_less_than(cmp)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;closure&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;value &amp;lt; cmp&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_less_than_or_equal_to(cmp)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;closure&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;value &amp;lt;= cmp&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_greater_than(cmp)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;closure&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;value &amp;gt; cmp&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_greater_than_or_equal_to(cmp)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;closure&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;value &amp;gt;= cmp&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_positive(value)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;direct&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;value &amp;gt;= 0&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_not_zero(value)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;direct&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;value != 0&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_in(elements)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;closure&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Any&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;value in tuple(elements)&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_file(value)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;direct&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str | Path&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Path(value).is_file()&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_dir(value)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;direct&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str | Path&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Path(value).is_dir()&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_iso_date_string(value)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;direct&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;datetime.fromisoformat(value)&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_iso_utc_date_string(value)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;direct&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Checks ISO + &lt;code&gt;tzinfo == UTC&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_email(value)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;direct&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;No whitespace, exactly one &lt;code&gt;@&lt;/code&gt;, non-empty parts, domain contains &lt;code&gt;.&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;has_unique_elements(*, key=None)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;closure&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Iterable[Any]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Detects duplicates (raises &lt;code&gt;DuplicatesError&lt;/code&gt;). Type-strict (&lt;code&gt;1 ≠ True&lt;/code&gt;), supports unhashables.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_empty(value)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;direct&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Sized&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;len(value) == 0&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_truthy(value)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;direct&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Any&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bool(value) is True&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_valid_filename(value)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;direct&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Cross-platform: forbidden chars, Windows reserved words, no leading/trailing space or dot, length ≤ 255&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;each(predicate)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;HOF&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Iterable[Any]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Applies &lt;code&gt;predicate&lt;/code&gt; to each element&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;a-few-implementations-in-detail&#34;&gt;A few implementations in detail&lt;a class=&#34;anchor&#34; href=&#34;#a-few-implementations-in-detail&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;is_email&#34;&gt;&lt;code&gt;is_email&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#is_email&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;is_email&lt;/span&gt;(value: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Assert that the string is a valid email address (fast check).&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34; &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; value:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt; InvariantViolationError(&lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;value&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#39; must not contain whitespace.&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; value&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;count(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;@&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;1&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt; InvariantViolationError(&lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;value&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#39; must contain exactly one &amp;#39;@&amp;#39; character.&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    local_part, domain_part &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; value&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;split(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;@&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; local_part &lt;span style=&#34;color:#ff79c6&#34;&gt;or&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; domain_part:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt; InvariantViolationError(&lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;value&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#39; must have non-empty local and domain parts.&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;.&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; domain_part:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt; InvariantViolationError(&lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Domain part of &amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;value&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#39; must contain at least one &amp;#39;.&amp;#39;.&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Four bare-minimum checks. No RFC 5322 regex. Deliberate: real email validation happens by &lt;em&gt;sending&lt;/em&gt; an email, not by regex.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.05.02 — TestExecutor[Driver]</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/02-test-executor/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/02-test-executor/</guid>
      <description>&lt;h1 id=&#34;020502testexecutordriver&#34;&gt;02.05.02 — &lt;code&gt;TestExecutor[Driver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#020502testexecutordriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing/internals/test_executor.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing/internals/test_executor.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;&lt;strong&gt;One job&lt;/strong&gt;: run &lt;strong&gt;one attempt&lt;/strong&gt; of a test with &lt;strong&gt;one driver&lt;/strong&gt;. Knows nothing about retries, the driver pool, or suite-level aggregation.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;executionoutcome&#34;&gt;&lt;code&gt;ExecutionOutcome&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#executionoutcome&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@final&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@dataclass(frozen&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;, slots&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;ExecutionOutcome&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    result: TestResult&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    skipped: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    setup_failed: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    should_retry: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    steps_count: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Field&lt;/th&gt;&#xA;          &lt;th&gt;Type&lt;/th&gt;&#xA;          &lt;th&gt;Meaning&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;result&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestResult&lt;/code&gt; = &lt;code&gt;Result[Any] | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;The chain&amp;rsquo;s result (Ok, Fail), or &lt;code&gt;None&lt;/code&gt; if skip / setup_failed.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;skipped&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;True if &lt;code&gt;test_runner.skipped&lt;/code&gt; (i.e. &lt;code&gt;Test(skipped=True)&lt;/code&gt;).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;setup_failed&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;True if the scenario&amp;rsquo;s &lt;code&gt;setup()&lt;/code&gt; raised.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;should_retry&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;True if the &lt;em&gt;retry rule&lt;/em&gt; applies (transient_error detected).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;steps_count&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Number of &lt;code&gt;act&lt;/code&gt; calls; &lt;code&gt;-1&lt;/code&gt; if skip or setup_failed.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;slots=True&lt;/code&gt;&lt;/strong&gt;: blocks dynamic attribute addition, saves memory. This object rides the hot path — &lt;code&gt;slots&lt;/code&gt; earns its keep.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;frozen=True&lt;/code&gt;&lt;/strong&gt;: immutable, safe to share across threads.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;@final&lt;/code&gt;&lt;/strong&gt;: no inheritance.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;execution-order-for-one-attempt&#34;&gt;Execution order for one attempt&lt;a class=&#34;anchor&#34; href=&#34;#execution-order-for-one-attempt&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────────────────┐&#xA;│  test_runner = test.spawn(driver, logger_with_taxonomy)              │&#xA;└──────────────────────────────┬───────────────────────────────────────┘&#xA;                               │&#xA;                               ▼&#xA;              ┌─────────────────────────────────┐&#xA;              │  test_runner.skipped ?          │── True ─► return Outcome(skipped=True, ...)&#xA;              └────────────────┬────────────────┘&#xA;                               │ False&#xA;                               ▼&#xA;              ┌─────────────────────────────────┐&#xA;              │  logger.test_name(test.name)    │   (annotation)&#xA;              └────────────────┬────────────────┘&#xA;                               │&#xA;                               ▼&#xA;              ┌─────────────────────────────────┐&#xA;              │  setup() (if not None)          │── raises ─► teardown() (if not None)&#xA;              └────────────────┬────────────────┘            ↓&#xA;                               │                  return Outcome(setup_failed=True, should_retry=True, ...)&#xA;                               ▼&#xA;              ┌─────────────────────────────────┐&#xA;              │  watchers.start(driver, logger, │&#xA;              │                 take_screenshot)│   (1 daemon thread per watcher)&#xA;              └────────────────┬────────────────┘&#xA;                               │&#xA;                               ▼&#xA;              ┌─────────────────────────────────┐&#xA;              │  _run_chain(chain_runners, ...) │── returns (result, should_retry)&#xA;              └────────────────┬────────────────┘&#xA;                               │&#xA;                               ▼&#xA;              ┌─────────────────────────────────┐&#xA;              │  watchers.stop()                │   (always)&#xA;              └────────────────┬────────────────┘&#xA;                               │&#xA;                               ▼&#xA;              ┌─────────────────────────────────┐&#xA;              │  teardown() (if not None)       │   (ALWAYS, even if chain failed)&#xA;              └────────────────┬────────────────┘     (exceptions are logged &amp;amp; swallowed)&#xA;                               │&#xA;                               ▼&#xA;              ┌─────────────────────────────────┐&#xA;              │  steps_count = act_counter.get()│&#xA;              │  return Outcome(...)            │&#xA;              └─────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;execute&#34;&gt;&lt;code&gt;execute&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#execute&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;execute&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, test: Test[Driver], &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    driver: Driver,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    taxonomy: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;tuple&lt;/span&gt;[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logger_with_taxonomy: ILogger,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logger_without_taxonomy: ILogger,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    attempt: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;, max_attempts: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; ExecutionOutcome:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    test_runner &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; test&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;spawn(driver, logger_with_taxonomy)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; test_runner&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;skipped:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; ExecutionOutcome(result&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, skipped&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;, setup_failed&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;False&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                should_retry&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;False&lt;/span&gt;, steps_count&lt;span style=&#34;color:#ff79c6&#34;&gt;=-&lt;/span&gt;&lt;span style=&#34;color:#bd93f9&#34;&gt;1&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logger_without_taxonomy&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;test_name(test&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; test_runner&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;setup &lt;span style=&#34;color:#ff79c6&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;try&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            test_runner&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;setup()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;except&lt;/span&gt; Exception &lt;span style=&#34;color:#ff79c6&#34;&gt;as&lt;/span&gt; exc:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            msg &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;test&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt; -- Setup failed (attempt &lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;attempt&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;max_attempts&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;): &lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;exc&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            logger_with_taxonomy&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;warning(msg)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; test_runner&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;teardown &lt;span style=&#34;color:#ff79c6&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_run_teardown(test_runner&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;teardown, test_name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;test&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name, logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger_with_taxonomy)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; ExecutionOutcome(result&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, skipped&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;False&lt;/span&gt;, setup_failed&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                    should_retry&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;, steps_count&lt;span style=&#34;color:#ff79c6&#34;&gt;=-&lt;/span&gt;&lt;span style=&#34;color:#bd93f9&#34;&gt;1&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    watchers: Sequence[Watcher[Driver]] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; test_runner&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;watchers &lt;span style=&#34;color:#ff79c6&#34;&gt;or&lt;/span&gt; []&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_start_watchers(watchers, driver&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;driver, test_name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;test&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name, taxonomy&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;taxonomy)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    result, should_retry &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_run_chain(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        test_runner&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;chain_runners, test_name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;test&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name, attempt&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;attempt,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        driver&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;driver, logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger_without_taxonomy,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        logger_with_taxonomy&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger_with_taxonomy, max_attempts&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;max_attempts,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_stop_watchers(watchers)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; test_runner&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;teardown &lt;span style=&#34;color:#ff79c6&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_run_teardown(test_runner&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;teardown, test_name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;test&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name, logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger_with_taxonomy)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    steps_count &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_act_counter&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;get()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; ExecutionOutcome(result&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;result, skipped&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;False&lt;/span&gt;, setup_failed&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;False&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                            should_retry&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;should_retry, steps_count&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;steps_count)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;two-loggers-why&#34;&gt;Two loggers: why&lt;a class=&#34;anchor&#34; href=&#34;#two-loggers-why&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;logger_with_taxonomy&lt;/code&gt; vs &lt;code&gt;logger_without_taxonomy&lt;/code&gt;:&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.10.02 — DriverBuilder[Driver]</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/02-driver-builder/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/02-driver-builder/</guid>
      <description>&lt;h1 id=&#34;021002driverbuilderdriver&#34;&gt;02.10.02 — &lt;code&gt;DriverBuilder[Driver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#021002driverbuilderdriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/infra/driver_builder.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/infra/driver_builder.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Encapsulates &lt;strong&gt;profile&lt;/strong&gt; handling (often a tmp copy of a user directory) and produces the &lt;code&gt;(driver, dispose)&lt;/code&gt; pair the pool expects.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;why-a-builder&#34;&gt;Why a builder?&lt;a class=&#34;anchor&#34; href=&#34;#why-a-builder&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Building a Selenium driver with a &lt;em&gt;profile&lt;/em&gt; requires:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Copy the profile&lt;/strong&gt; to a temp folder (otherwise Firefox/Chrome locks the original).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Start the driver&lt;/strong&gt; pointing at the temp folder.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;At the end&lt;/strong&gt;: &lt;code&gt;driver.quit()&lt;/code&gt;, then delete the temp folder.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;&lt;code&gt;DriverBuilder&lt;/code&gt; factors out those three steps:&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.11.02 — CliStore[TKeys] &#43; _CliField[T] &#43; phantom_validate</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/02-cli-store-phantoms/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/02-cli-store-phantoms/</guid>
      <description>&lt;h1 id=&#34;021102clistoretkeys--_clifieldt--phantom_validate&#34;&gt;02.11.02 — &lt;code&gt;CliStore[TKeys]&lt;/code&gt; + &lt;code&gt;_CliField[T]&lt;/code&gt; + &lt;code&gt;phantom_validate&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#021102clistoretkeys--_clifieldt--phantom_validate&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source files: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/opinionated/cli/store.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/opinionated/cli/store.py&lt;/code&gt;&lt;/a&gt;, &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/opinionated/cli/phantoms.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/opinionated/cli/phantoms.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Write-once&lt;/strong&gt; store for parsed CLI values. Each field validates on write. &lt;code&gt;TKeys: Literal[...]&lt;/code&gt; gives key autocomplete.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;_clifieldtwrite-once-field&#34;&gt;&lt;code&gt;_CliField[T]&lt;/code&gt; — write-once field&lt;a class=&#34;anchor&#34; href=&#34;#_clifieldtwrite-once-field&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_CliField&lt;/span&gt;[T]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;__init__&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, validate: ValidationChainBuilder[T]) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_value: T &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; _Unset &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; _UNSET&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_validate &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; validate&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;set&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, value: T) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;isinstance&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_value, _Unset):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt; RuntimeError(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Value already set.&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_validate(_validate(value))&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;execute()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;raise_if_invalid()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_value &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; value&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;get&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; T:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;isinstance&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_value, _Unset):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt; RuntimeError(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Value not set yet.&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_value&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;1-_unset-sentinel&#34;&gt;1. &lt;code&gt;_Unset&lt;/code&gt; sentinel&lt;a class=&#34;anchor&#34; href=&#34;#1-_unset-sentinel&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_Unset&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;pass&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_UNSET &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; _Unset()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Why not &lt;code&gt;None&lt;/code&gt;? Because &lt;code&gt;None&lt;/code&gt; can be a &lt;strong&gt;legitimate value&lt;/strong&gt; (&lt;code&gt;--profile-path&lt;/code&gt; isn&amp;rsquo;t mandatory; its post-parse value can legitimately be &lt;code&gt;None&lt;/code&gt;). A dedicated sentinel separates &amp;ldquo;&lt;em&gt;not set yet&lt;/em&gt;&amp;rdquo; from &amp;ldquo;&lt;em&gt;set to None&lt;/em&gt;&amp;rdquo;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.03.03 — The failure rail: NeutralAction*</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/03-neutral-rail/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/03-neutral-rail/</guid>
      <description>&lt;h1 id=&#34;020303the-failure-rail-neutralaction&#34;&gt;02.03.03 — The failure rail: &lt;code&gt;NeutralAction*&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#020303the-failure-rail-neutralaction&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Edge case in the builder: what happens when you &lt;em&gt;chain&lt;/em&gt; an action onto an &lt;code&gt;ActionChain&lt;/code&gt; that has already failed?&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;why-neutral-classes-rather-than-a-user-side-if&#34;&gt;Why &lt;em&gt;Neutral&lt;/em&gt; classes rather than a user-side &lt;code&gt;if&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#why-neutral-classes-rather-than-a-user-side-if&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;When you chain an action onto an already-failed &lt;code&gt;ActionChain&lt;/code&gt;, you slide into three &lt;em&gt;Neutral&lt;/em&gt; classes that honor the same API but do nothing.&lt;/p&gt;&#xA;&lt;p&gt;Picture the DSL without this. User code would look like:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;chain &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; act1&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(h)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(h)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;execute()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; chain&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;is_ok():&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    chain &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; chain&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;then(act2)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(h)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(h)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;execute()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; chain&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;is_ok():&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    chain &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; chain&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;then(act3)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(h)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(h)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;execute()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Imperative&lt;/em&gt;, unreadable, and the whole point of the DSL is gone. The neutral rail keeps the writing flat:&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.04.03 — .otherwise(...) and _any_of</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/03-otherwise-any-of/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/03-otherwise-any-of/</guid>
      <description>&lt;h1 id=&#34;020403otherwise-and-_any_of&#34;&gt;02.04.03 — &lt;code&gt;.otherwise(...)&lt;/code&gt; and &lt;code&gt;_any_of&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#020403otherwise-and-_any_of&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;How Ocarina expresses a &lt;strong&gt;logical OR&lt;/strong&gt; between two predicates without breaking error aggregation.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;the-problem&#34;&gt;The problem&lt;a class=&#34;anchor&#34; href=&#34;#the-problem&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Concrete case: you want &amp;ldquo;&lt;code&gt;age == 18&lt;/code&gt; OR &lt;code&gt;age &amp;lt;= 65&lt;/code&gt;.&amp;rdquo; The ROP/invariants syntax is a strict chain of assertions, and every &lt;code&gt;.assert_that()&lt;/code&gt; is an AND. How do you express an OR?&lt;/p&gt;&#xA;&lt;h2 id=&#34;code&#34;&gt;Code&lt;a class=&#34;anchor&#34; href=&#34;#code&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;otherwise&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, fallback: Predicate[T], &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, msg: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; ValidationAssertBlock[T]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_last_predicate &lt;span style=&#34;color:#ff79c6&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt; RuntimeError(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;otherwise() must follow assert_that().&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    fallback_with_msg &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; _with_msg(fallback, msg, &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_name)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    combined &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; _any_of(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_last_predicate, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;([&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_otherwise_predicates, fallback_with_msg])&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_chain&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_steps&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;pop()                    &lt;span style=&#34;color:#6272a4&#34;&gt;# replace the last step&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_chain&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;add_assertion(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_value, combined, &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_name)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_last_predicate &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; combined&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_otherwise_predicates&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;append(fallback_with_msg)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Guard&lt;/strong&gt;: &lt;code&gt;otherwise()&lt;/code&gt; has to follow an &lt;code&gt;assert_that()&lt;/code&gt;. With no prior predicate it raises. (In practice the type-checker already rules it out — &lt;code&gt;ValidationStartBlock.otherwise&lt;/code&gt; doesn&amp;rsquo;t exist.)&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Combination&lt;/strong&gt;: combine previous predicate + all accumulated &lt;code&gt;otherwise&lt;/code&gt; predicates + the new one, via &lt;code&gt;_any_of&lt;/code&gt;. Result: a composite predicate.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Step replacement&lt;/strong&gt;: &lt;em&gt;pop&lt;/em&gt; the last step from &lt;code&gt;_ValidationChain&lt;/code&gt; and &lt;em&gt;push&lt;/em&gt; the combined one. Otherwise the original predicate AND the combined one would both be present, and the original error would resurface.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;_any_of&#34;&gt;&lt;code&gt;_any_of&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#_any_of&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_any_of&lt;/span&gt;[T](&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;predicates: _PredicateWithMsg[T]) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; _PredicateWithMsg[T]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_try_predicate&lt;/span&gt;(p: _PredicateWithMsg[T], value: T) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; InvariantViolationError &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;try&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            p(value)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;except&lt;/span&gt; InvariantViolationError &lt;span style=&#34;color:#ff79c6&#34;&gt;as&lt;/span&gt; exc:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; exc&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;else&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;combined&lt;/span&gt;(value: T) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        errors &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; []&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; p &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; predicates:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            error &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; _try_predicate(p, value)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; error &lt;span style=&#34;color:#ff79c6&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt;                              &lt;span style=&#34;color:#6272a4&#34;&gt;# ✅ one predicate passes → all pass&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            errors&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;append(error)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        formatted_error_messages &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;  | &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;  | &amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;join(&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;(e) &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; e &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; errors)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        msg &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;All predicates failed for value &lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;value&lt;span style=&#34;color:#f1fa8c&#34;&gt;!r}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;» At least one of the following conditions must be satisfied:&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;formatted_error_messages&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt; InvariantViolationError(msg)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; _PredicateWithMsg(combined)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;First success wins&lt;/strong&gt;: the moment one predicate passes, &lt;code&gt;combined&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;All fail&lt;/strong&gt;: build an &lt;em&gt;aggregated&lt;/em&gt; message listing &lt;strong&gt;every&lt;/strong&gt; failure, separated by &lt;code&gt;|&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Result is a &lt;code&gt;_PredicateWithMsg&lt;/code&gt;&lt;/strong&gt;: plug it back into the chain like any other predicate.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Aggregation is recursive&lt;/strong&gt;: 3 successive &lt;code&gt;otherwise()&lt;/code&gt; calls fold &lt;code&gt;(P_orig OR P_otherwise1 OR P_otherwise2 OR P_otherwise3)&lt;/code&gt; into one predicate.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;example&#34;&gt;Example&lt;a class=&#34;anchor&#34; href=&#34;#example&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;validate(age, name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;age&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;assert_that(is_equal_to(&lt;span style=&#34;color:#bd93f9&#34;&gt;18&lt;/span&gt;), msg&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Must be 18 or at most 65&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;otherwise(is_less_than_or_equal_to(&lt;span style=&#34;color:#bd93f9&#34;&gt;65&lt;/span&gt;), msg&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Must be 18 or at most 65&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;execute()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;raise_if_invalid()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;&#xA;&lt;li&gt;If &lt;code&gt;age == 18&lt;/code&gt;: &lt;code&gt;is_equal_to(18)&lt;/code&gt; passes → OK.&lt;/li&gt;&#xA;&lt;li&gt;If &lt;code&gt;age == 30&lt;/code&gt;: &lt;code&gt;is_equal_to(18)&lt;/code&gt; raises, &lt;code&gt;is_less_than_or_equal_to(65)&lt;/code&gt; passes → OK.&lt;/li&gt;&#xA;&lt;li&gt;If &lt;code&gt;age == 70&lt;/code&gt;: both raise → &lt;code&gt;InvariantViolationError&lt;/code&gt; with:&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;age: All predicates failed for value 70.&#xA;» At least one of the following conditions must be satisfied:&#xA;  | age: Must be 18 or at most 65&#xA;  | age: Must be 18 or at most 65&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(The duplicated message comes from passing the same &lt;code&gt;msg=&lt;/code&gt; twice. Intentional here — we &lt;em&gt;want&lt;/em&gt; a single statement of intent.)&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.05.03 — TestFlow[Driver] — retry policy</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/03-test-flow-retries/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/03-test-flow-retries/</guid>
      <description>&lt;h1 id=&#34;020503testflowdriverretry-policy&#34;&gt;02.05.03 — &lt;code&gt;TestFlow[Driver]&lt;/code&gt; — retry policy&lt;a class=&#34;anchor&#34; href=&#34;#020503testflowdriverretry-policy&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing/internals/test_flow.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing/internals/test_flow.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;One job&lt;/strong&gt;: for a single &lt;code&gt;Test&lt;/code&gt;, run the retry loop, acquire a fresh driver per attempt, apply the backoff policy.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;why-an-intermediate-level-between-testsuite-and-testexecutor&#34;&gt;Why an intermediate level between &lt;code&gt;TestSuite&lt;/code&gt; and &lt;code&gt;TestExecutor&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#why-an-intermediate-level-between-testsuite-and-testexecutor&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Level&lt;/th&gt;&#xA;          &lt;th&gt;Doesn&amp;rsquo;t know about&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestExecutor&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Retries, the driver pool&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;&lt;code&gt;TestFlow&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Inter-test concurrency, suite-level aggregation&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestSuite&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;One attempt&amp;rsquo;s execution, retries&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;A &lt;em&gt;genuinely strict&lt;/em&gt; separation. &lt;code&gt;TestFlow&lt;/code&gt; is the only level that knows both the pool and the retry policy — combining them is its whole job.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.10.03 — Screenshotter[TDriver]</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/03-screenshotter/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/03-screenshotter/</guid>
      <description>&lt;h1 id=&#34;021003screenshottertdriver&#34;&gt;02.10.03 — &lt;code&gt;Screenshotter[TDriver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#021003screenshottertdriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/infra/screenshotter.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/infra/screenshotter.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Generic, &lt;em&gt;thread-safe&lt;/em&gt; screenshot utility. Driver-agnostic via a &lt;code&gt;Protocol&lt;/code&gt;, configurable via a &lt;code&gt;dataclass&lt;/code&gt;. Supports burst.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;protocol&#34;&gt;&lt;code&gt;Protocol&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#protocol&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;ScreenshotDriver&lt;/span&gt;(Protocol):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;save_screenshot&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, path: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Save standard viewport screenshot.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it. &lt;em&gt;Any object&lt;/em&gt; with &lt;code&gt;save_screenshot(path: str) -&amp;gt; bool&lt;/code&gt; qualifies. Selenium WebDriver has it natively (&lt;code&gt;driver.save_screenshot&lt;/code&gt;), Playwright can be wrapped, a fake driver in tests exposes it trivially.&lt;/p&gt;&#xA;&lt;h2 id=&#34;screenshotterconfigtdriver&#34;&gt;&lt;code&gt;ScreenshotterConfig[TDriver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#screenshotterconfigtdriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@dataclass(frozen&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;ScreenshotterConfig&lt;/span&gt;[TDriver: ScreenshotDriver]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    output_dir: Path&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    file_ext: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;.png&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    health_check: HealthCheck[TDriver] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    save_full_page: SaveFullPageScreenshot[TDriver] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    default_burst_delay: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;float&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;0.5&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    max_filename_retries: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;500&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    uuid_length: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;8&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Field&lt;/th&gt;&#xA;          &lt;th&gt;Type&lt;/th&gt;&#xA;          &lt;th&gt;Role&lt;/th&gt;&#xA;          &lt;th&gt;Default&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;output_dir&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Path&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Screenshot directory&lt;/td&gt;&#xA;          &lt;td&gt; — &lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;file_ext&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Extension (&lt;code&gt;.png&lt;/code&gt;, &lt;code&gt;.jpg&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;.png&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;health_check&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;HealthCheck[TDriver] | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;(driver) -&amp;gt; None&lt;/code&gt; that raises if the driver is dead&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;save_full_page&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;SaveFullPageScreenshot[TDriver] | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;(driver, path) -&amp;gt; bool&lt;/code&gt; for full-page screenshots (Firefox only)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;default_burst_delay&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Delay between 2 shots in burst mode&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;0.5&lt;/code&gt;s&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;max_filename_retries&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Max attempts to generate a unique name&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;500&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;uuid_length&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;UUID suffix length in the name&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;8&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;Immutable. Configure once, ship.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.11.03 — Auto CLI store &#43; flags &#43; validation</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/03-selenium-cli/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/03-selenium-cli/</guid>
      <description>&lt;h1 id=&#34;021103auto-cli-store--flags--validation&#34;&gt;02.11.03 — Auto CLI store + flags + validation&lt;a class=&#34;anchor&#34; href=&#34;#021103auto-cli-store--flags--validation&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/opinionated/cli/selenium/create_cli_store.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/opinionated/cli/selenium/create_cli_store.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;flags&#34;&gt;Flags&lt;a class=&#34;anchor&#34; href=&#34;#flags&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Flag&lt;/th&gt;&#xA;          &lt;th&gt;Default&lt;/th&gt;&#xA;          &lt;th&gt;Type&lt;/th&gt;&#xA;          &lt;th&gt;Validation&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;--driver-path&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_file&lt;/code&gt; (except &lt;code&gt;--browser safari&lt;/code&gt;)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;--profile-path&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_none&lt;/code&gt; OR &lt;code&gt;is_dir&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;--browser&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;None&lt;/code&gt; (required)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;argparse &lt;code&gt;choices&lt;/code&gt; (per OS)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;--not-headless&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;False&lt;/code&gt; (= headless by default)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bool&lt;/code&gt; (&lt;code&gt;store_true&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;phantom&lt;/em&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;--workers&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;5&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;is_positive&lt;/code&gt; + &lt;code&gt;is_not_zero&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;--logger&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;&amp;quot;terminal+file&amp;quot;&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;choices=LOGGERS_CHOICES&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;--wait-timeout&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;10&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;≤ 60&lt;/code&gt;, &lt;code&gt;&amp;gt; 0&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;--dont-force-delete-tmp-dirs&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;False&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bool&lt;/code&gt; (&lt;code&gt;store_true&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;phantom&lt;/em&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;--only&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;list[str]&lt;/code&gt; (&lt;code&gt;nargs=&amp;quot;+&amp;quot;&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;phantom&lt;/em&gt; + mutex&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;--exclude&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;list[str]&lt;/code&gt; (&lt;code&gt;nargs=&amp;quot;+&amp;quot;&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;phantom&lt;/em&gt; + mutex&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_DEFAULT_WORKERS_AMOUNT &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;5&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_DEFAULT_LOGGER: SupportedLogger &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;terminal+file&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_DEFAULT_BROWSER_AUTOMATION_TIMEOUT &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;10&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_MAX_BROWSER_AUTOMATION_TIMEOUT &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;60&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;literal&#34;&gt;&lt;code&gt;Literal[...]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#literal&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; SeleniumCliStoreKeys &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Literal[&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;driver_path&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;profile_path&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;browser&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;headless&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;workers&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;logger&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;wait_timeout&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;force_delete_tmp_dirs&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;only&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;exclude&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These keys are the store&amp;rsquo;s whole vocabulary. Call-sites use them: &lt;code&gt;store.get(&amp;quot;workers&amp;quot;)&lt;/code&gt;, &lt;code&gt;store.get(&amp;quot;browser&amp;quot;)&lt;/code&gt;, etc.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Chapter 02 — Ocarina, the framework</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/</guid>
      <description>&lt;h1 id=&#34;chapter-02ocarina-the-framework&#34;&gt;Chapter 02 — Ocarina, the framework&lt;a class=&#34;anchor&#34; href=&#34;#chapter-02ocarina-the-framework&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Unpacks the whole &lt;code&gt;ocarina&lt;/code&gt; framework (Python 3.14+, v1.1.10) and Railway Oriented Programming straight through to the reporting plugins. Structured as a layered walk: &lt;strong&gt;deepest&lt;/strong&gt; (the &lt;code&gt;Result[T]&lt;/code&gt; type) to &lt;strong&gt;most visible&lt;/strong&gt; (the &lt;code&gt;bootstrap&lt;/code&gt; that boots everything).&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;chapter-plan&#34;&gt;Chapter plan&lt;a class=&#34;anchor&#34; href=&#34;#chapter-plan&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;#&lt;/th&gt;&#xA;          &lt;th&gt;Subject&lt;/th&gt;&#xA;          &lt;th&gt;File or folder&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;01&lt;/td&gt;&#xA;          &lt;td&gt;Technical identity, dependencies, toolchain&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/01-identity/&#34;&gt;&lt;code&gt;01-identity.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;02&lt;/td&gt;&#xA;          &lt;td&gt;Full module tree + layered diagram&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/02-module-tree/&#34;&gt;&lt;code&gt;02-module-tree.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;03&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Railway Oriented Programming&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/&#34;&gt;&lt;code&gt;03-railway/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;04&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Invariants&lt;/strong&gt; (validate, assertions, chain_validations)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/&#34;&gt;&lt;code&gt;04-invariants/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;05&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Orchestration&lt;/strong&gt; (Test → TestSuite → TestCampaign → TestCycle)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/&#34;&gt;&lt;code&gt;05-orchestration/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;06&lt;/td&gt;&#xA;          &lt;td&gt;Lifecycle of a &lt;code&gt;Scenario&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/06-scenario/&#34;&gt;&lt;code&gt;06-scenario.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;07&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Watcher&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/07-watcher/&#34;&gt;&lt;code&gt;07-watcher.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;08&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;POMBase&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/08-pom-base/&#34;&gt;&lt;code&gt;08-pom-base.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;09&lt;/td&gt;&#xA;          &lt;td&gt;Ports &amp;amp; adapters (&lt;code&gt;ILogger&lt;/code&gt;, &lt;code&gt;ITakeScreenshot&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/09-ports/&#34;&gt;&lt;code&gt;09-ports.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;10&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt; (pool, builder, screenshotter, act counter, Selenium adapters)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/&#34;&gt;&lt;code&gt;10-infra/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;11&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Opinionated layer&lt;/strong&gt; (CLI, loggers, plugins, bootstrap)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/&#34;&gt;&lt;code&gt;11-opinionated/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;12&lt;/td&gt;&#xA;          &lt;td&gt;Custom types &amp;amp; custom errors&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/12-custom-types-errors/&#34;&gt;&lt;code&gt;12-custom-types-errors.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;related-reading&#34;&gt;Related reading&lt;a class=&#34;anchor&#34; href=&#34;#related-reading&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Functional programming: &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/03-functional/&#34;&gt;&lt;code&gt;../03-functional/&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;How the framework tests itself: &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/&#34;&gt;&lt;code&gt;../04-internal-tests/&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;Usage examples with project adapters: &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/07-ocarina-example/&#34;&gt;&lt;code&gt;../07-ocarina-example/&lt;/code&gt;&lt;/a&gt;, &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/08-ai-example/&#34;&gt;&lt;code&gt;../08-ai-example/&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;CI: &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/10-cicd/02-ocarina-workflows/&#34;&gt;&lt;code&gt;../10-cicd/02-ocarina-workflows.md&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;</description>
    </item>
    <item>
      <title>Chapter 02.03 — Railway Oriented Programming</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/</guid>
      <description>&lt;h1 id=&#34;chapter-0203railway-oriented-programming&#34;&gt;Chapter 02.03 — Railway Oriented Programming&lt;a class=&#34;anchor&#34; href=&#34;#chapter-0203railway-oriented-programming&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Ocarina&amp;rsquo;s beating heart. The whole DSL rides on this mechanic: success and failure as two &lt;em&gt;parallel rails&lt;/em&gt;. A failure &lt;em&gt;switches&lt;/em&gt; the train onto the failure rail and locks it there (short-circuit), but composition stays linear so the scenario still reads top-to-bottom.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;plan&#34;&gt;Plan&lt;a class=&#34;anchor&#34; href=&#34;#plan&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;#&lt;/th&gt;&#xA;          &lt;th&gt;File&lt;/th&gt;&#xA;          &lt;th&gt;Subject&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;01&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/01-result/&#34;&gt;&lt;code&gt;01-result.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Result[T] = Ok[T] | Fail&lt;/code&gt;, &lt;code&gt;is_ok&lt;/code&gt;/&lt;code&gt;is_fail&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;02&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/02-action-chain-states/&#34;&gt;&lt;code&gt;02-action-chain-states.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;State machine &lt;code&gt;ActionStart → ActionFailure → ActionSuccess → ActionChain&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;03&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/03-neutral-rail/&#34;&gt;&lt;code&gt;03-neutral-rail.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Failure rail: &lt;code&gt;NeutralActionStart&lt;/code&gt; / &lt;code&gt;NeutralActionFailure&lt;/code&gt; / &lt;code&gt;NeutralActionSuccess&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;04&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/04-chain-actions-fold/&#34;&gt;&lt;code&gt;04-chain-actions-fold.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;ChainRunner[T]&lt;/code&gt; (thunk) + &lt;code&gt;chain_actions&lt;/code&gt; (fold).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;05&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/05-create-act-hooks/&#34;&gt;&lt;code&gt;05-create-act-hooks.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_act&lt;/code&gt; and its hooks (&lt;code&gt;on_failure&lt;/code&gt;, &lt;code&gt;on_run_effect&lt;/code&gt;, &lt;code&gt;act_counter_effect&lt;/code&gt;).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;06&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/06-drive-page/&#34;&gt;&lt;code&gt;06-drive-page.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;drive_page&lt;/code&gt;, a semantic alias.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;07&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/07-match-page-when/&#34;&gt;&lt;code&gt;07-match-page-when.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;match_page&lt;/code&gt; / &lt;code&gt;when&lt;/code&gt; + exception policy.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;lineage&#34;&gt;Lineage&lt;a class=&#34;anchor&#34; href=&#34;#lineage&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Direct inspiration: &lt;strong&gt;Railway Oriented Programming&lt;/strong&gt;, popularized by Scott Wlaschin (F#).&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.03.04 — chain_actions and ChainRunner</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/04-chain-actions-fold/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/04-chain-actions-fold/</guid>
      <description>&lt;h1 id=&#34;020304chain_actions-and-chainrunner&#34;&gt;02.03.04 — &lt;code&gt;chain_actions&lt;/code&gt; and &lt;code&gt;ChainRunner&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#020304chain_actions-and-chainrunner&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing_with_railway/chain_actions.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing_with_railway/chain_actions.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;The primitive that makes the DSL &amp;ldquo;flat.&amp;rdquo; Without it, an N-step scenario is a staircase of &lt;code&gt;.then(...).failure(...).success(...).execute()&lt;/code&gt;. With it, it&amp;rsquo;s a list.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;parenthesis-hell&#34;&gt;“Parenthesis hell”&lt;a class=&#34;anchor&#34; href=&#34;#parenthesis-hell&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Solution:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# Flat, readable syntax&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;runner &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; chain_actions(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    action1&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(h1)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(h2),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    action2&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(h3)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(h4),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    action3&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(h5)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(h6)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;chain &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; runner&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;run()  &lt;span style=&#34;color:#6272a4&#34;&gt;# Execute when ready&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Benefits:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Lazy evaluation&lt;/strong&gt;: Build chain without executing&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Flat syntax&lt;/strong&gt;: No deep nesting&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Automatic short-circuiting&lt;/strong&gt;: Stops on first failure&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Composability&lt;/strong&gt;: ChainRunner is a value&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/blockquote&gt;&lt;p&gt;&lt;em&gt;(Excerpt from the docstring.)&lt;/em&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.04.04 — .then(...) and chain_validations(...)</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/04-then-chain-of-validations/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/04-then-chain-of-validations/</guid>
      <description>&lt;h1 id=&#34;020404then-and-chain_validations&#34;&gt;02.04.04 — &lt;code&gt;.then(...)&lt;/code&gt; and &lt;code&gt;chain_validations(...)&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#020404then-and-chain_validations&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Two distinct mechanisms to compose multiple validations into one.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;thennew_value&#34;&gt;&lt;code&gt;.then(new_value)&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#thennew_value&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;then&lt;/span&gt;[U](&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, new_value: U, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, name: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; ValidationStartBlock[U]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; ValidationStartBlock(new_value, &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_chain, name)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Switches the type&lt;/strong&gt; from &lt;code&gt;T&lt;/code&gt; to &lt;code&gt;U&lt;/code&gt;. The checker accepts the change.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Keeps the chain&lt;/strong&gt; (&lt;code&gt;self._chain&lt;/code&gt;). Past assertions stay in it; new ones &lt;em&gt;stack on top&lt;/em&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Swaps the &lt;code&gt;name&lt;/code&gt;&lt;/strong&gt;: the &lt;code&gt;name&lt;/code&gt; you pass to &lt;code&gt;.then()&lt;/code&gt; is the one used for the next assertions, not the previous block&amp;rsquo;s.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h3 id=&#34;canonical-use-case&#34;&gt;Canonical use case&lt;a class=&#34;anchor&#34; href=&#34;#canonical-use-case&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;validate(user, name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;user&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;assert_that(is_not_none)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;then(user&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;email, name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;user.email&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;assert_that(is_email)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;then(user&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;age, name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;user.age&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;assert_that(is_positive)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;assert_that(is_less_than_or_equal_to(&lt;span style=&#34;color:#bd93f9&#34;&gt;120&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;execute()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;raise_if_invalid()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Reads as: validate &lt;code&gt;user&lt;/code&gt;, then &lt;code&gt;user.email&lt;/code&gt;, then &lt;code&gt;user.age&lt;/code&gt;. &lt;em&gt;All&lt;/em&gt; pass → OK. &lt;em&gt;One&lt;/em&gt; fails → every error rolls up into the &lt;code&gt;AggregateInvariantViolationError&lt;/code&gt;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.05.04 — TestSuite[Driver] — parallelized engine</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/04-test-suite/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/04-test-suite/</guid>
      <description>&lt;h1 id=&#34;020504testsuitedriverparallelized-engine&#34;&gt;02.05.04 — &lt;code&gt;TestSuite[Driver]&lt;/code&gt; — parallelized engine&lt;a class=&#34;anchor&#34; href=&#34;#020504testsuitedriverparallelized-engine&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing/oc_test_suite.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing/oc_test_suite.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;One job&lt;/strong&gt;: run a sequence of &lt;code&gt;Test&lt;/code&gt;s in parallel, handle &lt;em&gt;saturation&lt;/em&gt;, ID filtering, and pre-execution invariant validation.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;constructor&#34;&gt;Constructor&lt;a class=&#34;anchor&#34; href=&#34;#constructor&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;__init__&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    name: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    tests: Sequence[Test[Driver]],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    create_logger: Thunk[ILogger],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    drivers_pool: WebDriversPool[Driver],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    take_screenshot: ITakeScreenshot[Driver],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    act_counter: ActCounter &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    transient_errors: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;tuple&lt;/span&gt;[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt;[Exception], &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    copy_indicator: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;COPY&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    put_space_after_copy_indicator: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    max_retries_per_test: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    autoscreen_on_fail: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;False&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    saturate_workers: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    only_ids: Iterable[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    exclude_ids: Iterable[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_guards_on_invoke(tests)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#6272a4&#34;&gt;# ... assignments ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_tests &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; filter_tests_by_ids(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        tests,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        only&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;only_ids,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        exclude&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;exclude_ids,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;create_logger()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;set_prefix(&lt;span style=&#34;color:#ff79c6&#34;&gt;lambda&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;: filtering tests...&amp;#34;&lt;/span&gt;),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Parameter&lt;/th&gt;&#xA;          &lt;th&gt;Type&lt;/th&gt;&#xA;          &lt;th&gt;Role&lt;/th&gt;&#xA;          &lt;th&gt;Default&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Suite name (appears in logs &amp;amp; reports)&lt;/td&gt;&#xA;          &lt;td&gt; — &lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;tests&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Sequence[Test[Driver]]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;List of tests to run&lt;/td&gt;&#xA;          &lt;td&gt; — &lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_logger&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Thunk[ILogger]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Logger factory&lt;/td&gt;&#xA;          &lt;td&gt; — &lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;drivers_pool&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;WebDriversPool[Driver]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Shared pool&lt;/td&gt;&#xA;          &lt;td&gt; — &lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;take_screenshot&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;ITakeScreenshot[Driver]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Callable &lt;code&gt;(driver, logger, category) -&amp;gt; None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt; — &lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;act_counter&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;ActCounter | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;If &lt;code&gt;None&lt;/code&gt; → &lt;code&gt;ThreadsBasedActCounter()&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;transient_errors&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;tuple[type[Exception], ...]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Exceptions that trigger a retry&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;()&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;copy_indicator&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Prefix for saturation-cloned tests&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;&amp;quot;COPY&amp;quot;&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;put_space_after_copy_indicator&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[COPY 1]&lt;/code&gt; (&lt;code&gt;True&lt;/code&gt;) vs &lt;code&gt;[COPY1]&lt;/code&gt; (&lt;code&gt;False&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;max_retries_per_test&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;int | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Max retries for a potentially flaky test&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;None&lt;/code&gt; → &lt;code&gt;_DEFAULT_MAX_RETRIES_PER_TEST&lt;/code&gt; (8)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;autoscreen_on_fail&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Automatically capture one or several screenshots (burst) on failure&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;False&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;saturate_workers&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bool | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Clone tests to saturate every worker if the pool is large enough&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;None&lt;/code&gt; → cascade: suite &amp;gt; campaign &amp;gt; bootstrap&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;only_ids&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Iterable[str]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;--only&lt;/code&gt; filter&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;()&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;exclude_ids&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Iterable[str]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;--exclude&lt;/code&gt; filter&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;()&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;guardrails&#34;&gt;Guardrails&lt;a class=&#34;anchor&#34; href=&#34;#guardrails&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;at-__init__-time&#34;&gt;At &lt;code&gt;__init__&lt;/code&gt; time&lt;a class=&#34;anchor&#34; href=&#34;#at-__init__-time&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_guards_on_invoke&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, tests: Sequence[Test[Driver]]) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    validate_test_runners_ids(tests&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;tests, name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;tests&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;execute()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;raise_if_invalid()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;→ Every &lt;code&gt;test_id&lt;/code&gt; must be unique. Otherwise: &lt;code&gt;AggregateInvariantViolationError&lt;/code&gt;. &lt;em&gt;Raised at construction&lt;/em&gt;, before any execution attempt. Duplicate a &lt;code&gt;test_id&lt;/code&gt; by accident and you find out instantly.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.10.04 — ActCounter &#43; ThreadsBasedActCounter</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/04-act-counter/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/04-act-counter/</guid>
      <description>&lt;h1 id=&#34;021004actcounter--threadsbasedactcounter&#34;&gt;02.10.04 — &lt;code&gt;ActCounter&lt;/code&gt; + &lt;code&gt;ThreadsBasedActCounter&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#021004actcounter--threadsbasedactcounter&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Thread-local counter of &lt;code&gt;act&lt;/code&gt; calls per attempt. Lets the report say &amp;ldquo;&lt;em&gt;this test made 17 steps before dying at step 18&lt;/em&gt;.&amp;rdquo;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;interface&#34;&gt;Interface&lt;a class=&#34;anchor&#34; href=&#34;#interface&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# src/ocarina/infra/act_counter.py&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;ActCounter&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;get&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;reset&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;incr_act_call_count&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;default-implementation&#34;&gt;Default implementation&lt;a class=&#34;anchor&#34; href=&#34;#default-implementation&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# src/ocarina/opinionated/infra/act_counter.py&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;from&lt;/span&gt; threading &lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; local&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_thread_local &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; local()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_COUNTER_KEY: Final[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;ocarina_counter&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;ActCounter&lt;/span&gt;(_ActCounter):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;get&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;getattr&lt;/span&gt;(_thread_local, _COUNTER_KEY, &lt;span style=&#34;color:#bd93f9&#34;&gt;0&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;reset&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;setattr&lt;/span&gt;(_thread_local, _COUNTER_KEY, &lt;span style=&#34;color:#bd93f9&#34;&gt;0&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;incr_act_call_count&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;hasattr&lt;/span&gt;(_thread_local, _COUNTER_KEY):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;reset()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;setattr&lt;/span&gt;(_thread_local, _COUNTER_KEY, &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;getattr&lt;/span&gt;(_thread_local, _COUNTER_KEY) &lt;span style=&#34;color:#ff79c6&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;1&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;threading.local()&lt;/code&gt;&lt;/strong&gt;: namespace where attributes are &lt;em&gt;per thread&lt;/em&gt;. Each thread has its own value.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;_COUNTER_KEY = &amp;quot;ocarina_counter&amp;quot;&lt;/code&gt;&lt;/strong&gt;: attribute key.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;No lock&lt;/strong&gt;: each thread only writes its own value, so no race.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;why-thread-local-rather-than-a-shared-counter--lock&#34;&gt;Why thread-local rather than a shared counter + lock?&lt;a class=&#34;anchor&#34; href=&#34;#why-thread-local-rather-than-a-shared-counter--lock&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Ocarina&amp;rsquo;s architecture says &lt;strong&gt;one thread = one test&lt;/strong&gt;. We&amp;rsquo;ve got no appetite for a whole &lt;em&gt;state monad&lt;/em&gt; — might as well park this &amp;ldquo;impurity&amp;rdquo; (still a piece of state) right here. That&amp;rsquo;s it.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.11.04 — Loggers: PrintLogger, FileLogger, PrintAndFileLogger, MutedLogger</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/04-loggers/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/04-loggers/</guid>
      <description>&lt;h1 id=&#34;021104loggers-printlogger-filelogger-printandfilelogger-mutedlogger&#34;&gt;02.11.04 — Loggers: &lt;code&gt;PrintLogger&lt;/code&gt;, &lt;code&gt;FileLogger&lt;/code&gt;, &lt;code&gt;PrintAndFileLogger&lt;/code&gt;, &lt;code&gt;MutedLogger&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#021104loggers-printlogger-filelogger-printandfilelogger-mutedlogger&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source folder: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/tree/main/src/ocarina/opinionated/loggers&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/opinionated/loggers/&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Four canonical &lt;code&gt;ILogger&lt;/code&gt; implementations + a &lt;code&gt;create_matching_logger&lt;/code&gt; factory. All opt-in.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;create_matching_logger&#34;&gt;&lt;code&gt;create_matching_logger&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#create_matching_logger&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;LOGGERS_CHOICES &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;terminal&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;file&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;terminal+file&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;muted&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;create_matching_logger&lt;/span&gt;(mode: SupportedLogger) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; ILogger:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; mode &lt;span style=&#34;color:#ff79c6&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;terminal&amp;#34;&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; PrintLogger()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; mode &lt;span style=&#34;color:#ff79c6&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;file&amp;#34;&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; FileLogger(base_dir&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;get_default_log_dir())&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; mode &lt;span style=&#34;color:#ff79c6&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;terminal+file&amp;#34;&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; PrintAndFileLogger(base_dir&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;get_default_log_dir())&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; mode &lt;span style=&#34;color:#ff79c6&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;muted&amp;#34;&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; MutedLogger()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt; ValueError(&lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Unsupported logger mode: &lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;mode&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;get_default_log_dir&lt;/span&gt;() &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; Path:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; Path&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;cwd() &lt;span style=&#34;color:#ff79c6&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;.ocarina_logs&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;→ By default, logs land in &lt;code&gt;.ocarina_logs/&lt;/code&gt; at the user project root. Gitignored by convention.&lt;/p&gt;&#xA;&lt;h2 id=&#34;printlogger&#34;&gt;&lt;code&gt;PrintLogger&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#printlogger&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;PrintLogger&lt;/span&gt;(ILogger):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;__init__&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_prefix_thunk: Thunk[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;lambda&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_domain_taxonomy: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;tuple&lt;/span&gt;[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;set_prefix&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, prefix_thunk: Thunk[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;]) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; Self:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_prefix_thunk &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; prefix_thunk&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;set_domain_taxonomy&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, taxonomy: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;tuple&lt;/span&gt;[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;]) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; Self:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_domain_taxonomy &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; taxonomy&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;raw&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;args, stream&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;**&lt;/span&gt;kwargs) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; stream &lt;span style=&#34;color:#ff79c6&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            stream &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; sys&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;stdout&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;args, file&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;stream, &lt;span style=&#34;color:#ff79c6&#34;&gt;**&lt;/span&gt;kwargs)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;info&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, msg, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;args, exc&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;**&lt;/span&gt;kwargs):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_print_log(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;[INFO]&amp;#34;&lt;/span&gt;, msg, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;args, exc&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;exc, stream&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;sys&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;stdout)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#6272a4&#34;&gt;# ... id. for critical / error / warning / debug / success / test_name ...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;State&lt;/strong&gt;: &lt;code&gt;_prefix_thunk&lt;/code&gt; (Thunk) + &lt;code&gt;_domain_taxonomy&lt;/code&gt; (tuple).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Output&lt;/strong&gt;: &lt;code&gt;sys.stdout&lt;/code&gt; (info, debug, success, test_name) or &lt;code&gt;sys.stderr&lt;/code&gt; (critical, error, warning, exception).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Format&lt;/strong&gt;: typically &lt;code&gt;&amp;lt;prefix&amp;gt; [LEVEL] &amp;lt;message&amp;gt;&lt;/code&gt;, with ANSI colors if terminal.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;cleanup()&lt;/code&gt;&lt;/strong&gt;: no-op.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;filelogger&#34;&gt;&lt;code&gt;FileLogger&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#filelogger&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@final&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;FileLogger&lt;/span&gt;(PrintLogger):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;__init__&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        base_dir: Path,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        with_flush_effect: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        with_fallback_on_print_logger_when_no_taxonomy_effect: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_base_dir &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; base_dir&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_with_flush_effect &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; with_flush_effect&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_with_fallback_on_print_logger_when_no_taxonomy_effect &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            with_fallback_on_print_logger_when_no_taxonomy_effect&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;super&lt;/span&gt;()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#50fa7b&#34;&gt;__init__&lt;/span&gt;()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @property&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_log_file_path&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; Path:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;len&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_domain_taxonomy) &lt;span style=&#34;color:#ff79c6&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;1&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            folder_path &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_base_dir&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            file_name &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_domain_taxonomy[&lt;span style=&#34;color:#bd93f9&#34;&gt;0&lt;/span&gt;]&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;.log&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;else&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            folder_path &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_base_dir&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;joinpath(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_domain_taxonomy[:&lt;span style=&#34;color:#ff79c6&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#bd93f9&#34;&gt;1&lt;/span&gt;])&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            file_name &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_domain_taxonomy[&lt;span style=&#34;color:#ff79c6&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#bd93f9&#34;&gt;1&lt;/span&gt;]&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;.log&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        folder_path&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;mkdir(parents&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;, exist_ok&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; folder_path &lt;span style=&#34;color:#ff79c6&#34;&gt;/&lt;/span&gt; file_name&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Taxonomy&lt;/th&gt;&#xA;          &lt;th&gt;Folder&lt;/th&gt;&#xA;          &lt;th&gt;File&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;(&amp;quot;&amp;quot;)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt; — &lt;/td&gt;&#xA;          &lt;td&gt;→ Fallback to PrintLogger if enabled&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;(&amp;quot;annoncements&amp;quot;,)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;base_dir&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;annoncements.log&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;(&amp;quot;e2e&amp;quot;, &amp;quot;Login&amp;quot;)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;base_dir/e2e&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Login.log&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;(&amp;quot;e2e&amp;quot;, &amp;quot;Login&amp;quot;, &amp;quot;happy paths&amp;quot;, &amp;quot;Login without OTP&amp;quot;)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;base_dir/e2e/Login/happy paths&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Login without OTP.log&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;The &lt;strong&gt;taxonomy is the folder tree&lt;/strong&gt;. The DOCX reports (see &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/05-plugins-reports/&#34;&gt;&lt;code&gt;05-plugins-reports.md&lt;/code&gt;&lt;/a&gt;) walk it recursively and emit one &lt;code&gt;.docx&lt;/code&gt; per test case.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Chapter 02.04 — Invariants</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/</guid>
      <description>&lt;h1 id=&#34;chapter-0204invariants&#34;&gt;Chapter 02.04 — Invariants&lt;a class=&#34;anchor&#34; href=&#34;#chapter-0204invariants&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Ocarina&amp;rsquo;s sub-DSL for typed, composable invariants. Used everywhere: CLI, POMs, the &lt;code&gt;custom_invariants/testing/&lt;/code&gt; that check suite consistency before any test runs.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;plan&#34;&gt;Plan&lt;a class=&#34;anchor&#34; href=&#34;#plan&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;#&lt;/th&gt;&#xA;          &lt;th&gt;File&lt;/th&gt;&#xA;          &lt;th&gt;Subject&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;01&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/01-validate-flow/&#34;&gt;&lt;code&gt;01-validate-flow.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;The full flow &lt;code&gt;validate(value) → assert_that → execute → raise_if_invalid&lt;/code&gt; + the &lt;code&gt;_ValidationChain&lt;/code&gt; class.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;02&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/02-assertions/&#34;&gt;&lt;code&gt;02-assertions.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Catalog of builtin assertions (&lt;code&gt;is_str&lt;/code&gt;, &lt;code&gt;is_email&lt;/code&gt;, &lt;code&gt;is_positive&lt;/code&gt;, &lt;code&gt;is_in&lt;/code&gt;, &lt;code&gt;has_unique_elements&lt;/code&gt;, &lt;code&gt;each&lt;/code&gt;, &lt;code&gt;is_valid_filename&lt;/code&gt;, …).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;03&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/03-otherwise-any-of/&#34;&gt;&lt;code&gt;03-otherwise-any-of.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;.otherwise(...)&lt;/code&gt; and the OR combination via &lt;code&gt;_any_of&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;04&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/04-then-chain-of-validations/&#34;&gt;&lt;code&gt;04-then-chain-of-validations.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;.then(new_value)&lt;/code&gt; and &lt;code&gt;chain_validations(...)&lt;/code&gt; for composition.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;05&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/05-business-vs-framework-validator/&#34;&gt;&lt;code&gt;05-business-vs-framework-validator.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;BusinessInvariantValidator&lt;/code&gt; vs &lt;code&gt;FrameworkInvariantValidator&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;06&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/06-invariant-errors/&#34;&gt;&lt;code&gt;06-invariant-errors.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;InvariantViolationError&lt;/code&gt;, &lt;code&gt;DuplicatesError&lt;/code&gt;, &lt;code&gt;AggregateInvariantViolationError&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;why-the-invariants-dsl&#34;&gt;Why the invariants DSL&lt;a class=&#34;anchor&#34; href=&#34;#why-the-invariants-dsl&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Pre-execution guard.&lt;/strong&gt; &lt;code&gt;TestSuite.__init__&lt;/code&gt; checks — &lt;em&gt;before&lt;/em&gt; launching anything — that test names / IDs are unique, &lt;code&gt;max_workers &amp;gt;= 1&lt;/code&gt;, etc.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;CLI validation.&lt;/strong&gt; Every flag carries its &lt;code&gt;validate=lambda chain: chain.assert_that(...)&lt;/code&gt; inside the &lt;code&gt;CliStore&lt;/code&gt;. Errors aggregate into one message.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;POM-side validation.&lt;/strong&gt; POM actions validate their parameters (&amp;quot;&lt;code&gt;retries&lt;/code&gt; must be positive&amp;quot;) via &lt;code&gt;validate(retries, name=&amp;quot;retries&amp;quot;).assert_that(is_positive).execute().raise_if_invalid()&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;The Holy Book sums it up:&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.03.05 — create_act and its hooks</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/05-create-act-hooks/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/05-create-act-hooks/</guid>
      <description>&lt;h1 id=&#34;020305create_act-and-its-hooks&#34;&gt;02.03.05 — &lt;code&gt;create_act&lt;/code&gt; and its hooks&lt;a class=&#34;anchor&#34; href=&#34;#020305create_act-and-its-hooks&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing_with_railway/constructors/create_act.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing_with_railway/constructors/create_act.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;The &lt;strong&gt;low-level primitive&lt;/strong&gt; that builds a &lt;em&gt;test step&lt;/em&gt; constructor. You call it &lt;strong&gt;once&lt;/strong&gt; to mint a project-specific &lt;code&gt;act&lt;/code&gt; verb. In theory a complex project might want several distinct &lt;code&gt;act&lt;/code&gt; verbs with different hooks, but no real-world need has shown up yet — recommendation: create &lt;strong&gt;one&lt;/strong&gt;. Not a singleton, just a convention.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;signature&#34;&gt;Signature&lt;a class=&#34;anchor&#34; href=&#34;#signature&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;create_act&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    pom: TPOM,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    action: Callable[[TPOM], TPOM],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    on_failure: Callable[[TPOM, Exception], Fail] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    on_run_effect: Effect &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    act_counter_effect: Effect &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; ActionStart[TPOM]:&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Parameter&lt;/th&gt;&#xA;          &lt;th&gt;Position&lt;/th&gt;&#xA;          &lt;th&gt;Type&lt;/th&gt;&#xA;          &lt;th&gt;Role&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;pom&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;positional&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TPOM&lt;/code&gt; (bound &lt;code&gt;POMBase&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;The page (or any &lt;code&gt;POMBase&lt;/code&gt; subclass) you&amp;rsquo;re acting on.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;action&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;positional&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Callable[[TPOM], TPOM]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;The function that acts on the page and returns the page (fluent). Identity-typed.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;on_failure&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;keyword-only&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Callable[[TPOM, Exception], Fail] | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Hook for &lt;strong&gt;refining&lt;/strong&gt; the &lt;code&gt;Fail&lt;/code&gt;: if you reach this, you&amp;rsquo;ve already failed; the hook just lets you type the error more precisely (e.g. &lt;code&gt;HttpErrorPageReachedError&lt;/code&gt;).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;on_run_effect&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;keyword-only&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Effect | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Side effect called &lt;strong&gt;before&lt;/strong&gt; the action (e.g.: log a step number).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;act_counter_effect&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;keyword-only&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Effect | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;Additional&lt;/em&gt; side effect called before the action. Default: increments the act counter.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;code&#34;&gt;Code&lt;a class=&#34;anchor&#34; href=&#34;#code&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;run_action&lt;/span&gt;() &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; Result[TPOM]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;try&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; act_counter_effect:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act_counter_effect()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;else&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ThreadsBasedActCounter()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;incr_act_call_count()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; on_run_effect:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            on_run_effect()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        result_pom &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; action(pom)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; Ok(result_pom)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;except&lt;/span&gt; Exception &lt;span style=&#34;color:#ff79c6&#34;&gt;as&lt;/span&gt; exc:  &lt;span style=&#34;color:#6272a4&#34;&gt;# noqa: BLE001&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; on_failure:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; on_failure(pom, exc)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; Fail(error&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;exc)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; ActionStart(run_action)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;The &lt;code&gt;try&lt;/code&gt; wraps EVERYTHING&lt;/strong&gt;: &lt;code&gt;act_counter_effect&lt;/code&gt;, &lt;code&gt;on_run_effect&lt;/code&gt;, &lt;em&gt;and&lt;/em&gt; the action. If any side effect raises (say &lt;code&gt;on_run_effect&lt;/code&gt; hits a dead resource), it counts as a test-step failure. Everything that runs &amp;ldquo;during the test step&amp;rdquo; IS the test step.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Order is deterministic&lt;/strong&gt;: counter → run effect → action. The counter ticks &lt;em&gt;even if&lt;/em&gt; the action ends up failing. Intentional — we want to know how many steps were &lt;em&gt;attempted&lt;/em&gt;, not how many passed.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;on_failure&lt;/code&gt; can return an enriched &lt;code&gt;Fail&lt;/code&gt;&lt;/strong&gt;: that&amp;rsquo;s where a &lt;code&gt;WebDriverException&lt;/code&gt; becomes an &lt;code&gt;HttpErrorPageReachedError&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;default-actcounter-threadsbasedactcounter&#34;&gt;Default &lt;code&gt;ActCounter&lt;/code&gt;: &lt;code&gt;ThreadsBasedActCounter&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#default-actcounter-threadsbasedactcounter&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;from&lt;/span&gt; ocarina.opinionated.infra.act_counter &lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; ActCounter &lt;span style=&#34;color:#ff79c6&#34;&gt;as&lt;/span&gt; ThreadsBasedActCounter&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; act_counter_effect:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    act_counter_effect()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;else&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ThreadsBasedActCounter()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;incr_act_call_count()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Source: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/opinionated/infra/act_counter.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/opinionated/infra/act_counter.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.04.05 — BusinessInvariantValidator vs FrameworkInvariantValidator</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/05-business-vs-framework-validator/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/05-business-vs-framework-validator/</guid>
      <description>&lt;h1 id=&#34;020405businessinvariantvalidator-vs-frameworkinvariantvalidator&#34;&gt;02.04.05 — &lt;code&gt;BusinessInvariantValidator&lt;/code&gt; vs &lt;code&gt;FrameworkInvariantValidator&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#020405businessinvariantvalidator-vs-frameworkinvariantvalidator&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Two factories, identical code, exist purely &lt;strong&gt;to signal intent&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;code&#34;&gt;Code&lt;a class=&#34;anchor&#34; href=&#34;#code&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_BaseCustomInvariantValidator&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @staticmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;create&lt;/span&gt;[T](&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        value: T,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        name: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        build_chain: Callable[[ValidationStartBlock[T], T], ValidationAssertBlock[T]],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; ValidationAssertBlock[T]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; _create_business_invariant_validator(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            value, name, &lt;span style=&#34;color:#ff79c6&#34;&gt;lambda&lt;/span&gt; chain: build_chain(chain, value)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@final&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;BusinessInvariantValidator&lt;/span&gt;(_BaseCustomInvariantValidator):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Factory for creating business domain invariant validators.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;    Use this for domain-specific validation logic (e.g., &amp;#34;user must be adult&amp;#34;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;    &amp;#34;order total must match items&amp;#34;).&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@final&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;FrameworkInvariantValidator&lt;/span&gt;(_BaseCustomInvariantValidator):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Factory for creating framework-level invariant validators.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;    Use this for technical/framework validation logic (e.g., &amp;#34;config must be valid&amp;#34;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;    &amp;#34;test structure must be correct&amp;#34;).&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_create_business_invariant_validator&lt;/span&gt;[T](&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    value: T,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    name: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    build_chain: ValidationChainBuilder[T],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; ValidationAssertBlock[T]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    start_block &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; validate(value, name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;name)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; build_chain(start_block)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;why-two-classes&#34;&gt;Why two classes?&lt;a class=&#34;anchor&#34; href=&#34;#why-two-classes&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;code&gt;BusinessInvariantValidator&lt;/code&gt; flags a business rule; &lt;code&gt;FrameworkInvariantValidator&lt;/code&gt;, a technical framework rule. Intent jumps off the page.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Grep&lt;/strong&gt;. &lt;code&gt;FrameworkInvariantValidator.create&lt;/code&gt; finds &lt;em&gt;every framework invariant&lt;/em&gt; in one search. Same for business rules on the project side.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Maintainability&lt;/strong&gt;. Want to add behavior (different log, different stack trace) to one or the other? Do it without touching every call-site.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;usage&#34;&gt;Usage&lt;a class=&#34;anchor&#34; href=&#34;#usage&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Invariant&lt;/th&gt;&#xA;          &lt;th&gt;Category&lt;/th&gt;&#xA;          &lt;th&gt;Where it lives&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;validate_workers_amount&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Framework&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/custom_invariants/testing/workers.py&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;validate_test_runners_ids&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Framework&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/custom_invariants/testing/oc_test_runners_ids.py&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;validate_test_runners_names&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Framework&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/custom_invariants/testing/oc_test_runners_names.py&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;validate_test_suites_names&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Framework&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/custom_invariants/testing/oc_test_suites_names.py&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;validate_test_campaigns_names&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Framework&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/custom_invariants/testing/oc_test_campaigns_names.py&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;validate_test_cycle_name&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Framework&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/custom_invariants/testing/oc_test_cycles_names.py&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;em&gt;Business example&lt;/em&gt;: “the username must be on the whitelist”&lt;/td&gt;&#xA;          &lt;td&gt;Business&lt;/td&gt;&#xA;          &lt;td&gt;To be written on the user project side&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;em&gt;Business example&lt;/em&gt;: “the booking date must be future and &amp;lt; 1 year”&lt;/td&gt;&#xA;          &lt;td&gt;Business&lt;/td&gt;&#xA;          &lt;td&gt;To be written on the user project side&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;full-pattern&#34;&gt;Full pattern&lt;a class=&#34;anchor&#34; href=&#34;#full-pattern&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# src/ocarina/custom_invariants/testing/workers.py&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;from&lt;/span&gt; ocarina.dsl.invariants.assertions &lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; is_not_zero, is_positive&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;from&lt;/span&gt; ocarina.dsl.invariants.validate &lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; FrameworkInvariantValidator&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_workers_amount_chain&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    chain: ValidationStartBlock[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    value: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; ValidationAssertBlock[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    msg &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Value Error: Number of workers must be at least 1 (got: &lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;value&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;).&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; chain&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;assert_that(is_positive, msg&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;msg)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;assert_that(is_not_zero, msg&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;msg)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;validate_workers_amount&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, workers_amount: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;, name: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; ValidationAssertBlock[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Validate that workers amount is at least 1.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; FrameworkInvariantValidator&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;create(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        workers_amount, name, _workers_amount_chain&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;&#xA;&lt;li&gt;Private &lt;code&gt;_workers_amount_chain(chain, value) -&amp;gt; ValidationAssertBlock&lt;/code&gt; defines the &lt;em&gt;what&lt;/em&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Public &lt;code&gt;validate_workers_amount(*, workers_amount, name)&lt;/code&gt; is the API and delegates to &lt;code&gt;FrameworkInvariantValidator.create&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Caller in &lt;code&gt;TestSuite.run&lt;/code&gt;: &lt;code&gt;validate_workers_amount(...).execute().raise_if_invalid()&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;which-factory-for-which-case&#34;&gt;Which factory for which case&lt;a class=&#34;anchor&#34; href=&#34;#which-factory-for-which-case&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Holy Book (excerpt from the &amp;ldquo;Extensibility&amp;rdquo; chapter):&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.05.05 — Worker saturation</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/05-saturation/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/05-saturation/</guid>
      <description>&lt;h1 id=&#34;020505worker-saturation&#34;&gt;02.05.05 — Worker saturation&lt;a class=&#34;anchor&#34; href=&#34;#020505worker-saturation&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Ocarina-specific trick: if a suite has fewer tests than workers, &lt;strong&gt;tests get randomly cloned&lt;/strong&gt; until the worker count is filled. Copies are renamed &lt;code&gt;[COPY 1] &amp;lt;name&amp;gt;&lt;/code&gt;, &lt;code&gt;[COPY 2] &amp;lt;name&amp;gt;&lt;/code&gt;, etc.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;why&#34;&gt;Why&lt;a class=&#34;anchor&#34; href=&#34;#why&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The Holy Book formalizes the rationale in the &amp;ldquo;First real-world hurdles&amp;rdquo; chapter:&lt;/p&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Its &lt;code&gt;saturate_workers&lt;/code&gt; option lets you force random cloning of tests inside a suite.&lt;/p&gt;&#xA;&lt;p&gt;As soon as there are more available &lt;em&gt;workers&lt;/em&gt; in the &lt;em&gt;DriversPool&lt;/em&gt; than there are tests to run in a suite, Ocarina will randomly clone tests, start every driver, and assign each one a test to perform.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.10.05 — Selenium adapters</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/05-selenium-adapters/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/05-selenium-adapters/</guid>
      <description>&lt;h1 id=&#34;021005selenium-adapters&#34;&gt;02.10.05 — Selenium adapters&lt;a class=&#34;anchor&#34; href=&#34;#021005selenium-adapters&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source folder: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/tree/main/src/ocarina/infra/selenium&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/infra/selenium/&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Everything that materializes the abstractions (&lt;code&gt;POMBase&lt;/code&gt;, &lt;code&gt;WebDriversPool&lt;/code&gt;, &lt;code&gt;Screenshotter&lt;/code&gt;) in Selenium flavor. &lt;strong&gt;Lives outside the pure DSL&lt;/strong&gt; — and since &lt;code&gt;1.1.3&lt;/code&gt; it has a twin: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/tree/main/src/ocarina/infra/playwright&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/infra/playwright/&lt;/code&gt;&lt;/a&gt; ships the exact same contracts in Playwright flavor. Swapping backend is now picking a folder, not writing one.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;files-in-the-folder&#34;&gt;Files in the folder&lt;a class=&#34;anchor&#34; href=&#34;#files-in-the-folder&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;File&lt;/th&gt;&#xA;          &lt;th&gt;Role&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_driver.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;_build_firefox&lt;/code&gt;, &lt;code&gt;_build_chrome&lt;/code&gt;, &lt;code&gt;_build_edge&lt;/code&gt;, &lt;code&gt;_build_safari&lt;/code&gt; + dispatch&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_drivers_pool.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_selenium_drivers_pool&lt;/code&gt; (uses &lt;code&gt;DriverBuilder&lt;/code&gt; + &lt;code&gt;WebDriversPool&lt;/code&gt;)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_screenshotter.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_selenium_screenshotter&lt;/code&gt; + &lt;code&gt;_selenium_save_full_page&lt;/code&gt; (Firefox-only)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;driver_healthcheck.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;driver_healthcheck(driver)&lt;/code&gt; — pings &lt;code&gt;driver.title&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;mixins.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;SeleniumTitleMixin&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;create_driverpy&#34;&gt;&lt;code&gt;create_driver.py&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#create_driverpy&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_build_firefox&lt;/span&gt;(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, profile_path, driver_path, headless, wait_timeout) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; WebDriver:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    service &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; FirefoxService(executable_path&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;driver_path)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    options &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; FirefoxOptions()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; headless:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        options&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;add_argument(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;-headless&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; profile_path:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        options&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;profile &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; FirefoxProfile(profile_path)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    driver &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Firefox(service&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;service, options&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;options)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    driver&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;implicitly_wait(wait_timeout)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; driver&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_build_chrome&lt;/span&gt;(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, profile_path, driver_path, headless, wait_timeout) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; WebDriver:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    service &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; ChromeService(executable_path&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;driver_path)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    options &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; ChromeOptions()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; headless:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        options&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;add_argument(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;--headless=new&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; profile_path:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        options&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;add_argument(&lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;--user-data-dir=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;profile_path&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    driver &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Chrome(service&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;service, options&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;options)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    driver&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;implicitly_wait(wait_timeout)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; driver&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_build_edge&lt;/span&gt;(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, profile_path, driver_path, headless, wait_timeout) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; WebDriver:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#6272a4&#34;&gt;# … same as Chrome with EdgeService/EdgeOptions/Edge …&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_build_safari&lt;/span&gt;(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, profile_path, driver_path, headless, wait_timeout) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; WebDriver:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#6272a4&#34;&gt;# … Safari supports neither driver_path (uses macOS&amp;#39; native safaridriver),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#6272a4&#34;&gt;#   nor profile_path (no custom profile), nor headless …&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;1-implicitly_wait-set-only-once&#34;&gt;1. &lt;code&gt;implicitly_wait&lt;/code&gt; set only once&lt;a class=&#34;anchor&#34; href=&#34;#1-implicitly_wait-set-only-once&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Each builder calls &lt;code&gt;driver.implicitly_wait(wait_timeout)&lt;/code&gt; &lt;em&gt;once&lt;/em&gt;. Any &lt;code&gt;find_element&lt;/code&gt; that doesn&amp;rsquo;t find the element immediately waits up to &lt;code&gt;wait_timeout&lt;/code&gt; seconds before raising &lt;code&gt;NoSuchElementException&lt;/code&gt;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.11.05 — Report plugins</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/05-plugins-reports/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/05-plugins-reports/</guid>
      <description>&lt;h1 id=&#34;021105report-plugins&#34;&gt;02.11.05 — Report plugins&lt;a class=&#34;anchor&#34; href=&#34;#021105report-plugins&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source folder: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/tree/main/src/ocarina/opinionated/plugins/reports&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/opinionated/plugins/reports/&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;pretty_print_results&lt;/code&gt;, &lt;code&gt;results_to_json&lt;/code&gt;, &lt;code&gt;generate_docx_proof&lt;/code&gt;, &lt;code&gt;timing&lt;/code&gt;. All opt-in. Run sequentially or in parallel via &lt;code&gt;run_plugins&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;pretty_print_results&#34;&gt;&lt;code&gt;pretty_print_results&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#pretty_print_results&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# src/ocarina/opinionated/plugins/reports/pretty_print_results.py&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;pretty_print_results&lt;/span&gt;(results: TestCycleResults, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, with_colors: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;False&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Display the full test results.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Campaign&#xA;• Suite&#xA;  &amp;gt; Test case 1&#xA;    » PASSED&#xA;  &amp;gt; Test case 2&#xA;    » FAILED&#xA;      → Error message&#xA;        ⫸ At step 3&#xA;  &amp;gt; Test case 3&#xA;    » SKIPPED&#xA;&#xA;Test results:&#xA;1 FAILED | 1 PASSED | 1 SKIPPED&lt;/code&gt;&lt;/pre&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Aspect&lt;/th&gt;&#xA;          &lt;th&gt;Detail&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Hierarchy&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;3 levels: campaign &lt;code&gt;•&lt;/code&gt;, suite &lt;code&gt;&amp;gt;&lt;/code&gt;, case &lt;code&gt;»&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Levels&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;PASSED&lt;/code&gt; (green), &lt;code&gt;FAILED&lt;/code&gt; (red), &lt;code&gt;SKIPPED&lt;/code&gt; (gray)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Error&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Message + step number where it failed (&amp;quot;&lt;em&gt;At step 3&lt;/em&gt;&amp;quot;)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Aggregated count at the bottom&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;results_to_json&#34;&gt;&lt;code&gt;results_to_json&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#results_to_json&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;generate_json_results&lt;/span&gt;(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, results: TestCycleResults, output_dir: Path, logger: ILogger) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Write test results to a JSON file in results_dir.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    output_dir&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;mkdir(parents&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;, exist_ok&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    max_stem_length &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;8&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    max_attempts &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;500&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; _ &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;range&lt;/span&gt;(max_attempts):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        filename &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;uuid&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;uuid4()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;hex[:max_stem_length]&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;.json&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        file_path &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; output_dir &lt;span style=&#34;color:#ff79c6&#34;&gt;/&lt;/span&gt; filename&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; file_path&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;exists():&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;break&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;else&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt; RuntimeError(&lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Can&amp;#39;t generate unique JSON filename in: &lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;output_dir&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;.&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;Campaign&amp;#34;&lt;/span&gt;: {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;Suite&amp;#34;&lt;/span&gt;: {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;test_case&amp;#34;&lt;/span&gt;: [&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {&lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;success&amp;#34;&lt;/span&gt; | &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;fail&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;error&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;...&amp;#34;&lt;/span&gt;},&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        counter,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        metadata&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      ]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_result_to_serializable&lt;/span&gt;(v: Any) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;dict&lt;/span&gt;[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;, &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; is_ok(v):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; {&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;success&amp;#34;&lt;/span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; is_fail(v):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; {&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;fail&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;error&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;(v&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;error)}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt; TypeError(&lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Expected Ok or Fail instances, but got: &lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt;(v)&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Filename is an 8-char hex UUID → 16⁸ ≈ 4 billion options. No collisions in practice. 500 safety retries.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Chapter 02.05 — Orchestration</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/</guid>
      <description>&lt;h1 id=&#34;chapter-0205orchestration&#34;&gt;Chapter 02.05 — Orchestration&lt;a class=&#34;anchor&#34; href=&#34;#chapter-0205orchestration&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;&lt;code&gt;Test → TestSuite → TestCampaign → TestCycle&lt;/code&gt;: how each level slots into the next, who owns parallelization, who owns retries, who decides skipping, and where the pre-execution invariants live.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;plan&#34;&gt;Plan&lt;a class=&#34;anchor&#34; href=&#34;#plan&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;#&lt;/th&gt;&#xA;          &lt;th&gt;File&lt;/th&gt;&#xA;          &lt;th&gt;Subject&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;01&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/01-test/&#34;&gt;&lt;code&gt;01-test.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;The &lt;code&gt;Test[Driver]&lt;/code&gt; class — &lt;code&gt;spawn&lt;/code&gt;, pre/post fragments, skip.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;02&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/02-test-executor/&#34;&gt;&lt;code&gt;02-test-executor.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestExecutor&lt;/code&gt; — execution of &lt;strong&gt;one&lt;/strong&gt; attempt, order: setup → watchers.start → chain → watchers.stop → teardown.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;03&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/03-test-flow-retries/&#34;&gt;&lt;code&gt;03-test-flow-retries.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestFlow&lt;/code&gt; — retry loop (1 + max_retries), linear backoff, setup-failure handling.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;04&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/04-test-suite/&#34;&gt;&lt;code&gt;04-test-suite.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestSuite&lt;/code&gt; — parallelization with &lt;code&gt;ThreadPoolExecutor&lt;/code&gt;, ID filtering, guardrails.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;05&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/05-saturation/&#34;&gt;&lt;code&gt;05-saturation.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Worker saturation: random cloning &lt;code&gt;[COPY N]&lt;/code&gt;. Why and how.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;06&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/06-test-campaign/&#34;&gt;&lt;code&gt;06-test-campaign.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestCampaign&lt;/code&gt; — sequence of suites, &lt;code&gt;campaign_has_failed&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;07&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/07-test-cycle-modes/&#34;&gt;&lt;code&gt;07-test-cycle-modes.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestCycle&lt;/code&gt; — smoke + main, &lt;code&gt;fail-fast&lt;/code&gt; vs &lt;code&gt;wait-for-all&lt;/code&gt; modes, &lt;code&gt;has_test_cycle_failed&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;08&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/08-filter-tests-by-ids/&#34;&gt;&lt;code&gt;08-filter-tests-by-ids.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;filter_tests_by_ids&lt;/code&gt; — &lt;code&gt;--only&lt;/code&gt;/&lt;code&gt;--exclude&lt;/code&gt;, mutex, ignores unknown IDs.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;diagram&#34;&gt;Diagram&lt;a class=&#34;anchor&#34; href=&#34;#diagram&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;            ┌────────────────────────────────────────────────┐&#xA;            │                  TestCycle                     │&#xA;            │                                                │&#xA;            │  ┌──────────────────────────────────────────┐  │&#xA;            │  │ smoke_tests_campaigns                    │  │ ◄── first, gate&#xA;            │  │  └─ TestCampaign                         │  │     mode: fail-fast |&#xA;            │  │      └─ TestSuite                        │  │           wait-for-all&#xA;            │  │          └─ Test                         │  │&#xA;            │  └──────────────────────────────────────────┘  │&#xA;            │                                                │&#xA;            │  ┌──────────────────────────────────────────┐  │&#xA;            │  │ campaigns (main)                         │  │ ◄── skipped if a&#xA;            │  │  └─ TestCampaign                         │  │     smoke fails&#xA;            │  │      └─ TestSuite                        │  │&#xA;            │  │          └─ Test                         │  │&#xA;            │  └──────────────────────────────────────────┘  │&#xA;            └────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;who-does-what&#34;&gt;Who does what?&lt;a class=&#34;anchor&#34; href=&#34;#who-does-what&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Level&lt;/th&gt;&#xA;          &lt;th&gt;Single responsibility&lt;/th&gt;&#xA;          &lt;th&gt;Concurrency&lt;/th&gt;&#xA;          &lt;th&gt;Out of scope&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;Test&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Metadata + &lt;code&gt;spawn(driver, logger)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;none&lt;/td&gt;&#xA;          &lt;td&gt;execution&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestExecutor&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;One &lt;strong&gt;attempt&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;none&lt;/td&gt;&#xA;          &lt;td&gt;retries, driver acquisition&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestFlow&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Retry&lt;/strong&gt; loop&lt;/td&gt;&#xA;          &lt;td&gt;none&lt;/td&gt;&#xA;          &lt;td&gt;parallelization, aggregation&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestSuite&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Parallelization + saturation + ID filtering&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;N threads&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;inter-suite sequence&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestCampaign&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Sequence of suites&lt;/td&gt;&#xA;          &lt;td&gt;suite-level&lt;/td&gt;&#xA;          &lt;td&gt;smoke vs main&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestCycle&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Smoke + main + mode&lt;/td&gt;&#xA;          &lt;td&gt;campaign-level&lt;/td&gt;&#xA;          &lt;td&gt;bootstrap / post-exec plugins&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;The strict separation is &lt;strong&gt;deliberate&lt;/strong&gt;. &lt;code&gt;TestExecutor&lt;/code&gt; knows nothing about retries. &lt;code&gt;TestFlow&lt;/code&gt; knows nothing about concurrency. &lt;code&gt;TestSuite&lt;/code&gt; knows nothing about campaign-level aggregation. One axis of responsibility per class.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.10.06 — The Playwright actor: one owner thread</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/06-playwright-actor/</link>
      <pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/06-playwright-actor/</guid>
      <description>&lt;h1 id=&#34;021006the-playwright-actor-one-owner-thread&#34;&gt;02.10.06 — The Playwright actor: one owner thread&lt;a class=&#34;anchor&#34; href=&#34;#021006the-playwright-actor-one-owner-thread&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source folder: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/tree/main/src/ocarina/infra/playwright&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/infra/playwright/&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;The Playwright adapter mirrors the Selenium one file for file (&lt;code&gt;create_driver&lt;/code&gt;, &lt;code&gt;create_drivers_pool&lt;/code&gt;, &lt;code&gt;create_screenshotter&lt;/code&gt;, &lt;code&gt;driver_healthcheck&lt;/code&gt;, &lt;code&gt;mixins&lt;/code&gt;), with &lt;strong&gt;one extra file&lt;/strong&gt;: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/infra/playwright/driver.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;driver.py&lt;/code&gt;&lt;/a&gt;. That file deserves a chapter of its own. It is what reconciles Playwright&amp;rsquo;s &lt;em&gt;sync&lt;/em&gt; API — inherently bound to one thread — with Ocarina&amp;rsquo;s threaded model (pool, warmup, Watcher). The model has been stressed and stabilized; it holds.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;the-problem-playwrights-sync-api-is-thread-affine&#34;&gt;The problem: Playwright&amp;rsquo;s sync API is thread-affine&lt;a class=&#34;anchor&#34; href=&#34;#the-problem-playwrights-sync-api-is-thread-affine&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Playwright&amp;rsquo;s &lt;strong&gt;sync&lt;/strong&gt; API binds every object it hands out — &lt;code&gt;Playwright&lt;/code&gt;, &lt;code&gt;Browser&lt;/code&gt;, &lt;code&gt;BrowserContext&lt;/code&gt;, &lt;code&gt;Page&lt;/code&gt;, &lt;code&gt;Locator&lt;/code&gt;, … — to the thread that called &lt;code&gt;sync_playwright().start()&lt;/code&gt;. Under the hood it is a &lt;em&gt;greenlet&lt;/em&gt; pinned to that thread. Touching any of them from another thread raises:&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.03.06 — drive_page</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/06-drive-page/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/06-drive-page/</guid>
      <description>&lt;h1 id=&#34;020306drive_page&#34;&gt;02.03.06 — &lt;code&gt;drive_page&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#020306drive_page&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/opinionated/dsl/drive_page.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/opinionated/dsl/drive_page.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;code&#34;&gt;Code&lt;a class=&#34;anchor&#34; href=&#34;#code&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;drive_page&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    first: ActionSuccess[TPOM], &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;rest: ActionSuccess[TPOM]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; ChainRunner[TPOM]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; chain_actions(first, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;rest)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Literally&lt;/strong&gt; &lt;code&gt;chain_actions&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-this-alias&#34;&gt;Why this alias?&lt;a class=&#34;anchor&#34; href=&#34;#why-this-alias&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;1-semantics-im-taking-control-of-a-page&#34;&gt;1. Semantics: &amp;ldquo;I&amp;rsquo;m taking control of a page&amp;rdquo;&lt;a class=&#34;anchor&#34; href=&#34;#1-semantics-im-taking-control-of-a-page&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Holy Book quote (&amp;ldquo;First scenarios&amp;rdquo;):&lt;/p&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;&lt;code&gt;drive_page&lt;/code&gt; expresses the fact that you are taking control of &lt;em&gt;one&lt;/em&gt; page.&#xA;Any &lt;em&gt;transition&lt;/em&gt; becomes explicit through the opening of a new &lt;code&gt;drive_page&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; [&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    drive_page(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        act(on_homepage, open_homepage)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        act(on_homepage, verify_homepage)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        act(on_homepage, click_cta)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ),                                          &lt;span style=&#34;color:#6272a4&#34;&gt;# ⬅ closing control of the homepage&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    drive_page(                                 &lt;span style=&#34;color:#6272a4&#34;&gt;# ⬅ opening control of the next page&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        act(on_target_page, verify_target_page)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;2-discipline&#34;&gt;2. Discipline&lt;a class=&#34;anchor&#34; href=&#34;#2-discipline&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Mixing acts from two different POMs inside one &lt;code&gt;drive_page&lt;/code&gt; is a &lt;em&gt;mypy&lt;/em&gt; error (see &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/02-action-chain-states/&#34;&gt;&lt;code&gt;02-action-chain-states.md&lt;/code&gt;&lt;/a&gt;, &amp;ldquo;narrowing via &lt;code&gt;act()&lt;/code&gt;&amp;rdquo; section). The semantics get enforced concretely.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.04.06 — Invariant error hierarchy</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/06-invariant-errors/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/04-invariants/06-invariant-errors/</guid>
      <description>&lt;h1 id=&#34;020406invariant-error-hierarchy&#34;&gt;02.04.06 — Invariant error hierarchy&lt;a class=&#34;anchor&#34; href=&#34;#020406invariant-error-hierarchy&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/invariants/errors.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/invariants/errors.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;InvariantViolationError (base — subclass of Exception)&#xA;├── DuplicatesError                  (raised by `has_unique_elements`)&#xA;└── AggregateInvariantViolationError (raised by `_ValidationResult.raise_if_invalid`)&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;invariantviolationerror&#34;&gt;&lt;code&gt;InvariantViolationError&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#invariantviolationerror&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;InvariantViolationError&lt;/span&gt;(Exception):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Base exception raised when an invariant is violated.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bare &lt;code&gt;Exception&lt;/code&gt; subclass, no extra logic. Every predicate raises this (or a subclass).&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;try&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    validate(value)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;assert_that(predicate)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;execute()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;raise_if_invalid()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;except&lt;/span&gt; InvariantViolationError &lt;span style=&#34;color:#ff79c6&#34;&gt;as&lt;/span&gt; e:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logger&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;error(&lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Validation failed: &lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;e&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;… catches &lt;strong&gt;every&lt;/strong&gt; violation with a single clause.&lt;/p&gt;&#xA;&lt;h2 id=&#34;duplicateserror&#34;&gt;&lt;code&gt;DuplicatesError&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#duplicateserror&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;DuplicatesError&lt;/span&gt;(InvariantViolationError):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;__init__&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, duplicates: Sequence[Any], message: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;duplicates &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; duplicates&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        msg &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; message &lt;span style=&#34;color:#ff79c6&#34;&gt;or&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Duplicate elements detected:&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;join(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34; - &lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;d&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; d &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; duplicates&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;super&lt;/span&gt;()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#50fa7b&#34;&gt;__init__&lt;/span&gt;(msg)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#xA;&lt;li&gt;Stashes the duplicate list in &lt;code&gt;.duplicates&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Default message: &amp;ldquo;&lt;em&gt;Duplicate elements detected:&lt;code&gt;\n&lt;/code&gt; - apple&lt;code&gt;\n&lt;/code&gt; - banana&lt;/em&gt;&amp;rdquo;.&lt;/li&gt;&#xA;&lt;li&gt;Accepts a custom &lt;code&gt;message=&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Raised by &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/invariants/assertions.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;has_unique_elements&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.05.06 — TestCampaign[Driver]</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/06-test-campaign/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/06-test-campaign/</guid>
      <description>&lt;h1 id=&#34;020506testcampaigndriver&#34;&gt;02.05.06 — &lt;code&gt;TestCampaign[Driver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#020506testcampaigndriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing/oc_test_campaign.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing/oc_test_campaign.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;One job&lt;/strong&gt;: run a &lt;strong&gt;sequence of suites&lt;/strong&gt; in order, share a workers config, and declare &lt;code&gt;campaign_has_failed&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;code&#34;&gt;Code&lt;a class=&#34;anchor&#34; href=&#34;#code&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;TestCampaign&lt;/span&gt;[Driver]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;__init__&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        name: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        suites: Sequence[TestSuite[Driver]],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        max_workers: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        saturate_workers: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        validate_test_suites_names(suites&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;suites, name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;suites&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;execute()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;raise_if_invalid()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; name&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_suites &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; suites&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_results: TestCampaignResults &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; {}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_max_workers &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; max_workers&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_saturate_workers &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; saturate_workers&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; suite &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_suites:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            suite&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_campaign_name &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;run_all&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, skip_all: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;False&lt;/span&gt;, saturate_workers: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; TestCampaignResults:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_results&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;clear()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        resolved_saturate_workers &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_saturate_workers&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_saturate_workers &lt;span style=&#34;color:#ff79c6&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;else&lt;/span&gt; saturate_workers&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; skip_all:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; suite &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_suites:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_results[suite&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    name: (&lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#bd93f9&#34;&gt;1&lt;/span&gt;, test_id)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; name, test_id &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; suite&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;test_names_and_ids&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_results&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; suite &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_suites:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_results[suite&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; suite&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;run(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                max_workers&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_max_workers,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                saturate_workers&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;resolved_saturate_workers,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            )&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_results&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;campaign_has_failed&lt;/span&gt;(results: TestCampaignResults) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;any&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        is_test_result_fail(outcome)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; campaign_results &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; results&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;values()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; outcome, _, _ &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; campaign_results&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;values()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;anatomy&#34;&gt;Anatomy&lt;a class=&#34;anchor&#34; href=&#34;#anatomy&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;constructor-guard&#34;&gt;Constructor guard&lt;a class=&#34;anchor&#34; href=&#34;#constructor-guard&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;validate_test_suites_names(suites&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;suites, name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;suites&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;execute()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;raise_if_invalid()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;→ Suite names inside a campaign must be unique — case-insensitively since &lt;code&gt;1.1.10&lt;/code&gt; (NFC + casefold). Raised &lt;em&gt;at construction&lt;/em&gt;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.11.06 — bootstrap &#43; run_plugins</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/06-bootstrap-launcher/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/06-bootstrap-launcher/</guid>
      <description>&lt;h1 id=&#34;021106bootstrap--run_plugins&#34;&gt;02.11.06 — &lt;code&gt;bootstrap&lt;/code&gt; + &lt;code&gt;run_plugins&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#021106bootstrap--run_plugins&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/opinionated/launcher/bootstrap.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/opinionated/launcher/bootstrap.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Entry point of an Ocarina project.&lt;br&gt;&#xA;Three steps: &lt;code&gt;cycle.run_all → run_plugins → post_exec&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;code&#34;&gt;Code&lt;a class=&#34;anchor&#34; href=&#34;#code&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;bootstrap&lt;/span&gt;[T](&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    test_cycle: TestCycle[T],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    run_plugins: Callable[[TestCycleResults], &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    post_exec: Callable[[TestCycleResults], &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    saturate_workers: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    results &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; test_cycle&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;run_all(saturate_workers&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;saturate_workers)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    run_plugins(results)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; post_exec:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        post_exec(results)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;order&#34;&gt;Order&lt;a class=&#34;anchor&#34; href=&#34;#order&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;1. cycle.run_all()  →  results&#xA;2. run_plugins(results)&#xA;3. post_exec(results) (optional)&lt;/code&gt;&lt;/pre&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Step&lt;/th&gt;&#xA;          &lt;th&gt;Effect&lt;/th&gt;&#xA;          &lt;th&gt;Why this order&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;1&lt;/td&gt;&#xA;          &lt;td&gt;All tests run&lt;/td&gt;&#xA;          &lt;td&gt;We need &lt;code&gt;results&lt;/code&gt; for the rest&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;2&lt;/td&gt;&#xA;          &lt;td&gt;Plugins fire (DOCX, JSON, etc.)&lt;/td&gt;&#xA;          &lt;td&gt;Must run &lt;em&gt;before&lt;/em&gt; &lt;code&gt;post_exec&lt;/code&gt; since it can take generation time&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;3&lt;/td&gt;&#xA;          &lt;td&gt;Post-exec (pretty_print + sys.exit)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;Last&lt;/em&gt; thing — after this, the process is dead&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;typing&#34;&gt;Typing&lt;a class=&#34;anchor&#34; href=&#34;#typing&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;run_plugins: Callable[[TestCycleResults], &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;post_exec: Callable[[TestCycleResults], &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Both receive the &lt;strong&gt;&lt;code&gt;results&lt;/code&gt;&lt;/strong&gt; as an argument:&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.03.07 — match_page / when</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/07-match-page-when/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/03-railway/07-match-page-when/</guid>
      <description>&lt;h1 id=&#34;020307match_page--when&#34;&gt;02.03.07 — &lt;code&gt;match_page&lt;/code&gt; / &lt;code&gt;when&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#020307match_page--when&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing_with_railway/match_page.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing_with_railway/match_page.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Bolted onto the framework &lt;strong&gt;after the fact&lt;/strong&gt;. The Holy Book puts it bluntly: &amp;ldquo;&lt;em&gt;&lt;code&gt;match_page&lt;/code&gt; and &lt;code&gt;when&lt;/code&gt; were added after the fact; the Igoristan was so random that the use case forced itself onto us. Their implementation was simple, proof of the grammar&amp;rsquo;s flexibility: other analogous structures could follow just as easily.&lt;/em&gt;&amp;rdquo;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;the-problem&#34;&gt;The problem&lt;a class=&#34;anchor&#34; href=&#34;#the-problem&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Some pages render differently:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Cookies banner showing or not.&lt;/li&gt;&#xA;&lt;li&gt;A/B test serving two UI variants.&lt;/li&gt;&#xA;&lt;li&gt;Degraded mode (maintenance page) instead of the real page.&lt;/li&gt;&#xA;&lt;li&gt;Anti-bot captcha.&lt;/li&gt;&#xA;&lt;li&gt;…&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The scenario needs to &lt;strong&gt;branch&lt;/strong&gt; without breaking the ROP chain.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.05.07 — TestCycle[Driver] &#43; modes</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/07-test-cycle-modes/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/07-test-cycle-modes/</guid>
      <description>&lt;h1 id=&#34;020507testcycledriver--modes&#34;&gt;02.05.07 — &lt;code&gt;TestCycle[Driver]&lt;/code&gt; + modes&lt;a class=&#34;anchor&#34; href=&#34;#020507testcycledriver--modes&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing/oc_test_cycle.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing/oc_test_cycle.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;One job&lt;/strong&gt;: orchestrate &lt;em&gt;smoke&lt;/em&gt; then &lt;em&gt;main&lt;/em&gt; campaigns, and apply a smoke-failure-handling mode.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;two-modes&#34;&gt;Two modes&lt;a class=&#34;anchor&#34; href=&#34;#two-modes&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; Mode &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Literal[&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;fail-fast-on-first-smoke-campaigns-sequence-fail&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;wait-for-all-smoke-tests&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Mode&lt;/th&gt;&#xA;          &lt;th&gt;Behavior&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;fail-fast-on-first-smoke-campaigns-sequence-fail&lt;/code&gt; (default)&lt;/td&gt;&#xA;          &lt;td&gt;First smoke campaign that fails → &lt;strong&gt;following&lt;/strong&gt; ones get &lt;em&gt;skipped&lt;/em&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;wait-for-all-smoke-tests&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Every&lt;/strong&gt; smoke campaign runs. If &lt;em&gt;any&lt;/em&gt; fails, the &lt;em&gt;main&lt;/em&gt; campaigns get skipped.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Case&lt;/th&gt;&#xA;          &lt;th&gt;Mode&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Dependency-style smoke (e.g. &amp;ldquo;login&amp;rdquo; then &amp;ldquo;dashboard accessible&amp;rdquo;)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;fail-fast&lt;/code&gt; (login dies → dashboard pointless)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Parallel smoke (e.g. &amp;ldquo;homepage&amp;rdquo; + &amp;ldquo;API ping&amp;rdquo;)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;wait-for-all&lt;/code&gt; (we want to see both)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&lt;code&gt;fail-fast&lt;/code&gt; is the default — it&amp;rsquo;s the common case.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.05.08 — filter_tests_by_ids</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/08-filter-tests-by-ids/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/05-orchestration/08-filter-tests-by-ids/</guid>
      <description>&lt;h1 id=&#34;020508filter_tests_by_ids&#34;&gt;02.05.08 — &lt;code&gt;filter_tests_by_ids&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#020508filter_tests_by_ids&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing/filter_tests_by_ids.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing/filter_tests_by_ids.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Called by &lt;code&gt;TestSuite.__init__&lt;/code&gt; to apply the CLI flags &lt;code&gt;--only &amp;lt;ids&amp;gt;&lt;/code&gt; and &lt;code&gt;--exclude &amp;lt;ids&amp;gt;&lt;/code&gt;. Mutually exclusive — can&amp;rsquo;t pass both.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;code&#34;&gt;Code&lt;a class=&#34;anchor&#34; href=&#34;#code&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;filter_tests_by_ids&lt;/span&gt;[Driver](&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    tests: Sequence[Test[Driver]],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    only: Iterable[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    exclude: Iterable[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logger: ILogger,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; Sequence[Test[Driver]]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    only_set &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;set&lt;/span&gt;(only)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    exclude_set &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;set&lt;/span&gt;(exclude)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; only_set &lt;span style=&#34;color:#ff79c6&#34;&gt;and&lt;/span&gt; exclude_set:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt; ValueError(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;--only and --exclude cannot be used together&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    known_ids &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; {test&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;test_id &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; test &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; tests}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; only_set:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        matched &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; only_set &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;amp;&lt;/span&gt; known_ids&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; matched:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            joined &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;, &amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;join(&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;sorted&lt;/span&gt;(matched))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            logger&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;info(&lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;--only: matched test IDs: &lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;joined&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; [test &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; test &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; tests &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; test&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;test_id &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; only_set]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; exclude_set:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        matched &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; exclude_set &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;amp;&lt;/span&gt; known_ids&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; matched:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            joined &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;, &amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;join(&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;sorted&lt;/span&gt;(matched))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            logger&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;info(&lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;--exclude: matched test IDs: &lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;joined&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; [test &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; test &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; tests &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; test&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;test_id &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; exclude_set]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; tests&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;four-guarantees&#34;&gt;Four guarantees&lt;a class=&#34;anchor&#34; href=&#34;#four-guarantees&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;1-mutex---only----exclude&#34;&gt;1. Mutex &lt;code&gt;--only&lt;/code&gt; / &lt;code&gt;--exclude&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#1-mutex---only----exclude&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; only_set &lt;span style=&#34;color:#ff79c6&#34;&gt;and&lt;/span&gt; exclude_set:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt; ValueError(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;--only and --exclude cannot be used together&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Passing both is a &lt;em&gt;runtime&lt;/em&gt; error (not a &lt;em&gt;mypy&lt;/em&gt; one). Semantically nonsense: &amp;ldquo;keep only A, B&amp;rdquo; AND &amp;ldquo;exclude C, D&amp;rdquo;? At the CLI level, doesn&amp;rsquo;t make sense. The mutex kills ambiguity.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Chapter 02.10 — Infrastructure</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/</guid>
      <description>&lt;h1 id=&#34;chapter-0210infrastructure&#34;&gt;Chapter 02.10 — Infrastructure&lt;a class=&#34;anchor&#34; href=&#34;#chapter-0210infrastructure&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Everything that touches &lt;em&gt;external resources&lt;/em&gt;: driver pool, builders, screenshotters, act counter, and the Selenium adapters that implement them. Since &lt;code&gt;1.1.3&lt;/code&gt;, a parallel &lt;code&gt;infra/playwright/&lt;/code&gt; ships the same contracts in Playwright flavor — same pool, same builder, same ports.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;plan&#34;&gt;Plan&lt;a class=&#34;anchor&#34; href=&#34;#plan&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;#&lt;/th&gt;&#xA;          &lt;th&gt;File&lt;/th&gt;&#xA;          &lt;th&gt;Subject&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;01&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/01-drivers-pool/&#34;&gt;&lt;code&gt;01-drivers-pool.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;WebDriversPool[Driver]&lt;/code&gt;: &lt;code&gt;Semaphore&lt;/code&gt;, &lt;code&gt;Queue&lt;/code&gt;, &lt;code&gt;warmup&lt;/code&gt; with watchdog, &lt;code&gt;shutdown&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;02&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/02-driver-builder/&#34;&gt;&lt;code&gt;02-driver-builder.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;DriverBuilder[Driver]&lt;/code&gt;: profile and tmp dirs handling.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;03&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/03-screenshotter/&#34;&gt;&lt;code&gt;03-screenshotter.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Screenshotter[TDriver]&lt;/code&gt; + &lt;code&gt;ScreenshotterConfig&lt;/code&gt;: burst, healthcheck, threadsafe.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;04&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/04-act-counter/&#34;&gt;&lt;code&gt;04-act-counter.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;ActCounter&lt;/code&gt; + &lt;code&gt;ThreadsBasedActCounter&lt;/code&gt; (thread-local).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;05&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/05-selenium-adapters/&#34;&gt;&lt;code&gt;05-selenium-adapters.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_driver&lt;/code&gt; (Firefox/Chrome/Edge/Safari), &lt;code&gt;create_drivers_pool&lt;/code&gt;, &lt;code&gt;create_screenshotter&lt;/code&gt;, &lt;code&gt;driver_healthcheck&lt;/code&gt;, &lt;code&gt;SeleniumTitleMixin&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;06&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/06-playwright-actor/&#34;&gt;&lt;code&gt;06-playwright-actor.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;The &lt;code&gt;PlaywrightDriver&lt;/code&gt; actor: owner thread, marshalling through &lt;code&gt;submit&lt;/code&gt;, the &lt;code&gt;call_timeout&lt;/code&gt; liveness ceiling, cross-thread warmup, observe-only Watcher.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;builtwebdriverdriver&#34;&gt;&lt;code&gt;BuiltWebDriver[Driver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#builtwebdriverdriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; BuiltWebDriver[Driver] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;tuple&lt;/span&gt;[Driver, Effect]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The exchange unit between the builder and the pool: a &lt;code&gt;(driver, dispose)&lt;/code&gt; tuple. &lt;code&gt;dispose&lt;/code&gt; is an &lt;code&gt;Effect&lt;/code&gt; (no argument, no return) that knows how to clean up &lt;em&gt;that exact&lt;/em&gt; driver (&lt;code&gt;driver.quit()&lt;/code&gt; + removal of the tmp profile if needed).&lt;/p&gt;</description>
    </item>
    <item>
      <title>Chapter 02.11 — opinionated/ layer</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/</guid>
      <description>&lt;h1 id=&#34;chapter-0211opinionated-layer&#34;&gt;Chapter 02.11 — &lt;code&gt;opinionated/&lt;/code&gt; layer&lt;a class=&#34;anchor&#34; href=&#34;#chapter-0211opinionated-layer&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Everything that&amp;rsquo;s &lt;strong&gt;opt-in&lt;/strong&gt;: a picky user can swap any of it. CLI, loggers, report plugins, bootstrap, &lt;code&gt;drive_page&lt;/code&gt; alias. This layer is what you see on the surface, not the core — the turnkey version Ocarina ships so a typical project doesn&amp;rsquo;t have to reinvent it.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;plan&#34;&gt;Plan&lt;a class=&#34;anchor&#34; href=&#34;#plan&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;#&lt;/th&gt;&#xA;          &lt;th&gt;File&lt;/th&gt;&#xA;          &lt;th&gt;Subject&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;01&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/01-cli-builder/&#34;&gt;&lt;code&gt;01-cli-builder.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;CliBuilder&lt;/code&gt;, &lt;code&gt;CliArg&lt;/code&gt;, &lt;code&gt;_SilentArgumentParser&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;02&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/02-cli-store-phantoms/&#34;&gt;&lt;code&gt;02-cli-store-phantoms.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;CliStore[TKeys]&lt;/code&gt;, &lt;code&gt;_CliField[T]&lt;/code&gt;, &lt;code&gt;phantom_validate&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;03&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/03-selenium-cli/&#34;&gt;&lt;code&gt;03-selenium-cli.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_selenium_auto_cli_store&lt;/code&gt; + flags + validation.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;04&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/04-loggers/&#34;&gt;&lt;code&gt;04-loggers.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;ILogger&lt;/code&gt; implementations: &lt;code&gt;PrintLogger&lt;/code&gt;, &lt;code&gt;FileLogger&lt;/code&gt;, &lt;code&gt;PrintAndFileLogger&lt;/code&gt;, &lt;code&gt;MutedLogger&lt;/code&gt;, &lt;code&gt;create_matching_logger&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;05&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/05-plugins-reports/&#34;&gt;&lt;code&gt;05-plugins-reports.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;pretty_print_results&lt;/code&gt;, &lt;code&gt;results_to_json&lt;/code&gt;, &lt;code&gt;generate_docx_proof&lt;/code&gt;, &lt;code&gt;timing&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;06&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/11-opinionated/06-bootstrap-launcher/&#34;&gt;&lt;code&gt;06-bootstrap-launcher.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bootstrap&lt;/code&gt; + &lt;code&gt;run_plugins&lt;/code&gt; (parallel).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;opt-out--opt-in&#34;&gt;Opt-out / opt-in&lt;a class=&#34;anchor&#34; href=&#34;#opt-out--opt-in&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Without &lt;code&gt;opinionated/&lt;/code&gt;&lt;/th&gt;&#xA;          &lt;th&gt;With &lt;code&gt;opinionated/&lt;/code&gt;&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Write your CLI from scratch&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_selenium_auto_cli_store()&lt;/code&gt; hands it to you&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Write your logger&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_matching_logger(&amp;quot;terminal+file&amp;quot;)&lt;/code&gt; hands it to you&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Write your reports&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;pretty_print_results&lt;/code&gt;, &lt;code&gt;generate_json_results&lt;/code&gt;, &lt;code&gt;generate_docx_proof&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Write your &lt;code&gt;main()&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bootstrap(test_cycle, run_plugins, post_exec)&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;alternatives&#34;&gt;Alternatives&lt;a class=&#34;anchor&#34; href=&#34;#alternatives&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Component&lt;/th&gt;&#xA;          &lt;th&gt;Replaceable by…&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;CliBuilder&lt;/code&gt; / &lt;code&gt;CliStore&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Any argparse-like lib (click, typer, fire)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;PrintLogger&lt;/code&gt; / &lt;code&gt;FileLogger&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Any &lt;code&gt;ILogger&lt;/code&gt; implementation&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;pretty_print_results&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Any &lt;code&gt;(TestCycleResults) -&amp;gt; None&lt;/code&gt; function&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;generate_docx_proof&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Same&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;bootstrap&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Any &lt;code&gt;() -&amp;gt; None&lt;/code&gt; function that calls &lt;code&gt;TestCycle.run_all&lt;/code&gt; + post-processes&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;drive_page&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;chain_actions&lt;/code&gt; directly, or a project alias (&lt;code&gt;drive_section&lt;/code&gt;, &lt;code&gt;drive_widget&lt;/code&gt;)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;</description>
    </item>
  </channel>
</rss>
