TestAutoSizeColumnTracker.java

/* ====================================================================
   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
==================================================================== */
package org.apache.poi.xssf.streaming;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.SheetUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;


/**
 * Tests the auto-sizing behaviour of {@link SXSSFSheet} when not all
 * rows fit into the memory window size etc.
 * <p>
 * see Bug #57450 which reported the original misbehaviour
 */
class TestAutoSizeColumnTracker {
    private SXSSFSheet sheet;
    private SXSSFWorkbook workbook;
    private AutoSizeColumnTracker tracker;
    private static final SortedSet<Integer> columns;
    static {
        SortedSet<Integer>_columns = new TreeSet<>();
        _columns.add(0);
        _columns.add(1);
        _columns.add(3);
        columns = Collections.unmodifiableSortedSet(_columns);
    }
    private static final String SHORT_MESSAGE = "short";
    private static final String LONG_MESSAGE = "This is a test of a long message! This is a test of a long message!";

    @BeforeEach
    void setUpSheetAndWorkbook() {
        workbook = new SXSSFWorkbook();
        sheet = workbook.createSheet();
        tracker = new AutoSizeColumnTracker(sheet);
    }

    @AfterEach
    void tearDownSheetAndWorkbook() throws IOException {
        if (sheet != null) {
            sheet.dispose();
        }
        if (workbook != null) {
            workbook.close();
        }
    }

    @Test
    void trackAndUntrackColumn() {
        assumeTrue(tracker.getTrackedColumns().isEmpty());
        tracker.trackColumn(0);
        Set<Integer> expected = new HashSet<>();
        expected.add(0);
        assertEquals(expected, tracker.getTrackedColumns());
        tracker.untrackColumn(0);
        assertTrue(tracker.getTrackedColumns().isEmpty());
    }

    @Test
    void trackAndUntrackColumns() {
        assumeTrue(tracker.getTrackedColumns().isEmpty());
        tracker.trackColumns(columns);
        assertEquals(columns, tracker.getTrackedColumns());
        tracker.untrackColumn(3);
        tracker.untrackColumn(0);
        tracker.untrackColumn(1);
        assertTrue(tracker.getTrackedColumns().isEmpty());
        tracker.trackColumn(0);
        tracker.trackColumns(columns);
        tracker.untrackColumn(4);
        assertEquals(columns, tracker.getTrackedColumns());
        tracker.untrackColumns(columns);
        assertTrue(tracker.getTrackedColumns().isEmpty());
    }

    @Test
    void trackAndUntrackAllColumns() {
        createColumnsAndTrackThemAll();
        tracker.untrackAllColumns();
        assertTrue(tracker.getTrackedColumns().isEmpty());
    }

    @Test
    void isColumnTracked() {
        assumeFalse(tracker.isColumnTracked(0));
        tracker.trackColumn(0);
        assertTrue(tracker.isColumnTracked(0));
        tracker.untrackColumn(0);
        assertFalse(tracker.isColumnTracked(0));
    }

    @Test
    void isColumnTrackedAndTrackAllColumns() {
        createColumnsAndTrackThemAll();
        tracker.untrackColumn(0);
        SortedSet<Integer> _newColumns = new TreeSet<>();
        _newColumns.add(1);
        _newColumns.add(3);
        SortedSet<Integer> newColumns = Collections.unmodifiableSortedSet(_newColumns);
        assertEquals(newColumns, tracker.getTrackedColumns());
        assertFalse(tracker.isColumnTracked(0));
    }

    @Test
    void getTrackedColumns() {
        assumeTrue(tracker.getTrackedColumns().isEmpty());

        for (int column : columns) {
            tracker.trackColumn(column);
        }

        assertEquals(3, tracker.getTrackedColumns().size());
        assertEquals(columns, tracker.getTrackedColumns());
    }

    @Test
    void isAllColumnsTracked() {
        assertFalse(tracker.isAllColumnsTracked());
        tracker.trackAllColumns();
        assertTrue(tracker.isAllColumnsTracked());
        tracker.untrackAllColumns();
        assertFalse(tracker.isAllColumnsTracked());
    }

    @Test
    void updateColumnWidths_and_getBestFitColumnWidth() {
        tracker.trackAllColumns();
        Row row1 = sheet.createRow(0);
        Row row2 = sheet.createRow(1);
        // A1, B1, D1
        for (int column : columns) {
            row1.createCell(column).setCellValue(LONG_MESSAGE);
            row2.createCell(column+1).setCellValue(SHORT_MESSAGE);
        }
        tracker.updateColumnWidths(row1);
        tracker.updateColumnWidths(row2);
        assertEquals(0, sheet.addMergedRegion(CellRangeAddress.valueOf("D1:E1")));

        assumeRequiredFontsAreInstalled(workbook, row1.getCell(columns.iterator().next()));

        // Excel 2013 and LibreOffice 4.2.8.2 both treat columns with merged regions as blank
        /*     A     B    C      D   E
         * 1 LONG  LONG        LONGMERGE
         * 2       SHORT SHORT     SHORT
         */

        // measured in Excel 2013. Sizes may vary.
        final int longMsgWidth = (int) (57.43*256);
        final int shortMsgWidth = (int) (4.86*256);

        checkColumnWidth(longMsgWidth, 0, true);
        checkColumnWidth(longMsgWidth, 0, false);
        checkColumnWidth(longMsgWidth, 1, true);
        checkColumnWidth(longMsgWidth, 1, false);
        checkColumnWidth(shortMsgWidth, 2, true);
        checkColumnWidth(shortMsgWidth, 2, false);
        checkColumnWidth(-1, 3, true);
        checkColumnWidth(longMsgWidth, 3, false);
        checkColumnWidth(shortMsgWidth, 4, true); //but is it really? shouldn't autosizing column E use "" from E1 and SHORT from E2?
        checkColumnWidth(shortMsgWidth, 4, false);
    }

    private void checkColumnWidth(int expectedWidth, int column, boolean useMergedCells) {
        final int bestFitWidth = tracker.getBestFitColumnWidth(column, useMergedCells);
        if (bestFitWidth < 0 && expectedWidth < 0) return;
        final double abs_error = Math.abs(bestFitWidth-expectedWidth);
        final double rel_error = abs_error / expectedWidth;
        assertTrue(rel_error <= 0.25, "check column width: " + rel_error + ", " + abs_error + ", " + expectedWidth + ", " + bestFitWidth);
    }

    private static void assumeRequiredFontsAreInstalled(final Workbook workbook, final Cell cell) {
        // autoSize will fail if required fonts are not installed, skip this test then
        Font font = workbook.getFontAt(cell.getCellStyle().getFontIndex());
        assumeTrue(SheetUtil.canComputeColumnWidth(font),
            "Cannot verify autoSizeColumn() because the necessary Fonts are not installed on this machine: " + font);
    }

    private void createColumnsAndTrackThemAll() {
        assumeTrue(tracker.getTrackedColumns().isEmpty());
        tracker.trackAllColumns();
        assertTrue(tracker.getTrackedColumns().isEmpty());

        Row row = sheet.createRow(0);
        for (int column : columns) {
            row.createCell(column);
        }
        // implicitly track the columns
        tracker.updateColumnWidths(row);
        assertEquals(columns, tracker.getTrackedColumns());
    }
}