From f255ca92757868548b96b3017979f8b88c42beba Mon Sep 17 00:00:00 2001 From: Mig Date: Wed, 20 Apr 2022 11:37:01 +0200 Subject: [PATCH 001/100] Prueba 1 --- Practica3testpush.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 Practica3testpush.txt diff --git a/Practica3testpush.txt b/Practica3testpush.txt new file mode 100644 index 00000000..b8b4a4e2 --- /dev/null +++ b/Practica3testpush.txt @@ -0,0 +1 @@ +hola \ No newline at end of file From 105b07ee9e584cd81eb6698050f5a8ae84f010c2 Mon Sep 17 00:00:00 2001 From: Mig Date: Sun, 8 May 2022 20:13:07 +0200 Subject: [PATCH 002/100] Fallo en excel report manager linea 222 --- .../a4i/persistence/ExcelReportManager.java | 23 +++++++++++++++---- .../a4i/persistence/PersistenceManager.java | 2 +- .../model/entities/MetricBuilderTest.java | 7 +++--- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java index f460c2ac..d49c473a 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java @@ -19,7 +19,9 @@ import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.sl.usermodel.Sheet; import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.WorkbookFactory; @@ -140,7 +142,7 @@ protected HSSFSheet getCleanSheet() throws EncryptedDocumentException, IOExcepti * Guarda en un hoja limpia con el nombre del id del informe todas las métricas y los indicadores que incluya */ @Override - public void saveReport() throws ReportNotDefinedException{ + public void saveReport(ReportI report) throws ReportNotDefinedException{ log.info("Guardando informe"); if(report==null) { throw new ReportNotDefinedException(); @@ -185,7 +187,13 @@ private void persistMetric(Metric metric) { Row row=sheet.createRow(rowIndex); log.info("Indice de fila nueva "+rowIndex); int cellIndex=0; - //Aquí debería incorporar el formato de fuente en las celdas + // Aquí debería incorporar el formato de fuente en las celdas + // docs sacados de aquí https://www.javatpoint.com/apache-poi-excel-font + // https://www.e-iceblue.com/Tutorials/Java/Spire.XLS-for-Java/Program-Guide/Cells/Apply-Fonts-in-Excel-in-Java.html + + CellStyle style = wb.createCellStyle(); + style.setFont((Font) formater.getMetricFont()); + row.createCell(cellIndex++).setCellValue(metric.getName()); row.createCell(cellIndex++).setCellValue(metric.getValue().toString()); row.createCell(cellIndex++).setCellValue(metric.getUnit()); @@ -205,12 +213,19 @@ private void persistIndicator(Indicator indicator) { Row row=sheet.createRow(rowIndex); log.info("Indice de fila nueva "+rowIndex); int cellIndex=0; + //Aquí debería indicar el formato de fuente en las celdas, que dependerá del estado del índice + + CellStyle style = wb.createCellStyle(); + // No existe getState??? + // solo hay que solucionar esto + style.setFont((Font) formater.getIndicatorFont(indicator.getState())); + row.createCell(cellIndex++).setCellValue(indicator.getName()); row.createCell(cellIndex++).setCellValue(indicator.getValue().toString()); - + row.createCell(cellIndex++).setCellValue(indicator.getUnit()); row.createCell(cellIndex++).setCellValue(indicator.getDescription()); - + row.createCell(cellIndex++).setCellValue(indicator.getSource()); row.createCell(cellIndex).setCellValue(indicator.getDate().toString()); log.info("Indice de celda final"+cellIndex); diff --git a/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java b/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java index 49b70c64..d239499e 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java @@ -28,7 +28,7 @@ public interface PersistenceManager { *

Persiste el informe

* @throws ReportNotDefinedException Si no se estableció el informe lanzará error */ - void saveReport() throws ReportNotDefinedException; + void saveReport(ReportI report) throws ReportNotDefinedException; /** *

Borra el informe

* diff --git a/src/test/java/us/muit/fs/a4i/test/model/entities/MetricBuilderTest.java b/src/test/java/us/muit/fs/a4i/test/model/entities/MetricBuilderTest.java index 5148aaf8..379e4a50 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/entities/MetricBuilderTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/entities/MetricBuilderTest.java @@ -78,11 +78,13 @@ void testMetricBuilder() { //Comenzamos probando el caso más sencillo, la métrica existe y el tipo es correcto MetricBuilder underTest = null; +// assertEquals("watchers", "gadsf", "El nombre establecido no es correcto"); try { underTest = new MetricBuilder("watchers", 33); } catch (MetricException e) { - fail("No debería haber saltado esta excepción"); e.printStackTrace(); + fail("No debería haber saltado esta excepción"); + } Metric newMetric = underTest.build(); log.info("Métrica creada "+newMetric.toString()); @@ -153,8 +155,7 @@ void testSource() { underTest.source("GitHub"); Metric newMetric = underTest.build(); log.info("Métrica creada "+newMetric.toString()); - assertEquals("GitHub",newMetric.getSource(),"Source no tiene el valor esperado"); - + assertEquals("GitHub",newMetric.getSource(),"Source no tiene el valor esperado"); } /** From b2b4209519d439aebf6fdc5506ff7937c61ae3a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Tue, 10 May 2022 11:35:23 +0200 Subject: [PATCH 003/100] =?UTF-8?q?Preparaci=C3=B3n=20pr=C3=A1ctica=20grad?= =?UTF-8?q?le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build.gradle modificado --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 396b277b..876e5931 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ dependencies { // https://mvnrepository.com/artifact/org.kohsuke/github-api //JAVADOC: https://github-api.kohsuke.org/apidocs/index.html - implementation group: 'org.kohsuke', name: 'github-api', version: '1.301' + api 'org.kohsuke:github-api:1.301' //Para la persistencia de informes usaremos la api apachepoi // https://mvnrepository.com/artifact/org.apache.poi/poi //JAVADOC: https://poi.apache.org/apidocs/5.0/ @@ -87,9 +87,9 @@ dependencies { //JAVADOC: https://javadoc.io/doc/org.mockito/mockito-junit-jupiter/latest/index.html testImplementation 'org.mockito:mockito-junit-jupiter:4.3.1' - testImplementation(platform('org.junit:junit-bom:5.8.2')) - //JAVADOC: https://www.javadoc.io/doc/org.junit.jupiter/junit-jupiter-api/latest/index.html - testImplementation('org.junit.jupiter:junit-jupiter') + testImplementation(platform('org.junit:junit-bom:5.8.2')) + //JAVADOC: https://www.javadoc.io/doc/org.junit.jupiter/junit-jupiter-api/latest/index.html + testImplementation('org.junit.jupiter:junit-jupiter') } From 166d71ed171ef03f7fb653472e287ef630138d13 Mon Sep 17 00:00:00 2001 From: Mig Date: Tue, 10 May 2022 19:24:15 +0200 Subject: [PATCH 004/100] Asumo que getState existe ya que se hace en otra tarea --- .../java/us/muit/fs/a4i/persistence/ExcelReportManager.java | 3 +-- .../java/us/muit/fs/a4i/test/model/entities/ReportTest.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java index d49c473a..6b27da22 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java @@ -217,8 +217,7 @@ private void persistIndicator(Indicator indicator) { //Aquí debería indicar el formato de fuente en las celdas, que dependerá del estado del índice CellStyle style = wb.createCellStyle(); - // No existe getState??? - // solo hay que solucionar esto + // TODO: No existe getState??? --> se implementará style.setFont((Font) formater.getIndicatorFont(indicator.getState())); row.createCell(cellIndex++).setCellValue(indicator.getName()); diff --git a/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java b/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java index 3f9f7f4c..a1334617 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java @@ -151,7 +151,7 @@ void testAddMetric() { //Verifico que se ha consultado el nombre una vez al invocar este método, se usa como clave para meterlo en un mapa, hay que consultarlo //¿Por qué falla? ¿Con qué no había contado? ¿Hay problemas en el test o en el código? //Prueba a sustituir por la línea comentada - Mockito.verify(metricIntMock).getName(); + Mockito.verify(metricIntMock,atLeast(1)).getName(); //Mockito.verify(metricIntMock, atLeast(1)).getName(); Metric metric=reportTested.getMetricByName("issues"); assertEquals(metric.getValue(),3,"Debería tener el valor especificado en el mock"); From 0bf7bf5c60eee8aa3882d010bb898905b69312cb Mon Sep 17 00:00:00 2001 From: JacintoJT Date: Mon, 16 May 2022 18:57:19 +0200 Subject: [PATCH 005/100] solving the first issue, sustito primo --- .../fs/a4i/config/IndicatorConfiguration.java | 36 +++++++++++++++++++ .../a4i/config/IndicatorsConfiguration.java | 19 ++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java create mode 100644 src/main/java/us/muit/fs/a4i/config/IndicatorsConfiguration.java diff --git a/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java b/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java new file mode 100644 index 00000000..5a42d6fe --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java @@ -0,0 +1,36 @@ +package us.muit.fs.a4i.config; + +import java.util.HashMap; +import java.util.List; + +import us.muit.fs.a4i.model.entities.Indicator.State; +import us.muit.fs.a4i.model.entities.Report; + +public class IndicatorConfiguration implements IndicatorsConfiguration { + + @Override + public HashMap definedMetric(String metricName, String metricType) { + // TODO Auto-generated method stub + return null; + } + + private String appIndicators = null; + @Override + public void setAppIndicators(String appIndicatorsPath) { + appIndicators = appIndicatorsPath; + + } + + @Override + public List listAllIndicators() { + // TODO Auto-generated method stub + return null; + } + + @Override + public State getIndicatorState(Report indicator) { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/src/main/java/us/muit/fs/a4i/config/IndicatorsConfiguration.java b/src/main/java/us/muit/fs/a4i/config/IndicatorsConfiguration.java new file mode 100644 index 00000000..b4c09f3e --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/config/IndicatorsConfiguration.java @@ -0,0 +1,19 @@ +package us.muit.fs.a4i.config; + +import java.util.HashMap; +import java.util.List; + +import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.Report; + +public interface IndicatorsConfiguration { + + public HashMap definedMetric(String metricName, String metricType); + + public void setAppIndicators(String appIndicatorsPath); + + public List listAllIndicators(); + + public Indicator.State getIndicatorState(Report indicator); + +} From c8b355aad86ab5c13f4e4f10cbbe347d48506850 Mon Sep 17 00:00:00 2001 From: JacintoJT Date: Mon, 16 May 2022 20:02:46 +0200 Subject: [PATCH 006/100] Empezando a trabajar el test --- .../config/IndicatorConfigurationTest.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java diff --git a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java new file mode 100644 index 00000000..28838563 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java @@ -0,0 +1,73 @@ +/** + * + */ +package us.muit.fs.a4i.test.config; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.util.logging.Logger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.config.Checker; +import us.muit.fs.a4i.config.IndicatorConfiguration; + +/** + * @author jacinto + * + */ +class IndicatorConfigurationTest { + + private static Logger log = Logger.getLogger(CheckerTest.class.getName()); + static IndicatorConfiguration underTest; + static String appConfPath; + + @BeforeAll + static void setUpBeforeClass() throws Exception { + //Acciones a realizar antes de ejecutar los tests de esta clase + appConfPath="src"+File.separator+"test"+File.separator+"resources"+File.separator+"appConfTest.json"; + } + + /** + * @throws java.lang.Exception + * @see org.junit.jupiter.api.AfterAll + */ + @AfterAll + static void tearDownAfterClass() throws Exception { + //Acciones a realizar despu�s de ejecutar todos los tests de esta clase + } + + /** + * @throws java.lang.Exception + * @see org.junit.jupiter.api.BeforeEach + */ + @BeforeEach + void setUp() throws Exception { + //Acciones a realizar antes de cada uno de los tests de esta clase + //Creo el objeto bajo test, un Checker + underTest = new IndicatorConfiguration(); + } + + /** + * @throws java.lang.Exception + * @see org.junit.jupiter.api.AfterEach + */ + @AfterEach + void tearDown() throws Exception { + //Acciones a realizar despu�s de cada uno de los tests de esta clase + } + + /** + * Test method for {@link us.muit.fs.a4i.config.IndicatorConfiguration#setAppIndicators(java.lang.String)}. + */ + @Test + void testSetAppIndicators() { + + } + +} From 6a841fae6a3727f486af40bf02fbb3d4b2a467bf Mon Sep 17 00:00:00 2001 From: JacintoJT Date: Thu, 19 May 2022 20:11:30 +0200 Subject: [PATCH 007/100] Changing Interface and implementing IndicatorConfiguration methods --- .../fs/a4i/config/IndicatorConfiguration.java | 107 ++++++++++++++++-- ...ion.java => IndicatorsConfigurationI.java} | 6 +- .../config/IndicatorConfigurationTest.java | 2 + 3 files changed, 102 insertions(+), 13 deletions(-) rename src/main/java/us/muit/fs/a4i/config/{IndicatorsConfiguration.java => IndicatorsConfigurationI.java} (58%) 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 5a42d6fe..f012718f 100644 --- a/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java @@ -1,20 +1,60 @@ package us.muit.fs.a4i.config; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.json.JsonReader; + +import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.Metric; import us.muit.fs.a4i.model.entities.Indicator.State; import us.muit.fs.a4i.model.entities.Report; -public class IndicatorConfiguration implements IndicatorsConfiguration { +public class IndicatorConfiguration implements IndicatorsConfigurationI { + private static Logger log = Logger.getLogger(IndicatorConfiguration.class.getName()); + private String a4iMetrics = "a4iDefault.json"; + private String appIndicators = null; + private HashMap indicators; + + + private void createMaps() { + indicators=new HashMap(); + } @Override - public HashMap definedMetric(String metricName, String metricType) { - // TODO Auto-generated method stub - return null; + public HashMap definedIndicator(String indicatorName, String indicatorType) { + log.info("IndicatorConfiguration solicitud de busqueda indicator " + indicatorName); + + HashMap indicatorDefinition=null; + + String filePath="/"+a4iMetrics; + log.info("Buscando el archivo " + filePath); + InputStream is=this.getClass().getResourceAsStream(filePath); + log.info("InputStream "+is+" para "+filePath); + InputStreamReader isr = new InputStreamReader(is); + + + indicatorDefinition = isDefinedIndicator(indicatorName, indicatorType, isr); + if ((indicatorDefinition==null) && appIndicators != null) { + is=new FileInputStream(appIndicators); + isr=new InputStreamReader(is); + indicatorDefinition = isDefinedIndicator(indicatorName, indicatorType, isr); + } + + return indicatorDefinition; } - private String appIndicators = null; + @Override public void setAppIndicators(String appIndicatorsPath) { appIndicators = appIndicatorsPath; @@ -23,14 +63,61 @@ public void setAppIndicators(String appIndicatorsPath) { @Override public List listAllIndicators() { - // TODO Auto-generated method stub - return null; + List indicatorsName = null; + Indicator indicator = null; + Iterator it = indicators.keySet().iterator(); + + while (it.hasNext()) { + String clave = it.next(); + Indicator valor = indicators.get(clave); + String name = valor.getName(); + indicatorsName.add(name); + } + + return indicatorsName; } @Override - public State getIndicatorState(Report indicator) { - // TODO Auto-generated method stub - return null; + public Indicator.State getIndicatorState(ReportItem indicator) { + // Suponemos que Indicator tiene el constructor que te devuelve una coleccion de + // ReportItems + + State estado = indicator.getIndicator().State; + return estado; + } + + + private HashMap isDefinedIndicator(String IndicatorName, String IndicatorType, InputStreamReader isr) + throws FileNotFoundException { + + HashMap indicatorDefinition=null; + + + JsonReader reader = Json.createReader(isr); + log.info("Creo el JsonReader"); + + JsonObject confObject = reader.readObject(); + log.info("Leo el objeto"); + reader.close(); + + log.info("Muestro la configuraci�n le�da " + confObject); + JsonArray indicators = confObject.getJsonArray("indicators"); + log.info("El n�mero de m�tricas es " + indicators.size()); + for (int i = 0; i < indicators.size(); i++) { + log.info("nombre: " + indicators.get(i).asJsonObject().getString("name")); + if (indicators.get(i).asJsonObject().getString("name").equals(IndicatorName)) { + log.info("Localizada la m�trica"); + log.info("tipo: " + indicators.get(i).asJsonObject().getString("type")); + if (indicators.get(i).asJsonObject().getString("type").equals(IndicatorType)) { + indicatorDefinition=new HashMap(); + indicatorDefinition.put("description", indicators.get(i).asJsonObject().getString("description")); + indicatorDefinition.put("unit", indicators.get(i).asJsonObject().getString("unit")); + } + + } + } + + return indicatorDefinition; } } diff --git a/src/main/java/us/muit/fs/a4i/config/IndicatorsConfiguration.java b/src/main/java/us/muit/fs/a4i/config/IndicatorsConfigurationI.java similarity index 58% rename from src/main/java/us/muit/fs/a4i/config/IndicatorsConfiguration.java rename to src/main/java/us/muit/fs/a4i/config/IndicatorsConfigurationI.java index b4c09f3e..9ce5ece2 100644 --- a/src/main/java/us/muit/fs/a4i/config/IndicatorsConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/IndicatorsConfigurationI.java @@ -6,14 +6,14 @@ import us.muit.fs.a4i.model.entities.Indicator; import us.muit.fs.a4i.model.entities.Report; -public interface IndicatorsConfiguration { +public interface IndicatorsConfigurationI { - public HashMap definedMetric(String metricName, String metricType); + public HashMap definedIndicator(String indicatorName, String indicatorType); public void setAppIndicators(String appIndicatorsPath); public List listAllIndicators(); - public Indicator.State getIndicatorState(Report indicator); + public Indicator.State getIndicatorState(ReportItem indicator); } diff --git a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java index 28838563..fdba3bf5 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java @@ -67,6 +67,8 @@ void tearDown() throws Exception { */ @Test void testSetAppIndicators() { + String prueba = "metrica1,2,3y4"; + } From 32b5f6d4accf9118937e154e7758e391de33ce63 Mon Sep 17 00:00:00 2001 From: JacintoJT Date: Thu, 19 May 2022 20:25:41 +0200 Subject: [PATCH 008/100] Test for definedIndicator() done --- .../config/IndicatorConfigurationTest.java | 86 +++++++++++++++---- 1 file changed, 71 insertions(+), 15 deletions(-) diff --git a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java index fdba3bf5..ac44b15d 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java @@ -1,11 +1,10 @@ -/** - * - */ package us.muit.fs.a4i.test.config; import static org.junit.jupiter.api.Assertions.*; import java.io.File; +import java.io.FileNotFoundException; +import java.util.HashMap; import java.util.logging.Logger; import org.junit.jupiter.api.AfterAll; @@ -17,13 +16,8 @@ import us.muit.fs.a4i.config.Checker; import us.muit.fs.a4i.config.IndicatorConfiguration; -/** - * @author jacinto - * - */ class IndicatorConfigurationTest { - - private static Logger log = Logger.getLogger(CheckerTest.class.getName()); + private static Logger log = Logger.getLogger(IndicatorConfigurationTest.class.getName()); static IndicatorConfiguration underTest; static String appConfPath; @@ -62,14 +56,76 @@ void tearDown() throws Exception { //Acciones a realizar despu�s de cada uno de los tests de esta clase } - /** - * Test method for {@link us.muit.fs.a4i.config.IndicatorConfiguration#setAppIndicators(java.lang.String)}. - */ + @Test + void testDefinedIndicator() { + //Creo valores Mock para verificar si comprueba bien el tipo + //Las m�tricas del test son de enteros, as� que creo un entero y un string (el primero no dar� problemas el segundo s�) + Double valOKMock = Double.valueOf(0.3); + String valKOMock = "KO"; + HashMap returnedMap=null; + //Primero, sin fichero de configuraci�n de aplicaci�n + try { + //Consulta un indicador no definido, con valor de tipo entero + //debe devolver null, no est� definido + log.info("Busco el indicador llamado pullReqGlory"); + returnedMap=underTest.definedIndicator("pullReqGlory", valOKMock.getClass().getName()); + assertNull(returnedMap, "Deber�a ser nulo, el indicador pullReqGlory no est� definido"); + + //Busco el indicador overdued con valor double, no deber�a dar problemas + log.info("Busco el indicador overdued"); + returnedMap=underTest.definedIndicator("overdued", valOKMock.getClass().getName()); + assertNotNull(returnedMap,"Deber�a devolver un hashmap, el indicador overdued est� definido"); + assertTrue(returnedMap.containsKey("unit"),"La clave unit tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("description"),"La clave description tiene que estar en el mapa"); + + //Busco una m�trica que existe pero con un tipo incorrecto en el valor + assertNull(underTest.definedIndicator("overdued", valKOMock.getClass().getName()), + "Deber�a ser nulo, el indicador overdued est� definido para Double"); + } catch (FileNotFoundException e) { + fail("El fichero est� en la carpeta resources"); + e.printStackTrace(); + } + //Ahora establezco el fichero de configuraci�n de la aplicaci�n, con un nombre de fichero que no existe + underTest.setAppIndicators("pepe"); + try { + //Busco un indicador que se que no est� en la configuraci�n de la api + returnedMap=underTest.definedIndicator("pullReqGlory", valOKMock.getClass().getName()); + fail("Deber�a lanzar una excepci�n porque intenta buscar en un fichero que no existe"); + } catch (FileNotFoundException e) { + log.info("Lanza la excepci�n adecuada, FileNotFoud"); + } catch (Exception e) { + fail("Lanza la excepci�n equivocada " + e); + } + + //Ahora establezco un fichero de configuraci�n de la aplicaci�n que s� existe + underTest.setAppIndicators(appConfPath); + try { + //Busco una m�trica que se que no est� en la configuraci�n de la api pero s� en la de la aplicaci�n + log.info("Busco el indicador llamado pullReqGlory"); + returnedMap=underTest.definedIndicator("pullReqGlory", valOKMock.getClass().getName()); + assertNotNull(returnedMap,"Deber�a devolver un hashmap, el indicador est� definido"); + assertTrue(returnedMap.containsKey("unit"),"La clave unit tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("description"),"La clave description tiene que estar en el mapa"); + } catch (FileNotFoundException e) { + fail("No deber�a devolver esta excepci�n"); + } catch (Exception e) { + fail("Lanza una excepci�n no reconocida " + e); + } + } + @Test void testSetAppIndicators() { - String prueba = "metrica1,2,3y4"; - - + fail("Not yet implemented"); + } + + @Test + void testListAllIndicators() { + fail("Not yet implemented"); + } + + @Test + void testGetIndicatorState() { + fail("Not yet implemented"); } } From f39a928204ebb514bdd8345c9f7018f859c6edaf Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Tue, 24 May 2022 13:37:04 +0200 Subject: [PATCH 009/100] =?UTF-8?q?Reordenaci=C3=B3n=20fichero=20build.gra?= =?UTF-8?q?dle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 55 ++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/build.gradle b/build.gradle index 396b277b..dc3bbeb2 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,34 @@ plugins { //Plugin para análisis estático de código id "nebula.lint" version "17.7.0" } +//Para publicar paquetes en github +//group = 'A4I' +publishing { + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/mit-fs/audit4improve-api") + credentials { + //las propiedades gpr.user y gpr.key están configuradas en gradle.properties en el raiz del proyecto, y se añade a .gitignore para que no se suban + //O bien configuro las variables de entorno GITHUB_LOGIN y GITHUB_PACKAGES + username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_LOGIN") + password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_PACKAGES") + } + } + } + publications { + gpr(MavenPublication) { + //Del tutorial https://docs.gradle.org/current/userguide/publishing_maven.html#publishing_maven + + groupId = 'us.mitfs.samples' + artifactId = 'a4i' + version = '0.0' + + from components.java + } + + } +} version = '0.0' tasks.withType(JavaCompile) { //Añadir la opción Xlint @@ -101,33 +129,6 @@ test { } -//Para publicar paquetes en github -//group = 'A4I' -publishing { - repositories { - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/mit-fs/audit4improve-api") - credentials { - //las propiedades gpr.user y gpr.key están configuradas en gradle.properties en el raiz del proyecto, y se añade a .gitignore para que no se suban - //O bien configuro las variables de entorno GITHUB_LOGIN y GITHUB_PACKAGES - username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_LOGIN") - password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_PACKAGES") - } - } - } - publications { - gpr(MavenPublication) { - //Del tutorial https://docs.gradle.org/current/userguide/publishing_maven.html#publishing_maven - - groupId = 'us.mitfs.samples' - artifactId = 'a4i' - version = '0.0' - from components.java - } - - } -} From 3b08bf67291219876525569f7c3d0782bd45b9a5 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Tue, 24 May 2022 13:43:04 +0200 Subject: [PATCH 010/100] =?UTF-8?q?Peque=C3=B1os=20cambios=20build.gradle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dc3bbeb2..ff848309 100644 --- a/build.gradle +++ b/build.gradle @@ -91,7 +91,7 @@ dependencies { // https://mvnrepository.com/artifact/org.kohsuke/github-api //JAVADOC: https://github-api.kohsuke.org/apidocs/index.html - implementation group: 'org.kohsuke', name: 'github-api', version: '1.301' + api 'org.kohsuke:github-api:1.301' //Para la persistencia de informes usaremos la api apachepoi // https://mvnrepository.com/artifact/org.apache.poi/poi //JAVADOC: https://poi.apache.org/apidocs/5.0/ From e0ffbba9d2bf108618702008c3abfa50e7b56b6a Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Tue, 24 May 2022 14:02:02 +0200 Subject: [PATCH 011/100] =?UTF-8?q?A=C3=B1adiendo=20pipeline=20al=20proyec?= =?UTF-8?q?to?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Añadida la carpeta workflows y el fichero pipeline.yml desde el repositorio local Pipeline inicial según https://docs.github.com/en/actions/quickstart --- .github/workflows/pipeline.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/pipeline.yml diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml new file mode 100644 index 00000000..75be1014 --- /dev/null +++ b/.github/workflows/pipeline.yml @@ -0,0 +1,17 @@ +name: GitHub Actions Demo +on: [push] +jobs: + Explore-GitHub-Actions: + runs-on: ubuntu-latest + steps: + - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "🧠This job is now running on a ${{ runner.os }} server hosted by GitHub!" + - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - name: Check out repository code + uses: actions/checkout@v3 + - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." + - run: echo "ðŸ–¥ï¸ The workflow is now ready to test your code on the runner." + - name: List files in the repository + run: | + ls ${{ github.workspace }} + - run: echo "ðŸ This job's status is ${{ job.status }}." From 9626804b48bedf7a64d4d3ea0fca32ee84340fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Tue, 24 May 2022 18:08:20 +0200 Subject: [PATCH 012/100] Ampliando el demostrador de pipelines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se añade un trabajo para mostrar diferentes contextos --- .github/workflows/pipeline.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 75be1014..7aaf8ebd 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -1,4 +1,4 @@ -name: GitHub Actions Demo +name: Demostrador para Acciones GitHub on: [push] jobs: Explore-GitHub-Actions: @@ -15,3 +15,20 @@ jobs: run: | ls ${{ github.workspace }} - run: echo "ðŸ This job's status is ${{ job.status }}." + + dump_contexts_to_log: + runs-on: ubuntu-latest + steps: + - name: Mostrando contexto GitHub + id: github_context_step + run: echo '${{ toJSON(github) }}' + - name: Mostrando contexto job + run: echo '${{ toJSON(job) }}' + - name: Mostrando contexto steps + run: echo '${{ toJSON(steps) }}' + - name: Mostrando contexto runner + run: echo '${{ toJSON(runner) }}' + - name: Mostrando contexto strategy + run: echo '${{ toJSON(strategy) }}' + - name: Mostrando contexto matrix + run: echo '${{ toJSON(matrix) }}' From 3ff8d927d5acc64cbed01c2ce5733f9f12f07c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Tue, 24 May 2022 19:21:28 +0200 Subject: [PATCH 013/100] =?UTF-8?q?A=C3=B1adiendo=20workflow=20seguridad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creando flujo de trabajo para tareas de seguridad --- .github/workflows/seguridad.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/workflows/seguridad.yml diff --git a/.github/workflows/seguridad.yml b/.github/workflows/seguridad.yml new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/.github/workflows/seguridad.yml @@ -0,0 +1 @@ + From fa6b9a1cda87f8fa8ee688441d387b58f6bee593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Tue, 24 May 2022 19:48:37 +0200 Subject: [PATCH 014/100] =?UTF-8?q?A=C3=B1adiendo=20pipeline=20pruebas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Este flujo ejecuta los tests unidad --- .github/workflows/pruebas.yml | 28 ++++++++++++++++++++++++++++ .github/workflows/seguridad.yml | 1 - 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/pruebas.yml delete mode 100644 .github/workflows/seguridad.yml diff --git a/.github/workflows/pruebas.yml b/.github/workflows/pruebas.yml new file mode 100644 index 00000000..f7ae993e --- /dev/null +++ b/.github/workflows/pruebas.yml @@ -0,0 +1,28 @@ +name: Flujo de trabajo para ejecutar los test +on: + workflow_dispatch: + push: + branches: [ V.0.2 ] + pull_request: + branches: [ V.0.2 ] + schedule: + - cron: '1 23 * * 0-4' +jobs: + Build: + runs-on: ubuntu-latest + env: + GITHUB_LOGIN: ${{ github.actor }} + GITHUB_PACKAGES: ${{ secrets.GITHUB_TOKEN }} + GITHUB_OAUTH: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Clonando el repositorio y estableciendo el espacio de trabajo + uses: actions/checkout@v3 + - name: Configurando java + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '16' + - name: Construyendo y probando el código + run: | + chmod +x gradlew + ./gradlew build diff --git a/.github/workflows/seguridad.yml b/.github/workflows/seguridad.yml deleted file mode 100644 index 8b137891..00000000 --- a/.github/workflows/seguridad.yml +++ /dev/null @@ -1 +0,0 @@ - From 708f13f2dd2d4e55fa42efb455ab3c14a7a269f3 Mon Sep 17 00:00:00 2001 From: estheruly1998 Date: Wed, 25 May 2022 11:24:30 +0200 Subject: [PATCH 015/100] tareas --- build.gradle | 2 +- src/main/java/us/muit/fs/a4i/model/entities/Report.java | 2 +- .../java/us/muit/fs/a4i/test/model/entities/ReportTest.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 7a65acdc..f523d240 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ plugins { //Plugin para análisis estático de código id "nebula.lint" version "17.7.0" } -version = '0.0' +//version = '2.0' tasks.withType(JavaCompile) { //Añadir la opción Xlint options.deprecation = true diff --git a/src/main/java/us/muit/fs/a4i/model/entities/Report.java b/src/main/java/us/muit/fs/a4i/model/entities/Report.java index dbc92fd0..e63d8dd3 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/Report.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/Report.java @@ -167,7 +167,7 @@ public String toString() { return repoinfo; } @Override - public Collection getAllMetrics() { + public getAllMetrics() { // TODO Auto-generated method stub return metrics.values(); } diff --git a/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java b/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java index 3f9f7f4c..6ba625d0 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java @@ -151,8 +151,8 @@ void testAddMetric() { //Verifico que se ha consultado el nombre una vez al invocar este método, se usa como clave para meterlo en un mapa, hay que consultarlo //¿Por qué falla? ¿Con qué no había contado? ¿Hay problemas en el test o en el código? //Prueba a sustituir por la línea comentada - Mockito.verify(metricIntMock).getName(); - //Mockito.verify(metricIntMock, atLeast(1)).getName(); + //Mockito.verify(metricIntMock).getName(); + Mockito.verify(metricIntMock, atLeast(1)).getName(); Metric metric=reportTested.getMetricByName("issues"); assertEquals(metric.getValue(),3,"Debería tener el valor especificado en el mock"); assertEquals(metric.getDescription(),"Tareas sin finalizar en el repositorio","Debería tener el valor especificado en el mock"); From f84e238b6855bfe90172dc72532a92f20110b24a Mon Sep 17 00:00:00 2001 From: juapalesp Date: Wed, 25 May 2022 13:35:18 +0200 Subject: [PATCH 016/100] Cambios realizados en la interfaz, clase y test de la clase report --- .../us/muit/fs/a4i/model/entities/Report.java | 95 ++--- .../muit/fs/a4i/model/entities/ReportI.java | 84 ++-- .../a4i/test/model/entities/ReportTest.java | 377 ++++++++++++------ 3 files changed, 330 insertions(+), 226 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/model/entities/Report.java b/src/main/java/us/muit/fs/a4i/model/entities/Report.java index c13a4c65..5e0f238f 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/Report.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/Report.java @@ -14,20 +14,20 @@ /** *

Aspectos generales de todos los informes

- *

Todos incluirán un conjunto de métricas de tipo numérico y otro de tipo Date

- * @author Isabel Román + *

Todos incluir�n un conjunto de m�tricas de tipo num�rico y otro de tipo Date

+ * @author Isabel Rom�n * */ public class Report implements ReportI { private static Logger log=Logger.getLogger(Report.class.getName()); /** - *

Identificador unívoco de la entidad a la que se refire el informe en el servidor remoto que se va a utilizar

+ *

Identificador un�voco de la entidad a la que se refire el informe en el servidor remoto que se va a utilizar

*/ - private String id; + private String entityId; /** - *

Los objetos que implementen esta interfaz recurren a calcuadoras con los algoritmos para el cálculo de indicadores

- *

Los algoritmos de cálculo de indicadores serán específicos para un tipo de informe

+ *

Los objetos que implementen esta interfaz recurren a calcuadoras con los algoritmos para el c�lculo de indicadores

+ *

Los algoritmos de c�lculo de indicadores ser�n espec�ficos para un tipo de informe

*/ private IndicatorsCalculator calc; @@ -36,7 +36,7 @@ public class Report implements ReportI { private ReportI.Type type=null; /** - * Mapa de Métricas + * Mapa de M�tricas * * */ @@ -52,48 +52,48 @@ public Report(){ createMaps(); } - public Report(String id){ + public Report(String entityId){ createMaps(); - this.id=id; + this.entityId=entityId; } public Report(Type type){ createMaps(); this.type=type; } - public Report(Type type,String id){ + public Report(Type type,String entityId){ createMaps(); this.type=type; - this.id=id; + this.entityId=entityId; } private void createMaps() { metrics=new HashMap(); indicators=new HashMap(); } /** - *

Busca la métrica solicita en el informe y la devuelve

+ *

Busca la m�trica solicita en el informe y la devuelve

*

Si no existe devuelve null

- * @param name Nombre de la métrica buscada - * @return la métrica localizada + * @param name Nombre de la m�trica buscada + * @return la m�trica localizada */ @Override public Metric getMetricByName(String name) { - log.info("solicitada métrica de nombre "+name); + log.info("solicitada m�trica de nombre "+name); Metric metric=null; if (metrics.containsKey(name)){ - log.info("La métrica está en el informe"); + log.info("La m�trica est� en el informe"); metric=metrics.get(name); } return metric; } /** - *

Añade una métrica al informe

+ *

A�ade una m�trica al informe

*/ @Override public void addMetric(Metric met) { metrics.put(met.getName(), met); - log.info("Añadida métrica "+met+" Con nombre "+met.getName()); + log.info("A�adida m�trica "+met+" Con nombre "+met.getName()); } /** *

Busca el indicador solicitado en el informe y lo devuelve

@@ -112,51 +112,31 @@ public Indicator getIndicatorByName(String name) { return indicator; } /** - *

Añade un indicador al informe

+ *

A�ade un indicador al informe

* */ @Override public void addIndicator(Indicator ind) { indicators.put(ind.getName(), ind); - log.info("Añadido indicador "+ind); + log.info("A�adido indicador "+ind); } /** - *

Calcula el indicador solicitado y lo incluye en el informe, si se necesita alguna métrica que no exista la calculadora la busca y la incluye

+ *

Calcula el indicador solicitado y lo incluye en el informe, si se necesita alguna m�trica que no exista la calculadora la busca y la incluye

*/ - @Override - public void calcIndicator(String name) { - try { - calc.calcIndicator(name, this); - } catch (IndicatorException e) { - log.info("No se puede calcular esta indicador, no se incluirá"); - } - } - @Override - public void setId(String id) { - this.id=id; - } + + @Override - public String getId() { - return id; + public String getEntityId() { + return entityId; } - @Override - public void setIndicatorsCalculator(IndicatorsCalculator calc) throws IndicatorException { - log.info("Se establece la calculadora de indicadores que va a usar este informe"); - if(this.type==null) { - this.type=calc.getReportType(); - log.info("El tipo del informe será "+this.type); - }else if(this.type!=calc.getReportType()){ - throw new IndicatorException("La calculadora no concuerda con el tipo de informe"); - } - this.calc=calc; - } + @Override public String toString() { String repoinfo; - repoinfo="Información del Informe:\n - Métricas: "; + repoinfo="Informaci�n del Informe:\n - M�tricas: "; for (String clave:metrics.keySet()) { repoinfo+="\n Clave: " + clave + metrics.get(clave); } @@ -171,22 +151,17 @@ public Collection getAllMetrics() { // TODO Auto-generated method stub return metrics.values(); } + @Override - public ReportI.Type getType() { - return type; - } - @Override - public void setType(ReportI.Type type) { - //Sólo se puede cambiar si no estaba aún establecido - //Un informe no puede cambiar de tipo - if (this.type==null) { - this.type = type; - } + public Collection getAllIndicators() { + // TODO Auto-generated method stub + return indicators.values(); } @Override - public void calcAllIndicators() { - // TODO Auto-generated method stub - + public ReportI.Type getType() { + return type; } + + } diff --git a/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java b/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java index 1b152edf..4cf12b09 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java @@ -8,8 +8,8 @@ public interface ReportI { /** - *

Tipos de informes, puede necesitarse cuando los algoritmos de cálculo de indicadores difieran según el tipo de informe

- *

Un informe sólo es de un tipo y no se puede modificar una vez establecido

+ *

Tipos de informes, puede necesitarse cuando los algoritmos de c�lculo de indicadores difieran seg�n el tipo de informe

+ *

Un informe s�lo es de un tipo y no se puede modificar una vez establecido

* */ @@ -20,74 +20,74 @@ public static enum Type{ ORGANIZATION } + ReportI.Type getType(); + + String getEntityId(); + + Metric getMetricByName(String name); + + Collection getAllMetrics(); + + void addMetric(Metric met); + + Indicator getIndicatorByName(String name); + + Collection getAllIndicators(); + + void addIndicator(Indicator ind); + /** - * Consulta una métrica de un informe a partir del nombre - * @param name Nombre de la métrica solicitada - * @return Métrica solicitada + * Consulta una m�trica de un informe a partir del nombre + * @param name Nombre de la m�trica solicitada + * @return M�trica solicitada */ - Metric getMetricByName(String name); + /** - * Obtiene todas las métricas del informe - * @return Colleción de métricas que contiene el informe + * Obtiene todas las m�tricas del informe + * @return Colleci�n de m�tricas que contiene el informe */ - Collection getAllMetrics(); + /** - * Añade una métrica al informe - * @param met Nueva métrica + * A�ade una m�trica al informe + * @param met Nueva m�trica */ - void addMetric(Metric met); + /** * Obtiene un indicador del informe a partir del nombre del mismo * @param name Nombre del indicador consultado * @return El indicador */ - Indicator getIndicatorByName(String name); + /** - * Añade un indicador al informe + * A�ade un indicador al informe * @param ind Nuevo indicador */ - void addIndicator(Indicator ind); + /** - * Calcula un indicador a partir de su nombre y lo añade al informe - * Si se basa en métricas que no están aún incluidas en el informe las incluye + * Calcula un indicador a partir de su nombre y lo a�ade al informe + * Si se basa en m�tricas que no est�n a�n incluidas en el informe las incluye * @param name Nombre del indicador que se quiere calcular */ - void calcIndicator(String name); /** - * Establede el identificador unívoco de la entidad a la que se refiere el informe, debe ser el identificador usado en el remoto - * @param id Identificador unívoco de la entidad a la que se refiere el informe en el remoto - */ - void setId(String id); + /** * Obtiene el identificador de la entidad a la que se refiere el informe - * @return Identificador unívoco de la entidad a la que se refiere el informe en el remoto + * @return Identificador un�voco de la entidad a la que se refiere el informe en el remoto */ - String getId(); + /** - * Establece la calculadora de indicadores, debe ser específica para el tipo de informe - * @param calc calculadora a utilizar para el cálculo de indicadores + * Establece la calculadora de indicadores, debe ser espec�fica para el tipo de informe + * @param calc calculadora a utilizar para el c�lculo de indicadores * @throws IndicatorException Si el tipo de la calculadora no coincide con el tipo de informe */ - void setIndicatorsCalculator(IndicatorsCalculator calc) throws IndicatorException; - /** - * Calcula todos los indicadores especificados por defecto para el tipo de informe y los incluye en el informe - * También incluye las métricas utiizadas - */ - void calcAllIndicators(); - /** - * Establece el tipo del informe, sólo se puede establecer una vez y debe coincidir con la el tipo de la calculadora usada - * @param type Tipo del informe - */ - void setType(ReportI.Type type); - /** - * Obtiene el tipo del informe - * @return Tipo del informe - */ - ReportI.Type getType(); + + + + } \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java b/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java index 3f9f7f4c..02de3dda 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java @@ -37,7 +37,7 @@ /** *

Test para probar la clase Report

- * @author Isabel Román + * @author Isabel Román * */ @ExtendWith(MockitoExtension.class) @@ -51,8 +51,8 @@ class ReportTest { private static IndicatorsCalculator indCalcMock= Mockito.mock(IndicatorsCalculator.class); - //Servirán para conocer el argumento con el que se ha invocado algún método de alguno de los mocks (sustitutos o representantes) - //ArgumentCaptor es un genérico, indico al declararlo el tipo del argumento que quiero capturar + //Servirán para conocer el argumento con el que se ha invocado algún método de alguno de los mocks (sustitutos o representantes) + //ArgumentCaptor es un genérico, indico al declararlo el tipo del argumento que quiero capturar @Captor private ArgumentCaptor intCaptor; @Captor @@ -70,7 +70,8 @@ class ReportTest { private static Metric metricDatMock= Mockito.mock(Metric.class); @Mock(serializable = true) private static Indicator indicatorIntMock= Mockito.mock(Indicator.class); - + @Mock(serializable = true) + private static Indicator indicatorDatMock= Mockito.mock(Indicator.class); private static Report reportTested; @@ -112,33 +113,73 @@ void tearDown() throws Exception { @Test @Tag("noacabado") void testReport() { - - fail("Not yet implemented"); // TODO + //fail("Not yet implemented"); // TODO + //El constructor está formado por el string id y por el tipo type. + reportTested=new Report("entityId"); + //reportTested=new Report("entityId"); falta el type + reportTested=new Report(); + ReportI.Type type=reportTested.getType(); + assertEquals(type,reportTested.getType(),"No se establece correctamente el tipo del informe"); + assertEquals("entityId",reportTested.getEntityId(),"No se establece correctamente el identificador del informe"); } + + + + + + /** - * Test del constructor pasándole el id + * Test del constructor pasándole el id * Test method for {@link us.muit.fs.a4i.model.entities.Report#Report(java.lang.String)}. */ @Test @Tag("noacabado") void testReportString() { - reportTested=new Report("id"); - - assertEquals("id",reportTested.getId(),"No se establece correctamente el identificador del informe"); - - + reportTested=new Report("entityId"); + assertEquals("entityId",reportTested.getEntityId(),"No se establece correctamente el identificador del informe"); + + } + + + + + + + + + + + + + @Test + @Tag("noacabado") + void testReportType() { + reportTested=new Report(); + ReportI.Type type=reportTested.getType(); + assertEquals(type,reportTested.getType(),"No se establece correctamente el tipo del informe"); } + + /** * Test method for {@link us.muit.fs.a4i.model.entities.Report#getMetricByName(java.lang.String)}. */ @Test @Tag("noacabado") void testGetMetricByName() { - fail("Not yet implemented"); // TODO + // TODO + String name="nombre"; + //Metric metric=new Metric(); + Metric metric= null; + reportTested=new Report(); + reportTested.getMetricByName(name); + assertEquals(null,reportTested.getMetricByName(name),"No se establece correctamente el buscador del nombre de la métrica"); + } - + + + /** * Test method for {@link us.muit.fs.a4i.model.entities.Report#addMetric(us.muit.fs.a4i.model.entities.Metric)}. */ @@ -146,39 +187,51 @@ void testGetMetricByName() { void testAddMetric() { reportTested=new Report(); setMetricsMocks(); - //Primero se prueba a añadir una métrica de tipo Integer + //Primero se prueba a añadir una métrica de tipo Integer reportTested.addMetric(metricIntMock); - //Verifico que se ha consultado el nombre una vez al invocar este método, se usa como clave para meterlo en un mapa, hay que consultarlo - //¿Por qué falla? ¿Con qué no había contado? ¿Hay problemas en el test o en el código? - //Prueba a sustituir por la línea comentada + //Verifico que se ha consultado el nombre una vez al invocar este método, se usa como clave para meterlo en un mapa, hay que consultarlo + //¿Por qué falla? ¿Con qué no había contado? ¿Hay problemas en el test o en el código? + //Prueba a sustituir por la línea comentada Mockito.verify(metricIntMock).getName(); //Mockito.verify(metricIntMock, atLeast(1)).getName(); Metric metric=reportTested.getMetricByName("issues"); - assertEquals(metric.getValue(),3,"Debería tener el valor especificado en el mock"); - assertEquals(metric.getDescription(),"Tareas sin finalizar en el repositorio","Debería tener el valor especificado en el mock"); + assertEquals(metric.getValue(),3,"Debería tener el valor especificado en el mock"); + assertEquals(metric.getDescription(),"Tareas sin finalizar en el repositorio","Debería tener el valor especificado en el mock"); - //Ahora se prueba una métrica de tipo Date + //Ahora se prueba una métrica de tipo Date reportTested.addMetric(metricDatMock); metric=reportTested.getMetricByName("lastPush"); - assertEquals(metric.getValue(),metricDatMock.getValue(),"Debería tener el valor especificado en el mock"); - assertEquals(metric.getDescription(),"Último push realizado en el repositorio","Debería tener el valor especificado en el mock"); + assertEquals(metric.getValue(),metricDatMock.getValue(),"Debería tener el valor especificado en el mock"); + assertEquals(metric.getDescription(),"Último push realizado en el repositorio","Debería tener el valor especificado en el mock"); - //Ahora se prueba a añadir otra vez la misma métrica pero con otro valor + //Ahora se prueba a añadir otra vez la misma métrica pero con otro valor reportTested.addMetric(metricIntMock); Mockito.when(metricIntMock.getValue()).thenReturn(55); metric=reportTested.getMetricByName("issues"); - assertEquals(metric.getValue(),55,"Debería tener el valor especificado en el mock"); - assertEquals(metric.getDescription(),"Tareas sin finalizar en el repositorio","Debería tener el valor especificado en el mock"); + assertEquals(metric.getValue(),55,"Debería tener el valor especificado en el mock"); + assertEquals(metric.getDescription(),"Tareas sin finalizar en el repositorio","Debería tener el valor especificado en el mock"); } - + + /** * Test method for {@link us.muit.fs.a4i.model.entities.Report#getIndicatorByName(java.lang.String)}. */ @Test @Tag("noacabado") void testGetIndicatorByName() { - fail("Not yet implemented"); // TODO + // TODO + String name="nombre"; + //Metric metric=new Metric(); + Indicator indicator= null; + reportTested=new Report(); + //reportTested.getIndicatorByName(name); + assertEquals(null,reportTested.getIndicatorByName(name),"No se establece correctamente el buscador del nombre del indicador"); + } + + + + /** * Test method for {@link us.muit.fs.a4i.model.entities.Report#addIndicator(us.muit.fs.a4i.model.entities.Indicator)}. @@ -186,129 +239,169 @@ void testGetIndicatorByName() { @Test @Tag("noacabado") void testAddIndicator() { - fail("Not yet implemented"); // TODO + //fail("Not yet implemented"); // TODO, HECHO + + reportTested=new Report(); + setIndicatorsMocks(); + //Primero se prueba a añadir una métrica de tipo Integer + reportTested.addIndicator(indicatorIntMock); + //Verifico que se ha consultado el nombre una vez al invocar este método, se usa como clave para meterlo en un mapa, hay que consultarlo + //¿Por qué falla? ¿Con qué no había contado? ¿Hay problemas en el test o en el código? + //Prueba a sustituir por la línea comentada + Mockito.verify(indicatorIntMock).getName(); + //Mockito.verify(metricIntMock, atLeast(1)).getName(); + Indicator indicator=reportTested.getIndicatorByName("issues"); + assertEquals(indicator.getValue(),3,"Debería tener el valor especificado en el mock"); + assertEquals(indicator.getDescription(),"Tareas sin finalizar en el repositorio","Debería tener el valor especificado en el mock"); + + //Ahora se prueba una métrica de tipo Date + reportTested.addIndicator(indicatorDatMock); + indicator=reportTested.getIndicatorByName("lastPush"); + assertEquals(indicator.getValue(),metricDatMock.getValue(),"Debería tener el valor especificado en el mock"); + assertEquals(indicator.getDescription(),"Último push realizado en el repositorio","Debería tener el valor especificado en el mock"); + + //Ahora se prueba a añadir otra vez la misma métrica pero con otro valor + reportTested.addIndicator(indicatorIntMock); + Mockito.when(indicatorIntMock.getValue()).thenReturn(55); + indicator=reportTested.getIndicatorByName("issues"); + assertEquals(indicator.getValue(),55,"Debería tener el valor especificado en el mock"); + assertEquals(indicator.getDescription(),"Tareas sin finalizar en el repositorio","Debería tener el valor especificado en el mock"); + } /** * Test method for {@link us.muit.fs.a4i.model.entities.Report#calcIndicator(java.lang.String)}. */ - @Test - void testCalcIndicator() { - //Definimos calculadora de tipo repositorio - Mockito.when(indCalcMock.getReportType()).thenReturn(Report.Type.REPOSITORY); - - - //Se configura la calculadora de indicadores del informe - try { - reportTested.setIndicatorsCalculator(indCalcMock); - Mockito.verify(indCalcMock).getReportType(); - } catch (IndicatorException e1) { - fail("No debería lanzar la excepción"); - } - //Se solicita el cálculo de un indicador determinado - reportTested.calcIndicator("issues"); - //Se observa con qué parámetros se invoca a la calculadora de indicadores - try { - Mockito.verify(indCalcMock).calcIndicator(strCaptor.capture(), reportCaptor.capture()); - //Elimine el comentario que aparece a continuación, ejecute el test y explique por qué falla - //Mockito.verify(indCalcMock).calcAllIndicators(reportTested); - } catch (IndicatorException e) { - fail("No debería lanzar la excepción"); - } - - //Se verfica que se usa el nombre correcto y se pasa la referencia al informe correcto - assertEquals("issues",strCaptor.getValue(),"Se solicita el cálculo de la métrica adecuada"); - assertEquals(reportTested,reportCaptor.getValue(),"Se pasa la referencia correcta del informe"); - //Hago un test que asegure que el propio informe captura y gestiona la excepción de que el indicador no existe - try { - Mockito.doThrow(new IndicatorException("El indicador no existe")).when(indCalcMock).calcIndicator("indKO", reportTested); - reportTested.calcIndicator("indKO"); - - } catch (IndicatorException e) { - fail("La excepción la debe gestionar el propio informe"); - } - - } +// @Test +// void testCalcIndicator() { +// //Definimos calculadora de tipo repositorio +// Mockito.when(indCalcMock.getReportType()).thenReturn(Report.Type.REPOSITORY); +// +// +// //Se configura la calculadora de indicadores del informe +// try { +// reportTested.setIndicatorsCalculator(indCalcMock); +// Mockito.verify(indCalcMock).getReportType(); +// } catch (IndicatorException e1) { +// fail("No debería lanzar la excepción"); +// } +// //Se solicita el cálculo de un indicador determinado +// reportTested.calcIndicator("issues"); +// //Se observa con qué parámetros se invoca a la calculadora de indicadores +// try { +// Mockito.verify(indCalcMock).calcIndicator(strCaptor.capture(), reportCaptor.capture()); +// //Elimine el comentario que aparece a continuación, ejecute el test y explique por qué falla +// //Mockito.verify(indCalcMock).calcAllIndicators(reportTested); +// } catch (IndicatorException e) { +// fail("No debería lanzar la excepción"); +// } +// +// //Se verfica que se usa el nombre correcto y se pasa la referencia al informe correcto +// assertEquals("issues",strCaptor.getValue(),"Se solicita el cálculo de la métrica adecuada"); +// assertEquals(reportTested,reportCaptor.getValue(),"Se pasa la referencia correcta del informe"); +// //Hago un test que asegure que el propio informe captura y gestiona la excepción de que el indicador no existe +// try { +// Mockito.doThrow(new IndicatorException("El indicador no existe")).when(indCalcMock).calcIndicator("indKO", reportTested); +// reportTested.calcIndicator("indKO"); +// +// } catch (IndicatorException e) { +// fail("La excepción la debe gestionar el propio informe"); +// } +// +// } /** * Test method for {@link us.muit.fs.a4i.model.entities.Report#setId(java.lang.String)}. */ - @Test - @Tag("noacabado") - void testSetId() { - fail("Not yet implemented"); // TODO - } +// @Test +// @Tag("noacabado") +// void testSetId() { +// fail("Not yet implemented"); // TODO +// } /** * Test method for {@link us.muit.fs.a4i.model.entities.Report#getId()}. */ @Test @Tag("noacabado") - void testGetId() { + void testGetEntityId() { fail("Not yet implemented"); // TODO } /** * Test method for {@link us.muit.fs.a4i.model.entities.Report#setIndicatorsCalculator(us.muit.fs.a4i.control.IndicatorsCalculator)}. */ - @Test - void testSetIndicatorsCalculator() { - //Definimos calculadora de tipo repositorio - Mockito.when(indCalcMock.getReportType()).thenReturn(ReportI.Type.REPOSITORY); - ReportI orgReport=new Report(ReportI.Type.ORGANIZATION); - ReportI repoReport=new Report(ReportI.Type.REPOSITORY); - ReportI report=new Report(); - //Vamos a probar establecer la calculadora en un informe que no tiene el tipo aún establecido (Debería tener el tipo de la calculadora al final) - //Para ello usamos report - try { - report.setIndicatorsCalculator(indCalcMock); - //Se ha tenido que consultar el tipo de calculadora - Mockito.verify(indCalcMock).getReportType(); - assertEquals(indCalcMock.getReportType(),report.getType()); - } catch (IndicatorException e) { - fail("No debería lanzar excepción"); - } - - //Vamos a probar a establecer la calculadora si el tipo de ambos coincide, uso repoReport - try { - repoReport.setIndicatorsCalculator(indCalcMock); - //Se ha tenido que consultar el tipo de calculadora - //Mockito.verify(indCalcMock, times(2)).getReportType(); - assertEquals(indCalcMock.getReportType(),repoReport.getType()); - } catch (IndicatorException e) { - fail("No debería lanzar excepción"); - } - - //Vamos a probar a establecer la calculadora si el tipo de la calculadora discrepa con el tipo del informe, uso orgReport +// @Test +// void testSetIndicatorsCalculator() { +// //Definimos calculadora de tipo repositorio +// Mockito.when(indCalcMock.getReportType()).thenReturn(ReportI.Type.REPOSITORY); +// ReportI orgReport=new Report(ReportI.Type.ORGANIZATION); +// ReportI repoReport=new Report(ReportI.Type.REPOSITORY); +// ReportI report=new Report(); +// //Vamos a probar establecer la calculadora en un informe que no tiene el tipo aún establecido (Debería tener el tipo de la calculadora al final) +// //Para ello usamos report +// try { +// report.setIndicatorsCalculator(indCalcMock); +// //Se ha tenido que consultar el tipo de calculadora +// Mockito.verify(indCalcMock).getReportType(); +// assertEquals(indCalcMock.getReportType(),report.getType()); +// } catch (IndicatorException e) { +// fail("No debería lanzar excepción"); +// } +// +// //Vamos a probar a establecer la calculadora si el tipo de ambos coincide, uso repoReport +// try { +// repoReport.setIndicatorsCalculator(indCalcMock); +// //Se ha tenido que consultar el tipo de calculadora +// //Mockito.verify(indCalcMock, times(2)).getReportType(); +// assertEquals(indCalcMock.getReportType(),repoReport.getType()); +// } catch (IndicatorException e) { +// fail("No debería lanzar excepción"); +// } +// +// //Vamos a probar a establecer la calculadora si el tipo de la calculadora discrepa con el tipo del informe, uso orgReport +// +// try { +// orgReport.setIndicatorsCalculator(indCalcMock); +// //Se ha tenido que consultar el tipo de calculadora +// fail("Debe saltar una excepción antes, no debería llegar aquí"); +// } catch (IndicatorException e) { +// +// log.info("Ha saltado la excepción de indicador"); +// //Suponga que los requisitos cambian, le piden que el mensaje debe ser "El tipo de la calculadora discrepa del tipo del informe" +// //Cambie el test para que lo verifique y ejecute ¿Qué ocurre? +// assertEquals(e.getMessage(),"La calculadora no concuerda con el tipo de informe","El mensaje es correcto"); +// } catch(Exception e) { +// fail("La excepción no es del tipo IndicatorException como se esperaba"); +// } +// //Esta verificación es para mostrar que se puede analizar también el comportamiento interno de la clase +// //en esta ocasión el número de veces que invoca a la calculadora durante el test +// //Probar a cambiar 5 por otro número y ejecutar +// Mockito.verify(indCalcMock,times(5)).getReportType(); +// +// } - try { - orgReport.setIndicatorsCalculator(indCalcMock); - //Se ha tenido que consultar el tipo de calculadora - fail("Debe saltar una excepción antes, no debería llegar aquí"); - } catch (IndicatorException e) { - - log.info("Ha saltado la excepción de indicador"); - //Suponga que los requisitos cambian, le piden que el mensaje debe ser "El tipo de la calculadora discrepa del tipo del informe" - //Cambie el test para que lo verifique y ejecute ¿Qué ocurre? - assertEquals(e.getMessage(),"La calculadora no concuerda con el tipo de informe","El mensaje es correcto"); - } catch(Exception e) { - fail("La excepción no es del tipo IndicatorException como se esperaba"); - } - //Esta verificación es para mostrar que se puede analizar también el comportamiento interno de la clase - //en esta ocasión el número de veces que invoca a la calculadora durante el test - //Probar a cambiar 5 por otro número y ejecutar - Mockito.verify(indCalcMock,times(5)).getReportType(); - - } - + //---------------------------------------------------------------------------------------------------------- /** * Test method for {@link us.muit.fs.a4i.model.entities.Report#toString()}. + * toString devuelve un tipo string de repoinfo formado por: + * "Informaci�n del Informe:\n - M�tricas: " + * "\n Clave: " + clave + metrics.get(clave) + * "\n Clave: " + clave + indicators.get(clave) */ @Test @Tag("noacabado") void testToString() { - fail("Not yet implemented"); // TODO + reportTested=new Report(); + String refString="referencia"; + try { + assertEquals(reportTested.toString().getClass(),refString.getClass(),"Comparación de tipo String"); + } catch (IndicatorException e) { + fail("ToString no devuelve un tipo String"); + } } + //---------------------------------------------------------------------------------------------------------- + /** * Test method for {@link us.muit.fs.a4i.model.entities.Report#getAllMetrics()}. @@ -316,7 +409,15 @@ void testToString() { @Test @Tag("noacabado") void testGetAllMetrics() { - fail("Not yet implemented"); // TODO + Metric metric= null; + reportTested=new Report(); + + try { + assertNull(reportTested.getAllMetrics(),"All metric Null"); + } catch (IndicatorException e) { + fail(�All Metrics Null� ); + } + } } void setMetricsMocks() { Date date=Date.from(LocalDateTime.now().toInstant(ZoneOffset.UTC)); @@ -325,8 +426,36 @@ void setMetricsMocks() { Mockito.when(metricIntMock.getValue()).thenReturn(3); Mockito.when(metricDatMock.getName()).thenReturn("lastPush"); - Mockito.when(metricDatMock.getDescription()).thenReturn("Último push realizado en el repositorio"); + Mockito.when(metricDatMock.getDescription()).thenReturn("Último push realizado en el repositorio"); Mockito.when(metricDatMock.getValue()).thenReturn(date); } + + + /** + * Test method for {@link us.muit.fs.a4i.model.entities.Report#getAllIndicators()}. + */ + @Test + @Tag("noacabado") + void testGetAllIndicators() { + Metric metric= null; + reportTested=new Report(); + try { + assertNull(reportTested.getAllIndicator(name),"All indicator Null"); + } catch (IndicatorException e) { + fail(�All Indicator Null� ); + } + } + } + void setIndicatorsMocks() { + Date date=Date.from(LocalDateTime.now().toInstant(ZoneOffset.UTC)); + Mockito.when(indicatorIntMock.getName()).thenReturn("issues"); + Mockito.when(indicatorIntMock.getDescription()).thenReturn("Tareas sin finalizar en el repositorio"); + Mockito.when(indicatorIntMock.getValue()).thenReturn(3); + + Mockito.when(indicatorDatMock.getName()).thenReturn("lastPush"); + Mockito.when(indicatorDatMock.getDescription()).thenReturn("Último push realizado en el repositorio"); + Mockito.when(indicatorDatMock.getValue()).thenReturn(date); + } + } From 7fe95f53b076dbf74a2d943aaff04cecb83e4b5e Mon Sep 17 00:00:00 2001 From: ivamatgon Date: Sat, 28 May 2022 11:35:28 +0200 Subject: [PATCH 017/100] Test Ivan --- .../persistence/ExcelReportManagerTest.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java diff --git a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java new file mode 100644 index 00000000..c2b17d3f --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java @@ -0,0 +1,65 @@ +/** + *

+ * Test para probar la clase ExcelReportManager + *

+ * + * @author Iván Matas González + * + */ +package us.muit.fs.a4i.test.persistence; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.logging.Logger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.verification.VerificationMode; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; + + +import us.muit.fs.a4i.exceptions.MetricException; +import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.Metric; +import us.muit.fs.a4i.persistence.ReportFormaterI; +import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.persistence.*; + +@ExtendWith(MockitoExtension.class) + +class ExcelReportManager { + private static Logger log = Logger.getLogger(ExcelReportManager.class.getName()); + + @Captor + private ArgumentCaptor intCaptor; + @Captor + private ArgumentCaptor strCaptor; + @Captor + private ArgumentCaptor FormaterCaptor; + + @Mock(serializable = true) + private static Metric metricIntMock= Mockito.mock(Metric.class); + @Mock(serializable = true) + private static Metric metricStrMock= Mockito.mock(Metric.class); + + @Test + void ExcelCreation() { + + } + + +} \ No newline at end of file From 2111c44bdc42e5070eb6e046281991b0f13a0c4f Mon Sep 17 00:00:00 2001 From: JacintoJT Date: Sun, 29 May 2022 11:53:07 +0200 Subject: [PATCH 018/100] Doing testSetAppIndicators --- .../muit/fs/a4i/test/config/IndicatorConfigurationTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java index ac44b15d..269851d1 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java @@ -20,6 +20,7 @@ class IndicatorConfigurationTest { private static Logger log = Logger.getLogger(IndicatorConfigurationTest.class.getName()); static IndicatorConfiguration underTest; static String appConfPath; + String appIndicatorsPath = "/test/home"; @BeforeAll static void setUpBeforeClass() throws Exception { @@ -115,7 +116,8 @@ void testDefinedIndicator() { @Test void testSetAppIndicators() { - fail("Not yet implemented"); + String AppIndicators = appIndicatorsPath; + assertEquals(AppIndicators, appIndicatorsPath, "Deberian ser iguales"); } @Test From c5da4bac0e283b5901f5a1b3512f8a1a29710eb5 Mon Sep 17 00:00:00 2001 From: Bilel Date: Sun, 29 May 2022 15:46:43 +0200 Subject: [PATCH 019/100] First step of work --- .../fs/a4i/config/MetricConfigurationI.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java diff --git a/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java b/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java new file mode 100644 index 00000000..0218b996 --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java @@ -0,0 +1,39 @@ +/** + *

Algoritmos para el c�lculo de indicadores espec�ficos al tipo de informe

+ */ +package us.muit.fs.a4i.control; + +import us.muit.fs.a4i.exceptions.IndicatorException; +import us.muit.fs.a4i.model.entities.ReportI; + +/** + * + *

Define los m�todos para calcular cada indicador y a�adirlo al informe

+ *

Puede hacerse uno a uno o todos a la vez

+ *

Las clases que la implementen ser�n espec�fias para un tipo de informe

+ * @author Isabel Rom�n + * + */ +public interface IndicatorsCalculator { + /** + *

Calcula el indicador con el nombre que se pasa y lo incluye en el informe + * Si las m�tricas que necesita no est�n en el informe las busca y las a�ade

+ * @param name Nombre del indicador a c�lcular + * @param report Informe sobre el que realizar el c�lculo + * @throws IndicatorException Si el indicador no est� definido en la calculadora + */ + + public void calcIndicator(String name,ReportI report) throws IndicatorException; + /** + *

Calcula todos los indicadores configurados para el tipo de informe que se pasa. Debe verificar primero que el tipo de informe que se pasa es correcto

+ * @param report Informe sobre el que realizar el c�lculo + * @throws IndicatorException Si el tipo del informe no coincide con el de la calculadora + */ + public void calcAllIndicators(ReportI report) throws IndicatorException; + + /** + * Devuelve el tipo de informe que maneja esta calculadora de indicadores + * @return El tipo de informes + */ + public ReportI.Type getReportType(); +} \ No newline at end of file From 5e7a761ce868aa2eed7c0bb19dbaa18fb2c2152d Mon Sep 17 00:00:00 2001 From: Antoine Date: Sun, 29 May 2022 17:01:17 +0200 Subject: [PATCH 020/100] test commit --- .../java/us/muit/fs/a4i/config/Checker.java | 184 +----------------- .../fs/a4i/config/MetricConfiguration.java | 75 +++++++ .../a4i/config/doc-files/newConfigPackage.gif | Bin 0 -> 146064 bytes 3 files changed, 85 insertions(+), 174 deletions(-) create mode 100644 src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java create mode 100644 src/main/java/us/muit/fs/a4i/config/doc-files/newConfigPackage.gif diff --git a/src/main/java/us/muit/fs/a4i/config/Checker.java b/src/main/java/us/muit/fs/a4i/config/Checker.java index 76b4896e..40adb46d 100644 --- a/src/main/java/us/muit/fs/a4i/config/Checker.java +++ b/src/main/java/us/muit/fs/a4i/config/Checker.java @@ -1,6 +1,3 @@ -/** - * - */ package us.muit.fs.a4i.config; import java.io.BufferedReader; @@ -10,181 +7,17 @@ import java.io.InputStreamReader; import java.util.HashMap; import java.util.logging.Logger; +import us.muit.fs.a4i.config.MetricConfiguration; +import us.muit.fs.a4i.config.IndicatorConfiguration; import javax.json.*; -/** - *

- * Clase para verificar las métricas e indicadores - *

- *

- * La API incluye sus métricas e indicadores por defecto en la carpeta - * resources, en el fichero a4iDefault.json - *

- *

- * Si la aplicación cliente crea nuevas métricas o indicadores las guarda en un - * fichero json de configuración, que tendrá que indicársele al Checker - *

- *

- * Deuda técnica: analizar la posibilidad de leer sólo una vez y mantener en - * memoria - *

- * - * @author Isabel Román - * - */ public class Checker { - private static Logger log = Logger.getLogger(Checker.class.getName()); - private String a4iMetrics = "a4iDefault.json"; - /* - * Definir la ruta completa del fichero de configuración - */ - private String appMetrics = null; - - public void setAppMetrics(String appMetricsPath) { - appMetrics = appMetricsPath; - } - - /** - *

- * Comprueba si la métrica está definida en el fichero por defecto o en el de la - * aplicación cliente - *

- *

- * También verifica que el tipo es el adecuado - *

- * - * @param metricName nombre de la métrica que se quiere comprobar - * @param metricType tipo de la métrica - * @return metricDefinition Si la métrica está definida y el tipo es correcto se devuelve un mapa con las unidades y la descripción - * @throws FileNotFoundException Si no se localiza el fichero de configuración - */ - public HashMap definedMetric(String metricName, String metricType) throws FileNotFoundException { - log.info("Checker solicitud de búsqueda métrica " + metricName); - - HashMap metricDefinition=null; - - String filePath="/"+a4iMetrics; - log.info("Buscando el archivo " + filePath); - InputStream is=this.getClass().getResourceAsStream(filePath); - log.info("InputStream "+is+" para "+filePath); - InputStreamReader isr = new InputStreamReader(is); - - - metricDefinition = isDefinedMetric(metricName, metricType, isr); - if ((metricDefinition==null) && appMetrics != null) { - is=new FileInputStream(appMetrics); - isr=new InputStreamReader(is); - metricDefinition = isDefinedMetric(metricName, metricType, isr); - } - - return metricDefinition; - } - - private HashMap isDefinedMetric(String metricName, String metricType, InputStreamReader isr) - throws FileNotFoundException { - - HashMap metricDefinition=null; - - - JsonReader reader = Json.createReader(isr); - log.info("Creo el JsonReader"); - - JsonObject confObject = reader.readObject(); - log.info("Leo el objeto"); - reader.close(); - - log.info("Muestro la configuración leída " + confObject); - JsonArray metrics = confObject.getJsonArray("metrics"); - log.info("El número de métricas es " + metrics.size()); - for (int i = 0; i < metrics.size(); i++) { - log.info("nombre: " + metrics.get(i).asJsonObject().getString("name")); - if (metrics.get(i).asJsonObject().getString("name").equals(metricName)) { - log.info("Localizada la métrica"); - log.info("tipo: " + metrics.get(i).asJsonObject().getString("type")); - if (metrics.get(i).asJsonObject().getString("type").equals(metricType)) { - metricDefinition=new HashMap(); - metricDefinition.put("description", metrics.get(i).asJsonObject().getString("description")); - metricDefinition.put("unit", metrics.get(i).asJsonObject().getString("unit")); - } - - } - } - - return metricDefinition; - } - - /** - *

- * Comprueba si el indicador está definido en el fichero por defecto o en el de - * la aplicación cliente - *

- *

- * También verifica que el tipo es el adecuado - *

- * - * @param indicatorName nombre del indicador que se quiere comprobar - * @param indicatorType tipo del indicador - * @return indicatorDefinition Si el indicador está definido y el tipo es correcto se devuelve un mapa con las unidades y la descripción - * @throws FileNotFoundException Si no se localiza el fichero de configuración - */ - public HashMap definedIndicator(String indicatorName, String indicatorType) throws FileNotFoundException { - HashMap indicatorDefinition=null; - log.info("Checker solicitud de búsqueda indicador " + indicatorName); - boolean defined = false; - - - String filePath="/"+a4iMetrics; - log.info("Buscando el archivo " + filePath); - InputStream is=this.getClass().getResourceAsStream(filePath); - log.info("InputStream "+is+" para "+filePath); - InputStreamReader isr = new InputStreamReader(is); - - - - indicatorDefinition = isDefinedIndicator(indicatorName, indicatorType, isr); - if ((indicatorDefinition==null) && appMetrics != null) { - is=new FileInputStream(appMetrics); - isr=new InputStreamReader(is); - indicatorDefinition = isDefinedIndicator(indicatorName, indicatorType, isr); - } - return indicatorDefinition; - } - - private HashMap isDefinedIndicator(String indicatorName, String indicatorType, InputStreamReader isr) - throws FileNotFoundException { - HashMap indicatorDefinition=null; - - JsonReader reader = Json.createReader(isr); - log.info("Creo el JsonReader"); - - JsonObject confObject = reader.readObject(); - log.info("Leo el objeto"); - reader.close(); - - log.info("Muestro la configuración leída " + confObject); - JsonArray indicators = confObject.getJsonArray("indicators"); - log.info("El número de indicadores es " + indicators.size()); - for (int i = 0; i < indicators.size(); i++) { - log.info("nombre: " + indicators.get(i).asJsonObject().getString("name")); - if (indicators.get(i).asJsonObject().getString("name").equals(indicatorName)) { - log.info("Localizado el indicador"); - log.info("tipo: " + indicators.get(i).asJsonObject().getString("type")); - if (indicators.get(i).asJsonObject().getString("type").equals(indicatorType)) { - indicatorDefinition=new HashMap(); - indicatorDefinition.put("description", indicators.get(i).asJsonObject().getString("description")); - indicatorDefinition.put("unit", indicators.get(i).asJsonObject().getString("unit")); - } - - } - } - - return indicatorDefinition; - } + private static Logger log = Logger.getLogger(Checker.class.getName()); public HashMap getMetricInfo(String metricName) throws FileNotFoundException { - log.info("Checker solicitud de búsqueda detalles de la métrica " + metricName); + log.info("Checker solicitud de b�squeda detalles de la m�trica " + metricName); HashMap metricDefinition=null; String filePath="/"+a4iMetrics; @@ -217,13 +50,13 @@ private HashMap getMetricInfo(String metricName, InputStreamReade log.info("Leo el objeto"); reader.close(); - log.info("Muestro la configuración leída " + confObject); + log.info("Muestro la configuraci�n le�da " + confObject); JsonArray metrics = confObject.getJsonArray("metrics"); - log.info("El número de métricas es " + metrics.size()); + log.info("El n�mero de m�tricas es " + metrics.size()); for (int i = 0; i < metrics.size(); i++) { log.info("nombre: " + metrics.get(i).asJsonObject().getString("name")); if (metrics.get(i).asJsonObject().getString("name").equals(metricName)) { - log.info("Localizada la métrica"); + log.info("Localizada la m�trica"); log.info("tipo: " + metrics.get(i).asJsonObject().getString("type")); metricDefinition=new HashMap(); metricDefinition.put("name", metrics.get(i).asJsonObject().getString("name")); @@ -237,4 +70,7 @@ private HashMap getMetricInfo(String metricName, InputStreamReade return metricDefinition; } + private IndicatorsConfigurationI indConf; + private MetricsConfigurationI metricConf; + } diff --git a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java new file mode 100644 index 00000000..7370b351 --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java @@ -0,0 +1,75 @@ + +package us.muit.fs.a4i.control; + +import us.muit.fs.a4i.exceptions.IndicatorException; +import us.muit.fs.a4i.model.entities.ReportI; + +public class MetricConfiguration { + + private String a4iMetrics = "a4iDefault.json"; + private String appMetrics = null; + + private HashMap isDefinedMetric(String metricName, String metricType, InputStreamReader isr) + throws FileNotFoundException { + + HashMap metricDefinition=null; + + + JsonReader reader = Json.createReader(isr); + log.info("Creo el JsonReader"); + + JsonObject confObject = reader.readObject(); + log.info("Leo el objeto"); + reader.close(); + + log.info("Muestro la configuraci�n le�da " + confObject); + JsonArray metrics = confObject.getJsonArray("metrics"); + log.info("El n�mero de m�tricas es " + metrics.size()); + for (int i = 0; i < metrics.size(); i++) { + log.info("nombre: " + metrics.get(i).asJsonObject().getString("name")); + if (metrics.get(i).asJsonObject().getString("name").equals(metricName)) { + log.info("Localizada la m�trica"); + log.info("tipo: " + metrics.get(i).asJsonObject().getString("type")); + if (metrics.get(i).asJsonObject().getString("type").equals(metricType)) { + metricDefinition=new HashMap(); + metricDefinition.put("description", metrics.get(i).asJsonObject().getString("description")); + metricDefinition.put("unit", metrics.get(i).asJsonObject().getString("unit")); + } + + } + } + + return metricDefinition; + } + + public interface MetricConfigurationI { + + public void setAppMetrics(String appMetricsPath) { + appMetrics = appMetricsPath; + } + + public HashMap definedMetric(String metricName, String metricType) throws FileNotFoundException { + log.info("Checker solicitud de búsqueda métrica " + metricName); + + HashMap metricDefinition=null; + + String filePath="/"+a4iMetrics; + log.info("Buscando el archivo " + filePath); + InputStream is=this.getClass().getResourceAsStream(filePath); + log.info("InputStream "+is+" para "+filePath); + InputStreamReader isr = new InputStreamReader(is); + + + metricDefinition = isDefinedMetric(metricName, metricType, isr); + if ((metricDefinition==null) && appMetrics != null) { + is=new FileInputStream(appMetrics); + isr=new InputStreamReader(is); + metricDefinition = isDefinedMetric(metricName, metricType, isr); + } + + return metricDefinition; + } + + + } +} \ No newline at end of file diff --git a/src/main/java/us/muit/fs/a4i/config/doc-files/newConfigPackage.gif b/src/main/java/us/muit/fs/a4i/config/doc-files/newConfigPackage.gif new file mode 100644 index 0000000000000000000000000000000000000000..f55ca844e640372850f45e6b2270a39961021fc1 GIT binary patch literal 146064 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

~$zxtXtVCJIPEfc!aSCdVv+J4%~-=%JkaW+V-}% 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 TQQsWiB*z%?sBEV}1Oxy(v7)Ya literal 0 HcmV?d00001 From b1caded999d8f4aa81762f8be1c3ddda36510d1f Mon Sep 17 00:00:00 2001 From: Bilel Date: Sun, 29 May 2022 17:36:32 +0200 Subject: [PATCH 021/100] Correction two files (Checker + MetricConfiguration) --- .../java/us/muit/fs/a4i/config/Checker.java | 12 ++---- .../fs/a4i/config/MetricConfiguration.java | 16 ++++++-- .../fs/a4i/config/MetricConfigurationI.java | 39 ------------------- 3 files changed, 15 insertions(+), 52 deletions(-) delete mode 100644 src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java diff --git a/src/main/java/us/muit/fs/a4i/config/Checker.java b/src/main/java/us/muit/fs/a4i/config/Checker.java index 40adb46d..ab641aa3 100644 --- a/src/main/java/us/muit/fs/a4i/config/Checker.java +++ b/src/main/java/us/muit/fs/a4i/config/Checker.java @@ -12,8 +12,9 @@ import javax.json.*; -public class Checker { - +public class Checker(IndicatorsConfiguration indConf, MetricsConfiguration metricConf){ + private String a4iMetrics = "a4iDefault.json"; + private String appMetrics = null; private static Logger log = Logger.getLogger(Checker.class.getName()); public HashMap getMetricInfo(String metricName) throws FileNotFoundException { @@ -33,7 +34,6 @@ public HashMap getMetricInfo(String metricName) throws FileNotFou isr=new InputStreamReader(is); metricDefinition = getMetricInfo(metricName, isr); } - return metricDefinition; } @@ -63,14 +63,8 @@ private HashMap getMetricInfo(String metricName, InputStreamReade metricDefinition.put("description", metrics.get(i).asJsonObject().getString("description")); metricDefinition.put("unit", metrics.get(i).asJsonObject().getString("unit")); metricDefinition.put("type", metrics.get(i).asJsonObject().getString("type")); - } } - return metricDefinition; } - - private IndicatorsConfigurationI indConf; - private MetricsConfigurationI metricConf; - } diff --git a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java index 7370b351..72936f41 100644 --- a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java @@ -1,6 +1,15 @@ package us.muit.fs.a4i.control; +import java.util.HashMap; +import java.io.FileNotFoundException; +import java.util.logging.Logger; +import javax.json.JsonReader; +import java.util.logging.Logger; +import javax.json.JsonObject; +import javax.json.JsonArray; +import java.io.InputStreamReader; + import us.muit.fs.a4i.exceptions.IndicatorException; import us.muit.fs.a4i.model.entities.ReportI; @@ -8,6 +17,7 @@ public class MetricConfiguration { private String a4iMetrics = "a4iDefault.json"; private String appMetrics = null; + private static Logger log = Logger.getLogger(Checker.class.getName()); private HashMap isDefinedMetric(String metricName, String metricType, InputStreamReader isr) throws FileNotFoundException { @@ -68,8 +78,6 @@ public HashMap definedMetric(String metricName, String metricType } return metricDefinition; - } - - - } + } + } } \ No newline at end of file diff --git a/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java b/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java deleted file mode 100644 index 0218b996..00000000 --- a/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - *

Algoritmos para el c�lculo de indicadores espec�ficos al tipo de informe

- */ -package us.muit.fs.a4i.control; - -import us.muit.fs.a4i.exceptions.IndicatorException; -import us.muit.fs.a4i.model.entities.ReportI; - -/** - * - *

Define los m�todos para calcular cada indicador y a�adirlo al informe

- *

Puede hacerse uno a uno o todos a la vez

- *

Las clases que la implementen ser�n espec�fias para un tipo de informe

- * @author Isabel Rom�n - * - */ -public interface IndicatorsCalculator { - /** - *

Calcula el indicador con el nombre que se pasa y lo incluye en el informe - * Si las m�tricas que necesita no est�n en el informe las busca y las a�ade

- * @param name Nombre del indicador a c�lcular - * @param report Informe sobre el que realizar el c�lculo - * @throws IndicatorException Si el indicador no est� definido en la calculadora - */ - - public void calcIndicator(String name,ReportI report) throws IndicatorException; - /** - *

Calcula todos los indicadores configurados para el tipo de informe que se pasa. Debe verificar primero que el tipo de informe que se pasa es correcto

- * @param report Informe sobre el que realizar el c�lculo - * @throws IndicatorException Si el tipo del informe no coincide con el de la calculadora - */ - public void calcAllIndicators(ReportI report) throws IndicatorException; - - /** - * Devuelve el tipo de informe que maneja esta calculadora de indicadores - * @return El tipo de informes - */ - public ReportI.Type getReportType(); -} \ No newline at end of file From b67fe0e6b1bb33255ca86d3b14943523b6ad389f Mon Sep 17 00:00:00 2001 From: juapalesp <101708343+juapalesp@users.noreply.github.com> Date: Thu, 2 Jun 2022 18:48:04 +0200 Subject: [PATCH 022/100] Update pruebas.yml --- .github/workflows/pruebas.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/pruebas.yml b/.github/workflows/pruebas.yml index f7ae993e..8adc43db 100644 --- a/.github/workflows/pruebas.yml +++ b/.github/workflows/pruebas.yml @@ -26,3 +26,15 @@ jobs: run: | chmod +x gradlew ./gradlew build +  Test: +    runs-on: ubuntu-latest +    env: +      GITHUB_LOGIN: ${{ github.actor }} +      GITHUB_PACKAGES: ${{ secrets.GITHUB_TOKEN }} +      GITHUB_OAUTH: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Ejecutando los test del código + run: | + chmod +x gradlew + ./gradlew test + From 472d6a1c155d429b11b2796a236fd5eec1559a7a Mon Sep 17 00:00:00 2001 From: juapalesp <101708343+juapalesp@users.noreply.github.com> Date: Thu, 2 Jun 2022 18:53:59 +0200 Subject: [PATCH 023/100] Update pruebas.yml --- .github/workflows/pruebas.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pruebas.yml b/.github/workflows/pruebas.yml index 8adc43db..83b5ad98 100644 --- a/.github/workflows/pruebas.yml +++ b/.github/workflows/pruebas.yml @@ -28,6 +28,7 @@ jobs: ./gradlew build   Test:     runs-on: ubuntu-latest + needs: Build     env:       GITHUB_LOGIN: ${{ github.actor }}       GITHUB_PACKAGES: ${{ secrets.GITHUB_TOKEN }} From 92845ef392ce9f928f9e822e201043b4c77a865c Mon Sep 17 00:00:00 2001 From: juapalesp <101708343+juapalesp@users.noreply.github.com> Date: Thu, 2 Jun 2022 19:00:59 +0200 Subject: [PATCH 024/100] Version Final pruebas.yml --- .github/workflows/pruebas.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pruebas.yml b/.github/workflows/pruebas.yml index 83b5ad98..7842a087 100644 --- a/.github/workflows/pruebas.yml +++ b/.github/workflows/pruebas.yml @@ -28,7 +28,7 @@ jobs: ./gradlew build   Test:     runs-on: ubuntu-latest - needs: Build +    needs: Build     env:       GITHUB_LOGIN: ${{ github.actor }}       GITHUB_PACKAGES: ${{ secrets.GITHUB_TOKEN }} From 4c7f6aa0b48299fe9c67d98436a65bdc6f7d52f3 Mon Sep 17 00:00:00 2001 From: juapalesp <101708343+juapalesp@users.noreply.github.com> Date: Thu, 2 Jun 2022 19:04:56 +0200 Subject: [PATCH 025/100] Rename .github/workflows/pruebas.yml to pruebas.yml --- .github/workflows/pruebas.yml => pruebas.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/pruebas.yml => pruebas.yml (100%) diff --git a/.github/workflows/pruebas.yml b/pruebas.yml similarity index 100% rename from .github/workflows/pruebas.yml rename to pruebas.yml From ae31c2eb85013a0073e0a908c4276e5609d9b608 Mon Sep 17 00:00:00 2001 From: juapalesp <101708343+juapalesp@users.noreply.github.com> Date: Thu, 2 Jun 2022 19:13:42 +0200 Subject: [PATCH 026/100] Version final --- pruebas.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/pruebas.yml b/pruebas.yml index 7842a087..dc6a8e18 100644 --- a/pruebas.yml +++ b/pruebas.yml @@ -33,7 +33,6 @@ jobs:       GITHUB_LOGIN: ${{ github.actor }}       GITHUB_PACKAGES: ${{ secrets.GITHUB_TOKEN }}       GITHUB_OAUTH: ${{ secrets.GITHUB_TOKEN }} - steps: - name: Ejecutando los test del código run: | chmod +x gradlew From e51ed6a7d8fd213b6bbe180e6b60d3c230f7839a Mon Sep 17 00:00:00 2001 From: estheruly1998 <101709135+estheruly1998@users.noreply.github.com> Date: Fri, 3 Jun 2022 18:52:14 +0200 Subject: [PATCH 027/100] Update build.gradle --- build.gradle | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index 5c042dc4..1a22fad3 100644 --- a/build.gradle +++ b/build.gradle @@ -11,16 +11,15 @@ plugins { // Apply the java-library plugin for API and implementation separation. id 'java-library' - //Añado el plugin para eclipse + //Añado el plugin para eclipse id 'eclipse' //para poder publicar paquetes en github id 'maven-publish' - //Plugin para análisis estático de código + //Plugin para análisis estático de código id "nebula.lint" version "17.7.0" } -<<<<<<< HEAD + //version = '2.0' -======= //Para publicar paquetes en github //group = 'A4I' publishing { @@ -29,7 +28,7 @@ publishing { name = "GitHubPackages" url = uri("https://maven.pkg.github.com/mit-fs/audit4improve-api") credentials { - //las propiedades gpr.user y gpr.key están configuradas en gradle.properties en el raiz del proyecto, y se añade a .gitignore para que no se suban + //las propiedades gpr.user y gpr.key están configuradas en gradle.properties en el raiz del proyecto, y se añade a .gitignore para que no se suban //O bien configuro las variables de entorno GITHUB_LOGIN y GITHUB_PACKAGES username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_LOGIN") password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_PACKAGES") @@ -50,17 +49,16 @@ publishing { } } version = '0.0' ->>>>>>> fa6b9a1cda87f8fa8ee688441d387b58f6bee593 tasks.withType(JavaCompile) { - //Añadir la opción Xlint + //Añadir la opción Xlint options.deprecation = true options.encoding = 'ISO-8859-1' } tasks.withType(Javadoc){ - description = "Genera la documentación" - //indicar que la codificación es ISO + description = "Genera la documentación" + //indicar que la codificación es ISO options.encoding = 'ISO-8859-1' options.charSet = 'ISO-8859-1' options.author = true @@ -91,7 +89,7 @@ repositories { dependencies { // This dependency is exported to consumers, that is to say found on their compile classpath. api 'org.apache.commons:commons-math3:3.6.1' - //Añado la dependencia de la librería github que vamos a usar + //Añado la dependencia de la librería github que vamos a usar // https://mvnrepository.com/artifact/org.kohsuke/github-api //JAVADOC: https://github-api.kohsuke.org/apidocs/index.html @@ -100,7 +98,7 @@ dependencies { // https://mvnrepository.com/artifact/org.apache.poi/poi //JAVADOC: https://poi.apache.org/apidocs/5.0/ implementation 'org.apache.poi:poi:5.2.1' - //Para leer la configuración como ficheros con datos en formato json + //Para leer la configuración como ficheros con datos en formato json // https://mvnrepository.com/artifact/javax.json/javax.json-api //JAVADOC: https://javadoc.io/doc/org.glassfish/javax.json/latest/overview-summary.html implementation group: 'javax.json', name: 'javax.json-api', version: '1.1.4' @@ -112,7 +110,7 @@ dependencies { // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:30.1.1-jre' - //Añado para usar mockito + //Añado para usar mockito //JAVADOC: https://javadoc.io/doc/org.mockito/mockito-core/4.3.1/overview-summary.html testImplementation 'org.mockito:mockito-core:4.3.1' // https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter From d5d3bdeb9aea9b627a31213f990c072f2715ee70 Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 9 Jun 2022 09:47:28 +0200 Subject: [PATCH 028/100] Nueva rama --- build.gradle | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index 876e5931..eab06348 100644 --- a/build.gradle +++ b/build.gradle @@ -11,24 +11,24 @@ plugins { // Apply the java-library plugin for API and implementation separation. id 'java-library' - //Añado el plugin para eclipse + //A�ado el plugin para eclipse id 'eclipse' //para poder publicar paquetes en github id 'maven-publish' - //Plugin para análisis estático de código + //Plugin para an�lisis est�tico de c�digo id "nebula.lint" version "17.7.0" } -version = '0.0' +//version = '0.1' tasks.withType(JavaCompile) { - //Añadir la opción Xlint + //A�adir la opci�n Xlint options.deprecation = true options.encoding = 'ISO-8859-1' } tasks.withType(Javadoc){ - description = "Genera la documentación" - //indicar que la codificación es ISO + description = "Genera la documentaci�n" + //indicar que la codificaci�n es ISO options.encoding = 'ISO-8859-1' options.charSet = 'ISO-8859-1' options.author = true @@ -59,7 +59,7 @@ repositories { dependencies { // This dependency is exported to consumers, that is to say found on their compile classpath. api 'org.apache.commons:commons-math3:3.6.1' - //Añado la dependencia de la librería github que vamos a usar + //A�ado la dependencia de la librer�a github que vamos a usar // https://mvnrepository.com/artifact/org.kohsuke/github-api //JAVADOC: https://github-api.kohsuke.org/apidocs/index.html @@ -68,7 +68,7 @@ dependencies { // https://mvnrepository.com/artifact/org.apache.poi/poi //JAVADOC: https://poi.apache.org/apidocs/5.0/ implementation 'org.apache.poi:poi:5.2.1' - //Para leer la configuración como ficheros con datos en formato json + //Para leer la configuraci�n como ficheros con datos en formato json // https://mvnrepository.com/artifact/javax.json/javax.json-api //JAVADOC: https://javadoc.io/doc/org.glassfish/javax.json/latest/overview-summary.html implementation group: 'javax.json', name: 'javax.json-api', version: '1.1.4' @@ -80,7 +80,7 @@ dependencies { // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:30.1.1-jre' - //Añado para usar mockito + //A�ado para usar mockito //JAVADOC: https://javadoc.io/doc/org.mockito/mockito-core/4.3.1/overview-summary.html testImplementation 'org.mockito:mockito-core:4.3.1' // https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter @@ -107,9 +107,9 @@ publishing { repositories { maven { name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/mit-fs/audit4improve-api") + url = uri("https://maven.pkg.github.com/antaramol/audit4improve-api") credentials { - //las propiedades gpr.user y gpr.key están configuradas en gradle.properties en el raiz del proyecto, y se añade a .gitignore para que no se suban + //las propiedades gpr.user y gpr.key est�n configuradas en gradle.properties en el raiz del proyecto, y se a�ade a .gitignore para que no se suban //O bien configuro las variables de entorno GITHUB_LOGIN y GITHUB_PACKAGES username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_LOGIN") password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_PACKAGES") @@ -122,7 +122,7 @@ publishing { groupId = 'us.mitfs.samples' artifactId = 'a4i' - version = '0.0' + version = '0.0.1-antaramol' from components.java } From ee23b9d21dd635c68c8a31373012042bb53c7368 Mon Sep 17 00:00:00 2001 From: juapalesp Date: Tue, 14 Jun 2022 02:14:56 +0200 Subject: [PATCH 029/100] =?UTF-8?q?a=C3=B1adido=20contextoo.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contextoo.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 contextoo.txt diff --git a/contextoo.txt b/contextoo.txt new file mode 100644 index 00000000..3ba19e9a --- /dev/null +++ b/contextoo.txt @@ -0,0 +1 @@ +goliii \ No newline at end of file From ecd0f590bcad890e43dc04dcc7feebe211545522 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Thu, 29 Dec 2022 11:02:33 +0100 Subject: [PATCH 030/100] Terminados issues #44 y #45 --- .../java/us/muit/fs/a4i/config/Checker.java | 87 +++++++++++++++++++ .../muit/fs/a4i/control/ReportFormaterI.java | 12 +++ .../us/muit/fs/a4i/model/entities/Report.java | 24 ++--- .../muit/fs/a4i/model/entities/ReportI.java | 10 +-- .../fs/a4i/model/entities/ReportItem.java | 44 ++++++---- .../fs/a4i/model/entities/ReportItemI.java | 6 ++ .../remote/GitHubRepositoryEnquirer.java | 46 ++++++---- .../fs/a4i/model/remote/RemoteEnquirer.java | 3 +- .../a4i/persistence/ExcelReportManager.java | 7 +- .../test/model/entities/ReportItemTest.java | 12 +-- .../a4i/test/model/entities/ReportTest.java | 9 +- src/test/resources/appConfTest.json | 6 +- 12 files changed, 198 insertions(+), 68 deletions(-) create mode 100644 src/main/java/us/muit/fs/a4i/control/ReportFormaterI.java diff --git a/src/main/java/us/muit/fs/a4i/config/Checker.java b/src/main/java/us/muit/fs/a4i/config/Checker.java index 76b4896e..8864af2b 100644 --- a/src/main/java/us/muit/fs/a4i/config/Checker.java +++ b/src/main/java/us/muit/fs/a4i/config/Checker.java @@ -45,6 +45,93 @@ public void setAppMetrics(String appMetricsPath) { appMetrics = appMetricsPath; } + + /** + *

+ * Comprueba si el elemento está definido en el fichero por defecto o en el de + * la aplicación cliente + *

+ *

+ * También verifica que el tipo es el adecuado + *

+ * + * @param indicatorName nombre del indicador que se quiere comprobar + * @param indicatorType tipo del indicador + * @return indicatorDefinition Si el indicador está definido y el tipo es correcto se devuelve un mapa con las unidades y la descripción + * @throws FileNotFoundException Si no se localiza el fichero de configuración + */ + public HashMap definedReportItem(String name, String type) throws FileNotFoundException { + HashMap itemDefinition=null; + log.info("Checker solicitud de búsqueda del elemento " + name); + boolean defined = false; + + + String filePath="/"+a4iMetrics; + log.info("Buscando el archivo " + filePath); + InputStream is=this.getClass().getResourceAsStream(filePath); + log.info("InputStream "+is+" para "+filePath); + InputStreamReader isr = new InputStreamReader(is); + + + + itemDefinition = isDefinedReportItem(name, type, isr); + if ((itemDefinition==null) && appMetrics != null) { + is=new FileInputStream(appMetrics); + isr=new InputStreamReader(is); + itemDefinition = isDefinedReportItem(name, type, isr); + } + + return itemDefinition; + } + + private HashMap isDefinedReportItem(String name, String type, InputStreamReader isr) + throws FileNotFoundException { + HashMap itemDefinition=null; + + JsonReader reader = Json.createReader(isr); + log.info("Creo el JsonReader"); + + JsonObject confObject = reader.readObject(); + log.info("Leo el objeto"); + reader.close(); + + log.info("Muestro la configuración leída " + confObject); + JsonArray items = confObject.getJsonArray("indicators"); + log.info("El número de indicadores es " + items.size()); + for (int i = 0; i < items.size(); i++) { + log.info("nombre: " + items.get(i).asJsonObject().getString("name")); + if (items.get(i).asJsonObject().getString("name").equals(name)) { + log.info("Localizado el elemento"); + log.info("tipo: " + items.get(i).asJsonObject().getString("type")); + if (items.get(i).asJsonObject().getString("type").equals(type)) { + itemDefinition=new HashMap(); + itemDefinition.put("description", items.get(i).asJsonObject().getString("description")); + itemDefinition.put("unit", items.get(i).asJsonObject().getString("unit")); + } + + } + } + + items = confObject.getJsonArray("metrics"); + log.info("El número de métricas es " + items.size()); + for (int i = 0; i < items.size(); i++) { + log.info("nombre: " + items.get(i).asJsonObject().getString("name")); + if (items.get(i).asJsonObject().getString("name").equals(name)) { + log.info("Localizado el elemento"); + log.info("tipo: " + items.get(i).asJsonObject().getString("type")); + if (items.get(i).asJsonObject().getString("type").equals(type)) { + itemDefinition=new HashMap(); + itemDefinition.put("description", items.get(i).asJsonObject().getString("description")); + itemDefinition.put("unit", items.get(i).asJsonObject().getString("unit")); + } + + } + } + + return itemDefinition; + } + + /** *

* Comprueba si la métrica está definida en el fichero por defecto o en el de la diff --git a/src/main/java/us/muit/fs/a4i/control/ReportFormaterI.java b/src/main/java/us/muit/fs/a4i/control/ReportFormaterI.java new file mode 100644 index 00000000..1b4c0078 --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/control/ReportFormaterI.java @@ -0,0 +1,12 @@ +/** + * + */ +package us.muit.fs.a4i.control; + +/** + * @author isa + * + */ +public interface ReportFormaterI { + +} diff --git a/src/main/java/us/muit/fs/a4i/model/entities/Report.java b/src/main/java/us/muit/fs/a4i/model/entities/Report.java index a18d1d06..0ee65f7d 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/Report.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/Report.java @@ -40,13 +40,13 @@ public class Report implements ReportI { * * */ - private HashMap metrics; + private HashMap metrics; /** * Mapa de indicadores */ - private HashMap indicators; + private HashMap indicators; public Report(){ createMaps(); @@ -66,8 +66,8 @@ public Report(Type type,String id){ this.id=id; } private void createMaps() { - metrics=new HashMap(); - indicators=new HashMap(); + metrics=new HashMap(); + indicators=new HashMap(); } /** *

Busca la métrica solicita en el informe y la devuelve

@@ -76,9 +76,9 @@ private void createMaps() { * @return la métrica localizada */ @Override - public Metric getMetricByName(String name) { + public ReportItem getMetricByName(String name) { log.info("solicitada métrica de nombre "+name); - Metric metric=null; + ReportItem metric=null; if (metrics.containsKey(name)){ log.info("La métrica está en el informe"); @@ -91,7 +91,7 @@ public Metric getMetricByName(String name) { */ @Override - public void addMetric(Metric met) { + public void addMetric(ReportItem met) { metrics.put(met.getName(), met); log.info("Añadida métrica "+met+" Con nombre "+met.getName()); } @@ -102,9 +102,9 @@ public void addMetric(Metric met) { * @return el indicador localizado */ @Override - public Indicator getIndicatorByName(String name) { + public ReportItem getIndicatorByName(String name) { log.info("solicitado indicador de nombre "+name); - Indicator indicator=null; + ReportItem indicator=null; if (indicators.containsKey(name)){ indicator=indicators.get(name); @@ -116,7 +116,7 @@ public Indicator getIndicatorByName(String name) { * */ @Override - public void addIndicator(Indicator ind) { + public void addIndicator(ReportItem ind) { indicators.put(ind.getName(), ind); log.info("Añadido indicador "+ind); @@ -167,7 +167,7 @@ public String toString() { return repoinfo; } @Override - public getAllMetrics() { + public Collection getAllMetrics() { // TODO Auto-generated method stub return metrics.values(); } @@ -188,5 +188,7 @@ public void calcAllIndicators() { // TODO Auto-generated method stub } + + } diff --git a/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java b/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java index 1b152edf..886a477c 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java @@ -25,30 +25,30 @@ public static enum Type{ * @param name Nombre de la métrica solicitada * @return Métrica solicitada */ - Metric getMetricByName(String name); + ReportItem getMetricByName(String name); /** * Obtiene todas las métricas del informe * @return Colleción de métricas que contiene el informe */ - Collection getAllMetrics(); + Collection getAllMetrics(); /** * Añade una métrica al informe * @param met Nueva métrica */ - void addMetric(Metric met); + void addMetric(ReportItem met); /** * Obtiene un indicador del informe a partir del nombre del mismo * @param name Nombre del indicador consultado * @return El indicador */ - Indicator getIndicatorByName(String name); + ReportItem getIndicatorByName(String name); /** * Añade un indicador al informe * @param ind Nuevo indicador */ - void addIndicator(Indicator ind); + void addIndicator(ReportItem ind); /** * Calcula un indicador a partir de su nombre y lo añade al informe * Si se basa en métricas que no están aún incluidas en el informe las incluye diff --git a/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java b/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java index a4570f92..2b99b831 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java @@ -15,30 +15,39 @@ import us.muit.fs.a4i.config.Context; import us.muit.fs.a4i.exceptions.MetricException; import us.muit.fs.a4i.model.entities.Indicator.IndicatorBuilder; - +import us.muit.fs.a4i.model.entities.ReportItemI; /** - * @author Isabel Román + * @author Isabel Román + * Entidad para guardar la información de un indicador o una métrica, elementos de un informe * */ -public class ReportItem { +public class ReportItem implements ReportItemI{ private Indicator indicator = null; private static Logger log=Logger.getLogger(ReportItem.class.getName()); /** - * Obligatorio + * Nombre del indicador/métrica */ private String name; /** - * Obligatorio + * Valor del indicador/métrica */ private T value; /** * Obligatorio - * Fecha en la que se tomó la medida (por defecto cuando se crea el objeto) + * Fecha en la que se construye el objeto o se toma la medida */ private Date date; - + /** + * Descripción del elemento del informe + */ private String description; + /** + * Origen del elemento + */ private String source; + /** + * Unidades de medida + */ private String unit; /** @@ -130,26 +139,29 @@ public static class ReportItemBuilder{ private T value; private String source; private String unit; - private Collection> metrics; - private ReportItem metric; - private IndicatorBuilder state; - public ReportItemBuilder(String reportItemName, T reportItemValue) throws MetricException { + private Collection metrics; + private ReportItem metric; + private IndicatorBuilder state; + public ReportItemBuilder(String name, T value) throws MetricException { HashMap reportItemDefinition=null; + /** + * Verifico si el elemento está definido y el tipo es correcto + */ //el nombre incluye java.lang, si puede eliminar si se manipula la cadena //hay que quedarse sólo con lo que va detrás del último punto o meter en el fichero el nombre completo //Pero ¿y si se usan tipos definidos en otras librerías? usar el nombre completo "desambigua" mejor - log.info("Verifico el ReportItem de nombre "+reportItemName+" con valor de tipo "+reportItemValue.getClass().getName()); + log.info("Verifico el ReportItem de nombre "+name+" con valor de tipo "+value.getClass().getName()); try { - reportItemDefinition=Context.getContext().getChecker().definedReportItem(reportItemName,reportItemValue.getClass().getName()); + reportItemDefinition=Context.getContext().getChecker().definedReportItem(name,value.getClass().getName()); if(reportItemDefinition!=null) { - this.name=reportItemName; - this.value=reportItemValue; + this.name=name; + this.value=value; this.date=Date.from(LocalDateTime.now().toInstant(ZoneOffset.UTC)); this.description=reportItemDefinition.get("description"); this.unit=reportItemDefinition.get("unit"); }else { - throw new MetricException("Métrica "+reportItemName+" no definida o tipo "+reportItemValue.getClass().getName()+" incorrecto"); + throw new MetricException("Métrica "+name+" no definida o tipo "+value.getClass().getName()+" incorrecto"); } }catch(IOException e) { throw new MetricException("El fichero de configuración de ReportItem no se puede abrir"); diff --git a/src/main/java/us/muit/fs/a4i/model/entities/ReportItemI.java b/src/main/java/us/muit/fs/a4i/model/entities/ReportItemI.java index c4907520..1743bc44 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/ReportItemI.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/ReportItemI.java @@ -41,6 +41,12 @@ public interface ReportItemI { */ public String getUnit(); + /** + * Consulta el indicador + * @return el indicador + */ + public Indicator getIndicator(); + // getIndicator no existe, no sabemos si hay que crearla o como /*** diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index 3e52feec..9e570e33 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -19,6 +19,8 @@ import us.muit.fs.a4i.model.entities.Metric.MetricBuilder; import us.muit.fs.a4i.model.entities.Report; import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; /** * @author Isabel Román @@ -70,42 +72,48 @@ public ReportI buildReport(String repositoryId) { * Métricas directas de tipo conteo */ - MetricBuilder subscribers = new Metric.MetricBuilder("subscribers", + /* MetricBuilder subscribers = new Metric.MetricBuilder("subscribers", + remoteRepo.getSubscribersCount());*/ + ReportItemBuilder subscribers = new ReportItem.ReportItemBuilder("subscribers", remoteRepo.getSubscribersCount()); subscribers.source("GitHub"); myRepo.addMetric(subscribers.build()); log.info("Añadida métrica suscriptores " + subscribers); - MetricBuilder forks = new Metric.MetricBuilder("forks", remoteRepo.getForksCount()); + /*MetricBuilder forks = new Metric.MetricBuilder("forks", remoteRepo.getForksCount()); + forks.source("GitHub");*/ + ReportItemBuilder forks = new ReportItem.ReportItemBuilder("forks", remoteRepo.getForksCount()); forks.source("GitHub"); myRepo.addMetric(forks.build()); log.info("Añadida métrica forks " + forks); - MetricBuilder watchers = new Metric.MetricBuilder("watchers", + /*MetricBuilder watchers = new Metric.MetricBuilder("watchers", + remoteRepo.getWatchersCount());*/ + ReportItemBuilder watchers = new ReportItem.ReportItemBuilder("watchers", remoteRepo.getWatchersCount()); watchers.source("GitHub"); myRepo.addMetric(watchers.build()); - MetricBuilder stars = new Metric.MetricBuilder("stars", remoteRepo.getStargazersCount()); + ReportItemBuilder stars = new ReportItem.ReportItemBuilder("stars", remoteRepo.getStargazersCount()); stars.source("GitHub"); myRepo.addMetric(stars.build()); - MetricBuilder issues = new Metric.MetricBuilder("issues", remoteRepo.getOpenIssueCount()); + ReportItemBuilder issues = new ReportItem.ReportItemBuilder("issues", remoteRepo.getOpenIssueCount()); issues.source("GitHub"); myRepo.addMetric(issues.build()); /** * Métricas directas de tipo fecha */ - MetricBuilder creation = new Metric.MetricBuilder("creation", remoteRepo.getCreatedAt()); + ReportItemBuilder creation = new ReportItem.ReportItemBuilder("creation", remoteRepo.getCreatedAt()); creation.source("GitHub"); myRepo.addMetric(creation.build()); - MetricBuilder push = new Metric.MetricBuilder("lastPush", remoteRepo.getPushedAt()); + ReportItemBuilder push = new ReportItem.ReportItemBuilder("lastPush", remoteRepo.getPushedAt()); push.description("Último push realizado en el repositorio").source("GitHub"); myRepo.addMetric(push.build()); - MetricBuilder updated = new Metric.MetricBuilder("lastUpdated", remoteRepo.getUpdatedAt()); + ReportItemBuilder updated = new ReportItem.ReportItemBuilder("lastUpdated", remoteRepo.getUpdatedAt()); push.description("Última actualización").source("GitHub"); myRepo.addMetric(updated.build()); /** @@ -126,12 +134,12 @@ public ReportI buildReport(String repositoryId) { } } - MetricBuilder totalAdditions = new Metric.MetricBuilder("totalAdditions", additions); + ReportItemBuilder totalAdditions = new ReportItem.ReportItemBuilder("totalAdditions", additions); totalAdditions.source("GitHub, calculada") .description("Suma el total de adiciones desde que el repositorio se creó"); myRepo.addMetric(totalAdditions.build()); - MetricBuilder totalDeletions = new Metric.MetricBuilder("totalDeletions", deletions); + ReportItemBuilder totalDeletions = new ReportItem.ReportItemBuilder("totalDeletions", deletions); totalDeletions.source("GitHub, calculada") .description("Suma el total de borrados desde que el repositorio se creó"); myRepo.addMetric(totalDeletions.build()); @@ -148,7 +156,7 @@ public ReportI buildReport(String repositoryId) { */ @Override - public Metric getMetric(String metricName, String repositoryId) throws MetricException { + public ReportItem getMetric(String metricName, String repositoryId) throws MetricException { GHRepository remoteRepo; GitHub gb = getConnection(); @@ -169,8 +177,8 @@ public Metric getMetric(String metricName, String repositoryId) throws MetricExc * @return La métrica creada * @throws MetricException Si la métrica no está definida se lanzará una excepción */ - private Metric getMetric(String metricName, GHRepository remoteRepo) throws MetricException { - Metric metric; + private ReportItem getMetric(String metricName, GHRepository remoteRepo) throws MetricException { + ReportItem metric; if (remoteRepo == null) { throw new MetricException("Intenta obtener una métrica sin haber obtenido los datos del repositorio"); } @@ -202,8 +210,8 @@ private Metric getMetric(String metricName, GHRepository remoteRepo) throws Metr * @return la métrica con el número total de adiciones desde el inicio * @throws MetricException Intenta crear una métrica no definida */ - private Metric getTotalAdditions(GHRepository remoteRepo) throws MetricException { - Metric metric = null; + private ReportItem getTotalAdditions(GHRepository remoteRepo) throws MetricException { + ReportItem metric = null; GHRepositoryStatistics data = remoteRepo.getStatistics(); List codeFreq; @@ -221,7 +229,7 @@ private Metric getTotalAdditions(GHRepository remoteRepo) throws MetricException } } - MetricBuilder totalAdditions = new Metric.MetricBuilder("totalAdditions", additions); + ReportItemBuilder totalAdditions = new ReportItem.ReportItemBuilder("totalAdditions", additions); totalAdditions.source("GitHub, calculada") .description("Suma el total de adiciones desde que el repositorio se creó"); metric = totalAdditions.build(); @@ -243,8 +251,8 @@ private Metric getTotalAdditions(GHRepository remoteRepo) throws MetricException * @return la métrica con el número total de eliminaciones desde el inicio * @throws MetricException Intenta crear una métrica no definida */ - private Metric getTotalDeletions(GHRepository remoteRepo) throws MetricException { - Metric metric = null; + private ReportItem getTotalDeletions(GHRepository remoteRepo) throws MetricException { + ReportItem metric = null; GHRepositoryStatistics data = remoteRepo.getStatistics(); List codeFreq; @@ -262,7 +270,7 @@ private Metric getTotalDeletions(GHRepository remoteRepo) throws MetricException } } - MetricBuilder totalDeletions = new Metric.MetricBuilder("totalDeletions", deletions); + ReportItemBuilder totalDeletions = new ReportItem.ReportItemBuilder("totalDeletions", deletions); totalDeletions.source("GitHub, calculada") .description("Suma el total de eliminaciones desde que el repositorio se creó"); metric = totalDeletions.build(); diff --git a/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java index b380d2f8..c36ecfe8 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java @@ -9,6 +9,7 @@ import us.muit.fs.a4i.model.entities.Metric; import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItem; /** *

Interfaz para desacoplar el mecanismo de obtención de métricas del servidor remoto que se use como fuente de las mismas

@@ -33,7 +34,7 @@ public interface RemoteEnquirer{ * @return La nueva métrica construida tras la consulta al remoto * @throws MetricException Si la métrica no está definida */ - public Metric getMetric(String metricName,String entityId) throws MetricException; + public ReportItem getMetric(String metricName,String entityId) throws MetricException; /** *

Devuelve las métricas que el objeto RemoteEnquirer concreto puede obtener del servidor remoto

diff --git a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java index 61ea10e6..e4d646ca 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java @@ -28,6 +28,7 @@ import us.muit.fs.a4i.model.entities.Indicator; import us.muit.fs.a4i.model.entities.Metric; import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItem; @@ -162,8 +163,8 @@ public void saveReport() throws ReportNotDefinedException{ rowIndex++; sheet.createRow(rowIndex).createCell(0).setCellValue("Métricas tomadas el día "); sheet.getRow(rowIndex).createCell(1).setCellValue(Date.from(LocalDateTime.now().toInstant(ZoneOffset.UTC)).toString()); - Collection collection=report.getAllMetrics(); - for(Metric metric:collection) { + Collection collection=report.getAllMetrics(); + for(ReportItem metric:collection) { persistMetric(metric); } @@ -177,7 +178,7 @@ public void saveReport() throws ReportNotDefinedException{ } } - private void persistMetric(Metric metric) { + private void persistMetric(ReportItem metric) { log.info("Introduzco métrica en la hoja"); int rowIndex=sheet.getLastRowNum(); diff --git a/src/test/java/us/muit/fs/a4i/test/model/entities/ReportItemTest.java b/src/test/java/us/muit/fs/a4i/test/model/entities/ReportItemTest.java index 13fb908a..ab88c046 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/entities/ReportItemTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/entities/ReportItemTest.java @@ -31,8 +31,8 @@ * @author Isabel Román * */ -class ReportItemBuilderTest { - private static Logger log = Logger.getLogger(ReportItemBuilderTest.class.getName()); +class ReportItemTest { + private static Logger log = Logger.getLogger(ReportItemTest.class.getName()); /** * @throws java.lang.Exception Se incluye por defecto al crear automáticamente los tests con eclipse @@ -72,7 +72,7 @@ void tearDown() throws Exception { * @see org.junit.jupiter.api.Test */ @Test - @Tag("integración") + @Tag("unidad") @DisplayName("Prueba constructor reportItem, las clases Context y Checker ya están disponibles") void testReportItemBuilder() { @@ -81,7 +81,7 @@ void testReportItemBuilder() { try { underTest = new ReportItemBuilder("watchers", 33); } catch (MetricException e) { - fail("No debería haber saltado esta excepción"); + fail("Watchers existe y no debería haber saltado esta excepción"); e.printStackTrace(); } ReportItem newMetric = underTest.build(); @@ -147,12 +147,12 @@ void testSource() { try { underTest = new ReportItemBuilder("watchers", 33); } catch (MetricException e) { - fail("No debería haber saltado esta excepción"); + fail("El elemento watchers existe, no debería haber saltado esta excepción"); e.printStackTrace(); } underTest.source("GitHub"); ReportItem newMetric = underTest.build(); - log.info("Métrica creada "+newMetric.toString()); + log.info("Métrica creada: "+newMetric.toString()); assertEquals("GitHub",newMetric.getSource(),"Source no tiene el valor esperado"); } diff --git a/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java b/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java index 6ba625d0..e1258b2e 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java @@ -30,6 +30,7 @@ import us.muit.fs.a4i.model.entities.Metric; import us.muit.fs.a4i.model.entities.Report; import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItem; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; @@ -65,11 +66,11 @@ class ReportTest { private ArgumentCaptor reportCaptor; @Mock(serializable = true) - private static Metric metricIntMock= Mockito.mock(Metric.class); + private static ReportItem metricIntMock= Mockito.mock(ReportItem.class); @Mock(serializable = true) - private static Metric metricDatMock= Mockito.mock(Metric.class); + private static ReportItem metricDatMock= Mockito.mock(ReportItem.class); @Mock(serializable = true) - private static Indicator indicatorIntMock= Mockito.mock(Indicator.class); + private static ReportItem indicatorIntMock= Mockito.mock(ReportItem.class); private static Report reportTested; @@ -153,7 +154,7 @@ void testAddMetric() { //Prueba a sustituir por la línea comentada //Mockito.verify(metricIntMock).getName(); Mockito.verify(metricIntMock, atLeast(1)).getName(); - Metric metric=reportTested.getMetricByName("issues"); + ReportItem metric=reportTested.getMetricByName("issues"); assertEquals(metric.getValue(),3,"Debería tener el valor especificado en el mock"); assertEquals(metric.getDescription(),"Tareas sin finalizar en el repositorio","Debería tener el valor especificado en el mock"); diff --git a/src/test/resources/appConfTest.json b/src/test/resources/appConfTest.json index 65c11a89..a09346cf 100644 --- a/src/test/resources/appConfTest.json +++ b/src/test/resources/appConfTest.json @@ -5,11 +5,11 @@ "type": "java.lang.Integer", "description": "Descargas realizadas", "unit": "downloads" - }, + }, { "name": "comments", "type": "java.lang.Integer", - "description": "Númeoro de comentarios", + "description": "N�meoro de comentarios", "unit": "comments" } ], @@ -23,7 +23,7 @@ { "name": "commentsInterest", "type": "java.lang.Double", - "description": "Ratio de comentarios con más de 1 respuesta frente al número total de comentarios", + "description": "Ratio de comentarios con m�s de 1 respuesta frente al n�mero total de comentarios", "unit": "ratio" } ] From ae52641c06ab6952cab9ed207d2e76ea04a7c937 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Fri, 30 Dec 2022 09:15:15 +0100 Subject: [PATCH 031/100] =?UTF-8?q?issue=2037,=20modificaciones=20para=20I?= =?UTF-8?q?ndicador=20Las=20pruebas=20est=C3=A1n=20incompletas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/us/muit/fs/a4i/config/Context.java | 3 +- .../fs/a4i/control/IndicatorsCalculator.java | 2 +- .../fs/a4i/control/RepositoryCalculator.java | 4 +- .../fs/a4i/exceptions/IndicatorException.java | 4 +- .../fs/a4i/exceptions/MetricException.java | 2 +- .../a4i/exceptions/ReportItemException.java | 28 +++ .../muit/fs/a4i/model/entities/Indicator.java | 166 +++--------------- .../fs/a4i/model/entities/IndicatorI.java | 36 ++++ .../fs/a4i/model/entities/ReportItem.java | 78 +++++--- .../remote/GitHubRepositoryEnquirer.java | 8 +- .../a4i/persistence/ExcelReportManager.java | 8 +- .../fs/a4i/persistence/ReportFormater.java | 11 +- .../fs/a4i/persistence/ReportFormaterI.java | 7 +- .../test/model/entities/ReportItemTest.java | 21 +-- 14 files changed, 185 insertions(+), 193 deletions(-) create mode 100644 src/main/java/us/muit/fs/a4i/exceptions/ReportItemException.java create mode 100644 src/main/java/us/muit/fs/a4i/model/entities/IndicatorI.java diff --git a/src/main/java/us/muit/fs/a4i/config/Context.java b/src/main/java/us/muit/fs/a4i/config/Context.java index db1a1b18..c1238aa6 100644 --- a/src/main/java/us/muit/fs/a4i/config/Context.java +++ b/src/main/java/us/muit/fs/a4i/config/Context.java @@ -15,6 +15,7 @@ import java.awt.Font; import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.IndicatorI; import us.muit.fs.a4i.model.entities.Metric; /** @@ -196,7 +197,7 @@ public static Font getMetricFont() { * @throws IOException problema al leer el fichero */ - public static Font getIndicatorFont(Indicator.State state) throws IOException { + public static Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOException { Font font = null; // TO DO diff --git a/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java b/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java index 37631cc3..561b1cff 100644 --- a/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java @@ -35,5 +35,5 @@ public interface IndicatorsCalculator { * Devuelve el tipo de informe que maneja esta calculadora de indicadores * @return El tipo de informes */ - public ReportI.Type getReportType(); + public ReportI.ReportType getReportType(); } diff --git a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java index b5efe228..cf85c093 100644 --- a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java @@ -43,7 +43,7 @@ private Indicator commitsPerUser(ReportI report) { return indicator; } @Override - public ReportI.Type getReportType() { - return ReportI.Type.REPOSITORY; + public ReportI.ReportType getReportType() { + return ReportI.ReportType.REPOSITORY; } } diff --git a/src/main/java/us/muit/fs/a4i/exceptions/IndicatorException.java b/src/main/java/us/muit/fs/a4i/exceptions/IndicatorException.java index f8da006b..03161730 100644 --- a/src/main/java/us/muit/fs/a4i/exceptions/IndicatorException.java +++ b/src/main/java/us/muit/fs/a4i/exceptions/IndicatorException.java @@ -4,9 +4,9 @@ * @author Isabel Román * */ -public class IndicatorException extends Exception { +public class IndicatorException extends Exception { /** - * Excepción que indica que se está intentando recuperar una entidad sin haber establecido su id + * Excepción al manejar Indicador */ private static final long serialVersionUID = 1L; /** diff --git a/src/main/java/us/muit/fs/a4i/exceptions/MetricException.java b/src/main/java/us/muit/fs/a4i/exceptions/MetricException.java index cfbe3837..5ec0a243 100644 --- a/src/main/java/us/muit/fs/a4i/exceptions/MetricException.java +++ b/src/main/java/us/muit/fs/a4i/exceptions/MetricException.java @@ -6,7 +6,7 @@ */ public class MetricException extends Exception { /** - * Excepción que indica que se está intentando recuperar una entidad sin haber establecido su id + * Excepción al manejar métrica */ private static final long serialVersionUID = 1L; /** diff --git a/src/main/java/us/muit/fs/a4i/exceptions/ReportItemException.java b/src/main/java/us/muit/fs/a4i/exceptions/ReportItemException.java new file mode 100644 index 00000000..7588c0cb --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/exceptions/ReportItemException.java @@ -0,0 +1,28 @@ +package us.muit.fs.a4i.exceptions; + +/** + * @author Isabel Román + * + */ +public class ReportItemException extends Exception { + /** + * Excepción al manejar ReportItem + */ + private static final long serialVersionUID = 1L; + /** + * Información sobre el error + */ + private String message; + /** + *

Constructor

+ * @param info Mensaje definiendo el error + */ + public ReportItemException(String info){ + message=info; + } + + @Override + public String getMessage(){ + return message; + } +} \ No newline at end of file diff --git a/src/main/java/us/muit/fs/a4i/model/entities/Indicator.java b/src/main/java/us/muit/fs/a4i/model/entities/Indicator.java index f8af021b..66e79aad 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/Indicator.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/Indicator.java @@ -5,6 +5,7 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.Collection; import java.util.Date; import java.util.logging.Logger; @@ -13,157 +14,46 @@ * @author Isabel Román * */ -public class Indicator { +public class Indicator implements IndicatorI{ private static Logger log=Logger.getLogger(Indicator.class.getName()); - private Date date; - private String source; - private String description; - private String name; - private T value; - private String unit; - private State state; + private Collectionmetrics; + private IndicatorState state; /** - *

Estados posibles del indicador, indican el grado de atención que requiere por parte del analista

- * @author Isabel Román - * + * Constructor */ - public static enum State{ - OK, - WARNING, - CRITICAL - }; - /** - *

Constructor privado, para construir desde fuera hay que usar el método build de IndicatorBuilder. Implementación patrón constructor

- * @param builder Clase constructora - */ - private Indicator(IndicatorBuilder builder){ - - this.description=builder.description; - this.name=builder.name; - this.value=builder.value; - this.source=builder.source; - this.unit=builder.unit; - this.date=builder.date; - this.state=builder.state; + public Indicator() { + this.state=IndicatorState.UNDEFINED; } - /** - * - * @return Descripción del indicador - */ - public String getDescription() { - return description; - } - /** - * - * @return Nombre del indicador - */ - public String getName() { - return name; - } - /** - * - * @return Unidades de medida del indicador - */ - public String getUnit() { - return unit; - } - /** - * - * @return Valor del indicador - */ - public T getValue() { - return value; + + public Indicator(IndicatorState state,Collection metrics){ + this.metrics=metrics; + this.state=state; } - /** - *

Clase constructora, se usará el método build para crear un objeto Indicator desde fuera

- * @author Isabel Román - * - * @param Tipo utilizado en el valor del indicador que va a construir - */ - public static class IndicatorBuilder{ - private String description; - private String name; - private Date date; - private T value; - private String source; - private String unit; - private State state; - /** - *

Constructor de la clase constructora, recibe los parámetros obligatgorios nombre y valor del indicador

- * @param indicatorName Nombre del indicador - * @param indicatorValue Valor del indicador - */ - public IndicatorBuilder(String indicatorName, T indicatorValue) { - this.name=indicatorName; - this.value=indicatorValue; - this.date=Date.from(LocalDateTime.now().toInstant(ZoneOffset.UTC)); - } - /** - *

Establece la descripción en el constructor

- * @param description descripción del indicador - * @return el objeto constructor - */ - public IndicatorBuilder description(String description){ - this.description=description; - return this; - } - /** - * - * @param source Fuente de la que se obtuvo el indicador - * @return el objeto constructor - */ - public IndicatorBuilder source(String source){ - this.source=source; - return this; - } - /** - * - * @param unit Unidad de medida del indicador - * @return el objeto constructor - */ - public IndicatorBuilder unit(String unit){ - this.unit=unit; - return this; - } - /** - * - * @param state Estado del indicador. Grado de atención que necesita por parte del analista - * @return el objeto constructor - */ - public IndicatorBuilder state(State state){ - this.state=state; - return this; - } - /** - *

Método de construcción del indicador, patrón constructor

- * @return El indicador construido - */ - public Indicator build(){ - return new Indicator(this); - } + public void setMetrics(Collection metrics) { + this.metrics=metrics; } + public void setState(IndicatorState state) { + this.state=state; + } @Override public String toString() { String info; - info="Indicador para "+description+", con valor=" + value + ", source=" + source - + ", unit=" + unit +" fecha de cálculo= "+ date+" Estado= "+state; + info="Indicador estado "+state+", a partir de metricas: " + metrics; return info; } - /** - * - * @return Fecha de creación del indicador - */ - public Date getDate() { - return date; + + @Override + public IndicatorState getState() { + // TODO Auto-generated method stub + return state; } - - /** - * - * @return Fuente de los datos para obtener el indicador - */ - public String getSource() { - return source; + + @Override + public Collection getMetrics() { + // TODO Auto-generated method stub + return metrics; } + } \ No newline at end of file diff --git a/src/main/java/us/muit/fs/a4i/model/entities/IndicatorI.java b/src/main/java/us/muit/fs/a4i/model/entities/IndicatorI.java new file mode 100644 index 00000000..8d6ad677 --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/model/entities/IndicatorI.java @@ -0,0 +1,36 @@ +/** + * + */ +package us.muit.fs.a4i.model.entities; + +import java.util.Collection; + +/** + * @author Isabel Román + * Interfaz para manejar los indicadores + * + */ +public interface IndicatorI { + /** + *

Estados posibles del indicador, indican el grado de atención que requiere por parte del analista

+ * @author Isabel Román + * + */ + public static enum IndicatorState{ + OK, + WARNING, + CRITICAL, + UNDEFINED + } + /** + * Devuelve el estado en el que se encuentra este indicador + * @return estado del indicador + */ + public IndicatorState getState(); + /** + * Devuelve el conjunto de métricas en las que se basa este indicador + * @return + */ + public Collection getMetrics(); + +} diff --git a/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java b/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java index 2b99b831..c4c4e2c3 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java @@ -13,16 +13,16 @@ import java.util.Collection; import us.muit.fs.a4i.config.Context; -import us.muit.fs.a4i.exceptions.MetricException; -import us.muit.fs.a4i.model.entities.Indicator.IndicatorBuilder; -import us.muit.fs.a4i.model.entities.ReportItemI; + +import us.muit.fs.a4i.exceptions.ReportItemException; + /** * @author Isabel Román * Entidad para guardar la información de un indicador o una métrica, elementos de un informe * */ public class ReportItem implements ReportItemI{ - private Indicator indicator = null; + private Indicator indicator = null; private static Logger log=Logger.getLogger(ReportItem.class.getName()); /** * Nombre del indicador/métrica @@ -115,8 +115,8 @@ public String getUnit() { * Consulta el indicador de ReportItem * @return indicador de ReportItem */ - public Indicator getIndicator() { - return indicator; + public Indicator getIndicator() { + return this.indicator; } @@ -125,7 +125,7 @@ public Indicator getIndicator() { * @return Fecha de consulta del ReportItem */ public Date getDate() { - return date; + return this.date; } /** @@ -138,11 +138,11 @@ public static class ReportItemBuilder{ private Date date; private T value; private String source; - private String unit; - private Collection metrics; - private ReportItem metric; - private IndicatorBuilder state; - public ReportItemBuilder(String name, T value) throws MetricException { + private String unit; + private Indicator indicator=null; + + + public ReportItemBuilder(String name, T value) throws ReportItemException { HashMap reportItemDefinition=null; /** * Verifico si el elemento está definido y el tipo es correcto @@ -152,21 +152,30 @@ public ReportItemBuilder(String name, T value) throws MetricException { //Pero ¿y si se usan tipos definidos en otras librerías? usar el nombre completo "desambigua" mejor log.info("Verifico el ReportItem de nombre "+name+" con valor de tipo "+value.getClass().getName()); try { - reportItemDefinition=Context.getContext().getChecker().definedReportItem(name,value.getClass().getName()); + //Compruebo si es un indicador + reportItemDefinition=Context.getContext().getChecker().definedIndicator(name,value.getClass().getName()); if(reportItemDefinition!=null) { + this.indicator=new Indicator(); + }else { + //Si no lo era, compruebo si es una métrica + reportItemDefinition=Context.getContext().getChecker().definedMetric(name,value.getClass().getName()); + } + if(reportItemDefinition!=null) { this.name=name; this.value=value; this.date=Date.from(LocalDateTime.now().toInstant(ZoneOffset.UTC)); this.description=reportItemDefinition.get("description"); this.unit=reportItemDefinition.get("unit"); }else { - throw new MetricException("Métrica "+name+" no definida o tipo "+value.getClass().getName()+" incorrecto"); + throw new ReportItemException("ReportItem "+name+" no definido o tipo "+value.getClass().getName()+" incorrecto"); } }catch(IOException e) { - throw new MetricException("El fichero de configuración de ReportItem no se puede abrir"); + throw new ReportItemException("El fichero de configuración de ReportItem no se puede abrir"); } - + /*Si el ReportItem era un indicador este se ha creado vacío, con el campo state a UNDEFINED + Si era una métrica el indicador está null + Si no era ninguno de los dos se ha lanzado una excepción*/ } /** @@ -192,27 +201,36 @@ public ReportItemBuilder date(Date date){ /** - *

Establece el estado del ReportItem

- * @param state Estado del ReportItem + *

Establece el estado del ReportItem si era un indicador

+ * @param state Estado del indicador * @return El propio constructor + * @throws ReportItemException, intenta establecer datos de tipo indicador en una métrica */ - public ReportItemBuilder state(IndicatorBuilder state){ - this.state=state; + public ReportItemBuilder indicator(IndicatorI.IndicatorState state)throws ReportItemException { + if(this.indicator!=null) { + this.indicator.setState(state); + }else { + throw new ReportItemException("El Report Item no es un indicador, no puede contener estado"); + } + return this; } - /** - *

Establece la métrica del ReportItem

- * @param metric Métrica del ReportItem + *

Establece el conjunto de métricas si el ReportItem es un indicador

+ * @param metrics * @return El propio constructor + * @throws ReportItemException */ - public ReportItemBuilder metric(ReportItem metric){ - this.metric=metric; + public ReportItemBuilder metrics(Collection metrics)throws ReportItemException { + if(this.indicator!=null) { + this.indicator.setMetrics(metrics); + }else { + throw new ReportItemException("El Report Item no es un indicador, no puede contener más metricas"); + } return this; } - /** *

Establece la fuente de información

* @param source Fuente de la que se extrajeron los datos @@ -238,9 +256,13 @@ public ReportItem build(){ @Override public String toString() { - String info; - info="ReportItem para "+description+", con valor=" + value + ", source=" + source + String info="ReportItem "; + if(this.indicator!=null) { + info="De tipo Indicador "; + } + info=info+"para "+description+", con valor=" + value + ", source=" + source + ", unit=" + unit +" fecha de la medida= "+ date; + return info; } diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index 9e570e33..0fdff6c8 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -14,7 +14,7 @@ import org.kohsuke.github.GHRepositoryStatistics.CodeFrequency; import us.muit.fs.a4i.exceptions.MetricException; - +import us.muit.fs.a4i.exceptions.ReportItemException; import us.muit.fs.a4i.model.entities.Metric; import us.muit.fs.a4i.model.entities.Metric.MetricBuilder; import us.muit.fs.a4i.model.entities.Report; @@ -237,6 +237,9 @@ private ReportItem getTotalAdditions(GHRepository remoteRepo) throws MetricExcep } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); + } catch (ReportItemException e) { + // TODO Auto-generated catch block + e.printStackTrace(); } return metric; @@ -278,6 +281,9 @@ private ReportItem getTotalDeletions(GHRepository remoteRepo) throws MetricExcep } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); + } catch (ReportItemException e) { + // TODO Auto-generated catch block + e.printStackTrace(); } return metric; diff --git a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java index e1914704..97cd8cca 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java @@ -26,9 +26,11 @@ import us.muit.fs.a4i.control.ReportManagerI; import us.muit.fs.a4i.exceptions.ReportNotDefinedException; import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.IndicatorI; import us.muit.fs.a4i.model.entities.Metric; import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; @@ -197,7 +199,7 @@ private void persistMetric(ReportItem metric) { } - private void persistIndicator(Indicator indicator) { + private void persistIndicator(ReportItemI indicator) { log.info("Introduzco indicador en la hoja"); @@ -211,9 +213,11 @@ private void persistIndicator(Indicator indicator) { row.createCell(cellIndex++).setCellValue(indicator.getValue().toString()); row.createCell(cellIndex++).setCellValue(indicator.getDescription()); + row.createCell(cellIndex++).setCellValue(indicator.getIndicator().getState().toString()); row.createCell(cellIndex).setCellValue(indicator.getDate().toString()); - log.info("Indice de celda final"+cellIndex); + + log.info("Indice de celda final "+cellIndex); } diff --git a/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java b/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java index d2b03db0..c08378cf 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java @@ -9,7 +9,8 @@ import us.muit.fs.a4i.config.Context; import us.muit.fs.a4i.model.entities.Indicator; -import us.muit.fs.a4i.model.entities.Indicator.State; + +import us.muit.fs.a4i.model.entities.IndicatorI; /** * @author Isabel Román @@ -27,10 +28,10 @@ public class ReportFormater implements ReportFormaterI { /** * Fomatos de fuente en función del estado del indicador */ - private HashMap indicatorsFont; + private HashMap indicatorsFont; ReportFormater(){ - this.indicatorsFont=new HashMap(); + this.indicatorsFont=new HashMap(); //Sólo se construye el mapa, conforme se vayan solicitando se irán rellenando } @Override @@ -48,7 +49,7 @@ public void setMetricFont(Font font) { } @Override - public Font getIndicatorFont(Indicator.State state) throws IOException { + public Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOException { if (!indicatorsFont.containsKey(state)){ try { indicatorsFont.put(state, Context.getIndicatorFont(state)); @@ -61,7 +62,7 @@ public Font getIndicatorFont(Indicator.State state) throws IOException { } @Override - public void setIndicatorFont(State state, Font font) { + public void setIndicatorFont(IndicatorI.IndicatorState state, Font font) { indicatorsFont.put(state, font); } diff --git a/src/main/java/us/muit/fs/a4i/persistence/ReportFormaterI.java b/src/main/java/us/muit/fs/a4i/persistence/ReportFormaterI.java index dba94f19..f300836a 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ReportFormaterI.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ReportFormaterI.java @@ -7,6 +7,8 @@ import java.io.IOException; import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.IndicatorI; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; /** *

Interfaz genérica para establecer el estilo de los informes si se persisten en un medio "visual"

@@ -32,17 +34,18 @@ public interface ReportFormaterI { * @throws IOException Si no se puede leer la configuración */ - Font getIndicatorFont(Indicator.State state) throws IOException; + Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOException; /** *

Establece la fuente para un indicador en un estado determinado

* @param state El estado correspondiente a la fuente que se está estableciendo * @param font La fuente a usar cuando el estado del indicador es el señalado en la parámetro state */ - void setIndicatorFont(Indicator.State state, Font font); + void setIndicatorFont(IndicatorI.IndicatorState state, Font font); /** *

Establece la fuente por defecto para los indicadores y métricas

*

Usada cuando el estado no está diferenciado o no se ha especificado una fuente para las métricas

* @param font Fuente por defecto */ void setDefaultFont(Font font); + } diff --git a/src/test/java/us/muit/fs/a4i/test/model/entities/ReportItemTest.java b/src/test/java/us/muit/fs/a4i/test/model/entities/ReportItemTest.java index ab88c046..a8560db0 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/entities/ReportItemTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/entities/ReportItemTest.java @@ -21,6 +21,7 @@ import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; import us.muit.fs.a4i.exceptions.MetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; import us.muit.fs.a4i.model.entities.ReportItem; /** @@ -80,7 +81,7 @@ void testReportItemBuilder() { ReportItemBuilder underTest = null; try { underTest = new ReportItemBuilder("watchers", 33); - } catch (MetricException e) { + } catch (ReportItemException e) { fail("Watchers existe y no debería haber saltado esta excepción"); e.printStackTrace(); } @@ -99,16 +100,16 @@ void testReportItemBuilder() { try { underTest = new ReportItemBuilder("watchers", "hola"); fail("Debería haber lanzado una excepción"); - } catch (MetricException e) { - log.info("Lanza la excepción adecuada, MetricException"); + } catch (ReportItemException e) { + log.info("Lanza la excepción adecuada, ReportItemException"); } catch (Exception e) { - fail("La excepción capturada es " + e + " cuando se esperaba de tipo MetricException"); + fail("La excepción capturada es " + e + " cuando se esperaba de tipo ReportItemException"); } //Forma ALTERNATIVA de verificar el lanzamiento de una excepción, usando la verificación assertThrows - MetricException thrown = assertThrows(MetricException.class, () -> { + ReportItemException thrown = assertThrows(ReportItemException.class, () -> { new ReportItemBuilder("watchers", "hola"); - }, "Se esperaba la excepción MetricException"); + }, "Se esperaba la excepción ReportItemException"); //verifica también que el mensaje es correcto assertEquals("Métrica watchers no definida o tipo java.lang.String incorrecto", thrown.getMessage(),"El mensaje de la excepción no es correcto"); //El constructor de métricas no permite que se incluyan métricas no definidas @@ -116,11 +117,11 @@ void testReportItemBuilder() { try { underTest = new ReportItemBuilder("pepe", "hola"); fail("Debería haber lanzado una excepción"); - } catch (MetricException e) { - log.info("Lanza la excepción adecuada, MetricException"); + } catch (ReportItemException e) { + log.info("Lanza la excepción adecuada, ReportItemException"); } catch (Exception e) { - fail("La excepción capturada es " + e + " cuando se esperaba de tipo MetricException"); + fail("La excepción capturada es " + e + " cuando se esperaba de tipo ReportItemException"); } } @@ -146,7 +147,7 @@ void testSource() { ReportItemBuilder underTest = null; try { underTest = new ReportItemBuilder("watchers", 33); - } catch (MetricException e) { + } catch (ReportItemException e) { fail("El elemento watchers existe, no debería haber saltado esta excepción"); e.printStackTrace(); } From 0ecaf96603ccaced07afc959eca34f4b9b52ad69 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Fri, 30 Dec 2022 12:56:40 +0100 Subject: [PATCH 032/100] Issues 33 y 41 Faltan test, hay aspectos incompletos --- .../fs/a4i/config/MetricConfiguration.java | 56 ++++++- .../fs/a4i/config/MetricConfigurationI.java | 1 + .../muit/fs/a4i/test/config/CheckerTest.java | 31 ++-- .../config/IndicatorConfigurationTest.java | 137 ++++++++++++++++++ .../muit/fs/a4i/test/config/MetricInfo.java | 2 +- 5 files changed, 210 insertions(+), 17 deletions(-) create mode 100644 src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java diff --git a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java index aae74089..54a819c3 100644 --- a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java @@ -49,7 +49,35 @@ private HashMap isDefinedMetric(String metricName, String metricT return metricDefinition; } - + private HashMap getMetric(String metricName, InputStreamReader isr) + throws FileNotFoundException { + + HashMap metricDefinition=null; + + + JsonReader reader = Json.createReader(isr); + log.info("Creo el JsonReader"); + + JsonObject confObject = reader.readObject(); + log.info("Leo el objeto"); + reader.close(); + + log.info("Muestro la configuración leída " + confObject); + JsonArray metrics = confObject.getJsonArray("metrics"); + log.info("El número de métricas es " + metrics.size()); + for (int i = 0; i < metrics.size(); i++) { + log.info("nombre: " + metrics.get(i).asJsonObject().getString("name")); + if (metrics.get(i).asJsonObject().getString("name").equals(metricName)) { + log.info("Localizada la métrica"); + metricDefinition=new HashMap(); + metricDefinition.put("type", metrics.get(i).asJsonObject().getString("type")); + metricDefinition.put("description", metrics.get(i).asJsonObject().getString("description")); + metricDefinition.put("unit", metrics.get(i).asJsonObject().getString("unit")); + } + } + + return metricDefinition; + } @Override public HashMap definedMetric(String name, String type) throws FileNotFoundException { log.info("Checker solicitud de búsqueda métrica " + name); @@ -79,6 +107,32 @@ public HashMap definedMetric(String name, String type) throws Fil } @Override + public HashMap getMetricInfo(String name) throws FileNotFoundException{ + log.info("Consulta información de la métrica "+name); + HashMap metricDefinition=null; + + String filePath="/"+Context.getDefaultRI(); + log.info("Buscando el archivo " + filePath); + InputStream is=this.getClass().getResourceAsStream(filePath); + log.info("InputStream "+is+" para "+filePath); + InputStreamReader isr = new InputStreamReader(is); + + /** + * Busca primero en el fichero de configuración de métricas por defecto + */ + metricDefinition = getMetric(name, isr); + /** + * En caso de que no estuviera ahí la métrica busco en el fichero de configuración de la aplicación + */ + if ((metricDefinition==null) && Context.getAppRI() != null) { + is=new FileInputStream(Context.getAppRI()); + isr=new InputStreamReader(is); + metricDefinition = getMetric(name, isr); + } + + return metricDefinition; + } + @Override public List listAllMetrics() throws FileNotFoundException { log.info("Consulta todas las métricas"); diff --git a/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java b/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java index f1c21495..c320c2c7 100644 --- a/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java +++ b/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java @@ -24,6 +24,7 @@ public interface MetricConfigurationI { * @throws FileNotFoundException Si no se localiza el fichero de configuración */ public HashMap definedMetric(String name,String type) throws FileNotFoundException; + public HashMap getMetricInfo(String name) throws FileNotFoundException; List listAllMetrics() throws FileNotFoundException; } diff --git a/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java b/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java index e1647899..efc537a7 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java @@ -19,6 +19,7 @@ import org.junit.jupiter.api.Test; import us.muit.fs.a4i.config.Checker; +import us.muit.fs.a4i.config.Context; /** * Test de la clase Checker que verifica las métricas e indicadores @@ -61,7 +62,7 @@ static void tearDownAfterClass() throws Exception { void setUp() throws Exception { //Acciones a realizar antes de cada uno de los tests de esta clase //Creo el objeto bajo test, un Checker - underTest = new Checker(); + underTest = Context.getContext().getChecker(); } /** @@ -116,18 +117,18 @@ void testDefinedMetric() { //Consulta una métrica no definida, con valor de tipo entero //debe devolver null, no está definida log.info("Busco la métrica llamada downloads"); - returnedMap=underTest.definedMetric("downloads", valOKMock.getClass().getName()); + returnedMap=underTest.getMetricConfiguration().definedMetric("downloads", valOKMock.getClass().getName()); assertNull(returnedMap, "Debería ser nulo, la métrica noexiste no está definida"); //Busco la métrica watchers con valor entero, no debería dar problemas log.info("Busco la métrica watchers"); - returnedMap=underTest.definedMetric("watchers", valOKMock.getClass().getName()); + returnedMap=underTest.getMetricConfiguration().definedMetric("watchers", valOKMock.getClass().getName()); assertNotNull(returnedMap,"Debería devolver un hashmap, la métrica está definida"); assertTrue(returnedMap.containsKey("unit"),"La clave unit tiene que estar en el mapa"); assertTrue(returnedMap.containsKey("description"),"La clave description tiene que estar en el mapa"); //Busco una métrica que existe pero con un tipo incorrecto en el valor - assertNull(underTest.definedMetric("watchers", valKOMock.getClass().getName()), + assertNull(underTest.getMetricConfiguration().definedMetric("watchers", valKOMock.getClass().getName()), "Debería ser nulo, la métrica está definida para Integer"); } catch (FileNotFoundException e) { fail("El fichero está en la carpeta resources"); @@ -135,10 +136,10 @@ void testDefinedMetric() { } //Ahora establezco el fichero de configuración de la aplicación, con un nombre de fichero que no existe - underTest.setAppMetrics("pepe"); + Context.setAppRI("pepe"); try { //Busco una métrica que se que no está en la configuración de la api - returnedMap=underTest.definedMetric("downloads", valOKMock.getClass().getName()); + returnedMap=underTest.getMetricConfiguration().definedMetric("downloads", valOKMock.getClass().getName()); fail("Debería lanzar una excepción porque intenta buscar en un fichero que no existe"); } catch (FileNotFoundException e) { log.info("Lanza la excepción adecuada, FileNotFoud"); @@ -147,11 +148,11 @@ void testDefinedMetric() { } //Ahora establezco un fichero de configuración de la aplicación que sí existe - underTest.setAppMetrics(appConfPath); + Context.setAppRI(appConfPath); try { //Busco una métrica que se que no está en la configuración de la api pero sí en la de la aplicación log.info("Busco la métrica llamada downloads"); - returnedMap=underTest.definedMetric("downloads", valOKMock.getClass().getName()); + returnedMap=underTest.getMetricConfiguration().definedMetric("downloads", valOKMock.getClass().getName()); assertNotNull(returnedMap,"Debería devolver un hashmap, la métrica está definida"); assertTrue(returnedMap.containsKey("unit"),"La clave unit tiene que estar en el mapa"); assertTrue(returnedMap.containsKey("description"),"La clave description tiene que estar en el mapa"); @@ -188,18 +189,18 @@ void testDefinedIndicator() { //Consulta un indicador no definido, con valor de tipo entero //debe devolver null, no está definido log.info("Busco el indicador llamado pullReqGlory"); - returnedMap=underTest.definedIndicator("pullReqGlory", valOKMock.getClass().getName()); + returnedMap=underTest.getIndicatorConfiguration().definedIndicator("pullReqGlory", valOKMock.getClass().getName()); assertNull(returnedMap, "Debería ser nulo, el indicador pullReqGlory no está definido"); //Busco el indicador overdued con valor double, no debería dar problemas log.info("Busco el indicador overdued"); - returnedMap=underTest.definedIndicator("overdued", valOKMock.getClass().getName()); + returnedMap=underTest.getIndicatorConfiguration().definedIndicator("overdued", valOKMock.getClass().getName()); assertNotNull(returnedMap,"Debería devolver un hashmap, el indicador overdued está definido"); assertTrue(returnedMap.containsKey("unit"),"La clave unit tiene que estar en el mapa"); assertTrue(returnedMap.containsKey("description"),"La clave description tiene que estar en el mapa"); //Busco una métrica que existe pero con un tipo incorrecto en el valor - assertNull(underTest.definedIndicator("overdued", valKOMock.getClass().getName()), + assertNull(underTest.getIndicatorConfiguration().definedIndicator("overdued", valKOMock.getClass().getName()), "Debería ser nulo, el indicador overdued está definido para Double"); } catch (FileNotFoundException e) { fail("El fichero está en la carpeta resources"); @@ -207,10 +208,10 @@ void testDefinedIndicator() { } //Ahora establezco el fichero de configuración de la aplicación, con un nombre de fichero que no existe - underTest.setAppMetrics("pepe"); + Context.setAppRI("pepe"); try { //Busco un indicador que se que no está en la configuración de la api - returnedMap=underTest.definedIndicator("pullReqGlory", valOKMock.getClass().getName()); + returnedMap=underTest.getIndicatorConfiguration().definedIndicator("pullReqGlory", valOKMock.getClass().getName()); fail("Debería lanzar una excepción porque intenta buscar en un fichero que no existe"); } catch (FileNotFoundException e) { log.info("Lanza la excepción adecuada, FileNotFoud"); @@ -219,11 +220,11 @@ void testDefinedIndicator() { } //Ahora establezco un fichero de configuración de la aplicación que sí existe - underTest.setAppMetrics(appConfPath); + Context.setAppRI(appConfPath); try { //Busco una métrica que se que no está en la configuración de la api pero sí en la de la aplicación log.info("Busco el indicador llamado pullReqGlory"); - returnedMap=underTest.definedIndicator("pullReqGlory", valOKMock.getClass().getName()); + returnedMap=underTest.getIndicatorConfiguration().definedIndicator("pullReqGlory", valOKMock.getClass().getName()); assertNotNull(returnedMap,"Debería devolver un hashmap, el indicador está definido"); assertTrue(returnedMap.containsKey("unit"),"La clave unit tiene que estar en el mapa"); assertTrue(returnedMap.containsKey("description"),"La clave description tiene que estar en el mapa"); diff --git a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java new file mode 100644 index 00000000..ffc4dce7 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java @@ -0,0 +1,137 @@ +/** + * + */ +package us.muit.fs.a4i.test.config; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.HashMap; +import java.util.logging.Logger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.config.Checker; +import us.muit.fs.a4i.config.Context; +import us.muit.fs.a4i.config.IndicatorConfiguration; + +class IndicatorConfigurationTest { + private static Logger log = Logger.getLogger(IndicatorConfigurationTest.class.getName()); + static IndicatorConfiguration underTest; + static String appConfPath; + String appIndicatorsPath = "/test/home"; + + @BeforeAll + static void setUpBeforeClass() throws Exception { + //Acciones a realizar antes de ejecutar los tests de esta clase + appConfPath="src"+File.separator+"test"+File.separator+"resources"+File.separator+"appConfTest.json"; + } + + /** + * @throws java.lang.Exception + * @see org.junit.jupiter.api.AfterAll + */ + @AfterAll + static void tearDownAfterClass() throws Exception { + //Acciones a realizar despu�s de ejecutar todos los tests de esta clase + } + + /** + * @throws java.lang.Exception + * @see org.junit.jupiter.api.BeforeEach + */ + @BeforeEach + void setUp() throws Exception { + //Acciones a realizar antes de cada uno de los tests de esta clase + //Creo el objeto bajo test, un Checker + underTest = new IndicatorConfiguration(); + } + + /** + * @throws java.lang.Exception + * @see org.junit.jupiter.api.AfterEach + */ + @AfterEach + void tearDown() throws Exception { + //Acciones a realizar despu�s de cada uno de los tests de esta clase + } + + @Test + void testDefinedIndicator() { + //Creo valores Mock para verificar si comprueba bien el tipo + //Las m�tricas del test son de enteros, as� que creo un entero y un string (el primero no dar� problemas el segundo s�) + Double valOKMock = Double.valueOf(0.3); + String valKOMock = "KO"; + HashMap returnedMap=null; + //Primero, sin fichero de configuraci�n de aplicaci�n + try { + //Consulta un indicador no definido, con valor de tipo entero + //debe devolver null, no est� definido + log.info("Busco el indicador llamado pullReqGlory"); + returnedMap=underTest.definedIndicator("pullReqGlory", valOKMock.getClass().getName()); + assertNull(returnedMap, "Deber�a ser nulo, el indicador pullReqGlory no est� definido"); + + //Busco el indicador overdued con valor double, no deber�a dar problemas + log.info("Busco el indicador overdued"); + returnedMap=underTest.definedIndicator("overdued", valOKMock.getClass().getName()); + assertNotNull(returnedMap,"Deber�a devolver un hashmap, el indicador overdued est� definido"); + assertTrue(returnedMap.containsKey("unit"),"La clave unit tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("description"),"La clave description tiene que estar en el mapa"); + + //Busco una m�trica que existe pero con un tipo incorrecto en el valor + assertNull(underTest.definedIndicator("overdued", valKOMock.getClass().getName()), + "Deber�a ser nulo, el indicador overdued est� definido para Double"); + } catch (FileNotFoundException e) { + fail("El fichero est� en la carpeta resources"); + e.printStackTrace(); + } + //Ahora establezco el fichero de configuraci�n de la aplicaci�n, con un nombre de fichero que no existe + Context.setAppRI("pepe"); + try { + //Busco un indicador que se que no est� en la configuraci�n de la api + returnedMap=underTest.definedIndicator("pullReqGlory", valOKMock.getClass().getName()); + fail("Deber�a lanzar una excepci�n porque intenta buscar en un fichero que no existe"); + } catch (FileNotFoundException e) { + log.info("Lanza la excepci�n adecuada, FileNotFoud"); + } catch (Exception e) { + fail("Lanza la excepci�n equivocada " + e); + } + + //Ahora establezco un fichero de configuraci�n de la aplicaci�n que s� existe + Context.setAppRI(appConfPath); + try { + //Busco una m�trica que se que no est� en la configuraci�n de la api pero s� en la de la aplicaci�n + log.info("Busco el indicador llamado pullReqGlory"); + returnedMap=underTest.definedIndicator("pullReqGlory", valOKMock.getClass().getName()); + assertNotNull(returnedMap,"Deber�a devolver un hashmap, el indicador est� definido"); + assertTrue(returnedMap.containsKey("unit"),"La clave unit tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("description"),"La clave description tiene que estar en el mapa"); + } catch (FileNotFoundException e) { + fail("No deber�a devolver esta excepci�n"); + } catch (Exception e) { + fail("Lanza una excepci�n no reconocida " + e); + } + } + + @Test + void testSetAppIndicators() { + String AppIndicators = appIndicatorsPath; + assertEquals(AppIndicators, appIndicatorsPath, "Deberian ser iguales"); + } + + @Test + void testListAllIndicators() { + fail("Not yet implemented"); + } + + @Test + void testGetIndicatorState() { + fail("Not yet implemented"); + } + +} diff --git a/src/test/java/us/muit/fs/a4i/test/config/MetricInfo.java b/src/test/java/us/muit/fs/a4i/test/config/MetricInfo.java index fbeacea4..013b2d3a 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/MetricInfo.java +++ b/src/test/java/us/muit/fs/a4i/test/config/MetricInfo.java @@ -28,7 +28,7 @@ class MetricInfo { void testGetChecker() { try { Checker checker=Context.getContext().getChecker(); - HashMap metricInfo=checker.getMetricInfo("issues"); + HashMap metricInfo=checker.getMetricConfiguration().getMetricInfo("issues"); assertEquals(metricInfo.get("name"),"issues","No se ha leído bien el nombre de la métrica"); assertEquals(metricInfo.get("type"),"java.lang.Integer","No se ha leído bien el tipo de la métrica"); assertEquals(metricInfo.get("description"),"Tareas sin finalizar en el repositorio","No se ha leído bien la descripción de la métrica"); From d82bdc2fd452499850af8cf2f24fcaff66d0212c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Tue, 10 Jan 2023 10:30:41 +0100 Subject: [PATCH 033/100] Preparacion V.0.2 Revisando el paquete control --- .../fs/a4i/control/IndicatorsCalculator.java | 4 +- .../us/muit/fs/a4i/control/ReportManager.java | 65 +++++++++++++++---- .../muit/fs/a4i/control/ReportManagerI.java | 44 +++---------- .../fs/a4i/control/RepositoryCalculator.java | 9 ++- .../us/muit/fs/a4i/model/entities/Report.java | 26 ++++---- .../muit/fs/a4i/model/entities/ReportI.java | 12 ++-- .../a4i/persistence/ExcelReportManager.java | 7 +- .../a4i/persistence/PersistenceManager.java | 8 +-- .../a4i/test/model/entities/ReportTest.java | 9 +-- 9 files changed, 101 insertions(+), 83 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java b/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java index 561b1cff..b11c74f3 100644 --- a/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java @@ -23,13 +23,13 @@ public interface IndicatorsCalculator { * @throws IndicatorException Si el indicador no está definido en la calculadora */ - public void calcIndicator(String name,ReportI report) throws IndicatorException; + public void calcIndicator(String indicatorName,ReportManagerI reportManager) throws IndicatorException; /** *

Calcula todos los indicadores configurados para el tipo de informe que se pasa. Debe verificar primero que el tipo de informe que se pasa es correcto

* @param report Informe sobre el que realizar el cálculo * @throws IndicatorException Si el tipo del informe no coincide con el de la calculadora */ - public void calcAllIndicators(ReportI report) throws IndicatorException; + public void calcAllIndicators(ReportManagerI reportManager) throws IndicatorException; /** * Devuelve el tipo de informe que maneja esta calculadora de indicadores diff --git a/src/main/java/us/muit/fs/a4i/control/ReportManager.java b/src/main/java/us/muit/fs/a4i/control/ReportManager.java index cfe066a7..828e5769 100644 --- a/src/main/java/us/muit/fs/a4i/control/ReportManager.java +++ b/src/main/java/us/muit/fs/a4i/control/ReportManager.java @@ -7,6 +7,7 @@ import us.muit.fs.a4i.exceptions.ReportNotDefinedException; import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItemI; import us.muit.fs.a4i.model.remote.RemoteEnquirer; import us.muit.fs.a4i.persistence.PersistenceManager; import us.muit.fs.a4i.persistence.ReportFormaterI; @@ -16,6 +17,8 @@ * */ public class ReportManager implements ReportManagerI { + + private static Logger log=Logger.getLogger(ReportManager.class.getName()); private ReportI report; private PersistenceManager persister; @@ -25,14 +28,29 @@ public class ReportManager implements ReportManagerI { private String entityId; + public ReportManager(PersistenceManager persister, RemoteEnquirer enquirer, IndicatorsCalculator calc) { + this.persister = persister; + this.enquirer = enquirer; + this.calc = calc; + } + /** + *

Borra el informe pasado como parámetro, según las reglas establecidas por el gestor de persistencia

+ * @param report El informe que se quiere borrar + */ + public static void deleteReport(ReportI report) { + + } - @Override + /** + *

Establece el objeto que se usará para consultar al servidor remoto y obtener las métricas

+ * @param remote Objeto RemoteEnquirer que consultará al servidor remoto + */ public void setRemoteEnquirer(RemoteEnquirer remote) { this.enquirer=remote; } - @Override + public void setPersistenceManager(PersistenceManager persistence) { this.persister=persistence; @@ -44,26 +62,28 @@ public void setFormater(ReportFormaterI formater) { } - @Override + public void setIndicatorCalc(IndicatorsCalculator calc) { this.calc=calc; } - - @Override + /** + *

Persiste el informe que recibe como parámetro, según las reglas del gestor de persistencia y formateador establecidos

+ * @param report

El informe a persistir

+ */ public void saveReport(ReportI report) { persister.setFormater(formater); try { persister.saveReport(report); } catch (ReportNotDefinedException e) { - log.info("No debería entrar aquí porque se acaba de establecer el informe"); + log.info("El informe que se quiere guardar no está definido"); e.printStackTrace(); } } @Override - public void save() throws ReportNotDefinedException { + public void saveReport() throws ReportNotDefinedException { if(report!=null) { saveReport(report); }else throw new ReportNotDefinedException(); @@ -71,17 +91,11 @@ public void save() throws ReportNotDefinedException { } @Override - public ReportI createReport(String entityId) { + public ReportI newReport(String entityId) { report=enquirer.buildReport(entityId); return report; } - @Override - public void deleteReport(ReportI report) { - // TODO Auto-generated method stub - - } - @Override public void deleteReport() { // TODO Auto-generated method stub @@ -96,4 +110,27 @@ public ReportI getReport() { return report; } + @Override + public void addMetric(String metricName) { + // TODO Auto-generated method stub + + } + + @Override + public ReportItemI getMetric(String metricName) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void addIndicator(String indicatorName) { + // TODO Auto-generated method stub + + } + @Override + public void getIndicator(String indicatorName) { + // TODO Auto-generated method stub + + } + } diff --git a/src/main/java/us/muit/fs/a4i/control/ReportManagerI.java b/src/main/java/us/muit/fs/a4i/control/ReportManagerI.java index f2c1c789..1e0b3a83 100644 --- a/src/main/java/us/muit/fs/a4i/control/ReportManagerI.java +++ b/src/main/java/us/muit/fs/a4i/control/ReportManagerI.java @@ -6,6 +6,7 @@ import us.muit.fs.a4i.persistence.PersistenceManager; import us.muit.fs.a4i.exceptions.ReportNotDefinedException; import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItemI; import us.muit.fs.a4i.model.remote.RemoteEnquirer; import us.muit.fs.a4i.persistence.ReportFormaterI; @@ -26,49 +27,24 @@ public interface ReportManagerI { * @return Devuelve el informe manejado */ public ReportI getReport(); - /** - *

Establece el objeto que se usará para consultar al servidor remoto y obtener las métricas

- * @param remote Objeto RemoteEnquirer que consultará al servidor remoto - */ - - public void setRemoteEnquirer(RemoteEnquirer remote); - /** - *

Establece el objeto PersistenceManager que se encargará de guardar el informe localmente

- * @param persistence Objeto PersistenceManager concreto - */ - public void setPersistenceManager(PersistenceManager persistence); + public void addMetric(String metricName); + public ReportItemI getMetric(String metricName); + public void addIndicator(String indicatorName); + public void getIndicator(String indicatorName); + public void saveReport() throws ReportNotDefinedException; + public void deleteReport(); /** *

Establece el formateador a usar

* @param formater El gestor de formato a utilizar */ public void setFormater(ReportFormaterI formater); - public void setIndicatorCalc(IndicatorsCalculator calc); - - /** - *

Persiste el informe que recibe como parámetro, según las reglas del gestor de persistencia y formateador establecidos

- * @param report

El informe a persistir

- */ - public void saveReport(ReportI report); - /** - *

Establecer el informe que se quiere crear

- * @throws ReportNotDefinedException Si no se había establecido un informe - */ - public void save() throws ReportNotDefinedException; - /** *

Crea un informe para la entidad indicada como parámetro, según las reglas del RemoteBuilder Establecido

*

El id debe identificar unívocamente a la entidad en el remoto

* @param id Identificador de la entidad a la que se refiere el informe * @return el informe creado */ - public ReportI createReport(String id); - /** - *

Borra el informe pasado como parámetro, según las reglas establecidas por el gestor de persistencia

- * @param report El informe que se quiere borrar - */ - public void deleteReport(ReportI report); - /** - *

Borra el informe que se está manejando actualmente, si la referencia no era nula, según las reglas establecidas por el gestor de persistencia

- */ - public void deleteReport(); + public ReportI newReport(String entityId); + + } diff --git a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java index cf85c093..34208d4b 100644 --- a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java @@ -5,6 +5,7 @@ import java.util.logging.Logger; +import us.muit.fs.a4i.exceptions.IndicatorException; import us.muit.fs.a4i.model.entities.Indicator; import us.muit.fs.a4i.model.entities.ReportI; @@ -18,8 +19,8 @@ public class RepositoryCalculator implements IndicatorsCalculator { private static Logger log=Logger.getLogger(RepositoryCalculator.class.getName()); @Override - public void calcIndicator(String name, ReportI report) { - log.info("Calcula el indicador de nombre "+name); + public void calcIndicator(String indicatorName, ReportManagerI reportManager) throws IndicatorException{ + log.info("Calcula el indicador de nombre "+indicatorName); /** * Tiene que mirar si están ya las métricas que necesita * Si están lo calcula @@ -34,7 +35,7 @@ public void calcIndicator(String name, ReportI report) { * */ @Override - public void calcAllIndicators(ReportI report) { + public void calcAllIndicators(ReportManagerI reportManager) throws IndicatorException{ log.info("Calcula todos los indicadores del repositorio y los incluye en el informe"); } private Indicator commitsPerUser(ReportI report) { @@ -46,4 +47,6 @@ private Indicator commitsPerUser(ReportI report) { public ReportI.ReportType getReportType() { return ReportI.ReportType.REPOSITORY; } + + } diff --git a/src/main/java/us/muit/fs/a4i/model/entities/Report.java b/src/main/java/us/muit/fs/a4i/model/entities/Report.java index 9e05a209..fd703b45 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/Report.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/Report.java @@ -36,17 +36,17 @@ public class Report implements ReportI { private ReportI.ReportType type=null; /** - * Mapa de M�tricas + * Mapa de Métricas * * */ - private HashMap metrics; + private HashMap metrics; /** * Mapa de indicadores */ - private HashMap indicators; + private HashMap indicators; public Report(){ createMaps(); @@ -66,8 +66,8 @@ public Report(ReportType type,String entityId){ this.entityId=entityId; } private void createMaps() { - metrics=new HashMap(); - indicators=new HashMap(); + metrics=new HashMap(); + indicators=new HashMap(); } /** *

Busca la m�trica solicita en el informe y la devuelve

@@ -75,9 +75,9 @@ private void createMaps() { * @param name Nombre de la m�trica buscada * @return la m�trica localizada */ - public ReportItem getMetricByName(String name) { + public ReportItemI getMetricByName(String name) { log.info("solicitada métrica de nombre "+name); - ReportItem metric=null; + ReportItemI metric=null; if (metrics.containsKey(name)){ log.info("La m�trica est� en el informe"); @@ -90,7 +90,7 @@ public ReportItem getMetricByName(String name) { */ @Override - public void addMetric(ReportItem met) { + public void addMetric(ReportItemI met) { metrics.put(met.getName(), met); log.info("A�adida m�trica "+met+" Con nombre "+met.getName()); } @@ -101,9 +101,9 @@ public void addMetric(ReportItem met) { * @return el indicador localizado */ @Override - public ReportItem getIndicatorByName(String name) { + public ReportItemI getIndicatorByName(String name) { log.info("solicitado indicador de nombre "+name); - ReportItem indicator=null; + ReportItemI indicator=null; if (indicators.containsKey(name)){ indicator=indicators.get(name); @@ -115,7 +115,7 @@ public ReportItem getIndicatorByName(String name) { * */ @Override - public void addIndicator(ReportItem ind) { + public void addIndicator(ReportItemI ind) { indicators.put(ind.getName(), ind); log.info("A�adido indicador "+ind); @@ -142,13 +142,13 @@ public String toString() { return repoinfo; } @Override - public Collection getAllMetrics() { + public Collection getAllMetrics() { // TODO Auto-generated method stub return metrics.values(); } @Override - public Collection getAllIndicators() { + public Collection getAllIndicators() { // TODO Auto-generated method stub return indicators.values(); } diff --git a/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java b/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java index 39d1add8..6b8f4265 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java @@ -34,17 +34,17 @@ public static enum ReportType{ * @param name Nombre de la métrica solicitada * @return Métrica solicitada */ - ReportItem getMetricByName(String name); + ReportItemI getMetricByName(String name); /** * Obtiene todas las métricas del informe * @return Colleción de métricas que contiene el informe */ - Collection getAllMetrics(); + Collection getAllMetrics(); /** * Añade una métrica al informe * @param met Nueva métrica */ - void addMetric(ReportItem met); + void addMetric(ReportItemI metric); /** * Obtiene un indicador del informe a partir del nombre del mismo @@ -52,12 +52,12 @@ public static enum ReportType{ * @return El indicador */ - ReportItem getIndicatorByName(String name); + ReportItemI getIndicatorByName(String indicatorName); /** * Obtiene todos los indicadores del informe * @return el conjunto de indicadores del informe */ - Collection getAllIndicators(); + Collection getAllIndicators(); /** * Añade un indicador al informe @@ -65,7 +65,7 @@ public static enum ReportType{ */ - void addIndicator(ReportItem ind); + void addIndicator(ReportItemI newIndicator); /** * Calcula un indicador a partir de su nombre y lo a�ade al informe diff --git a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java index 2fec295a..e6b9f728 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java @@ -163,8 +163,8 @@ public void saveReport(ReportI report) throws ReportNotDefinedException{ rowIndex++; sheet.createRow(rowIndex).createCell(0).setCellValue("Métricas tomadas el día "); sheet.getRow(rowIndex).createCell(1).setCellValue(Date.from(LocalDateTime.now().toInstant(ZoneOffset.UTC)).toString()); - Collection collection=report.getAllMetrics(); - for(ReportItem metric:collection) { + Collection collection=report.getAllMetrics(); + for(ReportItemI metric:collection) { persistMetric(metric); } @@ -240,8 +240,9 @@ private void persistIndicator(ReportItemI indicator) { } + @Override - public void deleteReport() throws ReportNotDefinedException { + public void deleteReport(ReportI report) throws ReportNotDefinedException { // TODO Auto-generated method stub } diff --git a/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java b/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java index 23e438a3..91a0da4d 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java @@ -7,7 +7,7 @@ /** *

Interfaz de los gestores de persistencia

- * @author isa + * @author Isabel Román * */ public interface PersistenceManager { @@ -21,14 +21,14 @@ public interface PersistenceManager { /** *

Persiste el informe

- * @throws ReportNotDefinedException Si no se estableció el informe lanzará error + * @throws ReportNotDefinedException, si el informe es nulo dará error */ void saveReport(ReportI report) throws ReportNotDefinedException; /** *

Borra el informe

* - * @throws ReportNotDefinedException Si no se estableció el informe dará error + * @throws ReportNotDefinedException,si el informe es nulo dará error */ - void deleteReport()throws ReportNotDefinedException; + void deleteReport(ReportI report)throws ReportNotDefinedException; } \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java b/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java index a0c08fbd..703b7482 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/entities/ReportTest.java @@ -31,6 +31,7 @@ import us.muit.fs.a4i.model.entities.Report; import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; @@ -185,7 +186,7 @@ void testAddMetric() { //Prueba a sustituir por la línea comentada Mockito.verify(metricIntMock,atLeast(1)).getName(); //Mockito.verify(metricIntMock, atLeast(1)).getName(); - ReportItem metric=reportTested.getMetricByName("issues"); + ReportItemI metric=reportTested.getMetricByName("issues"); assertEquals(metric.getValue(),3,"Deberíaa tener el valor especificado en el mock"); assertEquals(metric.getDescription(),"Tareas sin finalizar en el repositorio","Debería tener el valor especificado en el mock"); @@ -237,7 +238,7 @@ void testAddIndicator() { //Prueba a sustituir por la línea comentada Mockito.verify(indicatorIntMock).getName(); //Mockito.verify(metricIntMock, atLeast(1)).getName(); - ReportItem indicator=reportTested.getIndicatorByName("issues"); + ReportItemI indicator=reportTested.getIndicatorByName("issues"); assertEquals(indicator.getValue(),3,"Debería tener el valor especificado en el mock"); assertEquals(indicator.getDescription(),"Tareas sin finalizar en el repositorio","Debería tener el valor especificado en el mock"); @@ -396,7 +397,7 @@ void testToString() { @Test @Tag("noacabado") void testGetAllMetrics() { - ReportItem metric= null; + ReportItemI metric= null; reportTested=new Report(); try { @@ -424,7 +425,7 @@ void setMetricsMocks() { @Test @Tag("noacabado") void testGetAllIndicators() { - ReportItem indicator= null; + ReportItemI indicator= null; reportTested=new Report(); try { From b0966fe36799cd195bf7581ada0061eebb07e087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Wed, 11 Jan 2023 13:54:59 +0100 Subject: [PATCH 034/100] Preparando v.0.2 Trabajos principales en ReportManager --- .../java/us/muit/fs/a4i/config/Context.java | 5 +- .../us/muit/fs/a4i/control/ReportManager.java | 94 ++++++++- .../muit/fs/a4i/control/ReportManagerI.java | 8 +- .../fs/a4i/control/RepositoryCalculator.java | 2 +- .../fs/a4i/model/remote/GitHubEnquirer.java | 7 +- .../remote/GitHubRepositoryEnquirer.java | 2 + .../fs/a4i/model/remote/RemoteEnquirer.java | 9 +- .../a4i/persistence/PersistenceManager.java | 7 +- .../fs/a4i/persistence/ReportFormater.java | 2 +- src/main/resources/a4i.conf | 4 +- src/main/resources/log.properties | 4 +- .../a4i/test/control/ReportManagerTest.java | 190 ++++++++++++++++++ .../a4i/test/control/SupervisorControl.java | 4 + 13 files changed, 316 insertions(+), 22 deletions(-) create mode 100644 src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java diff --git a/src/main/java/us/muit/fs/a4i/config/Context.java b/src/main/java/us/muit/fs/a4i/config/Context.java index be1316a4..929759f3 100644 --- a/src/main/java/us/muit/fs/a4i/config/Context.java +++ b/src/main/java/us/muit/fs/a4i/config/Context.java @@ -63,7 +63,9 @@ public class Context { private Context() throws IOException { setProperties(); + log.info("Propiedades del contexto establecidas"); checker = new Checker(); + log.info("Checker creado"); } public static void setAppRI(String filename) { appFile=filename; @@ -144,6 +146,7 @@ public String getPersistenceType() throws IOException { * @throws IOException si hay problemas al consultar las propiedades */ public String getRemoteType() throws IOException { + log.info("Se solicita el tipo de remoto"); return properties.getProperty("remote.type"); } @@ -241,7 +244,7 @@ private void setProperties() throws IOException { InputStream is=this.getClass().getResourceAsStream(filePath); log.info("InputStream "+is+" para "+filePath); properties.load(is); - log.fine("Listado de propiedades "+properties); + log.info("Listado de propiedades "+properties); } diff --git a/src/main/java/us/muit/fs/a4i/control/ReportManager.java b/src/main/java/us/muit/fs/a4i/control/ReportManager.java index 828e5769..1b5b16f8 100644 --- a/src/main/java/us/muit/fs/a4i/control/ReportManager.java +++ b/src/main/java/us/muit/fs/a4i/control/ReportManager.java @@ -3,17 +3,22 @@ */ package us.muit.fs.a4i.control; +import java.io.IOException; import java.util.logging.Logger; +import us.muit.fs.a4i.config.Context; import us.muit.fs.a4i.exceptions.ReportNotDefinedException; import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItemI; +import us.muit.fs.a4i.model.remote.GitHubRepositoryEnquirer; import us.muit.fs.a4i.model.remote.RemoteEnquirer; +import us.muit.fs.a4i.persistence.ExcelReportManager; import us.muit.fs.a4i.persistence.PersistenceManager; +import us.muit.fs.a4i.persistence.ReportFormater; import us.muit.fs.a4i.persistence.ReportFormaterI; /** - * @author isa + * @author Isabel Román * */ public class ReportManager implements ReportManagerI { @@ -26,12 +31,61 @@ public class ReportManager implements ReportManagerI { private ReportFormaterI formater; private IndicatorsCalculator calc; private String entityId; + private ReportI.ReportType reportType; - public ReportManager(PersistenceManager persister, RemoteEnquirer enquirer, IndicatorsCalculator calc) { - this.persister = persister; - this.enquirer = enquirer; - this.calc = calc; + public ReportManager(ReportI.ReportType reportType) throws IOException { + + this.reportType=reportType; + this.formater=new ReportFormater(); + //Selecting the RemoteEnquierer + RemoteEnquirer.RemoteType remote=null; + + remote= RemoteEnquirer.RemoteType.valueOf(Context.getContext().getRemoteType()); + log.info("Tipo de remoto configurado: "+remote); + switch (remote) { + case GITHUB: + setGHEnquierer(); + break; + default: + log.info("Tipo de persistencia no implementando"); + + } + //Selecting the persister + PersistenceManager.PersistenceType persistence=null; + persistence=PersistenceManager.PersistenceType.valueOf(Context.getContext().getPersistenceType()); + switch(persistence) { + case DDBB: + log.info("La persistencia para base de datos aún no está implemetnada"); + break; + case EXCEL: + this.persister=new ExcelReportManager(); + log.info("Se instancia el objeto para persistir informes en excel"); + break; + default: + break; + + } + //Selecting the IndicatorCalculator + switch(this.reportType) { + case DEVELOPER: + log.info("La calculadora para indicadores de desarrolladores no está implementada"); + break; + case ORGANIZATION: + log.info("La calculadora para indicadores de organización no está implementada"); + break; + case PROJECT: + log.info("La calculadora para indicadores de proyecto no está implementada"); + break; + case REPOSITORY: + this.calc=new RepositoryCalculator(); + log.info("Se instancia la calculadora para indicadores de respositorio"); + break; + default: + break; + + } + } /** *

Borra el informe pasado como parámetro, según las reglas establecidas por el gestor de persistencia

@@ -91,7 +145,12 @@ public void saveReport() throws ReportNotDefinedException { } @Override - public ReportI newReport(String entityId) { + public ReportI newReport(String entityId,ReportI.ReportType reportType) throws Exception { + if (reportType != this.reportType){ + log.info("Se está intentando crear un tipo de informe diferente al ReportManager usado"); + throw new Exception("El tipo de informe no coincide con el del ReportManager"); + } + report=enquirer.buildReport(entityId); return report; } @@ -132,5 +191,28 @@ public void getIndicator(String indicatorName) { // TODO Auto-generated method stub } + + + private void setGHEnquierer() { + switch(this.reportType) { + case REPOSITORY: + this.enquirer=new GitHubRepositoryEnquirer(); + log.info("Se instancia el enquierer para informes de repositorio con info de github"); + break; + case DEVELOPER: + log.info("El enquierer para los informes de desarrolladores con info de github aún no está implementado"); + break; + case ORGANIZATION: + log.info("El enquierer para los informes de la organización con info de github aún no está implementado"); + break; + case PROJECT: + log.info("El enquierer para los informes de proyecto con info de github aún no está implementado"); + break; + default: + log.info("El tipo de informe que se está solicitando no está implementado"); + break; + } + } } + diff --git a/src/main/java/us/muit/fs/a4i/control/ReportManagerI.java b/src/main/java/us/muit/fs/a4i/control/ReportManagerI.java index 1e0b3a83..d8d5076e 100644 --- a/src/main/java/us/muit/fs/a4i/control/ReportManagerI.java +++ b/src/main/java/us/muit/fs/a4i/control/ReportManagerI.java @@ -20,6 +20,7 @@ * @author Isabel Román * */ + public interface ReportManagerI { /** @@ -41,10 +42,13 @@ public interface ReportManagerI { /** *

Crea un informe para la entidad indicada como parámetro, según las reglas del RemoteBuilder Establecido

*

El id debe identificar unívocamente a la entidad en el remoto

- * @param id Identificador de la entidad a la que se refiere el informe + * @param entityId Identificador de la entidad a la que se refiere el informe + * @param reportType El tipo de informe * @return el informe creado + * @throws Exception el tipo de informe no coincide con el del manager */ - public ReportI newReport(String entityId); + public ReportI newReport(String entityId, ReportI.ReportType reportType) throws Exception; + } diff --git a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java index 34208d4b..87868733 100644 --- a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java @@ -24,7 +24,7 @@ public void calcIndicator(String indicatorName, ReportManagerI reportManager) th /** * Tiene que mirar si están ya las métricas que necesita * Si están lo calcula - * Si no están busca las métricas, las añade y lo calcula + * Si no están busca las métricas, las añade al informe y lo calcula * */ diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java index fedad8d6..b8f12af7 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java @@ -41,15 +41,14 @@ public GitHubEnquirer() { */ protected GitHub getConnection() { - if(github==null) try { - - log.info("Creando el objeto GitHub"); + if(github==null) try { github = GitHubBuilder.fromEnvironment().build(); - + log.info("Creado el objeto GitHub"); }catch(Exception e) { log.info(e+" No se puede crear la instancia GitHub\n"); log.info("Recuerde que debe configurar las variables de entorno GITHUB_LOGIN y GITHUB_OAUTH con su nombre de usuario y token respectivamente"); + e.printStackTrace(); } return github; } diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index b1943c7c..9d5a58c9 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -35,6 +35,7 @@ public GitHubRepositoryEnquirer() { metricNames.add("subscribers"); metricNames.add("forks"); metricNames.add("watchers"); + log.info("Añadidas métricas al GHRepositoryEnquirer"); } @Override @@ -63,6 +64,7 @@ public ReportI buildReport(String repositoryId) { GitHub gb = getConnection(); remoteRepo = gb.getRepository(repositoryId); + log.info("El repositorio es de "+remoteRepo.getOwnerName()+" Y su descripción es "+remoteRepo.getDescription()); log.info("leído " + remoteRepo); myRepo = new Report(repositoryId); diff --git a/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java index f6ec8c68..fc6f6338 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java @@ -10,6 +10,7 @@ import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; /** *

Interfaz para desacoplar el mecanismo de obtención de métricas del servidor remoto que se use como fuente de las mismas

@@ -20,6 +21,10 @@ */ public interface RemoteEnquirer{ + public static enum RemoteType{ + GITHUB + } + /** *

Construye el informe sobre la entidad indicada con las métricas por defecto

* @param entityId Identificador unívoco en el remoto de la entidad sobre la que se quiere informar. @@ -34,11 +39,13 @@ public interface RemoteEnquirer{ * @return La nueva métrica construida tras la consulta al remoto * @throws MetricException Si la métrica no está definida */ - public ReportItem getMetric(String metricName,String entityId) throws MetricException; + public ReportItemI getMetric(String metricName,String entityId) throws MetricException; /** *

Devuelve las métricas que el objeto RemoteEnquirer concreto puede obtener del servidor remoto

* @return El listado de los nombres de métricas definidas */ public List getAvailableMetrics(); + + } diff --git a/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java b/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java index 91a0da4d..f2c3388c 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java @@ -11,8 +11,11 @@ * */ public interface PersistenceManager { - - + + public static enum PersistenceType{ + EXCEL, + DDBB + } /** *

Establece el elemento que establece el formato

* @param formater Elemento que maneja las características de formato diff --git a/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java b/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java index b5ab1ffd..dd8f65fd 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java @@ -30,7 +30,7 @@ public class ReportFormater implements ReportFormaterI { */ private HashMap indicatorsFont; - ReportFormater(){ + public ReportFormater(){ this.indicatorsFont=new HashMap(); //Sólo se construye el mapa, conforme se vayan solicitando se irán rellenando } diff --git a/src/main/resources/a4i.conf b/src/main/resources/a4i.conf index 7a5c087e..18a13dcd 100644 --- a/src/main/resources/a4i.conf +++ b/src/main/resources/a4i.conf @@ -1,8 +1,8 @@ #Fichero de configuración por defecto de la api #Tipo de persistencia que se quiere usar -persistence.type=excel +persistence.type=EXCEL #Tipo de remoto que se quiere usar -remote.type=github +remote.type=GITHUB #Caracteristicas por defecto de la fuente, cuando los informes son visuales Font.default.color=Black Font.default.height=12 diff --git a/src/main/resources/log.properties b/src/main/resources/log.properties index 75939489..e4d3243d 100644 --- a/src/main/resources/log.properties +++ b/src/main/resources/log.properties @@ -14,7 +14,7 @@ handlers = java.util.logging.FileHandler,java.util.logging.ConsoleHandler # configuración de manejador de archivos # nivel soportado para archivos -java.util.logging.FileHandler.level = INFO +java.util.logging.FileHandler.level = ALL # archivo de almacenamiento de las salidas de log java.util.logging.FileHandler.pattern = Log/trazas$.log # maximo tamaño de archivo en bytes @@ -30,7 +30,7 @@ java.util.logging.FileHandler.append = false # configuración de manejador de consola # nivel soportado para consola -java.util.logging.ConsoleHandler.level = INFO +java.util.logging.ConsoleHandler.level = ALL # clase para formatear salida hacia consola #java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter java.util.logging.ConsoleHandler.formatter = java.util.logging.AnsiFormatter \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java b/src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java new file mode 100644 index 00000000..06df8f20 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java @@ -0,0 +1,190 @@ +/** + * + */ +package us.muit.fs.a4i.test.control; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.util.logging.Logger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.control.ReportManager; +import us.muit.fs.a4i.control.ReportManagerI; +import us.muit.fs.a4i.model.entities.Report; +import us.muit.fs.a4i.model.entities.ReportI; + +/** + * @author isabo + * + */ +class ReportManagerTest { + private static Logger log=Logger.getLogger(ReportManagerTest.class.getName()); + + /** + * @throws java.lang.Exception + */ + @BeforeAll + static void setUpBeforeClass() throws Exception { + } + + /** + * @throws java.lang.Exception + */ + @AfterAll + static void tearDownAfterClass() throws Exception { + } + + /** + * @throws java.lang.Exception + */ + @BeforeEach + void setUp() throws Exception { + } + + /** + * @throws java.lang.Exception + */ + @AfterEach + void tearDown() throws Exception { + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#ReportManager(us.muit.fs.a4i.model.entities.ReportI.ReportType)}. + */ + @Test + void testReportManager() { + ReportManagerI manager=null; + ReportI report=null; + try { + manager=new ReportManager(ReportI.ReportType.REPOSITORY); + assertNotNull(manager,"No se ha creado el gestor"); + log.info("Creado el gestor"); + report=manager.newReport("MIT-FS/Audit4Improve-API", ReportI.ReportType.REPOSITORY); + log.info("Creado el informe"); + } catch (Exception e) { + fail("Se lanza alguna excepción al crear el gestor o el informe"); + e.printStackTrace(); + } + assertNotNull(report,"No se ha construido el informe"); + System.out.println(report); + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#deleteReport(us.muit.fs.a4i.model.entities.ReportI)}. + */ + @Test + void testDeleteReportReportI() { + fail("Not yet implemented"); + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#setRemoteEnquirer(us.muit.fs.a4i.model.remote.RemoteEnquirer)}. + */ + @Test + void testSetRemoteEnquirer() { + fail("Not yet implemented"); + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#setPersistenceManager(us.muit.fs.a4i.persistence.PersistenceManager)}. + */ + @Test + void testSetPersistenceManager() { + fail("Not yet implemented"); + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#setFormater(us.muit.fs.a4i.persistence.ReportFormaterI)}. + */ + @Test + void testSetFormater() { + fail("Not yet implemented"); + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#setIndicatorCalc(us.muit.fs.a4i.control.IndicatorsCalculator)}. + */ + @Test + void testSetIndicatorCalc() { + fail("Not yet implemented"); + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#saveReport(us.muit.fs.a4i.model.entities.ReportI)}. + */ + @Test + void testSaveReportReportI() { + fail("Not yet implemented"); + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#saveReport()}. + */ + @Test + void testSaveReport() { + fail("Not yet implemented"); + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#newReport(java.lang.String, us.muit.fs.a4i.model.entities.ReportI.ReportType)}. + */ + @Test + void testNewReport() { + fail("Not yet implemented"); + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#deleteReport()}. + */ + @Test + void testDeleteReport() { + fail("Not yet implemented"); + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#getReport()}. + */ + @Test + void testGetReport() { + fail("Not yet implemented"); + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#addMetric(java.lang.String)}. + */ + @Test + void testAddMetric() { + fail("Not yet implemented"); + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#getMetric(java.lang.String)}. + */ + @Test + void testGetMetric() { + fail("Not yet implemented"); + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#addIndicator(java.lang.String)}. + */ + @Test + void testAddIndicator() { + fail("Not yet implemented"); + } + + /** + * Test method for {@link us.muit.fs.a4i.control.ReportManager#getIndicator(java.lang.String)}. + */ + @Test + void testGetIndicator() { + fail("Not yet implemented"); + } + +} diff --git a/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java b/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java index 75a70cac..3b395a03 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java +++ b/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java @@ -61,8 +61,11 @@ public static void main(String[] args) { GHOrganization unaOrg = github.getOrganization("MIT-FS"); // PagedIterable repos=unaOrg.listRepositories(); System.out.println("Recupero la organización "+unaOrg.getId()); + + GHRepository githubrepo=github.getRepository("hub4j/github-api"); System.out.println("Este repositorio es de "+githubrepo.getOwnerName()+" Y su descripción es "+githubrepo.getDescription()); + GHRepositoryStatistics estadisticas=githubrepo.getStatistics(); log.info("Estadisticas recogidas"); @@ -94,6 +97,7 @@ public static void main(String[] args) { }catch(Exception e) { log.info(e+" No se puede crear la instancia GitHub\n"); log.info("Recuerde que debe configurar las variables de entorno GITHUB_LOGIN y GITHUB_OAUTH con su nombre de usuario y token respectivamente"); + e.printStackTrace(); } } From 62b79c21a9f6aee75cc322a5081bd9ae757a68b2 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Wed, 11 Jan 2023 22:55:07 +0100 Subject: [PATCH 035/100] =?UTF-8?q?Peque=C3=B1os=20cambios=20V.0.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fs/a4i/config/IndicatorConfiguration.java | 4 ++-- .../muit/fs/a4i/config/MetricConfiguration.java | 16 ++++++++-------- .../fs/a4i/control/RepositoryCalculator.java | 4 ++-- .../muit/fs/a4i/model/entities/ReportItem.java | 2 +- .../fs/a4i/persistence/PersistenceManager.java | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) 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 112d5798..a54d4b86 100644 --- a/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java @@ -77,7 +77,7 @@ private HashMap isDefinedIndicator(String indicatorName, String log.info("Muestro la configuraci�n le�da " + confObject); JsonArray indicators = confObject.getJsonArray("indicators"); - log.info("El n�mero de indicadores es " + indicators.size()); + log.info("El número de indicadores es " + indicators.size()); for (int i = 0; i < indicators.size(); i++) { log.info("nombre: " + indicators.get(i).asJsonObject().getString("name")); if (indicators.get(i).asJsonObject().getString("name").equals(indicatorName)) { @@ -97,7 +97,7 @@ private HashMap isDefinedIndicator(String indicatorName, String @Override public List listAllIndicators() throws FileNotFoundException { - log.info("Consulta todas las m�tricas"); + log.info("Consulta todas las métricas"); List allmetrics = new ArrayList(); diff --git a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java index 9d3c9edf..5b3c21b2 100644 --- a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java @@ -32,7 +32,7 @@ private HashMap isDefinedMetric(String metricName, String metric log.info("Muestro la configuración leída " + confObject); JsonArray metrics = confObject.getJsonArray("metrics"); - log.info("El número de mútricas es " + metrics.size()); + log.info("El número de métricas es " + metrics.size()); for (int i = 0; i < metrics.size(); i++) { log.info("nombre: " + metrics.get(i).asJsonObject().getString("name")); if (metrics.get(i).asJsonObject().getString("name").equals(metricName)) { @@ -137,7 +137,7 @@ public HashMap getMetricInfo(String name) throws FileNotFoundExc @Override public List listAllMetrics() throws FileNotFoundException { - log.info("Consulta todas las m�tricas"); + log.info("Consulta todas las métricas"); List allmetrics = new ArrayList(); @@ -154,11 +154,11 @@ public List listAllMetrics() throws FileNotFoundException { log.info("Leo el objeto"); reader.close(); - log.info("Muestro la configuraci�n le�da " + confObject); + log.info("Muestro la configuración leída " + confObject); JsonArray metrics = confObject.getJsonArray("metrics"); - log.info("El n�mero de m�tricas es " + metrics.size()); + log.info("El número de métricas es " + metrics.size()); for (int i = 0; i < metrics.size(); i++) { - log.info("A�ado nombre: " + metrics.get(i).asJsonObject().getString("name")); + log.info("Añado nombre: " + metrics.get(i).asJsonObject().getString("name")); allmetrics.add(metrics.get(i).asJsonObject().getString("name")); } if (Context.getAppRI() != null) { @@ -168,11 +168,11 @@ public List listAllMetrics() throws FileNotFoundException { confObject = reader.readObject(); reader.close(); - log.info("Muestro la configuraci�n le�da " + confObject); + log.info("Muestro la configuración leída " + confObject); metrics = confObject.getJsonArray("metrics"); - log.info("El n�mero de m�tricas es " + metrics.size()); + log.info("El número de m´rtricas es " + metrics.size()); for (int i = 0; i < metrics.size(); i++) { - log.info("A�ado nombre: " + metrics.get(i).asJsonObject().getString("name")); + log.info("Añado nombre: " + metrics.get(i).asJsonObject().getString("name")); allmetrics.add(metrics.get(i).asJsonObject().getString("name")); } } diff --git a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java index 219a6f72..21de1ce2 100644 --- a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java @@ -11,14 +11,14 @@ /** *

- * Implementa los m�todos para calcular indicadores referidos a un repositorio + * Implementa los métodos para calcular indicadores referidos a un repositorio * repositorio *

*

* Puede hacerse uno a uno o todos a la vez *

* - * @author Isabel Rom�n + * @author Isabel Román * */ public class RepositoryCalculator implements IndicatorsCalculator { diff --git a/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java b/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java index 8f300693..9343a35b 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java @@ -35,7 +35,7 @@ public class ReportItem implements ReportItemI { */ private Date date; /** - * Descripci�n del elemento del informe + * Descripción del elemento del informe */ private String description; /** diff --git a/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java b/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java index fa7079f1..ead56d96 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/PersistenceManager.java @@ -42,7 +42,7 @@ public static enum PersistenceType { * Borra el informe *

* - * @param report informe a persistir + * @param report informe a borrar * @throws ReportNotDefinedException si el informe es nulo dará error */ void deleteReport(ReportI report) throws ReportNotDefinedException; From 3d024fd0eedb48e6f05bcdc55dd5cb9a78c63b40 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Fri, 20 Jan 2023 20:24:37 +0100 Subject: [PATCH 036/100] Repaso del paquete config preparando V.0.2 --- .../java/us/muit/fs/a4i/config/Context.java | 68 ++++++++--- .../a4i/config/IndicatorConfigurationI.java | 13 ++- .../fs/a4i/config/MetricConfiguration.java | 5 +- .../fs/a4i/config/MetricConfigurationI.java | 8 +- src/main/resources/a4i.conf | 4 +- src/main/resources/a4iDefault.json | 2 +- .../muit/fs/a4i/test/config/ContextTest.java | 110 ++++++++++++++++-- src/test/resources/appConfTest.json | 4 +- src/test/resources/appTest.conf | 6 + 9 files changed, 186 insertions(+), 34 deletions(-) create mode 100644 src/test/resources/appTest.conf diff --git a/src/main/java/us/muit/fs/a4i/config/Context.java b/src/main/java/us/muit/fs/a4i/config/Context.java index c8cd3645..cccc33cb 100644 --- a/src/main/java/us/muit/fs/a4i/config/Context.java +++ b/src/main/java/us/muit/fs/a4i/config/Context.java @@ -4,6 +4,7 @@ package us.muit.fs.a4i.config; import java.awt.Font; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -45,19 +46,40 @@ public class Context { private static Logger log = Logger.getLogger(Context.class.getName()); private static Context contextInstance = null; + /** + * Propiedades de la API + * Se leerán del fichero de configuración por defecto y del especificado por la aplicación, si lo hubiera + */ private Properties properties = null; - // Fichero de propiedades de la API, embebido en el jar + /** + * Fichero de propiedades de configuración de la API, embebido en el jar + */ private static String confFile = "a4i.conf"; - // Fichero de propiedades de la API establecido por la aplicaci´pn cliente - private static String appConFile = null; - // Fichero de configuración de métricas e indicadores por defecto, embebido en - // el jar + + /** + * Fichero de especificación de métricas e indicadores por defecto, embebido en el jar + * + */ private static String defaultFile = "a4iDefault.json"; - // Fichero de configuración de métricas e indicadores establecido por la - // aplicación cliente + + /** + * Fichero de propiedades de la API establecido por la aplicación cliente + */ + private static String appConFile = null; + + /** + * Fichero de especificación de métricas e indicadores establecido por la aplicación cliente + */ private static String appFile = null; + /** + * Referencia al verificador de métricas e indicadores + */ private Checker checker = null; + /** + *

Constructor privado, sigue el patrón singleton. El único objeto posible se crea al invocar el método getContext

+ * @throws IOException + */ private Context() throws IOException { setProperties(); log.info("Propiedades del contexto establecidas"); @@ -65,14 +87,25 @@ private Context() throws IOException { log.info("Checker creado"); } + /** + *

Establece la ruta del fichero de métricas e indicadores indicado por el cliente/aplicación

+ * @param filename ruta al fichero de configuración de métricas e indicadores de la aplicación cliente + */ public static void setAppRI(String filename) { appFile = filename; } + /** + *

Consulta la ruta del fichero de configuración de métricas e indicadores del cliente/aplicación

+ * @return + */ public static String getAppRI() { return appFile; } + /** + * @return la ruta al fichero de configuración de indicadores y métricas por defecto + */ public static String getDefaultRI() { return defaultFile; } @@ -114,10 +147,11 @@ public static void setAppConf(String appConPath) throws IOException { appConFile = appConPath; // customFile=System.getenv("APP_HOME")+customFile; - // Otra opción, Usar una variable de entorno para la localizar la ruta de + // Otra opción, Usar una variable de entorno para localizar la ruta de // instalación y de ahí coger el fichero de configuración // También podría localizarse en el home de usuario getContext().properties.load(new FileInputStream(appConPath)); + log.info("Las nuevas propiedades son "+getContext().properties); } public Checker getChecker() { @@ -158,15 +192,21 @@ public String getRemoteType() throws IOException { * defecto se crea una fuente simple *

* - * @return La fuente por defecto para indicadores y m�tricas + * @return La fuente por defecto para indicadores y métricas */ public Font getDefaultFont() { - Font font = null; + //OJO el color no forma parte de la clase font, por loq ue ese atributo debe estar fuera + //Podría incluir un parámetro font para devolverlo a la salida y que lo que devuelva sea un String con el color + log.info("Busca la información de configuración de la fuente, por defecto"); + // TO DO String color = properties.getProperty("Font.default.color"); String height = properties.getProperty("Font.default.height"); String type = properties.getProperty("Font.default.type"); - return font; + log.info("Los datos son, color: "+color+" height: "+height+" type: "+type); + log.info("Intento crear la fuente"); + + return new Font(type, Font.ITALIC, Integer.valueOf(height)); } /** @@ -174,7 +214,7 @@ public Font getDefaultFont() { * No Implementado *

*

- * Deberóa leer las propiedades adecuadas, como color, taño, tipo... y construir + * Deberá leer las propiedades adecuadas, como color, tamaño, tipo... y construir * un objeto Font *

*

@@ -213,7 +253,7 @@ public static Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOEx /** *

- * Consulta el nombre de todas las propiedades le�dasí + * Consulta el nombre de todas las propiedades leídas *

* * @return Conjunto con todos los nombres de las propiedades de configuración @@ -234,7 +274,7 @@ public Set getPropertiesNames() throws IOException { * @throws IOException si hay problemas leyendo el fichero */ private void setProperties() throws IOException { - log.info("Lectura del fichero de configuraci�n por defecto"); + log.info("Lectura del fichero de configuración por defecto"); FileInputStream file; // Establecemos las propiedades por defecto, del fichero de configuración // embebido en el jar diff --git a/src/main/java/us/muit/fs/a4i/config/IndicatorConfigurationI.java b/src/main/java/us/muit/fs/a4i/config/IndicatorConfigurationI.java index fae80c94..5876b020 100644 --- a/src/main/java/us/muit/fs/a4i/config/IndicatorConfigurationI.java +++ b/src/main/java/us/muit/fs/a4i/config/IndicatorConfigurationI.java @@ -15,8 +15,19 @@ * */ public interface IndicatorConfigurationI { + /** + *

Verifica si el indicador existe y el tipo es correcto

+ * @param name nombre del indicador + * @param type tipo del valor del indicador + * @return mapa con los detalles del indicador + * @throws FileNotFoundException si hay algún problema con el fichero de configuración + */ public HashMap definedIndicator(String name, String type) throws FileNotFoundException; + /** + * @return lista con los nombres de todos los indicadores disponibles + * @throws FileNotFoundException en caso de problemas con el fichero de configuración + */ public List listAllIndicators() throws FileNotFoundException; /** @@ -25,7 +36,7 @@ public interface IndicatorConfigurationI { * configuración de estados en el fichero *

* - * @param indicator + * @param indicator ReportItem para el que se quiere calcular el estado * @return El estado del indicador */ public IndicatorI.IndicatorState getIndicatorState(ReportItemI indicator); diff --git a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java index 5b3c21b2..9255bd1c 100644 --- a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java @@ -67,8 +67,9 @@ private HashMap getMetric(String metricName, InputStreamReader i for (int i = 0; i < metrics.size(); i++) { log.info("nombre: " + metrics.get(i).asJsonObject().getString("name")); if (metrics.get(i).asJsonObject().getString("name").equals(metricName)) { - log.info("Localizada la m�trica"); + log.info("Localizada la métrica"); metricDefinition = new HashMap(); + metricDefinition.put("name", metricName); metricDefinition.put("type", metrics.get(i).asJsonObject().getString("type")); metricDefinition.put("description", metrics.get(i).asJsonObject().getString("description")); metricDefinition.put("unit", metrics.get(i).asJsonObject().getString("unit")); @@ -170,7 +171,7 @@ public List listAllMetrics() throws FileNotFoundException { log.info("Muestro la configuración leída " + confObject); metrics = confObject.getJsonArray("metrics"); - log.info("El número de m´rtricas es " + metrics.size()); + log.info("El número de métricas es " + metrics.size()); for (int i = 0; i < metrics.size(); i++) { log.info("Añado nombre: " + metrics.get(i).asJsonObject().getString("name")); allmetrics.add(metrics.get(i).asJsonObject().getString("name")); diff --git a/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java b/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java index ad4d026d..cb6cb827 100644 --- a/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java +++ b/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java @@ -23,16 +23,20 @@ public interface MetricConfigurationI { * @param metricType tipo de la métrica * @return metricDefinition Si la métrica está definida y el tipo es correcto se * devuelve un mapa con las unidades y la descripción - * @throws FileNotFoundException Si no se localiza el fichero de configuración + * @throws FileNotFoundException Si hay algún problema con el fichero de configuración */ public HashMap definedMetric(String metricName, String metricType) throws FileNotFoundException; /** * @param metricName nombre de la métrica que se consulta * @return la información de la métrica en un mapa - * @throws FileNotFoundException + * @throws FileNotFoundException Si hay algún problema con el fichero de configuración */ public HashMap getMetricInfo(String metricName) throws FileNotFoundException; + /** + * @return listado con los nombres de todas las métricas disponibles + * @throws FileNotFoundException Si hay algún problema con el fichero de configuración + */ List listAllMetrics() throws FileNotFoundException; } diff --git a/src/main/resources/a4i.conf b/src/main/resources/a4i.conf index 18a13dcd..022639ea 100644 --- a/src/main/resources/a4i.conf +++ b/src/main/resources/a4i.conf @@ -1,4 +1,4 @@ -#Fichero de configuración por defecto de la api +#Fichero de configuraci�n por defecto de la api #Tipo de persistencia que se quiere usar persistence.type=EXCEL #Tipo de remoto que se quiere usar @@ -6,4 +6,4 @@ remote.type=GITHUB #Caracteristicas por defecto de la fuente, cuando los informes son visuales Font.default.color=Black Font.default.height=12 -Font.default.type=Times \ No newline at end of file +Font.default.type=Arial \ No newline at end of file diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index 58f5d6d2..1fa0d608 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -38,7 +38,7 @@ { "name": "lastPush", "type": "java.util.Date", - "description": "Último push realizado en el repositorio", + "description": "Último push realizado en el repositorio", "unit": "date" } ], diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java index 75934d94..f575ef52 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java @@ -4,31 +4,51 @@ package us.muit.fs.a4i.test.config; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.awt.Font; +import java.awt.GraphicsEnvironment; +import java.io.File; import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; import java.util.logging.Logger; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import us.muit.fs.a4i.config.Context; /** - * @author Isabel Rom�n + * @author Isabel Román * */ class ContextTest { private static Logger log = Logger.getLogger(CheckerTest.class.getName()); - + /** + * Ruta al fichero de configuración de indicadores y métricas establecidos por la aplicación + */ + static String appConfPath; + /** + * Ruta al fichero de configuración de propiedades de funcionamiento establecidos por la aplicación + */ + static String appPath; /** * @throws java.lang.Exception */ @BeforeAll static void setUpBeforeClass() throws Exception { + appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + + "appConfTest.json"; + appPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + + "appTest.conf"; } /** @@ -57,16 +77,66 @@ void tearDown() throws Exception { */ @Test void testGetContext() { - fail("Not yet implemented"); + try { + assertNotNull(Context.getContext(),"Devuelve null"); + assertTrue(Context.getContext() instanceof us.muit.fs.a4i.config.Context,"No es del tipo apropiado"); + assertSame(Context.getContext(),Context.getContext(),"No se devuelve el mismo contexto siempre"); + + } catch (IOException e) { + fail("No debería lanzar esta excepción"); + e.printStackTrace(); + } + } + /** + * Test method for + * {@link us.muit.fs.a4i.config.Context#setAppRI(java.lang.String)}. + */ + @Test + @Tag("Integracion") + void testSetAppRI() { + /** + * Este test excede los límites, ya que no sólo verifica que se establece bien la ruta del fichero de especificación de métricas e indicadores sino que se leen bien los valores del mismo + * Sería un test de integración porque se requiere que estén ya desarrollados otras clases, a parte de Context + */ + try { + Context.setAppRI(appConfPath); + HashMap metricInfo=Context.getContext().getChecker().getMetricConfiguration().getMetricInfo("downloads"); + assertNotNull(metricInfo,"No se han leído los atributos de la métrica"); + assertEquals("downloads",metricInfo.get("name"),"El nombre no es el correcto"); + assertEquals("java.lang.Integer",metricInfo.get("type"),"El tipo no es el correcto"); + assertEquals("Descargas realizadas",metricInfo.get("description"),"La descripción no es el correcta"); + assertEquals("downloads",metricInfo.get("unit"),"Las uniddes no son correctas"); + + } catch (IOException e) { + fail("No se encuentra el fichero de especificación de métricas e indicadores"); + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + /** * Test method for * {@link us.muit.fs.a4i.config.Context#setAppConf(java.lang.String)}. */ @Test + @Tag("Integracion") void testSetAppConf() { - fail("Not yet implemented"); + /** + * Este test excede los límites, ya que no sólo verifica que se establece bien la ruta del fichero de configuración sino que se leen bien los valores del mismo + * Sería un test de integración porque se requiere que estén ya desarrollados otras clases, a parte de Context + */ + try { + + Context.setAppConf(appPath); + + + } catch (IOException e) { + fail("No se encuentra el fichero de especificación de métricas e indicadores"); + // TODO Auto-generated catch block + e.printStackTrace(); + } } /** @@ -74,7 +144,14 @@ void testSetAppConf() { */ @Test void testGetChecker() { - fail("Not yet implemented"); + try { + assertNotNull(Context.getContext().getChecker(),"No devuelve el checker"); + assertTrue(Context.getContext().getChecker() instanceof us.muit.fs.a4i.config.Checker,"No es del tipo apropiado"); + + } catch (IOException e) { + fail("No debería devolver esta excepción"); + e.printStackTrace(); + } } /** @@ -83,8 +160,8 @@ void testGetChecker() { @Test void testGetPersistenceType() { try { - assertEquals("excel", Context.getContext().getPersistenceType(), - "Deber�a estar definido el tipo excel por defecto en a4i.conf"); + assertEquals("EXCEL", Context.getContext().getPersistenceType(), + "En el fichero de configuración por defecto, a4i.conf, está definido el tipo excel"); } catch (IOException e) { fail("El fichero no se localiza"); e.printStackTrace(); @@ -97,8 +174,8 @@ void testGetPersistenceType() { @Test void testGetRemoteType() { try { - assertEquals("github", Context.getContext().getRemoteType(), - "Deber�a estar definido el tipo github por defecto en a4i.conf"); + assertEquals("GITHUB", Context.getContext().getRemoteType(), + "En el fichero de configuración por defecto, a4i.conf, está el tipo github"); } catch (IOException e) { fail("El fichero no se localiza"); e.printStackTrace(); @@ -110,7 +187,20 @@ void testGetRemoteType() { */ @Test void testGetDefaultFont() { - fail("Not yet implemented"); + try { + Font font=null; + String color; + //Uso esto para ver los tipos de fuentes de los que dispongo + String[] fontNames=GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); + log.info("listado de fuentes "+Arrays.toString(fontNames)); + font=Context.getContext().getDefaultFont(); + assertNotNull(font,"No se ha inicializado bien la fuente"); + assertEquals("Arial",font.getName(),"No es el tipo de fuente especificado en el fichero de propiedades"); + + } catch (IOException e) { + fail("No debería devolver esta excepción"); + e.printStackTrace(); + } } /** diff --git a/src/test/resources/appConfTest.json b/src/test/resources/appConfTest.json index a09346cf..fdfd7b59 100644 --- a/src/test/resources/appConfTest.json +++ b/src/test/resources/appConfTest.json @@ -9,7 +9,7 @@ { "name": "comments", "type": "java.lang.Integer", - "description": "N�meoro de comentarios", + "description": "Número de comentarios", "unit": "comments" } ], @@ -23,7 +23,7 @@ { "name": "commentsInterest", "type": "java.lang.Double", - "description": "Ratio de comentarios con m�s de 1 respuesta frente al n�mero total de comentarios", + "description": "Ratio de comentarios con más de 1 respuesta frente al número total de comentarios", "unit": "ratio" } ] diff --git a/src/test/resources/appTest.conf b/src/test/resources/appTest.conf new file mode 100644 index 00000000..27ca39d8 --- /dev/null +++ b/src/test/resources/appTest.conf @@ -0,0 +1,6 @@ +#Fichero de configuración establecido por la aplicación + +#Caracteristicas por defecto de la fuente, cuando los informes son visuales +Font.default.color=Red +Font.default.height=16 +Font.default.type=Times \ No newline at end of file From bb720f30805a3405af8a94e5b4eb32566f1b256c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Thu, 16 Feb 2023 13:44:25 +0100 Subject: [PATCH 037/100] Preparando V.0.2 Figuras UML actualizadas Mejora formato --- .../java/us/muit/fs/a4i/config/Context.java | 62 +++++++++----- .../a4i/config/IndicatorConfigurationI.java | 11 ++- .../fs/a4i/config/MetricConfigurationI.java | 9 +- .../fs/a4i/config/doc-files/configPackage.GIF | Bin 137752 -> 124645 bytes .../a4i/config/doc-files/newConfigPackage.gif | Bin 146064 -> 0 bytes .../us/muit/fs/a4i/control/ReportManager.java | 1 + .../fs/a4i/control/RepositoryCalculator.java | 3 +- .../us/muit/fs/a4i/model/entities/Report.java | 20 ++--- .../fs/a4i/model/entities/ReportItem.java | 4 +- .../entities/doc-files/entitiesPackage.GIF | Bin 190196 -> 101059 bytes .../muit/fs/a4i/test/config/ContextTest.java | 77 ++++++++++-------- 11 files changed, 106 insertions(+), 81 deletions(-) delete mode 100644 src/main/java/us/muit/fs/a4i/config/doc-files/newConfigPackage.gif diff --git a/src/main/java/us/muit/fs/a4i/config/Context.java b/src/main/java/us/muit/fs/a4i/config/Context.java index cccc33cb..008393a8 100644 --- a/src/main/java/us/muit/fs/a4i/config/Context.java +++ b/src/main/java/us/muit/fs/a4i/config/Context.java @@ -47,29 +47,31 @@ public class Context { private static Context contextInstance = null; /** - * Propiedades de la API - * Se leerán del fichero de configuración por defecto y del especificado por la aplicación, si lo hubiera + * Propiedades de la API Se leerán del fichero de configuración por defecto y + * del especificado por la aplicación, si lo hubiera */ private Properties properties = null; - /** + /** * Fichero de propiedades de configuración de la API, embebido en el jar */ private static String confFile = "a4i.conf"; - + /** - * Fichero de especificación de métricas e indicadores por defecto, embebido en el jar + * Fichero de especificación de métricas e indicadores por defecto, embebido en + * el jar * */ private static String defaultFile = "a4iDefault.json"; - + /** * Fichero de propiedades de la API establecido por la aplicación cliente */ private static String appConFile = null; - + /** - * Fichero de especificación de métricas e indicadores establecido por la aplicación cliente - */ + * Fichero de especificación de métricas e indicadores establecido por la + * aplicación cliente + */ private static String appFile = null; /** * Referencia al verificador de métricas e indicadores @@ -77,7 +79,11 @@ public class Context { private Checker checker = null; /** - *

Constructor privado, sigue el patrón singleton. El único objeto posible se crea al invocar el método getContext

+ *

+ * Constructor privado, sigue el patrón singleton. El único objeto posible se + * crea al invocar el método getContext + *

+ * * @throws IOException */ private Context() throws IOException { @@ -88,15 +94,24 @@ private Context() throws IOException { } /** - *

Establece la ruta del fichero de métricas e indicadores indicado por el cliente/aplicación

- * @param filename ruta al fichero de configuración de métricas e indicadores de la aplicación cliente + *

+ * Establece la ruta del fichero de métricas e indicadores indicado por el + * cliente/aplicación + *

+ * + * @param filename ruta al fichero de configuración de métricas e indicadores de + * la aplicación cliente */ public static void setAppRI(String filename) { appFile = filename; } /** - *

Consulta la ruta del fichero de configuración de métricas e indicadores del cliente/aplicación

+ *

+ * Consulta la ruta del fichero de configuración de métricas e indicadores del + * cliente/aplicación + *

+ * * @return */ public static String getAppRI() { @@ -104,7 +119,8 @@ public static String getAppRI() { } /** - * @return la ruta al fichero de configuración de indicadores y métricas por defecto + * @return la ruta al fichero de configuración de indicadores y métricas por + * defecto */ public static String getDefaultRI() { return defaultFile; @@ -151,7 +167,7 @@ public static void setAppConf(String appConPath) throws IOException { // instalación y de ahí coger el fichero de configuración // También podría localizarse en el home de usuario getContext().properties.load(new FileInputStream(appConPath)); - log.info("Las nuevas propiedades son "+getContext().properties); + log.info("Las nuevas propiedades son " + getContext().properties); } public Checker getChecker() { @@ -195,17 +211,19 @@ public String getRemoteType() throws IOException { * @return La fuente por defecto para indicadores y métricas */ public Font getDefaultFont() { - //OJO el color no forma parte de la clase font, por loq ue ese atributo debe estar fuera - //Podría incluir un parámetro font para devolverlo a la salida y que lo que devuelva sea un String con el color + // OJO el color no forma parte de la clase font, por loq ue ese atributo debe + // estar fuera + // Podría incluir un parámetro font para devolverlo a la salida y que lo que + // devuelva sea un String con el color log.info("Busca la información de configuración de la fuente, por defecto"); - + // TO DO String color = properties.getProperty("Font.default.color"); String height = properties.getProperty("Font.default.height"); String type = properties.getProperty("Font.default.type"); - log.info("Los datos son, color: "+color+" height: "+height+" type: "+type); + log.info("Los datos son, color: " + color + " height: " + height + " type: " + type); log.info("Intento crear la fuente"); - + return new Font(type, Font.ITALIC, Integer.valueOf(height)); } @@ -214,8 +232,8 @@ public Font getDefaultFont() { * No Implementado *

*

- * Deberá leer las propiedades adecuadas, como color, tamaño, tipo... y construir - * un objeto Font + * Deberá leer las propiedades adecuadas, como color, tamaño, tipo... y + * construir un objeto Font *

*

* Si no se ha definido una fuente para las métricas se debe devolver la fuente diff --git a/src/main/java/us/muit/fs/a4i/config/IndicatorConfigurationI.java b/src/main/java/us/muit/fs/a4i/config/IndicatorConfigurationI.java index 5876b020..f8dbf25a 100644 --- a/src/main/java/us/muit/fs/a4i/config/IndicatorConfigurationI.java +++ b/src/main/java/us/muit/fs/a4i/config/IndicatorConfigurationI.java @@ -16,17 +16,22 @@ */ public interface IndicatorConfigurationI { /** - *

Verifica si el indicador existe y el tipo es correcto

+ *

+ * Verifica si el indicador existe y el tipo es correcto + *

+ * * @param name nombre del indicador * @param type tipo del valor del indicador * @return mapa con los detalles del indicador - * @throws FileNotFoundException si hay algún problema con el fichero de configuración + * @throws FileNotFoundException si hay algún problema con el fichero de + * configuración */ public HashMap definedIndicator(String name, String type) throws FileNotFoundException; /** * @return lista con los nombres de todos los indicadores disponibles - * @throws FileNotFoundException en caso de problemas con el fichero de configuración + * @throws FileNotFoundException en caso de problemas con el fichero de + * configuración */ public List listAllIndicators() throws FileNotFoundException; diff --git a/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java b/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java index cb6cb827..b0a7dfc9 100644 --- a/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java +++ b/src/main/java/us/muit/fs/a4i/config/MetricConfigurationI.java @@ -23,20 +23,23 @@ public interface MetricConfigurationI { * @param metricType tipo de la métrica * @return metricDefinition Si la métrica está definida y el tipo es correcto se * devuelve un mapa con las unidades y la descripción - * @throws FileNotFoundException Si hay algún problema con el fichero de configuración + * @throws FileNotFoundException Si hay algún problema con el fichero de + * configuración */ public HashMap definedMetric(String metricName, String metricType) throws FileNotFoundException; /** * @param metricName nombre de la métrica que se consulta * @return la información de la métrica en un mapa - * @throws FileNotFoundException Si hay algún problema con el fichero de configuración + * @throws FileNotFoundException Si hay algún problema con el fichero de + * configuración */ public HashMap getMetricInfo(String metricName) throws FileNotFoundException; /** * @return listado con los nombres de todas las métricas disponibles - * @throws FileNotFoundException Si hay algún problema con el fichero de configuración + * @throws FileNotFoundException Si hay algún problema con el fichero de + * configuración */ List listAllMetrics() throws FileNotFoundException; } diff --git a/src/main/java/us/muit/fs/a4i/config/doc-files/configPackage.GIF b/src/main/java/us/muit/fs/a4i/config/doc-files/configPackage.GIF index 1f14908fccfe96039732e442a52cac0cade28f01..bcba3513305e8d1e705f345b61adeb10a9c3a912 100644 GIT binary patch literal 124645 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�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 IP(T0xJ5}*Fg8%>k literal 137752 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%0D_%wT<=ddKry!Io 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

>Uo4)*do;VHv9!$egucmlp$Z5nzQp; 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 diff --git a/src/main/java/us/muit/fs/a4i/config/doc-files/newConfigPackage.gif b/src/main/java/us/muit/fs/a4i/config/doc-files/newConfigPackage.gif deleted file mode 100644 index f55ca844e640372850f45e6b2270a39961021fc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 146064 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

*/ private String entityId; - /** - *

- * Los objetos que implementen esta interfaz recurren a calcuadoras con los - * algoritmos para el cálculo de indicadores - *

- *

- * Los algoritmos de cálculo de indicadores serán específicos para un tipo de - * informe - *

- */ - private IndicatorsCalculator calc; private ReportI.ReportType type = null; /** @@ -93,6 +82,7 @@ private void createMaps() { * @param name Nombre de la métrica buscada * @return la métrica localizada */ + @Override public ReportItemI getMetricByName(String name) { log.info("solicitada m�trica de nombre " + name); ReportItemI metric = null; @@ -148,7 +138,7 @@ public ReportItemI getIndicatorByName(String name) { public void addIndicator(ReportItemI ind) { indicators.put(ind.getName(), ind); - log.info("A�adido indicador " + ind); + log.info("Añadido indicador " + ind); } @@ -166,7 +156,7 @@ public String getEntityId() { @Override public String toString() { String repoinfo; - repoinfo = "Informaci�n del Informe:\n - Métricas: "; + repoinfo = "Información del Informe:\n - Métricas: "; for (String clave : metrics.keySet()) { repoinfo += "\n Clave: " + clave + metrics.get(clave); } @@ -179,13 +169,13 @@ public String toString() { @Override public Collection getAllMetrics() { - // TODO Auto-generated method stub + return metrics.values(); } @Override public Collection getAllIndicators() { - // TODO Auto-generated method stub + return indicators.values(); } diff --git a/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java b/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java index 9343a35b..39635410 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/ReportItem.java @@ -23,11 +23,11 @@ public class ReportItem implements ReportItemI { private IndicatorI indicator = null; private static Logger log = Logger.getLogger(ReportItem.class.getName()); /** - * Nombre del indicador/m�trica + * Nombre del indicador/métrica */ private String name; /** - * Valor del indicador/m�trica + * Valor del indicador/métrica */ private T value; /** diff --git a/src/main/java/us/muit/fs/a4i/model/entities/doc-files/entitiesPackage.GIF b/src/main/java/us/muit/fs/a4i/model/entities/doc-files/entitiesPackage.GIF index 4ea1fd86e4f949a9d3736b3a8bf1769688e84bd1..214f3c8a2d40409d91c6e342cc5b52932f390a34 100644 GIT binary patch delta 100912 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

s39}s)G(qs+l14T>EPt>PYyjY#iJ16%uty$ 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 literal 190196 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%0Ac7=X6dlaFfR*44oevn-}im)IC!;~^r0;f{yC7O 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 Nm${jrxmyMV06SgyTRH#$ diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java index f575ef52..0f7e3945 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java @@ -33,13 +33,16 @@ class ContextTest { private static Logger log = Logger.getLogger(CheckerTest.class.getName()); /** - * Ruta al fichero de configuración de indicadores y métricas establecidos por la aplicación + * Ruta al fichero de configuración de indicadores y métricas establecidos por + * la aplicación */ static String appConfPath; /** - * Ruta al fichero de configuración de propiedades de funcionamiento establecidos por la aplicación + * Ruta al fichero de configuración de propiedades de funcionamiento + * establecidos por la aplicación */ static String appPath; + /** * @throws java.lang.Exception */ @@ -47,8 +50,7 @@ class ContextTest { static void setUpBeforeClass() throws Exception { appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appConfTest.json"; - appPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator - + "appTest.conf"; + appPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appTest.conf"; } /** @@ -78,15 +80,15 @@ void tearDown() throws Exception { @Test void testGetContext() { try { - assertNotNull(Context.getContext(),"Devuelve null"); - assertTrue(Context.getContext() instanceof us.muit.fs.a4i.config.Context,"No es del tipo apropiado"); - assertSame(Context.getContext(),Context.getContext(),"No se devuelve el mismo contexto siempre"); - + assertNotNull(Context.getContext(), "Devuelve null"); + assertTrue(Context.getContext() instanceof us.muit.fs.a4i.config.Context, "No es del tipo apropiado"); + assertSame(Context.getContext(), Context.getContext(), "No se devuelve el mismo contexto siempre"); + } catch (IOException e) { fail("No debería lanzar esta excepción"); e.printStackTrace(); } - + } /** @@ -97,25 +99,28 @@ void testGetContext() { @Tag("Integracion") void testSetAppRI() { /** - * Este test excede los límites, ya que no sólo verifica que se establece bien la ruta del fichero de especificación de métricas e indicadores sino que se leen bien los valores del mismo - * Sería un test de integración porque se requiere que estén ya desarrollados otras clases, a parte de Context + * Este test excede los límites, ya que no sólo verifica que se establece bien + * la ruta del fichero de especificación de métricas e indicadores sino que se + * leen bien los valores del mismo Sería un test de integración porque se + * requiere que estén ya desarrollados otras clases, a parte de Context */ try { Context.setAppRI(appConfPath); - HashMap metricInfo=Context.getContext().getChecker().getMetricConfiguration().getMetricInfo("downloads"); - assertNotNull(metricInfo,"No se han leído los atributos de la métrica"); - assertEquals("downloads",metricInfo.get("name"),"El nombre no es el correcto"); - assertEquals("java.lang.Integer",metricInfo.get("type"),"El tipo no es el correcto"); - assertEquals("Descargas realizadas",metricInfo.get("description"),"La descripción no es el correcta"); - assertEquals("downloads",metricInfo.get("unit"),"Las uniddes no son correctas"); - + HashMap metricInfo = Context.getContext().getChecker().getMetricConfiguration() + .getMetricInfo("downloads"); + assertNotNull(metricInfo, "No se han leído los atributos de la métrica"); + assertEquals("downloads", metricInfo.get("name"), "El nombre no es el correcto"); + assertEquals("java.lang.Integer", metricInfo.get("type"), "El tipo no es el correcto"); + assertEquals("Descargas realizadas", metricInfo.get("description"), "La descripción no es el correcta"); + assertEquals("downloads", metricInfo.get("unit"), "Las uniddes no son correctas"); + } catch (IOException e) { fail("No se encuentra el fichero de especificación de métricas e indicadores"); // TODO Auto-generated catch block e.printStackTrace(); } } - + /** * Test method for * {@link us.muit.fs.a4i.config.Context#setAppConf(java.lang.String)}. @@ -124,14 +129,15 @@ void testSetAppRI() { @Tag("Integracion") void testSetAppConf() { /** - * Este test excede los límites, ya que no sólo verifica que se establece bien la ruta del fichero de configuración sino que se leen bien los valores del mismo - * Sería un test de integración porque se requiere que estén ya desarrollados otras clases, a parte de Context + * Este test excede los límites, ya que no sólo verifica que se establece bien + * la ruta del fichero de configuración sino que se leen bien los valores del + * mismo Sería un test de integración porque se requiere que estén ya + * desarrollados otras clases, a parte de Context */ try { - + Context.setAppConf(appPath); - - + } catch (IOException e) { fail("No se encuentra el fichero de especificación de métricas e indicadores"); // TODO Auto-generated catch block @@ -145,9 +151,10 @@ void testSetAppConf() { @Test void testGetChecker() { try { - assertNotNull(Context.getContext().getChecker(),"No devuelve el checker"); - assertTrue(Context.getContext().getChecker() instanceof us.muit.fs.a4i.config.Checker,"No es del tipo apropiado"); - + assertNotNull(Context.getContext().getChecker(), "No devuelve el checker"); + assertTrue(Context.getContext().getChecker() instanceof us.muit.fs.a4i.config.Checker, + "No es del tipo apropiado"); + } catch (IOException e) { fail("No debería devolver esta excepción"); e.printStackTrace(); @@ -188,15 +195,15 @@ void testGetRemoteType() { @Test void testGetDefaultFont() { try { - Font font=null; + Font font = null; String color; - //Uso esto para ver los tipos de fuentes de los que dispongo - String[] fontNames=GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); - log.info("listado de fuentes "+Arrays.toString(fontNames)); - font=Context.getContext().getDefaultFont(); - assertNotNull(font,"No se ha inicializado bien la fuente"); - assertEquals("Arial",font.getName(),"No es el tipo de fuente especificado en el fichero de propiedades"); - + // Uso esto para ver los tipos de fuentes de los que dispongo + String[] fontNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); + log.info("listado de fuentes " + Arrays.toString(fontNames)); + font = Context.getContext().getDefaultFont(); + assertNotNull(font, "No se ha inicializado bien la fuente"); + assertEquals("Arial", font.getName(), "No es el tipo de fuente especificado en el fichero de propiedades"); + } catch (IOException e) { fail("No debería devolver esta excepción"); e.printStackTrace(); From 3b6198d407347c28694909d042fbe654b988e48e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Thu, 16 Feb 2023 14:01:28 +0100 Subject: [PATCH 038/100] Mejorando V.0.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mejora documentación --- .../java/us/muit/fs/a4i/config/Checker.java | 13 ++++++++++--- .../java/us/muit/fs/a4i/config/Context.java | 5 ++++- .../muit/fs/a4i/model/entities/Indicator.java | 4 ++++ .../us/muit/fs/a4i/model/entities/ReportI.java | 5 +++++ .../fs/a4i/persistence/ExcelReportManager.java | 3 +++ .../fs/a4i/test/control/SupervisorControl.java | 18 +++++++++--------- 6 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/config/Checker.java b/src/main/java/us/muit/fs/a4i/config/Checker.java index 4e837399..639405e0 100644 --- a/src/main/java/us/muit/fs/a4i/config/Checker.java +++ b/src/main/java/us/muit/fs/a4i/config/Checker.java @@ -3,10 +3,11 @@ import java.util.logging.Logger; /** - * Mantengo separada la lógica de la gestión de configuración de métricas e - * indicadores porque en el futuro van a ser bastante diferentes En la versión + *

Esta clase permite acceder a las interfaces para configurar y verificar métricas e indicadores + * Se mantiene separada la lógica de la gestión de configuración de métricas e + * indicadores porque en el futuro van a ser bastante diferentes. En la versión * actual son muy similares y por tanto el diseño no es bueno ya que no - * identifica bien la reutilización + * identifica bien la reutilización

* * @author Isabel Román * @@ -22,10 +23,16 @@ public class Checker { this.indiConf = new IndicatorConfiguration(); } + /** + * @return interfaz para la configuración de métricas + */ public MetricConfigurationI getMetricConfiguration() { return this.metricConf; } + /** + * @return interfaz para la configuración de indicadores + */ public IndicatorConfigurationI getIndicatorConfiguration() { return this.indiConf; } diff --git a/src/main/java/us/muit/fs/a4i/config/Context.java b/src/main/java/us/muit/fs/a4i/config/Context.java index 008393a8..6a817879 100644 --- a/src/main/java/us/muit/fs/a4i/config/Context.java +++ b/src/main/java/us/muit/fs/a4i/config/Context.java @@ -112,7 +112,7 @@ public static void setAppRI(String filename) { * cliente/aplicación *

* - * @return + * @return ruta del fichero de configuración de métricas e indicadores de la aplicación cliente */ public static String getAppRI() { return appFile; @@ -170,6 +170,9 @@ public static void setAppConf(String appConPath) throws IOException { log.info("Las nuevas propiedades son " + getContext().properties); } + /** + * @return devuelve el verificador (checker) + */ public Checker getChecker() { return checker; } diff --git a/src/main/java/us/muit/fs/a4i/model/entities/Indicator.java b/src/main/java/us/muit/fs/a4i/model/entities/Indicator.java index 1558a5b7..c8a62521 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/Indicator.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/Indicator.java @@ -22,6 +22,10 @@ public Indicator() { this.state = IndicatorState.UNDEFINED; } + /** + * @param state Estado del nuevo indicador + * @param metrics Colección de métricas en las que se basa el indicador + */ public Indicator(IndicatorState state, Collection metrics) { this.metrics = metrics; this.state = state; diff --git a/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java b/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java index 52048c8d..bc631763 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java @@ -2,6 +2,11 @@ import java.util.Collection; +/** + *

Interfaz para la gestión de informes

+ * @author Isabel Román + * + */ public interface ReportI { /** *

diff --git a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java index db491780..1a3bf9cf 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java @@ -78,6 +78,9 @@ public class ExcelReportManager implements PersistenceManager, FileManager { protected HSSFWorkbook wb = null; protected HSSFSheet sheet = null; + /** + * @param report añade el informe + */ public void setReport(ReportI report) { log.info("Establece el informe"); this.report = report; diff --git a/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java b/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java index 5a449fad..1f39aac4 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java +++ b/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java @@ -17,10 +17,10 @@ /** * @author Isabel Rom�n Mart�nez - * @version 0.0 Esta clase se crea para poder probar algunas de las capacidades - * que ofrece la api github Ser� descartada posteriormente No usa - * Junit, sino que crea un main, no tiene verificaciones autom�ticas, - * la automatizaci�n no es posible + * @version 0.2 Esta clase se crea para poder probar algunas de las capacidades + * que ofrece la api github Serádescartada posteriormente No usa + * Junit, sino que crea un main, no tiene verificaciones automáticas, + * la automatización no es posible * */ public class SupervisorControl { @@ -38,7 +38,7 @@ public static void main(String[] args) { PagedIterable myOwnRepos = myinfo.listRepositories(10, GHMyself.RepositoryListFilter.OWNER); int count = 1; for (GHRepository repo : myOwnRepos.toList()) { - System.out.println("Nombre de mi repositorio n�mero " + count + " " + repo.getFullName()); + System.out.println("Nombre de mi repositorio número " + count + " " + repo.getFullName()); List proyectos = repo.listProjects().toList(); int i = 1; for (GHProject project : proyectos) { @@ -59,7 +59,7 @@ public static void main(String[] args) { // misOrganizaciones.iterator(); int i = 1; for (GHOrganization organizacion : misOrganizaciones) { - System.out.println(i + " Organizaci�n " + organizacion.getId() + " : " + organizacion); + System.out.println(i + " Organización " + organizacion.getId() + " : " + organizacion); PagedIterable repos = organizacion.listRepositories(); System.out.println(repos); i++; @@ -71,10 +71,10 @@ public static void main(String[] args) { */ GHOrganization unaOrg = github.getOrganization("MIT-FS"); // PagedIterable repos=unaOrg.listRepositories(); - System.out.println("Recupero la organizaci�n " + unaOrg.getId()); + System.out.println("Recupero la organización " + unaOrg.getId()); GHRepository githubrepo = github.getRepository("hub4j/github-api"); - System.out.println("Este repositorio es de " + githubrepo.getOwnerName() + " Y su descripci�n es " + System.out.println("Este repositorio es de " + githubrepo.getOwnerName() + " Y su descripción es " + githubrepo.getDescription()); GHRepositoryStatistics estadisticas = githubrepo.getStatistics(); @@ -84,7 +84,7 @@ public static void main(String[] args) { PagedIterable estDes = estadisticas.getContributorStats(); log.info("Desarrolladores recogidos"); List listaDesarrolladores = estDes.toList(); - System.out.println("N�mero de desarrolladores " + listaDesarrolladores.size()); + System.out.println("Número de desarrolladores " + listaDesarrolladores.size()); i = 1; From 5de299748e47b600f87eabf773f21f7653e5cddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Thu, 16 Feb 2023 14:31:24 +0100 Subject: [PATCH 039/100] Arreglos para javadoc --- .../{configPackage.GIF => configPackage.gif} | Bin .../java/us/muit/fs/a4i/config/package-info.java | 2 +- .../{entitiesPackage.GIF => entitiesPackage.gif} | Bin 3 files changed, 1 insertion(+), 1 deletion(-) rename src/main/java/us/muit/fs/a4i/config/doc-files/{configPackage.GIF => configPackage.gif} (100%) rename src/main/java/us/muit/fs/a4i/model/entities/doc-files/{entitiesPackage.GIF => entitiesPackage.gif} (100%) diff --git a/src/main/java/us/muit/fs/a4i/config/doc-files/configPackage.GIF b/src/main/java/us/muit/fs/a4i/config/doc-files/configPackage.gif similarity index 100% rename from src/main/java/us/muit/fs/a4i/config/doc-files/configPackage.GIF rename to src/main/java/us/muit/fs/a4i/config/doc-files/configPackage.gif diff --git a/src/main/java/us/muit/fs/a4i/config/package-info.java b/src/main/java/us/muit/fs/a4i/config/package-info.java index d7933af1..d768c7f5 100644 --- a/src/main/java/us/muit/fs/a4i/config/package-info.java +++ b/src/main/java/us/muit/fs/a4i/config/package-info.java @@ -6,7 +6,7 @@ * json *

* Paquete para la configuración * * @author Isabel Román diff --git a/src/main/java/us/muit/fs/a4i/model/entities/doc-files/entitiesPackage.GIF b/src/main/java/us/muit/fs/a4i/model/entities/doc-files/entitiesPackage.gif similarity index 100% rename from src/main/java/us/muit/fs/a4i/model/entities/doc-files/entitiesPackage.GIF rename to src/main/java/us/muit/fs/a4i/model/entities/doc-files/entitiesPackage.gif From acf699b1789f5bd86ac1c3766c870f04e9649e28 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Thu, 16 Feb 2023 22:16:38 +0100 Subject: [PATCH 040/100] Mejoras V.0.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se mejoran los tests de Checker, Context, IndicatorConfiguration y ExcelReportManager Se hacen también cambios en ExcelReportManager, IndicatorConfiguration y MetricInfo --- .../fs/a4i/config/IndicatorConfiguration.java | 4 +- .../a4i/persistence/ExcelReportManager.java | 50 ++++++----- .../muit/fs/a4i/test/config/CheckerTest.java | 36 ++++---- .../muit/fs/a4i/test/config/ContextTest.java | 11 ++- .../config/IndicatorConfigurationTest.java | 84 ++++++++++++------- .../muit/fs/a4i/test/config/MetricInfo.java | 10 +-- .../persistence/ExcelReportManagerTest.java | 4 + 7 files changed, 119 insertions(+), 80 deletions(-) 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 a54d4b86..25d7f569 100644 --- a/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java @@ -46,7 +46,7 @@ public class IndicatorConfiguration implements IndicatorConfigurationI { */ public HashMap definedIndicator(String name, String type) throws FileNotFoundException { HashMap indicatorDefinition = null; - log.info("Checker solicitud de b�squeda indicador " + name); + log.info("Checker solicitud de búsqueda indicador " + name); String filePath = "/" + Context.getDefaultRI(); log.info("Buscando el archivo " + filePath); @@ -75,7 +75,7 @@ private HashMap isDefinedIndicator(String indicatorName, String log.info("Leo el objeto"); reader.close(); - log.info("Muestro la configuraci�n le�da " + confObject); + log.info("Muestro la configuración leída " + confObject); JsonArray indicators = confObject.getJsonArray("indicators"); log.info("El número de indicadores es " + indicators.size()); for (int i = 0; i < indicators.size(); i++) { diff --git a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java index 1a3bf9cf..0f0a76d4 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java @@ -59,7 +59,7 @@ public class ExcelReportManager implements PersistenceManager, FileManager { *

*/ protected ReportFormaterI formater; - ReportI report; + FileInputStream inputStream = null; /** @@ -78,14 +78,17 @@ public class ExcelReportManager implements PersistenceManager, FileManager { protected HSSFWorkbook wb = null; protected HSSFSheet sheet = null; - /** - * @param report añade el informe - */ - public void setReport(ReportI report) { - log.info("Establece el informe"); - this.report = report; + public ExcelReportManager(String filePath, String fileName) { + super(); + this.filePath = filePath; + this.fileName = fileName; } + public ExcelReportManager() { + super(); + } + + @Override public void setFormater(ReportFormaterI formater) { log.info("Establece el formateador"); @@ -109,16 +112,16 @@ public void setName(String name) { /** *

- * El libro contendrán todos los informes de un tipo concreto Primero hay que - * abrir el libro Busco la hoja correspondiente a esta entidad, si ya existe la - * elimino Creo la hoja + * El libro contendrá todos los informes de un tipo concreto. Primero hay que + * abrir el libro. Busco la hoja correspondiente a esta entidad, si ya existe la + * elimino. Creo la hoja *

* * @return Hoja de excel * @throws IOException error al abrir el fichero * @throws EncryptedDocumentException documento protegido */ - protected HSSFSheet getCleanSheet() throws EncryptedDocumentException, IOException { + protected HSSFSheet getCleanSheet(String entityId) throws EncryptedDocumentException, IOException { log.info("Solicita una hoja nueva del libro manejado"); if (wb == null) { inputStream = new FileInputStream(filePath + fileName + ".xls"); @@ -133,22 +136,22 @@ protected HSSFSheet getCleanSheet() throws EncryptedDocumentException, IOExcepti **/ /** *

- * Verifico si la hoja existe y si es as� la extraigo + * Verifico si la hoja existe y si es así la extraigo *

*

* Si no existe la creo. */ - sheet = wb.getSheet(report.getEntityId().replaceAll("/", ".")); + sheet = wb.getSheet(entityId.replaceAll("/", ".")); if (sheet != null) { - log.info("Recuperada hoja, que ya exist�a"); + log.info("Recuperada hoja, que ya existía"); /* * Si la hoja existe la elimino */ int index = wb.getSheetIndex(sheet); wb.removeSheetAt(index); } - sheet = wb.createSheet(report.getEntityId().replaceAll("/", ".")); + sheet = wb.createSheet(entityId.replaceAll("/", ".")); log.info("Creada hoja nueva"); } @@ -169,7 +172,7 @@ public void saveReport(ReportI report) throws ReportNotDefinedException { try { FileOutputStream out; if (sheet == null) { - sheet = getCleanSheet(); + sheet = getCleanSheet(report.getEntityId()); } /** @@ -179,19 +182,26 @@ public void saveReport(ReportI report) throws ReportNotDefinedException { */ int rowIndex = sheet.getLastRowNum(); rowIndex++; - sheet.createRow(rowIndex).createCell(0).setCellValue("Métricas tomadas el día "); + sheet.createRow(rowIndex).createCell(0).setCellValue("Métricas guardadas el día "); sheet.getRow(rowIndex).createCell(1) .setCellValue(Date.from(LocalDateTime.now().toInstant(ZoneOffset.UTC)).toString()); Collection collection = report.getAllMetrics(); for (ReportItemI metric : collection) { persistMetric(metric); } - + //Ahora irían los indicadores + rowIndex++; + sheet.createRow(rowIndex).createCell(0).setCellValue("Indicadores"); + collection = report.getAllIndicators(); + for (ReportItemI indicator : collection) { + persistIndicator(indicator); + } + out = new FileOutputStream(filePath + fileName + ".xls"); wb.write(out); out.close(); } catch (Exception e) { - // TODO Auto-generated catch block + e.printStackTrace(); } } @@ -223,7 +233,7 @@ private void persistMetric(ReportItemI metric) { private void persistIndicator(ReportItemI indicator) { log.info("Introduzco indicador en la hoja"); - + //Mantengo uno diferente porque en el futuro la información del indicador será distinta a la de la métrica int rowIndex = sheet.getLastRowNum(); rowIndex++; Row row = sheet.createRow(rowIndex); diff --git a/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java b/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java index 4fbfda6b..a034e653 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java @@ -25,9 +25,9 @@ import us.muit.fs.a4i.config.Context; /** - * Test de la clase Checker que verifica las m�tricas e indicadores + * Test de la clase Checker que permite verificar y configurar las métricas e indicadores * - * @author Isabel Rom�n + * @author Isabel Román * @see org.junit.jupiter.api.Tag * */ @@ -39,48 +39,50 @@ class CheckerTest { static String appConfPath; /** + *

Acciones a realizar antes de ejecutar los tests definidos en esta clase

* @throws java.lang.Exception * @see org.junit.jupiter.api.BeforeAll */ @BeforeAll static void setUpBeforeClass() throws Exception { - // Acciones a realizar antes de ejecutar los tests de esta clase appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appConfTest.json"; } /** + *

Acciones a realizar después de ejecutar todos los tests de esta clase

* @throws java.lang.Exception * @see org.junit.jupiter.api.AfterAll */ @AfterAll static void tearDownAfterClass() throws Exception { - // Acciones a realizar despu�s de ejecutar todos los tests de esta clase + } /** + *

Acciones a realizar antes de cada uno de los tests de esta clase

* @throws java.lang.Exception * @see org.junit.jupiter.api.BeforeEach */ @BeforeEach void setUp() throws Exception { - // Acciones a realizar antes de cada uno de los tests de esta clase // Creo el objeto bajo test, un Checker underTest = Context.getContext().getChecker(); } /** + *

Acciones a realizar después de cada uno de los tests de esta clase

* @throws java.lang.Exception * @see org.junit.jupiter.api.AfterEach */ @AfterEach void tearDown() throws Exception { - // Acciones a realizar despu�s de cada uno de los tests de esta clase + } /** - * Test para el m�todo que establece el fichero de configuraci�n de la - * aplicaci�n + *

Test para el método que establece el fichero de configuración de la + * aplicación

* {@link us.muit.fs.a4i.config.Checker#setAppMetrics(java.lang.String)}. */ @Test @@ -89,26 +91,24 @@ void testSetAppMetrics() { } /** - * Test para verificar el m�todo + *

Test para verificar el método * {@link us.muit.fs.a4i.config.Checker#definedMetric(java.lang.String, java.lang.String)}. - * Si la m�trica est� definida y el tipo de valor que se quiere establecer es el - * adecuado debe devolver un hashmap con los datos de la m�trica, usando como + * Si la métrica está definida y el tipo de valor que se quiere establecer es el + * adecuado debe devolver un hashmap con los datos de la métrica, usando como * clave las etiquetas: *

    *
  • description
  • *
  • unit
  • *
- * Las m�tricas pueden estar definidas en el fichero de configuraci�n de la api - * (a4iDefault.json) o en otro fichero configurado por la aplicaci�n cliente. - * Para los test este fichero es appConfTest.json y se guarda junto al c�digo de + * Las métricas pueden estar definidas en el fichero de configuración de la api + * (a4iDefault.json) o en otro fichero configurado por la aplicación cliente. + * Para los test este fichero es appConfTest.json y se guarda junto al código de * test, en la carpeta resources * * @see org.junit.jupiter.api.Tag * @see org.junit.jupiter.api.Test - * @see org.junit.jupiter.api.DisplayName - * - * Test para verificar el m�todo - * {@link us.muit.fs.a4i.config.Checker#definedMetric(java.lang.String, java.lang.String)}. + * @see org.junit.jupiter.api.DisplayName * + *

*/ @Test @Tag("unidad") diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java index 0f7e3945..797b8081 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java @@ -72,6 +72,7 @@ void setUp() throws Exception { */ @AfterEach void tearDown() throws Exception { + //Ejecutar tras cada test } /** @@ -190,6 +191,7 @@ void testGetRemoteType() { } /** + *

Este test permite verificar que se lee bien la fuente, además es independiente del orden de ejecución del resto de test. La complejidad de la verifiación está impuesta por estar probando un singleton

* Test method for {@link us.muit.fs.a4i.config.Context#getDefaultFont()}. */ @Test @@ -198,11 +200,14 @@ void testGetDefaultFont() { Font font = null; String color; // Uso esto para ver los tipos de fuentes de los que dispongo - String[] fontNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); - log.info("listado de fuentes " + Arrays.toString(fontNames)); + //String[] fontNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); + //log.info("listado de fuentes " + Arrays.toString(fontNames)); font = Context.getContext().getDefaultFont(); assertNotNull(font, "No se ha inicializado bien la fuente"); - assertEquals("Arial", font.getName(), "No es el tipo de fuente especificado en el fichero de propiedades"); + //Al ser Context un singleton una vez creada la instancia no se puede eliminar "desde fuera" + //De manera que el valor de la fuente depende del orden en el que se ejecuten los test, y para que el test sea independiente de eso la verificación comprueba los dos posibles valores + assertTrue("Arial".equals(font.getName()) || "Times".equals(font.getName()),"No es el tipo de fuente especificado en el fichero de propiedades"); + } catch (IOException e) { fail("No debería devolver esta excepción"); diff --git a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java index 26a3ee14..a358eb9e 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java @@ -29,108 +29,128 @@ class IndicatorConfigurationTest { static String appConfPath; String appIndicatorsPath = "/test/home"; + /** + *

Acciones a realizar antes de ejecutar los tests definidos en esta clase. En este caso se establece la ruta al fichero de configuración, en la carpeta resources dentro del paquete de test

+ * @throws java.lang.Exception + * @see org.junit.jupiter.api.BeforeAll + */ @BeforeAll static void setUpBeforeClass() throws Exception { - // Acciones a realizar antes de ejecutar los tests de esta clase appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appConfTest.json"; } + /** + *

Acciones a realizar después de ejecutar todos los tests de esta clase

* @throws java.lang.Exception * @see org.junit.jupiter.api.AfterAll */ @AfterAll static void tearDownAfterClass() throws Exception { - // Acciones a realizar despu�s de ejecutar todos los tests de esta clase + log.info("He ejectuado todos los test definidos en esta clase"); } /** + *

Acciones a realizar antes de cada uno de los tests de esta clase + * en este caso se crea el objeto bajo test, un Checker

* @throws java.lang.Exception * @see org.junit.jupiter.api.BeforeEach */ @BeforeEach void setUp() throws Exception { - // Acciones a realizar antes de cada uno de los tests de esta clase - // Creo el objeto bajo test, un Checker underTest = new IndicatorConfiguration(); } /** + * Acciones a realizar después de cada uno de los tests de esta clase * @throws java.lang.Exception * @see org.junit.jupiter.api.AfterEach */ @AfterEach void tearDown() throws Exception { - // Acciones a realizar despu�s de cada uno de los tests de esta clase + log.info("Acabo de ejecutar un test definido en esta clase"); } + @Test void testDefinedIndicator() { - // Creo valores Mock para verificar si comprueba bien el tipo - // Las m�tricas del test son de enteros, as� que creo un entero y un string - // (el primero no dar� problemas el segundo s�) + // Creo un par de variables, que me servirán de valores para verificar si comprueba bien el tipo + // Las métricas del test son de tipo entero, así que creo un entero y un string + // (el primero no dará problemas el segundo sí) Double valOKMock = Double.valueOf(0.3); String valKOMock = "KO"; HashMap returnedMap = null; - // Primero, sin fichero de configuraci�n de aplicaci�n + // Primero, sin fichero de configuración de aplicación try { // Consulta un indicador no definido, con valor de tipo entero - // debe devolver null, no est� definido + // debe devolver null, no está definido log.info("Busco el indicador llamado pullReqGlory"); returnedMap = underTest.definedIndicator("pullReqGlory", valOKMock.getClass().getName()); - assertNull(returnedMap, "Deber�a ser nulo, el indicador pullReqGlory no est� definido"); + assertNull(returnedMap, "Debería ser nulo, el indicador pullReqGlory no está definido"); - // Busco el indicador overdued con valor double, no deber�a dar problemas + // Busco el indicador overdued con valor double, no debería dar problemas log.info("Busco el indicador overdued"); returnedMap = underTest.definedIndicator("overdued", valOKMock.getClass().getName()); - assertNotNull(returnedMap, "Deber�a devolver un hashmap, el indicador overdued est� definido"); + assertNotNull(returnedMap, "Debería devolver un hashmap, el indicador overdued está definido"); assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); - // Busco una m�trica que existe pero con un tipo incorrecto en el valor + // Busco una métrica que existe pero con un tipo incorrecto assertNull(underTest.definedIndicator("overdued", valKOMock.getClass().getName()), - "Deber�a ser nulo, el indicador overdued est� definido para Double"); + "Debería ser nulo, el indicador overdued está definido para Double"); } catch (FileNotFoundException e) { - fail("El fichero est� en la carpeta resources"); + fail("El fichero está en la carpeta resources"); e.printStackTrace(); } - // Ahora establezco el fichero de configuraci�n de la aplicaci�n, con un + // Ahora establezco el fichero de configuración de la aplicación, con un // nombre de fichero que no existe Context.setAppRI("pepe"); try { - // Busco un indicador que se que no est� en la configuraci�n de la api + // Busco un indicador que se que no está en la configuración de la api returnedMap = underTest.definedIndicator("pullReqGlory", valOKMock.getClass().getName()); - fail("Deber�a lanzar una excepci�n porque intenta buscar en un fichero que no existe"); + fail("Debería lanzar una excepción porque intenta buscar en un fichero que no existe"); } catch (FileNotFoundException e) { - log.info("Lanza la excepci�n adecuada, FileNotFoud"); + log.info("Lanza la excepción adecuada, FileNotFoud"); } catch (Exception e) { - fail("Lanza la excepci�n equivocada " + e); + fail("Lanza la excepción equivocada " + e); } - // Ahora establezco un fichero de configuraci�n de la aplicaci�n que s� + // Ahora establezco un fichero de configuración de la aplicación que sí // existe Context.setAppRI(appConfPath); try { - // Busco una m�trica que se que no est� en la configuraci�n de la api pero - // s� en la de la aplicaci�n + // Busco una métrica que se que no está en la configuración de la api pero + // sí en la de la aplicación log.info("Busco el indicador llamado pullReqGlory"); returnedMap = underTest.definedIndicator("pullReqGlory", valOKMock.getClass().getName()); - assertNotNull(returnedMap, "Deber�a devolver un hashmap, el indicador est� definido"); + assertNotNull(returnedMap, "Debería devolver un hashmap, el indicador está definido"); assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); } catch (FileNotFoundException e) { - fail("No deber�a devolver esta excepci�n"); + fail("No debería devolver esta excepción"); } catch (Exception e) { - fail("Lanza una excepci�n no reconocida " + e); + fail("Lanza una excepción no reconocida " + e); } } + /** + *

En este test verifico que si busco el nombre de una métrica el método que verifica el indicador no lo confunde

+ */ + @Test + void testDefinedIndicatorAsMetric() { + Integer valOKMock = Integer.valueOf(3); + + HashMap returnedMap = null; - @Test - void testSetAppIndicators() { - String AppIndicators = appIndicatorsPath; - assertEquals(AppIndicators, appIndicatorsPath, "Deberian ser iguales"); - } + try { + // Consulta el nombre de un indicador que en realidad es una métrica + log.info("Busco el indicador llamado pullReqGlory"); + returnedMap = underTest.definedIndicator("subscribers", valOKMock.getClass().getName()); + assertNull(returnedMap, "Debería ser nulo, subscribers es una métrica y no un indicador no está definido"); + }catch (Exception e) { + fail("Lanza la excepción " + e); + } + } @Test void testListAllIndicators() { diff --git a/src/test/java/us/muit/fs/a4i/test/config/MetricInfo.java b/src/test/java/us/muit/fs/a4i/test/config/MetricInfo.java index e821966c..edf12c8f 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/MetricInfo.java +++ b/src/test/java/us/muit/fs/a4i/test/config/MetricInfo.java @@ -16,7 +16,7 @@ import us.muit.fs.a4i.config.Context; /** - * @author isa + * @author Isabel Román * */ class MetricInfo { @@ -30,15 +30,15 @@ void testGetChecker() { try { Checker checker = Context.getContext().getChecker(); HashMap metricInfo = checker.getMetricConfiguration().getMetricInfo("issues"); - assertEquals(metricInfo.get("name"), "issues", "No se ha le�do bien el nombre de la m�trica"); - assertEquals(metricInfo.get("type"), "java.lang.Integer", "No se ha le�do bien el tipo de la m�trica"); + assertEquals(metricInfo.get("name"), "issues", "No se ha leído bien el nombre de la métrica"); + assertEquals(metricInfo.get("type"), "java.lang.Integer", "No se ha leído bien el tipo de la métrica"); assertEquals(metricInfo.get("description"), "Tareas sin finalizar en el repositorio", - "No se ha le�do bien la descripci�n de la m�trica"); + "No se ha leído bien la descripción de la métrica"); assertEquals(metricInfo.get("unit"), "issues", "No se ha le�do bien las unidades de la m�trica"); } catch (IOException e) { e.printStackTrace(); - fail("No deber�a devolver esta excepci�n"); + fail("No debería devolver esta excepción"); } } diff --git a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java index c9967143..781495bc 100644 --- a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java @@ -18,6 +18,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItem; import us.muit.fs.a4i.persistence.ExcelReportManager; import us.muit.fs.a4i.persistence.ReportFormaterI; @@ -38,6 +39,9 @@ class ExcelReportManagerTest { private static ReportItem metricIntMock = Mockito.mock(ReportItem.class); @Mock(serializable = true) private static ReportItem metricStrMock = Mockito.mock(ReportItem.class); + @Mock(serializable = true) + private static ReportI informe = Mockito.mock(ReportI.class); + @Test void ExcelCreation() { From 20379f44d2c0e72ec167e2b6f65ff198e3245c48 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Thu, 16 Feb 2023 23:20:47 +0100 Subject: [PATCH 041/100] Trabajando ExcelReportManager --- .../a4i/persistence/ExcelReportManager.java | 6 +-- .../a4i/test/control/ReportManagerTest.java | 2 +- .../persistence/ExcelReportManagerTest.java | 49 ++++++++++++++++++- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java index 0f0a76d4..2e22f392 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java @@ -122,7 +122,7 @@ public void setName(String name) { * @throws EncryptedDocumentException documento protegido */ protected HSSFSheet getCleanSheet(String entityId) throws EncryptedDocumentException, IOException { - log.info("Solicita una hoja nueva del libro manejado"); + log.info("Solicita una hoja nueva del libro manejado, para la entidad con id: "+entityId); if (wb == null) { inputStream = new FileInputStream(filePath + fileName + ".xls"); wb = (HSSFWorkbook) WorkbookFactory.create(inputStream); @@ -165,7 +165,7 @@ protected HSSFSheet getCleanSheet(String entityId) throws EncryptedDocumentExcep */ @Override public void saveReport(ReportI report) throws ReportNotDefinedException { - log.info("Guardando informe"); + log.info("Guardando informe con id: "+report.getEntityId()); if (report == null) { throw new ReportNotDefinedException(); } @@ -219,7 +219,7 @@ private void persistMetric(ReportItemI metric) { // https://www.e-iceblue.com/Tutorials/Java/Spire.XLS-for-Java/Program-Guide/Cells/Apply-Fonts-in-Excel-in-Java.html CellStyle style = wb.createCellStyle(); - style.setFont((Font) formater.getMetricFont()); + //style.setFont((Font) formater.getMetricFont()); row.createCell(cellIndex++).setCellValue(metric.getName()); row.createCell(cellIndex++).setCellValue(metric.getValue().toString()); diff --git a/src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java b/src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java index 81f3c677..bd3141e6 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java @@ -68,7 +68,7 @@ void testReportManager() { report = manager.newReport("MIT-FS/Audit4Improve-API", ReportI.ReportType.REPOSITORY); log.info("Creado el informe"); } catch (Exception e) { - fail("Se lanza alguna excepci�n al crear el gestor o el informe"); + fail("Se lanza alguna excepción al crear el gestor o el informe"); e.printStackTrace(); } assertNotNull(report, "No se ha construido el informe"); diff --git a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java index 781495bc..766904f7 100644 --- a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java @@ -8,8 +8,13 @@ */ package us.muit.fs.a4i.test.persistence; +import java.io.File; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; import java.util.logging.Logger; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -18,8 +23,10 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import us.muit.fs.a4i.exceptions.ReportNotDefinedException; import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; import us.muit.fs.a4i.persistence.ExcelReportManager; import us.muit.fs.a4i.persistence.ReportFormaterI; @@ -41,10 +48,50 @@ class ExcelReportManagerTest { private static ReportItem metricStrMock = Mockito.mock(ReportItem.class); @Mock(serializable = true) private static ReportI informe = Mockito.mock(ReportI.class); + private static String excelPath; + private static String excelName; + private static ExcelReportManager underTest; + /** + *

Acciones a realizar antes de ejecutar los tests definidos en esta clase

+ * @throws java.lang.Exception + * @see org.junit.jupiter.api.BeforeAll + */ + @BeforeAll + static void setUpBeforeClass() throws Exception { + + } - @Test void ExcelCreation() { + List lista=new ArrayList(); + Date fecha=new Date(); + Mockito.when(metricIntMock.getValue()).thenReturn(55); + Mockito.when(metricIntMock.getName()).thenReturn("downloads"); + Mockito.when(metricIntMock.getUnit()).thenReturn("downloads"); + Mockito.when(metricIntMock.getDescription()).thenReturn("Descargas realizadas"); + Mockito.when(metricIntMock.getDate()).thenReturn(fecha); + lista.add(metricIntMock); + + Mockito.when(metricStrMock.getValue()).thenReturn("2-2-22"); + Mockito.when(metricStrMock.getName()).thenReturn("lastPush"); + Mockito.when(metricStrMock.getUnit()).thenReturn("date"); + Mockito.when(metricStrMock.getDescription()).thenReturn("Último push realizado en el repositorio"); + Mockito.when(metricStrMock.getDate()).thenReturn(fecha); + lista.add(metricStrMock); + + Mockito.when(informe.getAllMetrics()).thenReturn(lista); + Mockito.when(informe.getEntityId()).thenReturn("entidadTest"); + + excelPath = new String("src" + File.separator + "test" + File.separator + "resources"+File.separator); + excelName= new String("excelTest"); + underTest=new ExcelReportManager(excelPath,excelName); + try { + log.info("El informe tiene el id "+informe.getEntityId()); + underTest.saveReport(informe); + } catch (ReportNotDefinedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } } } \ No newline at end of file From 3b4bb0cfada73d4df261331605dc8f44927837aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Thu, 4 May 2023 13:33:33 +0200 Subject: [PATCH 042/100] Eliminado ReportFormaterI del paquete control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Esta interfaz está definida en el paquete persistencia --- .../java/us/muit/fs/a4i/control/ReportFormaterI.java | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 src/main/java/us/muit/fs/a4i/control/ReportFormaterI.java diff --git a/src/main/java/us/muit/fs/a4i/control/ReportFormaterI.java b/src/main/java/us/muit/fs/a4i/control/ReportFormaterI.java deleted file mode 100644 index a858ae28..00000000 --- a/src/main/java/us/muit/fs/a4i/control/ReportFormaterI.java +++ /dev/null @@ -1,12 +0,0 @@ -/** - * - */ -package us.muit.fs.a4i.control; - -/** - * @author Isabel Román - * - */ -public interface ReportFormaterI { - -} From 285aa096b3146aaa0af74079372e9ec7f455999d Mon Sep 17 00:00:00 2001 From: celllarod Date: Tue, 9 May 2023 18:10:53 +0200 Subject: [PATCH 043/100] =?UTF-8?q?FEAT:=20se=20crea=20el=20patr=C3=B3n=20?= =?UTF-8?q?estrategia=20para=20los=20indicadores=20y=20se=20implementa=20p?= =?UTF-8?q?ara=20el=20indicdor=20"IssuesRatio"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fs/a4i/control/IndicatorStrategy.java | 35 +++++++++++ .../fs/a4i/control/IssuesRatioIndicator.java | 58 +++++++++++++++++++ .../NotAvailableMetricException.java | 29 ++++++++++ src/main/resources/a4iDefault.json | 18 ++++++ 4 files changed, 140 insertions(+) create mode 100644 src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java create mode 100644 src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicator.java create mode 100644 src/main/java/us/muit/fs/a4i/exceptions/NotAvailableMetricException.java diff --git a/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java b/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java new file mode 100644 index 00000000..a1f902fc --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java @@ -0,0 +1,35 @@ +package us.muit.fs.a4i.control; + +import java.util.List; + +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.model.entities.ReportItemI; + +/** + * + *

+ * Interfaz para calcular indicadores + *

+ * @author celllarod + * + */ +public interface IndicatorStrategy { + + + /** + * Calcula un indicador a partir de las métricas proporcionadas. + * @param + * @param metrics + * @throws NotAvailableMetricException + * @return indicador + */ + public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException; + + + /** + * Obtiene las métricas necesarias + * @return listado de métricas + */ + public List requiredMetrics(); + +} diff --git a/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicator.java b/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicator.java new file mode 100644 index 00000000..1f4af1c9 --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicator.java @@ -0,0 +1,58 @@ +package us.muit.fs.a4i.control; + +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; + +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; + +public class IssuesRatioIndicator implements IndicatorStrategy { + + private static Logger log = Logger.getLogger(Indicator.class.getName()); + + // Métricas necesarias para calcular el indicador + private static final List REQUIRED_METRICS = Arrays.asList("openIssues", "closedIssues"); + + @Override + public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { + // Se obtienen y se comprueba que se pasan las métricas necesarias para calcular + // el indicador. + var openIssues = metrics.stream().filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); + var closedIssues = metrics.stream().filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); + ReportItemI indicatorReport = null; + + if (openIssues.isPresent() && closedIssues.isPresent()) { + + // Se realiza el cálculo del indicador + Double issuesRatio = Double.parseDouble(openIssues.get().getValue().toString()) + / Double.parseDouble(closedIssues.get().getValue().toString()); + + try { + indicatorReport = new ReportItem.ReportItemBuilder("issuesRatio", issuesRatio) + .metrics(Arrays.asList(openIssues.get(), closedIssues.get())) + .indicator(IndicatorState.UNDEFINED).build(); + } catch (ReportItemException e) { + log.info("Error en ReportItemBuilder."); + e.printStackTrace(); + } + + } else { + log.info("No se han proporcionado las métricas necesarias"); + throw new NotAvailableMetricException("No se han proporcionado las métricas necesarias"); + } + + return (ReportItemI) indicatorReport; + } + + @Override + public List requiredMetrics() { + // Para calcular el indicador "IssuesRatio", serán necesarias las métricas + // "openIssues" y "closedIssues". + return REQUIRED_METRICS; + } +} \ No newline at end of file diff --git a/src/main/java/us/muit/fs/a4i/exceptions/NotAvailableMetricException.java b/src/main/java/us/muit/fs/a4i/exceptions/NotAvailableMetricException.java new file mode 100644 index 00000000..eeb57ddc --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/exceptions/NotAvailableMetricException.java @@ -0,0 +1,29 @@ +package us.muit.fs.a4i.exceptions; + +public class NotAvailableMetricException extends Exception{ + + /** + * Excepcion que ocurre cuando se quiere calcular un indicador pero no se proporcionan las métricas adecuadas. + */ + private static final long serialVersionUID = 1L; + /** + * Informacion sobre el error + */ + private String message; + + /** + *

+ * Constructor + *

+ * + * @param info Mensaje definiendo el error + */ + public NotAvailableMetricException(String info) { + message = info; + } + + @Override + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index 1fa0d608..e0bff341 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -40,6 +40,18 @@ "type": "java.util.Date", "description": "Último push realizado en el repositorio", "unit": "date" + }, + { + "name": "openIssues", + "type": "java.lang.Integer", + "description": "Numero de issues abiertas", + "unit": "issues" + }, + { + "name": "closedIssues", + "type": "java.lang.Integer", + "description": "Numero de issues cerradas", + "unit": "issues" } ], "indicators": [{ @@ -53,6 +65,12 @@ "type": "java.lang.Double", "description": "Ratio de issues vencidos frente a abiertos", "unit": "ratio" + }, + { + "name": "issuesRatio", + "type": "java.lang.Double", + "description": "Ratio de issues abiertos frente a cerrados", + "unit": "ratio" } ] } \ No newline at end of file From 7dc9a5448acc218ef7cf88a0d0f484195ffb5f3f Mon Sep 17 00:00:00 2001 From: frarosrap Date: Tue, 9 May 2023 20:44:11 +0200 Subject: [PATCH 044/100] =?UTF-8?q?Test=20context.getIndicatorFont(),=20y?= =?UTF-8?q?=20cambio=20algunos=20m=C3=A9todos=20est=C3=A1ticos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/us/muit/fs/a4i/config/Context.java | 4 +-- .../us/muit/fs/a4i/model/entities/Font.java | 32 +++++++++++++++++++ .../fs/a4i/persistence/ReportFormater.java | 9 ++++-- .../muit/fs/a4i/test/config/ContextTest.java | 27 ++++++++++++++-- src/test/resources/appTest.conf | 5 ++- 5 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 src/main/java/us/muit/fs/a4i/model/entities/Font.java diff --git a/src/main/java/us/muit/fs/a4i/config/Context.java b/src/main/java/us/muit/fs/a4i/config/Context.java index 6a817879..c2dde4de 100644 --- a/src/main/java/us/muit/fs/a4i/config/Context.java +++ b/src/main/java/us/muit/fs/a4i/config/Context.java @@ -245,7 +245,7 @@ public Font getDefaultFont() { * * @return la fuente para las métricas */ - public static Font getMetricFont() { + public Font getMetricFont() { Font font = null; // TO DO return font; @@ -265,7 +265,7 @@ public static Font getMetricFont() { * @throws IOException problema al leer el fichero */ - public static Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOException { + public Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOException { Font font = null; // TO DO diff --git a/src/main/java/us/muit/fs/a4i/model/entities/Font.java b/src/main/java/us/muit/fs/a4i/model/entities/Font.java new file mode 100644 index 00000000..e88321cf --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/model/entities/Font.java @@ -0,0 +1,32 @@ +package us.muit.fs.a4i.model.entities; + +public class Font extends java.awt.Font{ + + private String color; + + + public Font(String name, int size, String color){ + super(name,PLAIN,size); + //this.name = name; + //this.size = size; + this.color = color; + } + + public String getColor(){ + return this.color; + } + /* + public Font getFont(){ + return null; + + } + + public String getName(){ + return name; + } + + public int getSize(){ + return size; + } + */ +} diff --git a/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java b/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java index 48e4b4a3..23160dee 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java @@ -36,7 +36,12 @@ public ReportFormater() { @Override public Font getMetricFont() { if (metricFont == null) { - metricFont = Context.getMetricFont(); + try { + metricFont = Context.getContext().getMetricFont(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } } return metricFont; } @@ -51,7 +56,7 @@ public void setMetricFont(Font font) { public Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOException { if (!indicatorsFont.containsKey(state)) { try { - indicatorsFont.put(state, Context.getIndicatorFont(state)); + indicatorsFont.put(state, Context.getContext().getIndicatorFont(state)); } catch (IOException e) { indicatorsFont.put(state, Context.getContext().getDefaultFont()); } diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java index 797b8081..7cc3aeff 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import java.awt.Font; +import us.muit.fs.a4i.model.entities.Font; import java.awt.GraphicsEnvironment; import java.io.File; import java.io.IOException; @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import us.muit.fs.a4i.config.Context; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; /** * @author Isabel Román @@ -229,7 +230,29 @@ void testGetMetricFont() { */ @Test void testGetIndicatorFont() { - fail("Not yet implemented"); + try { + Font font = null; + + // Se le solicita la fuente del estado indefinido, que tendrá los valores por defecto al no estar + // definidas sus propiedades en el fichero de configuración utilizados en los tests. + font = Context.getContext().getIndicatorFont(IndicatorState.UNDEFINED); + assertNotNull(font, "No se ha inicializado bien la fuente"); + // El nombre o tipo de la fuente podrá ser Arial o Times según el momento en el que se realicen los tests. + assertTrue("Arial".equals(font.getName()) || "Times".equals(font.getName()), + "No es el tipo de fuente especificado en el fichero de propiedades"); + + // Se le solicita al contexto la fuente del estao "CRITICAL", cuyas propiedades están definidas en el + // fichero de configuración utilizado en los tests. + font = Context.getContext().getIndicatorFont(IndicatorState.CRITICAL); + assertNotNull(font, "No se ha inicializado bien la fuente"); + assertTrue("Verdana".equals(font.getName()),"No es el tipo de fuente especificado en el fichero de propiedades"); + assertTrue("Blue".equals(font.getColor()),"No es el color de fuente especificado en el fichero de propiedades"); + assertTrue(20 == font.getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + + } catch (IOException e) { + fail("No debería devolver esta excepción"); + e.printStackTrace(); + } } /** diff --git a/src/test/resources/appTest.conf b/src/test/resources/appTest.conf index 27ca39d8..60decfb0 100644 --- a/src/test/resources/appTest.conf +++ b/src/test/resources/appTest.conf @@ -3,4 +3,7 @@ #Caracteristicas por defecto de la fuente, cuando los informes son visuales Font.default.color=Red Font.default.height=16 -Font.default.type=Times \ No newline at end of file +Font.default.type=Times +Font.critical.color=Blue +Font.critical.height=20 +Font.critical.type=Verdana \ No newline at end of file From 5921af0e6fc728e18cf37dca9a4978020841c644 Mon Sep 17 00:00:00 2001 From: frarosrap Date: Thu, 11 May 2023 11:33:36 +0200 Subject: [PATCH 045/100] =?UTF-8?q?Cambios=20en=20ficheros=20de=20configur?= =?UTF-8?q?aci=C3=B3n=20y=20test=20GetIndicatorFont?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/a4i.conf | 6 +++++- src/test/java/us/muit/fs/a4i/test/config/ContextTest.java | 2 +- src/test/resources/appTest.conf | 5 +---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/resources/a4i.conf b/src/main/resources/a4i.conf index 022639ea..67ec8571 100644 --- a/src/main/resources/a4i.conf +++ b/src/main/resources/a4i.conf @@ -6,4 +6,8 @@ remote.type=GITHUB #Caracteristicas por defecto de la fuente, cuando los informes son visuales Font.default.color=Black Font.default.height=12 -Font.default.type=Arial \ No newline at end of file +Font.default.type=Arial +#Caracteristicas de la fuente para el indicador Critical +Font.critical.color=Blue +Font.critical.height=20 +Font.critical.type=Verdana \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java index 7cc3aeff..c204722c 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java @@ -242,7 +242,7 @@ void testGetIndicatorFont() { "No es el tipo de fuente especificado en el fichero de propiedades"); // Se le solicita al contexto la fuente del estao "CRITICAL", cuyas propiedades están definidas en el - // fichero de configuración utilizado en los tests. + // fichero de configuración por defecto. font = Context.getContext().getIndicatorFont(IndicatorState.CRITICAL); assertNotNull(font, "No se ha inicializado bien la fuente"); assertTrue("Verdana".equals(font.getName()),"No es el tipo de fuente especificado en el fichero de propiedades"); diff --git a/src/test/resources/appTest.conf b/src/test/resources/appTest.conf index 60decfb0..27ca39d8 100644 --- a/src/test/resources/appTest.conf +++ b/src/test/resources/appTest.conf @@ -3,7 +3,4 @@ #Caracteristicas por defecto de la fuente, cuando los informes son visuales Font.default.color=Red Font.default.height=16 -Font.default.type=Times -Font.critical.color=Blue -Font.critical.height=20 -Font.critical.type=Verdana \ No newline at end of file +Font.default.type=Times \ No newline at end of file From c2bed80b32479327a49b94200408176cfb211b58 Mon Sep 17 00:00:00 2001 From: manmanher Date: Thu, 11 May 2023 11:40:00 +0200 Subject: [PATCH 046/100] Issues 86 y 90 --- .../java/us/muit/fs/a4i/config/Context.java | 38 +++++++++++---- .../us/muit/fs/a4i/model/entities/Font.java | 16 +++++++ .../fs/a4i/persistence/ReportFormater.java | 9 +++- src/main/resources/a4i.conf | 6 ++- .../muit/fs/a4i/test/config/ContextTest.java | 46 +++++++++++++++++-- 5 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 src/main/java/us/muit/fs/a4i/model/entities/Font.java diff --git a/src/main/java/us/muit/fs/a4i/config/Context.java b/src/main/java/us/muit/fs/a4i/config/Context.java index 6a817879..26aaead7 100644 --- a/src/main/java/us/muit/fs/a4i/config/Context.java +++ b/src/main/java/us/muit/fs/a4i/config/Context.java @@ -3,7 +3,7 @@ */ package us.muit.fs.a4i.config; -import java.awt.Font; +/**import java.awt.Font;**/ import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -13,6 +13,7 @@ import java.util.logging.Logger; import us.muit.fs.a4i.model.entities.IndicatorI; +import us.muit.fs.a4i.model.entities.Font; /** *

@@ -214,20 +215,21 @@ public String getRemoteType() throws IOException { * @return La fuente por defecto para indicadores y métricas */ public Font getDefaultFont() { - // OJO el color no forma parte de la clase font, por loq ue ese atributo debe + // OJO el color no forma parte de la clase font, por lo que ese atributo debe // estar fuera // Podría incluir un parámetro font para devolverlo a la salida y que lo que // devuelva sea un String con el color log.info("Busca la información de configuración de la fuente, por defecto"); // TO DO - String color = properties.getProperty("Font.default.color"); + String color = properties.getProperty("Font.default.color"); String height = properties.getProperty("Font.default.height"); - String type = properties.getProperty("Font.default.type"); + String type = properties.getProperty("Font.default.type"); log.info("Los datos son, color: " + color + " height: " + height + " type: " + type); log.info("Intento crear la fuente"); return new Font(type, Font.ITALIC, Integer.valueOf(height)); + } /** @@ -245,7 +247,7 @@ public Font getDefaultFont() { * * @return la fuente para las métricas */ - public static Font getMetricFont() { + public Font getMetricFont() { Font font = null; // TO DO return font; @@ -265,10 +267,30 @@ public static Font getMetricFont() { * @throws IOException problema al leer el fichero */ - public static Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOException { + public Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOException { + /**He eliminado el static, así si funciona + * Hay que comprobar si el static estaba puesto con sentido desde un primer momento + * **/ Font font = null; - - // TO DO + // TODO: + String propiedad = "Font." + state.toString(); + + String color = properties.getProperty(propiedad + ".color"); + String height = properties.getProperty(propiedad + ".height"); + String type = properties.getProperty(propiedad + ".type"); + + log.info("Los datos son, color: " + color + " height: " + height + " type: " + type); + log.info("Intento crear la fuente"); + + if (color == null) { + color = properties.getProperty("Font.default.color"); + } else if (height == null) { + height = properties.getProperty("Font.default.height"); + } else { + type = properties.getProperty("Font.default.type"); + } + + font = new Font(type, Integer.valueOf(height), color); return font; } diff --git a/src/main/java/us/muit/fs/a4i/model/entities/Font.java b/src/main/java/us/muit/fs/a4i/model/entities/Font.java new file mode 100644 index 00000000..54049afd --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/model/entities/Font.java @@ -0,0 +1,16 @@ +package us.muit.fs.a4i.model.entities; + +public class Font extends java.awt.Font{ + + private String color; + + public Font(String name, int size, String color){ + super(name, PLAIN, size); + this.color = color; + } + + public String getColor(){ + return this.color; + } + +} diff --git a/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java b/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java index 48e4b4a3..23160dee 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java @@ -36,7 +36,12 @@ public ReportFormater() { @Override public Font getMetricFont() { if (metricFont == null) { - metricFont = Context.getMetricFont(); + try { + metricFont = Context.getContext().getMetricFont(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } } return metricFont; } @@ -51,7 +56,7 @@ public void setMetricFont(Font font) { public Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOException { if (!indicatorsFont.containsKey(state)) { try { - indicatorsFont.put(state, Context.getIndicatorFont(state)); + indicatorsFont.put(state, Context.getContext().getIndicatorFont(state)); } catch (IOException e) { indicatorsFont.put(state, Context.getContext().getDefaultFont()); } diff --git a/src/main/resources/a4i.conf b/src/main/resources/a4i.conf index 022639ea..14f7e1a2 100644 --- a/src/main/resources/a4i.conf +++ b/src/main/resources/a4i.conf @@ -6,4 +6,8 @@ remote.type=GITHUB #Caracteristicas por defecto de la fuente, cuando los informes son visuales Font.default.color=Black Font.default.height=12 -Font.default.type=Arial \ No newline at end of file +Font.default.type=Arial +#Caracteristicas de fuente de las métricas +Font.metric.color=Green +Font.metric.height=15 +Font.metric.type=Serif \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java index 797b8081..496b6471 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java @@ -4,12 +4,13 @@ package us.muit.fs.a4i.test.config; import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import java.awt.Font; +//import java.awt.Font; import java.awt.GraphicsEnvironment; import java.io.File; import java.io.IOException; @@ -25,6 +26,25 @@ import org.junit.jupiter.api.Test; import us.muit.fs.a4i.config.Context; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.Set; +import java.util.logging.Logger; + +import us.muit.fs.a4i.model.entities.IndicatorI; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.Set; +import java.util.logging.Logger; + +import us.muit.fs.a4i.model.entities.IndicatorI; +import us.muit.fs.a4i.model.entities.Font; /** * @author Isabel Román @@ -198,7 +218,7 @@ void testGetRemoteType() { void testGetDefaultFont() { try { Font font = null; - String color; + String color; // No entiendo cómo usarlo, ¿para darle valor luego?// // Uso esto para ver los tipos de fuentes de los que dispongo //String[] fontNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); //log.info("listado de fuentes " + Arrays.toString(fontNames)); @@ -206,8 +226,9 @@ void testGetDefaultFont() { assertNotNull(font, "No se ha inicializado bien la fuente"); //Al ser Context un singleton una vez creada la instancia no se puede eliminar "desde fuera" //De manera que el valor de la fuente depende del orden en el que se ejecuten los test, y para que el test sea independiente de eso la verificación comprueba los dos posibles valores + assertTrue("Black".equals(font.getColor()) || "Red".equals(font.getColor()),"No es el color de fuente especificado en el fichero de propiedades"); + assertTrue(12 == font.getSize() || 16 == font.getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); assertTrue("Arial".equals(font.getName()) || "Times".equals(font.getName()),"No es el tipo de fuente especificado en el fichero de propiedades"); - } catch (IOException e) { fail("No debería devolver esta excepción"); @@ -217,10 +238,25 @@ void testGetDefaultFont() { /** * Test method for {@link us.muit.fs.a4i.config.Context#getMetricFont()}. + * @throws IOException */ @Test - void testGetMetricFont() { - fail("Not yet implemented"); + + void testGetMetricFont(){ + try { + //fail("Not yet implemented"); + Font font = null; + String color; // No entiendo cómo usarlo, ¿para darle valor luego?// + font = Context.getContext().getMetricFont(); + assertNotNull(font, "No se ha inicializado bien la fuente"); + assertTrue("Green".equals(font.getColor()),"No es el color de fuente especificado en el fichero de propiedades"); + assertTrue(15 == font.getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + assertTrue("Serif".equals(font.getName()),"No es el tipo de fuente especificado en el fichero de propiedades"); + + }catch (IOException e) { + fail("No debería devolver esta excepción"); + e.printStackTrace(); + } } /** From 4d8a9b1b900db8810162154866fe6f4152cadf4e Mon Sep 17 00:00:00 2001 From: manmanher Date: Thu, 11 May 2023 12:22:27 +0200 Subject: [PATCH 047/100] Fixed errors with Issue #86 --- src/main/java/us/muit/fs/a4i/config/Context.java | 6 ++++-- src/main/resources/a4i.conf | 7 +++---- src/test/java/us/muit/fs/a4i/test/config/ContextTest.java | 2 ++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/config/Context.java b/src/main/java/us/muit/fs/a4i/config/Context.java index 26aaead7..436ed3c6 100644 --- a/src/main/java/us/muit/fs/a4i/config/Context.java +++ b/src/main/java/us/muit/fs/a4i/config/Context.java @@ -284,9 +284,11 @@ public Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOException if (color == null) { color = properties.getProperty("Font.default.color"); - } else if (height == null) { + } + if (height == null) { height = properties.getProperty("Font.default.height"); - } else { + } + if(type == null){ type = properties.getProperty("Font.default.type"); } diff --git a/src/main/resources/a4i.conf b/src/main/resources/a4i.conf index 1097ca48..1145a6ed 100644 --- a/src/main/resources/a4i.conf +++ b/src/main/resources/a4i.conf @@ -12,7 +12,6 @@ Font.metric.color=Green Font.metric.height=15 Font.metric.type=Serif #Caracteristicas de la fuente para el indicador Critical -Font.critical.color=Blue -Font.critical.height=20 -Font.critical.type=Verdana - +Font.CRITICAL.color=Blue +Font.CRITICAL.height=20 +Font.CRITICAL.type=Verdana \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java index a5ea31bf..661c304d 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java @@ -35,6 +35,8 @@ import java.util.logging.Logger; import us.muit.fs.a4i.model.entities.IndicatorI; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; From cfb173d06afa754445d9a49bae0662fd792399c6 Mon Sep 17 00:00:00 2001 From: frarosrap Date: Thu, 11 May 2023 12:22:42 +0200 Subject: [PATCH 048/100] issue 89 --- .../java/us/muit/fs/a4i/config/Context.java | 36 +++++++++++++++---- src/main/resources/a4i.conf | 6 ++-- .../muit/fs/a4i/test/config/ContextTest.java | 2 ++ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/config/Context.java b/src/main/java/us/muit/fs/a4i/config/Context.java index 26aaead7..8f0c9a26 100644 --- a/src/main/java/us/muit/fs/a4i/config/Context.java +++ b/src/main/java/us/muit/fs/a4i/config/Context.java @@ -228,7 +228,7 @@ public Font getDefaultFont() { log.info("Los datos son, color: " + color + " height: " + height + " type: " + type); log.info("Intento crear la fuente"); - return new Font(type, Font.ITALIC, Integer.valueOf(height)); + return new Font(type,Integer.valueOf(height),color); } @@ -248,9 +248,31 @@ public Font getDefaultFont() { * @return la fuente para las métricas */ public Font getMetricFont() { - Font font = null; - // TO DO - return font; + log.info("Busca la información de configuración de la fuente, para las métricas"); + + String type = properties.getProperty("Font.metric.type"); + String height = properties.getProperty("Font.metric.height"); + String color = properties.getProperty("Font.metric.color"); + + if (type==null){ + type = properties.getProperty("Font.default.type"); + log.info("El tipo de la fuente de las metricas es el valor por defecto"); + } + if (height==null){ + height = properties.getProperty("Font.default.height"); + log.info("El tamaño de la fuente de las metricas es el valor por defecto"); + } + if (color==null){ + color = properties.getProperty("Font.default.color"); + log.info("El color de la fuente de las metricas es el valor por defecto"); + } + + log.info("Los datos son, color: " + color + " height: " + height + " type: " + type); + + log.info("Intento crear la fuente"); + + return new Font(type, Integer.valueOf(height), color); + } /** @@ -284,9 +306,11 @@ public Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOException if (color == null) { color = properties.getProperty("Font.default.color"); - } else if (height == null) { + } + if (height == null) { height = properties.getProperty("Font.default.height"); - } else { + } + if (type == null){ type = properties.getProperty("Font.default.type"); } diff --git a/src/main/resources/a4i.conf b/src/main/resources/a4i.conf index 1097ca48..a142aca8 100644 --- a/src/main/resources/a4i.conf +++ b/src/main/resources/a4i.conf @@ -12,7 +12,7 @@ Font.metric.color=Green Font.metric.height=15 Font.metric.type=Serif #Caracteristicas de la fuente para el indicador Critical -Font.critical.color=Blue -Font.critical.height=20 -Font.critical.type=Verdana +Font.CRITICAL.color=Blue +Font.CRITICAL.height=20 +Font.CRITICAL.type=Verdana diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java index a5ea31bf..661c304d 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java @@ -35,6 +35,8 @@ import java.util.logging.Logger; import us.muit.fs.a4i.model.entities.IndicatorI; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; From c77fea1a705d4a72b595e1ef3b01d33a45742b93 Mon Sep 17 00:00:00 2001 From: celllarod Date: Thu, 11 May 2023 15:42:57 +0200 Subject: [PATCH 049/100] =?UTF-8?q?FIX:=20Se=20modifican=20los=20tipos=20g?= =?UTF-8?q?en=C3=A9ricos=20de=20la=20interfaz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../us/muit/fs/a4i/control/IndicatorStrategy.java | 7 ++++--- .../muit/fs/a4i/control/IssuesRatioIndicator.java | 15 ++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java b/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java index a1f902fc..226a612e 100644 --- a/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java @@ -6,14 +6,15 @@ import us.muit.fs.a4i.model.entities.ReportItemI; /** - * + * /** *

* Interfaz para calcular indicadores *

+ * @param * @author celllarod * */ -public interface IndicatorStrategy { +public interface IndicatorStrategy { /** @@ -23,7 +24,7 @@ public interface IndicatorStrategy { * @throws NotAvailableMetricException * @return indicador */ - public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException; + public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException; /** diff --git a/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicator.java b/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicator.java index 1f4af1c9..1a3f332d 100644 --- a/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicator.java +++ b/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicator.java @@ -2,6 +2,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.logging.Logger; import us.muit.fs.a4i.exceptions.NotAvailableMetricException; @@ -11,7 +12,7 @@ import us.muit.fs.a4i.model.entities.ReportItem; import us.muit.fs.a4i.model.entities.ReportItemI; -public class IssuesRatioIndicator implements IndicatorStrategy { +public class IssuesRatioIndicator implements IndicatorStrategy { private static Logger log = Logger.getLogger(Indicator.class.getName()); @@ -19,20 +20,20 @@ public class IssuesRatioIndicator implements IndicatorStrategy { private static final List REQUIRED_METRICS = Arrays.asList("openIssues", "closedIssues"); @Override - public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { + public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { // Se obtienen y se comprueba que se pasan las métricas necesarias para calcular // el indicador. - var openIssues = metrics.stream().filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); - var closedIssues = metrics.stream().filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); + Optional> openIssues = metrics.stream().filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); + Optional> closedIssues = metrics.stream().filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); ReportItemI indicatorReport = null; if (openIssues.isPresent() && closedIssues.isPresent()) { // Se realiza el cálculo del indicador - Double issuesRatio = Double.parseDouble(openIssues.get().getValue().toString()) - / Double.parseDouble(closedIssues.get().getValue().toString()); + Double issuesRatio = openIssues.get().getValue()/closedIssues.get().getValue(); try { + // Se crea el indicador indicatorReport = new ReportItem.ReportItemBuilder("issuesRatio", issuesRatio) .metrics(Arrays.asList(openIssues.get(), closedIssues.get())) .indicator(IndicatorState.UNDEFINED).build(); @@ -46,7 +47,7 @@ public ReportItemI calcIndicator(List> metrics) throws Not throw new NotAvailableMetricException("No se han proporcionado las métricas necesarias"); } - return (ReportItemI) indicatorReport; + return indicatorReport; } @Override From 69c152a2768ad6b92d56868a3ee709f8e09fcf94 Mon Sep 17 00:00:00 2001 From: vicguzher Date: Thu, 11 May 2023 16:12:36 +0200 Subject: [PATCH 050/100] Tarea 95 --- .../fs/a4i/control/IndicatorStrategy.java | 6 ++-- .../fs/a4i/control/IndicatorsCalculator.java | 3 ++ .../fs/a4i/control/RepositoryCalculator.java | 34 +++++++++++++++++-- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java b/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java index 226a612e..37aebfd4 100644 --- a/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java @@ -18,7 +18,7 @@ public interface IndicatorStrategy { /** - * Calcula un indicador a partir de las métricas proporcionadas. + * Calcula un indicador a partir de las m�tricas proporcionadas. * @param * @param metrics * @throws NotAvailableMetricException @@ -28,8 +28,8 @@ public interface IndicatorStrategy { /** - * Obtiene las métricas necesarias - * @return listado de métricas + * Obtiene las m�tricas necesarias + * @return listado de m�tricas */ public List requiredMetrics(); diff --git a/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java b/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java index e0b2835a..260239e2 100644 --- a/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java @@ -5,6 +5,7 @@ import us.muit.fs.a4i.exceptions.IndicatorException; import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.control.IndicatorStrategy; /** * @@ -53,4 +54,6 @@ public interface IndicatorsCalculator { * @return El tipo de informes */ public ReportI.ReportType getReportType(); + + public void setIndicator(String indicatorName, IndicatorStrategy strategy); } diff --git a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java index 4a2a2a68..59be0f82 100644 --- a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java @@ -3,11 +3,17 @@ */ package us.muit.fs.a4i.control; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; import java.util.logging.Logger; import us.muit.fs.a4i.exceptions.IndicatorException; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; import us.muit.fs.a4i.model.entities.Indicator; import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItemI; +import java.util.stream.Collectors; /** *

@@ -24,6 +30,8 @@ public class RepositoryCalculator implements IndicatorsCalculator { private static Logger log = Logger.getLogger(RepositoryCalculator.class.getName()); private static ReportI.ReportType reportType = ReportI.ReportType.REPOSITORY; + private static HashMap strategies = new HashMap<>(); + @Override public void calcIndicator(String indicatorName, ReportManagerI reportManager) throws IndicatorException { @@ -33,7 +41,22 @@ public void calcIndicator(String indicatorName, ReportManagerI reportManager) th * no están busca las métricas, las añade al informe y lo calcula * */ - + IndicatorStrategy indicatorStrategy = strategies.get(indicatorName); + List requiredMetrics = indicatorStrategy.requiredMetrics(); + List metrics = reportManager.getReport().getAllMetrics().stream().collect(Collectors.toList()); + List metricsName = metrics.stream().map(ReportItemI::getName).collect(Collectors.toList()); + if(metricsName.containsAll(requiredMetrics)) { + try { + indicatorStrategy.calcIndicator(metrics); + } catch (NotAvailableMetricException e) { + log.info("No se han proporcionado las m�tricas necesarias"); + e.printStackTrace(); + } + } + + + + } /** @@ -57,4 +80,11 @@ public ReportI.ReportType getReportType() { return reportType; } -} + @Override + public void setIndicator(String indicatorName, IndicatorStrategy strategy) { + // TODO Auto-generated method stub + strategies.put(indicatorName, strategy); + + } + +} \ No newline at end of file From a1b5f7a3a0c855ad66b409444204f04180718d45 Mon Sep 17 00:00:00 2001 From: vicguzher Date: Thu, 11 May 2023 16:21:10 +0200 Subject: [PATCH 051/100] FIXED: else --- .../us/muit/fs/a4i/control/RepositoryCalculator.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java index 59be0f82..3b8cc5b0 100644 --- a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java @@ -32,7 +32,6 @@ public class RepositoryCalculator implements IndicatorsCalculator { private static ReportI.ReportType reportType = ReportI.ReportType.REPOSITORY; private static HashMap strategies = new HashMap<>(); - @Override public void calcIndicator(String indicatorName, ReportManagerI reportManager) throws IndicatorException { log.info("Calcula el indicador de nombre " + indicatorName); @@ -45,18 +44,17 @@ public void calcIndicator(String indicatorName, ReportManagerI reportManager) th List requiredMetrics = indicatorStrategy.requiredMetrics(); List metrics = reportManager.getReport().getAllMetrics().stream().collect(Collectors.toList()); List metricsName = metrics.stream().map(ReportItemI::getName).collect(Collectors.toList()); - if(metricsName.containsAll(requiredMetrics)) { + if (metricsName.containsAll(requiredMetrics)) { try { indicatorStrategy.calcIndicator(metrics); } catch (NotAvailableMetricException e) { log.info("No se han proporcionado las m�tricas necesarias"); e.printStackTrace(); } + } else { + log.info("No se han proporcionado las m�tricas necesarias"); } - - - - + } /** @@ -84,7 +82,7 @@ public ReportI.ReportType getReportType() { public void setIndicator(String indicatorName, IndicatorStrategy strategy) { // TODO Auto-generated method stub strategies.put(indicatorName, strategy); - + } } \ No newline at end of file From fd1f8894cd84068c321786c3bbbbc078237787bd Mon Sep 17 00:00:00 2001 From: vicguzher Date: Thu, 11 May 2023 16:49:10 +0200 Subject: [PATCH 052/100] TEST: IssuesRatioIndicator --- .../fs/a4i/control/IssuesRatioIndicator.java | 12 +- .../control/IssuesRatioIndicatorTest.java | 121 ++++++++++++++++++ 2 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java diff --git a/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicator.java b/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicator.java index 1a3f332d..dc5a6ee1 100644 --- a/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicator.java +++ b/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicator.java @@ -16,12 +16,12 @@ public class IssuesRatioIndicator implements IndicatorStrategy { private static Logger log = Logger.getLogger(Indicator.class.getName()); - // Métricas necesarias para calcular el indicador + // M�tricas necesarias para calcular el indicador private static final List REQUIRED_METRICS = Arrays.asList("openIssues", "closedIssues"); @Override public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { - // Se obtienen y se comprueba que se pasan las métricas necesarias para calcular + // Se obtienen y se comprueba que se pasan las m�tricas necesarias para calcular // el indicador. Optional> openIssues = metrics.stream().filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); Optional> closedIssues = metrics.stream().filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); @@ -29,7 +29,7 @@ public ReportItemI calcIndicator(List> metrics) thro if (openIssues.isPresent() && closedIssues.isPresent()) { - // Se realiza el cálculo del indicador + // Se realiza el c�lculo del indicador Double issuesRatio = openIssues.get().getValue()/closedIssues.get().getValue(); try { @@ -43,8 +43,8 @@ public ReportItemI calcIndicator(List> metrics) thro } } else { - log.info("No se han proporcionado las métricas necesarias"); - throw new NotAvailableMetricException("No se han proporcionado las métricas necesarias"); + log.info("No se han proporcionado las m�tricas necesarias"); + throw new NotAvailableMetricException("No se han proporcionado las metricas necesarias"); } return indicatorReport; @@ -52,7 +52,7 @@ public ReportItemI calcIndicator(List> metrics) thro @Override public List requiredMetrics() { - // Para calcular el indicador "IssuesRatio", serán necesarias las métricas + // Para calcular el indicador "IssuesRatio", ser�n necesarias las m�tricas // "openIssues" y "closedIssues". return REQUIRED_METRICS; } diff --git a/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java new file mode 100644 index 00000000..6428cb95 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java @@ -0,0 +1,121 @@ +package us.muit.fs.a4i.test.control; + +import java.util.logging.Logger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; + +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; + +import us.muit.fs.a4i.control.IssuesRatioIndicator; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.model.entities.ReportItemI; + +public class IssuesRatioIndicatorTest { + + private static Logger log = Logger.getLogger(IssuesRatioIndicatorTest.class.getName()); + + /** + * @throws java.lang.Exception + */ + @BeforeAll + static void setUpBeforeClass() throws Exception { + } + + /** + * @throws java.lang.Exception + */ + @AfterAll + static void tearDownAfterClass() throws Exception { + } + + /** + * @throws java.lang.Exception + */ + @BeforeEach + void setUp() throws Exception { + } + + /** + * @throws java.lang.Exception + */ + @AfterEach + void tearDown() throws Exception { + } + + + + + @Test + public void testCalcIndicator() throws NotAvailableMetricException { + // Creamos los mocks necesarios + ReportItemI mockOpenIssues = Mockito.mock(ReportItemI.class); + ReportItemI mockClosedIssues = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para devolver valores predefinidos + Mockito.when(mockOpenIssues.getName()).thenReturn("openIssues"); + Mockito.when(mockOpenIssues.getValue()).thenReturn(10.0); + + Mockito.when(mockClosedIssues.getName()).thenReturn("closedIssues"); + Mockito.when(mockClosedIssues.getValue()).thenReturn(5.0); + + // Creamos una instancia de IssuesRatioIndicator + IssuesRatioIndicator indicator = new IssuesRatioIndicator(); + + // Ejecutamos el método que queremos probar con los mocks como argumentos + List> metrics = Arrays.asList(mockOpenIssues, mockClosedIssues); + ReportItemI result = indicator.calcIndicator(metrics); + + // Comprobamos que el resultado es el esperado + Assertions.assertEquals("issuesRatio", result.getName()); + Assertions.assertEquals(2.0, result.getValue()); + } + + @Test + public void testCalcIndicatorThrowsNotAvailableMetricException() { + // Creamos los mocks necesarios + ReportItemI mockOpenIssues = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para devolver valores predefinidos + Mockito.when(mockOpenIssues.getName()).thenReturn("openIssues"); + Mockito.when(mockOpenIssues.getValue()).thenReturn(10.0); + + // Creamos una instancia de IssuesRatioIndicator + IssuesRatioIndicator indicator = new IssuesRatioIndicator(); + + // Ejecutamos el método que queremos probar con una sola métrica + List> metrics = Arrays.asList(mockOpenIssues); + NotAvailableMetricException exception = Assertions.assertThrows(NotAvailableMetricException.class, + () -> indicator.calcIndicator(metrics)); + + // Comprobamos que se lanza la excepción adecuada + Assertions.assertEquals("No se han proporcionado las metricas necesarias", exception.getMessage()); + } + + + @Test + public void testRequiredMetrics() { + // Creamos una instancia de IssuesRatioIndicator + IssuesRatioIndicator indicator = new IssuesRatioIndicator(); + + // Ejecutamos el método que queremos probar + List requiredMetrics = indicator.requiredMetrics(); + + // Comprobamos que el resultado es el esperado + List expectedMetrics = Arrays.asList("openIssues", "closedIssues"); + Assertions.assertEquals(expectedMetrics, requiredMetrics); + } + } + + + From a94f75bf68f8ee2d0cc9e5957f082a29ad2fb863 Mon Sep 17 00:00:00 2001 From: vicguzher Date: Thu, 11 May 2023 16:51:01 +0200 Subject: [PATCH 053/100] FIX: warning de los test --- .../us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java index 6428cb95..f818c4b3 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java @@ -14,7 +14,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatchers; + import org.mockito.Mockito; import us.muit.fs.a4i.control.IssuesRatioIndicator; From 67aa3de8763103299328216e4c4061b5444bed84 Mon Sep 17 00:00:00 2001 From: celllarod Date: Thu, 11 May 2023 19:22:17 +0200 Subject: [PATCH 054/100] =?UTF-8?q?FIX:=20Se=20corrige=20tipo=20de=20la=20?= =?UTF-8?q?m=C3=A9trica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/a4iDefault.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index e0bff341..b6eda890 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -43,13 +43,13 @@ }, { "name": "openIssues", - "type": "java.lang.Integer", + "type": "java.lang.Double", "description": "Numero de issues abiertas", "unit": "issues" }, { "name": "closedIssues", - "type": "java.lang.Integer", + "type": "java.lang.Double", "description": "Numero de issues cerradas", "unit": "issues" } From 1da03cb74118cc27cea5e709bc9eb44bb51538f1 Mon Sep 17 00:00:00 2001 From: celllarod Date: Thu, 11 May 2023 19:22:43 +0200 Subject: [PATCH 055/100] TEST: Se crean tests para RepositoryCalculator --- .../fs/a4i/control/RepositoryCalculator.java | 13 +- .../control/RepositoryCalculatorTest.java | 169 ++++++++++++++++++ 2 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java diff --git a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java index 3b8cc5b0..22ce8c6e 100644 --- a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java @@ -3,7 +3,6 @@ */ package us.muit.fs.a4i.control; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.logging.Logger; @@ -36,12 +35,13 @@ public class RepositoryCalculator implements IndicatorsCalculator { public void calcIndicator(String indicatorName, ReportManagerI reportManager) throws IndicatorException { log.info("Calcula el indicador de nombre " + indicatorName); /** - * Tiene que mirar si están ya las métricas que necesita Si están lo calcula Si - * no están busca las métricas, las añade al informe y lo calcula + * Tiene que mirar si están ya las métricas que necesita Si están lo calcula + * Si no están busca las métricas, las añade al informe y lo calcula * */ IndicatorStrategy indicatorStrategy = strategies.get(indicatorName); List requiredMetrics = indicatorStrategy.requiredMetrics(); + log.info("Las métricas necesarias son: " + requiredMetrics.toString()); List metrics = reportManager.getReport().getAllMetrics().stream().collect(Collectors.toList()); List metricsName = metrics.stream().map(ReportItemI::getName).collect(Collectors.toList()); if (metricsName.containsAll(requiredMetrics)) { @@ -52,14 +52,14 @@ public void calcIndicator(String indicatorName, ReportManagerI reportManager) th e.printStackTrace(); } } else { - log.info("No se han proporcionado las m�tricas necesarias"); + log.info("No se han proporcionado las metricas necesarias"); } - } /** * Calcula todos los indicadores definidos para un repositorio Recupera todas - * las métricas que necesite y que no estén en el informe y las añade al mismo + * las métricas que necesite y que no estén en el informe y las añade al + * mismo * */ @Override @@ -80,7 +80,6 @@ public ReportI.ReportType getReportType() { @Override public void setIndicator(String indicatorName, IndicatorStrategy strategy) { - // TODO Auto-generated method stub strategies.put(indicatorName, strategy); } diff --git a/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java new file mode 100644 index 00000000..05eb1fae --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java @@ -0,0 +1,169 @@ +package us.muit.fs.a4i.test.control; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; + +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.control.IndicatorStrategy; +import us.muit.fs.a4i.control.IssuesRatioIndicator; +import us.muit.fs.a4i.control.ReportManagerI; +import us.muit.fs.a4i.control.RepositoryCalculator; +import us.muit.fs.a4i.exceptions.IndicatorException; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; +import us.muit.fs.a4i.model.entities.ReportItemI; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class RepositoryCalculatorTest { + + private static Logger log = Logger.getLogger(ReportManagerTest.class.getName()); + + /** + * @throws java.lang.Exception + */ + @BeforeAll + static void setUpBeforeClass() throws Exception { + } + + /** + * @throws java.lang.Exception + */ + @AfterAll + static void tearDownAfterClass() throws Exception { + + } + + /** + * @throws java.lang.Exception + */ + @BeforeEach + void setUp() throws Exception { + + } + + /** + * @throws java.lang.Exception + */ + @AfterEach + void tearDown() throws Exception { + } + + /** + * Test para el metodo calcIndicator de RepositoryCalculator + * {@link us.muit.fs.a4i.control.RepositoryCalculator.calcIndicator(String, + * ReportManagerI)}. + */ + @Test + @DisplayName("Prueba calcIndicator de RepositoryCalculator") + void testCalIndicator() { + + // Creamos la clase a probar + RepositoryCalculator repositoryCalculator = new RepositoryCalculator(); + + // Creamos mocks necesarios + ReportManagerI reportManagerMock = Mockito.mock(ReportManagerI.class); + ReportI report = Mockito.mock(ReportI.class); + + List metricsMock = new ArrayList<>(); + ReportItemI metric1 = Mockito.mock(ReportItemI.class); + ReportItemI metric2 = Mockito.mock(ReportItemI.class); + + metricsMock.add(metric1); + metricsMock.add(metric2); + + IndicatorStrategy indicatorStrategyMock = Mockito.mock(IndicatorStrategy.class); + + Mockito.when(reportManagerMock.getReport()).thenReturn(report); + + // Metrics + Mockito.when(report.getAllMetrics()).thenReturn(metricsMock); + + // Required metrics para el indicador forzando a que coincidan con las métricas creadas + List requiredMetrics = new ArrayList<>(); + requiredMetrics.add(metric1.getName()); + requiredMetrics.add(metric2.getName()); + Mockito.when(indicatorStrategyMock.requiredMetrics()).thenReturn(requiredMetrics); + + // Seteamos alguna estrategia + repositoryCalculator.setIndicator("indicator1", indicatorStrategyMock); + + + // Llamamos a calIndicator + try { + repositoryCalculator.calcIndicator("indicator1", reportManagerMock); + } catch (IndicatorException e) { + e.printStackTrace(); + } + + + // Verificamos que cada uno de los métodos hayan sido llamados + Mockito.verify(reportManagerMock).getReport(); + Mockito.verify(report).getAllMetrics(); + Mockito.verify(indicatorStrategyMock).requiredMetrics(); + try { + Mockito.verify(indicatorStrategyMock).calcIndicator(metricsMock); + } catch (NotAvailableMetricException e) { + e.printStackTrace(); + } + } + + + /** + * Test para el metodo calcIndicator de RepositoryCalculator + * Verifica que no llama a calIndiicator si no se le pasa la métrica adecuada + * {@link us.muit.fs.a4i.control.RepositoryCalculator.calcIndicator(String, + * ReportManagerI)}. + * @throws NotAvailableMetricException + * @throws ReportItemException */ + @Test + @Tag("integration") + @DisplayName("Prueba calcIndicator de RepositoryCalculator metricas incorrectas") + void testCalIndicatorNotRequiredMetrics() throws NotAvailableMetricException, ReportItemException{ + + // Creamos la clase a probar + RepositoryCalculator repositoryCalculator = new RepositoryCalculator(); + + // Creamos mocks necesarios + ReportManagerI reportManagerMock = Mockito.mock(ReportManagerI.class); + ReportI report = Mockito.mock(ReportI.class); + + List metricsMock = new ArrayList<>(); + + ReportItemBuilder mb1 = new ReportItem.ReportItemBuilder("issues", 2); + ReportItemBuilder mb2 = new ReportItem.ReportItemBuilder("closedIssues", 1.0); + + metricsMock.add(mb1.build()); + metricsMock.add(mb2.build()); + + IndicatorStrategy indicatorStrategyMock = Mockito.mock(IndicatorStrategy.class); + + Mockito.when(reportManagerMock.getReport()).thenReturn(report); + + // Metrics + Mockito.when(report.getAllMetrics()).thenReturn(metricsMock); + + + // Seteamos alguna estrategia + repositoryCalculator.setIndicator("issuesRatio", new IssuesRatioIndicator()); + + // Verificamos que no se ha llamado al método + Mockito.verify(indicatorStrategyMock, times(0)).calcIndicator(metricsMock); + } + +} From 38f02a7b40bbf5cd6f441c1cfdaf14963c83caad Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Tue, 12 Dec 2023 17:37:57 +0100 Subject: [PATCH 056/100] Cambios issues 95 y 96, Calculadora --- ...ssuesRatioIndicator.java => IssuesRatioIndicatorStrategy.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/java/us/muit/fs/a4i/control/{IssuesRatioIndicator.java => IssuesRatioIndicatorStrategy.java} (100%) diff --git a/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicator.java b/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicatorStrategy.java similarity index 100% rename from src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicator.java rename to src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicatorStrategy.java From d36073ca7d5f9568653484bf8ce99efdec12375b Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Tue, 12 Dec 2023 17:41:43 +0100 Subject: [PATCH 057/100] Issues 95 y 96 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modificación de la interfaz IndicatorsCalculator y la clase RepositoryCalculator Definición de la interfaz IndicatorStrategy, la excepción NotAvailableMetric e implementación de una estrategia concreta (que implemente la interfaz IndicatorStrategy). --- build.gradle | 1 + .../control/IssuesRatioIndicatorStrategy.java | 10 +- .../fs/a4i/control/RepositoryCalculator.java | 15 +- .../NotAvailableMetricException.java | 4 +- .../control/IssuesRatioIndicatorTest.java | 24 ++-- .../control/RepositoryCalculatorTest.java | 129 ++++++++++++++---- 6 files changed, 137 insertions(+), 46 deletions(-) diff --git a/build.gradle b/build.gradle index 55056e66..a59dbb73 100644 --- a/build.gradle +++ b/build.gradle @@ -124,6 +124,7 @@ dependencies { testImplementation(platform('org.junit:junit-bom:5.8.2')) //JAVADOC: https://www.javadoc.io/doc/org.junit.jupiter/junit-jupiter-api/latest/index.html testImplementation('org.junit.jupiter:junit-jupiter') + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicatorStrategy.java b/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicatorStrategy.java index dc5a6ee1..76fa1fa5 100644 --- a/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicatorStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicatorStrategy.java @@ -12,7 +12,7 @@ import us.muit.fs.a4i.model.entities.ReportItem; import us.muit.fs.a4i.model.entities.ReportItemI; -public class IssuesRatioIndicator implements IndicatorStrategy { +public class IssuesRatioIndicatorStrategy implements IndicatorStrategy { private static Logger log = Logger.getLogger(Indicator.class.getName()); @@ -28,9 +28,13 @@ public ReportItemI calcIndicator(List> metrics) thro ReportItemI indicatorReport = null; if (openIssues.isPresent() && closedIssues.isPresent()) { + Double issuesRatio; // Se realiza el c�lculo del indicador - Double issuesRatio = openIssues.get().getValue()/closedIssues.get().getValue(); + if(closedIssues.get().getValue()!=0) + issuesRatio = openIssues.get().getValue()/closedIssues.get().getValue(); + else + issuesRatio = openIssues.get().getValue(); try { // Se crea el indicador @@ -44,7 +48,7 @@ public ReportItemI calcIndicator(List> metrics) thro } else { log.info("No se han proporcionado las m�tricas necesarias"); - throw new NotAvailableMetricException("No se han proporcionado las metricas necesarias"); + throw new NotAvailableMetricException(REQUIRED_METRICS.toString()); } return indicatorReport; diff --git a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java index 22ce8c6e..9609ef62 100644 --- a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java @@ -35,31 +35,32 @@ public class RepositoryCalculator implements IndicatorsCalculator { public void calcIndicator(String indicatorName, ReportManagerI reportManager) throws IndicatorException { log.info("Calcula el indicador de nombre " + indicatorName); /** - * Tiene que mirar si están ya las métricas que necesita Si están lo calcula - * Si no están busca las métricas, las añade al informe y lo calcula + * Tiene que mirar si están ya las métricas que necesita Si están lo calcula Si + * no están busca las métricas, las añade al informe y lo calcula * */ IndicatorStrategy indicatorStrategy = strategies.get(indicatorName); List requiredMetrics = indicatorStrategy.requiredMetrics(); - log.info("Las métricas necesarias son: " + requiredMetrics.toString()); + log.info("Las m�tricas necesarias son: " + requiredMetrics.toString()); List metrics = reportManager.getReport().getAllMetrics().stream().collect(Collectors.toList()); List metricsName = metrics.stream().map(ReportItemI::getName).collect(Collectors.toList()); if (metricsName.containsAll(requiredMetrics)) { try { - indicatorStrategy.calcIndicator(metrics); + // ¡¡Faltaba añadir el indicador al informe!! + reportManager.getReport().addIndicator(indicatorStrategy.calcIndicator(metrics)); + log.info("Añadido al informe indicador"); } catch (NotAvailableMetricException e) { log.info("No se han proporcionado las m�tricas necesarias"); e.printStackTrace(); } - } else { + } else { log.info("No se han proporcionado las metricas necesarias"); } } /** * Calcula todos los indicadores definidos para un repositorio Recupera todas - * las métricas que necesite y que no estén en el informe y las añade al - * mismo + * las métricas que necesite y que no estén en el informe y las añade al mismo * */ @Override diff --git a/src/main/java/us/muit/fs/a4i/exceptions/NotAvailableMetricException.java b/src/main/java/us/muit/fs/a4i/exceptions/NotAvailableMetricException.java index eeb57ddc..d704b701 100644 --- a/src/main/java/us/muit/fs/a4i/exceptions/NotAvailableMetricException.java +++ b/src/main/java/us/muit/fs/a4i/exceptions/NotAvailableMetricException.java @@ -3,7 +3,7 @@ public class NotAvailableMetricException extends Exception{ /** - * Excepcion que ocurre cuando se quiere calcular un indicador pero no se proporcionan las métricas adecuadas. + * Excepcion que ocurre cuando se quiere calcular un indicador pero no se proporcionan las m�tricas adecuadas. */ private static final long serialVersionUID = 1L; /** @@ -19,7 +19,7 @@ public class NotAvailableMetricException extends Exception{ * @param info Mensaje definiendo el error */ public NotAvailableMetricException(String info) { - message = info; + message = "No se dispone de las métricas necesiarias " + info; } @Override diff --git a/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java index f818c4b3..b1dae5a6 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java @@ -1,4 +1,8 @@ package us.muit.fs.a4i.test.control; +/*** + * @author celllarod, curso 22/23 + * Pruebas añadidas por alumnos del curso 22/23 para probar la clase IssuesRatioIndicator + */ import java.util.logging.Logger; @@ -14,10 +18,10 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; - +import org.junit.jupiter.api.function.Executable; import org.mockito.Mockito; -import us.muit.fs.a4i.control.IssuesRatioIndicator; +import us.muit.fs.a4i.control.IssuesRatioIndicatorStrategy; import us.muit.fs.a4i.exceptions.NotAvailableMetricException; import us.muit.fs.a4i.model.entities.ReportItemI; @@ -70,7 +74,7 @@ public void testCalcIndicator() throws NotAvailableMetricException { Mockito.when(mockClosedIssues.getValue()).thenReturn(5.0); // Creamos una instancia de IssuesRatioIndicator - IssuesRatioIndicator indicator = new IssuesRatioIndicator(); + IssuesRatioIndicatorStrategy indicator = new IssuesRatioIndicatorStrategy(); // Ejecutamos el método que queremos probar con los mocks como argumentos List> metrics = Arrays.asList(mockOpenIssues, mockClosedIssues); @@ -79,6 +83,7 @@ public void testCalcIndicator() throws NotAvailableMetricException { // Comprobamos que el resultado es el esperado Assertions.assertEquals("issuesRatio", result.getName()); Assertions.assertEquals(2.0, result.getValue()); + Assertions.assertDoesNotThrow(()->indicator.calcIndicator(metrics)); } @Test @@ -91,25 +96,24 @@ public void testCalcIndicatorThrowsNotAvailableMetricException() { Mockito.when(mockOpenIssues.getValue()).thenReturn(10.0); // Creamos una instancia de IssuesRatioIndicator - IssuesRatioIndicator indicator = new IssuesRatioIndicator(); + IssuesRatioIndicatorStrategy indicator = new IssuesRatioIndicatorStrategy(); // Ejecutamos el método que queremos probar con una sola métrica List> metrics = Arrays.asList(mockOpenIssues); - NotAvailableMetricException exception = Assertions.assertThrows(NotAvailableMetricException.class, - () -> indicator.calcIndicator(metrics)); - // Comprobamos que se lanza la excepción adecuada - Assertions.assertEquals("No se han proporcionado las metricas necesarias", exception.getMessage()); + NotAvailableMetricException exception = Assertions.assertThrows(NotAvailableMetricException.class, + () -> indicator.calcIndicator(metrics)); + } @Test public void testRequiredMetrics() { // Creamos una instancia de IssuesRatioIndicator - IssuesRatioIndicator indicator = new IssuesRatioIndicator(); + IssuesRatioIndicatorStrategy indicatorStrategy = new IssuesRatioIndicatorStrategy(); // Ejecutamos el método que queremos probar - List requiredMetrics = indicator.requiredMetrics(); + List requiredMetrics = indicatorStrategy.requiredMetrics(); // Comprobamos que el resultado es el esperado List expectedMetrics = Arrays.asList("openIssues", "closedIssues"); diff --git a/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java index 05eb1fae..d8fd30b4 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java @@ -1,5 +1,10 @@ package us.muit.fs.a4i.test.control; +/*** + * @author celllarod, curso 22/23 + * Pruebas añadidas por alumnos del curso 22/23 para probar la clase RepositoryCalculator + */ + import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.times; @@ -18,12 +23,13 @@ import org.junit.jupiter.api.Test; import us.muit.fs.a4i.control.IndicatorStrategy; -import us.muit.fs.a4i.control.IssuesRatioIndicator; +import us.muit.fs.a4i.control.IssuesRatioIndicatorStrategy; import us.muit.fs.a4i.control.ReportManagerI; import us.muit.fs.a4i.control.RepositoryCalculator; import us.muit.fs.a4i.exceptions.IndicatorException; import us.muit.fs.a4i.exceptions.NotAvailableMetricException; import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.IndicatorI; import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItem; import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; @@ -79,14 +85,17 @@ void testCalIndicator() { // Creamos mocks necesarios ReportManagerI reportManagerMock = Mockito.mock(ReportManagerI.class); ReportI report = Mockito.mock(ReportI.class); - + List metricsMock = new ArrayList<>(); ReportItemI metric1 = Mockito.mock(ReportItemI.class); + // Faltaba: configurar el método getName de las métricas + Mockito.when(metric1.getName()).thenReturn("metric1"); ReportItemI metric2 = Mockito.mock(ReportItemI.class); - + Mockito.when(metric2.getName()).thenReturn("metric2"); + metricsMock.add(metric1); metricsMock.add(metric2); - + IndicatorStrategy indicatorStrategyMock = Mockito.mock(IndicatorStrategy.class); Mockito.when(reportManagerMock.getReport()).thenReturn(report); @@ -94,16 +103,24 @@ void testCalIndicator() { // Metrics Mockito.when(report.getAllMetrics()).thenReturn(metricsMock); - // Required metrics para el indicador forzando a que coincidan con las métricas creadas + // Required metrics para el indicador forzando a que coincidan con las m�tricas + // creadas List requiredMetrics = new ArrayList<>(); requiredMetrics.add(metric1.getName()); requiredMetrics.add(metric2.getName()); + ReportItemI nuevoIndicador = Mockito.mock(ReportItemI.class); + Mockito.when(nuevoIndicador.getName()).thenReturn("nuevoIndicador"); Mockito.when(indicatorStrategyMock.requiredMetrics()).thenReturn(requiredMetrics); + try { + Mockito.when(indicatorStrategyMock.calcIndicator(metricsMock)).thenReturn(nuevoIndicador); + } catch (NotAvailableMetricException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } // Seteamos alguna estrategia repositoryCalculator.setIndicator("indicator1", indicatorStrategyMock); - // Llamamos a calIndicator try { repositoryCalculator.calcIndicator("indicator1", reportManagerMock); @@ -111,9 +128,8 @@ void testCalIndicator() { e.printStackTrace(); } + // Verificamos que cada uno de los m�todos hayan sido llamados - // Verificamos que cada uno de los métodos hayan sido llamados - Mockito.verify(reportManagerMock).getReport(); Mockito.verify(report).getAllMetrics(); Mockito.verify(indicatorStrategyMock).requiredMetrics(); try { @@ -122,19 +138,85 @@ void testCalIndicator() { e.printStackTrace(); } } - - + /** - * Test para el metodo calcIndicator de RepositoryCalculator - * Verifica que no llama a calIndiicator si no se le pasa la métrica adecuada + * Test para el metodo calcIndicator de RepositoryCalculator Verifica que no + * llama a calIndiicator si no se le pasa la m�trica adecuada + * {@link us.muit.fs.a4i.control.RepositoryCalculator.calcIndicator(String, + * ReportManagerI)}. + * + * @throws NotAvailableMetricException + * @throws ReportItemException + */ + @Test + @Tag("unit") + @DisplayName("Prueba calcIndicator de RepositoryCalculator con metricas incorrectas y usando mocks") + void unitTestCalIndicatorNotRequiredMetrics() throws NotAvailableMetricException, ReportItemException { + // prueba la calculadora usando mocks, para el caso de que las métricas necesarias no estén disponibles + + // Creamos la clase a probar + RepositoryCalculator repositoryCalculator = new RepositoryCalculator(); + + // Creamos mocks necesarios + ReportManagerI reportManagerMock = Mockito.mock(ReportManagerI.class); + ReportI report = Mockito.mock(ReportI.class); + + List metricsMock = new ArrayList<>(); + ReportItemI metric1 = Mockito.mock(ReportItemI.class); + // Faltaba: configurar el método getName de las métricas + Mockito.when(metric1.getName()).thenReturn("metric1"); + ReportItemI metric2 = Mockito.mock(ReportItemI.class); + Mockito.when(metric2.getName()).thenReturn("metric2"); + + metricsMock.add(metric1); + metricsMock.add(metric2); + + IndicatorStrategy indicatorStrategyMock = Mockito.mock(IndicatorStrategy.class); + + Mockito.when(reportManagerMock.getReport()).thenReturn(report); + + // Metrics + Mockito.when(report.getAllMetrics()).thenReturn(metricsMock); + + // Required metrics para el indicador forzando a que coincidan con las m�tricas + // creadas + List requiredMetrics = new ArrayList<>(); + requiredMetrics.add("otra1"); + requiredMetrics.add("otra2"); + ReportItemI nuevoIndicador = Mockito.mock(ReportItemI.class); + Mockito.when(nuevoIndicador.getName()).thenReturn("nuevoIndicador"); + Mockito.when(indicatorStrategyMock.requiredMetrics()).thenReturn(requiredMetrics); + + // Seteamos alguna estrategia + repositoryCalculator.setIndicator("indicator1", indicatorStrategyMock); + + // Llamamos a calIndicator + try { + repositoryCalculator.calcIndicator("indicator1", reportManagerMock); + } catch (IndicatorException e) { + e.printStackTrace(); + } + + // Verificamos que no se ha llamado al m�todo + Mockito.verify(indicatorStrategyMock, times(0)).calcIndicator(metricsMock); + } + + /** + * Test para el metodo calcIndicator de RepositoryCalculator Verifica que no + * llama a calIndiicator si no se le pasa la m�trica adecuada * {@link us.muit.fs.a4i.control.RepositoryCalculator.calcIndicator(String, - * ReportManagerI)}. - * @throws NotAvailableMetricException - * @throws ReportItemException */ + * ReportManagerI)}. + * + * @throws NotAvailableMetricException + * @throws ReportItemException + */ @Test @Tag("integration") @DisplayName("Prueba calcIndicator de RepositoryCalculator metricas incorrectas") - void testCalIndicatorNotRequiredMetrics() throws NotAvailableMetricException, ReportItemException{ + void testCalIndicatorNotRequiredMetrics() throws NotAvailableMetricException, ReportItemException { + // Si no queremos depender de los objetos reales habría que eliminar las + // dependencias, pero en este caso se ha etiquetado como prueba + // de integración // Creamos la clase a probar RepositoryCalculator repositoryCalculator = new RepositoryCalculator(); @@ -142,15 +224,15 @@ void testCalIndicatorNotRequiredMetrics() throws NotAvailableMetricException, Re // Creamos mocks necesarios ReportManagerI reportManagerMock = Mockito.mock(ReportManagerI.class); ReportI report = Mockito.mock(ReportI.class); - + List metricsMock = new ArrayList<>(); - + ReportItemBuilder mb1 = new ReportItem.ReportItemBuilder("issues", 2); ReportItemBuilder mb2 = new ReportItem.ReportItemBuilder("closedIssues", 1.0); - + metricsMock.add(mb1.build()); metricsMock.add(mb2.build()); - + IndicatorStrategy indicatorStrategyMock = Mockito.mock(IndicatorStrategy.class); Mockito.when(reportManagerMock.getReport()).thenReturn(report); @@ -158,11 +240,10 @@ void testCalIndicatorNotRequiredMetrics() throws NotAvailableMetricException, Re // Metrics Mockito.when(report.getAllMetrics()).thenReturn(metricsMock); - // Seteamos alguna estrategia - repositoryCalculator.setIndicator("issuesRatio", new IssuesRatioIndicator()); - - // Verificamos que no se ha llamado al método + repositoryCalculator.setIndicator("issuesRatio", new IssuesRatioIndicatorStrategy()); + + // Verificamos que no se ha llamado al m�todo Mockito.verify(indicatorStrategyMock, times(0)).calcIndicator(metricsMock); } From 4cc3b701fbc5ed426e53ba6f65efd964cd7a37fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Fri, 1 Mar 2024 14:37:46 +0100 Subject: [PATCH 058/100] primeros cambios relacionados con Font --- gradle/wrapper/gradle-wrapper.properties | 2 +- .../us/muit/fs/a4i/model/entities/Font.java | 90 +++++++++++++++++-- .../fs/a4i/persistence/ReportFormater.java | 2 +- .../fs/a4i/persistence/ReportFormaterI.java | 3 +- src/main/resources/a4i.conf | 8 +- .../muit/fs/a4i/test/config/ContextTest.java | 25 +++--- .../fs/a4i/test/model/entities/testFont.java | 58 ++++++++++++ 7 files changed, 161 insertions(+), 27 deletions(-) create mode 100644 src/test/java/us/muit/fs/a4i/test/model/entities/testFont.java diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e6e5897..fae08049 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/us/muit/fs/a4i/model/entities/Font.java b/src/main/java/us/muit/fs/a4i/model/entities/Font.java index a2171045..9c6dd17c 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/Font.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/Font.java @@ -1,15 +1,87 @@ package us.muit.fs.a4i.model.entities; +import java.awt.Color; -public class Font extends java.awt.Font{ +public class Font { - private String color; + private Color color; + private java.awt.Font font; + /** + * Todos los valores por defecto + */ + public Font() { + this.color = Color.black; + this.font = new java.awt.Font ("Serif", java.awt.Font.PLAIN , 10); + } + /** + * Personalizando el color + * @param color color + */ + public Font (String color) { + this.font = new java.awt.Font ("Serif", java.awt.Font.PLAIN , 10); + switch(color){ + case "red","rojo": + this.color=Color.red; + break; + case "green","verde": + this.color=Color.green; + break; + case "blue","azul": + this.color=Color.blue; + break; + default: + this.color=Color.black; + } + } + /** + * Todos los Valores personalizados salvo el estilo + * @param family tipo de letra + * @param size tamaño + * @param color color + */ + public Font (String family, int size, String color) { + this.font=new java.awt.Font (family, java.awt.Font.PLAIN , size); + switch(color){ + case "red","rojo": + this.color=Color.red; + break; + case "green","verde": + this.color=Color.green; + break; + case "blue","azul": + this.color=Color.blue; + break; + default: + this.color=Color.black; + } + } + /** + * Todos los Valores personalizados + * @param family tipo de letra + * @param format formato + * @param size tamaño + * @param color color + */ + public Font (String family, int style, int size, String color) { + this.font=new java.awt.Font (family, style , size); + switch(color){ + case "red","rojo": + this.color=Color.red; + break; + case "green","verde": + this.color=Color.green; + break; + case "blue","azul": + this.color=Color.blue; + break; + default: + this.color=Color.black; + } + } - public Font(String name, int size, String color){ - super(name, PLAIN, size); - this.color = color; + public Color getColor() { + return color; } - - public String getColor(){ - return this.color; + public java.awt.Font getFont() { + return font; } -} +} \ No newline at end of file diff --git a/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java b/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java index 23160dee..7225b4a4 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ReportFormater.java @@ -3,11 +3,11 @@ */ package us.muit.fs.a4i.persistence; -import java.awt.Font; import java.io.IOException; import java.util.HashMap; import us.muit.fs.a4i.config.Context; +import us.muit.fs.a4i.model.entities.Font; import us.muit.fs.a4i.model.entities.IndicatorI; /** diff --git a/src/main/java/us/muit/fs/a4i/persistence/ReportFormaterI.java b/src/main/java/us/muit/fs/a4i/persistence/ReportFormaterI.java index bac58d16..872ee40e 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ReportFormaterI.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ReportFormaterI.java @@ -3,9 +3,10 @@ */ package us.muit.fs.a4i.persistence; -import java.awt.Font; + import java.io.IOException; +import us.muit.fs.a4i.model.entities.Font; import us.muit.fs.a4i.model.entities.IndicatorI; /** diff --git a/src/main/resources/a4i.conf b/src/main/resources/a4i.conf index 1145a6ed..a7f51276 100644 --- a/src/main/resources/a4i.conf +++ b/src/main/resources/a4i.conf @@ -1,17 +1,17 @@ -#Fichero de configuraci�n por defecto de la api +#Fichero de configuracion por defecto de la api #Tipo de persistencia que se quiere usar persistence.type=EXCEL #Tipo de remoto que se quiere usar remote.type=GITHUB #Caracteristicas por defecto de la fuente, cuando los informes son visuales -Font.default.color=Black +Font.default.color=black Font.default.height=12 Font.default.type=Arial #Caracteristicas de fuente de las métricas -Font.metric.color=Green +Font.metric.color=green Font.metric.height=15 Font.metric.type=Serif #Caracteristicas de la fuente para el indicador Critical -Font.CRITICAL.color=Blue +Font.CRITICAL.color=red Font.CRITICAL.height=20 Font.CRITICAL.type=Verdana \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java index 661c304d..57448f27 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java @@ -11,6 +11,8 @@ import static org.junit.jupiter.api.Assertions.fail; import us.muit.fs.a4i.model.entities.Font; // Clase que sustituye a java.awt.Font + +import java.awt.Color; import java.awt.GraphicsEnvironment; import java.io.File; import java.io.IOException; @@ -225,9 +227,9 @@ void testGetDefaultFont() { assertNotNull(font, "No se ha inicializado bien la fuente"); //Al ser Context un singleton una vez creada la instancia no se puede eliminar "desde fuera" //De manera que el valor de la fuente depende del orden en el que se ejecuten los test, y para que el test sea independiente de eso la verificación comprueba los dos posibles valores - assertTrue("Black".equals(font.getColor()) || "Red".equals(font.getColor()),"No es el color de fuente especificado en el fichero de propiedades"); - assertTrue(12 == font.getSize() || 16 == font.getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); - assertTrue("Arial".equals(font.getName()) || "Times".equals(font.getName()),"No es el tipo de fuente especificado en el fichero de propiedades"); + assertEquals(Color.BLACK.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); + assertEquals(12,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + assertEquals("Arial",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); } catch (IOException e) { fail("No debería devolver esta excepción"); @@ -245,12 +247,13 @@ void testGetMetricFont(){ try { //fail("Not yet implemented"); Font font = null; - String color; // No entiendo cómo usarlo, ¿para darle valor luego?// + font = Context.getContext().getMetricFont(); assertNotNull(font, "No se ha inicializado bien la fuente"); - assertTrue("Green".equals(font.getColor()),"No es el color de fuente especificado en el fichero de propiedades"); - assertTrue(15 == font.getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); - assertTrue("Serif".equals(font.getName()),"No es el tipo de fuente especificado en el fichero de propiedades"); + assertEquals(Color.GREEN.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); + assertTrue(15 == font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + assertEquals(java.awt.Font.SERIF,font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); + }catch (IOException e) { fail("No debería devolver esta excepción"); @@ -272,16 +275,16 @@ void testGetIndicatorFont() { font = Context.getContext().getIndicatorFont(IndicatorState.UNDEFINED); assertNotNull(font, "No se ha inicializado bien la fuente"); // El nombre o tipo de la fuente podrá ser Arial o Times según el momento en el que se realicen los tests. - assertTrue("Arial".equals(font.getName()) || "Times".equals(font.getName()), + assertTrue("Arial".equals(font.getFont().getFamily()) || "Times".equals(font.getFont().getFamily()), "No es el tipo de fuente especificado en el fichero de propiedades"); // Se le solicita al contexto la fuente del estao "CRITICAL", cuyas propiedades están definidas en el // fichero de configuración por defecto. font = Context.getContext().getIndicatorFont(IndicatorState.CRITICAL); assertNotNull(font, "No se ha inicializado bien la fuente"); - assertTrue("Verdana".equals(font.getName()),"No es el tipo de fuente especificado en el fichero de propiedades"); - assertTrue("Blue".equals(font.getColor()),"No es el color de fuente especificado en el fichero de propiedades"); - assertTrue(20 == font.getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + assertTrue("Verdana".equals(font.getFont().getFamily()),"No es el tipo de fuente especificado en el fichero de propiedades"); + assertTrue("RED".equals(font.getColor()),"No es el color de fuente especificado en el fichero de propiedades"); + assertTrue(20 == font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); } catch (IOException e) { fail("No debería devolver esta excepción"); diff --git a/src/test/java/us/muit/fs/a4i/test/model/entities/testFont.java b/src/test/java/us/muit/fs/a4i/test/model/entities/testFont.java new file mode 100644 index 00000000..91a6dfed --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/model/entities/testFont.java @@ -0,0 +1,58 @@ +/** + * + */ +package us.muit.fs.a4i.test.model.entities; + +import static org.junit.jupiter.api.Assertions.*; + +import java.awt.Color; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.model.entities.Font; + +/** + * + */ +class testFont { + + /** + * @throws java.lang.Exception + */ + @BeforeAll + static void setUpBeforeClass() throws Exception { + } + + /** + * Test method for {@link us.muit.fs.a4i.model.entities.Font#Font(java.lang.String, int, java.lang.String)}. + */ + @Test + void testFontStringIntString() { + fail("Not yet implemented"); + } + + /** + * Test method for {@link us.muit.fs.a4i.model.entities.Font#getColor()}. + */ + @Test + void testConstructor() { + Font underTest=new Font(java.awt.Font.SANS_SERIF,java.awt.Font.BOLD,12,"azul"); + assertEquals(Color.BLUE.toString(),underTest.getColor().toString(),"No se ha establecido bien el color"); + assertEquals(java.awt.Font.SANS_SERIF,underTest.getFont().getFamily(),"No se ha puesto bien el tipo de fuente"); + assertEquals(java.awt.Font.BOLD, underTest.getFont().getStyle(),"El estilo no se ha puesto bien"); + underTest=new Font("Verdana",12,"azul"); + assertEquals(Color.BLUE.toString(),underTest.getColor().toString(),"No se ha establecido bien el color"); + assertEquals("Verdana",underTest.getFont().getFamily(),"No se ha puesto bien el tipo de fuente"); + assertEquals(java.awt.Font.PLAIN, underTest.getFont().getStyle(),"El estilo no se ha puesto bien"); + underTest=new Font("azul"); + assertEquals(Color.BLUE.toString(),underTest.getColor().toString(),"No se ha establecido bien el color"); + assertEquals(java.awt.Font.SERIF,underTest.getFont().getFamily(),"No se ha puesto bien el tipo de fuente"); + assertEquals(java.awt.Font.PLAIN, underTest.getFont().getStyle(),"El estilo no se ha puesto bien"); + underTest=new Font(); + assertEquals(Color.BLACK.toString(),underTest.getColor().toString(),"No se ha establecido bien el color"); + assertEquals(java.awt.Font.SERIF,underTest.getFont().getFamily(),"No se ha puesto bien el tipo de fuente"); + assertEquals(java.awt.Font.PLAIN, underTest.getFont().getStyle(),"El estilo no se ha puesto bien"); + } + +} From a2aa305f906e05389d2bdfdb4d61a2d01c7c0cfe Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Mon, 4 Mar 2024 09:53:13 +0100 Subject: [PATCH 059/100] Terminado el issue 86 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementar el método getIndicatorFont de la clase Context, que devuelve la configuración de la fuente para el estado de indicador que se pase como parámetro. --- .../java/us/muit/fs/a4i/config/Context.java | 32 +- .../us/muit/fs/a4i/model/entities/Font.java | 48 ++- src/main/resources/a4i.conf | 27 +- .../muit/fs/a4i/test/config/ContextTest.java | 99 +----- .../muit/fs/a4i/test/config/ContextTest2.java | 299 ++++++++++++++++++ src/test/resources/appTest.conf | 21 +- 6 files changed, 397 insertions(+), 129 deletions(-) create mode 100644 src/test/java/us/muit/fs/a4i/test/config/ContextTest2.java diff --git a/src/main/java/us/muit/fs/a4i/config/Context.java b/src/main/java/us/muit/fs/a4i/config/Context.java index a3462f82..95ae9c16 100644 --- a/src/main/java/us/muit/fs/a4i/config/Context.java +++ b/src/main/java/us/muit/fs/a4i/config/Context.java @@ -3,8 +3,6 @@ */ package us.muit.fs.a4i.config; -/**import java.awt.Font;**/ -import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -67,7 +65,7 @@ public class Context { /** * Fichero de propiedades de la API establecido por la aplicación cliente */ - private static String appConFile = null; + private static String appConfFile = null; /** * Fichero de especificación de métricas e indicadores establecido por la @@ -161,7 +159,7 @@ public static void setAppConf(String appConPath) throws IOException { /** * Vuelve a leer las propiedades incluyendo las establecidas por la aplicación */ - appConFile = appConPath; + appConfFile = appConPath; // customFile=System.getenv("APP_HOME")+customFile; // Otra opción, Usar una variable de entorno para localizar la ruta de @@ -170,6 +168,10 @@ public static void setAppConf(String appConPath) throws IOException { getContext().properties.load(new FileInputStream(appConPath)); log.info("Las nuevas propiedades son " + getContext().properties); } + + public static String getAppConf() throws IOException { + return appConfFile; + } /** * @return devuelve el verificador (checker) @@ -267,25 +269,20 @@ public Font getMetricFont() { log.info("El color de la fuente de las metricas es el valor por defecto"); } - log.info("Los datos son, color: " + color + " height: " + height + " type: " + type); - - log.info("Intento crear la fuente"); + log.info("Llamo a newFont con los datos color: " + color + " height: " + height + " type: " + type); return new Font(type, Integer.valueOf(height), color); } /** - *

- * No Implementado - *

*

* Deberá leer las propiedades adecuadas, como color, tamaño, tipo... y * construir un objeto Font *

* * @param state Estado para el que se solicita el color de fuente - * @return La fuente para el indicador cuando el estado es el par�metro pasado + * @return La fuente para el indicador cuando el estado es el parametro pasado * @throws IOException problema al leer el fichero */ @@ -295,11 +292,12 @@ public Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOException * **/ Font font = null; // TODO: - String propiedad = "Font." + state.toString(); + String propertyState = "Font." + state.toString(); + log.info("Raiz que uso para buscar los datos del indicador en estado "+state+" "+propertyState); - String color = properties.getProperty(propiedad + ".color"); - String height = properties.getProperty(propiedad + ".height"); - String type = properties.getProperty(propiedad + ".type"); + String color = properties.getProperty(propertyState + ".color"); + String height = properties.getProperty(propertyState + ".height"); + String type = properties.getProperty(propertyState + ".type"); log.info("Los datos son, color: " + color + " height: " + height + " type: " + type); log.info("Intento crear la fuente"); @@ -309,9 +307,7 @@ public Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOException } if (height == null) { height = properties.getProperty("Font.default.height"); - } - if (type == null){ - } + } if(type == null){ type = properties.getProperty("Font.default.type"); } diff --git a/src/main/java/us/muit/fs/a4i/model/entities/Font.java b/src/main/java/us/muit/fs/a4i/model/entities/Font.java index 9c6dd17c..e31f851f 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/Font.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/Font.java @@ -1,8 +1,11 @@ package us.muit.fs.a4i.model.entities; import java.awt.Color; +import java.util.logging.Logger; + +import us.muit.fs.a4i.config.Context; public class Font { - + private static Logger log = Logger.getLogger(Font.class.getName()); private Color color; private java.awt.Font font; /** @@ -18,7 +21,7 @@ public Font() { */ public Font (String color) { this.font = new java.awt.Font ("Serif", java.awt.Font.PLAIN , 10); - switch(color){ + switch(color.toLowerCase()){ case "red","rojo": this.color=Color.red; break; @@ -28,6 +31,9 @@ public Font (String color) { case "blue","azul": this.color=Color.blue; break; + case "orange","naranja": + this.color=Color.ORANGE; + break; default: this.color=Color.black; } @@ -39,20 +45,25 @@ public Font (String color) { * @param color color */ public Font (String family, int size, String color) { - this.font=new java.awt.Font (family, java.awt.Font.PLAIN , size); - switch(color){ + log.info("Creando fuente con valores familia "+family+" tamano "+size+" color "+color); + this.font=new java.awt.Font (family.toLowerCase(), java.awt.Font.PLAIN , size); + switch(color.toLowerCase()){ case "red","rojo": - this.color=Color.red; + this.color=Color.RED; break; case "green","verde": - this.color=Color.green; + this.color=Color.GREEN; break; case "blue","azul": - this.color=Color.blue; + this.color=Color.BLUE; + break; + case "orange","naranja": + this.color=Color.ORANGE; break; default: - this.color=Color.black; - } + this.color=Color.BLACK; + } + log.info("Se ha creado fuente con el tipo "+font.getFamily()+" y el color "+color); } /** * Todos los Valores personalizados @@ -62,26 +73,29 @@ public Font (String family, int size, String color) { * @param color color */ public Font (String family, int style, int size, String color) { - this.font=new java.awt.Font (family, style , size); - switch(color){ + this.font=new java.awt.Font (family.toLowerCase(), style , size); + switch(color.toLowerCase()){ case "red","rojo": - this.color=Color.red; + this.color=Color.RED; break; case "green","verde": - this.color=Color.green; + this.color=Color.GREEN; break; case "blue","azul": - this.color=Color.blue; + this.color=Color.BLUE; + break; + case "orange","naranja": + this.color=Color.ORANGE; break; default: - this.color=Color.black; + this.color=Color.BLACK; } } public Color getColor() { - return color; + return this.color; } public java.awt.Font getFont() { - return font; + return this.font; } } \ No newline at end of file diff --git a/src/main/resources/a4i.conf b/src/main/resources/a4i.conf index a7f51276..0e40c255 100644 --- a/src/main/resources/a4i.conf +++ b/src/main/resources/a4i.conf @@ -3,15 +3,26 @@ persistence.type=EXCEL #Tipo de remoto que se quiere usar remote.type=GITHUB -#Caracteristicas por defecto de la fuente, cuando los informes son visuales -Font.default.color=black -Font.default.height=12 -Font.default.type=Arial + #Caracteristicas de fuente de las métricas Font.metric.color=green Font.metric.height=15 Font.metric.type=Serif -#Caracteristicas de la fuente para el indicador Critical -Font.CRITICAL.color=red -Font.CRITICAL.height=20 -Font.CRITICAL.type=Verdana \ No newline at end of file + +#Caracteristicas para la fuente de indicadores según estados del mismo +Font.OK.color=Black +Font.OK.height=12 +Font.OK.type=Serif + +Font.WARNING.color=Orange +Font.WARNING.height=12 +Font.WARNING.type=Serif + +Font.CRITICAL.color=Red +Font.CRITICAL.height=16 +Font.CRITICAL.type=Serif + +#Caracteristicas por defecto de la fuente, cuando los informes son visuales +Font.default.color=black +Font.default.height=12 +Font.default.type=Arial \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java index 57448f27..f198d851 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java @@ -1,5 +1,5 @@ /** - * + * Tests para ContextTest sin modificar los ficheros de ocnfiguraicón pro defecto en ningún momento */ package us.muit.fs.a4i.test.config; @@ -10,42 +10,24 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import us.muit.fs.a4i.model.entities.Font; // Clase que sustituye a java.awt.Font - import java.awt.Color; -import java.awt.GraphicsEnvironment; import java.io.File; import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; import java.util.logging.Logger; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import us.muit.fs.a4i.config.Context; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; -import java.util.Set; -import java.util.logging.Logger; -import us.muit.fs.a4i.model.entities.IndicatorI; import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; +import us.muit.fs.a4i.model.entities.Font; // Clase que sustituye a java.awt.Font + + -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; -import java.util.Set; -import java.util.logging.Logger; /** * @author Isabel Román @@ -112,61 +94,6 @@ void testGetContext() { } } - - /** - * Test method for - * {@link us.muit.fs.a4i.config.Context#setAppRI(java.lang.String)}. - */ - @Test - @Tag("Integracion") - void testSetAppRI() { - /** - * Este test excede los límites, ya que no sólo verifica que se establece bien - * la ruta del fichero de especificación de métricas e indicadores sino que se - * leen bien los valores del mismo Sería un test de integración porque se - * requiere que estén ya desarrollados otras clases, a parte de Context - */ - try { - Context.setAppRI(appConfPath); - HashMap metricInfo = Context.getContext().getChecker().getMetricConfiguration() - .getMetricInfo("downloads"); - assertNotNull(metricInfo, "No se han leído los atributos de la métrica"); - assertEquals("downloads", metricInfo.get("name"), "El nombre no es el correcto"); - assertEquals("java.lang.Integer", metricInfo.get("type"), "El tipo no es el correcto"); - assertEquals("Descargas realizadas", metricInfo.get("description"), "La descripción no es el correcta"); - assertEquals("downloads", metricInfo.get("unit"), "Las uniddes no son correctas"); - - } catch (IOException e) { - fail("No se encuentra el fichero de especificación de métricas e indicadores"); - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - /** - * Test method for - * {@link us.muit.fs.a4i.config.Context#setAppConf(java.lang.String)}. - */ - @Test - @Tag("Integracion") - void testSetAppConf() { - /** - * Este test excede los límites, ya que no sólo verifica que se establece bien - * la ruta del fichero de configuración sino que se leen bien los valores del - * mismo Sería un test de integración porque se requiere que estén ya - * desarrollados otras clases, a parte de Context - */ - try { - - Context.setAppConf(appPath); - - } catch (IOException e) { - fail("No se encuentra el fichero de especificación de métricas e indicadores"); - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - /** * Test method for {@link us.muit.fs.a4i.config.Context#getChecker()}. */ @@ -270,21 +197,27 @@ void testGetIndicatorFont() { try { Font font = null; - // Se le solicita la fuente del estado indefinido, que tendrá los valores por defecto al no estar + // Se le solicita la fuente del estado indefinido, que tendrá los valores por defecto Font.Default.xxx al no estar // definidas sus propiedades en el fichero de configuración utilizados en los tests. font = Context.getContext().getIndicatorFont(IndicatorState.UNDEFINED); assertNotNull(font, "No se ha inicializado bien la fuente"); // El nombre o tipo de la fuente podrá ser Arial o Times según el momento en el que se realicen los tests. - assertTrue("Arial".equals(font.getFont().getFamily()) || "Times".equals(font.getFont().getFamily()), - "No es el tipo de fuente especificado en el fichero de propiedades"); + assertEquals("Arial",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); + assertEquals(Color.BLACK,font.getColor(),"No es el color de fuente especificado en el fichero de propiedades"); + assertEquals(12,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); // Se le solicita al contexto la fuente del estao "CRITICAL", cuyas propiedades están definidas en el // fichero de configuración por defecto. font = Context.getContext().getIndicatorFont(IndicatorState.CRITICAL); + assertNotNull(font, "No se ha inicializado bien la fuente"); - assertTrue("Verdana".equals(font.getFont().getFamily()),"No es el tipo de fuente especificado en el fichero de propiedades"); - assertTrue("RED".equals(font.getColor()),"No es el color de fuente especificado en el fichero de propiedades"); - assertTrue(20 == font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + + + log.info("Datos fuente estado CRITICAL familia "+font.getFont().getFamily()+" tamano "+font.getFont().getSize()+" color "+font.getColor()); + + assertTrue(font.getFont().getFamily().equals("Serif"),"No es el tipo de fuente especificado en el fichero de propiedades"); + assertEquals(Color.RED,font.getColor(),"No es el color de fuente especificado en el fichero de propiedades"); + assertEquals(16,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); } catch (IOException e) { fail("No debería devolver esta excepción"); diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest2.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest2.java new file mode 100644 index 00000000..df5936d9 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest2.java @@ -0,0 +1,299 @@ +/** + * Separo los tests de Context en dos, para evitar el problema con el singleton: + * Si se modifican los ficheros de configuración los resultados son distintos, porque el orden de ejecución de los tests es aleatorio + */ +package us.muit.fs.a4i.test.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import us.muit.fs.a4i.model.entities.Font; // Clase que sustituye a java.awt.Font + +import java.awt.Color; +import java.awt.GraphicsEnvironment; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.logging.Logger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.config.Context; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.Set; +import java.util.logging.Logger; + +import us.muit.fs.a4i.model.entities.IndicatorI; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.Set; +import java.util.logging.Logger; + +/** + * @author Isabel Román + * + */ +class ContextTest2 { + private static Logger log = Logger.getLogger(CheckerTest.class.getName()); + /** + * Ruta al fichero de configuración de indicadores y métricas establecidos por + * la aplicación + */ + static String appConfPath; + /** + * Ruta al fichero de configuración de propiedades de funcionamiento + * establecidos por la aplicación + */ + static String appPath; + + /** + * @throws java.lang.Exception + */ + @BeforeAll + static void setUpBeforeClass() throws Exception { + appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + + "appConfTest.json"; + appPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appTest.conf"; + } + + /** + * @throws java.lang.Exception + */ + @AfterAll + static void tearDownAfterClass() throws Exception { + } + + /** + * @throws java.lang.Exception + */ + @BeforeEach + void setUp() throws Exception { + } + + /** + * @throws java.lang.Exception + */ + @AfterEach + void tearDown() throws Exception { + //Ejecutar tras cada test + } + + /** + * Test method for {@link us.muit.fs.a4i.config.Context#getContext()}. + */ + @Test + void testGetContext() { + try { + assertNotNull(Context.getContext(), "Devuelve null"); + assertTrue(Context.getContext() instanceof us.muit.fs.a4i.config.Context, "No es del tipo apropiado"); + assertSame(Context.getContext(), Context.getContext(), "No se devuelve el mismo contexto siempre"); + + } catch (IOException e) { + fail("No debería lanzar esta excepción"); + e.printStackTrace(); + } + + } + + /** + * Test method for + * {@link us.muit.fs.a4i.config.Context#setAppRI(java.lang.String)}. + */ + @Test + @Tag("Integracion") + void testSetAppRI() { + /** + * Este test excede los límites, ya que no sólo verifica que se establece bien + * la ruta del fichero de especificación de métricas e indicadores sino que se + * leen bien los valores del mismo Sería un test de integración porque se + * requiere que estén ya desarrollados otras clases, a parte de Context + */ + try { + Context.setAppRI(appConfPath); + HashMap metricInfo = Context.getContext().getChecker().getMetricConfiguration() + .getMetricInfo("downloads"); + assertNotNull(metricInfo, "No se han leído los atributos de la métrica"); + assertEquals("downloads", metricInfo.get("name"), "El nombre no es el correcto"); + assertEquals("java.lang.Integer", metricInfo.get("type"), "El tipo no es el correcto"); + assertEquals("Descargas realizadas", metricInfo.get("description"), "La descripción no es el correcta"); + assertEquals("downloads", metricInfo.get("unit"), "Las uniddes no son correctas"); + + } catch (IOException e) { + fail("No se encuentra el fichero de especificación de métricas e indicadores"); + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * Test method for + * {@link us.muit.fs.a4i.config.Context#setAppConf(java.lang.String)}. + */ + @Test + @Tag("Integracion") + void testSetAppConf() { + + try { + + Context.setAppConf(appPath); + assertTrue(appPath.equals(Context.getAppConf()),"No coincide la ruta del fichero de métricas con la configurada"); + + } catch (IOException e) { + fail("No se encuentra el fichero de especificación de métricas e indicadores"); + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * Test method for {@link us.muit.fs.a4i.config.Context#getChecker()}. + */ + @Test + void testGetChecker() { + try { + assertNotNull(Context.getContext().getChecker(), "No devuelve el checker"); + assertTrue(Context.getContext().getChecker() instanceof us.muit.fs.a4i.config.Checker, + "No es del tipo apropiado"); + + } catch (IOException e) { + fail("No debería devolver esta excepción"); + e.printStackTrace(); + } + } + + /** + * Test method for {@link us.muit.fs.a4i.config.Context#getPersistenceType()}. + */ + @Test + void testGetPersistenceType() { + try { + assertEquals("EXCEL", Context.getContext().getPersistenceType(), + "En el fichero de configuración por defecto, a4i.conf, está definido el tipo excel"); + } catch (IOException e) { + fail("El fichero no se localiza"); + e.printStackTrace(); + } + } + + /** + * Test method for {@link us.muit.fs.a4i.config.Context#getRemoteType()}. + */ + @Test + void testGetRemoteType() { + try { + assertEquals("GITHUB", Context.getContext().getRemoteType(), + "En el fichero de configuración por defecto, a4i.conf, está el tipo github"); + } catch (IOException e) { + fail("El fichero no se localiza"); + e.printStackTrace(); + } + } + + /** + *

Este test permite verificar que se lee bien la fuente, además es independiente del orden de ejecución del resto de test. La complejidad de la verifiación está impuesta por estar probando un singleton

+ * Test method for {@link us.muit.fs.a4i.config.Context#getDefaultFont()}. + */ + @Test + void testGetDefaultFont() { + try { + Font font = null; + String color; // No entiendo cómo usarlo, ¿para darle valor luego?// + // Uso esto para ver los tipos de fuentes de los que dispongo + //String[] fontNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); + //log.info("listado de fuentes " + Arrays.toString(fontNames)); + font = Context.getContext().getDefaultFont(); + assertNotNull(font, "No se ha inicializado bien la fuente"); + //Al ser Context un singleton una vez creada la instancia no se puede eliminar "desde fuera" + //De manera que el valor de la fuente depende del orden en el que se ejecuten los test, y para que el test sea independiente de eso la verificación comprueba los dos posibles valores + assertEquals(Color.BLACK.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); + assertEquals(12,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + assertEquals("Arial",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); + + } catch (IOException e) { + fail("No debería devolver esta excepción"); + e.printStackTrace(); + } + } + + /** + * Test method for {@link us.muit.fs.a4i.config.Context#getMetricFont()}. + * @throws IOException + */ + @Test + + void testGetMetricFont(){ + try { + //fail("Not yet implemented"); + Font font = null; + + font = Context.getContext().getMetricFont(); + assertNotNull(font, "No se ha inicializado bien la fuente"); + assertEquals(Color.GREEN.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); + assertTrue(15 == font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + assertEquals(java.awt.Font.SERIF,font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); + + + }catch (IOException e) { + fail("No debería devolver esta excepción"); + e.printStackTrace(); + } + } + + /** + * Test method for + * {@link us.muit.fs.a4i.config.Context#getIndicatorFont(us.muit.fs.a4i.model.entities.Indicator.State)}. + */ + @Test + void testGetIndicatorFont() { + try { + Font font = null; + + // Se le solicita la fuente del estado indefinido, que tendrá los valores por defecto al no estar + // definidas sus propiedades en el fichero de configuración utilizados en los tests. + font = Context.getContext().getIndicatorFont(IndicatorState.UNDEFINED); + assertNotNull(font, "No se ha inicializado bien la fuente"); + // El nombre o tipo de la fuente podrá ser Arial o Times según el momento en el que se realicen los tests. + assertEquals("Arial",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); + + // Se le solicita al contexto la fuente del estao "CRITICAL", cuyas propiedades están definidas en el + // fichero de configuración por defecto. + font = Context.getContext().getIndicatorFont(IndicatorState.CRITICAL); + assertNotNull(font, "No se ha inicializado bien la fuente"); + assertEquals("Times",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); + assertEquals("RED",font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); + assertEquals(16,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + + } catch (IOException e) { + fail("No debería devolver esta excepción"); + e.printStackTrace(); + } + } + + /** + * Test method for {@link us.muit.fs.a4i.config.Context#getPropertiesNames()}. + */ + @Test + void testGetPropertiesNames() { + fail("Not yet implemented"); + } + +} diff --git a/src/test/resources/appTest.conf b/src/test/resources/appTest.conf index 27ca39d8..a3fc0337 100644 --- a/src/test/resources/appTest.conf +++ b/src/test/resources/appTest.conf @@ -1,6 +1,21 @@ #Fichero de configuración establecido por la aplicación + + +#Caracteristicas para la fuente de indicadores según estados del mismo +Font.OK.color=Black +Font.OK.height=12 +Font.OK.type=Times + +Font.WARNING.color=Orange +Font.WARNING.height=12 +Font.WARNING.type=Times + +Font.CRITICAL.color=Red +Font.CRITICAL.height=16 +Font.CRITICAL.type=Times + #Caracteristicas por defecto de la fuente, cuando los informes son visuales -Font.default.color=Red -Font.default.height=16 -Font.default.type=Times \ No newline at end of file +Font.default.color=Black +Font.default.height=12 +Font.default.type=Arial \ No newline at end of file From 316215cc61f188f72dbffd260e3d7d1ca5cccec3 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Mon, 4 Mar 2024 10:15:03 +0100 Subject: [PATCH 060/100] Terminado Issue 89 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementación del método getMetricFont de la clase Context y corregir getDefaultFont. --- .../java/us/muit/fs/a4i/config/Context.java | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/config/Context.java b/src/main/java/us/muit/fs/a4i/config/Context.java index 95ae9c16..e4cfaaf9 100644 --- a/src/main/java/us/muit/fs/a4i/config/Context.java +++ b/src/main/java/us/muit/fs/a4i/config/Context.java @@ -3,6 +3,7 @@ */ package us.muit.fs.a4i.config; +import java.awt.Color; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -209,7 +210,7 @@ public String getRemoteType() throws IOException { /** *

- * No Implementado Debería leer las propiedades adecuadas, como color, tamaño, + * Lee las propiedades adecuadas, como color, tamaño, * tipo... y construir un objeto Font Si no se ha establecido un valor por * defecto se crea una fuente simple *

@@ -223,24 +224,43 @@ public Font getDefaultFont() { // devuelva sea un String con el color log.info("Busca la información de configuración de la fuente, por defecto"); - // TO DO - String color = properties.getProperty("Font.default.color"); - String height = properties.getProperty("Font.default.height"); - String type = properties.getProperty("Font.default.type"); + + String color = getDefaultParam("color"); + String height = getDefaultParam("height"); + String type = getDefaultParam("type"); + log.info("Los datos son, color: " + color + " height: " + height + " type: " + type); log.info("Intento crear la fuente"); return new Font(type,Integer.valueOf(height),color); } + private String getDefaultParam(String param) { + String property="Font.default."+param; + String value=properties.getProperty(property); + /** + * Se asegura de que se da un valor por defecto, aunque no esté configurado en el fichero + */ + if (value==null) { + switch(param) { + case "type": + value="Arial"; + break; + case "color": + value="black"; + break; + case "height": + value="12"; + break; + } + } + return value; + } /** *

- * No Implementado - *

- *

- * Deberá leer las propiedades adecuadas, como color, tamaño, tipo... y - * construir un objeto Font + * Lee las propiedades adecuadas, como color, tamaño, tipo... y + * construye un objeto Font para la fuente de las métricas *

*

* Si no se ha definido una fuente para las métricas se debe devolver la fuente @@ -257,15 +277,15 @@ public Font getMetricFont() { String color = properties.getProperty("Font.metric.color"); if (type==null){ - type = properties.getProperty("Font.default.type"); + type = getDefaultParam("type"); log.info("El tipo de la fuente de las metricas es el valor por defecto"); } if (height==null){ - height = properties.getProperty("Font.default.height"); + height =getDefaultParam("height"); log.info("El tamaño de la fuente de las metricas es el valor por defecto"); } if (color==null){ - color = properties.getProperty("Font.default.color"); + color = getDefaultParam("color"); log.info("El color de la fuente de las metricas es el valor por defecto"); } @@ -278,7 +298,7 @@ public Font getMetricFont() { /** *

* Deberá leer las propiedades adecuadas, como color, tamaño, tipo... y - * construir un objeto Font + * construir un objeto Font para la fuente del indicador en dicho estado *

* * @param state Estado para el que se solicita el color de fuente From 54176a899621817273bfd77870799a1b80cccd6e Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Mon, 4 Mar 2024 15:07:17 +0100 Subject: [PATCH 061/100] problemas al guardar el estilo --- build.gradle | 4 +- .../us/muit/fs/a4i/model/entities/Font.java | 1 - .../muit/fs/a4i/model/entities/Indicator.java | 3 +- .../a4i/persistence/ExcelReportManager.java | 113 ++++++++++++++---- src/main/resources/a4i.conf | 2 +- .../persistence/ExcelReportManagerTest.java | 37 +++++- src/test/resources/excelTest.xlsx | Bin 0 -> 6196 bytes 7 files changed, 125 insertions(+), 35 deletions(-) create mode 100644 src/test/resources/excelTest.xlsx diff --git a/build.gradle b/build.gradle index a59dbb73..4c05fa7c 100644 --- a/build.gradle +++ b/build.gradle @@ -101,7 +101,9 @@ dependencies { //Para la persistencia de informes usaremos la api apachepoi // https://mvnrepository.com/artifact/org.apache.poi/poi //JAVADOC: https://poi.apache.org/apidocs/5.0/ - implementation 'org.apache.poi:poi:5.2.1' + // implementation 'org.apache.poi:poi:5.2.5' + // https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml + implementation group: 'org.apache.poi', name: 'poi-ooxml', version: '3.9' //Para leer la configuración como ficheros con datos en formato json // https://mvnrepository.com/artifact/javax.json/javax.json-api //JAVADOC: https://javadoc.io/doc/org.glassfish/javax.json/latest/overview-summary.html diff --git a/src/main/java/us/muit/fs/a4i/model/entities/Font.java b/src/main/java/us/muit/fs/a4i/model/entities/Font.java index e31f851f..9350d5c7 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/Font.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/Font.java @@ -2,7 +2,6 @@ import java.awt.Color; import java.util.logging.Logger; -import us.muit.fs.a4i.config.Context; public class Font { private static Logger log = Logger.getLogger(Font.class.getName()); diff --git a/src/main/java/us/muit/fs/a4i/model/entities/Indicator.java b/src/main/java/us/muit/fs/a4i/model/entities/Indicator.java index c8a62521..a1e10a9e 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/Indicator.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/Indicator.java @@ -4,13 +4,14 @@ package us.muit.fs.a4i.model.entities; import java.util.Collection; +import java.util.Date; import java.util.logging.Logger; /** * @author Isabel Román * */ -public class Indicator implements IndicatorI { +public class Indicator implements IndicatorI{ private static Logger log = Logger.getLogger(Indicator.class.getName()); private Collection metrics; private IndicatorState state; diff --git a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java index 2e22f392..0543b60b 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java @@ -13,16 +13,32 @@ import java.util.logging.Logger; import org.apache.poi.EncryptedDocumentException; -import org.apache.poi.hssf.usermodel.HSSFSheet; + +/** + * import org.apache.poi.hssf.usermodel.HSSFSheet; + import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.hssf.usermodel.HSSFFont; +import org.apache.poi.hssf.usermodel.HSSFWorkbookFactory; +import org.apache.poi.hssf.util.HSSFColor; +import org.apache.poi.ss.usermodel.CellStyle; + */ +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFFont; + +import org.apache.poi.xssf.usermodel.XSSFColor; + import org.apache.poi.ss.usermodel.CellStyle; -import org.apache.poi.ss.usermodel.Font; + + import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.ss.usermodel.WorkbookFactory; + import us.muit.fs.a4i.exceptions.ReportNotDefinedException; import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItemI; +import us.muit.fs.a4i.model.entities.Font; /** *

@@ -53,6 +69,7 @@ */ public class ExcelReportManager implements PersistenceManager, FileManager { private static Logger log = Logger.getLogger(ExcelReportManager.class.getName()); + private static int fontIndex=0; /** *

* Referencia al gestor de estilo que se va a utilizar @@ -75,8 +92,8 @@ public class ExcelReportManager implements PersistenceManager, FileManager { */ protected String fileName = ""; - protected HSSFWorkbook wb = null; - protected HSSFSheet sheet = null; + protected XSSFWorkbook wb = null; + protected XSSFSheet sheet = null; public ExcelReportManager(String filePath, String fileName) { super(); @@ -121,11 +138,14 @@ public void setName(String name) { * @throws IOException error al abrir el fichero * @throws EncryptedDocumentException documento protegido */ - protected HSSFSheet getCleanSheet(String entityId) throws EncryptedDocumentException, IOException { + protected XSSFSheet getCleanSheet(String entityId) throws EncryptedDocumentException, IOException { log.info("Solicita una hoja nueva del libro manejado, para la entidad con id: "+entityId); if (wb == null) { - inputStream = new FileInputStream(filePath + fileName + ".xls"); - wb = (HSSFWorkbook) WorkbookFactory.create(inputStream); + inputStream = new FileInputStream(filePath + fileName); + // wb = (HSSFWorkbook) HSSWorkbookFactory.create(inputStream); + //XSSFWorkbookFactory factory=new SSSFWorkbookFactory(); + + wb = new XSSFWorkbook(inputStream); log.info("Generado workbook"); } @@ -188,6 +208,7 @@ public void saveReport(ReportI report) throws ReportNotDefinedException { Collection collection = report.getAllMetrics(); for (ReportItemI metric : collection) { persistMetric(metric); + rowIndex++; } //Ahora irían los indicadores rowIndex++; @@ -195,9 +216,10 @@ public void saveReport(ReportI report) throws ReportNotDefinedException { collection = report.getAllIndicators(); for (ReportItemI indicator : collection) { persistIndicator(indicator); + rowIndex++; } - out = new FileOutputStream(filePath + fileName + ".xls"); + out = new FileOutputStream(filePath + "NEW"+fileName); wb.write(out); out.close(); } catch (Exception e) { @@ -218,7 +240,7 @@ private void persistMetric(ReportItemI metric) { // docs sacados de aquí https://www.javatpoint.com/apache-poi-excel-font // https://www.e-iceblue.com/Tutorials/Java/Spire.XLS-for-Java/Program-Guide/Cells/Apply-Fonts-in-Excel-in-Java.html - CellStyle style = wb.createCellStyle(); + //CellStyle style = wb.createCellStyle(); //style.setFont((Font) formater.getMetricFont()); row.createCell(cellIndex++).setCellValue(metric.getName()); @@ -232,6 +254,7 @@ private void persistMetric(ReportItemI metric) { } private void persistIndicator(ReportItemI indicator) { + log.info("Introduzco indicador en la hoja"); //Mantengo uno diferente porque en el futuro la información del indicador será distinta a la de la métrica int rowIndex = sheet.getLastRowNum(); @@ -242,28 +265,68 @@ private void persistIndicator(ReportItemI indicator) { // Aquí debería indicar el formato de fuente en las celdas, que dependerá del // estado del índice + // CellStyle estilo=wb.getCellStyleAt((short) 1); + + //CellStyle style = wb.createCellStyle(); + + /* + style.cloneStyleFrom(estilo); + if(style.equals(estilo) && (style.getIndex()!=estilo.getIndex())) { + log.info("Los estilos son iguales pero no son el mismo"); + }else + { + log.info("Esto no va bien"); + } +*/ + try { + CellStyle style = wb.createCellStyle(); + XSSFFont poiFont = wb.createFont(); + + Font a4iFont=formater.getIndicatorFont(indicator.getIndicator().getState()); + //Establezco el color y la fuente a utilizar en el texto de los indicadores. + + // HSSFPalette palette = wb.getCustomPalette(); + // get the color which most closely matches the color you want to use + //HSSFColor myColor = palette.findSimilarColor(a4iFont.getColor().getRed(), a4iFont.getColor().getGreen(), a4iFont.getColor().getBlue()); + // get the palette index of that color + //HSSFColor myColor=new HSSFColor(0,0,a4iFont.getColor()); + byte[] color= {(byte) a4iFont.getColor().getRed(),(byte) a4iFont.getColor().getGreen(),(byte) a4iFont.getColor().getBlue()}; + XSSFColor myColor=new XSSFColor(color); + log.info("El nuevo color es "+myColor.getARGBHex()); + //short palIndex = myColor.getIndex(); + + poiFont.setFontHeight((short)a4iFont.getFont().getSize()); + poiFont.setFontName(a4iFont.getFont().getFamily()); + poiFont.setColor(myColor.getIndexed()); + log.info("La nueva fuente poi es "+poiFont.toString()); + + //style.setFillBackgroundColor(a4iFont.getColor().toString()); + style.setFont(poiFont); + //style.setFillBackgroundColor(myColor.getIndexed()); + log.info("Creado el estilo con indice "+style.getIndex()); + row.createCell(cellIndex++).setCellValue(indicator.getName()); + row.createCell(cellIndex++).setCellValue(indicator.getValue().toString()); + row.createCell(cellIndex++).setCellValue(indicator.getUnit()); + row.createCell(cellIndex++).setCellValue(indicator.getDescription()); + + row.createCell(cellIndex).setCellStyle(style); + //row.createCell(cellIndex).getCellStyle().cloneStyleFrom(style); + //row.getCell(cellIndex).getCellStyle().cloneStyleFrom(style); + log.info("Establecido el estilo con indice "+style.getIndex()+" en la celda "+cellIndex); + row.createCell(cellIndex++).setCellValue(indicator.getIndicator().getState().toString()); - CellStyle style = wb.createCellStyle(); + row.createCell(cellIndex++).setCellValue(indicator.getSource()); - try { - style.setFont((Font) formater.getIndicatorFont(indicator.getIndicator().getState())); + row.createCell(cellIndex).setCellValue(indicator.getDate().toString()); + + log.info("Indice de celda final " + cellIndex); + } catch (IOException e) { log.warning("Problema al abrir el fichero con los formatos"); e.printStackTrace(); } - row.createCell(cellIndex++).setCellValue(indicator.getName()); - row.createCell(cellIndex++).setCellValue(indicator.getValue().toString()); - row.createCell(cellIndex++).setCellValue(indicator.getUnit()); - row.createCell(cellIndex++).setCellValue(indicator.getDescription()); - - row.createCell(cellIndex++).setCellValue(indicator.getIndicator().getState().toString()); - - row.createCell(cellIndex++).setCellValue(indicator.getSource()); - - row.createCell(cellIndex).setCellValue(indicator.getDate().toString()); - - log.info("Indice de celda final " + cellIndex); + } diff --git a/src/main/resources/a4i.conf b/src/main/resources/a4i.conf index 0e40c255..70386dbf 100644 --- a/src/main/resources/a4i.conf +++ b/src/main/resources/a4i.conf @@ -20,7 +20,7 @@ Font.WARNING.type=Serif Font.CRITICAL.color=Red Font.CRITICAL.height=16 -Font.CRITICAL.type=Serif +Font.CRITICAL.type=Elephant #Caracteristicas por defecto de la fuente, cuando los informes son visuales Font.default.color=black diff --git a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java index 766904f7..a13b9306 100644 --- a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java @@ -3,7 +3,7 @@ * Test para probar la clase ExcelReportManager *

* - * @author Iv�n Matas Gonz�lez + * @author Ivan Matas * */ package us.muit.fs.a4i.test.persistence; @@ -24,10 +24,13 @@ import org.mockito.junit.jupiter.MockitoExtension; import us.muit.fs.a4i.exceptions.ReportNotDefinedException; +import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItem; import us.muit.fs.a4i.model.entities.ReportItemI; import us.muit.fs.a4i.persistence.ExcelReportManager; +import us.muit.fs.a4i.persistence.ReportFormater; import us.muit.fs.a4i.persistence.ReportFormaterI; @ExtendWith(MockitoExtension.class) @@ -48,6 +51,14 @@ class ExcelReportManagerTest { private static ReportItem metricStrMock = Mockito.mock(ReportItem.class); @Mock(serializable = true) private static ReportI informe = Mockito.mock(ReportI.class); + + @Mock(serializable = true) + private static ReportItem itemIndicatorIntMock = Mockito.mock(ReportItem.class); + + @Mock(serializable=true) + private static Indicator indicatorIntMock = Mockito.mock(Indicator.class); + + private static String excelPath; private static String excelName; private static ExcelReportManager underTest; @@ -61,30 +72,44 @@ static void setUpBeforeClass() throws Exception { } + @Test void ExcelCreation() { - List lista=new ArrayList(); + List listaMetric=new ArrayList(); + List listaInd=new ArrayList(); Date fecha=new Date(); Mockito.when(metricIntMock.getValue()).thenReturn(55); Mockito.when(metricIntMock.getName()).thenReturn("downloads"); Mockito.when(metricIntMock.getUnit()).thenReturn("downloads"); Mockito.when(metricIntMock.getDescription()).thenReturn("Descargas realizadas"); Mockito.when(metricIntMock.getDate()).thenReturn(fecha); - lista.add(metricIntMock); + listaMetric.add(metricIntMock); Mockito.when(metricStrMock.getValue()).thenReturn("2-2-22"); Mockito.when(metricStrMock.getName()).thenReturn("lastPush"); Mockito.when(metricStrMock.getUnit()).thenReturn("date"); Mockito.when(metricStrMock.getDescription()).thenReturn("Último push realizado en el repositorio"); Mockito.when(metricStrMock.getDate()).thenReturn(fecha); - lista.add(metricStrMock); + listaMetric.add(metricStrMock); + + Mockito.when(itemIndicatorIntMock.getValue()).thenReturn(22); + Mockito.when(itemIndicatorIntMock.getName()).thenReturn("otracosa"); + Mockito.when(itemIndicatorIntMock.getUnit()).thenReturn("cosas"); + Mockito.when(itemIndicatorIntMock.getDescription()).thenReturn("MetricaEjemplo"); + Mockito.when(itemIndicatorIntMock.getDate()).thenReturn(fecha); + Mockito.when(indicatorIntMock.getState()).thenReturn(IndicatorState.CRITICAL); + Mockito.when(itemIndicatorIntMock.getIndicator()).thenReturn(indicatorIntMock); + listaInd.add(itemIndicatorIntMock); - Mockito.when(informe.getAllMetrics()).thenReturn(lista); + Mockito.when(informe.getAllMetrics()).thenReturn(listaMetric); + Mockito.when(informe.getAllIndicators()).thenReturn(listaInd); Mockito.when(informe.getEntityId()).thenReturn("entidadTest"); excelPath = new String("src" + File.separator + "test" + File.separator + "resources"+File.separator); - excelName= new String("excelTest"); + excelName= new String("excelTest.xlsx"); underTest=new ExcelReportManager(excelPath,excelName); + + underTest.setFormater(new ReportFormater()); try { log.info("El informe tiene el id "+informe.getEntityId()); underTest.saveReport(informe); diff --git a/src/test/resources/excelTest.xlsx b/src/test/resources/excelTest.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4b39e1cc0714a93959606588b12a2548dd9d663c GIT binary patch literal 6196 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 literal 0 HcmV?d00001 From ba7607df09d315e81169c35b45c867e7c33a9605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Tue, 5 Mar 2024 11:10:30 +0100 Subject: [PATCH 062/100] Issue 93 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modificar el método persistIndicator de la clase ExcelReportManager para que configure la fuente del texto en las filas de indicadores de forma adecuada considerando el nuevo tipo Font --- build.gradle | 2 +- .../a4i/persistence/ExcelReportManager.java | 150 ++++++++++-------- src/main/resources/a4i.conf | 12 +- .../persistence/ExcelReportManagerTest.java | 7 +- 4 files changed, 90 insertions(+), 81 deletions(-) diff --git a/build.gradle b/build.gradle index 4c05fa7c..c63df61e 100644 --- a/build.gradle +++ b/build.gradle @@ -103,7 +103,7 @@ dependencies { //JAVADOC: https://poi.apache.org/apidocs/5.0/ // implementation 'org.apache.poi:poi:5.2.5' // https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml - implementation group: 'org.apache.poi', name: 'poi-ooxml', version: '3.9' + implementation group: 'org.apache.poi', name: 'poi-ooxml', version: '5.2.5' //Para leer la configuración como ficheros con datos en formato json // https://mvnrepository.com/artifact/javax.json/javax.json-api //JAVADOC: https://javadoc.io/doc/org.glassfish/javax.json/latest/overview-summary.html diff --git a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java index 0543b60b..12c1f9ff 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java @@ -10,6 +10,8 @@ import java.time.ZoneOffset; import java.util.Collection; import java.util.Date; +import java.util.HashMap; +import java.util.Map; import java.util.logging.Logger; import org.apache.poi.EncryptedDocumentException; @@ -26,14 +28,13 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFFont; - +import org.apache.poi.xssf.usermodel.XSSFCell; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; import org.apache.poi.xssf.usermodel.XSSFColor; - -import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.apache.poi.xssf.model.StylesTable; -import org.apache.poi.ss.usermodel.Row; - import us.muit.fs.a4i.exceptions.ReportNotDefinedException; import us.muit.fs.a4i.model.entities.ReportI; @@ -69,7 +70,8 @@ */ public class ExcelReportManager implements PersistenceManager, FileManager { private static Logger log = Logger.getLogger(ExcelReportManager.class.getName()); - private static int fontIndex=0; + + private Map styles=new HashMap(); /** *

* Referencia al gestor de estilo que se va a utilizar @@ -142,9 +144,7 @@ protected XSSFSheet getCleanSheet(String entityId) throws EncryptedDocumentExcep log.info("Solicita una hoja nueva del libro manejado, para la entidad con id: "+entityId); if (wb == null) { inputStream = new FileInputStream(filePath + fileName); - // wb = (HSSFWorkbook) HSSWorkbookFactory.create(inputStream); - //XSSFWorkbookFactory factory=new SSSFWorkbookFactory(); - + wb = new XSSFWorkbook(inputStream); log.info("Generado workbook"); @@ -180,15 +180,13 @@ protected XSSFSheet getCleanSheet(String entityId) throws EncryptedDocumentExcep } /** - * Guarda en un hoja limpia con el nombre del id del informe todas las métricas - * y los indicadores que incluya + * Un informe será una hoja en el libro excel + * Guarda en un hoja limpia con el nombre del id del informe + * Incluye todas las métricas y los indicadores que tenga report */ @Override - public void saveReport(ReportI report) throws ReportNotDefinedException { + public void saveReport(ReportI report){ log.info("Guardando informe con id: "+report.getEntityId()); - if (report == null) { - throw new ReportNotDefinedException(); - } try { FileOutputStream out; if (sheet == null) { @@ -196,9 +194,11 @@ public void saveReport(ReportI report) throws ReportNotDefinedException { } /** - * A partir de la última que haya Fila 1: Encabezado métricas Filas 2 a N:Para - * cada métrica del informe una fila Fila N+1: Encabezado indicadores Filas N+2 - * a M: Para cada indicador una fila + * A partir de la última que haya + * Fila 1: Encabezado métricas + * Filas 2 a N:Para cada métrica del informe una fila + * Fila N+1: Encabezado indicadores + * Filas N+2 a M: Para cada indicador una fila */ int rowIndex = sheet.getLastRowNum(); rowIndex++; @@ -233,7 +233,7 @@ private void persistMetric(ReportItemI metric) { int rowIndex = sheet.getLastRowNum(); rowIndex++; - Row row = sheet.createRow(rowIndex); + XSSFRow row = sheet.createRow(rowIndex); log.info("Indice de fila nueva " + rowIndex); int cellIndex = 0; // Aquí debería incorporar el formato de fuente en las celdas @@ -259,67 +259,79 @@ private void persistIndicator(ReportItemI indicator) { //Mantengo uno diferente porque en el futuro la información del indicador será distinta a la de la métrica int rowIndex = sheet.getLastRowNum(); rowIndex++; - Row row = sheet.createRow(rowIndex); + XSSFRow row = sheet.createRow(rowIndex); log.info("Indice de fila nueva " + rowIndex); - int cellIndex = 0; + int cellIndex = 0; + StylesTable stylesTable=wb.getStylesSource(); + stylesTable.ensureThemesTable(); - // Aquí debería indicar el formato de fuente en las celdas, que dependerá del - // estado del índice - // CellStyle estilo=wb.getCellStyleAt((short) 1); - - //CellStyle style = wb.createCellStyle(); - /* - style.cloneStyleFrom(estilo); - if(style.equals(estilo) && (style.getIndex()!=estilo.getIndex())) { - log.info("Los estilos son iguales pero no son el mismo"); - }else - { - log.info("Esto no va bien"); - } -*/ + + XSSFCellStyle style=styles.get(indicator.getIndicator().getState().toString()); try { - CellStyle style = wb.createCellStyle(); - XSSFFont poiFont = wb.createFont(); - - Font a4iFont=formater.getIndicatorFont(indicator.getIndicator().getState()); - //Establezco el color y la fuente a utilizar en el texto de los indicadores. + if (style==null){ + + style = stylesTable.createCellStyle(); + XSSFFont poiFont = wb.createFont(); + + Font a4iFont=formater.getIndicatorFont(indicator.getIndicator().getState()); + //Establezco el color y la fuente a utilizar en el texto de los indicadores. + + // HSSFPalette palette = wb.getCustomPalette(); + // get the color which most closely matches the color you want to use + //HSSFColor myColor = palette.findSimilarColor(a4iFont.getColor().getRed(), a4iFont.getColor().getGreen(), a4iFont.getColor().getBlue()); + // get the palette index of that color + //HSSFColor myColor=new HSSFColor(0,0,a4iFont.getColor()); + byte[] color= {(byte) a4iFont.getColor().getRed(),(byte) a4iFont.getColor().getGreen(),(byte) a4iFont.getColor().getBlue()}; + XSSFColor myColor=new XSSFColor(color); + + log.info("El nuevo color es "+myColor.getARGBHex()); + + + //myColor.setIndexed(newColor++); + + poiFont.setFontHeightInPoints((short)a4iFont.getFont().getSize()); + poiFont.setFontName(a4iFont.getFont().getFamily()); + poiFont.setColor(myColor); + poiFont.setBold(true); + poiFont.setItalic(false); + + log.info("La nueva fuente poi es "+poiFont.toString()); + + //style.setFillBackgroundColor(a4iFont.getColor().toString()); + style.setFont(poiFont); + style.setFillBackgroundColor(myColor); + + styles.put(indicator.getIndicator().getState().toString(), style); + + log.info("Creado el estilo con indice "+style.getIndex()); + } + XSSFCell cell; + + row.createCell(cellIndex).setCellValue(indicator.getName()); + sheet.autoSizeColumn(cellIndex++); - // HSSFPalette palette = wb.getCustomPalette(); - // get the color which most closely matches the color you want to use - //HSSFColor myColor = palette.findSimilarColor(a4iFont.getColor().getRed(), a4iFont.getColor().getGreen(), a4iFont.getColor().getBlue()); - // get the palette index of that color - //HSSFColor myColor=new HSSFColor(0,0,a4iFont.getColor()); - byte[] color= {(byte) a4iFont.getColor().getRed(),(byte) a4iFont.getColor().getGreen(),(byte) a4iFont.getColor().getBlue()}; - XSSFColor myColor=new XSSFColor(color); - log.info("El nuevo color es "+myColor.getARGBHex()); - //short palIndex = myColor.getIndex(); + row.createCell(cellIndex).setCellValue(indicator.getValue().toString()); + sheet.autoSizeColumn(cellIndex++); - poiFont.setFontHeight((short)a4iFont.getFont().getSize()); - poiFont.setFontName(a4iFont.getFont().getFamily()); - poiFont.setColor(myColor.getIndexed()); - log.info("La nueva fuente poi es "+poiFont.toString()); + row.createCell(cellIndex).setCellValue(indicator.getUnit()); + sheet.autoSizeColumn(cellIndex++); - //style.setFillBackgroundColor(a4iFont.getColor().toString()); - style.setFont(poiFont); - //style.setFillBackgroundColor(myColor.getIndexed()); - log.info("Creado el estilo con indice "+style.getIndex()); - row.createCell(cellIndex++).setCellValue(indicator.getName()); - row.createCell(cellIndex++).setCellValue(indicator.getValue().toString()); - row.createCell(cellIndex++).setCellValue(indicator.getUnit()); - row.createCell(cellIndex++).setCellValue(indicator.getDescription()); + row.createCell(cellIndex).setCellValue(indicator.getDescription()); + sheet.autoSizeColumn(cellIndex++); - row.createCell(cellIndex).setCellStyle(style); - //row.createCell(cellIndex).getCellStyle().cloneStyleFrom(style); - //row.getCell(cellIndex).getCellStyle().cloneStyleFrom(style); + cell=row.createCell(cellIndex); + cell.setCellStyle(style); log.info("Establecido el estilo con indice "+style.getIndex()+" en la celda "+cellIndex); - row.createCell(cellIndex++).setCellValue(indicator.getIndicator().getState().toString()); - - row.createCell(cellIndex++).setCellValue(indicator.getSource()); + cell.setCellValue(indicator.getIndicator().getState().toString()); + sheet.autoSizeColumn(cellIndex++); + + row.createCell(cellIndex).setCellValue(indicator.getSource()); + sheet.autoSizeColumn(cellIndex++); row.createCell(cellIndex).setCellValue(indicator.getDate().toString()); - - log.info("Indice de celda final " + cellIndex); + sheet.autoSizeColumn(cellIndex); + log.info("Indice de celda final " + cellIndex); } catch (IOException e) { log.warning("Problema al abrir el fichero con los formatos"); diff --git a/src/main/resources/a4i.conf b/src/main/resources/a4i.conf index 70386dbf..0dd5a061 100644 --- a/src/main/resources/a4i.conf +++ b/src/main/resources/a4i.conf @@ -6,23 +6,23 @@ remote.type=GITHUB #Caracteristicas de fuente de las métricas Font.metric.color=green -Font.metric.height=15 +Font.metric.height=10 Font.metric.type=Serif #Caracteristicas para la fuente de indicadores según estados del mismo Font.OK.color=Black -Font.OK.height=12 +Font.OK.height=10 Font.OK.type=Serif Font.WARNING.color=Orange -Font.WARNING.height=12 +Font.WARNING.height=10 Font.WARNING.type=Serif Font.CRITICAL.color=Red -Font.CRITICAL.height=16 -Font.CRITICAL.type=Elephant +Font.CRITICAL.height=12 +Font.CRITICAL.type="Times New Roman" #Caracteristicas por defecto de la fuente, cuando los informes son visuales Font.default.color=black -Font.default.height=12 +Font.default.height=10 Font.default.type=Arial \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java index a13b9306..e8cea18c 100644 --- a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java @@ -110,13 +110,10 @@ void ExcelCreation() { underTest=new ExcelReportManager(excelPath,excelName); underTest.setFormater(new ReportFormater()); - try { + log.info("El informe tiene el id "+informe.getEntityId()); underTest.saveReport(informe); - } catch (ReportNotDefinedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } + } } \ No newline at end of file From eac27adbfa6582d1fd6c30a8c86ba760c76356c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Tue, 5 Mar 2024 11:38:21 +0100 Subject: [PATCH 063/100] =?UTF-8?q?Fin=20gesti=C3=B3n=20de=20formato=20de?= =?UTF-8?q?=20fuentes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se da por concluido, por ahora, el código de gestión de formato de fuentes para métricas e indicadores Se aprecia mucho código repetido, debería optmizarse la clase ExcelReportManager --- .../a4i/persistence/ExcelReportManager.java | 227 ++++++++++-------- .../persistence/ExcelReportManagerTest.java | 33 ++- 2 files changed, 156 insertions(+), 104 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java index 12c1f9ff..cfb60868 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java @@ -1,6 +1,3 @@ -/** - * - */ package us.muit.fs.a4i.persistence; import java.io.FileInputStream; @@ -16,15 +13,6 @@ import org.apache.poi.EncryptedDocumentException; -/** - * import org.apache.poi.hssf.usermodel.HSSFSheet; - -import org.apache.poi.hssf.usermodel.HSSFWorkbook; -import org.apache.poi.hssf.usermodel.HSSFFont; -import org.apache.poi.hssf.usermodel.HSSFWorkbookFactory; -import org.apache.poi.hssf.util.HSSFColor; -import org.apache.poi.ss.usermodel.CellStyle; - */ import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFFont; @@ -34,8 +22,6 @@ import org.apache.poi.xssf.usermodel.XSSFRow; import org.apache.poi.xssf.model.StylesTable; - - import us.muit.fs.a4i.exceptions.ReportNotDefinedException; import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItemI; @@ -63,6 +49,9 @@ * Si la hoja exist�a la recupera y se añadirá sobre ella, no se elimina lo * anterior, si no existía se crea nueva *

+ *

+ * Deuda técnica. En la persistencia de métricas e indicadores se observa mucho código replicado, se debe optimizar + *

* * @author Isabel Román * @@ -70,15 +59,15 @@ */ public class ExcelReportManager implements PersistenceManager, FileManager { private static Logger log = Logger.getLogger(ExcelReportManager.class.getName()); - - private Map styles=new HashMap(); + + private Map styles = new HashMap(); /** *

* Referencia al gestor de estilo que se va a utilizar *

*/ protected ReportFormaterI formater; - + FileInputStream inputStream = null; /** @@ -107,7 +96,6 @@ public ExcelReportManager() { super(); } - @Override public void setFormater(ReportFormaterI formater) { log.info("Establece el formateador"); @@ -141,10 +129,10 @@ public void setName(String name) { * @throws EncryptedDocumentException documento protegido */ protected XSSFSheet getCleanSheet(String entityId) throws EncryptedDocumentException, IOException { - log.info("Solicita una hoja nueva del libro manejado, para la entidad con id: "+entityId); + log.info("Solicita una hoja nueva del libro manejado, para la entidad con id: " + entityId); if (wb == null) { inputStream = new FileInputStream(filePath + fileName); - + wb = new XSSFWorkbook(inputStream); log.info("Generado workbook"); @@ -180,13 +168,13 @@ protected XSSFSheet getCleanSheet(String entityId) throws EncryptedDocumentExcep } /** - * Un informe será una hoja en el libro excel - * Guarda en un hoja limpia con el nombre del id del informe - * Incluye todas las métricas y los indicadores que tenga report + * Un informe será una hoja en el libro excel Guarda en un hoja limpia con el + * nombre del id del informe Incluye todas las métricas y los indicadores que + * tenga report */ @Override - public void saveReport(ReportI report){ - log.info("Guardando informe con id: "+report.getEntityId()); + public void saveReport(ReportI report) { + log.info("Guardando informe con id: " + report.getEntityId()); try { FileOutputStream out; if (sheet == null) { @@ -194,11 +182,9 @@ public void saveReport(ReportI report){ } /** - * A partir de la última que haya - * Fila 1: Encabezado métricas - * Filas 2 a N:Para cada métrica del informe una fila - * Fila N+1: Encabezado indicadores - * Filas N+2 a M: Para cada indicador una fila + * A partir de la última que haya Fila 1: Encabezado métricas Filas 2 a N:Para + * cada métrica del informe una fila Fila N+1: Encabezado indicadores Filas N+2 + * a M: Para cada indicador una fila */ int rowIndex = sheet.getLastRowNum(); rowIndex++; @@ -210,20 +196,20 @@ public void saveReport(ReportI report){ persistMetric(metric); rowIndex++; } - //Ahora irían los indicadores + // Ahora irían los indicadores rowIndex++; - sheet.createRow(rowIndex).createCell(0).setCellValue("Indicadores"); + sheet.createRow(rowIndex).createCell(0).setCellValue("Indicadores"); collection = report.getAllIndicators(); for (ReportItemI indicator : collection) { persistIndicator(indicator); rowIndex++; } - - out = new FileOutputStream(filePath + "NEW"+fileName); + + out = new FileOutputStream(filePath + "NEW" + fileName); wb.write(out); out.close(); } catch (Exception e) { - + e.printStackTrace(); } } @@ -236,110 +222,145 @@ private void persistMetric(ReportItemI metric) { XSSFRow row = sheet.createRow(rowIndex); log.info("Indice de fila nueva " + rowIndex); int cellIndex = 0; - // Aquí debería incorporar el formato de fuente en las celdas - // docs sacados de aquí https://www.javatpoint.com/apache-poi-excel-font - // https://www.e-iceblue.com/Tutorials/Java/Spire.XLS-for-Java/Program-Guide/Cells/Apply-Fonts-in-Excel-in-Java.html - - //CellStyle style = wb.createCellStyle(); - //style.setFont((Font) formater.getMetricFont()); - - row.createCell(cellIndex++).setCellValue(metric.getName()); - row.createCell(cellIndex++).setCellValue(metric.getValue().toString()); - row.createCell(cellIndex++).setCellValue(metric.getUnit()); - row.createCell(cellIndex++).setCellValue(metric.getDescription()); - row.createCell(cellIndex++).setCellValue(metric.getSource()); + StylesTable stylesTable = wb.getStylesSource(); + stylesTable.ensureThemesTable(); + XSSFCellStyle style = styles.get("metricStyle"); + + if (style == null) { + + style = stylesTable.createCellStyle(); + XSSFFont poiFont = wb.createFont(); + + Font a4iFont = formater.getMetricFont(); + // Establezco el color y la fuente a utilizar en el texto de los indicadores. + byte[] color = { (byte) a4iFont.getColor().getRed(), (byte) a4iFont.getColor().getGreen(), + (byte) a4iFont.getColor().getBlue() }; + XSSFColor myColor = new XSSFColor(color); + log.info("El nuevo color es " + myColor.getARGBHex()); + + poiFont.setFontHeightInPoints((short) a4iFont.getFont().getSize()); + poiFont.setFontName(a4iFont.getFont().getFamily()); + poiFont.setColor(myColor); + poiFont.setBold(true); + poiFont.setItalic(false); + + log.info("La nueva fuente poi es " + poiFont.toString()); + + // style.setFillBackgroundColor(a4iFont.getColor().toString()); + style.setFont(poiFont); + style.setFillBackgroundColor(myColor); + styles.put("metricStyle", style); + log.info("Creado el estilo con indice " + style.getIndex()); + } + + XSSFCell cell; + + cell=row.createCell(cellIndex); + cell.setCellValue(metric.getName()); + cell.setCellStyle(style); + sheet.autoSizeColumn(cellIndex++); + + cell=row.createCell(cellIndex); + cell.setCellValue(metric.getValue().toString()); + cell.setCellStyle(style); + sheet.autoSizeColumn(cellIndex++); + + row.createCell(cellIndex).setCellValue(metric.getUnit()); + sheet.autoSizeColumn(cellIndex++); + + row.createCell(cellIndex).setCellValue(metric.getDescription()); + sheet.autoSizeColumn(cellIndex++); + + row.createCell(cellIndex).setCellValue(metric.getSource()); + sheet.autoSizeColumn(cellIndex++); + row.createCell(cellIndex).setCellValue(metric.getDate().toString()); - log.info("Indice de celda final" + cellIndex); + sheet.autoSizeColumn(cellIndex); + log.info("Indice de celda final " + cellIndex); } private void persistIndicator(ReportItemI indicator) { - log.info("Introduzco indicador en la hoja"); - //Mantengo uno diferente porque en el futuro la información del indicador será distinta a la de la métrica + // Mantengo uno diferente porque en el futuro la información del indicador será + // distinta a la de la métrica int rowIndex = sheet.getLastRowNum(); rowIndex++; XSSFRow row = sheet.createRow(rowIndex); log.info("Indice de fila nueva " + rowIndex); - int cellIndex = 0; - StylesTable stylesTable=wb.getStylesSource(); + int cellIndex = 0; + StylesTable stylesTable = wb.getStylesSource(); stylesTable.ensureThemesTable(); - - - XSSFCellStyle style=styles.get(indicator.getIndicator().getState().toString()); + XSSFCellStyle style = styles.get(indicator.getIndicator().getState().toString()); try { - if (style==null){ - + if (style == null) { + style = stylesTable.createCellStyle(); - XSSFFont poiFont = wb.createFont(); - - Font a4iFont=formater.getIndicatorFont(indicator.getIndicator().getState()); - //Establezco el color y la fuente a utilizar en el texto de los indicadores. - - // HSSFPalette palette = wb.getCustomPalette(); - // get the color which most closely matches the color you want to use - //HSSFColor myColor = palette.findSimilarColor(a4iFont.getColor().getRed(), a4iFont.getColor().getGreen(), a4iFont.getColor().getBlue()); - // get the palette index of that color - //HSSFColor myColor=new HSSFColor(0,0,a4iFont.getColor()); - byte[] color= {(byte) a4iFont.getColor().getRed(),(byte) a4iFont.getColor().getGreen(),(byte) a4iFont.getColor().getBlue()}; - XSSFColor myColor=new XSSFColor(color); - - log.info("El nuevo color es "+myColor.getARGBHex()); - - - //myColor.setIndexed(newColor++); - - poiFont.setFontHeightInPoints((short)a4iFont.getFont().getSize()); + XSSFFont poiFont = wb.createFont(); + + Font a4iFont = formater.getIndicatorFont(indicator.getIndicator().getState()); + // Establezco el color y la fuente a utilizar en el texto de los indicadores. + byte[] color = { (byte) a4iFont.getColor().getRed(), (byte) a4iFont.getColor().getGreen(), + (byte) a4iFont.getColor().getBlue() }; + XSSFColor myColor = new XSSFColor(color); + + log.info("El nuevo color es " + myColor.getARGBHex()); + + // myColor.setIndexed(newColor++); + + poiFont.setFontHeightInPoints((short) a4iFont.getFont().getSize()); poiFont.setFontName(a4iFont.getFont().getFamily()); - poiFont.setColor(myColor); + poiFont.setColor(myColor); poiFont.setBold(true); - poiFont.setItalic(false); - - log.info("La nueva fuente poi es "+poiFont.toString()); - - //style.setFillBackgroundColor(a4iFont.getColor().toString()); + poiFont.setItalic(false); + + log.info("La nueva fuente poi es " + poiFont.toString()); + + // style.setFillBackgroundColor(a4iFont.getColor().toString()); style.setFont(poiFont); style.setFillBackgroundColor(myColor); - + styles.put(indicator.getIndicator().getState().toString(), style); - - log.info("Creado el estilo con indice "+style.getIndex()); + + log.info("Creado el estilo con indice " + style.getIndex()); } XSSFCell cell; - - row.createCell(cellIndex).setCellValue(indicator.getName()); - sheet.autoSizeColumn(cellIndex++); - - row.createCell(cellIndex).setCellValue(indicator.getValue().toString()); + + cell=row.createCell(cellIndex); + cell.setCellValue(indicator.getName()); + cell.setCellStyle(style); + sheet.autoSizeColumn(cellIndex++); + + cell=row.createCell(cellIndex); + cell.setCellValue(indicator.getValue().toString()); + cell.setCellStyle(style); sheet.autoSizeColumn(cellIndex++); - + row.createCell(cellIndex).setCellValue(indicator.getUnit()); sheet.autoSizeColumn(cellIndex++); - + row.createCell(cellIndex).setCellValue(indicator.getDescription()); sheet.autoSizeColumn(cellIndex++); - - cell=row.createCell(cellIndex); - cell.setCellStyle(style); - log.info("Establecido el estilo con indice "+style.getIndex()+" en la celda "+cellIndex); + + cell = row.createCell(cellIndex); + cell.setCellStyle(style); + log.info("Establecido el estilo con indice " + style.getIndex() + " en la celda " + cellIndex); cell.setCellValue(indicator.getIndicator().getState().toString()); - sheet.autoSizeColumn(cellIndex++); - + sheet.autoSizeColumn(cellIndex++); + row.createCell(cellIndex).setCellValue(indicator.getSource()); - sheet.autoSizeColumn(cellIndex++); + sheet.autoSizeColumn(cellIndex++); row.createCell(cellIndex).setCellValue(indicator.getDate().toString()); sheet.autoSizeColumn(cellIndex); - log.info("Indice de celda final " + cellIndex); - + log.info("Indice de celda final " + cellIndex); + } catch (IOException e) { log.warning("Problema al abrir el fichero con los formatos"); e.printStackTrace(); } - - } @Override diff --git a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java index e8cea18c..e78f65fb 100644 --- a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java @@ -58,6 +58,19 @@ class ExcelReportManagerTest { @Mock(serializable=true) private static Indicator indicatorIntMock = Mockito.mock(Indicator.class); + @Mock(serializable = true) + private static ReportItem itemIndicatorIntMock2 = Mockito.mock(ReportItem.class); + + @Mock(serializable=true) + private static Indicator indicatorIntMock2 = Mockito.mock(Indicator.class); + + @Mock(serializable = true) + private static ReportItem itemIndicatorIntMock3 = Mockito.mock(ReportItem.class); + + @Mock(serializable=true) + private static Indicator indicatorIntMock3 = Mockito.mock(Indicator.class); + + private static String excelPath; private static String excelName; @@ -95,11 +108,29 @@ void ExcelCreation() { Mockito.when(itemIndicatorIntMock.getValue()).thenReturn(22); Mockito.when(itemIndicatorIntMock.getName()).thenReturn("otracosa"); Mockito.when(itemIndicatorIntMock.getUnit()).thenReturn("cosas"); - Mockito.when(itemIndicatorIntMock.getDescription()).thenReturn("MetricaEjemplo"); + Mockito.when(itemIndicatorIntMock.getDescription()).thenReturn("Indicador Ejemplo"); Mockito.when(itemIndicatorIntMock.getDate()).thenReturn(fecha); Mockito.when(indicatorIntMock.getState()).thenReturn(IndicatorState.CRITICAL); Mockito.when(itemIndicatorIntMock.getIndicator()).thenReturn(indicatorIntMock); listaInd.add(itemIndicatorIntMock); + + Mockito.when(itemIndicatorIntMock2.getValue()).thenReturn(67); + Mockito.when(itemIndicatorIntMock2.getName()).thenReturn("indicador2"); + Mockito.when(itemIndicatorIntMock2.getUnit()).thenReturn("unidad2"); + Mockito.when(itemIndicatorIntMock2.getDescription()).thenReturn("Indicador Ejemplo 2"); + Mockito.when(itemIndicatorIntMock2.getDate()).thenReturn(fecha); + Mockito.when(indicatorIntMock2.getState()).thenReturn(IndicatorState.WARNING); + Mockito.when(itemIndicatorIntMock2.getIndicator()).thenReturn(indicatorIntMock2); + listaInd.add(itemIndicatorIntMock2); + + Mockito.when(itemIndicatorIntMock3.getValue()).thenReturn(98); + Mockito.when(itemIndicatorIntMock3.getName()).thenReturn("indicador3"); + Mockito.when(itemIndicatorIntMock3.getUnit()).thenReturn("unidad3"); + Mockito.when(itemIndicatorIntMock3.getDescription()).thenReturn("IndicadorEjemplo3"); + Mockito.when(itemIndicatorIntMock3.getDate()).thenReturn(fecha); + Mockito.when(indicatorIntMock3.getState()).thenReturn(IndicatorState.CRITICAL); + Mockito.when(itemIndicatorIntMock3.getIndicator()).thenReturn(indicatorIntMock3); + listaInd.add(itemIndicatorIntMock3); Mockito.when(informe.getAllMetrics()).thenReturn(listaMetric); Mockito.when(informe.getAllIndicators()).thenReturn(listaInd); From dfd06aab5405332530987feef6939a6372d212db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Tue, 5 Mar 2024 13:31:04 +0100 Subject: [PATCH 064/100] issues 82 y 84 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Primera aproximación a la gestión de límites en los indicadores Modificación de la clase IndicatorConfiguration Hay mucho que hacer - los límties siemrpe representan valor más alto ¿y si es al revés? - El valor intermedio no se usa para nada en realidad, porque sólo hay tres estados posibles, se usan dos valores - Podría haber indicadores no numéricos --- .../fs/a4i/config/IndicatorConfiguration.java | 69 +++++++++++++++- src/main/resources/a4iDefault.json | 8 +- .../config/IndicatorConfigurationTest.java | 80 ++++++++++++++++++- 3 files changed, 147 insertions(+), 10 deletions(-) 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 25d7f569..59e2fd89 100644 --- a/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java @@ -17,6 +17,7 @@ import javax.json.JsonObject; import javax.json.JsonReader; +import us.muit.fs.a4i.model.entities.IndicatorI; import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; import us.muit.fs.a4i.model.entities.ReportItemI; @@ -27,6 +28,10 @@ public class IndicatorConfiguration implements IndicatorConfigurationI { private static Logger log = Logger.getLogger(Checker.class.getName()); + + public String CRITICAL_LIMIT = "limits.critical"; + public String WARNING_LIMIT = "limits.warning"; + public String OK_LIMIT = "limits.ok"; @Override /** @@ -87,6 +92,23 @@ private HashMap isDefinedIndicator(String indicatorName, String indicatorDefinition = new HashMap(); indicatorDefinition.put("description", indicators.get(i).asJsonObject().getString("description")); indicatorDefinition.put("unit", indicators.get(i).asJsonObject().getString("unit")); + + JsonObject limits = indicators.get(i).asJsonObject().getJsonObject("limits"); + int okLimit = 0; + int warningLimit = 0; + int criticalLimit = 0; + + if(limits != null) { + okLimit = limits.getInt("ok"); + warningLimit = limits.getInt("warning"); + criticalLimit = limits.getInt("critical"); + indicatorDefinition.put(OK_LIMIT, Integer.toString(okLimit)); + indicatorDefinition.put(WARNING_LIMIT, Integer.toString(warningLimit)); + indicatorDefinition.put(CRITICAL_LIMIT, Integer.toString(criticalLimit)); + } else { + log.info("El fichero de configuración no especifica límites para este indicador"); + } + } } @@ -141,9 +163,50 @@ public List listAllIndicators() throws FileNotFoundException { } @Override - public IndicatorState getIndicatorState(ReportItemI indicator) { - // TODO Auto-generated method stub - return null; + public IndicatorState getIndicatorState(ReportItemI indicator){ + //TODO: change indicator definitions key name to a constant. + + String indicatorType = indicator.getValue().getClass().getName(); + + IndicatorState finalState = IndicatorState.UNDEFINED; + try { + HashMap indicatorDefinition = definedIndicator(indicator.getName(), indicatorType); + + + String criticalLimit = indicatorDefinition.get(CRITICAL_LIMIT); + String warningLimit = indicatorDefinition.get(WARNING_LIMIT); + String okLimit = indicatorDefinition.get(OK_LIMIT); + + // Si no se han encontrado límites definidos para ese indicador el estado es UNDEFINED. + if(criticalLimit != null && warningLimit != null && okLimit != null) { + // Se tienen en cuenta los posibles tipos de indicadores para compararlos. + if(indicatorType.equals(Integer.class.getName())) { + Integer value = (Integer) indicator.getValue(); + + if(value >= Integer.parseInt(criticalLimit)) finalState = IndicatorState.CRITICAL; + else if(value <=Integer.parseInt(okLimit)) finalState = IndicatorState.OK; + else finalState = IndicatorState.WARNING; + + } else if(indicatorType.equals(Double.class.getName())) { + Double value = (Double) indicator.getValue(); + + if(value >= Integer.parseInt(criticalLimit)) finalState = IndicatorState.CRITICAL; + else if(value <= Integer.parseInt(okLimit)) finalState = IndicatorState.OK; + else finalState = IndicatorState.WARNING; + + } + } else { + log.warning("No se han encontrado límites definidos para el indicador: " + indicator.getName()); + finalState = IndicatorState.UNDEFINED; + } + + }catch(Exception e){ + + e.printStackTrace(); + } + log.info("El estado del Indicador "+indicator.getName()+" es "+finalState.toString()); + return finalState; } + } diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index b6eda890..7f703ec7 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -55,16 +55,18 @@ } ], "indicators": [{ - "name": "issuesProgess", + "name": "issuesProgress", "type": "java.lang.Double", "description": "Ratio de issues cerrados frente a totales", - "unit": "ratio" + "unit": "ratio", + "limits": { "ok": 2, "warning": 4, "critical": 6 } }, { "name": "overdued", "type": "java.lang.Double", "description": "Ratio de issues vencidos frente a abiertos", - "unit": "ratio" + "unit": "ratio", + "limits": { "ok": 5, "warning": 9, "critical": 12 } }, { "name": "issuesRatio", diff --git a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java index a358eb9e..4d947625 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java @@ -19,16 +19,24 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; import us.muit.fs.a4i.config.Context; import us.muit.fs.a4i.config.IndicatorConfiguration; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.IndicatorI; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; +import us.muit.fs.a4i.model.entities.ReportItemI; class IndicatorConfigurationTest { private static Logger log = Logger.getLogger(IndicatorConfigurationTest.class.getName()); static IndicatorConfiguration underTest; static String appConfPath; String appIndicatorsPath = "/test/home"; - + /** *

Acciones a realizar antes de ejecutar los tests definidos en esta clase. En este caso se establece la ruta al fichero de configuración, en la carpeta resources dentro del paquete de test

* @throws java.lang.Exception @@ -95,7 +103,10 @@ void testDefinedIndicator() { assertNotNull(returnedMap, "Debería devolver un hashmap, el indicador overdued está definido"); assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); - + // Se comprueba que los indicadores incluyen los limites definidos + assertTrue(returnedMap.containsKey(underTest.OK_LIMIT), "La clave correspondiente al limite del estado OK tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey(underTest.WARNING_LIMIT), "La clave correspondiente al limite del estado WARNING tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey(underTest.CRITICAL_LIMIT), "La clave correspondiente al limite del estado CRITICAL tiene que estar en el mapa"); // Busco una métrica que existe pero con un tipo incorrecto assertNull(underTest.definedIndicator("overdued", valKOMock.getClass().getName()), "Debería ser nulo, el indicador overdued está definido para Double"); @@ -127,6 +138,10 @@ void testDefinedIndicator() { assertNotNull(returnedMap, "Debería devolver un hashmap, el indicador está definido"); assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); + // Se comprueba que los indicadores incluyen los limites definidos + assertTrue(returnedMap.containsKey(underTest.OK_LIMIT), "La clave correspondiente al limite del estado OK tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey(underTest.WARNING_LIMIT), "La clave correspondiente al limite del estado WARNING tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey(underTest.CRITICAL_LIMIT), "La clave correspondiente al limite del estado CRITICAL tiene que estar en el mapa"); } catch (FileNotFoundException e) { fail("No debería devolver esta excepción"); } catch (Exception e) { @@ -158,8 +173,65 @@ void testListAllIndicators() { } @Test - void testGetIndicatorState() { - fail("Not yet implemented"); + void testGetIndicatorState(){ + // Prueba 1. + Context.setAppRI(appConfPath); + + ReportItemI indicator = null; + IndicatorI.IndicatorState estado = IndicatorI.IndicatorState.UNDEFINED; + + try { + indicator = new ReportItemBuilder("overdued", 2.0).build(); + } catch (ReportItemException e) { + fail("El archivo de configuración esperado no contiene el indicador necesario para esta prueba."); + } + + estado = underTest.getIndicatorState(indicator); + + assertTrue(estado == IndicatorI.IndicatorState.OK, "El estado es INCORRECTO. Debería de ser OK."); + + + + + try { + indicator = new ReportItemBuilder("overdued", 9.0).build(); + } catch (ReportItemException e) { + // TODO Auto-generated catch block + e.printStackTrace(); } + + estado = underTest.getIndicatorState(indicator); + + assertTrue(estado == IndicatorI.IndicatorState.WARNING, "El estado es INCORRECTO. Debería de ser WARNING."); + + + + try { + indicator = new ReportItemBuilder("overdued", 13.0).build(); + } catch (ReportItemException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + estado = underTest.getIndicatorState(indicator); + + assertTrue(estado == IndicatorI.IndicatorState.CRITICAL, "El estado es INCORRECTO. Debería de ser CRITICAL."); + + + + try { + indicator = new ReportItemBuilder("issuesRatio", 13.0).build(); + } catch (ReportItemException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + estado = underTest.getIndicatorState(indicator); + + assertTrue(estado == IndicatorI.IndicatorState.UNDEFINED, "El estado es INCORRECTO. Debería de ser UNDEFINED."); + + + } + } From 1fe87019ad985cced3af6f78ba7a443b10581ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Tue, 5 Mar 2024 14:04:50 +0100 Subject: [PATCH 065/100] issue 80 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Añadir método para eliminar un informe en ExcelReportManager Aún sin terminar --- .../a4i/persistence/ExcelReportManager.java | 27 ++++++++++++- .../persistence/ExcelReportManagerTest.java | 39 ++++++++++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java index cfb60868..1f2b312f 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java @@ -365,7 +365,32 @@ private void persistIndicator(ReportItemI indicator) { @Override public void deleteReport(ReportI report) throws ReportNotDefinedException { - // TODO Auto-generated method stub + + log.info("Eliminando informe excel"); + if (report == null) { + throw new ReportNotDefinedException(); + } + try { + inputStream = new FileInputStream(filePath + fileName); + wb = new XSSFWorkbook(inputStream); + log.info("Generado workbook"); + sheet = wb.getSheet(report.getEntityId()); + if (sheet != null) { + int index = wb.getSheetIndex(sheet); + wb.removeSheetAt(index); + FileOutputStream out; + out = new FileOutputStream(filePath + fileName); + wb.write(out); + out.close(); + + }else { + log.info("No existe el informe "+report.getEntityId()); + } + inputStream.close(); + }catch (Exception e) { + + e.printStackTrace(); + } } } diff --git a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java index e78f65fb..a83bed78 100644 --- a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportManagerTest.java @@ -8,6 +8,8 @@ */ package us.muit.fs.a4i.test.persistence; +import static org.junit.jupiter.api.Assertions.fail; + import java.io.File; import java.util.ArrayList; import java.util.Date; @@ -49,6 +51,7 @@ class ExcelReportManagerTest { private static ReportItem metricIntMock = Mockito.mock(ReportItem.class); @Mock(serializable = true) private static ReportItem metricStrMock = Mockito.mock(ReportItem.class); + @Mock(serializable = true) private static ReportI informe = Mockito.mock(ReportI.class); @@ -142,9 +145,41 @@ void ExcelCreation() { underTest.setFormater(new ReportFormater()); - log.info("El informe tiene el id "+informe.getEntityId()); - underTest.saveReport(informe); + log.info("El informe tiene el id "+informe.getEntityId()); + underTest.saveReport(informe); } + /** + *

Test para el m�todo de eliminar un informe en excel, solo verifica que si es null da excepcion

+ * @author Mariana Reyes Henriquez + */ + @Test + void ExcelDelete() { + excelPath = new String("src" + File.separator + "test" + File.separator + "resources"+File.separator); + excelName= new String("excelTest.xlsx"); + Mockito.when(informe.getEntityId()).thenReturn("entidadTest"); + + underTest=new ExcelReportManager(excelPath,excelName); + underTest.setFormater(new ReportFormater()); + + log.info("El informe tiene el id "+informe.getEntityId()); + underTest.saveReport(informe); + try { + log.info("Se intenta eliminar un informe que no existe"); + underTest.deleteReport(null); + fail("Debería dar excepcion"); + } catch (ReportNotDefinedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + try { + underTest.deleteReport(informe); + } catch (ReportNotDefinedException e) { + fail("No debería dar la excepción"); + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } \ No newline at end of file From 29502eaf86f60dff3ce7bf019d365e90be71c0a5 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Tue, 5 Mar 2024 17:09:31 +0100 Subject: [PATCH 066/100] =?UTF-8?q?Eliminado=20la=20introducci=C3=B3n=20de?= =?UTF-8?q?=20New=20en=20el=20nombre=20del=20fichero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/us/muit/fs/a4i/persistence/ExcelReportManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java index 1f2b312f..27b62b14 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java @@ -205,7 +205,7 @@ public void saveReport(ReportI report) { rowIndex++; } - out = new FileOutputStream(filePath + "NEW" + fileName); + out = new FileOutputStream(filePath + fileName); wb.write(out); out.close(); } catch (Exception e) { From 0532bafb6abfc975f5111596ee213987d4a803a1 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Tue, 5 Mar 2024 20:37:28 +0100 Subject: [PATCH 067/100] Trabajando en los issues 105 a 108 GithubOrganizationEnquirer --- .../remote/GitHubOrganizationEnquirer.java | 195 ++++++++++++++++++ src/main/resources/a4iDefault.json | 19 ++ .../GitHubOrganizationEnquirerTest.java | 154 ++++++++++++++ src/test/resources/excelTest.xlsx | Bin 6196 -> 9089 bytes 4 files changed, 368 insertions(+) create mode 100644 src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java create mode 100644 src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java new file mode 100644 index 00000000..b1e6ef29 --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java @@ -0,0 +1,195 @@ +/** + * + */ +package us.muit.fs.a4i.model.remote; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHTeam; +import org.kohsuke.github.GHUser; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.PagedIterable; +import org.kohsuke.github.GHProject; + + + +import us.muit.fs.a4i.config.MetricConfiguration; +import us.muit.fs.a4i.exceptions.MetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.Report; +import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; +import us.muit.fs.a4i.model.entities.ReportItemI; + +/** + *

+ * Aspectos generales de todos los informes + *

+ *

+ * Todos incluyen un conjunto de métricas de tipo numérico y otro de tipo Date + *

+ * + * @author Isabel Román + * + */ + +public class GitHubOrganizationEnquirer extends GitHubEnquirer { + private static Logger log = Logger.getLogger(Report.class.getName()); + /** + *

+ * Identificador unívoco de la entidad a la que se refire el informe en el + * servidor remoto que se va a utilizar + *

+ */ + private String entityId; + + + public GitHubOrganizationEnquirer() { + super(); + metricNames.add("members"); + metricNames.add("teams"); + metricNames.add("openProjects"); + metricNames.add("closedProjects"); + metricNames.add("repositoriesWithOpenPullRequest"); + metricNames.add("repositories"); + metricNames.add("pullRequest"); + + log.info("A�adidas m�tricas al GHRepositoryEnquirer"); + } + + @Override + public ReportI buildReport(String entityId) { + // TODO Auto-generated method stub + return null; + } + @SuppressWarnings("deprecation") + @Override + public ReportItem getMetric(String metricName, String entityId) throws MetricException { + log.info("Invocado getMetric para buscar "+metricName); + // Todas las métricas son de tipo integer (pero esto debería mirarlo en el fichero de configuración mejor...) + ReportItem metric = null; + List repos = null; + ReportItemBuilder reportBuilder = null; + Map repos2 = null; + List members = null; + List teams = null; + //PagedIterable teams = null; + log.info("????"); + PagedIterable projectsIterableOpen = null; + List projectsOpen = null; + PagedIterable projectsIterableClosed = null; + List projectsClosed = null; + Integer num_members = 0; + + + + try { + GitHub gb = getConnection(); + log.info("???? conexion"); + log.info("Identificador de entidad "+entityId); + GHOrganization remoteOrg = gb.getOrganization(entityId); + log.info("Obteniendo datos de la organización "+remoteOrg.getName()); + + MetricConfiguration metricConfiguration = new MetricConfiguration(); + metricConfiguration.listAllMetrics(); + switch (metricName) { + case "repositoriesWithOpenPullRequest": + log.info("repositoriesWithOpenPullRequest"); + repos = remoteOrg.getRepositoriesWithOpenPullRequests(); + reportBuilder = new ReportItem.ReportItemBuilder("repositoriesWithOpenPullRequest", + repos.size()); + reportBuilder.source("GitHub") + .description("Obtiene el número de repositorios con pull request abiertos."); + metric = reportBuilder.build(); + break; + case "repositories": + log.info("repositories"); + repos2 = remoteOrg.getRepositories(); + reportBuilder = new ReportItem.ReportItemBuilder("repositories", + repos2.size()); + reportBuilder.source("GitHub") + .description("Obtiene el número de repositorios de la organización."); + metric = reportBuilder.build(); + break; + case "pullRequest": + log.info("pullRequest"); + List pull_requests = remoteOrg.getPullRequests(); + reportBuilder = new ReportItem.ReportItemBuilder("pullRequest", + pull_requests.size()); + reportBuilder.source("GitHub") + .description("Obtiene el número total de pull requests abiertos de la organización."); + metric = reportBuilder.build(); + break; + case "members": + log.info("members"); + members = remoteOrg.listMembers().toList(); + log.info(String(members.size())); + + /*for (Object i: members) { + num_members++; + log.info(i); + }*/ + + reportBuilder = new ReportItem.ReportItemBuilder("members", members.size()); + reportBuilder.source("GitHub") + .description("Obtiene el número total de miembros de la organización."); + metric = reportBuilder.build(); + break; + case "teams": + log.info("teams"); + teams = remoteOrg.listTeams().toList();//.listTeams();//.getTeams(); + log.info(String(teams.size())); //.toList().size()); + reportBuilder = new ReportItem.ReportItemBuilder("teams", + teams.size());//teams.toList().size()); + reportBuilder.source("GitHub") + .description("Obtiene el número total de equipos de la organización."); + metric = reportBuilder.build(); + break; + case "OpenProjects": + log.info("OpenProjects"); + projectsIterableOpen = remoteOrg.listProjects(GHProject.ProjectStateFilter.OPEN); + projectsOpen = projectsIterableOpen.toList(); + log.info(projectsOpen.toString()); + reportBuilder = new ReportItem.ReportItemBuilder("openProjects", + projectsOpen.size()); + reportBuilder.source("GitHub") + .description("Obtiene el número total de projectos abiertos."); + metric = reportBuilder.build(); + break; + case "closedProjects": + log.info("closedProjects"); + projectsIterableClosed = remoteOrg.listProjects(GHProject.ProjectStateFilter.CLOSED); + projectsClosed = projectsIterableClosed.toList(); + + log.info(projectsClosed.toString()); + reportBuilder = new ReportItem.ReportItemBuilder("closedProjects", + projectsClosed.size()); + reportBuilder.source("GitHub") + .description("Obtiene el número total de projectos cerrados."); + metric = reportBuilder.build(); + break; + + default: + log.info("NONE"); + } + } + catch (Exception e) { + e.printStackTrace(); + throw new MetricException( + "No se puede acceder a la organización " + entityId + " para consultarlo"); + } + + return metric; + } + private String String(int size) { + // TODO Auto-generated method stub + return null; + } +} diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index 7f703ec7..f5dd3ed3 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -52,7 +52,26 @@ "type": "java.lang.Double", "description": "Numero de issues cerradas", "unit": "issues" + }, + { + "name": "openProjects", + "type": "java.lang.Integer", + "description": "Proyectos abiertos", + "unit": "projects" + }, + { + "name": "repositories", + "type": "java.lang.Integer", + "description": "Número de repositorios", + "unit": "repositories" + }, + { + "name": "repositoriesWithPullRequest", + "type": "java.lang.Integer", + "description": "Número de repositorios", + "unit": "repositories" } + ], "indicators": [{ "name": "issuesProgress", diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java new file mode 100644 index 00000000..66222a64 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java @@ -0,0 +1,154 @@ +package us.muit.fs.a4i.test.model.remote; + +/** + * + */ + + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.logging.Logger; + +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHRepositoryStatistics; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GHRepositoryStatistics.CodeFrequency; +import org.kohsuke.github.GHProject; + + +import org.junit.jupiter.api.Test; +import org.kohsuke.github.GHOrganization; + +import us.muit.fs.a4i.exceptions.MetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.Report; +import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; +import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; +import us.muit.fs.a4i.model.remote.GitHubEnquirer; +import us.muit.fs.a4i.model.remote.GitHubOrganizationEnquirer; + +/** + * @author Roberto Lama + * + */ +public class GitHubOrganizationEnquirerTest { + private static Logger log = Logger.getLogger(GitHubOrganizationEnquirerTest.class.getName()); + + /** + * Test method for + * GitHubOrganizationEnquirer + * @throws MetricException + * @throws ReportItemException + */ + @Test + void testGetPullRequest() throws MetricException, ReportItemException { + GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); + // TEST 2: PullRequest + ReportItem metricsPullRequest = ghEnquirer.getMetric("PullRequest", "MIT-FS"); + log.info(metricsPullRequest.getValue().toString()); + assertEquals(22, metricsPullRequest.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 22 pull requests + } + + + /** + * Test method for + * GitHubOrganizationEnquirer + * @throws MetricException + * @throws ReportItemException + */ + @Test + void testGetRepositories() throws MetricException, ReportItemException { + GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); + // TEST 3: Repositories + ReportItem metricsRepositories = ghEnquirer.getMetric("Repositories", "MIT-FS"); + log.info(metricsRepositories.getValue().toString()); + assertEquals(10,metricsRepositories.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 10 repositorios + } + + + + /** + * Test method for + * GitHubOrganizationEnquirer + * @throws MetricException + * @throws ReportItemException + */ + @Test + void testGetMembers() throws MetricException, ReportItemException { + GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); + // TEST 4: Members + ReportItem metricsMembers = ghEnquirer.getMetric("Members", "MIT-FS"); + log.info(metricsMembers.getValue().toString()); + assertEquals(30,metricsMembers.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 30 miembros + + } + + + /** + * Test method for + * GitHubOrganizationEnquirer + * @throws MetricException + * @throws ReportItemException + */ + @Test + void testGetTeams() throws MetricException, ReportItemException { + GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); + // TEST 5: Teams + ReportItem metricsTeams = ghEnquirer.getMetric("Teams", "MIT-FS"); + log.info(metricsTeams.getValue().toString()); + assertEquals(2,metricsTeams.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 2 teams + } + + /** + * Test method for + * GitHubOrganizationEnquirer + * @throws MetricException + * @throws ReportItemException + */ + @Test + void testGetOpenProjects() throws MetricException, ReportItemException { + GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); + // TEST 6: OpenProjects + ReportItem metricsOpenProjects = ghEnquirer.getMetric("OpenProjects", "MIT-FS"); + log.info(metricsOpenProjects.getValue().toString()); + assertEquals(0,metricsOpenProjects.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 0 proyectos abiertos (classic) + } + + /** + * Test method for + * GitHubOrganizationEnquirer + * @throws MetricException + * @throws ReportItemException + */ + @Test + void testGetClosedProjects() throws MetricException, ReportItemException { + GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); + // TEST 7: ClosedProjects + ReportItem metricsClosedProjects = ghEnquirer.getMetric("ClosedProjects", "MIT-FS"); + log.info(metricsClosedProjects.getValue().toString()); + assertEquals(2,metricsClosedProjects.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 2 proyectos cerrados (classic) + } + + /** + * Test method for + * GitHubOrganizationEnquirer + * @throws MetricException + * @throws ReportItemException + */ + @Test + void testGetRepositoriesWithOpenPullRequest() throws MetricException, ReportItemException { + GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); + + // TEST 1: RepositoriesWithOpenPullRequest + ReportItem metricsRepositoriesWithOpenPullRequest = ghEnquirer.getMetric("RepositoriesWithOpenPullRequest", "MIT-FS"); + log.info(metricsRepositoriesWithOpenPullRequest.getValue().toString()); + assertEquals(5,metricsRepositoriesWithOpenPullRequest.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 4 repositorios con pull requests abiertos + } + +} diff --git a/src/test/resources/excelTest.xlsx b/src/test/resources/excelTest.xlsx index 4b39e1cc0714a93959606588b12a2548dd9d663c..9a9db02efff7a18fe6aa84d462db9838feaf11f7 100644 GIT binary patch delta 5089 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 O+>b7#Z{^C*r~d$2%;zuw delta 3595 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 Date: Tue, 5 Mar 2024 20:41:49 +0100 Subject: [PATCH 068/100] GitHubOrganizationEquirer --- .../a4i/model/remote/GitHubOrganizationEnquirer.java | 4 ++-- .../model/remote/GitHubOrganizationEnquirerTest.java | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java index b1e6ef29..32e5e996 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java @@ -152,8 +152,8 @@ public ReportItem getMetric(String metricName, String entityId) throws .description("Obtiene el número total de equipos de la organización."); metric = reportBuilder.build(); break; - case "OpenProjects": - log.info("OpenProjects"); + case "openProjects": + log.info("openProjects"); projectsIterableOpen = remoteOrg.listProjects(GHProject.ProjectStateFilter.OPEN); projectsOpen = projectsIterableOpen.toList(); log.info(projectsOpen.toString()); diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java index 66222a64..d13d8d9f 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java @@ -50,7 +50,7 @@ public class GitHubOrganizationEnquirerTest { void testGetPullRequest() throws MetricException, ReportItemException { GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); // TEST 2: PullRequest - ReportItem metricsPullRequest = ghEnquirer.getMetric("PullRequest", "MIT-FS"); + ReportItem metricsPullRequest = ghEnquirer.getMetric("pullRequest", "MIT-FS"); log.info(metricsPullRequest.getValue().toString()); assertEquals(22, metricsPullRequest.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 22 pull requests } @@ -83,7 +83,7 @@ void testGetRepositories() throws MetricException, ReportItemException { void testGetMembers() throws MetricException, ReportItemException { GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); // TEST 4: Members - ReportItem metricsMembers = ghEnquirer.getMetric("Members", "MIT-FS"); + ReportItem metricsMembers = ghEnquirer.getMetric("members", "MIT-FS"); log.info(metricsMembers.getValue().toString()); assertEquals(30,metricsMembers.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 30 miembros @@ -100,7 +100,7 @@ void testGetMembers() throws MetricException, ReportItemException { void testGetTeams() throws MetricException, ReportItemException { GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); // TEST 5: Teams - ReportItem metricsTeams = ghEnquirer.getMetric("Teams", "MIT-FS"); + ReportItem metricsTeams = ghEnquirer.getMetric("teams", "MIT-FS"); log.info(metricsTeams.getValue().toString()); assertEquals(2,metricsTeams.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 2 teams } @@ -115,7 +115,7 @@ void testGetTeams() throws MetricException, ReportItemException { void testGetOpenProjects() throws MetricException, ReportItemException { GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); // TEST 6: OpenProjects - ReportItem metricsOpenProjects = ghEnquirer.getMetric("OpenProjects", "MIT-FS"); + ReportItem metricsOpenProjects = ghEnquirer.getMetric("openProjects", "MIT-FS"); log.info(metricsOpenProjects.getValue().toString()); assertEquals(0,metricsOpenProjects.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 0 proyectos abiertos (classic) } @@ -130,7 +130,7 @@ void testGetOpenProjects() throws MetricException, ReportItemException { void testGetClosedProjects() throws MetricException, ReportItemException { GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); // TEST 7: ClosedProjects - ReportItem metricsClosedProjects = ghEnquirer.getMetric("ClosedProjects", "MIT-FS"); + ReportItem metricsClosedProjects = ghEnquirer.getMetric("closedProjects", "MIT-FS"); log.info(metricsClosedProjects.getValue().toString()); assertEquals(2,metricsClosedProjects.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 2 proyectos cerrados (classic) } @@ -146,7 +146,7 @@ void testGetRepositoriesWithOpenPullRequest() throws MetricException, ReportItem GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); // TEST 1: RepositoriesWithOpenPullRequest - ReportItem metricsRepositoriesWithOpenPullRequest = ghEnquirer.getMetric("RepositoriesWithOpenPullRequest", "MIT-FS"); + ReportItem metricsRepositoriesWithOpenPullRequest = ghEnquirer.getMetric("repositoriesWithOpenPullRequest", "MIT-FS"); log.info(metricsRepositoriesWithOpenPullRequest.getValue().toString()); assertEquals(5,metricsRepositoriesWithOpenPullRequest.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 4 repositorios con pull requests abiertos } From 0bfcb0fcc3f84c5eee9bf2041dd459e6d0ce5a5e Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Fri, 8 Mar 2024 12:40:25 +0100 Subject: [PATCH 069/100] Mejoras de GitHubOrganizationEnquirer y sus tests --- .../remote/GitHubOrganizationEnquirer.java | 407 ++++++++++++------ .../remote/GitHubRepositoryEnquirer.java | 10 +- src/main/resources/a4iDefault.json | 42 +- .../GitHubOrganizationEnquirerTest.java | 50 +-- src/test/resources/excelTest.xlsx | Bin 9089 -> 6822 bytes src/test/resources/log.properties | 19 + 6 files changed, 359 insertions(+), 169 deletions(-) create mode 100644 src/test/resources/log.properties diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java index 32e5e996..c56239d0 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java @@ -3,37 +3,37 @@ */ package us.muit.fs.a4i.model.remote; -import java.util.Collection; +import java.io.IOException; + import java.util.List; -import java.util.Map; + import java.util.logging.Logger; + import org.kohsuke.github.GHOrganization; -import org.kohsuke.github.GHPullRequest; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GHTeam; -import org.kohsuke.github.GHUser; + import org.kohsuke.github.GitHub; import org.kohsuke.github.PagedIterable; import org.kohsuke.github.GHProject; -import us.muit.fs.a4i.config.MetricConfiguration; import us.muit.fs.a4i.exceptions.MetricException; + import us.muit.fs.a4i.exceptions.ReportItemException; import us.muit.fs.a4i.model.entities.Report; import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItem; import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; -import us.muit.fs.a4i.model.entities.ReportItemI; + /** *

- * Aspectos generales de todos los informes + * Esta clase permite consultar métricas sobre una organización GitHub *

*

- * Todos incluyen un conjunto de métricas de tipo numérico y otro de tipo Date + * Deuda técnica: sería necesario verificar mejor el funcionamiento de las consultas de proyectos cerrados y abiertos, no parece hacer lo esperado + * Habría que incluir más métricas y algún indicador *

* * @author Isabel Román @@ -41,7 +41,7 @@ */ public class GitHubOrganizationEnquirer extends GitHubEnquirer { - private static Logger log = Logger.getLogger(Report.class.getName()); + private static Logger log = Logger.getLogger(GitHubOrganizationEnquirer.class.getName()); /** *

* Identificador unívoco de la entidad a la que se refire el informe en el @@ -53,143 +53,282 @@ public class GitHubOrganizationEnquirer extends GitHubEnquirer { public GitHubOrganizationEnquirer() { super(); + metricNames.add("repositoriesWithOpenPullRequest"); + metricNames.add("repositories"); + metricNames.add("pullRequests"); metricNames.add("members"); metricNames.add("teams"); metricNames.add("openProjects"); metricNames.add("closedProjects"); - metricNames.add("repositoriesWithOpenPullRequest"); - metricNames.add("repositories"); - metricNames.add("pullRequest"); - - log.info("A�adidas m�tricas al GHRepositoryEnquirer"); + metricNames.add("followers"); + log.info("Incluidos nombres metricas en Enquirer"); } @Override - public ReportI buildReport(String entityId) { - // TODO Auto-generated method stub - return null; - } - @SuppressWarnings("deprecation") - @Override - public ReportItem getMetric(String metricName, String entityId) throws MetricException { - log.info("Invocado getMetric para buscar "+metricName); - // Todas las métricas son de tipo integer (pero esto debería mirarlo en el fichero de configuración mejor...) - ReportItem metric = null; - List repos = null; - ReportItemBuilder reportBuilder = null; - Map repos2 = null; - List members = null; - List teams = null; - //PagedIterable teams = null; - log.info("????"); - PagedIterable projectsIterableOpen = null; - List projectsOpen = null; - PagedIterable projectsIterableClosed = null; - List projectsClosed = null; - Integer num_members = 0; + public ReportI buildReport(String organizationId) { + ReportI report = null; + log.info("Invocado el metodo que construye un informe de organización, para la organizacion "+organizationId); + /** + *

+ * Información sobre la organizacion de GitHub + *

+ */ + GHOrganization organization; + /** + *

+ * En estos momentos cada vez que se invoca construyeObjeto se crea y rellena + * uno nuevo + *

+ *

+ * Deuda técnica: se puede optimizar consultando sólo las diferencias respecto a + * la fecha de la última representación local + *

+ */ + + try { + log.info("Nombre organizacion = " + organizationId); + + GitHub gb = getConnection(); + organization=gb.getOrganization(organizationId); + + log.info("La organizacion es de la empresa " + organization.getCompany() + " fue creada en " + + organization.getCreatedAt()+ " se puede contactar en "+organization.getEmail()); + log.info("leidos datos de la " + organization); + report = new Report(organizationId); + + /** + * Métricas directas de tipo conteo + */ + + + report.addMetric(getMembers(organization)); + log.info("Incluida metrica members "); + + report.addMetric(getTeams(organization)); + log.info("Incluida metrica teams "); + + report.addMetric(getFollowers(organization)); + log.info("Incluida metrica followers "); + - try { - GitHub gb = getConnection(); - log.info("???? conexion"); - log.info("Identificador de entidad "+entityId); - GHOrganization remoteOrg = gb.getOrganization(entityId); - log.info("Obteniendo datos de la organización "+remoteOrg.getName()); + report.addMetric(getPullRequests(organization)); + log.info("Incluida metrica pullRequests "); + + report.addMetric(getRepositories(organization)); + log.info("Incluida metrica repositories "); - MetricConfiguration metricConfiguration = new MetricConfiguration(); - metricConfiguration.listAllMetrics(); - switch (metricName) { - case "repositoriesWithOpenPullRequest": - log.info("repositoriesWithOpenPullRequest"); - repos = remoteOrg.getRepositoriesWithOpenPullRequests(); - reportBuilder = new ReportItem.ReportItemBuilder("repositoriesWithOpenPullRequest", - repos.size()); - reportBuilder.source("GitHub") - .description("Obtiene el número de repositorios con pull request abiertos."); - metric = reportBuilder.build(); - break; - case "repositories": - log.info("repositories"); - repos2 = remoteOrg.getRepositories(); - reportBuilder = new ReportItem.ReportItemBuilder("repositories", - repos2.size()); - reportBuilder.source("GitHub") - .description("Obtiene el número de repositorios de la organización."); - metric = reportBuilder.build(); - break; - case "pullRequest": - log.info("pullRequest"); - List pull_requests = remoteOrg.getPullRequests(); - reportBuilder = new ReportItem.ReportItemBuilder("pullRequest", - pull_requests.size()); - reportBuilder.source("GitHub") - .description("Obtiene el número total de pull requests abiertos de la organización."); - metric = reportBuilder.build(); - break; - case "members": - log.info("members"); - members = remoteOrg.listMembers().toList(); - log.info(String(members.size())); - - /*for (Object i: members) { - num_members++; - log.info(i); - }*/ - - reportBuilder = new ReportItem.ReportItemBuilder("members", members.size()); - reportBuilder.source("GitHub") - .description("Obtiene el número total de miembros de la organización."); - metric = reportBuilder.build(); - break; - case "teams": - log.info("teams"); - teams = remoteOrg.listTeams().toList();//.listTeams();//.getTeams(); - log.info(String(teams.size())); //.toList().size()); - reportBuilder = new ReportItem.ReportItemBuilder("teams", - teams.size());//teams.toList().size()); - reportBuilder.source("GitHub") - .description("Obtiene el número total de equipos de la organización."); - metric = reportBuilder.build(); - break; - case "openProjects": - log.info("openProjects"); - projectsIterableOpen = remoteOrg.listProjects(GHProject.ProjectStateFilter.OPEN); - projectsOpen = projectsIterableOpen.toList(); - log.info(projectsOpen.toString()); - reportBuilder = new ReportItem.ReportItemBuilder("openProjects", - projectsOpen.size()); - reportBuilder.source("GitHub") - .description("Obtiene el número total de projectos abiertos."); - metric = reportBuilder.build(); - break; - case "closedProjects": - log.info("closedProjects"); - projectsIterableClosed = remoteOrg.listProjects(GHProject.ProjectStateFilter.CLOSED); - projectsClosed = projectsIterableClosed.toList(); + report.addMetric(getRepositoriesWithOpenPullRequest(organization)); + log.info("Incluida metrica repositoriesWithPullRequest "); + + + report.addMetric(getOpenProjects(organization)); + log.info("Incluida metrica openProjects "); - log.info(projectsClosed.toString()); - reportBuilder = new ReportItem.ReportItemBuilder("closedProjects", - projectsClosed.size()); - reportBuilder.source("GitHub") - .description("Obtiene el número total de projectos cerrados."); - metric = reportBuilder.build(); - break; - - default: - log.info("NONE"); - } + report.addMetric(getClosedProjects(organization)); + log.info("Incluida metrica closedProjects "); + + + } catch (Exception e) { + log.severe("Problemas en la conexión " + e); } - catch (Exception e) { - e.printStackTrace(); - throw new MetricException( - "No se puede acceder a la organización " + entityId + " para consultarlo"); - } - + + return report; + } +/** + * Permite consultar desde fuera una única métrica de la organización con el id que se pase como parámetro + */ + @Override + public ReportItem getMetric(String metricName, String organizationId) throws MetricException { + log.info("Invocado getMetric para buscar "+metricName); + GHOrganization organization; + + GitHub gb = getConnection(); + try { + organization = gb.getOrganization(organizationId); + } catch (Exception e) { + e.printStackTrace(); + throw new MetricException( + "No se puede acceder a la organizacion remota " + organizationId + " para recuperarla"); + } + + return getMetric(metricName, organization); + } + + /** + *

+ * Crea la métrica solicitada consultando la organizacion que se pasa como + * parámetro + *

+ * + * @param metricName Métrica solicitada + * @param organization Organizacion + * @return La métrica creada + * @throws MetricException Si la métrica no está definida se lanzará una + * excepción + */ + private ReportItem getMetric(String metricName, GHOrganization organization) throws MetricException { + ReportItem metric=null; + if (organization == null) { + throw new MetricException("Intenta obtener una métrica sin haber obtenido los datos de la organizacion"); + } + switch (metricName) { + case "repositoriesWithOpenPullRequest": + metric=getRepositoriesWithOpenPullRequest(organization); + break; + case "repositories": + metric=getRepositories(organization); + break; + case "pullRequests": + metric=getPullRequests(organization); + break; + case "members": + metric=getMembers(organization); + break; + case "teams": + metric=getTeams(organization); + break; + case "openProjects": + metric=getOpenProjects(organization); + break; + case "closedProjects": + metric=getClosedProjects(organization); + break; + case "followers": + metric=getFollowers(organization); + break; + default: + throw new MetricException("La métrica " + metricName + " no está definida para un repositorio"); + } + return metric; } - private String String(int size) { - // TODO Auto-generated method stub - return null; + private ReportItem getRepositoriesWithOpenPullRequest(GHOrganization organization) { + log.info("Consultando los repositorios con pull requests abiertos"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("repositoriesWithOpenPullRequest", + organization.getRepositoriesWithOpenPullRequests().size()); + builder.source("GitHub"); + } catch (ReportItemException | IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getRepositories(GHOrganization organization) { + log.info("Consultando los repositorios"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("repositories", + organization.getPublicRepoCount()); + builder.source("GitHub"); + } catch (ReportItemException | IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getMembers(GHOrganization organization) { + log.info("Consultando los miembros"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("members", + organization.listMembers().toList().size()); + builder.source("GitHub"); + } catch (ReportItemException | IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getTeams(GHOrganization organization) { + log.info("Consultando los equipos"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("teams", + organization.getTeams().size()); + builder.source("GitHub"); + } catch (ReportItemException | IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getFollowers(GHOrganization organization) { + log.info("Consultando los seguidores"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("followers", + organization.getFollowersCount()); + builder.source("GitHub"); + } catch (ReportItemException | IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getPullRequests(GHOrganization organization) { + log.info("Consultando los pull requests"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("pullRequests", + organization.getPullRequests().size()); + builder.source("GitHub"); + } catch (ReportItemException | IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getOpenProjects(GHOrganization organization) { + log.info("Consultando los proyectos abiertos"); + ReportItemBuilder builder=null; + try { + PagedIterable pagina=organization.listProjects(GHProject.ProjectStateFilter.OPEN); + + List proyectos=pagina.toList(); + builder = new ReportItem.ReportItemBuilder("openProjects", + proyectos.size()); + + log.info("Proyectos "+proyectos); + for(GHProject pro:proyectos) { + log.info("Proyecto "+pro.getName()+" en estado "+pro.getState()); + } + builder.source("GitHub"); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getClosedProjects(GHOrganization organization) { + log.info("Consultando los proyectos cerrados"); + ReportItemBuilder builder=null; + try { +PagedIterable pagina=organization.listProjects(GHProject.ProjectStateFilter.CLOSED); + + List proyectos=pagina.toList(); + builder = new ReportItem.ReportItemBuilder("closedProjects", + proyectos.size()); + log.info("Proyectos "+proyectos); + for(GHProject pro:proyectos) { + log.info("Proyecto "+pro.getName()+" en estado "+pro.getState()); + } + builder.source("GitHub"); + } catch (ReportItemException | IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); } + } diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index 2e9f7165..b4061de7 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -22,6 +22,7 @@ /** * @author Isabel Román + * Deuda técnica: debería seguir la misma filosofía que GitHubOrganizationEnquirer para evitar la replicación de código * */ public class GitHubRepositoryEnquirer extends GitHubEnquirer { @@ -75,12 +76,7 @@ public ReportI buildReport(String repositoryId) { /** * Métricas directas de tipo conteo */ - - /* - * MetricBuilder subscribers = new - * Metric.MetricBuilder("subscribers", - * remoteRepo.getSubscribersCount()); - */ + ReportItemBuilder subscribers = new ReportItem.ReportItemBuilder("subscribers", remoteRepo.getSubscribersCount()); subscribers.source("GitHub"); @@ -170,7 +166,7 @@ public ReportI buildReport(String repositoryId) { } /** - * Permite consultar desde fuera una métrica del repositorio indicado + * Permite consultar desde fuera una única métrica del repositorio indicado */ @Override diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index f5dd3ed3..59ca7b7b 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -2,9 +2,21 @@ "metrics": [{ "name": "subscribers", "type": "java.lang.Integer", - "description": "Número de suscriptores, watchers en la web", + "description": "Número de suscriptores de un repositorio, watchers en la web", "unit": "subscribers" }, + { + "name": "members", + "type": "java.lang.Integer", + "description": "Miembros de una organización", + "unit": "members" + }, + { + "name": "teams", + "type": "java.lang.Integer", + "description": "Equipos de una organización", + "unit": "teams" + }, { "name": "forks", "type": "java.lang.Integer", @@ -14,15 +26,27 @@ { "name": "watchers", "type": "java.lang.Integer", - "description": "Observadores, en la web aparece com forks", + "description": "Observadores de un repositorio, en la web aparece com forks", "unit": "watchers" }, + { + "name": "followers", + "type": "java.lang.Integer", + "description": "Seguidores de una organización", + "unit": "followers" + }, { "name": "issues", "type": "java.lang.Integer", "description": "Tareas sin finalizar en el repositorio", "unit": "issues" }, + { + "name": "pullRequests", + "type": "java.lang.Integer", + "description": "Número de pull requests", + "unit": "pull requests" + }, { "name": "stars", "type": "java.lang.Integer", @@ -59,6 +83,12 @@ "description": "Proyectos abiertos", "unit": "projects" }, + { + "name": "closedProjects", + "type": "java.lang.Integer", + "description": "Proyectos cerrados", + "unit": "projects" + }, { "name": "repositories", "type": "java.lang.Integer", @@ -68,7 +98,13 @@ { "name": "repositoriesWithPullRequest", "type": "java.lang.Integer", - "description": "Número de repositorios", + "description": "Número de repositorios con pull requests", + "unit": "repositories" + }, + { + "name": "repositoriesWithOpenPullRequest", + "type": "java.lang.Integer", + "description": "Número de repositorios con pull requests pendientes", "unit": "repositories" } diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java index d13d8d9f..f67914f4 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java @@ -39,6 +39,7 @@ */ public class GitHubOrganizationEnquirerTest { private static Logger log = Logger.getLogger(GitHubOrganizationEnquirerTest.class.getName()); + GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); /** * Test method for @@ -48,11 +49,11 @@ public class GitHubOrganizationEnquirerTest { */ @Test void testGetPullRequest() throws MetricException, ReportItemException { - GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); + // TEST 2: PullRequest - ReportItem metricsPullRequest = ghEnquirer.getMetric("pullRequest", "MIT-FS"); - log.info(metricsPullRequest.getValue().toString()); - assertEquals(22, metricsPullRequest.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 22 pull requests + ReportItem metricsPullRequest = ghEnquirer.getMetric("pullRequests", "MIT-FS"); + + assertEquals(21, metricsPullRequest.getValue().intValue(), "el número de pullRequests no es el esperado"); // Tiene 22 pull requests } @@ -64,11 +65,11 @@ void testGetPullRequest() throws MetricException, ReportItemException { */ @Test void testGetRepositories() throws MetricException, ReportItemException { - GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); + // TEST 3: Repositories - ReportItem metricsRepositories = ghEnquirer.getMetric("Repositories", "MIT-FS"); - log.info(metricsRepositories.getValue().toString()); - assertEquals(10,metricsRepositories.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 10 repositorios + ReportItem metricsRepositories = ghEnquirer.getMetric("repositories", "MIT-FS"); + + assertEquals(12,metricsRepositories.getValue().intValue(), "El número de repositorios no es el esperado"); // Tiene 10 repositorios } @@ -81,11 +82,11 @@ void testGetRepositories() throws MetricException, ReportItemException { */ @Test void testGetMembers() throws MetricException, ReportItemException { - GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); + // TEST 4: Members ReportItem metricsMembers = ghEnquirer.getMetric("members", "MIT-FS"); - log.info(metricsMembers.getValue().toString()); - assertEquals(30,metricsMembers.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 30 miembros + + assertEquals(29,metricsMembers.getValue().intValue(), "El número de miembros no es el esperado"); // Tiene 30 miembros } @@ -98,11 +99,11 @@ void testGetMembers() throws MetricException, ReportItemException { */ @Test void testGetTeams() throws MetricException, ReportItemException { - GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); + // TEST 5: Teams ReportItem metricsTeams = ghEnquirer.getMetric("teams", "MIT-FS"); - log.info(metricsTeams.getValue().toString()); - assertEquals(2,metricsTeams.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 2 teams + + assertEquals(2,metricsTeams.getValue().intValue(), "El número equipos no es el esperado"); // Tiene 2 teams } /** @@ -113,11 +114,10 @@ void testGetTeams() throws MetricException, ReportItemException { */ @Test void testGetOpenProjects() throws MetricException, ReportItemException { - GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); + // TEST 6: OpenProjects - ReportItem metricsOpenProjects = ghEnquirer.getMetric("openProjects", "MIT-FS"); - log.info(metricsOpenProjects.getValue().toString()); - assertEquals(0,metricsOpenProjects.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 0 proyectos abiertos (classic) + ReportItem op = ghEnquirer.getMetric("openProjects", "MIT-FS"); + assertEquals(0,op.getValue().intValue(),"El número de proyectos abiertos no es el esperado"); } /** @@ -128,11 +128,11 @@ void testGetOpenProjects() throws MetricException, ReportItemException { */ @Test void testGetClosedProjects() throws MetricException, ReportItemException { - GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); + // TEST 7: ClosedProjects ReportItem metricsClosedProjects = ghEnquirer.getMetric("closedProjects", "MIT-FS"); - log.info(metricsClosedProjects.getValue().toString()); - assertEquals(2,metricsClosedProjects.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 2 proyectos cerrados (classic) + + assertEquals(2,metricsClosedProjects.getValue().intValue(), "El número de proyectos cerrados no es el esperado"); } /** @@ -143,12 +143,12 @@ void testGetClosedProjects() throws MetricException, ReportItemException { */ @Test void testGetRepositoriesWithOpenPullRequest() throws MetricException, ReportItemException { - GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); + // TEST 1: RepositoriesWithOpenPullRequest ReportItem metricsRepositoriesWithOpenPullRequest = ghEnquirer.getMetric("repositoriesWithOpenPullRequest", "MIT-FS"); - log.info(metricsRepositoriesWithOpenPullRequest.getValue().toString()); - assertEquals(5,metricsRepositoriesWithOpenPullRequest.getValue(), "Debería tener el valor especificado en el mock"); // Tiene 4 repositorios con pull requests abiertos - } + + assertEquals(6,metricsRepositoriesWithOpenPullRequest.getValue().intValue(), "El número de repositorios con pull requests abiertos no es el esperado"); + } } diff --git a/src/test/resources/excelTest.xlsx b/src/test/resources/excelTest.xlsx index 9a9db02efff7a18fe6aa84d462db9838feaf11f7..c105c068a4a76baae82e62168d25e640e9a604ad 100644 GIT binary patch literal 6822 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 Date: Fri, 8 Mar 2024 13:45:25 +0100 Subject: [PATCH 070/100] Mejoras en GitHubRepositoryEnquirer --- .../remote/GitHubRepositoryEnquirer.java | 274 ++++++++++++------ src/main/resources/a4iDefault.json | 6 + ...ExcelReportWithRepositoryEnquirerTest.java | 30 ++ src/test/resources/excelTest.xlsx | Bin 6822 -> 7969 bytes src/test/resources/log.properties | 4 +- 5 files changed, 228 insertions(+), 86 deletions(-) create mode 100644 src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportWithRepositoryEnquirerTest.java diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index b4061de7..4002f95d 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.logging.Logger; +import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHRepositoryStatistics; import org.kohsuke.github.GHRepositoryStatistics.CodeFrequency; @@ -39,19 +40,26 @@ public GitHubRepositoryEnquirer() { metricNames.add("subscribers"); metricNames.add("forks"); metricNames.add("watchers"); + metricNames.add("starts"); + metricNames.add("issues"); + metricNames.add("creation"); + metricNames.add("lastUpdated"); + metricNames.add("lastPush"); + metricNames.add("totalAdditions"); + metricNames.add("totalDeletions"); log.info("A�adidas m�tricas al GHRepositoryEnquirer"); } @Override public ReportI buildReport(String repositoryId) { - ReportI myRepo = null; + ReportI report = null; log.info("Invocado el m�todo que construye un objeto RepositoryReport"); /** *

* Información sobre el repositorio obtenida de GitHub *

*/ - GHRepository remoteRepo; + GHRepository repo; /** *

* En estos momentos cada vez que se invoca construyeObjeto se crea y rellena @@ -67,102 +75,62 @@ public ReportI buildReport(String repositoryId) { log.info("Nombre repo = " + repositoryId); GitHub gb = getConnection(); - remoteRepo = gb.getRepository(repositoryId); - log.info("El repositorio es de " + remoteRepo.getOwnerName() + " Y su descripción es " - + remoteRepo.getDescription()); - log.info("leído " + remoteRepo); - myRepo = new Report(repositoryId); + repo = gb.getRepository(repositoryId); + log.info("El repositorio es de " + repo.getOwnerName() + " Y su descripción es " + + repo.getDescription()); + log.info("leído " + repo); + report = new Report(repositoryId); /** * Métricas directas de tipo conteo */ - ReportItemBuilder subscribers = new ReportItem.ReportItemBuilder("subscribers", - remoteRepo.getSubscribersCount()); - subscribers.source("GitHub"); - myRepo.addMetric(subscribers.build()); - log.info("Añadida métrica suscriptores " + subscribers); - - /* - * MetricBuilder forks = new Metric.MetricBuilder("forks", - * remoteRepo.getForksCount()); forks.source("GitHub"); - */ - ReportItemBuilder forks = new ReportItem.ReportItemBuilder("forks", - remoteRepo.getForksCount()); - forks.source("GitHub"); - myRepo.addMetric(forks.build()); - log.info("Añadida métrica forks " + forks); - - /* - * MetricBuilder watchers = new - * Metric.MetricBuilder("watchers", remoteRepo.getWatchersCount()); - */ - ReportItemBuilder watchers = new ReportItem.ReportItemBuilder("watchers", - remoteRepo.getWatchersCount()); - watchers.source("GitHub"); - myRepo.addMetric(watchers.build()); - - ReportItemBuilder stars = new ReportItem.ReportItemBuilder("stars", - remoteRepo.getStargazersCount()); - stars.source("GitHub"); - myRepo.addMetric(stars.build()); - - ReportItemBuilder issues = new ReportItem.ReportItemBuilder("issues", - remoteRepo.getOpenIssueCount()); - issues.source("GitHub"); - myRepo.addMetric(issues.build()); + + report.addMetric(getSubscribers(repo)); + log.info("Incluida metrica suscribers "); + + report.addMetric(getForks(repo)); + log.info("Incluida metrica forks "); + + report.addMetric(getWatchers(repo)); + log.info("Incluida metrica watchers "); + + report.addMetric(getStars(repo)); + log.info("Incluida metrica stars "); + + report.addMetric(getIssues(repo)); + log.info("Incluida metrica issues "); + + + /** * Métricas directas de tipo fecha */ - - ReportItemBuilder creation = new ReportItem.ReportItemBuilder("creation", - remoteRepo.getCreatedAt()); - creation.source("GitHub"); - myRepo.addMetric(creation.build()); - - ReportItemBuilder push = new ReportItem.ReportItemBuilder("lastPush", remoteRepo.getPushedAt()); - push.description("Último push realizado en el repositorio").source("GitHub"); - myRepo.addMetric(push.build()); - - ReportItemBuilder updated = new ReportItem.ReportItemBuilder("lastUpdated", - remoteRepo.getUpdatedAt()); - push.description("Última actualización").source("GitHub"); - myRepo.addMetric(updated.build()); + report.addMetric(getCreation(repo)); + log.info("Incluida metrica creation "); + + report.addMetric(getLastPush(repo)); + log.info("Incluida metrica lastPush "); + + report.addMetric(getLastUpdated(repo)); + log.info("Incluida metrica lastUpdates "); + /** * Métricas más elaboradas, requieren más "esfuerzo" */ - - GHRepositoryStatistics data = remoteRepo.getStatistics(); - List codeFreq = data.getCodeFrequency(); - int additions = 0; - int deletions = 0; - for (CodeFrequency freq : codeFreq) { - - if ((freq.getAdditions() != 0) || (freq.getDeletions() != 0)) { - Date fecha = new Date((long) freq.getWeekTimestamp() * 1000); - log.info("Fecha modificaciones " + fecha); - additions += freq.getAdditions(); - deletions += freq.getDeletions(); - } - - } - ReportItemBuilder totalAdditions = new ReportItem.ReportItemBuilder("totalAdditions", - additions); - totalAdditions.source("GitHub, calculada") - .description("Suma el total de adiciones desde que el repositorio se creó"); - myRepo.addMetric(totalAdditions.build()); - - ReportItemBuilder totalDeletions = new ReportItem.ReportItemBuilder("totalDeletions", - deletions); - totalDeletions.source("GitHub, calculada") - .description("Suma el total de borrados desde que el repositorio se creó"); - myRepo.addMetric(totalDeletions.build()); - + + report.addMetric(getTotalAdditions(repo)); + log.info("Incluida metrica totalAdditions "); + + report.addMetric(getTotalDeletions(repo)); + log.info("Incluida metrica totalDeletions "); + + } catch (Exception e) { log.severe("Problemas en la conexión " + e); } - return myRepo; + return report; } /** @@ -209,6 +177,30 @@ private ReportItem getMetric(String metricName, GHRepository remoteRepo) throws case "totalDeletions": metric = getTotalDeletions(remoteRepo); break; + case "starts": + metric = getStars(remoteRepo); + break; + case "forks": + metric = getForks(remoteRepo); + break; + case "watchers": + metric = getWatchers(remoteRepo); + break; + case "subscribers": + metric = getSubscribers(remoteRepo); + break; + case "issues": + metric = getIssues(remoteRepo); + break; + case "creation": + metric = getCreation(remoteRepo); + break; + case "lastUpdated": + metric = getLastUpdated(remoteRepo); + break; + case "lastPush": + metric = getLastPush(remoteRepo); + break; default: throw new MetricException("La métrica " + metricName + " no está definida para un repositorio"); } @@ -310,5 +302,119 @@ private ReportItem getTotalDeletions(GHRepository remoteRepo) throws MetricExcep return metric; } - + + private ReportItem getSubscribers(GHRepository repo) { + log.info("Consultando los subscriptores"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("subscribers", + repo.getSubscribersCount()); + builder.source("GitHub"); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getForks(GHRepository repo) { + log.info("Consultando los forks"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("forks", + repo.getForksCount()); + builder.source("GitHub"); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getWatchers(GHRepository repo) { + log.info("Consultando los watchers"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("watchers", + repo.getWatchersCount()); + builder.source("GitHub"); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getStars(GHRepository repo) { + log.info("Consultando las starts"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("stars", + repo.getStargazersCount()); + builder.description("Numero de estrellas").source("GitHub"); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getIssues(GHRepository repo) { + log.info("Consultando los issues"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("issues", + repo.getOpenIssueCount()); + builder.description("Numero de asuntos abiertos").source("GitHub"); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + private ReportItem getCreation(GHRepository repo) { + log.info("Consultando los watchers"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("creation", + repo.getCreatedAt()); + builder.description("Fecha de creación del repositorio").source("GitHub"); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getLastPush(GHRepository repo) { + log.info("Consultando el ultimo push"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("lastPush", + repo.getPushedAt()); + builder.description("Último push realizado en el repositorio").source("GitHub"); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getLastUpdated(GHRepository repo) { + log.info("Consultando la ultima actualización"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("lastUpdated", + repo.getUpdatedAt()); + builder.description("Última actualización").source("GitHub"); + + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + + } diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index 59ca7b7b..7d8f4251 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -65,6 +65,12 @@ "description": "Último push realizado en el repositorio", "unit": "date" }, + { + "name": "lastUpdated", + "type": "java.util.Date", + "description": "Última actualización realizada en el repositorio", + "unit": "date" + }, { "name": "openIssues", "type": "java.lang.Double", diff --git a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportWithRepositoryEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportWithRepositoryEnquirerTest.java new file mode 100644 index 00000000..f3c5e88f --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportWithRepositoryEnquirerTest.java @@ -0,0 +1,30 @@ +package us.muit.fs.a4i.test.persistence; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.util.logging.Logger; + +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.model.remote.GitHubRepositoryEnquirer; +import us.muit.fs.a4i.persistence.ExcelReportManager; +import us.muit.fs.a4i.persistence.ReportFormater; + +class ExcelReportWithRepositoryEnquirerTest { + private static Logger log = Logger.getLogger(ExcelReportWithRepositoryEnquirerTest.class.getName()); + @Test + void testSaveRepositoryReport() { + String excelPath = new String("src" + File.separator + "test" + File.separator + "resources"+File.separator); + String excelName= new String("excelTest.xlsx"); + String repoName = new String("MIT-FS/Audit4Improve-API"); + ExcelReportManager underTest=new ExcelReportManager(excelPath,excelName); + + underTest.setFormater(new ReportFormater()); + + GitHubRepositoryEnquirer ghEnquirer = new GitHubRepositoryEnquirer(); + + underTest.saveReport(ghEnquirer.buildReport(repoName)); + } + +} diff --git a/src/test/resources/excelTest.xlsx b/src/test/resources/excelTest.xlsx index c105c068a4a76baae82e62168d25e640e9a604ad..8f9c5e5fa024103ca981a3c184e7712d810fc6a9 100644 GIT binary patch delta 4815 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 Date: Fri, 8 Mar 2024 14:41:38 +0100 Subject: [PATCH 071/100] 101 a 104: Cambios en GitHubRepositoryEnquierer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Falta la métrica de commits en el último sprint Es necesario mejorar los tests --- .../remote/GitHubRepositoryEnquirer.java | 127 +++++++++++++++--- src/main/resources/a4iDefault.json | 36 ++++- src/test/resources/excelTest.xlsx | Bin 7969 -> 8577 bytes src/test/resources/log.properties | 3 +- 4 files changed, 141 insertions(+), 25 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index 4002f95d..511c46bd 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.logging.Logger; +import org.kohsuke.github.GHIssueState; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHRepositoryStatistics; @@ -42,11 +43,15 @@ public GitHubRepositoryEnquirer() { metricNames.add("watchers"); metricNames.add("starts"); metricNames.add("issues"); + metricNames.add("closedIssues"); + metricNames.add("openIssues"); metricNames.add("creation"); metricNames.add("lastUpdated"); metricNames.add("lastPush"); metricNames.add("totalAdditions"); metricNames.add("totalDeletions"); + metricNames.add("collaborators"); + metricNames.add("ownerCommits"); log.info("A�adidas m�tricas al GHRepositoryEnquirer"); } @@ -80,6 +85,18 @@ public ReportI buildReport(String repositoryId) { + repo.getDescription()); log.info("leído " + repo); report = new Report(repositoryId); + + /** + * Métricas más elaboradas, requieren más "esfuerzo" + */ + + + report.addMetric(getTotalAdditions(repo)); + log.info("Incluida metrica totalAdditions "); + + report.addMetric(getTotalDeletions(repo)); + log.info("Incluida metrica totalDeletions "); + /** * Métricas directas de tipo conteo @@ -88,6 +105,12 @@ public ReportI buildReport(String repositoryId) { report.addMetric(getSubscribers(repo)); log.info("Incluida metrica suscribers "); + + report.addMetric(getCollaborators(repo)); + log.info("Incluida metrica collaborators "); + + report.addMetric(getOwnerCommits(repo)); + log.info("Incluida metrica ownerCommits "); report.addMetric(getForks(repo)); log.info("Incluida metrica forks "); @@ -101,6 +124,13 @@ public ReportI buildReport(String repositoryId) { report.addMetric(getIssues(repo)); log.info("Incluida metrica issues "); + report.addMetric(getOpenIssues(repo)); + log.info("Incluida metrica openIssues "); + + report.addMetric(getClosedIssues(repo)); + log.info("Incluida metrica closedIssues "); + + /** @@ -115,16 +145,8 @@ public ReportI buildReport(String repositoryId) { report.addMetric(getLastUpdated(repo)); log.info("Incluida metrica lastUpdates "); - /** - * Métricas más elaboradas, requieren más "esfuerzo" - */ - - report.addMetric(getTotalAdditions(repo)); - log.info("Incluida metrica totalAdditions "); - report.addMetric(getTotalDeletions(repo)); - log.info("Incluida metrica totalDeletions "); - + } catch (Exception e) { log.severe("Problemas en la conexión " + e); @@ -201,6 +223,18 @@ private ReportItem getMetric(String metricName, GHRepository remoteRepo) throws case "lastPush": metric = getLastPush(remoteRepo); break; + case "collaborators": + metric = getCollaborators(remoteRepo); + break; + case "ownerCommits": + metric = getOwnerCommits(remoteRepo); + break; + case "openIssues": + metric = getOpenIssues(remoteRepo); + break; + case "closedIssues": + metric = getClosedIssues(remoteRepo); + break; default: throw new MetricException("La métrica " + metricName + " no está definida para un repositorio"); } @@ -226,6 +260,7 @@ private ReportItem getTotalAdditions(GHRepository remoteRepo) throws MetricExcep ReportItem metric = null; GHRepositoryStatistics data = remoteRepo.getStatistics(); + List codeFreq; try { codeFreq = data.getCodeFrequency(); @@ -241,19 +276,17 @@ private ReportItem getTotalAdditions(GHRepository remoteRepo) throws MetricExcep } } - ReportItemBuilder totalAdditions = new ReportItem.ReportItemBuilder("totalAdditions", + ReportItemBuilder builder = new ReportItem.ReportItemBuilder("totalAdditions", additions); - totalAdditions.source("GitHub, calculada") + builder.source("GitHub, calculada") .description("Suma el total de adiciones desde que el repositorio se creó"); - metric = totalAdditions.build(); + metric = builder.build(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (ReportItemException e) { + } catch (Exception e) { // TODO Auto-generated catch block + log.warning("Problemas al leer codefrequency en getTotalAdditions"); e.printStackTrace(); - } + } return metric; } @@ -359,11 +392,42 @@ private ReportItem getStars(GHRepository repo) { return builder.build(); } + + private ReportItem getOwnerCommits(GHRepository repo) { + log.info("Consultando los commits del responsable del repositorio"); + ReportItemBuilder builder=null; + GHRepositoryStatistics data = repo.getStatistics(); + + try { + builder = new ReportItem.ReportItemBuilder("ownerCommits", + data.getParticipation().getOwnerCommits().size()); + + builder.description("Commits del responsable").source("GitHub"); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + private ReportItem getIssues(GHRepository repo) { log.info("Consultando los issues"); ReportItemBuilder builder=null; try { builder = new ReportItem.ReportItemBuilder("issues", + repo.getIssues(GHIssueState.ALL).size()); + builder.description("Numero de asuntos totales").source("GitHub"); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + private ReportItem getOpenIssues(GHRepository repo) { + log.info("Consultando los issues abiertos"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("openIssues", repo.getOpenIssueCount()); builder.description("Numero de asuntos abiertos").source("GitHub"); } catch (Exception e) { @@ -372,8 +436,35 @@ private ReportItem getIssues(GHRepository repo) { } return builder.build(); } + private ReportItem getClosedIssues(GHRepository repo) { + log.info("Consultando los issues cerrados"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("closedIssues", + repo.getIssues(GHIssueState.CLOSED).size()); + builder.description("Numero de asuntos cerrados").source("GitHub"); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getCollaborators(GHRepository repo) { + log.info("Consultando los colaboradores"); + ReportItemBuilder builder=null; + try { + builder = new ReportItem.ReportItemBuilder("collaborators", + repo.getCollaborators().size()); + builder.description("Numero de colaboradores en el repositorio").source("GitHub"); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } private ReportItem getCreation(GHRepository repo) { - log.info("Consultando los watchers"); + log.info("Consultando fecha de creación"); ReportItemBuilder builder=null; try { builder = new ReportItem.ReportItemBuilder("creation", diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index 7d8f4251..c90f3b1d 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -5,6 +5,18 @@ "description": "Número de suscriptores de un repositorio, watchers en la web", "unit": "subscribers" }, + { + "name": "collaborators", + "type": "java.lang.Integer", + "description": "Número de colaboradores de un repositorio", + "unit": "collaborators" + }, + { + "name": "ownerCommits", + "type": "java.lang.Integer", + "description": "Número de commits del propietario del repositorio", + "unit": "commits" + }, { "name": "members", "type": "java.lang.Integer", @@ -36,10 +48,16 @@ "unit": "followers" }, { - "name": "issues", + "name": "totalAdditions", "type": "java.lang.Integer", - "description": "Tareas sin finalizar en el repositorio", - "unit": "issues" + "description": "Inserciones desde que se inició el repositorio", + "unit": "additions" + }, + { + "name": "totalDeletions", + "type": "java.lang.Integer", + "description": "Eliminaciones desde que se inició el repositorio", + "unit": "deletions" }, { "name": "pullRequests", @@ -73,14 +91,20 @@ }, { "name": "openIssues", - "type": "java.lang.Double", + "type": "java.lang.Integer", "description": "Numero de issues abiertas", "unit": "issues" }, { "name": "closedIssues", - "type": "java.lang.Double", - "description": "Numero de issues cerradas", + "type": "java.lang.Integer", + "description": "Numero de issues cerrados", + "unit": "issues" + }, + { + "name": "issues", + "type": "java.lang.Integer", + "description": "Tareas totales", "unit": "issues" }, { diff --git a/src/test/resources/excelTest.xlsx b/src/test/resources/excelTest.xlsx index 8f9c5e5fa024103ca981a3c184e7712d810fc6a9..6c3ee61553d196a5d3893c9aa8a81dd7a268131e 100644 GIT binary patch delta 3255 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 E0NqRB;{X5v delta 2631 zcmY*bc{J4f8=eMZNMdY*$&7uHZO{-&$zT|Up+eRO*~2wTe6Pk~1lPQwA@c`b!ue!jVVfDbIA3qHHixQgSaH0=uldDha_xRl$H=}mwP5A&> z%@HT6Dv)-(l%0&VsfZDR$JHa^2Cp5-$y$+0t3K^RE*B>=g*!e6tzCaMqj*j%?cj7v zxb(&Vss2F%`o~~Np+E61I}?bUHnAJ3*%|a5B~|YyYttz9;fuUHAF;wnP+FMxg4y4y58qqt8Jk$jhdfW7iC6u z+L7mU#;radU`zwGhA+EqYUN;gGHus{*OS+{O(nF=^Aw_>EP&e}0|=nJm9@UTM(gZ=pZQ{^YX_b2+G)m6iPCcHe7-O!XtXx4f% zK-6MAF3<`)hsT=XS08vq&c%*q;Bdi!kBZgzv&6U29;NT7*gF-ngrif( zmfXzpRsvt%l?^jhjEQ^|7h237U>EsaaZhPaCb53lp7&yTv!&b#<%(5aScmrWMkmx| zM0K<35Z z5lzwG$Ca0jOk#-HadBWddoHO`lCD)AZNu*-&FQORC_xOn+}vvo7CaW-_l{vmuOjwr z_q1>6$&A~l=o1|>Ojh7q7i2;-iS4&nPC~|IU}R8`64EHevN|`Com3U-pkLorch47p zRY1kaq||OZT2e2a`|d1T3z7<0h{;2{)`Yd~U2lQIekh4hWebb75--bQoYEnkXn~Uz zhb85hiYX4)`Qv1_JzZ)`-FZk}c(m=~N7e0re>9ZGP%Uh46I`xXqMxS}Y+XBjRxb`+ zt&lyrXdc7e=&sRV(L@zBzV$T8QB70w>v78mD%tjb)ZLEdo$$)y3sQ^$KvJDv&b#4H zga+iseG6~$e?l)iYIvx9O!{c*nSge*Ahim~hrdtak}shIuEYC{!18f-5@&zHB5qt8 z%O^`DUJ`HXq<6vyub9kpA>q>EoWpa zaFVwYyZ*kin8&{>1it{(DQ*!P1*&82Ke9%TR#TulH#;53mhp8E)_@b#T1ORG_gPYv+2hG{in)j zHCDWv4KC1hoe2x9I9FXGM7q6yC-wt&D?a@bk>B8o_p6G<;?~c9of6m?EAH6*r(kY7 zXxNO4-1h|+1lw+5BCUh-6&BiL8dt$nXT}*VX7V_AQQXvz6=XfB(fs<3F)XA3*5_)c z4D3mZ7aA+PIf~-~fmnk7FSQp?K?H%R7OG;~-6*HOlPf0vAnvsxo8+a0CHFDF`YTk8m zinds`{$!{yv@K0{8K^NHEDXvFoh!?q24;#h$_@ujT&ud6dG2iT4Z8v6Cw*8aLu6;t zNG&?G9yitssKnTkhYA)OVUyJtZ?Fiysr5-N8;yYZ^N+06@IIsVnPU|ZQKQ!}#Trh@ zlm&oVzYu=3w2QI3SvWHPBM#C;i|5PtiY7Vz4O9RFt8GUeuNG#e%Vd0ZvDTahxEY99 z1;?yYS)zjYT>NTd5K)HzR>HlGIL+Qpre5S)(6CS&!Dgva$1?vl()OfDJHt%$L`vb( zPb4F1v@xb)u24PM`S!uI(T(am^~sqL@+>g!)kKbp|yXP({Z9MgUQT2H_LVJUGh-965H-|T?Nt5#Vim}<3w1@xTmLFrH8OMU~3 zVh+f>0*iDA$$KWGVNz5RP-H((a1K1WB#BQPY?gvDvwV{zuYFfw9Dq6z0}0#+$tFV~ zZ3fESU0j^o;*u9Z3rnrSp&(Q&B;dX3Uku($v*O&={dd!VyfPT#pWtY;;?s3MwbI>$derKM;Zz$2l_hv=RdR>qz;Oc+OLyCp#AXyO5;aK|0+6>Q2`s} z0fC~VKp>I*H6TX;8V)7P+2M;q7?EKhP-K7xIXKk+QgE=pdSqb0{_{Bd4zlw-mi9() zw7EBJUpy3}IY=OA<0t{nxMdUq{8pT1D=W6AO5tBu?0+?UUn2+L3T*?$3> CFp*LK diff --git a/src/test/resources/log.properties b/src/test/resources/log.properties index 2b2cb3d9..e13e2bf0 100644 --- a/src/test/resources/log.properties +++ b/src/test/resources/log.properties @@ -1,8 +1,9 @@ # especificacion de detalle de log # nivel de log global -.level = ALL +.level = OFF # javax.json.Json.level = FINEST us.muit.fs.a4i.model.remote.GitHubRepositoryEnquirer.level = INFO +us.muit.fs.a4i.persistence.ExcelReportManager.level = ALL # nivel clases From 536191311f725b7f820e0f09f720827fb9a69f77 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Fri, 8 Mar 2024 19:33:25 +0100 Subject: [PATCH 072/100] Arreglando javadoc --- .../us/muit/fs/a4i/config/package-info.java | 2 +- .../fs/a4i/control/IndicatorStrategy.java | 6 +- .../us/muit/fs/a4i/control/package-info.java | 8 +- .../muit/fs/a4i/exceptions/package-info.java | 5 +- .../fs/a4i/model/entities/package-info.java | 2 +- .../remote/GitHubRepositoryEnquirer.java | 79 ++++++++++++++++--- .../fs/a4i/model/remote/package-info.java | 4 +- .../muit/fs/a4i/persistence/package-info.java | 7 +- .../muit/fs/a4i/test/config/package-info.java | 4 +- .../fs/a4i/test/control/package-info.java | 2 +- .../a4i/test/model/entities/package-info.java | 2 +- .../a4i/test/model/remote/package-info.java | 2 +- .../fs/a4i/test/persistence/package-info.java | 2 +- 13 files changed, 95 insertions(+), 30 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/config/package-info.java b/src/main/java/us/muit/fs/a4i/config/package-info.java index d768c7f5..7ec62cf2 100644 --- a/src/main/java/us/muit/fs/a4i/config/package-info.java +++ b/src/main/java/us/muit/fs/a4i/config/package-info.java @@ -10,6 +10,6 @@ * alt="Paquete para la configuración" height="25%" width="50%"> * * @author Isabel Román - * @version 0.2 + * @version V.0.2 */ package us.muit.fs.a4i.config; diff --git a/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java b/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java index 37aebfd4..634dad0a 100644 --- a/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java @@ -18,7 +18,7 @@ public interface IndicatorStrategy { /** - * Calcula un indicador a partir de las m�tricas proporcionadas. + * Calcula un indicador a partir de las métricas proporcionadas. * @param * @param metrics * @throws NotAvailableMetricException @@ -28,8 +28,8 @@ public interface IndicatorStrategy { /** - * Obtiene las m�tricas necesarias - * @return listado de m�tricas + * Obtiene las métricas necesarias + * @return listado de métricas */ public List requiredMetrics(); diff --git a/src/main/java/us/muit/fs/a4i/control/package-info.java b/src/main/java/us/muit/fs/a4i/control/package-info.java index 737ce078..2966d62a 100644 --- a/src/main/java/us/muit/fs/a4i/control/package-info.java +++ b/src/main/java/us/muit/fs/a4i/control/package-info.java @@ -1,7 +1,11 @@ /** + *

* Clases e interfaces controladoras - * + *

+ *Paquete para clases controladoras * @author Isabel Román - * @version 0.2 + * @version V.0.2 */ package us.muit.fs.a4i.control; diff --git a/src/main/java/us/muit/fs/a4i/exceptions/package-info.java b/src/main/java/us/muit/fs/a4i/exceptions/package-info.java index 1703dbf5..868de094 100644 --- a/src/main/java/us/muit/fs/a4i/exceptions/package-info.java +++ b/src/main/java/us/muit/fs/a4i/exceptions/package-info.java @@ -2,11 +2,8 @@ *

* Excepciones propias de la aplicación *

- * Paquete para excepciones * * @author Isabel Román - * @version 0.0 + * @version V.0.2 */ package us.muit.fs.a4i.exceptions; diff --git a/src/main/java/us/muit/fs/a4i/model/entities/package-info.java b/src/main/java/us/muit/fs/a4i/model/entities/package-info.java index ba5ae06a..2ee672c3 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/package-info.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/package-info.java @@ -8,7 +8,7 @@ * alt="Paquete de entidades" height="50%" width="75%"> * * @author Isabel Román - * @version 0.0 + * @version V.0.2 */ package us.muit.fs.a4i.model.entities; \ No newline at end of file diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index 511c46bd..2d0e2e56 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -28,6 +28,9 @@ * */ public class GitHubRepositoryEnquirer extends GitHubEnquirer { + /** + * para trazar el código + */ private static Logger log = Logger.getLogger(GitHubRepositoryEnquirer.class.getName()); /** @@ -54,7 +57,9 @@ public GitHubRepositoryEnquirer() { metricNames.add("ownerCommits"); log.info("A�adidas m�tricas al GHRepositoryEnquirer"); } - + /** + * Devuelve el informe para el repositorio cuyo id se pasa como parámetro + */ @Override public ReportI buildReport(String repositoryId) { ReportI report = null; @@ -157,6 +162,9 @@ public ReportI buildReport(String repositoryId) { /** * Permite consultar desde fuera una única métrica del repositorio indicado + * @param metricName el nombre de la métrica + * @param repositoryId el id del repositorio + * @return el item para incluir en el informe del repositorio */ @Override @@ -335,7 +343,11 @@ private ReportItem getTotalDeletions(GHRepository remoteRepo) throws MetricExcep return metric; } - + /** + * Devuelve el número de suscriptores + * @param repo repositorio que se consulta + * @return item para el informe + */ private ReportItem getSubscribers(GHRepository repo) { log.info("Consultando los subscriptores"); ReportItemBuilder builder=null; @@ -349,7 +361,11 @@ private ReportItem getSubscribers(GHRepository repo) { } return builder.build(); } - + /** + * Devuelve el número de forks + * @param repo repositorio que se consulta + * @return item para el informe + */ private ReportItem getForks(GHRepository repo) { log.info("Consultando los forks"); ReportItemBuilder builder=null; @@ -363,7 +379,11 @@ private ReportItem getForks(GHRepository repo) { } return builder.build(); } - + /** + * Devuelve los usuarios que observan el repositorio + * @param repo repositorio que se consulta + * @return item para el informe + */ private ReportItem getWatchers(GHRepository repo) { log.info("Consultando los watchers"); ReportItemBuilder builder=null; @@ -377,7 +397,11 @@ private ReportItem getWatchers(GHRepository repo) { } return builder.build(); } - + /** + * Devuelve el número de estrellas + * @param repo repositorio que se consulta + * @return item para el informe + */ private ReportItem getStars(GHRepository repo) { log.info("Consultando las starts"); ReportItemBuilder builder=null; @@ -392,7 +416,11 @@ private ReportItem getStars(GHRepository repo) { return builder.build(); } - + /** + * Devuelve el número de commits que realiza el responsable del repositorio + * @param repo repositorio que se consulta + * @return item para el informe + */ private ReportItem getOwnerCommits(GHRepository repo) { log.info("Consultando los commits del responsable del repositorio"); ReportItemBuilder builder=null; @@ -409,7 +437,11 @@ private ReportItem getOwnerCommits(GHRepository repo) { } return builder.build(); } - + /** + * Devuelve el número de tickets + * @param repo repositorio que se consulta + * @return item para el informe + */ private ReportItem getIssues(GHRepository repo) { log.info("Consultando los issues"); ReportItemBuilder builder=null; @@ -423,6 +455,11 @@ private ReportItem getIssues(GHRepository repo) { } return builder.build(); } + /** + * Devuelve los issues abiertos en el repositorio que se pasa como parámetro + * @param repo repositorio que se consulta + * @return item para incluir en el informe + */ private ReportItem getOpenIssues(GHRepository repo) { log.info("Consultando los issues abiertos"); ReportItemBuilder builder=null; @@ -436,6 +473,11 @@ private ReportItem getOpenIssues(GHRepository repo) { } return builder.build(); } + /** + * Devuelve los issues cerrados del repositorio + * @param repo repositorio que se consulta + * @return item para incluir en el informe + */ private ReportItem getClosedIssues(GHRepository repo) { log.info("Consultando los issues cerrados"); ReportItemBuilder builder=null; @@ -449,7 +491,11 @@ private ReportItem getClosedIssues(GHRepository repo) { } return builder.build(); } - + /** + * Devuelve el número de colaboradores + * @param repo repositorio que se consulta + * @return item para el informe + */ private ReportItem getCollaborators(GHRepository repo) { log.info("Consultando los colaboradores"); ReportItemBuilder builder=null; @@ -463,6 +509,11 @@ private ReportItem getCollaborators(GHRepository repo) { } return builder.build(); } + /** + * Devuelve la fecha de creación del repositorio + * @param repo + * @return item para el informe + */ private ReportItem getCreation(GHRepository repo) { log.info("Consultando fecha de creación"); ReportItemBuilder builder=null; @@ -476,7 +527,11 @@ private ReportItem getCreation(GHRepository repo) { } return builder.build(); } - + /** + * Devuelve la fecha del último push en el repositorio que se pase como argumento + * @param repo repositorio que se consulta + * @return item para el informe + */ private ReportItem getLastPush(GHRepository repo) { log.info("Consultando el ultimo push"); ReportItemBuilder builder=null; @@ -490,7 +545,11 @@ private ReportItem getLastPush(GHRepository repo) { } return builder.build(); } - + /** + * Fecha de la última actualización + * @param repo repositorio que se consulta + * @return item para incluir en el informe del repositorio + */ private ReportItem getLastUpdated(GHRepository repo) { log.info("Consultando la ultima actualización"); ReportItemBuilder builder=null; diff --git a/src/main/java/us/muit/fs/a4i/model/remote/package-info.java b/src/main/java/us/muit/fs/a4i/model/remote/package-info.java index e5f67e75..fec4adf6 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/package-info.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/package-info.java @@ -4,7 +4,7 @@ * remotos *

*

- * Construyen informes y m�tricas a partir de la información extraída de + * Construyen informes y métricas a partir de la información extraída de * servidores remotos *

* Paquete para la consulta a remotos * * @author Isabel Román - * @version 0.0 + * @version V.0.2 */ package us.muit.fs.a4i.model.remote; \ No newline at end of file diff --git a/src/main/java/us/muit/fs/a4i/persistence/package-info.java b/src/main/java/us/muit/fs/a4i/persistence/package-info.java index 857dfde5..bd66caed 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/package-info.java +++ b/src/main/java/us/muit/fs/a4i/persistence/package-info.java @@ -1,8 +1,13 @@ /** + *

* Este paquete contiene las clases que facilitan el acceso a los datos * persistentes + *

+ * Paquete de persistencia * * @author Isabel Román - * @version 0.2 + * @version V.0.2 */ package us.muit.fs.a4i.persistence; \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/config/package-info.java b/src/test/java/us/muit/fs/a4i/test/config/package-info.java index b17ede77..b80eef2a 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/package-info.java +++ b/src/test/java/us/muit/fs/a4i/test/config/package-info.java @@ -1,8 +1,8 @@ /** - * Este paquete contiene los test para las clases de getión de configuración y + * Este paquete contiene los test para las clases de gestión de configuración y * contexto de la api Audit4Improve * - * @author Isabel Román + * @author Isabel Román * @version 0.0 */ package us.muit.fs.a4i.test.config; \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/control/package-info.java b/src/test/java/us/muit/fs/a4i/test/control/package-info.java index fcdf6f53..39375beb 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/package-info.java +++ b/src/test/java/us/muit/fs/a4i/test/control/package-info.java @@ -2,7 +2,7 @@ * Este paquete contiene las clases para probar los controladores de la api * Audit4Improve * - * @author Isabel Román + * @author Isabel Román * @version 0.0 */ package us.muit.fs.a4i.test.control; \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/model/entities/package-info.java b/src/test/java/us/muit/fs/a4i/test/model/entities/package-info.java index 8cee347d..35d38a83 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/entities/package-info.java +++ b/src/test/java/us/muit/fs/a4i/test/model/entities/package-info.java @@ -2,7 +2,7 @@ * Este paquete contiene las clases para probar las entidades de la api * Audit4Improve * - * @author Isabel Román + * @author Isabel Román * @version 0.0 */ package us.muit.fs.a4i.test.model.entities; \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/package-info.java b/src/test/java/us/muit/fs/a4i/test/model/remote/package-info.java index 3aaf5443..55fee05c 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/remote/package-info.java +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/package-info.java @@ -2,7 +2,7 @@ * Este paquete contiene las clases para probar los objetos de acceso a datos de * la api Audit4Improve * - * @author Isabel Román + * @author Isabel Román * @version 0.0 */ package us.muit.fs.a4i.test.model.remote; \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/persistence/package-info.java b/src/test/java/us/muit/fs/a4i/test/persistence/package-info.java index 86f9567d..6ef560b9 100644 --- a/src/test/java/us/muit/fs/a4i/test/persistence/package-info.java +++ b/src/test/java/us/muit/fs/a4i/test/persistence/package-info.java @@ -2,7 +2,7 @@ * Este paquete contiene las clases para probar las clases relacionadas con el * modelo, en la api Audit4Improve * - * @author Isabel Román + * @author Isabel Román * @version 0.0 */ package us.muit.fs.a4i.test.persistence; \ No newline at end of file From da4d75c2f274b4af62d61447eec359ef51dc971c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Fri, 21 Feb 2025 10:32:05 +0100 Subject: [PATCH 073/100] Indicador 3, rendimiento de un desarrollador MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rendimiento de un desarrollador en comparación con el equipo Tiene problemas, sobre todo no obtiene las métricas bien También se hacen Arreglos Gradle --- build.gradle | 9 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../DeveloperPerformanceStrategy.java | 88 ++++ .../IssuesRatioIndicatorStrategy.java | 15 +- .../model/remote/GitHubDeveloperEnquirer.java | 123 ++++++ .../remote/GitHubRepositoryEnquirer.java | 411 ++++++++++++++---- src/main/resources/a4iDefault.json | 25 ++ .../control/IssuesRatioIndicatorTest.java | 2 +- .../control/RepositoryCalculatorTest.java | 2 +- .../DeveloperPerformanceIndicatorTest.java | 69 +++ .../remote/GitHubDeveloperEnquirerTest.java | 32 ++ .../remote/GitHubRepositoryEnquirerTest.java | 76 ++++ 12 files changed, 749 insertions(+), 105 deletions(-) create mode 100644 src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java rename src/main/java/us/muit/fs/a4i/control/{ => strategies}/IssuesRatioIndicatorStrategy.java (78%) create mode 100644 src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java create mode 100644 src/test/java/us/muit/fs/a4i/test/control/strategies/DeveloperPerformanceIndicatorTest.java create mode 100644 src/test/java/us/muit/fs/a4i/test/model/remote/GitHubDeveloperEnquirerTest.java create mode 100644 src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java diff --git a/build.gradle b/build.gradle index c63df61e..a1ce0aaa 100644 --- a/build.gradle +++ b/build.gradle @@ -16,12 +16,12 @@ plugins { //para poder publicar paquetes en github id 'maven-publish' //Plugin para análisis estático de código - id "nebula.lint" version "17.7.0" + //id "nebula.lint" version "17.7.0" } -//version = '2.0' + //Para publicar paquetes en github -//group = 'A4I' + publishing { repositories { maven { @@ -41,7 +41,7 @@ publishing { groupId = 'us.mitfs.samples' artifactId = 'a4i' - version = '0.0' + version = '0.2' from components.java } @@ -49,6 +49,7 @@ publishing { } } version = '0.2' +//group = 'us.mitfs.samples' tasks.withType(JavaCompile) { //Añadir la opción Xlint options.deprecation = true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fae08049..ed0b41eb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-wrapper.jar.sha256 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java new file mode 100644 index 00000000..a8e611b3 --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java @@ -0,0 +1,88 @@ +/** + * + */ +package us.muit.fs.a4i.control.strategies; + +import us.muit.fs.a4i.control.IndicatorStrategy; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; + +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; + +/** + * Strategy for the calculation of a developer performance comparing him with + * the rest of developers + * + * @author fracrusan (year 23/24) + * @author Isabel Román + */ +public class DeveloperPerformanceStrategy implements IndicatorStrategy { + + private static Logger log = Logger.getLogger(Indicator.class.getName()); + // M�tricas necesarias para calcular el indicador + private static final List REQUIRED_METRICS = Arrays.asList("issuesLastMonth", "closedIssuesLastMonth", + "issues4DevLastMonth", "meanClosedIssuesLastMonth"); + + @Override + public ReportItemI calcIndicator(List metrics) throws NotAvailableMetricException { + // Se obtienen y se comprueba que se pasan las m�tricas necesarias para calcular + // el indicador. + Optional> issuesLastMonth = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(0).equals(((ReportItemI) m).getName())).findAny(); + Optional> closedIssuesLastMonth = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(1).equals(((ReportItemI) m).getName())).findAny(); + Optional> issues4DevLastMonth = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(2).equals(((ReportItemI) m).getName())).findAny(); + Optional> meanClosedIssuesLastMonth = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(3).equals(((ReportItemI) m).getName())).findAny(); + ReportItemI indicatorReport = null; + + if (issuesLastMonth.isPresent() && closedIssuesLastMonth.isPresent() && issues4DevLastMonth.isPresent() + && meanClosedIssuesLastMonth.isPresent()) { + Double rendimientoMiembro; + + // calculating indicator + if (issuesLastMonth.get().getValue() != 0 && issues4DevLastMonth.get().getValue() != 0 + && meanClosedIssuesLastMonth.get().getValue() != 0) + rendimientoMiembro = (closedIssuesLastMonth.get().getValue() / issuesLastMonth.get().getValue()) + / (meanClosedIssuesLastMonth.get().getValue() / issues4DevLastMonth.get().getValue()); + else if (meanClosedIssuesLastMonth.get().getValue() != 0) + rendimientoMiembro = 1.0; + else + rendimientoMiembro = 0.0; + + try { + // Se crea el indicador + indicatorReport = new ReportItem.ReportItemBuilder("rendimientoMiembro", rendimientoMiembro) + .metrics(Arrays.asList(issuesLastMonth.get(), closedIssuesLastMonth.get(), + issues4DevLastMonth.get(), meanClosedIssuesLastMonth.get())) + .indicator(IndicatorState.UNDEFINED).build(); + } catch (ReportItemException e) { + log.info("Error en ReportItemBuilder."); + e.printStackTrace(); + } + + } else { + log.info("No se han proporcionado las m�tricas necesarias"); + throw new NotAvailableMetricException(REQUIRED_METRICS.toString()); + } + + return indicatorReport; + } + + @Override + public List requiredMetrics() { + // Para calcular el indicador "rendimientoMiembro", ser�n necesarias las + // m�tricas + // "openIssues" y "closedIssues". + return REQUIRED_METRICS; + } + +} diff --git a/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicatorStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/IssuesRatioIndicatorStrategy.java similarity index 78% rename from src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicatorStrategy.java rename to src/main/java/us/muit/fs/a4i/control/strategies/IssuesRatioIndicatorStrategy.java index 76fa1fa5..8e07bd46 100644 --- a/src/main/java/us/muit/fs/a4i/control/IssuesRatioIndicatorStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/IssuesRatioIndicatorStrategy.java @@ -1,10 +1,11 @@ -package us.muit.fs.a4i.control; +package us.muit.fs.a4i.control.strategies; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.logging.Logger; +import us.muit.fs.a4i.control.IndicatorStrategy; import us.muit.fs.a4i.exceptions.NotAvailableMetricException; import us.muit.fs.a4i.exceptions.ReportItemException; import us.muit.fs.a4i.model.entities.Indicator; @@ -23,16 +24,18 @@ public class IssuesRatioIndicatorStrategy implements IndicatorStrategy { public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { // Se obtienen y se comprueba que se pasan las m�tricas necesarias para calcular // el indicador. - Optional> openIssues = metrics.stream().filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); - Optional> closedIssues = metrics.stream().filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); + Optional> openIssues = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); + Optional> closedIssues = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); ReportItemI indicatorReport = null; if (openIssues.isPresent() && closedIssues.isPresent()) { Double issuesRatio; // Se realiza el c�lculo del indicador - if(closedIssues.get().getValue()!=0) - issuesRatio = openIssues.get().getValue()/closedIssues.get().getValue(); + if (closedIssues.get().getValue() != 0) + issuesRatio = openIssues.get().getValue() / closedIssues.get().getValue(); else issuesRatio = openIssues.get().getValue(); @@ -51,7 +54,7 @@ public ReportItemI calcIndicator(List> metrics) thro throw new NotAvailableMetricException(REQUIRED_METRICS.toString()); } - return indicatorReport; + return indicatorReport; } @Override diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java new file mode 100644 index 00000000..5a642fa0 --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java @@ -0,0 +1,123 @@ +/** + * + */ +package us.muit.fs.a4i.model.remote; + +import java.util.List; +import java.util.logging.Logger; + +import org.kohsuke.github.GHEvent; +import org.kohsuke.github.GHEventInfo; +import org.kohsuke.github.GHEventPayload; +import org.kohsuke.github.GHProject; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHUser; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.PagedIterable; + +import us.muit.fs.a4i.exceptions.MetricException; +import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItem; + +import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; + +/** + * Deuda técnica + * Esta clase debe consultar datos sobre un desarrollador concreto de github + * Ahora mismo está en estado lamentable + * Simplemente busca los eventos de un desarrollador + * No localiza eventos de tipo ISSUE, que son los que se quería + */ +public class GitHubDeveloperEnquirer extends GitHubEnquirer { + public GitHubDeveloperEnquirer() { + super(); + metricNames.add("closedIssuesLastMonth"); + metricNames.add("assignedIssuesLastMonth"); + log.info("Incluidos nombres metricas en Enquirer"); + } + + private static Logger log = Logger.getLogger(GitHubDeveloperEnquirer.class.getName()); + /** + *

+ * Identificador unívoco de la entidad a la que se refire el informe en el + * servidor remoto que se va a utilizar + *

+ */ + private String entityId; + @Override + public ReportI buildReport(String developerId) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ReportItem getMetric(String metricName, String developerId) throws MetricException { + GHUser developer; + + GitHub gb = getConnection(); + try { + developer = gb.getUser(developerId); + log.info("Localizado el desarrollador "+developer.getName()); + } catch (Exception e) { + e.printStackTrace(); + throw new MetricException( + "No se puede acceder al desarrollador " + developerId + " para recuperarlo"); + } + + return getMetric(metricName, developer); + } + + private ReportItem getMetric(String metricName, GHUser developer) throws MetricException { + log.info("Localizando la metrica "+metricName); + ReportItem metric; + if (developer == null) { + throw new MetricException("Intenta obtener una métrica de desarrollador sin haber obtenido el desarrollador"); + } + switch (metricName) { + case "closedIssuesLastMonth": + metric = getClosedIssuesLastMonth(developer); + break; + case "assignedIssuesLastMonth": + metric = getAssignedIssuesLastMonth(developer); + break; + default: + throw new MetricException("La métrica " + metricName + " no está definida para un repositorio"); + } + + return metric; + } + + private ReportItem getClosedIssuesLastMonth(GHUser developer) { + log.info("Consultando los issues asignados a un desarrollador"); + ReportItemBuilder builder = null; + int issues=0; + + try { + PagedIterable events=developer.listEvents(); + for(GHEventInfo event:events) { + log.info("Evento tipo"+event.getType()+" en la fecha "+event.getCreatedAt()); + if(event.getType()==GHEvent.ISSUES) { + + GHEventPayload.Issue payload=event.getPayload(GHEventPayload.Issue.class); + log.info(payload.getAction()); + issues++; + + } + + } + builder = new ReportItem.ReportItemBuilder("closedIssuesLastMonth", issues); + builder.source("GitHub"); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getAssignedIssuesLastMonth(GHUser developer) { + // TODO Auto-generated method stub + return null; + } + + +} diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index 2d0e2e56..4b0ddc09 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -4,16 +4,22 @@ package us.muit.fs.a4i.model.remote; import java.io.IOException; +import java.util.Calendar; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.logging.Logger; +import org.kohsuke.github.GHIssue; import org.kohsuke.github.GHIssueState; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHRepositoryStatistics; import org.kohsuke.github.GHRepositoryStatistics.CodeFrequency; +import org.kohsuke.github.GHUser; import org.kohsuke.github.GitHub; +import org.kohsuke.github.PagedIterable; import us.muit.fs.a4i.exceptions.MetricException; import us.muit.fs.a4i.exceptions.ReportItemException; @@ -23,9 +29,10 @@ import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; /** - * @author Isabel Román - * Deuda técnica: debería seguir la misma filosofía que GitHubOrganizationEnquirer para evitar la replicación de código - * + * @author Isabel Román Deuda técnica: debería seguir la misma filosofía que + * GitHubOrganizationEnquirer para evitar la replicación de código + * deuda técnica: + * las métricas tras la etiqueta //equipo 3 tienen problemas, no están acordes al indicador para el que fueron creadas */ public class GitHubRepositoryEnquirer extends GitHubEnquirer { /** @@ -44,7 +51,7 @@ public GitHubRepositoryEnquirer() { metricNames.add("subscribers"); metricNames.add("forks"); metricNames.add("watchers"); - metricNames.add("starts"); + metricNames.add("starts"); metricNames.add("issues"); metricNames.add("closedIssues"); metricNames.add("openIssues"); @@ -55,11 +62,17 @@ public GitHubRepositoryEnquirer() { metricNames.add("totalDeletions"); metricNames.add("collaborators"); metricNames.add("ownerCommits"); + // equipo3 + metricNames.add("issuesLastMonth"); + metricNames.add("closedIssuesLastMonth"); + metricNames.add("meanClosedIssuesLastMonth"); + metricNames.add("issues4DevLastMonth"); log.info("A�adidas m�tricas al GHRepositoryEnquirer"); } - /** - * Devuelve el informe para el repositorio cuyo id se pasa como parámetro - */ + + /** + * Devuelve el informe para el repositorio cuyo id se pasa como parámetro + */ @Override public ReportI buildReport(String repositoryId) { ReportI report = null; @@ -86,73 +99,63 @@ public ReportI buildReport(String repositoryId) { GitHub gb = getConnection(); repo = gb.getRepository(repositoryId); - log.info("El repositorio es de " + repo.getOwnerName() + " Y su descripción es " - + repo.getDescription()); + log.info("El repositorio es de " + repo.getOwnerName() + " Y su descripción es " + repo.getDescription()); log.info("leído " + repo); report = new Report(repositoryId); - + /** * Métricas más elaboradas, requieren más "esfuerzo" */ - - + report.addMetric(getTotalAdditions(repo)); log.info("Incluida metrica totalAdditions "); - + report.addMetric(getTotalDeletions(repo)); log.info("Incluida metrica totalDeletions "); - /** * Métricas directas de tipo conteo */ - - + report.addMetric(getSubscribers(repo)); log.info("Incluida metrica suscribers "); - + report.addMetric(getCollaborators(repo)); log.info("Incluida metrica collaborators "); - + report.addMetric(getOwnerCommits(repo)); log.info("Incluida metrica ownerCommits "); - + report.addMetric(getForks(repo)); log.info("Incluida metrica forks "); - + report.addMetric(getWatchers(repo)); log.info("Incluida metrica watchers "); report.addMetric(getStars(repo)); log.info("Incluida metrica stars "); - + report.addMetric(getIssues(repo)); log.info("Incluida metrica issues "); - + report.addMetric(getOpenIssues(repo)); log.info("Incluida metrica openIssues "); - + report.addMetric(getClosedIssues(repo)); log.info("Incluida metrica closedIssues "); - - - /** * Métricas directas de tipo fecha */ report.addMetric(getCreation(repo)); log.info("Incluida metrica creation "); - + report.addMetric(getLastPush(repo)); log.info("Incluida metrica lastPush "); - + report.addMetric(getLastUpdated(repo)); log.info("Incluida metrica lastUpdates "); - - - - + } catch (Exception e) { log.severe("Problemas en la conexión " + e); } @@ -162,7 +165,8 @@ public ReportI buildReport(String repositoryId) { /** * Permite consultar desde fuera una única métrica del repositorio indicado - * @param metricName el nombre de la métrica + * + * @param metricName el nombre de la métrica * @param repositoryId el id del repositorio * @return el item para incluir en el informe del repositorio */ @@ -243,6 +247,19 @@ private ReportItem getMetric(String metricName, GHRepository remoteRepo) throws case "closedIssues": metric = getClosedIssues(remoteRepo); break; + // equipo 3 + case "issuesLastMonth": + metric = getIssuesLastMonth(remoteRepo); + break; + case "closedIssuesLastMonth": + metric = getClosedIssuesLastMonth(remoteRepo); + break; + case "meanClosedIssuesLastMonth": + metric = getMeanClosedIssuesLastMonth(remoteRepo); + break; + case "issues4DevLastMonth": + metric = issues4DevLastMonth(remoteRepo); + break; default: throw new MetricException("La métrica " + metricName + " no está definida para un repositorio"); } @@ -268,7 +285,7 @@ private ReportItem getTotalAdditions(GHRepository remoteRepo) throws MetricExcep ReportItem metric = null; GHRepositoryStatistics data = remoteRepo.getStatistics(); - + List codeFreq; try { codeFreq = data.getCodeFrequency(); @@ -284,8 +301,7 @@ private ReportItem getTotalAdditions(GHRepository remoteRepo) throws MetricExcep } } - ReportItemBuilder builder = new ReportItem.ReportItemBuilder("totalAdditions", - additions); + ReportItemBuilder builder = new ReportItem.ReportItemBuilder("totalAdditions", additions); builder.source("GitHub, calculada") .description("Suma el total de adiciones desde que el repositorio se creó"); metric = builder.build(); @@ -294,7 +310,7 @@ private ReportItem getTotalAdditions(GHRepository remoteRepo) throws MetricExcep // TODO Auto-generated catch block log.warning("Problemas al leer codefrequency en getTotalAdditions"); e.printStackTrace(); - } + } return metric; } @@ -343,144 +359,153 @@ private ReportItem getTotalDeletions(GHRepository remoteRepo) throws MetricExcep return metric; } + /** * Devuelve el número de suscriptores + * * @param repo repositorio que se consulta * @return item para el informe */ private ReportItem getSubscribers(GHRepository repo) { log.info("Consultando los subscriptores"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - builder = new ReportItem.ReportItemBuilder("subscribers", - repo.getSubscribersCount()); + builder = new ReportItem.ReportItemBuilder("subscribers", repo.getSubscribersCount()); builder.source("GitHub"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } + /** * Devuelve el número de forks + * * @param repo repositorio que se consulta * @return item para el informe */ private ReportItem getForks(GHRepository repo) { log.info("Consultando los forks"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - builder = new ReportItem.ReportItemBuilder("forks", - repo.getForksCount()); + builder = new ReportItem.ReportItemBuilder("forks", repo.getForksCount()); builder.source("GitHub"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } + /** * Devuelve los usuarios que observan el repositorio + * * @param repo repositorio que se consulta * @return item para el informe */ private ReportItem getWatchers(GHRepository repo) { log.info("Consultando los watchers"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - builder = new ReportItem.ReportItemBuilder("watchers", - repo.getWatchersCount()); + builder = new ReportItem.ReportItemBuilder("watchers", repo.getWatchersCount()); builder.source("GitHub"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } + /** * Devuelve el número de estrellas + * * @param repo repositorio que se consulta * @return item para el informe */ private ReportItem getStars(GHRepository repo) { log.info("Consultando las starts"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - builder = new ReportItem.ReportItemBuilder("stars", - repo.getStargazersCount()); + builder = new ReportItem.ReportItemBuilder("stars", repo.getStargazersCount()); builder.description("Numero de estrellas").source("GitHub"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } - + /** * Devuelve el número de commits que realiza el responsable del repositorio + * * @param repo repositorio que se consulta * @return item para el informe */ private ReportItem getOwnerCommits(GHRepository repo) { log.info("Consultando los commits del responsable del repositorio"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; GHRepositoryStatistics data = repo.getStatistics(); - + try { builder = new ReportItem.ReportItemBuilder("ownerCommits", data.getParticipation().getOwnerCommits().size()); - + builder.description("Commits del responsable").source("GitHub"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } + /** * Devuelve el número de tickets + * * @param repo repositorio que se consulta * @return item para el informe */ private ReportItem getIssues(GHRepository repo) { log.info("Consultando los issues"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - builder = new ReportItem.ReportItemBuilder("issues", - repo.getIssues(GHIssueState.ALL).size()); + builder = new ReportItem.ReportItemBuilder("issues", repo.getIssues(GHIssueState.ALL).size()); builder.description("Numero de asuntos totales").source("GitHub"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } + /** * Devuelve los issues abiertos en el repositorio que se pasa como parámetro + * * @param repo repositorio que se consulta * @return item para incluir en el informe */ private ReportItem getOpenIssues(GHRepository repo) { log.info("Consultando los issues abiertos"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - builder = new ReportItem.ReportItemBuilder("openIssues", - repo.getOpenIssueCount()); + builder = new ReportItem.ReportItemBuilder("openIssues", repo.getOpenIssueCount()); builder.description("Numero de asuntos abiertos").source("GitHub"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } + /** * Devuelve los issues cerrados del repositorio + * * @param repo repositorio que se consulta * @return item para incluir en el informe */ private ReportItem getClosedIssues(GHRepository repo) { log.info("Consultando los issues cerrados"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { builder = new ReportItem.ReportItemBuilder("closedIssues", repo.getIssues(GHIssueState.CLOSED).size()); @@ -488,83 +513,285 @@ private ReportItem getClosedIssues(GHRepository repo) { } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } + /** * Devuelve el número de colaboradores + * * @param repo repositorio que se consulta * @return item para el informe */ private ReportItem getCollaborators(GHRepository repo) { log.info("Consultando los colaboradores"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - builder = new ReportItem.ReportItemBuilder("collaborators", - repo.getCollaborators().size()); + builder = new ReportItem.ReportItemBuilder("collaborators", repo.getCollaborators().size()); builder.description("Numero de colaboradores en el repositorio").source("GitHub"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } + /** * Devuelve la fecha de creación del repositorio + * * @param repo * @return item para el informe */ private ReportItem getCreation(GHRepository repo) { log.info("Consultando fecha de creación"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - builder = new ReportItem.ReportItemBuilder("creation", - repo.getCreatedAt()); + builder = new ReportItem.ReportItemBuilder("creation", repo.getCreatedAt()); builder.description("Fecha de creación del repositorio").source("GitHub"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); - } + } + /** - * Devuelve la fecha del último push en el repositorio que se pase como argumento + * Devuelve la fecha del último push en el repositorio que se pase como + * argumento + * * @param repo repositorio que se consulta * @return item para el informe */ private ReportItem getLastPush(GHRepository repo) { log.info("Consultando el ultimo push"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - builder = new ReportItem.ReportItemBuilder("lastPush", - repo.getPushedAt()); + builder = new ReportItem.ReportItemBuilder("lastPush", repo.getPushedAt()); builder.description("Último push realizado en el repositorio").source("GitHub"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); - } + } + /** * Fecha de la última actualización + * * @param repo repositorio que se consulta * @return item para incluir en el informe del repositorio */ private ReportItem getLastUpdated(GHRepository repo) { log.info("Consultando la ultima actualización"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - builder = new ReportItem.ReportItemBuilder("lastUpdated", - repo.getUpdatedAt()); + builder = new ReportItem.ReportItemBuilder("lastUpdated", repo.getUpdatedAt()); builder.description("Última actualización").source("GitHub"); - + + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + + // Métodos añadidos por el equipo 3 + /** + * + * @param remoteRepo + * @return ReportItem with the number of issues created last month + * @throws MetricException + */ + private ReportItem getIssuesLastMonth(GHRepository remoteRepo) throws MetricException { + log.info("Consultando los issues abiertos un mes"); + int issuesLastMonth = 0; + ReportItemBuilder builder = null; + try { + // Calcular la fecha de hace un mes + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MONTH, -1); + Date lastMonth = cal.getTime(); + + // Obtener todas las issues creadas desde la fecha de hace un mes + PagedIterable issues = remoteRepo.queryIssues().state(GHIssueState.ALL).since(lastMonth).list(); + + // Contar el número de issues + for (GHIssue issue : issues) { + if (issue.getCreatedAt().after(lastMonth)) { + issuesLastMonth++; + log.finer("issue manejado por "+issue.getUser().getName()); + + } + } + + } catch (IOException e) { + throw new MetricException( + "Error al obtener el número de issues creadas en el último mes: " + e.getMessage()); + } + + // Construir y devolver la métrica + try { + builder = new ReportItem.ReportItemBuilder("issuesLastMonth", issuesLastMonth); + builder.description("Issues creadas en el mes").source("GitHub"); + + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + return builder.build(); + } + + /** + * + * @param remoteRepo + * @return ReportItem with the issues closed last month + * @throws MetricException + */ + private ReportItem getClosedIssuesLastMonth(GHRepository remoteRepo) throws MetricException { + int closedIssuesLastMonth = 0; + ReportItemBuilder builder = null; + try { + // Calcular la fecha de hace un mes + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MONTH, -1); + Date lastMonth = cal.getTime(); + + // Obtener todas las issues cerradas desde la fecha de hace un mes + PagedIterable issues = remoteRepo.queryIssues().state(GHIssueState.CLOSED).since(lastMonth).list(); + + // Contar el número de issues cerradas + for (GHIssue issue : issues) { + if (issue.getClosedAt() != null && issue.getClosedAt().after(lastMonth)) { + closedIssuesLastMonth++; + } + } + + } catch (Exception e) { + throw new MetricException( + "Error al obtener el número de issues cerradas en el último mes: " + e.getMessage()); + } + + // Construir y devolver la métrica + try { + builder = new ReportItem.ReportItemBuilder("closedIssuesLastMonth", closedIssuesLastMonth); + builder.description("Issues cerradas en el mes").source("GitHub"); + + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + return builder.build(); + } + + /** + * + * @param remoteRepo + * @return ReportItem with the average of closed issues per member in a month + * @throws MetricException + */ + private ReportItem getMeanClosedIssuesLastMonth(GHRepository remoteRepo) throws MetricException { + int closedIssuesLastMonth = 0; + int activeMembers = 0; + Map issuesClosedByMember = new HashMap<>(); + ReportItemBuilder builder = null; + + try { + // Calcular la fecha de hace un mes + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MONTH, -1); + Date lastMonth = cal.getTime(); + + // Obtener todas las issues cerradas desde la fecha de hace un mes + PagedIterable issues = remoteRepo.queryIssues().state(GHIssueState.CLOSED).since(lastMonth).list(); + + // Contar el número de issues cerradas por cada miembro + for (GHIssue issue : issues) { + if (issue.getClosedAt() != null && issue.getClosedAt().after(lastMonth)) { + GHUser closer = issue.getClosedBy(); + if (closer != null) { + issuesClosedByMember.put(closer.getLogin(), + issuesClosedByMember.getOrDefault(closer.getLogin(), 0) + 1); + closedIssuesLastMonth++; + } + } + } + + // Contar el número de miembros activos + activeMembers = issuesClosedByMember.size(); + + } catch (IOException e) { + throw new MetricException( + "Error al obtener el promedio de issues cerradas por miembro activo en el último mes: " + + e.getMessage()); + } + + // Calcular el promedio de issues que cierra un miembro + double avgClosedIssuesPerMember = activeMembers > 0 ? (double) closedIssuesLastMonth / activeMembers : 0.0; + + // Construir y devolver la métrica + + try { + builder = new ReportItem.ReportItemBuilder("meanClosedIssuesLastMonth", avgClosedIssuesPerMember); + builder.description("Average of closed issues per member").source("GitHub"); + } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } + return builder.build(); - } - - - + } + + /** + * + * @param remoteRepo + * @return ReportItem with a map indicating the issues assigned to each member in a month + * @throws MetricException + * + * Deuda técnica: esto no está bien porque aquí este reportitem es un mapa y luego lo tratan como si fuera un double... no es coherente una parte con la otra. + */ + private ReportItem issues4DevLastMonth(GHRepository remoteRepo) throws MetricException { + Map issuesAssignedByMember = new HashMap<>(); + ReportItemBuilder> builder = null; + + try { + // Calcular la fecha de hace un mes + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MONTH, -1); + Date lastMonth = cal.getTime(); + + // Obtener todas las issues creadas desde la fecha de hace un mes + PagedIterable issues = remoteRepo.queryIssues().state(GHIssueState.ALL).since(lastMonth).list(); + + // Contar el número de issues asignadas a cada miembro + for (GHIssue issue : issues) { + if (issue.getCreatedAt().after(lastMonth) && !issue.getAssignees().isEmpty()) { + for (GHUser assignee : issue.getAssignees()) { + issuesAssignedByMember.put(assignee.getLogin(), + issuesAssignedByMember.getOrDefault(assignee.getLogin(), 0) + 1); + } + } + } + + } catch (IOException e) { + throw new MetricException("Error al obtener el número de issues asignadas a cada miembro en el último mes: " + + e.getMessage()); + } + + // Construir y devolver la métrica + try { + builder = new ReportItem.ReportItemBuilder>("issues4DevLastMonth", + issuesAssignedByMember); + builder.description("Issues assigned to a developer last month").source("GitHub"); + + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + return builder.build(); + } + } diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index c90f3b1d..ba84bd33 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -136,8 +136,33 @@ "type": "java.lang.Integer", "description": "Número de repositorios con pull requests pendientes", "unit": "repositories" + }, + { + "name": "issuesLastMonth", + "type": "java.lang.Integer", + "description": "Número de trabajos/issues creados en el mes", + "unit": "issues/month" + }, + { + "name": "closedIssuesLastMonth", + "type": "java.lang.Integer", + "description": "Número de repositorios trabajos/issues concluidos en el mes", + "unit": "issues/month" + }, + { + "name": "meanClosedIssuesLastMonth", + "type": "java.lang.Double", + "description": "Media de issues cerrados en el pasado mes por desarrollador", + "unit": "issues/developer" + }, + { + "name": "assignedIssuesLastMonth", + "type": "java.lang.Integer", + "description": "Issues asignados a un desarrollador en el último mes", + "unit": "issues/month" } + ], "indicators": [{ "name": "issuesProgress", diff --git a/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java index b1dae5a6..1afdd2d7 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java @@ -21,7 +21,7 @@ import org.junit.jupiter.api.function.Executable; import org.mockito.Mockito; -import us.muit.fs.a4i.control.IssuesRatioIndicatorStrategy; +import us.muit.fs.a4i.control.strategies.IssuesRatioIndicatorStrategy; import us.muit.fs.a4i.exceptions.NotAvailableMetricException; import us.muit.fs.a4i.model.entities.ReportItemI; diff --git a/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java index d8fd30b4..eb500cc8 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java @@ -23,9 +23,9 @@ import org.junit.jupiter.api.Test; import us.muit.fs.a4i.control.IndicatorStrategy; -import us.muit.fs.a4i.control.IssuesRatioIndicatorStrategy; import us.muit.fs.a4i.control.ReportManagerI; import us.muit.fs.a4i.control.RepositoryCalculator; +import us.muit.fs.a4i.control.strategies.IssuesRatioIndicatorStrategy; import us.muit.fs.a4i.exceptions.IndicatorException; import us.muit.fs.a4i.exceptions.NotAvailableMetricException; import us.muit.fs.a4i.exceptions.ReportItemException; diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/DeveloperPerformanceIndicatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/DeveloperPerformanceIndicatorTest.java new file mode 100644 index 00000000..87efb13c --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/DeveloperPerformanceIndicatorTest.java @@ -0,0 +1,69 @@ +/** + * + */ +package us.muit.fs.a4i.test.control.strategies; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import us.muit.fs.a4i.control.strategies.DeveloperPerformanceStrategy; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; + +/** + * + */ +class DeveloperPerformanceIndicatorTest { + + private DeveloperPerformanceStrategy indicador; + + @BeforeEach + public void setUp() { + DeveloperPerformanceStrategy indicador = new DeveloperPerformanceStrategy(); + } + + @Test + public void testCalcIndicator() throws ReportItemException, NotAvailableMetricException { + // Crear métricas simuladas + ReportItemI createdIssuesMes = new ReportItem.ReportItemBuilder("createdIssuesMes", 100.0).build(); + ReportItemI closedIssuesMes = new ReportItem.ReportItemBuilder("closedIssuesMes", 80.0).build(); + ReportItemI assignedDevIssuesMes = new ReportItem.ReportItemBuilder("assingedDevIssuesMes", 8.0).build(); + ReportItemI closedDevIssuesMes = new ReportItem.ReportItemBuilder("meanClosedIssuesLastMonth", 5.0).build(); + + // Calcular el indicador + List> metrics = Arrays.asList(createdIssuesMes, closedIssuesMes, assignedDevIssuesMes, closedDevIssuesMes); + ReportItemI result = indicador.calcIndicator(metrics); + + // Verificar el resultado + Double expectedIndicator = (80.0 / 100.0) / (5.0 / 8.0); + assertEquals(expectedIndicator, result.getValue()); + } + + @Test + public void testCalcIndicatorWithMissingMetrics() throws ReportItemException, NotAvailableMetricException { + // Crear métricas simuladas incompletas + ReportItemI createdIssuesMes = new ReportItem.ReportItemBuilder("createdIssuesMes", 100.0).build(); + ReportItemI meanClosedIssuesLastMonth = new ReportItem.ReportItemBuilder("meanClosedIssuesLastMonth", 80.0).build(); + + // Calcular el indicador + List> metrics = Arrays.asList(createdIssuesMes, meanClosedIssuesLastMonth); + + // Verificar que se lanza una excepción + assertThrows(NotAvailableMetricException.class, () -> { + indicador.calcIndicator(metrics); + }); + } +} + diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubDeveloperEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubDeveloperEnquirerTest.java new file mode 100644 index 00000000..de7c33d2 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubDeveloperEnquirerTest.java @@ -0,0 +1,32 @@ +package us.muit.fs.a4i.test.model.remote; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.logging.Logger; + +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.exceptions.MetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.remote.GitHubDeveloperEnquirer; + +class GitHubDeveloperEnquirerTest { + private static Logger log = Logger.getLogger(GitHubOrganizationEnquirerTest.class.getName()); + GitHubDeveloperEnquirer ghEnquirer = new GitHubDeveloperEnquirer(); + + /** + * Test method for + * GitHubOrganizationEnquirer + * @throws MetricException + * @throws ReportItemException + */ + @Test + void testAssignedIssuesLastMonth() throws MetricException { + ReportItem metric = ghEnquirer.getMetric("closedIssuesLastMonth","Isabel-Roman"); + assertEquals(metric.getName(),"closedIssuesLastMonth"); + log.info(metric.getValue().toString()); + log.info(metric.getDescription()); + } + +} diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java new file mode 100644 index 00000000..97a5bf95 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java @@ -0,0 +1,76 @@ +/** + * + */ +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.Test; + +import us.muit.fs.a4i.exceptions.MetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.remote.GitHubRepositoryEnquirer; +/** + * + */ +class GitHubRepositoryEnquirerTest { + + private static Logger log = Logger.getLogger(GitHubOrganizationEnquirerTest.class.getName()); + GitHubRepositoryEnquirer ghEnquirer = new GitHubRepositoryEnquirer(); + + /** + * Test method for + * GitHubRepositoryEnquirer, verifing that issuesLastMonth is correctly obtained + * @throws MetricException + * @throws ReportItemException + */ + @Test + void testIssuesLastMonth() throws MetricException, ReportItemException { + ReportItem metric = ghEnquirer.getMetric("issuesLastMonth", "MIT-FS/Audit4Improve-API"); + assertEquals(metric.getName(),"issuesLastMonth"); + log.info(metric.getValue().toString()); + log.info(metric.getDescription()); + } + + /** + * Test method for + * GitHubRepositoryEnquirer, verifing that closedIssuesLastMonth is correctly obtained + * @throws MetricException + * @throws ReportItemException + */ + @Test + void testClosedIssuesLastMonth() throws MetricException, ReportItemException { + ReportItem metric = ghEnquirer.getMetric("closedIssuesLastMonth", "MIT-FS/Audit4Improve-API"); + assertEquals(metric.getName(),"closedIssuesLastMonth"); + log.info(metric.getValue().toString()); + log.info(metric.getDescription()); + } + /** + * Test method for + * GitHubRepositoryEnquirer, verifing that closedIssuesLastMonth is correctly obtained + * @throws MetricException + * @throws ReportItemException + */ + @Test + void testMeanClosedIssuesLastMonth() throws MetricException, ReportItemException { + ReportItem metric = ghEnquirer.getMetric("meanClosedIssuesLastMonth", "MIT-FS/Audit4Improve-API"); + assertEquals(metric.getName(),"meanClosedIssuesLastMonth"); + log.info(metric.getValue().toString()); + log.info(metric.getDescription()); + } + + + /** + * Test method for {@link us.muit.fs.a4i.model.remote.GitHubEnquirer#getAvailableMetrics()}. + */ + @Test + void testGetAvailableMetrics() { + List availableMetrics=ghEnquirer.getAvailableMetrics(); + log.info(availableMetrics.toString()); + } + +} From ab59263b50c13191b7932c3b7c1bd8543158eaf0 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Sun, 23 Feb 2025 00:13:12 +0100 Subject: [PATCH 074/100] Indicador equipo4 pullRequests Sin verificar --- .../PullRequestIndicatorStrategy.java | 87 +++++++++++++++ .../remote/GitHubRepositoryEnquirer.java | 53 +++++++++ .../PullRequestIndicatorStrategyTest.java | 103 ++++++++++++++++++ .../remote/GitHubRepositoryEnquirerTest.java | 56 ++++++++++ 4 files changed, 299 insertions(+) create mode 100644 src/main/java/us/muit/fs/a4i/control/strategies/PullRequestIndicatorStrategy.java create mode 100644 src/test/java/us/muit/fs/a4i/test/control/strategies/PullRequestIndicatorStrategyTest.java diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/PullRequestIndicatorStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/PullRequestIndicatorStrategy.java new file mode 100644 index 00000000..3df84f71 --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/control/strategies/PullRequestIndicatorStrategy.java @@ -0,0 +1,87 @@ +/** + * + */ +package us.muit.fs.a4i.control.strategies; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; + +import us.muit.fs.a4i.control.IndicatorStrategy; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; + +/** + * % de pull requests cerrados, sobre el total + * @author Sergio García López + * + */ +public class PullRequestIndicatorStrategy implements IndicatorStrategy { + private static Logger log = Logger.getLogger(PullRequestIndicatorStrategy.class.getName()); + + // Métricas necesarias para calcular el indicador + private static final List REQUIRED_METRICS = Arrays.asList("totalPullReq", "closedPullReq"); + + @Override + public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { + + // Indicador a devolver + ReportItemI indicatorReport = null; + + // Estado del indicador + IndicatorState estado = IndicatorState.UNDEFINED; + + Optional> totalPullReq = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); + Optional> closedPullReq = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); + + + + if (totalPullReq.isPresent() && closedPullReq.isPresent()) { + + // Calculamos el indicador + Double pullRequestCompletion = 0.0; + + if (totalPullReq.get().getValue() > 0) { + pullRequestCompletion = 100 * closedPullReq.get().getValue() / totalPullReq.get().getValue(); + + + // Criterios de calidad (porcentuales) + if (pullRequestCompletion > 75) { + estado = IndicatorState.OK; + } else if (pullRequestCompletion > 50) { + estado = IndicatorState.WARNING; + } else { + estado = IndicatorState.CRITICAL; + } + } + + try { + + indicatorReport = new ReportItem.ReportItemBuilder("pullRequestCompletion", + pullRequestCompletion).metrics(Arrays.asList(totalPullReq.get(), closedPullReq.get())).indicator(estado) + .build(); + + } catch (ReportItemException e) { + log.info("Error en ReportItemBuilder: " + e); + } + + } else { + log.info("Falta alguna de las métricas"); + throw new NotAvailableMetricException(REQUIRED_METRICS.toString()); + } + + return indicatorReport; + + } + + @Override + public List requiredMetrics() { + return REQUIRED_METRICS; + } +} diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index 4b0ddc09..00f1f898 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -14,6 +14,7 @@ import org.kohsuke.github.GHIssue; import org.kohsuke.github.GHIssueState; import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHPullRequest; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHRepositoryStatistics; import org.kohsuke.github.GHRepositoryStatistics.CodeFrequency; @@ -67,6 +68,9 @@ public GitHubRepositoryEnquirer() { metricNames.add("closedIssuesLastMonth"); metricNames.add("meanClosedIssuesLastMonth"); metricNames.add("issues4DevLastMonth"); + //equipo 4 + metricNames.add("totalPullReq"); + metricNames.add("closedPullReq"); log.info("A�adidas m�tricas al GHRepositoryEnquirer"); } @@ -260,6 +264,13 @@ private ReportItem getMetric(String metricName, GHRepository remoteRepo) throws case "issues4DevLastMonth": metric = issues4DevLastMonth(remoteRepo); break; + //equipo 4 + case "pullResquestTotales": + metric = getTotalPullReq(remoteRepo); + break; + case "closedPullReq": + metric = getClosedPullReq(remoteRepo); + break; default: throw new MetricException("La métrica " + metricName + " no está definida para un repositorio"); } @@ -793,5 +804,47 @@ private ReportItem issues4DevLastMonth(GHRepository remoteRepo) throws MetricExc return builder.build(); } + private ReportItem getClosedPullReq(GHRepository repo){ + log.info("Consultando los pull requests completados"); + ReportItemBuilder builder = null; + + int completedPullRequests = 0; + + try { + + for (GHPullRequest pullRequest : repo.getPullRequests(GHIssueState.CLOSED)) { + + completedPullRequests++; + + } + builder = new ReportItem.ReportItemBuilder("closedPullReq", completedPullRequests); + builder.description("Número de pull requests completados").source("GitHub"); + } catch (Exception e) { + e.printStackTrace(); + } + return builder.build(); + } + + private ReportItem getTotalPullReq(GHRepository repo){ + log.info("Consultando los pull requests totales"); + ReportItemBuilder builder = null; + + int totalPullRequests = 0; + + try { + + for (GHPullRequest pullRequest : repo.getPullRequests(GHIssueState.ALL)) { + + totalPullRequests++; + + } + builder = new ReportItem.ReportItemBuilder("totalPullReq", totalPullRequests); + builder.description("Número de pull requests totales").source("GitHub"); + } catch (Exception e) { + e.printStackTrace(); + } + return builder.build(); + } + } diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/PullRequestIndicatorStrategyTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/PullRequestIndicatorStrategyTest.java new file mode 100644 index 00000000..8f84586a --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/PullRequestIndicatorStrategyTest.java @@ -0,0 +1,103 @@ +package us.muit.fs.a4i.test.control.strategies; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.model.entities.ReportItemI; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; +import us.muit.fs.a4i.control.strategies.PullRequestIndicatorStrategy; +/** + * Equipo cuatro curso 23/24 + */ +class PullRequestIndicatorStrategyTest { + + @BeforeAll + static void setUpBeforeClass() throws Exception { + } + + @AfterAll + static void tearDownAfterClass() throws Exception { + } + + @BeforeEach + void setUp() throws Exception { + } + + @AfterEach + void tearDown() throws Exception { + } + + @Test + public void testCalcIndicator() throws NotAvailableMetricException { + // Creamos los mocks + ReportItemI mockPullReqTotales = Mockito.mock(ReportItemI.class); + ReportItemI mockPullReqCompletados = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para que el porcentaje sea el 60% + Mockito.when(mockPullReqTotales.getName()).thenReturn("totalPullReq"); + Mockito.when(mockPullReqTotales.getValue()).thenReturn(100.0); + + Mockito.when(mockPullReqCompletados.getName()).thenReturn("closedPullReq"); + Mockito.when(mockPullReqCompletados.getValue()).thenReturn(60.0); + + // Creamos la calculadora del indicador + PullRequestIndicatorStrategy indicadorCalc = new PullRequestIndicatorStrategy(); + + // Calculamos el indicador + List> metricas = Arrays.asList(mockPullReqTotales, mockPullReqCompletados); + ReportItemI resultado = indicadorCalc.calcIndicator(metricas); + + // Comprobamos que el resultado es el esperado + Assertions.assertEquals("pullRequestCompletion", resultado.getName()); + Assertions.assertEquals(60.0, resultado.getValue()); + Assertions.assertDoesNotThrow(()->indicadorCalc.calcIndicator(metricas)); + } + + @Test + public void testCalcIndicatorThrowsNotAvailableMetricException() { + // Creamos un mock + ReportItemI mockPullReqTotales = Mockito.mock(ReportItemI.class); + + // Configuramos el mock + Mockito.when(mockPullReqTotales.getName()).thenReturn("totalPullReq"); + Mockito.when(mockPullReqTotales.getValue()).thenReturn(100.0); + + // Creamos la calculadora del indicador + PullRequestIndicatorStrategy indicadorCalc = new PullRequestIndicatorStrategy(); + + // Probamos con una sola métrica + List> metricas = Arrays.asList(mockPullReqTotales); + // Comprobamos que se lanza la excepción + NotAvailableMetricException exception = Assertions.assertThrows(NotAvailableMetricException.class, + () -> indicadorCalc.calcIndicator(metricas)); + + } + + @Test + public void testRequiredMetrics() { + // Creamos la calculadora del indicador + PullRequestIndicatorStrategy indicadorCalc = new PullRequestIndicatorStrategy(); + + // Obtenemos las metricas del indicador + List metricas = indicadorCalc.requiredMetrics(); + + // Comprobamos que el resultado es el esperado + List metricasEsperadas = Arrays.asList("totalPullReq", "closedPullReq"); + Assertions.assertEquals(metricasEsperadas, metricas); + } + + } diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java index 97a5bf95..e890ee30 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java @@ -72,5 +72,61 @@ void testGetAvailableMetrics() { List availableMetrics=ghEnquirer.getAvailableMetrics(); log.info(availableMetrics.toString()); } + + @Test + void testGetTotalPullRequests() throws MetricException { + + // Nombre de la métrica que queremos consultar + String nombreMetrica = "totalPullReq"; + + // Repositorio del que se quiere obtener la métrica + String repositoryId = "MIT-FS/Audit4Improve-API"; + + // Variable para almacenar el número total de pull requests + ReportItem metrica = null; + + // Número total de pull requests que habrá en el repositorio + int numPullRequests = 61; + + // Creamos el RemoteEnquirer para el repositorio GitHub + GitHubRepositoryEnquirer enquirer = new GitHubRepositoryEnquirer(); + + // Obtenemos el número total de pull requests + metrica = enquirer.getMetric(nombreMetrica, repositoryId); + log.info(metrica.toString()); + + // Comprobamos que el resultado coincida con el número total de pull requests real + assertEquals(numPullRequests, metrica.getValue()); + + } + + /** + * @throws MetricException + */ + @Test + void testCompletedPullRequests() throws MetricException { + + // Nombre de la métrica que queremos consultar + String nombreMetrica = "completedPullReq"; + + // Repositorio del que se quiere obtener la métrica + String repositoryId = "MIT-FS/Audit4Improve-API"; + + // Variable para almacenar el número de pull requests completados + ReportItem metrica = null; + + // Número de pull requests completados que habrá en el repositorio + int numCompletedPull = 51; + + // Creamos el RemoteEnquirer para el repositorio GitHub + GitHubRepositoryEnquirer enquirer = new GitHubRepositoryEnquirer(); + + // Obtenemos el número de pull requests completados + metrica = enquirer.getMetric(nombreMetrica, repositoryId); + log.info(metrica.toString()); + + // Comprobamos que el resultado coincida con el número de pull requests completados real + assertEquals(numCompletedPull, metrica.getValue()); + } } From 9a6bd7e53c570af3f557571f257a1144ecf34c49 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Sun, 23 Feb 2025 13:59:25 +0100 Subject: [PATCH 075/100] Arreglando OrganizationEquirer --- .../us/muit/fs/a4i/control/ReportManager.java | 1 + .../DeveloperPerformanceStrategy.java | 1 + .../IssuesRatioIndicatorStrategy.java | 5 +- .../PullRequestIndicatorStrategy.java | 4 +- .../RepositoryCalculator.java | 10 +++- .../model/remote/GitHubDeveloperEnquirer.java | 1 + .../remote/GitHubOrganizationEnquirer.java | 55 ++++++++++++++----- .../remote/GitHubRepositoryEnquirer.java | 2 +- src/main/resources/a4iDefault.json | 12 ++++ .../control/RepositoryCalculatorTest.java | 2 +- .../GitHubOrganizationEnquirerTest.java | 18 +++--- 11 files changed, 82 insertions(+), 29 deletions(-) rename src/main/java/us/muit/fs/a4i/control/{ => strategies}/RepositoryCalculator.java (90%) diff --git a/src/main/java/us/muit/fs/a4i/control/ReportManager.java b/src/main/java/us/muit/fs/a4i/control/ReportManager.java index dfb3b412..b1380569 100644 --- a/src/main/java/us/muit/fs/a4i/control/ReportManager.java +++ b/src/main/java/us/muit/fs/a4i/control/ReportManager.java @@ -7,6 +7,7 @@ import java.util.logging.Logger; import us.muit.fs.a4i.config.Context; +import us.muit.fs.a4i.control.strategies.RepositoryCalculator; import us.muit.fs.a4i.exceptions.ReportNotDefinedException; import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItemI; diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java index a8e611b3..eefb4644 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java @@ -22,6 +22,7 @@ * * @author fracrusan (year 23/24) * @author Isabel Román + * RECUERDA: los indicadores tienen que estar incluidos en el fichero de configuración a4iDefault.json */ public class DeveloperPerformanceStrategy implements IndicatorStrategy { diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/IssuesRatioIndicatorStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/IssuesRatioIndicatorStrategy.java index 8e07bd46..50961e36 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/IssuesRatioIndicatorStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/IssuesRatioIndicatorStrategy.java @@ -12,7 +12,10 @@ import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; import us.muit.fs.a4i.model.entities.ReportItem; import us.muit.fs.a4i.model.entities.ReportItemI; - +/** + * @author alumnos del curso 23/24, grupo 3 + * RECUERDA: los indicadores tienen que estar incluidos en el fichero de configuración a4iDefault.json + */ public class IssuesRatioIndicatorStrategy implements IndicatorStrategy { private static Logger log = Logger.getLogger(Indicator.class.getName()); diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/PullRequestIndicatorStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/PullRequestIndicatorStrategy.java index 3df84f71..019e35a9 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/PullRequestIndicatorStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/PullRequestIndicatorStrategy.java @@ -17,7 +17,8 @@ /** * % de pull requests cerrados, sobre el total - * @author Sergio García López + * @author Sergio García López (equipo 4 del curso 23/24) + * RECUERDA: los indicadores tienen que estar incluidos en el fichero de configuración a4iDefault.json * */ public class PullRequestIndicatorStrategy implements IndicatorStrategy { @@ -52,6 +53,7 @@ public ReportItemI calcIndicator(List> metrics) thro // Criterios de calidad (porcentuales) + //estos límites deben estar configurados en el fichero a4iDefault.json, no aquí if (pullRequestCompletion > 75) { estado = IndicatorState.OK; } else if (pullRequestCompletion > 50) { diff --git a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java b/src/main/java/us/muit/fs/a4i/control/strategies/RepositoryCalculator.java similarity index 90% rename from src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java rename to src/main/java/us/muit/fs/a4i/control/strategies/RepositoryCalculator.java index 9609ef62..204025cc 100644 --- a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/RepositoryCalculator.java @@ -1,12 +1,15 @@ /** * */ -package us.muit.fs.a4i.control; +package us.muit.fs.a4i.control.strategies; import java.util.HashMap; import java.util.List; import java.util.logging.Logger; +import us.muit.fs.a4i.control.IndicatorStrategy; +import us.muit.fs.a4i.control.IndicatorsCalculator; +import us.muit.fs.a4i.control.ReportManagerI; import us.muit.fs.a4i.exceptions.IndicatorException; import us.muit.fs.a4i.exceptions.NotAvailableMetricException; import us.muit.fs.a4i.model.entities.Indicator; @@ -17,12 +20,11 @@ /** *

* Implementa los métodos para calcular indicadores referidos a un repositorio - * repositorio *

*

* Puede hacerse uno a uno o todos a la vez *

- * + * RECUERDA: los indicadores tienen que estar incluidos en el fichero de configuración a4iDefault.json * @author Isabel Román * */ @@ -85,4 +87,6 @@ public void setIndicator(String indicatorName, IndicatorStrategy strategy) { } + + } \ No newline at end of file diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java index 5a642fa0..f89ec055 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java @@ -27,6 +27,7 @@ * Ahora mismo está en estado lamentable * Simplemente busca los eventos de un desarrollador * No localiza eventos de tipo ISSUE, que son los que se quería + * RECUERDA: las métricas tienen que estar incluidas en el fichero de configuración a4iDefault.json */ public class GitHubDeveloperEnquirer extends GitHubEnquirer { public GitHubDeveloperEnquirer() { diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java index c56239d0..fccae6da 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java @@ -15,8 +15,7 @@ import org.kohsuke.github.GitHub; import org.kohsuke.github.PagedIterable; import org.kohsuke.github.GHProject; - - +import org.kohsuke.github.GHRepository; import us.muit.fs.a4i.exceptions.MetricException; @@ -34,6 +33,7 @@ *

* Deuda técnica: sería necesario verificar mejor el funcionamiento de las consultas de proyectos cerrados y abiertos, no parece hacer lo esperado * Habría que incluir más métricas y algún indicador + * RECUERDA: las métricas tienen que estar incluidas en el fichero de configuración a4iDefault.json *

* * @author Isabel Román @@ -250,11 +250,13 @@ private ReportItem getTeams(GHOrganization organization) { log.info("Consultando los equipos"); ReportItemBuilder builder=null; try { + int size=organization.getTeams().size(); + log.info("Numero de equipos"+size); builder = new ReportItem.ReportItemBuilder("teams", organization.getTeams().size()); builder.source("GitHub"); } catch (ReportItemException | IOException e) { - // TODO Auto-generated catch block + log.fine("unable to retry teams"); e.printStackTrace(); } return builder.build(); @@ -289,16 +291,28 @@ private ReportItem getPullRequests(GHOrganization organization) { } private ReportItem getOpenProjects(GHOrganization organization) { - log.info("Consultando los proyectos abiertos"); + ReportItemBuilder builder=null; try { + log.info("Consultando los proyectos abiertos en "+organization.getUrl()); + int number=0; + //first we look for projects associated with the organization PagedIterable pagina=organization.listProjects(GHProject.ProjectStateFilter.OPEN); - List proyectos=pagina.toList(); + number=number+proyectos.size(); + //second we look for projects associated with the repos + PagedIterable repositories=organization.listRepositories(); + + for(GHRepository repo:repositories) { + PagedIterable repoproyects=repo.listProjects(GHProject.ProjectStateFilter.OPEN); + number=number+repoproyects.toList().size(); + } + + + log.info("Open projects "+number); builder = new ReportItem.ReportItemBuilder("openProjects", - proyectos.size()); + number); - log.info("Proyectos "+proyectos); for(GHProject pro:proyectos) { log.info("Proyecto "+pro.getName()+" en estado "+pro.getState()); } @@ -311,24 +325,37 @@ private ReportItem getOpenProjects(GHOrganization organization) { } private ReportItem getClosedProjects(GHOrganization organization) { - log.info("Consultando los proyectos cerrados"); ReportItemBuilder builder=null; try { -PagedIterable pagina=organization.listProjects(GHProject.ProjectStateFilter.CLOSED); - + log.info("Consultando los proyectos cerrados en "+organization.getUrl()); + int number=0; + //first we look for projects associated with the organization + PagedIterable pagina=organization.listProjects(GHProject.ProjectStateFilter.CLOSED); List proyectos=pagina.toList(); + number=number+proyectos.size(); + //second we look for projects associated with the repos + PagedIterable repositories=organization.listRepositories(); + + for(GHRepository repo:repositories) { + PagedIterable repoproyects=repo.listProjects(GHProject.ProjectStateFilter.CLOSED); + number=number+repoproyects.toList().size(); + } + + + log.info("Closed projects "+number); builder = new ReportItem.ReportItemBuilder("closedProjects", - proyectos.size()); - log.info("Proyectos "+proyectos); + number); + for(GHProject pro:proyectos) { log.info("Proyecto "+pro.getName()+" en estado "+pro.getState()); } builder.source("GitHub"); - } catch (ReportItemException | IOException e) { + } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); + } } diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index 00f1f898..ae1c5771 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -13,7 +13,6 @@ import org.kohsuke.github.GHIssue; import org.kohsuke.github.GHIssueState; -import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHPullRequest; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHRepositoryStatistics; @@ -34,6 +33,7 @@ * GitHubOrganizationEnquirer para evitar la replicación de código * deuda técnica: * las métricas tras la etiqueta //equipo 3 tienen problemas, no están acordes al indicador para el que fueron creadas + * RECUERDA: las métricas tienen que estar incluidas en el fichero de configuración a4iDefault.json */ public class GitHubRepositoryEnquirer extends GitHubEnquirer { /** diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index ba84bd33..c317330f 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -160,6 +160,18 @@ "type": "java.lang.Integer", "description": "Issues asignados a un desarrollador en el último mes", "unit": "issues/month" + }, + { + "name": "totalPullReq", + "type": "java.lang.Integer", + "description": "Número de pull requests en un repositorio", + "unit": "PR" + }, + { + "name": "closedPullReq", + "type": "java.lang.Integer", + "description": "Número de pull requests cerradas en un repositorio", + "unit": "PR" } diff --git a/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java index eb500cc8..ca594df8 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java @@ -24,8 +24,8 @@ import us.muit.fs.a4i.control.IndicatorStrategy; import us.muit.fs.a4i.control.ReportManagerI; -import us.muit.fs.a4i.control.RepositoryCalculator; import us.muit.fs.a4i.control.strategies.IssuesRatioIndicatorStrategy; +import us.muit.fs.a4i.control.strategies.RepositoryCalculator; import us.muit.fs.a4i.exceptions.IndicatorException; import us.muit.fs.a4i.exceptions.NotAvailableMetricException; import us.muit.fs.a4i.exceptions.ReportItemException; diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java index f67914f4..c19d5b47 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.util.Date; @@ -34,7 +35,8 @@ import us.muit.fs.a4i.model.remote.GitHubOrganizationEnquirer; /** - * @author Roberto Lama + * @author Roberto Lama (Curso 22/23) + * RECUERDA: el token tiene que tener permiso de acceso a la organización para que los tests puedan ejecutarse * */ public class GitHubOrganizationEnquirerTest { @@ -53,7 +55,7 @@ void testGetPullRequest() throws MetricException, ReportItemException { // TEST 2: PullRequest ReportItem metricsPullRequest = ghEnquirer.getMetric("pullRequests", "MIT-FS"); - assertEquals(21, metricsPullRequest.getValue().intValue(), "el número de pullRequests no es el esperado"); // Tiene 22 pull requests + assertTrue(metricsPullRequest.getValue().intValue()>0, "el número de pullRequests no es el esperado"); // Tiene PR } @@ -69,7 +71,7 @@ void testGetRepositories() throws MetricException, ReportItemException { // TEST 3: Repositories ReportItem metricsRepositories = ghEnquirer.getMetric("repositories", "MIT-FS"); - assertEquals(12,metricsRepositories.getValue().intValue(), "El número de repositorios no es el esperado"); // Tiene 10 repositorios + assertTrue(metricsRepositories.getValue().intValue()>0, "El número de repositorios no es el esperado"); // Tiene repositorios } @@ -86,7 +88,7 @@ void testGetMembers() throws MetricException, ReportItemException { // TEST 4: Members ReportItem metricsMembers = ghEnquirer.getMetric("members", "MIT-FS"); - assertEquals(29,metricsMembers.getValue().intValue(), "El número de miembros no es el esperado"); // Tiene 30 miembros + assertTrue(metricsMembers.getValue().intValue()>0, "El número de miembros no es el esperado"); // Tiene 30 miembros } @@ -102,8 +104,8 @@ void testGetTeams() throws MetricException, ReportItemException { // TEST 5: Teams ReportItem metricsTeams = ghEnquirer.getMetric("teams", "MIT-FS"); + assertTrue(metricsTeams.getValue().intValue()>3, "El número equipos no es el esperado"); // Tiene más de 3 - assertEquals(2,metricsTeams.getValue().intValue(), "El número equipos no es el esperado"); // Tiene 2 teams } /** @@ -117,7 +119,7 @@ void testGetOpenProjects() throws MetricException, ReportItemException { // TEST 6: OpenProjects ReportItem op = ghEnquirer.getMetric("openProjects", "MIT-FS"); - assertEquals(0,op.getValue().intValue(),"El número de proyectos abiertos no es el esperado"); + assertTrue(op.getValue().intValue()>0,"El número de proyectos abiertos no es el esperado"); } /** @@ -132,7 +134,7 @@ void testGetClosedProjects() throws MetricException, ReportItemException { // TEST 7: ClosedProjects ReportItem metricsClosedProjects = ghEnquirer.getMetric("closedProjects", "MIT-FS"); - assertEquals(2,metricsClosedProjects.getValue().intValue(), "El número de proyectos cerrados no es el esperado"); + assertTrue(metricsClosedProjects.getValue().intValue()>0, "El número de proyectos cerrados no es el esperado"); } /** @@ -148,7 +150,7 @@ void testGetRepositoriesWithOpenPullRequest() throws MetricException, ReportItem // TEST 1: RepositoriesWithOpenPullRequest ReportItem metricsRepositoriesWithOpenPullRequest = ghEnquirer.getMetric("repositoriesWithOpenPullRequest", "MIT-FS"); - assertEquals(6,metricsRepositoriesWithOpenPullRequest.getValue().intValue(), "El número de repositorios con pull requests abiertos no es el esperado"); + assertTrue(metricsRepositoriesWithOpenPullRequest.getValue().intValue()>0, "El número de repositorios con pull requests abiertos no es el esperado"); } } From 2bd6028bbd17fa13bdd0aac71d44467270237b4a Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Sun, 23 Feb 2025 17:14:14 +0100 Subject: [PATCH 076/100] Cerrando equipos 3 y 4 --- .../DeveloperPerformanceStrategy.java | 2 +- .../remote/GitHubRepositoryEnquirer.java | 2 +- src/main/resources/a4iDefault.json | 49 ++++++++++++++----- .../remote/GitHubRepositoryEnquirerTest.java | 22 ++++----- 4 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java index eefb4644..c7934ce0 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java @@ -61,7 +61,7 @@ else if (meanClosedIssuesLastMonth.get().getValue() != 0) try { // Se crea el indicador - indicatorReport = new ReportItem.ReportItemBuilder("rendimientoMiembro", rendimientoMiembro) + indicatorReport = new ReportItem.ReportItemBuilder("developerPerformance", rendimientoMiembro) .metrics(Arrays.asList(issuesLastMonth.get(), closedIssuesLastMonth.get(), issues4DevLastMonth.get(), meanClosedIssuesLastMonth.get())) .indicator(IndicatorState.UNDEFINED).build(); diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index ae1c5771..55748f42 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -265,7 +265,7 @@ private ReportItem getMetric(String metricName, GHRepository remoteRepo) throws metric = issues4DevLastMonth(remoteRepo); break; //equipo 4 - case "pullResquestTotales": + case "totalPullReq": metric = getTotalPullReq(remoteRepo); break; case "closedPullReq": diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index c317330f..b03877c9 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -1,5 +1,6 @@ { - "metrics": [{ + "metrics": [ + { "name": "subscribers", "type": "java.lang.Integer", "description": "Número de suscriptores de un repositorio, watchers en la web", @@ -137,19 +138,19 @@ "description": "Número de repositorios con pull requests pendientes", "unit": "repositories" }, - { + { "name": "issuesLastMonth", "type": "java.lang.Integer", "description": "Número de trabajos/issues creados en el mes", "unit": "issues/month" }, - { + { "name": "closedIssuesLastMonth", "type": "java.lang.Integer", "description": "Número de repositorios trabajos/issues concluidos en el mes", "unit": "issues/month" }, - { + { "name": "meanClosedIssuesLastMonth", "type": "java.lang.Double", "description": "Media de issues cerrados en el pasado mes por desarrollador", @@ -161,40 +162,64 @@ "description": "Issues asignados a un desarrollador en el último mes", "unit": "issues/month" }, - { + { "name": "totalPullReq", "type": "java.lang.Integer", "description": "Número de pull requests en un repositorio", "unit": "PR" }, - { + { "name": "closedPullReq", "type": "java.lang.Integer", "description": "Número de pull requests cerradas en un repositorio", "unit": "PR" } - - ], - "indicators": [{ + "indicators": [ + { "name": "issuesProgress", "type": "java.lang.Double", "description": "Ratio de issues cerrados frente a totales", "unit": "ratio", - "limits": { "ok": 2, "warning": 4, "critical": 6 } + "limits": { + "ok": 2, + "warning": 4, + "critical": 6 + } }, { "name": "overdued", "type": "java.lang.Double", "description": "Ratio de issues vencidos frente a abiertos", "unit": "ratio", - "limits": { "ok": 5, "warning": 9, "critical": 12 } + "limits": { + "ok": 5, + "warning": 9, + "critical": 12 + } }, { "name": "issuesRatio", "type": "java.lang.Double", "description": "Ratio de issues abiertos frente a cerrados", "unit": "ratio" - } + }, + { + "name": "pullRequestCompletion", + "type": "java.lang.Double", + "description": "% PR cerrados frente a totales", + "unit": "%", + "limits": { + "ok": 75, + "warning": 50, + "critical": 25 + } + }, + { + "name": "developerPerfomance", + "type": "java.lang.Double", + "description": "Ratio de issues cerradas frente a creadas del desarrollador frente al equipo", + "unit": "ratio" + }, ] } \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java index e890ee30..82523ad4 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java @@ -83,11 +83,8 @@ void testGetTotalPullRequests() throws MetricException { String repositoryId = "MIT-FS/Audit4Improve-API"; // Variable para almacenar el número total de pull requests - ReportItem metrica = null; - - // Número total de pull requests que habrá en el repositorio - int numPullRequests = 61; - + ReportItem metrica = null; + // Creamos el RemoteEnquirer para el repositorio GitHub GitHubRepositoryEnquirer enquirer = new GitHubRepositoryEnquirer(); @@ -96,8 +93,8 @@ void testGetTotalPullRequests() throws MetricException { log.info(metrica.toString()); // Comprobamos que el resultado coincida con el número total de pull requests real - assertEquals(numPullRequests, metrica.getValue()); - + assertTrue(metrica.getValue()>0,"Bad number of PR"); + } /** @@ -107,17 +104,15 @@ void testGetTotalPullRequests() throws MetricException { void testCompletedPullRequests() throws MetricException { // Nombre de la métrica que queremos consultar - String nombreMetrica = "completedPullReq"; + String nombreMetrica = "closedPullReq"; // Repositorio del que se quiere obtener la métrica String repositoryId = "MIT-FS/Audit4Improve-API"; // Variable para almacenar el número de pull requests completados - ReportItem metrica = null; - - // Número de pull requests completados que habrá en el repositorio - int numCompletedPull = 51; + ReportItem metrica = null; + // Creamos el RemoteEnquirer para el repositorio GitHub GitHubRepositoryEnquirer enquirer = new GitHubRepositoryEnquirer(); @@ -126,7 +121,8 @@ void testCompletedPullRequests() throws MetricException { log.info(metrica.toString()); // Comprobamos que el resultado coincida con el número de pull requests completados real - assertEquals(numCompletedPull, metrica.getValue()); + + assertTrue(0 Date: Sun, 23 Feb 2025 17:59:04 +0100 Subject: [PATCH 077/100] PRPerformance Indicator Equipo 5 --- .../strategies/PRPerformanceStrategy.java | 101 ++++++++++++ .../remote/GitHubRepositoryEnquirer.java | 156 ++++++++++++++++++ src/main/resources/a4iDefault.json | 38 ++++- .../a4i/test/control/ReportManagerTest.java | 2 +- .../strategies/PRPerfomanceStrategyTest.java | 92 +++++++++++ .../RepositoryCalculatorTest.java | 3 +- 6 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 src/main/java/us/muit/fs/a4i/control/strategies/PRPerformanceStrategy.java create mode 100644 src/test/java/us/muit/fs/a4i/test/control/strategies/PRPerfomanceStrategyTest.java rename src/test/java/us/muit/fs/a4i/test/control/{ => strategies}/RepositoryCalculatorTest.java (98%) diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/PRPerformanceStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/PRPerformanceStrategy.java new file mode 100644 index 00000000..e2c319dc --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/control/strategies/PRPerformanceStrategy.java @@ -0,0 +1,101 @@ +/** + * + */ +package us.muit.fs.a4i.control.strategies; + +/** + * Strategy for the calculation of the indicator PRPerformance + */ + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; + +import us.muit.fs.a4i.control.IndicatorStrategy; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; + +public class PRPerformanceStrategy implements IndicatorStrategy { + + private static Logger log = Logger.getLogger(Indicator.class.getName()); + + // M�tricas necesarias para calcular el indicador + private static final List REQUIRED_METRICS = Arrays.asList("pullRequestsAcceptedLastYear", + "pullRequestsAcceptedLastMonth", + "pullRequestsRejectedLastMonth", + "pullRequestsRejectedLastYear"); + + @Override + public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { + + // Obtener las métricas necesarias + Optional> pullRequestsAcceptedLastYear = metrics.stream() + .filter(m -> "pullRequestsAcceptedLastYear".equals(m.getName())) + .findAny(); + Optional> pullRequestsAcceptedLastMonth = metrics.stream() + .filter(m -> "pullRequestsAcceptedLastMonth".equals(m.getName())) + .findAny(); + Optional> pullRequestsRejectedLastYear = metrics.stream() + .filter(m -> "pullRequestsRejectedLastYear".equals(m.getName())) + .findAny(); + Optional> pullRequestsRejectedLastMonth = metrics.stream() + .filter(m -> "pullRequestsRejectedLastMonth".equals(m.getName())) + .findAny(); + + ReportItemI indicatorReport = null; + + // Comprobar que todas las métricas necesarias están presentes + if (pullRequestsAcceptedLastYear.isPresent() && pullRequestsAcceptedLastMonth.isPresent() && + pullRequestsRejectedLastYear.isPresent() && pullRequestsRejectedLastMonth.isPresent()) { + + // Obtener los valores de las métricas + Double acceptedLastYear = pullRequestsAcceptedLastYear.get().getValue(); + Double acceptedLastMonth = pullRequestsAcceptedLastMonth.get().getValue(); + Double rejectedLastYear = pullRequestsRejectedLastYear.get().getValue(); + Double rejectedLastMonth = pullRequestsRejectedLastMonth.get().getValue(); + + // Calcular el ratio de issues + Double pullRequestsAcceptance = 0.0; + if (acceptedLastYear != 0 && acceptedLastMonth != 0) { + pullRequestsAcceptance = rejectedLastYear / acceptedLastYear - rejectedLastMonth / acceptedLastMonth; + } else { + // Si una compañía productora de software no ha hecho un PR en el último mes, mal va. + pullRequestsAcceptance = 1.0; + } + + try { + // Crear el indicador + indicatorReport = new ReportItem.ReportItemBuilder("PRPerformance", pullRequestsAcceptance) + .metrics(Arrays.asList( + pullRequestsAcceptedLastYear.get(), + pullRequestsAcceptedLastMonth.get(), + pullRequestsRejectedLastYear.get(), + pullRequestsRejectedLastMonth.get())) + .indicator(IndicatorState.OK) + .build(); + } catch (ReportItemException e) { + log.info("Error en ReportItemBuilder."); + e.printStackTrace(); + } + + } else { + log.info("No se han proporcionado las métricas necesarias"); + throw new NotAvailableMetricException(REQUIRED_METRICS.toString()); + } + + return indicatorReport; + } + + @Override + public List requiredMetrics() { + // Para calcular el indicador "IssuesRatio", ser�n necesarias las m�tricas + // "openIssues" y "closedIssues". + return REQUIRED_METRICS; + } +} + diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index 55748f42..76b83173 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -4,12 +4,15 @@ package us.muit.fs.a4i.model.remote; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.kohsuke.github.GHIssue; import org.kohsuke.github.GHIssueState; @@ -71,6 +74,11 @@ public GitHubRepositoryEnquirer() { //equipo 4 metricNames.add("totalPullReq"); metricNames.add("closedPullReq"); + //equipo 5 + metricNames.add("PRAcceptedLastYear"); + metricNames.add("PRAcceptedLastMonth"); + metricNames.add("PRRejectedLastYear"); + metricNames.add("PRRejectedLastMonth"); log.info("A�adidas m�tricas al GHRepositoryEnquirer"); } @@ -271,6 +279,18 @@ private ReportItem getMetric(String metricName, GHRepository remoteRepo) throws case "closedPullReq": metric = getClosedPullReq(remoteRepo); break; + case "PRAcceptedLastYear": + metric = getPRAcceptedLastYear(remoteRepo); + break; + case "PRAcceptedLastMonth": + metric = getPRAcceptedLastMonth(remoteRepo); + break; + case "PRRejectedLastYear": + metric = getPRRejectedLastYear(remoteRepo); + break; + case "PRRejectedLastMonth": + metric = getPRRejectedLastMonth(remoteRepo); + break; default: throw new MetricException("La métrica " + metricName + " no está definida para un repositorio"); } @@ -846,5 +866,141 @@ private ReportItem getTotalPullReq(GHRepository repo){ return builder.build(); } + //Equipo 5 + + /** + *

+ * Filtra las solicitudes de extracción (pull requests) según la fecha de creación y el estado de aceptación. + *

+ * + * @param pullRequests la lista de solicitudes de extracción a filtrar + * @param startDate la fecha de inicio del intervalo de tiempo para filtrar + * @param endDate la fecha de finalización del intervalo de tiempo para filtrar + * @param accepted indica si se deben filtrar las solicitudes de extracción aceptadas (true) o rechazadas (false) + * @return la lista de solicitudes de extracción que cumplen con los criterios de filtrado + */ + private static List filterPullRequests(List pullRequests, LocalDateTime startDate, LocalDateTime endDate, boolean accepted) { + return pullRequests.stream() + .filter(pr -> { + try { + LocalDateTime createdDate = pr.getCreatedAt().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); + return createdDate.isAfter(startDate) && createdDate.isBefore(endDate) && (accepted ? pr.isMerged() : !pr.isMerged()); + } catch (IOException e) { + log.warning("Failed to get creation date for PR #" + pr.getNumber() + "\n"+e); + return false; + } + }) + .collect(Collectors.toList()); + } + + /** + *

+ * Obtención del número de solicitudes de extracción aceptadas en el último año. + *

+ * + * @param remoteRepo el repositorio remoto sobre el que consultar + * @return la métrica con el número de solicitudes de extracción aceptadas en el último año + * @throws MetricException si ocurre un error al obtener las solicitudes de extracción + */ + private ReportItem getPRAcceptedLastYear(GHRepository remoteRepo) throws MetricException { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime oneYearAgo = now.minusYears(1); + + try { + List pullRequests = remoteRepo.getPullRequests(GHIssueState.CLOSED); + List acceptedLastYear = filterPullRequests(pullRequests, oneYearAgo, now, true); + + ReportItemBuilder acceptedLastYearMetric = new ReportItem.ReportItemBuilder<>("PRAcceptedLastYear", acceptedLastYear.size()); + acceptedLastYearMetric.source("GitHub, calculada") + .description("Número de solicitudes de extracción aceptadas en el último año"); + + return acceptedLastYearMetric.build(); + } catch (IOException | ReportItemException e) { + throw new MetricException("Error al obtener las solicitudes de extracción aceptadas en el último año\n" + e); + } + } + + /** + *

+ * Obtención del número de solicitudes de extracción aceptadas en el último mes. + *

+ * + * @param remoteRepo el repositorio remoto sobre el que consultar + * @return la métrica con el número de solicitudes de extracción aceptadas en el último mes + * @throws MetricException si ocurre un error al obtener las solicitudes de extracción + */ + private ReportItem getPRAcceptedLastMonth(GHRepository remoteRepo) throws MetricException { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime oneMonthAgo = now.minusMonths(1); + + try { + List pullRequests = remoteRepo.getPullRequests(GHIssueState.CLOSED); + List acceptedLastMonth = filterPullRequests(pullRequests, oneMonthAgo, now, true); + + ReportItemBuilder acceptedLastMonthMetric = new ReportItem.ReportItemBuilder<>("PRAcceptedLastMonth", acceptedLastMonth.size()); + acceptedLastMonthMetric.source("GitHub, calculada") + .description("Número de solicitudes de extracción aceptadas en el último mes"); + + return acceptedLastMonthMetric.build(); + } catch (IOException | ReportItemException e) { + throw new MetricException("Error al obtener las solicitudes de extracción aceptadas en el último mes\n" + e); + } + } + + + /** + *

+ * Obtención del número de solicitudes de extracción rechazadas en el último año. + *

+ * + * @param remoteRepo el repositorio remoto sobre el que consultar + * @return la métrica con el número de solicitudes de extracción rechazadas en el último año + * @throws MetricException si ocurre un error al obtener las solicitudes de extracción + */ + private ReportItem getPRRejectedLastYear(GHRepository remoteRepo) throws MetricException { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime oneYearAgo = now.minusYears(1); + + try { + List pullRequests = remoteRepo.getPullRequests(GHIssueState.CLOSED); + List rejectedLastYear = filterPullRequests(pullRequests, oneYearAgo, now, false); + + ReportItemBuilder rejectedLastYearMetric = new ReportItem.ReportItemBuilder<>("PRRejectedLastYear", rejectedLastYear.size()); + rejectedLastYearMetric.source("GitHub, calculada") + .description("Número de solicitudes de extracción rechazadas en el último año"); + + return rejectedLastYearMetric.build(); + } catch (IOException | ReportItemException e) { + throw new MetricException("Error al obtener las solicitudes de extracción rechazadas en el último año\n" + e); + } + } + + + /** + *

+ * Obtención del número de solicitudes de extracción rechazadas en el último mes. + *

+ * + * @param remoteRepo el repositorio remoto sobre el que consultar + * @return la métrica con el número de solicitudes de extracción rechazadas en el último mes + * @throws MetricException si ocurre un error al obtener las solicitudes de extracción + */ + private ReportItem getPRRejectedLastMonth(GHRepository remoteRepo) throws MetricException { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime oneMonthAgo = now.minusMonths(1); + + try { + List pullRequests = remoteRepo.getPullRequests(GHIssueState.CLOSED); + List rejectedLastMonth = filterPullRequests(pullRequests, oneMonthAgo, now, false); + + ReportItemBuilder rejectedLastMonthMetric = new ReportItem.ReportItemBuilder<>("PRRejectedLastMonth", rejectedLastMonth.size()); + rejectedLastMonthMetric.source("GitHub, calculada") + .description("Número de solicitudes de extracción rechazadas en el último mes"); + + return rejectedLastMonthMetric.build(); + } catch (IOException | ReportItemException e) { + throw new MetricException("Error al obtener las solicitudes de extracción rechazadas en el último mes\n" + e); + } + } } diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index b03877c9..43b1b526 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -173,7 +173,32 @@ "type": "java.lang.Integer", "description": "Número de pull requests cerradas en un repositorio", "unit": "PR" - } + }, + { + "name": "PRAcceptedLastYear", + "type": "java.lang.Integer", + "description": "Número de pull requests aceptadas el pasado año", + "unit": "PR" + }, + { + "name": "PRAcceptedLastMonth", + "type": "java.lang.Integer", + "description": "Número de pull requests aceptadas el pasado mes", + "unit": "PR" + }, + { + "name": "PRRejectedLastMonth", + "type": "java.lang.Integer", + "description": "Número de pull requests rechazadas el pasado mes", + "unit": "PR" + }, + { + "name": "PRRejectedLastYear", + "type": "java.lang.Integer", + "description": "Número de pull requests rechazadas el pasado año", + "unit": "PR" + } + ], "indicators": [ { @@ -221,5 +246,16 @@ "description": "Ratio de issues cerradas frente a creadas del desarrollador frente al equipo", "unit": "ratio" }, + { + "name": "PRPerformance", + "type": "java.lang.Double", + "description": "% de PR rechazados en el mes comparado con el % rechazado en el año", + "unit": "%", + "limits": { + "ok": 25, + "warning": 50, + "critical": 75 + } + } ] } \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java b/src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java index bd3141e6..ae721fec 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java @@ -22,7 +22,7 @@ * @author isabo * */ -class ReportManagerTest { +public class ReportManagerTest { private static Logger log = Logger.getLogger(ReportManagerTest.class.getName()); /** diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/PRPerfomanceStrategyTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/PRPerfomanceStrategyTest.java new file mode 100644 index 00000000..1e19fefc --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/PRPerfomanceStrategyTest.java @@ -0,0 +1,92 @@ +package us.muit.fs.a4i.test.control.strategies; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import us.muit.fs.a4i.control.strategies.PRPerformanceStrategy; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.model.entities.ReportItemI; + +class PRPerfomanceStrategyTest { + + public void testCalcIndicator() throws NotAvailableMetricException { + // Creamos los mocks necesarios + ReportItemI mockAcceptedLastYear = Mockito.mock(ReportItemI.class); + ReportItemI mockAcceptedLastMonth = Mockito.mock(ReportItemI.class); + ReportItemI mockRejectedLastYear = Mockito.mock(ReportItemI.class); + ReportItemI mockRejectedLastMonth = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para devolver valores predefinidos + Mockito.when(mockAcceptedLastYear.getName()).thenReturn("PRAcceptedLastYear"); + Mockito.when(mockAcceptedLastYear.getValue()).thenReturn((double)100); + + Mockito.when(mockAcceptedLastMonth.getName()).thenReturn("PRAcceptedLastMonth"); + Mockito.when(mockAcceptedLastMonth.getValue()).thenReturn((double)10); + + Mockito.when(mockRejectedLastYear.getName()).thenReturn("PRRejectedLastYear"); + Mockito.when(mockRejectedLastYear.getValue()).thenReturn((double)20); + + Mockito.when(mockRejectedLastMonth.getName()).thenReturn("PRRejectedLastMonth"); + Mockito.when(mockRejectedLastMonth.getValue()).thenReturn((double)2); + + // Creamos una instancia de PRPerformanceStrategy + PRPerformanceStrategy indicator = new PRPerformanceStrategy(); + + // Ejecutamos el método que queremos probar con los mocks como argumentos + List> metrics = Arrays.asList( + mockAcceptedLastYear, + mockAcceptedLastMonth, + mockRejectedLastYear, + mockRejectedLastMonth + ); + ReportItemI result = indicator.calcIndicator(metrics); + + // Comprobamos que el resultado es el esperado + Assertions.assertEquals("PRPerformance", result.getName()); + Assertions.assertEquals((double)100/20-(double)10/2, result.getValue()); + Assertions.assertDoesNotThrow(() -> indicator.calcIndicator(metrics)); + } + + @Test + public void testCalcIndicatorThrowsNotAvailableMetricException() { + // Creamos los mocks necesarios + ReportItemI mockAcceptedLastYear = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para devolver valores predefinidos + Mockito.when(mockAcceptedLastYear.getName()).thenReturn("PRAcceptedLastYear"); + Mockito.when(mockAcceptedLastYear.getValue()).thenReturn((double)100); + + // Creamos una instancia de PullRequestsAcceptanceIndicatorStrategy + PRPerformanceStrategy indicator = new PRPerformanceStrategy(); + + // Ejecutamos el método que queremos probar con una sola métrica + List> metrics = Arrays.asList(mockAcceptedLastYear); + // Comprobamos que se lanza la excepción adecuada + NotAvailableMetricException exception = Assertions.assertThrows(NotAvailableMetricException.class, + () -> indicator.calcIndicator(metrics)); + } + + @Test + public void testRequiredMetrics() { + // Creamos una instancia de PRPerformanceStrategy + PRPerformanceStrategy indicatorStrategy = new PRPerformanceStrategy(); + + // Ejecutamos el método que queremos probar + List requiredMetrics = indicatorStrategy.requiredMetrics(); + + // Comprobamos que el resultado es el esperado + List expectedMetrics = Arrays.asList( + "PRAcceptedLastYear", + "PRAcceptedLastMonth", + "PRRejectedLastMonth", + "PRRejectedLastYear" + ); + Assertions.assertEquals(expectedMetrics, requiredMetrics); + } +} diff --git a/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/RepositoryCalculatorTest.java similarity index 98% rename from src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java rename to src/test/java/us/muit/fs/a4i/test/control/strategies/RepositoryCalculatorTest.java index ca594df8..0a9d9fb5 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/RepositoryCalculatorTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/RepositoryCalculatorTest.java @@ -1,4 +1,4 @@ -package us.muit.fs.a4i.test.control; +package us.muit.fs.a4i.test.control.strategies; /*** * @author celllarod, curso 22/23 @@ -33,6 +33,7 @@ import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItem; import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; +import us.muit.fs.a4i.test.control.ReportManagerTest; import us.muit.fs.a4i.model.entities.ReportItemI; import static org.junit.jupiter.api.Assertions.assertThrows; From 2d1acd9991877d404b3547ff983447e19aca2d52 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Sun, 23 Feb 2025 18:30:33 +0100 Subject: [PATCH 078/100] Indicador fixTime equipo 6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit trabajo muy inacabado, las métricas no están trabajadas --- .../control/strategies/FixTimeStrategy.java | 76 ++++++++++++++++ .../strategies/PRPerformanceStrategy.java | 1 + .../strategies/FixTimeStrategyTest.java | 90 +++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 src/main/java/us/muit/fs/a4i/control/strategies/FixTimeStrategy.java create mode 100644 src/test/java/us/muit/fs/a4i/test/control/strategies/FixTimeStrategyTest.java diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/FixTimeStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/FixTimeStrategy.java new file mode 100644 index 00000000..101871aa --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/control/strategies/FixTimeStrategy.java @@ -0,0 +1,76 @@ +/** + * + */ +package us.muit.fs.a4i.control.strategies; + +/** + * + */ +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; + +import us.muit.fs.a4i.control.IndicatorStrategy; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; +/** + * Estrategia de indicador equipo 6 del curso 23/24, no coincide con lo que decían ni las métricas están incluidas en ningún Enquirer + * Aparece una clase Equirer MetricasG6.java pero es una copia incompleta del que se les da y no añade los indicadores que aquí aparecen + * REMEMBER: metrics must be included in a4iDefault.json + */ + +public class FixTimeStrategy implements IndicatorStrategy { + + private static Logger log = Logger.getLogger(Indicator.class.getName()); + + // M�tricas necesarias para calcular el indicador + private static final List REQUIRED_METRICS = Arrays.asList("clasificacion", "correccion", "num_errores"); + + @Override + public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { + // Se obtienen y se comprueba que se pasan las m�tricas necesarias para calcular + // el indicador. + Optional> clasificacion = metrics.stream().filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); + Optional> correccion = metrics.stream().filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); + Optional> num_errores = metrics.stream().filter(m -> REQUIRED_METRICS.get(2).equals(m.getName())).findAny(); + ReportItemI indicatorReport = null; + + if (clasificacion.isPresent() && correccion.isPresent() && num_errores.isPresent()) { + Double procesoDeIssues; + + // Se realiza el c�lculo del indicador + if(clasificacion.get().getValue()!=0 && correccion.get().getValue()!=0 && num_errores.get().getValue()!=0) + procesoDeIssues = (clasificacion.get().getValue()+correccion.get().getValue())/num_errores.get().getValue(); + else + procesoDeIssues = 0.0; + + try { + // Se crea el indicador + indicatorReport = new ReportItem.ReportItemBuilder("fixTime", procesoDeIssues) + .metrics(Arrays.asList(clasificacion.get(), correccion.get(), num_errores.get())) + .indicator(IndicatorState.UNDEFINED).build(); + } catch (ReportItemException e) { + log.info("Error en ReportItemBuilder."); + e.printStackTrace(); + } + + } else { + log.info("No se han proporcionado las m�tricas necesarias"); + throw new NotAvailableMetricException(REQUIRED_METRICS.toString()); + } + + return indicatorReport; + } + + @Override + public List requiredMetrics() { + // Para calcular el indicador "IssuesRatio", ser�n necesarias las m�tricas + // "openIssues" y "closedIssues". + return REQUIRED_METRICS; + } +} \ No newline at end of file diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/PRPerformanceStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/PRPerformanceStrategy.java index e2c319dc..ecb2ee40 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/PRPerformanceStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/PRPerformanceStrategy.java @@ -5,6 +5,7 @@ /** * Strategy for the calculation of the indicator PRPerformance + * REMEMBER: metrics must be included in a4iDefault.json */ import java.util.Arrays; diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/FixTimeStrategyTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/FixTimeStrategyTest.java new file mode 100644 index 00000000..dbcd2a57 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/FixTimeStrategyTest.java @@ -0,0 +1,90 @@ +package us.muit.fs.a4i.test.control.strategies; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; + +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.mockito.Mockito; + +import us.muit.fs.a4i.control.strategies.FixTimeStrategy; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.model.entities.ReportItemI; + +class FixTimeStrategyTest { + + @Test + public void testCalcIndicator() throws NotAvailableMetricException { + // Creamos los mocks necesarios + ReportItemI mockClasificacion = Mockito.mock(ReportItemI.class); + ReportItemI mockCorreccion = Mockito.mock(ReportItemI.class); + ReportItemI mockNum_errores = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para devolver valores predefinidos + Mockito.when(mockClasificacion.getName()).thenReturn("clasificacion"); + Mockito.when(mockClasificacion.getValue()).thenReturn(10.0); + + Mockito.when(mockCorreccion.getName()).thenReturn("correccion"); + Mockito.when(mockCorreccion.getValue()).thenReturn(5.0); + + Mockito.when(mockNum_errores.getName()).thenReturn("num_errores"); + Mockito.when(mockNum_errores.getValue()).thenReturn(5.0); + + // Creamos una instancia de la estrategia + FixTimeStrategy strategy = new FixTimeStrategy(); + + // Ejecutamos el método que queremos probar con los mocks como argumentos + List> metrics = Arrays.asList(mockClasificacion, mockCorreccion, mockNum_errores); + ReportItemI result = strategy.calcIndicator(metrics); + + // Comprobamos que el resultado es el esperado + Assertions.assertEquals("ProcesoDeIssues", result.getName()); + Assertions.assertEquals(3.0, result.getValue()); + Assertions.assertDoesNotThrow(()->strategy.calcIndicator(metrics)); + + + } + + @Test + public void testCalcIndicatorThrowsNotAvailableMetricException() { + // Creamos los mocks necesarios + ReportItemI mockClasificacion = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para devolver valores predefinidos + Mockito.when(mockClasificacion.getName()).thenReturn("clasificacion"); + Mockito.when(mockClasificacion.getValue()).thenReturn(10.0); + + // Creamos una instancia de la estrategia + FixTimeStrategy strategy = new FixTimeStrategy(); + + // Ejecutamos el método que queremos probar con una sola métrica + List> metrics = Arrays.asList(mockClasificacion); + // Comprobamos que se lanza la excepción adecuada + NotAvailableMetricException exception = Assertions.assertThrows(NotAvailableMetricException.class, + () -> strategy.calcIndicator(metrics)); + + } + + + @Test + public void testRequiredMetrics() { + + // Creamos una instancia de la estrategia + FixTimeStrategy strategy = new FixTimeStrategy(); + + // Ejecutamos el método que queremos probar + List requiredMetrics = strategy.requiredMetrics(); + + // Comprobamos que el resultado es el esperado + List expectedMetrics = Arrays.asList("clasificacion", "correccion", "num_errores"); + Assertions.assertEquals(expectedMetrics, requiredMetrics); + } + } \ No newline at end of file From a0c81a1ee99aecaf69c1180cd50f15e412463581 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Sun, 23 Feb 2025 18:56:05 +0100 Subject: [PATCH 079/100] Indicador repoActivity Equipo 8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trabajo no terminado, falta la obtención de métricas y tests También se ha revisado el formato --- .../fs/a4i/control/IndicatorStrategy.java | 25 +- .../fs/a4i/control/IndicatorsCalculator.java | 2 +- .../us/muit/fs/a4i/control/package-info.java | 5 +- .../DeveloperPerformanceStrategy.java | 4 +- .../control/strategies/FixTimeStrategy.java | 26 +- .../IssuesRatioIndicatorStrategy.java | 5 +- .../strategies/PRPerformanceStrategy.java | 108 ++--- .../PullRequestIndicatorStrategy.java | 37 +- .../strategies/RepoActivityStrategy.java | 124 ++++++ .../strategies/RepositoryCalculator.java | 8 +- .../model/remote/GitHubDeveloperEnquirer.java | 51 ++- .../remote/GitHubOrganizationEnquirer.java | 220 +++++---- .../remote/GitHubRepositoryEnquirer.java | 419 ++++++++++-------- .../a4i/persistence/ExcelReportManager.java | 27 +- .../fs/a4i/persistence/ReportFormaterI.java | 1 - src/main/resources/a4iDefault.json | 37 +- .../control/IssuesRatioIndicatorTest.java | 124 +++--- .../a4i/test/control/SupervisorControl.java | 6 +- .../DeveloperPerformanceIndicatorTest.java | 71 +-- .../strategies/FixTimeStrategyTest.java | 132 +++--- .../strategies/PRPerfomanceStrategyTest.java | 138 +++--- .../PullRequestIndicatorStrategyTest.java | 149 +++---- .../strategies/RepositoryCalculatorTest.java | 3 +- .../remote/GitHubDeveloperEnquirerTest.java | 12 +- .../GitHubOrganizationEnquirerTest.java | 137 +++--- .../remote/GitHubRepositoryEnquirerTest.java | 104 +++-- .../persistence/ExcelReportManagerTest.java | 107 ++--- ...ExcelReportWithRepositoryEnquirerTest.java | 13 +- 28 files changed, 1116 insertions(+), 979 deletions(-) create mode 100644 src/main/java/us/muit/fs/a4i/control/strategies/RepoActivityStrategy.java diff --git a/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java b/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java index 634dad0a..608406fb 100644 --- a/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/IndicatorStrategy.java @@ -10,27 +10,28 @@ *

* Interfaz para calcular indicadores *

+ * * @param * @author celllarod * */ public interface IndicatorStrategy { - - /** - * Calcula un indicador a partir de las métricas proporcionadas. - * @param - * @param metrics - * @throws NotAvailableMetricException - * @return indicador - */ - public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException; - - + /** + * Calcula un indicador a partir de las métricas proporcionadas. + * + * @param + * @param metrics + * @throws NotAvailableMetricException + * @return indicador + */ + public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException; + /** * Obtiene las métricas necesarias + * * @return listado de métricas */ public List requiredMetrics(); - + } diff --git a/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java b/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java index 260239e2..75be7aab 100644 --- a/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java @@ -54,6 +54,6 @@ public interface IndicatorsCalculator { * @return El tipo de informes */ public ReportI.ReportType getReportType(); - + public void setIndicator(String indicatorName, IndicatorStrategy strategy); } diff --git a/src/main/java/us/muit/fs/a4i/control/package-info.java b/src/main/java/us/muit/fs/a4i/control/package-info.java index 2966d62a..f416de30 100644 --- a/src/main/java/us/muit/fs/a4i/control/package-info.java +++ b/src/main/java/us/muit/fs/a4i/control/package-info.java @@ -1,10 +1,11 @@ /** *

* Clases e interfaces controladoras - *

- * + * Paquete para clases controladoras + * * @author Isabel Román * @version V.0.2 */ diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java index c7934ce0..78f310bb 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/DeveloperPerformanceStrategy.java @@ -21,8 +21,8 @@ * the rest of developers * * @author fracrusan (year 23/24) - * @author Isabel Román - * RECUERDA: los indicadores tienen que estar incluidos en el fichero de configuración a4iDefault.json + * @author Isabel Román RECUERDA: los indicadores tienen que estar incluidos en + * el fichero de configuración a4iDefault.json */ public class DeveloperPerformanceStrategy implements IndicatorStrategy { diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/FixTimeStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/FixTimeStrategy.java index 101871aa..ded05153 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/FixTimeStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/FixTimeStrategy.java @@ -18,10 +18,13 @@ import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; import us.muit.fs.a4i.model.entities.ReportItem; import us.muit.fs.a4i.model.entities.ReportItemI; + /** - * Estrategia de indicador equipo 6 del curso 23/24, no coincide con lo que decían ni las métricas están incluidas en ningún Enquirer - * Aparece una clase Equirer MetricasG6.java pero es una copia incompleta del que se les da y no añade los indicadores que aquí aparecen - * REMEMBER: metrics must be included in a4iDefault.json + * Estrategia de indicador equipo 6 del curso 23/24, no coincide con lo que + * decían ni las métricas están incluidas en ningún Enquirer Aparece una clase + * Equirer MetricasG6.java pero es una copia incompleta del que se les da y no + * añade los indicadores que aquí aparecen REMEMBER: metrics must be included in + * a4iDefault.json */ public class FixTimeStrategy implements IndicatorStrategy { @@ -35,17 +38,22 @@ public class FixTimeStrategy implements IndicatorStrategy { public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { // Se obtienen y se comprueba que se pasan las m�tricas necesarias para calcular // el indicador. - Optional> clasificacion = metrics.stream().filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); - Optional> correccion = metrics.stream().filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); - Optional> num_errores = metrics.stream().filter(m -> REQUIRED_METRICS.get(2).equals(m.getName())).findAny(); + Optional> clasificacion = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); + Optional> correccion = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); + Optional> num_errores = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(2).equals(m.getName())).findAny(); ReportItemI indicatorReport = null; if (clasificacion.isPresent() && correccion.isPresent() && num_errores.isPresent()) { Double procesoDeIssues; // Se realiza el c�lculo del indicador - if(clasificacion.get().getValue()!=0 && correccion.get().getValue()!=0 && num_errores.get().getValue()!=0) - procesoDeIssues = (clasificacion.get().getValue()+correccion.get().getValue())/num_errores.get().getValue(); + if (clasificacion.get().getValue() != 0 && correccion.get().getValue() != 0 + && num_errores.get().getValue() != 0) + procesoDeIssues = (clasificacion.get().getValue() + correccion.get().getValue()) + / num_errores.get().getValue(); else procesoDeIssues = 0.0; @@ -64,7 +72,7 @@ public ReportItemI calcIndicator(List> metrics) thro throw new NotAvailableMetricException(REQUIRED_METRICS.toString()); } - return indicatorReport; + return indicatorReport; } @Override diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/IssuesRatioIndicatorStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/IssuesRatioIndicatorStrategy.java index 50961e36..38f62f4c 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/IssuesRatioIndicatorStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/IssuesRatioIndicatorStrategy.java @@ -12,9 +12,10 @@ import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; import us.muit.fs.a4i.model.entities.ReportItem; import us.muit.fs.a4i.model.entities.ReportItemI; + /** - * @author alumnos del curso 23/24, grupo 3 - * RECUERDA: los indicadores tienen que estar incluidos en el fichero de configuración a4iDefault.json + * @author alumnos del curso 23/24, grupo 3 RECUERDA: los indicadores tienen que + * estar incluidos en el fichero de configuración a4iDefault.json */ public class IssuesRatioIndicatorStrategy implements IndicatorStrategy { diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/PRPerformanceStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/PRPerformanceStrategy.java index ecb2ee40..e6c8ab9f 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/PRPerformanceStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/PRPerformanceStrategy.java @@ -26,70 +26,61 @@ public class PRPerformanceStrategy implements IndicatorStrategy { private static Logger log = Logger.getLogger(Indicator.class.getName()); // M�tricas necesarias para calcular el indicador - private static final List REQUIRED_METRICS = Arrays.asList("pullRequestsAcceptedLastYear", - "pullRequestsAcceptedLastMonth", - "pullRequestsRejectedLastMonth", - "pullRequestsRejectedLastYear"); + private static final List REQUIRED_METRICS = Arrays.asList("pullRequestsAcceptedLastYear", + "pullRequestsAcceptedLastMonth", "pullRequestsRejectedLastMonth", "pullRequestsRejectedLastYear"); @Override public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { - // Obtener las métricas necesarias - Optional> pullRequestsAcceptedLastYear = metrics.stream() - .filter(m -> "pullRequestsAcceptedLastYear".equals(m.getName())) - .findAny(); - Optional> pullRequestsAcceptedLastMonth = metrics.stream() - .filter(m -> "pullRequestsAcceptedLastMonth".equals(m.getName())) - .findAny(); - Optional> pullRequestsRejectedLastYear = metrics.stream() - .filter(m -> "pullRequestsRejectedLastYear".equals(m.getName())) - .findAny(); - Optional> pullRequestsRejectedLastMonth = metrics.stream() - .filter(m -> "pullRequestsRejectedLastMonth".equals(m.getName())) - .findAny(); - - ReportItemI indicatorReport = null; - - // Comprobar que todas las métricas necesarias están presentes - if (pullRequestsAcceptedLastYear.isPresent() && pullRequestsAcceptedLastMonth.isPresent() && - pullRequestsRejectedLastYear.isPresent() && pullRequestsRejectedLastMonth.isPresent()) { - - // Obtener los valores de las métricas - Double acceptedLastYear = pullRequestsAcceptedLastYear.get().getValue(); - Double acceptedLastMonth = pullRequestsAcceptedLastMonth.get().getValue(); - Double rejectedLastYear = pullRequestsRejectedLastYear.get().getValue(); - Double rejectedLastMonth = pullRequestsRejectedLastMonth.get().getValue(); - - // Calcular el ratio de issues - Double pullRequestsAcceptance = 0.0; - if (acceptedLastYear != 0 && acceptedLastMonth != 0) { - pullRequestsAcceptance = rejectedLastYear / acceptedLastYear - rejectedLastMonth / acceptedLastMonth; - } else { - // Si una compañía productora de software no ha hecho un PR en el último mes, mal va. - pullRequestsAcceptance = 1.0; - } - - try { - // Crear el indicador - indicatorReport = new ReportItem.ReportItemBuilder("PRPerformance", pullRequestsAcceptance) - .metrics(Arrays.asList( - pullRequestsAcceptedLastYear.get(), - pullRequestsAcceptedLastMonth.get(), - pullRequestsRejectedLastYear.get(), - pullRequestsRejectedLastMonth.get())) - .indicator(IndicatorState.OK) - .build(); - } catch (ReportItemException e) { - log.info("Error en ReportItemBuilder."); - e.printStackTrace(); - } - - } else { - log.info("No se han proporcionado las métricas necesarias"); + // Obtener las métricas necesarias + Optional> pullRequestsAcceptedLastYear = metrics.stream() + .filter(m -> "pullRequestsAcceptedLastYear".equals(m.getName())).findAny(); + Optional> pullRequestsAcceptedLastMonth = metrics.stream() + .filter(m -> "pullRequestsAcceptedLastMonth".equals(m.getName())).findAny(); + Optional> pullRequestsRejectedLastYear = metrics.stream() + .filter(m -> "pullRequestsRejectedLastYear".equals(m.getName())).findAny(); + Optional> pullRequestsRejectedLastMonth = metrics.stream() + .filter(m -> "pullRequestsRejectedLastMonth".equals(m.getName())).findAny(); + + ReportItemI indicatorReport = null; + + // Comprobar que todas las métricas necesarias están presentes + if (pullRequestsAcceptedLastYear.isPresent() && pullRequestsAcceptedLastMonth.isPresent() + && pullRequestsRejectedLastYear.isPresent() && pullRequestsRejectedLastMonth.isPresent()) { + + // Obtener los valores de las métricas + Double acceptedLastYear = pullRequestsAcceptedLastYear.get().getValue(); + Double acceptedLastMonth = pullRequestsAcceptedLastMonth.get().getValue(); + Double rejectedLastYear = pullRequestsRejectedLastYear.get().getValue(); + Double rejectedLastMonth = pullRequestsRejectedLastMonth.get().getValue(); + + // Calcular el ratio de issues + Double pullRequestsAcceptance = 0.0; + if (acceptedLastYear != 0 && acceptedLastMonth != 0) { + pullRequestsAcceptance = rejectedLastYear / acceptedLastYear - rejectedLastMonth / acceptedLastMonth; + } else { + // Si una compañía productora de software no ha hecho un PR en el último mes, + // mal va. + pullRequestsAcceptance = 1.0; + } + + try { + // Crear el indicador + indicatorReport = new ReportItem.ReportItemBuilder("PRPerformance", pullRequestsAcceptance) + .metrics(Arrays.asList(pullRequestsAcceptedLastYear.get(), pullRequestsAcceptedLastMonth.get(), + pullRequestsRejectedLastYear.get(), pullRequestsRejectedLastMonth.get())) + .indicator(IndicatorState.OK).build(); + } catch (ReportItemException e) { + log.info("Error en ReportItemBuilder."); + e.printStackTrace(); + } + + } else { + log.info("No se han proporcionado las métricas necesarias"); throw new NotAvailableMetricException(REQUIRED_METRICS.toString()); - } + } - return indicatorReport; + return indicatorReport; } @Override @@ -99,4 +90,3 @@ public List requiredMetrics() { return REQUIRED_METRICS; } } - diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/PullRequestIndicatorStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/PullRequestIndicatorStrategy.java index 019e35a9..f83562e9 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/PullRequestIndicatorStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/PullRequestIndicatorStrategy.java @@ -17,8 +17,10 @@ /** * % de pull requests cerrados, sobre el total - * @author Sergio García López (equipo 4 del curso 23/24) - * RECUERDA: los indicadores tienen que estar incluidos en el fichero de configuración a4iDefault.json + * + * @author Sergio García López (equipo 4 del curso 23/24) RECUERDA: los + * indicadores tienen que estar incluidos en el fichero de configuración + * a4iDefault.json * */ public class PullRequestIndicatorStrategy implements IndicatorStrategy { @@ -30,19 +32,17 @@ public class PullRequestIndicatorStrategy implements IndicatorStrategy { @Override public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { - // Indicador a devolver + // Indicador a devolver ReportItemI indicatorReport = null; // Estado del indicador IndicatorState estado = IndicatorState.UNDEFINED; - + Optional> totalPullReq = metrics.stream() .filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); Optional> closedPullReq = metrics.stream() .filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); - - if (totalPullReq.isPresent() && closedPullReq.isPresent()) { // Calculamos el indicador @@ -50,24 +50,23 @@ public ReportItemI calcIndicator(List> metrics) thro if (totalPullReq.get().getValue() > 0) { pullRequestCompletion = 100 * closedPullReq.get().getValue() / totalPullReq.get().getValue(); - - - // Criterios de calidad (porcentuales) - //estos límites deben estar configurados en el fichero a4iDefault.json, no aquí - if (pullRequestCompletion > 75) { - estado = IndicatorState.OK; - } else if (pullRequestCompletion > 50) { - estado = IndicatorState.WARNING; - } else { - estado = IndicatorState.CRITICAL; - } + + // Criterios de calidad (porcentuales) + // estos límites deben estar configurados en el fichero a4iDefault.json, no aquí + if (pullRequestCompletion > 75) { + estado = IndicatorState.OK; + } else if (pullRequestCompletion > 50) { + estado = IndicatorState.WARNING; + } else { + estado = IndicatorState.CRITICAL; + } } try { indicatorReport = new ReportItem.ReportItemBuilder("pullRequestCompletion", - pullRequestCompletion).metrics(Arrays.asList(totalPullReq.get(), closedPullReq.get())).indicator(estado) - .build(); + pullRequestCompletion).metrics(Arrays.asList(totalPullReq.get(), closedPullReq.get())) + .indicator(estado).build(); } catch (ReportItemException e) { log.info("Error en ReportItemBuilder: " + e); diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/RepoActivityStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/RepoActivityStrategy.java new file mode 100644 index 00000000..82ce0fc8 --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/control/strategies/RepoActivityStrategy.java @@ -0,0 +1,124 @@ +/** + * + */ +package us.muit.fs.a4i.control.strategies; + +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; + +import us.muit.fs.a4i.control.IndicatorStrategy; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; + +/** + * Clase para calcular el indicador compuesto de issues, pull requests y + * contribuyentes. + * + * @param Tipo de dato del indicador. + */ +public class RepoActivityStrategy implements IndicatorStrategy { + private static Logger log = Logger.getLogger(RepoActivityStrategy.class.getName()); + + // Métricas necesarias para calcular el indicador + private static final List REQUIRED_METRICS = Arrays.asList("closedIssues", "issues", "closedPullReq", + "totalPullReq", "activeContributor", "totalContributor"); + + @Override + public ReportItemI calcIndicator(List> metricas) throws NotAvailableMetricException { + + // Variables para almacenar las métricas + ReportItemI issuesCerrados = null; + ReportItemI issuesTotales = null; + ReportItemI pullRequestsCerrados = null; + ReportItemI pullRequestsTotales = null; + ReportItemI contribuyentesActivos = null; + ReportItemI contribuyentesTotales = null; + + // Indicador a devolver + ReportItemI indicatorReport = null; + + // Estado del indicador + IndicatorState estado = IndicatorState.UNDEFINED; + + // Buscamos las métricas en la lista + for (ReportItemI metrica : metricas) { + switch (metrica.getName()) { + case "closedIssues": + issuesCerrados = metrica; + break; + case "issues": + issuesTotales = metrica; + break; + case "closedPullReq": + pullRequestsCerrados = metrica; + break; + case "totalPullReq": + pullRequestsTotales = metrica; + break; + case "activeContributor": + contribuyentesActivos = metrica; + break; + case "totalContributor": + contribuyentesTotales = metrica; + break; + } + } + + if (issuesCerrados != null && issuesTotales != null && pullRequestsCerrados != null + && pullRequestsTotales != null && contribuyentesActivos != null && contribuyentesTotales != null) { + + // Verificamos que los valores totales no sean cero para evitar divisiones por + // cero + if (issuesTotales.getValue() == 0 || pullRequestsTotales.getValue() == 0 + || contribuyentesTotales.getValue() == 0) { + throw new NotAvailableMetricException("Los valores totales no pueden ser cero."); + } + + // Calculamos el indicador + double issuesRatio = (issuesCerrados.getValue() / issuesTotales.getValue()) * 40; + double pullRequestsRatio = (pullRequestsCerrados.getValue() / pullRequestsTotales.getValue()) * 40; + double contribuyentesRatio = (contribuyentesActivos.getValue() / contribuyentesTotales.getValue()) * 20; + + double indicador = issuesRatio + pullRequestsRatio + contribuyentesRatio; + + // Criterios de calidad + if (indicador >= 90) { + estado = IndicatorState.OK; + } else if (indicador >= 80) { + estado = IndicatorState.WARNING; + } else if (indicador >= 70) { + estado = IndicatorState.WARNING; + } else if (indicador >= 50) { + estado = IndicatorState.CRITICAL; + } else if (indicador >= 26) { + estado = IndicatorState.CRITICAL; + } else { + estado = IndicatorState.UNDEFINED; + } + + try { + indicatorReport = new ReportItem.ReportItemBuilder("repoActivity", indicador) + .metrics(Arrays.asList(issuesCerrados, issuesTotales, pullRequestsCerrados, pullRequestsTotales, + contribuyentesActivos, contribuyentesTotales)) + .indicator(estado).build(); + } catch (ReportItemException e) { + log.info("Error en ReportItemBuilder: " + e); + } + + } else { + log.info("Falta alguna de las métricas"); + throw new NotAvailableMetricException(REQUIRED_METRICS.toString()); + } + + return indicatorReport; + } + + @Override + public List requiredMetrics() { + return REQUIRED_METRICS; + } +} diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/RepositoryCalculator.java b/src/main/java/us/muit/fs/a4i/control/strategies/RepositoryCalculator.java index 204025cc..1e4e93a3 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/RepositoryCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/RepositoryCalculator.java @@ -24,7 +24,9 @@ *

* Puede hacerse uno a uno o todos a la vez *

- * RECUERDA: los indicadores tienen que estar incluidos en el fichero de configuración a4iDefault.json + * RECUERDA: los indicadores tienen que estar incluidos en el fichero de + * configuración a4iDefault.json + * * @author Isabel Román * */ @@ -55,7 +57,7 @@ public void calcIndicator(String indicatorName, ReportManagerI reportManager) th log.info("No se han proporcionado las m�tricas necesarias"); e.printStackTrace(); } - } else { + } else { log.info("No se han proporcionado las metricas necesarias"); } } @@ -87,6 +89,4 @@ public void setIndicator(String indicatorName, IndicatorStrategy strategy) { } - - } \ No newline at end of file diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java index f89ec055..e45aeada 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java @@ -22,18 +22,17 @@ import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; /** - * Deuda técnica - * Esta clase debe consultar datos sobre un desarrollador concreto de github - * Ahora mismo está en estado lamentable - * Simplemente busca los eventos de un desarrollador - * No localiza eventos de tipo ISSUE, que son los que se quería - * RECUERDA: las métricas tienen que estar incluidas en el fichero de configuración a4iDefault.json + * Deuda técnica Esta clase debe consultar datos sobre un desarrollador concreto + * de github Ahora mismo está en estado lamentable Simplemente busca los eventos + * de un desarrollador No localiza eventos de tipo ISSUE, que son los que se + * quería RECUERDA: las métricas tienen que estar incluidas en el fichero de + * configuración a4iDefault.json */ public class GitHubDeveloperEnquirer extends GitHubEnquirer { public GitHubDeveloperEnquirer() { super(); - metricNames.add("closedIssuesLastMonth"); - metricNames.add("assignedIssuesLastMonth"); + metricNames.add("closedIssuesLastMonth"); + metricNames.add("assignedIssuesLastMonth"); log.info("Incluidos nombres metricas en Enquirer"); } @@ -45,6 +44,7 @@ public GitHubDeveloperEnquirer() { *

*/ private String entityId; + @Override public ReportI buildReport(String developerId) { // TODO Auto-generated method stub @@ -58,21 +58,21 @@ public ReportItem getMetric(String metricName, String developerId) throws Metric GitHub gb = getConnection(); try { developer = gb.getUser(developerId); - log.info("Localizado el desarrollador "+developer.getName()); + log.info("Localizado el desarrollador " + developer.getName()); } catch (Exception e) { e.printStackTrace(); - throw new MetricException( - "No se puede acceder al desarrollador " + developerId + " para recuperarlo"); + throw new MetricException("No se puede acceder al desarrollador " + developerId + " para recuperarlo"); } - + return getMetric(metricName, developer); } - + private ReportItem getMetric(String metricName, GHUser developer) throws MetricException { - log.info("Localizando la metrica "+metricName); + log.info("Localizando la metrica " + metricName); ReportItem metric; if (developer == null) { - throw new MetricException("Intenta obtener una métrica de desarrollador sin haber obtenido el desarrollador"); + throw new MetricException( + "Intenta obtener una métrica de desarrollador sin haber obtenido el desarrollador"); } switch (metricName) { case "closedIssuesLastMonth": @@ -91,21 +91,21 @@ private ReportItem getMetric(String metricName, GHUser developer) throws MetricE private ReportItem getClosedIssuesLastMonth(GHUser developer) { log.info("Consultando los issues asignados a un desarrollador"); ReportItemBuilder builder = null; - int issues=0; - + int issues = 0; + try { - PagedIterable events=developer.listEvents(); - for(GHEventInfo event:events) { - log.info("Evento tipo"+event.getType()+" en la fecha "+event.getCreatedAt()); - if(event.getType()==GHEvent.ISSUES) { + PagedIterable events = developer.listEvents(); + for (GHEventInfo event : events) { + log.info("Evento tipo" + event.getType() + " en la fecha " + event.getCreatedAt()); + if (event.getType() == GHEvent.ISSUES) { - GHEventPayload.Issue payload=event.getPayload(GHEventPayload.Issue.class); + GHEventPayload.Issue payload = event.getPayload(GHEventPayload.Issue.class); log.info(payload.getAction()); issues++; - - } - + } + + } builder = new ReportItem.ReportItemBuilder("closedIssuesLastMonth", issues); builder.source("GitHub"); } catch (Exception e) { @@ -119,6 +119,5 @@ private ReportItem getAssignedIssuesLastMonth(GHUser developer) { // TODO Auto-generated method stub return null; } - } diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java index fccae6da..cd594b94 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java @@ -9,7 +9,6 @@ import java.util.logging.Logger; - import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GitHub; @@ -25,15 +24,15 @@ import us.muit.fs.a4i.model.entities.ReportItem; import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; - /** *

* Esta clase permite consultar métricas sobre una organización GitHub *

*

- * Deuda técnica: sería necesario verificar mejor el funcionamiento de las consultas de proyectos cerrados y abiertos, no parece hacer lo esperado - * Habría que incluir más métricas y algún indicador - * RECUERDA: las métricas tienen que estar incluidas en el fichero de configuración a4iDefault.json + * Deuda técnica: sería necesario verificar mejor el funcionamiento de las + * consultas de proyectos cerrados y abiertos, no parece hacer lo esperado + * Habría que incluir más métricas y algún indicador RECUERDA: las métricas + * tienen que estar incluidas en el fichero de configuración a4iDefault.json *

* * @author Isabel Román @@ -49,8 +48,7 @@ public class GitHubOrganizationEnquirer extends GitHubEnquirer { *

*/ private String entityId; - - + public GitHubOrganizationEnquirer() { super(); metricNames.add("repositoriesWithOpenPullRequest"); @@ -60,14 +58,14 @@ public GitHubOrganizationEnquirer() { metricNames.add("teams"); metricNames.add("openProjects"); metricNames.add("closedProjects"); - metricNames.add("followers"); + metricNames.add("followers"); log.info("Incluidos nombres metricas en Enquirer"); } - + @Override public ReportI buildReport(String organizationId) { ReportI report = null; - log.info("Invocado el metodo que construye un informe de organización, para la organizacion "+organizationId); + log.info("Invocado el metodo que construye un informe de organización, para la organizacion " + organizationId); /** *

* Información sobre la organizacion de GitHub @@ -89,10 +87,10 @@ public ReportI buildReport(String organizationId) { log.info("Nombre organizacion = " + organizationId); GitHub gb = getConnection(); - organization=gb.getOrganization(organizationId); - + organization = gb.getOrganization(organizationId); + log.info("La organizacion es de la empresa " + organization.getCompany() + " fue creada en " - + organization.getCreatedAt()+ " se puede contactar en "+organization.getEmail()); + + organization.getCreatedAt() + " se puede contactar en " + organization.getEmail()); log.info("leidos datos de la " + organization); report = new Report(organizationId); @@ -100,9 +98,6 @@ public ReportI buildReport(String organizationId) { * Métricas directas de tipo conteo */ - - - report.addMetric(getMembers(organization)); log.info("Incluida metrica members "); @@ -112,36 +107,35 @@ public ReportI buildReport(String organizationId) { report.addMetric(getFollowers(organization)); log.info("Incluida metrica followers "); - report.addMetric(getPullRequests(organization)); log.info("Incluida metrica pullRequests "); report.addMetric(getRepositories(organization)); log.info("Incluida metrica repositories "); - + report.addMetric(getRepositoriesWithOpenPullRequest(organization)); log.info("Incluida metrica repositoriesWithPullRequest "); - - + report.addMetric(getOpenProjects(organization)); log.info("Incluida metrica openProjects "); - - report.addMetric(getClosedProjects(organization)); + + report.addMetric(getClosedProjects(organization)); log.info("Incluida metrica closedProjects "); - } catch (Exception e) { log.severe("Problemas en la conexión " + e); } return report; } -/** - * Permite consultar desde fuera una única métrica de la organización con el id que se pase como parámetro - */ + + /** + * Permite consultar desde fuera una única métrica de la organización con el id + * que se pase como parámetro + */ @Override public ReportItem getMetric(String metricName, String organizationId) throws MetricException { - log.info("Invocado getMetric para buscar "+metricName); + log.info("Invocado getMetric para buscar " + metricName); GHOrganization organization; GitHub gb = getConnection(); @@ -155,58 +149,59 @@ public ReportItem getMetric(String metricName, String organizationId) t return getMetric(metricName, organization); } - + /** *

* Crea la métrica solicitada consultando la organizacion que se pasa como * parámetro *

* - * @param metricName Métrica solicitada + * @param metricName Métrica solicitada * @param organization Organizacion * @return La métrica creada * @throws MetricException Si la métrica no está definida se lanzará una * excepción */ private ReportItem getMetric(String metricName, GHOrganization organization) throws MetricException { - ReportItem metric=null; + ReportItem metric = null; if (organization == null) { throw new MetricException("Intenta obtener una métrica sin haber obtenido los datos de la organizacion"); } switch (metricName) { case "repositoriesWithOpenPullRequest": - metric=getRepositoriesWithOpenPullRequest(organization); + metric = getRepositoriesWithOpenPullRequest(organization); break; case "repositories": - metric=getRepositories(organization); + metric = getRepositories(organization); break; case "pullRequests": - metric=getPullRequests(organization); + metric = getPullRequests(organization); break; case "members": - metric=getMembers(organization); + metric = getMembers(organization); break; case "teams": - metric=getTeams(organization); + metric = getTeams(organization); break; case "openProjects": - metric=getOpenProjects(organization); + metric = getOpenProjects(organization); break; case "closedProjects": - metric=getClosedProjects(organization); + metric = getClosedProjects(organization); break; case "followers": - metric=getFollowers(organization); - break; + metric = getFollowers(organization); + break; default: throw new MetricException("La métrica " + metricName + " no está definida para un repositorio"); } return metric; } + private ReportItem getRepositoriesWithOpenPullRequest(GHOrganization organization) { log.info("Consultando los repositorios con pull requests abiertos"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { builder = new ReportItem.ReportItemBuilder("repositoriesWithOpenPullRequest", organization.getRepositoriesWithOpenPullRequests().size()); @@ -214,148 +209,139 @@ private ReportItem getRepositoriesWithOpenPullRequest(GHOrganization organizatio } catch (ReportItemException | IOException e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } - + private ReportItem getRepositories(GHOrganization organization) { log.info("Consultando los repositorios"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - builder = new ReportItem.ReportItemBuilder("repositories", - organization.getPublicRepoCount()); + builder = new ReportItem.ReportItemBuilder("repositories", organization.getPublicRepoCount()); builder.source("GitHub"); } catch (ReportItemException | IOException e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } - + private ReportItem getMembers(GHOrganization organization) { log.info("Consultando los miembros"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - builder = new ReportItem.ReportItemBuilder("members", - organization.listMembers().toList().size()); + builder = new ReportItem.ReportItemBuilder("members", organization.listMembers().toList().size()); builder.source("GitHub"); } catch (ReportItemException | IOException e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } - + private ReportItem getTeams(GHOrganization organization) { log.info("Consultando los equipos"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - int size=organization.getTeams().size(); - log.info("Numero de equipos"+size); - builder = new ReportItem.ReportItemBuilder("teams", - organization.getTeams().size()); + int size = organization.getTeams().size(); + log.info("Numero de equipos" + size); + builder = new ReportItem.ReportItemBuilder("teams", organization.getTeams().size()); builder.source("GitHub"); } catch (ReportItemException | IOException e) { log.fine("unable to retry teams"); e.printStackTrace(); - } + } return builder.build(); } - + private ReportItem getFollowers(GHOrganization organization) { log.info("Consultando los seguidores"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - builder = new ReportItem.ReportItemBuilder("followers", - organization.getFollowersCount()); + builder = new ReportItem.ReportItemBuilder("followers", organization.getFollowersCount()); builder.source("GitHub"); } catch (ReportItemException | IOException e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } - + private ReportItem getPullRequests(GHOrganization organization) { log.info("Consultando los pull requests"); - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - builder = new ReportItem.ReportItemBuilder("pullRequests", - organization.getPullRequests().size()); + builder = new ReportItem.ReportItemBuilder("pullRequests", organization.getPullRequests().size()); builder.source("GitHub"); } catch (ReportItemException | IOException e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } - + private ReportItem getOpenProjects(GHOrganization organization) { - - ReportItemBuilder builder=null; + + ReportItemBuilder builder = null; try { - log.info("Consultando los proyectos abiertos en "+organization.getUrl()); - int number=0; - //first we look for projects associated with the organization - PagedIterable pagina=organization.listProjects(GHProject.ProjectStateFilter.OPEN); - List proyectos=pagina.toList(); - number=number+proyectos.size(); - //second we look for projects associated with the repos - PagedIterable repositories=organization.listRepositories(); - - for(GHRepository repo:repositories) { - PagedIterable repoproyects=repo.listProjects(GHProject.ProjectStateFilter.OPEN); - number=number+repoproyects.toList().size(); + log.info("Consultando los proyectos abiertos en " + organization.getUrl()); + int number = 0; + // first we look for projects associated with the organization + PagedIterable pagina = organization.listProjects(GHProject.ProjectStateFilter.OPEN); + List proyectos = pagina.toList(); + number = number + proyectos.size(); + // second we look for projects associated with the repos + PagedIterable repositories = organization.listRepositories(); + + for (GHRepository repo : repositories) { + PagedIterable repoproyects = repo.listProjects(GHProject.ProjectStateFilter.OPEN); + number = number + repoproyects.toList().size(); } - - - log.info("Open projects "+number); - builder = new ReportItem.ReportItemBuilder("openProjects", - number); - - for(GHProject pro:proyectos) { - log.info("Proyecto "+pro.getName()+" en estado "+pro.getState()); + + log.info("Open projects " + number); + builder = new ReportItem.ReportItemBuilder("openProjects", number); + + for (GHProject pro : proyectos) { + log.info("Proyecto " + pro.getName() + " en estado " + pro.getState()); } builder.source("GitHub"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); } - + private ReportItem getClosedProjects(GHOrganization organization) { - ReportItemBuilder builder=null; + ReportItemBuilder builder = null; try { - log.info("Consultando los proyectos cerrados en "+organization.getUrl()); - int number=0; - //first we look for projects associated with the organization - PagedIterable pagina=organization.listProjects(GHProject.ProjectStateFilter.CLOSED); - List proyectos=pagina.toList(); - number=number+proyectos.size(); - //second we look for projects associated with the repos - PagedIterable repositories=organization.listRepositories(); - - for(GHRepository repo:repositories) { - PagedIterable repoproyects=repo.listProjects(GHProject.ProjectStateFilter.CLOSED); - number=number+repoproyects.toList().size(); + log.info("Consultando los proyectos cerrados en " + organization.getUrl()); + int number = 0; + // first we look for projects associated with the organization + PagedIterable pagina = organization.listProjects(GHProject.ProjectStateFilter.CLOSED); + List proyectos = pagina.toList(); + number = number + proyectos.size(); + // second we look for projects associated with the repos + PagedIterable repositories = organization.listRepositories(); + + for (GHRepository repo : repositories) { + PagedIterable repoproyects = repo.listProjects(GHProject.ProjectStateFilter.CLOSED); + number = number + repoproyects.toList().size(); } - - - log.info("Closed projects "+number); - builder = new ReportItem.ReportItemBuilder("closedProjects", - number); - - for(GHProject pro:proyectos) { - log.info("Proyecto "+pro.getName()+" en estado "+pro.getState()); + + log.info("Closed projects " + number); + builder = new ReportItem.ReportItemBuilder("closedProjects", number); + + for (GHProject pro : proyectos) { + log.info("Proyecto " + pro.getName() + " en estado " + pro.getState()); } builder.source("GitHub"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); - } + } return builder.build(); - + } - + } diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index 76b83173..6b4b66a8 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -33,10 +33,11 @@ /** * @author Isabel Román Deuda técnica: debería seguir la misma filosofía que - * GitHubOrganizationEnquirer para evitar la replicación de código - * deuda técnica: - * las métricas tras la etiqueta //equipo 3 tienen problemas, no están acordes al indicador para el que fueron creadas - * RECUERDA: las métricas tienen que estar incluidas en el fichero de configuración a4iDefault.json + * GitHubOrganizationEnquirer para evitar la replicación de código deuda + * técnica: las métricas tras la etiqueta //equipo 3 tienen problemas, + * no están acordes al indicador para el que fueron creadas RECUERDA: + * las métricas tienen que estar incluidas en el fichero de + * configuración a4iDefault.json */ public class GitHubRepositoryEnquirer extends GitHubEnquirer { /** @@ -71,10 +72,10 @@ public GitHubRepositoryEnquirer() { metricNames.add("closedIssuesLastMonth"); metricNames.add("meanClosedIssuesLastMonth"); metricNames.add("issues4DevLastMonth"); - //equipo 4 + // equipo 4 metricNames.add("totalPullReq"); metricNames.add("closedPullReq"); - //equipo 5 + // equipo 5 metricNames.add("PRAcceptedLastYear"); metricNames.add("PRAcceptedLastMonth"); metricNames.add("PRRejectedLastYear"); @@ -272,13 +273,13 @@ private ReportItem getMetric(String metricName, GHRepository remoteRepo) throws case "issues4DevLastMonth": metric = issues4DevLastMonth(remoteRepo); break; - //equipo 4 + // equipo 4 case "totalPullReq": metric = getTotalPullReq(remoteRepo); break; case "closedPullReq": metric = getClosedPullReq(remoteRepo); - break; + break; case "PRAcceptedLastYear": metric = getPRAcceptedLastYear(remoteRepo); break; @@ -627,12 +628,12 @@ private ReportItem getLastUpdated(GHRepository repo) { } // Métodos añadidos por el equipo 3 - /** - * - * @param remoteRepo - * @return ReportItem with the number of issues created last month - * @throws MetricException - */ + /** + * + * @param remoteRepo + * @return ReportItem with the number of issues created last month + * @throws MetricException + */ private ReportItem getIssuesLastMonth(GHRepository remoteRepo) throws MetricException { log.info("Consultando los issues abiertos un mes"); int issuesLastMonth = 0; @@ -650,8 +651,8 @@ private ReportItem getIssuesLastMonth(GHRepository remoteRepo) throws MetricExce for (GHIssue issue : issues) { if (issue.getCreatedAt().after(lastMonth)) { issuesLastMonth++; - log.finer("issue manejado por "+issue.getUser().getName()); - + log.finer("issue manejado por " + issue.getUser().getName()); + } } @@ -672,7 +673,7 @@ private ReportItem getIssuesLastMonth(GHRepository remoteRepo) throws MetricExce return builder.build(); } - + /** * * @param remoteRepo @@ -715,14 +716,14 @@ private ReportItem getClosedIssuesLastMonth(GHRepository remoteRepo) throws Metr return builder.build(); } - + /** * * @param remoteRepo * @return ReportItem with the average of closed issues per member in a month * @throws MetricException */ - private ReportItem getMeanClosedIssuesLastMonth(GHRepository remoteRepo) throws MetricException { + private ReportItem getMeanClosedIssuesLastMonth(GHRepository remoteRepo) throws MetricException { int closedIssuesLastMonth = 0; int activeMembers = 0; Map issuesClosedByMember = new HashMap<>(); @@ -778,12 +779,15 @@ private ReportItem getMeanClosedIssuesLastMonth(GHRepository remoteRepo) throws /** * * @param remoteRepo - * @return ReportItem with a map indicating the issues assigned to each member in a month + * @return ReportItem with a map indicating the issues assigned to each member + * in a month * @throws MetricException * - * Deuda técnica: esto no está bien porque aquí este reportitem es un mapa y luego lo tratan como si fuera un double... no es coherente una parte con la otra. + * Deuda técnica: esto no está bien porque aquí este + * reportitem es un mapa y luego lo tratan como si fuera + * un double... no es coherente una parte con la otra. */ - private ReportItem issues4DevLastMonth(GHRepository remoteRepo) throws MetricException { + private ReportItem issues4DevLastMonth(GHRepository remoteRepo) throws MetricException { Map issuesAssignedByMember = new HashMap<>(); ReportItemBuilder> builder = null; @@ -824,183 +828,204 @@ private ReportItem issues4DevLastMonth(GHRepository remoteRepo) throws MetricExc return builder.build(); } - private ReportItem getClosedPullReq(GHRepository repo){ + + private ReportItem getClosedPullReq(GHRepository repo) { log.info("Consultando los pull requests completados"); - ReportItemBuilder builder = null; - - int completedPullRequests = 0; - - try { - - for (GHPullRequest pullRequest : repo.getPullRequests(GHIssueState.CLOSED)) { - - completedPullRequests++; - - } - builder = new ReportItem.ReportItemBuilder("closedPullReq", completedPullRequests); - builder.description("Número de pull requests completados").source("GitHub"); - } catch (Exception e) { - e.printStackTrace(); - } - return builder.build(); + ReportItemBuilder builder = null; + + int completedPullRequests = 0; + + try { + + for (GHPullRequest pullRequest : repo.getPullRequests(GHIssueState.CLOSED)) { + + completedPullRequests++; + + } + builder = new ReportItem.ReportItemBuilder("closedPullReq", completedPullRequests); + builder.description("Número de pull requests completados").source("GitHub"); + } catch (Exception e) { + e.printStackTrace(); + } + return builder.build(); } - - private ReportItem getTotalPullReq(GHRepository repo){ + + private ReportItem getTotalPullReq(GHRepository repo) { log.info("Consultando los pull requests totales"); - ReportItemBuilder builder = null; - - int totalPullRequests = 0; - - try { - - for (GHPullRequest pullRequest : repo.getPullRequests(GHIssueState.ALL)) { - - totalPullRequests++; - - } - builder = new ReportItem.ReportItemBuilder("totalPullReq", totalPullRequests); - builder.description("Número de pull requests totales").source("GitHub"); - } catch (Exception e) { - e.printStackTrace(); - } - return builder.build(); + ReportItemBuilder builder = null; + + int totalPullRequests = 0; + + try { + + for (GHPullRequest pullRequest : repo.getPullRequests(GHIssueState.ALL)) { + + totalPullRequests++; + + } + builder = new ReportItem.ReportItemBuilder("totalPullReq", totalPullRequests); + builder.description("Número de pull requests totales").source("GitHub"); + } catch (Exception e) { + e.printStackTrace(); + } + return builder.build(); + } + + // Equipo 5 + + /** + *

+ * Filtra las solicitudes de extracción (pull requests) según la fecha de + * creación y el estado de aceptación. + *

+ * + * @param pullRequests la lista de solicitudes de extracción a filtrar + * @param startDate la fecha de inicio del intervalo de tiempo para filtrar + * @param endDate la fecha de finalización del intervalo de tiempo para + * filtrar + * @param accepted indica si se deben filtrar las solicitudes de extracción + * aceptadas (true) o rechazadas (false) + * @return la lista de solicitudes de extracción que cumplen con los criterios + * de filtrado + */ + private static List filterPullRequests(List pullRequests, LocalDateTime startDate, + LocalDateTime endDate, boolean accepted) { + return pullRequests.stream().filter(pr -> { + try { + LocalDateTime createdDate = pr.getCreatedAt().toInstant().atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + return createdDate.isAfter(startDate) && createdDate.isBefore(endDate) + && (accepted ? pr.isMerged() : !pr.isMerged()); + } catch (IOException e) { + log.warning("Failed to get creation date for PR #" + pr.getNumber() + "\n" + e); + return false; + } + }).collect(Collectors.toList()); + } + + /** + *

+ * Obtención del número de solicitudes de extracción aceptadas en el último año. + *

+ * + * @param remoteRepo el repositorio remoto sobre el que consultar + * @return la métrica con el número de solicitudes de extracción aceptadas en el + * último año + * @throws MetricException si ocurre un error al obtener las solicitudes de + * extracción + */ + private ReportItem getPRAcceptedLastYear(GHRepository remoteRepo) throws MetricException { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime oneYearAgo = now.minusYears(1); + + try { + List pullRequests = remoteRepo.getPullRequests(GHIssueState.CLOSED); + List acceptedLastYear = filterPullRequests(pullRequests, oneYearAgo, now, true); + + ReportItemBuilder acceptedLastYearMetric = new ReportItem.ReportItemBuilder<>("PRAcceptedLastYear", + acceptedLastYear.size()); + acceptedLastYearMetric.source("GitHub, calculada") + .description("Número de solicitudes de extracción aceptadas en el último año"); + + return acceptedLastYearMetric.build(); + } catch (IOException | ReportItemException e) { + throw new MetricException( + "Error al obtener las solicitudes de extracción aceptadas en el último año\n" + e); + } + } + + /** + *

+ * Obtención del número de solicitudes de extracción aceptadas en el último mes. + *

+ * + * @param remoteRepo el repositorio remoto sobre el que consultar + * @return la métrica con el número de solicitudes de extracción aceptadas en el + * último mes + * @throws MetricException si ocurre un error al obtener las solicitudes de + * extracción + */ + private ReportItem getPRAcceptedLastMonth(GHRepository remoteRepo) throws MetricException { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime oneMonthAgo = now.minusMonths(1); + + try { + List pullRequests = remoteRepo.getPullRequests(GHIssueState.CLOSED); + List acceptedLastMonth = filterPullRequests(pullRequests, oneMonthAgo, now, true); + + ReportItemBuilder acceptedLastMonthMetric = new ReportItem.ReportItemBuilder<>( + "PRAcceptedLastMonth", acceptedLastMonth.size()); + acceptedLastMonthMetric.source("GitHub, calculada") + .description("Número de solicitudes de extracción aceptadas en el último mes"); + + return acceptedLastMonthMetric.build(); + } catch (IOException | ReportItemException e) { + throw new MetricException( + "Error al obtener las solicitudes de extracción aceptadas en el último mes\n" + e); + } + } + + /** + *

+ * Obtención del número de solicitudes de extracción rechazadas en el último + * año. + *

+ * + * @param remoteRepo el repositorio remoto sobre el que consultar + * @return la métrica con el número de solicitudes de extracción rechazadas en + * el último año + * @throws MetricException si ocurre un error al obtener las solicitudes de + * extracción + */ + private ReportItem getPRRejectedLastYear(GHRepository remoteRepo) throws MetricException { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime oneYearAgo = now.minusYears(1); + + try { + List pullRequests = remoteRepo.getPullRequests(GHIssueState.CLOSED); + List rejectedLastYear = filterPullRequests(pullRequests, oneYearAgo, now, false); + + ReportItemBuilder rejectedLastYearMetric = new ReportItem.ReportItemBuilder<>("PRRejectedLastYear", + rejectedLastYear.size()); + rejectedLastYearMetric.source("GitHub, calculada") + .description("Número de solicitudes de extracción rechazadas en el último año"); + + return rejectedLastYearMetric.build(); + } catch (IOException | ReportItemException e) { + throw new MetricException( + "Error al obtener las solicitudes de extracción rechazadas en el último año\n" + e); + } + } + + /** + *

+ * Obtención del número de solicitudes de extracción rechazadas en el último + * mes. + *

+ * + * @param remoteRepo el repositorio remoto sobre el que consultar + * @return la métrica con el número de solicitudes de extracción rechazadas en + * el último mes + * @throws MetricException si ocurre un error al obtener las solicitudes de + * extracción + */ + private ReportItem getPRRejectedLastMonth(GHRepository remoteRepo) throws MetricException { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime oneMonthAgo = now.minusMonths(1); + + try { + List pullRequests = remoteRepo.getPullRequests(GHIssueState.CLOSED); + List rejectedLastMonth = filterPullRequests(pullRequests, oneMonthAgo, now, false); + + ReportItemBuilder rejectedLastMonthMetric = new ReportItem.ReportItemBuilder<>( + "PRRejectedLastMonth", rejectedLastMonth.size()); + rejectedLastMonthMetric.source("GitHub, calculada") + .description("Número de solicitudes de extracción rechazadas en el último mes"); + + return rejectedLastMonthMetric.build(); + } catch (IOException | ReportItemException e) { + throw new MetricException( + "Error al obtener las solicitudes de extracción rechazadas en el último mes\n" + e); + } } - - //Equipo 5 - - /** - *

- * Filtra las solicitudes de extracción (pull requests) según la fecha de creación y el estado de aceptación. - *

- * - * @param pullRequests la lista de solicitudes de extracción a filtrar - * @param startDate la fecha de inicio del intervalo de tiempo para filtrar - * @param endDate la fecha de finalización del intervalo de tiempo para filtrar - * @param accepted indica si se deben filtrar las solicitudes de extracción aceptadas (true) o rechazadas (false) - * @return la lista de solicitudes de extracción que cumplen con los criterios de filtrado - */ - private static List filterPullRequests(List pullRequests, LocalDateTime startDate, LocalDateTime endDate, boolean accepted) { - return pullRequests.stream() - .filter(pr -> { - try { - LocalDateTime createdDate = pr.getCreatedAt().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); - return createdDate.isAfter(startDate) && createdDate.isBefore(endDate) && (accepted ? pr.isMerged() : !pr.isMerged()); - } catch (IOException e) { - log.warning("Failed to get creation date for PR #" + pr.getNumber() + "\n"+e); - return false; - } - }) - .collect(Collectors.toList()); - } - - - /** - *

- * Obtención del número de solicitudes de extracción aceptadas en el último año. - *

- * - * @param remoteRepo el repositorio remoto sobre el que consultar - * @return la métrica con el número de solicitudes de extracción aceptadas en el último año - * @throws MetricException si ocurre un error al obtener las solicitudes de extracción - */ - private ReportItem getPRAcceptedLastYear(GHRepository remoteRepo) throws MetricException { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime oneYearAgo = now.minusYears(1); - - try { - List pullRequests = remoteRepo.getPullRequests(GHIssueState.CLOSED); - List acceptedLastYear = filterPullRequests(pullRequests, oneYearAgo, now, true); - - ReportItemBuilder acceptedLastYearMetric = new ReportItem.ReportItemBuilder<>("PRAcceptedLastYear", acceptedLastYear.size()); - acceptedLastYearMetric.source("GitHub, calculada") - .description("Número de solicitudes de extracción aceptadas en el último año"); - - return acceptedLastYearMetric.build(); - } catch (IOException | ReportItemException e) { - throw new MetricException("Error al obtener las solicitudes de extracción aceptadas en el último año\n" + e); - } - } - - /** - *

- * Obtención del número de solicitudes de extracción aceptadas en el último mes. - *

- * - * @param remoteRepo el repositorio remoto sobre el que consultar - * @return la métrica con el número de solicitudes de extracción aceptadas en el último mes - * @throws MetricException si ocurre un error al obtener las solicitudes de extracción - */ - private ReportItem getPRAcceptedLastMonth(GHRepository remoteRepo) throws MetricException { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime oneMonthAgo = now.minusMonths(1); - - try { - List pullRequests = remoteRepo.getPullRequests(GHIssueState.CLOSED); - List acceptedLastMonth = filterPullRequests(pullRequests, oneMonthAgo, now, true); - - ReportItemBuilder acceptedLastMonthMetric = new ReportItem.ReportItemBuilder<>("PRAcceptedLastMonth", acceptedLastMonth.size()); - acceptedLastMonthMetric.source("GitHub, calculada") - .description("Número de solicitudes de extracción aceptadas en el último mes"); - - return acceptedLastMonthMetric.build(); - } catch (IOException | ReportItemException e) { - throw new MetricException("Error al obtener las solicitudes de extracción aceptadas en el último mes\n" + e); - } - } - - - /** - *

- * Obtención del número de solicitudes de extracción rechazadas en el último año. - *

- * - * @param remoteRepo el repositorio remoto sobre el que consultar - * @return la métrica con el número de solicitudes de extracción rechazadas en el último año - * @throws MetricException si ocurre un error al obtener las solicitudes de extracción - */ - private ReportItem getPRRejectedLastYear(GHRepository remoteRepo) throws MetricException { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime oneYearAgo = now.minusYears(1); - - try { - List pullRequests = remoteRepo.getPullRequests(GHIssueState.CLOSED); - List rejectedLastYear = filterPullRequests(pullRequests, oneYearAgo, now, false); - - ReportItemBuilder rejectedLastYearMetric = new ReportItem.ReportItemBuilder<>("PRRejectedLastYear", rejectedLastYear.size()); - rejectedLastYearMetric.source("GitHub, calculada") - .description("Número de solicitudes de extracción rechazadas en el último año"); - - return rejectedLastYearMetric.build(); - } catch (IOException | ReportItemException e) { - throw new MetricException("Error al obtener las solicitudes de extracción rechazadas en el último año\n" + e); - } - } - - - /** - *

- * Obtención del número de solicitudes de extracción rechazadas en el último mes. - *

- * - * @param remoteRepo el repositorio remoto sobre el que consultar - * @return la métrica con el número de solicitudes de extracción rechazadas en el último mes - * @throws MetricException si ocurre un error al obtener las solicitudes de extracción - */ - private ReportItem getPRRejectedLastMonth(GHRepository remoteRepo) throws MetricException { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime oneMonthAgo = now.minusMonths(1); - - try { - List pullRequests = remoteRepo.getPullRequests(GHIssueState.CLOSED); - List rejectedLastMonth = filterPullRequests(pullRequests, oneMonthAgo, now, false); - - ReportItemBuilder rejectedLastMonthMetric = new ReportItem.ReportItemBuilder<>("PRRejectedLastMonth", rejectedLastMonth.size()); - rejectedLastMonthMetric.source("GitHub, calculada") - .description("Número de solicitudes de extracción rechazadas en el último mes"); - - return rejectedLastMonthMetric.build(); - } catch (IOException | ReportItemException e) { - throw new MetricException("Error al obtener las solicitudes de extracción rechazadas en el último mes\n" + e); - } - } } diff --git a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java index 27b62b14..e250cdc6 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ExcelReportManager.java @@ -50,7 +50,8 @@ * anterior, si no existía se crea nueva *

*

- * Deuda técnica. En la persistencia de métricas e indicadores se observa mucho código replicado, se debe optimizar + * Deuda técnica. En la persistencia de métricas e indicadores se observa mucho + * código replicado, se debe optimizar *

* * @author Isabel Román @@ -255,12 +256,12 @@ private void persistMetric(ReportItemI metric) { XSSFCell cell; - cell=row.createCell(cellIndex); + cell = row.createCell(cellIndex); cell.setCellValue(metric.getName()); cell.setCellStyle(style); sheet.autoSizeColumn(cellIndex++); - cell=row.createCell(cellIndex); + cell = row.createCell(cellIndex); cell.setCellValue(metric.getValue().toString()); cell.setCellStyle(style); sheet.autoSizeColumn(cellIndex++); @@ -327,12 +328,12 @@ private void persistIndicator(ReportItemI indicator) { } XSSFCell cell; - cell=row.createCell(cellIndex); + cell = row.createCell(cellIndex); cell.setCellValue(indicator.getName()); cell.setCellStyle(style); sheet.autoSizeColumn(cellIndex++); - cell=row.createCell(cellIndex); + cell = row.createCell(cellIndex); cell.setCellValue(indicator.getValue().toString()); cell.setCellStyle(style); sheet.autoSizeColumn(cellIndex++); @@ -365,7 +366,7 @@ private void persistIndicator(ReportItemI indicator) { @Override public void deleteReport(ReportI report) throws ReportNotDefinedException { - + log.info("Eliminando informe excel"); if (report == null) { throw new ReportNotDefinedException(); @@ -383,14 +384,14 @@ public void deleteReport(ReportI report) throws ReportNotDefinedException { out = new FileOutputStream(filePath + fileName); wb.write(out); out.close(); - - }else { - log.info("No existe el informe "+report.getEntityId()); + + } else { + log.info("No existe el informe " + report.getEntityId()); } inputStream.close(); - }catch (Exception e) { - - e.printStackTrace(); - } + } catch (Exception e) { + + e.printStackTrace(); + } } } diff --git a/src/main/java/us/muit/fs/a4i/persistence/ReportFormaterI.java b/src/main/java/us/muit/fs/a4i/persistence/ReportFormaterI.java index 872ee40e..e6b8c26c 100644 --- a/src/main/java/us/muit/fs/a4i/persistence/ReportFormaterI.java +++ b/src/main/java/us/muit/fs/a4i/persistence/ReportFormaterI.java @@ -3,7 +3,6 @@ */ package us.muit.fs.a4i.persistence; - import java.io.IOException; import us.muit.fs.a4i.model.entities.Font; diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index 43b1b526..8ce78c2f 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -60,12 +60,7 @@ "description": "Eliminaciones desde que se inició el repositorio", "unit": "deletions" }, - { - "name": "pullRequests", - "type": "java.lang.Integer", - "description": "Número de pull requests", - "unit": "pull requests" - }, + { "name": "stars", "type": "java.lang.Integer", @@ -96,18 +91,6 @@ "description": "Numero de issues abiertas", "unit": "issues" }, - { - "name": "closedIssues", - "type": "java.lang.Integer", - "description": "Numero de issues cerrados", - "unit": "issues" - }, - { - "name": "issues", - "type": "java.lang.Integer", - "description": "Tareas totales", - "unit": "issues" - }, { "name": "openProjects", "type": "java.lang.Integer", @@ -137,6 +120,18 @@ "type": "java.lang.Integer", "description": "Número de repositorios con pull requests pendientes", "unit": "repositories" + }, + { + "name": "closedIssues", + "type": "java.lang.Integer", + "description": "Numero de issues cerrados", + "unit": "issues" + }, + { + "name": "issues", + "type": "java.lang.Integer", + "description": "Tareas totales", + "unit": "issues" }, { "name": "issuesLastMonth", @@ -162,6 +157,12 @@ "description": "Issues asignados a un desarrollador en el último mes", "unit": "issues/month" }, + { + "name": "pullRequests", + "type": "java.lang.Integer", + "description": "Número de pull requests", + "unit": "pull requests" + }, { "name": "totalPullReq", "type": "java.lang.Integer", diff --git a/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java index 1afdd2d7..07cf1ea8 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/IssuesRatioIndicatorTest.java @@ -1,4 +1,5 @@ package us.muit.fs.a4i.test.control; + /*** * @author celllarod, curso 22/23 * Pruebas añadidas por alumnos del curso 22/23 para probar la clase IssuesRatioIndicator @@ -28,7 +29,7 @@ public class IssuesRatioIndicatorTest { private static Logger log = Logger.getLogger(IssuesRatioIndicatorTest.class.getName()); - + /** * @throws java.lang.Exception */ @@ -56,70 +57,63 @@ void setUp() throws Exception { @AfterEach void tearDown() throws Exception { } - - - - - @Test - public void testCalcIndicator() throws NotAvailableMetricException { - // Creamos los mocks necesarios - ReportItemI mockOpenIssues = Mockito.mock(ReportItemI.class); - ReportItemI mockClosedIssues = Mockito.mock(ReportItemI.class); - - // Configuramos los mocks para devolver valores predefinidos - Mockito.when(mockOpenIssues.getName()).thenReturn("openIssues"); - Mockito.when(mockOpenIssues.getValue()).thenReturn(10.0); - - Mockito.when(mockClosedIssues.getName()).thenReturn("closedIssues"); - Mockito.when(mockClosedIssues.getValue()).thenReturn(5.0); - - // Creamos una instancia de IssuesRatioIndicator - IssuesRatioIndicatorStrategy indicator = new IssuesRatioIndicatorStrategy(); - - // Ejecutamos el método que queremos probar con los mocks como argumentos - List> metrics = Arrays.asList(mockOpenIssues, mockClosedIssues); - ReportItemI result = indicator.calcIndicator(metrics); - - // Comprobamos que el resultado es el esperado - Assertions.assertEquals("issuesRatio", result.getName()); - Assertions.assertEquals(2.0, result.getValue()); - Assertions.assertDoesNotThrow(()->indicator.calcIndicator(metrics)); - } - - @Test - public void testCalcIndicatorThrowsNotAvailableMetricException() { - // Creamos los mocks necesarios - ReportItemI mockOpenIssues = Mockito.mock(ReportItemI.class); - - // Configuramos los mocks para devolver valores predefinidos - Mockito.when(mockOpenIssues.getName()).thenReturn("openIssues"); - Mockito.when(mockOpenIssues.getValue()).thenReturn(10.0); - - // Creamos una instancia de IssuesRatioIndicator - IssuesRatioIndicatorStrategy indicator = new IssuesRatioIndicatorStrategy(); - - // Ejecutamos el método que queremos probar con una sola métrica - List> metrics = Arrays.asList(mockOpenIssues); - // Comprobamos que se lanza la excepción adecuada - NotAvailableMetricException exception = Assertions.assertThrows(NotAvailableMetricException.class, - () -> indicator.calcIndicator(metrics)); - - } - - - @Test - public void testRequiredMetrics() { - // Creamos una instancia de IssuesRatioIndicator - IssuesRatioIndicatorStrategy indicatorStrategy = new IssuesRatioIndicatorStrategy(); - - // Ejecutamos el método que queremos probar - List requiredMetrics = indicatorStrategy.requiredMetrics(); - - // Comprobamos que el resultado es el esperado - List expectedMetrics = Arrays.asList("openIssues", "closedIssues"); - Assertions.assertEquals(expectedMetrics, requiredMetrics); - } + + @Test + public void testCalcIndicator() throws NotAvailableMetricException { + // Creamos los mocks necesarios + ReportItemI mockOpenIssues = Mockito.mock(ReportItemI.class); + ReportItemI mockClosedIssues = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para devolver valores predefinidos + Mockito.when(mockOpenIssues.getName()).thenReturn("openIssues"); + Mockito.when(mockOpenIssues.getValue()).thenReturn(10.0); + + Mockito.when(mockClosedIssues.getName()).thenReturn("closedIssues"); + Mockito.when(mockClosedIssues.getValue()).thenReturn(5.0); + + // Creamos una instancia de IssuesRatioIndicator + IssuesRatioIndicatorStrategy indicator = new IssuesRatioIndicatorStrategy(); + + // Ejecutamos el método que queremos probar con los mocks como argumentos + List> metrics = Arrays.asList(mockOpenIssues, mockClosedIssues); + ReportItemI result = indicator.calcIndicator(metrics); + + // Comprobamos que el resultado es el esperado + Assertions.assertEquals("issuesRatio", result.getName()); + Assertions.assertEquals(2.0, result.getValue()); + Assertions.assertDoesNotThrow(() -> indicator.calcIndicator(metrics)); + } + + @Test + public void testCalcIndicatorThrowsNotAvailableMetricException() { + // Creamos los mocks necesarios + ReportItemI mockOpenIssues = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para devolver valores predefinidos + Mockito.when(mockOpenIssues.getName()).thenReturn("openIssues"); + Mockito.when(mockOpenIssues.getValue()).thenReturn(10.0); + + // Creamos una instancia de IssuesRatioIndicator + IssuesRatioIndicatorStrategy indicator = new IssuesRatioIndicatorStrategy(); + + // Ejecutamos el método que queremos probar con una sola métrica + List> metrics = Arrays.asList(mockOpenIssues); + // Comprobamos que se lanza la excepción adecuada + NotAvailableMetricException exception = Assertions.assertThrows(NotAvailableMetricException.class, + () -> indicator.calcIndicator(metrics)); + } - + @Test + public void testRequiredMetrics() { + // Creamos una instancia de IssuesRatioIndicator + IssuesRatioIndicatorStrategy indicatorStrategy = new IssuesRatioIndicatorStrategy(); + // Ejecutamos el método que queremos probar + List requiredMetrics = indicatorStrategy.requiredMetrics(); + + // Comprobamos que el resultado es el esperado + List expectedMetrics = Arrays.asList("openIssues", "closedIssues"); + Assertions.assertEquals(expectedMetrics, requiredMetrics); + } +} diff --git a/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java b/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java index 1f39aac4..11a43dd3 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java +++ b/src/test/java/us/muit/fs/a4i/test/control/SupervisorControl.java @@ -18,9 +18,9 @@ /** * @author Isabel Rom�n Mart�nez * @version 0.2 Esta clase se crea para poder probar algunas de las capacidades - * que ofrece la api github Serádescartada posteriormente No usa - * Junit, sino que crea un main, no tiene verificaciones automáticas, - * la automatización no es posible + * que ofrece la api github Serádescartada posteriormente No usa Junit, + * sino que crea un main, no tiene verificaciones automáticas, la + * automatización no es posible * */ public class SupervisorControl { diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/DeveloperPerformanceIndicatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/DeveloperPerformanceIndicatorTest.java index 87efb13c..0c6acf92 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/strategies/DeveloperPerformanceIndicatorTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/DeveloperPerformanceIndicatorTest.java @@ -27,43 +27,48 @@ */ class DeveloperPerformanceIndicatorTest { - private DeveloperPerformanceStrategy indicador; + private DeveloperPerformanceStrategy indicador; - @BeforeEach - public void setUp() { - DeveloperPerformanceStrategy indicador = new DeveloperPerformanceStrategy(); - } + @BeforeEach + public void setUp() { + DeveloperPerformanceStrategy indicador = new DeveloperPerformanceStrategy(); + } - @Test - public void testCalcIndicator() throws ReportItemException, NotAvailableMetricException { - // Crear métricas simuladas - ReportItemI createdIssuesMes = new ReportItem.ReportItemBuilder("createdIssuesMes", 100.0).build(); - ReportItemI closedIssuesMes = new ReportItem.ReportItemBuilder("closedIssuesMes", 80.0).build(); - ReportItemI assignedDevIssuesMes = new ReportItem.ReportItemBuilder("assingedDevIssuesMes", 8.0).build(); - ReportItemI closedDevIssuesMes = new ReportItem.ReportItemBuilder("meanClosedIssuesLastMonth", 5.0).build(); + @Test + public void testCalcIndicator() throws ReportItemException, NotAvailableMetricException { + // Crear métricas simuladas + ReportItemI createdIssuesMes = new ReportItem.ReportItemBuilder("createdIssuesMes", 100.0) + .build(); + ReportItemI closedIssuesMes = new ReportItem.ReportItemBuilder("closedIssuesMes", 80.0).build(); + ReportItemI assignedDevIssuesMes = new ReportItem.ReportItemBuilder("assingedDevIssuesMes", 8.0) + .build(); + ReportItemI closedDevIssuesMes = new ReportItem.ReportItemBuilder("meanClosedIssuesLastMonth", + 5.0).build(); - // Calcular el indicador - List> metrics = Arrays.asList(createdIssuesMes, closedIssuesMes, assignedDevIssuesMes, closedDevIssuesMes); - ReportItemI result = indicador.calcIndicator(metrics); - - // Verificar el resultado - Double expectedIndicator = (80.0 / 100.0) / (5.0 / 8.0); - assertEquals(expectedIndicator, result.getValue()); - } + // Calcular el indicador + List> metrics = Arrays.asList(createdIssuesMes, closedIssuesMes, assignedDevIssuesMes, + closedDevIssuesMes); + ReportItemI result = indicador.calcIndicator(metrics); - @Test - public void testCalcIndicatorWithMissingMetrics() throws ReportItemException, NotAvailableMetricException { - // Crear métricas simuladas incompletas - ReportItemI createdIssuesMes = new ReportItem.ReportItemBuilder("createdIssuesMes", 100.0).build(); - ReportItemI meanClosedIssuesLastMonth = new ReportItem.ReportItemBuilder("meanClosedIssuesLastMonth", 80.0).build(); + // Verificar el resultado + Double expectedIndicator = (80.0 / 100.0) / (5.0 / 8.0); + assertEquals(expectedIndicator, result.getValue()); + } - // Calcular el indicador - List> metrics = Arrays.asList(createdIssuesMes, meanClosedIssuesLastMonth); + @Test + public void testCalcIndicatorWithMissingMetrics() throws ReportItemException, NotAvailableMetricException { + // Crear métricas simuladas incompletas + ReportItemI createdIssuesMes = new ReportItem.ReportItemBuilder("createdIssuesMes", 100.0) + .build(); + ReportItemI meanClosedIssuesLastMonth = new ReportItem.ReportItemBuilder( + "meanClosedIssuesLastMonth", 80.0).build(); - // Verificar que se lanza una excepción - assertThrows(NotAvailableMetricException.class, () -> { - indicador.calcIndicator(metrics); - }); - } + // Calcular el indicador + List> metrics = Arrays.asList(createdIssuesMes, meanClosedIssuesLastMonth); + + // Verificar que se lanza una excepción + assertThrows(NotAvailableMetricException.class, () -> { + indicador.calcIndicator(metrics); + }); + } } - diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/FixTimeStrategyTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/FixTimeStrategyTest.java index dbcd2a57..98b79ff4 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/strategies/FixTimeStrategyTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/FixTimeStrategyTest.java @@ -21,70 +21,68 @@ class FixTimeStrategyTest { - @Test - public void testCalcIndicator() throws NotAvailableMetricException { - // Creamos los mocks necesarios - ReportItemI mockClasificacion = Mockito.mock(ReportItemI.class); - ReportItemI mockCorreccion = Mockito.mock(ReportItemI.class); - ReportItemI mockNum_errores = Mockito.mock(ReportItemI.class); - - // Configuramos los mocks para devolver valores predefinidos - Mockito.when(mockClasificacion.getName()).thenReturn("clasificacion"); - Mockito.when(mockClasificacion.getValue()).thenReturn(10.0); - - Mockito.when(mockCorreccion.getName()).thenReturn("correccion"); - Mockito.when(mockCorreccion.getValue()).thenReturn(5.0); - - Mockito.when(mockNum_errores.getName()).thenReturn("num_errores"); - Mockito.when(mockNum_errores.getValue()).thenReturn(5.0); - - // Creamos una instancia de la estrategia - FixTimeStrategy strategy = new FixTimeStrategy(); - - // Ejecutamos el método que queremos probar con los mocks como argumentos - List> metrics = Arrays.asList(mockClasificacion, mockCorreccion, mockNum_errores); - ReportItemI result = strategy.calcIndicator(metrics); - - // Comprobamos que el resultado es el esperado - Assertions.assertEquals("ProcesoDeIssues", result.getName()); - Assertions.assertEquals(3.0, result.getValue()); - Assertions.assertDoesNotThrow(()->strategy.calcIndicator(metrics)); - - - } - - @Test - public void testCalcIndicatorThrowsNotAvailableMetricException() { - // Creamos los mocks necesarios - ReportItemI mockClasificacion = Mockito.mock(ReportItemI.class); - - // Configuramos los mocks para devolver valores predefinidos - Mockito.when(mockClasificacion.getName()).thenReturn("clasificacion"); - Mockito.when(mockClasificacion.getValue()).thenReturn(10.0); - - // Creamos una instancia de la estrategia - FixTimeStrategy strategy = new FixTimeStrategy(); - - // Ejecutamos el método que queremos probar con una sola métrica - List> metrics = Arrays.asList(mockClasificacion); - // Comprobamos que se lanza la excepción adecuada - NotAvailableMetricException exception = Assertions.assertThrows(NotAvailableMetricException.class, - () -> strategy.calcIndicator(metrics)); - - } - - - @Test - public void testRequiredMetrics() { - - // Creamos una instancia de la estrategia - FixTimeStrategy strategy = new FixTimeStrategy(); - - // Ejecutamos el método que queremos probar - List requiredMetrics = strategy.requiredMetrics(); - - // Comprobamos que el resultado es el esperado - List expectedMetrics = Arrays.asList("clasificacion", "correccion", "num_errores"); - Assertions.assertEquals(expectedMetrics, requiredMetrics); - } - } \ No newline at end of file + @Test + public void testCalcIndicator() throws NotAvailableMetricException { + // Creamos los mocks necesarios + ReportItemI mockClasificacion = Mockito.mock(ReportItemI.class); + ReportItemI mockCorreccion = Mockito.mock(ReportItemI.class); + ReportItemI mockNum_errores = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para devolver valores predefinidos + Mockito.when(mockClasificacion.getName()).thenReturn("clasificacion"); + Mockito.when(mockClasificacion.getValue()).thenReturn(10.0); + + Mockito.when(mockCorreccion.getName()).thenReturn("correccion"); + Mockito.when(mockCorreccion.getValue()).thenReturn(5.0); + + Mockito.when(mockNum_errores.getName()).thenReturn("num_errores"); + Mockito.when(mockNum_errores.getValue()).thenReturn(5.0); + + // Creamos una instancia de la estrategia + FixTimeStrategy strategy = new FixTimeStrategy(); + + // Ejecutamos el método que queremos probar con los mocks como argumentos + List> metrics = Arrays.asList(mockClasificacion, mockCorreccion, mockNum_errores); + ReportItemI result = strategy.calcIndicator(metrics); + + // Comprobamos que el resultado es el esperado + Assertions.assertEquals("ProcesoDeIssues", result.getName()); + Assertions.assertEquals(3.0, result.getValue()); + Assertions.assertDoesNotThrow(() -> strategy.calcIndicator(metrics)); + + } + + @Test + public void testCalcIndicatorThrowsNotAvailableMetricException() { + // Creamos los mocks necesarios + ReportItemI mockClasificacion = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para devolver valores predefinidos + Mockito.when(mockClasificacion.getName()).thenReturn("clasificacion"); + Mockito.when(mockClasificacion.getValue()).thenReturn(10.0); + + // Creamos una instancia de la estrategia + FixTimeStrategy strategy = new FixTimeStrategy(); + + // Ejecutamos el método que queremos probar con una sola métrica + List> metrics = Arrays.asList(mockClasificacion); + // Comprobamos que se lanza la excepción adecuada + NotAvailableMetricException exception = Assertions.assertThrows(NotAvailableMetricException.class, + () -> strategy.calcIndicator(metrics)); + + } + + @Test + public void testRequiredMetrics() { + + // Creamos una instancia de la estrategia + FixTimeStrategy strategy = new FixTimeStrategy(); + + // Ejecutamos el método que queremos probar + List requiredMetrics = strategy.requiredMetrics(); + + // Comprobamos que el resultado es el esperado + List expectedMetrics = Arrays.asList("clasificacion", "correccion", "num_errores"); + Assertions.assertEquals(expectedMetrics, requiredMetrics); + } +} \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/PRPerfomanceStrategyTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/PRPerfomanceStrategyTest.java index 1e19fefc..c47e191c 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/strategies/PRPerfomanceStrategyTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/PRPerfomanceStrategyTest.java @@ -16,77 +16,69 @@ class PRPerfomanceStrategyTest { public void testCalcIndicator() throws NotAvailableMetricException { - // Creamos los mocks necesarios - ReportItemI mockAcceptedLastYear = Mockito.mock(ReportItemI.class); - ReportItemI mockAcceptedLastMonth = Mockito.mock(ReportItemI.class); - ReportItemI mockRejectedLastYear = Mockito.mock(ReportItemI.class); - ReportItemI mockRejectedLastMonth = Mockito.mock(ReportItemI.class); - - // Configuramos los mocks para devolver valores predefinidos - Mockito.when(mockAcceptedLastYear.getName()).thenReturn("PRAcceptedLastYear"); - Mockito.when(mockAcceptedLastYear.getValue()).thenReturn((double)100); - - Mockito.when(mockAcceptedLastMonth.getName()).thenReturn("PRAcceptedLastMonth"); - Mockito.when(mockAcceptedLastMonth.getValue()).thenReturn((double)10); - - Mockito.when(mockRejectedLastYear.getName()).thenReturn("PRRejectedLastYear"); - Mockito.when(mockRejectedLastYear.getValue()).thenReturn((double)20); - - Mockito.when(mockRejectedLastMonth.getName()).thenReturn("PRRejectedLastMonth"); - Mockito.when(mockRejectedLastMonth.getValue()).thenReturn((double)2); - - // Creamos una instancia de PRPerformanceStrategy - PRPerformanceStrategy indicator = new PRPerformanceStrategy(); - - // Ejecutamos el método que queremos probar con los mocks como argumentos - List> metrics = Arrays.asList( - mockAcceptedLastYear, - mockAcceptedLastMonth, - mockRejectedLastYear, - mockRejectedLastMonth - ); - ReportItemI result = indicator.calcIndicator(metrics); - - // Comprobamos que el resultado es el esperado - Assertions.assertEquals("PRPerformance", result.getName()); - Assertions.assertEquals((double)100/20-(double)10/2, result.getValue()); - Assertions.assertDoesNotThrow(() -> indicator.calcIndicator(metrics)); - } - - @Test - public void testCalcIndicatorThrowsNotAvailableMetricException() { - // Creamos los mocks necesarios - ReportItemI mockAcceptedLastYear = Mockito.mock(ReportItemI.class); - - // Configuramos los mocks para devolver valores predefinidos - Mockito.when(mockAcceptedLastYear.getName()).thenReturn("PRAcceptedLastYear"); - Mockito.when(mockAcceptedLastYear.getValue()).thenReturn((double)100); - - // Creamos una instancia de PullRequestsAcceptanceIndicatorStrategy - PRPerformanceStrategy indicator = new PRPerformanceStrategy(); - - // Ejecutamos el método que queremos probar con una sola métrica - List> metrics = Arrays.asList(mockAcceptedLastYear); - // Comprobamos que se lanza la excepción adecuada - NotAvailableMetricException exception = Assertions.assertThrows(NotAvailableMetricException.class, - () -> indicator.calcIndicator(metrics)); - } - - @Test - public void testRequiredMetrics() { - // Creamos una instancia de PRPerformanceStrategy - PRPerformanceStrategy indicatorStrategy = new PRPerformanceStrategy(); - - // Ejecutamos el método que queremos probar - List requiredMetrics = indicatorStrategy.requiredMetrics(); - - // Comprobamos que el resultado es el esperado - List expectedMetrics = Arrays.asList( - "PRAcceptedLastYear", - "PRAcceptedLastMonth", - "PRRejectedLastMonth", - "PRRejectedLastYear" - ); - Assertions.assertEquals(expectedMetrics, requiredMetrics); - } + // Creamos los mocks necesarios + ReportItemI mockAcceptedLastYear = Mockito.mock(ReportItemI.class); + ReportItemI mockAcceptedLastMonth = Mockito.mock(ReportItemI.class); + ReportItemI mockRejectedLastYear = Mockito.mock(ReportItemI.class); + ReportItemI mockRejectedLastMonth = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para devolver valores predefinidos + Mockito.when(mockAcceptedLastYear.getName()).thenReturn("PRAcceptedLastYear"); + Mockito.when(mockAcceptedLastYear.getValue()).thenReturn((double) 100); + + Mockito.when(mockAcceptedLastMonth.getName()).thenReturn("PRAcceptedLastMonth"); + Mockito.when(mockAcceptedLastMonth.getValue()).thenReturn((double) 10); + + Mockito.when(mockRejectedLastYear.getName()).thenReturn("PRRejectedLastYear"); + Mockito.when(mockRejectedLastYear.getValue()).thenReturn((double) 20); + + Mockito.when(mockRejectedLastMonth.getName()).thenReturn("PRRejectedLastMonth"); + Mockito.when(mockRejectedLastMonth.getValue()).thenReturn((double) 2); + + // Creamos una instancia de PRPerformanceStrategy + PRPerformanceStrategy indicator = new PRPerformanceStrategy(); + + // Ejecutamos el método que queremos probar con los mocks como argumentos + List> metrics = Arrays.asList(mockAcceptedLastYear, mockAcceptedLastMonth, + mockRejectedLastYear, mockRejectedLastMonth); + ReportItemI result = indicator.calcIndicator(metrics); + + // Comprobamos que el resultado es el esperado + Assertions.assertEquals("PRPerformance", result.getName()); + Assertions.assertEquals((double) 100 / 20 - (double) 10 / 2, result.getValue()); + Assertions.assertDoesNotThrow(() -> indicator.calcIndicator(metrics)); + } + + @Test + public void testCalcIndicatorThrowsNotAvailableMetricException() { + // Creamos los mocks necesarios + ReportItemI mockAcceptedLastYear = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para devolver valores predefinidos + Mockito.when(mockAcceptedLastYear.getName()).thenReturn("PRAcceptedLastYear"); + Mockito.when(mockAcceptedLastYear.getValue()).thenReturn((double) 100); + + // Creamos una instancia de PullRequestsAcceptanceIndicatorStrategy + PRPerformanceStrategy indicator = new PRPerformanceStrategy(); + + // Ejecutamos el método que queremos probar con una sola métrica + List> metrics = Arrays.asList(mockAcceptedLastYear); + // Comprobamos que se lanza la excepción adecuada + NotAvailableMetricException exception = Assertions.assertThrows(NotAvailableMetricException.class, + () -> indicator.calcIndicator(metrics)); + } + + @Test + public void testRequiredMetrics() { + // Creamos una instancia de PRPerformanceStrategy + PRPerformanceStrategy indicatorStrategy = new PRPerformanceStrategy(); + + // Ejecutamos el método que queremos probar + List requiredMetrics = indicatorStrategy.requiredMetrics(); + + // Comprobamos que el resultado es el esperado + List expectedMetrics = Arrays.asList("PRAcceptedLastYear", "PRAcceptedLastMonth", "PRRejectedLastMonth", + "PRRejectedLastYear"); + Assertions.assertEquals(expectedMetrics, requiredMetrics); + } } diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/PullRequestIndicatorStrategyTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/PullRequestIndicatorStrategyTest.java index 8f84586a..42599e96 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/strategies/PullRequestIndicatorStrategyTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/PullRequestIndicatorStrategyTest.java @@ -20,84 +20,85 @@ import us.muit.fs.a4i.model.entities.ReportItemI; import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; import us.muit.fs.a4i.control.strategies.PullRequestIndicatorStrategy; + /** * Equipo cuatro curso 23/24 */ class PullRequestIndicatorStrategyTest { - @BeforeAll - static void setUpBeforeClass() throws Exception { - } - - @AfterAll - static void tearDownAfterClass() throws Exception { - } - - @BeforeEach - void setUp() throws Exception { - } - - @AfterEach - void tearDown() throws Exception { - } - - @Test - public void testCalcIndicator() throws NotAvailableMetricException { - // Creamos los mocks - ReportItemI mockPullReqTotales = Mockito.mock(ReportItemI.class); - ReportItemI mockPullReqCompletados = Mockito.mock(ReportItemI.class); - - // Configuramos los mocks para que el porcentaje sea el 60% - Mockito.when(mockPullReqTotales.getName()).thenReturn("totalPullReq"); - Mockito.when(mockPullReqTotales.getValue()).thenReturn(100.0); - - Mockito.when(mockPullReqCompletados.getName()).thenReturn("closedPullReq"); - Mockito.when(mockPullReqCompletados.getValue()).thenReturn(60.0); - - // Creamos la calculadora del indicador - PullRequestIndicatorStrategy indicadorCalc = new PullRequestIndicatorStrategy(); - - // Calculamos el indicador - List> metricas = Arrays.asList(mockPullReqTotales, mockPullReqCompletados); - ReportItemI resultado = indicadorCalc.calcIndicator(metricas); - - // Comprobamos que el resultado es el esperado - Assertions.assertEquals("pullRequestCompletion", resultado.getName()); - Assertions.assertEquals(60.0, resultado.getValue()); - Assertions.assertDoesNotThrow(()->indicadorCalc.calcIndicator(metricas)); - } - - @Test - public void testCalcIndicatorThrowsNotAvailableMetricException() { - // Creamos un mock - ReportItemI mockPullReqTotales = Mockito.mock(ReportItemI.class); - - // Configuramos el mock - Mockito.when(mockPullReqTotales.getName()).thenReturn("totalPullReq"); - Mockito.when(mockPullReqTotales.getValue()).thenReturn(100.0); - - // Creamos la calculadora del indicador - PullRequestIndicatorStrategy indicadorCalc = new PullRequestIndicatorStrategy(); - - // Probamos con una sola métrica - List> metricas = Arrays.asList(mockPullReqTotales); - // Comprobamos que se lanza la excepción - NotAvailableMetricException exception = Assertions.assertThrows(NotAvailableMetricException.class, - () -> indicadorCalc.calcIndicator(metricas)); - - } - - @Test - public void testRequiredMetrics() { - // Creamos la calculadora del indicador - PullRequestIndicatorStrategy indicadorCalc = new PullRequestIndicatorStrategy(); - - // Obtenemos las metricas del indicador - List metricas = indicadorCalc.requiredMetrics(); - - // Comprobamos que el resultado es el esperado - List metricasEsperadas = Arrays.asList("totalPullReq", "closedPullReq"); - Assertions.assertEquals(metricasEsperadas, metricas); - } + @BeforeAll + static void setUpBeforeClass() throws Exception { + } + + @AfterAll + static void tearDownAfterClass() throws Exception { + } + + @BeforeEach + void setUp() throws Exception { + } + + @AfterEach + void tearDown() throws Exception { + } + + @Test + public void testCalcIndicator() throws NotAvailableMetricException { + // Creamos los mocks + ReportItemI mockPullReqTotales = Mockito.mock(ReportItemI.class); + ReportItemI mockPullReqCompletados = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para que el porcentaje sea el 60% + Mockito.when(mockPullReqTotales.getName()).thenReturn("totalPullReq"); + Mockito.when(mockPullReqTotales.getValue()).thenReturn(100.0); + + Mockito.when(mockPullReqCompletados.getName()).thenReturn("closedPullReq"); + Mockito.when(mockPullReqCompletados.getValue()).thenReturn(60.0); + + // Creamos la calculadora del indicador + PullRequestIndicatorStrategy indicadorCalc = new PullRequestIndicatorStrategy(); + // Calculamos el indicador + List> metricas = Arrays.asList(mockPullReqTotales, mockPullReqCompletados); + ReportItemI resultado = indicadorCalc.calcIndicator(metricas); + + // Comprobamos que el resultado es el esperado + Assertions.assertEquals("pullRequestCompletion", resultado.getName()); + Assertions.assertEquals(60.0, resultado.getValue()); + Assertions.assertDoesNotThrow(() -> indicadorCalc.calcIndicator(metricas)); + } + + @Test + public void testCalcIndicatorThrowsNotAvailableMetricException() { + // Creamos un mock + ReportItemI mockPullReqTotales = Mockito.mock(ReportItemI.class); + + // Configuramos el mock + Mockito.when(mockPullReqTotales.getName()).thenReturn("totalPullReq"); + Mockito.when(mockPullReqTotales.getValue()).thenReturn(100.0); + + // Creamos la calculadora del indicador + PullRequestIndicatorStrategy indicadorCalc = new PullRequestIndicatorStrategy(); + + // Probamos con una sola métrica + List> metricas = Arrays.asList(mockPullReqTotales); + // Comprobamos que se lanza la excepción + NotAvailableMetricException exception = Assertions.assertThrows(NotAvailableMetricException.class, + () -> indicadorCalc.calcIndicator(metricas)); + + } + + @Test + public void testRequiredMetrics() { + // Creamos la calculadora del indicador + PullRequestIndicatorStrategy indicadorCalc = new PullRequestIndicatorStrategy(); + + // Obtenemos las metricas del indicador + List metricas = indicadorCalc.requiredMetrics(); + + // Comprobamos que el resultado es el esperado + List metricasEsperadas = Arrays.asList("totalPullReq", "closedPullReq"); + Assertions.assertEquals(metricasEsperadas, metricas); } + +} diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/RepositoryCalculatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/RepositoryCalculatorTest.java index 0a9d9fb5..25c9a7d0 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/strategies/RepositoryCalculatorTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/RepositoryCalculatorTest.java @@ -153,7 +153,8 @@ void testCalIndicator() { @Tag("unit") @DisplayName("Prueba calcIndicator de RepositoryCalculator con metricas incorrectas y usando mocks") void unitTestCalIndicatorNotRequiredMetrics() throws NotAvailableMetricException, ReportItemException { - // prueba la calculadora usando mocks, para el caso de que las métricas necesarias no estén disponibles + // prueba la calculadora usando mocks, para el caso de que las métricas + // necesarias no estén disponibles // Creamos la clase a probar RepositoryCalculator repositoryCalculator = new RepositoryCalculator(); diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubDeveloperEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubDeveloperEnquirerTest.java index de7c33d2..b0584e59 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubDeveloperEnquirerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubDeveloperEnquirerTest.java @@ -16,15 +16,15 @@ class GitHubDeveloperEnquirerTest { GitHubDeveloperEnquirer ghEnquirer = new GitHubDeveloperEnquirer(); /** - * Test method for - * GitHubOrganizationEnquirer - * @throws MetricException - * @throws ReportItemException + * Test method for GitHubOrganizationEnquirer + * + * @throws MetricException + * @throws ReportItemException */ @Test void testAssignedIssuesLastMonth() throws MetricException { - ReportItem metric = ghEnquirer.getMetric("closedIssuesLastMonth","Isabel-Roman"); - assertEquals(metric.getName(),"closedIssuesLastMonth"); + ReportItem metric = ghEnquirer.getMetric("closedIssuesLastMonth", "Isabel-Roman"); + assertEquals(metric.getName(), "closedIssuesLastMonth"); log.info(metric.getValue().toString()); log.info(metric.getDescription()); } diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java index c19d5b47..9940ad6a 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java @@ -4,7 +4,6 @@ * */ - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -20,7 +19,6 @@ import org.kohsuke.github.GHRepositoryStatistics.CodeFrequency; import org.kohsuke.github.GHProject; - import org.junit.jupiter.api.Test; import org.kohsuke.github.GHOrganization; @@ -35,8 +33,8 @@ import us.muit.fs.a4i.model.remote.GitHubOrganizationEnquirer; /** - * @author Roberto Lama (Curso 22/23) - * RECUERDA: el token tiene que tener permiso de acceso a la organización para que los tests puedan ejecutarse + * @author Roberto Lama (Curso 22/23) RECUERDA: el token tiene que tener permiso + * de acceso a la organización para que los tests puedan ejecutarse * */ public class GitHubOrganizationEnquirerTest { @@ -44,113 +42,114 @@ public class GitHubOrganizationEnquirerTest { GitHubOrganizationEnquirer ghEnquirer = new GitHubOrganizationEnquirer(); /** - * Test method for - * GitHubOrganizationEnquirer - * @throws MetricException - * @throws ReportItemException + * Test method for GitHubOrganizationEnquirer + * + * @throws MetricException + * @throws ReportItemException */ @Test void testGetPullRequest() throws MetricException, ReportItemException { - + // TEST 2: PullRequest ReportItem metricsPullRequest = ghEnquirer.getMetric("pullRequests", "MIT-FS"); - - assertTrue(metricsPullRequest.getValue().intValue()>0, "el número de pullRequests no es el esperado"); // Tiene PR + + assertTrue(metricsPullRequest.getValue().intValue() > 0, "el número de pullRequests no es el esperado"); // Tiene + // PR } - - + /** - * Test method for - * GitHubOrganizationEnquirer - * @throws MetricException - * @throws ReportItemException + * Test method for GitHubOrganizationEnquirer + * + * @throws MetricException + * @throws ReportItemException */ @Test void testGetRepositories() throws MetricException, ReportItemException { - - // TEST 3: Repositories - ReportItem metricsRepositories = ghEnquirer.getMetric("repositories", "MIT-FS"); - - assertTrue(metricsRepositories.getValue().intValue()>0, "El número de repositorios no es el esperado"); // Tiene repositorios + + // TEST 3: Repositories + ReportItem metricsRepositories = ghEnquirer.getMetric("repositories", "MIT-FS"); + + assertTrue(metricsRepositories.getValue().intValue() > 0, "El número de repositorios no es el esperado"); // Tiene + // repositorios } - - - + /** - * Test method for - * GitHubOrganizationEnquirer - * @throws MetricException - * @throws ReportItemException + * Test method for GitHubOrganizationEnquirer + * + * @throws MetricException + * @throws ReportItemException */ @Test void testGetMembers() throws MetricException, ReportItemException { - - // TEST 4: Members - ReportItem metricsMembers = ghEnquirer.getMetric("members", "MIT-FS"); - assertTrue(metricsMembers.getValue().intValue()>0, "El número de miembros no es el esperado"); // Tiene 30 miembros - + // TEST 4: Members + ReportItem metricsMembers = ghEnquirer.getMetric("members", "MIT-FS"); + + assertTrue(metricsMembers.getValue().intValue() > 0, "El número de miembros no es el esperado"); // Tiene 30 + // miembros + } - /** - * Test method for - * GitHubOrganizationEnquirer - * @throws MetricException - * @throws ReportItemException + * Test method for GitHubOrganizationEnquirer + * + * @throws MetricException + * @throws ReportItemException */ @Test void testGetTeams() throws MetricException, ReportItemException { - + // TEST 5: Teams ReportItem metricsTeams = ghEnquirer.getMetric("teams", "MIT-FS"); - assertTrue(metricsTeams.getValue().intValue()>3, "El número equipos no es el esperado"); // Tiene más de 3 - + assertTrue(metricsTeams.getValue().intValue() > 3, "El número equipos no es el esperado"); // Tiene más de 3 + } - + /** - * Test method for - * GitHubOrganizationEnquirer - * @throws MetricException - * @throws ReportItemException + * Test method for GitHubOrganizationEnquirer + * + * @throws MetricException + * @throws ReportItemException */ @Test void testGetOpenProjects() throws MetricException, ReportItemException { - + // TEST 6: OpenProjects ReportItem op = ghEnquirer.getMetric("openProjects", "MIT-FS"); - assertTrue(op.getValue().intValue()>0,"El número de proyectos abiertos no es el esperado"); + assertTrue(op.getValue().intValue() > 0, "El número de proyectos abiertos no es el esperado"); } - + /** - * Test method for - * GitHubOrganizationEnquirer - * @throws MetricException - * @throws ReportItemException + * Test method for GitHubOrganizationEnquirer + * + * @throws MetricException + * @throws ReportItemException */ @Test void testGetClosedProjects() throws MetricException, ReportItemException { - + // TEST 7: ClosedProjects ReportItem metricsClosedProjects = ghEnquirer.getMetric("closedProjects", "MIT-FS"); - - assertTrue(metricsClosedProjects.getValue().intValue()>0, "El número de proyectos cerrados no es el esperado"); + + assertTrue(metricsClosedProjects.getValue().intValue() > 0, + "El número de proyectos cerrados no es el esperado"); } - + /** - * Test method for - * GitHubOrganizationEnquirer - * @throws MetricException - * @throws ReportItemException + * Test method for GitHubOrganizationEnquirer + * + * @throws MetricException + * @throws ReportItemException */ @Test void testGetRepositoriesWithOpenPullRequest() throws MetricException, ReportItemException { - - + // TEST 1: RepositoriesWithOpenPullRequest - ReportItem metricsRepositoriesWithOpenPullRequest = ghEnquirer.getMetric("repositoriesWithOpenPullRequest", "MIT-FS"); - - assertTrue(metricsRepositoriesWithOpenPullRequest.getValue().intValue()>0, "El número de repositorios con pull requests abiertos no es el esperado"); - } - + ReportItem metricsRepositoriesWithOpenPullRequest = ghEnquirer + .getMetric("repositoriesWithOpenPullRequest", "MIT-FS"); + + assertTrue(metricsRepositoriesWithOpenPullRequest.getValue().intValue() > 0, + "El número de repositorios con pull requests abiertos no es el esperado"); + } + } diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java index 82523ad4..4811d81b 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java @@ -14,105 +14,110 @@ import us.muit.fs.a4i.exceptions.ReportItemException; import us.muit.fs.a4i.model.entities.ReportItem; import us.muit.fs.a4i.model.remote.GitHubRepositoryEnquirer; + /** * */ class GitHubRepositoryEnquirerTest { - + private static Logger log = Logger.getLogger(GitHubOrganizationEnquirerTest.class.getName()); GitHubRepositoryEnquirer ghEnquirer = new GitHubRepositoryEnquirer(); /** - * Test method for - * GitHubRepositoryEnquirer, verifing that issuesLastMonth is correctly obtained - * @throws MetricException - * @throws ReportItemException + * Test method for GitHubRepositoryEnquirer, verifing that issuesLastMonth is + * correctly obtained + * + * @throws MetricException + * @throws ReportItemException */ @Test - void testIssuesLastMonth() throws MetricException, ReportItemException { - ReportItem metric = ghEnquirer.getMetric("issuesLastMonth", "MIT-FS/Audit4Improve-API"); - assertEquals(metric.getName(),"issuesLastMonth"); + void testIssuesLastMonth() throws MetricException, ReportItemException { + ReportItem metric = ghEnquirer.getMetric("issuesLastMonth", "MIT-FS/Audit4Improve-API"); + assertEquals(metric.getName(), "issuesLastMonth"); log.info(metric.getValue().toString()); log.info(metric.getDescription()); } - + /** - * Test method for - * GitHubRepositoryEnquirer, verifing that closedIssuesLastMonth is correctly obtained - * @throws MetricException - * @throws ReportItemException + * Test method for GitHubRepositoryEnquirer, verifing that closedIssuesLastMonth + * is correctly obtained + * + * @throws MetricException + * @throws ReportItemException */ @Test - void testClosedIssuesLastMonth() throws MetricException, ReportItemException { - ReportItem metric = ghEnquirer.getMetric("closedIssuesLastMonth", "MIT-FS/Audit4Improve-API"); - assertEquals(metric.getName(),"closedIssuesLastMonth"); + void testClosedIssuesLastMonth() throws MetricException, ReportItemException { + ReportItem metric = ghEnquirer.getMetric("closedIssuesLastMonth", "MIT-FS/Audit4Improve-API"); + assertEquals(metric.getName(), "closedIssuesLastMonth"); log.info(metric.getValue().toString()); log.info(metric.getDescription()); } + /** - * Test method for - * GitHubRepositoryEnquirer, verifing that closedIssuesLastMonth is correctly obtained - * @throws MetricException - * @throws ReportItemException + * Test method for GitHubRepositoryEnquirer, verifing that closedIssuesLastMonth + * is correctly obtained + * + * @throws MetricException + * @throws ReportItemException */ @Test - void testMeanClosedIssuesLastMonth() throws MetricException, ReportItemException { - ReportItem metric = ghEnquirer.getMetric("meanClosedIssuesLastMonth", "MIT-FS/Audit4Improve-API"); - assertEquals(metric.getName(),"meanClosedIssuesLastMonth"); + void testMeanClosedIssuesLastMonth() throws MetricException, ReportItemException { + ReportItem metric = ghEnquirer.getMetric("meanClosedIssuesLastMonth", "MIT-FS/Audit4Improve-API"); + assertEquals(metric.getName(), "meanClosedIssuesLastMonth"); log.info(metric.getValue().toString()); log.info(metric.getDescription()); } - /** - * Test method for {@link us.muit.fs.a4i.model.remote.GitHubEnquirer#getAvailableMetrics()}. + * Test method for + * {@link us.muit.fs.a4i.model.remote.GitHubEnquirer#getAvailableMetrics()}. */ @Test void testGetAvailableMetrics() { - List availableMetrics=ghEnquirer.getAvailableMetrics(); + List availableMetrics = ghEnquirer.getAvailableMetrics(); log.info(availableMetrics.toString()); } - + @Test void testGetTotalPullRequests() throws MetricException { - + // Nombre de la métrica que queremos consultar String nombreMetrica = "totalPullReq"; - + // Repositorio del que se quiere obtener la métrica String repositoryId = "MIT-FS/Audit4Improve-API"; - - // Variable para almacenar el número total de pull requests + + // Variable para almacenar el número total de pull requests ReportItem metrica = null; - - // Creamos el RemoteEnquirer para el repositorio GitHub - GitHubRepositoryEnquirer enquirer = new GitHubRepositoryEnquirer(); - // Obtenemos el número total de pull requests - metrica = enquirer.getMetric(nombreMetrica, repositoryId); - log.info(metrica.toString()); + // Creamos el RemoteEnquirer para el repositorio GitHub + GitHubRepositoryEnquirer enquirer = new GitHubRepositoryEnquirer(); + + // Obtenemos el número total de pull requests + metrica = enquirer.getMetric(nombreMetrica, repositoryId); + log.info(metrica.toString()); + + // Comprobamos que el resultado coincida con el número total de pull requests + // real + assertTrue(metrica.getValue() > 0, "Bad number of PR"); - // Comprobamos que el resultado coincida con el número total de pull requests real - assertTrue(metrica.getValue()>0,"Bad number of PR"); - } - + /** * @throws MetricException */ @Test void testCompletedPullRequests() throws MetricException { - + // Nombre de la métrica que queremos consultar String nombreMetrica = "closedPullReq"; - + // Repositorio del que se quiere obtener la métrica String repositoryId = "MIT-FS/Audit4Improve-API"; - + // Variable para almacenar el número de pull requests completados ReportItem metrica = null; - - + // Creamos el RemoteEnquirer para el repositorio GitHub GitHubRepositoryEnquirer enquirer = new GitHubRepositoryEnquirer(); @@ -120,9 +125,10 @@ void testCompletedPullRequests() throws MetricException { metrica = enquirer.getMetric(nombreMetrica, repositoryId); log.info(metrica.toString()); - // Comprobamos que el resultado coincida con el número de pull requests completados real - - assertTrue(0 metricIntMock = Mockito.mock(ReportItem.class); @Mock(serializable = true) private static ReportItem metricStrMock = Mockito.mock(ReportItem.class); - + @Mock(serializable = true) private static ReportI informe = Mockito.mock(ReportI.class); - + @Mock(serializable = true) private static ReportItem itemIndicatorIntMock = Mockito.mock(ReportItem.class); - - @Mock(serializable=true) + + @Mock(serializable = true) private static Indicator indicatorIntMock = Mockito.mock(Indicator.class); - + @Mock(serializable = true) private static ReportItem itemIndicatorIntMock2 = Mockito.mock(ReportItem.class); - - @Mock(serializable=true) + + @Mock(serializable = true) private static Indicator indicatorIntMock2 = Mockito.mock(Indicator.class); - + @Mock(serializable = true) private static ReportItem itemIndicatorIntMock3 = Mockito.mock(ReportItem.class); - - @Mock(serializable=true) + + @Mock(serializable = true) private static Indicator indicatorIntMock3 = Mockito.mock(Indicator.class); - - - + private static String excelPath; private static String excelName; private static ExcelReportManager underTest; + /** - *

Acciones a realizar antes de ejecutar los tests definidos en esta clase

+ *

+ * Acciones a realizar antes de ejecutar los tests definidos en esta clase + *

+ * * @throws java.lang.Exception * @see org.junit.jupiter.api.BeforeAll */ @BeforeAll static void setUpBeforeClass() throws Exception { - + } - - + @Test void ExcelCreation() { - List listaMetric=new ArrayList(); - List listaInd=new ArrayList(); - Date fecha=new Date(); + List listaMetric = new ArrayList(); + List listaInd = new ArrayList(); + Date fecha = new Date(); Mockito.when(metricIntMock.getValue()).thenReturn(55); Mockito.when(metricIntMock.getName()).thenReturn("downloads"); Mockito.when(metricIntMock.getUnit()).thenReturn("downloads"); - Mockito.when(metricIntMock.getDescription()).thenReturn("Descargas realizadas"); - Mockito.when(metricIntMock.getDate()).thenReturn(fecha); + Mockito.when(metricIntMock.getDescription()).thenReturn("Descargas realizadas"); + Mockito.when(metricIntMock.getDate()).thenReturn(fecha); listaMetric.add(metricIntMock); - + Mockito.when(metricStrMock.getValue()).thenReturn("2-2-22"); Mockito.when(metricStrMock.getName()).thenReturn("lastPush"); Mockito.when(metricStrMock.getUnit()).thenReturn("date"); - Mockito.when(metricStrMock.getDescription()).thenReturn("Último push realizado en el repositorio"); - Mockito.when(metricStrMock.getDate()).thenReturn(fecha); + Mockito.when(metricStrMock.getDescription()).thenReturn("Último push realizado en el repositorio"); + Mockito.when(metricStrMock.getDate()).thenReturn(fecha); listaMetric.add(metricStrMock); - + Mockito.when(itemIndicatorIntMock.getValue()).thenReturn(22); Mockito.when(itemIndicatorIntMock.getName()).thenReturn("otracosa"); Mockito.when(itemIndicatorIntMock.getUnit()).thenReturn("cosas"); - Mockito.when(itemIndicatorIntMock.getDescription()).thenReturn("Indicador Ejemplo"); - Mockito.when(itemIndicatorIntMock.getDate()).thenReturn(fecha); + Mockito.when(itemIndicatorIntMock.getDescription()).thenReturn("Indicador Ejemplo"); + Mockito.when(itemIndicatorIntMock.getDate()).thenReturn(fecha); Mockito.when(indicatorIntMock.getState()).thenReturn(IndicatorState.CRITICAL); Mockito.when(itemIndicatorIntMock.getIndicator()).thenReturn(indicatorIntMock); listaInd.add(itemIndicatorIntMock); - + Mockito.when(itemIndicatorIntMock2.getValue()).thenReturn(67); Mockito.when(itemIndicatorIntMock2.getName()).thenReturn("indicador2"); Mockito.when(itemIndicatorIntMock2.getUnit()).thenReturn("unidad2"); - Mockito.when(itemIndicatorIntMock2.getDescription()).thenReturn("Indicador Ejemplo 2"); - Mockito.when(itemIndicatorIntMock2.getDate()).thenReturn(fecha); + Mockito.when(itemIndicatorIntMock2.getDescription()).thenReturn("Indicador Ejemplo 2"); + Mockito.when(itemIndicatorIntMock2.getDate()).thenReturn(fecha); Mockito.when(indicatorIntMock2.getState()).thenReturn(IndicatorState.WARNING); Mockito.when(itemIndicatorIntMock2.getIndicator()).thenReturn(indicatorIntMock2); listaInd.add(itemIndicatorIntMock2); - + Mockito.when(itemIndicatorIntMock3.getValue()).thenReturn(98); Mockito.when(itemIndicatorIntMock3.getName()).thenReturn("indicador3"); Mockito.when(itemIndicatorIntMock3.getUnit()).thenReturn("unidad3"); - Mockito.when(itemIndicatorIntMock3.getDescription()).thenReturn("IndicadorEjemplo3"); - Mockito.when(itemIndicatorIntMock3.getDate()).thenReturn(fecha); + Mockito.when(itemIndicatorIntMock3.getDescription()).thenReturn("IndicadorEjemplo3"); + Mockito.when(itemIndicatorIntMock3.getDate()).thenReturn(fecha); Mockito.when(indicatorIntMock3.getState()).thenReturn(IndicatorState.CRITICAL); Mockito.when(itemIndicatorIntMock3.getIndicator()).thenReturn(indicatorIntMock3); listaInd.add(itemIndicatorIntMock3); - + Mockito.when(informe.getAllMetrics()).thenReturn(listaMetric); Mockito.when(informe.getAllIndicators()).thenReturn(listaInd); Mockito.when(informe.getEntityId()).thenReturn("entidadTest"); - - excelPath = new String("src" + File.separator + "test" + File.separator + "resources"+File.separator); - excelName= new String("excelTest.xlsx"); - underTest=new ExcelReportManager(excelPath,excelName); - + + excelPath = new String("src" + File.separator + "test" + File.separator + "resources" + File.separator); + excelName = new String("excelTest.xlsx"); + underTest = new ExcelReportManager(excelPath, excelName); + underTest.setFormater(new ReportFormater()); - - log.info("El informe tiene el id "+informe.getEntityId()); + + log.info("El informe tiene el id " + informe.getEntityId()); underTest.saveReport(informe); - } + /** - *

Test para el m�todo de eliminar un informe en excel, solo verifica que si es null da excepcion

+ *

+ * Test para el m�todo de eliminar un informe en excel, solo verifica que si es + * null da excepcion + *

+ * * @author Mariana Reyes Henriquez */ @Test void ExcelDelete() { - excelPath = new String("src" + File.separator + "test" + File.separator + "resources"+File.separator); - excelName= new String("excelTest.xlsx"); + excelPath = new String("src" + File.separator + "test" + File.separator + "resources" + File.separator); + excelName = new String("excelTest.xlsx"); Mockito.when(informe.getEntityId()).thenReturn("entidadTest"); - - underTest=new ExcelReportManager(excelPath,excelName); + + underTest = new ExcelReportManager(excelPath, excelName); underTest.setFormater(new ReportFormater()); - - log.info("El informe tiene el id "+informe.getEntityId()); + + log.info("El informe tiene el id " + informe.getEntityId()); underTest.saveReport(informe); try { log.info("Se intenta eliminar un informe que no existe"); @@ -181,5 +186,5 @@ void ExcelDelete() { e.printStackTrace(); } } - + } \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportWithRepositoryEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportWithRepositoryEnquirerTest.java index f3c5e88f..f3699cf3 100644 --- a/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportWithRepositoryEnquirerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/persistence/ExcelReportWithRepositoryEnquirerTest.java @@ -13,17 +13,18 @@ class ExcelReportWithRepositoryEnquirerTest { private static Logger log = Logger.getLogger(ExcelReportWithRepositoryEnquirerTest.class.getName()); + @Test void testSaveRepositoryReport() { - String excelPath = new String("src" + File.separator + "test" + File.separator + "resources"+File.separator); - String excelName= new String("excelTest.xlsx"); + String excelPath = new String("src" + File.separator + "test" + File.separator + "resources" + File.separator); + String excelName = new String("excelTest.xlsx"); String repoName = new String("MIT-FS/Audit4Improve-API"); - ExcelReportManager underTest=new ExcelReportManager(excelPath,excelName); - + ExcelReportManager underTest = new ExcelReportManager(excelPath, excelName); + underTest.setFormater(new ReportFormater()); - + GitHubRepositoryEnquirer ghEnquirer = new GitHubRepositoryEnquirer(); - + underTest.saveReport(ghEnquirer.buildReport(repoName)); } From e43c05b8f170fa993272728da4b4802f89debbb3 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Sun, 23 Feb 2025 19:37:17 +0100 Subject: [PATCH 080/100] Indicador conventionsCompliant del equipo 1 del curso 23/24 Indicador completo, solo necesita revision --- .../java/us/muit/fs/a4i/config/GitFlow.java | 60 ++++ .../ConventionsCompliantStrategy.java | 140 ++++++++++ .../remote/GitHubRepositoryEnquirer.java | 264 ++++++++++++++++++ src/main/resources/a4iDefault.json | 42 ++- .../ConventionsCompliantStrategyTest.java | 177 ++++++++++++ .../remote/GitHubRepositoryEnquirerTest.java | 80 ++++++ 6 files changed, 759 insertions(+), 4 deletions(-) create mode 100644 src/main/java/us/muit/fs/a4i/config/GitFlow.java create mode 100644 src/main/java/us/muit/fs/a4i/control/strategies/ConventionsCompliantStrategy.java create mode 100644 src/test/java/us/muit/fs/a4i/test/control/strategies/ConventionsCompliantStrategyTest.java diff --git a/src/main/java/us/muit/fs/a4i/config/GitFlow.java b/src/main/java/us/muit/fs/a4i/config/GitFlow.java new file mode 100644 index 00000000..149d48cc --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/config/GitFlow.java @@ -0,0 +1,60 @@ +/** + * + */ +package us.muit.fs.a4i.config; + +/** + * Clase incluida por el equipo 1 del curso 23/24 para apoyo a las métricas de conformidad con convenciones de la organización + */ + + +public enum GitFlow { + // MARK: - Enum cases + /** The main branch */ + MAIN, + /** The develop branch */ + DEVELOP, + /** A feature branch */ + FEATURE, + /** A release branch */ + RELEASE, + /** A hotfix branch */ + HOTFIX; + + // MARK: - Methods + @Override + /** + * Returns the string representation of the enum case + */ + public String toString() { + return switch (this) { + case MAIN -> "main"; + case DEVELOP -> "develop"; + case FEATURE -> "feature"; + case RELEASE -> "release"; + case HOTFIX -> "hotfix"; + }; + } + + /** + * Checks if the branch name is a valid Git Flow branch + * @param branchName the name of the branch + * @return true if the branch name is a valid Git Flow branch + */ + public static boolean isGitFlowBranch(String branchName) { + return branchName.equals(MAIN.toString()) + || branchName.equals(DEVELOP.toString()) + || branchName.matches(branchNamePattern(FEATURE.toString())) + || branchName.matches(branchNamePattern(RELEASE.toString())) + || branchName.matches(branchNamePattern(HOTFIX.toString())); + } + + /** + * Returns the pattern for the branch name + * @param branchType the type of branch + * @return the regex pattern for the branch name + */ + private static String branchNamePattern(String branchType) { + return "^" + branchType + "/[a-zA-Z0-9_-]+$"; + } +} \ No newline at end of file diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/ConventionsCompliantStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/ConventionsCompliantStrategy.java new file mode 100644 index 00000000..f544062a --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/control/strategies/ConventionsCompliantStrategy.java @@ -0,0 +1,140 @@ +/** + * + */ +package us.muit.fs.a4i.control.strategies; + +/** + * + */ +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; + +import java.util.Optional; + +import us.muit.fs.a4i.control.IndicatorStrategy; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; + +public class ConventionsCompliantStrategy implements IndicatorStrategy { + // MARK: - Attributes + private static Logger log = Logger.getLogger(Indicator.class.getName()); + // The metrics needed to calculate the indicator output + private static final List REQUIRED_METRICS = Arrays.asList( + "conventionalCommits", + "commitsWithDescription", + "issuesWithLabels", + "gitFlowBranches", + "conventionalPullRequests"); + public static final String ID = "conventionsCompliant"; + + // Weights for each metric + private double WEIGHT_CONVENTIONAL_COMMITS = 0.2; + private double WEIGHT_COMMITS_WITH_DESCRIPTION = 0.2; + private double WEIGHT_ISSUES_WITH_LABELS = 0.2; + private double WEIGHT_GIT_FLOW_BRANCHES = 0.2; + private double WEIGHT_CONVENTIONAL_PULL_REQUESTS = 0.2; + + // MARK: - Constructor + /** + * Constructor. Default weights are equal for all metrics (0.2). + */ + public ConventionsCompliantStrategy() { + super(); + } + /** + * Constructor. Set the weights for each metric. The sum of the weights must be equal to 1. + * @param weightConventionalCommits the weight for the conventional commits metric + * @param weightCommitsWithDescription the weight for the commits with description metric + * @param weightIssuesWithLabels the weight for the issues with labels metric + * @param weightGitFlowBranches the weight for the git flow branches metric + * @param weightConventionalPullRequests the weight for the conventional pull requests metric + */ + public ConventionsCompliantStrategy(Double weightConventionalCommits, Double weightCommitsWithDescription, + Double weightIssuesWithLabels, Double weightGitFlowBranches, + Double weightConventionalPullRequests) throws IllegalArgumentException { + // Control that the sum of the weights is equal to 1 + if (weightConventionalCommits + weightCommitsWithDescription + weightIssuesWithLabels + weightGitFlowBranches + weightConventionalPullRequests != 1) { + throw new IllegalArgumentException("The sum of the weights must be equal to 1"); + } + + // Set the weights + this.WEIGHT_CONVENTIONAL_COMMITS = weightConventionalCommits; + this.WEIGHT_COMMITS_WITH_DESCRIPTION = weightCommitsWithDescription; + this.WEIGHT_ISSUES_WITH_LABELS = weightIssuesWithLabels; + this.WEIGHT_GIT_FLOW_BRANCHES = weightGitFlowBranches; + this.WEIGHT_CONVENTIONAL_PULL_REQUESTS = weightConventionalPullRequests; + } + + // MARK: - Implemented methods + // Calculate the indicator output based on the provided metrics + @Override + public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { + // Attributes + Optional> conventionalCommits = metrics.stream().filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); + Optional> commitsWithDescription = metrics.stream().filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); + Optional> issuesWithLabels = metrics.stream().filter(m -> REQUIRED_METRICS.get(2).equals(m.getName())).findAny(); + Optional> gitFlowBranches = metrics.stream().filter(m -> REQUIRED_METRICS.get(3).equals(m.getName())).findAny(); + Optional> conventionalPullRequests = metrics.stream().filter(m -> REQUIRED_METRICS.get(4).equals(m.getName())).findAny(); + ReportItemI indicatorReport = null; + + // Check if the required metrics are present + if (conventionalCommits.isPresent() && commitsWithDescription.isPresent() && issuesWithLabels.isPresent() && gitFlowBranches.isPresent() && conventionalPullRequests.isPresent()) { + // Calculate the indicator + Double metric1, metric2, metric3, metric4, metric5; + // Initialize the metrics + if (conventionalCommits.get().getValue() >= 0.8499) metric1 = conventionalCommits.get().getValue(); + else metric1 = 0.0; + if (commitsWithDescription.get().getValue() >= 0.8499) metric2 = commitsWithDescription.get().getValue(); + else metric2 = 0.0; + if (issuesWithLabels.get().getValue() >= 0.8499) metric3 = issuesWithLabels.get().getValue(); + else metric3 = 0.0; + if (gitFlowBranches.get().getValue() >= 0.999) metric4 = 1.0; + else metric4 = 0.0; + if (conventionalPullRequests.get().getValue() >= 0.8499) metric5 = conventionalPullRequests.get().getValue(); + else metric5 = 0.0; + + Double indicatorValue = + WEIGHT_CONVENTIONAL_COMMITS * metric1 + + WEIGHT_COMMITS_WITH_DESCRIPTION * metric2 + + WEIGHT_ISSUES_WITH_LABELS * metric3 + + WEIGHT_GIT_FLOW_BRANCHES * metric4 + + WEIGHT_CONVENTIONAL_PULL_REQUESTS * metric5; + + try { + // Create the indicator + indicatorReport = new ReportItem.ReportItemBuilder(ID, indicatorValue) + .metrics(Arrays.asList( + conventionalCommits.get(), + commitsWithDescription.get(), + issuesWithLabels.get(), + gitFlowBranches.get(), + conventionalPullRequests.get()) + ) + .indicator(IndicatorState.UNDEFINED).build(); + } catch (ReportItemException e) { + log.info("Error en ReportItemBuilder."); + e.printStackTrace(); + } + + } else { + log.info("No se han proporcionado las métricas necesarias"); + throw new NotAvailableMetricException(REQUIRED_METRICS.toString()); + } + + // Temporarily throw + return indicatorReport; + } + + // Return the metrics required to calculate the indicator output + @Override + public List requiredMetrics() { + return REQUIRED_METRICS; + } + +} + diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index 6b4b66a8..6cc366a4 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -14,6 +14,8 @@ import java.util.logging.Logger; import java.util.stream.Collectors; +import org.kohsuke.github.GHBranch; +import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHIssue; import org.kohsuke.github.GHIssueState; import org.kohsuke.github.GHPullRequest; @@ -24,6 +26,7 @@ import org.kohsuke.github.GitHub; import org.kohsuke.github.PagedIterable; +import us.muit.fs.a4i.config.GitFlow; import us.muit.fs.a4i.exceptions.MetricException; import us.muit.fs.a4i.exceptions.ReportItemException; import us.muit.fs.a4i.model.entities.Report; @@ -80,6 +83,13 @@ public GitHubRepositoryEnquirer() { metricNames.add("PRAcceptedLastMonth"); metricNames.add("PRRejectedLastYear"); metricNames.add("PRRejectedLastMonth"); + // Equipo 1 + metricNames.add("conventionalCommits"); + metricNames.add("commitsWithDescription"); + metricNames.add("issuesWithLabels"); + metricNames.add("gitFlowBranches"); + metricNames.add("conventionalPullRequests"); + log.info("A�adidas m�tricas al GHRepositoryEnquirer"); } @@ -292,6 +302,23 @@ private ReportItem getMetric(String metricName, GHRepository remoteRepo) throws case "PRRejectedLastMonth": metric = getPRRejectedLastMonth(remoteRepo); break; + // equipo 1 + // Begin: RepositoryIndicatorStrategy metrics + case "conventionalCommits": + metric = getConventionalCommits(remoteRepo); + break; + case "commitsWithDescription": + metric = getCommitsWithDescription(remoteRepo); + break; + case "issuesWithLabels": + metric = getIssuesWithLabels(remoteRepo); + break; + case "gitFlowBranches": + metric = getGitFlowBranches(remoteRepo); + break; + case "conventionalPullRequests": + metric = getConventionalPullRequests(remoteRepo); + break; default: throw new MetricException("La métrica " + metricName + " no está definida para un repositorio"); } @@ -1028,4 +1055,241 @@ private ReportItem getPRRejectedLastMonth(GHRepository remoteRepo) throws Metric "Error al obtener las solicitudes de extracción rechazadas en el último mes\n" + e); } } + + // Metricas equipo 1 curso 23/24 + /** + *

+ * Obtiene el ratio de commits convencionales en el último mes + *

+ * + * @param remoteRepo Repositorio remoto + * @return La métrica con el ratio de commits convencionales + * @throws MetricException Si se produce un error al consultar los commits o al + * crear la métrica + */ + private ReportItem getConventionalCommits(GHRepository remoteRepo) throws MetricException { + // Attributes + ReportItem metric = null; + List commits; + + // Logic + // Query the commits in the last month to check if they are conventional + try { + commits = remoteRepo.queryCommits().since(new Date(System.currentTimeMillis() - 30 * 24 * 60 * 60 * 1000)) + .list().toList(); + + // Calculate the ratio of conventional commits + Double conventionalRatio; + if (commits.size() == 0) { + conventionalRatio = 0.0; + } else { + int conventionalCommits = 0; + for (GHCommit commit : commits) { + if (commit.getCommitShortInfo().getMessage().matches( + "^(revert: )?(feat|fix|docs|style|refactor|perf|test|chore)(\\(.+\\))?: .{1,50}")) { + conventionalCommits++; + } + } + conventionalRatio = (double) conventionalCommits / commits.size(); + } + + // Create the metric + ReportItemBuilder conventionalCommitsMetric = new ReportItem.ReportItemBuilder( + "conventionalCommits", conventionalRatio); + conventionalCommitsMetric.source("GitHub, calculada") + .description("Número de commits convencionales en el último mes"); + metric = conventionalCommitsMetric.build(); + } catch (IOException e) { + throw new MetricException("Error al consultar los commits del repositorio"); + } catch (ReportItemException e) { + throw new MetricException("Error al crear la métrica"); + } + return metric; + } + + /** + *

+ * Obtiene el ratio de commits con descripción en el último mes + *

+ * + * @param remoteRepo Repositorio remoto + * @return La métrica con el ratio de commits con descripción + * @throws MetricException Si se produce un error al consultar los commits o al + * crear la métrica + */ + private ReportItem getCommitsWithDescription(GHRepository remoteRepo) throws MetricException { + // Attributes + ReportItem metric = null; + List commits; + + // Logic + // Query the commits in the last month to check if they have a description + try { + commits = remoteRepo.queryCommits().since(new Date(System.currentTimeMillis() - 30 * 24 * 60 * 60 * 1000)) + .list().toList(); + + // Calculate the ratio of commits with description + Double commitsWithDescriptionRatio; + if (commits.size() == 0) { + commitsWithDescriptionRatio = 0.0; + } else { + int commitsWithDescription = 0; + for (GHCommit commit : commits) { + if (commit.getCommitShortInfo().getMessage().matches(".*\\n\\n.*")) { + commitsWithDescription++; + } + } + commitsWithDescriptionRatio = (double) commitsWithDescription / commits.size(); + } + + // Create the metric + ReportItemBuilder commitsWithDescriptionMetric = new ReportItem.ReportItemBuilder( + "commitsWithDescription", commitsWithDescriptionRatio); + commitsWithDescriptionMetric.source("GitHub, calculada") + .description("Número de commits con descripción en el último mes"); + metric = commitsWithDescriptionMetric.build(); + } catch (IOException e) { + throw new MetricException("Error al consultar los commits del repositorio"); + } catch (ReportItemException e) { + throw new MetricException("Error al crear la métrica"); + } + return metric; + } + + /** + *

+ * Obtiene el ratio de issues con etiquetas en el repositorio + *

+ * + * @param remoteRepo Repositorio remoto + * @return La métrica con el ratio de issues con etiquetas + * @throws MetricException Si se produce un error al consultar los issues o al + * crear la métrica + */ + private ReportItem getIssuesWithLabels(GHRepository remoteRepo) throws MetricException { + // Attributes + ReportItem metric = null; + List issues; + + // Logic + // Query the open issues to check if they have labels + try { + issues = remoteRepo.getIssues(GHIssueState.OPEN); + + // Calculate the ratio of issues with labels + // By default, the ratio is 1.0 (100%) if there are no issues + Double issuesWithLabelsRatio = 1.0; + + if (issues.size() > 0) { + int issuesWithLabels = issues.stream().filter(issue -> issue.getLabels().size() > 0).toList().size(); + issuesWithLabelsRatio = (double) issuesWithLabels / issues.size(); + } + + ReportItemBuilder issuesWithLabelsMetric = new ReportItem.ReportItemBuilder( + "issuesWithLabels", issuesWithLabelsRatio); + issuesWithLabelsMetric.source("GitHub, calculada") + .description("Número de issues con etiquetas en el repositorio"); + metric = issuesWithLabelsMetric.build(); + } catch (IOException e) { + throw new MetricException("Error al consultar los issues del repositorio"); + } catch (ReportItemException e) { + throw new MetricException("Error al crear la métrica"); + } + return metric; + } + + /** + *

+ * Obtiene el ratio de ramas que siguen las convenciones de Git Flow en el + * repositorio + *

+ * + * @param remoteRepo Repositorio remoto + * @return La métrica con el ratio de ramas que siguen las convenciones de Git + * Flow + * @throws MetricException Si se produce un error al consultar las ramas o al + * crear la métrica + */ + private ReportItem getGitFlowBranches(GHRepository remoteRepo) throws MetricException { + // Attributes + ReportItem metric = null; + List branches; + + // Logic + // Query the branches to check if they follow the Git Flow naming conventions + try { + branches = remoteRepo.getBranches().values().stream().toList(); + + // Calculate the ratio of Git Flow branches + Double gitFlowBranchesRatio; + if (branches.size() == 0) { + gitFlowBranchesRatio = 0.0; + } else { + gitFlowBranchesRatio = (double) branches.stream() + .filter(branch -> GitFlow.isGitFlowBranch(branch.getName())).toList().size() / branches.size(); + } + + // Create the metric + ReportItemBuilder gitFlowBranchesMetric = new ReportItem.ReportItemBuilder( + "gitFlowBranches", gitFlowBranchesRatio); + gitFlowBranchesMetric.source("GitHub, calculada") + .description("Número de ramas que siguen las convenciones de Git Flow en el repositorio"); + metric = gitFlowBranchesMetric.build(); + } catch (IOException e) { + throw new MetricException("Error al consultar las ramas del repositorio"); + } catch (ReportItemException e) { + throw new MetricException("Error al crear la métrica"); + } + return metric; + } + + /** + *

+ * Obtiene el ratio de pull requests convencionales en el último mes + *

+ * + * @param remoteRepo Repositorio remoto + * @return La métrica con el ratio de pull requests convencionales + * @throws MetricException Si se produce un error al consultar los pull requests + * o al crear la métrica + */ + private ReportItem getConventionalPullRequests(GHRepository remoteRepo) throws MetricException { + ReportItem metric = null; + List pullRequests; + + // Logic + // Query the pull requests in the last month to check if they are conventional + try { + pullRequests = remoteRepo.queryPullRequests().state(GHIssueState.OPEN).list().toList(); + + // Calculate the ratio of conventional pull requests + Double conventionalPullRequestsRatio; + if (pullRequests.size() == 0) { + conventionalPullRequestsRatio = 0.0; + } else { + int conventionalPullRequests = 0; + for (GHPullRequest pullRequest : pullRequests) { + if (pullRequest.getTitle().matches( + "^(revert: )?(feat|fix|docs|style|refactor|perf|test|chore)(\\(.+\\))?: .{1,50}")) { + conventionalPullRequests++; + } + } + conventionalPullRequestsRatio = (double) conventionalPullRequests / pullRequests.size(); + } + + // Create the metric + ReportItemBuilder conventionalPullRequestsMetric = new ReportItem.ReportItemBuilder( + "conventionalPullRequests", conventionalPullRequestsRatio); + conventionalPullRequestsMetric.source("GitHub, calculada") + .description("Número de pull requests convencionales en el último mes"); + metric = conventionalPullRequestsMetric.build(); + } catch (IOException e) { + throw new MetricException("Error al consultar los pull requests del repositorio"); + } catch (ReportItemException e) { + throw new MetricException("Error al crear la métrica"); + } + + return metric; + } + } diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index 8ce78c2f..7e1dff1b 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -60,7 +60,6 @@ "description": "Eliminaciones desde que se inició el repositorio", "unit": "deletions" }, - { "name": "stars", "type": "java.lang.Integer", @@ -121,7 +120,7 @@ "description": "Número de repositorios con pull requests pendientes", "unit": "repositories" }, - { + { "name": "closedIssues", "type": "java.lang.Integer", "description": "Numero de issues cerrados", @@ -198,8 +197,37 @@ "type": "java.lang.Integer", "description": "Número de pull requests rechazadas el pasado año", "unit": "PR" - } - + }, + { + "name": "conventionalCommits", + "type": "java.lang.Double", + "description": "Ratio de commits convencionales", + "unit": "ratio" + }, + { + "name": "commitsWithDescription", + "type": "java.lang.Double", + "description": "Ratio de commits con descripción", + "unit": "ratio" + }, + { + "name": "issuesWithLabels", + "type": "java.lang.Double", + "description": "Ratio de issues con más de una etiqueta", + "unit": "ratio" + }, + { + "name": "gitFlowBranches", + "type": "java.lang.Double", + "description": "Ratio de ramas que siguen gitflow", + "unit": "ratio" + }, + { + "name": "conventionalPullRequests", + "type": "java.lang.Double", + "description": "Ratio de PRs convencionales", + "unit": "ratio" + } ], "indicators": [ { @@ -257,6 +285,12 @@ "warning": 50, "critical": 75 } + }, + { + "name": "conventionsCompliant", + "type": "java.lang.Double", + "description": "Indicador de conformidad con las convenciones en el repo", + "unit": "ratio" } ] } \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/ConventionsCompliantStrategyTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/ConventionsCompliantStrategyTest.java new file mode 100644 index 00000000..db351d02 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/ConventionsCompliantStrategyTest.java @@ -0,0 +1,177 @@ +package us.muit.fs.a4i.test.control.strategies; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.junit.jupiter.api.Assertions; + +import us.muit.fs.a4i.control.strategies.ConventionsCompliantStrategy; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.model.entities.ReportItemI; + +public class ConventionsCompliantStrategyTest { + // MARK: - Attributes + // The indicator strategy to be tested + static ConventionsCompliantStrategy repositoryIndicatorStrategy; + // The expected calculation of the indicator + static Double calculation; + + // MARK: - Setup methods + /** + * @throws java.lang.Exception + */ + @SuppressWarnings("unchecked") + @BeforeAll + static void setUpBeforeClass() throws Exception { + // Create the indicator strategy, here we use the default weights + repositoryIndicatorStrategy = new ConventionsCompliantStrategy(); + + // Based on the provided metrics, the indicator should be calculated as follows: + calculation = (0.2 * 0.86) + (0.2 * 0) + (0.2 * 0.92) + (0.2 * 0) + (0.2 * 0.98); + } + + /** + * @throws java.lang.Exception + */ + @AfterAll + static void tearDownAfterClass() throws Exception { + } + + /** + * @throws java.lang.Exception + */ + @BeforeEach + void setUp() throws Exception { + } + + /** + * @throws java.lang.Exception + */ + @AfterEach + void tearDown() throws Exception { + } + + // MARK: - Test methods + /** + * The indicator is calculated based on the following metrics: + * Metric 1: Ratio of commits complying with summary naming conventions (conventional commits). Must be above 85%. + + * Metric 2: Ratio of commits that have both a summary and a description. Must be above 85%. + * Metric 3: Ratio of issues that have at least one label, compared to the total amount of issues. Must be above 85%. + * Metric 4: The ratio of all branches that follow the correct naming conventions. Must be a 100% compliance. + * Metric 5: Ratio of pull requests following correct naming and description conventions. Must be above 85%. + * + * Where RepoIndicator = w1 * C1 + w2 * C2 + w3 * C3 + w4 * C4 + w5 * C5, and: + * C_i = M_i if M_i >= 0.85 else 0 when i in {1, 2, 3, 5} + * C_4 = 1 if M_4 == 1 else 0 + * @throws NotAvailableMetricException + */ + @Test + public void testCalcIndicator() throws NotAvailableMetricException { + // Mock the required metrics + ReportItemI mockConventionalCommits = Mockito.mock(ReportItemI.class); + ReportItemI mockCommitsWithDescription = Mockito.mock(ReportItemI.class); + ReportItemI mockIssuesWithLabels = Mockito.mock(ReportItemI.class); + ReportItemI mockGitFlowBranches = Mockito.mock(ReportItemI.class); + ReportItemI mockConventionalPullRequests = Mockito.mock(ReportItemI.class); + + // Mock the metric values + when(mockConventionalCommits.getValue()).thenReturn(0.86); + when(mockCommitsWithDescription.getValue()).thenReturn(0.84); + when(mockIssuesWithLabels.getValue()).thenReturn(0.92); + when(mockGitFlowBranches.getValue()).thenReturn(0.85); + when(mockConventionalPullRequests.getValue()).thenReturn(0.98); + // Mock the metric names + when(mockConventionalCommits.getName()).thenReturn("conventionalCommits"); + when(mockCommitsWithDescription.getName()).thenReturn("commitsWithDescription"); + when(mockIssuesWithLabels.getName()).thenReturn("issuesWithLabels"); + when(mockGitFlowBranches.getName()).thenReturn("gitFlowBranches"); + when(mockConventionalPullRequests.getName()).thenReturn("conventionalPullRequests"); + // Set up the list of metrics + List> metrics = Arrays.asList( + mockConventionalCommits, + mockCommitsWithDescription, + mockIssuesWithLabels, + mockGitFlowBranches, + mockConventionalPullRequests + ); + + // Calculate the indicator output + assertDoesNotThrow(() -> { + repositoryIndicatorStrategy.calcIndicator(metrics); + }, "The calculation threw an exception, which it should not."); + + ReportItemI result = repositoryIndicatorStrategy.calcIndicator(metrics); + + assertNotNull(result, "The indicator result is null."); + + // Check the result + Assertions.assertEquals(ConventionsCompliantStrategy.ID, + result.getName(), + "The indicator name is not correct, expected: " + ConventionsCompliantStrategy.ID + ", but was: " + result.getName()); + + Assertions.assertEquals(calculation, result.getValue(), "The indicator value from the calculation is not correct."); + } + + /** + * Test the calculation of the indicator when the required metrics are not available + */ + @Test + public void testCalcIndicatorThrowsNotAvailableMetricException() { + // Mock the required metrics + ReportItemI mockConventionalCommits = Mockito.mock(ReportItemI.class); + ReportItemI mockCommitsWithDescription = Mockito.mock(ReportItemI.class); + ReportItemI mockIssuesWithLabels = Mockito.mock(ReportItemI.class); + ReportItemI mockConventionalPullRequests = Mockito.mock(ReportItemI.class); + + // Mock the metric values + when(mockConventionalCommits.getValue()).thenReturn(0.86); + when(mockCommitsWithDescription.getValue()).thenReturn(0.84); + when(mockIssuesWithLabels.getValue()).thenReturn(0.92); + when(mockConventionalPullRequests.getValue()).thenReturn(0.98); + // Mock the metric names + when(mockConventionalCommits.getName()).thenReturn("conventionalCommits"); + when(mockCommitsWithDescription.getName()).thenReturn("commitsWithDescription"); + when(mockIssuesWithLabels.getName()).thenReturn("issuesWithLabels"); + when(mockConventionalPullRequests.getName()).thenReturn("conventionalPullRequests"); + // Set up the list of metrics + List> metrics = Arrays.asList( + mockConventionalCommits, + mockCommitsWithDescription, + mockIssuesWithLabels, + mockConventionalPullRequests + ); + + // Check that the method throws the expected exception + Assertions.assertThrows(NotAvailableMetricException.class, () -> { + repositoryIndicatorStrategy.calcIndicator(metrics); + }); + } + + /** + * Test the required metrics for the indicator calculation + */ + @Test + public void testRequiredMetrics() { + List expectedRequirements = Arrays.asList("conventionalCommits", "commitsWithDescription", "issuesWithLabels", "gitFlowBranches", "conventionalPullRequests"); + + for (String requirement : expectedRequirements) { + Assertions.assertTrue(repositoryIndicatorStrategy.requiredMetrics().contains(requirement), + "The required metrics for RepositoryIndicatorStrategyTest did not contain the following expected metric: " + requirement); + } + } +} diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java index 4811d81b..1d66f69e 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java @@ -8,11 +8,13 @@ import java.util.List; import java.util.logging.Logger; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import us.muit.fs.a4i.exceptions.MetricException; 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.model.remote.GitHubRepositoryEnquirer; /** @@ -131,4 +133,82 @@ void testCompletedPullRequests() throws MetricException { assertTrue(0 < metrica.getValue(), "Bad number of closed PR"); } + // Test del equipo 1 del curso 23/24 + // MARK: Tests + @Test + @DisplayName("Test getConventionalCommits") + @SuppressWarnings("rawtypes") + void testGetConventionalCommits() { + ReportItemI reportItem = testGetMetric("conventionalCommits"); + assertNotNull(reportItem, "Getting conventional commits failed: reportItem is null"); + assertNotNull(reportItem.getValue(), "Getting conventional commits failed: value is null"); + // Check that the value is a number between 0 and 1 + double value = (double) reportItem.getValue(); + assertTrue(value >= 0 && value <= 1, "Getting conventional commits failed: value is not between 0 and 1"); + } + + @Test + @DisplayName("Test issuesWithLabels") + @SuppressWarnings("rawtypes") + void testGetIssuesWithLabels() { + ReportItemI reportItem = testGetMetric("issuesWithLabels"); + assertNotNull(reportItem, "Getting issues with labels failed: reportItem is null"); + assertNotNull(reportItem.getValue(), "Getting issues with labels failed: value is null"); + // Check that the value is a number between 0 and 1 + double value = (double) reportItem.getValue(); + assertTrue(value >= 0 && value <= 1, "Getting issues with labels failed: value is not between 0 and 1"); + } + + @Test + @DisplayName("Test gitFlowBranches") + @SuppressWarnings("rawtypes") + void testGetGitFlowBranches() { + ReportItemI reportItem = testGetMetric("gitFlowBranches"); + assertNotNull(reportItem, "Getting git flow branches failed: reportItem is null"); + assertNotNull(reportItem.getValue(), "Getting git flow branches failed: value is null"); + // Check that the value is a number between 0 and 1 + double value = (double) reportItem.getValue(); + assertTrue(value >= 0 && value <= 1, "Getting git flow branches failed: value is not between 0 and 1"); + } + + @Test + @DisplayName("Test commitsWithDescription") + @SuppressWarnings("rawtypes") + void testGetCommits() { + ReportItemI reportItem = testGetMetric("commitsWithDescription"); + assertNotNull(reportItem, "Getting commits with description failed: reportItem is null"); + assertNotNull(reportItem.getValue(), "Getting commits with description failed: value is null"); + // Check that the value is a number between 0 and 1 + double value = (double) reportItem.getValue(); + assertTrue(value >= 0 && value <= 1, "Getting commits with description failed: value is not between 0 and 1"); + } + + @Test + @DisplayName("Test conventionalPullRequests") + @SuppressWarnings("rawtypes") + void testGetRepository() { + ReportItemI reportItem = testGetMetric("commitsWithDescription"); + assertNotNull(reportItem, "Getting repository failed: reportItem is null"); + assertNotNull(reportItem.getValue(), "Getting repository failed: value is null"); + // Check that the value is a number between 0 and 1 + double value = (double) reportItem.getValue(); + assertTrue(value >= 0 && value <= 1, "Getting repository failed: value is not between 0 and 1"); + } + + // MARK: Test helper methods + @SuppressWarnings("rawtypes") + ReportItemI testGetMetric(String metricString) { + log.info("Consultando metrica " + metricString); + ReportItemI reportItem; + try { + reportItem = ghEnquirer.getMetric(metricString, "MIT-FS/Audit4Improve-API"); + } catch (MetricException e) { + e.printStackTrace(); + fail("Exception thrown while getting conventional commits: " + e.getMessage()); + return null; + } + + assertNotNull(reportItem, "Getting the metric (" + metricString + ") failed: reportItem is null"); + return reportItem; + } } From 1ee4fc2b333c9d3fba3da70e520b3af2ca9a35bb Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Sun, 23 Feb 2025 20:10:45 +0100 Subject: [PATCH 081/100] Incluyendo indicador teamsBalanceI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tiene algunos problemas, el nombre del indicador coincide con el de una métrica Había una inconsistencia en la declaración del tipo en A4iconf y el uso en el código (Integer) --- .../strategies/TeamsBalanceStrategy.java | 77 ++++++++++++++++ .../remote/GitHubOrganizationEnquirer.java | 77 +++++++++++++++- src/main/resources/a4iDefault.json | 15 +++- .../strategies/TeamsBalanceStrategyTest.java | 90 +++++++++++++++++++ .../GitHubOrganizationEnquirerTest.java | 14 +++ 5 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 src/main/java/us/muit/fs/a4i/control/strategies/TeamsBalanceStrategy.java create mode 100644 src/test/java/us/muit/fs/a4i/test/control/strategies/TeamsBalanceStrategyTest.java diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/TeamsBalanceStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/TeamsBalanceStrategy.java new file mode 100644 index 00000000..68e3345d --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/control/strategies/TeamsBalanceStrategy.java @@ -0,0 +1,77 @@ +/** + * + */ +package us.muit.fs.a4i.control.strategies; + +import us.muit.fs.a4i.control.IndicatorStrategy; +/** + * + */ +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; + +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; + +import java.util.logging.Logger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.lang.Math; +import java.lang.Double; + +public class TeamsBalanceStrategy implements IndicatorStrategy { + + private static Logger log = Logger.getLogger(Indicator.class.getName()); + + // M�tricas necesarias para calcular el indicador + private static final List REQUIRED_METRICS = Arrays.asList("teamsBalance","repositories"); + + @Override + public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { + // Se obtienen y se comprueba que se pasan las m�tricas necesarias para calcular + // el indicador. + Optional> teamsBalance = metrics.stream().filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); + Optional> repositories = metrics.stream().filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); + ReportItemI indicatorReport = null; + log.info("Teamsbalance: "+teamsBalance.toString()); + log.info("Repositories: "+repositories.toString()); + if (teamsBalance.isPresent() && repositories.isPresent()) { + Double teamsBalanceResult; + + // Se realiza el c�lculo del indicador + teamsBalanceResult = (Double)teamsBalance.get().getValue()*100/repositories.get().getValue(); + + log.info("teamsBalanceResult: "+teamsBalanceResult.toString()); + try { + // Se crea el indicador + //No se entiende por qué el indicador tiene el mismo nombere que una de las métricas + indicatorReport = new ReportItem.ReportItemBuilder("teamsBalanceI", teamsBalanceResult). + metrics(Arrays.asList(teamsBalance.get(), repositories.get())) + .indicator(IndicatorState.UNDEFINED).build(); + + log.info("IndicatorReport: "+indicatorReport.toString()); + } catch (ReportItemException e) { + log.info("Error en ReportItemBuilder."); + e.printStackTrace(); + } + + } else { + log.info("No se han proporcionado las métricas necesarias"); + throw new NotAvailableMetricException(REQUIRED_METRICS.toString()); + } + + return indicatorReport; + } + + @Override + public List requiredMetrics() { + // Para calcular el indicador "IssuesRatio", ser�n necesarias las m�tricas + // "openIssues" y "closedIssues". + return REQUIRED_METRICS; + } +} + diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java index cd594b94..32b50c2a 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java @@ -4,11 +4,13 @@ package us.muit.fs.a4i.model.remote; import java.io.IOException; - +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; - +import java.util.Map; import java.util.logging.Logger; +import org.kohsuke.github.GHIssueState; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GitHub; @@ -59,6 +61,8 @@ public GitHubOrganizationEnquirer() { metricNames.add("openProjects"); metricNames.add("closedProjects"); metricNames.add("followers"); + //Equipo 7 23/24 + metricNames.add("teamsBalance"); log.info("Incluidos nombres metricas en Enquirer"); } @@ -192,6 +196,9 @@ private ReportItem getMetric(String metricName, GHOrganization organization) thr case "followers": metric = getFollowers(organization); break; + case "teamsBalance": + metric=getTeamsBalance(organization); + break; default: throw new MetricException("La métrica " + metricName + " no está definida para un repositorio"); } @@ -343,5 +350,71 @@ private ReportItem getClosedProjects(GHOrganization organization) { return builder.build(); } + private Map getIssuesPerRepository(GHOrganization organization) { + log.info("Consultando los issues de cada uno de los repositorios de la organización"); + Map mapa = new HashMap<>(); + try { + + PagedIterable repositorios = organization.listRepositories(); + for (GHRepository repo : repositorios) { + mapa.put(repo,repo.getIssues(GHIssueState.OPEN).size()); + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return mapa; + } + + private Map getTeamsPerRepository(GHOrganization organization) { + log.info("Consultando el númerdo de equipos por repositorio"); + ReportItemBuilder> builder=null; + Map mapa = new HashMap<>(); + try { + PagedIterable repositorios = organization.listRepositories(); + for (GHRepository repo : repositorios) { + mapa.put(repo,repo.getTeams().size()); + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return mapa; + } + + private ReportItem getTeamsBalance(GHOrganization organization) { + + log.info("Consultando el equilibrio entre equipos e issues de los repositorios"); + Integer issuesTotales= this.getIssuesPerRepository(organization).size(); + Integer desarrolladoresTotales=(Integer) this.getTeams(organization).getValue(); + Integer numProjects = (Integer) this.getRepositories(organization).getValue(); + Map issuesPerProject =this.getIssuesPerRepository(organization); + Map teamsPerProject = this.getTeamsPerRepository(organization); + + Double desajustePromedioOrganizacion=0.0; + List desajustePromedioProyecto = new ArrayList(); + + //Es raro que sea Integer y en la declaración de a4iDefault esta(ba) como Double + ReportItemBuilder builder=null; + try { + //Calcular el desajuste promedio de cada proyecto y guardarlo en desajustePromedioProyecto + issuesPerProject.forEach((repo,numIssues)-> + desajustePromedioProyecto.add((double) (Math.abs(numIssues/(issuesTotales)-(teamsPerProject.get(repo)/desarrolladoresTotales)))) + ); + + //Sumar todos los desajustes y guardarlos en desajustePromedioOrganizacion + for (Double desajuste: desajustePromedioProyecto){ + desajustePromedioOrganizacion+=desajuste; + } + + builder = new ReportItem.ReportItemBuilder("teamsBalance", (int) Math.round(desajustePromedioOrganizacion)); + builder.source("GitHub"); + } catch (ReportItemException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return builder.build(); + } + } diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index 7e1dff1b..f7c86ba6 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -227,7 +227,13 @@ "type": "java.lang.Double", "description": "Ratio de PRs convencionales", "unit": "ratio" - } + }, + { + "name": "teamsBalance", + "type": "java.lang.Integer", + "description": "Balance de equipos y open issues", + "unit": "ratio" + } ], "indicators": [ { @@ -291,6 +297,13 @@ "type": "java.lang.Double", "description": "Indicador de conformidad con las convenciones en el repo", "unit": "ratio" + }, + { + "name": "teamsBalanceI", + "type": "java.lang.Double", + "description": "Balance de equipos y open issues", + "unit": "ratio", + "limits": { "ok": 0.2, "warning": 0.6, "critical": 0.8 } } ] } \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/TeamsBalanceStrategyTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/TeamsBalanceStrategyTest.java new file mode 100644 index 00000000..a3e7e3ad --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/TeamsBalanceStrategyTest.java @@ -0,0 +1,90 @@ +package us.muit.fs.a4i.test.control.strategies; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.mockito.Mockito.*; +import org.mockito.Mockito; + +import us.muit.fs.a4i.control.strategies.TeamsBalanceStrategy; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; + +import us.muit.fs.a4i.model.entities.ReportItemI; + +public class TeamsBalanceStrategyTest { + private static Logger log = Logger.getLogger(TeamsBalanceStrategyTest.class.getName()); + + /** + * @throws java.lang.Exception + */ + @BeforeAll + static void setUpBeforeClass() throws Exception { + } + + /** + * @throws java.lang.Exception + */ + @AfterAll + static void tearDownAfterClass() throws Exception { + } + + /** + * @throws java.lang.Exception + */ + @BeforeEach + void setUp() throws Exception { + } + + /** + * @throws java.lang.Exception + */ + @AfterEach + void tearDown() throws Exception { + } + + + @Test + public void testTeamsBalance() throws NotAvailableMetricException { + // Creamos los mocks necesarios + ReportItemI mockTeamBalance = Mockito.mock(ReportItemI.class); + ReportItemI mockRepositories = Mockito.mock(ReportItemI.class); + + // Configuramos los mocks para devolver valores predefinidos + Mockito.when(mockTeamBalance.getName()).thenReturn("teamsBalance"); + Mockito.when(mockTeamBalance.getValue()).thenReturn(1.0); + + Mockito.when(mockRepositories.getName()).thenReturn("repositories"); + Mockito.when(mockRepositories.getValue()).thenReturn(5.0); + + // Creamos una instancia de IssuesRatioIndicator + TeamsBalanceStrategy indicator = new TeamsBalanceStrategy(); + + // Ejecutamos el método que queremos probar con los mocks como argumentos + List> metrics = Arrays.asList(mockTeamBalance, mockRepositories); + log.info("MockteamBalance: " + mockTeamBalance); + log.info("MockRepositories: " + mockRepositories); + log.info("Metrics: " + metrics); + ReportItemI result = indicator.calcIndicator(metrics); + + // Comprobamos que el resultado es el esperado + log.info("Result: " + result); + Assertions.assertEquals("teamsBalance", result.getName()); + Assertions.assertEquals(20.0, result.getValue()); + Assertions.assertDoesNotThrow(()->indicator.calcIndicator(metrics)); + } + + + + +} + diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java index 9940ad6a..c4529a31 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubOrganizationEnquirerTest.java @@ -5,6 +5,7 @@ */ import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; @@ -151,5 +152,18 @@ void testGetRepositoriesWithOpenPullRequest() throws MetricException, ReportItem assertTrue(metricsRepositoriesWithOpenPullRequest.getValue().intValue() > 0, "El número de repositorios con pull requests abiertos no es el esperado"); } + /** + * Test method for + * GitHubOrganizationEnquirer + * @throws MetricException + * @throws ReportItemException + */ + @Test + void testTeamsBalance() throws MetricException, ReportItemException { + //"message":"Must have admin rights to Repository." + //Es necesario tener permisos de administrador en la cuenta dueña de la organización + ReportItem teamsBalance = ghEnquirer.getMetric("teamsBalance", "MIT-FS"); + assertNotNull(teamsBalance.getValue(), "No se ha podido obtener el balance equipos-repositorios"); + } } From 7b05f3c935c35e54be7875b338b92cfc4919bbed Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Tue, 25 Feb 2025 17:07:36 +0100 Subject: [PATCH 082/100] actualizando gradle --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ed0b41eb..2733ed5d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-wrapper.jar.sha256 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From d35a5c377903a01d91bd2ac6ae676ace49c38c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Thu, 27 Feb 2025 13:53:05 +0100 Subject: [PATCH 083/100] Limpiando RepositoryEnquirer --- .../fs/a4i/exceptions/MetricException.java | 2 +- .../fs/a4i/model/remote/GitHubEnquirer.java | 8 +- .../remote/GitHubRepositoryEnquirer.java | 99 +++++++++++++------ 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/exceptions/MetricException.java b/src/main/java/us/muit/fs/a4i/exceptions/MetricException.java index 6e384c20..84cef6fe 100644 --- a/src/main/java/us/muit/fs/a4i/exceptions/MetricException.java +++ b/src/main/java/us/muit/fs/a4i/exceptions/MetricException.java @@ -4,7 +4,7 @@ * @author Isabel Román * */ -public class MetricException extends Exception { +public class MetricException extends RuntimeException { /** * Excepción al manejar métrica */ diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java index 44e911a8..cfade777 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java @@ -4,12 +4,18 @@ package us.muit.fs.a4i.model.remote; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.logging.Logger; +import java.util.function.*; +import java.util.Map; +import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; import org.kohsuke.github.GitHubBuilder; +import us.muit.fs.a4i.model.entities.ReportItem; + /** *

* Clase abstracta con los métodos comunes a los constructores que recogen la @@ -29,7 +35,7 @@ public abstract class GitHubEnquirer implements RemoteEnquirer { private static Logger log = Logger.getLogger(GitHubEnquirer.class.getName()); protected List metricNames; - + protected Map> myQueries=new HashMap>(); /** *

* Referencia al objeto GitHub que permite hacer consultas al servidor Github diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index 6cc366a4..fbff03e5 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -41,6 +41,11 @@ * no están acordes al indicador para el que fueron creadas RECUERDA: * las métricas tienen que estar incluidas en el fichero de * configuración a4iDefault.json + * + * TODO: esto está en periodo de limpieza, vamos a utilizar un mapa de punteros a funciones para quitarnos el case de enmedio, y ya que estamos + * podemos eliminar la lista de métricas y usar las keys del mapa (Esto falta) + * Debería haber un constructor para esto, para que la creación del mapa esté fuera + * */ public class GitHubRepositoryEnquirer extends GitHubEnquirer { /** @@ -56,6 +61,22 @@ public class GitHubRepositoryEnquirer extends GitHubEnquirer { public GitHubRepositoryEnquirer() { super(); + myQueries.put("subscribers", GitHubRepositoryEnquirer::getSubscribers); + myQueries.put("forks", GitHubRepositoryEnquirer::getForks); + myQueries.put("watchers", GitHubRepositoryEnquirer::getWatchers); + myQueries.put("starts", GitHubRepositoryEnquirer::getStars); + myQueries.put("issues", GitHubRepositoryEnquirer::getIssues); + myQueries.put("closedIssues", GitHubRepositoryEnquirer::getClosedIssues); + myQueries.put("openIssues", GitHubRepositoryEnquirer::getOpenIssues); + myQueries.put("creation", GitHubRepositoryEnquirer::getCreation); + myQueries.put("lastUpdated", GitHubRepositoryEnquirer::getLastUpdated); + myQueries.put("lastPush", GitHubRepositoryEnquirer::getLastPush); + myQueries.put("totalAdditions", GitHubRepositoryEnquirer::getTotalAdditions); + + myQueries.put("totalDeletions", GitHubRepositoryEnquirer::getTotalDeletions); + myQueries.put("collaborators", GitHubRepositoryEnquirer::getCollaborators); + myQueries.put("ownerCommits", GitHubRepositoryEnquirer::getOwnerCommits); + metricNames.add("subscribers"); metricNames.add("forks"); metricNames.add("watchers"); @@ -67,23 +88,40 @@ public GitHubRepositoryEnquirer() { metricNames.add("lastUpdated"); metricNames.add("lastPush"); metricNames.add("totalAdditions"); + metricNames.add("totalDeletions"); metricNames.add("collaborators"); metricNames.add("ownerCommits"); // equipo3 + myQueries.put("issuesLastMonth", GitHubRepositoryEnquirer::getIssuesLastMonth); + myQueries.put("closedIssuesLastMonth", GitHubRepositoryEnquirer::getClosedIssuesLastMonth); + myQueries.put("meanClosedIssuesLastMonth", GitHubRepositoryEnquirer::getMeanClosedIssuesLastMonth); + myQueries.put("issues4DevLastMonth", GitHubRepositoryEnquirer::getIssues4DevLastMonth); metricNames.add("issuesLastMonth"); metricNames.add("closedIssuesLastMonth"); metricNames.add("meanClosedIssuesLastMonth"); metricNames.add("issues4DevLastMonth"); // equipo 4 + myQueries.put("totalPullReq", GitHubRepositoryEnquirer::getTotalPullReq); + myQueries.put("closedPullReq", GitHubRepositoryEnquirer::getClosedPullReq); metricNames.add("totalPullReq"); metricNames.add("closedPullReq"); // equipo 5 + myQueries.put("PRAcceptedLastYear", GitHubRepositoryEnquirer::getPRAcceptedLastYear); + myQueries.put("PRAcceptedLastMonth", GitHubRepositoryEnquirer::getPRAcceptedLastMonth); + myQueries.put("PRRejectedLastYear", GitHubRepositoryEnquirer::getPRRejectedLastYear); + myQueries.put("PRRejectedLastMonth", GitHubRepositoryEnquirer::getPRRejectedLastMonth); + metricNames.add("PRAcceptedLastYear"); metricNames.add("PRAcceptedLastMonth"); metricNames.add("PRRejectedLastYear"); metricNames.add("PRRejectedLastMonth"); // Equipo 1 + myQueries.put("conventionalCommits", GitHubRepositoryEnquirer::getConventionalCommits); + myQueries.put("commitsWithDescription", GitHubRepositoryEnquirer::getCommitsWithDescription); + myQueries.put("issuesWithLabels", GitHubRepositoryEnquirer::getIssuesWithLabels); + myQueries.put("gitFlowBranches", GitHubRepositoryEnquirer::getGitFlowBranches); + myQueries.put("conventionalPullRequests", GitHubRepositoryEnquirer::getConventionalPullRequests); metricNames.add("conventionalCommits"); metricNames.add("commitsWithDescription"); metricNames.add("issuesWithLabels"); @@ -227,6 +265,8 @@ private ReportItem getMetric(String metricName, GHRepository remoteRepo) throws if (remoteRepo == null) { throw new MetricException("Intenta obtener una métrica sin haber obtenido los datos del repositorio"); } + metric=myQueries.get(metricName).apply(remoteRepo); + /* switch (metricName) { case "totalAdditions": metric = getTotalAdditions(remoteRepo); @@ -322,6 +362,7 @@ private ReportItem getMetric(String metricName, GHRepository remoteRepo) throws default: throw new MetricException("La métrica " + metricName + " no está definida para un repositorio"); } + */ return metric; } @@ -340,7 +381,7 @@ private ReportItem getMetric(String metricName, GHRepository remoteRepo) throws * @return la métrica con el número total de adiciones desde el inicio * @throws MetricException Intenta crear una métrica no definida */ - private ReportItem getTotalAdditions(GHRepository remoteRepo) throws MetricException { + static private ReportItem getTotalAdditions(GHRepository remoteRepo) throws MetricException { ReportItem metric = null; GHRepositoryStatistics data = remoteRepo.getStatistics(); @@ -383,7 +424,7 @@ private ReportItem getTotalAdditions(GHRepository remoteRepo) throws MetricExcep * @return la métrica con el n�mero total de eliminaciones desde el inicio * @throws MetricException Intenta crear una métrica no definida */ - private ReportItem getTotalDeletions(GHRepository remoteRepo) throws MetricException { + static private ReportItem getTotalDeletions(GHRepository remoteRepo) throws MetricException { ReportItem metric = null; GHRepositoryStatistics data = remoteRepo.getStatistics(); @@ -425,7 +466,7 @@ private ReportItem getTotalDeletions(GHRepository remoteRepo) throws MetricExcep * @param repo repositorio que se consulta * @return item para el informe */ - private ReportItem getSubscribers(GHRepository repo) { + static private ReportItem getSubscribers(GHRepository repo) { log.info("Consultando los subscriptores"); ReportItemBuilder builder = null; try { @@ -444,7 +485,7 @@ private ReportItem getSubscribers(GHRepository repo) { * @param repo repositorio que se consulta * @return item para el informe */ - private ReportItem getForks(GHRepository repo) { + static private ReportItem getForks(GHRepository repo) { log.info("Consultando los forks"); ReportItemBuilder builder = null; try { @@ -463,7 +504,7 @@ private ReportItem getForks(GHRepository repo) { * @param repo repositorio que se consulta * @return item para el informe */ - private ReportItem getWatchers(GHRepository repo) { + static private ReportItem getWatchers(GHRepository repo) { log.info("Consultando los watchers"); ReportItemBuilder builder = null; try { @@ -482,7 +523,7 @@ private ReportItem getWatchers(GHRepository repo) { * @param repo repositorio que se consulta * @return item para el informe */ - private ReportItem getStars(GHRepository repo) { + static private ReportItem getStars(GHRepository repo) { log.info("Consultando las starts"); ReportItemBuilder builder = null; try { @@ -501,7 +542,7 @@ private ReportItem getStars(GHRepository repo) { * @param repo repositorio que se consulta * @return item para el informe */ - private ReportItem getOwnerCommits(GHRepository repo) { + static private ReportItem getOwnerCommits(GHRepository repo) { log.info("Consultando los commits del responsable del repositorio"); ReportItemBuilder builder = null; GHRepositoryStatistics data = repo.getStatistics(); @@ -524,7 +565,7 @@ private ReportItem getOwnerCommits(GHRepository repo) { * @param repo repositorio que se consulta * @return item para el informe */ - private ReportItem getIssues(GHRepository repo) { + static private ReportItem getIssues(GHRepository repo) { log.info("Consultando los issues"); ReportItemBuilder builder = null; try { @@ -543,7 +584,7 @@ private ReportItem getIssues(GHRepository repo) { * @param repo repositorio que se consulta * @return item para incluir en el informe */ - private ReportItem getOpenIssues(GHRepository repo) { + static private ReportItem getOpenIssues(GHRepository repo) { log.info("Consultando los issues abiertos"); ReportItemBuilder builder = null; try { @@ -562,7 +603,7 @@ private ReportItem getOpenIssues(GHRepository repo) { * @param repo repositorio que se consulta * @return item para incluir en el informe */ - private ReportItem getClosedIssues(GHRepository repo) { + static private ReportItem getClosedIssues(GHRepository repo) { log.info("Consultando los issues cerrados"); ReportItemBuilder builder = null; try { @@ -582,7 +623,7 @@ private ReportItem getClosedIssues(GHRepository repo) { * @param repo repositorio que se consulta * @return item para el informe */ - private ReportItem getCollaborators(GHRepository repo) { + static private ReportItem getCollaborators(GHRepository repo) { log.info("Consultando los colaboradores"); ReportItemBuilder builder = null; try { @@ -601,7 +642,7 @@ private ReportItem getCollaborators(GHRepository repo) { * @param repo * @return item para el informe */ - private ReportItem getCreation(GHRepository repo) { + static private ReportItem getCreation(GHRepository repo) { log.info("Consultando fecha de creación"); ReportItemBuilder builder = null; try { @@ -621,7 +662,7 @@ private ReportItem getCreation(GHRepository repo) { * @param repo repositorio que se consulta * @return item para el informe */ - private ReportItem getLastPush(GHRepository repo) { + static private ReportItem getLastPush(GHRepository repo) { log.info("Consultando el ultimo push"); ReportItemBuilder builder = null; try { @@ -640,7 +681,7 @@ private ReportItem getLastPush(GHRepository repo) { * @param repo repositorio que se consulta * @return item para incluir en el informe del repositorio */ - private ReportItem getLastUpdated(GHRepository repo) { + static private ReportItem getLastUpdated(GHRepository repo) { log.info("Consultando la ultima actualización"); ReportItemBuilder builder = null; try { @@ -661,7 +702,7 @@ private ReportItem getLastUpdated(GHRepository repo) { * @return ReportItem with the number of issues created last month * @throws MetricException */ - private ReportItem getIssuesLastMonth(GHRepository remoteRepo) throws MetricException { + static private ReportItem getIssuesLastMonth(GHRepository remoteRepo) throws MetricException { log.info("Consultando los issues abiertos un mes"); int issuesLastMonth = 0; ReportItemBuilder builder = null; @@ -707,7 +748,7 @@ private ReportItem getIssuesLastMonth(GHRepository remoteRepo) throws MetricExce * @return ReportItem with the issues closed last month * @throws MetricException */ - private ReportItem getClosedIssuesLastMonth(GHRepository remoteRepo) throws MetricException { + static private ReportItem getClosedIssuesLastMonth(GHRepository remoteRepo) throws MetricException { int closedIssuesLastMonth = 0; ReportItemBuilder builder = null; try { @@ -750,7 +791,7 @@ private ReportItem getClosedIssuesLastMonth(GHRepository remoteRepo) throws Metr * @return ReportItem with the average of closed issues per member in a month * @throws MetricException */ - private ReportItem getMeanClosedIssuesLastMonth(GHRepository remoteRepo) throws MetricException { + static private ReportItem getMeanClosedIssuesLastMonth(GHRepository remoteRepo) throws MetricException { int closedIssuesLastMonth = 0; int activeMembers = 0; Map issuesClosedByMember = new HashMap<>(); @@ -814,7 +855,7 @@ private ReportItem getMeanClosedIssuesLastMonth(GHRepository remoteRepo) throws * reportitem es un mapa y luego lo tratan como si fuera * un double... no es coherente una parte con la otra. */ - private ReportItem issues4DevLastMonth(GHRepository remoteRepo) throws MetricException { + static private ReportItem getIssues4DevLastMonth(GHRepository remoteRepo) throws MetricException { Map issuesAssignedByMember = new HashMap<>(); ReportItemBuilder> builder = null; @@ -856,7 +897,7 @@ private ReportItem issues4DevLastMonth(GHRepository remoteRepo) throws MetricExc return builder.build(); } - private ReportItem getClosedPullReq(GHRepository repo) { + static private ReportItem getClosedPullReq(GHRepository repo) { log.info("Consultando los pull requests completados"); ReportItemBuilder builder = null; @@ -877,7 +918,7 @@ private ReportItem getClosedPullReq(GHRepository repo) { return builder.build(); } - private ReportItem getTotalPullReq(GHRepository repo) { + static private ReportItem getTotalPullReq(GHRepository repo) { log.info("Consultando los pull requests totales"); ReportItemBuilder builder = null; @@ -941,7 +982,7 @@ private static List filterPullRequests(List pullRe * @throws MetricException si ocurre un error al obtener las solicitudes de * extracción */ - private ReportItem getPRAcceptedLastYear(GHRepository remoteRepo) throws MetricException { + static private ReportItem getPRAcceptedLastYear(GHRepository remoteRepo) throws MetricException { LocalDateTime now = LocalDateTime.now(); LocalDateTime oneYearAgo = now.minusYears(1); @@ -972,7 +1013,7 @@ private ReportItem getPRAcceptedLastYear(GHRepository remoteRepo) throws MetricE * @throws MetricException si ocurre un error al obtener las solicitudes de * extracción */ - private ReportItem getPRAcceptedLastMonth(GHRepository remoteRepo) throws MetricException { + static private ReportItem getPRAcceptedLastMonth(GHRepository remoteRepo) throws MetricException { LocalDateTime now = LocalDateTime.now(); LocalDateTime oneMonthAgo = now.minusMonths(1); @@ -1004,7 +1045,7 @@ private ReportItem getPRAcceptedLastMonth(GHRepository remoteRepo) throws Metric * @throws MetricException si ocurre un error al obtener las solicitudes de * extracción */ - private ReportItem getPRRejectedLastYear(GHRepository remoteRepo) throws MetricException { + static private ReportItem getPRRejectedLastYear(GHRepository remoteRepo) throws MetricException { LocalDateTime now = LocalDateTime.now(); LocalDateTime oneYearAgo = now.minusYears(1); @@ -1036,7 +1077,7 @@ private ReportItem getPRRejectedLastYear(GHRepository remoteRepo) throws MetricE * @throws MetricException si ocurre un error al obtener las solicitudes de * extracción */ - private ReportItem getPRRejectedLastMonth(GHRepository remoteRepo) throws MetricException { + static private ReportItem getPRRejectedLastMonth(GHRepository remoteRepo) throws MetricException { LocalDateTime now = LocalDateTime.now(); LocalDateTime oneMonthAgo = now.minusMonths(1); @@ -1067,7 +1108,7 @@ private ReportItem getPRRejectedLastMonth(GHRepository remoteRepo) throws Metric * @throws MetricException Si se produce un error al consultar los commits o al * crear la métrica */ - private ReportItem getConventionalCommits(GHRepository remoteRepo) throws MetricException { + static private ReportItem getConventionalCommits(GHRepository remoteRepo) throws MetricException { // Attributes ReportItem metric = null; List commits; @@ -1117,7 +1158,7 @@ private ReportItem getConventionalCommits(GHRepository remoteRepo) throw * @throws MetricException Si se produce un error al consultar los commits o al * crear la métrica */ - private ReportItem getCommitsWithDescription(GHRepository remoteRepo) throws MetricException { + static private ReportItem getCommitsWithDescription(GHRepository remoteRepo) throws MetricException { // Attributes ReportItem metric = null; List commits; @@ -1166,7 +1207,7 @@ private ReportItem getCommitsWithDescription(GHRepository remoteRepo) th * @throws MetricException Si se produce un error al consultar los issues o al * crear la métrica */ - private ReportItem getIssuesWithLabels(GHRepository remoteRepo) throws MetricException { + static private ReportItem getIssuesWithLabels(GHRepository remoteRepo) throws MetricException { // Attributes ReportItem metric = null; List issues; @@ -1210,7 +1251,7 @@ private ReportItem getIssuesWithLabels(GHRepository remoteRepo) throws M * @throws MetricException Si se produce un error al consultar las ramas o al * crear la métrica */ - private ReportItem getGitFlowBranches(GHRepository remoteRepo) throws MetricException { + static private ReportItem getGitFlowBranches(GHRepository remoteRepo) throws MetricException { // Attributes ReportItem metric = null; List branches; @@ -1253,7 +1294,7 @@ private ReportItem getGitFlowBranches(GHRepository remoteRepo) throws Me * @throws MetricException Si se produce un error al consultar los pull requests * o al crear la métrica */ - private ReportItem getConventionalPullRequests(GHRepository remoteRepo) throws MetricException { + static private ReportItem getConventionalPullRequests(GHRepository remoteRepo) throws MetricException { ReportItem metric = null; List pullRequests; From ea29f8b25294c4facd0f43bbf8f6c3436c626cd7 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Fri, 28 Feb 2025 11:03:56 +0100 Subject: [PATCH 084/100] Aplicando criterio mapa de punteros al resto de los equirers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Los test del enquirer de organización y de desarrollador no están revisados --- .../model/remote/GitHubDeveloperEnquirer.java | 14 +++-- .../fs/a4i/model/remote/GitHubEnquirer.java | 18 +++--- .../remote/GitHubOrganizationEnquirer.java | 56 ++++++++++--------- .../remote/GitHubRepositoryEnquirer.java | 28 +++------- 4 files changed, 56 insertions(+), 60 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java index e45aeada..12428320 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java @@ -28,11 +28,11 @@ * quería RECUERDA: las métricas tienen que estar incluidas en el fichero de * configuración a4iDefault.json */ -public class GitHubDeveloperEnquirer extends GitHubEnquirer { +public class GitHubDeveloperEnquirer extends GitHubEnquirer { public GitHubDeveloperEnquirer() { super(); - metricNames.add("closedIssuesLastMonth"); - metricNames.add("assignedIssuesLastMonth"); + myQueries.put("closedIssuesLastMonth",GitHubDeveloperEnquirer::getClosedIssuesLastMonth); + myQueries.put("assignedIssuesLastMonth",GitHubDeveloperEnquirer::getAssignedIssuesLastMonth); log.info("Incluidos nombres metricas en Enquirer"); } @@ -74,6 +74,7 @@ private ReportItem getMetric(String metricName, GHUser developer) throws MetricE throw new MetricException( "Intenta obtener una métrica de desarrollador sin haber obtenido el desarrollador"); } + /* switch (metricName) { case "closedIssuesLastMonth": metric = getClosedIssuesLastMonth(developer); @@ -84,11 +85,12 @@ private ReportItem getMetric(String metricName, GHUser developer) throws MetricE default: throw new MetricException("La métrica " + metricName + " no está definida para un repositorio"); } + */ - return metric; + return myQueries.get(metricName).apply(developer); } - private ReportItem getClosedIssuesLastMonth(GHUser developer) { + static private ReportItem getClosedIssuesLastMonth(GHUser developer) { log.info("Consultando los issues asignados a un desarrollador"); ReportItemBuilder builder = null; int issues = 0; @@ -115,7 +117,7 @@ private ReportItem getClosedIssuesLastMonth(GHUser developer) { return builder.build(); } - private ReportItem getAssignedIssuesLastMonth(GHUser developer) { + static private ReportItem getAssignedIssuesLastMonth(GHUser developer) { // TODO Auto-generated method stub return null; } diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java index cfade777..aa7b6f74 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java @@ -30,12 +30,13 @@ *

* * @author Isabel Román + * @param * */ -public abstract class GitHubEnquirer implements RemoteEnquirer { - private static Logger log = Logger.getLogger(GitHubEnquirer.class.getName()); - protected List metricNames; - protected Map> myQueries=new HashMap>(); +public abstract class GitHubEnquirer implements RemoteEnquirer { + private static Logger log = Logger.getLogger(GitHubEnquirer.class.getName()); + protected Map> myQueries; + /** *

* Referencia al objeto GitHub que permite hacer consultas al servidor Github @@ -48,7 +49,7 @@ public abstract class GitHubEnquirer implements RemoteEnquirer { private GitHub github = null; public GitHubEnquirer() { - metricNames = new ArrayList(); + myQueries=new HashMap>(); } /** @@ -75,12 +76,13 @@ protected GitHub getConnection() { return github; } - protected void setMetric(String newMetric) { - metricNames.add(newMetric); + protected void setMetric(String newMetric,Function functionPointer) { + myQueries.put(newMetric, functionPointer); } public List getAvailableMetrics() { - return metricNames; + List metrics=new ArrayList(myQueries.keySet()); + return metrics; } } diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java index 32b50c2a..d9d5e343 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java @@ -41,7 +41,7 @@ * */ -public class GitHubOrganizationEnquirer extends GitHubEnquirer { +public class GitHubOrganizationEnquirer extends GitHubEnquirer { private static Logger log = Logger.getLogger(GitHubOrganizationEnquirer.class.getName()); /** *

@@ -53,17 +53,19 @@ public class GitHubOrganizationEnquirer extends GitHubEnquirer { public GitHubOrganizationEnquirer() { super(); - metricNames.add("repositoriesWithOpenPullRequest"); - metricNames.add("repositories"); - metricNames.add("pullRequests"); - metricNames.add("members"); - metricNames.add("teams"); - metricNames.add("openProjects"); - metricNames.add("closedProjects"); - metricNames.add("followers"); + myQueries.put("repositoriesWithOpenPullRequest",GitHubOrganizationEnquirer::getRepositoriesWithOpenPullRequest); + myQueries.put("repositories",GitHubOrganizationEnquirer::getRepositories); + myQueries.put("pullRequests",GitHubOrganizationEnquirer::getPullRequests); + myQueries.put("members",GitHubOrganizationEnquirer::getMembers); + myQueries.put("teams",GitHubOrganizationEnquirer::getTeams); + myQueries.put("openProjects",GitHubOrganizationEnquirer::getOpenProjects); + myQueries.put("closedProjects",GitHubOrganizationEnquirer::getClosedProjects); + myQueries.put("followers",GitHubOrganizationEnquirer::getFollowers); + //Equipo 7 23/24 - metricNames.add("teamsBalance"); - log.info("Incluidos nombres metricas en Enquirer"); + myQueries.put("teamsBalance",GitHubOrganizationEnquirer::getTeamsBalance); + + log.info("Incluidas métricas en Enquirer"); } @Override @@ -206,7 +208,7 @@ private ReportItem getMetric(String metricName, GHOrganization organization) thr return metric; } - private ReportItem getRepositoriesWithOpenPullRequest(GHOrganization organization) { + static private ReportItem getRepositoriesWithOpenPullRequest(GHOrganization organization) { log.info("Consultando los repositorios con pull requests abiertos"); ReportItemBuilder builder = null; try { @@ -220,7 +222,7 @@ private ReportItem getRepositoriesWithOpenPullRequest(GHOrganization organizatio return builder.build(); } - private ReportItem getRepositories(GHOrganization organization) { + static private ReportItem getRepositories(GHOrganization organization) { log.info("Consultando los repositorios"); ReportItemBuilder builder = null; try { @@ -233,7 +235,7 @@ private ReportItem getRepositories(GHOrganization organization) { return builder.build(); } - private ReportItem getMembers(GHOrganization organization) { + static private ReportItem getMembers(GHOrganization organization) { log.info("Consultando los miembros"); ReportItemBuilder builder = null; try { @@ -246,7 +248,7 @@ private ReportItem getMembers(GHOrganization organization) { return builder.build(); } - private ReportItem getTeams(GHOrganization organization) { + static private ReportItem getTeams(GHOrganization organization) { log.info("Consultando los equipos"); ReportItemBuilder builder = null; try { @@ -261,7 +263,7 @@ private ReportItem getTeams(GHOrganization organization) { return builder.build(); } - private ReportItem getFollowers(GHOrganization organization) { + static private ReportItem getFollowers(GHOrganization organization) { log.info("Consultando los seguidores"); ReportItemBuilder builder = null; try { @@ -274,7 +276,7 @@ private ReportItem getFollowers(GHOrganization organization) { return builder.build(); } - private ReportItem getPullRequests(GHOrganization organization) { + static private ReportItem getPullRequests(GHOrganization organization) { log.info("Consultando los pull requests"); ReportItemBuilder builder = null; try { @@ -287,7 +289,7 @@ private ReportItem getPullRequests(GHOrganization organization) { return builder.build(); } - private ReportItem getOpenProjects(GHOrganization organization) { + static private ReportItem getOpenProjects(GHOrganization organization) { ReportItemBuilder builder = null; try { @@ -319,7 +321,7 @@ private ReportItem getOpenProjects(GHOrganization organization) { return builder.build(); } - private ReportItem getClosedProjects(GHOrganization organization) { + static private ReportItem getClosedProjects(GHOrganization organization) { ReportItemBuilder builder = null; try { log.info("Consultando los proyectos cerrados en " + organization.getUrl()); @@ -350,7 +352,7 @@ private ReportItem getClosedProjects(GHOrganization organization) { return builder.build(); } - private Map getIssuesPerRepository(GHOrganization organization) { + static private Map getIssuesPerRepository(GHOrganization organization) { log.info("Consultando los issues de cada uno de los repositorios de la organización"); Map mapa = new HashMap<>(); try { @@ -366,7 +368,7 @@ private Map getIssuesPerRepository(GHOrganization organiz return mapa; } - private Map getTeamsPerRepository(GHOrganization organization) { + static private Map getTeamsPerRepository(GHOrganization organization) { log.info("Consultando el númerdo de equipos por repositorio"); ReportItemBuilder> builder=null; Map mapa = new HashMap<>(); @@ -382,14 +384,14 @@ private Map getTeamsPerRepository(GHOrganization organiza return mapa; } - private ReportItem getTeamsBalance(GHOrganization organization) { + static private ReportItem getTeamsBalance(GHOrganization organization) { log.info("Consultando el equilibrio entre equipos e issues de los repositorios"); - Integer issuesTotales= this.getIssuesPerRepository(organization).size(); - Integer desarrolladoresTotales=(Integer) this.getTeams(organization).getValue(); - Integer numProjects = (Integer) this.getRepositories(organization).getValue(); - Map issuesPerProject =this.getIssuesPerRepository(organization); - Map teamsPerProject = this.getTeamsPerRepository(organization); + Integer issuesTotales= getIssuesPerRepository(organization).size(); + Integer desarrolladoresTotales=(Integer) getTeams(organization).getValue(); + Integer numProjects = (Integer) getRepositories(organization).getValue(); + Map issuesPerProject =getIssuesPerRepository(organization); + Map teamsPerProject = getTeamsPerRepository(organization); Double desajustePromedioOrganizacion=0.0; List desajustePromedioProyecto = new ArrayList(); diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index fbff03e5..52e47ca8 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -11,6 +11,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -47,11 +48,12 @@ * Debería haber un constructor para esto, para que la creación del mapa esté fuera * */ -public class GitHubRepositoryEnquirer extends GitHubEnquirer { +public class GitHubRepositoryEnquirer extends GitHubEnquirer { /** * para trazar el código */ private static Logger log = Logger.getLogger(GitHubRepositoryEnquirer.class.getName()); +// protected Map> myQueries=new HashMap>(); /** *

@@ -76,7 +78,7 @@ public GitHubRepositoryEnquirer() { myQueries.put("totalDeletions", GitHubRepositoryEnquirer::getTotalDeletions); myQueries.put("collaborators", GitHubRepositoryEnquirer::getCollaborators); myQueries.put("ownerCommits", GitHubRepositoryEnquirer::getOwnerCommits); - + /* metricNames.add("subscribers"); metricNames.add("forks"); metricNames.add("watchers"); @@ -92,42 +94,30 @@ public GitHubRepositoryEnquirer() { metricNames.add("totalDeletions"); metricNames.add("collaborators"); metricNames.add("ownerCommits"); + */ // equipo3 myQueries.put("issuesLastMonth", GitHubRepositoryEnquirer::getIssuesLastMonth); myQueries.put("closedIssuesLastMonth", GitHubRepositoryEnquirer::getClosedIssuesLastMonth); myQueries.put("meanClosedIssuesLastMonth", GitHubRepositoryEnquirer::getMeanClosedIssuesLastMonth); myQueries.put("issues4DevLastMonth", GitHubRepositoryEnquirer::getIssues4DevLastMonth); - metricNames.add("issuesLastMonth"); - metricNames.add("closedIssuesLastMonth"); - metricNames.add("meanClosedIssuesLastMonth"); - metricNames.add("issues4DevLastMonth"); + // equipo 4 myQueries.put("totalPullReq", GitHubRepositoryEnquirer::getTotalPullReq); myQueries.put("closedPullReq", GitHubRepositoryEnquirer::getClosedPullReq); - metricNames.add("totalPullReq"); - metricNames.add("closedPullReq"); + // equipo 5 myQueries.put("PRAcceptedLastYear", GitHubRepositoryEnquirer::getPRAcceptedLastYear); myQueries.put("PRAcceptedLastMonth", GitHubRepositoryEnquirer::getPRAcceptedLastMonth); myQueries.put("PRRejectedLastYear", GitHubRepositoryEnquirer::getPRRejectedLastYear); myQueries.put("PRRejectedLastMonth", GitHubRepositoryEnquirer::getPRRejectedLastMonth); - - metricNames.add("PRAcceptedLastYear"); - metricNames.add("PRAcceptedLastMonth"); - metricNames.add("PRRejectedLastYear"); - metricNames.add("PRRejectedLastMonth"); + // Equipo 1 myQueries.put("conventionalCommits", GitHubRepositoryEnquirer::getConventionalCommits); myQueries.put("commitsWithDescription", GitHubRepositoryEnquirer::getCommitsWithDescription); myQueries.put("issuesWithLabels", GitHubRepositoryEnquirer::getIssuesWithLabels); myQueries.put("gitFlowBranches", GitHubRepositoryEnquirer::getGitFlowBranches); myQueries.put("conventionalPullRequests", GitHubRepositoryEnquirer::getConventionalPullRequests); - metricNames.add("conventionalCommits"); - metricNames.add("commitsWithDescription"); - metricNames.add("issuesWithLabels"); - metricNames.add("gitFlowBranches"); - metricNames.add("conventionalPullRequests"); - + log.info("A�adidas m�tricas al GHRepositoryEnquirer"); } From 8e8d599a53eab2f5d27ea2ff02e9d93f55f334df Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Fri, 28 Feb 2025 19:57:52 +0100 Subject: [PATCH 085/100] Reorganizando RemoteEnquirer --- .../muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java | 6 ++++++ .../java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java | 6 ++++++ .../fs/a4i/model/remote/GitHubOrganizationEnquirer.java | 2 +- .../java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java | 7 +++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java index 12428320..7b1aed2e 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubDeveloperEnquirer.java @@ -122,4 +122,10 @@ static private ReportItem getAssignedIssuesLastMonth(GHUser developer) { return null; } + @Override + public RemoteType getRemoteType() { + // TODO Auto-generated method stub + return null; + } + } diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java index aa7b6f74..b9aed056 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java @@ -15,6 +15,7 @@ import org.kohsuke.github.GitHubBuilder; import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.remote.RemoteEnquirer.RemoteType; /** *

@@ -36,6 +37,7 @@ public abstract class GitHubEnquirer implements RemoteEnquirer { private static Logger log = Logger.getLogger(GitHubEnquirer.class.getName()); protected Map> myQueries; + private RemoteEnquirer.RemoteType type= RemoteEnquirer.RemoteType.GITHUB; /** *

@@ -84,5 +86,9 @@ public List getAvailableMetrics() { List metrics=new ArrayList(myQueries.keySet()); return metrics; } + public RemoteType getRemoteType() { + return type; + } + } diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java index d9d5e343..2cb833b7 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubOrganizationEnquirer.java @@ -369,7 +369,7 @@ static private Map getIssuesPerRepository(GHOrganization } static private Map getTeamsPerRepository(GHOrganization organization) { - log.info("Consultando el númerdo de equipos por repositorio"); + log.info("Consultando el número de equipos por repositorio"); ReportItemBuilder> builder=null; Map mapa = new HashMap<>(); try { diff --git a/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java index 3371910e..261c9bb6 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java @@ -67,5 +67,12 @@ public static enum RemoteType { * @return El listado de los nombres de métricas definidas */ public List getAvailableMetrics(); + /** + *

+ * Devuelve el tipo de remoto al que se consulta + *

+ * @return El tipo del remoto consultado + */ + public RemoteType getRemoteType(); } From a3e97a24fae0c44b20f134a1d4bcaeb9266482c2 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Sun, 16 Mar 2025 09:54:04 +0100 Subject: [PATCH 086/100] Mejora tests clase Context --- .../java/us/muit/fs/a4i/config/Context.java | 16 +++-- src/main/resources/a4i.conf | 2 +- .../muit/fs/a4i/test/config/ContextTest.java | 32 +++++---- .../muit/fs/a4i/test/config/ContextTest2.java | 66 +++++++++++-------- src/test/resources/appTest.conf | 25 ++++--- 5 files changed, 86 insertions(+), 55 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/config/Context.java b/src/main/java/us/muit/fs/a4i/config/Context.java index e4cfaaf9..7eac31d1 100644 --- a/src/main/java/us/muit/fs/a4i/config/Context.java +++ b/src/main/java/us/muit/fs/a4i/config/Context.java @@ -4,6 +4,7 @@ package us.muit.fs.a4i.config; import java.awt.Color; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -19,16 +20,21 @@ * Clase para la gestión de los parámetros de contexto *

*

- * El objetivo de Context es el manejo de la configuración + * El objetivo de Context es el manejo de la configuración de la api. La configuración por defecto se separa en dos ficheros principales: + *

    + *
  1. a4i.conf: contiene la configuración por defecto de la api. Permite seleccionar el tipo de remoto con el que se quiere interaccionar, el tipo de persistencia de los informes y características de presentación de estos informes.
  2. + *
  3. a4iDefault.json: contiene la configuración por defecto de métricas e indicadores. Esta configuración se maneja en la clase checker
  4. + *
+ * Hay una configuración embebida en el jar, es decir, una configuración por defecto. Pero esta puede ser modificada si la aplicación cliente define ficheros de configuración personalizados. *

*

- * En el estado actual Contexto sólo es una aproximación a las posiblidades de + * En el estado actual Context es una aproximación a las posiblidades de * configuración. Se presentan posibilidades para: *

*
    - *
  • Localizar el fichero en la carpeta resources, incluida en el jar
  • - *
  • Localizar el fichero en el home de usuario
  • - *
  • Localizar el fichero en una ruta introducida de forma "programada"
  • + *
  • Localizar el fichero a4i.conf en la carpeta resources, incluida en el jar
  • + *
  • Localizar el fichero *.conf (configuración personalizada) en el home de usuario
  • + *
  • Localizar el fichero *.conf (configuración personalizada) en una ruta introducida de forma "programada"
  • *
*

* Único punto para acceso a variables que pueden ser leídas por cualquiera, diff --git a/src/main/resources/a4i.conf b/src/main/resources/a4i.conf index 0dd5a061..71ebc8a7 100644 --- a/src/main/resources/a4i.conf +++ b/src/main/resources/a4i.conf @@ -20,7 +20,7 @@ Font.WARNING.type=Serif Font.CRITICAL.color=Red Font.CRITICAL.height=12 -Font.CRITICAL.type="Times New Roman" +Font.CRITICAL.type=Arial #Caracteristicas por defecto de la fuente, cuando los informes son visuales Font.default.color=black diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java index f198d851..ee9a2a07 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java @@ -31,7 +31,7 @@ /** * @author Isabel Román - * + * Verificación de la clase Context cuando sólo se utiliza el fichero de configuración por defecto (a4i.conf) */ class ContextTest { private static Logger log = Logger.getLogger(CheckerTest.class.getName()); @@ -48,13 +48,13 @@ class ContextTest { /** * @throws java.lang.Exception - */ + @BeforeAll static void setUpBeforeClass() throws Exception { appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appConfTest.json"; appPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appTest.conf"; - } + } */ /** * @throws java.lang.Exception @@ -155,7 +155,7 @@ void testGetDefaultFont() { //Al ser Context un singleton una vez creada la instancia no se puede eliminar "desde fuera" //De manera que el valor de la fuente depende del orden en el que se ejecuten los test, y para que el test sea independiente de eso la verificación comprueba los dos posibles valores assertEquals(Color.BLACK.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); - assertEquals(12,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + assertEquals(10,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); assertEquals("Arial",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); } catch (IOException e) { @@ -172,13 +172,13 @@ void testGetDefaultFont() { void testGetMetricFont(){ try { - //fail("Not yet implemented"); + Font font = null; font = Context.getContext().getMetricFont(); assertNotNull(font, "No se ha inicializado bien la fuente"); assertEquals(Color.GREEN.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); - assertTrue(15 == font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + assertTrue(10 == font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); assertEquals(java.awt.Font.SERIF,font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); @@ -199,12 +199,13 @@ void testGetIndicatorFont() { // Se le solicita la fuente del estado indefinido, que tendrá los valores por defecto Font.Default.xxx al no estar // definidas sus propiedades en el fichero de configuración utilizados en los tests. - font = Context.getContext().getIndicatorFont(IndicatorState.UNDEFINED); + font = Context.getContext().getIndicatorFont(IndicatorState.OK); assertNotNull(font, "No se ha inicializado bien la fuente"); // El nombre o tipo de la fuente podrá ser Arial o Times según el momento en el que se realicen los tests. - assertEquals("Arial",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); + log.info("familia "+font.getFont().getFamily()); + assertEquals("Serif",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); assertEquals(Color.BLACK,font.getColor(),"No es el color de fuente especificado en el fichero de propiedades"); - assertEquals(12,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + assertEquals(10,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); // Se le solicita al contexto la fuente del estao "CRITICAL", cuyas propiedades están definidas en el // fichero de configuración por defecto. @@ -214,10 +215,10 @@ void testGetIndicatorFont() { log.info("Datos fuente estado CRITICAL familia "+font.getFont().getFamily()+" tamano "+font.getFont().getSize()+" color "+font.getColor()); - - assertTrue(font.getFont().getFamily().equals("Serif"),"No es el tipo de fuente especificado en el fichero de propiedades"); + + assertTrue(font.getFont().getFamily().equals("Arial"),"No es el tipo de fuente especificado en el fichero de propiedades"); assertEquals(Color.RED,font.getColor(),"No es el color de fuente especificado en el fichero de propiedades"); - assertEquals(16,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + assertEquals(12,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); } catch (IOException e) { fail("No debería devolver esta excepción"); @@ -227,10 +228,13 @@ void testGetIndicatorFont() { /** * Test method for {@link us.muit.fs.a4i.config.Context#getPropertiesNames()}. + * @throws IOException + * Este método de verificación está incompleto, deberá ser completado commo ejercicio + * Verificar que los nombres son correctos */ @Test - void testGetPropertiesNames() { - fail("Not yet implemented"); + void testGetPropertiesNames() throws IOException { + log.info(Context.getContext().getPropertiesNames().toString()); } } diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest2.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest2.java index df5936d9..82ed1386 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest2.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest2.java @@ -50,7 +50,7 @@ /** * @author Isabel Román - * + * Verificación de la clase context cuando hay ficheros de configuración personalizados */ class ContextTest2 { private static Logger log = Logger.getLogger(CheckerTest.class.getName()); @@ -67,12 +67,18 @@ class ContextTest2 { /** * @throws java.lang.Exception + * Se establecen los ficheros de configuración de la api y de métricas e indicadores propietarios */ @BeforeAll static void setUpBeforeClass() throws Exception { - appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + log.info("Estableciendo las rutas de los ficheros de configuración del cliente"); + //Fichero de métricas e indicadores establecido por el cliente + appPath= "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appConfTest.json"; - appPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appTest.conf"; + //Fichero de configuración de la api establecido por el cliente + appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appTest.conf"; + Context.setAppConf(appConfPath); + Context.setAppRI(appPath); } /** @@ -125,17 +131,19 @@ void testSetAppRI() { * Este test excede los límites, ya que no sólo verifica que se establece bien * la ruta del fichero de especificación de métricas e indicadores sino que se * leen bien los valores del mismo Sería un test de integración porque se - * requiere que estén ya desarrollados otras clases, a parte de Context + * requiere que estén ya desarrollados otras clases, aparte de Context (usa la clase Checker) + * EJERCICIO + * Se podría crear un mock de checker para que se convirtiera en un test unidad */ try { - Context.setAppRI(appConfPath); + HashMap metricInfo = Context.getContext().getChecker().getMetricConfiguration() .getMetricInfo("downloads"); assertNotNull(metricInfo, "No se han leído los atributos de la métrica"); assertEquals("downloads", metricInfo.get("name"), "El nombre no es el correcto"); assertEquals("java.lang.Integer", metricInfo.get("type"), "El tipo no es el correcto"); assertEquals("Descargas realizadas", metricInfo.get("description"), "La descripción no es el correcta"); - assertEquals("downloads", metricInfo.get("unit"), "Las uniddes no son correctas"); + assertEquals("downloads", metricInfo.get("unit"), "Las unidades no son correctas"); } catch (IOException e) { fail("No se encuentra el fichero de especificación de métricas e indicadores"); @@ -186,8 +194,8 @@ void testGetChecker() { @Test void testGetPersistenceType() { try { - assertEquals("EXCEL", Context.getContext().getPersistenceType(), - "En el fichero de configuración por defecto, a4i.conf, está definido el tipo excel"); + assertEquals("WORD", Context.getContext().getPersistenceType(), + "En el fichero de configuración personalizado está definido el tipo word"); } catch (IOException e) { fail("El fichero no se localiza"); e.printStackTrace(); @@ -200,8 +208,8 @@ void testGetPersistenceType() { @Test void testGetRemoteType() { try { - assertEquals("GITHUB", Context.getContext().getRemoteType(), - "En el fichero de configuración por defecto, a4i.conf, está el tipo github"); + assertEquals("GITLAB", Context.getContext().getRemoteType(), + "En el fichero de configuración personalizado está el tipo gitlab"); } catch (IOException e) { fail("El fichero no se localiza"); e.printStackTrace(); @@ -209,8 +217,9 @@ void testGetRemoteType() { } /** - *

Este test permite verificar que se lee bien la fuente, además es independiente del orden de ejecución del resto de test. La complejidad de la verifiación está impuesta por estar probando un singleton

+ *

Este test permite verificar que se lee bien la fuente. Este método es idéntico al de la clase ContextTest porque el fichero personalizado no modifica los parámetros por defecto

* Test method for {@link us.muit.fs.a4i.config.Context#getDefaultFont()}. + * */ @Test void testGetDefaultFont() { @@ -221,11 +230,9 @@ void testGetDefaultFont() { //String[] fontNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); //log.info("listado de fuentes " + Arrays.toString(fontNames)); font = Context.getContext().getDefaultFont(); - assertNotNull(font, "No se ha inicializado bien la fuente"); - //Al ser Context un singleton una vez creada la instancia no se puede eliminar "desde fuera" - //De manera que el valor de la fuente depende del orden en el que se ejecuten los test, y para que el test sea independiente de eso la verificación comprueba los dos posibles valores + assertNotNull(font, "No se ha inicializado bien la fuente"); assertEquals(Color.BLACK.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); - assertEquals(12,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + assertEquals(10,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); assertEquals("Arial",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); } catch (IOException e) { @@ -235,6 +242,7 @@ void testGetDefaultFont() { } /** + *

Este test permite verificar si se sobreescribe la configuración por defecto

* Test method for {@link us.muit.fs.a4i.config.Context#getMetricFont()}. * @throws IOException */ @@ -242,14 +250,16 @@ void testGetDefaultFont() { void testGetMetricFont(){ try { - //fail("Not yet implemented"); + Font font = null; - + // Uso esto para ver los tipos de fuentes de los que dispongo + //String[] fontNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); + //log.info("listado de fuentes " + Arrays.toString(fontNames)); font = Context.getContext().getMetricFont(); assertNotNull(font, "No se ha inicializado bien la fuente"); - assertEquals(Color.GREEN.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); + assertEquals(Color.RED.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); assertTrue(15 == font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); - assertEquals(java.awt.Font.SERIF,font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); + assertEquals("Arial",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); }catch (IOException e) { @@ -269,18 +279,18 @@ void testGetIndicatorFont() { // Se le solicita la fuente del estado indefinido, que tendrá los valores por defecto al no estar // definidas sus propiedades en el fichero de configuración utilizados en los tests. + font = Context.getContext().getIndicatorFont(IndicatorState.UNDEFINED); assertNotNull(font, "No se ha inicializado bien la fuente"); // El nombre o tipo de la fuente podrá ser Arial o Times según el momento en el que se realicen los tests. assertEquals("Arial",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); - // Se le solicita al contexto la fuente del estao "CRITICAL", cuyas propiedades están definidas en el - // fichero de configuración por defecto. + // Se le solicita al contexto la fuente del estao "CRITICAL" font = Context.getContext().getIndicatorFont(IndicatorState.CRITICAL); assertNotNull(font, "No se ha inicializado bien la fuente"); - assertEquals("Times",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); - assertEquals("RED",font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); - assertEquals(16,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); + assertEquals("Courier New",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); + assertEquals(Color.RED.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); + assertEquals(20,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); } catch (IOException e) { fail("No debería devolver esta excepción"); @@ -288,12 +298,16 @@ void testGetIndicatorFont() { } } + /** * Test method for {@link us.muit.fs.a4i.config.Context#getPropertiesNames()}. + * @throws IOException + * Este método de verificación está incompleto, deberá ser completado commo ejercicio + * Verificar que los nombres son correctos */ @Test - void testGetPropertiesNames() { - fail("Not yet implemented"); + void testGetPropertiesNames() throws IOException { + log.info(Context.getContext().getPropertiesNames().toString()); } } diff --git a/src/test/resources/appTest.conf b/src/test/resources/appTest.conf index a3fc0337..7b35bf69 100644 --- a/src/test/resources/appTest.conf +++ b/src/test/resources/appTest.conf @@ -1,21 +1,28 @@ #Fichero de configuración establecido por la aplicación +#Tipo de persistencia que se quiere usar +persistence.type=WORD +#Tipo de remoto que se quiere usar +remote.type=GITLAB +#Caracteristicas de fuente de las métricas +Font.metric.color=Red +Font.metric.height=15 +Font.metric.type=Arial + #Caracteristicas para la fuente de indicadores según estados del mismo Font.OK.color=Black -Font.OK.height=12 -Font.OK.type=Times +Font.OK.height=10 +Font.OK.type=Serif Font.WARNING.color=Orange -Font.WARNING.height=12 -Font.WARNING.type=Times +Font.WARNING.height=10 +Font.WARNING.type=Serif Font.CRITICAL.color=Red -Font.CRITICAL.height=16 -Font.CRITICAL.type=Times +Font.CRITICAL.height=20 +Font.CRITICAL.type=Courier New #Caracteristicas por defecto de la fuente, cuando los informes son visuales -Font.default.color=Black -Font.default.height=12 -Font.default.type=Arial \ No newline at end of file +#no lo modifico From 620f775dde9b4ea19d44f1875319bc8bef2227b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Sun, 16 Mar 2025 10:03:54 +0100 Subject: [PATCH 087/100] Update pruebas.yml --- .github/workflows/pruebas.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pruebas.yml b/.github/workflows/pruebas.yml index f7ae993e..2db842e3 100644 --- a/.github/workflows/pruebas.yml +++ b/.github/workflows/pruebas.yml @@ -12,8 +12,8 @@ jobs: runs-on: ubuntu-latest env: GITHUB_LOGIN: ${{ github.actor }} - GITHUB_PACKAGES: ${{ secrets.GITHUB_TOKEN }} - GITHUB_OAUTH: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PACKAGES: ${{ secrets.GHTOKEN }} + GITHUB_OAUTH: ${{ secrets.GHTOKEN }} steps: - name: Clonando el repositorio y estableciendo el espacio de trabajo uses: actions/checkout@v3 From 41981a0b748cbfc3535e73cf1a3696f6a43c4069 Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Sun, 16 Mar 2025 21:37:04 +0100 Subject: [PATCH 088/100] =?UTF-8?q?Preparando=20la=20pr=C3=A1ctica=20de=20?= =?UTF-8?q?pruebas=20unidad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/us/muit/fs/a4i/config/Checker.java | 12 +- .../java/us/muit/fs/a4i/config/Context.java | 113 ++++---- .../java/us/muit/fs/a4i/config/GitFlow.java | 95 +++---- .../fs/a4i/config/IndicatorConfiguration.java | 91 ++++--- .../ConventionsCompliantStrategy.java | 244 +++++++++--------- .../strategies/TeamsBalanceStrategy.java | 30 ++- .../us/muit/fs/a4i/model/entities/Font.java | 53 +--- .../muit/fs/a4i/test/config/CheckerTest.java | 107 +------- .../muit/fs/a4i/test/config/CheckerTest2.java | 200 ++++++++++++++ .../muit/fs/a4i/test/config/ContextTest.java | 7 +- 10 files changed, 534 insertions(+), 418 deletions(-) create mode 100644 src/test/java/us/muit/fs/a4i/test/config/CheckerTest2.java diff --git a/src/main/java/us/muit/fs/a4i/config/Checker.java b/src/main/java/us/muit/fs/a4i/config/Checker.java index 639405e0..f7514b12 100644 --- a/src/main/java/us/muit/fs/a4i/config/Checker.java +++ b/src/main/java/us/muit/fs/a4i/config/Checker.java @@ -3,11 +3,13 @@ import java.util.logging.Logger; /** - *

Esta clase permite acceder a las interfaces para configurar y verificar métricas e indicadores - * Se mantiene separada la lógica de la gestión de configuración de métricas e - * indicadores porque en el futuro van a ser bastante diferentes. En la versión - * actual son muy similares y por tanto el diseño no es bueno ya que no - * identifica bien la reutilización

+ *

+ * Esta clase permite acceder a las interfaces para configurar y verificar + * métricas e indicadores Se mantiene separada la lógica de la gestión de + * configuración de métricas e indicadores porque en el futuro van a ser + * bastante diferentes. En la versión actual son muy similares y por tanto el + * diseño no es bueno ya que no identifica bien la reutilización + *

* * @author Isabel Román * diff --git a/src/main/java/us/muit/fs/a4i/config/Context.java b/src/main/java/us/muit/fs/a4i/config/Context.java index 7eac31d1..2a11d0d2 100644 --- a/src/main/java/us/muit/fs/a4i/config/Context.java +++ b/src/main/java/us/muit/fs/a4i/config/Context.java @@ -20,21 +20,31 @@ * Clase para la gestión de los parámetros de contexto *

*

- * El objetivo de Context es el manejo de la configuración de la api. La configuración por defecto se separa en dos ficheros principales: + * El objetivo de Context es el manejo de la configuración de la api. La + * configuración por defecto se separa en dos ficheros principales: *

    - *
  1. a4i.conf: contiene la configuración por defecto de la api. Permite seleccionar el tipo de remoto con el que se quiere interaccionar, el tipo de persistencia de los informes y características de presentación de estos informes.
  2. - *
  3. a4iDefault.json: contiene la configuración por defecto de métricas e indicadores. Esta configuración se maneja en la clase checker
  4. + *
  5. a4i.conf: contiene la configuración por defecto de la api. Permite + * seleccionar el tipo de remoto con el que se quiere interaccionar, el tipo de + * persistencia de los informes y características de presentación de estos + * informes.
  6. + *
  7. a4iDefault.json: contiene la configuración por defecto de métricas e + * indicadores. Esta configuración se maneja en la clase checker
  8. *
- * Hay una configuración embebida en el jar, es decir, una configuración por defecto. Pero esta puede ser modificada si la aplicación cliente define ficheros de configuración personalizados. + * Hay una configuración embebida en el jar, es decir, una configuración por + * defecto. Pero esta puede ser modificada si la aplicación cliente define + * ficheros de configuración personalizados. *

*

* En el estado actual Context es una aproximación a las posiblidades de * configuración. Se presentan posibilidades para: *

*
    - *
  • Localizar el fichero a4i.conf en la carpeta resources, incluida en el jar
  • - *
  • Localizar el fichero *.conf (configuración personalizada) en el home de usuario
  • - *
  • Localizar el fichero *.conf (configuración personalizada) en una ruta introducida de forma "programada"
  • + *
  • Localizar el fichero a4i.conf en la carpeta resources, incluida en el + * jar
  • + *
  • Localizar el fichero *.conf (configuración personalizada) en el home de + * usuario
  • + *
  • Localizar el fichero *.conf (configuración personalizada) en una ruta + * introducida de forma "programada"
  • *
*

* Único punto para acceso a variables que pueden ser leídas por cualquiera, @@ -118,7 +128,8 @@ public static void setAppRI(String filename) { * cliente/aplicación *

* - * @return ruta del fichero de configuración de métricas e indicadores de la aplicación cliente + * @return ruta del fichero de configuración de métricas e indicadores de la + * aplicación cliente */ public static String getAppRI() { return appFile; @@ -175,9 +186,9 @@ public static void setAppConf(String appConPath) throws IOException { getContext().properties.load(new FileInputStream(appConPath)); log.info("Las nuevas propiedades son " + getContext().properties); } - - public static String getAppConf() throws IOException { - return appConfFile; + + public static String getAppConf() throws IOException { + return appConfFile; } /** @@ -216,9 +227,9 @@ public String getRemoteType() throws IOException { /** *

- * Lee las propiedades adecuadas, como color, tamaño, - * tipo... y construir un objeto Font Si no se ha establecido un valor por - * defecto se crea una fuente simple + * Lee las propiedades adecuadas, como color, tamaño, tipo... y construir un + * objeto Font Si no se ha establecido un valor por defecto se crea una fuente + * simple *

* * @return La fuente por defecto para indicadores y métricas @@ -230,33 +241,34 @@ public Font getDefaultFont() { // devuelva sea un String con el color log.info("Busca la información de configuración de la fuente, por defecto"); - - String color = getDefaultParam("color"); + String color = getDefaultParam("color"); String height = getDefaultParam("height"); - String type = getDefaultParam("type"); + String type = getDefaultParam("type"); log.info("Los datos son, color: " + color + " height: " + height + " type: " + type); log.info("Intento crear la fuente"); - return new Font(type,Integer.valueOf(height),color); - + return new Font(type, Integer.valueOf(height), color); + } + private String getDefaultParam(String param) { - String property="Font.default."+param; - String value=properties.getProperty(property); + String property = "Font.default." + param; + String value = properties.getProperty(property); /** - * Se asegura de que se da un valor por defecto, aunque no esté configurado en el fichero + * Se asegura de que se da un valor por defecto, aunque no esté configurado en + * el fichero */ - if (value==null) { - switch(param) { + if (value == null) { + switch (param) { case "type": - value="Arial"; + value = "Arial"; break; case "color": - value="black"; + value = "black"; break; case "height": - value="12"; + value = "12"; break; } } @@ -265,8 +277,8 @@ private String getDefaultParam(String param) { /** *

- * Lee las propiedades adecuadas, como color, tamaño, tipo... y - * construye un objeto Font para la fuente de las métricas + * Lee las propiedades adecuadas, como color, tamaño, tipo... y construye un + * objeto Font para la fuente de las métricas *

*

* Si no se ha definido una fuente para las métricas se debe devolver la fuente @@ -282,21 +294,21 @@ public Font getMetricFont() { String height = properties.getProperty("Font.metric.height"); String color = properties.getProperty("Font.metric.color"); - if (type==null){ + if (type == null) { type = getDefaultParam("type"); log.info("El tipo de la fuente de las metricas es el valor por defecto"); } - if (height==null){ - height =getDefaultParam("height"); + if (height == null) { + height = getDefaultParam("height"); log.info("El tamaño de la fuente de las metricas es el valor por defecto"); } - if (color==null){ + if (color == null) { color = getDefaultParam("color"); log.info("El color de la fuente de las metricas es el valor por defecto"); } - log.info("Llamo a newFont con los datos color: " + color + " height: " + height + " type: " + type); - + log.info("Llamo a newFont con los datos color: " + color + " height: " + height + " type: " + type); + return new Font(type, Integer.valueOf(height), color); } @@ -313,31 +325,32 @@ public Font getMetricFont() { */ public Font getIndicatorFont(IndicatorI.IndicatorState state) throws IOException { - /**He eliminado el static, así si funciona - * Hay que comprobar si el static estaba puesto con sentido desde un primer momento - * **/ + /** + * He eliminado el static, así si funciona Hay que comprobar si el static estaba + * puesto con sentido desde un primer momento + **/ Font font = null; // TODO: String propertyState = "Font." + state.toString(); - log.info("Raiz que uso para buscar los datos del indicador en estado "+state+" "+propertyState); - - String color = properties.getProperty(propertyState + ".color"); + log.info("Raiz que uso para buscar los datos del indicador en estado " + state + " " + propertyState); + + String color = properties.getProperty(propertyState + ".color"); String height = properties.getProperty(propertyState + ".height"); - String type = properties.getProperty(propertyState + ".type"); - + String type = properties.getProperty(propertyState + ".type"); + log.info("Los datos son, color: " + color + " height: " + height + " type: " + type); log.info("Intento crear la fuente"); - + if (color == null) { - color = properties.getProperty("Font.default.color"); - } + color = properties.getProperty("Font.default.color"); + } if (height == null) { height = properties.getProperty("Font.default.height"); - } - if(type == null){ - type = properties.getProperty("Font.default.type"); } - + if (type == null) { + type = properties.getProperty("Font.default.type"); + } + font = new Font(type, Integer.valueOf(height), color); return font; } diff --git a/src/main/java/us/muit/fs/a4i/config/GitFlow.java b/src/main/java/us/muit/fs/a4i/config/GitFlow.java index 149d48cc..1e87ee07 100644 --- a/src/main/java/us/muit/fs/a4i/config/GitFlow.java +++ b/src/main/java/us/muit/fs/a4i/config/GitFlow.java @@ -4,57 +4,58 @@ package us.muit.fs.a4i.config; /** - * Clase incluida por el equipo 1 del curso 23/24 para apoyo a las métricas de conformidad con convenciones de la organización + * Clase incluida por el equipo 1 del curso 23/24 para apoyo a las métricas de + * conformidad con convenciones de la organización */ - public enum GitFlow { - // MARK: - Enum cases - /** The main branch */ - MAIN, - /** The develop branch */ - DEVELOP, - /** A feature branch */ - FEATURE, - /** A release branch */ - RELEASE, - /** A hotfix branch */ - HOTFIX; + // MARK: - Enum cases + /** The main branch */ + MAIN, + /** The develop branch */ + DEVELOP, + /** A feature branch */ + FEATURE, + /** A release branch */ + RELEASE, + /** A hotfix branch */ + HOTFIX; - // MARK: - Methods - @Override - /** - * Returns the string representation of the enum case - */ - public String toString() { - return switch (this) { - case MAIN -> "main"; - case DEVELOP -> "develop"; - case FEATURE -> "feature"; - case RELEASE -> "release"; - case HOTFIX -> "hotfix"; - }; - } + // MARK: - Methods + @Override + /** + * Returns the string representation of the enum case + */ + public String toString() { + return switch (this) { + case MAIN -> "main"; + case DEVELOP -> "develop"; + case FEATURE -> "feature"; + case RELEASE -> "release"; + case HOTFIX -> "hotfix"; + }; + } - /** - * Checks if the branch name is a valid Git Flow branch - * @param branchName the name of the branch - * @return true if the branch name is a valid Git Flow branch - */ - public static boolean isGitFlowBranch(String branchName) { - return branchName.equals(MAIN.toString()) - || branchName.equals(DEVELOP.toString()) - || branchName.matches(branchNamePattern(FEATURE.toString())) - || branchName.matches(branchNamePattern(RELEASE.toString())) - || branchName.matches(branchNamePattern(HOTFIX.toString())); - } + /** + * Checks if the branch name is a valid Git Flow branch + * + * @param branchName the name of the branch + * @return true if the branch name is a valid Git Flow branch + */ + public static boolean isGitFlowBranch(String branchName) { + return branchName.equals(MAIN.toString()) || branchName.equals(DEVELOP.toString()) + || branchName.matches(branchNamePattern(FEATURE.toString())) + || branchName.matches(branchNamePattern(RELEASE.toString())) + || branchName.matches(branchNamePattern(HOTFIX.toString())); + } - /** - * Returns the pattern for the branch name - * @param branchType the type of branch - * @return the regex pattern for the branch name - */ - private static String branchNamePattern(String branchType) { - return "^" + branchType + "/[a-zA-Z0-9_-]+$"; - } + /** + * Returns the pattern for the branch name + * + * @param branchType the type of branch + * @return the regex pattern for the branch name + */ + private static String branchNamePattern(String branchType) { + return "^" + branchType + "/[a-zA-Z0-9_-]+$"; + } } \ No newline at end of file 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 59e2fd89..082a0f63 100644 --- a/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java @@ -28,7 +28,7 @@ public class IndicatorConfiguration implements IndicatorConfigurationI { private static Logger log = Logger.getLogger(Checker.class.getName()); - + public String CRITICAL_LIMIT = "limits.critical"; public String WARNING_LIMIT = "limits.warning"; public String OK_LIMIT = "limits.ok"; @@ -92,13 +92,13 @@ private HashMap isDefinedIndicator(String indicatorName, String indicatorDefinition = new HashMap(); indicatorDefinition.put("description", indicators.get(i).asJsonObject().getString("description")); indicatorDefinition.put("unit", indicators.get(i).asJsonObject().getString("unit")); - + JsonObject limits = indicators.get(i).asJsonObject().getJsonObject("limits"); int okLimit = 0; int warningLimit = 0; int criticalLimit = 0; - - if(limits != null) { + + if (limits != null) { okLimit = limits.getInt("ok"); warningLimit = limits.getInt("warning"); criticalLimit = limits.getInt("critical"); @@ -107,8 +107,8 @@ private HashMap isDefinedIndicator(String indicatorName, String indicatorDefinition.put(CRITICAL_LIMIT, Integer.toString(criticalLimit)); } else { log.info("El fichero de configuración no especifica límites para este indicador"); - } - + } + } } @@ -163,50 +163,55 @@ public List listAllIndicators() throws FileNotFoundException { } @Override - public IndicatorState getIndicatorState(ReportItemI indicator){ - //TODO: change indicator definitions key name to a constant. - + public IndicatorState getIndicatorState(ReportItemI indicator) { + // TODO: change indicator definitions key name to a constant. + String indicatorType = indicator.getValue().getClass().getName(); - + IndicatorState finalState = IndicatorState.UNDEFINED; try { - HashMap indicatorDefinition = definedIndicator(indicator.getName(), indicatorType); - - - String criticalLimit = indicatorDefinition.get(CRITICAL_LIMIT); - String warningLimit = indicatorDefinition.get(WARNING_LIMIT); - String okLimit = indicatorDefinition.get(OK_LIMIT); - - // Si no se han encontrado límites definidos para ese indicador el estado es UNDEFINED. - if(criticalLimit != null && warningLimit != null && okLimit != null) { - // Se tienen en cuenta los posibles tipos de indicadores para compararlos. - if(indicatorType.equals(Integer.class.getName())) { - Integer value = (Integer) indicator.getValue(); - - if(value >= Integer.parseInt(criticalLimit)) finalState = IndicatorState.CRITICAL; - else if(value <=Integer.parseInt(okLimit)) finalState = IndicatorState.OK; - else finalState = IndicatorState.WARNING; - - } else if(indicatorType.equals(Double.class.getName())) { - Double value = (Double) indicator.getValue(); - - if(value >= Integer.parseInt(criticalLimit)) finalState = IndicatorState.CRITICAL; - else if(value <= Integer.parseInt(okLimit)) finalState = IndicatorState.OK; - else finalState = IndicatorState.WARNING; - + HashMap indicatorDefinition = definedIndicator(indicator.getName(), indicatorType); + + String criticalLimit = indicatorDefinition.get(CRITICAL_LIMIT); + String warningLimit = indicatorDefinition.get(WARNING_LIMIT); + String okLimit = indicatorDefinition.get(OK_LIMIT); + + // Si no se han encontrado límites definidos para ese indicador el estado es + // UNDEFINED. + if (criticalLimit != null && warningLimit != null && okLimit != null) { + // Se tienen en cuenta los posibles tipos de indicadores para compararlos. + if (indicatorType.equals(Integer.class.getName())) { + Integer value = (Integer) indicator.getValue(); + + if (value >= Integer.parseInt(criticalLimit)) + finalState = IndicatorState.CRITICAL; + else if (value <= Integer.parseInt(okLimit)) + finalState = IndicatorState.OK; + else + finalState = IndicatorState.WARNING; + + } else if (indicatorType.equals(Double.class.getName())) { + Double value = (Double) indicator.getValue(); + + if (value >= Integer.parseInt(criticalLimit)) + finalState = IndicatorState.CRITICAL; + else if (value <= Integer.parseInt(okLimit)) + finalState = IndicatorState.OK; + else + finalState = IndicatorState.WARNING; + + } + } else { + log.warning("No se han encontrado límites definidos para el indicador: " + indicator.getName()); + finalState = IndicatorState.UNDEFINED; } - } else { - log.warning("No se han encontrado límites definidos para el indicador: " + indicator.getName()); - finalState = IndicatorState.UNDEFINED; - } - - }catch(Exception e){ - + + } catch (Exception e) { + e.printStackTrace(); } - log.info("El estado del Indicador "+indicator.getName()+" es "+finalState.toString()); + log.info("El estado del Indicador " + indicator.getName() + " es " + finalState.toString()); return finalState; } - } diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/ConventionsCompliantStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/ConventionsCompliantStrategy.java index f544062a..d12a7b6f 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/ConventionsCompliantStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/ConventionsCompliantStrategy.java @@ -21,120 +21,132 @@ import us.muit.fs.a4i.model.entities.ReportItemI; public class ConventionsCompliantStrategy implements IndicatorStrategy { - // MARK: - Attributes - private static Logger log = Logger.getLogger(Indicator.class.getName()); - // The metrics needed to calculate the indicator output - private static final List REQUIRED_METRICS = Arrays.asList( - "conventionalCommits", - "commitsWithDescription", - "issuesWithLabels", - "gitFlowBranches", - "conventionalPullRequests"); - public static final String ID = "conventionsCompliant"; - - // Weights for each metric - private double WEIGHT_CONVENTIONAL_COMMITS = 0.2; - private double WEIGHT_COMMITS_WITH_DESCRIPTION = 0.2; - private double WEIGHT_ISSUES_WITH_LABELS = 0.2; - private double WEIGHT_GIT_FLOW_BRANCHES = 0.2; - private double WEIGHT_CONVENTIONAL_PULL_REQUESTS = 0.2; - - // MARK: - Constructor - /** - * Constructor. Default weights are equal for all metrics (0.2). - */ - public ConventionsCompliantStrategy() { - super(); - } - /** - * Constructor. Set the weights for each metric. The sum of the weights must be equal to 1. - * @param weightConventionalCommits the weight for the conventional commits metric - * @param weightCommitsWithDescription the weight for the commits with description metric - * @param weightIssuesWithLabels the weight for the issues with labels metric - * @param weightGitFlowBranches the weight for the git flow branches metric - * @param weightConventionalPullRequests the weight for the conventional pull requests metric - */ - public ConventionsCompliantStrategy(Double weightConventionalCommits, Double weightCommitsWithDescription, - Double weightIssuesWithLabels, Double weightGitFlowBranches, - Double weightConventionalPullRequests) throws IllegalArgumentException { - // Control that the sum of the weights is equal to 1 - if (weightConventionalCommits + weightCommitsWithDescription + weightIssuesWithLabels + weightGitFlowBranches + weightConventionalPullRequests != 1) { - throw new IllegalArgumentException("The sum of the weights must be equal to 1"); - } - - // Set the weights - this.WEIGHT_CONVENTIONAL_COMMITS = weightConventionalCommits; - this.WEIGHT_COMMITS_WITH_DESCRIPTION = weightCommitsWithDescription; - this.WEIGHT_ISSUES_WITH_LABELS = weightIssuesWithLabels; - this.WEIGHT_GIT_FLOW_BRANCHES = weightGitFlowBranches; - this.WEIGHT_CONVENTIONAL_PULL_REQUESTS = weightConventionalPullRequests; - } - - // MARK: - Implemented methods - // Calculate the indicator output based on the provided metrics - @Override - public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { - // Attributes - Optional> conventionalCommits = metrics.stream().filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); - Optional> commitsWithDescription = metrics.stream().filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); - Optional> issuesWithLabels = metrics.stream().filter(m -> REQUIRED_METRICS.get(2).equals(m.getName())).findAny(); - Optional> gitFlowBranches = metrics.stream().filter(m -> REQUIRED_METRICS.get(3).equals(m.getName())).findAny(); - Optional> conventionalPullRequests = metrics.stream().filter(m -> REQUIRED_METRICS.get(4).equals(m.getName())).findAny(); - ReportItemI indicatorReport = null; - - // Check if the required metrics are present - if (conventionalCommits.isPresent() && commitsWithDescription.isPresent() && issuesWithLabels.isPresent() && gitFlowBranches.isPresent() && conventionalPullRequests.isPresent()) { - // Calculate the indicator - Double metric1, metric2, metric3, metric4, metric5; - // Initialize the metrics - if (conventionalCommits.get().getValue() >= 0.8499) metric1 = conventionalCommits.get().getValue(); - else metric1 = 0.0; - if (commitsWithDescription.get().getValue() >= 0.8499) metric2 = commitsWithDescription.get().getValue(); - else metric2 = 0.0; - if (issuesWithLabels.get().getValue() >= 0.8499) metric3 = issuesWithLabels.get().getValue(); - else metric3 = 0.0; - if (gitFlowBranches.get().getValue() >= 0.999) metric4 = 1.0; - else metric4 = 0.0; - if (conventionalPullRequests.get().getValue() >= 0.8499) metric5 = conventionalPullRequests.get().getValue(); - else metric5 = 0.0; - - Double indicatorValue = - WEIGHT_CONVENTIONAL_COMMITS * metric1 + - WEIGHT_COMMITS_WITH_DESCRIPTION * metric2 + - WEIGHT_ISSUES_WITH_LABELS * metric3 + - WEIGHT_GIT_FLOW_BRANCHES * metric4 + - WEIGHT_CONVENTIONAL_PULL_REQUESTS * metric5; - - try { - // Create the indicator - indicatorReport = new ReportItem.ReportItemBuilder(ID, indicatorValue) - .metrics(Arrays.asList( - conventionalCommits.get(), - commitsWithDescription.get(), - issuesWithLabels.get(), - gitFlowBranches.get(), - conventionalPullRequests.get()) - ) - .indicator(IndicatorState.UNDEFINED).build(); - } catch (ReportItemException e) { - log.info("Error en ReportItemBuilder."); - e.printStackTrace(); - } - - } else { - log.info("No se han proporcionado las métricas necesarias"); - throw new NotAvailableMetricException(REQUIRED_METRICS.toString()); - } - - // Temporarily throw - return indicatorReport; - } - - // Return the metrics required to calculate the indicator output - @Override - public List requiredMetrics() { - return REQUIRED_METRICS; - } - -} + // MARK: - Attributes + private static Logger log = Logger.getLogger(Indicator.class.getName()); + // The metrics needed to calculate the indicator output + private static final List REQUIRED_METRICS = Arrays.asList("conventionalCommits", "commitsWithDescription", + "issuesWithLabels", "gitFlowBranches", "conventionalPullRequests"); + public static final String ID = "conventionsCompliant"; + + // Weights for each metric + private double WEIGHT_CONVENTIONAL_COMMITS = 0.2; + private double WEIGHT_COMMITS_WITH_DESCRIPTION = 0.2; + private double WEIGHT_ISSUES_WITH_LABELS = 0.2; + private double WEIGHT_GIT_FLOW_BRANCHES = 0.2; + private double WEIGHT_CONVENTIONAL_PULL_REQUESTS = 0.2; + + // MARK: - Constructor + /** + * Constructor. Default weights are equal for all metrics (0.2). + */ + public ConventionsCompliantStrategy() { + super(); + } + + /** + * Constructor. Set the weights for each metric. The sum of the weights must be + * equal to 1. + * + * @param weightConventionalCommits the weight for the conventional commits + * metric + * @param weightCommitsWithDescription the weight for the commits with + * description metric + * @param weightIssuesWithLabels the weight for the issues with labels + * metric + * @param weightGitFlowBranches the weight for the git flow branches + * metric + * @param weightConventionalPullRequests the weight for the conventional pull + * requests metric + */ + public ConventionsCompliantStrategy(Double weightConventionalCommits, Double weightCommitsWithDescription, + Double weightIssuesWithLabels, Double weightGitFlowBranches, Double weightConventionalPullRequests) + throws IllegalArgumentException { + // Control that the sum of the weights is equal to 1 + if (weightConventionalCommits + weightCommitsWithDescription + weightIssuesWithLabels + weightGitFlowBranches + + weightConventionalPullRequests != 1) { + throw new IllegalArgumentException("The sum of the weights must be equal to 1"); + } + + // Set the weights + this.WEIGHT_CONVENTIONAL_COMMITS = weightConventionalCommits; + this.WEIGHT_COMMITS_WITH_DESCRIPTION = weightCommitsWithDescription; + this.WEIGHT_ISSUES_WITH_LABELS = weightIssuesWithLabels; + this.WEIGHT_GIT_FLOW_BRANCHES = weightGitFlowBranches; + this.WEIGHT_CONVENTIONAL_PULL_REQUESTS = weightConventionalPullRequests; + } + + // MARK: - Implemented methods + // Calculate the indicator output based on the provided metrics + @Override + public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { + // Attributes + Optional> conventionalCommits = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); + Optional> commitsWithDescription = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); + Optional> issuesWithLabels = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(2).equals(m.getName())).findAny(); + Optional> gitFlowBranches = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(3).equals(m.getName())).findAny(); + Optional> conventionalPullRequests = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(4).equals(m.getName())).findAny(); + ReportItemI indicatorReport = null; + + // Check if the required metrics are present + if (conventionalCommits.isPresent() && commitsWithDescription.isPresent() && issuesWithLabels.isPresent() + && gitFlowBranches.isPresent() && conventionalPullRequests.isPresent()) { + // Calculate the indicator + Double metric1, metric2, metric3, metric4, metric5; + // Initialize the metrics + if (conventionalCommits.get().getValue() >= 0.8499) + metric1 = conventionalCommits.get().getValue(); + else + metric1 = 0.0; + if (commitsWithDescription.get().getValue() >= 0.8499) + metric2 = commitsWithDescription.get().getValue(); + else + metric2 = 0.0; + if (issuesWithLabels.get().getValue() >= 0.8499) + metric3 = issuesWithLabels.get().getValue(); + else + metric3 = 0.0; + if (gitFlowBranches.get().getValue() >= 0.999) + metric4 = 1.0; + else + metric4 = 0.0; + if (conventionalPullRequests.get().getValue() >= 0.8499) + metric5 = conventionalPullRequests.get().getValue(); + else + metric5 = 0.0; + Double indicatorValue = WEIGHT_CONVENTIONAL_COMMITS * metric1 + WEIGHT_COMMITS_WITH_DESCRIPTION * metric2 + + WEIGHT_ISSUES_WITH_LABELS * metric3 + WEIGHT_GIT_FLOW_BRANCHES * metric4 + + WEIGHT_CONVENTIONAL_PULL_REQUESTS * metric5; + + try { + // Create the indicator + indicatorReport = new ReportItem.ReportItemBuilder(ID, indicatorValue) + .metrics(Arrays.asList(conventionalCommits.get(), commitsWithDescription.get(), + issuesWithLabels.get(), gitFlowBranches.get(), conventionalPullRequests.get())) + .indicator(IndicatorState.UNDEFINED).build(); + } catch (ReportItemException e) { + log.info("Error en ReportItemBuilder."); + e.printStackTrace(); + } + + } else { + log.info("No se han proporcionado las métricas necesarias"); + throw new NotAvailableMetricException(REQUIRED_METRICS.toString()); + } + + // Temporarily throw + return indicatorReport; + } + + // Return the metrics required to calculate the indicator output + @Override + public List requiredMetrics() { + return REQUIRED_METRICS; + } + +} diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/TeamsBalanceStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/TeamsBalanceStrategy.java index 68e3345d..f306270a 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/TeamsBalanceStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/TeamsBalanceStrategy.java @@ -28,32 +28,35 @@ public class TeamsBalanceStrategy implements IndicatorStrategy { private static Logger log = Logger.getLogger(Indicator.class.getName()); // M�tricas necesarias para calcular el indicador - private static final List REQUIRED_METRICS = Arrays.asList("teamsBalance","repositories"); + private static final List REQUIRED_METRICS = Arrays.asList("teamsBalance", "repositories"); @Override public ReportItemI calcIndicator(List> metrics) throws NotAvailableMetricException { // Se obtienen y se comprueba que se pasan las m�tricas necesarias para calcular // el indicador. - Optional> teamsBalance = metrics.stream().filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); - Optional> repositories = metrics.stream().filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); + Optional> teamsBalance = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(0).equals(m.getName())).findAny(); + Optional> repositories = metrics.stream() + .filter(m -> REQUIRED_METRICS.get(1).equals(m.getName())).findAny(); ReportItemI indicatorReport = null; - log.info("Teamsbalance: "+teamsBalance.toString()); - log.info("Repositories: "+repositories.toString()); + log.info("Teamsbalance: " + teamsBalance.toString()); + log.info("Repositories: " + repositories.toString()); if (teamsBalance.isPresent() && repositories.isPresent()) { Double teamsBalanceResult; // Se realiza el c�lculo del indicador - teamsBalanceResult = (Double)teamsBalance.get().getValue()*100/repositories.get().getValue(); + teamsBalanceResult = (Double) teamsBalance.get().getValue() * 100 / repositories.get().getValue(); - log.info("teamsBalanceResult: "+teamsBalanceResult.toString()); + log.info("teamsBalanceResult: " + teamsBalanceResult.toString()); try { // Se crea el indicador - //No se entiende por qué el indicador tiene el mismo nombere que una de las métricas - indicatorReport = new ReportItem.ReportItemBuilder("teamsBalanceI", teamsBalanceResult). - metrics(Arrays.asList(teamsBalance.get(), repositories.get())) - .indicator(IndicatorState.UNDEFINED).build(); + // No se entiende por qué el indicador tiene el mismo nombere que una de las + // métricas + indicatorReport = new ReportItem.ReportItemBuilder("teamsBalanceI", teamsBalanceResult) + .metrics(Arrays.asList(teamsBalance.get(), repositories.get())) + .indicator(IndicatorState.UNDEFINED).build(); - log.info("IndicatorReport: "+indicatorReport.toString()); + log.info("IndicatorReport: " + indicatorReport.toString()); } catch (ReportItemException e) { log.info("Error en ReportItemBuilder."); e.printStackTrace(); @@ -64,7 +67,7 @@ public ReportItemI calcIndicator(List> metrics) thro throw new NotAvailableMetricException(REQUIRED_METRICS.toString()); } - return indicatorReport; + return indicatorReport; } @Override @@ -74,4 +77,3 @@ public List requiredMetrics() { return REQUIRED_METRICS; } } - diff --git a/src/main/java/us/muit/fs/a4i/model/entities/Font.java b/src/main/java/us/muit/fs/a4i/model/entities/Font.java index 9350d5c7..f0024ecd 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/Font.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/Font.java @@ -20,22 +20,7 @@ public Font() { */ public Font (String color) { this.font = new java.awt.Font ("Serif", java.awt.Font.PLAIN , 10); - switch(color.toLowerCase()){ - case "red","rojo": - this.color=Color.red; - break; - case "green","verde": - this.color=Color.green; - break; - case "blue","azul": - this.color=Color.blue; - break; - case "orange","naranja": - this.color=Color.ORANGE; - break; - default: - this.color=Color.black; - } + setColor(color); } /** * Todos los Valores personalizados salvo el estilo @@ -46,22 +31,7 @@ public Font (String color) { public Font (String family, int size, String color) { log.info("Creando fuente con valores familia "+family+" tamano "+size+" color "+color); this.font=new java.awt.Font (family.toLowerCase(), java.awt.Font.PLAIN , size); - switch(color.toLowerCase()){ - case "red","rojo": - this.color=Color.RED; - break; - case "green","verde": - this.color=Color.GREEN; - break; - case "blue","azul": - this.color=Color.BLUE; - break; - case "orange","naranja": - this.color=Color.ORANGE; - break; - default: - this.color=Color.BLACK; - } + setColor(color); log.info("Se ha creado fuente con el tipo "+font.getFamily()+" y el color "+color); } /** @@ -73,6 +43,17 @@ public Font (String family, int size, String color) { */ public Font (String family, int style, int size, String color) { this.font=new java.awt.Font (family.toLowerCase(), style , size); + setColor(color); + } + + public Color getColor() { + return this.color; + } + public java.awt.Font getFont() { + return this.font; + } + + private void setColor(String color) { switch(color.toLowerCase()){ case "red","rojo": this.color=Color.RED; @@ -90,11 +71,5 @@ public Font (String family, int style, int size, String color) { this.color=Color.BLACK; } } - - public Color getColor() { - return this.color; - } - public java.awt.Font getFont() { - return this.font; - } + } \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java b/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java index a034e653..2bbef38f 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java @@ -38,26 +38,7 @@ class CheckerTest { static Checker underTest; static String appConfPath; - /** - *

Acciones a realizar antes de ejecutar los tests definidos en esta clase

- * @throws java.lang.Exception - * @see org.junit.jupiter.api.BeforeAll - */ - @BeforeAll - static void setUpBeforeClass() throws Exception { - appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator - + "appConfTest.json"; - } - /** - *

Acciones a realizar después de ejecutar todos los tests de esta clase

- * @throws java.lang.Exception - * @see org.junit.jupiter.api.AfterAll - */ - @AfterAll - static void tearDownAfterClass() throws Exception { - - } /** *

Acciones a realizar antes de cada uno de los tests de esta clase

@@ -70,25 +51,6 @@ void setUp() throws Exception { underTest = Context.getContext().getChecker(); } - /** - *

Acciones a realizar después de cada uno de los tests de esta clase

- * @throws java.lang.Exception - * @see org.junit.jupiter.api.AfterEach - */ - @AfterEach - void tearDown() throws Exception { - - } - - /** - *

Test para el método que establece el fichero de configuración de la - * aplicación

- * {@link us.muit.fs.a4i.config.Checker#setAppMetrics(java.lang.String)}. - */ - @Test - void testSetAppMetrics() { - fail("Not yet implemented"); // TODO - } /** *

Test para verificar el método @@ -112,12 +74,12 @@ void testSetAppMetrics() { */ @Test @Tag("unidad") - @DisplayName("Prueba para el m�todo definedMetric, que verifica si la m�trica est� definida con un tipo determinado y devuelve su configuraci�n") + @DisplayName("Prueba para el método definedMetric, que verifica si la métrica está definida con un tipo determinado y devuelve su configuración") void testDefinedMetric() { // Creo valores Mock para verificar si comprueba bien el tipo // Las m�tricas del test son de enteros, as� que creo un entero y un string (el - // primero no dar� problemas el segundo s�) + // primero no dar� problemas el segundo sí) Integer valOKMock = Integer.valueOf(3); String valKOMock = "KO"; HashMap returnedMap = null; @@ -140,39 +102,10 @@ void testDefinedMetric() { assertNull(underTest.getMetricConfiguration().definedMetric("watchers", valKOMock.getClass().getName()), "Deber�a ser nulo, la m�trica est� definida para Integer"); } catch (FileNotFoundException e) { - fail("El fichero est� en la carpeta resources"); + fail("El fichero NO est� en la carpeta resources"); e.printStackTrace(); } - - // Ahora establezco el fichero de configuraci�n de la aplicaci�n, con un nombre - // de fichero que no existe - Context.setAppRI("pepe"); - try { - // Busco una m�trica que se que no est� en la configuraci�n de la api - returnedMap = underTest.getMetricConfiguration().definedMetric("downloads", valOKMock.getClass().getName()); - fail("Deber�a lanzar una excepci�n porque intenta buscar en un fichero que no existe"); - } catch (FileNotFoundException e) { - log.info("Lanza la excepci�n adecuada, FileNotFoud"); - } catch (Exception e) { - fail("Lanza la excepci�n equivocada " + e); - } - - // Ahora establezco un fichero de configuraci�n de la aplicaci�n que s� existe - Context.setAppRI(appConfPath); - try { - // Busco una m�trica que se que no est� en la configuraci�n de la api pero s� en - // la de la aplicaci�n - log.info("Busco la m�trica llamada downloads"); - returnedMap = underTest.getMetricConfiguration().definedMetric("downloads", valOKMock.getClass().getName()); - assertNotNull(returnedMap, "Deber�a devolver un hashmap, la m�trica est� definida"); - assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); - assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); - } catch (FileNotFoundException e) { - fail("No deber�a devolver esta excepci�n"); - } catch (Exception e) { - fail("Lanza una excepci�n no reconocida " + e); - } - + } /** @@ -220,37 +153,7 @@ void testDefinedIndicator() { e.printStackTrace(); } - // Ahora establezco el fichero de configuraci�n de la aplicaci�n, con un nombre - // de fichero que no existe - Context.setAppRI("pepe"); - try { - // Busco un indicador que se que no est� en la configuraci�n de la api - returnedMap = underTest.getIndicatorConfiguration().definedIndicator("pullReqGlory", - valOKMock.getClass().getName()); - fail("Deber�a lanzar una excepci�n porque intenta buscar en un fichero que no existe"); - } catch (FileNotFoundException e) { - log.info("Lanza la excepci�n adecuada, FileNotFoud"); - } catch (Exception e) { - fail("Lanza la excepci�n equivocada " + e); - } - - // Ahora establezco un fichero de configuraci�n de la aplicaci�n que s� existe - Context.setAppRI(appConfPath); - try { - // Busco una m�trica que se que no est� en la configuraci�n de la api pero s� en - // la de la aplicaci�n - log.info("Busco el indicador llamado pullReqGlory"); - returnedMap = underTest.getIndicatorConfiguration().definedIndicator("pullReqGlory", - valOKMock.getClass().getName()); - assertNotNull(returnedMap, "Deber�a devolver un hashmap, el indicador est� definido"); - assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); - assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); - } catch (FileNotFoundException e) { - fail("No deber�a devolver esta excepci�n"); - } catch (Exception e) { - fail("Lanza una excepci�n no reconocida " + e); - } - + } } diff --git a/src/test/java/us/muit/fs/a4i/test/config/CheckerTest2.java b/src/test/java/us/muit/fs/a4i/test/config/CheckerTest2.java new file mode 100644 index 00000000..97d48fcf --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/config/CheckerTest2.java @@ -0,0 +1,200 @@ +package us.muit.fs.a4i.test.config; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.HashMap; +import java.util.logging.Logger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.config.Checker; +import us.muit.fs.a4i.config.Context; + +/** + * Verificación de Checker para fichero de configuración de métricas + * personalizado + */ +class CheckerTest2 { + private static Logger log = Logger.getLogger(CheckerTest.class.getName()); + static Checker underTest; + static String appConfPath; + + /** + *

+ * Acciones a realizar antes de ejecutar los tests definidos en esta clase + *

+ * + * @throws java.lang.Exception + * @see org.junit.jupiter.api.BeforeAll + */ + @BeforeAll + static void setUpBeforeClass() throws Exception { + appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + + "appConfTest.json"; + Context.setAppRI(appConfPath); + } + + /** + *

+ * Acciones a realizar después de ejecutar todos los tests de esta clase + *

+ * + * @throws java.lang.Exception + * @see org.junit.jupiter.api.AfterAll + */ + @AfterAll + static void tearDownAfterClass() throws Exception { + + } + + /** + *

+ * Acciones a realizar antes de cada uno de los tests de esta clase + *

+ * + * @throws java.lang.Exception + * @see org.junit.jupiter.api.BeforeEach + */ + @BeforeEach + void setUp() throws Exception { + // Creo el objeto bajo test, un Checker + underTest = Context.getContext().getChecker(); + } + + /** + *

+ * Acciones a realizar después de cada uno de los tests de esta clase + *

+ * + * @throws java.lang.Exception + * @see org.junit.jupiter.api.AfterEach + */ + @AfterEach + void tearDown() throws Exception { + + } + + /** + *

+ * Test para verificar que el método que establece el fichero de configuración de la + * aplicación devuelve la excepción adecuada si no encuentra el fichero + *

+ * {@link us.muit.fs.a4i.config.Checker#setAppMetrics(java.lang.String)}. + */ + @Test + void testSetAppMetrics() { + HashMap returnedMap = null; + Integer valOKMock = Integer.valueOf(3); + String valKOMock = "KO"; + // Ahora establezco el fichero de configuraci�n de la aplicaci�n, con un nombre + // de fichero que no existe + Context.setAppRI("pepe"); + try { + // Busco una m�trica que se que no est� en la configuraci�n de la api + returnedMap = underTest.getMetricConfiguration().definedMetric("downloads", valOKMock.getClass().getName()); + fail("Antes de llegar aquí debería lanzar una excepción, porque intenta buscar en un fichero que no existe"); + } catch (FileNotFoundException e) { + log.info("Lanza la excepci�n adecuada, FileNotFoud"); + } catch (Exception e) { + fail("Lanza la excepci�n equivocada " + e); + } + //Vuelvo a dejar el fichero de configuración correcto + Context.setAppRI(appConfPath); + } + + /** + *

+ * Test para verificar el método + * {@link us.muit.fs.a4i.config.Checker#definedMetric(java.lang.String, java.lang.String)}. + * Si la métrica está definida y el tipo de valor que se quiere establecer es el + * adecuado debe devolver un hashmap con los datos de la métrica, usando como + * clave las etiquetas: + *

    + *
  • description
  • + *
  • unit
  • + *
+ * Las métricas pueden estar definidas en el fichero de configuración de la api + * (a4iDefault.json) o en otro fichero configurado por la aplicación cliente. + * Para los test este fichero es appConfTest.json y se guarda junto al código de + * test, en la carpeta resources + * + * @see org.junit.jupiter.api.Tag + * @see org.junit.jupiter.api.Test + * @see org.junit.jupiter.api.DisplayName * + *

+ */ + @Test + @Tag("unidad") + @DisplayName("Prueba para el método definedMetric, que verifica si la métrica está definida con un tipo determinado y devuelve su configuración") + void testDefinedMetric() { + + // Creo valores Mock para verificar si comprueba bien el tipo + // Las m�tricas del test son de enteros, as� que creo un entero y un string (el + // primero no dar� problemas el segundo sí) + Integer valOKMock = Integer.valueOf(3); + String valKOMock = "KO"; + HashMap returnedMap = null; + + + try { + // Busco una m�trica que se que no est� en la configuraci�n de la api pero s� en + // la de la aplicaci�n + log.info("Busco la m�trica llamada downloads"); + returnedMap = underTest.getMetricConfiguration().definedMetric("downloads", valOKMock.getClass().getName()); + assertNotNull(returnedMap, "Deber�a devolver un hashmap, la m�trica est� definida"); + assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); + } catch (FileNotFoundException e) { + fail("No deber�a devolver esta excepci�n"); + } catch (Exception e) { + fail("Lanza una excepci�n no reconocida " + e); + } + + } + + /** + * @see org.junit.jupiter.api.Tag + * @see org.junit.jupiter.api.Test + * @see org.junit.jupiter.api.DisplayName + * + * Test para el m�todo + * {@link us.muit.fs.a4i.config.Checker#definedIndicator(java.lang.String, java.lang.String)}. + */ + @Test + @Tag("unidad") + @DisplayName("Prueba para el m�todo definedIndicator, que verifica si el indicador est� definido con un tipo determinado y devuelve su configuraci�n") + void testDefinedIndicator() { + + // Creo valores Mock para verificar si comprueba bien el tipo + // Las m�tricas del test son de enteros, as� que creo un entero y un string (el + // primero no dar� problemas el segundo s�) + Double valOKMock = Double.valueOf(0.3); + String valKOMock = "KO"; + HashMap returnedMap = null; + + try { + // Busco una m�trica que se que no est� en la configuraci�n de la api pero s� en + // la de la aplicaci�n + log.info("Busco el indicador llamado pullReqGlory"); + returnedMap = underTest.getIndicatorConfiguration().definedIndicator("pullReqGlory", + valOKMock.getClass().getName()); + assertNotNull(returnedMap, "Deber�a devolver un hashmap, el indicador est� definido"); + assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); + } catch (FileNotFoundException e) { + fail("No deber�a devolver esta excepci�n"); + } catch (Exception e) { + fail("Lanza una excepci�n no reconocida " + e); + } + + } + +} \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java index ee9a2a07..bc77ce48 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java @@ -19,6 +19,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import us.muit.fs.a4i.config.Context; @@ -168,8 +169,8 @@ void testGetDefaultFont() { * Test method for {@link us.muit.fs.a4i.config.Context#getMetricFont()}. * @throws IOException */ - @Test - + @DisplayName("Verificación lectura de configuración de fuente de métricas") + @Test void testGetMetricFont(){ try { @@ -192,6 +193,7 @@ void testGetMetricFont(){ * Test method for * {@link us.muit.fs.a4i.config.Context#getIndicatorFont(us.muit.fs.a4i.model.entities.Indicator.State)}. */ + @DisplayName("Verificación lectura de configuración de fuente de indicadores") @Test void testGetIndicatorFont() { try { @@ -232,6 +234,7 @@ void testGetIndicatorFont() { * Este método de verificación está incompleto, deberá ser completado commo ejercicio * Verificar que los nombres son correctos */ + @DisplayName("Verificación obtención nombre de propiedades configuradas") @Test void testGetPropertiesNames() throws IOException { log.info(Context.getContext().getPropertiesNames().toString()); From a546b2e4d019da510aafeb13e3c78544bb1e5922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Mon, 17 Mar 2025 10:03:01 +0100 Subject: [PATCH 089/100] =?UTF-8?q?Preparando=20pr=C3=A1ctica=20de=20prueb?= =?UTF-8?q?as=20unidad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fs/a4i/config/IndicatorConfiguration.java | 109 ++++++++------- .../fs/a4i/config/MetricConfiguration.java | 21 +++ .../muit/fs/a4i/test/config/CheckerTest.java | 2 +- .../config/IndicatorConfigurationTest.java | 55 +++++--- .../test/config/MetricConfigurationTest.java | 126 ++++++++++++++++++ .../muit/fs/a4i/test/config/MetricInfo.java | 46 ------- src/test/resources/appConfTest.json | 16 ++- 7 files changed, 264 insertions(+), 111 deletions(-) create mode 100644 src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest.java delete mode 100644 src/test/java/us/muit/fs/a4i/test/config/MetricInfo.java 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 59e2fd89..8240b18d 100644 --- a/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java @@ -28,10 +28,10 @@ public class IndicatorConfiguration implements IndicatorConfigurationI { private static Logger log = Logger.getLogger(Checker.class.getName()); - - public String CRITICAL_LIMIT = "limits.critical"; - public String WARNING_LIMIT = "limits.warning"; - public String OK_LIMIT = "limits.ok"; + + private String CRITICAL_LIMIT = "limits.critical"; + private String WARNING_LIMIT = "limits.warning"; + private String OK_LIMIT = "limits.ok"; @Override /** @@ -69,6 +69,16 @@ public HashMap definedIndicator(String name, String type) throws return indicatorDefinition; } + /** + * Busca el indicador con un nombre y un tipo determinado y devuelve sus + * características en un mapa + * + * @param indicatorName + * @param indicatorType + * @param isr + * @return + * @throws FileNotFoundException + */ private HashMap isDefinedIndicator(String indicatorName, String indicatorType, InputStreamReader isr) throws FileNotFoundException { HashMap indicatorDefinition = null; @@ -90,15 +100,17 @@ private HashMap isDefinedIndicator(String indicatorName, String log.info("tipo: " + indicators.get(i).asJsonObject().getString("type")); if (indicators.get(i).asJsonObject().getString("type").equals(indicatorType)) { indicatorDefinition = new HashMap(); + indicatorDefinition.put("name", indicatorName); + indicatorDefinition.put("type", indicatorType); indicatorDefinition.put("description", indicators.get(i).asJsonObject().getString("description")); indicatorDefinition.put("unit", indicators.get(i).asJsonObject().getString("unit")); - + JsonObject limits = indicators.get(i).asJsonObject().getJsonObject("limits"); int okLimit = 0; int warningLimit = 0; int criticalLimit = 0; - - if(limits != null) { + + if (limits != null) { okLimit = limits.getInt("ok"); warningLimit = limits.getInt("warning"); criticalLimit = limits.getInt("critical"); @@ -107,8 +119,8 @@ private HashMap isDefinedIndicator(String indicatorName, String indicatorDefinition.put(CRITICAL_LIMIT, Integer.toString(criticalLimit)); } else { log.info("El fichero de configuración no especifica límites para este indicador"); - } - + } + } } @@ -163,50 +175,55 @@ public List listAllIndicators() throws FileNotFoundException { } @Override - public IndicatorState getIndicatorState(ReportItemI indicator){ - //TODO: change indicator definitions key name to a constant. - + public IndicatorState getIndicatorState(ReportItemI indicator) { + // TODO: change indicator definitions key name to a constant. + //Necesita optimización del cálculo de estado String indicatorType = indicator.getValue().getClass().getName(); - + IndicatorState finalState = IndicatorState.UNDEFINED; try { - HashMap indicatorDefinition = definedIndicator(indicator.getName(), indicatorType); - - - String criticalLimit = indicatorDefinition.get(CRITICAL_LIMIT); - String warningLimit = indicatorDefinition.get(WARNING_LIMIT); - String okLimit = indicatorDefinition.get(OK_LIMIT); - - // Si no se han encontrado límites definidos para ese indicador el estado es UNDEFINED. - if(criticalLimit != null && warningLimit != null && okLimit != null) { - // Se tienen en cuenta los posibles tipos de indicadores para compararlos. - if(indicatorType.equals(Integer.class.getName())) { - Integer value = (Integer) indicator.getValue(); - - if(value >= Integer.parseInt(criticalLimit)) finalState = IndicatorState.CRITICAL; - else if(value <=Integer.parseInt(okLimit)) finalState = IndicatorState.OK; - else finalState = IndicatorState.WARNING; - - } else if(indicatorType.equals(Double.class.getName())) { - Double value = (Double) indicator.getValue(); - - if(value >= Integer.parseInt(criticalLimit)) finalState = IndicatorState.CRITICAL; - else if(value <= Integer.parseInt(okLimit)) finalState = IndicatorState.OK; - else finalState = IndicatorState.WARNING; - + HashMap indicatorDefinition = definedIndicator(indicator.getName(), indicatorType); + + String criticalLimit = indicatorDefinition.get(CRITICAL_LIMIT); + String warningLimit = indicatorDefinition.get(WARNING_LIMIT); + String okLimit = indicatorDefinition.get(OK_LIMIT); + + // Si no se han encontrado límites definidos para ese indicador el estado es + // UNDEFINED. + if (criticalLimit != null && warningLimit != null && okLimit != null) { + // Se tienen en cuenta los posibles tipos de indicadores para compararlos. + if (indicatorType.equals(Integer.class.getName())) { + Integer value = (Integer) indicator.getValue(); + + if (value >= Integer.parseInt(criticalLimit)) + finalState = IndicatorState.CRITICAL; + else if (value <= Integer.parseInt(okLimit)) + finalState = IndicatorState.OK; + else + finalState = IndicatorState.WARNING; + + } else if (indicatorType.equals(Double.class.getName())) { + Double value = (Double) indicator.getValue(); + + if (value >= Integer.parseInt(criticalLimit)) + finalState = IndicatorState.CRITICAL; + else if (value <= Integer.parseInt(okLimit)) + finalState = IndicatorState.OK; + else + finalState = IndicatorState.WARNING; + + } + } else { + log.warning("No se han encontrado límites definidos para el indicador: " + indicator.getName()); + finalState = IndicatorState.UNDEFINED; } - } else { - log.warning("No se han encontrado límites definidos para el indicador: " + indicator.getName()); - finalState = IndicatorState.UNDEFINED; - } - - }catch(Exception e){ - + + } catch (Exception e) { + e.printStackTrace(); } - log.info("El estado del Indicador "+indicator.getName()+" es "+finalState.toString()); + log.info("El estado del Indicador " + indicator.getName() + " es " + finalState.toString()); return finalState; } - } diff --git a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java index 9255bd1c..d602ad4d 100644 --- a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java @@ -18,6 +18,17 @@ public class MetricConfiguration implements MetricConfigurationI { private static Logger log = Logger.getLogger(Checker.class.getName()); + /** + * Método privado que verifica si una métrica está definida en un fichero de + * configuración determinado y si efectivamente tiene el tipo esperado + * + * @param metricName String con el nombre de la métrica a buscar + * @param metricType String con el tipo de la métrica + * @param isr InputStreamReader del fichero de configuración + * @return Mapa de parámetros de la métrica, siempre que exista y sea del tipo + * especificado + * @throws FileNotFoundException + */ private HashMap isDefinedMetric(String metricName, String metricType, InputStreamReader isr) throws FileNotFoundException { @@ -40,6 +51,8 @@ private HashMap isDefinedMetric(String metricName, String metric log.info("tipo: " + metrics.get(i).asJsonObject().getString("type")); if (metrics.get(i).asJsonObject().getString("type").equals(metricType)) { metricDefinition = new HashMap(); + metricDefinition.put("name", metricName); + metricDefinition.put("type", metricType); metricDefinition.put("description", metrics.get(i).asJsonObject().getString("description")); metricDefinition.put("unit", metrics.get(i).asJsonObject().getString("unit")); } @@ -50,6 +63,14 @@ private HashMap isDefinedMetric(String metricName, String metric return metricDefinition; } + /** + * Obtiene los datos de una métrica en un fichero de configuración + * + * @param metricName String con el nombre de la métrica buscada + * @param isr InputStreamReader del fichero de configuración + * @return Mapa con las características de la métrica + * @throws FileNotFoundException + */ private HashMap getMetric(String metricName, InputStreamReader isr) throws FileNotFoundException { HashMap metricDefinition = null; diff --git a/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java b/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java index a034e653..e6225aa5 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java @@ -112,7 +112,7 @@ void testSetAppMetrics() { */ @Test @Tag("unidad") - @DisplayName("Prueba para el m�todo definedMetric, que verifica si la m�trica est� definida con un tipo determinado y devuelve su configuraci�n") + @DisplayName("Prueba para el método definedMetric, que verifica si la métrica est� definida con un tipo determinado y devuelve su configuración") void testDefinedMetric() { // Creo valores Mock para verificar si comprueba bien el tipo diff --git a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java index 4d947625..d67e0a83 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java @@ -80,7 +80,6 @@ void tearDown() throws Exception { log.info("Acabo de ejecutar un test definido en esta clase"); } - @Test void testDefinedIndicator() { // Creo un par de variables, que me servirán de valores para verificar si comprueba bien el tipo @@ -91,22 +90,37 @@ void testDefinedIndicator() { HashMap returnedMap = null; // Primero, sin fichero de configuración de aplicación try { + // Consulta un indicador no definido, con valor de tipo entero // debe devolver null, no está definido - log.info("Busco el indicador llamado pullReqGlory"); - returnedMap = underTest.definedIndicator("pullReqGlory", valOKMock.getClass().getName()); - assertNull(returnedMap, "Debería ser nulo, el indicador pullReqGlory no está definido"); + log.info("Busco el indicador llamado noexiste"); + returnedMap = underTest.definedIndicator("noexiste", valOKMock.getClass().getName()); + assertNull(returnedMap, "Debería ser nulo, el indicador noexiste no está definido"); + /* + * Indicador que existe en el fichero de configuración por defecto + * { + "name": "issuesProgress", + "type": "java.lang.Double", + "description": "Ratio de issues cerrados frente a totales", + "unit": "ratio", + "limits": { + "ok": 2, + "warning": 4, + "critical": 6 + } + } + */ // Busco el indicador overdued con valor double, no debería dar problemas - log.info("Busco el indicador overdued"); - returnedMap = underTest.definedIndicator("overdued", valOKMock.getClass().getName()); - assertNotNull(returnedMap, "Debería devolver un hashmap, el indicador overdued está definido"); + log.info("Busco el indicador issuesProgress"); + returnedMap = underTest.definedIndicator("issuesProgress", valOKMock.getClass().getName()); + assertNotNull(returnedMap, "Debería devolver un hashmap, el indicador issuesProgress está definido"); assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); // Se comprueba que los indicadores incluyen los limites definidos - assertTrue(returnedMap.containsKey(underTest.OK_LIMIT), "La clave correspondiente al limite del estado OK tiene que estar en el mapa"); - assertTrue(returnedMap.containsKey(underTest.WARNING_LIMIT), "La clave correspondiente al limite del estado WARNING tiene que estar en el mapa"); - assertTrue(returnedMap.containsKey(underTest.CRITICAL_LIMIT), "La clave correspondiente al limite del estado CRITICAL tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("limits.ok"), "La clave correspondiente al limite del estado OK tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("limits.warning"), "La clave correspondiente al limite del estado WARNING tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("limits.critical"), "La clave correspondiente al limite del estado CRITICAL tiene que estar en el mapa"); // Busco una métrica que existe pero con un tipo incorrecto assertNull(underTest.definedIndicator("overdued", valKOMock.getClass().getName()), "Debería ser nulo, el indicador overdued está definido para Double"); @@ -114,6 +128,16 @@ void testDefinedIndicator() { fail("El fichero está en la carpeta resources"); e.printStackTrace(); } + } + + @Test + void testDefinedIndicatorCustom() { + // Creo un par de variables, que me servirán de valores para verificar si comprueba bien el tipo + // Las métricas del test son de tipo entero, así que creo un entero y un string + // (el primero no dará problemas el segundo sí) + Double valOKMock = Double.valueOf(0.3); + String valKOMock = "KO"; + HashMap returnedMap = null; // Ahora establezco el fichero de configuración de la aplicación, con un // nombre de fichero que no existe Context.setAppRI("pepe"); @@ -123,14 +147,15 @@ void testDefinedIndicator() { fail("Debería lanzar una excepción porque intenta buscar en un fichero que no existe"); } catch (FileNotFoundException e) { log.info("Lanza la excepción adecuada, FileNotFoud"); + Context.setAppRI(appConfPath); } catch (Exception e) { fail("Lanza la excepción equivocada " + e); } - // Ahora establezco un fichero de configuración de la aplicación que sí - // existe + // Ahora establezco un fichero de configuración de la aplicación Context.setAppRI(appConfPath); try { + // Busco una métrica que se que no está en la configuración de la api pero // sí en la de la aplicación log.info("Busco el indicador llamado pullReqGlory"); @@ -139,9 +164,9 @@ void testDefinedIndicator() { assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); // Se comprueba que los indicadores incluyen los limites definidos - assertTrue(returnedMap.containsKey(underTest.OK_LIMIT), "La clave correspondiente al limite del estado OK tiene que estar en el mapa"); - assertTrue(returnedMap.containsKey(underTest.WARNING_LIMIT), "La clave correspondiente al limite del estado WARNING tiene que estar en el mapa"); - assertTrue(returnedMap.containsKey(underTest.CRITICAL_LIMIT), "La clave correspondiente al limite del estado CRITICAL tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("limits.ok"), "La clave correspondiente al limite del estado OK tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("limits.warning"), "La clave correspondiente al limite del estado WARNING tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("limits.critical"), "La clave correspondiente al limite del estado CRITICAL tiene que estar en el mapa"); } catch (FileNotFoundException e) { fail("No debería devolver esta excepción"); } catch (Exception e) { diff --git a/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest.java b/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest.java new file mode 100644 index 00000000..1fb05580 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest.java @@ -0,0 +1,126 @@ +package us.muit.fs.a4i.test.config; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.logging.Logger; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.config.Checker; +import us.muit.fs.a4i.config.Context; +import us.muit.fs.a4i.config.MetricConfiguration; +import us.muit.fs.a4i.config.MetricConfigurationI; + +class MetricConfigurationTest { + private static Logger log = Logger.getLogger(MetricConfigurationTest.class.getName()); + static MetricConfiguration underTest; + + /** + *

+ * Acciones a realizar antes de ejecutar los tests definidos en esta clase. + *

+ * + * @throws java.lang.Exception + * @see org.junit.jupiter.api.BeforeAll + */ + @BeforeAll + static void setUpBeforeClass() throws Exception { + underTest = new MetricConfiguration(); + } + + @Test + void testDefinedMetric() { + try { + /* + * En el fichero por defecto la métrica issues está definida del siguiente modo + * { "name": "issues", "type": "java.lang.Integer", "description": + * "Tareas totales", "unit": "issues" } + */ + // Primero se busca la métrica con el tipo definido + HashMap metricInfo = underTest.definedMetric("issues", "java.lang.Integer"); + assertEquals("issues", metricInfo.get("name"), "No se ha leído bien el nombre de la métrica"); + assertEquals("java.lang.Integer", metricInfo.get("type"), "No se ha leído bien el tipo de la métrica"); + assertEquals("Tareas totales",metricInfo.get("description"), + "No se ha leído bien la descripción de la métrica"); + assertEquals(metricInfo.get("unit"), "issues", "No se ha leído bien las unidades de la métrica"); + + // ahora busco con un tipo incorrecto + // Primero se busca la métrica con el tipo definido + metricInfo = underTest.definedMetric("issues", "java.lang.String"); + assertNull(metricInfo, "No debería haber localizado la métrica"); + } catch (IOException e) { + + e.printStackTrace(); + fail("No debería devolver esta excepción"); + } + } + + /** + * Verifica la localización de una métrica que existe en el fichero de + * configuración por defecto + */ + @DisplayName("Verificación de lectura métrica disponible en configuración por defecto") + @Test + void testGetMetricInfoOK() { + try { + /* + * En el fichero por defecto la métrica issues está definida del siguiente modo + * { "name": "issues", "type": "java.lang.Integer", "description": + * "Tareas totales", "unit": "issues" } + */ + // Primero se busca una métrica que existe + HashMap metricInfo = underTest.getMetricInfo("issues"); + assertEquals( "issues", metricInfo.get("name"),"No se ha leído bien el nombre de la métrica"); + assertEquals("java.lang.Integer", metricInfo.get("type"), "No se ha leído bien el tipo de la métrica"); + assertEquals("Tareas totales",metricInfo.get("description"), + "No se ha leído bien la descripción de la métrica"); + assertEquals("issues", metricInfo.get("unit"),"No se ha leído bien las unidades de la métrica"); + } catch (IOException e) { + + e.printStackTrace(); + fail("No debería devolver esta excepción"); + } + } + + /** + * Verifica la localización de una métrica que NO existe en el fichero de + * configuración por defecto + */ + @DisplayName("Verificación de lectura métrica no existente") + @Test + void testGetMetricInfoKO() { + try { + /* + * En el fichero por defecto la métrica noexiste no existe + */ + + HashMap metricInfo = underTest.getMetricInfo("noexiste"); + assertNull(metricInfo, "El mapa no debe haberse creado"); + } catch (IOException e) { + fail("Lanza excepcion indebida, no localiza el fichero"); + e.printStackTrace(); + + } + } + + @Test + void testListAllMetrics() { + /* + * Actualmente hay 39 métricas en el fichero de configuración por defecto + * (17/3/25) + */ + try { + assertEquals(39, underTest.listAllMetrics().size(), "El número de métricas leídas no es correcto"); + } catch (FileNotFoundException e) { + fail("Lanza excepción indebida, no localiza el fichero"); + e.printStackTrace(); + } + } + +} diff --git a/src/test/java/us/muit/fs/a4i/test/config/MetricInfo.java b/src/test/java/us/muit/fs/a4i/test/config/MetricInfo.java deleted file mode 100644 index edf12c8f..00000000 --- a/src/test/java/us/muit/fs/a4i/test/config/MetricInfo.java +++ /dev/null @@ -1,46 +0,0 @@ -/** - * - */ -package us.muit.fs.a4i.test.config; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.IOException; -import java.util.HashMap; - -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; - -import us.muit.fs.a4i.config.Checker; -import us.muit.fs.a4i.config.Context; - -/** - * @author Isabel Román - * - */ -class MetricInfo { - - /** - * Test method for {@link us.muit.fs.a4i.config.Context#getChecker()}. - */ - @Test - @Tag("Integracion") - void testGetChecker() { - try { - Checker checker = Context.getContext().getChecker(); - HashMap metricInfo = checker.getMetricConfiguration().getMetricInfo("issues"); - assertEquals(metricInfo.get("name"), "issues", "No se ha leído bien el nombre de la métrica"); - assertEquals(metricInfo.get("type"), "java.lang.Integer", "No se ha leído bien el tipo de la métrica"); - assertEquals(metricInfo.get("description"), "Tareas sin finalizar en el repositorio", - "No se ha leído bien la descripción de la métrica"); - assertEquals(metricInfo.get("unit"), "issues", "No se ha le�do bien las unidades de la m�trica"); - } catch (IOException e) { - - e.printStackTrace(); - fail("No debería devolver esta excepción"); - } - - } - -} diff --git a/src/test/resources/appConfTest.json b/src/test/resources/appConfTest.json index fdfd7b59..ae5c655c 100644 --- a/src/test/resources/appConfTest.json +++ b/src/test/resources/appConfTest.json @@ -5,7 +5,7 @@ "type": "java.lang.Integer", "description": "Descargas realizadas", "unit": "downloads" - }, + }, { "name": "comments", "type": "java.lang.Integer", @@ -18,13 +18,23 @@ "name": "pullReqGlory", "type": "java.lang.Double", "description": "Ratio de pull request aceptados frente a solicitados", - "unit": "ratio" + "unit": "ratio", + "limits": { + "ok": 2, + "warning": 4, + "critical": 6 + } }, { "name": "commentsInterest", "type": "java.lang.Double", "description": "Ratio de comentarios con más de 1 respuesta frente al número total de comentarios", - "unit": "ratio" + "unit": "ratio", + "limits": { + "ok": 2, + "warning": 4, + "critical": 6 + } } ] } \ No newline at end of file From 6d37638a03c8546d7054dcea291a027f8adf3a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Mon, 17 Mar 2025 11:41:08 +0100 Subject: [PATCH 090/100] Mejora de la arquitectura paquete config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preparando práctica de pruebas unidad Context se encarga de construir los objetos de consulta de métricas e indicadores. Context es la única responsable del contexto de la API Para las clases de consulta de métricas e indicadores se han creado por separado el caso de fichero configurado por el cliente y no (test y test2) Se han separado en métodos de test individuales cada caso (se ha dejado uno complejo en IndicadorTest para mostrar la diferencia) Revisado RepositoryCalculatorTest --- .../java/us/muit/fs/a4i/config/Checker.java | 10 +- .../java/us/muit/fs/a4i/config/Context.java | 28 +- .../fs/a4i/config/IndicatorConfiguration.java | 24 +- .../fs/a4i/config/MetricConfiguration.java | 31 +- .../fs/a4i/control/IndicatorsCalculator.java | 4 +- .../RepositoryCalculator.java | 28 +- .../control/{ => managers}/ReportManager.java | 8 +- .../us/muit/fs/a4i/control/package-info.java | 1 + .../control/strategies/FixTimeStrategy.java | 4 +- .../strategies/TeamsBalanceStrategy.java | 2 +- .../muit/fs/a4i/model/entities/ReportI.java | 2 +- .../fs/a4i/model/remote/GitHubEnquirer.java | 32 +- .../remote/GitHubRepositoryEnquirer.java | 177 ++--------- .../fs/a4i/model/remote/RemoteEnquirer.java | 4 +- src/main/resources/a4iDefault.json | 6 + src/main/resources/log.properties | 11 + .../muit/fs/a4i/test/config/CheckerTest.java | 160 ---------- .../muit/fs/a4i/test/config/CheckerTest2.java | 200 ------------- .../muit/fs/a4i/test/config/ContextTest.java | 153 ++++++---- .../muit/fs/a4i/test/config/ContextTest2.java | 171 +++++------ .../config/IndicatorConfigurationTest.java | 201 +++++++------ .../config/IndicatorConfigurationTest2.java | 86 ++++++ .../test/config/MetricConfigurationTest.java | 239 ++++++++++++--- .../test/config/MetricConfigurationTest2.java | 277 ++++++++++++++++++ .../a4i/test/control/ReportManagerTest.java | 201 ------------- .../calculators/RepositoryCalculatorTest.java | 211 +++++++++++++ .../control/managers/ReportManagerTest.java | 51 ++++ .../ConventionsCompliantStrategyTest.java | 21 -- .../strategies/FixTimeStrategyTest.java | 2 +- .../strategies/RepositoryCalculatorTest.java | 252 ---------------- .../remote/GitHubRepositoryEnquirerTest.java | 9 + src/test/resources/excelTest.xlsx | Bin 8577 -> 8658 bytes src/test/resources/log.properties | 11 +- 33 files changed, 1258 insertions(+), 1359 deletions(-) rename src/main/java/us/muit/fs/a4i/control/{strategies => calculators}/RepositoryCalculator.java (77%) rename src/main/java/us/muit/fs/a4i/control/{ => managers}/ReportManager.java (96%) delete mode 100644 src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java delete mode 100644 src/test/java/us/muit/fs/a4i/test/config/CheckerTest2.java create mode 100644 src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest2.java create mode 100644 src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest2.java delete mode 100644 src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java create mode 100644 src/test/java/us/muit/fs/a4i/test/control/calculators/RepositoryCalculatorTest.java create mode 100644 src/test/java/us/muit/fs/a4i/test/control/managers/ReportManagerTest.java delete mode 100644 src/test/java/us/muit/fs/a4i/test/control/strategies/RepositoryCalculatorTest.java diff --git a/src/main/java/us/muit/fs/a4i/config/Checker.java b/src/main/java/us/muit/fs/a4i/config/Checker.java index f7514b12..bda1088d 100644 --- a/src/main/java/us/muit/fs/a4i/config/Checker.java +++ b/src/main/java/us/muit/fs/a4i/config/Checker.java @@ -19,10 +19,12 @@ public class Checker { private MetricConfigurationI metricConf; private IndicatorConfigurationI indiConf; - - Checker() { - this.metricConf = new MetricConfiguration(); - this.indiConf = new IndicatorConfiguration(); +/** + * El constructor recibe los objetos configuradores, que podrán ser diferentes según el contexto. + */ + Checker(MetricConfigurationI metricConf, IndicatorConfigurationI indiConf) { + this.metricConf = metricConf; + this.indiConf = indiConf; } /** diff --git a/src/main/java/us/muit/fs/a4i/config/Context.java b/src/main/java/us/muit/fs/a4i/config/Context.java index 2a11d0d2..d304c92f 100644 --- a/src/main/java/us/muit/fs/a4i/config/Context.java +++ b/src/main/java/us/muit/fs/a4i/config/Context.java @@ -51,7 +51,8 @@ * configuradas sólo por la clase context *

*

- * Sigue el patrón singleton + * Sigue el patrón singleton. Context tiene la responsabilidad de crear Checker, es decir, debe seleccionar el IndicatorConfiguration y el MetricConfiguration adecuados al contexto. + * Se crean la primera vez que se solicita Context, por lo que los ficheros de configuración no se pueden cambiar "en caliente", sólo al inicio de la ejecución *

* * @author Isabel Román @@ -102,11 +103,10 @@ public class Context { * * @throws IOException */ - private Context() throws IOException { - setProperties(); + private Context(Checker checker) throws IOException { + setProperties(); + this.checker = checker; log.info("Propiedades del contexto establecidas"); - checker = new Checker(); - log.info("Checker creado"); } /** @@ -119,6 +119,7 @@ private Context() throws IOException { * la aplicación cliente */ public static void setAppRI(String filename) { + log.fine("Fichero configuración de métricas establecido "+filename); appFile = filename; } @@ -158,10 +159,23 @@ public static Context getContext() throws IOException { * Si no está creada crea la instancia única con las propiedades por defecto */ if (contextInstance == null) { - contextInstance = new Context(); + Checker checker=createChecker(); + contextInstance = new Context(checker); } return contextInstance; } + /** + * Método responsable de seleccionar el IndicatorConfiguration y el MetricConfiguration adecuado al contexto + * DEUDA TÉCNICA: + * En esta versión al crear el checker no hay ninguna comprobación de contexto, pero en el futuro deberá primero examinarse las propiedas de configuración para decidir que configuradores hay que crear. + * @return + */ + static private Checker createChecker(){ + IndicatorConfigurationI indConf=new IndicatorConfiguration(Context.getDefaultRI(),Context.getAppRI()); + MetricConfigurationI metConf=new MetricConfiguration(Context.getDefaultRI(),Context.getAppRI()); + log.info("Creados los configuradores de métricas e indicadores, se crea checker"); + return new Checker(metConf,indConf); + } /** *

@@ -178,7 +192,7 @@ public static void setAppConf(String appConPath) throws IOException { * Vuelve a leer las propiedades incluyendo las establecidas por la aplicación */ appConfFile = appConPath; - + log.fine("Fichero configuración de la API establecido "+appConPath); // customFile=System.getenv("APP_HOME")+customFile; // Otra opción, Usar una variable de entorno para localizar la ruta de // instalación y de ahí coger el fichero de configuración 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 62ef7b87..75b54f30 100644 --- a/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/IndicatorConfiguration.java @@ -26,12 +26,24 @@ * */ public class IndicatorConfiguration implements IndicatorConfigurationI { + /** + * El creador de IndicatorConfiguration tiene que indicar los nombres de los ficheros de métricas por defecto y del cliente + * @param defaultRI nombre del fichero de métricas por defecto + * @param appRI nombre del fichero de métricas definido por el cliente + */ + public IndicatorConfiguration(String defaultRI, String appRI) { + super(); + this.defaultRI = defaultRI; + this.appRI = appRI; + } private static Logger log = Logger.getLogger(Checker.class.getName()); private String CRITICAL_LIMIT = "limits.critical"; private String WARNING_LIMIT = "limits.warning"; private String OK_LIMIT = "limits.ok"; + private String defaultRI; + private String appRI; @Override /** @@ -53,15 +65,15 @@ public HashMap definedIndicator(String name, String type) throws HashMap indicatorDefinition = null; log.info("Checker solicitud de búsqueda indicador " + name); - String filePath = "/" + Context.getDefaultRI(); + String filePath = "/" + defaultRI; log.info("Buscando el archivo " + filePath); InputStream is = this.getClass().getResourceAsStream(filePath); log.info("InputStream " + is + " para " + filePath); InputStreamReader isr = new InputStreamReader(is); indicatorDefinition = isDefinedIndicator(name, type, isr); - if ((indicatorDefinition == null) && Context.getAppRI() != null) { - is = new FileInputStream(Context.getAppRI()); + if ((indicatorDefinition == null) && appRI != null) { + is = new FileInputStream(appRI); isr = new InputStreamReader(is); indicatorDefinition = isDefinedIndicator(name, type, isr); } @@ -135,7 +147,7 @@ public List listAllIndicators() throws FileNotFoundException { List allmetrics = new ArrayList(); - String filePath = "/" + Context.getDefaultRI(); + String filePath = "/" + defaultRI; log.info("Buscando el archivo " + filePath); InputStream is = this.getClass().getResourceAsStream(filePath); log.info("InputStream " + is + " para " + filePath); @@ -155,8 +167,8 @@ public List listAllIndicators() throws FileNotFoundException { log.info("Añado nombre: " + metrics.get(i).asJsonObject().getString("name")); allmetrics.add(metrics.get(i).asJsonObject().getString("name")); } - if (Context.getAppRI() != null) { - is = new FileInputStream(Context.getAppRI()); + if (appRI != null) { + is = new FileInputStream(appRI); isr = new InputStreamReader(is); reader = Json.createReader(isr); confObject = reader.readObject(); diff --git a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java index d602ad4d..38818e2c 100644 --- a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java @@ -15,8 +15,21 @@ import javax.json.JsonReader; public class MetricConfiguration implements MetricConfigurationI { + + /** + * El creador de MetricConfiguration tiene que indicar los nombres de los ficheros de métricas por defecto y del cliente + * @param defaultRI nombre del fichero de métricas por defecto + * @param appRI nombre del fichero de métricas definido por el cliente + */ + public MetricConfiguration(String defaultRI, String appRI) { + super(); + this.defaultRI = defaultRI; + this.appRI = appRI; + } private static Logger log = Logger.getLogger(Checker.class.getName()); + private String defaultRI; + private String appRI; /** * Método privado que verifica si una métrica está definida en un fichero de @@ -106,7 +119,7 @@ public HashMap definedMetric(String name, String type) throws Fi HashMap metricDefinition = null; - String filePath = "/" + Context.getDefaultRI(); + String filePath = "/" + defaultRI; log.info("Buscando el archivo " + filePath); InputStream is = this.getClass().getResourceAsStream(filePath); log.info("InputStream " + is + " para " + filePath); @@ -120,8 +133,8 @@ public HashMap definedMetric(String name, String type) throws Fi * En caso de que no estuviera ahí la métrica busco en el fichero de * configuración de la aplicación */ - if ((metricDefinition == null) && Context.getAppRI() != null) { - is = new FileInputStream(Context.getAppRI()); + if ((metricDefinition == null) && appRI != null) { + is = new FileInputStream(appRI); isr = new InputStreamReader(is); metricDefinition = isDefinedMetric(name, type, isr); } @@ -134,7 +147,7 @@ public HashMap getMetricInfo(String name) throws FileNotFoundExc log.info("Consulta información de la métrica " + name); HashMap metricDefinition = null; - String filePath = "/" + Context.getDefaultRI(); + String filePath = "/" + defaultRI; log.info("Buscando el archivo " + filePath); InputStream is = this.getClass().getResourceAsStream(filePath); log.info("InputStream " + is + " para " + filePath); @@ -148,8 +161,8 @@ public HashMap getMetricInfo(String name) throws FileNotFoundExc * En caso de que no estuviera ahí la métrica busco en el fichero de * configuración de la aplicación */ - if ((metricDefinition == null) && Context.getAppRI() != null) { - is = new FileInputStream(Context.getAppRI()); + if ((metricDefinition == null) && appRI != null) { + is = new FileInputStream(appRI); isr = new InputStreamReader(is); metricDefinition = getMetric(name, isr); } @@ -163,7 +176,7 @@ public List listAllMetrics() throws FileNotFoundException { List allmetrics = new ArrayList(); - String filePath = "/" + Context.getDefaultRI(); + String filePath = "/" + defaultRI; log.info("Buscando el archivo " + filePath); InputStream is = this.getClass().getResourceAsStream(filePath); log.info("InputStream " + is + " para " + filePath); @@ -183,8 +196,8 @@ public List listAllMetrics() throws FileNotFoundException { log.info("Añado nombre: " + metrics.get(i).asJsonObject().getString("name")); allmetrics.add(metrics.get(i).asJsonObject().getString("name")); } - if (Context.getAppRI() != null) { - is = new FileInputStream(Context.getAppRI()); + if (appRI != null) { + is = new FileInputStream(appRI); isr = new InputStreamReader(is); reader = Json.createReader(isr); confObject = reader.readObject(); diff --git a/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java b/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java index 75be7aab..18fb4f46 100644 --- a/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/IndicatorsCalculator.java @@ -4,6 +4,7 @@ package us.muit.fs.a4i.control; import us.muit.fs.a4i.exceptions.IndicatorException; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.control.IndicatorStrategy; @@ -26,7 +27,7 @@ public interface IndicatorsCalculator { /** *

* Calcula el indicador con el nombre que se pasa y lo incluye en el informe Si - * las métricas que necesita no están en el informe las busca y las añade + * las métricas que necesita no están en el informe usa ReportManagerI para localizarla antes *

* * @param indicatorName Nombre del indicador a calcular @@ -45,6 +46,7 @@ public interface IndicatorsCalculator { * @param reportManager Informe sobre el que realizar el c�lculo * @throws IndicatorException Si el tipo del informe no coincide con el de la * calculadora + * @throws NotAvailableMetricException Si falta alguna métrica en el informe */ public void calcAllIndicators(ReportManagerI reportManager) throws IndicatorException; diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/RepositoryCalculator.java b/src/main/java/us/muit/fs/a4i/control/calculators/RepositoryCalculator.java similarity index 77% rename from src/main/java/us/muit/fs/a4i/control/strategies/RepositoryCalculator.java rename to src/main/java/us/muit/fs/a4i/control/calculators/RepositoryCalculator.java index 1e4e93a3..5081609f 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/RepositoryCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/calculators/RepositoryCalculator.java @@ -1,7 +1,7 @@ /** * */ -package us.muit.fs.a4i.control.strategies; +package us.muit.fs.a4i.control.calculators; import java.util.HashMap; import java.util.List; @@ -45,21 +45,25 @@ public void calcIndicator(String indicatorName, ReportManagerI reportManager) th */ IndicatorStrategy indicatorStrategy = strategies.get(indicatorName); List requiredMetrics = indicatorStrategy.requiredMetrics(); - log.info("Las m�tricas necesarias son: " + requiredMetrics.toString()); + log.fine("Las métricas necesarias son: " + requiredMetrics.toString()); List metrics = reportManager.getReport().getAllMetrics().stream().collect(Collectors.toList()); List metricsName = metrics.stream().map(ReportItemI::getName).collect(Collectors.toList()); - if (metricsName.containsAll(requiredMetrics)) { - try { - // ¡¡Faltaba añadir el indicador al informe!! - reportManager.getReport().addIndicator(indicatorStrategy.calcIndicator(metrics)); - log.info("Añadido al informe indicador"); - } catch (NotAvailableMetricException e) { - log.info("No se han proporcionado las m�tricas necesarias"); - e.printStackTrace(); + for (String metric : requiredMetrics) { + if (!metricsName.contains(metric)) { + log.fine("se añade la métrica " + metric + " que no estaba disponible aún en el informe"); + reportManager.addMetric(metric); } - } else { - log.info("No se han proporcionado las metricas necesarias"); } + + try { + // añadir el indicador al informe + reportManager.getReport().addIndicator(indicatorStrategy.calcIndicator(reportManager.getReport().getAllMetrics().stream().collect(Collectors.toList()))); + log.info("Añadido al informe indicador"); + } catch (NotAvailableMetricException e) { + log.info("No se han proporcionado todas las métricas necesarias"); + e.printStackTrace(); + } + } /** diff --git a/src/main/java/us/muit/fs/a4i/control/ReportManager.java b/src/main/java/us/muit/fs/a4i/control/managers/ReportManager.java similarity index 96% rename from src/main/java/us/muit/fs/a4i/control/ReportManager.java rename to src/main/java/us/muit/fs/a4i/control/managers/ReportManager.java index b1380569..f580b415 100644 --- a/src/main/java/us/muit/fs/a4i/control/ReportManager.java +++ b/src/main/java/us/muit/fs/a4i/control/managers/ReportManager.java @@ -1,13 +1,15 @@ /** * */ -package us.muit.fs.a4i.control; +package us.muit.fs.a4i.control.managers; import java.io.IOException; import java.util.logging.Logger; import us.muit.fs.a4i.config.Context; -import us.muit.fs.a4i.control.strategies.RepositoryCalculator; +import us.muit.fs.a4i.control.IndicatorsCalculator; +import us.muit.fs.a4i.control.ReportManagerI; +import us.muit.fs.a4i.control.calculators.RepositoryCalculator; import us.muit.fs.a4i.exceptions.ReportNotDefinedException; import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItemI; @@ -19,7 +21,7 @@ import us.muit.fs.a4i.persistence.ReportFormaterI; /** - * @author Isabel Rom�n + * @author Isabel Román * */ public class ReportManager implements ReportManagerI { diff --git a/src/main/java/us/muit/fs/a4i/control/package-info.java b/src/main/java/us/muit/fs/a4i/control/package-info.java index f416de30..f01fe61d 100644 --- a/src/main/java/us/muit/fs/a4i/control/package-info.java +++ b/src/main/java/us/muit/fs/a4i/control/package-info.java @@ -1,6 +1,7 @@ /** *

* Clases e interfaces controladoras + * Facilitan el cálculo de indicadores y la gestión de informes *

* { diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/TeamsBalanceStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/TeamsBalanceStrategy.java index f306270a..5c568713 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/TeamsBalanceStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/TeamsBalanceStrategy.java @@ -52,7 +52,7 @@ public ReportItemI calcIndicator(List> metrics) thro // Se crea el indicador // No se entiende por qué el indicador tiene el mismo nombere que una de las // métricas - indicatorReport = new ReportItem.ReportItemBuilder("teamsBalanceI", teamsBalanceResult) + indicatorReport = new ReportItem.ReportItemBuilder("teamsBalance", teamsBalanceResult) .metrics(Arrays.asList(teamsBalance.get(), repositories.get())) .indicator(IndicatorState.UNDEFINED).build(); diff --git a/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java b/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java index bc631763..fbbaeb20 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/ReportI.java @@ -77,7 +77,7 @@ public static enum ReportType { Collection getAllIndicators(); /** - * A�ade un indicador al informe + * Añaade un indicador al informe * * @param newIndicator nuevo indicador */ diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java index b9aed056..fc624a38 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubEnquirer.java @@ -19,15 +19,20 @@ /** *

- * Clase abstracta con los métodos comunes a los constructores que recogen la - * información del servicio GitHub + * Clase abstracta con los métodos comunes a los constructores que consultan la + * información del servicio GitHub. Usan Github como backend *

*

* Para las consultas a github se recurre a la API github-API *

*

* Actualmente sólo incluye el establecimiento del identificador de entidad y la - * obtención del objeto GitHub para las consultas + * obtención del objeto GitHub para las consultas. + *

+ * protected Map> myQueries; será un mapa de + * funciones, en el que la clave es el nombre de la métrica y el valor es la + * referencia a la función que permite recuperar esa métrica + *

*

* * @author Isabel Román @@ -35,9 +40,9 @@ * */ public abstract class GitHubEnquirer implements RemoteEnquirer { - private static Logger log = Logger.getLogger(GitHubEnquirer.class.getName()); - protected Map> myQueries; - private RemoteEnquirer.RemoteType type= RemoteEnquirer.RemoteType.GITHUB; + private static Logger log = Logger.getLogger(GitHubEnquirer.class.getName()); + protected Map> myQueries; + private RemoteEnquirer.RemoteType type = RemoteEnquirer.RemoteType.GITHUB; /** *

@@ -51,7 +56,7 @@ public abstract class GitHubEnquirer implements RemoteEnquirer { private GitHub github = null; public GitHubEnquirer() { - myQueries=new HashMap>(); + myQueries = new HashMap>(); } /** @@ -78,17 +83,24 @@ protected GitHub getConnection() { return github; } - protected void setMetric(String newMetric,Function functionPointer) { + /** + * Permite añadir una nueva función de búsqueda de métrica al mapa + * + * @param newMetric Nombre de la métrica nueva + * @param functionPointer Referencia a la función que implementa el algoritmo + * para recuperar la métrica + */ + protected void setMetric(String newMetric, Function functionPointer) { myQueries.put(newMetric, functionPointer); } public List getAvailableMetrics() { - List metrics=new ArrayList(myQueries.keySet()); + List metrics = new ArrayList(myQueries.keySet()); return metrics; } + public RemoteType getRemoteType() { return type; } - } diff --git a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java index 52e47ca8..a93333e0 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/GitHubRepositoryEnquirer.java @@ -36,9 +36,8 @@ import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; /** - * @author Isabel Román Deuda técnica: debería seguir la misma filosofía que - * GitHubOrganizationEnquirer para evitar la replicación de código deuda - * técnica: las métricas tras la etiqueta //equipo 3 tienen problemas, + * @author Isabel Román y alumnos de factoría software del MUIT de la ETSI (US) + * deuda técnica: las métricas tras la etiqueta //equipo 3 tienen problemas, * no están acordes al indicador para el que fueron creadas RECUERDA: * las métricas tienen que estar incluidas en el fichero de * configuración a4iDefault.json @@ -53,7 +52,6 @@ public class GitHubRepositoryEnquirer extends GitHubEnquirer { * para trazar el código */ private static Logger log = Logger.getLogger(GitHubRepositoryEnquirer.class.getName()); -// protected Map> myQueries=new HashMap>(); /** *

@@ -63,6 +61,7 @@ public class GitHubRepositoryEnquirer extends GitHubEnquirer { public GitHubRepositoryEnquirer() { super(); + //para cada métrica se añade en el mapa la función que la consulta y como clave el nombre de la métrica myQueries.put("subscribers", GitHubRepositoryEnquirer::getSubscribers); myQueries.put("forks", GitHubRepositoryEnquirer::getForks); myQueries.put("watchers", GitHubRepositoryEnquirer::getWatchers); @@ -78,23 +77,7 @@ public GitHubRepositoryEnquirer() { myQueries.put("totalDeletions", GitHubRepositoryEnquirer::getTotalDeletions); myQueries.put("collaborators", GitHubRepositoryEnquirer::getCollaborators); myQueries.put("ownerCommits", GitHubRepositoryEnquirer::getOwnerCommits); - /* - metricNames.add("subscribers"); - metricNames.add("forks"); - metricNames.add("watchers"); - metricNames.add("starts"); - metricNames.add("issues"); - metricNames.add("closedIssues"); - metricNames.add("openIssues"); - metricNames.add("creation"); - metricNames.add("lastUpdated"); - metricNames.add("lastPush"); - metricNames.add("totalAdditions"); - - metricNames.add("totalDeletions"); - metricNames.add("collaborators"); - metricNames.add("ownerCommits"); - */ + // equipo3 myQueries.put("issuesLastMonth", GitHubRepositoryEnquirer::getIssuesLastMonth); myQueries.put("closedIssuesLastMonth", GitHubRepositoryEnquirer::getClosedIssuesLastMonth); @@ -118,7 +101,7 @@ public GitHubRepositoryEnquirer() { myQueries.put("gitFlowBranches", GitHubRepositoryEnquirer::getGitFlowBranches); myQueries.put("conventionalPullRequests", GitHubRepositoryEnquirer::getConventionalPullRequests); - log.info("A�adidas m�tricas al GHRepositoryEnquirer"); + log.info("Añadidas métricas al GHRepositoryEnquirer"); } /** @@ -127,7 +110,7 @@ public GitHubRepositoryEnquirer() { @Override public ReportI buildReport(String repositoryId) { ReportI report = null; - log.info("Invocado el m�todo que construye un objeto RepositoryReport"); + log.info("Invocado el método que construye un objeto RepositoryReport"); /** *

* Información sobre el repositorio obtenida de GitHub @@ -147,66 +130,41 @@ public ReportI buildReport(String repositoryId) { try { log.info("Nombre repo = " + repositoryId); - +/* GitHub gb = getConnection(); repo = gb.getRepository(repositoryId); log.info("El repositorio es de " + repo.getOwnerName() + " Y su descripción es " + repo.getDescription()); log.info("leído " + repo); + */ report = new Report(repositoryId); /** * Métricas más elaboradas, requieren más "esfuerzo" */ - report.addMetric(getTotalAdditions(repo)); + report.addMetric(getMetric("totalAdditions", repositoryId)); log.info("Incluida metrica totalAdditions "); - report.addMetric(getTotalDeletions(repo)); + report.addMetric(getMetric("totalDeletions", repositoryId)); log.info("Incluida metrica totalDeletions "); /** * Métricas directas de tipo conteo */ - report.addMetric(getSubscribers(repo)); + report.addMetric(getMetric("subscribers", repositoryId)); log.info("Incluida metrica suscribers "); - report.addMetric(getCollaborators(repo)); - log.info("Incluida metrica collaborators "); - - report.addMetric(getOwnerCommits(repo)); - log.info("Incluida metrica ownerCommits "); - - report.addMetric(getForks(repo)); - log.info("Incluida metrica forks "); - - report.addMetric(getWatchers(repo)); - log.info("Incluida metrica watchers "); - - report.addMetric(getStars(repo)); - log.info("Incluida metrica stars "); - - report.addMetric(getIssues(repo)); - log.info("Incluida metrica issues "); - - report.addMetric(getOpenIssues(repo)); - log.info("Incluida metrica openIssues "); - - report.addMetric(getClosedIssues(repo)); - log.info("Incluida metrica closedIssues "); - + /** * Métricas directas de tipo fecha */ - report.addMetric(getCreation(repo)); + report.addMetric(getMetric("creation", repositoryId)); log.info("Incluida metrica creation "); - report.addMetric(getLastPush(repo)); + report.addMetric(getMetric("lastPush", repositoryId)); log.info("Incluida metrica lastPush "); - - report.addMetric(getLastUpdated(repo)); - log.info("Incluida metrica lastUpdates "); - + } catch (Exception e) { log.severe("Problemas en la conexión " + e); } @@ -256,104 +214,6 @@ private ReportItem getMetric(String metricName, GHRepository remoteRepo) throws throw new MetricException("Intenta obtener una métrica sin haber obtenido los datos del repositorio"); } metric=myQueries.get(metricName).apply(remoteRepo); - /* - switch (metricName) { - case "totalAdditions": - metric = getTotalAdditions(remoteRepo); - break; - case "totalDeletions": - metric = getTotalDeletions(remoteRepo); - break; - case "starts": - metric = getStars(remoteRepo); - break; - case "forks": - metric = getForks(remoteRepo); - break; - case "watchers": - metric = getWatchers(remoteRepo); - break; - case "subscribers": - metric = getSubscribers(remoteRepo); - break; - case "issues": - metric = getIssues(remoteRepo); - break; - case "creation": - metric = getCreation(remoteRepo); - break; - case "lastUpdated": - metric = getLastUpdated(remoteRepo); - break; - case "lastPush": - metric = getLastPush(remoteRepo); - break; - case "collaborators": - metric = getCollaborators(remoteRepo); - break; - case "ownerCommits": - metric = getOwnerCommits(remoteRepo); - break; - case "openIssues": - metric = getOpenIssues(remoteRepo); - break; - case "closedIssues": - metric = getClosedIssues(remoteRepo); - break; - // equipo 3 - case "issuesLastMonth": - metric = getIssuesLastMonth(remoteRepo); - break; - case "closedIssuesLastMonth": - metric = getClosedIssuesLastMonth(remoteRepo); - break; - case "meanClosedIssuesLastMonth": - metric = getMeanClosedIssuesLastMonth(remoteRepo); - break; - case "issues4DevLastMonth": - metric = issues4DevLastMonth(remoteRepo); - break; - // equipo 4 - case "totalPullReq": - metric = getTotalPullReq(remoteRepo); - break; - case "closedPullReq": - metric = getClosedPullReq(remoteRepo); - break; - case "PRAcceptedLastYear": - metric = getPRAcceptedLastYear(remoteRepo); - break; - case "PRAcceptedLastMonth": - metric = getPRAcceptedLastMonth(remoteRepo); - break; - case "PRRejectedLastYear": - metric = getPRRejectedLastYear(remoteRepo); - break; - case "PRRejectedLastMonth": - metric = getPRRejectedLastMonth(remoteRepo); - break; - // equipo 1 - // Begin: RepositoryIndicatorStrategy metrics - case "conventionalCommits": - metric = getConventionalCommits(remoteRepo); - break; - case "commitsWithDescription": - metric = getCommitsWithDescription(remoteRepo); - break; - case "issuesWithLabels": - metric = getIssuesWithLabels(remoteRepo); - break; - case "gitFlowBranches": - metric = getGitFlowBranches(remoteRepo); - break; - case "conventionalPullRequests": - metric = getConventionalPullRequests(remoteRepo); - break; - default: - throw new MetricException("La métrica " + metricName + " no está definida para un repositorio"); - } - */ - return metric; } @@ -372,16 +232,17 @@ private ReportItem getMetric(String metricName, GHRepository remoteRepo) throws * @throws MetricException Intenta crear una métrica no definida */ static private ReportItem getTotalAdditions(GHRepository remoteRepo) throws MetricException { + log.fine("buscando totaladditions"); ReportItem metric = null; GHRepositoryStatistics data = remoteRepo.getStatistics(); - + log.fine("estadisticas "+data.toString()); List codeFreq; try { codeFreq = data.getCodeFrequency(); int additions = 0; - + for (CodeFrequency freq : codeFreq) { if (freq.getAdditions() != 0) { @@ -397,7 +258,7 @@ static private ReportItem getTotalAdditions(GHRepository remoteRepo) throws Metr metric = builder.build(); } catch (Exception e) { - // TODO Auto-generated catch block + log.warning("Problemas al leer codefrequency en getTotalAdditions"); e.printStackTrace(); } diff --git a/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java index 261c9bb6..36587b3b 100644 --- a/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java +++ b/src/main/java/us/muit/fs/a4i/model/remote/RemoteEnquirer.java @@ -28,7 +28,9 @@ * */ public interface RemoteEnquirer { - + /** + * Hasta el momento sólo se ha desarrollado la conexión con github como backend + */ public static enum RemoteType { GITHUB } diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index f7c86ba6..33cee922 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -304,6 +304,12 @@ "description": "Balance de equipos y open issues", "unit": "ratio", "limits": { "ok": 0.2, "warning": 0.6, "critical": 0.8 } + }, + { + "name": "fixTime", + "type": "java.lang.Double", + "description": "Tiempo para arreglos", + "unit": "ratio" } ] } \ No newline at end of file diff --git a/src/main/resources/log.properties b/src/main/resources/log.properties index e4d3243d..8a387663 100644 --- a/src/main/resources/log.properties +++ b/src/main/resources/log.properties @@ -1,7 +1,18 @@ +# para que este fichero de configuración de trazas sea efectivo es necesario pasarlo como parámetro de la máquina virtual al ejecutar +#-Djava.util.logging.config.file="src\main\resources\log.properties" + # especificacion de detalle de log # nivel de log global .level = ALL javax.json.Json.level = FINEST +us.muit.fs.a4i.model.remote.GitHubRepositoryEnquirer.level = FINEST + +us.muit.fs.a4i.persistence.ExcelReportManager.level = FINEST +us.muit.fs.a4i.config.Context.level=FINEST +us.muit.fs.a4i.config.Checker.level=FINEST +us.muit.fs.a4i.config.IndicatorConfiguration.level=FINEST +us.muit.fs.a4i.config.MetricConfiguration.level=FINEST +us.muit.fs.a4i.control.calculators.RepositoryCalculator.level = FINEST # nivel clases diff --git a/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java b/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java deleted file mode 100644 index d67adec6..00000000 --- a/src/test/java/us/muit/fs/a4i/test/config/CheckerTest.java +++ /dev/null @@ -1,160 +0,0 @@ -/** - * - */ -package us.muit.fs.a4i.test.config; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.File; -import java.io.FileNotFoundException; -import java.util.HashMap; -import java.util.logging.Logger; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; - -import us.muit.fs.a4i.config.Checker; -import us.muit.fs.a4i.config.Context; - -/** - * Test de la clase Checker que permite verificar y configurar las métricas e indicadores - * - * @author Isabel Román - * @see org.junit.jupiter.api.Tag - * - */ - -@Tag("unidad") -class CheckerTest { - private static Logger log = Logger.getLogger(CheckerTest.class.getName()); - static Checker underTest; - static String appConfPath; - - - - /** - *

Acciones a realizar antes de cada uno de los tests de esta clase

- * @throws java.lang.Exception - * @see org.junit.jupiter.api.BeforeEach - */ - @BeforeEach - void setUp() throws Exception { - // Creo el objeto bajo test, un Checker - underTest = Context.getContext().getChecker(); - } - - - /** - *

Test para verificar el método - * {@link us.muit.fs.a4i.config.Checker#definedMetric(java.lang.String, java.lang.String)}. - * Si la métrica está definida y el tipo de valor que se quiere establecer es el - * adecuado debe devolver un hashmap con los datos de la métrica, usando como - * clave las etiquetas: - *

    - *
  • description
  • - *
  • unit
  • - *
- * Las métricas pueden estar definidas en el fichero de configuración de la api - * (a4iDefault.json) o en otro fichero configurado por la aplicación cliente. - * Para los test este fichero es appConfTest.json y se guarda junto al código de - * test, en la carpeta resources - * - * @see org.junit.jupiter.api.Tag - * @see org.junit.jupiter.api.Test - * @see org.junit.jupiter.api.DisplayName * - *

- */ - @Test - @Tag("unidad") - @DisplayName("Prueba para el método definedMetric, que verifica si la métrica está definida con un tipo determinado y devuelve su configuración") - - void testDefinedMetric() { - - // Creo valores Mock para verificar si comprueba bien el tipo - // Las m�tricas del test son de enteros, as� que creo un entero y un string (el - // primero no dar� problemas el segundo sí) - Integer valOKMock = Integer.valueOf(3); - String valKOMock = "KO"; - HashMap returnedMap = null; - // Primero, sin fichero de configuraci�n de aplicaci�n - try { - // Consulta una m�trica no definida, con valor de tipo entero - // debe devolver null, no est� definida - log.info("Busco la m�trica llamada downloads"); - returnedMap = underTest.getMetricConfiguration().definedMetric("downloads", valOKMock.getClass().getName()); - assertNull(returnedMap, "Deber�a ser nulo, la m�trica noexiste no est� definida"); - - // Busco la m�trica watchers con valor entero, no deber�a dar problemas - log.info("Busco la m�trica watchers"); - returnedMap = underTest.getMetricConfiguration().definedMetric("watchers", valOKMock.getClass().getName()); - assertNotNull(returnedMap, "Deber�a devolver un hashmap, la m�trica est� definida"); - assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); - assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); - - // Busco una m�trica que existe pero con un tipo incorrecto en el valor - assertNull(underTest.getMetricConfiguration().definedMetric("watchers", valKOMock.getClass().getName()), - "Deber�a ser nulo, la m�trica est� definida para Integer"); - } catch (FileNotFoundException e) { - fail("El fichero NO est� en la carpeta resources"); - e.printStackTrace(); - } - - } - - /** - * @see org.junit.jupiter.api.Tag - * @see org.junit.jupiter.api.Test - * @see org.junit.jupiter.api.DisplayName - * - * Test para el m�todo - * {@link us.muit.fs.a4i.config.Checker#definedIndicator(java.lang.String, java.lang.String)}. - */ - @Test - @Tag("unidad") - @DisplayName("Prueba para el m�todo definedIndicator, que verifica si el indicador est� definido con un tipo determinado y devuelve su configuraci�n") - void testDefinedIndicator() { - - // Creo valores Mock para verificar si comprueba bien el tipo - // Las m�tricas del test son de enteros, as� que creo un entero y un string (el - // primero no dar� problemas el segundo s�) - Double valOKMock = Double.valueOf(0.3); - String valKOMock = "KO"; - HashMap returnedMap = null; - // Primero, sin fichero de configuraci�n de aplicaci�n - try { - // Consulta un indicador no definido, con valor de tipo entero - // debe devolver null, no est� definido - log.info("Busco el indicador llamado pullReqGlory"); - returnedMap = underTest.getIndicatorConfiguration().definedIndicator("pullReqGlory", - valOKMock.getClass().getName()); - assertNull(returnedMap, "Deber�a ser nulo, el indicador pullReqGlory no est� definido"); - - // Busco el indicador overdued con valor double, no deber�a dar problemas - log.info("Busco el indicador overdued"); - returnedMap = underTest.getIndicatorConfiguration().definedIndicator("overdued", - valOKMock.getClass().getName()); - assertNotNull(returnedMap, "Deber�a devolver un hashmap, el indicador overdued est� definido"); - assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); - assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); - - // Busco una m�trica que existe pero con un tipo incorrecto en el valor - assertNull( - underTest.getIndicatorConfiguration().definedIndicator("overdued", valKOMock.getClass().getName()), - "Deber�a ser nulo, el indicador overdued est� definido para Double"); - } catch (FileNotFoundException e) { - fail("El fichero est� en la carpeta resources"); - e.printStackTrace(); - } - - - } - -} diff --git a/src/test/java/us/muit/fs/a4i/test/config/CheckerTest2.java b/src/test/java/us/muit/fs/a4i/test/config/CheckerTest2.java deleted file mode 100644 index 97d48fcf..00000000 --- a/src/test/java/us/muit/fs/a4i/test/config/CheckerTest2.java +++ /dev/null @@ -1,200 +0,0 @@ -package us.muit.fs.a4i.test.config; - -import static org.junit.jupiter.api.Assertions.*; - -import java.io.File; -import java.io.FileNotFoundException; -import java.util.HashMap; -import java.util.logging.Logger; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; - -import us.muit.fs.a4i.config.Checker; -import us.muit.fs.a4i.config.Context; - -/** - * Verificación de Checker para fichero de configuración de métricas - * personalizado - */ -class CheckerTest2 { - private static Logger log = Logger.getLogger(CheckerTest.class.getName()); - static Checker underTest; - static String appConfPath; - - /** - *

- * Acciones a realizar antes de ejecutar los tests definidos en esta clase - *

- * - * @throws java.lang.Exception - * @see org.junit.jupiter.api.BeforeAll - */ - @BeforeAll - static void setUpBeforeClass() throws Exception { - appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator - + "appConfTest.json"; - Context.setAppRI(appConfPath); - } - - /** - *

- * Acciones a realizar después de ejecutar todos los tests de esta clase - *

- * - * @throws java.lang.Exception - * @see org.junit.jupiter.api.AfterAll - */ - @AfterAll - static void tearDownAfterClass() throws Exception { - - } - - /** - *

- * Acciones a realizar antes de cada uno de los tests de esta clase - *

- * - * @throws java.lang.Exception - * @see org.junit.jupiter.api.BeforeEach - */ - @BeforeEach - void setUp() throws Exception { - // Creo el objeto bajo test, un Checker - underTest = Context.getContext().getChecker(); - } - - /** - *

- * Acciones a realizar después de cada uno de los tests de esta clase - *

- * - * @throws java.lang.Exception - * @see org.junit.jupiter.api.AfterEach - */ - @AfterEach - void tearDown() throws Exception { - - } - - /** - *

- * Test para verificar que el método que establece el fichero de configuración de la - * aplicación devuelve la excepción adecuada si no encuentra el fichero - *

- * {@link us.muit.fs.a4i.config.Checker#setAppMetrics(java.lang.String)}. - */ - @Test - void testSetAppMetrics() { - HashMap returnedMap = null; - Integer valOKMock = Integer.valueOf(3); - String valKOMock = "KO"; - // Ahora establezco el fichero de configuraci�n de la aplicaci�n, con un nombre - // de fichero que no existe - Context.setAppRI("pepe"); - try { - // Busco una m�trica que se que no est� en la configuraci�n de la api - returnedMap = underTest.getMetricConfiguration().definedMetric("downloads", valOKMock.getClass().getName()); - fail("Antes de llegar aquí debería lanzar una excepción, porque intenta buscar en un fichero que no existe"); - } catch (FileNotFoundException e) { - log.info("Lanza la excepci�n adecuada, FileNotFoud"); - } catch (Exception e) { - fail("Lanza la excepci�n equivocada " + e); - } - //Vuelvo a dejar el fichero de configuración correcto - Context.setAppRI(appConfPath); - } - - /** - *

- * Test para verificar el método - * {@link us.muit.fs.a4i.config.Checker#definedMetric(java.lang.String, java.lang.String)}. - * Si la métrica está definida y el tipo de valor que se quiere establecer es el - * adecuado debe devolver un hashmap con los datos de la métrica, usando como - * clave las etiquetas: - *

    - *
  • description
  • - *
  • unit
  • - *
- * Las métricas pueden estar definidas en el fichero de configuración de la api - * (a4iDefault.json) o en otro fichero configurado por la aplicación cliente. - * Para los test este fichero es appConfTest.json y se guarda junto al código de - * test, en la carpeta resources - * - * @see org.junit.jupiter.api.Tag - * @see org.junit.jupiter.api.Test - * @see org.junit.jupiter.api.DisplayName * - *

- */ - @Test - @Tag("unidad") - @DisplayName("Prueba para el método definedMetric, que verifica si la métrica está definida con un tipo determinado y devuelve su configuración") - void testDefinedMetric() { - - // Creo valores Mock para verificar si comprueba bien el tipo - // Las m�tricas del test son de enteros, as� que creo un entero y un string (el - // primero no dar� problemas el segundo sí) - Integer valOKMock = Integer.valueOf(3); - String valKOMock = "KO"; - HashMap returnedMap = null; - - - try { - // Busco una m�trica que se que no est� en la configuraci�n de la api pero s� en - // la de la aplicaci�n - log.info("Busco la m�trica llamada downloads"); - returnedMap = underTest.getMetricConfiguration().definedMetric("downloads", valOKMock.getClass().getName()); - assertNotNull(returnedMap, "Deber�a devolver un hashmap, la m�trica est� definida"); - assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); - assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); - } catch (FileNotFoundException e) { - fail("No deber�a devolver esta excepci�n"); - } catch (Exception e) { - fail("Lanza una excepci�n no reconocida " + e); - } - - } - - /** - * @see org.junit.jupiter.api.Tag - * @see org.junit.jupiter.api.Test - * @see org.junit.jupiter.api.DisplayName - * - * Test para el m�todo - * {@link us.muit.fs.a4i.config.Checker#definedIndicator(java.lang.String, java.lang.String)}. - */ - @Test - @Tag("unidad") - @DisplayName("Prueba para el m�todo definedIndicator, que verifica si el indicador est� definido con un tipo determinado y devuelve su configuraci�n") - void testDefinedIndicator() { - - // Creo valores Mock para verificar si comprueba bien el tipo - // Las m�tricas del test son de enteros, as� que creo un entero y un string (el - // primero no dar� problemas el segundo s�) - Double valOKMock = Double.valueOf(0.3); - String valKOMock = "KO"; - HashMap returnedMap = null; - - try { - // Busco una m�trica que se que no est� en la configuraci�n de la api pero s� en - // la de la aplicaci�n - log.info("Busco el indicador llamado pullReqGlory"); - returnedMap = underTest.getIndicatorConfiguration().definedIndicator("pullReqGlory", - valOKMock.getClass().getName()); - assertNotNull(returnedMap, "Deber�a devolver un hashmap, el indicador est� definido"); - assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); - assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); - } catch (FileNotFoundException e) { - fail("No deber�a devolver esta excepci�n"); - } catch (Exception e) { - fail("Lanza una excepci�n no reconocida " + e); - } - - } - -} \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java index bc77ce48..a1d1cb8f 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest.java @@ -27,15 +27,12 @@ import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; import us.muit.fs.a4i.model.entities.Font; // Clase que sustituye a java.awt.Font - - - /** - * @author Isabel Román - * Verificación de la clase Context cuando sólo se utiliza el fichero de configuración por defecto (a4i.conf) + * @author Isabel Román Verificación de la clase Context cuando sólo se utiliza + * el fichero de configuración por defecto (a4i.conf) */ class ContextTest { - private static Logger log = Logger.getLogger(CheckerTest.class.getName()); + private static Logger log = Logger.getLogger(ContextTest.class.getName()); /** * Ruta al fichero de configuración de indicadores y métricas establecidos por * la aplicación @@ -49,13 +46,13 @@ class ContextTest { /** * @throws java.lang.Exception - - @BeforeAll - static void setUpBeforeClass() throws Exception { - appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator - + "appConfTest.json"; - appPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appTest.conf"; - } */ + * + * @BeforeAll static void setUpBeforeClass() throws Exception { appConfPath = + * "src" + File.separator + "test" + File.separator + "resources" + + * File.separator + "appConfTest.json"; appPath = "src" + + * File.separator + "test" + File.separator + "resources" + + * File.separator + "appTest.conf"; } + */ /** * @throws java.lang.Exception @@ -76,7 +73,7 @@ void setUp() throws Exception { */ @AfterEach void tearDown() throws Exception { - //Ejecutar tras cada test + // Ejecutar tras cada test } /** @@ -95,6 +92,7 @@ void testGetContext() { } } + /** * Test method for {@link us.muit.fs.a4i.config.Context#getChecker()}. */ @@ -140,7 +138,11 @@ void testGetRemoteType() { } /** - *

Este test permite verificar que se lee bien la fuente, además es independiente del orden de ejecución del resto de test. La complejidad de la verifiación está impuesta por estar probando un singleton

+ *

+ * Este test permite verificar que se lee bien la fuente, además es + * independiente del orden de ejecución del resto de test. La complejidad de la + * verifiación está impuesta por estar probando un singleton + *

* Test method for {@link us.muit.fs.a4i.config.Context#getDefaultFont()}. */ @Test @@ -149,15 +151,22 @@ void testGetDefaultFont() { Font font = null; String color; // No entiendo cómo usarlo, ¿para darle valor luego?// // Uso esto para ver los tipos de fuentes de los que dispongo - //String[] fontNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); - //log.info("listado de fuentes " + Arrays.toString(fontNames)); + // String[] fontNames = + // GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); + // log.info("listado de fuentes " + Arrays.toString(fontNames)); font = Context.getContext().getDefaultFont(); assertNotNull(font, "No se ha inicializado bien la fuente"); - //Al ser Context un singleton una vez creada la instancia no se puede eliminar "desde fuera" - //De manera que el valor de la fuente depende del orden en el que se ejecuten los test, y para que el test sea independiente de eso la verificación comprueba los dos posibles valores - assertEquals(Color.BLACK.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); - assertEquals(10,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); - assertEquals("Arial",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); + // Al ser Context un singleton una vez creada la instancia no se puede eliminar + // "desde fuera" + // De manera que el valor de la fuente depende del orden en el que se ejecuten + // los test, y para que el test sea independiente de eso la verificación + // comprueba los dos posibles valores + assertEquals(Color.BLACK.toString(), font.getColor().toString(), + "No es el color de fuente especificado en el fichero de propiedades"); + assertEquals(10, font.getFont().getSize(), + "No es el tamaño de fuente especificado en el fichero de propiedades"); + assertEquals("Arial", font.getFont().getFamily(), + "No es el tipo de fuente especificado en el fichero de propiedades"); } catch (IOException e) { fail("No debería devolver esta excepción"); @@ -167,26 +176,29 @@ void testGetDefaultFont() { /** * Test method for {@link us.muit.fs.a4i.config.Context#getMetricFont()}. - * @throws IOException + * + * @throws IOException */ @DisplayName("Verificación lectura de configuración de fuente de métricas") - @Test - void testGetMetricFont(){ + @Test + void testGetMetricFont() { try { - - Font font = null; - - font = Context.getContext().getMetricFont(); - assertNotNull(font, "No se ha inicializado bien la fuente"); - assertEquals(Color.GREEN.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); - assertTrue(10 == font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); - assertEquals(java.awt.Font.SERIF,font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); - - - }catch (IOException e) { - fail("No debería devolver esta excepción"); - e.printStackTrace(); - } + + Font font = null; + + font = Context.getContext().getMetricFont(); + assertNotNull(font, "No se ha inicializado bien la fuente"); + assertEquals(Color.GREEN.toString(), font.getColor().toString(), + "No es el color de fuente especificado en el fichero de propiedades"); + assertTrue(10 == font.getFont().getSize(), + "No es el tamaño de fuente especificado en el fichero de propiedades"); + assertEquals(java.awt.Font.SERIF, font.getFont().getFamily(), + "No es el tipo de fuente especificado en el fichero de propiedades"); + + } catch (IOException e) { + fail("No debería devolver esta excepción"); + e.printStackTrace(); + } } /** @@ -198,41 +210,52 @@ void testGetMetricFont(){ void testGetIndicatorFont() { try { Font font = null; - - // Se le solicita la fuente del estado indefinido, que tendrá los valores por defecto Font.Default.xxx al no estar - // definidas sus propiedades en el fichero de configuración utilizados en los tests. + + // Se le solicita la fuente del estado indefinido, que tendrá los valores por + // defecto Font.Default.xxx al no estar + // definidas sus propiedades en el fichero de configuración utilizados en los + // tests. font = Context.getContext().getIndicatorFont(IndicatorState.OK); assertNotNull(font, "No se ha inicializado bien la fuente"); - // El nombre o tipo de la fuente podrá ser Arial o Times según el momento en el que se realicen los tests. - log.info("familia "+font.getFont().getFamily()); - assertEquals("Serif",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); - assertEquals(Color.BLACK,font.getColor(),"No es el color de fuente especificado en el fichero de propiedades"); - assertEquals(10,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); - - // Se le solicita al contexto la fuente del estao "CRITICAL", cuyas propiedades están definidas en el + // El nombre o tipo de la fuente podrá ser Arial o Times según el momento en el + // que se realicen los tests. + log.info("familia " + font.getFont().getFamily()); + assertEquals("Serif", font.getFont().getFamily(), + "No es el tipo de fuente especificado en el fichero de propiedades"); + assertEquals(Color.BLACK, font.getColor(), + "No es el color de fuente especificado en el fichero de propiedades"); + assertEquals(10, font.getFont().getSize(), + "No es el tamaño de fuente especificado en el fichero de propiedades"); + + // Se le solicita al contexto la fuente del estao "CRITICAL", cuyas propiedades + // están definidas en el // fichero de configuración por defecto. font = Context.getContext().getIndicatorFont(IndicatorState.CRITICAL); - + assertNotNull(font, "No se ha inicializado bien la fuente"); - - - log.info("Datos fuente estado CRITICAL familia "+font.getFont().getFamily()+" tamano "+font.getFont().getSize()+" color "+font.getColor()); - - assertTrue(font.getFont().getFamily().equals("Arial"),"No es el tipo de fuente especificado en el fichero de propiedades"); - assertEquals(Color.RED,font.getColor(),"No es el color de fuente especificado en el fichero de propiedades"); - assertEquals(12,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); - - } catch (IOException e) { - fail("No debería devolver esta excepción"); - e.printStackTrace(); - } + + log.info("Datos fuente estado CRITICAL familia " + font.getFont().getFamily() + " tamano " + + font.getFont().getSize() + " color " + font.getColor()); + + assertTrue(font.getFont().getFamily().equals("Arial"), + "No es el tipo de fuente especificado en el fichero de propiedades"); + assertEquals(Color.RED, font.getColor(), + "No es el color de fuente especificado en el fichero de propiedades"); + assertEquals(12, font.getFont().getSize(), + "No es el tamaño de fuente especificado en el fichero de propiedades"); + + } catch (IOException e) { + fail("No debería devolver esta excepción"); + e.printStackTrace(); + } } /** * Test method for {@link us.muit.fs.a4i.config.Context#getPropertiesNames()}. - * @throws IOException - * Este método de verificación está incompleto, deberá ser completado commo ejercicio - * Verificar que los nombres son correctos + * + * @throws IOException Este método de verificación está incompleto, deberá ser + * completado commo ejercicio Verificar que los nombres son + * correctos */ @DisplayName("Verificación obtención nombre de propiedades configuradas") @Test diff --git a/src/test/java/us/muit/fs/a4i/test/config/ContextTest2.java b/src/test/java/us/muit/fs/a4i/test/config/ContextTest2.java index 82ed1386..70df9b87 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/ContextTest2.java +++ b/src/test/java/us/muit/fs/a4i/test/config/ContextTest2.java @@ -49,11 +49,11 @@ import java.util.logging.Logger; /** - * @author Isabel Román - * Verificación de la clase context cuando hay ficheros de configuración personalizados + * @author Isabel Román Verificación de la clase context cuando hay ficheros de + * configuración personalizados */ class ContextTest2 { - private static Logger log = Logger.getLogger(CheckerTest.class.getName()); + private static Logger log = Logger.getLogger(ContextTest2.class.getName()); /** * Ruta al fichero de configuración de indicadores y métricas establecidos por * la aplicación @@ -66,16 +66,15 @@ class ContextTest2 { static String appPath; /** - * @throws java.lang.Exception - * Se establecen los ficheros de configuración de la api y de métricas e indicadores propietarios + * @throws java.lang.Exception Se establecen los ficheros de configuración de la + * api y de métricas e indicadores propietarios */ @BeforeAll static void setUpBeforeClass() throws Exception { log.info("Estableciendo las rutas de los ficheros de configuración del cliente"); - //Fichero de métricas e indicadores establecido por el cliente - appPath= "src" + File.separator + "test" + File.separator + "resources" + File.separator - + "appConfTest.json"; - //Fichero de configuración de la api establecido por el cliente + // Fichero de métricas e indicadores establecido por el cliente + appPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appConfTest.json"; + // Fichero de configuración de la api establecido por el cliente appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appTest.conf"; Context.setAppConf(appConfPath); Context.setAppRI(appPath); @@ -100,7 +99,7 @@ void setUp() throws Exception { */ @AfterEach void tearDown() throws Exception { - //Ejecutar tras cada test + // Ejecutar tras cada test } /** @@ -120,38 +119,6 @@ void testGetContext() { } - /** - * Test method for - * {@link us.muit.fs.a4i.config.Context#setAppRI(java.lang.String)}. - */ - @Test - @Tag("Integracion") - void testSetAppRI() { - /** - * Este test excede los límites, ya que no sólo verifica que se establece bien - * la ruta del fichero de especificación de métricas e indicadores sino que se - * leen bien los valores del mismo Sería un test de integración porque se - * requiere que estén ya desarrollados otras clases, aparte de Context (usa la clase Checker) - * EJERCICIO - * Se podría crear un mock de checker para que se convirtiera en un test unidad - */ - try { - - HashMap metricInfo = Context.getContext().getChecker().getMetricConfiguration() - .getMetricInfo("downloads"); - assertNotNull(metricInfo, "No se han leído los atributos de la métrica"); - assertEquals("downloads", metricInfo.get("name"), "El nombre no es el correcto"); - assertEquals("java.lang.Integer", metricInfo.get("type"), "El tipo no es el correcto"); - assertEquals("Descargas realizadas", metricInfo.get("description"), "La descripción no es el correcta"); - assertEquals("downloads", metricInfo.get("unit"), "Las unidades no son correctas"); - - } catch (IOException e) { - fail("No se encuentra el fichero de especificación de métricas e indicadores"); - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - /** * Test method for * {@link us.muit.fs.a4i.config.Context#setAppConf(java.lang.String)}. @@ -159,11 +126,12 @@ void testSetAppRI() { @Test @Tag("Integracion") void testSetAppConf() { - + try { Context.setAppConf(appPath); - assertTrue(appPath.equals(Context.getAppConf()),"No coincide la ruta del fichero de métricas con la configurada"); + assertTrue(appPath.equals(Context.getAppConf()), + "No coincide la ruta del fichero de métricas con la configurada"); } catch (IOException e) { fail("No se encuentra el fichero de especificación de métricas e indicadores"); @@ -217,7 +185,11 @@ void testGetRemoteType() { } /** - *

Este test permite verificar que se lee bien la fuente. Este método es idéntico al de la clase ContextTest porque el fichero personalizado no modifica los parámetros por defecto

+ *

+ * Este test permite verificar que se lee bien la fuente. Este método es + * idéntico al de la clase ContextTest porque el fichero personalizado no + * modifica los parámetros por defecto + *

* Test method for {@link us.muit.fs.a4i.config.Context#getDefaultFont()}. * */ @@ -225,15 +197,19 @@ void testGetRemoteType() { void testGetDefaultFont() { try { Font font = null; - String color; // No entiendo cómo usarlo, ¿para darle valor luego?// + String color; // Uso esto para ver los tipos de fuentes de los que dispongo - //String[] fontNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); - //log.info("listado de fuentes " + Arrays.toString(fontNames)); + // String[] fontNames = + // GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); + // log.info("listado de fuentes " + Arrays.toString(fontNames)); font = Context.getContext().getDefaultFont(); - assertNotNull(font, "No se ha inicializado bien la fuente"); - assertEquals(Color.BLACK.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); - assertEquals(10,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); - assertEquals("Arial",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); + assertNotNull(font, "No se ha inicializado bien la fuente"); + assertEquals(Color.BLACK.toString(), font.getColor().toString(), + "No es el color de fuente especificado en el fichero de propiedades"); + assertEquals(10, font.getFont().getSize(), + "No es el tamaño de fuente especificado en el fichero de propiedades"); + assertEquals("Arial", font.getFont().getFamily(), + "No es el tipo de fuente especificado en el fichero de propiedades"); } catch (IOException e) { fail("No debería devolver esta excepción"); @@ -242,30 +218,36 @@ void testGetDefaultFont() { } /** - *

Este test permite verificar si se sobreescribe la configuración por defecto

+ *

+ * Este test permite verificar si se sobreescribe la configuración por defecto + *

* Test method for {@link us.muit.fs.a4i.config.Context#getMetricFont()}. - * @throws IOException + * + * @throws IOException */ @Test - - void testGetMetricFont(){ + + void testGetMetricFont() { try { - - Font font = null; - // Uso esto para ver los tipos de fuentes de los que dispongo - //String[] fontNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); - //log.info("listado de fuentes " + Arrays.toString(fontNames)); - font = Context.getContext().getMetricFont(); - assertNotNull(font, "No se ha inicializado bien la fuente"); - assertEquals(Color.RED.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); - assertTrue(15 == font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); - assertEquals("Arial",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); - - - }catch (IOException e) { - fail("No debería devolver esta excepción"); - e.printStackTrace(); - } + + Font font = null; + // Uso esto para ver los tipos de fuentes de los que dispongo + // String[] fontNames = + // GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); + // log.info("listado de fuentes " + Arrays.toString(fontNames)); + font = Context.getContext().getMetricFont(); + assertNotNull(font, "No se ha inicializado bien la fuente"); + assertEquals(Color.RED.toString(), font.getColor().toString(), + "No es el color de fuente especificado en el fichero de propiedades"); + assertTrue(15 == font.getFont().getSize(), + "No es el tamaño de fuente especificado en el fichero de propiedades"); + assertEquals("Arial", font.getFont().getFamily(), + "No es el tipo de fuente especificado en el fichero de propiedades"); + + } catch (IOException e) { + fail("No debería devolver esta excepción"); + e.printStackTrace(); + } } /** @@ -276,34 +258,41 @@ void testGetMetricFont(){ void testGetIndicatorFont() { try { Font font = null; - - // Se le solicita la fuente del estado indefinido, que tendrá los valores por defecto al no estar - // definidas sus propiedades en el fichero de configuración utilizados en los tests. - + + // Se le solicita la fuente del estado indefinido, que tendrá los valores por + // defecto al no estar + // definidas sus propiedades en el fichero de configuración utilizados en los + // tests. + font = Context.getContext().getIndicatorFont(IndicatorState.UNDEFINED); assertNotNull(font, "No se ha inicializado bien la fuente"); - // El nombre o tipo de la fuente podrá ser Arial o Times según el momento en el que se realicen los tests. - assertEquals("Arial",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); - + // El nombre o tipo de la fuente podrá ser Arial o Times según el momento en el + // que se realicen los tests. + assertEquals("Arial", font.getFont().getFamily(), + "No es el tipo de fuente especificado en el fichero de propiedades"); + // Se le solicita al contexto la fuente del estao "CRITICAL" font = Context.getContext().getIndicatorFont(IndicatorState.CRITICAL); assertNotNull(font, "No se ha inicializado bien la fuente"); - assertEquals("Courier New",font.getFont().getFamily(),"No es el tipo de fuente especificado en el fichero de propiedades"); - assertEquals(Color.RED.toString(),font.getColor().toString(),"No es el color de fuente especificado en el fichero de propiedades"); - assertEquals(20,font.getFont().getSize(),"No es el tamaño de fuente especificado en el fichero de propiedades"); - - } catch (IOException e) { - fail("No debería devolver esta excepción"); - e.printStackTrace(); - } - } + assertEquals("Courier New", font.getFont().getFamily(), + "No es el tipo de fuente especificado en el fichero de propiedades"); + assertEquals(Color.RED.toString(), font.getColor().toString(), + "No es el color de fuente especificado en el fichero de propiedades"); + assertEquals(20, font.getFont().getSize(), + "No es el tamaño de fuente especificado en el fichero de propiedades"); + } catch (IOException e) { + fail("No debería devolver esta excepción"); + e.printStackTrace(); + } + } /** * Test method for {@link us.muit.fs.a4i.config.Context#getPropertiesNames()}. - * @throws IOException - * Este método de verificación está incompleto, deberá ser completado commo ejercicio - * Verificar que los nombres son correctos + * + * @throws IOException Este método de verificación está incompleto, deberá ser + * completado commo ejercicio Verificar que los nombres son + * correctos */ @Test void testGetPropertiesNames() throws IOException { diff --git a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java index d67e0a83..ab6dfe53 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest.java @@ -18,11 +18,11 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.Mockito; -import us.muit.fs.a4i.config.Context; import us.muit.fs.a4i.config.IndicatorConfiguration; import us.muit.fs.a4i.exceptions.ReportItemException; import us.muit.fs.a4i.model.entities.Indicator; @@ -31,14 +31,24 @@ import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; import us.muit.fs.a4i.model.entities.ReportItemI; +/** + * Clase para verificar IndicatorConfiguration cuando se usa el fichero de + * configuración de indicadores por defecto y además el personalizado por el + * cliente + */ class IndicatorConfigurationTest { private static Logger log = Logger.getLogger(IndicatorConfigurationTest.class.getName()); static IndicatorConfiguration underTest; static String appConfPath; + private static String defaultFile = "a4iDefault.json"; String appIndicatorsPath = "/test/home"; - + /** - *

Acciones a realizar antes de ejecutar los tests definidos en esta clase. En este caso se establece la ruta al fichero de configuración, en la carpeta resources dentro del paquete de test

+ *

+ * Acciones a realizar antes de ejecutar los tests definidos en esta clase. Se + * va a crear un objeto IndicatorConfiguration + *

+ * * @throws java.lang.Exception * @see org.junit.jupiter.api.BeforeAll */ @@ -46,11 +56,14 @@ class IndicatorConfigurationTest { static void setUpBeforeClass() throws Exception { appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appConfTest.json"; + underTest = new IndicatorConfiguration(defaultFile, appConfPath); } - /** - *

Acciones a realizar después de ejecutar todos los tests de esta clase

+ *

+ * Acciones a realizar después de ejecutar todos los tests de esta clase + *

+ * * @throws java.lang.Exception * @see org.junit.jupiter.api.AfterAll */ @@ -59,30 +72,22 @@ static void tearDownAfterClass() throws Exception { log.info("He ejectuado todos los test definidos en esta clase"); } - /** - *

Acciones a realizar antes de cada uno de los tests de esta clase - * en este caso se crea el objeto bajo test, un Checker

- * @throws java.lang.Exception - * @see org.junit.jupiter.api.BeforeEach - */ - @BeforeEach - void setUp() throws Exception { - underTest = new IndicatorConfiguration(); - } - /** * Acciones a realizar después de cada uno de los tests de esta clase + * * @throws java.lang.Exception * @see org.junit.jupiter.api.AfterEach */ @AfterEach void tearDown() throws Exception { - log.info("Acabo de ejecutar un test definido en esta clase"); + log.info("Acabo de ejecutar un test definido en esta clase"); } + @DisplayName("Verifica lectura fichero de configuración por defecto") @Test void testDefinedIndicator() { - // Creo un par de variables, que me servirán de valores para verificar si comprueba bien el tipo + // Creo un par de variables, que me servirán de valores para verificar si + // comprueba bien el tipo // Las métricas del test son de tipo entero, así que creo un entero y un string // (el primero no dará problemas el segundo sí) Double valOKMock = Double.valueOf(0.3); @@ -90,7 +95,7 @@ void testDefinedIndicator() { HashMap returnedMap = null; // Primero, sin fichero de configuración de aplicación try { - + // Consulta un indicador no definido, con valor de tipo entero // debe devolver null, no está definido log.info("Busco el indicador llamado noexiste"); @@ -98,18 +103,10 @@ void testDefinedIndicator() { assertNull(returnedMap, "Debería ser nulo, el indicador noexiste no está definido"); /* - * Indicador que existe en el fichero de configuración por defecto - * { - "name": "issuesProgress", - "type": "java.lang.Double", - "description": "Ratio de issues cerrados frente a totales", - "unit": "ratio", - "limits": { - "ok": 2, - "warning": 4, - "critical": 6 - } - } + * Indicador que existe en el fichero de configuración por defecto { "name": + * "issuesProgress", "type": "java.lang.Double", "description": + * "Ratio de issues cerrados frente a totales", "unit": "ratio", "limits": { + * "ok": 2, "warning": 4, "critical": 6 } } */ // Busco el indicador overdued con valor double, no debería dar problemas log.info("Busco el indicador issuesProgress"); @@ -118,9 +115,12 @@ void testDefinedIndicator() { assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); // Se comprueba que los indicadores incluyen los limites definidos - assertTrue(returnedMap.containsKey("limits.ok"), "La clave correspondiente al limite del estado OK tiene que estar en el mapa"); - assertTrue(returnedMap.containsKey("limits.warning"), "La clave correspondiente al limite del estado WARNING tiene que estar en el mapa"); - assertTrue(returnedMap.containsKey("limits.critical"), "La clave correspondiente al limite del estado CRITICAL tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("limits.ok"), + "La clave correspondiente al limite del estado OK tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("limits.warning"), + "La clave correspondiente al limite del estado WARNING tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("limits.critical"), + "La clave correspondiente al limite del estado CRITICAL tiene que estar en el mapa"); // Busco una métrica que existe pero con un tipo incorrecto assertNull(underTest.definedIndicator("overdued", valKOMock.getClass().getName()), "Debería ser nulo, el indicador overdued está definido para Double"); @@ -129,33 +129,23 @@ void testDefinedIndicator() { e.printStackTrace(); } } - + + /** + * Verifico que encuentra el indicador en el fichero propietario, y que si este + * fichero no existe lanza la excepción adecuada + */ + @DisplayName("Verifica lectura fichero de configuración propietario") @Test void testDefinedIndicatorCustom() { - // Creo un par de variables, que me servirán de valores para verificar si comprueba bien el tipo + // Creo un par de variables, que me servirán de valores para verificar si + // comprueba bien el tipo // Las métricas del test son de tipo entero, así que creo un entero y un string // (el primero no dará problemas el segundo sí) Double valOKMock = Double.valueOf(0.3); String valKOMock = "KO"; HashMap returnedMap = null; - // Ahora establezco el fichero de configuración de la aplicación, con un - // nombre de fichero que no existe - Context.setAppRI("pepe"); try { - // Busco un indicador que se que no está en la configuración de la api - returnedMap = underTest.definedIndicator("pullReqGlory", valOKMock.getClass().getName()); - fail("Debería lanzar una excepción porque intenta buscar en un fichero que no existe"); - } catch (FileNotFoundException e) { - log.info("Lanza la excepción adecuada, FileNotFoud"); - Context.setAppRI(appConfPath); - } catch (Exception e) { - fail("Lanza la excepción equivocada " + e); - } - // Ahora establezco un fichero de configuración de la aplicación - Context.setAppRI(appConfPath); - try { - // Busco una métrica que se que no está en la configuración de la api pero // sí en la de la aplicación log.info("Busco el indicador llamado pullReqGlory"); @@ -164,22 +154,30 @@ void testDefinedIndicatorCustom() { assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); // Se comprueba que los indicadores incluyen los limites definidos - assertTrue(returnedMap.containsKey("limits.ok"), "La clave correspondiente al limite del estado OK tiene que estar en el mapa"); - assertTrue(returnedMap.containsKey("limits.warning"), "La clave correspondiente al limite del estado WARNING tiene que estar en el mapa"); - assertTrue(returnedMap.containsKey("limits.critical"), "La clave correspondiente al limite del estado CRITICAL tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("limits.ok"), + "La clave correspondiente al limite del estado OK tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("limits.warning"), + "La clave correspondiente al limite del estado WARNING tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("limits.critical"), + "La clave correspondiente al limite del estado CRITICAL tiene que estar en el mapa"); } catch (FileNotFoundException e) { fail("No debería devolver esta excepción"); } catch (Exception e) { fail("Lanza una excepción no reconocida " + e); } + } - /** - *

En este test verifico que si busco el nombre de una métrica el método que verifica el indicador no lo confunde

- */ - @Test - void testDefinedIndicatorAsMetric() { + + /** + *

+ * En este test verifico que si busco el nombre de una métrica el método que + * verifica el indicador no lo confunde + *

+ */ + @Test + void testDefinedIndicatorAsMetric() { Integer valOKMock = Integer.valueOf(3); - + HashMap returnedMap = null; try { @@ -187,76 +185,73 @@ void testDefinedIndicatorAsMetric() { log.info("Busco el indicador llamado pullReqGlory"); returnedMap = underTest.definedIndicator("subscribers", valOKMock.getClass().getName()); assertNull(returnedMap, "Debería ser nulo, subscribers es una métrica y no un indicador no está definido"); - }catch (Exception e) { + } catch (Exception e) { fail("Lanza la excepción " + e); } - } + } @Test void testListAllIndicators() { - fail("Not yet implemented"); + /* + * En el momento de codificar este test (21/3/25) el número de indicadores en el + * fichero de configuración por defecto es 9 y en el de la aplicación 2 + */ + try { + assertEquals(11, underTest.listAllIndicators().size(), "El número de indicadores no es el esperado"); + } catch (FileNotFoundException e) { + fail("No debería lanzar esta excepción"); + e.printStackTrace(); + } } @Test - void testGetIndicatorState(){ - // Prueba 1. - Context.setAppRI(appConfPath); - + void testGetIndicatorState() { ReportItemI indicator = null; IndicatorI.IndicatorState estado = IndicatorI.IndicatorState.UNDEFINED; - + try { indicator = new ReportItemBuilder("overdued", 2.0).build(); } catch (ReportItemException e) { fail("El archivo de configuración esperado no contiene el indicador necesario para esta prueba."); } - + estado = underTest.getIndicatorState(indicator); - + assertTrue(estado == IndicatorI.IndicatorState.OK, "El estado es INCORRECTO. Debería de ser OK."); - - - - - try { - indicator = new ReportItemBuilder("overdued", 9.0).build(); - } catch (ReportItemException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - + + try { + indicator = new ReportItemBuilder("overdued", 9.0).build(); + } catch (ReportItemException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + estado = underTest.getIndicatorState(indicator); - + assertTrue(estado == IndicatorI.IndicatorState.WARNING, "El estado es INCORRECTO. Debería de ser WARNING."); + try { + indicator = new ReportItemBuilder("overdued", 13.0).build(); + } catch (ReportItemException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } - - try { - indicator = new ReportItemBuilder("overdued", 13.0).build(); - } catch (ReportItemException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - estado = underTest.getIndicatorState(indicator); - + assertTrue(estado == IndicatorI.IndicatorState.CRITICAL, "El estado es INCORRECTO. Debería de ser CRITICAL."); + try { + indicator = new ReportItemBuilder("issuesRatio", 13.0).build(); + } catch (ReportItemException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } - - try { - indicator = new ReportItemBuilder("issuesRatio", 13.0).build(); - } catch (ReportItemException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - estado = underTest.getIndicatorState(indicator); - - assertTrue(estado == IndicatorI.IndicatorState.UNDEFINED, "El estado es INCORRECTO. Debería de ser UNDEFINED."); + assertTrue(estado == IndicatorI.IndicatorState.UNDEFINED, "El estado es INCORRECTO. Debería de ser UNDEFINED."); } - } diff --git a/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest2.java b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest2.java new file mode 100644 index 00000000..6c2e0f4b --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/config/IndicatorConfigurationTest2.java @@ -0,0 +1,86 @@ +/** + * + */ +package us.muit.fs.a4i.test.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.HashMap; +import java.util.List; +import java.util.logging.Logger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +import us.muit.fs.a4i.config.Context; +import us.muit.fs.a4i.config.IndicatorConfiguration; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.Indicator; +import us.muit.fs.a4i.model.entities.IndicatorI; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; +import us.muit.fs.a4i.model.entities.ReportItemI; + +/** + * Clase para verificar IndicatorConfiguration cuando se configura un fichero de + * cliente que no existe + */ +class IndicatorConfigurationTest2 { + private static Logger log = Logger.getLogger(IndicatorConfigurationTest2.class.getName()); + static IndicatorConfiguration underTest; + static String appConfPath; + private static String defaultFile = "a4iDefault.json"; + String appIndicatorsPath = "/test/home"; + + /** + *

+ * Acciones a realizar antes de ejecutar los tests definidos en esta clase. Se + * va a crear un objeto IndicatorConfiguration + *

+ * + * @throws java.lang.Exception + * @see org.junit.jupiter.api.BeforeAll + */ + @BeforeAll + static void setUpBeforeClass() throws Exception { + appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + + "appConfTestKO.json"; + underTest = new IndicatorConfiguration(defaultFile, appConfPath); + } + + /** + * Verifico que si el fichero no existe lanza la excepción adecuada + */ + @DisplayName("Intento de acceso a fichero que no existe") + @Test + void testDefinedIndicatorCustom() { + + List indicadores = null; + try { + + // Busco una métrica que se que no está en la configuración de la api pero + // sí en la de la aplicación + log.info("Consulto los indicadores"); + indicadores = underTest.listAllIndicators(); + fail("Debería haber devuelto una excepción"); + + } catch (FileNotFoundException e) { + assertNotNull(e, "Debería haber recibido una excepción"); + } catch (Exception e) { + fail("Lanza una excepción no reconocida " + e); + } + } + +} diff --git a/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest.java b/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest.java index 1fb05580..4d0e3687 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest.java @@ -1,3 +1,6 @@ +/** + * + */ package us.muit.fs.a4i.test.config; import static org.junit.jupiter.api.Assertions.*; @@ -6,121 +9,261 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.logging.Logger; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import us.muit.fs.a4i.config.Checker; -import us.muit.fs.a4i.config.Context; +import us.muit.fs.a4i.config.IndicatorConfiguration; import us.muit.fs.a4i.config.MetricConfiguration; import us.muit.fs.a4i.config.MetricConfigurationI; +/** + * Verificación de la clase MetricConfiguration si sólo se usa el fichero de + * ocnfiguración por defecto + */ class MetricConfigurationTest { + private static Logger log = Logger.getLogger(MetricConfigurationTest.class.getName()); - static MetricConfiguration underTest; + private static MetricConfigurationI underTest; + static String appConfPath; + private static String defaultFile = "a4iDefault.json"; /** - *

- * Acciones a realizar antes de ejecutar los tests definidos en esta clase. - *

- * * @throws java.lang.Exception - * @see org.junit.jupiter.api.BeforeAll */ @BeforeAll static void setUpBeforeClass() throws Exception { - underTest = new MetricConfiguration(); + + underTest = new MetricConfiguration(defaultFile, null); } + /** + *

+ * Test para verificar el método + * {@link us.muit.fs.a4i.config.MetricConfiguration#definedMetric(java.lang.String, java.lang.String)}. + * Si la métrica no está definida no debería devolver el mapa clave las + * etiquetas: + * + * Las métricas pueden estar definidas en el fichero de configuración de la api + * (a4iDefault.json) o en otro fichero configurado por la aplicación cliente. + * Para los test este fichero es appConfTest.json y se guarda junto al código de + * test, en la carpeta resources + * + * @see org.junit.jupiter.api.Tag + * @see org.junit.jupiter.api.Test + * @see org.junit.jupiter.api.DisplayName * + *

+ */ + @Tag("unidad") + @DisplayName("Verificación definedMetric si la métrica no existe en el fichero de configuración por defecto") @Test - void testDefinedMetric() { + void testDefinedMetric1() { + + // Las métricas del test son de enteros, así que creo un entero y un string (el + // primero no dará problemas el segundo sí + Integer valOKMock = Integer.valueOf(3); + String valKOMock = "KO"; + HashMap returnedMap = null; + // Primero, sin fichero de configuraci�n de aplicaci�n try { + // Busco una métrica que se que no está en la configuración de la api + log.info("Busco la métrica llamada downloads"); + returnedMap = underTest.definedMetric("downloads", valOKMock.getClass().getName()); + assertNull(returnedMap, + "Debería ser nulo, la métrica downloads no está definida en el fichero de configuración por defecto"); + } catch (FileNotFoundException e) { + fail("El fichero NO está en la carpeta resources"); + e.printStackTrace(); + } + + } + + /** + *

+ * Test para verificar el método + * {@link us.muit.fs.a4i.config.MetricConfiguration#definedMetric(java.lang.String, java.lang.String)}. + * Si la métrica está definida y el tipo de valor que se quiere establecer es el + * adecuado debe devolver un hashmap con los datos de la métrica, usando como + * clave las etiquetas: + *

    + *
  • description
  • + *
  • unit
  • + *
+ * Las métricas pueden estar definidas en el fichero de configuración de la api + * (a4iDefault.json) o en otro fichero configurado por la aplicación cliente. + * Para los test este fichero es appConfTest.json y se guarda junto al código de + * test, en la carpeta resources + * + * @see org.junit.jupiter.api.Tag + * @see org.junit.jupiter.api.Test + * @see org.junit.jupiter.api.DisplayName + *

+ */ + @Tag("unidad") + @DisplayName("Verificación definedMetric si la métrica existe en el fichero de configuración por defecto y el tipo es correcto") + @Test + void testDefinedMetric2() { + // Creo valores Mock para verificar si comprueba bien el tipo + // Las m�tricas del test son de enteros, as� que creo un entero y un string (el + // primero no dar� problemas el segundo sí) + Integer valOKMock = Integer.valueOf(3); + + HashMap returnedMap = null; + // Primero, sin fichero de configuraci�n de aplicaci�n + try { + /* * En el fichero por defecto la métrica issues está definida del siguiente modo * { "name": "issues", "type": "java.lang.Integer", "description": * "Tareas totales", "unit": "issues" } */ // Primero se busca la métrica con el tipo definido - HashMap metricInfo = underTest.definedMetric("issues", "java.lang.Integer"); + HashMap metricInfo = underTest.definedMetric("issues", valOKMock.getClass().getName()); assertEquals("issues", metricInfo.get("name"), "No se ha leído bien el nombre de la métrica"); assertEquals("java.lang.Integer", metricInfo.get("type"), "No se ha leído bien el tipo de la métrica"); - assertEquals("Tareas totales",metricInfo.get("description"), + assertEquals("Tareas totales", metricInfo.get("description"), "No se ha leído bien la descripción de la métrica"); assertEquals(metricInfo.get("unit"), "issues", "No se ha leído bien las unidades de la métrica"); - // ahora busco con un tipo incorrecto - // Primero se busca la métrica con el tipo definido - metricInfo = underTest.definedMetric("issues", "java.lang.String"); - assertNull(metricInfo, "No debería haber localizado la métrica"); - } catch (IOException e) { - + } catch (FileNotFoundException e) { + fail("El fichero NO está en la carpeta resources"); e.printStackTrace(); - fail("No debería devolver esta excepción"); } + } /** - * Verifica la localización de una métrica que existe en el fichero de - * configuración por defecto + *

+ * Test para verificar el método + * {@link us.muit.fs.a4i.config.MetricConfiguration#definedMetric(java.lang.String, java.lang.String)}. + * Si la métrica está definida pero el tipo de valor no es el adecuado no debe + * devolver el mapa Las métricas pueden estar definidas en el fichero de + * configuración de la api (a4iDefault.json) o en otro fichero configurado por + * la aplicación cliente. Para los test este fichero es appConfTest.json y se + * guarda junto al código de test, en la carpeta resources + * + * @see org.junit.jupiter.api.Tag + * @see org.junit.jupiter.api.Test + * @see org.junit.jupiter.api.DisplayName * + *

*/ - @DisplayName("Verificación de lectura métrica disponible en configuración por defecto") + @Tag("unidad") + @DisplayName("Verificación definedMetric si la métrica existe en el fichero de configuración por defecto pero el tipo es incorrecto") @Test - void testGetMetricInfoOK() { + void testDefinedMetric3() { + // Creo valores Mock para verificar si comprueba bien el tipo + // Las m�tricas del test son de enteros, as� que creo un entero y un string (el + // primero no dar� problemas el segundo sí) + Integer valOKMock = Integer.valueOf(3); + String valKOMock = "KO"; + HashMap returnedMap = null; + // Primero, sin fichero de configuraci�n de aplicaci�n try { + /* * En el fichero por defecto la métrica issues está definida del siguiente modo * { "name": "issues", "type": "java.lang.Integer", "description": * "Tareas totales", "unit": "issues" } */ - // Primero se busca una métrica que existe - HashMap metricInfo = underTest.getMetricInfo("issues"); - assertEquals( "issues", metricInfo.get("name"),"No se ha leído bien el nombre de la métrica"); - assertEquals("java.lang.Integer", metricInfo.get("type"), "No se ha leído bien el tipo de la métrica"); - assertEquals("Tareas totales",metricInfo.get("description"), - "No se ha leído bien la descripción de la métrica"); - assertEquals("issues", metricInfo.get("unit"),"No se ha leído bien las unidades de la métrica"); - } catch (IOException e) { + // Busco la métrica con tipo incorrecto + HashMap metricInfo = underTest.definedMetric("issues", valKOMock.getClass().getName()); + assertNull(metricInfo, "La métrica no tiene el tipo correcto, no debería devolver el mapa"); + + } catch (FileNotFoundException e) { + fail("El fichero NO está en la carpeta resources"); + e.printStackTrace(); + } + + } + + /** + * Test method for + * {@link us.muit.fs.a4i.config.MetricConfiguration#getMetricInfo(java.lang.String)}. + * Si la métrica no está configurada no debe devolver el mapa + */ + @Tag("unidad") + @DisplayName("Verificación de getMetricInfo si la métrica no existe en el fichero de configuración") + @Test + void testGetMetricInfo1() { + HashMap returnedMap = null; + // Las métricas del test son de enteros, así que creo un entero y un string (el + // primero no dará problemas el segundo sí + Integer valOKMock = Integer.valueOf(3); + String valKOMock = "KO"; + // Primero, sin fichero de configuraci�n de aplicaci�n + try { + // Busco una métrica que se que no está en la configuración de la api + log.info("Busco la métrica llamada downloads"); + returnedMap = underTest.getMetricInfo("downloads"); + assertNull(returnedMap, + "Debería ser nulo, la métrica downloads no está definida en el fichero de configuración por defecto"); + } catch (FileNotFoundException e) { + fail("El fichero NO está en la carpeta resources"); e.printStackTrace(); - fail("No debería devolver esta excepción"); } + } /** - * Verifica la localización de una métrica que NO existe en el fichero de - * configuración por defecto + * Test method for + * {@link us.muit.fs.a4i.config.MetricConfiguration#definedMetric(java.lang.String, java.lang.String)}. + * Si la métrica existe debe devolver el mapa con los valores */ - @DisplayName("Verificación de lectura métrica no existente") + @Tag("unidad") + @DisplayName("Verificación getMetricInfo si la métrica existe en el fichero de configuración por defecto") @Test - void testGetMetricInfoKO() { + void testGetMetricInfo2() { + HashMap returnedMap = null; + try { + /* - * En el fichero por defecto la métrica noexiste no existe + * En el fichero por defecto la métrica issues está definida del siguiente modo + * { "name": "issues", "type": "java.lang.Integer", "description": + * "Tareas totales", "unit": "issues" } */ + // Primero se busca la métrica con el tipo definido + HashMap metricInfo = underTest.getMetricInfo("issues"); + assertEquals("issues", metricInfo.get("name"), "No se ha leído bien el nombre de la métrica"); + assertEquals("java.lang.Integer", metricInfo.get("type"), "No se ha leído bien el tipo de la métrica"); + assertEquals("Tareas totales", metricInfo.get("description"), + "No se ha leído bien la descripción de la métrica"); + assertEquals(metricInfo.get("unit"), "issues", "No se ha leído bien las unidades de la métrica"); - HashMap metricInfo = underTest.getMetricInfo("noexiste"); - assertNull(metricInfo, "El mapa no debe haberse creado"); - } catch (IOException e) { - fail("Lanza excepcion indebida, no localiza el fichero"); + } catch (FileNotFoundException e) { + fail("El fichero NO está en la carpeta resources"); e.printStackTrace(); - } + } + /** + * Test method for + * {@link us.muit.fs.a4i.config.MetricConfiguration#listAllMetrics()}. El número + * de métricas debe coincidir con las que haya en el fichero de configuración + */ + @Tag("unidad") + @DisplayName("Verificación de consulta de métricas para el fichero de configuración por defecto") @Test void testListAllMetrics() { - /* - * Actualmente hay 39 métricas en el fichero de configuración por defecto - * (17/3/25) - */ + List metricsList; try { - assertEquals(39, underTest.listAllMetrics().size(), "El número de métricas leídas no es correcto"); + metricsList = underTest.listAllMetrics(); + /** + * En el momento de codificar este test 21/3/25 el número de métricas en el + * fichero de configuración por defecto es 39 + */ + assertEquals(39, metricsList.size(), "No lee correctamente las métricas"); } catch (FileNotFoundException e) { - fail("Lanza excepción indebida, no localiza el fichero"); + fail("No debería lanzar esta excepción"); e.printStackTrace(); } + } } diff --git a/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest2.java b/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest2.java new file mode 100644 index 00000000..1f34222c --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest2.java @@ -0,0 +1,277 @@ +/** + * + */ +package us.muit.fs.a4i.test.config; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.logging.Logger; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.config.IndicatorConfiguration; +import us.muit.fs.a4i.config.MetricConfiguration; +import us.muit.fs.a4i.config.MetricConfigurationI; + +/** + * En esta clase se verifica la clase MetricConfiguration pero usando un fichero + * de configuración de métricas de la aplicación cliente + */ +class MetricConfigurationTest2 { + + private static Logger log = Logger.getLogger(MetricConfigurationTest2.class.getName()); + private static MetricConfigurationI underTest; + static String appConfPath; + private static String defaultFile = "a4iDefault.json"; + String appIndicatorsPath = "/test/home"; + + /** + * @throws java.lang.Exception + */ + @BeforeAll + static void setUpBeforeClass() throws Exception { + appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + + "appConfTest.json"; + underTest = new MetricConfiguration(defaultFile, appConfPath); + } + + /** + * Test method for + * {@link us.muit.fs.a4i.config.MetricConfiguration#definedMetric(java.lang.String, java.lang.String)}. + * En este caso se usa un único métdo para verificar todos los casos + *

+ * Test para verificar el método + * {@link us.muit.fs.a4i.config.MetricConfiguration#definedMetric(java.lang.String, java.lang.String)}. + * Si la métrica está definida y el tipo de valor que se quiere establecer es el + * adecuado debe devolver un hashmap con los datos de la métrica, usando como + * clave las etiquetas: + *

    + *
  • description
  • + *
  • unit
  • + *
+ * Pero si no está definida no debe crear el mapa Las métricas pueden estar + * definidas en el fichero de configuración de la api (a4iDefault.json) o en + * otro fichero configurado por la aplicación cliente. Para los test este + * fichero es appConfTest.json y se guarda junto al código de test, en la + * carpeta resources + * + * @see org.junit.jupiter.api.Tag + * @see org.junit.jupiter.api.Test + * @see org.junit.jupiter.api.DisplayName + *

+ */ + @Tag("unidad") + @DisplayName("Verificación del método definedMetric") + @Test + void testDefinedMetric() { + // Creo valores Mock para verificar si comprueba bien el tipo + // Las m�tricas del test son de enteros, as� que creo un entero y un string (el + // primero no dar� problemas el segundo sí) + Integer valOKMock = Integer.valueOf(3); + String valKOMock = "KO"; + HashMap returnedMap = null; + // Primero, sin fichero de configuraci�n de aplicaci�n + try { + // Consulta una m�trica no definida, con valor de tipo entero + // debe devolver null, no est� definida + log.info("Busco la métrica llamada NoExiste"); + returnedMap = underTest.definedMetric("NoExiste", valOKMock.getClass().getName()); + assertNull(returnedMap, "Debería ser nulo, la métrica NoExiste no está definida"); + /* + * En el fichero por defecto la métrica issues está definida del siguiente modo + * { "name": "issues", "type": "java.lang.Integer", "description": + * "Tareas totales", "unit": "issues" } + */ + // Primero se busca la métrica con el tipo definido + HashMap metricInfo = underTest.definedMetric("issues", "java.lang.Integer"); + assertEquals("issues", metricInfo.get("name"), "No se ha leído bien el nombre de la métrica"); + assertEquals("java.lang.Integer", metricInfo.get("type"), "No se ha leído bien el tipo de la métrica"); + assertEquals("Tareas totales", metricInfo.get("description"), + "No se ha leído bien la descripción de la métrica"); + assertEquals(metricInfo.get("unit"), "issues", "No se ha leído bien las unidades de la métrica"); + + // Busco la métrica watchers con valor entero + log.info("Busco la métrica watchers"); + returnedMap = underTest.definedMetric("watchers", valOKMock.getClass().getName()); + assertNotNull(returnedMap, "Debería devolver un hashmap, la métrica está definida"); + assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); + + // Busco la métrica watchers con un tipo incorrecto + assertNull(underTest.definedMetric("watchers", valKOMock.getClass().getName()), + "Debería ser nulo, la métrica está definida para Integer"); + // Busco una métrica que se que no está en la configuración de la api pero sí en + // la de la aplicación + log.info("Busco la métrica llamada downloads"); + returnedMap = underTest.definedMetric("downloads", valOKMock.getClass().getName()); + assertNotNull(returnedMap, "Debería devolver un hashmap, la métrica está definida"); + assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); + } catch (FileNotFoundException e) { + fail("El fichero NO está en la carpeta resources"); + e.printStackTrace(); + } + + } + + /** + * Test method for + * {@link us.muit.fs.a4i.config.MetricConfiguration#definedMetric(java.lang.String, java.lang.String)}. + * /** + *

+ * Test para verificar el método + * {@link us.muit.fs.a4i.config.MetricConfiguration#metricInfo(java.lang.String)}. + * Si la métrica está definida en el fichero de configuración del cliente debe + * devolver un hashmap con los datos de la métrica, usando como clave las + * etiquetas: + *

    + *
  • description
  • + *
  • unit
  • + *
+ * Pero si no está definida no debe crear el mapa Las métricas pueden estar + * definidas en el fichero de configuración de la api (a4iDefault.json) o en + * otro fichero configurado por la aplicación cliente. Para los test este + * fichero es appConfTest.json y se guarda junto al código de test, en la + * carpeta resources + * + * @see org.junit.jupiter.api.Tag + * @see org.junit.jupiter.api.Test + * @see org.junit.jupiter.api.DisplayName + *

+ */ + @Tag("unidad") + @DisplayName("Verificación de lectura métrica disponible en configuración de la aplicación") + @Test + void testGetMetricInfo1() { + HashMap returnedMap; + try { + // Busco una métrica que se que no está en la configuración de la api pero sí en + // la de la aplicación + log.info("Busco la métrica llamada downloads"); + returnedMap = underTest.getMetricInfo("downloads"); + assertNotNull(returnedMap, "Debería devolver un hashmap, la métrica está definida"); + assertTrue(returnedMap.containsKey("unit"), "La clave unit tiene que estar en el mapa"); + assertTrue(returnedMap.containsKey("description"), "La clave description tiene que estar en el mapa"); + + } catch (FileNotFoundException e) { + fail("no debería devolver esta excepción"); + e.printStackTrace(); + } + } + + /** + * Test method for + * {@link us.muit.fs.a4i.config.MetricConfiguration#listAllMetrics()}. Debe + * devolver una lista con todas las métricas, en ambos ficheros + */ + @Tag("unidad") + @DisplayName("Verificación de consulta de métricas con los dos ficheros de configuración") + @Test + void testListAllMetrics() { + List metricsList; + try { + metricsList = underTest.listAllMetrics(); + /** + * En el momento de codificar este test 21/3/25 el número de métricas totales es + * 41 (39 de configuración + 2 del cliente) + */ + assertEquals(41, metricsList.size(), "No lee correctamente las métricas"); + } catch (FileNotFoundException e) { + fail("No debería lanzar esta excepción"); + e.printStackTrace(); + } + + } + + /** + * Test method for + * {@link us.muit.fs.a4i.config.MetricConfiguration#definedMetric(java.lang.String, java.lang.String)}. + * /** + *

+ * Test para verificar el método + * {@link us.muit.fs.a4i.config.MetricConfiguration#metricInfo(java.lang.String)}. + * Si la métrica está definida en el fichero por defecto debe devolver un + * hashmap con los datos de la métrica, usando como clave las etiquetas: + *

    + *
  • description
  • + *
  • unit
  • + *
+ * Pero si no está definida no debe crear el mapa Las métricas pueden estar + * definidas en el fichero de configuración de la api (a4iDefault.json) o en + * otro fichero configurado por la aplicación cliente. Para los test este + * fichero es appConfTest.json y se guarda junto al código de test, en la + * carpeta resources + * + * @see org.junit.jupiter.api.Tag + * @see org.junit.jupiter.api.Test + * @see org.junit.jupiter.api.DisplayName + *

+ */ + @Tag("unidad") + @DisplayName("Verificación de lectura métrica disponible en configuración por defecto") + @Test + void testGetMetricInfo2() { + try { + /* + * En el fichero por defecto la métrica issues está definida del siguiente modo + * { "name": "issues", "type": "java.lang.Integer", "description": + * "Tareas totales", "unit": "issues" } + */ + // Primero se busca una métrica que existe + HashMap metricInfo = underTest.getMetricInfo("issues"); + assertEquals("issues", metricInfo.get("name"), "No se ha leído bien el nombre de la métrica"); + assertEquals("java.lang.Integer", metricInfo.get("type"), "No se ha leído bien el tipo de la métrica"); + assertEquals("Tareas totales", metricInfo.get("description"), + "No se ha leído bien la descripción de la métrica"); + assertEquals("issues", metricInfo.get("unit"), "No se ha leído bien las unidades de la métrica"); + } catch (IOException e) { + + e.printStackTrace(); + fail("No debería devolver esta excepción"); + } + } + + /** + * Test method for + * {@link us.muit.fs.a4i.config.MetricConfiguration#definedMetric(java.lang.String, java.lang.String)}. + * /** + *

+ * Test para verificar el método + * {@link us.muit.fs.a4i.config.MetricConfiguration#definedMetric(java.lang.String, java.lang.String)}. + * Si la métrica no está definida no debe crear el mapa Las métricas pueden + * estar definidas en el fichero de configuración de la api (a4iDefault.json) o + * en otro fichero configurado por la aplicación cliente. Para los test este + * fichero es appConfTest.json y se guarda junto al código de test, en la + * carpeta resources + * + * @see org.junit.jupiter.api.Tag + * @see org.junit.jupiter.api.Test + * @see org.junit.jupiter.api.DisplayName + *

+ */ + @DisplayName("Verificación de lectura métrica no existente") + @Test + void testGetMetricInfo3() { + try { + /* + * En el fichero por defecto la métrica noexiste no existe + */ + + HashMap metricInfo = underTest.getMetricInfo("noexiste"); + assertNull(metricInfo, "El mapa no debe haberse creado"); + } catch (IOException e) { + fail("Lanza excepcion indebida, no localiza el fichero"); + e.printStackTrace(); + + } + } + +} diff --git a/src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java b/src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java deleted file mode 100644 index ae721fec..00000000 --- a/src/test/java/us/muit/fs/a4i/test/control/ReportManagerTest.java +++ /dev/null @@ -1,201 +0,0 @@ -/** - * - */ -package us.muit.fs.a4i.test.control; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; - -import java.util.logging.Logger; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import us.muit.fs.a4i.control.ReportManager; -import us.muit.fs.a4i.control.ReportManagerI; -import us.muit.fs.a4i.model.entities.ReportI; - -/** - * @author isabo - * - */ -public class ReportManagerTest { - private static Logger log = Logger.getLogger(ReportManagerTest.class.getName()); - - /** - * @throws java.lang.Exception - */ - @BeforeAll - static void setUpBeforeClass() throws Exception { - } - - /** - * @throws java.lang.Exception - */ - @AfterAll - static void tearDownAfterClass() throws Exception { - } - - /** - * @throws java.lang.Exception - */ - @BeforeEach - void setUp() throws Exception { - } - - /** - * @throws java.lang.Exception - */ - @AfterEach - void tearDown() throws Exception { - } - - /** - * Test method for - * {@link us.muit.fs.a4i.control.ReportManager#ReportManager(us.muit.fs.a4i.model.entities.ReportI.ReportType)}. - */ - @Test - void testReportManager() { - ReportManagerI manager = null; - ReportI report = null; - try { - manager = new ReportManager(ReportI.ReportType.REPOSITORY); - assertNotNull(manager, "No se ha creado el gestor"); - log.info("Creado el gestor"); - report = manager.newReport("MIT-FS/Audit4Improve-API", ReportI.ReportType.REPOSITORY); - log.info("Creado el informe"); - } catch (Exception e) { - fail("Se lanza alguna excepción al crear el gestor o el informe"); - e.printStackTrace(); - } - assertNotNull(report, "No se ha construido el informe"); - System.out.println(report); - } - - /** - * Test method for - * {@link us.muit.fs.a4i.control.ReportManager#deleteReport(us.muit.fs.a4i.model.entities.ReportI)}. - */ - @Test - void testDeleteReportReportI() { - fail("Not yet implemented"); - } - - /** - * Test method for - * {@link us.muit.fs.a4i.control.ReportManager#setRemoteEnquirer(us.muit.fs.a4i.model.remote.RemoteEnquirer)}. - */ - @Test - void testSetRemoteEnquirer() { - fail("Not yet implemented"); - } - - /** - * Test method for - * {@link us.muit.fs.a4i.control.ReportManager#setPersistenceManager(us.muit.fs.a4i.persistence.PersistenceManager)}. - */ - @Test - void testSetPersistenceManager() { - fail("Not yet implemented"); - } - - /** - * Test method for - * {@link us.muit.fs.a4i.control.ReportManager#setFormater(us.muit.fs.a4i.persistence.ReportFormaterI)}. - */ - @Test - void testSetFormater() { - fail("Not yet implemented"); - } - - /** - * Test method for - * {@link us.muit.fs.a4i.control.ReportManager#setIndicatorCalc(us.muit.fs.a4i.control.IndicatorsCalculator)}. - */ - @Test - void testSetIndicatorCalc() { - fail("Not yet implemented"); - } - - /** - * Test method for - * {@link us.muit.fs.a4i.control.ReportManager#saveReport(us.muit.fs.a4i.model.entities.ReportI)}. - */ - @Test - void testSaveReportReportI() { - fail("Not yet implemented"); - } - - /** - * Test method for {@link us.muit.fs.a4i.control.ReportManager#saveReport()}. - */ - @Test - void testSaveReport() { - fail("Not yet implemented"); - } - - /** - * Test method for - * {@link us.muit.fs.a4i.control.ReportManager#newReport(java.lang.String, us.muit.fs.a4i.model.entities.ReportI.ReportType)}. - */ - @Test - void testNewReport() { - fail("Not yet implemented"); - } - - /** - * Test method for {@link us.muit.fs.a4i.control.ReportManager#deleteReport()}. - */ - @Test - void testDeleteReport() { - fail("Not yet implemented"); - } - - /** - * Test method for {@link us.muit.fs.a4i.control.ReportManager#getReport()}. - */ - @Test - void testGetReport() { - fail("Not yet implemented"); - } - - /** - * Test method for - * {@link us.muit.fs.a4i.control.ReportManager#addMetric(java.lang.String)}. - */ - @Test - void testAddMetric() { - fail("Not yet implemented"); - } - - /** - * Test method for - * {@link us.muit.fs.a4i.control.ReportManager#getMetric(java.lang.String)}. - */ - @Test - void testGetMetric() { - fail("Not yet implemented"); - } - - /** - * Test method for - * {@link us.muit.fs.a4i.control.ReportManager#addIndicator(java.lang.String)}. - */ - @Test - void testAddIndicator() { - fail("Not yet implemented"); - } - - /** - * Test method for - * {@link us.muit.fs.a4i.control.ReportManager#getIndicator(java.lang.String)}. - */ - @Test - void testGetIndicator() { - fail("Not yet implemented"); - } - -} diff --git a/src/test/java/us/muit/fs/a4i/test/control/calculators/RepositoryCalculatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/calculators/RepositoryCalculatorTest.java new file mode 100644 index 00000000..ef885c1f --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/control/calculators/RepositoryCalculatorTest.java @@ -0,0 +1,211 @@ +package us.muit.fs.a4i.test.control.calculators; + +/*** + * @author celllarod, curso 22/23 + * Pruebas añadidas por alumnos del curso 22/23 para probar la clase RepositoryCalculator + * REF: https://javadoc.io/doc/org.mockito/mockito-core/4.3.1/org/mockito/Mockito.html + */ + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.control.IndicatorStrategy; +import us.muit.fs.a4i.control.ReportManagerI; +import us.muit.fs.a4i.control.calculators.RepositoryCalculator; +import us.muit.fs.a4i.control.strategies.IssuesRatioIndicatorStrategy; + +import us.muit.fs.a4i.exceptions.IndicatorException; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.IndicatorI; +import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; + +import us.muit.fs.a4i.model.entities.ReportItemI; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +public class RepositoryCalculatorTest { + + private static Logger log = Logger.getLogger(RepositoryCalculatorTest.class.getName()); + + /** + * Test para el metodo calcIndicator de RepositoryCalculator cuando las métricas + * que se pasan no son las necesarias RepositoryCalculator debe antes obtenerlas + * {@link us.muit.fs.a4i.control.RepositoryCalculator.calcIndicator(String, + * ReportManagerI)}. + * + * @throws NotAvailableMetricException + * @throws ReportItemException + */ + @Test + @Tag("unidad") + @DisplayName("Prueba calcIndicator de RepositoryCalculator metricas incorrectas") + void testCalIndicatorNotRequiredMetrics() throws NotAvailableMetricException, ReportItemException { + // Creamos mocks necesarios, de menor a mayor + // primero las métricas + + ReportItemI metrica1 = Mockito.mock(ReportItemI.class); + ReportItemI metrica2 = Mockito.mock(ReportItemI.class); + // Tengo que hacer que metrica1 y metrica2 devuelvan lo que devolvería el de + // verdad + Mockito.when(metrica1.getName()).thenReturn("issues"); + Mockito.when(metrica1.getValue()).thenReturn(2); + + Mockito.when(metrica2.getName()).thenReturn("closedIssues"); + Mockito.when(metrica2.getValue()).thenReturn(1.0); + List metricsMock = new ArrayList<>(); + metricsMock.add(metrica1); + metricsMock.add(metrica2); + + // Ahora la estrategia de cálculo + // Empezando por Las dependencias que va indicar la estrategia de cálculo + List required = new ArrayList(); + required.add("otradiferente"); + required.add("closedIssues"); + + // El indicador que va a calcular + ReportItemI indicator = Mockito.mock(ReportItemI.class); + Mockito.when(indicator.getName()).thenReturn("indicador"); + + IndicatorStrategy indicatorStrategyMock = Mockito.mock(IndicatorStrategy.class); + Mockito.when(indicatorStrategyMock.requiredMetrics()).thenReturn(required); + Mockito.when(indicatorStrategyMock.calcIndicator(metricsMock)).thenReturn(indicator); + + // Ahora gestor de informe + // Empezamos creando el informe + ReportI report = Mockito.mock(ReportI.class); + Mockito.when(report.getAllMetrics()).thenReturn(metricsMock); + Mockito.when(report.getMetricByName("issues")).thenReturn(metrica1); + Mockito.when(report.getMetricByName("closedIssues")).thenReturn(metrica2); + + ReportManagerI reportManagerMock = Mockito.mock(ReportManagerI.class); + Mockito.when(reportManagerMock.getMetric("issues")).thenReturn(metrica1); + Mockito.when(reportManagerMock.getMetric("closedIssues")).thenReturn(metrica2); + Mockito.when(reportManagerMock.getReport()).thenReturn(report); + + + // Creamos la clase a probar + RepositoryCalculator repositoryCalculator = new RepositoryCalculator(); + repositoryCalculator.setIndicator("indicator", indicatorStrategyMock); + try { + repositoryCalculator.calcIndicator("indicator", reportManagerMock); + + + } catch (IndicatorException e) { + fail("No debería lanzar la excepción"); + e.printStackTrace(); + } + + // Verificamos que se ha invocado el método para añadir la métrica que falta, pero las otras no + Mockito.verify(reportManagerMock, times(1)).addMetric("otradiferente"); + Mockito.verify(reportManagerMock, times(0)).addMetric("issues"); + Mockito.verify(reportManagerMock, times(0)).addMetric("clossedIssues"); + + + // Verificamos que se han comprobado las métricas, invocado el método de cálculo + // y que el indicador se ha añadido al informe + Mockito.verify(indicatorStrategyMock, times(1)).requiredMetrics(); + Mockito.verify(indicatorStrategyMock, times(1)).calcIndicator(metricsMock); + Mockito.verify(report, times(1)).addIndicator(indicator); + //Al menos una vez + Mockito.verify(reportManagerMock,Mockito.atLeastOnce()).getReport(); + + Mockito.verify(report,Mockito.atLeastOnce()).getAllMetrics(); + } + + /** + * Test para el metodo calcIndicator de RepositoryCalculator cuando las métricas + * disponibles son suficientes + * {@link us.muit.fs.a4i.control.RepositoryCalculator.calcIndicator(String, + * ReportManagerI)}. + * + * @throws NotAvailableMetricException + * @throws ReportItemException + */ + @Test + @Tag("unidad") + @DisplayName("Prueba calcIndicator de RepositoryCalculator metricas correctas") + void testCalIndicatorWithRequiredMetrics() throws NotAvailableMetricException, ReportItemException { + // Si no queremos depender de los objetos reales habría que eliminar las + // dependencias, pero en este caso se ha etiquetado como prueba + // de integración + + // Creamos la clase a probar + RepositoryCalculator repositoryCalculator = new RepositoryCalculator(); + + // Creamos mocks necesarios + + ReportItemI metrica1 = Mockito.mock(ReportItemI.class); + ReportItemI metrica2 = Mockito.mock(ReportItemI.class); + // Tengo que hacer que metrica1 y metrica2 devuelvan lo que devolvería el de + // verdad + Mockito.when(metrica1.getName()).thenReturn("issues"); + Mockito.when(metrica1.getValue()).thenReturn(2); + + Mockito.when(metrica2.getName()).thenReturn("closedIssues"); + Mockito.when(metrica2.getValue()).thenReturn(1.0); + List metricsMock = new ArrayList<>(); + metricsMock.add(metrica1); + metricsMock.add(metrica2); + List required = new ArrayList(); + required.add("issues"); + required.add("closedIssues"); + + ReportItemI indicator = Mockito.mock(ReportItemI.class); + Mockito.when(indicator.getName()).thenReturn("indicador"); + + ReportI report = Mockito.mock(ReportI.class); + Mockito.when(report.getAllMetrics()).thenReturn(metricsMock); + + ReportManagerI reportManagerMock = Mockito.mock(ReportManagerI.class); + Mockito.when(reportManagerMock.getMetric("issues")).thenReturn(metrica1); + Mockito.when(reportManagerMock.getMetric("closedIssues")).thenReturn(metrica2); + Mockito.when(reportManagerMock.getReport()).thenReturn(report); + + IndicatorStrategy indicatorStrategyMock = Mockito.mock(IndicatorStrategy.class); + Mockito.when(indicatorStrategyMock.calcIndicator(metricsMock)).thenReturn(indicator); + Mockito.when(indicatorStrategyMock.requiredMetrics()).thenReturn(required); + + repositoryCalculator.setIndicator("indicator", indicatorStrategyMock); + try { + repositoryCalculator.calcIndicator("indicator", reportManagerMock); + } catch (IndicatorException e) { + fail("No debería lanzar la excepción"); + e.printStackTrace(); + } + // Verificamos que NO se ha invocado el método para añadir ninguna de las + // métricas + Mockito.verify(reportManagerMock, times(0)).addMetric("issues"); + Mockito.verify(reportManagerMock, times(0)).addMetric("clossedIssues"); + + // Verificamos que se han comprobado las métricas, invocado el método de cálculo + // y que el indicador se ha añadido al informe + Mockito.verify(indicatorStrategyMock, times(1)).requiredMetrics(); + Mockito.verify(indicatorStrategyMock, times(1)).calcIndicator(metricsMock); + Mockito.verify(report, times(1)).addIndicator(indicator); + + //Al menos una vez + Mockito.verify(reportManagerMock,Mockito.atLeastOnce()).getReport(); + Mockito.verify(report,Mockito.atLeastOnce()).getAllMetrics(); + } + +} diff --git a/src/test/java/us/muit/fs/a4i/test/control/managers/ReportManagerTest.java b/src/test/java/us/muit/fs/a4i/test/control/managers/ReportManagerTest.java new file mode 100644 index 00000000..45046983 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/control/managers/ReportManagerTest.java @@ -0,0 +1,51 @@ +/** + * + */ +package us.muit.fs.a4i.test.control.managers; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.logging.Logger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + + +import us.muit.fs.a4i.control.ReportManagerI; +import us.muit.fs.a4i.control.managers.ReportManager; +import us.muit.fs.a4i.model.entities.ReportI; + +/** + * @author Isabel Román Martínez + * + */ +public class ReportManagerTest { + private static Logger log = Logger.getLogger(ReportManagerTest.class.getName()); + + /** + * Test method for + * {@link us.muit.fs.a4i.control.ReportManager#ReportManager(us.muit.fs.a4i.model.entities.ReportI.ReportType)}. + */ + @Test + void testReportManager() { + ReportManagerI manager = null; + ReportI report = null; + try { + manager = new ReportManager(ReportI.ReportType.REPOSITORY); + assertNotNull(manager, "No se ha creado el gestor"); + log.info("Creado el gestor"); + report = manager.newReport("MIT-FS/Audit4Improve-API", ReportI.ReportType.REPOSITORY); + log.info("Creado el informe"); + } catch (Exception e) { + fail("Se lanza alguna excepción al crear el gestor o el informe"); + e.printStackTrace(); + } + assertNotNull(report, "No se ha construido el informe"); + System.out.println(report); + } + +} diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/ConventionsCompliantStrategyTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/ConventionsCompliantStrategyTest.java index db351d02..ef738582 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/strategies/ConventionsCompliantStrategyTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/ConventionsCompliantStrategyTest.java @@ -44,27 +44,6 @@ static void setUpBeforeClass() throws Exception { calculation = (0.2 * 0.86) + (0.2 * 0) + (0.2 * 0.92) + (0.2 * 0) + (0.2 * 0.98); } - /** - * @throws java.lang.Exception - */ - @AfterAll - static void tearDownAfterClass() throws Exception { - } - - /** - * @throws java.lang.Exception - */ - @BeforeEach - void setUp() throws Exception { - } - - /** - * @throws java.lang.Exception - */ - @AfterEach - void tearDown() throws Exception { - } - // MARK: - Test methods /** * The indicator is calculated based on the following metrics: diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/FixTimeStrategyTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/FixTimeStrategyTest.java index 98b79ff4..38652f72 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/strategies/FixTimeStrategyTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/FixTimeStrategyTest.java @@ -46,7 +46,7 @@ public void testCalcIndicator() throws NotAvailableMetricException { ReportItemI result = strategy.calcIndicator(metrics); // Comprobamos que el resultado es el esperado - Assertions.assertEquals("ProcesoDeIssues", result.getName()); + Assertions.assertEquals("fixTime", result.getName()); Assertions.assertEquals(3.0, result.getValue()); Assertions.assertDoesNotThrow(() -> strategy.calcIndicator(metrics)); diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/RepositoryCalculatorTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/RepositoryCalculatorTest.java deleted file mode 100644 index 25c9a7d0..00000000 --- a/src/test/java/us/muit/fs/a4i/test/control/strategies/RepositoryCalculatorTest.java +++ /dev/null @@ -1,252 +0,0 @@ -package us.muit.fs.a4i.test.control.strategies; - -/*** - * @author celllarod, curso 22/23 - * Pruebas añadidas por alumnos del curso 22/23 para probar la clase RepositoryCalculator - */ - -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.times; - -import org.mockito.Mockito; - -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Logger; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; - -import us.muit.fs.a4i.control.IndicatorStrategy; -import us.muit.fs.a4i.control.ReportManagerI; -import us.muit.fs.a4i.control.strategies.IssuesRatioIndicatorStrategy; -import us.muit.fs.a4i.control.strategies.RepositoryCalculator; -import us.muit.fs.a4i.exceptions.IndicatorException; -import us.muit.fs.a4i.exceptions.NotAvailableMetricException; -import us.muit.fs.a4i.exceptions.ReportItemException; -import us.muit.fs.a4i.model.entities.IndicatorI; -import us.muit.fs.a4i.model.entities.ReportI; -import us.muit.fs.a4i.model.entities.ReportItem; -import us.muit.fs.a4i.model.entities.ReportItem.ReportItemBuilder; -import us.muit.fs.a4i.test.control.ReportManagerTest; -import us.muit.fs.a4i.model.entities.ReportItemI; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class RepositoryCalculatorTest { - - private static Logger log = Logger.getLogger(ReportManagerTest.class.getName()); - - /** - * @throws java.lang.Exception - */ - @BeforeAll - static void setUpBeforeClass() throws Exception { - } - - /** - * @throws java.lang.Exception - */ - @AfterAll - static void tearDownAfterClass() throws Exception { - - } - - /** - * @throws java.lang.Exception - */ - @BeforeEach - void setUp() throws Exception { - - } - - /** - * @throws java.lang.Exception - */ - @AfterEach - void tearDown() throws Exception { - } - - /** - * Test para el metodo calcIndicator de RepositoryCalculator - * {@link us.muit.fs.a4i.control.RepositoryCalculator.calcIndicator(String, - * ReportManagerI)}. - */ - @Test - @DisplayName("Prueba calcIndicator de RepositoryCalculator") - void testCalIndicator() { - - // Creamos la clase a probar - RepositoryCalculator repositoryCalculator = new RepositoryCalculator(); - - // Creamos mocks necesarios - ReportManagerI reportManagerMock = Mockito.mock(ReportManagerI.class); - ReportI report = Mockito.mock(ReportI.class); - - List metricsMock = new ArrayList<>(); - ReportItemI metric1 = Mockito.mock(ReportItemI.class); - // Faltaba: configurar el método getName de las métricas - Mockito.when(metric1.getName()).thenReturn("metric1"); - ReportItemI metric2 = Mockito.mock(ReportItemI.class); - Mockito.when(metric2.getName()).thenReturn("metric2"); - - metricsMock.add(metric1); - metricsMock.add(metric2); - - IndicatorStrategy indicatorStrategyMock = Mockito.mock(IndicatorStrategy.class); - - Mockito.when(reportManagerMock.getReport()).thenReturn(report); - - // Metrics - Mockito.when(report.getAllMetrics()).thenReturn(metricsMock); - - // Required metrics para el indicador forzando a que coincidan con las m�tricas - // creadas - List requiredMetrics = new ArrayList<>(); - requiredMetrics.add(metric1.getName()); - requiredMetrics.add(metric2.getName()); - ReportItemI nuevoIndicador = Mockito.mock(ReportItemI.class); - Mockito.when(nuevoIndicador.getName()).thenReturn("nuevoIndicador"); - Mockito.when(indicatorStrategyMock.requiredMetrics()).thenReturn(requiredMetrics); - try { - Mockito.when(indicatorStrategyMock.calcIndicator(metricsMock)).thenReturn(nuevoIndicador); - } catch (NotAvailableMetricException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - // Seteamos alguna estrategia - repositoryCalculator.setIndicator("indicator1", indicatorStrategyMock); - - // Llamamos a calIndicator - try { - repositoryCalculator.calcIndicator("indicator1", reportManagerMock); - } catch (IndicatorException e) { - e.printStackTrace(); - } - - // Verificamos que cada uno de los m�todos hayan sido llamados - - Mockito.verify(report).getAllMetrics(); - Mockito.verify(indicatorStrategyMock).requiredMetrics(); - try { - Mockito.verify(indicatorStrategyMock).calcIndicator(metricsMock); - } catch (NotAvailableMetricException e) { - e.printStackTrace(); - } - } - - /** - * Test para el metodo calcIndicator de RepositoryCalculator Verifica que no - * llama a calIndiicator si no se le pasa la m�trica adecuada - * {@link us.muit.fs.a4i.control.RepositoryCalculator.calcIndicator(String, - * ReportManagerI)}. - * - * @throws NotAvailableMetricException - * @throws ReportItemException - */ - @Test - @Tag("unit") - @DisplayName("Prueba calcIndicator de RepositoryCalculator con metricas incorrectas y usando mocks") - void unitTestCalIndicatorNotRequiredMetrics() throws NotAvailableMetricException, ReportItemException { - // prueba la calculadora usando mocks, para el caso de que las métricas - // necesarias no estén disponibles - - // Creamos la clase a probar - RepositoryCalculator repositoryCalculator = new RepositoryCalculator(); - - // Creamos mocks necesarios - ReportManagerI reportManagerMock = Mockito.mock(ReportManagerI.class); - ReportI report = Mockito.mock(ReportI.class); - - List metricsMock = new ArrayList<>(); - ReportItemI metric1 = Mockito.mock(ReportItemI.class); - // Faltaba: configurar el método getName de las métricas - Mockito.when(metric1.getName()).thenReturn("metric1"); - ReportItemI metric2 = Mockito.mock(ReportItemI.class); - Mockito.when(metric2.getName()).thenReturn("metric2"); - - metricsMock.add(metric1); - metricsMock.add(metric2); - - IndicatorStrategy indicatorStrategyMock = Mockito.mock(IndicatorStrategy.class); - - Mockito.when(reportManagerMock.getReport()).thenReturn(report); - - // Metrics - Mockito.when(report.getAllMetrics()).thenReturn(metricsMock); - - // Required metrics para el indicador forzando a que coincidan con las m�tricas - // creadas - List requiredMetrics = new ArrayList<>(); - requiredMetrics.add("otra1"); - requiredMetrics.add("otra2"); - ReportItemI nuevoIndicador = Mockito.mock(ReportItemI.class); - Mockito.when(nuevoIndicador.getName()).thenReturn("nuevoIndicador"); - Mockito.when(indicatorStrategyMock.requiredMetrics()).thenReturn(requiredMetrics); - - // Seteamos alguna estrategia - repositoryCalculator.setIndicator("indicator1", indicatorStrategyMock); - - // Llamamos a calIndicator - try { - repositoryCalculator.calcIndicator("indicator1", reportManagerMock); - } catch (IndicatorException e) { - e.printStackTrace(); - } - - // Verificamos que no se ha llamado al m�todo - Mockito.verify(indicatorStrategyMock, times(0)).calcIndicator(metricsMock); - } - - /** - * Test para el metodo calcIndicator de RepositoryCalculator Verifica que no - * llama a calIndiicator si no se le pasa la m�trica adecuada - * {@link us.muit.fs.a4i.control.RepositoryCalculator.calcIndicator(String, - * ReportManagerI)}. - * - * @throws NotAvailableMetricException - * @throws ReportItemException - */ - @Test - @Tag("integration") - @DisplayName("Prueba calcIndicator de RepositoryCalculator metricas incorrectas") - void testCalIndicatorNotRequiredMetrics() throws NotAvailableMetricException, ReportItemException { - // Si no queremos depender de los objetos reales habría que eliminar las - // dependencias, pero en este caso se ha etiquetado como prueba - // de integración - - // Creamos la clase a probar - RepositoryCalculator repositoryCalculator = new RepositoryCalculator(); - - // Creamos mocks necesarios - ReportManagerI reportManagerMock = Mockito.mock(ReportManagerI.class); - ReportI report = Mockito.mock(ReportI.class); - - List metricsMock = new ArrayList<>(); - - ReportItemBuilder mb1 = new ReportItem.ReportItemBuilder("issues", 2); - ReportItemBuilder mb2 = new ReportItem.ReportItemBuilder("closedIssues", 1.0); - - metricsMock.add(mb1.build()); - metricsMock.add(mb2.build()); - - IndicatorStrategy indicatorStrategyMock = Mockito.mock(IndicatorStrategy.class); - - Mockito.when(reportManagerMock.getReport()).thenReturn(report); - - // Metrics - Mockito.when(report.getAllMetrics()).thenReturn(metricsMock); - - // Seteamos alguna estrategia - repositoryCalculator.setIndicator("issuesRatio", new IssuesRatioIndicatorStrategy()); - - // Verificamos que no se ha llamado al m�todo - Mockito.verify(indicatorStrategyMock, times(0)).calcIndicator(metricsMock); - } - -} diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java index 1d66f69e..c7969ae8 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/GitHubRepositoryEnquirerTest.java @@ -13,6 +13,7 @@ import us.muit.fs.a4i.exceptions.MetricException; import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.ReportI; import us.muit.fs.a4i.model.entities.ReportItem; import us.muit.fs.a4i.model.entities.ReportItemI; import us.muit.fs.a4i.model.remote.GitHubRepositoryEnquirer; @@ -211,4 +212,12 @@ ReportItemI testGetMetric(String metricString) { assertNotNull(reportItem, "Getting the metric (" + metricString + ") failed: reportItem is null"); return reportItem; } + + //Test de construcción del informe (ReportI) + @Test + void testGetReport() { + ReportI report=ghEnquirer.buildReport("MIT-FS/Audit4Improve-API"); + assertNotNull(report,"No construye el informe"); + log.info("Informe construido "+report.toString()); + } } diff --git a/src/test/resources/excelTest.xlsx b/src/test/resources/excelTest.xlsx index 6c3ee61553d196a5d3893c9aa8a81dd7a268131e..b7300c1b51e333cd54a6c99df592bfb51051854d 100644 GIT binary patch delta 5558 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 C`t9rh delta 5447 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 Q4=y&S^Df9aH21sz0E#Z%bN~PV diff --git a/src/test/resources/log.properties b/src/test/resources/log.properties index e13e2bf0..5e34c3d3 100644 --- a/src/test/resources/log.properties +++ b/src/test/resources/log.properties @@ -1,9 +1,16 @@ +# para que este fichero de configuración de trazas sea efectivo es necesario pasarlo como parámetro de la máquina virtual al ejecutar +#-Djava.util.logging.config.file="src\test\resources\log.properties" # especificacion de detalle de log # nivel de log global .level = OFF # javax.json.Json.level = FINEST us.muit.fs.a4i.model.remote.GitHubRepositoryEnquirer.level = INFO -us.muit.fs.a4i.persistence.ExcelReportManager.level = ALL +us.muit.fs.a4i.persistence.ExcelReportManager.level = FINEST +us.muit.fs.a4i.config.Context.level=FINEST +us.muit.fs.a4i.config.Checker.level=FINEST +us.muit.fs.a4i.config.IndicatorConfiguration.level=FINEST +us.muit.fs.a4i.config.MetricConfiguration.level=FINEST +us.muit.fs.a4i.control.calculators.RepositoryCalculator.level = FINEST # nivel clases @@ -15,6 +22,6 @@ handlers = java.util.logging.ConsoleHandler # configuración de manejador de consola # nivel soportado para consola -java.util.logging.ConsoleHandler.level = INFO +java.util.logging.ConsoleHandler.level = FINEST # clase para formatear salida hacia consola java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter From 3cf4718677fb6fcd0f2d747c17ec3c128286c53f Mon Sep 17 00:00:00 2001 From: MIT-FS Date: Sat, 22 Mar 2025 18:42:05 +0100 Subject: [PATCH 091/100] =?UTF-8?q?Incluida=20gesti=C3=B3n=20de=20excepcio?= =?UTF-8?q?nes=20FileNotFound=20en=20MetricsConfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fs/a4i/config/MetricConfiguration.java | 167 +++++++++++------- .../test/config/MetricConfigurationTest.java | 48 ++++- .../test/config/MetricConfigurationTest2.java | 50 +++++- 3 files changed, 197 insertions(+), 68 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java index 38818e2c..c7521f58 100644 --- a/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java +++ b/src/main/java/us/muit/fs/a4i/config/MetricConfiguration.java @@ -15,12 +15,14 @@ import javax.json.JsonReader; public class MetricConfiguration implements MetricConfigurationI { - - /** - * El creador de MetricConfiguration tiene que indicar los nombres de los ficheros de métricas por defecto y del cliente - * @param defaultRI nombre del fichero de métricas por defecto - * @param appRI nombre del fichero de métricas definido por el cliente - */ + + /** + * El creador de MetricConfiguration tiene que indicar los nombres de los + * ficheros de métricas por defecto y del cliente + * + * @param defaultRI nombre del fichero de métricas por defecto + * @param appRI nombre del fichero de métricas definido por el cliente + */ public MetricConfiguration(String defaultRI, String appRI) { super(); this.defaultRI = defaultRI; @@ -40,10 +42,9 @@ public MetricConfiguration(String defaultRI, String appRI) { * @param isr InputStreamReader del fichero de configuración * @return Mapa de parámetros de la métrica, siempre que exista y sea del tipo * especificado - * @throws FileNotFoundException + * */ - private HashMap isDefinedMetric(String metricName, String metricType, InputStreamReader isr) - throws FileNotFoundException { + private HashMap isDefinedMetric(String metricName, String metricType, InputStreamReader isr) { HashMap metricDefinition = null; @@ -82,9 +83,9 @@ private HashMap isDefinedMetric(String metricName, String metric * @param metricName String con el nombre de la métrica buscada * @param isr InputStreamReader del fichero de configuración * @return Mapa con las características de la métrica - * @throws FileNotFoundException + * */ - private HashMap getMetric(String metricName, InputStreamReader isr) throws FileNotFoundException { + private HashMap getMetric(String metricName, InputStreamReader isr) { HashMap metricDefinition = null; @@ -118,25 +119,36 @@ public HashMap definedMetric(String name, String type) throws Fi log.info("Checker solicitud de búsqueda métrica " + name); HashMap metricDefinition = null; - + InputStream is = null; + InputStreamReader isr = null; String filePath = "/" + defaultRI; - log.info("Buscando el archivo " + filePath); - InputStream is = this.getClass().getResourceAsStream(filePath); - log.info("InputStream " + is + " para " + filePath); - InputStreamReader isr = new InputStreamReader(is); - - /** - * Busca primero en el fichero de configuración de métricas por defecto - */ - metricDefinition = isDefinedMetric(name, type, isr); - /** - * En caso de que no estuviera ahí la métrica busco en el fichero de - * configuración de la aplicación - */ - if ((metricDefinition == null) && appRI != null) { - is = new FileInputStream(appRI); + try { + + log.info("Buscando el archivo " + filePath); + is = this.getClass().getResourceAsStream(filePath); + log.info("InputStream " + is + " para " + filePath); isr = new InputStreamReader(is); + + /** + * Busca primero en el fichero de configuración de métricas por defecto + */ metricDefinition = isDefinedMetric(name, type, isr); + } catch (NullPointerException e) { + throw new FileNotFoundException( + "No se localiza el fichero de la configuración por defecto de la API " + filePath); + } + try { + /** + * En caso de que no estuviera ahí la métrica busco en el fichero de + * configuración de la aplicación + */ + if ((metricDefinition == null) && appRI != null) { + is = new FileInputStream(appRI); + isr = new InputStreamReader(is); + metricDefinition = isDefinedMetric(name, type, isr); + } + } catch (NullPointerException e) { + throw new FileNotFoundException("No se localiza el fichero de la aplicación cliente " + appRI); } return metricDefinition; @@ -146,25 +158,37 @@ public HashMap definedMetric(String name, String type) throws Fi public HashMap getMetricInfo(String name) throws FileNotFoundException { log.info("Consulta información de la métrica " + name); HashMap metricDefinition = null; - String filePath = "/" + defaultRI; - log.info("Buscando el archivo " + filePath); - InputStream is = this.getClass().getResourceAsStream(filePath); - log.info("InputStream " + is + " para " + filePath); - InputStreamReader isr = new InputStreamReader(is); - - /** - * Busca primero en el fichero de configuración de métricas por defecto - */ - metricDefinition = getMetric(name, isr); - /** - * En caso de que no estuviera ahí la métrica busco en el fichero de - * configuración de la aplicación - */ - if ((metricDefinition == null) && appRI != null) { - is = new FileInputStream(appRI); + InputStream is = null; + InputStreamReader isr = null; + try { + + log.info("Buscando el archivo " + filePath); + + is = this.getClass().getResourceAsStream(filePath); + log.info("InputStream " + is + " para " + filePath); isr = new InputStreamReader(is); + + /** + * Busca primero en el fichero de configuración de métricas por defecto + */ metricDefinition = getMetric(name, isr); + } catch (NullPointerException e) { + throw new FileNotFoundException( + "No se localiza el fichero de la configuración por defecto de la API " + filePath); + } + try { + /** + * En caso de que no estuviera ahí la métrica busco en el fichero de + * configuración de la aplicación + */ + if ((metricDefinition == null) && appRI != null) { + is = new FileInputStream(appRI); + isr = new InputStreamReader(is); + metricDefinition = getMetric(name, isr); + } + } catch (NullPointerException e) { + throw new FileNotFoundException("No se localiza el fichero de la aplicación cliente " + appRI); } return metricDefinition; @@ -175,32 +199,24 @@ public List listAllMetrics() throws FileNotFoundException { log.info("Consulta todas las métricas"); List allmetrics = new ArrayList(); + InputStream is = null; + InputStreamReader isr = null; + JsonReader reader = null; + JsonObject confObject = null; + JsonArray metrics = null; String filePath = "/" + defaultRI; - log.info("Buscando el archivo " + filePath); - InputStream is = this.getClass().getResourceAsStream(filePath); - log.info("InputStream " + is + " para " + filePath); - InputStreamReader isr = new InputStreamReader(is); - - JsonReader reader = Json.createReader(isr); - log.info("Creo el JsonReader"); - - JsonObject confObject = reader.readObject(); - log.info("Leo el objeto"); - reader.close(); - - log.info("Muestro la configuración leída " + confObject); - JsonArray metrics = confObject.getJsonArray("metrics"); - log.info("El número de métricas es " + metrics.size()); - for (int i = 0; i < metrics.size(); i++) { - log.info("Añado nombre: " + metrics.get(i).asJsonObject().getString("name")); - allmetrics.add(metrics.get(i).asJsonObject().getString("name")); - } - if (appRI != null) { - is = new FileInputStream(appRI); + try { + log.info("Buscando el archivo " + filePath); + is = this.getClass().getResourceAsStream(filePath); + log.info("InputStream " + is + " para " + filePath); isr = new InputStreamReader(is); + reader = Json.createReader(isr); + log.info("Creo el JsonReader"); + confObject = reader.readObject(); + log.info("Leo el objeto"); reader.close(); log.info("Muestro la configuración leída " + confObject); @@ -210,6 +226,29 @@ public List listAllMetrics() throws FileNotFoundException { log.info("Añado nombre: " + metrics.get(i).asJsonObject().getString("name")); allmetrics.add(metrics.get(i).asJsonObject().getString("name")); } + } catch (NullPointerException e) { + throw new FileNotFoundException( + "No se localiza el fichero de la configuración por defecto de la API " + filePath); + } + + try { + if (appRI != null) { + is = new FileInputStream(appRI); + isr = new InputStreamReader(is); + reader = Json.createReader(isr); + confObject = reader.readObject(); + reader.close(); + + log.info("Muestro la configuración leída " + confObject); + metrics = confObject.getJsonArray("metrics"); + log.info("El número de métricas es " + metrics.size()); + for (int i = 0; i < metrics.size(); i++) { + log.info("Añado nombre: " + metrics.get(i).asJsonObject().getString("name")); + allmetrics.add(metrics.get(i).asJsonObject().getString("name")); + } + } + } catch (NullPointerException e) { + throw new FileNotFoundException("No se localiza el fichero de la aplicación cliente " + appRI); } return allmetrics; diff --git a/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest.java b/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest.java index 4d0e3687..b2d8082e 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest.java +++ b/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest.java @@ -13,6 +13,7 @@ import java.util.logging.Logger; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -33,10 +34,11 @@ class MetricConfigurationTest { private static String defaultFile = "a4iDefault.json"; /** - * @throws java.lang.Exception + * @throws java.lang.Exception Crea el objeto bajo test únicamente con el + * fichero de configuración por defecto */ - @BeforeAll - static void setUpBeforeClass() throws Exception { + @BeforeEach + void setUp() throws Exception { underTest = new MetricConfiguration(defaultFile, null); } @@ -266,4 +268,44 @@ void testListAllMetrics() { } + /** + *

+ * Test para verificar que se lanza adecuadamente la excepción de fichero no + * localizado en todos los métodos + * + * @see org.junit.jupiter.api.Tag + * @see org.junit.jupiter.api.Test + * @see org.junit.jupiter.api.DisplayName + *

+ */ + @DisplayName("Verificación de excepción FileNotFound cuando el fichero por defecto no está bien indicado") + @Test + void testExceptionFile() { + underTest = new MetricConfiguration("defaultKO", appConfPath); + /* + * el fichero por defecto no existe, pruebo los tres métodos + */ + FileNotFoundException thrown = assertThrows(FileNotFoundException.class, + + () -> underTest.getMetricInfo("downloads"), + "Debería haber lanzado la excepción de fichero no encontrado, pero no lo ha hecho"); + + assertTrue(thrown.getMessage().contains("defaultKO"), "La excepción debería indicar el fichero no localizado"); + + thrown = assertThrows(FileNotFoundException.class, + + () -> underTest.definedMetric("downloads", "java.lang.Integer"), + "Debería haber lanzado la excepción de fichero no encontrado, pero no lo ha hecho"); + + assertTrue(thrown.getMessage().contains("defaultKO"), "La excepción debería indicar el fichero no localizado"); + + thrown = assertThrows(FileNotFoundException.class, + + () -> underTest.listAllMetrics(), + "Debería haber lanzado la excepción de fichero no encontrado, pero no lo ha hecho"); + + assertTrue(thrown.getMessage().contains("defaultKO"), "La excepción debería indicar el fichero no localizado"); + + } + } diff --git a/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest2.java b/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest2.java index 1f34222c..33b37cca 100644 --- a/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest2.java +++ b/src/test/java/us/muit/fs/a4i/test/config/MetricConfigurationTest2.java @@ -40,7 +40,7 @@ class MetricConfigurationTest2 { static void setUpBeforeClass() throws Exception { appConfPath = "src" + File.separator + "test" + File.separator + "resources" + File.separator + "appConfTest.json"; - underTest = new MetricConfiguration(defaultFile, appConfPath); + } /** @@ -72,6 +72,7 @@ static void setUpBeforeClass() throws Exception { @DisplayName("Verificación del método definedMetric") @Test void testDefinedMetric() { + underTest = new MetricConfiguration(defaultFile, appConfPath); // Creo valores Mock para verificar si comprueba bien el tipo // Las m�tricas del test son de enteros, as� que creo un entero y un string (el // primero no dar� problemas el segundo sí) @@ -151,6 +152,7 @@ void testDefinedMetric() { @DisplayName("Verificación de lectura métrica disponible en configuración de la aplicación") @Test void testGetMetricInfo1() { + underTest = new MetricConfiguration(defaultFile, appConfPath); HashMap returnedMap; try { // Busco una métrica que se que no está en la configuración de la api pero sí en @@ -176,6 +178,7 @@ void testGetMetricInfo1() { @DisplayName("Verificación de consulta de métricas con los dos ficheros de configuración") @Test void testListAllMetrics() { + underTest = new MetricConfiguration(defaultFile, appConfPath); List metricsList; try { metricsList = underTest.listAllMetrics(); @@ -219,6 +222,7 @@ void testListAllMetrics() { @DisplayName("Verificación de lectura métrica disponible en configuración por defecto") @Test void testGetMetricInfo2() { + underTest = new MetricConfiguration(defaultFile, appConfPath); try { /* * En el fichero por defecto la métrica issues está definida del siguiente modo @@ -260,6 +264,7 @@ void testGetMetricInfo2() { @DisplayName("Verificación de lectura métrica no existente") @Test void testGetMetricInfo3() { + underTest = new MetricConfiguration(defaultFile, appConfPath); try { /* * En el fichero por defecto la métrica noexiste no existe @@ -274,4 +279,47 @@ void testGetMetricInfo3() { } } + /** + *

+ * Test para verificar que se lanza adecuadamente la excepción de fichero no + * localizado en todos los métodos + * + * @see org.junit.jupiter.api.Tag + * @see org.junit.jupiter.api.Test + * @see org.junit.jupiter.api.DisplayName + *

+ */ + @DisplayName("Verificación de excepción FileNotFound cuando el fichero de configuración del cliente no está bien especificado") + @Test + void testExceptionFile() { + + underTest = new MetricConfiguration(defaultFile, "clienteKO"); + /* + * el fichero de la api cliente no existe, pruebo los tres métodos + */ + FileNotFoundException thrown = assertThrows(FileNotFoundException.class, + + () -> underTest.definedMetric("downloads", "java.lang.Integer"), + "Debería haber lanzado la excepción de fichero no encontrado, pero no lo ha hecho"); + + assertTrue(thrown.getMessage().contains("clienteKO"), "La excepción debería indicar el fichero no localizado"); + + thrown = assertThrows(FileNotFoundException.class, + + () -> underTest.listAllMetrics(), + "Debería haber lanzado la excepción de fichero no encontrado, pero no lo ha hecho"); + + assertTrue(thrown.getMessage().contains("clienteKO"), "La excepción debería indicar el fichero no localizado"); + + /* + * el fichero de la api cliente no existe + */ + thrown = assertThrows(FileNotFoundException.class, + + () -> underTest.getMetricInfo("downloads"), + "Debería haber lanzado la excepción de fichero no encontrado, pero no lo ha hecho"); + + assertTrue(thrown.getMessage().contains("clienteKO"), "La excepción debería indicar el fichero no localizado"); + } + } From 4396e3e8e92124ebc0487a1895c7e3ab0ae7752b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Rom=C3=A1n?= Date: Thu, 3 Apr 2025 12:27:34 +0200 Subject: [PATCH 092/100] Update build.gradle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a1ce0aaa..95067239 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ tasks.withType(Javadoc){ options.version = true options.use = true options.memberLevel = JavadocMemberLevel.PROTECTED - options.footer = "MIT-FS: Curso 2021/22" + options.footer = "MIT-FS" title = "Audit4Improve-API" } From 6b8b9b46fdbd43e7b1e61976e98158101b2f45c3 Mon Sep 17 00:00:00 2001 From: juan Date: Sat, 17 May 2025 11:34:18 +0200 Subject: [PATCH 093/100] Add test IndicatorStrategy and Modified JSON to add metrics and indicator --- .vscode/settings.json | 3 + src/main/resources/a4iDefault.json | 35 ++++ .../control/strategies/IEFStrategyTest.java | 151 ++++++++++++++++++ src/test/resources/appConfTest.json | 35 ++++ 4 files changed, 224 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 src/test/java/us/muit/fs/a4i/test/control/strategies/IEFStrategyTest.java diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..7b016a89 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/src/main/resources/a4iDefault.json b/src/main/resources/a4iDefault.json index 33cee922..6b6965fe 100644 --- a/src/main/resources/a4iDefault.json +++ b/src/main/resources/a4iDefault.json @@ -233,6 +233,30 @@ "type": "java.lang.Integer", "description": "Balance de equipos y open issues", "unit": "ratio" + }, + { + "name": "cycleTime", + "type": "java.lang.Double", + "description": "Tiempo medio que tarda una tarea en completarse", + "unit": "horas" + }, + { + "name": "waitTime", + "type": "java.lang.Double", + "description": "Tiempo medio que las tareas permanecen en espera o bloqueadas", + "unit": "horas" + }, + { + "name": "throughput", + "type": "java.lang.Double", + "description": "Número de tareas completadas en un período determinado", + "unit": "tareas/semana" + }, + { + "name": "WIP", + "type": "java.lang.Double", + "description": "Promedio de tareas en curso (trabajo en progreso)", + "unit": "tareas" } ], "indicators": [ @@ -310,6 +334,17 @@ "type": "java.lang.Double", "description": "Tiempo para arreglos", "unit": "ratio" + }, + { + "name": "IEF", + "type": "java.lang.Double", + "description": "Ãndice de Eficiencia del Flujo", + "unit": "índice", + "limits": { + "ok": 0.8, + "warning": 0.6, + "critical": 0.4 + } } ] } \ No newline at end of file diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/IEFStrategyTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/IEFStrategyTest.java new file mode 100644 index 00000000..a707dea5 --- /dev/null +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/IEFStrategyTest.java @@ -0,0 +1,151 @@ + +package us.muit.fs.a4i.test.control.strategies; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +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.control.strategies.IEFStrategy; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; +import us.muit.fs.a4i.model.entities.ReportItemI; +import us.muit.fs.a4i.model.entities.ReportItem; + +public class IEFStrategyTest { + private static Logger log = Logger.getLogger(IEFStrategyTest.class.getName()); + private IEFStrategy strat; + + @BeforeEach + void setUp() { + strat = new IEFStrategy(); + } + + @Test + public void testIEF_Excelente() throws NotAvailableMetricException, ReportItemException { + // Creamos mocks para cada métrica + ReportItemI mC = mock(ReportItemI.class); + ReportItemI mW = mock(ReportItemI.class); + ReportItemI mT = mock(ReportItemI.class); + ReportItemI mP = mock(ReportItemI.class); + + when(mC.getName()).thenReturn("cycleTime"); + when(mC.getValue()).thenReturn(0.0); + when(mW.getName()).thenReturn("waitTime"); + when(mW.getValue()).thenReturn(0.0); + when(mT.getName()).thenReturn("throughput"); + when(mT.getValue()).thenReturn(50.0); + when(mP.getName()).thenReturn("WIP"); + when(mP.getValue()).thenReturn(0.0); + + List> metrics = List.of(mC, mW, mT, mP); + log.info("Métricas de entrada: " + metrics); + + ReportItemI result = strat.calcIndicator(metrics); + log.info("Resultado IEF: " + result); + + assertEquals("IEF", result.getName()); + assertEquals(1.0, result.getValue(), 1e-6); + assertNotNull(result.getIndicator()); + assertEquals(IndicatorState.EXCELENTE, result.getIndicator().getState()); + } + + @Test + public void testIEF_Bueno() throws NotAvailableMetricException, ReportItemException { + ReportItemI mC = mock(ReportItemI.class); + ReportItemI mW = mock(ReportItemI.class); + ReportItemI mT = mock(ReportItemI.class); + ReportItemI mP = mock(ReportItemI.class); + + when(mC.getName()).thenReturn("cycleTime"); + when(mC.getValue()).thenReturn(40.0); // 0.25 + when(mW.getName()).thenReturn("waitTime"); + when(mW.getValue()).thenReturn(20.0); // 0.25 + when(mT.getName()).thenReturn("throughput"); + when(mT.getValue()).thenReturn(35.0); // 0.7 + when(mP.getName()).thenReturn("WIP"); + when(mP.getValue()).thenReturn(5.0); // 0.25 + + List> metrics = List.of(mC, mW, mT, mP); + ReportItemI result = strat.calcIndicator(metrics); + + assertEquals("IEF", result.getName()); + assertEquals(0.8125, result.getValue(), 1e-4); // cae en "Excelente" + assertNotNull(result.getIndicator()); + assertEquals(IndicatorState.BUENO, result.getIndicator().getState()); // este valor da "Excelente" + } + + @Test + public void testIEF_Aceptable() throws NotAvailableMetricException, ReportItemException { + // Creamos mocks para cada métrica + ReportItemI mC = mock(ReportItemI.class); + ReportItemI mW = mock(ReportItemI.class); + ReportItemI mT = mock(ReportItemI.class); + ReportItemI mP = mock(ReportItemI.class); + + when(mC.getName()).thenReturn("cycleTime"); + when(mC.getValue()).thenReturn(80.0); + when(mW.getName()).thenReturn("waitTime"); + when(mW.getValue()).thenReturn(40.0); + when(mT.getName()).thenReturn("throughput"); + when(mT.getValue()).thenReturn(25.0); + when(mP.getName()).thenReturn("WIP"); + when(mP.getValue()).thenReturn(10.0); + + List> metrics = List.of(mC, mW, mT, mP); + log.info("Métricas de entrada: " + metrics); + + ReportItemI result = strat.calcIndicator(metrics); + log.info("Resultado IEF: " + result); + + assertEquals("IEF", result.getName()); + assertEquals(0.5, result.getValue(), 1e-6); + assertNotNull(result.getIndicator()); + assertEquals(IndicatorState.ACEPTABLE, result.getIndicator().getState()); + } + + @Test + public void testIEF_Bajo() throws NotAvailableMetricException, ReportItemException { + // Creamos mocks para cada métrica + ReportItemI mC = mock(ReportItemI.class); + ReportItemI mW = mock(ReportItemI.class); + ReportItemI mT = mock(ReportItemI.class); + ReportItemI mP = mock(ReportItemI.class); + + when(mC.getName()).thenReturn("cycleTime"); + when(mC.getValue()).thenReturn(200.0); + when(mW.getName()).thenReturn("waitTime"); + when(mW.getValue()).thenReturn(100.0); + when(mT.getName()).thenReturn("throughput"); + when(mT.getValue()).thenReturn(0.0); + when(mP.getName()).thenReturn("WIP"); + when(mP.getValue()).thenReturn(30.0); + + List> metrics = List.of(mC, mW, mT, mP); + log.info("Métricas de entrada: " + metrics); + + ReportItemI result = strat.calcIndicator(metrics); + log.info("Resultado IEF: " + result); + + assertEquals("IEF", result.getName()); + assertEquals(0.0, result.getValue(), 1e-6); + assertNotNull(result.getIndicator()); + assertEquals(IndicatorState.BAJO, result.getIndicator().getState()); + } + + @Test + public void testIEF_FaltanMetricas() { + // Solo una métrica + ReportItemI mC = new ReportItem.ReportItemBuilder<>("cycleTime", 10.0).build(); + List> metrics = List.of(mC); + + assertThrows(NotAvailableMetricException.class,() -> strat.calcIndicator(metrics)); + } +} + + diff --git a/src/test/resources/appConfTest.json b/src/test/resources/appConfTest.json index ae5c655c..ba3bd76c 100644 --- a/src/test/resources/appConfTest.json +++ b/src/test/resources/appConfTest.json @@ -11,6 +11,30 @@ "type": "java.lang.Integer", "description": "Número de comentarios", "unit": "comments" + }, + { + "name": "cycleTime", + "type": "java.lang.Double", + "description": "Tiempo medio que tarda una tarea en completarse", + "unit": "horas" + }, + { + "name": "waitTime", + "type": "java.lang.Double", + "description": "Tiempo medio que las tareas permanecen en espera o bloqueadas", + "unit": "horas" + }, + { + "name": "throughput", + "type": "java.lang.Double", + "description": "Número de tareas completadas en un período determinado", + "unit": "tareas/semana" + }, + { + "name": "WIP", + "type": "java.lang.Double", + "description": "Promedio de tareas en curso (trabajo en progreso)", + "unit": "tareas" } ], "indicators": [ @@ -35,6 +59,17 @@ "warning": 4, "critical": 6 } + }, + { + "name": "IEF", + "type": "java.lang.Double", + "description": "Ãndice de Eficiencia del Flujo", + "unit": "índice", + "limits": { + "ok": 0.8, + "warning": 0.6, + "critical": 0.4 + } } ] } \ No newline at end of file From 26bf196d86be88ea16e64b34e0a274599186717f Mon Sep 17 00:00:00 2001 From: liamorFS Date: Sat, 17 May 2025 13:33:09 +0200 Subject: [PATCH 094/100] Add IEFRemoteEnquirerTest.java --- .../us/muit/fs/a4i/test/model/remote/IEFRemoteEnquirerTest.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/test/java/us/muit/fs/a4i/test/model/remote/IEFRemoteEnquirerTest.java diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/IEFRemoteEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/IEFRemoteEnquirerTest.java new file mode 100644 index 00000000..e69de29b From b48564a5a5db3cdfdef1414c99de612be81f0971 Mon Sep 17 00:00:00 2001 From: liamorFS Date: Sat, 17 May 2025 13:34:55 +0200 Subject: [PATCH 095/100] Add IEFRemoteEnquirerTest.java --- .../model/remote/IEFRemoteEnquirerTest.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/IEFRemoteEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/IEFRemoteEnquirerTest.java index e69de29b..87c183eb 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/remote/IEFRemoteEnquirerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/IEFRemoteEnquirerTest.java @@ -0,0 +1,97 @@ +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.DisplayName; +import org.junit.jupiter.api.Test; + +import us.muit.fs.a4i.exceptions.MetricException; +import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; +import us.muit.fs.a4i.model.remote.IEFRemoteEnquirer; + +/** + * Test de integración para IEFRemoteEnquirer, + * consulta las métricas Kanban–Scrum directamente en GitHub. + */ +class IEFRemoteEnquirerTest { + + private static final Logger log = Logger.getLogger(IEFRemoteEnquirerTest.class.getName()); + // Instancia del enquirer; usará la configuración de GitHub (token, etc.) del entorno + private final IEFRemoteEnquirer ghEnquirer = new IEFRemoteEnquirer(); + + private static final String REPO = "MIT-FS/Audit4Improve-API"; + + @Test + @DisplayName("cycleTime: media horas desde backlog hasta cierre") + void testCycleTime() throws MetricException { + ReportItem metric = (ReportItem) ghEnquirer.getMetric("cycleTime", REPO); + assertEquals("cycleTime", metric.getName(), "El nombre debe ser cycleTime"); + double value = metric.getValue(); + log.info("cycleTime = " + value + "h"); + // Debe ser al menos 0 + assertTrue(value >= 0, "cycleTime debe ser >= 0"); + assertNotNull(metric.getDescription(), "Debe tener descripción"); + } + + @Test + @DisplayName("waitTime: media horas hasta entrar en progreso") + void testWaitTime() throws MetricException { + ReportItem metric = (ReportItem) ghEnquirer.getMetric("waitTime", REPO); + assertEquals("waitTime", metric.getName(), "El nombre debe ser waitTime"); + double value = metric.getValue(); + log.info("waitTime = " + value + "h"); + assertTrue(value >= 0, "waitTime debe ser >= 0"); + assertNotNull(metric.getDescription(), "Debe tener descripción"); + } + + @Test + @DisplayName("throughput: tareas cerradas última semana") + void testThroughput() throws MetricException { + ReportItem metric = (ReportItem) ghEnquirer.getMetric("throughput", REPO); + assertEquals("throughput", metric.getName(), "El nombre debe ser throughput"); + double value = metric.getValue(); + log.info("throughput = " + value + " tareas/semana"); + assertTrue(value >= 0, "throughput debe ser >= 0"); + assertNotNull(metric.getDescription(), "Debe tener descripción"); + } + + @Test + @DisplayName("WIP: promedio tareas en curso en cuatro etapas") + void testWIP() throws MetricException { + ReportItem metric = (ReportItem) ghEnquirer.getMetric("WIP", REPO); + assertEquals("WIP", metric.getName(), "El nombre debe ser WIP"); + double value = metric.getValue(); + log.info("WIP = " + value + " tareas"); + assertTrue(value >= 0, "WIP debe ser >= 0"); + assertNotNull(metric.getDescription(), "Debe tener descripción"); + } + + @Test + @DisplayName("getAvailableMetrics() debe listar las cuatro métricas") + void testGetAvailableMetrics() { + List list = ghEnquirer.getAvailableMetrics(); + log.info("Available metrics: " + list); + assertEquals(4, list.size(), "Deben ser cuatro métricas"); + assertTrue(list.containsAll(List.of("cycleTime","waitTime","throughput","WIP"))); + } + + @Test + @DisplayName("buildReport() debe devolver ReportI con 4 ítems") + void testBuildReport() { + ReportI report = ghEnquirer.buildReport(REPO); + assertNotNull(report, "El reporte no debe ser nulo"); + List> items = report.getItems(); + log.info("Informe generado con ítems: " + items); + assertEquals(4, items.size(), "Informe debe contener 4 métricas"); + // Verificamos rápidamente los nombres + assertTrue(items.stream().anyMatch(i -> "cycleTime".equals(i.getName()))); + assertTrue(items.stream().anyMatch(i -> "waitTime".equals(i.getName()))); + assertTrue(items.stream().anyMatch(i -> "throughput".equals(i.getName()))); + assertTrue(items.stream().anyMatch(i -> "WIP".equals(i.getName()))); + } +} From 6541f4b58bd51a32ff72db7d38d7ff96d7140d32 Mon Sep 17 00:00:00 2001 From: liamorFS Date: Sat, 17 May 2025 14:05:44 +0200 Subject: [PATCH 096/100] Add IEFStrategy.java --- src/main/java/us/muit/fs/a4i/control/strategies/IEFStrategy.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main/java/us/muit/fs/a4i/control/strategies/IEFStrategy.java diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/IEFStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/IEFStrategy.java new file mode 100644 index 00000000..e69de29b From cbd9a11f66c277bf240bdcc9599816bb150cb5a8 Mon Sep 17 00:00:00 2001 From: liamorFS Date: Sat, 17 May 2025 14:06:39 +0200 Subject: [PATCH 097/100] Add IEFStrategy.java --- .../a4i/control/strategies/IEFStrategy.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/IEFStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/IEFStrategy.java index e69de29b..21e6f8e2 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/IEFStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/IEFStrategy.java @@ -0,0 +1,84 @@ +package us.muit.fs.a4i.control.strategies; + +import us.muit.fs.a4i.control.IndicatorStrategy; +import us.muit.fs.a4i.exceptions.NotAvailableMetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.IndicatorI.IndicatorState; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Estrategia para calcular el Ãndice de Eficiencia del Flujo (IEF). + */ +public class IEFStrategy implements IndicatorStrategy { + private static final Logger log = Logger.getLogger(IEFStrategy.class.getName()); + + private static final List REQUIRED = List.of( + "cycleTime", "waitTime", "throughput", "WIP" + ); + + private final double w1 = 0.25, w2 = 0.25, w3 = 0.25, w4 = 0.25; + private final double maxC = 160.0, maxW = 80.0, maxT = 50.0, maxP = 20.0; + + @Override + public ReportItemI calcIndicator(List> metrics) + throws NotAvailableMetricException { + log.info("Calculando IEF con métricas: " + metrics); + Map m = metrics.stream() + .collect(Collectors.toMap(ReportItemI::getName, ReportItemI::getValue)); + + for (String req : REQUIRED) { + if (!m.containsKey(req)) { + throw new NotAvailableMetricException("Falta métrica " + req); + } + } + + double cycle = m.get("cycleTime"), + wait = m.get("waitTime"), + thr = m.get("throughput"), + wip = m.get("WIP"); + + double normC = clamp(cycle / maxC), + normW = clamp(wait / maxW), + normT = clamp(thr / maxT), + normP = clamp(wip / maxP); + + double score = w1 * (1 - normC) + + w2 * (1 - normW) + + w3 * normT + + w4 * (1 - normP); + + IndicatorState state = classify(score); + + try { + return new ReportItem.ReportItemBuilder("IEF", score) + .metrics(metrics) + .indicator(state) + .build(); + } catch (ReportItemException e) { + throw new NotAvailableMetricException("Error building IEF ReportItem: " + e.getMessage(), e); + } + } + + @Override + public List requiredMetrics() { + return REQUIRED; + } + + private double clamp(double v) { + return v < 0 ? 0 : (v > 1 ? 1 : v); + } + + private IndicatorState classify(double x) { + if (x >= 0.8) return IndicatorState.OK; + if (x >= 0.6) return IndicatorState.OK; + if (x >= 0.4) return IndicatorState.WARNING; + return IndicatorState.CRITICAL; + } +} From 435e8b648e3e721d19390620df5d04d0e6f94351 Mon Sep 17 00:00:00 2001 From: juan Date: Sat, 17 May 2025 23:55:44 +0200 Subject: [PATCH 098/100] Add IEFRemoteEnquirer --- .vscode/settings.json | 3 + .../a4i/model/remote/IEFRemoteEnquirer.java | 150 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 src/main/java/us/muit/fs/a4i/model/remote/IEFRemoteEnquirer.java diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..7b016a89 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/src/main/java/us/muit/fs/a4i/model/remote/IEFRemoteEnquirer.java b/src/main/java/us/muit/fs/a4i/model/remote/IEFRemoteEnquirer.java new file mode 100644 index 00000000..b0a79e8c --- /dev/null +++ b/src/main/java/us/muit/fs/a4i/model/remote/IEFRemoteEnquirer.java @@ -0,0 +1,150 @@ +package us.muit.fs.a4i.model.remote; + +import us.muit.fs.a4i.exceptions.MetricException; +import us.muit.fs.a4i.exceptions.ReportItemException; +import us.muit.fs.a4i.model.entities.Report; +import us.muit.fs.a4i.model.entities.ReportI; +import us.muit.fs.a4i.model.entities.ReportItem; +import us.muit.fs.a4i.model.entities.ReportItemI; +import org.kohsuke.github.*; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.List; + +/** + * RemoteEnquirer para extraer las métricas Kanban–Scrum de un repositorio GitHub. + * + * Métricas: + * 1) cycleTime – Tiempo medio (h) desde que una tarea está en "Sprint Backlog/Pendiente" + * (punto de partida) hasta su cierre. + * 2) waitTime – Tiempo medio (h) que una tarea permanece en estados previos a "En progreso". + * 3) throughput – Número de tareas cerradas en la última semana (tareas/semana). + * 4) WIP – Promedio de tareas abiertas en las cuatro etapas activas del tablero. + */ +public class IEFRemoteEnquirer implements RemoteEnquirer { + + private final GitHub github; + + private ReportItemI safeBuildReportItem(String metricName, Object value) { + try { + // Creamos el builder con el tipo correcto + ReportItem.ReportItemBuilder builder = new ReportItem.ReportItemBuilder<>(metricName, value); + return builder.build(); + } catch (ReportItemException e) { + throw new RuntimeException("Error creando ReportItem", e); + } + } + public IEFRemoteEnquirer(GitHub github) { + this.github = github; + } + + @Override + public ReportI buildReport(String entityId) { + // Construye un informe añadiendo cada métrica soportada + ReportI report = new Report(); + for (String metric : getAvailableMetrics()) { + try { + report.addMetric(getMetric(metric, entityId)); + } catch (MetricException e) { + System.err.println("Error al añadir la métrica '" + metric + "': " + e.getMessage()); + } + } + return report; + } + + @Override + public ReportItemI getMetric(String metricName, String entityId) throws MetricException { + try { + GHRepository repo = github.getRepository(entityId); + Instant now = Instant.now(); + Instant oneWeekAgo = now.minus(Duration.ofDays(7)); + + switch (metricName) { + case "throughput": + case "waitTime": + case "cycleTime": { + // Listado de todas las issues cerradas + PagedIterable closedIssues = repo.listIssues(GHIssueState.CLOSED); + int count = 0; + double totalWait = 0, totalCycle = 0; + + for (GHIssue issue : closedIssues.toList()) { + Instant closedAt = issue.getClosedAt().toInstant(); + // Sólo consideramos las cerradas durante la última semana para throughput + if (closedAt.isBefore(oneWeekAgo)) continue; + count++; + + // Fecha de creación: corresponde a entrada en Sprint Backlog/Pendiente + Instant createdAt = issue.getCreatedAt().toInstant(); + + // Calcular waitTime: desde creación hasta etiquetado "En progreso" + Instant enterProgress = createdAt; + for (GHIssueEvent ev : issue.listEvents().toList()) { + if ("labeled".equals(ev.getEvent()) + && "En progreso".equals(ev.getLabel().getName())) { + enterProgress = ev.getCreatedAt().toInstant(); + break; + } + } + totalWait += Duration.between(createdAt, enterProgress).toHours(); + + // Calcular cycleTime: ahora es desde creación (Sprint Backlog) hasta cierre + totalCycle += Duration.between(createdAt, closedAt).toHours(); + } +double value; + if ("throughput".equals(metricName)) { + // throughput = número de issues cerradas en la última semana + value = count; + } else if ("waitTime".equals(metricName)) { + // waitTime = tiempo medio en espera antes de entrar en progreso + value = count > 0 ? totalWait / count : 0; + } else { + // cycleTime = tiempo medio desde backlog (creación) hasta cierre + value = count > 0 ? totalCycle / count : 0; + } + return safeBuildReportItem(metricName, value); + + } + + case "WIP": { + // WIP = promedio de tareas abiertas en las cuatro etapas activas + PagedIterable openIssues = repo.listIssues(GHIssueState.OPEN); + int backlog=0, inProgress=0, reviewPend=0, inReview=0; + + for (GHIssue issue : openIssues.toList()) { + for (GHLabel lbl : issue.getLabels()) { + switch (lbl.getName()) { + case "Sprint Backlog/Pendiente": backlog++; break; + case "En progreso": inProgress++; break; + case "Pendiente de Revisión": reviewPend++; break; + case "En revisión": inReview++; break; + } + } + } + double wipValue = (backlog + inProgress + reviewPend + inReview) / 4.0; + return safeBuildReportItem("WIP", wipValue); + } + + default: + throw new MetricException("Métrica no soportada: " + metricName); + } + + } catch (IOException e) { + throw new MetricException("Error consultando GitHub: " + e.getMessage()); + + } + } + + @Override + public List getAvailableMetrics() { + // Definimos las métricas que este RemoteEnquirer puede consultar + return List.of("cycleTime", "waitTime", "throughput", "WIP"); + } + + @Override + public RemoteType getRemoteType() { + return RemoteType.GITHUB; + } +} From a833f30c525a2fc2985d5ebcc34703b212b38d4e Mon Sep 17 00:00:00 2001 From: juan Date: Sun, 18 May 2025 00:04:21 +0200 Subject: [PATCH 099/100] Modified TestRemoteEnquire --- .../model/remote/IEFRemoteEnquirerTest.java | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/test/java/us/muit/fs/a4i/test/model/remote/IEFRemoteEnquirerTest.java b/src/test/java/us/muit/fs/a4i/test/model/remote/IEFRemoteEnquirerTest.java index 87c183eb..54272aca 100644 --- a/src/test/java/us/muit/fs/a4i/test/model/remote/IEFRemoteEnquirerTest.java +++ b/src/test/java/us/muit/fs/a4i/test/model/remote/IEFRemoteEnquirerTest.java @@ -2,11 +2,15 @@ import static org.junit.jupiter.api.Assertions.*; +import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.kohsuke.github.GitHub; import us.muit.fs.a4i.exceptions.MetricException; import us.muit.fs.a4i.model.entities.ReportI; @@ -19,12 +23,20 @@ * consulta las métricas Kanban–Scrum directamente en GitHub. */ class IEFRemoteEnquirerTest { + private static final Logger log = Logger.getLogger(IEFRemoteEnquirerTest.class.getName()); - private static final Logger log = Logger.getLogger(IEFRemoteEnquirerTest.class.getName()); - // Instancia del enquirer; usará la configuración de GitHub (token, etc.) del entorno - private final IEFRemoteEnquirer ghEnquirer = new IEFRemoteEnquirer(); + private static final String REPO = "MIT-FS/Audit4Improve-API-G10"; - private static final String REPO = "MIT-FS/Audit4Improve-API"; + private GitHub github; + private IEFRemoteEnquirer ghEnquirer; + + @BeforeEach + void setUp() throws IOException { + // Asume que tienes la variable de entorno GITHUB_TOKEN con el token de acceso + String token = System.getenv("GITHUB_TOKEN"); + github = GitHub.connectUsingOAuth(token); + ghEnquirer = new IEFRemoteEnquirer(github); + } @Test @DisplayName("cycleTime: media horas desde backlog hasta cierre") @@ -85,7 +97,7 @@ void testGetAvailableMetrics() { void testBuildReport() { ReportI report = ghEnquirer.buildReport(REPO); assertNotNull(report, "El reporte no debe ser nulo"); - List> items = report.getItems(); + List items = new ArrayList<>(report.getAllMetrics()); log.info("Informe generado con ítems: " + items); assertEquals(4, items.size(), "Informe debe contener 4 métricas"); // Verificamos rápidamente los nombres From 4006b28949156a0c3cfe25b4e8c91d81ff188397 Mon Sep 17 00:00:00 2001 From: juan Date: Sun, 18 May 2025 00:20:42 +0200 Subject: [PATCH 100/100] Modified IEFStrategy and IEFStrategyTest --- .../fs/a4i/control/strategies/IEFStrategy.java | 10 ++++++---- .../test/control/strategies/IEFStrategyTest.java | 14 ++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/us/muit/fs/a4i/control/strategies/IEFStrategy.java b/src/main/java/us/muit/fs/a4i/control/strategies/IEFStrategy.java index 21e6f8e2..627c8127 100644 --- a/src/main/java/us/muit/fs/a4i/control/strategies/IEFStrategy.java +++ b/src/main/java/us/muit/fs/a4i/control/strategies/IEFStrategy.java @@ -12,6 +12,8 @@ import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Collectors; +import java.util.Collection; + /** * Estrategia para calcular el Ãndice de Eficiencia del Flujo (IEF). @@ -58,11 +60,11 @@ public ReportItemI calcIndicator(List> metrics) try { return new ReportItem.ReportItemBuilder("IEF", score) - .metrics(metrics) - .indicator(state) - .build(); + .metrics((Collection) metrics) + .indicator(state) + .build(); } catch (ReportItemException e) { - throw new NotAvailableMetricException("Error building IEF ReportItem: " + e.getMessage(), e); + throw new NotAvailableMetricException("Error building IEF ReportItem: " + e.getMessage()); } } diff --git a/src/test/java/us/muit/fs/a4i/test/control/strategies/IEFStrategyTest.java b/src/test/java/us/muit/fs/a4i/test/control/strategies/IEFStrategyTest.java index a707dea5..c7bc0b1f 100644 --- a/src/test/java/us/muit/fs/a4i/test/control/strategies/IEFStrategyTest.java +++ b/src/test/java/us/muit/fs/a4i/test/control/strategies/IEFStrategyTest.java @@ -25,7 +25,6 @@ public class IEFStrategyTest { void setUp() { strat = new IEFStrategy(); } - @Test public void testIEF_Excelente() throws NotAvailableMetricException, ReportItemException { // Creamos mocks para cada métrica @@ -52,7 +51,7 @@ public void testIEF_Excelente() throws NotAvailableMetricException, ReportItemEx assertEquals("IEF", result.getName()); assertEquals(1.0, result.getValue(), 1e-6); assertNotNull(result.getIndicator()); - assertEquals(IndicatorState.EXCELENTE, result.getIndicator().getState()); + assertEquals(IndicatorState.OK, result.getIndicator().getState()); } @Test @@ -77,7 +76,7 @@ public void testIEF_Bueno() throws NotAvailableMetricException, ReportItemExcept assertEquals("IEF", result.getName()); assertEquals(0.8125, result.getValue(), 1e-4); // cae en "Excelente" assertNotNull(result.getIndicator()); - assertEquals(IndicatorState.BUENO, result.getIndicator().getState()); // este valor da "Excelente" + assertEquals(IndicatorState.OK, result.getIndicator().getState()); // este valor da "Excelente" } @Test @@ -106,7 +105,7 @@ public void testIEF_Aceptable() throws NotAvailableMetricException, ReportItemEx assertEquals("IEF", result.getName()); assertEquals(0.5, result.getValue(), 1e-6); assertNotNull(result.getIndicator()); - assertEquals(IndicatorState.ACEPTABLE, result.getIndicator().getState()); + assertEquals(IndicatorState.WARNING, result.getIndicator().getState()); } @Test @@ -135,16 +134,15 @@ public void testIEF_Bajo() throws NotAvailableMetricException, ReportItemExcepti assertEquals("IEF", result.getName()); assertEquals(0.0, result.getValue(), 1e-6); assertNotNull(result.getIndicator()); - assertEquals(IndicatorState.BAJO, result.getIndicator().getState()); + assertEquals(IndicatorState.CRITICAL, result.getIndicator().getState()); } @Test - public void testIEF_FaltanMetricas() { - // Solo una métrica + public void testIEF_FaltanMetricas() throws ReportItemException { ReportItemI mC = new ReportItem.ReportItemBuilder<>("cycleTime", 10.0).build(); List> metrics = List.of(mC); - assertThrows(NotAvailableMetricException.class,() -> strat.calcIndicator(metrics)); + assertThrows(NotAvailableMetricException.class, () -> strat.calcIndicator(metrics)); } }

~$zxtXtVCJIPEfc!aSCdVv+J4%~-=%JkaW+V-}% z&8=>C%iG>cvXPG@OALbRTiz}s5|*rMWQ9wQPRny5Hq`N4410)$-Cq zP171UfZ4nYe&VICuHuqK8On$X8i;F_Otj094$iQKH_YJ7^}DQ zW+t4z!>LP3S@!`p{$1hYTqH9F|BTI+4&|1Ts3qtR6q>&8p4x zf3aEVPILOxn#S~|Lw)H|SJTv%KDDY-&1z4RdesQ#q^#$`3o7^q4Vo$@_h^|u-25gm z>;27Mg@YO+M@lum4qsz>v&+h;*wv$swy39#YG_k?)z+>ysHLrKO<$77$|dld2~6%T zWA{&vTCfaH{J=d|P)l2Vw7lm{?|R$&-nDyOG)j!`)`_(p(4H}wS9xQChuPo*f2P3A ztnh+cT3W@6v$;1J>oZSg+!bdyi#5J*gmb*$q->99D}B6j8@SExvCd9Kx9;iO0LJ)epH=bFf*U?N&TfANPx;APzVC{uI^&_v_yC9f^9Vk- zX>}Yde7DPh~R&V0GyZ-gCkG<@_s&h!XefD5clQ1%GagcA^A*=S)&dZmM`t9+?qtYZxDkD z5(65zkH7rqPyhP+`3CVOi|p(VmVfiJ{o1XzZ2*{U*d~D8#%%@oY5}N#1h{|*=zs$V zEoFC2&lhgI2U;fgZL^YrOm~10$bbfzfDhP!Bp8Aam~yN{eOPx&02F$>Q+8_SEmw6x zF@Oe8k$?P`gF3i_JScm^wR1hV6+gFTv1WZnn0>ydeM*>wOn7h_IDOk8aofgY6KG!; z2VjTCf!DW$!-s@j*m0G`YSEHlNQjXTh>O^Ve>jMZn23qEh=Rz7dkBe-h=`h~ zh?eMxo*0RnNQt6&iI13xk|2qfXo;2hiKd8(oVbdZ2#BW`imKR%u?UK@_=>hzin8d6 zqX>(-#**pAZpj`J9g^tg`osE+c8kNh}`+W3v)2#@NR zjI!vD(Fl#dKtHpMfe$EcavI}k}N5cFgcSk>4YdqaXU$I zDv6Uyi5)ShFkFIqlc0L+H!e03OI1eTM>Z7btNgSC@DR zmw@S*d#RU<`IwUVmI79Wqi2@mmzb~?fN^CXYF95?LJSauK~SL#Ub&U3xtgrmnr5;L z7-oAQd0bK`K*Lg*kU5!=d6$&Qo4eVVxEY+jshf{CD7GnUSlE;Q#L_$828C8pFS&(4$)7_hlmJ?0_!)**M=O@;W@3nT&gp#6w`0jCg+KY9K{=uR`JAA~ zpo`aewb_?Z=ycb)ENX{9@X0{$S)wL-qA1#E>d6(3=8?+42>Q8~n^Ad~SEGS z3B&+%Us|fBda5BglK*4Tq8+Iw8|Z-Lwy2LPtB^XYjasX?N~^kht2?3AI4 zq-y%6PzqY2edC#woEdUyIY4?5n)iYg4TJ_Tx1y*zuH;&- z@@IEtaRV4xpOS`hL|RI_>a6kltnv!2^Lnpos;$1Ne2_;O2MU?JTCDiVd9WsRHEFL1 zTdxXBuL7%PF8G(*`KytKoP-H_*~%BC7Zv8Zu^ij6J4g?)DO}>ZgRTm(5qF#Y8KD7M zp%kjJcItRI7Ni7Pu`Z~1poF2>9u2Poo0)z1v_ym_=I;UdWUMU z$+u0LS#qhChSe#Y*(tVUySKS1D4WTae|niedawpcsl4}QYDY3vySR+2CG>E6b~v() z+d4l8Nmdz$Ll~Gd%CCi+w`GgBVEeguJG!3Bx7@j}i#e$TYn(i5h8;Mpm};p!s+ea> zx}j^jwyU=eyObB}ng3danp(3)sTbwBc9VO&$U7EOs}=R24EuIsQv1BEsc&}(l46C6 zwy24fn2f?Wk+(>ToG6Qsn2NcWy|Ac`*;~HXn7-rNi{v|s;%kc7yT0!$zu*hM!kE6t z7{2+dj_CXAy}XEu=gYm?$iA6K!0{`-jA*}yc!-PWhr76l4qS*5?1&Fsi4P2j7o5Sr z*uLX?zMrVQq8N-KT)zsezWj^6K zi$46nD-6Ui+`c)Skm8iQO1#8e;k^6sxKwLr^2uTJ5XG#y3}GO-Si!1uxC}7LC%ns( zgSVt)JjP~Bqx-6}NSUwJD!4i+#!C6BEalOM?#Z#dS`XIehD`?B>n&|&Z7GP<;$n~d&DVTnV#(8|kN2;Wx zOsr>#n8eDa;PMMPm=G$r!b7{OtQmN0%A#y^5^Jne>Y%$jshMG!$qCDM5t>bV z$jV%*Rs6X1Ahi&-6%?k(b{54|+{hsIxa+B^Vo|vsxeQ}fv`ERFEL+Y&d(J-_l&aL6 zY@Cy?9LVjwyR z$@D-E(reY#JQbB}Rg+v5SRED-y$lh(p5DBwmkWm_TcC#fupXMbqFcJP3)i5lyP#{g zbDXIZi=hu{(`y^I!b-REytQ`O(x>m?#m8yM!-txH|m8|~Cy z9mS|w6;OTAPVCwHHqn!`*`S>j(BOuhoygFz3{!lSb%?beiClb#w0!!`w;R{DJ=brY z+iuObja{h5nV>O!h6p;Ssw~fmJhmUt&|*ceB4|M;Abb?haur=n{op^9=oUDKHC}W zP?mujt)BMJW4G6cnirOI(^Xd($ULUsJ>%9Z*0%H6qP-8w@YSL%we?*)UG2!!{760? z4R#2{`{3hN@zt#PI+zU>>I#w}Ev+@3fzG#1)g7d`X0U!ja=b7;L52yoBmk+=<=C_c zMfSF~!389)8dXqBB$pTR@CQXkYvU6D2T&Vvt{BmCZ0EraTTox1#tv-34q$1r$-)jE zZRIN|3|qi{J}?X%noNcs1L2ud3pqf9{z@3Tplpl4J+N%(^);n|(igSBWbd&H0+4HQ z(&xIN;l#5G0T2L4W+!>E2r)2Oe8CQuo(Y(y=)?pW@~{PZj!K2W3t_N&k$!D&#tZCM zOSJ&%)Yq_ju?V|i4l=;%F=}MWrWb_}?Ce3vG(PSi`Qx@jwOf4_Pt9oY%{uT$-#MNY z`XJvJecn}J?>c@Jk`OClmKSRxK)K=>wQy{_umWgs>Ic5? zd?637Pyr~u7uGJu>#+rg)ef>wOuMiGf=+JGb8LZa=x`ou>>vYpwdsD*^7lEj_3`P+ z5$(n9qr8yqZ;U^VCGGWG@msL+d5z1vQ0K$E>kywlTOdoStYdTj@n{$AJbxFpa06A( zOENI?7j78K0UTD31D`r3-Inm)jc1mGxvHBDTnt8iq+>nv1 z){mDF(cIjH!(t3H2>;uKNwbq+J9YukPzm5b*p>hXGPM&{C4k+og9Qltb7Mt-OyvY1 z(>#hZcw063T^FEQT$Z$ukLh}mH3fdz3adP8-Q7>z=oo=vGSEl4-* zI%W>@RHQJ7Y;Vrvr86N+c`Qv18|bjORINA1W`>9(QrM~@(+=AmBQEiXI5Ihn@#A~* z>H}v7S1Qkz&erm(#KkO3(y&a#GVKE}Kmi9Nus{P3L@+@G7i6$O2OoqmLJ23NutEzj z#4tk*H{`HG4-=#(8VBi7hQwv)3DH1hmO+rj09PE49u@&qMwtQivC%_)L^O~@9_z_* zAAM#FP{sgpoc~dudZ5{-o_%U8kwp}Fd@;xmuf#IT2V<~uOJcMHGt3W_k)$<&=8@#D zwvdaf9Z5KIGpID@879qk;`C{fn|O&$pfnJYM9;a-BB)Nt+I&->Nf2Tu4LK(gW|E0+ zvXju(+98B9f_8aKuEMy|jI(t%%WR9dc44fYLJ&a6A&zh$sE~u$S;T?P;pYmC>*}C)lM$-TGZj**D$`^=$|YXJaBHY~q2snPvND@#UY{1h z&!D5kApZ#BkJ{`BDNwoasH000Vn>B&Ll)q?X*F{rvnpcS#nyTFf-aeISVeWCRfAnY zq@8GGwX=!-@T;K5>^pW}xy^}y(UKvyVdUZXqcPAu&k(W};ws;L zTU)JOd5uEu+d>xR=?Acb-C|ay+VLtGq&P!q3(jb@7vjvqayVXj0B$F_n-(crHJ208 zufP7pggkP|C#Sq}%P+?~bIE0-Qa~Gtq*0k@oD{vwBY`|{NFMbu(sOzcgt0>)om@~E zCQYo7$P%ecF?1-K6mUq;_i0^oXb|l5N(c`oiCXM5I z;s1;m{C0A4PdEv}9nV0$@36~5+SAANzW3Yp;4ar}@21v;QLgm^$f`iGfdB&#l&sFrsg(<{u3 zO<4?K1k<`BkKZuNT$wN&`Q+k|%#`aRB2iUudP0!-c*<`^yv+U%GQ{q5#a;Ori*H2I z#I;eyDoo4XZ<2$MTKFJd3Ng6d$D1fvxnTc#TSje8xP!&wLt7#j7mrHNb4b!4C_x)?4w6Gpc}tFk;7lp^0z`u`Q7N^^f&UV+j>qV77>uAEDE{L- zViwbw$3$i_m3hqG6{ti<5)p|?B)TGj&O`>nQRf6iBHHZ(8qE~aGPJqPac0Oz43ZHw zi8mwwv9q1115oh<#3L6SsYc~oWrZ%6=y9so4oekM5(vTGppp?Ow-oB5cA^CURH{i5JhpIIZ-{BBj#Y(EE`lsB#a2OjMwbirYX5(%I-wzt z2C^-!IOySlcpfe1rCVL)42DwYPxDx@QIbI;cp64lJ4Y94>k->-l=TDy60 z9)-}=K+J&vWc-0`RB@oESojiAYT+{T7}Z}utH-S_=Z}mq4KS-ho21+zYi_06w|aQi zk`kmAPuWjvU@6;^^pO#N^y?#Og`akeB`t0NY_JYH5U)6~q=4Z?O$a8ob;w|8BB=;= z7Z%oubWw*V+)3m76VQS7*T4S-aDWAjrPwJ*M(2D|>uN+Ga1Lm}1pgWmlL7=!1>vZ` z9fC$Z=s7yi<>;Cn1`v>Rr@IC}ct|!%C-Lw(-~lJ^brL!(ivvs#wO(pJA&rwR0rSz; zNJ@Jn+3PSGx~B3W=X`jg%b(EHzRX-Sp;SpjJBA@BkRf!h^B~0dIF%ZO@FEhu1SA7@ zxHyIIuV%>NvU3q&Jr*KuSr}aN1Pk#1KNiqd88jL%7BSSL3acOoK#8?Z#`fxjgWI75LD4o2!a5x)Oj~9OkNA)a^1gew2z=6u@YeitN*r#k}&Z}FW zo^L~RO=WJ|#9a`qVs1-xW+c+^`3^t3Kjc@yDu#xE8wImG3EL&Xox*|qL05ln<8jF5 zvd9x;X2`hXzQRvbF_21S$J(vs0qJ+GqCSQ_lmxqEfQcK>%YRitNZk}t`br)R;jYAt z><9`PiVGeRK6zoAu_y<2JEg)>qO>`aXLd zBf%0h!4pKm9$5wn8N?PC+Q{; zd%G8m-ve)_$i$6pt6MO1+Jizt(%9l zC>%2ilr6ldDceFgnG=qBlroeaJK?dN$TFA;fRsUw6`-sD05$~D40h=|!Q+fyv5sf) z!@+?U2a2FR`U5jkhzr6qK&p#VS%C*2FT!YtU|2+E2^K+I0Zb5xEgO|9bDS(&&tv&bpV2nDumi9Arn;if|a$@D+hE z4~3zMWaOb`q($+XmDvD}u+Tr;LYuwl$hc@Pv7ktkLB{$*D}vx9?emE=={QZ8!kVZIRZVDb{pdEFZFnKZ%Cs{%cLBeIYL6_4eY1$?t2|5Uykr{lECFv$< zTEUzYuxMC12>~ds#2lxSht1oSNI`?c!-OT2UxPk8Ti9)!kDdMRBSg#aPvtaQoq`^JQ!US#{y;6j(abY#`NTrlexW`+B z8%TrZu!#w}E|AHH=fDh-FbR1ih`j)dzo?9@p^L~;j_a6*NH7UW_zhaQ0fkF6%U~Rt zP&h6C7042XNWd7dAVbM&3V!>DN%)nZ2qc2&zL??#$Yd13_=Ltw2y^I^$Or%g&>30* zDnhxp!Prb~Q;3}5h!HxI+5EeD8HxJi8{(yBDO(2%tQq@L>*LK`h-0V zR7U;NLLJmY-KsQHB3#7R2@^*S{*b|J=I0cROE$#YV*8f7f2?@)1C9pF8 zF&eY{(_p>TQRP)Z1y(?X)b{(-e%)6>^*c(<*HQf#vbmpyEmVRn*oKAJT?JK(TM963sA(#k1?!UPi$Z9TDe%2q2;Q>q29ISIPu5PgsYb}-tJEn1X4 z*-5okLi^c&{Z+xOSVdi2V%-@ofXRYISXaeZpiS9~4cJ)yTgClZOqErAJyyg2{0R1J zRZ11vgw5Pb&DqP{Sk5)rP(@VF{aA`sS=HrO%#B^x)!f5nPT}-aTqRmhU09@{A?Z1j z#_70W(%a=_-sT076*~|cu@ai=93;Hj8{t+eCCYOJkZ3RweQ1*HjU2KS5+iwA0bxo4 zQ75DHNjAMF1asbjnpd!F%J^lFWvD1)O-0bfUCZSy!bMz81>nS0RnXm2!fn3+M%`Rp zV8eA>jD^+4mDtps-Oznq#+6;4m0*RP;0PAr2hQF8W!;Ru*}`2}jeTGQ?opSu z)nFFJUH%PVIi_3%Hdc>KR5kYB{srVuJ>U~Q%+{^r4|d?Am0SlF+?loD*3DoTUfqx# z-iWnX+a2V?#bkkP<2eT9#ceELB8I0W<5NcEDFov0wcY|*k{d~1cAb$bt|s-RrUe#PYh1pE*<6foX z)Ft3;23Sef;L2TKHs0AwwqQ3_WXnZla%N#izTuu7Tu}z$X!ci1uG~UyVbq;qPNrv_ zRp)co+1;ht5awfRo?+FEW~SYb9d71^cIW`JFcY!X0y!}#ArfDHku^=7a>eB%X_24w zS_?_J5$TfL8L{ALuqW{_tgYpCg5HO2p5$2`<%MYm!`FKDSI^yDI40yjhG#Pcp5eKurE{$Snx=}X>eO0M0C?ci}v=RICyt?uNB zrCdSvW3WD9;9X~N&f`3;lT2Xfn*XNjx;Cb9rIDIruxgDR@ij4UQj#2rIVw$Ql>42R ze%>NkF`P?E9;^|!-QI4!UUs!>dF>~9h1$w4sC0Yem%Uw0hG#?$)_sm=em>{MMdxc? zWD&Mwr5@^@c42TXRvQ*-pFZsk&T7$y?Qw?cf3EFmp6yKyYJs-rem-l0o$AkaYJ5iM za&Bj;2IV~+NRCSenyl>W#%|*g9*P#aRu0N(4Kcs&+O;j7T=tOS0YU?@L2P<#(FtSX z0UeAc9%!Hv#LjN#Ngne)kT9@r?4~ilt=#~>Z5M`J(4OSd=HuP=**<;ld?sfQ&ePUT zXGgYS+5X>rrfu0RXvXF3Q2$k93NP@SE^EMLvtxk#^JaTyzd?7@jCS<>WvYW-qyapWh7=-`-Yq-h9~$X9=s+HAcq}}J`wLV;vV0@ z%ckG^#$V$N<*PngIX2whHtVY%YY?C243F`^ecjV#?&f}Q1&?9wbyq6aHIUn0E9)LHl=p?9V?CYk@P_4|e% z4cBvBCv?^(dZ8!wU{~vw-{%$Qb+BG`23GC3uki$j`#djUaev`CPwS<|U|;>@FF$!> zr|n-Kb3gvopoeh&MfBrFcd>5!L6&sHFGK3-II|c1R2Fy@^PB|JdYAL3xlQk|^;Rjd zcV31N+Ra*PK9c<=~Aj$pI+sn06{R(Ehx$9uai$hO-YCR|L3zIY~6R}K2ck}M;`#135!iN(tUR)WpkOP+~B*>Cr^OqbKo>cZxp-JN| z=_!}kaM|>P)U6L}NKoIh@!&sdZ}0v+{NT%OsH_>Ie*FCUm)ZZ8o%nxYvAs4LWse02 zV1Wl3IGKW^DOlNpiWR6CT%7qAnSc`VMOuPwA?V?S9U^v_YK(0dnO__t*xFnghWH_7 zcCqLgUa6r5p=PQrb{LHj8WtmqB!2cSZBWK=SA*j*(u9qlY$1I-`)7 zhKN~RL8j^-qq3=Z;fX$udRn5QqMFvHr=f_UVUo^PB4Pi+M1tEVngIGNw9!gCt)16- zsphriEfY~aZLUNev<@M+C!OWhM4^UoNujVj%4l$}BGTxpuORNIsg8fe=$WcQGCUz=1TKsrs~>a9@vB3E$ZLki z9;UI%jRjjN%S1|C>Z&DTeB*~z4I_z?nk zCreeA2hu|5HEocWTPOV{L+)loG}&d##1l?9_-XdDk#JVwgs?VBDaWqe40o;rld9<4 zAsbsLvLM$yxUD&2`met(0{*wnD=RFs;HDaU^TAdCzgRiKxLQoNz_DeV@W=n2>bT0F z0_JhvqIXr>Fg`oWHtey>Ubfd39i7rL?Mhu=?i6)YdqqAW-CLM7u@tA3*W!n+(&i4Y z5_2H2i!|`FTMu+S^#9Ro@AXqE(;vj1<2<>g1$X{p#Jhqj`RDs4e6fJ4`>5){fXCe6 z!-SqpzQ~3P8#4a#W-|1-C~mqkp!oO(sfszyRV%Wbz!u2BOz}-*bvcrfK=HHgaj=6P z)K7ReBDQV`#3Sp%NY}iTrEU!`KVzH8)Fz^kYjMO)UecPJ#`cgB?&&<_`5+Lb#giV= zBn)c{Vtxdrlp?{cUkfDA=uRgy`<<_T5M1B>Q0FpWO|XirSJI}lwueYbR~^UEIbKxw&k>#d~%Vdd=fphXOnrIvOWA6#;-P)za9aQ zUw$)@7t8p#i7{|fjWeAto5(nR;qo&V`ywD?Hpf}!v5K;6A}`AaIrfe0e6flU{VL}% zgpo0qDchpP!WqoM$x)0>VV&z*sm^t>(}(9`j&r_dyRk8iI?ZDj5s6bhF+Jpk5`o4+ zj0P^Sp@)cMQW`Y6#mV4}tDO!tjxzLglRiCEI7nHd+=xUfWacb#CF^41ItDps%5Qwc zk{sq*mQD4sQI`JmBN!`n$2#sZrlJAe0BagYoJO;N2=r*fj^)6)b(D%ND;B__SWKh- zN76wPF7qJq%_PN7{VA45l^5=K< zNs&oXwXQi~rG2<(SEGCnmYlOtMjbUs(D^cs9D?5&Z`wXCzVVM1JgNC=wJ|1A@O|$4 zmFgszq5}r(M29P?V+RXZltFfzsJh>zPzoECjZT2E+*C3f*V(NUGB8Vggz(l})+sK0G?_z$)X*l@K?u6G)?D3Ew>>-|>q~C%y6OgMP$AXnqEZUS zVZltigS!qZkMK)L+qOMNnZ7qhgHH=noR8= zDhZ*7v5Vmu=VTdSKru|}MdO-8gAvK-ucGoBxL9fqyi4V*9$h0@*Gki*<~_3IcqwI^ z67Z;njc-iV*xoxPkjUiarbhKPD>-TwkC&aCY2l|L^s+XOh`VNB6X#S-BxAcfzVn4q zLRUF~C#<;J2XmrxPp}cht10mgT|GiImPF5YFAl3frDjjDGA*G987`hPos%K|PIQBs zK5J5Hh1xPV6Q%McFgdNct7e|ej)4=cM_(Fb2$O8Gq(iVxjfrJFG8n*}TeAX1YQ_B# zF1&Q>reo`yGeHXVaeZvEgOciHBOj=ct@HG!vpv;r6;IL$rBjJTeCV$sM5`yX6MD|W z5E(ZOO;wxZb3&qAZPQyP*~9Tm@LKPQK*PtZRwkE$UD+v{E`9h7B+djLdmU9vgV^5#iSiuFm%ivw_*o|GzYbJczL#A99 zBN*no&-&sE_Pfac?Uh3M%<58qJnR@9JaF~1T!wazc~F8MhqU$Q?~In2$n)*qN_r(S zhCJ$LqHopTR_gu!ac0vFVW!ry@K1*3V^0qAucvzLz87}JVcO9mFYM*gc47r7cC6fs zy_@JYj8Z1kTBX+N7Qm{TF5}lknaDlI_Lb;q6pq^&a6M z+t{hrcSV>_5!FkXTuPDJtQ=F~n3}G5AbB+mdm)@swUqZcn;$Vs+%cQWDVqf%T!&O& zft^Ne;f4VAAiltucodrc)yEH6#O1xv(At${MG$dMg%-g%r&@sY4? zT^j~p2x1=viPL+Plk6Q|8*<#LkzKPn3i4T-&tRb>zDK&%1k$-leE{Ikl~B0(kVp(2 zpoByb`X779oAOW(bZy~5_0yhBq7U*!D_(>VrQSuUT+9)8lYR}G+u`7U7 z{~Nuyqqg;xf^DBKe%(-|k4i~kAyVYXDI;T1mLnF>Ej}ZaDc)lB;TxfvMUoGH+1_kv zBsd}B2VS2WI+;UCQ*2dV8K$Jf^&V`2p)epxZrGVY{v=TpT_v6sp_Pl)xR{~^q0`74 zn-I|wx<}P?%euUnR=FRx6hst4hvoq#S6W0wQJqU1AnIYA1wN5`$z81(N@xUL;0+t! z@g4Hy zie$EAO*NcjzLsElBp+sE-{@2~G9wI9o0!R^Ul!&_1sviT=C5gFYOqd)5vYzLM4U%EkDc}t{<1F3ecaGsvG2p~~q>NstNv2koMHWgvCxxEb=TIm1 z1uETH#ZD%Pl}c$>9_6)MVsH&0aJdUwIZ`YJ$8=3Z{`F2;dC4eV5>~2&CiN%gDXNb0 z1bx=T4OnK)CEzx}X$~@#oAQq~=446Tm)9BD;9VeEM(40Z;6tVK?Xf zFRE#D&LQ-XV0n4mXc5+*3hMCP;h+Mi%}@n;))|75sw-{@5q?{J7#cjn#JB(t(kUcy zROOgVWpvHs5H{%)1|)S9skgc!mD-28ep+re;2NeRo0`n?f#~!xXsBaeBuo;jE)Ak| z+MSF{wcy7fe`5rIscr_Ftb7;RvydaM2LZ`Cp$YsmqqCME~)LPez@Bj%nI8 z7-9CIue2qYA{jBd*Rb{}bNVXprD)!@5n^#;uKksIl^nog=bs8E$l7YhK3U&Y=z>zD zBi5vY;_6CStj5e-wDQ}rpU?cLc1Olsl`S0Gr(PA(j&8;u| z?k8(jO{4zb*1#;C*dGu=OXaDDXErGj=HL7&B?xP7zS6|!x}Lv5V+QBo{Qi`Q9`Nz) zS8xj6c;zKC-Y;^l<+TSVE{+*&Qfis+m|ClG5+hr+5^PV5<0s0m6j*Z=Wq6Q3n6F)^JA8C{O8b!I8X zW?adNSIb?kf$1(p>aE(Xj%|$J`x0_)wJYY=v=|>L#sOQM3~5?9DRPW+(x7=ms(tT;?SSqOt}?ZHT62 z|1vKS7i02Oq#5I0n@zF{@+lySAxwE~KHIb0!P)Fa#SSKFG~*U(GK8gROZplb5O(Sh zsjqfyWlXFvd8{xU3tG`?CFX4` zr7ZnHRlBmRyc+xnMEg6;oq%!`_r8Qa?OI?w_`wqjRxonq0mN$qYL^*9G> zBqLc)omT+oEk1|xI0CgB@rC$B)m>i?MVsqG&~YGlM{7FLKBVSAX|H=s$GTJr()4F9 zLkDlIAEW6G5tY((9A)L+=V~Wy`=TmVe_H(343`$Q@;UOqYAtpNaeH4sdvn?q9mpA#!Z~I`8tXU5bw~bXvESnd};2abJEUQupvl*0)T{_=@s4Nfz`{ z;HYY2INM|%k3JGd8x%Z6_<5KwC#_#=7YEV~Ekz^;NetmO3ZK=^GWm|e z;!mh*%_(wwf7Eu@n)9Y|$E{!(PB-<<)W>TeT+XkZR`v?Z`0PW59@*pjO`*+h?37DQ)e714>6*O2d67hy>A#*r?#cK|I$ z1felMvoFhpF?+h-YPfB#`i^z@uG0B$f@kxFqn=t{^Hr;S=ixV(9ccg2qfbt#tP} zxZ`ecI-mpVH?vMV7t^hIHlmiPH#|?Z9FmrVd3ZP;dxHvf1#cV@4=`v~=Y{L?(n<0XzEGij&k>WR_jAvbKzdo zTaUqB#J6uyQXGx84S#rLrE5xel^ zT7vOc$vZkvC$CEq?A&)?v0Vkr@*viupSnoWN}z<77Px#>$>N5{X$~%OuJ8sesWD?tdPT+_C-5Dw3|1O;6Q-{1s*Jj(BQ&? z2^%t;r;s7VhYKM_lnBwH!-@?%O4R6(Ol{=R#J!N5)E&HmhUbL|E^a944mu}&;`jqkY8qMxry<_X$g)Dg=G{s*V z8`iv;b7#+=L5CJSnsn*5%3{=u8>Xvi*RKuhlf+IZRG&yg9+f&&=ud@9f42N>v$s^J zQENL5$~))o;xv^KkC>bH&Zo(Nr=%Vnr_-W9qxj>_$tvX4IG%v%Yi?2=M%IVHy|^GPbr?D9DF*o)66E3+Ik zz3a%F)6VkZTu;j|x!bS5Km7wwBskHGuTMGu%r2zchRJHHAcu@p(n(3P%Pzj4*(Wc( zH1rWM%q+}H#v4_0?5h(0x-7=JS_HLJP?Zsk(phP()z({KQ|5+O-MS$YT!F1?Hbk8) zb4f$_)Y8h5jAIJSFYP;XPwd2tu31CTBQrQLnT!)aE{~NDN}h}@*Uu}b<+e?9(@e5l zoUr__wmXNr*2}5tJ(OCT2z3)(<^m;G-*CUgs=#4>4OZcWYt<*RQN02&GQDDL|25LY zFwT&|u%H2KV`U)54CJq%kuYS2QBGOql_do-t{8}6`BrEcNk+=~*5%W^Ws}`A+bzYd zmOf|;eOFC;tDMqYqrDw8N}{>cPvGvf^>5Dn?!8m#+_nT0KdI>yI_Q_y{q{|8)3goE zoTc0sxT~AQQa}O?l-cgPU)z{O3pHj@NW>_#>#-6a8&PDwmeB{*eKfTA)MYN^3sSr( zuiWy>9gIOPUojuDo=DyvS8ZtHwK`q2r&RCTsN2qSUOMxA8a_5*XIEMNxRw3h)Tzxk z_Mh`)Tg;tf@>)-vpG(_W>9`J;$<+r|lWOM?Wm{=u-Fw?T~M>5~~ODlf}7t!OA58?oHxz!0)4 zdnSa^@VbSy3OcWJ&D%+%=yo6h4)Jmva$KYqC$d(3idB1QA6R77xU%qyh*h-W6+MKS zC%y#-Lc}6$WJ4PUnl5E{dOd1A>*Bxn%a9j%H7kC6&tqE?eYCPm)8;#ZugJ~sUU-YEHI)=X-3GPy0 zQAVb|A|m|xZ6AOcRmK=+xKGZKmbLs0J^yepIkmXVmZK@sk#x926-KF&*TSLi*qFT( zu5NjSB%9B|)67YpO@<{rWC~ALEY=}2Xw4g4Bh}Uv=rt2r2Q1^z?g%wX_J^5|gyc4{ zDYg;rWNyUxL;!o4Pa(-_QYxxUWHPi2UX@6clmSuv0P2+^>XV@jb*QeW$QI5yv@`S= z78$3;L1S7Ho!fyPCaoFF@Kh6y<-}7w+lfbi73w?6@gO|wgE|a?ba#m~=hUD%yJwzJ zQ6>GI({@)U89Gu>kqqeu-C03KzKbG2Qi`TLk*6;6xO%MbaOR8ZSHW6K zUcLopVJ!^JY{oM+F7<*vW7@H#dH)}A0WYK3Jn02{nl*XdN3A|>-AI|nLTQOGns_bR zldPxFy;4t4wL9naa(Kcz5_XTbBPunu>DC+Cj;Rwdn1R06OJ!tYw4fLUC`g+L)1tPt zr%ml@U+db|vR1XStu1X~JKNae*0#C5?QVgqTjBn8xV|-RZ;{Je<)&5{_XI^UnoEr5 zY9hMRZLTsjk&NkTHyPPoZFjYMTJVCxp4io{JnfSi>Bi zuX;ZmVi1p5#3d&2iA|hh6#uVS#Vuy>i(MRJ7|&S7H6}4C%_v_{@P@AM8<#|n@F^i;WVWV1 zjb`9G_Q$pXjmsZ(8=brv)uq~Y5#fJ&!+aZt)1;? zV|&}$u6DP<{cUk`d;i?tCil6`o$hd>d)?|Tce~;JZfWBa$e&q87<8GIvep|e1U!;5 z-8E;K%CV2{1Z*HAV_jf(3_iNds2A(*bX?0!N#~m4~l#3ijY~ zmDf`9NY0O9Hl|@ymzpbn&SBi(1~CwW7z%`ATD}1j1r9KE@f-92;}_^dH+s>9E_9?D zo#;$Iy3>^o^`A?<=}CXO)uWE}scXIJQm3xZF~)VLV~px$7dw8zPTsKZSGrU8xzH0s zjHeGBx^9OqfZ=X-y`$aeZ{K@>mC<&-4?ghNEj)GQj(Ec-{_u@w{NooNdBsoO@sh7R zNx-C$2i*2;0LcGl3iEHo}G)RG-Xcinp|>=oB3<;1nR(AsxN}e(T5+O@-VZc zx{*HLg>0qi!Ur;pFib)WXb?lqCDBVaydk>KwZAg%pA2H;AOHQ=KmYS@|NL(N|BnFu zPXGZB{}9js3orp2kO3DE03(n9ColjVPyzGrxgw0aFpvSs3-9U&#!3&>;+ObJ&kBFtj-v(ju^!b8HZ8pm~rcrF&dpw>JsDY z4A1F`&Kha3?MTb;$gAm`tMJfH==3k>m{7a`@4AAH!1(U#iVp15G4Kow9#N1T2`s?m z4#3(D=(y47Nb4Jw;k2%iy1FaC*fAjKF6|03|L|y#?*Pvs$C2zJa_!RbAtjO?Co=6W zvLZ9mA}JCgF)}1MQY1lgBu%m;QF0?w@*`EUC2z33{>vKq?i0}97Xk4!ZeWXgB@b`1 zEcAg9Sx3cceKUL6fw<^FXPB8;j(3#ct_(@N~fj=%PJ9u zGOiG55uBPbg!)RRz!I9|>`>k)61iz8&GP%WgL@8UEtFvrCV}Q+pyq524fLyWFyRff z@C*raFbz`%3llL9(+v}oF&7gK8xt}ga||PsG6$2uXfQJak20HZzc{eKpi2Wq^9V&S zyN<93@9`lCOu9l-03nRP^bZ8fiy(~<{~sZ&x?WK3w$LCwvo~4LG+9ssd-F4c6JriC zGy$(0J#!p)@Cj3p14}SCL(n-x(+9Z_F|n`)dy@vC)Bf-a3boS;m$N#L@H1~S20L>F zu@gLfFg-=nJG0X|(X%_tQ#jA_IqQ+Z%(Fa4i`f3m4L|`rGhzO=f+u_OM{JTT_U%B= z!rzQfEp-TNx-XA_%HxC*<8@* z;T#SUlV%aK&#rQ+jJ(N)GK+?SW*7#O0CpO$8;RSluWzSOtG}?(5@k=6gg2az-Z7n zL6bKLF9iXQ0|inyx3utlE;Um#A+u8+jgw026zx)TG~YBGzq1;TkT$;&x^}Ql$!<;+ z6;b~b@Z7ZSdeb1skyGPTQRnmsYqL0=5K%uhPcxNLd9zME)m3YgPUDnRQP5QJlvEX! zO>cEm!%kQ4Q8;5&RFxq~@lfsB3(%BPyZrL07&ID7)@34f1c$_0_vg!BO z^5S9+_}r+EG$^U|NFeC4|8Dm17wN$l6ZA#~6l1s(HEEMOQ4k&16CuSD2u%|<%d@<~ z(*!fN?gnf)ds86c(+6Gi2Zzu!#nTE|6F&)T2R+teyAd|malVW&G$&R60&Gt^l}?G1 zA~o~t?&(*DlR2{!1U0qoFcTr+t_YiuQcX|;jn-4)R6L>62!Rz>ebwi{b2$(79C6b$ zzpfhzk{|UEzxMRG_VK*-Yf|$ON&)O_9d#kStL@sh92wFZN6V6(*)uqOU<)|zbvr=5E3@%}mU|VROf;ae3$f)qO zb*OA$J?>mzv?{I7%pxw%x(0Y*E~qF7qtK{B{o}I=ByaSRFWavdyKzY7FHYTbbj3Cw z!8YtBwNr!BX*Cree>F85w^7GdIE#~Q?-x~AM|f938=|1SyQc4F5KWsNRSC0HJ_)=oWB054U9uQ63c^Mak!G-qZk;(GF-AvQNGCeo44id-zpl)l%VfQ)lu|Rrm)FwN!T%iCxuHg>{L6n1F>iSCe>( zeHe;;wNKe=|9t_K1(Osr@sL?xmn~3Zq0-_8yr_&xL=u8Bd4&%xF?6M{=|t6aLou=B zu98@eZ%5e`7OmA^T~T>)*UJ92n_g6yAkoe2C|%2}`22|Zx~ypy(Rf)dr(*O&xstG` z&o}7Ov=D?th=EA+(nyaqiy$Rp&5KfLc7nw=Xmd3oLwHtccvN{+PBT@APm@;jw^v;` zmUDKNZP|yJ7?-#9H@DM?RdW#lfc|h$Bk@ZS0^kjZ0TTe=2ddD2Pu2d=APs8RWKop^ z?H8A~0t^5E02}}grc(jCHk_Y}nE~JdK7k4lz+$Z!?8=K(X_LAD3=_lv6rPKosWv@T z6%?4cI|x>liF>(bES z3k~RRwEXfG*?2AZ|L|oV_9m5q5r9cvk=I;x&k!9sU#CTOJ(QET(t%>M`php!$%R`V zG(`Q%r=6FM3yY8-*^)XLEt?V)4awnJvB|m@kj}DQh^dk#nP1(*M>%PYJtU#dLODZY z5?ps52$WN8xM>BfQXN%s53)HommI5fAxYJ{PL$ z7h^6#14kHG&QYJZ0#`_MjH-VIcTvM zmyPq60hO5J|Ir0)^*9|mguC{Z`B#Zy`MOz^Yn@P+xm%(I^`og3hXGZunJrBRN8KH+P#Rk&(KAi0PfKmw6k=h76Gq zw^gLfsIs7s$dJ0Ig{t>jQImn!T#uS4D-K+l`lFHuEI(14u1S2Oby8v=2DX4N(SR5A z%b(&RFK=KHz=G$5ZZLy5ym2_V$+4h=m5M()XIpin^OWyGI#fCJo-4YELGy?u79NX} zZ$H&N;o3MyI}MUS3j!bw%-I2oP8eVy0B|4-u2~s^ANLoN+)BR6w4SfeLP07~=W4%v=ms-M}y%o#6n~&shM}S)iLM5@=h^0Swd; zzztNu&4W&w-+&yWv>PouQpItH@fu_aTd@l)vKyNq)ebkI_O#Vba20T_1DEYIaDZ)i z+PCq#H2d1oF54%Y+vgWJ`P6Bfy?|S@+&%N$znQ&=fM_dnKJq@wLLdIHE!w)J z9h9jPX#BLVv*^l?!8coXTEy$hb|~)Qm`r&4IFSq7tuRXywR({4HT(!>lkYXck=NuK z{~nQv^5LLbT-!A+%e8tpbcRmolMVD@G=&&60WaUr=Jv8MZ0`NU;4APjYncMwY&y6Px+C%88@(h6=;b(ulo-j%e1b!HW|WT znm^&z{T#Y@yS6hO4hSgDGa=N^S)Ji|*FmAr&l$PP9Pbg}xd^|}P2uosJMrbtntwn} z#o+7izP7R1(D7aXKtb0=pu&O&4K`G0(4jC`?kOVVZ{R`wBL!*s)~Gnmvm)Z9Qeg((WUsOqtmmnf6V(Ax7>SH_AXMO9m>L zGEIX0vhACUnBTy05yw?rIPRyuafeaV9JR}#}(}tvf;n0hyNL7 zyjU}A!<7Ge^NhD=GUWVk7PBMvHkWWgPY)lw{d)D%%EU3wL!iw3nRN|stGl&6pusuELAU8-oJs&Yycshpx} z)lo-<8Y-wo89FMWrP`FLtb?9p>#4ZjIw(|-I>qX%vl^AtFpOkE477euORZVT+z>;y zF^GW%8e72?gPp|S)!`e<5$70VlSL-jXzTj*E@qtxhgz7lt&13J%rO>bZ|XWW@4dfG z))~J1!fRZ-TY?!_z4az1*l$M;9G8MxI_8>ZQd(9Ue#3kR-zQWE5SStl{uPOg+ofl+ zfvbHo3>sko@L)123pm}$9jfpEC@H6hSbYE>fCea{;a3}DEbjN0B>xY7QXh(+AblOv zo=~wLCgk;XqI%hV!V3T>CbPxKlM#jq8ZZDrvWE|Bi8UyfkVm3-Bwt5zeD*2R@^F4x zHd)4rt$Q11a0zB{GK5vu@VSa(mU!VSVz&6>f@S>4xymRgSmmm5ta!wngJwA7U@oqV zYk)bP`iRsE`10CY;LhW}+iT)B^e2s~A#jFl-6 z`YI{8T1ZxgQ6}!GuuaOShk_=N2{DKv1~`ZTwy>v+8KQxBh0zU?BEz8|QKmRWN==lQ z6htHq5lTcdUXhX*rPMIdH%>(2e^xWZ9=#@Z4NHu3s$>};g{Cr&F%Do%CY`bw$Znqi zg?kLNpa1|N8NxtM&gP?~MIhh}Q#)D8(f}Cz_@!&RpiyKpA&Hp@06rZe&t`@phBJ95 z0cVOMd{*Wc_k<@C>j_MMG;y8sh=*r^DUf*VlMItF@-JG*!5b!&9c1tU+Y>QMYJy=>4-_X!A){<*JuYs^Y-#4bX!OmEW-Tr_c;C@PVDGAhSZKz=UG3SDHHDrhGNN_;s*<^qc6eB(+dt zbyQUJixfdcTG0y%OQInJX#x{ULjE=MfgNmURSc39=H&^XM735XxB!i?=tB%QKtnPe zW3kR~2w#D#m>C&En8_XNFN<3oZnku|#r?~5zB3r&tm-b-oGYqk%<6IkW;wC$OLeO0 zq3crjnRjzAfZ=2j`Wp;P)S_g}><7s>Y=dSd zH8lcgYaYVw*hGfgS|(#VIY>rv{i3z8K?e@TOKZ9!W=zwbpBfv!3ez{n%xhj7Mw-=#iC}p>n|i&aT_6EQo?CK5CZP3+qAGa7 zXSr7O|KNlJ>@j%4_N+%F5DL>t#Z-Q-DqsUKtkGq$52Oq2uluSK(5I#>An#|(tFE0UTQ{DH|9*{%jVnOG-LCd=}nGl(5&Xq(59RF z>T6#>HyDNi&u~gNWTY^Dkx7fR1oP%|3GA!rkR0+hJ#Xe znrWQvj4V5?Gv{{N(}KB9rqQ!mSSv%2wM_;r12Z$40XHyUSoPRI7TWQ2`m&g=nTmXd z|7tiugO{KnhA$DlurMAbFvWT6VKgHe-Y6(Fqk+tq9{0B)cE;NrqBdnRM!G<21~I%r zcUG_cZEX*++l<)^xbd2rabuO;=Qd2?)IDu4S~cIH6%udg)tJ3hk~ywJo`EIft*)d= z%M>SSH2Qg`p;mn3&VvRck}(os^_ZhCPO*+lcw$P;RK-tzRmsVxVH&H7#3{b1%z4bH zS;<_0FJH3%Tn^$#ZLr8dKQj3cimWI%*~#^baYSXj;hJ95ra0Yk%}El}Xlb0|_(a1E zzLJR=MpB*$vo^uqb+{dME?stRws-kj9A*AhyVP8fe7`uGXWHh>$|>(MRg>S-|Dc=h zg^9b5#<*UH1;?vZDvT;I%OP3%r$br*01g!0+O4Io*EWK#3ecl90ewttm6;iOrusAA z45QaWTccqxk!uIM8x*dk86$1BwPeh;wWQpf7AmxP&~v9^m3IJh10&omLFYY-&?SZ% z1f!;5TK1f&b#(tSGncvfy9-0_n)S<`HnW%QYo@Q8n`uq*PqV&nzh?OFzTL!^-@WG- zFMi42=C7(5V#*kv#q3=%59b$V!aQ7OfZQ`ZazYme$bfr758o3k`ZHq(6>{cdK}DxN zJEvsg(^E}1Wjq#vE_QV`W@9boP&k)!E2m>UrE?SXb0Szk8TKf}Qezdk|4}>DVIpRO zO1Fa`R)RORbdC}$FjsX&NGnCQQ0;I+Db#@WGznrb2{t4KdD10g<`{%_92bL3L1Zza zaT;%xcgYk))|M_|g?@jAE_$|h_||sk5=84a8)R5KyMbm}1z3TnOoIb&i7{vX1|*`9 zBYT!4N|Jt4bWH!@SAI1li8x>NMNQKrL|0^p=rk`oQ!`=HS%@fJgs6#gCW&pABx0g3 z_hlSTB7TRMXXodIaTZ|QmreACe&}_7WR^{F_I-C&XRQcdpg2uzCSRlh7lDUu#IY9! z1{+E)C}@ID zw_!54CL-3TflRDmDVf-L1gHwR@whK})waw6w~PDhUKm~t?t zfkp^I4M{6jrghf1JvTs+ldwNA;g@Na7Ut|~@!)MKAe|x8j zyFq7jxPGeFl541j^R`#P_=^H2T6PsN^fq_h*G$+IiufgW2_sG6v@i$i3j(hKH~CIA7QsW2uLF2ycR8Z~xXicj#^CraH~m zZG@9;xFKzZ_LlIHcWgnhW5ifxk|0Vp@BswCBv51PVNTWm|iq8aR z^~oe-RCnC9i-d@yQu1BV^k)8vhV@%gK)nWu0=`V;?4U!WnZ==Z^0) zbv;LOEq0vnBZAk7kbJ79Ce?y%8mAtm6nDCE5QM1$v4oogrrRS=fO9c^X=g+Vh}N}L z7t>$hg>dY0e|;8|du3M$cTJQ$>bcr5tic|DiqtlI*vAi6?N9GaNShFX$$TqQ$Lu z=0jx^F#A`DqX>T96s_#llOigV&5@*L1d6|5S4G+{01KgIG_c5Il+jd{_=P#{304uB zg>@pg?{FTYX^u0M{vYpZc@~>fwqPP=R3qP9AnrUarcoPT4vgsMaofdPU&wVc|7vc zZ@bZcXqzu{wydFbuE!%AV$>VbCa>Bwg@!~bT4cDBffitZtD}#os(e+4+hsUad43sl zhiGd=@`;khVYtb9E-I;ZcIQr#Yga+^IGWKJ?xq}_v26lJwsyy@!#EgVrJ@Cc{JnZ7qyD=wQWX*z*=+Kv{5g6Tu2%n4a#VgjxmOvvuURD zYjq>5QukPpQg*-p15*Y}gT$$jIx#@eDS-eqzs4D;Gn<_K+q91=wDVwP^hCbSLw4~h zaAFIVchy$`8 z@@J!W`!H~&U0Tb1g9#;t6S(auFkjNQlz}>{xG-a8Og&6ZQ$=S_6V zZUD{0vqMDi%+4&$)NIYoyv=CP&D#vk&J51c9L~}F%;e0?;*8DET+ZSA&Dea-@%+x_ zJkQ>|&h&iGTJ&0+%4%$1Pcw&5#7xU4bcqk z%@l3V+lAFt<&(F2~xYv z*4U*Px7A!HP_AjR-e}1WT(dX}%c;zcONT72oPmK_Q4g#rvz&4ZWWU>a)(OnA~nQW!L^*h$%2nV$iF*A*F1ZvFc-C4@zs1nc7GhZmV1A8{GvhJ zc!y}m`)Y*-+s63%q(l<0bR5H0c(s!lf2ECxWd@|bsE93!etAeFlsIOdEs4-tXFeLl zSTvG`YlTJWLw^{5DGa%}eI(#i+A2JfB%CCb=r3iay+^{6vAtefoTL9dxr*~umT6{Z z3>V1#vfA$Iub+Lokmya`9g1l-AnYNCMZ!!)sU!sGsh9nX)3XfL6UqFoU=|jg-&m%Q z&5*)Ozk{k%zf8+O8&RX|n>Pz}OBPN`iZhj-?#Tq+HA=j@T`W zvn+RzB@UY?MN=I7wA}d;!=M!)EZ}kS2AJ?Kd|a)1#ziFhl|QZ}TPZQ^BCX+7phHaL z^wQoMv*beQg#~)u$mHZ(`F;x%Soxa0{6${O~dSC*I46-PTmb zvp06iD5uu738~4VoVNarlG>e>s_XgFzA88DR7bx+HmJyn>x7N8^9b#}zSv3kgOe)O z(riB zoUE?}ap`4}fj85VQzs+&Z2n5<$Jti+3kd|MiB*P-;T ztN>qbLZK^P0LG*CVE#w9d&F*y|~t%Qkz=XFAJ+|LuxO`3nB|l)uX2Sna#c z*2FBH$;`-hzb1Q;9I<1@j9FIzyH#79=mC?Y=B==4?l5Aky~MiYnBhZEIpo0sS27&x z&&?dXe@y%JeJ>nNa()~_nUtH!e|R{=OET$a{=@%w*L^rjxe2?Z+qU+S&Z_+S82IuV zap|L-o#{VJe?5Y^>81YviT>ww!*mzv=-TM^0uaeSDN_b2S;2w_4J!MDkRic=4Ic_b zNHO8SgAy?gB-n7FMr8^+j+9t&AV-i8Lt;$$kmJUJ!yaZrlS$JsP5X52`_^;i@m7b$b0Ti zJoD63CuL&)sOpAH_7rqbLVKb|5_Sx$t+e1oqwF@+N()U<&R8?eIKf2AG`2@GwawE= zi5qUX-9{BGFwQh>l+j8nn=~>@Q_bzRPg{NL*570!4pUY=y;RjjYh{&GM~%ZvxU*=b z?N(%eC3Y{hgpnkZVL%zCP;3d!#0~I5a!9@%-4hVW^CxRp><_qQ_L?DMCivjxm?o16B~;7$br7Y^1Tv|+%G>71+*yUix+Ay zXO1@ij?dd1-F5iFeb0O7-D(A@v0Q{Z&PZo#^<7D2spHjmYlS@K8tkj1)>&w=V@mp= zs*5gC-}HXon7d>)?oaCeE+o=Qine_Nr)#q{{BS+%xxuNaqC)&}$d7`Ckz|VH)pFo+ zO?C5Qcg?&o)R0A$QBFBety9cKZ#G%XbA49PSD_^g*k?}@^)6R`dmUETFy(zVTXT1v zbJ_w5-lwx$VyK(Jd`k|2~43eS+QRyH*s*-x{HZ^u- z4S^>z5(p)7Gz0Z)P1m8Hj1VY6EZHxB1Y%JRFQOzHWv3xZ%%R_U#;yc?;%Ms$4~39e zohmV?KQZaxOKJqhcb&~!|4GTVGQqv?#Zitz=~F$Zf`h`%QIChKMk9j$~nN!6t8as;wKidO@poA2k}GDF_(tTbgqm&VTi#EaFB_H2m=Ri zcpyWBAp-*raU_2WNja^o%5VKnb#~!x2IzzBX8-- zVAYO#itHU@kV7p^d;+w>jLvxQlcta?0s)}7j)*WZO@_WJJX|A4d%W{rZf>ERdlMl; zworiw5-mpVy3iJiV4?xJhecsHaJmkZ;Q3MEuM^>j0$GxtDi}dcGTKivwh(}C3h5Io zP{T+%DAI1lZ8{0=8b>4{g9k|!4v7Wn&c^uAvA%F2@PX4iGZIk$04o?o=YeTJ4-CbD ztkXUGWYCbB_{9%ZP$iPsC_ym61#ap!ir%6ZljNm8gbBp2-ue*=Gon`;dU(bfBU?Wc z(%2Fu;}c z=#W`PVZg>|LPYum8U|;r@rHU(%D90YrvfNZr@AWoKos&^Q%YQIH!eN187eFbf-Vigf@9vPHDnHz0L%{ zUYpue2QampJQYI#IJyW2gaHB041k^oB8e3Q00%3LDm3xTQ$5Qk6I$RY0@zxGf16VU z&wPVH7IATE~xa%7h zfrA*x;0?UEXrU=u<){Ch&g>N!N8bAeD&QReV|Q`JaiE2Ay0qvSKl(v1L2>{f01ls^ z!oRIWaYHv1<^ZT^L(a)fOax%D9$x6cmA(*6tlaGX;*{!y?zV_AAZQUeki-oPn^ZAW z6b@9zL=4cd1&g|nMt>Ir4$_xc0jzl>8s(-#x;l|9yde{dsHhAOo79S?p*`CVd)`}~ zRy1(oBpPjOChQu6wSFEAf8Qc~Fd>FdaDlBD(SPrMU+68+ySbBr`Fkt-!ZX7<1CWZV zhM=+&ld9tLJ%KB~vl_tTGc5TS224;ZF=(t$V1zWtk^5l}Iw6KYVK)?Hj?UsNsYpQ= z915-A3eyQTQ-Lkd(Y4;XB;1<8UAi4@Bb7hu6=3ru(2=E9vXpOIF55vZWz)8CgPldG zEmw-I)UrWZa~2)EuG6|UUfY!FnEB2swwIyK>5eK0K%y8?hfK1YR7wfI0wfBDu3mCO5!2v@3!H8xw?x zgp0}&jhc`xFtlpHst=r}WO%X9t0`f)f!u=#6_chn*n+WZDricE)Z?gGL_|=yMYhVt zXkvjGODkPe#}$C9v~oIR1T%V+j$si0D05Uye~BxO;70Lj5~m7@ z!K)Twm;tOZv~H>?WDvB(x`8fWua?M!Do{A2aS}9x2#rdEF+0Z_0LQaSsYbqE6${FLvO`{RJ3c+s50RY;_+yhE$`M@7oN2@$Z|58U85CfR7 zh?|tGVW=@~T)Sjg1R(HAgAfBr)H~4vtB~kD@f$Eus0lcFL%%$VKG6rNK()Ut%zZeN zM9D&0;zH}L)PN|L19rPX7Mf6vL3kbrSUV&Yzd#dv4}{JH%hEWs}q1v zAi8Xtro%G`lVUOtdw^uHr;wnj>)gCQY`H>kfHTOq>TJt83<-TQr$nU46KM&4V!i5= zyY*_%kg&$CNsnowPW>{d@~lpCLV%g_LoQJeZ92J{aw&<>CO8;SgrLJsj3%7B4g`Hq zdb9(y3xM)$Dm{FUF1btiJ1nM3OM?i5b~L9hn5mkIrY(R2ta71bV1#b8jwFdm$jiEI z(oc-guVkRPPngky36J*Ny5xkt`;0yqNhS)VNA}u<@=PX=SVTJ9$U|(CIJpo;@I4%G zgAURwgD3=dvN9aFGNy8XK&f)cyJHFYcm@V7GApqw--Ev!Fff%Eh8W1N0|OAhQkb30 zgzUqFLHj_v8#IKVJ`a;LI2#5uxPi4J2t4z>sIxCEO$IJ42xu6TL1W1J3Lo}xL*bm&olqRl|56Iaq16`@Eg76G zAxtjnsy5ZhP3#d(aRbd_{kG)k!Wx84L&_b~T2|?LP1BJ=a1$0L#GT**q*z&{-YB-} zY9-Fpog2I@WnH!%+>K-{!_=Zy=31oRiKS{=uJ1BIS=H4%A*O5s0DkS)ABcnm;D}2Y z0IS<6pV}q_B?x4grbYY&Ak!wF+NlRf*u(`t(t-%Mgq1V> zq7j9vr<$UPpG7DMORs?aLxc6Ym)g`fks73u1UfTCF>pkT{}e6f8#8mdC@OQ0F{KY3 z6NXZBNc^%{Im1~=U`~g0h+%jGuC2O&G6M2kzxzDIikREQIy^N?ulyMCOy+DEs})ZgE3GDgX-f-t$Y)bb*rIlszC$GWJm_N9EP1dw9U;bE2}Y+ zh>$m;sNAbGP~gTJSp*}{D5i>p8KB&o60Zb@ku`_RZ!Zo^->B6NFzpaW@zlTIAF@P-wvT%r^z8Cs4&< z{G8ZN6j1oG7Wjg=@WhCXZ6}Nnv}j61ON5dYaDYk3xq8&jT}%jGs9}x}24ylIrIlIc z%uegnPL~5PVMu^aFyfIqs5h8|)Z-=~mEoI9&<7<5h?`+;a)YfyV>QOb{n9z9%eWvD z7&zgxC0R0%`o=+BKac|Bw31mq1cgg=k%_=L9*t0e8i-ZwPo1T(g>_;mrrAu)(Cy?W znMHllIv4|#i&H=CEr?5v8Y}B! z_bHK!gR2`ctGg4w;!{47rKm2AG(3d^J$(Z|m4ueNyJa4n8*oInyi?@xzVoRVIKyR{ zTF|bfOR>$~uXJRSydsZLk9ADYZsfc0lH;y)yNHasFaQFI7&DA0zP$XQ5?o;x{!3fs ztXn~7aUF){kbge7A;9oo8$lU241ViSE=+xx4UfLe=xL1b8ia6Anhlqp= zom-4p#Dw(-dj!Oh9ny$sr!sb@j4>&K|ER?LdVrY)vWpD~nEY4-$b=Oj-}+**p{>WQ zSphxDzPM) z%1rpEW0Fhb1xdu}CVIZ-OkHQq|D_HQs!Hk@1}yu=PoOfCa7*tU?G^=8JgX_>z^fkF z%Qs?=r2b1%GqtB6%n%1HR!huSI<9LYP2cS3-?FtAZ$brb7Uzj^U#XSVoXv@z)*O6b z1-8rszEs$FV>O%4CEl$^gVIr zAkOe02uL7fhVU>$d#@;#w`97BB@Sirggkm|IcX{aG}XM1<0d*BVlPgt6*w?SP=Rtf zCxcoqdfO+>i|`dVSx-E$_c}09HqRsbvY5qf{?w)-I7bUbPe+zTXgbCA#>L|;NEEF@ zcsydKQ&h*J$PGj&E!Zm={|5A*{ei$MPZ+(9bKIzAIy>gwJVO8~5;f7Ro6|$qQ|AjI zxWPI7QW!wgCcc$~rh~p{ayWy6res3B6@aF#7LtN|x=_sCe7lY{4il7XxmB*Xp|i$Q z{5)Lm8T-0Rz3cULc28i&TwWI~Ql%+SNU8;N8acMkF#?n8i-|Dkz@7|)Zl=4lYEg`o z=ET~lMGicZtWvQ0JY%=zEF;x2Hz)BfG^V;gj*@U?IA=?CthMqvlN8Z@4@rNTwDCjL zUl%R)le-_#)AL40?~U+_R9?giFohw4HWc$WT*1acYNFR8UcKn*sioe^*8g42&S9l)8TedK<;Ff;1{LSdENH=>;`m4}a`GMv7GFgdBva&n6XK>ko8*AF;be@_q@)*PbXgEhi z02!7uGk!0PyU3;rSg1SV$3_D{9K;961kVS=p#@z1jG@PWDULOWNkBOq@M{d6yv$R+ zpF@CZM8$7{w4bsv*8hY;c=t;P^xpq{AWN_Hs&*AcIhk7*`QZf^6DLVPKEpzHJd3oY z-1St=rs4%Z96$q6tpa(I2v5K{f%=39b!5k*?ajwHdsF3w(>T-5vZf2X;ctC_0F_J` zC}o*=2>?}4|5>sy2^lI|bRbNC0B{N>!?dE}4`KtA5#)tK6Ty-hx5T(1pMmZ5Cm@0IsM9fAYPj z;DY2O7~y~VN!Xx;7%r$`gdlD>VSy)_D4>b~vbf@d5RM4niZF60A&WOY_@aq6hNz>B z`Y8xvh9Q>NA&f^JIG!+)?ABX%PeK`Gac^K!&}IZBLsDNK@#Pmtpe*&2mmqP8rC$Rr zG^Sq*rP)$#UqWOiLv4CVrzC8xRi#S<}sf+CCnnv5D~XkKbEteUN^Dk+zg25S;y!gg5_o?|NND!ANw zDTx>gF~X8E$EsQqn0{fZDsBrkw3a}GPL;0@OT}cV~qt)&2olUFE|V%!$jLJ!(T7# z#vAhvMD4C_5+x_4t>Sqvr+BiBHnn~td+M%dcj{)@R(8p%l}mlH2yL*Uz1O{FxE{{y zsopZ)mu<1G%PX<#idDGdi=Il1+lT*o8n2qe*(#@KW(iS2B#9}^N`wseAMMEdl|Yvy=r%n z2mq*TbE~>-YJ2Yco}tE7zcUriFaNLviuS05i5%pSl7tIX<=*#^NCGQPu}j-8E2%e( zgn>2wz&AM5_SSv%3K0NlUn8~lxg!EsTj4XNC*Z8XvtI@3#AFM^#);VTqr}6 zQ;!=I3_CH1XhSRN!DT!whe)HM6tNhio_+C!Qj;Rq$f&fD$qSHC#{$geHg=^29>8=ykUuOShOMrHKi*}YRqyPp!LJoC}3}!aT8O(ub2~$@Bo|vK+$b5m0UeU^*;_}I+ z=nbnVp)o6#ZVAW;QVX1fod4ti-*>6gOb>L*1gv8b=SoekFLu+zl3CL>xB4X(a|PTc zu=1F;2tEaQ@GR^geZskUhO=6mVV7EO*+@<9rB8b;31Qhqs7QV`Q+D-}AVmqtelC+; zr32IM4C^Mlk}phjg{JZ#H;}eg>Uew`on$9frtI!^l)0^qS$B&&cTqQ1o2(le_{G)t zE>syes7H3J1K;~5j2M3B)Gj5fR}5=m-Bq7ku%1Wu_;%ed4qMwO^QDliOd zCPoDFcfv+}@PS3v;VvGPhZQcchNtLXoMzaLXz=VmA`R z?|a|i1~C;XOM6QO^8fU9SA%$Nc#=Y8`jmU8B2$iVQVz{)H|dj4_;JaZ1Qv74=BMfr z27i8uAZhc2-U8x}tQZ+*tWZ0@PjVSez@69j^5zsvT2hqM3ztD^$)?VJNq*fVEULcD zN?O+GZRf4r?Z%laF9$Pka2(x|=jJ?L^(}yKBb-m%B-zXfgfN(1+n3ijDPc*rSG>ia zm7hntI#xE3hy)3l!wO_Lt(PI7Gix`wJlx8Pb=GFe1Q9afJg@OM*$SJe!4#_OXKTk{ z5Nj!aiHc$lUmK}G)9{4(T2a`cG)c4|?X`Z5Q94B+f0F;>&4320Z;jhMn4sl}VDWf5f&0Wbn? zE|;mfO=bmy+f4d|VG&`hKqrx=_4$&aEGpy8S)H1mOTsWv_hr-4(EL_kP+peXHLxhR zir)%Y(pOxrX%Vy{-75VA@>o?N005AB0AzWauCPESi@?>hiZWaxn1fGTpbb5>E}x)MiiF zc2aU$%#%cV?H*l9aZMi0LG~`M2W(+Yh&v7M=KVgXi zVv^nsUAXiGmA%;_IDnsh1Yx)tWnn}WbPq`Q9O1c%vam!XAi(ZO(pWi+3?#)-=!9b| zkTz`@WEoldI8stb4lCV@V%-|!I2}BVO7l$DEkPh^5t;d1%W%;hwV)NvSxRv=;UKw+ zdlH_#PoZyCE^hsD960H?m zLfzjYKF32*)N>pRBT6E4WK_rKRFRy}7hPh%d6@Az)xv3_^36=ZMV0zZ(M_4+iK$}z zd7l=c*eIS`_JJEnj^1MVvgoVjfgia(wBqRVP2*75T92!g=sF<7>q+>y#TpCmX z4#dFJ8QnGpfEb)y${9o$Ox+Lo1RAJ-)QtotRD?w&!x>1O11wN7RKX2=<4a7P1OJfO zTnvL_(ZoHqHf0 z(i%UKBq(TH0LbJ{D&!}aoI~zlO%jtpFl6^wi3-*v0DPn{OdU3E9YPl6>X`)>bk9c| z5ScVY4q!$~lm=eJKpNP@Cu{*49uVK}LpNf4GdotfGV5;Lh` z+nE_Z?G;wSnPeRzn59qNWYaB`US)Y*<-JQ>mLYn1*>ZssAUa-`Adi*NO{E~_=!Mz( zycQn47Sowm?m)|sS!9s4o{rt#GqUDBM8`5@M;NSTYeJiTjZlc?4E2?wO8B!;g_sIiXZy7j_yHp;CQ;Mq z;#4h-ZdxCa5XpkEl=I=3YSJdd_*l2FMgk~>$+3ha7-aAagI3m6^yFr+^&g~{#U zFl@mJI%R<#DA5&x7EGYSBt$s!30Y!eGNhbn?4ZvngAS5}$(02FVraB5fNJ0#>PnTc7`o1+1Jgb z9QNg#RZ?F`3qKXvQ(Q}1h7KI&C87|^mgH5_)efYU6P$pL=NTT{z!sb#8s0S~q}Uee zCE6So>Zphb;}zTMz2~md14REB%p>k9#9`F5-J5x)pHMNUz^PmIjZ}km;`?>f^(kv~ zmfKS)EBnn9gC$=sX6vyoRn?f{c6RGYIcu@n;{@ zt-HjFqTCCtWfLbEo-J9HU^-!--RkYEQgxvhVAYn2lDq?49VD>ZKT& zYca~+Sk|OC*;7m=XyXLEJW~+9_n7|q1@NQW10T?gR8}MBldBW?AZR_`b zo6*2u_Kq9-rtf;92mRTf?n+5~4un$zK>p_M4+IZWqQUfN+|)f~l=`PYT&n8-2*frM(ku7 zyuoQuK}9@eOQwOj@FXapYz-%bK#;6*p$fKS!k)$eOQ?>RCJ-nTMcJ`s{Ok{6^u;1* z5B$jRw`if}ouMt=k&*G*T0vf+UDD>6-J6hE`gGFrNa3iqFyss(rU47;bkfHuQWVk> z^LVDNVT&HJR=2>RsXfzaezN-z>e~ESSQ)A}-D=#mpl%q1v(YajGMhxX@+;$)!$`uC zED0}Qf-e8_f+XxRCfu?l1oJKnb1(n0B@^hE!2Nf&cIJG3{4bV0v!Hp4SXck@bzbU>rDMMv`ud~{0NG(KZ9LbEhP zr?X5q^fdEwFy}HcANA|aGA(b1Zb)uK+;B;ZaST`lYv77skYi|E<5S)!Pzt~Uqin+R z&xQZ0KpJR4#v*J05QZ>h9RN0^~I%Z7*s%9x5_ZQ0Y&6M zOKid5fW%2kq!3?RK^$;0P(jJ2DIF3qs|ZE>*xrWfg~Q;0+A;(YCIc7zXBYs19_i$b zqNyqKAEyx%Cav5;$YJtqp_wt4p%U&a@gQi88Lrh1An}n{#0#3a38q~MKS|C!&CYb0 zj(ca?9?lOcUCtlP6Uj9msWBa9O5>N6o^>JJcI%wCB*VVws#D(|?bX-AAvljAbuj-Q zwJ`8mBRxQA=_E^l~B-13KiI4@f`h>tjnOL&Vg-jW2nMH@S|>xGvMUkUzPL zcX*FOIgs->l4m%PGr5*y`IM`;k(2qCqxgk4d6_3UlvBBxM>&jl`IvjTk30F7D>;aF zd58bFoUeJ9%lVGC`IXE0o`%6M`*@NsxuFyDhu<>S@K1wxM>K*R zWV~$XU@QV(#3#9R9q4SD+zLr(#0=(j#A=VS)PV$Vt(gu(6&S%(Y;e(?B>(?bi=nEqtP+8hG^Wky z6}_@06u)XhZ!~?_I(^hb{nS5w)l0qAQ~lLTJu&br4r~F}^M)pb{n-D5ec6k>*^~X* zt9{zDJ=(kd+P{6AyRCx0<$`V_UN1{S4EEO9L4eZXf_)mS(OS1l^slWP#Ho{1abdl(xhxym^8|S$;hNE zQVxica3tH3=Af_}7ct>@LomihWuJ&O92Nsk#F;uTN~}3?Vnm0;Abt|c5ERjfJ$LRT z_Apt)s6cBzJQ@_DRfIjUD*I?utI>~In<{(SP-CdS=@eT`WH5swHR+H|WSaWY&iG!a)d;nWOq`w^0=TyzgnA(l_Q=(a_Qa`!L zlu7YE)vH;zcKsT5Y}vDE*S38dcW&0oV${>U`o^%|!-*F+e%$ylP02JVXFd$Ib7Ik> zH=j-nI`-yXv450KU3+5G$xxS9Of1HY>zRg4Z_nPnSH1A*zqhad*uMPf;@f|^PhY$M z_j8ZHO!&huKKT3_u)YHOd+$K@4lJWCTQ2g7% z&M*+@GXNk6((|HZw0HobJOMyJ&_fXz=pelgMKypl!f@dKlPVlQH3vaK62Q+)Kbh2r zEq*X+m=)dtC8Jwa#nxJ7Fah>XNmd8|0XJ6oK^bJmwPP4bVCCpjh!Cw42R=>up)#o! z(bL_5_WdCegC?3aEo@KvDql0jKaxQGD_F{V8# z8BA%q%d@(o(upp=cv|@|tgKQhGND$+3o*_j`p6;61|v%;vi6!8sGX(~tf!u$>Z>S> z_G(KfuzaqJvCwp4&T2fHM%n4O&aw=ts~B3$X2}|YY-os8U-sy$_kMZwvll-6-N|R(`tNUt zoqX)&cfa=VfnQ(!FrN;>}y{2#y2|xK2U(X8=&QsBZeDr;Ra$r zjV3-)F`{&kCLAP&2sy}^4TcbeBRt^fI5*@9 zPCFS3U?PIBLEQ{yb%_Wj0+5r0d8>(63u0Nf@G_{dS&?l-oMI98lC>iaqYBD`%TY+S zg@rWb6N^|C7OjXGGQJUxZ0w>G;W)-44$2`_teYJ1c*P>pjE_XrV$uFs#6_r~V553~ zU(lA6x)?@fb%R+`z_zlr1Z9Y#S&LBe1vi}$?Ph<0S-xoUkgCWDG;otzWFT3yoHgwz zh9P4fn>I#MLIsvlBbh6oHny%sre$PYnC&Ev@FWt0coT<>}!xsrff$ z+`u>g!?{5ws@Y9%4hMAWaS!8^Q%*6hr<~*r6L&^8&UBhHhp3T3aMU>rc(SKC&nZti z?`fuTj?OveluvWu>7V5|M|1oPlRM2+p6B$FpbN#PJ+ZaVh@R=8%dsd$&!o`o_y=1M zO$J2mDbaH3lc6TX7E0L@Qi*1C7^8!!L-V;%gO;?THI3;wM{3fGZj_`AO=(Vhnp2zt z^`#Ldk2{|O(1U6eOh;vCMTd%1qYAX8C0*)7g__lWMzx-e3g=4*&3FMRkNAX z+)~(JGINi+U2p0V0~n()MJh@OZm)upTBP_O6QkSfhB+*oXcUfc#OUydNnB#q++YhP zR`H5i++r8MSg)Eu@da;?1{mKM$Fcb#Wcuq@{(lDfuyqL6=kw$I4TET3Qm!+562MHHc#ybKzXArdsu__)nx%Su`bjWwF27qSQ8 z3Y_xDwZzPlZj*G(W@xrbE29}OsT7UX$ObmEwfB;~iW`C#+~CA`I!-B<0 z*KF>Jehc)7uDY5A%kqu4I*yErsne3F2Eiu7h9-;T?=CWd-CXU-ZmZ=0>*=toE3&SV zPOZEeE-d1PEb`6A7Q<&IO}z3=&W7#jYVPNBi0fu+&3+L7kZ=wo9IsYlhBB;ga$b+l zI7w}QtueYz$ns6T@S-aKsSV%8vfRtq2(Z1fugz2m%Az9dZg4OVY|1W9|5(l}?5t*% zt(-{k#R5m}bOXdZ(b{$)0Vwb{^kETMLvdBAA9I3vzjiCl%g>jLu!JHyS6a@urhMsCerNm>=*Ix887J!;iAYE zZvZ*%^@6O`27}+UuxMrmWe$$%!bS0!`44{)!_O zYbPwvn4~2#Z$KdGrWZ5eQ<7l?)Ib}LCM7GzPb$Ovw0!p~)J&6XyTZiFz2 zZZK|?h6W9i*RW12GH=l!?rh-m(>M?7;B(dtac&@N2m_1=e{1Q^ey)RQz#HoLO~x0;S^O72=rkKfM6eD z6h~F@0Zdc{AYdOtaY{IpNQ=}+iGda;!ASWI@fhME6oFQjsEJPNTk1q4z=%^8f;_Ax zjEp83n-LC%K?Q!G3ebuxuLUDW#Z$s`v8og_F#-)%fKw`hP6EInl0ghO#R0sd3T|Lr zm~jg@#W*pdOP9zXSZND1Wh5#hYBB;PBq0o9fMaGN64FDtVhIy+;1jlB$%f$u-asBN zXj3gH1~lOkWB?ODW(#5<2Y4nUVgLuuO3?1)<(ji;A_)*vvY0My1zkxug>Q~xGQj_w zjOJ+W*y<0kB2ND1FwV#kA?-*%iHi<{@V#Wv8lP)42gwj^2@ZGgXS8e*8Dhd1R1ATQ zX;c&Ch?2VCaNdg5=pJHR?a#tYsl9rV{+6z|&eLp?;S=zt6PL7yc%wMB31F!SQ%<2Z zLKI4+Ccyi)=Y;z(m0vi7q;Rl2P zCz8QqoJbPfQ6k!<#*mg#Z>Amlq+Sd|7;*p;#6Y>Swox*|9Wfyc#8GZzf-!S#KFN#S z>`$}0h$&u)vBqX3=Z^rPre0GiS(~Djm4Qc>`-=dQz2Tsq9f@bWZ|4uJXx>X{x z1~V{gl^RH z7&Rf9nr&tO1{6cAdl6?B5MW|K^b~WH3XDJ=+VU*VvPS)Y16so_TSYZ?p?%LX075|? z^4B#`EPVeLfbGtLWFUaK?G4=EloSISbz&I@{ zAtZ=S5^BscG6Jny^{*sh3?@P{4}v6WGgm_56D}qq7U5E#;#+>;D};<~E-fhj^OSUn zAUA6YpK&illXpMQy86Pu5Tmi0&IWTP^HA@UE-7w6j*O5_DQnW`atSxXvw5RNA=@?b zh6&WBk9S$O(L{?W*iR9MOpRAbz{Y|rbgnBP?I4pf48ub7{@9bi0?TqnARVG#Ph*D) zcujaiWpN`oQ299Y!4J+-0HQ$x%eR(uln^W+FGrL$bksFy;Q{}IfdvrO6wCKSd6_jv z)|Ho;nO_X>2(Ou)t*=tze$Vo4Lq%+VwoZH?SDryZKixViC|KD)uoGI5i@eD{3{hAueW&(x|zt>vmbL!Zyq0h|AzIyN|40)y@Ez=e(B)2trR|b<4uk^s?T@@}WL{oW9&$#4IDa}}k;C0g4i_Pql z1&eEyQg^(RD3{#O2?2DIm!YEK}1`l9r(93{CBS>o3g7(8Mr_M zE1Sg-?`NtOB9<{k9YR>{85j{QjRJsEIOR?;SQ(oLQ8<`3UE656g-zdJ5eA@=+{Fhb zArrI!g=qy-q$><6fM>H}wB4~-5Y5&K!pe?rB78V%cz9>N1qVc>ZhJ)@?_>uv;s*%A z9gEvrP}L<6B8WpmDy|!{AetXJm;We92CLAj)AgC0%&EJ$${O@2>XqKauidhfj4Ewd zWjf1(+TSz@50_6crt`qEj1VDj_@2q+#)#skuO|QP>@3c>Zglf(X|9q%w?4lb_T0_b zBzI6|8aZR-a&3~ zApq_GL}z&)YT*J;G!FDZ5ptj|TQn~>!8NiC$Fp3^k%JxtXUk2juY!r3tzu>+VO0R* zw0IU#V)(Tiq8CG$XxBMY5DT1N!eKr5Fv7qcf$_H=MWA&y8K_{99xu%o7&CmfU~J3D zJo+Wwc4$swxeB*eQWXs}p(B*#H_?SRM}?ygr6NF@HEq+QBO(%PlOX6Zs0-{@-)r-j zdi(B=i~LKr!WeQL3kQ*?m_)Co=cXE6R|WqCX~H(|v(JV0R zU%wpNR{1vQj^1lSE4lJv&6h0wU`Nf8D?wo`+Y$j5RuOPC$)kZD_R=lYQXWH_M6STXf{chPzv!8Oqej9<4G0Xu#-4*#@SDk__e&t)$;hOsjuS9$gtTUnsH7Y!d;F9T>zt<9oSkBOPt{)CqBVl^s=exT@qrEo_k#Wq$g^&ViKl;2W3;!pe}d;Aa2AM7(>P7?VxR6z0c6P{KZLfum893{Vln+`xh6WyWL{ zGv2(3QDaY>pEN?mxsxbPVj6??)JQC&(vZrOX3PooqS2@tr}EU~bLvxz$}|y8nicEM zsUahlB?}7a)1YvDa>dCtD>0y8b%K3*5+}rA7?JM1yYwN}rXd9@1)LQx)c;RN$6`IH zY#7hBk_|8AYjx^HvxTjmC413pSD(rdLu~r@aoM45ml6wVRIKaJYPH@)JJmAS)KfJP z$)stRrhUkhD__pMx%21Hqf4Joy}I@5%In=whCO)PfAIa;AyDwwiIeOQAn1C zUH!E}MGr|LfK$qVwANP>rudqLnN4UITBr%elZugy@uE*SnV8I6580+$Mv?KB3~Oj9 z6<1?O6?tNkW!-q-jj%}unP*V-CRc_1>#x_n zfk~JTA{G~Aahe9CoO+%~ELO+j6_jSQHAoqW)|Ry;w5SF1z*LJAVP>RBGK*|lKaH!A zZ4o+pC%4;@xX>Fh{eatp7h38UgV+j**;0;G3t3`PB^6muyA7J>o`5Nv6<}lC_z+@< zEjSfO?mkv-UTaZS8eV@US{b^ZU4`dUL-`akS+g0;Ai#CC+y8J`#;!|e!AGiPbEOyW zX;xb>;+w3Vf7(WFhi)246_Xq5tQTE$GMLGL#tj4P)$Z-bU3f8UjrG@HQzuZ^WS4FB z*=WPJUw>)0-D;;C+qrLSLZMa{%P&i)S<1#TiLj7W;%jl2zqy$qx+KA&P!2qTmhzl0 zxA}3iKX3R}!XsOlxlodX5d#is>;+t2VyX#T#0DSr`SRKfQC<}VkV(Y`?46kCS?eIaO&cYG6WL+8K@`H>lpF4#JdiDMtY1H(9-%M^SesHO0$-f@dR=tic^KA2rxIUWkncTQ@=JwnhvTlN_`7l>LfNhJyvpEohr_8 zELgU#iRU|{Ol5s;umx4R@|Cbm<=SFN%jk3pB>$};&HePpJ5OqnU;D~g2n7e2>{ZTR zfpTB->R2Zw5%EbsyI~lsm!aB;PEc5sN)rX+n+)AeFX-c!5sTOpT&iY4*Q;BY@MJo* zyycIN(xbBaRMMk`$5%pNjV^nQA%9}GZ7|OUaB z8MqBHpWnji*wf_(&AWY{lRz6yFl`LBOg3m91E=nVSD}x?TVOm`b|_v|6Em z%8*4n`q9>nhAAQKKw&KvG^QzQFHypRiX;QQ7S^IxB7p#yPzZG)1(ySW^dN|N5Wp2t zh9o*|-T;7=tC6DbC!*?;Mc6BiksK71wxD9m<%N+tS;VS&9ndM`WQ&r>0gm*m30^+a zlbPvlMpB%SMdU!p;ACv3a;pqX;NW>{)QGtUYP*R1}BZUCyT(rT;2_m zXfJAX$p}N}QdVH2w2Y&}tPA{5q$Imc%ZV8a?QZlID94yaFkyVnV?%1(;$FG8-`rTq z0@tVU9nLBSddfw~XP86rt6_)JL@13z>H84%ltrz%E7zm*p8Nd&l{fu406PV-9!4So zTN1_=0)T)pj3~2V2tWYNEsOy07Y^r50~K(v1po*j)|lve)7v}YPY7ebU$+QJq#l4Y zELiFzdOH9dz!gjc;08=z@V6`?k_Xsb>TeiE)TQqBMZo$NwFz#6b8lV_j7aE zf*3rfZ;X&+AZe((?q~Akb1&is0=a?WG7&2ofG83;xcwU>v4t4?;CPv+0vza;jDY9B zDo&PxkkKHJVwZ^bVbJ#_hJ5=R;4%}(FG%1$cLRIDAId2I#)0S3kPPK#MHpN#eXMkD z!ypF9=XP7LK=vYd4B-$>V^w)HOr8NyJz{a9aYYkDIEI5SdR0-{Gh@Q#F^Yvn^VU47 z1trw77V>2>ZiPl*G)#uHN0WpwAw(p}fLxzKXhPQ>*(Dx1wS%b?D?m7eM3^==SA=DQ zK@XyAuvY~SP;M*Gdc?M3t0oLAz!Jl-0!6R|X%`6sP;FHpZJ4kEPxm2NC~c*uI4ss} z0l*TGuxiP$2#sfWShoTX5K^V*5Eo*FYFKP^s0?q|Z4LkuUdR%Q5MgT)hYwJ7bk}#E zV0ZLlcq;a3fd^`72#9nCh4#XPafkql!(MgxE%FurH%n0?BJ+4@@NOCLZfdA}$-r+S zVJ`tkfHxp*P9|;2;B5t1cL7)#VW0-DXnnFa6A=*uvBnk9pldp$AR3VpXpnwl;ccG~ zi{&;Epuh{~#tW{PZy6zQppc9>z!Fs^cqSouD&`X65{>#sWPmt{-lz~q85EvS1wK#(h*))R0AMiDi4igXVP0rz7-A7USrL)&E$3wf7V!yHPy|)S z5;q|R{DlB$AQmVwB5Bf30gn|Fm?SY9V%5k?7! zR=Ji@Q3XGMls?fyTR|1vCKU0QIDR2;$-o8DRxhfkZepMb3&CTckYdTOU_StE$$$_c zp^A|p1{om?J4TQ8VkAoWFlljFpCAcbAY?YiZP+*!kuU=&VGt}P@dN(zinv%eXQzgWg9*CXjL5k}?AT;q6oJ@u88bsAX0|V$#A?PPff*(` zAQ?}X))V>YCU&H7=w?0ImL@H7UkGFW7GJVK%yb(gSCbnFULk>lG$oVaVJgw!gE~l{ zXVYj3+Mo_vA4mA0w}Mos2>=#qp#ks%q-JVCS$bB-c6Eq_TBrrZ)|I5D6g}CLaPX8y zlv!zj2B>FR(v=W_36lV$U08)5%3m2ehTAt5K|x=J2?}zk zmG**o5k`rm<|BTQ2|>Da5k_Aq(S`#Sd2P6i9>+P!#}FSe3?DWS2@wXdCY&iYEfRrs zVn8n%v6$YL5^Bg0dU|fJcYYP&nd)SP^ioZabeiHv5fY!KcQznoWWUnedD; zpg6r4Z6_gka8Q|t16UpgZ8Chb5IRpb5CWl@Jyt(v0!{E!Ju@;TmXc5x z$d0zrk>|;MFrrP9;ZjM&M$r>zF|!nR(?=|aG%$#8E%hLJ1#AJSW$mL>Dl>DVB%#x> z3}KLS){&CfN;cE=t>QYarE&v^My_u|Ad=h<`6EHAdyL@2k`@vV48gA ze5lDIrkD(3P?$q@suKc^eGh?}?*+1!#Ce4gN1PLVGH`3$CS|LL1LbBATc8Ge0)L>B z3A7n-iX&wjfeZl{30*)$L{u-%X%YFB24pJ(nP9ZG2XD+XkT>RIg~253lO@lBAQ^~) zwDTtRGg~LoCl1yyKPkvRgNClp z5tIMn5eC#-$JO zm!Ad)IazJAhl`K*gd2KZZ$KMW00f2!pZ-*e=7$M4;CAmACTnOBNcL{8_7W*Ur_Koq z&){w5#ueN~EDGGHp7xFzf~22?E{Gvy0?cDTwws1A!netdW1*?3feE!2hxVetK{3IV zxtp=puJ-b#!{B_)SgFFfn^c8Ox5G#|zFx0H6Sg!ypL&pknTog`%v5BJsyj zXA7~53vP6U_`CiOS<%KF7aZjNX=oVVh9$?I~L8EfQm6O&tGd^C2@Y=mty}$!(vdS z5c8~xJxmhon9s8qA}?WOw=pNGHDZZq7FBE|>9uXXYpZ6(suDj|H|a zhx56F)J>ytS@$VWQDdtE)ldWJJ)1-`(9%CS7{@&r9zR&ew8Ee~{nIMB#y~9}xQk6Z zBoP3B0B0~FF(GxC7;qVi3|mPBCQ)n47j*~VbShengqIPRfO!ZohW2t^U#JNlnu+d3 zL}O@m0-%Rb{h6qT0B(R@C=sKf$=3OsmXeZ-3hSktX_n_Tb$EG~%clihT^UQ)cEC)p zDlp8m?9?Ffy9n8+a8Nxz3zc#B5t4v>Iy{Rq;eI`I1`T+c{udMyK@tCN$_U1Iexv=0 zIIwHOa03~vhc}^&5yD~{WSp|y67Y6^yO)jd7IrgH7s8DLZXj^OsABiW&0A|~A+o0? zB6#}NIiC}DFHDS?zz86A(O_gp=!O{zxf%@zD1^#9Z$?A-b-C<}&II zgQARc6()li^5x!d%jPmosF|)8;YL-*GA@eMOz1MJV&GfuKf z(&~vMm*M3AxipvtKKPkb3gZbi)@1)9DejFMt~cp|)h6zE91T7X6+l_fBGl6u4kf ztdL^SThvH|O6Psvaj+N zi68mZ@#yF7-q5le)pJ@8w;Q(g(yTT)?dcjbuSv6h#5<4yUl}J@c(Pew0e%_>O*xZC zU$TOkzbUWJtbfZVJW(-OmfuMO<8-c%f1hXRq&+FvP>`geaFPjDON|R}T)ZsEy z?eXOA{W3BD@k!Z0fyzJ)1eMI7!h!||X7Z6qQ!-8aEMCNzQR7CA9X);o8B*j(k|i6*wE_LOaqf0wEESdSg8jUjy-7BAy{GsK@sy>HLfzLZpB_5t4ttVfneY2C5X@< zv9Dsm4m0alS=YRp0yCTnc%U-Je-|nS%Q$Uhfm8!K>}<8FW6qNi78VScF=Jw2Q>O-7 z(Cy^dVb!{(N)@hH$YDu$-aM7GL8-X~e}!vRAY$c{3;XrFHMncU$yy&L%@*ukWrWcJ zevb8QWyrS`ww>J9JM`-VvqyJNFnMC@+~0bxn*S_r*OWg8ZYUww3d$`p!SahwGWP$j zLT{n=2u#S0Ofn%R6Q?f3a3y7Ks0XGSEX*)O5l0+Jh8svkF+~+uWU)mTKdR>jmtLf? z#*s2{1GIq%Lr^Ztcta32>-w6@FzeFVFS5bTnytLk{t7O@Bd>zaHO6E!Ei~60yK6fi z(?Q+VLbJE67?V#ifeM1Fs@ZHSlRo_bYfwJ}<5~|P=02&j&B_F0 z4mRfuoXxc$2~}{Qy3*TfB8QkOj7z%g+zLG9cr33@BRd_At-RWkO0q`1LTf+BQ1jDG z;0}B&tie7@EKx!^J&m;F_DYkvzwRoiEaUc~k4*)m1JXSrG8smbVQi$frd;(uMTBay*6w;&Fs&N0bUDcurHxg!2J6hR*|2Oc$f}s! zQOX_nycbNq0>-MkR{hiQx+aT!>r8#~q;?#8M-ds+9I1yEKv9S6R7nF$^TD>e_g?Z!+f6#tgiaH$U1=-xI#~1k4z>318qQOzuS%3|?WSx~ zEg~b%3&^H3mEUzLGk}T|oze%o$^1r9Mcdr5B88U-vMnMLsfgsl^*C&4OM@R2$_-=y zLK2qHgdg0{GETU{kF<+@V4{fEa0N8u5X&^+J4m7i#;bwFO=DT{4x9k9yw~jTEa? zQgV5odpM>ns9_0pv>FZ1(ssbBNd`4+ijEK0WH8_XCx&^^44Ok9CSe#HD`h#7!_Cr`x5QCVn|gWCBx|&n#v$scFn;R@0f+ z4CXZ5AWdtAvzXhg<}iVYO<*=NncC!LGvz5xVa_w1^X%q2eW^`po)ez#oMu1Una+W( zQ=Zu@=Qh_#O@LODpT`8KFC_}mW%^Q~>Le&e;fV$@pdtU87~pmGSj5&RA@d! z`psV!beqHksYaJc(tKVMocDx^I>AXyfqvAO{e)&hp$SiJveTjh#Su8idCqYrbg4CE zCP7EKQlQebr4|)xOP?y!V;(bF)8ZwGIQWuXhSjY5_|{p~x>mNrBq{o6>jx_|p-f;R z4R&=y8lH+HNxZ?MeMJL~CgFy`>NOJ{#cN>m+RVH9^|0!^t6mFB*u^^bmxU!J8YG0+ zbSieRkL76^iaA)vBKEFO>8m92O4&5v=&z%_Y+gYdSqbU&4S*G9U-5cd$6B_Uww>%@ zuSr_G3U{%AeJy1{OIW|+^|P;?gkO0pp?e~enthsGtYoLD*vDdbx`st)bTU_+&wXeM8EMPhp9$Wt8@`&sxH4r!^#AZsEQ`!S6D`@vPiE^_p&YpiRe2c1 zkXDwhoaG^ZwaR-V#+SF;Wle~gLS*i;nZpcbFr%5v(n>R%u^eYHtGUZ^-rJS&EM_vSj*Icf2 znfu)8#Z0*<(I~4)vgPa*=7ieU#&!u^neD_;h)@!_b)GRuW{N~h&Y!?DrM+F{Pf$9` z>)soz-EC<$FXqcbrU<;}YVSRREX(C?HUF-`?e8J8SrguVGpx-Fa7JSpT8O4dg$O?H zfbVVG?-n?r8UAlX?`<*B+IYclFp0y4K@9VL%bvf@?m2TA6zK-}yw9y=iR7EjA*VQE z8g6lbC*#}|x;VZM-kN21nczp?+r;zzWqxN|;Rhfm1kn# z@}iI4?t}q+%qent(LY}9rB^fU8SNhfMW^8gV&FUm%l-Pghu+Y_=eXeUU3=Yod+Jwi z{^Ct9ed>e0@R)x*O6^Q{JJi;XmKzfi7CWOK$lnFGLgp0@o9QZvYlfo@DA$@St z!I9`cF1!$H6T>oewh1)DG!(;*Qie&0ffx{jAXLLSWE^FX0l0bz!J)&WNV`4sLtbLR zKNLh2ya6uwmQ0w0Fc`!|G?zdWi7;fuo*=_Wq(l%Z!b-%%2owY0vj|Oi!b}9k7D);f zdI?wxMVsiuQ&h!u`NCCnMYY=kG>zZUM1K66Ww1ek#0X?G$b{?` zFVsSX>_s#zi3&u=R5VC>u*Zv_hk`6eTy)5eWRYPsK#%OjW!Oh)yU1zWMT69bWgx|k z7>LNx2W8O6Z2Lu#v`H4xN3*-hSwzT))X8pa#)OQ?zmds|$jHc{hnD2FXxz!BbPAJm z%3S2gkAOsQ{6=Gp$;p^WkVs04utt~gSca9f$djxGXvhd<;LCle2eQ0~z?7V?T)~oT%fvK_ObAKEtVE{&^oXkL#;feb zu+#`;s0WRNtI1e~8Cl768A^+Y$*oMBl_bqGL`=t2&DF%mvm6Oi+(;$l#br3k%Ph)$ zsLL6#NEedtQxT!!Jq z&WjLC?##%Qbdl@)n~u;IJBjo>Lkxw`8nw|%tU)7WEXTR5PqvK7bXif1 zoRQAVh`?OXqiE5Q*iDzPPO&V?5K`8BlOKGIf5Ij+i*v%5$hiJ&l-fUJOJp%@lN1 zZuQTT*w4!xQ`|hx7tL9aPzipWiBLts>%7z=b&1|QPJ10lo2ApU8(5ZYN@b8ms0BfP z)rf+<#*|dS-pp8v70q5fNFZ&@N0q>1Mb;Nd$__D&yA(}gCCI*H(w0ohf=o)4uv=Ia zKs=>d)yxDgVAs8UyKyYVpTtHUO+mq|+kvpbqRm&ZEJ}2}2qKNgdH_)tLBW`e)O49! zi)hwc1+P4Kv{>;k{^vCI4&7`-pwsRNBz&QZOQzsRTa!p z|5eR%G-2mWS&_J2n1!-!H3>6)*a%iy@kLq`q)m;i%Qtn3rtM%(4M}LgTi|V04bD|g zg;O8KLDx-TeALIkZDJ_P-<1fuU}@A^wd!5t{9XMtjaZdYQmIULOZCo^s z)4$o!G*x1wfZSLWW2Gcr{9I7^U0g0?;yg~rVU$!)#x~X^SSapA*u5pw7~RthUNkP= z_*gks%A{RTq$T4y zB;{v@M-;YaY=bsaW(j9T$q^z#(yU=HX4;dT*wD03?*-m1=85>t*^^b*z5Go?^7KsN|A#8@|Sni0d z<_MZ5%5SE`5+>@gChHzf=2z-pXUx`hVQ61g;>6|I%LGfBw#b@oiba<5Z^x?QB5xf2fsis@GrQb;A zT+sH`w#L!3KJ3#*?PKJSm!)QsCYQ)oAxO1UnXFr`JW?M<c)j$@ST?#LI&8TC?P&B1i zI~DAM#8+^vX`c>8FEnoVhH#HYZlrcbr7oA3JX$AZ(*HKv(=2DUO%Vcz)88ayaK_CsCFskR zRgd`VYh3RLhjGIeTnhB#Zd_||`BBVPV!J+XZhdg4Xwl9D&H$xOunuOehUJ8;2f~i= zCQsoB|8ZBea2NS*jJV5mhS;uDXpp#X7BSS5rQ8A+*@h)gI;H6@7swQH@-;v0>Sf`y zUd9_AiYmuxqFU5SoOWo9C}MWyW1(PhjqOf?Ci0=xO^M~s2)r$fna70Fw$0&m=kgtH^%&@LXg5Y6l=rbT?UjH{ za-{DNX;IB}UlkowMy*{rPGDi52}Q5zXy_2#hFBvVT~w#rq1DV|B?%BU_-jP&dZ$EA z)cC?7$!ULfRh;rUk6&8mPR;D`@Ref@_jGku;6I00+6-+2ylj|HOjqxCRUE;bCmc5q zX<9EvIll>U=XI|w^;gzcSvGPKVcv~!WzlYT8}!c-bytjDPG;x%Ih^LKYA+W^FUQ8N z&XVC;EALFlW_`j^^liXA<|8Shn-M|!LHa=$fH9(c7a|jh{uFpk$=KFEMd5+k6 zpmc3Fwe65S+L@$VGB--lz08XTcZ^W=hX!4TXWs%ns({8~)^>_>F& zq_%eQOwaEQRYhiZiay#?xA`8<&4?#Y)(>_5o@14L-p~idq2_&VNs4&)mKqmo9>i;P zhuggDY`D$Na{t^oZu}$n#d7WtId17ncX?n3*eU+~Rn)ZZ=N2dj$N?5&rnck**4bRX z+`xqEM#f9Prk2L!V3v+$W!~P|ZsNcPe?ffK{RfDB0tXT-Xz(DygbEWXyHRhU!W)_( zQmkn4BF2mwH*)Ogk)Vtl1eMW?W>B9pll%15Q&y1PGJz~rCTwYvUP+nCmg$Q|Z|BFL z6n`o!>T=-Ef<*@cMW_#^K%)EVp`}x+!Lwz56%t;KF0aI9>5Zrs2xp z_M{kc@@Ii92ToskcxO(>l}%q38y#=q`J9k)Xk8^MoyS?IsEvq>+O|q|33cw z`p?QvzW+af0Rpy680uZrmw*Xoq?mC8-Qe3u%`r9*O5-U8O=7IA#}j7lMfDd^<9SF= zb>FqLm|_J%!gbjj{_Xf9kU`3}{~bnQ_!wl2 zE-JKzNEs3YRaDgx6wP8N8Kl~GcvZOKN{(sBrDHj@Xw-xhri9dZHX^xZg7vZaCY*7~ zIVYWU>PF8E23q8%odKd`RB1QJaFB~idim0skf{k+c1Wd|9->iMxRX8wop>3PeQFw? zY?W^MDX5`}Ix4AbDuV-J8{u$dsqlT4R18fTWRrDP(PLP5Pd3PvR2PXS9(9R=2BVmI z(gdcNtttx}pUXP?EVR)|JFQpE9_SaffswQfO^>ZtSWGd5;Z%y5GUXDwdZs1pONfrg zRCwx{+g?k=vZrpg`SKS7k^1`kFTeo{yryg&AvZ8vxGI_%l=Wnnqvbw0Ksf7}>MG`~ zTjs8`8hP+qC1rHbG^!}1BWbuR!YOZ5Po65f{4&fj%lsG1_(Fu>%rx3;5KkN{qi~oy z#q`u^u_iFoyPS&;91SHADq-{WaKOi#;}IF)T_E4#SY` z;eIOB~w{`j0V|MRso z*M>3$-QJX?=H1*j$|kE*XJ_2lhz2%gd7>B#J{5+LGcFs-uFF04QB*I5Vy!@GvcQ zsiQBIrf}SjKKNSmM^X7%qLw@M^xNRB&u#aO;@V9@it!o-UyHiD9eHufX}t_^%7JRu5G$UVtzM0{Zb)>FKdrAO^e zgjIW5rv4-rqjd#c7m-@kCTKF5EMsUjT+iG(qng~QFlPpAA{3oSxhPU`hDaO9DI|Hj`LZ##9vX*i1vf3RYC&MH26LidJyE9Cl(yMVFaykABpb z7%=$9KeA9Gef-qOE@Bk%cb8CE6>mE2PK%Jx> z>&xR$LW#sdhRuqse5JpLaRY3@a*zN2JK*j{1(QUc#A=y>2@YW-L7-j6gM<@ZMDFGh zzzxn(54j0zUUkZYbn848qS7O4nX^J_vzyO~K`y;X!e|^aAucSHQF0d|Z_xu9+j)!~ zgJqI>Au5Sii2)8U0>qO*tbL*!NM;-b&0yJUG0a5B5yhD@*{D*X4hyvT(ne%sm!+IiZ`}NimGIJU)KsUSVApIhVnWhZgVTM zoJzO4z!8HOtn2?e>KbIIqB>qkrzT9r>=TlC$)swFLQj!SX1Om>iBaRKUH_qUzV`i1 zCU$#Y#Lf01t6Em`Qdyc1g63&Jaaug-g2oN~HB8+yj{WxO)T-{+dfcrrhVjy$43EvR zd-5r%jH2H8J#0I_TuorSP6wpuitpn~c?4g=)25amQS+2M_)Gcv785Hfj1 z$nQX!=MpJ`dD51?wBxQk5bypOUyTG$VjdmW!I+6P=CY$r3xjFd@@v(uel_jv|9qS* z`c~9><5)G{JCNs~NobK;DXd-Rzxc#6F7b^=yyF(vxW_Th@sE#O zqWdKF6QzQmH!kImLf2^q>)(F+&p-aiA3pOH-!Qbp02W|7 zBm)2fAo?lb00N%?G9Uyx-~viu0}|i_T3`h}AO%Js26CVVcHjnvk{}3<;0KoA1)5+6 zo}db*APain3c6qmiXaNYAPvgk3))}|lHd*MU=Ge84tgK~N# z5C1@$7fG5y=}9}q3KQX;?AV6(gpg6ownWz_ z2^Y|4%%YtMPI$%$Ap~DZV)m3?S>atMUgA7pqA?aDG9IHcCL=R0qcb)mG(Mv=Mk6&& zqcv6|HeRDPW+OLlqc?UVIDVrzh9fy%BQcI6I+mk4rXxG9qdT@EJienm#v?t>qdmr> zBxJ%cWC9Xm#2_vN7RpJX6_$Io%MCe-8!gq0X~^EFmpiE!SO}6Y$_`oWo+J<>DrTfc zZX`!;#8mxCb+$IO^g9F+W+hjC zrB{Y!SB@oElBHOdC0d@PTAC$Wre#~MrCY`&T+ZcN)}>sIr6gcNO43?T*xl)giunvz zgS}CC5m;+MPy+$lfsBh!){eu8RWe-UN0uaIR;Fc+BvnpkR1&2ZPNrveCTL1!XmX}# zmL_SQCPt#>W~L@jTv>^UP1I^W6n-MMv+jGCSBeod5))f+U0qc zCwi`@dbVeJz9)OaCwYS5F`gr54r6^nrGDloe{N=f;-`NGD1Z*=eHN&F9_W4wsDUOZ zfg5(pJk`gJC7N?UA zqap;r8yG1&P(c7B!#wN)0Q`WGwu3xK!X$*IS8gFYECK)kKmcIE8?J*2sA)3LLI7mK z93U;-wtLziAApZ2MrnxQcXifJY&gNar|MgL;N4X7kWO;MChg^|^eEe_Z!9>mDj%t*KYLk9ysgf$Hwq`8=z#9ywJWv4w(5b0zA(y&= zYN{qXXhE9xDh|{qutMcA62hE*VX$TFXG*&CN zM(eR!YqoalwpQ!6T5Gs=>$p-YxI(MAax1y2tGRyUi0B$$#^ z5_OP;X<4f4>{(e^qO)!&!fL3(ekg{5Xv01%heB+^GHk?F?8G{(#V+iF%IO=h1F=d1 z7yy6+3&I2!~00L;~Je+Boq5;V+tqL3fCQtzYpg}Uo10nRP&dMpRwyM-(sg`o=(t<74 z20$37fX*&0uZk?m;;hKdLpki}(xPeAJ}sB}K&uw(ZEEI6@@;Oerr+jj-x}-R4({F( zuHY7~;wCQRwkF^ruHzbRYzA)RF78&IWG0ko0qIxS@#Ufpg`T7cku}&_oKzJGEWO#Q zz6gT=gaSmcLjc5qJ}5#32*faiEC3*YCg_zu@PeAc!AL9ur;2K-7VoMO@9~~0@-DCP zHgBz}>hiW~m1b?PzAfBVF8~Mt0{^FZfa~0N_9})IzbwZJHiH)Y56If^V$a?>z9Ym&R)K>Z|~Z zZ1AZ-_tI~#DzMwyECRA7fIjY4rlwVbFb9jLX@+osdhl0@FbbP+2TLvslkjQ2@CTo; z3y<&&w{QvDa0AgM0u1`H)a&O8Kw>kh;& zw5~u1!wLYeMd$++7=b|$LysCS^FA*bJ1_L2@fn+O^0ILm`=~siX(q74l&UZOwlA?( zshh5>FaQAfaw+&;sRJnO7XMBuo$f-hwga)!>8@(;mRc{=s;_HusU$A~l|pT?wrck- zu-r8ngo?Z=oeuDVH8VGSIQ!cJOFw<_YgI zedcN}=P=~*vMvj=FAwuCJ8m$?a5D4oGOO@0N3$^-^D-_1N+!c{8cd;X%4&_2q2!Qu zdPPX2)e~o$6K4xM3;;9`0uJ;n7wZF>$}APbGZ4(f3RJN`1Vii61MdC+PwWB$gn^p= zKs4wB^$vhlL?$z5EX7`|#A@tAH#9^uG{!nKMMrc+Pqagmav`{?+LG@)$STRUgC*0c zIZ$x;>Mb#{gCx*xmH);q0#m`rt}pn;t(U$mBr_ub)2bkkZLzv(+Ai`I?sTlq^a3mJ z0e|!`eDX)~0x$F|$-?v&o^;obLuRX`XO85Z>gmENDZX2hu z8>{hauQ6=bHf`VbY|CjHeDc^P?XIRR*1oTvdMVdpBrPDoMOrlgS7|C2<0^-*A9JA~ zKW+Fnt05O7{{PxB+HPUo=Di2^Z4DE3CYBD82!d6@?i=S6Sa7GfukUc*c}ud@XR#A)Y46`=M$$O9Gc11)gC zEaVY5X(+~n^8#HJu#Td)>8{_tp&$g2{Ig~58l)Eu03$;85EhoFd1cNkH zKe8RGtS}($EvxJyNB06pH_U?c-A1LHioy1tES<)!`o^(G-!THRgY_;lCR%mYzAF9} zE75K_$w;R6^?IqBB0$>$0Ip7O_fqoJlJec|xy&xEf@5oGR{Dc`>!tIsrIT>rVmg3# zx>kNVr)RpTYr3Z+E~u}1sfT(OMnWdwbx{ATcz2z1kidj0rQL4aKwAA}iaSxI=FDb4 z_G;HS70&|;R53kNL1r%k5bU^QKs$^x`z~=s}`5a&OmA-ETYw$b-1DaE<&H8`?k8hdxFVv22(b{(x?ke_5GCOR+1HY`8 z&x4*yvXx)Ao~CI7$m)AzZB*vj76`y6Ot5#)t(q3?l?H$qtiz;xto1s60E{Vj z^ZQletV(}(JUi`Gzwb#8GADO!B2%(3fGwED^VA~x=O?4duBlZ!z5@_G{AT{smO}t+ z@^V{lt5~mf)SiHLGXeAoI%>}13L%+C{+8?sCH@>J3$z1-RFJ& z%YBsVKS0cz*G^zTf&~X6Oo;#Rph1NV1wvftkl{m%6){rOs4(KijvPTs1UYddNr(hR zepH!KpgckV(#&J01wpcrH*tp4xsvC^lO`+vbSShV(S$}(W<07i>BgoimpTPHl zRgqea3iT<_tXX{ujFc%8u}u5Wrd7L^En8(V+Qw~rzm;2U>p$v0p&!6S8O8|`!5VZ3Ggxa^u!7_b|0^HG8 z%fe~|n$QYc0Y@K|AN%Nm$q_BsfN1;ll}4_7tujdm8$OjDHac~xSFvxMUY+}O?bfqz z_bxs>`0w4zqd!01Jo*3i*%w0h?lnGCitej=B9SCgn9=;;04;V_P)#3R00;n$)Lt`fpJBid5CKro zq9=?h)i`dK1di*?2;`_(bFE<{G0;Jh=#Tkc*Bbi-gv1`$0FvFVjFAm#lNoAR^^rV$>fG3 zF@cNanQ5-s=9`;~;W(Umem0pnBX&>d_J~$*Xrqf}59y_qZd%@_-;4Tar=t#(R1otU zv{OqRrPtPlP0c#sg8wbG>#=p}v}}Xt<5z5NyJlN%j@u49ZMfBro9?;SHrs81B$*^d zk{L7qGBCP&PF%=mYRrj# za016M&gqPFv?GV^h&4R!QICBrqaXG5M?nUXkZo*ZzvdLUOeiBqi+PJ+5En&BZi@>y zSk@#lnMq9o$z+@KWRc=Tu?M}YL47Layu5}XIogYqYgA<`=lDu;oianIw8|_8^!C4BS)M_rO?QhDN~+Y$t;tJ!is3OXhm8AVL41(S+Xk*2QZD3oh%Gn(Cm<~ObB zOCppZp6khCz&Ez*8CVoaZp=nNNJ~Q=jsrr#=7a&wUF2l%V%4 zC_E1eP=yM#p8ib64P+n*We_x;6-8)8EsD{L!ZV{B-RLkn3et~;l%yjyX+}$m(vw~^ zr7T^kMpYV9m&z2TGqq_=amvw?xs#`Sd5elN0aUUKm8eBE>QRxJRHZJJsZDk2Q=tmg z6lG#?WwEMRuo~3C`D&|O?|`Mi%7dMf~qQVOt1nIh9345FV6Yj^g0LI^Qo_YwY4q$PzS%*?pC({x#g|+Zm_-n z#a6eyHSTYZYh2$dx45}ou5+7<+vpN^y81&db+2pPCm?|a=FU--^fzV)T=eeIiH{O(u3{pIg}{TpBa)At7HRWAyZ(MLSI`-fVXZXVlM(`iURgh)V zalHl^G{4dKPF9S{TLBg}hG|^jo-KK3PhL)wuN>tpS9!~tVg7QNPm|^&|M(pxLfT<~ zLcEGG9AF}8>$X5c66~ns(UG2Xr7xZ7O?Ud!p&oUqPd)0ANQTg{o^?+;Cz*d+tccs~>uc_5_oZH&`D2djU0`%C^4!?PlP2e~Zfkub zU=i=i#7~^@pmn@18xQ%%mr?SSr~KvbCfZ+iC(KrTOqc)n(oU_br5<56BZR1)`mA*< zQmy~k>s=pv*w0?}wWs~b`d zt)YF*1Y^G9tv`M2FJJr7ue}03pRS<(P%-Z1Uun#*Pth&@R=(T( zElz*^=b!&)+rR(&5C6ic$4ZP{7y%Qq3|M3a=o)GClpzwFY2$$J_w23$*KYR4PxUl# z139n*J@AYoVG@SQ_(-q>dxl$HjN<5pAa-E|GGYE?LJM%99o8WLl3`*Nf(l|HvAE{$ z1|kyHO9n83Qx*XY=w%UnfCp7@1+nIr{14`Q@CpB4E(-Sq@`R~r-VWmoOO|YjmB8gs z%CG*!&j+jU{{-+1?=KCj3=P*%4du@b?XL~x@D1k>Y&J$c+|D98&n&czooH?JO0W8Y z43L~|UsCMw?9cZUaRL{y5gG9j9WfFiaT5DO5~zv~O)wKRQDvsci6H#o z2kvlVuBQ3Q5K>%e7uBy9vu{yAE@B)nSGv!Q zMhwVyEaddc5WVgY1Iv#3#`5BZ8ZWLIG4C3+arL%Q2~W`*?XV8F4-DT%AQnNZz(fIu zZ!d_21c}b*HV^EWa2}ly8V@lG59tH-aUcKru^*9c5vJ-7H*p{d(pa2BQZNMoRDf9K zp%xBc5i*4q{Qw8F003}AAa;QTB4Gu>AX6A}9xz1(Zh#7WpdEsN1=cJfZ=m{kp(0h_ z05TyJ6|z8tfeHX%1&pvAcHscDKm|%NAy>c+NP-=<;1-X99c17I1;QL~Ko|@`y?$U9 za3Bm~zzA#M2iT!1)9WK)@w^s625cg)7-p0PudxWPEXPvt&gkMmg%!Jynbyy5q_F1H za^cPj2g8qOs^$}M=`H=TF9EY$0y9wtlK>6V4iA%x5|a+QqYPaSP%JT>N@ks4WF4^% z%mk4f4epC*j;~NJG2@XQ$0#&Ob2R@=Q#4VtG*k05RkJl&^EFX(S4ykr3bHnB(?>cn zEOsFV)@&VAAPwZ978X(_elrzaupy7L9994m7ShlNp+J-~6B^Pd=b;>GB0AZj5FB98 zs6aZ|fe^rQ9ta^OFhx4oVFiFwI9G8P&Qm&DP&y+77L9Nsk7{WL$T2_ zwMLAts36#(CItiys_%<-VF&-MNEpO`i>k;-Ig|Xb6kbTn^KP?Cy%aBofdzI3SOTsI zpK~5`K>)&%7Qm7a9AF?^!W2W17gPW~cfkf1!2t+D9(=+B?K)fU@j}#;Uz$mM<9oRty-c(z| zl}gWbUau5FCBa4o@=LY#)k}pz1<>R%egZ7r=5+1!5*d()9LDBiMl#76AvufKcso z9(W-<31UCl0SA=QWEr(UmeN3pmF(z_?XnQ#j8+~iaI!K^L-G&DvQgv^$?zmJZ@5)N zafK~g1qN}6F9*wO#Xu0RGArSe7Yr1B4-_d6v~QMAM)PqQ@0Wa!k*!)*E`M}LBUH5d zib%T=nk1C($o5eZu31wx@}d?T7jN=HJCI?X`^{`7iHu?5q(3XE_!tM5H!q6%*D8rgvhHl=tAIa5h>%|;a~lNlppx+H}m zW@#E;O6*tph*yyns97~e=_7%cm|(1sg;g0Y?J<h$QtZ@YZfq9fWVh{8AFAigb?*jBL|I_qhS@Xh?QcyYRT+uE;*CF^+O)+8} z0KgOlf)H%5U2V{Ef%Z;U5s3#RdC~bLEV*6xxW__wuSGX$F1xZvw;=8ntM>V`HE~I{ zSTVX7BvckB6#*=3fg?RyI#clmdXi^B@nIttP5nB2MbbAw(t9IU1$fgOJrbocr9g1| zI2+QhA6W%dAOJ#%n~PE+c3KA3D-Brk25>-r#g#vk@{t*m9T>qYEjg88mF&Gupm53X#s}^|AHNubRIf-W90!hK-OK?tXxtvIGQC1H4n>sYF`$$OHJXG4;F}eDy z<$SECy3SJ&SFKzv(J~>_dRqOE1oeRtIAzJ15$`TkfbDx#p)gO_;c`p7B2BTIQ!<>p z*(S^xp6^tl`E+n8B0$CjXiZ#&;WfbNS-?wuUFTsexRl3PJ?ljKF-qG+4}unWdm#du z0{s>?TX{oYFO7(7DI9qg6S1y)=bi5uR6!HC{$?(&Brnm=TEZ!tS#6BUezD6vSXIq> zgIi1*N!cv(_S;3h&=nLy$z9xMw5{k0-ItJ;wLL=4F#!u_0srC#R?7kJ_Ey9N&QbYQ zqk6rc+;eT1T)i11Z`eDj;Kf6|I%OA#H5Wbw7oiQlN(6C`EW6@2TeC5{;!Ag{TD{|i zjvb7{pJA8DLGr~5LXVfOG0Cs0nbm?H6z)2L7pPJOYPiWEc!J|uP}sJZwh-l>37QS7 z%`>proAAEzu-UD)6&H`+qfF0*k=SjV)c@Pmr~cI4+^D;~s0ku6iznad8PUaKldDik0pO+KG$FZC1(X=D-*qM~H@N`-ab=IftS?*j zn{9Rs&N=ylQ%U8Lo(bg+#n3#zQ{|a~-dVxD+{fMA4L#j~|E|=%?1f+TX*EM;t|1Q4 zEZo|sV&ItlVkPa?=Jy**otPkWuIn9t{ks$e z@iq(%W!ibZ``F#rF_8Ebu9ep^$gH}Q0b-uOcJl}p6gUu}!h{AF|0*O1@!>&)4Hs6V zIMHFmh7lbmlqk`lNR1#be#F@Eq{@sLQ?f(}lH^2=EGwF#y zwYSyBE+g|je36~r%93+R_xYIk_2`Mg7p59?bn)ls(+@_B|9r3c_`lgtc6``7W&GUX z1NOh(f0p@mUx5M|7+`|=A(q*K|Mdk?BwcY8n=&`lv)T<_<;G!$az(R_Lrk9R7e_92ucFGvB!s;Q3JT5{Kk>DdEBJoE)3bK!YETkU^ zc}Pj@5s`abEaLPnQXaT8|7NS%FsYTK*xC;%ggAgh_8763p%Z6S;bMm-m`1-&2&q>5m~3ct6rl9j_#UPYr6-z6Eg zVWn4f8x5P%^&Ve|XRU>i9NOfww&qNSiM-0<5$6V2(fw7haJyUF28-Cj;x(d&ZJL8f zW139N(KX750fzrn6NWd2b4k#M#M0Q>x4y;oPgK-e7eh!t^=xPc9M07H%K|tr%ow5=7rTCzSzRFh@aNAuwn%X#tMW1(Tov71u=);L!(xAxBedNLDZg*VVj@mHs6v5Lby zTQL99R*VX7xNRAZMs)eu$%ctUF9xQG3SG!>yCpPjpus?qO-03J+jkV<#b?T_5oBh} z_gx{USCYdxbmls;%F`O;VVy)PlI^(1e-8AZdtA5(SqA`)PJsh@4->-hgFNfGPAvdW zOT@#IgLmPBb|~ToQg?{1=W(T?vml@Ptr8}vkm}g#EWgn$881To-aWm2JBz4qW)J+o zgplxqHCzs3Mx|(DPh!D6Q$!%`iuXyc`zsV-sh{AbvFfVh83~u8vFLD1_ zM_#h=$#eXXC*N<#Z@!Vwn7M+pBuLuyIUTD^mpTf)Hh6*Y%m;4cm{03Fwerr&oe%zf zk-5pr=ku%ey;mdNUW;b7jAJAISPHsv`~2^}|2LZqUUd_Mpj?L{b?l%5-IF^iusl@P zTC+4`;zG+mwo_(S0R%hW>(DRP$xt=UR-_9o9mTuE&P_v1S^1llJG0 zwx^JKW|K)aHt2T{LBcvO36)VPmAJ7R-Eo7~BMFLdCJ+Hoq^2eO^j;8jD7COWywD{8 zKv7(BJz-aB+H*3xV}S{{0tS>2B(oF;6(N7R%kXh#=LcCT^av}+tRdeTo zC|a;bT}6~Ry8$tht1hm zLjCR2a2ExDi_L7Rtaf_pa@%c#abA7 zB7VURGEjE)*M~9pdK@|{4*Fl08GL8+B8ym@DF$=lp`vMYf6!-?EqY@TX>#s4lO$=0 zi6~}DhJKcVoIT2;KYA?Bhh)K~ok`}CG#L?+APg868&vdZ;x48a5t!TI(5oX{d?&p@@!k ze~BrkfI6IQ#$%xPk@JbFA-R#O3UMbVZ2hT3^eGTd8K;4`ajJ0x21=)65hWkSZ*zkY z#6|_(b2!0y5V-Sc35W(-q6-M{SvN-!!Vo_;G+GfJ zbXAY@p03J!&uMSLCaSfUh5l)NpO|Ny`mei*a`)n*-dcVsXI9Gxq{L^WBRixZ8>H1@ zkR^$uTy%$|Xnl5ZWpw0{@QO!{1FZx*5WX2UqpuZW0cXOp`1UgL#r{oE*V@tLkXQz|soD=tmM>(l% z2017auq=yM>WZU0`C?RirUEN`94n-~Ij>(>hpw810Y`tHim;q`Hb0i8GKsIQm75|5 zxsbSDeMPzP8K$wfXG7YFZVDAjl}$4XB{Banh%XzZAV-iVAq-m3JqI9C2k?pkphC6a zy3(UUX0kM4*E|5Q0wn`bIKXHDU{cogn8AYs*#krgSggUi1^uKD?}HHAhy&&oCmsbq zG2mS5BL-nGy(S|G{L{Tw&AL&i3&jL%YG9}abt>w>Pxj1>~g}{ zz!E&Jx@V9c48qJQ!Z;VgBs{{oHy_3dqdu`ZMAD=zsT$fxw)DUVFk@-`m{7E^F^AHD zI*cU(GIO;83;Dv%LSPJs&{}Inb=hK(s(4P%cwy7@897fIjM@QCf@) zg;1^83PigDnAe(#S&OFssdI6oR*(y+>wA$>Tb~}<4lN9x^oz%N+)Y(!qkUSR()W9G z$i_hFDl!PHB>9aC`>z%$vDk^YB*(Uq`nD^3wJQ6DyXT+xrdVOR!e9!fikey)xiyvh zZ(O*zri{v^tjd?T%5K)mfvc}^m~u^~R8RRDm33LEky-M4o}ai20+l_bLX61}%t*C6 z5Cki-_>9rpbv(=`$eMtSrgc8>%g8_lECY7c#b31$Cpdr!+o8pJfl=FWKEqIVubE%; zV=~>yo(lPmDzdHenW`fi$x8nzs>+F!_e!O+3a6y2$Nvn_;KHEvdRi9Cg#+ap%9Wh2gRG{#XF2d3$LG6|er&ZBt)}*=H%BrWT zsw~}dDILLze51YLvM&s!auKs*i$`ykZHlA<*+pp@7R$lwRqtTTi_Y7dnR zkw%2q z+tNse#;8?$;2N-e`X_(*X8{e^fo(9gV=qJ5&W|jq%V;ieEs_r#s}skEi|oI8y0-Gy zzYt2&j*O)Ii@*Abo6{_5xe{8Wj-9s`tICKvv7Jg-a7c&N3EBtQ+Ry2mii^3J>eCBB zt71!8H$V^n94Eg~qG5q@bZWvS4!{eT@XJyRT~O_p|0Phn=oG}J1&U%RNlkz~rB6`Z z8}6hFEMRKj3&#eL)!Wlii=wUJe9ni0q4e{bdFP%D+qPdTscK`WMFyOI4ABlsrlXa9 zQRdHr4d4M@8_EzOp{RB~9ljMC(GPt(EB$_ti?|~FedE_TkiF3Pe4nDs$_UKa?%N}9 zE32Bj!456K8f#j^XEht1##maiJ9@Ik=i(yU;)gwwm0G3zO+^D*ps^7RJ8fm4>#jY$ z61qkZK}*a6RpiT~#2RMQ&!|18R|pLN#t5MZGN9BT$PNzGiwp6@yAS|M-PJ#$Cln?S z*}6Udw`L3Obw0>In1`|_z;-AxnArMkwM%KS3SOc6E4lV5+ZPVW1tB4TDBy!m=(pi8 zyaY>oluNpl=%}63m-f`m)IWJYUP+Vm?TN(g+wSeqUhnA6@9Ey}_`dJ{F7N=a?*;$w`Ofd-?(U&v>Ona15#}Fb?TiAhJXgId9ClWQH*e{&Bxe)%;FTD zs-U{9e7M*oJI9e7f01$1g)a7E{~Cn-=ar3y5l)j4`k?^~Tk*{HH!7TQOMkS-z?Z$o zHJ#XSulI4?*+ps?P8!2N zzN<7$6#_)89u64%$PCa z#fT-#2os}nV#tjpC*E>nGe@2+W%~3u^Qcdtq&J#gty6VOqaq!SmOcAt$exliWg?cz z>~G+~g%2lQ-1u?i$(1i>-rPA=Ws5=|U0od~ZKSM8j}&@3HFuQQU-NWL-Me|yMthzo zZ+rb|?Ki= z3SpZNL;4zo5W$n~11iLh5{e`fO_I^%xO#3-Cb$@0d~QY>#iB<1o9MGHJ17P0vO)d-ygZSqu4**%Of=I} zb4@nebW=EDB-xKc2K!S_K_@jV4Za6oV~|7s z8HP9Cj8or+%mi;C3G@LVDhw^LDdS64l^Oo)-)dQ{dTG`dKR9S-tdgz~v)-%XhA8c4C+4v>%IDKOOgpnA7tDdSJ9NBZ3$d!%k zaLNcnMjFISMK(K6rSZM8Kmak77hk zM)Ui+VOF`dcw>y){+eT)wO$)adwniSQqE@`5A&Bnef!dyEhJlXiusGM(2?8CapDgL zt$SWtL2GqG)jd6QyIgzk(Dy$xuQO)1wUY#srdAX-2H}ncoM3&@vVCMKGv{dO>th-Q z`#i0u7h9JPy!_tMWqw#oEFFB@^h!?-{rz)^Vg!@ksDA!_{PWj;|KgNkgk$v?>RoB8 z-IsLtrIRXuX45X1^^eAq)8_M@wS2~24 z3uL$XSOeV?Ia_5=afAb*;YO#S%Grx)k8)i0cF4gTf{!4>P$3UJq_K}|uY@IA;Yk?r zD}NcMMKbzL4D820H!VXF?10$t_`?n=r~n7^h!!&Z;Dfc9h8>bPj0`X_5P2-(4KJz1 z4>-rRo}F%bw?h~Da)z#VEpT%=6q?6|G&gl=N;km5Vj~^-NJ#b#Vd~n207M9qS^yvb z0nj8GE_sx7tN;KGn1>=rxii_+!jkM*0U8oXkXi&F8LZ62mEc$l2Uu%fBiY3m8A8ic z4P+ew$V41Z$*zsCgP3?EV*r#?5L13~fbR4E8A5~+3C7?+541|%Kfe>i#=j}oMprr9lIR=m5(&-EC@Tcz?Gqt)1Ip%;{j$_g=b<% zv+-gOF6Cwrl!om-=v<+9l$pW$l!KlJSp)*&_eWB>^qIPit5aPlJ3>m8mA=hcLU1WT zL6-8EV~yDzE7DeixO5Bz`6p5`Rz6`avmpL#5^f6N)|P@cMCz<0YI5OlB9?^w}cd#U-DSwA{bHevCd%@$atZJw;Z6Bw1~kX0I&i#5QN1pZly)vvVu&s zql{EQBRgEI0x^gXAr^iJJ9w1;kdY#z5Qs^Gk;R~xWYp2flGuV`%*Eath(W*d6}dWSdzV(6oXpYaom%x;0^MK z>xfm-5+K?9f604iJV_RsjuFxWN`?+YZYD;L;dnM-_4`j7n?Q z$C`|&MK`%hgN|~0!wdreFltf%9;DQ7sKo&c8d0_Qc$1B;Wq;qg$#S(c5~2(MSB9Br zVNhYVrM|%mh`EbR9)PLGMKvc?{AXWxau=c`X`}-X-h|s1uh=dF-xsgi$BBKg8@H8`ih7+W~;}CLi6>&RAQV z@_5C?FRE{8Ih5d*{%FF_jog-QS=Tqn;7H%SQ(oMlGEHu;KYcK+pfX&iI8b+Klxgws zW`YWKbm=C8+|MciG^Y+|xUDnsY-0905=KWkFJh3=3;P#IJ}~lOGEoSEtCzmR=r7L2 z`EGvK*B}m6`a{$WaJ5GFrSgb5W0L*T8|1s6VArV|+aZrFprOyvwM>)ho@KSz`vx+I z@kb;P9|ZeCC#|H_w?2=`ifH zzrzrQK-=TyGA)b$Six}~ot2}HW)X!QDySt|WiX6gh_R(AP|_{{5M(emUzP+3Wikjc z#_KN$2d_i#OJO?@47+rU1E~eqE#D(1we;%uy#X3GcEh}?ic>V)TBLx-OwOt{k3*U3 z0Ria2ML-dz@I3-7Km*JlX4#KjKmcL*gE1;KG$JG0+AgreCABbzWg0D9VkNHdCsOjK z2V8_)61iDgfmMRBLdZa3B0*Nlv^TJ>MWWYd7SOH%u1P9nPjKe5wlfFk| zB{ZNxB*`WJQ?aaM+lcTxzjkPcFvtX}%Qw~&h%gv~S-LEivWRuCq!h%2W~!)O+e3vj zH5hEBT57_12qiSSK@n7oSgNHI%rS}(!RXSicABLWjIl|a2Q(BxAwv(#%D2_1E~^8m zX_-TrS}Zhd+R+RSSSQvMf@ILTrORLF=n| zFo$D-C$8|Za?3&-P_m$UMYTx3Su`(;I;ON)sml7NkqS00AcA%%1SMfZ@C&NanSXG(ac->k)DBsGA5tMmi3u0S+rVK;zIN4?>6?iLw>QC|dBh zT8K%4Py~OQNo&~!)ANHLSO;mLz9=Llg5U+u(+xN>gkoI33v03kBO{Wk1-0@gJL0l? z@-iC2tDr%m65%ZSBeNdTBi-p9RNA9n$pouXNwr){w)_`6OPO+Dz@$40Kr@I%H~@Sj z2w*G%i}I|$W0GOQsf!whKh!rI%d?Cs#8N{8UChL%GpoM*tRUpZ%F4hpT!DnKEUq&+ zg1|s&LONc%HC|k_Wo#*hn<+DF4|e#29t!}q@WO+@C1HR~g2*%ETP6|Qh>we<9zm!6 ze)YPW{q1q48b#x%`z60{0C#5a&F8RCw# zBBNXaCZzKLV03_dkO(iVFxW(rdisN7s)EfFPR$_*_S&c8l&R)}FPF+OV^hCO2nOiH zwa=tJRWmjDWCu&CuqZ49_q#=yBBK+mB~|K8+LVwr0xDmLF6ivVmh!j%G_nANP8}oB zr&`dM;Yq}3qr4jlCXok~)5}7jDUeF1EqDWVL(WW)N{d)X0*%gT={J6gF00!obHT64 zD~QdjC!U-o5{K}E21sSN?Ln|@K#HBD7jT~*_Vll=$= zOF}d>$j=*8(oNDNRvV+igr!}?u{@1ch`P30Ys5@*16^d$dc&xJKulU1t%cwPNq{&C zB_(yrx*-$@SS_jOGPUyh&0hT|AKXA+bqRKWR!^${P-B4^{DFjkRqL$Eb_fO=WWbK= z5orCjJyllLx`b-uLs=q(2SEQX9II9!1VYV9 zCUXq{hQcLKv&C4X%7!4cToS4iWU4<ileHmQFfa& zf3peS6o@Lo1V5;NOuE(C@&kwx#DJ9q7h5IQlu!{0&!agAVc>-)b1zmbD)chF8I7)E zVK+#l#me&7LqSr#ywR4D1f%4q^e`y`F_gR_ zRt<|1(@Q^K3t0lA8)%jvNd{VguO@xnf{5LnB_kFQ zFe`SzgcOXxx@@UJ(4>HLTJ7uEj~gu|y|s9?B!}{WUPPyl6Dm?OwU2Yd0FZ=U5Qb$u zF5e`-FC5oy1P@yJ-+9nQicLD3>)C_gg@|ev8hkBajKFpiFW2l@ZtDA&TYzRh>xF(6C?$S?#U8zMz<0u6X zb_3&5K8QKo1uo#Gtu16>Fi~j~29vlOV#u52p@-n$itq&s>1hp~G#5pXmS`~-TClBh z8VK3#HFE(<+|7Yd`q%g)(2Hm=K^|W6Yt(Rh)F5L70lTmW1frO7oF|b#Hd~@Fi$A~8 z=j3g!L46kQ$s74i3MkH5moxXYSNib{&l@`~~>Zkv7D27re zOoXNCEQGl0)<(2AYy(p4 z;Ji2V->Lx?YZuWWqK^x|8%y}H8|;~%ue zZ+vAuM1!It$akLIo%K;rn?sI-$^gVQxYGYV#I=@@q6njez8Ia^q1x(n?oWdFhG>K@ z)4mD6rd%IWl6HintbQ<=LZi~gtdPvliddv5Dvo6U-6|5XgocXJ4T*7EL@LNh0BF9s zqy^?nhH_X&oqSZ&#xCKN%lIG2(My=^fCL~r?z|++t zyh8tcL~WDf8PCRM!%itA#&|XI{hj|W0jMbO3jhbGq(!v0DAQN%@&gB;%?b@9PzyY6 zg3{*((C*t&o{|Sc@TM?;K0fq`atd0Zq|hukD6y zUtROmRl$rRgpmHLgydbyvc#ZqiHCWXhG2#zqf=UiOK$@-uM6 zuwFVTfimsL#lvX7ufYosNs#}9`oXH*GZqg|=z5SI(}<(D5QAl_w-unVguyl%d!tnE zW&sd}@MH%?cnhz-_K)!0#Bf`4P>dQ)2!upuJiSjeaA&jZn9bT0+yWg^-5|C(u;PlF zOqHCO`Jirbpoch-g6^sik9w(REMX;fy(Qa=T^w1&5296XLpQ( zRsKczWGPS+Vh3eVr7Qn@Vst3?O0~i!gvAfwy+`xngW5~PU!*%Ge`UHaRW^tF9(`*m{s`Tqk5U}v>B zaNlwI1=t^K1{#+jg4QwU-+|P%wu=}Wu~Q(0`hBKgfDle7Q6X=5=0^n~qWGGFy(MSi zMzx64V1)m9IGl7KZup>%1i7diJ5`V3+wo^;Y#r<{Mr zppu}03ThOghRR^*piU7QD5HxyDyWPL+BjWuyw!N=q(m2S0;g-8LOg#^^F;{7-!eUCCvci3enn6evOKG!L znp$P5)(-pRe%hVZT(sK)mlZ-GSyW3V)pc5`apMI=ulQa0(_vc$p* zVmsm%w1r6WYylB=`LY`Cf;MZ@{Haxl0a1BPBEAkQ7s1){ULz#C@m4w z*F8IAKUfZy9$%^c-kf6Fu>Bb+wJE6LyGT5@aPewA#tDp2-x7~N+ zowwe5k2jyT5l^}!wfb7QY2cxzXys>G{%SVMh;QAt*cy|ZeprB?D1Mhr6eUUEc?VDg}xA}(A6N%fxD$lO_4`0>5FVSWFmGTR;+~Ga8*1M;qp|76YnIWeCMGD&c>sd z3hM1=!eE~2a@RDH<*t884Am4qw$|UqT}z z8|lbLLh?MrpbzHg=f{o#l7At@)+J*|MK^v>a8Ilx3yTLf4-OHJ`I{cu497}PsgA`sOUzd-u2hG`36;)+?jW`zyyFaM*-9N@ad6|D zT^6}(~`HW=pLnKNsSRR zax1k61HGr7GwKX72&L6#`nWH6{)-AKkP>xxYXdwpw165@mT0n?AXrYr`&QvMuAOsl1&{s5+BMb&R=_gw#s#5xonP@v8 zK4Uo?ntf-gn%yjCI}4vlK8~^)^rSR@SV*OsGNnTWsV0xQRNZZ{pQu!6N{J}Wurbt$ zE*+_mlFC?qMHHupJ6J4%>P0OIu#=hUXHiQC!kh-Nw~-C%XgAngd`^_LD}rn_yOY^$ z#vmHwxxx25TPAu)VvQjkZnSiP0Mo2v1z1xGA#gy;P0b4_ytrCBgscf!d0A4rL|Hb(uR9ZH7|)vY~m9WW;3~s zC|;c@Qqk7`mcv7Y;xx-yQll!-!?U$VTx53+ z(~Y|9A#yWGK<)N&vLIt$Rkwg;T3Z;l+p^A8;BjpVZxMFzf4DQ`>YTjGVg?nuniSpI zVCY(;`Y_6`J-2QtI;0u%GUAD=qi-+^9`JJiGdwr=xVX)erZuig5nN#h6;f9Y6_BW_ zI?+V{bkbEf%;RVVV~2c|5)2t2gAh4jdCaLWn@z1l28dqHe+v=@DV6I7HKCVKvO*%w z*(D+JaD}Y3K-ddOq!!62M<(P^i-k_4+5cJuJHTKtTF|<`grHA34l<8m(6u0Y-MV7u zQQ?U|n;>L2%R`ZzPI^OAsTa;R`^>z`0ynValAk=~>!wg)2xAzS*L>zT@A=Myp7Wyr z{OCiEdC;3a^`&1u>07UQ*R%fhrjNbsV{d!cqn`7%_q^tNzx&kh-uJ@qyzhTM`r_aI z^}}C2@tdFg=i?sw#)m!igWr7RJ3ssX(%*jdhyQ!)AHVz5_dfEk&wcHqAN$PDz4*=F z{`2b|{+MTkFh+cyo4q+RP_2A;s@gD?ADz8G-8_WGr>#Is$OBms;9IoAI{XQol!l@) znj+*tMC`(Qtx!AcLRw*h!N`Llhy=jQgMV$>QSgXH@PyWF0e~qNY_NkO@Ku4WL$jIP zU-3wnfW{a^gk8;pBHUHLKm>$E+b>yEJ3*AdSrnGNQ%2>{a(Ep7L17e1VSAvLBuD}y zTwxYwAr@lc7HXjvb|Dv9p%{{(6`G+IhT#{cVHu9$8Ma{>njsvrp%})Y8rESK1_c^| zp&okS9+n{c%LpnUK?Fl-gq3O3*F?y`&h;EetbYT9Rd4G9_0MC0Tl9VCW@YHc(p1%~XDeUzX)CJY@qZrBMndTZU0q2F6$l z)ML_RR|4i+zNJy}rCYXTRm!DYW@ThP=3mN9VSXlG$|YuoC27_rTdL++8fI&%CTfo5 zW+J6pY9><3=39E^YZ8N9Hc*SkRoCTkvNQ(mQ3 z)@E-825LrSVMZtab_S+wiX(dLpZ-ZzP&NiMpk06YL7NCbSRC4N5Q0uvgrX%w0itK> z(878W1Uq(yTQHg*P$WI2O-2lZJTSsdu!GX=q|`mdQ1}FZy@Y4TgCY>tg#I9r&_WpS z!a%~{Nn(iF<=SX4Lg&l_Arz$FiGeVD=sNVBE`%tx0avv}%uHI=iaCx69Z_7(1JFbs zdGctF0;O*fLt3g?kOqdD4XKd&=8+O9kT#GqoTc}GB`ECWl2U1s!sT~7<&%0zmV)J# z5+)ceCzuK+mTqa8a;YYyC6qpCl!|GXN~vKYX_YRik^%;qdg+;d>79B= zDVhGMmwIXcor>w8ddHkHsa^s`T2`r%0%@Zbsh(=WoZ2a*K5A#lS=az(&YJJ#JuEbX7Yx;G+5GB7_W0#)M7K zhB;`2LKx_R=9~{mTAy8rP!z->P~`C}iJc+ZIuy(<2%E6+8YU1RNd8@Q_*GT#U`obB z-hD&=7<5I9$`EzE*oVOsZsAyRWf+JV2LtV>y8>;{mRxI2DbX5j(IPFAsw!*RX}ON+ zQ8Fs360M_7ZPHS!)DEf9wyC&2t)L2LlO8SDCT(MCY1ww|*(&GRn(d&j<=W z28P+XEp*}~-GXb`b}7^@s-}i*(&8=NhOOEX?%)P)nX2j5mhIm%uAKfYT;65T3g@3T zuF($ZQjV?R7H;N>D&gYopgyPPs_ot$Zt0S4-r7tVMU~JJ2AvfUExcDpN=Z4afJg)b z%;ns{`e)7sap6X2X|+z7HbNFZsLaN2zP4?Q}DBLssWEG4)3Mh=5VVHaILzk z4j*gOmM{UI@Bk}t(dz98v+xY(Ex7(^mXalrIW&sR{ROsba0z3h|Vls&Ared4$O_jE_0Gt3IeBZrB-~nTDeQB%4)Gh0(WsIAQ`xLgQ!Nqz8}l(Eb22OQ zGBa~Cn+N{?nNj`)t?SN%o4iS%=!sWp^8@n9opAF%_KBPric#P|p_nrl+yFTjiaIk& zIo|*}JH-~5GfCWluxSCWfq}^0bF2wlK6f3jO{hQbvp(xHJ_|Ij0rWxVv)ApjKNoaE z?=w=(7-dzP&T7~F8qrMBSV^T-yjOI22aHk5-!#xKK3Ph#5ObLZHygDZP#So;7pnmcrrEZyg>)4IIY7XpBY9x_weO z+Hq9>>-Apqbzl4SUr%9}kcT(@uloEEKjpN|0^H0J*<;~I^KA5$9ri>aQ~m~+M+r5; zLiXXD*7nq2F#~pJi}q-fc4?b71d)+6Uq)36?Pe_2{`^u`Yt*+~*Txkd&L&rC@z!HI zmyy9wDdF3g5n&5S+)rCqjhRqYblYh&cXK=Ub3=D@Uq+auG-ixkttbN>V@7t1hdz`S ze4y8w0H{34D1)rixdr#bsnZYL*!3=xP!$|M@mp2IYr}mQPzN_kK@%>uSZny~cS?7F z3;2K&c!8HRH0z9NmkB!vfOuU7gNxTZr~n+G2^GXanf#21qDn3F?rH4t3TQZkaQJ*< zYD5C!i3$XOFjPS%)WX3Y++Lv74X9P}bPB^F*pDq)Quvj}is*hs=p@9a%8?@C}4(~vw1#Lff1ldAy9aHq{*7}_65dlq~#-+orW%O1%X(E(A~!_?CY<*&w)}2 zQ=ChZBwi5)j@d~9FW}%ffvgSk$PX5qrhf*D-nc)3M8Qb9j3GA1uJTCAv@h3hZYQBq z5Hp&Sd92I&tkZg}S5P!e^=gmjWy}Mh0RRvX#xQ_En*c#H=tC_aKo}7FKCnZZ1i&5W z!!S?*00aPq>w_XZfOrK%0E9xbQ+p`P1CUTb4rl?jUwE{a`>z}Ov;%+zpnIDj!1u62 z&Be(B(DAzmz=iKaw8u%c8-{BeNJpwb41hst6vVU_+B(z;)a}BZ(2Jk{A>i)TLI6Yp zp)p#dRRJcncuNf0R-JeFipHf$#7XSJ4MfE-EP@zBMJC9FFvNhUF+|YR1Vfy}5DL;R zOu{-iGDdnG&|HL_DFS@10~aXULJ=5E@O+EX0O zV3%WN=))o;Kr~1=u;&960J}aALbh)^h0ntZT=+Z)03AR6g~PeyuK*l`JFxGAo9{an zzyTo~0GkIxgX@D90Q)>3{=O4Jr9-IiqK z#3Y;da2_o*<-!b`N6{k2hqM?yEVpHtJVlZovLlpCvPvAu4Uk6sggrQKwRMDJfGXWtsMM_4*ZTSg~WtmNiQ@ z6Qf0v)-GZc>8)C}Y~#XhOBb%)xo-2yZClqb-ne)94&Dp@m@wkQeG@ZYZ1}I^$dCow z<$4x#X3d*9clP`lbZF6|NtZT#8g**bt68^p{Tg;`*|TZawtahay&K9v#~{{u-U>Ki zc_E0_m7RcPy96B9=ZC{Sc`Q!pt7XCKt_R1NhuPkBUh{py-}u2kK0v5>RpNlv_h&fR zT?3-cOW$=@f@r}6oce10ynVDgt}AF5ap)kZ%3%RPS`bKvmK-431%aJbC@Ly;{P zS{U5P4<9;QAsJn8*hvc&08l}N08pwT6Ix_I=8rcXl4rxGs5(d5$hh&xhjx+wL>`Ge5@ViS$drm5TEZv)m7*tKg1XWdGJvG%;TP^j}SWA7i)>vK5mDOEo z#Y9$Ce}y&JSzRsm)>m~^wpU|uMOIc`iM93EXRVc$R!Kzt7Tj>f9hcm4%{>?0bk$v# z-FDqoO_><#0WFMBHM1`iu!7OAD}AcCBR}T|0b)EBLeYm102Ef3zJ2sj;PTxCfB@de*I_N%a~RY}0MO4qO}YyIhQ9d->^I?s0cf}@dL&^-(lTXm2q6}b z@q-W97CK2CBQ;7GUbwiS zB`A|=XAusKT1N(9V!*J-nW8j`9UO*)YR;S?Vuu%D6wyMGcGh9UqJrM$A{lbTh_sf$ z2PXs!LeMC)CWwR)W*0?t8VT={>KvvWmY#|U6Lu5)xB@%pZ{G`zNOzidg`xt9{cI5=N^2r7g3)4 z^36XV{q)sepZ)gTe_uA;c>64d`ODJ($K;MfVP^%9v-@s5RR~4E97Y8r?4upcc}{e~ zLJulLY&_ywPZ0(No`~rq6N>-QoIv*J#Kv}T4h&)liRru~2GzwbA$zmbj)pfjDuxeJ-cw5N z<{^nmAa5A%i-a()S}f;90@QvZEh; zP@z4-_-4R@bBC2RXC3{K&o>(w2~8rU94q+5LuxU@S|DIZFX7*x6k(*4IPCxy!I9HQ zM4_l9=^*UT+Ae%X1qlhydk~Sw7SeE(B_%_QM-ov+<9355?Ti;PppxGFRuw>jE^0R$ zoE9V}Nj(jNAxL4zT1EoBI1!PL85LBTx@5SessweLyd6_aViPnZ=q3`$s7{2(%T3A> ztep&A&OB+9NMr?6?vunTKSODiN^O#pWqs>hyUG-~))lV6yX#)vidUs66R?35>|hC7 zSi>F`v81X0UvGZXnObaz9$LW8dI&ZTFhIbY&Vfz@^A()o=wlJsVMlu2Nw9q+r?VE@ ztaQ@jS%-zzVT3p!bMoai`rr$N(QqKa_}3oS(gU_$aiR98Kqg^OVP`wM+)z)nL)^K^ zO0`gdhysecT}-z_JEKLEOtR9yJ@G-+-3b-mz*i!u=`NiqoIiH7Qf zM}|R?dhP1-!bB!^?1FG}3KJKDs*5w(Zc&)C2v6pr3quf5C#rK)qC&C=kgS7`K8fAm z79lt}(Ig}FLP#M}N6FEJly`lV<$m*OS6gDLZ)^S3C)_u?yoNQdD%Nq1d92?aFU7n* z7BZ0kcU(%W3?nOvRq~RV++-&|8Ol*MO}s+mQqO=^VL$N7!#bvhyNyISv)0plLO>4>M(H5a$!AEmRidKMzMaZ;FDiQ;de5gb!tiTg~cyCEIN}~U=C;%I# z)7&Wa#TeW`z8MXJd>0|2hb-xfbh8>ee1wBYSdFe_RUIM1Ac;fq%qWWJNKk-UIW2%; zEiTyw<-lkZimYuOInhEGc!5(|FuU9+Vbet-DY%xfgH5F)#2%%%4nh#3+w*Y6Rd>Sw z)#;V>#R31Zq*~cLK7y*T?DJ>yTukJ#k@6|IYw=h0`|ycF+{G4`_{Lc*aitMM6KR$5 z$Vpyulb;;rDQB1b7_0KJ7ziRVThWTXCN!F}Xy!r-8qImWbDQfN=Q_`M&u_k>7##ga zw^lmRZ@q;dJss&zkAc*ep7bAu0Z2;U0uEvzIi9A`+LZ&twdb8ivWuN^le4zh+5Yvm zquuLj6ualfJ~*`BopL;tJ5MXOy0~k>?Q8$~-M@ZqSgsi0PI=`gEgo^CnDSH6OrMP# zKh~OFewLQcJm&GbdCzwq^Z@UwFEfEkm!BT>saO5#S%30)<@qyuYkfTyF8bmB-5&S3 z*ZuA%MGvI-p7(!~?@q$N#o$lM_Q%J4@{^BYi-yQ8Yr=qlEXaaey*Z=aKj0jV$JykzTQpe58#0e$S>DsTfo5CI*|cRsKd2nX7- zj{-3+1V7LEZp?4a5B>V)tV|DFw8LQ(AwW1|l@dgw9N_t;@0J!%dv>9`5CQ-=WF3z1 zQD{MC0-zyuDF`!92zh}~sNg|%sr@j45%BML0MHA+5Dddm4974tm}y@BKEujbuO4nD z2FdF7?n?9Os`6^d_D~S|-f;Qa5D1m8rdI6pS3ArtT~dA?;A zARzoagL(3>{MK*Cn1XAJPbvb+zR<50WDWp2F~}^D6rZ98`)c5nLKq?e;Utd?UlA5# zQ5I*h$@F2d=qKR{P)!eX@0Kt*!kI7TZBPNqJ&1OOt52p)wpL5@pdo-iqVi4jwY7OxU3vr;R!QhliZ z3^dZPTy#U}ppGo7j_Jx0Eyq$V(~>RMa?-AG8maLT`>+(HZxEl6^Wd=ZFisd}X&dX% z^8gbBjj_l&Pvdw|3a7CHISDBgG4jG<9#jAUmf>NxBQhUgAC%#pbRcB517QN7J(vR& zGHV~K>0k(4VK}8JYq5X)F7`L$ksVJ$ji@yANEF7fb1 zW7Mk(@p?E^dk8c8Pz)MTm6oQbco-2fl|!A>W1a8=fc)iPJg8q30SF>f=3cXcR)#fQ zGZ@Bm6ZGL65g-#_49yHASNS1F;9*&W)=pa#w#L?^8t8dA-hv%a`I{v1_yWn+8{z{ zA}<#=Rb)q&WJ}fymw{9VP(W|MQ{815B4Gk8&jBxwR)H0ln9ne2R1Oc(FLkL>n(=0l zF=vzQL^lt{W>i&eD)-(BR&TT_WGt=vEIrzRI+B%tKxRO|LxA4@X=FOc2-*RH`b7&& zOF*X79|;IDGvR^!rz<+lQm6nr0}9jT)Jp$YT7U2L$f)GgSl8ItshB}goPEsQqR$=}$IDgP+jHDfM(@kXVOAkV6&a8%@!k37F`&`jv zQ&)9Ymvx=Y48Ia}zhzVjuw6`55*WdkNX3`p!k2E>cX8Kud$)IicXx|7c#ZdXhxd1p zS9pQfd6TzzahG_bS9yt7daJj1*Wz|{R}$c&d4+d*!S{NTcY3Fnd6n0A$5(vA_j|3E zeA730fme6mB6sVed+(Q2)`C>_w|fDjFZefq_o9FKSAX&U_kQovsAsW zw^Y|E{zy~=0nukcNdjMWj(b*Dd2ulb&W%-2FN+ZlQBerVnelw}@K}(Y|M*fOVG>L? zjD1ub1M)Z~__exBU>pD*o3)k!Kmb0=gh8l3Tyr0QOJN@19(h5A4$>zR z5_d@1A$g_;$@LQjASC=imEkc=p{+b&nktkI29-ipo4eYpzZ$Gl<3Im9BS6h%R9m)P zUe;!@(fasUlx!7eDQu%yJy?_A*Q*3!&k4(-|(mrv({sk2iZS$+Q!B8&}!+7+peDkZdbVo4woHy|a0&Im5Nb zB@to_@5?Qxh=c9ZJTKMrl5MbuydRJc1d_Zd$r#i z$8%iAIn}H z6gfQ^seH@N+Zm4*&NtmI^~;ycT#~#(88iXw#6T;~($@dm64%`_*VA$>k37P$TxaE+ zystdL#j3*PG6hwA4sBe}o88%;-3#L-5;p^yew@%x1GLDNG#o8e1zdCLdCJdQXm{+b z@iNrMUE+Q*LuGZw=bFiz8`-(syn~x(;gY!LAq;S!Ui^F?2K3g%VA5nD3~u0Tw$9*j z;NTM;;TK-vFA(4#J_aCO;v=5o0UqHA{sz<+d$HGizZZSamwm_AdPzlovv+rOS9^8W zd)ESYwfBDS7k5cMe!W-a&$oPkx8|8wei_l5q21?y9_XQL8Ny)AHN()!rM_Q75jdDM zmI2ZSZoxBZ7b4qsG$;VVG^+npj;+Fpicm%yu2E!^NW^5eBy26>E6lrSmQosJU>jV? z^*P=Tyv3os;*y*%LvXv}ov*~;;n4srw!+mj!Rch+=?FjTc3s!UQtA-DEU}L3ZopCK z(2pr`$t#=kC;L{NoYPTEtNT3YKOgi%U&%WEm0o)OHd?#rO#?u%{ju*kSCOI?Mz>L( zuu-C<5FWr1fp#aV02ACkDi(pjtmNyZ0!buF5?rbf40ibWrcRQ&{N~=?SKZyQj~n4W z%<1?K7xU5wd>;Hs6C?rPB|#G=Vb{=r7!sXbTA#PeUAvWb1$Dp9q4Cu5KFb4}yHSzZ zMPL8-pZ|#oAn4stwy&VUg9sDGAegYB!-o(f8q9Q~p2Uj~6#>DBQJJK6^Fq#RCyz=R zk?a-$2oPpPrg`jk1pwe+*t}W+0w6$E2!}|RE6@;fpfUiakn(WS6c#e&N;Jtxx@;G0 zg`0M}RyMGC^MIBBKH9BAkSt`oDsO)Og4~6Jt4OwG%`Wn>2*;urI3$_f72`(Rri*UG zK+{yr%V1mHv=bJlZzhs~8@~kkb7W=8W<^rQthRDz%$zTa*4#O>Wy_&CZ$8Z$b!O0` zMTaikPEw{!#4<(8WSAkx-@t(vtpSC|n(7)Z6$Bu^m$q_qnIHkA{=OBDk3l4qX2=LN|HkotcA~mtIf)6mE zCF3M0ayXBRD+X|qO(9ZNA$H6E5xK}otrapLihr@y;*z!CkPJIp{4kF^TVx_kmN0Qi zBXr0ZKN#vw=&Lc@Ax#i~Prkr-_>8GHED(a}DmTKy$sHU3VLgd9D-lxOh zKu>~;7_lBSjIdc!T0o`M4m%&RgO*u^(6~#C0oVjvAr6cLY$n3;TH+7utbim+Rfw@d zTG2LD>`ej0MdgSgts_8Oabk9%Vx*;%LsKziA`cfMuFFms*E%bPBwS)eV@SM6SqNcf zt`^;El2SLFb=9qDD5PcozNuZh#=U86>+R2Jq%pJmxjyyrd=CNvKNN}oh0ul(}NH}CxO z&_^%*c=aGSo~xh^bHg&K|3;H*Yku&$Hr8*fb7Ex`PI>iDya@A$l`ws1KDkYuk1ZN7 z{%XHSwUC9kTQ%a<#E`-_&9DO#g@7MB;=qU~h?y=?ZX(?XEhULe0gX6lx*g0^STYZ8&~vnTnZ&XK!p{{fHqM#Zp7u1o zA{z0CNKB#8jXA7xdHM#bs*oIiXM>&O-WAjD{DFHLnV3_Yc_NxW)Vq7XkZrJ z-Y_lGpwW$4lTjMv;Ri7iDMfYLR%XVBI;{LfNH_>XSYQ%{$MEfpVPF?#3L&_YLSB`%;s6r` zMBIu3N65!S>6(_i*GqU2gDFjLB$6!PDjG7srIA+YA`t+wj4XRLw+9?28B#OPH;xe!J z5u8RcCoSQXOmd=^8mR~-*vtuG!icxYOw}mZ$*PoUG@|@vB`26z%K_XHBL0XAhjf9~%uj6#(pRgM(n9|FNEY{W7nPi0JxaO~hiqXTEa6Mx%UTq! zHop0tD0jtsLh8~Kw)4o7Nq_&w2ub+3$B|$}BrYY1MG)D@NG|e`l|1Am4>`zBUUHL{ zY~(0cnaM;JGLU_2WFjls%R%hFBQb$$-Eu!l|T zVh_`3g$_<39qijuVX+;8+O;feC__zJm%hrSSha1M-I)%`xiB8@cENp9{2Hppw7wyv zAI(b8q?dKT<{1n}tnGqNj=N;HMjQ`4Dru&H)%_h~; zPHxMeq%W^H(W+Zed!Y&4lt?8qy99xRhJWu)9&_zWTWm)fCxQEnQncx{@1D^R7Cq?V z6(MF_Jjq+zH^-PbUc7e(C{(wbOv&~1eF6VzY!{w89v(3)MdF>$E?(-O554F|AN6Qt z9V6KL6h)G4DvyA?x#Sg6+0a-P3P;7UY8lqvf1>+&EP`MH-90-h$dyOxur;-SAJ>~A z19S}@*XP}6j7|M=lAqh(yH|1b>8`Qd7@TJx!=c|>xzc`*td^1|f3_nZS@iGQ$FvnR z>E}=X`r9ARMnjs_3-sVr=|d6{b{Zu?FjtaQEiocWQ4=VV6aVLuK&N3h;6f8GaUnRc z1vw!vg|HIMk`g%~22qhdI|4?3pn+ncBO%dNm|+eOWI$uJPhr6rje}KU5EwF$R=hws zHgOj>AXw!Vl%c`lVy0Rm1~DrZuKQ;Z-P=g6`(|?)Ds2REW%(d>_7!p0uqXNf+98#=Hh&#aX~cFCJpo} zF_0ELF;F(K3t~`!V-QI`;d>rf;PQb$K~CRB4Ow_m2wYPj(U zFXUe0r(KZcepgt1^wwC#xNS6-evoGw@aIBmSdG?rjo8>c$R;4)w<%cHhN2Q-$e{&D z(k<~LFI8|Ye?TMY!$&_6Eg&&1WYZMeA`j^Z69+IE<0BJcV1n~t5?3N!BVmtRG$#c_ z8g=m&Z6Qeyv`|ulIO1X$zaj=aKdqV{t<`6#R$bU4^l%lD}f1l zBP`IO0y(h~V1XhIpg&_*KZ+wsYIKhCFfibf6aQfO7#CtcCBiL$7!vEqa#vyqcQZDH za5v_F13aNXOTrc?(if297$bCn*&lH)dAjs5;vlo(2jzG^ z(ef;w(T)NMFuMSVA>s#ZWudY%$1vr?3|$DD-ln^SR)!UCPL7ZO8Z zKr5gv>>?~gFb`GmFY}X#S1~_vvIqwth~9D%5cm`CBa(JEkae*LVssNeQHd~-IDWLF z9k&=az%Tl^oph&fLzj89SVMnjS*a6vk5-=CH4i1lI_%|MQQ=;J!W3EhaaaISPuJ($q_gb&{TCe!pulkCv^2)Ac7O+IdC&vS+2%E49TYo?q z91PkiL}_S7`B)qn0BDdmUo|4d2{&0~v3Vs48psuXPyzJVBQX*NA$uY4Bda4J6Aoai zm*^r98WL=wIMK;CgHbn(lQ>wVKoE2|lbDgB2k`jAB+p_EDqmSBlCQMtHMiMVp}xQh$9ksG;0#%uoj zzsvD^O|b={#cRt&5C81;69}wdGTCpQ#*8r+dzof!DO6c9NtoA4a;FgqI@gUpQ%dn? zGfSrlHdDeRe8MK2!YI7LD%`>>{K76A!!SIGYec*ZkC$3qOQGYrb`S#z#%zu z7!1ZCktfU?&GvlH_?*u>6TiRF%5Z4E!-c=$_Ou^|pHX_ytr4g4JcOIkafE_eyyzLk zn8t{Cwc**H+t-UYG{(~R%bL~2(8of#Va@tn(k6Y@rWVE)}&PI33 zX6(%bUBSCdtr98-I9}#;hbLewDouP?%ExZsEu|rR? zEEZG$v8y2Q2jx_Sw*xH7Vre5{mtC|}6YVc2cV=!G!Hk-Y18CpoTY z-PxY~*=S95)NI6U%_;w&eaTTqa~iK;XKBw_h09eaw2*ev>^ zHEuu%IngYH@Gh%Su^tE%EwR&;wgNdZPpcRb_edI1k|eZB8F9y>Od<)8Lm0Kt7>q_j z*j25|XU5y?RD~B_;dWy(y^A@vF&+%s@;%@5jmrA4$`9esSErh%;t{Z6kGwh(v_KY$ z1|n;d9^LJNseXYxS5b5 z;u0)+B%JE=5}BA8CEm-OrMfj}qn5!Ie#8}Xft{L|)!Sy1Dz?ryu9%xv#5_h${Qt}m`duK}14XD3%a)OpRAV)dK~`}iH&zlNSN@Jwa05`;xIV$=X5k%@ z0F^JX1*`Zh)dDTJ3KAoWJ}(ULEy;(=kK^ap|j$ZagcGD4BA7DN(k2G2Zh!;&_#NfMS60Iej2s`Z{^+!T&O%G<_wwsu?l?s*{)*dTrD^l87Yg=4tT<6GH3gb1tkp@DxH8<^$n~W}7xO zeA8KgmUDtq0v9mB2*z^o`XC#Hq@TU49>@_CQ};UFrW8&zxR12+Dz`! z5SB%qLFFgNso3PHB^RpxvyRwN6&i9PpsFDP08U<1;K2?phyN^zXeIJM2oFFS_V|!y z#Pw`GKJ38n7eD#p$yo207$xWW(aIHyxK}O6yNNEIASH4*ch0@qn6wOqZf_{`Jl1?) z{KkL$x`p2kap^MMA0dN440Q41`?^GGDc?}yf zT=+0zM2HY2T4Y#JqQi|2HBRK1aic|$9yyjI`LW~3l_o)wG>P$~BuvCI5!;tj=T4qI zef|U*ROnEmMU5UsnpEjhrcIqbg&I}rRH{|2Ud5VK>sGE^y?zB7R_s`^WzC+=sVqh` zrZ+O7?MW<#GPp+RNn#iB>D^kvuQ zL(42LdVXq`*ZuEJk37)m8&Ea=UK>rN&nSyz7)^!&NGQMi5;M)b)6~naKEcYtK_D&^vd=IaDXX!g|BOVk z$i!GXOuPc?+fP6p2m1`a)i#RF!C^q;vP&<&1T#!A$0V~%GsUdu1{9N;3%5*g7;(Dl zFk1{g0|6wGMFjE8G0#0WvkbrWqAW5>K2B_q z$rENYN0+6Hon)Sc64@&iI`%$xfV}lz0{zs?-ChxcNZ=`>Wk?<%>vNdaI;CXk)<91Z z=H4SCiX;;UlNmQ;kw+%EWRp)uc|*0D!hzM=P89C0ybK-hu*MWBB)-n%`NOet0%*6j zjMxdlJ$WIjB>(_kTpA5vPvY6>hIsKI+J@L+;V;7a()!@b2Erj|!3NrHk*ZVMY!Rtv zyhxZZVz>bZOd8rH#>?8N`@2RD%7n8alllZ5f=}M!3DTa|tLM zoFtOb#~AG*27a8IORC@ojet#K4l%@bG=jl0Dx_|UDB}eQm!H^Vi+!I2Whg~CN>Y}R zC-j*ApHj+Yn@p%HILeR&?*#G($@s5@@^De#SZK)rd}1D=n+NF@sKvfW=4XF_VFl(< z4)1j5f-Be#>ki_bb!5#TE9lWd9Fs)HC?sk3;z)PASv1`x1aR>w#PV!cl4H(e7uICm zF0UEQ-Ca{M*;(Gi1Y$N`#6X=}AYlytVv8G$A(+cs#2>eyPU*Enk6{ZY5fMkiE!@+O z^RT4cKC+7mwqp@#xYsUZFumc`O(Ev6n?u-9iw6iPkKB_8JHh}`4ED&7|0Ln_4%QBu z*aUN?1Zq%)I#i+-wM?i|)l%GIqE-GzW_r;A8fq7rs}&70wVcNv!svixoO2ffPzcZe zdeoSPRiQ`a5P&dNAXV?ZZWz9EXA7$u!WHDxokDz(1B%%Yp8a62?zA9eoW_m=D6Vz_ z=vq~OHxEKg5NpWP84JN_IFVG*q}=&|JgWE*v;ESq6?KwGt@F`f_J|=SrDI|Vibx>w z2%IXNA}@vqP`-ASGky64W%p8uw;j?gvIP=5T6WoB{$P$b5SwWvAsd?*)w$1wZgizP z-P$xYbLXnfo8knVXv~Qrv;aVO!y5oh^ly$;$V3vCrnCSQM0D+Fj58NPhzdZ4hn?lc z0n~cCs|iCA3p~IxkqNu=hOlUift^at8%!S%t#7}C9WC)@!Hyh6g|S=+>kK0Q2}!sp zb-sg)5SK>b$$(9^_cEUH_{s=``n6>@@-HLeW{{7ncxu@kDT0K-3xxDdwh!5HhjUCl z#0k!i{@ja$_2p2QI)r+)1L%oqTo881K$e<`L@ydgT`qUo%U=d_nD?==wruJ)uKFo& zo_Z2LEH8*!%g!5=e3Hi8i*#-khy($*9fPp69qUNzfjN>L2k-(DZjEPxI&JrfBXtt^^J&WhoCo6d}WYVTkTRScQUcMsyfN zAcfiShtwVLYTBV-TGt95$=dRQ_53P09jtEzzt)~pZSWEy+*JujykN6To$hdUyzD3w zuCEiD>P!q#b}*tK+;-?f4=U4ILWH=&yI_qMF+u-t7kdLcxt zYf)GX9(Wg?uEO#fe!#nflhoJLdjOQlOk|*3n|Gbw3c$)b$YgW>3#C)?Y2_LbdCc9L zbQg5Q`)UwJo8;~!q#f%B5x`)==$bQBC+Spu8q3;^+cc-(4QBnzKXRKFMyFsHwq6K{ z{@iWq6sSY``Cyqk&9w(f{m)Sv(ia~@GPbVglL~P*j%j* zKmJms*>UVXA1^@*_CQF^CYt9DFdhv9b<| z8%f}T!O}f%5}X%{31E5*ZY!Rw!wwnKLFT)f8^k2LI*VG??A5`EGam%11|d6L-KldHlV znz^1mT#VST3G7nCOw`0pehdKO2l8J`4GYWTOC3yRd?s_Bu$qp)Wrq!#MLQEFi z=@s45C(ywiXEU0bX%agz57P+@Rg;fJG?-s8oFJhT8c8~?dm4&KwP}Hfj=70X=)`KY z#%sjJtuHpYSqUD|5oa;R zk8?U+1QcGpn`iXLNL0kbkOxCZEN1LS;+dGwxRx@^#)pK+h?Gd6sE6fHhHumfegB{b zH~f%`#0vCNB}{}1_M1c8P(^z5!=oX@M%yMOFgWXTO7(e@sDsDH>DKJ z!6Z!Q+Qw*L!!ct@o~Q?LyvSv6%E;_SeW*w;VMC6b3Xo)ztQ3n@Lc8B!7q3ajl8Bcp zxkr1s$FT&))hJE<$j60LNRxONK#|0JiObX!54MyPdL)*zOwCzLNLmz(NT`?uqb$th zM9$NhWU|SlA7OiCnGzg7$71_Y=}aDqFEVD zSae1Q^^=_hM8E8l+x(-HkWR$TUsSrUXwN70(!Gilt&ZKSc|DSdrq1s+ZcI3Vab6x~l*{0AZ*C z`U|cAh=J0v)F(2vC;EdM_@PYjI3L150XioEfuR79RKURjEf}Ez3IGdQhskp%L8ORv zcrlAGwn6JBKRO}n+k&`J2x5~xn|w)k3`%Hm%^zKqyj&QW+){s}O~B}+4sFwH)z)nd zldAuG%I=iP#M}uz71y*_20G=95#>%nwMw3#%o6(9UUdxwbaY;jb)^-$7y`&amY{m#hQfMPaGQ?JH zCEB7j+6rk-66MZy<%y(~N;vIPK2IQlv||Vim&y@yRTu7}!KeCXGjYk3pW2lTS1F6 ziaQBRyP&*No^sfZS7-o)NV5JRgqCUuKtqPUJ)J@z;-Q(e%BwFWzQgI^Dn@$@JgOAB z2^*1+TmY>yt=YB8y_gy~Mr9PrEv-#_L>Awq$w7%$p8Z(_mIO^m1{QYUI<{lfT?P{_ z3dg)i#8f|d^+ps0%$#7az{tu3icge|%jC}2jfQvai9?187aodx9Z6L54Icd+ zdb2lEdoX>=4oRX2Op7+BiLbxdzg3*0-8tWgsGwvZ+==i5Qoh3j9p!oOgX7UR_#)Uz z%ZM!q--5Y1mBKxT;sQAfKD?yJ#MkT|W7*t9)W-n^U}rjP$f3RCZwBXV zjolPo1_(|I7`;gA?1^#SiPe4Gq$*yG^xdb>HCgiFRd=-dIQb zs!Bs*CEF#)fKQ%zKJ8gPjYuekAgB=HK63yzJ(8n>x#C~sY1j1J482QdBw)JKUnFIo z0U4hM3FoGE>cO<#eemNv##2EZZZ8rpm5O=ZOT4%T^0pYb^g-} z{_3Ov-lM=|w8+=s;AB2&m<-|{_uxSCIymymJAtS+?r;oVIKdR8F%9ZozzQiNLI7XV zWn~Hp%~BfqB)BM!wqC-ch%g5TT(!h83E}x2yqUri@-}8xh<+NLHb&OqO#ffF}tY;4`4;Ku5FS`237aaF(S^vgBHBtz3%uIaJ|*VbHZ~Jk~1RoL1Dc3=+ z2R+VRs)XI2>fmT-PWS6h2!{$(S#C=v3+>X^#nR#`=%`mD>Hx9Hva}VXM#+(=JX{tw zp9bnHUSs;U%{*k|G(M0auTc92>X|jo+H8p|VQK=0@+c3#dq&K5hX2zy#T@WF=XI8F z5PnSF-P1fS(djg64tH0K^wtfZ?gV$k40loO9tsssit;?ORQiorfy)*-B6f`J^S;h#eG(W;Lw7W@=fRTFZ;%(RCD0Y zV0VRD2tUlBXw2l+jS+tz$JEi<#Z2kO;H=Ha+@*`K?g|tSinu^>#j z&GHd83#gt;R6m^AWORCI_(YCuHRX@AQk!S+Sx-1?633H?b`TP^?x_mVgHpyEcR#BLpx zZf})QG_U6jhI4xE=Pz&3o)AYX2h`_=WbTv>87)_H=Ej-NOgsO-E;kVaqW_AzK6Yw# zM4;`roVEt}mfUHR+M@#sg7idQw_1y0~&rr9NUv6K!lUMt-mmH`(cN2b7 z35NGmAN8l@a-D!o=Dc-y2MeZrAGCW@7DY_RZ1n@*@Hgd1eAmvTid~m?_p?w7RVRuP zsdJ_GiZ%PgZNB>QCUzdzctf{MhG}>ucVqg#c-@psMVa~>G4_LxczGm5VO)t!XZzZ> zeNypL=>+uzAJ;2?XP-CocFuCC)zh&s=jTq$H-(0Z)ME%ob3q+zLZ-~<&k5=ePuzIx zQndW3@O99v4zc%Xv2T6NNBAJO_*!ho+1~G9fBi|^74rWt<4TEm)+STZZxrBp^ox)L zUf8nR2Z((F2NEo3@F2p33KueL=RYCar%Hh@3qo6_a45-yJ%1XsDG;SmnNbJoqi59R z)TdKZ@`Sm}WzeJ>mEEXU@kS=G5sAfMsum+XNy6p{Yd4JDFuLuM;cYi}nBRGThsnze zxNhFMd=CpQ?ANa2!FC1PeVoj0-@BLV{_TqlGvLpfEwkIqcr;?iq8%fCoVYS#y`f=) zmW>zhT+Z!2>+Zcfa9!SX^#*^Oo4E1czI6*{{+s_daO1@95+2sOdSU3os^cEMy)a(w z)Jqo^#&)PknPihhN~xrfP$p?*kyKWhWh6ju$>f)k zJlQ0fT3#9Emq~_kgO^#lDP)^j#`&h0blQ1mkaNO`=bd`K=_j9o-Z{xInhet;qKPWH zD5H%!`Y5E4N;>JHV5J08G+7a3DO3X$WnuqVNF5XwhS12glZObsWa>Uv@kn2W48{c1 zh#Q{hR)akel&U_GDiZ^%6QLC#F*m$AV}~vc^WL(VH2W;G(K`Fyw9-Ob?X}HTyKT1C zdMoX=)QTG}wc2XS?YG@x3+}m@;1B~E(y-vV5&xaNXV zD5VK2yfDKJJNz)j5lh@?R`dj0kXJIj3XQ8#t%%`Owh9$hhY!({Q%WOVgwnALaV1ba z9E+-8s;%_8YJY7v<8Iqsb>xmuF16%fH$E}voqPT{=%I@~I_dS{e9*_OF0&I# z7gqJ6j8rjYBFOtKC~`qP&6+!=2ht8yR;S-8ljpzI{~S>mnh;wfq?bS5c;!YvZfMh& zHw}H$&l~N$-PlW?i6-DzuQc}4OYQym=SN>X_`~2J#w0Pk7e4*s*MGnL`QyL8{{8d+ zzyI{dz3{Eid;~K1VwkD2Nfn zA8H~QhB{aimFC1kO%aO2uwoRc7``HMk!(G5B7CZ5MJ!hFie}6r6sMTQqAih!L_8t` z<(G*YhyiWMDBHn^F-2vVk&jKpBiZ&SH9y|b|BQMx6d?h5$YJ<#ZFv+V*aZ2=Cu*^h zX{013EorwgYSMsZq~sfwD9TSL?}Da0B`Q;?%2l%RJ|*MG37^6gSEa;5ArwmNba#>j z@dQ>4K}o7aWU~=v@Kg-381H_f43`|QctfMeumDsn9kw!pl9+@*G7*Dna{3bZV zDb8^&1S_~xNUzE{Gp(cq8az57%Oa!_YKFurEi{N#bfusUQe_!3)X+akgii$BvvbUw zNYDT@$7Yg~bj=Jkegsd zr8bMoOQsUbfuQst`n=$;EE3KCu!Wu(vt5|{2C~zi61AvCJt|U@%FT}wub&&dB|=J? zqT@+bK`aaCfLPY4R*AH$ZgI&`ba>MW(xZkHNhnXBsnmywK@0-XL<|_Z*16KPu6DgE zUi|}BP#uVuFLlUAm%<>qVvwULDiOz6l9Qdx3@M~S9adHOtH>$@2ZxfTS@Sw6ZL&lX zH_$9-Lo3?RlD4#NX%5RUkjEmjMuK>!f#(okjsP^-#tBa6< z28ESueD%~F801d^8=zR05-~IBpzm3|fPOk0i&<96^ z+VgG-rc?2*24`E*;Mxek7k00bJrV=Urn${-elwgah!vY^YAFx`W`?zSU+gC zX)RR;p~fEXWf>ewhc;*`0M{;~#;ZseeC!|j##tm{pm3!#t?5m3+EF#7WeB$_%X?Ne zgDYZch@O1Ro#>-hQvq+QZn9XaKnP__Az^DLN){j!1k?L5Zl^h-qhJ%e*v3Bge&Q)Y zq4=oFN_n++?b(&@#%v;}w(*ofMY4G^n6k2KF?+-R;BAD!mp{`)_C?@|?sc=f-R`!w zri95%2DMC7B{hjjb~|n%Lxg1qq7YY3>g*_=XhbK0(L-LsYj676Ar|gW4&NP-VSHBN z7QZ;g>X2tu)eJ6G!5z{e5c29~&?WO(%?xo22Yn!O>^j)P zF7|QilEkjEZi1$6%WXq;WEKkO&r{qfj^(&4}pzVL>h zV2+2UaJvtLbt7yFQyATPzR&fI=#Q`J&UB{tMi}tob}FsKJ*1- zDODAECK6`+l<{3+njZZ`U5)vFpdxD56I}Q*zZCAB8;#}vFXZ9;0ruD_9{u^#zy9>e zbyffBLeH<0mHl6j|4gv~PRJp}$T^^i;obY9$JUSPFIxp)D13+LyIV-L2d4QG^9zRv#Xl1`;AE zmZHzSgufM-tuzQ!^@@ko6dsaSfV7&;T#8p^TLH?Jgu$9f!H5ijU!1X@Lg1i&xX;Fg zT_%KqDJ~;3x|_1)LYZr=Eol*BkH*$ zLpG#`>CP(3h^R^3dBu_d;vu+=gtRpT`pLwgHCy?q4jbMIOLWNX{n@)s)>-`Bq+wv# zb)iGXq)Z-{KfaX7VIIsF;4zs*u32A3CPj@Q-E8sFym24B;i6R3A>s9mL1JS^oE}PE z-096ERaWIim0e31g_V89R?Lc1RG|=xVUGw8i?rZ}=|-Qx`;?LlC9Y|&IzpyE%0on0Qp zLE=Y3f}Q;BC1-XfE45Y-TGgvDU>%YsGW{dC=_G#~qeyB+ym`o>U1FCJ6I=!!Oh!bx z?Z>$V9$tE;Z~kTk0cI?QP|RcnN>yS~8YQFvUkJ_KB!OikiDJZnA)Rcxkjhl!Q$&L=iVSNCB3abtCjuUC zmZOLksgYt((Z$NgY$A$0)i}1{Fga#TOy*czmGuSGF}7lfFvU~-n-r!_$mPV;IVJt= zXF+7AePm|WCF7B{shd*7Au0vJ1z1d;#LFpDM{2AqruRi zu@sjvU4S*E-HmHxr{}qq1WPG_K*g5L1eyfNAlPtoa3z3jHZ7DvLZ|Rk%9eH{WJM>viYjofpFwKf`{`idZDz_Q?RReEbz$iX$rOnK6N1_Y&W_5+ zq1wDLYPv?On+}<3HpIx{$8JvQ$ttbc?&Y%bBZZWlTR`K~hDf{us@6W}KN%hD@fEo8 zYeUp3eE=-b>*QgN@FPsMOfiuw(Gg!X4y@k)0?p=Pg!|k;4B&3=#sKf? z?(X(3@BXgv`Y!PXZ}9>z@(!=@E^qQ4ukto8^FFWiIxqD`Z}mbi_D-+$Zg2J;ukPZm z?tbX!j;~A>-SCBwSV~^I;s-oF78T}bo|)@8iQoDjX~^Q{IqpX^n2jifk`ehY7d1@) z1F!%WFaaO%|0eJNE3g4GFaj?y1UGO5Kd=N>Fa=+*17|QX^zYRWL+z3;2(zBW9FJg) z48EamfE?p^*=c%m$RyTc2wUdR#xM=nFhMevRmfCPd@x9WMazYx!k$|EGUvFEJByA;2{X%Pi(nSeMeJC6vD1G9fFMdo+ zCN@R=2JYoT1e<}e9M7@F#qNycit9cdpt00-%?{9l@$9xA57RLr7xG=D7Nf8X%CszR TQQsWiB*z%?sBEV}1Oxy(v7)Ya diff --git a/src/main/java/us/muit/fs/a4i/control/ReportManager.java b/src/main/java/us/muit/fs/a4i/control/ReportManager.java index 2c6235ce..dfb3b412 100644 --- a/src/main/java/us/muit/fs/a4i/control/ReportManager.java +++ b/src/main/java/us/muit/fs/a4i/control/ReportManager.java @@ -95,6 +95,7 @@ public ReportManager(ReportI.ReportType reportType) throws IOException { * @param report El informe que se quiere borrar */ public static void deleteReport(ReportI report) { + log.info("deleteReport No implementado"); } diff --git a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java index 21de1ce2..4a2a2a68 100644 --- a/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java +++ b/src/main/java/us/muit/fs/a4i/control/RepositoryCalculator.java @@ -23,6 +23,7 @@ */ public class RepositoryCalculator implements IndicatorsCalculator { private static Logger log = Logger.getLogger(RepositoryCalculator.class.getName()); + private static ReportI.ReportType reportType = ReportI.ReportType.REPOSITORY; @Override public void calcIndicator(String indicatorName, ReportManagerI reportManager) throws IndicatorException { @@ -53,7 +54,7 @@ private Indicator commitsPerUser(ReportI report) { @Override public ReportI.ReportType getReportType() { - return ReportI.ReportType.REPOSITORY; + return reportType; } } diff --git a/src/main/java/us/muit/fs/a4i/model/entities/Report.java b/src/main/java/us/muit/fs/a4i/model/entities/Report.java index 8ba0463f..9b2ae71a 100644 --- a/src/main/java/us/muit/fs/a4i/model/entities/Report.java +++ b/src/main/java/us/muit/fs/a4i/model/entities/Report.java @@ -30,17 +30,6 @@ public class Report implements ReportI { *