Skip to content

Commit d416d7f

Browse files
feat: Reestructuración completa de tests y mejoras en arquitectura
- Reorganización de estructura de tests con separación clara entre unit/integration/functional - Migración de herramientas de tools/ a tests/integration/ para mejor organización - Implementación de agedys_pure_manager.py con lógica de negocio pura y testeable - Mejoras en configuración de pytest con pytest.ini - Nuevos tests de integración para workflows completos de agedys y brass - Tests unitarios mejorados con mejor cobertura y separación de responsabilidades - Eliminación de archivos obsoletos y duplicados en tools/ - Mejoras en la estructura de directorios de tests para mejor mantenibilidad
1 parent 2fa4478 commit d416d7f

102 files changed

Lines changed: 3964 additions & 1091 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.coveragerc

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,11 @@
11
[run]
2-
source = src/
3-
omit =
2+
source = src
3+
omit =
4+
src/common/__init__.py
5+
src/common/logger.py
6+
src/common/user_adapter.py
47
*/tests/*
5-
*/test_*
6-
*/__pycache__/*
7-
*/venv/*
8-
*/.venv/*
98

109
[report]
11-
exclude_lines =
12-
pragma: no cover
13-
def __repr__
14-
if self.debug:
15-
if settings.DEBUG
16-
raise AssertionError
17-
raise NotImplementedError
18-
if 0:
19-
if __name__ == .__main__.:
20-
class .*\bProtocol\):
21-
@(abc\.)?abstractmethod
22-
23-
[html]
24-
directory = htmlcov
25-
title = Scripts Python - Coverage Report
10+
show_missing = True
11+
skip_covered = True

.github/workflows/python-ci.yml

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
name: Python CI
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
9+
concurrency:
10+
group: python-ci-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
smoke-test-runners:
15+
name: Smoke Runners
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 5
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v4
21+
- name: Set up Python 3.11
22+
uses: actions/setup-python@v5
23+
with:
24+
python-version: '3.11'
25+
# Using explicit cache step below instead of built-in cache for clarity
26+
- name: Cache pip
27+
uses: actions/cache@v3
28+
with:
29+
path: ~/.cache/pip
30+
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
31+
restore-keys: |
32+
${{ runner.os }}-pip-
33+
- name: Install minimal deps
34+
run: |
35+
python -m pip install --upgrade pip
36+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
37+
pip install pytest
38+
- name: Run smoke tests (runners)
39+
run: |
40+
pytest -q tests/test_runners_smoke.py
41+
42+
build-test:
43+
name: Test (matrix)
44+
runs-on: ubuntu-latest
45+
timeout-minutes: 20
46+
needs: smoke-test-runners
47+
strategy:
48+
fail-fast: false
49+
matrix:
50+
python-version: [ '3.9', '3.10', '3.11' ]
51+
52+
steps:
53+
- name: Checkout repository
54+
uses: actions/checkout@v4
55+
- name: Set up Python ${{ matrix.python-version }}
56+
uses: actions/setup-python@v5
57+
with:
58+
python-version: ${{ matrix.python-version }}
59+
# Using explicit cache step below instead of built-in cache for clarity
60+
61+
- name: Cache pip
62+
uses: actions/cache@v3
63+
with:
64+
path: ~/.cache/pip
65+
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
66+
restore-keys: |
67+
${{ runner.os }}-pip-
68+
69+
- name: Install dependencies (project + test + coverage)
70+
run: |
71+
python -m pip install --upgrade pip
72+
if [ -f requirements.txt ]; then pip install -r requirements.txt coverage pytest; else pip install coverage pytest; fi
73+
74+
- name: Run tests with coverage (pytest)
75+
env:
76+
PYTHONWARNINGS: ignore::DeprecationWarning
77+
run: |
78+
coverage run -m pytest
79+
coverage json -o coverage.json
80+
81+
- name: Coverage summary
82+
run: |
83+
coverage report -m --fail-under=85
84+
85+
- name: Generate coverage artifacts (XML + HTML)
86+
run: |
87+
coverage xml
88+
coverage html
89+
# Ensure json already generated; if missing (fallback) generate now
90+
if [ ! -f coverage.json ]; then coverage json -o coverage.json; fi
91+
92+
- name: Upload coverage XML
93+
uses: actions/upload-artifact@v4
94+
with:
95+
name: coverage-xml-${{ matrix.python-version }}
96+
path: coverage.xml
97+
if-no-files-found: error
98+
- name: Upload coverage HTML report
99+
uses: actions/upload-artifact@v4
100+
with:
101+
name: coverage-report-html
102+
path: htmlcov
103+
if-no-files-found: error
104+
105+
- name: Publish Coverage Summary
106+
run: |
107+
echo "Parsing coverage.json for total percent..."
108+
if [ ! -f coverage.json ]; then echo 'coverage.json missing'; exit 1; fi
109+
total=$(python -c "import json; d=json.load(open('coverage.json')); t=d.get('totals',{}); p=t.get('percent_covered') or t.get('percent_covered_display') or 0.0; print(f'{p:.2f}')")
110+
echo "Total coverage: $total%"
111+
{
112+
echo '### 📊 Resumen de Cobertura de Tests'
113+
echo ''
114+
echo "- **Cobertura Total:** ${total}%"
115+
echo ''
116+
echo '> Generado automáticamente a partir de coverage.json'
117+
} >> "$GITHUB_STEP_SUMMARY"
118+
119+
- name: Upload coverage to Codecov
120+
uses: codecov/codecov-action@v3
121+
with:
122+
files: coverage.xml
123+
flags: py${{ matrix.python-version }}
124+
fail_ci_if_error: true
125+
verbose: true
126+
token: ${{ secrets.CODECOV_TOKEN }}

README.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Sistema de Gestión de Tareas Empresariales
22

3+
[![CI](https://github.com/ardelperal/scripts-python/actions/workflows/python-ci.yml/badge.svg?branch=main)](https://github.com/ardelperal/scripts-python/actions/workflows/python-ci.yml)
4+
[![Coverage](https://codecov.io/gh/ardelperal/scripts-python/branch/main/graph/badge.svg)](https://codecov.io/gh/ardelperal/scripts-python)
5+
36
Sistema de **monitoreo continuo** para la gestión automatizada de tareas empresariales desarrollado en Python. El objetivo principal es ejecutar el script maestro `run_master.py` que funciona como un **daemon de producción** que monitorea y ejecuta automáticamente todos los módulos del sistema según horarios específicos.
47

58
## 🎯 Objetivo Principal
@@ -85,6 +88,47 @@ class MiTarea(TareaDiaria):
8588
registry = TaskRegistry(extra_daily=[MiTarea()])
8689
```
8790

91+
### Helper de Ejecución Unificada
92+
93+
Para reducir boilerplate en los `run_*.py`, todas las tareas se ejecutan mediante `execute_task_with_standard_boilerplate` (`common.utils`).
94+
95+
Características:
96+
* Logging estándar con fichero dedicado `logs/<tarea>.log`.
97+
* Banners `=== INICIO TAREA X ===` / `=== FIN TAREA X ===`.
98+
* Modos soportados: normal, `--force` (ignora planificación y NO marca completada) y `--dry-run` (sólo evalúa planificación).
99+
* Detección automática del método de lógica: `execute_specific_logic` > `execute_logic` > `execute`.
100+
* Invoca `initialize()` si existe antes de la lógica.
101+
* Marca completada sólo en tareas diarias exitosas (y no en `--force`).
102+
103+
Ejemplo de runner minimalista:
104+
105+
```python
106+
import sys
107+
from common.utils import execute_task_with_standard_boilerplate
108+
from correos.correos_task import CorreosTask
109+
110+
def main():
111+
task = CorreosTask()
112+
code = execute_task_with_standard_boilerplate("CORREOS", task_obj=task)
113+
sys.exit(code)
114+
115+
if __name__ == "__main__":
116+
main()
117+
```
118+
119+
Para lógica puntual sin clase se puede usar `custom_logic=callable`, pero se recomienda migrar a clases `TareaDiaria` / `TareaContinua` para uniformidad y testabilidad.
120+
121+
#### Añadir una nueva tarea
122+
1. Crear clase `TareaDiaria` o `TareaContinua` con `execute_specific_logic`.
123+
2. Registrar en `TaskRegistry` o pasar como `extra_*`.
124+
3. Crear `run_<tarea>.py` que sólo instancie y llame al helper.
125+
4. Añadir tests (mock de planificación y lógica).
126+
127+
#### ensure_project_root_in_path
128+
129+
Los runners llaman a `ensure_project_root_in_path()` (en `common.utils`) para insertar `src` en `sys.path` de forma idempotente, eliminando bloques repetidos de manipulación manual.
130+
131+
88132
### Acceso unificado a BD
89133

90134
```python
@@ -150,6 +194,8 @@ El sistema ajusta automáticamente los tiempos de espera entre ciclos según el
150194
- [Testing](#testing)
151195
- [Variables de Entorno Principales](#variables-de-entorno-principales)
152196
- [Arquitectura](#arquitectura)
197+
- [Arquitectura](#arquitectura)
198+
- [Arquitectura de Tareas](#arquitectura-de-tareas)
153199

154200
## Estructura del Proyecto
155201

@@ -1494,6 +1540,113 @@ docker-compose down -v
14941540

14951541
## Arquitectura
14961542

1543+
### Arquitectura de Tareas
1544+
1545+
Esta sección describe cómo se estructuran y colaboran los componentes que permiten ejecutar cada módulo de negocio de forma consistente, testeable y extensible.
1546+
1547+
#### 1. Componentes Principales
1548+
1549+
| Componente | Responsabilidad | Código típico |
1550+
|------------|-----------------|---------------|
1551+
| Script Runner (`scripts/run_x.py`) | Punto de entrada ejecutable: parsea argumentos CLI, inicializa logging y delega en la Task | `scripts/run_no_conformidades.py` |
1552+
| Task (`BaseTask`, `TareaDiaria`, `TareaContinua`) | Orquestación de la lógica: decide si ejecutar, encapsula medición, logging estructurado y control de errores | `src/no_conformidades/no_conformidades_task.py` |
1553+
| Manager | Lógica de dominio y acceso a datos (queries, composición de datos, generación de HTML) | `no_conformidades_manager.py` / `*_manager.py` |
1554+
| TaskRegistry | Registro central de instancias de tareas para el script maestro | `common/task_registry.py` |
1555+
| Master Runner (`run_master.py`) | Bucle continuo que consulta el `TaskRegistry` y lanza tareas según frecuencia / tipo | `scripts/run_master.py` |
1556+
1557+
Separar estas capas reduce acoplamiento: los runners quedan triviales, las Tasks son testeables aislando sus métodos de decisión y ejecución con mocks, y los Managers concentran la lógica SQL / dominio reutilizable.
1558+
1559+
#### 2. Flujo General (Runner Individual)
1560+
1561+
```
1562+
parse_args()
1563+
setup_logging()
1564+
with Task() as task:
1565+
if args.force_flags:
1566+
task.ejecutar_forzado(sub-selección)
1567+
elif task.debe_ejecutarse():
1568+
task.ejecutar()
1569+
else:
1570+
log("skip")
1571+
```
1572+
1573+
La Task maneja internamente:
1574+
1. Registro de inicio (`event=task_start`).
1575+
2. Llamada a `execute_specific_logic()` (implementación concreta).
1576+
3. Marcado de completitud (`marcar_como_completada()`) sólo si la ejecución fue efectiva.
1577+
4. Registro de fin (`event=task_end`, `exit_code`).
1578+
5. Captura y log estructurado de excepciones sin comprometer el proceso principal.
1579+
1580+
#### 3. Flujo General (Master Runner)
1581+
1582+
1. Crea / reutiliza instancia de `TaskRegistry`.
1583+
2. Obtiene listas: `get_daily_tasks()` y `get_continuous_tasks()`.
1584+
3. Para cada tarea diaria: evalúa `debe_ejecutarse()` (frecuencia + horario + festivos) antes de lanzar.
1585+
4. Para cada tarea continua: se ejecuta en cada ciclo.
1586+
5. Aplica timeouts y registra resultados agregados para observabilidad.
1587+
1588+
#### 4. Contrato Simplificado de una Task
1589+
1590+
| Método | Propósito |
1591+
|--------|-----------|
1592+
| `debe_ejecutarse()` | Decide si corresponde ejecutar (diarias) |
1593+
| `execute_specific_logic()` | Lógica principal; devuelve bool éxito |
1594+
| `marcar_como_completada()` | Actualiza estado persistente (última ejecución) |
1595+
1596+
Errores lanzados en `execute_specific_logic()` se capturan en el wrapper de `BaseTask` para asegurar logging uniforme y evitar caída del ciclo maestro.
1597+
1598+
#### 5. Caso Específico: `NoConformidadesTask`
1599+
1600+
La tarea combina dos sub-tareas independientes: Calidad y Técnica. Para maximizar testabilidad se dividió en métodos discretos:
1601+
1602+
| Método | Rol |
1603+
|--------|-----|
1604+
| `debe_ejecutar_tarea_calidad()` | Evalúa si hay NC de calidad que justifiquen envío |
1605+
| `debe_ejecutar_tarea_tecnica()` | Evalúa si hay AR técnicas pendientes |
1606+
| `ejecutar_logica_calidad()` | Construye datos + HTML y registra envío (usa `NoConformidadesManagerPure`) |
1607+
| `ejecutar_logica_tecnica()` | Agrega datos técnicos por usuario mediante `get_technical_report_data_for_user()` |
1608+
| `execute_specific_logic()` | Orquesta decisiones, ejecuta subtareas y consolida resultado (éxito parcial permitido) |
1609+
1610+
Características clave:
1611+
* Separación de decisión vs ejecución -> tests unitarios rápidos (mocks sobre cada rama).
1612+
* Agregación técnica: una sola llamada por técnico en vez de 3 queries separadas (eficiencia y menor riesgo de inconsistencia temporal).
1613+
* Tolerancia a fallos: excepción en una sub-tarea no detiene la otra; se reporta resultado combinado.
1614+
* Flags de forzado (`--force-calidad`, `--force-tecnica`, `--force-all`) saltan las evaluaciones de `debe_ejecutar_*`.
1615+
1616+
Secuencia simplificada (técnica + calidad):
1617+
1618+
```
1619+
execute_specific_logic():
1620+
resultados = []
1621+
if forzar_calidad or debe_ejecutar_tarea_calidad():
1622+
try: resultados.append(ejecutar_logica_calidad())
1623+
except Exception: log(error)
1624+
if forzar_tecnica or debe_ejecutar_tarea_tecnica():
1625+
try: resultados.append(ejecutar_logica_tecnica())
1626+
except Exception: log(error)
1627+
return any(resultados) # éxito si al menos una rama hizo trabajo
1628+
```
1629+
1630+
#### 6. Beneficios de la Arquitectura de Tareas
1631+
1632+
| Beneficio | Explicación |
1633+
|-----------|-------------|
1634+
| Testabilidad | Métodos pequeños permiten mocks específicos y alta cobertura |
1635+
| Observabilidad | Eventos start/end homogéneos y exit codes previsibles |
1636+
| Evolutividad | Añadir una nueva Task sólo requiere implementarla y registrarla |
1637+
| Aislamiento de fallos | Una Task con error no compromete el ciclo maestro |
1638+
| Reutilización | Managers compartidos entre múltiples Tasks o runners futuros |
1639+
| Rendimiento | Reducción de queries duplicadas y posibilidad futura de caching |
1640+
1641+
#### 7. Próximos Mejoras Potenciales
1642+
1643+
* Persistir métricas (duración, número de registros procesados) para dashboards.
1644+
* Sistema de descubrimiento dinámico de Tasks (entry points / plugin folder).
1645+
* Instrumentación opcional (trazas / spans) para tareas de larga duración.
1646+
* Caching de resultados intermedios entre subtareas (cuando comparten dataset base).
1647+
1648+
---
1649+
14971650
### Módulos Comunes (`src/common/`)
14981651

14991652
- **config.py**: Gestión centralizada de configuración

pytest.ini

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
; Pytest configuration for test discovery after directory restructuring
2+
[pytest]
3+
testpaths = tests
4+
python_files = test_*.py
5+
python_classes = Test*
6+
python_functions = test_*
7+
addopts = -ra --strict-markers
8+
filterwarnings =
9+
ignore::DeprecationWarning
10+
pythonpath =
11+
.
12+
src
13+
markers =
14+
unit: tests unitarios
15+
integration: tests de integracion
16+
slow: tests lentos

0 commit comments

Comments
 (0)