/*
 * Copyright (C) 2011 The Guava Authors
 *
 * Licensed 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 com.google.common.testing;

import static com.google.common.truth.Truth.assertThat;

import com.google.common.testing.GcFinalization.FinalizationPredicate;
import com.google.common.util.concurrent.SettableFuture;
import java.lang.ref.WeakReference;
import java.util.WeakHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import junit.framework.TestCase;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * Tests for {@link GcFinalization}.
 *
 * @author Martin Buchholz
 * @author mike nonemacher
 */
@AndroidIncompatible // depends on details of gc

public class GcFinalizationTest extends TestCase {

  // ----------------------------------------------------------------
  // Ordinary tests of successful method execution
  // ----------------------------------------------------------------

  public void testAwait_CountDownLatch() {
    final CountDownLatch latch = new CountDownLatch(1);
    Object x =
        new Object() {
          @Override
          protected void finalize() {
            latch.countDown();
          }
        };
    x = null; // Hint to the JIT that x is unreachable
    GcFinalization.await(latch);
    assertEquals(0, latch.getCount());
  }

  public void testAwaitDone_Future() {
    final SettableFuture<@Nullable Void> future = SettableFuture.create();
    Object x =
        new Object() {
          @Override
          protected void finalize() {
            future.set(null);
          }
        };
    x = null; // Hint to the JIT that x is unreachable
    GcFinalization.awaitDone(future);
    assertTrue(future.isDone());
    assertFalse(future.isCancelled());
  }

  public void testAwaitDone_Future_Cancel() {
    final SettableFuture<@Nullable Void> future = SettableFuture.create();
    Object x =
        new Object() {
          @Override
          protected void finalize() {
            future.cancel(false);
          }
        };
    x = null; // Hint to the JIT that x is unreachable
    GcFinalization.awaitDone(future);
    assertTrue(future.isDone());
    assertTrue(future.isCancelled());
  }

  public void testAwaitClear() {
    final WeakReference<Object> ref = new WeakReference<>(new Object());
    GcFinalization.awaitClear(ref);
    assertNull(ref.get());
  }

  public void testAwaitDone_FinalizationPredicate() {
    final WeakHashMap<Object, Object> map = new WeakHashMap<>();
    map.put(new Object(), Boolean.TRUE);
    GcFinalization.awaitDone(
        new FinalizationPredicate() {
          @Override
          public boolean isDone() {
            return map.isEmpty();
          }
        });
    assertTrue(map.isEmpty());
  }

  // ----------------------------------------------------------------
  // Test that interrupts result in RuntimeException, not InterruptedException.
  // Trickier than it looks, because runFinalization swallows interrupts.
  // ----------------------------------------------------------------

  class Interruptenator extends Thread {
    final AtomicBoolean shutdown;

    Interruptenator(final Thread interruptee) {
      this(interruptee, new AtomicBoolean(false));
    }

    Interruptenator(final Thread interruptee, final AtomicBoolean shutdown) {
      super(
          new Runnable() {
            @Override
            public void run() {
              while (!shutdown.get()) {
                interruptee.interrupt();
                Thread.yield();
              }
            }
          });
      this.shutdown = shutdown;
      start();
    }

    void shutdown() {
      shutdown.set(true);
      while (this.isAlive()) {
        Thread.yield();
      }
    }
  }

  void assertWrapsInterruptedException(RuntimeException e) {
    assertThat(e).hasMessageThat().contains("Unexpected interrupt");
    assertThat(e).hasCauseThat().isInstanceOf(InterruptedException.class);
  }

  public void testAwait_CountDownLatch_Interrupted() {
    Interruptenator interruptenator = new Interruptenator(Thread.currentThread());
    try {
      final CountDownLatch latch = new CountDownLatch(1);
      try {
        GcFinalization.await(latch);
        fail("should throw");
      } catch (RuntimeException expected) {
        assertWrapsInterruptedException(expected);
      }
    } finally {
      interruptenator.shutdown();
      Thread.interrupted();
    }
  }

  public void testAwaitDone_Future_Interrupted_Interrupted() {
    Interruptenator interruptenator = new Interruptenator(Thread.currentThread());
    try {
      final SettableFuture<@Nullable Void> future = SettableFuture.create();
      try {
        GcFinalization.awaitDone(future);
        fail("should throw");
      } catch (RuntimeException expected) {
        assertWrapsInterruptedException(expected);
      }
    } finally {
      interruptenator.shutdown();
      Thread.interrupted();
    }
  }

  public void testAwaitClear_Interrupted() {
    Interruptenator interruptenator = new Interruptenator(Thread.currentThread());
    try {
      final WeakReference<Object> ref = new WeakReference<Object>(Boolean.TRUE);
      try {
        GcFinalization.awaitClear(ref);
        fail("should throw");
      } catch (RuntimeException expected) {
        assertWrapsInterruptedException(expected);
      }
    } finally {
      interruptenator.shutdown();
      Thread.interrupted();
    }
  }

  public void testAwaitDone_FinalizationPredicate_Interrupted() {
    Interruptenator interruptenator = new Interruptenator(Thread.currentThread());
    try {
      try {
        GcFinalization.awaitDone(
            new FinalizationPredicate() {
              @Override
              public boolean isDone() {
                return false;
              }
            });
        fail("should throw");
      } catch (RuntimeException expected) {
        assertWrapsInterruptedException(expected);
      }
    } finally {
      interruptenator.shutdown();
      Thread.interrupted();
    }
  }

  /**
   * awaitFullGc() is not quite as reliable a way to ensure calling of a specific finalize method as
   * the more direct await* methods, but should be reliable enough in practice to avoid flakiness of
   * this test. (And if it isn't, we'd like to know about it first!)
   */
  public void testAwaitFullGc() {
    final CountDownLatch finalizerRan = new CountDownLatch(1);
    final WeakReference<Object> ref =
        new WeakReference<Object>(
            new Object() {
              @Override
              protected void finalize() {
                finalizerRan.countDown();
              }
            });

    // Don't copy this into your own test!
    // Use e.g. awaitClear or await(CountDownLatch) instead.
    GcFinalization.awaitFullGc();

    // If this test turns out to be flaky, add a second call to awaitFullGc()
    // GcFinalization.awaitFullGc();

    assertEquals(0, finalizerRan.getCount());
    assertNull(ref.get());
  }
}
