blob: 38018f3627dcc9cfd28529a9367e099947f366e4 [file] [log] [blame]
/* ====================================================================
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.hssf.record.aggregates;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.zip.CRC32;
import org.apache.poi.hssf.HSSFTestDataSamples;
import org.apache.poi.hssf.model.RecordStream;
import org.apache.poi.hssf.model.RowBlocksReader;
import org.apache.poi.hssf.record.BlankRecord;
import org.apache.poi.hssf.record.CellValueRecordInterface;
import org.apache.poi.hssf.record.FormulaRecord;
import org.apache.poi.hssf.record.MulBlankRecord;
import org.apache.poi.hssf.record.Record;
import org.apache.poi.hssf.record.SharedFormulaRecord;
import org.apache.poi.hssf.record.WindowTwoRecord;
import org.apache.poi.hssf.record.aggregates.RecordAggregate.RecordVisitor;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.util.HexRead;
import org.junit.jupiter.api.Test;
/**
* Tests for {@link ValueRecordsAggregate}
*/
final class TestValueRecordsAggregate {
private static final String ABNORMAL_SHARED_FORMULA_FLAG_TEST_FILE = "AbnormalSharedFormulaFlag.xls";
private final ValueRecordsAggregate valueRecord = new ValueRecordsAggregate();
private List<CellValueRecordInterface> getValueRecords() {
List<CellValueRecordInterface> list = new ArrayList<>();
for ( CellValueRecordInterface rec : valueRecord ) {
list.add(rec);
}
return Collections.unmodifiableList(list);
}
/**
* Make sure the shared formula DOESNT makes it to the FormulaRecordAggregate when being parsed
* as part of the value records
*/
@Test
void testSharedFormula() {
List<org.apache.poi.hssf.record.Record> records = new ArrayList<>();
records.add(new FormulaRecord());
records.add(new SharedFormulaRecord());
records.add(new WindowTwoRecord());
constructValueRecord(records);
List<CellValueRecordInterface> cvrs = getValueRecords();
//Ensure that the SharedFormulaRecord has been converted
assertEquals(1, cvrs.size());
CellValueRecordInterface record = cvrs.get(0);
assertNotNull( record, "Row contains a value" );
assertTrue( ( record instanceof FormulaRecordAggregate ), "First record is a FormulaRecordsAggregate");
}
private void constructValueRecord(List<org.apache.poi.hssf.record.Record> records) {
RowBlocksReader rbr = new RowBlocksReader(new RecordStream(records, 0));
SharedValueManager sfrh = rbr.getSharedFormulaManager();
RecordStream rs = rbr.getPlainRecordStream();
while(rs.hasNext()) {
Record rec = rs.getNext();
valueRecord.construct((CellValueRecordInterface)rec, rs, sfrh);
}
}
private static List<org.apache.poi.hssf.record.Record> testData() {
List<org.apache.poi.hssf.record.Record> records = new ArrayList<>();
FormulaRecord formulaRecord = new FormulaRecord();
BlankRecord blankRecord = new BlankRecord();
formulaRecord.setRow(1);
formulaRecord.setColumn((short) 1);
blankRecord.setRow(2);
blankRecord.setColumn((short) 2);
records.add(formulaRecord);
records.add(blankRecord);
records.add(new WindowTwoRecord());
return records;
}
@Test
void testInsertCell() {
assertEquals(0, getValueRecords().size());
BlankRecord blankRecord = newBlankRecord();
valueRecord.insertCell( blankRecord );
assertEquals(1, getValueRecords().size());
}
@Test
void testRemoveCell() {
BlankRecord blankRecord1 = newBlankRecord();
valueRecord.insertCell( blankRecord1 );
BlankRecord blankRecord2 = newBlankRecord();
valueRecord.removeCell( blankRecord2 );
assertEquals(0, getValueRecords().size());
// removing an already empty cell just falls through
valueRecord.removeCell( blankRecord2 );
}
@Test
void testGetPhysicalNumberOfCells() {
assertEquals(0, valueRecord.getPhysicalNumberOfCells());
BlankRecord blankRecord1 = newBlankRecord();
valueRecord.insertCell( blankRecord1 );
assertEquals(1, valueRecord.getPhysicalNumberOfCells());
valueRecord.removeCell( blankRecord1 );
assertEquals(0, valueRecord.getPhysicalNumberOfCells());
}
@Test
void testGetFirstCellNum() {
assertEquals( -1, valueRecord.getFirstCellNum() );
valueRecord.insertCell( newBlankRecord( 2, 2 ) );
assertEquals( 2, valueRecord.getFirstCellNum() );
valueRecord.insertCell( newBlankRecord( 3, 3 ) );
assertEquals( 2, valueRecord.getFirstCellNum() );
// Note: Removal doesn't currently reset the first column. It probably should but it doesn't.
valueRecord.removeCell( newBlankRecord( 2, 2 ) );
assertEquals( 2, valueRecord.getFirstCellNum() );
}
@Test
void testGetLastCellNum() {
assertEquals( -1, valueRecord.getLastCellNum() );
valueRecord.insertCell( newBlankRecord( 2, 2 ) );
assertEquals( 2, valueRecord.getLastCellNum() );
valueRecord.insertCell( newBlankRecord( 3, 3 ) );
assertEquals( 3, valueRecord.getLastCellNum() );
// Note: Removal doesn't currently reset the last column. It probably should but it doesn't.
valueRecord.removeCell( newBlankRecord( 3, 3 ) );
assertEquals( 3, valueRecord.getLastCellNum() );
}
private static final class SerializerVisitor implements RecordVisitor {
private final byte[] _buf;
private int _writeIndex;
public SerializerVisitor(byte[] buf) {
_buf = buf;
_writeIndex = 0;
}
@Override
public void visitRecord(org.apache.poi.hssf.record.Record r) {
r.serialize(_writeIndex, _buf);
_writeIndex += r.getRecordSize();
}
public int getWriteIndex() {
return _writeIndex;
}
}
@Test
void testSerialize() {
byte[] expectedArray = HexRead.readFromString(""
+ "06 00 16 00 " // Formula
+ "01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 "
+ "01 02 06 00 " // Blank
+ "02 00 02 00 00 00");
byte[] actualArray = new byte[expectedArray.length];
List<org.apache.poi.hssf.record.Record> records = testData();
constructValueRecord(records);
SerializerVisitor sv = new SerializerVisitor(actualArray);
valueRecord.visitCellsForRow(1, sv);
valueRecord.visitCellsForRow(2, sv);
assertEquals(actualArray.length, sv.getWriteIndex());
assertArrayEquals(expectedArray, actualArray);
}
private static BlankRecord newBlankRecord() {
return newBlankRecord( 2, 2 );
}
private static BlankRecord newBlankRecord(int col, int row) {
BlankRecord blankRecord = new BlankRecord();
blankRecord.setRow( row );
blankRecord.setColumn( (short) col );
return blankRecord;
}
/**
* Sometimes the 'shared formula' flag (<tt>FormulaRecord.isSharedFormula()</tt>) is set when
* there is no corresponding SharedFormulaRecord available. SharedFormulaRecord definitions do
* not span multiple sheets. They are are only defined within a sheet, and thus they do not
* have a sheet index field (only row and column range fields).<br>
* So it is important that the code which locates the SharedFormulaRecord for each
* FormulaRecord does not allow matches across sheets.</br>
*
* Prior to bugzilla 44449 (Feb 2008), POI <tt>ValueRecordsAggregate.construct(int, List)</tt>
* allowed <tt>SharedFormulaRecord</tt>s to be erroneously used across sheets. That incorrect
* behaviour is shown by this test.<p>
*
* <b>Notes on how to produce the test spreadsheet</b>:</p>
* The setup for this test (AbnormalSharedFormulaFlag.xls) is rather fragile, insomuchas
* re-saving the file (either with Excel or POI) clears the flag.<br>
* <ol>
* <li>A new spreadsheet was created in Excel (File | New | Blank Workbook).</li>
* <li>Sheet3 was deleted.</li>
* <li>Sheet2!A1 formula was set to '="second formula"', and fill-dragged through A1:A8.</li>
* <li>Sheet1!A1 formula was set to '="first formula"', and also fill-dragged through A1:A8.</li>
* <li>Four rows on Sheet1 "5" through "8" were deleted ('delete rows' alt-E D, not 'clear' Del).</li>
* <li>The spreadsheet was saved as AbnormalSharedFormulaFlag.xls.</li>
* </ol>
* Prior to the row delete action the spreadsheet has two <tt>SharedFormulaRecord</tt>s. One
* for each sheet. To expose the bug, the shared formulas have been made to overlap.<br>
* The row delete action (as described here) seems to to delete the
* <tt>SharedFormulaRecord</tt> from Sheet1 (but not clear the 'shared formula' flags.<br>
* There are other variations on this theme to create the same effect.
*
*/
@Test
void testSpuriousSharedFormulaFlag() throws Exception {
long actualCRC = getFileCRC(HSSFTestDataSamples.openSampleFileStream(ABNORMAL_SHARED_FORMULA_FLAG_TEST_FILE));
long expectedCRC = 2277445406L;
if(actualCRC != expectedCRC) {
System.err.println("Expected crc " + expectedCRC + " but got " + actualCRC);
throw failUnexpectedTestFileChange();
}
HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook(ABNORMAL_SHARED_FORMULA_FLAG_TEST_FILE);
HSSFSheet s = wb.getSheetAt(0); // Sheet1
String cellFormula;
cellFormula = getFormulaFromFirstCell(s, 0); // row "1"
// the problem is not observable in the first row of the shared formula
assertEquals("\"first formula\"", cellFormula, "Something else wrong with this test case");
// but the problem is observable in rows 2,3,4
cellFormula = getFormulaFromFirstCell(s, 1); // row "2"
assertNotEquals("\"second formula\"", cellFormula, "found bug 44449 (Wrong SharedFormulaRecord was used).");
assertEquals("\"first formula\"", cellFormula, "Something else wrong with this test case");
wb.close();
}
private static String getFormulaFromFirstCell(HSSFSheet s, int rowIx) {
return s.getRow(rowIx).getCell(0).getCellFormula();
}
/**
* If someone opened this particular test file in Excel and saved it, the peculiar condition
* which causes the target bug would probably disappear. This test would then just succeed
* regardless of whether the fix was present. So a CRC check is performed to make it less easy
* for that to occur.
*/
private static RuntimeException failUnexpectedTestFileChange() {
String msg = "Test file '" + ABNORMAL_SHARED_FORMULA_FLAG_TEST_FILE + "' has changed. "
+ "This junit may not be properly testing for the target bug. "
+ "Either revert the test file or ensure that the new version "
+ "has the right characteristics to test the target bug.";
// A breakpoint in ValueRecordsAggregate.handleMissingSharedFormulaRecord(FormulaRecord)
// should get hit during parsing of Sheet1.
// If the test spreadsheet is created as directed, this condition should occur.
// It is easy to upset the test spreadsheet (for example re-saving will destroy the
// peculiar condition we are testing for).
throw new RuntimeException(msg);
}
/**
* gets a CRC checksum for the content of a file
*/
private static long getFileCRC(InputStream is) {
CRC32 crc = new CRC32();
byte[] buf = new byte[2048];
try {
while(true) {
int bytesRead = is.read(buf);
if(bytesRead < 1) {
break;
}
crc.update(buf, 0, bytesRead);
}
is.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
return crc.getValue();
}
@Test
void testRemoveNewRow_bug46312() throws IOException {
// To make bug occur, rowIndex needs to be >= ValueRecordsAggregate.records.length
int rowIndex = 30;
ValueRecordsAggregate vra = new ValueRecordsAggregate();
// bug 46312 - Specified rowIndex 30 is outside the allowable range (0..30)
assertDoesNotThrow(() -> vra.removeAllCellsValuesForRow(rowIndex));
// same bug as demonstrated through usermodel API
try (HSSFWorkbook wb = new HSSFWorkbook()) {
HSSFSheet sheet = wb.createSheet();
HSSFRow row = sheet.createRow(rowIndex);
// must not add any cells to the new row if we want to see the bug
// row.createCell(0); // this causes ValueRecordsAggregate.records to auto-extend
assertDoesNotThrow(() -> sheet.createRow(rowIndex));
}
}
/**
* Tests various manipulations of blank cells, to make sure that {@link MulBlankRecord}s
* are use appropriately
*/
@Test
void testMultipleBlanks() {
BlankRecord brA2 = newBlankRecord(0, 1);
BlankRecord brB2 = newBlankRecord(1, 1);
BlankRecord brC2 = newBlankRecord(2, 1);
BlankRecord brD2 = newBlankRecord(3, 1);
BlankRecord brE2 = newBlankRecord(4, 1);
BlankRecord brB3 = newBlankRecord(1, 2);
BlankRecord brC3 = newBlankRecord(2, 2);
valueRecord.insertCell(brA2);
valueRecord.insertCell(brB2);
valueRecord.insertCell(brD2);
confirmMulBlank(3, 1, 1);
valueRecord.insertCell(brC3);
confirmMulBlank(4, 1, 2);
valueRecord.insertCell(brB3);
valueRecord.insertCell(brE2);
confirmMulBlank(6, 3, 0);
valueRecord.insertCell(brC2);
confirmMulBlank(7, 2, 0);
valueRecord.removeCell(brA2);
confirmMulBlank(6, 2, 0);
valueRecord.removeCell(brC2);
confirmMulBlank(5, 2, 1);
valueRecord.removeCell(brC3);
confirmMulBlank(4, 1, 2);
}
private void confirmMulBlank(int expectedTotalBlankCells,
int expectedNumberOfMulBlankRecords, int expectedNumberOfSingleBlankRecords) {
// assumed row ranges set-up by caller:
final int firstRow = 1;
final int lastRow = 2;
final class BlankStats {
public int countBlankCells;
public int countMulBlankRecords;
public int countSingleBlankRecords;
}
final BlankStats bs = new BlankStats();
RecordVisitor rv = r -> {
if (r instanceof MulBlankRecord) {
MulBlankRecord mbr = (MulBlankRecord) r;
bs.countMulBlankRecords++;
bs.countBlankCells += mbr.getNumColumns();
} else if (r instanceof BlankRecord) {
bs.countSingleBlankRecords++;
bs.countBlankCells++;
}
};
for (int rowIx = firstRow; rowIx <=lastRow; rowIx++) {
if (valueRecord.rowHasCells(rowIx)) {
valueRecord.visitCellsForRow(rowIx, rv);
}
}
assertEquals(expectedTotalBlankCells, bs.countBlankCells);
assertEquals(expectedNumberOfMulBlankRecords, bs.countMulBlankRecords);
assertEquals(expectedNumberOfSingleBlankRecords, bs.countSingleBlankRecords);
}
}