diff --git a/src/ooxml/testcases/org/apache/poi/xssf/XSSFMemoryLeakTests.java b/src/ooxml/testcases/org/apache/poi/xssf/XSSFMemoryLeakTests.java new file mode 100644 index 0000000000..958e54554c --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xssf/XSSFMemoryLeakTests.java @@ -0,0 +1,144 @@ +/* ==================================================================== + 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; + +import org.apache.poi.util.MemoryLeakVerifier; +import org.apache.poi.xssf.usermodel.XSSFCell; +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.After; +import org.junit.Test; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCell; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertSame; + +/** + * A test which uses {@link MemoryLeakVerifier} to ensure that certain + * objects are not left over in memory after the test. + * + * E.g. verifies that objects are freed when stuff is removed from sheets or rows + */ +public class XSSFMemoryLeakTests { + private final MemoryLeakVerifier verifier = new MemoryLeakVerifier(); + + // keep some items in memory, so checks in tearDown() actually + // verify that they do not keep certain objects in memory, + // e.g. nested CT... objects which should be released + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private List references = new ArrayList<>(); + + @After + public void tearDown() { + verifier.assertGarbageCollected(); + } + + @Test + public void testWriteRow() throws IOException { + final XSSFWorkbook wb = new XSSFWorkbook(); + final XSSFSheet sheet1 = wb.createSheet("Sheet1"); + final XSSFRow row = sheet1.createRow(0); + final XSSFCell cell = row.createCell(0); + cell.setCellValue("hello"); + + // Cannot check the CTCell here as it is reused now and thus + // not freed until we free up the Cell itself + //verifier.addObject(ctCell); + + try (OutputStream out = new ByteArrayOutputStream(8192)) { + wb.write(out); + } + + CTCell ctCell = cell.getCTCell(); + assertSame("The CTCell should not be replaced", + cell.getCTCell(), ctCell); + assertSame("The CTCell in the row should not be replaced", + row.getCTRow().getCArray(0), ctCell); + + wb.close(); + } + + @Test + public void testRemoveCellFromRow() throws IOException { + final XSSFWorkbook wb = new XSSFWorkbook(); + final XSSFSheet sheet1 = wb.createSheet("Sheet1"); + final XSSFRow rowToCheck = sheet1.createRow(0); + references.add(rowToCheck); + + XSSFCell cell = rowToCheck.createCell(0); + cell.setCellValue("hello"); + + // previously the CTCell was still referenced in the CTRow, verify that it is freed + verifier.addObject(cell); + verifier.addObject(cell.getCTCell()); + + rowToCheck.removeCell(cell); + + wb.close(); + } + + @Test + public void testRemove2CellsFromRow() throws IOException { + final XSSFWorkbook wb = new XSSFWorkbook(); + final XSSFSheet sheet1 = wb.createSheet("Sheet1"); + final XSSFRow rowToCheck = sheet1.createRow(0); + references.add(rowToCheck); + + XSSFCell cell1 = rowToCheck.createCell(0); + cell1.setCellValue("hello"); + XSSFCell cell2 = rowToCheck.createCell(1); + cell2.setCellValue("world"); + + // previously the CTCell was still referenced in the CTRow, verify that it is freed + verifier.addObject(cell1); + verifier.addObject(cell1.getCTCell()); + verifier.addObject(cell2); + verifier.addObject(cell2.getCTCell()); + + rowToCheck.removeCell(cell2); + rowToCheck.removeCell(cell1); + + wb.close(); + } + + @Test + public void testRemoveRowFromSheet() throws IOException { + final XSSFWorkbook wb1 = new XSSFWorkbook(); + final XSSFSheet sheetToCheck = wb1.createSheet("Sheet1"); + references.add(sheetToCheck); + final XSSFRow row = sheetToCheck.createRow(0); + final XSSFCell cell = row.createCell(0); + cell.setCellValue(1); + + // ensure that the row-data is not kept somewhere in another member + verifier.addObject(row.getCTRow()); + verifier.addObject(row); + verifier.addObject(cell.getCTCell()); + verifier.addObject(cell); + + sheetToCheck.removeRow(row); + + wb1.close(); + } +} diff --git a/src/testcases/org/apache/poi/util/MemoryLeakVerifier.java b/src/testcases/org/apache/poi/util/MemoryLeakVerifier.java new file mode 100644 index 0000000000..c69baa7900 --- /dev/null +++ b/src/testcases/org/apache/poi/util/MemoryLeakVerifier.java @@ -0,0 +1,106 @@ +/* ==================================================================== + 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.util; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertNull; + +/** + * A simple utility class that can verify that objects have been successfully garbage collected. + * + * Usage is something like + * + * private final MemoryLeakVerifier verifier = new MemoryLeakVerifier(); + + {@literal}After + public void tearDown() { + verifier.assertGarbageCollected(); + } + + {@literal}Test + public void someTest() { + ... + verifier.addObject(object); + } + + * + * This will verify at the end of the test if the object is actually removed by the + * garbage collector or if it lingers in memory for some reason. + * + * Idea taken from http://stackoverflow.com/a/7410460/411846 + */ +public class MemoryLeakVerifier { + private static final int MAX_GC_ITERATIONS = 50; + private static final int GC_SLEEP_TIME = 100; + + private final List> references = new ArrayList<>(); + + public MemoryLeakVerifier() { + } + + public void addObject(Object object) { + references.add(new WeakReference<>(object)); + } + + /** + * Attempts to perform a full garbage collection so that all weak references will be removed. Usually only + * a single GC is required, but there have been situations where some unused memory is not cleared up on the + * first pass. This method performs a full garbage collection and then validates that the weak reference + * now has been cleared. If it hasn't then the thread will sleep for 100 milliseconds and then retry up to + * 50 more times. If after this the object still has not been collected then the assertion will fail. + * + * Based upon the method described in: http://www.javaworld.com/javaworld/javatips/jw-javatip130.html + */ + public void assertGarbageCollected() { + assertGarbageCollected(MAX_GC_ITERATIONS); + } + + /** + * Used only for testing the class itself where we would like to fail faster than 5 seconds + * @param maxIterations The number of times a GC will be invoked until a possible memory leak is reported + */ + void assertGarbageCollected(int maxIterations) { + try { + for(WeakReference ref : references) { + assertGarbageCollected(ref, maxIterations); + } + } catch (InterruptedException e) { + // just ensure that we quickly return when the thread is interrupted + } + } + + private static void assertGarbageCollected(WeakReference ref, int maxIterations) throws InterruptedException { + Runtime runtime = Runtime.getRuntime(); + for (int i = 0; i < maxIterations; i++) { + runtime.runFinalization(); + runtime.gc(); + if (ref.get() == null) + break; + + // Pause for a while and then go back around the loop to try again... + //EventQueue.invokeAndWait(Procedure.NoOp); // Wait for the AWT event queue to have completed processing + Thread.sleep(GC_SLEEP_TIME); + } + + assertNull("Object should not exist after " + MAX_GC_ITERATIONS + " collections, but still had: " + ref.get(), + ref.get()); + } +}