Dev #6
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user