diff --git a/pom.xml b/pom.xml index 3e48a69e..d78af34a 100644 --- a/pom.xml +++ b/pom.xml @@ -116,6 +116,16 @@ org.jenkins-ci.plugins jackson2-api + + org.apache.poi + poi + 5.4.1 + + + org.apache.poi + poi-ooxml + 5.4.1 + @@ -164,6 +174,25 @@ org.jenkins-ci.tools maven-hpi-plugin + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + generate-report + test + + report + + + + diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 91a3d37c..00000000 Binary files a/src/.DS_Store and /dev/null differ diff --git a/src/main/.DS_Store b/src/main/.DS_Store deleted file mode 100644 index 79de900f..00000000 Binary files a/src/main/.DS_Store and /dev/null differ diff --git a/src/main/java/io/jenkins/plugins/reporter/model/Item.java b/src/main/java/io/jenkins/plugins/reporter/model/Item.java index d5ebe13d..ff03a542 100644 --- a/src/main/java/io/jenkins/plugins/reporter/model/Item.java +++ b/src/main/java/io/jenkins/plugins/reporter/model/Item.java @@ -9,6 +9,7 @@ import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -72,8 +73,11 @@ public LinkedHashMap getResult() { return result; } - return getItems() - .stream() + if (items == null || items.isEmpty()) { + return new LinkedHashMap<>(); + } + + return items.stream() .map(Item::getResult) .flatMap(map -> map.entrySet().stream()) .collect(Collectors.groupingBy(Map.Entry::getKey, LinkedHashMap::new, Collectors.summingInt(Map.Entry::getValue))); @@ -102,11 +106,14 @@ public void setResult(LinkedHashMap result) { } public List getItems() { + if (items == null) { + items = new ArrayList<>(); + } return items; } public boolean hasItems() { - return !Objects.isNull(items) && !items.isEmpty(); + return items != null && !items.isEmpty(); } public void setItems(List items) { @@ -114,6 +121,9 @@ public void setItems(List items) { } public void addItem(Item item) { - this.items.add(item); + if (items == null) { + items = new ArrayList<>(); + } + items.add(item); } } \ No newline at end of file diff --git a/src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java b/src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java new file mode 100644 index 00000000..638fb0da --- /dev/null +++ b/src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java @@ -0,0 +1,246 @@ +package io.jenkins.plugins.reporter.provider; + +import io.jenkins.plugins.reporter.model.Provider; +import io.jenkins.plugins.reporter.model.ReportDto; +import io.jenkins.plugins.reporter.model.ReportParser; +import io.jenkins.plugins.reporter.util.TabularData; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Abstract base class for Excel-based providers + * Contains common functionality for Excel file parsing + */ +public abstract class AbstractExcelProvider extends Provider { + + private static final long serialVersionUID = 5463781055792899347L; + + /** Default constructor */ + protected AbstractExcelProvider() { + super(); + } + + /** + * Base class for Excel parsers + */ + public abstract static class AbstractExcelParser extends ReportParser { + + private static final long serialVersionUID = -8689695008930386641L; + protected final String id; + protected List parserMessages; + + /** Constructor */ + protected AbstractExcelParser(String id) { + super(); + this.id = id; + this.parserMessages = new ArrayList<>(); + } + + /** Returns the parser identifier */ + public String getId() { + return id; + } + + /** Detects the table position in an Excel sheet */ + protected TablePosition detectTablePosition(Sheet sheet) { + int startRow = -1; + int startCol = -1; + int maxNonEmptyConsecutiveCells = 0; + int headerRowIndex = -1; + + // Check the first 20 rows to find the table start + int maxRowsToCheck = Math.min(20, sheet.getLastRowNum() + 1); + + for (int rowIndex = 0; rowIndex < maxRowsToCheck; rowIndex++) { + Row row = sheet.getRow(rowIndex); + if (row == null) continue; + + int nonEmptyConsecutiveCells = 0; + int firstNonEmptyCellIndex = -1; + + // Check the cells in the row + for (int colIndex = 0; colIndex < 100; colIndex++) { // Arbitrary limit of 100 columns + Cell cell = row.getCell(colIndex, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); + if (cell != null) { + if (firstNonEmptyCellIndex == -1) { + firstNonEmptyCellIndex = colIndex; + } + nonEmptyConsecutiveCells++; + } else if (firstNonEmptyCellIndex != -1) { + // Found an empty cell after non-empty cells + break; + } + } + + // If we found a row with more consecutive non-empty cells than before + if (nonEmptyConsecutiveCells > maxNonEmptyConsecutiveCells && nonEmptyConsecutiveCells >= 2) { + maxNonEmptyConsecutiveCells = nonEmptyConsecutiveCells; + headerRowIndex = rowIndex; + startCol = firstNonEmptyCellIndex; + } + } + + // If we found a potential header + if (headerRowIndex != -1) { + startRow = headerRowIndex; + } else { + // Default to the first row + startRow = 0; + startCol = 0; + } + + return new TablePosition(startRow, startCol); + } + + /** Checks if a row is empty */ + protected boolean isRowEmpty(Row row, int startCol, int columnCount) { + if (row == null) return true; + + for (int i = startCol; i < startCol + columnCount; i++) { + Cell cell = row.getCell(i, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); + if (cell != null && !getCellValueAsString(cell).trim().isEmpty()) { + return false; + } + } + return true; + } + + /** Extracts data from an Excel sheet */ + protected TabularData extractSheetData(Sheet sheet, List referenceHeader) { + // Detect table position + TablePosition tablePos = detectTablePosition(sheet); + int startRow = tablePos.getStartRow(); + int startCol = tablePos.getStartCol(); + + // Get the header row + Row headerRow = sheet.getRow(startRow); + if (headerRow == null) { + parserMessages.add(String.format("Skipped sheet '%s' - No header row found", sheet.getSheetName())); + return null; + } + + // Extract headers + List header = new ArrayList<>(); + int lastCol = headerRow.getLastCellNum(); + for (int colIdx = startCol; colIdx < lastCol; colIdx++) { + Cell cell = headerRow.getCell(colIdx, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); + if (cell != null) { + header.add(getCellValueAsString(cell)); + } else { + // If we find an empty cell in the header, stop + break; + } + } + + // Check that the header has at least 2 columns + if (header.size() < 2) { + parserMessages.add(String.format("Skipped sheet '%s' - Header has less than 2 columns", sheet.getSheetName())); + return null; + } + + // If a reference header is provided, check that it matches + if (referenceHeader != null && !header.equals(referenceHeader)) { + parserMessages.add(String.format("Skipped sheet '%s' - Header does not match reference header", sheet.getSheetName())); + return null; + } + + // Extract data rows + List> rows = new ArrayList<>(); + int headerColumnCount = header.size(); + + for (int rowIdx = startRow + 1; rowIdx <= sheet.getLastRowNum(); rowIdx++) { + Row dataRow = sheet.getRow(rowIdx); + + // Skip if row is null + if (dataRow == null) { + continue; + } + + List rowData = new ArrayList<>(); + boolean hasData = false; + + // Extract data from the row + for (int colIdx = startCol; colIdx < startCol + headerColumnCount; colIdx++) { + Cell cell = dataRow.getCell(colIdx, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); + String value = getCellValueAsString(cell); + rowData.add(value); + if (cell != null && !value.trim().isEmpty()) { + hasData = true; + } + } + + // Only add rows that have at least one non-empty cell + if (hasData) { + rows.add(rowData); + } + } + + // Only return data if we found any rows + return rows.isEmpty() ? null : new TabularData(id, header, rows); + } + + /** Converts an Excel cell value to a string */ + protected String getCellValueAsString(Cell cell) { + if (cell == null) { + return ""; + } + + switch (cell.getCellType()) { + case STRING: + return cell.getStringCellValue(); + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) { + return cell.getDateCellValue().toString(); + } else { + // To avoid scientific notation display + double value = cell.getNumericCellValue(); + if (value == Math.floor(value)) { + return String.format("%.0f", value); + } else { + return String.valueOf(value); + } + } + case BOOLEAN: + return String.valueOf(cell.getBooleanCellValue()); + case FORMULA: + try { + return String.valueOf(cell.getNumericCellValue()); + } catch (IllegalStateException e) { + try { + return String.valueOf(cell.getStringCellValue()); + } catch (IllegalStateException e2) { + return "#ERROR"; + } + } + default: + return ""; + } + } + + /** Internal class to store the position of a table in an Excel sheet */ + protected static class TablePosition { + private final int startRow; + private final int startCol; + + public TablePosition(int startRow, int startCol) { + this.startRow = startRow; + this.startCol = startCol; + } + + public int getStartRow() { + return startRow; + } + + public int getStartCol() { + return startCol; + } + } + } +} + diff --git a/src/main/java/io/jenkins/plugins/reporter/provider/Csv.java b/src/main/java/io/jenkins/plugins/reporter/provider/Csv.java index 4417152d..272f6742 100644 --- a/src/main/java/io/jenkins/plugins/reporter/provider/Csv.java +++ b/src/main/java/io/jenkins/plugins/reporter/provider/Csv.java @@ -7,12 +7,11 @@ import hudson.Extension; import io.jenkins.plugins.reporter.Messages; -import io.jenkins.plugins.reporter.model.Item; import io.jenkins.plugins.reporter.model.Provider; import io.jenkins.plugins.reporter.model.ReportDto; import io.jenkins.plugins.reporter.model.ReportParser; +import io.jenkins.plugins.reporter.util.TabularData; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.math.NumberUtils; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; @@ -24,18 +23,23 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +/** + * Provider for CSV files + */ public class Csv extends Provider { private static final long serialVersionUID = 9141170397250309265L; private static final String ID = "csv"; + /** Default constructor */ @DataBoundConstructor public Csv() { super(); // empty constructor required for stapler } + /** Creates a CSV parser */ @Override public ReportParser createParser() { if (getActualId().equals(getDescriptor().getId())) { @@ -45,41 +49,45 @@ public ReportParser createParser() { return new CsvCustomParser(getActualId()); } - /** Descriptor for this provider. */ + /** Descriptor for this provider */ @Symbol("csv") @Extension - public static class Descriptor extends Provider.ProviderDescriptor { - /** Creates the descriptor instance. */ + public static class Descriptor extends ProviderDescriptor { + /** Creates a descriptor instance */ public Descriptor() { super(ID); } } + /** + * Parser for CSV files + */ public static class CsvCustomParser extends ReportParser { private static final long serialVersionUID = -8689695008930386640L; private final String id; - private List parserMessages; + /** Constructor */ public CsvCustomParser(String id) { super(); this.id = id; this.parserMessages = new ArrayList(); } + /** Returns the parser identifier */ public String getId() { return id; } - + /** Detects the delimiter used in the CSV file */ private char detectDelimiter(File file) throws IOException { // List of possible delimiters char[] delimiters = { ',', ';', '\t', '|' }; int[] delimiterCounts = new int[delimiters.length]; - // Read the lines of the file to detect the delimiter + // Read the file lines to detect the delimiter try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) { int linesToCheck = 5; // Number of lines to check int linesChecked = 0; @@ -105,13 +113,14 @@ private char detectDelimiter(File file) throws IOException { return detectedDelimiter; } - + /** Parses a CSV file and creates a ReportDto */ @Override public ReportDto parse(File file) throws IOException { - // Get delimiter + // Delimiter detection char delimiter = detectDelimiter(file); + // CSV parser configuration final CsvMapper mapper = new CsvMapper(); final CsvSchema schema = mapper.schemaFor(String[].class).withColumnSeparator(delimiter); @@ -121,139 +130,18 @@ public ReportDto parse(File file) throws IOException { mapper.enable(CsvParser.Feature.INSERT_NULLS_FOR_MISSING_COLUMNS); mapper.enable(CsvParser.Feature.TRIM_SPACES); + // Read CSV data final MappingIterator> it = mapper.readerForListOf(String.class) .with(schema) .readValues(file); - ReportDto report = new ReportDto(); - report.setId(getId()); - report.setItems(new ArrayList<>()); - + // Extract headers and rows final List header = it.next(); final List> rows = it.readAll(); - int rowCount = 0; - final int headerColumnCount = header.size(); - int colIdxValueStart = 0; - - if (headerColumnCount >= 2) { - rowCount = rows.size(); - } else { - parserMessages.add(String.format("skipped file - First line has %d elements", headerColumnCount + 1)); - } - - /** Parse all data rows */ - for (int rowIdx = 0; rowIdx < rowCount; rowIdx++) { - String parentId = "report"; - List row = rows.get(rowIdx); - Item last = null; - boolean lastItemAdded = false; - LinkedHashMap result = new LinkedHashMap<>(); - boolean emptyFieldFound = false; - int rowSize = row.size(); - - /** Parse untill first data line is found to get data and value field */ - if (colIdxValueStart == 0) { - /** Col 0 is assumed to be string */ - for (int colIdx = rowSize - 1; colIdx > 1; colIdx--) { - String value = row.get(colIdx); - - if (NumberUtils.isCreatable(value)) { - colIdxValueStart = colIdx; - } else { - if (colIdxValueStart > 0) { - parserMessages - .add(String.format("Found data - fields number = %d - numeric fields = %d", - colIdxValueStart, rowSize - colIdxValueStart)); - } - break; - } - } - } - - String valueId = ""; - /** Parse line if first data line is OK and line has more element than header */ - if ((colIdxValueStart > 0) && (rowSize >= headerColumnCount)) { - /** Check line and header size matching */ - for (int colIdx = 0; colIdx < headerColumnCount; colIdx++) { - String id = header.get(colIdx); - String value = row.get(colIdx); - - /** Check value fields */ - if ((colIdx < colIdxValueStart)) { - /** Test if text item is a value or empty */ - if ((NumberUtils.isCreatable(value)) || (StringUtils.isBlank(value))) { - /** Empty field found - message */ - if (colIdx == 0) { - parserMessages - .add(String.format("skipped line %d - First column item empty - col = %d ", - rowIdx + 2, colIdx + 1)); - break; - } else { - emptyFieldFound = true; - /** Continue next column parsing */ - continue; - } - } else { - /** Check if field values are present after empty cells */ - if (emptyFieldFound) { - parserMessages.add(String.format("skipped line %d Empty field in col = %d ", - rowIdx + 2, colIdx + 1)); - break; - } - } - valueId += value; - Optional parent = report.findItem(parentId, report.getItems()); - Item item = new Item(); - lastItemAdded = false; - item.setId(valueId); - item.setName(value); - String finalValueId = valueId; - if (parent.isPresent()) { - Item p = parent.get(); - if (!p.hasItems()) { - p.setItems(new ArrayList<>()); - } - if (p.getItems().stream().noneMatch(i -> i.getId().equals(finalValueId))) { - p.addItem(item); - lastItemAdded = true; - } - } else { - if (report.getItems().stream().noneMatch(i -> i.getId().equals(finalValueId))) { - report.getItems().add(item); - lastItemAdded = true; - } - } - parentId = valueId; - last = item; - } else { - Number val = 0; - if (NumberUtils.isCreatable(value)) { - val = NumberUtils.createNumber(value); - } - result.put(id, val.intValue()); - } - } - } else { - /** Skip file if first data line has no value field */ - if (colIdxValueStart == 0) { - parserMessages.add(String.format("skipped line %d - First data row not found", rowIdx + 2)); - continue; - } else { - parserMessages - .add(String.format("skipped line %d - line has fewer element than title", rowIdx + 2)); - continue; - } - } - /** If last item was created, it will be added to report */ - if (lastItemAdded) { - last.setResult(result); - } else { - parserMessages.add(String.format("ignored line %d - Same fields already exists", rowIdx + 2)); - } - } - // report.setParserLog(parserMessages); - return report; + // Create TabularData object and process it + TabularData tabularData = new TabularData(id, header, rows); + return tabularData.processData(parserMessages); } } -} \ No newline at end of file +} diff --git a/src/main/java/io/jenkins/plugins/reporter/provider/Excel.java b/src/main/java/io/jenkins/plugins/reporter/provider/Excel.java new file mode 100644 index 00000000..ed86c667 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/reporter/provider/Excel.java @@ -0,0 +1,105 @@ +package io.jenkins.plugins.reporter.provider; + +import hudson.Extension; +import io.jenkins.plugins.reporter.Messages; +import io.jenkins.plugins.reporter.model.ReportDto; +import io.jenkins.plugins.reporter.model.ReportParser; +import io.jenkins.plugins.reporter.util.TabularData; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.List; + +/** + * Provider for Excel (XLSX) files + */ +public class Excel extends AbstractExcelProvider { + + private static final long serialVersionUID = 9141170397250309265L; + + private static final String ID = "excel"; + + /** Default constructor */ + @DataBoundConstructor + public Excel() { + super(); + // empty constructor required for stapler + } + + /** Creates an Excel parser */ + @Override + public ReportParser createParser() { + if (getActualId().equals(getDescriptor().getId())) { + throw new IllegalArgumentException(Messages.Provider_Error()); + } + + return new ExcelParser(getActualId()); + } + + /** Descriptor for this provider */ + @Symbol("excel") + @Extension + public static class Descriptor extends ProviderDescriptor { + /** Creates a descriptor instance */ + public Descriptor() { + super(ID); + } + } + + /** + * Parser for Excel files + */ + public static class ExcelParser extends AbstractExcelParser { + + private static final long serialVersionUID = -8689695008930386641L; + + /** Constructor */ + public ExcelParser(String id) { + super(id); + } + + /** Parses an Excel file and creates a ReportDto */ + @Override + public ReportDto parse(File file) throws IOException { + try (FileInputStream fis = new FileInputStream(file); + Workbook workbook = new XSSFWorkbook(fis)) { + + TabularData result = null; + List referenceHeader = null; + + // Process all sheets in the workbook + for (int i = 0; i < workbook.getNumberOfSheets(); i++) { + Sheet sheet = workbook.getSheetAt(i); + + // Extract data from the sheet + TabularData sheetData = extractSheetData(sheet, referenceHeader); + + // If the sheet contains valid data + if (sheetData != null) { + // If it's the first valid sheet, use it as reference + if (result == null) { + result = sheetData; + referenceHeader = sheetData.getHeader(); + } else { + // Otherwise, add the sheet rows to the result + result.getRows().addAll(sheetData.getRows()); + } + } + } + + // If no sheet contains valid data + if (result == null) { + throw new IOException("No valid data found in Excel file"); + } + + // Process tabular data + return result.processData(parserMessages); + } + } + } +} diff --git a/src/main/java/io/jenkins/plugins/reporter/provider/ExcelMulti.java b/src/main/java/io/jenkins/plugins/reporter/provider/ExcelMulti.java new file mode 100644 index 00000000..88b65edd --- /dev/null +++ b/src/main/java/io/jenkins/plugins/reporter/provider/ExcelMulti.java @@ -0,0 +1,150 @@ +package io.jenkins.plugins.reporter.provider; + +import hudson.Extension; +import io.jenkins.plugins.reporter.Messages; +import io.jenkins.plugins.reporter.model.ReportDto; +import io.jenkins.plugins.reporter.model.ReportParser; +import io.jenkins.plugins.reporter.util.TabularData; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Provider for multi-sheet Excel (XLSX) files with header consistency enforcement + * This provider parses all sheets in an Excel file, enforcing header consistency across sheets. + * Only sheets with identical headers (compared to the first valid sheet) will be processed. + */ +public class ExcelMulti extends AbstractExcelProvider { + + private static final long serialVersionUID = 1845129735392309267L; + + private static final String ID = "excelMulti"; + + /** Default constructor */ + @DataBoundConstructor + public ExcelMulti() { + super(); + // empty constructor required for stapler + } + + /** Creates an ExcelMulti parser */ + @Override + public ReportParser createParser() { + if (getActualId().equals(getDescriptor().getId())) { + throw new IllegalArgumentException(Messages.Provider_Error()); + } + + return new ExcelMultiParser(getActualId()); + } + + /** Descriptor for this provider */ + @Symbol("excelMulti") + @Extension + public static class Descriptor extends ProviderDescriptor { + /** Creates a descriptor instance */ + public Descriptor() { + super(ID); + } + } + + /** + * Parser for multi-sheet Excel files with header consistency enforcement + */ + public static class ExcelMultiParser extends AbstractExcelParser { + + private static final long serialVersionUID = 3724789235028726491L; + + /** Constructor */ + public ExcelMultiParser(String id) { + super(id); + } + + /** + * Parses an Excel file with multiple sheets and creates a ReportDto + * Only processes sheets with consistent headers (identical to the first valid sheet) + */ + @Override + public ReportDto parse(File file) throws IOException { + try (FileInputStream fis = new FileInputStream(file); + Workbook workbook = new XSSFWorkbook(fis)) { + + TabularData result = null; + List referenceHeader = null; + int totalSheets = workbook.getNumberOfSheets(); + int processedSheets = 0; + int skippedSheets = 0; + List processedSheetNames = new ArrayList<>(); + List skippedSheetNames = new ArrayList<>(); + List> allRows = new ArrayList<>(); + + // Add an initial parser message about number of sheets + parserMessages.add(String.format("Excel file contains %d sheets", totalSheets)); + + // First pass: validate sheets and collect all rows + for (int i = 0; i < totalSheets; i++) { + Sheet sheet = workbook.getSheetAt(i); + String sheetName = sheet.getSheetName(); + + // Extract data from the sheet + TabularData sheetData = extractSheetData(sheet, referenceHeader); + + // If the sheet contains valid data + if (sheetData != null) { + // If it's the first valid sheet, use it as reference + if (referenceHeader == null) { + referenceHeader = sheetData.getHeader(); + allRows.addAll(sheetData.getRows()); + processedSheets++; + processedSheetNames.add(sheetName); + parserMessages.add(String.format("Processing sheet '%s' as reference sheet with %d columns: %s", + sheetName, referenceHeader.size(), String.join(", ", referenceHeader))); + } else { + // For subsequent sheets, add rows to the collection + allRows.addAll(sheetData.getRows()); + processedSheets++; + processedSheetNames.add(sheetName); + parserMessages.add(String.format("Processing sheet '%s' - adding %d rows", + sheetName, sheetData.getRows().size())); + } + } else { + skippedSheets++; + skippedSheetNames.add(sheetName); + parserMessages.add(String.format("Skipped sheet '%s' - Invalid format or no data", sheetName)); + } + } + + // Create final TabularData with all rows + if (referenceHeader != null && !allRows.isEmpty()) { + result = new TabularData(getId(), referenceHeader, allRows); + } + + // Add summary information to parser messages + parserMessages.add(String.format("Processed %d sheets: %s", + processedSheets, String.join(", ", processedSheetNames))); + + if (skippedSheets > 0) { + parserMessages.add(String.format("Skipped %d sheets: %s", + skippedSheets, String.join(", ", skippedSheetNames))); + } + + // If no sheet contains valid data + if (result == null) { + throw new IOException("No valid data found in Excel file. All sheets were skipped."); + } + + parserMessages.add(String.format("Total rows collected: %d", allRows.size())); + + // Process and return the final tabular data + return result.processData(parserMessages); + } + } + } +} + diff --git a/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java b/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java new file mode 100644 index 00000000..f1817c21 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java @@ -0,0 +1,192 @@ +package io.jenkins.plugins.reporter.util; + +import io.jenkins.plugins.reporter.model.Item; +import io.jenkins.plugins.reporter.model.ReportDto; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; + +import java.io.Serializable; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Utility class for tabular data processing + * Combines data storage and processing functionality + */ +public class TabularData implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String sourceId; + private final List header; + private final List> rows; + + /** Constructor */ + public TabularData(String sourceId, List header, List> rows) { + this.sourceId = sourceId; + this.header = header; + this.rows = rows; + } + + /** Returns the list of column headers */ + public List getHeader() { + return header; + } + + /** Returns the list of data rows */ + public List> getRows() { + List> rowsCopy = new ArrayList<>(); + for (List row : rows) { + rowsCopy.add(new ArrayList<>(row)); + } + return rowsCopy; + } + + /** + * Process tabular data to create a ReportDto + * @param parserMessages List to store parser messages + * @return ReportDto containing the processed data + */ + public ReportDto processData(List parserMessages) { + // Create the report + ReportDto report = new ReportDto(); + report.setId(sourceId); + report.setItems(new ArrayList<>()); + + int rowCount = 0; + final int headerColumnCount = header.size(); + + // First two columns are always category columns, rest are value columns + final int categoryColumns = 2; + final int valueColumns = headerColumnCount - categoryColumns; + + if (headerColumnCount >= 2) { + rowCount = rows.size(); + parserMessages.add(String.format("Processing data with %d rows, %d category columns, %d value columns", + rowCount, categoryColumns, valueColumns)); + parserMessages.add(String.format("Headers: %s", String.join(", ", header))); + } else { + parserMessages.add(String.format("skipped file - First line has %d elements", headerColumnCount)); + return report; + } + + /** Parse all data rows */ + for (int rowIdx = 0; rowIdx < rowCount; rowIdx++) { + List row = rows.get(rowIdx); + + // Add debug info for first and last row + if (rowIdx == 0 || rowIdx == rows.size() - 1) { // Only log first and last row for brevity + parserMessages.add(String.format("Processing row %d: %s", + rowIdx + 1, + String.join(", ", row.subList(0, Math.min(row.size(), 5))) + + (row.size() > 5 ? "..." : ""))); + } + + if (row.size() < headerColumnCount) { + parserMessages.add(String.format("skipped line %d - line has fewer elements than header", rowIdx + 2)); + continue; + } + + // Process category columns + String[] categories = new String[categoryColumns]; + boolean validRow = true; + for (int i = 0; i < categoryColumns; i++) { + categories[i] = row.get(i).trim(); + if (categories[i].isEmpty()) { + validRow = false; + break; + } + } + + if (!validRow) { + parserMessages.add(String.format("skipped line %d - empty category", rowIdx + 2)); + continue; + } + + // Process each category level + Item currentItem = null; + String parentId = "report"; + + for (int level = 0; level < categories.length; level++) { + String categoryName = categories[level]; + String categoryId = level == 0 ? categoryName : parentId + categoryName; + + if (level == 0) { + // Find or create root level item + Optional existing = report.getItems().stream() + .filter(i -> i.getName().equals(categoryName)) + .findFirst(); + + if (existing.isPresent()) { + currentItem = existing.get(); + } else { + currentItem = new Item(); + currentItem.setId(categoryId); + currentItem.setName(categoryName); + report.getItems().add(currentItem); + parserMessages.add(String.format("Created new root item: %s", categoryName)); + } + } else { + // Find or create sub-item + if (!currentItem.hasItems()) { + currentItem.setItems(new ArrayList<>()); + } + + Optional existing = currentItem.getItems().stream() + .filter(i -> i.getName().equals(categoryName)) + .findFirst(); + + if (existing.isPresent()) { + currentItem = existing.get(); + } else { + Item newItem = new Item(); + newItem.setId(categoryId); + newItem.setName(categoryName); + currentItem.getItems().add(newItem); + currentItem = newItem; + parserMessages.add(String.format("Created new sub-item: %s under %s", + categoryName, categories[level-1])); + } + } + parentId = categoryId; + } + + // Process value columns + if (currentItem != null) { + LinkedHashMap values = new LinkedHashMap<>(); + for (int i = categoryColumns; i < headerColumnCount && i < row.size(); i++) { + String headerName = header.get(i); + String value = row.get(i); + int numericValue = NumberUtils.isCreatable(value) ? + NumberUtils.createNumber(value).intValue() : 0; + values.put(headerName, numericValue); + } + + if (currentItem.getResult() == null) { + currentItem.setResult(values); + } else { + // Merge values with existing results + LinkedHashMap existing = currentItem.getResult(); + for (Map.Entry entry : values.entrySet()) { + existing.merge(entry.getKey(), entry.getValue(), Integer::sum); + } + } + + parserMessages.add(String.format("Processed %s: %s", currentItem.getName(), + values.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining(", ")))); + } + } + + // Add debug info about final report structure + parserMessages.add(String.format("Final report contains %d root items", report.getItems().size())); + for (Item item : report.getItems()) { + parserMessages.add(String.format("Root item: %s with %d subitems", + item.getName(), + item.hasItems() ? item.getItems().size() : 0)); + } + + return report; + } +} diff --git a/src/main/webapp/.DS_Store b/src/main/webapp/.DS_Store deleted file mode 100644 index a9ed938a..00000000 Binary files a/src/main/webapp/.DS_Store and /dev/null differ diff --git a/src/test/java/io/jenkins/plugins/reporter/provider/CreateExcelMultiSample.java b/src/test/java/io/jenkins/plugins/reporter/provider/CreateExcelMultiSample.java new file mode 100644 index 00000000..32a67181 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/reporter/provider/CreateExcelMultiSample.java @@ -0,0 +1,233 @@ +package io.jenkins.plugins.reporter.provider; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +/** + * Utility class to generate multi-tab Excel test files for testing the ExcelMulti provider functionality. + */ +public class CreateExcelMultiSample { + + public static void main(String[] args) throws IOException { + // Create test files in the resources directory + String resourcesDir = "src/test/resources"; + + // Create multi-sheet Excel file with consistent headers + createConsistentHeadersExcelFile(new File(resourcesDir, "test-excel-multi-consistent.xlsx")); + + // Create multi-sheet Excel file with inconsistent headers + createInconsistentHeadersExcelFile(new File(resourcesDir, "test-excel-multi-inconsistent.xlsx")); + + // Create multi-sheet Excel file with mixed validity + createMixedValidityExcelFile(new File(resourcesDir, "test-excel-multi-mixed.xlsx")); + + System.out.println("Multi-tab test Excel files created successfully in " + resourcesDir); + } + + /** + * Creates a multi-sheet Excel file with consistent headers across all sheets. + */ + public static void createConsistentHeadersExcelFile(File file) throws IOException { + Workbook workbook = new XSSFWorkbook(); + + // Create first sheet + Sheet sheet1 = workbook.createSheet("Sheet 1"); + + // Create header row + Row headerRow1 = sheet1.createRow(0); + headerRow1.createCell(0).setCellValue("Category"); + headerRow1.createCell(1).setCellValue("Subcategory"); + headerRow1.createCell(2).setCellValue("Value1"); + headerRow1.createCell(3).setCellValue("Value2"); + + // Create data rows + Row dataRow1 = sheet1.createRow(1); + dataRow1.createCell(0).setCellValue("Category A"); + dataRow1.createCell(1).setCellValue("Subcat A1"); + dataRow1.createCell(2).setCellValue(10); + dataRow1.createCell(3).setCellValue(20); + + Row dataRow2 = sheet1.createRow(2); + dataRow2.createCell(0).setCellValue("Category A"); + dataRow2.createCell(1).setCellValue("Subcat A2"); + dataRow2.createCell(2).setCellValue(15); + dataRow2.createCell(3).setCellValue(25); + + // Create second sheet with the same headers + Sheet sheet2 = workbook.createSheet("Sheet 2"); + + // Create identical header row + Row headerRow2 = sheet2.createRow(0); + headerRow2.createCell(0).setCellValue("Category"); + headerRow2.createCell(1).setCellValue("Subcategory"); + headerRow2.createCell(2).setCellValue("Value1"); + headerRow2.createCell(3).setCellValue("Value2"); + + // Create data rows with different data + Row dataRow3 = sheet2.createRow(1); + dataRow3.createCell(0).setCellValue("Category B"); + dataRow3.createCell(1).setCellValue("Subcat B1"); + dataRow3.createCell(2).setCellValue(30); + dataRow3.createCell(3).setCellValue(40); + + Row dataRow4 = sheet2.createRow(2); + dataRow4.createCell(0).setCellValue("Category B"); + dataRow4.createCell(1).setCellValue("Subcat B2"); + dataRow4.createCell(2).setCellValue(35); + dataRow4.createCell(3).setCellValue(45); + + // Create third sheet with the same headers + Sheet sheet3 = workbook.createSheet("Sheet 3"); + + // Create identical header row + Row headerRow3 = sheet3.createRow(0); + headerRow3.createCell(0).setCellValue("Category"); + headerRow3.createCell(1).setCellValue("Subcategory"); + headerRow3.createCell(2).setCellValue("Value1"); + headerRow3.createCell(3).setCellValue("Value2"); + + // Create data rows with different data + Row dataRow5 = sheet3.createRow(1); + dataRow5.createCell(0).setCellValue("Category C"); + dataRow5.createCell(1).setCellValue("Subcat C1"); + dataRow5.createCell(2).setCellValue(50); + dataRow5.createCell(3).setCellValue(60); + + // Write to file + try (FileOutputStream fileOut = new FileOutputStream(file)) { + workbook.write(fileOut); + } + workbook.close(); + } + + /** + * Creates a multi-sheet Excel file with inconsistent headers across sheets. + */ + public static void createInconsistentHeadersExcelFile(File file) throws IOException { + Workbook workbook = new XSSFWorkbook(); + + // Create first sheet + Sheet sheet1 = workbook.createSheet("Sheet 1"); + + // Create header row + Row headerRow1 = sheet1.createRow(0); + headerRow1.createCell(0).setCellValue("Category"); + headerRow1.createCell(1).setCellValue("Subcategory"); + headerRow1.createCell(2).setCellValue("Value1"); + headerRow1.createCell(3).setCellValue("Value2"); + + // Create data rows + Row dataRow1 = sheet1.createRow(1); + dataRow1.createCell(0).setCellValue("Category A"); + dataRow1.createCell(1).setCellValue("Subcat A1"); + dataRow1.createCell(2).setCellValue(10); + dataRow1.createCell(3).setCellValue(20); + + // Create second sheet with different headers + Sheet sheet2 = workbook.createSheet("Sheet 2"); + + // Create different header row (different column order) + Row headerRow2 = sheet2.createRow(0); + headerRow2.createCell(0).setCellValue("Category"); + headerRow2.createCell(1).setCellValue("Value1"); // Swapped order + headerRow2.createCell(2).setCellValue("Subcategory"); + headerRow2.createCell(3).setCellValue("Value2"); + + // Create data rows + Row dataRow2 = sheet2.createRow(1); + dataRow2.createCell(0).setCellValue("Category B"); + dataRow2.createCell(1).setCellValue(30); + dataRow2.createCell(2).setCellValue("Subcat B1"); + dataRow2.createCell(3).setCellValue(40); + + // Create third sheet with different headers + Sheet sheet3 = workbook.createSheet("Sheet 3"); + + // Create different header row (different column names) + Row headerRow3 = sheet3.createRow(0); + headerRow3.createCell(0).setCellValue("Group"); // Different name + headerRow3.createCell(1).setCellValue("Subcategory"); + headerRow3.createCell(2).setCellValue("Score1"); // Different name + headerRow3.createCell(3).setCellValue("Score2"); // Different name + + // Create data rows + Row dataRow3 = sheet3.createRow(1); + dataRow3.createCell(0).setCellValue("Category C"); + dataRow3.createCell(1).setCellValue("Subcat C1"); + dataRow3.createCell(2).setCellValue(50); + dataRow3.createCell(3).setCellValue(60); + + // Write to file + try (FileOutputStream fileOut = new FileOutputStream(file)) { + workbook.write(fileOut); + } + workbook.close(); + } + + /** + * Creates a multi-sheet Excel file with mixed validity (some valid sheets, some invalid). + */ + public static void createMixedValidityExcelFile(File file) throws IOException { + Workbook workbook = new XSSFWorkbook(); + + // Create first sheet (valid) + Sheet sheet1 = workbook.createSheet("Valid Sheet 1"); + + // Create header row + Row headerRow1 = sheet1.createRow(0); + headerRow1.createCell(0).setCellValue("Category"); + headerRow1.createCell(1).setCellValue("Subcategory"); + headerRow1.createCell(2).setCellValue("Value1"); + headerRow1.createCell(3).setCellValue("Value2"); + + // Create data rows + Row dataRow1 = sheet1.createRow(1); + dataRow1.createCell(0).setCellValue("Category A"); + dataRow1.createCell(1).setCellValue("Subcat A1"); + dataRow1.createCell(2).setCellValue(10); + dataRow1.createCell(3).setCellValue(20); + + // Create second sheet (empty - invalid) + workbook.createSheet("Empty Sheet"); + + // Create third sheet (valid - same header) + Sheet sheet3 = workbook.createSheet("Valid Sheet 2"); + + // Create header row (same as first sheet) + Row headerRow3 = sheet3.createRow(0); + headerRow3.createCell(0).setCellValue("Category"); + headerRow3.createCell(1).setCellValue("Subcategory"); + headerRow3.createCell(2).setCellValue("Value1"); + headerRow3.createCell(3).setCellValue("Value2"); + + // Create data rows + Row dataRow3 = sheet3.createRow(1); + dataRow3.createCell(0).setCellValue("Category C"); + dataRow3.createCell(1).setCellValue("Subcat C1"); + dataRow3.createCell(2).setCellValue(50); + dataRow3.createCell(3).setCellValue(60); + + // Create fourth sheet (invalid - fewer columns) + Sheet sheet4 = workbook.createSheet("Invalid Sheet - Fewer Columns"); + + // Create header row with fewer columns + Row headerRow4 = sheet4.createRow(0); + headerRow4.createCell(0).setCellValue("Category"); + headerRow4.createCell(1).setCellValue("Value1"); + + // Create data row + Row dataRow4 = sheet4.createRow(1); + dataRow4.createCell(0).setCellValue("Category D"); + dataRow4.createCell(1).setCellValue(70); + + // Write to file + try (FileOutputStream fileOut = new FileOutputStream(file)) { + workbook.write(fileOut); + } + workbook.close(); + } +} + diff --git a/src/test/java/io/jenkins/plugins/reporter/provider/CreateExcelSample.java b/src/test/java/io/jenkins/plugins/reporter/provider/CreateExcelSample.java new file mode 100644 index 00000000..255d8cac --- /dev/null +++ b/src/test/java/io/jenkins/plugins/reporter/provider/CreateExcelSample.java @@ -0,0 +1,64 @@ +package io.jenkins.plugins.reporter.provider; + +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Utilitaire pour créer un fichier Excel de test dans le répertoire etc/ + */ +public class CreateExcelSample { + + public static void main(String[] args) { + String filePath = "/home/ubuntu/workspace/nested-data-reporting-plugin/etc/report.xlsx"; + + try { + // Créer un nouveau classeur Excel + Workbook workbook = new XSSFWorkbook(); + + // Créer une feuille + Sheet sheet = workbook.createSheet("Sample Data"); + + // Créer l'en-tête + Row headerRow = sheet.createRow(0); + headerRow.createCell(0).setCellValue("Category"); + headerRow.createCell(1).setCellValue("Subcategory"); + headerRow.createCell(2).setCellValue("Value1"); + headerRow.createCell(3).setCellValue("Value2"); + + // Créer les données + Row row1 = sheet.createRow(1); + row1.createCell(0).setCellValue("Category A"); + row1.createCell(1).setCellValue(""); + row1.createCell(2).setCellValue(10); + row1.createCell(3).setCellValue(20); + + Row row2 = sheet.createRow(2); + row2.createCell(0).setCellValue("Category B"); + row2.createCell(1).setCellValue(""); + row2.createCell(2).setCellValue(30); + row2.createCell(3).setCellValue(40); + + Row row3 = sheet.createRow(3); + row3.createCell(0).setCellValue("Category C"); + row3.createCell(1).setCellValue(""); + row3.createCell(2).setCellValue(50); + row3.createCell(3).setCellValue(60); + + // Écrire dans le fichier + try (FileOutputStream fileOut = new FileOutputStream(filePath)) { + workbook.write(fileOut); + } + + // Fermer le classeur + workbook.close(); + + System.out.println("Fichier Excel créé avec succès à " + filePath); + + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/test/java/io/jenkins/plugins/reporter/provider/ExcelMultiTest.java b/src/test/java/io/jenkins/plugins/reporter/provider/ExcelMultiTest.java new file mode 100644 index 00000000..c406f578 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/reporter/provider/ExcelMultiTest.java @@ -0,0 +1,358 @@ +package io.jenkins.plugins.reporter.provider; + +import io.jenkins.plugins.reporter.model.Item; +import io.jenkins.plugins.reporter.model.ReportDto; +import io.jenkins.plugins.reporter.model.ReportParser; +import org.apache.poi.ss.usermodel.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import static org.junit.Assert.*; + +/** + * Tests for the ExcelMulti provider class. + * This test suite validates the functionality of the ExcelMulti provider, + * which handles multi-sheet Excel files with header consistency enforcement. + */ +public class ExcelMultiTest { + + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + private File consistentHeadersFile; + private File inconsistentHeadersFile; + private File mixedValidityFile; + private ExcelMulti provider; + + /** + * Set up the test files and provider before each test. + */ + @Before + public void setUp() throws IOException { + // Generate the test files if they don't exist + String resourcesDir = "src/test/resources"; + consistentHeadersFile = new File(resourcesDir, "test-excel-multi-consistent.xlsx"); + inconsistentHeadersFile = new File(resourcesDir, "test-excel-multi-inconsistent.xlsx"); + mixedValidityFile = new File(resourcesDir, "test-excel-multi-mixed.xlsx"); + + if (!consistentHeadersFile.exists()) { + CreateExcelMultiSample.createConsistentHeadersExcelFile(consistentHeadersFile); + } + + if (!inconsistentHeadersFile.exists()) { + CreateExcelMultiSample.createInconsistentHeadersExcelFile(inconsistentHeadersFile); + } + + if (!mixedValidityFile.exists()) { + CreateExcelMultiSample.createMixedValidityExcelFile(mixedValidityFile); + } + + // Create and configure the provider + provider = new ExcelMulti(); + provider.setId("test-excel-multi"); + + // Add debug logging in setUp + if (consistentHeadersFile.exists()) { + System.out.println("Test file exists: " + consistentHeadersFile.getAbsolutePath()); + System.out.println("File size: " + consistentHeadersFile.length() + " bytes"); + } else { + System.out.println("Test file does not exist: " + consistentHeadersFile.getAbsolutePath()); + } + + if (mixedValidityFile.exists()) { + System.out.println("Test file exists: " + mixedValidityFile.getAbsolutePath()); + System.out.println("File size: " + mixedValidityFile.length() + " bytes"); + } else { + System.out.println("Test file does not exist: " + mixedValidityFile.getAbsolutePath()); + } + + // Debug Excel structure + try (Workbook workbook = WorkbookFactory.create(consistentHeadersFile)) { + System.out.println("=== Excel Structure Debug (consistentHeadersFile) ==="); + System.out.println("Number of sheets: " + workbook.getNumberOfSheets()); + for (int i = 0; i < workbook.getNumberOfSheets(); i++) { + Sheet sheet = workbook.getSheetAt(i); + System.out.println("Sheet " + i + ": " + sheet.getSheetName()); + Row headerRow = sheet.getRow(0); + if (headerRow != null) { + System.out.println(" Headers:"); + for (Cell cell : headerRow) { + System.out.println(" - " + cell.getStringCellValue()); + } + } + } + System.out.println("========================="); + } catch (Exception e) { + System.out.println("Error reading Excel structure: " + e.getMessage()); + } + + try (Workbook workbook = WorkbookFactory.create(mixedValidityFile)) { + System.out.println("=== Excel Structure Debug (mixedValidityFile) ==="); + System.out.println("Number of sheets: " + workbook.getNumberOfSheets()); + for (int i = 0; i < workbook.getNumberOfSheets(); i++) { + Sheet sheet = workbook.getSheetAt(i); + System.out.println("Sheet " + i + ": " + sheet.getSheetName()); + Row headerRow = sheet.getRow(0); + if (headerRow != null) { + System.out.println(" Headers:"); + for (Cell cell : headerRow) { + System.out.println(" - " + cell.getStringCellValue()); + } + } else { + System.out.println(" No header row found"); + } + } + System.out.println("========================="); + } catch (Exception e) { + System.out.println("Error reading Excel structure: " + e.getMessage()); + } + } + + /** + * Test the basic functionality of the ExcelMulti provider. + * This test verifies that the provider can parse a simple multi-sheet Excel file + * with consistent headers across all sheets. + */ + @Test + public void testBasicFunctionality() throws IOException { + // Create the parser + ReportParser parser = provider.createParser(); + assertNotNull("Parser should not be null", parser); + + // Parse the file with consistent headers + ReportDto report = parser.parse(consistentHeadersFile); + + // Verify the report is correctly parsed + assertNotNull("Report should not be null", report); + assertEquals("Report ID should match provider ID", "test-excel-multi", report.getId()); + + // Check that we have items + List items = report.getItems(); + assertNotNull("Items list should not be null", items); + assertFalse("Items list should not be empty", items.isEmpty()); + } + + /** + * Test that data from all sheets with consistent headers is aggregated. + * This test verifies that the ExcelMulti provider correctly aggregates data + * from all sheets that have consistent headers. + */ + @Test + public void testDataAggregation() throws IOException { + // Create the parser + ReportParser parser = provider.createParser(); + + // Parse the file with consistent headers + ReportDto report = parser.parse(consistentHeadersFile); + + // Verify data from all sheets is included + List items = report.getItems(); + + // Debug logging + System.out.println("=== Data Aggregation Test Debug ==="); + System.out.println("Total items found: " + items.size()); + for (Item item : items) { + System.out.println("Found category: " + item.getName()); + if (item.getItems() != null) { + for (Item subItem : item.getItems()) { + System.out.println(" - Subcategory: " + subItem.getName()); + } + } + } + System.out.println("================================"); + + // Find items from different sheets + Optional categoryA = items.stream() + .filter(item -> item.getName().equals("Category A")) + .findFirst(); + + Optional categoryB = items.stream() + .filter(item -> item.getName().equals("Category B")) + .findFirst(); + + Optional categoryC = items.stream() + .filter(item -> item.getName().equals("Category C")) + .findFirst(); + + // Verify all categories are present (from different sheets) + assertTrue("Category A should be present", categoryA.isPresent()); + assertTrue("Category B should be present", categoryB.isPresent()); + assertTrue("Category C should be present", categoryC.isPresent()); + + // Verify subcategories for Category A + if (categoryA.isPresent()) { + List subcategoriesA = categoryA.get().getItems(); + assertNotNull("Subcategories for Category A should not be null", subcategoriesA); + assertEquals("Category A should have 2 subcategories", 2, subcategoriesA.size()); + + // Check subcategory names + assertEquals("First subcategory of A should be Subcat A1", + "Subcat A1", subcategoriesA.get(0).getName()); + assertEquals("Second subcategory of A should be Subcat A2", + "Subcat A2", subcategoriesA.get(1).getName()); + } + + // Verify subcategories for Category B + if (categoryB.isPresent()) { + List subcategoriesB = categoryB.get().getItems(); + assertNotNull("Subcategories for Category B should not be null", subcategoriesB); + assertEquals("Category B should have 2 subcategories", 2, subcategoriesB.size()); + + // Check subcategory names + assertEquals("First subcategory of B should be Subcat B1", + "Subcat B1", subcategoriesB.get(0).getName()); + assertEquals("Second subcategory of B should be Subcat B2", + "Subcat B2", subcategoriesB.get(1).getName()); + } + } + + /** + * Test that the ExcelMulti provider correctly enforces header consistency. + * This test verifies that only sheets with consistent headers are processed. + */ + @Test + public void testHeaderConsistencyValidation() throws IOException { + // Create the parser and make it an instance of ExcelMultiParser to access parser messages + ExcelMulti.ExcelMultiParser parser = (ExcelMulti.ExcelMultiParser) provider.createParser(); + + // Parse the file with inconsistent headers + ReportDto report = parser.parse(inconsistentHeadersFile); + + // Verify only data from the first sheet is included (as other sheets have inconsistent headers) + List items = report.getItems(); + + // There should only be Category A (from first sheet) + Optional categoryA = items.stream() + .filter(item -> item.getName().equals("Category A")) + .findFirst(); + + Optional categoryB = items.stream() + .filter(item -> item.getName().equals("Category B")) + .findFirst(); + + Optional categoryC = items.stream() + .filter(item -> item.getName().equals("Category C")) + .findFirst(); + + // Verify only Category A is present (as other sheets have inconsistent headers) + assertTrue("Category A should be present", categoryA.isPresent()); + assertFalse("Category B should not be present", categoryB.isPresent()); + assertFalse("Category C should not be present", categoryC.isPresent()); + } + + /** + * Test that the ExcelMulti provider correctly handles invalid sheets. + * This test verifies that invalid sheets are skipped and valid sheets are processed. + */ + @Test + public void testHandlingInvalidSheets() throws IOException { + // Create the parser + ReportParser parser = provider.createParser(); + + // Parse the file with mixed validity + ReportDto report = parser.parse(mixedValidityFile); + + // Verify only data from valid sheets is included + List items = report.getItems(); + + // Debug logging + System.out.println("=== Invalid Sheets Test Debug ==="); + System.out.println("Total items found: " + items.size()); + for (Item item : items) { + System.out.println("Found category: " + item.getName()); + if (item.getItems() != null) { + for (Item subItem : item.getItems()) { + System.out.println(" - Subcategory: " + subItem.getName()); + } + } + } + System.out.println("================================"); + + // There should be Categories A and C (from valid sheets) + Optional categoryA = items.stream() + .filter(item -> item.getName().equals("Category A")) + .findFirst(); + + Optional categoryC = items.stream() + .filter(item -> item.getName().equals("Category C")) + .findFirst(); + + Optional categoryD = items.stream() + .filter(item -> item.getName().equals("Category D")) + .findFirst(); + + // Verify only Categories A and C are present (from valid sheets) + assertTrue("Category A should be present", categoryA.isPresent()); + assertTrue("Category C should be present", categoryC.isPresent()); + assertFalse("Category D should not be present", categoryD.isPresent()); + } + + /** + * Test that the ExcelMulti provider generates appropriate parser messages. + * This test verifies that detailed messages are generated for skipped sheets. + */ + @Test + public void testParserMessages() throws IOException { + // Create the parser and make it an instance of ExcelMultiParser to access parser messages + ExcelMulti.ExcelMultiParser parser = (ExcelMulti.ExcelMultiParser) provider.createParser(); + + // Parse the file with mixed validity + parser.parse(mixedValidityFile); + + // Get parser messages + List messages = parser.parserMessages; + + // Verify that parser messages were generated + assertFalse("Parser messages should not be empty", messages.isEmpty()); + + // Check for expected messages + boolean foundInitialMessage = false; + boolean foundSkippedEmptySheet = false; + boolean foundSkippedFewerColumnsSheet = false; + boolean foundSummaryMessage = false; + + for (String message : messages) { + if (message.contains("Excel file contains")) { + foundInitialMessage = true; + } + if (message.contains("Empty Sheet")) { + foundSkippedEmptySheet = true; + } + if (message.contains("Invalid Sheet - Fewer Columns")) { + foundSkippedFewerColumnsSheet = true; + } + if (message.contains("Skipped") && message.contains("sheets:")) { + foundSummaryMessage = true; + } + } + + assertTrue("Should have initial message about sheet count", foundInitialMessage); + assertTrue("Should have message about skipped empty sheet", foundSkippedEmptySheet); + assertTrue("Should have message about skipped sheet with fewer columns", foundSkippedFewerColumnsSheet); + assertTrue("Should have summary message about skipped sheets", foundSummaryMessage); + } + + /** + * Test that the ExcelMulti provider correctly handles error cases. + * This test verifies that appropriate exceptions are thrown for invalid files. + */ + @Test(expected = IOException.class) + public void testErrorHandling() throws IOException { + // Create the parser + ReportParser parser = provider.createParser(); + + // Try to parse a non-existent file + File nonExistentFile = new File("non-existent-file.xlsx"); + parser.parse(nonExistentFile); + + // Should throw IOException + } +} + diff --git a/src/test/java/io/jenkins/plugins/reporter/provider/ExcelTestFileGenerator.java b/src/test/java/io/jenkins/plugins/reporter/provider/ExcelTestFileGenerator.java new file mode 100644 index 00000000..a9e12634 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/reporter/provider/ExcelTestFileGenerator.java @@ -0,0 +1,130 @@ +package io.jenkins.plugins.reporter.provider; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +/** + * Utility class to generate test Excel files for testing the Excel parser functionality. + */ +public class ExcelTestFileGenerator { + + public static void main(String[] args) throws IOException { + // Create test files in the resources directory + String resourcesDir = "src/test/resources"; + + // Create normal Excel file + createNormalExcelFile(new File(resourcesDir, "test-excel-normal.xlsx")); + + // Create Excel file with offset header + createOffsetHeaderExcelFile(new File(resourcesDir, "test-excel-offset.xlsx")); + + // Create Excel file with mixed data + createMixedDataExcelFile(new File(resourcesDir, "test-excel-mixed.xlsx")); + + System.out.println("Test Excel files created successfully in " + resourcesDir); + } + + /** + * Creates a normal Excel file with header in the first row and data below. + */ + public static void createNormalExcelFile(File file) throws IOException { + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("Test Sheet"); + + // Create header row + Row headerRow = sheet.createRow(0); + headerRow.createCell(0).setCellValue("Category"); + headerRow.createCell(1).setCellValue("Subcategory"); + headerRow.createCell(2).setCellValue("Value1"); + headerRow.createCell(3).setCellValue("Value2"); + + // Create data rows + Row dataRow1 = sheet.createRow(1); + dataRow1.createCell(0).setCellValue("Category A"); + dataRow1.createCell(1).setCellValue(""); + dataRow1.createCell(2).setCellValue(10); + dataRow1.createCell(3).setCellValue(20); + + Row dataRow2 = sheet.createRow(2); + dataRow2.createCell(0).setCellValue("Category B"); + dataRow2.createCell(1).setCellValue(""); + dataRow2.createCell(2).setCellValue(30); + dataRow2.createCell(3).setCellValue(40); + + // Write to file + try (FileOutputStream fileOut = new FileOutputStream(file)) { + workbook.write(fileOut); + } + workbook.close(); + } + + /** + * Creates an Excel file with header not in the first row. + */ + public static void createOffsetHeaderExcelFile(File file) throws IOException { + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("Test Sheet"); + + // Add some empty rows and cells + sheet.createRow(0).createCell(0).setCellValue("This is not the header"); + sheet.createRow(1); // Empty row + + // Create header row at position 3 + Row headerRow = sheet.createRow(3); + headerRow.createCell(0).setCellValue("Category"); + headerRow.createCell(1).setCellValue("Subcategory"); + headerRow.createCell(2).setCellValue("Value1"); + headerRow.createCell(3).setCellValue("Value2"); + + // Create data rows + Row dataRow1 = sheet.createRow(4); + dataRow1.createCell(0).setCellValue("Category A"); + dataRow1.createCell(1).setCellValue(""); + dataRow1.createCell(2).setCellValue(10); + dataRow1.createCell(3).setCellValue(20); + + // Write to file + try (FileOutputStream fileOut = new FileOutputStream(file)) { + workbook.write(fileOut); + } + workbook.close(); + } + + /** + * Creates an Excel file with mixed data types and nested structure. + */ + public static void createMixedDataExcelFile(File file) throws IOException { + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("Test Sheet"); + + // Create header row + Row headerRow = sheet.createRow(0); + headerRow.createCell(0).setCellValue("Parent"); + headerRow.createCell(1).setCellValue("Child"); + headerRow.createCell(2).setCellValue("Value1"); + headerRow.createCell(3).setCellValue("Value2"); + + // Create data rows with nested structure + Row dataRow1 = sheet.createRow(1); + dataRow1.createCell(0).setCellValue("Parent"); + dataRow1.createCell(1).setCellValue("Child"); + dataRow1.createCell(2).setCellValue(30); + dataRow1.createCell(3).setCellValue(40); + + // Add some rows with different data types + Row dataRow2 = sheet.createRow(2); + dataRow2.createCell(0).setCellValue("Parent"); + dataRow2.createCell(1).setCellValue("Child2"); + dataRow2.createCell(2).setCellValue("Not a number"); // String instead of number + dataRow2.createCell(3).setCellValue(50); + + // Write to file + try (FileOutputStream fileOut = new FileOutputStream(file)) { + workbook.write(fileOut); + } + workbook.close(); + } +} diff --git a/src/test/java/io/jenkins/plugins/reporter/provider/RegenerateTestFiles.java b/src/test/java/io/jenkins/plugins/reporter/provider/RegenerateTestFiles.java new file mode 100644 index 00000000..81bb00f1 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/reporter/provider/RegenerateTestFiles.java @@ -0,0 +1,31 @@ +package io.jenkins.plugins.reporter.provider; + +import java.io.File; +import java.io.IOException; + +/** + * Temporary utility class to regenerate test Excel files. + * Run this class to update test files before running tests. + */ +public class RegenerateTestFiles { + + public static void main(String[] args) throws IOException { + // Temporary file to regenerate test data + File resourcesDir = new File("src/test/resources"); + if (!resourcesDir.exists()) { + resourcesDir.mkdirs(); + } + + System.out.println("Regenerating test Excel files..."); + + CreateExcelMultiSample.createConsistentHeadersExcelFile( + new File(resourcesDir, "test-excel-multi-consistent.xlsx")); + CreateExcelMultiSample.createInconsistentHeadersExcelFile( + new File(resourcesDir, "test-excel-multi-inconsistent.xlsx")); + CreateExcelMultiSample.createMixedValidityExcelFile( + new File(resourcesDir, "test-excel-multi-mixed.xlsx")); + + System.out.println("Test Excel files regenerated successfully!"); + } +} + diff --git a/src/test/resources/test-excel-mixed.xlsx b/src/test/resources/test-excel-mixed.xlsx new file mode 100644 index 00000000..8a522ac1 Binary files /dev/null and b/src/test/resources/test-excel-mixed.xlsx differ diff --git a/src/test/resources/test-excel-multi-consistent.xlsx b/src/test/resources/test-excel-multi-consistent.xlsx new file mode 100644 index 00000000..8ca5d849 Binary files /dev/null and b/src/test/resources/test-excel-multi-consistent.xlsx differ diff --git a/src/test/resources/test-excel-multi-inconsistent.xlsx b/src/test/resources/test-excel-multi-inconsistent.xlsx new file mode 100644 index 00000000..e4113032 Binary files /dev/null and b/src/test/resources/test-excel-multi-inconsistent.xlsx differ diff --git a/src/test/resources/test-excel-multi-mixed.xlsx b/src/test/resources/test-excel-multi-mixed.xlsx new file mode 100644 index 00000000..01d877b3 Binary files /dev/null and b/src/test/resources/test-excel-multi-mixed.xlsx differ diff --git a/src/test/resources/test-excel-normal.xlsx b/src/test/resources/test-excel-normal.xlsx new file mode 100644 index 00000000..0f3b6a59 Binary files /dev/null and b/src/test/resources/test-excel-normal.xlsx differ diff --git a/src/test/resources/test-excel-offset.xlsx b/src/test/resources/test-excel-offset.xlsx new file mode 100644 index 00000000..635ac393 Binary files /dev/null and b/src/test/resources/test-excel-offset.xlsx differ diff --git a/src/test/resources/test-excel.xlsx b/src/test/resources/test-excel.xlsx new file mode 100644 index 00000000..76873415 --- /dev/null +++ b/src/test/resources/test-excel.xlsx @@ -0,0 +1 @@ +PK