diff --git a/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java b/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java index 75b54f30..9730a9b5 100644 --- a/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java @@ -122,6 +122,7 @@ private HashMap isDefinedIndicator(String indicatorName, String int warningLimit = 0; int criticalLimit = 0; + if (limits != null) { okLimit = limits.getInt("ok"); warningLimit = limits.getInt("warning"); diff --git a/src/main/java/us/muit/fs/a4i/control/GDIStrategy.java b/src/main/java/us/muit/fs/a4i/control/GDIStrategy.java new file mode 100644 index 00000000..f9367aa8 --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/control/GDIStrategy.java @@ -0,0 +1,55 @@ +package us.muit.fs.a4i.control; + +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +public class GDIStrategy implements IndicatorStrategy { + + private static final String METRIC_TOTAL = "issues_total"; + private static final String METRIC_ETIQUETADOS = "issues_etiquetados"; + private static final String RESULT_NAME = "grado_documentacion_issues"; + + @Override + public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { + Optional> totalOpt = metrics.stream() + .filter(m -> METRIC_TOTAL.equals(m.getName())) + .findFirst(); + + Optional> etiquetadosOpt = metrics.stream() + .filter(m -> METRIC_ETIQUETADOS.equals(m.getName())) + .findFirst(); + + if (totalOpt.isEmpty() || etiquetadosOpt.isEmpty()) { + throw new NotAvailableMetricException("Faltan métricas necesarias para calcular el indicador."); + } + + double total = totalOpt.get().getValue(); + double etiquetados = etiquetadosOpt.get().getValue(); + + if (total == 0) { + throw new RuntimeException("El valor de 'issues_total' no puede ser cero."); + } + + double resultado = (etiquetados / total) * 100.0; + + try { + // Intentamos crear el ReportItem y si algo falla, envolvemos la excepción. + return new ReportItem.ReportItemBuilder<>(RESULT_NAME, resultado).build(); + } catch (ReportItemException e) { + // Envolvemos la ReportItemException en una RuntimeException + throw new RuntimeException("Error al crear el ReportItem: " + e.getMessage(), e); + } + } + + + @Override + public List requiredMetrics() { + return Arrays.asList(METRIC_TOTAL, METRIC_ETIQUETADOS); + } +} diff --git a/src/main/java/us/muit/fs/a4i/model/remote/ExtraccionMetricas.java b/src/main/java/us/muit/fs/a4i/model/remote/ExtraccionMetricas.java new file mode 100644 index 00000000..fa46906a --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/model/remote/ExtraccionMetricas.java @@ -0,0 +1,104 @@ +package us.muit.fs.a4i.model.remote; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import org.kohsuke.github.GHIssue; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; + +import us.muit.fs.a4i.exceptions.MetricException; +import us.muit.fs.a4i.model.entities.ReportItemI; +import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; +import us.muit.fs.a4i.model.entities.Report; +import us.muit.fs.a4i.model.entities.ReportI; + + +public class ExtraccionMetricas implements RemoteEnquirer { + + private static final List SUPPORTED_METRICS = Arrays.asList("totalIssues", "labeledIssues"); + + @Override + public ReportI buildReport(String entityId) { + try { + Report report = new Report(ReportI.ReportType.REPOSITORY, entityId); + + for (String metric : getAvailableMetrics()) { + ReportItemI item = getMetric(metric, entityId); + report.addMetric(item); + } + + return report; + + } catch (MetricException e) { + throw new RuntimeException("Error al construir el informe: " + e.getMessage(), e); + } + } + + @Override + public ReportItemI getMetric(String metricName, String entityId) throws MetricException { + if (!SUPPORTED_METRICS.contains(metricName)) { + throw new MetricException("Métrica no soportada: " + metricName); + } + + if (!entityId.contains("/")) { + entityId = entityId + "/Audit4Improve-API"; + } + + try { + GitHub github; + String token = System.getenv("GITHUB_PACKAGES"); + + if (token != null && !token.isEmpty()) { + github = new GitHubBuilder().withOAuthToken(token).build(); + } else { + github = GitHub.connectAnonymously(); + } + GHRepository repo = github.getRepository(entityId); + + int total = 0; + int etiquetados = 0; + + for (GHIssue issue : repo.getIssues(org.kohsuke.github.GHIssueState.OPEN)) { + if (!issue.isPullRequest()) { + total++; + if (!issue.getLabels().isEmpty()) { + etiquetados++; + } + } + } + + if (metricName.equals("totalIssues")) { + return new ReportItemBuilder("totalIssues", (double) total) + .source("GitHub") + .build(); + } else if (metricName.equals("labeledIssues")) { + return new ReportItemBuilder("labeledIssues", (double) etiquetados) + .source("GitHub") + .build(); + } else { + throw new MetricException("Métrica desconocida: " + metricName); + } + + } catch (IOException e) { + System.err.println("IOException: " + e.getMessage()); + throw new MetricException("Error al conectar con GitHub: " + e.getMessage()); + } catch (Exception e) { + System.err.println("Exception: " + e.getMessage()); + throw new MetricException("Error al construir ReportItem: " + e.getMessage()); + } + } + + @Override + public List getAvailableMetrics() { + return SUPPORTED_METRICS; + } + + @Override + public RemoteType getRemoteType() { + return RemoteType.GITHUB; + } + +} diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index 33cee922..f7f34308 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -90,6 +90,20 @@ "description": "Numero de issues abiertas", "unit": "issues" }, + { + "name": "issues_total", + "type": "java.lang.Double", + "description": "Número de issues totales", + "unit": "issues" + + }, + { + "name": "issues_etiquetados", + "type": "java.lang.Double", + "description": "Número de issues etiquetados", + "unit": "issues" + + }, { "name": "openProjects", "type": "java.lang.Integer", @@ -127,8 +141,8 @@ "unit": "issues" }, { - "name": "issues", - "type": "java.lang.Integer", + "name": "totalIssues", + "type": "java.lang.Double", "description": "Tareas totales", "unit": "issues" }, @@ -275,6 +289,17 @@ "critical": 25 } }, + { + "name": "grado_documentacion_issues", + "type": "java.lang.Double", + "description": "% Issues etiquetados frente a totales", + "unit": "%", + "limits": { + "ok": 71, + "warning": 51, + "critical": 31 + } + }, { "name": "developerPerfomance", "type": "java.lang.Double", @@ -298,6 +323,12 @@ "description": "Indicador de conformidad con las convenciones en el repo", "unit": "ratio" }, + { + "name": "labeledIssues", + "type": "java.lang.Double", + "description": "Número de issues con al menos una etiqueta", + "unit": "issues" + }, { "name": "teamsBalanceI", "type": "java.lang.Double", diff --git a/src/test/java/us/muit/fs/a4i/test/control/IndicatorStrategyTest.java b/src/test/java/us/muit/fs/a4i/test/control/IndicatorStrategyTest.java new file mode 100644 index 00000000..19b224e2 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/control/IndicatorStrategyTest.java @@ -0,0 +1,59 @@ +package us.muit.fs.a4i.test.control; + +import static org.junit.jupiter.api.Assertions.*; + +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; +import us.muit.fs.a4i.control.GDIStrategy; +import us.muit.fs.a4i.control.IndicatorStrategy; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class IndicatorStrategyTest { + + @Test + @DisplayName("Test correcto del cálculo GDI con métricas válidas") + void testCalcIndicator_OK() throws NotAvailableMetricException, ReportItemException { + IndicatorStrategy strategy = new GDIStrategy(); + + ReportItemI total = new ReportItem.ReportItemBuilder<>("issues_total", 10.0).build(); + ReportItemI etiquetados = new ReportItem.ReportItemBuilder<>("issues_etiquetados", 7.0).build(); + + List> metrics = Arrays.asList(total, etiquetados); + + ReportItemI result = strategy.calcIndicator(metrics); + assertEquals(70.0, result.getValue(), 0.001, "El cálculo del GDI es incorrecto"); + assertEquals("grado_documentacion_issues", result.getName(), "El nombre del resultado no es el esperado"); + } + + @Test + @DisplayName("Test de excepción cuando faltan métricas") + void testCalcIndicator_MissingMetrics() throws ReportItemException { + IndicatorStrategy strategy = new GDIStrategy(); + + ReportItemI etiquetados = new ReportItem.ReportItemBuilder<>("issues_etiquetados", 7.0).build(); + + List> metrics = List.of(etiquetados); + + assertThrows(NotAvailableMetricException.class, () -> { + strategy.calcIndicator(metrics); + }, "Se esperaba una excepción por falta de métricas"); + } + + @Test + @DisplayName("Test de requiredMetrics() devuelve métricas necesarias") + void testRequiredMetrics() { + IndicatorStrategy strategy = new GDIStrategy(); + List required = strategy.requiredMetrics(); + + assertTrue(required.contains("issues_total")); + assertTrue(required.contains("issues_etiquetados")); + assertEquals(2, required.size(), "Se esperaban exactamente dos métricas"); + } +} \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/RemoteEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/RemoteEnquirerTest.java new file mode 100644 index 00000000..46a909c7 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/RemoteEnquirerTest.java @@ -0,0 +1,86 @@ +package us.muit.fs.a4i.test.model.remote; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.logging.Logger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.exceptions.MetricException; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; +import us.muit.fs.a4i.model.remote.ExtraccionMetricas; +import us.muit.fs.a4i.model.remote.GitHubRepositoryEnquirer; +import us.muit.fs.a4i.model.remote.RemoteEnquirer; + +class RemoteEnquirerTest { + + private static Logger log = Logger.getLogger(RemoteEnquirerTest.class.getName()); + + private RemoteEnquirer enquirer = new ExtraccionMetricas(); // Asegúrate que esta clase existe y está implementada + + + // Test que certifica que se puede obtener la métrica "totalIssues" (el resultado no es nulo, + // el nombre de la métrica coincide al completo y el valor es de tipo numérico). + + @Test + void testGetTotalIssuesMetric() throws MetricException { + String repoId = "MIT-FS"; // Reemplazar con un repositorio válido de prueba + + ReportItemI metric = enquirer.getMetric("totalIssues", repoId); + assertNotNull(metric, "La métrica 'totalIssues' no debe ser null"); + assertEquals("totalIssues", metric.getName()); + assertTrue(metric.getValue() instanceof Number, "El valor debe ser numérico"); + + log.info("Total Issues: " + metric.getValue()); + } + + // Test que certifica que se puede obtener la métrica "labeledIssues" (el resultado no es nulo, + // el nombre de la métrica coincide al completo y el valor es de tipo numérico). + + @Test + void testGetLabeledIssuesMetric() throws MetricException { + String repoId = "MIT-FS"; + + ReportItemI metric = enquirer.getMetric("labeledIssues", repoId); + assertNotNull(metric, "La métrica 'labeledIssues' no debe ser null"); + assertEquals("labeledIssues", metric.getName()); + assertTrue(metric.getValue() instanceof Number, "El valor debe ser numérico"); + + log.info("Issues con etiquetas: " + metric.getValue()); + } + + + // Test que verifica que el objeto informa correctamente de las métricas que soporta (que + // la lista no es nula y que dentro de las posibilidades están "totalIssues" y "labeledIssues") + + @Test + void testGetAvailableMetrics() { + List metrics = enquirer.getAvailableMetrics(); + assertNotNull(metrics); + assertTrue(metrics.contains("totalIssues"), "Debe contener la métrica 'totalIssues'"); + assertTrue(metrics.contains("labeledIssues"), "Debe contener la métrica 'labeledIssues'"); + } + + // Test que intenta pedir una métrica que no existe, para comprobar que la excepción salta como se espera + + + @Test + void testInvalidMetricThrowsException() { + String repoId = "Isabel-Roman"; + + assertThrows(MetricException.class, () -> { + enquirer.getMetric("nonExistentMetric", repoId); + }); + } + + + // Test que verifica que el tipo de la clase está correctamente identificado como GITHUB + + @Test + void testGetRemoteType() { + assertEquals(RemoteEnquirer.RemoteType.GITHUB, enquirer.getRemoteType()); + } +}