02.10.04 — ActCounter + ThreadsBasedActCounter#

Compteur thread-local du nombre d’act exécutés par tentative. Permet de reporter au rapport « ce test a fait 17 steps avant d’échouer au step 18 ».

Interface#

# src/ocarina/infra/act_counter.py
class ActCounter:
    def get(self) -> int: ...
    def reset(self) -> None: ...
    def incr_act_call_count(self) -> None: ...

Implémentation par défaut#

# src/ocarina/opinionated/infra/act_counter.py
from threading import local

_thread_local = local()
_COUNTER_KEY: Final[str] = "ocarina_counter"


class ActCounter(_ActCounter):
    def get(self) -> int:
        return getattr(_thread_local, _COUNTER_KEY, 0)

    def reset(self) -> None:
        setattr(_thread_local, _COUNTER_KEY, 0)

    def incr_act_call_count(self) -> None:
        if not hasattr(_thread_local, _COUNTER_KEY):
            self.reset()
        setattr(_thread_local, _COUNTER_KEY, getattr(_thread_local, _COUNTER_KEY) + 1)
  • threading.local() : un namespace dont les attributs sont par thread. Chaque thread a sa propre valeur.
  • _COUNTER_KEY = "ocarina_counter" : la clé d’attribut.
  • Pas de lock : chaque thread n’écrit que sa propre valeur, donc pas de race condition.

Pourquoi thread-local plutôt qu’un compteur partagé + lock ?#

Ocarina est architecturé de sorte qu’un thread = un test. Et aucune envie d’introduire toute une state monad ou quoi, autant placer cette « impureté » (qui reste un “état”) ici. C’est tout.

Usage dans create_act#

def run_action() -> Result[TPOM]:
    try:
        if act_counter_effect:
            act_counter_effect()
        else:
            ThreadsBasedActCounter().incr_act_call_count()
        ...

À chaque act(pom, action) exécuté, le compteur du thread courant est incrémenté.

Usage dans TestFlow.run#

for attempt in range(1, max_attempts + 1):
    self._act_counter.reset()                           # ◄── reset AVANT chaque tentative
    with self._drivers_pool.acquire() as driver:
        outcome = self._executor.execute(...)
    ...

Et à la fin :

self._act_counter.reset()                               # ◄── reset après la dernière tentative
return last_result, last_steps_count, test.test_id

Usage dans TestExecutor.execute#

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

Le steps_count est lu à la fin d’une tentative. C’est ce qui apparaît dans le TestSuiteResult = (TestResult, steps_count, test_id), et dans le pretty_print_results (« ⫸ At step 3 » quand un test fail).

act_counter_effect — pourquoi un hook customisable#

Parce que c’est une impureté et qu’on veut garder un œil dessus en termes de testabilité. L’exposer ne coûte rien. C’est tout.

Le default reste « incrémente le compteur thread-local d’Ocarina », ce qui couvre 99% des cas.

Pourquoi deux fichiers (infra/ + opinionated/infra/)#

FichierRôle
src/ocarina/infra/act_counter.pyInterface ActCounter. Décrit le contrat. Utilisable par n’importe qui.
src/ocarina/opinionated/infra/act_counter.pyImplémentation ThreadsBasedActCounter. Choix opinionated d’utiliser threading.local.