Chapter 02.04 — Invariants

Chapter 02.04 — Invariants#

Ocarina’s sub-DSL for typed, composable invariants. Used everywhere: CLI, POMs, the custom_invariants/testing/ that check suite consistency before any test runs.

Plan#

#FileSubject
0101-validate-flow.mdThe full flow validate(value) → assert_that → execute → raise_if_invalid + the _ValidationChain class.
0202-assertions.mdCatalog of builtin assertions (is_str, is_email, is_positive, is_in, has_unique_elements, each, is_valid_filename, …).
0303-otherwise-any-of.md.otherwise(...) and the OR combination via _any_of.
0404-then-chain-of-validations.md.then(new_value) and chain_validations(...) for composition.
0505-business-vs-framework-validator.mdBusinessInvariantValidator vs FrameworkInvariantValidator
0606-invariant-errors.mdInvariantViolationError, DuplicatesError, AggregateInvariantViolationError.

Why the invariants DSL#

  1. Pre-execution guard. TestSuite.__init__ checks — before launching anything — that test names / IDs are unique, max_workers >= 1, etc.
  2. CLI validation. Every flag carries its validate=lambda chain: chain.assert_that(...) inside the CliStore. Errors aggregate into one message.
  3. POM-side validation. POM actions validate their parameters ("retries must be positive") via validate(retries, name="retries").assert_that(is_positive).execute().raise_if_invalid().

The Holy Book sums it up:

Chapter 02.05 — Orchestration

Chapter 02.05 — Orchestration#

Test → TestSuite → TestCampaign → TestCycle: how each level slots into the next, who owns parallelization, who owns retries, who decides skipping, and where the pre-execution invariants live.

Plan#

#FileSubject
0101-test.mdThe Test[Driver] class — spawn, pre/post fragments, skip.
0202-test-executor.mdTestExecutor — execution of one attempt, order: setup → watchers.start → chain → watchers.stop → teardown.
0303-test-flow-retries.mdTestFlow — retry loop (1 + max_retries), linear backoff, setup-failure handling.
0404-test-suite.mdTestSuite — parallelization with ThreadPoolExecutor, ID filtering, guardrails.
0505-saturation.mdWorker saturation: random cloning [COPY N]. Why and how.
0606-test-campaign.mdTestCampaign — sequence of suites, campaign_has_failed.
0707-test-cycle-modes.mdTestCycle — smoke + main, fail-fast vs wait-for-all modes, has_test_cycle_failed.
0808-filter-tests-by-ids.mdfilter_tests_by_ids — --only/--exclude, mutex, ignores unknown IDs.

Diagram#

            ┌────────────────────────────────────────────────┐
            │                  TestCycle                     │
            │                                                │
            │  ┌──────────────────────────────────────────┐  │
            │  │ smoke_tests_campaigns                    │  │ ◄── first, gate
            │  │  └─ TestCampaign                         │  │     mode: fail-fast |
            │  │      └─ TestSuite                        │  │           wait-for-all
            │  │          └─ Test                         │  │
            │  └──────────────────────────────────────────┘  │
            │                                                │
            │  ┌──────────────────────────────────────────┐  │
            │  │ campaigns (main)                         │  │ ◄── skipped if a
            │  │  └─ TestCampaign                         │  │     smoke fails
            │  │      └─ TestSuite                        │  │
            │  │          └─ Test                         │  │
            │  └──────────────────────────────────────────┘  │
            └────────────────────────────────────────────────┘

Who does what?#

LevelSingle responsibilityConcurrencyOut of scope
TestMetadata + spawn(driver, logger)noneexecution
TestExecutorOne attemptnoneretries, driver acquisition
TestFlowRetry loopnoneparallelization, aggregation
TestSuiteParallelization + saturation + ID filteringN threadsinter-suite sequence
TestCampaignSequence of suitessuite-levelsmoke vs main
TestCycleSmoke + main + modecampaign-levelbootstrap / post-exec plugins

The strict separation is deliberate. TestExecutor knows nothing about retries. TestFlow knows nothing about concurrency. TestSuite knows nothing about campaign-level aggregation. One axis of responsibility per class.

04.06 — Property-based testing (hypothesis)

04.06 — Property-based testing (hypothesis)#

A single file (test_invariants_properties.py), but the pattern is interesting: we generate inputs and verify universal properties of the invariant predicates.

The concept#

hypothesis auto-generates test cases that satisfy given constraints, then checks a property holds for every generated case. On failure, hypothesis does shrinking to find the smallest counter-example.

from hypothesis import given, strategies as st

@given(st.integers())
def test_is_positive_passes_on_positive(value):
    if value >= 0:
        is_positive(value)             # must not raise
    else:
        with pytest.raises(InvariantViolationError):
            is_positive(value)         # must raise

hypothesis generates integers (typically 100 by default), checks the property on each. Unexpected exception → fail. The shrinker finds the smallest failing case (0? -1? MIN_INT?).