[ADDED] Implementar calculadora COCOMO para estimar costos de proyectos

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mauro Rosero P. 2025-03-12 13:59:35 -05:00
parent 0efd6cf2b8
commit 2ba3bb68ec
Signed by: mrosero
GPG key ID: 83BD2A5F674B7E26

330
bin/cocomo.py Executable file
View file

@ -0,0 +1,330 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
COCOMO Calculator
Este script calcula el costo de un proyecto de software utilizando el modelo COCOMO,
basado en el conteo de líneas de código en un directorio dado.
Licencia: AGPL-3.0
Modified: 2025-03-12
"""
import os
import sys
import argparse
import logging
import fnmatch
from pathlib import Path
from math import ceil
# Configuración de logging
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING)
console_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
# Configuración global del logger
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[])
logger = logging.getLogger('cocomo')
logger.addHandler(console_handler)
# Variable para controlar si se muestra el resultado en la consola
SHOW_RESULTS = True
# Constantes para el modelo COCOMO básico
COCOMO_MODELS = {
# [a, b, c, d] - Coeficientes para Esfuerzo = a * (KLOC^b) y Tiempo = c * (Esfuerzo^d)
'organic': [2.4, 1.05, 2.5, 0.38], # Proyectos simples, pequeños equipos con experiencia
'semi-detached': [3.0, 1.12, 2.5, 0.35], # Proyectos intermedios
'embedded': [3.6, 1.20, 2.5, 0.32] # Proyectos complejos
}
# Extensiones de archivo a considerar por defecto
DEFAULT_EXTENSIONS = [
'.py', '.java', '.c', '.cpp', '.cc', '.cxx', '.h', '.hpp', '.js', '.ts',
'.html', '.css', '.php', '.rb', '.go', '.rs', '.swift', '.kt', '.scala',
'.sh', '.bash', '.pl', '.pm', '.lua', '.sql'
]
# Archivos a ignorar por defecto
DEFAULT_IGNORE_PATTERNS = [
'*.min.js', '*.min.css', '**/node_modules/**', '**/venv/**', '**/.git/**',
'**/__pycache__/**', '**/build/**', '**/dist/**', '**/bin/config/rates/**',
'**/bin/msg/**'
]
def count_lines_of_code(project_path, extensions=None, ignore_patterns=None):
"""
Cuenta las líneas de código no vacías en los archivos con las extensiones especificadas.
Excluye comentarios en archivos Python.
Args:
project_path: Ruta del directorio del proyecto
extensions: Lista de extensiones de archivo a considerar
ignore_patterns: Lista de patrones de archivo a ignorar
Returns:
Número total de líneas de código
"""
if extensions is None:
extensions = DEFAULT_EXTENSIONS
if ignore_patterns is None:
ignore_patterns = DEFAULT_IGNORE_PATTERNS
total_lines = 0
files_counted = 0
skipped_files = 0
logger.info(f"Contando líneas de código en: {project_path}")
logger.info(f"Extensiones: {extensions}")
for root, _, files in os.walk(project_path):
for file in files:
file_path = os.path.join(root, file)
rel_path = os.path.relpath(file_path, project_path)
# Verificar si el archivo debe ser ignorado
should_ignore = False
for pattern in ignore_patterns:
if fnmatch.fnmatch(rel_path, pattern):
should_ignore = True
break
if should_ignore:
logger.debug(f"Ignorando archivo: {rel_path}")
skipped_files += 1
continue
# Verificar si la extensión está en la lista
file_extension = os.path.splitext(file)[1].lower()
if file_extension not in extensions:
logger.debug(f"Extensión no considerada: {rel_path}")
skipped_files += 1
continue
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Contar líneas no vacías (excluyendo comentarios en Python)
if file_extension == '.py':
non_empty_lines = 0
in_multiline_comment = False
for line in lines:
line = line.strip()
# Saltar líneas vacías
if not line:
continue
# Manejar comentarios multilinea
if '"""' in line or "'''" in line:
# Si ya estamos en un comentario multilinea, verificamos si termina
if in_multiline_comment:
if line.endswith('"""') or line.endswith("'''"):
in_multiline_comment = False
continue
# Si encontramos el inicio de un comentario multilinea
elif line.startswith('"""') or line.startswith("'''"):
# Verificar si el comentario comienza y termina en la misma línea
if line.endswith('"""') or line.endswith("'''"):
if line.count('"""') == 2 or line.count("'''") == 2:
continue
else:
in_multiline_comment = True
continue
# Saltar si estamos en un comentario multilinea
if in_multiline_comment:
continue
# Saltar comentarios de una línea
if line.startswith('#'):
continue
# Contar línea de código válida
non_empty_lines += 1
total_lines += non_empty_lines
else:
# Para otros lenguajes, simplemente contamos líneas no vacías
non_empty_lines = sum(1 for line in lines if line.strip())
total_lines += non_empty_lines
files_counted += 1
if files_counted % 100 == 0:
logger.info(f"Procesados {files_counted} archivos, {total_lines} líneas contadas hasta ahora.")
except Exception as e:
logger.warning(f"Error al procesar archivo {file_path}: {e}")
skipped_files += 1
logger.info(f"Conteo completo. Procesados {files_counted} archivos, ignorados {skipped_files}.")
return total_lines
def estimate_cocomo(loc, model_type='organic', cost_per_pm=5000):
"""
Calcula las estimaciones COCOMO basadas en las líneas de código.
Args:
loc: Líneas de código
model_type: Tipo de modelo COCOMO ('organic', 'semi-detached', 'embedded')
cost_per_pm: Costo por persona-mes en USD
Returns:
Dictionary con los resultados del cálculo
"""
# Convertir LOC a KLOC (miles de líneas de código)
kloc = loc / 1000
# Obtener coeficientes del modelo
a, b, c, d = COCOMO_MODELS[model_type]
# Calcular esfuerzo en persona-mes
effort_pm = a * (kloc ** b)
# Calcular tiempo de desarrollo en meses
dev_time = c * (effort_pm ** d)
# Calcular personas requeridas (promedio)
avg_staff = effort_pm / dev_time
# Calcular costo total
total_cost = effort_pm * cost_per_pm
# Redondear valores para presentación
effort_pm = round(effort_pm, 2)
dev_time = round(dev_time, 2)
avg_staff = round(avg_staff, 2)
total_cost = round(total_cost, 2)
# Preparar resultados
results = {
'loc': loc,
'kloc': round(kloc, 2),
'model': model_type,
'effort_pm': effort_pm,
'dev_time': dev_time,
'avg_staff': avg_staff,
'cost_per_pm': cost_per_pm,
'total_cost': total_cost
}
return results
def show_result(message):
"""
Función para mostrar resultados en la consola.
Solo muestra mensajes si SHOW_RESULTS es True.
"""
if SHOW_RESULTS:
print(message)
def determine_cocomo_model(loc):
"""
Determina automáticamente el modelo COCOMO basado en el tamaño del proyecto.
Args:
loc: Líneas de código
Returns:
Tipo de modelo COCOMO ('organic', 'semi-detached', 'embedded')
"""
kloc = loc / 1000
if kloc < 50:
return 'organic' # Proyectos pequeños (menos de 50K líneas)
elif kloc < 300:
return 'semi-detached' # Proyectos medianos (entre 50K y 300K líneas)
else:
return 'embedded' # Proyectos grandes (más de 300K líneas)
def print_results(results):
"""
Imprime los resultados del cálculo COCOMO de manera formateada.
Args:
results: Dictionary con los resultados del cálculo COCOMO
"""
print("\n========== RESULTADOS DEL ANÁLISIS COCOMO ==========")
print(f"Proyecto: {results['project_path']}")
print(f"Líneas de código: {results['loc']} ({results['kloc']} KLOC)")
print(f"Modelo COCOMO: {results['model'].upper()}")
print("\n--- Estimaciones ---")
print(f"Esfuerzo: {results['effort_pm']} persona-meses")
print(f"Tiempo de desarrollo: {results['dev_time']} meses")
print(f"Personal promedio: {results['avg_staff']} personas")
print("\n--- Costos ---")
print(f"Costo por persona-mes: ${results['cost_per_pm']}")
print(f"Costo total estimado: ${results['total_cost']:,.2f}")
print("=====================================================\n")
if __name__ == "__main__":
# Parámetros de línea de comandos
parser = argparse.ArgumentParser(description='Calculadora COCOMO para estimar costos de proyectos de software')
parser.add_argument('--project', required=True, help='Ruta del directorio del proyecto a analizar')
parser.add_argument('--model', choices=['organic', 'semi-detached', 'embedded', 'auto'],
default='auto', help='Tipo de modelo COCOMO a utilizar (por defecto: auto)')
parser.add_argument('--cost', type=float, default=5000.0,
help='Costo por persona-mes en USD (por defecto: 5000)')
parser.add_argument('--ignore', action='append',
help='Patrones adicionales de archivos a ignorar')
parser.add_argument('--ext', action='append',
help='Extensiones adicionales de archivo a considerar')
parser.add_argument('-q', '--quiet', action='store_true',
help='No mostrar resultados detallados')
parser.add_argument('-v', '--verbose', action='store_true',
help='Mostrar información detallada del proceso')
args = parser.parse_args()
# Configurar nivel de log según parámetros
if args.verbose:
console_handler.setLevel(logging.INFO)
# Configurar si se muestran resultados
SHOW_RESULTS = not args.quiet
# Verificar que el directorio del proyecto existe
project_path = Path(args.project)
if not project_path.exists() or not project_path.is_dir():
logger.error(f"El directorio del proyecto no existe: {args.project}")
print(f"ERROR: El directorio del proyecto no existe: {args.project}")
sys.exit(1)
# Preparar extensiones y patrones de ignorar
extensions = DEFAULT_EXTENSIONS.copy()
if args.ext:
extensions.extend([ext if ext.startswith('.') else f'.{ext}' for ext in args.ext])
ignore_patterns = DEFAULT_IGNORE_PATTERNS.copy()
if args.ignore:
ignore_patterns.extend(args.ignore)
try:
# Contar líneas de código
loc = count_lines_of_code(project_path, extensions, ignore_patterns)
# Determinar el modelo COCOMO a utilizar
if args.model == 'auto':
model_type = determine_cocomo_model(loc)
logger.info(f"Modelo seleccionado automáticamente: {model_type}")
else:
model_type = args.model
# Calcular estimaciones COCOMO
results = estimate_cocomo(loc, model_type, args.cost)
# Añadir ruta del proyecto para la presentación
results['project_path'] = str(project_path)
# Mostrar resultados
print_results(results)
except Exception as e:
logger.error(f"Error durante el cálculo COCOMO: {e}")
print(f"ERROR: {e}")
sys.exit(1)