8.1 KiB
Sviluppo
Setup locale, pytest, ruff, e come estendere il client.
Setup
git clone https://192.168.0.183:8085/truetype/viandante.git
cd viandante
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
Lo sviluppo quotidiano è Python puro; nessun build system esterno
(setuptools è trasparente via pyproject.toml).
Test
pytest # 66+ test (telnet + ansi + deadsouls + mapper + logger)
pytest tests/test_logger.py -v # solo il logger (più verboso)
pytest -k "test_rolling_parser" # filtrato per nome
pytest --tb=short # traceback corti
I test usano pytest.ini_options = ["-q"] di default in pyproject.toml.
Cosa è coperto
| File | Copre |
|---|---|
test_ansi.py |
stripper ANSI + visible_len |
test_telnet.py |
IACParser (state machine completa) + TelnetConnection (mock asyncio) |
test_deadsouls.py |
parse_exits + RollingRoomParser scenario multi‑chunk |
test_mapper.py |
Mapper.visit, link, layout BFS, orphan placement, render |
test_logger.py |
EventLogger round‑trip JSONL, MD rendering per ogni kind, redazione password, idempotenza close(), lone‑ESC safety |
Cosa non è coperto
cli.pyend‑to‑end (richiede telnet server; il vero test è una smoke run contro il docker locale di Dead Souls).mapper._render_vertical_row(casi degeneri con celle non continue; non aggiunti perché non osservati in pratica).- Re‑entrata del logger in un worker thread (lock difensivo è documentato ma non testato).
Lint
ruff check src tests
ruff format src tests
Config in pyproject.toml:
[tool.ruff]
line-length = 100
target-version = "py310"
(Aggiungi regole strict select = ["E", "F", "I", "B", "UP"] se
vuoi enforcement.)
Smoke test end‑to‑end
Dead Souls docker su localhost:6666:
# 2 secondi, log JSONL + MD
LOGJSON=$(mktemp /tmp/mud-json.XXXXXX.jsonl)
LOGMD=$(mktemp /tmp/mud-md.XXXXXX.md)
timeout 2 python -m viandante --host localhost --port 6666 \
--log-json "$LOGJSON" --log-md "$LOGMD" </dev/null >/dev/null 2>&1 || true
wc -l "$LOGJSON" "$LOGMD"
head -3 "$LOGJSON"
head -8 "$LOGMD"
Aspettative: almeno 2‑3 righe JSONL (session_start, text, session_end)
e un header MD con host/port/user.
Come aggiungere un modulo
Esempio: nuovo modulo directions.py che assorbe EXIT_ALIASES da
deadsouls.py. Procedura:
- Crea
src/viandante/directions.pycon le costanti enormalize_exits(). - Aggiorna
deadsouls.py→ re‑import dadirectionsper back‑compat. - Aggiorna
gmcp.py,mapper.py,cli.py→ import dadirections. - Scrivi
tests/test_directions.py. - Aggiorna questo file (
docs/development.md) edocs/modules.md. pytest+ruff check.
Principi
- Foglia pura sopra orchestratore. Solo
cli.pypuò dipendere da più moduli. Mai il contrario. - No mutazione cross‑module. Ogni modulo possiede il suo stato.
- Mai dipendere dal payload GMCP da mapper o deadsouls. Sono moduli di testo puro.
- Mai import
clida moduli bassi. Soloclisa diprompt_toolkit.
Debugging tipico
| Sintomo | Tool |
|---|---|
| IAC parser perso un byte | pytest tests/test_telnet.py -v + aggiungere print in _byte() |
| GMCP module non riconosciuto | leggere gmcp.parse_gmcp con data=b"..." in REPL |
| Stanze non rilevate senza GMCP | stampare state.parser._buf dopo feed_text |
Mapper collassa in [mappa troppo larga] |
abbassare max_cols/max_rows in render(...) |
| Stanza GMCP arriva ma render non aggiorna | state.mapper.current_room punta ancora al vecchio? |
| Password finisce nel log | grep --log-md per hunter2/*** ratio |
| Sessione hung dopo quit | conn.close() chiamato? finally cleanup? |
Aggiungere un nuovo comando slash
- In
cli.py:_handle_local_command, aggiungi un ramoelif cmd == "foo":. - Se modifica stato → metti la logica prima del
if state.logger is not None: state.logger.local_cmd(...). - Se inoltra al MUD → usa
await conn.send_line("foo"). - Aggiungi voce nella tabella di
docs/usage.mde_print_help(). - Test: aggiungi in
tests/test_cli.py(futuro; ancora non esiste).
Versioning
__version__ in __init__.py (0.1.0 in v0.2). Bump:
- patch (0.1.x): bug fix, no nuovi flag.
- minor (0.x.0): nuovi flag, nuove feature senza break della CLI.
- major (x.0.0): break di
--log-*schema eventi o rename simboli pubblici dei moduli.
CI/CD — Gitea Actions
I workflow vivono in .gitea/workflows/:
ci.yml— ruff + matrix pytest su Python 3.10/3.11/3.12, trigger supushepull_requestversomain.concurrency.cancel-in-progressannulla i run sorpassati (utile per push consecutivi su PR).release.yml—gate(ruff + pytest) →build(python -m build, sdist + wheel) su tagv*. Gli artifactviandante-*.tar.gzeviandante-*.whlfiniscono nella pagina del job, scaricabili.
Stato operativit\u00e0: l'infrastruttura YAML \u00e8 committata e pushata
(v0.2.0 \u2192 b20f3c0f). Finch\u00e9 non registri un runner sul Gitea,
i job restano in queued e il badge [](...) rimane grigio.
Senza runner la pipeline non gira.
Setup del runner (una tantum)
Sulla macchina che eseguir\u00e0 i job serve Docker (l'immagine
ubuntu-22.04 parte in un container):
-
Genera il token di registrazione dalla UI:
http://192.168.0.183:8085/-/admin/runners \u2192 "Crea nuovo runner"(NON \u00e8 il token account, \u00e8 un one-shot scelto da te). -
Al primo avvio usa
--rm -itper vedere in chiaro eventuali errori di registrazione (token sbagliato, permessi su/var/run/docker.sock):docker run --rm -it --name act_runner \ -e GITEA_INSTANCE_URL=http://192.168.0.183:8085 \ -e GITEA_RUNNER_REGISTRATION_TOKEN=<TOKEN_DEL_PUNTO_1> \ -e GITEA_RUNNER_NAME=homelab-runner \ -v /var/run/docker.sock:/var/run/docker.sock \ gitea/act_runner:latest -
Quando vedi
Runner is now onlinenel log, sostituisci con la versione daemonizzata (-d,--restart unless-stopped). Unact_runnerDocker (gitea/act_runner:latest) di default pubblica le labelubuntu-latest,ubuntu-24.04,ubuntu-22.04; i workflow diviandanteusanoruns-on: ubuntu-22.04(in.gitea/workflows/*.yml) che le matcha — ogni job gira in un container pulito basato su Ubuntu 22.04 (Docker‑in‑Docker).
NB sul token: l'URL
/-/admin/runnersgenera un runner globale (visibile a tutti i repo del Gitea). Per limitare il runner a un solo repo, il token si legge da/-/settings/actions/runnersdi quel repo (la riga registrata appare nella sezione "Runners" del repo specifico, non nella pagina admin globale).
Egress verso github.com (possibile blocker)
Senza egress dal runner host a github.com, le action composite
(actions/checkout@v4, actions/setup-python@v5,
actions/upload-artifact@v4) non possono essere scaricate e il primo
step fallisce con failed to download action. Workaround:
-
Ridireziona il download delle action in
app.inidel server (chiaveDEFAULT_ACTIONS_URL, introdotta in Gitea 1.21+, defaulthttps://github.com):[actions] DEFAULT_ACTIONS_URL = https://<your-mirror-url>NB: nomi pi\u00f9 vecchi tipo
ENABLE_GITHUB_ACTIONS_MIRROR/ACTIONS_DEFAULT_BASE_URLnon esistono in 1.26 \u2014 usa soloDEFAULT_ACTIONS_URL. -
Oppure bundle locale: pre-popolare le action in un image di runner.
Note pratiche
- Cache
pip:cache: pip+cache-dependency-path: pyproject.toml. Cache key =python-version + hash(pyproject.toml), quindi 3.10/3.11/3.12 hanno cache separate (no cross-version poisoning). - Tag release pattern: trigger su
v*(v0.2.0,v0.3,v1.0.0-rc1). Workflow di release:git tag vX.Y.Z && git push origin vX.Y.Z. - Badge
[](...)in README.md \u00e8 hardcoded av0.2.0: aggiornalo a mano a ogni release (vedi checklist indocs/gitea.md).