Dev #6

Merged
w-vandal merged 5 commits from dev into main 2026-08-30 13:13:36 +00:00
5 changed files with 133 additions and 47 deletions
Showing only changes of commit bb84b7b377 - Show all commits
+34 -1
View File
@@ -2,7 +2,14 @@ name: Build and deploy
on:
push:
branches: [main]
branches: [main, dev]
# Le job "test" tourne sur CHAQUE push (main et dev) : jusqu'ici aucune
# étape de CI n'exécutait la suite de tests, rien n'empêchait un commit
# cassé d'atteindre la production (voir l'audit qualité de la Phase 0 du
# plan). "build-and-push"/"deploy", eux, restent réservés à main (via le
# filtre "if" sur gitea.ref) — un push sur dev ne doit jamais redéployer
# la prod, seulement faire tourner les tests.
# Secrets à configurer dans Gitea (Paramètres du dépôt > Actions > Secrets) :
# REGISTRY_HOST adresse du registre d'images (ex: gitea.exemple.com)
@@ -19,7 +26,32 @@ on:
# github.com / du miroir gitea.com depuis le runner self-hosted.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Conteneurs officiels plutôt que actions/setup-python|setup-node du
# marketplace — même logique que le reste de ce fichier (éviter une
# dépendance à la disponibilité de github.com/du miroir gitea.com
# depuis le runner self-hosted), et le runner n'a pas besoin
# d'installer quoi que ce soit d'autre que Docker, déjà requis pour
# build-and-push.
- name: Tests Python (pytest)
run: |
docker run --rm -v "$PWD":/app -w /app python:3.13-slim sh -c "
pip install --quiet -r requirements-dev.txt &&
python -m pytest tests/ -q
"
- name: Tests JS (node:test — logique pure de static/js/play/, voir le plan de modularisation)
run: |
docker run --rm -v "$PWD":/app -w /app node:20-slim \
node --test static/js/play/__tests__/*.test.js
build-and-push:
needs: test
if: gitea.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -41,6 +73,7 @@ jobs:
deploy:
needs: build-and-push
if: gitea.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+5 -23
View File
@@ -1,5 +1,7 @@
import db
from .compute_operation import compute_new_value
def apply_data_action(slug, action):
"""Exécute au moment du clic (mode jouable) une action "modifier_donnee" :
@@ -28,29 +30,9 @@ def apply_data_action(slug, action):
col = col + "_id"
current = row.get(col)
if operation == "definir_texte":
new_value = raw_value or ""
elif operation == "definir_nombre":
try:
new_value = float(raw_value) if field_def["type"] == "nombre_decimal" else int(float(raw_value))
except (TypeError, ValueError):
new_value = 0
elif operation == "definir_bool_vrai":
new_value = 1
elif operation == "definir_bool_faux":
new_value = 0
elif operation == "basculer_bool":
new_value = 0 if current else 1
elif operation in ("incrementer", "decrementer"):
try:
amount = float(raw_value) if raw_value not in (None, "") else 0
except (TypeError, ValueError):
amount = 0
if operation == "decrementer":
amount = -amount
result = (current or 0) + amount
new_value = result if field_def["type"] == "nombre_decimal" else int(result)
else:
try:
new_value = compute_new_value(operation, current, raw_value, field_def["type"] == "nombre_decimal")
except ValueError:
return False
new_value = _clamp_to_field_bounds(new_value, field_def)
+5 -23
View File
@@ -1,5 +1,7 @@
import db
from .compute_operation import compute_new_value
def apply_variable_action(slug, action):
"""Exécute au moment du clic (mode jouable) une action
@@ -25,29 +27,9 @@ def apply_variable_action(slug, action):
elif var_type == "booleen":
current = 1 if str(current).strip().lower() in ("1", "true", "vrai") else 0
if operation == "definir_texte":
new_value = raw_value or ""
elif operation == "definir_nombre":
try:
new_value = float(raw_value) if var_type == "nombre_decimal" else int(float(raw_value))
except (TypeError, ValueError):
new_value = 0
elif operation == "definir_bool_vrai":
new_value = 1
elif operation == "definir_bool_faux":
new_value = 0
elif operation == "basculer_bool":
new_value = 0 if current else 1
elif operation in ("incrementer", "decrementer"):
try:
amount = float(raw_value) if raw_value not in (None, "") else 0
except (TypeError, ValueError):
amount = 0
if operation == "decrementer":
amount = -amount
result = (current or 0) + amount
new_value = result if var_type == "nombre_decimal" else int(result)
else:
try:
new_value = compute_new_value(operation, current, raw_value, var_type == "nombre_decimal")
except ValueError:
return False
return db.update_global_variable_value(slug, var_name, new_value)
+35
View File
@@ -0,0 +1,35 @@
def compute_new_value(operation, current, raw_value, is_decimal):
"""Calcule la nouvelle valeur d'un champ d'objet ou d'une variable
globale pour une opération de DATA_OPERATIONS (voir
screens/labels/data_operations.py) — logique partagée par
apply_data_action.py (champ d'objet) et apply_variable_action.py
(variable globale), qui ne diffèrent que par l'origine de `current`
et la façon dont le résultat est persisté. `is_decimal` : True pour
un champ/une variable de type "nombre_decimal" (résultat numérique
gardé en flottant), False sinon (résultat toujours entier).
Lève ValueError si `operation` n'est pas reconnue — à l'appelant de
la traduire en son propre échec (ex. `return False`)."""
if operation == "definir_texte":
return raw_value or ""
if operation == "definir_nombre":
try:
return float(raw_value) if is_decimal else int(float(raw_value))
except (TypeError, ValueError):
return 0
if operation == "definir_bool_vrai":
return 1
if operation == "definir_bool_faux":
return 0
if operation == "basculer_bool":
return 0 if current else 1
if operation in ("incrementer", "decrementer"):
try:
amount = float(raw_value) if raw_value not in (None, "") else 0
except (TypeError, ValueError):
amount = 0
if operation == "decrementer":
amount = -amount
result = (current or 0) + amount
return result if is_decimal else int(result)
raise ValueError(f"opération inconnue : {operation!r}")
+54
View File
@@ -0,0 +1,54 @@
"""Tests unitaires de compute_new_value (screens/data_actions/compute_operation.py)
— logique partagée par apply_data_action.py (champ d'objet) et
apply_variable_action.py (variable globale), factorisée pour ne plus être
dupliquée dans les deux fichiers avant que la Phase 2 (hasard/opérations
mathématiques) n'y ajoute 6 nouvelles opérations. Verrouille le
comportement des 7 opérations existantes."""
import pytest
from screens.data_actions.compute_operation import compute_new_value
def test_definir_texte():
assert compute_new_value("definir_texte", "ancien", "nouveau", False) == "nouveau"
assert compute_new_value("definir_texte", "ancien", "", False) == ""
assert compute_new_value("definir_texte", "ancien", None, False) == ""
def test_definir_nombre_entier_et_decimal():
assert compute_new_value("definir_nombre", 0, "5", False) == 5
assert isinstance(compute_new_value("definir_nombre", 0, "5", False), int)
assert compute_new_value("definir_nombre", 0, "5.7", True) == 5.7
assert compute_new_value("definir_nombre", 0, "pas un nombre", False) == 0
def test_definir_bool():
assert compute_new_value("definir_bool_vrai", 0, None, False) == 1
assert compute_new_value("definir_bool_faux", 1, None, False) == 0
def test_basculer_bool():
assert compute_new_value("basculer_bool", 0, None, False) == 1
assert compute_new_value("basculer_bool", 1, None, False) == 0
assert compute_new_value("basculer_bool", None, None, False) == 1
def test_incrementer_decrementer_entier():
assert compute_new_value("incrementer", 10, "5", False) == 15
assert compute_new_value("decrementer", 10, "5", False) == 5
assert compute_new_value("incrementer", None, "5", False) == 5
def test_incrementer_decimal_garde_les_decimales():
result = compute_new_value("incrementer", 10.0, "0.5", True)
assert result == 10.5
assert isinstance(result, float)
def test_incrementer_valeur_invalide_ne_change_rien():
assert compute_new_value("incrementer", 10, "pas un nombre", False) == 10
def test_operation_inconnue_leve_value_error():
with pytest.raises(ValueError):
compute_new_value("multiplier", 10, "2", False)