/* ====================================================================
   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 static org.junit.jupiter.api.Assertions.assertNull;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

/**
 * 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
    void tearDown() {
        verifier.assertGarbageCollected();
    }

    {@literal}Test
    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.
 */
public class MemoryLeakVerifier {
    private static final int MAX_GC_ITERATIONS = 50;
    private static final int GC_SLEEP_TIME     = 100;

    private final List<WeakReference<Object>> 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<Object> ref : references) {
                assertGarbageCollected(ref, maxIterations);
            }
        } catch (InterruptedException e) {
            // just ensure that we quickly return when the thread is interrupted
        }
    }

    @SuppressForbidden("Use finalization as long as it is available here and remove it when the JDK stops providing it")
    private static void assertGarbageCollected(WeakReference<Object> 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(ref.get(), "Object should not exist after " + MAX_GC_ITERATIONS + " collections, but still had: " + ref.get());
    }
}
