<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Chapter 04 — Framework internal tests on Ocarina — manual</title>
    <link>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/</link>
    <description>Recent content in Chapter 04 — Framework internal tests 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/04-internal-tests/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>04.01 — From the outside like a user strategy</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/01-strategy/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/01-strategy/</guid>
      <description>&lt;h1 id=&#34;0401from-the-outside-like-a-user-strategy&#34;&gt;04.01 — &amp;quot;&lt;em&gt;From the outside like a user&lt;/em&gt;&amp;quot; strategy&lt;a class=&#34;anchor&#34; href=&#34;#0401from-the-outside-like-a-user-strategy&#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;The posture lives in the scenario tests&amp;rsquo; &lt;code&gt;conftest.py&lt;/code&gt;. No test cheats by peeking at internals.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;testsscenariosconftestpy&#34;&gt;&lt;code&gt;tests/scenarios/conftest.py&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#testsscenariosconftestpy&#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;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;FakeDriver&lt;/code&gt; (dataclass)&lt;/td&gt;&#xA;          &lt;td&gt;Minimal driver: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;disposed&lt;/code&gt;. No Selenium method.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;make_built_driver()&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Factory &lt;code&gt;(FakeDriver, dispose)&lt;/code&gt;, i.e. the signature &lt;code&gt;WebDriversPool&lt;/code&gt; expects.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;make_pool(max_size=2)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;WebDriversPool[FakeDriver]&lt;/code&gt; ready to go.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;RecordingPOM(POMBase)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;POM that records its calls and can be configured to raise.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;acting(pom, step)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Full &lt;code&gt;ActionSuccess[RecordingPOM]&lt;/code&gt; (failure+success as no-ops).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;scenario_of(&amp;quot;ok&amp;quot;, &amp;quot;ok2&amp;quot;)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestScenario[FakeDriver]&lt;/code&gt; with N steps.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;failing_scenario(step=&amp;quot;boom&amp;quot;, exc=...)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;1-step scenario that raises.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;make_test(name, scenario=..., test_id=..., skipped=False)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Test[FakeDriver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;make_suite(name, tests, pool=..., transient_errors=..., max_retries_per_test=...)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestSuite[FakeDriver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;make_campaign(name, suites, max_workers=1)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestCampaign[FakeDriver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;make_cycle(name=&amp;quot;cycle&amp;quot;, campaigns=..., smoke=..., mode=...)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestCycle[FakeDriver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;run_chain(runner)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Helper that executes a &lt;code&gt;ChainRunner&lt;/code&gt; and returns the &lt;code&gt;ActionChain&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;fakedriver&#34;&gt;&lt;code&gt;FakeDriver&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#fakedriver&#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&#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;FakeDriver&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    title: &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;fake&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    disposed: &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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s &lt;strong&gt;all&lt;/strong&gt;.&#xA;&lt;strong&gt;Ocarina&amp;rsquo;s core calls no Selenium API.&lt;/strong&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>04.02 — Cram tests (prysk)</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/02-cram-prysk/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/02-cram-prysk/</guid>
      <description>&lt;h1 id=&#34;0402cram-tests-prysk&#34;&gt;04.02 — Cram tests (&lt;code&gt;prysk&lt;/code&gt;)&lt;a class=&#34;anchor&#34; href=&#34;#0402cram-tests-prysk&#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;CLI tests in &lt;em&gt;cram&lt;/em&gt; format: a &lt;code&gt;.t&lt;/code&gt; file contains shell commands and their expected output. Tool: &lt;code&gt;prysk&lt;/code&gt; (modern rewrite of &lt;code&gt;cram&lt;/code&gt;).&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;t-file&#34;&gt;&lt;code&gt;.t&lt;/code&gt; file&lt;a class=&#34;anchor&#34; href=&#34;#t-file&#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;Valid args produce a deterministic parsed config&#xA;&#xA;  $ touch driver&#xA;  $ &amp;#34;$PYTHON&amp;#34; &amp;#34;$TESTDIR/_demo_cli.py&amp;#34; --browser firefox --driver-path driver --workers 3 --wait-timeout 20 --logger terminal&#xA;  browser=firefox&#xA;  driver_path=driver&#xA;  headless=True&#xA;  workers=3&#xA;  wait_timeout=20&#xA;  logger=terminal&#xA;  only=()&#xA;  exclude=()&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;&#xA;&lt;li&gt;First line: title (comment for the human).&lt;/li&gt;&#xA;&lt;li&gt;Blank line.&lt;/li&gt;&#xA;&lt;li&gt;Lines starting with &lt;code&gt; $&lt;/code&gt;: shell command run.&lt;/li&gt;&#xA;&lt;li&gt;Following lines (indent 2): expected output.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Actual output differs → test fails.&lt;/p&gt;</description>
    </item>
    <item>
      <title>04.03 — Scenario tests (pytest &#43; allure)</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/03-pytest-scenarios/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/03-pytest-scenarios/</guid>
      <description>&lt;h1 id=&#34;0403scenario-tests-pytest--allure&#34;&gt;04.03 — Scenario tests (pytest + allure)&lt;a class=&#34;anchor&#34; href=&#34;#0403scenario-tests-pytest--allure&#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;15 &lt;code&gt;test_*.py&lt;/code&gt; files in &lt;code&gt;tests/scenarios/&lt;/code&gt; cover the DSL, orchestration and the Playwright actor. Dynamic tests.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;listing&#34;&gt;Listing&lt;a class=&#34;anchor&#34; href=&#34;#listing&#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;tests/scenarios/&#xA;├── conftest.py                              # FakeDriver, RecordingPOM, make_*&#xA;├── test_railway_and_action_chain.py         # create_act, drive_page, Result, ChainRunner&#xA;├── test_match_page.py                       # match_page / when (first/last branches, exceptions)&#xA;├── test_validate.py                         # validate, assert_that, otherwise, then, chain_validations&#xA;├── test_invariants_properties.py            # hypothesis (see 06-hypothesis-properties)&#xA;├── test_drivers_pool.py                     # WebDriversPool (acquire, warmup, shutdown, semaphore leaks)&#xA;├── test_screenshotter.py                    # Screenshotter (single shot, burst, healthcheck, threadsafety)&#xA;├── test_driver_builder.py                   # DriverBuilder (profile tmp dir, dispose)&#xA;├── test_watcher.py                          # Watcher (start, stop, callback errors swallowed, dedup cache)&#xA;├── test_playwright_driver_actor.py          # PlaywrightDriver actor (sync_playwright mocked, no browser)&#xA;├── test_playwright_adapter.py               # real-browser smoke (skipped if no Chromium installed)&#xA;├── test_test_suite.py                       # TestSuite (parallel, saturation, only/exclude, transient_errors)&#xA;├── test_test_cycle_and_bootstrap.py         # TestCycle, mode fail-fast vs wait-for-all, bootstrap&#xA;├── test_loggers_and_reports.py              # PrintLogger, FileLogger, pretty_print, JSON&#xA;├── test_docx_tests_proofs.py                # generate_docx_proof (parse logs, insert images)&#xA;└── test_env.py                              # smoke test: versions, Python 3.14+&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;example-internal-test&#34;&gt;Example internal test&lt;a class=&#34;anchor&#34; href=&#34;#example-internal-test&#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;@allure.epic(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Railway / action chain&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@allure.feature(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Action chain&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@allure.tag(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;act&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;handlers&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;happy-path&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@allure.severity(allure&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;severity_level&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;CRITICAL)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@allure.label(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;layer&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;unit&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@allure.title(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;A successful act reports success; the success handler fires once&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;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;test_success_act_fires_success_handler&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;    pom &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; RecordingPOM()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    calls &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; {&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;success&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#bd93f9&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;failure&amp;#34;&lt;/span&gt;: &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;    chain &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;        create_act(pom, &lt;span style=&#34;color:#ff79c6&#34;&gt;lambda&lt;/span&gt; p: p&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;step(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;click&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;failure(&lt;span style=&#34;color:#ff79c6&#34;&gt;lambda&lt;/span&gt; _: calls&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#50fa7b&#34;&gt;__setitem__&lt;/span&gt;(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;failure&amp;#34;&lt;/span&gt;, calls[&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;failure&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;.&lt;/span&gt;success(&lt;span style=&#34;color:#ff79c6&#34;&gt;lambda&lt;/span&gt;: calls&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#50fa7b&#34;&gt;__setitem__&lt;/span&gt;(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;success&amp;#34;&lt;/span&gt;, calls[&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;success&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;.&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;&#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;assert&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;    &lt;span style=&#34;color:#ff79c6&#34;&gt;assert&lt;/span&gt; pom&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;calls &lt;span style=&#34;color:#ff79c6&#34;&gt;==&lt;/span&gt; [&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;click&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;assert&lt;/span&gt; calls &lt;span style=&#34;color:#ff79c6&#34;&gt;==&lt;/span&gt; {&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;success&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#bd93f9&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;failure&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#bd93f9&#34;&gt;0&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;Assertion&lt;/th&gt;&#xA;          &lt;th&gt;Target&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;chain.is_ok()&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Public API&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;pom.calls == [&amp;quot;click&amp;quot;]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Observable POM behavior (recording)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;calls == {&amp;quot;success&amp;quot;: 1, &amp;quot;failure&amp;quot;: 0}&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Right handler called&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;allure-annotations&#34;&gt;Allure annotations&lt;a class=&#34;anchor&#34; href=&#34;#allure-annotations&#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;Category&lt;/th&gt;&#xA;          &lt;th&gt;Typical values&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;epic&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&amp;ldquo;Railway / action chain&amp;rdquo;, &amp;ldquo;Invariants&amp;rdquo;, &amp;ldquo;Orchestration&amp;rdquo;, &amp;ldquo;Watcher&amp;rdquo;, &amp;ldquo;Drivers pool&amp;rdquo;, &amp;ldquo;Reports&amp;rdquo;, &amp;ldquo;Match page&amp;rdquo;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;feature&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&amp;ldquo;Action chain&amp;rdquo;, &amp;ldquo;Validate&amp;rdquo;, &amp;ldquo;TestSuite&amp;rdquo;, &amp;ldquo;TestCycle&amp;rdquo;, &amp;ldquo;Bootstrap&amp;rdquo;, &amp;ldquo;Pretty print&amp;rdquo;, &amp;ldquo;JSON&amp;rdquo;, &amp;ldquo;DOCX proofs&amp;rdquo;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;tag&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&amp;ldquo;act&amp;rdquo;, &amp;ldquo;handlers&amp;rdquo;, &amp;ldquo;happy-path&amp;rdquo;, &amp;ldquo;error-handling&amp;rdquo;, &amp;ldquo;retry&amp;rdquo;, &amp;ldquo;transient&amp;rdquo;, &amp;ldquo;smoke&amp;rdquo;, &amp;ldquo;skip&amp;rdquo;, &amp;ldquo;saturation&amp;rdquo;, &amp;ldquo;parallel&amp;rdquo;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;severity&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;CRITICAL &amp;gt; NORMAL &amp;gt; MINOR &amp;gt; TRIVIAL&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;label(&amp;quot;layer&amp;quot;, ...)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&amp;ldquo;unit&amp;rdquo;, &amp;ldquo;integration&amp;rdquo;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;test_test_suitepy&#34;&gt;&lt;code&gt;test_test_suite.py&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#test_test_suitepy&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Tests pass in sequential mode (&lt;code&gt;max_workers=1&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;Tests pass in parallel mode (&lt;code&gt;max_workers=N&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;--only&lt;/code&gt; / &lt;code&gt;--exclude&lt;/code&gt; filtering.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;--only&lt;/code&gt; + &lt;code&gt;--exclude&lt;/code&gt; mutex.&lt;/li&gt;&#xA;&lt;li&gt;Saturation: tests cloned with &lt;code&gt;[COPY N]&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Retries on transient errors.&lt;/li&gt;&#xA;&lt;li&gt;Setup failure → SKIPPED when every attempt fails at setup.&lt;/li&gt;&#xA;&lt;li&gt;A &lt;code&gt;skipped=True&lt;/code&gt; test stops immediately.&lt;/li&gt;&#xA;&lt;li&gt;Guardrails (unique names, unique IDs).&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;take_screenshot&lt;/code&gt; called on fail if &lt;code&gt;autoscreen_on_fail&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Exact attempt counting on &lt;code&gt;max_retries&lt;/code&gt; (one test covers the loop edge).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;test_match_pagepy&#34;&gt;&lt;code&gt;test_match_page.py&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#test_match_pagepy&#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;Case&lt;/th&gt;&#xA;          &lt;th&gt;Assertion&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;First branch matches → executed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;chain.is_ok()&lt;/code&gt;, later branches not evaluated&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Second branch matches → executed&lt;/td&gt;&#xA;          &lt;td&gt;id.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;No branch matches&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;NoMatchingBranchError&lt;/code&gt; in the &lt;code&gt;Fail&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Branch matches, but its &lt;code&gt;then&lt;/code&gt; fails&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Fail&lt;/code&gt; propagated&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;condition&lt;/code&gt; raises an ordinary &lt;code&gt;Exception&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Treated as &lt;code&gt;False&lt;/code&gt;, later branches tried&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;condition&lt;/code&gt; raises an exception from the &lt;code&gt;raised_exceptions&lt;/code&gt; tuple&lt;/td&gt;&#xA;          &lt;td&gt;Re-raise&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Branch &lt;code&gt;then=[]&lt;/code&gt; matches&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Ok(None)&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;match_page&lt;/code&gt; in the middle of a &lt;code&gt;test_chain&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Short-circuits if failed&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;test_drivers_poolpy&#34;&gt;&lt;code&gt;test_drivers_pool.py&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#test_drivers_poolpy&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Uses a &lt;code&gt;FakeDriverFactory&lt;/code&gt; that can be configured to raise / block / sleep, and counts its calls.&lt;/p&gt;</description>
    </item>
    <item>
      <title>04.04 — Static typing tests (pytest-mypy-plugins)</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/04-mypy-plugins-types/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/04-mypy-plugins-types/</guid>
      <description>&lt;h1 id=&#34;0404static-typing-tests-pytest-mypy-plugins&#34;&gt;04.04 — &lt;em&gt;Static typing&lt;/em&gt; tests (&lt;code&gt;pytest-mypy-plugins&lt;/code&gt;)&lt;a class=&#34;anchor&#34; href=&#34;#0404static-typing-tests-pytest-mypy-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;Five &lt;code&gt;*test_types.yml&lt;/code&gt; files that test &lt;em&gt;the checker&amp;rsquo;s behavior&lt;/em&gt;. A testing dimension &lt;strong&gt;unique&lt;/strong&gt; to Ocarina in its way: we make sure that what &lt;em&gt;should&lt;/em&gt; be a &lt;em&gt;mypy&lt;/em&gt; error actually is one.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;listing&#34;&gt;Listing&lt;a class=&#34;anchor&#34; href=&#34;#listing&#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;tests/&#xA;├── dsl/&#xA;│   ├── invariants/test_types.yml&#xA;│   └── testing_with_railway/test_types.yml&#xA;└── opinionated/&#xA;    ├── cli/test_types.yml&#xA;    ├── infra/test_types.yml&#xA;    └── plugins/reports/...   (snapshots — not YAML)&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;yaml&#34;&gt;YAML&lt;a class=&#34;anchor&#34; href=&#34;#yaml&#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-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#ff79c6&#34;&gt;case&lt;/span&gt;: compatible_predicate_types&#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;description&lt;/span&gt;: Predicates should accept values of compatible types&#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;main&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;    from ocarina.dsl.invariants.validate import validate&#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;    from ocarina.dsl.invariants.assertions import is_email, is_positive&#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;    validate(1234).assert_that(is_positive)&#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;    validate(&amp;#34;a@a.com&amp;#34;).assert_that(is_email)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Without &lt;code&gt;out:&lt;/code&gt; → we expect &lt;code&gt;mypy&lt;/code&gt; to pass &lt;strong&gt;without error&lt;/strong&gt; on the given code.&lt;/p&gt;</description>
    </item>
    <item>
      <title>04.05 — Snapshot testing (syrupy)</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/05-syrupy-snapshots/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/05-syrupy-snapshots/</guid>
      <description>&lt;h1 id=&#34;0405snapshot-testing-syrupy&#34;&gt;04.05 — Snapshot testing (&lt;code&gt;syrupy&lt;/code&gt;)&lt;a class=&#34;anchor&#34; href=&#34;#0405snapshot-testing-syrupy&#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;syrupy&lt;/code&gt; is a pytest plugin that serializes a test&amp;rsquo;s output into a &lt;code&gt;.ambr&lt;/code&gt; file and compares to subsequent runs. Used for the output of &lt;code&gt;pretty_print_results&lt;/code&gt; and &lt;code&gt;results_to_json&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;ambr&#34;&gt;&lt;code&gt;.ambr&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#ambr&#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;tests/opinionated/plugins/reports/__snapshots__/&#xA;├── test_pretty_print_results.ambr&#xA;└── test_results_to_json.ambr&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Generated and managed by &lt;code&gt;syrupy&lt;/code&gt;.&lt;/p&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;&lt;span style=&#34;color:#6272a4&#34;&gt;# tests/opinionated/plugins/reports/test_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;test_pretty_print_results_renders_campaign_suite_test&lt;/span&gt;(snapshot, capsys):&#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; {&#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;Dashboard&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;Login happy paths&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;Login - without OTP&amp;#34;&lt;/span&gt;: (Ok(&lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;), &lt;span style=&#34;color:#bd93f9&#34;&gt;5&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;login_no_otp&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;Login - with OTP&amp;#34;&lt;/span&gt;: (Fail(error&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;RuntimeError(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;OTP missed&amp;#34;&lt;/span&gt;)), &lt;span style=&#34;color:#bd93f9&#34;&gt;8&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;login_otp&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;Skipped one&amp;#34;&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:#bd93f9&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;skipped_one&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;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    pretty_print_results(results, with_colors&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;    out &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; capsys&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;readouterr()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;out&#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;assert&lt;/span&gt; out &lt;span style=&#34;color:#ff79c6&#34;&gt;==&lt;/span&gt; snapshot&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;snapshot&lt;/code&gt; (&lt;code&gt;syrupy&lt;/code&gt; fixture):&lt;/p&gt;</description>
    </item>
    <item>
      <title>04.06 — Property-based testing (hypothesis)</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/06-hypothesis-properties/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/06-hypothesis-properties/</guid>
      <description>&lt;h1 id=&#34;0406property-based-testing-hypothesis&#34;&gt;04.06 — Property-based testing (&lt;code&gt;hypothesis&lt;/code&gt;)&lt;a class=&#34;anchor&#34; href=&#34;#0406property-based-testing-hypothesis&#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;A single file (&lt;code&gt;test_invariants_properties.py&lt;/code&gt;), but the pattern is interesting: we &lt;strong&gt;generate&lt;/strong&gt; inputs and verify &lt;strong&gt;universal properties&lt;/strong&gt; of the invariant predicates.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;the-concept&#34;&gt;The concept&lt;a class=&#34;anchor&#34; href=&#34;#the-concept&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;hypothesis&lt;/code&gt; auto-generates &lt;em&gt;test cases&lt;/em&gt; that satisfy given constraints, then checks a property holds for every generated case. On failure, &lt;em&gt;hypothesis&lt;/em&gt; does &lt;strong&gt;shrinking&lt;/strong&gt; to find the &lt;strong&gt;smallest counter-example&lt;/strong&gt;.&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;from&lt;/span&gt; hypothesis &lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; given, strategies &lt;span style=&#34;color:#ff79c6&#34;&gt;as&lt;/span&gt; st&#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;@given(st&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;integers())&#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;test_is_positive_passes_on_positive&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;if&lt;/span&gt; value &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;gt;=&lt;/span&gt; &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;        is_positive(value)             &lt;span style=&#34;color:#6272a4&#34;&gt;# must not raise&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;with&lt;/span&gt; pytest&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;raises(InvariantViolationError):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            is_positive(value)         &lt;span style=&#34;color:#6272a4&#34;&gt;# must raise&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;→ &lt;code&gt;hypothesis&lt;/code&gt; generates integers (typically 100 by default), checks the property on each. &lt;em&gt;Unexpected&lt;/em&gt; exception → fail. The shrinker finds the smallest failing case (&lt;code&gt;0&lt;/code&gt;? &lt;code&gt;-1&lt;/code&gt;? &lt;code&gt;MIN_INT&lt;/code&gt;?).&lt;/p&gt;</description>
    </item>
    <item>
      <title>04.07 — Coverage policy: what&#39;s tested, what isn&#39;t, why</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/07-coverage-policy/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/07-coverage-policy/</guid>
      <description>&lt;h1 id=&#34;0407coverage-policy-whats-tested-what-isnt-why&#34;&gt;04.07 — Coverage policy: what&amp;rsquo;s tested, what isn&amp;rsquo;t, why&lt;a class=&#34;anchor&#34; href=&#34;#0407coverage-policy-whats-tested-what-isnt-why&#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;Line coverage is &lt;strong&gt;explicitly scoped&lt;/strong&gt; to the pure DSL and agnostic infra. The rest gets tested by other means (cram, types, snapshots, e2e) or stays out of scope (inert shapes).&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;pyprojecttomltoolcoveragerun&#34;&gt;&lt;code&gt;pyproject.toml#tool.coverage.run&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#pyprojecttomltoolcoveragerun&#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-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[tool.coverage.run]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;omit = [&#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;*/__init__.py&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;*/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 style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;*/test_*.py&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;src/ocarina/custom_errors/*&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;src/ocarina/custom_types/*&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;src/ocarina/ports/*&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;src/ocarina/opinionated/loggers/custom_types/*&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;src/**/*singleton.py&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;src/**/consts/**&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:#6272a4&#34;&gt;# Selenium adapter layer — exercised only against a real browser.&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;src/ocarina/infra/selenium/*&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;src/ocarina/dsl/testing/selenium/*&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;src/ocarina/opinionated/cli/selenium/*&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;src/ocarina/pom/selenium/muted.py&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:#6272a4&#34;&gt;# Playwright adapter layer — exercised only against a real browser.&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;src/ocarina/infra/playwright/*&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;src/ocarina/dsl/testing/playwright/*&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;src/ocarina/opinionated/cli/playwright/*&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;src/ocarina/pom/playwright/*&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;src/ocarina/opinionated/cli/phantoms.py&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:#6272a4&#34;&gt;# Opinionated loggers — only file_logger.py stays in scope.&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;src/ocarina/opinionated/loggers/create_matching_logger.py&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;src/ocarina/opinionated/loggers/muted_logger.py&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;src/ocarina/opinionated/loggers/print_logger.py&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;src/ocarina/opinionated/loggers/print_and_file_logger.py&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;src/ocarina/opinionated/loggers/utils/*&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:#6272a4&#34;&gt;# Traversed by cram tests&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;src/ocarina/opinionated/cli/store.py&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;src/ocarina/opinionated/cli/builder.py&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;h2 id=&#34;categories&#34;&gt;Categories&lt;a class=&#34;anchor&#34; href=&#34;#categories&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;1-obvious-exclusions&#34;&gt;1. Obvious exclusions&lt;a class=&#34;anchor&#34; href=&#34;#1-obvious-exclusions&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Pattern&lt;/th&gt;&#xA;          &lt;th&gt;Justification&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;*/__init__.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Empty modules (Python 3 often doesn&amp;rsquo;t need them)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;*/tests/*&lt;/code&gt;, &lt;code&gt;*/test_*.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Tests don&amp;rsquo;t test themselves&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h3 id=&#34;2-types--errors--ports--shapes&#34;&gt;2. Types / errors / ports / shapes&lt;a class=&#34;anchor&#34; href=&#34;#2-types--errors--ports--shapes&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Path&lt;/th&gt;&#xA;          &lt;th&gt;Justification&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;src/ocarina/custom_types/*&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Just &lt;code&gt;type X = ...&lt;/code&gt; and frozen dataclasses. No runtime logic.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/custom_errors/*&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Just Exception subclasses. No logic.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/ports/*&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;ABCs and Protocols. No behavior.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/opinionated/loggers/custom_types/*&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;id.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/**/consts/**&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Constants (&lt;code&gt;LOGGERS_CHOICES = (...)&lt;/code&gt;).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/**/*singleton.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Trivial Singleton wrappers.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h3 id=&#34;3-selenium-and-playwright-layers-e2e-only&#34;&gt;3. Selenium and Playwright layers (e2e only)&lt;a class=&#34;anchor&#34; href=&#34;#3-selenium-and-playwright-layers-e2e-only&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Path&lt;/th&gt;&#xA;          &lt;th&gt;Justification&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;src/ocarina/infra/selenium/*&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Real Selenium code (&lt;code&gt;Chrome()&lt;/code&gt;, &lt;code&gt;Firefox()&lt;/code&gt;). Only runs with a real browser.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/dsl/testing/selenium/*&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_selenium_test&lt;/code&gt;, &lt;code&gt;create_selenium_watcher&lt;/code&gt; (trivial factories on &lt;code&gt;Test&lt;/code&gt; / &lt;code&gt;Watcher&lt;/code&gt;).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/opinionated/cli/selenium/*&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_selenium_*_cli_store&lt;/code&gt; (reads &lt;code&gt;platform.system&lt;/code&gt;, instantiates).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/pom/selenium/muted.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Utility &lt;code&gt;MutedPOM&lt;/code&gt; (nearly empty).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/infra/playwright/*&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Real Playwright code + the &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;PlaywrightDriver&lt;/code&gt; actor&lt;/a&gt;. The end-to-end only proves out against a real browser.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/dsl/testing/playwright/*&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_playwright_test&lt;/code&gt;, &lt;code&gt;create_playwright_watcher&lt;/code&gt; (same trivial factories).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/opinionated/cli/playwright/*&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_playwright_cli_store&lt;/code&gt; (Playwright CLI).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;src/ocarina/pom/playwright/*&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;MutedPlaywrightPOM&lt;/code&gt; + &lt;code&gt;mixins&lt;/code&gt; (Null Object, nearly empty).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;These files are &lt;strong&gt;covered&lt;/strong&gt; by:&lt;/p&gt;</description>
    </item>
    <item>
      <title>04.08 — Allure &#43; composite action allure-history &#43; GH Pages deploy</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/08-allure-history/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/08-allure-history/</guid>
      <description>&lt;h1 id=&#34;0408allure--composite-action-allure-history--gh-pages-deploy&#34;&gt;04.08 — Allure + composite action &lt;code&gt;allure-history&lt;/code&gt; + GH Pages deploy&lt;a class=&#34;anchor&#34; href=&#34;#0408allure--composite-action-allure-history--gh-pages-deploy&#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;The Allure report gets generated on every CI, &lt;em&gt;archived&lt;/em&gt; on a dedicated Git branch, then &lt;em&gt;published&lt;/em&gt; on GitHub Pages. Live URL: &lt;a href=&#34;https://mojo-molotov.github.io/ocarina/allure-report/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;https://mojo-molotov.github.io/ocarina/allure-report/&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;pipeline&#34;&gt;Pipeline&lt;a class=&#34;anchor&#34; href=&#34;#pipeline&#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;│ main_ci.yml (push/PR main, or dispatch)                │&#xA;└──────────────────┬─────────────────────────────────────┘&#xA;                   ▼&#xA;   ┌───────────────────────────────────┐&#xA;   │ job : test                        │&#xA;   │  - matrix (ubuntu / windows × py) │&#xA;   │  - make test (cram + pytest)      │&#xA;   │  - upload-artifact allure-results │&#xA;   └───────────────┬───────────────────┘&#xA;                   ▼&#xA;   ┌───────────────────────────────────┐&#xA;   │ job : allure-history              │&#xA;   │  - download all allure-results    │&#xA;   │  - local composite action:        │&#xA;   │       allure-history/action.yml   │&#xA;   │  - upload-artifact allure-report  │&#xA;   └───────────────┬───────────────────┘&#xA;                   ▼&#xA;   ┌───────────────────────────────────┐&#xA;   │ job : deploy                      │&#xA;   │  - download allure-report         │&#xA;   │  - prepare pages-root/            │&#xA;   │  - actions/upload-pages-artifact  │&#xA;   │  - actions/deploy-pages           │&#xA;   └───────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;allure-history&#34;&gt;&lt;code&gt;allure-history&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#allure-history&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;.github/actions/allure-history/action.yml&lt;/code&gt;: composite action &lt;strong&gt;local&lt;/strong&gt; to the repo (&lt;code&gt;uses: ./.github/actions/allure-history&lt;/code&gt;).&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
